@gajae-code/coding-agent 0.2.4 → 0.3.0
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 +27 -0
- package/README.md +1 -1
- package/dist/types/async/job-manager.d.ts +145 -2
- package/dist/types/commands/harness.d.ts +37 -0
- package/dist/types/config/settings-schema.d.ts +13 -3
- package/dist/types/config/settings.d.ts +3 -1
- package/dist/types/deep-interview/render-middleware.d.ts +5 -0
- package/dist/types/discovery/helpers.d.ts +1 -0
- package/dist/types/exec/bash-executor.d.ts +8 -1
- 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/restricted-role-agent-bash.d.ts +2 -0
- package/dist/types/gjc-runtime/state-graph.d.ts +4 -0
- package/dist/types/gjc-runtime/state-migrations.d.ts +24 -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-validation.d.ts +6 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +137 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +81 -7
- 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 +30 -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 +2 -29
- package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
- package/dist/types/modes/components/hook-selector.d.ts +1 -0
- package/dist/types/modes/components/skill-hud/render.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/theme/defaults/index.d.ts +45 -9477
- package/dist/types/modes/theme/theme.d.ts +1 -5
- 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 +8 -0
- package/dist/types/session/streaming-output.d.ts +11 -0
- package/dist/types/skill-state/active-state.d.ts +3 -0
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +1 -1
- package/dist/types/skill-state/workflow-state-contract.d.ts +24 -0
- package/dist/types/task/executor.d.ts +3 -0
- package/dist/types/task/types.d.ts +56 -3
- package/dist/types/tools/bash-allowed-prefixes.d.ts +5 -0
- package/dist/types/tools/bash.d.ts +24 -0
- package/dist/types/tools/cron.d.ts +110 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/monitor.d.ts +54 -0
- package/dist/types/tools/subagent.d.ts +11 -1
- package/dist/types/web/search/index.d.ts +1 -0
- package/dist/types/web/search/provider.d.ts +11 -4
- package/dist/types/web/search/providers/duckduckgo.d.ts +57 -0
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +7 -7
- package/src/async/job-manager.ts +522 -6
- package/src/cli/agents-cli.ts +3 -0
- 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/harness.ts +592 -0
- package/src/commands/team.ts +36 -39
- package/src/config/settings-schema.ts +15 -2
- package/src/config/settings.ts +49 -7
- package/src/deep-interview/render-middleware.ts +366 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +9 -2
- package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
- package/src/defaults/gjc/skills/team/SKILL.md +47 -21
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +78 -11
- package/src/discovery/helpers.ts +5 -0
- package/src/eval/js/shared/rewrite-imports.ts +1 -2
- package/src/exec/bash-executor.ts +20 -9
- 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/deep-interview-runtime.ts +40 -21
- package/src/gjc-runtime/goal-mode-request.ts +11 -3
- package/src/gjc-runtime/ralplan-runtime.ts +27 -10
- package/src/gjc-runtime/restricted-role-agent-bash.ts +5 -0
- package/src/gjc-runtime/state-graph.ts +86 -0
- package/src/gjc-runtime/state-migrations.ts +132 -0
- package/src/gjc-runtime/state-renderer.ts +345 -0
- package/src/gjc-runtime/state-runtime.ts +733 -21
- package/src/gjc-runtime/state-validation.ts +49 -0
- package/src/gjc-runtime/state-writer.ts +718 -0
- package/src/gjc-runtime/team-runtime.ts +1083 -89
- package/src/gjc-runtime/ultragoal-runtime.ts +348 -19
- package/src/gjc-runtime/workflow-manifest.generated.json +1497 -0
- package/src/gjc-runtime/workflow-manifest.ts +425 -0
- package/src/harness-control-plane/classifier.ts +128 -0
- package/src/harness-control-plane/control-endpoint.ts +137 -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 +553 -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 +97 -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 +25 -42
- package/src/internal-urls/docs-index.generated.ts +6 -4
- package/src/lsp/render.ts +1 -1
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/acp/acp-client-bridge.ts +1 -1
- package/src/modes/components/agent-dashboard.ts +1 -1
- package/src/modes/components/assistant-message.ts +5 -1
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/hook-selector.ts +72 -2
- package/src/modes/components/skill-hud/render.ts +7 -2
- package/src/modes/controllers/event-controller.ts +71 -6
- package/src/modes/controllers/extension-ui-controller.ts +6 -0
- package/src/modes/controllers/input-controller.ts +19 -3
- package/src/modes/controllers/selector-controller.ts +3 -2
- package/src/modes/interactive-mode.ts +21 -2
- package/src/modes/theme/defaults/index.ts +0 -196
- package/src/modes/theme/theme.ts +35 -35
- package/src/modes/types.ts +2 -0
- package/src/prompts/agents/architect.md +5 -1
- package/src/prompts/agents/critic.md +5 -1
- package/src/prompts/agents/executor.md +13 -0
- package/src/prompts/agents/frontmatter.md +1 -0
- package/src/prompts/agents/planner.md +5 -1
- package/src/prompts/tools/bash.md +9 -0
- package/src/prompts/tools/cron.md +25 -0
- package/src/prompts/tools/monitor.md +30 -0
- package/src/prompts/tools/subagent.md +33 -3
- package/src/runtime-mcp/oauth-flow.ts +4 -2
- package/src/sdk.ts +7 -0
- package/src/session/agent-session.ts +247 -38
- package/src/session/session-manager.ts +13 -1
- package/src/session/streaming-output.ts +21 -0
- package/src/skill-state/active-state.ts +222 -78
- package/src/skill-state/deep-interview-mutation-guard.ts +91 -13
- package/src/skill-state/initial-phase.ts +2 -0
- package/src/skill-state/workflow-state-contract.ts +26 -0
- package/src/task/agents.ts +1 -0
- package/src/task/executor.ts +51 -8
- package/src/task/index.ts +120 -8
- package/src/task/render.ts +6 -3
- package/src/task/types.ts +57 -3
- package/src/tools/ask.ts +28 -7
- package/src/tools/bash-allowed-prefixes.ts +169 -0
- package/src/tools/bash.ts +190 -29
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/cron.ts +665 -0
- package/src/tools/index.ts +20 -2
- package/src/tools/monitor.ts +136 -0
- package/src/tools/subagent.ts +255 -64
- package/src/vim/engine.ts +3 -3
- package/src/web/search/index.ts +31 -18
- package/src/web/search/provider.ts +57 -12
- package/src/web/search/providers/duckduckgo.ts +279 -0
- package/src/web/search/types.ts +2 -0
- package/src/modes/theme/dark.json +0 -95
- package/src/modes/theme/defaults/alabaster.json +0 -93
- package/src/modes/theme/defaults/amethyst.json +0 -96
- package/src/modes/theme/defaults/anthracite.json +0 -93
- package/src/modes/theme/defaults/basalt.json +0 -91
- package/src/modes/theme/defaults/birch.json +0 -95
- package/src/modes/theme/defaults/dark-abyss.json +0 -91
- package/src/modes/theme/defaults/dark-arctic.json +0 -104
- package/src/modes/theme/defaults/dark-aurora.json +0 -95
- package/src/modes/theme/defaults/dark-catppuccin.json +0 -107
- package/src/modes/theme/defaults/dark-cavern.json +0 -91
- package/src/modes/theme/defaults/dark-copper.json +0 -95
- package/src/modes/theme/defaults/dark-cosmos.json +0 -90
- package/src/modes/theme/defaults/dark-cyberpunk.json +0 -102
- package/src/modes/theme/defaults/dark-dracula.json +0 -98
- package/src/modes/theme/defaults/dark-eclipse.json +0 -91
- package/src/modes/theme/defaults/dark-ember.json +0 -95
- package/src/modes/theme/defaults/dark-equinox.json +0 -90
- package/src/modes/theme/defaults/dark-forest.json +0 -96
- package/src/modes/theme/defaults/dark-github.json +0 -105
- package/src/modes/theme/defaults/dark-gruvbox.json +0 -112
- package/src/modes/theme/defaults/dark-lavender.json +0 -95
- package/src/modes/theme/defaults/dark-lunar.json +0 -89
- package/src/modes/theme/defaults/dark-midnight.json +0 -95
- package/src/modes/theme/defaults/dark-monochrome.json +0 -94
- package/src/modes/theme/defaults/dark-monokai.json +0 -98
- package/src/modes/theme/defaults/dark-nebula.json +0 -90
- package/src/modes/theme/defaults/dark-nord.json +0 -97
- package/src/modes/theme/defaults/dark-ocean.json +0 -101
- package/src/modes/theme/defaults/dark-one.json +0 -100
- package/src/modes/theme/defaults/dark-poimandres.json +0 -141
- package/src/modes/theme/defaults/dark-rainforest.json +0 -91
- package/src/modes/theme/defaults/dark-reef.json +0 -91
- package/src/modes/theme/defaults/dark-retro.json +0 -92
- package/src/modes/theme/defaults/dark-rose-pine.json +0 -96
- package/src/modes/theme/defaults/dark-sakura.json +0 -95
- package/src/modes/theme/defaults/dark-slate.json +0 -95
- package/src/modes/theme/defaults/dark-solarized.json +0 -97
- package/src/modes/theme/defaults/dark-solstice.json +0 -90
- package/src/modes/theme/defaults/dark-starfall.json +0 -91
- package/src/modes/theme/defaults/dark-sunset.json +0 -99
- package/src/modes/theme/defaults/dark-swamp.json +0 -90
- package/src/modes/theme/defaults/dark-synthwave.json +0 -103
- package/src/modes/theme/defaults/dark-taiga.json +0 -91
- package/src/modes/theme/defaults/dark-terminal.json +0 -95
- package/src/modes/theme/defaults/dark-tokyo-night.json +0 -101
- package/src/modes/theme/defaults/dark-tundra.json +0 -91
- package/src/modes/theme/defaults/dark-twilight.json +0 -91
- package/src/modes/theme/defaults/dark-volcanic.json +0 -91
- package/src/modes/theme/defaults/graphite.json +0 -92
- package/src/modes/theme/defaults/light-arctic.json +0 -107
- package/src/modes/theme/defaults/light-aurora-day.json +0 -91
- package/src/modes/theme/defaults/light-canyon.json +0 -91
- package/src/modes/theme/defaults/light-catppuccin.json +0 -106
- package/src/modes/theme/defaults/light-cirrus.json +0 -90
- package/src/modes/theme/defaults/light-coral.json +0 -95
- package/src/modes/theme/defaults/light-cyberpunk.json +0 -96
- package/src/modes/theme/defaults/light-dawn.json +0 -90
- package/src/modes/theme/defaults/light-dunes.json +0 -91
- package/src/modes/theme/defaults/light-eucalyptus.json +0 -95
- package/src/modes/theme/defaults/light-forest.json +0 -100
- package/src/modes/theme/defaults/light-frost.json +0 -95
- package/src/modes/theme/defaults/light-github.json +0 -115
- package/src/modes/theme/defaults/light-glacier.json +0 -91
- package/src/modes/theme/defaults/light-gruvbox.json +0 -108
- package/src/modes/theme/defaults/light-haze.json +0 -90
- package/src/modes/theme/defaults/light-honeycomb.json +0 -95
- package/src/modes/theme/defaults/light-lagoon.json +0 -91
- package/src/modes/theme/defaults/light-lavender.json +0 -95
- package/src/modes/theme/defaults/light-meadow.json +0 -91
- package/src/modes/theme/defaults/light-mint.json +0 -95
- package/src/modes/theme/defaults/light-monochrome.json +0 -101
- package/src/modes/theme/defaults/light-ocean.json +0 -99
- package/src/modes/theme/defaults/light-one.json +0 -99
- package/src/modes/theme/defaults/light-opal.json +0 -91
- package/src/modes/theme/defaults/light-orchard.json +0 -91
- package/src/modes/theme/defaults/light-paper.json +0 -95
- package/src/modes/theme/defaults/light-poimandres.json +0 -141
- package/src/modes/theme/defaults/light-prism.json +0 -90
- package/src/modes/theme/defaults/light-retro.json +0 -98
- package/src/modes/theme/defaults/light-sand.json +0 -95
- package/src/modes/theme/defaults/light-savanna.json +0 -91
- package/src/modes/theme/defaults/light-solarized.json +0 -102
- package/src/modes/theme/defaults/light-soleil.json +0 -90
- package/src/modes/theme/defaults/light-sunset.json +0 -99
- package/src/modes/theme/defaults/light-synthwave.json +0 -98
- package/src/modes/theme/defaults/light-tokyo-night.json +0 -111
- package/src/modes/theme/defaults/light-wetland.json +0 -91
- package/src/modes/theme/defaults/light-zenith.json +0 -89
- package/src/modes/theme/defaults/limestone.json +0 -94
- package/src/modes/theme/defaults/mahogany.json +0 -97
- package/src/modes/theme/defaults/marble.json +0 -93
- package/src/modes/theme/defaults/obsidian.json +0 -91
- package/src/modes/theme/defaults/onyx.json +0 -91
- package/src/modes/theme/defaults/pearl.json +0 -93
- package/src/modes/theme/defaults/porcelain.json +0 -91
- package/src/modes/theme/defaults/quartz.json +0 -96
- package/src/modes/theme/defaults/sandstone.json +0 -95
- package/src/modes/theme/defaults/titanium.json +0 -90
- package/src/modes/theme/light.json +0 -93
package/src/sdk.ts
CHANGED
|
@@ -294,6 +294,8 @@ export interface CreateAgentSessionOptions {
|
|
|
294
294
|
agentId?: string;
|
|
295
295
|
/** Display name for the agent in IRC. Default: "main" or "sub". */
|
|
296
296
|
agentDisplayName?: string;
|
|
297
|
+
/** Optional restricted bash command prefixes for read-only role agents. */
|
|
298
|
+
bashAllowedPrefixes?: string[];
|
|
297
299
|
/** Optional shared agent registry for IRC routing. Default: AgentRegistry.global(). */
|
|
298
300
|
agentRegistry?: AgentRegistry;
|
|
299
301
|
/** Parent task ID prefix for nested artifact naming (e.g., "6-Extensions") */
|
|
@@ -325,6 +327,8 @@ export interface CreateAgentSessionOptions {
|
|
|
325
327
|
forkContextSeed?: ForkContextSeed;
|
|
326
328
|
/** Optional provider state override. Fork-context children should omit this by default. */
|
|
327
329
|
providerSessionState?: Map<string, ProviderSessionState>;
|
|
330
|
+
/** Cooperative pause checkpoint passed through to Agent. */
|
|
331
|
+
shouldPause?: () => boolean;
|
|
328
332
|
}
|
|
329
333
|
|
|
330
334
|
/** Result from createAgentSession */
|
|
@@ -655,6 +659,7 @@ function createCustomToolsExtension(tools: CustomTool[]): ExtensionFactory {
|
|
|
655
659
|
reason: "auto_retry_start",
|
|
656
660
|
attempt: event.attempt,
|
|
657
661
|
maxAttempts: event.maxAttempts,
|
|
662
|
+
unbounded: event.unbounded,
|
|
658
663
|
delayMs: event.delayMs,
|
|
659
664
|
errorMessage: event.errorMessage,
|
|
660
665
|
},
|
|
@@ -1166,6 +1171,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1166
1171
|
return agent?.state.model ?? model;
|
|
1167
1172
|
},
|
|
1168
1173
|
getAgentId: () => resolvedAgentId,
|
|
1174
|
+
bashAllowedPrefixes: options.bashAllowedPrefixes,
|
|
1169
1175
|
getToolByName: name => session?.getToolByName(name),
|
|
1170
1176
|
agentRegistry,
|
|
1171
1177
|
getSessionSpawns: () => options.spawns ?? "*",
|
|
@@ -1794,6 +1800,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1794
1800
|
requestMaxRetries: retrySettings.requestMaxRetries,
|
|
1795
1801
|
streamMaxRetries: retrySettings.streamMaxRetries,
|
|
1796
1802
|
kimiApiFormat: settings.get("providers.kimiApiFormat") ?? "anthropic",
|
|
1803
|
+
shouldPause: options.shouldPause,
|
|
1797
1804
|
preferWebsockets: preferOpenAICodexWebsockets,
|
|
1798
1805
|
getToolContext: tc => toolContextStore.getContext(tc),
|
|
1799
1806
|
getApiKey: async provider => {
|
|
@@ -167,6 +167,7 @@ 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";
|
|
@@ -263,7 +264,14 @@ export type AgentSessionEvent =
|
|
|
263
264
|
/** True when compaction was skipped for a benign reason (no model, no candidates, nothing to compact). */
|
|
264
265
|
skipped?: boolean;
|
|
265
266
|
}
|
|
266
|
-
| {
|
|
267
|
+
| {
|
|
268
|
+
type: "auto_retry_start";
|
|
269
|
+
attempt: number;
|
|
270
|
+
maxAttempts: number;
|
|
271
|
+
delayMs: number;
|
|
272
|
+
errorMessage: string;
|
|
273
|
+
unbounded?: boolean;
|
|
274
|
+
}
|
|
267
275
|
| { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string }
|
|
268
276
|
| { type: "retry_fallback_applied"; from: string; to: string; role: string }
|
|
269
277
|
| { type: "retry_fallback_succeeded"; model: string; role: string }
|
|
@@ -282,6 +290,11 @@ export type AgentSessionEvent =
|
|
|
282
290
|
*/
|
|
283
291
|
const SAFE_PATH_COMPONENT = /^[A-Za-z0-9_-][A-Za-z0-9._-]{0,63}$/;
|
|
284
292
|
|
|
293
|
+
function isUnderProjectGjc(cwd: string, targetPath: string): boolean {
|
|
294
|
+
const relative = path.relative(path.join(path.resolve(cwd), ".gjc"), path.resolve(targetPath));
|
|
295
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
296
|
+
}
|
|
297
|
+
|
|
285
298
|
/** Listener function for agent session events */
|
|
286
299
|
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
|
|
287
300
|
export type AsyncJobSnapshotItem = Pick<AsyncJob, "id" | "type" | "status" | "label" | "startTime">;
|
|
@@ -629,7 +642,11 @@ function createHandoffFileName(date = new Date()): string {
|
|
|
629
642
|
// ============================================================================
|
|
630
643
|
|
|
631
644
|
/** Tools that require user permission before execution when an ACP client is connected. */
|
|
632
|
-
const PERMISSION_REQUIRED_TOOLS = new Set(["bash", "edit", "delete", "move"]);
|
|
645
|
+
const PERMISSION_REQUIRED_TOOLS = new Set(["bash", "monitor", "edit", "delete", "move"]);
|
|
646
|
+
|
|
647
|
+
function isShellExecutionPermissionTool(toolName: string): boolean {
|
|
648
|
+
return toolName === "bash" || toolName === "monitor";
|
|
649
|
+
}
|
|
633
650
|
|
|
634
651
|
/** Permission options presented to the client on each gated tool call. */
|
|
635
652
|
const PERMISSION_OPTIONS: ClientBridgePermissionOption[] = [
|
|
@@ -694,7 +711,7 @@ function getPermissionIntent(
|
|
|
694
711
|
args: unknown,
|
|
695
712
|
): { toolName: string; title: string; paths?: string[]; cacheKey: string } | undefined {
|
|
696
713
|
const a = args && typeof args === "object" && !Array.isArray(args) ? (args as Record<string, unknown>) : {};
|
|
697
|
-
if (toolName
|
|
714
|
+
if (isShellExecutionPermissionTool(toolName)) {
|
|
698
715
|
const cmd = getStringProperty(a, "command")?.slice(0, 80);
|
|
699
716
|
return { toolName, title: cmd || toolName, cacheKey: toolName };
|
|
700
717
|
}
|
|
@@ -848,6 +865,7 @@ export class AgentSession {
|
|
|
848
865
|
|
|
849
866
|
// Retry state
|
|
850
867
|
#retryAbortController: AbortController | undefined = undefined;
|
|
868
|
+
#retryNowRequested = false;
|
|
851
869
|
#retryAttempt = 0;
|
|
852
870
|
#retryPromise: Promise<void> | undefined = undefined;
|
|
853
871
|
#retryResolve: (() => void) | undefined = undefined;
|
|
@@ -1497,7 +1515,13 @@ export class AgentSession {
|
|
|
1497
1515
|
*/
|
|
1498
1516
|
#cancelOwnAsyncJobs(): void {
|
|
1499
1517
|
if (!this.#agentId) return;
|
|
1500
|
-
AsyncJobManager.instance()
|
|
1518
|
+
const manager = AsyncJobManager.instance();
|
|
1519
|
+
if (!manager) return;
|
|
1520
|
+
// Run owner cleanups first so cron timers (and any other owner-scoped
|
|
1521
|
+
// resource cleanup) cannot register fresh jobs while we tear down the
|
|
1522
|
+
// existing ones. Cleanup callbacks are error-isolated inside the manager.
|
|
1523
|
+
manager.runOwnerCleanups({ ownerId: this.#agentId });
|
|
1524
|
+
manager.cancelAll({ ownerId: this.#agentId });
|
|
1501
1525
|
}
|
|
1502
1526
|
|
|
1503
1527
|
// =========================================================================
|
|
@@ -1877,6 +1901,15 @@ export class AgentSession {
|
|
|
1877
1901
|
attempt: this.#retryAttempt,
|
|
1878
1902
|
});
|
|
1879
1903
|
this.#retryAttempt = 0;
|
|
1904
|
+
// Settle the retry gate here, colocated with the success event, rather
|
|
1905
|
+
// than relying on the generic #resolveRetry() at the end of the
|
|
1906
|
+
// agent_end branch. That tail resolver is bypassed by every early
|
|
1907
|
+
// return in agent_end (successful `yield`, handoff-abort skip-maintenance,
|
|
1908
|
+
// missing assistant message), so a retry that recovers on a yield turn
|
|
1909
|
+
// would otherwise leave #retryPromise unresolved — wedging
|
|
1910
|
+
// #waitForPostPromptRecovery and the session as permanently busy.
|
|
1911
|
+
// #resolveRetry() is idempotent, so the later tail call is a no-op.
|
|
1912
|
+
this.#resolveRetry();
|
|
1880
1913
|
}
|
|
1881
1914
|
}
|
|
1882
1915
|
|
|
@@ -1991,6 +2024,18 @@ export class AgentSession {
|
|
|
1991
2024
|
const didRetry = await this.#handleRetryableError(msg);
|
|
1992
2025
|
if (didRetry) return; // Retry was initiated, don't proceed to compaction
|
|
1993
2026
|
}
|
|
2027
|
+
if (this.#retryAttempt > 0) {
|
|
2028
|
+
// A prior retry ended on a non-retryable (terminal) message: emit
|
|
2029
|
+
// the terminal retry-end and reset so observers clear retry state.
|
|
2030
|
+
const attempt = this.#retryAttempt;
|
|
2031
|
+
this.#retryAttempt = 0;
|
|
2032
|
+
await this.#emitSessionEvent({
|
|
2033
|
+
type: "auto_retry_end",
|
|
2034
|
+
success: false,
|
|
2035
|
+
attempt,
|
|
2036
|
+
finalError: msg.errorMessage,
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
1994
2039
|
this.#resolveRetry();
|
|
1995
2040
|
|
|
1996
2041
|
const compactionTask = this.#checkCompaction(msg);
|
|
@@ -2861,6 +2906,7 @@ export class AgentSession {
|
|
|
2861
2906
|
maxAttempts: event.maxAttempts,
|
|
2862
2907
|
delayMs: event.delayMs,
|
|
2863
2908
|
errorMessage: event.errorMessage,
|
|
2909
|
+
unbounded: event.unbounded,
|
|
2864
2910
|
});
|
|
2865
2911
|
} else if (event.type === "auto_retry_end") {
|
|
2866
2912
|
await this.#extensionRunner.emit({
|
|
@@ -3395,8 +3441,9 @@ export class AgentSession {
|
|
|
3395
3441
|
if (!permissionIntent) {
|
|
3396
3442
|
return await target.execute(toolCallId, args as never, signal, onUpdate, ctx);
|
|
3397
3443
|
}
|
|
3444
|
+
const isShellExecutionTool = isShellExecutionPermissionTool(target.name);
|
|
3398
3445
|
const command =
|
|
3399
|
-
|
|
3446
|
+
isShellExecutionTool && args && typeof args === "object" && !Array.isArray(args)
|
|
3400
3447
|
? getStringProperty(args as Record<string, unknown>, "command")
|
|
3401
3448
|
: undefined;
|
|
3402
3449
|
const commandContent = command
|
|
@@ -3426,7 +3473,7 @@ export class AgentSession {
|
|
|
3426
3473
|
toolCallId,
|
|
3427
3474
|
toolName: target.name,
|
|
3428
3475
|
title: permissionIntent.title,
|
|
3429
|
-
...(
|
|
3476
|
+
...(isShellExecutionTool ? { kind: "execute" } : {}),
|
|
3430
3477
|
status: "pending",
|
|
3431
3478
|
rawInput: args,
|
|
3432
3479
|
...(commandContent ? { content: commandContent } : {}),
|
|
@@ -3474,7 +3521,7 @@ export class AgentSession {
|
|
|
3474
3521
|
* prompts or tool execution can run.
|
|
3475
3522
|
*/
|
|
3476
3523
|
#wrapToolForDeepInterviewMutationGuard<T extends AgentTool>(tool: T): T {
|
|
3477
|
-
if (!["edit", "write", "ast_edit"].includes(tool.name)) return tool;
|
|
3524
|
+
if (!["edit", "write", "ast_edit", "bash"].includes(tool.name)) return tool;
|
|
3478
3525
|
return new Proxy(tool, {
|
|
3479
3526
|
get: (target, prop) => {
|
|
3480
3527
|
if (prop !== "execute") return Reflect.get(target, prop, target);
|
|
@@ -5016,7 +5063,18 @@ export class AgentSession {
|
|
|
5016
5063
|
/**
|
|
5017
5064
|
* Abort current operation and wait for agent to become idle.
|
|
5018
5065
|
*/
|
|
5019
|
-
async abort(options?: {
|
|
5066
|
+
async abort(options?: {
|
|
5067
|
+
goalReason?: "interrupted" | "internal";
|
|
5068
|
+
timeoutMs?: number;
|
|
5069
|
+
cause?:
|
|
5070
|
+
| "user_interrupt"
|
|
5071
|
+
| "new_session"
|
|
5072
|
+
| "session_switch"
|
|
5073
|
+
| "compaction"
|
|
5074
|
+
| "handoff"
|
|
5075
|
+
| "tool_abort"
|
|
5076
|
+
| "internal";
|
|
5077
|
+
}): Promise<void> {
|
|
5020
5078
|
this.abortRetry();
|
|
5021
5079
|
this.#promptGeneration++;
|
|
5022
5080
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
@@ -5063,6 +5121,18 @@ export class AgentSession {
|
|
|
5063
5121
|
if (this.#toolChoiceQueue.hasInFlight) {
|
|
5064
5122
|
this.#toolChoiceQueue.reject("aborted");
|
|
5065
5123
|
}
|
|
5124
|
+
|
|
5125
|
+
// Steer-on-interrupt: after a genuine user interrupt, resume with any
|
|
5126
|
+
// queued steering instead of going idle. Lifecycle/teardown causes
|
|
5127
|
+
// (default "internal") suppress this; new-session/handoff additionally
|
|
5128
|
+
// clear the steering queue, and compaction resumes via its own path.
|
|
5129
|
+
if ((options?.cause ?? "internal") === "user_interrupt" && this.agent.hasQueuedSteering()) {
|
|
5130
|
+
this.#scheduleAgentContinue({
|
|
5131
|
+
delayMs: 1,
|
|
5132
|
+
generation: this.#promptGeneration,
|
|
5133
|
+
shouldContinue: () => this.agent.hasQueuedSteering(),
|
|
5134
|
+
});
|
|
5135
|
+
}
|
|
5066
5136
|
}
|
|
5067
5137
|
|
|
5068
5138
|
/**
|
|
@@ -5920,7 +5990,14 @@ export class AgentSession {
|
|
|
5920
5990
|
if (artifactsDir) {
|
|
5921
5991
|
const handoffFilePath = path.join(artifactsDir, createHandoffFileName());
|
|
5922
5992
|
try {
|
|
5923
|
-
|
|
5993
|
+
if (isUnderProjectGjc(this.sessionManager.getCwd(), handoffFilePath)) {
|
|
5994
|
+
await writeArtifact(handoffFilePath, `${handoffText}\n`, {
|
|
5995
|
+
cwd: this.sessionManager.getCwd(),
|
|
5996
|
+
audit: { category: "artifact", verb: "write", owner: "gjc-runtime" },
|
|
5997
|
+
});
|
|
5998
|
+
} else {
|
|
5999
|
+
await Bun.write(handoffFilePath, `${handoffText}\n`);
|
|
6000
|
+
}
|
|
5924
6001
|
savedPath = handoffFilePath;
|
|
5925
6002
|
} catch (error) {
|
|
5926
6003
|
logger.warn("Failed to save handoff document to disk", {
|
|
@@ -7110,19 +7187,14 @@ export class AgentSession {
|
|
|
7110
7187
|
// =========================================================================
|
|
7111
7188
|
|
|
7112
7189
|
/**
|
|
7113
|
-
*
|
|
7114
|
-
*
|
|
7115
|
-
*
|
|
7190
|
+
* Whether an error should be retried. Uses the ordered classifier:
|
|
7191
|
+
* context-overflow routes to compaction; clearly-terminal coded errors
|
|
7192
|
+
* (auth/400/not-found) surface immediately; usage-limit, transient, and
|
|
7193
|
+
* unknown/no-code errors are retryable.
|
|
7116
7194
|
*/
|
|
7117
7195
|
#isRetryableError(message: AssistantMessage): boolean {
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
// Context overflow is handled by compaction, not retry
|
|
7121
|
-
const contextWindow = this.model?.contextWindow ?? 0;
|
|
7122
|
-
if (isContextOverflow(message, contextWindow)) return false;
|
|
7123
|
-
|
|
7124
|
-
const err = message.errorMessage;
|
|
7125
|
-
return this.#isTransientErrorMessage(err) || isUsageLimitError(err);
|
|
7196
|
+
const classification = this.#classifyErrorForRetry(message);
|
|
7197
|
+
return classification === "usage_limit" || classification === "transient" || classification === "unknown";
|
|
7126
7198
|
}
|
|
7127
7199
|
|
|
7128
7200
|
#isTransientErrorMessage(errorMessage: string): boolean {
|
|
@@ -7148,6 +7220,63 @@ export class AgentSession {
|
|
|
7148
7220
|
);
|
|
7149
7221
|
}
|
|
7150
7222
|
|
|
7223
|
+
#isTerminalErrorMessage(errorMessage: string): boolean {
|
|
7224
|
+
// Errors that will never succeed on retry (auth/permission, malformed
|
|
7225
|
+
// request, unknown/unsupported model). These surface immediately rather
|
|
7226
|
+
// than retry forever.
|
|
7227
|
+
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(
|
|
7228
|
+
errorMessage,
|
|
7229
|
+
);
|
|
7230
|
+
}
|
|
7231
|
+
|
|
7232
|
+
#extractExplicitHttpStatusFromErrorMessage(errorMessage: string): number | undefined {
|
|
7233
|
+
// Parse only explicit HTTP/status wording. Do not treat generic
|
|
7234
|
+
// `error: 400` as an HTTP status because rate-limit copy can say
|
|
7235
|
+
// "rate limit error: 400 requests per minute".
|
|
7236
|
+
const match = /\b(?:http(?:\s+status)?|status(?:[\s_-]+code)?)(?:\s+|[:=]\s*)(\d{3})\b/i.exec(errorMessage);
|
|
7237
|
+
if (!match) return undefined;
|
|
7238
|
+
const status = Number(match[1]);
|
|
7239
|
+
return Number.isFinite(status) && status >= 100 && status <= 599 ? status : undefined;
|
|
7240
|
+
}
|
|
7241
|
+
|
|
7242
|
+
/**
|
|
7243
|
+
* Ordered retry classification: overflow (compaction) -> terminal (surface)
|
|
7244
|
+
* -> usage_limit (rotation) -> transient (retry) -> unknown (retry).
|
|
7245
|
+
*/
|
|
7246
|
+
#classifyErrorForRetry(
|
|
7247
|
+
message: AssistantMessage,
|
|
7248
|
+
): "none" | "overflow" | "terminal" | "usage_limit" | "transient" | "unknown" {
|
|
7249
|
+
if (message.stopReason !== "error" || !message.errorMessage) return "none";
|
|
7250
|
+
const contextWindow = this.model?.contextWindow ?? 0;
|
|
7251
|
+
if (isContextOverflow(message, contextWindow)) return "overflow";
|
|
7252
|
+
const err = message.errorMessage;
|
|
7253
|
+
// Stream-envelope errors are only transient in the pre-message_start
|
|
7254
|
+
// variant; any other envelope failure is structural and must surface.
|
|
7255
|
+
if (/anthropic stream envelope error:/i.test(err)) {
|
|
7256
|
+
return this.#isTransientEnvelopeErrorMessage(err) ? "transient" : "terminal";
|
|
7257
|
+
}
|
|
7258
|
+
const explicitStatus = this.#extractExplicitHttpStatusFromErrorMessage(err);
|
|
7259
|
+
const structuredStatus = message.errorStatus;
|
|
7260
|
+
const terminalStatus = explicitStatus ?? structuredStatus;
|
|
7261
|
+
const isTerminalHttp4xx =
|
|
7262
|
+
terminalStatus !== undefined &&
|
|
7263
|
+
terminalStatus >= 400 &&
|
|
7264
|
+
terminalStatus < 500 &&
|
|
7265
|
+
terminalStatus !== 408 &&
|
|
7266
|
+
terminalStatus !== 425 &&
|
|
7267
|
+
terminalStatus !== 429;
|
|
7268
|
+
if (this.#isTerminalErrorMessage(err)) return "terminal";
|
|
7269
|
+
if (isUsageLimitError(err)) return "usage_limit";
|
|
7270
|
+
// Explicit HTTP/status wording is authoritative. Structured provider status
|
|
7271
|
+
// is also authoritative except for rate-limit copy where providers may have
|
|
7272
|
+
// parsed an incidental quota number such as "400 requests per minute".
|
|
7273
|
+
if (isTerminalHttp4xx && (explicitStatus !== undefined || !/rate.?limit|too many requests/i.test(err))) {
|
|
7274
|
+
return "terminal";
|
|
7275
|
+
}
|
|
7276
|
+
if (this.#isTransientErrorMessage(err)) return "transient";
|
|
7277
|
+
return "unknown";
|
|
7278
|
+
}
|
|
7279
|
+
|
|
7151
7280
|
#getRetryFallbackChains(): RetryFallbackChains {
|
|
7152
7281
|
const configuredChains = this.settings.get("retry.fallbackChains");
|
|
7153
7282
|
if (!configuredChains || typeof configuredChains !== "object") return {};
|
|
@@ -7417,6 +7546,8 @@ export class AgentSession {
|
|
|
7417
7546
|
async #handleRetryableError(message: AssistantMessage): Promise<boolean> {
|
|
7418
7547
|
const retrySettings = this.settings.getGroup("retry");
|
|
7419
7548
|
if (!retrySettings.enabled) return false;
|
|
7549
|
+
const retryClassification = this.#classifyErrorForRetry(message);
|
|
7550
|
+
const unboundedClass = retryClassification === "transient" || retryClassification === "unknown";
|
|
7420
7551
|
|
|
7421
7552
|
const generation = this.#promptGeneration;
|
|
7422
7553
|
this.#retryAttempt++;
|
|
@@ -7429,7 +7560,7 @@ export class AgentSession {
|
|
|
7429
7560
|
this.#retryResolve = resolve;
|
|
7430
7561
|
}
|
|
7431
7562
|
|
|
7432
|
-
if (this.#retryAttempt > retrySettings.maxRetries) {
|
|
7563
|
+
if (!unboundedClass && this.#retryAttempt > retrySettings.maxRetries) {
|
|
7433
7564
|
// Max retries exceeded, emit final failure and reset
|
|
7434
7565
|
await this.#emitSessionEvent({
|
|
7435
7566
|
type: "auto_retry_end",
|
|
@@ -7486,7 +7617,16 @@ export class AgentSession {
|
|
|
7486
7617
|
// assistant error message is preserved in agent state so the caller
|
|
7487
7618
|
// can act on it.
|
|
7488
7619
|
const maxDelayMs = retrySettings.maxDelayMs;
|
|
7489
|
-
if (
|
|
7620
|
+
if (unboundedClass && !switchedCredential && !switchedModel) {
|
|
7621
|
+
// Retry forever: honor a provider-supplied wait, otherwise cap the
|
|
7622
|
+
// exponential backoff at the ceiling instead of giving up.
|
|
7623
|
+
if (parsedRetryAfterMs !== undefined) {
|
|
7624
|
+
delayMs = Math.max(delayMs, parsedRetryAfterMs);
|
|
7625
|
+
} else if (maxDelayMs > 0) {
|
|
7626
|
+
delayMs = Math.min(delayMs, maxDelayMs);
|
|
7627
|
+
}
|
|
7628
|
+
}
|
|
7629
|
+
if (!unboundedClass && maxDelayMs > 0 && delayMs > maxDelayMs && !switchedCredential && !switchedModel) {
|
|
7490
7630
|
const attempt = this.#retryAttempt;
|
|
7491
7631
|
this.#retryAttempt = 0;
|
|
7492
7632
|
await this.#emitSessionEvent({
|
|
@@ -7499,12 +7639,22 @@ export class AgentSession {
|
|
|
7499
7639
|
return false;
|
|
7500
7640
|
}
|
|
7501
7641
|
|
|
7642
|
+
// Create and install the backoff abort controller BEFORE emitting
|
|
7643
|
+
// auto_retry_start, so a synchronous retryNow()/abortRetry() invoked from
|
|
7644
|
+
// an event subscriber (e.g. the TUI Esc handler) is not lost in the gap
|
|
7645
|
+
// between the event and the controller assignment.
|
|
7646
|
+
const retryAbortController = new AbortController();
|
|
7647
|
+
this.#retryAbortController?.abort();
|
|
7648
|
+
this.#retryAbortController = retryAbortController;
|
|
7649
|
+
this.#retryNowRequested = false;
|
|
7650
|
+
|
|
7502
7651
|
await this.#emitSessionEvent({
|
|
7503
7652
|
type: "auto_retry_start",
|
|
7504
7653
|
attempt: this.#retryAttempt,
|
|
7505
7654
|
maxAttempts: retrySettings.maxRetries,
|
|
7506
7655
|
delayMs,
|
|
7507
7656
|
errorMessage,
|
|
7657
|
+
unbounded: unboundedClass,
|
|
7508
7658
|
});
|
|
7509
7659
|
|
|
7510
7660
|
// Remove error message from agent state (keep in session for history)
|
|
@@ -7514,34 +7664,49 @@ export class AgentSession {
|
|
|
7514
7664
|
}
|
|
7515
7665
|
|
|
7516
7666
|
// Wait with exponential backoff (abortable).
|
|
7517
|
-
const retryAbortController = new AbortController();
|
|
7518
|
-
this.#retryAbortController?.abort();
|
|
7519
|
-
this.#retryAbortController = retryAbortController;
|
|
7520
7667
|
try {
|
|
7521
7668
|
await scheduler.wait(delayMs, { signal: retryAbortController.signal });
|
|
7522
7669
|
} catch {
|
|
7523
7670
|
if (this.#retryAbortController !== retryAbortController) {
|
|
7524
7671
|
return false;
|
|
7525
7672
|
}
|
|
7526
|
-
// Aborted during sleep - emit end event so UI can clean up
|
|
7527
|
-
const attempt = this.#retryAttempt;
|
|
7528
|
-
this.#retryAttempt = 0;
|
|
7529
7673
|
this.#retryAbortController = undefined;
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7674
|
+
if (this.#retryNowRequested) {
|
|
7675
|
+
// Retry-now: skip the remaining backoff and fall through to
|
|
7676
|
+
// re-attempt immediately (keeps the retry session alive).
|
|
7677
|
+
this.#retryNowRequested = false;
|
|
7678
|
+
} else {
|
|
7679
|
+
// Aborted during sleep (cancel) - emit end event so UI can clean up
|
|
7680
|
+
const attempt = this.#retryAttempt;
|
|
7681
|
+
this.#retryAttempt = 0;
|
|
7682
|
+
await this.#emitSessionEvent({
|
|
7683
|
+
type: "auto_retry_end",
|
|
7684
|
+
success: false,
|
|
7685
|
+
attempt,
|
|
7686
|
+
finalError: "Retry cancelled",
|
|
7687
|
+
});
|
|
7688
|
+
this.#resolveRetry();
|
|
7689
|
+
return false;
|
|
7690
|
+
}
|
|
7538
7691
|
}
|
|
7539
7692
|
if (this.#retryAbortController === retryAbortController) {
|
|
7540
7693
|
this.#retryAbortController = undefined;
|
|
7541
7694
|
}
|
|
7542
7695
|
|
|
7543
7696
|
// Retry via continue() outside the agent_end event callback chain.
|
|
7544
|
-
|
|
7697
|
+
// If the scheduled continue cannot run — it throws (e.g. AgentBusyError from a
|
|
7698
|
+
// concurrent turn, or "Cannot continue ...") or is skipped because a newer
|
|
7699
|
+
// generation took over — the agent_end that normally resolves #retryPromise
|
|
7700
|
+
// never arrives. Finalize the retry in that case so #waitForPostPromptRecovery
|
|
7701
|
+
// (and the in-flight prompt holding it open) cannot wedge the session as
|
|
7702
|
+
// permanently busy, which would turn every later prompt() into a
|
|
7703
|
+
// non-recoverable AgentBusyError loop.
|
|
7704
|
+
this.#scheduleAgentContinue({
|
|
7705
|
+
delayMs: 1,
|
|
7706
|
+
generation,
|
|
7707
|
+
onError: () => this.#failRetryRecovery("Retry continuation failed to start"),
|
|
7708
|
+
onSkip: () => this.#failRetryRecovery("Retry continuation was superseded"),
|
|
7709
|
+
});
|
|
7545
7710
|
|
|
7546
7711
|
return true;
|
|
7547
7712
|
}
|
|
@@ -7550,8 +7715,41 @@ export class AgentSession {
|
|
|
7550
7715
|
* Cancel in-progress retry.
|
|
7551
7716
|
*/
|
|
7552
7717
|
abortRetry(): void {
|
|
7718
|
+
this.#retryNowRequested = false;
|
|
7553
7719
|
this.#retryAbortController?.abort();
|
|
7554
|
-
// Note:
|
|
7720
|
+
// Note: #retryAttempt is reset in the catch block of #handleRetryableError
|
|
7721
|
+
this.#resolveRetry();
|
|
7722
|
+
}
|
|
7723
|
+
|
|
7724
|
+
/**
|
|
7725
|
+
* Skip the current retry backoff and re-attempt immediately. Distinct from
|
|
7726
|
+
* abortRetry(), which cancels the retry and returns to idle. No-op when no
|
|
7727
|
+
* retry backoff is active.
|
|
7728
|
+
*/
|
|
7729
|
+
retryNow(): void {
|
|
7730
|
+
if (!this.#retryAbortController) return;
|
|
7731
|
+
this.#retryNowRequested = true;
|
|
7732
|
+
this.#retryAbortController.abort();
|
|
7733
|
+
}
|
|
7734
|
+
|
|
7735
|
+
/**
|
|
7736
|
+
* Finalize a pending auto-retry that can no longer reach a resolving agent_end
|
|
7737
|
+
* (the scheduled continue threw or was superseded). Without this, #retryPromise
|
|
7738
|
+
* stays unresolved, #waitForPostPromptRecovery never returns, the owning
|
|
7739
|
+
* prompt's in-flight count is never released, and the session reports
|
|
7740
|
+
* `isStreaming === true` forever — turning every later prompt() into a
|
|
7741
|
+
* non-recoverable AgentBusyError. No-op once the retry has already settled.
|
|
7742
|
+
*/
|
|
7743
|
+
#failRetryRecovery(reason: string): void {
|
|
7744
|
+
if (!this.#retryPromise) return;
|
|
7745
|
+
const attempt = this.#retryAttempt;
|
|
7746
|
+
this.#retryAttempt = 0;
|
|
7747
|
+
void this.#emitSessionEvent({
|
|
7748
|
+
type: "auto_retry_end",
|
|
7749
|
+
success: false,
|
|
7750
|
+
attempt,
|
|
7751
|
+
finalError: reason,
|
|
7752
|
+
});
|
|
7555
7753
|
this.#resolveRetry();
|
|
7556
7754
|
}
|
|
7557
7755
|
|
|
@@ -8268,6 +8466,8 @@ export class AgentSession {
|
|
|
8268
8466
|
const previousFallbackSelectedMCPToolNames = previousSessionFile
|
|
8269
8467
|
? this.#getSessionDefaultSelectedMCPToolNames(previousSessionFile)
|
|
8270
8468
|
: undefined;
|
|
8469
|
+
const previousAgentSteeringQueue = this.agent.snapshotSteering();
|
|
8470
|
+
const previousAgentFollowUpQueue = this.agent.snapshotFollowUp();
|
|
8271
8471
|
|
|
8272
8472
|
this.#steeringMessages = [];
|
|
8273
8473
|
this.#followUpMessages = [];
|
|
@@ -8286,6 +8486,12 @@ export class AgentSession {
|
|
|
8286
8486
|
const fallbackSelectedMCPToolNames = this.#getSessionDefaultSelectedMCPToolNames(sessionPath);
|
|
8287
8487
|
await this.#restoreMCPSelectionsForSessionContext(sessionContext, { fallbackSelectedMCPToolNames });
|
|
8288
8488
|
|
|
8489
|
+
// The target session is loaded and MCP selections are restored: the
|
|
8490
|
+
// switch is committed far enough to discard pre-switch delivery queues.
|
|
8491
|
+
// Clear before session_switch hooks, so messages enqueued by hooks belong
|
|
8492
|
+
// to the new session and remain deliverable.
|
|
8493
|
+
this.agent.clearAllQueues();
|
|
8494
|
+
|
|
8289
8495
|
// Emit session_switch event to hooks
|
|
8290
8496
|
if (this.#extensionRunner) {
|
|
8291
8497
|
await this.#extensionRunner.emit({
|
|
@@ -8380,6 +8586,9 @@ export class AgentSession {
|
|
|
8380
8586
|
this.#followUpMessages = previousFollowUpMessages;
|
|
8381
8587
|
this.#pendingNextTurnMessages = previousPendingNextTurnMessages;
|
|
8382
8588
|
this.#scheduledHiddenNextTurnGeneration = previousScheduledHiddenNextTurnGeneration;
|
|
8589
|
+
this.agent.clearAllQueues();
|
|
8590
|
+
this.agent.restoreSteering(previousAgentSteeringQueue);
|
|
8591
|
+
this.agent.restoreFollowUp(previousAgentFollowUpQueue);
|
|
8383
8592
|
if (previousModel) {
|
|
8384
8593
|
this.agent.setModel(previousModel);
|
|
8385
8594
|
}
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
Snowflake,
|
|
28
28
|
toError,
|
|
29
29
|
} from "@gajae-code/utils";
|
|
30
|
+
import { writeTextAtomic } from "../gjc-runtime/state-writer";
|
|
30
31
|
import { ArtifactManager } from "./artifacts";
|
|
31
32
|
import {
|
|
32
33
|
type BlobPutResult,
|
|
@@ -56,6 +57,10 @@ import type { SessionStorage, SessionStorageWriter } from "./session-storage";
|
|
|
56
57
|
import { FileSessionStorage, MemorySessionStorage } from "./session-storage";
|
|
57
58
|
|
|
58
59
|
export const CURRENT_SESSION_VERSION = 3;
|
|
60
|
+
function isUnderProjectGjc(cwd: string, targetPath: string): boolean {
|
|
61
|
+
const relative = path.relative(path.join(path.resolve(cwd), ".gjc"), path.resolve(targetPath));
|
|
62
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
63
|
+
}
|
|
59
64
|
|
|
60
65
|
export interface SessionHeader {
|
|
61
66
|
type: "session";
|
|
@@ -384,6 +389,7 @@ const migratedSessionRoots = new Set<string>();
|
|
|
384
389
|
* Best effort: callers decide whether migration failures should surface.
|
|
385
390
|
*/
|
|
386
391
|
function migrateSessionDirPath(oldPath: string, newPath: string): void {
|
|
392
|
+
// Session-dir lifecycle migration: moves/removes whole directories, not file content writes.
|
|
387
393
|
const existing = fs.statSync(newPath, { throwIfNoEntry: false });
|
|
388
394
|
if (existing?.isDirectory()) {
|
|
389
395
|
for (const file of fs.readdirSync(oldPath)) {
|
|
@@ -752,7 +758,13 @@ function writeTerminalBreadcrumb(cwd: string, sessionFile: string): void {
|
|
|
752
758
|
const breadcrumbFile = path.join(breadcrumbDir, terminalId);
|
|
753
759
|
const content = `${cwd}\n${sessionFile}\n`;
|
|
754
760
|
// Best-effort — don't break session creation if breadcrumb fails
|
|
755
|
-
|
|
761
|
+
const write = isUnderProjectGjc(cwd, breadcrumbFile)
|
|
762
|
+
? writeTextAtomic(breadcrumbFile, content, {
|
|
763
|
+
cwd,
|
|
764
|
+
audit: { category: "artifact", verb: "write", owner: "gjc-runtime" },
|
|
765
|
+
})
|
|
766
|
+
: Bun.write(breadcrumbFile, content);
|
|
767
|
+
write.catch(() => {});
|
|
756
768
|
}
|
|
757
769
|
|
|
758
770
|
/**
|
|
@@ -58,6 +58,17 @@ export interface OutputSinkOptions {
|
|
|
58
58
|
onChunk?: (chunk: string) => void;
|
|
59
59
|
/** Minimum ms between onChunk calls. 0 = every chunk (default). */
|
|
60
60
|
chunkThrottleMs?: number;
|
|
61
|
+
/**
|
|
62
|
+
* Unthrottled per-chunk callback fired *after* sanitization but *before*
|
|
63
|
+
* any throttle gating, column capping, or head/tail bookkeeping. Used by
|
|
64
|
+
* background-job substrate to record the complete process stream for the
|
|
65
|
+
* Monitor tool while keeping `onChunk` cheap for UI/progress.
|
|
66
|
+
*
|
|
67
|
+
* Receives the sanitized chunk verbatim; never receives the column-capped
|
|
68
|
+
* or minimized text. Implementations must be fast and side-effect-free
|
|
69
|
+
* relative to the sink (the sink does not catch errors from this callback).
|
|
70
|
+
*/
|
|
71
|
+
onRawChunk?: (chunk: string) => void;
|
|
61
72
|
}
|
|
62
73
|
|
|
63
74
|
export interface TruncationResult {
|
|
@@ -672,6 +683,7 @@ export class OutputSink {
|
|
|
672
683
|
readonly #spillThreshold: number;
|
|
673
684
|
readonly #headLimit: number;
|
|
674
685
|
readonly #onChunk?: (chunk: string) => void;
|
|
686
|
+
readonly #onRawChunk?: (chunk: string) => void;
|
|
675
687
|
readonly #chunkThrottleMs: number;
|
|
676
688
|
readonly #maxColumns: number;
|
|
677
689
|
|
|
@@ -684,6 +696,7 @@ export class OutputSink {
|
|
|
684
696
|
maxColumns = 0,
|
|
685
697
|
onChunk,
|
|
686
698
|
chunkThrottleMs = 0,
|
|
699
|
+
onRawChunk,
|
|
687
700
|
} = options ?? {};
|
|
688
701
|
this.#artifactPath = artifactPath;
|
|
689
702
|
this.#artifactId = artifactId;
|
|
@@ -691,6 +704,7 @@ export class OutputSink {
|
|
|
691
704
|
this.#headLimit = Math.max(0, headBytes);
|
|
692
705
|
this.#maxColumns = Math.max(0, maxColumns);
|
|
693
706
|
this.#onChunk = onChunk;
|
|
707
|
+
this.#onRawChunk = onRawChunk;
|
|
694
708
|
this.#chunkThrottleMs = chunkThrottleMs;
|
|
695
709
|
}
|
|
696
710
|
|
|
@@ -701,6 +715,13 @@ export class OutputSink {
|
|
|
701
715
|
push(chunk: string): void {
|
|
702
716
|
chunk = sanitizeWithOptionalSixelPassthrough(chunk, sanitizeText);
|
|
703
717
|
|
|
718
|
+
// Unthrottled raw-chunk hook fires before any throttle/cap gating so
|
|
719
|
+
// downstream consumers (e.g. AsyncJobManager.appendOutput) can record
|
|
720
|
+
// the complete process stream while UI/progress callbacks remain throttled.
|
|
721
|
+
if (this.#onRawChunk && chunk.length > 0) {
|
|
722
|
+
this.#onRawChunk(chunk);
|
|
723
|
+
}
|
|
724
|
+
|
|
704
725
|
// Throttled onChunk: only call the callback when enough time has passed.
|
|
705
726
|
// Live preview gets the raw (pre-cap) chunk so the TUI never lags behind
|
|
706
727
|
// what reached the sink — the column cap is for the persisted LLM view.
|