@coder/ai-sdk-agent 0.7.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -196,6 +196,15 @@ Pass a Coder **API token** or **session token** as `token`; it is sent as the
196
196
  `Coder-Session-Token` header (REST) and used to authenticate the streaming WebSocket. Create
197
197
  a token with `coder tokens create`, or reuse your CLI session.
198
198
 
199
+ When `baseUrl`/`token` are not passed, they default from the `CODER_URL` and
200
+ `CODER_SESSION_TOKEN` environment variables (the same convention as
201
+ `@coder/ai-sdk-sandbox`'s transports); explicit settings win over the
202
+ environment. With the variables set, connection config disappears entirely:
203
+
204
+ ```ts
205
+ const agent = new CoderAgent({ organizationId }); // uses CODER_URL + CODER_SESSION_TOKEN
206
+ ```
207
+
199
208
  You can also pass a pre‑built client:
200
209
 
201
210
  ```ts
@@ -312,7 +321,9 @@ await agent.generate({ prompt: "…" });
312
321
  // e.g. attribute where the turn spent its time:
313
322
  for (const ev of events) {
314
323
  if (ev.type === "http:response")
315
- console.log(`${ev.method} ${ev.path} → ${ev.status} in ${ev.durationMs.toFixed(0)}ms`);
324
+ console.log(
325
+ `${ev.op}: ${ev.method} ${ev.path} → ${ev.status} in ${ev.durationMs.toFixed(0)}ms`,
326
+ );
316
327
  if (ev.type === "ws:event" && ev.event.type === "action_required")
317
328
  console.log(`tool calls arrived at +${ev.timestamp - events[0]!.timestamp}ms`);
318
329
  if (ev.type === "segment:settle")
@@ -326,12 +337,12 @@ such as a message's `created_at`, for delivery‑lag measurements):
326
337
 
327
338
  | event | when | payload (besides `timestamp`) |
328
339
  | ---------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
329
- | `http:request` | a REST request is sent | `id` (correlates the pair), `method`, `path` |
330
- | `http:response` | response headers arrive (incl. non‑2xx, `ok:false`) | `id`, `method`, `path`, `status`, `ok`, `durationMs` |
331
- | `http:error` | the fetch itself rejects (network failure, abort) | `id`, `method`, `path`, `message`, `durationMs` |
340
+ | `http:request` | a REST request is sent | `id` (correlates the pair), `op` (the client operation, e.g. `"createChatMessage"`), `method`, `path` |
341
+ | `http:response` | response headers arrive (incl. non‑2xx, `ok:false`) | `id`, `op`, `method`, `path`, `status`, `ok`, `durationMs` |
342
+ | `http:error` | the fetch itself rejects (network failure, abort) | `id`, `op`, `method`, `path`, `message`, `durationMs` |
332
343
  | `ws:dial` | a stream connection attempt starts | `chatId`, `reader` (identifies the `streamChatEvents` call), `attempt` (1‑based per reader, increments per redial), `url` |
333
344
  | `ws:open` | the WebSocket handshake completes | `chatId`, `reader`, `attempt` |
334
- | `ws:event` | a decoded stream event arrives | `chatId`, `reader`, `attempt`, `event` (the decoded `ChatStreamEvent`, by reference — don't mutate) |
345
+ | `ws:event` | a decoded stream event arrives | `chatId`, `reader`, `attempt`, `event` (the decoded `ChatStreamEvent`, by reference — don't mutate), `forwarded` (the reader's replay verdict) |
335
346
  | `ws:close` | the connection ends (exactly one per dial) | `chatId`, `reader`, `attempt`, `code`/`reason` when the server/network closed it; absent when the reader closed it (settle, teardown, redial) |
336
347
  | `ws:error` | a socket error or unparseable frame | `chatId`, `reader`, `attempt`, `message` |
337
348
  | `ws:redial` | a dropped connection is about to be redialed | `chatId`, `reader`, `attempt` (the ended connection), `consecutiveFailures`, `maxConsecutiveFailures`, `backoffMs` |
@@ -347,9 +358,24 @@ Semantics worth knowing:
347
358
  - **No secrets** — events carry no headers and no tokens (auth travels in the
348
359
  `Coder-Session-Token` header, which is deliberately excluded); `path`/`url`
349
360
  never contain credentials.
350
- - `ws:event` fires at arrival, **before** replay dedup: after a redial, chatd's
351
- replay of the in‑progress episode is visible here (correlate with `reader`/`attempt`)
352
- even though the reader suppresses the duplicates it forwards to the turn.
361
+ - `http:*` events name their operation: `op` is the public `CoderChatClient`
362
+ method performing the exchange (`"createChat"`, `"createChatMessage"`,
363
+ `"getMessages"`, `"submitToolResults"`, … — the `CoderClientOperation`
364
+ union), so per‑operation classification never has to reverse‑engineer
365
+ `path`. `method`/`path` stay for generic consumers; `archiveChat` stamps its
366
+ own `op` even though it issues the same `PATCH` as `updateChat`.
367
+ - `ws:event` fires at arrival: after a redial, chatd's replay of the
368
+ in‑progress episode is visible here (correlate with `reader`/`attempt`),
369
+ stamped with the reader's own replay verdict — `forwarded: false` exactly on
370
+ the duplicate deltas the reader suppresses from the turn, so subscribers
371
+ never re‑derive the episode filter.
372
+ - `forwarded` does **not** reflect snapshot dedup: repeated or revised
373
+ `message` snapshots are always `forwarded: true`, because reconciling them
374
+ is deliberately the consumer's job past the transport layer —
375
+ `TurnTranslator`'s per‑message ledger decides what a revision re‑emits, and
376
+ that disposition is not stamped on transport frames. Use `ws:event` for
377
+ span pairing and replay accounting; subscribers needing content fidelity
378
+ must consume model output (or `TurnTranslator`), not transport frames.
353
379
  - Every `ws:*` event carries `reader` — a monotonic id for the
354
380
  `streamChatEvents` call (reader) behind the connection, allocated from one
355
381
  process‑wide counter so it stays unique across model and client instances.
@@ -427,6 +453,25 @@ for a few seconds, during which archiving 409s — `archive()` retries those 409
427
453
  `settleRetryDelayMs`) and rethrows the last one if the chat never settles. Any
428
454
  other failure, including your own abort, rethrows immediately.
429
455
 
456
+ `archive()` and `interrupt()` target the session's chat — or, after the session
457
+ was dropped (`resetSession()`, or the automatic discard after a fresh‑chat
458
+ stream failure — see [Handling errors](#handling-errors)), the **last‑known
459
+ chat id** (`agent.lastKnownChatId`), so a stranded chat is still cleaned up
460
+ instead of leaking. Generation never uses the last‑known id: a turn after a
461
+ drop creates a fresh chat as always. Every chat stranded by an _automatic_
462
+ discard is also recorded on a ledger (`agent.strandedChatIds`, oldest first) —
463
+ with `maxRetries` several failed attempts can strand one chat each while only
464
+ the final attempt's error surfaces — and one `archive()` retires them all,
465
+ oldest first, alongside its primary target. Both methods report what they
466
+ acted on instead of silently no‑oping: `archive()` resolves
467
+ `{ archived: true, chatId, archivedChatIds }` (each archived id is cleared as
468
+ a cleanup target) or `{ archived: false }` when no chat exists at all;
469
+ `interrupt()` resolves `{ interrupted: true, chatId }` /
470
+ `{ interrupted: false }` the same way. Deliberate abandonment is different:
471
+ `resetSession()` does **not** add to the ledger (you may want that chat kept),
472
+ so after a manual reset the old chat is targetable only until a new chat
473
+ supersedes `lastKnownChatId`.
474
+
430
475
  To make cleanup ride scope exit instead of a `finally` you have to remember, the
431
476
  agent is an **async disposable**:
432
477
 
@@ -460,7 +505,11 @@ carry structured detail you can branch on:
460
505
  repeat (workspace/MCP tooling, fresh attachment uploads); otherwise it is
461
506
  `false` — a retry would resubmit the prompt as a new user turn and could
462
507
  duplicate those effects. A failure mid‑`stream()` surfaces on the stream,
463
- outside the retry wrapper. The last transport failure is in `cause`.
508
+ outside the retry wrapper. The last transport failure is in `cause`, and
509
+ `chatId` names the chat the failed turn had created or attached to (absent
510
+ when it failed before a chat existed) — after the fresh‑chat discard the
511
+ chat still exists server‑side, and `archive()` keeps targeting it via
512
+ `agent.lastKnownChatId` ([Cleanup](#cleanup)).
464
513
 
465
514
  ```ts
466
515
  import { CoderApiError, CoderChatError } from "@coder/ai-sdk-agent";
@@ -675,7 +724,8 @@ Coder servers — no experimental endpoints.
675
724
  named "80" and never resolve; serve the preview on a higher port.
676
725
 
677
726
  The preview helpers call non‑chat endpoints, so they need `baseUrl` + `token`
678
- credentials — pass them alongside `client` if you construct one yourself.
727
+ credentials — pass them alongside `client` if you construct one yourself (or
728
+ let them default from `CODER_URL`/`CODER_SESSION_TOKEN`).
679
729
 
680
730
  ## Workspaces & quota
681
731
 
@@ -835,7 +885,7 @@ sequence pinpoints _where_ a turn died:
835
885
 
836
886
  | field | description |
837
887
  | --------------------------------- | ------------------------------------------------------------------------------------------------ |
838
- | `client` \| (`baseUrl` + `token`) | connection (one or the other) |
888
+ | `client` \| (`baseUrl` + `token`) | connection (one or the other; `baseUrl`/`token` default from `CODER_URL`/`CODER_SESSION_TOKEN`) |
839
889
  | `organizationId` | org UUID that owns the chat (required) |
840
890
  | `model` | model hint: UUID, `provider:model`, model id, or display‑name substring |
841
891
  | `instructions` | system prompt |
@@ -936,7 +986,9 @@ export async function runTurn(workflowId: string, prompt: string): Promise<strin
936
986
  // still persist the id, or the retried step orphans a live chat and its
937
987
  // partial effects. Exception: after an exhausted stream failure on a
938
988
  // chat this very call created, the SDK has already discarded the dead
939
- // session (agent.chatId is undefined) and nothing is written — see the
989
+ // session (agent.chatId is undefined) and nothing is written — the thrown
990
+ // CoderStreamError's chatId still names the stranded chat, and
991
+ // agent.archive() retires it via agent.lastKnownChatId — see the
940
992
  // CoderStreamError notes below for what retrying means then.
941
993
  if (agent.chatId) await checkpoints.set(workflowId, agent.chatId);
942
994
  }
@@ -1053,12 +1105,15 @@ try {
1053
1105
  the dead session so a retry — `maxRetries` or a re‑run step — starts clean
1054
1106
  on a fresh chat. The discard happens for _every_ chat the failed turn
1055
1107
  itself created — **including the effectful, `isRetryable: false` case**:
1056
- the id is gone, the `finally` checkpoint writes nothing, and the
1057
- interrupted chat — whose workspace/MCP tools may already have acted —
1058
- becomes an unacknowledged orphan for the reconciliation sweep (below). A
1059
- retried step then starts a fresh chat that has _not_ seen the orphan's
1060
- effects: gate that retry on the same idempotency judgment as any
1061
- re‑submission. **On a resumed chat it is always `isRetryable: false`:**
1108
+ `agent.chatId` is `undefined` and the `finally` checkpoint writes nothing —
1109
+ but the stranded chat is not nameless: the error's **`chatId`** field names
1110
+ it, and `interrupt()`/`archive()` keep targeting it via
1111
+ `agent.lastKnownChatId` ([Cleanup](#cleanup)), so the step's error path can
1112
+ retire the orphan directly — or checkpoint `err.chatId` for the
1113
+ reconciliation sweep (below) when its workspace/MCP effects need
1114
+ reconciling first. A retried step then starts a fresh chat that has _not_
1115
+ seen the orphan's effects: gate that retry on the same idempotency judgment
1116
+ as any re‑submission. **On a resumed chat it is always `isRetryable: false`:**
1062
1117
  re‑running the step resubmits the same prompt as a _new user turn_, with the
1063
1118
  failed attempt's partial output still in history (usually fine — the model
1064
1119
  sees its own aborted attempt), and any workspace/MCP tool effects that ran
package/dist/index.d.ts CHANGED
@@ -344,12 +344,27 @@ interface TransportEventBase {
344
344
  /** Wall-clock observation time in ms since epoch (`Date.now()`). */
345
345
  timestamp: number;
346
346
  }
347
+ /**
348
+ * Names the `CoderChatClient` operation performing an HTTP exchange (issue
349
+ * #112). The public client method names are the stable vocabulary, so
350
+ * subscribers can classify per-operation timings without reverse-engineering
351
+ * `path` (whose API prefix is private and may change). New client methods
352
+ * extend this union; treat unrecognized values as "some other operation"
353
+ * rather than matching exhaustively.
354
+ */
355
+ type CoderClientOperation = "listModelConfigs" | "resolveModelConfigId" | "createChat" | "getChat" | "createChatMessage" | "getMessages" | "submitToolResults" | "interruptChat" | "updateChat" | "archiveChat" | "uploadChatFile" | "getChatFile";
347
356
  interface HttpTransportEventBase extends TransportEventBase {
348
357
  /**
349
358
  * Correlates a request with its response/error. Monotonically increasing
350
359
  * per client instance.
351
360
  */
352
361
  id: number;
362
+ /**
363
+ * The client operation performing this exchange — the public
364
+ * `CoderChatClient` method name. `archiveChat` stamps its own operation
365
+ * even though it issues the same `PATCH` as `updateChat`.
366
+ */
367
+ op: CoderClientOperation;
353
368
  /** HTTP method, e.g. `POST`. */
354
369
  method: string;
355
370
  /**
@@ -415,14 +430,31 @@ interface StreamOpenTransportEvent extends StreamTransportEventBase {
415
430
  }
416
431
  /**
417
432
  * A decoded stream event arrived on the wire. Emitted at arrival time for
418
- * every event in a frame batch, BEFORE replay dedup — after a redial, chatd's
419
- * replay of the in-progress episode is visible here even though the reader
420
- * suppresses the duplicates it forwards. `event` is the decoded object by
421
- * reference (no copy): do not mutate it.
433
+ * every event in a frame batch — after a redial, chatd's replay of the
434
+ * in-progress episode is still visible here, stamped with the reader's own
435
+ * replay verdict (`forwarded`). `event` is the decoded object by reference
436
+ * (no copy): do not mutate it.
422
437
  */
423
438
  interface StreamEventTransportEvent extends StreamTransportEventBase {
424
439
  type: "ws:event";
425
440
  event: ChatStreamEvent;
441
+ /**
442
+ * The reader's replay disposition for this frame (issue #111): `false`
443
+ * exactly when the episode filter suppresses it from the consumption path —
444
+ * a redial's replay of an in-progress delta at or below the last yielded
445
+ * `seq` of the same episode — and `true` when the reader forwards it to its
446
+ * consumer. It is a dedup verdict, not a delivery receipt: a consumer
447
+ * tearing down mid-queue can still discard a forwarded frame.
448
+ *
449
+ * Snapshot dedup is deliberately NOT reflected here: repeated or revised
450
+ * `message` snapshots are always `forwarded: true`, because reconciling
451
+ * them is the consumer's job past the transport layer — TurnTranslator's
452
+ * per-message emitted-content ledger decides what a revision re-emits.
453
+ * That disposition is not stamped on transport frames; subscribers needing
454
+ * content fidelity must consume model output (or TurnTranslator), not
455
+ * `ws:event`.
456
+ */
457
+ forwarded: boolean;
426
458
  }
427
459
  /**
428
460
  * The connection ended. Exactly one per dial: carries `code`/`reason` when
@@ -586,7 +618,10 @@ interface StreamChatEventsOptions {
586
618
  * in-progress generation attempt's `message_part` deltas from `seq` 1, so
587
619
  * parts of an episode
588
620
  * (`history_version`, `generation_attempt`) at or below the last yielded `seq`
589
- * are suppressed here rather than double-yielded. The iteration ends in five
621
+ * are suppressed here rather than double-yielded. That episode filter runs at
622
+ * frame arrival (not at consumption), so each frame's `ws:event` can carry
623
+ * the reader's own verdict as `forwarded` (issue #111) while `timestamp`
624
+ * keeps meaning arrival time. The iteration ends in five
590
625
  * ways: `signal` aborting (clean return), a non-transient 4xx upgrade
591
626
  * rejection (terminal {@link CoderApiError} — bad/expired token or a deleted
592
627
  * chat cannot succeed on retry, while 408/425/429 consume the redial budget),
@@ -815,18 +850,49 @@ interface ChatAttachment extends UploadedChatFile {
815
850
  */
816
851
  toFilePart(): FilePart;
817
852
  }
853
+ /**
854
+ * Result of {@link CoderAgent.interrupt}: `interrupted: false` means there was
855
+ * no chat to interrupt (neither a live session nor a
856
+ * {@link CoderAgent.lastKnownChatId}) and no server call was made.
857
+ */
858
+ type CoderInterruptResult = {
859
+ interrupted: true;
860
+ chatId: string;
861
+ } | {
862
+ interrupted: false;
863
+ chatId?: undefined;
864
+ };
865
+ /**
866
+ * Result of {@link CoderAgent.archive}: `archived: false` means there was no
867
+ * chat to archive (neither a live session nor a
868
+ * {@link CoderAgent.lastKnownChatId}) and no server call was made. Failures
869
+ * still throw — see {@link CoderAgent.archive}. `chatId` is the primary
870
+ * target (the session's or last-known chat); `archivedChatIds` additionally
871
+ * lists every chat archived by the call, oldest first — more than one when
872
+ * earlier stranded chats ({@link CoderAgent.strandedChatIds}) were retired
873
+ * alongside it.
874
+ */
875
+ type CoderArchiveResult = {
876
+ archived: true;
877
+ chatId: string;
878
+ archivedChatIds: string[];
879
+ } | {
880
+ archived: false;
881
+ chatId?: undefined;
882
+ archivedChatIds?: undefined;
883
+ };
818
884
  interface CoderAgentSettings<TOOLS extends ToolSet = {}> {
819
885
  /**
820
886
  * A pre-built {@link CoderChatClient}, used instead of constructing one from
821
887
  * `baseUrl`/`token`. The preview helpers ({@link CoderAgent.getPreview},
822
888
  * {@link CoderAgent.sharePreview}) call non-chat endpoints and therefore
823
- * still need credentials — pass `baseUrl` + `token` alongside `client` to
824
- * enable them.
889
+ * still need credentials — pass `baseUrl` + `token` alongside `client` (or
890
+ * set `CODER_URL` + `CODER_SESSION_TOKEN`) to enable them.
825
891
  */
826
892
  client?: CoderChatClient;
827
- /** Coder deployment base URL, e.g. `https://dev.coder.com`. */
893
+ /** Coder deployment base URL, e.g. `https://dev.coder.com`. Defaults to `CODER_URL`. */
828
894
  baseUrl?: string;
829
- /** Coder API/session token (sent as `Coder-Session-Token`). */
895
+ /** Coder API/session token (sent as `Coder-Session-Token`). Defaults to `CODER_SESSION_TOKEN`. */
830
896
  token?: string;
831
897
  fetch?: CoderChatClientOptions["fetch"];
832
898
  webSocketFactory?: CoderChatClientOptions["webSocketFactory"];
@@ -964,6 +1030,25 @@ declare class CoderAgent<TOOLS extends ToolSet = {}> implements Agent<never, TOO
964
1030
  get client(): CoderChatClient;
965
1031
  /** The current chatd chat id, once a turn has started. */
966
1032
  get chatId(): string | undefined;
1033
+ /**
1034
+ * The last chat id this agent created or attached to, retained for CLEANUP
1035
+ * even after the session was dropped — a fresh-chat stream failure discards
1036
+ * the session ({@link CoderAgent.chatId} becomes `undefined`, so a retry
1037
+ * starts fresh), but the failed chat still exists server-side.
1038
+ * {@link CoderAgent.archive} and {@link CoderAgent.interrupt} fall back to
1039
+ * this id; generation never does. Superseded when a later turn creates or
1040
+ * attaches to another chat; cleared once {@link CoderAgent.archive}
1041
+ * succeeds against it.
1042
+ */
1043
+ get lastKnownChatId(): string | undefined;
1044
+ /**
1045
+ * Chat ids stranded by automatic session discards and not yet cleaned up,
1046
+ * oldest first. Each retryable fresh-chat stream failure strands one chat,
1047
+ * so with `maxRetries` > 0 several can accumulate while only the final
1048
+ * attempt's error (and {@link CoderAgent.lastKnownChatId}) names the
1049
+ * newest. {@link CoderAgent.archive} retires all of them.
1050
+ */
1051
+ get strandedChatIds(): string[];
967
1052
  generate(options: Parameters<InnerAgent<TOOLS>["generate"]>[0]): ReturnType<InnerAgent<TOOLS>["generate"]>;
968
1053
  stream(options: Parameters<InnerAgent<TOOLS>["stream"]>[0]): ReturnType<InnerAgent<TOOLS>["stream"]>;
969
1054
  /**
@@ -972,33 +1057,61 @@ declare class CoderAgent<TOOLS extends ToolSet = {}> implements Agent<never, TOO
972
1057
  * `id`, `provider`/`model`, or `display_name`.
973
1058
  */
974
1059
  listModels(signal?: AbortSignal): Promise<ChatModelConfig[]>;
975
- /** Start a fresh chatd chat on the next turn. */
1060
+ /**
1061
+ * Start a fresh chatd chat on the next turn. The dropped chat still exists
1062
+ * server-side: {@link CoderAgent.archive} / {@link CoderAgent.interrupt}
1063
+ * keep targeting it (via {@link CoderAgent.lastKnownChatId}) until a new
1064
+ * chat supersedes it or an archive succeeds against it.
1065
+ */
976
1066
  resetSession(): void;
977
1067
  /**
978
1068
  * Interrupt the in-flight generation, if any. Resolves as soon as the server
979
1069
  * acknowledges the interrupt — the run keeps winding down asynchronously for
980
1070
  * a few seconds afterwards (see {@link CoderAgent.archive}).
1071
+ *
1072
+ * Targets the session's chat, or — after the session was dropped (a
1073
+ * fresh-chat stream failure, or {@link CoderAgent.resetSession}) — the
1074
+ * {@link CoderAgent.lastKnownChatId}, whose run may still be winding down
1075
+ * server-side. With neither, there is nothing to interrupt and it resolves
1076
+ * `{ interrupted: false }` without a server call (inspect the result rather
1077
+ * than relying on a silent no-op).
981
1078
  */
982
1079
  interrupt(opts?: {
983
1080
  signal?: AbortSignal;
984
- }): Promise<void>;
1081
+ }): Promise<CoderInterruptResult>;
985
1082
  /**
986
1083
  * Archive the underlying chat (safe cleanup; hides it from listings).
987
1084
  *
1085
+ * Targets the session's chat, or — after the session was dropped (a
1086
+ * fresh-chat stream failure, or {@link CoderAgent.resetSession}) — the
1087
+ * {@link CoderAgent.lastKnownChatId}, so the stranded chat is still cleaned
1088
+ * up; chats stranded by EARLIER automatic discards
1089
+ * ({@link CoderAgent.strandedChatIds} — `maxRetries` can accumulate several)
1090
+ * are retired in the same call. On success each archived id is cleared as a
1091
+ * cleanup target; with no id at all, there is nothing to archive and it
1092
+ * resolves `{ archived: false }` without a server call (inspect the result
1093
+ * rather than relying on a silent no-op).
1094
+ *
988
1095
  * A freshly interrupted/settled chat can keep winding down server-side for a
989
1096
  * few seconds, during which the server rejects archiving with a 409. Those
990
- * 409s are retried (~1s apart, ~15s overall — see `settleDeadlineMs` /
991
- * `settleRetryDelayMs`) and the last one is rethrown if the chat never
992
- * settles; any other failure, including a caller abort, rethrows immediately.
1097
+ * 409s are retried (~1s apart — see `settleRetryDelayMs`) inside ONE
1098
+ * `settleDeadlineMs` window (~15s) shared by the whole call — however many
1099
+ * chats it archives — and the last 409 is rethrown if the window closes
1100
+ * first; any other failure, including a caller abort, rethrows immediately.
1101
+ * Chats are archived oldest first, each id cleared as its archive succeeds —
1102
+ * a mid-list failure rethrows and leaves the remaining ids targetable by a
1103
+ * later retry.
993
1104
  */
994
1105
  archive(opts?: {
995
1106
  signal?: AbortSignal;
996
- }): Promise<void>;
1107
+ }): Promise<CoderArchiveResult>;
997
1108
  /**
998
1109
  * Clean up the chat when the agent leaves an `await using` scope, so cleanup
999
1110
  * rides scope exit instead of a separate call you have to remember in a
1000
1111
  * `finally`. Interrupts any in-flight server run, then archives the chat
1001
- * (retrying while the interrupted run settles). Best-effort and bounded
1112
+ * (retrying while the interrupted run settles) — including a chat stranded
1113
+ * by a session discard (see {@link CoderAgent.lastKnownChatId}), which a
1114
+ * failed `generate()` would otherwise leak. Best-effort and bounded
1002
1115
  * (~15s overall): disposal errors are swallowed after the bounded attempts
1003
1116
  * so they can't mask the scope's own error — call {@link CoderAgent.archive}
1004
1117
  * directly when you need guaranteed cleanup.
@@ -1128,10 +1241,42 @@ declare class CoderLanguageModel implements LanguageModelV4 {
1128
1241
  readonly supportedUrls: Record<string, RegExp[]>;
1129
1242
  constructor(config: CoderLanguageModelConfig);
1130
1243
  get chatId(): string | undefined;
1244
+ /**
1245
+ * The last chat id this model created or attached to, for CLEANUP targeting
1246
+ * only — generation always targets {@link chatId}. It survives
1247
+ * {@link resetSession} (including the automatic session discard after a
1248
+ * fresh-chat stream failure, which clears {@link chatId} so a retry starts
1249
+ * fresh while the failed chat still exists server-side), is superseded when
1250
+ * a later turn creates or attaches to another chat, and is cleared via
1251
+ * {@link noteChatCleanedUp} once cleanup actually succeeded against it.
1252
+ */
1253
+ get lastKnownChatId(): string | undefined;
1254
+ /**
1255
+ * Chat ids stranded by automatic session discards and not yet cleaned up,
1256
+ * oldest first. Each retryable fresh-chat stream failure strands one chat;
1257
+ * under AI-SDK `maxRetries` several can accumulate while only the final
1258
+ * attempt's error (and {@link lastKnownChatId}) names the newest —
1259
+ * `CoderAgent.archive()` retires all of them. Ids leave the list only via
1260
+ * {@link noteChatCleanedUp}.
1261
+ */
1262
+ get strandedChatIds(): string[];
1263
+ /**
1264
+ * Forgets `chatId` as a cleanup target — call it only after cleanup
1265
+ * actually succeeded against that chat (e.g. `CoderAgent.archive()`
1266
+ * archived it), so later cleanup calls don't re-target an already-archived
1267
+ * chat: removes it from {@link strandedChatIds} and, when it is the
1268
+ * last-known id, clears {@link lastKnownChatId} (match-guarded: a stale
1269
+ * completion must not erase a newer chat's id). The session field
1270
+ * ({@link chatId}) is deliberately untouched — archiving does not reset the
1271
+ * session.
1272
+ */
1273
+ noteChatCleanedUp(chatId: string): void;
1131
1274
  /**
1132
1275
  * Drops the current session so the next turn creates a fresh chat. Also
1133
1276
  * closes the retained event stream, if any (fire-and-forget — a sync reset
1134
1277
  * must not wait on socket teardown, and `close()` never rejects).
1278
+ * {@link lastKnownChatId} deliberately survives the reset: the dropped chat
1279
+ * still exists server-side and cleanup must still be able to target it.
1135
1280
  */
1136
1281
  resetSession(): void;
1137
1282
  /**
@@ -1340,11 +1485,21 @@ declare class CoderApiError extends CoderAgentError {
1340
1485
  */
1341
1486
  declare class CoderStreamError extends APICallError {
1342
1487
  name: string;
1488
+ /**
1489
+ * The chatd chat id the failed turn had created or attached to, when known.
1490
+ * A fresh-chat stream failure discards the session so a retry starts on a
1491
+ * fresh chat (`CoderAgent.chatId` becomes `undefined`), but the failed chat
1492
+ * still exists server-side — this field names it so callers can clean it up
1493
+ * (`CoderAgent.archive()` also retains it as the last-known cleanup target).
1494
+ * `undefined` when the failure happened before any chat existed.
1495
+ */
1496
+ readonly chatId: string | undefined;
1343
1497
  constructor(args: {
1344
1498
  message: string;
1345
1499
  url: string;
1346
1500
  cause?: unknown;
1347
1501
  isRetryable?: boolean;
1502
+ chatId?: string;
1348
1503
  });
1349
1504
  }
1350
1505
  /** A chat generation ended in an error status. */
@@ -1357,4 +1512,4 @@ declare class CoderChatError extends CoderAgentError {
1357
1512
  constructor(payload: ChatErrorPayload);
1358
1513
  }
1359
1514
 
1360
- export { CHAT_ATTACHMENT_MEDIA_TYPES, CODER_PROVIDER_OPTIONS, type Chat, type ChatAttachment, type ChatAttachmentMediaType, type ChatBusyBehavior, type ChatClientType, type ChatErrorPayload, type ChatFileInput, type ChatInputPart, type ChatInputPartType, type ChatMessage, type ChatMessagePart, type ChatMessagePartType, type ChatMessageRole, type ChatMessageUsage, type ChatMessagesResponse, type ChatMessagesToUIMessagesOptions, type ChatModelConfig, type ChatPlanMode, type ChatQueuedMessage, type ChatStatus, type ChatStreamActionRequired, type ChatStreamEvent, type ChatStreamEventType, type ChatStreamMessagePart, type ChatStreamRetry, type ChatStreamStatus, type ChatStreamToolCall, type ChatWatchEvent, type ChatWatchEventKind, CoderAgent, CoderAgentError, type CoderAgentSettings, CoderApiError, CoderChatClient, type CoderChatClientOptions, CoderChatError, type CoderFileProviderOptions, CoderLanguageModel, type CoderLanguageModelConfig, CoderStreamError, type CoderTransportEvent, type CreateChatMessageRequest, type CreateChatMessageResponse, type CreateChatRequest, type DynamicTool, type FileContent, type FilePartUploader, type HttpErrorTransportEvent, type HttpRequestTransportEvent, type HttpResponseTransportEvent, MAX_CHAT_FILE_SIZE_BYTES, type PreviewShareLevel, type ResolvedFile, type SegmentSettleTransportEvent, type SegmentStartTransportEvent, type SharePreviewOptions, type SharedWorkspacePreview, type StreamCloseTransportEvent, type StreamDialTransportEvent, type StreamErrorTransportEvent, type StreamEventTransportEvent, type StreamOpenTransportEvent, type StreamRedialTransportEvent, type SubmitToolResultsRequest, TERMINAL_STATUSES, type ToolResult, type TransportEventHandler, type TurnAction, TurnTranslator, type UpdateChatRequest, type UploadChatFileResponse, type UploadedChatFile, type UserContent, type WatchChatEventsOptions, type WebSocketFactory, type WebSocketLike, type WorkspaceFileStore, type WorkspacePlacement, type WorkspacePreview, type WorkspacePreviewOptions, chatMessagesToUIMessages, classifyTurnAction, dataContentToFileContent, dynamicToolNames, extractSystemPrompt, resolveFileContent, streamChatEvents, toolsToDynamicTools, userContentToInputParts, watchChatEvents };
1515
+ export { CHAT_ATTACHMENT_MEDIA_TYPES, CODER_PROVIDER_OPTIONS, type Chat, type ChatAttachment, type ChatAttachmentMediaType, type ChatBusyBehavior, type ChatClientType, type ChatErrorPayload, type ChatFileInput, type ChatInputPart, type ChatInputPartType, type ChatMessage, type ChatMessagePart, type ChatMessagePartType, type ChatMessageRole, type ChatMessageUsage, type ChatMessagesResponse, type ChatMessagesToUIMessagesOptions, type ChatModelConfig, type ChatPlanMode, type ChatQueuedMessage, type ChatStatus, type ChatStreamActionRequired, type ChatStreamEvent, type ChatStreamEventType, type ChatStreamMessagePart, type ChatStreamRetry, type ChatStreamStatus, type ChatStreamToolCall, type ChatWatchEvent, type ChatWatchEventKind, CoderAgent, CoderAgentError, type CoderAgentSettings, CoderApiError, type CoderArchiveResult, CoderChatClient, type CoderChatClientOptions, CoderChatError, type CoderClientOperation, type CoderFileProviderOptions, type CoderInterruptResult, CoderLanguageModel, type CoderLanguageModelConfig, CoderStreamError, type CoderTransportEvent, type CreateChatMessageRequest, type CreateChatMessageResponse, type CreateChatRequest, type DynamicTool, type FileContent, type FilePartUploader, type HttpErrorTransportEvent, type HttpRequestTransportEvent, type HttpResponseTransportEvent, MAX_CHAT_FILE_SIZE_BYTES, type PreviewShareLevel, type ResolvedFile, type SegmentSettleTransportEvent, type SegmentStartTransportEvent, type SharePreviewOptions, type SharedWorkspacePreview, type StreamCloseTransportEvent, type StreamDialTransportEvent, type StreamErrorTransportEvent, type StreamEventTransportEvent, type StreamOpenTransportEvent, type StreamRedialTransportEvent, type SubmitToolResultsRequest, TERMINAL_STATUSES, type ToolResult, type TransportEventHandler, type TurnAction, TurnTranslator, type UpdateChatRequest, type UploadChatFileResponse, type UploadedChatFile, type UserContent, type WatchChatEventsOptions, type WebSocketFactory, type WebSocketLike, type WorkspaceFileStore, type WorkspacePlacement, type WorkspacePreview, type WorkspacePreviewOptions, chatMessagesToUIMessages, classifyTurnAction, dataContentToFileContent, dynamicToolNames, extractSystemPrompt, resolveFileContent, streamChatEvents, toolsToDynamicTools, userContentToInputParts, watchChatEvents };