@gajae-code/coding-agent 0.15.5 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/CHANGELOG.md +85 -1
  2. package/dist/types/async/job-manager.d.ts +83 -0
  3. package/dist/types/cli/args.d.ts +2 -0
  4. package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
  5. package/dist/types/cli/model-presets-cli.d.ts +9 -0
  6. package/dist/types/cli/root-flags.d.ts +14 -0
  7. package/dist/types/cli/update-cli.d.ts +15 -0
  8. package/dist/types/cli.d.ts +7 -0
  9. package/dist/types/commands/auth-gateway.d.ts +3 -0
  10. package/dist/types/commands/gc.d.ts +13 -0
  11. package/dist/types/commands/launch.d.ts +10 -0
  12. package/dist/types/commands/model-presets.d.ts +21 -0
  13. package/dist/types/config/autorouting-tier-map.d.ts +120 -0
  14. package/dist/types/config/file-lock.d.ts +54 -2
  15. package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
  16. package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
  17. package/dist/types/config/keybindings.d.ts +3 -2
  18. package/dist/types/config/model-preset-registry.d.ts +368 -0
  19. package/dist/types/config/model-profile-activation.d.ts +39 -0
  20. package/dist/types/config/model-profiles.d.ts +5 -3
  21. package/dist/types/config/model-registry.d.ts +17 -5
  22. package/dist/types/config/settings-schema.d.ts +19 -1
  23. package/dist/types/config/settings.d.ts +2 -0
  24. package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
  25. package/dist/types/coordinator-mcp/projection-scan.d.ts +54 -0
  26. package/dist/types/crash/record-loader.d.ts +15 -0
  27. package/dist/types/extensibility/extensions/types.d.ts +2 -5
  28. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
  29. package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
  30. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  31. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  32. package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
  33. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +72 -1
  34. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  35. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  36. package/dist/types/index.d.ts +1 -0
  37. package/dist/types/main.d.ts +3 -1
  38. package/dist/types/master-mode/context.d.ts +21 -0
  39. package/dist/types/master-mode/first-request.d.ts +34 -0
  40. package/dist/types/master-mode/snapshot.d.ts +32 -0
  41. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  42. package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
  43. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  44. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  45. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  46. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  47. package/dist/types/modes/jobs-observer.d.ts +36 -0
  48. package/dist/types/modes/print-mode.d.ts +17 -0
  49. package/dist/types/modes/ui-language.d.ts +7 -1
  50. package/dist/types/sdk/broker/broker.d.ts +80 -1
  51. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  52. package/dist/types/sdk/broker/index.d.ts +3 -0
  53. package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
  54. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  55. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  56. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  57. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  58. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  59. package/dist/types/sdk/broker/state-version.d.ts +4 -1
  60. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  61. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  62. package/dist/types/sdk/bus/index.d.ts +6 -0
  63. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
  64. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
  65. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  66. package/dist/types/sdk/cli/index.d.ts +1 -0
  67. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  68. package/dist/types/sdk/cli/rows.d.ts +4 -3
  69. package/dist/types/sdk/cli/session-cli.d.ts +18 -2
  70. package/dist/types/sdk/client/client.d.ts +3 -2
  71. package/dist/types/sdk/client/discovery.d.ts +1 -0
  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/host.d.ts +10 -0
  77. package/dist/types/sdk/host/model-pin.d.ts +7 -2
  78. package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
  79. package/dist/types/sdk/host/session-runtime.d.ts +67 -1
  80. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  81. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  82. package/dist/types/sdk/lifecycle-session.d.ts +2 -0
  83. package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
  84. package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
  85. package/dist/types/sdk/router/session-router.d.ts +11 -0
  86. package/dist/types/sdk/session.d.ts +16 -0
  87. package/dist/types/sdk/startup-capability.d.ts +2 -1
  88. package/dist/types/session/account-inventory.d.ts +1 -0
  89. package/dist/types/session/agent-session.d.ts +24 -7
  90. package/dist/types/session/credential-store-errors.d.ts +1 -0
  91. package/dist/types/session/fold-coordinator.d.ts +201 -0
  92. package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
  93. package/dist/types/session/sdk-run-capability.d.ts +2 -0
  94. package/dist/types/session/session-manager.d.ts +65 -0
  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/telemetry/control.d.ts +11 -0
  98. package/dist/types/telemetry/events.d.ts +20 -0
  99. package/dist/types/telemetry/index.d.ts +3 -0
  100. package/dist/types/telemetry/transport.d.ts +22 -0
  101. package/dist/types/tools/bash-interactive.d.ts +23 -0
  102. package/dist/types/tools/bash.d.ts +4 -0
  103. package/dist/types/tools/gh.d.ts +8 -0
  104. package/dist/types/tools/index.d.ts +10 -5
  105. package/dist/types/web/search/providers/base.d.ts +9 -4
  106. package/dist/types/web/search/providers/xai.d.ts +2 -1
  107. package/dist/types/web/search/types.d.ts +14 -0
  108. package/package.json +10 -7
  109. package/scripts/generate-hotkeys-docs.ts +4 -0
  110. package/scripts/generate-sdk-operation-inventory.ts +9 -3
  111. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  112. package/src/async/job-manager.ts +671 -39
  113. package/src/cli/accounts-cli.ts +9 -5
  114. package/src/cli/args.ts +17 -0
  115. package/src/cli/auth-broker-cli.ts +40 -12
  116. package/src/cli/auth-gateway-cli.ts +273 -97
  117. package/src/cli/config-cli.ts +2 -2
  118. package/src/cli/fast-help.ts +4 -0
  119. package/src/cli/model-presets-cli.ts +100 -0
  120. package/src/cli/root-flags.ts +5 -0
  121. package/src/cli/update-cli.ts +117 -16
  122. package/src/cli.ts +6 -2
  123. package/src/commands/auth-gateway.ts +12 -8
  124. package/src/commands/gc.ts +19 -0
  125. package/src/commands/launch.ts +86 -18
  126. package/src/commands/model-presets.ts +24 -0
  127. package/src/commands/sdk.ts +113 -21
  128. package/src/config/autorouting-tier-map.ts +44 -0
  129. package/src/config/file-lock-gc.ts +9 -4
  130. package/src/config/file-lock.ts +890 -99
  131. package/src/config/internal/model-preset-registry-test-state.ts +28 -0
  132. package/src/config/internal/model-preset-registry-test-support.ts +11 -0
  133. package/src/config/keybindings.ts +6 -2
  134. package/src/config/model-preset-registry.ts +2795 -0
  135. package/src/config/model-profile-activation.ts +115 -28
  136. package/src/config/model-profile-contract.ts +4 -1
  137. package/src/config/model-profiles.ts +56 -14
  138. package/src/config/model-registry.ts +1044 -122
  139. package/src/config/model-resolver.ts +5 -0
  140. package/src/config/settings-schema.ts +19 -1
  141. package/src/config/settings.ts +11 -1
  142. package/src/coordinator-mcp/model-preset.ts +32 -17
  143. package/src/coordinator-mcp/projection-scan.ts +592 -0
  144. package/src/coordinator-mcp/question-state.ts +9 -11
  145. package/src/coordinator-mcp/server.ts +111 -42
  146. package/src/coordinator-mcp/session-reaper.ts +9 -3
  147. package/src/crash/index-store.ts +2 -0
  148. package/src/crash/record-loader.ts +10 -0
  149. package/src/extensibility/extensions/loader.ts +0 -1
  150. package/src/extensibility/extensions/types.ts +2 -3
  151. package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
  152. package/src/gjc-runtime/empty-delete-gc.ts +301 -0
  153. package/src/gjc-runtime/gc-render.ts +29 -0
  154. package/src/gjc-runtime/gc-runtime.ts +174 -11
  155. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  156. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  157. package/src/gjc-runtime/session-state-lock.ts +1321 -147
  158. package/src/gjc-runtime/session-state-sidecar.ts +1004 -25
  159. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  160. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
  161. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  162. package/src/hooks/skill-state.ts +6 -0
  163. package/src/index.ts +1 -0
  164. package/src/internal-urls/docs-index.generated.ts +14 -13
  165. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  166. package/src/lsp/config.ts +4 -3
  167. package/src/lsp/path-trust.ts +12 -6
  168. package/src/main.ts +66 -4
  169. package/src/master-mode/context.ts +50 -0
  170. package/src/master-mode/first-request.ts +65 -0
  171. package/src/master-mode/snapshot.ts +205 -0
  172. package/src/modes/acp/acp-agent.ts +616 -161
  173. package/src/modes/components/footer.ts +4 -2
  174. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  175. package/src/modes/components/jobs-overlay-model.ts +99 -16
  176. package/src/modes/components/model-selector.ts +158 -18
  177. package/src/modes/components/oauth-selector.ts +6 -1
  178. package/src/modes/components/settings-selector.ts +1 -0
  179. package/src/modes/components/status-line/model-name.ts +53 -0
  180. package/src/modes/components/status-line/priority-row.ts +107 -0
  181. package/src/modes/components/status-line/segments.ts +29 -10
  182. package/src/modes/components/tool-status-header.ts +151 -15
  183. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  184. package/src/modes/controllers/input-controller.ts +51 -6
  185. package/src/modes/controllers/selector-controller.ts +191 -96
  186. package/src/modes/interactive-mode.ts +14 -1
  187. package/src/modes/jobs-observer.ts +149 -16
  188. package/src/modes/print-mode.ts +64 -0
  189. package/src/modes/runtime-init.ts +1 -1
  190. package/src/modes/ui-language.ts +49 -2
  191. package/src/modes/utils/injected-user-submission.ts +15 -1
  192. package/src/prompts/system/master-mode.md +28 -0
  193. package/src/prompts/tools/bash.md +2 -2
  194. package/src/prompts/tools/fold-receipt.md +3 -0
  195. package/src/prompts/tools/job.md +1 -1
  196. package/src/sdk/acp/adapter.ts +17 -2
  197. package/src/sdk/broker/broker.ts +1671 -34
  198. package/src/sdk/broker/endpoint-authority.ts +86 -0
  199. package/src/sdk/broker/ensure.ts +5 -0
  200. package/src/sdk/broker/index.ts +3 -0
  201. package/src/sdk/broker/lifecycle.ts +217 -46
  202. package/src/sdk/broker/master-capability.ts +232 -0
  203. package/src/sdk/broker/session-index.ts +121 -17
  204. package/src/sdk/broker/session-scope.ts +272 -0
  205. package/src/sdk/broker/spawn-authority.ts +1031 -0
  206. package/src/sdk/broker/spawn-substrate.ts +548 -0
  207. package/src/sdk/broker/state-version.ts +26 -14
  208. package/src/sdk/broker/transport.ts +1 -0
  209. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  210. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  211. package/src/sdk/bus/chat-daemon-runtime.ts +46 -5
  212. package/src/sdk/bus/index.ts +51 -4
  213. package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
  214. package/src/sdk/bus/prompt-reconciliation.ts +28 -4
  215. package/src/sdk/bus/slack-conversation.ts +5 -1
  216. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  217. package/src/sdk/cli/index.ts +1 -0
  218. package/src/sdk/cli/master-cli.ts +259 -0
  219. package/src/sdk/cli/rows.ts +11 -8
  220. package/src/sdk/cli/session-cli.ts +276 -6
  221. package/src/sdk/client/client.ts +14 -8
  222. package/src/sdk/client/discovery.ts +3 -1
  223. package/src/sdk/host/control/dispatch.ts +101 -37
  224. package/src/sdk/host/control/index.ts +2 -0
  225. package/src/sdk/host/control/operations.ts +2 -0
  226. package/src/sdk/host/control/runtime-gate.ts +203 -11
  227. package/src/sdk/host/host.ts +27 -2
  228. package/src/sdk/host/model-pin.ts +185 -25
  229. package/src/sdk/host/sdk-run-capability.ts +1 -0
  230. package/src/sdk/host/session-runtime.test.ts +581 -43
  231. package/src/sdk/host/session-runtime.ts +655 -101
  232. package/src/sdk/lifecycle/broker-client.ts +28 -0
  233. package/src/sdk/lifecycle/service.ts +279 -20
  234. package/src/sdk/lifecycle-session.ts +10 -2
  235. package/src/sdk/prompt-deadline-manager.ts +16 -1
  236. package/src/sdk/protocol/adapter-validation.ts +27 -0
  237. package/src/sdk/protocol/operation-inventory.generated.json +53 -2
  238. package/src/sdk/protocol/operation-registry.ts +17 -4
  239. package/src/sdk/router/session-router.ts +294 -154
  240. package/src/sdk/session.ts +440 -167
  241. package/src/sdk/startup-capability.ts +1 -0
  242. package/src/session/account-inventory.ts +27 -11
  243. package/src/session/agent-session.ts +1292 -180
  244. package/src/session/blob-store.ts +15 -5
  245. package/src/session/credential-store-errors.ts +2 -0
  246. package/src/session/fold-coordinator.ts +433 -0
  247. package/src/session/internal/managed-session-storage.ts +153 -78
  248. package/src/session/sdk-run-capability-internal.ts +15 -0
  249. package/src/session/sdk-run-capability.ts +2 -0
  250. package/src/session/session-manager.ts +342 -28
  251. package/src/session/yield-queue.ts +24 -10
  252. package/src/setup/frictionless-onboarding.ts +2 -0
  253. package/src/setup/provider-presets.json +1 -1
  254. package/src/slash-commands/helpers/credential-switch.ts +6 -1
  255. package/src/task/executor.ts +19 -3
  256. package/src/telemetry/control.ts +26 -0
  257. package/src/telemetry/events.ts +1268 -0
  258. package/src/telemetry/index.ts +3 -0
  259. package/src/telemetry/transport.ts +87 -0
  260. package/src/tools/bash-interactive.ts +182 -85
  261. package/src/tools/bash.ts +1028 -109
  262. package/src/tools/gh.ts +104 -48
  263. package/src/tools/github-cache.ts +2 -2
  264. package/src/tools/index.ts +11 -5
  265. package/src/tools/tool-catalog.generated.ts +1 -1
  266. package/src/web/search/provider.ts +120 -11
  267. package/src/web/search/providers/anthropic.ts +89 -17
  268. package/src/web/search/providers/base.ts +9 -4
  269. package/src/web/search/providers/gemini.ts +87 -15
  270. package/src/web/search/providers/openai-compatible.ts +23 -8
  271. package/src/web/search/providers/xai.ts +98 -12
  272. package/src/web/search/types.ts +16 -0
@@ -7299,8 +7299,49 @@ export class SessionManager {
7299
7299
  /** Serializes model, SDK, and ACP cwd transitions; dispose joins this tail. */
7300
7300
  #cwdTransitionTail: Promise<void> = Promise.resolve();
7301
7301
  #cwdTransitionOwner: symbol | undefined;
7302
+ #cwdMoveAdmissionClosed = false;
7302
7303
  #cwdReadLeaseOwner = Symbol("cwd-read-lease-owner");
7303
7304
  #cwdGeneration = 0;
7305
+ /**
7306
+ * Listeners invoked after every committed `moveTo`, for every move surface
7307
+ * (`move_session`, TUI `/move`, SDK/ACP `session.cwd.move`). They receive the previous
7308
+ * and new cwd and the previous session file, and run best-effort: a listener throw is
7309
+ * logged and never turns an already-durable move into a rejection.
7310
+ */
7311
+ #afterMoveListeners = new Set<
7312
+ (move: { previousCwd: string; newCwd: string; previousSessionFile: string | undefined }) => void | Promise<void>
7313
+ >();
7314
+ #beforeMoveListeners = new Set<
7315
+ (move: {
7316
+ previousCwd: string;
7317
+ newCwd: string;
7318
+ previousSessionFile: string | undefined;
7319
+ newSessionFile: string | undefined;
7320
+ previousCwdIdentity: { dev: bigint; ino: bigint };
7321
+ newCwdIdentity: { dev: bigint; ino: bigint };
7322
+ }) => void | Promise<void>
7323
+ >();
7324
+ #moveAbortListeners = new Set<
7325
+ (move: {
7326
+ previousCwd: string;
7327
+ newCwd: string;
7328
+ previousSessionFile: string | undefined;
7329
+ newSessionFile: string | undefined;
7330
+ previousCwdIdentity: { dev: bigint; ino: bigint };
7331
+ newCwdIdentity: { dev: bigint; ino: bigint };
7332
+ preserveRecoveryJournal?: boolean;
7333
+ }) => void | Promise<void>
7334
+ >();
7335
+ #movePublicationListeners = new Set<
7336
+ (move: {
7337
+ previousCwd: string;
7338
+ newCwd: string;
7339
+ previousSessionFile: string | undefined;
7340
+ newSessionFile: string | undefined;
7341
+ previousCwdIdentity: { dev: bigint; ino: bigint };
7342
+ newCwdIdentity: { dev: bigint; ino: bigint };
7343
+ }) => void | Promise<void>
7344
+ >();
7304
7345
  /** Number of tool executions currently holding a shared read lease on `cwd`. */
7305
7346
  #cwdReaderCount = 0;
7306
7347
  /** Resolved when the last outstanding read lease is released. */
@@ -8385,13 +8426,13 @@ export class SessionManager {
8385
8426
  this.#usageStatistics = commit.usageStatistics;
8386
8427
  this.#flushed = true;
8387
8428
  this.#ensuredOnDisk = true;
8388
- this.#managedPersistExpectedIdentity = this.#captureManagedPersistIdentity(sessionFile);
8429
+ this.#managedPersistExpectedIdentity =
8430
+ this.destination.kind === "managed" ? this.#captureManagedPersistIdentity(sessionFile) : undefined;
8389
8431
  this.#lazyReopenSucceeded = true;
8390
8432
  this.#lazyReopenFallbackReason = undefined;
8391
8433
  initialized = true;
8392
8434
  return true;
8393
- } catch (error) {
8394
- SessionManagerTestHooks.lastSidecarInitError = toError(error).message;
8435
+ } catch {
8395
8436
  return false;
8396
8437
  } finally {
8397
8438
  if (!initialized) {
@@ -8572,7 +8613,8 @@ export class SessionManager {
8572
8613
  this.#lazyReopenFallbackReason = "bounded_first_open_descriptor_changed";
8573
8614
  return false;
8574
8615
  }
8575
- this.#managedPersistExpectedIdentity = this.#captureManagedPersistIdentity(sessionFile);
8616
+ this.#managedPersistExpectedIdentity =
8617
+ this.destination.kind === "managed" ? this.#captureManagedPersistIdentity(sessionFile) : undefined;
8576
8618
  this.#sessionId = discovery.header.id;
8577
8619
  this.#sessionName = discovery.header.title;
8578
8620
  this.#titleSource = discovery.header.titleSource;
@@ -10727,10 +10769,139 @@ export class SessionManager {
10727
10769
  async joinCwdTransition(): Promise<void> {
10728
10770
  await this.#cwdTransitionTail;
10729
10771
  }
10772
+
10773
+ async closeCwdMoveAdmission(): Promise<void> {
10774
+ await this.runExclusiveCwdTransition(async () => {
10775
+ this.#cwdMoveAdmissionClosed = true;
10776
+ });
10777
+ }
10730
10778
  getCwdGeneration(): number {
10731
10779
  return this.#cwdGeneration;
10732
10780
  }
10733
10781
 
10782
+ /**
10783
+ * Register a listener invoked after every committed `moveTo`. Returns an idempotent
10784
+ * unregister handle. Every move surface funnels through `moveTo`, so a single
10785
+ * registration covers `move_session`, `/move`, and SDK/ACP `session.cwd.move`.
10786
+ */
10787
+ registerAfterMoveListener(
10788
+ listener: (move: {
10789
+ previousCwd: string;
10790
+ newCwd: string;
10791
+ previousSessionFile: string | undefined;
10792
+ }) => void | Promise<void>,
10793
+ ): () => void {
10794
+ this.#afterMoveListeners.add(listener);
10795
+ return () => {
10796
+ this.#afterMoveListeners.delete(listener);
10797
+ };
10798
+ }
10799
+
10800
+ registerBeforeMoveListener(
10801
+ listener: (move: {
10802
+ previousCwd: string;
10803
+ newCwd: string;
10804
+ previousSessionFile: string | undefined;
10805
+ newSessionFile: string | undefined;
10806
+ previousCwdIdentity: { dev: bigint; ino: bigint };
10807
+ newCwdIdentity: { dev: bigint; ino: bigint };
10808
+ preserveRecoveryJournal?: boolean;
10809
+ }) => void | Promise<void>,
10810
+ ): () => void {
10811
+ this.#beforeMoveListeners.add(listener);
10812
+ return () => {
10813
+ this.#beforeMoveListeners.delete(listener);
10814
+ };
10815
+ }
10816
+
10817
+ async #runBeforeMoveListeners(move: {
10818
+ previousCwd: string;
10819
+ newCwd: string;
10820
+ previousSessionFile: string | undefined;
10821
+ newSessionFile: string | undefined;
10822
+ previousCwdIdentity: { dev: bigint; ino: bigint };
10823
+ newCwdIdentity: { dev: bigint; ino: bigint };
10824
+ }): Promise<void> {
10825
+ for (const listener of [...this.#beforeMoveListeners]) await listener(move);
10826
+ }
10827
+
10828
+ registerMovePublicationListener(
10829
+ listener: (move: {
10830
+ previousCwd: string;
10831
+ newCwd: string;
10832
+ previousSessionFile: string | undefined;
10833
+ newSessionFile: string | undefined;
10834
+ previousCwdIdentity: { dev: bigint; ino: bigint };
10835
+ newCwdIdentity: { dev: bigint; ino: bigint };
10836
+ preserveRecoveryJournal?: boolean;
10837
+ }) => void | Promise<void>,
10838
+ ): () => void {
10839
+ this.#movePublicationListeners.add(listener);
10840
+ return () => {
10841
+ this.#movePublicationListeners.delete(listener);
10842
+ };
10843
+ }
10844
+
10845
+ async #runMovePublicationListeners(move: {
10846
+ previousCwd: string;
10847
+ newCwd: string;
10848
+ previousSessionFile: string | undefined;
10849
+ newSessionFile: string | undefined;
10850
+ previousCwdIdentity: { dev: bigint; ino: bigint };
10851
+ newCwdIdentity: { dev: bigint; ino: bigint };
10852
+ }): Promise<void> {
10853
+ for (const listener of [...this.#movePublicationListeners]) await listener(move);
10854
+ }
10855
+
10856
+ registerMoveAbortListener(
10857
+ listener: (move: {
10858
+ previousCwd: string;
10859
+ newCwd: string;
10860
+ previousSessionFile: string | undefined;
10861
+ newSessionFile: string | undefined;
10862
+ previousCwdIdentity: { dev: bigint; ino: bigint };
10863
+ newCwdIdentity: { dev: bigint; ino: bigint };
10864
+ preserveRecoveryJournal?: boolean;
10865
+ }) => void | Promise<void>,
10866
+ ): () => void {
10867
+ this.#moveAbortListeners.add(listener);
10868
+ return () => {
10869
+ this.#moveAbortListeners.delete(listener);
10870
+ };
10871
+ }
10872
+
10873
+ async #runMoveAbortListeners(move: {
10874
+ previousCwd: string;
10875
+ newCwd: string;
10876
+ previousSessionFile: string | undefined;
10877
+ newSessionFile: string | undefined;
10878
+ previousCwdIdentity: { dev: bigint; ino: bigint };
10879
+ newCwdIdentity: { dev: bigint; ino: bigint };
10880
+ preserveRecoveryJournal?: boolean;
10881
+ }): Promise<void> {
10882
+ for (const listener of [...this.#moveAbortListeners]) {
10883
+ try {
10884
+ await listener(move);
10885
+ } catch (error) {
10886
+ logger.warn("move-abort listener failed", { error: String(error), cwd: move.newCwd });
10887
+ }
10888
+ }
10889
+ }
10890
+
10891
+ async #runAfterMoveListeners(move: {
10892
+ previousCwd: string;
10893
+ newCwd: string;
10894
+ previousSessionFile: string | undefined;
10895
+ }): Promise<void> {
10896
+ for (const listener of [...this.#afterMoveListeners]) {
10897
+ try {
10898
+ await listener(move);
10899
+ } catch (error) {
10900
+ logger.warn("after-move listener failed", { error: String(error), cwd: move.newCwd });
10901
+ }
10902
+ }
10903
+ }
10904
+
10734
10905
  #ownsCwdTransition(): boolean {
10735
10906
  const owner = this.#cwdTransitionOwner;
10736
10907
  return owner !== undefined && cwdTransitionAls.getStore() === owner;
@@ -10801,15 +10972,73 @@ export class SessionManager {
10801
10972
  options?: {
10802
10973
  expectedIdentity?: { dev: bigint; ino: bigint };
10803
10974
  targetHandle?: { stat: (opts: { bigint: true }) => Promise<fs.BigIntStats> };
10975
+ expectedSourceIdentity?: { dev: bigint; ino: bigint };
10976
+ sourceHandle?: { stat: (opts: { bigint: true }) => Promise<fs.BigIntStats> };
10804
10977
  },
10805
10978
  ): Promise<void> {
10806
10979
  if (!this.#ownsCwdTransition()) {
10807
10980
  return this.runExclusiveCwdTransition(() => this.moveTo(newCwd, options));
10808
10981
  }
10809
- const resolvedCwd = path.resolve(newCwd);
10982
+ if (this.#cwdMoveAdmissionClosed) throw new Error("Session cwd move admission is closed.");
10983
+ if (!options?.sourceHandle) {
10984
+ const sourceHandle = await SessionManager.openNoFollowDirectory(this.cwd);
10985
+ try {
10986
+ const sourceIdentity = await sourceHandle.stat({ bigint: true });
10987
+ if (!sourceIdentity.isDirectory()) throw new Error(`Current cwd is not a directory: ${this.cwd}`);
10988
+ return await this.moveTo(newCwd, {
10989
+ ...options,
10990
+ expectedSourceIdentity: options?.expectedSourceIdentity ?? {
10991
+ dev: sourceIdentity.dev,
10992
+ ino: sourceIdentity.ino,
10993
+ },
10994
+ sourceHandle,
10995
+ });
10996
+ } finally {
10997
+ await sourceHandle.close();
10998
+ }
10999
+ }
11000
+ const resolvedCwd = await fs.promises.realpath(path.resolve(newCwd));
11001
+ const resolvedIdentity = await fs.promises.lstat(resolvedCwd, { bigint: true });
11002
+ if (!resolvedIdentity.isDirectory() || resolvedIdentity.isSymbolicLink()) {
11003
+ throw new Error(`Refusing to rescope: target is not a directory: ${resolvedCwd}`);
11004
+ }
11005
+ if (!options?.targetHandle) {
11006
+ const targetHandle = await SessionManager.openNoFollowDirectory(resolvedCwd);
11007
+ try {
11008
+ const targetIdentity = await targetHandle.stat({ bigint: true });
11009
+ const revalidatedCwd = await fs.promises.realpath(path.resolve(newCwd));
11010
+ const namedIdentity = await fs.promises.lstat(revalidatedCwd, { bigint: true });
11011
+ if (
11012
+ revalidatedCwd !== resolvedCwd ||
11013
+ !namedIdentity.isDirectory() ||
11014
+ namedIdentity.isSymbolicLink() ||
11015
+ namedIdentity.dev !== resolvedIdentity.dev ||
11016
+ namedIdentity.ino !== resolvedIdentity.ino ||
11017
+ targetIdentity.dev !== resolvedIdentity.dev ||
11018
+ targetIdentity.ino !== resolvedIdentity.ino
11019
+ )
11020
+ throw new Error(`Refusing to rescope: target identity changed: ${resolvedCwd}`);
11021
+ return await this.moveTo(resolvedCwd, {
11022
+ ...options,
11023
+ expectedIdentity: options?.expectedIdentity ?? {
11024
+ dev: targetIdentity.dev,
11025
+ ino: targetIdentity.ino,
11026
+ },
11027
+ targetHandle,
11028
+ });
11029
+ } finally {
11030
+ await targetHandle.close();
11031
+ }
11032
+ }
10810
11033
  if (options?.expectedIdentity || options?.targetHandle) {
10811
11034
  await this.#assertCwdTargetIdentity(resolvedCwd, options);
10812
11035
  }
11036
+ if (options?.expectedSourceIdentity || options?.sourceHandle) {
11037
+ await this.#assertCwdTargetIdentity(this.cwd, {
11038
+ expectedIdentity: options.expectedSourceIdentity,
11039
+ targetHandle: options.sourceHandle,
11040
+ });
11041
+ }
10813
11042
  if (resolvedCwd === this.cwd) return;
10814
11043
  const previousCwd = this.cwd;
10815
11044
  const previousSessionDir = this.sessionDir;
@@ -10825,6 +11054,19 @@ export class SessionManager {
10825
11054
  )
10826
11055
  : this.destination;
10827
11056
  const newSessionDir = nextDestination.directory;
11057
+ const nextSessionFile = this.#sessionFile
11058
+ ? path.join(newSessionDir, path.basename(this.#sessionFile))
11059
+ : undefined;
11060
+ const pinnedSourceIdentity = await options.sourceHandle.stat({ bigint: true });
11061
+ const pinnedTargetIdentity = await options.targetHandle.stat({ bigint: true });
11062
+ const moveDetails = {
11063
+ previousCwd,
11064
+ newCwd: resolvedCwd,
11065
+ previousSessionFile,
11066
+ newSessionFile: nextSessionFile,
11067
+ previousCwdIdentity: { dev: pinnedSourceIdentity.dev, ino: pinnedSourceIdentity.ino },
11068
+ newCwdIdentity: { dev: pinnedTargetIdentity.dev, ino: pinnedTargetIdentity.ino },
11069
+ };
10828
11070
  let hadSessionFile = false;
10829
11071
  const managedMove = this.destination.kind === "managed" && nextDestination.kind === "managed";
10830
11072
  let managedSourceStore: ManagedSessionDescendantStore | undefined;
@@ -10832,16 +11074,29 @@ export class SessionManager {
10832
11074
  let rollbackManagedMove: (() => Promise<void>) | undefined;
10833
11075
  let residentTransition: PreparedResidentStoreTransition | undefined;
10834
11076
  const hadPersistedSession = this.#ensuredOnDisk;
11077
+ try {
11078
+ await this.#runBeforeMoveListeners(moveDetails);
11079
+ } catch (error) {
11080
+ await this.#runMoveAbortListeners(moveDetails);
11081
+ throw error;
11082
+ }
10835
11083
 
11084
+ let targetIdentityRevalidated = false;
11085
+ let publicationStarted = false;
10836
11086
  if (this.persist && this.#sessionFile) {
10837
11087
  // Close the persist writer before moving files
10838
- await this.#closePersistWriter();
11088
+ try {
11089
+ await this.#closePersistWriter();
11090
+ } catch (error) {
11091
+ await this.#runMoveAbortListeners(moveDetails);
11092
+ throw error;
11093
+ }
10839
11094
  this.#persistChain = Promise.resolve();
10840
11095
  this.#persistError = undefined;
10841
11096
  this.#persistErrorReported = false;
10842
11097
 
10843
11098
  const oldSessionFile = this.#sessionFile;
10844
- const newSessionFile = path.join(newSessionDir, path.basename(oldSessionFile));
11099
+ const newSessionFile = nextSessionFile!;
10845
11100
  const oldArtifactDir = oldSessionFile.slice(0, -6); // strip .jsonl
10846
11101
  const newArtifactDir = newSessionFile.slice(0, -6);
10847
11102
  let managedTranscript!: ReturnType<ManagedSessionDescendantStore["readExpected"]>;
@@ -10851,24 +11106,30 @@ export class SessionManager {
10851
11106
  hadSessionFile = managedMove ? false : this.#storage.existsSync(oldSessionFile);
10852
11107
  let movedSessionFile = false;
10853
11108
  let movedArtifactDir = false;
10854
- const materializedEntries = materializeResidentEntriesForReadSync(
10855
- this.#fileEntries,
10856
- this.#residentBlobStores(),
10857
- );
10858
- const transitionEntries: FileEntry[] = materializedEntries.map(entry =>
10859
- entry.type === "session" ? { ...entry, cwd: resolvedCwd } : entry,
10860
- );
10861
- residentTransition = this.#prepareResidentTextStoreTransition(
10862
- {
10863
- target: { sessionId: this.#sessionId, sessionFile: newSessionFile },
10864
- primary: {
10865
- mode: "materialize",
10866
- sourceEntries: transitionEntries,
10867
- sourceStores: { textStore: null, imageStore: this.#residentImageBlobStore },
11109
+ try {
11110
+ const materializedEntries = materializeResidentEntriesForReadSync(
11111
+ this.#fileEntries,
11112
+ this.#residentBlobStores(),
11113
+ );
11114
+ const transitionEntries: FileEntry[] = materializedEntries.map(entry =>
11115
+ entry.type === "session" ? { ...entry, cwd: resolvedCwd } : entry,
11116
+ );
11117
+ residentTransition = this.#prepareResidentTextStoreTransition(
11118
+ {
11119
+ target: { sessionId: this.#sessionId, sessionFile: newSessionFile },
11120
+ primary: {
11121
+ mode: "materialize",
11122
+ sourceEntries: transitionEntries,
11123
+ sourceStores: { textStore: null, imageStore: this.#residentImageBlobStore },
11124
+ },
10868
11125
  },
10869
- },
10870
- "retain-and-throw",
10871
- );
11126
+ "retain-and-throw",
11127
+ );
11128
+ } catch (error) {
11129
+ residentTransition?.dispose();
11130
+ await this.#runMoveAbortListeners(moveDetails);
11131
+ throw error;
11132
+ }
10872
11133
  const discardResidentTransitionAndThrow = (error: unknown): never => {
10873
11134
  residentTransition?.dispose();
10874
11135
  throw error;
@@ -10911,6 +11172,14 @@ export class SessionManager {
10911
11172
  throw new Error("artifact_destination_mismatch");
10912
11173
  destination.fsyncTree();
10913
11174
  };
11175
+ try {
11176
+ await this.#runMovePublicationListeners(moveDetails);
11177
+ publicationStarted = true;
11178
+ } catch (error) {
11179
+ residentTransition?.dispose();
11180
+ await this.#runMoveAbortListeners(moveDetails);
11181
+ throw error;
11182
+ }
10914
11183
  try {
10915
11184
  if (managedMove) {
10916
11185
  if (previousDestination.kind !== "managed" || nextDestination.kind !== "managed")
@@ -10996,6 +11265,16 @@ export class SessionManager {
10996
11265
  if (!isEnoent(err)) throw err;
10997
11266
  }
10998
11267
  }
11268
+ if (options?.expectedIdentity || options?.targetHandle) {
11269
+ if (options.expectedSourceIdentity || options.sourceHandle) {
11270
+ await this.#assertCwdTargetIdentity(previousCwd, {
11271
+ expectedIdentity: options.expectedSourceIdentity,
11272
+ targetHandle: options.sourceHandle,
11273
+ });
11274
+ }
11275
+ await this.#assertCwdTargetIdentity(resolvedCwd, options);
11276
+ targetIdentityRevalidated = true;
11277
+ }
10999
11278
  } catch (err) {
11000
11279
  if (managedMove && managedDestinationStore) {
11001
11280
  try {
@@ -11037,6 +11316,7 @@ export class SessionManager {
11037
11316
  )
11038
11317
  await managedSourceStore.publishNoReplace(path.basename(oldSessionFile), managedTranscript.bytes);
11039
11318
  } catch (rollbackErr) {
11319
+ await this.#runMoveAbortListeners({ ...moveDetails, preserveRecoveryJournal: true });
11040
11320
  discardResidentTransitionAndThrow(
11041
11321
  new Error(`Failed to rollback managed move: ${toError(rollbackErr).message}`, {
11042
11322
  cause: toError(err),
@@ -11044,18 +11324,46 @@ export class SessionManager {
11044
11324
  );
11045
11325
  }
11046
11326
  } else {
11047
- if (movedArtifactDir) await movePathAcrossDevicesSafe(newArtifactDir, oldArtifactDir);
11048
- if (movedSessionFile) await movePathAcrossDevicesSafe(newSessionFile, oldSessionFile);
11327
+ try {
11328
+ if (movedArtifactDir) await movePathAcrossDevicesSafe(newArtifactDir, oldArtifactDir);
11329
+ if (movedSessionFile) await movePathAcrossDevicesSafe(newSessionFile, oldSessionFile);
11330
+ } catch (rollbackError) {
11331
+ await this.#runMoveAbortListeners({ ...moveDetails, preserveRecoveryJournal: true });
11332
+ discardResidentTransitionAndThrow(
11333
+ new Error(`Failed to rollback move: ${toError(rollbackError).message}`, { cause: toError(err) }),
11334
+ );
11335
+ }
11049
11336
  }
11337
+ await this.#runMoveAbortListeners(moveDetails);
11050
11338
  discardResidentTransitionAndThrow(err);
11051
11339
  }
11052
11340
  this.#sessionFile = newSessionFile;
11053
11341
  }
11342
+ if (!publicationStarted) {
11343
+ try {
11344
+ await this.#runMovePublicationListeners(moveDetails);
11345
+ publicationStarted = true;
11346
+ } catch (error) {
11347
+ await this.#runMoveAbortListeners(moveDetails);
11348
+ throw error;
11349
+ }
11350
+ }
11054
11351
 
11055
11352
  // Update cwd and sessionDir after physical publication succeeds. Metadata failures restore the source
11056
11353
  // authority but deliberately retain any destination publication evidence rather than deleting it.
11057
- if (options?.expectedIdentity || options?.targetHandle) {
11058
- await this.#assertCwdTargetIdentity(resolvedCwd, options);
11354
+ if (!targetIdentityRevalidated && (options?.expectedIdentity || options?.targetHandle)) {
11355
+ try {
11356
+ if (options.expectedSourceIdentity || options.sourceHandle) {
11357
+ await this.#assertCwdTargetIdentity(previousCwd, {
11358
+ expectedIdentity: options.expectedSourceIdentity,
11359
+ targetHandle: options.sourceHandle,
11360
+ });
11361
+ }
11362
+ await this.#assertCwdTargetIdentity(resolvedCwd, options);
11363
+ } catch (error) {
11364
+ await this.#runMoveAbortListeners(moveDetails);
11365
+ throw error;
11366
+ }
11059
11367
  }
11060
11368
  this.#cwdGeneration += 1;
11061
11369
  this.cwd = resolvedCwd;
@@ -11095,6 +11403,7 @@ export class SessionManager {
11095
11403
  await rollbackManagedMove();
11096
11404
  } catch (rollbackError) {
11097
11405
  if (toError(rollbackError).message !== "cleanup_pending") {
11406
+ await this.#runMoveAbortListeners({ ...moveDetails, preserveRecoveryJournal: true });
11098
11407
  throw new Error(`Failed to rollback managed move: ${toError(rollbackError).message}`, {
11099
11408
  cause: toError(error),
11100
11409
  });
@@ -11115,12 +11424,14 @@ export class SessionManager {
11115
11424
  const header = this.#fileEntries.find(entry => entry.type === "session") as SessionHeader | undefined;
11116
11425
  if (header) applyHeaderPatch(header, { cwd: previousCwd });
11117
11426
  this.#headerExportRevision++;
11427
+ await this.#runMoveAbortListeners(moveDetails);
11118
11428
  throw error;
11119
11429
  }
11120
11430
  // The destination has already been published. Retain it rather than moving it
11121
11431
  // back over the source after a metadata failure.
11122
11432
  this.#managedTranscriptStoreCache = null;
11123
11433
  this.#headerExportRevision++;
11434
+ await this.#runAfterMoveListeners(moveDetails);
11124
11435
  throw error;
11125
11436
  }
11126
11437
  if (residentTransition) this.#commitResidentTextStoreTransition(residentTransition);
@@ -11129,6 +11440,9 @@ export class SessionManager {
11129
11440
  if (this.#sessionFile) {
11130
11441
  writeTerminalBreadcrumb(resolvedCwd, this.#sessionFile);
11131
11442
  }
11443
+ // The move is fully committed here; notify every surface's after-move listeners
11444
+ // (coordinator runtime-state relocation, postmortem finalizer rebinding, ...).
11445
+ await this.#runAfterMoveListeners(moveDetails);
11132
11446
  }
11133
11447
 
11134
11448
  /** Sync version for initial creation (no existing writer to close). */
@@ -18,8 +18,9 @@ export interface YieldDispatcher<P> {
18
18
  export interface YieldQueueOptions {
19
19
  isStreaming: () => boolean;
20
20
  injectStreaming(msg: AgentMessage): void;
21
- injectIdle(messages: AgentMessage[]): Promise<void>;
22
- scheduleIdleFlush(run: () => Promise<void>): void;
21
+ injectIdle(messages: AgentMessage[], signal?: AbortSignal): Promise<void>;
22
+ scheduleIdleFlush(run: (signal?: AbortSignal) => Promise<void>, onSkip: () => void): void;
23
+ getIdleFlushSignal?(): AbortSignal | undefined;
23
24
  }
24
25
 
25
26
  type YieldFlushMode = "streaming" | "idle";
@@ -39,6 +40,7 @@ export class YieldQueue {
39
40
  readonly #dispatchers = new Map<string, StoredDispatcher>();
40
41
  readonly #entries = new Map<string, unknown[]>();
41
42
  #idleFlushPending = false;
43
+ #idleFlushPendingOwner: symbol | undefined;
42
44
 
43
45
  constructor(options: YieldQueueOptions) {
44
46
  this.#options = options;
@@ -82,9 +84,10 @@ export class YieldQueue {
82
84
  return false;
83
85
  }
84
86
 
85
- async flush(mode: YieldFlushMode): Promise<void> {
87
+ async flush(mode: YieldFlushMode, signal?: AbortSignal): Promise<void> {
86
88
  if (mode === "idle") {
87
89
  this.#idleFlushPending = false;
90
+ this.#idleFlushPendingOwner = undefined;
88
91
  }
89
92
  const idleMessages: AgentMessage[] = [];
90
93
  for (const [kind, dispatcher] of this.#dispatchers) {
@@ -105,16 +108,20 @@ export class YieldQueue {
105
108
  }
106
109
  if (mode === "idle" && idleMessages.length > 0) {
107
110
  try {
108
- await this.#options.injectIdle(idleMessages);
111
+ await this.#options.injectIdle(idleMessages, signal ?? this.#options.getIdleFlushSignal?.());
109
112
  } catch (error) {
110
113
  logger.warn("Yield queue idle dispatch failed", { error: formatError(error) });
111
114
  }
112
115
  }
113
116
  }
114
117
 
115
- clear(): void {
118
+ clear(onDrop?: (kind: string, entries: readonly unknown[]) => void): void {
119
+ if (onDrop) {
120
+ for (const [kind, entries] of this.#entries) onDrop(kind, entries);
121
+ }
116
122
  this.#entries.clear();
117
123
  this.#idleFlushPending = false;
124
+ this.#idleFlushPendingOwner = undefined;
118
125
  }
119
126
 
120
127
  /** Drop only the queued entries of a single kind, leaving other kinds intact. */
@@ -140,14 +147,21 @@ export class YieldQueue {
140
147
  #scheduleIdleFlush(): void {
141
148
  if (this.#idleFlushPending) return;
142
149
  this.#idleFlushPending = true;
150
+ const owner = Symbol("idle-flush");
151
+ this.#idleFlushPendingOwner = owner;
152
+ const releaseOwner = () => {
153
+ if (this.#idleFlushPendingOwner !== owner) return;
154
+ this.#idleFlushPendingOwner = undefined;
155
+ this.#idleFlushPending = false;
156
+ };
143
157
  try {
144
- this.#options.scheduleIdleFlush(async () => {
145
- this.#idleFlushPending = false;
158
+ this.#options.scheduleIdleFlush(async signal => {
159
+ releaseOwner();
146
160
  if (this.#options.isStreaming()) return;
147
- await this.flush("idle");
148
- });
161
+ await this.flush("idle", signal);
162
+ }, releaseOwner);
149
163
  } catch (error) {
150
- this.#idleFlushPending = false;
164
+ releaseOwner();
151
165
  logger.warn("Yield queue idle flush scheduling failed", { error: formatError(error) });
152
166
  }
153
167
  }
@@ -748,6 +748,7 @@ export function shouldOfferOnboarding(state: FrictionlessOnboardingState): boole
748
748
 
749
749
  export interface AutomaticOnboardingInput {
750
750
  normalInteractive: boolean;
751
+ automation?: boolean;
751
752
  initialMessage?: string;
752
753
  initialMessages: readonly string[];
753
754
  initialImages?: readonly unknown[];
@@ -757,6 +758,7 @@ export interface AutomaticOnboardingInput {
757
758
  export function shouldOfferAutomaticOnboarding(input: AutomaticOnboardingInput): boolean {
758
759
  return (
759
760
  input.normalInteractive &&
761
+ !input.automation &&
760
762
  !input.initialMessage &&
761
763
  input.initialMessages.length === 0 &&
762
764
  (input.initialImages?.length ?? 0) === 0 &&
@@ -4,7 +4,7 @@
4
4
  {"id":"glm","aliases":["zai","z-ai","bigmodel"],"name":"GLM / zAI","description":"OpenAI-compatible GLM endpoint from zAI/BigModel","compatibility":"openai","api":"openai-completions","providerId":"glm-proxy","baseUrl":"https://api.z.ai/api/paas/v4","apiKeyEnv":"ZAI_API_KEY","models":["glm-4.6"],"compat":{"supportsDeveloperRole":false,"supportsReasoningEffort":false,"thinkingFormat":"zai","reasoningContentField":"reasoning_content"}},
5
5
  {"id":"alibaba-token-plan","aliases":["alibaba","token-plan"],"name":"Alibaba Token Plan","description":"OpenAI-compatible Alibaba Token Plan endpoint","compatibility":"openai","api":"openai-completions","providerId":"alibaba-token-plan","baseUrl":"https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1","apiKeyEnv":"ALIBABA_TOKEN_PLAN_API_KEY","models":["qwen3.8-max-preview","qwen3.8-max","glm-5.2","deepseek-v4-pro"],"modelApi":{"qwen3.8-max-preview":"openai-responses","qwen3.8-max":"openai-responses","glm-5.2":"openai-completions","deepseek-v4-pro":"openai-completions"},"compat":{"supportsDeveloperRole":false}},
6
6
  {"id":"cline-pass","aliases":["clinepass","cline"],"name":"ClinePass","description":"ClinePass subscription models discovered from Cline's upstream models.dev catalog","compatibility":"openai","api":"openai-completions","providerId":"cline-pass","baseUrl":"https://api.cline.bot/api/v1","apiKeyEnv":"CLINE_API_KEY","discovery":{"type":"models-dev","modelsDevProvider":"cline-pass"},"compat":{"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"reasoningContentField":"reasoning"}},
7
- {"id":"commandcode-goat","aliases":["commandcode","command-code","goat"],"name":"Command Code GOAT","description":"Command Code GOAT coding-plan models discovered from its OpenAI-compatible Provider API","compatibility":"openai","api":"openai-completions","providerId":"commandcode-goat","baseUrl":"https://api.commandcode.ai/provider/v1","apiKeyEnv":"CMD_API_KEY","discovery":{"type":"openai-models-list","apiByModelPrefix":{"claude-":"anthropic-messages"}},"compat":{"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"reasoningContentField":"reasoning_content"}},
7
+ {"id":"commandcode-goat","aliases":["commandcode","command-code","goat"],"name":"Command Code GOAT","description":"Command Code GOAT coding-plan models discovered from its OpenAI-compatible Provider API","compatibility":"openai","api":"openai-completions","providerId":"commandcode-goat","baseUrl":"https://api.commandcode.ai/provider/v1","apiKeyEnv":"CMD_API_KEY","discovery":{"type":"openai-models-list","apiByModelPrefix":{"claude":"anthropic-messages","anthropic/claude":"anthropic-messages"}},"compat":{"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","reasoningContentField":"reasoning_content"}},
8
8
  {"id":"litellm","aliases":["litellm-proxy"],"name":"LiteLLM Proxy","description":"OpenAI-compatible LiteLLM proxy endpoint (user-supplied base URL) with live model discovery","compatibility":"openai","api":"openai-completions","providerId":"litellm","parameterized":true,"apiKeyEnv":"LITELLM_API_KEY","discovery":{"type":"openai-models-list"}},
9
9
  {"id":"openai-compatible-proxy","aliases":["openai-proxy","compatible-proxy","custom-proxy"],"name":"OpenAI-Compatible Proxy","description":"Generic OpenAI-compatible gateway (OpenRouter, OpenCode Go, custom) with live model discovery","compatibility":"openai","api":"openai-completions","providerId":"openai-compatible-proxy","parameterized":true,"apiKeyEnv":"OPENAI_COMPATIBLE_API_KEY","discovery":{"type":"openai-models-list"}}
10
10
  ]
@@ -131,7 +131,12 @@ export async function switchSessionCredentialCommand(runtime: SlashCommandRuntim
131
131
  }
132
132
 
133
133
  try {
134
- authStorage.switchSessionCredential(provider, runtime.session.credentialSessionId, parsed.selector);
134
+ authStorage.switchSessionCredential(
135
+ provider,
136
+ runtime.session.credentialSessionId,
137
+ parsed.selector,
138
+ runtime.session.modelRegistry.getAuthStorageOwner?.(),
139
+ );
135
140
  } catch (error) {
136
141
  return error instanceof Error ? error.message : String(error);
137
142
  }
@@ -1121,6 +1121,8 @@ export async function runSubprocessOnce(options: ExecutorOptions): Promise<Singl
1121
1121
  const abortController = new AbortController();
1122
1122
  const abortSignal = abortController.signal;
1123
1123
  let activeSession: AgentSession | null = null;
1124
+ let ownedModelRegistry: ModelRegistry | undefined;
1125
+ let ownedAuthStorage: AuthStorage | undefined;
1124
1126
  let unsubscribe: (() => void) | null = null;
1125
1127
  let yieldCalled = false;
1126
1128
  let pauseRequested = false;
@@ -1703,9 +1705,19 @@ export async function runSubprocessOnce(options: ExecutorOptions): Promise<Singl
1703
1705
  checkAbort();
1704
1706
  // Pin authStorage to modelRegistry.authStorage — mirrors the createAgentSession invariant.
1705
1707
  const registryFromParent = options.modelRegistry !== undefined;
1706
- const modelRegistry =
1707
- options.modelRegistry ??
1708
- new ModelRegistry(options.authStorage ?? (await awaitAbortable(discoverAuthStorage())));
1708
+ const registryAuthStorage =
1709
+ options.authStorage ??
1710
+ options.modelRegistry?.authStorage ??
1711
+ (await awaitAbortable(discoverAuthStorage(settings.getAgentDir())));
1712
+ if (options.modelRegistry === undefined && options.authStorage === undefined)
1713
+ ownedAuthStorage = registryAuthStorage;
1714
+ ownedModelRegistry = options.modelRegistry
1715
+ ? undefined
1716
+ : new ModelRegistry(registryAuthStorage, path.join(settings.getAgentDir(), "models.yml"), settings, {
1717
+ agentDir: settings.getAgentDir(),
1718
+ automaticRefresh: false,
1719
+ });
1720
+ const modelRegistry = options.modelRegistry ?? ownedModelRegistry!;
1709
1721
  const authStorage = modelRegistry.authStorage;
1710
1722
  if (options.authStorage && options.authStorage !== authStorage) {
1711
1723
  throw new Error(
@@ -2500,6 +2512,10 @@ export async function runSubprocessOnce(options: ExecutorOptions): Promise<Singl
2500
2512
  // Ignore cleanup errors
2501
2513
  }
2502
2514
  }
2515
+ ownedModelRegistry?.dispose();
2516
+ ownedModelRegistry = undefined;
2517
+ ownedAuthStorage?.close();
2518
+ ownedAuthStorage = undefined;
2503
2519
  }
2504
2520
 
2505
2521
  return {