@gajae-code/coding-agent 0.15.6 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/dist/types/async/job-manager.d.ts +32 -1
  3. package/dist/types/browser-backend.d.ts +12 -0
  4. package/dist/types/cli/args.d.ts +2 -0
  5. package/dist/types/cli/root-flags.d.ts +14 -0
  6. package/dist/types/cli/update-cli.d.ts +14 -0
  7. package/dist/types/cli.d.ts +7 -0
  8. package/dist/types/commands/launch.d.ts +10 -0
  9. package/dist/types/commands/sdk.d.ts +2 -0
  10. package/dist/types/config/autorouting-tier-map.d.ts +24 -0
  11. package/dist/types/config/billing-path.d.ts +25 -0
  12. package/dist/types/config/config-schema-version.d.ts +13 -1
  13. package/dist/types/config/file-lock.d.ts +5 -0
  14. package/dist/types/config/model-registry.d.ts +22 -0
  15. package/dist/types/config/settings-schema.d.ts +27 -8
  16. package/dist/types/config/settings.d.ts +2 -0
  17. package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
  18. package/dist/types/crash/record-loader.d.ts +15 -0
  19. package/dist/types/extensibility/extensions/types.d.ts +8 -3
  20. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  21. package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
  22. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  23. package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
  24. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
  25. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  26. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  27. package/dist/types/main.d.ts +1 -0
  28. package/dist/types/master-mode/context.d.ts +21 -0
  29. package/dist/types/master-mode/first-request.d.ts +34 -0
  30. package/dist/types/master-mode/snapshot.d.ts +32 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +21 -21
  32. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  33. package/dist/types/modes/components/model-selector.d.ts +4 -0
  34. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  35. package/dist/types/modes/components/session-selector.d.ts +1 -1
  36. package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
  37. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  38. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  39. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  40. package/dist/types/modes/interactive-mode.d.ts +16 -2
  41. package/dist/types/modes/print-mode.d.ts +17 -0
  42. package/dist/types/modes/theme/theme.d.ts +2 -1
  43. package/dist/types/modes/types.d.ts +2 -0
  44. package/dist/types/modes/ui-language.d.ts +7 -1
  45. package/dist/types/sdk/broker/broker.d.ts +79 -2
  46. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  47. package/dist/types/sdk/broker/ensure.d.ts +3 -0
  48. package/dist/types/sdk/broker/index.d.ts +3 -0
  49. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
  50. package/dist/types/sdk/broker/lifecycle.d.ts +40 -0
  51. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  52. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  53. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  54. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  55. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  56. package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
  57. package/dist/types/sdk/broker/state-version.d.ts +21 -1
  58. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  59. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  60. package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
  61. package/dist/types/sdk/bus/index.d.ts +9 -1
  62. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
  63. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
  64. package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
  65. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  66. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  67. package/dist/types/sdk/cli/index.d.ts +1 -0
  68. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  69. package/dist/types/sdk/cli/rows.d.ts +12 -5
  70. package/dist/types/sdk/cli/session-cli.d.ts +20 -3
  71. package/dist/types/sdk/client/client.d.ts +3 -2
  72. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  73. package/dist/types/sdk/host/control/index.d.ts +1 -1
  74. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  75. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  76. package/dist/types/sdk/host/events.d.ts +1 -0
  77. package/dist/types/sdk/host/host.d.ts +10 -0
  78. package/dist/types/sdk/host/query/cursor.d.ts +4 -2
  79. package/dist/types/sdk/host/query/handlers.d.ts +1 -0
  80. package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
  81. package/dist/types/sdk/host/session-runtime.d.ts +70 -17
  82. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  83. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  84. package/dist/types/sdk/lifecycle-session.d.ts +3 -1
  85. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
  86. package/dist/types/sdk/receipt-state.d.ts +2 -0
  87. package/dist/types/sdk/router/session-router.d.ts +6 -0
  88. package/dist/types/sdk/session.d.ts +16 -0
  89. package/dist/types/sdk/startup-capability.d.ts +2 -1
  90. package/dist/types/sdk/turn-result.d.ts +6 -0
  91. package/dist/types/session/agent-session.d.ts +52 -7
  92. package/dist/types/session/credential-store-errors.d.ts +1 -0
  93. package/dist/types/session/fold-coordinator.d.ts +29 -2
  94. package/dist/types/session/session-manager.d.ts +79 -2
  95. package/dist/types/session/yield-queue.d.ts +5 -4
  96. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  97. package/dist/types/tools/ask.d.ts +1 -0
  98. package/dist/types/tools/bash.d.ts +16 -0
  99. package/dist/types/tools/browser/launch.d.ts +10 -0
  100. package/dist/types/tools/browser/registry.d.ts +8 -0
  101. package/dist/types/tools/gh.d.ts +8 -0
  102. package/dist/types/tools/index.d.ts +10 -10
  103. package/dist/types/tools/job.d.ts +3 -1
  104. package/dist/types/utils/external-editor.d.ts +7 -0
  105. package/examples/extensions/README.md +1 -1
  106. package/examples/extensions/pirate.ts +7 -4
  107. package/package.json +7 -7
  108. package/scripts/generate-sdk-operation-inventory.ts +13 -1
  109. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  110. package/src/async/job-manager.ts +107 -9
  111. package/src/browser-backend.ts +29 -0
  112. package/src/cli/accounts-cli.ts +9 -5
  113. package/src/cli/args.ts +17 -0
  114. package/src/cli/fast-help.ts +1 -0
  115. package/src/cli/plugin-cli.ts +29 -0
  116. package/src/cli/root-flags.ts +5 -0
  117. package/src/cli/update-cli.ts +140 -23
  118. package/src/commands/launch.ts +86 -18
  119. package/src/commands/sdk.ts +135 -28
  120. package/src/config/autorouting-tier-map.ts +12 -0
  121. package/src/config/billing-path.ts +42 -0
  122. package/src/config/config-schema-version.ts +18 -1
  123. package/src/config/file-lock.ts +158 -83
  124. package/src/config/model-preset-registry.ts +178 -23
  125. package/src/config/model-profiles.ts +5 -5
  126. package/src/config/model-registry.ts +291 -42
  127. package/src/config/model-resolver.ts +5 -0
  128. package/src/config/settings-schema.ts +27 -8
  129. package/src/config/settings.ts +51 -15
  130. package/src/coordinator-mcp/projection-scan.ts +27 -6
  131. package/src/coordinator-mcp/question-state.ts +9 -11
  132. package/src/coordinator-mcp/server.ts +115 -21
  133. package/src/crash/index-store.ts +2 -0
  134. package/src/crash/record-loader.ts +10 -0
  135. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  136. package/src/extensibility/extensions/runner.ts +27 -0
  137. package/src/extensibility/extensions/types.ts +8 -3
  138. package/src/extensibility/gjc-plugins/installer.ts +51 -11
  139. package/src/extensibility/runtime-skill-discovery.ts +110 -3
  140. package/src/extensibility/skills.ts +22 -12
  141. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  142. package/src/gjc-runtime/launch-worktree.ts +285 -4
  143. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  144. package/src/gjc-runtime/session-state-lock.ts +213 -14
  145. package/src/gjc-runtime/session-state-sidecar.ts +938 -39
  146. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  147. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -3
  148. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  149. package/src/hooks/skill-state.ts +6 -0
  150. package/src/internal-urls/docs-index.generated.ts +15 -15
  151. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  152. package/src/main.ts +61 -3
  153. package/src/master-mode/context.ts +50 -0
  154. package/src/master-mode/first-request.ts +65 -0
  155. package/src/master-mode/snapshot.ts +205 -0
  156. package/src/modes/acp/acp-agent.ts +610 -161
  157. package/src/modes/components/footer.ts +4 -2
  158. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  159. package/src/modes/components/model-selector.ts +76 -8
  160. package/src/modes/components/oauth-selector.ts +6 -1
  161. package/src/modes/components/session-selector.ts +23 -15
  162. package/src/modes/components/sessions-dashboard.ts +8 -5
  163. package/src/modes/components/settings-selector.ts +1 -0
  164. package/src/modes/components/status-line/model-name.ts +53 -0
  165. package/src/modes/components/status-line/priority-row.ts +107 -0
  166. package/src/modes/components/status-line/segments.ts +23 -9
  167. package/src/modes/components/tool-status-header.ts +151 -15
  168. package/src/modes/controllers/command-controller.ts +1 -1
  169. package/src/modes/controllers/event-controller.ts +30 -4
  170. package/src/modes/controllers/extension-ui-controller.ts +13 -13
  171. package/src/modes/controllers/input-controller.ts +30 -2
  172. package/src/modes/controllers/selector-controller.ts +200 -98
  173. package/src/modes/interactive-mode.ts +36 -12
  174. package/src/modes/print-mode.ts +64 -0
  175. package/src/modes/prompt-action-autocomplete.ts +30 -4
  176. package/src/modes/runtime-init.ts +12 -9
  177. package/src/modes/theme/theme.ts +5 -0
  178. package/src/modes/types.ts +2 -0
  179. package/src/modes/ui-language.ts +49 -2
  180. package/src/modes/utils/ui-helpers.ts +22 -3
  181. package/src/prompts/agent-fragments/browser-aside.md +22 -0
  182. package/src/prompts/system/master-mode.md +28 -0
  183. package/src/prompts/tools/ask.md +1 -1
  184. package/src/prompts/tools/bash.md +1 -0
  185. package/src/prompts/tools/job.md +1 -1
  186. package/src/sdk/acp/adapter.ts +5 -0
  187. package/src/sdk/broker/broker.ts +1646 -27
  188. package/src/sdk/broker/endpoint-authority.ts +86 -0
  189. package/src/sdk/broker/ensure.ts +81 -3
  190. package/src/sdk/broker/index.ts +3 -0
  191. package/src/sdk/broker/lifecycle-ledger.ts +6 -0
  192. package/src/sdk/broker/lifecycle.ts +440 -75
  193. package/src/sdk/broker/master-capability.ts +232 -0
  194. package/src/sdk/broker/session-index.ts +121 -17
  195. package/src/sdk/broker/session-scope.ts +272 -0
  196. package/src/sdk/broker/spawn-authority.ts +1031 -0
  197. package/src/sdk/broker/spawn-substrate.ts +591 -0
  198. package/src/sdk/broker/startup-budget.ts +144 -2
  199. package/src/sdk/broker/state-version.ts +43 -14
  200. package/src/sdk/broker/transport.ts +1 -0
  201. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  202. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  203. package/src/sdk/bus/chat-daemon-runtime.ts +136 -18
  204. package/src/sdk/bus/discord-daemon.ts +50 -10
  205. package/src/sdk/bus/index.ts +84 -7
  206. package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
  207. package/src/sdk/bus/lifecycle-commands.ts +4 -0
  208. package/src/sdk/bus/notification-service.ts +22 -5
  209. package/src/sdk/bus/prompt-reconciliation.ts +7 -2
  210. package/src/sdk/bus/reconciliation-store.ts +55 -31
  211. package/src/sdk/bus/slack-conversation.ts +1 -1
  212. package/src/sdk/bus/slack-daemon.ts +72 -38
  213. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  214. package/src/sdk/cli/index.ts +1 -0
  215. package/src/sdk/cli/master-cli.ts +259 -0
  216. package/src/sdk/cli/rows.ts +41 -15
  217. package/src/sdk/cli/session-cli.ts +297 -26
  218. package/src/sdk/client/client.ts +14 -8
  219. package/src/sdk/host/control/dispatch.ts +101 -40
  220. package/src/sdk/host/control/index.ts +2 -0
  221. package/src/sdk/host/control/operations.ts +2 -0
  222. package/src/sdk/host/control/runtime-gate.ts +203 -11
  223. package/src/sdk/host/events.ts +1 -0
  224. package/src/sdk/host/host.ts +29 -2
  225. package/src/sdk/host/query/cursor.ts +52 -7
  226. package/src/sdk/host/query/handlers.ts +33 -12
  227. package/src/sdk/host/query/revision-store.ts +12 -0
  228. package/src/sdk/host/session-runtime.test.ts +673 -14
  229. package/src/sdk/host/session-runtime.ts +674 -103
  230. package/src/sdk/host/surface-policy.ts +3 -3
  231. package/src/sdk/lifecycle/broker-client.ts +28 -0
  232. package/src/sdk/lifecycle/service.ts +279 -20
  233. package/src/sdk/lifecycle-session.ts +17 -4
  234. package/src/sdk/prompt-deadline-manager.ts +2 -1
  235. package/src/sdk/protocol/operation-inventory.generated.json +105 -3
  236. package/src/sdk/protocol/operation-registry.ts +14 -4
  237. package/src/sdk/receipt-state.ts +10 -0
  238. package/src/sdk/router/session-router.ts +219 -118
  239. package/src/sdk/session.ts +416 -195
  240. package/src/sdk/startup-capability.ts +1 -0
  241. package/src/sdk/turn-result.ts +22 -0
  242. package/src/session/agent-session.ts +1628 -318
  243. package/src/session/blob-store.ts +15 -5
  244. package/src/session/credential-store-errors.ts +2 -0
  245. package/src/session/fold-coordinator.ts +40 -3
  246. package/src/session/internal/managed-session-storage.ts +153 -78
  247. package/src/session/session-manager.ts +850 -97
  248. package/src/session/yield-queue.ts +24 -10
  249. package/src/setup/frictionless-onboarding.ts +2 -0
  250. package/src/slash-commands/builtin-registry.ts +41 -0
  251. package/src/task/executor.ts +9 -4
  252. package/src/telemetry/events.ts +1138 -12
  253. package/src/tools/ask-contract.test.ts +64 -0
  254. package/src/tools/ask-contract.ts +36 -1
  255. package/src/tools/ask.ts +10 -8
  256. package/src/tools/bash-interactive.ts +11 -2
  257. package/src/tools/bash.ts +412 -75
  258. package/src/tools/browser/launch.ts +37 -0
  259. package/src/tools/browser/registry.ts +99 -9
  260. package/src/tools/cron.ts +6 -4
  261. package/src/tools/descriptors.test.ts +15 -0
  262. package/src/tools/descriptors.ts +5 -1
  263. package/src/tools/gh.ts +104 -48
  264. package/src/tools/github-cache.ts +2 -2
  265. package/src/tools/index.ts +10 -6
  266. package/src/tools/job.ts +4 -1
  267. package/src/tools/monitor.ts +12 -9
  268. package/src/tools/resolve.ts +30 -10
  269. package/src/tools/tool-catalog.generated.ts +2 -2
  270. package/src/utils/external-editor.ts +72 -1
@@ -70,6 +70,7 @@ import {
70
70
  type GroupPrefix,
71
71
  type GroupTypeMap,
72
72
  getDefault,
73
+ normalizeConfigSchemaVersion,
73
74
  reconcileSettingsSchema,
74
75
  SETTINGS_SCHEMA,
75
76
  type SettingPath,
@@ -748,6 +749,11 @@ export class Settings implements NotificationSettingsReader {
748
749
  return !this.#persist || !this.#hasRecoveredConfigSyntax;
749
750
  }
750
751
 
752
+ /** Whether this instance can publish a mutation to a durable config file. */
753
+ canPersistDurableConfig(): boolean {
754
+ return this.#persist && !this.#hasRecoveredConfigSyntax;
755
+ }
756
+
751
757
  /**
752
758
  * Set a setting value (sync).
753
759
  * Updates global settings and reserves its background persistence slot before
@@ -1520,15 +1526,19 @@ export class Settings implements NotificationSettingsReader {
1520
1526
  this.#hasInvalidNotificationGlobal = true;
1521
1527
  }
1522
1528
  }
1529
+ // ONE normalized marker feeds every version decision (future-schema,
1530
+ // migration-pending, and the ordered registry in #migrateRawSettings): a
1531
+ // malformed value must never be read as a future schema here — which would
1532
+ // skip legacy migration entirely — and as unmigrated by the registry.
1533
+ const configSchemaVersion = normalizeConfigSchemaVersion(parsedRaw.configSchemaVersion);
1523
1534
  this.#futureSchemaVersion =
1524
1535
  filePath === this.#configPath &&
1525
- typeof parsedRaw.configSchemaVersion === "number" &&
1526
- parsedRaw.configSchemaVersion > CONFIG_SCHEMA_VERSION;
1536
+ configSchemaVersion !== undefined &&
1537
+ configSchemaVersion > CONFIG_SCHEMA_VERSION;
1527
1538
 
1528
- const configSchemaVersion = parsedRaw.configSchemaVersion;
1529
1539
  if (
1530
1540
  filePath === this.#configPath &&
1531
- (typeof configSchemaVersion !== "number" || configSchemaVersion < CONFIG_SCHEMA_VERSION)
1541
+ (configSchemaVersion === undefined || configSchemaVersion < CONFIG_SCHEMA_VERSION)
1532
1542
  ) {
1533
1543
  this.#schemaMigrationPending = true;
1534
1544
  }
@@ -2082,7 +2092,7 @@ export class Settings implements NotificationSettingsReader {
2082
2092
  return false;
2083
2093
  }
2084
2094
  const schemaVersion = (root as Record<string, unknown> | null | undefined)?.configSchemaVersion;
2085
- if (typeof schemaVersion === "number" && schemaVersion > CONFIG_SCHEMA_VERSION) {
2095
+ if ((normalizeConfigSchemaVersion(schemaVersion) ?? 0) > CONFIG_SCHEMA_VERSION) {
2086
2096
  this.#warnLegacyFallbackMigration(
2087
2097
  `Settings: database workflow migration skipped: ${this.#configPath} is a future config schema (configSchemaVersion ${schemaVersion} > ${CONFIG_SCHEMA_VERSION}); leaving legacy rows for the next load`,
2088
2098
  );
@@ -2333,7 +2343,7 @@ export class Settings implements NotificationSettingsReader {
2333
2343
  // sets #futureSchemaVersion, so it must check the target schema
2334
2344
  // itself and never patch it or consume the legacy source.
2335
2345
  const targetSchemaVersion = (tx.root as Record<string, unknown> | null | undefined)?.configSchemaVersion;
2336
- if (typeof targetSchemaVersion === "number" && targetSchemaVersion > CONFIG_SCHEMA_VERSION) {
2346
+ if ((normalizeConfigSchemaVersion(targetSchemaVersion) ?? 0) > CONFIG_SCHEMA_VERSION) {
2337
2347
  this.#warnLegacyFallbackMigration(
2338
2348
  `Settings: config-root workflow migration skipped: ${target} is a future config schema (configSchemaVersion ${targetSchemaVersion} > ${CONFIG_SCHEMA_VERSION})`,
2339
2349
  );
@@ -3575,7 +3585,7 @@ export class Settings implements NotificationSettingsReader {
3575
3585
  // read-only across Settings; never patch it (mirrors the config-root
3576
3586
  // migration guard) so a future schema cannot inherit stale keys.
3577
3587
  const targetSchemaVersion = (tx.root as Record<string, unknown> | null | undefined)?.configSchemaVersion;
3578
- if (typeof targetSchemaVersion === "number" && targetSchemaVersion > CONFIG_SCHEMA_VERSION) {
3588
+ if ((normalizeConfigSchemaVersion(targetSchemaVersion) ?? 0) > CONFIG_SCHEMA_VERSION) {
3579
3589
  this.#warnLegacyFallbackMigration(
3580
3590
  `Settings: project workflow migration skipped: ${target} is a future config schema (configSchemaVersion ${targetSchemaVersion} > ${CONFIG_SCHEMA_VERSION})`,
3581
3591
  );
@@ -4092,7 +4102,7 @@ export class Settings implements NotificationSettingsReader {
4092
4102
  markerPath = `${source}.fallback-invalid`,
4093
4103
  ): Promise<void> {
4094
4104
  const targetSchemaVersion = (tx.root as Record<string, unknown> | null | undefined)?.configSchemaVersion;
4095
- if (typeof targetSchemaVersion === "number" && targetSchemaVersion > CONFIG_SCHEMA_VERSION) return;
4105
+ if ((normalizeConfigSchemaVersion(targetSchemaVersion) ?? 0) > CONFIG_SCHEMA_VERSION) return;
4096
4106
  if (tx.root !== undefined && (tx.root === null || typeof tx.root !== "object" || Array.isArray(tx.root))) {
4097
4107
  return;
4098
4108
  }
@@ -4821,7 +4831,7 @@ export class Settings implements NotificationSettingsReader {
4821
4831
  // guard) would rewrite configuration an older binary treats as
4822
4832
  // read-only.
4823
4833
  const targetSchemaVersion = (tx.root as Record<string, unknown> | null | undefined)?.configSchemaVersion;
4824
- if (typeof targetSchemaVersion === "number" && targetSchemaVersion > CONFIG_SCHEMA_VERSION) {
4834
+ if ((normalizeConfigSchemaVersion(targetSchemaVersion) ?? 0) > CONFIG_SCHEMA_VERSION) {
4825
4835
  return;
4826
4836
  }
4827
4837
  // Unset only while the target still matches the migration's fallback
@@ -5399,11 +5409,41 @@ export class Settings implements NotificationSettingsReader {
5399
5409
 
5400
5410
  /** Apply registered schema migrations once, using configSchemaVersion as the durable marker. */
5401
5411
  #migrateRawSettings(raw: RawSettings): RawSettings {
5402
- const configuredVersion = raw.configSchemaVersion;
5412
+ const rawVersion = raw.configSchemaVersion;
5413
+ const configuredVersion = normalizeConfigSchemaVersion(rawVersion);
5414
+ if (rawVersion !== undefined && rawVersion !== configuredVersion) {
5415
+ logger.warn("Settings: config has a malformed configSchemaVersion; migrating from the normalized version", {
5416
+ configSchemaVersion: String(rawVersion),
5417
+ normalized: String(configuredVersion),
5418
+ });
5419
+ }
5403
5420
  if (configuredVersion === CONFIG_SCHEMA_VERSION) return raw;
5404
5421
  if (typeof configuredVersion === "number" && configuredVersion > CONFIG_SCHEMA_VERSION) return raw;
5405
5422
 
5406
- // Migration registry v0 -> v1.
5423
+ // Ordered migration registry. Each step runs ONLY for files below its
5424
+ // target version: the v0 steps are not all idempotent (ask.timeout's
5425
+ // milliseconds-to-seconds conversion would re-divide a valid v1 value),
5426
+ // so a v1 file must skip straight to the v2 step.
5427
+ const fromVersion = typeof configuredVersion === "number" ? configuredVersion : 0;
5428
+ if (fromVersion < 1) this.#migrateRawSettingsV0ToV1(raw);
5429
+ // v1 -> v2: interruptMode -> toolInterruptPolicy.
5430
+ if (fromVersion < 2 && "interruptMode" in raw) {
5431
+ if (!("toolInterruptPolicy" in raw)) {
5432
+ const legacy = raw.interruptMode;
5433
+ if (legacy === "wait") raw.toolInterruptPolicy = "finish_tools";
5434
+ else if (legacy === "immediate") raw.toolInterruptPolicy = "abort_tools";
5435
+ // Any other value is malformed: drop it and let the schema default apply.
5436
+ }
5437
+ delete raw.interruptMode;
5438
+ }
5439
+
5440
+ raw.configSchemaVersion = CONFIG_SCHEMA_VERSION;
5441
+
5442
+ return raw;
5443
+ }
5444
+
5445
+ /** v0 -> v1 transforms. Runs only for files that predate schema version 1. */
5446
+ #migrateRawSettingsV0ToV1(raw: RawSettings): void {
5407
5447
  // queueMode -> steeringMode
5408
5448
  normalizeSessionDirectoryMigration(raw);
5409
5449
  if ("queueMode" in raw && !("steeringMode" in raw)) {
@@ -5568,10 +5608,6 @@ export class Settings implements NotificationSettingsReader {
5568
5608
  delete providersObj.imageCustomKeyEnv;
5569
5609
  }
5570
5610
  }
5571
-
5572
- raw.configSchemaVersion = CONFIG_SCHEMA_VERSION;
5573
-
5574
- return raw;
5575
5611
  }
5576
5612
 
5577
5613
  // ─────────────────────────────────────────────────────────────────────────
@@ -36,6 +36,8 @@ function projectionPlatform(): NodeJS.Platform {
36
36
 
37
37
  /** Post-filter parse-candidate cap. Exhaustion returns an explicit incomplete result. */
38
38
  export const COORDINATOR_JSON_SCAN_CAP = 10_000;
39
+ /** Number of fresh authoritative attempts allowed for a scan that observed churn. */
40
+ export const COORDINATOR_JSON_SCAN_RETRY_ATTEMPTS = 3;
39
41
 
40
42
  export interface ProjectionScanStat {
41
43
  size: number | bigint;
@@ -193,10 +195,10 @@ function trimProjectionRootPath(dir: string): string {
193
195
  }
194
196
 
195
197
  /**
196
- * Windows has no descriptor-relative `openat` binding in Node. Keep the root pathname
197
- * authoritative by checking its identity before and after every mutable-path operation.
198
+ * Node exposes no descriptor-relative `openat` binding on Windows or macOS. Keep the
199
+ * root pathname authoritative by checking its identity around every path operation.
198
200
  */
199
- async function openProjectionDirectoryWindowsSafe(dir: string): Promise<ProjectionScanDirectory> {
201
+ async function openProjectionDirectoryPathBracketed(dir: string): Promise<ProjectionScanDirectory> {
200
202
  const rootPath = trimProjectionRootPath(dir);
201
203
  let opened: import("node:fs").BigIntStats;
202
204
  try {
@@ -224,8 +226,8 @@ async function openProjectionDirectoryWindowsSafe(dir: string): Promise<Projecti
224
226
  };
225
227
 
226
228
  const entryPath = (entry: string): string => {
227
- // Directory entries come from readdir and are therefore single components on
228
- // Windows. Keep the invariant explicit so an injected or corrupted entry can
229
+ // Directory entries come from readdir and are therefore single components.
230
+ // Keep the invariant explicit so an injected or corrupted entry can
229
231
  // never turn a child operation into mutable pathname traversal.
230
232
  if (
231
233
  entry.length === 0 ||
@@ -288,7 +290,7 @@ async function openProjectionDirectoryWindowsSafe(dir: string): Promise<Projecti
288
290
  */
289
291
  async function openProjectionDirectorySafe(dir: string): Promise<ProjectionScanDirectory> {
290
292
  const platform = projectionPlatform();
291
- if (platform === "win32") return openProjectionDirectoryWindowsSafe(dir);
293
+ if (platform === "win32" || platform === "darwin") return openProjectionDirectoryPathBracketed(dir);
292
294
  if (platform !== "linux") throw new ProjectionScanUnsupportedError();
293
295
  const noFollow = fs.constants.O_NOFOLLOW;
294
296
  const nonBlock = fs.constants.O_NONBLOCK;
@@ -569,3 +571,22 @@ export async function listCoordinatorJsonFiles(
569
571
  await authority?.close().catch(() => undefined);
570
572
  }
571
573
  }
574
+
575
+ /**
576
+ * Retry only scans invalidated by observed directory/file churn. A candidate cap,
577
+ * unsupported authority, parse error, or any other failure remains fail-closed.
578
+ * Each attempt acquires a new authority, so no partial result crosses attempts.
579
+ */
580
+ export async function listCoordinatorJsonFilesWithRetry(
581
+ dir: string,
582
+ io: ProjectionScanFs = defaultFs,
583
+ cap: number = COORDINATOR_JSON_SCAN_CAP,
584
+ maxAttempts: number = COORDINATOR_JSON_SCAN_RETRY_ATTEMPTS,
585
+ ): Promise<ProjectionScanResult> {
586
+ const attempts = Math.max(1, Math.floor(maxAttempts));
587
+ let scan = await listCoordinatorJsonFiles(dir, io, cap);
588
+ for (let attempt = 1; attempt < attempts && scan.incomplete && scan.raced > 0; attempt++) {
589
+ scan = await listCoordinatorJsonFiles(dir, io, cap);
590
+ }
591
+ return scan;
592
+ }
@@ -1029,7 +1029,7 @@ function assertTransaction(transaction: CoordinatorSessionTransactionV1, namespa
1029
1029
  const rejectedHistorical = request.phase === "rejected";
1030
1030
  if (
1031
1031
  rejectedHistorical
1032
- ? !request.safe_receipt || request.safe_receipt.status !== "rejected"
1032
+ ? request.safe_receipt?.status !== "rejected"
1033
1033
  : question.answer_request_id !== request.request_id ||
1034
1034
  question.claim_fence_epoch !== request.claim_fence_epoch
1035
1035
  )
@@ -1083,7 +1083,7 @@ function assertTransaction(transaction: CoordinatorSessionTransactionV1, namespa
1083
1083
  event.transaction_revision > transaction.revision ||
1084
1084
  typeof event.kind !== "string" ||
1085
1085
  event.kind.length === 0 ||
1086
- !["turn", "question", "report", "session", "deletion"].includes(event.entity) ||
1086
+ !isOutboxEntity(event.entity) ||
1087
1087
  typeof event.entity_id !== "string" ||
1088
1088
  event.entity_id.length === 0 ||
1089
1089
  !isRecord(event.payload) ||
@@ -1296,7 +1296,7 @@ export async function listCanonicalActiveSessions(
1296
1296
  paths.registryLock,
1297
1297
  async () => {
1298
1298
  const registry = await readJson<NamespaceRegistryV1>(paths.registry);
1299
- if (!registry || registry.schema_version !== 1 || registry.namespace_id !== path.basename(paths.root))
1299
+ if (registry?.schema_version !== 1 || registry.namespace_id !== path.basename(paths.root))
1300
1300
  throw new Error("state_corrupt");
1301
1301
  return Object.values(registry.roster ?? {})
1302
1302
  .filter(entry => entry.active || entry.dirty)
@@ -1333,7 +1333,7 @@ export async function readDeliveryDiscoveryCursor(
1333
1333
  paths.registryLock,
1334
1334
  async () => {
1335
1335
  const registry = await readJson<NamespaceRegistryV1>(paths.registry);
1336
- if (!registry || registry.schema_version !== 1 || registry.namespace_id !== path.basename(paths.root))
1336
+ if (registry?.schema_version !== 1 || registry.namespace_id !== path.basename(paths.root))
1337
1337
  throw new Error("state_corrupt");
1338
1338
  return registry.delivery_discovery_cursor ?? "";
1339
1339
  },
@@ -1660,7 +1660,7 @@ export async function startCreationRemote(
1660
1660
  ): Promise<CreationRequestV1> {
1661
1661
  return await withNamespaceRegistry(paths, async registry => {
1662
1662
  const request = registry.creations[keyDigest];
1663
- if (!request || !request.sidecar_verifier) throw new Error("state_corrupt");
1663
+ if (!request?.sidecar_verifier) throw new Error("state_corrupt");
1664
1664
  if (
1665
1665
  request.phase === "claimed" &&
1666
1666
  (request.sidecar_verifier.key_id !== expectedVerifier.key_id ||
@@ -1687,8 +1687,7 @@ export async function reconcileCreationRemoteVerifier(
1687
1687
  ): Promise<CreationRequestV1> {
1688
1688
  return await withNamespaceRegistry(paths, async registry => {
1689
1689
  const request = registry.creations[keyDigest];
1690
- if (!request || !request.sidecar_verifier || request.phase !== "remote_started")
1691
- throw new Error("terminal_uncertain");
1690
+ if (!request?.sidecar_verifier || request.phase !== "remote_started") throw new Error("terminal_uncertain");
1692
1691
  if (!/^[a-f0-9]{64}$/.test(usedKeyId)) throw new Error("terminal_uncertain");
1693
1692
  if (usedKeyId === candidate.key_id) request.sidecar_verifier = candidate;
1694
1693
  else if (usedKeyId !== request.sidecar_verifier.key_id) throw new Error("terminal_uncertain");
@@ -2241,7 +2240,7 @@ export async function enumeratePublicDeliveries(
2241
2240
  paths.registryLock,
2242
2241
  async () => {
2243
2242
  const registry = await readJson<NamespaceRegistryV1>(paths.registry);
2244
- if (!registry || registry.schema_version !== 1 || registry.namespace_id !== path.basename(paths.root))
2243
+ if (registry?.schema_version !== 1 || registry.namespace_id !== path.basename(paths.root))
2245
2244
  throw new Error("state_corrupt");
2246
2245
  return [...new Set([...Object.keys(registry.roster ?? {}), ...Object.keys(registry.retained_sessions ?? {})])]
2247
2246
  .filter(name => COORDINATOR_SESSION_ID_PATTERN.test(name))
@@ -2526,7 +2525,7 @@ export async function replaceCreationRetirementIntent(
2526
2525
  if (request.phase !== "remote_started" && request.phase !== "uncertain" && request.phase !== "retired")
2527
2526
  throw new Error("retire_not_allowed");
2528
2527
  const staged = request.retirement_intent;
2529
- if (!staged || staged.phase !== "intent" || staged.broker_proof) throw new Error("state_corrupt");
2528
+ if (staged?.phase !== "intent" || staged.broker_proof) throw new Error("state_corrupt");
2530
2529
  if (staged.retirement_key_digest !== retirementKeyDigest) throw new Error("idempotency_conflict");
2531
2530
  assertCreationRetirementProofMatches(request, proof, false);
2532
2531
  staged.phase = "pre_effect_rejected";
@@ -2716,8 +2715,7 @@ export async function removeSessionTransaction(
2716
2715
  // exporter lease expires instead of deleting an undelivered event.
2717
2716
  if (Object.values(transaction.outbox).some(event => event.public_delivery.state !== "acknowledged"))
2718
2717
  return false;
2719
- await fs.rm(file, { force: true });
2720
- await syncCoordinatorDirectory(path.dirname(file));
2718
+ await removeCoordinatorStateFile(file);
2721
2719
  delete registry.roster?.[sessionId];
2722
2720
  delete registry.retained_sessions?.[sessionId];
2723
2721
  return true;
@@ -83,7 +83,7 @@ import {
83
83
  requireCoordinatorMutation,
84
84
  safeOpenCoordinatorArtifact,
85
85
  } from "./policy";
86
- import { listCoordinatorJsonFiles } from "./projection-scan";
86
+ import { listCoordinatorJsonFilesWithRetry } from "./projection-scan";
87
87
  import {
88
88
  answerBindingMatches,
89
89
  buildCoordinatorAskAnswerSchema,
@@ -484,6 +484,18 @@ interface CoordinatorEventInput {
484
484
  }
485
485
 
486
486
  const UNOBSERVED_COMPENSATION_CODE = "broker_compensation_unobserved";
487
+ const OBSERVED_BROKER_FAILURE_CODES = new Set([
488
+ "worktree_preparation_timeout",
489
+ "dependency_preparation_timeout",
490
+ "readiness_timeout",
491
+ "spawn_failed",
492
+ "worktree_in_use",
493
+ "startup_admission_timeout",
494
+ "startup_admission_refused",
495
+ "incarnation_unavailable",
496
+ "invalid_input",
497
+ "broker_request_unavailable",
498
+ ]);
487
499
 
488
500
  const MISSING_FINAL_RESPONSE_ADVISORY = "completion_missing_final_response";
489
501
  const PROMPT_ACK_TIMEOUT_REASON = "runtime_prompt_ack_timeout";
@@ -835,7 +847,7 @@ function toolSchema(name: CoordinatorToolName): {
835
847
  properties: {
836
848
  after_seq: { type: "integer", minimum: 0 },
837
849
  session_id: sessionId,
838
- event_types: { type: "array", items: { type: "string" } },
850
+ event_types: { type: "array", items: { type: "string", enum: [...COORDINATOR_EVENT_KINDS] } },
839
851
  timeout_ms: {
840
852
  type: "number",
841
853
  description: "Bounded event long-poll timeout in milliseconds, capped at 30 seconds.",
@@ -1216,6 +1228,14 @@ const PUBLIC_ERROR_MESSAGES: Record<string, string> = {
1216
1228
  broker_discovery_unavailable: "SDK broker discovery is unavailable.",
1217
1229
  broker_request_unavailable: "SDK broker request is unavailable.",
1218
1230
  broker_transport_unavailable: "SDK broker transport is unavailable.",
1231
+ startup_admission_timeout: "SDK host startup was not admitted before the queue wait cutoff.",
1232
+ startup_admission_refused: "SDK host startup was refused because the broker no longer owns the session root.",
1233
+ worktree_in_use: "The requested worktree is already held by another live session.",
1234
+ worktree_preparation_timeout: "Worktree preparation exceeded its deadline before the session host was spawned.",
1235
+ dependency_preparation_timeout: "Dependency preparation exceeded its deadline before the session host was spawned.",
1236
+ spawn_failed: "Session host could not be spawned.",
1237
+ readiness_timeout: "Session did not become ready before the SDK readiness deadline.",
1238
+ incarnation_unavailable: "OS process incarnation authority is unavailable.",
1219
1239
  session_closing: "Coordinator session is closing.",
1220
1240
  session_unavailable: "Coordinator session is unavailable.",
1221
1241
  session_not_activatable: "Coordinator session cannot be activated.",
@@ -1498,7 +1518,7 @@ function publicSdkAcknowledgement(result: RuntimePromptAcknowledgement): Record<
1498
1518
  }
1499
1519
 
1500
1520
  async function listJsonFiles(dir: string): Promise<unknown[]> {
1501
- const scan = await listCoordinatorJsonFiles(dir);
1521
+ const scan = await listCoordinatorJsonFilesWithRetry(dir);
1502
1522
  if (scan.capped) throw new Error("coordinator_projection_scan_incomplete");
1503
1523
  if (scan.skippedEmpty > 0 || scan.skippedDebris > 0) {
1504
1524
  logger.warn("Coordinator projection scan skipped debris", {
@@ -1530,7 +1550,10 @@ function brokerSessionId(record: Record<string, unknown>): string | null {
1530
1550
  }
1531
1551
 
1532
1552
  function brokerSessionScope(record: Record<string, unknown>): string | null {
1533
- return firstString(asRecord(record.locator) ?? {}, ["repo"]);
1553
+ // Locator v2 records the canonical workspace directory as `cwd`; the removed
1554
+ // `repo` field is never translated, so reading it here silently dropped every
1555
+ // broker row and failed open with an empty scoped listing.
1556
+ return firstString(asRecord(record.locator) ?? {}, ["cwd"]);
1534
1557
  }
1535
1558
 
1536
1559
  function sameCanonicalPath(left: string, right: string, platform: NodeJS.Platform): boolean {
@@ -2634,11 +2657,6 @@ async function writeActiveTurn(namespaceDir: string, turn: TurnRecord): Promise<
2634
2657
  });
2635
2658
  }
2636
2659
 
2637
- async function clearActiveTurn(namespaceDir: string, turn: TurnRecord): Promise<void> {
2638
- const active = asRecord(await readJsonFile(activeTurnFile(namespaceDir, turn.session_id)));
2639
- if (active?.turn_id === turn.turn_id) await removeCoordinatorFile(activeTurnFile(namespaceDir, turn.session_id));
2640
- }
2641
-
2642
2660
  async function readSessionState(namespaceDir: string, sessionId: string): Promise<CoordinatorSessionState | null> {
2643
2661
  return (await readJsonFile(sessionStateFile(namespaceDir, sessionId))) as CoordinatorSessionState | null;
2644
2662
  }
@@ -2658,9 +2676,6 @@ async function writeSessionStateUnlocked(
2658
2676
  } = {},
2659
2677
  ): Promise<CoordinatorSessionState> {
2660
2678
  const persisted = await readSessionState(namespaceDir, sessionId);
2661
- const session = asRecord(
2662
- await readJsonFile(path.join(namespaceDir, "sessions", `${safeExternalId("session", sessionId)}.json`)),
2663
- );
2664
2679
  // An overwrite is a canonical lifecycle repair, not proof that tool activity ended.
2665
2680
  const previous = options.overwrite ? null : persisted;
2666
2681
  const hasCurrentTurn = Object.hasOwn(options, "currentTurnId");
@@ -5044,6 +5059,58 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
5044
5059
  const keyDigest = createHash("sha256").update(idempotencyKey).digest("hex");
5045
5060
  return path.join(namespaceDir, "idempotency", `${keyDigest}.json`);
5046
5061
  }
5062
+ const idempotencyFlights = new Map<string, { requestDigest: string; promise: Promise<Record<string, unknown>> }>();
5063
+ type IdempotencyFlightAdmission =
5064
+ | { kind: "conflict"; response: Record<string, unknown> }
5065
+ | { kind: "joined"; promise: Promise<Record<string, unknown>> }
5066
+ | { kind: "owner"; run: (operation: Promise<Record<string, unknown>>) => Promise<Record<string, unknown>> };
5067
+ function admitIdempotencyFlight(
5068
+ tool: string,
5069
+ idempotencyKey: string,
5070
+ canonicalArgs: Record<string, unknown>,
5071
+ ): IdempotencyFlightAdmission {
5072
+ const keyDigest = createHash("sha256").update(idempotencyKey).digest("hex");
5073
+ const requestDigest = createHash("sha256")
5074
+ .update(canonicalJson({ tool, args: canonicalArgs }))
5075
+ .digest("hex");
5076
+ const existingFlight = idempotencyFlights.get(keyDigest);
5077
+ if (existingFlight) {
5078
+ if (existingFlight.requestDigest !== requestDigest)
5079
+ return {
5080
+ kind: "conflict",
5081
+ response: {
5082
+ ok: false,
5083
+ error: { code: "idempotency_conflict", message: "idempotency key was used with a different request" },
5084
+ },
5085
+ };
5086
+ return { kind: "joined", promise: existingFlight.promise };
5087
+ }
5088
+ const result = Promise.withResolvers<Record<string, unknown>>();
5089
+ const flight = { requestDigest, promise: result.promise };
5090
+ idempotencyFlights.set(keyDigest, flight);
5091
+ return {
5092
+ kind: "owner",
5093
+ run: async operation => {
5094
+ void operation.then(result.resolve, result.reject);
5095
+ try {
5096
+ return await result.promise;
5097
+ } finally {
5098
+ if (idempotencyFlights.get(keyDigest) === flight) idempotencyFlights.delete(keyDigest);
5099
+ }
5100
+ },
5101
+ };
5102
+ }
5103
+ async function withIdempotencyFlight(
5104
+ tool: string,
5105
+ idempotencyKey: string,
5106
+ canonicalArgs: Record<string, unknown>,
5107
+ operation: () => Promise<Record<string, unknown>>,
5108
+ ): Promise<Record<string, unknown>> {
5109
+ const admission = admitIdempotencyFlight(tool, idempotencyKey, canonicalArgs);
5110
+ if (admission.kind === "conflict") return admission.response;
5111
+ if (admission.kind === "joined") return await admission.promise;
5112
+ return await admission.run(operation());
5113
+ }
5047
5114
  async function withOrderedSessionStateLocks<T>(
5048
5115
  lockFiles: readonly string[],
5049
5116
  operation: () => Promise<T>,
@@ -5075,6 +5142,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
5075
5142
  recoverInProgress = false,
5076
5143
  isNonterminal: (response: Record<string, unknown>) => boolean = isRouterRequestAmbiguous,
5077
5144
  lockAlreadyHeld = false,
5145
+ flightAlreadyOwned = false,
5078
5146
  ): Promise<Record<string, unknown>> {
5079
5147
  const keyDigest = createHash("sha256").update(idempotencyKey).digest("hex");
5080
5148
  const requestDigest = createHash("sha256")
@@ -5179,7 +5247,11 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
5179
5247
  });
5180
5248
  return response;
5181
5249
  };
5182
- return lockAlreadyHeld ? execute() : withSessionStateLock(lockFile, execute);
5250
+ const executeWithLock = async (): Promise<Record<string, unknown>> =>
5251
+ lockAlreadyHeld ? await execute() : await withSessionStateLock(lockFile, execute);
5252
+ return flightAlreadyOwned
5253
+ ? await executeWithLock()
5254
+ : await withIdempotencyFlight(tool, idempotencyKey, canonicalArgs, executeWithLock);
5183
5255
  }
5184
5256
 
5185
5257
  async function brokerSession(
@@ -5268,7 +5340,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
5268
5340
  } catch (error) {
5269
5341
  const sessionId = remoteSession.value;
5270
5342
  if (!sessionId) {
5271
- if (error instanceof SdkClientError && error.code === "broker_request_unavailable") throw error;
5343
+ if (error instanceof SdkClientError && OBSERVED_BROKER_FAILURE_CODES.has(error.code)) throw error;
5272
5344
  throw new SdkClientError(
5273
5345
  UNOBSERVED_COMPENSATION_CODE,
5274
5346
  "Coordinator creation outcome could not be identified for cleanup.",
@@ -5281,11 +5353,28 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
5281
5353
  sessionId,
5282
5354
  );
5283
5355
  } catch (closeError) {
5284
- throw new SdkClientError(
5285
- UNOBSERVED_COMPENSATION_CODE,
5286
- `Coordinator creation failed after remote session ${sessionId} was created, and compensation failed.`,
5287
- { primary: error, compensation: closeError, session_id: sessionId },
5288
- );
5356
+ let listed: Record<string, unknown>;
5357
+ try {
5358
+ listed = await paginatedBrokerSessionList(cwd, { cwd });
5359
+ } catch (listError) {
5360
+ throw new SdkClientError(
5361
+ UNOBSERVED_COMPENSATION_CODE,
5362
+ `Coordinator creation failed after remote session ${sessionId} was created, and compensation failed.`,
5363
+ { primary: error, compensation: closeError, list: listError, session_id: sessionId },
5364
+ );
5365
+ }
5366
+ const rows = jsonRecords(Array.isArray(listed.sessions) ? listed.sessions : []);
5367
+ const row = rows.find(candidate => brokerSessionId(candidate) === sessionId);
5368
+ const deadUncertain =
5369
+ row !== undefined &&
5370
+ row.live !== true &&
5371
+ (row.terminalUncertain === true || (row as Record<string, unknown>).terminal_uncertain === true);
5372
+ if (!deadUncertain)
5373
+ throw new SdkClientError(
5374
+ UNOBSERVED_COMPENSATION_CODE,
5375
+ `Coordinator creation failed after remote session ${sessionId} was created, and compensation failed.`,
5376
+ { primary: error, compensation: closeError, session_id: sessionId },
5377
+ );
5289
5378
  }
5290
5379
  throw error;
5291
5380
  }
@@ -6713,7 +6802,7 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
6713
6802
  // any stale pointer directly: once the turn projection is rewritten
6714
6803
  // terminal, readActiveTurn() intentionally rejects it and cannot
6715
6804
  // discover the legacy active-turn file to clear.
6716
- await fs.rm(activeTurnFile(namespaceDir, sessionId), { force: true });
6805
+ await removeCoordinatorFile(activeTurnFile(namespaceDir, sessionId));
6717
6806
  }
6718
6807
  // A runtime sidecar can advance a live session while the canonical WAL still
6719
6808
  // needs projection repair. Preserve that observed lifecycle state (and its
@@ -8780,7 +8869,10 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
8780
8869
  code === "live_session"
8781
8870
  );
8782
8871
  };
8783
- return await withOrderedSessionStateLocks(
8872
+ const flight = admitIdempotencyFlight(name, retirementKey, canonicalArgs);
8873
+ if (flight.kind === "conflict") return flight.response;
8874
+ if (flight.kind === "joined") return await flight.promise;
8875
+ const retirementOperation = withOrderedSessionStateLocks(
8784
8876
  [idempotencyLockFile(creationKey), idempotencyLockFile(retirementKey)],
8785
8877
  async () =>
8786
8878
  await withToolIdempotency(
@@ -8964,8 +9056,10 @@ export function createCoordinatorMcpServer(options: CoordinatorMcpServerOptions
8964
9056
  true,
8965
9057
  isRetirementRetryable,
8966
9058
  true,
9059
+ true,
8967
9060
  ),
8968
9061
  );
9062
+ return await flight.run(retirementOperation);
8969
9063
  }
8970
9064
  if (name === "gjc_coordinator_activate_session") {
8971
9065
  requireCoordinatorMutation(config, "sessions", args);
@@ -613,6 +613,7 @@ export function applyCrashEvent(index: CrashIndex, event: CrashEvent, now: numbe
613
613
  }
614
614
 
615
615
  // occurrence
616
+ if (event.provenance === "eval" || event.provenance === "bun_test") return false;
616
617
  // Clear before deduplication: a replayed occurrence after main-index publish
617
618
  // but before retirement-sidecar publish must still supersede a stale tombstone.
618
619
  index.retiredFingerprints = index.retiredFingerprints.filter(fingerprint => fingerprint !== event.fingerprint);
@@ -698,6 +699,7 @@ async function recoverAndRecomputeRetainedCounts(index: CrashIndex, crashLogPath
698
699
  const recordsByFingerprint = new Map<string, LoadedCrashRecord[]>();
699
700
  const seenRecordIds = new Set<string>();
700
701
  for (const record of parseRecoverableCrashRecords(contents)) {
702
+ if (record.provenance === "eval" || record.provenance === "bun_test") continue;
701
703
  if (seenRecordIds.has(record.recordId)) continue;
702
704
  seenRecordIds.add(record.recordId);
703
705
  const records = recordsByFingerprint.get(record.fingerprint) ?? [];
@@ -15,6 +15,7 @@ import {
15
15
  CRASH_FINGERPRINT_VERSION,
16
16
  CRASH_RECORD_MARKER,
17
17
  type CrashFingerprint,
18
+ type CrashProvenance,
18
19
  computeCrashFingerprint,
19
20
  parseCrashRecordMarker,
20
21
  } from "@gajae-code/utils";
@@ -33,6 +34,7 @@ export interface LoadedCrashRecord {
33
34
  readonly body: string;
34
35
  /** The `Name: message` part of the header line. */
35
36
  readonly headline: string;
37
+ readonly provenance?: CrashProvenance;
36
38
  }
37
39
 
38
40
  interface ParsedCrashRecord {
@@ -78,6 +80,7 @@ function parseCandidates(contents: string): ParsedCrashRecord[] {
78
80
  let at = 0;
79
81
  let buffer: string[] = [];
80
82
  let started = false;
83
+ let provenance: CrashProvenance | undefined;
81
84
  let pending: ParsedCrashRecord | undefined;
82
85
  for (const line of contents.split("\n")) {
83
86
  if (pending) {
@@ -92,6 +95,12 @@ function parseCandidates(contents: string): ParsedCrashRecord[] {
92
95
  continue;
93
96
  }
94
97
  headline = header[2] ?? "";
98
+ const label = line.slice(line.indexOf("[") + 1, line.indexOf("]"));
99
+ const provenanceValue = label
100
+ .split(";")
101
+ .find(part => part.startsWith("provenance="))
102
+ ?.slice(11);
103
+ provenance = provenanceValue === "eval" || provenanceValue === "bun_test" ? provenanceValue : undefined;
95
104
  at = parsedAt;
96
105
  buffer = [];
97
106
  started = true;
@@ -116,6 +125,7 @@ function parseCandidates(contents: string): ParsedCrashRecord[] {
116
125
  messageClass: fingerprint?.messageClass ?? "",
117
126
  body: lines.join("\n").trimEnd(),
118
127
  headline,
128
+ ...(provenance === undefined ? {} : { provenance }),
119
129
  },
120
130
  complete: false,
121
131
  bound: marker.version === CRASH_FINGERPRINT_VERSION && fingerprint !== undefined,
@@ -378,7 +378,7 @@ Auto-answer has a clarity cap: unless the architect confidence is `high` and unc
378
378
 
379
379
  When the user's answer is free-text that carries reasoning, constraints, or scope decisions, do not forward it to scoring as a lossy one-line label. First structure it into a compact interpretation using the canonical sections — **Decision**, **Reasoning**, **Constraints (user-stated)**, **Out of scope (user-stated)**, and **Codebase context (verified)** (omit empty sections). Then confirm with exactly one `ask` that nothing is lost or misrepresented: the `ask` question body MUST render the full structured interpretation — every non-empty canonical section, verbatim — before the confirmation prompt. The user is approving that specific interpretation, so it must be visible inside the question body; never ask "does this capture it?" / "이 해석이 맞아?" without first displaying the interpretation itself. A confirmation `ask` whose body omits the interpretation it is asking about is a hard error: re-issue it with the interpretation shown. Apply `language.instruction` when present.
380
380
 
381
- Offer options such as **Send as-is**, **Add a constraint**, **Mark something out of scope**, **Add context**, and **Rewrite**, plus free-text. If the user picks anything other than "Send as-is", collect the exact missing text with one follow-up `ask` (never infer it from the option label), fold it into the structured interpretation, and re-confirm. Do not advance to scoring while the user is still saying something is missing.
381
+ Offer options such as **Send as-is**, **Add a constraint**, **Mark something out of scope**, **Add context**, and **Rewrite**, plus free-text through the Ask UI's automatic Other/custom-input control. Never add the automatic custom-input label or a pseudo-option that imitates it, such as **Other (type your own)**, **Other (specify)**, **type your own**, **custom input**, **직접 입력**, or **직접 입력으로 추가**; a plain domain value such as **Other** is valid when it is a genuine answer, not a request for free text, and none of these custom-input labels may receive `recommended`. If the user picks anything other than "Send as-is", collect the exact missing text with one follow-up `ask` whose question body asks for the text and whose options are only real non-custom choices such as cancel/keep-current (never infer it from the option label), fold it into the structured interpretation, and re-confirm. Do not advance to scoring while the user is still saying something is missing.
382
382
 
383
383
  Skip Refine for short answers with no attached reasoning (e.g. "Yes" / "No" / a single proper noun), for pre-built option picks where the structure is already explicit, for auto-confirmed code/brownfield facts, and for architect auto-answers (already structured by Step 2b′). A refined answer counts as direct user judgment: record the round in `refined_rounds` and reset `auto_answer_streak` to 0. Feed the confirmed structured interpretation — not the raw free text — into Step 2c scoring and established-facts maintenance.
384
384