amicus 4.7.1 → 4.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +988 -1
- package/README.md +5 -4
- package/docs/CITATIONS.md +122 -0
- package/docs/ROADMAP.md +33 -5
- package/docs/SHIMS.md +1 -1
- package/docs/configuration.md +3 -3
- package/docs/council.md +179 -32
- package/docs/doc-system.md +1 -1
- package/docs/publishing.md +2 -0
- package/docs/troubleshooting.md +3 -3
- package/docs/usage.md +2 -2
- package/electron/ipc-setup.js +18 -2
- package/electron/main.js +46 -3
- package/electron/setup-ui-aliases.js +2 -2
- package/electron/setup-ui-model.js +99 -9
- package/electron/setup-ui-styles.js +22 -0
- package/electron/setup-ui.js +231 -29
- 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 +4 -1
- 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/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/src/cli-handlers-doctor.js +18 -14
- 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/parse-stage2.js +1 -1
- 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 +45 -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 +166 -161
- 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 +20 -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/fanout-leg.js +22 -1
- 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 +1 -1
- package/src/sidecar/setup.js +137 -2
- package/src/utils/alias-audit.js +81 -3
- 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-alias-check.js +152 -0
- package/src/utils/model-canonicalization.js +64 -0
- package/src/utils/model-shortlist.js +100 -0
- package/src/utils/no-output-backstop.js +1 -1
- package/src/utils/provider-default-picker.js +93 -45
- package/src/utils/provider-default-prompt.js +1 -1
- package/src/utils/quick-picks.js +2 -2
- package/src/utils/remediation-hints.js +37 -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 +30 -4
- package/src/workspace/seat-space.js +143 -0
package/src/council/run-retry.js
CHANGED
|
@@ -14,153 +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');
|
|
20
19
|
const { resolveNoOutputBackstopMs } = require('../utils/no-output-backstop');
|
|
21
20
|
const { waveStillDeadNote, srcLegStillDeadNote, retryLegStillDeadNote, missingLegStillDeadNote }
|
|
22
21
|
= require('./run-retry-notes');
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Dedup helper (Task-4 review hardening): the same seat can arrive twice in
|
|
34
|
-
* one grouping pass — two dead legs naming it, or a dead wave and a dead leg
|
|
35
|
-
* both naming it. One seat must still mean ONE `firstFailures` entry — first
|
|
36
|
-
* occurrence wins — while every SOURCE record is kept regardless (srcWaves/
|
|
37
|
-
* srcLegs are the audit trail and are never deduped). The critic unit's
|
|
38
|
-
* `.models` is fixed at creation (there is only ever one critic seat), so
|
|
39
|
-
* only bench/lens units grow `.models` here — the critic call sites pass
|
|
40
|
-
* `trackModel: false`.
|
|
41
|
-
*/
|
|
42
|
-
function recordFailure(unit, seat, ff, trackModel = true) {
|
|
43
|
-
if (unit.firstFailures.some(f => f.seat === seat)) { return; }
|
|
44
|
-
unit.firstFailures.push(ff);
|
|
45
|
-
if (trackModel) { unit.models.push(seat); }
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Group Stage-1 losses into retry units. Pure — no I/O.
|
|
50
|
-
* Bench losses (a dead bench wave's models + dead bench legs) collapse into
|
|
51
|
-
* ONE retry wave; the critic and each lens retry as solos (their briefings
|
|
52
|
-
* differ). Stable order: bench, critic, lenses ascending. The critic matches
|
|
53
|
-
* on EITHER carrier — waveId convention or model — mirroring
|
|
54
|
-
* verdict.js summarizeSeatLoss.
|
|
55
|
-
*/
|
|
56
|
-
function groupStage1Losses(o, deadWaves = [], deadLegs = []) {
|
|
57
|
-
const isCriticWave = (w) =>
|
|
58
|
-
w.waveId === `${o.runId}-c1` || (!!o.critic && (w.models || []).includes(o.critic));
|
|
59
|
-
const bench = { unit: 'bench', waveId: `${o.runId}-s1r1`, retryOfWaveId: `${o.runId}-s1`,
|
|
60
|
-
models: [], firstFailures: [], srcWaves: [], srcLegs: [] };
|
|
61
|
-
const lensUnits = new Map(); // lensIndex (number, or null for unmappable) -> unit
|
|
62
|
-
const criticUnit = { unit: 'critic', waveId: `${o.runId}-c1r1`, retryOfWaveId: `${o.runId}-c1`,
|
|
63
|
-
models: o.critic ? [o.critic] : [], firstFailures: [], srcWaves: [], srcLegs: [] };
|
|
64
|
-
|
|
65
|
-
const lensUnitFor = (i) => {
|
|
66
|
-
if (!lensUnits.has(i)) {
|
|
67
|
-
// Task-4 review hardening: an unmappable loss (lensIndexOf resolved
|
|
68
|
-
// neither the waveId convention nor a model-roster membership) must
|
|
69
|
-
// still be GROUPABLE — dropping it here would let it vanish before the
|
|
70
|
-
// orchestrator ever sees it — but must not manufacture a fake
|
|
71
|
-
// `-lnullr1` waveId. The orchestrator refuses to launch any unit with
|
|
72
|
-
// `lensIndex === null` and routes its sources to skipped instead.
|
|
73
|
-
lensUnits.set(i, i === null
|
|
74
|
-
? { unit: 'lens', lensIndex: null, waveId: null, retryOfWaveId: null,
|
|
75
|
-
models: [], firstFailures: [], srcWaves: [], srcLegs: [] }
|
|
76
|
-
: { unit: 'lens', lensIndex: i, waveId: `${o.runId}-l${i}r1`,
|
|
77
|
-
retryOfWaveId: `${o.runId}-l${i}`, models: [], firstFailures: [], srcWaves: [], srcLegs: [] });
|
|
78
|
-
}
|
|
79
|
-
return lensUnits.get(i);
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
for (const w of deadWaves) {
|
|
83
|
-
const models = w.models || [];
|
|
84
|
-
if (o.lenses) {
|
|
85
|
-
const u = lensUnitFor(lensIndexOf(o, w.waveId, models[0]));
|
|
86
|
-
u.srcWaves.push(w);
|
|
87
|
-
models.forEach(seat => recordFailure(u, seat, { seat, class: 'wave', waveId: w.waveId, reason: w.reason }));
|
|
88
|
-
} else if (isCriticWave(w)) {
|
|
89
|
-
criticUnit.srcWaves.push(w);
|
|
90
|
-
recordFailure(criticUnit, o.critic,
|
|
91
|
-
{ seat: o.critic, class: 'wave', waveId: w.waveId, reason: w.reason }, false);
|
|
92
|
-
} else {
|
|
93
|
-
bench.srcWaves.push(w);
|
|
94
|
-
models.forEach(seat => recordFailure(bench, seat, { seat, class: 'wave', waveId: w.waveId, reason: w.reason }));
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
for (const leg of deadLegs) {
|
|
98
|
-
const seat = leg.modelInput || leg.model;
|
|
99
|
-
const ff = { seat, class: 'leg', status: leg.status, reason: leg.error || null };
|
|
100
|
-
if (o.lenses) {
|
|
101
|
-
const u = lensUnitFor(lensIndexOf(o, null, seat));
|
|
102
|
-
u.srcLegs.push(leg);
|
|
103
|
-
recordFailure(u, seat, ff);
|
|
104
|
-
} else if (o.critic && seat === o.critic) {
|
|
105
|
-
criticUnit.srcLegs.push(leg);
|
|
106
|
-
recordFailure(criticUnit, seat, ff, false);
|
|
107
|
-
} else {
|
|
108
|
-
bench.srcLegs.push(leg);
|
|
109
|
-
recordFailure(bench, seat, ff);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const out = [];
|
|
114
|
-
// Task-4 review hardening: gate on whether the unit received any SOURCE
|
|
115
|
-
// record, not on firstFailures.length — a zero-model dead wave contributes
|
|
116
|
-
// a srcWaves entry but nothing to firstFailures/models (nothing for the
|
|
117
|
-
// `.forEach` above to iterate), and must still surface here so the
|
|
118
|
-
// orchestrator can route it to skipped instead of it vanishing silently.
|
|
119
|
-
if (bench.srcWaves.length > 0 || bench.srcLegs.length > 0) { out.push(bench); }
|
|
120
|
-
if (criticUnit.srcWaves.length > 0 || criticUnit.srcLegs.length > 0) { out.push(criticUnit); }
|
|
121
|
-
// Coordinator-review MINOR-7a: null sorts LAST (Infinity), not first (0) —
|
|
122
|
-
// an unmappable loss is not "lens index 0"; it should not perturb the
|
|
123
|
-
// ascending order of the real, well-indexed lens retries.
|
|
124
|
-
out.push(...[...lensUnits.values()].sort((a, b) => (a.lensIndex ?? Infinity) - (b.lensIndex ?? Infinity)));
|
|
125
|
-
return out;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** The briefing a retry unit re-issues — same builders Stage 1 used. */
|
|
129
|
-
function briefingFor(o, unit) {
|
|
130
|
-
if (unit.unit === 'critic') { return briefings.buildCriticBriefing({ briefing: o.briefing, date: o.date }); }
|
|
131
|
-
if (unit.unit === 'lens') {
|
|
132
|
-
return briefings.buildLensBriefing({ lens: o.lenses[unit.lensIndex - 1], briefing: o.briefing, date: o.date });
|
|
133
|
-
}
|
|
134
|
-
return briefings.buildSeatBriefing({ briefing: o.briefing, date: o.date });
|
|
135
|
-
}
|
|
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');
|
|
136
29
|
|
|
137
30
|
/**
|
|
138
31
|
* The retry pass. Serial by design (spec D-order: bench, critic, lenses) —
|
|
139
32
|
* the per-wave-fallback path, where waves actually die, is exactly where
|
|
140
33
|
* concurrent relaunches would race the same server start again.
|
|
141
34
|
*/
|
|
142
|
-
async function retryStage1Losses(ctx, { deadWaves = [], deadLegs = [],
|
|
35
|
+
async function retryStage1Losses(ctx, { deadWaves = [], deadLegs = [],
|
|
36
|
+
counts = { reviewed: 0, total: 0 }, seatOf = new Map() } = {}) {
|
|
143
37
|
const { o, launchers } = ctx;
|
|
144
|
-
|
|
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,
|
|
145
55
|
stillDeadWaves: [], stillDeadLegs: [], skippedDeadWaves: [], skippedDeadLegs: [],
|
|
146
|
-
stillDeadRetryLegs: [] };
|
|
56
|
+
stillDeadRetryLegs: [], seatOf: new Map(), orphanLegs: [], attemptedSeats: new Set() };
|
|
147
57
|
// Task 5 (#129): SL-2 retries the SAME model under the SAME conditions, so a
|
|
148
58
|
// latency failure is structurally unhealable. Double the window, clamped to
|
|
149
59
|
// the leg timeout so the failure CLASS stays NO_OUTPUT_BACKSTOP rather than
|
|
150
60
|
// silently becoming an ordinary timeout at a low --timeout. 2*0 === 0 keeps
|
|
151
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).
|
|
152
63
|
const legTimeoutMs = (o.timeout || 15) * 60 * 1000;
|
|
153
64
|
const escalatedBackstopMs = Math.min(
|
|
154
65
|
2 * (Number.isFinite(o.noOutputBackstopMs) ? o.noOutputBackstopMs : resolveNoOutputBackstopMs()),
|
|
155
66
|
legTimeoutMs,
|
|
156
67
|
);
|
|
157
68
|
|
|
158
|
-
for (const unit of groupStage1Losses(o, deadWaves, deadLegs)) {
|
|
69
|
+
for (const unit of groupStage1Losses(o, deadWaves, deadLegs, seatOf, twins)) {
|
|
159
70
|
// Task-4 review hardening: a unit this pass cannot even ATTEMPT — an
|
|
160
71
|
// unmappable lens loss (no carrier resolved an index), a lens index
|
|
161
72
|
// outside the run's actual lens roster (coordinator-review MINOR-7b: a
|
|
162
73
|
// malformed waveId like "...-l99" must not become an out-of-range
|
|
163
|
-
// `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
|
|
164
75
|
// named zero models — is never launched. Its sources fall back to the
|
|
165
76
|
// ordinary skipped-loss path so the caller's normal degrade notes still
|
|
166
77
|
// fire; being unmappable is not an exemption from the record.
|
|
@@ -196,6 +107,56 @@ async function retryStage1Losses(ctx, { deadWaves = [], deadLegs = [], counts =
|
|
|
196
107
|
if (isAbortExit(res.exitCode)) { out.aborted = res.exitCode; return out; }
|
|
197
108
|
|
|
198
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);
|
|
199
160
|
if (legs.length === 0) {
|
|
200
161
|
// The retry wave itself died wholesale — final failure keeps each
|
|
201
162
|
// source's granularity (D5): wave-origin stays a dead-wave, leg-origin
|
|
@@ -207,32 +168,46 @@ async function retryStage1Losses(ctx, { deadWaves = [], deadLegs = [], counts =
|
|
|
207
168
|
// the "wave wins" precedent from the grouping-level dedup); a seat
|
|
208
169
|
// already covered by its wave's note is skipped when its srcLeg is
|
|
209
170
|
// reached.
|
|
210
|
-
|
|
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);
|
|
211
175
|
for (const w of unit.srcWaves) {
|
|
212
176
|
out.stillDeadNotes.push(waveStillDeadNote(w, unit));
|
|
213
177
|
out.stillDeadWaves.push(w);
|
|
214
|
-
for (const m of (w.models || [])) { notedSeats.add(m); }
|
|
215
178
|
}
|
|
216
|
-
for (const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
out.stillDeadNotes.push(srcLegStillDeadNote(l, unit, counts));
|
|
221
|
-
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);
|
|
222
183
|
}
|
|
223
184
|
continue;
|
|
224
185
|
}
|
|
225
|
-
const usable = new Set(materializeReviews(o.runDir, legs).map(m => m.leg));
|
|
226
|
-
|
|
227
|
-
|
|
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
|
|
228
194
|
for (const leg of legs) {
|
|
229
|
-
const seat = leg.modelInput || leg.model;
|
|
230
|
-
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;
|
|
231
204
|
// SL-2 fix-wave: a retry response should only ever name seats THIS unit
|
|
232
|
-
// launched for (unit.models is
|
|
233
|
-
//
|
|
234
|
-
// with no firstFailures entry,
|
|
235
|
-
//
|
|
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
|
|
236
211
|
// 'unknown'" why for a seat that never failed) and no still-dead note —
|
|
237
212
|
// the seat's first-attempt review stands untouched, rather than this
|
|
238
213
|
// stray leg doubling it into a duplicate bench entry alongside the real one.
|
|
@@ -241,20 +216,32 @@ async function retryStage1Losses(ctx, { deadWaves = [], deadLegs = [], counts =
|
|
|
241
216
|
out.recoveredLegs.push(leg);
|
|
242
217
|
ctx.degrade.note({ channel: 'stage1-retry', kind: 'heal',
|
|
243
218
|
what: `seat ${seat} reviewed on retry`,
|
|
244
|
-
|
|
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'
|
|
245
223
|
? `its first wave ${ff.waveId} produced no legs (${ff.reason}) and was relaunched once`
|
|
246
|
-
:
|
|
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`,
|
|
247
227
|
effect: 'The seat is in this council; nothing was lost',
|
|
248
228
|
data: { seat, retryWaveId: unit.waveId, retryOfWaveId: unit.retryOfWaveId, firstFailure: ff } });
|
|
249
229
|
} else {
|
|
250
230
|
out.stillDeadNotes.push(retryLegStillDeadNote(seat, ff, leg, unit, counts));
|
|
231
|
+
out.attemptedSeats.add(key);
|
|
251
232
|
out.stillDeadRetryLegs.push(leg);
|
|
252
|
-
|
|
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') {
|
|
253
240
|
if (!lostWaveSeats.has(ff.waveId)) { lostWaveSeats.set(ff.waveId, []); }
|
|
254
|
-
lostWaveSeats.get(ff.waveId).push(seat);
|
|
241
|
+
lostWaveSeats.get(ff.waveId).push({ alias: seat, seat: bound || (launched.get(key) || {}).seat || null });
|
|
255
242
|
} else {
|
|
256
|
-
const src =
|
|
257
|
-
if (src) { out.stillDeadLegs.push(src); }
|
|
243
|
+
const src = claimSrc(key);
|
|
244
|
+
if (src) { out.stillDeadLegs.push(src); out.attemptedSeats.add(srcRowKey(src)); }
|
|
258
245
|
}
|
|
259
246
|
}
|
|
260
247
|
}
|
|
@@ -262,31 +249,49 @@ async function retryStage1Losses(ctx, { deadWaves = [], deadLegs = [], counts =
|
|
|
262
249
|
// that came back WITH a leg record. A partial wave return (unit models
|
|
263
250
|
// [a,b], the wave comes back with only a's leg) leaves 'b' invisible to
|
|
264
251
|
// that loop entirely — no heal, no still-dead note, no skip: it would
|
|
265
|
-
// vanish from every array. Reconcile against the
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
for (const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
+
}
|
|
283
277
|
}
|
|
284
278
|
}
|
|
285
279
|
// Wave-origin seats still lost: the return-contract wave entry carries only
|
|
286
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();
|
|
287
288
|
for (const w of unit.srcWaves) {
|
|
289
|
+
if (reconciled.has(w.waveId)) { continue; }
|
|
290
|
+
reconciled.add(w.waveId);
|
|
288
291
|
const lost = lostWaveSeats.get(w.waveId) || [];
|
|
289
|
-
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
|
+
}
|
|
290
295
|
}
|
|
291
296
|
}
|
|
292
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 };
|