chorus-codes 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +3 -3
  3. package/.next/cache/.previewinfo +1 -1
  4. package/.next/cache/.rscinfo +1 -1
  5. package/.next/fallback-build-manifest.json +3 -3
  6. package/.next/prerender-manifest.json +3 -3
  7. package/.next/server/app/_global-error.html +1 -1
  8. package/.next/server/app/_global-error.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/server/app/_not-found.html +1 -1
  15. package/.next/server/app/_not-found.rsc +1 -1
  16. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  17. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  18. package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  19. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  20. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  21. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  22. package/.next/server/app/new.html +1 -1
  23. package/.next/server/app/new.rsc +1 -1
  24. package/.next/server/app/new.segments/_full.segment.rsc +1 -1
  25. package/.next/server/app/new.segments/_head.segment.rsc +1 -1
  26. package/.next/server/app/new.segments/_index.segment.rsc +1 -1
  27. package/.next/server/app/new.segments/_tree.segment.rsc +1 -1
  28. package/.next/server/app/new.segments/new/__PAGE__.segment.rsc +1 -1
  29. package/.next/server/app/new.segments/new.segment.rsc +1 -1
  30. package/.next/server/app/onboarding.html +1 -1
  31. package/.next/server/app/onboarding.rsc +1 -1
  32. package/.next/server/app/onboarding.segments/_full.segment.rsc +1 -1
  33. package/.next/server/app/onboarding.segments/_head.segment.rsc +1 -1
  34. package/.next/server/app/onboarding.segments/_index.segment.rsc +1 -1
  35. package/.next/server/app/onboarding.segments/_tree.segment.rsc +1 -1
  36. package/.next/server/app/onboarding.segments/onboarding/__PAGE__.segment.rsc +1 -1
  37. package/.next/server/app/onboarding.segments/onboarding.segment.rsc +1 -1
  38. package/.next/server/app/personas.html +1 -1
  39. package/.next/server/app/personas.rsc +1 -1
  40. package/.next/server/app/personas.segments/_full.segment.rsc +1 -1
  41. package/.next/server/app/personas.segments/_head.segment.rsc +1 -1
  42. package/.next/server/app/personas.segments/_index.segment.rsc +1 -1
  43. package/.next/server/app/personas.segments/_tree.segment.rsc +1 -1
  44. package/.next/server/app/personas.segments/personas/__PAGE__.segment.rsc +1 -1
  45. package/.next/server/app/personas.segments/personas.segment.rsc +1 -1
  46. package/.next/server/app/settings.html +1 -1
  47. package/.next/server/app/settings.rsc +1 -1
  48. package/.next/server/app/settings.segments/_full.segment.rsc +1 -1
  49. package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  50. package/.next/server/app/settings.segments/_index.segment.rsc +1 -1
  51. package/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  52. package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +1 -1
  53. package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
  54. package/.next/server/app/templates.html +1 -1
  55. package/.next/server/app/templates.rsc +1 -1
  56. package/.next/server/app/templates.segments/_full.segment.rsc +1 -1
  57. package/.next/server/app/templates.segments/_head.segment.rsc +1 -1
  58. package/.next/server/app/templates.segments/_index.segment.rsc +1 -1
  59. package/.next/server/app/templates.segments/_tree.segment.rsc +1 -1
  60. package/.next/server/app/templates.segments/templates/__PAGE__.segment.rsc +1 -1
  61. package/.next/server/app/templates.segments/templates.segment.rsc +1 -1
  62. package/.next/server/middleware-build-manifest.js +3 -3
  63. package/.next/server/pages/404.html +1 -1
  64. package/.next/server/pages/500.html +1 -1
  65. package/.next/server/server-reference-manifest.js +1 -1
  66. package/.next/server/server-reference-manifest.json +1 -1
  67. package/.next/trace +1 -1
  68. package/.next/trace-build +1 -1
  69. package/dist/cli/commands/doctor.js +116 -0
  70. package/dist/cli/commands/doctor.js.map +1 -0
  71. package/dist/cli/commands/init.js +211 -0
  72. package/dist/cli/commands/init.js.map +1 -0
  73. package/dist/cli/commands/start.js +240 -0
  74. package/dist/cli/commands/start.js.map +1 -0
  75. package/dist/cli/commands/status.js +54 -0
  76. package/dist/cli/commands/status.js.map +1 -0
  77. package/dist/cli/commands/stop.js +97 -0
  78. package/dist/cli/commands/stop.js.map +1 -0
  79. package/dist/cli/connect.js +108 -0
  80. package/dist/cli/connect.js.map +1 -0
  81. package/dist/cli/index.js +99 -0
  82. package/dist/cli/index.js.map +1 -0
  83. package/dist/cli/port-utils.js +260 -0
  84. package/dist/cli/port-utils.js.map +1 -0
  85. package/dist/cli/runtime-env.js +60 -0
  86. package/dist/cli/runtime-env.js.map +1 -0
  87. package/dist/cli/shared.js +54 -0
  88. package/dist/cli/shared.js.map +1 -0
  89. package/dist/cli/ui.js +60 -0
  90. package/dist/cli/ui.js.map +1 -0
  91. package/dist/daemon/agents/claude.js +98 -0
  92. package/dist/daemon/agents/claude.js.map +1 -0
  93. package/dist/daemon/agents/codex.js +160 -0
  94. package/dist/daemon/agents/codex.js.map +1 -0
  95. package/dist/daemon/agents/gemini.js +111 -0
  96. package/dist/daemon/agents/gemini.js.map +1 -0
  97. package/dist/daemon/agents/index.js +59 -0
  98. package/dist/daemon/agents/index.js.map +1 -0
  99. package/dist/daemon/agents/kimi.js +206 -0
  100. package/dist/daemon/agents/kimi.js.map +1 -0
  101. package/dist/daemon/agents/opencode.js +228 -0
  102. package/dist/daemon/agents/opencode.js.map +1 -0
  103. package/dist/daemon/agents/openrouter.js +274 -0
  104. package/dist/daemon/agents/openrouter.js.map +1 -0
  105. package/dist/daemon/agents/parsers/claude.js +63 -0
  106. package/dist/daemon/agents/parsers/claude.js.map +1 -0
  107. package/dist/daemon/agents/parsers/codex.js +51 -0
  108. package/dist/daemon/agents/parsers/codex.js.map +1 -0
  109. package/dist/daemon/agents/parsers/gemini.js +144 -0
  110. package/dist/daemon/agents/parsers/gemini.js.map +1 -0
  111. package/dist/daemon/agents/parsers/index.js +31 -0
  112. package/dist/daemon/agents/parsers/index.js.map +1 -0
  113. package/dist/daemon/agents/parsers/kimi.js +8 -0
  114. package/dist/daemon/agents/parsers/kimi.js.map +1 -0
  115. package/dist/daemon/agents/parsers/opencode.js +105 -0
  116. package/dist/daemon/agents/parsers/opencode.js.map +1 -0
  117. package/dist/daemon/agents/parsers/openrouter.js +69 -0
  118. package/dist/daemon/agents/parsers/openrouter.js.map +1 -0
  119. package/dist/daemon/agents/parsers/shared.js +17 -0
  120. package/dist/daemon/agents/parsers/shared.js.map +1 -0
  121. package/dist/daemon/agents/preflight.js +83 -0
  122. package/dist/daemon/agents/preflight.js.map +1 -0
  123. package/dist/daemon/agents/quote.js +45 -0
  124. package/dist/daemon/agents/quote.js.map +1 -0
  125. package/dist/daemon/agents/sandbox-guard.js +69 -0
  126. package/dist/daemon/agents/sandbox-guard.js.map +1 -0
  127. package/dist/daemon/agents/types.js +6 -0
  128. package/dist/daemon/agents/types.js.map +1 -0
  129. package/dist/daemon/api-response.js +65 -0
  130. package/dist/daemon/api-response.js.map +1 -0
  131. package/dist/daemon/error-detector.js +329 -0
  132. package/dist/daemon/error-detector.js.map +1 -0
  133. package/dist/daemon/headless.js +533 -0
  134. package/dist/daemon/headless.js.map +1 -0
  135. package/dist/daemon/index.js +333 -0
  136. package/dist/daemon/index.js.map +1 -0
  137. package/dist/daemon/openrouter.js +192 -0
  138. package/dist/daemon/openrouter.js.map +1 -0
  139. package/dist/daemon/orchestrators/claude.js +163 -0
  140. package/dist/daemon/orchestrators/claude.js.map +1 -0
  141. package/dist/daemon/orchestrators/codex.js +101 -0
  142. package/dist/daemon/orchestrators/codex.js.map +1 -0
  143. package/dist/daemon/orchestrators/cursor-windsurf.js +118 -0
  144. package/dist/daemon/orchestrators/cursor-windsurf.js.map +1 -0
  145. package/dist/daemon/orchestrators/gemini.js +108 -0
  146. package/dist/daemon/orchestrators/gemini.js.map +1 -0
  147. package/dist/daemon/orchestrators/index.js +90 -0
  148. package/dist/daemon/orchestrators/index.js.map +1 -0
  149. package/dist/daemon/orchestrators/kimi.js +108 -0
  150. package/dist/daemon/orchestrators/kimi.js.map +1 -0
  151. package/dist/daemon/orchestrators/opencode.js +152 -0
  152. package/dist/daemon/orchestrators/opencode.js.map +1 -0
  153. package/dist/daemon/orchestrators/shared.js +60 -0
  154. package/dist/daemon/orchestrators/shared.js.map +1 -0
  155. package/dist/daemon/output-watcher.js +131 -0
  156. package/dist/daemon/output-watcher.js.map +1 -0
  157. package/dist/daemon/participant-aborts.js +123 -0
  158. package/dist/daemon/participant-aborts.js.map +1 -0
  159. package/dist/daemon/reaper.js +46 -0
  160. package/dist/daemon/reaper.js.map +1 -0
  161. package/dist/daemon/routes/chats-events.js +62 -0
  162. package/dist/daemon/routes/chats-events.js.map +1 -0
  163. package/dist/daemon/routes/chats-stream.js +241 -0
  164. package/dist/daemon/routes/chats-stream.js.map +1 -0
  165. package/dist/daemon/routes/chats-validation.js +13 -0
  166. package/dist/daemon/routes/chats-validation.js.map +1 -0
  167. package/dist/daemon/routes/chats.js +545 -0
  168. package/dist/daemon/routes/chats.js.map +1 -0
  169. package/dist/daemon/routes/openrouter.js +103 -0
  170. package/dist/daemon/routes/openrouter.js.map +1 -0
  171. package/dist/daemon/routes/settings.js +199 -0
  172. package/dist/daemon/routes/settings.js.map +1 -0
  173. package/dist/daemon/routes/stats.js +155 -0
  174. package/dist/daemon/routes/stats.js.map +1 -0
  175. package/dist/daemon/routes/system.js +208 -0
  176. package/dist/daemon/routes/system.js.map +1 -0
  177. package/dist/daemon/routes/templates-personas.js +254 -0
  178. package/dist/daemon/routes/templates-personas.js.map +1 -0
  179. package/dist/daemon/routes/voices.js +139 -0
  180. package/dist/daemon/routes/voices.js.map +1 -0
  181. package/dist/daemon/runner/doer-driver.js +346 -0
  182. package/dist/daemon/runner/doer-driver.js.map +1 -0
  183. package/dist/daemon/runner/doer.js +336 -0
  184. package/dist/daemon/runner/doer.js.map +1 -0
  185. package/dist/daemon/runner/prior-round.js +140 -0
  186. package/dist/daemon/runner/prior-round.js.map +1 -0
  187. package/dist/daemon/runner/prompt-builder.js +292 -0
  188. package/dist/daemon/runner/prompt-builder.js.map +1 -0
  189. package/dist/daemon/runner/review-only-phase.js +103 -0
  190. package/dist/daemon/runner/review-only-phase.js.map +1 -0
  191. package/dist/daemon/runner/reviewer-driver.js +410 -0
  192. package/dist/daemon/runner/reviewer-driver.js.map +1 -0
  193. package/dist/daemon/runner/reviewer.js +384 -0
  194. package/dist/daemon/runner/reviewer.js.map +1 -0
  195. package/dist/daemon/runner/run-with-fallback.js +56 -0
  196. package/dist/daemon/runner/run-with-fallback.js.map +1 -0
  197. package/dist/daemon/runner/sanitize-name.js +8 -0
  198. package/dist/daemon/runner/sanitize-name.js.map +1 -0
  199. package/dist/daemon/runner/stream-file-writer.js +116 -0
  200. package/dist/daemon/runner/stream-file-writer.js.map +1 -0
  201. package/dist/daemon/runner/swap-sidecar.js +102 -0
  202. package/dist/daemon/runner/swap-sidecar.js.map +1 -0
  203. package/dist/daemon/runner/template-fallback.js +119 -0
  204. package/dist/daemon/runner/template-fallback.js.map +1 -0
  205. package/dist/daemon/runner/types.js +3 -0
  206. package/dist/daemon/runner/types.js.map +1 -0
  207. package/dist/daemon/runner/verdict.js +51 -0
  208. package/dist/daemon/runner/verdict.js.map +1 -0
  209. package/dist/daemon/runner-multiplex.js +364 -0
  210. package/dist/daemon/runner-multiplex.js.map +1 -0
  211. package/dist/daemon/runner.js +427 -0
  212. package/dist/daemon/runner.js.map +1 -0
  213. package/dist/daemon/ship.js +340 -0
  214. package/dist/daemon/ship.js.map +1 -0
  215. package/dist/daemon/template-cache.js +37 -0
  216. package/dist/daemon/template-cache.js.map +1 -0
  217. package/dist/daemon/tmux-types.js +9 -0
  218. package/dist/daemon/tmux-types.js.map +1 -0
  219. package/dist/daemon/tmux.js +341 -0
  220. package/dist/daemon/tmux.js.map +1 -0
  221. package/dist/lib/atomic-write.js +55 -0
  222. package/dist/lib/atomic-write.js.map +1 -0
  223. package/dist/lib/chat-events-bus.js +27 -0
  224. package/dist/lib/chat-events-bus.js.map +1 -0
  225. package/dist/lib/chat-slug.js +105 -0
  226. package/dist/lib/chat-slug.js.map +1 -0
  227. package/dist/lib/cli-detect.js +388 -0
  228. package/dist/lib/cli-detect.js.map +1 -0
  229. package/dist/lib/cli-health.js +156 -0
  230. package/dist/lib/cli-health.js.map +1 -0
  231. package/dist/lib/cli-paths.js +113 -0
  232. package/dist/lib/cli-paths.js.map +1 -0
  233. package/dist/lib/cli-precheck.js +141 -0
  234. package/dist/lib/cli-precheck.js.map +1 -0
  235. package/dist/lib/db/chats.js +244 -0
  236. package/dist/lib/db/chats.js.map +1 -0
  237. package/dist/lib/db/connection.js +254 -0
  238. package/dist/lib/db/connection.js.map +1 -0
  239. package/dist/lib/db/index.js +34 -0
  240. package/dist/lib/db/index.js.map +1 -0
  241. package/dist/lib/db/personas.js +65 -0
  242. package/dist/lib/db/personas.js.map +1 -0
  243. package/dist/lib/db/phase-events.js +172 -0
  244. package/dist/lib/db/phase-events.js.map +1 -0
  245. package/dist/lib/db/secrets.js +53 -0
  246. package/dist/lib/db/secrets.js.map +1 -0
  247. package/dist/lib/db/settings.js +47 -0
  248. package/dist/lib/db/settings.js.map +1 -0
  249. package/dist/lib/db/templates.js +75 -0
  250. package/dist/lib/db/templates.js.map +1 -0
  251. package/dist/lib/db/voices.js +184 -0
  252. package/dist/lib/db/voices.js.map +1 -0
  253. package/dist/lib/lineage-maps.js +200 -0
  254. package/dist/lib/lineage-maps.js.map +1 -0
  255. package/dist/lib/logger.js +186 -0
  256. package/dist/lib/logger.js.map +1 -0
  257. package/dist/lib/personas.js +117 -0
  258. package/dist/lib/personas.js.map +1 -0
  259. package/dist/lib/runtime-path.js +222 -0
  260. package/dist/lib/runtime-path.js.map +1 -0
  261. package/dist/lib/settings/billing.js +58 -0
  262. package/dist/lib/settings/billing.js.map +1 -0
  263. package/dist/lib/settings/permissions.js +81 -0
  264. package/dist/lib/settings/permissions.js.map +1 -0
  265. package/dist/lib/settings/transport.js +113 -0
  266. package/dist/lib/settings/transport.js.map +1 -0
  267. package/dist/lib/telemetry.js +290 -0
  268. package/dist/lib/telemetry.js.map +1 -0
  269. package/dist/lib/template-schema.js +319 -0
  270. package/dist/lib/template-schema.js.map +1 -0
  271. package/dist/lib/template-validation.js +82 -0
  272. package/dist/lib/template-validation.js.map +1 -0
  273. package/dist/lib/voices.js +533 -0
  274. package/dist/lib/voices.js.map +1 -0
  275. package/dist/mcp/client.js +138 -0
  276. package/dist/mcp/client.js.map +1 -0
  277. package/dist/mcp/index.js +178 -0
  278. package/dist/mcp/index.js.map +1 -0
  279. package/dist/mcp/tools.js +355 -0
  280. package/dist/mcp/tools.js.map +1 -0
  281. package/package.json +2 -1
  282. package/.next/dev/static/chunks/05w9_next_dist_shared_lib_0beh7rg._.js +0 -6077
  283. package/.next/dev/static/chunks/05w9_next_dist_shared_lib_0beh7rg._.js.map +0 -69
  284. package/.next/dev/static/chunks/05w9_next_dist_shared_lib_0pjsj.j._.js +0 -6318
  285. package/.next/dev/static/chunks/05w9_next_dist_shared_lib_0pjsj.j._.js.map +0 -71
  286. package/.next/dev/types/cache-life.d.ts +0 -145
  287. package/.next/dev/types/routes.d.ts +0 -84
  288. package/.next/dev/types/validator.ts +0 -178
  289. /package/.next/static/{dJlbRLlhISA0JGtHKVqgQ → 8H2I8TTPlrKuWCV-SxY5f}/_buildManifest.js +0 -0
  290. /package/.next/static/{dJlbRLlhISA0JGtHKVqgQ → 8H2I8TTPlrKuWCV-SxY5f}/_clientMiddlewareManifest.js +0 -0
  291. /package/.next/static/{dJlbRLlhISA0JGtHKVqgQ → 8H2I8TTPlrKuWCV-SxY5f}/_ssgManifest.js +0 -0
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ /**
3
+ * Per-participant fallback-swap sidecar.
4
+ *
5
+ * Lives at `<participantDir>/_swaps.json` next to `answer.md` /
6
+ * `_stats.json`. Mirrors the existing sidecar pattern — JSON, read by
7
+ * the run-artifacts route at refresh time, no DB schema change.
8
+ *
9
+ * Why a sidecar and not the DB:
10
+ * - phase_events packs warnings as opaque text (`output: "[lineage_fallback]
11
+ * <message>"`). Reconstructing structured fromLineage/toModel/etc. on
12
+ * replay would mean parsing that string — fragile.
13
+ * - The SSE stream shuts off for terminal chats, so reload-after-done
14
+ * loses the cli_warning events entirely.
15
+ * - Sidecar survives indefinitely on disk and is cheap to read.
16
+ *
17
+ * Append-only — multiple swaps in a single slot's chain (rare, but
18
+ * possible: codex → openrouter-gpt → claude) all land in one file.
19
+ */
20
+ var __importDefault = (this && this.__importDefault) || function (mod) {
21
+ return (mod && mod.__esModule) ? mod : { "default": mod };
22
+ };
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.appendSwapSidecar = appendSwapSidecar;
25
+ exports.readSwapSidecar = readSwapSidecar;
26
+ const fs_1 = __importDefault(require("fs"));
27
+ const path_1 = __importDefault(require("path"));
28
+ const SIDECAR_NAME = '_swaps.json';
29
+ /**
30
+ * Append a swap entry to the participant's sidecar. Reads the current
31
+ * file (treats missing / malformed as empty), pushes the entry, writes
32
+ * the result back. Synchronous + best-effort: a failed write is logged
33
+ * to console.error and otherwise swallowed — the SSE event still went
34
+ * out, so the live UI shows the swap; only the post-reload card is
35
+ * affected.
36
+ */
37
+ function appendSwapSidecar(participantDir, entry) {
38
+ try {
39
+ const filePath = path_1.default.join(participantDir, SIDECAR_NAME);
40
+ let existing = [];
41
+ if (fs_1.default.existsSync(filePath)) {
42
+ try {
43
+ const raw = fs_1.default.readFileSync(filePath, 'utf-8');
44
+ const parsed = JSON.parse(raw);
45
+ if (Array.isArray(parsed)) {
46
+ existing = parsed;
47
+ }
48
+ else {
49
+ // Non-array payload — preserve the bad file before overwriting
50
+ // so a manual recovery is possible (vs. silently wiping it).
51
+ renameToCorrupt(filePath);
52
+ }
53
+ }
54
+ catch {
55
+ // Truncated / unparseable JSON, almost certainly from a crash
56
+ // mid-write of a prior version (pre-atomic). Side-band it instead
57
+ // of dropping; next append starts fresh.
58
+ renameToCorrupt(filePath);
59
+ }
60
+ }
61
+ existing.push(entry);
62
+ // Atomic write: stage to <name>.tmp then renameSync. POSIX rename is
63
+ // atomic within the same directory — readers either see the old
64
+ // file or the new one, never a half-written intermediate.
65
+ const tmpPath = `${filePath}.tmp`;
66
+ fs_1.default.writeFileSync(tmpPath, JSON.stringify(existing, null, 2), 'utf-8');
67
+ fs_1.default.renameSync(tmpPath, filePath);
68
+ }
69
+ catch (err) {
70
+ console.error('[chorus] failed to write swap sidecar:', err instanceof Error ? err.message : String(err));
71
+ }
72
+ }
73
+ function renameToCorrupt(filePath) {
74
+ try {
75
+ fs_1.default.renameSync(filePath, `${filePath}.corrupt-${Date.now()}`);
76
+ }
77
+ catch {
78
+ /* best-effort — if the rename fails, the next writeFileSync
79
+ * overwrites the bad file anyway. */
80
+ }
81
+ }
82
+ /**
83
+ * Read all swap entries from a participant's sidecar. Used by the
84
+ * run-artifacts route to surface fallback swaps on the run page even
85
+ * after the SSE stream has closed.
86
+ */
87
+ function readSwapSidecar(participantDir) {
88
+ const filePath = path_1.default.join(participantDir, SIDECAR_NAME);
89
+ if (!fs_1.default.existsSync(filePath))
90
+ return [];
91
+ try {
92
+ const raw = fs_1.default.readFileSync(filePath, 'utf-8');
93
+ const parsed = JSON.parse(raw);
94
+ if (Array.isArray(parsed))
95
+ return parsed;
96
+ }
97
+ catch {
98
+ /* malformed — treat as empty */
99
+ }
100
+ return [];
101
+ }
102
+ //# sourceMappingURL=swap-sidecar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"swap-sidecar.js","sourceRoot":"","sources":["../../../src/daemon/runner/swap-sidecar.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;GAiBG;;;;;AA6BH,8CAsCC;AAgBD,0CAWC;AA5FD,4CAAoB;AACpB,gDAAwB;AAgBxB,MAAM,YAAY,GAAG,aAAa,CAAC;AAEnC;;;;;;;GAOG;AACH,SAAgB,iBAAiB,CAC/B,cAAsB,EACtB,KAAgB;IAEhB,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;QACzD,IAAI,QAAQ,GAAgB,EAAE,CAAC;QAC/B,IAAI,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC5B,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,YAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;gBAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;gBAC/B,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;oBAC1B,QAAQ,GAAG,MAAqB,CAAC;gBACnC,CAAC;qBAAM,CAAC;oBACN,+DAA+D;oBAC/D,6DAA6D;oBAC7D,eAAe,CAAC,QAAQ,CAAC,CAAC;gBAC5B,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,8DAA8D;gBAC9D,kEAAkE;gBAClE,yCAAyC;gBACzC,eAAe,CAAC,QAAQ,CAAC,CAAC;YAC5B,CAAC;QACH,CAAC;QACD,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrB,qEAAqE;QACrE,gEAAgE;QAChE,0DAA0D;QAC1D,MAAM,OAAO,GAAG,GAAG,QAAQ,MAAM,CAAC;QAClC,YAAE,CAAC,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;QACtE,YAAE,CAAC,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;IACnC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CACX,wCAAwC,EACxC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CACjD,CAAC;IACJ,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,QAAgB;IACvC,IAAI,CAAC;QACH,YAAE,CAAC,UAAU,CAAC,QAAQ,EAAE,GAAG,QAAQ,YAAY,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP;6CACqC;IACvC,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,SAAgB,eAAe,CAAC,cAAsB;IACpD,MAAM,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;IACzD,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,EAAE,CAAC;IACxC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;YAAE,OAAO,MAAqB,CAAC;IAC1D,CAAC;IAAC,MAAM,CAAC;QACP,gCAAgC;IAClC,CAAC;IACD,OAAO,EAAE,CAAC;AACZ,CAAC"}
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ /**
3
+ * Template-level fallback chain.
4
+ *
5
+ * Per-slot fallback (`candidate.models[]`) handles "if claude-opus fails, try
6
+ * claude-sonnet" — same lineage, same shim, same auth, just a different
7
+ * `--model` argv.
8
+ *
9
+ * Template-level fallback (`template.fallback[]`) is the catch-all that fires
10
+ * when ANY slot exhausts its per-slot chain. The user sets it once at the
11
+ * template root, and chorus applies it to every slot — cross-lineage swaps
12
+ * are first-class: a codex reviewer hitting quota can fall through to a
13
+ * claude or kimi fallback.
14
+ *
15
+ * Strict (lineage, model) dedup:
16
+ * - Skip a fallback row that matches the slot's own current model — would
17
+ * just fail again.
18
+ * - Skip a fallback row that matches ANOTHER active slot in the same
19
+ * phase. Example: reviewers=[kimi, deepseek] + fallback=[kimi]
20
+ * should NOT spawn a second kimi reviewer when deepseek fails.
21
+ * - Cross-lineage fallback dedup uses (lineage, model) tuples so two slots
22
+ * of different lineages on the same model name (rare) don't collide.
23
+ *
24
+ * Diversity-first ordering:
25
+ * When multiple fallbacks survive dedup, sort by lineage occurrence
26
+ * across active slots (least-represented first). With reviewers
27
+ * [openai, google, anthropic] and fallbacks [anthropic/haiku,
28
+ * moonshot/kimi], the kimi entry runs FIRST — moonshot has 0 active
29
+ * slots, anthropic has 1. Within a single lineage, user-declared
30
+ * order wins. Lets the user spec a long fallback list without having
31
+ * to manually micro-order it for diversity.
32
+ *
33
+ * Cross-lineage swap mechanics:
34
+ * When a fallback's lineage differs from the slot's, the runner re-resolves
35
+ * the shim from the agent registry (`pickShimForVoice(entry.lineage,
36
+ * entry.model)`) for that one attempt. The slot's identity (agentName,
37
+ * on-disk dir, participant key) stays bound to the slot's primary lineage
38
+ * so the cockpit card doesn't re-key mid-run; the runner emits a
39
+ * `cli_warning` with `reason: 'lineage_fallback'` so the UI can show
40
+ * "switched to claude-opus-4-7 (cross-lineage)".
41
+ */
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.buildSlotFallbackChain = buildSlotFallbackChain;
44
+ /**
45
+ * Compose the slot's effective (lineage, model) chain by appending matching
46
+ * template fallbacks (deduped) onto the slot's per-slot chain. The chain
47
+ * mixes the slot's primary lineage with cross-lineage fallbacks at the
48
+ * tail; the runner walks it in order, picking the right shim per entry.
49
+ *
50
+ * Caller is responsible for passing a stable `lineage` value across all
51
+ * slots and template-fallback rows (don't mix cockpit-side and daemon-side
52
+ * names in the same call).
53
+ *
54
+ * @param slot The slot whose chain we're building (its primary +
55
+ * its per-slot fallbacks).
56
+ * @param activeSlots All slots in the same phase, including `slot`. Used
57
+ * to dedup template fallbacks that would duplicate an
58
+ * already-running voice.
59
+ * @param templateFallback The template-root `fallback` array (or undefined).
60
+ * @returns Extended (lineage, model) chain — slot.models first, then
61
+ * deduped template fallbacks (same-lineage and cross-lineage).
62
+ */
63
+ function buildSlotFallbackChain(slot, activeSlots, templateFallback) {
64
+ const chain = (slot.models ?? []).map((m) => ({
65
+ lineage: slot.lineage,
66
+ model: m,
67
+ }));
68
+ // Slot with no models at all: emit one undefined entry so the runner makes
69
+ // exactly one attempt with the lineage default.
70
+ if (chain.length === 0) {
71
+ chain.push({ lineage: slot.lineage, model: undefined });
72
+ }
73
+ if (!templateFallback || templateFallback.length === 0)
74
+ return chain;
75
+ // Pre-compute the dedup set: every (lineage, model) currently active in
76
+ // this phase, including the slot's own per-slot fallbacks.
77
+ const skipKeys = new Set();
78
+ // Lineage-occurrence count across active slots — used to prefer
79
+ // cross-lineage fallbacks first ("most unique combination" = least
80
+ // represented lineage). A slot whose primary lineage is openai pulls
81
+ // the openai count up; a fallback into a lineage with count=0 wins
82
+ // ties over one with count=1.
83
+ const lineageCount = new Map();
84
+ for (const s of activeSlots) {
85
+ lineageCount.set(s.lineage, (lineageCount.get(s.lineage) ?? 0) + 1);
86
+ for (const m of s.models ?? []) {
87
+ skipKeys.add(`${s.lineage}:${m}`);
88
+ }
89
+ }
90
+ const candidates = [];
91
+ let declared = 0;
92
+ for (const fb of templateFallback) {
93
+ for (const m of fb.models ?? []) {
94
+ const key = `${fb.lineage}:${m}`;
95
+ if (skipKeys.has(key)) {
96
+ declared++;
97
+ continue;
98
+ }
99
+ skipKeys.add(key);
100
+ candidates.push({ lineage: fb.lineage, model: m, declaredIdx: declared });
101
+ declared++;
102
+ }
103
+ }
104
+ // Sort: lineages absent from active slots first (count=0), then less-
105
+ // represented lineages, then user-declared order within a lineage.
106
+ // Stable sort — equal keys preserve declared order.
107
+ candidates.sort((a, b) => {
108
+ const ca = lineageCount.get(a.lineage) ?? 0;
109
+ const cb = lineageCount.get(b.lineage) ?? 0;
110
+ if (ca !== cb)
111
+ return ca - cb;
112
+ return a.declaredIdx - b.declaredIdx;
113
+ });
114
+ for (const c of candidates) {
115
+ chain.push({ lineage: c.lineage, model: c.model });
116
+ }
117
+ return chain;
118
+ }
119
+ //# sourceMappingURL=template-fallback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-fallback.js","sourceRoot":"","sources":["../../../src/daemon/runner/template-fallback.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/daemon/runner/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verdictFromReviewerText = verdictFromReviewerText;
4
+ /**
5
+ * Reviewer-text → verdict heuristic.
6
+ *
7
+ * Reviewers don't return a structured boolean; they write English. This
8
+ * tries to extract `approve / request-changes / null` from their final
9
+ * answer.md, scanning the tail first (where verdicts typically live)
10
+ * before falling back to the whole text. Word-boundary regex matches
11
+ * avoid false positives like "approached" → approve.
12
+ *
13
+ * Returns:
14
+ * true = reviewer approved
15
+ * false = reviewer disagreed / requested changes
16
+ * null = ambiguous (caller should treat as failed/inconclusive)
17
+ *
18
+ * The 80-char floor protects against `## DONE`-only answers being
19
+ * counted as ambiguous-empty rather than auto-failures upstream.
20
+ */
21
+ function verdictFromReviewerText(content) {
22
+ const stripped = content.replace(/##\s*DONE\s*$/i, '').trim();
23
+ // Contractions matter: a reviewer writing "don't approve" or "can't
24
+ // approve" would slip past the spaced-out forms below if we only matched
25
+ // `do not approve` / `cannot approve`. Both spellings are common in real
26
+ // reviews. The optional `['’]?t` segment catches both straight (') and
27
+ // typographic (’) apostrophes — LLMs emit the latter often.
28
+ const negatives = /\b(request changes|requesting changes|disagree|reject(?:ed|ing)?|blocker|(?:do not|don['’]?t) (?:approve|merge)|(?:cannot|can['’]?t) (?:approve|merge)|nack)\b/;
29
+ const positives = /\b(approve(?:d|s)?|lgtm|looks good to me|no concerns|ship it|ack)\b/;
30
+ // Check verdict keywords FIRST — a terse but explicit reply like
31
+ // "approve ## DONE" (15 chars after sentinel strip) is unambiguous and
32
+ // shouldn't be filtered out by the length floor. Tail wins over whole
33
+ // so an analytical review mentioning "good practice" mid-paragraph
34
+ // doesn't get auto-approved without an explicit verdict at the end.
35
+ const tail = stripped.slice(-400).toLowerCase();
36
+ if (negatives.test(tail))
37
+ return false;
38
+ if (positives.test(tail))
39
+ return true;
40
+ const whole = stripped.toLowerCase();
41
+ if (negatives.test(whole))
42
+ return false;
43
+ if (positives.test(whole))
44
+ return true;
45
+ // No verdict keyword anywhere — return null (ambiguous). The 20-char
46
+ // floor was previously applied BEFORE the regex, which dropped valid
47
+ // terse approvals like "approve ## DONE". It's no longer needed: short
48
+ // replies without a keyword still resolve to null here.
49
+ return null;
50
+ }
51
+ //# sourceMappingURL=verdict.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verdict.js","sourceRoot":"","sources":["../../../src/daemon/runner/verdict.ts"],"names":[],"mappings":";;AAiBA,0DA+BC;AAhDD;;;;;;;;;;;;;;;;GAgBG;AACH,SAAgB,uBAAuB,CAAC,OAAe;IACrD,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAE9D,oEAAoE;IACpE,yEAAyE;IACzE,yEAAyE;IACzE,uEAAuE;IACvE,4DAA4D;IAC5D,MAAM,SAAS,GACb,gKAAgK,CAAC;IACnK,MAAM,SAAS,GACb,qEAAqE,CAAC;IAExE,iEAAiE;IACjE,uEAAuE;IACvE,sEAAsE;IACtE,mEAAmE;IACnE,oEAAoE;IACpE,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;IAChD,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IACvC,IAAI,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAEtC,MAAM,KAAK,GAAG,QAAQ,CAAC,WAAW,EAAE,CAAC;IACrC,IAAI,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACxC,IAAI,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAEvC,qEAAqE;IACrE,qEAAqE;IACrE,uEAAuE;IACvE,wDAAwD;IACxD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,364 @@
1
+ "use strict";
2
+ /**
3
+ * Multi-subscriber wrapper around runChat.
4
+ *
5
+ * Singleton runner registry — exactly one runChat per chatId, ever. SSE
6
+ * re-attachers (browser refresh, tab open, polling, MCP wait_for_chat)
7
+ * all subscribe to the same in-memory event bus instead of re-firing the
8
+ * runner. Without this, every refresh of the run page used to spawn a
9
+ * fresh doer + 2 reviewers, hammering the LLM CLIs and thrashing memory.
10
+ *
11
+ * onEvent fans out to every subscribed SSE and persists side effects
12
+ * exactly once, regardless of subscriber count.
13
+ */
14
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ var desc = Object.getOwnPropertyDescriptor(m, k);
17
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
18
+ desc = { enumerable: true, get: function() { return m[k]; } };
19
+ }
20
+ Object.defineProperty(o, k2, desc);
21
+ }) : (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ o[k2] = m[k];
24
+ }));
25
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
26
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
27
+ }) : function(o, v) {
28
+ o["default"] = v;
29
+ });
30
+ var __importStar = (this && this.__importStar) || (function () {
31
+ var ownKeys = function(o) {
32
+ ownKeys = Object.getOwnPropertyNames || function (o) {
33
+ var ar = [];
34
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
35
+ return ar;
36
+ };
37
+ return ownKeys(o);
38
+ };
39
+ return function (mod) {
40
+ if (mod && mod.__esModule) return mod;
41
+ var result = {};
42
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
43
+ __setModuleDefault(result, mod);
44
+ return result;
45
+ };
46
+ })();
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.getActiveRun = getActiveRun;
49
+ exports.abortActiveRun = abortActiveRun;
50
+ exports.phaseEventToRunnerEvent = phaseEventToRunnerEvent;
51
+ exports.runWithMultiplex = runWithMultiplex;
52
+ exports.activeRunsSnapshot = activeRunsSnapshot;
53
+ exports.activeRunsCount = activeRunsCount;
54
+ const index_js_1 = require("../lib/db/index.js");
55
+ const logger_js_1 = require("../lib/logger.js");
56
+ const participantAborts = __importStar(require("./participant-aborts.js"));
57
+ const runner_js_1 = require("./runner.js");
58
+ const activeRuns = new Map();
59
+ function getActiveRun(chatId) {
60
+ return activeRuns.get(chatId);
61
+ }
62
+ function abortActiveRun(chatId) {
63
+ const active = activeRuns.get(chatId);
64
+ if (!active)
65
+ return false;
66
+ active.abortController.abort();
67
+ return true;
68
+ }
69
+ /**
70
+ * Reconstruct a RunnerEvent from a persisted phase_events row. Used to
71
+ * replay past events to a freshly-attached SSE so the run page renders
72
+ * the history without waiting for the next live event. Returns null for
73
+ * rows we can't faithfully reconstruct.
74
+ */
75
+ function phaseEventToRunnerEvent(chatId, ev) {
76
+ const baseType = ev.state === 'drafting'
77
+ ? 'phase_start'
78
+ : ev.state === 'submitted'
79
+ ? 'phase_done'
80
+ : ev.state === 'blocked'
81
+ ? 'phase_failed'
82
+ : null;
83
+ if (!baseType) {
84
+ console.warn(`[chorus] phase event replay: unmapped state "${ev.state}" for chat ${chatId}`);
85
+ return null;
86
+ }
87
+ return {
88
+ chatId,
89
+ type: baseType,
90
+ payload: {
91
+ phaseIdx: ev.phase_idx,
92
+ kind: ev.phase_kind,
93
+ role: ev.role,
94
+ agent: ev.agent_id ?? undefined,
95
+ output: ev.output ?? undefined,
96
+ replay: true,
97
+ },
98
+ ts: ev.started_at,
99
+ };
100
+ }
101
+ const VALID_PHASE_KINDS = [
102
+ 'plan',
103
+ 'spec',
104
+ 'tests',
105
+ 'implement',
106
+ 'review',
107
+ 'verify',
108
+ 'divergence',
109
+ 'review_only',
110
+ ];
111
+ const VALID_CHAT_STATUSES = [
112
+ 'drafting',
113
+ 'reviewing',
114
+ 'approved',
115
+ 'merged',
116
+ 'blocked',
117
+ 'cancelled',
118
+ 'failed',
119
+ 'no_review',
120
+ ];
121
+ function parseAttachedFiles(raw) {
122
+ if (!raw)
123
+ return undefined;
124
+ try {
125
+ const parsed = JSON.parse(raw);
126
+ if (Array.isArray(parsed) && parsed.every((p) => typeof p === 'string')) {
127
+ return parsed;
128
+ }
129
+ }
130
+ catch {
131
+ /* ignore */
132
+ }
133
+ return undefined;
134
+ }
135
+ function runWithMultiplex(args) {
136
+ const { chatId, template, chat, tmuxMgr, errorDetector } = args;
137
+ // Explicit cancellation goes through POST /chats/:id/cancel which calls
138
+ // entry.abortController.abort(). Closing an SSE does NOT abort.
139
+ const abortController = new AbortController();
140
+ const subscribers = new Set();
141
+ // Pending DB writes from onEvent. Fire-and-forget here would race
142
+ // against the activeRuns.delete in `.finally()` below — a reattaching
143
+ // SSE could see activeRuns empty (slot released) but read the stale
144
+ // chats row (status='reviewing') and start a duplicate run. Drain
145
+ // this set before releasing the slot.
146
+ const pendingWrites = new Set();
147
+ const trackWrite = (p) => {
148
+ pendingWrites.add(p);
149
+ p.finally(() => pendingWrites.delete(p));
150
+ return p;
151
+ };
152
+ const onEvent = (event) => {
153
+ const line = `data: ${JSON.stringify(event)}\n\n`;
154
+ const toRemove = [];
155
+ for (const sub of Array.from(subscribers)) {
156
+ try {
157
+ if (sub.paused) {
158
+ sub.queue.push(line);
159
+ if (sub.queue.length > 1000) {
160
+ // Queue cap exceeded; drop subscriber to prevent unbounded
161
+ // memory.
162
+ toRemove.push(sub);
163
+ sub.close();
164
+ }
165
+ }
166
+ else {
167
+ const canContinue = sub.write(line);
168
+ if (!canContinue) {
169
+ // Buffer full; pause. Drain listener (set up by the SSE
170
+ // handler) flushes the queue when the kernel buffer recovers.
171
+ sub.paused = true;
172
+ }
173
+ }
174
+ }
175
+ catch {
176
+ /* dead subscriber */
177
+ toRemove.push(sub);
178
+ }
179
+ }
180
+ for (const sub of toRemove) {
181
+ subscribers.delete(sub);
182
+ }
183
+ if (event.type === 'phase_start' ||
184
+ event.type === 'phase_done' ||
185
+ event.type === 'phase_failed') {
186
+ const payload = event.payload;
187
+ const kind = payload.kind;
188
+ const phaseKind = VALID_PHASE_KINDS.includes(kind)
189
+ ? kind
190
+ : 'plan';
191
+ // Fire-and-forget — onEvent is typed `(e) => void` and is called
192
+ // synchronously from the runner; awaiting here would block the
193
+ // entire fan-out chain. SQLite serializes writes via WAL anyway.
194
+ // Tracked in pendingWrites so the .finally drain ensures DB state
195
+ // is consistent before activeRuns.delete fires.
196
+ void trackWrite(index_js_1.phaseEvents
197
+ .create({
198
+ chat_id: chatId,
199
+ phase_idx: payload.phaseIdx ?? 0,
200
+ phase_kind: phaseKind,
201
+ role: payload.role ?? 'doer',
202
+ agent_id: payload.agent ?? null,
203
+ state: event.type === 'phase_start'
204
+ ? 'drafting'
205
+ : event.type === 'phase_done'
206
+ ? 'submitted'
207
+ : 'blocked',
208
+ output: payload.output ?? null,
209
+ cost_usd: 0,
210
+ tokens_in: 0,
211
+ tokens_out: 0,
212
+ started_at: event.ts,
213
+ finished_at: event.type === 'phase_done' || event.type === 'phase_failed'
214
+ ? Date.now()
215
+ : null,
216
+ })
217
+ .catch((err) => {
218
+ (0, logger_js_1.chatLogger)(chatId).error({ err: err instanceof Error ? err.message : String(err) }, 'phaseEvents.create failed');
219
+ }));
220
+ }
221
+ // Persist per-CLI failure events (cli_error / cli_warning) so the
222
+ // cockpit can surface "this reviewer failed with <reason>" on the
223
+ // per-card UI AND post-mortem inspection (sqlite, /chats/:id) shows
224
+ // the failure even after chat-done has fired. Without this, transient
225
+ // subprocess crashes (opencode lock contention, codex quota, gemini
226
+ // rate-limit-with-empty-stdout) wrote 0-byte answer.md files and
227
+ // disappeared from the DB.
228
+ //
229
+ // cli_error ⇒ state='errored', cli_warning ⇒ state='warning'. The
230
+ // replay path (phaseEventToRunnerEvent) ignores both states the same
231
+ // way it always has, so live subscribers are unaffected. Pre-fix every
232
+ // cli_warning landed as state='errored', which made a successful
233
+ // per-slot model fallback look like a reviewer crash in the audit
234
+ // trail.
235
+ if (event.type === 'cli_error' || event.type === 'cli_warning') {
236
+ const payload = event.payload;
237
+ const kind = payload.phaseKind;
238
+ const phaseKind = kind && VALID_PHASE_KINDS.includes(kind)
239
+ ? kind
240
+ : 'review';
241
+ const errorObj = payload.error ?? {};
242
+ const message = errorObj.message ??
243
+ payload.message ??
244
+ 'unknown error';
245
+ const isWarning = event.type === 'cli_warning';
246
+ const persistedState = isWarning ? 'warning' : 'errored';
247
+ const tag = errorObj.kind ??
248
+ (isWarning ? 'cli_warning' : 'cli_error');
249
+ void trackWrite(index_js_1.phaseEvents
250
+ .create({
251
+ chat_id: chatId,
252
+ phase_idx: payload.phaseIdx ?? 0,
253
+ phase_kind: phaseKind,
254
+ role: payload.role ?? 'reviewer',
255
+ agent_id: payload.agent ?? null,
256
+ state: persistedState,
257
+ // Pack the failure / warning context into output so the
258
+ // cockpit's existing event-list rendering surfaces the
259
+ // message without a schema change.
260
+ output: `[${tag}] ${message}`,
261
+ cost_usd: 0,
262
+ tokens_in: 0,
263
+ tokens_out: 0,
264
+ started_at: event.ts,
265
+ finished_at: event.ts,
266
+ })
267
+ .catch((err) => {
268
+ (0, logger_js_1.chatLogger)(chatId).error({ err: err instanceof Error ? err.message : String(err) }, `phaseEvents.create (${persistedState}) failed`);
269
+ }));
270
+ }
271
+ // Update chats.status on terminal event. Runner emits status='completed'
272
+ // for the happy path; we map to 'approved' to fit the chats.status
273
+ // enum. Tracked so .finally drains before releasing the activeRuns
274
+ // slot — otherwise a reattaching SSE could see no active run + stale
275
+ // 'reviewing' status and start a dup run.
276
+ if (event.type === 'chat_done') {
277
+ const payload = event.payload;
278
+ const status = payload.status ?? 'completed';
279
+ // verdict is the reviewer-level outcome (separate from system-level
280
+ // status). Always persist when present so review-only chats with
281
+ // verdict='request_changes' are distinguishable from standard chats
282
+ // whose status='approved' implicitly means verdict='approved'. Cap
283
+ // defensively at 32 chars — verdicts are enum-shaped strings;
284
+ // anything longer is bogus.
285
+ const rawVerdict = payload.verdict;
286
+ const verdict = typeof rawVerdict === 'string' && rawVerdict.length > 0 && rawVerdict.length <= 32
287
+ ? rawVerdict
288
+ : null;
289
+ void trackWrite(index_js_1.chats
290
+ .update(chatId, {
291
+ status: (status === 'completed' ? 'approved' : status),
292
+ ...(verdict !== null ? { verdict } : {}),
293
+ ...(typeof payload.prUrl === 'string' && payload.prUrl.length > 0
294
+ ? { pr_url: payload.prUrl }
295
+ : {}),
296
+ ...(typeof payload.shipError === 'string' && payload.shipError.length > 0
297
+ ? { ship_error: payload.shipError }
298
+ : {}),
299
+ finished_at: Date.now(),
300
+ })
301
+ .catch((err) => {
302
+ (0, logger_js_1.chatLogger)(chatId).error({ err: err instanceof Error ? err.message : String(err) }, 'chats.update on chat_done failed');
303
+ }));
304
+ }
305
+ };
306
+ const promise = (0, runner_js_1.runChat)({
307
+ chatId,
308
+ template,
309
+ work: chat.work,
310
+ artifact: chat.artifact ?? undefined,
311
+ repoPath: chat.repo_path ?? undefined,
312
+ attachedFiles: parseAttachedFiles(chat.attached_files),
313
+ abortSignal: abortController.signal,
314
+ tmuxMgr,
315
+ errorDetector,
316
+ onEvent,
317
+ }).finally(async () => {
318
+ // Drain pending DB writes BEFORE releasing the slot. Without this,
319
+ // the chat_done chats.update can still be in flight when a
320
+ // reattaching SSE sees activeRuns empty + reads stale chats row →
321
+ // starts a duplicate run, burns subscription quota, writes duplicate
322
+ // phase events. allSettled so a failed write doesn't leak unhandled
323
+ // rejections — individual .catch handlers above already log.
324
+ if (pendingWrites.size > 0) {
325
+ await Promise.allSettled(Array.from(pendingWrites));
326
+ }
327
+ activeRuns.delete(chatId);
328
+ // Server-initiated subscriber close. The cockpit closes its
329
+ // EventSource on chat_done already, but a misbehaving / disconnected
330
+ // client can leave the subscriber object pinned in the set. Without
331
+ // this sweep the underlying hijacked socket lingers (held open by
332
+ // Fastify's raw.write reference) until the OS TCP keepalive reaps
333
+ // it. close() swallows its own errors.
334
+ for (const sub of Array.from(subscribers)) {
335
+ try {
336
+ sub.close();
337
+ }
338
+ catch {
339
+ /* dead socket — already closed by the client */
340
+ }
341
+ }
342
+ subscribers.clear();
343
+ // Drop any per-participant abort controllers left over by aborted /
344
+ // crashed runners. They should already have released themselves via
345
+ // their `finally` blocks, but a stale entry would leak across chats
346
+ // if a runner exited abnormally.
347
+ participantAborts.cleanupChat(chatId);
348
+ });
349
+ const entry = { promise, subscribers, abortController };
350
+ activeRuns.set(chatId, entry);
351
+ return entry;
352
+ }
353
+ /**
354
+ * Snapshot of all active runs — for graceful shutdown only. Don't use
355
+ * for steady-state route handling; getActiveRun(chatId) is the right
356
+ * accessor for the chat-keyed lookup.
357
+ */
358
+ function activeRunsSnapshot() {
359
+ return Array.from(activeRuns.values());
360
+ }
361
+ function activeRunsCount() {
362
+ return activeRuns.size;
363
+ }
364
+ //# sourceMappingURL=runner-multiplex.js.map