@gajae-code/coding-agent 0.15.6 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/dist/types/async/job-manager.d.ts +32 -1
  3. package/dist/types/browser-backend.d.ts +12 -0
  4. package/dist/types/cli/args.d.ts +2 -0
  5. package/dist/types/cli/root-flags.d.ts +14 -0
  6. package/dist/types/cli/update-cli.d.ts +14 -0
  7. package/dist/types/cli.d.ts +7 -0
  8. package/dist/types/commands/launch.d.ts +10 -0
  9. package/dist/types/commands/sdk.d.ts +2 -0
  10. package/dist/types/config/autorouting-tier-map.d.ts +24 -0
  11. package/dist/types/config/billing-path.d.ts +25 -0
  12. package/dist/types/config/config-schema-version.d.ts +13 -1
  13. package/dist/types/config/file-lock.d.ts +5 -0
  14. package/dist/types/config/model-registry.d.ts +22 -0
  15. package/dist/types/config/settings-schema.d.ts +27 -8
  16. package/dist/types/config/settings.d.ts +2 -0
  17. package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
  18. package/dist/types/crash/record-loader.d.ts +15 -0
  19. package/dist/types/extensibility/extensions/types.d.ts +8 -3
  20. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  21. package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
  22. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  23. package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
  24. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
  25. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  26. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  27. package/dist/types/main.d.ts +1 -0
  28. package/dist/types/master-mode/context.d.ts +21 -0
  29. package/dist/types/master-mode/first-request.d.ts +34 -0
  30. package/dist/types/master-mode/snapshot.d.ts +32 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +21 -21
  32. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  33. package/dist/types/modes/components/model-selector.d.ts +4 -0
  34. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  35. package/dist/types/modes/components/session-selector.d.ts +1 -1
  36. package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
  37. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  38. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  39. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  40. package/dist/types/modes/interactive-mode.d.ts +16 -2
  41. package/dist/types/modes/print-mode.d.ts +17 -0
  42. package/dist/types/modes/theme/theme.d.ts +2 -1
  43. package/dist/types/modes/types.d.ts +2 -0
  44. package/dist/types/modes/ui-language.d.ts +7 -1
  45. package/dist/types/sdk/broker/broker.d.ts +79 -2
  46. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  47. package/dist/types/sdk/broker/ensure.d.ts +3 -0
  48. package/dist/types/sdk/broker/index.d.ts +3 -0
  49. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
  50. package/dist/types/sdk/broker/lifecycle.d.ts +40 -0
  51. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  52. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  53. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  54. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  55. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  56. package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
  57. package/dist/types/sdk/broker/state-version.d.ts +21 -1
  58. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  59. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  60. package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
  61. package/dist/types/sdk/bus/index.d.ts +9 -1
  62. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
  63. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
  64. package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
  65. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  66. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  67. package/dist/types/sdk/cli/index.d.ts +1 -0
  68. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  69. package/dist/types/sdk/cli/rows.d.ts +12 -5
  70. package/dist/types/sdk/cli/session-cli.d.ts +20 -3
  71. package/dist/types/sdk/client/client.d.ts +3 -2
  72. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  73. package/dist/types/sdk/host/control/index.d.ts +1 -1
  74. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  75. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  76. package/dist/types/sdk/host/events.d.ts +1 -0
  77. package/dist/types/sdk/host/host.d.ts +10 -0
  78. package/dist/types/sdk/host/query/cursor.d.ts +4 -2
  79. package/dist/types/sdk/host/query/handlers.d.ts +1 -0
  80. package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
  81. package/dist/types/sdk/host/session-runtime.d.ts +70 -17
  82. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  83. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  84. package/dist/types/sdk/lifecycle-session.d.ts +3 -1
  85. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
  86. package/dist/types/sdk/receipt-state.d.ts +2 -0
  87. package/dist/types/sdk/router/session-router.d.ts +6 -0
  88. package/dist/types/sdk/session.d.ts +16 -0
  89. package/dist/types/sdk/startup-capability.d.ts +2 -1
  90. package/dist/types/sdk/turn-result.d.ts +6 -0
  91. package/dist/types/session/agent-session.d.ts +52 -7
  92. package/dist/types/session/credential-store-errors.d.ts +1 -0
  93. package/dist/types/session/fold-coordinator.d.ts +29 -2
  94. package/dist/types/session/session-manager.d.ts +79 -2
  95. package/dist/types/session/yield-queue.d.ts +5 -4
  96. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  97. package/dist/types/tools/ask.d.ts +1 -0
  98. package/dist/types/tools/bash.d.ts +16 -0
  99. package/dist/types/tools/browser/launch.d.ts +10 -0
  100. package/dist/types/tools/browser/registry.d.ts +8 -0
  101. package/dist/types/tools/gh.d.ts +8 -0
  102. package/dist/types/tools/index.d.ts +10 -10
  103. package/dist/types/tools/job.d.ts +3 -1
  104. package/dist/types/utils/external-editor.d.ts +7 -0
  105. package/examples/extensions/README.md +1 -1
  106. package/examples/extensions/pirate.ts +7 -4
  107. package/package.json +7 -7
  108. package/scripts/generate-sdk-operation-inventory.ts +13 -1
  109. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  110. package/src/async/job-manager.ts +107 -9
  111. package/src/browser-backend.ts +29 -0
  112. package/src/cli/accounts-cli.ts +9 -5
  113. package/src/cli/args.ts +17 -0
  114. package/src/cli/fast-help.ts +1 -0
  115. package/src/cli/plugin-cli.ts +29 -0
  116. package/src/cli/root-flags.ts +5 -0
  117. package/src/cli/update-cli.ts +140 -23
  118. package/src/commands/launch.ts +86 -18
  119. package/src/commands/sdk.ts +135 -28
  120. package/src/config/autorouting-tier-map.ts +12 -0
  121. package/src/config/billing-path.ts +42 -0
  122. package/src/config/config-schema-version.ts +18 -1
  123. package/src/config/file-lock.ts +158 -83
  124. package/src/config/model-preset-registry.ts +178 -23
  125. package/src/config/model-profiles.ts +5 -5
  126. package/src/config/model-registry.ts +291 -42
  127. package/src/config/model-resolver.ts +5 -0
  128. package/src/config/settings-schema.ts +27 -8
  129. package/src/config/settings.ts +51 -15
  130. package/src/coordinator-mcp/projection-scan.ts +27 -6
  131. package/src/coordinator-mcp/question-state.ts +9 -11
  132. package/src/coordinator-mcp/server.ts +115 -21
  133. package/src/crash/index-store.ts +2 -0
  134. package/src/crash/record-loader.ts +10 -0
  135. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  136. package/src/extensibility/extensions/runner.ts +27 -0
  137. package/src/extensibility/extensions/types.ts +8 -3
  138. package/src/extensibility/gjc-plugins/installer.ts +51 -11
  139. package/src/extensibility/runtime-skill-discovery.ts +110 -3
  140. package/src/extensibility/skills.ts +22 -12
  141. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  142. package/src/gjc-runtime/launch-worktree.ts +285 -4
  143. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  144. package/src/gjc-runtime/session-state-lock.ts +213 -14
  145. package/src/gjc-runtime/session-state-sidecar.ts +938 -39
  146. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  147. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -3
  148. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  149. package/src/hooks/skill-state.ts +6 -0
  150. package/src/internal-urls/docs-index.generated.ts +15 -15
  151. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  152. package/src/main.ts +61 -3
  153. package/src/master-mode/context.ts +50 -0
  154. package/src/master-mode/first-request.ts +65 -0
  155. package/src/master-mode/snapshot.ts +205 -0
  156. package/src/modes/acp/acp-agent.ts +610 -161
  157. package/src/modes/components/footer.ts +4 -2
  158. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  159. package/src/modes/components/model-selector.ts +76 -8
  160. package/src/modes/components/oauth-selector.ts +6 -1
  161. package/src/modes/components/session-selector.ts +23 -15
  162. package/src/modes/components/sessions-dashboard.ts +8 -5
  163. package/src/modes/components/settings-selector.ts +1 -0
  164. package/src/modes/components/status-line/model-name.ts +53 -0
  165. package/src/modes/components/status-line/priority-row.ts +107 -0
  166. package/src/modes/components/status-line/segments.ts +23 -9
  167. package/src/modes/components/tool-status-header.ts +151 -15
  168. package/src/modes/controllers/command-controller.ts +1 -1
  169. package/src/modes/controllers/event-controller.ts +30 -4
  170. package/src/modes/controllers/extension-ui-controller.ts +13 -13
  171. package/src/modes/controllers/input-controller.ts +30 -2
  172. package/src/modes/controllers/selector-controller.ts +200 -98
  173. package/src/modes/interactive-mode.ts +36 -12
  174. package/src/modes/print-mode.ts +64 -0
  175. package/src/modes/prompt-action-autocomplete.ts +30 -4
  176. package/src/modes/runtime-init.ts +12 -9
  177. package/src/modes/theme/theme.ts +5 -0
  178. package/src/modes/types.ts +2 -0
  179. package/src/modes/ui-language.ts +49 -2
  180. package/src/modes/utils/ui-helpers.ts +22 -3
  181. package/src/prompts/agent-fragments/browser-aside.md +22 -0
  182. package/src/prompts/system/master-mode.md +28 -0
  183. package/src/prompts/tools/ask.md +1 -1
  184. package/src/prompts/tools/bash.md +1 -0
  185. package/src/prompts/tools/job.md +1 -1
  186. package/src/sdk/acp/adapter.ts +5 -0
  187. package/src/sdk/broker/broker.ts +1646 -27
  188. package/src/sdk/broker/endpoint-authority.ts +86 -0
  189. package/src/sdk/broker/ensure.ts +81 -3
  190. package/src/sdk/broker/index.ts +3 -0
  191. package/src/sdk/broker/lifecycle-ledger.ts +6 -0
  192. package/src/sdk/broker/lifecycle.ts +440 -75
  193. package/src/sdk/broker/master-capability.ts +232 -0
  194. package/src/sdk/broker/session-index.ts +121 -17
  195. package/src/sdk/broker/session-scope.ts +272 -0
  196. package/src/sdk/broker/spawn-authority.ts +1031 -0
  197. package/src/sdk/broker/spawn-substrate.ts +591 -0
  198. package/src/sdk/broker/startup-budget.ts +144 -2
  199. package/src/sdk/broker/state-version.ts +43 -14
  200. package/src/sdk/broker/transport.ts +1 -0
  201. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  202. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  203. package/src/sdk/bus/chat-daemon-runtime.ts +136 -18
  204. package/src/sdk/bus/discord-daemon.ts +50 -10
  205. package/src/sdk/bus/index.ts +84 -7
  206. package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
  207. package/src/sdk/bus/lifecycle-commands.ts +4 -0
  208. package/src/sdk/bus/notification-service.ts +22 -5
  209. package/src/sdk/bus/prompt-reconciliation.ts +7 -2
  210. package/src/sdk/bus/reconciliation-store.ts +55 -31
  211. package/src/sdk/bus/slack-conversation.ts +1 -1
  212. package/src/sdk/bus/slack-daemon.ts +72 -38
  213. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  214. package/src/sdk/cli/index.ts +1 -0
  215. package/src/sdk/cli/master-cli.ts +259 -0
  216. package/src/sdk/cli/rows.ts +41 -15
  217. package/src/sdk/cli/session-cli.ts +297 -26
  218. package/src/sdk/client/client.ts +14 -8
  219. package/src/sdk/host/control/dispatch.ts +101 -40
  220. package/src/sdk/host/control/index.ts +2 -0
  221. package/src/sdk/host/control/operations.ts +2 -0
  222. package/src/sdk/host/control/runtime-gate.ts +203 -11
  223. package/src/sdk/host/events.ts +1 -0
  224. package/src/sdk/host/host.ts +29 -2
  225. package/src/sdk/host/query/cursor.ts +52 -7
  226. package/src/sdk/host/query/handlers.ts +33 -12
  227. package/src/sdk/host/query/revision-store.ts +12 -0
  228. package/src/sdk/host/session-runtime.test.ts +673 -14
  229. package/src/sdk/host/session-runtime.ts +674 -103
  230. package/src/sdk/host/surface-policy.ts +3 -3
  231. package/src/sdk/lifecycle/broker-client.ts +28 -0
  232. package/src/sdk/lifecycle/service.ts +279 -20
  233. package/src/sdk/lifecycle-session.ts +17 -4
  234. package/src/sdk/prompt-deadline-manager.ts +2 -1
  235. package/src/sdk/protocol/operation-inventory.generated.json +105 -3
  236. package/src/sdk/protocol/operation-registry.ts +14 -4
  237. package/src/sdk/receipt-state.ts +10 -0
  238. package/src/sdk/router/session-router.ts +219 -118
  239. package/src/sdk/session.ts +416 -195
  240. package/src/sdk/startup-capability.ts +1 -0
  241. package/src/sdk/turn-result.ts +22 -0
  242. package/src/session/agent-session.ts +1628 -318
  243. package/src/session/blob-store.ts +15 -5
  244. package/src/session/credential-store-errors.ts +2 -0
  245. package/src/session/fold-coordinator.ts +40 -3
  246. package/src/session/internal/managed-session-storage.ts +153 -78
  247. package/src/session/session-manager.ts +850 -97
  248. package/src/session/yield-queue.ts +24 -10
  249. package/src/setup/frictionless-onboarding.ts +2 -0
  250. package/src/slash-commands/builtin-registry.ts +41 -0
  251. package/src/task/executor.ts +9 -4
  252. package/src/telemetry/events.ts +1138 -12
  253. package/src/tools/ask-contract.test.ts +64 -0
  254. package/src/tools/ask-contract.ts +36 -1
  255. package/src/tools/ask.ts +10 -8
  256. package/src/tools/bash-interactive.ts +11 -2
  257. package/src/tools/bash.ts +412 -75
  258. package/src/tools/browser/launch.ts +37 -0
  259. package/src/tools/browser/registry.ts +99 -9
  260. package/src/tools/cron.ts +6 -4
  261. package/src/tools/descriptors.test.ts +15 -0
  262. package/src/tools/descriptors.ts +5 -1
  263. package/src/tools/gh.ts +104 -48
  264. package/src/tools/github-cache.ts +2 -2
  265. package/src/tools/index.ts +10 -6
  266. package/src/tools/job.ts +4 -1
  267. package/src/tools/monitor.ts +12 -9
  268. package/src/tools/resolve.ts +30 -10
  269. package/src/tools/tool-catalog.generated.ts +2 -2
  270. package/src/utils/external-editor.ts +72 -1
@@ -293,6 +293,43 @@ function resolveSystemChromium(): string | undefined {
293
293
  return found;
294
294
  }
295
295
 
296
+ export function resolveSystemChromiumForTest(): string | undefined {
297
+ return resolveSystemChromium();
298
+ }
299
+
300
+ export function resetSystemChromiumForTest(): void {
301
+ resolvedChromium = undefined;
302
+ }
303
+
304
+ export function headlessExecutableForProfileReuse(): string | undefined {
305
+ return resolveSystemChromium() ?? trustedBrowserEnv("PUPPETEER_EXECUTABLE_PATH");
306
+ }
307
+
308
+ /**
309
+ * On macOS the Cookies DB is encrypted with a Keychain key whose service name is
310
+ * brand-specific ("Chrome Safe Storage" vs "Chromium Safe Storage"). A Chrome profile
311
+ * copied into a bundled Chromium therefore decrypts to garbage with no warning.
312
+ * Return true only when the headless executable can actually decrypt the discovered profile.
313
+ */
314
+ export function canDecryptProfileCookies(
315
+ executablePath: string | undefined,
316
+ discoveredUserDataDir: string | undefined,
317
+ platform: NodeJS.Platform = process.platform,
318
+ ): boolean {
319
+ if (platform !== "darwin") return true;
320
+ if (!discoveredUserDataDir) return true;
321
+ if (!executablePath) return false;
322
+ const lowerExe = executablePath.toLowerCase();
323
+ const lowerDir = discoveredUserDataDir.toLowerCase();
324
+ const isChromiumExe = lowerExe.includes("chromium");
325
+ const isChromeExe = lowerExe.includes("google chrome") || lowerExe.includes("/google/chrome");
326
+ const isChromiumProfile = lowerDir.includes("chromium");
327
+ const isChromeProfile = lowerDir.includes("google") && lowerDir.includes("chrome");
328
+ if (isChromeProfile && isChromiumExe && !isChromeExe) return false;
329
+ if (isChromiumProfile && isChromeExe) return false;
330
+ return true;
331
+ }
332
+
296
333
  /** Edge is Chromium-based but keeps its own profile format under its own user data root. */
297
334
  const EDGE_EXECUTABLE_PATTERN =
298
335
  /(?:^|[/\\])(?:msedge(?:\.exe)?|microsoft-edge(?:-(?:stable|beta|dev|canary))?|com\.microsoft\.Edge|Microsoft Edge(?: Beta| Dev| Canary)?)$/i;
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs";
2
+ import * as os from "node:os";
2
3
  import * as path from "node:path";
3
4
  import { logger } from "@gajae-code/utils";
4
5
  import type { Subprocess } from "bun";
@@ -12,7 +13,14 @@ import {
12
13
  killExistingByPath,
13
14
  waitForCdp,
14
15
  } from "./attach";
15
- import { BROWSER_PROTOCOL_TIMEOUT_MS, launchHeadlessBrowser, loadPuppeteer, type UserAgentOverride } from "./launch";
16
+ import {
17
+ BROWSER_PROTOCOL_TIMEOUT_MS,
18
+ canDecryptProfileCookies,
19
+ headlessExecutableForProfileReuse,
20
+ launchHeadlessBrowser,
21
+ loadPuppeteer,
22
+ type UserAgentOverride,
23
+ } from "./launch";
16
24
  import { defaultDiscoveryEnv } from "./profile-discovery";
17
25
  import type { ProfileReusePosture } from "./profile-posture";
18
26
  import { resolveProfileReuse } from "./profile-reuse";
@@ -55,6 +63,17 @@ type SpawnedChromeProfileKind = Extract<BrowserKind, { kind: "chrome-profile" }>
55
63
  const browsers = new Map<string, BrowserHandle>();
56
64
  const browserOpenPromises = new Map<string, Promise<BrowserHandle>>();
57
65
 
66
+ /**
67
+ * Chrome profile warm-up dirs this module created, keyed by the handle that owns one.
68
+ *
69
+ * Ownership is a registry-internal fact, not a property of the handle: `BrowserHandle`
70
+ * is exported and mutable, so a field would let any caller point recursive deletion at
71
+ * an arbitrary path. Recording the dir here — only on the launch path that created it —
72
+ * means disposal can delete exactly what this module made and nothing else. A path
73
+ * prefix check would not prove ownership; an entry in this map does.
74
+ */
75
+ const ownedWarmupDirs = new WeakMap<BrowserHandle, string>();
76
+
58
77
  /**
59
78
  * Upper bound on the CDP `browser.close()` round-trip during a forced (signal-path)
60
79
  * teardown before we fall back to killing the Chrome process tree. Only applies when
@@ -110,6 +129,19 @@ export function browserKeyForTest(
110
129
  return browserKey(kind, geo, opts.profileReuse);
111
130
  }
112
131
 
132
+ /**
133
+ * Create and register a warm-up dir as registry-owned for `handle`.
134
+ *
135
+ * Exists so tests can exercise disposal without spawning Chrome. The caller cannot
136
+ * supply the deletion target: this module creates the directory itself before recording
137
+ * ownership, preserving the same invariant as the production launch path.
138
+ */
139
+ export function createOwnedWarmupDirForTest(handle: BrowserHandle): string {
140
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "gjc-profile-warmup-"));
141
+ ownedWarmupDirs.set(handle, dir);
142
+ return dir;
143
+ }
144
+
113
145
  export async function acquireBrowser(kind: BrowserKind, opts: AcquireBrowserOptions): Promise<BrowserHandle> {
114
146
  const geo = kind.kind === "headless" ? normalizeGeo(opts.geo) : undefined;
115
147
  const key = browserKey(kind, geo, opts.profileReuse);
@@ -148,17 +180,38 @@ async function openBrowserHandle(
148
180
  discoveryEnv: defaultDiscoveryEnv(fs.existsSync),
149
181
  });
150
182
  if (reuse.warning) logger.warn(reuse.warning);
151
- if (reuse.mode === "real" && reuse.warmupDir) {
183
+ if (reuse.mode === "real" && reuse.warmupDir && reuse.discovered) {
184
+ const executablePath = headlessExecutableForProfileReuse();
185
+ if (!canDecryptProfileCookies(executablePath, reuse.discovered.userDataDir)) {
186
+ try {
187
+ fs.rmSync(reuse.warmupDir, { recursive: true, force: true });
188
+ } catch {}
189
+ const hint = executablePath
190
+ ? `Real Chrome profile at ${reuse.discovered.userDataDir} cannot be decrypted by ${executablePath} on macOS (Chrome Safe Storage vs Chromium Safe Storage Keychain mismatch). Using synthetic browser state instead. Set PUPPETEER_EXECUTABLE_PATH to the matching Chrome/Chromium binary.`
191
+ : `Real Chrome profile at ${reuse.discovered.userDataDir} found but no compatible browser executable is available on macOS (bundled Chromium cannot decrypt Chrome's cookies — Chrome Safe Storage vs Chromium Safe Storage). Using synthetic browser state instead. Set PUPPETEER_EXECUTABLE_PATH to /Applications/Google Chrome.app/Contents/MacOS/Google Chrome or install Chrome.`;
192
+ logger.warn(hint);
193
+ } else {
194
+ profileWarmupDir = reuse.warmupDir;
195
+ }
196
+ } else if (reuse.mode === "real" && reuse.warmupDir) {
152
197
  profileWarmupDir = reuse.warmupDir;
153
198
  }
154
199
  }
155
- const browser = await launchHeadlessBrowser({
156
- headless: kind.headless,
157
- viewport: opts.viewport,
158
- profileWarmupDir,
159
- ...(geo ? { geo } : {}),
160
- });
161
- return {
200
+ let browser: Browser;
201
+ try {
202
+ browser = await launchHeadlessBrowser({
203
+ headless: kind.headless,
204
+ viewport: opts.viewport,
205
+ profileWarmupDir,
206
+ ...(geo ? { geo } : {}),
207
+ });
208
+ } catch (error) {
209
+ // Launch failed after the warm-up copy landed: drop the temp dir here,
210
+ // because no handle will exist to carry it into releaseBrowser().
211
+ removeWarmupDir(profileWarmupDir);
212
+ throw error;
213
+ }
214
+ const handle: BrowserHandle = {
162
215
  key,
163
216
 
164
217
  kind,
@@ -168,6 +221,11 @@ async function openBrowserHandle(
168
221
  refCount: 0,
169
222
  stealth: { browserSession: null, override: null },
170
223
  };
224
+ // Record ownership only here, where this module created the dir. Disposal
225
+ // deletes solely what this map holds, so a caller-supplied path is never
226
+ // a deletion target.
227
+ if (profileWarmupDir) ownedWarmupDirs.set(handle, profileWarmupDir);
228
+ return handle;
171
229
  }
172
230
  if (kind.kind === "connected") {
173
231
  const cdpUrl = kind.cdpUrl.replace(/\/+$/, "");
@@ -418,6 +476,35 @@ export async function releaseBrowser(handle: BrowserHandle, opts: { kill: boolea
418
476
  }
419
477
  }
420
478
 
479
+ /**
480
+ * Delete the warm-up dir this module created for `handle`, if any.
481
+ *
482
+ * The path comes from `ownedWarmupDirs`, never from the caller, so recursive deletion
483
+ * can only ever target a dir this registry made. The entry is consumed first, making a
484
+ * second call a no-op even if disposal runs twice.
485
+ *
486
+ * Best-effort: a leftover temp dir is preferable to a teardown that throws.
487
+ */
488
+ function removeOwnedWarmupDir(handle: BrowserHandle): void {
489
+ const dir = ownedWarmupDirs.get(handle);
490
+ if (dir === undefined) return;
491
+ ownedWarmupDirs.delete(handle);
492
+ removeWarmupDir(dir);
493
+ }
494
+
495
+ /** Delete a dir this module created. Callers must have proven ownership first. */
496
+ function removeWarmupDir(dir: string | undefined): void {
497
+ if (!dir) return;
498
+ try {
499
+ fs.rmSync(dir, { recursive: true, force: true });
500
+ } catch (err) {
501
+ logger.debug("Failed to remove Chrome profile warm-up dir", {
502
+ dir,
503
+ error: (err as Error).message,
504
+ });
505
+ }
506
+ }
507
+
421
508
  async function disposeBrowserHandle(handle: BrowserHandle, opts: { kill: boolean }): Promise<void> {
422
509
  if (handle.kind.kind === "headless") {
423
510
  // Capture the launched Chrome process before close() so a forced (signal-path)
@@ -436,6 +523,9 @@ async function disposeBrowserHandle(handle: BrowserHandle, opts: { kill: boolean
436
523
  }
437
524
  }
438
525
  if (opts.kill && proc?.pid !== undefined) await gracefulKillTreeOnce(proc.pid);
526
+ // Chrome owns files under the warm-up dir until the process is gone, so this
527
+ // runs after close()/kill rather than alongside them.
528
+ removeOwnedWarmupDir(handle);
439
529
  return;
440
530
  }
441
531
  if (handle.kind.kind === "connected") {
package/src/tools/cron.ts CHANGED
@@ -453,13 +453,15 @@ function deliverCronFire(record: CronScheduleRecord): Promise<void> | undefined
453
453
  cron_expression: record.snapshot.cron_expression,
454
454
  recurring: record.snapshot.recurring,
455
455
  };
456
- const sendPromise = record.session.sendCustomMessage?.(
456
+ if (!record.session.sendCustomMessage) {
457
+ // The custom-message router is the only delivery path; treating its absence
458
+ // as a successful fire would silently retire a one-shot schedule.
459
+ return Promise.reject(new Error("Cron delivery requires a session custom-message router."));
460
+ }
461
+ return record.session.sendCustomMessage(
457
462
  { customType: "cron-fire", content, display: false, attribution: "agent", details },
458
463
  { triggerTurn: true, deliverAs: "followUp" },
459
464
  );
460
- if (sendPromise) return sendPromise;
461
- record.session.steer?.({ customType: "cron-fire", content, details });
462
- return undefined;
463
465
  }
464
466
 
465
467
  function scheduleRecord(ownerId: string | undefined, state: OwnerScheduleState, record: CronScheduleRecord): void {
@@ -39,6 +39,7 @@ function makeSession(overrides: Record<string, unknown> = {}): any {
39
39
  "calc.enabled": true,
40
40
  "skill.enabled": true,
41
41
  "browser.enabled": true,
42
+ "browser.backend": "native",
42
43
  "computer.enabled": true,
43
44
  "checkpoint.enabled": true,
44
45
  "irc.enabled": true,
@@ -371,6 +372,20 @@ describe("tool descriptor compatibility gate", () => {
371
372
  }
372
373
  });
373
374
 
375
+ test("hides the built-in browser tool when Aside is the selected backend", () => {
376
+ const context = availabilityContext();
377
+
378
+ expect(BUILTIN_TOOL_DESCRIPTORS.browser.isAvailable(makeSession({ "browser.backend": "aside" }), context)).toBe(
379
+ false,
380
+ );
381
+ expect(BUILTIN_TOOL_DESCRIPTORS.browser.isAvailable(makeSession({ "browser.backend": "native" }), context)).toBe(
382
+ true,
383
+ );
384
+ expect(BUILTIN_TOOL_DESCRIPTORS.browser.isAvailable(makeSession({ "browser.backend": undefined }), context)).toBe(
385
+ true,
386
+ );
387
+ });
388
+
374
389
  test("descriptor creation is side-effect free; materialization registers cleanup exactly once", () => {
375
390
  const cleanupCalls: Array<() => void> = [];
376
391
  let constructed = 0;
@@ -1,6 +1,7 @@
1
1
  import type { AgentTool } from "@gajae-code/agent-core";
2
2
  import type { RawArgumentValidationResult, TSchema } from "@gajae-code/ai/types";
3
3
  import { $which } from "@gajae-code/utils";
4
+ import { resolveBrowserBackend } from "../browser-backend";
4
5
  import type { ToolFactory, ToolSession } from ".";
5
6
  import { selectAskParameters } from "./ask-contract";
6
7
  import { isComputerCallable, isComputerLoadablePlatform } from "./computer-policy";
@@ -260,7 +261,10 @@ function availableFor(name: string, session: ToolSession, context = defaultAvail
260
261
  if (name === "search_tool_bm25") return context.discoveryActive ?? resolveDiscoveryActive(session);
261
262
  if (name === "calc") return Boolean(session.settings.get("calc.enabled"));
262
263
  if (name === "skill" || name === "skill_discovery") return Boolean(session.settings.get("skill.enabled"));
263
- if (name === "browser") return Boolean(session.settings.get("browser.enabled"));
264
+ if (name === "browser")
265
+ return (
266
+ Boolean(session.settings.get("browser.enabled")) && resolveBrowserBackend(session.settings).exposesBuiltinTool
267
+ );
264
268
  if (name === "computer") return isComputerCallable(session);
265
269
  if (name === "checkpoint" || name === "rewind")
266
270
  return Boolean(session.settings.get("checkpoint.enabled")) && (session.taskDepth ?? 0) === 0;
package/src/tools/gh.ts CHANGED
@@ -2806,6 +2806,14 @@ export interface PrDiffFile {
2806
2806
  changeType: "modified" | "added" | "deleted" | "renamed" | "binary";
2807
2807
  /** Pre-image path for renames/deletes; same as `path` otherwise. */
2808
2808
  oldPath?: string;
2809
+ /**
2810
+ * Set when `path` kept its C-style escaped source form because the quoted
2811
+ * token held invalid UTF-8 or a malformed escape; distinguishes it from a
2812
+ * valid path whose decoded text looks identical.
2813
+ */
2814
+ pathEscaped?: boolean;
2815
+ /** Same as {@link PrDiffFile.pathEscaped}, for `oldPath`. */
2816
+ oldPathEscaped?: boolean;
2809
2817
  /** Byte offset of the section's `diff --git` line in the unified diff. */
2810
2818
  startOffset: number;
2811
2819
  /** Byte offset of the next section (or end-of-text). */
@@ -2861,6 +2869,8 @@ export function parsePrUnifiedDiff(text: string): PrDiffPayload {
2861
2869
  interface ParsedDiffHeaderToken {
2862
2870
  value: string;
2863
2871
  nextIndex: number;
2872
+ /** Set when the token kept its C-style escaped source form. */
2873
+ escaped?: boolean;
2864
2874
  }
2865
2875
 
2866
2876
  function skipDiffHeaderSpaces(text: string, index: number): number {
@@ -2869,63 +2879,87 @@ function skipDiffHeaderSpaces(text: string, index: number): number {
2869
2879
  return i;
2870
2880
  }
2871
2881
 
2872
- function parseDiffQuotedEscape(text: string, slashIndex: number): ParsedDiffHeaderToken {
2873
- const next = text.charAt(slashIndex + 1);
2874
- if (next === "") return { value: "\\", nextIndex: slashIndex + 1 };
2882
+ interface ParsedDiffQuotedEscape {
2883
+ byte: number;
2884
+ nextIndex: number;
2885
+ }
2875
2886
 
2876
- if (next >= "0" && next <= "7") {
2877
- let end = slashIndex + 1;
2878
- while (end < text.length && end < slashIndex + 4) {
2879
- const digit = text.charAt(end);
2880
- if (digit < "0" || digit > "7") break;
2881
- end += 1;
2882
- }
2883
- return {
2884
- value: String.fromCharCode(Number.parseInt(text.slice(slashIndex + 1, end), 8)),
2885
- nextIndex: end,
2886
- };
2887
- }
2887
+ const DIFF_PATH_ENCODER = new TextEncoder();
2888
+ const DIFF_PATH_DECODER = new TextDecoder("utf-8", { fatal: true });
2888
2889
 
2889
- switch (next) {
2890
- case "a":
2891
- return { value: "\x07", nextIndex: slashIndex + 2 };
2892
- case "b":
2893
- return { value: "\b", nextIndex: slashIndex + 2 };
2894
- case "f":
2895
- return { value: "\f", nextIndex: slashIndex + 2 };
2896
- case "n":
2897
- return { value: "\n", nextIndex: slashIndex + 2 };
2898
- case "r":
2899
- return { value: "\r", nextIndex: slashIndex + 2 };
2900
- case "t":
2901
- return { value: "\t", nextIndex: slashIndex + 2 };
2902
- case "v":
2903
- return { value: "\v", nextIndex: slashIndex + 2 };
2904
- case "\\":
2905
- case '"':
2906
- return { value: next, nextIndex: slashIndex + 2 };
2907
- default:
2908
- return { value: next, nextIndex: slashIndex + 2 };
2890
+ const DIFF_NAMED_ESCAPE_BYTES: Record<string, number> = {
2891
+ a: 0x07,
2892
+ b: 0x08,
2893
+ f: 0x0c,
2894
+ n: 0x0a,
2895
+ r: 0x0d,
2896
+ t: 0x09,
2897
+ v: 0x0b,
2898
+ "\\": 0x5c,
2899
+ '"': 0x22,
2900
+ };
2901
+
2902
+ /**
2903
+ * Git quotes paths C-style: its named escapes plus exactly three octal digits
2904
+ * per byte (leading digit 0-3), so a multi-byte UTF-8 character arrives as
2905
+ * consecutive octal escapes. Anything else is malformed for this grammar.
2906
+ */
2907
+ function parseDiffQuotedEscape(text: string, slashIndex: number): ParsedDiffQuotedEscape | undefined {
2908
+ const first = text.charAt(slashIndex + 1);
2909
+ if (first >= "0" && first <= "3") {
2910
+ const second = text.charAt(slashIndex + 2);
2911
+ const third = text.charAt(slashIndex + 3);
2912
+ if (second < "0" || second > "7" || third < "0" || third > "7") return undefined;
2913
+ return { byte: Number.parseInt(text.slice(slashIndex + 1, slashIndex + 4), 8), nextIndex: slashIndex + 4 };
2909
2914
  }
2915
+ const named = DIFF_NAMED_ESCAPE_BYTES[first];
2916
+ if (named === undefined) return undefined;
2917
+ return { byte: named, nextIndex: slashIndex + 2 };
2910
2918
  }
2911
2919
 
2920
+ /**
2921
+ * Escaped bytes are decoded as strict UTF-8. A token holding invalid UTF-8 or
2922
+ * a malformed escape keeps its escaped source form, so distinct non-UTF-8
2923
+ * byte sequences never collapse into one replacement-character path.
2924
+ */
2912
2925
  function parseDiffQuotedToken(text: string, startIndex: number): ParsedDiffHeaderToken | undefined {
2913
2926
  if (text.charAt(startIndex) !== '"') return undefined;
2914
- let value = "";
2915
- for (let i = startIndex + 1; i < text.length; i += 1) {
2916
- const ch = text.charAt(i);
2917
- if (ch === '"') return { value, nextIndex: i + 1 };
2927
+ const bytes: number[] = [];
2928
+ let malformed = false;
2929
+ for (let i = startIndex + 1; i < text.length; ) {
2930
+ const codePoint = text.codePointAt(i) as number;
2931
+ const ch = String.fromCodePoint(codePoint);
2932
+ if (ch === '"') {
2933
+ if (!malformed) {
2934
+ try {
2935
+ return { value: DIFF_PATH_DECODER.decode(Uint8Array.from(bytes)), nextIndex: i + 1 };
2936
+ } catch {}
2937
+ }
2938
+ return { value: text.slice(startIndex + 1, i), nextIndex: i + 1, escaped: true };
2939
+ }
2918
2940
  if (ch !== "\\") {
2919
- value += ch;
2941
+ bytes.push(...DIFF_PATH_ENCODER.encode(ch));
2942
+ i += ch.length;
2920
2943
  continue;
2921
2944
  }
2922
2945
  const escaped = parseDiffQuotedEscape(text, i);
2923
- value += escaped.value;
2924
- i = escaped.nextIndex - 1;
2946
+ if (escaped === undefined) {
2947
+ malformed = true;
2948
+ i += 2;
2949
+ continue;
2950
+ }
2951
+ bytes.push(escaped.byte);
2952
+ i = escaped.nextIndex;
2925
2953
  }
2926
2954
  return undefined;
2927
2955
  }
2928
2956
 
2957
+ function parseDiffPathLine(line: string, prefix: string): { value: string; escaped?: boolean } {
2958
+ const rest = line.slice(prefix.length);
2959
+ const token = parseDiffQuotedToken(rest, 0);
2960
+ return token ? { value: token.value, escaped: token.escaped } : { value: rest };
2961
+ }
2962
+
2929
2963
  function parseDiffHeaderToken(text: string, startIndex: number): ParsedDiffHeaderToken | undefined {
2930
2964
  const start = skipDiffHeaderSpaces(text, startIndex);
2931
2965
  if (start >= text.length) return undefined;
@@ -2940,7 +2974,14 @@ function stripPrDiffPathPrefix(value: string, prefix: "a/" | "b/"): string | und
2940
2974
  return value.startsWith(prefix) ? value.slice(prefix.length) : undefined;
2941
2975
  }
2942
2976
 
2943
- function parsePrDiffHeaderPaths(header: string): { oldPath?: string; newPath?: string } {
2977
+ interface PrDiffHeaderPaths {
2978
+ oldPath?: string;
2979
+ newPath?: string;
2980
+ oldPathEscaped?: boolean;
2981
+ newPathEscaped?: boolean;
2982
+ }
2983
+
2984
+ function parsePrDiffHeaderPaths(header: string): PrDiffHeaderPaths {
2944
2985
  const trail = header.slice("diff --git ".length);
2945
2986
  if (trail.startsWith('"')) {
2946
2987
  const oldToken = parseDiffQuotedToken(trail, 0);
@@ -2950,6 +2991,8 @@ function parsePrDiffHeaderPaths(header: string): { oldPath?: string; newPath?: s
2950
2991
  return {
2951
2992
  oldPath: stripPrDiffPathPrefix(oldToken.value, "a/"),
2952
2993
  newPath: stripPrDiffPathPrefix(newToken.value, "b/"),
2994
+ oldPathEscaped: oldToken.escaped,
2995
+ newPathEscaped: newToken.escaped,
2953
2996
  };
2954
2997
  }
2955
2998
 
@@ -2980,6 +3023,8 @@ function parsePrDiffSection(section: string, startOffset: number, endOffset: num
2980
3023
  const headerPaths = parsePrDiffHeaderPaths(header);
2981
3024
  let oldPath = headerPaths.oldPath;
2982
3025
  let newPath = headerPaths.newPath;
3026
+ let oldPathEscaped = headerPaths.oldPathEscaped;
3027
+ let newPathEscaped = headerPaths.newPathEscaped;
2983
3028
 
2984
3029
  let changeType: PrDiffFile["changeType"] = "modified";
2985
3030
  let isBinary = false;
@@ -2999,11 +3044,11 @@ function parsePrDiffSection(section: string, startOffset: number, endOffset: num
2999
3044
  }
3000
3045
  if (line.startsWith("rename from ")) {
3001
3046
  changeType = "renamed";
3002
- oldPath = line.slice("rename from ".length);
3047
+ ({ value: oldPath, escaped: oldPathEscaped } = parseDiffPathLine(line, "rename from "));
3003
3048
  continue;
3004
3049
  }
3005
3050
  if (line.startsWith("rename to ")) {
3006
- newPath = line.slice("rename to ".length);
3051
+ ({ value: newPath, escaped: newPathEscaped } = parseDiffPathLine(line, "rename to "));
3007
3052
  continue;
3008
3053
  }
3009
3054
  if (line.startsWith("Binary files ") && line.endsWith(" differ")) {
@@ -3028,8 +3073,15 @@ function parsePrDiffSection(section: string, startOffset: number, endOffset: num
3028
3073
  deletions = 0;
3029
3074
  }
3030
3075
 
3031
- const displayPath =
3032
- changeType === "deleted" ? (oldPath ?? newPath ?? "(unknown)") : (newPath ?? oldPath ?? "(unknown)");
3076
+ const preferOld = changeType === "deleted";
3077
+ const displayPath = preferOld ? (oldPath ?? newPath ?? "(unknown)") : (newPath ?? oldPath ?? "(unknown)");
3078
+ const displayEscaped = preferOld
3079
+ ? oldPath !== undefined
3080
+ ? oldPathEscaped
3081
+ : newPathEscaped
3082
+ : newPath !== undefined
3083
+ ? newPathEscaped
3084
+ : oldPathEscaped;
3033
3085
  const file: PrDiffFile = {
3034
3086
  path: displayPath,
3035
3087
  additions,
@@ -3038,8 +3090,12 @@ function parsePrDiffSection(section: string, startOffset: number, endOffset: num
3038
3090
  startOffset,
3039
3091
  endOffset,
3040
3092
  };
3041
- if (oldPath && oldPath !== displayPath) {
3093
+ if (displayEscaped) {
3094
+ file.pathEscaped = true;
3095
+ }
3096
+ if (oldPath && (oldPath !== displayPath || oldPathEscaped !== displayEscaped)) {
3042
3097
  file.oldPath = oldPath;
3098
+ if (oldPathEscaped) file.oldPathEscaped = true;
3043
3099
  }
3044
3100
  return file;
3045
3101
  }
@@ -103,7 +103,7 @@ export function openDb(): Database | null {
103
103
  // than running an in-place ALTER dance.
104
104
  const userVersion = (db.prepare("PRAGMA user_version").get() as { user_version?: number } | undefined)
105
105
  ?.user_version;
106
- if (userVersion !== undefined && userVersion < 3) {
106
+ if (userVersion !== undefined && userVersion < 4) {
107
107
  db.run("DROP TABLE IF EXISTS github_view_cache");
108
108
  }
109
109
  db.run(`
@@ -120,7 +120,7 @@ export function openDb(): Database | null {
120
120
  PRIMARY KEY (auth_key, repo, kind, number, include_comments)
121
121
  );
122
122
  CREATE INDEX IF NOT EXISTS idx_github_view_cache_fetched ON github_view_cache(fetched_at);
123
- PRAGMA user_version = 3;
123
+ PRAGMA user_version = 4;
124
124
  `);
125
125
  protectDbFiles(dbPath);
126
126
  cachedDb = db;
@@ -1,7 +1,7 @@
1
1
  import type { AgentTelemetryConfig, AgentTool } from "@gajae-code/agent-core";
2
2
  import type { Model, ServiceTier, ToolChoice } from "@gajae-code/ai/core";
3
3
  import { $env, logger } from "@gajae-code/utils";
4
- import type { AsyncJobManager } from "../async";
4
+ import type { AsyncJobManager, FoldReason } from "../async";
5
5
  import type { PromptTemplate } from "../config/prompt-templates";
6
6
  import type { Settings } from "../config/settings";
7
7
  import type { Skill } from "../extensibility/skills";
@@ -240,12 +240,14 @@ export interface ToolSession {
240
240
  registerForegroundFoldParticipant?: (adapter: FoldAdapter) => () => void;
241
241
  /** Whether a foreground wait is currently foldable into a background job. */
242
242
  hasForegroundBashBackgroundRequestHandler?: () => boolean;
243
- /** Request that the active foreground wait fold into a background job, if supported. */
244
- requestForegroundBashBackground?: () => Promise<boolean>;
243
+ /** Request that a foreground wait fold into a background job, if supported. `reason` names the trigger; `adapter` targets a specific wait instead of the newest registration. */
244
+ requestForegroundBashBackground?: (reason?: FoldReason, adapter?: FoldAdapter) => Promise<boolean>;
245
+ /** Current tool interrupt policy; `finish_tools` lets the running tool finish, so a steer must not fold it. */
246
+ getToolInterruptPolicy?: () => "abort_tools" | "finish_tools";
245
247
 
246
248
  /** Get the session-owned or inherited async job manager. */
247
249
  getAsyncJobManager?: () => AsyncJobManager | undefined;
248
- /** Resolves when the session queues user steering (or `signal` aborts) without consuming it; wait-style tools use this to end their observation early. */
250
+ /** Resolves on a steer admitted after the wait starts, or when `signal` aborts, without consuming the steer. */
249
251
  waitForUserSteering?: (signal: AbortSignal) => Promise<void>;
250
252
  /** Get session ID */
251
253
  getSessionId?: () => string | null;
@@ -271,6 +273,10 @@ export interface ToolSession {
271
273
  bashAllowedPrefixes?: string[];
272
274
  /** Restriction policy for sessions that deliberately expose a narrow bash surface. */
273
275
  bashRestrictionProfile?: BashRestrictionProfile;
276
+ /** Process-local master capability injected into direct Bash children only. */
277
+ getMasterBashCapability?: () => string | undefined;
278
+ /** Stable master lineage identity used across session switches and branches. */
279
+ getMasterOwnerSessionId?: () => string | undefined;
274
280
  /** Optional per-session allowlist for tools exposed through search_tool_bm25. */
275
281
  discoverableToolAllowedNames?: readonly string[];
276
282
  /** Throw instead of warn when toolNames contains an unknown name. */
@@ -375,8 +381,6 @@ export interface ToolSession {
375
381
  buildToolChoice?(toolName: string): ToolChoice | undefined;
376
382
  /** Build a named tool-choice decision, preserving whether exact named forcing survived capability degradation. */
377
383
  buildToolChoiceResult?(toolName: string): import("../utils/tool-choice").NamedToolChoiceResult;
378
- /** Steer a hidden custom message into the conversation (e.g. a preview reminder). */
379
- steer?(message: { customType: string; content: string; details?: unknown }): void;
380
384
  /** Peek the currently in-flight tool-choice queue directive's invocation handler. Used by the `resolve` tool to dispatch to the pending action. */
381
385
  peekQueueInvoker?(): ((input: unknown) => Promise<unknown> | unknown) | undefined;
382
386
  /** Peek the long-lived "standing" resolve handler registered by a mode (e.g. plan mode).
package/src/tools/job.ts CHANGED
@@ -3,7 +3,7 @@ import type { Component } from "@gajae-code/tui";
3
3
  import { Text } from "@gajae-code/tui";
4
4
  import { prompt } from "@gajae-code/utils";
5
5
  import * as z from "zod/v4";
6
- import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled, jobElapsedMs } from "../async";
6
+ import { type AsyncJob, AsyncJobManager, type FoldReason, isBackgroundJobSupportEnabled, jobElapsedMs } from "../async";
7
7
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
8
8
  import type { Theme } from "../modes/theme/theme";
9
9
  import jobDescription from "../prompts/tools/job.md" with { type: "text" };
@@ -50,6 +50,8 @@ interface JobSnapshot {
50
50
  status: "running" | "completed" | "failed" | "cancelled";
51
51
  label: string;
52
52
  durationMs: number;
53
+ /** Present when the job was folded out of a foreground wait. */
54
+ foldReason?: FoldReason;
53
55
  resultText?: string;
54
56
  errorText?: string;
55
57
  }
@@ -344,6 +346,7 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
344
346
  status: latest.status as JobSnapshot["status"],
345
347
  label: latest.label,
346
348
  durationMs: jobElapsedMs(latest, now),
349
+ ...(current?.metadata?.foldReason ? { foldReason: current.metadata.foldReason } : {}),
347
350
  ...(latest.resultText ? { resultText: latest.resultText } : {}),
348
351
  ...(latest.errorText ? { errorText: latest.errorText } : {}),
349
352
  };
@@ -229,19 +229,22 @@ export class MonitorTool implements AgentTool<typeof monitorSchema, MonitorToolD
229
229
  );
230
230
  pendingNotifications = MAX_PENDING_MONITOR_NOTIFICATIONS;
231
231
  }
232
- const sendPromise = this.session.sendCustomMessage?.(
233
- { customType: "task-notification", content, display: false, attribution: "agent", details },
234
- { triggerTurn: true, deliverAs: "followUp" },
235
- );
236
- if (sendPromise) {
237
- void sendPromise.catch(error => {
232
+ if (!this.session.sendCustomMessage) {
233
+ logger.warn("Monitor task-notification dropped: session has no custom-message router", {
234
+ taskId: jobId,
235
+ });
236
+ return;
237
+ }
238
+ void this.session
239
+ .sendCustomMessage(
240
+ { customType: "task-notification", content, display: false, attribution: "agent", details },
241
+ { triggerTurn: true, deliverAs: "followUp" },
242
+ )
243
+ .catch(error => {
238
244
  logger.warn("Monitor task-notification delivery failed", {
239
245
  error: error instanceof Error ? error.message : String(error),
240
246
  });
241
247
  });
242
- } else {
243
- this.session.steer?.({ customType: "task-notification", content, details });
244
- }
245
248
  };
246
249
  const schedulePersistentNotification = (line: string) => {
247
250
  latestLine = line;