@gajae-code/coding-agent 0.16.7 → 0.17.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 (321) hide show
  1. package/CHANGELOG.md +180 -1
  2. package/dist/types/capability/fs.d.ts +37 -4
  3. package/dist/types/capability/index.d.ts +19 -0
  4. package/dist/types/capability/types.d.ts +9 -0
  5. package/dist/types/cli/doctor/args.d.ts +36 -0
  6. package/dist/types/cli/doctor/checks.d.ts +18 -0
  7. package/dist/types/cli/doctor/config-repairs.d.ts +38 -0
  8. package/dist/types/cli/doctor/context.d.ts +89 -0
  9. package/dist/types/cli/doctor/files.d.ts +27 -0
  10. package/dist/types/cli/doctor/ids.d.ts +20 -0
  11. package/dist/types/cli/doctor/install-repairs.d.ts +94 -0
  12. package/dist/types/cli/doctor/journal.d.ts +43 -0
  13. package/dist/types/cli/doctor/managed-link.d.ts +39 -0
  14. package/dist/types/cli/doctor/native-probe.d.ts +2 -0
  15. package/dist/types/cli/doctor/permission-repairs.d.ts +25 -0
  16. package/dist/types/cli/doctor/plugin-quarantine.d.ts +77 -0
  17. package/dist/types/cli/doctor/plugin-restore.d.ts +40 -0
  18. package/dist/types/cli/doctor/plugin-targets.d.ts +11 -0
  19. package/dist/types/cli/doctor/probe-client.d.ts +9 -0
  20. package/dist/types/cli/doctor/probe-types.d.ts +18 -0
  21. package/dist/types/cli/doctor/probe.d.ts +3 -0
  22. package/dist/types/cli/doctor/projection-probe.d.ts +2 -0
  23. package/dist/types/cli/doctor/repairs.d.ts +28 -0
  24. package/dist/types/cli/doctor/report.d.ts +27 -0
  25. package/dist/types/cli/doctor/runner.d.ts +4 -0
  26. package/dist/types/cli/doctor/safety.d.ts +10 -0
  27. package/dist/types/cli/doctor/service-repairs.d.ts +8 -0
  28. package/dist/types/cli/doctor/service-targets.d.ts +41 -0
  29. package/dist/types/cli/doctor/types.d.ts +145 -0
  30. package/dist/types/cli/doctor-cli.d.ts +7 -0
  31. package/dist/types/cli/doctor-supervisor.d.ts +121 -0
  32. package/dist/types/cli/doctor-worker.d.ts +29 -0
  33. package/dist/types/cli/install-activation.d.ts +158 -0
  34. package/dist/types/cli/macos-community-app.d.ts +41 -0
  35. package/dist/types/cli/mcp-cli.d.ts +0 -11
  36. package/dist/types/cli/update-cli.d.ts +41 -4
  37. package/dist/types/cli-main.d.ts +174 -0
  38. package/dist/types/cli.d.ts +1 -171
  39. package/dist/types/commands/doctor.d.ts +7 -0
  40. package/dist/types/commands/sdk.d.ts +17 -8
  41. package/dist/types/config/autorouting-tier-map.d.ts +19 -0
  42. package/dist/types/config/file-lock.d.ts +27 -3
  43. package/dist/types/config/model-preset-registry.d.ts +1 -0
  44. package/dist/types/config/model-registry.d.ts +12 -5
  45. package/dist/types/config/model-resolver.d.ts +16 -3
  46. package/dist/types/config/models-config-schema.d.ts +2 -0
  47. package/dist/types/config/settings-schema.d.ts +1 -1
  48. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +1 -1
  49. package/dist/types/coordinator-mcp/session-reaper.d.ts +15 -5
  50. package/dist/types/cursor.d.ts +9 -9
  51. package/dist/types/discovery/agents-md.d.ts +6 -1
  52. package/dist/types/discovery/helpers.d.ts +36 -7
  53. package/dist/types/discovery/index.d.ts +1 -1
  54. package/dist/types/discovery/mcp-json.d.ts +2 -0
  55. package/dist/types/extensibility/extensions/loader.d.ts +22 -1
  56. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +14 -1
  57. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -1
  58. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  59. package/dist/types/extensibility/plugins/manager.d.ts +33 -4
  60. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +6 -0
  61. package/dist/types/extensibility/plugins/marketplace/manager.d.ts +186 -0
  62. package/dist/types/extensibility/plugins/marketplace/registry.d.ts +27 -0
  63. package/dist/types/extensibility/plugins/marketplace/source-resolver.d.ts +16 -1
  64. package/dist/types/extensibility/runtime-skill-discovery.d.ts +16 -3
  65. package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +6 -0
  66. package/dist/types/gjc-runtime/launch-worktree.d.ts +53 -0
  67. package/dist/types/gjc-runtime/session-layout.d.ts +8 -0
  68. package/dist/types/gjc-runtime/session-state-lock.d.ts +15 -0
  69. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
  70. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +6 -0
  71. package/dist/types/main.d.ts +19 -1
  72. package/dist/types/modes/acp/acp-agent.d.ts +40 -0
  73. package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
  74. package/dist/types/modes/components/model-selector.d.ts +2 -0
  75. package/dist/types/modes/interactive-mode.d.ts +1 -1
  76. package/dist/types/runtime-mcp/config-writer.d.ts +47 -0
  77. package/dist/types/runtime-mcp/policy-json-edit.d.ts +60 -0
  78. package/dist/types/runtime-mcp/startup-policy.d.ts +4 -0
  79. package/dist/types/runtime-mcp/types.d.ts +2 -2
  80. package/dist/types/sdk/acp/adapter.d.ts +5 -1
  81. package/dist/types/sdk/broker/broker.d.ts +51 -0
  82. package/dist/types/sdk/broker/daemon-entry.d.ts +39 -0
  83. package/dist/types/sdk/broker/discovery.d.ts +38 -0
  84. package/dist/types/sdk/broker/doctor-restart.d.ts +79 -0
  85. package/dist/types/sdk/broker/endpoint-authority.d.ts +7 -0
  86. package/dist/types/sdk/broker/ensure.d.ts +14 -0
  87. package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
  88. package/dist/types/sdk/broker/process-incarnation.d.ts +50 -0
  89. package/dist/types/sdk/broker/runtime.d.ts +26 -0
  90. package/dist/types/sdk/broker/session-index.d.ts +13 -4
  91. package/dist/types/sdk/broker/worktree-occupancy.d.ts +14 -0
  92. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +16 -0
  93. package/dist/types/sdk/bus/chat-daemon-control.d.ts +101 -6
  94. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +16 -0
  95. package/dist/types/sdk/bus/daemon-startup-exclusion.d.ts +43 -0
  96. package/dist/types/sdk/bus/doctor-daemon-restart-client.d.ts +51 -0
  97. package/dist/types/sdk/bus/doctor-daemon-restart.d.ts +39 -0
  98. package/dist/types/sdk/bus/index.d.ts +2 -0
  99. package/dist/types/sdk/bus/notification-service.d.ts +3 -0
  100. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +11 -0
  101. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  102. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -0
  103. package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +1 -0
  104. package/dist/types/sdk/bus/telegram-daemon.d.ts +36 -5
  105. package/dist/types/sdk/cli/session-cli.d.ts +1 -0
  106. package/dist/types/sdk/host/session-runtime.d.ts +4 -0
  107. package/dist/types/sdk/host/surface-policy.d.ts +3 -1
  108. package/dist/types/sdk/lifecycle/service.d.ts +44 -10
  109. package/dist/types/sdk/prompt-deadline-lease.d.ts +6 -3
  110. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -2
  111. package/dist/types/sdk/prompt-failure.d.ts +47 -0
  112. package/dist/types/sdk/prompt-status.d.ts +19 -0
  113. package/dist/types/sdk/router/session-router.d.ts +3 -4
  114. package/dist/types/sdk/service-artifact-paths.d.ts +16 -0
  115. package/dist/types/sdk/session-authority.d.ts +9 -0
  116. package/dist/types/sdk/session.d.ts +3 -1
  117. package/dist/types/session/agent-session.d.ts +1 -1
  118. package/dist/types/session/blob-store.d.ts +1 -1
  119. package/dist/types/session/fallback-chain-controller.d.ts +3 -6
  120. package/dist/types/session/internal/managed-session-scope.d.ts +44 -0
  121. package/dist/types/session/primary-control-surface.d.ts +4 -0
  122. package/dist/types/session/session-manager.d.ts +6 -0
  123. package/dist/types/system-prompt.d.ts +6 -0
  124. package/dist/types/tools/ask.d.ts +25 -0
  125. package/dist/types/tools/image-gen.d.ts +0 -4
  126. package/dist/types/tools/skill-discovery.d.ts +7 -5
  127. package/dist/types/tools/ultragoal-ask-guard.d.ts +7 -4
  128. package/dist/types/utils/changelog.d.ts +0 -9
  129. package/dist/types/utils/mupdf-wasm.d.ts +1 -0
  130. package/package.json +9 -8
  131. package/scripts/compile-args.ts +8 -4
  132. package/src/capability/fs.ts +395 -17
  133. package/src/capability/index.ts +273 -10
  134. package/src/capability/types.ts +9 -0
  135. package/src/cli/customize-doctor.ts +18 -12
  136. package/src/cli/doctor/args.ts +236 -0
  137. package/src/cli/doctor/checks.ts +431 -0
  138. package/src/cli/doctor/config-repairs.ts +517 -0
  139. package/src/cli/doctor/context.ts +149 -0
  140. package/src/cli/doctor/files.ts +149 -0
  141. package/src/cli/doctor/ids.ts +115 -0
  142. package/src/cli/doctor/install-repairs.ts +335 -0
  143. package/src/cli/doctor/journal.ts +423 -0
  144. package/src/cli/doctor/managed-link.ts +514 -0
  145. package/src/cli/doctor/native-probe.ts +13 -0
  146. package/src/cli/doctor/permission-repairs.ts +242 -0
  147. package/src/cli/doctor/plugin-quarantine.ts +377 -0
  148. package/src/cli/doctor/plugin-restore.ts +59 -0
  149. package/src/cli/doctor/plugin-targets.ts +373 -0
  150. package/src/cli/doctor/probe-client.ts +195 -0
  151. package/src/cli/doctor/probe-types.ts +21 -0
  152. package/src/cli/doctor/probe.ts +42 -0
  153. package/src/cli/doctor/projection-probe.ts +249 -0
  154. package/src/cli/doctor/repairs.ts +1157 -0
  155. package/src/cli/doctor/report.ts +176 -0
  156. package/src/cli/doctor/runner.ts +342 -0
  157. package/src/cli/doctor/safety.ts +40 -0
  158. package/src/cli/doctor/service-repairs.ts +452 -0
  159. package/src/cli/doctor/service-targets.ts +274 -0
  160. package/src/cli/doctor/types.ts +164 -0
  161. package/src/cli/doctor-cli.ts +62 -0
  162. package/src/cli/doctor-supervisor.ts +571 -0
  163. package/src/cli/doctor-worker.ts +181 -0
  164. package/src/cli/install-activation.ts +666 -0
  165. package/src/cli/macos-community-app.ts +1449 -0
  166. package/src/cli/mcp-cli.ts +11 -21
  167. package/src/cli/nofile-limit.ts +5 -1
  168. package/src/cli/plugin-cli.ts +6 -1
  169. package/src/cli/update-cli.ts +362 -264
  170. package/src/cli-main.ts +517 -0
  171. package/src/cli.ts +64 -433
  172. package/src/commands/autoresearch.ts +11 -5
  173. package/src/commands/completion.ts +1 -1
  174. package/src/commands/doctor.ts +13 -0
  175. package/src/commands/sdk.ts +58 -10
  176. package/src/commands/ultragoal.ts +11 -5
  177. package/src/config/autorouting-tier-map.ts +7 -0
  178. package/src/config/file-lock.ts +736 -71
  179. package/src/config/mcp-schema.json +3 -3
  180. package/src/config/model-preset-registry.ts +1 -0
  181. package/src/config/model-registry.ts +80 -27
  182. package/src/config/model-resolver.ts +33 -4
  183. package/src/config/models-config-schema.ts +2 -0
  184. package/src/config/settings-schema.ts +6 -2
  185. package/src/coordinator-mcp/question-gate-codec.ts +1 -0
  186. package/src/coordinator-mcp/server.ts +534 -51
  187. package/src/coordinator-mcp/session-reaper.ts +64 -8
  188. package/src/cursor.ts +223 -53
  189. package/src/discovery/agents-md.ts +74 -8
  190. package/src/discovery/agents.ts +32 -7
  191. package/src/discovery/builtin.ts +184 -75
  192. package/src/discovery/claude-plugins.ts +85 -17
  193. package/src/discovery/cline.ts +38 -10
  194. package/src/discovery/cursor.ts +19 -6
  195. package/src/discovery/gemini.ts +62 -22
  196. package/src/discovery/github.ts +13 -3
  197. package/src/discovery/helpers.ts +483 -89
  198. package/src/discovery/index.ts +1 -0
  199. package/src/discovery/mcp-json.ts +36 -10
  200. package/src/discovery/opencode.ts +36 -19
  201. package/src/discovery/ssh.ts +11 -8
  202. package/src/discovery/windsurf.ts +12 -3
  203. package/src/exec/bash-executor.ts +10 -3
  204. package/src/exec/isolated-shell.ts +15 -2
  205. package/src/extensibility/extensions/loader.ts +88 -26
  206. package/src/extensibility/gjc-plugins/lifecycle.ts +189 -0
  207. package/src/extensibility/gjc-plugins/paths.ts +4 -1
  208. package/src/extensibility/gjc-plugins/types.ts +27 -0
  209. package/src/extensibility/plugins/manager.ts +200 -22
  210. package/src/extensibility/plugins/marketplace/cache.ts +55 -2
  211. package/src/extensibility/plugins/marketplace/manager.ts +1262 -162
  212. package/src/extensibility/plugins/marketplace/registry.ts +153 -0
  213. package/src/extensibility/plugins/marketplace/source-resolver.ts +116 -10
  214. package/src/extensibility/runtime-skill-discovery.ts +61 -9
  215. package/src/extensibility/skill-management.ts +7 -1
  216. package/src/gjc-runtime/autoresearch-runtime.ts +10 -1
  217. package/src/gjc-runtime/launch-worktree.test.ts +248 -0
  218. package/src/gjc-runtime/launch-worktree.ts +275 -38
  219. package/src/gjc-runtime/repository-binding.ts +26 -1
  220. package/src/gjc-runtime/session-layout.ts +10 -0
  221. package/src/gjc-runtime/session-state-lock.ts +330 -23
  222. package/src/gjc-runtime/session-state-sidecar.ts +328 -80
  223. package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
  224. package/src/harness-control-plane/session-lease.ts +2 -2
  225. package/src/internal-urls/docs-index.generated.ts +28 -26
  226. package/src/main.ts +150 -29
  227. package/src/memories/index.ts +24 -2
  228. package/src/modes/acp/acp-agent.ts +522 -68
  229. package/src/modes/components/agent-dashboard.ts +3 -1
  230. package/src/modes/components/model-selector.ts +28 -19
  231. package/src/modes/components/plugin-settings.ts +2 -1
  232. package/src/modes/controllers/selector-controller.ts +15 -9
  233. package/src/modes/interactive-mode.ts +21 -3
  234. package/src/prompts/system/system-prompt.md +5 -1
  235. package/src/prompts/tools/browser.md +1 -1
  236. package/src/prompts/tools/move-session.md +4 -0
  237. package/src/prompts/tools/read.md +2 -1
  238. package/src/prompts/tools/skill-discovery.md +6 -5
  239. package/src/prompts/tools/task.md +1 -0
  240. package/src/runtime-mcp/config-writer.ts +228 -0
  241. package/src/runtime-mcp/oauth-discovery.ts +114 -14
  242. package/src/runtime-mcp/policy-json-edit.ts +129 -0
  243. package/src/runtime-mcp/startup-policy.ts +14 -0
  244. package/src/runtime-mcp/types.ts +2 -2
  245. package/src/sdk/acp/adapter.ts +27 -12
  246. package/src/sdk/broker/broker.ts +273 -44
  247. package/src/sdk/broker/daemon-entry.ts +119 -0
  248. package/src/sdk/broker/discovery.ts +173 -20
  249. package/src/sdk/broker/doctor-restart.ts +206 -0
  250. package/src/sdk/broker/endpoint-authority.ts +36 -0
  251. package/src/sdk/broker/ensure.ts +49 -14
  252. package/src/sdk/broker/lifecycle.ts +174 -42
  253. package/src/sdk/broker/process-incarnation.ts +86 -2
  254. package/src/sdk/broker/runtime.ts +145 -21
  255. package/src/sdk/broker/session-index.ts +136 -22
  256. package/src/sdk/broker/spawn-substrate.ts +4 -1
  257. package/src/sdk/broker/transport.ts +5 -0
  258. package/src/sdk/broker/worktree-occupancy.ts +21 -1
  259. package/src/sdk/bus/chat-daemon-cli.ts +63 -0
  260. package/src/sdk/bus/chat-daemon-control.ts +260 -64
  261. package/src/sdk/bus/chat-daemon-runtime.ts +141 -25
  262. package/src/sdk/bus/daemon-startup-exclusion.ts +119 -0
  263. package/src/sdk/bus/doctor-daemon-restart-client.ts +291 -0
  264. package/src/sdk/bus/doctor-daemon-restart.ts +92 -0
  265. package/src/sdk/bus/index.ts +252 -51
  266. package/src/sdk/bus/kind-aware-reconciliation.ts +76 -41
  267. package/src/sdk/bus/notification-service.ts +21 -3
  268. package/src/sdk/bus/reconciliation-store.ts +17 -11
  269. package/src/sdk/bus/telegram-daemon-cli.ts +91 -5
  270. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  271. package/src/sdk/bus/telegram-daemon-control.ts +47 -0
  272. package/src/sdk/bus/telegram-daemon-owner-registry.ts +1 -1
  273. package/src/sdk/bus/telegram-daemon.ts +178 -13
  274. package/src/sdk/cli/session-cli.ts +100 -12
  275. package/src/sdk/host/host.ts +2 -7
  276. package/src/sdk/host/session-runtime.test.ts +329 -8
  277. package/src/sdk/host/session-runtime.ts +202 -50
  278. package/src/sdk/host/surface-policy.ts +8 -1
  279. package/src/sdk/lifecycle/service.ts +199 -22
  280. package/src/sdk/mcp/server.ts +1 -1
  281. package/src/sdk/prompt-deadline-lease.ts +12 -4
  282. package/src/sdk/prompt-deadline-manager.ts +5 -7
  283. package/src/sdk/prompt-failure.ts +148 -0
  284. package/src/sdk/prompt-status.ts +30 -1
  285. package/src/sdk/prompt-watchdog.ts +11 -3
  286. package/src/sdk/protocol/operation-inventory.generated.json +18 -0
  287. package/src/sdk/protocol/operation-registry.ts +20 -8
  288. package/src/sdk/router/session-router.ts +8 -3
  289. package/src/sdk/service-artifact-paths.ts +26 -0
  290. package/src/sdk/session-authority.ts +10 -0
  291. package/src/sdk/session.ts +361 -194
  292. package/src/session/agent-session.ts +398 -93
  293. package/src/session/blob-store.ts +9 -9
  294. package/src/session/fallback-chain-controller.ts +3 -7
  295. package/src/session/internal/managed-session-scope.ts +445 -29
  296. package/src/session/internal/managed-session-storage.ts +38 -6
  297. package/src/session/primary-control-surface.ts +12 -0
  298. package/src/session/session-manager.ts +11 -3
  299. package/src/session/session-storage.ts +1 -1
  300. package/src/session-import/codex.ts +6 -1
  301. package/src/session-import/redact.ts +7 -0
  302. package/src/skill-state/workflow-mutation-guard.ts +645 -39
  303. package/src/slash-commands/builtin-registry.ts +7 -3
  304. package/src/system-prompt.ts +8 -0
  305. package/src/task/executor.ts +36 -3
  306. package/src/task/types.ts +27 -5
  307. package/src/tools/ask.ts +114 -34
  308. package/src/tools/browser/actions.ts +2 -2
  309. package/src/tools/browser/tab-worker.ts +11 -8
  310. package/src/tools/descriptors.ts +6 -1
  311. package/src/tools/fetch.ts +7 -2
  312. package/src/tools/image-gen.ts +33 -34
  313. package/src/tools/read.ts +78 -32
  314. package/src/tools/skill-discovery.ts +17 -7
  315. package/src/tools/tool-catalog.generated.ts +8 -8
  316. package/src/tools/ultragoal-ask-guard.ts +16 -26
  317. package/src/utils/changelog.ts +7 -1
  318. package/src/utils/git.ts +3 -1
  319. package/src/utils/markit.ts +24 -2
  320. package/src/utils/mupdf-wasm.ts +26 -0
  321. package/src/web/search/providers/openai-compatible.ts +191 -14
@@ -42,6 +42,7 @@ import {
42
42
  type ManagedAttemptDecision,
43
43
  type ManagedAttemptOutcome,
44
44
  type MidRunMaintenanceOutcome,
45
+ markNonDispatchedToolEvent,
45
46
  type RunCancellationDomain,
46
47
  type RunCancellationDomainBridge,
47
48
  type RunResourceProducerLease,
@@ -114,6 +115,7 @@ import {
114
115
  isContextOverflow,
115
116
  isFastModeEffectiveForProvider,
116
117
  isUsageLimitError,
118
+ modelSupportsMaintenanceCalls,
117
119
  modelSupportsServiceTier,
118
120
  modelsAreEqual,
119
121
  streamSimple,
@@ -315,10 +317,14 @@ import {
315
317
  import {
316
318
  assertNonEmptyGjcSessionId,
317
319
  modeStatePath as sessionModeStatePath,
318
- sessionRuntimeDir,
320
+ sessionRuntimeStatePath,
319
321
  sessionStateDir,
320
322
  } from "../gjc-runtime/session-layout";
321
- import { sessionStateLockFailureFields, shouldWarnPersistFailure } from "../gjc-runtime/session-state-lock";
323
+ import {
324
+ isRetryableSessionStateLockContention,
325
+ sessionStateLockFailureFields,
326
+ shouldWarnPersistFailure,
327
+ } from "../gjc-runtime/session-state-lock";
322
328
  import {
323
329
  type CoordinatorToolObservation,
324
330
  clearCoordinatorRuntimeStateRescope,
@@ -947,7 +953,7 @@ export interface AgentSessionConfig {
947
953
  discoveryMode?: "off" | "mcp-only" | "all";
948
954
  /** MCP tool names to activate for the current session when discovery mode is enabled. */
949
955
  initialSelectedMCPToolNames?: string[];
950
- /** Keep persisted MCP names until a deferred exact catalog becomes available. */
956
+ /** Keep persisted MCP names until a deferred (exact or conventional) catalog becomes available. */
951
957
  preserveUnavailableInitialMCPToolSelection?: boolean;
952
958
  /** Built-in discoverable tool names restored for the current all-discovery session. */
953
959
  initialSelectedDiscoveredBuiltinToolNames?: string[];
@@ -1673,13 +1679,10 @@ function dedupeIrcReply(text: string): string {
1673
1679
  }
1674
1680
  let result = out.join("\n");
1675
1681
  if (Buffer.byteLength(result, "utf8") > IRC_REPLY_MAX_BYTES) {
1676
- // Trim by characters until we're under the byte budget — handles multi-byte
1677
- // glyphs at the boundary without splitting them.
1682
+ // Bound the UTF-8 prefix without repeatedly measuring the shrinking reply.
1678
1683
  const suffix = "\n[…truncated]";
1679
1684
  const budget = IRC_REPLY_MAX_BYTES - Buffer.byteLength(suffix, "utf8");
1680
- while (Buffer.byteLength(result, "utf8") > budget) {
1681
- result = result.slice(0, -1);
1682
- }
1685
+ result = truncateHeadBytes(result, budget).text;
1683
1686
  result += suffix;
1684
1687
  }
1685
1688
  return result;
@@ -2032,6 +2035,20 @@ export type BeforeAgentStartContributor = (event: {
2032
2035
  const AGENT_END_WORKER_INTEGRATION_TIMEOUT_MS = 5_000;
2033
2036
  const POST_PUBLICATION_ERROR_MAX_BYTES = 512;
2034
2037
 
2038
+ /**
2039
+ * Bounded repair for a coordinator runtime-state update that lost its lock outright.
2040
+ *
2041
+ * The lock itself already waits out a busy peer, so reaching this path means a whole
2042
+ * acquisition budget elapsed without the lock ever moving. Retrying covers the remaining
2043
+ * case that budget cannot: a holder whose single critical section legitimately outlives
2044
+ * it. The delay is far longer than the lock's own backoff because the contention being
2045
+ * waited out is a whole foreign write, not a scheduling hiccup, and the retries stay small
2046
+ * so a persistently wedged lock is still reported promptly instead of stalling the
2047
+ * session's persist queue.
2048
+ */
2049
+ const COORDINATOR_PERSIST_CONTENTION_RETRIES = 3;
2050
+ const COORDINATOR_PERSIST_RETRY_DELAY_MS = 2_000;
2051
+
2035
2052
  export type WorkerIntegrationOutcome =
2036
2053
  | { status: "completed" }
2037
2054
  | { status: "failed"; error: string }
@@ -2876,6 +2893,7 @@ export class AgentSession {
2876
2893
  this.#retryReplayUnsafeEpoch = undefined;
2877
2894
  this.#firstEventTimeoutRetryStartedAt = Date.now();
2878
2895
  this.#providerRetryMaxAttempts = undefined;
2896
+ this.#codexCredentialModelUnavailableRetried = false;
2879
2897
  }
2880
2898
 
2881
2899
  #markRetryReplayUnsafe(): void {
@@ -2914,6 +2932,8 @@ export class AgentSession {
2914
2932
  #retryNowRequested = false;
2915
2933
  #firstEventTimeoutRetryStartedAt: number | undefined;
2916
2934
  #providerRetryMaxAttempts: number | undefined;
2935
+ /** One content-free retry is allowed after Codex says the active account lacks the model. */
2936
+ #codexCredentialModelUnavailableRetried = false;
2917
2937
  #retryAttempt = 0;
2918
2938
  #retryPromise: Promise<void> | undefined = undefined;
2919
2939
  #retryResolve: (() => void) | undefined = undefined;
@@ -3048,11 +3068,14 @@ export class AgentSession {
3048
3068
  */
3049
3069
  #registerRuntimeStateFinalizer(): void {
3050
3070
  this.#unregisterRuntimeStateFinalizer?.();
3051
- const currentContext = () => ({
3052
- sessionId: this.sessionId,
3053
- cwd: this.sessionManager.getCwd(),
3054
- sessionFile: this.sessionManager.getSessionFile(),
3055
- });
3071
+ const currentContext = () => {
3072
+ const identity = { sessionId: this.sessionId, cwd: this.sessionManager.getCwd() };
3073
+ return {
3074
+ ...identity,
3075
+ sessionFile: this.sessionManager.getSessionFile(),
3076
+ stateFile: this.#runtimeStateMarkerFile(identity),
3077
+ };
3078
+ };
3056
3079
  this.#unregisterRuntimeStateFinalizer = registerCoordinatorRuntimeStateFinalizer(
3057
3080
  currentContext(),
3058
3081
  currentContext,
@@ -4399,6 +4422,11 @@ export class AgentSession {
4399
4422
  this.agent.setProvisionalAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
4400
4423
  const contentIndex = assistantMessageEvent.contentIndex ?? 0;
4401
4424
  const block = message.content[contentIndex];
4425
+ if (
4426
+ block?.type === "toolCall" &&
4427
+ (block.escapedNonAsciiArguments === true || block.escapedUnicodeArgumentEvidence !== undefined)
4428
+ )
4429
+ return false;
4402
4430
  if (block?.type === "toolCall" && block.id) this.#provisionalStreamingToolCallIds.add(block.id);
4403
4431
  if (
4404
4432
  assistantMessageEvent.type !== "toolcall_start" &&
@@ -4467,6 +4495,9 @@ export class AgentSession {
4467
4495
  newCwdIdentity: move.newCwdIdentity,
4468
4496
  previousSessionFile: move.previousSessionFile ?? null,
4469
4497
  newSessionFile: move.newSessionFile ?? null,
4498
+ // The rescope family must decide pin-vs-derived from the marker THIS session
4499
+ // owns, never from the ambient pin (#5473).
4500
+ stateFile: this.#runtimeStateMarkerFile({ sessionId: this.sessionId, cwd: move.previousCwd }),
4470
4501
  });
4471
4502
  });
4472
4503
  this.#unregisterMoveAbortListener = this.sessionManager.registerMoveAbortListener(async move => {
@@ -4478,6 +4509,7 @@ export class AgentSession {
4478
4509
  sessionId: this.sessionId,
4479
4510
  cwd: move.newCwd,
4480
4511
  sessionFile: move.newSessionFile ?? null,
4512
+ stateFile: this.#runtimeStateMarkerFile({ sessionId: this.sessionId, cwd: move.newCwd }),
4481
4513
  },
4482
4514
  moveId,
4483
4515
  move.previousCwd,
@@ -4496,6 +4528,7 @@ export class AgentSession {
4496
4528
  sessionId: this.sessionId,
4497
4529
  cwd: move.newCwd,
4498
4530
  sessionFile: move.newSessionFile ?? null,
4531
+ stateFile: this.#runtimeStateMarkerFile({ sessionId: this.sessionId, cwd: move.newCwd }),
4499
4532
  },
4500
4533
  move.previousCwd,
4501
4534
  moveId,
@@ -4507,14 +4540,19 @@ export class AgentSession {
4507
4540
  // fence refuses every later persist) and rebinds the postmortem finalizer, which
4508
4541
  // otherwise keeps writing terminal state to the launch root's cwd/session file.
4509
4542
  this.#unregisterAfterMoveListener = this.sessionManager.registerAfterMoveListener(async move => {
4510
- let completed = false;
4511
4543
  try {
4544
+ // A move can promote an explicit --session-dir destination to managed
4545
+ // storage without changing the session id. Its local root changes from
4546
+ // artifacts/local to scratch, so complete migration before releasing
4547
+ // the rescope barrier or allowing the next prompt's sync resolver.
4548
+ await initializeLocalRoot(this.#localProtocolOptions());
4512
4549
  const relocated = await relocateCoordinatorRuntimeStateForRescope(
4513
4550
  {
4514
4551
  sessionId: this.sessionId,
4515
4552
  cwd: move.newCwd,
4516
4553
  sessionFile: this.sessionManager.getSessionFile() ?? null,
4517
4554
  previousSessionFile: move.previousSessionFile ?? null,
4555
+ stateFile: this.#runtimeStateMarkerFile({ sessionId: this.sessionId, cwd: move.newCwd }),
4518
4556
  },
4519
4557
  move.previousCwd,
4520
4558
  );
@@ -4524,17 +4562,50 @@ export class AgentSession {
4524
4562
  sessionId: this.sessionId,
4525
4563
  cwd: move.newCwd,
4526
4564
  sessionFile: this.sessionManager.getSessionFile() ?? null,
4565
+ stateFile: this.#runtimeStateMarkerFile({ sessionId: this.sessionId, cwd: move.newCwd }),
4527
4566
  },
4528
4567
  this.#coordinatorRescopeMoveId,
4529
4568
  move.previousCwd,
4530
4569
  );
4531
- completed = true;
4570
+ } catch (error) {
4571
+ // Publication has already committed, so the move-abort listener cannot
4572
+ // recover this state. Retry coordinator relocation independently of the
4573
+ // failed local-root gate so later persists cannot remain behind a barrier
4574
+ // that no longer has a rollback path.
4575
+ try {
4576
+ const relocated = await relocateCoordinatorRuntimeStateForRescope(
4577
+ {
4578
+ sessionId: this.sessionId,
4579
+ cwd: move.newCwd,
4580
+ sessionFile: this.sessionManager.getSessionFile() ?? null,
4581
+ previousSessionFile: move.previousSessionFile ?? null,
4582
+ stateFile: this.#runtimeStateMarkerFile({ sessionId: this.sessionId, cwd: move.newCwd }),
4583
+ },
4584
+ move.previousCwd,
4585
+ );
4586
+ if (!relocated) throw new Error("Coordinator runtime state rescope recovery was refused.");
4587
+ await clearCoordinatorRuntimeStateRescope(
4588
+ {
4589
+ sessionId: this.sessionId,
4590
+ cwd: move.newCwd,
4591
+ sessionFile: this.sessionManager.getSessionFile() ?? null,
4592
+ stateFile: this.#runtimeStateMarkerFile({ sessionId: this.sessionId, cwd: move.newCwd }),
4593
+ },
4594
+ this.#coordinatorRescopeMoveId,
4595
+ move.previousCwd,
4596
+ );
4597
+ } catch (recoveryError) {
4598
+ logger.error("Failed to recover coordinator runtime state after committed session move", {
4599
+ cwd: move.newCwd,
4600
+ error: String(error),
4601
+ recoveryError: String(recoveryError),
4602
+ });
4603
+ }
4604
+ throw error;
4532
4605
  } finally {
4533
4606
  this.#registerRuntimeStateFinalizer();
4534
- if (completed) {
4535
- this.#coordinatorRescopeMoveId = undefined;
4536
- this.#endCoordinatorRescopeBarrier();
4537
- }
4607
+ this.#coordinatorRescopeMoveId = undefined;
4608
+ this.#endCoordinatorRescopeBarrier();
4538
4609
  }
4539
4610
  });
4540
4611
  // Power assertions are taken per turn (see #beginInFlight); nothing acquired here.
@@ -4553,6 +4624,10 @@ export class AgentSession {
4553
4624
  sessionId: this.sessionId,
4554
4625
  cwd: this.sessionManager.getCwd(),
4555
4626
  sessionFile: this.sessionManager.getSessionFile() ?? null,
4627
+ stateFile: this.#runtimeStateMarkerFile({
4628
+ sessionId: this.sessionId,
4629
+ cwd: this.sessionManager.getCwd(),
4630
+ }),
4556
4631
  };
4557
4632
  if (hasCoordinatorRuntimeStateRescopeJournal(rescopeRecoveryContext)) {
4558
4633
  this.extendStartupTurnBarrier(recoverCoordinatorRuntimeStateRescope(rescopeRecoveryContext));
@@ -6120,19 +6195,30 @@ export class AgentSession {
6120
6195
  );
6121
6196
  }
6122
6197
 
6198
+ /**
6199
+ * The runtime-state marker THIS session owns, or `null` when it has none.
6200
+ *
6201
+ * The process-wide `GJC_COORDINATOR_SESSION_STATE_FILE` pin describes exactly one session:
6202
+ * the one the launcher minted it for. A nested in-process session — the role-agent/subagent
6203
+ * fan-out — is a different session, so it must never read or write the parent's marker. The
6204
+ * sidecar's identity fence refuses every such write (the reported 102x rejection while a
6205
+ * fan-out was live), and a write that did land would rewrite the parent's lifecycle under
6206
+ * the child's identity. A nested session therefore owns its own session-derived marker.
6207
+ */
6208
+ #runtimeStateMarkerFile(input: { sessionId: string; cwd: string }): string | null {
6209
+ if (!input.sessionId.trim()) return null;
6210
+ const pinned = process.env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
6211
+ if (this.taskDepth > 0) return sessionRuntimeStatePath(input.cwd, input.sessionId);
6212
+ return pinned || sessionRuntimeStatePath(input.cwd, input.sessionId);
6213
+ }
6214
+
6123
6215
  #captureCoordinatorRuntimeStatePersistContext(): CoordinatorRuntimeStatePersistContext {
6124
6216
  const context = {
6125
6217
  sessionId: this.sessionId,
6126
6218
  cwd: this.sessionManager.getCwd(),
6127
6219
  sessionFile: this.sessionManager.getSessionFile(),
6128
6220
  };
6129
- const explicitStateFile = process.env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
6130
- const stateFile =
6131
- explicitStateFile ||
6132
- (context.sessionId.trim()
6133
- ? path.join(sessionRuntimeDir(context.cwd, context.sessionId), "runtime-state.json")
6134
- : null);
6135
- return { ...context, stateFile };
6221
+ return { ...context, stateFile: this.#runtimeStateMarkerFile(context) };
6136
6222
  }
6137
6223
 
6138
6224
  /**
@@ -6179,17 +6265,30 @@ export class AgentSession {
6179
6265
  observation: CoordinatorToolObservation | undefined,
6180
6266
  propagateFailure: boolean,
6181
6267
  ): Promise<void> {
6182
- try {
6183
- await persistCoordinatorRuntimeStateFromEvent(event, context, observation);
6184
- } catch (error) {
6185
- this.#warnPersistFailure(
6186
- "Failed to persist coordinator runtime state",
6187
- error,
6188
- context.stateFile,
6189
- context.sessionId,
6190
- { event: event.type },
6191
- );
6192
- if (propagateFailure) throw error;
6268
+ for (let attempt = 0; ; attempt++) {
6269
+ try {
6270
+ await persistCoordinatorRuntimeStateFromEvent(event, context, observation);
6271
+ return;
6272
+ } catch (error) {
6273
+ // Losing the lock drops this update permanently: nothing re-derives the
6274
+ // snapshot afterwards, so the coordinator keeps reporting stale lifecycle
6275
+ // and tool activity for this session until an unrelated event happens to
6276
+ // win. A pure-contention refusal is therefore retried before it is
6277
+ // reported; every other failure is a standing condition, reported at once.
6278
+ if (attempt < COORDINATOR_PERSIST_CONTENTION_RETRIES && isRetryableSessionStateLockContention(error)) {
6279
+ await Bun.sleep(COORDINATOR_PERSIST_RETRY_DELAY_MS);
6280
+ continue;
6281
+ }
6282
+ this.#warnPersistFailure(
6283
+ "Failed to persist coordinator runtime state",
6284
+ error,
6285
+ context.stateFile,
6286
+ context.sessionId,
6287
+ { event: event.type, ...(attempt > 0 ? { retries: attempt } : {}) },
6288
+ );
6289
+ if (propagateFailure) throw error;
6290
+ return;
6291
+ }
6193
6292
  }
6194
6293
  }
6195
6294
 
@@ -6371,6 +6470,18 @@ export class AgentSession {
6371
6470
  ): Promise<void> => {
6372
6471
  const attemptScope = (event as AgentEvent & { scope?: AttemptScope }).scope;
6373
6472
 
6473
+ // These lifecycle boundaries can be delivered without awaiting this listener.
6474
+ // Revoke streaming-edit cache generations before any admission, spill, or
6475
+ // extension work so a completed read cannot publish across the boundary.
6476
+ if (event.type === "turn_start") this.#resetStreamingEditState();
6477
+ if (event.type === "message_end" && event.message.role === "toolResult") {
6478
+ const details = event.message.details;
6479
+ if (event.message.toolName === "edit" && details && typeof details === "object" && "path" in details) {
6480
+ const editPath = (details as { path?: unknown }).path;
6481
+ if (typeof editPath === "string") this.#invalidateFileCacheForPath(editPath);
6482
+ }
6483
+ }
6484
+
6374
6485
  if (
6375
6486
  event.type === "tool_execution_start" ||
6376
6487
  event.type === "tool_execution_update" ||
@@ -6725,7 +6836,6 @@ export class AgentSession {
6725
6836
  }
6726
6837
 
6727
6838
  if (event.type === "turn_start") {
6728
- this.#resetStreamingEditState();
6729
6839
  // TTSR: Reset buffer on turn start
6730
6840
  this.#ttsrManager?.resetBuffer();
6731
6841
  }
@@ -6969,10 +7079,6 @@ export class AgentSession {
6969
7079
  isError?: boolean;
6970
7080
  content?: Array<TextContent | ImageContent>;
6971
7081
  };
6972
- // Invalidate streaming edit cache when edit tool completes to prevent stale data
6973
- if (toolName === "edit" && details?.path) {
6974
- this.#invalidateFileCacheForPath(details.path);
6975
- }
6976
7082
  if (toolName === "todo_write" && !isError && Array.isArray(details?.phases)) {
6977
7083
  this.setTodoPhases(details.phases);
6978
7084
  }
@@ -8220,6 +8326,9 @@ export class AgentSession {
8220
8326
  this.#streamingEditPrecheckedToolCallIds.clear();
8221
8327
  this.#streamingEditParsedToolCallCache.clear();
8222
8328
  this.#streamingEditFileCache.clear();
8329
+ for (const resolvedPath of this.#streamingEditPrecachePending.keys()) {
8330
+ this.#invalidateStreamingEditPrecachePath(resolvedPath);
8331
+ }
8223
8332
  }
8224
8333
 
8225
8334
  #getStreamingEditToolCall(event: AgentEvent): StreamingEditParsedToolCall | undefined {
@@ -8300,26 +8409,50 @@ export class AgentSession {
8300
8409
  return block?.type === "toolCall" && this.#provisionalStreamingToolCallIds.has(block.id);
8301
8410
  }
8302
8411
 
8303
- #streamingEditPrecachePending = new Set<string>();
8412
+ #streamingEditPrecachePending = new Map<string, symbol>();
8413
+ #streamingEditPrecacheStale = new Map<string, Set<symbol>>();
8304
8414
  async #preCacheFileAsync(resolvedPath: string): Promise<void> {
8305
8415
  if (this.#streamingEditFileCache.has(resolvedPath)) return;
8306
8416
  if (this.#streamingEditPrecachePending.has(resolvedPath)) return;
8307
- this.#streamingEditPrecachePending.add(resolvedPath);
8417
+ // Keep at most one invalidated read alongside the current generation. Once
8418
+ // two reads are already in flight, further invalidations wait for one to
8419
+ // settle instead of accumulating another whole-file read.
8420
+ if ((this.#streamingEditPrecacheStale.get(resolvedPath)?.size ?? 0) >= 2) return;
8421
+ const token = Symbol();
8422
+ this.#streamingEditPrecachePending.set(resolvedPath, token);
8308
8423
  try {
8309
8424
  const stat = await fs.promises.stat(resolvedPath);
8310
8425
  if (stat.size > MAX_EDIT_FILE_BYTES) return;
8311
8426
 
8427
+ if (this.#streamingEditPrecachePending.get(resolvedPath) !== token) return;
8312
8428
  const rawText = await fs.promises.readFile(resolvedPath, "utf-8");
8429
+ if (this.#streamingEditPrecachePending.get(resolvedPath) !== token) return;
8313
8430
  if (this.#streamingEditFileCache.has(resolvedPath)) return;
8314
8431
  const { text } = stripBom(rawText);
8315
8432
  this.#streamingEditFileCache.set(resolvedPath, normalizeToLF(text));
8316
8433
  } catch {
8317
8434
  // Don't cache on read errors (including ENOENT) - let the edit tool handle them
8318
8435
  } finally {
8319
- this.#streamingEditPrecachePending.delete(resolvedPath);
8436
+ if (this.#streamingEditPrecachePending.get(resolvedPath) === token) {
8437
+ this.#streamingEditPrecachePending.delete(resolvedPath);
8438
+ }
8439
+ const stale = this.#streamingEditPrecacheStale.get(resolvedPath);
8440
+ if (stale?.delete(token) && stale.size === 0) this.#streamingEditPrecacheStale.delete(resolvedPath);
8320
8441
  }
8321
8442
  }
8322
8443
 
8444
+ #invalidateStreamingEditPrecachePath(resolvedPath: string): void {
8445
+ const token = this.#streamingEditPrecachePending.get(resolvedPath);
8446
+ if (token === undefined) return;
8447
+ this.#streamingEditPrecachePending.delete(resolvedPath);
8448
+ let stale = this.#streamingEditPrecacheStale.get(resolvedPath);
8449
+ if (!stale) {
8450
+ stale = new Set();
8451
+ this.#streamingEditPrecacheStale.set(resolvedPath, stale);
8452
+ }
8453
+ stale.add(token);
8454
+ }
8455
+
8323
8456
  #ensureFileCache(resolvedPath: string): void {
8324
8457
  if (this.#streamingEditFileCache.has(resolvedPath)) return;
8325
8458
 
@@ -8337,9 +8470,27 @@ export class AgentSession {
8337
8470
 
8338
8471
  /** Invalidate cache for a file after an edit completes to prevent stale data */
8339
8472
  #invalidateFileCacheForPath(filePath: string): void {
8340
- const resolvedPath = this.#resolveSessionFsPath(filePath);
8473
+ let resolvedPath: string | undefined;
8474
+ try {
8475
+ resolvedPath = this.#resolveSessionFsPath(filePath);
8476
+ } catch (error) {
8477
+ // Tool-result admission must survive malformed paths and unavailable local roots.
8478
+ // Without a resolved key, revoke every generation before any lifecycle await.
8479
+ this.#streamingEditFileCache.clear();
8480
+ this.#streamingEditPrecachePending.delete(filePath);
8481
+ this.#streamingEditPrecacheStale.delete(filePath);
8482
+ for (const pendingPath of this.#streamingEditPrecachePending.keys()) {
8483
+ this.#invalidateStreamingEditPrecachePath(pendingPath);
8484
+ }
8485
+ logger.debug("Failed to resolve streaming-edit cache invalidation path", {
8486
+ path: filePath,
8487
+ error: String(error),
8488
+ });
8489
+ return;
8490
+ }
8341
8491
  if (resolvedPath === undefined) return;
8342
8492
  this.#streamingEditFileCache.delete(resolvedPath);
8493
+ this.#invalidateStreamingEditPrecachePath(resolvedPath);
8343
8494
  }
8344
8495
 
8345
8496
  /**
@@ -8789,6 +8940,7 @@ export class AgentSession {
8789
8940
  args: event.args,
8790
8941
  intent: event.intent,
8791
8942
  };
8943
+ if (isNonDispatchedToolEvent(event)) markNonDispatchedToolEvent(extensionEvent);
8792
8944
  await this.#extensionRunner.emit(extensionEvent, undefined, deliveryScope);
8793
8945
  } else if (event.type === "tool_execution_update") {
8794
8946
  const extensionEvent: ToolExecutionUpdateEvent = {
@@ -8807,6 +8959,7 @@ export class AgentSession {
8807
8959
  result: event.result,
8808
8960
  isError: event.isError ?? false,
8809
8961
  };
8962
+ if (isNonDispatchedToolEvent(event)) markNonDispatchedToolEvent(extensionEvent);
8810
8963
  await this.#extensionRunner.emit(extensionEvent, undefined, deliveryScope);
8811
8964
  } else if (event.type === "auto_compaction_start") {
8812
8965
  await this.#extensionRunner.emit(
@@ -10281,13 +10434,13 @@ export class AgentSession {
10281
10434
  const bridge = this.#clientBridge;
10282
10435
  const acpEnabled = Boolean(bridge?.capabilities.requestPermission && bridge.requestPermission);
10283
10436
  const sdkEnabled = this.#sdkPermissionProvider !== undefined;
10284
- const activeSkill = this.#activeSkillState?.skill ?? "";
10285
- const activeSkillSession = this.#activeSkillState?.sessionId ?? "";
10437
+ // The wrapped behavior depends only on live session state (cwd, session id,
10438
+ // session agent dir) plus the ACP/SDK permission surfaces below. Active-skill
10439
+ // state is deliberately absent: the Ultragoal ask guard binds to the caller
10440
+ // session id alone, so a skill transition does not change any wrapper.
10286
10441
  return [
10287
10442
  "workflow-mutation-v1",
10288
-
10289
- "ultragoal-ask-v1",
10290
- `active=${activeSkill}:${activeSkillSession}`,
10443
+ "ultragoal-ask-v2",
10291
10444
  `acp=${acpEnabled ? "on" : "off"}:sdk=${sdkEnabled ? "on" : "off"}:${this.#acpPermissionWrapperVersion}`,
10292
10445
  ].join("|");
10293
10446
  }
@@ -10297,6 +10450,7 @@ export class AgentSession {
10297
10450
  let wrappersByVersion = this.#guardedToolWrapperCache.get(tool);
10298
10451
  const cached = wrappersByVersion?.get(cacheKey);
10299
10452
  if (cached) return cached as T;
10453
+ wrappersByVersion?.clear();
10300
10454
  const innerTool = tool instanceof ExtensionToolWrapper ? tool.getInnerTool() : tool;
10301
10455
  const guarded = this.#wrapToolForCwdTransitionFence(
10302
10456
  this.#wrapToolForWorkflowMutationGuard(
@@ -10304,10 +10458,7 @@ export class AgentSession {
10304
10458
  guardToolForUltragoalAsk(
10305
10459
  innerTool,
10306
10460
  () => this.sessionManager.getCwd(),
10307
- () => ({
10308
- activeSkillState: this.getActiveSkillState(),
10309
- sessionId: this.sessionManager.getSessionId(),
10310
- }),
10461
+ () => ({ sessionId: this.sessionManager.getSessionId() }),
10311
10462
  () => this.getSessionAgentDir(),
10312
10463
  ),
10313
10464
  ),
@@ -13089,11 +13240,12 @@ export class AgentSession {
13089
13240
  claimsGenuineUserIntent: options?.claimsGenuineUserIntent,
13090
13241
  onPromoted: options?.onPromoted,
13091
13242
  sdkRunToken: options?.sdkRunToken,
13243
+ scheduleNonAdmittedWake: false,
13092
13244
  onQueued: message => {
13093
13245
  if (this.#abortUnwind && !options?.forceOneAtATime) this.#abortUnwindSteerFallbacks.push(message);
13094
13246
  },
13095
13247
  });
13096
- this.#scheduleNonAdmittedSteerContinuation();
13248
+ this.#scheduleNonAdmittedQueuedContinuation();
13097
13249
  return;
13098
13250
  }
13099
13251
  if (options?.forceOneAtATime) this.#sequentialSteerMessages.add(message);
@@ -13111,15 +13263,14 @@ export class AgentSession {
13111
13263
  }
13112
13264
 
13113
13265
  /**
13114
- * A steer that was not admitted into a live run was requeued as a follow-up.
13266
+ * A queued message that was not admitted to the live loop needs a wakeup when
13267
+ * the loop has already ended but the session is still unwinding its prompt.
13115
13268
  * `#queueFollowUp` already schedules delivery when the session is idle; this
13116
- * covers the unwind window where the session still reports busy only because
13117
- * a finished prompt is unwinding (deferred agent_end / post-prompt work), so
13118
- * the follow-up gate refuses while no loop actually owns the queue. A live
13119
- * loop, a prompt still in preflight, or a drained queue makes the scheduled
13120
- * continue a no-op.
13269
+ * covers the unwind window where the public session still reports busy even
13270
+ * though no Agent loop owns the queue. A live loop, a prompt still in
13271
+ * preflight, or a drained queue makes the scheduled continue a no-op.
13121
13272
  */
13122
- #scheduleNonAdmittedSteerContinuation(): void {
13273
+ #scheduleNonAdmittedQueuedContinuation(): void {
13123
13274
  // `#queueFollowUp` already schedules delivery when the session is idle; this
13124
13275
  // only covers the window where that gate refuses (a finished prompt is
13125
13276
  // unwinding) while no loop owns the queue. The resumable-tail requirement
@@ -13158,6 +13309,7 @@ export class AgentSession {
13158
13309
  onPromoted?: (promotion: { startsOwnRun?: boolean; removed?: boolean }) => void;
13159
13310
  sdkRunToken?: string;
13160
13311
  onQueued?: (message: AgentMessage) => void;
13312
+ scheduleNonAdmittedWake?: boolean;
13161
13313
  },
13162
13314
  ): Promise<QueuedFollowUpOwner> {
13163
13315
  this.#assertNoHandoffTransition();
@@ -13189,10 +13341,12 @@ export class AgentSession {
13189
13341
  // without waiting for the next user turn. A later accepted follow-up must
13190
13342
  // not start unrelated queued work ahead of it, because that work has a
13191
13343
  // different cancellation and terminal owner.
13192
- if (queueWasEmpty)
13344
+ if (queueWasEmpty) {
13193
13345
  this.#scheduleQueuedFollowUpContinuation(() =>
13194
13346
  this.agent.snapshotFollowUp().some(candidate => candidate === message),
13195
13347
  );
13348
+ if (options?.scheduleNonAdmittedWake !== false) this.#scheduleNonAdmittedQueuedContinuation();
13349
+ }
13196
13350
  return {
13197
13351
  cancel: () => {
13198
13352
  const deferredIndex = this.#deferredSdkFollowUps.indexOf(message);
@@ -19774,6 +19928,10 @@ export class AgentSession {
19774
19928
  * provider session state and configured WebSocket transport preference
19775
19929
  * instead of falling back to a fresh HTTP/SSE session. Mirrors the
19776
19930
  * `providerSessionId ?? sessionId` affinity the agent loop sends per turn.
19931
+ *
19932
+ * `maintenanceCall` is what lets an agent-level provider (for example Devin
19933
+ * over ACP) refuse work it cannot serve instead of forwarding a
19934
+ * summarization prompt to a billed upstream agent.
19777
19935
  */
19778
19936
  #maintenanceProviderTransport(): {
19779
19937
  sessionId: string | undefined;
@@ -19781,6 +19939,7 @@ export class AgentSession {
19781
19939
  providerSessionState: Map<string, ProviderSessionState>;
19782
19940
  preferWebsockets: boolean | undefined;
19783
19941
  remoteCompactionFallbackHealth: RemoteCompactionFallbackHealthHooks;
19942
+ maintenanceCall: boolean;
19784
19943
  } {
19785
19944
  const providerSessionId = this.agent.providerSessionId ?? this.agent.sessionId;
19786
19945
  return {
@@ -19789,6 +19948,7 @@ export class AgentSession {
19789
19948
  providerSessionState: this.#providerSessionState,
19790
19949
  preferWebsockets: this.agent.preferWebsockets,
19791
19950
  remoteCompactionFallbackHealth: this.#remoteCompactionFallbackHealth,
19951
+ maintenanceCall: true,
19792
19952
  };
19793
19953
  }
19794
19954
 
@@ -19933,6 +20093,35 @@ export class AgentSession {
19933
20093
  // disabled/off settings so a resource-floor breach still compacts before OOM.
19934
20094
  if (!options?.force && compactionSettings.strategy === "off") return { kind: "skipped" };
19935
20095
  if (!options?.force && reason !== "idle" && !compactionSettings.enabled) return { kind: "skipped" };
20096
+ // Agent-level providers (e.g. Devin over ACP) own conversation history and
20097
+ // refuse GJC maintenance calls by contract. Context-full maintenance walks
20098
+ // the candidate chain while handoff generation calls the session model
20099
+ // directly, so when every model the selected action can reach is
20100
+ // agent-level the attempt can only report a guaranteed refusal on every
20101
+ // threshold crossing — treat it as a benign skip like maintenance being off.
20102
+ // A session_before_compact hook can still serve context-full maintenance
20103
+ // without a model call, so it keeps the session eligible.
20104
+ const handoffSelected = compactionSettings.strategy === "handoff" && reason !== "overflow";
20105
+ const reachableModels =
20106
+ handoffSelected && this.model
20107
+ ? [this.model]
20108
+ : this.#getCompactionModelCandidates(this.#modelRegistry.getAvailable());
20109
+ const hookServesMaintenance =
20110
+ !handoffSelected && this.#extensionRunner?.hasHandlers("session_before_compact") === true;
20111
+ if (
20112
+ reachableModels.length > 0 &&
20113
+ !reachableModels.some(modelSupportsMaintenanceCalls) &&
20114
+ !hookServesMaintenance
20115
+ ) {
20116
+ logger.debug(
20117
+ "Auto-compaction skipped: every reachable model is an agent-level provider that refuses maintenance calls",
20118
+ {
20119
+ reason,
20120
+ provider: this.model?.provider,
20121
+ },
20122
+ );
20123
+ return { kind: "skipped" };
20124
+ }
19936
20125
  const generation = this.#promptGeneration;
19937
20126
  if (
19938
20127
  options?.deferHandoffMaintenance !== false &&
@@ -20190,7 +20379,24 @@ export class AgentSession {
20190
20379
  details = compactionPrep.details;
20191
20380
  preserveData = compactionPrep.preserveData;
20192
20381
  } else {
20193
- const candidates = this.#getCompactionModelCandidates(availableModels);
20382
+ // Agent-level providers refuse maintenance calls by contract, so they
20383
+ // can only add a guaranteed refusal to the failure list — never spend a
20384
+ // call on one while a text-model candidate exists. The pre-flight
20385
+ // reachability check normally catches the all-agent-level case; this
20386
+ // covers a mid-flight model swap landing here.
20387
+ const candidates =
20388
+ this.#getCompactionModelCandidates(availableModels).filter(modelSupportsMaintenanceCalls);
20389
+ if (candidates.length === 0) {
20390
+ await this.#emitSessionEvent({
20391
+ type: "auto_compaction_end",
20392
+ action,
20393
+ result: undefined,
20394
+ aborted: false,
20395
+ willRetry: false,
20396
+ skipped: true,
20397
+ });
20398
+ return { kind: "skipped" };
20399
+ }
20194
20400
  maintenanceAttemptSignature = this.#buildAutoMaintenanceAttemptSignature(
20195
20401
  action,
20196
20402
  preparation,
@@ -20533,6 +20739,30 @@ export class AgentSession {
20533
20739
  );
20534
20740
  }
20535
20741
 
20742
+ #isCodexCredentialModelUnavailable(message: AssistantMessage): boolean {
20743
+ return (
20744
+ message.api === "openai-codex-responses" &&
20745
+ message.provider === "openai-codex" &&
20746
+ message.transportFailure?.credentialModelUnavailable === true
20747
+ );
20748
+ }
20749
+
20750
+ #canRotateCodexCredential(message: AssistantMessage): boolean {
20751
+ if (!this.#isCodexCredentialModelUnavailable(message)) return false;
20752
+ if (this.#codexCredentialModelUnavailableRetried || assistantMessageHasVisibleOrToolContent(message))
20753
+ return false;
20754
+ const authStorage = this.#modelRegistry.authStorage;
20755
+ const provider = message.provider;
20756
+ const owner = this.#modelRegistry.getAuthStorageOwner();
20757
+ return (
20758
+ authStorage.getSessionCredentialType(provider, this.credentialSessionId) === "oauth" &&
20759
+ !authStorage.hasRuntimeApiKey(provider) &&
20760
+ !authStorage.hasRuntimeCredentialSelector(provider) &&
20761
+ !authStorage.hasSessionCredentialSelector(provider, this.credentialSessionId) &&
20762
+ !authStorage.hasConfigApiKey(provider, owner)
20763
+ );
20764
+ }
20765
+
20536
20766
  #isRetryableError(message: AssistantMessage): boolean {
20537
20767
  if (this.#isTerminalProviderFirstEventTimeout(message)) return false;
20538
20768
  if (message.errorMessage?.startsWith("Model fallback chain exhausted;")) return false;
@@ -20545,6 +20775,16 @@ export class AgentSession {
20545
20775
  const contextWindow = this.model?.contextWindow ?? 0;
20546
20776
  if (classifyContextOverflow(message, transportFailure, contextWindow)) return false;
20547
20777
  const managedFallback = this.#defaultFallbackChain().chain.entries.length > 1;
20778
+ // An account-specific model rejection that cannot rotate to another
20779
+ // credential stays terminal only on the session's own retry path; managed
20780
+ // fallback still advances the model chain through the controller.
20781
+ if (
20782
+ this.#isCodexCredentialModelUnavailable(message) &&
20783
+ !this.#canRotateCodexCredential(message) &&
20784
+ !managedFallback
20785
+ ) {
20786
+ return false;
20787
+ }
20548
20788
  if (!managedFallback) {
20549
20789
  const classification = this.#classifyErrorForRetry(message);
20550
20790
  return (
@@ -20675,6 +20915,9 @@ export class AgentSession {
20675
20915
  if (message.errorKind === "local_buffer_overflow") return "local_buffer_overflow";
20676
20916
  if (this.#isTypedFirstEventTimeout(message)) return "first_event_timeout";
20677
20917
  if (this.#isTypedEmptyResponse(message)) return "empty_response";
20918
+ if (this.#isCodexCredentialModelUnavailable(message)) {
20919
+ return this.#canRotateCodexCredential(message) ? "unknown" : "terminal";
20920
+ }
20678
20921
  if (!message.errorMessage) return "none";
20679
20922
  const err = message.errorMessage;
20680
20923
  // Managed-attempt local failures from restored sessions may lack the
@@ -21490,7 +21733,7 @@ export class AgentSession {
21490
21733
  * Marks the credential that just failed and reports whether the session
21491
21734
  * actually moved to a DIFFERENT stored credential.
21492
21735
  *
21493
- * Three invariants this enforces, in order:
21736
+ * Credential mutation invariants:
21494
21737
  *
21495
21738
  * 1. **Pin guard, first and for every trigger class.** A pinned credential
21496
21739
  * (`--api-key` or `--credential`) must never be mutated or rotated away
@@ -21500,18 +21743,23 @@ export class AgentSession {
21500
21743
  * path invalidated pinned credentials outright.
21501
21744
  * 2. **A terminal `forbidden` never mutates credential state.** Rotation
21502
21745
  * would hide an authorization defect and cycle through healthy rows.
21503
- * 3. **Distinct-row proof in BOTH branches.** `invalidateCredentialMatching`
21504
- * reports "I matched and blocked a row", which is not the same as "the
21505
- * session now uses a different credential" — with a single-row pool it is
21506
- * true while nothing rotated. Both branches therefore re-resolve and
21507
- * require the active key to have actually changed.
21746
+ * 3. Auth failures retain their existing key-change proof. Quota, rate-limit
21747
+ * and OAuth account-model rejections mark before resolving and require two known,
21748
+ * different stored row IDs before reporting rotation. This is mark-time
21749
+ * identity, not dispatch-bound attribution across shared sessions.
21508
21750
  */
21509
21751
  async #markFailedCredential(trigger: {
21510
21752
  class: FallbackTriggerClass;
21511
21753
  retryAfterMs?: number;
21512
21754
  authDisposition?: AuthDisposition;
21513
21755
  }): Promise<"rotated" | "exhausted" | "unchanged"> {
21514
- if (!this.model || (trigger.class !== "auth" && trigger.class !== "quota" && trigger.class !== "rate_limit")) {
21756
+ if (
21757
+ !this.model ||
21758
+ (trigger.class !== "auth" &&
21759
+ trigger.class !== "quota" &&
21760
+ trigger.class !== "rate_limit" &&
21761
+ trigger.class !== "credential")
21762
+ ) {
21515
21763
  return "unchanged";
21516
21764
  }
21517
21765
  // (2) Terminal forbidden: no credential state may change.
@@ -21520,27 +21768,43 @@ export class AgentSession {
21520
21768
  const authStorage = this.#modelRegistry.authStorage;
21521
21769
  const provider = this.model.provider;
21522
21770
  // (1) Pin guard, before any mutation and for every branch.
21523
- if (authStorage.hasRuntimeApiKey(provider) || authStorage.hasRuntimeCredentialSelector(provider)) {
21771
+ if (
21772
+ authStorage.hasRuntimeApiKey(provider) ||
21773
+ authStorage.hasRuntimeCredentialSelector(provider) ||
21774
+ authStorage.hasSessionCredentialSelector(provider, this.credentialSessionId) ||
21775
+ authStorage.hasConfigApiKey(provider, this.#modelRegistry.getAuthStorageOwner())
21776
+ ) {
21524
21777
  return "unchanged";
21525
21778
  }
21526
21779
 
21527
21780
  const credentialSessionId = this.credentialSessionId;
21528
- const activeApiKey = await this.#modelRegistry.getApiKey(this.model, credentialSessionId);
21529
-
21530
- let remaining: boolean;
21531
- if (trigger.class === "auth") {
21532
- if (!isAuthenticated(activeApiKey)) return "unchanged";
21533
- remaining = await authStorage.invalidateCredentialMatching(provider, activeApiKey, {
21534
- sessionId: credentialSessionId,
21535
- owner: this.#modelRegistry.getAuthStorageOwner(),
21536
- });
21537
- if (!remaining) return "unchanged";
21538
- } else {
21539
- remaining = await authStorage.markUsageLimitReached(provider, credentialSessionId, {
21540
- retryAfterMs: trigger.retryAfterMs,
21781
+ if (trigger.class !== "auth") {
21782
+ if (
21783
+ trigger.class === "credential" &&
21784
+ authStorage.getSessionCredentialType(provider, credentialSessionId) !== "oauth"
21785
+ )
21786
+ return "unchanged";
21787
+ const before = authStorage.getSessionCredentialRowId(provider, credentialSessionId);
21788
+ const remaining = await authStorage.markUsageLimitReached(provider, credentialSessionId, {
21789
+ // Account-model rejection retains default backoff, not response retry-after.
21790
+ ...(trigger.class === "credential" ? {} : { retryAfterMs: trigger.retryAfterMs }),
21541
21791
  owner: this.#modelRegistry.getAuthStorageOwner(),
21792
+ ...(before === undefined ? {} : { rowId: before }),
21542
21793
  });
21794
+ if (!remaining) return "exhausted";
21795
+ await this.#modelRegistry.getApiKey(this.model, credentialSessionId);
21796
+ const after = authStorage.getSessionCredentialRowId(provider, credentialSessionId);
21797
+ if (before !== undefined && after !== undefined && before !== after) return "rotated";
21798
+ return "unchanged";
21543
21799
  }
21800
+ const activeApiKey = await this.#modelRegistry.getApiKey(this.model, credentialSessionId);
21801
+
21802
+ if (!isAuthenticated(activeApiKey)) return "unchanged";
21803
+ const remaining = await authStorage.invalidateCredentialMatching(provider, activeApiKey, {
21804
+ sessionId: credentialSessionId,
21805
+ owner: this.#modelRegistry.getAuthStorageOwner(),
21806
+ });
21807
+ if (!remaining) return "unchanged";
21544
21808
 
21545
21809
  // (3) Distinct-row proof.
21546
21810
  if ((await this.#modelRegistry.getApiKey(this.model, credentialSessionId)) !== activeApiKey) {
@@ -21626,10 +21890,19 @@ export class AgentSession {
21626
21890
  }
21627
21891
  : false;
21628
21892
  }
21893
+ const canRotateCodexCredential = this.#canRotateCodexCredential(message);
21894
+ if (this.#isCodexCredentialModelUnavailable(message) && !canRotateCodexCredential && !managedFallback) {
21895
+ return managedOutcome
21896
+ ? {
21897
+ type: "terminal",
21898
+ terminal: { stopReason: "error", messages: [message] },
21899
+ }
21900
+ : false;
21901
+ }
21629
21902
  // retry.enabled=false always surfaces immediately, matching the explicit
21630
21903
  // user opt-out. The managed provider-fallback chain keeps its own
21631
21904
  // availability policy.
21632
- if (!retrySettings.enabled && !managedFallback) {
21905
+ if (!retrySettings.enabled && !managedFallback && !canRotateCodexCredential) {
21633
21906
  return managedOutcome
21634
21907
  ? {
21635
21908
  type: "terminal",
@@ -21709,6 +21982,19 @@ export class AgentSession {
21709
21982
  // extension lifecycle participation. Mark the failed credential and
21710
21983
  // retry with the next stored credential of the same provider.
21711
21984
  let credentialRotated = false;
21985
+ if (canRotateCodexCredential && trigger.class === "credential") {
21986
+ const mark = await this.#markFailedCredential(trigger);
21987
+ this.#codexCredentialModelUnavailableRetried = true;
21988
+ credentialRotated = mark === "rotated";
21989
+ if (!credentialRotated && !managedFallback) {
21990
+ return managedOutcome
21991
+ ? {
21992
+ type: "terminal",
21993
+ terminal: { stopReason: "error", messages: [message] },
21994
+ }
21995
+ : false;
21996
+ }
21997
+ }
21712
21998
  if (
21713
21999
  !managedFallback &&
21714
22000
  !providerRetryCeilingReached &&
@@ -21771,6 +22057,15 @@ export class AgentSession {
21771
22057
  if (providerRetryCeilingReached && outcome === "retry") {
21772
22058
  outcome = controller.advance() ? "advance" : "exhausted";
21773
22059
  }
22060
+ // The one content-free account retry was already spent: never spend
22061
+ // another same-model attempt on a third account, advance instead.
22062
+ if (
22063
+ outcome === "retry" &&
22064
+ this.#isCodexCredentialModelUnavailable(message) &&
22065
+ this.#codexCredentialModelUnavailableRetried
22066
+ ) {
22067
+ outcome = controller.advance() ? "advance" : "exhausted";
22068
+ }
21774
22069
  } else {
21775
22070
  outcome = providerRetryCeilingReached
21776
22071
  ? "exhausted"
@@ -21778,6 +22073,15 @@ export class AgentSession {
21778
22073
  ? "retry"
21779
22074
  : "exhausted";
21780
22075
  }
22076
+ if (
22077
+ managedFallback &&
22078
+ outcome !== "retry" &&
22079
+ trigger.class === "rate_limit" &&
22080
+ trigger.retryAfterMs !== undefined &&
22081
+ failedSelector
22082
+ ) {
22083
+ this.#modelRegistry.suppressSelector(failedSelector, Date.now() + trigger.retryAfterMs);
22084
+ }
21781
22085
  // Credential rotation is unbounded: a fresh credential is a different
21782
22086
  // retry dimension from transient-error backoff, so it overrides maxRetries
21783
22087
  // exhaustion and forces an immediate same-model retry.
@@ -21841,15 +22145,11 @@ export class AgentSession {
21841
22145
  credentialRotated || outcome === "advance"
21842
22146
  ? 0
21843
22147
  : managedFallback
21844
- ? effectiveFallbackDelay(retrySettings.baseDelayMs, retrySettings.maxDelayMs, attemptsUsed, retryAfterMs)
22148
+ ? effectiveFallbackDelay(retrySettings.baseDelayMs, retrySettings.maxDelayMs, attemptsUsed)
21845
22149
  : retryAfterMs !== undefined
21846
22150
  ? Math.min(retryAfterMs, retrySettings.maxDelayMs)
21847
22151
  : cappedExponentialWithFullJitter(retrySettings.baseDelayMs, retrySettings.maxDelayMs, attemptsUsed);
21848
22152
 
21849
- if (managedFallback && trigger.class === "rate_limit" && trigger.retryAfterMs !== undefined && failedSelector) {
21850
- this.#modelRegistry.suppressSelector(failedSelector, Date.now() + trigger.retryAfterMs);
21851
- }
21852
-
21853
22153
  const retry = async (ownership?: ManagedAttemptContinuationOwnership): Promise<void> => {
21854
22154
  const activePromptHandle = this.activePromptHandle;
21855
22155
  const cancellationSignal =
@@ -21858,7 +22158,12 @@ export class AgentSession {
21858
22158
  if (ownership && (!ownership.isCurrent() || cancellationSignal?.aborted)) return;
21859
22159
  if (retryCancelled()) return;
21860
22160
  let quotaPoolExhausted = false;
21861
- if (managedFallback && !credentialRotated && !providerRetryCeilingReached) {
22161
+ if (
22162
+ managedFallback &&
22163
+ !credentialRotated &&
22164
+ !providerRetryCeilingReached &&
22165
+ !(this.#isCodexCredentialModelUnavailable(message) && this.#codexCredentialModelUnavailableRetried)
22166
+ ) {
21862
22167
  const mark = await this.#markFailedCredential(trigger);
21863
22168
  if (mark === "rotated") credentialRotated = true;
21864
22169
  quotaPoolExhausted = mark === "exhausted";