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