@gajae-code/coding-agent 0.2.5 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/dist/types/async/job-manager.d.ts +91 -2
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/commands/harness.d.ts +37 -0
- package/dist/types/config/keybindings.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +10 -4
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/debug/crash-diagnostics.d.ts +45 -0
- package/dist/types/debug/runtime-gauges.d.ts +6 -0
- package/dist/types/deep-interview/render-middleware.d.ts +6 -0
- package/dist/types/eval/py/executor.d.ts +2 -0
- package/dist/types/eval/py/kernel.d.ts +2 -0
- package/dist/types/exec/bash-executor.d.ts +10 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +6 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -0
- package/dist/types/gjc-runtime/cli-write-receipt.d.ts +24 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -0
- package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
- package/dist/types/gjc-runtime/state-migrations.d.ts +33 -0
- package/dist/types/gjc-runtime/state-renderer.d.ts +65 -0
- package/dist/types/gjc-runtime/state-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/state-schema.d.ts +317 -0
- package/dist/types/gjc-runtime/state-validation.d.ts +6 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +147 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
- package/dist/types/gjc-runtime/workflow-command-ref.d.ts +43 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +54 -0
- package/dist/types/harness-control-plane/classifier.d.ts +13 -0
- package/dist/types/harness-control-plane/control-endpoint.d.ts +31 -0
- package/dist/types/harness-control-plane/finalize.d.ts +47 -0
- package/dist/types/harness-control-plane/frame-mapper.d.ts +29 -0
- package/dist/types/harness-control-plane/operate.d.ts +35 -0
- package/dist/types/harness-control-plane/owner.d.ts +46 -0
- package/dist/types/harness-control-plane/preserve.d.ts +19 -0
- package/dist/types/harness-control-plane/receipts.d.ts +88 -0
- package/dist/types/harness-control-plane/rpc-adapter.d.ts +66 -0
- package/dist/types/harness-control-plane/seams.d.ts +21 -0
- package/dist/types/harness-control-plane/session-lease.d.ts +65 -0
- package/dist/types/harness-control-plane/state-machine.d.ts +19 -0
- package/dist/types/harness-control-plane/storage.d.ts +53 -0
- package/dist/types/harness-control-plane/types.d.ts +162 -0
- package/dist/types/hooks/skill-keywords.d.ts +2 -1
- package/dist/types/hooks/skill-state.d.ts +23 -29
- package/dist/types/internal-urls/agent-protocol.d.ts +2 -2
- package/dist/types/internal-urls/artifact-protocol.d.ts +2 -2
- package/dist/types/internal-urls/registry-helpers.d.ts +8 -7
- package/dist/types/internal-urls/types.d.ts +4 -0
- package/dist/types/lsp/index.d.ts +10 -10
- package/dist/types/modes/bridge/auth.d.ts +12 -0
- package/dist/types/modes/bridge/bridge-client-bridge.d.ts +9 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +44 -0
- package/dist/types/modes/bridge/bridge-ui-context.d.ts +88 -0
- package/dist/types/modes/bridge/event-stream.d.ts +8 -0
- package/dist/types/modes/components/custom-editor.d.ts +6 -0
- package/dist/types/modes/components/hook-selector.d.ts +1 -0
- package/dist/types/modes/components/jobs-overlay-model.d.ts +31 -0
- package/dist/types/modes/components/jobs-overlay.d.ts +30 -0
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/components/status-line.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +8 -0
- package/dist/types/modes/index.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/jobs-observer.d.ts +57 -0
- package/dist/types/modes/rpc/host-tools.d.ts +1 -16
- package/dist/types/modes/rpc/host-uris.d.ts +1 -38
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +20 -0
- package/dist/types/modes/shared/agent-wire/command-validation.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +24 -0
- package/dist/types/modes/shared/agent-wire/handshake.d.ts +46 -0
- package/dist/types/modes/shared/agent-wire/host-tool-bridge.d.ts +16 -0
- package/dist/types/modes/shared/agent-wire/host-uri-bridge.d.ts +17 -0
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +44 -0
- package/dist/types/modes/shared/agent-wire/responses.d.ts +4 -0
- package/dist/types/modes/shared/agent-wire/scopes.d.ts +18 -0
- package/dist/types/modes/shared/agent-wire/ui-request-broker.d.ts +42 -0
- package/dist/types/modes/shared/agent-wire/ui-result.d.ts +27 -0
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +19 -1
- package/dist/types/skill-state/active-state.d.ts +2 -0
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
- package/dist/types/skill-state/workflow-state-contract.d.ts +25 -2
- package/dist/types/skill-state/workflow-state-version.d.ts +3 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/id.d.ts +7 -0
- package/dist/types/task/index.d.ts +5 -0
- package/dist/types/task/receipt.d.ts +85 -0
- package/dist/types/task/spawn-gate.d.ts +38 -0
- package/dist/types/task/types.d.ts +198 -14
- package/dist/types/tools/cron.d.ts +6 -0
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/subagent.d.ts +26 -1
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/async/job-manager.ts +334 -6
- package/src/cli/args.ts +9 -2
- package/src/cli/auth-broker-cli.ts +1 -0
- package/src/cli/config-cli.ts +10 -2
- package/src/cli.ts +2 -0
- package/src/commands/deep-interview.ts +1 -0
- package/src/commands/harness.ts +862 -0
- package/src/commands/launch.ts +2 -2
- package/src/commands/state.ts +2 -1
- package/src/commands/team.ts +54 -39
- package/src/config/keybindings.ts +6 -0
- package/src/config/settings-schema.ts +13 -3
- package/src/config/settings.ts +5 -0
- package/src/dap/client.ts +17 -3
- package/src/debug/crash-diagnostics.ts +223 -0
- package/src/debug/runtime-gauges.ts +20 -0
- package/src/deep-interview/render-middleware.ts +372 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ralplan/SKILL.md +31 -2
- package/src/defaults/gjc/skills/team/SKILL.md +47 -21
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +106 -13
- package/src/eval/py/executor.ts +21 -1
- package/src/eval/py/kernel.ts +15 -0
- package/src/exec/bash-executor.ts +41 -0
- package/src/extensibility/custom-tools/types.ts +1 -0
- package/src/extensibility/extensions/types.ts +6 -0
- package/src/extensibility/shared-events.ts +1 -0
- package/src/gjc-runtime/cli-write-receipt.ts +31 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +98 -42
- package/src/gjc-runtime/goal-mode-request.ts +11 -3
- package/src/gjc-runtime/ralplan-runtime.ts +235 -43
- package/src/gjc-runtime/state-graph.ts +86 -0
- package/src/gjc-runtime/state-migrations.ts +179 -0
- package/src/gjc-runtime/state-renderer.ts +345 -0
- package/src/gjc-runtime/state-runtime.ts +1155 -46
- package/src/gjc-runtime/state-schema.ts +192 -0
- package/src/gjc-runtime/state-validation.ts +49 -0
- package/src/gjc-runtime/state-writer.ts +749 -0
- package/src/gjc-runtime/team-runtime.ts +1255 -189
- package/src/gjc-runtime/ultragoal-runtime.ts +460 -43
- package/src/gjc-runtime/workflow-command-ref.ts +239 -0
- package/src/gjc-runtime/workflow-manifest.generated.json +1601 -0
- package/src/gjc-runtime/workflow-manifest.ts +427 -0
- package/src/harness-control-plane/classifier.ts +128 -0
- package/src/harness-control-plane/control-endpoint.ts +148 -0
- package/src/harness-control-plane/finalize.ts +222 -0
- package/src/harness-control-plane/frame-mapper.ts +286 -0
- package/src/harness-control-plane/operate.ts +225 -0
- package/src/harness-control-plane/owner.ts +600 -0
- package/src/harness-control-plane/preserve.ts +102 -0
- package/src/harness-control-plane/receipts.ts +216 -0
- package/src/harness-control-plane/rpc-adapter.ts +276 -0
- package/src/harness-control-plane/seams.ts +39 -0
- package/src/harness-control-plane/session-lease.ts +388 -0
- package/src/harness-control-plane/state-machine.ts +98 -0
- package/src/harness-control-plane/storage.ts +257 -0
- package/src/harness-control-plane/types.ts +214 -0
- package/src/hooks/skill-keywords.ts +4 -2
- package/src/hooks/skill-state.ts +197 -64
- package/src/internal-urls/agent-protocol.ts +68 -21
- package/src/internal-urls/artifact-protocol.ts +12 -17
- package/src/internal-urls/docs-index.generated.ts +3 -2
- package/src/internal-urls/registry-helpers.ts +19 -16
- package/src/internal-urls/types.ts +4 -0
- package/src/lsp/client.ts +18 -2
- package/src/main.ts +21 -5
- package/src/modes/bridge/auth.ts +41 -0
- package/src/modes/bridge/bridge-client-bridge.ts +47 -0
- package/src/modes/bridge/bridge-mode.ts +520 -0
- package/src/modes/bridge/bridge-ui-context.ts +200 -0
- package/src/modes/bridge/event-stream.ts +70 -0
- package/src/modes/components/assistant-message.ts +5 -1
- package/src/modes/components/custom-editor.ts +101 -0
- package/src/modes/components/hook-selector.ts +133 -20
- package/src/modes/components/jobs-overlay-model.ts +109 -0
- package/src/modes/components/jobs-overlay.ts +172 -0
- package/src/modes/components/status-line/presets.ts +7 -5
- package/src/modes/components/status-line/segments.ts +25 -0
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/status-line.ts +9 -1
- package/src/modes/controllers/event-controller.ts +71 -6
- package/src/modes/controllers/extension-ui-controller.ts +43 -1
- package/src/modes/controllers/input-controller.ts +105 -9
- package/src/modes/controllers/selector-controller.ts +31 -1
- package/src/modes/index.ts +1 -0
- package/src/modes/interactive-mode.ts +28 -0
- package/src/modes/jobs-observer.ts +204 -0
- package/src/modes/rpc/host-tools.ts +1 -186
- package/src/modes/rpc/host-uris.ts +1 -235
- package/src/modes/rpc/rpc-client.ts +25 -10
- package/src/modes/rpc/rpc-mode.ts +12 -381
- package/src/modes/shared/agent-wire/command-dispatch.ts +341 -0
- package/src/modes/shared/agent-wire/command-validation.ts +131 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +108 -0
- package/src/modes/shared/agent-wire/handshake.ts +117 -0
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +194 -0
- package/src/modes/shared/agent-wire/host-uri-bridge.ts +236 -0
- package/src/modes/shared/agent-wire/protocol.ts +96 -0
- package/src/modes/shared/agent-wire/responses.ts +17 -0
- package/src/modes/shared/agent-wire/scopes.ts +89 -0
- package/src/modes/shared/agent-wire/ui-request-broker.ts +150 -0
- package/src/modes/shared/agent-wire/ui-result.ts +48 -0
- package/src/modes/types.ts +2 -0
- package/src/prompts/agents/executor.md +13 -0
- package/src/prompts/tools/subagent.md +39 -4
- package/src/prompts/tools/task-summary.md +3 -9
- package/src/prompts/tools/task.md +5 -1
- package/src/sdk.ts +8 -0
- package/src/session/agent-session.ts +445 -71
- package/src/session/session-manager.ts +13 -1
- package/src/skill-state/active-state.ts +58 -65
- package/src/skill-state/deep-interview-mutation-guard.ts +114 -17
- package/src/skill-state/initial-phase.ts +2 -0
- package/src/skill-state/workflow-state-contract.ts +33 -4
- package/src/skill-state/workflow-state-version.ts +3 -0
- package/src/slash-commands/builtin-registry.ts +8 -0
- package/src/task/executor.ts +79 -13
- package/src/task/id.ts +33 -0
- package/src/task/index.ts +376 -74
- package/src/task/output-manager.ts +5 -4
- package/src/task/receipt.ts +297 -0
- package/src/task/render.ts +54 -134
- package/src/task/spawn-gate.ts +132 -0
- package/src/task/types.ts +104 -10
- package/src/tools/ask.ts +88 -27
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash.ts +1 -1
- package/src/tools/cron.ts +48 -0
- package/src/tools/find.ts +4 -1
- package/src/tools/index.ts +2 -0
- package/src/tools/path-utils.ts +3 -2
- package/src/tools/read.ts +1 -0
- package/src/tools/search.ts +1 -0
- package/src/tools/skill.ts +6 -1
- package/src/tools/subagent.ts +423 -79
|
@@ -167,10 +167,12 @@ import type { HookCommandContext } from "../extensibility/hooks/types";
|
|
|
167
167
|
import type { Skill, SkillWarning } from "../extensibility/skills";
|
|
168
168
|
import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
|
|
169
169
|
import { buildGjcRuntimeSessionEnv, consumePendingGoalModeRequest } from "../gjc-runtime/goal-mode-request";
|
|
170
|
+
import { writeArtifact } from "../gjc-runtime/state-writer";
|
|
170
171
|
import { requestGjcWorkerIntegrationAttempt } from "../gjc-runtime/team-runtime";
|
|
171
172
|
import { GoalRuntime } from "../goals/runtime";
|
|
172
173
|
import type { Goal, GoalModeState } from "../goals/state";
|
|
173
174
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
175
|
+
import { ensureWorkflowSkillActivationState } from "../hooks/skill-state";
|
|
174
176
|
import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
175
177
|
import { resolveMemoryBackend } from "../memory-backend";
|
|
176
178
|
import { getCurrentThemeName, theme } from "../modes/theme/theme";
|
|
@@ -263,7 +265,14 @@ export type AgentSessionEvent =
|
|
|
263
265
|
/** True when compaction was skipped for a benign reason (no model, no candidates, nothing to compact). */
|
|
264
266
|
skipped?: boolean;
|
|
265
267
|
}
|
|
266
|
-
| {
|
|
268
|
+
| {
|
|
269
|
+
type: "auto_retry_start";
|
|
270
|
+
attempt: number;
|
|
271
|
+
maxAttempts: number;
|
|
272
|
+
delayMs: number;
|
|
273
|
+
errorMessage: string;
|
|
274
|
+
unbounded?: boolean;
|
|
275
|
+
}
|
|
267
276
|
| { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string }
|
|
268
277
|
| { type: "retry_fallback_applied"; from: string; to: string; role: string }
|
|
269
278
|
| { type: "retry_fallback_succeeded"; model: string; role: string }
|
|
@@ -282,9 +291,14 @@ export type AgentSessionEvent =
|
|
|
282
291
|
*/
|
|
283
292
|
const SAFE_PATH_COMPONENT = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/;
|
|
284
293
|
|
|
294
|
+
function isUnderProjectGjc(cwd: string, targetPath: string): boolean {
|
|
295
|
+
const relative = path.relative(path.join(path.resolve(cwd), ".gjc"), path.resolve(targetPath));
|
|
296
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
297
|
+
}
|
|
298
|
+
|
|
285
299
|
/** Listener function for agent session events */
|
|
286
300
|
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
|
|
287
|
-
export type AsyncJobSnapshotItem = Pick<AsyncJob, "id" | "type" | "status" | "label" | "startTime">;
|
|
301
|
+
export type AsyncJobSnapshotItem = Pick<AsyncJob, "id" | "type" | "status" | "label" | "startTime" | "metadata">;
|
|
288
302
|
|
|
289
303
|
export interface AsyncJobSnapshot {
|
|
290
304
|
running: AsyncJobSnapshotItem[];
|
|
@@ -852,12 +866,14 @@ export class AgentSession {
|
|
|
852
866
|
|
|
853
867
|
// Retry state
|
|
854
868
|
#retryAbortController: AbortController | undefined = undefined;
|
|
869
|
+
#retryNowRequested = false;
|
|
855
870
|
#retryAttempt = 0;
|
|
856
871
|
#retryPromise: Promise<void> | undefined = undefined;
|
|
857
872
|
#retryResolve: (() => void) | undefined = undefined;
|
|
858
873
|
#activeRetryFallback: ActiveRetryFallbackState | undefined = undefined;
|
|
859
874
|
// Todo completion reminder state
|
|
860
875
|
#todoReminderCount = 0;
|
|
876
|
+
#lastGoalReminderAssistantTimestamp: number | undefined = undefined;
|
|
861
877
|
#todoPhases: TodoPhase[] = [];
|
|
862
878
|
#toolChoiceQueue = new ToolChoiceQueue();
|
|
863
879
|
|
|
@@ -958,6 +974,12 @@ export class AgentSession {
|
|
|
958
974
|
* without producing an aborted message_end). */
|
|
959
975
|
#planCompactAbortPending = false;
|
|
960
976
|
|
|
977
|
+
/** One-shot flag armed by `abort({ silent: true })` (e.g. Esc consuming a
|
|
978
|
+
* queued steer). Consumed in #handleAgentEvent to stamp `SILENT_ABORT_MARKER`
|
|
979
|
+
* on the resulting aborted assistant `message_end` so the interrupt does not
|
|
980
|
+
* surface a red "Operation aborted" line; cleared by a later non-silent abort
|
|
981
|
+
* or by `abort`'s safety net when no aborted message_end is produced. */
|
|
982
|
+
#silentAbortPending = false;
|
|
961
983
|
/** Monotonic counter for `enqueueCustomMessageDisplay` tag generation;
|
|
962
984
|
* combined with `Date.now()` so tags stay unique even across rapid
|
|
963
985
|
* same-tick enqueues. */
|
|
@@ -1036,6 +1058,7 @@ export class AgentSession {
|
|
|
1036
1058
|
this.#promptInFlightCount = Math.max(0, this.#promptInFlightCount - 1);
|
|
1037
1059
|
if (this.#promptInFlightCount === 0) {
|
|
1038
1060
|
this.#releasePowerAssertion();
|
|
1061
|
+
this.#flushPendingBackgroundExchanges();
|
|
1039
1062
|
this.#flushPendingAgentEnd();
|
|
1040
1063
|
}
|
|
1041
1064
|
}
|
|
@@ -1043,6 +1066,7 @@ export class AgentSession {
|
|
|
1043
1066
|
#resetInFlight(): void {
|
|
1044
1067
|
this.#promptInFlightCount = 0;
|
|
1045
1068
|
this.#releasePowerAssertion();
|
|
1069
|
+
this.#flushPendingBackgroundExchanges();
|
|
1046
1070
|
this.#flushPendingAgentEnd();
|
|
1047
1071
|
}
|
|
1048
1072
|
|
|
@@ -1471,6 +1495,10 @@ export class AgentSession {
|
|
|
1471
1495
|
return tag;
|
|
1472
1496
|
}
|
|
1473
1497
|
|
|
1498
|
+
getAgentId(): string | undefined {
|
|
1499
|
+
return this.#agentId;
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1474
1502
|
getAsyncJobSnapshot(options?: { recentLimit?: number }): AsyncJobSnapshot | null {
|
|
1475
1503
|
const manager = AsyncJobManager.instance();
|
|
1476
1504
|
if (!manager) return null;
|
|
@@ -1481,6 +1509,7 @@ export class AgentSession {
|
|
|
1481
1509
|
status: job.status,
|
|
1482
1510
|
label: job.label,
|
|
1483
1511
|
startTime: job.startTime,
|
|
1512
|
+
metadata: job.metadata,
|
|
1484
1513
|
}));
|
|
1485
1514
|
const recent = manager.getRecentJobs(options?.recentLimit ?? 5, ownerFilter).map(job => ({
|
|
1486
1515
|
id: job.id,
|
|
@@ -1488,6 +1517,7 @@ export class AgentSession {
|
|
|
1488
1517
|
status: job.status,
|
|
1489
1518
|
label: job.label,
|
|
1490
1519
|
startTime: job.startTime,
|
|
1520
|
+
metadata: job.metadata,
|
|
1491
1521
|
}));
|
|
1492
1522
|
const delivery = manager.getDeliveryState(ownerFilter);
|
|
1493
1523
|
return { running, recent, delivery };
|
|
@@ -1630,10 +1660,11 @@ export class AgentSession {
|
|
|
1630
1660
|
event.type === "message_end" &&
|
|
1631
1661
|
event.message.role === "assistant" &&
|
|
1632
1662
|
event.message.stopReason === "aborted" &&
|
|
1633
|
-
this.#planCompactAbortPending
|
|
1663
|
+
(this.#planCompactAbortPending || this.#silentAbortPending)
|
|
1634
1664
|
) {
|
|
1635
1665
|
(event.message as AssistantMessage).errorMessage = SILENT_ABORT_MARKER;
|
|
1636
1666
|
this.#planCompactAbortPending = false;
|
|
1667
|
+
this.#silentAbortPending = false;
|
|
1637
1668
|
}
|
|
1638
1669
|
|
|
1639
1670
|
// Deobfuscate assistant message content for display emission — the LLM echoes back
|
|
@@ -1887,6 +1918,15 @@ export class AgentSession {
|
|
|
1887
1918
|
attempt: this.#retryAttempt,
|
|
1888
1919
|
});
|
|
1889
1920
|
this.#retryAttempt = 0;
|
|
1921
|
+
// Settle the retry gate here, colocated with the success event, rather
|
|
1922
|
+
// than relying on the generic #resolveRetry() at the end of the
|
|
1923
|
+
// agent_end branch. That tail resolver is bypassed by every early
|
|
1924
|
+
// return in agent_end (successful `yield`, handoff-abort skip-maintenance,
|
|
1925
|
+
// missing assistant message), so a retry that recovers on a yield turn
|
|
1926
|
+
// would otherwise leave #retryPromise unresolved — wedging
|
|
1927
|
+
// #waitForPostPromptRecovery and the session as permanently busy.
|
|
1928
|
+
// #resolveRetry() is idempotent, so the later tail call is a no-op.
|
|
1929
|
+
this.#resolveRetry();
|
|
1890
1930
|
}
|
|
1891
1931
|
}
|
|
1892
1932
|
|
|
@@ -1992,6 +2032,9 @@ export class AgentSession {
|
|
|
1992
2032
|
|
|
1993
2033
|
if (this.#assistantEndedWithSuccessfulYield(msg)) {
|
|
1994
2034
|
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
2035
|
+
if (msg.stopReason !== "error" && msg.stopReason !== "aborted" && (await this.#checkGoalCompletion(msg))) {
|
|
2036
|
+
return;
|
|
2037
|
+
}
|
|
1995
2038
|
return;
|
|
1996
2039
|
}
|
|
1997
2040
|
this.#lastSuccessfulYieldToolCallId = undefined;
|
|
@@ -2001,6 +2044,18 @@ export class AgentSession {
|
|
|
2001
2044
|
const didRetry = await this.#handleRetryableError(msg);
|
|
2002
2045
|
if (didRetry) return; // Retry was initiated, don't proceed to compaction
|
|
2003
2046
|
}
|
|
2047
|
+
if (this.#retryAttempt > 0) {
|
|
2048
|
+
// A prior retry ended on a non-retryable (terminal) message: emit
|
|
2049
|
+
// the terminal retry-end and reset so observers clear retry state.
|
|
2050
|
+
const attempt = this.#retryAttempt;
|
|
2051
|
+
this.#retryAttempt = 0;
|
|
2052
|
+
await this.#emitSessionEvent({
|
|
2053
|
+
type: "auto_retry_end",
|
|
2054
|
+
success: false,
|
|
2055
|
+
attempt,
|
|
2056
|
+
finalError: msg.errorMessage,
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2004
2059
|
this.#resolveRetry();
|
|
2005
2060
|
|
|
2006
2061
|
const compactionTask = this.#checkCompaction(msg);
|
|
@@ -2015,6 +2070,9 @@ export class AgentSession {
|
|
|
2015
2070
|
if (this.#enforceRewindBeforeYield()) {
|
|
2016
2071
|
return;
|
|
2017
2072
|
}
|
|
2073
|
+
if (await this.#checkGoalCompletion(msg)) {
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2018
2076
|
await this.#checkTodoCompletion();
|
|
2019
2077
|
}
|
|
2020
2078
|
}
|
|
@@ -2103,13 +2161,23 @@ export class AgentSession {
|
|
|
2103
2161
|
delayMs?: number;
|
|
2104
2162
|
generation?: number;
|
|
2105
2163
|
shouldContinue?: () => boolean;
|
|
2106
|
-
onSkip?: () => void;
|
|
2107
|
-
onError?: () => void;
|
|
2164
|
+
onSkip?: (reason: "generation_changed" | "aborted_signal" | "queue_drained") => void;
|
|
2165
|
+
onError?: (error: unknown) => void;
|
|
2108
2166
|
}): void {
|
|
2167
|
+
const scheduledGeneration = options?.generation;
|
|
2168
|
+
const signal = this.#postPromptTasksAbortController.signal;
|
|
2109
2169
|
this.#schedulePostPromptTask(
|
|
2110
2170
|
async () => {
|
|
2171
|
+
if (signal.aborted) {
|
|
2172
|
+
options?.onSkip?.("aborted_signal");
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
if (scheduledGeneration !== undefined && this.#promptGeneration !== scheduledGeneration) {
|
|
2176
|
+
options?.onSkip?.("generation_changed");
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2111
2179
|
if (options?.shouldContinue && !options.shouldContinue()) {
|
|
2112
|
-
options.onSkip?.();
|
|
2180
|
+
options.onSkip?.("queue_drained");
|
|
2113
2181
|
return;
|
|
2114
2182
|
}
|
|
2115
2183
|
try {
|
|
@@ -2119,17 +2187,45 @@ export class AgentSession {
|
|
|
2119
2187
|
logger.warn("agent.continue failed after scheduling", {
|
|
2120
2188
|
error: error instanceof Error ? error.message : String(error),
|
|
2121
2189
|
});
|
|
2122
|
-
options?.onError?.();
|
|
2190
|
+
options?.onError?.(error);
|
|
2123
2191
|
}
|
|
2124
2192
|
},
|
|
2125
|
-
{
|
|
2126
|
-
delayMs: options?.delayMs,
|
|
2127
|
-
generation: options?.generation,
|
|
2128
|
-
onSkip: options?.onSkip,
|
|
2129
|
-
},
|
|
2193
|
+
{ delayMs: options?.delayMs },
|
|
2130
2194
|
);
|
|
2131
2195
|
}
|
|
2132
2196
|
|
|
2197
|
+
#logCompactionContinuationSkipped(
|
|
2198
|
+
source: "auto_continue_prompt" | "queued_continue" | "overflow_retry",
|
|
2199
|
+
reason: string,
|
|
2200
|
+
): void {
|
|
2201
|
+
logger.warn("Auto-compaction continuation skipped", { source, reason });
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
#logCompactionContinuationError(
|
|
2205
|
+
source: "auto_continue_prompt" | "queued_continue" | "overflow_retry",
|
|
2206
|
+
error: unknown,
|
|
2207
|
+
): void {
|
|
2208
|
+
logger.warn("Auto-compaction continuation failed", {
|
|
2209
|
+
source,
|
|
2210
|
+
reason: error instanceof Error && error.name === "AgentBusyError" ? "queue_drained" : "not_resumable_tail",
|
|
2211
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2212
|
+
});
|
|
2213
|
+
}
|
|
2214
|
+
|
|
2215
|
+
#isResumableAgentTail(): boolean {
|
|
2216
|
+
const lastMsg = this.agent.state.messages.at(-1);
|
|
2217
|
+
return lastMsg !== undefined && lastMsg.role !== "assistant";
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
#stripOverflowFailedTurnForRetry(): void {
|
|
2221
|
+
const messages = this.agent.state.messages;
|
|
2222
|
+
const lastMsg = messages.at(-1);
|
|
2223
|
+
const contextWindow = this.model?.contextWindow ?? 0;
|
|
2224
|
+
if (lastMsg?.role === "assistant" && isContextOverflow(lastMsg as AssistantMessage, contextWindow)) {
|
|
2225
|
+
this.agent.replaceMessages(messages.slice(0, -1));
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2133
2229
|
#scheduleAutoContinuePrompt(generation: number): void {
|
|
2134
2230
|
const continuePrompt = async () => {
|
|
2135
2231
|
await this.#promptWithMessage(
|
|
@@ -2140,16 +2236,28 @@ export class AgentSession {
|
|
|
2140
2236
|
timestamp: Date.now(),
|
|
2141
2237
|
},
|
|
2142
2238
|
autoContinuePrompt,
|
|
2143
|
-
{ skipPostPromptRecoveryWait: true },
|
|
2239
|
+
{ skipPostPromptRecoveryWait: true, skipCompactionCheck: true },
|
|
2144
2240
|
);
|
|
2145
2241
|
};
|
|
2146
|
-
|
|
2147
|
-
|
|
2242
|
+
const scheduledGeneration = generation;
|
|
2243
|
+
const signal = this.#postPromptTasksAbortController.signal;
|
|
2244
|
+
this.#trackPostPromptTask(
|
|
2245
|
+
(async () => {
|
|
2148
2246
|
await Promise.resolve();
|
|
2149
|
-
if (signal.aborted)
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2247
|
+
if (signal.aborted) {
|
|
2248
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
if (this.#promptGeneration !== scheduledGeneration) {
|
|
2252
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "generation_changed");
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2255
|
+
try {
|
|
2256
|
+
await continuePrompt();
|
|
2257
|
+
} catch (error) {
|
|
2258
|
+
this.#logCompactionContinuationError("auto_continue_prompt", error);
|
|
2259
|
+
}
|
|
2260
|
+
})(),
|
|
2153
2261
|
);
|
|
2154
2262
|
}
|
|
2155
2263
|
|
|
@@ -2871,6 +2979,7 @@ export class AgentSession {
|
|
|
2871
2979
|
maxAttempts: event.maxAttempts,
|
|
2872
2980
|
delayMs: event.delayMs,
|
|
2873
2981
|
errorMessage: event.errorMessage,
|
|
2982
|
+
unbounded: event.unbounded,
|
|
2874
2983
|
});
|
|
2875
2984
|
} else if (event.type === "auto_retry_end") {
|
|
2876
2985
|
await this.#extensionRunner.emit({
|
|
@@ -3485,7 +3594,7 @@ export class AgentSession {
|
|
|
3485
3594
|
* prompts or tool execution can run.
|
|
3486
3595
|
*/
|
|
3487
3596
|
#wrapToolForDeepInterviewMutationGuard<T extends AgentTool>(tool: T): T {
|
|
3488
|
-
if (!["edit", "write", "ast_edit"].includes(tool.name)) return tool;
|
|
3597
|
+
if (!["edit", "write", "ast_edit", "bash"].includes(tool.name)) return tool;
|
|
3489
3598
|
return new Proxy(tool, {
|
|
3490
3599
|
get: (target, prop) => {
|
|
3491
3600
|
if (prop !== "execute") return Reflect.get(target, prop, target);
|
|
@@ -4291,12 +4400,17 @@ export class AgentSession {
|
|
|
4291
4400
|
// Canonical GJC workflow skills (deep-interview, ralplan, ultragoal, team)
|
|
4292
4401
|
// own their `.gjc/state/skill-active-state.json` row through the
|
|
4293
4402
|
// `gjc state handoff` and `gjc state clear` runtime verbs. The prompt
|
|
4294
|
-
// observer
|
|
4295
|
-
//
|
|
4296
|
-
//
|
|
4297
|
-
//
|
|
4298
|
-
//
|
|
4299
|
-
//
|
|
4403
|
+
// observer must not overwrite an existing row (that clobbered handoff
|
|
4404
|
+
// lineage `handoff_from`/`handoff_at` and desynced the HUD). But a fresh
|
|
4405
|
+
// `/skill:<name>` invocation has no row yet, so seed `.gjc/state`
|
|
4406
|
+
// idempotently here: `ensureWorkflowSkillActivationState` writes the
|
|
4407
|
+
// initial mode-state + active row only when the skill is not already
|
|
4408
|
+
// active, so the mutation guard and Stop hook engage immediately instead
|
|
4409
|
+
// of relying on the skill prompt to run its own state-init steps.
|
|
4410
|
+
if (active) {
|
|
4411
|
+
await ensureWorkflowSkillActivationState({ cwd: this.sessionManager.getCwd(), skill, sessionId });
|
|
4412
|
+
}
|
|
4413
|
+
// In-memory tracking keeps `getActiveSkillState` accurate for the chain guard.
|
|
4300
4414
|
this.#activeSkillState = active ? { skill, sessionId } : undefined;
|
|
4301
4415
|
}
|
|
4302
4416
|
|
|
@@ -4942,6 +5056,13 @@ export class AgentSession {
|
|
|
4942
5056
|
return this.#steeringMessages.length + this.#followUpMessages.length + this.#pendingNextTurnMessages.length;
|
|
4943
5057
|
}
|
|
4944
5058
|
|
|
5059
|
+
/** Whether the agent has queued steering messages that a `user_interrupt`
|
|
5060
|
+
* abort would resume into (steer-on-interrupt). Drives the Esc-on-steer UX:
|
|
5061
|
+
* the first Esc consumes the steer and auto-continues, a second Esc aborts. */
|
|
5062
|
+
get hasQueuedSteering(): boolean {
|
|
5063
|
+
return this.agent.hasQueuedSteering();
|
|
5064
|
+
}
|
|
5065
|
+
|
|
4945
5066
|
/** Get pending messages (read-only). Returns the public text-only view;
|
|
4946
5067
|
* internal `{text, tag?}` records are mapped to `.text` so callers
|
|
4947
5068
|
* (`updatePendingMessagesDisplay`, `restoreQueuedMessagesToEditor`) see
|
|
@@ -5027,7 +5148,28 @@ export class AgentSession {
|
|
|
5027
5148
|
/**
|
|
5028
5149
|
* Abort current operation and wait for agent to become idle.
|
|
5029
5150
|
*/
|
|
5030
|
-
async abort(options?: {
|
|
5151
|
+
async abort(options?: {
|
|
5152
|
+
goalReason?: "interrupted" | "internal";
|
|
5153
|
+
timeoutMs?: number;
|
|
5154
|
+
cause?:
|
|
5155
|
+
| "user_interrupt"
|
|
5156
|
+
| "new_session"
|
|
5157
|
+
| "session_switch"
|
|
5158
|
+
| "compaction"
|
|
5159
|
+
| "handoff"
|
|
5160
|
+
| "tool_abort"
|
|
5161
|
+
| "internal";
|
|
5162
|
+
/** Suppress the "Operation aborted" line on the resulting aborted message
|
|
5163
|
+
* by stamping `SILENT_ABORT_MARKER`. Used when Esc consumes a queued steer
|
|
5164
|
+
* and resumes via steer-on-interrupt, so the interrupt reads as a quiet
|
|
5165
|
+
* hand-off rather than a failure. */
|
|
5166
|
+
silent?: boolean;
|
|
5167
|
+
}): Promise<void> {
|
|
5168
|
+
if (options?.silent) {
|
|
5169
|
+
this.#silentAbortPending = true;
|
|
5170
|
+
} else {
|
|
5171
|
+
this.#silentAbortPending = false;
|
|
5172
|
+
}
|
|
5031
5173
|
this.abortRetry();
|
|
5032
5174
|
this.#promptGeneration++;
|
|
5033
5175
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
@@ -5067,6 +5209,10 @@ export class AgentSession {
|
|
|
5067
5209
|
// block runs, but nested prompt setup/finalizers may still be unwinding. Without this,
|
|
5068
5210
|
// a subsequent prompt() can incorrectly observe the session as busy after an abort.
|
|
5069
5211
|
this.#resetInFlight();
|
|
5212
|
+
// Safety net: clear the silent-abort flag if it was never consumed (the
|
|
5213
|
+
// abort produced no aborted assistant message_end to stamp). Prevents the
|
|
5214
|
+
// marker from leaking onto a later, unrelated abort.
|
|
5215
|
+
this.#silentAbortPending = false;
|
|
5070
5216
|
// Safety net: if the agent loop aborted without producing an assistant
|
|
5071
5217
|
// message (e.g. failed before the first stream), the in-flight yield was
|
|
5072
5218
|
// never resolved or rejected by the normal message_end path. Reject it now
|
|
@@ -5074,6 +5220,18 @@ export class AgentSession {
|
|
|
5074
5220
|
if (this.#toolChoiceQueue.hasInFlight) {
|
|
5075
5221
|
this.#toolChoiceQueue.reject("aborted");
|
|
5076
5222
|
}
|
|
5223
|
+
|
|
5224
|
+
// Steer-on-interrupt: after a genuine user interrupt, resume with any
|
|
5225
|
+
// queued steering instead of going idle. Lifecycle/teardown causes
|
|
5226
|
+
// (default "internal") suppress this; new-session/handoff additionally
|
|
5227
|
+
// clear the steering queue, and compaction resumes via its own path.
|
|
5228
|
+
if ((options?.cause ?? "internal") === "user_interrupt" && this.agent.hasQueuedSteering()) {
|
|
5229
|
+
this.#scheduleAgentContinue({
|
|
5230
|
+
delayMs: 1,
|
|
5231
|
+
generation: this.#promptGeneration,
|
|
5232
|
+
shouldContinue: () => this.agent.hasQueuedSteering(),
|
|
5233
|
+
});
|
|
5234
|
+
}
|
|
5077
5235
|
}
|
|
5078
5236
|
|
|
5079
5237
|
/**
|
|
@@ -5129,6 +5287,9 @@ export class AgentSession {
|
|
|
5129
5287
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
5130
5288
|
|
|
5131
5289
|
this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
|
|
5290
|
+
if (this.model) {
|
|
5291
|
+
this.sessionManager.appendModelChange(`${this.model.provider}/${this.model.id}`);
|
|
5292
|
+
}
|
|
5132
5293
|
this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
|
|
5133
5294
|
if (nextDiscoverySessionToolNames) {
|
|
5134
5295
|
await this.#applyActiveToolsByName(nextDiscoverySessionToolNames, { persistMCPSelection: false });
|
|
@@ -5920,6 +6081,11 @@ export class AgentSession {
|
|
|
5920
6081
|
this.#pendingNextTurnMessages = [];
|
|
5921
6082
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
5922
6083
|
this.#todoReminderCount = 0;
|
|
6084
|
+
if (model) {
|
|
6085
|
+
this.sessionManager.appendModelChange(`${model.provider}/${model.id}`);
|
|
6086
|
+
}
|
|
6087
|
+
this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
|
|
6088
|
+
this.sessionManager.appendServiceTierChange(this.serviceTier ?? null);
|
|
5923
6089
|
|
|
5924
6090
|
// Inject the handoff document as a custom message
|
|
5925
6091
|
const handoffContent = createHandoffContext(handoffText);
|
|
@@ -5931,7 +6097,14 @@ export class AgentSession {
|
|
|
5931
6097
|
if (artifactsDir) {
|
|
5932
6098
|
const handoffFilePath = path.join(artifactsDir, createHandoffFileName());
|
|
5933
6099
|
try {
|
|
5934
|
-
|
|
6100
|
+
if (isUnderProjectGjc(this.sessionManager.getCwd(), handoffFilePath)) {
|
|
6101
|
+
await writeArtifact(handoffFilePath, `${handoffText}\n`, {
|
|
6102
|
+
cwd: this.sessionManager.getCwd(),
|
|
6103
|
+
audit: { category: "artifact", verb: "write", owner: "gjc-runtime" },
|
|
6104
|
+
});
|
|
6105
|
+
} else {
|
|
6106
|
+
await Bun.write(handoffFilePath, `${handoffText}\n`);
|
|
6107
|
+
}
|
|
5935
6108
|
savedPath = handoffFilePath;
|
|
5936
6109
|
} catch (error) {
|
|
5937
6110
|
logger.warn("Failed to save handoff document to disk", {
|
|
@@ -6200,6 +6373,39 @@ export class AgentSession {
|
|
|
6200
6373
|
toolChoice: todoWriteToolChoice,
|
|
6201
6374
|
};
|
|
6202
6375
|
}
|
|
6376
|
+
|
|
6377
|
+
async #checkGoalCompletion(assistantMessage: AssistantMessage): Promise<boolean> {
|
|
6378
|
+
const state = this.getGoalModeState();
|
|
6379
|
+
if (!state?.enabled || state.goal.status !== "active") {
|
|
6380
|
+
this.#lastGoalReminderAssistantTimestamp = undefined;
|
|
6381
|
+
return false;
|
|
6382
|
+
}
|
|
6383
|
+
if (this.#lastGoalReminderAssistantTimestamp === assistantMessage.timestamp) {
|
|
6384
|
+
return false;
|
|
6385
|
+
}
|
|
6386
|
+
this.#lastGoalReminderAssistantTimestamp = assistantMessage.timestamp;
|
|
6387
|
+
|
|
6388
|
+
const continuationPrompt = this.#goalRuntime.buildContinuationPrompt();
|
|
6389
|
+
if (!continuationPrompt) return false;
|
|
6390
|
+
const reminder = [
|
|
6391
|
+
"<system-reminder>",
|
|
6392
|
+
"You stopped while a goal is still active and uncleared.",
|
|
6393
|
+
"Continue working on the active goal until it is verified complete, paused, or dropped.",
|
|
6394
|
+
"",
|
|
6395
|
+
continuationPrompt,
|
|
6396
|
+
"</system-reminder>",
|
|
6397
|
+
].join("\n");
|
|
6398
|
+
|
|
6399
|
+
logger.debug("Goal completion: sending active-goal reminder", { goalId: state.goal.id });
|
|
6400
|
+
this.agent.appendMessage({
|
|
6401
|
+
role: "developer",
|
|
6402
|
+
content: [{ type: "text", text: reminder }],
|
|
6403
|
+
attribution: "agent",
|
|
6404
|
+
timestamp: Date.now(),
|
|
6405
|
+
});
|
|
6406
|
+
this.#scheduleAgentContinue({ generation: this.#promptGeneration });
|
|
6407
|
+
return true;
|
|
6408
|
+
}
|
|
6203
6409
|
/**
|
|
6204
6410
|
* Check if agent stopped with incomplete todos and prompt to continue.
|
|
6205
6411
|
*/
|
|
@@ -6843,12 +7049,34 @@ export class AgentSession {
|
|
|
6843
7049
|
willRetry: false,
|
|
6844
7050
|
skipped: true,
|
|
6845
7051
|
});
|
|
6846
|
-
if (
|
|
7052
|
+
if (willRetry) {
|
|
7053
|
+
this.#stripOverflowFailedTurnForRetry();
|
|
7054
|
+
if (this.#isResumableAgentTail()) {
|
|
7055
|
+
this.#scheduleAgentContinue({
|
|
7056
|
+
delayMs: 100,
|
|
7057
|
+
generation,
|
|
7058
|
+
onSkip: skipReason => this.#logCompactionContinuationSkipped("overflow_retry", skipReason),
|
|
7059
|
+
onError: error => this.#logCompactionContinuationError("overflow_retry", error),
|
|
7060
|
+
});
|
|
7061
|
+
} else {
|
|
7062
|
+
const tail = this.agent.state.messages.at(-1) as AssistantMessage | undefined;
|
|
7063
|
+
logger.warn("Auto-compaction continuation skipped", {
|
|
7064
|
+
source: "overflow_retry",
|
|
7065
|
+
reason: "not_resumable_tail",
|
|
7066
|
+
role: tail?.role,
|
|
7067
|
+
stopReason: tail?.stopReason,
|
|
7068
|
+
});
|
|
7069
|
+
}
|
|
7070
|
+
} else if (reason !== "idle" && this.agent.hasQueuedMessages()) {
|
|
6847
7071
|
this.#scheduleAgentContinue({
|
|
6848
7072
|
delayMs: 100,
|
|
6849
7073
|
generation,
|
|
6850
7074
|
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
7075
|
+
onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
|
|
7076
|
+
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
6851
7077
|
});
|
|
7078
|
+
} else if (reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
7079
|
+
this.#scheduleAutoContinuePrompt(generation);
|
|
6852
7080
|
}
|
|
6853
7081
|
return;
|
|
6854
7082
|
}
|
|
@@ -7050,26 +7278,36 @@ export class AgentSession {
|
|
|
7050
7278
|
};
|
|
7051
7279
|
await this.#emitSessionEvent({ type: "auto_compaction_end", action, result, aborted: false, willRetry });
|
|
7052
7280
|
|
|
7053
|
-
if (!willRetry && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
7054
|
-
this.#scheduleAutoContinuePrompt(generation);
|
|
7055
|
-
}
|
|
7056
|
-
|
|
7057
7281
|
if (willRetry) {
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7282
|
+
this.#stripOverflowFailedTurnForRetry();
|
|
7283
|
+
if (!this.#isResumableAgentTail()) {
|
|
7284
|
+
const tail = this.agent.state.messages.at(-1) as AssistantMessage | undefined;
|
|
7285
|
+
logger.warn("Auto-compaction continuation skipped", {
|
|
7286
|
+
source: "overflow_retry",
|
|
7287
|
+
reason: "not_resumable_tail",
|
|
7288
|
+
role: tail?.role,
|
|
7289
|
+
stopReason: tail?.stopReason,
|
|
7290
|
+
});
|
|
7291
|
+
} else {
|
|
7292
|
+
this.#scheduleAgentContinue({
|
|
7293
|
+
delayMs: 100,
|
|
7294
|
+
generation,
|
|
7295
|
+
onSkip: reason => this.#logCompactionContinuationSkipped("overflow_retry", reason),
|
|
7296
|
+
onError: error => this.#logCompactionContinuationError("overflow_retry", error),
|
|
7297
|
+
});
|
|
7062
7298
|
}
|
|
7063
|
-
|
|
7064
|
-
this.#scheduleAgentContinue({ delayMs: 100, generation });
|
|
7065
|
-
} else if (this.agent.hasQueuedMessages()) {
|
|
7299
|
+
} else if (reason !== "idle" && this.agent.hasQueuedMessages()) {
|
|
7066
7300
|
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
|
7067
7301
|
// Kick the loop so queued messages are actually delivered.
|
|
7068
7302
|
this.#scheduleAgentContinue({
|
|
7069
7303
|
delayMs: 100,
|
|
7070
7304
|
generation,
|
|
7071
7305
|
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
7306
|
+
onSkip: reason => this.#logCompactionContinuationSkipped("queued_continue", reason),
|
|
7307
|
+
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
7072
7308
|
});
|
|
7309
|
+
} else if (reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
7310
|
+
this.#scheduleAutoContinuePrompt(generation);
|
|
7073
7311
|
}
|
|
7074
7312
|
} catch (error) {
|
|
7075
7313
|
if (autoCompactionSignal.aborted) {
|
|
@@ -7121,19 +7359,14 @@ export class AgentSession {
|
|
|
7121
7359
|
// =========================================================================
|
|
7122
7360
|
|
|
7123
7361
|
/**
|
|
7124
|
-
*
|
|
7125
|
-
*
|
|
7126
|
-
*
|
|
7362
|
+
* Whether an error should be retried. Uses the ordered classifier:
|
|
7363
|
+
* context-overflow routes to compaction; clearly-terminal coded errors
|
|
7364
|
+
* (auth/400/not-found) surface immediately; usage-limit, transient, and
|
|
7365
|
+
* unknown/no-code errors are retryable.
|
|
7127
7366
|
*/
|
|
7128
7367
|
#isRetryableError(message: AssistantMessage): boolean {
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
// Context overflow is handled by compaction, not retry
|
|
7132
|
-
const contextWindow = this.model?.contextWindow ?? 0;
|
|
7133
|
-
if (isContextOverflow(message, contextWindow)) return false;
|
|
7134
|
-
|
|
7135
|
-
const err = message.errorMessage;
|
|
7136
|
-
return this.#isTransientErrorMessage(err) || isUsageLimitError(err);
|
|
7368
|
+
const classification = this.#classifyErrorForRetry(message);
|
|
7369
|
+
return classification === "usage_limit" || classification === "transient" || classification === "unknown";
|
|
7137
7370
|
}
|
|
7138
7371
|
|
|
7139
7372
|
#isTransientErrorMessage(errorMessage: string): boolean {
|
|
@@ -7159,6 +7392,63 @@ export class AgentSession {
|
|
|
7159
7392
|
);
|
|
7160
7393
|
}
|
|
7161
7394
|
|
|
7395
|
+
#isTerminalErrorMessage(errorMessage: string): boolean {
|
|
7396
|
+
// Errors that will never succeed on retry (auth/permission, malformed
|
|
7397
|
+
// request, unknown/unsupported model). These surface immediately rather
|
|
7398
|
+
// than retry forever.
|
|
7399
|
+
return /unauthorized|forbidden|authentication_error|permission_error|permission denied|invalid api key|invalid_request_error|invalid request|bad request|bad_request|validation_error|unprocessable|payload too large|payment required|insufficient_quota|insufficient credits|missing required (parameter|field)|invalid schema|invalid tool_choice|unsupported (parameter|value|model)|model_not_found|no such model|unknown model|does not (exist|support)|request was aborted|request aborted|the user aborted/i.test(
|
|
7400
|
+
errorMessage,
|
|
7401
|
+
);
|
|
7402
|
+
}
|
|
7403
|
+
|
|
7404
|
+
#extractExplicitHttpStatusFromErrorMessage(errorMessage: string): number | undefined {
|
|
7405
|
+
// Parse only explicit HTTP/status wording. Do not treat generic
|
|
7406
|
+
// `error: 400` as an HTTP status because rate-limit copy can say
|
|
7407
|
+
// "rate limit error: 400 requests per minute".
|
|
7408
|
+
const match = /\b(?:http(?:\s+status)?|status(?:[\s_-]+code)?)(?:\s+|[:=]\s*)(\d{3})\b/i.exec(errorMessage);
|
|
7409
|
+
if (!match) return undefined;
|
|
7410
|
+
const status = Number(match[1]);
|
|
7411
|
+
return Number.isFinite(status) && status >= 100 && status <= 599 ? status : undefined;
|
|
7412
|
+
}
|
|
7413
|
+
|
|
7414
|
+
/**
|
|
7415
|
+
* Ordered retry classification: overflow (compaction) -> terminal (surface)
|
|
7416
|
+
* -> usage_limit (rotation) -> transient (retry) -> unknown (retry).
|
|
7417
|
+
*/
|
|
7418
|
+
#classifyErrorForRetry(
|
|
7419
|
+
message: AssistantMessage,
|
|
7420
|
+
): "none" | "overflow" | "terminal" | "usage_limit" | "transient" | "unknown" {
|
|
7421
|
+
if (message.stopReason !== "error" || !message.errorMessage) return "none";
|
|
7422
|
+
const contextWindow = this.model?.contextWindow ?? 0;
|
|
7423
|
+
if (isContextOverflow(message, contextWindow)) return "overflow";
|
|
7424
|
+
const err = message.errorMessage;
|
|
7425
|
+
// Stream-envelope errors are only transient in the pre-message_start
|
|
7426
|
+
// variant; any other envelope failure is structural and must surface.
|
|
7427
|
+
if (/anthropic stream envelope error:/i.test(err)) {
|
|
7428
|
+
return this.#isTransientEnvelopeErrorMessage(err) ? "transient" : "terminal";
|
|
7429
|
+
}
|
|
7430
|
+
const explicitStatus = this.#extractExplicitHttpStatusFromErrorMessage(err);
|
|
7431
|
+
const structuredStatus = message.errorStatus;
|
|
7432
|
+
const terminalStatus = explicitStatus ?? structuredStatus;
|
|
7433
|
+
const isTerminalHttp4xx =
|
|
7434
|
+
terminalStatus !== undefined &&
|
|
7435
|
+
terminalStatus >= 400 &&
|
|
7436
|
+
terminalStatus < 500 &&
|
|
7437
|
+
terminalStatus !== 408 &&
|
|
7438
|
+
terminalStatus !== 425 &&
|
|
7439
|
+
terminalStatus !== 429;
|
|
7440
|
+
if (this.#isTerminalErrorMessage(err)) return "terminal";
|
|
7441
|
+
if (isUsageLimitError(err)) return "usage_limit";
|
|
7442
|
+
// Explicit HTTP/status wording is authoritative. Structured provider status
|
|
7443
|
+
// is also authoritative except for rate-limit copy where providers may have
|
|
7444
|
+
// parsed an incidental quota number such as "400 requests per minute".
|
|
7445
|
+
if (isTerminalHttp4xx && (explicitStatus !== undefined || !/rate.?limit|too many requests/i.test(err))) {
|
|
7446
|
+
return "terminal";
|
|
7447
|
+
}
|
|
7448
|
+
if (this.#isTransientErrorMessage(err)) return "transient";
|
|
7449
|
+
return "unknown";
|
|
7450
|
+
}
|
|
7451
|
+
|
|
7162
7452
|
#getRetryFallbackChains(): RetryFallbackChains {
|
|
7163
7453
|
const configuredChains = this.settings.get("retry.fallbackChains");
|
|
7164
7454
|
if (!configuredChains || typeof configuredChains !== "object") return {};
|
|
@@ -7428,6 +7718,8 @@ export class AgentSession {
|
|
|
7428
7718
|
async #handleRetryableError(message: AssistantMessage): Promise<boolean> {
|
|
7429
7719
|
const retrySettings = this.settings.getGroup("retry");
|
|
7430
7720
|
if (!retrySettings.enabled) return false;
|
|
7721
|
+
const retryClassification = this.#classifyErrorForRetry(message);
|
|
7722
|
+
const unboundedClass = retryClassification === "transient" || retryClassification === "unknown";
|
|
7431
7723
|
|
|
7432
7724
|
const generation = this.#promptGeneration;
|
|
7433
7725
|
this.#retryAttempt++;
|
|
@@ -7440,7 +7732,7 @@ export class AgentSession {
|
|
|
7440
7732
|
this.#retryResolve = resolve;
|
|
7441
7733
|
}
|
|
7442
7734
|
|
|
7443
|
-
if (this.#retryAttempt > retrySettings.maxRetries) {
|
|
7735
|
+
if (!unboundedClass && this.#retryAttempt > retrySettings.maxRetries) {
|
|
7444
7736
|
// Max retries exceeded, emit final failure and reset
|
|
7445
7737
|
await this.#emitSessionEvent({
|
|
7446
7738
|
type: "auto_retry_end",
|
|
@@ -7497,7 +7789,16 @@ export class AgentSession {
|
|
|
7497
7789
|
// assistant error message is preserved in agent state so the caller
|
|
7498
7790
|
// can act on it.
|
|
7499
7791
|
const maxDelayMs = retrySettings.maxDelayMs;
|
|
7500
|
-
if (
|
|
7792
|
+
if (unboundedClass && !switchedCredential && !switchedModel) {
|
|
7793
|
+
// Retry forever: honor a provider-supplied wait, otherwise cap the
|
|
7794
|
+
// exponential backoff at the ceiling instead of giving up.
|
|
7795
|
+
if (parsedRetryAfterMs !== undefined) {
|
|
7796
|
+
delayMs = Math.max(delayMs, parsedRetryAfterMs);
|
|
7797
|
+
} else if (maxDelayMs > 0) {
|
|
7798
|
+
delayMs = Math.min(delayMs, maxDelayMs);
|
|
7799
|
+
}
|
|
7800
|
+
}
|
|
7801
|
+
if (!unboundedClass && maxDelayMs > 0 && delayMs > maxDelayMs && !switchedCredential && !switchedModel) {
|
|
7501
7802
|
const attempt = this.#retryAttempt;
|
|
7502
7803
|
this.#retryAttempt = 0;
|
|
7503
7804
|
await this.#emitSessionEvent({
|
|
@@ -7510,12 +7811,22 @@ export class AgentSession {
|
|
|
7510
7811
|
return false;
|
|
7511
7812
|
}
|
|
7512
7813
|
|
|
7814
|
+
// Create and install the backoff abort controller BEFORE emitting
|
|
7815
|
+
// auto_retry_start, so a synchronous retryNow()/abortRetry() invoked from
|
|
7816
|
+
// an event subscriber (e.g. the TUI Esc handler) is not lost in the gap
|
|
7817
|
+
// between the event and the controller assignment.
|
|
7818
|
+
const retryAbortController = new AbortController();
|
|
7819
|
+
this.#retryAbortController?.abort();
|
|
7820
|
+
this.#retryAbortController = retryAbortController;
|
|
7821
|
+
this.#retryNowRequested = false;
|
|
7822
|
+
|
|
7513
7823
|
await this.#emitSessionEvent({
|
|
7514
7824
|
type: "auto_retry_start",
|
|
7515
7825
|
attempt: this.#retryAttempt,
|
|
7516
7826
|
maxAttempts: retrySettings.maxRetries,
|
|
7517
7827
|
delayMs,
|
|
7518
7828
|
errorMessage,
|
|
7829
|
+
unbounded: unboundedClass,
|
|
7519
7830
|
});
|
|
7520
7831
|
|
|
7521
7832
|
// Remove error message from agent state (keep in session for history)
|
|
@@ -7525,34 +7836,49 @@ export class AgentSession {
|
|
|
7525
7836
|
}
|
|
7526
7837
|
|
|
7527
7838
|
// Wait with exponential backoff (abortable).
|
|
7528
|
-
const retryAbortController = new AbortController();
|
|
7529
|
-
this.#retryAbortController?.abort();
|
|
7530
|
-
this.#retryAbortController = retryAbortController;
|
|
7531
7839
|
try {
|
|
7532
7840
|
await scheduler.wait(delayMs, { signal: retryAbortController.signal });
|
|
7533
7841
|
} catch {
|
|
7534
7842
|
if (this.#retryAbortController !== retryAbortController) {
|
|
7535
7843
|
return false;
|
|
7536
7844
|
}
|
|
7537
|
-
// Aborted during sleep - emit end event so UI can clean up
|
|
7538
|
-
const attempt = this.#retryAttempt;
|
|
7539
|
-
this.#retryAttempt = 0;
|
|
7540
7845
|
this.#retryAbortController = undefined;
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7846
|
+
if (this.#retryNowRequested) {
|
|
7847
|
+
// Retry-now: skip the remaining backoff and fall through to
|
|
7848
|
+
// re-attempt immediately (keeps the retry session alive).
|
|
7849
|
+
this.#retryNowRequested = false;
|
|
7850
|
+
} else {
|
|
7851
|
+
// Aborted during sleep (cancel) - emit end event so UI can clean up
|
|
7852
|
+
const attempt = this.#retryAttempt;
|
|
7853
|
+
this.#retryAttempt = 0;
|
|
7854
|
+
await this.#emitSessionEvent({
|
|
7855
|
+
type: "auto_retry_end",
|
|
7856
|
+
success: false,
|
|
7857
|
+
attempt,
|
|
7858
|
+
finalError: "Retry cancelled",
|
|
7859
|
+
});
|
|
7860
|
+
this.#resolveRetry();
|
|
7861
|
+
return false;
|
|
7862
|
+
}
|
|
7549
7863
|
}
|
|
7550
7864
|
if (this.#retryAbortController === retryAbortController) {
|
|
7551
7865
|
this.#retryAbortController = undefined;
|
|
7552
7866
|
}
|
|
7553
7867
|
|
|
7554
7868
|
// Retry via continue() outside the agent_end event callback chain.
|
|
7555
|
-
|
|
7869
|
+
// If the scheduled continue cannot run — it throws (e.g. AgentBusyError from a
|
|
7870
|
+
// concurrent turn, or "Cannot continue ...") or is skipped because a newer
|
|
7871
|
+
// generation took over — the agent_end that normally resolves #retryPromise
|
|
7872
|
+
// never arrives. Finalize the retry in that case so #waitForPostPromptRecovery
|
|
7873
|
+
// (and the in-flight prompt holding it open) cannot wedge the session as
|
|
7874
|
+
// permanently busy, which would turn every later prompt() into a
|
|
7875
|
+
// non-recoverable AgentBusyError loop.
|
|
7876
|
+
this.#scheduleAgentContinue({
|
|
7877
|
+
delayMs: 1,
|
|
7878
|
+
generation,
|
|
7879
|
+
onError: () => this.#failRetryRecovery("Retry continuation failed to start"),
|
|
7880
|
+
onSkip: () => this.#failRetryRecovery("Retry continuation was superseded"),
|
|
7881
|
+
});
|
|
7556
7882
|
|
|
7557
7883
|
return true;
|
|
7558
7884
|
}
|
|
@@ -7561,8 +7887,41 @@ export class AgentSession {
|
|
|
7561
7887
|
* Cancel in-progress retry.
|
|
7562
7888
|
*/
|
|
7563
7889
|
abortRetry(): void {
|
|
7890
|
+
this.#retryNowRequested = false;
|
|
7564
7891
|
this.#retryAbortController?.abort();
|
|
7565
|
-
// Note:
|
|
7892
|
+
// Note: #retryAttempt is reset in the catch block of #handleRetryableError
|
|
7893
|
+
this.#resolveRetry();
|
|
7894
|
+
}
|
|
7895
|
+
|
|
7896
|
+
/**
|
|
7897
|
+
* Skip the current retry backoff and re-attempt immediately. Distinct from
|
|
7898
|
+
* abortRetry(), which cancels the retry and returns to idle. No-op when no
|
|
7899
|
+
* retry backoff is active.
|
|
7900
|
+
*/
|
|
7901
|
+
retryNow(): void {
|
|
7902
|
+
if (!this.#retryAbortController) return;
|
|
7903
|
+
this.#retryNowRequested = true;
|
|
7904
|
+
this.#retryAbortController.abort();
|
|
7905
|
+
}
|
|
7906
|
+
|
|
7907
|
+
/**
|
|
7908
|
+
* Finalize a pending auto-retry that can no longer reach a resolving agent_end
|
|
7909
|
+
* (the scheduled continue threw or was superseded). Without this, #retryPromise
|
|
7910
|
+
* stays unresolved, #waitForPostPromptRecovery never returns, the owning
|
|
7911
|
+
* prompt's in-flight count is never released, and the session reports
|
|
7912
|
+
* `isStreaming === true` forever — turning every later prompt() into a
|
|
7913
|
+
* non-recoverable AgentBusyError. No-op once the retry has already settled.
|
|
7914
|
+
*/
|
|
7915
|
+
#failRetryRecovery(reason: string): void {
|
|
7916
|
+
if (!this.#retryPromise) return;
|
|
7917
|
+
const attempt = this.#retryAttempt;
|
|
7918
|
+
this.#retryAttempt = 0;
|
|
7919
|
+
void this.#emitSessionEvent({
|
|
7920
|
+
type: "auto_retry_end",
|
|
7921
|
+
success: false,
|
|
7922
|
+
attempt,
|
|
7923
|
+
finalError: reason,
|
|
7924
|
+
});
|
|
7566
7925
|
this.#resolveRetry();
|
|
7567
7926
|
}
|
|
7568
7927
|
|
|
@@ -8190,13 +8549,17 @@ export class AgentSession {
|
|
|
8190
8549
|
return;
|
|
8191
8550
|
}
|
|
8192
8551
|
if (this.isStreaming) {
|
|
8193
|
-
|
|
8552
|
+
// Re-poll while streaming, but do not let this housekeeping timer
|
|
8553
|
+
// keep the event loop alive on its own (CPU-7).
|
|
8554
|
+
const pollTimer = setTimeout(attempt, 50);
|
|
8555
|
+
pollTimer.unref?.();
|
|
8194
8556
|
return;
|
|
8195
8557
|
}
|
|
8196
8558
|
this.#scheduledBackgroundExchangeFlush = false;
|
|
8197
8559
|
this.#flushPendingBackgroundExchanges();
|
|
8198
8560
|
};
|
|
8199
|
-
setTimeout(attempt, 0);
|
|
8561
|
+
const kickoff = setTimeout(attempt, 0);
|
|
8562
|
+
kickoff.unref?.();
|
|
8200
8563
|
}
|
|
8201
8564
|
|
|
8202
8565
|
#flushPendingBackgroundExchanges(): void {
|
|
@@ -8279,6 +8642,8 @@ export class AgentSession {
|
|
|
8279
8642
|
const previousFallbackSelectedMCPToolNames = previousSessionFile
|
|
8280
8643
|
? this.#getSessionDefaultSelectedMCPToolNames(previousSessionFile)
|
|
8281
8644
|
: undefined;
|
|
8645
|
+
const previousAgentSteeringQueue = this.agent.snapshotSteering();
|
|
8646
|
+
const previousAgentFollowUpQueue = this.agent.snapshotFollowUp();
|
|
8282
8647
|
|
|
8283
8648
|
this.#steeringMessages = [];
|
|
8284
8649
|
this.#followUpMessages = [];
|
|
@@ -8297,6 +8662,12 @@ export class AgentSession {
|
|
|
8297
8662
|
const fallbackSelectedMCPToolNames = this.#getSessionDefaultSelectedMCPToolNames(sessionPath);
|
|
8298
8663
|
await this.#restoreMCPSelectionsForSessionContext(sessionContext, { fallbackSelectedMCPToolNames });
|
|
8299
8664
|
|
|
8665
|
+
// The target session is loaded and MCP selections are restored: the
|
|
8666
|
+
// switch is committed far enough to discard pre-switch delivery queues.
|
|
8667
|
+
// Clear before session_switch hooks, so messages enqueued by hooks belong
|
|
8668
|
+
// to the new session and remain deliverable.
|
|
8669
|
+
this.agent.clearAllQueues();
|
|
8670
|
+
|
|
8300
8671
|
// Emit session_switch event to hooks
|
|
8301
8672
|
if (this.#extensionRunner) {
|
|
8302
8673
|
await this.#extensionRunner.emit({
|
|
@@ -8391,6 +8762,9 @@ export class AgentSession {
|
|
|
8391
8762
|
this.#followUpMessages = previousFollowUpMessages;
|
|
8392
8763
|
this.#pendingNextTurnMessages = previousPendingNextTurnMessages;
|
|
8393
8764
|
this.#scheduledHiddenNextTurnGeneration = previousScheduledHiddenNextTurnGeneration;
|
|
8765
|
+
this.agent.clearAllQueues();
|
|
8766
|
+
this.agent.restoreSteering(previousAgentSteeringQueue);
|
|
8767
|
+
this.agent.restoreFollowUp(previousAgentFollowUpQueue);
|
|
8394
8768
|
if (previousModel) {
|
|
8395
8769
|
this.agent.setModel(previousModel);
|
|
8396
8770
|
}
|