amicus 4.7.0 → 4.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +948 -1
- package/README.md +3 -2
- package/docs/CITATIONS.md +114 -0
- package/docs/ROADMAP.md +33 -5
- package/docs/SHIMS.md +1 -1
- package/docs/configuration.md +3 -3
- package/docs/council.md +181 -34
- package/docs/doc-system.md +1 -1
- package/docs/publishing.md +2 -0
- package/docs/testing.md +1 -1
- package/docs/troubleshooting.md +6 -4
- package/docs/usage.md +15 -11
- package/electron/setup-ui-aliases.js +2 -2
- package/electron/workspace-ui/index.html +9 -0
- package/electron/workspace-ui/live-dead-seats.js +228 -0
- package/electron/workspace-ui/live-model.js +10 -236
- package/electron/workspace-ui/live-seats.js +126 -0
- package/electron/workspace-ui/workspace-app.js +6 -41
- package/electron/workspace-ui/workspace-banners.js +95 -0
- package/electron/workspace-ui/workspace-lazy.js +55 -12
- package/electron/workspace-ui/workspace-matrix.js +2 -2
- package/electron/workspace-ui/workspace-panels.js +42 -10
- package/electron/workspace-ui/workspace-render.js +2 -2
- package/electron/workspace-ui/workspace-seats.js +101 -17
- package/package.json +6 -3
- package/schemas/council-run-live.schema.json +1 -0
- package/schemas/council-run.schema.json +19 -0
- package/schemas/council-tally.schema.json +34 -2
- package/schemas/council-verdict.schema.json +15 -0
- package/scripts/postinstall.js +6 -3
- package/scripts/setup-hooks.js +49 -3
- package/skills/second-opinion/COUNCIL-DESIGN.md +9 -4
- package/skills/second-opinion/MANUAL-ORCHESTRATION.md +1 -1
- package/skills/second-opinion/SKILL.md +25 -11
- package/skills/sidecar/SKILL.md +3 -1
- package/src/cli-handlers-doctor.js +7 -0
- package/src/cli-handlers-resume-continue.js +20 -0
- package/src/council/anonymize.js +80 -11
- package/src/council/briefings-chair.js +272 -0
- package/src/council/briefings-stage2.js +12 -140
- package/src/council/debate.js +120 -15
- package/src/council/ledger-join.js +284 -0
- package/src/council/ledger-stats.js +100 -0
- package/src/council/ledger.js +177 -88
- package/src/council/peer-split.js +196 -0
- package/src/council/report-html.js +12 -5
- package/src/council/report-md.js +146 -0
- package/src/council/report.js +188 -112
- package/src/council/run-assemble.js +100 -79
- package/src/council/run-chair.js +17 -1
- package/src/council/run-debate-revote.js +268 -0
- package/src/council/run-debate.js +92 -102
- package/src/council/run-finish.js +70 -0
- package/src/council/run-launch.js +56 -18
- package/src/council/run-retry-group.js +266 -0
- package/src/council/run-retry-keys.js +74 -0
- package/src/council/run-retry-launch.js +55 -0
- package/src/council/run-retry-notes.js +65 -13
- package/src/council/run-retry.js +179 -162
- package/src/council/run-stage1-launch.js +27 -10
- package/src/council/run-stage1-rows.js +220 -0
- package/src/council/run-stage1-superseded.js +156 -0
- package/src/council/run-stage2.js +65 -5
- package/src/council/run-stages.js +72 -69
- package/src/council/run-state.js +1 -1
- package/src/council/run-stats-entry.js +71 -0
- package/src/council/run-verdict-files.js +52 -0
- package/src/council/run.js +42 -52
- package/src/council/seats.js +262 -0
- package/src/council/stage1-bind.js +142 -0
- package/src/council/street-cred.js +258 -0
- package/src/council/tally.js +100 -49
- package/src/council/verdict.js +49 -1
- package/src/headless.js +83 -6
- package/src/mcp-tools.js +71 -1
- package/src/observe/council-legs.js +7 -1
- package/src/observe/live-doc.js +3 -3
- package/src/sidecar/continue.js +8 -23
- package/src/sidecar/fanout-leg.js +22 -1
- package/src/sidecar/fanout-retry.js +15 -3
- package/src/sidecar/fanout-wave-io.js +26 -1
- package/src/sidecar/fanout.js +4 -10
- package/src/sidecar/leg-ids.js +19 -0
- package/src/sidecar/models-probe.js +7 -4
- package/src/sidecar/reopen-spend.js +32 -0
- package/src/sidecar/resume.js +1 -1
- package/src/sidecar/setup.js +13 -2
- package/src/utils/config.js +109 -11
- package/src/utils/curated-models.js +17 -2
- package/src/utils/degrade.js +5 -0
- package/src/utils/doctor-engine-check.js +32 -0
- package/src/utils/engine-install-scan.js +98 -15
- package/src/utils/engine-repair.js +96 -2
- package/src/utils/no-output-backstop.js +1 -1
- package/src/utils/remediation-hints.js +42 -0
- package/src/utils/session-index-prune.js +297 -0
- package/src/utils/session-metadata-tmp-sweep.js +1 -1
- package/src/workspace/artifact-guard.js +8 -114
- package/src/workspace/artifact-names.js +222 -0
- package/src/workspace/fold-format.js +9 -6
- package/src/workspace/live-normalize.js +6 -2
- package/src/workspace/matrix-model.js +141 -19
- package/src/workspace/run-detail.js +27 -1
- package/src/workspace/seat-space.js +143 -0
package/src/council/run-retry.js
CHANGED
|
@@ -14,142 +14,64 @@
|
|
|
14
14
|
* only.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
const briefings = require('./briefings');
|
|
18
17
|
const { materializeReviews, isAbortExit } = require('./run-launch');
|
|
19
18
|
const runState = require('./run-state');
|
|
19
|
+
const { resolveNoOutputBackstopMs } = require('../utils/no-output-backstop');
|
|
20
20
|
const { waveStillDeadNote, srcLegStillDeadNote, retryLegStillDeadNote, missingLegStillDeadNote }
|
|
21
21
|
= require('./run-retry-notes');
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Dedup helper (Task-4 review hardening): the same seat can arrive twice in
|
|
33
|
-
* one grouping pass — two dead legs naming it, or a dead wave and a dead leg
|
|
34
|
-
* both naming it. One seat must still mean ONE `firstFailures` entry — first
|
|
35
|
-
* occurrence wins — while every SOURCE record is kept regardless (srcWaves/
|
|
36
|
-
* srcLegs are the audit trail and are never deduped). The critic unit's
|
|
37
|
-
* `.models` is fixed at creation (there is only ever one critic seat), so
|
|
38
|
-
* only bench/lens units grow `.models` here — the critic call sites pass
|
|
39
|
-
* `trackModel: false`.
|
|
40
|
-
*/
|
|
41
|
-
function recordFailure(unit, seat, ff, trackModel = true) {
|
|
42
|
-
if (unit.firstFailures.some(f => f.seat === seat)) { return; }
|
|
43
|
-
unit.firstFailures.push(ff);
|
|
44
|
-
if (trackModel) { unit.models.push(seat); }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Group Stage-1 losses into retry units. Pure — no I/O.
|
|
49
|
-
* Bench losses (a dead bench wave's models + dead bench legs) collapse into
|
|
50
|
-
* ONE retry wave; the critic and each lens retry as solos (their briefings
|
|
51
|
-
* differ). Stable order: bench, critic, lenses ascending. The critic matches
|
|
52
|
-
* on EITHER carrier — waveId convention or model — mirroring
|
|
53
|
-
* verdict.js summarizeSeatLoss.
|
|
54
|
-
*/
|
|
55
|
-
function groupStage1Losses(o, deadWaves = [], deadLegs = []) {
|
|
56
|
-
const isCriticWave = (w) =>
|
|
57
|
-
w.waveId === `${o.runId}-c1` || (!!o.critic && (w.models || []).includes(o.critic));
|
|
58
|
-
const bench = { unit: 'bench', waveId: `${o.runId}-s1r1`, retryOfWaveId: `${o.runId}-s1`,
|
|
59
|
-
models: [], firstFailures: [], srcWaves: [], srcLegs: [] };
|
|
60
|
-
const lensUnits = new Map(); // lensIndex (number, or null for unmappable) -> unit
|
|
61
|
-
const criticUnit = { unit: 'critic', waveId: `${o.runId}-c1r1`, retryOfWaveId: `${o.runId}-c1`,
|
|
62
|
-
models: o.critic ? [o.critic] : [], firstFailures: [], srcWaves: [], srcLegs: [] };
|
|
63
|
-
|
|
64
|
-
const lensUnitFor = (i) => {
|
|
65
|
-
if (!lensUnits.has(i)) {
|
|
66
|
-
// Task-4 review hardening: an unmappable loss (lensIndexOf resolved
|
|
67
|
-
// neither the waveId convention nor a model-roster membership) must
|
|
68
|
-
// still be GROUPABLE — dropping it here would let it vanish before the
|
|
69
|
-
// orchestrator ever sees it — but must not manufacture a fake
|
|
70
|
-
// `-lnullr1` waveId. The orchestrator refuses to launch any unit with
|
|
71
|
-
// `lensIndex === null` and routes its sources to skipped instead.
|
|
72
|
-
lensUnits.set(i, i === null
|
|
73
|
-
? { unit: 'lens', lensIndex: null, waveId: null, retryOfWaveId: null,
|
|
74
|
-
models: [], firstFailures: [], srcWaves: [], srcLegs: [] }
|
|
75
|
-
: { unit: 'lens', lensIndex: i, waveId: `${o.runId}-l${i}r1`,
|
|
76
|
-
retryOfWaveId: `${o.runId}-l${i}`, models: [], firstFailures: [], srcWaves: [], srcLegs: [] });
|
|
77
|
-
}
|
|
78
|
-
return lensUnits.get(i);
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
for (const w of deadWaves) {
|
|
82
|
-
const models = w.models || [];
|
|
83
|
-
if (o.lenses) {
|
|
84
|
-
const u = lensUnitFor(lensIndexOf(o, w.waveId, models[0]));
|
|
85
|
-
u.srcWaves.push(w);
|
|
86
|
-
models.forEach(seat => recordFailure(u, seat, { seat, class: 'wave', waveId: w.waveId, reason: w.reason }));
|
|
87
|
-
} else if (isCriticWave(w)) {
|
|
88
|
-
criticUnit.srcWaves.push(w);
|
|
89
|
-
recordFailure(criticUnit, o.critic,
|
|
90
|
-
{ seat: o.critic, class: 'wave', waveId: w.waveId, reason: w.reason }, false);
|
|
91
|
-
} else {
|
|
92
|
-
bench.srcWaves.push(w);
|
|
93
|
-
models.forEach(seat => recordFailure(bench, seat, { seat, class: 'wave', waveId: w.waveId, reason: w.reason }));
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
for (const leg of deadLegs) {
|
|
97
|
-
const seat = leg.modelInput || leg.model;
|
|
98
|
-
const ff = { seat, class: 'leg', status: leg.status, reason: leg.error || null };
|
|
99
|
-
if (o.lenses) {
|
|
100
|
-
const u = lensUnitFor(lensIndexOf(o, null, seat));
|
|
101
|
-
u.srcLegs.push(leg);
|
|
102
|
-
recordFailure(u, seat, ff);
|
|
103
|
-
} else if (o.critic && seat === o.critic) {
|
|
104
|
-
criticUnit.srcLegs.push(leg);
|
|
105
|
-
recordFailure(criticUnit, seat, ff, false);
|
|
106
|
-
} else {
|
|
107
|
-
bench.srcLegs.push(leg);
|
|
108
|
-
recordFailure(bench, seat, ff);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const out = [];
|
|
113
|
-
// Task-4 review hardening: gate on whether the unit received any SOURCE
|
|
114
|
-
// record, not on firstFailures.length — a zero-model dead wave contributes
|
|
115
|
-
// a srcWaves entry but nothing to firstFailures/models (nothing for the
|
|
116
|
-
// `.forEach` above to iterate), and must still surface here so the
|
|
117
|
-
// orchestrator can route it to skipped instead of it vanishing silently.
|
|
118
|
-
if (bench.srcWaves.length > 0 || bench.srcLegs.length > 0) { out.push(bench); }
|
|
119
|
-
if (criticUnit.srcWaves.length > 0 || criticUnit.srcLegs.length > 0) { out.push(criticUnit); }
|
|
120
|
-
// Coordinator-review MINOR-7a: null sorts LAST (Infinity), not first (0) —
|
|
121
|
-
// an unmappable loss is not "lens index 0"; it should not perturb the
|
|
122
|
-
// ascending order of the real, well-indexed lens retries.
|
|
123
|
-
out.push(...[...lensUnits.values()].sort((a, b) => (a.lensIndex ?? Infinity) - (b.lensIndex ?? Infinity)));
|
|
124
|
-
return out;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/** The briefing a retry unit re-issues — same builders Stage 1 used. */
|
|
128
|
-
function briefingFor(o, unit) {
|
|
129
|
-
if (unit.unit === 'critic') { return briefings.buildCriticBriefing({ briefing: o.briefing, date: o.date }); }
|
|
130
|
-
if (unit.unit === 'lens') {
|
|
131
|
-
return briefings.buildLensBriefing({ lens: o.lenses[unit.lensIndex - 1], briefing: o.briefing, date: o.date });
|
|
132
|
-
}
|
|
133
|
-
return briefings.buildSeatBriefing({ briefing: o.briefing, date: o.date });
|
|
134
|
-
}
|
|
22
|
+
// briefingFor + bindRetryWave live in ./run-retry-launch (v4.8 T-A2 split); the pad/bind core it wraps is stage1-bind.js :: bindPaddedWave (SI-27).
|
|
23
|
+
const { briefingFor, bindRetryWave } = require('./run-retry-launch');
|
|
24
|
+
// Loss grouping lives in ./run-retry-group (v4.8 PR0 size-gate split).
|
|
25
|
+
// groupStage1Losses is re-exported below — run-retry.test.js imports it
|
|
26
|
+
// from here.
|
|
27
|
+
const { groupStage1Losses, planStillDeadSources, seatKey, twinAliases, legLossKey,
|
|
28
|
+
srcLegClaimer } = require('./run-retry-group');
|
|
135
29
|
|
|
136
30
|
/**
|
|
137
31
|
* The retry pass. Serial by design (spec D-order: bench, critic, lenses) —
|
|
138
32
|
* the per-wave-fallback path, where waves actually die, is exactly where
|
|
139
33
|
* concurrent relaunches would race the same server start again.
|
|
140
34
|
*/
|
|
141
|
-
async function retryStage1Losses(ctx, { deadWaves = [], deadLegs = [],
|
|
35
|
+
async function retryStage1Losses(ctx, { deadWaves = [], deadLegs = [],
|
|
36
|
+
counts = { reviewed: 0, total: 0 }, seatOf = new Map() } = {}) {
|
|
142
37
|
const { o, launchers } = ctx;
|
|
143
|
-
|
|
38
|
+
// v4.8 T-A6 (SI-TWINS): THE run's one `twinAliases`. It was derived independently at four
|
|
39
|
+
// sites across three files; now it is made here and threaded to all of them — the two
|
|
40
|
+
// run-retry-group.js consumers below by argument, and run-stage1-rows.js through `out.twins`,
|
|
41
|
+
// which run-stages.js hands straight to `pushDeadSeatRows`. Two collections that disagree is
|
|
42
|
+
// the desync mutants DESYNCLEG and DESYNCPLAN pin: a retried twin re-acquires its own FIRST
|
|
43
|
+
// leg, which already carries a `superseded` row, so that leg is billed twice.
|
|
44
|
+
const twins = twinAliases(o.seats); // the roster's repeated aliases (v4.8 T2.2)
|
|
45
|
+
// `seatOf`/`orphanLegs` are part of the CONTRACT, not a debugging aid: a
|
|
46
|
+
// recovered leg is a RETRY-wave object Stage-1's seatOf has never seen, so
|
|
47
|
+
// without publishing these bindings every healed seat re-materializes
|
|
48
|
+
// unattributed downstream (spec §4.4).
|
|
49
|
+
// `attemptedSeats` (v4.8 PR2b Task 8): the seat-keyed "was this seat actually
|
|
50
|
+
// retried?" gate. Structured data, never a scan of stillDeadNotes — the full
|
|
51
|
+
// rationale sits at run-stage1-rows.js's fallback, its only consumer.
|
|
52
|
+
// `twins` rides on the return for the same reason `seatOf` does: the consumer must ask with
|
|
53
|
+
// the collection this pass minted with, not one it re-derives (v4.8 T-A6).
|
|
54
|
+
const out = { aborted: null, recoveredLegs: [], stillDeadNotes: [], twins,
|
|
144
55
|
stillDeadWaves: [], stillDeadLegs: [], skippedDeadWaves: [], skippedDeadLegs: [],
|
|
145
|
-
stillDeadRetryLegs: [] };
|
|
56
|
+
stillDeadRetryLegs: [], seatOf: new Map(), orphanLegs: [], attemptedSeats: new Set() };
|
|
57
|
+
// Task 5 (#129): SL-2 retries the SAME model under the SAME conditions, so a
|
|
58
|
+
// latency failure is structurally unhealable. Double the window, clamped to
|
|
59
|
+
// the leg timeout so the failure CLASS stays NO_OUTPUT_BACKSTOP rather than
|
|
60
|
+
// silently becoming an ordinary timeout at a low --timeout. 2*0 === 0 keeps
|
|
61
|
+
// the disable hatch. (o.timeout || 15) * 60 * 1000 mirrors fanout.js:254.
|
|
62
|
+
// ⚠️ #135 C0 took this 240s -> 600s; deliberate, see CHANGELOG (council A1, PR #182).
|
|
63
|
+
const legTimeoutMs = (o.timeout || 15) * 60 * 1000;
|
|
64
|
+
const escalatedBackstopMs = Math.min(
|
|
65
|
+
2 * (Number.isFinite(o.noOutputBackstopMs) ? o.noOutputBackstopMs : resolveNoOutputBackstopMs()),
|
|
66
|
+
legTimeoutMs,
|
|
67
|
+
);
|
|
146
68
|
|
|
147
|
-
for (const unit of groupStage1Losses(o, deadWaves, deadLegs)) {
|
|
69
|
+
for (const unit of groupStage1Losses(o, deadWaves, deadLegs, seatOf, twins)) {
|
|
148
70
|
// Task-4 review hardening: a unit this pass cannot even ATTEMPT — an
|
|
149
71
|
// unmappable lens loss (no carrier resolved an index), a lens index
|
|
150
72
|
// outside the run's actual lens roster (coordinator-review MINOR-7b: a
|
|
151
73
|
// malformed waveId like "...-l99" must not become an out-of-range
|
|
152
|
-
// `o.lenses[98]` access inside briefingFor), or a unit whose sources
|
|
74
|
+
// `o.lenses[98]` access inside run-retry-launch.js's briefingFor), or a unit whose sources
|
|
153
75
|
// named zero models — is never launched. Its sources fall back to the
|
|
154
76
|
// ordinary skipped-loss path so the caller's normal degrade notes still
|
|
155
77
|
// fire; being unmappable is not an exemption from the record.
|
|
@@ -171,7 +93,8 @@ async function retryStage1Losses(ctx, { deadWaves = [], deadLegs = [], counts =
|
|
|
171
93
|
councilRunId: o.runId, councilName: o.councilName,
|
|
172
94
|
tag: o.tag, // v4.7 F8 D16: rides the same forward as councilRunId/councilName.
|
|
173
95
|
fallback: o.fallback, catalog: o.catalog,
|
|
174
|
-
waveId: unit.waveId, retryOfWaveId: unit.retryOfWaveId, prompt: briefingFor(o, unit)
|
|
96
|
+
waveId: unit.waveId, retryOfWaveId: unit.retryOfWaveId, prompt: briefingFor(o, unit),
|
|
97
|
+
noOutputBackstopMs: escalatedBackstopMs };
|
|
175
98
|
// Dispatch by UNIT TYPE, not model count (spec §4: bench is always a wave —
|
|
176
99
|
// even down to its last surviving seat — critic/lens are always solos).
|
|
177
100
|
// A model-count proxy (`models.length === 1`) is wrong for a bench unit
|
|
@@ -184,6 +107,56 @@ async function retryStage1Losses(ctx, { deadWaves = [], deadLegs = [], counts =
|
|
|
184
107
|
if (isAbortExit(res.exitCode)) { out.aborted = res.exitCode; return out; }
|
|
185
108
|
|
|
186
109
|
const legs = (res.wave && Array.isArray(res.wave.legs)) ? res.wave.legs : [];
|
|
110
|
+
const { retrySeatOf, orphanLegs } = bindRetryWave(unit, legs);
|
|
111
|
+
for (const [l, s] of retrySeatOf) { out.seatOf.set(l, s); }
|
|
112
|
+
// A retry leg that matches no roster slot is the same attribution failure
|
|
113
|
+
// Stage-1 announces — but this module emits heals ONLY and never notes a
|
|
114
|
+
// degrade (see the @module docblock), so it is REPORTED here and emitted by
|
|
115
|
+
// the caller, exactly as stillDeadNotes already are.
|
|
116
|
+
for (const leg of orphanLegs) { out.orphanLegs.push({ waveId: unit.waveId, leg }); }
|
|
117
|
+
// Every seat this unit LAUNCHED for, keyed exactly the way recordFailure
|
|
118
|
+
// keyed it: the identified seat's id, else the alias.
|
|
119
|
+
// ⚠️ v4.8 T-A4: a key is NOT one slot. Since T2.2 two UNATTRIBUTABLE twins on one alias
|
|
120
|
+
// mint two slots that both key by that alias, and a first-wins entry collapsed them —
|
|
121
|
+
// announcing ONE dead seat where two died (B1) while every note built from it read slot
|
|
122
|
+
// 0's firstFailure (B2). So an entry carries a slot COUNT and its OWN per-slot
|
|
123
|
+
// firstFailures. Only `unit.models` — the launch PLAN — mints: srcWaves/srcLegs are the
|
|
124
|
+
// union safety net the reconcile below rests on, and must leave a key at >=1 without
|
|
125
|
+
// inflating one that arrives through both. Inflating it manufactures a phantom dead seat.
|
|
126
|
+
// ⚠️ v4.8 H4: an alias is no longer a seat identity. A `new Map(unit.models
|
|
127
|
+
// .map(...))` lookup overwrites the duplicate key, so a twin bench resolves
|
|
128
|
+
// BOTH still-lost seats to the LAST twin — an affirmative mis-attribution,
|
|
129
|
+
// strictly worse than the `null` that says "unidentified". Every feeder
|
|
130
|
+
// below must be seat-keyed for the same reason: a MIX (['x#1','x#2','x'])
|
|
131
|
+
// can never match `seenSeats`, so a run where both twins healed would emit
|
|
132
|
+
// a phantom dead-leg degrade and exit 2.
|
|
133
|
+
const launched = new Map(); // key -> {alias, seat, slots, ffs}; alias is what notes render
|
|
134
|
+
const addLaunched = (s, alias, ff, mint = false) => {
|
|
135
|
+
const k = seatKey(s, alias);
|
|
136
|
+
if (!launched.has(k)) { launched.set(k, { alias, seat: s || null, slots: 0, ffs: [] }); }
|
|
137
|
+
if (mint) { const rec = launched.get(k); rec.slots += 1; rec.ffs.push(ff || null); }
|
|
138
|
+
};
|
|
139
|
+
// NOT lockstep by construction — recordFailure pushes `firstFailures` always but models/seats
|
|
140
|
+
// only `if (trackModel)`. Slot i is one seat's whole record EMERGENTLY, on two measured facts:
|
|
141
|
+
// no unit MIXES the flag (bench/lens sites pass true, both critic sites false), so a tracking
|
|
142
|
+
// unit grows all three together; and the critic unit's `firstFailures` caps at 1 — its INEXACT
|
|
143
|
+
// arm needs `twins.has(o.critic)` with a null `criticSeatObj`, and `twins` IS `twinAliases(o.seats)`,
|
|
144
|
+
// so the first implies the second non-null. The one MEASURED break (a `-c1` wave, no `o.critic`:
|
|
145
|
+
// ff 1 / models 0) the `models.length === 0` skip above ate. Pinned: run-retry-lockstep.test.js.
|
|
146
|
+
unit.models.forEach((m, i) => addLaunched(unit.seats[i] || null, m, unit.firstFailures[i], true));
|
|
147
|
+
for (const w of unit.srcWaves) {
|
|
148
|
+
(w.models || []).forEach((m, i) => addLaunched((w.seats || [])[i] || null, m));
|
|
149
|
+
}
|
|
150
|
+
for (const l of unit.srcLegs) { addLaunched(seatOf.get(l) || null, l.modelInput || l.model); }
|
|
151
|
+
// A Stage-1 source leg's key, from the binding Stage 1 published for it.
|
|
152
|
+
const srcLegKey = l => seatKey(seatOf.get(l) || null, l.modelInput || l.model);
|
|
153
|
+
// v4.8 T2.2: ONE source per still-dead outcome (srcLegs.find handed the first one
|
|
154
|
+
// out twice, so the second orphaned twin left no record), and the ROW key that seat
|
|
155
|
+
// will be looked up by downstream. Both move with recordFailure's dedup or a retried
|
|
156
|
+
// twin re-acquires its own first leg — which already has a `superseded` row, so that
|
|
157
|
+
// leg's cost lands in runStats twice while its retry leg lands nowhere.
|
|
158
|
+
const claimSrc = srcLegClaimer(unit.srcLegs, srcLegKey);
|
|
159
|
+
const srcRowKey = l => legLossKey(seatOf.get(l) || null, l.modelInput || l.model, l, twins);
|
|
187
160
|
if (legs.length === 0) {
|
|
188
161
|
// The retry wave itself died wholesale — final failure keeps each
|
|
189
162
|
// source's granularity (D5): wave-origin stays a dead-wave, leg-origin
|
|
@@ -195,32 +168,46 @@ async function retryStage1Losses(ctx, { deadWaves = [], deadLegs = [], counts =
|
|
|
195
168
|
// the "wave wins" precedent from the grouping-level dedup); a seat
|
|
196
169
|
// already covered by its wave's note is skipped when its srcLeg is
|
|
197
170
|
// reached.
|
|
198
|
-
|
|
171
|
+
// v4.8 PR5c: the srcWave/srcLeg dedup lives in run-retry-group.planStillDeadSources —
|
|
172
|
+
// an unidentified wave slot and a bound leg for the same seat key in DIFFERENT
|
|
173
|
+
// spaces, so a naive test announces one seat twice. See that helper for the budget.
|
|
174
|
+
const plan = planStillDeadSources(unit, seatOf, o.seats, twins);
|
|
199
175
|
for (const w of unit.srcWaves) {
|
|
200
176
|
out.stillDeadNotes.push(waveStillDeadNote(w, unit));
|
|
201
177
|
out.stillDeadWaves.push(w);
|
|
202
|
-
for (const m of (w.models || [])) { notedSeats.add(m); }
|
|
203
178
|
}
|
|
204
|
-
for (const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
out.stillDeadNotes.push(srcLegStillDeadNote(l, unit, counts));
|
|
209
|
-
out.stillDeadLegs.push(l);
|
|
179
|
+
for (const k of plan.attempted) { out.attemptedSeats.add(k); }
|
|
180
|
+
for (const { leg, seatId } of plan.legs) {
|
|
181
|
+
out.stillDeadNotes.push(srcLegStillDeadNote(leg, unit, counts, seatId));
|
|
182
|
+
out.stillDeadLegs.push(leg);
|
|
210
183
|
}
|
|
211
184
|
continue;
|
|
212
185
|
}
|
|
213
|
-
const usable = new Set(materializeReviews(o.runDir, legs).map(m => m.leg));
|
|
214
|
-
|
|
215
|
-
|
|
186
|
+
const usable = new Set(materializeReviews(o.runDir, legs, retrySeatOf).map(m => m.leg));
|
|
187
|
+
// Seat-keyed off THIS wave's own bindings (an unbound leg keys by alias,
|
|
188
|
+
// matching how its unidentified roster slot was keyed above).
|
|
189
|
+
// v4.8 T-A4: a COUNT, not a presence Set — two unattributable twins share one key, so
|
|
190
|
+
// "was it seen at all?" cannot tell one returned leg from two. Filled by the loop below,
|
|
191
|
+
// which visits every leg (including one skipped for having no slot, as `legs.map` did).
|
|
192
|
+
const seenSeats = new Map(); // key -> legs this wave returned for it
|
|
193
|
+
const lostWaveSeats = new Map(); // waveId -> [{alias, seat}] still lost from a wave-origin
|
|
216
194
|
for (const leg of legs) {
|
|
217
|
-
const seat = leg.modelInput || leg.model;
|
|
218
|
-
const
|
|
195
|
+
const seat = leg.modelInput || leg.model; // ALIAS — what every note renders
|
|
196
|
+
const bound = retrySeatOf.get(leg) || null;
|
|
197
|
+
const key = seatKey(bound, seat);
|
|
198
|
+
// This leg answers the key's NEXT unanswered slot, and reads THAT slot's own
|
|
199
|
+
// firstFailure (T-A4 B2). Counted BEFORE the `!ff` skip below, so the tally the
|
|
200
|
+
// reconcile reads still means "legs this wave returned for the key".
|
|
201
|
+
const slot = seenSeats.get(key) || 0;
|
|
202
|
+
seenSeats.set(key, slot + 1);
|
|
203
|
+
const ff = (launched.get(key) || { ffs: [] }).ffs[slot] || null;
|
|
219
204
|
// SL-2 fix-wave: a retry response should only ever name seats THIS unit
|
|
220
|
-
// launched for (unit.models is
|
|
221
|
-
//
|
|
222
|
-
// with no firstFailures entry,
|
|
223
|
-
//
|
|
205
|
+
// launched for (`unit.models` is index-parallel to `firstFailures` EMERGENTLY,
|
|
206
|
+
// for the two reasons the mint above states) — but if a leg turns up for a seat
|
|
207
|
+
// with no firstFailures entry, or (v4.8 T-A4) beyond the LAST slot its key
|
|
208
|
+
// minted, that seat never lost its seat in the first place. `slots` is an
|
|
209
|
+
// upper bound as well as a lower one, and this is where the upper half is
|
|
210
|
+
// spent. Skip it entirely: no heal (it would fabricate an "ended
|
|
224
211
|
// 'unknown'" why for a seat that never failed) and no still-dead note —
|
|
225
212
|
// the seat's first-attempt review stands untouched, rather than this
|
|
226
213
|
// stray leg doubling it into a duplicate bench entry alongside the real one.
|
|
@@ -229,20 +216,32 @@ async function retryStage1Losses(ctx, { deadWaves = [], deadLegs = [], counts =
|
|
|
229
216
|
out.recoveredLegs.push(leg);
|
|
230
217
|
ctx.degrade.note({ channel: 'stage1-retry', kind: 'heal',
|
|
231
218
|
what: `seat ${seat} reviewed on retry`,
|
|
232
|
-
|
|
219
|
+
// A 'missing' ff (a seat whose leg never came back) has a reason and a
|
|
220
|
+
// waveId but NEVER a status — the leg arm would render "ended
|
|
221
|
+
// 'undefined'" for a seat that never had a leg at all.
|
|
222
|
+
why: ff.class === 'wave'
|
|
233
223
|
? `its first wave ${ff.waveId} produced no legs (${ff.reason}) and was relaunched once`
|
|
234
|
-
:
|
|
224
|
+
: ff.class === 'missing'
|
|
225
|
+
? `${ff.reason} in wave ${ff.waveId}, and it was relaunched once`
|
|
226
|
+
: `its first leg ended '${ff ? ff.status : 'unknown'}' with no usable output and was relaunched once`,
|
|
235
227
|
effect: 'The seat is in this council; nothing was lost',
|
|
236
228
|
data: { seat, retryWaveId: unit.waveId, retryOfWaveId: unit.retryOfWaveId, firstFailure: ff } });
|
|
237
229
|
} else {
|
|
238
230
|
out.stillDeadNotes.push(retryLegStillDeadNote(seat, ff, leg, unit, counts));
|
|
231
|
+
out.attemptedSeats.add(key);
|
|
239
232
|
out.stillDeadRetryLegs.push(leg);
|
|
240
|
-
|
|
233
|
+
// Both WAVE-origin classes route here: a 'missing' loss carries a
|
|
234
|
+
// waveId and no srcLeg, so the else branch would drop it from every
|
|
235
|
+
// array. Task-6 carry: an UNBINDABLE retry leg leaves `bound` null
|
|
236
|
+
// where the roster slot was known — fall back to the launched record
|
|
237
|
+
// rather than publishing seats:[null]. Never a guess: this line is
|
|
238
|
+
// reachable only when `key` is already in `launched`.
|
|
239
|
+
if (ff.class !== 'leg') {
|
|
241
240
|
if (!lostWaveSeats.has(ff.waveId)) { lostWaveSeats.set(ff.waveId, []); }
|
|
242
|
-
lostWaveSeats.get(ff.waveId).push(seat);
|
|
241
|
+
lostWaveSeats.get(ff.waveId).push({ alias: seat, seat: bound || (launched.get(key) || {}).seat || null });
|
|
243
242
|
} else {
|
|
244
|
-
const src =
|
|
245
|
-
if (src) { out.stillDeadLegs.push(src); }
|
|
243
|
+
const src = claimSrc(key);
|
|
244
|
+
if (src) { out.stillDeadLegs.push(src); out.attemptedSeats.add(srcRowKey(src)); }
|
|
246
245
|
}
|
|
247
246
|
}
|
|
248
247
|
}
|
|
@@ -250,31 +249,49 @@ async function retryStage1Losses(ctx, { deadWaves = [], deadLegs = [], counts =
|
|
|
250
249
|
// that came back WITH a leg record. A partial wave return (unit models
|
|
251
250
|
// [a,b], the wave comes back with only a's leg) leaves 'b' invisible to
|
|
252
251
|
// that loop entirely — no heal, no still-dead note, no skip: it would
|
|
253
|
-
// vanish from every array. Reconcile against the
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
for (const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
252
|
+
// vanish from every array. Reconcile against `launched` — the union of
|
|
253
|
+
// unit.models, every srcWave's models and every srcLeg's seat, not
|
|
254
|
+
// unit.models alone, so this holds even if some future change to the
|
|
255
|
+
// grouping made unit.models an incomplete union — so every launched SLOT
|
|
256
|
+
// lands in exactly one of recovered / still-dead / skipped. Slot-indexed:
|
|
257
|
+
// there is no leg here to read a binding off, so the seat comes from the
|
|
258
|
+
// roster slot itself, never from an alias lookup.
|
|
259
|
+
for (const [key, rec] of launched) {
|
|
260
|
+
// v4.8 T-A4: `max(slots, 1) - seen` notes, not a presence test. `max(…, 1)` is what
|
|
261
|
+
// keeps the union net a net: feeders 2 and 3 mint no slot, so a key only THEY know
|
|
262
|
+
// still reconciles exactly ONCE, and a seat that arrives through both the launch plan
|
|
263
|
+
// and a srcLeg is counted by the plan alone. `rec.seat` is shared across a key's slots
|
|
264
|
+
// and that is sound: two slots can only share a key when NEITHER was identified (an
|
|
265
|
+
// identified slot keys by its own seat id), so every slot of such a key has seat null.
|
|
266
|
+
for (let i = seenSeats.get(key) || 0; i < Math.max(rec.slots, 1); i++) {
|
|
267
|
+
const ff = rec.ffs[i] || null;
|
|
268
|
+
out.stillDeadNotes.push(missingLegStillDeadNote(rec.alias, ff, unit, counts));
|
|
269
|
+
out.attemptedSeats.add(key);
|
|
270
|
+
if (ff && ff.class !== 'leg') { // wave-origin, incl. a 'missing' seat
|
|
271
|
+
if (!lostWaveSeats.has(ff.waveId)) { lostWaveSeats.set(ff.waveId, []); }
|
|
272
|
+
lostWaveSeats.get(ff.waveId).push({ alias: rec.alias, seat: rec.seat });
|
|
273
|
+
} else {
|
|
274
|
+
const src = claimSrc(key);
|
|
275
|
+
if (src) { out.stillDeadLegs.push(src); out.attemptedSeats.add(srcRowKey(src)); }
|
|
276
|
+
}
|
|
271
277
|
}
|
|
272
278
|
}
|
|
273
279
|
// Wave-origin seats still lost: the return-contract wave entry carries only
|
|
274
280
|
// the still-lost subset (a partially healed wave is not wholly dead).
|
|
281
|
+
// `seats` is narrowed in LOCKSTEP with `models`, or run-stage1-rows.js's
|
|
282
|
+
// dead-seat loop index-zips a healed seat against a lost seat's alias.
|
|
283
|
+
// Since v4.8 a wave can contribute SEVERAL srcWave records sharing one
|
|
284
|
+
// waveId (one per missing seat, stage1-bind.js's missingSeatDeadWave), so
|
|
285
|
+
// the lookup fires once per waveId — not once per record, which would push
|
|
286
|
+
// every still-lost seat of that wave N times.
|
|
287
|
+
const reconciled = new Set();
|
|
275
288
|
for (const w of unit.srcWaves) {
|
|
289
|
+
if (reconciled.has(w.waveId)) { continue; }
|
|
290
|
+
reconciled.add(w.waveId);
|
|
276
291
|
const lost = lostWaveSeats.get(w.waveId) || [];
|
|
277
|
-
if (lost.length > 0) {
|
|
292
|
+
if (lost.length > 0) {
|
|
293
|
+
out.stillDeadWaves.push({ ...w, models: lost.map(x => x.alias), seats: lost.map(x => x.seat) });
|
|
294
|
+
}
|
|
278
295
|
}
|
|
279
296
|
}
|
|
280
297
|
return out;
|
|
@@ -9,10 +9,17 @@
|
|
|
9
9
|
const briefings = require('./briefings');
|
|
10
10
|
const runState = require('./run-state');
|
|
11
11
|
const { isAbortExit } = require('./run-launch');
|
|
12
|
+
const { buildSeats } = require('./seats');
|
|
12
13
|
|
|
13
14
|
/** Launch all Stage-1 legs (wave + critic/lens solos), collect run docs. */
|
|
14
15
|
async function launchStage1(ctx) {
|
|
15
16
|
const { o, launchers } = ctx;
|
|
17
|
+
// Seat identity for THIS launch. run.js:133 sets o.seats from asm.preflightSeats;
|
|
18
|
+
// buildSeats is pure and total, so a direct require() caller or a legacy run dir
|
|
19
|
+
// reconstructs the same table rather than binding nothing (spec §4.3).
|
|
20
|
+
const seats = Array.isArray(o.seats) && o.seats.length > 0
|
|
21
|
+
? o.seats
|
|
22
|
+
: buildSeats(o.models, o.critic, o.lenses);
|
|
16
23
|
// `noCostGate` rides EVERY launch object in this file (here, the findings
|
|
17
24
|
// repair, the judge wave, the judge repair) — see run-launch.js's fanout call.
|
|
18
25
|
const common = {
|
|
@@ -37,27 +44,32 @@ async function launchStage1(ctx) {
|
|
|
37
44
|
o.models.forEach((m, i) => {
|
|
38
45
|
const waveId = `${o.runId}-l${i + 1}`;
|
|
39
46
|
record(waveId);
|
|
40
|
-
seated.push({ waveId, models: [m] });
|
|
47
|
+
seated.push({ waveId, models: [m], roster: seats.slice(i, i + 1) });
|
|
41
48
|
launches.push(launchers.launchSolo({
|
|
42
|
-
...common, model: m, waveId,
|
|
49
|
+
...common, model: m, waveId, seats: seated[seated.length - 1].roster,
|
|
43
50
|
prompt: briefings.buildLensBriefing({ lens: o.lenses[i], briefing: o.briefing, date: o.date }),
|
|
44
51
|
}));
|
|
45
52
|
});
|
|
46
53
|
} else {
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
54
|
+
const seats1 = o.models.filter(m => m !== o.critic);
|
|
55
|
+
if (seats1.length > 0) {
|
|
49
56
|
record(`${o.runId}-s1`);
|
|
50
|
-
|
|
57
|
+
// MUST mirror :54's `m !== o.critic` exactly. Filtering on `s.id !== o.criticSeat`
|
|
58
|
+
// instead would drop ONE twin where the alias filter drops BOTH, shifting every
|
|
59
|
+
// legId slot by one on a bench preflightSeats never saw.
|
|
60
|
+
seated.push({ waveId: `${o.runId}-s1`, models: seats1.slice(),
|
|
61
|
+
roster: seats.filter(s => s.alias !== o.critic) });
|
|
51
62
|
launches.push(launchers.launchWave({
|
|
52
|
-
...common, models:
|
|
63
|
+
...common, models: seats1, waveId: `${o.runId}-s1`, seats: seated[seated.length - 1].roster,
|
|
53
64
|
prompt: briefings.buildSeatBriefing({ briefing: o.briefing, date: o.date }),
|
|
54
65
|
}));
|
|
55
66
|
}
|
|
56
67
|
if (o.critic) {
|
|
57
68
|
record(`${o.runId}-c1`);
|
|
58
|
-
seated.push({ waveId: `${o.runId}-c1`, models: [o.critic]
|
|
69
|
+
seated.push({ waveId: `${o.runId}-c1`, models: [o.critic],
|
|
70
|
+
roster: seats.filter(s => s.alias === o.critic).slice(0, 1) });
|
|
59
71
|
launches.push(launchers.launchSolo({
|
|
60
|
-
...common, model: o.critic, waveId: `${o.runId}-c1`,
|
|
72
|
+
...common, model: o.critic, waveId: `${o.runId}-c1`, seats: seated[seated.length - 1].roster,
|
|
61
73
|
prompt: briefings.buildCriticBriefing({ briefing: o.briefing, date: o.date }),
|
|
62
74
|
}));
|
|
63
75
|
}
|
|
@@ -66,12 +78,17 @@ async function launchStage1(ctx) {
|
|
|
66
78
|
let aborted = null;
|
|
67
79
|
const legs = [];
|
|
68
80
|
const deadWaves = [];
|
|
81
|
+
const waves = [];
|
|
69
82
|
results.forEach((r, i) => {
|
|
70
83
|
ctx.addWave(r.wave);
|
|
71
84
|
const abort = isAbortExit(r.exitCode);
|
|
72
85
|
if (abort) { aborted = r.exitCode; }
|
|
73
86
|
const got = (r.wave && Array.isArray(r.wave.legs)) ? r.wave.legs : [];
|
|
74
87
|
legs.push(...got);
|
|
88
|
+
// Per-wave partition, captured BEFORE the flatten above erases attribution.
|
|
89
|
+
// bindSeats is called once per entry by the consumer; a wave that produced
|
|
90
|
+
// nothing still gets an entry so its roster is never lost.
|
|
91
|
+
waves.push({ waveId: seated[i].waveId, roster: seated[i].roster, legs: got });
|
|
75
92
|
// ⚠️ Step 10's uncovered half. A wave that died BEFORE its legs (the server
|
|
76
93
|
// never started; `database is locked`) contributes NOTHING to `legs`, so
|
|
77
94
|
// deadLegs cannot see it either — which is how run v441plan01 recorded
|
|
@@ -83,12 +100,12 @@ async function launchStage1(ctx) {
|
|
|
83
100
|
if (got.length > 0 || abort) { return; }
|
|
84
101
|
if (r.errorDoc && r.errorDoc.code === 'BUDGET_EXCEEDED') { return; }
|
|
85
102
|
deadWaves.push({
|
|
86
|
-
waveId: seated[i].waveId, models: seated[i].models,
|
|
103
|
+
waveId: seated[i].waveId, models: seated[i].models, seats: seated[i].roster,
|
|
87
104
|
reason: (r.wave && (r.wave.reason || r.wave.error))
|
|
88
105
|
|| (r.errorDoc && r.errorDoc.message) || 'the wave produced no legs',
|
|
89
106
|
});
|
|
90
107
|
});
|
|
91
|
-
return { aborted, legs, deadWaves };
|
|
108
|
+
return { aborted, legs, deadWaves, waves };
|
|
92
109
|
}
|
|
93
110
|
|
|
94
111
|
module.exports = { launchStage1 };
|