@wjarka/cezarion 0.16.0 → 0.17.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 (907) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +594 -207
  3. package/dist/agent-config/account-identity.d.ts +3 -19
  4. package/dist/agent-config/account-identity.js +8 -5
  5. package/dist/agent-config/account-identity.js.map +1 -1
  6. package/dist/agent-config/catalog.d.ts +3 -14
  7. package/dist/agent-config/catalog.js.map +1 -1
  8. package/dist/agent-config/files.d.ts +2 -6
  9. package/dist/agent-config/files.js +2 -6
  10. package/dist/agent-config/files.js.map +1 -1
  11. package/dist/agent-config/model-settings/types.d.ts +1 -1
  12. package/dist/agent-config/seed.d.ts +11 -3
  13. package/dist/agent-config/seed.js +52 -48
  14. package/dist/agent-config/seed.js.map +1 -1
  15. package/dist/agent-config/service.js +3 -2
  16. package/dist/agent-config/service.js.map +1 -1
  17. package/dist/agent-config/validate.d.ts +1 -4
  18. package/dist/agent-config/validate.js.map +1 -1
  19. package/dist/application-update/helper.d.ts +2 -2
  20. package/dist/application-update/helper.js +7 -12
  21. package/dist/application-update/helper.js.map +1 -1
  22. package/dist/application-update/launcher.d.ts +4 -0
  23. package/dist/application-update/launcher.js +18 -12
  24. package/dist/application-update/launcher.js.map +1 -1
  25. package/dist/application-update/service.d.ts +3 -3
  26. package/dist/application-update/service.js +1 -1
  27. package/dist/application-update/service.js.map +1 -1
  28. package/dist/archive-on-merge-setting.d.ts +15 -0
  29. package/dist/archive-on-merge-setting.js +14 -0
  30. package/dist/archive-on-merge-setting.js.map +1 -0
  31. package/dist/artifacts/cli.d.ts +8 -0
  32. package/dist/artifacts/cli.js +21 -2
  33. package/dist/artifacts/cli.js.map +1 -1
  34. package/dist/artifacts/resolve.js +2 -1
  35. package/dist/artifacts/resolve.js.map +1 -1
  36. package/dist/artifacts/store.js.map +1 -1
  37. package/dist/automations/automation-cli.d.ts +59 -0
  38. package/dist/automations/automation-cli.js +509 -0
  39. package/dist/automations/automation-cli.js.map +1 -0
  40. package/dist/automations/builtin-skill.d.ts +7 -0
  41. package/dist/automations/builtin-skill.js +14 -0
  42. package/dist/automations/builtin-skill.js.map +1 -0
  43. package/dist/automations/github-baseline.d.ts +3 -0
  44. package/dist/automations/github-baseline.js +17 -0
  45. package/dist/automations/github-baseline.js.map +1 -0
  46. package/dist/automations/github-poller.d.ts +23 -4
  47. package/dist/automations/github-poller.js +157 -88
  48. package/dist/automations/github-poller.js.map +1 -1
  49. package/dist/automations/prompts.d.ts +12 -0
  50. package/dist/automations/prompts.js +160 -0
  51. package/dist/automations/prompts.js.map +1 -0
  52. package/dist/automations/schedule-runner.d.ts +43 -24
  53. package/dist/automations/schedule-runner.js +207 -20
  54. package/dist/automations/schedule-runner.js.map +1 -1
  55. package/dist/automations/scheduler.d.ts +10 -2
  56. package/dist/automations/scheduler.js +200 -54
  57. package/dist/automations/scheduler.js.map +1 -1
  58. package/dist/automations/service.d.ts +777 -0
  59. package/dist/automations/service.js +444 -0
  60. package/dist/automations/service.js.map +1 -0
  61. package/dist/automations/store.d.ts +17 -7
  62. package/dist/automations/store.js +88 -17
  63. package/dist/automations/store.js.map +1 -1
  64. package/dist/automations/task-template.d.ts +7 -2
  65. package/dist/automations/task-template.js +93 -44
  66. package/dist/automations/task-template.js.map +1 -1
  67. package/dist/automations/tool-adapter.d.ts +6 -0
  68. package/dist/automations/tool-adapter.js +108 -0
  69. package/dist/automations/tool-adapter.js.map +1 -0
  70. package/dist/automations/types.d.ts +1 -405
  71. package/dist/automations/types.js +1 -218
  72. package/dist/automations/types.js.map +1 -1
  73. package/dist/autosave-cleanup.d.ts +26 -0
  74. package/dist/autosave-cleanup.js +87 -0
  75. package/dist/autosave-cleanup.js.map +1 -0
  76. package/dist/autosave-git.d.ts +3 -0
  77. package/dist/autosave-git.js +67 -35
  78. package/dist/autosave-git.js.map +1 -1
  79. package/dist/autosave-group.d.ts +8 -0
  80. package/dist/autosave-group.js +61 -0
  81. package/dist/autosave-group.js.map +1 -0
  82. package/dist/autosave-holders.d.ts +31 -4
  83. package/dist/autosave-holders.js +92 -7
  84. package/dist/autosave-holders.js.map +1 -1
  85. package/dist/autosave-windows.d.ts +14 -0
  86. package/dist/autosave-windows.js +36 -2
  87. package/dist/autosave-windows.js.map +1 -1
  88. package/dist/ci-wait/client.d.ts +20 -3
  89. package/dist/ci-wait/client.js +40 -44
  90. package/dist/ci-wait/client.js.map +1 -1
  91. package/dist/ci-wait/controller.d.ts +622 -36
  92. package/dist/ci-wait/controller.js +152 -20
  93. package/dist/ci-wait/controller.js.map +1 -1
  94. package/dist/ci-wait/errors.d.ts +19 -0
  95. package/dist/ci-wait/errors.js +180 -2
  96. package/dist/ci-wait/errors.js.map +1 -1
  97. package/dist/ci-wait/github.d.ts +25 -1
  98. package/dist/ci-wait/github.js +76 -9
  99. package/dist/ci-wait/github.js.map +1 -1
  100. package/dist/ci-wait/mcp.d.ts +0 -1
  101. package/dist/ci-wait/mcp.js +0 -2
  102. package/dist/ci-wait/mcp.js.map +1 -1
  103. package/dist/ci-wait/supervisor.d.ts +7 -0
  104. package/dist/ci-wait/supervisor.js +113 -17
  105. package/dist/ci-wait/supervisor.js.map +1 -1
  106. package/dist/ci-wait/tools.d.ts +536 -16
  107. package/dist/ci-wait/tools.js +165 -20
  108. package/dist/ci-wait/tools.js.map +1 -1
  109. package/dist/config.d.ts +3 -17
  110. package/dist/config.js +4 -60
  111. package/dist/config.js.map +1 -1
  112. package/dist/contract/active-provider.d.ts +12 -0
  113. package/dist/contract/agent-profiles.d.ts +2 -10
  114. package/dist/contract/ask-schema.d.ts +22 -4
  115. package/dist/contract/ask.d.ts +6 -4
  116. package/dist/contract/attachments.d.ts +35 -0
  117. package/dist/contract/attention.d.ts +46 -37
  118. package/dist/contract/automation-requests.d.ts +308 -0
  119. package/dist/contract/automation-schedule.d.ts +40 -11
  120. package/dist/contract/automation-storage.d.ts +512 -0
  121. package/dist/contract/automations.d.ts +421 -40
  122. package/dist/contract/ci-wait.d.ts +41 -4
  123. package/dist/contract/config-requests.d.ts +36 -0
  124. package/dist/contract/conversations.d.ts +24 -10
  125. package/dist/contract/defined.d.ts +8 -0
  126. package/dist/contract/delegation.d.ts +312 -8
  127. package/dist/contract/events.d.ts +24 -3
  128. package/dist/contract/github-requests.d.ts +26 -0
  129. package/dist/contract/github.d.ts +60 -27
  130. package/dist/contract/handoff.d.ts +43 -0
  131. package/dist/contract/health.d.ts +2 -2
  132. package/dist/contract/image-path.d.ts +6 -0
  133. package/dist/contract/index.d.ts +31 -0
  134. package/dist/contract/index.js +3756 -1525
  135. package/dist/contract/live-coordination.d.ts +158 -0
  136. package/dist/contract/live-limits.d.ts +8 -0
  137. package/dist/contract/live.d.ts +123 -0
  138. package/dist/contract/pending-ask.d.ts +58 -0
  139. package/dist/contract/permissions.d.ts +54 -0
  140. package/dist/contract/preview.d.ts +33 -0
  141. package/dist/contract/project-limits.d.ts +33 -0
  142. package/dist/contract/projects.d.ts +486 -20
  143. package/dist/contract/provider-requests.d.ts +77 -0
  144. package/dist/contract/query-requests.d.ts +52 -0
  145. package/dist/contract/repo-requests.d.ts +12 -0
  146. package/dist/contract/run-labels.d.ts +8 -0
  147. package/dist/contract/run-limits.d.ts +8 -0
  148. package/dist/contract/run-predicates.d.ts +32 -0
  149. package/dist/contract/run-requests.d.ts +33 -0
  150. package/dist/contract/run-search.d.ts +193 -0
  151. package/dist/contract/runs.d.ts +2232 -1894
  152. package/dist/contract/scheduled-tasks.d.ts +187 -0
  153. package/dist/contract/skills.d.ts +22 -772
  154. package/dist/contract/team-skill-ref.d.ts +10 -0
  155. package/dist/{core → contract}/tool-display.d.ts +4 -10
  156. package/dist/contract/ui-state-requests.d.ts +60 -0
  157. package/dist/contract/version.d.ts +3 -0
  158. package/dist/contract/voice.d.ts +213 -0
  159. package/dist/contract/waits.d.ts +1204 -0
  160. package/dist/contract/workflow-file.d.ts +37 -0
  161. package/dist/contract/workflow-requests.d.ts +7 -0
  162. package/dist/contract/workflows.d.ts +1 -2
  163. package/dist/contract/workspace.d.ts +34 -82
  164. package/dist/contract/zoned-time.d.ts +2 -11
  165. package/dist/core/agent-env.d.ts +4 -16
  166. package/dist/core/agent-env.js +14 -16
  167. package/dist/core/agent-env.js.map +1 -1
  168. package/dist/core/agent-profiles.d.ts +28 -40
  169. package/dist/core/agent-profiles.js +56 -51
  170. package/dist/core/agent-profiles.js.map +1 -1
  171. package/dist/core/agent-runner.d.ts +88 -44
  172. package/dist/core/agent-runner.js +17 -18
  173. package/dist/core/agent-runner.js.map +1 -1
  174. package/dist/core/ask.d.ts +3 -0
  175. package/dist/core/ask.js +12 -0
  176. package/dist/core/ask.js.map +1 -1
  177. package/dist/core/backend-detect.js +16 -2
  178. package/dist/core/backend-detect.js.map +1 -1
  179. package/dist/core/claude-cli-runner.d.ts +23 -4
  180. package/dist/core/claude-cli-runner.js +280 -76
  181. package/dist/core/claude-cli-runner.js.map +1 -1
  182. package/dist/core/claude-model-catalog.d.ts +2 -10
  183. package/dist/core/claude-model-catalog.js +2 -10
  184. package/dist/core/claude-model-catalog.js.map +1 -1
  185. package/dist/core/claude-ui-mapper.d.ts +2 -2
  186. package/dist/core/claude-ui-mapper.js +94 -2
  187. package/dist/core/claude-ui-mapper.js.map +1 -1
  188. package/dist/core/codex-app-server-runner.d.ts +3 -4
  189. package/dist/core/codex-app-server-runner.js +146 -51
  190. package/dist/core/codex-app-server-runner.js.map +1 -1
  191. package/dist/core/codex-app-server-transport.d.ts +4 -3
  192. package/dist/core/codex-app-server-transport.js +11 -7
  193. package/dist/core/codex-app-server-transport.js.map +1 -1
  194. package/dist/core/codex-approvals.d.ts +12 -0
  195. package/dist/core/codex-approvals.js +161 -0
  196. package/dist/core/codex-approvals.js.map +1 -0
  197. package/dist/core/codex-permissions.d.ts +1 -1
  198. package/dist/core/codex-ui-mapper.js +45 -1
  199. package/dist/core/codex-ui-mapper.js.map +1 -1
  200. package/dist/core/cursor-acp-runner.d.ts +1 -1
  201. package/dist/core/cursor-acp-runner.js +39 -15
  202. package/dist/core/cursor-acp-runner.js.map +1 -1
  203. package/dist/core/cursor-cli-flags.d.ts +19 -0
  204. package/dist/core/cursor-cli-flags.js +47 -0
  205. package/dist/core/cursor-cli-flags.js.map +1 -0
  206. package/dist/core/cursor-model-catalog.d.ts +22 -0
  207. package/dist/core/cursor-model-catalog.js +53 -24
  208. package/dist/core/cursor-model-catalog.js.map +1 -1
  209. package/dist/core/cursor-print-capabilities.d.ts +16 -0
  210. package/dist/core/cursor-print-capabilities.js +141 -0
  211. package/dist/core/cursor-print-capabilities.js.map +1 -0
  212. package/dist/core/cursor-print-process.d.ts +31 -0
  213. package/dist/core/cursor-print-process.js +117 -0
  214. package/dist/core/cursor-print-process.js.map +1 -0
  215. package/dist/core/cursor-print-runner.d.ts +31 -0
  216. package/dist/core/cursor-print-runner.js +555 -0
  217. package/dist/core/cursor-print-runner.js.map +1 -0
  218. package/dist/core/cursor-print-tool-catalog.d.ts +10 -0
  219. package/dist/core/cursor-print-tool-catalog.js +91 -0
  220. package/dist/core/cursor-print-tool-catalog.js.map +1 -0
  221. package/dist/core/cursor-print-ui-mapper.d.ts +47 -0
  222. package/dist/core/cursor-print-ui-mapper.js +553 -0
  223. package/dist/core/cursor-print-ui-mapper.js.map +1 -0
  224. package/dist/core/cursor-runner.d.ts +24 -0
  225. package/dist/core/cursor-runner.js +149 -0
  226. package/dist/core/cursor-runner.js.map +1 -0
  227. package/dist/core/cursor-ui-mapper.js +5 -2
  228. package/dist/core/cursor-ui-mapper.js.map +1 -1
  229. package/dist/core/defined.d.ts +1 -0
  230. package/dist/core/defined.js +2 -0
  231. package/dist/core/defined.js.map +1 -0
  232. package/dist/core/hook-items.d.ts +14 -0
  233. package/dist/core/hook-items.js +13 -0
  234. package/dist/core/hook-items.js.map +1 -0
  235. package/dist/core/host-model-catalog.d.ts +2 -9
  236. package/dist/core/host-model-catalog.js +2 -9
  237. package/dist/core/host-model-catalog.js.map +1 -1
  238. package/dist/core/managed-session.js +15 -3
  239. package/dist/core/managed-session.js.map +1 -1
  240. package/dist/core/model-identity.d.ts +4 -21
  241. package/dist/core/model-identity.js.map +1 -1
  242. package/dist/core/model-presets.d.ts +2 -13
  243. package/dist/core/model-presets.js +2 -13
  244. package/dist/core/model-presets.js.map +1 -1
  245. package/dist/core/omp-model-catalog.js.map +1 -1
  246. package/dist/core/omp-runner.d.ts +6 -7
  247. package/dist/core/omp-runner.js +92 -33
  248. package/dist/core/omp-runner.js.map +1 -1
  249. package/dist/core/omp-ui-mapper.js +1 -1
  250. package/dist/core/omp-ui-mapper.js.map +1 -1
  251. package/dist/core/opencode-model-catalog.js.map +1 -1
  252. package/dist/core/opencode-permissions.d.ts +52 -0
  253. package/dist/core/opencode-permissions.js +101 -0
  254. package/dist/core/opencode-permissions.js.map +1 -0
  255. package/dist/core/opencode-server-runner.d.ts +10 -9
  256. package/dist/core/opencode-server-runner.js +286 -28
  257. package/dist/core/opencode-server-runner.js.map +1 -1
  258. package/dist/core/opencode-session-error.d.ts +15 -7
  259. package/dist/core/opencode-session-error.js +25 -9
  260. package/dist/core/opencode-session-error.js.map +1 -1
  261. package/dist/core/opencode-ui-mapper.js +2 -2
  262. package/dist/core/opencode-ui-mapper.js.map +1 -1
  263. package/dist/core/pi-model-catalog.js.map +1 -1
  264. package/dist/core/pi-runner.d.ts +12 -4
  265. package/dist/core/pi-runner.js +171 -63
  266. package/dist/core/pi-runner.js.map +1 -1
  267. package/dist/core/pi-ui-mapper.js +1 -1
  268. package/dist/core/pi-ui-mapper.js.map +1 -1
  269. package/dist/core/process-usage.d.ts +5 -0
  270. package/dist/core/process-usage.js +24 -4
  271. package/dist/core/process-usage.js.map +1 -1
  272. package/dist/core/provider-auth.d.ts +16 -107
  273. package/dist/core/provider-auth.js +16 -132
  274. package/dist/core/provider-auth.js.map +1 -1
  275. package/dist/core/runner-factory.d.ts +4 -1
  276. package/dist/core/runner-factory.js +3 -3
  277. package/dist/core/runner-factory.js.map +1 -1
  278. package/dist/core/runner-runtime.d.ts +22 -0
  279. package/dist/core/runner-runtime.js +66 -5
  280. package/dist/core/runner-runtime.js.map +1 -1
  281. package/dist/core/runner-tool-grants.d.ts +21 -0
  282. package/dist/core/runner-tool-grants.js +64 -0
  283. package/dist/core/runner-tool-grants.js.map +1 -0
  284. package/dist/core/secret-redaction.d.ts +3 -20
  285. package/dist/core/secret-redaction.js +3 -20
  286. package/dist/core/secret-redaction.js.map +1 -1
  287. package/dist/core/session-process.d.ts +22 -2
  288. package/dist/core/session-process.js +170 -12
  289. package/dist/core/session-process.js.map +1 -1
  290. package/dist/core/shell-env.d.ts +2 -15
  291. package/dist/core/shell-env.js +2 -15
  292. package/dist/core/shell-env.js.map +1 -1
  293. package/dist/core/ui-events.d.ts +60 -47
  294. package/dist/core/ui-events.js +0 -23
  295. package/dist/core/ui-events.js.map +1 -1
  296. package/dist/core/usage-limit.js.map +1 -1
  297. package/dist/data-gitignore.js +3 -1
  298. package/dist/data-gitignore.js.map +1 -1
  299. package/dist/delegation/capacity.d.ts +4 -8
  300. package/dist/delegation/capacity.js +4 -8
  301. package/dist/delegation/capacity.js.map +1 -1
  302. package/dist/delegation/cli.js +123 -21
  303. package/dist/delegation/cli.js.map +1 -1
  304. package/dist/delegation/context.d.ts +19 -3
  305. package/dist/delegation/context.js +217 -22
  306. package/dist/delegation/context.js.map +1 -1
  307. package/dist/delegation/conversations.d.ts +4 -3
  308. package/dist/delegation/conversations.js +7 -5
  309. package/dist/delegation/conversations.js.map +1 -1
  310. package/dist/delegation/destroy-observation.d.ts +1 -1
  311. package/dist/delegation/destroy-observation.js.map +1 -1
  312. package/dist/delegation/effort.js.map +1 -1
  313. package/dist/delegation/execution-identity.d.ts +1 -1
  314. package/dist/delegation/execution-identity.js +41 -7
  315. package/dist/delegation/execution-identity.js.map +1 -1
  316. package/dist/delegation/idle-test-app.d.ts +4 -0
  317. package/dist/delegation/idle-test-app.js +174 -0
  318. package/dist/delegation/idle-test-app.js.map +1 -0
  319. package/dist/delegation/input.js.map +1 -1
  320. package/dist/delegation/park-notice.d.ts +42 -0
  321. package/dist/delegation/park-notice.js +71 -0
  322. package/dist/delegation/park-notice.js.map +1 -0
  323. package/dist/delegation/policy.d.ts +2 -0
  324. package/dist/delegation/policy.js +9 -1
  325. package/dist/delegation/policy.js.map +1 -1
  326. package/dist/delegation/process-liveness.d.ts +10 -15
  327. package/dist/delegation/process-liveness.js +11 -35
  328. package/dist/delegation/process-liveness.js.map +1 -1
  329. package/dist/delegation/provision.js +21 -19
  330. package/dist/delegation/provision.js.map +1 -1
  331. package/dist/delegation/questions.d.ts +3 -1
  332. package/dist/delegation/questions.js +10 -5
  333. package/dist/delegation/questions.js.map +1 -1
  334. package/dist/delegation/readiness.d.ts +2 -2
  335. package/dist/delegation/results.d.ts +4 -1
  336. package/dist/delegation/results.js +49 -18
  337. package/dist/delegation/results.js.map +1 -1
  338. package/dist/delegation/retry-backoff.d.ts +8 -0
  339. package/dist/delegation/retry-backoff.js +8 -0
  340. package/dist/delegation/retry-backoff.js.map +1 -1
  341. package/dist/delegation/routes.d.ts +114 -30
  342. package/dist/delegation/routes.js +4 -1
  343. package/dist/delegation/routes.js.map +1 -1
  344. package/dist/delegation/scratch-cleanup.d.ts +4 -5
  345. package/dist/delegation/scratch-cleanup.js +10 -8
  346. package/dist/delegation/scratch-cleanup.js.map +1 -1
  347. package/dist/delegation/service.d.ts +12 -4
  348. package/dist/delegation/service.js +76 -28
  349. package/dist/delegation/service.js.map +1 -1
  350. package/dist/delegation/transport.d.ts +154 -70
  351. package/dist/delegation/wait.d.ts +11 -0
  352. package/dist/delegation/wait.js +24 -0
  353. package/dist/delegation/wait.js.map +1 -1
  354. package/dist/delegation/windows-test-app.d.ts +5 -0
  355. package/dist/delegation/windows-test-app.js +63 -0
  356. package/dist/delegation/windows-test-app.js.map +1 -0
  357. package/dist/delegation/windows-workspace-identity.d.ts +3 -0
  358. package/dist/delegation/windows-workspace-identity.js +22 -0
  359. package/dist/delegation/windows-workspace-identity.js.map +1 -0
  360. package/dist/delegation/workspace.d.ts +5 -10
  361. package/dist/delegation/workspace.js +136 -75
  362. package/dist/delegation/workspace.js.map +1 -1
  363. package/dist/detached-process.d.ts +103 -0
  364. package/dist/detached-process.js +301 -0
  365. package/dist/detached-process.js.map +1 -0
  366. package/dist/discovery/catalog.d.ts +2 -1
  367. package/dist/discovery/catalog.js.map +1 -1
  368. package/dist/discovery/cli.js +3 -1
  369. package/dist/discovery/cli.js.map +1 -1
  370. package/dist/git-diff-base.d.ts +6 -61
  371. package/dist/git-diff-base.js +6 -70
  372. package/dist/git-diff-base.js.map +1 -1
  373. package/dist/git-index-copy.d.ts +4 -0
  374. package/dist/git-index-copy.js +10 -0
  375. package/dist/git-index-copy.js.map +1 -0
  376. package/dist/git-worktree-lock.d.ts +2 -4
  377. package/dist/git-worktree-lock.js +2 -4
  378. package/dist/git-worktree-lock.js.map +1 -1
  379. package/dist/git-worktree-release.d.ts +2 -2
  380. package/dist/git-worktree-release.js.map +1 -1
  381. package/dist/git-worktree.d.ts +47 -76
  382. package/dist/git-worktree.js +240 -94
  383. package/dist/git-worktree.js.map +1 -1
  384. package/dist/handoff.d.ts +51 -41
  385. package/dist/handoff.js +130 -78
  386. package/dist/handoff.js.map +1 -1
  387. package/dist/index.js +60 -39
  388. package/dist/index.js.map +1 -1
  389. package/dist/merge-observations.d.ts +13 -0
  390. package/dist/merge-observations.js +55 -0
  391. package/dist/merge-observations.js.map +1 -0
  392. package/dist/open-url.js +4 -14
  393. package/dist/open-url.js.map +1 -1
  394. package/dist/paths.d.ts +10 -13
  395. package/dist/paths.js +36 -17
  396. package/dist/paths.js.map +1 -1
  397. package/dist/planner.d.ts +2 -1
  398. package/dist/planner.js +9 -6
  399. package/dist/planner.js.map +1 -1
  400. package/dist/preview/agent-address.d.ts +34 -0
  401. package/dist/preview/agent-address.js +51 -0
  402. package/dist/preview/agent-address.js.map +1 -0
  403. package/dist/preview/agent-marker.d.ts +17 -0
  404. package/dist/preview/agent-marker.js +71 -0
  405. package/dist/preview/agent-marker.js.map +1 -0
  406. package/dist/preview/cdp-proxy.d.ts +54 -0
  407. package/dist/preview/cdp-proxy.js +151 -0
  408. package/dist/preview/cdp-proxy.js.map +1 -0
  409. package/dist/preview/cdp.d.ts +39 -3
  410. package/dist/preview/cdp.js +155 -57
  411. package/dist/preview/cdp.js.map +1 -1
  412. package/dist/preview/chromium.d.ts +18 -6
  413. package/dist/preview/chromium.js +112 -25
  414. package/dist/preview/chromium.js.map +1 -1
  415. package/dist/preview/dev-server.d.ts +1 -1
  416. package/dist/preview/dev-server.js +20 -44
  417. package/dist/preview/dev-server.js.map +1 -1
  418. package/dist/preview/host.d.ts +70 -3
  419. package/dist/preview/host.js +314 -28
  420. package/dist/preview/host.js.map +1 -1
  421. package/dist/preview/registration.d.ts +10 -5
  422. package/dist/preview/registration.js +2 -0
  423. package/dist/preview/registration.js.map +1 -1
  424. package/dist/preview/session.d.ts +2 -0
  425. package/dist/preview/session.js +10 -0
  426. package/dist/preview/session.js.map +1 -1
  427. package/dist/process-wait/exit-file.d.ts +29 -0
  428. package/dist/process-wait/exit-file.js +67 -0
  429. package/dist/process-wait/exit-file.js.map +1 -0
  430. package/dist/process-wait/supervisor.d.ts +30 -0
  431. package/dist/process-wait/supervisor.js +71 -0
  432. package/dist/process-wait/supervisor.js.map +1 -0
  433. package/dist/release/snapshot.d.ts +6 -6
  434. package/dist/runs/agent-tmpdir.d.ts +51 -36
  435. package/dist/runs/agent-tmpdir.js +389 -132
  436. package/dist/runs/agent-tmpdir.js.map +1 -1
  437. package/dist/runs/auto-name.d.ts +6 -6
  438. package/dist/runs/auto-name.js +1 -1
  439. package/dist/runs/auto-name.js.map +1 -1
  440. package/dist/runs/client-request.d.ts +3 -3
  441. package/dist/runs/client-request.js +3 -3
  442. package/dist/runs/client-request.js.map +1 -1
  443. package/dist/runs/delegation-state.d.ts +11 -2
  444. package/dist/runs/delegation-state.js.map +1 -1
  445. package/dist/runs/event-history-agent-context.d.ts +14 -0
  446. package/dist/runs/event-history-agent-context.js +105 -0
  447. package/dist/runs/event-history-agent-context.js.map +1 -0
  448. package/dist/runs/event-history-context.d.ts +15 -0
  449. package/dist/runs/event-history-context.js +109 -0
  450. package/dist/runs/event-history-context.js.map +1 -0
  451. package/dist/runs/event-history.d.ts +4 -1
  452. package/dist/runs/event-history.js +29 -130
  453. package/dist/runs/event-history.js.map +1 -1
  454. package/dist/runs/history-stream.d.ts +2 -0
  455. package/dist/runs/history-stream.js +62 -0
  456. package/dist/runs/history-stream.js.map +1 -0
  457. package/dist/runs/legacy-index.d.ts +6 -19
  458. package/dist/runs/legacy-index.js +6 -19
  459. package/dist/runs/legacy-index.js.map +1 -1
  460. package/dist/runs/live-item-snapshots.d.ts +13 -0
  461. package/dist/runs/live-item-snapshots.js +79 -0
  462. package/dist/runs/live-item-snapshots.js.map +1 -0
  463. package/dist/runs/loaded-run-healing.d.ts +3 -0
  464. package/dist/runs/loaded-run-healing.js +17 -0
  465. package/dist/runs/loaded-run-healing.js.map +1 -0
  466. package/dist/runs/outcome-commit.d.ts +98 -0
  467. package/dist/runs/outcome-commit.js +132 -0
  468. package/dist/runs/outcome-commit.js.map +1 -0
  469. package/dist/runs/pull-requests.d.ts +14 -0
  470. package/dist/runs/pull-requests.js +96 -0
  471. package/dist/runs/pull-requests.js.map +1 -0
  472. package/dist/runs/raw-record.d.ts +3 -15
  473. package/dist/runs/raw-record.js +3 -15
  474. package/dist/runs/raw-record.js.map +1 -1
  475. package/dist/runs/retention.d.ts +2 -2
  476. package/dist/runs/retention.js +7 -0
  477. package/dist/runs/retention.js.map +1 -1
  478. package/dist/runs/review-gate.d.ts +1 -1
  479. package/dist/runs/review-gate.js.map +1 -1
  480. package/dist/runs/run-claims.d.ts +2 -9
  481. package/dist/runs/run-claims.js.map +1 -1
  482. package/dist/runs/run-database.d.ts +29 -34
  483. package/dist/runs/run-database.js +124 -46
  484. package/dist/runs/run-database.js.map +1 -1
  485. package/dist/runs/run-index.d.ts +6 -25
  486. package/dist/runs/run-index.js +25 -38
  487. package/dist/runs/run-index.js.map +1 -1
  488. package/dist/runs/run-row.d.ts +5 -34
  489. package/dist/runs/run-row.js +13 -38
  490. package/dist/runs/run-row.js.map +1 -1
  491. package/dist/runs/store.d.ts +1597 -744
  492. package/dist/runs/store.js +1231 -739
  493. package/dist/runs/store.js.map +1 -1
  494. package/dist/runs/task-markers.d.ts +1 -0
  495. package/dist/runs/task-markers.js +5 -1
  496. package/dist/runs/task-markers.js.map +1 -1
  497. package/dist/runs/task-refs.d.ts +3 -3
  498. package/dist/runs/transcript-facts-fold.d.ts +64 -0
  499. package/dist/runs/transcript-facts-fold.js +109 -0
  500. package/dist/runs/transcript-facts-fold.js.map +1 -0
  501. package/dist/runs/transcript-facts-load.d.ts +11 -0
  502. package/dist/runs/transcript-facts-load.js +66 -0
  503. package/dist/runs/transcript-facts-load.js.map +1 -0
  504. package/dist/runs/transcript-facts-queue.d.ts +30 -0
  505. package/dist/runs/transcript-facts-queue.js +227 -0
  506. package/dist/runs/transcript-facts-queue.js.map +1 -0
  507. package/dist/runs/transcript-facts-worker.d.ts +15 -0
  508. package/dist/runs/transcript-facts-worker.js +26 -0
  509. package/dist/runs/transcript-facts-worker.js.map +1 -0
  510. package/dist/runs/transcript-facts.d.ts +13 -51
  511. package/dist/runs/transcript-facts.js +86 -202
  512. package/dist/runs/transcript-facts.js.map +1 -1
  513. package/dist/runs/ui-event-sink.d.ts +5 -30
  514. package/dist/runs/ui-event-sink.js +8 -30
  515. package/dist/runs/ui-event-sink.js.map +1 -1
  516. package/dist/runs/webhook.d.ts +104 -38
  517. package/dist/runs/webhook.js +180 -77
  518. package/dist/runs/webhook.js.map +1 -1
  519. package/dist/server/app-type.d.ts +2 -17
  520. package/dist/server/archive-on-merge.d.ts +35 -0
  521. package/dist/server/archive-on-merge.js +202 -0
  522. package/dist/server/archive-on-merge.js.map +1 -0
  523. package/dist/server/capabilities.d.ts +3 -15
  524. package/dist/server/capabilities.js +6 -19
  525. package/dist/server/capabilities.js.map +1 -1
  526. package/dist/server/checkout-progress.d.ts +10 -0
  527. package/dist/server/checkout-progress.js +27 -0
  528. package/dist/server/checkout-progress.js.map +1 -0
  529. package/dist/server/checkout.d.ts +10 -19
  530. package/dist/server/checkout.js +15 -45
  531. package/dist/server/checkout.js.map +1 -1
  532. package/dist/server/cockpit-ownership.d.ts +2 -0
  533. package/dist/server/cockpit-ownership.js +9 -0
  534. package/dist/server/cockpit-ownership.js.map +1 -1
  535. package/dist/server/cold-repo-handles.d.ts +2 -7
  536. package/dist/server/cold-repo-handles.js +2 -7
  537. package/dist/server/cold-repo-handles.js.map +1 -1
  538. package/dist/server/completion-actions.d.ts +30 -0
  539. package/dist/server/completion-actions.js +204 -0
  540. package/dist/server/completion-actions.js.map +1 -0
  541. package/dist/server/create-project.d.ts +71 -0
  542. package/dist/server/create-project.js +448 -0
  543. package/dist/server/create-project.js.map +1 -0
  544. package/dist/server/forge/github-filters.d.ts +7 -1
  545. package/dist/server/forge/github-filters.js +66 -51
  546. package/dist/server/forge/github-filters.js.map +1 -1
  547. package/dist/server/forge/github-merge-requirements.d.ts +18 -0
  548. package/dist/server/forge/github-merge-requirements.js +137 -0
  549. package/dist/server/forge/github-merge-requirements.js.map +1 -0
  550. package/dist/server/forge/github.d.ts +98 -111
  551. package/dist/server/forge/github.js +625 -306
  552. package/dist/server/forge/github.js.map +1 -1
  553. package/dist/server/forge/types.d.ts +25 -22
  554. package/dist/server/fs-browse.d.ts +3 -28
  555. package/dist/server/fs-browse.js.map +1 -1
  556. package/dist/server/git-changes.d.ts +5 -15
  557. package/dist/server/git-changes.js +3 -50
  558. package/dist/server/git-changes.js.map +1 -1
  559. package/dist/server/git.d.ts +3 -1
  560. package/dist/server/git.js +12 -0
  561. package/dist/server/git.js.map +1 -1
  562. package/dist/server/github.d.ts +1 -1
  563. package/dist/server/github.js +1 -1
  564. package/dist/server/github.js.map +1 -1
  565. package/dist/server/launch-key.d.ts +2 -4
  566. package/dist/server/launch-key.js +2 -4
  567. package/dist/server/launch-key.js.map +1 -1
  568. package/dist/server/live-routes.d.ts +68 -0
  569. package/dist/server/live-routes.js +147 -0
  570. package/dist/server/live-routes.js.map +1 -0
  571. package/dist/server/open-in-app.d.ts +4 -13
  572. package/dist/server/open-in-app.js +5 -41
  573. package/dist/server/open-in-app.js.map +1 -1
  574. package/dist/server/open-in-targets.d.ts +42 -0
  575. package/dist/server/open-in-targets.js +110 -0
  576. package/dist/server/open-in-targets.js.map +1 -0
  577. package/dist/server/open-in-terminal.d.ts +3 -0
  578. package/dist/server/open-in-terminal.js +6 -26
  579. package/dist/server/open-in-terminal.js.map +1 -1
  580. package/dist/server/preview-cdp-socket.d.ts +21 -0
  581. package/dist/server/preview-cdp-socket.js +112 -0
  582. package/dist/server/preview-cdp-socket.js.map +1 -0
  583. package/dist/server/preview-socket.d.ts +11 -11
  584. package/dist/server/preview-socket.js +22 -22
  585. package/dist/server/preview-socket.js.map +1 -1
  586. package/dist/server/preview-sockets.d.ts +14 -0
  587. package/dist/server/preview-sockets.js +18 -0
  588. package/dist/server/preview-sockets.js.map +1 -0
  589. package/dist/server/project-context.d.ts +54 -18
  590. package/dist/server/project-context.js +178 -5
  591. package/dist/server/project-context.js.map +1 -1
  592. package/dist/server/provider-action-gate.d.ts +0 -2
  593. package/dist/server/provider-action-gate.js +0 -21
  594. package/dist/server/provider-action-gate.js.map +1 -1
  595. package/dist/server/provider-auth-runtime.d.ts +3 -18
  596. package/dist/server/provider-auth-runtime.js +3 -18
  597. package/dist/server/provider-auth-runtime.js.map +1 -1
  598. package/dist/server/repo-branches.d.ts +3 -9
  599. package/dist/server/repo-branches.js +6 -22
  600. package/dist/server/repo-branches.js.map +1 -1
  601. package/dist/server/run-event-feed.d.ts +20 -0
  602. package/dist/server/run-event-feed.js +190 -0
  603. package/dist/server/run-event-feed.js.map +1 -0
  604. package/dist/server/server-deps.d.ts +37 -0
  605. package/dist/server/server-deps.js +114 -0
  606. package/dist/server/server-deps.js.map +1 -0
  607. package/dist/server/server.d.ts +14082 -11998
  608. package/dist/server/server.js +785 -1647
  609. package/dist/server/server.js.map +1 -1
  610. package/dist/server/start-run-options.d.ts +82 -0
  611. package/dist/server/start-run-options.js +111 -0
  612. package/dist/server/start-run-options.js.map +1 -0
  613. package/dist/server/validators.d.ts +3 -29
  614. package/dist/server/validators.js +1 -2
  615. package/dist/server/validators.js.map +1 -1
  616. package/dist/server/ws.d.ts +3 -23
  617. package/dist/server/ws.js +3 -23
  618. package/dist/server/ws.js.map +1 -1
  619. package/dist/server-install/engine.d.ts +6 -9
  620. package/dist/server-install/engine.js +2 -9
  621. package/dist/server-install/engine.js.map +1 -1
  622. package/dist/server-install/platforms/ubuntu-vps.d.ts +1 -1
  623. package/dist/server-install/state.d.ts +2 -4
  624. package/dist/server-install/state.js +2 -4
  625. package/dist/server-install/state.js.map +1 -1
  626. package/dist/server-install/steps.d.ts +2 -5
  627. package/dist/server-install/steps.js +3 -6
  628. package/dist/server-install/steps.js.map +1 -1
  629. package/dist/server-install/types.d.ts +3 -6
  630. package/dist/server-install/types.js +2 -5
  631. package/dist/server-install/types.js.map +1 -1
  632. package/dist/server-install/ui.d.ts +2 -8
  633. package/dist/server-install/ui.js +3 -3
  634. package/dist/server-install/ui.js.map +1 -1
  635. package/dist/session-context.d.ts +64 -0
  636. package/dist/session-context.js +264 -0
  637. package/dist/session-context.js.map +1 -0
  638. package/dist/skills-provision.d.ts +62 -0
  639. package/dist/skills-provision.js +325 -0
  640. package/dist/skills-provision.js.map +1 -0
  641. package/dist/skills-remote.d.ts +29 -27
  642. package/dist/skills-remote.js +236 -100
  643. package/dist/skills-remote.js.map +1 -1
  644. package/dist/skills.d.ts +25 -36
  645. package/dist/skills.js +43 -54
  646. package/dist/skills.js.map +1 -1
  647. package/dist/subcommands.d.ts +11 -0
  648. package/dist/subcommands.js +24 -0
  649. package/dist/subcommands.js.map +1 -0
  650. package/dist/task-cli/cli.js +78 -25
  651. package/dist/task-cli/cli.js.map +1 -1
  652. package/dist/task-cli/discovery.d.ts +4 -2
  653. package/dist/task-cli/discovery.js +14 -13
  654. package/dist/task-cli/discovery.js.map +1 -1
  655. package/dist/task-cli/hint.d.ts +20 -0
  656. package/dist/task-cli/hint.js +14 -0
  657. package/dist/task-cli/hint.js.map +1 -0
  658. package/dist/task-cli/http.d.ts +1 -0
  659. package/dist/task-cli/http.js +3 -1
  660. package/dist/task-cli/http.js.map +1 -1
  661. package/dist/task-cli/projections.d.ts +14 -11
  662. package/dist/task-cli/projections.js +15 -11
  663. package/dist/task-cli/projections.js.map +1 -1
  664. package/dist/task-cli/watch.d.ts +18 -10
  665. package/dist/task-cli/watch.js +128 -20
  666. package/dist/task-cli/watch.js.map +1 -1
  667. package/dist/ui-state.d.ts +17 -3
  668. package/dist/ui-state.js +56 -9
  669. package/dist/ui-state.js.map +1 -1
  670. package/dist/update-check.d.ts +0 -10
  671. package/dist/update-check.js +1 -16
  672. package/dist/update-check.js.map +1 -1
  673. package/dist/voice/decide.d.ts +32 -0
  674. package/dist/voice/decide.js +297 -0
  675. package/dist/voice/decide.js.map +1 -0
  676. package/dist/voice/decision-log.d.ts +52 -0
  677. package/dist/voice/decision-log.js +107 -0
  678. package/dist/voice/decision-log.js.map +1 -0
  679. package/dist/voice/engine.d.ts +119 -0
  680. package/dist/voice/engine.js +60 -0
  681. package/dist/voice/engine.js.map +1 -0
  682. package/dist/voice/fake-engine.d.ts +10 -0
  683. package/dist/voice/fake-engine.js +192 -0
  684. package/dist/voice/fake-engine.js.map +1 -0
  685. package/dist/voice/jev-engine.d.ts +17 -0
  686. package/dist/voice/jev-engine.js +110 -0
  687. package/dist/voice/jev-engine.js.map +1 -0
  688. package/dist/voice/pending-ask.d.ts +17 -0
  689. package/dist/voice/pending-ask.js +50 -0
  690. package/dist/voice/pending-ask.js.map +1 -0
  691. package/dist/voice/routes.d.ts +115 -0
  692. package/dist/voice/routes.js +91 -0
  693. package/dist/voice/routes.js.map +1 -0
  694. package/dist/voice/sentence.d.ts +21 -0
  695. package/dist/voice/sentence.js +61 -0
  696. package/dist/voice/sentence.js.map +1 -0
  697. package/dist/voice/thresholds.d.ts +28 -0
  698. package/dist/voice/thresholds.js +29 -0
  699. package/dist/voice/thresholds.js.map +1 -0
  700. package/dist/webhooks/cli.d.ts +18 -0
  701. package/dist/webhooks/cli.js +145 -0
  702. package/dist/webhooks/cli.js.map +1 -0
  703. package/dist/webhooks/prompt.d.ts +5 -0
  704. package/dist/webhooks/prompt.js +26 -0
  705. package/dist/webhooks/prompt.js.map +1 -0
  706. package/dist/workflows/active-run.d.ts +167 -0
  707. package/dist/workflows/active-run.js +21 -0
  708. package/dist/workflows/active-run.js.map +1 -0
  709. package/dist/workflows/continuation-admission.d.ts +913 -0
  710. package/dist/workflows/continuation-admission.js +265 -0
  711. package/dist/workflows/continuation-admission.js.map +1 -0
  712. package/dist/workflows/execution-account.d.ts +13 -0
  713. package/dist/workflows/execution-account.js +18 -0
  714. package/dist/workflows/execution-account.js.map +1 -0
  715. package/dist/workflows/input-acknowledgement.d.ts +152 -0
  716. package/dist/workflows/input-acknowledgement.js +38 -0
  717. package/dist/workflows/input-acknowledgement.js.map +1 -0
  718. package/dist/workflows/line-order.d.ts +5 -0
  719. package/dist/workflows/line-order.js +25 -0
  720. package/dist/workflows/line-order.js.map +1 -0
  721. package/dist/workflows/outcome-batches.d.ts +20 -0
  722. package/dist/workflows/outcome-batches.js +25 -0
  723. package/dist/workflows/outcome-batches.js.map +1 -0
  724. package/dist/workflows/question-routing.d.ts +8 -0
  725. package/dist/workflows/question-routing.js +18 -0
  726. package/dist/workflows/question-routing.js.map +1 -0
  727. package/dist/workflows/run-scheduling.d.ts +35 -0
  728. package/dist/workflows/run-scheduling.js +61 -0
  729. package/dist/workflows/run-scheduling.js.map +1 -0
  730. package/dist/workflows/run-states.d.ts +190 -0
  731. package/dist/workflows/run-states.js +295 -0
  732. package/dist/workflows/run-states.js.map +1 -0
  733. package/dist/workflows/run.d.ts +1296 -278
  734. package/dist/workflows/run.js +5730 -3265
  735. package/dist/workflows/run.js.map +1 -1
  736. package/dist/workflows/turn-end.d.ts +116 -0
  737. package/dist/workflows/turn-end.js +68 -0
  738. package/dist/workflows/turn-end.js.map +1 -0
  739. package/dist/workflows/types.d.ts +35 -130
  740. package/dist/workflows/types.js +47 -100
  741. package/dist/workflows/types.js.map +1 -1
  742. package/dist/workflows/waits.d.ts +74 -0
  743. package/dist/workflows/waits.js +197 -0
  744. package/dist/workflows/waits.js.map +1 -0
  745. package/dist/workspace/agent-accounts.d.ts +5 -38
  746. package/dist/workspace/agent-accounts.js +14 -40
  747. package/dist/workspace/agent-accounts.js.map +1 -1
  748. package/dist/workspace/agent-profiles.d.ts +40 -16
  749. package/dist/workspace/agent-profiles.js +77 -17
  750. package/dist/workspace/agent-profiles.js.map +1 -1
  751. package/dist/workspace/config.d.ts +48 -49
  752. package/dist/workspace/config.js +68 -72
  753. package/dist/workspace/config.js.map +1 -1
  754. package/dist/workspace/migrations.d.ts +3 -13
  755. package/dist/workspace/migrations.js.map +1 -1
  756. package/dist/workspace/projects-cli.d.ts +1 -1
  757. package/dist/workspace/projects-cli.js.map +1 -1
  758. package/dist/workspace/projects.d.ts +15 -18
  759. package/dist/workspace/projects.js +20 -16
  760. package/dist/workspace/projects.js.map +1 -1
  761. package/dist/workspace/semaphore.d.ts +11 -45
  762. package/dist/workspace/semaphore.js +6 -18
  763. package/dist/workspace/semaphore.js.map +1 -1
  764. package/dist/workspace/ui-state.d.ts +6 -4
  765. package/dist/workspace/ui-state.js +6 -4
  766. package/dist/workspace/ui-state.js.map +1 -1
  767. package/dist/workspace/webhooks.d.ts +40 -0
  768. package/dist/workspace/webhooks.js +146 -0
  769. package/dist/workspace/webhooks.js.map +1 -0
  770. package/dist/worktree-manifest.d.ts +165 -0
  771. package/dist/worktree-manifest.js +530 -0
  772. package/dist/worktree-manifest.js.map +1 -0
  773. package/dist/worktree-setup.js.map +1 -1
  774. package/package.json +6 -5
  775. package/scripts/benchmark-run-store-fixture.ts +7 -3
  776. package/scripts/check-pack.mjs +3 -1
  777. package/scripts/inline-contract.mjs +80 -27
  778. package/scripts/install-as-command.mjs +3 -1
  779. package/scripts/measure-transcript-facts-health.mjs +87 -0
  780. package/scripts/measure-transcript-facts-lag.mjs +68 -0
  781. package/scripts/mock-ci-tool.mjs +49 -6
  782. package/scripts/mock-claude-model-refusal.mjs +42 -0
  783. package/scripts/mock-claude.mjs +833 -591
  784. package/scripts/mock-codex-app-server.mjs +785 -418
  785. package/scripts/mock-cursor-acp.mjs +296 -125
  786. package/scripts/mock-cursor-print.mjs +256 -0
  787. package/scripts/mock-deferred-turn-start.mjs +16 -0
  788. package/scripts/mock-delivery-control.d.mts +23 -0
  789. package/scripts/mock-delivery-control.mjs +80 -0
  790. package/scripts/mock-env-record.mjs +11 -0
  791. package/scripts/mock-failure-control.d.mts +13 -0
  792. package/scripts/mock-failure-control.mjs +68 -0
  793. package/scripts/mock-multi-pr.mjs +7 -0
  794. package/scripts/mock-nudge.mjs +45 -0
  795. package/scripts/mock-omp-rpc.mjs +429 -255
  796. package/scripts/mock-opencode-serve.mjs +972 -557
  797. package/scripts/mock-pi-rpc.mjs +558 -331
  798. package/scripts/mock-runner-crash.mjs +18 -2
  799. package/scripts/mock-session-context.d.mts +5 -0
  800. package/scripts/mock-session-context.mjs +56 -0
  801. package/scripts/mock-silent-tail.mjs +10 -3
  802. package/scripts/pi-default-tools.mjs +22 -0
  803. package/scripts/pi-project-skills.mjs +59 -0
  804. package/scripts/probe-cursor-print.mjs +627 -0
  805. package/scripts/transcript-facts-fixture.mjs +53 -0
  806. package/scripts/voice-replay.ts +50 -0
  807. package/scripts/windows-workspace-identity.cs +208 -0
  808. package/scripts/windows-workspace-identity.ps1 +10 -0
  809. package/web/dist/assets/alert-dialog-DfctKUMw.js +1 -0
  810. package/web/dist/assets/button-CeupvoKG.js +41 -0
  811. package/web/dist/assets/centered-state-hDsyXrnH.js +1 -0
  812. package/web/dist/assets/cez-idle-D3aj_9lp.js +1 -0
  813. package/web/dist/assets/diff-BnIXZK3i2.js +3 -0
  814. package/web/dist/assets/diff-controls-GewpW9aF.js +1 -0
  815. package/web/dist/assets/diff-view-Byo7gpvW.js +11 -0
  816. package/web/dist/assets/dropdown-menu-COj_J5wd.js +6 -0
  817. package/web/dist/assets/eye-B8okNWXU.js +1 -0
  818. package/web/dist/assets/git-toolbar-DY0z0n4S.js +1 -0
  819. package/web/dist/assets/github-DwU6ALRK.js +1 -0
  820. package/web/dist/assets/{image-preview-FOEfqpZP.js → image-preview-C_t9q45f.js} +1 -1
  821. package/web/dist/assets/index-BOfE8pyD.js +34 -0
  822. package/web/dist/assets/index-D9HpsmdW.css +1 -0
  823. package/web/dist/assets/live-protocol-Uce2NwEX.js +64 -0
  824. package/web/dist/assets/live-worker-BizfZSoh.ts +33 -0
  825. package/web/dist/assets/markdown-BBDDg-4t.js +1 -0
  826. package/web/dist/assets/nav-row-styles-AoIhhIwf.js +1 -0
  827. package/web/dist/assets/popover-D56fhiXj.js +1 -0
  828. package/web/dist/assets/preview-pane-connected-BmVqfSU5.js +3 -0
  829. package/web/dist/assets/reference-chip-gQNk-4jc.js +1 -0
  830. package/web/dist/assets/repo-git-B5ECDUuv.js +1 -0
  831. package/web/dist/assets/{repo-git-CoKp-g9K.css → repo-git-DVXNOJDT.css} +1 -1
  832. package/web/dist/assets/run-header-C0Y1NvEM.js +2 -0
  833. package/web/dist/assets/run-header-CC7VUTg9.css +1 -0
  834. package/web/dist/assets/skills-DUeTkZ9z.js +1 -0
  835. package/web/dist/assets/status-dot-1PR0i69P.js +1 -0
  836. package/web/dist/assets/tab-link-DmWk4l5O.js +1 -0
  837. package/web/dist/assets/task-changes-DKZXuW89.js +1 -0
  838. package/web/dist/assets/task-commits-BZ6KxWdF.js +1 -0
  839. package/web/dist/assets/task-files-DlF7Apcn.js +2 -0
  840. package/web/dist/assets/task-github-item-CVPo3geZ.js +1 -0
  841. package/web/dist/assets/task-thread-DtQ6uGfQ.css +1 -0
  842. package/web/dist/assets/task-thread-mm4-ECyo.js +9 -0
  843. package/web/dist/assets/thread-state-CIw2BlhT.js +4 -0
  844. package/web/dist/assets/use-hand-to-agent-state-BCDjzHnG.js +1 -0
  845. package/web/dist/assets/use-hand-to-agent-state-C7esPJ-N.css +1 -0
  846. package/web/dist/assets/workflows-D8vYD-ql.js +7 -0
  847. package/web/dist/assets/zoomable-image-CxQSsfcn.js +1 -0
  848. package/web/dist/index.html +13 -17
  849. package/web/dist/live-worker.js +4 -0
  850. package/dist/core/tool-display.js +0 -179
  851. package/dist/core/tool-display.js.map +0 -1
  852. package/dist/runs/human-ask-summary.d.ts +0 -9
  853. package/dist/runs/human-ask-summary.js +0 -39
  854. package/dist/runs/human-ask-summary.js.map +0 -1
  855. package/dist/runs/run-search.d.ts +0 -29
  856. package/dist/runs/run-search.js +0 -63
  857. package/dist/runs/run-search.js.map +0 -1
  858. package/dist/skills-banner.d.ts +0 -22
  859. package/dist/skills-banner.js +0 -41
  860. package/dist/skills-banner.js.map +0 -1
  861. package/dist/skills-update.d.ts +0 -88
  862. package/dist/skills-update.js +0 -392
  863. package/dist/skills-update.js.map +0 -1
  864. package/dist/todos.d.ts +0 -48
  865. package/dist/todos.js +0 -201
  866. package/dist/todos.js.map +0 -1
  867. package/web/dist/assets/agent-accounts-CFOBMVHc.js +0 -1
  868. package/web/dist/assets/alert-dialog-BrpNUwMt.js +0 -1
  869. package/web/dist/assets/button-C_rd7CPp.js +0 -43
  870. package/web/dist/assets/compare-variants-D6WXhvXA.js +0 -1
  871. package/web/dist/assets/diff-DrEARXTV.js +0 -3
  872. package/web/dist/assets/diff-controls-B1-j7nFg.js +0 -1
  873. package/web/dist/assets/diff-view-DvGLjdTk.js +0 -6
  874. package/web/dist/assets/dropdown-menu-BegOZFrj.js +0 -1
  875. package/web/dist/assets/format-DpNe3S0A.js +0 -1
  876. package/web/dist/assets/git-toolbar-Cw4ljmhB.js +0 -1
  877. package/web/dist/assets/github-CfV5sqL0.js +0 -1
  878. package/web/dist/assets/index-DgWWlOLy.js +0 -13
  879. package/web/dist/assets/index-fBFnuvxu.css +0 -1
  880. package/web/dist/assets/nav-row-styles-vp2JbhuJ.js +0 -1
  881. package/web/dist/assets/popover-BohP6WTx.js +0 -1
  882. package/web/dist/assets/preview-pane-connected-qTA5KNoJ.js +0 -3
  883. package/web/dist/assets/preview-state-vuMviq6q.js +0 -1
  884. package/web/dist/assets/prompt-templates-Cubdut_C.js +0 -15
  885. package/web/dist/assets/repo-git-Cf4t5PD0.js +0 -1
  886. package/web/dist/assets/run-diff-BpkUW37Z.js +0 -3
  887. package/web/dist/assets/run-header-68WHf9wV.js +0 -1
  888. package/web/dist/assets/run-header-8GObNx6F.css +0 -1
  889. package/web/dist/assets/search-x-I9Q8pgUa.js +0 -1
  890. package/web/dist/assets/skills-DFBiAuJG.js +0 -1
  891. package/web/dist/assets/skills-DiVIgKeU.js +0 -1
  892. package/web/dist/assets/square-terminal-B6RYYieR.js +0 -1
  893. package/web/dist/assets/status-dot-CqTJHSP9.js +0 -1
  894. package/web/dist/assets/tab-link-B07Hu4QX.js +0 -1
  895. package/web/dist/assets/task-changes--lQF6EYf.js +0 -1
  896. package/web/dist/assets/task-commits-Cx0qy4DN.js +0 -1
  897. package/web/dist/assets/task-files-D50s-gKW.js +0 -2
  898. package/web/dist/assets/task-github-item-DaqmSwYT.js +0 -1
  899. package/web/dist/assets/task-thread-BPQ64lui.js +0 -7
  900. package/web/dist/assets/task-thread-zNE2Q9E-.css +0 -1
  901. package/web/dist/assets/textarea-D20O4Pw0.js +0 -1
  902. package/web/dist/assets/thread-state-DWcFMrvb.js +0 -4
  903. package/web/dist/assets/use-hand-to-agent-state-BLIGmwbd.css +0 -1
  904. package/web/dist/assets/use-hand-to-agent-state-Cy9eyyfL.js +0 -1
  905. package/web/dist/assets/utils-JsL3ZsJf.js +0 -64
  906. package/web/dist/assets/workflows-jwG7uxqi.js +0 -11
  907. package/web/dist/assets/zoomable-image-BJJ64KGc.js +0 -1
@@ -1,35 +1,45 @@
1
+ import { defined } from '../core/defined.js';
2
+ import { autosaveCleanupBlocker, clearAutosaveCleanup } from '../autosave-cleanup.js';
1
3
  import { EventEmitter } from 'node:events';
2
4
  import { workerEvidenceRunIds, workerExecutionSchema } from './worker-execution.js';
3
5
  import { agentTmpDirLocations, agentTmpDirMayExist, agentTmpDirOwnershipProven, removeAgentTmpDir } from './agent-tmpdir.js';
4
6
  import { removeArtifacts } from '../artifacts/lifecycle.js';
5
- import { randomUUID } from 'node:crypto';
7
+ import { randomUUID, createHash } from 'node:crypto';
6
8
  import { isDeepStrictEqual } from 'node:util';
7
9
  import { appendFileSync, closeSync, constants, fstatSync, fsyncSync, lstatSync, openSync, readSync, realpathSync, readdirSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
8
10
  import { join, resolve } from 'node:path';
9
11
  import { z } from 'zod';
10
- import { ciWaitSchema, agentInputSchema, inboxClaimSchema, delegationStateSchema, workerCreationReceiptSchema, workerCollectedResultSchema, workerResultFileSchema, continuationMessageSchema, previewServerSchema, toRunSummary, workerDestroyRetrySchema, runRecordSchema as contractRunRecordSchema, } from '../contract/index.js';
11
- import { matchesRunQuery, sqlPrefilterTokens } from './run-search.js';
12
+ import { ciWaitSchema } from '../contract/index.js';
13
+ import { workerWaitSchema } from '../contract/index.js';
14
+ import { agentInputSchema, inboxClaimSchema, delegationStateSchema, workerCreationReceiptSchema, workerCollectedResultSchema, workerResultFileSchema, workerDestroyRetrySchema, } from '../contract/index.js';
15
+ import { automationIdOf, stepStateSchema as contractStepStateSchema, queuedMessageSchema, toRunSummary, runRecordSchema as contractRunRecordSchema, runPullRequestSchema, mergedPullRequestSchema, } from '../contract/index.js';
16
+ import { previewServerSchema } from '../contract/index.js';
17
+ import { isUsageLimitHold, waitSchema, LAST_WAITS_LIMIT, TIMER_MAX_SECONDS, TIMER_MIN_SECONDS, WAIT_LIMIT, } from '../contract/index.js';
18
+ import { engineOwnsRun, isSweepable } from '../contract/index.js';
19
+ import { matchesRunSearch, sqlPrefilterTokens } from '../contract/index.js';
12
20
  import { storedDelegationStateSchema } from './delegation-state.js';
13
21
  import { HistoryCompressor } from './history-compressor.js';
22
+ import { LiveItemSnapshots } from './live-item-snapshots.js';
14
23
  import { emptyFacts, stampOf, TranscriptFactsIndex } from './transcript-facts.js';
15
24
  import { hasPlainHistory, historyPaths, readHistoryText, readHistoryTextAsync, removeHistory, restoreHistory } from './history-file.js';
16
25
  import { workerExecutionIdentitySchema } from '../delegation/execution-identity.js';
17
- import { reconcileWorkerWait } from '../delegation/wait.js';
18
26
  import { capacityError, workerCapacity } from '../delegation/capacity.js';
19
27
  import { inspectGeneration, isCurrentProcess, recordedProcessLive, processStartToken } from '../delegation/process-liveness.js';
20
28
  import { collectSecretValues, redactDeep, redactSecrets } from '../core/secret-redaction.js';
21
29
  // Pure, dependency-free reference helpers — the same sanity bound the marker parser applies.
22
- import { MAX_REF } from './task-refs.js';
30
+ import { archivePullRequests, pullRequestKey, mergePullRequests } from './pull-requests.js';
31
+ import { extractTaskRefs, MAX_REF } from './task-refs.js';
23
32
  // Type-only module (zod + nothing else), so this cannot cycle back into the store.
24
33
  import { workflowDefSchema } from '../workflows/types.js';
34
+ import { applyOutcomes } from './outcome-commit.js';
25
35
  import { RUNNER_IDS } from '../core/agent-runner.js';
26
36
  import { ARCHIVED_ROOT, RUNS_DB_FILE, RUNS_IMPORT_COMPLETE_KEY, RunConflictError, RunDatabase, RunDatabaseBusyError, } from './run-database.js';
37
+ import { restoreLostReferencedPullRequest } from './loaded-run-healing.js';
27
38
  import { encodeRunRow, isLiveRecord, isLiveStatus } from './run-row.js';
28
39
  import { collectRawExtras, encodeRawRecord } from './raw-record.js';
29
40
  import { assertNoLegacyCockpit, assertNoLegacyWriter, backUpLegacyIndex, LEGACY_INDEX_FILE, LegacyWriterError, readLegacyIndex } from './legacy-index.js';
30
41
  import { toRunStoreOpenError } from './store-open-error.js';
31
42
  import { claimOwnerLive, closeClaimSession, openClaimSession } from './run-claims.js';
32
- const usageCounterSchema = z.number().finite().nonnegative();
33
43
  /**
34
44
  * A runner id as it may appear in a PERSISTED record, normalized to the three
35
45
  * ids the rest of cezar speaks (#547).
@@ -53,317 +63,346 @@ const usageCounterSchema = z.number().finite().nonnegative();
53
63
  const storedRunnerSchema = z
54
64
  .enum([...RUNNER_IDS, 'claude-cli'])
55
65
  .transform((id) => (id === 'claude-cli' ? 'claude' : id));
56
- const stepStateSchema = z.object({
57
- id: z.string(),
58
- name: z.string(),
59
- kind: z.enum(['agent', 'check']),
60
- status: z.enum(['pending', 'running', 'waiting', 'review', 'done', 'failed', 'cancelled', 'skipped']),
61
- iterations: z.number(),
62
- /** Durable count of onFail retry loops consumed by this check. */
63
- retriesUsed: z.number().int().nonnegative().optional(),
64
- tokensUsed: z.number(),
65
- inputTokens: usageCounterSchema.optional(),
66
- outputTokens: usageCounterSchema.optional(),
67
- usageInvocationsStarted: usageCounterSchema.optional(),
68
- usageInvocationsObserved: usageCounterSchema.optional(),
69
- usageTurnsStarted: usageCounterSchema.optional(),
70
- usageTurnsRecorded: usageCounterSchema.optional(),
71
- usageInvocationEpoch: usageCounterSchema.optional(),
72
- startedAt: z.string().optional(),
73
- finishedAt: z.string().optional(),
74
- error: z.string().optional(),
75
- /** Latest backend-owned session id, used for same-backend Continue. */
76
- sessionId: z.string().optional(),
77
- /** Backend that owns `sessionId`. Optional so pre-affinity runs.json files still parse;
78
- * `storedRunnerSchema` so a legacy `claude-cli` folds to `claude` instead of failing (#547). */
79
- backend: storedRunnerSchema.optional(),
80
- /** Agent profile (account) this step actually spawned under — `default`, or a stored profile
81
- * id (spec 2026-07-29-agent-profiles). Recorded rather than re-derived because a session id
82
- * only means something inside the config dir that created it: `sessionId` and `profileId` are
83
- * a PAIR. Without it, changing the project's account would silently make Continue resume
84
- * against the wrong account's session store. Absent = the discovered default. */
85
- profileId: z.string().optional(),
86
- /** Dollar cost reported by the claude CLI for this step's turns. */
87
- costUsd: z.number().optional(),
88
- /** Explicit provenance for engine-generated continuation steps. */
89
- synthetic: z.literal('continuation').optional(),
90
- });
91
- /** One prompt message stacked onto a run while it waits for a free agent slot
92
- * (#472). Folded into `{{task}}` at dequeue by `hydrateQueuedInput`; never
93
- * delivered as its own turn — a follow-up turn would reach only the first step
94
- * of a chain and would race the opening turn. */
95
- const queuedMessageSchema = z.object({
96
- id: z.string(),
97
- text: z.string(),
98
- /** `/api/v1/runs/:id/images/…` URLs — the base64 never enters `runs.json`. */
99
- images: z.array(z.string()).optional(),
100
- createdAt: z.string(),
101
- });
66
+ const stepStateSchema = contractStepStateSchema.extend({ backend: storedRunnerSchema.optional() });
102
67
  /** Exported for `./run-index.ts`, the read-only reader of the same records. Nothing else should
103
68
  * parse a stored run — see `reconcileLoadedRun` for why a second parser is a correctness risk. */
104
- export const runRecordSchema = z.object({
105
- id: z.string(),
106
- title: z.string(),
107
- /** Display title (#389): the auto-derived summary of the first agent turn,
108
- * or the user's inline edit (`PATCH /api/runs/:id` sets it together with
109
- * `title` so edits always win). The UI shows `titleSummary ?? title`. */
110
- titleSummary: z.string().optional(),
111
- /** `git diff --shortstat` of the worktree vs its base, refreshed on every
112
- * turn-end (#389) — what the quick list / table shows without a git call.
113
- * `repointed` (#751) is optional and only ever written as `true`: it marks the
114
- * runs whose numbers were narrowed to uncommitted work because the agent had
115
- * checked another branch out into the worktree. Optional is load-bearing here —
116
- * `runs.json` is `safeParse`d as one array, so a required addition would
117
- * silently drop every pre-existing run. */
118
- diffStat: z
119
- .object({
120
- adds: z.number(),
121
- dels: z.number(),
122
- files: z.number(),
123
- repointed: z.boolean().optional(),
124
- })
125
- .optional(),
126
- workflow: z.string(),
127
- task: z.string(),
128
- /** Prompt messages stacked onto this run while it was queued (#472). Optional:
129
- * `undefined` on every pre-#472 record reads as an empty stack. Like `task`,
130
- * `text` is the user's own prompt and is replayed into `{{task}}`, so it is
131
- * deliberately NOT in `redactPatch`'s field list — scrubbing it would corrupt
132
- * the run the same way scrubbing `task` would. */
133
- queuedMessages: z.array(queuedMessageSchema).optional(),
134
- /** Durable Continue opening message; id is its synthetic step id. No base64 in the index. */
135
- continuationMessage: continuationMessageSchema.optional(),
136
- /** A malformed explicit ASK must never be collected as worker success. */
137
- invalidAsk: z.boolean().optional(),
69
+ export const runRecordSchema = contractRunRecordSchema.extend({
138
70
  delegation: storedDelegationStateSchema,
139
- /** Non-human input must retain attribution through restart, separately from human answers. */
140
- agentInputs: z.array(agentInputSchema).optional(),
141
- ciWait: ciWaitSchema.optional(),
142
- lastCiWait: ciWaitSchema.optional(),
143
- /** Retained recovery observation when previous CI metadata cannot be trusted. */
144
- lastCiWaitError: z.string().max(256).optional(),
145
- /** Dev servers the agent registered with `cezar_preview_serve` (#781). Optional so old files parse. */
146
- previewServers: z.array(previewServerSchema).optional(),
147
- /** URLs of images attached to the initial task prompt, for the thread's first bubble
148
- * (#image-display) — persisted like agent screenshots, served from `/images/`. */
149
- taskImages: z.array(z.string()).optional(),
150
- model: z.string().optional(),
151
- /** Reasoning-effort pin (#45). Canonical `low`/`medium`/`high`/`xhigh`/`max`.
152
- * Absent = harness default. Additive: pre-#45 records omit it and still parse. */
153
- effort: z.string().max(32).optional(),
154
- /** Canonical provider/model identity (#405) — the normalised `provider/model`
155
- * (e.g. `anthropic/claude-opus-4-8`) the run actually used, resolved from the
156
- * free-text `model` against the chosen runner. Additive and optional: pre-#405
157
- * records carry only `model`, and it stays the human/hand-edit surface; this
158
- * is the parseable identity cost attribution and reproducible replay key off.
159
- *
160
- * Read in production by the session header's agent badge (#546), which shows it
161
- * whenever it says something `model` does not — so this is no longer a
162
- * write-only field whose next reader has to guess whether it is load-bearing. */
163
- modelIdentity: z.string().optional(),
164
- /** Agent backend this run used — drives "open in CLI" resume command. `storedRunnerSchema`
165
- * so a legacy `claude-cli` record folds to `claude` instead of failing the whole index (#547). */
71
+ lastWaitsError: contractRunRecordSchema.shape.lastWaitsError.catch(undefined),
72
+ automaticWakes: contractRunRecordSchema.shape.automaticWakes.catch(undefined),
73
+ waitDeliveryFailures: contractRunRecordSchema.shape.waitDeliveryFailures.catch(undefined),
166
74
  runner: storedRunnerSchema.optional(),
167
- /** Per-task agent-account override from the composer (spec 2026-07-29-agent-profiles), applying
168
- * to steps that run on `runner`. Steps on a DIFFERENT backend still resolve from the project's
169
- * own selection — an override for Claude says nothing about which Codex account a mixed
170
- * workflow's codex step should use. Absent = follow the project. */
171
- agentProfile: z.string().optional(),
172
- /** Echo of the extra system prompt this run actually used (R2): the
173
- * `POST /api/runs` override, or the `config.json` default it fell back to.
174
- * Deliberately NOT the full composed prompt — skill bodies and the handoff
175
- * contract are derivable from the persisted workflow and would bloat the
176
- * index. Resolved at execute time (a queued run picks up config edits). */
177
- systemPrompt: z.string().optional(),
178
- /** Per-task follow-up inbox contract (spec 007, #444). Missing on old runs
179
- * means enabled — the historical behavior. */
180
- generateFollowups: z.boolean().optional(),
181
- /** Effective autonomous mode (#489), set at creation from StartRunInput.autonomous.
182
- * This records the resolved launch mode, not whether a user clicked a checkbox:
183
- * the composer may select it through a skill or workspace default (#458).
184
- * Used by the autonomous nudge and terminal review policy. Continue/recovery
185
- * preserve it; absent on legacy records means non-autonomous. */
186
- autonomous: z.boolean().optional(),
187
- /** Task webhook opt-in (#589). Absent = off; `POST /runs/:id/notify` flips it at any time. */
188
75
  notify: z.boolean().optional().catch(undefined),
189
- /** The last task-webhook delivery's outcome (#589), written by `runs/webhook.ts`. */
190
76
  webhook: contractRunRecordSchema.shape.webhook.catch(undefined),
191
- /** Optional provenance for tasks launched by a project GitHub automation. */
192
- automation: z
193
- .object({
194
- automationId: z.string(),
195
- automationRevision: z.number().int().positive(),
196
- receiptId: z.string(),
197
- event: z.string(),
198
- githubUrl: z.string().url(),
199
- })
200
- .optional(),
201
- /** Provenance for a task a scheduled automation launched. Its own key so a pre-schedule cezar
202
- * strips it instead of failing the whole index on a missing `githubUrl`. */
203
- automationTrigger: z
204
- .object({
205
- automationId: z.string(),
206
- automationRevision: z.number().int().positive(),
207
- receiptId: z.string(),
208
- trigger: z.enum(['schedule', 'catch-up', 'manual']),
209
- occurrenceAt: z.string(),
210
- })
211
- .optional(),
212
- /** A pending worker destroy's automatic retry state (hearsay-tools/cezarion#879); a malformed one is dropped, never the run. */
213
77
  destroyRetry: contractRunRecordSchema.shape.destroyRetry.catch(undefined),
214
- status: z.enum(['queued', 'running', 'waiting', 'review', 'done', 'failed', 'cancelled']),
215
78
  stopping: contractRunRecordSchema.shape.stopping.catch(undefined),
216
- /** Sub-state of `running` (spec 2026-07-18-subagent-monitoring-status, #490):
217
- * `monitoring` while the agent is still working on its own downstream work.
218
- * Optional/absent on old runs; cleared when the run resumes or ends. */
219
- activity: z.enum(['monitoring']).optional(),
220
- hasPendingHumanAsk: contractRunRecordSchema.shape.hasPendingHumanAsk.catch(undefined),
221
- /** Exact server-computed deadline for the next automatic monitoring check. */
222
- monitoringWakeAt: z.string().datetime().optional().catch(undefined),
223
- /** True only for the live epoch that exhausted all automatic monitoring checks. */
224
- monitoringWakeCapReached: z.boolean().optional(),
225
- /**
226
- * Exact deadline at which a run stopped by a provider USAGE LIMIT resumes itself
227
- * (spec 2026-08-03-auto-resume-after-usage-limit) — the reset instant the provider named plus a
228
- * short grace. Present only while such a resume is pending: the run is `failed`, the timer is
229
- * armed, and the cockpit says so. Deliberately survives a restart (`RunStore.open` keeps it) —
230
- * it is what lets `recover()` re-arm a wait that may be hours long.
231
- */
79
+ hasPendingPermission: contractRunRecordSchema.shape.hasPendingPermission.catch(undefined),
232
80
  autoResumeAt: z.string().datetime().optional().catch(undefined),
233
- /** Consecutive automatic resumes since the last human turn — the safety cap's counter.
234
- * Persisted so a restart cannot reset a loop back to zero. */
235
81
  autoResumeAttempts: z.number().int().min(0).optional().catch(undefined),
236
- createdAt: z.string(),
237
- startedAt: z.string().optional(),
238
- finishedAt: z.string().optional(),
239
- tokensUsed: z.number(),
240
- inputTokens: usageCounterSchema.optional(),
241
- outputTokens: usageCounterSchema.optional(),
242
- costUsd: z.number().optional(),
243
- /** First GitHub PR URL spotted in the transcript (the janitor trick). */
244
- pullRequestUrl: z.string().optional(),
245
- /** The PR this task is ABOUT (#407, spec 2026-07-16-pr-autodiscovery):
246
- * auto-discovered from conversation references for tasks that work on an
247
- * existing PR (review/continue/merge). Display-only tier — `pullRequestUrl`
248
- * (the PR this task CREATED) always wins, and action gates ignore this. */
249
- referencedPullRequestUrl: z.string().optional(),
250
- /** The PR/issue number this task is ABOUT (spec 2026-07-17-task-auto-naming):
251
- * regex-extracted from the task prompt, upgradable by the namer's
252
- * cross-checked output. Display tier — never gates actions. */
253
- prNumber: z.number().optional(),
254
- issueNumber: z.number().optional(),
255
- /** Provenance for an `issueNumber` seeded by referenced-issue discovery.
256
- * Persisted so ambiguity can revoke only the janitor's own value, including
257
- * after a restart. Any prompt, namer, or marker write clears this flag. */
258
- referencedIssueNumberSeeded: z.boolean().optional(),
259
- /** Who owns the display title: `user` (PATCH rename — never auto-overwritten),
260
- * `marker` (agent-declared via `CEZ:TITLE`, spec 2026-07-18-task-ref-markers —
261
- * beats the namer, silences live refresh) or `auto` (namer-owned — a later
262
- * namer result may replace it). Missing on old runs = legacy behavior (auto
263
- * fills only an unset titleSummary). Precedence: user > marker > auto. */
264
- titleOrigin: z.enum(['user', 'auto', 'marker']).optional(),
265
- /** References the agent itself declared via `CEZ:PR=` / `CEZ:ISSUE=` markers
266
- * (spec 2026-07-18-task-ref-markers). Presence of a kind makes it
267
- * authoritative: the namer may no longer write that kind, and a declared PR
268
- * owns the referenced tier's resolution. */
269
- markerRefs: z.object({ pr: z.number().optional(), issue: z.number().optional() }).optional(),
270
- /** Distinct PR URLs spotted so far — the referenced tier's working set,
271
- * persisted so a resumed run keeps disambiguating against the full history
272
- * instead of re-adopting the next URL as "the only one". Capped. */
273
- referencedPrCandidates: z.array(z.string()).optional(),
274
- /** The issue this task is ABOUT (spec 2026-07-21-report-ref-discovery):
275
- * auto-discovered from `github.com/…/issues/N` links in the conversation,
276
- * mirroring the referenced-PR tier. Display-only; never gates actions. */
277
- referencedIssueUrl: z.string().optional(),
278
- /** Distinct issue URLs spotted so far — the referenced-issue working set,
279
- * persisted like `referencedPrCandidates`. Capped. */
280
- referencedIssueCandidates: z.array(z.string()).optional(),
281
- /** Explicit execution policy. `false` means the run intentionally uses the repo root;
282
- * absent on older runs and for the default isolated-worktree mode. */
283
- worktree: z.literal(false).optional(),
284
- /** Task worktree (spec 006) — absent for in-place runs and after explicit cleanup. */
285
- worktreePath: z.string().optional(),
286
- /** The task's own branch (`cez/<id8>`), created off `baseBranch`. */
287
- branch: z.string().optional(),
288
- /** Stable baseline for session git views: a worktree's fork ref, or an in-place run's starting commit. */
289
- baseBranch: z.string().optional(),
290
- /** Set when count-based retention (#483) reclaimed this run's worktree
291
- * *directory* (the `cez/<id8>` branch is kept). Presence means "materialized
292
- * dir gone, recoverable via `git worktree add`"; it excludes the run from the
293
- * retention budget until the dir is re-materialized (resume clears it). */
294
- worktreeReclaimedAt: z.string().optional(),
295
- /** Worktree setup (#917). An unreadable value drops the field, never the run. */
82
+ pullRequests: z.preprocess((value) => Array.isArray(value)
83
+ ? value.flatMap((entry) => {
84
+ const parsed = runPullRequestSchema.safeParse(entry);
85
+ return parsed.success ? [parsed.data] : [];
86
+ })
87
+ : undefined, z.array(runPullRequestSchema).optional()),
296
88
  worktreeSetup: contractRunRecordSchema.shape.worktreeSetup.catch(undefined),
297
- /** Parallel variants (spec 010): tasks sharing a groupId are one group. */
298
- groupId: z.string().optional(),
299
- /** Variant letter within the group — 'A' | 'B' | 'C' (kept as a string). */
300
- variant: z.string().optional(),
301
- /** Idempotent start (#504): the caller's request id and the hash of the start payload it
302
- * named (`src/runs/client-request.ts`). Absent on every run started without one. */
303
- clientRequestId: z.string().optional(),
304
- clientRequestHash: z.string().optional(),
305
- /** Peak resident memory (bytes) / process count observed across the run's
306
- * agent process trees (#348) — written when a session's telemetry ends.
307
- * Optional: old runs.json files and `ps`-less platforms have neither. */
308
- peakRssBytes: z.number().optional(),
309
- peakProcCount: z.number().optional(),
310
89
  archived: z.boolean().default(false),
311
- archivedAt: z.string().optional(),
312
- /** Pinned to the top of this project's task list (#935): plain per-task state, the
313
- * same class as `archived` and `seenAt`. Optional with NO default, unlike `archived`:
314
- * absent is what every runs.json written before this carries and it already means
315
- * "not pinned", so nothing needs filling in on parse and an unpin can simply delete
316
- * the key rather than persist a `false` older cezars never wrote. */
317
- pinned: z.boolean().optional(),
318
- pinnedAt: z.string().optional(),
319
- /** Read receipt (#unread-done-items): the ISO time the cockpit last opened this
320
- * run's thread. A finished run reads as "unread" until it has been seen since it
321
- * finished — see `isUnread()` in the cockpit's `lib/read-state.ts`. Absent on old
322
- * runs, on every run not yet opened, and on one `setUnread` put back to unread
323
- * (#775) — the unread rule treats all three alike. */
324
- seenAt: z.string().optional(),
325
- currentStepId: z.string().optional(),
326
- error: z.string().optional(),
327
- steps: z.array(stepStateSchema),
328
- /** Full workflow definition, persisted so a `queued` run can be re-enqueued
329
- * after a restart (#367) — including ad-hoc "(planned)" chains that exist
330
- * nowhere else.
331
- *
332
- * Typed, not `z.record(z.string(), z.unknown())`: this key goes out over the
333
- * wire on every run route, and `unknown` is wider than anything the server
334
- * can serialize — which made the route's own type (hono's `JSONValue`, whose
335
- * index signature admits `object | symbol | undefined`) impossible for the
336
- * contract to describe. `.catch(undefined)` keeps an older or hand-edited
337
- * entry from failing the whole index parse: a def that no longer fits simply
338
- * drops, and `reviveWorkflow` falls back to the catalog by name.
339
- *
340
- * That drop is PERMANENT, not per-boot — the index is re-serialized from the
341
- * parsed records (`saveNow`), so the next save writes runs.json back without
342
- * it. Harmless for a catalog workflow, which re-resolves by name; fatal for
343
- * the ad-hoc "(planned)" chain this field exists to preserve, which has no
344
- * catalog entry to fall back to. Nothing written since #367 fails the schema
345
- * (`name`, `source` and `steps` have been on every persisted def), so tighten
346
- * `workflowStepSchema` only with that in mind: a narrowing here silently eats
347
- * queued runs rather than degrading them. */
348
90
  workflowDef: workflowDefSchema.optional().catch(undefined),
91
+ steps: z.array(stepStateSchema),
92
+ automation: contractRunRecordSchema.shape.automation.unwrap().extend({
93
+ automationRevision: z.number().int().positive(),
94
+ githubUrl: z.string().url(),
95
+ }).optional(),
96
+ automationTrigger: contractRunRecordSchema.shape.automationTrigger.unwrap().extend({
97
+ automationRevision: z.number().int().positive(),
98
+ }).optional(),
99
+ /** The handoff file's autosave records (hearsay-tools/cezarion#1809); unreadable drops the field. */
100
+ handoffRecords: contractRunRecordSchema.shape.handoffRecords.catch(undefined),
349
101
  });
350
- /** Salvage CI state identically for live stores and the read-only workspace index. */
102
+ /** The keys an older record used for parks. Schema 4 reads the CI and monitoring ones into
103
+ * `waits`; phase 4b (hearsay-tools/cezarion#1236) reads the pending-question summary into a human
104
+ * wait. None is written again (spec 2026-10-17-unified-waits, § Compatibility). */
105
+ const LEGACY_WAIT_KEYS = ['ciWait', 'lastCiWait', 'lastCiWaitError', 'monitoringWakeAt', 'monitoringWakeCapReached', 'activity', 'hasPendingHumanAsk'];
106
+ const UNREADABLE_WAIT = 'Wait unavailable — a saved wait is unreadable; register a new wait.';
107
+ /** A stored `waits`/`lastWaits` entry of kind `ci`, the kind `pr` replaced (hearsay-tools/cezarion#1425). */
108
+ const isCiKindEntry = (entry) => !!entry && typeof entry === 'object' && entry.kind === 'ci';
109
+ function hasCiKindEntries(row) {
110
+ return ['waits', 'lastWaits'].some((key) => Array.isArray(row[key]) && row[key].some(isCiKindEntry));
111
+ }
112
+ function hasLegacyWaitFields(row) {
113
+ return !!row && typeof row === 'object' && (LEGACY_WAIT_KEYS.some((key) => key in row) || hasCiKindEntries(row) || agentWorkerWaits(row).length > 0 || rootCompletion(row) !== undefined);
114
+ }
115
+ /** A root's stored `delegation.completion` (the old completion gate's marker), which schema 7 reads
116
+ * into the list and drops (hearsay-tools/cezarion#1301). */
117
+ function rootCompletion(row) {
118
+ const delegation = row.delegation;
119
+ if (!delegation || typeof delegation !== 'object' || delegation.role !== 'root')
120
+ return undefined;
121
+ const { completion } = delegation;
122
+ return completion && typeof completion === 'object' ? completion : undefined;
123
+ }
124
+ /** The worker waits a record still holds in `delegation` (store schemas 6 and 7,
125
+ * hearsay-tools/cezarion#1235, #1301), in the order they settled: `lastWait`, then `wait`. The
126
+ * completion gate's wait, the one `delegation.completion.waitId` names, is flagged `completion`: it
127
+ * was Cezar's own, not the agent's. A value that does not parse is left where it is, for the
128
+ * delegation schema to quarantine as it always has. */
129
+ function agentWorkerWaits(row) {
130
+ const delegation = row.delegation;
131
+ if (!delegation || typeof delegation !== 'object')
132
+ return [];
133
+ const { role } = delegation;
134
+ if (role !== 'root' && role !== 'worker')
135
+ return [];
136
+ const waitId = rootCompletion(row)?.waitId;
137
+ const found = [];
138
+ for (const key of ['lastWait', 'wait']) {
139
+ const parsed = workerWaitSchema.safeParse(delegation[key]);
140
+ if (parsed.success)
141
+ found.push({ key, wait: parsed.data, completion: waitId === parsed.data.id });
142
+ }
143
+ return found;
144
+ }
145
+ /** A legacy `CiWait` as a wait entry. Its wake input's id was the wait's own (`wakeId`, which
146
+ * `run.ts` set to the wait id), so the entry keeps that id and delivery still finds the input. */
147
+ export function ciWaitEntry(wait) {
148
+ const { wakeId: _wakeId, phase, result, deliveredAt, ...identity } = wait;
149
+ return {
150
+ ...identity, kind: 'pr', source: 'agent', via: 'cezar_wait_for_ci',
151
+ phase: phase === 'registered' || phase === 'parked' ? 'open' : phase === 'wake-pending' ? 'resolved' : phase === 'delivered' ? 'delivered' : 'cancelled',
152
+ ...(result ? { result, resolvedAt: result.observedAt } : {}),
153
+ ...(deliveredAt ? { deliveredAt } : {}),
154
+ };
155
+ }
156
+ /**
157
+ * A legacy agent `delegation.wait` as a worker wait entry (store schema 6, hearsay-tools/cezarion#1235),
158
+ * judged from the wait alone: what became of its wake input is `upgradeLegacyWaits`' to apply.
159
+ * `registered`/`parked` → open; `wake-pending` → resolved with its `outcome`/`timeout` reason, or
160
+ * cancelled. Undefined for the `message` stand-in, which was never an agent's wait: a message no
161
+ * longer resolves a wait, and its input still wakes the run on its own.
162
+ *
163
+ * Deterministic, never read from the clock: a row is upgraded on every read until open rewrites
164
+ * it, and two reads must agree. A `WorkerWait` kept no registration time, so `timeoutSeconds` is
165
+ * the 600 default, `registeredAt` is 600 s before the original deadline (kept as it was), and the
166
+ * settled stamps come from the wait itself: `resolvedAt` is the latest observation it holds, else
167
+ * its deadline, and `cancelledAt` is its deadline. `now` is only the fallback for a deadline that
168
+ * does not read, which the wait schema never admits.
169
+ */
170
+ export function workerWaitEntry(wait, now) {
171
+ if (wait.phase === 'wake-pending' && wait.reason === 'message')
172
+ return undefined;
173
+ const deadline = Date.parse(wait.deadline);
174
+ const registeredAt = Number.isFinite(deadline) ? new Date(deadline - 600_000).toISOString() : now;
175
+ const open = wait.phase !== 'wake-pending';
176
+ // A pending wait from before reasons were recorded settled on its persisted observations, as
177
+ // `reconcileWorkerWait` infers it: satisfied by them is an outcome, anything else its deadline.
178
+ const satisfied = (ids, reported) => wait.mode === 'all' ? ids.every((id) => reported.includes(id)) : reported.length > 0;
179
+ const inferred = wait.phase === 'wake-pending' && wait.reason === undefined
180
+ ? (wait.requestIds ? satisfied(wait.requestIds, (wait.requestOutcomes ?? []).map((outcome) => outcome.requestId))
181
+ : satisfied(wait.workerIds, (wait.outcomes ?? []).map((outcome) => outcome.workerId))) ? 'outcome' : 'timeout'
182
+ : undefined;
183
+ const reason = wait.reason === 'outcome' || wait.reason === 'timeout' ? wait.reason : inferred;
184
+ const observed = [...(wait.outcomes ?? []), ...(wait.requestOutcomes ?? [])].map((outcome) => outcome.observedAt).sort().at(-1);
185
+ const resolvedAt = reason === 'outcome' && observed ? observed : wait.deadline;
186
+ return {
187
+ id: wait.id, kind: 'worker', source: 'agent', via: 'cez-worker-wait', registeredAt, deadline: wait.deadline, timeoutSeconds: 600, turnId: 'upgraded',
188
+ phase: open ? 'open' : reason ? 'resolved' : 'cancelled',
189
+ ...(wait.mode ? { mode: wait.mode } : {}),
190
+ ...(wait.requestIds
191
+ ? { requestIds: wait.requestIds, requestOutcomes: wait.requestOutcomes ?? [] }
192
+ : { workerIds: wait.workerIds, revisions: wait.revisions ?? [], outcomes: wait.outcomes }),
193
+ ...(!open && reason ? { result: { reason }, resolvedAt } : {}),
194
+ ...(!open && !reason ? { cancelledAt: wait.deadline } : {}),
195
+ };
196
+ }
197
+ /** Whether a record is a root still parked on its workers: an open or resolved worker wait in the
198
+ * list. Such a root has its pending human question re-read from the transcript before a manager
199
+ * parks it. Every stored `delegation.wait`, the completion gate's included, is read into the list
200
+ * on open (store schema 7, hearsay-tools/cezarion#1301), so the list is the only place to look. */
201
+ export function holdsWorkerWait(run) {
202
+ if (run.delegation?.role !== 'root')
203
+ return false;
204
+ return !!run.waits?.some((wait) => wait.kind === 'worker' && (wait.phase === 'open' || wait.phase === 'resolved'));
205
+ }
206
+ /** A stable id for the timer a legacy monitoring park becomes. The row keeps its legacy JSON
207
+ * until it is next written, so every read of it must name the same wait. */
208
+ function legacyTimerId(runId, wakeAt) {
209
+ return legacyWaitId(`legacy-monitoring-timer:${String(runId)}:${wakeAt}`);
210
+ }
211
+ /** The human wait a stored pending-question summary becomes: one per run, the same on every read. */
212
+ function legacyHumanWaitId(runId) {
213
+ return legacyWaitId(`legacy-pending-ask:${String(runId)}`);
214
+ }
215
+ /** A name-based (version 5 shaped) UUID, so a row read twice upgrades to the same entry. */
216
+ function legacyWaitId(name) {
217
+ const hex = createHash('sha256').update(name).digest('hex');
218
+ const variant = (8 | (Number.parseInt(hex[16], 16) & 3)).toString(16);
219
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-5${hex.slice(13, 16)}-${variant}${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
220
+ }
221
+ /**
222
+ * Upgrade-on-read (store schema 4, spec 2026-10-17-unified-waits): read a schema-3 record's CI and
223
+ * monitoring fields into its wait list, then delete them. Mutates `row`, before validation.
224
+ *
225
+ * - `ciWait`: `registered`/`parked` → an open `pr` checks wait, `wake-pending` → resolved (resolved when
226
+ * its result was observed), `delivered`/`withdrawn` → `lastWaits` as delivered/cancelled.
227
+ * - `lastCiWait` → `lastWaits`: `delivered` stays delivered, anything else was withdrawn intent and
228
+ * reads as cancelled. `lastCiWaitError` → `lastWaitsError`.
229
+ * - `activity: 'monitoring'` on a live run: with `monitoringWakeAt`, an open timer due then
230
+ * (`seconds` is what remains, clamped to a timer's bounds); without one, no wait. Either way a
231
+ * `running` run parks as `waiting`, its running step with it, as every waiting park writes it.
232
+ * On a finished run the fields are stale and only dropped.
233
+ * - `hasPendingHumanAsk: true` on a `waiting` run with no open human wait (phase 4b,
234
+ * hearsay-tools/cezarion#1236): an open `human` wait, `ask-line`, with no `askSeq`. Recovery shows
235
+ * it (`surfaceHumanWait`), which attaches it to the question still pending in the transcript
236
+ * instead of raising a second card. Anywhere else the summary is dropped: the human wait it stood
237
+ * beside since phase 2 already says the same.
238
+ *
239
+ * - `delegation.wait`/`lastWait` registered by the agent (store schema 6, hearsay-tools/cezarion#1235)
240
+ * → `workerWaitEntry`, then settled by what became of the wake input (`wakeId`, else the wait's own
241
+ * id): a wait whose input was delivered reads as delivered, a `wake-pending` one whose input is
242
+ * still queued stays resolved and records it as `wakeInputId`, so it is delivered once, and a
243
+ * `lastWait` that was never delivered reads as cancelled. The `message` stand-in is dropped. No
244
+ * input is added or removed: a cancelled receipt's undelivered wake stays queued and is delivered
245
+ * as an ordinary input. The completion gate's wait (store schema 7, hearsay-tools/cezarion#1301) becomes a
246
+ * `source: 'cezar'`, `via: 'completion'` entry the same way, and `delegation.completion` is dropped. A current wait
247
+ * (the stand-in included) whose wake a provider took on a `waiting` run is the old engine's crash
248
+ * between the delivery checkpoint and the resume: the run reads `running`, so recovery resumes
249
+ * it. A wake an inbox read acknowledged, or one beside a pending question, resumes nothing.
250
+ *
251
+ * - A `waits`/`lastWaits` entry of kind `ci` (hearsay-tools/cezarion#1425) → kind `pr` with
252
+ * `until: 'checks'` and `wakeOnConflict: true`, what a `ci` wait did.
253
+ *
254
+ * An unreadable legacy CI wait gets the per-entry salvage `parseRunRecords` gives `waits`: it is
255
+ * dropped, `lastWaitsError` says so, and the status is left alone. A `waiting` run with no open
256
+ * wait reads as Needs you, which a human can resume.
257
+ */
258
+ /** A live run and its live step move from `from` to `to` together, as every park and resume writes them. */
259
+ function setLiveStatus(row, from, to) {
260
+ row.status = to;
261
+ if (Array.isArray(row.steps))
262
+ for (const step of row.steps) {
263
+ if (step && typeof step === 'object' && step.status === from)
264
+ step.status = to;
265
+ }
266
+ }
267
+ const resumeParked = (row) => setLiveStatus(row, 'waiting', 'running');
268
+ export function upgradeLegacyWaits(row) {
269
+ if (!hasLegacyWaitFields(row))
270
+ return;
271
+ // Kind `ci` waited for checks and woke on a conflict (hearsay-tools/cezarion#1425).
272
+ for (const key of ['waits', 'lastWaits']) {
273
+ if (Array.isArray(row[key]))
274
+ row[key] = row[key].map((entry) => isCiKindEntry(entry)
275
+ ? { ...entry, kind: 'pr', until: 'checks', wakeOnConflict: true } : entry);
276
+ }
277
+ const opened = [];
278
+ const settled = [];
279
+ let unreadable = false;
280
+ const legacyCi = (value, previous) => {
281
+ if (value === undefined)
282
+ return;
283
+ const parsed = ciWaitSchema.safeParse(value);
284
+ if (!parsed.success) {
285
+ unreadable = true;
286
+ return;
287
+ }
288
+ const entry = ciWaitEntry(parsed.data);
289
+ if (previous && entry.phase !== 'delivered')
290
+ entry.phase = 'cancelled';
291
+ (entry.phase === 'open' || entry.phase === 'resolved' ? opened : settled).push(entry);
292
+ };
293
+ // The previous wait settled before the current one did, so it goes first.
294
+ legacyCi(row.lastCiWait, true);
295
+ legacyCi(row.ciWait, false);
296
+ const agentWaits = agentWorkerWaits(row);
297
+ if (agentWaits.length) {
298
+ // The wait's own stamps decide every time the entry carries (`workerWaitEntry`), not the clock.
299
+ const now = typeof row.createdAt === 'string' ? row.createdAt : new Date(0).toISOString();
300
+ const inputs = Array.isArray(row.agentInputs) ? row.agentInputs : [];
301
+ const current = agentWaits.find((found) => found.key === 'wait');
302
+ // The old engine retired a delivered wake into `lastWait` with the resume: a current wait whose
303
+ // wake a provider took on a `waiting` run is a crash between the two, and resumed. An inbox
304
+ // read starts no provider turn, and a pending question still holds the run, as they did then.
305
+ const interrupted = (key, input) => key === 'wait' && row.status === 'waiting' &&
306
+ typeof input?.deliveredAt === 'string' && !input.inboxClaim?.acknowledgedAt && row.hasPendingHumanAsk !== true;
307
+ for (const { key, wait, completion } of agentWaits) {
308
+ // A wait that settles has its copy in `lastWait` too: it is the same wait.
309
+ if (key === 'lastWait' && current?.wait.id === wait.id)
310
+ continue;
311
+ const input = inputs.find((candidate) => candidate.id === (wait.wakeId ?? wait.id));
312
+ const entry = workerWaitEntry(wait, now);
313
+ // Cezar's completion gate wait is its own: no agent registered it (hearsay-tools/cezarion#1301).
314
+ // It keeps the deadline it always carried, with the 600 s default registration.
315
+ if (entry && completion)
316
+ Object.assign(entry, { source: 'cezar', via: 'completion', mode: 'all' });
317
+ // The `message` stand-in becomes no entry, but its delivered message may be that crash too.
318
+ if (!entry) {
319
+ if (interrupted(key, input))
320
+ resumeParked(row);
321
+ continue;
322
+ }
323
+ const deliveredAt = typeof input?.deliveredAt === 'string' ? input.deliveredAt : undefined;
324
+ if (entry.phase !== 'open' && deliveredAt) {
325
+ Object.assign(entry, { phase: 'delivered', deliveredAt });
326
+ if (interrupted(key, input))
327
+ resumeParked(row);
328
+ }
329
+ else if (key === 'lastWait' && entry.phase !== 'cancelled')
330
+ Object.assign(entry, { phase: 'cancelled', cancelledAt: wait.deadline });
331
+ else if (entry.phase === 'resolved' && input)
332
+ entry.wakeInputId = input.id;
333
+ (entry.phase === 'open' || entry.phase === 'resolved' ? opened : settled).push(entry);
334
+ }
335
+ }
336
+ const live = row.status === 'running' || row.status === 'queued' || row.status === 'waiting';
337
+ if (row.activity === 'monitoring' && live) {
338
+ const wakeAt = row.monitoringWakeAt;
339
+ // Schema 3 read an unreadable deadline as none (`.catch(undefined)`): no wait.
340
+ if (typeof wakeAt === 'string' && z.iso.datetime().safeParse(wakeAt).success) {
341
+ const now = Date.now();
342
+ const seconds = Math.min(TIMER_MAX_SECONDS, Math.max(TIMER_MIN_SECONDS, Math.round((Date.parse(wakeAt) - now) / 1000)));
343
+ opened.push({
344
+ id: legacyTimerId(row.id, wakeAt), kind: 'timer', source: 'agent', via: 'monitoring-line',
345
+ registeredAt: new Date(now).toISOString(), deadline: wakeAt, phase: 'open', turnId: 'legacy', seconds,
346
+ });
347
+ }
348
+ if (row.status === 'running')
349
+ setLiveStatus(row, 'running', 'waiting');
350
+ }
351
+ const asksHuman = (entry) => !!entry && typeof entry === 'object' && entry.kind === 'human' && entry.phase === 'open';
352
+ if (row.hasPendingHumanAsk === true && row.status === 'waiting' &&
353
+ !(Array.isArray(row.waits) && row.waits.some(asksHuman)) && !opened.some(asksHuman)) {
354
+ opened.push({
355
+ id: legacyHumanWaitId(row.id), kind: 'human', source: 'agent', via: 'ask-line',
356
+ registeredAt: typeof row.createdAt === 'string' && z.iso.datetime().safeParse(row.createdAt).success ? row.createdAt : new Date(0).toISOString(),
357
+ phase: 'open', turnId: 'legacy',
358
+ });
359
+ }
360
+ if (opened.length) {
361
+ const current = Array.isArray(row.waits) ? row.waits : [];
362
+ if (row.waits !== undefined && !Array.isArray(row.waits))
363
+ unreadable = true;
364
+ row.waits = [...current, ...opened.filter((wait) => !current.some((entry) => entry?.id === wait.id))];
365
+ }
366
+ if (settled.length) {
367
+ const current = Array.isArray(row.lastWaits) ? row.lastWaits : [];
368
+ if (row.lastWaits !== undefined && !Array.isArray(row.lastWaits))
369
+ unreadable = true;
370
+ row.lastWaits = [...settled, ...current].slice(-LAST_WAITS_LIMIT);
371
+ }
372
+ if (typeof row.lastCiWaitError === 'string' && row.lastWaitsError === undefined)
373
+ row.lastWaitsError = row.lastCiWaitError;
374
+ if (unreadable)
375
+ row.lastWaitsError = UNREADABLE_WAIT;
376
+ for (const key of LEGACY_WAIT_KEYS)
377
+ delete row[key];
378
+ if (agentWaits.length || rootCompletion(row)) {
379
+ const delegation = { ...row.delegation };
380
+ for (const { key } of agentWaits)
381
+ delete delegation[key];
382
+ delete delegation.completion;
383
+ row.delegation = delegation;
384
+ }
385
+ }
386
+ /** Read legacy wait fields into the wait list and salvage per entry, identically for live stores
387
+ * and the read-only workspace index. */
351
388
  export function parseRunRecords(raw) {
352
389
  if (Array.isArray(raw))
353
390
  for (const row of raw) {
354
391
  if (!row || typeof row !== 'object')
355
392
  continue;
356
- if (row.ciWait !== undefined && !ciWaitSchema.safeParse(row.ciWait).success) {
357
- delete row.ciWait;
358
- if (['running', 'queued', 'waiting'].includes(row.status)) {
359
- row.status = 'failed';
360
- delete row.activity;
361
- row.error = 'CI wait state is unreadable; continue the task to register a new wait.';
362
- }
363
- }
364
- if (row.lastCiWait !== undefined && !ciWaitSchema.safeParse(row.lastCiWait).success) {
365
- delete row.lastCiWait;
366
- row.lastCiWaitError = 'CI wait unavailable — saved observation is unreadable; register a new wait.';
393
+ upgradeLegacyWaits(row);
394
+ // One unreadable wait must not evict the run, nor its readable siblings.
395
+ for (const key of ['waits', 'lastWaits']) {
396
+ if (row[key] === undefined)
397
+ continue;
398
+ const entries = Array.isArray(row[key]) ? row[key] : [];
399
+ const readable = entries.filter((entry) => waitSchema.safeParse(entry).success);
400
+ if (!Array.isArray(row[key]) || readable.length !== entries.length)
401
+ row.lastWaitsError = UNREADABLE_WAIT;
402
+ if (readable.length)
403
+ row[key] = readable;
404
+ else
405
+ delete row[key];
367
406
  }
368
407
  // One unreadable registration must not evict the run, nor its readable siblings (#781).
369
408
  if (row.previewServers !== undefined) {
@@ -384,11 +423,12 @@ function storedRow({ revision, seq, data, extras }) {
384
423
  * server entry) the runtime has decided to drop, and keeps dropping on the next write, as it always
385
424
  * has; only what the schema does not know is put back. */
386
425
  function decodeStoredRecord(raw) {
426
+ const upgraded = hasLegacyWaitFields(raw);
387
427
  const parsed = parseRunRecords([raw]);
388
428
  if (!parsed.success)
389
429
  return undefined;
390
430
  const run = parsed.data[0];
391
- return { run, extras: collectRawExtras(raw, run) };
431
+ return { run, extras: collectRawExtras(raw, run), ...(upgraded ? { upgraded: true } : {}) };
392
432
  }
393
433
  /** One database row's `data` back into a record and its extras. Undefined when it does not parse:
394
434
  * one unreadable row costs that row only. */
@@ -419,6 +459,9 @@ const OPEN_RETRY_DELAYS_MS = [50, 100, 200, 400, 800, 1600];
419
459
  function sleepSync(ms) {
420
460
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
421
461
  }
462
+ /** Runs (and conversation roots, one family query each) the open-time facts warm-up enumerates
463
+ * per event-loop turn (hearsay-tools/cezarion#949), so each slice stays a few milliseconds. */
464
+ const WARM_IDS_PER_SLICE = 100;
422
465
  const MAX_RUNS_KEPT = 300;
423
466
  /** How many archived root runs the run lists carry per project (#864): `GET /run-summaries?archived=recent`
424
467
  * and `GET /workspace/runs-index`. Unarchived runs are never cut; older archived runs page in
@@ -442,25 +485,9 @@ function refUrlRepo(url) {
442
485
  return owner && name ? `${owner}/${name}`.toLowerCase() : undefined;
443
486
  }
444
487
  /**
445
- * May the referenced tier ADOPT this URL as the task's subject? (#945)
446
- *
447
- * The tier was text-scoped but never repo-scoped: `PR_URL_RE` matches any
448
- * `github.com/<owner>/<repo>/pull/N`, so a research task that cites one upstream PR handed the
449
- * resolver exactly one candidate and it became the task's identity — an `oko` task wearing
450
- * `supabase/cli#6056`. Nothing compared the URL's repository with the project's own.
451
- *
452
- * A foreign URL is adoptable only when the TASK PROMPT corroborates it: the prompt names that
453
- * `owner/repo`, which a pasted URL does inherently. That is the trust boundary this module already
454
- * uses elsewhere — the prompt and the agent's own turn text are trusted, scraped tool output is
455
- * not — and it is what keeps the legitimate cross-repo case working (#819:
456
- * `om-auto-fix-pr https://github.com/open-mercato/open-mercato/pull/1977` started from cezar).
457
- *
458
- * Unknown handle → today's behavior exactly (`AGENTS.md` zero config: degrade, never fail). An
459
- * unparseable URL is left alone for the same reason — the guard only ever removes an association
460
- * it can PROVE is foreign.
461
- *
462
- * Note what this does not touch: `referenced*Candidates` keep recording every URL as evidence.
463
- * The fix changes what is *promoted*, never what is *collected* (the #526 rule).
488
+ * A foreign referenced URL needs task-prompt corroboration of its repository. Unknown project
489
+ * handles preserve the degraded rule. This check vets the resolved winner, not the candidate
490
+ * list.
464
491
  */
465
492
  function isRepoScopedRef(url, task, handle) {
466
493
  if (!handle)
@@ -470,6 +497,13 @@ function isRepoScopedRef(url, task, handle) {
470
497
  return true;
471
498
  if (repo === `${handle.owner}/${handle.name}`.toLowerCase())
472
499
  return true;
500
+ return promptNamesRepo(url, task);
501
+ }
502
+ /** Positive scope evidence for permanent declarations; legacy discovery still fails open. */
503
+ function promptNamesRepo(url, task) {
504
+ const repo = refUrlRepo(url);
505
+ if (!repo)
506
+ return false;
473
507
  // Match whole owner/repository segments: naming acme/service2 must not corroborate
474
508
  // acme/service. Slashes remain valid boundaries for full URLs and their /pull or /issues path.
475
509
  const escapedRepo = repo.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -491,9 +525,30 @@ function isRepoScopedRef(url, task, handle) {
491
525
  * an archived run, so a cleared field is the whole cancellation.
492
526
  */
493
527
  function clearPendingAutoResume(run) {
528
+ // A withdrawn appointment settles the run failed with its kept error (hearsay-tools/cezarion#1236),
529
+ // the state a usage-limited run was in before it waited.
530
+ if (isUsageLimitHold(run)) {
531
+ run.status = 'failed';
532
+ run.finishedAt ??= new Date().toISOString();
533
+ }
494
534
  run.autoResumeAt = undefined;
495
535
  run.autoResumeAttempts = undefined;
496
536
  }
537
+ /**
538
+ * The write rule (hearsay-tools/cezarion#1496): a resume time is the appointment of a `waiting`
539
+ * run, so every commit takes it off a record that is not `waiting`. A copy, set to `undefined`
540
+ * and never deleted: a caller may stage the held record itself. `autoResumeAttempts` stays, since
541
+ * `MAX_AUTO_RESUMES` counts attempts across resumes.
542
+ */
543
+ function withoutStaleResumes(input) {
544
+ let staged;
545
+ for (const [id, run] of input) {
546
+ if (run?.autoResumeAt === undefined || isUsageLimitHold(run))
547
+ continue;
548
+ (staged ??= new Map(input)).set(id, { ...run, autoResumeAt: undefined });
549
+ }
550
+ return staged ?? input;
551
+ }
497
552
  /**
498
553
  * Archiving is resigning from a task, so it retires the pin too (#935) — a pin on a task the
499
554
  * user has filed away is stale by definition, and the archived view collapses into one bucket
@@ -539,22 +594,15 @@ function eventTextFragments(event) {
539
594
  return fragments;
540
595
  }
541
596
  /**
542
- * Where a CREATION CLAIM may come from — the trust boundary the created tier was missing.
543
- *
544
- * `CREATED_PR_RE` used to be matched against everything an event carried, tool OUTPUT included,
545
- * so a transcript that merely QUOTES a `gh pr create` line handed the run a PR it never opened.
546
- * Not hypothetical: the task that fixed the reference chips printed another run's stored events
547
- * while investigating them, and cezar read `"title": "Ran gh pr create --repo …"` out of that
548
- * dump and adopted a PR from a DIFFERENT repository as its own — permanently, because the first
549
- * created URL wins and the real `gh pr create` that followed was never looked at.
550
- *
551
- * So the claim must come from the agent's own words, or from the tool title cezar itself renders
552
- * from the command it saw run. Tool output and tool input are the transcript of the world, not a
553
- * statement about this run. The URL is still read from the whole event — `gh` prints it in the
554
- * output — because it is the CLAIM that needs a trustworthy source, not the link.
597
+ * Creation claims come from agent prose or cezar-rendered tool titles, never arbitrary tool
598
+ * output/input. The claimed URL may still come from the event’s command result.
555
599
  */
556
600
  function eventCreationClaimFragments(event) {
557
601
  const fragments = [];
602
+ const nested = event.item;
603
+ if (event.parentItemId !== undefined || (nested && typeof nested === 'object' &&
604
+ nested.parentItemId !== undefined))
605
+ return fragments;
558
606
  // A `tool-result` event's `result` IS raw command output; on every other event the top-level
559
607
  // text is the agent's own.
560
608
  if (event.type !== 'tool-result') {
@@ -595,21 +643,9 @@ function eventAgentTextFragments(event) {
595
643
  return fragments;
596
644
  }
597
645
  /**
598
- * The referenced tier's resolution rule, shared by the PR and issue janitors:
599
- * a marker-declared number (spec 2026-07-18-task-ref-markers) owns the answer
600
- * outright — only a candidate URL ending in that number resolves, and a
601
- * contradiction clears the chip. Without a declaration: one distinct URL is
602
- * the subject; among several, prefer the exact URL pasted in the prompt, then
603
- * the one whose number the task prompt names (and
604
- * only when exactly one matches); otherwise ambiguous — no chip beats a wrong
605
- * chip.
606
- *
607
- * Whatever that produces is then repo-scoped (#945): a winner from another repository that the
608
- * prompt does not corroborate is vetoed — see `isRepoScopedRef`. The veto is applied to the
609
- * RESULT rather than to the candidate list on purpose, so the guard stays strictly subtractive:
610
- * filtering first would let a project-local candidate win a two-candidate race today's rule calls
611
- * ambiguous, which is a wider behavior change than the defect warrants. As written this function
612
- * can only ever lose a value, never gain one.
646
+ * Resolve declarations and prompt-supported candidates without guessing through ambiguity. Apply
647
+ * repository scope to the winner only; filtering candidates first could promote an otherwise
648
+ * ambiguous reference.
613
649
  */
614
650
  function resolveReferencedRef(candidates, task, declared, handle) {
615
651
  const resolved = resolveCandidate(candidates, task, declared);
@@ -648,20 +684,8 @@ function refUrlNumber(url) {
648
684
  return Number.isInteger(n) && n > 0 && n < MAX_REF ? n : undefined;
649
685
  }
650
686
  /**
651
- * The PR declaration the REFERENCED tier is allowed to act on.
652
- *
653
- * `CEZ:PR=N` means one of two things depending on when the agent writes it: on the way in it
654
- * names the PR the task is ABOUT, and once the task has opened a PR of its own the marker
655
- * contract asks it to re-declare with the new number ("Re-emit with the new number if the subject
656
- * changes (e.g. you open a PR later in the task)"). A declaration naming the PR this run CREATED
657
- * is therefore a statement about the CREATED tier, which `pullRequestUrl` already carries — and
658
- * feeding it to the referenced tier ERASES the about-PR, because `resolveReferencedRef` clears
659
- * the chip when no candidate matches the declared number (a task on #4326 that opened
660
- * #5366 dropped from two chips to one the moment it declared #5366).
661
- *
662
- * Both tiers stay true instead: the created PR is the created PR, and the reference resolves as
663
- * if that declaration had not been made — which is exactly what it was before the task opened
664
- * anything.
687
+ * Ignore a created-PR declaration when resolving the referenced subject, so a task can retain
688
+ * both its original subject and its new PR.
665
689
  */
666
690
  function referencedPrDeclaration(run) {
667
691
  const declared = run.markerRefs?.pr;
@@ -669,6 +693,50 @@ function referencedPrDeclaration(run) {
669
693
  return undefined;
670
694
  return declared === refUrlNumber(run.pullRequestUrl) ? undefined : declared;
671
695
  }
696
+ /** Prompt attribution survives declarations of follow-up issues. Bare hashes become issues
697
+ * only after an issue signal; explicit PR prompts never acquire an invented issue identity. */
698
+ function originatingIssue(run) {
699
+ // Only an issue-led subject is stable attribution. Mentions inside comparisons or
700
+ // background instructions remain evidence, but an agent declaration may supersede them.
701
+ const prompt = run.task.trimStart()
702
+ .replace(/^(?:please\s+)?(?:(?:can|could|would|will)\s+you\s+|i\s+(?:want|would\s+like)\s+you\s+to\s+)/i, '')
703
+ .replace(/^(?:please\s+)?(?:fix|resolve|address|implement|investigate|review|work(?:\s+on)?)\s+/i, '');
704
+ const subject = /^(?:(?:the\s+)?(?:(?:github|fork|upstream)\s+)?issue\s*|(?:fork|upstream)\s+)?(?:https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/issues\/\d+|(?:[\w.-]+\/[\w.-]+)?#?\d+)\b/i.exec(prompt);
705
+ if (!subject)
706
+ return undefined;
707
+ const refs = extractTaskRefs(subject[0]);
708
+ return refs.issueNumber ?? ((run.issueNumber !== undefined || run.markerRefs?.issue !== undefined)
709
+ ? refs.ambiguousNumber : undefined);
710
+ }
711
+ function referencedIssueDeclaration(run) {
712
+ return run.markerRefs?.issue === undefined ? undefined : originatingIssue(run) ?? run.markerRefs.issue;
713
+ }
714
+ function resolveIssueReference(run, handle) {
715
+ const declared = referencedIssueDeclaration(run);
716
+ const candidates = run.referencedIssueCandidates ?? [];
717
+ // Prompt URLs remain evidence even in legacy records without a candidate working set.
718
+ const promptUrls = [...new Set([...run.task.matchAll(new RegExp(ISSUE_URL_RE.source, 'g'))]
719
+ .map(match => match[0]).filter(url => refUrlNumber(url) === declared))];
720
+ if (promptUrls.length > 1)
721
+ return undefined;
722
+ if (promptUrls.length === 1)
723
+ return resolveReferencedRef(promptUrls, run.task, declared, handle);
724
+ // A number cannot choose between two repositories' issues with that same number.
725
+ if (declared !== undefined && new Set(candidates.filter(url => refUrlNumber(url) === declared)).size > 1) {
726
+ return undefined;
727
+ }
728
+ return resolveReferencedRef(candidates, run.task, declared, handle);
729
+ }
730
+ function restoreOriginatingIssue(run) {
731
+ const origin = originatingIssue(run);
732
+ if (origin === undefined || run.markerRefs?.issue === undefined)
733
+ return;
734
+ run.issueNumber = origin;
735
+ delete run.referencedIssueNumberSeeded;
736
+ if (run.referencedIssueCandidates !== undefined || refUrlNumber(run.referencedIssueUrl) !== origin) {
737
+ run.referencedIssueUrl = resolveIssueReference(run);
738
+ }
739
+ }
672
740
  /**
673
741
  * The PR URL a creation phrase *introduces*, or undefined. The created URL is
674
742
  * the first one at or after the `CREATED_PR_RE` phrase — a PR the same event
@@ -690,6 +758,33 @@ function createdPrUrl(haystack) {
690
758
  }
691
759
  return before;
692
760
  }
761
+ /** Associate one URL per claim; command outputs remain separate from authored claims. */
762
+ function createdPrUrls(claim, haystack, toolOutput) {
763
+ const phrases = [...claim.matchAll(new RegExp(CREATED_PR_RE.source, 'gi'))];
764
+ if (!phrases.length)
765
+ return [];
766
+ const urls = phrases.flatMap((phrase, index) => {
767
+ const preceding = claim.slice(index ? phrases[index - 1].index + phrases[index - 1][0].length : 0, phrase.index);
768
+ const before = [...preceding.matchAll(new RegExp(PR_URL_RE.source, 'g'))].at(-1);
769
+ // URL-first reports put only punctuation/whitespace between the URL and its claim.
770
+ // An earlier URL followed by unrelated prose is still context, not a created resource.
771
+ if (before && /^[\s:—–-]*(?:draft\s+)?$/i.test(preceding.slice(before.index + before[0].length)))
772
+ return [before[0]];
773
+ const after = PR_URL_RE.exec(claim.slice(phrase.index, phrases[index + 1]?.index))?.[0];
774
+ return after ? [after] : [];
775
+ });
776
+ if (!urls.length && toolOutput !== undefined) {
777
+ // gh prints one URL per successful command. Never promote more output links than commands.
778
+ const outputs = [...toolOutput.matchAll(new RegExp(PR_URL_RE.source, 'g'))];
779
+ urls.push(...outputs.slice(0, phrases.length).map(match => match[0]));
780
+ }
781
+ if (!urls.length && phrases.length === 1) {
782
+ const url = createdPrUrl(`${claim} ${haystack}`);
783
+ if (url)
784
+ urls.push(url);
785
+ }
786
+ return [...new Set(urls)];
787
+ }
693
788
  /**
694
789
  * Reconcile one record just read off disk with the fact that whichever process wrote it is gone.
695
790
  *
@@ -704,6 +799,18 @@ function createdPrUrl(haystack) {
704
799
  * are marked failed so no ghost stays behind.
705
800
  */
706
801
  export function reconcileLoadedRun(run, opts) {
802
+ settleLoadedStop(run);
803
+ restoreLoadedUsageLimitHold(run);
804
+ failLoadedInterruptedRun(run, opts);
805
+ clearLoadedStaleResume(run);
806
+ restoreLostReferencedPullRequest(run, referencedPrDeclaration, resolveReferencedRef);
807
+ restoreOriginatingIssue(run);
808
+ // Repository scoping follows reconciliation: stores arm a handle asynchronously, while
809
+ // the read-only index applies its cached handle to these fresh records (#97). This ordering
810
+ // also scopes any referenced PR restored above without filtering candidate evidence first.
811
+ return run;
812
+ }
813
+ function settleLoadedStop(run) {
707
814
  // An accepted Stop is durable intent, never an interrupted run to auto-resume.
708
815
  if (run.stopping) {
709
816
  run.status = 'cancelled';
@@ -714,13 +821,23 @@ export function reconcileLoadedRun(run, opts) {
714
821
  }
715
822
  }
716
823
  run.stopping = undefined;
824
+ }
825
+ function restoreLoadedUsageLimitHold(run) {
826
+ // A usage-limited run is waiting for its resume (hearsay-tools/cezarion#1236, phase 4a). Rows a
827
+ // schema-7 cezar wrote kept it failed with the appointment; they read as the waiting run they are.
828
+ if (run.status === 'failed' && run.autoResumeAt !== undefined) {
829
+ run.status = 'waiting';
830
+ run.finishedAt = undefined;
831
+ }
832
+ }
833
+ function failLoadedInterruptedRun(run, opts) {
717
834
  // A run that was live when the previous process exited can never finish —
718
835
  // surface that instead of a forever-"running" ghost. `review` survives
719
836
  // restarts on purpose: the gate is pure data (worktree + branch + record)
720
837
  // with no live process, so the diff panel, Send back (resume) and Draft PR
721
838
  // all still work.
722
- if (!opts?.keepLive &&
723
- (run.status === 'running' || run.status === 'queued' || run.status === 'waiting')) {
839
+ // A usage-limit hold runs no process, so no exit interrupted it: its appointment stands.
840
+ if (!opts?.keepLive && engineOwnsRun(run)) {
724
841
  run.status = 'failed';
725
842
  run.error = 'interrupted — cezar process exited during the run';
726
843
  run.finishedAt = run.finishedAt ?? new Date().toISOString();
@@ -729,44 +846,19 @@ export function reconcileLoadedRun(run, opts) {
729
846
  step.status = 'failed';
730
847
  }
731
848
  }
732
- if (!['running', 'waiting', 'queued'].includes(run.status)) {
733
- run.activity = undefined;
734
- run.monitoringWakeAt = undefined;
735
- }
849
+ }
850
+ function clearLoadedStaleResume(run) {
851
+ // Open waits are left alone at every status: RunManager cancels them when a run ends (spec
852
+ // 2026-10-17-unified-waits), and recovery reads the ones a crash left behind.
736
853
  // A pending usage-limit resume survives the restart on purpose (the wait can be
737
854
  // hours) — `RunManager.recover()` re-arms it from this field. It can only mean
738
- // anything on a `failed` run, so anywhere else it is stale bookkeeping.
739
- if (run.status !== 'failed')
855
+ // anything on a `waiting` run, so anywhere else it is stale bookkeeping.
856
+ if (run.status !== 'waiting')
740
857
  run.autoResumeAt = undefined;
741
- // The wake counter is intentionally process-local, so a restarted process
742
- // starts a fresh epoch instead of displaying a stale cap.
743
- run.monitoringWakeCapReached = undefined;
744
- // Heal a record written before `referencedPrDeclaration` existed: a task that re-declared
745
- // `CEZ:PR` with the PR it had just CREATED cleared the PR it was ABOUT, because no candidate
746
- // could match the created number. The evidence is all still on the record — only the
747
- // conclusion drawn from it was wrong — so re-resolve without that declaration instead of
748
- // asking for a migration. Deliberately one-directional: it only runs on a record that HAS no
749
- // referenced PR, so it can never take one away from a record written by an older cezar whose
750
- // candidate list no longer explains it. `prNumber` is not recoverable this way (the
751
- // declaration overwrote it) and is left alone — the restored URL is what paints the chip.
752
- if (run.referencedPullRequestUrl === undefined &&
753
- run.markerRefs?.pr !== undefined &&
754
- referencedPrDeclaration(run) === undefined) {
755
- run.referencedPullRequestUrl = resolveReferencedRef(run.referencedPrCandidates ?? [], run.task, undefined);
756
- }
757
- // Repository scoping follows reconciliation: stores arm a handle asynchronously, while
758
- // the read-only index applies its cached handle to these fresh records (#97). This ordering
759
- // also scopes any referenced PR restored above without filtering candidate evidence first.
760
- return run;
761
858
  }
762
859
  /**
763
- * Settle a live run whose owning process died, when a control adopts it (#779, plan step 3), so
764
- * that adopting never starts agent work: only Continue may, with the user's own input. A run that
765
- * never started is cancelled before it began, which Continue restarts as it would after a Stop (the
766
- * same "untouched" test as `isUntouchedCancelledRun`); any other live run is interrupted, exactly as
767
- * an open that does not recover settles it, and Continue resumes its last session. With `stop`
768
- * (the control is Stop, and this is its run) a live run is cancelled instead, as an accepted Stop
769
- * always reads. Mutates `run`.
860
+ * Settle an adopted dead owner’s live run without starting work. Untouched runs cancel; started
861
+ * runs become interrupted. Stop explicitly cancels its target. Mutates run.
770
862
  */
771
863
  export function settleOrphanedRun(run, opts = {}) {
772
864
  if (opts.stop && (run.status === 'queued' || run.status === 'running' || run.status === 'waiting'))
@@ -788,6 +880,19 @@ export function settleOrphanedRun(run, opts = {}) {
788
880
  */
789
881
  export function rescopeRun(run, handle) {
790
882
  let changed = false;
883
+ if (handle && run.pullRequests) {
884
+ const entries = run.pullRequests.map(entry => {
885
+ if (entry.source !== 'declared' || !entry.url || isRepoScopedRef(entry.url, run.task, handle))
886
+ return entry;
887
+ // Revoke an uncorroborated URL, preserving the accepted declaration and its order.
888
+ return { number: entry.number, source: entry.source };
889
+ });
890
+ const next = mergePullRequests(undefined, entries, `https://github.com/${handle.owner}/${handle.name}`);
891
+ if (!isDeepStrictEqual(next, run.pullRequests)) {
892
+ run.pullRequests = next;
893
+ changed = true;
894
+ }
895
+ }
791
896
  if (run.referencedPullRequestUrl &&
792
897
  !isRepoScopedRef(run.referencedPullRequestUrl, run.task, handle)) {
793
898
  run.referencedPullRequestUrl = undefined;
@@ -809,15 +914,15 @@ export function rescopeRun(run, handle) {
809
914
  return changed;
810
915
  }
811
916
  /**
812
- * Every field `RunStore.open` may rewrite while loading a record: what `reconcileLoadedRun` and
813
- * `refreshHumanAskSummary` assign. Compared before and after, it tells open() which rows
814
- * normalization changed, so it marks those dirty and no others. Edit it together with either
815
- * function, or a normalized row stays unsaved (the store tests pin each field).
917
+ * Every field `RunStore.open` may rewrite while loading a record: what `reconcileLoadedRun`
918
+ * assigns. Compared before and after, it tells open() which rows normalization changed, so it
919
+ * marks those dirty and no others. Edit it together with that function, or a normalized row stays unsaved (the store tests pin each field).
816
920
  */
817
921
  function loadNormalizedFields(run) {
818
922
  return JSON.stringify([
819
- run.stopping, run.status, run.finishedAt, run.error, run.activity, run.monitoringWakeAt, run.autoResumeAt,
820
- run.monitoringWakeCapReached, run.referencedPullRequestUrl, run.hasPendingHumanAsk, run.steps.map((step) => step.status),
923
+ run.stopping, run.status, run.finishedAt, run.error, run.autoResumeAt,
924
+ run.referencedPullRequestUrl, run.pullRequests,
925
+ run.issueNumber, run.referencedIssueUrl, run.referencedIssueNumberSeeded, run.hasPendingPermission, run.steps.map((step) => step.status),
821
926
  ]);
822
927
  }
823
928
  /** Test seam (#779): `beforeTransaction` runs once `runs.json` is read and parsed, before the
@@ -859,28 +964,9 @@ function legacyIndexRows(bytes) {
859
964
  return { rows: [...rows.values()], skipped, total: raw.length };
860
965
  }
861
966
  /**
862
- * Import `runs.json` into a database that has never completed an import (#779, plan step 4).
863
- *
864
- * Nothing is read while a live cockpit other than this process owns the project
865
- * (`assertNoLegacyCockpit`): an older cezar still writes `runs.json` then, and the import could
866
- * not commit. Otherwise the file is read once, through one descriptor, and parsed; it is checked
867
- * again before anything is written, so a file that moved while it was parsed costs no write. Then
868
- * ONE transaction writes every record and the completion marker, or nothing:
869
- * - the marker is checked again once the transaction holds the write lock, so of two processes
870
- * importing at once exactly one writes; the other finds the marker and writes nothing;
871
- * - right before COMMIT it refuses (`LegacyWriterError`) when an older cezar may still be writing
872
- * `runs.json` (`assertNoLegacyWriter`) — the check that decides;
873
- * - only then are the exact bytes kept beside it (`backUpLegacyIndex`: synced, never half-written,
874
- * an existing backup trusted only when it holds the same bytes), and the marker records what
875
- * the import read: size, sha256 and the backup's name. A refused attempt leaves no backup;
876
- * - a crash or refusal leaves no marker, so the next open imports again from the start.
877
- * `runs.json` itself is left as it was, and nothing writes it again: an older cezar keeps reading
878
- * the history as it stood at the upgrade, and what it writes afterwards is never imported.
879
- *
880
- * A record that does not parse is skipped and the rest are imported; an index that does not parse
881
- * at all imports as an empty history. Either way the import that commits says so once, naming the
882
- * backup: the bytes survive there and in `runs.json`, which used to be overwritten by the next save
883
- * instead.
967
+ * Import legacy rows and the completion marker atomically, once. Recheck live-writer evidence
968
+ * before commit and preserve exact input bytes in a synced backup. Leave runs.json untouched;
969
+ * invalid entries are skipped and reported.
884
970
  */
885
971
  function importLegacyIndex(db, dataDir) {
886
972
  assertNoLegacyCockpit(dataDir);
@@ -915,13 +1001,23 @@ function importLegacyIndex(db, dataDir) {
915
1001
  }
916
1002
  /**
917
1003
  * Finished rows whose stored summary an open-time normalization would still change: a referenced
918
- * PR an older cezar's created-PR declaration erased (see `reconcileLoadedRun`). Not live, so not
1004
+ * PR an older cezar's created-PR declaration erased, or issue attribution displaced by a
1005
+ * follow-up declaration (see `reconcileLoadedRun`). Not live, so not
919
1006
  * in the `live` column; open decodes these few and keeps the ones it actually repaired.
920
1007
  */
921
- const LEGACY_REFERENCE_HEAL_SQL = "json_extract(summary, '$.markerRefs.pr') IS NOT NULL AND json_extract(summary, '$.referencedPullRequestUrl') IS NULL";
1008
+ const LEGACY_REFERENCE_HEAL_SQL = "(json_extract(summary, '$.markerRefs.pr') IS NOT NULL AND json_extract(summary, '$.referencedPullRequestUrl') IS NULL)" +
1009
+ " OR (NOT EXISTS (SELECT 1 FROM meta WHERE key = 'issue-attribution:' || runs.id AND value = CAST(runs.revision AS TEXT)) AND json_extract(summary, '$.markerRefs.issue') IS NOT NULL)";
1010
+ /**
1011
+ * Rows whose record names the automation that launched it while the stored summary does not
1012
+ * (hearsay-tools/cezarion#1332): summaries a cezar wrote before `RunSummary.automationId`
1013
+ * existed. Open rewrites each one's summary once, so the sidebar's Automations tab classifies a
1014
+ * finished task nobody has opened since the upgrade. `instr` keeps the JSON parse off every
1015
+ * other row.
1016
+ */
1017
+ const AUTOMATION_ORIGIN_HEAL_SQL = "instr(data, '\"automation') > 0 AND json_extract(summary, '$.automationId') IS NULL" +
1018
+ " AND (json_extract(data, '$.automation.automationId') IS NOT NULL OR json_extract(data, '$.automationTrigger.automationId') IS NOT NULL)";
922
1019
  /** Mark-all-read's rule (see `RunStore.markAllRead`) over the columns and the stored summary. */
923
1020
  const MARK_ALL_READ_SQL = "archived = 0 AND status IN ('done', 'failed') AND finished_at IS NOT NULL" +
924
- " AND NOT (status = 'failed' AND json_extract(summary, '$.autoResumeAt') IS NOT NULL)" +
925
1021
  " AND (json_extract(summary, '$.seenAt') IS NULL OR json_extract(summary, '$.seenAt') < finished_at)";
926
1022
  /** History retention's candidates among the given ids (a JSON array): finished, without
927
1023
  * delegation, and not pinned. */
@@ -1089,51 +1185,10 @@ const recordedProcessSchema = z.object({ pid: z.number().int().positive(), start
1089
1185
  const workerProcessRecordSchema = z.object({ generation: z.string().uuid(), controller: recordedProcessSchema,
1090
1186
  processes: z.array(recordedProcessSchema).max(WORKER_PROCESS_CAP) }).strict();
1091
1187
  const startToken = (pid) => { const token = processStartToken(pid); return token === undefined ? {} : { startToken: token }; };
1092
- /** The bulk-archive sweep's one predicate (#780): finished, not archived, not scheduled (a
1093
- * `failed` run waiting out a usage limit), not an owned worker (those leave with their parent),
1094
- * then filtered by pin state. Mirrored clause for clause by `isSweepable` in the cockpit's
1095
- * `lib/tasks-table.ts`. */
1096
- export function isSweepable(run, scope) {
1097
- if (run.archived || !['done', 'failed', 'cancelled'].includes(run.status))
1098
- return false;
1099
- if (run.status === 'failed' && run.autoResumeAt !== undefined)
1100
- return false;
1101
- if (run.delegation?.role === 'worker')
1102
- return false;
1103
- if (scope === 'unpinned')
1104
- return !run.pinned;
1105
- if (scope === 'pinned')
1106
- return run.pinned === true;
1107
- return true;
1108
- }
1109
1188
  /**
1110
- * File-backed run store: one row per run in `runs.db` (#779, `./run-database.ts`) plus one
1111
- * append-only NDJSON event file per run. Also the in-process event bus the SSE endpoints
1112
- * subscribe to: emits `('run', RunRecord)` and `('event', { runId, event: RunEvent })`.
1113
- *
1114
- * Only the held set lives in memory (#779, Amendment 2): live runs, the runs a RunManager pins,
1115
- * their delegation families, and runs whose latest write has not settled yet (see `isHeld`).
1116
- * `getRun` answers a held run with the held object, so `commitIndex` still installs into the
1117
- * record a caller holds; any other run is read from `runs.db` as a fresh copy every time (frozen
1118
- * under vitest, so an in-place write throws instead of being lost). Writes to a finished run go
1119
- * through the methods here, which load it, change it and persist the row like any other.
1120
- *
1121
- * Two write paths, both through `writeIndex`:
1122
- * - optimistic: a method changes the held record, marks it dirty and schedules the debounced
1123
- * save, which writes the dirty rows and deletions only;
1124
- * - durable (`commitIndex`): one transaction with the changed rows AND everything a debounced
1125
- * save still owes. Only once it commits do the records change in memory, and only then do
1126
- * subscribers hear about it.
1127
- *
1128
- * Several processes may open one project (`serve` and a headless `cez run`), so a store writes
1129
- * only the delegation families it claims in `run_claims` (#779, plan step 3; `run-claims.ts`).
1130
- * Everything it holds is claimed: open claims the live rows nobody alive owns and leaves the rest
1131
- * alone, and a write to a run not in memory claims its family first, or refuses it while another
1132
- * live process owns it. Claims are released once a family leaves memory, and all of them on
1133
- * `close()`; a crashed owner's are taken over only once it is proven dead. Every write is fenced
1134
- * in its transaction: if a row is not as this store last saw it, or its claim is gone, nothing is
1135
- * written, the evidence goes to `run_conflicts` and this store stops writing that row
1136
- * (`handleConflicts`).
1189
+ * Run rows in runs.db plus append-only NDJSON events and an in-process event bus. Hold live,
1190
+ * pinned, related and unsettled records only. Optimistic writes debounce; durable commits
1191
+ * publish after persistence. Family claims fence every write.
1137
1192
  */
1138
1193
  export class RunStore extends EventEmitter {
1139
1194
  dataDir;
@@ -1189,6 +1244,11 @@ export class RunStore extends EventEmitter {
1189
1244
  compressor;
1190
1245
  /** Transcript facts the delegation paths ask about, so they never re-read a transcript (#880). */
1191
1246
  facts;
1247
+ liveItems = new LiveItemSnapshots();
1248
+ /** Capture both sources on the same synchronous sequence boundary before history IO. */
1249
+ liveReadSnapshot(runId) {
1250
+ return { events: this.liveItems.read(runId), throughSeq: this.seqs.get(runId) ?? Infinity };
1251
+ }
1192
1252
  constructor(dataDir) {
1193
1253
  super();
1194
1254
  this.dataDir = dataDir;
@@ -1219,7 +1279,7 @@ export class RunStore extends EventEmitter {
1219
1279
  const started = Date.now();
1220
1280
  for (let attempt = 0;; attempt++) {
1221
1281
  try {
1222
- return RunStore.openOnce(dataDir, path, opts?.keepLive === true);
1282
+ return RunStore.openOnce(dataDir, path, opts?.keepLive === true, opts?.maintenance === true);
1223
1283
  }
1224
1284
  catch (error) {
1225
1285
  const transient = error instanceof RunDatabaseBusyError || (error instanceof LegacyWriterError && error.reason === 'cockpit');
@@ -1230,7 +1290,7 @@ export class RunStore extends EventEmitter {
1230
1290
  }
1231
1291
  }
1232
1292
  }
1233
- static openOnce(dataDir, path, keepLive) {
1293
+ static openOnce(dataDir, path, keepLive, maintenance) {
1234
1294
  mkdirSync(join(dataDir, 'runs'), { recursive: true });
1235
1295
  const store = new RunStore(dataDir);
1236
1296
  store.keepLive = keepLive;
@@ -1240,8 +1300,10 @@ export class RunStore extends EventEmitter {
1240
1300
  importLegacyIndex(db, dataDir);
1241
1301
  store.db = db;
1242
1302
  store.owner = openClaimSession();
1243
- store.loadHeldRows();
1244
- store.warmTranscriptFacts();
1303
+ if (!maintenance) {
1304
+ store.loadHeldRows();
1305
+ store.warmTranscriptFacts();
1306
+ }
1245
1307
  }
1246
1308
  catch (error) {
1247
1309
  if (store.owner) {
@@ -1316,8 +1378,26 @@ export class RunStore extends EventEmitter {
1316
1378
  continue;
1317
1379
  const decoded = decode(row);
1318
1380
  // Held only when the repair changed it: the next save writes it, then it leaves.
1319
- if (decoded)
1320
- this.adoptLoadedRun(decoded, { keepLive: this.keepLive, onlyIfChanged: true }, row);
1381
+ if (decoded) {
1382
+ // Persist verification once even when legacy issue attribution was already correct.
1383
+ const needsIssueCheck = decoded.run.markerRefs?.issue !== undefined &&
1384
+ this.db.getMeta(`issue-attribution:${row.id}`) !== String(row.revision);
1385
+ this.adoptLoadedRun(decoded, { keepLive: this.keepLive, onlyIfChanged: !needsIssueCheck }, row);
1386
+ if (needsIssueCheck)
1387
+ this.dirty.add(row.id);
1388
+ }
1389
+ }
1390
+ const stale = this.db.listWhere(AUTOMATION_ORIGIN_HEAL_SQL).filter((row) => !this.held.has(row.id));
1391
+ const restamp = this.claimFamilies(stale.map(rowFamily));
1392
+ for (const row of stale) {
1393
+ if (!restamp.has(rowFamily(row)))
1394
+ continue;
1395
+ const decoded = decode(row);
1396
+ // Nothing in the record changes, only its summary: held dirty, the next save rewrites it.
1397
+ if (decoded) {
1398
+ this.adoptLoadedRun(decoded, { keepLive: this.keepLive }, row);
1399
+ this.dirty.add(row.id);
1400
+ }
1321
1401
  }
1322
1402
  for (const rootId of this.anchoredFamilies())
1323
1403
  this.holdFamily(rootId, decode);
@@ -1327,17 +1407,16 @@ export class RunStore extends EventEmitter {
1327
1407
  }
1328
1408
  /** Normalize a record just read from `row` (see `reconcileLoadedRun`) and hold it. Marks the row
1329
1409
  * dirty only when normalization changed it, so the next save persists exactly the runs open()
1330
- * rewrote. The caller has claimed the row's family. */
1331
- adoptLoadedRun({ run, extras }, opts, row) {
1410
+ * rewrote. A row read up from schema-3 wait fields counts as changed: its rewrite drops them
1411
+ * and recomputes its summary and live column (`upgradeLegacyWaits`). The caller has claimed
1412
+ * the row's family. */
1413
+ adoptLoadedRun({ run, extras, upgraded }, opts, row) {
1332
1414
  const before = loadNormalizedFields(run);
1333
- if (run.delegation?.role === 'root' && (run.status === 'waiting' || run.delegation.wait !== undefined)) {
1334
- this.syncHumanAskSummary(run);
1335
- }
1336
1415
  if (opts.settle)
1337
1416
  settleOrphanedRun(run, { stop: opts.stop });
1338
1417
  else
1339
1418
  reconcileLoadedRun(run, opts);
1340
- const changed = loadNormalizedFields(run) !== before;
1419
+ const changed = upgraded === true || loadNormalizedFields(run) !== before;
1341
1420
  if (opts.onlyIfChanged && !changed)
1342
1421
  return;
1343
1422
  this.held.set(run.id, run);
@@ -1492,12 +1571,9 @@ export class RunStore extends EventEmitter {
1492
1571
  return ownership === 'foreign' || ownership === 'orphaned' ? RUN_IN_USE_ELSEWHERE : undefined;
1493
1572
  }
1494
1573
  /**
1495
- * Take over the delegation family of an `orphaned` run for a control: claim it from its dead
1496
- * owner (or from nobody), then load it and settle its live rows (`settleOrphanedRun`) — never
1497
- * keep them for recovery to resume, since only Continue may start agent work. With `stop` the
1498
- * control is Stop: `id` itself is settled as cancelled rather than interrupted. Returns the
1499
- * family's root id, or undefined when a live owner holds it after all or it is gone. The caller
1500
- * runs recovery for that family next (`RunManager.adoptOrphanedRun`), which then repairs it.
1574
+ * Claim an orphaned family and settle its live rows before a control acts. Stop cancels its
1575
+ * target. Return the root id, or undefined for a live owner or missing run; adoption never
1576
+ * resumes work.
1501
1577
  */
1502
1578
  adoptFamily(id, opts = {}) {
1503
1579
  const family = this.db?.familyOf(id);
@@ -1670,6 +1746,14 @@ export class RunStore extends EventEmitter {
1670
1746
  holders.add(holder);
1671
1747
  return run;
1672
1748
  }
1749
+ /** Publish a run's current record again without a write: something read beside the record
1750
+ * changed, such as the run leaving the manager's live registries, which a detail's finish
1751
+ * verdict reads (hearsay-tools/cezarion#1301). */
1752
+ republish(id) {
1753
+ const run = this.peek(id);
1754
+ if (run)
1755
+ this.emit('run', run, 'live-release');
1756
+ }
1673
1757
  /** Release `holder`'s pin. The run leaves memory at the next sweep once nothing else holds it. */
1674
1758
  unpin(id, holder) {
1675
1759
  const holders = this.pins.get(id);
@@ -1680,20 +1764,8 @@ export class RunStore extends EventEmitter {
1680
1764
  this.sweepIfSettled([id]);
1681
1765
  }
1682
1766
  /**
1683
- * Tell the store which repository this project IS (#945), so the referenced tier stops adopting
1684
- * another repo's PR/issue as the task's subject. See `isRepoScopedRef` for the rule.
1685
- *
1686
- * A setter rather than an `open()` option because `open()` is synchronous and the handle costs a
1687
- * `gh` spawn: callers arm this in the background so boot never waits on the network. `null` is a
1688
- * first-class answer meaning "cannot be known" (no `gh`, no remote, a non-git root) and leaves
1689
- * the store in exactly its pre-#945 behavior.
1690
- *
1691
- * Arming also HEALS records already poisoned by the un-scoped rule, on the `reconcileLoadedRun`
1692
- * precedent: the evidence is all still on the record (`referenced*Candidates`), only the
1693
- * conclusion drawn from it was wrong, so re-deciding beats asking for a migration. It rewrites
1694
- * values, never the format, and is one-directional by construction — see `rescopeRun`. Held
1695
- * runs are re-checked in memory; of the rest, only rows whose stored summary references another
1696
- * repository are decoded, and the healed ones are written, so their summaries heal too.
1767
+ * Set discovered repository identity without blocking boot. Recompute conclusions from retained
1768
+ * evidence, including cold records; null retains unknown-handle behavior.
1697
1769
  */
1698
1770
  setRepoHandle(handle) {
1699
1771
  this.repoHandle = handle;
@@ -1743,7 +1815,7 @@ export class RunStore extends EventEmitter {
1743
1815
  /**
1744
1816
  * The live set, newest first: runs whose record is live (`isLiveRecord`) or that a RunManager
1745
1817
  * pins. Finished runs are not here — read one by id (`getRun`), by an indexed query
1746
- * (`listWorkersOf`, `findRunByClientRequestId`, `listGroupRuns`, `listRunsWithWorktree`,
1818
+ * (`listWorkersOf`, `findRunByClientRequestId`, `listRunsWithWorktree`,
1747
1819
  * `listBranchOwners`), or as list rows (`listRunSummaries`).
1748
1820
  */
1749
1821
  listRuns() {
@@ -1819,7 +1891,9 @@ export class RunStore extends EventEmitter {
1819
1891
  * One page of archived ROOT runs, newest first (#864): `GET /run-summaries/archived`, the
1820
1892
  * archived runs past the window. Held runs list by their record, as in `listRunSummaries`.
1821
1893
  * `before` is the previous page's `nextCursor`; with `q`, only runs `matchesRunQuery` keeps, and
1822
- * `total` counts those. A cursor this store did not write answers `{ error }`.
1894
+ * `total` counts those. `automationNames` lets `q` match a task by its automation's name, as the
1895
+ * list's origin line shows it (hearsay-tools/cezarion#1616). A cursor this store did not write
1896
+ * answers `{ error }`.
1823
1897
  *
1824
1898
  * Ranking reads every archived root's key (id, created_at, seq: no record, no summary), so the
1825
1899
  * page and `total` agree with the held overlay; only the page's own rows are read in full.
@@ -1832,6 +1906,16 @@ export class RunStore extends EventEmitter {
1832
1906
  return { error: 'before is not a cursor this server wrote' };
1833
1907
  }
1834
1908
  const q = options.q?.trim() ?? '';
1909
+ const ranked = this.rankArchivedRows(q, options.automationNames);
1910
+ this.overlayHeldArchived(ranked, q, options.automationNames);
1911
+ const ordered = [...ranked.entries()].sort(([, a], [, b]) => newestFirst(a, b));
1912
+ return this.archivedPage(ordered, after, options.limit);
1913
+ }
1914
+ /** The archived roots in `runs.db`, each by its key: every one without `q` (no record, no
1915
+ * summary read), and with `q` the searched rows `matchesRunSearch` keeps, which carry the summary
1916
+ * the search read. A deleted, held or unreadable run's row is left out; a held run joins in
1917
+ * `overlayHeldArchived`. */
1918
+ rankArchivedRows(q, automationNames) {
1835
1919
  const skipped = (id) => this.deleted.has(id) || this.held.has(id) || this.unreadable.has(id);
1836
1920
  const ranked = new Map();
1837
1921
  if (q === '') {
@@ -1840,30 +1924,33 @@ export class RunStore extends EventEmitter {
1840
1924
  ranked.set(key.id, key);
1841
1925
  }
1842
1926
  else {
1843
- for (const row of this.db?.searchRootSummaries(sqlPrefilterTokens(q)) ?? []) {
1927
+ for (const row of this.db?.searchRootSummaries(sqlPrefilterTokens(q, { automationNames })) ?? []) {
1844
1928
  if (skipped(row.id))
1845
1929
  continue;
1846
1930
  const summary = parseStoredSummary(row.summary) ?? this.coldSummary(row.id);
1847
- if (summary?.archived && matchesRunQuery(summary, q))
1931
+ if (summary?.archived && matchesRunSearch(summary, q, { automationNames }))
1848
1932
  ranked.set(row.id, { ...row, summary });
1849
1933
  }
1850
1934
  }
1935
+ return ranked;
1936
+ }
1937
+ /** Lay the held archived roots over `ranked`, each by its record: it replaces its stored row
1938
+ * (already left out of `ranked`), and with `q` it must match. A held worker never lists. */
1939
+ overlayHeldArchived(ranked, q, automationNames) {
1851
1940
  for (const [id, run] of this.held) {
1852
1941
  if (!run.archived || run.delegation?.role === 'worker')
1853
1942
  continue;
1854
1943
  const summary = toRunSummary(run);
1855
- if (q === '' || matchesRunQuery(summary, q))
1944
+ if (q === '' || matchesRunSearch(summary, q, { automationNames }))
1856
1945
  ranked.set(id, { ...this.listOrder(run), summary });
1857
1946
  }
1858
- const ordered = [...ranked.entries()].sort(([, a], [, b]) => newestFirst(a, b));
1947
+ }
1948
+ /** The page of `ordered` after the cursor `after`: its summaries, the cursor of the next page,
1949
+ * and `total`, the count of all of `ordered`. */
1950
+ archivedPage(ordered, after, limit) {
1859
1951
  const rest = after ? ordered.filter(([, key]) => newestFirst(key, after) > 0) : ordered;
1860
- const page = rest.slice(0, options.limit);
1861
- const cold = new Map((this.db?.getSummaries(page.filter(([, key]) => !key.summary).map(([id]) => id)) ?? [])
1862
- .map((row) => [row.id, parseStoredSummary(row.summary) ?? this.coldSummary(row.id)]));
1863
- const runs = page.flatMap(([id, key]) => {
1864
- const summary = key.summary ?? cold.get(id);
1865
- return summary ? [summary] : [];
1866
- });
1952
+ const page = rest.slice(0, limit);
1953
+ const runs = this.hydrateArchivedPage(page);
1867
1954
  const last = page.at(-1)?.[1];
1868
1955
  return {
1869
1956
  runs,
@@ -1871,8 +1958,19 @@ export class RunStore extends EventEmitter {
1871
1958
  total: ordered.length,
1872
1959
  };
1873
1960
  }
1961
+ /** The page's summaries: a ranked key's own, else its stored one, read for the rows that have
1962
+ * none in a single query. A row whose stored summary does not fit the contract is projected from
1963
+ * its record; a row that cannot be read at all is left out. */
1964
+ hydrateArchivedPage(page) {
1965
+ const cold = new Map((this.db?.getSummaries(page.filter(([, key]) => !key.summary).map(([id]) => id)) ?? [])
1966
+ .map((row) => [row.id, parseStoredSummary(row.summary) ?? this.coldSummary(row.id)]));
1967
+ return page.flatMap(([id, key]) => {
1968
+ const summary = key.summary ?? cold.get(id);
1969
+ return summary ? [summary] : [];
1970
+ });
1971
+ }
1874
1972
  /**
1875
- * The ROOT runs, archived or not, that match `query` (`matchesRunQuery`), newest first: at most
1973
+ * The ROOT runs, archived or not, that match `query` (`matchesRunSearch`), newest first: at most
1876
1974
  * `limit`, and whether more matched. The owned half of the workspace search (#864); held runs
1877
1975
  * match by their record.
1878
1976
  */
@@ -1884,7 +1982,7 @@ export class RunStore extends EventEmitter {
1884
1982
  if (this.deleted.has(row.id) || this.held.has(row.id) || this.unreadable.has(row.id))
1885
1983
  return true;
1886
1984
  const summary = parseStoredSummary(row.summary) ?? this.coldSummary(row.id);
1887
- if (summary && matchesRunQuery(summary, query))
1985
+ if (summary && matchesRunSearch(summary, query))
1888
1986
  matched.set(row.id, [summary, row]);
1889
1987
  return matched.size <= limit;
1890
1988
  });
@@ -1892,7 +1990,7 @@ export class RunStore extends EventEmitter {
1892
1990
  if (run.delegation?.role === 'worker')
1893
1991
  continue;
1894
1992
  const summary = toRunSummary(run);
1895
- if (matchesRunQuery(summary, query))
1993
+ if (matchesRunSearch(summary, query))
1896
1994
  matched.set(id, [summary, this.listOrder(run)]);
1897
1995
  }
1898
1996
  const runs = sortNewestFirst(matched.values());
@@ -1986,6 +2084,61 @@ export class RunStore extends EventEmitter {
1986
2084
  }
1987
2085
  return [...ids];
1988
2086
  }
2087
+ /** All retained tasks this automation started, including variants, finished and archived runs.
2088
+ * Logs and receipts may compact independently. Read only matching keys in SQLite and overlay
2089
+ * held records so a debounced provenance update or deletion is reflected immediately. */
2090
+ countAutomationRuns(automationId) {
2091
+ const where = "json_extract(data, '$.automation.automationId') = ? OR " +
2092
+ "json_extract(data, '$.automationTrigger.automationId') = ?";
2093
+ const ids = new Set((this.db?.listKeysWhere(where, [automationId, automationId]) ?? [])
2094
+ .map((key) => key.id));
2095
+ for (const id of this.deleted)
2096
+ ids.delete(id);
2097
+ for (const [id, run] of this.held) {
2098
+ if (run.automation?.automationId === automationId || run.automationTrigger?.automationId === automationId)
2099
+ ids.add(id);
2100
+ else
2101
+ ids.delete(id);
2102
+ }
2103
+ return ids.size;
2104
+ }
2105
+ /** First non-archived task referencing a project issue, including secondary references. */
2106
+ findNonArchivedRunByIssue(repo, number) {
2107
+ const url = `https://github.com/${repo}/issues/${number}`.toLowerCase();
2108
+ const matches = (run) => {
2109
+ if (run.archived)
2110
+ return false;
2111
+ const urls = [run.referencedIssueUrl, ...(run.referencedIssueCandidates ?? []),
2112
+ ...(run.automation?.event.startsWith('issue.') ? [run.automation.githubUrl] : []),
2113
+ ...[...run.task.matchAll(new RegExp(ISSUE_URL_RE.source, 'gi'))].map((match) => match[0])]
2114
+ .filter((value) => value !== undefined)
2115
+ .map((value) => value.replace(/\/+$/, '').toLowerCase())
2116
+ .filter((value) => value.endsWith(`/issues/${number}`));
2117
+ // The legacy issueNumber is display-only and cannot establish repository identity.
2118
+ // Explicit task markers remain project-scoped unless a qualified URL contradicts them.
2119
+ return urls.includes(url) || (urls.length === 0 && run.markerRefs?.issue === number);
2120
+ };
2121
+ for (const run of this.held.values())
2122
+ if (matches(run))
2123
+ return run.id;
2124
+ // Secondary references are record-only: query them in SQLite rather than hydrating every
2125
+ // finished transcript or relying on the slim summary's primary issue number.
2126
+ const where = `archived = 0 AND (
2127
+ json_extract(data, '$.issueNumber') = ? OR json_extract(data, '$.markerRefs.issue') = ?
2128
+ OR lower(rtrim(json_extract(data, '$.referencedIssueUrl'), '/')) = ?
2129
+ OR EXISTS (SELECT 1 FROM json_each(data, '$.referencedIssueCandidates') WHERE lower(rtrim(value, '/')) = ?)
2130
+ OR (json_extract(data, '$.automation.event') LIKE 'issue.%' AND lower(rtrim(json_extract(data, '$.automation.githubUrl'), '/')) = ?)
2131
+ OR instr(lower(json_extract(data, '$.task')), ?) > 0
2132
+ )`;
2133
+ for (const key of this.db?.listKeysWhere(where, [number, number, url, url, url, url]) ?? []) {
2134
+ if (this.held.has(key.id) || this.deleted.has(key.id) || this.unreadable.has(key.id))
2135
+ continue;
2136
+ const run = this.getRun(key.id);
2137
+ if (run && matches(run))
2138
+ return key.id;
2139
+ }
2140
+ return undefined;
2141
+ }
1989
2142
  /**
1990
2143
  * The runs automation receipts launched, by receipt id (`automation.receiptId` or
1991
2144
  * `automationTrigger.receiptId`). Memory answers first; a receipt it does not know costs one
@@ -2041,10 +2194,6 @@ export class RunStore extends EventEmitter {
2041
2194
  ids.add(id);
2042
2195
  return [...ids];
2043
2196
  }
2044
- /** The members of one parallel-variant group (spec 010), in no particular order. */
2045
- listGroupRuns(groupId) {
2046
- return this.queryRuns(this.db?.listByGroup(groupId) ?? [], (run) => run.groupId === groupId).map(([run]) => run);
2047
- }
2048
2197
  /** Runs with a materialized worktree directory (`worktreePath`, not reclaimed), most recently
2049
2198
  * finished first, then newest first: worktree retention and the worktrees panel. */
2050
2199
  listRunsWithWorktree() {
@@ -2138,13 +2287,16 @@ export class RunStore extends EventEmitter {
2138
2287
  effort: input.effort,
2139
2288
  runner: input.runner,
2140
2289
  agentProfile: input.agentProfile,
2141
- generateFollowups: input.generateFollowups,
2142
2290
  autonomous: input.autonomous,
2291
+ archiveWhenDone: input.archiveWhenDone,
2292
+ autoMergePr: input.autoMergePr,
2143
2293
  ...(input.notify ? { notify: true } : {}),
2144
2294
  worktree: input.worktree,
2145
2295
  groupId: input.groupId,
2146
2296
  variant: input.variant,
2147
2297
  ...(input.clientRequestId === undefined ? {} : { clientRequestId: input.clientRequestId, clientRequestHash: input.clientRequestHash }),
2298
+ ...(input.automation ? { automation: input.automation } : {}),
2299
+ ...(input.automationTrigger ? { automationTrigger: input.automationTrigger } : {}),
2148
2300
  status: 'queued',
2149
2301
  createdAt: new Date().toISOString(),
2150
2302
  tokensUsed: 0,
@@ -2181,31 +2333,13 @@ export class RunStore extends EventEmitter {
2181
2333
  }
2182
2334
  this.commitIndex(staged);
2183
2335
  }
2184
- /** Observable atomic input checkpoint: a failed write publishes nothing. */
2185
- /** Accepted input the harness will never read returns to the queue (#505): only its
2186
- * delivery and consumption receipts are cleared; order and identity are kept. */
2187
- requeueUnconsumedAgentInputs(id, ids) {
2188
- const run = this.peek(id);
2189
- if (!run?.agentInputs || !ids.length)
2190
- return;
2191
- this.commitAgentInputs(id, run.agentInputs.map(input => {
2192
- if (!ids.includes(input.id))
2193
- return input;
2194
- const { deliveredAt: _delivered, consumedAt: _consumed, awaitingRead: _awaiting, ...queued } = input;
2195
- return queued;
2196
- }));
2197
- }
2198
- /** Model consumption observed by the current session (#505). Never un-sets deliveredAt. */
2199
- commitAgentInputsConsumed(id, ids, at) {
2200
- const run = this.peek(id);
2201
- if (!run?.agentInputs || !ids.length)
2202
- return;
2203
- this.commitAgentInputs(id, run.agentInputs.map(input => {
2204
- if (!ids.includes(input.id) || !input.deliveredAt || input.consumedAt)
2205
- return input;
2206
- const { awaitingRead: _awaiting, ...read } = input;
2207
- return { ...read, consumedAt: at };
2208
- }));
2336
+ /** Crash recovery (#505): input a harness accepted but was never seen reading goes
2337
+ * back to the queue. Returns the requeued IDs. */
2338
+ requeueAwaitingReadInputs(id) {
2339
+ const ids = (this.peek(id)?.agentInputs ?? []).filter(input => input.awaitingRead && input.deliveredAt && !input.consumedAt).map(input => input.id);
2340
+ if (ids.length)
2341
+ this.commitOutcome(id, { kind: 'requeued', inputIds: ids });
2342
+ return ids;
2209
2343
  }
2210
2344
  /** Delivered, but the harness never confirmed reading it (#505): stop awaiting a read. */
2211
2345
  commitAgentInputsUnconfirmed(id, ids) {
@@ -2219,18 +2353,18 @@ export class RunStore extends EventEmitter {
2219
2353
  return unconfirmed;
2220
2354
  }));
2221
2355
  }
2222
- /** Crash recovery (#505): input a harness accepted but was never seen reading goes
2223
- * back to the queue. Returns the requeued IDs. */
2224
- requeueAwaitingReadInputs(id) {
2225
- const ids = (this.peek(id)?.agentInputs ?? []).filter(input => input.awaitingRead && input.deliveredAt && !input.consumedAt).map(input => input.id);
2226
- this.requeueUnconsumedAgentInputs(id, ids);
2227
- return ids;
2228
- }
2229
- commitAgentInputs(id, inputs, openingContinuationInputId) {
2356
+ /** Observable atomic input checkpoint for the queue itself: enqueue, retire, reorder. Delivery
2357
+ * and reads are outcomes (`commitOutcome`). */
2358
+ commitAgentInputs(id, inputs) {
2230
2359
  const run = this.peek(id);
2231
2360
  if (!run)
2232
2361
  throw new Error('missing agent input target');
2233
2362
  const agentInputs = inputs.map(input => agentInputSchema.parse(input));
2363
+ // Receipts are outcomes (hearsay-tools/cezarion#1805): only `commitOutcome` writes them.
2364
+ const stored = new Map((run.agentInputs ?? []).map(input => [input.id, input]));
2365
+ if (agentInputs.some(input => input.deliveredAt !== stored.get(input.id)?.deliveredAt || input.consumedAt !== stored.get(input.id)?.consumedAt)) {
2366
+ throw new Error('agent input receipts change only through commitOutcome');
2367
+ }
2234
2368
  const now = new Date().toISOString();
2235
2369
  for (const input of run.agentInputs ?? []) {
2236
2370
  const claim = input.inboxClaim;
@@ -2241,10 +2375,7 @@ export class RunStore extends EventEmitter {
2241
2375
  throw new Error('live inbox receipt changed');
2242
2376
  }
2243
2377
  }
2244
- if (openingContinuationInputId && (run.continuationMessage?.agentInputId !== openingContinuationInputId ||
2245
- !agentInputs.some(input => input.id === openingContinuationInputId && input.deliveredAt)))
2246
- throw new Error('opening agent input checkpoint changed');
2247
- this.commitIndex(new Map([[id, { ...run, agentInputs, ...(openingContinuationInputId ? { continuationMessage: undefined } : {}) }]]));
2378
+ this.commitIndex(new Map([[id, { ...run, agentInputs }]]));
2248
2379
  }
2249
2380
  /** Reserve only unread conversation inputs in one durable index replacement. */
2250
2381
  claimInboxInputs(runId, ids, claim) {
@@ -2292,8 +2423,7 @@ export class RunStore extends EventEmitter {
2292
2423
  if (matching.some(input => input.deliveredAt || input.inboxClaim?.acknowledgedAt || input.inboxClaim.expiresAt <= at)) {
2293
2424
  throw new Error('inbox receipt expired or displaced');
2294
2425
  }
2295
- this.commitIndex(new Map([[runId, { ...run, agentInputs: run.agentInputs.map(input => input.inboxClaim?.receiptId === receiptId
2296
- ? agentInputSchema.parse({ ...input, deliveredAt: at, inboxClaim: { ...input.inboxClaim, acknowledgedAt: at } }) : input) }]]));
2426
+ this.commitOutcome(runId, { kind: 'inbox-acknowledged', receiptId, at });
2297
2427
  return 'acknowledged';
2298
2428
  }
2299
2429
  releaseInboxInputs(runId, receiptId, generation) {
@@ -2328,67 +2458,102 @@ export class RunStore extends EventEmitter {
2328
2458
  return agentInputSchema.parse(unclaimed);
2329
2459
  }) }]]));
2330
2460
  }
2331
- /** CI intent and its wake entry share one atomic index replacement. */
2332
- commitCiWait(id, wait, input) {
2461
+ /**
2462
+ * A wait and its wake input share one atomic index replacement (spec 2026-10-17-unified-waits).
2463
+ * Upserts `wait` by id into `waits` and clears `lastWaitsError`; `input` is appended once (an
2464
+ * input whose id is already queued stays as it is). Only an open or resolved wait is committed
2465
+ * here: settling is an outcome (`commitOutcome`). A new open wait
2466
+ * past `WAIT_LIMIT` open ones is refused, so the list stays bounded whatever the caller checked.
2467
+ */
2468
+ commitWait(id, wait, input) {
2333
2469
  const run = this.peek(id);
2334
2470
  if (!run)
2335
- throw new Error('missing CI wait target');
2336
- const ciWait = ciWaitSchema.parse(wait);
2337
- const entry = input ? agentInputSchema.parse(input) : undefined;
2338
- this.commitIndex(new Map([[id, { ...run, ciWait, lastCiWaitError: undefined, ...(entry ? { agentInputs: run.agentInputs?.some(row => row.id === entry.id)
2339
- ? run.agentInputs : [...(run.agentInputs ?? []), entry] } : {}) }]]));
2340
- }
2341
- /** Retire the wait and precisely its pending wake, optionally accepting human input. */
2342
- commitCiWaitWithdrawal(id, acceptedHumanMessage) {
2471
+ throw new Error('missing wait target');
2472
+ const entry = waitSchema.parse(wait);
2473
+ if (entry.phase !== 'open' && entry.phase !== 'resolved')
2474
+ throw new Error('a settled wait is committed through its delivery or cancellation');
2475
+ const waits = run.waits ?? [];
2476
+ const known = waits.some((row) => row.id === entry.id);
2477
+ if (!known && entry.phase === 'open' && waits.filter((row) => row.phase === 'open').length >= WAIT_LIMIT) {
2478
+ throw new Error(`open wait limit reached (${WAIT_LIMIT})`);
2479
+ }
2480
+ const agentInput = input ? agentInputSchema.parse(input) : undefined;
2481
+ this.commitIndex(new Map([[id, { ...run, waits: known ? waits.map((row) => row.id === entry.id ? entry : row) : [...waits, entry], lastWaitsError: undefined,
2482
+ ...(agentInput ? { agentInputs: run.agentInputs?.some((row) => row.id === agentInput.id)
2483
+ ? run.agentInputs : [...(run.agentInputs ?? []), agentInput] } : {}) }]]));
2484
+ }
2485
+ /** A park that gives up its slot (hearsay-tools/cezarion#1235): the run and its step read
2486
+ * `waiting`, committed at once instead of on the debounced save, so a refused write throws with
2487
+ * nothing changed and the caller keeps the slot. */
2488
+ commitParked(id, stepId) {
2343
2489
  const run = this.peek(id);
2344
- if (!run?.ciWait)
2490
+ if (!run)
2345
2491
  return;
2346
- const { ciWait, ...rest } = run;
2347
- const lastCiWait = { ...ciWait, phase: 'withdrawn' };
2348
- this.commitIndex(new Map([[id, { ...rest, ciWait: undefined, lastCiWait,
2349
- ...(run.agentInputs ? { agentInputs: run.agentInputs.filter(input => input.id !== ciWait.wakeId || input.deliveredAt) } : {}),
2350
- ...(acceptedHumanMessage ? { queuedMessages: [...(run.queuedMessages ?? []), queuedMessageSchema.parse(acceptedHumanMessage)] } : {}),
2351
- }]]));
2492
+ this.commitIndex(new Map([[id, { ...run, status: 'waiting', activity: undefined,
2493
+ ...(stepId ? { steps: run.steps.map((step) => step.id === stepId ? { ...step, status: 'waiting' } : step) } : {}) }]]));
2352
2494
  }
2353
- /** Provider acceptance and receipt retirement have one delivery checkpoint. */
2354
- commitCiWaitDelivery(id, inputId) {
2495
+ /** A usage-limit appointment withdrawn, committed at once instead of on the debounced save
2496
+ * (hearsay-tools/cezarion#1236): a crash right after the withdrawal must not find it on disk,
2497
+ * or recovery re-arms work the user cancelled. A run held for its resume settles `failed`. */
2498
+ commitAutoResumeWithdrawal(id) {
2355
2499
  const run = this.peek(id);
2356
- if (!run?.ciWait || run.ciWait.wakeId !== inputId)
2500
+ if (!run || (run.autoResumeAt === undefined && run.autoResumeAttempts === undefined))
2357
2501
  return;
2358
- const { ciWait, ...rest } = run;
2359
- const deliveredAt = new Date().toISOString();
2360
- this.commitIndex(new Map([[id, { ...rest, ciWait: undefined, lastCiWait: { ...ciWait, phase: 'delivered', deliveredAt },
2361
- agentInputs: (run.agentInputs ?? []).map(input => input.id === inputId ? { ...input, deliveredAt } : input), }]]));
2502
+ const held = isUsageLimitHold(run);
2503
+ this.commitIndex(new Map([[id, { ...run, autoResumeAt: undefined, autoResumeAttempts: undefined,
2504
+ ...(held ? { status: 'failed', finishedAt: new Date().toISOString() } : {}) }]]));
2505
+ }
2506
+ /** Legacy CI checkpoint over `commitWait`, kept until its callers move (unified waits, Task 4). */
2507
+ commitCiWait(id, wait, input) {
2508
+ this.commitWait(id, ciWaitEntry(ciWaitSchema.parse(wait)), input);
2362
2509
  }
2363
- /** Retain the settled receipt and retire exactly one wait together with any human message that superseded it. */
2364
- commitWorkerWaitWithdrawal(id, waitId, acceptedHumanMessage) {
2510
+ /** A human message beside a run's waits (hearsay-tools/cezarion#1235): accepted durably, and
2511
+ * every wait stays. A lifecycle opening becomes
2512
+ * the human's and keeps its wake text, so the message opens the continuation (and answers a
2513
+ * pending question) and the owed results still reach the agent. */
2514
+ commitHumanMessageBesideWaits(id, acceptedHumanMessage) {
2365
2515
  const run = this.peek(id);
2366
- if (!run?.delegation || run.delegation.role === 'invalid' || run.delegation.wait?.id !== waitId) {
2367
- throw new Error('worker wait changed before withdrawal');
2368
- }
2369
- const { wait, ...delegation } = run.delegation;
2370
- const message = acceptedHumanMessage ? queuedMessageSchema.parse(acceptedHumanMessage) : undefined;
2371
- this.commitIndex(new Map([[id, { ...run, delegation: { ...delegation, lastWait: wait.phase === 'wake-pending'
2372
- ? reconcileWorkerWait(wait, [], new Date().toISOString())
2373
- : { ...wait, phase: 'wake-pending', reason: wait.reason ?? 'cancelled', wakeId: wait.wakeId ?? wait.id } },
2374
- // An adopted agent input belongs to its sender, even when it carries the wait receipt.
2375
- ...(run.agentInputs ? { agentInputs: run.agentInputs.filter(input => input.id !== wait.wakeId || input.deliveredAt || input.source === 'agent') } : {}),
2376
- ...(message ? {
2377
- queuedMessages: [...(run.queuedMessages ?? []), message],
2378
- ...(run.continuationMessage ? { continuationMessage: {
2379
- ...run.continuationMessage, origin: 'human',
2380
- text: run.continuationMessage.origin === 'lifecycle' ? '' : run.continuationMessage.text,
2381
- } } : {}),
2382
- } : {}),
2516
+ if (!run)
2517
+ throw new Error('missing queued message target');
2518
+ this.commitIndex(new Map([[id, { ...run, queuedMessages: [...(run.queuedMessages ?? []), queuedMessageSchema.parse(acceptedHumanMessage)],
2519
+ ...(run.continuationMessage ? { continuationMessage: { ...run.continuationMessage, origin: 'human' } } : {}),
2383
2520
  }]]));
2384
2521
  }
2385
- /** Successful deferred human delivery consumes only its persisted queue ID.
2386
- * A crash before this checkpoint may replay that same message on recovery. */
2387
- commitQueuedMessageDelivery(id, messageId) {
2522
+ /**
2523
+ * The one write of a run outcome (hearsay-tools/cezarion#1805): an input or wait delivered, a wake
2524
+ * charged, a wait settled, a question answered, staged by `applyOutcomes` with what it depends on
2525
+ * (`deps`) and committed in one transaction, with every pending row. A crash keeps all of it or
2526
+ * none. Nothing changes: nothing is written. Needs no live session: it reads the stored record.
2527
+ *
2528
+ * A refused write throws with nothing changed. `bestEffort` is for a cleanup path, which must
2529
+ * never stop on it: it warns and applies the change in memory for the debounced save to retry.
2530
+ * A conversation request outcome is the other sanctioned write: `commitConversation`.
2531
+ */
2532
+ commitOutcome(id, outcomes, deps = {}, opts = {}) {
2388
2533
  const run = this.peek(id);
2389
2534
  if (!run)
2390
- throw new Error('missing queued message target');
2391
- this.commitIndex(new Map([[id, { ...run, queuedMessages: (run.queuedMessages ?? []).filter(message => message.id !== messageId) }]]));
2535
+ return { committed: false, answered: [] };
2536
+ const staged = applyOutcomes(run, Array.isArray(outcomes) ? outcomes : [outcomes], deps);
2537
+ if (!staged.changed)
2538
+ return { committed: false, answered: [] };
2539
+ try {
2540
+ this.commitIndex(new Map([[id, staged.next]]));
2541
+ }
2542
+ catch (error) {
2543
+ if (!opts.bestEffort)
2544
+ throw error;
2545
+ console.warn(`[cez] outcome checkpoint failed: ${error instanceof Error ? error.message : String(error)}`);
2546
+ const live = this.record(id);
2547
+ if (!live)
2548
+ return { committed: false, answered: staged.answered };
2549
+ for (const key of Object.keys(live))
2550
+ if (!(key in staged.next))
2551
+ delete live[key];
2552
+ Object.assign(live, staged.next);
2553
+ this.touch(live);
2554
+ return { committed: false, answered: staged.answered };
2555
+ }
2556
+ return { committed: true, answered: staged.answered };
2392
2557
  }
2393
2558
  /** An acknowledged inactive-root Finish must survive the async diff and restart. */
2394
2559
  commitRootFinishIntent(id) {
@@ -2419,7 +2584,7 @@ export class RunStore extends EventEmitter {
2419
2584
  const { finishRequestedAt: _intent, ...delegation } = run.delegation;
2420
2585
  const finishedAt = new Date().toISOString();
2421
2586
  this.commitIndex(new Map([[id, { ...run, delegation, status, finishedAt, currentStepId: undefined, autoResumeAttempts: undefined,
2422
- activity: undefined, monitoringWakeAt: undefined, monitoringWakeCapReached: undefined,
2587
+ activity: undefined,
2423
2588
  steps: run.steps.map(step => step.status === 'waiting' || step.status === 'running'
2424
2589
  ? { ...step, status: 'done', finishedAt: step.finishedAt ?? finishedAt } : step),
2425
2590
  }]]));
@@ -2664,16 +2829,12 @@ export class RunStore extends EventEmitter {
2664
2829
  return run;
2665
2830
  }
2666
2831
  /**
2667
- * The durable write: `staged` holds the records this operation replaces (`null` deletes one),
2668
- * in the order subscribers hear about them. The transaction is the durability boundary;
2669
- * flush() is deliberately best-effort.
2670
- *
2671
- * One transaction carries the staged rows and every row a debounced save still owes, so the
2672
- * database never holds a commit without the optimistic writes made before it. If it fails,
2673
- * nothing changed anywhere: the held records, the database, the pending dirty rows and the
2674
- * subscribers all see the store as it was.
2832
+ * Durably replace or delete staged rows together with pending dirty rows. Persist before
2833
+ * changing held objects or notifying subscribers. On failure retain all pending state; flush()
2834
+ * remains best-effort.
2675
2835
  */
2676
- commitIndex(staged, source) {
2836
+ commitIndex(input, source) {
2837
+ const staged = this.prepareCompletionActions(withoutStaleResumes(input));
2677
2838
  if (!this.db)
2678
2839
  throw this.openFailure ?? new Error('runs database unavailable: nothing can be saved');
2679
2840
  this.assertWritable(staged);
@@ -2687,8 +2848,13 @@ export class RunStore extends EventEmitter {
2687
2848
  }
2688
2849
  this.committing.add(id);
2689
2850
  const current = this.held.get(id);
2690
- if (current)
2851
+ if (current) {
2852
+ for (const key of Object.keys(current)) {
2853
+ if (!(key in next))
2854
+ delete current[key];
2855
+ }
2691
2856
  Object.assign(current, next);
2857
+ }
2692
2858
  else
2693
2859
  this.held.set(id, next);
2694
2860
  }
@@ -2706,6 +2872,107 @@ export class RunStore extends EventEmitter {
2706
2872
  this.committing.delete(id);
2707
2873
  this.sweepIfSettled(staged.keys());
2708
2874
  }
2875
+ for (const [id, next] of staged)
2876
+ if (next?.archived)
2877
+ this.compressor.enqueue(id);
2878
+ }
2879
+ /** Completion and its intent/archive share the same committed rows, including root Finish. */
2880
+ prepareCompletionActions(input) {
2881
+ const staged = new Map(input);
2882
+ for (const [id, candidate] of input) {
2883
+ if (!candidate)
2884
+ continue;
2885
+ const previous = this.peek(id);
2886
+ let next = candidate;
2887
+ const changed = candidate.status === 'done' && !candidate.archived && (previous?.status !== 'done' ||
2888
+ Boolean(previous.archiveWhenDone) !== Boolean(candidate.archiveWhenDone) ||
2889
+ Boolean(previous.autoMergePr) !== Boolean(candidate.autoMergePr));
2890
+ if (changed && (candidate.archiveWhenDone || candidate.autoMergePr)) {
2891
+ next = { ...candidate, completionAction: { id: randomUUID(), phase: 'pending' } };
2892
+ staged.set(id, next);
2893
+ if (next.archiveWhenDone && !next.autoMergePr) {
2894
+ const blocker = this.completionArchiveBlocker(next);
2895
+ next.completionAction = { ...next.completionAction, phase: blocker ? 'blocked' : 'complete', ...(blocker ? { message: blocker } : {}) };
2896
+ if (!blocker)
2897
+ this.stageArchive(staged, next, true);
2898
+ }
2899
+ }
2900
+ else if (candidate.status !== 'done' || (!candidate.archiveWhenDone && !candidate.autoMergePr)) {
2901
+ if (candidate.completionAction)
2902
+ staged.set(id, { ...candidate, completionAction: undefined });
2903
+ }
2904
+ }
2905
+ return staged;
2906
+ }
2907
+ completionArchiveBlocker(root) {
2908
+ const family = [root, ...this.listWorkersOf(root.id)];
2909
+ for (const run of family) {
2910
+ const facts = this.facts.peek(run.id);
2911
+ if (!facts || !['done', 'review', 'failed', 'cancelled'].includes(run.status) || run.waits?.some(wait => wait.phase === 'open') ||
2912
+ run.hasPendingPermission || facts.pendingAsk !== undefined)
2913
+ return 'Task or worker work still needs attention. Open the task or archive manually.';
2914
+ if (run.delegation?.role === 'invalid' || (run.delegation?.role === 'worker' && this.readWorkerExecution(run.id)?.phase !== 'complete'))
2915
+ return 'Worker execution has not settled. Open the worker or archive manually.';
2916
+ }
2917
+ return undefined;
2918
+ }
2919
+ /** One archive write for manual actions, merge observations, completion, and sweeps.
2920
+ * Archiving files history away; it never stops a process or removes a worktree. */
2921
+ stageArchive(staged, root, archived) {
2922
+ const ids = root.delegation?.role === 'worker' ? [root.id] :
2923
+ [root.id, ...new Set([...(this.db?.listIdsByParent(root.id) ?? []), ...this.listWorkersOf(root.id).map(run => run.id)])];
2924
+ const now = new Date().toISOString();
2925
+ for (const id of ids) {
2926
+ const member = staged.get(id) ?? this.peek(id);
2927
+ if (!member)
2928
+ continue;
2929
+ if (!archived) {
2930
+ this.compressor.cancel(id);
2931
+ restoreHistory(this.dataDir, id);
2932
+ }
2933
+ const next = { ...member, archiveBlocker: undefined };
2934
+ archiveFields(next, archived, now);
2935
+ staged.set(id, next);
2936
+ }
2937
+ }
2938
+ /** A bounded persisted queue also covers projects without a live RunManager. */
2939
+ listCompletionCandidates(afterSeq = 0, limit = 32) {
2940
+ return this.db?.listCompletionCandidates(afterSeq, limit) ?? [];
2941
+ }
2942
+ canCompleteAction(id, token) {
2943
+ const run = this.record(id);
2944
+ return Boolean(run && run.status === 'done' && !run.archived && (run.autoMergePr || run.archiveWhenDone) &&
2945
+ run.completionAction?.id === token && run.completionAction.phase !== 'complete' && !this.writeRefusal(id));
2946
+ }
2947
+ claimCompletionAction(id, token) {
2948
+ if (!this.canCompleteAction(id, token))
2949
+ return false;
2950
+ const run = this.record(id);
2951
+ this.assertWritable(new Map([[id, run]]));
2952
+ // A normal write may defer its claim until the transaction. A remote mutation
2953
+ // needs the committed claim now, and must not act on a stale row read before it.
2954
+ const claim = this.db?.getClaim(familyKey(run));
2955
+ return Boolean(claim && claim.session === this.owner?.session &&
2956
+ claim.generation === this.claimed.get(familyKey(run)) &&
2957
+ this.db?.get(id)?.revision === this.base.get(id)?.revision);
2958
+ }
2959
+ completeAction(id, token, phase, message) {
2960
+ const run = this.record(id);
2961
+ if (!run || !this.canCompleteAction(id, token))
2962
+ return false;
2963
+ if (run.completionAction?.phase === phase && run.completionAction.message === message)
2964
+ return true;
2965
+ const next = { ...run, completionAction: { id: token, phase, ...(message ? { message } : {}) } };
2966
+ const staged = new Map([[id, next]]);
2967
+ if (phase === 'complete' && next.archiveWhenDone) {
2968
+ const blocker = this.completionArchiveBlocker(next);
2969
+ if (blocker)
2970
+ next.completionAction = { id: token, phase: 'blocked', message: blocker };
2971
+ else
2972
+ this.stageArchive(staged, next, true);
2973
+ }
2974
+ this.commitIndex(staged);
2975
+ return true;
2709
2976
  }
2710
2977
  /**
2711
2978
  * Refuse (throwing, with nothing changed) a commit that stages a quarantined run, or a run whose
@@ -2754,18 +3021,17 @@ export class RunStore extends EventEmitter {
2754
3021
  delete run.referencedIssueNumberSeeded;
2755
3022
  }
2756
3023
  const normalized = { ...patch };
2757
- if (normalized.status && !['running', 'waiting', 'queued'].includes(normalized.status)) {
2758
- normalized.activity = undefined;
2759
- normalized.monitoringWakeAt = undefined;
2760
- normalized.monitoringWakeCapReached = undefined;
2761
- }
2762
- // …and the mirror image for the usage-limit resume (spec
2763
- // 2026-08-03-auto-resume-after-usage-limit): it is a promise made ABOUT a failed run, so a
2764
- // run coming back to life — the resume itself, a user Continue, a re-queue — retires it.
2765
- // The manager's timer re-checks the record before it fires, so a cleared field is enough.
2766
- if (normalized.status && ['running', 'waiting', 'queued'].includes(normalized.status)) {
2767
- normalized.autoResumeAt = undefined;
3024
+ if (normalized.pullRequestUrl !== undefined) {
3025
+ this.accumulatePullRequests(run, [run.pullRequestUrl, normalized.pullRequestUrl].flatMap((url) => {
3026
+ const number = refUrlNumber(url);
3027
+ if (number === undefined)
3028
+ return [];
3029
+ const parsed = runPullRequestSchema.safeParse({ number, url, source: 'created' });
3030
+ return parsed.success ? [parsed.data] : [];
3031
+ }));
2768
3032
  }
3033
+ // Waits are not cleared here: RunManager cancels them explicitly when a run ends (spec
3034
+ // 2026-10-17-unified-waits), so a terminal patch never retires one as a side effect.
2769
3035
  const archived = Object.prototype.hasOwnProperty.call(normalized, 'archived')
2770
3036
  ? Boolean(normalized.archived)
2771
3037
  : undefined;
@@ -2776,7 +3042,22 @@ export class RunStore extends EventEmitter {
2776
3042
  // Restore before mutating so a throw leaves status and archived unchanged (#818 S3).
2777
3043
  if (archived === false)
2778
3044
  restoreHistory(this.dataDir, id);
3045
+ // Opted-in completion and the handoff's records (the file shows only committed ones) take this
3046
+ // transaction. Default-off Finish keeps its best-effort write so a locked database cannot stop cleanup.
3047
+ if ((normalized.status === 'done' && (run.archiveWhenDone || run.autoMergePr)) || normalized.handoffRecords !== undefined ||
3048
+ normalized.archiveWhenDone !== undefined || normalized.autoMergePr !== undefined) {
3049
+ const next = { ...run, ...this.redactPatch(normalized) };
3050
+ if (archived !== undefined)
3051
+ archiveFields(next, archived, new Date().toISOString());
3052
+ this.commitIndex(new Map([[id, next]]));
3053
+ return this.record(id);
3054
+ }
2779
3055
  Object.assign(run, this.redactPatch(normalized));
3056
+ // The usage-limit resume is the appointment of a `waiting` run (hearsay-tools/cezarion#1236),
3057
+ // so a run coming back to life or settling retires it. `commitIndex` applies the same rule to
3058
+ // every commit (`withoutStaleResumes`); this debounced path mutates the held record without it.
3059
+ if (run.status !== 'waiting')
3060
+ run.autoResumeAt = undefined;
2780
3061
  if (normalized.task !== undefined)
2781
3062
  this.resolveEditedTaskRefs(run);
2782
3063
  if (archived !== undefined)
@@ -2798,7 +3079,7 @@ export class RunStore extends EventEmitter {
2798
3079
  run.referencedPullRequestUrl = resolveReferencedRef(run.referencedPrCandidates, run.task, referencedPrDeclaration(run), this.repoHandle);
2799
3080
  }
2800
3081
  if (run.referencedIssueCandidates !== undefined) {
2801
- run.referencedIssueUrl = resolveReferencedRef(run.referencedIssueCandidates, run.task, run.markerRefs?.issue, this.repoHandle);
3082
+ run.referencedIssueUrl = resolveIssueReference(run, this.repoHandle);
2802
3083
  const number = refUrlNumber(run.referencedIssueUrl);
2803
3084
  if (run.referencedIssueNumberSeeded) {
2804
3085
  // We may change only the number this janitor owns, including clearing an ambiguous one.
@@ -2893,6 +3174,123 @@ export class RunStore extends EventEmitter {
2893
3174
  run.costUsd = run.steps.some((step) => step.costUsd !== undefined) ? cost : undefined;
2894
3175
  this.touch(run);
2895
3176
  }
3177
+ listArchiveOnMergeCandidates(afterSeq = 0, limit = 32) {
3178
+ return this.db?.listArchiveOnMergeCandidates(afterSeq, limit) ?? [];
3179
+ }
3180
+ /** Use unnormalized persisted evidence: cold reads must not turn interrupted work into safety. */
3181
+ archiveFamily(id, isActive) {
3182
+ const ids = [id, ...new Set([...(this.db?.listIdsByParent(id) ?? []), ...this.listWorkersOf(id).map(run => run.id)])];
3183
+ if (ids.length > 1025)
3184
+ return undefined;
3185
+ const family = [];
3186
+ const block = (message, runId = id) => { this.setArchiveBlocker(id, { message, runId }); return undefined; };
3187
+ for (const memberId of ids) {
3188
+ const row = this.db?.get(memberId);
3189
+ const raw = row ? JSON.parse(row.data) : this.held.get(memberId);
3190
+ const decoded = decodeRunRow(JSON.stringify(raw));
3191
+ if (!decoded)
3192
+ return block('Task history is unavailable. Open the task or archive it manually.', memberId);
3193
+ // Do not let the runtime schema's per-entry salvage hide malformed PR authority.
3194
+ if (raw && typeof raw === 'object' && 'pullRequests' in raw &&
3195
+ (!Array.isArray(raw.pullRequests) || raw.pullRequests.some(entry => !runPullRequestSchema.safeParse(entry).success)))
3196
+ return block('PR associations are invalid. Check the task PRs or archive manually.', memberId);
3197
+ const run = this.held.get(memberId) ?? decoded.run;
3198
+ if (row && !this.held.has(memberId))
3199
+ this.decodeCold(row); // records the synchronous write fence
3200
+ if (this.deleted.has(memberId) || this.quarantined.has(memberId) || this.writeRefusal(memberId))
3201
+ return block('Task history is owned elsewhere or cannot be changed. Retry after its owner stops.', memberId);
3202
+ if (this.pins.has(memberId) || isActive(memberId) || isLiveRecord(run) || row?.live ||
3203
+ !['review', 'done', 'failed', 'cancelled'].includes(run.status))
3204
+ return block('Task work is still active. Wait for it to finish or archive manually.', memberId);
3205
+ if (run.waits?.some(wait => wait.kind === 'human' && wait.phase === 'open') || run.hasPendingPermission)
3206
+ return block('Task needs an answer or permission. Open it to respond, or archive manually.', memberId);
3207
+ if (run.delegation?.role === 'invalid')
3208
+ return block('Worker ownership is invalid. Inspect the task or archive manually.', memberId);
3209
+ const facts = this.facts.peek(memberId);
3210
+ if (!facts || facts.pendingAsk !== undefined)
3211
+ return block('Task has an unanswered question or unread history. Open it to respond, or archive manually.', memberId);
3212
+ if (run.delegation?.role === 'worker' && this.readWorkerExecution(memberId)?.phase !== 'complete')
3213
+ return block('Worker execution has not settled. Open the worker or archive manually.', memberId);
3214
+ // Process and scratch cleanup evidence protects deletion, not reversible filing.
3215
+ family.push(run);
3216
+ }
3217
+ if (family[0]?.delegation?.role === 'worker' || family[0]?.archived)
3218
+ return undefined;
3219
+ return family;
3220
+ }
3221
+ /** Blockers are feedback, never archive authority or cleanup permission. */
3222
+ setArchiveBlocker(id, blocker) {
3223
+ const run = this.record(id);
3224
+ if (!run || run.archived || isDeepStrictEqual(run.archiveBlocker, blocker))
3225
+ return;
3226
+ this.commitIndex(new Map([[id, { ...run, archiveBlocker: blocker }]]));
3227
+ }
3228
+ archiveMergeBlocker(snapshot, missing, repoBase) {
3229
+ const url = `https://github.com/${missing.owner}/${missing.repo}/pull/${missing.number}`;
3230
+ const family = [snapshot.rootId, ...(this.db?.listIdsByParent(snapshot.rootId) ?? [])].flatMap(id => {
3231
+ const run = this.peek(id);
3232
+ return run ? [run] : [];
3233
+ });
3234
+ const member = family.find(run => archivePullRequests([run], repoBase)?.some(pr => pullRequestKey(pr) === pullRequestKey(missing)));
3235
+ this.setArchiveBlocker(snapshot.rootId, { message: `PR ${missing.owner}/${missing.repo}#${missing.number} is not confirmed merged. Check the PR association or archive manually.`,
3236
+ runId: member?.id ?? snapshot.rootId, pullRequestUrl: url });
3237
+ }
3238
+ hasArchiveOnMergeAssociations(id) {
3239
+ return this.db?.archiveFamilyHasPullRequests(id) ?? false;
3240
+ }
3241
+ async prepareArchiveOnMerge(id) {
3242
+ const ids = [id, ...(this.db?.listIdsByParent(id) ?? [])];
3243
+ if (ids.length > 1025)
3244
+ return;
3245
+ for (const memberId of ids)
3246
+ await this.facts.ready(memberId);
3247
+ }
3248
+ archiveOnMergeSnapshot(id, repoBase, isActive = () => false) {
3249
+ const family = this.archiveFamily(id, isActive);
3250
+ if (!family)
3251
+ return undefined;
3252
+ return this.archiveSnapshot(family, repoBase);
3253
+ }
3254
+ archiveSnapshot(family, repoBase) {
3255
+ const pullRequests = archivePullRequests(family, repoBase);
3256
+ if (!pullRequests) {
3257
+ this.setArchiveBlocker(family[0].id, { message: 'PR associations cannot be resolved. Check task and worker PRs, or archive manually.' });
3258
+ return undefined;
3259
+ }
3260
+ const fingerprint = archiveDigest(pullRequests);
3261
+ if (family[0].autoArchive?.fingerprint === fingerprint)
3262
+ return undefined;
3263
+ return { rootId: family[0].id, fingerprint, pullRequests, token: archiveDigest(family.map(({ archiveBlocker: _blocker, ...run }) => run)) };
3264
+ }
3265
+ /** No await between final revalidation and the fenced, all-or-nothing cascade. */
3266
+ archiveAfterMerge(snapshot, observations, repoBase, isActive = () => false) {
3267
+ const family = this.archiveFamily(snapshot.rootId, isActive);
3268
+ if (!family)
3269
+ return false;
3270
+ const current = this.archiveSnapshot(family, repoBase);
3271
+ if (!current || current.token !== snapshot.token || current.fingerprint !== snapshot.fingerprint)
3272
+ return false;
3273
+ const parsed = observations.map(observation => mergedPullRequestSchema.safeParse(observation));
3274
+ if (parsed.some(result => !result.success))
3275
+ return false;
3276
+ const merged = parsed.flatMap(result => result.success ? [result.data] : []);
3277
+ const byKey = new Map(merged.map(pr => [pullRequestKey(pr), pr]));
3278
+ if (byKey.size !== current.pullRequests.length || current.pullRequests.some(pr => !byKey.has(pullRequestKey(pr))))
3279
+ return false;
3280
+ const restoredAt = Math.max(0, ...family.map(run => run.manualUnarchivedAt ? Date.parse(run.manualUnarchivedAt) : 0));
3281
+ if (restoredAt && merged.every(pr => Date.parse(pr.mergedAt) <= restoredAt))
3282
+ return false;
3283
+ const now = new Date().toISOString();
3284
+ if (merged.some(pr => Date.parse(pr.mergedAt) > Date.parse(now)))
3285
+ return false;
3286
+ const staged = new Map();
3287
+ this.stageArchive(staged, family[0], true);
3288
+ staged.get(snapshot.rootId).autoArchive = { reason: 'pull-requests-merged', archivedAt: now, fingerprint: current.fingerprint, pullRequests: merged };
3289
+ this.commitIndex(staged);
3290
+ for (const id of staged.keys())
3291
+ this.compressor.enqueue(id);
3292
+ return true;
3293
+ }
2896
3294
  setArchived(id, archived) {
2897
3295
  const run = this.record(id);
2898
3296
  if (!run)
@@ -2902,30 +3300,23 @@ export class RunStore extends EventEmitter {
2902
3300
  }
2903
3301
  /** Parent + owned workers; returns how many records flipped `archived`. */
2904
3302
  applyArchivedCascade(run, archived) {
3303
+ const staged = new Map();
3304
+ this.stageArchive(staged, run, archived);
2905
3305
  let changed = 0;
2906
- if (this.applyArchived(run, archived))
2907
- changed++;
2908
- // Owned workers nest under their parent only while both share Active/Archived (#250).
2909
- // Cascade from the parent only — archiving one worker stays independent.
2910
- if (run.delegation?.role !== 'worker') {
2911
- for (const { id } of this.listWorkersOf(run.id)) {
2912
- const child = this.record(id);
2913
- if (child && this.applyArchived(child, archived))
2914
- changed++;
2915
- }
3306
+ for (const [id, next] of staged) {
3307
+ if (next && this.peek(id)?.archived !== archived)
3308
+ changed++;
3309
+ if (next)
3310
+ next.completionAction = undefined; // an explicit filing/restoration retires pending work
2916
3311
  }
3312
+ this.commitIndex(staged);
2917
3313
  return changed;
2918
3314
  }
2919
3315
  applyArchived(run, archived) {
2920
3316
  const changed = run.archived !== archived;
2921
3317
  // Transcript first: a restore throw aborts with the record unchanged (#818 S3).
2922
3318
  this.syncTranscriptForm(run.id, archived);
2923
- run.archived = archived;
2924
- run.archivedAt = archived ? new Date().toISOString() : undefined;
2925
- if (archived) {
2926
- clearPendingAutoResume(run);
2927
- clearPin(run);
2928
- }
3319
+ archiveFields(run, archived, new Date().toISOString());
2929
3320
  this.touch(run);
2930
3321
  return changed;
2931
3322
  }
@@ -3021,17 +3412,18 @@ export class RunStore extends EventEmitter {
3021
3412
  /** Bulk-archive finished runs (#780). `isSweepable` decides which; `archived` counts every
3022
3413
  * record that flipped (cascaded workers included), `ids` lists the top-level runs picked and
3023
3414
  * `pinnedIds` the subset that carried a pin before `clearPin` dropped it. */
3024
- archiveFinished(scope) {
3415
+ archiveFinished(scope, automation) {
3025
3416
  let archived = 0;
3026
3417
  // Snapshot first: cascade may archive still-live workers, mutating the held set, and the pin
3027
3418
  // has to be read before `applyArchived` clears it. Runs not in memory are picked off their
3028
3419
  // stored summary, which carries every field `isSweepable` reads.
3029
- const picked = [...this.held.values()].filter((run) => isSweepable(run, scope)).map((run) => [run, this.listOrder(run)]);
3420
+ const picked = [...this.held.values()].filter((run) => isSweepable({ ...run, automationId: automationIdOf(run) }, scope, automation))
3421
+ .map((run) => [run, this.listOrder(run)]);
3030
3422
  for (const row of this.db?.listWhere("archived = 0 AND status IN ('done', 'failed', 'cancelled') AND parent_run_id IS NULL") ?? []) {
3031
3423
  if (this.held.has(row.id) || this.deleted.has(row.id))
3032
3424
  continue;
3033
3425
  const summary = parseStoredSummary(row.summary);
3034
- if (summary && isSweepable(summary, scope))
3426
+ if (summary && isSweepable(summary, scope, automation))
3035
3427
  picked.push([summary, row]);
3036
3428
  }
3037
3429
  // Another process's runs are not this store's to archive (#779, plan step 3): they stay out.
@@ -3082,31 +3474,13 @@ export class RunStore extends EventEmitter {
3082
3474
  this.touch(run);
3083
3475
  return run;
3084
3476
  }
3085
- /** Bulk mark-read: stamp every currently-unread finished run; returns the count.
3086
- * "Unread" here is the same rule the cockpit paints (`isUnread` in read-state.ts),
3087
- * clause for clause:
3088
- * - a `done` or `failed` run that finished and has not been seen since;
3089
- * - cancelled runs are never unread — you stopped them yourself;
3090
- * - archived ones never are either, since archiving is a stronger "done with this"
3091
- * than reading;
3092
- * - and a `failed` run with a pending `autoResumeAt` is not a done item AT ALL
3093
- * (`isScheduledResume`, spec 2026-08-03-auto-resume-after-usage-limit): it has an
3094
- * appointment to pick the work back up, so there is no outcome to have missed.
3095
- *
3096
- * Keeping the two rules identical is what makes the returned count the number the
3097
- * cockpit's unread badge was showing. The `autoResumeAt` clause is the one that drifted
3098
- * (#803): `isUnread` gained it with auto-resume and this sweep did not, so a task waiting
3099
- * out a usage limit was uncounted by the badge but stamped read by the sweep — and this
3100
- * comment asserted an invariant the code no longer held.
3101
- *
3102
- * This rule lives in two languages of the same repo, which is why it has now drifted
3103
- * once. The cockpit cannot import it (`packages/web` does not depend on the service, and
3104
- * should not), so a single definition would have to move to `packages/contract` — the one
3105
- * package both sides already import. Worth doing; deliberately not done here, because
3106
- * widening the contract package's remit from "shapes" to "behavior" is a design change
3107
- * that deserves its own review rather than riding along in a bug fix. Until then: EDIT
3108
- * BOTH, and the case-table tests on either side are what catch you if you don't. */
3109
- markAllRead() {
3477
+ /**
3478
+ * Stamp unread finished runs in bulk and return the count. Keep the rule aligned with cockpit
3479
+ * isUnread: done/failed, finished since seen, not archived; cancelled and usage-limit waiting
3480
+ * runs are excluded. `automation` narrows it to one origin, as in `archiveFinished`
3481
+ * (hearsay-tools/cezarion#1616); absent stamps both.
3482
+ */
3483
+ markAllRead(automation) {
3110
3484
  const now = new Date().toISOString();
3111
3485
  let count = 0;
3112
3486
  // Runs not in memory are picked by the same rule over their stored summary, then re-checked
@@ -3123,11 +3497,12 @@ export class RunStore extends EventEmitter {
3123
3497
  for (const run of candidates) {
3124
3498
  const unread = !run.archived &&
3125
3499
  (run.status === 'done' || run.status === 'failed') &&
3126
- !(run.status === 'failed' && run.autoResumeAt !== undefined) &&
3127
3500
  run.finishedAt !== undefined &&
3128
3501
  (run.seenAt === undefined || run.seenAt < run.finishedAt);
3129
3502
  if (!unread)
3130
3503
  continue;
3504
+ if (automation !== undefined && (automationIdOf(run) !== undefined) !== automation)
3505
+ continue;
3131
3506
  run.seenAt = now;
3132
3507
  this.touch(run);
3133
3508
  count++;
@@ -3146,7 +3521,7 @@ export class RunStore extends EventEmitter {
3146
3521
  // Scrub credentials before the event touches disk or the live wire (#427):
3147
3522
  // tool-result output is persisted verbatim and served back over the API, so
3148
3523
  // a secret in an agent's command output would otherwise land in `.ai/cezar/`.
3149
- const full = this.redact({ ...event, seq, ts: new Date().toISOString() });
3524
+ const full = this.redact(defined({ ...event, seq, ts: new Date().toISOString() }));
3150
3525
  // Sync append keeps event order without a write queue; local NDJSON
3151
3526
  // appends at agent-event rates are effectively free.
3152
3527
  if (!hasPlainHistory(this.dataDir, runId))
@@ -3155,9 +3530,7 @@ export class RunStore extends EventEmitter {
3155
3530
  appendFileSync(this.eventsPath(runId), line, 'utf8');
3156
3531
  this.facts.append(runId, full, Buffer.byteLength(line));
3157
3532
  this.maybeEnqueueHistoryCompress(run);
3158
- if ((full.type === 'ask.requested' || full.type === 'human-input-delivered') &&
3159
- this.syncHumanAskSummary(run))
3160
- this.touch(run);
3533
+ this.liveItems.observe(runId, full, false);
3161
3534
  this.emit('event', { runId, event: full });
3162
3535
  // The janitor trick: agents print the PR URL after `gh pr create` — the
3163
3536
  // first one spotted in the transcript becomes the run's PR link. Scans v1
@@ -3174,23 +3547,31 @@ export class RunStore extends EventEmitter {
3174
3547
  const claim = eventCreationClaimFragments(full).join(' ');
3175
3548
  if (haystack.length > 0) {
3176
3549
  let changed = false;
3177
- if (!run.pullRequestUrl) {
3178
- const created = CREATED_PR_RE.test(claim) ? createdPrUrl(`${claim} ${haystack}`) : undefined;
3179
- if (created) {
3180
- this.updateRun(runId, { pullRequestUrl: created });
3181
- // Adopting the created tier can RELEASE a declaration the referenced tier was holding
3182
- // (see `referencedPrDeclaration`), so re-resolve here too: the about-PR must come back
3183
- // whether the marker arrived before the creation evidence or after it.
3184
- const resolved = resolveReferencedRef(run.referencedPrCandidates ?? [], run.task, referencedPrDeclaration(run), this.repoHandle);
3185
- if (resolved !== run.referencedPullRequestUrl) {
3186
- run.referencedPullRequestUrl = resolved;
3187
- changed = true;
3188
- }
3550
+ // One associated URL per trusted creation claim; replayed v1/v2 reports merge by identity.
3551
+ const item = full.item;
3552
+ const toolOutput = item?.kind === 'tool' && typeof item.output === 'string' ? item.output : undefined;
3553
+ const created = createdPrUrls(claim, haystack, toolOutput);
3554
+ for (const url of created) {
3555
+ const number = refUrlNumber(url);
3556
+ const parsed = runPullRequestSchema.safeParse({ number, url, source: 'created' });
3557
+ if (!parsed.success)
3558
+ continue;
3559
+ changed = this.accumulatePullRequests(run, [parsed.data]) || changed;
3560
+ if (!run.pullRequestUrl) {
3561
+ run.pullRequestUrl = url;
3562
+ changed = true;
3189
3563
  }
3190
- else if (PR_URL_RE.test(haystack) && this.trackReferencedPrs(run, haystack)) {
3564
+ }
3565
+ if (created.length > 0) {
3566
+ const resolved = resolveReferencedRef(run.referencedPrCandidates ?? [], run.task, referencedPrDeclaration(run), this.repoHandle);
3567
+ if (resolved !== run.referencedPullRequestUrl) {
3568
+ run.referencedPullRequestUrl = resolved;
3191
3569
  changed = true;
3192
3570
  }
3193
3571
  }
3572
+ else if (!run.pullRequestUrl && PR_URL_RE.test(haystack) && this.trackReferencedPrs(run, haystack)) {
3573
+ changed = true;
3574
+ }
3194
3575
  // Issue links feed their own referenced tier regardless of PR state —
3195
3576
  // a task that created a PR can still be ABOUT an issue
3196
3577
  // (spec 2026-07-21-report-ref-discovery).
@@ -3242,7 +3623,7 @@ export class RunStore extends EventEmitter {
3242
3623
  if (candidatesChanged)
3243
3624
  run.referencedIssueCandidates = [...seen];
3244
3625
  const prev = run.referencedIssueUrl;
3245
- run.referencedIssueUrl = resolveReferencedRef(run.referencedIssueCandidates ?? [], run.task, run.markerRefs?.issue, this.repoHandle);
3626
+ run.referencedIssueUrl = resolveIssueReference(run, this.repoHandle);
3246
3627
  let numberChanged = false;
3247
3628
  if (run.markerRefs?.issue === undefined && ISSUE_URL_RE.test(seedHaystack)) {
3248
3629
  if (run.referencedIssueUrl && run.issueNumber === undefined) {
@@ -3266,7 +3647,7 @@ export class RunStore extends EventEmitter {
3266
3647
  /**
3267
3648
  * Apply agent-declared reference markers (spec 2026-07-18-task-ref-markers).
3268
3649
  * Marker values are authoritative for the display tier: they overwrite the
3269
- * regex/namer numbers, and a declared PR re-resolves the referenced URL
3650
+ * regex/namer numbers unless the prompt identifies the originating issue, and a declared PR re-resolves the referenced URL
3270
3651
  * against the candidate working set — including down to `undefined` when no
3271
3652
  * candidate matches (a wrong chip is worse than no chip). The created tier
3272
3653
  * (`pullRequestUrl`) is deliberately untouched.
@@ -3279,6 +3660,17 @@ export class RunStore extends EventEmitter {
3279
3660
  const run = this.record(runId);
3280
3661
  if (!run || (refs.pr === undefined && refs.issue === undefined))
3281
3662
  return run;
3663
+ const numbers = [...new Set([...(run.markerRefs?.pr === undefined ? [] : [run.markerRefs.pr]),
3664
+ ...(refs.prs ?? (refs.pr === undefined ? [] : [refs.pr]))])];
3665
+ if (numbers.length)
3666
+ this.accumulatePullRequests(run, numbers.flatMap((number) => {
3667
+ const matches = (run.referencedPrCandidates ?? []).filter((url) => refUrlNumber(url) === number &&
3668
+ (this.repoHandle ? isRepoScopedRef(url, run.task, this.repoHandle) : promptNamesRepo(url, run.task)));
3669
+ const url = matches.length === 1 ? matches[0] : matches.length === 0 && this.repoHandle
3670
+ ? `https://github.com/${this.repoHandle.owner}/${this.repoHandle.name}/pull/${number}` : undefined;
3671
+ const parsed = runPullRequestSchema.safeParse({ number, ...(url ? { url } : {}), source: 'declared' });
3672
+ return parsed.success ? [parsed.data] : [];
3673
+ }));
3282
3674
  run.markerRefs = {
3283
3675
  ...run.markerRefs,
3284
3676
  ...(refs.pr !== undefined ? { pr: refs.pr } : {}),
@@ -3291,28 +3683,53 @@ export class RunStore extends EventEmitter {
3291
3683
  run.prNumber = refs.pr;
3292
3684
  }
3293
3685
  if (refs.issue !== undefined) {
3294
- run.issueNumber = refs.issue;
3686
+ run.issueNumber = originatingIssue(run) ?? refs.issue;
3295
3687
  delete run.referencedIssueNumberSeeded;
3296
3688
  }
3297
3689
  if (run.markerRefs.pr !== undefined) {
3298
3690
  run.referencedPullRequestUrl = resolveReferencedRef(run.referencedPrCandidates ?? [], run.task, referencedPrDeclaration(run), this.repoHandle);
3299
3691
  }
3300
3692
  if (run.markerRefs.issue !== undefined) {
3301
- run.referencedIssueUrl = resolveReferencedRef(run.referencedIssueCandidates ?? [], run.task, run.markerRefs.issue, this.repoHandle);
3693
+ run.referencedIssueUrl = resolveIssueReference(run, this.repoHandle);
3302
3694
  }
3303
3695
  this.touch(run);
3304
3696
  return run;
3305
3697
  }
3698
+ /** Authoritative creation keeps the first scalar for publishing, plus every created identity. */
3699
+ recordCreatedPr(runId, url) {
3700
+ const run = this.record(runId);
3701
+ const parsed = runPullRequestSchema.safeParse({ number: refUrlNumber(url), url, source: 'created' });
3702
+ if (!run || !parsed.success)
3703
+ return run;
3704
+ let changed = this.accumulatePullRequests(run, [parsed.data]);
3705
+ if (!run.pullRequestUrl) {
3706
+ run.pullRequestUrl = url;
3707
+ changed = true;
3708
+ }
3709
+ if (changed)
3710
+ this.touch(run);
3711
+ return run;
3712
+ }
3713
+ accumulatePullRequests(run, entries) {
3714
+ const next = mergePullRequests(run.pullRequests, entries, this.repoHandle
3715
+ ? `https://github.com/${this.repoHandle.owner}/${this.repoHandle.name}` : undefined);
3716
+ if (isDeepStrictEqual(next, run.pullRequests))
3717
+ return false;
3718
+ run.pullRequests = next;
3719
+ return true;
3720
+ }
3306
3721
  /**
3307
3722
  * Fan an event out to live subscribers WITHOUT writing it to the NDJSON
3308
3723
  * file — the channel for coalesced `item.delta` flushes (protocol-v2
3309
3724
  * performance guardrail: raw deltas never hit disk; replay = the persisted
3310
3725
  * snapshots). Stamped with `seq`/`ts` like persisted lines so the live
3311
- * wire keeps one ordering axis; the seq simply never appears in a replay
3312
- * (gaps are fine — dedup compares with `>`).
3726
+ * wire keeps one ordering axis. Finite polling coalesces active item content
3727
+ * into bounded in-memory snapshots on that clock; disk replay still has gaps
3728
+ * (dedup compares with `>`).
3313
3729
  */
3314
3730
  emitEphemeral(runId, event) {
3315
3731
  const full = this.redact({ ...event, seq: this.nextSeq(runId), ts: new Date().toISOString() });
3732
+ this.liveItems.observe(runId, full, true);
3316
3733
  this.emit('event', { runId, event: full });
3317
3734
  return full;
3318
3735
  }
@@ -3350,19 +3767,89 @@ export class RunStore extends EventEmitter {
3350
3767
  transcriptFacts(runId) {
3351
3768
  return this.facts.get(runId) ?? emptyFacts();
3352
3769
  }
3770
+ /** The run's transcript facts when they are known: undefined when the transcript exists but its
3771
+ * facts are not current or cannot be read, so a caller can tell "no ask" from "no evidence". */
3772
+ knownTranscriptFacts(runId) {
3773
+ return this.facts.peek(runId);
3774
+ }
3353
3775
  hasProjection(runId, projectionId) {
3354
3776
  return this.transcriptFacts(runId).projectionIds.includes(projectionId);
3355
3777
  }
3356
3778
  factsWarming = Promise.resolve();
3357
- /** After open, build the indexes the delegation reconcile will ask for first, off the request
3358
- * path: every member of every family with a conversation (#880). */
3779
+ /** Indexed family enumeration, including members not yet flushed to SQLite. */
3780
+ listFamilyRunIds(rootId) {
3781
+ const ids = new Set([rootId, ...(this.db?.listIdsByParent(rootId) ?? [])]);
3782
+ for (const [id, run] of this.held) {
3783
+ if (familyRootOf(run) === rootId)
3784
+ ids.add(id);
3785
+ else if (id !== rootId)
3786
+ ids.delete(id);
3787
+ }
3788
+ for (const id of this.deleted)
3789
+ ids.delete(id);
3790
+ return [...ids];
3791
+ }
3792
+ recoveryFactIds(familyRootId) {
3793
+ if (familyRootId !== undefined)
3794
+ return this.listFamilyRunIds(familyRootId);
3795
+ const ids = new Set();
3796
+ for (const [id, run] of this.held) {
3797
+ if (!isLiveRecord(run))
3798
+ continue;
3799
+ const root = familyRootOf(run);
3800
+ for (const member of root === undefined ? [id] : this.listFamilyRunIds(root))
3801
+ ids.add(member);
3802
+ }
3803
+ return [...ids];
3804
+ }
3805
+ /** Readiness owns no run snapshot across an await: refresh only the currently held owner. */
3806
+ async prepareTranscriptFacts(runIds) {
3807
+ for (const id of new Set(runIds)) {
3808
+ // Exact sidecars resolve immediately too: recovery must yield to timers/I/O between IDs.
3809
+ await new Promise(resolve => setImmediate(resolve));
3810
+ await this.facts.ready(id);
3811
+ }
3812
+ }
3813
+ prepareRecoveryFacts(familyRootId) {
3814
+ return this.prepareTranscriptFacts(this.recoveryFactIds(familyRootId));
3815
+ }
3816
+ /** Register IDs only here; the index yields between cache reads and off-loop rebuilds. */
3359
3817
  warmTranscriptFacts() {
3360
- // A microtask, not a timer: open returns first, and no timer outlives a store that never warms.
3361
- this.factsWarming = Promise.resolve().then(() => {
3362
- const ids = [];
3363
- for (const rootId of this.listConversationRootIds())
3364
- ids.push(rootId, ...(this.db?.listIdsByParent(rootId) ?? []));
3365
- return this.facts.warm(ids);
3818
+ this.factsWarming = Promise.resolve().then(async () => {
3819
+ if (!this.db)
3820
+ return;
3821
+ const ids = new Set(this.recoveryFactIds());
3822
+ // hearsay-tools/cezarion#949: this microtask ends open()'s synchronous slice, and on a
3823
+ // large history the JSON scan for conversation roots plus one family query per root cost
3824
+ // as much again. Past one slice's worth of runs, yield before the scan and between root
3825
+ // batches; a small store (every unit test) still registers in this one microtask.
3826
+ const large = this.listRunIds().length > WARM_IDS_PER_SLICE;
3827
+ const turn = async () => {
3828
+ await new Promise(resolve => setImmediate(resolve));
3829
+ return this.db !== null;
3830
+ };
3831
+ if (large && !await turn())
3832
+ return;
3833
+ const roots = this.listConversationRootIds();
3834
+ for (let i = 0; i < roots.length; i++) {
3835
+ if (large && i > 0 && i % WARM_IDS_PER_SLICE === 0 && !await turn())
3836
+ return;
3837
+ for (const id of this.listFamilyRunIds(roots[i]))
3838
+ ids.add(id);
3839
+ }
3840
+ if (large && !await turn())
3841
+ return;
3842
+ // Re-list after the yields: a run deleted meanwhile must not be registered again.
3843
+ const present = this.listRunIds();
3844
+ if (large) {
3845
+ const live = new Set(present);
3846
+ for (const id of ids)
3847
+ if (!live.has(id))
3848
+ ids.delete(id);
3849
+ }
3850
+ for (const id of present)
3851
+ ids.add(id);
3852
+ await this.facts.warm(ids);
3366
3853
  }).catch(() => undefined);
3367
3854
  }
3368
3855
  /** Resolves when the open-time warm-up is done (tests). */
@@ -3379,18 +3866,6 @@ export class RunStore extends EventEmitter {
3379
3866
  return [];
3380
3867
  }
3381
3868
  }
3382
- /** The owned store's `refreshHumanAskSummary`, from the index, on the same rules: no history
3383
- * (legacy runs) keeps the summary, an unreadable one requests attention. True when it changed. */
3384
- syncHumanAskSummary(run) {
3385
- const facts = this.facts.get(run.id);
3386
- const next = !facts ? true
3387
- : facts.bytes > 0 || facts.archive !== undefined ? facts.pendingAsk !== undefined
3388
- : run.hasPendingHumanAsk === true;
3389
- if (next === run.hasPendingHumanAsk)
3390
- return false;
3391
- run.hasPendingHumanAsk = next;
3392
- return true;
3393
- }
3394
3869
  readEvents(runId) {
3395
3870
  try {
3396
3871
  const raw = readHistoryText(this.dataDir, runId);
@@ -3498,6 +3973,8 @@ export class RunStore extends EventEmitter {
3498
3973
  }
3499
3974
  /** `workerResourcesSafe` with the refusal's live PIDs (empty when no PID explains it). */
3500
3975
  workerResourceHolders(id, generation, resourceId, opts = {}) {
3976
+ if (autosaveCleanupBlocker(this.dataDir, id))
3977
+ return [];
3501
3978
  const run = this.peek(id);
3502
3979
  const proof = this.readWorkerExecution(id);
3503
3980
  if (run?.delegation?.role !== 'worker' || run.delegation.workspace.ownerRunId !== id ||
@@ -3540,6 +4017,8 @@ export class RunStore extends EventEmitter {
3540
4017
  /** Cleanup can outlive its index row, but never its generation or terminal task intent.
3541
4018
  * `cwds` is the scratch reprobe tick's shared `/proc` snapshot (hearsay-tools/cezarion#879). */
3542
4019
  workerScratchResourcesSafe(id, generation, resourceId, cwds) {
4020
+ if (autosaveCleanupBlocker(this.dataDir, id))
4021
+ return false;
3543
4022
  const run = this.peek(id), proof = this.readWorkerExecution(id);
3544
4023
  if (proof?.phase !== 'complete' || proof.generation !== generation || proof.scratchCleanup?.resourceId !== resourceId ||
3545
4024
  (run && ['queued', 'running', 'waiting'].includes(run.status)))
@@ -3726,6 +4205,8 @@ export class RunStore extends EventEmitter {
3726
4205
  return proof?.phase === 'complete';
3727
4206
  }
3728
4207
  canDeleteRun(id) {
4208
+ if (autosaveCleanupBlocker(this.dataDir, id))
4209
+ return false;
3729
4210
  const run = this.peek(id);
3730
4211
  if (run?.delegation?.role === 'invalid')
3731
4212
  return false;
@@ -3821,6 +4302,7 @@ export class RunStore extends EventEmitter {
3821
4302
  rmSync(this.handoffPath(id), { force: true });
3822
4303
  rmSync(this.imagesDir(id), { recursive: true, force: true });
3823
4304
  removeArtifacts(this.dataDir, id);
4305
+ clearAutosaveCleanup(this.dataDir, id);
3824
4306
  }
3825
4307
  deleteRun(id) {
3826
4308
  if (!this.canDeleteRun(id))
@@ -3830,6 +4312,8 @@ export class RunStore extends EventEmitter {
3830
4312
  return false;
3831
4313
  if (run?.delegation?.role === 'worker' || run?.delegation?.role === 'root')
3832
4314
  return this.deleteDelegatedRun(id);
4315
+ if (run)
4316
+ clearAutosaveCleanup(this.dataDir, id);
3833
4317
  const existed = run !== undefined;
3834
4318
  this.held.delete(id);
3835
4319
  if (existed) {
@@ -3891,6 +4375,7 @@ export class RunStore extends EventEmitter {
3891
4375
  /** The held record is gone; the next save or commit deletes its row, fenced by `family`'s claim
3892
4376
  * and by the revision this store last saw (kept in `base` until the delete is written). */
3893
4377
  markDeleted(id, family) {
4378
+ this.liveItems.forget(id);
3894
4379
  const read = this.coldBase.get(id);
3895
4380
  if (!this.base.has(id) && read)
3896
4381
  this.base.set(id, storedRow(read));
@@ -3989,7 +4474,10 @@ export class RunStore extends EventEmitter {
3989
4474
  const encode = (run) => {
3990
4475
  const encoded = encodeRawRecord(run, (this.base.get(run.id) ?? this.coldBase.get(run.id))?.extras);
3991
4476
  extras.set(run.id, encoded.extras);
3992
- upserts.push(encodeRunRow(run, encoded.data));
4477
+ const projected = { ...run };
4478
+ restoreOriginatingIssue(projected);
4479
+ upserts.push({ ...encodeRunRow(run, encoded.data),
4480
+ issueAttributionCurrent: isDeepStrictEqual(projected, run) });
3993
4481
  };
3994
4482
  for (const [id, next] of staged) {
3995
4483
  if (next)
@@ -4043,6 +4531,7 @@ export class RunStore extends EventEmitter {
4043
4531
  this.base.set(row.id, { revision: commit.revisions.get(row.id), seq: commit.seqs.get(row.id), data: row.data, extras: extras.get(row.id) });
4044
4532
  }
4045
4533
  for (const id of deletes) {
4534
+ this.liveItems.forget(id);
4046
4535
  this.base.delete(id);
4047
4536
  this.deletedFamilies.delete(id);
4048
4537
  this.removeOwedHistory(id);
@@ -4068,23 +4557,9 @@ export class RunStore extends EventEmitter {
4068
4557
  return { owner: this.owner, claims, rows };
4069
4558
  }
4070
4559
  /**
4071
- * A fenced write found rows that changed under this store (#779, plan step 3); the write itself
4072
- * has already failed with nothing changed, and its caller sees that error.
4073
- *
4074
- * A conflict is UNEXPECTED when this store owned the row's family when it last read or wrote it:
4075
- * a held or pending row, or one read inside a family it claimed. Another writer then broke the
4076
- * claim. For each such row the original (what this store last saw), local (what it meant to
4077
- * write, or that it meant to delete it) and current versions go to `run_conflicts` first. Only
4078
- * once that evidence is stored does the row leave the pending set, so no retry rewrites it; this
4079
- * store then refuses to write it again, and its held copy is replaced by the current row (or
4080
- * dropped when deleted) and announced. That refusal lasts as long as this store: the explicit
4081
- * recovery is a restart, which reads the row as it now stands, and the evidence row stays for a
4082
- * person to compare. Other pending rows stay pending and the next save writes them.
4083
- *
4084
- * If the evidence cannot be stored, nothing changes: the rows stay pending, and the next write
4085
- * meets the same conflict and tries again. A row read before its family was claimed may simply
4086
- * have been finished by its previous owner in between: a stale read, not a conflict, so only the
4087
- * write fails.
4560
+ * Handle a fenced-write conflict. Save unexpected conflict evidence before clearing pending
4561
+ * state or quarantining the row. If evidence persistence fails, retain pending writes. Other
4562
+ * rows remain writable; stale unclaimed reads only fail their write.
4088
4563
  */
4089
4564
  handleConflicts(error, staged) {
4090
4565
  const unexpected = error.conflicts.filter((conflict) => this.held.has(conflict.id) || this.deleted.has(conflict.id) || this.coldBase.get(conflict.id)?.owned === true);
@@ -4111,6 +4586,7 @@ export class RunStore extends EventEmitter {
4111
4586
  }
4112
4587
  unexpected.forEach((conflict, index) => {
4113
4588
  const family = this.familyOf(conflict.id);
4589
+ this.liveItems.forget(conflict.id);
4114
4590
  this.quarantined.add(conflict.id);
4115
4591
  this.dirty.delete(conflict.id);
4116
4592
  this.deleted.delete(conflict.id);
@@ -4174,6 +4650,9 @@ export class RunStore extends EventEmitter {
4174
4650
  * absent. It saves nothing more, and a durable commit on it throws.
4175
4651
  */
4176
4652
  close() {
4653
+ this.liveItems.clear();
4654
+ // Demand-bound feeds stop before storage disappears during project removal/shutdown.
4655
+ this.emit('closed');
4177
4656
  this.compressor.stop();
4178
4657
  this.facts.stop();
4179
4658
  this.facts.flush();
@@ -4198,4 +4677,17 @@ export class RunStore extends EventEmitter {
4198
4677
  this.db = null;
4199
4678
  }
4200
4679
  }
4680
+ function archiveDigest(value) {
4681
+ return createHash('sha256').update(JSON.stringify(value)).digest('hex');
4682
+ }
4683
+ function archiveFields(run, archived, now) {
4684
+ run.archived = archived;
4685
+ run.archivedAt = archived ? now : undefined;
4686
+ if (archived) {
4687
+ clearPendingAutoResume(run);
4688
+ clearPin(run);
4689
+ }
4690
+ else
4691
+ run.manualUnarchivedAt = now;
4692
+ }
4201
4693
  //# sourceMappingURL=store.js.map