@markusylisiurunen/tau 0.3.44 → 0.3.45
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 +3 -3
- package/dist/core/nook/client.js +11 -4
- package/dist/core/nook/client.js.map +1 -1
- package/dist/core/nook/setup.js +5 -2
- package/dist/core/nook/setup.js.map +1 -1
- package/dist/core/telegram/adapter.js +38 -14
- package/dist/core/telegram/adapter.js.map +1 -1
- package/dist/core/telegram/session_manager.js +404 -32
- package/dist/core/telegram/session_manager.js.map +1 -1
- package/dist/core/tools/web.js +5 -1
- package/dist/core/tools/web.js.map +1 -1
- package/dist/core/version.js +1 -1
- package/dist/nook/README.md +3 -3
- package/dist/nook/worker/index.js +70 -1
- package/dist/nook/worker/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
1
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
2
|
import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
@@ -24,10 +24,49 @@ const persistedTelegramSessionRecordV1Schema = z
|
|
|
24
24
|
tauSessionId: z.string().min(1).optional(),
|
|
25
25
|
})
|
|
26
26
|
.strip();
|
|
27
|
-
const
|
|
27
|
+
const persistedTelegramSessionRecordV2Schema = persistedTelegramSessionRecordV1Schema.extend({
|
|
28
28
|
state: telegramSessionStateValueSchema,
|
|
29
29
|
error: z.string().min(1).optional(),
|
|
30
30
|
});
|
|
31
|
+
const persistedTelegramSessionRecordV3Schema = persistedTelegramSessionRecordV2Schema.extend({
|
|
32
|
+
activeTurnIds: z.array(z.string().min(1)),
|
|
33
|
+
});
|
|
34
|
+
const persistedTelegramTurnFailureSchema = z.discriminatedUnion("status", [
|
|
35
|
+
z
|
|
36
|
+
.object({
|
|
37
|
+
status: z.literal("failed"),
|
|
38
|
+
stopReason: z.literal("error"),
|
|
39
|
+
errorMessage: z.string().optional(),
|
|
40
|
+
})
|
|
41
|
+
.strip(),
|
|
42
|
+
z
|
|
43
|
+
.object({
|
|
44
|
+
status: z.literal("blocked"),
|
|
45
|
+
reason: z.literal("auto-compaction-failed"),
|
|
46
|
+
message: z.string(),
|
|
47
|
+
})
|
|
48
|
+
.strip(),
|
|
49
|
+
]);
|
|
50
|
+
const persistedTelegramTurnNotificationSchema = z.discriminatedUnion("kind", [
|
|
51
|
+
z
|
|
52
|
+
.object({
|
|
53
|
+
kind: z.literal("failed"),
|
|
54
|
+
historyEntryId: z.string().min(1),
|
|
55
|
+
timestamp: z.string().datetime(),
|
|
56
|
+
failure: persistedTelegramTurnFailureSchema,
|
|
57
|
+
})
|
|
58
|
+
.strip(),
|
|
59
|
+
z
|
|
60
|
+
.object({
|
|
61
|
+
kind: z.literal("rejected"),
|
|
62
|
+
historyEntryId: z.string().min(1),
|
|
63
|
+
timestamp: z.string().datetime(),
|
|
64
|
+
})
|
|
65
|
+
.strip(),
|
|
66
|
+
]);
|
|
67
|
+
const persistedTelegramSessionRecordSchema = persistedTelegramSessionRecordV3Schema.extend({
|
|
68
|
+
pendingTurnNotifications: z.array(persistedTelegramTurnNotificationSchema),
|
|
69
|
+
});
|
|
31
70
|
const telegramSessionStateSchema = z.discriminatedUnion("version", [
|
|
32
71
|
z
|
|
33
72
|
.object({
|
|
@@ -38,17 +77,41 @@ const telegramSessionStateSchema = z.discriminatedUnion("version", [
|
|
|
38
77
|
z
|
|
39
78
|
.object({
|
|
40
79
|
version: z.literal(2),
|
|
80
|
+
sessions: z.array(persistedTelegramSessionRecordV2Schema),
|
|
81
|
+
})
|
|
82
|
+
.strip(),
|
|
83
|
+
z
|
|
84
|
+
.object({
|
|
85
|
+
version: z.literal(3),
|
|
86
|
+
sessions: z.array(persistedTelegramSessionRecordV3Schema),
|
|
87
|
+
})
|
|
88
|
+
.strip(),
|
|
89
|
+
z
|
|
90
|
+
.object({
|
|
91
|
+
version: z.literal(4),
|
|
41
92
|
sessions: z.array(persistedTelegramSessionRecordSchema),
|
|
42
93
|
})
|
|
43
94
|
.strip(),
|
|
44
95
|
]);
|
|
45
96
|
function normalizePersistedTelegramSessions(state) {
|
|
46
|
-
if (state.version ===
|
|
97
|
+
if (state.version === 4) {
|
|
47
98
|
return state.sessions;
|
|
48
99
|
}
|
|
100
|
+
if (state.version === 3) {
|
|
101
|
+
return state.sessions.map((record) => ({ ...record, pendingTurnNotifications: [] }));
|
|
102
|
+
}
|
|
103
|
+
if (state.version === 2) {
|
|
104
|
+
return state.sessions.map((record) => ({
|
|
105
|
+
...record,
|
|
106
|
+
activeTurnIds: [],
|
|
107
|
+
pendingTurnNotifications: [],
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
49
110
|
return state.sessions.map((record) => ({
|
|
50
111
|
...record,
|
|
51
112
|
state: record.tauSessionId ? "waiting-input" : "queued",
|
|
113
|
+
activeTurnIds: [],
|
|
114
|
+
pendingTurnNotifications: [],
|
|
52
115
|
}));
|
|
53
116
|
}
|
|
54
117
|
export function resolveTelegramSessionStatePath(workspaceRoot) {
|
|
@@ -59,6 +122,7 @@ const PUBLIC_SESSION_ID_LENGTH = 8;
|
|
|
59
122
|
const NANOSECONDS_PER_MILLISECOND = 1000000n;
|
|
60
123
|
const PROVISION_SCRIPT_PATH = ".tau/scripts/provision";
|
|
61
124
|
const MAX_PROVISION_DIAGNOSTIC_CHARS = 2_000;
|
|
125
|
+
const TURN_RECOVERY_POLL_INTERVAL_MS = 1_000;
|
|
62
126
|
const ACTIVE_STATES = new Set([
|
|
63
127
|
"queued",
|
|
64
128
|
"preparing-workspace",
|
|
@@ -215,7 +279,11 @@ class TelegramSessionManagerImpl {
|
|
|
215
279
|
consumedFacetEventCounts: new Map(),
|
|
216
280
|
emittedAssistantMessageIds: new Set(),
|
|
217
281
|
emittedNoticeIds: new Set(),
|
|
218
|
-
|
|
282
|
+
activeTurnIds: new Set(),
|
|
283
|
+
settledTurnIds: new Set(),
|
|
284
|
+
pendingTurnNotifications: new Map(),
|
|
285
|
+
emittedTurnNotificationIds: new Set(),
|
|
286
|
+
turnChangeResolvers: new Map(),
|
|
219
287
|
};
|
|
220
288
|
this.sessions.set(id, entry);
|
|
221
289
|
try {
|
|
@@ -259,6 +327,21 @@ class TelegramSessionManagerImpl {
|
|
|
259
327
|
}
|
|
260
328
|
return (this.provisionFailures.get(sessionId) ?? []).map((failure) => ({ ...failure }));
|
|
261
329
|
}
|
|
330
|
+
getPendingTurnNotifications(sessionId) {
|
|
331
|
+
const entry = this.getEntryBySessionId(sessionId);
|
|
332
|
+
if (!entry) {
|
|
333
|
+
return [];
|
|
334
|
+
}
|
|
335
|
+
return Array.from(entry.pendingTurnNotifications.values(), (notification) => this.toTurnNotificationEvent(entry, notification));
|
|
336
|
+
}
|
|
337
|
+
async acknowledgeTurnNotification(sessionId, historyEntryId) {
|
|
338
|
+
const entry = this.requireSession(sessionId);
|
|
339
|
+
if (!entry.pendingTurnNotifications.delete(historyEntryId)) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
entry.emittedTurnNotificationIds.delete(historyEntryId);
|
|
343
|
+
await this.persistSessions();
|
|
344
|
+
}
|
|
262
345
|
async getSessionSnapshot(sessionId) {
|
|
263
346
|
const entry = this.getEntryBySessionId(sessionId);
|
|
264
347
|
if (!entry?.tauSession) {
|
|
@@ -284,8 +367,10 @@ class TelegramSessionManagerImpl {
|
|
|
284
367
|
if (!entry.tauSession) {
|
|
285
368
|
throw new TelegramSessionManagerError("not_ready", "session is still preparing");
|
|
286
369
|
}
|
|
287
|
-
const
|
|
288
|
-
|
|
370
|
+
const requestedMode = options?.mode ?? "submit";
|
|
371
|
+
const running = entry.record.state === "running" || Boolean(entry.activeSubmit);
|
|
372
|
+
const mode = requestedMode === "auto" ? (running ? "steer" : "submit") : requestedMode;
|
|
373
|
+
if (mode !== "steer" && running) {
|
|
289
374
|
throw new TelegramSessionManagerError("busy", "session is running");
|
|
290
375
|
}
|
|
291
376
|
void this.submitText(entry, trimmed, "user-message", options?.additionalSystemMessage, mode);
|
|
@@ -293,7 +378,8 @@ class TelegramSessionManagerImpl {
|
|
|
293
378
|
}
|
|
294
379
|
async interruptSession(sessionId) {
|
|
295
380
|
const entry = this.requireSession(sessionId);
|
|
296
|
-
if (entry.record.state !== "running" ||
|
|
381
|
+
if (entry.record.state !== "running" ||
|
|
382
|
+
(!entry.activeSubmit && entry.activeTurnIds.size === 0)) {
|
|
297
383
|
return {
|
|
298
384
|
session: this.toRecord(entry),
|
|
299
385
|
interrupted: false,
|
|
@@ -381,8 +467,9 @@ class TelegramSessionManagerImpl {
|
|
|
381
467
|
if (!project || this.sessions.has(record.id)) {
|
|
382
468
|
continue;
|
|
383
469
|
}
|
|
470
|
+
const { activeTurnIds, pendingTurnNotifications, ...sessionRecord } = record;
|
|
384
471
|
const entry = {
|
|
385
|
-
record:
|
|
472
|
+
record: sessionRecord,
|
|
386
473
|
logs: [],
|
|
387
474
|
project,
|
|
388
475
|
abortController: new AbortController(),
|
|
@@ -390,13 +477,20 @@ class TelegramSessionManagerImpl {
|
|
|
390
477
|
consumedFacetEventCounts: new Map(),
|
|
391
478
|
emittedAssistantMessageIds: new Set(),
|
|
392
479
|
emittedNoticeIds: new Set(),
|
|
393
|
-
|
|
480
|
+
activeTurnIds: new Set(activeTurnIds),
|
|
481
|
+
settledTurnIds: new Set(),
|
|
482
|
+
pendingTurnNotifications: new Map(pendingTurnNotifications.map((notification) => [
|
|
483
|
+
notification.historyEntryId,
|
|
484
|
+
notification,
|
|
485
|
+
])),
|
|
486
|
+
emittedTurnNotificationIds: new Set(),
|
|
487
|
+
turnChangeResolvers: new Map(),
|
|
394
488
|
};
|
|
395
489
|
this.sessions.set(record.id, entry);
|
|
396
490
|
}
|
|
397
491
|
await this.cleanupOrphanedWorkspaces();
|
|
398
492
|
await Promise.all(Array.from(this.sessions.values(), async (entry) => {
|
|
399
|
-
if (entry.record.state === "failed") {
|
|
493
|
+
if (entry.record.state === "failed" && entry.activeTurnIds.size === 0) {
|
|
400
494
|
return;
|
|
401
495
|
}
|
|
402
496
|
try {
|
|
@@ -415,6 +509,9 @@ class TelegramSessionManagerImpl {
|
|
|
415
509
|
}
|
|
416
510
|
}));
|
|
417
511
|
await this.persistSessions();
|
|
512
|
+
for (const entry of this.sessions.values()) {
|
|
513
|
+
this.emitPendingTurnNotifications(entry);
|
|
514
|
+
}
|
|
418
515
|
}
|
|
419
516
|
async cleanupOrphanedWorkspaces() {
|
|
420
517
|
const workspaceRoots = new Set([this.workspaceRoot]);
|
|
@@ -498,7 +595,13 @@ class TelegramSessionManagerImpl {
|
|
|
498
595
|
});
|
|
499
596
|
this.initializeRecoveredSnapshotDeliveryState(entry, await tauSession.snapshot());
|
|
500
597
|
entry.record.error = undefined;
|
|
501
|
-
|
|
598
|
+
if (entry.activeTurnIds.size > 0) {
|
|
599
|
+
this.setState(entry, "running");
|
|
600
|
+
this.trackRecoveredTurns(entry, tauSession);
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
this.setState(entry, "waiting-input");
|
|
604
|
+
}
|
|
502
605
|
this.log(entry, "info", "session recovered", { tauSessionId, workspacePath });
|
|
503
606
|
if (shouldPrepareWorkspace) {
|
|
504
607
|
this.startProvision(entry, tauSession, provisionTargets);
|
|
@@ -513,7 +616,7 @@ class TelegramSessionManagerImpl {
|
|
|
513
616
|
.catch(() => undefined)
|
|
514
617
|
.then(async () => {
|
|
515
618
|
const state = {
|
|
516
|
-
version:
|
|
619
|
+
version: 4,
|
|
517
620
|
sessions: Array.from(this.sessions.values(), (entry) => ({
|
|
518
621
|
id: entry.record.id,
|
|
519
622
|
projectId: entry.record.projectId,
|
|
@@ -523,6 +626,8 @@ class TelegramSessionManagerImpl {
|
|
|
523
626
|
updatedAt: entry.record.updatedAt,
|
|
524
627
|
...(entry.record.tauSessionId ? { tauSessionId: entry.record.tauSessionId } : {}),
|
|
525
628
|
...(entry.record.error ? { error: entry.record.error } : {}),
|
|
629
|
+
activeTurnIds: [...entry.activeTurnIds],
|
|
630
|
+
pendingTurnNotifications: [...entry.pendingTurnNotifications.values()],
|
|
526
631
|
})),
|
|
527
632
|
};
|
|
528
633
|
await mkdir(dirname(persistencePath), { recursive: true });
|
|
@@ -591,6 +696,7 @@ class TelegramSessionManagerImpl {
|
|
|
591
696
|
this.handleClientEvent(entry, event);
|
|
592
697
|
});
|
|
593
698
|
const snapshot = await tauSession.snapshot();
|
|
699
|
+
this.reconcileSnapshotTurns(entry, snapshot, { rejectMissingActiveTurns: false });
|
|
594
700
|
this.initializeSnapshotMessageDeliveryState(entry, snapshot);
|
|
595
701
|
this.handleSnapshotNotices(entry, snapshot);
|
|
596
702
|
this.log(entry, "info", "session preparation complete", {
|
|
@@ -698,35 +804,57 @@ class TelegramSessionManagerImpl {
|
|
|
698
804
|
const previousSubmit = entry.activeSubmit;
|
|
699
805
|
const tauSession = entry.tauSession;
|
|
700
806
|
const payload = this.buildSubmitPayload(text, additionalSystemMessage);
|
|
807
|
+
const requestedHistoryEntryId = mode === "submit" ? `telegram-turn-${randomUUID()}` : undefined;
|
|
701
808
|
let submitPromise;
|
|
702
809
|
submitPromise = (async () => {
|
|
810
|
+
let requestStarted = false;
|
|
703
811
|
try {
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
812
|
+
if (requestedHistoryEntryId) {
|
|
813
|
+
entry.activeTurnIds.add(requestedHistoryEntryId);
|
|
814
|
+
await this.persistSessions();
|
|
815
|
+
}
|
|
816
|
+
requestStarted = true;
|
|
817
|
+
const result = mode === "steer"
|
|
818
|
+
? await tauSession.steer(payload)
|
|
819
|
+
: await tauSession.submit(payload, { historyEntryId: requestedHistoryEntryId });
|
|
820
|
+
if (requestedHistoryEntryId && result.userHistoryEntryId !== requestedHistoryEntryId) {
|
|
821
|
+
throw new Error(`Tau accepted turn '${result.userHistoryEntryId}' instead of requested turn '${requestedHistoryEntryId}'`);
|
|
822
|
+
}
|
|
823
|
+
this.reconcileTurnRecord(entry, {
|
|
824
|
+
userHistoryEntryId: result.userHistoryEntryId,
|
|
825
|
+
state: "settled",
|
|
826
|
+
outcome: result.turn,
|
|
827
|
+
}, { owned: true });
|
|
828
|
+
await this.persistSessions();
|
|
829
|
+
this.emitPendingTurnNotifications(entry);
|
|
830
|
+
this.log(entry, result.turn.status === "failed" || result.turn.status === "blocked" ? "error" : "info", "message finished", {
|
|
709
831
|
source,
|
|
710
832
|
turn: result.turn,
|
|
711
833
|
userHistoryEntryId: result.userHistoryEntryId,
|
|
712
834
|
});
|
|
713
|
-
if (!entry.cancelRequested
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
type: "session-turn-failed",
|
|
718
|
-
sessionId: entry.record.id,
|
|
719
|
-
projectId: entry.record.projectId,
|
|
720
|
-
timestamp: this.now().toISOString(),
|
|
721
|
-
failure,
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
else if (!failure && (!entry.activeSubmit || entry.activeSubmit === submitPromise)) {
|
|
725
|
-
this.setState(entry, "waiting-input");
|
|
726
|
-
}
|
|
835
|
+
if (!entry.cancelRequested &&
|
|
836
|
+
(!entry.activeSubmit || entry.activeSubmit === submitPromise) &&
|
|
837
|
+
entry.activeTurnIds.size === 0) {
|
|
838
|
+
this.setState(entry, "waiting-input");
|
|
727
839
|
}
|
|
728
840
|
}
|
|
729
841
|
catch (error) {
|
|
842
|
+
if (requestedHistoryEntryId && !requestStarted) {
|
|
843
|
+
entry.activeTurnIds.delete(requestedHistoryEntryId);
|
|
844
|
+
}
|
|
845
|
+
if (!entry.cancelRequested && requestedHistoryEntryId && requestStarted) {
|
|
846
|
+
try {
|
|
847
|
+
if (await this.recoverRejectedSubmit(entry, tauSession, requestedHistoryEntryId, error)) {
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
catch (recoveryError) {
|
|
852
|
+
this.log(entry, "warn", "failed to inspect rejected submit", {
|
|
853
|
+
userHistoryEntryId: requestedHistoryEntryId,
|
|
854
|
+
cause: formatErrorDiagnostic(recoveryError),
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
}
|
|
730
858
|
if (!entry.cancelRequested) {
|
|
731
859
|
const diagnostic = formatErrorDiagnostic(error);
|
|
732
860
|
entry.record.error = diagnostic;
|
|
@@ -765,13 +893,234 @@ class TelegramSessionManagerImpl {
|
|
|
765
893
|
.finally(() => {
|
|
766
894
|
if (entry.activeSubmit === trackedSubmit) {
|
|
767
895
|
entry.activeSubmit = undefined;
|
|
768
|
-
if (!entry.cancelRequested &&
|
|
896
|
+
if (!entry.cancelRequested &&
|
|
897
|
+
entry.record.state === "running" &&
|
|
898
|
+
entry.activeTurnIds.size === 0) {
|
|
769
899
|
this.setState(entry, "waiting-input");
|
|
770
900
|
}
|
|
771
901
|
}
|
|
772
902
|
});
|
|
773
903
|
return submitPromise;
|
|
774
904
|
}
|
|
905
|
+
async recoverRejectedSubmit(entry, tauSession, historyEntryId, error) {
|
|
906
|
+
const snapshot = await tauSession.snapshot();
|
|
907
|
+
const turn = Object.hasOwn(snapshot.turns, historyEntryId)
|
|
908
|
+
? snapshot.turns[historyEntryId]
|
|
909
|
+
: undefined;
|
|
910
|
+
if (!turn) {
|
|
911
|
+
this.recordTurnRejection(entry, historyEntryId);
|
|
912
|
+
await this.persistSessions();
|
|
913
|
+
this.emitPendingTurnNotifications(entry);
|
|
914
|
+
this.log(entry, "warn", "submit rejected before turn acceptance", {
|
|
915
|
+
userHistoryEntryId: historyEntryId,
|
|
916
|
+
cause: formatErrorDiagnostic(error),
|
|
917
|
+
});
|
|
918
|
+
return true;
|
|
919
|
+
}
|
|
920
|
+
this.reconcileTurnRecord(entry, turn);
|
|
921
|
+
await this.persistSessions();
|
|
922
|
+
this.emitPendingTurnNotifications(entry);
|
|
923
|
+
this.log(entry, "warn", "submit response recovered from turn ledger", {
|
|
924
|
+
userHistoryEntryId: historyEntryId,
|
|
925
|
+
turn,
|
|
926
|
+
cause: formatErrorDiagnostic(error),
|
|
927
|
+
});
|
|
928
|
+
if (turn.state === "running") {
|
|
929
|
+
await this.followRunningTurn(entry, tauSession, historyEntryId);
|
|
930
|
+
}
|
|
931
|
+
else if (entry.activeTurnIds.size === 0) {
|
|
932
|
+
this.setState(entry, "waiting-input");
|
|
933
|
+
}
|
|
934
|
+
return true;
|
|
935
|
+
}
|
|
936
|
+
trackRecoveredTurns(entry, tauSession) {
|
|
937
|
+
const trackedTurns = Promise.all([...entry.activeTurnIds].map(async (historyEntryId) => await this.followRunningTurn(entry, tauSession, historyEntryId))).then(() => undefined);
|
|
938
|
+
entry.activeSubmit = trackedTurns;
|
|
939
|
+
void trackedTurns
|
|
940
|
+
.catch((error) => {
|
|
941
|
+
this.log(entry, "error", "recovered turn tracking failed", {
|
|
942
|
+
cause: formatErrorDiagnostic(error),
|
|
943
|
+
});
|
|
944
|
+
})
|
|
945
|
+
.finally(() => {
|
|
946
|
+
if (entry.activeSubmit === trackedTurns) {
|
|
947
|
+
entry.activeSubmit = undefined;
|
|
948
|
+
if (!entry.cancelRequested && entry.record.state === "running") {
|
|
949
|
+
this.setState(entry, "waiting-input");
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
async followRunningTurn(entry, tauSession, historyEntryId) {
|
|
955
|
+
while (entry.activeTurnIds.has(historyEntryId) && !entry.cancelRequested) {
|
|
956
|
+
await this.waitForTurnChange(entry, historyEntryId);
|
|
957
|
+
if (!entry.activeTurnIds.has(historyEntryId) || entry.cancelRequested) {
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
let snapshot;
|
|
961
|
+
try {
|
|
962
|
+
snapshot = await tauSession.snapshot();
|
|
963
|
+
}
|
|
964
|
+
catch (error) {
|
|
965
|
+
this.log(entry, "warn", "failed to refresh recovered turn", {
|
|
966
|
+
userHistoryEntryId: historyEntryId,
|
|
967
|
+
cause: formatErrorDiagnostic(error),
|
|
968
|
+
});
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
const turn = Object.hasOwn(snapshot.turns, historyEntryId)
|
|
972
|
+
? snapshot.turns[historyEntryId]
|
|
973
|
+
: undefined;
|
|
974
|
+
if (!turn) {
|
|
975
|
+
this.recordTurnRejection(entry, historyEntryId);
|
|
976
|
+
}
|
|
977
|
+
else {
|
|
978
|
+
this.reconcileTurnRecord(entry, turn);
|
|
979
|
+
}
|
|
980
|
+
await this.persistSessions();
|
|
981
|
+
this.emitPendingTurnNotifications(entry);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
waitForTurnChange(entry, historyEntryId) {
|
|
985
|
+
return new Promise((resolve) => {
|
|
986
|
+
let settled = false;
|
|
987
|
+
const finish = () => {
|
|
988
|
+
if (settled) {
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
settled = true;
|
|
992
|
+
clearTimeout(timeout);
|
|
993
|
+
entry.abortController.signal.removeEventListener("abort", finish);
|
|
994
|
+
if (entry.turnChangeResolvers.get(historyEntryId) === finish) {
|
|
995
|
+
entry.turnChangeResolvers.delete(historyEntryId);
|
|
996
|
+
}
|
|
997
|
+
resolve();
|
|
998
|
+
};
|
|
999
|
+
const timeout = setTimeout(finish, TURN_RECOVERY_POLL_INTERVAL_MS);
|
|
1000
|
+
entry.turnChangeResolvers.set(historyEntryId, finish);
|
|
1001
|
+
entry.abortController.signal.addEventListener("abort", finish, { once: true });
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
reconcileTurnRecord(entry, turn, options = {}) {
|
|
1005
|
+
const historyEntryId = turn.userHistoryEntryId;
|
|
1006
|
+
if (turn.state === "running") {
|
|
1007
|
+
if (entry.settledTurnIds.has(historyEntryId) || entry.activeTurnIds.has(historyEntryId)) {
|
|
1008
|
+
return false;
|
|
1009
|
+
}
|
|
1010
|
+
entry.activeTurnIds.add(historyEntryId);
|
|
1011
|
+
return true;
|
|
1012
|
+
}
|
|
1013
|
+
if (entry.settledTurnIds.has(historyEntryId)) {
|
|
1014
|
+
return false;
|
|
1015
|
+
}
|
|
1016
|
+
entry.settledTurnIds.add(historyEntryId);
|
|
1017
|
+
const wasActive = entry.activeTurnIds.delete(historyEntryId);
|
|
1018
|
+
if (!wasActive && !options.owned) {
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
entry.turnChangeResolvers.get(historyEntryId)?.();
|
|
1022
|
+
if (turn.outcome.status === "failed" || turn.outcome.status === "blocked") {
|
|
1023
|
+
this.recordTurnFailure(entry, historyEntryId, turn.outcome);
|
|
1024
|
+
}
|
|
1025
|
+
if (entry.activeTurnIds.size === 0 &&
|
|
1026
|
+
!entry.activeSubmit &&
|
|
1027
|
+
!entry.cancelRequested &&
|
|
1028
|
+
entry.record.state === "running") {
|
|
1029
|
+
this.setState(entry, "waiting-input");
|
|
1030
|
+
}
|
|
1031
|
+
return true;
|
|
1032
|
+
}
|
|
1033
|
+
recordTurnFailure(entry, historyEntryId, failure) {
|
|
1034
|
+
if (entry.pendingTurnNotifications.has(historyEntryId)) {
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
entry.pendingTurnNotifications.set(historyEntryId, {
|
|
1038
|
+
kind: "failed",
|
|
1039
|
+
historyEntryId,
|
|
1040
|
+
timestamp: this.now().toISOString(),
|
|
1041
|
+
failure: structuredClone(failure),
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
recordTurnRejection(entry, historyEntryId) {
|
|
1045
|
+
entry.activeTurnIds.delete(historyEntryId);
|
|
1046
|
+
entry.settledTurnIds.add(historyEntryId);
|
|
1047
|
+
entry.turnChangeResolvers.get(historyEntryId)?.();
|
|
1048
|
+
if (!entry.pendingTurnNotifications.has(historyEntryId)) {
|
|
1049
|
+
entry.pendingTurnNotifications.set(historyEntryId, {
|
|
1050
|
+
kind: "rejected",
|
|
1051
|
+
historyEntryId,
|
|
1052
|
+
timestamp: this.now().toISOString(),
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
if (entry.activeTurnIds.size === 0 &&
|
|
1056
|
+
!entry.activeSubmit &&
|
|
1057
|
+
!entry.cancelRequested &&
|
|
1058
|
+
entry.record.state === "running") {
|
|
1059
|
+
this.setState(entry, "waiting-input");
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
toTurnNotificationEvent(entry, notification) {
|
|
1063
|
+
if (notification.kind === "failed") {
|
|
1064
|
+
return {
|
|
1065
|
+
type: "session-turn-failed",
|
|
1066
|
+
sessionId: entry.record.id,
|
|
1067
|
+
projectId: entry.record.projectId,
|
|
1068
|
+
timestamp: notification.timestamp,
|
|
1069
|
+
historyEntryId: notification.historyEntryId,
|
|
1070
|
+
failure: structuredClone(notification.failure),
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
return {
|
|
1074
|
+
type: "session-turn-rejected",
|
|
1075
|
+
sessionId: entry.record.id,
|
|
1076
|
+
projectId: entry.record.projectId,
|
|
1077
|
+
timestamp: notification.timestamp,
|
|
1078
|
+
historyEntryId: notification.historyEntryId,
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
emitPendingTurnNotifications(entry) {
|
|
1082
|
+
for (const notification of entry.pendingTurnNotifications.values()) {
|
|
1083
|
+
if (entry.emittedTurnNotificationIds.has(notification.historyEntryId)) {
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
entry.emittedTurnNotificationIds.add(notification.historyEntryId);
|
|
1087
|
+
this.emit(this.toTurnNotificationEvent(entry, notification));
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
reconcileSnapshotTurns(entry, snapshot, options) {
|
|
1091
|
+
let changed = false;
|
|
1092
|
+
const snapshotTurnIds = new Set(Object.keys(snapshot.turns));
|
|
1093
|
+
if (options.rejectMissingActiveTurns) {
|
|
1094
|
+
for (const historyEntryId of entry.activeTurnIds) {
|
|
1095
|
+
if (!snapshotTurnIds.has(historyEntryId)) {
|
|
1096
|
+
this.recordTurnRejection(entry, historyEntryId);
|
|
1097
|
+
changed = true;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
for (const turn of Object.values(snapshot.turns)) {
|
|
1102
|
+
if (turn.state === "running" || entry.activeTurnIds.has(turn.userHistoryEntryId)) {
|
|
1103
|
+
changed = this.reconcileTurnRecord(entry, turn) || changed;
|
|
1104
|
+
}
|
|
1105
|
+
else {
|
|
1106
|
+
entry.settledTurnIds.add(turn.userHistoryEntryId);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
return changed;
|
|
1110
|
+
}
|
|
1111
|
+
persistTurnState(entry) {
|
|
1112
|
+
void this.persistSessions()
|
|
1113
|
+
.then(() => {
|
|
1114
|
+
this.emitPendingTurnNotifications(entry);
|
|
1115
|
+
})
|
|
1116
|
+
.catch((error) => {
|
|
1117
|
+
this.onLog?.({
|
|
1118
|
+
level: "error",
|
|
1119
|
+
message: "turn state persistence failed",
|
|
1120
|
+
data: { cause: formatErrorDiagnostic(error) },
|
|
1121
|
+
});
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
775
1124
|
buildSessionAttributes(entry) {
|
|
776
1125
|
const configuredRepositories = "repo" in entry.project
|
|
777
1126
|
? [entry.project.repo]
|
|
@@ -1031,6 +1380,11 @@ class TelegramSessionManagerImpl {
|
|
|
1031
1380
|
}
|
|
1032
1381
|
handleClientEvent(entry, clientEvent) {
|
|
1033
1382
|
if (clientEvent.delta.type === "snapshot.reset") {
|
|
1383
|
+
if (this.reconcileSnapshotTurns(entry, clientEvent.delta.snapshot, {
|
|
1384
|
+
rejectMissingActiveTurns: false,
|
|
1385
|
+
})) {
|
|
1386
|
+
this.persistTurnState(entry);
|
|
1387
|
+
}
|
|
1034
1388
|
this.handleSnapshotNotices(entry, clientEvent.delta.snapshot);
|
|
1035
1389
|
if (clientEvent.cause.type !== "assistant-message") {
|
|
1036
1390
|
return;
|
|
@@ -1046,6 +1400,12 @@ class TelegramSessionManagerImpl {
|
|
|
1046
1400
|
return;
|
|
1047
1401
|
}
|
|
1048
1402
|
for (const change of clientEvent.delta.changes) {
|
|
1403
|
+
if (change.type === "turn.set") {
|
|
1404
|
+
if (this.reconcileTurnRecord(entry, change.turn)) {
|
|
1405
|
+
this.persistTurnState(entry);
|
|
1406
|
+
}
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1049
1409
|
if (change.type === "facet.set") {
|
|
1050
1410
|
this.handleFacetProgress(entry, change.facet);
|
|
1051
1411
|
continue;
|
|
@@ -1063,6 +1423,7 @@ class TelegramSessionManagerImpl {
|
|
|
1063
1423
|
}
|
|
1064
1424
|
}
|
|
1065
1425
|
initializeRecoveredSnapshotDeliveryState(entry, snapshot) {
|
|
1426
|
+
this.reconcileSnapshotTurns(entry, snapshot, { rejectMissingActiveTurns: true });
|
|
1066
1427
|
this.initializeSnapshotMessageDeliveryState(entry, snapshot);
|
|
1067
1428
|
for (const item of snapshot.timeline.items) {
|
|
1068
1429
|
if (item.type === "notice") {
|
|
@@ -1232,6 +1593,17 @@ class ScopedTelegramSessionManager {
|
|
|
1232
1593
|
}
|
|
1233
1594
|
return this.sessionManager.getProvisionFailures(sessionId);
|
|
1234
1595
|
}
|
|
1596
|
+
getPendingTurnNotifications(sessionId) {
|
|
1597
|
+
const session = this.sessionManager.getSession(sessionId);
|
|
1598
|
+
if (!session || !this.isVisibleSession(session)) {
|
|
1599
|
+
return [];
|
|
1600
|
+
}
|
|
1601
|
+
return this.sessionManager.getPendingTurnNotifications(sessionId);
|
|
1602
|
+
}
|
|
1603
|
+
async acknowledgeTurnNotification(sessionId, historyEntryId) {
|
|
1604
|
+
this.requireSession(sessionId);
|
|
1605
|
+
await this.sessionManager.acknowledgeTurnNotification(sessionId, historyEntryId);
|
|
1606
|
+
}
|
|
1235
1607
|
async getSessionSnapshot(sessionId) {
|
|
1236
1608
|
this.requireSession(sessionId);
|
|
1237
1609
|
return await this.sessionManager.getSessionSnapshot(sessionId);
|