@gajae-code/coding-agent 0.13.2 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (610) hide show
  1. package/CHANGELOG.md +215 -8
  2. package/README.md +2 -0
  3. package/bin/gajaessi.js +4 -0
  4. package/dist/types/async/job-manager.d.ts +55 -2
  5. package/dist/types/async/support.d.ts +14 -0
  6. package/dist/types/capability/mcp.d.ts +6 -0
  7. package/dist/types/capability/types.d.ts +6 -0
  8. package/dist/types/cli/accounts-cli.d.ts +19 -0
  9. package/dist/types/cli/args.d.ts +3 -0
  10. package/dist/types/cli/config-cli.d.ts +6 -0
  11. package/dist/types/cli/crash-cli.d.ts +10 -0
  12. package/dist/types/cli/customize-doctor.d.ts +145 -0
  13. package/dist/types/cli/daemon-cli.d.ts +3 -1
  14. package/dist/types/cli/mcp-cli.d.ts +11 -0
  15. package/dist/types/cli/notify-cli.d.ts +1 -1
  16. package/dist/types/cli/root-flags.d.ts +12 -0
  17. package/dist/types/cli/setup-cli.d.ts +3 -1
  18. package/dist/types/cli/skills-cli.d.ts +2 -1
  19. package/dist/types/cli/update-cli.d.ts +19 -1
  20. package/dist/types/cli.d.ts +6 -0
  21. package/dist/types/commands/accounts.d.ts +40 -0
  22. package/dist/types/commands/crash.d.ts +24 -0
  23. package/dist/types/commands/customize.d.ts +22 -0
  24. package/dist/types/commands/daemon.d.ts +1 -1
  25. package/dist/types/commands/launch.d.ts +6 -0
  26. package/dist/types/commands/setup.d.ts +6 -0
  27. package/dist/types/commands/skills.d.ts +6 -1
  28. package/dist/types/commands/update.d.ts +7 -0
  29. package/dist/types/config/atomic-yaml-patch-worker.d.ts +25 -0
  30. package/dist/types/config/atomic-yaml-patch.d.ts +62 -2
  31. package/dist/types/config/config-schema-version.d.ts +10 -0
  32. package/dist/types/config/file-lock.d.ts +18 -0
  33. package/dist/types/config/model-profile-activation.d.ts +3 -1
  34. package/dist/types/config/model-registry.d.ts +10 -1
  35. package/dist/types/config/models-config-schema.d.ts +2 -0
  36. package/dist/types/config/settings-schema.d.ts +73 -9
  37. package/dist/types/config/settings.d.ts +63 -0
  38. package/dist/types/config/skill-settings-defaults.d.ts +26 -0
  39. package/dist/types/config/task-delegation.d.ts +39 -0
  40. package/dist/types/config/update-channel.d.ts +10 -0
  41. package/dist/types/coordinator-mcp/server.d.ts +5 -1
  42. package/dist/types/crash/index-store.d.ts +87 -0
  43. package/dist/types/crash/nudge.d.ts +50 -0
  44. package/dist/types/crash/record-loader.d.ts +17 -0
  45. package/dist/types/crash/report.d.ts +134 -0
  46. package/dist/types/crash/sanitize.d.ts +25 -0
  47. package/dist/types/cursor.d.ts +13 -2
  48. package/dist/types/customization/import.d.ts +37 -0
  49. package/dist/types/customization/index.d.ts +4 -0
  50. package/dist/types/customization/inventory.d.ts +25 -0
  51. package/dist/types/customization/mutations.d.ts +32 -0
  52. package/dist/types/customization/types.d.ts +115 -0
  53. package/dist/types/daemon/control-types.d.ts +8 -1
  54. package/dist/types/daemon/operator-contract.d.ts +4 -4
  55. package/dist/types/discovery/claude.d.ts +39 -1
  56. package/dist/types/discovery/codex.d.ts +39 -1
  57. package/dist/types/discovery/index.d.ts +2 -0
  58. package/dist/types/discovery/mcp-compat.d.ts +30 -0
  59. package/dist/types/edit/renderer.d.ts +23 -0
  60. package/dist/types/extensibility/extensions/runner.d.ts +3 -0
  61. package/dist/types/extensibility/extensions/types.d.ts +21 -4
  62. package/dist/types/extensibility/gjc-plugins/observability.d.ts +6 -1
  63. package/dist/types/extensibility/gjc-plugins/registry.d.ts +8 -1
  64. package/dist/types/extensibility/hooks/loader.d.ts +19 -5
  65. package/dist/types/extensibility/runtime-skill-discovery.d.ts +22 -1
  66. package/dist/types/extensibility/skill-management.d.ts +107 -0
  67. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +3 -1
  68. package/dist/types/gjc-runtime/launch-worktree.d.ts +8 -0
  69. package/dist/types/gjc-runtime/linux-proc.d.ts +3 -0
  70. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +10 -6
  71. package/dist/types/gjc-runtime/session-state-lock.d.ts +104 -0
  72. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +120 -2
  73. package/dist/types/gjc-runtime/state-writer.d.ts +10 -1
  74. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -2
  75. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -6
  76. package/dist/types/gjc-runtime/workflow-settings.d.ts +109 -0
  77. package/dist/types/hooks/events.d.ts +60 -0
  78. package/dist/types/hooks/normalize.d.ts +83 -0
  79. package/dist/types/hooks/skill-state.d.ts +9 -2
  80. package/dist/types/main.d.ts +2 -1
  81. package/dist/types/modes/acp/abort-scope.d.ts +13 -0
  82. package/dist/types/modes/acp/acp-agent.d.ts +7 -4
  83. package/dist/types/modes/components/customization/customization-dashboard.d.ts +44 -0
  84. package/dist/types/modes/components/customization/import-wizard.d.ts +21 -0
  85. package/dist/types/modes/components/customization/index.d.ts +1 -0
  86. package/dist/types/modes/components/dynamic-theme-text.d.ts +9 -0
  87. package/dist/types/modes/components/gajae-pet-widget.d.ts +6 -28
  88. package/dist/types/modes/components/irc-sidebar.d.ts +1 -0
  89. package/dist/types/modes/components/iterm-pet-transport.d.ts +122 -0
  90. package/dist/types/modes/components/oauth-selector.d.ts +10 -4
  91. package/dist/types/modes/components/pet-capability.d.ts +7 -21
  92. package/dist/types/modes/components/settings-selector.d.ts +5 -0
  93. package/dist/types/modes/components/status-line/gh.d.ts +6 -2
  94. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  95. package/dist/types/modes/controllers/selector-controller.d.ts +7 -8
  96. package/dist/types/modes/controllers/stt-controller.d.ts +1 -1
  97. package/dist/types/modes/interactive-mode.d.ts +1 -0
  98. package/dist/types/modes/theme/defaults/index.d.ts +118 -0
  99. package/dist/types/modes/types.d.ts +1 -0
  100. package/dist/types/modes/utils/injected-user-submission.d.ts +10 -0
  101. package/dist/types/runtime-mcp/config.d.ts +9 -0
  102. package/dist/types/runtime-mcp/manager.d.ts +26 -1
  103. package/dist/types/runtime-mcp/oauth-discovery.d.ts +4 -0
  104. package/dist/types/runtime-mcp/oauth-flow.d.ts +24 -1
  105. package/dist/types/runtime-mcp/pool-key.d.ts +2 -0
  106. package/dist/types/runtime-mcp/protocol.d.ts +278 -0
  107. package/dist/types/runtime-mcp/scope-config.d.ts +29 -0
  108. package/dist/types/runtime-mcp/tool-bridge.d.ts +15 -13
  109. package/dist/types/runtime-mcp/transports/http.d.ts +9 -0
  110. package/dist/types/runtime-mcp/types.d.ts +102 -10
  111. package/dist/types/sdk/acp/adapter.d.ts +26 -5
  112. package/dist/types/sdk/broker/broker.d.ts +3 -55
  113. package/dist/types/sdk/broker/lifecycle.d.ts +18 -12
  114. package/dist/types/sdk/broker/session-index.d.ts +14 -5
  115. package/dist/types/sdk/broker/startup-failure.d.ts +16 -1
  116. package/dist/types/sdk/bus/chat-daemon-control.d.ts +51 -1
  117. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -41
  118. package/dist/types/sdk/bus/config.d.ts +11 -0
  119. package/dist/types/sdk/bus/conversation-store.d.ts +0 -1
  120. package/dist/types/sdk/bus/daemon-paths.d.ts +6 -0
  121. package/dist/types/sdk/bus/discord-conversation.d.ts +2 -0
  122. package/dist/types/sdk/bus/discord-daemon.d.ts +17 -12
  123. package/dist/types/sdk/bus/existing-thread-readiness.d.ts +4 -34
  124. package/dist/types/sdk/bus/inbound-reaction-ordering.d.ts +61 -0
  125. package/dist/types/sdk/bus/index.d.ts +85 -99
  126. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +29 -7
  127. package/dist/types/sdk/bus/managed-daemon.d.ts +2 -32
  128. package/dist/types/sdk/bus/notification-service.d.ts +52 -0
  129. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -1
  130. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +16 -5
  131. package/dist/types/sdk/bus/reconciliation-store.d.ts +113 -5
  132. package/dist/types/sdk/bus/session-control.d.ts +25 -1
  133. package/dist/types/sdk/bus/slack-conversation.d.ts +4 -0
  134. package/dist/types/sdk/bus/slack-daemon.d.ts +9 -17
  135. package/dist/types/sdk/bus/slack-live-provider.d.ts +3 -1
  136. package/dist/types/sdk/bus/slack-provider.d.ts +4 -0
  137. package/dist/types/sdk/bus/slack-thread-binding.d.ts +6 -40
  138. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +30 -104
  139. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +10 -0
  140. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +262 -19
  141. package/dist/types/sdk/bus/telegram-daemon-orphan-reap.d.ts +125 -0
  142. package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +22 -0
  143. package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -310
  144. package/dist/types/sdk/bus/telegram-reference.d.ts +1 -47
  145. package/dist/types/sdk/bus/threaded-render.d.ts +1 -0
  146. package/dist/types/sdk/bus/topic-registry.d.ts +33 -17
  147. package/dist/types/sdk/cli/rows.d.ts +2 -11
  148. package/dist/types/sdk/cli/session-cli.d.ts +15 -25
  149. package/dist/types/sdk/client/client.d.ts +97 -1
  150. package/dist/types/sdk/host/control/dispatch.d.ts +0 -7
  151. package/dist/types/sdk/host/control/index.d.ts +1 -1
  152. package/dist/types/sdk/host/control/operations.d.ts +18 -5
  153. package/dist/types/sdk/host/control/runtime-gate.d.ts +19 -0
  154. package/dist/types/sdk/host/host.d.ts +15 -1
  155. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  156. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  157. package/dist/types/sdk/host/session-runtime.d.ts +127 -7
  158. package/dist/types/sdk/host/types.d.ts +1 -1
  159. package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
  160. package/dist/types/sdk/index.d.ts +2 -1
  161. package/dist/types/sdk/lifecycle/broker-client.d.ts +8 -0
  162. package/dist/types/sdk/lifecycle/client.d.ts +59 -0
  163. package/dist/types/sdk/lifecycle/index.d.ts +3 -0
  164. package/dist/types/sdk/lifecycle/service.d.ts +232 -0
  165. package/dist/types/sdk/mcp/server.d.ts +9 -6
  166. package/dist/types/sdk/prompt-deadline-lease.d.ts +43 -0
  167. package/dist/types/sdk/prompt-deadline-manager.d.ts +28 -0
  168. package/dist/types/sdk/prompt-status.d.ts +10 -3
  169. package/dist/types/sdk/protocol/operation-registry.d.ts +5 -0
  170. package/dist/types/sdk/receipt-state.d.ts +17 -0
  171. package/dist/types/sdk/reconciliation-extensions.d.ts +2 -0
  172. package/dist/types/sdk/router/index.d.ts +1 -0
  173. package/dist/types/sdk/router/session-router.d.ts +152 -0
  174. package/dist/types/sdk/session-list.d.ts +22 -0
  175. package/dist/types/sdk/session-reconnect.d.ts +43 -1
  176. package/dist/types/sdk/session.d.ts +34 -1
  177. package/dist/types/sdk/transport/serve-cli.d.ts +1 -4
  178. package/dist/types/session/account-inventory.d.ts +86 -0
  179. package/dist/types/session/agent-session.d.ts +145 -6
  180. package/dist/types/session/agent-storage.d.ts +10 -0
  181. package/dist/types/session/coordinator-tool-label.d.ts +28 -0
  182. package/dist/types/session/internal/managed-session-storage.d.ts +23 -4
  183. package/dist/types/session/messages.d.ts +1 -1
  184. package/dist/types/session/secure-token-file.d.ts +3 -0
  185. package/dist/types/session/session-manager.d.ts +47 -1
  186. package/dist/types/session/startup-auth-config.d.ts +44 -0
  187. package/dist/types/session/terminal-abort.d.ts +378 -0
  188. package/dist/types/session/yield-queue.d.ts +7 -0
  189. package/dist/types/session-import/claude.d.ts +26 -0
  190. package/dist/types/session-import/codex.d.ts +88 -0
  191. package/dist/types/session-import/command.d.ts +35 -0
  192. package/dist/types/session-import/detect.d.ts +22 -0
  193. package/dist/types/session-import/index.d.ts +7 -0
  194. package/dist/types/session-import/provider-codex.d.ts +22 -0
  195. package/dist/types/session-import/provider-service.d.ts +38 -0
  196. package/dist/types/session-import/redact.d.ts +23 -0
  197. package/dist/types/session-import/service.d.ts +47 -0
  198. package/dist/types/session-import/types.d.ts +120 -0
  199. package/dist/types/setup/paseo/check.d.ts +16 -0
  200. package/dist/types/setup/paseo/install-saga.d.ts +115 -0
  201. package/dist/types/setup/paseo/json-publisher.d.ts +76 -0
  202. package/dist/types/setup/paseo/orchestration-preferences.d.ts +24 -0
  203. package/dist/types/setup/paseo/paseo-ownership.d.ts +87 -0
  204. package/dist/types/setup/paseo/paseo-setup.d.ts +29 -0
  205. package/dist/types/setup/paseo/provider-config.d.ts +35 -0
  206. package/dist/types/setup/paseo/remove.d.ts +14 -0
  207. package/dist/types/setup/paseo/result-types.d.ts +59 -0
  208. package/dist/types/setup/paseo/setup-deps.d.ts +73 -0
  209. package/dist/types/setup/paseo/skills-bridge.d.ts +44 -0
  210. package/dist/types/skill-state/active-state.d.ts +5 -1
  211. package/dist/types/slash-commands/acp-builtins.d.ts +5 -1
  212. package/dist/types/slash-commands/builtin-registry.d.ts +2 -0
  213. package/dist/types/slash-commands/helpers/credential-switch.d.ts +12 -0
  214. package/dist/types/slash-commands/helpers/usage-report.d.ts +5 -6
  215. package/dist/types/slash-commands/types.d.ts +7 -1
  216. package/dist/types/task/executor.d.ts +10 -0
  217. package/dist/types/task/index.d.ts +3 -1
  218. package/dist/types/tools/ask-answer-registry.d.ts +6 -3
  219. package/dist/types/tools/bash-sleep-advisory.d.ts +14 -0
  220. package/dist/types/tools/bash.d.ts +2 -6
  221. package/dist/types/tools/browser/attach.d.ts +11 -0
  222. package/dist/types/tools/browser/launch.d.ts +20 -0
  223. package/dist/types/tools/browser/profile-discovery.d.ts +6 -0
  224. package/dist/types/tools/browser/render.d.ts +1 -0
  225. package/dist/types/tools/browser.d.ts +3 -1
  226. package/dist/types/tools/image-gen.d.ts +10 -2
  227. package/dist/types/tools/index.d.ts +8 -0
  228. package/dist/types/tools/monitor.d.ts +1 -1
  229. package/dist/types/tools/skill-discovery.d.ts +8 -1
  230. package/dist/types/tools/subagent.d.ts +2 -0
  231. package/dist/types/tools/ultragoal-ask-guard.d.ts +2 -2
  232. package/dist/types/utils/herdr-pane.d.ts +76 -0
  233. package/package.json +19 -9
  234. package/scripts/build-binary.ts +2 -1
  235. package/scripts/build-sdk-package-smoke.ts +2 -13
  236. package/scripts/capture-sticky-viewport-showcase.ts +0 -1
  237. package/scripts/compile-args.ts +2 -0
  238. package/scripts/generate-sdk-adapter-parity-manifest.ts +31 -16
  239. package/scripts/generate-sdk-operation-inventory.ts +28 -0
  240. package/scripts/generate-telegram-baseline-manifest.ts +1 -0
  241. package/scripts/generate-tool-catalog.ts +6 -4
  242. package/scripts/verify-gjc-sdk-canonicalization.ts +5 -39
  243. package/src/async/job-manager.ts +352 -17
  244. package/src/async/support.ts +25 -0
  245. package/src/autoresearch/dashboard.ts +28 -13
  246. package/src/capability/index.ts +3 -1
  247. package/src/capability/mcp.ts +6 -0
  248. package/src/capability/types.ts +6 -0
  249. package/src/cli/accounts-cli.ts +395 -0
  250. package/src/cli/args.ts +23 -0
  251. package/src/cli/auth-broker-cli.ts +136 -66
  252. package/src/cli/auth-gateway-cli.ts +94 -70
  253. package/src/cli/config-cli.ts +27 -2
  254. package/src/cli/crash-cli.ts +78 -0
  255. package/src/cli/customize-doctor.ts +1605 -0
  256. package/src/cli/daemon-cli.ts +12 -3
  257. package/src/cli/fast-help.ts +1 -0
  258. package/src/cli/mcp-cli.ts +128 -60
  259. package/src/cli/notify-cli.ts +8 -14
  260. package/src/cli/root-flags.ts +8 -0
  261. package/src/cli/setup-cli.ts +145 -0
  262. package/src/cli/skills-cli.ts +38 -2
  263. package/src/cli/update-cli.ts +167 -12
  264. package/src/cli.ts +4 -0
  265. package/src/commands/accounts.ts +71 -0
  266. package/src/commands/crash.ts +35 -0
  267. package/src/commands/customize.ts +84 -0
  268. package/src/commands/daemon.ts +2 -2
  269. package/src/commands/deep-interview.ts +2 -0
  270. package/src/commands/launch.ts +1 -0
  271. package/src/commands/mcp.ts +12 -7
  272. package/src/commands/ralplan.ts +2 -0
  273. package/src/commands/sdk.ts +65 -32
  274. package/src/commands/setup.ts +5 -0
  275. package/src/commands/skills.ts +14 -4
  276. package/src/commands/ultragoal.ts +10 -1
  277. package/src/commands/update.ts +26 -15
  278. package/src/config/atomic-yaml-patch-worker.ts +57 -0
  279. package/src/config/atomic-yaml-patch.ts +769 -29
  280. package/src/config/config-schema-version.ts +10 -0
  281. package/src/config/file-lock.ts +70 -1
  282. package/src/config/mcp-schema.json +52 -14
  283. package/src/config/model-profile-activation.ts +23 -2
  284. package/src/config/model-profiles.ts +88 -21
  285. package/src/config/model-registry.ts +147 -21
  286. package/src/config/models-config-schema.ts +1 -1
  287. package/src/config/settings-schema.ts +77 -7
  288. package/src/config/settings.ts +4038 -199
  289. package/src/config/skill-settings-defaults.ts +41 -3
  290. package/src/config/task-delegation.ts +87 -0
  291. package/src/config/update-channel.ts +23 -0
  292. package/src/coordinator-mcp/server.ts +377 -308
  293. package/src/crash/index-store.ts +723 -0
  294. package/src/crash/nudge.ts +89 -0
  295. package/src/crash/record-loader.ts +149 -0
  296. package/src/crash/report.ts +551 -0
  297. package/src/crash/sanitize.ts +129 -0
  298. package/src/cursor.ts +36 -10
  299. package/src/customization/import.ts +946 -0
  300. package/src/customization/index.ts +4 -0
  301. package/src/customization/inventory.ts +335 -0
  302. package/src/customization/mutations.ts +141 -0
  303. package/src/customization/types.ts +223 -0
  304. package/src/daemon/control-types.ts +8 -1
  305. package/src/daemon/operator-contract.ts +6 -6
  306. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +31 -6
  307. package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -22
  308. package/src/defaults/gjc/skills/ralplan/SKILL.md +36 -23
  309. package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
  310. package/src/discovery/claude-hooks.ts +38 -0
  311. package/src/discovery/claude.ts +142 -209
  312. package/src/discovery/codex-hooks.ts +38 -0
  313. package/src/discovery/codex.ts +132 -198
  314. package/src/discovery/helpers.ts +8 -1
  315. package/src/discovery/index.ts +2 -0
  316. package/src/discovery/mcp-compat.ts +234 -0
  317. package/src/discovery/mcp-json.ts +6 -0
  318. package/src/edit/renderer.ts +40 -0
  319. package/src/edit/streaming.ts +4 -2
  320. package/src/exec/bash-executor.ts +24 -20
  321. package/src/export/html/index.ts +51 -2
  322. package/src/extensibility/extensions/loader.ts +9 -1
  323. package/src/extensibility/extensions/runner.ts +17 -4
  324. package/src/extensibility/extensions/types.ts +31 -6
  325. package/src/extensibility/gjc-plugins/constrained-hooks.ts +33 -0
  326. package/src/extensibility/gjc-plugins/installer.ts +9 -5
  327. package/src/extensibility/gjc-plugins/observability.ts +8 -2
  328. package/src/extensibility/gjc-plugins/registry.ts +13 -2
  329. package/src/extensibility/gjc-plugins/runtime-adapters.ts +3 -1
  330. package/src/extensibility/gjc-plugins/schema.ts +222 -39
  331. package/src/extensibility/hooks/loader.ts +154 -10
  332. package/src/extensibility/runtime-skill-discovery.ts +257 -60
  333. package/src/extensibility/skill-management.ts +368 -0
  334. package/src/extensibility/skills.ts +52 -16
  335. package/src/gjc-runtime/deep-interview-runtime.ts +45 -61
  336. package/src/gjc-runtime/launch-worktree.ts +56 -2
  337. package/src/gjc-runtime/linux-proc.ts +7 -1
  338. package/src/gjc-runtime/ralplan-runtime.ts +100 -164
  339. package/src/gjc-runtime/session-state-lock.ts +992 -0
  340. package/src/gjc-runtime/session-state-sidecar.ts +644 -142
  341. package/src/gjc-runtime/state-writer.ts +39 -14
  342. package/src/gjc-runtime/tmux-owner-isolation.ts +5 -4
  343. package/src/gjc-runtime/ultragoal-guard.ts +11 -4
  344. package/src/gjc-runtime/ultragoal-runtime.ts +44 -41
  345. package/src/gjc-runtime/workflow-settings.ts +596 -0
  346. package/src/goals/tools/goal-tool.ts +6 -1
  347. package/src/hooks/events.ts +291 -0
  348. package/src/hooks/native-skill-hook.ts +4 -0
  349. package/src/hooks/normalize.ts +369 -0
  350. package/src/hooks/skill-state.ts +165 -46
  351. package/src/internal-urls/docs-index.generated.ts +41 -32
  352. package/src/main.ts +36 -5
  353. package/src/migrate/mcp-mapper.ts +25 -2
  354. package/src/modes/acp/abort-scope.ts +29 -0
  355. package/src/modes/acp/acp-agent.ts +423 -98
  356. package/src/modes/components/assistant-message.ts +6 -1
  357. package/src/modes/components/chrome.ts +4 -3
  358. package/src/modes/components/customization/customization-dashboard.ts +443 -0
  359. package/src/modes/components/customization/import-wizard.ts +372 -0
  360. package/src/modes/components/customization/index.ts +1 -0
  361. package/src/modes/components/dynamic-theme-text.ts +22 -0
  362. package/src/modes/components/gajae-pet-widget.ts +607 -193
  363. package/src/modes/components/irc-sidebar.ts +11 -0
  364. package/src/modes/components/iterm-pet-transport.ts +585 -0
  365. package/src/modes/components/model-selector.ts +1 -6
  366. package/src/modes/components/oauth-selector.ts +277 -86
  367. package/src/modes/components/pet-capability.ts +35 -31
  368. package/src/modes/components/settings-selector.ts +29 -5
  369. package/src/modes/components/status-line/gh.ts +45 -1
  370. package/src/modes/components/status-line/segments.ts +19 -4
  371. package/src/modes/components/tool-status-header.ts +13 -3
  372. package/src/modes/controllers/command-controller.ts +20 -8
  373. package/src/modes/controllers/extension-ui-controller.ts +56 -257
  374. package/src/modes/controllers/input-controller.ts +10 -0
  375. package/src/modes/controllers/runtime-mcp-command-controller.ts +16 -3
  376. package/src/modes/controllers/selector-controller.ts +238 -77
  377. package/src/modes/controllers/stt-controller.ts +3 -3
  378. package/src/modes/interactive-mode.ts +88 -16
  379. package/src/modes/print-mode.ts +35 -6
  380. package/src/modes/runtime-init.ts +21 -68
  381. package/src/modes/theme/defaults/index.ts +2 -0
  382. package/src/modes/theme/defaults/ouroboros.json +118 -0
  383. package/src/modes/types.ts +1 -0
  384. package/src/modes/utils/injected-user-submission.ts +25 -0
  385. package/src/modes/utils/terminal-bell.ts +15 -1
  386. package/src/prompts/agent-fragments/ralplan-persistence.md +1 -1
  387. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  388. package/src/prompts/agents/critic.md +1 -1
  389. package/src/prompts/system/rlm-research.md +1 -1
  390. package/src/prompts/system/system-prompt.md +0 -2
  391. package/src/prompts/tools/bash.md +7 -1
  392. package/src/prompts/tools/browser.md +1 -1
  393. package/src/prompts/tools/cron.md +6 -6
  394. package/src/prompts/tools/read.md +0 -2
  395. package/src/prompts/tools/skill-discovery.md +4 -2
  396. package/src/runtime-mcp/client.ts +427 -23
  397. package/src/runtime-mcp/config.ts +37 -5
  398. package/src/runtime-mcp/manager-pool.test.ts +56 -0
  399. package/src/runtime-mcp/manager.ts +52 -2
  400. package/src/runtime-mcp/oauth-discovery.ts +13 -0
  401. package/src/runtime-mcp/oauth-flow.ts +46 -1
  402. package/src/runtime-mcp/pool-key.ts +5 -1
  403. package/src/runtime-mcp/pool.test.ts +9 -0
  404. package/src/runtime-mcp/protocol.test.ts +353 -0
  405. package/src/runtime-mcp/protocol.ts +624 -0
  406. package/src/runtime-mcp/scope-config.ts +78 -0
  407. package/src/runtime-mcp/tool-bridge.ts +44 -10
  408. package/src/runtime-mcp/transports/http.ts +84 -13
  409. package/src/runtime-mcp/types.ts +123 -12
  410. package/src/sdk/acp/adapter.ts +328 -97
  411. package/src/sdk/broker/broker.ts +160 -741
  412. package/src/sdk/broker/ensure.ts +9 -1
  413. package/src/sdk/broker/lifecycle.ts +510 -116
  414. package/src/sdk/broker/operation-receipt-ledger.ts +11 -2
  415. package/src/sdk/broker/process-incarnation.ts +19 -0
  416. package/src/sdk/broker/session-index.ts +516 -117
  417. package/src/sdk/broker/startup-failure.ts +28 -5
  418. package/src/sdk/broker/transport.ts +1 -13
  419. package/src/sdk/bus/chat-daemon-cli.ts +4 -2
  420. package/src/sdk/bus/chat-daemon-control.ts +67 -6
  421. package/src/sdk/bus/chat-daemon-runtime.ts +183 -859
  422. package/src/sdk/bus/config.ts +13 -0
  423. package/src/sdk/bus/conversation-store.ts +59 -171
  424. package/src/sdk/bus/daemon-paths.ts +20 -0
  425. package/src/sdk/bus/discord-conversation.ts +2 -0
  426. package/src/sdk/bus/discord-daemon.ts +524 -126
  427. package/src/sdk/bus/discord-live-provider.ts +206 -79
  428. package/src/sdk/bus/existing-thread-readiness.ts +4 -97
  429. package/src/sdk/bus/inbound-reaction-ordering.ts +122 -0
  430. package/src/sdk/bus/index.ts +2480 -456
  431. package/src/sdk/bus/kind-aware-reconciliation.ts +204 -34
  432. package/src/sdk/bus/lifecycle-commands.ts +2 -2
  433. package/src/sdk/bus/managed-daemon.ts +2 -145
  434. package/src/sdk/bus/notification-service.ts +196 -0
  435. package/src/sdk/bus/operator-runtime.ts +1 -1
  436. package/src/sdk/bus/prompt-reconciliation.ts +24 -15
  437. package/src/sdk/bus/reconciliation-store.ts +553 -24
  438. package/src/sdk/bus/session-control.ts +49 -3
  439. package/src/sdk/bus/slack-conversation.ts +4 -0
  440. package/src/sdk/bus/slack-daemon.ts +733 -134
  441. package/src/sdk/bus/slack-live-provider.ts +65 -15
  442. package/src/sdk/bus/slack-provider.ts +4 -0
  443. package/src/sdk/bus/slack-thread-binding.ts +9 -104
  444. package/src/sdk/bus/telegram-adoption-intent.ts +299 -209
  445. package/src/sdk/bus/telegram-daemon-cli.ts +194 -7
  446. package/src/sdk/bus/telegram-daemon-contract.ts +265 -19
  447. package/src/sdk/bus/telegram-daemon-control.ts +3 -13
  448. package/src/sdk/bus/telegram-daemon-orphan-reap.ts +498 -0
  449. package/src/sdk/bus/telegram-daemon-owner-registry.ts +176 -0
  450. package/src/sdk/bus/telegram-daemon.ts +2552 -2280
  451. package/src/sdk/bus/telegram-reference.ts +1 -226
  452. package/src/sdk/bus/telegram-setup.ts +2 -2
  453. package/src/sdk/bus/threaded-render.ts +1 -0
  454. package/src/sdk/bus/topic-registry.ts +279 -146
  455. package/src/sdk/cli/rows.ts +2 -35
  456. package/src/sdk/cli/session-cli.ts +776 -700
  457. package/src/sdk/client/client.ts +892 -1
  458. package/src/sdk/host/control/dispatch.ts +93 -58
  459. package/src/sdk/host/control/index.ts +1 -1
  460. package/src/sdk/host/control/operations.ts +18 -10
  461. package/src/sdk/host/control/runtime-gate.ts +40 -0
  462. package/src/sdk/host/host.ts +63 -12
  463. package/src/sdk/host/query/handlers.ts +66 -7
  464. package/src/sdk/host/reverse-leases.ts +2 -2
  465. package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
  466. package/src/sdk/host/session-runtime.test.ts +2787 -34
  467. package/src/sdk/host/session-runtime.ts +1489 -101
  468. package/src/sdk/host/surface-policy.ts +12 -8
  469. package/src/sdk/host/types.ts +1 -1
  470. package/src/sdk/host/websocket-transport.lifecycle.test.ts +31 -59
  471. package/src/sdk/host/websocket-transport.ts +85 -50
  472. package/src/sdk/index.ts +2 -1
  473. package/src/sdk/lifecycle/broker-client.ts +53 -0
  474. package/src/sdk/lifecycle/client.ts +234 -0
  475. package/src/sdk/lifecycle/index.ts +3 -0
  476. package/src/sdk/{bus/recent-activity.ts → lifecycle/recent-sessions.ts} +5 -2
  477. package/src/sdk/lifecycle/service.ts +720 -0
  478. package/src/sdk/mcp/server.ts +226 -188
  479. package/src/sdk/prompt-deadline-lease.ts +55 -0
  480. package/src/sdk/prompt-deadline-manager.ts +148 -0
  481. package/src/sdk/prompt-status.ts +11 -3
  482. package/src/sdk/protocol/operation-inventory.generated.json +272 -129
  483. package/src/sdk/protocol/operation-registry.ts +71 -8
  484. package/src/sdk/receipt-state.ts +43 -0
  485. package/src/sdk/reconciliation-extensions.ts +14 -0
  486. package/src/sdk/router/index.ts +1 -0
  487. package/src/sdk/router/session-router.ts +1658 -0
  488. package/src/sdk/session-list.ts +93 -0
  489. package/src/sdk/session-reconnect.ts +45 -1
  490. package/src/sdk/session.ts +716 -120
  491. package/src/sdk/transport/serve-cli.ts +18 -34
  492. package/src/session/account-inventory.ts +603 -0
  493. package/src/session/agent-session.ts +2873 -559
  494. package/src/session/agent-storage.ts +35 -0
  495. package/src/session/blob-store.ts +32 -1
  496. package/src/session/coordinator-tool-label.ts +40 -0
  497. package/src/session/internal/managed-session-scope.ts +4 -0
  498. package/src/session/internal/managed-session-storage.ts +199 -31
  499. package/src/session/messages.ts +1 -1
  500. package/src/session/secure-token-file.ts +80 -0
  501. package/src/session/session-manager.ts +757 -57
  502. package/src/session/session-storage.ts +25 -16
  503. package/src/session/startup-auth-config.ts +297 -0
  504. package/src/session/terminal-abort.ts +1253 -0
  505. package/src/session/yield-queue.ts +57 -15
  506. package/src/session-import/claude.ts +334 -0
  507. package/src/session-import/codex.ts +1033 -0
  508. package/src/session-import/command.ts +77 -0
  509. package/src/session-import/detect.ts +133 -0
  510. package/src/session-import/index.ts +7 -0
  511. package/src/session-import/provider-codex.ts +327 -0
  512. package/src/session-import/provider-service.ts +457 -0
  513. package/src/session-import/redact.ts +128 -0
  514. package/src/session-import/service.ts +1264 -0
  515. package/src/session-import/types.ts +148 -0
  516. package/src/setup/paseo/check.ts +213 -0
  517. package/src/setup/paseo/install-saga.ts +293 -0
  518. package/src/setup/paseo/json-publisher.ts +263 -0
  519. package/src/setup/paseo/orchestration-preferences.ts +86 -0
  520. package/src/setup/paseo/paseo-ownership.ts +224 -0
  521. package/src/setup/paseo/paseo-setup.ts +253 -0
  522. package/src/setup/paseo/provider-config.ts +139 -0
  523. package/src/setup/paseo/remove.ts +183 -0
  524. package/src/setup/paseo/result-types.ts +79 -0
  525. package/src/setup/paseo/setup-deps.ts +175 -0
  526. package/src/setup/paseo/skills-bridge.ts +260 -0
  527. package/src/skill-state/active-state.ts +21 -11
  528. package/src/slash-commands/acp-builtins.ts +9 -1
  529. package/src/slash-commands/builtin-registry.ts +117 -19
  530. package/src/slash-commands/helpers/credential-switch.ts +143 -0
  531. package/src/slash-commands/helpers/mcp.ts +9 -3
  532. package/src/slash-commands/helpers/usage-report.ts +61 -56
  533. package/src/slash-commands/types.ts +7 -2
  534. package/src/task/executor.ts +30 -11
  535. package/src/task/index.ts +66 -7
  536. package/src/task/render.ts +23 -9
  537. package/src/tools/ask-answer-registry.ts +23 -7
  538. package/src/tools/ask.ts +8 -4
  539. package/src/tools/bash-sleep-advisory.ts +115 -0
  540. package/src/tools/bash.ts +98 -18
  541. package/src/tools/browser/attach.ts +71 -3
  542. package/src/tools/browser/launch.ts +86 -13
  543. package/src/tools/browser/profile-discovery.ts +57 -7
  544. package/src/tools/browser/render.ts +8 -1
  545. package/src/tools/browser.ts +130 -21
  546. package/src/tools/computer.enforcement.test.ts +1 -1
  547. package/src/tools/computer.ts +3 -2
  548. package/src/tools/cron.ts +12 -1
  549. package/src/tools/image-gen.ts +349 -142
  550. package/src/tools/index.ts +8 -0
  551. package/src/tools/job.ts +33 -6
  552. package/src/tools/monitor.ts +41 -2
  553. package/src/tools/skill-discovery.ts +21 -29
  554. package/src/tools/subagent.ts +142 -24
  555. package/src/tools/todo-contract.ts +55 -3
  556. package/src/tools/todo-write.ts +4 -3
  557. package/src/tools/tool-catalog.generated.ts +6 -6
  558. package/src/tools/ultragoal-ask-guard.ts +10 -3
  559. package/src/utils/herdr-pane.ts +345 -0
  560. package/src/utils/title-generator.ts +2 -0
  561. package/src/web/scrapers/index.ts +6 -4
  562. package/dist/types/cli/malloc-env-guard.test.d.ts +0 -1
  563. package/dist/types/defaults/gjc-defaults.test.d.ts +0 -1
  564. package/dist/types/memory-backend/lazy-loading.test.d.ts +0 -1
  565. package/dist/types/runtime/boot-ordering.test.d.ts +0 -1
  566. package/dist/types/runtime/lazy-service.test.d.ts +0 -1
  567. package/dist/types/runtime/network-prewarm-service.test.d.ts +0 -1
  568. package/dist/types/runtime/workspace-tree-service.test.d.ts +0 -1
  569. package/dist/types/runtime-mcp/config.test.d.ts +0 -1
  570. package/dist/types/runtime-mcp/http.test.d.ts +0 -1
  571. package/dist/types/runtime-mcp/manager-pool.test.d.ts +0 -1
  572. package/dist/types/runtime-mcp/pool-key.test.d.ts +0 -1
  573. package/dist/types/runtime-mcp/pool.test.d.ts +0 -1
  574. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +0 -117
  575. package/dist/types/sdk/bus/lifecycle-orchestrator.d.ts +0 -141
  576. package/dist/types/sdk/bus/telegram-cli.d.ts +0 -31
  577. package/dist/types/sdk/elevation/allowlist.d.ts +0 -20
  578. package/dist/types/sdk/elevation/audit.d.ts +0 -93
  579. package/dist/types/sdk/elevation/capability.d.ts +0 -3
  580. package/dist/types/sdk/elevation/digest.d.ts +0 -72
  581. package/dist/types/sdk/elevation/dispatch-receipt.d.ts +0 -85
  582. package/dist/types/sdk/elevation/grant-ledger.d.ts +0 -163
  583. package/dist/types/sdk/elevation/owner.d.ts +0 -48
  584. package/dist/types/sdk/host/import-graph.test.d.ts +0 -1
  585. package/dist/types/sdk/host/sdk-surface-parity.test.d.ts +0 -1
  586. package/dist/types/sdk/host/session-runtime.test.d.ts +0 -1
  587. package/dist/types/sdk/host/websocket-transport.lifecycle.test.d.ts +0 -1
  588. package/dist/types/session/auth-broker-config.d.ts +0 -13
  589. package/dist/types/session/heap-eviction-artifacts.test.d.ts +0 -1
  590. package/dist/types/skills/skills.test.d.ts +0 -1
  591. package/dist/types/slash-commands/notify-command.test.d.ts +0 -1
  592. package/dist/types/task/token-log.test.d.ts +0 -1
  593. package/dist/types/tools/computer.enforcement.test.d.ts +0 -1
  594. package/dist/types/tools/descriptors.test.d.ts +0 -1
  595. package/scripts/g005-daemon-smoke.ts +0 -180
  596. package/scripts/g011-daemon-path-smoke.ts +0 -153
  597. package/src/sdk/bus/lifecycle-control-runtime.ts +0 -1733
  598. package/src/sdk/bus/lifecycle-orchestrator.ts +0 -489
  599. package/src/sdk/bus/telegram-cli.ts +0 -212
  600. package/src/sdk/elevation/allowlist.ts +0 -54
  601. package/src/sdk/elevation/audit.ts +0 -208
  602. package/src/sdk/elevation/capability.ts +0 -41
  603. package/src/sdk/elevation/digest.ts +0 -129
  604. package/src/sdk/elevation/dispatch-receipt.ts +0 -184
  605. package/src/sdk/elevation/grant-ledger.ts +0 -1142
  606. package/src/sdk/elevation/owner.ts +0 -109
  607. package/src/session/auth-broker-config.ts +0 -109
  608. /package/dist/types/{ai-core-import-gate.test.d.ts → discovery/claude-hooks.d.ts} +0 -0
  609. /package/dist/types/{cli/fixture-report.test.d.ts → discovery/codex-hooks.d.ts} +0 -0
  610. /package/dist/types/sdk/{bus/recent-activity.d.ts → lifecycle/recent-sessions.d.ts} +0 -0
@@ -0,0 +1,723 @@
1
+ /**
2
+ * Compacted crash signature index.
3
+ *
4
+ * The index is **advisory, never authority**. It exists so `gjc crash report`
5
+ * and the startup nudge can say "signature X happened 235 times since Aug 2"
6
+ * without re-parsing a 500 KiB log. It can never authorize, suppress or
7
+ * auto-target anything: a `reportedAt` stamp changes default highlighting, not
8
+ * permission, and every submission is independently confirmed.
9
+ *
10
+ * Increments normally come from the append-only journal. When that write was
11
+ * lost, compaction may recover a never-indexed signature only from a complete
12
+ * v1 crash record whose fingerprint recomputes from its own diagnostic text.
13
+ * Evicted reported or acknowledged signatures stay durably retired.
14
+ */
15
+ import { createHash, randomUUID } from "node:crypto";
16
+ import * as fs from "node:fs/promises";
17
+ import * as path from "node:path";
18
+ import {
19
+ appendCrashEvent,
20
+ CRASH_FINGERPRINT_PATTERN,
21
+ type CrashEvent,
22
+ getCrashEventsPath,
23
+ getCrashIndexPath,
24
+ getCrashLogPath,
25
+ isEnoent,
26
+ parseCrashEventLine,
27
+ parseCrashRecordMarker,
28
+ } from "@gajae-code/utils";
29
+ import { withFileLock } from "../config/file-lock";
30
+ import { type LoadedCrashRecord, parseRecoverableCrashRecords } from "./record-loader";
31
+
32
+ export const CRASH_INDEX_VERSION = 1;
33
+ /** Per-entry message preview cap. */
34
+ export const CRASH_INDEX_MESSAGE_MAX_BYTES = 512;
35
+ /** Per-entry serialized cap. */
36
+ export const CRASH_INDEX_ENTRY_MAX_BYTES = 1024;
37
+ /** Whole-index serialized cap. */
38
+ export const CRASH_INDEX_MAX_BYTES = 256 * 1024;
39
+ /** Entry-count cap; with the per-entry cap this keeps the file under the byte cap. */
40
+ export const CRASH_INDEX_MAX_SIGNATURES = 128;
41
+ /** How many `.corrupt-*` siblings are kept before the oldest are deleted. */
42
+ export const CRASH_INDEX_MAX_QUARANTINE = 3;
43
+ /** Dedupe window for occurrence ids, so a re-merged journal cannot double-count. */
44
+ const RECENT_EVENT_ID_LIMIT = 256;
45
+ /** Whole rotated-journal digests retained to make publish-before-delete replay idempotent. */
46
+ const RECENT_JOURNAL_DIGEST_LIMIT = 64;
47
+ /** Timestamps outside this window are hostile-but-valid JSON and are rejected. */
48
+ const MIN_TIMESTAMP_MS = Date.UTC(2020, 0, 1);
49
+ const MAX_FUTURE_SKEW_MS = 24 * 60 * 60 * 1000;
50
+ /** Bounded read of the crash log when recomputing retained counts. */
51
+ const CRASH_LOG_SCAN_MAX_BYTES = 1024 * 1024;
52
+ /** A complete retained record consumes more than 64 bytes, so this covers every distinct fingerprint in the scan. */
53
+ const CRASH_RETIRED_MAX_FINGERPRINTS = Math.ceil(CRASH_LOG_SCAN_MAX_BYTES / 64);
54
+ /**
55
+ * Bounded read of a rotated journal. The journal is rotated away at every
56
+ * compaction, so this only has to cover one startup interval; when a pathological
57
+ * run does exceed it, the *newest* events are the ones kept.
58
+ */
59
+ const CRASH_JOURNAL_SCAN_MAX_BYTES = 4 * 1024 * 1024;
60
+
61
+ export interface CrashSignatureEntry {
62
+ fpv: number;
63
+ errorName: string;
64
+ messageClass: string;
65
+ /** Occurrences ever journaled for this signature. Never decreases. */
66
+ lifetimeCount: number;
67
+ /** Occurrences whose record is still present in the current crash log. */
68
+ retainedCount: number;
69
+ firstSeen: number;
70
+ lastSeen: number;
71
+ lastRecordId: string;
72
+ reportedAt?: number;
73
+ reportedIssueUrl?: string;
74
+ acknowledgedAt?: number;
75
+ /** Issues this install already "+1"ed, so re-invocations cannot spam comments. */
76
+ commentedIssues?: string[];
77
+ }
78
+
79
+ export interface CrashIndex {
80
+ version: number;
81
+ updatedAt: number;
82
+ /** Epoch ms of the last startup nudge, or 0. */
83
+ lastNudgedAt: number;
84
+ /** True when a new signature could not be stored because nothing was evictable. */
85
+ overflow: boolean;
86
+ recentEventIds: string[];
87
+ recentJournalDigests: string[];
88
+ /** Log-recovered occurrence ids awaiting a possibly delayed journal event. */
89
+ recoveredRecordIds: string[];
90
+ /** Reported or acknowledged signatures evicted from the index; never recovered from the log. */
91
+ retiredFingerprints: string[];
92
+ signatures: Record<string, CrashSignatureEntry>;
93
+ }
94
+
95
+ interface CrashRetiredIndex {
96
+ version: number;
97
+ fingerprints: string[];
98
+ recoveredRecordIds: string[];
99
+ }
100
+
101
+ export interface CrashStatePaths {
102
+ index: string;
103
+ events: string;
104
+ crashLog: string;
105
+ }
106
+
107
+ export function resolveCrashStatePaths(agentDir?: string): CrashStatePaths {
108
+ return {
109
+ index: getCrashIndexPath(agentDir),
110
+ events: getCrashEventsPath(agentDir),
111
+ crashLog: getCrashLogPath(agentDir),
112
+ };
113
+ }
114
+
115
+ export function emptyCrashIndex(): CrashIndex {
116
+ return {
117
+ version: CRASH_INDEX_VERSION,
118
+ updatedAt: 0,
119
+ lastNudgedAt: 0,
120
+ overflow: false,
121
+ recentEventIds: [],
122
+ recentJournalDigests: [],
123
+ recoveredRecordIds: [],
124
+ retiredFingerprints: [],
125
+ signatures: Object.create(null) as Record<string, CrashSignatureEntry>,
126
+ };
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // Strict parsing
131
+ // ---------------------------------------------------------------------------
132
+
133
+ const ENTRY_KEYS = new Set([
134
+ "fpv",
135
+ "errorName",
136
+ "messageClass",
137
+ "lifetimeCount",
138
+ "retainedCount",
139
+ "firstSeen",
140
+ "lastSeen",
141
+ "lastRecordId",
142
+ "reportedAt",
143
+ "reportedIssueUrl",
144
+ "acknowledgedAt",
145
+ "commentedIssues",
146
+ ]);
147
+ const INDEX_KEYS = new Set([
148
+ "version",
149
+ "updatedAt",
150
+ "lastNudgedAt",
151
+ "overflow",
152
+ "recentEventIds",
153
+ "recentJournalDigests",
154
+ "recoveredRecordIds",
155
+ "retiredFingerprints",
156
+ "signatures",
157
+ ]);
158
+
159
+ const CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/;
160
+
161
+ function isCleanString(value: unknown, maxBytes: number): value is string {
162
+ return typeof value === "string" && !CONTROL_CHARS.test(value) && Buffer.byteLength(value, "utf8") <= maxBytes;
163
+ }
164
+
165
+ function isCount(value: unknown): value is number {
166
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= Number.MAX_SAFE_INTEGER;
167
+ }
168
+
169
+ function isTimestamp(value: unknown, now: number): value is number {
170
+ return (
171
+ typeof value === "number" &&
172
+ Number.isSafeInteger(value) &&
173
+ value >= MIN_TIMESTAMP_MS &&
174
+ value <= now + MAX_FUTURE_SKEW_MS
175
+ );
176
+ }
177
+
178
+ /** `JSON.parse` with a reviver that refuses prototype-polluting keys. */
179
+ function parseJsonNullProto(raw: string): unknown {
180
+ return JSON.parse(raw, function reviver(key, value) {
181
+ if (key === "__proto__" || key === "constructor" || key === "prototype") return undefined;
182
+ if (value && typeof value === "object" && !Array.isArray(value)) return Object.assign(Object.create(null), value);
183
+ return value;
184
+ }) as unknown;
185
+ }
186
+
187
+ function retiredIndexPath(indexPath: string): string {
188
+ return `${indexPath}.retired`;
189
+ }
190
+
191
+ function parseRetiredIndex(raw: string): CrashRetiredIndex | undefined {
192
+ let parsed: unknown;
193
+ try {
194
+ parsed = parseJsonNullProto(raw);
195
+ } catch {
196
+ return undefined;
197
+ }
198
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
199
+ const body = parsed as Record<string, unknown>;
200
+ if (body.version !== CRASH_INDEX_VERSION || !Array.isArray(body.fingerprints)) return undefined;
201
+ if (body.fingerprints.length > CRASH_RETIRED_MAX_FINGERPRINTS) return undefined;
202
+ if (!body.fingerprints.every(value => typeof value === "string" && CRASH_FINGERPRINT_PATTERN.test(value)))
203
+ return undefined;
204
+ if (body.recoveredRecordIds !== undefined) {
205
+ if (!Array.isArray(body.recoveredRecordIds) || body.recoveredRecordIds.length > CRASH_RETIRED_MAX_FINGERPRINTS)
206
+ return undefined;
207
+ if (!body.recoveredRecordIds.every(value => typeof value === "string" && /^[0-9a-f]{8,32}$/.test(value)))
208
+ return undefined;
209
+ }
210
+ return {
211
+ version: CRASH_INDEX_VERSION,
212
+ fingerprints: [...body.fingerprints],
213
+ recoveredRecordIds: [...((body.recoveredRecordIds as string[] | undefined) ?? [])],
214
+ };
215
+ }
216
+
217
+ function parseEntry(value: unknown, now: number): CrashSignatureEntry | undefined {
218
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
219
+ const raw = value as Record<string, unknown>;
220
+ for (const key of Object.keys(raw)) if (!ENTRY_KEYS.has(key)) return undefined;
221
+ if (typeof raw.fpv !== "number" || !Number.isSafeInteger(raw.fpv) || raw.fpv < 1 || raw.fpv > 999) return undefined;
222
+ if (!isCleanString(raw.errorName, 128)) return undefined;
223
+ if (!isCleanString(raw.messageClass, CRASH_INDEX_MESSAGE_MAX_BYTES)) return undefined;
224
+ if (!isCount(raw.lifetimeCount) || !isCount(raw.retainedCount)) return undefined;
225
+ if (raw.retainedCount > raw.lifetimeCount) return undefined;
226
+ if (!isTimestamp(raw.firstSeen, now) || !isTimestamp(raw.lastSeen, now)) return undefined;
227
+ if (raw.lastSeen < raw.firstSeen) return undefined;
228
+ if (typeof raw.lastRecordId !== "string" || !/^[0-9a-f]{8,32}$/.test(raw.lastRecordId)) return undefined;
229
+ if (raw.reportedAt !== undefined && !isTimestamp(raw.reportedAt, now)) return undefined;
230
+ if (raw.acknowledgedAt !== undefined && !isTimestamp(raw.acknowledgedAt, now)) return undefined;
231
+ if (raw.reportedIssueUrl !== undefined && !isCleanString(raw.reportedIssueUrl, 256)) return undefined;
232
+ if (raw.commentedIssues !== undefined) {
233
+ if (!Array.isArray(raw.commentedIssues) || raw.commentedIssues.length > 32) return undefined;
234
+ if (!raw.commentedIssues.every(url => isCleanString(url, 256))) return undefined;
235
+ }
236
+ const entry: CrashSignatureEntry = {
237
+ fpv: raw.fpv,
238
+ errorName: raw.errorName,
239
+ messageClass: raw.messageClass,
240
+ lifetimeCount: raw.lifetimeCount,
241
+ retainedCount: raw.retainedCount,
242
+ firstSeen: raw.firstSeen,
243
+ lastSeen: raw.lastSeen,
244
+ lastRecordId: raw.lastRecordId,
245
+ };
246
+ if (raw.reportedAt !== undefined) entry.reportedAt = raw.reportedAt;
247
+ if (raw.reportedIssueUrl !== undefined) entry.reportedIssueUrl = raw.reportedIssueUrl;
248
+ if (raw.acknowledgedAt !== undefined) entry.acknowledgedAt = raw.acknowledgedAt;
249
+ if (raw.commentedIssues !== undefined) entry.commentedIssues = [...(raw.commentedIssues as string[])];
250
+ if (Buffer.byteLength(JSON.stringify(entry), "utf8") > CRASH_INDEX_ENTRY_MAX_BYTES) return undefined;
251
+ return entry;
252
+ }
253
+
254
+ /**
255
+ * Strict index parse. Any deviation — unknown key, wrong alphabet, control
256
+ * character, out-of-range timestamp, count overflow — rejects the whole file so
257
+ * it is quarantined and rebuilt from the journal rather than trusted.
258
+ */
259
+ export function parseCrashIndex(raw: string, now: number = Date.now()): CrashIndex | undefined {
260
+ if (Buffer.byteLength(raw, "utf8") > CRASH_INDEX_MAX_BYTES) return undefined;
261
+ let parsed: unknown;
262
+ try {
263
+ parsed = parseJsonNullProto(raw);
264
+ } catch {
265
+ return undefined;
266
+ }
267
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
268
+ const body = parsed as Record<string, unknown>;
269
+ for (const key of Object.keys(body)) if (!INDEX_KEYS.has(key)) return undefined;
270
+ if (body.version !== CRASH_INDEX_VERSION) return undefined;
271
+ if (!isTimestamp(body.updatedAt, now)) return undefined;
272
+ if (body.lastNudgedAt !== 0 && !isTimestamp(body.lastNudgedAt, now)) return undefined;
273
+ if (typeof body.overflow !== "boolean") return undefined;
274
+ if (!Array.isArray(body.recentEventIds) || body.recentEventIds.length > RECENT_EVENT_ID_LIMIT) return undefined;
275
+ if (!body.recentEventIds.every(id => typeof id === "string" && /^[0-9a-f]{8,32}$/.test(id))) return undefined;
276
+ if (body.recentJournalDigests !== undefined) {
277
+ if (!Array.isArray(body.recentJournalDigests) || body.recentJournalDigests.length > RECENT_JOURNAL_DIGEST_LIMIT)
278
+ return undefined;
279
+ if (!body.recentJournalDigests.every(digest => typeof digest === "string" && /^[0-9a-f]{64}$/.test(digest)))
280
+ return undefined;
281
+ }
282
+ if (body.recoveredRecordIds !== undefined) {
283
+ if (!Array.isArray(body.recoveredRecordIds) || body.recoveredRecordIds.length > CRASH_RETIRED_MAX_FINGERPRINTS)
284
+ return undefined;
285
+ if (!body.recoveredRecordIds.every(id => typeof id === "string" && /^[0-9a-f]{8,32}$/.test(id))) return undefined;
286
+ }
287
+ if (body.retiredFingerprints !== undefined) {
288
+ if (!Array.isArray(body.retiredFingerprints) || body.retiredFingerprints.length > CRASH_INDEX_MAX_SIGNATURES)
289
+ return undefined;
290
+ if (
291
+ !body.retiredFingerprints.every(
292
+ fingerprint => typeof fingerprint === "string" && CRASH_FINGERPRINT_PATTERN.test(fingerprint),
293
+ )
294
+ )
295
+ return undefined;
296
+ }
297
+ if (!body.signatures || typeof body.signatures !== "object" || Array.isArray(body.signatures)) return undefined;
298
+
299
+ const signatures = Object.create(null) as Record<string, CrashSignatureEntry>;
300
+ const rawSignatures = body.signatures as Record<string, unknown>;
301
+ const fingerprints = Object.keys(rawSignatures);
302
+ if (fingerprints.length > CRASH_INDEX_MAX_SIGNATURES) return undefined;
303
+ for (const fingerprint of fingerprints) {
304
+ if (!CRASH_FINGERPRINT_PATTERN.test(fingerprint)) return undefined;
305
+ const entry = parseEntry(rawSignatures[fingerprint], now);
306
+ if (!entry) return undefined;
307
+ signatures[fingerprint] = entry;
308
+ }
309
+ return {
310
+ version: CRASH_INDEX_VERSION,
311
+ updatedAt: body.updatedAt,
312
+ lastNudgedAt: body.lastNudgedAt as number,
313
+ overflow: body.overflow,
314
+ recentEventIds: [...(body.recentEventIds as string[])],
315
+ recentJournalDigests: [...((body.recentJournalDigests as string[] | undefined) ?? [])],
316
+ recoveredRecordIds: [...((body.recoveredRecordIds as string[] | undefined) ?? [])],
317
+ retiredFingerprints: [...((body.retiredFingerprints as string[] | undefined) ?? [])],
318
+ signatures,
319
+ };
320
+ }
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // No-follow IO
324
+ // ---------------------------------------------------------------------------
325
+
326
+ const NOFOLLOW = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
327
+
328
+ /** Read a file refusing to traverse a symlink at the final component. */
329
+ async function readNoFollow(target: string, maxBytes: number): Promise<string | undefined> {
330
+ let handle: fs.FileHandle | undefined;
331
+ try {
332
+ handle = await fs.open(target, fs.constants.O_RDONLY | NOFOLLOW);
333
+ const stat = await handle.stat();
334
+ if (!stat.isFile()) return undefined;
335
+ const length = Math.min(stat.size, maxBytes);
336
+ const buffer = Buffer.allocUnsafe(length);
337
+ await handle.read(buffer, 0, length, Math.max(0, stat.size - length));
338
+ return buffer.toString("utf8");
339
+ } catch (error) {
340
+ if (isEnoent(error)) return undefined;
341
+ return undefined;
342
+ } finally {
343
+ await handle?.close().catch(() => {});
344
+ }
345
+ }
346
+
347
+ async function writeAtomic(target: string, contents: string): Promise<void> {
348
+ const temp = `${target}.tmp.${process.pid}.${randomUUID()}`;
349
+ try {
350
+ await fs.writeFile(temp, contents, { mode: 0o600, flag: "wx" });
351
+ await fs.rename(temp, target);
352
+ } catch (error) {
353
+ await fs.rm(temp, { force: true }).catch(() => {});
354
+ throw error;
355
+ }
356
+ }
357
+
358
+ async function quarantineIndex(indexPath: string, now: number): Promise<string | undefined> {
359
+ const target = `${indexPath}.corrupt-${now}`;
360
+ try {
361
+ await fs.rename(indexPath, target);
362
+ } catch {
363
+ return undefined;
364
+ }
365
+ try {
366
+ const dir = path.dirname(indexPath);
367
+ const base = `${path.basename(indexPath)}.corrupt-`;
368
+ const siblings = (await fs.readdir(dir)).filter(name => name.startsWith(base)).sort();
369
+ for (const stale of siblings.slice(0, Math.max(0, siblings.length - CRASH_INDEX_MAX_QUARANTINE))) {
370
+ await fs.rm(path.join(dir, stale), { force: true }).catch(() => {});
371
+ }
372
+ } catch {}
373
+ return target;
374
+ }
375
+
376
+ async function readRetiredIndex(indexPath: string): Promise<CrashRetiredIndex | undefined> {
377
+ const target = retiredIndexPath(indexPath);
378
+ const raw = await readNoFollow(target, CRASH_LOG_SCAN_MAX_BYTES);
379
+ if (raw !== undefined) return parseRetiredIndex(raw);
380
+ return (await crashLogExists(target)) === false
381
+ ? { version: CRASH_INDEX_VERSION, fingerprints: [], recoveredRecordIds: [] }
382
+ : undefined;
383
+ }
384
+
385
+ async function crashLogExists(crashLogPath: string): Promise<boolean | undefined> {
386
+ let handle: fs.FileHandle | undefined;
387
+ try {
388
+ handle = await fs.open(crashLogPath, fs.constants.O_RDONLY | NOFOLLOW);
389
+ const stat = await handle.stat();
390
+ return stat.isFile() ? true : undefined;
391
+ } catch (error) {
392
+ if (isEnoent(error)) return false;
393
+ return undefined;
394
+ } finally {
395
+ await handle?.close().catch(() => {});
396
+ }
397
+ }
398
+
399
+ async function pruneRetiredFingerprints(index: CrashIndex, crashLogPath: string): Promise<void> {
400
+ if (index.retiredFingerprints.length === 0) return;
401
+ const contents = await readNoFollow(crashLogPath, CRASH_LOG_SCAN_MAX_BYTES);
402
+ if (contents === undefined) {
403
+ if ((await crashLogExists(crashLogPath)) === false) index.retiredFingerprints = [];
404
+ return;
405
+ }
406
+ const retained = new Set(parseRecoverableCrashRecords(contents).map(record => record.fingerprint));
407
+ index.retiredFingerprints = index.retiredFingerprints.filter(fingerprint => retained.has(fingerprint));
408
+ const retainedIds = new Set(parseRecoverableCrashRecords(contents).map(record => record.recordId));
409
+ index.recoveredRecordIds = index.recoveredRecordIds.filter(recordId => retainedIds.has(recordId));
410
+ }
411
+
412
+ // ---------------------------------------------------------------------------
413
+ // Merge
414
+ // ---------------------------------------------------------------------------
415
+
416
+ function boundMessageClass(value: string): string {
417
+ if (Buffer.byteLength(value, "utf8") <= CRASH_INDEX_MESSAGE_MAX_BYTES) return value;
418
+ const bytes = Buffer.from(value, "utf8");
419
+ let end = CRASH_INDEX_MESSAGE_MAX_BYTES;
420
+ while (end > 0 && (bytes[end - 1] & 0xc0) === 0x80) end--;
421
+ if (end > 0 && (bytes[end - 1] ?? 0) >= 0xc0) end--;
422
+ return bytes.subarray(0, end).toString("utf8");
423
+ }
424
+
425
+ function evictOne(index: CrashIndex): boolean {
426
+ let victim: string | undefined;
427
+ let victimSeen = Number.POSITIVE_INFINITY;
428
+ for (const [fingerprint, entry] of Object.entries(index.signatures)) {
429
+ // Unreported signatures are never evicted: losing them is exactly the
430
+ // failure this feature exists to prevent.
431
+ if (entry.reportedAt === undefined && entry.acknowledgedAt === undefined) continue;
432
+ if (entry.lastSeen < victimSeen) {
433
+ victim = fingerprint;
434
+ victimSeen = entry.lastSeen;
435
+ }
436
+ }
437
+ if (!victim) return false;
438
+ if (!index.retiredFingerprints.includes(victim)) {
439
+ if (index.retiredFingerprints.length >= CRASH_RETIRED_MAX_FINGERPRINTS) return false;
440
+ index.retiredFingerprints.push(victim);
441
+ }
442
+ delete index.signatures[victim];
443
+ return true;
444
+ }
445
+
446
+ /** Apply one journal event to the in-memory index. Returns whether it changed anything. */
447
+ export function applyCrashEvent(index: CrashIndex, event: CrashEvent, now: number): boolean {
448
+ if (event.at > now + MAX_FUTURE_SKEW_MS || event.at < MIN_TIMESTAMP_MS) return false;
449
+ if (event.kind === "nudged") {
450
+ if (event.at <= index.lastNudgedAt) return false;
451
+ index.lastNudgedAt = event.at;
452
+ return true;
453
+ }
454
+ const existing = index.signatures[event.fingerprint];
455
+ if (event.kind === "reported") {
456
+ if (!existing) return false;
457
+ if (event.commented) {
458
+ const commented = existing.commentedIssues ?? [];
459
+ if (commented.includes(event.issueUrl)) return false;
460
+ existing.commentedIssues = [...commented, event.issueUrl].slice(-32);
461
+ return true;
462
+ }
463
+ if (existing.reportedAt !== undefined && existing.reportedAt >= event.at) return false;
464
+ existing.reportedAt = event.at;
465
+ existing.reportedIssueUrl = event.issueUrl;
466
+ return true;
467
+ }
468
+ if (event.kind === "acknowledged") {
469
+ if (!existing) return false;
470
+ if (existing.acknowledgedAt !== undefined && existing.acknowledgedAt >= event.at) return false;
471
+ existing.acknowledgedAt = event.at;
472
+ return true;
473
+ }
474
+
475
+ // occurrence
476
+ // Clear before deduplication: a replayed occurrence after main-index publish
477
+ // but before retirement-sidecar publish must still supersede a stale tombstone.
478
+ index.retiredFingerprints = index.retiredFingerprints.filter(fingerprint => fingerprint !== event.fingerprint);
479
+ if (index.recoveredRecordIds.includes(event.recordId)) {
480
+ index.recoveredRecordIds = index.recoveredRecordIds.filter(recordId => recordId !== event.recordId);
481
+ return false;
482
+ }
483
+ if (index.recentEventIds.includes(event.recordId)) return false;
484
+ index.recentEventIds.push(event.recordId);
485
+ if (index.recentEventIds.length > RECENT_EVENT_ID_LIMIT)
486
+ index.recentEventIds.splice(0, index.recentEventIds.length - RECENT_EVENT_ID_LIMIT);
487
+ // A journal occurrence is newer authority than a prior eviction tombstone.
488
+ // Clear it before capacity eviction so a full retirement ledger cannot block
489
+ // the signature's explicit revival.
490
+ if (existing) {
491
+ existing.lifetimeCount += 1;
492
+ existing.firstSeen = Math.min(existing.firstSeen, event.at);
493
+ if (event.at >= existing.lastSeen) {
494
+ existing.lastSeen = event.at;
495
+ existing.lastRecordId = event.recordId;
496
+ if (event.messageClass) existing.messageClass = boundMessageClass(event.messageClass);
497
+ }
498
+ return true;
499
+ }
500
+ if (Object.keys(index.signatures).length >= CRASH_INDEX_MAX_SIGNATURES && !evictOne(index)) {
501
+ // Nothing evictable: stop adding new entries and surface the overflow in
502
+ // `gjc crash report` rather than dropping an unreported signature.
503
+ index.overflow = true;
504
+ return false;
505
+ }
506
+ index.signatures[event.fingerprint] = {
507
+ fpv: event.fpv,
508
+ errorName: event.errorName,
509
+ messageClass: boundMessageClass(event.messageClass),
510
+ lifetimeCount: 1,
511
+ retainedCount: 0,
512
+ firstSeen: event.at,
513
+ lastSeen: event.at,
514
+ lastRecordId: event.recordId,
515
+ };
516
+ return true;
517
+ }
518
+
519
+ /**
520
+ * Recover never-indexed signatures from bound records, then recompute retained counts.
521
+ *
522
+ * Journal counts remain authoritative for known signatures. A never-seen
523
+ * signature is admitted only from complete v1 records that recompute to their
524
+ * marker fingerprint; its initial lifetime is the number of distinct retained
525
+ * record ids. Repeated compaction cannot add those records again.
526
+ *
527
+ * The count is held at `lifetimeCount`, which `parseEntry` requires it not to
528
+ * exceed. The log can hold more records than the journal counted — a fatal whose
529
+ * journal append failed, or that spent the per-process latch, still writes its
530
+ * record — and without this the compactor would write an index its own reader
531
+ * quarantines whole on the next read.
532
+ */
533
+ async function recoverAndRecomputeRetainedCounts(index: CrashIndex, crashLogPath: string, now: number): Promise<void> {
534
+ for (const entry of Object.values(index.signatures)) entry.retainedCount = 0;
535
+ const contents = await readNoFollow(crashLogPath, CRASH_LOG_SCAN_MAX_BYTES);
536
+ if (contents === undefined) return;
537
+ const retainedRecordIds = new Set<string>();
538
+ for (const line of contents.split("\n")) {
539
+ const marker = parseCrashRecordMarker(line);
540
+ if (!marker) continue;
541
+ if (retainedRecordIds.has(marker.recordId)) continue;
542
+ retainedRecordIds.add(marker.recordId);
543
+ const entry = index.signatures[marker.fingerprint];
544
+ if (entry && entry.retainedCount < entry.lifetimeCount) entry.retainedCount += 1;
545
+ }
546
+ const recordsByFingerprint = new Map<string, LoadedCrashRecord[]>();
547
+ const seenRecordIds = new Set<string>();
548
+ for (const record of parseRecoverableCrashRecords(contents)) {
549
+ if (seenRecordIds.has(record.recordId)) continue;
550
+ seenRecordIds.add(record.recordId);
551
+ const records = recordsByFingerprint.get(record.fingerprint) ?? [];
552
+ records.push(record);
553
+ recordsByFingerprint.set(record.fingerprint, records);
554
+ }
555
+ for (const [fingerprint, records] of recordsByFingerprint) {
556
+ const oldest = records.reduce((candidate, record) => (record.at < candidate.at ? record : candidate));
557
+ const newest = records.reduce((candidate, record) => (record.at >= candidate.at ? record : candidate));
558
+ if (!oldest || !newest) continue;
559
+ let entry = index.signatures[fingerprint];
560
+ if (!entry) {
561
+ if (index.retiredFingerprints.includes(fingerprint)) continue;
562
+ if (oldest.at < MIN_TIMESTAMP_MS || newest.at > now + MAX_FUTURE_SKEW_MS) continue;
563
+ if (Object.keys(index.signatures).length >= CRASH_INDEX_MAX_SIGNATURES && !evictOne(index)) {
564
+ index.overflow = true;
565
+ continue;
566
+ }
567
+ entry = {
568
+ fpv: newest.fpv,
569
+ errorName: newest.errorName,
570
+ messageClass: boundMessageClass(newest.messageClass),
571
+ lifetimeCount: records.length,
572
+ retainedCount: records.length,
573
+ firstSeen: oldest.at,
574
+ lastSeen: newest.at,
575
+ lastRecordId: newest.recordId,
576
+ };
577
+ index.signatures[fingerprint] = entry;
578
+ for (const record of records) {
579
+ if (!index.recoveredRecordIds.includes(record.recordId)) index.recoveredRecordIds.push(record.recordId);
580
+ if (index.recentEventIds.includes(record.recordId)) continue;
581
+ index.recentEventIds.push(record.recordId);
582
+ if (index.recentEventIds.length > RECENT_EVENT_ID_LIMIT)
583
+ index.recentEventIds.splice(0, index.recentEventIds.length - RECENT_EVENT_ID_LIMIT);
584
+ }
585
+ }
586
+ }
587
+ }
588
+
589
+ async function drainJournal(paths: CrashStatePaths): Promise<string[]> {
590
+ const dir = path.dirname(paths.events);
591
+ const base = `${path.basename(paths.events)}.compacting-`;
592
+ const pending: string[] = [];
593
+ try {
594
+ for (const name of await fs.readdir(dir)) if (name.startsWith(base)) pending.push(path.join(dir, name));
595
+ } catch {}
596
+ pending.sort();
597
+ const rotated = `${paths.events}.compacting-${Date.now()}-${process.pid}`;
598
+ try {
599
+ await fs.rename(paths.events, rotated);
600
+ pending.push(rotated);
601
+ } catch {
602
+ // No journal to rotate; leftovers from a crashed compaction still apply.
603
+ }
604
+ return pending;
605
+ }
606
+
607
+ export interface CompactCrashIndexOptions {
608
+ paths?: CrashStatePaths;
609
+ now?: number;
610
+ }
611
+
612
+ /**
613
+ * Merge journal events into the index under the cross-process file lock.
614
+ *
615
+ * Bounded and idempotent: the journal is rotated aside before it is read (so a
616
+ * concurrent fatal append lands in a fresh file rather than being lost to a
617
+ * truncate), occurrence ids are deduped, and a crashed compaction's leftover
618
+ * file is picked up by the next run.
619
+ */
620
+ export async function compactCrashIndex(options: CompactCrashIndexOptions = {}): Promise<CrashIndex> {
621
+ const paths = options.paths ?? resolveCrashStatePaths();
622
+ const now = options.now ?? Date.now();
623
+ await fs.mkdir(path.dirname(paths.index), { recursive: true, mode: 0o700 });
624
+ return withFileLock(paths.index, async () => {
625
+ const raw = await readNoFollow(paths.index, CRASH_INDEX_MAX_BYTES + 1);
626
+ let index = raw === undefined ? emptyCrashIndex() : parseCrashIndex(raw, now);
627
+ if (!index) {
628
+ await quarantineIndex(paths.index, now);
629
+ index = emptyCrashIndex();
630
+ }
631
+ const retired = await readRetiredIndex(paths.index);
632
+ if (retired === undefined) throw new Error("Crash retirement ledger is unreadable");
633
+ for (const fingerprint of retired.fingerprints) {
634
+ if (!index.retiredFingerprints.includes(fingerprint)) index.retiredFingerprints.push(fingerprint);
635
+ }
636
+ for (const recordId of retired.recoveredRecordIds) {
637
+ if (!index.recoveredRecordIds.includes(recordId)) index.recoveredRecordIds.push(recordId);
638
+ }
639
+ await pruneRetiredFingerprints(index, paths.crashLog);
640
+
641
+ const drained = await drainJournal(paths);
642
+ for (const file of drained) {
643
+ const contents = await readNoFollow(file, CRASH_JOURNAL_SCAN_MAX_BYTES);
644
+ if (contents === undefined) continue;
645
+ const digest = createHash("sha256").update(contents).digest("hex");
646
+ if (index.recentJournalDigests.includes(digest)) {
647
+ // A batch replay after main-index publish still heals a stale retirement
648
+ // sidecar, but must not reapply counts or other state transitions.
649
+ for (const line of contents.split("\n")) {
650
+ const event = parseCrashEventLine(line);
651
+ if (event?.kind === "occurrence")
652
+ index.retiredFingerprints = index.retiredFingerprints.filter(
653
+ fingerprint => fingerprint !== event.fingerprint,
654
+ );
655
+ }
656
+ continue;
657
+ }
658
+ for (const line of contents.split("\n")) {
659
+ const event = parseCrashEventLine(line);
660
+ if (event) applyCrashEvent(index, event, now);
661
+ }
662
+ index.recentJournalDigests.push(digest);
663
+ if (index.recentJournalDigests.length > RECENT_JOURNAL_DIGEST_LIMIT)
664
+ index.recentJournalDigests.splice(0, index.recentJournalDigests.length - RECENT_JOURNAL_DIGEST_LIMIT);
665
+ }
666
+
667
+ await recoverAndRecomputeRetainedCounts(index, paths.crashLog, now);
668
+ index.updatedAt = now;
669
+ const serializeIndex = (): string =>
670
+ `${JSON.stringify({ ...index, retiredFingerprints: [], recoveredRecordIds: [] })}\n`;
671
+ let serialized = serializeIndex();
672
+ while (Buffer.byteLength(serialized, "utf8") > CRASH_INDEX_MAX_BYTES && evictOne(index)) {
673
+ serialized = serializeIndex();
674
+ }
675
+ if (Buffer.byteLength(serialized, "utf8") > CRASH_INDEX_MAX_BYTES) {
676
+ index.overflow = true;
677
+ serialized = serializeIndex();
678
+ }
679
+ await writeAtomic(
680
+ retiredIndexPath(paths.index),
681
+ `${JSON.stringify({ version: CRASH_INDEX_VERSION, fingerprints: index.retiredFingerprints, recoveredRecordIds: index.recoveredRecordIds })}\n`,
682
+ );
683
+ await fs.rm(paths.index, { force: true });
684
+ await writeAtomic(paths.index, serialized);
685
+ for (const file of drained) await fs.rm(file, { force: true }).catch(() => {});
686
+ return index;
687
+ });
688
+ }
689
+
690
+ /** Read the index without compacting. Missing or invalid files read as empty. */
691
+ export async function readCrashIndex(paths: CrashStatePaths = resolveCrashStatePaths()): Promise<CrashIndex> {
692
+ const raw = await readNoFollow(paths.index, CRASH_INDEX_MAX_BYTES + 1);
693
+ if (raw === undefined) return emptyCrashIndex();
694
+ return parseCrashIndex(raw) ?? emptyCrashIndex();
695
+ }
696
+
697
+ /**
698
+ * Record a state change through the journal, then compact.
699
+ *
700
+ * Writing through the journal (rather than editing the index directly) is what
701
+ * makes concurrent writers safe: two processes stamping `reportedAt` at the
702
+ * same moment both land an event, and the compactor merges them.
703
+ */
704
+ export async function recordCrashStateEvent(
705
+ event: CrashEvent,
706
+ options: CompactCrashIndexOptions = {},
707
+ ): Promise<CrashIndex> {
708
+ const paths = options.paths ?? resolveCrashStatePaths();
709
+ await fs.mkdir(path.dirname(paths.events), { recursive: true, mode: 0o700 });
710
+ appendCrashEvent(event, paths.events);
711
+ return compactCrashIndex({ paths, now: options.now });
712
+ }
713
+
714
+ export interface CrashSignatureView extends CrashSignatureEntry {
715
+ fingerprint: string;
716
+ }
717
+
718
+ /** Signatures newest-first, which is the order both the CLI and the nudge use. */
719
+ export function listCrashSignatures(index: CrashIndex): CrashSignatureView[] {
720
+ return Object.entries(index.signatures)
721
+ .map(([fingerprint, entry]) => ({ fingerprint, ...entry }))
722
+ .sort((a, b) => b.lastSeen - a.lastSeen);
723
+ }