@gajae-code/coding-agent 0.16.7 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (321) hide show
  1. package/CHANGELOG.md +180 -1
  2. package/dist/types/capability/fs.d.ts +37 -4
  3. package/dist/types/capability/index.d.ts +19 -0
  4. package/dist/types/capability/types.d.ts +9 -0
  5. package/dist/types/cli/doctor/args.d.ts +36 -0
  6. package/dist/types/cli/doctor/checks.d.ts +18 -0
  7. package/dist/types/cli/doctor/config-repairs.d.ts +38 -0
  8. package/dist/types/cli/doctor/context.d.ts +89 -0
  9. package/dist/types/cli/doctor/files.d.ts +27 -0
  10. package/dist/types/cli/doctor/ids.d.ts +20 -0
  11. package/dist/types/cli/doctor/install-repairs.d.ts +94 -0
  12. package/dist/types/cli/doctor/journal.d.ts +43 -0
  13. package/dist/types/cli/doctor/managed-link.d.ts +39 -0
  14. package/dist/types/cli/doctor/native-probe.d.ts +2 -0
  15. package/dist/types/cli/doctor/permission-repairs.d.ts +25 -0
  16. package/dist/types/cli/doctor/plugin-quarantine.d.ts +77 -0
  17. package/dist/types/cli/doctor/plugin-restore.d.ts +40 -0
  18. package/dist/types/cli/doctor/plugin-targets.d.ts +11 -0
  19. package/dist/types/cli/doctor/probe-client.d.ts +9 -0
  20. package/dist/types/cli/doctor/probe-types.d.ts +18 -0
  21. package/dist/types/cli/doctor/probe.d.ts +3 -0
  22. package/dist/types/cli/doctor/projection-probe.d.ts +2 -0
  23. package/dist/types/cli/doctor/repairs.d.ts +28 -0
  24. package/dist/types/cli/doctor/report.d.ts +27 -0
  25. package/dist/types/cli/doctor/runner.d.ts +4 -0
  26. package/dist/types/cli/doctor/safety.d.ts +10 -0
  27. package/dist/types/cli/doctor/service-repairs.d.ts +8 -0
  28. package/dist/types/cli/doctor/service-targets.d.ts +41 -0
  29. package/dist/types/cli/doctor/types.d.ts +145 -0
  30. package/dist/types/cli/doctor-cli.d.ts +7 -0
  31. package/dist/types/cli/doctor-supervisor.d.ts +121 -0
  32. package/dist/types/cli/doctor-worker.d.ts +29 -0
  33. package/dist/types/cli/install-activation.d.ts +158 -0
  34. package/dist/types/cli/macos-community-app.d.ts +41 -0
  35. package/dist/types/cli/mcp-cli.d.ts +0 -11
  36. package/dist/types/cli/update-cli.d.ts +41 -4
  37. package/dist/types/cli-main.d.ts +174 -0
  38. package/dist/types/cli.d.ts +1 -171
  39. package/dist/types/commands/doctor.d.ts +7 -0
  40. package/dist/types/commands/sdk.d.ts +17 -8
  41. package/dist/types/config/autorouting-tier-map.d.ts +19 -0
  42. package/dist/types/config/file-lock.d.ts +27 -3
  43. package/dist/types/config/model-preset-registry.d.ts +1 -0
  44. package/dist/types/config/model-registry.d.ts +12 -5
  45. package/dist/types/config/model-resolver.d.ts +16 -3
  46. package/dist/types/config/models-config-schema.d.ts +2 -0
  47. package/dist/types/config/settings-schema.d.ts +1 -1
  48. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +1 -1
  49. package/dist/types/coordinator-mcp/session-reaper.d.ts +15 -5
  50. package/dist/types/cursor.d.ts +9 -9
  51. package/dist/types/discovery/agents-md.d.ts +6 -1
  52. package/dist/types/discovery/helpers.d.ts +36 -7
  53. package/dist/types/discovery/index.d.ts +1 -1
  54. package/dist/types/discovery/mcp-json.d.ts +2 -0
  55. package/dist/types/extensibility/extensions/loader.d.ts +22 -1
  56. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +14 -1
  57. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -1
  58. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  59. package/dist/types/extensibility/plugins/manager.d.ts +33 -4
  60. package/dist/types/extensibility/plugins/marketplace/cache.d.ts +6 -0
  61. package/dist/types/extensibility/plugins/marketplace/manager.d.ts +186 -0
  62. package/dist/types/extensibility/plugins/marketplace/registry.d.ts +27 -0
  63. package/dist/types/extensibility/plugins/marketplace/source-resolver.d.ts +16 -1
  64. package/dist/types/extensibility/runtime-skill-discovery.d.ts +16 -3
  65. package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +6 -0
  66. package/dist/types/gjc-runtime/launch-worktree.d.ts +53 -0
  67. package/dist/types/gjc-runtime/session-layout.d.ts +8 -0
  68. package/dist/types/gjc-runtime/session-state-lock.d.ts +15 -0
  69. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
  70. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +6 -0
  71. package/dist/types/main.d.ts +19 -1
  72. package/dist/types/modes/acp/acp-agent.d.ts +40 -0
  73. package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
  74. package/dist/types/modes/components/model-selector.d.ts +2 -0
  75. package/dist/types/modes/interactive-mode.d.ts +1 -1
  76. package/dist/types/runtime-mcp/config-writer.d.ts +47 -0
  77. package/dist/types/runtime-mcp/policy-json-edit.d.ts +60 -0
  78. package/dist/types/runtime-mcp/startup-policy.d.ts +4 -0
  79. package/dist/types/runtime-mcp/types.d.ts +2 -2
  80. package/dist/types/sdk/acp/adapter.d.ts +5 -1
  81. package/dist/types/sdk/broker/broker.d.ts +51 -0
  82. package/dist/types/sdk/broker/daemon-entry.d.ts +39 -0
  83. package/dist/types/sdk/broker/discovery.d.ts +38 -0
  84. package/dist/types/sdk/broker/doctor-restart.d.ts +79 -0
  85. package/dist/types/sdk/broker/endpoint-authority.d.ts +7 -0
  86. package/dist/types/sdk/broker/ensure.d.ts +14 -0
  87. package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
  88. package/dist/types/sdk/broker/process-incarnation.d.ts +50 -0
  89. package/dist/types/sdk/broker/runtime.d.ts +26 -0
  90. package/dist/types/sdk/broker/session-index.d.ts +13 -4
  91. package/dist/types/sdk/broker/worktree-occupancy.d.ts +14 -0
  92. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +16 -0
  93. package/dist/types/sdk/bus/chat-daemon-control.d.ts +101 -6
  94. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +16 -0
  95. package/dist/types/sdk/bus/daemon-startup-exclusion.d.ts +43 -0
  96. package/dist/types/sdk/bus/doctor-daemon-restart-client.d.ts +51 -0
  97. package/dist/types/sdk/bus/doctor-daemon-restart.d.ts +39 -0
  98. package/dist/types/sdk/bus/index.d.ts +2 -0
  99. package/dist/types/sdk/bus/notification-service.d.ts +3 -0
  100. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +11 -0
  101. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  102. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -0
  103. package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +1 -0
  104. package/dist/types/sdk/bus/telegram-daemon.d.ts +36 -5
  105. package/dist/types/sdk/cli/session-cli.d.ts +1 -0
  106. package/dist/types/sdk/host/session-runtime.d.ts +4 -0
  107. package/dist/types/sdk/host/surface-policy.d.ts +3 -1
  108. package/dist/types/sdk/lifecycle/service.d.ts +44 -10
  109. package/dist/types/sdk/prompt-deadline-lease.d.ts +6 -3
  110. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -2
  111. package/dist/types/sdk/prompt-failure.d.ts +47 -0
  112. package/dist/types/sdk/prompt-status.d.ts +19 -0
  113. package/dist/types/sdk/router/session-router.d.ts +3 -4
  114. package/dist/types/sdk/service-artifact-paths.d.ts +16 -0
  115. package/dist/types/sdk/session-authority.d.ts +9 -0
  116. package/dist/types/sdk/session.d.ts +3 -1
  117. package/dist/types/session/agent-session.d.ts +1 -1
  118. package/dist/types/session/blob-store.d.ts +1 -1
  119. package/dist/types/session/fallback-chain-controller.d.ts +3 -6
  120. package/dist/types/session/internal/managed-session-scope.d.ts +44 -0
  121. package/dist/types/session/primary-control-surface.d.ts +4 -0
  122. package/dist/types/session/session-manager.d.ts +6 -0
  123. package/dist/types/system-prompt.d.ts +6 -0
  124. package/dist/types/tools/ask.d.ts +25 -0
  125. package/dist/types/tools/image-gen.d.ts +0 -4
  126. package/dist/types/tools/skill-discovery.d.ts +7 -5
  127. package/dist/types/tools/ultragoal-ask-guard.d.ts +7 -4
  128. package/dist/types/utils/changelog.d.ts +0 -9
  129. package/dist/types/utils/mupdf-wasm.d.ts +1 -0
  130. package/package.json +9 -8
  131. package/scripts/compile-args.ts +8 -4
  132. package/src/capability/fs.ts +395 -17
  133. package/src/capability/index.ts +273 -10
  134. package/src/capability/types.ts +9 -0
  135. package/src/cli/customize-doctor.ts +18 -12
  136. package/src/cli/doctor/args.ts +236 -0
  137. package/src/cli/doctor/checks.ts +431 -0
  138. package/src/cli/doctor/config-repairs.ts +517 -0
  139. package/src/cli/doctor/context.ts +149 -0
  140. package/src/cli/doctor/files.ts +149 -0
  141. package/src/cli/doctor/ids.ts +115 -0
  142. package/src/cli/doctor/install-repairs.ts +335 -0
  143. package/src/cli/doctor/journal.ts +423 -0
  144. package/src/cli/doctor/managed-link.ts +514 -0
  145. package/src/cli/doctor/native-probe.ts +13 -0
  146. package/src/cli/doctor/permission-repairs.ts +242 -0
  147. package/src/cli/doctor/plugin-quarantine.ts +377 -0
  148. package/src/cli/doctor/plugin-restore.ts +59 -0
  149. package/src/cli/doctor/plugin-targets.ts +373 -0
  150. package/src/cli/doctor/probe-client.ts +195 -0
  151. package/src/cli/doctor/probe-types.ts +21 -0
  152. package/src/cli/doctor/probe.ts +42 -0
  153. package/src/cli/doctor/projection-probe.ts +249 -0
  154. package/src/cli/doctor/repairs.ts +1157 -0
  155. package/src/cli/doctor/report.ts +176 -0
  156. package/src/cli/doctor/runner.ts +342 -0
  157. package/src/cli/doctor/safety.ts +40 -0
  158. package/src/cli/doctor/service-repairs.ts +452 -0
  159. package/src/cli/doctor/service-targets.ts +274 -0
  160. package/src/cli/doctor/types.ts +164 -0
  161. package/src/cli/doctor-cli.ts +62 -0
  162. package/src/cli/doctor-supervisor.ts +571 -0
  163. package/src/cli/doctor-worker.ts +181 -0
  164. package/src/cli/install-activation.ts +666 -0
  165. package/src/cli/macos-community-app.ts +1449 -0
  166. package/src/cli/mcp-cli.ts +11 -21
  167. package/src/cli/nofile-limit.ts +5 -1
  168. package/src/cli/plugin-cli.ts +6 -1
  169. package/src/cli/update-cli.ts +362 -264
  170. package/src/cli-main.ts +517 -0
  171. package/src/cli.ts +64 -433
  172. package/src/commands/autoresearch.ts +11 -5
  173. package/src/commands/completion.ts +1 -1
  174. package/src/commands/doctor.ts +13 -0
  175. package/src/commands/sdk.ts +58 -10
  176. package/src/commands/ultragoal.ts +11 -5
  177. package/src/config/autorouting-tier-map.ts +7 -0
  178. package/src/config/file-lock.ts +736 -71
  179. package/src/config/mcp-schema.json +3 -3
  180. package/src/config/model-preset-registry.ts +1 -0
  181. package/src/config/model-registry.ts +80 -27
  182. package/src/config/model-resolver.ts +33 -4
  183. package/src/config/models-config-schema.ts +2 -0
  184. package/src/config/settings-schema.ts +6 -2
  185. package/src/coordinator-mcp/question-gate-codec.ts +1 -0
  186. package/src/coordinator-mcp/server.ts +534 -51
  187. package/src/coordinator-mcp/session-reaper.ts +64 -8
  188. package/src/cursor.ts +223 -53
  189. package/src/discovery/agents-md.ts +74 -8
  190. package/src/discovery/agents.ts +32 -7
  191. package/src/discovery/builtin.ts +184 -75
  192. package/src/discovery/claude-plugins.ts +85 -17
  193. package/src/discovery/cline.ts +38 -10
  194. package/src/discovery/cursor.ts +19 -6
  195. package/src/discovery/gemini.ts +62 -22
  196. package/src/discovery/github.ts +13 -3
  197. package/src/discovery/helpers.ts +483 -89
  198. package/src/discovery/index.ts +1 -0
  199. package/src/discovery/mcp-json.ts +36 -10
  200. package/src/discovery/opencode.ts +36 -19
  201. package/src/discovery/ssh.ts +11 -8
  202. package/src/discovery/windsurf.ts +12 -3
  203. package/src/exec/bash-executor.ts +10 -3
  204. package/src/exec/isolated-shell.ts +15 -2
  205. package/src/extensibility/extensions/loader.ts +88 -26
  206. package/src/extensibility/gjc-plugins/lifecycle.ts +189 -0
  207. package/src/extensibility/gjc-plugins/paths.ts +4 -1
  208. package/src/extensibility/gjc-plugins/types.ts +27 -0
  209. package/src/extensibility/plugins/manager.ts +200 -22
  210. package/src/extensibility/plugins/marketplace/cache.ts +55 -2
  211. package/src/extensibility/plugins/marketplace/manager.ts +1262 -162
  212. package/src/extensibility/plugins/marketplace/registry.ts +153 -0
  213. package/src/extensibility/plugins/marketplace/source-resolver.ts +116 -10
  214. package/src/extensibility/runtime-skill-discovery.ts +61 -9
  215. package/src/extensibility/skill-management.ts +7 -1
  216. package/src/gjc-runtime/autoresearch-runtime.ts +10 -1
  217. package/src/gjc-runtime/launch-worktree.test.ts +248 -0
  218. package/src/gjc-runtime/launch-worktree.ts +275 -38
  219. package/src/gjc-runtime/repository-binding.ts +26 -1
  220. package/src/gjc-runtime/session-layout.ts +10 -0
  221. package/src/gjc-runtime/session-state-lock.ts +330 -23
  222. package/src/gjc-runtime/session-state-sidecar.ts +328 -80
  223. package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
  224. package/src/harness-control-plane/session-lease.ts +2 -2
  225. package/src/internal-urls/docs-index.generated.ts +28 -26
  226. package/src/main.ts +150 -29
  227. package/src/memories/index.ts +24 -2
  228. package/src/modes/acp/acp-agent.ts +522 -68
  229. package/src/modes/components/agent-dashboard.ts +3 -1
  230. package/src/modes/components/model-selector.ts +28 -19
  231. package/src/modes/components/plugin-settings.ts +2 -1
  232. package/src/modes/controllers/selector-controller.ts +15 -9
  233. package/src/modes/interactive-mode.ts +21 -3
  234. package/src/prompts/system/system-prompt.md +5 -1
  235. package/src/prompts/tools/browser.md +1 -1
  236. package/src/prompts/tools/move-session.md +4 -0
  237. package/src/prompts/tools/read.md +2 -1
  238. package/src/prompts/tools/skill-discovery.md +6 -5
  239. package/src/prompts/tools/task.md +1 -0
  240. package/src/runtime-mcp/config-writer.ts +228 -0
  241. package/src/runtime-mcp/oauth-discovery.ts +114 -14
  242. package/src/runtime-mcp/policy-json-edit.ts +129 -0
  243. package/src/runtime-mcp/startup-policy.ts +14 -0
  244. package/src/runtime-mcp/types.ts +2 -2
  245. package/src/sdk/acp/adapter.ts +27 -12
  246. package/src/sdk/broker/broker.ts +273 -44
  247. package/src/sdk/broker/daemon-entry.ts +119 -0
  248. package/src/sdk/broker/discovery.ts +173 -20
  249. package/src/sdk/broker/doctor-restart.ts +206 -0
  250. package/src/sdk/broker/endpoint-authority.ts +36 -0
  251. package/src/sdk/broker/ensure.ts +49 -14
  252. package/src/sdk/broker/lifecycle.ts +174 -42
  253. package/src/sdk/broker/process-incarnation.ts +86 -2
  254. package/src/sdk/broker/runtime.ts +145 -21
  255. package/src/sdk/broker/session-index.ts +136 -22
  256. package/src/sdk/broker/spawn-substrate.ts +4 -1
  257. package/src/sdk/broker/transport.ts +5 -0
  258. package/src/sdk/broker/worktree-occupancy.ts +21 -1
  259. package/src/sdk/bus/chat-daemon-cli.ts +63 -0
  260. package/src/sdk/bus/chat-daemon-control.ts +260 -64
  261. package/src/sdk/bus/chat-daemon-runtime.ts +141 -25
  262. package/src/sdk/bus/daemon-startup-exclusion.ts +119 -0
  263. package/src/sdk/bus/doctor-daemon-restart-client.ts +291 -0
  264. package/src/sdk/bus/doctor-daemon-restart.ts +92 -0
  265. package/src/sdk/bus/index.ts +252 -51
  266. package/src/sdk/bus/kind-aware-reconciliation.ts +76 -41
  267. package/src/sdk/bus/notification-service.ts +21 -3
  268. package/src/sdk/bus/reconciliation-store.ts +17 -11
  269. package/src/sdk/bus/telegram-daemon-cli.ts +91 -5
  270. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  271. package/src/sdk/bus/telegram-daemon-control.ts +47 -0
  272. package/src/sdk/bus/telegram-daemon-owner-registry.ts +1 -1
  273. package/src/sdk/bus/telegram-daemon.ts +178 -13
  274. package/src/sdk/cli/session-cli.ts +100 -12
  275. package/src/sdk/host/host.ts +2 -7
  276. package/src/sdk/host/session-runtime.test.ts +329 -8
  277. package/src/sdk/host/session-runtime.ts +202 -50
  278. package/src/sdk/host/surface-policy.ts +8 -1
  279. package/src/sdk/lifecycle/service.ts +199 -22
  280. package/src/sdk/mcp/server.ts +1 -1
  281. package/src/sdk/prompt-deadline-lease.ts +12 -4
  282. package/src/sdk/prompt-deadline-manager.ts +5 -7
  283. package/src/sdk/prompt-failure.ts +148 -0
  284. package/src/sdk/prompt-status.ts +30 -1
  285. package/src/sdk/prompt-watchdog.ts +11 -3
  286. package/src/sdk/protocol/operation-inventory.generated.json +18 -0
  287. package/src/sdk/protocol/operation-registry.ts +20 -8
  288. package/src/sdk/router/session-router.ts +8 -3
  289. package/src/sdk/service-artifact-paths.ts +26 -0
  290. package/src/sdk/session-authority.ts +10 -0
  291. package/src/sdk/session.ts +361 -194
  292. package/src/session/agent-session.ts +398 -93
  293. package/src/session/blob-store.ts +9 -9
  294. package/src/session/fallback-chain-controller.ts +3 -7
  295. package/src/session/internal/managed-session-scope.ts +445 -29
  296. package/src/session/internal/managed-session-storage.ts +38 -6
  297. package/src/session/primary-control-surface.ts +12 -0
  298. package/src/session/session-manager.ts +11 -3
  299. package/src/session/session-storage.ts +1 -1
  300. package/src/session-import/codex.ts +6 -1
  301. package/src/session-import/redact.ts +7 -0
  302. package/src/skill-state/workflow-mutation-guard.ts +645 -39
  303. package/src/slash-commands/builtin-registry.ts +7 -3
  304. package/src/system-prompt.ts +8 -0
  305. package/src/task/executor.ts +36 -3
  306. package/src/task/types.ts +27 -5
  307. package/src/tools/ask.ts +114 -34
  308. package/src/tools/browser/actions.ts +2 -2
  309. package/src/tools/browser/tab-worker.ts +11 -8
  310. package/src/tools/descriptors.ts +6 -1
  311. package/src/tools/fetch.ts +7 -2
  312. package/src/tools/image-gen.ts +33 -34
  313. package/src/tools/read.ts +78 -32
  314. package/src/tools/skill-discovery.ts +17 -7
  315. package/src/tools/tool-catalog.generated.ts +8 -8
  316. package/src/tools/ultragoal-ask-guard.ts +16 -26
  317. package/src/utils/changelog.ts +7 -1
  318. package/src/utils/git.ts +3 -1
  319. package/src/utils/markit.ts +24 -2
  320. package/src/utils/mupdf-wasm.ts +26 -0
  321. package/src/web/search/providers/openai-compatible.ts +191 -14
@@ -142,6 +142,13 @@ export interface SessionIndexEvent {
142
142
  endpointFileId?: string;
143
143
  lifecycleRequestId?: string;
144
144
  terminalUncertain?: boolean;
145
+ /**
146
+ * Distinguishes the terminal-uncertain claim written by a forced stop of a
147
+ * stale-endpoint session from the one written by the fail-closed tail of an
148
+ * ordinary teardown. Only the forced claim releases the session's worktree;
149
+ * see `worktreeOccupant`.
150
+ */
151
+ forcedStaleRelease?: boolean;
145
152
  /** OS process incarnation (C1); absent on legacy v1/v2 events. */
146
153
  hostIncarnation?: string;
147
154
  /** Present on host_heartbeat checkpoints (C2). */
@@ -164,6 +171,8 @@ export interface IndexedSession {
164
171
  indexSeq: number;
165
172
  lifecycleRequestId?: string;
166
173
  terminalUncertain?: boolean;
174
+ /** True only for a terminal-uncertain claim written by a forced stale-worktree release. */
175
+ forcedStaleRelease?: boolean;
167
176
  hostIncarnation?: string;
168
177
  identityProvenance: SessionIdentityProvenance;
169
178
  activity?: SessionActivity;
@@ -598,6 +607,7 @@ function projectIdentity(
598
607
  endpointFileId: latest.endpointFileId,
599
608
  lifecycleRequestId: latest.lifecycleRequestId,
600
609
  terminalUncertain,
610
+ ...(latest.forcedStaleRelease === true ? { forcedStaleRelease: true } : {}),
601
611
  indexSeq: latest.indexSeq,
602
612
  hostIncarnation: latest.hostIncarnation,
603
613
  masterRole: latest.masterRole,
@@ -879,21 +889,27 @@ function compactEvents(events: SessionIndexEvent[], policy: ResolvedRetentionPol
879
889
  }
880
890
  if (policy.maxRows >= 1 && kept.length > policy.maxRows) {
881
891
  const keptLatest = new Map<string, SessionIndexEvent>();
892
+ const rowsBySession = new Map<string, number>();
882
893
  for (const event of kept) {
883
894
  const previous = keptLatest.get(event.sessionId);
884
895
  if (previous === undefined || event.indexSeq > previous.indexSeq) keptLatest.set(event.sessionId, event);
896
+ rowsBySession.set(event.sessionId, (rowsBySession.get(event.sessionId) ?? 0) + 1);
885
897
  }
886
898
  const anchorSession = kept.find(event => event.indexSeq === maxIndexSeq)?.sessionId;
887
899
  const candidates = [...keptLatest.entries()]
888
900
  .filter(([sessionId]) => sessionId !== anchorSession)
889
901
  .filter(([, latest]) => !(policy.tombstoneRule === "retain" && latest.type === "session_deleted"))
890
902
  .sort((a, b) => a[1].indexSeq - b[1].indexSeq);
891
- let result = kept;
903
+ let remaining = kept.length;
904
+ const evicted = new Set<string>();
892
905
  for (const [sessionId] of candidates) {
893
- if (result.length <= policy.maxRows) break;
894
- result = result.filter(event => event.sessionId !== sessionId);
906
+ if (remaining <= policy.maxRows) break;
907
+ evicted.add(sessionId);
908
+ remaining -= rowsBySession.get(sessionId)!;
895
909
  }
896
- return result;
910
+ // Evict whole sessions in the same order without repeatedly copying the
911
+ // surviving history for every victim while rotation holds the index lock.
912
+ return kept.filter(event => !evicted.has(event.sessionId));
897
913
  }
898
914
  return kept;
899
915
  }
@@ -1233,8 +1249,59 @@ export class SessionIndex {
1233
1249
  // captured here (refresh() does this via #refreshUnderLock).
1234
1250
  this.#changeStamp = await readIndexChangeStamp(this.#agentDir);
1235
1251
  }
1252
+ /**
1253
+ * Parse/checksum history before contending for the global lock (#5438). Under
1254
+ * the lock, prove both file identities and the exact prepared bytes still
1255
+ * match. Growth is safe only after comparing the whole prefix, not merely
1256
+ * its size/mtime: a same-inode rewrite followed by append must replay.
1257
+ * There is no optimistic retry loop; replacement or corruption falls back
1258
+ * to the authoritative locked replay and existing quarantine repair.
1259
+ */
1260
+ async #replayPreparedAppendUnderLock(scan: SessionIndexScan, prior: SessionIndexChangeStamp): Promise<void> {
1261
+ const stamp = await readIndexChangeStamp(this.#agentDir);
1262
+ if (
1263
+ scan.diagnosis.status !== "healthy" ||
1264
+ stamp.snapshot.exists !== prior.snapshot.exists ||
1265
+ stamp.snapshot.ino !== prior.snapshot.ino ||
1266
+ stamp.log.exists !== prior.log.exists ||
1267
+ stamp.log.ino !== prior.log.ino
1268
+ ) {
1269
+ await this.#replayUnderLock();
1270
+ return;
1271
+ }
1272
+ const read = async (file: string): Promise<Buffer | undefined> => {
1273
+ try {
1274
+ return await fs.readFile(file);
1275
+ } catch (error) {
1276
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
1277
+ throw error;
1278
+ }
1279
+ };
1280
+ const [snapshot, log] = await Promise.all([read(snapshotFor(this.#agentDir)), read(logFor(this.#agentDir))]);
1281
+ const sameSnapshot =
1282
+ snapshot === undefined
1283
+ ? scan.snapshotContents === undefined
1284
+ : scan.snapshotContents !== undefined && snapshot.equals(scan.snapshotContents);
1285
+ const samePrefix =
1286
+ log === undefined
1287
+ ? scan.logContents === undefined
1288
+ : scan.logContents !== undefined &&
1289
+ log.length >= scan.logContents.length &&
1290
+ log.subarray(0, scan.logContents.length).equals(scan.logContents);
1291
+ if (!sameSnapshot || !samePrefix) {
1292
+ await this.#replayUnderLock();
1293
+ return;
1294
+ }
1295
+ this.#adoptScan(scan);
1296
+ if ((log?.length ?? 0) > this.#logOffset) await this.#tailUnderLock(this.indexSeq, true, true);
1297
+ }
1236
1298
  async #replayUnderLock(): Promise<void> {
1237
1299
  const scan = await this.#scan();
1300
+ this.#adoptScan(scan);
1301
+ await this.#writeAuditUnderLock();
1302
+ this.#changeStamp = await readIndexChangeStamp(this.#agentDir);
1303
+ }
1304
+ #adoptScan(scan: SessionIndexScan): void {
1238
1305
  if (scan.diagnosis.status === "unsupported") throw scan.unsupportedError!;
1239
1306
  this.#events = [...scan.snapshotEvents, ...scan.validLogEvents];
1240
1307
  this.#warnings = [];
@@ -1245,8 +1312,6 @@ export class SessionIndex {
1245
1312
  this.#corruptSuffix = scan.diagnosis.status === "corrupt";
1246
1313
  if (scan.diagnosis.reason === "invalid snapshot") this.#warnings.push("Invalid session index snapshot");
1247
1314
  if (this.#corruptSuffix) this.#warnings.push("Corrupt session index entry; replay truncated");
1248
- await this.#writeAuditUnderLock();
1249
- this.#changeStamp = await readIndexChangeStamp(this.#agentDir);
1250
1315
  }
1251
1316
  /** Seed the audit dedupe set once, then append records for newly-rejected events. */
1252
1317
  async #writeAuditUnderLock(): Promise<void> {
@@ -1472,7 +1537,7 @@ export class SessionIndex {
1472
1537
  await this.#replayUnderLock();
1473
1538
  return { ...scan.diagnosis, repaired: true, quarantinePath };
1474
1539
  }
1475
- async #tailUnderLock(snapshotSeq = this.indexSeq, allowResync = true): Promise<void> {
1540
+ async #tailUnderLock(snapshotSeq = this.indexSeq, allowResync = true, strictSequence = false): Promise<void> {
1476
1541
  let data: Buffer;
1477
1542
  try {
1478
1543
  const handle = await fs.open(logFor(this.#agentDir), "r");
@@ -1508,10 +1573,19 @@ export class SessionIndex {
1508
1573
  corrupt = true;
1509
1574
  continue;
1510
1575
  }
1511
- if (corrupt || event.indexSeq <= snapshotSeq) continue;
1576
+ // Valid JSON need not be an event object. In particular, null must
1577
+ // reach replay/repair rather than throw while reading its fields.
1578
+ if (event === null || typeof event !== "object" || Array.isArray(event)) {
1579
+ corrupt = true;
1580
+ continue;
1581
+ }
1582
+ if (corrupt || (!strictSequence && event.indexSeq <= snapshotSeq)) continue;
1512
1583
  const { checksum, ...unsigned } = event;
1513
1584
  if (checksum !== sessionIndexChecksum(unsigned) || event.indexSeq !== this.indexSeq + 1) corrupt = true;
1514
- else this.#events.push(event);
1585
+ else {
1586
+ this.#events.push(event);
1587
+ if (!hasSessionLocatorV2(event)) this.#warn(legacyLocatorDiagnostic(event));
1588
+ }
1515
1589
  }
1516
1590
  if (hasUnterminatedSuffix) corrupt = true;
1517
1591
  if (corrupt) {
@@ -1563,8 +1637,10 @@ export class SessionIndex {
1563
1637
  input.pid > 0
1564
1638
  )
1565
1639
  ownIncarnation = input.processIncarnation ?? processIncarnation(input.pid);
1640
+ const preparedStamp = await readIndexChangeStamp(this.#agentDir);
1641
+ const preparedScan = await this.#scan();
1566
1642
  return await withSessionIndexLock("append", this.#agentDir, async () => {
1567
- await this.#replayUnderLock();
1643
+ await this.#replayPreparedAppendUnderLock(preparedScan, preparedStamp);
1568
1644
  if (this.#corruptSuffix) {
1569
1645
  // A corrupt suffix used to hard-fail every append until a human ran
1570
1646
  // `gjc gc --repair-session-index` — but the writers that corrupt the
@@ -1596,14 +1672,36 @@ export class SessionIndex {
1596
1672
  if (ownIncarnation !== undefined && unsigned.hostIncarnation === undefined)
1597
1673
  unsigned.hostIncarnation = ownIncarnation;
1598
1674
  const event: SessionIndexEvent = { ...unsigned, checksum: sessionIndexChecksum(unsigned) };
1599
- await appendSync(logFor(this.#agentDir), JSON.stringify(event));
1600
- await this.#refreshUnderLock();
1601
- if ((await fs.stat(logFor(this.#agentDir))).size >= ROTATE_BYTES) await this.#rotate();
1675
+ const serialized = JSON.stringify(event);
1676
+ // Publish to memory only after the complete append and fsync succeed.
1677
+ // No need to reread/checksum the row this lock holder just wrote.
1678
+ await appendSync(logFor(this.#agentDir), serialized);
1679
+ // Keep caller-owned input/return objects separate from durable state.
1680
+ this.#events.push(JSON.parse(serialized) as SessionIndexEvent);
1681
+ this.#logOffset += Buffer.byteLength(serialized) + 1;
1682
+ if (!hasSessionLocatorV2(event)) this.#warn(legacyLocatorDiagnostic(event));
1683
+ await this.#writeAuditUnderLock();
1684
+ if (this.#logOffset >= ROTATE_BYTES) await this.#rotate();
1685
+ this.#changeStamp = await readIndexChangeStamp(this.#agentDir);
1602
1686
  return event;
1603
1687
  });
1604
1688
  });
1605
1689
  }
1606
- async unregisterIfCurrent(expected: IndexedSession): Promise<boolean> {
1690
+ async unregisterIfCurrent(
1691
+ expected: Pick<
1692
+ SessionIndexEvent,
1693
+ | "sessionId"
1694
+ | "locator"
1695
+ | "endpointGeneration"
1696
+ | "pid"
1697
+ | "indexSeq"
1698
+ | "endpointMtimeMs"
1699
+ | "endpointFileId"
1700
+ | "lifecycleRequestId"
1701
+ | "processIncarnation"
1702
+ | "hostIncarnation"
1703
+ >,
1704
+ ): Promise<boolean> {
1607
1705
  const indexPath = path.resolve(logFor(this.#agentDir));
1608
1706
  return await SessionIndex.#enqueue(indexPath, async () => {
1609
1707
  await fs.mkdir(dirFor(this.#agentDir), { recursive: true, mode: 0o700 });
@@ -1632,6 +1730,7 @@ export class SessionIndex {
1632
1730
  session.endpointGeneration === expected.endpointGeneration &&
1633
1731
  session.pid === expected.pid &&
1634
1732
  session.endpointMtimeMs === expected.endpointMtimeMs &&
1733
+ session.endpointFileId === expected.endpointFileId &&
1635
1734
  session.lifecycleRequestId === expected.lifecycleRequestId &&
1636
1735
  session.processIncarnation === expected.processIncarnation &&
1637
1736
  (session.hostIncarnation ?? session.processIncarnation) ===
@@ -1678,6 +1777,7 @@ export class SessionIndex {
1678
1777
  : { processIncarnation: expected.processIncarnation }),
1679
1778
  ...(expected.hostIncarnation === undefined ? {} : { hostIncarnation: expected.hostIncarnation }),
1680
1779
  ...(expected.endpointMtimeMs === undefined ? {} : { endpointMtimeMs: expected.endpointMtimeMs }),
1780
+ ...(expected.endpointFileId === undefined ? {} : { endpointFileId: expected.endpointFileId }),
1681
1781
  ...(expected.lifecycleRequestId === undefined
1682
1782
  ? {}
1683
1783
  : { lifecycleRequestId: expected.lifecycleRequestId }),
@@ -1747,15 +1847,23 @@ export class SessionIndex {
1747
1847
  );
1748
1848
  }
1749
1849
  async #rotate(): Promise<void> {
1750
- await this.#snapshotUnderLock();
1751
- const file = logFor(this.#agentDir);
1752
- await replaceAtomically(file, "");
1850
+ // Every caller has already replayed/refreshed under this same lock.
1851
+ // Replaying before snapshot and again after rotation needlessly parses
1852
+ // the entire history multiple times in the append critical section.
1853
+ const events = compactEvents(this.#events, this.#policy);
1854
+ await replaceAtomically(
1855
+ snapshotFor(this.#agentDir),
1856
+ JSON.stringify({ version: SESSION_INDEX_SNAPSHOT_VERSION, indexSeq: this.indexSeq, events }),
1857
+ );
1858
+ await replaceAtomically(logFor(this.#agentDir), "");
1859
+ this.#events = events;
1753
1860
  this.#logOffset = 0;
1754
- // Sync in-memory state with the compacted snapshot while the lock is
1755
- // held: without this the instance keeps pre-compaction events and a
1756
- // fresh change stamp, so later refreshIfChanged() polls would never
1757
- // observe the compaction (#4689 review).
1758
- await this.#replayUnderLock();
1861
+ this.#corruptSuffix = false;
1862
+ this.#warnings = [];
1863
+ for (const event of events) {
1864
+ if (!hasSessionLocatorV2(event)) this.#warn(legacyLocatorDiagnostic(event));
1865
+ }
1866
+ this.#changeStamp = await readIndexChangeStamp(this.#agentDir);
1759
1867
  }
1760
1868
 
1761
1869
  listSessions(): SessionList {
@@ -2002,6 +2110,7 @@ export class SessionIndex {
2002
2110
  | "processIncarnation"
2003
2111
  | "hostIncarnation"
2004
2112
  | "endpointMtimeMs"
2113
+ | "endpointFileId"
2005
2114
  | "lifecycleRequestId"
2006
2115
  >,
2007
2116
  ): { type: "host_unregistered" | "session_closed" | "session_deleted"; indexSeq: number } | undefined {
@@ -2023,6 +2132,7 @@ export class SessionIndex {
2023
2132
  event.type === "session_closed" ||
2024
2133
  event.type === "session_deleted") &&
2025
2134
  (event.hostIncarnation ?? event.processIncarnation) === expectedIncarnation &&
2135
+ event.endpointFileId === expected.endpointFileId &&
2026
2136
  (supersededAt === undefined || event.indexSeq < supersededAt) &&
2027
2137
  !followsApplicableTombstone(
2028
2138
  this.#events,
@@ -2071,6 +2181,7 @@ export class SessionIndex {
2071
2181
  | "processIncarnation"
2072
2182
  | "hostIncarnation"
2073
2183
  | "endpointMtimeMs"
2184
+ | "endpointFileId"
2074
2185
  | "lifecycleRequestId"
2075
2186
  >,
2076
2187
  ): number | undefined {
@@ -2088,6 +2199,7 @@ export class SessionIndex {
2088
2199
  return this.#events.findLast(
2089
2200
  event =>
2090
2201
  event.type === "session_closed" &&
2202
+ event.endpointFileId === expected.endpointFileId &&
2091
2203
  (supersededAt === undefined || event.indexSeq < supersededAt) &&
2092
2204
  !followsApplicableTombstone(
2093
2205
  this.#events,
@@ -2236,6 +2348,7 @@ export class SessionIndex {
2236
2348
  | "lifecycleRequestId"
2237
2349
  | "hostIncarnation"
2238
2350
  | "processIncarnation"
2351
+ | "endpointFileId"
2239
2352
  >,
2240
2353
  ): { indexSeq: number; lifecycleRequestId?: string } | undefined {
2241
2354
  const lifecycleRequestId = registration.lifecycleRequestId;
@@ -2247,6 +2360,7 @@ export class SessionIndex {
2247
2360
  item.sessionId === registration.sessionId &&
2248
2361
  item.endpointGeneration === registration.endpointGeneration &&
2249
2362
  item.pid === registration.pid &&
2363
+ item.endpointFileId === registration.endpointFileId &&
2250
2364
  resolveEquivalentPath(item.locator.cwd) === resolveEquivalentPath(registration.locator.cwd) &&
2251
2365
  path.resolve(item.locator.stateRoot) === path.resolve(registration.locator.stateRoot) &&
2252
2366
  (lifecycleRequestId === undefined || item.lifecycleRequestId === lifecycleRequestId) &&
@@ -13,7 +13,7 @@ import {
13
13
  type ManagedTmuxLaunchProof,
14
14
  verifyManagedGjcTmuxSession,
15
15
  } from "../../gjc-runtime/tmux-sessions";
16
- import { processIncarnation } from "./process-incarnation";
16
+ import { observeProcessIncarnation, processIncarnation } from "./process-incarnation";
17
17
  import type { SpawnSubstrateLaunchSpec, SpawnSubstrateProof, SpawnSubstrateProvider } from "./spawn-authority";
18
18
 
19
19
  export type SpawnMultiplexerSelection = "none" | "tmux" | "psmux" | "proof_failed";
@@ -249,6 +249,9 @@ function startHeadless(spec: SpawnSubstrateLaunchSpec, env: NodeJS.ProcessEnv):
249
249
  }
250
250
 
251
251
  function defaultIsProcessGone(pid: number): boolean {
252
+ const observation = observeProcessIncarnation(pid);
253
+ if (observation.status === "absent") return true;
254
+ if (observation.status === "present") return false;
252
255
  try {
253
256
  process.kill(pid, 0);
254
257
  return false;
@@ -15,7 +15,12 @@ const BROKER_OPERATIONS = new Set([
15
15
  "session.close",
16
16
  "session.delete",
17
17
  "session.reconcile_uncertain",
18
+ "session.lookup",
18
19
  "broker.shutdown",
20
+ "broker.status",
21
+ "broker.prepare_restart",
22
+ "broker.commit_restart",
23
+ "broker.cancel_restart",
19
24
  "session.control",
20
25
  "broker.lookup_lifecycle",
21
26
  "model.resolve",
@@ -67,6 +67,20 @@ export function observeProcess(
67
67
  * `uncertain` counts as occupied: refusing a launch is recoverable by picking
68
68
  * another worktree name, whereas two live sessions sharing a checkout corrupts
69
69
  * work already done.
70
+ *
71
+ * A row carrying BOTH `terminalUncertain` and `forcedStaleRelease` is the one
72
+ * exception. That pair is written only by the forced stop of a stale-endpoint
73
+ * session: teardown was requested explicitly, so parking its worktree until an
74
+ * OS probe happens to prove exit would leave the checkout locked indefinitely
75
+ * (#5581) exactly when the probe returns `uncertain`. Such a row is released
76
+ * regardless of the process observation.
77
+ *
78
+ * `terminalUncertain` on its own is not enough. The fail-closed tail of a
79
+ * signal-escalated teardown sets it after SIGKILL without ever proving the child
80
+ * gone, so that process may still be running in the checkout; releasing on the
81
+ * flag alone would let a second session into the same worktree. Those rows stay
82
+ * under the strict "only definitive exit releases" rule like every other
83
+ * ordinary retained row.
70
84
  */
71
85
  export function worktreeOccupant(
72
86
  sessions: readonly IndexedSession[],
@@ -79,7 +93,13 @@ export function worktreeOccupant(
79
93
  const target = resolveEquivalentPath(worktreePath);
80
94
  for (const session of sessions) {
81
95
  const sessionWorktreeRoot = session.locator.worktreeRoot;
82
- if (session.terminal || typeof sessionWorktreeRoot !== "string" || sessionWorktreeRoot.length === 0) continue;
96
+ if (
97
+ session.terminal ||
98
+ (session.terminalUncertain === true && session.forcedStaleRelease === true) ||
99
+ typeof sessionWorktreeRoot !== "string" ||
100
+ sessionWorktreeRoot.length === 0
101
+ )
102
+ continue;
83
103
  if (resolveEquivalentPath(sessionWorktreeRoot) !== target) continue;
84
104
  // `live` is heartbeat-derived and can be stale. Only positive process-exit
85
105
  // evidence releases a matching retained session's worktree.
@@ -10,9 +10,11 @@ import {
10
10
  type ChatDaemonKind,
11
11
  chatDaemonGeneration,
12
12
  clearChatDaemonControlRequest,
13
+ clearChatDoctorControlRequest,
13
14
  hasSafeChatDaemonStateShape,
14
15
  readChatDaemonControlRequest,
15
16
  readChatDaemonState,
17
+ readChatDoctorControlRequest,
16
18
  releaseChatDaemonOwnership,
17
19
  renewChatDaemonHeartbeat,
18
20
  } from "./chat-daemon-control";
@@ -24,6 +26,7 @@ import {
24
26
  notificationConfigFromFile,
25
27
  resolveNotificationProvider,
26
28
  } from "./config";
29
+ import { doctorDaemonIdentityMatches, doctorDaemonOccupancySettled } from "./doctor-daemon-restart";
27
30
 
28
31
  export interface ChatDaemonRuntimeHandle {
29
32
  start(): Promise<void>;
@@ -31,6 +34,26 @@ export interface ChatDaemonRuntimeHandle {
31
34
  transportHealthy?(): boolean;
32
35
  /** Executes operator commands that must run inside the owning daemon. */
33
36
  bindExistingRoot?(request: ChatDaemonCommandBindInput): Promise<ChatDaemonCommandOutcome>;
37
+ prepareDoctorRestart?(): void;
38
+ cancelDoctorRestart?(): void;
39
+ doctorOccupancy?(): {
40
+ attached: number;
41
+ inflight: number;
42
+ inbound: number;
43
+ outbound: number;
44
+ cleanup: number;
45
+ };
46
+ doctorRestartReady?(): boolean;
47
+ doctorRestartStatus?(
48
+ identity: {
49
+ owner: "discord" | "slack";
50
+ ownerId: string;
51
+ generation: number;
52
+ incarnation: string;
53
+ },
54
+ requestId: string,
55
+ leaseExpiresAt: number,
56
+ ): unknown;
34
57
  }
35
58
 
36
59
  export interface RunChatDaemonInternalDeps {
@@ -237,7 +260,47 @@ export async function runChatDaemonInternal(
237
260
  );
238
261
  };
239
262
  const bindExistingRoot = activeRuntime.bindExistingRoot?.bind(activeRuntime);
263
+ let doctorPreparedRequestId: string | undefined;
240
264
  while (!stopping) {
265
+ const doctorRequest = await readChatDoctorControlRequest(agentDir, kind);
266
+ if (doctorRequest) {
267
+ const identity = {
268
+ owner: kind,
269
+ ownerId,
270
+ generation: chatDaemonGeneration(kind),
271
+ incarnation: ownedIncarnation,
272
+ } as const;
273
+ // The shared predicate is the single definition of "this request addresses
274
+ // exactly me"; spelling the four fields out here would let this owner and
275
+ // the doctor client drift apart silently.
276
+ if (
277
+ doctorDaemonIdentityMatches(doctorRequest, identity) &&
278
+ (doctorRequest.leaseExpiresAt === undefined || doctorRequest.leaseExpiresAt > Date.now())
279
+ ) {
280
+ if (doctorRequest.action === "prepare") {
281
+ activeRuntime.prepareDoctorRestart?.();
282
+ doctorPreparedRequestId = doctorRequest.requestId;
283
+ } else if (doctorRequest.action === "cancel" && doctorPreparedRequestId === doctorRequest.requestId) {
284
+ activeRuntime.cancelDoctorRestart?.();
285
+ await clearChatDoctorControlRequest(agentDir, kind, doctorRequest.requestId);
286
+ doctorPreparedRequestId = undefined;
287
+ } else if (doctorRequest.action === "commit" && doctorPreparedRequestId === doctorRequest.requestId) {
288
+ const occupancy = activeRuntime.doctorOccupancy?.();
289
+ if (
290
+ occupancy !== undefined &&
291
+ doctorDaemonOccupancySettled(occupancy) &&
292
+ activeRuntime.doctorRestartReady?.()
293
+ ) {
294
+ // Keep committed intent durable until the restart
295
+ // coordinator proves old death/cleanup and successor
296
+ // publication.
297
+ stopping = true;
298
+ await stopRuntime();
299
+ doctorPreparedRequestId = undefined;
300
+ }
301
+ }
302
+ }
303
+ }
241
304
  const request = await readChatDaemonControlRequest(agentDir, kind);
242
305
  if (request?.ownerId === ownerId && request.incarnation === incarnation) {
243
306
  await clearChatDaemonControlRequest(agentDir, kind, request.requestId);