@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
@@ -15,6 +15,8 @@ export const MANAGED_OWNER_GENERATION_ENV = "GJC_TMUX_OWNER_GENERATION";
15
15
  export const MANAGED_OWNER_STATE_DIR_ENV = "GJC_TMUX_OWNER_STATE_DIR";
16
16
  export const MANAGED_OWNER_RUN_ID_ENV = "GJC_MANAGED_OWNER_RUN_ID";
17
17
  export const MANAGED_OWNER_INCARNATION_ENV = "GJC_MANAGED_OWNER_INCARNATION";
18
+ /** Suppresses command-derived durable artifacts for Broker-owned opaque spawn children. */
19
+ export const MANAGED_OWNER_REDACT_COMMAND_ENV = "GJC_MANAGED_OWNER_REDACT_COMMAND";
18
20
 
19
21
  let bootstrapSigtermPending = false;
20
22
  const captureBootstrapSigterm = () => {
@@ -140,26 +142,32 @@ export async function runManagedOwnerSupervisor(): Promise<void> {
140
142
  if (!supervisorStartTime) throw new Error("managed_owner_supervisor_start_time_unavailable");
141
143
  await fs.mkdir(root, { recursive: true, mode: 0o700 });
142
144
  const childToken = crypto.randomUUID();
143
- const binding: ManagedOwnerBinding = {
144
- schema_version: 2,
145
- generation,
146
- session_id: sessionId,
147
- run_id: runId,
148
- endpoint_incarnation: incarnation,
149
- child_token: childToken,
150
- command,
151
- command_sha256: commandDigest(command),
152
- supervisor_pid: process.pid,
153
- supervisor_start_time: supervisorStartTime,
154
- created_at: new Date().toISOString(),
155
- };
156
- await writeDurableExclusive(path.join(root, `child-${childToken}.binding.json`), binding);
145
+ const redactCommand = process.env[MANAGED_OWNER_REDACT_COMMAND_ENV] === "1";
146
+ const binding: ManagedOwnerBinding | null = redactCommand
147
+ ? null
148
+ : {
149
+ schema_version: 2,
150
+ generation,
151
+ session_id: sessionId,
152
+ run_id: runId,
153
+ endpoint_incarnation: incarnation,
154
+ child_token: childToken,
155
+ command,
156
+ command_sha256: commandDigest(command),
157
+ supervisor_pid: process.pid,
158
+ supervisor_start_time: supervisorStartTime,
159
+ created_at: new Date().toISOString(),
160
+ };
161
+ if (binding) await writeDurableExclusive(path.join(root, `child-${childToken}.binding.json`), binding);
162
+ const childEnvironment: NodeJS.ProcessEnv = { ...process.env, [MANAGED_OWNER_CHILD_TOKEN_ENV]: childToken };
163
+ delete childEnvironment[MANAGED_OWNER_COMMAND_ENV];
164
+ delete childEnvironment[MANAGED_OWNER_REDACT_COMMAND_ENV];
157
165
  const child = Bun.spawn({
158
166
  cmd: command,
159
167
  stdin: "inherit",
160
168
  stdout: "inherit",
161
169
  stderr: "inherit",
162
- env: { ...process.env, [MANAGED_OWNER_CHILD_TOKEN_ENV]: childToken },
170
+ env: childEnvironment,
163
171
  });
164
172
  const childStartTime = await managedOwnerProcessProvenance(child.pid);
165
173
  if (!childStartTime) throw new Error("managed_owner_child_start_time_unavailable");
@@ -167,24 +175,26 @@ export async function runManagedOwnerSupervisor(): Promise<void> {
167
175
  if (!childProcess) {
168
176
  const exitCode = await child.exited;
169
177
  if (child.signalCode === "SIGABRT") {
170
- const receipt: ManagedOwnerSigabrtReceipt = {
171
- schema_version: 2,
172
- generation,
173
- session_id: sessionId,
174
- run_id: runId,
175
- endpoint_incarnation: incarnation,
176
- child_token: childToken,
177
- command_sha256: binding.command_sha256,
178
- supervisor_pid: process.pid,
179
- supervisor_start_time: supervisorStartTime,
180
- child_pid: child.pid,
181
- child_start_time: childStartTime,
182
- signal: "SIGABRT",
183
- signal_number: 6,
184
- exit_code: exitCode,
185
- received_at: new Date().toISOString(),
186
- };
187
- await writeDurableExclusive(path.join(root, `sigabrt-${childToken}.receipt.json`), receipt);
178
+ if (binding) {
179
+ const receipt: ManagedOwnerSigabrtReceipt = {
180
+ schema_version: 2,
181
+ generation,
182
+ session_id: sessionId,
183
+ run_id: runId,
184
+ endpoint_incarnation: incarnation,
185
+ child_token: childToken,
186
+ command_sha256: binding.command_sha256,
187
+ supervisor_pid: process.pid,
188
+ supervisor_start_time: supervisorStartTime,
189
+ child_pid: child.pid,
190
+ child_start_time: childStartTime,
191
+ signal: "SIGABRT",
192
+ signal_number: 6,
193
+ exit_code: exitCode,
194
+ received_at: new Date().toISOString(),
195
+ };
196
+ await writeDurableExclusive(path.join(root, `sigabrt-${childToken}.receipt.json`), receipt);
197
+ }
188
198
  process.exitCode = 134;
189
199
  return;
190
200
  }
@@ -246,24 +256,26 @@ export async function runManagedOwnerSupervisor(): Promise<void> {
246
256
  });
247
257
  }
248
258
  if (child.signalCode === "SIGABRT") {
249
- const receipt: ManagedOwnerSigabrtReceipt = {
250
- schema_version: 2,
251
- generation,
252
- session_id: sessionId,
253
- run_id: runId,
254
- endpoint_incarnation: incarnation,
255
- child_token: childToken,
256
- command_sha256: binding.command_sha256,
257
- supervisor_pid: process.pid,
258
- supervisor_start_time: supervisorStartTime,
259
- child_pid: child.pid,
260
- child_start_time: childStartTime,
261
- signal: "SIGABRT",
262
- signal_number: 6,
263
- exit_code: exitCode,
264
- received_at: new Date().toISOString(),
265
- };
266
- await writeDurableExclusive(path.join(root, `sigabrt-${childToken}.receipt.json`), receipt);
259
+ if (binding) {
260
+ const receipt: ManagedOwnerSigabrtReceipt = {
261
+ schema_version: 2,
262
+ generation,
263
+ session_id: sessionId,
264
+ run_id: runId,
265
+ endpoint_incarnation: incarnation,
266
+ child_token: childToken,
267
+ command_sha256: binding.command_sha256,
268
+ supervisor_pid: process.pid,
269
+ supervisor_start_time: supervisorStartTime,
270
+ child_pid: child.pid,
271
+ child_start_time: childStartTime,
272
+ signal: "SIGABRT",
273
+ signal_number: 6,
274
+ exit_code: exitCode,
275
+ received_at: new Date().toISOString(),
276
+ };
277
+ await writeDurableExclusive(path.join(root, `sigabrt-${childToken}.receipt.json`), receipt);
278
+ }
267
279
  process.exitCode = 134;
268
280
  return;
269
281
  }
@@ -39,6 +39,7 @@ const LOCK_ACQUIRE_RETRY_MS = 5;
39
39
  const LOCK_ACQUIRE_MAX_RETRY_MS = 100;
40
40
  const LOCK_ACQUIRE_TIMEOUT_MS = 2_000;
41
41
  const LOCK_STALE_MS = 30_000;
42
+ const RELEASED_TRANSITION_GRACE_MS = 1_000;
42
43
 
43
44
  interface LockRetryBudget {
44
45
  startedAt: number;
@@ -54,6 +55,10 @@ function lockRetryElapsedMs(budget: LockRetryBudget): number {
54
55
  return Math.max(0, performance.now() - budget.startedAt);
55
56
  }
56
57
 
58
+ function lockRetryExhausted(budget: LockRetryBudget): boolean {
59
+ return lockRetryElapsedMs(budget) >= LOCK_ACQUIRE_TIMEOUT_MS;
60
+ }
61
+
57
62
  async function waitForLockRetry(budget: LockRetryBudget): Promise<boolean> {
58
63
  budget.attempts++;
59
64
  const remainingMs = LOCK_ACQUIRE_TIMEOUT_MS - lockRetryElapsedMs(budget);
@@ -77,8 +82,8 @@ async function waitForLockRetry(budget: LockRetryBudget): Promise<boolean> {
77
82
  * owner record is the sibling `<file>.lock.transition.owner`. `mkdir` admits exactly one
78
83
  * contender, while the held directory prevents a successor until release unlinks the
79
84
  * validated sidecar and atomically `rmdir`s the claim. A crash before either step leaves a
80
- * fail-closed directory for explicit recovery; concurrent automatic reclaim cannot prove
81
- * that a directory + sibling sidecar still name the claim it inspected.
85
+ * directory that a successor may reclaim only with proof: a host-qualified owner whose pid
86
+ * is proven dead, plus an exact directory-generation and tree match at removal time.
82
87
  *
83
88
  * The separate sidecar keeps the claim directory empty, which is what makes `rmdir` the
84
89
  * identity-safe portable release primitive. The path stays distinct from `<file>.lock`
@@ -409,6 +414,8 @@ export const SessionStateLockTestHooks: {
409
414
  forcedQuarantineName?: string;
410
415
  /** @internal Observes bounded acquisition retries without changing their timing. */
411
416
  afterAcquireContention?: (lockFile: string, attempt: number, elapsedMs: number) => void;
417
+ /** @internal Runs after transition mkdir contention and before stale-claim inspection. */
418
+ afterTransitionClaimContention?: (transitionDir: string) => void | Promise<void>;
412
419
  } = {};
413
420
 
414
421
  /** Raised when the lock could not be acquired; callers map it to their own refusal. */
@@ -464,6 +471,112 @@ export class SessionStateLockUnavailableError extends Error {
464
471
  }
465
472
  }
466
473
 
474
+ /**
475
+ * Log fields describing a persist failure: the error text plus, when a session state
476
+ * lock refusal sits anywhere in the cause chain, its typed `reason` and `lockPath`.
477
+ * Wrapping errors (`PreviousRuntimeStateReadError`, `AggregateError`) are searched
478
+ * so the actionable detail is never buried behind a generic message.
479
+ */
480
+ export function sessionStateLockFailureFields(error: unknown): {
481
+ error: string;
482
+ reason?: SessionStateLockUnavailableReason;
483
+ lockPath?: string;
484
+ } {
485
+ const fields: { error: string; reason?: SessionStateLockUnavailableReason; lockPath?: string } = {
486
+ error: String(error),
487
+ };
488
+ const seen = new Set<unknown>();
489
+ const queue: unknown[] = [error];
490
+ while (queue.length > 0) {
491
+ const current = queue.shift();
492
+ if (current === null || typeof current !== "object" || seen.has(current)) continue;
493
+ seen.add(current);
494
+ if (current instanceof SessionStateLockUnavailableError) {
495
+ if (current.reason !== undefined) fields.reason = current.reason;
496
+ if (current.lockPath !== undefined) fields.lockPath = current.lockPath;
497
+ if (fields.reason !== undefined && fields.lockPath !== undefined) return fields;
498
+ }
499
+ if (current instanceof AggregateError) queue.push(...current.errors);
500
+ if ("cause" in current) queue.push((current as { cause?: unknown }).cause);
501
+ }
502
+ return fields;
503
+ }
504
+
505
+ function boundedPersistFailureClassPart(value: unknown, fallback: string): string {
506
+ if (typeof value !== "string" && typeof value !== "number") return fallback;
507
+ const normalized = String(value)
508
+ .replace(/[^A-Za-z0-9_.-]/gu, "_")
509
+ .slice(0, 64);
510
+ return normalized || fallback;
511
+ }
512
+
513
+ function persistFailureProperty(value: unknown, name: string): unknown {
514
+ if (value === null || (typeof value !== "object" && typeof value !== "function")) return undefined;
515
+ try {
516
+ return Reflect.get(value, name);
517
+ } catch {
518
+ return undefined;
519
+ }
520
+ }
521
+
522
+ function persistFailureConstructorName(error: unknown): string {
523
+ if (error === null) return "null";
524
+ if (typeof error !== "object" && typeof error !== "function") return typeof error;
525
+ const ctor = persistFailureProperty(error, "constructor");
526
+ const name = persistFailureProperty(ctor, "name");
527
+ return typeof name === "string" && name.length > 0 ? name : "Object";
528
+ }
529
+
530
+ /**
531
+ * Build a bounded warn-window partition without including arbitrary error text. Typed lock
532
+ * refusals use their stable reason; all other failures use the root constructor and the
533
+ * first structured `code`/`errno` found in the error/cause chain.
534
+ */
535
+ function persistFailureClass(error: unknown): string {
536
+ const lockFields = sessionStateLockFailureFields(error);
537
+ if (lockFields.reason !== undefined) return `reason:${lockFields.reason}`;
538
+ const seen = new Set<unknown>();
539
+ const queue: unknown[] = [error];
540
+ let code: unknown;
541
+ let errno: unknown;
542
+ while (queue.length > 0) {
543
+ const current = queue.shift();
544
+ if (current === null || typeof current !== "object" || seen.has(current)) continue;
545
+ seen.add(current);
546
+ code ??= persistFailureProperty(current, "code");
547
+ errno ??= persistFailureProperty(current, "errno");
548
+ if (code !== undefined && errno !== undefined) break;
549
+ if (current instanceof AggregateError) queue.push(...current.errors);
550
+ const cause = persistFailureProperty(current, "cause");
551
+ if (cause !== undefined) queue.push(cause);
552
+ }
553
+ return `error:${boundedPersistFailureClassPart(persistFailureConstructorName(error), "Object")}:code=${boundedPersistFailureClassPart(code, "none")}:errno=${boundedPersistFailureClassPart(errno, "none")}`;
554
+ }
555
+
556
+ export const PERSIST_FAILURE_WARN_WINDOW_MS = 30_000;
557
+ const persistFailureLastWarnedAt = new Map<string, number>();
558
+
559
+ /**
560
+ * Whether a persist failure should be logged at warn level. The first failure for each
561
+ * document and stable failure class in a 30s window warns; repeats are the caller's cue
562
+ * to log at debug instead.
563
+ */
564
+ export function shouldWarnPersistFailure(key: string, error: unknown, now = Date.now()): boolean {
565
+ for (const [otherKey, warnedAt] of persistFailureLastWarnedAt) {
566
+ if (now - warnedAt >= PERSIST_FAILURE_WARN_WINDOW_MS) persistFailureLastWarnedAt.delete(otherKey);
567
+ }
568
+ const scopedKey = `${key}\u0000${persistFailureClass(error)}`;
569
+ const last = persistFailureLastWarnedAt.get(scopedKey);
570
+ if (last !== undefined && now - last < PERSIST_FAILURE_WARN_WINDOW_MS) return false;
571
+ persistFailureLastWarnedAt.set(scopedKey, now);
572
+ return true;
573
+ }
574
+
575
+ /** @internal Test seam: forget every persist-failure warn window. */
576
+ export function resetPersistFailureWarnWindows(): void {
577
+ persistFailureLastWarnedAt.clear();
578
+ }
579
+
467
580
  function lockUnavailable(
468
581
  lockPath: string,
469
582
  reason: SessionStateLockUnavailableReason,
@@ -1308,7 +1421,13 @@ async function acquireOwnerLock(
1308
1421
  } catch {
1309
1422
  throw error;
1310
1423
  }
1311
- if (!validLockOwner(observed) || observed.released !== true) throw error;
1424
+ if (!validLockOwner(observed)) throw error;
1425
+ if (observed.released !== true) {
1426
+ if (await lockOwnerIsAlive(observed)) throw error;
1427
+ const removed = exactUnlinkOwnerRecord(file, current, quarantineName);
1428
+ if (removed !== "removed" && removed !== "absent") throw error;
1429
+ return await createOwnerLock(file, owner, quarantineName);
1430
+ }
1312
1431
  try {
1313
1432
  return await rewriteHeldOwnerRecord(file, current, owner);
1314
1433
  } catch (error) {
@@ -1330,7 +1449,10 @@ async function acquireOwnerLock(
1330
1449
  *
1331
1450
  * Stale-owner reclaim still uses the native identity-bound detach protocol because no live
1332
1451
  * owner or transition claim can vouch for that pathname. A mismatch here likewise leaves a
1333
- * successor strictly alone.
1452
+ * successor strictly alone. A released tombstone is reclaimable on every platform when its
1453
+ * owner host is this installation and the transition directory is proven empty and unchanged:
1454
+ * the tombstone is the release marker, while the directory generation binds that marker to
1455
+ * the exact claim that release failed to remove.
1334
1456
  */
1335
1457
  async function releaseOwnerLock(file: string, held: LockOwnerSnapshot): Promise<void> {
1336
1458
  let owner: unknown;
@@ -1378,6 +1500,7 @@ async function releaseOwnerLock(file: string, held: LockOwnerSnapshot): Promise<
1378
1500
  type SessionStateLockReclaimResult =
1379
1501
  | "absent_or_changed"
1380
1502
  | "legacy_directory_unprovenanced"
1503
+ | "owner_removed"
1381
1504
  | "owner_live_or_unverifiable"
1382
1505
  | "owner_record_fresh"
1383
1506
  | "owner_unprovenanced"
@@ -1416,7 +1539,8 @@ async function reclaimStaleOwnerRecord(
1416
1539
  // A successor that took the path in the final window keeps it; this call just loses.
1417
1540
  if (outcome === "refused")
1418
1541
  throw new SessionStateLockUnavailableError(new Error("Stale owner record could not be reclaimed."));
1419
- return outcome === "removed" || outcome === "absent" ? "reclaimed" : "absent_or_changed";
1542
+ if (outcome === "removed") return "owner_removed";
1543
+ return "absent_or_changed";
1420
1544
  }
1421
1545
 
1422
1546
  /**
@@ -1548,14 +1672,18 @@ async function releaseTransitionClaim(
1548
1672
  }
1549
1673
  }
1550
1674
 
1551
- async function reclaimStaleTransitionClaim(transitionDir: string, quarantineName: string): Promise<void> {
1552
- const stat = await fs.lstat(transitionDir).catch(() => null);
1553
- if (!stat) return;
1675
+ async function reclaimStaleTransitionClaim(transitionDir: string, quarantineName: string): Promise<boolean> {
1676
+ let stat: fsSync.BigIntStats;
1677
+ try {
1678
+ stat = await fs.lstat(transitionDir, { bigint: true });
1679
+ } catch {
1680
+ return false;
1681
+ }
1554
1682
  // Regular-file claims belong to the superseded protocol. They retain the old
1555
1683
  // exact-identity stale path; released PID-1 tombstones deliberately require
1556
1684
  // explicit cleanup before this atomic-directory protocol can take over.
1557
1685
  if (stat.isFile()) {
1558
- await reclaimStaleOwnerRecord(
1686
+ const reclaimed = await reclaimStaleOwnerRecord(
1559
1687
  transitionDir,
1560
1688
  {
1561
1689
  afterInspection: SessionStateLockTestHooks.afterTransitionStaleInspection,
@@ -1563,12 +1691,78 @@ async function reclaimStaleTransitionClaim(transitionDir: string, quarantineName
1563
1691
  },
1564
1692
  quarantineName,
1565
1693
  );
1566
- return;
1694
+ return reclaimed === "owner_removed";
1567
1695
  }
1568
1696
  if (!stat.isDirectory()) throw new SessionStateLockUnavailableError();
1569
- // No portable operation can atomically prove that this directory + sibling
1570
- // sidecar are still the dead claim two concurrent reclaimers inspected. A
1571
- // crashed atomic claim therefore stays fail-closed for explicit recovery.
1697
+ const ownerSnapshot = await captureRegularLockOwner(`${transitionDir}.owner`);
1698
+ if (!ownerSnapshot) return false;
1699
+ let owner: unknown;
1700
+ try {
1701
+ owner = JSON.parse(ownerSnapshot.bytes);
1702
+ } catch {
1703
+ return false;
1704
+ }
1705
+ if (!validLockOwner(owner)) return false;
1706
+ await SessionStateLockTestHooks.afterTransitionStaleInspection?.(transitionDir);
1707
+ if (owner.released === true) {
1708
+ // A released record is safe only when it is qualified by this installation.
1709
+ // Never let a shared-volume tombstone, an absent identity, or a legacy foreign
1710
+ // identity authorize removal of a claim this process cannot own.
1711
+ if (owner.owner_host_id === undefined) return false;
1712
+ const currentHost = await currentOwnerHostId();
1713
+ const legacyHost = await currentLegacyOwnerHostId();
1714
+ if (owner.owner_host_id !== currentHost && owner.owner_host_id !== legacyHost) return false;
1715
+ if (Date.now() - Number(ownerSnapshot.mtimeNs / 1_000_000n) < RELEASED_TRANSITION_GRACE_MS) return false;
1716
+ } else if (await lockOwnerIsAlive(owner)) {
1717
+ // Only a host-qualified owner whose pid is PROVEN dead (ESRCH, or a live pid
1718
+ // with a provably different incarnation) authorizes reclaim. The generation
1719
+ // + exact-tree checks below then bind the removal to the very directory
1720
+ // inspected here, on every platform; a claim stranded by a force-quit
1721
+ // (`postmortem.quit`) would otherwise wall the session directory forever.
1722
+ return false;
1723
+ }
1724
+ const generation = transitionGenerationFromStat(stat);
1725
+ const nativePath = await canonicalOwnedTransitionPath(transitionDir).catch(() => null);
1726
+ if (!nativePath) return false;
1727
+ const captured = nativeSessionStateLock().snapshotDirectoryTree(nativePath);
1728
+ if (!captured.ok || !captured.snapshot) return false;
1729
+ const root = captured.snapshot.entries.find(entry => entry.relativePath === "");
1730
+ if (
1731
+ !root ||
1732
+ root.kind !== "directory" ||
1733
+ root.dev !== String(generation.dev) ||
1734
+ root.ino !== String(generation.ino) ||
1735
+ root.nlink !== String(generation.nlink) ||
1736
+ root.mtimeNs !== String(generation.mtimeNs) ||
1737
+ root.ctimeNs !== String(generation.ctimeNs)
1738
+ )
1739
+ return false;
1740
+ if (captured.snapshot.entries.length !== 1) return false;
1741
+ const removed = nativeSessionStateLock().exactRemoveDirectoryTree(nativePath, captured.snapshot);
1742
+ if (removed.ok || removed.code === "not_found") {
1743
+ exactUnlinkOwnerRecord(`${transitionDir}.owner`, ownerSnapshot, quarantineName);
1744
+ return removed.ok;
1745
+ }
1746
+ if (
1747
+ removed.code === "cleanup_pending" &&
1748
+ removed.payloadDurable === true &&
1749
+ removed.detachedPath === `${nativePath}.removing` &&
1750
+ removed.retainedSuccessorPath === undefined &&
1751
+ removed.retainedUnknownPath === undefined &&
1752
+ removed.retainedPlaceholderPath === undefined
1753
+ ) {
1754
+ // The captured claim is proven empty, so the native detach receipt leaves an
1755
+ // empty directory at detachedPath. Remove that exact directory directly: calling
1756
+ // the detach primitive again would deterministically create a second `.removing`
1757
+ // collision on POSIX. rmdir never follows a directory's contents and refuses if a
1758
+ // successor populated the detached path.
1759
+ await removeTransitionDir(removed.detachedPath);
1760
+ exactUnlinkOwnerRecord(`${transitionDir}.owner`, ownerSnapshot, quarantineName);
1761
+ return true;
1762
+ }
1763
+ throw new SessionStateLockUnavailableError(
1764
+ new Error(`Stale transition claim could not be reclaimed (${removed.code ?? "unknown"}).`),
1765
+ );
1572
1766
  }
1573
1767
 
1574
1768
  /** Run one pathname transition under an atomic `mkdir`/`rmdir` claim. */
@@ -1828,7 +2022,12 @@ async function withLockPathTransition<T>(
1828
2022
  } catch (error) {
1829
2023
  const code = (error as NodeJS.ErrnoException).code;
1830
2024
  if (code !== "EEXIST" && !isTransientLockError(error)) throw new SessionStateLockUnavailableError(error);
1831
- await reclaimStaleTransitionClaim(transitionDir, quarantineName);
2025
+ await SessionStateLockTestHooks.afterTransitionClaimContention?.(transitionDir);
2026
+ if (await reclaimStaleTransitionClaim(transitionDir, quarantineName)) {
2027
+ if (lockRetryExhausted(budget))
2028
+ throw lockUnavailable(transitionDir, "transition_claim_timeout", budget, error);
2029
+ continue;
2030
+ }
1832
2031
  if (!(await waitForLockRetry(budget)))
1833
2032
  throw lockUnavailable(transitionDir, "transition_claim_timeout", budget, error);
1834
2033
  continue;