@prevalentware/opencode-goal-plugin 0.1.32 → 0.1.33
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 +2 -2
- package/dist/server.js +744 -78
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -159,8 +159,8 @@ Defaults:
|
|
|
159
159
|
- `defer_while_tasks_active`: `true`; when enabled, goal auto-continuation waits for active OpenCode Task child sessions and their orchestrator reconciliation before sending the next goal prompt.
|
|
160
160
|
- `max_auto_turns`: `25`
|
|
161
161
|
- `min_continue_interval_seconds`: `3`
|
|
162
|
-
- `max_turn_time`: unset by default; set a positive number of seconds to retry one active-goal continuation prompt when a model turn remains busy for that long. Each new busy event resets the watchdog. Idle, built-in retry, session deletion, active Task children, and restricted agents suppress the retry. Watchdog retries are independent of `min_continue_interval_seconds` and
|
|
163
|
-
- `max_prompt_failures`: `3
|
|
162
|
+
- `max_turn_time`: unset by default; set a positive number of seconds to retry one active-goal continuation prompt when a model turn remains busy for that long. Each new busy event resets the watchdog. Idle, built-in retry, session deletion, active Task children, and restricted agents suppress the retry. Watchdog retries are independent of `min_continue_interval_seconds` and never consume auto-turn or no-progress budgets, but recognized transport failures still count toward the `max_prompt_failures` ceiling.
|
|
163
|
+
- `max_prompt_failures`: `3`; consecutive transport or no-response continuation failures pause the goal at this ceiling. Prompt delivery alone does not reset the count; substantive assistant or tool progress, a new goal, or an explicit resume does.
|
|
164
164
|
- `default_token_budget`: unset by default; when set, new goals inherit this token budget.
|
|
165
165
|
- `max_goal_duration_seconds`: unset by default; when set, new goals inherit this elapsed-time safety limit.
|
|
166
166
|
- `no_progress_token_threshold`: `50`; output-token floor used to judge whether a goal continuation turn made progress.
|
package/dist/server.js
CHANGED
|
@@ -34,6 +34,15 @@ var CheckpointSchema = Schema.Struct({
|
|
|
34
34
|
summary: Schema.String,
|
|
35
35
|
timestamp: Schema.Number
|
|
36
36
|
});
|
|
37
|
+
var PendingAttemptSchema = Schema.Struct({
|
|
38
|
+
id: Schema.String,
|
|
39
|
+
reservedAt: Schema.Number,
|
|
40
|
+
started: Schema.Boolean,
|
|
41
|
+
delivered: Schema.Boolean,
|
|
42
|
+
committed: Schema.Boolean,
|
|
43
|
+
armNoProgress: Schema.Boolean,
|
|
44
|
+
previousLastContinuationAt: Schema.NullOr(Schema.Number)
|
|
45
|
+
});
|
|
37
46
|
var GoalSchema = Schema.Struct({
|
|
38
47
|
sessionID: Schema.String,
|
|
39
48
|
objective: Schema.String,
|
|
@@ -50,6 +59,7 @@ var GoalSchema = Schema.Struct({
|
|
|
50
59
|
autoTurns: Schema.Number,
|
|
51
60
|
lastContinuationAt: NullableNumber,
|
|
52
61
|
continuationFailures: Schema.optionalWith(Schema.Number, { default: () => 0 }),
|
|
62
|
+
pendingAttempt: Schema.optionalWith(Schema.NullOr(PendingAttemptSchema), { default: () => null }),
|
|
53
63
|
lastStatus: Schema.optionalWith(NullableString, { default: () => null }),
|
|
54
64
|
maxAutoTurns: Schema.optionalWith(NullableNumber, { default: () => null }),
|
|
55
65
|
maxDurationSeconds: Schema.optionalWith(NullableNumber, { default: () => null }),
|
|
@@ -172,6 +182,8 @@ function normalizeGoal(goal) {
|
|
|
172
182
|
goal.lastAssistantMessageID ??= "";
|
|
173
183
|
goal.lastPromptAgent ??= null;
|
|
174
184
|
goal.awaitingContinuationProgress = goal.awaitingContinuationProgress === true;
|
|
185
|
+
goal.lastContinuationAt = typeof goal.lastContinuationAt === "number" && Number.isFinite(goal.lastContinuationAt) ? Math.floor(goal.lastContinuationAt >= 1000000000000 ? goal.lastContinuationAt / 1000 : goal.lastContinuationAt) : null;
|
|
186
|
+
goal.pendingAttempt = normalizePendingAttempt(goal.pendingAttempt);
|
|
175
187
|
goal.continuationBaselineMessageID ??= "";
|
|
176
188
|
goal.continuationBaselineSummary ??= "";
|
|
177
189
|
goal.noProgressTurns = nonNegativeInteger(goal.noProgressTurns, 0);
|
|
@@ -184,6 +196,22 @@ function normalizeGoal(goal) {
|
|
|
184
196
|
goal.stopReason ??= null;
|
|
185
197
|
return goal;
|
|
186
198
|
}
|
|
199
|
+
function normalizePendingAttempt(attempt) {
|
|
200
|
+
if (!attempt || typeof attempt !== "object")
|
|
201
|
+
return null;
|
|
202
|
+
return {
|
|
203
|
+
id: typeof attempt.id === "string" && attempt.id ? attempt.id : randomId(),
|
|
204
|
+
reservedAt: typeof attempt.reservedAt === "number" && Number.isFinite(attempt.reservedAt) ? attempt.reservedAt : Date.now(),
|
|
205
|
+
started: attempt.started === true,
|
|
206
|
+
delivered: attempt.delivered === true,
|
|
207
|
+
committed: attempt.committed === true,
|
|
208
|
+
armNoProgress: attempt.armNoProgress !== false,
|
|
209
|
+
previousLastContinuationAt: typeof attempt.previousLastContinuationAt === "number" && Number.isFinite(attempt.previousLastContinuationAt) ? attempt.previousLastContinuationAt : null
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function randomId() {
|
|
213
|
+
return `att_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
214
|
+
}
|
|
187
215
|
function normalizeCreateOptions(input) {
|
|
188
216
|
if (typeof input === "number" || input === null) {
|
|
189
217
|
return {
|
|
@@ -262,11 +290,19 @@ function snapshot(goal) {
|
|
|
262
290
|
sampledAt
|
|
263
291
|
};
|
|
264
292
|
}
|
|
293
|
+
function snapshotInternal(goal) {
|
|
294
|
+
return { ...snapshot(goal), pendingAttempt: goal.pendingAttempt };
|
|
295
|
+
}
|
|
265
296
|
async function getGoal(sessionID) {
|
|
266
297
|
const state = await readState();
|
|
267
298
|
const goal = state.goals[sessionID];
|
|
268
299
|
return goal ? snapshot(goal) : null;
|
|
269
300
|
}
|
|
301
|
+
async function getGoalInternal(sessionID) {
|
|
302
|
+
const state = await readState();
|
|
303
|
+
const goal = state.goals[sessionID];
|
|
304
|
+
return goal ? snapshotInternal(goal) : null;
|
|
305
|
+
}
|
|
270
306
|
async function createGoal(sessionID, objective, options) {
|
|
271
307
|
const value = validateObjective(objective);
|
|
272
308
|
const normalizedOptions = normalizeCreateOptions(options);
|
|
@@ -293,6 +329,7 @@ async function createGoal(sessionID, objective, options) {
|
|
|
293
329
|
autoTurns: 0,
|
|
294
330
|
lastContinuationAt: null,
|
|
295
331
|
continuationFailures: 0,
|
|
332
|
+
pendingAttempt: null,
|
|
296
333
|
lastStatus: paused ? "Goal recorded from Plan mode; execution paused until resumed from Build mode." : "Goal set.",
|
|
297
334
|
maxAutoTurns: normalizedOptions.maxAutoTurns,
|
|
298
335
|
maxDurationSeconds: normalizedOptions.maxDurationSeconds,
|
|
@@ -336,6 +373,11 @@ async function updateGoalObjective(sessionID, objective, status = "active", opti
|
|
|
336
373
|
goal.closedAt = null;
|
|
337
374
|
goal.stopReason = planModePause ? PLAN_MODE_STOP_REASON : null;
|
|
338
375
|
goal.budgetWrapupSent = false;
|
|
376
|
+
if (goal.status === "active") {
|
|
377
|
+
goal.continuationFailures = 0;
|
|
378
|
+
goal.pendingAttempt = null;
|
|
379
|
+
goal.awaitingContinuationProgress = false;
|
|
380
|
+
}
|
|
339
381
|
if (agent)
|
|
340
382
|
goal.lastPromptAgent = agent;
|
|
341
383
|
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 +429,7 @@ async function setGoalStatus(sessionID, status, agent) {
|
|
|
387
429
|
goal.updatedAt = nowSeconds();
|
|
388
430
|
goal.lastAccountedAt = status === "active" ? goal.updatedAt : null;
|
|
389
431
|
goal.continuationFailures = status === "active" ? 0 : goal.continuationFailures;
|
|
432
|
+
goal.pendingAttempt = status === "active" ? null : goal.pendingAttempt;
|
|
390
433
|
goal.noProgressTurns = status === "active" ? 0 : goal.noProgressTurns;
|
|
391
434
|
goal.stopReason = status === "active" ? null : "paused";
|
|
392
435
|
goal.budgetWrapupSent = status === "active" ? false : goal.budgetWrapupSent;
|
|
@@ -462,6 +505,7 @@ async function recordAssistantProgress(sessionID, input) {
|
|
|
462
505
|
const threshold = positiveIntegerOrNull(input.noProgressTokenThreshold) ?? goal.noProgressTokenThreshold;
|
|
463
506
|
const maxNoProgressTurns = positiveIntegerOrNull(input.maxNoProgressTurns) ?? goal.maxNoProgressTurns;
|
|
464
507
|
const summary = summarizeText(text);
|
|
508
|
+
const substantive = /[\p{L}\p{N}]/u.test(text);
|
|
465
509
|
const previousSummary = summarizeText(goal.lastAssistantText);
|
|
466
510
|
const repeatedMessage = Boolean(messageID && messageID === goal.lastAssistantMessageID);
|
|
467
511
|
const changed = Boolean(summary && summary !== previousSummary);
|
|
@@ -471,9 +515,18 @@ async function recordAssistantProgress(sessionID, input) {
|
|
|
471
515
|
goal.lastAssistantText = text;
|
|
472
516
|
if (messageID)
|
|
473
517
|
goal.lastAssistantMessageID = messageID;
|
|
474
|
-
|
|
518
|
+
if (substantive && summary && (!repeatedMessage || changed)) {
|
|
519
|
+
const attempt = goal.pendingAttempt;
|
|
520
|
+
if (attempt == null || input.completedAt == null || input.completedAt >= attempt.reservedAt) {
|
|
521
|
+
goal.continuationFailures = 0;
|
|
522
|
+
goal.pendingAttempt = null;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const attemptForCompletion = goal.pendingAttempt;
|
|
526
|
+
const continuationTurnCompleted = input.evaluateContinuation === true && goal.awaitingContinuationProgress && Boolean(messageID) && messageID !== goal.continuationBaselineMessageID && (input.completedAt == null || attemptForCompletion == null || input.completedAt >= attemptForCompletion.reservedAt);
|
|
475
527
|
if (continuationTurnCompleted) {
|
|
476
528
|
goal.awaitingContinuationProgress = false;
|
|
529
|
+
goal.pendingAttempt = null;
|
|
477
530
|
const lowOutput = outputTokens > 0 && outputTokens < (threshold ?? DEFAULT_NO_PROGRESS_TOKEN_THRESHOLD);
|
|
478
531
|
const changedSinceContinuation = Boolean(summary && summary !== goal.continuationBaselineSummary);
|
|
479
532
|
if (lowOutput && !changedSinceContinuation) {
|
|
@@ -514,32 +567,84 @@ async function reserveContinuation(sessionID, maxAutoTurns, minIntervalSeconds)
|
|
|
514
567
|
if (goal.lastContinuationAt && now - goal.lastContinuationAt < minIntervalSeconds)
|
|
515
568
|
return null;
|
|
516
569
|
goal.autoTurns += 1;
|
|
570
|
+
const previousLastContinuationAt = goal.lastContinuationAt;
|
|
517
571
|
goal.lastContinuationAt = now;
|
|
518
572
|
goal.continuationBaselineMessageID = goal.lastAssistantMessageID;
|
|
519
573
|
goal.continuationBaselineSummary = summarizeText(goal.lastAssistantText);
|
|
574
|
+
goal.pendingAttempt = {
|
|
575
|
+
id: randomId(),
|
|
576
|
+
reservedAt: Date.now(),
|
|
577
|
+
started: false,
|
|
578
|
+
delivered: false,
|
|
579
|
+
committed: true,
|
|
580
|
+
armNoProgress: true,
|
|
581
|
+
previousLastContinuationAt
|
|
582
|
+
};
|
|
583
|
+
goal.awaitingContinuationProgress = false;
|
|
520
584
|
goal.lastStatus = `Auto-continue ${goal.autoTurns} reserved.`;
|
|
521
585
|
pushHistory(goal, "autoContinue", goal.lastStatus);
|
|
522
586
|
goal.updatedAt = now;
|
|
523
|
-
return
|
|
587
|
+
return snapshotInternal(goal);
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
async function rollbackContinuationAttempt(sessionID) {
|
|
591
|
+
return mutate((state) => {
|
|
592
|
+
const goal = state.goals[sessionID];
|
|
593
|
+
if (!goal)
|
|
594
|
+
return false;
|
|
595
|
+
const attempt = goal.pendingAttempt;
|
|
596
|
+
if (!attempt || attempt.delivered || !attempt.committed) {
|
|
597
|
+
if (attempt && !attempt.delivered)
|
|
598
|
+
goal.pendingAttempt = null;
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
goal.autoTurns = Math.max(0, goal.autoTurns - 1);
|
|
602
|
+
goal.lastContinuationAt = attempt.previousLastContinuationAt;
|
|
603
|
+
goal.pendingAttempt = null;
|
|
604
|
+
goal.awaitingContinuationProgress = false;
|
|
605
|
+
goal.lastStatus = "Auto-continue attempt canceled before delivery.";
|
|
606
|
+
goal.updatedAt = nowSeconds();
|
|
607
|
+
return true;
|
|
524
608
|
});
|
|
525
609
|
}
|
|
526
|
-
async function recordContinuationResult(sessionID, result, maxFailures) {
|
|
610
|
+
async function recordContinuationResult(sessionID, result, maxFailures, options) {
|
|
527
611
|
return mutate((state) => {
|
|
528
612
|
const goal = state.goals[sessionID];
|
|
529
613
|
if (!goal || isClosed(goal.status))
|
|
530
|
-
return goal ?
|
|
614
|
+
return goal ? snapshotInternal(goal) : null;
|
|
531
615
|
const now = nowSeconds();
|
|
532
616
|
goal.updatedAt = now;
|
|
533
617
|
if (result === "success") {
|
|
534
|
-
goal.continuationFailures = 0;
|
|
535
618
|
if (goal.status === "active") {
|
|
619
|
+
const attempt = goal.pendingAttempt;
|
|
620
|
+
if (attempt) {
|
|
621
|
+
attempt.delivered = true;
|
|
622
|
+
attempt.started = attempt.started || options?.started === true;
|
|
623
|
+
attempt.armNoProgress = options?.armNoProgress ?? attempt.armNoProgress;
|
|
624
|
+
if (attempt.armNoProgress)
|
|
625
|
+
goal.awaitingContinuationProgress = true;
|
|
626
|
+
} else {
|
|
627
|
+
goal.pendingAttempt = {
|
|
628
|
+
id: randomId(),
|
|
629
|
+
reservedAt: Date.now(),
|
|
630
|
+
started: options?.started === true,
|
|
631
|
+
delivered: true,
|
|
632
|
+
committed: false,
|
|
633
|
+
armNoProgress: options?.armNoProgress !== false,
|
|
634
|
+
previousLastContinuationAt: goal.lastContinuationAt
|
|
635
|
+
};
|
|
636
|
+
if (goal.pendingAttempt.armNoProgress)
|
|
637
|
+
goal.awaitingContinuationProgress = true;
|
|
638
|
+
}
|
|
536
639
|
goal.lastStatus = "Auto-continue prompt sent.";
|
|
537
|
-
goal.awaitingContinuationProgress = true;
|
|
538
640
|
}
|
|
539
|
-
return
|
|
641
|
+
return snapshotInternal(goal);
|
|
540
642
|
}
|
|
643
|
+
if (options?.requirePending && goal.pendingAttempt == null)
|
|
644
|
+
return null;
|
|
541
645
|
goal.continuationFailures += 1;
|
|
542
646
|
goal.awaitingContinuationProgress = false;
|
|
647
|
+
goal.pendingAttempt = null;
|
|
543
648
|
goal.lastStatus = `Auto-continue failed ${goal.continuationFailures} time(s).`;
|
|
544
649
|
pushHistory(goal, "error", goal.lastStatus);
|
|
545
650
|
if (goal.continuationFailures >= maxFailures) {
|
|
@@ -551,7 +656,44 @@ async function recordContinuationResult(sessionID, result, maxFailures) {
|
|
|
551
656
|
goal.blocker = "Auto-continue prompt failed repeatedly. Resume the goal to retry.";
|
|
552
657
|
pushHistory(goal, "paused", goal.lastStatus);
|
|
553
658
|
}
|
|
554
|
-
return
|
|
659
|
+
return snapshotInternal(goal);
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
async function markPendingContinuationStarted(sessionID) {
|
|
663
|
+
const state = await readState();
|
|
664
|
+
const current = state.goals[sessionID];
|
|
665
|
+
if (!current || current.status !== "active")
|
|
666
|
+
return current ? snapshotInternal(current) : null;
|
|
667
|
+
if (current.pendingAttempt == null || current.pendingAttempt.started)
|
|
668
|
+
return snapshotInternal(current);
|
|
669
|
+
return mutate((state2) => {
|
|
670
|
+
const goal = state2.goals[sessionID];
|
|
671
|
+
if (!goal || goal.status !== "active")
|
|
672
|
+
return goal ? snapshotInternal(goal) : null;
|
|
673
|
+
if (goal.pendingAttempt == null || goal.pendingAttempt.started)
|
|
674
|
+
return snapshotInternal(goal);
|
|
675
|
+
goal.pendingAttempt.started = true;
|
|
676
|
+
goal.updatedAt = nowSeconds();
|
|
677
|
+
return snapshotInternal(goal);
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
async function recordToolProgress(sessionID, text, expectedAttemptID) {
|
|
681
|
+
return mutate((state) => {
|
|
682
|
+
const goal = state.goals[sessionID];
|
|
683
|
+
if (!goal || goal.status !== "active")
|
|
684
|
+
return goal ? snapshotInternal(goal) : null;
|
|
685
|
+
const value = text?.trim() ?? "";
|
|
686
|
+
if (!value)
|
|
687
|
+
return snapshotInternal(goal);
|
|
688
|
+
if (goal.continuationFailures === 0 && goal.pendingAttempt == null)
|
|
689
|
+
return snapshotInternal(goal);
|
|
690
|
+
if (goal.pendingAttempt != null && expectedAttemptID !== undefined && expectedAttemptID !== goal.pendingAttempt.id) {
|
|
691
|
+
return snapshotInternal(goal);
|
|
692
|
+
}
|
|
693
|
+
goal.continuationFailures = 0;
|
|
694
|
+
goal.pendingAttempt = null;
|
|
695
|
+
goal.updatedAt = nowSeconds();
|
|
696
|
+
return snapshotInternal(goal);
|
|
555
697
|
});
|
|
556
698
|
}
|
|
557
699
|
function reserveWrapup(goal) {
|
|
@@ -560,7 +702,7 @@ function reserveWrapup(goal) {
|
|
|
560
702
|
goal.budgetWrapupSent = true;
|
|
561
703
|
goal.updatedAt = nowSeconds();
|
|
562
704
|
pushHistory(goal, "limited", `${goal.status}: ${goal.stopReason ?? "goal limit reached"}; requested final handoff.`);
|
|
563
|
-
return
|
|
705
|
+
return snapshotInternal(goal);
|
|
564
706
|
}
|
|
565
707
|
function maybeStopForBudget(goal) {
|
|
566
708
|
if (goal.status !== "active")
|
|
@@ -783,6 +925,11 @@ var DEFAULT_RESTRICTED_AGENTS = ["plan"];
|
|
|
783
925
|
var TASK_SETTLE_DELAY_MS = 25;
|
|
784
926
|
var SNAPSHOT_IDLE_HOLD_MS = 250;
|
|
785
927
|
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
928
|
+
var STALE_PENDING_MS = 30000;
|
|
929
|
+
var RETRY_SETTLE_MS = 25;
|
|
930
|
+
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;
|
|
931
|
+
var NON_TRANSPORT_TERMINAL_PATTERN = /\b(?:abort(?:ed)?|interrupt(?:ed|ion)?)\b/i;
|
|
932
|
+
var NON_PROGRESS_TOOLS = new Set(["get_goal", "get_goal_history"]);
|
|
786
933
|
var TASK_TERMINAL_STATES = new Set(["completed", "error", "cancelled"]);
|
|
787
934
|
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
935
|
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 +972,9 @@ function commandNameFromOptions(options) {
|
|
|
825
972
|
function positiveIntegerOrNull2(value) {
|
|
826
973
|
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : null;
|
|
827
974
|
}
|
|
975
|
+
function nonNegativeIntegerOrNull(value) {
|
|
976
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
977
|
+
}
|
|
828
978
|
function timeoutMillisecondsFromSeconds(value) {
|
|
829
979
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0)
|
|
830
980
|
return null;
|
|
@@ -994,6 +1144,103 @@ function isIdleEvent(event) {
|
|
|
994
1144
|
const status = event.properties?.status;
|
|
995
1145
|
return event.type === "session.status" && typeof status === "object" && status !== null && status.type === "idle";
|
|
996
1146
|
}
|
|
1147
|
+
function isTransportError(error) {
|
|
1148
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
1149
|
+
if (!message || NON_TRANSPORT_TERMINAL_PATTERN.test(message))
|
|
1150
|
+
return false;
|
|
1151
|
+
if (TRANSPORT_ERROR_PATTERN.test(message))
|
|
1152
|
+
return true;
|
|
1153
|
+
return false;
|
|
1154
|
+
}
|
|
1155
|
+
function transportErrorMessageFromEvent(props) {
|
|
1156
|
+
for (const candidate of [props.error, props.message, props.reason]) {
|
|
1157
|
+
if (typeof candidate === "string" && candidate.trim())
|
|
1158
|
+
return candidate.trim();
|
|
1159
|
+
if (isRecord(candidate)) {
|
|
1160
|
+
for (const key of ["message", "error", "reason", "description"]) {
|
|
1161
|
+
const value = candidate[key];
|
|
1162
|
+
if (typeof value === "string" && value.trim())
|
|
1163
|
+
return value.trim();
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return "";
|
|
1168
|
+
}
|
|
1169
|
+
function continuationRetryDelayMs(minIntervalSeconds, attemptAt, now = Date.now()) {
|
|
1170
|
+
return Math.max(0, attemptAt + minIntervalSeconds * 1000 - now) + RETRY_SETTLE_MS;
|
|
1171
|
+
}
|
|
1172
|
+
function continuationDelayFromSnapshot(minIntervalSeconds, lastContinuationAt, now = Date.now()) {
|
|
1173
|
+
if (lastContinuationAt == null)
|
|
1174
|
+
return RETRY_SETTLE_MS;
|
|
1175
|
+
return Math.max(0, (lastContinuationAt + minIntervalSeconds + 1) * 1000 - now) + RETRY_SETTLE_MS;
|
|
1176
|
+
}
|
|
1177
|
+
function pendingAttemptOf(goal) {
|
|
1178
|
+
return goal?.pendingAttempt ?? null;
|
|
1179
|
+
}
|
|
1180
|
+
function pendingReadyForFailure(attempt, deliveredLocally, now = Date.now()) {
|
|
1181
|
+
if (!attempt)
|
|
1182
|
+
return false;
|
|
1183
|
+
if (attempt.started)
|
|
1184
|
+
return true;
|
|
1185
|
+
if (deliveredLocally)
|
|
1186
|
+
return false;
|
|
1187
|
+
return now - attempt.reservedAt >= STALE_PENDING_MS;
|
|
1188
|
+
}
|
|
1189
|
+
async function reconcileLocalMarkerAfterProgress(locallyDelivered, sessionID, goal) {
|
|
1190
|
+
if (!goal || goal.continuationFailures !== 0)
|
|
1191
|
+
return;
|
|
1192
|
+
const internal = await getGoalInternal(sessionID);
|
|
1193
|
+
if (internal && internal.pendingAttempt == null)
|
|
1194
|
+
locallyDelivered.delete(sessionID);
|
|
1195
|
+
}
|
|
1196
|
+
var TOOL_FAILURE_STATES = new Set([
|
|
1197
|
+
"failed",
|
|
1198
|
+
"failure",
|
|
1199
|
+
"error",
|
|
1200
|
+
"cancelled",
|
|
1201
|
+
"canceled",
|
|
1202
|
+
"aborted",
|
|
1203
|
+
"abort",
|
|
1204
|
+
"interrupted",
|
|
1205
|
+
"running",
|
|
1206
|
+
"pending",
|
|
1207
|
+
"in_progress",
|
|
1208
|
+
"in-progress",
|
|
1209
|
+
"incomplete",
|
|
1210
|
+
"partial",
|
|
1211
|
+
"timeout",
|
|
1212
|
+
"timed_out"
|
|
1213
|
+
]);
|
|
1214
|
+
function toolOutputFailed(output) {
|
|
1215
|
+
if (!isRecord(output))
|
|
1216
|
+
return true;
|
|
1217
|
+
if (typeof output.error === "string" && output.error.trim())
|
|
1218
|
+
return true;
|
|
1219
|
+
if (output.success === false)
|
|
1220
|
+
return true;
|
|
1221
|
+
const text = typeof output.output === "string" ? output.output.trim() : "";
|
|
1222
|
+
const state = output.state ?? output.status;
|
|
1223
|
+
if (typeof state === "string") {
|
|
1224
|
+
const normalized = state.trim().toLowerCase();
|
|
1225
|
+
if (TOOL_FAILURE_STATES.has(normalized))
|
|
1226
|
+
return true;
|
|
1227
|
+
if (["completed", "complete", "success", "succeeded", "ok", "done"].includes(normalized))
|
|
1228
|
+
return false;
|
|
1229
|
+
}
|
|
1230
|
+
if (isRecord(output.metadata)) {
|
|
1231
|
+
const metaState = output.metadata.state ?? output.metadata.status;
|
|
1232
|
+
if (typeof metaState === "string" && TOOL_FAILURE_STATES.has(metaState.trim().toLowerCase()))
|
|
1233
|
+
return true;
|
|
1234
|
+
}
|
|
1235
|
+
const taskState = parseTaskState(text);
|
|
1236
|
+
if (taskState)
|
|
1237
|
+
return taskState !== "completed";
|
|
1238
|
+
if (/^state:\s*(failed|failure|error|cancelled|canceled|aborted|abort|interrupted|running|pending|incomplete|partial|timeout|timed_out)\b/im.test(text))
|
|
1239
|
+
return true;
|
|
1240
|
+
if (/^<error>/i.test(text) || /^<tool-error>/i.test(text) || /^error:/i.test(text))
|
|
1241
|
+
return true;
|
|
1242
|
+
return false;
|
|
1243
|
+
}
|
|
997
1244
|
function sessionIDFromEvent(event) {
|
|
998
1245
|
const direct = event.properties?.sessionID;
|
|
999
1246
|
if (typeof direct === "string")
|
|
@@ -1277,15 +1524,21 @@ class TaskTracker {
|
|
|
1277
1524
|
}
|
|
1278
1525
|
async function recordAssistantMessage(sessionID, message, options, evaluateContinuation = false) {
|
|
1279
1526
|
if (!message)
|
|
1280
|
-
return;
|
|
1281
|
-
await
|
|
1282
|
-
|
|
1283
|
-
|
|
1527
|
+
return { goal: null, progressed: false };
|
|
1528
|
+
const before = await getGoal(sessionID);
|
|
1529
|
+
const id = messageID(message) ?? "";
|
|
1530
|
+
const text = textFromMessage(message);
|
|
1531
|
+
const progressed = Boolean(/[\p{L}\p{N}]/u.test(text) && (id !== (before?.lastAssistantMessageID ?? "") || text !== (before?.lastAssistantText ?? "")));
|
|
1532
|
+
const goal = await recordAssistantProgress(sessionID, {
|
|
1533
|
+
messageID: id,
|
|
1534
|
+
text,
|
|
1284
1535
|
outputTokens: outputTokensFromMessage(message) ?? null,
|
|
1285
1536
|
noProgressTokenThreshold: positiveIntegerOrNull2(options.no_progress_token_threshold),
|
|
1286
1537
|
maxNoProgressTurns: positiveIntegerOrNull2(options.max_no_progress_turns),
|
|
1287
|
-
evaluateContinuation
|
|
1538
|
+
evaluateContinuation,
|
|
1539
|
+
completedAt: messageCompletedAt(message)
|
|
1288
1540
|
});
|
|
1541
|
+
return { goal, progressed };
|
|
1289
1542
|
}
|
|
1290
1543
|
function mergeSystemReminder(output, reminder) {
|
|
1291
1544
|
if (!reminder.trim())
|
|
@@ -1367,11 +1620,20 @@ function textFromToolResult(result) {
|
|
|
1367
1620
|
}
|
|
1368
1621
|
return;
|
|
1369
1622
|
}
|
|
1623
|
+
function toolAttemptKey(sessionID, callID) {
|
|
1624
|
+
return `${sessionID}\x00${callID}`;
|
|
1625
|
+
}
|
|
1626
|
+
function clearToolAttemptsForSession(attempts, sessionID) {
|
|
1627
|
+
for (const key of [...attempts.keys()]) {
|
|
1628
|
+
if (key.startsWith(`${sessionID}\x00`))
|
|
1629
|
+
attempts.delete(key);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1370
1632
|
var server = async ({ client }, options) => {
|
|
1371
1633
|
const autoContinue = options?.auto_continue ?? true;
|
|
1372
1634
|
const deferWhileTasksActive = options?.defer_while_tasks_active ?? true;
|
|
1373
1635
|
const maxAutoTurns = positiveIntegerOrNull2(options?.max_auto_turns) ?? DEFAULT_MAX_AUTO_TURNS;
|
|
1374
|
-
const minInterval =
|
|
1636
|
+
const minInterval = nonNegativeIntegerOrNull(options?.min_continue_interval_seconds) ?? DEFAULT_CONTINUE_INTERVAL_SECONDS;
|
|
1375
1637
|
const maxTurnTimeMs = timeoutMillisecondsFromSeconds(options?.max_turn_time);
|
|
1376
1638
|
const maxPromptFailures = positiveIntegerOrNull2(options?.max_prompt_failures) ?? DEFAULT_MAX_PROMPT_FAILURES;
|
|
1377
1639
|
const registerCommand = options?.register_command ?? true;
|
|
@@ -1381,9 +1643,14 @@ var server = async ({ client }, options) => {
|
|
|
1381
1643
|
const scheduledContinuations = new Map;
|
|
1382
1644
|
const turnWatchdogs = new Map;
|
|
1383
1645
|
const busySessions = new Set;
|
|
1646
|
+
const nativeRetrySessions = new Set;
|
|
1647
|
+
const locallyDeliveredPendingSessions = new Set;
|
|
1648
|
+
const toolAttempts = new Map;
|
|
1649
|
+
const watchdogRescuedSessions = new Set;
|
|
1384
1650
|
const planAgents = restrictedAgentSet(options);
|
|
1385
1651
|
const isPlanAgent = (agent) => typeof agent === "string" && planAgents.has(agent.trim().toLowerCase());
|
|
1386
1652
|
const goalServices = { options: options ?? {}, isPlanAgent };
|
|
1653
|
+
let disposed = false;
|
|
1387
1654
|
async function taskBlockStatus(sessionID) {
|
|
1388
1655
|
if (!deferWhileTasksActive)
|
|
1389
1656
|
return false;
|
|
@@ -1403,6 +1670,8 @@ var server = async ({ client }, options) => {
|
|
|
1403
1670
|
function armTurnWatchdog(sessionID) {
|
|
1404
1671
|
if (maxTurnTimeMs == null)
|
|
1405
1672
|
return;
|
|
1673
|
+
if (watchdogRescuedSessions.has(sessionID))
|
|
1674
|
+
return;
|
|
1406
1675
|
clearTurnWatchdog(sessionID);
|
|
1407
1676
|
const watchdog = {
|
|
1408
1677
|
timer: setTimeout(() => void runTurnWatchdog(sessionID, watchdog), maxTurnTimeMs)
|
|
@@ -1415,7 +1684,9 @@ var server = async ({ client }, options) => {
|
|
|
1415
1684
|
async function runTurnWatchdog(sessionID, watchdog) {
|
|
1416
1685
|
let claimedContinuation = false;
|
|
1417
1686
|
try {
|
|
1418
|
-
if (
|
|
1687
|
+
if (disposed)
|
|
1688
|
+
return;
|
|
1689
|
+
if (turnWatchdogs.get(sessionID) !== watchdog || !busySessions.has(sessionID) || watchdogRescuedSessions.has(sessionID))
|
|
1419
1690
|
return;
|
|
1420
1691
|
const goal = await getGoal(sessionID);
|
|
1421
1692
|
if (turnWatchdogs.get(sessionID) !== watchdog || !busySessions.has(sessionID))
|
|
@@ -1428,6 +1699,8 @@ var server = async ({ client }, options) => {
|
|
|
1428
1699
|
const latestTurnAgent = agentFromMessage(latestAssistant);
|
|
1429
1700
|
if (isPlanAgent(latestTurnAgent))
|
|
1430
1701
|
return;
|
|
1702
|
+
const observedBeforeRescue = await recordAssistantMessage(sessionID, latestAssistant, options ?? {});
|
|
1703
|
+
await reconcileLocalMarkerAfterProgress(locallyDeliveredPendingSessions, sessionID, observedBeforeRescue.goal);
|
|
1431
1704
|
const taskStatus = await taskBlockStatus(sessionID);
|
|
1432
1705
|
if (turnWatchdogs.get(sessionID) !== watchdog || !busySessions.has(sessionID))
|
|
1433
1706
|
return;
|
|
@@ -1441,9 +1714,16 @@ var server = async ({ client }, options) => {
|
|
|
1441
1714
|
turnWatchdogs.delete(sessionID);
|
|
1442
1715
|
activeContinuations.add(sessionID);
|
|
1443
1716
|
claimedContinuation = true;
|
|
1717
|
+
watchdogRescuedSessions.add(sessionID);
|
|
1444
1718
|
await sendContinuation(client, sessionID, continuationPrompt(current), current.lastPromptAgent ?? latestTurnAgent ?? null);
|
|
1719
|
+
await recordContinuationResult(sessionID, "success", maxPromptFailures, { armNoProgress: false, started: true });
|
|
1720
|
+
locallyDeliveredPendingSessions.add(sessionID);
|
|
1721
|
+
clearTurnWatchdog(sessionID);
|
|
1445
1722
|
} catch (error) {
|
|
1446
1723
|
try {
|
|
1724
|
+
if (claimedContinuation && isTransportError(error)) {
|
|
1725
|
+
await recordContinuationResult(sessionID, "failure", maxPromptFailures);
|
|
1726
|
+
}
|
|
1447
1727
|
await client.app?.log?.({
|
|
1448
1728
|
body: {
|
|
1449
1729
|
service: "opencode-goal-plugin",
|
|
@@ -1462,38 +1742,74 @@ var server = async ({ client }, options) => {
|
|
|
1462
1742
|
turnWatchdogs.delete(sessionID);
|
|
1463
1743
|
}
|
|
1464
1744
|
}
|
|
1465
|
-
function
|
|
1466
|
-
|
|
1745
|
+
function cancelScheduledContinuation(sessionID) {
|
|
1746
|
+
const scheduled = scheduledContinuations.get(sessionID);
|
|
1747
|
+
if (scheduled)
|
|
1748
|
+
clearTimeout(scheduled.timer);
|
|
1749
|
+
scheduledContinuations.delete(sessionID);
|
|
1750
|
+
}
|
|
1751
|
+
function scheduleSettledContinuation(sessionID, delayMs = TASK_SETTLE_DELAY_MS, replace = false, purpose = "settle") {
|
|
1752
|
+
if (disposed)
|
|
1467
1753
|
return;
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1754
|
+
if (!replace && scheduledContinuations.has(sessionID))
|
|
1755
|
+
return;
|
|
1756
|
+
if (replace)
|
|
1757
|
+
cancelScheduledContinuation(sessionID);
|
|
1758
|
+
const scheduled = {};
|
|
1759
|
+
const timer = setTimeout(async () => {
|
|
1760
|
+
try {
|
|
1761
|
+
if (scheduledContinuations.get(sessionID) !== scheduled || nativeRetrySessions.has(sessionID))
|
|
1762
|
+
return;
|
|
1763
|
+
if (purpose === "retry") {
|
|
1764
|
+
const goal = await getGoalInternal(sessionID);
|
|
1765
|
+
if (!goal || goal.continuationFailures === 0 && goal.pendingAttempt == null)
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
if (scheduledContinuations.get(sessionID) !== scheduled || nativeRetrySessions.has(sessionID))
|
|
1769
|
+
return;
|
|
1770
|
+
await runAutoContinue(sessionID, true, scheduled);
|
|
1771
|
+
} finally {
|
|
1772
|
+
if (scheduledContinuations.get(sessionID) === scheduled)
|
|
1773
|
+
scheduledContinuations.delete(sessionID);
|
|
1774
|
+
}
|
|
1471
1775
|
}, Math.max(0, delayMs));
|
|
1776
|
+
scheduled.timer = timer;
|
|
1777
|
+
scheduled.purpose = purpose;
|
|
1472
1778
|
const maybeUnref = timer;
|
|
1473
1779
|
if (typeof maybeUnref.unref === "function")
|
|
1474
1780
|
maybeUnref.unref();
|
|
1475
|
-
scheduledContinuations.set(sessionID,
|
|
1781
|
+
scheduledContinuations.set(sessionID, scheduled);
|
|
1476
1782
|
}
|
|
1477
|
-
async function runAutoContinue(sessionID, fromTaskDeferral = false) {
|
|
1783
|
+
async function runAutoContinue(sessionID, fromTaskDeferral = false, scheduled) {
|
|
1784
|
+
if (disposed)
|
|
1785
|
+
return;
|
|
1478
1786
|
if (busySessions.has(sessionID))
|
|
1479
1787
|
return;
|
|
1480
1788
|
if (activeContinuations.has(sessionID))
|
|
1481
1789
|
return;
|
|
1482
1790
|
activeContinuations.add(sessionID);
|
|
1791
|
+
let attemptReservedAt = Date.now();
|
|
1483
1792
|
try {
|
|
1484
1793
|
const latestAssistant = await fetchLatestAssistant(client, sessionID);
|
|
1485
1794
|
taskTracker.observeAssistantMessage(sessionID, latestAssistant);
|
|
1486
1795
|
const taskStatus = await taskBlockStatus(sessionID);
|
|
1487
1796
|
if (taskStatus && taskStatus.blocked) {
|
|
1488
1797
|
taskDeferredSessions.add(sessionID);
|
|
1489
|
-
if (taskStatus.retryAt != null)
|
|
1490
|
-
scheduleSettledContinuation(sessionID, taskStatus.retryAt - Date.now());
|
|
1798
|
+
if (taskStatus.retryAt != null) {
|
|
1799
|
+
scheduleSettledContinuation(sessionID, taskStatus.retryAt - Date.now(), scheduled != null);
|
|
1800
|
+
}
|
|
1491
1801
|
return;
|
|
1492
1802
|
}
|
|
1493
1803
|
if (busySessions.has(sessionID))
|
|
1494
1804
|
return;
|
|
1495
|
-
await recordAssistantMessage(sessionID, latestAssistant, options ?? {}, true);
|
|
1496
|
-
|
|
1805
|
+
const observed = await recordAssistantMessage(sessionID, latestAssistant, options ?? {}, true);
|
|
1806
|
+
await reconcileLocalMarkerAfterProgress(locallyDeliveredPendingSessions, sessionID, observed.goal);
|
|
1807
|
+
const queued = scheduledContinuations.get(sessionID);
|
|
1808
|
+
if (observed.progressed && queued?.purpose !== "settle")
|
|
1809
|
+
cancelScheduledContinuation(sessionID);
|
|
1810
|
+
if (scheduled && scheduledContinuations.get(sessionID) !== scheduled)
|
|
1811
|
+
return;
|
|
1812
|
+
const current = await getGoalInternal(sessionID);
|
|
1497
1813
|
if (!current)
|
|
1498
1814
|
return;
|
|
1499
1815
|
const latestTurnAgent = agentFromMessage(latestAssistant);
|
|
@@ -1509,13 +1825,64 @@ var server = async ({ client }, options) => {
|
|
|
1509
1825
|
return;
|
|
1510
1826
|
}
|
|
1511
1827
|
taskDeferredSessions.delete(sessionID);
|
|
1828
|
+
const attempt = pendingAttemptOf(current);
|
|
1829
|
+
if (current.status === "active" && attempt != null) {
|
|
1830
|
+
const deliveredLocally = locallyDeliveredPendingSessions.has(sessionID);
|
|
1831
|
+
if (!pendingReadyForFailure(attempt, deliveredLocally)) {
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
const afterFailure = await recordContinuationResult(sessionID, "failure", maxPromptFailures, {
|
|
1835
|
+
requirePending: true
|
|
1836
|
+
});
|
|
1837
|
+
if (afterFailure)
|
|
1838
|
+
locallyDeliveredPendingSessions.delete(sessionID);
|
|
1839
|
+
if (autoContinue && afterFailure?.status === "active") {
|
|
1840
|
+
scheduleSettledContinuation(sessionID, continuationRetryDelayMs(minInterval, attempt.reservedAt), true, "retry");
|
|
1841
|
+
}
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
const queuedBeforeReserve = scheduledContinuations.get(sessionID);
|
|
1845
|
+
if (queuedBeforeReserve && queuedBeforeReserve !== scheduled)
|
|
1846
|
+
return;
|
|
1847
|
+
if (!autoContinue)
|
|
1848
|
+
return;
|
|
1849
|
+
if (nativeRetrySessions.has(sessionID))
|
|
1850
|
+
return;
|
|
1512
1851
|
const goal = await reserveContinuation(sessionID, maxAutoTurns, minInterval);
|
|
1513
1852
|
if (!goal)
|
|
1514
1853
|
return;
|
|
1854
|
+
attemptReservedAt = goal.pendingAttempt?.reservedAt ?? Date.now();
|
|
1855
|
+
if (nativeRetrySessions.has(sessionID)) {
|
|
1856
|
+
await rollbackContinuationAttempt(sessionID);
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
if (scheduled && scheduledContinuations.get(sessionID) !== scheduled) {
|
|
1860
|
+
await rollbackContinuationAttempt(sessionID);
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1515
1863
|
await sendContinuation(client, sessionID, goal.status === "active" ? continuationPrompt(goal) : limitPrompt(goal), goal.lastPromptAgent ?? latestTurnAgent ?? null);
|
|
1516
|
-
|
|
1864
|
+
if (disposed) {
|
|
1865
|
+
await rollbackContinuationAttempt(sessionID);
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
const delivered = await recordContinuationResult(sessionID, "success", maxPromptFailures);
|
|
1869
|
+
locallyDeliveredPendingSessions.add(sessionID);
|
|
1870
|
+
if (!delivered?.pendingAttempt?.delivered) {
|
|
1871
|
+
await rollbackContinuationAttempt(sessionID);
|
|
1872
|
+
}
|
|
1517
1873
|
} catch (error) {
|
|
1518
|
-
|
|
1874
|
+
if (disposed) {
|
|
1875
|
+
await rollbackContinuationAttempt(sessionID);
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
if (isTransportError(error)) {
|
|
1879
|
+
const afterFailure = await recordContinuationResult(sessionID, "failure", maxPromptFailures);
|
|
1880
|
+
if (autoContinue && afterFailure?.status === "active") {
|
|
1881
|
+
scheduleSettledContinuation(sessionID, continuationRetryDelayMs(minInterval, attemptReservedAt), true, "retry");
|
|
1882
|
+
}
|
|
1883
|
+
} else {
|
|
1884
|
+
await rollbackContinuationAttempt(sessionID);
|
|
1885
|
+
}
|
|
1519
1886
|
await client.app?.log?.({
|
|
1520
1887
|
body: {
|
|
1521
1888
|
service: "opencode-goal-plugin",
|
|
@@ -1530,12 +1897,17 @@ var server = async ({ client }, options) => {
|
|
|
1530
1897
|
}
|
|
1531
1898
|
return {
|
|
1532
1899
|
async dispose() {
|
|
1533
|
-
|
|
1534
|
-
|
|
1900
|
+
disposed = true;
|
|
1901
|
+
for (const scheduled of scheduledContinuations.values())
|
|
1902
|
+
clearTimeout(scheduled.timer);
|
|
1535
1903
|
scheduledContinuations.clear();
|
|
1536
1904
|
for (const watchdog of turnWatchdogs.values())
|
|
1537
1905
|
clearTimeout(watchdog.timer);
|
|
1538
1906
|
turnWatchdogs.clear();
|
|
1907
|
+
watchdogRescuedSessions.clear();
|
|
1908
|
+
locallyDeliveredPendingSessions.clear();
|
|
1909
|
+
nativeRetrySessions.clear();
|
|
1910
|
+
toolAttempts.clear();
|
|
1539
1911
|
},
|
|
1540
1912
|
async config(config) {
|
|
1541
1913
|
if (!registerCommand)
|
|
@@ -1622,9 +1994,41 @@ var server = async ({ client }, options) => {
|
|
|
1622
1994
|
},
|
|
1623
1995
|
async "tool.execute.before"(input) {
|
|
1624
1996
|
taskTracker.noteTaskCall(input);
|
|
1997
|
+
const sessionID = typeof input?.sessionID === "string" ? input.sessionID : undefined;
|
|
1998
|
+
const callID = typeof input?.callID === "string" ? input.callID : undefined;
|
|
1999
|
+
if (sessionID && callID) {
|
|
2000
|
+
const goal = await getGoalInternal(sessionID);
|
|
2001
|
+
toolAttempts.set(toolAttemptKey(sessionID, callID), goal?.pendingAttempt?.id ?? null);
|
|
2002
|
+
}
|
|
1625
2003
|
},
|
|
1626
2004
|
async "tool.execute.after"(input, output) {
|
|
1627
2005
|
taskTracker.noteTaskOutput(input, output);
|
|
2006
|
+
const sessionID = typeof input?.sessionID === "string" ? input.sessionID : undefined;
|
|
2007
|
+
const callID = typeof input?.callID === "string" ? input.callID : undefined;
|
|
2008
|
+
const attemptKey = sessionID && callID ? toolAttemptKey(sessionID, callID) : undefined;
|
|
2009
|
+
const expectedAttemptID = attemptKey ? toolAttempts.get(attemptKey) : undefined;
|
|
2010
|
+
if (attemptKey)
|
|
2011
|
+
toolAttempts.delete(attemptKey);
|
|
2012
|
+
if (!sessionID)
|
|
2013
|
+
return;
|
|
2014
|
+
if (typeof input?.tool === "string" && NON_PROGRESS_TOOLS.has(input.tool.toLowerCase()))
|
|
2015
|
+
return;
|
|
2016
|
+
const toolResult = output;
|
|
2017
|
+
if (toolOutputFailed(toolResult))
|
|
2018
|
+
return;
|
|
2019
|
+
const text = typeof toolResult.output === "string" ? toolResult.output : undefined;
|
|
2020
|
+
if (!text)
|
|
2021
|
+
return;
|
|
2022
|
+
const before = await getGoalInternal(sessionID);
|
|
2023
|
+
const scheduled = scheduledContinuations.get(sessionID);
|
|
2024
|
+
const hasFailureEpisode = Boolean(before && (before.continuationFailures > 0 || before.pendingAttempt != null));
|
|
2025
|
+
if (!before || !hasFailureEpisode && scheduled?.purpose !== "recovery")
|
|
2026
|
+
return;
|
|
2027
|
+
const progressed = await recordToolProgress(sessionID, text, expectedAttemptID);
|
|
2028
|
+
if (progressed?.continuationFailures === 0 && progressed.pendingAttempt == null) {
|
|
2029
|
+
locallyDeliveredPendingSessions.delete(sessionID);
|
|
2030
|
+
cancelScheduledContinuation(sessionID);
|
|
2031
|
+
}
|
|
1628
2032
|
},
|
|
1629
2033
|
async "chat.message"(input, output) {
|
|
1630
2034
|
const sessionID = typeof input?.sessionID === "string" ? input.sessionID : output.message?.sessionID;
|
|
@@ -1639,7 +2043,11 @@ var server = async ({ client }, options) => {
|
|
|
1639
2043
|
if (!sessionID)
|
|
1640
2044
|
return;
|
|
1641
2045
|
await accountUsage(sessionID, tokensFromMessages(output.messages));
|
|
1642
|
-
await recordAssistantMessage(sessionID, latestAssistantMessage(output.messages), options ?? {});
|
|
2046
|
+
const observed = await recordAssistantMessage(sessionID, latestAssistantMessage(output.messages), options ?? {});
|
|
2047
|
+
await reconcileLocalMarkerAfterProgress(locallyDeliveredPendingSessions, sessionID, observed.goal);
|
|
2048
|
+
const scheduled = scheduledContinuations.get(sessionID);
|
|
2049
|
+
if (observed.progressed && scheduled?.purpose !== "settle")
|
|
2050
|
+
cancelScheduledContinuation(sessionID);
|
|
1643
2051
|
},
|
|
1644
2052
|
async "experimental.chat.system.transform"(input, output) {
|
|
1645
2053
|
if (typeof input.sessionID !== "string")
|
|
@@ -1666,44 +2074,91 @@ var server = async ({ client }, options) => {
|
|
|
1666
2074
|
if (sessionID && eventType === "session.status") {
|
|
1667
2075
|
const status = event.properties?.status;
|
|
1668
2076
|
if (isRecord(status) && typeof status.type === "string") {
|
|
1669
|
-
if (status.type === "busy")
|
|
2077
|
+
if (status.type === "busy") {
|
|
1670
2078
|
busySessions.add(sessionID);
|
|
2079
|
+
nativeRetrySessions.delete(sessionID);
|
|
2080
|
+
}
|
|
1671
2081
|
if (status.type === "busy")
|
|
1672
2082
|
armTurnWatchdog(sessionID);
|
|
2083
|
+
if (status.type === "busy")
|
|
2084
|
+
await markPendingContinuationStarted(sessionID);
|
|
1673
2085
|
if (status.type === "idle") {
|
|
1674
2086
|
busySessions.delete(sessionID);
|
|
2087
|
+
nativeRetrySessions.delete(sessionID);
|
|
1675
2088
|
clearTurnWatchdog(sessionID);
|
|
2089
|
+
watchdogRescuedSessions.delete(sessionID);
|
|
1676
2090
|
}
|
|
1677
|
-
if (status.type === "retry")
|
|
2091
|
+
if (status.type === "retry") {
|
|
2092
|
+
nativeRetrySessions.add(sessionID);
|
|
1678
2093
|
clearTurnWatchdog(sessionID);
|
|
2094
|
+
cancelScheduledContinuation(sessionID);
|
|
2095
|
+
}
|
|
1679
2096
|
taskTracker.observeSessionStatus(sessionID, status.type);
|
|
1680
2097
|
}
|
|
1681
2098
|
}
|
|
1682
2099
|
if (sessionID && eventType === "session.idle") {
|
|
1683
2100
|
busySessions.delete(sessionID);
|
|
2101
|
+
nativeRetrySessions.delete(sessionID);
|
|
1684
2102
|
clearTurnWatchdog(sessionID);
|
|
2103
|
+
watchdogRescuedSessions.delete(sessionID);
|
|
1685
2104
|
taskTracker.observeSessionStatus(sessionID, "idle");
|
|
1686
2105
|
}
|
|
2106
|
+
if (sessionID && eventType === "session.error") {
|
|
2107
|
+
const inNativeRetry = nativeRetrySessions.has(sessionID);
|
|
2108
|
+
busySessions.delete(sessionID);
|
|
2109
|
+
clearTurnWatchdog(sessionID);
|
|
2110
|
+
if (inNativeRetry)
|
|
2111
|
+
return;
|
|
2112
|
+
nativeRetrySessions.delete(sessionID);
|
|
2113
|
+
watchdogRescuedSessions.delete(sessionID);
|
|
2114
|
+
const props = event.properties ?? {};
|
|
2115
|
+
const errorMessage = transportErrorMessageFromEvent(props);
|
|
2116
|
+
if (errorMessage && isTransportError(errorMessage)) {
|
|
2117
|
+
const goal = await getGoalInternal(sessionID);
|
|
2118
|
+
if (goal?.status === "active") {
|
|
2119
|
+
const attempt = pendingAttemptOf(goal);
|
|
2120
|
+
if (attempt != null) {
|
|
2121
|
+
const afterFailure = await recordContinuationResult(sessionID, "failure", maxPromptFailures, {
|
|
2122
|
+
requirePending: true
|
|
2123
|
+
});
|
|
2124
|
+
if (afterFailure)
|
|
2125
|
+
locallyDeliveredPendingSessions.delete(sessionID);
|
|
2126
|
+
if (autoContinue && afterFailure?.status === "active") {
|
|
2127
|
+
scheduleSettledContinuation(sessionID, continuationRetryDelayMs(minInterval, attempt.reservedAt), true, "retry");
|
|
2128
|
+
}
|
|
2129
|
+
} else if (autoContinue) {
|
|
2130
|
+
scheduleSettledContinuation(sessionID, continuationDelayFromSnapshot(minInterval, goal.lastContinuationAt), false, "recovery");
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
1687
2135
|
if (sessionID && eventType === "session.deleted") {
|
|
1688
2136
|
busySessions.delete(sessionID);
|
|
1689
2137
|
clearTurnWatchdog(sessionID);
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
2138
|
+
watchdogRescuedSessions.delete(sessionID);
|
|
2139
|
+
locallyDeliveredPendingSessions.delete(sessionID);
|
|
2140
|
+
nativeRetrySessions.delete(sessionID);
|
|
2141
|
+
cancelScheduledContinuation(sessionID);
|
|
1694
2142
|
taskDeferredSessions.delete(sessionID);
|
|
2143
|
+
clearToolAttemptsForSession(toolAttempts, sessionID);
|
|
1695
2144
|
taskTracker.observeSessionDeleted(sessionID);
|
|
1696
2145
|
}
|
|
1697
2146
|
if (sessionID && event.type === "message.updated") {
|
|
1698
2147
|
const props = event.properties ?? {};
|
|
1699
2148
|
const message = [props.info, props.message].find((value) => value && typeof value === "object");
|
|
1700
2149
|
taskTracker.observeAssistantMessage(sessionID, message);
|
|
1701
|
-
await recordAssistantMessage(sessionID, message, options ?? {});
|
|
2150
|
+
const observed = await recordAssistantMessage(sessionID, message, options ?? {});
|
|
2151
|
+
await reconcileLocalMarkerAfterProgress(locallyDeliveredPendingSessions, sessionID, observed.goal);
|
|
2152
|
+
const scheduled = scheduledContinuations.get(sessionID);
|
|
2153
|
+
if (observed.progressed && scheduled?.purpose !== "settle")
|
|
2154
|
+
cancelScheduledContinuation(sessionID);
|
|
1702
2155
|
}
|
|
1703
|
-
if (!
|
|
2156
|
+
if (!isIdleEvent(event))
|
|
1704
2157
|
return;
|
|
1705
2158
|
if (!sessionID)
|
|
1706
2159
|
return;
|
|
2160
|
+
if (!autoContinue && (await getGoalInternal(sessionID))?.pendingAttempt == null)
|
|
2161
|
+
return;
|
|
1707
2162
|
await runAutoContinue(sessionID);
|
|
1708
2163
|
}
|
|
1709
2164
|
};
|
|
@@ -1718,7 +2173,7 @@ async function setupV2(context) {
|
|
|
1718
2173
|
const autoContinue = options.auto_continue ?? true;
|
|
1719
2174
|
const deferWhileTasksActive = options.defer_while_tasks_active ?? true;
|
|
1720
2175
|
const maxAutoTurns = positiveIntegerOrNull2(options.max_auto_turns) ?? DEFAULT_MAX_AUTO_TURNS;
|
|
1721
|
-
const minInterval =
|
|
2176
|
+
const minInterval = nonNegativeIntegerOrNull(options.min_continue_interval_seconds) ?? DEFAULT_CONTINUE_INTERVAL_SECONDS;
|
|
1722
2177
|
const maxTurnTimeMs = timeoutMillisecondsFromSeconds(options.max_turn_time);
|
|
1723
2178
|
const maxPromptFailures = positiveIntegerOrNull2(options.max_prompt_failures) ?? DEFAULT_MAX_PROMPT_FAILURES;
|
|
1724
2179
|
const registerCommand = options.register_command ?? true;
|
|
@@ -1728,6 +2183,10 @@ async function setupV2(context) {
|
|
|
1728
2183
|
const scheduledContinuations = new Map;
|
|
1729
2184
|
const turnWatchdogs = new Map;
|
|
1730
2185
|
const busySessions = new Set;
|
|
2186
|
+
const nativeRetrySessions = new Set;
|
|
2187
|
+
const locallyDeliveredPendingSessions = new Set;
|
|
2188
|
+
const watchdogRescuedSessions = new Set;
|
|
2189
|
+
const toolAttempts = new Map;
|
|
1731
2190
|
const planAgents = restrictedAgentSet(options);
|
|
1732
2191
|
const isPlanAgent = (agent) => typeof agent === "string" && planAgents.has(agent.trim().toLowerCase());
|
|
1733
2192
|
const goalServices = { options, isPlanAgent };
|
|
@@ -1736,6 +2195,7 @@ async function setupV2(context) {
|
|
|
1736
2195
|
const stepTextBuffers = new Map;
|
|
1737
2196
|
const stepTokenSums = new Map;
|
|
1738
2197
|
const registrations = [];
|
|
2198
|
+
let disposed = false;
|
|
1739
2199
|
function stepKey(sessionID, messageID2) {
|
|
1740
2200
|
return `${sessionID}\x00${messageID2}`;
|
|
1741
2201
|
}
|
|
@@ -1764,6 +2224,8 @@ async function setupV2(context) {
|
|
|
1764
2224
|
function armTurnWatchdog(sessionID) {
|
|
1765
2225
|
if (maxTurnTimeMs == null)
|
|
1766
2226
|
return;
|
|
2227
|
+
if (watchdogRescuedSessions.has(sessionID))
|
|
2228
|
+
return;
|
|
1767
2229
|
clearTurnWatchdog(sessionID);
|
|
1768
2230
|
const watchdog = {
|
|
1769
2231
|
timer: setTimeout(() => void runTurnWatchdog(sessionID, watchdog), maxTurnTimeMs)
|
|
@@ -1776,7 +2238,9 @@ async function setupV2(context) {
|
|
|
1776
2238
|
async function runTurnWatchdog(sessionID, watchdog) {
|
|
1777
2239
|
let claimedContinuation = false;
|
|
1778
2240
|
try {
|
|
1779
|
-
if (
|
|
2241
|
+
if (disposed)
|
|
2242
|
+
return;
|
|
2243
|
+
if (turnWatchdogs.get(sessionID) !== watchdog || !busySessions.has(sessionID) || watchdogRescuedSessions.has(sessionID))
|
|
1780
2244
|
return;
|
|
1781
2245
|
const goal = await getGoal(sessionID);
|
|
1782
2246
|
if (turnWatchdogs.get(sessionID) !== watchdog || !busySessions.has(sessionID))
|
|
@@ -1791,7 +2255,7 @@ async function setupV2(context) {
|
|
|
1791
2255
|
return;
|
|
1792
2256
|
if (taskStatus && taskStatus.blocked)
|
|
1793
2257
|
return;
|
|
1794
|
-
const current = await
|
|
2258
|
+
const current = await getGoalInternal(sessionID);
|
|
1795
2259
|
if (turnWatchdogs.get(sessionID) !== watchdog || !busySessions.has(sessionID))
|
|
1796
2260
|
return;
|
|
1797
2261
|
if (current?.status !== "active" || isPlanAgent(current.lastPromptAgent) || activeContinuationsV2.has(sessionID))
|
|
@@ -1799,9 +2263,20 @@ async function setupV2(context) {
|
|
|
1799
2263
|
turnWatchdogs.delete(sessionID);
|
|
1800
2264
|
activeContinuationsV2.add(sessionID);
|
|
1801
2265
|
claimedContinuation = true;
|
|
2266
|
+
watchdogRescuedSessions.add(sessionID);
|
|
1802
2267
|
await sendContinuation2(sessionID, continuationPrompt(current), current.lastPromptAgent ?? latestStep?.agent ?? null);
|
|
2268
|
+
await recordContinuationResult(sessionID, "success", maxPromptFailures, { armNoProgress: false, started: true });
|
|
2269
|
+
locallyDeliveredPendingSessions.add(sessionID);
|
|
2270
|
+
clearTurnWatchdog(sessionID);
|
|
1803
2271
|
} catch (error) {
|
|
1804
|
-
|
|
2272
|
+
try {
|
|
2273
|
+
if (claimedContinuation && isTransportError(error)) {
|
|
2274
|
+
await recordContinuationResult(sessionID, "failure", maxPromptFailures);
|
|
2275
|
+
}
|
|
2276
|
+
v2ErrorLog("Turn watchdog retry failed", error);
|
|
2277
|
+
} catch {
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
1805
2280
|
} finally {
|
|
1806
2281
|
if (claimedContinuation)
|
|
1807
2282
|
activeContinuationsV2.delete(sessionID);
|
|
@@ -1809,24 +2284,53 @@ async function setupV2(context) {
|
|
|
1809
2284
|
turnWatchdogs.delete(sessionID);
|
|
1810
2285
|
}
|
|
1811
2286
|
}
|
|
1812
|
-
function
|
|
1813
|
-
|
|
2287
|
+
function cancelScheduledContinuation(sessionID) {
|
|
2288
|
+
const scheduled = scheduledContinuations.get(sessionID);
|
|
2289
|
+
if (scheduled)
|
|
2290
|
+
clearTimeout(scheduled.timer);
|
|
2291
|
+
scheduledContinuations.delete(sessionID);
|
|
2292
|
+
}
|
|
2293
|
+
function scheduleSettledContinuation(sessionID, delayMs = TASK_SETTLE_DELAY_MS, replace = false, purpose = "settle") {
|
|
2294
|
+
if (disposed)
|
|
1814
2295
|
return;
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
2296
|
+
if (!replace && scheduledContinuations.has(sessionID))
|
|
2297
|
+
return;
|
|
2298
|
+
if (replace)
|
|
2299
|
+
cancelScheduledContinuation(sessionID);
|
|
2300
|
+
const scheduled = {};
|
|
2301
|
+
const timer = setTimeout(async () => {
|
|
2302
|
+
try {
|
|
2303
|
+
if (scheduledContinuations.get(sessionID) !== scheduled || nativeRetrySessions.has(sessionID))
|
|
2304
|
+
return;
|
|
2305
|
+
if (purpose === "retry") {
|
|
2306
|
+
const goal = await getGoalInternal(sessionID);
|
|
2307
|
+
if (!goal || goal.continuationFailures === 0 && goal.pendingAttempt == null)
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
if (scheduledContinuations.get(sessionID) !== scheduled || nativeRetrySessions.has(sessionID))
|
|
2311
|
+
return;
|
|
2312
|
+
await runAutoContinue(sessionID, true, scheduled);
|
|
2313
|
+
} finally {
|
|
2314
|
+
if (scheduledContinuations.get(sessionID) === scheduled)
|
|
2315
|
+
scheduledContinuations.delete(sessionID);
|
|
2316
|
+
}
|
|
1818
2317
|
}, Math.max(0, delayMs));
|
|
2318
|
+
scheduled.timer = timer;
|
|
2319
|
+
scheduled.purpose = purpose;
|
|
1819
2320
|
const maybeUnref = timer;
|
|
1820
2321
|
if (typeof maybeUnref.unref === "function")
|
|
1821
2322
|
maybeUnref.unref();
|
|
1822
|
-
scheduledContinuations.set(sessionID,
|
|
2323
|
+
scheduledContinuations.set(sessionID, scheduled);
|
|
1823
2324
|
}
|
|
1824
|
-
async function runAutoContinue(sessionID, fromTaskDeferral = false) {
|
|
2325
|
+
async function runAutoContinue(sessionID, fromTaskDeferral = false, scheduled) {
|
|
2326
|
+
if (disposed)
|
|
2327
|
+
return;
|
|
1825
2328
|
if (busySessions.has(sessionID))
|
|
1826
2329
|
return;
|
|
1827
2330
|
if (activeContinuationsV2.has(sessionID))
|
|
1828
2331
|
return;
|
|
1829
2332
|
activeContinuationsV2.add(sessionID);
|
|
2333
|
+
let attemptReservedAt = Date.now();
|
|
1830
2334
|
try {
|
|
1831
2335
|
const latestStep = latestStepBySession.get(sessionID);
|
|
1832
2336
|
if (latestStep?.messageID) {
|
|
@@ -1835,23 +2339,33 @@ async function setupV2(context) {
|
|
|
1835
2339
|
const taskStatus = taskBlockStatus(sessionID);
|
|
1836
2340
|
if (taskStatus && taskStatus.blocked) {
|
|
1837
2341
|
taskDeferredSessions.add(sessionID);
|
|
1838
|
-
if (taskStatus.retryAt != null)
|
|
1839
|
-
scheduleSettledContinuation(sessionID, taskStatus.retryAt - Date.now());
|
|
2342
|
+
if (taskStatus.retryAt != null) {
|
|
2343
|
+
scheduleSettledContinuation(sessionID, taskStatus.retryAt - Date.now(), scheduled != null);
|
|
2344
|
+
}
|
|
1840
2345
|
return;
|
|
1841
2346
|
}
|
|
1842
2347
|
if (busySessions.has(sessionID))
|
|
1843
2348
|
return;
|
|
1844
2349
|
if (latestStep) {
|
|
1845
|
-
await
|
|
2350
|
+
const beforeProgress = await getGoalInternal(sessionID);
|
|
2351
|
+
const after = await recordAssistantProgress(sessionID, {
|
|
1846
2352
|
messageID: latestStep.messageID,
|
|
1847
2353
|
text: latestStep.text,
|
|
1848
2354
|
outputTokens: latestStep.outputTokens,
|
|
1849
2355
|
noProgressTokenThreshold: positiveIntegerOrNull2(options.no_progress_token_threshold),
|
|
1850
2356
|
maxNoProgressTurns: positiveIntegerOrNull2(options.max_no_progress_turns),
|
|
1851
|
-
evaluateContinuation: true
|
|
2357
|
+
evaluateContinuation: true,
|
|
2358
|
+
completedAt: latestStep.completedAt
|
|
1852
2359
|
});
|
|
2360
|
+
await reconcileLocalMarkerAfterProgress(locallyDeliveredPendingSessions, sessionID, after);
|
|
2361
|
+
const progressed = Boolean(after && (after.lastAssistantMessageID !== (beforeProgress?.lastAssistantMessageID ?? "") || after.lastAssistantText !== (beforeProgress?.lastAssistantText ?? "")));
|
|
2362
|
+
const queuedAfterProgress = scheduledContinuations.get(sessionID);
|
|
2363
|
+
if (progressed && queuedAfterProgress?.purpose !== "settle")
|
|
2364
|
+
cancelScheduledContinuation(sessionID);
|
|
1853
2365
|
}
|
|
1854
|
-
|
|
2366
|
+
if (scheduled && scheduledContinuations.get(sessionID) !== scheduled)
|
|
2367
|
+
return;
|
|
2368
|
+
const current = await getGoalInternal(sessionID);
|
|
1855
2369
|
if (!current)
|
|
1856
2370
|
return;
|
|
1857
2371
|
const latestTurnAgent = latestStep?.agent;
|
|
@@ -1867,13 +2381,64 @@ async function setupV2(context) {
|
|
|
1867
2381
|
return;
|
|
1868
2382
|
}
|
|
1869
2383
|
taskDeferredSessions.delete(sessionID);
|
|
2384
|
+
const attempt = pendingAttemptOf(current);
|
|
2385
|
+
if (current.status === "active" && attempt != null) {
|
|
2386
|
+
const deliveredLocally = locallyDeliveredPendingSessions.has(sessionID);
|
|
2387
|
+
if (!pendingReadyForFailure(attempt, deliveredLocally)) {
|
|
2388
|
+
return;
|
|
2389
|
+
}
|
|
2390
|
+
const afterFailure = await recordContinuationResult(sessionID, "failure", maxPromptFailures, {
|
|
2391
|
+
requirePending: true
|
|
2392
|
+
});
|
|
2393
|
+
if (afterFailure)
|
|
2394
|
+
locallyDeliveredPendingSessions.delete(sessionID);
|
|
2395
|
+
if (autoContinue && afterFailure?.status === "active") {
|
|
2396
|
+
scheduleSettledContinuation(sessionID, continuationRetryDelayMs(minInterval, attempt.reservedAt), true, "retry");
|
|
2397
|
+
}
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
const queuedBeforeReserve = scheduledContinuations.get(sessionID);
|
|
2401
|
+
if (queuedBeforeReserve && queuedBeforeReserve !== scheduled)
|
|
2402
|
+
return;
|
|
2403
|
+
if (!autoContinue)
|
|
2404
|
+
return;
|
|
2405
|
+
if (nativeRetrySessions.has(sessionID))
|
|
2406
|
+
return;
|
|
1870
2407
|
const goal = await reserveContinuation(sessionID, maxAutoTurns, minInterval);
|
|
1871
2408
|
if (!goal)
|
|
1872
2409
|
return;
|
|
2410
|
+
attemptReservedAt = goal.pendingAttempt?.reservedAt ?? Date.now();
|
|
2411
|
+
if (nativeRetrySessions.has(sessionID)) {
|
|
2412
|
+
await rollbackContinuationAttempt(sessionID);
|
|
2413
|
+
return;
|
|
2414
|
+
}
|
|
2415
|
+
if (scheduled && scheduledContinuations.get(sessionID) !== scheduled) {
|
|
2416
|
+
await rollbackContinuationAttempt(sessionID);
|
|
2417
|
+
return;
|
|
2418
|
+
}
|
|
1873
2419
|
await sendContinuation2(sessionID, goal.status === "active" ? continuationPrompt(goal) : limitPrompt(goal), goal.lastPromptAgent ?? latestTurnAgent ?? null);
|
|
1874
|
-
|
|
2420
|
+
if (disposed) {
|
|
2421
|
+
await rollbackContinuationAttempt(sessionID);
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
const delivered = await recordContinuationResult(sessionID, "success", maxPromptFailures);
|
|
2425
|
+
locallyDeliveredPendingSessions.add(sessionID);
|
|
2426
|
+
if (!delivered?.pendingAttempt?.delivered) {
|
|
2427
|
+
await rollbackContinuationAttempt(sessionID);
|
|
2428
|
+
}
|
|
1875
2429
|
} catch (error) {
|
|
1876
|
-
|
|
2430
|
+
if (disposed) {
|
|
2431
|
+
await rollbackContinuationAttempt(sessionID);
|
|
2432
|
+
return;
|
|
2433
|
+
}
|
|
2434
|
+
if (isTransportError(error)) {
|
|
2435
|
+
const afterFailure = await recordContinuationResult(sessionID, "failure", maxPromptFailures);
|
|
2436
|
+
if (autoContinue && afterFailure?.status === "active") {
|
|
2437
|
+
scheduleSettledContinuation(sessionID, continuationRetryDelayMs(minInterval, attemptReservedAt), true, "retry");
|
|
2438
|
+
}
|
|
2439
|
+
} else {
|
|
2440
|
+
await rollbackContinuationAttempt(sessionID);
|
|
2441
|
+
}
|
|
1877
2442
|
v2ErrorLog("Auto-continue failed", error);
|
|
1878
2443
|
} finally {
|
|
1879
2444
|
activeContinuationsV2.delete(sessionID);
|
|
@@ -1893,31 +2458,76 @@ async function setupV2(context) {
|
|
|
1893
2458
|
case "session.status": {
|
|
1894
2459
|
const status = data.status;
|
|
1895
2460
|
if (sessionID && isRecord(status) && typeof status.type === "string") {
|
|
1896
|
-
if (status.type === "busy")
|
|
2461
|
+
if (status.type === "busy") {
|
|
1897
2462
|
busySessions.add(sessionID);
|
|
1898
|
-
|
|
2463
|
+
nativeRetrySessions.delete(sessionID);
|
|
1899
2464
|
armTurnWatchdog(sessionID);
|
|
2465
|
+
await markPendingContinuationStarted(sessionID);
|
|
2466
|
+
}
|
|
1900
2467
|
if (status.type === "idle") {
|
|
1901
2468
|
busySessions.delete(sessionID);
|
|
2469
|
+
nativeRetrySessions.delete(sessionID);
|
|
1902
2470
|
clearTurnWatchdog(sessionID);
|
|
2471
|
+
watchdogRescuedSessions.delete(sessionID);
|
|
1903
2472
|
}
|
|
1904
|
-
if (status.type === "retry")
|
|
2473
|
+
if (status.type === "retry") {
|
|
2474
|
+
nativeRetrySessions.add(sessionID);
|
|
1905
2475
|
clearTurnWatchdog(sessionID);
|
|
2476
|
+
cancelScheduledContinuation(sessionID);
|
|
2477
|
+
}
|
|
1906
2478
|
taskTracker.observeSessionStatus(sessionID, status.type);
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
2479
|
+
if (status.type === "idle") {
|
|
2480
|
+
const goal = await getGoalInternal(sessionID);
|
|
2481
|
+
if (autoContinue || goal?.pendingAttempt != null)
|
|
2482
|
+
await runAutoContinue(sessionID);
|
|
2483
|
+
}
|
|
1910
2484
|
}
|
|
1911
2485
|
return;
|
|
1912
2486
|
}
|
|
1913
2487
|
case "session.idle": {
|
|
1914
2488
|
if (sessionID) {
|
|
1915
2489
|
busySessions.delete(sessionID);
|
|
2490
|
+
nativeRetrySessions.delete(sessionID);
|
|
1916
2491
|
clearTurnWatchdog(sessionID);
|
|
2492
|
+
watchdogRescuedSessions.delete(sessionID);
|
|
1917
2493
|
taskTracker.observeSessionStatus(sessionID, "idle");
|
|
1918
2494
|
}
|
|
1919
|
-
if (
|
|
1920
|
-
await
|
|
2495
|
+
if (sessionID) {
|
|
2496
|
+
const goal = await getGoalInternal(sessionID);
|
|
2497
|
+
if (autoContinue || goal?.pendingAttempt != null)
|
|
2498
|
+
await runAutoContinue(sessionID);
|
|
2499
|
+
}
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
2502
|
+
case "session.error": {
|
|
2503
|
+
if (!sessionID)
|
|
2504
|
+
return;
|
|
2505
|
+
const inNativeRetry = nativeRetrySessions.has(sessionID);
|
|
2506
|
+
busySessions.delete(sessionID);
|
|
2507
|
+
clearTurnWatchdog(sessionID);
|
|
2508
|
+
if (inNativeRetry)
|
|
2509
|
+
return;
|
|
2510
|
+
nativeRetrySessions.delete(sessionID);
|
|
2511
|
+
watchdogRescuedSessions.delete(sessionID);
|
|
2512
|
+
const errorMessage = transportErrorMessageFromEvent(data);
|
|
2513
|
+
if (errorMessage && isTransportError(errorMessage)) {
|
|
2514
|
+
const goal = await getGoalInternal(sessionID);
|
|
2515
|
+
if (goal?.status === "active") {
|
|
2516
|
+
const attempt = pendingAttemptOf(goal);
|
|
2517
|
+
if (attempt != null) {
|
|
2518
|
+
const afterFailure = await recordContinuationResult(sessionID, "failure", maxPromptFailures, {
|
|
2519
|
+
requirePending: true
|
|
2520
|
+
});
|
|
2521
|
+
if (afterFailure)
|
|
2522
|
+
locallyDeliveredPendingSessions.delete(sessionID);
|
|
2523
|
+
if (autoContinue && afterFailure?.status === "active") {
|
|
2524
|
+
scheduleSettledContinuation(sessionID, continuationRetryDelayMs(minInterval, attempt.reservedAt), true, "retry");
|
|
2525
|
+
}
|
|
2526
|
+
} else if (autoContinue) {
|
|
2527
|
+
scheduleSettledContinuation(sessionID, continuationDelayFromSnapshot(minInterval, goal.lastContinuationAt), false, "recovery");
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
1921
2531
|
return;
|
|
1922
2532
|
}
|
|
1923
2533
|
case "session.deleted": {
|
|
@@ -1925,11 +2535,15 @@ async function setupV2(context) {
|
|
|
1925
2535
|
return;
|
|
1926
2536
|
busySessions.delete(sessionID);
|
|
1927
2537
|
clearTurnWatchdog(sessionID);
|
|
2538
|
+
watchdogRescuedSessions.delete(sessionID);
|
|
2539
|
+
locallyDeliveredPendingSessions.delete(sessionID);
|
|
2540
|
+
nativeRetrySessions.delete(sessionID);
|
|
1928
2541
|
const scheduled = scheduledContinuations.get(sessionID);
|
|
1929
2542
|
if (scheduled)
|
|
1930
|
-
clearTimeout(scheduled);
|
|
2543
|
+
clearTimeout(scheduled.timer);
|
|
1931
2544
|
scheduledContinuations.delete(sessionID);
|
|
1932
2545
|
taskDeferredSessions.delete(sessionID);
|
|
2546
|
+
clearToolAttemptsForSession(toolAttempts, sessionID);
|
|
1933
2547
|
taskTracker.observeSessionDeleted(sessionID);
|
|
1934
2548
|
latestStepBySession.delete(sessionID);
|
|
1935
2549
|
stepTokenSums.delete(sessionID);
|
|
@@ -1956,7 +2570,7 @@ async function setupV2(context) {
|
|
|
1956
2570
|
});
|
|
1957
2571
|
if (!stepTextBuffers.has(stepKey(sessionID, messageID2)))
|
|
1958
2572
|
stepTextBuffers.set(stepKey(sessionID, messageID2), "");
|
|
1959
|
-
latestStepBySession.set(sessionID, { messageID: messageID2, agent, text: "", outputTokens: null });
|
|
2573
|
+
latestStepBySession.set(sessionID, { messageID: messageID2, agent, text: "", outputTokens: null, completedAt: event.created });
|
|
1960
2574
|
return;
|
|
1961
2575
|
}
|
|
1962
2576
|
case "session.text.delta": {
|
|
@@ -1985,18 +2599,26 @@ async function setupV2(context) {
|
|
|
1985
2599
|
const text = stepTextBuffers.get(stepKey(sessionID, messageID2)) ?? "";
|
|
1986
2600
|
stepTextBuffers.delete(stepKey(sessionID, messageID2));
|
|
1987
2601
|
const outputTokens = outputTokensFromRecord(data.tokens) ?? null;
|
|
1988
|
-
await recordAssistantProgress(sessionID, {
|
|
2602
|
+
const afterStep = await recordAssistantProgress(sessionID, {
|
|
1989
2603
|
messageID: messageID2,
|
|
1990
2604
|
text,
|
|
1991
2605
|
outputTokens,
|
|
1992
2606
|
noProgressTokenThreshold: positiveIntegerOrNull2(options.no_progress_token_threshold),
|
|
1993
|
-
maxNoProgressTurns: positiveIntegerOrNull2(options.max_no_progress_turns)
|
|
2607
|
+
maxNoProgressTurns: positiveIntegerOrNull2(options.max_no_progress_turns),
|
|
2608
|
+
completedAt: event.created
|
|
1994
2609
|
});
|
|
2610
|
+
await reconcileLocalMarkerAfterProgress(locallyDeliveredPendingSessions, sessionID, afterStep);
|
|
2611
|
+
if (/[\p{L}\p{N}]/u.test(text)) {
|
|
2612
|
+
const scheduled = scheduledContinuations.get(sessionID);
|
|
2613
|
+
if (scheduled?.purpose === "recovery")
|
|
2614
|
+
cancelScheduledContinuation(sessionID);
|
|
2615
|
+
}
|
|
1995
2616
|
latestStepBySession.set(sessionID, {
|
|
1996
2617
|
messageID: messageID2,
|
|
1997
2618
|
agent: latestStepBySession.get(sessionID)?.agent,
|
|
1998
2619
|
text,
|
|
1999
|
-
outputTokens
|
|
2620
|
+
outputTokens,
|
|
2621
|
+
completedAt: event.created
|
|
2000
2622
|
});
|
|
2001
2623
|
return;
|
|
2002
2624
|
}
|
|
@@ -2013,18 +2635,26 @@ async function setupV2(context) {
|
|
|
2013
2635
|
const text = stepTextBuffers.get(stepKey(sessionID, messageID2)) ?? "";
|
|
2014
2636
|
stepTextBuffers.delete(stepKey(sessionID, messageID2));
|
|
2015
2637
|
const outputTokens = outputTokensFromRecord(data.tokens) ?? null;
|
|
2016
|
-
await recordAssistantProgress(sessionID, {
|
|
2638
|
+
const afterStep = await recordAssistantProgress(sessionID, {
|
|
2017
2639
|
messageID: messageID2,
|
|
2018
2640
|
text,
|
|
2019
2641
|
outputTokens,
|
|
2020
2642
|
noProgressTokenThreshold: positiveIntegerOrNull2(options.no_progress_token_threshold),
|
|
2021
|
-
maxNoProgressTurns: positiveIntegerOrNull2(options.max_no_progress_turns)
|
|
2643
|
+
maxNoProgressTurns: positiveIntegerOrNull2(options.max_no_progress_turns),
|
|
2644
|
+
completedAt: event.created
|
|
2022
2645
|
});
|
|
2646
|
+
await reconcileLocalMarkerAfterProgress(locallyDeliveredPendingSessions, sessionID, afterStep);
|
|
2647
|
+
if (/[\p{L}\p{N}]/u.test(text)) {
|
|
2648
|
+
const scheduled = scheduledContinuations.get(sessionID);
|
|
2649
|
+
if (scheduled?.purpose === "recovery")
|
|
2650
|
+
cancelScheduledContinuation(sessionID);
|
|
2651
|
+
}
|
|
2023
2652
|
latestStepBySession.set(sessionID, {
|
|
2024
2653
|
messageID: messageID2,
|
|
2025
2654
|
agent: latestStepBySession.get(sessionID)?.agent,
|
|
2026
2655
|
text,
|
|
2027
|
-
outputTokens
|
|
2656
|
+
outputTokens,
|
|
2657
|
+
completedAt: event.created
|
|
2028
2658
|
});
|
|
2029
2659
|
return;
|
|
2030
2660
|
}
|
|
@@ -2052,13 +2682,44 @@ async function setupV2(context) {
|
|
|
2052
2682
|
for (const tool of goalToolsV2(goalServices))
|
|
2053
2683
|
draft.add(tool);
|
|
2054
2684
|
}));
|
|
2055
|
-
registrations.push(await context.tool.hook("execute.before", (input) => {
|
|
2685
|
+
registrations.push(await context.tool.hook("execute.before", async (input) => {
|
|
2056
2686
|
taskTracker.noteTaskCall({ tool: input.tool, sessionID: input.sessionID, callID: input.id });
|
|
2687
|
+
const sessionID = typeof input.sessionID === "string" ? input.sessionID : undefined;
|
|
2688
|
+
const callID = typeof input.id === "string" ? input.id : undefined;
|
|
2689
|
+
if (sessionID && callID) {
|
|
2690
|
+
const goal = await getGoalInternal(sessionID);
|
|
2691
|
+
toolAttempts.set(toolAttemptKey(sessionID, callID), goal?.pendingAttempt?.id ?? null);
|
|
2692
|
+
}
|
|
2057
2693
|
}));
|
|
2058
|
-
registrations.push(await context.tool.hook("execute.after", (input) => {
|
|
2694
|
+
registrations.push(await context.tool.hook("execute.after", async (input) => {
|
|
2695
|
+
const sessionID = typeof input.sessionID === "string" ? input.sessionID : undefined;
|
|
2696
|
+
const callID = typeof input.id === "string" ? input.id : undefined;
|
|
2697
|
+
const attemptKey = sessionID && callID ? toolAttemptKey(sessionID, callID) : undefined;
|
|
2698
|
+
const expectedAttemptID = attemptKey ? toolAttempts.get(attemptKey) : undefined;
|
|
2699
|
+
if (attemptKey)
|
|
2700
|
+
toolAttempts.delete(attemptKey);
|
|
2059
2701
|
if (input.status !== "completed")
|
|
2060
2702
|
return;
|
|
2703
|
+
const text = textFromToolResult(input.result);
|
|
2061
2704
|
taskTracker.noteTaskOutput({ tool: input.tool, sessionID: input.sessionID, callID: input.id }, { output: textFromToolResult(input.result) });
|
|
2705
|
+
if (!sessionID || typeof input.tool !== "string")
|
|
2706
|
+
return;
|
|
2707
|
+
if (NON_PROGRESS_TOOLS.has(input.tool.toLowerCase()))
|
|
2708
|
+
return;
|
|
2709
|
+
if (toolOutputFailed(input.result))
|
|
2710
|
+
return;
|
|
2711
|
+
if (!text)
|
|
2712
|
+
return;
|
|
2713
|
+
const before = await getGoalInternal(sessionID);
|
|
2714
|
+
const scheduled = scheduledContinuations.get(sessionID);
|
|
2715
|
+
const hasFailureEpisode = Boolean(before && (before.continuationFailures > 0 || before.pendingAttempt != null));
|
|
2716
|
+
if (!before || !hasFailureEpisode && scheduled?.purpose !== "recovery")
|
|
2717
|
+
return;
|
|
2718
|
+
const progressed = await recordToolProgress(sessionID, text, expectedAttemptID);
|
|
2719
|
+
if (progressed?.continuationFailures === 0 && progressed.pendingAttempt == null) {
|
|
2720
|
+
locallyDeliveredPendingSessions.delete(sessionID);
|
|
2721
|
+
cancelScheduledContinuation(sessionID);
|
|
2722
|
+
}
|
|
2062
2723
|
}));
|
|
2063
2724
|
registrations.push(await context.session.hook("context", (sessionContext) => {
|
|
2064
2725
|
const reminder = systemReminder();
|
|
@@ -2085,14 +2746,19 @@ async function setupV2(context) {
|
|
|
2085
2746
|
}
|
|
2086
2747
|
})();
|
|
2087
2748
|
return async () => {
|
|
2749
|
+
disposed = true;
|
|
2088
2750
|
abortController.abort();
|
|
2089
|
-
for (const
|
|
2090
|
-
clearTimeout(timer);
|
|
2751
|
+
for (const scheduled of scheduledContinuations.values())
|
|
2752
|
+
clearTimeout(scheduled.timer);
|
|
2091
2753
|
scheduledContinuations.clear();
|
|
2092
2754
|
for (const watchdog of turnWatchdogs.values())
|
|
2093
2755
|
clearTimeout(watchdog.timer);
|
|
2094
2756
|
turnWatchdogs.clear();
|
|
2095
2757
|
activeContinuationsV2.clear();
|
|
2758
|
+
nativeRetrySessions.clear();
|
|
2759
|
+
locallyDeliveredPendingSessions.clear();
|
|
2760
|
+
watchdogRescuedSessions.clear();
|
|
2761
|
+
toolAttempts.clear();
|
|
2096
2762
|
for (const registration of registrations)
|
|
2097
2763
|
await registration.dispose();
|
|
2098
2764
|
const termination = Promise.allSettled([consumer, eventIterator?.return?.()]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prevalentware/opencode-goal-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"description": "OpenCode goal plugin that adds Codex-style long-running goal mode, /goal commands, persistence, and TUI status for AI coding agents.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"opencode",
|