@open-mercato/cezar 0.9.3 → 0.10.0-pr892.1298

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 (253) hide show
  1. package/README.md +165 -36
  2. package/dist/agent-config/account-identity.d.ts +44 -0
  3. package/dist/agent-config/account-identity.js +128 -0
  4. package/dist/agent-config/account-identity.js.map +1 -0
  5. package/dist/agent-config/catalog.d.ts +8 -0
  6. package/dist/agent-config/catalog.js +18 -0
  7. package/dist/agent-config/catalog.js.map +1 -1
  8. package/dist/agent-config/model-settings/claude.d.ts +2 -0
  9. package/dist/agent-config/model-settings/claude.js +11 -0
  10. package/dist/agent-config/model-settings/claude.js.map +1 -0
  11. package/dist/agent-config/model-settings/codex.d.ts +2 -0
  12. package/dist/agent-config/model-settings/codex.js +16 -0
  13. package/dist/agent-config/model-settings/codex.js.map +1 -0
  14. package/dist/agent-config/model-settings/opencode.d.ts +2 -0
  15. package/dist/agent-config/model-settings/opencode.js +8 -0
  16. package/dist/agent-config/model-settings/opencode.js.map +1 -0
  17. package/dist/agent-config/model-settings/pi.d.ts +12 -0
  18. package/dist/agent-config/model-settings/pi.js +18 -0
  19. package/dist/agent-config/model-settings/pi.js.map +1 -0
  20. package/dist/agent-config/model-settings/shared.d.ts +9 -0
  21. package/dist/agent-config/model-settings/shared.js +93 -0
  22. package/dist/agent-config/model-settings/shared.js.map +1 -0
  23. package/dist/agent-config/model-settings/types.d.ts +9 -0
  24. package/dist/agent-config/model-settings/types.js +2 -0
  25. package/dist/agent-config/model-settings/types.js.map +1 -0
  26. package/dist/agent-config/models.d.ts +9 -0
  27. package/dist/agent-config/models.js +32 -0
  28. package/dist/agent-config/models.js.map +1 -0
  29. package/dist/agent-config/service.js +4 -4
  30. package/dist/agent-config/service.js.map +1 -1
  31. package/dist/automations/types.d.ts +4 -0
  32. package/dist/automations/types.js +2 -1
  33. package/dist/automations/types.js.map +1 -1
  34. package/dist/config.d.ts +10 -1
  35. package/dist/config.js +48 -5
  36. package/dist/config.js.map +1 -1
  37. package/dist/contract/agent-config.d.ts +2 -0
  38. package/dist/contract/agent-profiles.d.ts +347 -0
  39. package/dist/contract/automations.d.ts +24 -0
  40. package/dist/contract/events.d.ts +59 -0
  41. package/dist/contract/github.d.ts +88 -0
  42. package/dist/contract/health.d.ts +5 -1
  43. package/dist/contract/index.d.ts +1 -0
  44. package/dist/contract/index.js +981 -544
  45. package/dist/contract/projects.d.ts +33 -5
  46. package/dist/contract/runs.d.ts +247 -0
  47. package/dist/contract/skills.d.ts +7 -0
  48. package/dist/contract/workflows.d.ts +6 -0
  49. package/dist/contract/workspace.d.ts +137 -2
  50. package/dist/core/agent-env.js +41 -16
  51. package/dist/core/agent-env.js.map +1 -1
  52. package/dist/core/agent-model-policy.d.ts +10 -0
  53. package/dist/core/agent-model-policy.js +36 -0
  54. package/dist/core/agent-model-policy.js.map +1 -0
  55. package/dist/core/agent-profiles.d.ts +62 -0
  56. package/dist/core/agent-profiles.js +90 -0
  57. package/dist/core/agent-profiles.js.map +1 -0
  58. package/dist/core/agent-runner.d.ts +37 -6
  59. package/dist/core/agent-runner.js +35 -3
  60. package/dist/core/agent-runner.js.map +1 -1
  61. package/dist/core/backend-detect.d.ts +5 -5
  62. package/dist/core/backend-detect.js +44 -7
  63. package/dist/core/backend-detect.js.map +1 -1
  64. package/dist/core/claude-cli-runner.js +10 -5
  65. package/dist/core/claude-cli-runner.js.map +1 -1
  66. package/dist/core/codex-app-server-runner.js +13 -3
  67. package/dist/core/codex-app-server-runner.js.map +1 -1
  68. package/dist/core/codex-app-server-transport.js +7 -2
  69. package/dist/core/codex-app-server-transport.js.map +1 -1
  70. package/dist/core/model-identity.d.ts +23 -11
  71. package/dist/core/model-identity.js +41 -24
  72. package/dist/core/model-identity.js.map +1 -1
  73. package/dist/core/model-presets.d.ts +8 -2
  74. package/dist/core/model-presets.js +51 -8
  75. package/dist/core/model-presets.js.map +1 -1
  76. package/dist/core/opencode-model-catalog.d.ts +32 -0
  77. package/dist/core/opencode-model-catalog.js +153 -0
  78. package/dist/core/opencode-model-catalog.js.map +1 -0
  79. package/dist/core/opencode-server-runner.d.ts +2 -0
  80. package/dist/core/opencode-server-runner.js +42 -11
  81. package/dist/core/opencode-server-runner.js.map +1 -1
  82. package/dist/core/pi-runner.d.ts +24 -0
  83. package/dist/core/pi-runner.js +349 -0
  84. package/dist/core/pi-runner.js.map +1 -0
  85. package/dist/core/pi-ui-mapper.d.ts +34 -0
  86. package/dist/core/pi-ui-mapper.js +270 -0
  87. package/dist/core/pi-ui-mapper.js.map +1 -0
  88. package/dist/core/provider-auth.d.ts +95 -3
  89. package/dist/core/provider-auth.js +234 -15
  90. package/dist/core/provider-auth.js.map +1 -1
  91. package/dist/core/provider-availability.d.ts +1 -1
  92. package/dist/core/provider-availability.js +2 -1
  93. package/dist/core/provider-availability.js.map +1 -1
  94. package/dist/core/runner-factory.js +3 -0
  95. package/dist/core/runner-factory.js.map +1 -1
  96. package/dist/core/shell-env.d.ts +28 -0
  97. package/dist/core/shell-env.js +56 -0
  98. package/dist/core/shell-env.js.map +1 -0
  99. package/dist/core/ui-events.d.ts +1 -1
  100. package/dist/core/usage-limit.d.ts +42 -0
  101. package/dist/core/usage-limit.js +209 -0
  102. package/dist/core/usage-limit.js.map +1 -0
  103. package/dist/index.js +3 -2
  104. package/dist/index.js.map +1 -1
  105. package/dist/paths.d.ts +53 -5
  106. package/dist/paths.js +63 -9
  107. package/dist/paths.js.map +1 -1
  108. package/dist/planner.js +6 -0
  109. package/dist/planner.js.map +1 -1
  110. package/dist/release/snapshot.d.ts +13 -2
  111. package/dist/release/snapshot.js +25 -1
  112. package/dist/release/snapshot.js.map +1 -1
  113. package/dist/runs/agent-tmpdir.d.ts +41 -0
  114. package/dist/runs/agent-tmpdir.js +183 -0
  115. package/dist/runs/agent-tmpdir.js.map +1 -0
  116. package/dist/runs/auto-name.js +6 -1
  117. package/dist/runs/auto-name.js.map +1 -1
  118. package/dist/runs/event-history.d.ts +50 -0
  119. package/dist/runs/event-history.js +613 -0
  120. package/dist/runs/event-history.js.map +1 -0
  121. package/dist/runs/run-index.d.ts +19 -0
  122. package/dist/runs/run-index.js +43 -0
  123. package/dist/runs/run-index.js.map +1 -0
  124. package/dist/runs/store.d.ts +78 -19
  125. package/dist/runs/store.js +175 -43
  126. package/dist/runs/store.js.map +1 -1
  127. package/dist/server/forge/github.d.ts +169 -0
  128. package/dist/server/forge/github.js +511 -0
  129. package/dist/server/forge/github.js.map +1 -1
  130. package/dist/server/forge/index.d.ts +9 -0
  131. package/dist/server/forge/index.js +14 -0
  132. package/dist/server/forge/index.js.map +1 -1
  133. package/dist/server/github.d.ts +2 -2
  134. package/dist/server/github.js +1 -1
  135. package/dist/server/github.js.map +1 -1
  136. package/dist/server/open-in-app.d.ts +2 -1
  137. package/dist/server/open-in-app.js +3 -1
  138. package/dist/server/open-in-app.js.map +1 -1
  139. package/dist/server/open-in-terminal.d.ts +46 -1
  140. package/dist/server/open-in-terminal.js +72 -13
  141. package/dist/server/open-in-terminal.js.map +1 -1
  142. package/dist/server/provider-action-gate.js +3 -1
  143. package/dist/server/provider-action-gate.js.map +1 -1
  144. package/dist/server/server.d.ts +2199 -428
  145. package/dist/server/server.js +1171 -139
  146. package/dist/server/server.js.map +1 -1
  147. package/dist/server-install/engine.js +5 -18
  148. package/dist/server-install/engine.js.map +1 -1
  149. package/dist/server-install/steps.js +6 -2
  150. package/dist/server-install/steps.js.map +1 -1
  151. package/dist/skills.js +4 -0
  152. package/dist/skills.js.map +1 -1
  153. package/dist/workflows/run.d.ts +202 -2
  154. package/dist/workflows/run.js +782 -115
  155. package/dist/workflows/run.js.map +1 -1
  156. package/dist/workflows/types.d.ts +3 -0
  157. package/dist/workflows/types.js +11 -2
  158. package/dist/workflows/types.js.map +1 -1
  159. package/dist/workspace/agent-accounts.d.ts +153 -0
  160. package/dist/workspace/agent-accounts.js +304 -0
  161. package/dist/workspace/agent-accounts.js.map +1 -0
  162. package/dist/workspace/agent-profiles.d.ts +78 -0
  163. package/dist/workspace/agent-profiles.js +115 -0
  164. package/dist/workspace/agent-profiles.js.map +1 -0
  165. package/dist/workspace/config.d.ts +19 -1
  166. package/dist/workspace/config.js +56 -2
  167. package/dist/workspace/config.js.map +1 -1
  168. package/dist/workspace/projects-cli.js +54 -4
  169. package/dist/workspace/projects-cli.js.map +1 -1
  170. package/dist/workspace/projects.d.ts +19 -1
  171. package/dist/workspace/projects.js +34 -1
  172. package/dist/workspace/projects.js.map +1 -1
  173. package/dist/workspace/semaphore.d.ts +39 -0
  174. package/dist/workspace/semaphore.js +31 -0
  175. package/dist/workspace/semaphore.js.map +1 -1
  176. package/package.json +3 -3
  177. package/scripts/mock-claude.mjs +20 -0
  178. package/scripts/mock-pi-rpc.mjs +83 -0
  179. package/web/dist/assets/{alert-dialog-V9cigyAO.js → alert-dialog-Byp42_IG.js} +1 -1
  180. package/web/dist/assets/arrow-down-CGDbDYKD.js +1 -0
  181. package/web/dist/assets/arrow-left-ESCjr1Hq.js +1 -0
  182. package/web/dist/assets/centered-state-LfAW-igm.js +43 -0
  183. package/web/dist/assets/chevron-right-Du-P1IUd.js +1 -0
  184. package/web/dist/assets/{collapsible-BlF-7CeC.js → collapsible-CBiiqDJM.js} +1 -1
  185. package/web/dist/assets/{commit-list-D2mKElUx.js → commit-list-6PPve7PE.js} +1 -1
  186. package/web/dist/assets/{compare-variants-CFBY16Rc.js → compare-variants-BDnp6mXJ.js} +1 -1
  187. package/web/dist/assets/{diff-DE6u5dTe.js → diff-_jK0OWpF.js} +2 -2
  188. package/web/dist/assets/{diff-stat-B1j6L7C6.js → diff-stat-CIrTu4Ym.js} +1 -1
  189. package/web/dist/assets/{diff-view-Gb2kk3Le.js → diff-view-COA_5IyY.js} +1 -1
  190. package/web/dist/assets/dropdown-menu-BofYpFFw.js +1 -0
  191. package/web/dist/assets/editable-title-_J0zFDl-.js +1 -0
  192. package/web/dist/assets/{ellipsis-vertical-BbxtXb6s.js → ellipsis-vertical-BeMR-l62.js} +1 -1
  193. package/web/dist/assets/{file-j5O4NryU.js → file-DY996Pee.js} +1 -1
  194. package/web/dist/assets/{folder-w9Lwj9x9.js → folder-NO3Ec-TY.js} +1 -1
  195. package/web/dist/assets/git-toolbar-BqJeUWcD.js +1 -0
  196. package/web/dist/assets/github-C96HPV9E.js +1 -0
  197. package/web/dist/assets/{image-preview-CcFdkZXW.js → image-preview-D9cCDfKQ.js} +1 -1
  198. package/web/dist/assets/index-Bx-grqa9.css +2 -0
  199. package/web/dist/assets/index-DoVvGDfW.js +7 -0
  200. package/web/dist/assets/{markdown-q9lm7kQ2.js → markdown-DDzk11zj.js} +1 -1
  201. package/web/dist/assets/new-task-form-BNLLgC6P.js +1 -0
  202. package/web/dist/assets/pill-CVM1EmV4.js +1 -0
  203. package/web/dist/assets/{project-router-DudOmkHf.js → project-router-LRCtP48W.js} +1 -1
  204. package/web/dist/assets/prompt-templates-DFOvYgtW.js +15 -0
  205. package/web/dist/assets/{refresh-cw-_C2EGJK5.js → refresh-cw-D1ys6QY2.js} +1 -1
  206. package/web/dist/assets/repo-git-CzUnsgWY.js +1 -0
  207. package/web/dist/assets/{run-diff-BLoKEnTx.js → run-diff-BlnqwfSS.js} +2 -2
  208. package/web/dist/assets/run-header-Bn7cN0Z1.js +1 -0
  209. package/web/dist/assets/{search-x-BgxaJgro.js → search-x-D6FSpxId.js} +1 -1
  210. package/web/dist/assets/{skill-empty-hint-D5MOmycv.js → skill-empty-hint-iwpCcqgI.js} +1 -1
  211. package/web/dist/assets/skills-CezbOaol.js +1 -0
  212. package/web/dist/assets/{sparkles-ANSka5bH.js → sparkles-MrQrgCI9.js} +1 -1
  213. package/web/dist/assets/{square-terminal-qojq20cH.js → square-terminal-TTH64lO7.js} +1 -1
  214. package/web/dist/assets/tab-link-FYksz037.js +1 -0
  215. package/web/dist/assets/task-changes-ClWlnzBL.js +1 -0
  216. package/web/dist/assets/{task-commits-yXmDrCoE.js → task-commits-Crbrl86b.js} +1 -1
  217. package/web/dist/assets/{task-files-BDRmCCPN.js → task-files-uSCWyD0-.js} +2 -2
  218. package/web/dist/assets/task-thread-DPmamBA1.js +9 -0
  219. package/web/dist/assets/{textarea-DVO7pg8Y.js → textarea-B9Ahj4E4.js} +1 -1
  220. package/web/dist/assets/thread-loading-BH_ypQoy.js +1 -0
  221. package/web/dist/assets/{trash-2-DKcnmg6e.js → trash-2-D8S3V3fu.js} +1 -1
  222. package/web/dist/assets/{triangle-alert-CPUEVSZ3.js → triangle-alert-bB7p9n5E.js} +1 -1
  223. package/web/dist/assets/{upload-D5Jrq7QK.js → upload-Bd5oRLPV.js} +1 -1
  224. package/web/dist/assets/{use-desktop-CXae6uyk.js → use-desktop-BNvbRBeZ.js} +1 -1
  225. package/web/dist/assets/{use-submit-shortcut-RKJLKdsW.js → use-submit-shortcut-B-xhu4Bf.js} +1 -1
  226. package/web/dist/assets/utils-BhpbVMwN.js +64 -0
  227. package/web/dist/assets/{workflows-CGyTdyds.js → workflows-RmfbM9Vy.js} +3 -3
  228. package/web/dist/assets/{zoomable-image-hcqwUIlQ.js → zoomable-image-BCW6ZBV0.js} +1 -1
  229. package/web/dist/index.html +24 -23
  230. package/web/dist/assets/arrow-down-DflHdNlN.js +0 -1
  231. package/web/dist/assets/arrow-left-IIL4uR-C.js +0 -1
  232. package/web/dist/assets/centered-state-BkAJVLJE.js +0 -43
  233. package/web/dist/assets/chevron-right-CxAoD1eD.js +0 -1
  234. package/web/dist/assets/circle-check-CdwcIbkx.js +0 -1
  235. package/web/dist/assets/circle-x-Cnc43xJ2.js +0 -1
  236. package/web/dist/assets/dropdown-menu-Ns-rY6sm.js +0 -1
  237. package/web/dist/assets/editable-title-BGuiDxFT.js +0 -1
  238. package/web/dist/assets/git-pull-request-DlSop3Ch.js +0 -1
  239. package/web/dist/assets/git-toolbar-BdJ8XSxI.js +0 -1
  240. package/web/dist/assets/github-CCLn2OAU.js +0 -1
  241. package/web/dist/assets/index-BmHHLbwc.js +0 -6
  242. package/web/dist/assets/index-u8OAnq0r.css +0 -2
  243. package/web/dist/assets/new-task-form-BuYxAWN7.js +0 -1
  244. package/web/dist/assets/pill-t97MvPW_.js +0 -1
  245. package/web/dist/assets/prompt-templates-C5iO3Lda.js +0 -15
  246. package/web/dist/assets/repo-git-DqQzj33d.js +0 -1
  247. package/web/dist/assets/run-header-Dho4eurS.js +0 -1
  248. package/web/dist/assets/skills-B5g064CC.js +0 -1
  249. package/web/dist/assets/tab-link-CIcwFJtC.js +0 -1
  250. package/web/dist/assets/task-changes-yRfsEsnh.js +0 -1
  251. package/web/dist/assets/task-thread-C9AnIz4Z.js +0 -9
  252. package/web/dist/assets/thread-loading-DJDl701Z.js +0 -1
  253. package/web/dist/assets/utils-Wtkd5E6j.js +0 -64
@@ -0,0 +1,19 @@
1
+ import { type RunRecord } from './store.ts';
2
+ /**
3
+ * The READ-ONLY half of `runs.json`, for the workspace-level run index (`GET
4
+ * /workspace/runs-index`) — the one place that must read a project's runs WITHOUT owning it.
5
+ *
6
+ * `RunStore.open` cannot be used here and the reason is the whole point of this module. Opening a
7
+ * store `mkdir`s `<dataDir>/runs/`, and the caller that opens one goes on to build a
8
+ * `ProjectContext` — which prunes orphan worktrees and calls `manager.recover()`, resuming
9
+ * interrupted runs. Building the workspace index must never do any of that: answering "which
10
+ * tasks exist" would restart agents across every registered project, and typing into a search box
11
+ * would spend tokens. Cold projects are precisely the ones the index exists to reach, so
12
+ * `contexts.peek()` (which returns nothing for them) is not an answer either.
13
+ *
14
+ * What this does share with the store is the schema and `reconcileLoadedRun`, so a `running` row
15
+ * left behind by a crashed process reads as interrupted here exactly as it would once the project
16
+ * were opened for real. A second, subtly different parse of that field would show a task as
17
+ * running in the palette and failed the moment you clicked it.
18
+ */
19
+ export declare function readRunIndexFromDisk(dataDir: string): RunRecord[];
@@ -0,0 +1,43 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { z } from 'zod';
4
+ import { reconcileLoadedRun, runRecordSchema } from './store.js';
5
+ /**
6
+ * The READ-ONLY half of `runs.json`, for the workspace-level run index (`GET
7
+ * /workspace/runs-index`) — the one place that must read a project's runs WITHOUT owning it.
8
+ *
9
+ * `RunStore.open` cannot be used here and the reason is the whole point of this module. Opening a
10
+ * store `mkdir`s `<dataDir>/runs/`, and the caller that opens one goes on to build a
11
+ * `ProjectContext` — which prunes orphan worktrees and calls `manager.recover()`, resuming
12
+ * interrupted runs. Building the workspace index must never do any of that: answering "which
13
+ * tasks exist" would restart agents across every registered project, and typing into a search box
14
+ * would spend tokens. Cold projects are precisely the ones the index exists to reach, so
15
+ * `contexts.peek()` (which returns nothing for them) is not an answer either.
16
+ *
17
+ * What this does share with the store is the schema and `reconcileLoadedRun`, so a `running` row
18
+ * left behind by a crashed process reads as interrupted here exactly as it would once the project
19
+ * were opened for real. A second, subtly different parse of that field would show a task as
20
+ * running in the palette and failed the moment you clicked it.
21
+ */
22
+ export function readRunIndexFromDisk(dataDir) {
23
+ const indexPath = join(dataDir, 'runs.json');
24
+ if (!existsSync(indexPath))
25
+ return [];
26
+ try {
27
+ const raw = JSON.parse(readFileSync(indexPath, 'utf8'));
28
+ const parsed = z.array(runRecordSchema).safeParse(raw);
29
+ if (!parsed.success)
30
+ return [];
31
+ // `reconcileLoadedRun` mutates, which is safe here in a way it is not in the store: these
32
+ // records were just parsed into fresh objects that nothing else holds a reference to.
33
+ // Never `keepLive` — this reader has no RunManager, so there is nothing to recover into.
34
+ return parsed.data.map((run) => reconcileLoadedRun(run));
35
+ }
36
+ catch {
37
+ // Corrupt or unreadable index. A project that cannot be read contributes nothing to the
38
+ // index rather than failing the whole workspace's search — the same degrade-quietly rule
39
+ // the rest of the registry follows.
40
+ return [];
41
+ }
42
+ }
43
+ //# sourceMappingURL=run-index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-index.js","sourceRoot":"","sources":["../../src/runs/run-index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAkB,MAAM,YAAY,CAAC;AAEjF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAAe;IAClD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;IAC7C,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,EAAE,CAAC;IACtC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;QACxD,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QACvD,IAAI,CAAC,MAAM,CAAC,OAAO;YAAE,OAAO,EAAE,CAAC;QAC/B,0FAA0F;QAC1F,sFAAsF;QACtF,yFAAyF;QACzF,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3D,CAAC;IAAC,MAAM,CAAC;QACP,wFAAwF;QACxF,yFAAyF;QACzF,oCAAoC;QACpC,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC"}
@@ -1,5 +1,6 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import { z } from 'zod';
3
+ import type { RunnerId } from '../core/agent-runner.ts';
3
4
  export type RunStatus = 'queued' | 'running' | 'waiting' | 'review' | 'done' | 'failed' | 'cancelled';
4
5
  /**
5
6
  * A sub-state of `running` (spec 2026-07-18-subagent-monitoring-status, #490):
@@ -40,11 +41,14 @@ declare const stepStateSchema: z.ZodObject<{
40
41
  finishedAt: z.ZodOptional<z.ZodString>;
41
42
  error: z.ZodOptional<z.ZodString>;
42
43
  sessionId: z.ZodOptional<z.ZodString>;
43
- backend: z.ZodOptional<z.ZodEnum<{
44
+ backend: z.ZodOptional<z.ZodPipe<z.ZodEnum<{
44
45
  claude: "claude";
46
+ "claude-cli": "claude-cli";
45
47
  codex: "codex";
46
48
  opencode: "opencode";
47
- }>>;
49
+ pi: "pi";
50
+ }>, z.ZodTransform<"claude" | "codex" | "opencode" | "pi", "claude" | "claude-cli" | "codex" | "opencode" | "pi">>>;
51
+ profileId: z.ZodOptional<z.ZodString>;
48
52
  costUsd: z.ZodOptional<z.ZodNumber>;
49
53
  }, z.core.$strip>;
50
54
  /** One prompt message stacked onto a run while it waits for a free agent slot
@@ -57,7 +61,9 @@ declare const queuedMessageSchema: z.ZodObject<{
57
61
  images: z.ZodOptional<z.ZodArray<z.ZodString>>;
58
62
  createdAt: z.ZodString;
59
63
  }, z.core.$strip>;
60
- declare const runRecordSchema: z.ZodObject<{
64
+ /** Exported for `./run-index.ts`, the read-only reader of the same file. Nothing else should
65
+ * parse `runs.json` — see `reconcileLoadedRun` for why a second parser is a correctness risk. */
66
+ export declare const runRecordSchema: z.ZodObject<{
61
67
  id: z.ZodString;
62
68
  title: z.ZodString;
63
69
  titleSummary: z.ZodOptional<z.ZodString>;
@@ -78,11 +84,14 @@ declare const runRecordSchema: z.ZodObject<{
78
84
  taskImages: z.ZodOptional<z.ZodArray<z.ZodString>>;
79
85
  model: z.ZodOptional<z.ZodString>;
80
86
  modelIdentity: z.ZodOptional<z.ZodString>;
81
- runner: z.ZodOptional<z.ZodEnum<{
87
+ runner: z.ZodOptional<z.ZodPipe<z.ZodEnum<{
82
88
  claude: "claude";
89
+ "claude-cli": "claude-cli";
83
90
  codex: "codex";
84
91
  opencode: "opencode";
85
- }>>;
92
+ pi: "pi";
93
+ }>, z.ZodTransform<"claude" | "codex" | "opencode" | "pi", "claude" | "claude-cli" | "codex" | "opencode" | "pi">>>;
94
+ agentProfile: z.ZodOptional<z.ZodString>;
86
95
  systemPrompt: z.ZodOptional<z.ZodString>;
87
96
  generateFollowups: z.ZodOptional<z.ZodBoolean>;
88
97
  autonomous: z.ZodOptional<z.ZodBoolean>;
@@ -107,6 +116,8 @@ declare const runRecordSchema: z.ZodObject<{
107
116
  }>>;
108
117
  monitoringWakeAt: z.ZodCatch<z.ZodOptional<z.ZodString>>;
109
118
  monitoringWakeCapReached: z.ZodOptional<z.ZodBoolean>;
119
+ autoResumeAt: z.ZodCatch<z.ZodOptional<z.ZodString>>;
120
+ autoResumeAttempts: z.ZodCatch<z.ZodOptional<z.ZodNumber>>;
110
121
  createdAt: z.ZodString;
111
122
  startedAt: z.ZodOptional<z.ZodString>;
112
123
  finishedAt: z.ZodOptional<z.ZodString>;
@@ -175,11 +186,14 @@ declare const runRecordSchema: z.ZodObject<{
175
186
  finishedAt: z.ZodOptional<z.ZodString>;
176
187
  error: z.ZodOptional<z.ZodString>;
177
188
  sessionId: z.ZodOptional<z.ZodString>;
178
- backend: z.ZodOptional<z.ZodEnum<{
189
+ backend: z.ZodOptional<z.ZodPipe<z.ZodEnum<{
179
190
  claude: "claude";
191
+ "claude-cli": "claude-cli";
180
192
  codex: "codex";
181
193
  opencode: "opencode";
182
- }>>;
194
+ pi: "pi";
195
+ }>, z.ZodTransform<"claude" | "codex" | "opencode" | "pi", "claude" | "claude-cli" | "codex" | "opencode" | "pi">>>;
196
+ profileId: z.ZodOptional<z.ZodString>;
183
197
  costUsd: z.ZodOptional<z.ZodNumber>;
184
198
  }, z.core.$strip>>;
185
199
  workflowDef: z.ZodCatch<z.ZodOptional<z.ZodObject<{
@@ -195,6 +209,7 @@ declare const runRecordSchema: z.ZodObject<{
195
209
  claude: "claude";
196
210
  codex: "codex";
197
211
  opencode: "opencode";
212
+ pi: "pi";
198
213
  }>>;
199
214
  allowedTools: z.ZodOptional<z.ZodArray<z.ZodString>>;
200
215
  bashAllowlist: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -222,6 +237,22 @@ export interface RunEvent {
222
237
  type: string;
223
238
  [key: string]: unknown;
224
239
  }
240
+ /**
241
+ * Reconcile one record just read off disk with the fact that whichever process wrote it is gone.
242
+ *
243
+ * Mutates and returns `run`. Extracted from `RunStore.open` so the read-only index reader
244
+ * (`./run-index.ts`) answers the SAME question about a `running` row on disk. Two parsers that
245
+ * disagree here is a visible bug, not an internal one: the cockpit would show a task as running
246
+ * in the ⌘K index and failed the moment you opened it.
247
+ *
248
+ * `keepLive` (#367): leave `queued`/`running`/`waiting` untouched so the caller can recover them
249
+ * (RunManager.recover re-queues queued runs, resumes interrupted ones). Without it — one-shot CLI
250
+ * paths that never recover, and the index reader, which has no manager at all — live-looking runs
251
+ * are marked failed so no ghost stays behind.
252
+ */
253
+ export declare function reconcileLoadedRun(run: RunRecord, opts?: {
254
+ keepLive?: boolean;
255
+ }): RunRecord;
225
256
  /**
226
257
  * File-backed run store: `runs.json` index (atomic tmp+rename writes, the
227
258
  * pattern from @cezar/core's IssueStore) plus one append-only NDJSON event
@@ -233,12 +264,7 @@ export declare class RunStore extends EventEmitter {
233
264
  private runs;
234
265
  private saveTimer;
235
266
  private constructor();
236
- /**
237
- * `keepLive` (#367): leave `queued`/`running`/`waiting` statuses untouched
238
- * so the caller can recover them (RunManager.recover re-queues queued runs,
239
- * resumes interrupted ones). Without it — one-shot CLI paths that never
240
- * recover — live-looking runs are marked failed so no ghost stays behind.
241
- */
267
+ /** See `reconcileLoadedRun` for what `keepLive` (#367) decides about live-looking rows. */
242
268
  static open(dataDir: string, opts?: {
243
269
  keepLive?: boolean;
244
270
  }): RunStore;
@@ -249,7 +275,9 @@ export declare class RunStore extends EventEmitter {
249
275
  workflow: string;
250
276
  task: string;
251
277
  model?: string;
252
- runner?: 'claude' | 'codex' | 'opencode';
278
+ runner?: RunnerId;
279
+ /** Composer's per-task agent account (spec 2026-07-29-agent-profiles). */
280
+ agentProfile?: string;
253
281
  generateFollowups?: boolean;
254
282
  autonomous?: boolean;
255
283
  worktree?: false;
@@ -292,13 +320,44 @@ export declare class RunStore extends EventEmitter {
292
320
  * updated record rides the existing `run` SSE with no new event. Idempotent by
293
321
  * design: opening an already-read thread just re-stamps a later `seenAt`. */
294
322
  setRead(id: string): RunRecord | undefined;
323
+ /** Mark one run as UNread (#775): drop the read receipt so the run rejoins the unread
324
+ * list. The inverse of `setRead` and, like it, `touch`es so the updated record rides the
325
+ * existing `run` SSE.
326
+ *
327
+ * Deleting the field rather than adding a "manually unread" flag is the whole point:
328
+ * absent `seenAt` is ALREADY what every reader treats as unread (`isUnread` in the
329
+ * cockpit's read-state.ts, and `markAllRead`'s clause-for-clause copy of it below), so
330
+ * clearing needs no new state and writes a shape any older cezar already parses.
331
+ *
332
+ * Deliberately unconditional: clearing a receipt is always a legal write, so this
333
+ * succeeds for an already-unread run (idempotent) and for statuses that can never wear
334
+ * the marker. WHETHER the action means anything for a given run is UI policy, and lives
335
+ * in the cockpit's `runActionFlags` — the same split the rest of the store keeps. */
336
+ setUnread(id: string): RunRecord | undefined;
295
337
  /** Bulk mark-read: stamp every currently-unread finished run; returns the count.
296
338
  * "Unread" here is the same rule the cockpit paints (`isUnread` in read-state.ts),
297
- * clause for clause: a `done` or `failed` run that finished and has not been seen
298
- * since. Cancelled runs are never unread — you stopped them yourself — and archived
299
- * ones never are either, since archiving is a stronger "done with this" than reading;
300
- * both are skipped, as are runs already read. Keeping the two rules identical is what
301
- * makes the returned count the number the cockpit's unread badge was showing. */
339
+ * clause for clause:
340
+ * - a `done` or `failed` run that finished and has not been seen since;
341
+ * - cancelled runs are never unread — you stopped them yourself;
342
+ * - archived ones never are either, since archiving is a stronger "done with this"
343
+ * than reading;
344
+ * - and a `failed` run with a pending `autoResumeAt` is not a done item AT ALL
345
+ * (`isScheduledResume`, spec 2026-08-03-auto-resume-after-usage-limit): it has an
346
+ * appointment to pick the work back up, so there is no outcome to have missed.
347
+ *
348
+ * Keeping the two rules identical is what makes the returned count the number the
349
+ * cockpit's unread badge was showing. The `autoResumeAt` clause is the one that drifted
350
+ * (#803): `isUnread` gained it with auto-resume and this sweep did not, so a task waiting
351
+ * out a usage limit was uncounted by the badge but stamped read by the sweep — and this
352
+ * comment asserted an invariant the code no longer held.
353
+ *
354
+ * This rule lives in two languages of the same repo, which is why it has now drifted
355
+ * once. The cockpit cannot import it (`packages/web` does not depend on the service, and
356
+ * should not), so a single definition would have to move to `packages/contract` — the one
357
+ * package both sides already import. Worth doing; deliberately not done here, because
358
+ * widening the contract package's remit from "shapes" to "behavior" is a design change
359
+ * that deserves its own review rather than riding along in a bug fix. Until then: EDIT
360
+ * BOTH, and the case-table tests on either side are what catch you if you don't. */
302
361
  markAllRead(): number;
303
362
  appendEvent(runId: string, event: {
304
363
  type: string;
@@ -6,7 +6,31 @@ import { z } from 'zod';
6
6
  import { collectSecretValues, redactDeep, redactSecrets } from '../core/secret-redaction.js';
7
7
  // Type-only module (zod + nothing else), so this cannot cycle back into the store.
8
8
  import { workflowDefSchema } from '../workflows/types.js';
9
+ import { RUNNER_IDS } from '../core/agent-runner.js';
9
10
  const usageCounterSchema = z.number().finite().nonnegative();
11
+ /**
12
+ * A runner id as it may appear in a PERSISTED record, normalized to the three
13
+ * ids the rest of cezar speaks (#547).
14
+ *
15
+ * `claude-cli` is the legacy spelling of `claude` — still a member of
16
+ * `AgentBackend` and still accepted by `createRunner`, and named by
17
+ * `BACKWARD_COMPATIBILITY.md` §3 as an id `runs.json` keeps parseable. The enum
18
+ * here did not accept it, so that promise was false: the loader `safeParse`s the
19
+ * WHOLE array, so one record carrying it would have dropped every run in the
20
+ * file — the exact failure mode §3 exists to warn about.
21
+ *
22
+ * Parse-and-fold rather than widen: the legacy id is accepted on the way in and
23
+ * collapsed to `claude`, so no consumer, wire type or contract schema ever sees
24
+ * a fourth runner. The narrowing is one-way and permanent (the index is
25
+ * re-serialized from the parsed records), which is what "old run records
26
+ * normalise identically to `claude`" in `core/model-identity.ts` has always
27
+ * claimed. Use ONLY for read-back of stored state — request bodies, settings and
28
+ * workflow step defs stay the three selectable ids (`RunnerId`), because nothing
29
+ * should be able to ASK for the legacy spelling.
30
+ */
31
+ const storedRunnerSchema = z
32
+ .enum([...RUNNER_IDS, 'claude-cli'])
33
+ .transform((id) => (id === 'claude-cli' ? 'claude' : id));
10
34
  const stepStateSchema = z.object({
11
35
  id: z.string(),
12
36
  name: z.string(),
@@ -26,8 +50,15 @@ const stepStateSchema = z.object({
26
50
  error: z.string().optional(),
27
51
  /** Latest backend-owned session id, used for same-backend Continue. */
28
52
  sessionId: z.string().optional(),
29
- /** Backend that owns `sessionId`. Optional so pre-affinity runs.json files still parse. */
30
- backend: z.enum(['claude', 'codex', 'opencode']).optional(),
53
+ /** Backend that owns `sessionId`. Optional so pre-affinity runs.json files still parse;
54
+ * `storedRunnerSchema` so a legacy `claude-cli` folds to `claude` instead of failing (#547). */
55
+ backend: storedRunnerSchema.optional(),
56
+ /** Agent profile (account) this step actually spawned under — `default`, or a stored profile
57
+ * id (spec 2026-07-29-agent-profiles). Recorded rather than re-derived because a session id
58
+ * only means something inside the config dir that created it: `sessionId` and `profileId` are
59
+ * a PAIR. Without it, changing the project's account would silently make Continue resume
60
+ * against the wrong account's session store. Absent = the discovered default. */
61
+ profileId: z.string().optional(),
31
62
  /** Dollar cost reported by the claude CLI for this step's turns. */
32
63
  costUsd: z.number().optional(),
33
64
  });
@@ -42,7 +73,9 @@ const queuedMessageSchema = z.object({
42
73
  images: z.array(z.string()).optional(),
43
74
  createdAt: z.string(),
44
75
  });
45
- const runRecordSchema = z.object({
76
+ /** Exported for `./run-index.ts`, the read-only reader of the same file. Nothing else should
77
+ * parse `runs.json` — see `reconcileLoadedRun` for why a second parser is a correctness risk. */
78
+ export const runRecordSchema = z.object({
46
79
  id: z.string(),
47
80
  title: z.string(),
48
81
  /** Display title (#389): the auto-derived summary of the first agent turn,
@@ -80,10 +113,20 @@ const runRecordSchema = z.object({
80
113
  * (e.g. `anthropic/claude-opus-4-8`) the run actually used, resolved from the
81
114
  * free-text `model` against the chosen runner. Additive and optional: pre-#405
82
115
  * records carry only `model`, and it stays the human/hand-edit surface; this
83
- * is the parseable identity cost attribution and reproducible replay key off. */
116
+ * is the parseable identity cost attribution and reproducible replay key off.
117
+ *
118
+ * Read in production by the session header's agent badge (#546), which shows it
119
+ * whenever it says something `model` does not — so this is no longer a
120
+ * write-only field whose next reader has to guess whether it is load-bearing. */
84
121
  modelIdentity: z.string().optional(),
85
- /** Agent backend this run used — drives "open in CLI" resume command. */
86
- runner: z.enum(['claude', 'codex', 'opencode']).optional(),
122
+ /** Agent backend this run used — drives "open in CLI" resume command. `storedRunnerSchema`
123
+ * so a legacy `claude-cli` record folds to `claude` instead of failing the whole index (#547). */
124
+ runner: storedRunnerSchema.optional(),
125
+ /** Per-task agent-account override from the composer (spec 2026-07-29-agent-profiles), applying
126
+ * to steps that run on `runner`. Steps on a DIFFERENT backend still resolve from the project's
127
+ * own selection — an override for Claude says nothing about which Codex account a mixed
128
+ * workflow's codex step should use. Absent = follow the project. */
129
+ agentProfile: z.string().optional(),
87
130
  /** Echo of the extra system prompt this run actually used (R2): the
88
131
  * `POST /api/runs` override, or the `config.json` default it fell back to.
89
132
  * Deliberately NOT the full composed prompt — skill bodies and the handoff
@@ -118,6 +161,17 @@ const runRecordSchema = z.object({
118
161
  monitoringWakeAt: z.string().datetime().optional().catch(undefined),
119
162
  /** True only for the live epoch that exhausted all automatic monitoring checks. */
120
163
  monitoringWakeCapReached: z.boolean().optional(),
164
+ /**
165
+ * Exact deadline at which a run stopped by a provider USAGE LIMIT resumes itself
166
+ * (spec 2026-08-03-auto-resume-after-usage-limit) — the reset instant the provider named plus a
167
+ * short grace. Present only while such a resume is pending: the run is `failed`, the timer is
168
+ * armed, and the cockpit says so. Deliberately survives a restart (`RunStore.open` keeps it) —
169
+ * it is what lets `recover()` re-arm a wait that may be hours long.
170
+ */
171
+ autoResumeAt: z.string().datetime().optional().catch(undefined),
172
+ /** Consecutive automatic resumes since the last human turn — the safety cap's counter.
173
+ * Persisted so a restart cannot reset a loop back to zero. */
174
+ autoResumeAttempts: z.number().int().min(0).optional().catch(undefined),
121
175
  createdAt: z.string(),
122
176
  startedAt: z.string().optional(),
123
177
  finishedAt: z.string().optional(),
@@ -191,7 +245,8 @@ const runRecordSchema = z.object({
191
245
  /** Read receipt (#unread-done-items): the ISO time the cockpit last opened this
192
246
  * run's thread. A finished run reads as "unread" until it has been seen since it
193
247
  * finished — see `isUnread()` in the cockpit's `lib/read-state.ts`. Absent on old
194
- * runs and on every run not yet opened, which the unread rule treats as unread. */
248
+ * runs, on every run not yet opened, and on one `setUnread` put back to unread
249
+ * (#775) — the unread rule treats all three alike. */
195
250
  seenAt: z.string().optional(),
196
251
  currentStepId: z.string().optional(),
197
252
  error: z.string().optional(),
@@ -235,6 +290,19 @@ const MAX_PR_CANDIDATES = 8;
235
290
  * invisible to the janitor, #407). Reasoning items are skipped: thinking text
236
291
  * speculates about PRs the task never touches.
237
292
  */
293
+ /**
294
+ * Archiving IS resigning from a task, so an archived run can never carry a pending usage-limit
295
+ * resume (spec 2026-08-03-auto-resume-after-usage-limit). The rule lives HERE rather than in the
296
+ * archive route because the bulk "Archive finished" sweep never goes through that route, and a
297
+ * user who archives fifty finished tasks has resigned from all fifty.
298
+ *
299
+ * The engine needs no telling: its timer re-reads the record before it fires and no sweep re-arms
300
+ * an archived run, so a cleared field is the whole cancellation.
301
+ */
302
+ function clearPendingAutoResume(run) {
303
+ run.autoResumeAt = undefined;
304
+ run.autoResumeAttempts = undefined;
305
+ }
238
306
  function eventTextFragments(event) {
239
307
  const fragments = [];
240
308
  for (const key of ['text', 'result', 'message']) {
@@ -326,6 +394,49 @@ function createdPrUrl(haystack) {
326
394
  }
327
395
  return before;
328
396
  }
397
+ /**
398
+ * Reconcile one record just read off disk with the fact that whichever process wrote it is gone.
399
+ *
400
+ * Mutates and returns `run`. Extracted from `RunStore.open` so the read-only index reader
401
+ * (`./run-index.ts`) answers the SAME question about a `running` row on disk. Two parsers that
402
+ * disagree here is a visible bug, not an internal one: the cockpit would show a task as running
403
+ * in the ⌘K index and failed the moment you opened it.
404
+ *
405
+ * `keepLive` (#367): leave `queued`/`running`/`waiting` untouched so the caller can recover them
406
+ * (RunManager.recover re-queues queued runs, resumes interrupted ones). Without it — one-shot CLI
407
+ * paths that never recover, and the index reader, which has no manager at all — live-looking runs
408
+ * are marked failed so no ghost stays behind.
409
+ */
410
+ export function reconcileLoadedRun(run, opts) {
411
+ // A run that was live when the previous process exited can never finish —
412
+ // surface that instead of a forever-"running" ghost. `review` survives
413
+ // restarts on purpose: the gate is pure data (worktree + branch + record)
414
+ // with no live process, so the diff panel, Send back (resume) and Draft PR
415
+ // all still work.
416
+ if (!opts?.keepLive &&
417
+ (run.status === 'running' || run.status === 'queued' || run.status === 'waiting')) {
418
+ run.status = 'failed';
419
+ run.error = 'interrupted — cezar process exited during the run';
420
+ run.finishedAt = run.finishedAt ?? new Date().toISOString();
421
+ for (const step of run.steps) {
422
+ if (step.status === 'running' || step.status === 'waiting')
423
+ step.status = 'failed';
424
+ }
425
+ }
426
+ if (!['running', 'waiting', 'queued'].includes(run.status)) {
427
+ run.activity = undefined;
428
+ run.monitoringWakeAt = undefined;
429
+ }
430
+ // A pending usage-limit resume survives the restart on purpose (the wait can be
431
+ // hours) — `RunManager.recover()` re-arms it from this field. It can only mean
432
+ // anything on a `failed` run, so anywhere else it is stale bookkeeping.
433
+ if (run.status !== 'failed')
434
+ run.autoResumeAt = undefined;
435
+ // The wake counter is intentionally process-local, so a restarted process
436
+ // starts a fresh epoch instead of displaying a stale cap.
437
+ run.monitoringWakeCapReached = undefined;
438
+ return run;
439
+ }
329
440
  /**
330
441
  * File-backed run store: `runs.json` index (atomic tmp+rename writes, the
331
442
  * pattern from @cezar/core's IssueStore) plus one append-only NDJSON event
@@ -341,12 +452,7 @@ export class RunStore extends EventEmitter {
341
452
  this.dataDir = dataDir;
342
453
  this.setMaxListeners(100);
343
454
  }
344
- /**
345
- * `keepLive` (#367): leave `queued`/`running`/`waiting` statuses untouched
346
- * so the caller can recover them (RunManager.recover re-queues queued runs,
347
- * resumes interrupted ones). Without it — one-shot CLI paths that never
348
- * recover — live-looking runs are marked failed so no ghost stays behind.
349
- */
455
+ /** See `reconcileLoadedRun` for what `keepLive` (#367) decides about live-looking rows. */
350
456
  static open(dataDir, opts) {
351
457
  mkdirSync(join(dataDir, 'runs'), { recursive: true });
352
458
  const store = new RunStore(dataDir);
@@ -357,31 +463,7 @@ export class RunStore extends EventEmitter {
357
463
  const parsed = z.array(runRecordSchema).safeParse(raw);
358
464
  if (parsed.success) {
359
465
  for (const run of parsed.data) {
360
- // A run that was live when the previous process exited can never
361
- // finish — surface that instead of a forever-"running" ghost.
362
- // `review` survives restarts on purpose: the gate is pure data
363
- // (worktree + branch + record) with no live process, so the diff
364
- // panel, Send back (resume) and Draft PR all still work.
365
- if (!opts?.keepLive &&
366
- (run.status === 'running' ||
367
- run.status === 'queued' ||
368
- run.status === 'waiting')) {
369
- run.status = 'failed';
370
- run.error = 'interrupted — cezar process exited during the run';
371
- run.finishedAt = run.finishedAt ?? new Date().toISOString();
372
- for (const step of run.steps) {
373
- if (step.status === 'running' || step.status === 'waiting')
374
- step.status = 'failed';
375
- }
376
- }
377
- if (!['running', 'waiting', 'queued'].includes(run.status)) {
378
- run.activity = undefined;
379
- run.monitoringWakeAt = undefined;
380
- }
381
- // The wake counter is intentionally process-local, so a restarted
382
- // process starts a fresh epoch instead of displaying a stale cap.
383
- run.monitoringWakeCapReached = undefined;
384
- store.runs.set(run.id, run);
466
+ store.runs.set(run.id, reconcileLoadedRun(run, opts));
385
467
  }
386
468
  }
387
469
  }
@@ -411,6 +493,7 @@ export class RunStore extends EventEmitter {
411
493
  task: input.task,
412
494
  model: input.model,
413
495
  runner: input.runner,
496
+ agentProfile: input.agentProfile,
414
497
  generateFollowups: input.generateFollowups,
415
498
  autonomous: input.autonomous,
416
499
  worktree: input.worktree,
@@ -450,6 +533,13 @@ export class RunStore extends EventEmitter {
450
533
  normalized.monitoringWakeAt = undefined;
451
534
  normalized.monitoringWakeCapReached = undefined;
452
535
  }
536
+ // …and the mirror image for the usage-limit resume (spec
537
+ // 2026-08-03-auto-resume-after-usage-limit): it is a promise made ABOUT a failed run, so a
538
+ // run coming back to life — the resume itself, a user Continue, a re-queue — retires it.
539
+ // The manager's timer re-checks the record before it fires, so a cleared field is enough.
540
+ if (normalized.status && ['running', 'waiting', 'queued'].includes(normalized.status)) {
541
+ normalized.autoResumeAt = undefined;
542
+ }
453
543
  Object.assign(run, this.redactPatch(normalized));
454
544
  this.touch(run);
455
545
  return run;
@@ -536,6 +626,8 @@ export class RunStore extends EventEmitter {
536
626
  return undefined;
537
627
  run.archived = archived;
538
628
  run.archivedAt = archived ? new Date().toISOString() : undefined;
629
+ if (archived)
630
+ clearPendingAutoResume(run);
539
631
  this.touch(run);
540
632
  return run;
541
633
  }
@@ -546,6 +638,7 @@ export class RunStore extends EventEmitter {
546
638
  if (!run.archived && ['done', 'failed', 'cancelled'].includes(run.status)) {
547
639
  run.archived = true;
548
640
  run.archivedAt = new Date().toISOString();
641
+ clearPendingAutoResume(run);
549
642
  this.touch(run);
550
643
  count++;
551
644
  }
@@ -564,19 +657,58 @@ export class RunStore extends EventEmitter {
564
657
  this.touch(run);
565
658
  return run;
566
659
  }
660
+ /** Mark one run as UNread (#775): drop the read receipt so the run rejoins the unread
661
+ * list. The inverse of `setRead` and, like it, `touch`es so the updated record rides the
662
+ * existing `run` SSE.
663
+ *
664
+ * Deleting the field rather than adding a "manually unread" flag is the whole point:
665
+ * absent `seenAt` is ALREADY what every reader treats as unread (`isUnread` in the
666
+ * cockpit's read-state.ts, and `markAllRead`'s clause-for-clause copy of it below), so
667
+ * clearing needs no new state and writes a shape any older cezar already parses.
668
+ *
669
+ * Deliberately unconditional: clearing a receipt is always a legal write, so this
670
+ * succeeds for an already-unread run (idempotent) and for statuses that can never wear
671
+ * the marker. WHETHER the action means anything for a given run is UI policy, and lives
672
+ * in the cockpit's `runActionFlags` — the same split the rest of the store keeps. */
673
+ setUnread(id) {
674
+ const run = this.runs.get(id);
675
+ if (!run)
676
+ return undefined;
677
+ delete run.seenAt;
678
+ this.touch(run);
679
+ return run;
680
+ }
567
681
  /** Bulk mark-read: stamp every currently-unread finished run; returns the count.
568
682
  * "Unread" here is the same rule the cockpit paints (`isUnread` in read-state.ts),
569
- * clause for clause: a `done` or `failed` run that finished and has not been seen
570
- * since. Cancelled runs are never unread — you stopped them yourself — and archived
571
- * ones never are either, since archiving is a stronger "done with this" than reading;
572
- * both are skipped, as are runs already read. Keeping the two rules identical is what
573
- * makes the returned count the number the cockpit's unread badge was showing. */
683
+ * clause for clause:
684
+ * - a `done` or `failed` run that finished and has not been seen since;
685
+ * - cancelled runs are never unread — you stopped them yourself;
686
+ * - archived ones never are either, since archiving is a stronger "done with this"
687
+ * than reading;
688
+ * - and a `failed` run with a pending `autoResumeAt` is not a done item AT ALL
689
+ * (`isScheduledResume`, spec 2026-08-03-auto-resume-after-usage-limit): it has an
690
+ * appointment to pick the work back up, so there is no outcome to have missed.
691
+ *
692
+ * Keeping the two rules identical is what makes the returned count the number the
693
+ * cockpit's unread badge was showing. The `autoResumeAt` clause is the one that drifted
694
+ * (#803): `isUnread` gained it with auto-resume and this sweep did not, so a task waiting
695
+ * out a usage limit was uncounted by the badge but stamped read by the sweep — and this
696
+ * comment asserted an invariant the code no longer held.
697
+ *
698
+ * This rule lives in two languages of the same repo, which is why it has now drifted
699
+ * once. The cockpit cannot import it (`packages/web` does not depend on the service, and
700
+ * should not), so a single definition would have to move to `packages/contract` — the one
701
+ * package both sides already import. Worth doing; deliberately not done here, because
702
+ * widening the contract package's remit from "shapes" to "behavior" is a design change
703
+ * that deserves its own review rather than riding along in a bug fix. Until then: EDIT
704
+ * BOTH, and the case-table tests on either side are what catch you if you don't. */
574
705
  markAllRead() {
575
706
  const now = new Date().toISOString();
576
707
  let count = 0;
577
708
  for (const run of this.runs.values()) {
578
709
  const unread = !run.archived &&
579
710
  (run.status === 'done' || run.status === 'failed') &&
711
+ !(run.status === 'failed' && run.autoResumeAt !== undefined) &&
580
712
  run.finishedAt !== undefined &&
581
713
  (run.seenAt === undefined || run.seenAt < run.finishedAt);
582
714
  if (!unread)