Skip to content

Expose stream auto-compaction checkpoint options #65

@anasibnanwar1-droid

Description

@anasibnanwar1-droid

Problem

Clients cannot implement safe Codex-style mid-task auto-compaction from above session.stream().

Current SDK shape:

  • MessageOptions only exposes images, files, outputFormat, includePartialMessages, and abortSignal:
    export interface MessageOptions {
    images?: Base64ImageSource[];
    files?: DocumentSource[];
    outputFormat?: OutputFormat;
    includePartialMessages?: boolean;
    abortSignal?: AbortSignal;
    }
  • streamFromClient() sends one client.addUserMessage(...), then yields bridge messages until the daemon reports completion:
    export async function* streamFromClient(
    client: StreamableClient,
    sessionId: string,
    activeBridges: Set<MessageBridge>,
    prompt: string,
    options?: MessageOptions
    ): AsyncGenerator<DroidStreamEvent, void, undefined> {
    throwIfAborted(options?.abortSignal);
    const startedAt = Date.now();
    let resolveDone: () => void = () => {};
    const donePromise = new Promise<void>((resolve) => {
    resolveDone = resolve;
    });
    const bridge = new MessageBridge(resolveDone, {
    includePartialMessages: options?.includePartialMessages,
    sessionId,
    startedAt,
    outputFormat: options?.outputFormat,
    });
    activeBridges.add(bridge);
    const unsubscribe = client.onNotification(bridge.notificationHandler);
    let resolveAbort: () => void = () => {};
    const abortPromise = new Promise<void>((resolve) => {
    resolveAbort = resolve;
    });
    const cleanupAbortSignal = wireAbortSignal(options?.abortSignal, () => {
    bridge.signalDone();
    resolveAbort();
    void client.interruptSession().catch(() => {});
    });
    try {
    await Promise.race([
    client.addUserMessage({
    text: prompt,
    images: options?.images,
    files: options?.files,
    outputFormat: options?.outputFormat,
    }),
    donePromise,
    abortPromise,
    ]);
    throwIfAborted(options?.abortSignal);
    for await (const msg of bridge.messages()) {
    throwIfAborted(options?.abortSignal);
    yield msg;
    }
    throwIfAborted(options?.abortSignal);
    } finally {
    cleanupAbortSignal();
    unsubscribe();
    activeBridges.delete(bridge);
    }
    }
  • DaemonSession.stream() and DroidSession.stream() delegate directly to streamFromClient():
    async *stream(
    prompt: string,
    options?: MessageOptions
    ): AsyncGenerator<DroidStreamEvent, void, undefined> {
    this._ensureNotClosed();
    yield* streamFromClient(
    this._client,
    this._sessionId,
    this._activeBridges,
    prompt,
    options
    );

    async *stream(
    prompt: string,
    options?: MessageOptions
    ): AsyncGenerator<DroidStreamEvent, void, undefined> {
    this._ensureNotClosed();
    yield* streamFromClient(
    this._client,
    this._sessionId,
    this._activeBridges,
    prompt,
    options
    );
  • Public stream/add_user_message schemas do not include a same-turn auto-compaction checkpoint option:
    • rich client schema has message fields but no compaction config:
      export const AddUserMessageRequestParamsSchema = z.object({
      messageId: z.string().optional(),
      text: z.string(),
      images: Base64ImageSourceSchema.array().optional(),
      files: DocumentSourceSchema.array().optional(),
      outputFormat: OutputFormatSchema.optional(),
      skipAgentLoop: z.boolean().optional(),
      queuePlacement: z.nativeEnum(QueuePlacement).optional(),
      role: z.nativeEnum(MessageRole).optional(),
      visibility: z.nativeEnum(MessageVisibility).optional(),
      userMessageSource: z.nativeEnum(SessionOrigin).optional(),
      });
    • daemon schema only extends with sessionId:
      export const DaemonAddUserMessageRequestParamsSchema =
      AddUserMessageRequestParamsSchema.extend({
      sessionId: z.string(),
      });
    • legacy/CLI schema is strict and only includes attachments/output:
      export const AddUserMessageRequestParamsSchema = z
      .object({
      messageId: z.string().optional(),
      text: z.string(),
      images: z.array(Base64ImageSourceSchema).optional(),
      files: z.array(DocumentSourceSchema).optional(),
      outputFormat: OutputFormatSchema.optional(),
      })
      .strict();

Settings-level compaction fields such as compactionTokenLimit, compactionTokenLimitPerModel, or compactionThresholdCheckEnabled do not solve this by themselves: they are not a same-turn checkpoint in the stream/add_user_message path. Because the SDK stream is not a backpressure checkpoint, clients should not react to tool_result notifications by calling compactSession() while the live turn is still active. The daemon may already be continuing into the next model request.

Requested capability

Expose an SDK/daemon auto-compaction checkpoint that runs after tool results are persisted and before the next model request inside a single session.stream() turn.

Desired flow:

model request finishes
tool calls run
tool results are appended to session history
daemon checks whether auto-compaction is due
if due: emit compacting status, compact session, refresh context, adopt swapped backing session id
daemon continues with the next model request

Possible SDK API

Either add stream options that are forwarded into droid.add_user_message / daemon request params:

session.stream(prompt, {
  includePartialMessages: true,
  autoCompactTokenLimit,
  // optional if daemon owns the whole policy:
  shouldAutoCompact,
  beforeNextModelRequest,
  onCompactionStatus,
});

Or keep the daemon fully policy-owned and expose only the effective threshold/config field, e.g.:

session.stream(prompt, {
  includePartialMessages: true,
  autoCompactTokenLimit,
});

Required semantics

  • Never compact while assistant text is actively streaming.
  • Compact only between model calls, after tool results are appended and before the next model request.
  • Emit active status events while paused, e.g. compacting_conversation, Compacting conversation..., and Compaction complete.
  • If compaction swaps the backing session id, the same live continuation must continue against the new backing session.
  • Queued sends should wait until compaction finishes.
  • Existing manual compactSession() remains a separate operation and can still be rejected by clients while active.

Downstream context

Droid Control tracks the downstream need here:
anasibnanwar1-droid/droid-maxxing#42

That app can handle user-visible thresholds/UI, but it needs the SDK/daemon checkpoint to implement true same-task mid-task compaction safely.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions