@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.
Files changed (3) hide show
  1. package/README.md +7 -3
  2. package/dist/server.js +858 -88
  3. 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 { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
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(dirname(file), { recursive: true, mode: 448 });
111
- const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
112
- await writeFile(tmp, JSON.stringify(state, null, 2) + `
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
- const continuationTurnCompleted = input.evaluateContinuation === true && goal.awaitingContinuationProgress && Boolean(messageID) && messageID !== goal.continuationBaselineMessageID;
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 snapshot(goal);
691
+ return snapshotInternal(goal);
524
692
  });
525
693
  }
526
- async function recordContinuationResult(sessionID, result, maxFailures) {
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 ? snapshot(goal) : null;
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 snapshot(goal);
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 snapshot(goal);
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 snapshot(goal);
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 recordAssistantProgress(sessionID, {
1282
- messageID: messageID(message),
1283
- text: textFromMessage(message),
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 = positiveIntegerOrNull2(options?.min_continue_interval_seconds) ?? DEFAULT_CONTINUE_INTERVAL_SECONDS;
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 (turnWatchdogs.get(sessionID) !== watchdog || !busySessions.has(sessionID))
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 scheduleSettledContinuation(sessionID, delayMs = TASK_SETTLE_DELAY_MS) {
1466
- if (scheduledContinuations.has(sessionID))
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
- const timer = setTimeout(() => {
1469
- scheduledContinuations.delete(sessionID);
1470
- runAutoContinue(sessionID, true);
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, timer);
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
- const current = await getGoal(sessionID);
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
- await recordContinuationResult(sessionID, "success", maxPromptFailures);
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
- await recordContinuationResult(sessionID, "failure", maxPromptFailures);
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
- for (const timer of scheduledContinuations.values())
1534
- clearTimeout(timer);
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
- const scheduled = scheduledContinuations.get(sessionID);
1691
- if (scheduled)
1692
- clearTimeout(scheduled);
1693
- scheduledContinuations.delete(sessionID);
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 (!autoContinue || !isIdleEvent(event))
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 = positiveIntegerOrNull2(options.min_continue_interval_seconds) ?? DEFAULT_CONTINUE_INTERVAL_SECONDS;
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 (turnWatchdogs.get(sessionID) !== watchdog || !busySessions.has(sessionID))
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 getGoal(sessionID);
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
- v2ErrorLog("Turn watchdog retry failed", error);
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 scheduleSettledContinuation(sessionID, delayMs = TASK_SETTLE_DELAY_MS) {
1813
- if (scheduledContinuations.has(sessionID))
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
- const timer = setTimeout(() => {
1816
- scheduledContinuations.delete(sessionID);
1817
- runAutoContinue(sessionID, true);
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, timer);
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 recordAssistantProgress(sessionID, {
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
- const current = await getGoal(sessionID);
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
- await recordContinuationResult(sessionID, "success", maxPromptFailures);
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
- await recordContinuationResult(sessionID, "failure", maxPromptFailures);
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
- if (status.type === "busy")
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
- if (autoContinue && sessionID && isRecord(status) && status.type === "idle") {
1909
- await runAutoContinue(sessionID);
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 (autoContinue && sessionID)
1920
- await runAutoContinue(sessionID);
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 timer of scheduledContinuations.values())
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?.()]);