@coder/ai-sdk-agent 0.8.0 → 0.9.1
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 +128 -50
- package/dist/index.d.ts +270 -26
- package/dist/index.js +464 -90
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -30,6 +30,15 @@ var CoderApiError = class extends CoderAgentError {
|
|
|
30
30
|
};
|
|
31
31
|
var CoderStreamError = class extends APICallError {
|
|
32
32
|
name = "CoderStreamError";
|
|
33
|
+
/**
|
|
34
|
+
* The chatd chat id the failed turn had created or attached to, when known.
|
|
35
|
+
* A fresh-chat stream failure discards the session so a retry starts on a
|
|
36
|
+
* fresh chat (`CoderAgent.chatId` becomes `undefined`), but the failed chat
|
|
37
|
+
* still exists server-side — this field names it so callers can clean it up
|
|
38
|
+
* (`CoderAgent.archive()` also retains it as the last-known cleanup target).
|
|
39
|
+
* `undefined` when the failure happened before any chat existed.
|
|
40
|
+
*/
|
|
41
|
+
chatId;
|
|
33
42
|
constructor(args) {
|
|
34
43
|
super({
|
|
35
44
|
message: args.message,
|
|
@@ -42,6 +51,7 @@ var CoderStreamError = class extends APICallError {
|
|
|
42
51
|
isRetryable: args.isRetryable ?? true,
|
|
43
52
|
cause: args.cause
|
|
44
53
|
});
|
|
54
|
+
this.chatId = args.chatId;
|
|
45
55
|
}
|
|
46
56
|
};
|
|
47
57
|
var CoderChatError = class extends CoderAgentError {
|
|
@@ -303,6 +313,52 @@ async function* streamChatEventsLoop(options) {
|
|
|
303
313
|
abortListenerAdded = true;
|
|
304
314
|
}
|
|
305
315
|
}
|
|
316
|
+
const ingest = (next) => {
|
|
317
|
+
let progress = false;
|
|
318
|
+
let forwarded = true;
|
|
319
|
+
if (next.type === "message_part") {
|
|
320
|
+
const mp = next.message_part;
|
|
321
|
+
const hv = mp?.history_version;
|
|
322
|
+
const ga = mp?.generation_attempt;
|
|
323
|
+
const seq = mp?.seq;
|
|
324
|
+
if (hv !== void 0 && ga !== void 0 && seq !== void 0) {
|
|
325
|
+
if (hv === lastHistoryVersion && ga === lastGenerationAttempt && lastSeq !== void 0 && seq <= lastSeq) {
|
|
326
|
+
forwarded = false;
|
|
327
|
+
} else {
|
|
328
|
+
lastHistoryVersion = hv;
|
|
329
|
+
lastGenerationAttempt = ga;
|
|
330
|
+
lastSeq = seq;
|
|
331
|
+
progress = true;
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
sawUntrackableDelta = true;
|
|
335
|
+
}
|
|
336
|
+
} else if (next.type === "message" && next.message) {
|
|
337
|
+
const serialized = JSON.stringify(next.message);
|
|
338
|
+
if (lastMessageJson.get(next.message.id) !== serialized) {
|
|
339
|
+
lastMessageJson.set(next.message.id, serialized);
|
|
340
|
+
progress = true;
|
|
341
|
+
}
|
|
342
|
+
if (maxCommittedId === void 0 || next.message.id > maxCommittedId) {
|
|
343
|
+
maxCommittedId = next.message.id;
|
|
344
|
+
sawUntrackableDelta = false;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (progress) {
|
|
348
|
+
failures = 0;
|
|
349
|
+
backoffMs = STREAM_BACKOFF_INITIAL_MS;
|
|
350
|
+
}
|
|
351
|
+
emit?.({
|
|
352
|
+
type: "ws:event",
|
|
353
|
+
chatId,
|
|
354
|
+
reader,
|
|
355
|
+
attempt,
|
|
356
|
+
event: next,
|
|
357
|
+
forwarded,
|
|
358
|
+
timestamp: Date.now()
|
|
359
|
+
});
|
|
360
|
+
if (forwarded) queue.push(next);
|
|
361
|
+
};
|
|
306
362
|
const onMessage = (ev) => {
|
|
307
363
|
if (finished) return;
|
|
308
364
|
let batch;
|
|
@@ -325,27 +381,9 @@ async function* streamChatEventsLoop(options) {
|
|
|
325
381
|
return;
|
|
326
382
|
}
|
|
327
383
|
if (Array.isArray(batch)) {
|
|
328
|
-
for (const e of batch)
|
|
329
|
-
queue.push(e);
|
|
330
|
-
emit?.({
|
|
331
|
-
type: "ws:event",
|
|
332
|
-
chatId,
|
|
333
|
-
reader,
|
|
334
|
-
attempt,
|
|
335
|
-
event: e,
|
|
336
|
-
timestamp: Date.now()
|
|
337
|
-
});
|
|
338
|
-
}
|
|
384
|
+
for (const e of batch) ingest(e);
|
|
339
385
|
} else if (batch && typeof batch === "object") {
|
|
340
|
-
|
|
341
|
-
emit?.({
|
|
342
|
-
type: "ws:event",
|
|
343
|
-
chatId,
|
|
344
|
-
reader,
|
|
345
|
-
attempt,
|
|
346
|
-
event: batch,
|
|
347
|
-
timestamp: Date.now()
|
|
348
|
-
});
|
|
386
|
+
ingest(batch);
|
|
349
387
|
}
|
|
350
388
|
wake();
|
|
351
389
|
};
|
|
@@ -383,40 +421,7 @@ async function* streamChatEventsLoop(options) {
|
|
|
383
421
|
try {
|
|
384
422
|
while (true) {
|
|
385
423
|
while (queue.length > 0) {
|
|
386
|
-
|
|
387
|
-
let progress = false;
|
|
388
|
-
if (next.type === "message_part") {
|
|
389
|
-
const mp = next.message_part;
|
|
390
|
-
const hv = mp?.history_version;
|
|
391
|
-
const ga = mp?.generation_attempt;
|
|
392
|
-
const seq = mp?.seq;
|
|
393
|
-
if (hv !== void 0 && ga !== void 0 && seq !== void 0) {
|
|
394
|
-
if (hv === lastHistoryVersion && ga === lastGenerationAttempt && lastSeq !== void 0 && seq <= lastSeq) {
|
|
395
|
-
continue;
|
|
396
|
-
}
|
|
397
|
-
lastHistoryVersion = hv;
|
|
398
|
-
lastGenerationAttempt = ga;
|
|
399
|
-
lastSeq = seq;
|
|
400
|
-
progress = true;
|
|
401
|
-
} else {
|
|
402
|
-
sawUntrackableDelta = true;
|
|
403
|
-
}
|
|
404
|
-
} else if (next.type === "message" && next.message) {
|
|
405
|
-
const serialized = JSON.stringify(next.message);
|
|
406
|
-
if (lastMessageJson.get(next.message.id) !== serialized) {
|
|
407
|
-
lastMessageJson.set(next.message.id, serialized);
|
|
408
|
-
progress = true;
|
|
409
|
-
}
|
|
410
|
-
if (maxCommittedId === void 0 || next.message.id > maxCommittedId) {
|
|
411
|
-
maxCommittedId = next.message.id;
|
|
412
|
-
sawUntrackableDelta = false;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
if (progress) {
|
|
416
|
-
failures = 0;
|
|
417
|
-
backoffMs = STREAM_BACKOFF_INITIAL_MS;
|
|
418
|
-
}
|
|
419
|
-
yield next;
|
|
424
|
+
yield queue.shift();
|
|
420
425
|
}
|
|
421
426
|
if (finished) break;
|
|
422
427
|
await new Promise((resolve) => {
|
|
@@ -446,7 +451,8 @@ async function* streamChatEventsLoop(options) {
|
|
|
446
451
|
throw new CoderStreamError({
|
|
447
452
|
message: "chat stream dropped mid-turn and cannot be resumed: this server does not stamp deltas with history_version/generation_attempt/seq, so a redial would replay them",
|
|
448
453
|
url,
|
|
449
|
-
cause: failure
|
|
454
|
+
cause: failure,
|
|
455
|
+
chatId
|
|
450
456
|
});
|
|
451
457
|
}
|
|
452
458
|
if (failure) lastFailure = failure;
|
|
@@ -455,7 +461,8 @@ async function* streamChatEventsLoop(options) {
|
|
|
455
461
|
throw new CoderStreamError({
|
|
456
462
|
message: `Coder chat stream ${path} could not be (re-)established after ${failures} consecutive connection failures without progress`,
|
|
457
463
|
url,
|
|
458
|
-
cause: lastFailure
|
|
464
|
+
cause: lastFailure,
|
|
465
|
+
chatId
|
|
459
466
|
});
|
|
460
467
|
}
|
|
461
468
|
emit?.({
|
|
@@ -667,9 +674,11 @@ var CoderChatClient = class {
|
|
|
667
674
|
/**
|
|
668
675
|
* Issue a request and return the raw `Response` on success (body unread). On a
|
|
669
676
|
* non-2xx status the body is consumed to build a {@link CoderApiError}. Shared
|
|
670
|
-
* by JSON requests and the raw upload/download endpoints.
|
|
677
|
+
* by JSON requests and the raw upload/download endpoints. `op` names the
|
|
678
|
+
* public client method on the exchange's `http:*` events (issue #112) — the
|
|
679
|
+
* emit site is shared, so each call site threads its own literal.
|
|
671
680
|
*/
|
|
672
|
-
async #send(method, path, opts) {
|
|
681
|
+
async #send(op, method, path, opts) {
|
|
673
682
|
const headers = {
|
|
674
683
|
"Coder-Session-Token": this.#token,
|
|
675
684
|
...opts?.headers
|
|
@@ -684,7 +693,7 @@ var CoderChatClient = class {
|
|
|
684
693
|
if (emit) {
|
|
685
694
|
requestId = ++this.#httpSeq;
|
|
686
695
|
startedAt = performance.now();
|
|
687
|
-
emit({ type: "http:request", id: requestId, method, path, timestamp: Date.now() });
|
|
696
|
+
emit({ type: "http:request", id: requestId, op, method, path, timestamp: Date.now() });
|
|
688
697
|
}
|
|
689
698
|
let res;
|
|
690
699
|
try {
|
|
@@ -693,6 +702,7 @@ var CoderChatClient = class {
|
|
|
693
702
|
emit?.({
|
|
694
703
|
type: "http:error",
|
|
695
704
|
id: requestId,
|
|
705
|
+
op,
|
|
696
706
|
method,
|
|
697
707
|
path,
|
|
698
708
|
message: err instanceof Error ? err.message : String(err),
|
|
@@ -704,6 +714,7 @@ var CoderChatClient = class {
|
|
|
704
714
|
emit?.({
|
|
705
715
|
type: "http:response",
|
|
706
716
|
id: requestId,
|
|
717
|
+
op,
|
|
707
718
|
method,
|
|
708
719
|
path,
|
|
709
720
|
status: res.status,
|
|
@@ -733,8 +744,8 @@ var CoderChatClient = class {
|
|
|
733
744
|
return void 0;
|
|
734
745
|
}
|
|
735
746
|
}
|
|
736
|
-
async #request(method, path, body, signal) {
|
|
737
|
-
const res = await this.#send(method, path, {
|
|
747
|
+
async #request(op, method, path, body, signal) {
|
|
748
|
+
const res = await this.#send(op, method, path, {
|
|
738
749
|
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
739
750
|
headers: body === void 0 ? void 0 : { "Content-Type": "application/json" },
|
|
740
751
|
signal
|
|
@@ -744,6 +755,7 @@ var CoderChatClient = class {
|
|
|
744
755
|
// --- REST -----------------------------------------------------------------
|
|
745
756
|
listModelConfigs(signal) {
|
|
746
757
|
return this.#request(
|
|
758
|
+
"listModelConfigs",
|
|
747
759
|
"GET",
|
|
748
760
|
`${API_PREFIX}/model-configs`,
|
|
749
761
|
void 0,
|
|
@@ -751,13 +763,14 @@ var CoderChatClient = class {
|
|
|
751
763
|
);
|
|
752
764
|
}
|
|
753
765
|
createChat(req, signal) {
|
|
754
|
-
return this.#request("POST", API_PREFIX, req, signal);
|
|
766
|
+
return this.#request("createChat", "POST", API_PREFIX, req, signal);
|
|
755
767
|
}
|
|
756
768
|
getChat(chatId, signal) {
|
|
757
|
-
return this.#request("GET", `${API_PREFIX}/${chatId}`, void 0, signal);
|
|
769
|
+
return this.#request("getChat", "GET", `${API_PREFIX}/${chatId}`, void 0, signal);
|
|
758
770
|
}
|
|
759
771
|
createChatMessage(chatId, req, signal) {
|
|
760
772
|
return this.#request(
|
|
773
|
+
"createChatMessage",
|
|
761
774
|
"POST",
|
|
762
775
|
`${API_PREFIX}/${chatId}/messages`,
|
|
763
776
|
req,
|
|
@@ -771,6 +784,7 @@ var CoderChatClient = class {
|
|
|
771
784
|
if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
|
|
772
785
|
const q = params.toString();
|
|
773
786
|
return this.#request(
|
|
787
|
+
"getMessages",
|
|
774
788
|
"GET",
|
|
775
789
|
`${API_PREFIX}/${chatId}/messages${q ? `?${q}` : ""}`,
|
|
776
790
|
void 0,
|
|
@@ -778,7 +792,13 @@ var CoderChatClient = class {
|
|
|
778
792
|
);
|
|
779
793
|
}
|
|
780
794
|
submitToolResults(chatId, req, signal) {
|
|
781
|
-
return this.#request(
|
|
795
|
+
return this.#request(
|
|
796
|
+
"submitToolResults",
|
|
797
|
+
"POST",
|
|
798
|
+
`${API_PREFIX}/${chatId}/tool-results`,
|
|
799
|
+
req,
|
|
800
|
+
signal
|
|
801
|
+
);
|
|
782
802
|
}
|
|
783
803
|
/**
|
|
784
804
|
* Interrupt the chat's in-flight run (`POST /interrupt`). The server flips
|
|
@@ -797,18 +817,42 @@ var CoderChatClient = class {
|
|
|
797
817
|
const opts = optsOrSignal instanceof AbortSignal ? { signal: optsOrSignal } : optsOrSignal;
|
|
798
818
|
const query = opts?.wait ? "?wait=true" : "";
|
|
799
819
|
return this.#request(
|
|
820
|
+
"interruptChat",
|
|
800
821
|
"POST",
|
|
801
822
|
`${API_PREFIX}/${chatId}/interrupt${query}`,
|
|
802
823
|
void 0,
|
|
803
824
|
opts?.signal
|
|
804
825
|
);
|
|
805
826
|
}
|
|
827
|
+
/**
|
|
828
|
+
* Withdraw a QUEUED (not yet materialized) message from the chat's
|
|
829
|
+
* submission queue (`DELETE /queue/{id}`). `queuedMessageId` is the
|
|
830
|
+
* queue-entry id from `CreateChatMessageResponse.queued_message` — a
|
|
831
|
+
* different id space from committed chat message ids. Rejects with a
|
|
832
|
+
* {@link CoderApiError}: 404 when the entry no longer exists (already
|
|
833
|
+
* promoted into history, or deleted), 409 when the chat has no queue.
|
|
834
|
+
*/
|
|
835
|
+
deleteQueuedMessage(chatId, queuedMessageId, signal) {
|
|
836
|
+
return this.#request(
|
|
837
|
+
"deleteQueuedMessage",
|
|
838
|
+
"DELETE",
|
|
839
|
+
`${API_PREFIX}/${chatId}/queue/${queuedMessageId}`,
|
|
840
|
+
void 0,
|
|
841
|
+
signal
|
|
842
|
+
);
|
|
843
|
+
}
|
|
806
844
|
updateChat(chatId, req, signal) {
|
|
807
|
-
return this.#request("PATCH", `${API_PREFIX}/${chatId}`, req, signal);
|
|
845
|
+
return this.#request("updateChat", "PATCH", `${API_PREFIX}/${chatId}`, req, signal);
|
|
808
846
|
}
|
|
809
847
|
/** Convenience: archive a chat (soft-hide; safe for cleanup). */
|
|
810
848
|
archiveChat(chatId, signal) {
|
|
811
|
-
return this
|
|
849
|
+
return this.#request(
|
|
850
|
+
"archiveChat",
|
|
851
|
+
"PATCH",
|
|
852
|
+
`${API_PREFIX}/${chatId}`,
|
|
853
|
+
{ archived: true },
|
|
854
|
+
signal
|
|
855
|
+
);
|
|
812
856
|
}
|
|
813
857
|
// --- Files ----------------------------------------------------------------
|
|
814
858
|
/**
|
|
@@ -846,6 +890,7 @@ var CoderChatClient = class {
|
|
|
846
890
|
headers["Content-Disposition"] = `attachment; filename="${ascii}"; filename*=UTF-8''${utf8}`;
|
|
847
891
|
}
|
|
848
892
|
const res = await this.#send(
|
|
893
|
+
"uploadChatFile",
|
|
849
894
|
"POST",
|
|
850
895
|
`${API_PREFIX}/files?organization=${encodeURIComponent(organizationId)}`,
|
|
851
896
|
{ body: resolved.body, headers, signal }
|
|
@@ -863,7 +908,9 @@ var CoderChatClient = class {
|
|
|
863
908
|
}
|
|
864
909
|
/** Download a chat file's bytes and media type by id. */
|
|
865
910
|
async getChatFile(fileId, signal) {
|
|
866
|
-
const res = await this.#send("GET", `${API_PREFIX}/files/${fileId}`, {
|
|
911
|
+
const res = await this.#send("getChatFile", "GET", `${API_PREFIX}/files/${fileId}`, {
|
|
912
|
+
signal
|
|
913
|
+
});
|
|
867
914
|
const buf = await res.arrayBuffer();
|
|
868
915
|
return {
|
|
869
916
|
bytes: new Uint8Array(buf),
|
|
@@ -917,7 +964,13 @@ var CoderChatClient = class {
|
|
|
917
964
|
async resolveModelConfigId(hint, signal) {
|
|
918
965
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
919
966
|
if (UUID_RE.test(hint)) return hint;
|
|
920
|
-
const raw = await this
|
|
967
|
+
const raw = await this.#request(
|
|
968
|
+
"resolveModelConfigId",
|
|
969
|
+
"GET",
|
|
970
|
+
`${API_PREFIX}/model-configs`,
|
|
971
|
+
void 0,
|
|
972
|
+
signal
|
|
973
|
+
);
|
|
921
974
|
const configs = (Array.isArray(raw) ? raw : []).filter(
|
|
922
975
|
(c) => typeof c === "object" && c !== null
|
|
923
976
|
);
|
|
@@ -1312,6 +1365,16 @@ function joinContent(content, kind) {
|
|
|
1312
1365
|
for (const p of content) if (p.type === kind) full += p.text ?? "";
|
|
1313
1366
|
return full;
|
|
1314
1367
|
}
|
|
1368
|
+
function canonicalJson(value) {
|
|
1369
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
1370
|
+
if (value !== null && typeof value === "object") {
|
|
1371
|
+
const entries = Object.entries(value);
|
|
1372
|
+
entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1373
|
+
const body = entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`);
|
|
1374
|
+
return `{${body.join(",")}}`;
|
|
1375
|
+
}
|
|
1376
|
+
return JSON.stringify(value) ?? "null";
|
|
1377
|
+
}
|
|
1315
1378
|
var TurnTranslator = class {
|
|
1316
1379
|
#dynamicToolNames;
|
|
1317
1380
|
#submittedToolCallIds;
|
|
@@ -1362,13 +1425,30 @@ var TurnTranslator = class {
|
|
|
1362
1425
|
* to a segment that already streamed) — they arrive only as replays: the
|
|
1363
1426
|
* initial sync when resuming a chat, or a mid-turn `history_reset` re-send
|
|
1364
1427
|
* of the full history. Ingest skips them entirely so a replay can neither
|
|
1365
|
-
* re-emit earlier turns' content nor inflate this turn's usage.
|
|
1428
|
+
* re-emit earlier turns' content nor inflate this turn's usage. Mutable for
|
|
1429
|
+
* exactly one transition: a QUEUED submission starts at the pre-turn cursor
|
|
1430
|
+
* and advances to the materialized user message's id when the gate anchors
|
|
1431
|
+
* (see {@link QueuedSubmissionGate}) — replayed pre-anchor tail messages
|
|
1432
|
+
* then stay filtered on every redial, which reuses the turn's ORIGINAL
|
|
1433
|
+
* `after_id` and therefore re-delivers them.
|
|
1366
1434
|
*/
|
|
1367
1435
|
#turnCursor;
|
|
1436
|
+
/** The queued-submission attribution gate; undefined once anchored (#114). */
|
|
1437
|
+
#queuedGate;
|
|
1368
1438
|
constructor(opts) {
|
|
1369
1439
|
this.#dynamicToolNames = opts.dynamicToolNames;
|
|
1370
1440
|
this.#turnCursor = opts.turnCursor ?? 0;
|
|
1371
1441
|
this.#submittedToolCallIds = opts.submittedToolCallIds ?? /* @__PURE__ */ new Set();
|
|
1442
|
+
const queued = opts.queuedSubmission;
|
|
1443
|
+
if (queued) {
|
|
1444
|
+
this.#queuedGate = {
|
|
1445
|
+
queuedId: queued.id,
|
|
1446
|
+
expected: queued.content !== void 0 ? canonicalJson(queued.content) : void 0,
|
|
1447
|
+
candidateId: void 0,
|
|
1448
|
+
buffered: [],
|
|
1449
|
+
deadEnd: void 0
|
|
1450
|
+
};
|
|
1451
|
+
}
|
|
1372
1452
|
}
|
|
1373
1453
|
get terminalStatus() {
|
|
1374
1454
|
return this.#terminalStatus;
|
|
@@ -1383,6 +1463,32 @@ var TurnTranslator = class {
|
|
|
1383
1463
|
get error() {
|
|
1384
1464
|
return this.#error;
|
|
1385
1465
|
}
|
|
1466
|
+
/**
|
|
1467
|
+
* The turn's EFFECTIVE attribution cursor: the constructor's `turnCursor`
|
|
1468
|
+
* until a queued submission anchors, the materialized user message's id
|
|
1469
|
+
* after. Distinct from the stream's redial cursor, which deliberately stays
|
|
1470
|
+
* the turn's original `after_id` (see `streamChatEvents`).
|
|
1471
|
+
*/
|
|
1472
|
+
get turnCursor() {
|
|
1473
|
+
return this.#turnCursor;
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Whether a queued submission is still awaiting its own user-message
|
|
1477
|
+
* snapshot (#114). While true, nothing is attributed to this turn — no
|
|
1478
|
+
* content, usage, tool calls, or settle statuses — because the stream
|
|
1479
|
+
* window still belongs to the chat's concurrent run.
|
|
1480
|
+
*/
|
|
1481
|
+
get awaitingQueuedSubmission() {
|
|
1482
|
+
return this.#queuedGate !== void 0;
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Set when a queued submission's wait can never complete (see
|
|
1486
|
+
* {@link QueuedSubmissionGate}); the model layer throws it so the turn
|
|
1487
|
+
* fails fast instead of hanging until timeout/abort.
|
|
1488
|
+
*/
|
|
1489
|
+
get queuedSubmissionDeadEnd() {
|
|
1490
|
+
return this.#queuedGate?.deadEnd;
|
|
1491
|
+
}
|
|
1386
1492
|
// --- block helpers --------------------------------------------------------
|
|
1387
1493
|
#openText(out) {
|
|
1388
1494
|
if (this.#reasoning.id) this.#closeReasoning(out);
|
|
@@ -1525,6 +1631,8 @@ var TurnTranslator = class {
|
|
|
1525
1631
|
}
|
|
1526
1632
|
// --- ingest ---------------------------------------------------------------
|
|
1527
1633
|
ingest(ev) {
|
|
1634
|
+
const gate = this.#queuedGate;
|
|
1635
|
+
if (gate) return this.#ingestGated(gate, ev);
|
|
1528
1636
|
const out = [];
|
|
1529
1637
|
switch (ev.type) {
|
|
1530
1638
|
case "message_part":
|
|
@@ -1573,6 +1681,82 @@ var TurnTranslator = class {
|
|
|
1573
1681
|
}
|
|
1574
1682
|
return out;
|
|
1575
1683
|
}
|
|
1684
|
+
/**
|
|
1685
|
+
* Ingest while the queued-submission gate is up (#114): the stream window
|
|
1686
|
+
* still belongs to the chat's concurrent run, so nothing is attributed —
|
|
1687
|
+
* this only advances cursor bookkeeping, runs the anchor correlation
|
|
1688
|
+
* protocol (see {@link QueuedSubmissionGate}), and detects dead ends.
|
|
1689
|
+
* Returns parts only at the anchoring transition, when buffered snapshots
|
|
1690
|
+
* of this turn's own already-committed messages are replayed through the
|
|
1691
|
+
* normal path.
|
|
1692
|
+
*/
|
|
1693
|
+
#ingestGated(gate, ev) {
|
|
1694
|
+
switch (ev.type) {
|
|
1695
|
+
case "message": {
|
|
1696
|
+
const message = ev.message;
|
|
1697
|
+
if (!message) break;
|
|
1698
|
+
if (message.id > this.#maxMessageId) this.#maxMessageId = message.id;
|
|
1699
|
+
if (message.id <= this.#turnCursor) break;
|
|
1700
|
+
if (message.role === "user" && this.#matchesQueuedContent(gate, message)) {
|
|
1701
|
+
if (gate.queuedId === void 0) return this.#anchorQueuedTurn(message.id);
|
|
1702
|
+
gate.candidateId = message.id;
|
|
1703
|
+
gate.buffered = [];
|
|
1704
|
+
} else if (gate.candidateId !== void 0) {
|
|
1705
|
+
gate.buffered.push(message);
|
|
1706
|
+
}
|
|
1707
|
+
break;
|
|
1708
|
+
}
|
|
1709
|
+
case "queue_update": {
|
|
1710
|
+
if (gate.queuedId === void 0) break;
|
|
1711
|
+
const queue = ev.queued_messages ?? [];
|
|
1712
|
+
if (queue.some((entry) => entry.id === gate.queuedId)) {
|
|
1713
|
+
gate.candidateId = void 0;
|
|
1714
|
+
gate.buffered = [];
|
|
1715
|
+
break;
|
|
1716
|
+
}
|
|
1717
|
+
if (gate.candidateId !== void 0) {
|
|
1718
|
+
const buffered = gate.buffered;
|
|
1719
|
+
const out = this.#anchorQueuedTurn(gate.candidateId);
|
|
1720
|
+
for (const message of buffered) this.#ingestMessage(out, message);
|
|
1721
|
+
return out;
|
|
1722
|
+
}
|
|
1723
|
+
gate.deadEnd = {
|
|
1724
|
+
message: "The queued submission left the chat's queue without materializing as this turn's user message (deleted externally, or its materialized form could not be correlated); this turn never started.",
|
|
1725
|
+
kind: "queued_submission_lost",
|
|
1726
|
+
retryable: false
|
|
1727
|
+
};
|
|
1728
|
+
break;
|
|
1729
|
+
}
|
|
1730
|
+
case "error": {
|
|
1731
|
+
if (!ev.error) break;
|
|
1732
|
+
gate.deadEnd = {
|
|
1733
|
+
message: "The chat's active run failed while this turn's submission was queued behind it; the queued message never materialized, so this turn never started.",
|
|
1734
|
+
detail: ev.error.detail ? `${ev.error.message}: ${ev.error.detail}` : ev.error.message,
|
|
1735
|
+
kind: "queued_submission_lost",
|
|
1736
|
+
retryable: false
|
|
1737
|
+
};
|
|
1738
|
+
break;
|
|
1739
|
+
}
|
|
1740
|
+
default:
|
|
1741
|
+
break;
|
|
1742
|
+
}
|
|
1743
|
+
return [];
|
|
1744
|
+
}
|
|
1745
|
+
/** Whether a user-message snapshot's content matches the queued entry's. */
|
|
1746
|
+
#matchesQueuedContent(gate, message) {
|
|
1747
|
+
if (gate.expected === void 0) return true;
|
|
1748
|
+
return canonicalJson(message.content ?? []) === gate.expected;
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Opens the turn's attribution window at `anchorId` — this turn's own user
|
|
1752
|
+
* message — and drops the gate. Replays of the pre-anchor tail (redials
|
|
1753
|
+
* reuse the turn's ORIGINAL cursor) stay filtered by the advanced cursor.
|
|
1754
|
+
*/
|
|
1755
|
+
#anchorQueuedTurn(anchorId) {
|
|
1756
|
+
this.#turnCursor = anchorId;
|
|
1757
|
+
this.#queuedGate = void 0;
|
|
1758
|
+
return [];
|
|
1759
|
+
}
|
|
1576
1760
|
#ingestMessagePart(out, ev) {
|
|
1577
1761
|
const mp = ev.message_part;
|
|
1578
1762
|
if (!mp || mp.role !== "assistant" && mp.role !== "tool") return;
|
|
@@ -1752,6 +1936,25 @@ var CoderLanguageModel = class {
|
|
|
1752
1936
|
supportedUrls = {};
|
|
1753
1937
|
#config;
|
|
1754
1938
|
#chatId;
|
|
1939
|
+
/**
|
|
1940
|
+
* The last chat id this model created or attached to, retained as a CLEANUP
|
|
1941
|
+
* target — see {@link lastKnownChatId}. Unlike `#chatId` it survives
|
|
1942
|
+
* {@link resetSession}, and is only replaced when a later turn's chat
|
|
1943
|
+
* supersedes it or cleared by {@link noteChatCleanedUp} once cleanup
|
|
1944
|
+
* actually succeeded against it.
|
|
1945
|
+
*/
|
|
1946
|
+
#lastKnownChatId;
|
|
1947
|
+
/**
|
|
1948
|
+
* Every chat id stranded by an AUTOMATIC session discard (a stream failure
|
|
1949
|
+
* on a chat the failed turn itself created) and not yet cleaned up. A
|
|
1950
|
+
* single `#lastKnownChatId` slot is not enough here: each AI-SDK
|
|
1951
|
+
* `maxRetries` attempt strands its own fresh chat and only the final
|
|
1952
|
+
* attempt's error reaches the caller, so without this ledger every attempt
|
|
1953
|
+
* before the last would leak unnamed. Drained by {@link noteChatCleanedUp}.
|
|
1954
|
+
* Deliberate abandonment ({@link resetSession}) does NOT record here — the
|
|
1955
|
+
* caller chose to walk away and may want the chat kept.
|
|
1956
|
+
*/
|
|
1957
|
+
#strandedChatIds = /* @__PURE__ */ new Set();
|
|
1755
1958
|
#lastSeenMessageId = 0;
|
|
1756
1959
|
#resolvedModelConfigId;
|
|
1757
1960
|
#modelResolved = false;
|
|
@@ -1767,15 +1970,80 @@ var CoderLanguageModel = class {
|
|
|
1767
1970
|
this.#config = config;
|
|
1768
1971
|
this.modelId = config.model ?? "chatd";
|
|
1769
1972
|
this.#chatId = config.chatId;
|
|
1973
|
+
this.#lastKnownChatId = config.chatId;
|
|
1974
|
+
if (config.lastSeenMessageId !== void 0) {
|
|
1975
|
+
if (!Number.isSafeInteger(config.lastSeenMessageId) || config.lastSeenMessageId <= 0) {
|
|
1976
|
+
throw new CoderAgentError(
|
|
1977
|
+
`lastSeenMessageId must be a positive integer (got ${config.lastSeenMessageId}); persist the value of the lastSeenMessageId getter and supply it verbatim.`
|
|
1978
|
+
);
|
|
1979
|
+
}
|
|
1980
|
+
if (!config.chatId) {
|
|
1981
|
+
throw new CoderAgentError(
|
|
1982
|
+
"lastSeenMessageId requires chatId: a resume cursor is meaningless without the chat it belongs to."
|
|
1983
|
+
);
|
|
1984
|
+
}
|
|
1985
|
+
this.#lastSeenMessageId = config.lastSeenMessageId;
|
|
1986
|
+
}
|
|
1770
1987
|
this.#emitTransportEvent = safeTransportEmitter(config.onTransportEvent);
|
|
1771
1988
|
}
|
|
1772
1989
|
get chatId() {
|
|
1773
1990
|
return this.#chatId;
|
|
1774
1991
|
}
|
|
1992
|
+
/**
|
|
1993
|
+
* The resume cursor: the newest chat message id this session has observed,
|
|
1994
|
+
* or `undefined` before any turn has streamed (nothing to persist then —
|
|
1995
|
+
* a fresh instance seeds from history instead). Persist it alongside
|
|
1996
|
+
* {@link chatId} once a turn settles and supply both to a future instance
|
|
1997
|
+
* ({@link CoderLanguageModelConfig.lastSeenMessageId}) to skip the resumed
|
|
1998
|
+
* turn's pre-prompt seed GET. Read it between turns, not mid-generation:
|
|
1999
|
+
* the cursor advances when a segment ends.
|
|
2000
|
+
*/
|
|
2001
|
+
get lastSeenMessageId() {
|
|
2002
|
+
return this.#lastSeenMessageId > 0 ? this.#lastSeenMessageId : void 0;
|
|
2003
|
+
}
|
|
2004
|
+
/**
|
|
2005
|
+
* The last chat id this model created or attached to, for CLEANUP targeting
|
|
2006
|
+
* only — generation always targets {@link chatId}. It survives
|
|
2007
|
+
* {@link resetSession} (including the automatic session discard after a
|
|
2008
|
+
* fresh-chat stream failure, which clears {@link chatId} so a retry starts
|
|
2009
|
+
* fresh while the failed chat still exists server-side), is superseded when
|
|
2010
|
+
* a later turn creates or attaches to another chat, and is cleared via
|
|
2011
|
+
* {@link noteChatCleanedUp} once cleanup actually succeeded against it.
|
|
2012
|
+
*/
|
|
2013
|
+
get lastKnownChatId() {
|
|
2014
|
+
return this.#lastKnownChatId;
|
|
2015
|
+
}
|
|
2016
|
+
/**
|
|
2017
|
+
* Chat ids stranded by automatic session discards and not yet cleaned up,
|
|
2018
|
+
* oldest first. Each retryable fresh-chat stream failure strands one chat;
|
|
2019
|
+
* under AI-SDK `maxRetries` several can accumulate while only the final
|
|
2020
|
+
* attempt's error (and {@link lastKnownChatId}) names the newest —
|
|
2021
|
+
* `CoderAgent.archive()` retires all of them. Ids leave the list only via
|
|
2022
|
+
* {@link noteChatCleanedUp}.
|
|
2023
|
+
*/
|
|
2024
|
+
get strandedChatIds() {
|
|
2025
|
+
return [...this.#strandedChatIds];
|
|
2026
|
+
}
|
|
2027
|
+
/**
|
|
2028
|
+
* Forgets `chatId` as a cleanup target — call it only after cleanup
|
|
2029
|
+
* actually succeeded against that chat (e.g. `CoderAgent.archive()`
|
|
2030
|
+
* archived it), so later cleanup calls don't re-target an already-archived
|
|
2031
|
+
* chat: removes it from {@link strandedChatIds} and, when it is the
|
|
2032
|
+
* last-known id, clears {@link lastKnownChatId} (match-guarded: a stale
|
|
2033
|
+
* completion must not erase a newer chat's id). The session field
|
|
2034
|
+
* ({@link chatId}) is deliberately untouched — archiving does not reset the
|
|
2035
|
+
* session.
|
|
2036
|
+
*/
|
|
2037
|
+
noteChatCleanedUp(chatId) {
|
|
2038
|
+
this.#strandedChatIds.delete(chatId);
|
|
2039
|
+
if (this.#lastKnownChatId === chatId) this.#lastKnownChatId = void 0;
|
|
2040
|
+
}
|
|
1775
2041
|
/**
|
|
1776
2042
|
* Drops the current session so the next turn creates a fresh chat. Also
|
|
1777
2043
|
* closes the retained event stream, if any (fire-and-forget — a sync reset
|
|
1778
2044
|
* must not wait on socket teardown, and `close()` never rejects).
|
|
2045
|
+
* {@link lastKnownChatId} deliberately survives the reset: the dropped chat
|
|
2046
|
+
* still exists server-side and cleanup must still be able to target it.
|
|
1779
2047
|
*/
|
|
1780
2048
|
resetSession() {
|
|
1781
2049
|
this.#chatId = void 0;
|
|
@@ -1886,9 +2154,9 @@ var CoderLanguageModel = class {
|
|
|
1886
2154
|
* pending calls — nothing to recover, the caller keeps waiting on the
|
|
1887
2155
|
* stream, bounded by `requestTimeoutMs`/abort.
|
|
1888
2156
|
*/
|
|
1889
|
-
async #recoverRequiresAction(chatId, dynamicNames,
|
|
2157
|
+
async #recoverRequiresAction(chatId, dynamicNames, turnCursor, signal) {
|
|
1890
2158
|
const { messages } = await this.#config.client.getMessages(chatId, { limit: 200 }, signal);
|
|
1891
|
-
const turnMessages = messages.filter((m) => m.id >
|
|
2159
|
+
const turnMessages = messages.filter((m) => m.id > turnCursor).sort((a, b) => a.id - b.id);
|
|
1892
2160
|
const lastAssistant = turnMessages.findLast((m) => m.role === "assistant");
|
|
1893
2161
|
if (!lastAssistant) return [];
|
|
1894
2162
|
const handled = /* @__PURE__ */ new Set();
|
|
@@ -2042,10 +2310,20 @@ var CoderLanguageModel = class {
|
|
|
2042
2310
|
let discardSession = false;
|
|
2043
2311
|
let interruptSent = false;
|
|
2044
2312
|
let turnChatId;
|
|
2313
|
+
let queuedTurn = false;
|
|
2314
|
+
let queuedEntry;
|
|
2045
2315
|
const interrupt = () => {
|
|
2046
2316
|
const id = turnChatId ?? this.#chatId;
|
|
2047
2317
|
if (interruptSent || !id) return;
|
|
2048
2318
|
interruptSent = true;
|
|
2319
|
+
if (queuedTurn && (translator === void 0 || translator.awaitingQueuedSubmission)) {
|
|
2320
|
+
const entryId = queuedEntry?.id;
|
|
2321
|
+
if (entryId !== void 0) {
|
|
2322
|
+
void this.#config.client.deleteQueuedMessage(id, entryId).catch(() => {
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2049
2327
|
void this.#config.client.interruptChat(id).catch(() => {
|
|
2050
2328
|
});
|
|
2051
2329
|
};
|
|
@@ -2129,7 +2407,15 @@ var CoderLanguageModel = class {
|
|
|
2129
2407
|
},
|
|
2130
2408
|
signal
|
|
2131
2409
|
);
|
|
2132
|
-
|
|
2410
|
+
if (resp.message) {
|
|
2411
|
+
afterId = resp.message.id;
|
|
2412
|
+
} else if (resp.queued) {
|
|
2413
|
+
afterId = this.#lastSeenMessageId;
|
|
2414
|
+
queuedTurn = true;
|
|
2415
|
+
queuedEntry = resp.queued_message;
|
|
2416
|
+
} else {
|
|
2417
|
+
afterId = this.#lastSeenMessageId;
|
|
2418
|
+
}
|
|
2133
2419
|
}
|
|
2134
2420
|
} else {
|
|
2135
2421
|
if (!this.#chatId)
|
|
@@ -2145,16 +2431,24 @@ var CoderLanguageModel = class {
|
|
|
2145
2431
|
}
|
|
2146
2432
|
const chatId = this.#chatId;
|
|
2147
2433
|
turnChatId = chatId;
|
|
2434
|
+
this.#lastKnownChatId = chatId;
|
|
2148
2435
|
if (segmentChat) segmentChat.chatId = chatId;
|
|
2149
2436
|
const dynamicNames = dynamicToolNames(options.tools);
|
|
2150
|
-
|
|
2437
|
+
const translatorOptions = {
|
|
2151
2438
|
dynamicToolNames: dynamicNames,
|
|
2152
2439
|
turnCursor: afterId ?? 0,
|
|
2153
2440
|
// The retained stream redials with the TURN's original cursor, so a
|
|
2154
2441
|
// reconnect can replay an earlier segment's `action_required`
|
|
2155
2442
|
// snapshot; calls this session already answered must not run again.
|
|
2156
2443
|
submittedToolCallIds: this.#submittedToolCallIds
|
|
2157
|
-
}
|
|
2444
|
+
};
|
|
2445
|
+
if (queuedTurn) {
|
|
2446
|
+
const queuedSubmission = {};
|
|
2447
|
+
if (queuedEntry?.id !== void 0) queuedSubmission.id = queuedEntry.id;
|
|
2448
|
+
if (queuedEntry?.content !== void 0) queuedSubmission.content = queuedEntry.content;
|
|
2449
|
+
translatorOptions.queuedSubmission = queuedSubmission;
|
|
2450
|
+
}
|
|
2451
|
+
translator = new TurnTranslator(translatorOptions);
|
|
2158
2452
|
let sinceRequiresAction = 0;
|
|
2159
2453
|
let grace;
|
|
2160
2454
|
let recovery;
|
|
@@ -2195,7 +2489,11 @@ var CoderLanguageModel = class {
|
|
|
2195
2489
|
recovery = this.#recoverRequiresAction(
|
|
2196
2490
|
chatId,
|
|
2197
2491
|
dynamicNames,
|
|
2198
|
-
|
|
2492
|
+
// The turn's EFFECTIVE cursor: for a queued submission it has
|
|
2493
|
+
// advanced past the materialized user message (a pause can
|
|
2494
|
+
// only arm after the gate anchored), so the derivation never
|
|
2495
|
+
// sees the concurrent predecessor run's tail (#114).
|
|
2496
|
+
translator.turnCursor,
|
|
2199
2497
|
signal ? AbortSignal.any([signal, recoveryAbort.signal]) : recoveryAbort.signal
|
|
2200
2498
|
).catch(() => []);
|
|
2201
2499
|
continue;
|
|
@@ -2219,6 +2517,8 @@ var CoderLanguageModel = class {
|
|
|
2219
2517
|
stream.consumed(next);
|
|
2220
2518
|
if (raced.done) break;
|
|
2221
2519
|
const parts = translator.ingest(raced.value);
|
|
2520
|
+
const deadEnd = translator.queuedSubmissionDeadEnd;
|
|
2521
|
+
if (deadEnd) throw new CoderChatError(deadEnd);
|
|
2222
2522
|
if (raced.value.type === "error" && translator.error && !translator.terminalStatus) {
|
|
2223
2523
|
const peek = stream.read();
|
|
2224
2524
|
const bound = startGraceTimer(0);
|
|
@@ -2243,7 +2543,11 @@ var CoderLanguageModel = class {
|
|
|
2243
2543
|
} catch (err) {
|
|
2244
2544
|
throwIfAborted();
|
|
2245
2545
|
if (err instanceof CoderStreamError) {
|
|
2546
|
+
if (err.chatId === void 0 && turnChatId !== void 0) {
|
|
2547
|
+
err.chatId = turnChatId;
|
|
2548
|
+
}
|
|
2246
2549
|
if (turnCreatedChat) {
|
|
2550
|
+
if (turnChatId) this.#strandedChatIds.add(turnChatId);
|
|
2247
2551
|
discardSession = true;
|
|
2248
2552
|
}
|
|
2249
2553
|
const effectful = chatHasWorkspace || chatHasMcpServers || uploadedAttachment;
|
|
@@ -2252,7 +2556,8 @@ var CoderLanguageModel = class {
|
|
|
2252
2556
|
message: `${err.message}; automatic retry is disabled because ${turnCreatedChat ? "the turn has server-side effects a replay would repeat (workspace/MCP tools that may have executed, or freshly uploaded attachments)" : "the chat has prior state (a retry would resubmit this turn's prompt as a new user turn)"}`,
|
|
2253
2557
|
url: err.url,
|
|
2254
2558
|
cause: err,
|
|
2255
|
-
isRetryable: false
|
|
2559
|
+
isRetryable: false,
|
|
2560
|
+
chatId: err.chatId
|
|
2256
2561
|
});
|
|
2257
2562
|
}
|
|
2258
2563
|
if (err instanceof CoderAgentError && !(err instanceof CoderApiError) && !(err instanceof CoderChatError)) {
|
|
@@ -2503,6 +2808,7 @@ var CoderAgent = class {
|
|
|
2503
2808
|
mcpServerIds: settings.mcpServerIds,
|
|
2504
2809
|
planMode: settings.planMode,
|
|
2505
2810
|
chatId: settings.chatId,
|
|
2811
|
+
lastSeenMessageId: settings.lastSeenMessageId,
|
|
2506
2812
|
requestTimeoutMs: settings.requestTimeoutMs,
|
|
2507
2813
|
onTransportEvent: settings.onTransportEvent
|
|
2508
2814
|
});
|
|
@@ -2531,6 +2837,39 @@ var CoderAgent = class {
|
|
|
2531
2837
|
get chatId() {
|
|
2532
2838
|
return this.#model.chatId;
|
|
2533
2839
|
}
|
|
2840
|
+
/**
|
|
2841
|
+
* The session's resume cursor: the newest chat message id observed, or
|
|
2842
|
+
* `undefined` before any turn has streamed. Persist it alongside
|
|
2843
|
+
* {@link chatId} when a turn ends and supply both to a future instance
|
|
2844
|
+
* ({@link CoderAgentSettings.lastSeenMessageId}) to skip the resumed turn's
|
|
2845
|
+
* pre-prompt seed GET. Read it between turns, not mid-generation.
|
|
2846
|
+
*/
|
|
2847
|
+
get lastSeenMessageId() {
|
|
2848
|
+
return this.#model.lastSeenMessageId;
|
|
2849
|
+
}
|
|
2850
|
+
/**
|
|
2851
|
+
* The last chat id this agent created or attached to, retained for CLEANUP
|
|
2852
|
+
* even after the session was dropped — a fresh-chat stream failure discards
|
|
2853
|
+
* the session ({@link CoderAgent.chatId} becomes `undefined`, so a retry
|
|
2854
|
+
* starts fresh), but the failed chat still exists server-side.
|
|
2855
|
+
* {@link CoderAgent.archive} and {@link CoderAgent.interrupt} fall back to
|
|
2856
|
+
* this id; generation never does. Superseded when a later turn creates or
|
|
2857
|
+
* attaches to another chat; cleared once {@link CoderAgent.archive}
|
|
2858
|
+
* succeeds against it.
|
|
2859
|
+
*/
|
|
2860
|
+
get lastKnownChatId() {
|
|
2861
|
+
return this.#model.lastKnownChatId;
|
|
2862
|
+
}
|
|
2863
|
+
/**
|
|
2864
|
+
* Chat ids stranded by automatic session discards and not yet cleaned up,
|
|
2865
|
+
* oldest first. Each retryable fresh-chat stream failure strands one chat,
|
|
2866
|
+
* so with `maxRetries` > 0 several can accumulate while only the final
|
|
2867
|
+
* attempt's error (and {@link CoderAgent.lastKnownChatId}) names the
|
|
2868
|
+
* newest. {@link CoderAgent.archive} retires all of them.
|
|
2869
|
+
*/
|
|
2870
|
+
get strandedChatIds() {
|
|
2871
|
+
return this.#model.strandedChatIds;
|
|
2872
|
+
}
|
|
2534
2873
|
generate(options) {
|
|
2535
2874
|
return this.#inner.generate(options);
|
|
2536
2875
|
}
|
|
@@ -2546,7 +2885,12 @@ var CoderAgent = class {
|
|
|
2546
2885
|
listModels(signal) {
|
|
2547
2886
|
return this.#client.listModelConfigs(signal);
|
|
2548
2887
|
}
|
|
2549
|
-
/**
|
|
2888
|
+
/**
|
|
2889
|
+
* Start a fresh chatd chat on the next turn. The dropped chat still exists
|
|
2890
|
+
* server-side: {@link CoderAgent.archive} / {@link CoderAgent.interrupt}
|
|
2891
|
+
* keep targeting it (via {@link CoderAgent.lastKnownChatId}) until a new
|
|
2892
|
+
* chat supersedes it or an archive succeeds against it.
|
|
2893
|
+
*/
|
|
2550
2894
|
resetSession() {
|
|
2551
2895
|
this.#model.resetSession();
|
|
2552
2896
|
}
|
|
@@ -2554,37 +2898,67 @@ var CoderAgent = class {
|
|
|
2554
2898
|
* Interrupt the in-flight generation, if any. Resolves as soon as the server
|
|
2555
2899
|
* acknowledges the interrupt — the run keeps winding down asynchronously for
|
|
2556
2900
|
* a few seconds afterwards (see {@link CoderAgent.archive}).
|
|
2901
|
+
*
|
|
2902
|
+
* Targets the session's chat, or — after the session was dropped (a
|
|
2903
|
+
* fresh-chat stream failure, or {@link CoderAgent.resetSession}) — the
|
|
2904
|
+
* {@link CoderAgent.lastKnownChatId}, whose run may still be winding down
|
|
2905
|
+
* server-side. With neither, there is nothing to interrupt and it resolves
|
|
2906
|
+
* `{ interrupted: false }` without a server call (inspect the result rather
|
|
2907
|
+
* than relying on a silent no-op).
|
|
2557
2908
|
*/
|
|
2558
2909
|
async interrupt(opts) {
|
|
2559
|
-
const id = this.#model.chatId;
|
|
2560
|
-
if (!id) return;
|
|
2910
|
+
const id = this.#model.chatId ?? this.#model.lastKnownChatId;
|
|
2911
|
+
if (!id) return { interrupted: false };
|
|
2561
2912
|
await this.#model.closePausedStream();
|
|
2562
2913
|
await this.#client.interruptChat(id, opts?.signal);
|
|
2914
|
+
return { interrupted: true, chatId: id };
|
|
2563
2915
|
}
|
|
2564
2916
|
/**
|
|
2565
2917
|
* Archive the underlying chat (safe cleanup; hides it from listings).
|
|
2566
2918
|
*
|
|
2919
|
+
* Targets the session's chat, or — after the session was dropped (a
|
|
2920
|
+
* fresh-chat stream failure, or {@link CoderAgent.resetSession}) — the
|
|
2921
|
+
* {@link CoderAgent.lastKnownChatId}, so the stranded chat is still cleaned
|
|
2922
|
+
* up; chats stranded by EARLIER automatic discards
|
|
2923
|
+
* ({@link CoderAgent.strandedChatIds} — `maxRetries` can accumulate several)
|
|
2924
|
+
* are retired in the same call. On success each archived id is cleared as a
|
|
2925
|
+
* cleanup target; with no id at all, there is nothing to archive and it
|
|
2926
|
+
* resolves `{ archived: false }` without a server call (inspect the result
|
|
2927
|
+
* rather than relying on a silent no-op).
|
|
2928
|
+
*
|
|
2567
2929
|
* A freshly interrupted/settled chat can keep winding down server-side for a
|
|
2568
2930
|
* few seconds, during which the server rejects archiving with a 409. Those
|
|
2569
|
-
* 409s are retried (~1s apart
|
|
2570
|
-
* `
|
|
2571
|
-
*
|
|
2931
|
+
* 409s are retried (~1s apart — see `settleRetryDelayMs`) inside ONE
|
|
2932
|
+
* `settleDeadlineMs` window (~15s) shared by the whole call — however many
|
|
2933
|
+
* chats it archives — and the last 409 is rethrown if the window closes
|
|
2934
|
+
* first; any other failure, including a caller abort, rethrows immediately.
|
|
2935
|
+
* Chats are archived oldest first, each id cleared as its archive succeeds —
|
|
2936
|
+
* a mid-list failure rethrows and leaves the remaining ids targetable by a
|
|
2937
|
+
* later retry.
|
|
2572
2938
|
*/
|
|
2573
2939
|
async archive(opts) {
|
|
2574
|
-
const
|
|
2575
|
-
if (!
|
|
2940
|
+
const primary = this.#model.chatId ?? this.#model.lastKnownChatId;
|
|
2941
|
+
if (!primary) return { archived: false };
|
|
2576
2942
|
await this.#model.closePausedStream();
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2943
|
+
const targets = [.../* @__PURE__ */ new Set([...this.#model.strandedChatIds, primary])];
|
|
2944
|
+
const deadline = Date.now() + this.#settleDeadlineMs;
|
|
2945
|
+
for (const id of targets) {
|
|
2946
|
+
await archiveWhenSettled(this.#client, id, {
|
|
2947
|
+
deadlineMs: Math.max(deadline - Date.now(), 1),
|
|
2948
|
+
retryDelayMs: this.#settleRetryDelayMs,
|
|
2949
|
+
signal: opts?.signal
|
|
2950
|
+
});
|
|
2951
|
+
this.#model.noteChatCleanedUp(id);
|
|
2952
|
+
}
|
|
2953
|
+
return { archived: true, chatId: primary, archivedChatIds: targets };
|
|
2582
2954
|
}
|
|
2583
2955
|
/**
|
|
2584
2956
|
* Clean up the chat when the agent leaves an `await using` scope, so cleanup
|
|
2585
2957
|
* rides scope exit instead of a separate call you have to remember in a
|
|
2586
2958
|
* `finally`. Interrupts any in-flight server run, then archives the chat
|
|
2587
|
-
* (retrying while the interrupted run settles)
|
|
2959
|
+
* (retrying while the interrupted run settles) — including a chat stranded
|
|
2960
|
+
* by a session discard (see {@link CoderAgent.lastKnownChatId}), which a
|
|
2961
|
+
* failed `generate()` would otherwise leak. Best-effort and bounded
|
|
2588
2962
|
* (~15s overall): disposal errors are swallowed after the bounded attempts
|
|
2589
2963
|
* so they can't mask the scope's own error — call {@link CoderAgent.archive}
|
|
2590
2964
|
* directly when you need guaranteed cleanup.
|