chorus-codes 0.8.24 → 0.8.26
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.
- package/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +1 -1
- package/.next/build-manifest.json +2 -2
- package/.next/prerender-manifest.json +3 -3
- package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/connect/page.js +1 -1
- package/.next/server/app/connect/page.js.nft.json +1 -1
- package/.next/server/app/connect/page_client-reference-manifest.js +1 -1
- package/.next/server/app/demo/[scenario]/page.js +1 -1
- package/.next/server/app/demo/[scenario]/page.js.nft.json +1 -1
- package/.next/server/app/demo/[scenario]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/new/page.js +1 -1
- package/.next/server/app/new/page.js.nft.json +1 -1
- package/.next/server/app/new/page_client-reference-manifest.js +1 -1
- package/.next/server/app/new.html +1 -1
- package/.next/server/app/new.rsc +3 -3
- package/.next/server/app/new.segments/_full.segment.rsc +3 -3
- package/.next/server/app/new.segments/_head.segment.rsc +1 -1
- package/.next/server/app/new.segments/_index.segment.rsc +2 -2
- package/.next/server/app/new.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/new.segments/new/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/new.segments/new.segment.rsc +1 -1
- package/.next/server/app/onboarding/page.js +1 -1
- package/.next/server/app/onboarding/page.js.nft.json +1 -1
- package/.next/server/app/onboarding/page_client-reference-manifest.js +1 -1
- package/.next/server/app/onboarding.html +1 -1
- package/.next/server/app/onboarding.rsc +3 -3
- package/.next/server/app/onboarding.segments/_full.segment.rsc +3 -3
- package/.next/server/app/onboarding.segments/_head.segment.rsc +1 -1
- package/.next/server/app/onboarding.segments/_index.segment.rsc +2 -2
- package/.next/server/app/onboarding.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/onboarding.segments/onboarding/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/onboarding.segments/onboarding.segment.rsc +1 -1
- package/.next/server/app/page.js +2 -2
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app/personas/page.js +1 -1
- package/.next/server/app/personas/page.js.nft.json +1 -1
- package/.next/server/app/personas/page_client-reference-manifest.js +1 -1
- package/.next/server/app/personas.html +1 -1
- package/.next/server/app/personas.rsc +3 -3
- package/.next/server/app/personas.segments/_full.segment.rsc +3 -3
- package/.next/server/app/personas.segments/_head.segment.rsc +1 -1
- package/.next/server/app/personas.segments/_index.segment.rsc +2 -2
- package/.next/server/app/personas.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/personas.segments/personas/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/personas.segments/personas.segment.rsc +1 -1
- package/.next/server/app/runs/[runId]/page.js +2 -2
- package/.next/server/app/runs/[runId]/page.js.nft.json +1 -1
- package/.next/server/app/runs/[runId]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/runs/page.js +2 -2
- package/.next/server/app/runs/page.js.nft.json +1 -1
- package/.next/server/app/runs/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/page.js +3 -3
- package/.next/server/app/settings/page.js.nft.json +1 -1
- package/.next/server/app/settings/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings/permissions/page.js +1 -1
- package/.next/server/app/settings/permissions/page.js.nft.json +1 -1
- package/.next/server/app/settings/permissions/page_client-reference-manifest.js +1 -1
- package/.next/server/app/settings.html +1 -1
- package/.next/server/app/settings.rsc +3 -3
- package/.next/server/app/settings.segments/_full.segment.rsc +3 -3
- package/.next/server/app/settings.segments/_head.segment.rsc +1 -1
- package/.next/server/app/settings.segments/_index.segment.rsc +2 -2
- package/.next/server/app/settings.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/settings.segments/settings.segment.rsc +1 -1
- package/.next/server/app/templates/page.js +2 -2
- package/.next/server/app/templates/page.js.nft.json +1 -1
- package/.next/server/app/templates/page_client-reference-manifest.js +1 -1
- package/.next/server/app/templates.html +1 -1
- package/.next/server/app/templates.rsc +3 -3
- package/.next/server/app/templates.segments/_full.segment.rsc +3 -3
- package/.next/server/app/templates.segments/_head.segment.rsc +1 -1
- package/.next/server/app/templates.segments/_index.segment.rsc +2 -2
- package/.next/server/app/templates.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/templates.segments/templates/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/templates.segments/templates.segment.rsc +1 -1
- package/.next/server/app-paths-manifest.json +1 -1
- package/.next/server/chunks/189.js +1 -0
- package/.next/server/chunks/{144.js → 21.js} +1 -1
- package/.next/server/chunks/{668.js → 313.js} +1 -1
- package/.next/server/chunks/681.js +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/249-2e840495c38ee022.js +25 -0
- package/.next/static/chunks/641-2908cb9553b8753a.js +1 -0
- package/.next/static/chunks/690-092c26db4082d49a.js +1 -0
- package/.next/static/chunks/app/connect/{page-a3a0af374f90ad4c.js → page-ad4409761e870bd0.js} +1 -1
- package/.next/static/chunks/app/demo/[scenario]/{page-6a0e4aec4bb96fee.js → page-39673968f543c473.js} +1 -1
- package/.next/static/chunks/app/new/{page-b96d75506030acf8.js → page-b5f609ab9413ac00.js} +1 -1
- package/.next/static/chunks/app/onboarding/{page-4be5a1d944e32672.js → page-8b0850fef487abdc.js} +1 -1
- package/.next/static/chunks/app/{page-35375a7c8b3d117a.js → page-9bdbaad592d0ce56.js} +1 -1
- package/.next/static/chunks/app/personas/{page-3884f8907107a4e6.js → page-440c6033a773100c.js} +1 -1
- package/.next/static/chunks/app/runs/[runId]/{page-b5bcf0c093389207.js → page-ffc36f12f1b63ebe.js} +1 -1
- package/.next/static/chunks/app/runs/{page-376175c1ac803558.js → page-6962ea572c9e4b74.js} +1 -1
- package/.next/static/chunks/app/settings/page-ad7180ee0d142704.js +25 -0
- package/.next/static/chunks/app/settings/permissions/{page-c90795aa9299bbe8.js → page-cd767401ac71a29c.js} +1 -1
- package/.next/static/chunks/app/templates/page-0112ab3c7ab5185d.js +1 -0
- package/.next/static/css/df4972a256406ec7.css +3 -0
- package/.next/trace +20 -20
- package/.next/trace-build +1 -1
- package/dist/daemon/cli-semaphore.js +266 -0
- package/dist/daemon/cli-semaphore.js.map +1 -0
- package/dist/daemon/routes/settings.js +39 -0
- package/dist/daemon/routes/settings.js.map +1 -1
- package/dist/daemon/runner/doer-driver.js +243 -214
- package/dist/daemon/runner/doer-driver.js.map +1 -1
- package/dist/daemon/runner/doer.js +24 -1
- package/dist/daemon/runner/doer.js.map +1 -1
- package/dist/daemon/runner/reviewer-driver.js +299 -255
- package/dist/daemon/runner/reviewer-driver.js.map +1 -1
- package/dist/daemon/runner/reviewer.js +27 -1
- package/dist/daemon/runner/reviewer.js.map +1 -1
- package/dist/daemon/runner.js +19 -0
- package/dist/daemon/runner.js.map +1 -1
- package/dist/lib/db/chats.js +28 -0
- package/dist/lib/db/chats.js.map +1 -1
- package/dist/lib/db/connection.js +6 -0
- package/dist/lib/db/connection.js.map +1 -1
- package/dist/lib/db/schema.sql +6 -0
- package/dist/lib/model-pricing.js +306 -0
- package/dist/lib/model-pricing.js.map +1 -0
- package/dist/lib/settings/concurrency.js +101 -0
- package/dist/lib/settings/concurrency.js.map +1 -0
- package/package.json +1 -1
- package/.next/server/chunks/946.js +0 -1
- package/.next/static/chunks/116-8bf7e014066cedde.js +0 -25
- package/.next/static/chunks/15-d438a2b057302bed.js +0 -1
- package/.next/static/chunks/641-60721f44faf711b9.js +0 -1
- package/.next/static/chunks/app/settings/page-1792a3e289409b2d.js +0 -25
- package/.next/static/chunks/app/templates/page-1449b0aea2e7cb68.js +0 -1
- package/.next/static/css/d2bb161eb5bee944.css +0 -3
- /package/.next/static/{jdUcCHB2b5lVrf4v8iQjl → eOeXty5cBGWg7xnmtF6ST}/_buildManifest.js +0 -0
- /package/.next/static/{jdUcCHB2b5lVrf4v8iQjl → eOeXty5cBGWg7xnmtF6ST}/_ssgManifest.js +0 -0
|
@@ -45,6 +45,8 @@ const cli_precheck_js_1 = require("../../lib/cli-precheck.js");
|
|
|
45
45
|
const index_js_1 = require("../../lib/db/index.js");
|
|
46
46
|
const permissions_js_1 = require("../../lib/settings/permissions.js");
|
|
47
47
|
const transport_js_1 = require("../../lib/settings/transport.js");
|
|
48
|
+
const concurrency_js_1 = require("../../lib/settings/concurrency.js");
|
|
49
|
+
const cli_semaphore_js_1 = require("../cli-semaphore.js");
|
|
48
50
|
const index_js_2 = require("../agents/index.js");
|
|
49
51
|
const output_watcher_js_1 = require("../output-watcher.js");
|
|
50
52
|
const participantAborts = __importStar(require("../participant-aborts.js"));
|
|
@@ -55,13 +57,25 @@ const sanitize_name_js_1 = require("./sanitize-name.js");
|
|
|
55
57
|
const swap_sidecar_js_1 = require("./swap-sidecar.js");
|
|
56
58
|
const template_fallback_js_1 = require("./template-fallback.js");
|
|
57
59
|
const verdict_js_1 = require("./verdict.js");
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Local-CLI reviewer concurrency is enforced daemon-wide by
|
|
62
|
+
* `cli-semaphore.ts` — global cap (`maxParallelCli`) + per-CLI cap
|
|
63
|
+
* (`perCli['opencode-cli']` etc.). Settings are user-tunable via
|
|
64
|
+
* /settings; defaults are the same numbers we used to hardcode here
|
|
65
|
+
* (3 global, opencode/gemini/kimi capped at 2 each). The semaphore is
|
|
66
|
+
* shared across chats, not per-chat — that's where the OOM risk lives.
|
|
67
|
+
*
|
|
68
|
+
* HTTP-dispatched shims (openrouter and friends) bypass the semaphore
|
|
69
|
+
* entirely — they're network calls and don't consume local resources.
|
|
70
|
+
*/
|
|
71
|
+
/**
|
|
72
|
+
* Type guard for shim names that map to our capped CLI lineage keys.
|
|
73
|
+
* Anything that isn't one of these is treated as a non-cap'd lineage
|
|
74
|
+
* (defensive — covers future shim names we forgot to add to CLI_LINEAGES).
|
|
75
|
+
*/
|
|
76
|
+
function isCappedLineage(shimName) {
|
|
77
|
+
return concurrency_js_1.CLI_LINEAGES.includes(shimName);
|
|
78
|
+
}
|
|
65
79
|
async function runReviewers(chatDir, chatId, phase, phaseIdx, round, doerOutput, work, filesBlock, tmuxMgr, errorDetector, onEvent, abortSignal, templateFallbackReviewer) {
|
|
66
80
|
if (!phase.reviewer || phase.reviewer.candidates.length === 0) {
|
|
67
81
|
return { agreed: true, summary: '', allFailed: false };
|
|
@@ -78,13 +92,14 @@ async function runReviewers(chatDir, chatId, phase, phaseIdx, round, doerOutput,
|
|
|
78
92
|
const required = phase.reviewer.require;
|
|
79
93
|
// Split candidates by transport. HTTP-dispatched shims (openrouter,
|
|
80
94
|
// future API-only shims) consume zero local CPU/RAM — they're just
|
|
81
|
-
// network calls — so
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
95
|
+
// network calls — so they bypass the cli-semaphore entirely and run
|
|
96
|
+
// unbounded parallel. Per-shim rate limiting (e.g. OpenRouter's 429
|
|
97
|
+
// with Retry-After) is the upstream's job; chorus shouldn't double-
|
|
98
|
+
// throttle. Local CLI candidates go through the semaphore which
|
|
99
|
+
// enforces both the global cap and the per-CLI cap; the wait happens
|
|
100
|
+
// INSIDE runReviewer right before spawn so a reviewer that's queued
|
|
101
|
+
// still emits its phase_start / participant cards, just in a
|
|
102
|
+
// "waiting for slot" state.
|
|
88
103
|
const localCandidateIdxs = [];
|
|
89
104
|
const httpCandidateIdxs = [];
|
|
90
105
|
for (let i = 0; i < candidates.length; i++) {
|
|
@@ -116,26 +131,15 @@ async function runReviewers(chatDir, chatId, phase, phaseIdx, round, doerOutput,
|
|
|
116
131
|
});
|
|
117
132
|
}
|
|
118
133
|
}
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return;
|
|
127
|
-
const i = localCursor++;
|
|
128
|
-
if (i >= localCandidateIdxs.length)
|
|
129
|
-
return;
|
|
130
|
-
await runOne(localCandidateIdxs[i]);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
const localWorkerCount = Math.min(REVIEWER_CONCURRENCY, localCandidateIdxs.length);
|
|
134
|
-
// HTTP bucket: unbounded parallel — Promise.all over runOne directly.
|
|
135
|
-
// Per-shim rate limiting (e.g. OpenRouter's 429 with Retry-After) is
|
|
136
|
-
// the upstream's job; chorus shouldn't double-throttle.
|
|
134
|
+
// Both buckets fire in parallel — the cli-semaphore inside
|
|
135
|
+
// runReviewer is what enforces the local-CLI caps, so we don't need
|
|
136
|
+
// a worker pool here. Reviewers continue to all run to completion
|
|
137
|
+
// (no cancel-on-consensus); that's the established behaviour from
|
|
138
|
+
// chorus-085 (see memory `feedback_let_all_reviewers_finish`),
|
|
139
|
+
// unchanged by this PR. We're only swapping the worker-pool
|
|
140
|
+
// implementation for a daemon-wide semaphore.
|
|
137
141
|
await Promise.all([
|
|
138
|
-
...
|
|
142
|
+
...localCandidateIdxs.map((i) => runOne(i)),
|
|
139
143
|
...httpCandidateIdxs.map((i) => runOne(i)),
|
|
140
144
|
]);
|
|
141
145
|
const agreedCount = reviews.filter((r) => r.outcome === 'agreed').length;
|
|
@@ -160,12 +164,13 @@ async function runReviewer(chatDir, chatId, phase, phaseIdx, round, reviewerIdx,
|
|
|
160
164
|
const reviewerModel = candidate.models?.[0];
|
|
161
165
|
const shim = (0, index_js_2.pickShimForVoice)(candidate.lineage, reviewerModel);
|
|
162
166
|
const agentName = shim.name;
|
|
167
|
+
const isHttp = (0, index_js_2.isHttpDispatchedShim)(shim);
|
|
163
168
|
// Pre-spawn precheck — same gate as runDoer. A reviewer that fails
|
|
164
169
|
// precheck returns null, which the phase loop already handles by
|
|
165
170
|
// counting it toward the all-reviewers-failed threshold and continuing
|
|
166
171
|
// with the remaining reviewers. HTTP-dispatched shims (openrouter)
|
|
167
172
|
// skip this — auth is the secrets table, checked inside the shim.
|
|
168
|
-
if (!
|
|
173
|
+
if (!isHttp) {
|
|
169
174
|
const preRev = await (0, cli_precheck_js_1.precheckLineage)(candidate.lineage);
|
|
170
175
|
if (!preRev.ok) {
|
|
171
176
|
onEvent({
|
|
@@ -187,6 +192,34 @@ async function runReviewer(chatDir, chatId, phase, phaseIdx, round, reviewerIdx,
|
|
|
187
192
|
return null;
|
|
188
193
|
}
|
|
189
194
|
}
|
|
195
|
+
// Acquire the daemon-wide CLI slot (global + per-lineage). Local CLI
|
|
196
|
+
// only — HTTP-dispatched shims aren't a memory pressure source and
|
|
197
|
+
// bypass the semaphore. The slot is held for the reviewer's entire
|
|
198
|
+
// lifetime, including any per-slot fallback chain — this is
|
|
199
|
+
// conservative when a fallback swaps to a different lineage (we keep
|
|
200
|
+
// the original slot rather than swap), but worst case is over-
|
|
201
|
+
// counting the original lineage's quota during the swap window. The
|
|
202
|
+
// global cap still holds.
|
|
203
|
+
//
|
|
204
|
+
// The abortSignal is passed so a chat cancelled while this reviewer
|
|
205
|
+
// is queued behind the cap doesn't leave a stale waiter blocking the
|
|
206
|
+
// semaphore head forever. On abort, acquire rejects → we return null
|
|
207
|
+
// (treated as a failed reviewer by the phase loop) without spawning.
|
|
208
|
+
//
|
|
209
|
+
// `releaseSlot` is null for HTTP shims and the precheck-failed early-
|
|
210
|
+
// return; the finally block below is robust to that.
|
|
211
|
+
let releaseSlot = null;
|
|
212
|
+
if (!isHttp && isCappedLineage(agentName)) {
|
|
213
|
+
try {
|
|
214
|
+
releaseSlot = await (0, cli_semaphore_js_1.acquire)(agentName, abortSignal);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// Aborted while waiting for slot — don't proceed. The phase loop
|
|
218
|
+
// counts this reviewer as failed which preserves "all-failed"
|
|
219
|
+
// semantics for the chat-level verdict.
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
190
223
|
const roundDir = path_1.default.join(chatDir, `round-${round}`);
|
|
191
224
|
const reviewerDir = path_1.default.join(roundDir, `reviewer-${agentName}-${reviewerIdx}`);
|
|
192
225
|
if (!fs_1.default.existsSync(reviewerDir)) {
|
|
@@ -194,16 +227,41 @@ async function runReviewer(chatDir, chatId, phase, phaseIdx, round, reviewerIdx,
|
|
|
194
227
|
}
|
|
195
228
|
const askFile = path_1.default.join(reviewerDir, 'ask.md');
|
|
196
229
|
const answerFile = path_1.default.join(reviewerDir, 'answer.md');
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
230
|
+
// Outer try/finally — guarantees the cli-semaphore slot is returned
|
|
231
|
+
// on every path: headless's nested try/finally for participantAborts,
|
|
232
|
+
// tmux's nested try/finally for the poll interval, AND any thrown
|
|
233
|
+
// error in persona resolution or ask building. `releaseSlot` is null
|
|
234
|
+
// for HTTP shims (acquire was skipped) — the optional-call is the
|
|
235
|
+
// guard.
|
|
236
|
+
try {
|
|
237
|
+
// Resolve reviewer persona — same fallback + warning pattern as runDoer.
|
|
238
|
+
let reviewerPersonaPrompt;
|
|
239
|
+
if (candidate.persona) {
|
|
240
|
+
const personaId = candidate.persona;
|
|
241
|
+
try {
|
|
242
|
+
const row = await index_js_1.personas.getById(personaId);
|
|
243
|
+
if (row) {
|
|
244
|
+
reviewerPersonaPrompt = row.system_prompt;
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
onEvent({
|
|
248
|
+
chatId,
|
|
249
|
+
type: 'cli_warning',
|
|
250
|
+
payload: {
|
|
251
|
+
phaseId: phase.id,
|
|
252
|
+
phaseIdx,
|
|
253
|
+
round,
|
|
254
|
+
role: 'reviewer',
|
|
255
|
+
agent: `${agentName}-${reviewerIdx}`,
|
|
256
|
+
kind: 'persona_missing',
|
|
257
|
+
message: `Reviewer persona "${personaId}" not found in personas table — running with generic prompt. Check the template's reviewer candidate persona field.`,
|
|
258
|
+
},
|
|
259
|
+
ts: Date.now(),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
205
262
|
}
|
|
206
|
-
|
|
263
|
+
catch (err) {
|
|
264
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
207
265
|
onEvent({
|
|
208
266
|
chatId,
|
|
209
267
|
type: 'cli_warning',
|
|
@@ -213,184 +271,75 @@ async function runReviewer(chatDir, chatId, phase, phaseIdx, round, reviewerIdx,
|
|
|
213
271
|
round,
|
|
214
272
|
role: 'reviewer',
|
|
215
273
|
agent: `${agentName}-${reviewerIdx}`,
|
|
216
|
-
kind: '
|
|
217
|
-
message: `Reviewer persona "${personaId}"
|
|
274
|
+
kind: 'persona_lookup_failed',
|
|
275
|
+
message: `Reviewer persona lookup for "${personaId}" failed: ${message} — running with generic prompt.`,
|
|
218
276
|
},
|
|
219
277
|
ts: Date.now(),
|
|
220
278
|
});
|
|
221
279
|
}
|
|
222
280
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const thisSlot = {
|
|
260
|
-
lineage: candidate.lineage,
|
|
261
|
-
models: candidate.models ?? [],
|
|
262
|
-
};
|
|
263
|
-
const chain = (0, template_fallback_js_1.buildSlotFallbackChain)(thisSlot, allReviewerSlots, templateFallbackReviewer);
|
|
264
|
-
return await (0, run_with_fallback_js_1.runWithChainFallback)(chain, async (entry) => {
|
|
265
|
-
// Cross-lineage swap: when the entry's lineage differs from the
|
|
266
|
-
// slot's primary, re-resolve the shim. The slot's identity
|
|
267
|
-
// (agentName, reviewerDir, participant key) stays bound to the
|
|
268
|
-
// primary lineage so the cockpit card doesn't re-key mid-run —
|
|
269
|
-
// the cli_warning below tells the UI a swap happened.
|
|
270
|
-
const entryShim = entry.lineage === candidate.lineage
|
|
271
|
-
? shim
|
|
272
|
-
: (0, index_js_2.pickShimForVoice)(entry.lineage, entry.model);
|
|
273
|
-
return (0, reviewer_js_1.runReviewerHeadless)({
|
|
274
|
-
shim: entryShim,
|
|
275
|
-
chatId,
|
|
276
|
-
phase,
|
|
277
|
-
round,
|
|
278
|
-
reviewerIdx,
|
|
279
|
-
candidateLineage: entry.lineage,
|
|
280
|
-
candidateModel: entry.model,
|
|
281
|
-
agentName,
|
|
282
|
-
askContent: ask,
|
|
283
|
-
answerFile,
|
|
284
|
-
reviewerDir,
|
|
285
|
-
abortSignal: handle.signal,
|
|
286
|
-
onEvent,
|
|
287
|
-
});
|
|
288
|
-
}, (from, to, fromIdx) => {
|
|
289
|
-
const sameLineage = from.lineage === to.lineage;
|
|
290
|
-
const reason = sameLineage ? 'model_fallback' : 'lineage_fallback';
|
|
291
|
-
const message = sameLineage
|
|
292
|
-
? `Reviewer model "${from.model ?? '(default)'}" produced no answer; retrying with "${to.model ?? '(default)'}".`
|
|
293
|
-
: `Reviewer ${from.lineage}/${from.model ?? '(default)'} failed; switching to ${to.lineage}/${to.model ?? '(default)'} (cross-lineage fallback).`;
|
|
294
|
-
// Structured daemon-log line. Pairs with the [reviewer] attempt-
|
|
295
|
-
// failed line that was just emitted by reviewer.ts: tail the log
|
|
296
|
-
// and you see "attempt failed" → "fallback fired" → next
|
|
297
|
-
// "attempt failed" or success in order, per slot.
|
|
298
|
-
console.warn(`[reviewer] fallback fired chat=${chatId} round=${round} ` +
|
|
299
|
-
`slot=${agentName}-${reviewerIdx} reason=${reason} ` +
|
|
300
|
-
`from=${from.lineage}/${from.model ?? '(default)'} ` +
|
|
301
|
-
`to=${to.lineage}/${to.model ?? '(default)'} ` +
|
|
302
|
-
`chain_idx=${fromIdx}`);
|
|
303
|
-
onEvent({
|
|
304
|
-
chatId,
|
|
305
|
-
type: 'cli_warning',
|
|
306
|
-
payload: {
|
|
307
|
-
phaseId: phase.id,
|
|
281
|
+
const ask = (0, prompt_builder_js_1.buildReviewerAsk)(phase, phaseIdx, round, work, doerOutput, filesBlock, reviewerPersonaPrompt);
|
|
282
|
+
fs_1.default.writeFileSync(askFile, ask);
|
|
283
|
+
// Per-slot model fallback: when candidate.models lists multiple models
|
|
284
|
+
// we try them in order, falling through on `null` (no answer produced).
|
|
285
|
+
// The boolean verdict `false` (disagreement) is a real result and stops
|
|
286
|
+
// the chain — runWithModelFallback only re-tries on literal null.
|
|
287
|
+
const transport = await (0, transport_js_1.getTransport)();
|
|
288
|
+
if (transport === 'headless' && shim.runHeadless) {
|
|
289
|
+
const handle = participantAborts.register(chatId, participantAborts.participantKey('reviewer', agentName, reviewerIdx), abortSignal);
|
|
290
|
+
try {
|
|
291
|
+
// Compose: this slot's per-slot chain + template-level
|
|
292
|
+
// fallback.reviewer (same lineage, dedup'd against this slot AND
|
|
293
|
+
// every other reviewer slot in the phase so we don't spawn a
|
|
294
|
+
// duplicate voice).
|
|
295
|
+
const allReviewerSlots = (phase.reviewer?.candidates ?? []).map((c) => ({
|
|
296
|
+
lineage: c.lineage,
|
|
297
|
+
models: c.models ?? [],
|
|
298
|
+
}));
|
|
299
|
+
const thisSlot = {
|
|
300
|
+
lineage: candidate.lineage,
|
|
301
|
+
models: candidate.models ?? [],
|
|
302
|
+
};
|
|
303
|
+
const chain = (0, template_fallback_js_1.buildSlotFallbackChain)(thisSlot, allReviewerSlots, templateFallbackReviewer);
|
|
304
|
+
return await (0, run_with_fallback_js_1.runWithChainFallback)(chain, async (entry) => {
|
|
305
|
+
// Cross-lineage swap: when the entry's lineage differs from the
|
|
306
|
+
// slot's primary, re-resolve the shim. The slot's identity
|
|
307
|
+
// (agentName, reviewerDir, participant key) stays bound to the
|
|
308
|
+
// primary lineage so the cockpit card doesn't re-key mid-run —
|
|
309
|
+
// the cli_warning below tells the UI a swap happened.
|
|
310
|
+
const entryShim = entry.lineage === candidate.lineage
|
|
311
|
+
? shim
|
|
312
|
+
: (0, index_js_2.pickShimForVoice)(entry.lineage, entry.model);
|
|
313
|
+
return (0, reviewer_js_1.runReviewerHeadless)({
|
|
314
|
+
shim: entryShim,
|
|
315
|
+
chatId,
|
|
316
|
+
phase,
|
|
308
317
|
round,
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
fromModel: from.model ?? '(default)',
|
|
335
|
-
toModel: to.model ?? '(default)',
|
|
336
|
-
fallbackIdx: fromIdx,
|
|
337
|
-
ts: Date.now(),
|
|
338
|
-
});
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
finally {
|
|
342
|
-
handle.release();
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
// Reviewers don't share sessions across rounds — each round wants a
|
|
346
|
-
// fresh perspective on the new doer output. Across-phase reuse never
|
|
347
|
-
// makes sense.
|
|
348
|
-
const perms = await (0, permissions_js_1.getPermissions)();
|
|
349
|
-
const sessionName = (0, sanitize_name_js_1.sanitizeName)(`chorus-${chatId}-${phase.id}-reviewer-${agentName}-${reviewerIdx}`);
|
|
350
|
-
const session = await tmuxMgr.acquire({
|
|
351
|
-
chatId,
|
|
352
|
-
phaseId: phase.id,
|
|
353
|
-
role: 'reviewer',
|
|
354
|
-
round,
|
|
355
|
-
shareSessionAcrossRounds: false,
|
|
356
|
-
shareSessionAcrossPhases: false,
|
|
357
|
-
shim,
|
|
358
|
-
spawnOpts: {
|
|
359
|
-
sessionName,
|
|
360
|
-
cwd: reviewerDir,
|
|
361
|
-
model: candidate.models?.[0],
|
|
362
|
-
sandbox: perms.sandboxProfile,
|
|
363
|
-
autoApprove: perms.autoApprovePrompts,
|
|
364
|
-
networkAccess: perms.networkAccess,
|
|
365
|
-
},
|
|
366
|
-
agentName: `${agentName}-${reviewerIdx}`,
|
|
367
|
-
});
|
|
368
|
-
if (shim.clearKeys && shim.clearKeys.length > 0) {
|
|
369
|
-
tmuxMgr.sendKeys(session.name, [...shim.clearKeys]);
|
|
370
|
-
}
|
|
371
|
-
if (shim.preNudge)
|
|
372
|
-
shim.preNudge(session.name);
|
|
373
|
-
const prompt = shim.formatPrompt({
|
|
374
|
-
promptFile: askFile,
|
|
375
|
-
answerFile,
|
|
376
|
-
task: `Review: ${phase.title}`,
|
|
377
|
-
expectDoneSentinel: true,
|
|
378
|
-
});
|
|
379
|
-
// Wait for the CLI's TUI to finish cold-start before pasting (6s
|
|
380
|
-
// covers Codex's slow cold-start). See doer-driver for rationale.
|
|
381
|
-
await new Promise((r) => setTimeout(r, 6000));
|
|
382
|
-
tmuxMgr.pasteBuffer(session.name, prompt);
|
|
383
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
384
|
-
tmuxMgr.sendKeys(session.name, ['Enter']);
|
|
385
|
-
// Failure-mode polling — same pattern as the doer.
|
|
386
|
-
const pollHandle = setInterval(() => {
|
|
387
|
-
try {
|
|
388
|
-
const pane = tmuxMgr.capturePane(session.name);
|
|
389
|
-
const err = errorDetector.inspect(session.name, candidate.lineage, pane);
|
|
390
|
-
if (err) {
|
|
391
|
-
const recoveryKeys = err.kind === 'permission_prompt' ? shim.recoverKeys?.permission_prompt : undefined;
|
|
392
|
-
if (recoveryKeys && recoveryKeys.length > 0) {
|
|
393
|
-
tmuxMgr.sendKeys(session.name, [...recoveryKeys]);
|
|
318
|
+
reviewerIdx,
|
|
319
|
+
candidateLineage: entry.lineage,
|
|
320
|
+
candidateModel: entry.model,
|
|
321
|
+
agentName,
|
|
322
|
+
askContent: ask,
|
|
323
|
+
answerFile,
|
|
324
|
+
reviewerDir,
|
|
325
|
+
abortSignal: handle.signal,
|
|
326
|
+
onEvent,
|
|
327
|
+
});
|
|
328
|
+
}, (from, to, fromIdx) => {
|
|
329
|
+
const sameLineage = from.lineage === to.lineage;
|
|
330
|
+
const reason = sameLineage ? 'model_fallback' : 'lineage_fallback';
|
|
331
|
+
const message = sameLineage
|
|
332
|
+
? `Reviewer model "${from.model ?? '(default)'}" produced no answer; retrying with "${to.model ?? '(default)'}".`
|
|
333
|
+
: `Reviewer ${from.lineage}/${from.model ?? '(default)'} failed; switching to ${to.lineage}/${to.model ?? '(default)'} (cross-lineage fallback).`;
|
|
334
|
+
// Structured daemon-log line. Pairs with the [reviewer] attempt-
|
|
335
|
+
// failed line that was just emitted by reviewer.ts: tail the log
|
|
336
|
+
// and you see "attempt failed" → "fallback fired" → next
|
|
337
|
+
// "attempt failed" or success in order, per slot.
|
|
338
|
+
console.warn(`[reviewer] fallback fired chat=${chatId} round=${round} ` +
|
|
339
|
+
`slot=${agentName}-${reviewerIdx} reason=${reason} ` +
|
|
340
|
+
`from=${from.lineage}/${from.model ?? '(default)'} ` +
|
|
341
|
+
`to=${to.lineage}/${to.model ?? '(default)'} ` +
|
|
342
|
+
`chain_idx=${fromIdx}`);
|
|
394
343
|
onEvent({
|
|
395
344
|
chatId,
|
|
396
345
|
type: 'cli_warning',
|
|
@@ -399,59 +348,154 @@ async function runReviewer(chatDir, chatId, phase, phaseIdx, round, reviewerIdx,
|
|
|
399
348
|
round,
|
|
400
349
|
role: 'reviewer',
|
|
401
350
|
agent: `${agentName}-${reviewerIdx}`,
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
351
|
+
reason,
|
|
352
|
+
fromLineage: from.lineage,
|
|
353
|
+
toLineage: to.lineage,
|
|
354
|
+
fromModel: from.model ?? '(default)',
|
|
355
|
+
toModel: to.model ?? '(default)',
|
|
356
|
+
fallbackIdx: fromIdx,
|
|
357
|
+
message,
|
|
405
358
|
},
|
|
406
359
|
ts: Date.now(),
|
|
407
360
|
});
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
//
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
round,
|
|
425
|
-
role: 'reviewer',
|
|
426
|
-
agent: `${agentName}-${reviewerIdx}`,
|
|
427
|
-
error: err,
|
|
428
|
-
},
|
|
361
|
+
// Persist a sidecar so swap cards survive page reloads — the
|
|
362
|
+
// SSE stream shuts off for terminal chats, and phase_events
|
|
363
|
+
// packs warnings as opaque text. Mirrors the _stats.json /
|
|
364
|
+
// _meta.json pattern: append-only JSON array, read by the
|
|
365
|
+
// run-artifacts route at the next refresh tick.
|
|
366
|
+
(0, swap_sidecar_js_1.appendSwapSidecar)(reviewerDir, {
|
|
367
|
+
round,
|
|
368
|
+
phaseId: phase.id,
|
|
369
|
+
role: 'reviewer',
|
|
370
|
+
agent: `${agentName}-${reviewerIdx}`,
|
|
371
|
+
reason,
|
|
372
|
+
fromLineage: from.lineage,
|
|
373
|
+
toLineage: to.lineage,
|
|
374
|
+
fromModel: from.model ?? '(default)',
|
|
375
|
+
toModel: to.model ?? '(default)',
|
|
376
|
+
fallbackIdx: fromIdx,
|
|
429
377
|
ts: Date.now(),
|
|
430
378
|
});
|
|
431
|
-
}
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
finally {
|
|
382
|
+
handle.release();
|
|
432
383
|
}
|
|
433
384
|
}
|
|
434
|
-
|
|
435
|
-
|
|
385
|
+
// Reviewers don't share sessions across rounds — each round wants a
|
|
386
|
+
// fresh perspective on the new doer output. Across-phase reuse never
|
|
387
|
+
// makes sense.
|
|
388
|
+
const perms = await (0, permissions_js_1.getPermissions)();
|
|
389
|
+
const sessionName = (0, sanitize_name_js_1.sanitizeName)(`chorus-${chatId}-${phase.id}-reviewer-${agentName}-${reviewerIdx}`);
|
|
390
|
+
const session = await tmuxMgr.acquire({
|
|
391
|
+
chatId,
|
|
392
|
+
phaseId: phase.id,
|
|
393
|
+
role: 'reviewer',
|
|
394
|
+
round,
|
|
395
|
+
shareSessionAcrossRounds: false,
|
|
396
|
+
shareSessionAcrossPhases: false,
|
|
397
|
+
shim,
|
|
398
|
+
spawnOpts: {
|
|
399
|
+
sessionName,
|
|
400
|
+
cwd: reviewerDir,
|
|
401
|
+
model: candidate.models?.[0],
|
|
402
|
+
sandbox: perms.sandboxProfile,
|
|
403
|
+
autoApprove: perms.autoApprovePrompts,
|
|
404
|
+
networkAccess: perms.networkAccess,
|
|
405
|
+
},
|
|
406
|
+
agentName: `${agentName}-${reviewerIdx}`,
|
|
407
|
+
});
|
|
408
|
+
if (shim.clearKeys && shim.clearKeys.length > 0) {
|
|
409
|
+
tmuxMgr.sendKeys(session.name, [...shim.clearKeys]);
|
|
436
410
|
}
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
|
|
411
|
+
if (shim.preNudge)
|
|
412
|
+
shim.preNudge(session.name);
|
|
413
|
+
const prompt = shim.formatPrompt({
|
|
414
|
+
promptFile: askFile,
|
|
415
|
+
answerFile,
|
|
416
|
+
task: `Review: ${phase.title}`,
|
|
417
|
+
expectDoneSentinel: true,
|
|
442
418
|
});
|
|
443
|
-
|
|
444
|
-
|
|
419
|
+
// Wait for the CLI's TUI to finish cold-start before pasting (6s
|
|
420
|
+
// covers Codex's slow cold-start). See doer-driver for rationale.
|
|
421
|
+
await new Promise((r) => setTimeout(r, 6000));
|
|
422
|
+
tmuxMgr.pasteBuffer(session.name, prompt);
|
|
423
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
424
|
+
tmuxMgr.sendKeys(session.name, ['Enter']);
|
|
425
|
+
// Failure-mode polling — same pattern as the doer.
|
|
426
|
+
const pollHandle = setInterval(() => {
|
|
427
|
+
try {
|
|
428
|
+
const pane = tmuxMgr.capturePane(session.name);
|
|
429
|
+
const err = errorDetector.inspect(session.name, candidate.lineage, pane);
|
|
430
|
+
if (err) {
|
|
431
|
+
const recoveryKeys = err.kind === 'permission_prompt' ? shim.recoverKeys?.permission_prompt : undefined;
|
|
432
|
+
if (recoveryKeys && recoveryKeys.length > 0) {
|
|
433
|
+
tmuxMgr.sendKeys(session.name, [...recoveryKeys]);
|
|
434
|
+
onEvent({
|
|
435
|
+
chatId,
|
|
436
|
+
type: 'cli_warning',
|
|
437
|
+
payload: {
|
|
438
|
+
phaseId: phase.id,
|
|
439
|
+
round,
|
|
440
|
+
role: 'reviewer',
|
|
441
|
+
agent: `${agentName}-${reviewerIdx}`,
|
|
442
|
+
recovered: err.kind,
|
|
443
|
+
keys: [...recoveryKeys],
|
|
444
|
+
detail: err.detail,
|
|
445
|
+
},
|
|
446
|
+
ts: Date.now(),
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
// Fire-and-forget — see doer-driver for rationale.
|
|
451
|
+
(0, cli_health_js_1.recordHealth)({
|
|
452
|
+
lineage: candidate.lineage,
|
|
453
|
+
status: (0, cli_health_js_1.kindToStatus)(err.kind),
|
|
454
|
+
message: err.message,
|
|
455
|
+
resetAt: err.resetAt,
|
|
456
|
+
}).catch((healthErr) => {
|
|
457
|
+
console.error(`[chorus] recordHealth failed for ${candidate.lineage}:`, healthErr);
|
|
458
|
+
});
|
|
459
|
+
onEvent({
|
|
460
|
+
chatId,
|
|
461
|
+
type: 'cli_error',
|
|
462
|
+
payload: {
|
|
463
|
+
phaseId: phase.id,
|
|
464
|
+
round,
|
|
465
|
+
role: 'reviewer',
|
|
466
|
+
agent: `${agentName}-${reviewerIdx}`,
|
|
467
|
+
error: err,
|
|
468
|
+
},
|
|
469
|
+
ts: Date.now(),
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
// ignore
|
|
476
|
+
}
|
|
477
|
+
}, 2000);
|
|
478
|
+
try {
|
|
479
|
+
const result = await (0, output_watcher_js_1.waitForAnswer)(answerFile, {
|
|
480
|
+
timeoutMs: phase.timeoutMs ?? template_schema_js_1.DEFAULT_TMUX_PHASE_TIMEOUT_MS,
|
|
481
|
+
doneSentinel: '## DONE',
|
|
482
|
+
});
|
|
483
|
+
if (!result.full || result.content.trim().length === 0) {
|
|
484
|
+
// Watcher resolved on timeout/silence with no real answer.
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
return (0, verdict_js_1.verdictFromReviewerText)(result.content);
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
// Timed out or watcher errored — no valid answer produced.
|
|
445
491
|
return null;
|
|
446
492
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
// Timed out or watcher errored — no valid answer produced.
|
|
451
|
-
return null;
|
|
493
|
+
finally {
|
|
494
|
+
clearInterval(pollHandle);
|
|
495
|
+
}
|
|
452
496
|
}
|
|
453
497
|
finally {
|
|
454
|
-
|
|
498
|
+
releaseSlot?.();
|
|
455
499
|
}
|
|
456
500
|
}
|
|
457
501
|
//# sourceMappingURL=reviewer-driver.js.map
|