@gajae-code/coding-agent 0.12.0 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (247) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/dist/types/async/job-manager.d.ts +17 -3
  3. package/dist/types/config/settings-schema.d.ts +56 -2
  4. package/dist/types/exa/mcp-client.d.ts +10 -1
  5. package/dist/types/exec/bash-executor.d.ts +45 -12
  6. package/dist/types/extensibility/extensions/types.d.ts +16 -2
  7. package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
  8. package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
  9. package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
  10. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
  11. package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
  12. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
  13. package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
  14. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  15. package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
  16. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +23 -3
  17. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  18. package/dist/types/gjc-runtime/team-runtime.d.ts +25 -0
  19. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
  20. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +10 -0
  21. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +37 -80
  22. package/dist/types/modes/acp/acp-agent.d.ts +2 -19
  23. package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
  24. package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
  25. package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
  26. package/dist/types/modes/components/session-observer-overlay.d.ts +18 -0
  27. package/dist/types/modes/components/settings-selector.d.ts +9 -0
  28. package/dist/types/modes/components/status-line/gh.d.ts +5 -0
  29. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
  30. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  31. package/dist/types/modes/controllers/event-controller.d.ts +1 -1
  32. package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
  33. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  34. package/dist/types/modes/interactive-mode.d.ts +9 -0
  35. package/dist/types/modes/theme/theme.d.ts +3 -1
  36. package/dist/types/modes/types.d.ts +32 -0
  37. package/dist/types/reminders/star-reminder.d.ts +3 -13
  38. package/dist/types/runtime-mcp/manager.d.ts +1 -0
  39. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  40. package/dist/types/sdk/broker/transport.d.ts +2 -0
  41. package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
  42. package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -1
  43. package/dist/types/sdk/bus/config.d.ts +6 -2
  44. package/dist/types/sdk/bus/index.d.ts +29 -0
  45. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +8 -1
  46. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  47. package/dist/types/sdk/bus/reconciliation-store.d.ts +7 -3
  48. package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
  49. package/dist/types/sdk/bus/rich-render.d.ts +2 -0
  50. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
  51. package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
  52. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -4
  53. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
  54. package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
  55. package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
  56. package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
  57. package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
  58. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  59. package/dist/types/sdk/prompt-status.d.ts +23 -3
  60. package/dist/types/sdk/session.d.ts +6 -0
  61. package/dist/types/session/agent-session.d.ts +40 -2
  62. package/dist/types/session/artifacts.d.ts +8 -0
  63. package/dist/types/session/blob-store.d.ts +57 -4
  64. package/dist/types/session/internal/managed-session-storage.d.ts +30 -4
  65. package/dist/types/session/session-manager.d.ts +23 -6
  66. package/dist/types/session/session-storage.d.ts +4 -0
  67. package/dist/types/session/streaming-output.d.ts +36 -7
  68. package/dist/types/setup/credential-import.d.ts +12 -0
  69. package/dist/types/skill-state/active-state.d.ts +12 -0
  70. package/dist/types/skill-state/workflow-hud.d.ts +6 -0
  71. package/dist/types/task/executor.d.ts +3 -2
  72. package/dist/types/task/index.d.ts +7 -1
  73. package/dist/types/task/output-manager.d.ts +1 -0
  74. package/dist/types/task/provider-retry-status.d.ts +8 -0
  75. package/dist/types/task/receipt.d.ts +9 -1
  76. package/dist/types/task/types.d.ts +23 -0
  77. package/dist/types/tools/ask-answer-registry.d.ts +2 -2
  78. package/dist/types/tools/bash-interactive.d.ts +4 -1
  79. package/dist/types/tools/bash.d.ts +3 -1
  80. package/dist/types/tools/image-gen.d.ts +4 -0
  81. package/dist/types/tools/index.d.ts +6 -0
  82. package/dist/types/tools/output-meta.d.ts +23 -2
  83. package/dist/types/tools/path-utils.d.ts +1 -0
  84. package/dist/types/tools/read.d.ts +0 -6
  85. package/dist/types/tools/review.d.ts +5 -7
  86. package/dist/types/tools/subagent.d.ts +2 -0
  87. package/dist/types/utils/gh.d.ts +13 -0
  88. package/dist/types/utils/image-resize.d.ts +1 -0
  89. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  90. package/dist/types/web/search/providers/searxng.d.ts +7 -0
  91. package/dist/types/web/search/providers/xai.d.ts +2 -0
  92. package/package.json +10 -8
  93. package/scripts/acp-conformance-agent.ts +218 -0
  94. package/scripts/capture-gjc-bundle-settings.ts +306 -0
  95. package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
  96. package/scripts/generate-sdk-operation-inventory.ts +5 -0
  97. package/scripts/resident-memory-bench.ts +1341 -0
  98. package/scripts/run-acp-conformance.ts +290 -0
  99. package/src/async/job-manager.ts +99 -39
  100. package/src/cli/fast-help.ts +1 -1
  101. package/src/cli/plugin-cli.ts +176 -18
  102. package/src/commands/team.ts +19 -1
  103. package/src/config/file-lock.ts +11 -2
  104. package/src/config/settings-schema.ts +53 -2
  105. package/src/coordinator-mcp/question-gate-codec.ts +1 -1
  106. package/src/coordinator-mcp/server.ts +5 -12
  107. package/src/debug/index.ts +42 -17
  108. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
  109. package/src/defaults/gjc/skills/ralplan/SKILL.md +25 -20
  110. package/src/defaults/gjc/skills/team/SKILL.md +2 -0
  111. package/src/defaults/gjc/skills/ultragoal/SKILL.md +39 -26
  112. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
  113. package/src/defaults/gjc-defaults.ts +4 -4
  114. package/src/exa/mcp-client.ts +12 -3
  115. package/src/exec/bash-executor.ts +144 -27
  116. package/src/extensibility/extensions/runner.ts +15 -0
  117. package/src/extensibility/extensions/types.ts +18 -1
  118. package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
  119. package/src/extensibility/gjc-plugins/index.ts +19 -2
  120. package/src/extensibility/gjc-plugins/installer.ts +189 -50
  121. package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
  122. package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
  123. package/src/extensibility/gjc-plugins/observability.ts +3 -3
  124. package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
  125. package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
  126. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
  127. package/src/extensibility/gjc-plugins/schema.ts +84 -12
  128. package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
  129. package/src/extensibility/gjc-plugins/types.ts +141 -0
  130. package/src/extensibility/gjc-plugins/validation.ts +9 -0
  131. package/src/gjc-runtime/launch-tmux.ts +25 -6
  132. package/src/gjc-runtime/launch-worktree.ts +103 -1
  133. package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
  134. package/src/gjc-runtime/managed-owner-admission.ts +1 -1
  135. package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
  136. package/src/gjc-runtime/ralplan-runtime.ts +491 -27
  137. package/src/gjc-runtime/team-launch.ts +8 -0
  138. package/src/gjc-runtime/team-runtime.ts +140 -32
  139. package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
  140. package/src/gjc-runtime/tmux-owner-isolation.ts +10 -0
  141. package/src/gjc-runtime/tmux-sessions.ts +58 -13
  142. package/src/gjc-runtime/ultragoal-evidence.ts +6 -74
  143. package/src/gjc-runtime/ultragoal-runtime.ts +635 -612
  144. package/src/gjc-runtime/workflow-manifest.generated.json +37 -99
  145. package/src/gjc-runtime/workflow-manifest.ts +21 -70
  146. package/src/internal-urls/docs-index.generated.ts +16 -16
  147. package/src/modes/DESIGN.md +104 -0
  148. package/src/modes/acp/acp-agent.ts +298 -186
  149. package/src/modes/components/gjc-bundle-settings.ts +508 -0
  150. package/src/modes/components/irc-sidebar.ts +30 -1
  151. package/src/modes/components/notifications-settings-editor.ts +12 -1
  152. package/src/modes/components/oauth-selector.ts +68 -30
  153. package/src/modes/components/session-observer-overlay.ts +305 -86
  154. package/src/modes/components/settings-selector.ts +56 -5
  155. package/src/modes/components/status-line/gh.ts +16 -0
  156. package/src/modes/components/tool-status-header.ts +3 -15
  157. package/src/modes/components/transcript-viewer-overlay.ts +99 -24
  158. package/src/modes/controllers/command-controller.ts +42 -34
  159. package/src/modes/controllers/event-controller.ts +27 -16
  160. package/src/modes/controllers/extension-ui-controller.ts +193 -34
  161. package/src/modes/controllers/selector-controller.ts +160 -22
  162. package/src/modes/interactive-mode.ts +189 -62
  163. package/src/modes/jobs-observer.ts +1 -1
  164. package/src/modes/runtime-init.ts +3 -0
  165. package/src/modes/theme/theme.ts +11 -9
  166. package/src/modes/types.ts +88 -0
  167. package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
  168. package/src/prompts/system/eager-todo.md +8 -4
  169. package/src/prompts/system/system-prompt.md +11 -4
  170. package/src/prompts/tools/bash.md +5 -5
  171. package/src/prompts/tools/search-tool-bm25.md +5 -0
  172. package/src/prompts/tools/task.md +1 -1
  173. package/src/reminders/star-reminder.ts +4 -42
  174. package/src/runtime-mcp/manager.ts +19 -4
  175. package/src/sdk/acp/adapter.ts +22 -6
  176. package/src/sdk/broker/lifecycle.ts +18 -13
  177. package/src/sdk/broker/transport.ts +35 -3
  178. package/src/sdk/bus/attachment-registry.ts +5 -1
  179. package/src/sdk/bus/chat-daemon-cli.ts +4 -39
  180. package/src/sdk/bus/chat-daemon-control.ts +7 -3
  181. package/src/sdk/bus/config.ts +18 -2
  182. package/src/sdk/bus/index.ts +661 -126
  183. package/src/sdk/bus/kind-aware-reconciliation.ts +148 -73
  184. package/src/sdk/bus/lifecycle-commands.ts +4 -1
  185. package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
  186. package/src/sdk/bus/reconciliation-store.ts +95 -16
  187. package/src/sdk/bus/rich-draft.ts +1 -1
  188. package/src/sdk/bus/rich-render.ts +2 -2
  189. package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
  190. package/src/sdk/bus/telegram-cli.ts +33 -8
  191. package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
  192. package/src/sdk/bus/telegram-daemon-contract.ts +12 -4
  193. package/src/sdk/bus/telegram-daemon.ts +916 -23
  194. package/src/sdk/bus/telegram-reference.ts +40 -3
  195. package/src/sdk/bus/threaded-render.ts +48 -3
  196. package/src/sdk/bus/topic-registry.ts +20 -0
  197. package/src/sdk/host/query/revision-store.ts +36 -2
  198. package/src/sdk/host/reverse-leases.ts +44 -7
  199. package/src/sdk/prompt-status.ts +16 -3
  200. package/src/sdk/protocol/operation-inventory.generated.json +33 -0
  201. package/src/sdk/session.ts +89 -9
  202. package/src/session/agent-session.ts +844 -173
  203. package/src/session/artifacts.ts +22 -0
  204. package/src/session/auth-broker-config.ts +10 -3
  205. package/src/session/blob-store.ts +915 -24
  206. package/src/session/internal/managed-session-scope.ts +200 -111
  207. package/src/session/internal/managed-session-storage.ts +492 -62
  208. package/src/session/session-manager.ts +1614 -435
  209. package/src/session/session-storage.ts +38 -45
  210. package/src/session/streaming-output.ts +238 -68
  211. package/src/setup/credential-import.ts +75 -13
  212. package/src/skill-state/active-state.ts +32 -0
  213. package/src/skill-state/workflow-hud.ts +17 -0
  214. package/src/skill-state/workflow-mutation-guard.ts +496 -2
  215. package/src/task/executor.ts +122 -39
  216. package/src/task/index.ts +280 -45
  217. package/src/task/output-manager.ts +24 -14
  218. package/src/task/provider-retry-status.ts +16 -0
  219. package/src/task/receipt.ts +34 -8
  220. package/src/task/render.ts +6 -2
  221. package/src/task/types.ts +108 -0
  222. package/src/tools/ask-answer-registry.ts +12 -6
  223. package/src/tools/ask.ts +86 -55
  224. package/src/tools/ast-edit.ts +1 -0
  225. package/src/tools/ast-grep.ts +1 -0
  226. package/src/tools/bash-interactive.ts +8 -3
  227. package/src/tools/bash.ts +503 -60
  228. package/src/tools/find.ts +1 -0
  229. package/src/tools/image-gen.ts +29 -5
  230. package/src/tools/index.ts +6 -0
  231. package/src/tools/output-meta.ts +108 -14
  232. package/src/tools/path-utils.ts +3 -2
  233. package/src/tools/read.ts +18 -0
  234. package/src/tools/review.ts +5 -7
  235. package/src/tools/search.ts +1 -0
  236. package/src/tools/subagent-render.ts +6 -2
  237. package/src/tools/subagent.ts +6 -0
  238. package/src/tools/telegram-send.ts +6 -3
  239. package/src/utils/gh.ts +39 -0
  240. package/src/utils/image-resize.ts +3 -0
  241. package/src/web/search/providers/anthropic.ts +4 -4
  242. package/src/web/search/providers/kimi.ts +15 -5
  243. package/src/web/search/providers/searxng.ts +47 -4
  244. package/src/web/search/providers/xai.ts +7 -2
  245. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
  246. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
  247. package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
package/src/task/index.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  * - Progress tracking via JSON events
13
13
  * - Session artifacts for debugging
14
14
  */
15
+ import { randomUUID } from "node:crypto";
15
16
  import * as fs from "node:fs/promises";
16
17
  import * as os from "node:os";
17
18
  import path from "node:path";
@@ -19,7 +20,7 @@ import type { AgentTool, AgentToolResult, AgentToolUpdateCallback } from "@gajae
19
20
  import type { Model, Usage } from "@gajae-code/ai";
20
21
  import { $pickenv, prompt, Snowflake } from "@gajae-code/utils";
21
22
  import type { ToolSession } from "..";
22
- import { AsyncJobManager, OwnerSubagentShutdownError, type ResumeRunner } from "../async";
23
+ import { AsyncJobManager, OwnerSubagentShutdownError, type ResumeRunner, type SubagentRunOutcome } from "../async";
23
24
  import { resolveAgentModelPatterns } from "../config/model-resolver";
24
25
  import type { Theme } from "../modes/theme/theme";
25
26
  import planModeSubagentPrompt from "../prompts/system/plan-mode-subagent.md" with { type: "text" };
@@ -53,6 +54,7 @@ import {
53
54
  resolveTaskRepositoryBinding,
54
55
  } from "../gjc-runtime/repository-binding";
55
56
  import { initializeLocalRoot, type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
57
+ import { ArtifactManager } from "../session/artifacts";
56
58
  import { generateCommitMessage } from "../utils/commit-message-generator";
57
59
  import * as git from "../utils/git";
58
60
  import { discoverAgents, filterVisibleAgents, getAgent } from "./discovery";
@@ -88,6 +90,7 @@ interface TaskResumeDescriptor {
88
90
  params: TaskParams;
89
91
  task: TaskItem & { id: string };
90
92
  sessionFile: string | null;
93
+ durableOutputAllowed?: boolean;
91
94
  forkContextSeed?: ForkContextSeed;
92
95
  agentSource: AgentDefinition["source"];
93
96
  }
@@ -98,6 +101,23 @@ function isTaskResumeDescriptor(value: unknown): value is TaskResumeDescriptor {
98
101
  function renderTaskAssignment(assignment: string, simpleMode: TaskSimpleMode): string {
99
102
  return renderSubagentUserPrompt(assignment, simpleMode === "independent");
100
103
  }
104
+
105
+ export function subagentRunOutcomeFromSingleResult(
106
+ finalText: string,
107
+ singleResult: Pick<SingleResult, "aborted" | "exitCode" | "paused" | "setupFailure"> | undefined,
108
+ ): string | SubagentRunOutcome {
109
+ if (singleResult?.paused) return { kind: "paused" };
110
+ if (singleResult && ((singleResult.aborted ?? false) || singleResult.exitCode !== 0)) {
111
+ return {
112
+ kind: "failed",
113
+ text: finalText,
114
+ ...(singleResult.setupFailure && !singleResult.aborted
115
+ ? { setupFailureSummary: singleResult.setupFailure.summary }
116
+ : {}),
117
+ };
118
+ }
119
+ return finalText;
120
+ }
101
121
  function createUsageTotals(): Usage {
102
122
  return {
103
123
  input: 0,
@@ -403,6 +423,37 @@ export function resolveForkContextMaxTokens(configured: number, model: Model | u
403
423
  // ═══════════════════════════════════════════════════════════════════════════
404
424
  // Tool Class
405
425
  // ═══════════════════════════════════════════════════════════════════════════
426
+ interface SessionLifetimeArtifactsState {
427
+ dir?: string;
428
+ manager?: ArtifactManager;
429
+ ensurePromise?: Promise<string | null>;
430
+ outputPrefix: string;
431
+ authorized: boolean;
432
+ cleanupRegistered: boolean;
433
+ originalGetArtifactsDir?: ToolSession["getArtifactsDir"];
434
+ originalGetAuthorizedArtifactsDirs?: ToolSession["getAuthorizedArtifactsDirs"];
435
+ originalGetArtifactManager?: ToolSession["getArtifactManager"];
436
+ originalAgentOutputManager?: AgentOutputManager;
437
+ installedGetArtifactsDir?: ToolSession["getArtifactsDir"];
438
+ installedGetAuthorizedArtifactsDirs?: ToolSession["getAuthorizedArtifactsDirs"];
439
+ installedGetArtifactManager?: ToolSession["getArtifactManager"];
440
+ installedAgentOutputManager?: AgentOutputManager;
441
+ }
442
+
443
+ const sessionLifetimeArtifacts = new WeakMap<ToolSession, SessionLifetimeArtifactsState>();
444
+
445
+ function sessionLifetimeArtifactsState(session: ToolSession): SessionLifetimeArtifactsState {
446
+ let state = sessionLifetimeArtifacts.get(session);
447
+ if (!state) {
448
+ state = {
449
+ authorized: false,
450
+ cleanupRegistered: false,
451
+ outputPrefix: `0-Session${randomUUID().replaceAll("-", "")}`,
452
+ };
453
+ sessionLifetimeArtifacts.set(session, state);
454
+ }
455
+ return state;
456
+ }
406
457
 
407
458
  /**
408
459
  * Task tool - Delegate tasks to specialized agents.
@@ -419,6 +470,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
419
470
  readonly renderResult = renderResult;
420
471
  readonly #discoveredAgents: AgentDefinition[];
421
472
  readonly #blockedAgent: string | undefined;
473
+ /**
474
+ * Session-lifetime durable artifact state is shared atomically by every
475
+ * TaskTool created for the same parent ToolSession.
476
+ */
422
477
 
423
478
  get parameters(): TaskToolSchemaInstance {
424
479
  const isolationEnabled = this.session.settings.get("task.isolation.mode") !== "none";
@@ -461,6 +516,172 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
461
516
  return this.session.settings.get("task.simple");
462
517
  }
463
518
 
519
+ /**
520
+ * Ensure a session-lifetime artifact directory when the parent has no session file.
521
+ * Returns null only when durable allocation fails (mkdir error) — callers must not
522
+ * advertise agent:// URIs without a successful allocation.
523
+ */
524
+ async #ensureSessionLifetimeArtifacts(): Promise<{ dir: string; manager: ArtifactManager } | null> {
525
+ const state = sessionLifetimeArtifactsState(this.session);
526
+ if (state.authorized && state.dir && state.manager) return { dir: state.dir, manager: state.manager };
527
+
528
+ const sessionArtifactsDir = this.session.getArtifactsDir?.() ?? null;
529
+ if (sessionArtifactsDir) {
530
+ const existingManager = this.session.getArtifactManager?.() ?? null;
531
+ if (existingManager && path.resolve(existingManager.dir) !== path.resolve(sessionArtifactsDir)) return null;
532
+ const manager = existingManager ?? new ArtifactManager(sessionArtifactsDir);
533
+ state.dir = sessionArtifactsDir;
534
+ state.manager = manager;
535
+ if (!(await this.#authorizeSessionLifetimeArtifacts(state, sessionArtifactsDir, manager, false))) return null;
536
+ return { dir: sessionArtifactsDir, manager };
537
+ }
538
+ if (!this.session.registerSessionCleanup) return null;
539
+
540
+ state.ensurePromise ??= this.#allocateSessionLifetimeArtifacts(state);
541
+ const dir = await state.ensurePromise;
542
+ if (!dir || !state.manager) {
543
+ state.ensurePromise = undefined;
544
+ return null;
545
+ }
546
+ return { dir, manager: state.manager };
547
+ }
548
+
549
+ async #allocateSessionLifetimeArtifacts(state: SessionLifetimeArtifactsState): Promise<string | null> {
550
+ let dir: string;
551
+ try {
552
+ dir = await fs.mkdtemp(path.join(os.tmpdir(), "gjc-task-session-"));
553
+ } catch {
554
+ state.dir = undefined;
555
+ state.manager = undefined;
556
+ return null;
557
+ }
558
+ state.dir = dir;
559
+ state.manager = new ArtifactManager(dir);
560
+ if (!(await this.#authorizeSessionLifetimeArtifacts(state, dir, state.manager, true))) return null;
561
+ return dir;
562
+ }
563
+
564
+ /**
565
+ * Expose the durable root on the parent ToolSession so subsequent parent-side
566
+ * tools and same-session descendants can resolve agent:// without registry-wide
567
+ * enumeration (#3302 scoped authorization).
568
+ */
569
+ async #authorizeSessionLifetimeArtifacts(
570
+ state: SessionLifetimeArtifactsState,
571
+ dir: string,
572
+ manager: ArtifactManager,
573
+ owned: boolean,
574
+ ): Promise<boolean> {
575
+ if (state.authorized) return true;
576
+ state.originalGetArtifactsDir = this.session.getArtifactsDir;
577
+ state.originalGetAuthorizedArtifactsDirs = this.session.getAuthorizedArtifactsDirs;
578
+ state.originalGetArtifactManager = this.session.getArtifactManager;
579
+ state.originalAgentOutputManager = this.session.agentOutputManager;
580
+
581
+ state.installedGetArtifactsDir = () => state.originalGetArtifactsDir?.() ?? dir;
582
+ state.installedGetAuthorizedArtifactsDirs = () => {
583
+ const dirs: string[] = [];
584
+ const add = (candidate: string | null | undefined) => {
585
+ if (!candidate) return;
586
+ const normalized = path.resolve(candidate);
587
+ if (!dirs.includes(normalized)) dirs.push(normalized);
588
+ };
589
+ for (const authorized of state.originalGetAuthorizedArtifactsDirs?.() ?? []) add(authorized);
590
+ add(dir);
591
+ return dirs;
592
+ };
593
+ state.installedGetArtifactManager = () => manager;
594
+ if (owned || !this.session.agentOutputManager) {
595
+ state.installedAgentOutputManager = new AgentOutputManager(() => this.session.getArtifactsDir?.() ?? null, {
596
+ parentPrefix: owned ? state.outputPrefix : undefined,
597
+ getAuthorizedArtifactsDirs: () => this.session.getAuthorizedArtifactsDirs?.() ?? [],
598
+ });
599
+ }
600
+
601
+ let cleanupRan = false;
602
+ const cleanup = async () => {
603
+ cleanupRan = true;
604
+ try {
605
+ if (owned) await fs.rm(dir, { recursive: true, force: true });
606
+ } finally {
607
+ if (this.session.getArtifactsDir === state.installedGetArtifactsDir)
608
+ this.session.getArtifactsDir = state.originalGetArtifactsDir;
609
+ if (this.session.getAuthorizedArtifactsDirs === state.installedGetAuthorizedArtifactsDirs)
610
+ this.session.getAuthorizedArtifactsDirs = state.originalGetAuthorizedArtifactsDirs;
611
+ if (this.session.getArtifactManager === state.installedGetArtifactManager)
612
+ this.session.getArtifactManager = state.originalGetArtifactManager;
613
+ if (this.session.agentOutputManager === state.installedAgentOutputManager)
614
+ this.session.agentOutputManager = state.originalAgentOutputManager;
615
+ sessionLifetimeArtifacts.delete(this.session);
616
+ }
617
+ };
618
+ if (owned) {
619
+ const registerCleanup = this.session.registerSessionCleanup;
620
+ if (!registerCleanup) {
621
+ state.dir = undefined;
622
+ state.manager = undefined;
623
+ state.ensurePromise = undefined;
624
+ sessionLifetimeArtifacts.delete(this.session);
625
+ await fs.rm(dir, { recursive: true, force: true }).catch(() => {});
626
+ return false;
627
+ }
628
+ try {
629
+ registerCleanup(cleanup);
630
+ } catch {
631
+ state.dir = undefined;
632
+ state.manager = undefined;
633
+ state.ensurePromise = undefined;
634
+ sessionLifetimeArtifacts.delete(this.session);
635
+ await fs.rm(dir, { recursive: true, force: true }).catch(() => {});
636
+ return false;
637
+ }
638
+ if (cleanupRan) return false;
639
+ state.cleanupRegistered = true;
640
+ }
641
+
642
+ state.authorized = true;
643
+ this.session.getArtifactsDir = state.installedGetArtifactsDir;
644
+ this.session.getAuthorizedArtifactsDirs = state.installedGetAuthorizedArtifactsDirs;
645
+ this.session.getArtifactManager = state.installedGetArtifactManager;
646
+ if (state.installedAgentOutputManager) this.session.agentOutputManager = state.installedAgentOutputManager;
647
+ return true;
648
+ }
649
+
650
+ /**
651
+ * Resolve the effective artifacts directory for this task batch.
652
+ * File-backed sessions use the session artifacts path; in-memory parents use
653
+ * the session-lifetime durable temp root when allocation succeeds.
654
+ */
655
+ async #resolveEffectiveArtifactsDir(): Promise<{
656
+ sessionArtifactsDir: string | null;
657
+ durableArtifactsDir: string | null;
658
+ effectiveArtifactsDir: string | undefined;
659
+ parentArtifactManager: ArtifactManager | undefined;
660
+ }> {
661
+ const sessionFile = this.session.getSessionFile();
662
+ const sessionArtifactsDir = sessionFile ? sessionFile.slice(0, -6) : null;
663
+ if (sessionArtifactsDir) {
664
+ const candidateManager = this.session.getArtifactManager?.() ?? undefined;
665
+ const parentArtifactManager =
666
+ candidateManager && path.resolve(candidateManager.dir) === path.resolve(sessionArtifactsDir)
667
+ ? candidateManager
668
+ : undefined;
669
+ return {
670
+ sessionArtifactsDir,
671
+ durableArtifactsDir: null,
672
+ effectiveArtifactsDir: sessionArtifactsDir,
673
+ parentArtifactManager,
674
+ };
675
+ }
676
+ const durable = await this.#ensureSessionLifetimeArtifacts();
677
+ return {
678
+ sessionArtifactsDir: null,
679
+ durableArtifactsDir: durable?.dir ?? null,
680
+ effectiveArtifactsDir: durable?.dir,
681
+ parentArtifactManager: durable?.manager,
682
+ };
683
+ }
684
+
464
685
  /**
465
686
  * Create a TaskTool instance.
466
687
  * Repository authority is captured from session cwd *before* agent discovery so
@@ -572,6 +793,25 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
572
793
  };
573
794
  }
574
795
 
796
+ // Prefer file-backed session artifacts; otherwise allocate a session-lifetime
797
+ // durable root so detached outputs remain readable for the parent session.
798
+ // Resolve before ID allocation so agentOutputManager can scan the durable root.
799
+ const { effectiveArtifactsDir: batchArtifactsDir, parentArtifactManager: asyncParentArtifactManager } =
800
+ await this.#resolveEffectiveArtifactsDir();
801
+ let externalTaskSessionsDir: string | undefined;
802
+ if (!batchArtifactsDir) {
803
+ // Durable allocation failed: keep child session jsonl under local:// only.
804
+ // Do not advertise agent:// URIs without durable output backing.
805
+ const asyncLocalOptions: LocalProtocolOptions = {
806
+ getArtifactsDir: this.session.getArtifactsDir ?? (() => null),
807
+ isManagedDestination: this.session.isManagedSessionDestination,
808
+ getSessionId: this.session.getSessionId ?? (() => null),
809
+ };
810
+ await initializeLocalRoot(asyncLocalOptions);
811
+ externalTaskSessionsDir = resolveLocalUrlToPath("local://subagents/sessions", asyncLocalOptions);
812
+ await fs.mkdir(externalTaskSessionsDir, { recursive: true, mode: 0o700 });
813
+ }
814
+
575
815
  const outputManager =
576
816
  this.session.agentOutputManager ?? new AgentOutputManager(this.session.getArtifactsDir ?? (() => null));
577
817
  const uniqueIds = await outputManager.allocateBatch(taskItems.map(t => t.id));
@@ -651,19 +891,19 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
651
891
  resumeMessage: message,
652
892
  sessionFiles: new Map([[descriptor.task.id, descriptor.sessionFile]]),
653
893
  suppressRoiReconciliation: true,
894
+ ...(descriptor.durableOutputAllowed === true
895
+ ? {}
896
+ : {
897
+ persistence: {
898
+ effectiveArtifactsDir: undefined,
899
+ parentArtifactManager: undefined,
900
+ },
901
+ }),
654
902
  },
655
903
  );
656
904
  const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
657
905
  const singleResult = result.details?.results[0];
658
- if (singleResult?.paused) return { kind: "paused" };
659
- // A resumed subprocess that aborted or exited non-zero is a failed
660
- // resume, not a completed one. Throw the rendered failure summary
661
- // (finalText) so the leg is reported failed instead of being
662
- // returned as successful continuation.
663
- if (singleResult && ((singleResult.aborted ?? false) || singleResult.exitCode !== 0)) {
664
- throw new Error(finalText);
665
- }
666
- return finalText;
906
+ return subagentRunOutcomeFromSingleResult(finalText, singleResult);
667
907
  },
668
908
  {
669
909
  id: `${descriptor.task.id}-resume-${Snowflake.next()}`,
@@ -705,20 +945,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
705
945
  });
706
946
  };
707
947
  const frozenForkSeeds = new Map<string, ForkContextSeed>();
708
- const asyncParentArtifactManager = this.session.getArtifactManager?.() ?? undefined;
709
- const parentSessionFileForBatch = this.session.getSessionFile();
710
- const batchArtifactsDir = parentSessionFileForBatch ? parentSessionFileForBatch.slice(0, -6) : null;
711
- let externalTaskSessionsDir: string | undefined;
712
- if (!batchArtifactsDir) {
713
- const asyncLocalOptions: LocalProtocolOptions = {
714
- getArtifactsDir: this.session.getArtifactsDir ?? (() => null),
715
- isManagedDestination: this.session.isManagedSessionDestination,
716
- getSessionId: this.session.getSessionId ?? (() => null),
717
- };
718
- await initializeLocalRoot(asyncLocalOptions);
719
- externalTaskSessionsDir = resolveLocalUrlToPath("local://subagents/sessions", asyncLocalOptions);
720
- await fs.mkdir(externalTaskSessionsDir, { recursive: true, mode: 0o700 });
721
- }
722
948
 
723
949
  for (let i = 0; i < taskItems.length; i++) {
724
950
  const taskItem = taskItems[i];
@@ -784,6 +1010,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
784
1010
  {
785
1011
  sessionFiles: new Map([[uniqueId, subtaskSessionFile]]),
786
1012
  suppressRoiReconciliation: true,
1013
+ persistence: {
1014
+ effectiveArtifactsDir: batchArtifactsDir,
1015
+ parentArtifactManager: asyncParentArtifactManager,
1016
+ },
787
1017
  },
788
1018
  );
789
1019
  const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
@@ -809,6 +1039,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
809
1039
  }
810
1040
  : undefined;
811
1041
  progress.retryState = undefined;
1042
+ progress.setupFailure = singleResult?.setupFailure;
812
1043
  }
813
1044
  completedJobs += 1;
814
1045
  if (singleResult && ((singleResult.aborted ?? false) || singleResult.exitCode !== 0)) {
@@ -831,10 +1062,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
831
1062
  `Background task batch complete: ${completedJobs}/${taskItems.length} finished.`,
832
1063
  );
833
1064
  }
834
- if (singleResult?.paused) {
835
- return { kind: "paused" };
836
- }
837
- return finalText;
1065
+ return subagentRunOutcomeFromSingleResult(finalText, singleResult);
838
1066
  } catch (error) {
839
1067
  if (progress) {
840
1068
  progress.status = "failed";
@@ -895,6 +1123,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
895
1123
  params,
896
1124
  task: { ...taskItem, id: uniqueId },
897
1125
  sessionFile: subtaskSessionFile,
1126
+ durableOutputAllowed: Boolean(batchArtifactsDir),
898
1127
  forkContextSeed: frozenForkSeed,
899
1128
  agentSource: fallbackAgentSource,
900
1129
  } satisfies TaskResumeDescriptor,
@@ -991,6 +1220,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
991
1220
  runMode?: "initial" | "resume" | "message";
992
1221
  resumeMessage?: string;
993
1222
  sessionFiles?: ReadonlyMap<string, string | null>;
1223
+ persistence?: {
1224
+ effectiveArtifactsDir: string | undefined;
1225
+ parentArtifactManager: ArtifactManager | undefined;
1226
+ };
994
1227
  /**
995
1228
  * Set for per-child async runs: the spawnPlan is carried for gate
996
1229
  * consistency, but batch-level ROI reconciliation must not be computed
@@ -1240,11 +1473,12 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1240
1473
 
1241
1474
  const preferredIsolationBackend = parseIsolationMode(isolationMode);
1242
1475
 
1243
- // Derive artifacts directory
1244
- const sessionFile = this.session.getSessionFile();
1245
- const artifactsDir = sessionFile ? sessionFile.slice(0, -6) : null;
1246
- const tempArtifactsDir = artifactsDir ? null : path.join(os.tmpdir(), `gjc-task-${Snowflake.next()}`);
1247
- const effectiveArtifactsDir = artifactsDir || tempArtifactsDir!;
1476
+ // File-backed session artifacts, or a session-lifetime durable temp root for
1477
+ // in-memory parents. Never use a per-task temp dir that is deleted on return —
1478
+ // advertised agent:// URIs must remain readable for the parent session lifetime.
1479
+ const { effectiveArtifactsDir, parentArtifactManager } =
1480
+ executionOverrides?.persistence ?? (await this.#resolveEffectiveArtifactsDir());
1481
+ const hasDurableOutputBacking = Boolean(effectiveArtifactsDir);
1248
1482
 
1249
1483
  // Share the parent session's local:// root with subagents so they read/write the same scratch space
1250
1484
  const localProtocolOptions: LocalProtocolOptions = {
@@ -1253,9 +1487,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1253
1487
  getSessionId: this.session.getSessionId ?? (() => null),
1254
1488
  };
1255
1489
 
1256
- // Subagents adopt the parent's ArtifactManager so artifact IDs are unique
1257
- // across the whole tree and outputs land flat in the parent's dir.
1258
- const parentArtifactManager = this.session.getArtifactManager?.() ?? undefined;
1490
+ // Subagents adopt the parent's ArtifactManager (including session-lifetime
1491
+ // durable managers) so artifact IDs are unique and agent:// resolves under
1492
+ // scoped authorization for the whole tree.
1259
1493
 
1260
1494
  // Initialize progress tracking
1261
1495
  const progressMap = new Map<number, AgentProgress>();
@@ -1461,7 +1695,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1461
1695
  thinkingLevel: thinkingLevelOverride,
1462
1696
  outputSchema: effectiveOutputSchema,
1463
1697
  sessionFile: taskSessionFile,
1464
- persistArtifacts: !!artifactsDir,
1698
+ persistArtifacts: hasDurableOutputBacking,
1465
1699
  artifactsDir: effectiveArtifactsDir,
1466
1700
  managedPersistence,
1467
1701
  contextFile: contextFilePath,
@@ -1481,6 +1715,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1481
1715
  agentRegistry: this.session.agentRegistry,
1482
1716
  settings: this.session.settings,
1483
1717
  inheritedServiceTier: this.session.serviceTier,
1718
+ isFastForSubagentProvider: provider => this.session.isFastForSubagentProvider?.(provider) ?? false,
1484
1719
  contextFiles,
1485
1720
  skills: availableSkills,
1486
1721
  autoloadSkills: resolvedAutoloadSkills,
@@ -1534,7 +1769,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1534
1769
  thinkingLevel: thinkingLevelOverride,
1535
1770
  outputSchema: effectiveOutputSchema,
1536
1771
  sessionFile: taskSessionFile,
1537
- persistArtifacts: !!artifactsDir,
1772
+ persistArtifacts: hasDurableOutputBacking,
1538
1773
  artifactsDir: effectiveArtifactsDir,
1539
1774
  managedPersistence,
1540
1775
  contextFile: contextFilePath,
@@ -1554,6 +1789,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1554
1789
  agentRegistry: this.session.agentRegistry,
1555
1790
  settings: this.session.settings,
1556
1791
  inheritedServiceTier: this.session.serviceTier,
1792
+ isFastForSubagentProvider: provider => this.session.isFastForSubagentProvider?.(provider) ?? false,
1557
1793
  contextFiles,
1558
1794
  skills: availableSkills,
1559
1795
  autoloadSkills: resolvedAutoloadSkills,
@@ -1694,7 +1930,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1694
1930
  abortReason: "Cancelled before start",
1695
1931
  };
1696
1932
  });
1697
- if (!artifactsDir) {
1933
+ // Only advertise agent:// when outputs land on durable backing (file-backed
1934
+ // session artifacts or the session-lifetime durable temp root). Ephemeral
1935
+ // or failed allocation must not emit dead URIs (#3471 / #295).
1936
+ if (!hasDurableOutputBacking) {
1698
1937
  for (const result of results) {
1699
1938
  delete result.outputMeta;
1700
1939
  delete result.outputPath;
@@ -1904,12 +2143,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1904
2143
  mergeSummary,
1905
2144
  });
1906
2145
 
1907
- // Cleanup temp directory if used
1908
- const shouldCleanupTempArtifacts =
1909
- tempArtifactsDir && (!isIsolated || changesApplied === true || changesApplied === null);
1910
- if (shouldCleanupTempArtifacts) {
1911
- await fs.rm(tempArtifactsDir, { recursive: true, force: true });
1912
- }
2146
+ // Session-lifetime durable dirs must outlive this return so parent +
2147
+ // same-session descendants can resolve agent://. Do not rm them here.
1913
2148
 
1914
2149
  const details: TaskToolDetails = {
1915
2150
  projectAgentsDir,
@@ -29,10 +29,15 @@ export class AgentOutputManager {
29
29
  #initPromise: Promise<void> | undefined;
30
30
  readonly #getArtifactsDir: () => string | null;
31
31
  readonly #parentPrefix: string | undefined;
32
+ readonly #getAuthorizedArtifactsDirs: (() => readonly string[]) | undefined;
32
33
 
33
- constructor(getArtifactsDir: () => string | null, options?: { parentPrefix?: string }) {
34
+ constructor(
35
+ getArtifactsDir: () => string | null,
36
+ options?: { parentPrefix?: string; getAuthorizedArtifactsDirs?: () => readonly string[] },
37
+ ) {
34
38
  this.#getArtifactsDir = getArtifactsDir;
35
39
  this.#parentPrefix = options?.parentPrefix;
40
+ this.#getAuthorizedArtifactsDirs = options?.getAuthorizedArtifactsDirs;
36
41
  }
37
42
 
38
43
  /**
@@ -53,24 +58,29 @@ export class AgentOutputManager {
53
58
  * This ensures we don't overwrite outputs when resuming a session.
54
59
  */
55
60
  async #scanExistingOutputs(): Promise<void> {
56
- const dir = this.#getArtifactsDir();
57
- if (!dir) return;
58
-
59
- let files: string[];
60
- try {
61
- files = await fs.readdir(dir);
62
- } catch {
63
- return; // Directory doesn't exist yet
64
- }
61
+ const dirs: string[] = [];
62
+ const add = (dir: string | null | undefined) => {
63
+ if (!dir || dirs.includes(dir)) return;
64
+ dirs.push(dir);
65
+ };
66
+ add(this.#getArtifactsDir());
67
+ for (const dir of this.#getAuthorizedArtifactsDirs?.() ?? []) add(dir);
68
+ if (dirs.length === 0) return;
65
69
 
66
70
  const pattern = this.#parentPrefix
67
71
  ? new RegExp(`^${escapeRegExp(this.#parentPrefix)}\\.(\\d+)-.*\\.${RESERVED_OUTPUT_EXTENSIONS_PATTERN}$`)
68
72
  : new RegExp(`^(\\d+)-.*\\.${RESERVED_OUTPUT_EXTENSIONS_PATTERN}$`);
69
-
70
73
  let maxId = -1;
71
- for (const file of files) {
72
- const match = file.match(pattern);
73
- if (match) {
74
+ for (const dir of dirs) {
75
+ let files: string[];
76
+ try {
77
+ files = await fs.readdir(dir);
78
+ } catch {
79
+ continue;
80
+ }
81
+ for (const file of files) {
82
+ const match = file.match(pattern);
83
+ if (!match) continue;
74
84
  const id = Number.parseInt(match[1], 10);
75
85
  if (id > maxId) maxId = id;
76
86
  }
@@ -4,8 +4,24 @@ export type ProviderRetryKind = NonNullable<AgentProgress["retryState"]>["kind"]
4
4
 
5
5
  const FIRST_EVENT_TIMEOUT_PATTERN = /stream timed out while waiting for the first event/i;
6
6
  const IDLE_STREAM_STALL_PATTERN = /stream stalled while waiting for the next event/i;
7
+ const STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE = "stream_first_event_timeout";
7
8
 
8
9
  export function classifyProviderRetry(errorMessage: string): ProviderRetryKind {
10
+ return classifyProviderRetryFromTransport({ errorMessage });
11
+ }
12
+
13
+ /**
14
+ * Prefer typed transport facts when present; fall back to message regex for
15
+ * message-only callers that never received `transportFailure.providerCode`.
16
+ */
17
+ export function classifyProviderRetryFromTransport(facts: {
18
+ providerCode?: string;
19
+ errorMessage?: string;
20
+ }): ProviderRetryKind {
21
+ if (facts.providerCode?.toLowerCase() === STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE) {
22
+ return "first_event_timeout";
23
+ }
24
+ const errorMessage = facts.errorMessage ?? "";
9
25
  if (FIRST_EVENT_TIMEOUT_PATTERN.test(errorMessage)) return "first_event_timeout";
10
26
  if (IDLE_STREAM_STALL_PATTERN.test(errorMessage)) return "idle_stream_stall";
11
27
  return "provider_error";