@prevalentware/opencode-goal-plugin 0.1.32 → 0.1.34
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 +7 -3
- package/dist/server.js +858 -88
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -3,11 +3,120 @@
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
5
5
|
// src/state.ts
|
|
6
|
-
import {
|
|
6
|
+
import { mkdir, readFile } from "fs/promises";
|
|
7
7
|
import { homedir } from "os";
|
|
8
|
-
import { dirname, join } from "path";
|
|
8
|
+
import { dirname as dirname2, join } from "path";
|
|
9
9
|
import { Data, Effect, Schema } from "effect";
|
|
10
10
|
|
|
11
|
+
// src/atomic-write.ts
|
|
12
|
+
import { randomUUID } from "crypto";
|
|
13
|
+
import { chmod, open, rename, unlink } from "fs/promises";
|
|
14
|
+
import { dirname } from "path";
|
|
15
|
+
function isUnsupportedSyncDirError(error, platform) {
|
|
16
|
+
const code = error?.code;
|
|
17
|
+
return code === "EINVAL" || code === "ENOTSUP" || code === "EOPNOTSUPP" || code === "EISDIR" || platform === "win32" && (code === "EPERM" || code === "EACCES" || code === "EBADF");
|
|
18
|
+
}
|
|
19
|
+
function isTransientRenameError(error, platform) {
|
|
20
|
+
if (platform !== "win32")
|
|
21
|
+
return false;
|
|
22
|
+
const code = error?.code;
|
|
23
|
+
return code === "EPERM" || code === "EACCES" || code === "EBUSY";
|
|
24
|
+
}
|
|
25
|
+
async function bestEffort(action) {
|
|
26
|
+
try {
|
|
27
|
+
await action();
|
|
28
|
+
} catch {}
|
|
29
|
+
}
|
|
30
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
31
|
+
var defaultDirOpenOps = {
|
|
32
|
+
async open(dir, flags) {
|
|
33
|
+
const handle = await open(dir, flags);
|
|
34
|
+
return {
|
|
35
|
+
sync: () => handle.sync(),
|
|
36
|
+
close: () => handle.close()
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
async function syncDirectory(dir, ops = defaultDirOpenOps, platform = process.platform) {
|
|
41
|
+
let fsHandle = null;
|
|
42
|
+
try {
|
|
43
|
+
const handle = await ops.open(dir, "r");
|
|
44
|
+
fsHandle = handle;
|
|
45
|
+
await handle.sync();
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (!isUnsupportedSyncDirError(error, platform))
|
|
48
|
+
throw error;
|
|
49
|
+
} finally {
|
|
50
|
+
const cleanupHandle = fsHandle;
|
|
51
|
+
if (cleanupHandle)
|
|
52
|
+
await bestEffort(() => cleanupHandle.close());
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
var defaultAtomicWriteOps = {
|
|
56
|
+
platform: process.platform,
|
|
57
|
+
async open(path, flags, mode) {
|
|
58
|
+
const handle = await open(path, flags, mode);
|
|
59
|
+
return {
|
|
60
|
+
write: (data) => handle.writeFile(data),
|
|
61
|
+
sync: () => handle.sync(),
|
|
62
|
+
close: () => handle.close()
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
rename,
|
|
66
|
+
chmod,
|
|
67
|
+
unlink,
|
|
68
|
+
syncDir: (dir, platform) => syncDirectory(dir, defaultDirOpenOps, platform),
|
|
69
|
+
sleep: defaultSleep
|
|
70
|
+
};
|
|
71
|
+
var RENAME_ATTEMPTS = 3;
|
|
72
|
+
var RENAME_RETRY_DELAY_MS = 20;
|
|
73
|
+
async function renameWithRetry(ops, from, to) {
|
|
74
|
+
let attempt = 0;
|
|
75
|
+
for (;; ) {
|
|
76
|
+
try {
|
|
77
|
+
await ops.rename(from, to);
|
|
78
|
+
return;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
attempt += 1;
|
|
81
|
+
if (!isTransientRenameError(error, ops.platform) || attempt >= RENAME_ATTEMPTS)
|
|
82
|
+
throw error;
|
|
83
|
+
const delay = RENAME_RETRY_DELAY_MS * attempt;
|
|
84
|
+
await ops.sleep(delay);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function atomicWriteFile(file, data, ops = defaultAtomicWriteOps) {
|
|
89
|
+
const tmp = `${file}.${randomUUID()}.tmp`;
|
|
90
|
+
let handle = null;
|
|
91
|
+
let created = false;
|
|
92
|
+
let renamed = false;
|
|
93
|
+
try {
|
|
94
|
+
handle = await ops.open(tmp, "wx", 384);
|
|
95
|
+
created = true;
|
|
96
|
+
await handle.write(data);
|
|
97
|
+
await handle.sync();
|
|
98
|
+
await handle.close();
|
|
99
|
+
handle = null;
|
|
100
|
+
await renameWithRetry(ops, tmp, file);
|
|
101
|
+
renamed = true;
|
|
102
|
+
await bestEffort(() => ops.chmod(file, 384));
|
|
103
|
+
try {
|
|
104
|
+
await ops.syncDir(dirname(file), ops.platform);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (!isUnsupportedSyncDirError(error, ops.platform))
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
const cleanupHandle = handle;
|
|
111
|
+
if (cleanupHandle)
|
|
112
|
+
await bestEffort(() => cleanupHandle.close());
|
|
113
|
+
if (created && !renamed)
|
|
114
|
+
await bestEffort(() => ops.unlink(tmp));
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/state.ts
|
|
11
120
|
class StateReadError extends Data.TaggedError("StateReadError") {
|
|
12
121
|
}
|
|
13
122
|
|
|
@@ -34,6 +143,15 @@ var CheckpointSchema = Schema.Struct({
|
|
|
34
143
|
summary: Schema.String,
|
|
35
144
|
timestamp: Schema.Number
|
|
36
145
|
});
|
|
146
|
+
var PendingAttemptSchema = Schema.Struct({
|
|
147
|
+
id: Schema.String,
|
|
148
|
+
reservedAt: Schema.Number,
|
|
149
|
+
started: Schema.Boolean,
|
|
150
|
+
delivered: Schema.Boolean,
|
|
151
|
+
committed: Schema.Boolean,
|
|
152
|
+
armNoProgress: Schema.Boolean,
|
|
153
|
+
previousLastContinuationAt: Schema.NullOr(Schema.Number)
|
|
154
|
+
});
|
|
37
155
|
var GoalSchema = Schema.Struct({
|
|
38
156
|
sessionID: Schema.String,
|
|
39
157
|
objective: Schema.String,
|
|
@@ -50,6 +168,7 @@ var GoalSchema = Schema.Struct({
|
|
|
50
168
|
autoTurns: Schema.Number,
|
|
51
169
|
lastContinuationAt: NullableNumber,
|
|
52
170
|
continuationFailures: Schema.optionalWith(Schema.Number, { default: () => 0 }),
|
|
171
|
+
pendingAttempt: Schema.optionalWith(Schema.NullOr(PendingAttemptSchema), { default: () => null }),
|
|
53
172
|
lastStatus: Schema.optionalWith(NullableString, { default: () => null }),
|
|
54
173
|
maxAutoTurns: Schema.optionalWith(NullableNumber, { default: () => null }),
|
|
55
174
|
maxDurationSeconds: Schema.optionalWith(NullableNumber, { default: () => null }),
|
|
@@ -107,14 +226,9 @@ function writeStateEffect(state) {
|
|
|
107
226
|
return Effect.tryPromise({
|
|
108
227
|
try: async () => {
|
|
109
228
|
const file = statePath();
|
|
110
|
-
await mkdir(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
`, { mode: 384 });
|
|
114
|
-
await rename(tmp, file);
|
|
115
|
-
await chmod(file, 384).catch(() => {
|
|
116
|
-
return;
|
|
117
|
-
});
|
|
229
|
+
await mkdir(dirname2(file), { recursive: true, mode: 448 });
|
|
230
|
+
await atomicWriteFile(file, JSON.stringify(state, null, 2) + `
|
|
231
|
+
`);
|
|
118
232
|
},
|
|
119
233
|
catch: (cause) => new StateWriteError({ cause })
|
|
120
234
|
});
|
|
@@ -172,6 +286,8 @@ function normalizeGoal(goal) {
|
|
|
172
286
|
goal.lastAssistantMessageID ??= "";
|
|
173
287
|
goal.lastPromptAgent ??= null;
|
|
174
288
|
goal.awaitingContinuationProgress = goal.awaitingContinuationProgress === true;
|
|
289
|
+
goal.lastContinuationAt = typeof goal.lastContinuationAt === "number" && Number.isFinite(goal.lastContinuationAt) ? Math.floor(goal.lastContinuationAt >= 1000000000000 ? goal.lastContinuationAt / 1000 : goal.lastContinuationAt) : null;
|
|
290
|
+
goal.pendingAttempt = normalizePendingAttempt(goal.pendingAttempt);
|
|
175
291
|
goal.continuationBaselineMessageID ??= "";
|
|
176
292
|
goal.continuationBaselineSummary ??= "";
|
|
177
293
|
goal.noProgressTurns = nonNegativeInteger(goal.noProgressTurns, 0);
|
|
@@ -184,6 +300,22 @@ function normalizeGoal(goal) {
|
|
|
184
300
|
goal.stopReason ??= null;
|
|
185
301
|
return goal;
|
|
186
302
|
}
|
|
303
|
+
function normalizePendingAttempt(attempt) {
|
|
304
|
+
if (!attempt || typeof attempt !== "object")
|
|
305
|
+
return null;
|
|
306
|
+
return {
|
|
307
|
+
id: typeof attempt.id === "string" && attempt.id ? attempt.id : randomId(),
|
|
308
|
+
reservedAt: typeof attempt.reservedAt === "number" && Number.isFinite(attempt.reservedAt) ? attempt.reservedAt : Date.now(),
|
|
309
|
+
started: attempt.started === true,
|
|
310
|
+
delivered: attempt.delivered === true,
|
|
311
|
+
committed: attempt.committed === true,
|
|
312
|
+
armNoProgress: attempt.armNoProgress !== false,
|
|
313
|
+
previousLastContinuationAt: typeof attempt.previousLastContinuationAt === "number" && Number.isFinite(attempt.previousLastContinuationAt) ? attempt.previousLastContinuationAt : null
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function randomId() {
|
|
317
|
+
return `att_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
318
|
+
}
|
|
187
319
|
function normalizeCreateOptions(input) {
|
|
188
320
|
if (typeof input === "number" || input === null) {
|
|
189
321
|
return {
|
|
@@ -262,11 +394,19 @@ function snapshot(goal) {
|
|
|
262
394
|
sampledAt
|
|
263
395
|
};
|
|
264
396
|
}
|
|
397
|
+
function snapshotInternal(goal) {
|
|
398
|
+
return { ...snapshot(goal), pendingAttempt: goal.pendingAttempt };
|
|
399
|
+
}
|
|
265
400
|
async function getGoal(sessionID) {
|
|
266
401
|
const state = await readState();
|
|
267
402
|
const goal = state.goals[sessionID];
|
|
268
403
|
return goal ? snapshot(goal) : null;
|
|
269
404
|
}
|
|
405
|
+
async function getGoalInternal(sessionID) {
|
|
406
|
+
const state = await readState();
|
|
407
|
+
const goal = state.goals[sessionID];
|
|
408
|
+
return goal ? snapshotInternal(goal) : null;
|
|
409
|
+
}
|
|
270
410
|
async function createGoal(sessionID, objective, options) {
|
|
271
411
|
const value = validateObjective(objective);
|
|
272
412
|
const normalizedOptions = normalizeCreateOptions(options);
|
|
@@ -293,6 +433,7 @@ async function createGoal(sessionID, objective, options) {
|
|
|
293
433
|
autoTurns: 0,
|
|
294
434
|
lastContinuationAt: null,
|
|
295
435
|
continuationFailures: 0,
|
|
436
|
+
pendingAttempt: null,
|
|
296
437
|
lastStatus: paused ? "Goal recorded from Plan mode; execution paused until resumed from Build mode." : "Goal set.",
|
|
297
438
|
maxAutoTurns: normalizedOptions.maxAutoTurns,
|
|
298
439
|
maxDurationSeconds: normalizedOptions.maxDurationSeconds,
|
|
@@ -336,6 +477,11 @@ async function updateGoalObjective(sessionID, objective, status = "active", opti
|
|
|
336
477
|
goal.closedAt = null;
|
|
337
478
|
goal.stopReason = planModePause ? PLAN_MODE_STOP_REASON : null;
|
|
338
479
|
goal.budgetWrapupSent = false;
|
|
480
|
+
if (goal.status === "active") {
|
|
481
|
+
goal.continuationFailures = 0;
|
|
482
|
+
goal.pendingAttempt = null;
|
|
483
|
+
goal.awaitingContinuationProgress = false;
|
|
484
|
+
}
|
|
339
485
|
if (agent)
|
|
340
486
|
goal.lastPromptAgent = agent;
|
|
341
487
|
goal.lastStatus = planModePause ? "Goal objective updated; execution paused while the session is in Plan mode." : goal.status === "active" ? "Goal objective updated and resumed." : "Goal objective updated and paused.";
|
|
@@ -387,6 +533,7 @@ async function setGoalStatus(sessionID, status, agent) {
|
|
|
387
533
|
goal.updatedAt = nowSeconds();
|
|
388
534
|
goal.lastAccountedAt = status === "active" ? goal.updatedAt : null;
|
|
389
535
|
goal.continuationFailures = status === "active" ? 0 : goal.continuationFailures;
|
|
536
|
+
goal.pendingAttempt = status === "active" ? null : goal.pendingAttempt;
|
|
390
537
|
goal.noProgressTurns = status === "active" ? 0 : goal.noProgressTurns;
|
|
391
538
|
goal.stopReason = status === "active" ? null : "paused";
|
|
392
539
|
goal.budgetWrapupSent = status === "active" ? false : goal.budgetWrapupSent;
|
|
@@ -462,6 +609,7 @@ async function recordAssistantProgress(sessionID, input) {
|
|
|
462
609
|
const threshold = positiveIntegerOrNull(input.noProgressTokenThreshold) ?? goal.noProgressTokenThreshold;
|
|
463
610
|
const maxNoProgressTurns = positiveIntegerOrNull(input.maxNoProgressTurns) ?? goal.maxNoProgressTurns;
|
|
464
611
|
const summary = summarizeText(text);
|
|
612
|
+
const substantive = /[\p{L}\p{N}]/u.test(text);
|
|
465
613
|
const previousSummary = summarizeText(goal.lastAssistantText);
|
|
466
614
|
const repeatedMessage = Boolean(messageID && messageID === goal.lastAssistantMessageID);
|
|
467
615
|
const changed = Boolean(summary && summary !== previousSummary);
|
|
@@ -471,9 +619,18 @@ async function recordAssistantProgress(sessionID, input) {
|
|
|
471
619
|
goal.lastAssistantText = text;
|
|
472
620
|
if (messageID)
|
|
473
621
|
goal.lastAssistantMessageID = messageID;
|
|
474
|
-
|
|
622
|
+
if (substantive && summary && (!repeatedMessage || changed)) {
|
|
623
|
+
const attempt = goal.pendingAttempt;
|
|
624
|
+
if (attempt == null || input.completedAt == null || input.completedAt >= attempt.reservedAt) {
|
|
625
|
+
goal.continuationFailures = 0;
|
|
626
|
+
goal.pendingAttempt = null;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
const attemptForCompletion = goal.pendingAttempt;
|
|
630
|
+
const continuationTurnCompleted = input.evaluateContinuation === true && goal.awaitingContinuationProgress && Boolean(messageID) && messageID !== goal.continuationBaselineMessageID && (input.completedAt == null || attemptForCompletion == null || input.completedAt >= attemptForCompletion.reservedAt);
|
|
475
631
|
if (continuationTurnCompleted) {
|
|
476
632
|
goal.awaitingContinuationProgress = false;
|
|
633
|
+
goal.pendingAttempt = null;
|
|
477
634
|
const lowOutput = outputTokens > 0 && outputTokens < (threshold ?? DEFAULT_NO_PROGRESS_TOKEN_THRESHOLD);
|
|
478
635
|
const changedSinceContinuation = Boolean(summary && summary !== goal.continuationBaselineSummary);
|
|
479
636
|
if (lowOutput && !changedSinceContinuation) {
|
|
@@ -514,32 +671,84 @@ async function reserveContinuation(sessionID, maxAutoTurns, minIntervalSeconds)
|
|
|
514
671
|
if (goal.lastContinuationAt && now - goal.lastContinuationAt < minIntervalSeconds)
|
|
515
672
|
return null;
|
|
516
673
|
goal.autoTurns += 1;
|
|
674
|
+
const previousLastContinuationAt = goal.lastContinuationAt;
|
|
517
675
|
goal.lastContinuationAt = now;
|
|
518
676
|
goal.continuationBaselineMessageID = goal.lastAssistantMessageID;
|
|
519
677
|
goal.continuationBaselineSummary = summarizeText(goal.lastAssistantText);
|
|
678
|
+
goal.pendingAttempt = {
|
|
679
|
+
id: randomId(),
|
|
680
|
+
reservedAt: Date.now(),
|
|
681
|
+
started: false,
|
|
682
|
+
delivered: false,
|
|
683
|
+
committed: true,
|
|
684
|
+
armNoProgress: true,
|
|
685
|
+
previousLastContinuationAt
|
|
686
|
+
};
|
|
687
|
+
goal.awaitingContinuationProgress = false;
|
|
520
688
|
goal.lastStatus = `Auto-continue ${goal.autoTurns} reserved.`;
|
|
521
689
|
pushHistory(goal, "autoContinue", goal.lastStatus);
|
|
522
690
|
goal.updatedAt = now;
|
|
523
|
-
return
|
|
691
|
+
return snapshotInternal(goal);
|
|
524
692
|
});
|
|
525
693
|
}
|
|
526
|
-
async function
|
|
694
|
+
async function rollbackContinuationAttempt(sessionID) {
|
|
695
|
+
return mutate((state) => {
|
|
696
|
+
const goal = state.goals[sessionID];
|
|
697
|
+
if (!goal)
|
|
698
|
+
return false;
|
|
699
|
+
const attempt = goal.pendingAttempt;
|
|
700
|
+
if (!attempt || attempt.delivered || !attempt.committed) {
|
|
701
|
+
if (attempt && !attempt.delivered)
|
|
702
|
+
goal.pendingAttempt = null;
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
goal.autoTurns = Math.max(0, goal.autoTurns - 1);
|
|
706
|
+
goal.lastContinuationAt = attempt.previousLastContinuationAt;
|
|
707
|
+
goal.pendingAttempt = null;
|
|
708
|
+
goal.awaitingContinuationProgress = false;
|
|
709
|
+
goal.lastStatus = "Auto-continue attempt canceled before delivery.";
|
|
710
|
+
goal.updatedAt = nowSeconds();
|
|
711
|
+
return true;
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
async function recordContinuationResult(sessionID, result, maxFailures, options) {
|
|
527
715
|
return mutate((state) => {
|
|
528
716
|
const goal = state.goals[sessionID];
|
|
529
717
|
if (!goal || isClosed(goal.status))
|
|
530
|
-
return goal ?
|
|
718
|
+
return goal ? snapshotInternal(goal) : null;
|
|
531
719
|
const now = nowSeconds();
|
|
532
720
|
goal.updatedAt = now;
|
|
533
721
|
if (result === "success") {
|
|
534
|
-
goal.continuationFailures = 0;
|
|
535
722
|
if (goal.status === "active") {
|
|
723
|
+
const attempt = goal.pendingAttempt;
|
|
724
|
+
if (attempt) {
|
|
725
|
+
attempt.delivered = true;
|
|
726
|
+
attempt.started = attempt.started || options?.started === true;
|
|
727
|
+
attempt.armNoProgress = options?.armNoProgress ?? attempt.armNoProgress;
|
|
728
|
+
if (attempt.armNoProgress)
|
|
729
|
+
goal.awaitingContinuationProgress = true;
|
|
730
|
+
} else {
|
|
731
|
+
goal.pendingAttempt = {
|
|
732
|
+
id: randomId(),
|
|
733
|
+
reservedAt: Date.now(),
|
|
734
|
+
started: options?.started === true,
|
|
735
|
+
delivered: true,
|
|
736
|
+
committed: false,
|
|
737
|
+
armNoProgress: options?.armNoProgress !== false,
|
|
738
|
+
previousLastContinuationAt: goal.lastContinuationAt
|
|
739
|
+
};
|
|
740
|
+
if (goal.pendingAttempt.armNoProgress)
|
|
741
|
+
goal.awaitingContinuationProgress = true;
|
|
742
|
+
}
|
|
536
743
|
goal.lastStatus = "Auto-continue prompt sent.";
|
|
537
|
-
goal.awaitingContinuationProgress = true;
|
|
538
744
|
}
|
|
539
|
-
return
|
|
745
|
+
return snapshotInternal(goal);
|
|
540
746
|
}
|
|
747
|
+
if (options?.requirePending && goal.pendingAttempt == null)
|
|
748
|
+
return null;
|
|
541
749
|
goal.continuationFailures += 1;
|
|
542
750
|
goal.awaitingContinuationProgress = false;
|
|
751
|
+
goal.pendingAttempt = null;
|
|
543
752
|
goal.lastStatus = `Auto-continue failed ${goal.continuationFailures} time(s).`;
|
|
544
753
|
pushHistory(goal, "error", goal.lastStatus);
|
|
545
754
|
if (goal.continuationFailures >= maxFailures) {
|
|
@@ -551,7 +760,44 @@ async function recordContinuationResult(sessionID, result, maxFailures) {
|
|
|
551
760
|
goal.blocker = "Auto-continue prompt failed repeatedly. Resume the goal to retry.";
|
|
552
761
|
pushHistory(goal, "paused", goal.lastStatus);
|
|
553
762
|
}
|
|
554
|
-
return
|
|
763
|
+
return snapshotInternal(goal);
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
async function markPendingContinuationStarted(sessionID) {
|
|
767
|
+
const state = await readState();
|
|
768
|
+
const current = state.goals[sessionID];
|
|
769
|
+
if (!current || current.status !== "active")
|
|
770
|
+
return current ? snapshotInternal(current) : null;
|
|
771
|
+
if (current.pendingAttempt == null || current.pendingAttempt.started)
|
|
772
|
+
return snapshotInternal(current);
|
|
773
|
+
return mutate((state2) => {
|
|
774
|
+
const goal = state2.goals[sessionID];
|
|
775
|
+
if (!goal || goal.status !== "active")
|
|
776
|
+
return goal ? snapshotInternal(goal) : null;
|
|
777
|
+
if (goal.pendingAttempt == null || goal.pendingAttempt.started)
|
|
778
|
+
return snapshotInternal(goal);
|
|
779
|
+
goal.pendingAttempt.started = true;
|
|
780
|
+
goal.updatedAt = nowSeconds();
|
|
781
|
+
return snapshotInternal(goal);
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
async function recordToolProgress(sessionID, text, expectedAttemptID) {
|
|
785
|
+
return mutate((state) => {
|
|
786
|
+
const goal = state.goals[sessionID];
|
|
787
|
+
if (!goal || goal.status !== "active")
|
|
788
|
+
return goal ? snapshotInternal(goal) : null;
|
|
789
|
+
const value = text?.trim() ?? "";
|
|
790
|
+
if (!value)
|
|
791
|
+
return snapshotInternal(goal);
|
|
792
|
+
if (goal.continuationFailures === 0 && goal.pendingAttempt == null)
|
|
793
|
+
return snapshotInternal(goal);
|
|
794
|
+
if (goal.pendingAttempt != null && expectedAttemptID !== undefined && expectedAttemptID !== goal.pendingAttempt.id) {
|
|
795
|
+
return snapshotInternal(goal);
|
|
796
|
+
}
|
|
797
|
+
goal.continuationFailures = 0;
|
|
798
|
+
goal.pendingAttempt = null;
|
|
799
|
+
goal.updatedAt = nowSeconds();
|
|
800
|
+
return snapshotInternal(goal);
|
|
555
801
|
});
|
|
556
802
|
}
|
|
557
803
|
function reserveWrapup(goal) {
|
|
@@ -560,7 +806,7 @@ function reserveWrapup(goal) {
|
|
|
560
806
|
goal.budgetWrapupSent = true;
|
|
561
807
|
goal.updatedAt = nowSeconds();
|
|
562
808
|
pushHistory(goal, "limited", `${goal.status}: ${goal.stopReason ?? "goal limit reached"}; requested final handoff.`);
|
|
563
|
-
return
|
|
809
|
+
return snapshotInternal(goal);
|
|
564
810
|
}
|
|
565
811
|
function maybeStopForBudget(goal) {
|
|
566
812
|
if (goal.status !== "active")
|
|
@@ -783,6 +1029,11 @@ var DEFAULT_RESTRICTED_AGENTS = ["plan"];
|
|
|
783
1029
|
var TASK_SETTLE_DELAY_MS = 25;
|
|
784
1030
|
var SNAPSHOT_IDLE_HOLD_MS = 250;
|
|
785
1031
|
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
1032
|
+
var STALE_PENDING_MS = 30000;
|
|
1033
|
+
var RETRY_SETTLE_MS = 25;
|
|
1034
|
+
var TRANSPORT_ERROR_PATTERN = /\b(?:network|fetch|socket|connect|connection|timeout|timed out|ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|ENOTFOUND|EPIPE|transport|stream|websocket|offline|internet|request failed|proxy)\b/i;
|
|
1035
|
+
var NON_TRANSPORT_TERMINAL_PATTERN = /\b(?:abort(?:ed)?|interrupt(?:ed|ion)?)\b/i;
|
|
1036
|
+
var NON_PROGRESS_TOOLS = new Set(["get_goal", "get_goal_history"]);
|
|
786
1037
|
var TASK_TERMINAL_STATES = new Set(["completed", "error", "cancelled"]);
|
|
787
1038
|
var PLAN_MODE_CREATE_NOTICE = 'Goal recorded while the session is in Plan mode, so execution is paused. Do not start implementation work now. Ask the user to switch to Build mode and resume the goal (for example with "/goal resume") to begin execution.';
|
|
788
1039
|
var LIMITED_GOAL_NOTICE = "Safety limit reached. Do not start or continue substantive work for this goal. Summarize useful progress, remaining work, and blockers, then wait for the user to resume or edit the goal.";
|
|
@@ -825,6 +1076,9 @@ function commandNameFromOptions(options) {
|
|
|
825
1076
|
function positiveIntegerOrNull2(value) {
|
|
826
1077
|
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
|
|
827
1078
|
}
|
|
1079
|
+
function nonNegativeIntegerOrNull(value) {
|
|
1080
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
1081
|
+
}
|
|
828
1082
|
function timeoutMillisecondsFromSeconds(value) {
|
|
829
1083
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0)
|
|
830
1084
|
return null;
|
|
@@ -994,6 +1248,103 @@ function isIdleEvent(event) {
|
|
|
994
1248
|
const status = event.properties?.status;
|
|
995
1249
|
return event.type === "session.status" && typeof status === "object" && status !== null && status.type === "idle";
|
|
996
1250
|
}
|
|
1251
|
+
function isTransportError(error) {
|
|
1252
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
1253
|
+
if (!message || NON_TRANSPORT_TERMINAL_PATTERN.test(message))
|
|
1254
|
+
return false;
|
|
1255
|
+
if (TRANSPORT_ERROR_PATTERN.test(message))
|
|
1256
|
+
return true;
|
|
1257
|
+
return false;
|
|
1258
|
+
}
|
|
1259
|
+
function transportErrorMessageFromEvent(props) {
|
|
1260
|
+
for (const candidate of [props.error, props.message, props.reason]) {
|
|
1261
|
+
if (typeof candidate === "string" && candidate.trim())
|
|
1262
|
+
return candidate.trim();
|
|
1263
|
+
if (isRecord(candidate)) {
|
|
1264
|
+
for (const key of ["message", "error", "reason", "description"]) {
|
|
1265
|
+
const value = candidate[key];
|
|
1266
|
+
if (typeof value === "string" && value.trim())
|
|
1267
|
+
return value.trim();
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
return "";
|
|
1272
|
+
}
|
|
1273
|
+
function continuationRetryDelayMs(minIntervalSeconds, attemptAt, now = Date.now()) {
|
|
1274
|
+
return Math.max(0, attemptAt + minIntervalSeconds * 1000 - now) + RETRY_SETTLE_MS;
|
|
1275
|
+
}
|
|
1276
|
+
function continuationDelayFromSnapshot(minIntervalSeconds, lastContinuationAt, now = Date.now()) {
|
|
1277
|
+
if (lastContinuationAt == null)
|
|
1278
|
+
return RETRY_SETTLE_MS;
|
|
1279
|
+
return Math.max(0, (lastContinuationAt + minIntervalSeconds + 1) * 1000 - now) + RETRY_SETTLE_MS;
|
|
1280
|
+
}
|
|
1281
|
+
function pendingAttemptOf(goal) {
|
|
1282
|
+
return goal?.pendingAttempt ?? null;
|
|
1283
|
+
}
|
|
1284
|
+
function pendingReadyForFailure(attempt, deliveredLocally, now = Date.now()) {
|
|
1285
|
+
if (!attempt)
|
|
1286
|
+
return false;
|
|
1287
|
+
if (attempt.started)
|
|
1288
|
+
return true;
|
|
1289
|
+
if (deliveredLocally)
|
|
1290
|
+
return false;
|
|
1291
|
+
return now - attempt.reservedAt >= STALE_PENDING_MS;
|
|
1292
|
+
}
|
|
1293
|
+
async function reconcileLocalMarkerAfterProgress(locallyDelivered, sessionID, goal) {
|
|
1294
|
+
if (!goal || goal.continuationFailures !== 0)
|
|
1295
|
+
return;
|
|
1296
|
+
const internal = await getGoalInternal(sessionID);
|
|
1297
|
+
if (internal && internal.pendingAttempt == null)
|
|
1298
|
+
locallyDelivered.delete(sessionID);
|
|
1299
|
+
}
|
|
1300
|
+
var TOOL_FAILURE_STATES = new Set([
|
|
1301
|
+
"failed",
|
|
1302
|
+
"failure",
|
|
1303
|
+
"error",
|
|
1304
|
+
"cancelled",
|
|
1305
|
+
"canceled",
|
|
1306
|
+
"aborted",
|
|
1307
|
+
"abort",
|
|
1308
|
+
"interrupted",
|
|
1309
|
+
"running",
|
|
1310
|
+
"pending",
|
|
1311
|
+
"in_progress",
|
|
1312
|
+
"in-progress",
|
|
1313
|
+
"incomplete",
|
|
1314
|
+
"partial",
|
|
1315
|
+
"timeout",
|
|
1316
|
+
"timed_out"
|
|
1317
|
+
]);
|
|
1318
|
+
function toolOutputFailed(output) {
|
|
1319
|
+
if (!isRecord(output))
|
|
1320
|
+
return true;
|
|
1321
|
+
if (typeof output.error === "string" && output.error.trim())
|
|
1322
|
+
return true;
|
|
1323
|
+
if (output.success === false)
|
|
1324
|
+
return true;
|
|
1325
|
+
const text = typeof output.output === "string" ? output.output.trim() : "";
|
|
1326
|
+
const state = output.state ?? output.status;
|
|
1327
|
+
if (typeof state === "string") {
|
|
1328
|
+
const normalized = state.trim().toLowerCase();
|
|
1329
|
+
if (TOOL_FAILURE_STATES.has(normalized))
|
|
1330
|
+
return true;
|
|
1331
|
+
if (["completed", "complete", "success", "succeeded", "ok", "done"].includes(normalized))
|
|
1332
|
+
return false;
|
|
1333
|
+
}
|
|
1334
|
+
if (isRecord(output.metadata)) {
|
|
1335
|
+
const metaState = output.metadata.state ?? output.metadata.status;
|
|
1336
|
+
if (typeof metaState === "string" && TOOL_FAILURE_STATES.has(metaState.trim().toLowerCase()))
|
|
1337
|
+
return true;
|
|
1338
|
+
}
|
|
1339
|
+
const taskState = parseTaskState(text);
|
|
1340
|
+
if (taskState)
|
|
1341
|
+
return taskState !== "completed";
|
|
1342
|
+
if (/^state:\s*(failed|failure|error|cancelled|canceled|aborted|abort|interrupted|running|pending|incomplete|partial|timeout|timed_out)\b/im.test(text))
|
|
1343
|
+
return true;
|
|
1344
|
+
if (/^<error>/i.test(text) || /^<tool-error>/i.test(text) || /^error:/i.test(text))
|
|
1345
|
+
return true;
|
|
1346
|
+
return false;
|
|
1347
|
+
}
|
|
997
1348
|
function sessionIDFromEvent(event) {
|
|
998
1349
|
const direct = event.properties?.sessionID;
|
|
999
1350
|
if (typeof direct === "string")
|
|
@@ -1277,15 +1628,21 @@ class TaskTracker {
|
|
|
1277
1628
|
}
|
|
1278
1629
|
async function recordAssistantMessage(sessionID, message, options, evaluateContinuation = false) {
|
|
1279
1630
|
if (!message)
|
|
1280
|
-
return;
|
|
1281
|
-
await
|
|
1282
|
-
|
|
1283
|
-
|
|
1631
|
+
return { goal: null, progressed: false };
|
|
1632
|
+
const before = await getGoal(sessionID);
|
|
1633
|
+
const id = messageID(message) ?? "";
|
|
1634
|
+
const text = textFromMessage(message);
|
|
1635
|
+
const progressed = Boolean(/[\p{L}\p{N}]/u.test(text) && (id !== (before?.lastAssistantMessageID ?? "") || text !== (before?.lastAssistantText ?? "")));
|
|
1636
|
+
const goal = await recordAssistantProgress(sessionID, {
|
|
1637
|
+
messageID: id,
|
|
1638
|
+
text,
|
|
1284
1639
|
outputTokens: outputTokensFromMessage(message) ?? null,
|
|
1285
1640
|
noProgressTokenThreshold: positiveIntegerOrNull2(options.no_progress_token_threshold),
|
|
1286
1641
|
maxNoProgressTurns: positiveIntegerOrNull2(options.max_no_progress_turns),
|
|
1287
|
-
evaluateContinuation
|
|
1642
|
+
evaluateContinuation,
|
|
1643
|
+
completedAt: messageCompletedAt(message)
|
|
1288
1644
|
});
|
|
1645
|
+
return { goal, progressed };
|
|
1289
1646
|
}
|
|
1290
1647
|
function mergeSystemReminder(output, reminder) {
|
|
1291
1648
|
if (!reminder.trim())
|
|
@@ -1367,11 +1724,20 @@ function textFromToolResult(result) {
|
|
|
1367
1724
|
}
|
|
1368
1725
|
return;
|
|
1369
1726
|
}
|
|
1727
|
+
function toolAttemptKey(sessionID, callID) {
|
|
1728
|
+
return `${sessionID}\x00${callID}`;
|
|
1729
|
+
}
|
|
1730
|
+
function clearToolAttemptsForSession(attempts, sessionID) {
|
|
1731
|
+
for (const key of [...attempts.keys()]) {
|
|
1732
|
+
if (key.startsWith(`${sessionID}\x00`))
|
|
1733
|
+
attempts.delete(key);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1370
1736
|
var server = async ({ client }, options) => {
|
|
1371
1737
|
const autoContinue = options?.auto_continue ?? true;
|
|
1372
1738
|
const deferWhileTasksActive = options?.defer_while_tasks_active ?? true;
|
|
1373
1739
|
const maxAutoTurns = positiveIntegerOrNull2(options?.max_auto_turns) ?? DEFAULT_MAX_AUTO_TURNS;
|
|
1374
|
-
const minInterval =
|
|
1740
|
+
const minInterval = nonNegativeIntegerOrNull(options?.min_continue_interval_seconds) ?? DEFAULT_CONTINUE_INTERVAL_SECONDS;
|
|
1375
1741
|
const maxTurnTimeMs = timeoutMillisecondsFromSeconds(options?.max_turn_time);
|
|
1376
1742
|
const maxPromptFailures = positiveIntegerOrNull2(options?.max_prompt_failures) ?? DEFAULT_MAX_PROMPT_FAILURES;
|
|
1377
1743
|
const registerCommand = options?.register_command ?? true;
|
|
@@ -1381,9 +1747,14 @@ var server = async ({ client }, options) => {
|
|
|
1381
1747
|
const scheduledContinuations = new Map;
|
|
1382
1748
|
const turnWatchdogs = new Map;
|
|
1383
1749
|
const busySessions = new Set;
|
|
1750
|
+
const nativeRetrySessions = new Set;
|
|
1751
|
+
const locallyDeliveredPendingSessions = new Set;
|
|
1752
|
+
const toolAttempts = new Map;
|
|
1753
|
+
const watchdogRescuedSessions = new Set;
|
|
1384
1754
|
const planAgents = restrictedAgentSet(options);
|
|
1385
1755
|
const isPlanAgent = (agent) => typeof agent === "string" && planAgents.has(agent.trim().toLowerCase());
|
|
1386
1756
|
const goalServices = { options: options ?? {}, isPlanAgent };
|
|
1757
|
+
let disposed = false;
|
|
1387
1758
|
async function taskBlockStatus(sessionID) {
|
|
1388
1759
|
if (!deferWhileTasksActive)
|
|
1389
1760
|
return false;
|
|
@@ -1403,6 +1774,8 @@ var server = async ({ client }, options) => {
|
|
|
1403
1774
|
function armTurnWatchdog(sessionID) {
|
|
1404
1775
|
if (maxTurnTimeMs == null)
|
|
1405
1776
|
return;
|
|
1777
|
+
if (watchdogRescuedSessions.has(sessionID))
|
|
1778
|
+
return;
|
|
1406
1779
|
clearTurnWatchdog(sessionID);
|
|
1407
1780
|
const watchdog = {
|
|
1408
1781
|
timer: setTimeout(() => void runTurnWatchdog(sessionID, watchdog), maxTurnTimeMs)
|
|
@@ -1415,7 +1788,9 @@ var server = async ({ client }, options) => {
|
|
|
1415
1788
|
async function runTurnWatchdog(sessionID, watchdog) {
|
|
1416
1789
|
let claimedContinuation = false;
|
|
1417
1790
|
try {
|
|
1418
|
-
if (
|
|
1791
|
+
if (disposed)
|
|
1792
|
+
return;
|
|
1793
|
+
if (turnWatchdogs.get(sessionID) !== watchdog || !busySessions.has(sessionID) || watchdogRescuedSessions.has(sessionID))
|
|
1419
1794
|
return;
|
|
1420
1795
|
const goal = await getGoal(sessionID);
|
|
1421
1796
|
if (turnWatchdogs.get(sessionID) !== watchdog || !busySessions.has(sessionID))
|
|
@@ -1428,6 +1803,8 @@ var server = async ({ client }, options) => {
|
|
|
1428
1803
|
const latestTurnAgent = agentFromMessage(latestAssistant);
|
|
1429
1804
|
if (isPlanAgent(latestTurnAgent))
|
|
1430
1805
|
return;
|
|
1806
|
+
const observedBeforeRescue = await recordAssistantMessage(sessionID, latestAssistant, options ?? {});
|
|
1807
|
+
await reconcileLocalMarkerAfterProgress(locallyDeliveredPendingSessions, sessionID, observedBeforeRescue.goal);
|
|
1431
1808
|
const taskStatus = await taskBlockStatus(sessionID);
|
|
1432
1809
|
if (turnWatchdogs.get(sessionID) !== watchdog || !busySessions.has(sessionID))
|
|
1433
1810
|
return;
|
|
@@ -1441,9 +1818,16 @@ var server = async ({ client }, options) => {
|
|
|
1441
1818
|
turnWatchdogs.delete(sessionID);
|
|
1442
1819
|
activeContinuations.add(sessionID);
|
|
1443
1820
|
claimedContinuation = true;
|
|
1821
|
+
watchdogRescuedSessions.add(sessionID);
|
|
1444
1822
|
await sendContinuation(client, sessionID, continuationPrompt(current), current.lastPromptAgent ?? latestTurnAgent ?? null);
|
|
1823
|
+
await recordContinuationResult(sessionID, "success", maxPromptFailures, { armNoProgress: false, started: true });
|
|
1824
|
+
locallyDeliveredPendingSessions.add(sessionID);
|
|
1825
|
+
clearTurnWatchdog(sessionID);
|
|
1445
1826
|
} catch (error) {
|
|
1446
1827
|
try {
|
|
1828
|
+
if (claimedContinuation && isTransportError(error)) {
|
|
1829
|
+
await recordContinuationResult(sessionID, "failure", maxPromptFailures);
|
|
1830
|
+
}
|
|
1447
1831
|
await client.app?.log?.({
|
|
1448
1832
|
body: {
|
|
1449
1833
|
service: "opencode-goal-plugin",
|
|
@@ -1462,38 +1846,74 @@ var server = async ({ client }, options) => {
|
|
|
1462
1846
|
turnWatchdogs.delete(sessionID);
|
|
1463
1847
|
}
|
|
1464
1848
|
}
|
|
1465
|
-
function
|
|
1466
|
-
|
|
1849
|
+
function cancelScheduledContinuation(sessionID) {
|
|
1850
|
+
const scheduled = scheduledContinuations.get(sessionID);
|
|
1851
|
+
if (scheduled)
|
|
1852
|
+
clearTimeout(scheduled.timer);
|
|
1853
|
+
scheduledContinuations.delete(sessionID);
|
|
1854
|
+
}
|
|
1855
|
+
function scheduleSettledContinuation(sessionID, delayMs = TASK_SETTLE_DELAY_MS, replace = false, purpose = "settle") {
|
|
1856
|
+
if (disposed)
|
|
1857
|
+
return;
|
|
1858
|
+
if (!replace && scheduledContinuations.has(sessionID))
|
|
1467
1859
|
return;
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1860
|
+
if (replace)
|
|
1861
|
+
cancelScheduledContinuation(sessionID);
|
|
1862
|
+
const scheduled = {};
|
|
1863
|
+
const timer = setTimeout(async () => {
|
|
1864
|
+
try {
|
|
1865
|
+
if (scheduledContinuations.get(sessionID) !== scheduled || nativeRetrySessions.has(sessionID))
|
|
1866
|
+
return;
|
|
1867
|
+
if (purpose === "retry") {
|
|
1868
|
+
const goal = await getGoalInternal(sessionID);
|
|
1869
|
+
if (!goal || goal.continuationFailures === 0 && goal.pendingAttempt == null)
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
if (scheduledContinuations.get(sessionID) !== scheduled || nativeRetrySessions.has(sessionID))
|
|
1873
|
+
return;
|
|
1874
|
+
await runAutoContinue(sessionID, true, scheduled);
|
|
1875
|
+
} finally {
|
|
1876
|
+
if (scheduledContinuations.get(sessionID) === scheduled)
|
|
1877
|
+
scheduledContinuations.delete(sessionID);
|
|
1878
|
+
}
|
|
1471
1879
|
}, Math.max(0, delayMs));
|
|
1880
|
+
scheduled.timer = timer;
|
|
1881
|
+
scheduled.purpose = purpose;
|
|
1472
1882
|
const maybeUnref = timer;
|
|
1473
1883
|
if (typeof maybeUnref.unref === "function")
|
|
1474
1884
|
maybeUnref.unref();
|
|
1475
|
-
scheduledContinuations.set(sessionID,
|
|
1885
|
+
scheduledContinuations.set(sessionID, scheduled);
|
|
1476
1886
|
}
|
|
1477
|
-
async function runAutoContinue(sessionID, fromTaskDeferral = false) {
|
|
1887
|
+
async function runAutoContinue(sessionID, fromTaskDeferral = false, scheduled) {
|
|
1888
|
+
if (disposed)
|
|
1889
|
+
return;
|
|
1478
1890
|
if (busySessions.has(sessionID))
|
|
1479
1891
|
return;
|
|
1480
1892
|
if (activeContinuations.has(sessionID))
|
|
1481
1893
|
return;
|
|
1482
1894
|
activeContinuations.add(sessionID);
|
|
1895
|
+
let attemptReservedAt = Date.now();
|
|
1483
1896
|
try {
|
|
1484
1897
|
const latestAssistant = await fetchLatestAssistant(client, sessionID);
|
|
1485
1898
|
taskTracker.observeAssistantMessage(sessionID, latestAssistant);
|
|
1486
1899
|
const taskStatus = await taskBlockStatus(sessionID);
|
|
1487
1900
|
if (taskStatus && taskStatus.blocked) {
|
|
1488
1901
|
taskDeferredSessions.add(sessionID);
|
|
1489
|
-
if (taskStatus.retryAt != null)
|
|
1490
|
-
scheduleSettledContinuation(sessionID, taskStatus.retryAt - Date.now());
|
|
1902
|
+
if (taskStatus.retryAt != null) {
|
|
1903
|
+
scheduleSettledContinuation(sessionID, taskStatus.retryAt - Date.now(), scheduled != null);
|
|
1904
|
+
}
|
|
1491
1905
|
return;
|
|
1492
1906
|
}
|
|
1493
1907
|
if (busySessions.has(sessionID))
|
|
1494
1908
|
return;
|
|
1495
|
-
await recordAssistantMessage(sessionID, latestAssistant, options ?? {}, true);
|
|
1496
|
-
|
|
1909
|
+
const observed = await recordAssistantMessage(sessionID, latestAssistant, options ?? {}, true);
|
|
1910
|
+
await reconcileLocalMarkerAfterProgress(locallyDeliveredPendingSessions, sessionID, observed.goal);
|
|
1911
|
+
const queued = scheduledContinuations.get(sessionID);
|
|
1912
|
+
if (observed.progressed && queued?.purpose !== "settle")
|
|
1913
|
+
cancelScheduledContinuation(sessionID);
|
|
1914
|
+
if (scheduled && scheduledContinuations.get(sessionID) !== scheduled)
|
|
1915
|
+
return;
|
|
1916
|
+
const current = await getGoalInternal(sessionID);
|
|
1497
1917
|
if (!current)
|
|
1498
1918
|
return;
|
|
1499
1919
|
const latestTurnAgent = agentFromMessage(latestAssistant);
|
|
@@ -1509,13 +1929,64 @@ var server = async ({ client }, options) => {
|
|
|
1509
1929
|
return;
|
|
1510
1930
|
}
|
|
1511
1931
|
taskDeferredSessions.delete(sessionID);
|
|
1932
|
+
const attempt = pendingAttemptOf(current);
|
|
1933
|
+
if (current.status === "active" && attempt != null) {
|
|
1934
|
+
const deliveredLocally = locallyDeliveredPendingSessions.has(sessionID);
|
|
1935
|
+
if (!pendingReadyForFailure(attempt, deliveredLocally)) {
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
const afterFailure = await recordContinuationResult(sessionID, "failure", maxPromptFailures, {
|
|
1939
|
+
requirePending: true
|
|
1940
|
+
});
|
|
1941
|
+
if (afterFailure)
|
|
1942
|
+
locallyDeliveredPendingSessions.delete(sessionID);
|
|
1943
|
+
if (autoContinue && afterFailure?.status === "active") {
|
|
1944
|
+
scheduleSettledContinuation(sessionID, continuationRetryDelayMs(minInterval, attempt.reservedAt), true, "retry");
|
|
1945
|
+
}
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
const queuedBeforeReserve = scheduledContinuations.get(sessionID);
|
|
1949
|
+
if (queuedBeforeReserve && queuedBeforeReserve !== scheduled)
|
|
1950
|
+
return;
|
|
1951
|
+
if (!autoContinue)
|
|
1952
|
+
return;
|
|
1953
|
+
if (nativeRetrySessions.has(sessionID))
|
|
1954
|
+
return;
|
|
1512
1955
|
const goal = await reserveContinuation(sessionID, maxAutoTurns, minInterval);
|
|
1513
1956
|
if (!goal)
|
|
1514
1957
|
return;
|
|
1958
|
+
attemptReservedAt = goal.pendingAttempt?.reservedAt ?? Date.now();
|
|
1959
|
+
if (nativeRetrySessions.has(sessionID)) {
|
|
1960
|
+
await rollbackContinuationAttempt(sessionID);
|
|
1961
|
+
return;
|
|
1962
|
+
}
|
|
1963
|
+
if (scheduled && scheduledContinuations.get(sessionID) !== scheduled) {
|
|
1964
|
+
await rollbackContinuationAttempt(sessionID);
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1515
1967
|
await sendContinuation(client, sessionID, goal.status === "active" ? continuationPrompt(goal) : limitPrompt(goal), goal.lastPromptAgent ?? latestTurnAgent ?? null);
|
|
1516
|
-
|
|
1968
|
+
if (disposed) {
|
|
1969
|
+
await rollbackContinuationAttempt(sessionID);
|
|
1970
|
+
return;
|
|
1971
|
+
}
|
|
1972
|
+
const delivered = await recordContinuationResult(sessionID, "success", maxPromptFailures);
|
|
1973
|
+
locallyDeliveredPendingSessions.add(sessionID);
|
|
1974
|
+
if (!delivered?.pendingAttempt?.delivered) {
|
|
1975
|
+
await rollbackContinuationAttempt(sessionID);
|
|
1976
|
+
}
|
|
1517
1977
|
} catch (error) {
|
|
1518
|
-
|
|
1978
|
+
if (disposed) {
|
|
1979
|
+
await rollbackContinuationAttempt(sessionID);
|
|
1980
|
+
return;
|
|
1981
|
+
}
|
|
1982
|
+
if (isTransportError(error)) {
|
|
1983
|
+
const afterFailure = await recordContinuationResult(sessionID, "failure", maxPromptFailures);
|
|
1984
|
+
if (autoContinue && afterFailure?.status === "active") {
|
|
1985
|
+
scheduleSettledContinuation(sessionID, continuationRetryDelayMs(minInterval, attemptReservedAt), true, "retry");
|
|
1986
|
+
}
|
|
1987
|
+
} else {
|
|
1988
|
+
await rollbackContinuationAttempt(sessionID);
|
|
1989
|
+
}
|
|
1519
1990
|
await client.app?.log?.({
|
|
1520
1991
|
body: {
|
|
1521
1992
|
service: "opencode-goal-plugin",
|
|
@@ -1530,12 +2001,17 @@ var server = async ({ client }, options) => {
|
|
|
1530
2001
|
}
|
|
1531
2002
|
return {
|
|
1532
2003
|
async dispose() {
|
|
1533
|
-
|
|
1534
|
-
|
|
2004
|
+
disposed = true;
|
|
2005
|
+
for (const scheduled of scheduledContinuations.values())
|
|
2006
|
+
clearTimeout(scheduled.timer);
|
|
1535
2007
|
scheduledContinuations.clear();
|
|
1536
2008
|
for (const watchdog of turnWatchdogs.values())
|
|
1537
2009
|
clearTimeout(watchdog.timer);
|
|
1538
2010
|
turnWatchdogs.clear();
|
|
2011
|
+
watchdogRescuedSessions.clear();
|
|
2012
|
+
locallyDeliveredPendingSessions.clear();
|
|
2013
|
+
nativeRetrySessions.clear();
|
|
2014
|
+
toolAttempts.clear();
|
|
1539
2015
|
},
|
|
1540
2016
|
async config(config) {
|
|
1541
2017
|
if (!registerCommand)
|
|
@@ -1622,9 +2098,41 @@ var server = async ({ client }, options) => {
|
|
|
1622
2098
|
},
|
|
1623
2099
|
async "tool.execute.before"(input) {
|
|
1624
2100
|
taskTracker.noteTaskCall(input);
|
|
2101
|
+
const sessionID = typeof input?.sessionID === "string" ? input.sessionID : undefined;
|
|
2102
|
+
const callID = typeof input?.callID === "string" ? input.callID : undefined;
|
|
2103
|
+
if (sessionID && callID) {
|
|
2104
|
+
const goal = await getGoalInternal(sessionID);
|
|
2105
|
+
toolAttempts.set(toolAttemptKey(sessionID, callID), goal?.pendingAttempt?.id ?? null);
|
|
2106
|
+
}
|
|
1625
2107
|
},
|
|
1626
2108
|
async "tool.execute.after"(input, output) {
|
|
1627
2109
|
taskTracker.noteTaskOutput(input, output);
|
|
2110
|
+
const sessionID = typeof input?.sessionID === "string" ? input.sessionID : undefined;
|
|
2111
|
+
const callID = typeof input?.callID === "string" ? input.callID : undefined;
|
|
2112
|
+
const attemptKey = sessionID && callID ? toolAttemptKey(sessionID, callID) : undefined;
|
|
2113
|
+
const expectedAttemptID = attemptKey ? toolAttempts.get(attemptKey) : undefined;
|
|
2114
|
+
if (attemptKey)
|
|
2115
|
+
toolAttempts.delete(attemptKey);
|
|
2116
|
+
if (!sessionID)
|
|
2117
|
+
return;
|
|
2118
|
+
if (typeof input?.tool === "string" && NON_PROGRESS_TOOLS.has(input.tool.toLowerCase()))
|
|
2119
|
+
return;
|
|
2120
|
+
const toolResult = output;
|
|
2121
|
+
if (toolOutputFailed(toolResult))
|
|
2122
|
+
return;
|
|
2123
|
+
const text = typeof toolResult.output === "string" ? toolResult.output : undefined;
|
|
2124
|
+
if (!text)
|
|
2125
|
+
return;
|
|
2126
|
+
const before = await getGoalInternal(sessionID);
|
|
2127
|
+
const scheduled = scheduledContinuations.get(sessionID);
|
|
2128
|
+
const hasFailureEpisode = Boolean(before && (before.continuationFailures > 0 || before.pendingAttempt != null));
|
|
2129
|
+
if (!before || !hasFailureEpisode && scheduled?.purpose !== "recovery")
|
|
2130
|
+
return;
|
|
2131
|
+
const progressed = await recordToolProgress(sessionID, text, expectedAttemptID);
|
|
2132
|
+
if (progressed?.continuationFailures === 0 && progressed.pendingAttempt == null) {
|
|
2133
|
+
locallyDeliveredPendingSessions.delete(sessionID);
|
|
2134
|
+
cancelScheduledContinuation(sessionID);
|
|
2135
|
+
}
|
|
1628
2136
|
},
|
|
1629
2137
|
async "chat.message"(input, output) {
|
|
1630
2138
|
const sessionID = typeof input?.sessionID === "string" ? input.sessionID : output.message?.sessionID;
|
|
@@ -1639,7 +2147,11 @@ var server = async ({ client }, options) => {
|
|
|
1639
2147
|
if (!sessionID)
|
|
1640
2148
|
return;
|
|
1641
2149
|
await accountUsage(sessionID, tokensFromMessages(output.messages));
|
|
1642
|
-
await recordAssistantMessage(sessionID, latestAssistantMessage(output.messages), options ?? {});
|
|
2150
|
+
const observed = await recordAssistantMessage(sessionID, latestAssistantMessage(output.messages), options ?? {});
|
|
2151
|
+
await reconcileLocalMarkerAfterProgress(locallyDeliveredPendingSessions, sessionID, observed.goal);
|
|
2152
|
+
const scheduled = scheduledContinuations.get(sessionID);
|
|
2153
|
+
if (observed.progressed && scheduled?.purpose !== "settle")
|
|
2154
|
+
cancelScheduledContinuation(sessionID);
|
|
1643
2155
|
},
|
|
1644
2156
|
async "experimental.chat.system.transform"(input, output) {
|
|
1645
2157
|
if (typeof input.sessionID !== "string")
|
|
@@ -1666,44 +2178,91 @@ var server = async ({ client }, options) => {
|
|
|
1666
2178
|
if (sessionID && eventType === "session.status") {
|
|
1667
2179
|
const status = event.properties?.status;
|
|
1668
2180
|
if (isRecord(status) && typeof status.type === "string") {
|
|
1669
|
-
if (status.type === "busy")
|
|
2181
|
+
if (status.type === "busy") {
|
|
1670
2182
|
busySessions.add(sessionID);
|
|
2183
|
+
nativeRetrySessions.delete(sessionID);
|
|
2184
|
+
}
|
|
1671
2185
|
if (status.type === "busy")
|
|
1672
2186
|
armTurnWatchdog(sessionID);
|
|
2187
|
+
if (status.type === "busy")
|
|
2188
|
+
await markPendingContinuationStarted(sessionID);
|
|
1673
2189
|
if (status.type === "idle") {
|
|
1674
2190
|
busySessions.delete(sessionID);
|
|
2191
|
+
nativeRetrySessions.delete(sessionID);
|
|
1675
2192
|
clearTurnWatchdog(sessionID);
|
|
2193
|
+
watchdogRescuedSessions.delete(sessionID);
|
|
1676
2194
|
}
|
|
1677
|
-
if (status.type === "retry")
|
|
2195
|
+
if (status.type === "retry") {
|
|
2196
|
+
nativeRetrySessions.add(sessionID);
|
|
1678
2197
|
clearTurnWatchdog(sessionID);
|
|
2198
|
+
cancelScheduledContinuation(sessionID);
|
|
2199
|
+
}
|
|
1679
2200
|
taskTracker.observeSessionStatus(sessionID, status.type);
|
|
1680
2201
|
}
|
|
1681
2202
|
}
|
|
1682
2203
|
if (sessionID && eventType === "session.idle") {
|
|
1683
2204
|
busySessions.delete(sessionID);
|
|
2205
|
+
nativeRetrySessions.delete(sessionID);
|
|
1684
2206
|
clearTurnWatchdog(sessionID);
|
|
2207
|
+
watchdogRescuedSessions.delete(sessionID);
|
|
1685
2208
|
taskTracker.observeSessionStatus(sessionID, "idle");
|
|
1686
2209
|
}
|
|
2210
|
+
if (sessionID && eventType === "session.error") {
|
|
2211
|
+
const inNativeRetry = nativeRetrySessions.has(sessionID);
|
|
2212
|
+
busySessions.delete(sessionID);
|
|
2213
|
+
clearTurnWatchdog(sessionID);
|
|
2214
|
+
if (inNativeRetry)
|
|
2215
|
+
return;
|
|
2216
|
+
nativeRetrySessions.delete(sessionID);
|
|
2217
|
+
watchdogRescuedSessions.delete(sessionID);
|
|
2218
|
+
const props = event.properties ?? {};
|
|
2219
|
+
const errorMessage = transportErrorMessageFromEvent(props);
|
|
2220
|
+
if (errorMessage && isTransportError(errorMessage)) {
|
|
2221
|
+
const goal = await getGoalInternal(sessionID);
|
|
2222
|
+
if (goal?.status === "active") {
|
|
2223
|
+
const attempt = pendingAttemptOf(goal);
|
|
2224
|
+
if (attempt != null) {
|
|
2225
|
+
const afterFailure = await recordContinuationResult(sessionID, "failure", maxPromptFailures, {
|
|
2226
|
+
requirePending: true
|
|
2227
|
+
});
|
|
2228
|
+
if (afterFailure)
|
|
2229
|
+
locallyDeliveredPendingSessions.delete(sessionID);
|
|
2230
|
+
if (autoContinue && afterFailure?.status === "active") {
|
|
2231
|
+
scheduleSettledContinuation(sessionID, continuationRetryDelayMs(minInterval, attempt.reservedAt), true, "retry");
|
|
2232
|
+
}
|
|
2233
|
+
} else if (autoContinue) {
|
|
2234
|
+
scheduleSettledContinuation(sessionID, continuationDelayFromSnapshot(minInterval, goal.lastContinuationAt), false, "recovery");
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
1687
2239
|
if (sessionID && eventType === "session.deleted") {
|
|
1688
2240
|
busySessions.delete(sessionID);
|
|
1689
2241
|
clearTurnWatchdog(sessionID);
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
2242
|
+
watchdogRescuedSessions.delete(sessionID);
|
|
2243
|
+
locallyDeliveredPendingSessions.delete(sessionID);
|
|
2244
|
+
nativeRetrySessions.delete(sessionID);
|
|
2245
|
+
cancelScheduledContinuation(sessionID);
|
|
1694
2246
|
taskDeferredSessions.delete(sessionID);
|
|
2247
|
+
clearToolAttemptsForSession(toolAttempts, sessionID);
|
|
1695
2248
|
taskTracker.observeSessionDeleted(sessionID);
|
|
1696
2249
|
}
|
|
1697
2250
|
if (sessionID && event.type === "message.updated") {
|
|
1698
2251
|
const props = event.properties ?? {};
|
|
1699
2252
|
const message = [props.info, props.message].find((value) => value && typeof value === "object");
|
|
1700
2253
|
taskTracker.observeAssistantMessage(sessionID, message);
|
|
1701
|
-
await recordAssistantMessage(sessionID, message, options ?? {});
|
|
2254
|
+
const observed = await recordAssistantMessage(sessionID, message, options ?? {});
|
|
2255
|
+
await reconcileLocalMarkerAfterProgress(locallyDeliveredPendingSessions, sessionID, observed.goal);
|
|
2256
|
+
const scheduled = scheduledContinuations.get(sessionID);
|
|
2257
|
+
if (observed.progressed && scheduled?.purpose !== "settle")
|
|
2258
|
+
cancelScheduledContinuation(sessionID);
|
|
1702
2259
|
}
|
|
1703
|
-
if (!
|
|
2260
|
+
if (!isIdleEvent(event))
|
|
1704
2261
|
return;
|
|
1705
2262
|
if (!sessionID)
|
|
1706
2263
|
return;
|
|
2264
|
+
if (!autoContinue && (await getGoalInternal(sessionID))?.pendingAttempt == null)
|
|
2265
|
+
return;
|
|
1707
2266
|
await runAutoContinue(sessionID);
|
|
1708
2267
|
}
|
|
1709
2268
|
};
|
|
@@ -1718,7 +2277,7 @@ async function setupV2(context) {
|
|
|
1718
2277
|
const autoContinue = options.auto_continue ?? true;
|
|
1719
2278
|
const deferWhileTasksActive = options.defer_while_tasks_active ?? true;
|
|
1720
2279
|
const maxAutoTurns = positiveIntegerOrNull2(options.max_auto_turns) ?? DEFAULT_MAX_AUTO_TURNS;
|
|
1721
|
-
const minInterval =
|
|
2280
|
+
const minInterval = nonNegativeIntegerOrNull(options.min_continue_interval_seconds) ?? DEFAULT_CONTINUE_INTERVAL_SECONDS;
|
|
1722
2281
|
const maxTurnTimeMs = timeoutMillisecondsFromSeconds(options.max_turn_time);
|
|
1723
2282
|
const maxPromptFailures = positiveIntegerOrNull2(options.max_prompt_failures) ?? DEFAULT_MAX_PROMPT_FAILURES;
|
|
1724
2283
|
const registerCommand = options.register_command ?? true;
|
|
@@ -1728,6 +2287,10 @@ async function setupV2(context) {
|
|
|
1728
2287
|
const scheduledContinuations = new Map;
|
|
1729
2288
|
const turnWatchdogs = new Map;
|
|
1730
2289
|
const busySessions = new Set;
|
|
2290
|
+
const nativeRetrySessions = new Set;
|
|
2291
|
+
const locallyDeliveredPendingSessions = new Set;
|
|
2292
|
+
const watchdogRescuedSessions = new Set;
|
|
2293
|
+
const toolAttempts = new Map;
|
|
1731
2294
|
const planAgents = restrictedAgentSet(options);
|
|
1732
2295
|
const isPlanAgent = (agent) => typeof agent === "string" && planAgents.has(agent.trim().toLowerCase());
|
|
1733
2296
|
const goalServices = { options, isPlanAgent };
|
|
@@ -1736,6 +2299,7 @@ async function setupV2(context) {
|
|
|
1736
2299
|
const stepTextBuffers = new Map;
|
|
1737
2300
|
const stepTokenSums = new Map;
|
|
1738
2301
|
const registrations = [];
|
|
2302
|
+
let disposed = false;
|
|
1739
2303
|
function stepKey(sessionID, messageID2) {
|
|
1740
2304
|
return `${sessionID}\x00${messageID2}`;
|
|
1741
2305
|
}
|
|
@@ -1764,6 +2328,8 @@ async function setupV2(context) {
|
|
|
1764
2328
|
function armTurnWatchdog(sessionID) {
|
|
1765
2329
|
if (maxTurnTimeMs == null)
|
|
1766
2330
|
return;
|
|
2331
|
+
if (watchdogRescuedSessions.has(sessionID))
|
|
2332
|
+
return;
|
|
1767
2333
|
clearTurnWatchdog(sessionID);
|
|
1768
2334
|
const watchdog = {
|
|
1769
2335
|
timer: setTimeout(() => void runTurnWatchdog(sessionID, watchdog), maxTurnTimeMs)
|
|
@@ -1776,7 +2342,9 @@ async function setupV2(context) {
|
|
|
1776
2342
|
async function runTurnWatchdog(sessionID, watchdog) {
|
|
1777
2343
|
let claimedContinuation = false;
|
|
1778
2344
|
try {
|
|
1779
|
-
if (
|
|
2345
|
+
if (disposed)
|
|
2346
|
+
return;
|
|
2347
|
+
if (turnWatchdogs.get(sessionID) !== watchdog || !busySessions.has(sessionID) || watchdogRescuedSessions.has(sessionID))
|
|
1780
2348
|
return;
|
|
1781
2349
|
const goal = await getGoal(sessionID);
|
|
1782
2350
|
if (turnWatchdogs.get(sessionID) !== watchdog || !busySessions.has(sessionID))
|
|
@@ -1791,7 +2359,7 @@ async function setupV2(context) {
|
|
|
1791
2359
|
return;
|
|
1792
2360
|
if (taskStatus && taskStatus.blocked)
|
|
1793
2361
|
return;
|
|
1794
|
-
const current = await
|
|
2362
|
+
const current = await getGoalInternal(sessionID);
|
|
1795
2363
|
if (turnWatchdogs.get(sessionID) !== watchdog || !busySessions.has(sessionID))
|
|
1796
2364
|
return;
|
|
1797
2365
|
if (current?.status !== "active" || isPlanAgent(current.lastPromptAgent) || activeContinuationsV2.has(sessionID))
|
|
@@ -1799,9 +2367,20 @@ async function setupV2(context) {
|
|
|
1799
2367
|
turnWatchdogs.delete(sessionID);
|
|
1800
2368
|
activeContinuationsV2.add(sessionID);
|
|
1801
2369
|
claimedContinuation = true;
|
|
2370
|
+
watchdogRescuedSessions.add(sessionID);
|
|
1802
2371
|
await sendContinuation2(sessionID, continuationPrompt(current), current.lastPromptAgent ?? latestStep?.agent ?? null);
|
|
2372
|
+
await recordContinuationResult(sessionID, "success", maxPromptFailures, { armNoProgress: false, started: true });
|
|
2373
|
+
locallyDeliveredPendingSessions.add(sessionID);
|
|
2374
|
+
clearTurnWatchdog(sessionID);
|
|
1803
2375
|
} catch (error) {
|
|
1804
|
-
|
|
2376
|
+
try {
|
|
2377
|
+
if (claimedContinuation && isTransportError(error)) {
|
|
2378
|
+
await recordContinuationResult(sessionID, "failure", maxPromptFailures);
|
|
2379
|
+
}
|
|
2380
|
+
v2ErrorLog("Turn watchdog retry failed", error);
|
|
2381
|
+
} catch {
|
|
2382
|
+
return;
|
|
2383
|
+
}
|
|
1805
2384
|
} finally {
|
|
1806
2385
|
if (claimedContinuation)
|
|
1807
2386
|
activeContinuationsV2.delete(sessionID);
|
|
@@ -1809,24 +2388,53 @@ async function setupV2(context) {
|
|
|
1809
2388
|
turnWatchdogs.delete(sessionID);
|
|
1810
2389
|
}
|
|
1811
2390
|
}
|
|
1812
|
-
function
|
|
1813
|
-
|
|
2391
|
+
function cancelScheduledContinuation(sessionID) {
|
|
2392
|
+
const scheduled = scheduledContinuations.get(sessionID);
|
|
2393
|
+
if (scheduled)
|
|
2394
|
+
clearTimeout(scheduled.timer);
|
|
2395
|
+
scheduledContinuations.delete(sessionID);
|
|
2396
|
+
}
|
|
2397
|
+
function scheduleSettledContinuation(sessionID, delayMs = TASK_SETTLE_DELAY_MS, replace = false, purpose = "settle") {
|
|
2398
|
+
if (disposed)
|
|
2399
|
+
return;
|
|
2400
|
+
if (!replace && scheduledContinuations.has(sessionID))
|
|
1814
2401
|
return;
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
2402
|
+
if (replace)
|
|
2403
|
+
cancelScheduledContinuation(sessionID);
|
|
2404
|
+
const scheduled = {};
|
|
2405
|
+
const timer = setTimeout(async () => {
|
|
2406
|
+
try {
|
|
2407
|
+
if (scheduledContinuations.get(sessionID) !== scheduled || nativeRetrySessions.has(sessionID))
|
|
2408
|
+
return;
|
|
2409
|
+
if (purpose === "retry") {
|
|
2410
|
+
const goal = await getGoalInternal(sessionID);
|
|
2411
|
+
if (!goal || goal.continuationFailures === 0 && goal.pendingAttempt == null)
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
if (scheduledContinuations.get(sessionID) !== scheduled || nativeRetrySessions.has(sessionID))
|
|
2415
|
+
return;
|
|
2416
|
+
await runAutoContinue(sessionID, true, scheduled);
|
|
2417
|
+
} finally {
|
|
2418
|
+
if (scheduledContinuations.get(sessionID) === scheduled)
|
|
2419
|
+
scheduledContinuations.delete(sessionID);
|
|
2420
|
+
}
|
|
1818
2421
|
}, Math.max(0, delayMs));
|
|
2422
|
+
scheduled.timer = timer;
|
|
2423
|
+
scheduled.purpose = purpose;
|
|
1819
2424
|
const maybeUnref = timer;
|
|
1820
2425
|
if (typeof maybeUnref.unref === "function")
|
|
1821
2426
|
maybeUnref.unref();
|
|
1822
|
-
scheduledContinuations.set(sessionID,
|
|
2427
|
+
scheduledContinuations.set(sessionID, scheduled);
|
|
1823
2428
|
}
|
|
1824
|
-
async function runAutoContinue(sessionID, fromTaskDeferral = false) {
|
|
2429
|
+
async function runAutoContinue(sessionID, fromTaskDeferral = false, scheduled) {
|
|
2430
|
+
if (disposed)
|
|
2431
|
+
return;
|
|
1825
2432
|
if (busySessions.has(sessionID))
|
|
1826
2433
|
return;
|
|
1827
2434
|
if (activeContinuationsV2.has(sessionID))
|
|
1828
2435
|
return;
|
|
1829
2436
|
activeContinuationsV2.add(sessionID);
|
|
2437
|
+
let attemptReservedAt = Date.now();
|
|
1830
2438
|
try {
|
|
1831
2439
|
const latestStep = latestStepBySession.get(sessionID);
|
|
1832
2440
|
if (latestStep?.messageID) {
|
|
@@ -1835,23 +2443,33 @@ async function setupV2(context) {
|
|
|
1835
2443
|
const taskStatus = taskBlockStatus(sessionID);
|
|
1836
2444
|
if (taskStatus && taskStatus.blocked) {
|
|
1837
2445
|
taskDeferredSessions.add(sessionID);
|
|
1838
|
-
if (taskStatus.retryAt != null)
|
|
1839
|
-
scheduleSettledContinuation(sessionID, taskStatus.retryAt - Date.now());
|
|
2446
|
+
if (taskStatus.retryAt != null) {
|
|
2447
|
+
scheduleSettledContinuation(sessionID, taskStatus.retryAt - Date.now(), scheduled != null);
|
|
2448
|
+
}
|
|
1840
2449
|
return;
|
|
1841
2450
|
}
|
|
1842
2451
|
if (busySessions.has(sessionID))
|
|
1843
2452
|
return;
|
|
1844
2453
|
if (latestStep) {
|
|
1845
|
-
await
|
|
2454
|
+
const beforeProgress = await getGoalInternal(sessionID);
|
|
2455
|
+
const after = await recordAssistantProgress(sessionID, {
|
|
1846
2456
|
messageID: latestStep.messageID,
|
|
1847
2457
|
text: latestStep.text,
|
|
1848
2458
|
outputTokens: latestStep.outputTokens,
|
|
1849
2459
|
noProgressTokenThreshold: positiveIntegerOrNull2(options.no_progress_token_threshold),
|
|
1850
2460
|
maxNoProgressTurns: positiveIntegerOrNull2(options.max_no_progress_turns),
|
|
1851
|
-
evaluateContinuation: true
|
|
2461
|
+
evaluateContinuation: true,
|
|
2462
|
+
completedAt: latestStep.completedAt
|
|
1852
2463
|
});
|
|
2464
|
+
await reconcileLocalMarkerAfterProgress(locallyDeliveredPendingSessions, sessionID, after);
|
|
2465
|
+
const progressed = Boolean(after && (after.lastAssistantMessageID !== (beforeProgress?.lastAssistantMessageID ?? "") || after.lastAssistantText !== (beforeProgress?.lastAssistantText ?? "")));
|
|
2466
|
+
const queuedAfterProgress = scheduledContinuations.get(sessionID);
|
|
2467
|
+
if (progressed && queuedAfterProgress?.purpose !== "settle")
|
|
2468
|
+
cancelScheduledContinuation(sessionID);
|
|
1853
2469
|
}
|
|
1854
|
-
|
|
2470
|
+
if (scheduled && scheduledContinuations.get(sessionID) !== scheduled)
|
|
2471
|
+
return;
|
|
2472
|
+
const current = await getGoalInternal(sessionID);
|
|
1855
2473
|
if (!current)
|
|
1856
2474
|
return;
|
|
1857
2475
|
const latestTurnAgent = latestStep?.agent;
|
|
@@ -1867,13 +2485,64 @@ async function setupV2(context) {
|
|
|
1867
2485
|
return;
|
|
1868
2486
|
}
|
|
1869
2487
|
taskDeferredSessions.delete(sessionID);
|
|
2488
|
+
const attempt = pendingAttemptOf(current);
|
|
2489
|
+
if (current.status === "active" && attempt != null) {
|
|
2490
|
+
const deliveredLocally = locallyDeliveredPendingSessions.has(sessionID);
|
|
2491
|
+
if (!pendingReadyForFailure(attempt, deliveredLocally)) {
|
|
2492
|
+
return;
|
|
2493
|
+
}
|
|
2494
|
+
const afterFailure = await recordContinuationResult(sessionID, "failure", maxPromptFailures, {
|
|
2495
|
+
requirePending: true
|
|
2496
|
+
});
|
|
2497
|
+
if (afterFailure)
|
|
2498
|
+
locallyDeliveredPendingSessions.delete(sessionID);
|
|
2499
|
+
if (autoContinue && afterFailure?.status === "active") {
|
|
2500
|
+
scheduleSettledContinuation(sessionID, continuationRetryDelayMs(minInterval, attempt.reservedAt), true, "retry");
|
|
2501
|
+
}
|
|
2502
|
+
return;
|
|
2503
|
+
}
|
|
2504
|
+
const queuedBeforeReserve = scheduledContinuations.get(sessionID);
|
|
2505
|
+
if (queuedBeforeReserve && queuedBeforeReserve !== scheduled)
|
|
2506
|
+
return;
|
|
2507
|
+
if (!autoContinue)
|
|
2508
|
+
return;
|
|
2509
|
+
if (nativeRetrySessions.has(sessionID))
|
|
2510
|
+
return;
|
|
1870
2511
|
const goal = await reserveContinuation(sessionID, maxAutoTurns, minInterval);
|
|
1871
2512
|
if (!goal)
|
|
1872
2513
|
return;
|
|
2514
|
+
attemptReservedAt = goal.pendingAttempt?.reservedAt ?? Date.now();
|
|
2515
|
+
if (nativeRetrySessions.has(sessionID)) {
|
|
2516
|
+
await rollbackContinuationAttempt(sessionID);
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
if (scheduled && scheduledContinuations.get(sessionID) !== scheduled) {
|
|
2520
|
+
await rollbackContinuationAttempt(sessionID);
|
|
2521
|
+
return;
|
|
2522
|
+
}
|
|
1873
2523
|
await sendContinuation2(sessionID, goal.status === "active" ? continuationPrompt(goal) : limitPrompt(goal), goal.lastPromptAgent ?? latestTurnAgent ?? null);
|
|
1874
|
-
|
|
2524
|
+
if (disposed) {
|
|
2525
|
+
await rollbackContinuationAttempt(sessionID);
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
const delivered = await recordContinuationResult(sessionID, "success", maxPromptFailures);
|
|
2529
|
+
locallyDeliveredPendingSessions.add(sessionID);
|
|
2530
|
+
if (!delivered?.pendingAttempt?.delivered) {
|
|
2531
|
+
await rollbackContinuationAttempt(sessionID);
|
|
2532
|
+
}
|
|
1875
2533
|
} catch (error) {
|
|
1876
|
-
|
|
2534
|
+
if (disposed) {
|
|
2535
|
+
await rollbackContinuationAttempt(sessionID);
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
if (isTransportError(error)) {
|
|
2539
|
+
const afterFailure = await recordContinuationResult(sessionID, "failure", maxPromptFailures);
|
|
2540
|
+
if (autoContinue && afterFailure?.status === "active") {
|
|
2541
|
+
scheduleSettledContinuation(sessionID, continuationRetryDelayMs(minInterval, attemptReservedAt), true, "retry");
|
|
2542
|
+
}
|
|
2543
|
+
} else {
|
|
2544
|
+
await rollbackContinuationAttempt(sessionID);
|
|
2545
|
+
}
|
|
1877
2546
|
v2ErrorLog("Auto-continue failed", error);
|
|
1878
2547
|
} finally {
|
|
1879
2548
|
activeContinuationsV2.delete(sessionID);
|
|
@@ -1893,31 +2562,76 @@ async function setupV2(context) {
|
|
|
1893
2562
|
case "session.status": {
|
|
1894
2563
|
const status = data.status;
|
|
1895
2564
|
if (sessionID && isRecord(status) && typeof status.type === "string") {
|
|
1896
|
-
if (status.type === "busy")
|
|
2565
|
+
if (status.type === "busy") {
|
|
1897
2566
|
busySessions.add(sessionID);
|
|
1898
|
-
|
|
2567
|
+
nativeRetrySessions.delete(sessionID);
|
|
1899
2568
|
armTurnWatchdog(sessionID);
|
|
2569
|
+
await markPendingContinuationStarted(sessionID);
|
|
2570
|
+
}
|
|
1900
2571
|
if (status.type === "idle") {
|
|
1901
2572
|
busySessions.delete(sessionID);
|
|
2573
|
+
nativeRetrySessions.delete(sessionID);
|
|
1902
2574
|
clearTurnWatchdog(sessionID);
|
|
2575
|
+
watchdogRescuedSessions.delete(sessionID);
|
|
1903
2576
|
}
|
|
1904
|
-
if (status.type === "retry")
|
|
2577
|
+
if (status.type === "retry") {
|
|
2578
|
+
nativeRetrySessions.add(sessionID);
|
|
1905
2579
|
clearTurnWatchdog(sessionID);
|
|
2580
|
+
cancelScheduledContinuation(sessionID);
|
|
2581
|
+
}
|
|
1906
2582
|
taskTracker.observeSessionStatus(sessionID, status.type);
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
2583
|
+
if (status.type === "idle") {
|
|
2584
|
+
const goal = await getGoalInternal(sessionID);
|
|
2585
|
+
if (autoContinue || goal?.pendingAttempt != null)
|
|
2586
|
+
await runAutoContinue(sessionID);
|
|
2587
|
+
}
|
|
1910
2588
|
}
|
|
1911
2589
|
return;
|
|
1912
2590
|
}
|
|
1913
2591
|
case "session.idle": {
|
|
1914
2592
|
if (sessionID) {
|
|
1915
2593
|
busySessions.delete(sessionID);
|
|
2594
|
+
nativeRetrySessions.delete(sessionID);
|
|
1916
2595
|
clearTurnWatchdog(sessionID);
|
|
2596
|
+
watchdogRescuedSessions.delete(sessionID);
|
|
1917
2597
|
taskTracker.observeSessionStatus(sessionID, "idle");
|
|
1918
2598
|
}
|
|
1919
|
-
if (
|
|
1920
|
-
await
|
|
2599
|
+
if (sessionID) {
|
|
2600
|
+
const goal = await getGoalInternal(sessionID);
|
|
2601
|
+
if (autoContinue || goal?.pendingAttempt != null)
|
|
2602
|
+
await runAutoContinue(sessionID);
|
|
2603
|
+
}
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
case "session.error": {
|
|
2607
|
+
if (!sessionID)
|
|
2608
|
+
return;
|
|
2609
|
+
const inNativeRetry = nativeRetrySessions.has(sessionID);
|
|
2610
|
+
busySessions.delete(sessionID);
|
|
2611
|
+
clearTurnWatchdog(sessionID);
|
|
2612
|
+
if (inNativeRetry)
|
|
2613
|
+
return;
|
|
2614
|
+
nativeRetrySessions.delete(sessionID);
|
|
2615
|
+
watchdogRescuedSessions.delete(sessionID);
|
|
2616
|
+
const errorMessage = transportErrorMessageFromEvent(data);
|
|
2617
|
+
if (errorMessage && isTransportError(errorMessage)) {
|
|
2618
|
+
const goal = await getGoalInternal(sessionID);
|
|
2619
|
+
if (goal?.status === "active") {
|
|
2620
|
+
const attempt = pendingAttemptOf(goal);
|
|
2621
|
+
if (attempt != null) {
|
|
2622
|
+
const afterFailure = await recordContinuationResult(sessionID, "failure", maxPromptFailures, {
|
|
2623
|
+
requirePending: true
|
|
2624
|
+
});
|
|
2625
|
+
if (afterFailure)
|
|
2626
|
+
locallyDeliveredPendingSessions.delete(sessionID);
|
|
2627
|
+
if (autoContinue && afterFailure?.status === "active") {
|
|
2628
|
+
scheduleSettledContinuation(sessionID, continuationRetryDelayMs(minInterval, attempt.reservedAt), true, "retry");
|
|
2629
|
+
}
|
|
2630
|
+
} else if (autoContinue) {
|
|
2631
|
+
scheduleSettledContinuation(sessionID, continuationDelayFromSnapshot(minInterval, goal.lastContinuationAt), false, "recovery");
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
1921
2635
|
return;
|
|
1922
2636
|
}
|
|
1923
2637
|
case "session.deleted": {
|
|
@@ -1925,11 +2639,15 @@ async function setupV2(context) {
|
|
|
1925
2639
|
return;
|
|
1926
2640
|
busySessions.delete(sessionID);
|
|
1927
2641
|
clearTurnWatchdog(sessionID);
|
|
2642
|
+
watchdogRescuedSessions.delete(sessionID);
|
|
2643
|
+
locallyDeliveredPendingSessions.delete(sessionID);
|
|
2644
|
+
nativeRetrySessions.delete(sessionID);
|
|
1928
2645
|
const scheduled = scheduledContinuations.get(sessionID);
|
|
1929
2646
|
if (scheduled)
|
|
1930
|
-
clearTimeout(scheduled);
|
|
2647
|
+
clearTimeout(scheduled.timer);
|
|
1931
2648
|
scheduledContinuations.delete(sessionID);
|
|
1932
2649
|
taskDeferredSessions.delete(sessionID);
|
|
2650
|
+
clearToolAttemptsForSession(toolAttempts, sessionID);
|
|
1933
2651
|
taskTracker.observeSessionDeleted(sessionID);
|
|
1934
2652
|
latestStepBySession.delete(sessionID);
|
|
1935
2653
|
stepTokenSums.delete(sessionID);
|
|
@@ -1956,7 +2674,7 @@ async function setupV2(context) {
|
|
|
1956
2674
|
});
|
|
1957
2675
|
if (!stepTextBuffers.has(stepKey(sessionID, messageID2)))
|
|
1958
2676
|
stepTextBuffers.set(stepKey(sessionID, messageID2), "");
|
|
1959
|
-
latestStepBySession.set(sessionID, { messageID: messageID2, agent, text: "", outputTokens: null });
|
|
2677
|
+
latestStepBySession.set(sessionID, { messageID: messageID2, agent, text: "", outputTokens: null, completedAt: event.created });
|
|
1960
2678
|
return;
|
|
1961
2679
|
}
|
|
1962
2680
|
case "session.text.delta": {
|
|
@@ -1985,18 +2703,26 @@ async function setupV2(context) {
|
|
|
1985
2703
|
const text = stepTextBuffers.get(stepKey(sessionID, messageID2)) ?? "";
|
|
1986
2704
|
stepTextBuffers.delete(stepKey(sessionID, messageID2));
|
|
1987
2705
|
const outputTokens = outputTokensFromRecord(data.tokens) ?? null;
|
|
1988
|
-
await recordAssistantProgress(sessionID, {
|
|
2706
|
+
const afterStep = await recordAssistantProgress(sessionID, {
|
|
1989
2707
|
messageID: messageID2,
|
|
1990
2708
|
text,
|
|
1991
2709
|
outputTokens,
|
|
1992
2710
|
noProgressTokenThreshold: positiveIntegerOrNull2(options.no_progress_token_threshold),
|
|
1993
|
-
maxNoProgressTurns: positiveIntegerOrNull2(options.max_no_progress_turns)
|
|
2711
|
+
maxNoProgressTurns: positiveIntegerOrNull2(options.max_no_progress_turns),
|
|
2712
|
+
completedAt: event.created
|
|
1994
2713
|
});
|
|
2714
|
+
await reconcileLocalMarkerAfterProgress(locallyDeliveredPendingSessions, sessionID, afterStep);
|
|
2715
|
+
if (/[\p{L}\p{N}]/u.test(text)) {
|
|
2716
|
+
const scheduled = scheduledContinuations.get(sessionID);
|
|
2717
|
+
if (scheduled?.purpose === "recovery")
|
|
2718
|
+
cancelScheduledContinuation(sessionID);
|
|
2719
|
+
}
|
|
1995
2720
|
latestStepBySession.set(sessionID, {
|
|
1996
2721
|
messageID: messageID2,
|
|
1997
2722
|
agent: latestStepBySession.get(sessionID)?.agent,
|
|
1998
2723
|
text,
|
|
1999
|
-
outputTokens
|
|
2724
|
+
outputTokens,
|
|
2725
|
+
completedAt: event.created
|
|
2000
2726
|
});
|
|
2001
2727
|
return;
|
|
2002
2728
|
}
|
|
@@ -2013,18 +2739,26 @@ async function setupV2(context) {
|
|
|
2013
2739
|
const text = stepTextBuffers.get(stepKey(sessionID, messageID2)) ?? "";
|
|
2014
2740
|
stepTextBuffers.delete(stepKey(sessionID, messageID2));
|
|
2015
2741
|
const outputTokens = outputTokensFromRecord(data.tokens) ?? null;
|
|
2016
|
-
await recordAssistantProgress(sessionID, {
|
|
2742
|
+
const afterStep = await recordAssistantProgress(sessionID, {
|
|
2017
2743
|
messageID: messageID2,
|
|
2018
2744
|
text,
|
|
2019
2745
|
outputTokens,
|
|
2020
2746
|
noProgressTokenThreshold: positiveIntegerOrNull2(options.no_progress_token_threshold),
|
|
2021
|
-
maxNoProgressTurns: positiveIntegerOrNull2(options.max_no_progress_turns)
|
|
2747
|
+
maxNoProgressTurns: positiveIntegerOrNull2(options.max_no_progress_turns),
|
|
2748
|
+
completedAt: event.created
|
|
2022
2749
|
});
|
|
2750
|
+
await reconcileLocalMarkerAfterProgress(locallyDeliveredPendingSessions, sessionID, afterStep);
|
|
2751
|
+
if (/[\p{L}\p{N}]/u.test(text)) {
|
|
2752
|
+
const scheduled = scheduledContinuations.get(sessionID);
|
|
2753
|
+
if (scheduled?.purpose === "recovery")
|
|
2754
|
+
cancelScheduledContinuation(sessionID);
|
|
2755
|
+
}
|
|
2023
2756
|
latestStepBySession.set(sessionID, {
|
|
2024
2757
|
messageID: messageID2,
|
|
2025
2758
|
agent: latestStepBySession.get(sessionID)?.agent,
|
|
2026
2759
|
text,
|
|
2027
|
-
outputTokens
|
|
2760
|
+
outputTokens,
|
|
2761
|
+
completedAt: event.created
|
|
2028
2762
|
});
|
|
2029
2763
|
return;
|
|
2030
2764
|
}
|
|
@@ -2052,13 +2786,44 @@ async function setupV2(context) {
|
|
|
2052
2786
|
for (const tool of goalToolsV2(goalServices))
|
|
2053
2787
|
draft.add(tool);
|
|
2054
2788
|
}));
|
|
2055
|
-
registrations.push(await context.tool.hook("execute.before", (input) => {
|
|
2789
|
+
registrations.push(await context.tool.hook("execute.before", async (input) => {
|
|
2056
2790
|
taskTracker.noteTaskCall({ tool: input.tool, sessionID: input.sessionID, callID: input.id });
|
|
2791
|
+
const sessionID = typeof input.sessionID === "string" ? input.sessionID : undefined;
|
|
2792
|
+
const callID = typeof input.id === "string" ? input.id : undefined;
|
|
2793
|
+
if (sessionID && callID) {
|
|
2794
|
+
const goal = await getGoalInternal(sessionID);
|
|
2795
|
+
toolAttempts.set(toolAttemptKey(sessionID, callID), goal?.pendingAttempt?.id ?? null);
|
|
2796
|
+
}
|
|
2057
2797
|
}));
|
|
2058
|
-
registrations.push(await context.tool.hook("execute.after", (input) => {
|
|
2798
|
+
registrations.push(await context.tool.hook("execute.after", async (input) => {
|
|
2799
|
+
const sessionID = typeof input.sessionID === "string" ? input.sessionID : undefined;
|
|
2800
|
+
const callID = typeof input.id === "string" ? input.id : undefined;
|
|
2801
|
+
const attemptKey = sessionID && callID ? toolAttemptKey(sessionID, callID) : undefined;
|
|
2802
|
+
const expectedAttemptID = attemptKey ? toolAttempts.get(attemptKey) : undefined;
|
|
2803
|
+
if (attemptKey)
|
|
2804
|
+
toolAttempts.delete(attemptKey);
|
|
2059
2805
|
if (input.status !== "completed")
|
|
2060
2806
|
return;
|
|
2807
|
+
const text = textFromToolResult(input.result);
|
|
2061
2808
|
taskTracker.noteTaskOutput({ tool: input.tool, sessionID: input.sessionID, callID: input.id }, { output: textFromToolResult(input.result) });
|
|
2809
|
+
if (!sessionID || typeof input.tool !== "string")
|
|
2810
|
+
return;
|
|
2811
|
+
if (NON_PROGRESS_TOOLS.has(input.tool.toLowerCase()))
|
|
2812
|
+
return;
|
|
2813
|
+
if (toolOutputFailed(input.result))
|
|
2814
|
+
return;
|
|
2815
|
+
if (!text)
|
|
2816
|
+
return;
|
|
2817
|
+
const before = await getGoalInternal(sessionID);
|
|
2818
|
+
const scheduled = scheduledContinuations.get(sessionID);
|
|
2819
|
+
const hasFailureEpisode = Boolean(before && (before.continuationFailures > 0 || before.pendingAttempt != null));
|
|
2820
|
+
if (!before || !hasFailureEpisode && scheduled?.purpose !== "recovery")
|
|
2821
|
+
return;
|
|
2822
|
+
const progressed = await recordToolProgress(sessionID, text, expectedAttemptID);
|
|
2823
|
+
if (progressed?.continuationFailures === 0 && progressed.pendingAttempt == null) {
|
|
2824
|
+
locallyDeliveredPendingSessions.delete(sessionID);
|
|
2825
|
+
cancelScheduledContinuation(sessionID);
|
|
2826
|
+
}
|
|
2062
2827
|
}));
|
|
2063
2828
|
registrations.push(await context.session.hook("context", (sessionContext) => {
|
|
2064
2829
|
const reminder = systemReminder();
|
|
@@ -2085,14 +2850,19 @@ async function setupV2(context) {
|
|
|
2085
2850
|
}
|
|
2086
2851
|
})();
|
|
2087
2852
|
return async () => {
|
|
2853
|
+
disposed = true;
|
|
2088
2854
|
abortController.abort();
|
|
2089
|
-
for (const
|
|
2090
|
-
clearTimeout(timer);
|
|
2855
|
+
for (const scheduled of scheduledContinuations.values())
|
|
2856
|
+
clearTimeout(scheduled.timer);
|
|
2091
2857
|
scheduledContinuations.clear();
|
|
2092
2858
|
for (const watchdog of turnWatchdogs.values())
|
|
2093
2859
|
clearTimeout(watchdog.timer);
|
|
2094
2860
|
turnWatchdogs.clear();
|
|
2095
2861
|
activeContinuationsV2.clear();
|
|
2862
|
+
nativeRetrySessions.clear();
|
|
2863
|
+
locallyDeliveredPendingSessions.clear();
|
|
2864
|
+
watchdogRescuedSessions.clear();
|
|
2865
|
+
toolAttempts.clear();
|
|
2096
2866
|
for (const registration of registrations)
|
|
2097
2867
|
await registration.dispose();
|
|
2098
2868
|
const termination = Promise.allSettled([consumer, eventIterator?.return?.()]);
|