@gajae-code/coding-agent 0.16.3 → 0.16.6

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 (277) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/cli/args.d.ts +2 -2
  4. package/dist/types/cli/customize-doctor.d.ts +22 -0
  5. package/dist/types/cli/root-flags.d.ts +4 -2
  6. package/dist/types/cli.d.ts +1 -1
  7. package/dist/types/commands/launch.d.ts +1 -1
  8. package/dist/types/config/autorouting-tier-map.d.ts +3 -0
  9. package/dist/types/config/file-lock.d.ts +31 -3
  10. package/dist/types/config/model-registry.d.ts +4 -0
  11. package/dist/types/config/model-resolver.d.ts +9 -0
  12. package/dist/types/config/settings-schema.d.ts +29 -1
  13. package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
  14. package/dist/types/coordinator-mcp/server.d.ts +7 -0
  15. package/dist/types/discovery/claude-plugins.d.ts +3 -1
  16. package/dist/types/discovery/helpers.d.ts +35 -0
  17. package/dist/types/exec/bash-executor.d.ts +17 -1
  18. package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
  19. package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
  20. package/dist/types/exec/bash-shell-signals.d.ts +2 -0
  21. package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
  22. package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
  23. package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
  24. package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
  25. package/dist/types/exec/bash-shell-worker.d.ts +1 -0
  26. package/dist/types/exec/isolated-shell.d.ts +19 -0
  27. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
  28. package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
  29. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  30. package/dist/types/extensibility/extensions/runner.d.ts +46 -6
  31. package/dist/types/extensibility/extensions/types.d.ts +16 -0
  32. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
  33. package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
  34. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
  35. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
  36. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  37. package/dist/types/extensibility/hooks/types.d.ts +4 -0
  38. package/dist/types/extensibility/runtime-skill-discovery.d.ts +5 -2
  39. package/dist/types/extensibility/shared-events.d.ts +1 -1
  40. package/dist/types/extensibility/skill-management.d.ts +9 -4
  41. package/dist/types/extensibility/skills.d.ts +2 -0
  42. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
  43. package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
  44. package/dist/types/gjc-runtime/session-state-lock.d.ts +14 -0
  45. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
  46. package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
  47. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  48. package/dist/types/modes/components/bash-execution.d.ts +1 -0
  49. package/dist/types/modes/components/execution-shared.d.ts +1 -0
  50. package/dist/types/modes/components/settings-selector.d.ts +6 -1
  51. package/dist/types/modes/components/status-line/command.d.ts +35 -0
  52. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  53. package/dist/types/modes/components/status-line/types.d.ts +5 -0
  54. package/dist/types/modes/components/tool-status-header.d.ts +14 -0
  55. package/dist/types/modes/controllers/event-controller.d.ts +6 -0
  56. package/dist/types/modes/interactive-mode.d.ts +3 -0
  57. package/dist/types/modes/types.d.ts +7 -0
  58. package/dist/types/runtime-mcp/client.d.ts +9 -1
  59. package/dist/types/runtime-mcp/manager.d.ts +7 -1
  60. package/dist/types/runtime-mcp/pool.d.ts +22 -3
  61. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  62. package/dist/types/runtime-mcp/types.d.ts +22 -0
  63. package/dist/types/sdk/broker/ensure.d.ts +32 -0
  64. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
  65. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  66. package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
  67. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
  68. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
  69. package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
  70. package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
  71. package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
  72. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  73. package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
  74. package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
  75. package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
  76. package/dist/types/sdk/client/client.d.ts +6 -0
  77. package/dist/types/sdk/host/host.d.ts +3 -1
  78. package/dist/types/sdk/host/session-runtime.d.ts +26 -1
  79. package/dist/types/sdk/lifecycle/service.d.ts +1 -0
  80. package/dist/types/sdk/router/session-router.d.ts +10 -2
  81. package/dist/types/sdk/session-api-key.d.ts +26 -0
  82. package/dist/types/sdk/session.d.ts +5 -1
  83. package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
  84. package/dist/types/session/agent-session.d.ts +11 -0
  85. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  86. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  87. package/dist/types/session/messages.d.ts +1 -0
  88. package/dist/types/session/session-manager.d.ts +3 -1
  89. package/dist/types/setup/paseo/announce.d.ts +112 -0
  90. package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
  91. package/dist/types/skill-state/canonical-skills.d.ts +4 -0
  92. package/dist/types/system-prompt.d.ts +10 -0
  93. package/dist/types/task/executor.d.ts +5 -1
  94. package/dist/types/tools/bash.d.ts +1 -0
  95. package/dist/types/tools/fetch.d.ts +7 -0
  96. package/dist/types/tools/github-cache.d.ts +2 -1
  97. package/dist/types/tools/index.d.ts +10 -0
  98. package/dist/types/tools/path-utils.d.ts +2 -0
  99. package/dist/types/tools/search.d.ts +6 -0
  100. package/package.json +9 -7
  101. package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
  102. package/scripts/generate-sdk-operation-inventory.ts +11 -0
  103. package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
  104. package/src/capability/index.ts +14 -2
  105. package/src/capability/types.ts +4 -0
  106. package/src/cli/args.ts +20 -9
  107. package/src/cli/customize-doctor.ts +155 -12
  108. package/src/cli/root-flags.ts +5 -3
  109. package/src/cli.ts +22 -0
  110. package/src/commands/sdk.ts +74 -31
  111. package/src/config/autorouting-tier-map.ts +1 -0
  112. package/src/config/file-lock-gc.ts +40 -2
  113. package/src/config/file-lock.ts +379 -31
  114. package/src/config/model-preset-registry.ts +120 -19
  115. package/src/config/model-profiles.ts +40 -0
  116. package/src/config/model-registry.ts +73 -23
  117. package/src/config/model-resolver.ts +48 -0
  118. package/src/config/settings-schema.ts +33 -1
  119. package/src/config/settings.ts +1 -0
  120. package/src/coordinator-mcp/question-state.ts +33 -2
  121. package/src/coordinator-mcp/server.ts +477 -101
  122. package/src/crash/sanitize.ts +1 -1
  123. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
  124. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
  125. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
  126. package/src/discovery/builtin.ts +52 -55
  127. package/src/discovery/claude-plugins.ts +12 -4
  128. package/src/discovery/helpers.ts +307 -34
  129. package/src/exec/bash-executor.ts +330 -50
  130. package/src/exec/bash-shell-guardian.ts +388 -0
  131. package/src/exec/bash-shell-runtime-entry.ts +5 -0
  132. package/src/exec/bash-shell-signals.ts +29 -0
  133. package/src/exec/bash-shell-supervisor-entry.ts +3 -0
  134. package/src/exec/bash-shell-supervisor.ts +359 -0
  135. package/src/exec/bash-shell-worker-entry.ts +5 -0
  136. package/src/exec/bash-shell-worker-protocol.ts +36 -0
  137. package/src/exec/bash-shell-worker.ts +116 -0
  138. package/src/exec/isolated-shell.ts +393 -0
  139. package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
  140. package/src/extensibility/extensions/function-hooks.ts +959 -0
  141. package/src/extensibility/extensions/index.ts +22 -0
  142. package/src/extensibility/extensions/loader.ts +53 -2
  143. package/src/extensibility/extensions/runner.ts +895 -264
  144. package/src/extensibility/extensions/types.ts +42 -0
  145. package/src/extensibility/extensions/wrapper.ts +56 -15
  146. package/src/extensibility/gjc-plugins/compiler.ts +228 -60
  147. package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
  148. package/src/extensibility/gjc-plugins/installer.ts +13 -12
  149. package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
  150. package/src/extensibility/gjc-plugins/paths.ts +13 -0
  151. package/src/extensibility/gjc-plugins/registry.ts +113 -1
  152. package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
  153. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
  154. package/src/extensibility/gjc-plugins/schema.ts +121 -4
  155. package/src/extensibility/gjc-plugins/types.ts +25 -0
  156. package/src/extensibility/hooks/loader.ts +133 -16
  157. package/src/extensibility/hooks/types.ts +28 -0
  158. package/src/extensibility/runtime-skill-discovery.ts +149 -190
  159. package/src/extensibility/shared-events.ts +1 -1
  160. package/src/extensibility/skill-management.ts +200 -52
  161. package/src/extensibility/skills.ts +73 -25
  162. package/src/gjc-runtime/empty-delete-gc.ts +94 -4
  163. package/src/gjc-runtime/gc-runtime.ts +29 -0
  164. package/src/gjc-runtime/linux-proc.ts +14 -0
  165. package/src/gjc-runtime/repository-binding.ts +1 -1
  166. package/src/gjc-runtime/session-state-lock.ts +143 -10
  167. package/src/gjc-runtime/session-state-sidecar.ts +27 -11
  168. package/src/gjc-runtime/state-writer.ts +30 -29
  169. package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
  170. package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
  171. package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
  172. package/src/gjc-runtime/workflow-manifest.ts +16 -0
  173. package/src/harness-control-plane/session-lease.ts +2 -2
  174. package/src/internal-urls/docs-index.generated.ts +12 -12
  175. package/src/lsp/config.ts +10 -3
  176. package/src/lsp/index.ts +1 -1
  177. package/src/lsp/path-trust.ts +69 -14
  178. package/src/main.ts +78 -35
  179. package/src/modes/DESIGN.md +54 -0
  180. package/src/modes/acp/acp-agent.ts +16 -3
  181. package/src/modes/components/assistant-message.ts +22 -9
  182. package/src/modes/components/bash-execution.ts +4 -1
  183. package/src/modes/components/execution-shared.ts +4 -1
  184. package/src/modes/components/model-selector.ts +166 -33
  185. package/src/modes/components/session-observer-overlay.ts +1 -1
  186. package/src/modes/components/settings-selector.ts +1075 -235
  187. package/src/modes/components/status-line/command.ts +190 -0
  188. package/src/modes/components/status-line/index.ts +1 -0
  189. package/src/modes/components/status-line/segments.ts +32 -5
  190. package/src/modes/components/status-line/types.ts +5 -0
  191. package/src/modes/components/tool-status-header.ts +396 -22
  192. package/src/modes/controllers/command-controller.ts +3 -0
  193. package/src/modes/controllers/event-controller.ts +151 -7
  194. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  195. package/src/modes/controllers/input-controller.ts +6 -3
  196. package/src/modes/controllers/selector-controller.ts +21 -0
  197. package/src/modes/interactive-mode.ts +53 -6
  198. package/src/modes/print-mode.ts +5 -1
  199. package/src/modes/prompt-action-autocomplete.ts +54 -27
  200. package/src/modes/runtime-init.ts +2 -0
  201. package/src/modes/types.ts +17 -3
  202. package/src/modes/utils/ui-helpers.ts +2 -0
  203. package/src/prompts/system/system-prompt.md +8 -0
  204. package/src/prompts/tools/search.md +2 -0
  205. package/src/prompts/tools/subagent.md +1 -0
  206. package/src/runtime-mcp/client.ts +60 -17
  207. package/src/runtime-mcp/manager.ts +189 -14
  208. package/src/runtime-mcp/pool.test.ts +218 -0
  209. package/src/runtime-mcp/pool.ts +224 -23
  210. package/src/runtime-mcp/transports/stdio.ts +158 -24
  211. package/src/runtime-mcp/types.ts +24 -0
  212. package/src/sdk/broker/broker.ts +124 -32
  213. package/src/sdk/broker/ensure.ts +248 -41
  214. package/src/sdk/broker/lifecycle-ledger.ts +7 -17
  215. package/src/sdk/broker/lifecycle.ts +56 -6
  216. package/src/sdk/broker/startup-failure.ts +15 -6
  217. package/src/sdk/bus/chat-daemon-control.ts +3 -1
  218. package/src/sdk/bus/chat-daemon-runtime.ts +72 -6
  219. package/src/sdk/bus/chat-effect-journal.ts +132 -7
  220. package/src/sdk/bus/conversation-store.ts +29 -0
  221. package/src/sdk/bus/lifecycle-commands.ts +17 -11
  222. package/src/sdk/bus/slack-conversation.ts +1 -0
  223. package/src/sdk/bus/slack-daemon.ts +478 -49
  224. package/src/sdk/bus/slack-live-provider.ts +12 -0
  225. package/src/sdk/bus/slack-provider.ts +5 -0
  226. package/src/sdk/bus/telegram-daemon.ts +16 -2
  227. package/src/sdk/bus/topic-registry.ts +19 -0
  228. package/src/sdk/cli/master-cli.ts +1 -1
  229. package/src/sdk/cli/session-cli.ts +11 -8
  230. package/src/sdk/client/client.ts +66 -17
  231. package/src/sdk/host/host.ts +29 -9
  232. package/src/sdk/host/query/revision-store.ts +19 -13
  233. package/src/sdk/host/reverse-leases.ts +7 -4
  234. package/src/sdk/host/session-runtime.test.ts +52 -10
  235. package/src/sdk/host/session-runtime.ts +383 -44
  236. package/src/sdk/lifecycle/service.ts +4 -0
  237. package/src/sdk/protocol/operation-inventory.generated.json +77 -0
  238. package/src/sdk/router/session-router.ts +89 -38
  239. package/src/sdk/session-api-key.ts +94 -0
  240. package/src/sdk/session.ts +228 -55
  241. package/src/sdk/transport/relay.ts +35 -13
  242. package/src/sdk/transport/serve-cli.ts +3 -3
  243. package/src/session/agent-session.ts +240 -90
  244. package/src/session/contribution-prep.ts +214 -21
  245. package/src/session/fallback-chain-controller.ts +38 -0
  246. package/src/session/internal/managed-session-storage.ts +19 -5
  247. package/src/session/messages.ts +5 -0
  248. package/src/session/session-manager.ts +29 -5
  249. package/src/session-import/redact.ts +5 -1
  250. package/src/setup/paseo/announce.ts +456 -0
  251. package/src/setup/paseo/setup-deps.ts +15 -3
  252. package/src/skill-state/canonical-skills.ts +12 -0
  253. package/src/skills/skills.test.ts +41 -21
  254. package/src/slash-commands/builtin-registry.ts +1 -0
  255. package/src/system-prompt.ts +29 -3
  256. package/src/task/executor.ts +7 -1
  257. package/src/task/index.ts +21 -4
  258. package/src/tools/bash.ts +63 -19
  259. package/src/tools/cron.ts +1 -1
  260. package/src/tools/fetch.ts +29 -1
  261. package/src/tools/gh.ts +26 -3
  262. package/src/tools/github-cache.ts +68 -16
  263. package/src/tools/index.ts +10 -0
  264. package/src/tools/job.ts +21 -4
  265. package/src/tools/monitor.ts +8 -2
  266. package/src/tools/path-utils.ts +7 -0
  267. package/src/tools/read.ts +14 -2
  268. package/src/tools/render-utils.ts +1 -3
  269. package/src/tools/resource-gc.ts +28 -8
  270. package/src/tools/search.ts +116 -58
  271. package/src/tools/skill-discovery.ts +5 -0
  272. package/src/tools/skill.ts +37 -2
  273. package/src/tools/subagent.ts +6 -3
  274. package/src/tools/tool-catalog.generated.ts +10 -3
  275. package/src/tools/ultragoal-ask-guard.ts +2 -2
  276. package/src/web/insane/bridge.ts +113 -23
  277. package/src/web/search/providers/perplexity.ts +102 -41
@@ -235,6 +235,10 @@ export async function resolvePromptInput(input: string | undefined, description:
235
235
  export interface LoadContextFilesOptions {
236
236
  /** Working directory to start walking up from. Default: getProjectDir() */
237
237
  cwd?: string;
238
+ /** Agent directory backing native user-scope context and system prompt files. */
239
+ agentDir?: string;
240
+ /** Resolver-owned classification for `agentDir`; preserve it across HOME/config refreshes. */
241
+ profileAuthority?: "default" | "custom";
238
242
  }
239
243
 
240
244
  function dedupeExactContextFiles(
@@ -265,7 +269,11 @@ export async function loadProjectContextFilesResult(
265
269
  options: LoadContextFilesOptions = {},
266
270
  ): Promise<ProjectContextFilesResult> {
267
271
  const resolvedCwd = options.cwd ?? getProjectDir();
268
- const result = await loadCapability(contextFileCapability.id, { cwd: resolvedCwd });
272
+ const result = await loadCapability(contextFileCapability.id, {
273
+ cwd: resolvedCwd,
274
+ agentDir: options.agentDir,
275
+ profileAuthority: options.profileAuthority,
276
+ });
269
277
  const items = result.items as ContextFile[];
270
278
 
271
279
  // Native user-global context applies everywhere and is least specific, so it
@@ -311,7 +319,11 @@ export async function loadProjectContextFiles(
311
319
  export async function loadSystemPromptFiles(options: LoadContextFilesOptions = {}): Promise<string | null> {
312
320
  const resolvedCwd = options.cwd ?? getProjectDir();
313
321
 
314
- const result = await loadCapability<SystemPromptFile>(systemPromptCapability.id, { cwd: resolvedCwd });
322
+ const result = await loadCapability<SystemPromptFile>(systemPromptCapability.id, {
323
+ cwd: resolvedCwd,
324
+ agentDir: options.agentDir,
325
+ profileAuthority: options.profileAuthority,
326
+ });
315
327
 
316
328
  if (result.items.length === 0) return null;
317
329
 
@@ -368,10 +380,16 @@ export interface BuildSystemPromptOptions {
368
380
  pluginAppendices?: string;
369
381
  /** Repeat full tool descriptions in system prompt. Default: false */
370
382
  repeatToolDescriptions?: boolean;
383
+ /** Language guidance for internal technical reasoning. Default: off. */
384
+ reasoningLanguage?: "off" | "english";
371
385
  /** Skills settings for discovery. */
372
386
  skillsSettings?: SkillsSettings;
373
387
  /** Working directory. Default: getProjectDir() */
374
388
  cwd?: string;
389
+ /** Agent directory backing native user-scope prompt discovery. */
390
+ agentDir?: string;
391
+ /** Resolver-owned classification for `agentDir`; preserve it across HOME/config refreshes. */
392
+ profileAuthority?: "default" | "custom";
375
393
  /** Pre-loaded context files (skips discovery if provided). */
376
394
  contextFiles?: Array<{ path: string; content: string; depth?: number }>;
377
395
  /** Skills provided directly to system prompt construction. */
@@ -542,6 +560,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
542
560
  appendSystemPrompt,
543
561
  pluginAppendices,
544
562
  repeatToolDescriptions = false,
563
+ reasoningLanguage = "off",
545
564
  toolNames: providedToolNames,
546
565
  cwd,
547
566
  contextFiles: providedContextFiles,
@@ -600,10 +619,16 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
600
619
 
601
620
  const systemPromptCustomizationPromise = logger.time("loadSystemPromptFiles", loadSystemPromptFiles, {
602
621
  cwd: resolvedCwd,
622
+ agentDir: options.agentDir,
623
+ profileAuthority: options.profileAuthority,
603
624
  });
604
625
  const contextFilesPromise = providedContextFiles
605
626
  ? Promise.resolve({ contextFiles: providedContextFiles, warnings: [] })
606
- : logger.time("loadProjectContextFiles", loadProjectContextFilesResult, { cwd: resolvedCwd });
627
+ : logger.time("loadProjectContextFiles", loadProjectContextFilesResult, {
628
+ cwd: resolvedCwd,
629
+ agentDir: options.agentDir,
630
+ profileAuthority: options.profileAuthority,
631
+ });
607
632
  const workspaceTreePromise =
608
633
  providedWorkspaceTree !== undefined
609
634
  ? Promise.resolve(providedWorkspaceTree)
@@ -710,6 +735,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
710
735
  tools: toolNames,
711
736
  toolInfo,
712
737
  repeatToolDescriptions,
738
+ reasoningLanguageEnglish: reasoningLanguage === "english",
713
739
  toolRefs,
714
740
  environment,
715
741
  contextFiles: sanitizedContextFiles,
@@ -281,6 +281,9 @@ export interface ExecutorOptions {
281
281
  authStorage?: AuthStorage;
282
282
  modelRegistry?: ModelRegistry;
283
283
  settings?: Settings;
284
+ /** Parent session's selected agent directory and immutable resolver-owned authority. */
285
+ agentDir?: string;
286
+ profileAuthority?: "default" | "custom";
284
287
  /** Parent session's registry; shared by child sessions for IRC routing and roster visibility. */
285
288
  agentRegistry?: AgentRegistry;
286
289
  /**
@@ -300,7 +303,8 @@ export interface ExecutorOptions {
300
303
  managedPersistence?: ManagedTaskPersistence;
301
304
  /**
302
305
  * The parent session's ENDPOINT-owned AsyncJobManager (resolved by the
303
- * TaskTool via forEndpoint(sessionId) ?? instance()). Model metadata and
306
+ * TaskTool via its opaque async endpoint accessor, then the process-global
307
+ * fallback). Model metadata and
304
308
  * live-handle state for THIS subagent are recorded in the SAME manager the
305
309
  * task job runs in — with concurrent top-level sessions the process-global
306
310
  * instance belongs to a different session and would surface this subagent
@@ -1951,6 +1955,8 @@ export async function runSubprocessOnce(options: ExecutorOptions): Promise<Singl
1951
1955
  const { session } = await awaitAbortable(
1952
1956
  canonicalSdk.createAgentSession({
1953
1957
  cwd: worktree ?? cwd,
1958
+ agentDir: options.agentDir,
1959
+ profileAuthority: options.profileAuthority,
1954
1960
  authStorage,
1955
1961
  modelRegistry,
1956
1962
  settings: subagentSettings,
package/src/task/index.ts CHANGED
@@ -375,6 +375,14 @@ function createTaskModeError(text: string): AgentToolResult<TaskToolDetails> {
375
375
  };
376
376
  }
377
377
 
378
+ /** Remove only strict-provider blank placeholders for the optional caller-authored schema. */
379
+ function normalizeTaskAdmissionParams(params: TaskParams): TaskParams {
380
+ if (typeof params.schema !== "string" || params.schema.trim() !== "") return params;
381
+ const normalized = { ...params };
382
+ delete normalized.schema;
383
+ return normalized;
384
+ }
385
+
378
386
  function validateTaskModeParams(simpleMode: TaskSimpleMode, params: TaskParams): string | undefined {
379
387
  const { contextEnabled, customSchemaEnabled } = getTaskSimpleModeCapabilities(simpleMode);
380
388
  const disallowedFields: string[] = [];
@@ -888,7 +896,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
888
896
  * thread P1).
889
897
  */
890
898
  #resolveOwnedJobManager(): AsyncJobManager | undefined {
891
- const endpointId = this.session.getSessionId?.() ?? undefined;
899
+ const endpointId = this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? undefined;
892
900
  return (
893
901
  this.session.getAsyncJobManager?.() ?? AsyncJobManager.forEndpoint(endpointId) ?? AsyncJobManager.instance()
894
902
  );
@@ -900,7 +908,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
900
908
  signal?: AbortSignal,
901
909
  onUpdate?: AgentToolUpdateCallback<TaskToolDetails>,
902
910
  ): Promise<AgentToolResult<TaskToolDetails>> {
903
- const params = rawParams as TaskParams;
911
+ const params = normalizeTaskAdmissionParams(rawParams as TaskParams);
904
912
  const simpleMode = this.#getTaskSimpleMode();
905
913
  const validationError = validateTaskModeParams(simpleMode, params);
906
914
  if (validationError) {
@@ -1245,7 +1253,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1245
1253
  manager,
1246
1254
  resumeToolCallId ?? descriptor.toolCallId,
1247
1255
  resumeJobId,
1248
- this.session.getSessionId?.() ?? undefined,
1256
+ this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? undefined,
1249
1257
  );
1250
1258
  return resumeJobId;
1251
1259
  };
@@ -1477,7 +1485,12 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
1477
1485
  },
1478
1486
  },
1479
1487
  );
1480
- registerOwnedIfLineaged(manager, _toolCallId, jobId, this.session.getSessionId?.() ?? undefined);
1488
+ registerOwnedIfLineaged(
1489
+ manager,
1490
+ _toolCallId,
1491
+ jobId,
1492
+ this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? undefined,
1493
+ );
1481
1494
  startedJobs.push({ jobId, taskId: taskItem.id });
1482
1495
  if (typeof manager.registerResumeDescriptor === "function") {
1483
1496
  manager.registerResumeDescriptor(
@@ -2324,6 +2337,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
2324
2337
  modelRegistry: this.session.modelRegistry,
2325
2338
  agentRegistry: this.session.agentRegistry,
2326
2339
  settings: this.session.settings,
2340
+ agentDir: this.session.getSessionAgentDir?.() ?? this.session.settings.getAgentDir(),
2341
+ profileAuthority: this.session.profileAuthority,
2327
2342
  inheritedServiceTier: this.session.serviceTier,
2328
2343
  contextFiles,
2329
2344
  skills: availableSkills,
@@ -2408,6 +2423,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
2408
2423
  modelRegistry: this.session.modelRegistry,
2409
2424
  agentRegistry: this.session.agentRegistry,
2410
2425
  settings: this.session.settings,
2426
+ agentDir: this.session.getSessionAgentDir?.() ?? this.session.settings.getAgentDir(),
2427
+ profileAuthority: this.session.profileAuthority,
2411
2428
  inheritedServiceTier: this.session.serviceTier,
2412
2429
  contextFiles,
2413
2430
  skills: availableSkills,
package/src/tools/bash.ts CHANGED
@@ -103,6 +103,7 @@ const BASH_ERROR_MAX_BYTES = 4096;
103
103
  const ARTIFACT_SAVE_DIAGNOSTIC_MAX_BYTES = 256;
104
104
  const BASH_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
105
105
  const MASTER_CAPABILITY_ENV = "GJC_MASTER_CAPABILITY";
106
+ const MASTER_OWNER_SESSION_ENV = "GJC_MASTER_OWNER_SESSION_ID";
106
107
  const DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS = 60_000;
107
108
  const ACP_RELEASE_TIMEOUT_MS = 1_000;
108
109
  const READ_ONLY_BASH_ENV: Record<string, string> = {
@@ -186,8 +187,9 @@ function artifactReferenceIsReachable(text: string, result: BashResult | BashInt
186
187
  );
187
188
  }
188
189
 
189
- function suffixPrefixOverlap(source: string, target: string): number {
190
+ export function suffixPrefixOverlap(source: string, target: string): number {
190
191
  if (source.length === 0 || target.length === 0) return 0;
192
+ if (target.startsWith(source)) return source.length;
191
193
  const prefix = new Uint32Array(target.length);
192
194
  for (let index = 1; index < target.length; index += 1) {
193
195
  let length = prefix[index - 1] ?? 0;
@@ -244,10 +246,17 @@ function failureStatusCause(
244
246
  if (result.cancelled) return leadingStatus ?? lastStatus ?? "Command cancelled";
245
247
  if (isInteractiveResult(result) && result.timedOut) return lastStatus ?? "Command timed out";
246
248
  if (result.exitCode === undefined) return "Command failed: missing exit status";
247
- if (result.exitCode !== 0) return `Command exited with code ${result.exitCode}`;
249
+ if (result.exitCode !== 0) return nonZeroExitCause(result);
248
250
  return undefined;
249
251
  }
250
252
 
253
+ function nonZeroExitCause(result: BashResult | BashInteractiveResult): string {
254
+ if ("signal" in result && result.signal) {
255
+ return `Command terminated by ${result.signal} (exit code ${result.exitCode})`;
256
+ }
257
+ return `Command exited with code ${result.exitCode}`;
258
+ }
259
+
251
260
  function removeTrailingFailureCause(text: string, cause: string | undefined): string {
252
261
  if (!cause) return text;
253
262
  for (const suffix of [`\n\n${cause}`, `\n${cause}`, cause]) {
@@ -717,7 +726,12 @@ export function masterCommandEnvOverrides(
717
726
  directMasterSpawn: boolean,
718
727
  ): Record<string, string> {
719
728
  if (directMasterSpawn) return {};
720
- return Object.fromEntries(Object.entries(env ?? {}).filter(([key]) => key !== MASTER_CAPABILITY_ENV));
729
+ // The master capability is single-use spawn authority and the owner id is
730
+ // spawn-lineage identity: neither is ambient state for ordinary Bash or a
731
+ // chained/pipelined descendant (issue #5374).
732
+ return Object.fromEntries(
733
+ Object.entries(env ?? {}).filter(([key]) => key !== MASTER_CAPABILITY_ENV && key !== MASTER_OWNER_SESSION_ENV),
734
+ );
721
735
  }
722
736
 
723
737
  function escapeBashEnvValueForDisplay(value: string): string {
@@ -896,9 +910,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
896
910
  throw new ToolError(formatBashFailureMessage(result, `${outputText}\n\nCommand failed: missing exit status`));
897
911
  }
898
912
  if (result.exitCode !== 0) {
899
- throw new ToolError(
900
- formatBashFailureMessage(result, `${outputText}\n\nCommand exited with code ${result.exitCode}`),
901
- );
913
+ const cause = nonZeroExitCause(result);
914
+ throw new ToolError(formatBashFailureMessage(result, `${outputText}\n\n${cause}`));
902
915
  }
903
916
  return outputText;
904
917
  }
@@ -1107,7 +1120,12 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
1107
1120
  manager.failNow(jobId, jobGeneration, "Bash job owner was torn down.");
1108
1121
  });
1109
1122
  void completion.promise.finally(unregisterOwnerCleanup);
1110
- registerOwnedIfLineaged(manager, options.toolCallId, jobId, this.session.getSessionId?.() ?? undefined);
1123
+ registerOwnedIfLineaged(
1124
+ manager,
1125
+ options.toolCallId,
1126
+ jobId,
1127
+ this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? undefined,
1128
+ );
1111
1129
 
1112
1130
  return {
1113
1131
  jobId,
@@ -1132,7 +1150,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
1132
1150
  * running (review thread P1).
1133
1151
  */
1134
1152
  #resolveOwnedJobManager(): AsyncJobManager | undefined {
1135
- const endpointId = this.session.getSessionId?.() ?? undefined;
1153
+ const endpointId = this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? undefined;
1136
1154
  if (this.session.getAsyncJobManager) return this.session.getAsyncJobManager();
1137
1155
  return AsyncJobManager.forEndpoint(endpointId) ?? AsyncJobManager.instance();
1138
1156
  }
@@ -1433,7 +1451,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
1433
1451
  // command that contained shell syntax into a privileged one.
1434
1452
  const directMasterSpawn = isStrictDirectSdkSpawnCommand(rawCommand) && isStrictDirectSdkSpawnCommand(command);
1435
1453
  const masterCapability = directMasterSpawn ? this.session.getMasterBashCapability?.() : undefined;
1436
- const masterOwnerSessionId = directMasterSpawn ? this.session.getMasterOwnerSessionId?.() : undefined;
1454
+ // Master ownership is lineage identity, not session identity: it travels
1455
+ // under its own variable on every bash env so a master-owned child can
1456
+ // still read its OWN id from GJC_SESSION_ID (issue #5374).
1457
+ const masterOwnerSessionId = this.session.getMasterOwnerSessionId?.();
1437
1458
  const commandEnvOverrides = masterCommandEnvOverrides(expandedEnv, directMasterSpawn);
1438
1459
  const resolvedEnv = {
1439
1460
  ...buildGjcRuntimeSessionEnv({
@@ -1446,7 +1467,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
1446
1467
  : {}),
1447
1468
  ...commandEnvOverrides,
1448
1469
  ...(masterCapability ? { [MASTER_CAPABILITY_ENV]: masterCapability } : {}),
1449
- ...(masterOwnerSessionId ? { GJC_MASTER_OWNER_SESSION_ID: masterOwnerSessionId } : {}),
1470
+ ...(masterOwnerSessionId ? { [MASTER_OWNER_SESSION_ENV]: masterOwnerSessionId } : {}),
1450
1471
  ...(this.session.bashRestrictionProfile === "read-only" ? READ_ONLY_BASH_ENV : {}),
1451
1472
  ...(allowedPrefixes && allowedPrefixes.length > 0 ? { [GJC_RESTRICTED_ROLE_AGENT_BASH_ENV]: "1" } : {}),
1452
1473
  };
@@ -1498,8 +1519,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
1498
1519
  resolvedEnv: Record<string, string> | undefined,
1499
1520
  ): Promise<BashResult> {
1500
1521
  const masterCapability = resolvedEnv?.[MASTER_CAPABILITY_ENV];
1522
+ const ownerSessionId = resolvedEnv?.[MASTER_OWNER_SESSION_ENV];
1501
1523
  const sessionAgentDir = resolvedEnv?.GJC_CODING_AGENT_DIR;
1502
- if (masterCapability === undefined || sessionAgentDir === undefined)
1524
+ if (masterCapability === undefined || ownerSessionId === undefined || sessionAgentDir === undefined)
1503
1525
  throw new ToolError("Master spawn authority context is unavailable.");
1504
1526
  const args = parseDirectSdkSpawnArgs(command);
1505
1527
  args.agentDir = sessionAgentDir;
@@ -1507,8 +1529,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
1507
1529
  const request = runSdkSpawn(args, {
1508
1530
  env: {
1509
1531
  ...process.env,
1510
- GJC_MASTER_CAPABILITY: masterCapability,
1511
- GJC_SESSION_ID: resolvedEnv?.GJC_MASTER_OWNER_SESSION_ID ?? this.session.getSessionId?.() ?? undefined,
1532
+ [MASTER_CAPABILITY_ENV]: masterCapability,
1533
+ [MASTER_OWNER_SESSION_ENV]: ownerSessionId,
1512
1534
  GJC_CODING_AGENT_DIR: sessionAgentDir,
1513
1535
  },
1514
1536
  });
@@ -1681,7 +1703,12 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
1681
1703
  // Monitor jobs are exact owned background work of the turn that started
1682
1704
  // them: register the five-tuple so scope:"owned" terminal abort stops the
1683
1705
  // monitor too (review thread P2).
1684
- registerOwnedIfLineaged(manager, opts.toolCallId, jobId, this.session.getSessionId?.() ?? undefined);
1706
+ registerOwnedIfLineaged(
1707
+ manager,
1708
+ opts.toolCallId,
1709
+ jobId,
1710
+ this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? undefined,
1711
+ );
1685
1712
  currentJobId = jobId;
1686
1713
  return { jobId, label, commandCwd: prepared.commandCwd };
1687
1714
  }
@@ -1883,19 +1910,31 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
1883
1910
  }
1884
1911
  if (waitResult.kind === "completed") {
1885
1912
  ownedManager.acknowledgeDeliveries([job.jobId]);
1886
- unregisterForegroundOwnedBash(ownedManager, job.jobId, this.session.getSessionId?.() ?? "local");
1913
+ unregisterForegroundOwnedBash(
1914
+ ownedManager,
1915
+ job.jobId,
1916
+ this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? "local",
1917
+ );
1887
1918
  return waitResult.result;
1888
1919
  }
1889
1920
  if (waitResult.kind === "failed") {
1890
1921
  ownedManager.acknowledgeDeliveries([job.jobId]);
1891
- unregisterForegroundOwnedBash(ownedManager, job.jobId, this.session.getSessionId?.() ?? "local");
1922
+ unregisterForegroundOwnedBash(
1923
+ ownedManager,
1924
+ job.jobId,
1925
+ this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? "local",
1926
+ );
1892
1927
  throw waitResult.error;
1893
1928
  }
1894
1929
  if (waitResult.kind === "aborted") {
1895
1930
  ownedManager.cancel(job.jobId);
1896
1931
  const terminal = await job.completion;
1897
1932
  ownedManager.acknowledgeDeliveries([job.jobId]);
1898
- unregisterForegroundOwnedBash(ownedManager, job.jobId, this.session.getSessionId?.() ?? "local");
1933
+ unregisterForegroundOwnedBash(
1934
+ ownedManager,
1935
+ job.jobId,
1936
+ this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? "local",
1937
+ );
1899
1938
  if (terminal.kind === "failed") {
1900
1939
  throw new ToolAbortError(
1901
1940
  formatManagedAbortFailure(terminal.error, terminal.result, job.getLatestText()),
@@ -2357,7 +2396,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
2357
2396
  // already ran above, so exactly ONE remote handle exists and is retained
2358
2397
  // across the fold.
2359
2398
  const bridgeManager = ownedManager;
2360
- const bridgeEndpointId = this.session.getSessionId?.() ?? "local";
2399
+ const bridgeEndpointId = this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? "local";
2361
2400
  const bridgeLabel = command.length > 120 ? `${command.slice(0, 117)}...` : command;
2362
2401
  const bridgeCompletion = Promise.withResolvers<ManagedBashJobCompletion>();
2363
2402
  let bridgeForegroundSettled = false;
@@ -2621,7 +2660,12 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
2621
2660
  throw error;
2622
2661
  }
2623
2662
  const ptyGeneration = ptyManager.getJob(ptyJobId)?.generation ?? ptyJobId;
2624
- registerOwnedIfLineaged(ptyManager, toolCallId, ptyJobId, this.session.getSessionId?.() ?? undefined);
2663
+ registerOwnedIfLineaged(
2664
+ ptyManager,
2665
+ toolCallId,
2666
+ ptyJobId,
2667
+ this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? undefined,
2668
+ );
2625
2669
  const unregisterPtyOwnerCleanup = ptyManager.registerOwnerCleanup(ptyOwnerId, () => {
2626
2670
  killPtyOnce();
2627
2671
  ptyManager.failNow(ptyJobId, ptyGeneration, ownerTeardownText());
package/src/tools/cron.ts CHANGED
@@ -592,7 +592,7 @@ export class CronTool implements AgentTool<typeof cronSchema, CronToolDetails> {
592
592
  }
593
593
 
594
594
  #resolveOwnedJobManager(): AsyncJobManager | undefined {
595
- const endpointId = this.session.getSessionId?.() ?? undefined;
595
+ const endpointId = this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? undefined;
596
596
  return (
597
597
  this.session.getAsyncJobManager?.() ?? AsyncJobManager.forEndpoint(endpointId) ?? AsyncJobManager.instance()
598
598
  );
@@ -1167,13 +1167,36 @@ interface ReadUrlCacheEntry {
1167
1167
  wrappedPreambleChars?: number;
1168
1168
  }
1169
1169
 
1170
+ // Entries hold whole-page outputs and image payloads, so this map must stay
1171
+ // bounded in long-lived hosts (TUI, SDK broker). Each fetch caches one entry
1172
+ // under up to two keys (requested + final URL). FIFO eviction is sufficient:
1173
+ // a miss simply refetches, so correctness never depends on a hit.
1174
+ export const READ_URL_CACHE_MAX_KEYS = 64;
1175
+
1170
1176
  const readUrlCache = new Map<string, ReadUrlCacheEntry>();
1171
1177
 
1172
1178
  function getReadUrlCacheKey(session: ToolSession, requestedUrl: string, raw: boolean): string {
1173
- const scope = session.getSessionFile() ?? session.cwd;
1179
+ const sessionFile = session.getSessionFile();
1180
+ const sessionId = session.getSessionId?.();
1181
+ const scope = sessionFile ? `file:${sessionFile}` : sessionId ? `session:${sessionId}` : `cwd:${session.cwd}`;
1174
1182
  return `${scope}::${raw ? "raw" : "rendered"}::${normalizeUrl(requestedUrl)}`;
1175
1183
  }
1176
1184
 
1185
+ /** Test-only seam for deterministic module-global cache assertions. */
1186
+ export const readUrlCacheTestHooks = {
1187
+ get size(): number {
1188
+ return readUrlCache.size;
1189
+ },
1190
+ get retainedOutputChars(): number {
1191
+ let total = 0;
1192
+ for (const entry of new Set(readUrlCache.values())) total += entry.output.length;
1193
+ return total;
1194
+ },
1195
+ reset(): void {
1196
+ readUrlCache.clear();
1197
+ },
1198
+ };
1199
+
1177
1200
  async function readArtifactOutput(session: ToolSession, artifactId: string): Promise<string | null> {
1178
1201
  const artifactsDir = session.getArtifactsDir?.();
1179
1202
  if (!artifactsDir) return null;
@@ -1224,6 +1247,11 @@ async function ensureReadUrlCacheArtifact(session: ToolSession, entry: ReadUrlCa
1224
1247
  function cacheReadUrlEntry(session: ToolSession, requestedUrl: string, raw: boolean, entry: ReadUrlCacheEntry): void {
1225
1248
  readUrlCache.set(getReadUrlCacheKey(session, requestedUrl, raw), entry);
1226
1249
  readUrlCache.set(getReadUrlCacheKey(session, entry.details.finalUrl, raw), entry);
1250
+ while (readUrlCache.size > READ_URL_CACHE_MAX_KEYS) {
1251
+ const oldest = readUrlCache.keys().next().value;
1252
+ if (oldest === undefined) break;
1253
+ readUrlCache.delete(oldest);
1254
+ }
1227
1255
  }
1228
1256
 
1229
1257
  async function buildReadUrlCacheEntry(
package/src/tools/gh.ts CHANGED
@@ -2738,7 +2738,14 @@ export async function getOrFetchIssue(options: IssueViewLookupOptions): Promise<
2738
2738
  }
2739
2739
  }
2740
2740
 
2741
- const doFetch = () => fetchIssueViewFresh(options.cwd, repo, identifier, includeComments, options.signal);
2741
+ const doFetch = (signal?: AbortSignal) =>
2742
+ fetchIssueViewFresh(
2743
+ options.cwd,
2744
+ repo,
2745
+ identifier,
2746
+ includeComments,
2747
+ signal && options.signal ? AbortSignal.any([signal, options.signal]) : (signal ?? options.signal),
2748
+ );
2742
2749
 
2743
2750
  if (!repo || cacheNumber === undefined) {
2744
2751
  const fresh = await doFetch();
@@ -2752,6 +2759,7 @@ export async function getOrFetchIssue(options: IssueViewLookupOptions): Promise<
2752
2759
  includeComments,
2753
2760
  settings: options.settings,
2754
2761
  authKey,
2762
+ signal: options.signal,
2755
2763
  fetchFresh: doFetch,
2756
2764
  });
2757
2765
  return {
@@ -2771,7 +2779,14 @@ export async function getOrFetchIssue(options: IssueViewLookupOptions): Promise<
2771
2779
  export async function getOrFetchPr(options: PrViewLookupOptions): Promise<ViewLookupResult<GhPrViewData>> {
2772
2780
  const includeComments = options.includeComments ?? true;
2773
2781
  const authKey = options.cacheAuthKey === undefined ? (resolveGithubCacheAuthKey() ?? null) : options.cacheAuthKey;
2774
- const doFetch = () => fetchPrViewFresh(options.cwd, options.repo, options.number, includeComments, options.signal);
2782
+ const doFetch = (signal?: AbortSignal) =>
2783
+ fetchPrViewFresh(
2784
+ options.cwd,
2785
+ options.repo,
2786
+ options.number,
2787
+ includeComments,
2788
+ signal && options.signal ? AbortSignal.any([signal, options.signal]) : (signal ?? options.signal),
2789
+ );
2775
2790
  const lookup = await getOrFetchView<GhPrViewData>({
2776
2791
  repo: options.repo,
2777
2792
  kind: "pr",
@@ -2779,6 +2794,7 @@ export async function getOrFetchPr(options: PrViewLookupOptions): Promise<ViewLo
2779
2794
  includeComments,
2780
2795
  settings: options.settings,
2781
2796
  authKey,
2797
+ signal: options.signal,
2782
2798
  fetchFresh: doFetch,
2783
2799
  });
2784
2800
  return {
@@ -3121,7 +3137,13 @@ async function fetchPrDiffFresh(
3121
3137
  */
3122
3138
  export async function getOrFetchPrDiff(options: PrDiffLookupOptions): Promise<ViewLookupResult<PrDiffPayload>> {
3123
3139
  const authKey = options.cacheAuthKey === undefined ? (resolveGithubCacheAuthKey() ?? null) : options.cacheAuthKey;
3124
- const doFetch = () => fetchPrDiffFresh(options.cwd, options.repo, options.number, options.signal);
3140
+ const doFetch = (signal?: AbortSignal) =>
3141
+ fetchPrDiffFresh(
3142
+ options.cwd,
3143
+ options.repo,
3144
+ options.number,
3145
+ signal && options.signal ? AbortSignal.any([signal, options.signal]) : (signal ?? options.signal),
3146
+ );
3125
3147
  const lookup = await getOrFetchView<PrDiffPayload>({
3126
3148
  repo: options.repo,
3127
3149
  kind: "pr-diff",
@@ -3129,6 +3151,7 @@ export async function getOrFetchPrDiff(options: PrDiffLookupOptions): Promise<Vi
3129
3151
  includeComments: false,
3130
3152
  settings: options.settings,
3131
3153
  authKey,
3154
+ signal: options.signal,
3132
3155
  fetchFresh: doFetch,
3133
3156
  });
3134
3157
  return {
@@ -152,6 +152,26 @@ function evictExpired(db: Database, hardTtlMs: number): void {
152
152
  */
153
153
  const SWEEP_INTERVAL_MS = 60_000;
154
154
  let lastSweepAt = 0;
155
+ const backgroundRefreshes = new Map<string, { generation: number; controller: AbortController }>();
156
+ let backgroundRefreshGeneration = 0;
157
+ const BACKGROUND_REFRESH_TIMEOUT_MS = 30_000;
158
+
159
+ function cacheIdentityKey(
160
+ authKey: string,
161
+ repo: string,
162
+ kind: CacheKind,
163
+ number: number,
164
+ includeComments: boolean,
165
+ ): string {
166
+ return JSON.stringify([authKey, normalizeRepo(repo), kind, number, includeComments]);
167
+ }
168
+
169
+ function retireBackgroundRefresh(key: string): void {
170
+ const entry = backgroundRefreshes.get(key);
171
+ if (!entry) return;
172
+ entry.controller.abort();
173
+ backgroundRefreshes.delete(key);
174
+ }
155
175
 
156
176
  function sweepIfDue(hardTtlMs: number): void {
157
177
  const now = Date.now();
@@ -296,6 +316,12 @@ export function invalidate(
296
316
  includeComments?: boolean,
297
317
  authKey: string = DEFAULT_CACHE_AUTH_KEY,
298
318
  ): void {
319
+ if (includeComments === undefined) {
320
+ retireBackgroundRefresh(cacheIdentityKey(authKey, repo, kind, number, false));
321
+ retireBackgroundRefresh(cacheIdentityKey(authKey, repo, kind, number, true));
322
+ } else {
323
+ retireBackgroundRefresh(cacheIdentityKey(authKey, repo, kind, number, includeComments));
324
+ }
299
325
  const db = openDb();
300
326
  if (!db) return;
301
327
  try {
@@ -342,6 +368,9 @@ export function resetForTests(): void {
342
368
  cachedDb = null;
343
369
  openAttempted = false;
344
370
  lastSweepAt = 0;
371
+ for (const { controller } of backgroundRefreshes.values()) controller.abort();
372
+ backgroundRefreshes.clear();
373
+ backgroundRefreshGeneration += 1;
345
374
  }
346
375
 
347
376
  // ────────────────────────────────────────────────────────────────────────────
@@ -365,9 +394,10 @@ export interface CacheLookupOptions<T> {
365
394
  * must bypass persistent cache reads/writes.
366
395
  */
367
396
  authKey?: string | null;
368
- fetchFresh: () => Promise<FreshResult<T>>;
397
+ fetchFresh: (signal?: AbortSignal) => Promise<FreshResult<T>>;
369
398
  settings?: Settings | undefined;
370
399
  now?: number;
400
+ signal?: AbortSignal;
371
401
  }
372
402
 
373
403
  export type CacheStatus = "miss" | "fresh" | "stale" | "disabled";
@@ -448,22 +478,41 @@ function scheduleBackgroundRefresh<T>(
448
478
  kind: CacheKind,
449
479
  number: number,
450
480
  includeComments: boolean,
451
- fetchFresh: () => Promise<FreshResult<T>>,
481
+ fetchFresh: (signal?: AbortSignal) => Promise<FreshResult<T>>,
452
482
  ): void {
453
- queueMicrotask(() => {
454
- const promise = fetchFresh();
455
- promise
456
- .then(fresh => {
483
+ const key = cacheIdentityKey(authKey, repo, kind, number, includeComments);
484
+ if (backgroundRefreshes.has(key)) return;
485
+ const generation = backgroundRefreshGeneration;
486
+ const controller = new AbortController();
487
+ const entry = { generation, controller };
488
+ backgroundRefreshes.set(key, entry);
489
+ queueMicrotask(async () => {
490
+ const timeoutHandle = setTimeout(() => {
491
+ controller.abort(new Error("GitHub background refresh timed out"));
492
+ if (backgroundRefreshes.get(key) === entry) backgroundRefreshes.delete(key);
493
+ logger.debug("github cache: background refresh timed out", { repo, kind, number });
494
+ }, BACKGROUND_REFRESH_TIMEOUT_MS);
495
+ try {
496
+ const fresh = await fetchFresh(controller.signal);
497
+ if (
498
+ !controller.signal.aborted &&
499
+ backgroundRefreshes.get(key) === entry &&
500
+ backgroundRefreshGeneration === generation
501
+ ) {
457
502
  storeResult(authKey, repo, kind, number, includeComments, fresh, Date.now());
458
- })
459
- .catch(err => {
460
- logger.debug("github cache: background refresh failed", {
461
- err: String(err),
462
- repo,
463
- kind,
464
- number,
465
- });
503
+ }
504
+ } catch (err) {
505
+ controller.abort();
506
+ logger.debug("github cache: background refresh failed", {
507
+ err: String(err),
508
+ repo,
509
+ kind,
510
+ number,
466
511
  });
512
+ } finally {
513
+ clearTimeout(timeoutHandle);
514
+ if (backgroundRefreshes.get(key) === entry) backgroundRefreshes.delete(key);
515
+ }
467
516
  });
468
517
  }
469
518
 
@@ -473,9 +522,10 @@ export async function getOrFetchView<T>(options: CacheLookupOptions<T>): Promise
473
522
  const authKey = options.authKey === undefined ? DEFAULT_CACHE_AUTH_KEY : options.authKey;
474
523
 
475
524
  if (!ttl.enabled || authKey === null) {
476
- const fresh = await options.fetchFresh();
525
+ const fresh = await options.fetchFresh(options.signal);
477
526
  return { ...fresh, status: "disabled", fetchedAt: now };
478
527
  }
528
+ const cacheKey = cacheIdentityKey(authKey, options.repo, options.kind, options.number, options.includeComments);
479
529
 
480
530
  // Enforce the *configured* hard TTL against on-disk rows. This is what
481
531
  // makes `github.cache.hardTtlSec` a real retention cap rather than a soft
@@ -496,6 +546,7 @@ export async function getOrFetchView<T>(options: CacheLookupOptions<T>): Promise
496
546
  // Past hard TTL: drop the row eagerly so the on-disk exposure window
497
547
  // is bounded even if `fetchFresh()` then fails (network down, gh
498
548
  // auth lapse, etc.) and we never get to overwrite it.
549
+ retireBackgroundRefresh(cacheKey);
499
550
  invalidate(options.repo, options.kind, options.number, options.includeComments, authKey);
500
551
  } else if (age <= ttl.softMs) {
501
552
  return {
@@ -524,8 +575,9 @@ export async function getOrFetchView<T>(options: CacheLookupOptions<T>): Promise
524
575
  }
525
576
  }
526
577
 
527
- const fresh = await options.fetchFresh();
578
+ const fresh = await options.fetchFresh(options.signal);
528
579
  const fetchedAt = Date.now();
580
+ retireBackgroundRefresh(cacheKey);
529
581
  storeResult(authKey, options.repo, options.kind, options.number, options.includeComments, fresh, fetchedAt);
530
582
  return { ...fresh, status: "miss", fetchedAt };
531
583
  }