amicus 4.8.1 → 4.9.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 +316 -0
- package/README.md +8 -3
- package/docs/ROADMAP.md +101 -10
- package/docs/configuration.md +54 -4
- package/docs/council.md +102 -14
- package/docs/troubleshooting.md +9 -2
- package/docs/usage.md +128 -12
- package/electron/ipc-setup.js +41 -18
- package/electron/main.js +19 -5
- package/electron/offer-session.js +51 -0
- package/electron/setup-ui.js +46 -39
- package/electron/workspace-ui/live-dead-seats.js +163 -91
- package/electron/workspace-ui/workspace-banners.js +30 -7
- package/electron/workspace-ui/workspace-matrix.js +23 -3
- package/electron/workspace-ui/workspace-seats.js +95 -79
- package/package.json +1 -1
- package/schemas/council-run.schema.json +2 -2
- package/schemas/council-tally.schema.json +17 -1
- package/schemas/council-verdict.schema.json +12 -4
- package/schemas/run.schema.json +6 -1
- package/skills/second-opinion/COUNCIL-DESIGN.md +1 -1
- package/skills/second-opinion/MANUAL-ORCHESTRATION.md +1 -1
- package/skills/second-opinion/MODEL-NOTES.md +88 -9
- package/skills/second-opinion/SEAT-BRIEFS.md +36 -4
- package/skills/second-opinion/SKILL.md +151 -36
- package/src/cli-council-run-bench.js +98 -6
- package/src/cli-handlers-council-run.js +18 -6
- package/src/cli-handlers-council.js +57 -7
- package/src/cli-handlers-doctor.js +1 -1
- package/src/cli.js +3 -1
- package/src/council/anonymize.js +2 -1
- package/src/council/briefings-chair-task.js +161 -0
- package/src/council/briefings-chair.js +33 -8
- package/src/council/briefings-debate.js +79 -13
- package/src/council/briefings-stage2-task.js +236 -0
- package/src/council/briefings-stage2.js +103 -26
- package/src/council/briefings-task.js +167 -0
- package/src/council/briefings.js +41 -4
- package/src/council/chair-fallback.js +95 -0
- package/src/council/debate.js +38 -21
- package/src/council/findings.js +3 -2
- package/src/council/ledger.js +2 -2
- package/src/council/parse-stage2.js +63 -15
- package/src/council/report-cost.js +61 -0
- package/src/council/report-html.js +26 -4
- package/src/council/report-md.js +30 -2
- package/src/council/report.js +40 -37
- package/src/council/run-assemble.js +21 -6
- package/src/council/run-chair.js +44 -95
- package/src/council/run-debate-revote.js +81 -49
- package/src/council/run-debate.js +51 -34
- package/src/council/run-finish.js +5 -3
- package/src/council/run-retry-keys.js +4 -4
- package/src/council/run-retry-launch.js +4 -4
- package/src/council/run-retry-notes.js +72 -15
- package/src/council/run-stage1-launch.js +4 -4
- package/src/council/run-stage1-rows.js +9 -6
- package/src/council/run-stage2.js +81 -47
- package/src/council/run-stages.js +9 -21
- package/src/council/run-stats-entry.js +46 -1
- package/src/council/run.js +28 -13
- package/src/council/seats.js +2 -2
- package/src/council/stage1-bind.js +3 -2
- package/src/council/verdict-seat-loss.js +124 -0
- package/src/council/verdict.js +108 -99
- package/src/headless.js +256 -49
- package/src/mcp-council-bench.js +64 -3
- package/src/mcp-council-run.js +10 -3
- package/src/mcp-server.js +52 -12
- package/src/mcp-tools.js +41 -5
- package/src/observe/council-legs.js +2 -2
- package/src/opencode-client.js +19 -1
- package/src/pack/pack-forward.js +15 -12
- package/src/pack/pack-resolve.js +1 -1
- package/src/prompt-builder.js +17 -1
- package/src/sidecar/fanout-leg-fallback.js +2 -1
- package/src/sidecar/fanout-leg.js +26 -0
- package/src/sidecar/fanout.js +1 -1
- package/src/sidecar/list-council.js +178 -0
- package/src/sidecar/list-limit.js +3 -1
- package/src/sidecar/list-search.js +2 -1
- package/src/sidecar/models-render.js +71 -0
- package/src/sidecar/models.js +19 -45
- package/src/sidecar/read.js +34 -10
- package/src/sidecar/reopen-spend.js +2 -1
- package/src/sidecar/setup.js +13 -4
- package/src/sidecar/start.js +2 -1
- package/src/template/render.js +16 -7
- package/src/utils/alias-audit.js +10 -3
- package/src/utils/alias-shadow-writer.js +220 -0
- package/src/utils/alias-shadow.js +294 -0
- package/src/utils/config.js +1 -1
- package/src/utils/curated-models.js +19 -9
- package/src/utils/degrade.js +12 -5
- package/src/utils/doctor-alias-check.js +2 -5
- package/src/utils/engine-log-parse.js +289 -0
- package/src/utils/engine-log-tail.js +114 -0
- package/src/utils/engine-log.js +250 -0
- package/src/utils/engine-skew-records.js +146 -0
- package/src/utils/engine-skew.js +300 -0
- package/src/utils/gateway-router.js +21 -3
- package/src/utils/model-canonicalization.js +55 -6
- package/src/utils/model-catalog.js +27 -9
- package/src/utils/model-fetcher.js +69 -16
- package/src/utils/model-shortlist.js +5 -2
- package/src/utils/provider-default-picker.js +6 -3
- package/src/utils/quick-picks.js +45 -7
- package/src/utils/result-schema.js +17 -1
- package/src/utils/text-sanitize.js +81 -0
- package/src/utils/ttft.js +57 -0
- package/src/utils/untrusted-fence.js +111 -1
- package/src/workspace/fold-format.js +28 -7
- package/src/workspace/live-normalize.js +2 -1
- package/src/workspace/matrix-model.js +6 -2
- package/src/workspace/run-detail.js +33 -7
- package/src/workspace/seat-space.js +10 -6
package/docs/council.md
CHANGED
|
@@ -21,6 +21,7 @@ orchestration recipe. This page is the reference for the artifacts that recipe p
|
|
|
21
21
|
|
|
22
22
|
- [The pipeline, end to end](#the-pipeline-end-to-end)
|
|
23
23
|
- [`amicus council run`](#amicus-council-run)
|
|
24
|
+
- [Task mode (`--intent task`)](#task-mode---intent-task)
|
|
24
25
|
- [Debate mode](#debate-mode)
|
|
25
26
|
- [Council Workspace (GUI)](#council-workspace-gui)
|
|
26
27
|
- [Auto-open on `amicus_council_run` (v4.5)](#auto-open-on-amicus_council_run-v45)
|
|
@@ -112,6 +113,7 @@ amicus council run --prompt-file <briefing.md>
|
|
|
112
113
|
[--gateway auto|direct|openrouter] [--no-validate-model]
|
|
113
114
|
[--template <name|path>] [--artifact <file>] [--var k=v] # v4.5, see docs/usage.md#briefing-templates
|
|
114
115
|
[--pack <name|path>] # v4.5, see docs/usage.md#policy-packs
|
|
116
|
+
[--intent review|task] # v4.9, see Task mode below
|
|
115
117
|
```
|
|
116
118
|
|
|
117
119
|
**The headless engine (v4.0).** Everything the `second-opinion` skill orchestrates by hand in
|
|
@@ -258,7 +260,10 @@ cross-process lock.
|
|
|
258
260
|
`verdict.json` here is the **undecided** verdict — same schema as [`amicus council
|
|
259
261
|
verdict`](#amicus-council-verdict)'s output (council family v2) plus **`overallVerdict`**
|
|
260
262
|
(`"Ship it" | "Fix these first" | "Fundamental rethink" | null`), parsed from the chair's final
|
|
261
|
-
`VERDICT:` line.
|
|
263
|
+
`VERDICT:` line — or, on a task run (`--intent task`, v4.9), `"Converged" | "Split" |
|
|
264
|
+
"Insufficient" | null` parsed from its final `ANSWER:` line instead (both scales, and why they
|
|
265
|
+
are disjoint, are in the `overallVerdict` key note under [`amicus council
|
|
266
|
+
verdict`](#amicus-council-verdict)). Example excerpt:
|
|
262
267
|
|
|
263
268
|
```json
|
|
264
269
|
{
|
|
@@ -282,6 +287,82 @@ Consumers gate on **tiers + the chair verdict line** (`overallVerdict`), per the
|
|
|
282
287
|
report-only Stage-4 policy. Headless runs pin `meta.claudeInCouncil: false`,
|
|
283
288
|
`meta.runType: "headless"`, and the chair is excluded from the street-cred universe.
|
|
284
289
|
|
|
290
|
+
### Task mode (`--intent task`)
|
|
291
|
+
|
|
292
|
+
By default a council **reviews** the material it is given. `--intent task` (over MCP: the `intent`
|
|
293
|
+
parameter on `amicus_council_run`, spelled `'task'`) points the same pipeline at **open-ended work**
|
|
294
|
+
instead: the bench *produces* what the briefing asks for, and the chair synthesizes an **answer**
|
|
295
|
+
rather than a verdict about a review that never happened.
|
|
296
|
+
|
|
297
|
+
`review` is the default, and it is never stored: `--intent review` is accepted and normalized away
|
|
298
|
+
at every door, so nothing writes `intent: "review"` onto `run.json`, `tally.json` or `verdict.json`.
|
|
299
|
+
Only `"task"` is ever recorded — the same emit-when-set idiom as `--tag`. Any other value is a
|
|
300
|
+
pre-flight `BAD_ARGS` failure before any spend.
|
|
301
|
+
|
|
302
|
+
**What changes, stage by stage:**
|
|
303
|
+
|
|
304
|
+
| Stage | Review intent (default) | Task intent |
|
|
305
|
+
|---|---|---|
|
|
306
|
+
| Stage 1 | *"You are one reviewer… Review the material"* — each seat critiques the briefing | *"you are not reviewing the briefing, you are executing it"* — each seat produces the deliverable, then declares the load-bearing claims it rests on |
|
|
307
|
+
| Stage 2 rank | Order the reviews by how **accurate** each critique was | Order the responses from the one that **best does the work the briefing asked for** to the one that does it least well |
|
|
308
|
+
| Stage 2 adjudicate | For every finding id, `agree`/`dispute`/`neutral` on the **critique** | For every claim id, `agree`/`dispute`/`neutral` on whether **the claim holds** |
|
|
309
|
+
| Chair | `VERDICT: Ship it \| Fix these first \| Fundamental rethink` | `ANSWER: Converged \| Split \| Insufficient` |
|
|
310
|
+
| Reliability ledger | One row per (run × model) appended | **Nothing appended** |
|
|
311
|
+
|
|
312
|
+
**The two scales are disjoint on purpose.** They share no value and no keyword, which is what lets
|
|
313
|
+
each parser stay blind to the other's line: a task run can never report `"Ship it"`, and a review run
|
|
314
|
+
can never report `"Converged"`. The run's intent is what selects the parser — including on a Stage-5
|
|
315
|
+
`amicus council verdict` rebuild long after the run is over, where a carried `overallVerdict` from
|
|
316
|
+
the *wrong* scale is refused and the chair's prose is re-parsed instead. Full field semantics are in
|
|
317
|
+
the `overallVerdict` and `intent` key notes under
|
|
318
|
+
[`amicus council verdict`](#amicus-council-verdict).
|
|
319
|
+
|
|
320
|
+
**What does *not* change.** The Stage-1 output contract is identical — the same trailing fenced JSON
|
|
321
|
+
skeleton, the same `blocker | major | minor | nit` severity enum, the same required-non-empty
|
|
322
|
+
`location`, validated by the same validator and repaired by the same bounded repair loop. Only the
|
|
323
|
+
frame and the field *glosses* fork. In task mode `location` is the grounding discipline: it names
|
|
324
|
+
what the claim rests on — a source, a computation, or the literal word `assumption`. An empty
|
|
325
|
+
`findings[]` under a real `overall` is a valid task response, exactly as it is a valid review, and
|
|
326
|
+
the bench is told so rather than left to invent claims to fill the array.
|
|
327
|
+
|
|
328
|
+
Two things a task bundle carries that a review bundle never does: the Stage-2 judge packet ends with
|
|
329
|
+
a `--- THE BRIEFING (what every response was asked to do) ---` section — judges cannot rank *how well
|
|
330
|
+
the work was done* without the ask — and that section is fenced as reference material, because it is
|
|
331
|
+
the first time briefing text reaches a judge in band.
|
|
332
|
+
|
|
333
|
+
**Task runs write no reliability rows, and say so.** Two gates enforce it (the engine's own append
|
|
334
|
+
and `amicus council tally`'s), and `council tally` refuses a `meta.intent` that is neither spelling
|
|
335
|
+
rather than letting a near-miss slide into the ledger. Where the skip is **load-bearing** it is
|
|
336
|
+
**announced, and the announcement does not degrade the run** — a `Note:` record on the
|
|
337
|
+
`ledger-skipped` channel with `kind: "info"`, which the degrade sink cannot use to flip a run's
|
|
338
|
+
`degraded` state. ⚠️ It is emitted at one site, not on every task run: the chair-fallback promotion
|
|
339
|
+
arm, reached only after the chair's own attempts have all failed and the run still has budget,
|
|
340
|
+
because that arm is the one step that draws on ledger history a task run never fed. A task run whose
|
|
341
|
+
chair answers has no `ledger-skipped` note, and needs none. `info` records are announcements, not losses: the report gives
|
|
342
|
+
them their own **Notes:** list and keeps them out of `## What was lost`. Relatedly,
|
|
343
|
+
[`amicus council stats`](#amicus-council-stats) on an empty ledger now names where rows come from
|
|
344
|
+
instead of implying that no council ever ran.
|
|
345
|
+
|
|
346
|
+
**Read the tiers correctly.** A task run's report carries the line *"Tiers report peer concurrence,
|
|
347
|
+
never verification."* directly under the tier counts, and the chair's own packet carries the same
|
|
348
|
+
caveat beside the adjudications it is weighing. Peer agreement on a generative bench is correlation
|
|
349
|
+
between models trained on overlapping priors — a tier says *how many peers concurred*, never *that
|
|
350
|
+
the claim was checked*.
|
|
351
|
+
|
|
352
|
+
**Review runs are byte-identical.** Not "unchanged as far as we know": the review path composes
|
|
353
|
+
through the same dispatcher, and the shared packet — section headers, `Review by <model>` labels,
|
|
354
|
+
empty-section wordings — is used verbatim in both intents. One vocabulary, two instructions.
|
|
355
|
+
|
|
356
|
+
**Limitations, as of v4.9:**
|
|
357
|
+
|
|
358
|
+
- **One intent per run.** There is no mixed bench; the whole run is a task run or a review run.
|
|
359
|
+
- **Intent is not pack-settable.** A [policy pack](./usage.md#policy-packs) cannot carry it — pass
|
|
360
|
+
the flag (or the MCP parameter) explicitly.
|
|
361
|
+
- **`--claude-review` is refused with `--intent task`.** Entering a file as review N+1 is review
|
|
362
|
+
machinery and has no task-mode meaning.
|
|
363
|
+
- **Task runs build no reliability history**, so they never contribute to — and never benefit from —
|
|
364
|
+
`amicus council stats`, including the ledger-driven chair-fallback promotion.
|
|
365
|
+
|
|
285
366
|
### Debate mode
|
|
286
367
|
|
|
287
368
|
`--debate` adds a **Stage-2.5 rebuttal round** between cross-review and the final tally
|
|
@@ -757,7 +838,8 @@ named here. That is why the v4.8 keys below each needed their own line.
|
|
|
757
838
|
**Key notes:**
|
|
758
839
|
- `schemaVersion` — verdict-document schema version (currently `2`).
|
|
759
840
|
- `type` — document-type discriminator; always `"council-verdict"` (council family v2 envelope).
|
|
760
|
-
- `overallVerdict` — the chair's
|
|
841
|
+
- `overallVerdict` — the chair's terminal-line outcome, on **one of two disjoint scales, chosen by the run's intent**. A review run (no `intent` key) carries a verdict-scale value — `"Ship it"`, `"Fix these first"`, `"Fundamental rethink"` — parsed from the chair's `VERDICT:` line. A **task run** (`intent: "task"`, v4.9) carries an answer-scale value — `"Converged"`, `"Split"`, `"Insufficient"` — parsed from the chair's `ANSWER:` line. The two scales share no value and no keyword, which is what lets each parser be blind to the other's line; a task run therefore never reports `"Ship it"`, and a review run never reports `"Converged"`. `null` on either scale when no chair terminal line was produced (populated by the headless engine during Stage 3; `null` for a plain `council verdict` merge without engine integration).
|
|
842
|
+
- `intent` — **v4.9**, optional, `"task"` only. Present exactly when the run was launched with `--intent task`; absent means review — the engine never writes `"review"` (emit-when-task, the same idiom as `tag`). It is the key every renderer forks on: the report's concurrence qualifier and header word, the fold's and the Workspace chip's `ANSWER:`/`VERDICT:` label, and the chair's own packet/parser upstream of this document.
|
|
761
843
|
- `seats` — **v4.8**, optional. The tally record's `meta.seats` (same `{id, alias, role, lens, position}` shape), promoted to the top level next to `seatLoss`. Present only when the tally record carried one, i.e. only when the bench repeated an alias. It is what makes the `alias#N` ids on `findings[].raiserSeat`, `adjudications[].seat` and `runStats[].seat` resolvable from the verdict **alone** — before v4.8 the verdict named seats it could not resolve. `council report` reads it to give each seat its own adjudication-matrix column; when it is absent, or is not an array of objects each carrying a string `id`, the **adjudication matrix** falls back to alias space whole and renders exactly as it did before v4.8. ⚠️ **That fallback is the matrix's alone — it is not a whole-document guarantee.** The street-cred table beside it labels each row from `streetCred[].seat` whenever the row carries one, a predicate independent of this key, so a verdict with seated `streetCred[]` rows and no usable `seats` renders seat ids in the street-cred table and aliases in the matrix (measured on an absent `seats`, a non-array `seats`, and an array-of-strings `seats`). In-process both fields come from the same twin bench and travel together; the split is reachable on a hand-assembled or externally-supplied record, which `buildVerdict`'s own docblock names. A verdict written before v4.8 carries no `streetCred[].seat` at all and is unaffected.
|
|
762
844
|
- `findings[].raiserSeat` — **v4.8**, optional. The raising seat's id, carried through from the tally record; absent unless the bench repeated an alias. `findings[].raiser` stays the alias.
|
|
763
845
|
- `findings[].sameModelCorroboration` — **v4.8**, optional, `true` only. Carried through from the tally record; see the tally-record notes above for the stamp's meaning **and for the two directions in which it is wrong** (it misses one model behind two aliases, and it fires falsely on one alias behind two executables).
|
|
@@ -802,7 +884,12 @@ wrapper over `buildReport()` (`src/council/report.js`, which builds the neutral
|
|
|
802
884
|
dispatches to `report-md.js` / `report-html.js` — the two renderers that own the string formats).
|
|
803
885
|
|
|
804
886
|
**What it renders**, in this order: a header (run type, id, date, chair, council members), a
|
|
805
|
-
|
|
887
|
+
tier-count summary table — headed **Verdict summary** on a review run and **Answer summary** on a
|
|
888
|
+
task run, since a task run produces an answer, not a verdict, and on a task run followed by the
|
|
889
|
+
one-line concurrence qualifier (*tiers report peer concurrence, never verification*) — a
|
|
890
|
+
**What was lost** section
|
|
891
|
+
when the run degraded (plus a **Notes** list for informational records, e.g. a task run's
|
|
892
|
+
ledger-skipped announcement), the **adjudication matrix** (finding × judge, `✓`/`✗`/`–` with
|
|
806
893
|
`*` marking the raiser's own vote), the **peers-only street-cred table**, **findings grouped by
|
|
807
894
|
tier** (Disputed first), and a **cost table** (per-model status/duration/cost + wave total,
|
|
808
895
|
sourced from `runStats[].usage`).
|
|
@@ -873,17 +960,18 @@ matrix does not carry — `` `†` `` marks a finding corroborated only by anoth
|
|
|
873
960
|
same model — so from v4.8.0 the two legends are no longer interchangeable. That line, and the `†`
|
|
874
961
|
itself, appear only on a run that actually raised such a finding, which is a twin bench only.
|
|
875
962
|
|
|
876
|
-
⚠️ **v4.8
|
|
877
|
-
sentence above is scoped to the **matrix**, and stays true.
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
963
|
+
⚠️ **v4.8 filed a SECOND report/Workspace divergence, in the street-cred table — CLOSED in v4.9.**
|
|
964
|
+
The sentence above is scoped to the **matrix**, and stays true. All three street-cred renderers now
|
|
965
|
+
label each row `seat || model`: `report-md.js` and `report-html.js` (v4.8), and the Workspace's own
|
|
966
|
+
(`electron/workspace-ui/workspace-matrix.js :: renderVerdict`, v4.9). A twin bench reads
|
|
967
|
+
`gemini#1` / `gemini#2` in every one of them, instead of the Workspace reading `gemini` twice with
|
|
968
|
+
different numbers under one identical name; on any bench with no repeated alias all three are
|
|
969
|
+
unchanged, because every seat id there *is* its alias. ⚠️ **Blind mode is the deliberate exception
|
|
970
|
+
and is unchanged**: the Workspace still shows the anonymised label, falls back to the model alias
|
|
971
|
+
when no label resolves, and renders a seat id in neither case — a seat id contains its alias, so
|
|
972
|
+
printing one would defeat blind mode (the same rule the matrix follows two paragraphs above).
|
|
973
|
+
`BACKLOG.md` filed this behind the gate *"`opts.labelOf` must accept a seat id"*, which the fix did
|
|
974
|
+
not need: `labelOf` stays alias-keyed, because the value blind mode has to show was never the seat.
|
|
887
975
|
|
|
888
976
|
This is the same renderer the `second-opinion` skill calls in Stage 5 to produce `report.html`.
|
|
889
977
|
**`report.md` and this renderer's output are two different files** — `report.md` is Claude-authored
|
package/docs/troubleshooting.md
CHANGED
|
@@ -237,11 +237,18 @@ For full headless configuration, see [docs/configuration.md](./configuration.md)
|
|
|
237
237
|
|
|
238
238
|
**Cause:** The mechanism only knows that the deadline passed with no substantive activity — no output, reasoning, or tool calls — for the backstop window (300 s by default for an ordinary leg). It does **not** know *why*. Do not read this as "the endpoint is dead" or "the model isn't being served" — the message deliberately stops short of that claim, because at one of its two firing sites the backstop can win the race before the prompt send even resolves, so acceptance itself was never observed. Treat `NO_OUTPUT_BACKSTOP` as "silence past the deadline," not a diagnosis.
|
|
239
239
|
|
|
240
|
-
|
|
240
|
+
Two trailing clauses are the exceptions, and neither is the mechanism's inference:
|
|
241
|
+
|
|
242
|
+
- ` — engine log: <excerpt>` (since v4.9) is the OpenCode engine's own ERROR line for this exact session, quoted verbatim from the engine's log. That clause is the engine speaking. "For this exact session" is strict: the line must name your session in one of the engine's own session fields (`session.id=`, `id=`, …), so a neighbouring session's failure is never quoted at you — and when nothing on disk meets that bar, the clause is simply absent rather than approximate.
|
|
243
|
+
- ` (engine skew: server <a> ≠ installed <b>)` (since v4.9) means the engine serving **this** run reported a different version than the `opencode-ai` sitting in the install that launched it — two facts, both published by the software itself, neither guessed. It names the server this leg actually talked to, as of this leg's most recent session: a skew seen on some other server in the same process, or one that was fixed part-way through, never rides along on a report it does not belong to. This is #133's own shape; see the Lesson below.
|
|
244
|
+
|
|
245
|
+
The two clauses are independent: either can appear without the other, and the skew clause is **not** conditional on the log read succeeding. With neither, the message is exactly what it was before v4.9. Everything before them is still only "silence past the deadline".
|
|
246
|
+
|
|
247
|
+
**Lesson from #133:** an earlier version of this message *did* guess a cause ("likely a listed-but-not-serving model or a dead endpoint"), and that guess sent 30 minutes of debugging at model ids and API keys on a real incident. The actual cause was an OpenCode engine version skew between the npx-cached copy and the global install — sitting in the engine's own log the whole time, at the exact timestamp of every dead session. **Both halves are now surfaced for you** (v4.9): the log line becomes the ` — engine log:` clause, and the skew itself is detected at session-create time — announced on stderr as `[amicus] engine version skew: …` (once per standing skew per server, not once per session) and repeated in the ` (engine skew: …)` clause above. The remedy is to make the two copies the same version — update whichever is behind (`npm i -g amicus`, or re-run the installer for the surface that failed). `amicus doctor` will **not** confirm this one: its engine check compares npx-cached copies against the global install and is structurally blind to the copy the running server loaded, so it can report clean while this notice is firing. One related line can also appear, once per process: `[amicus] server identity unavailable — skew attribution is process-wide; SDK shape may have changed`. It means amicus could not read which server a client is talking to, so skew records stop being kept per server and share one bucket — the skew detection still works, but a clause could in principle be attributed to the wrong server when a run talks to more than one. It is a report about amicus itself, not about your models; if you see it, the OpenCode SDK has probably changed shape and amicus needs an update. If neither clause is present, read the log yourself before chasing model/endpoint theories — see the fix below for where it lives.
|
|
241
248
|
|
|
242
249
|
**Confirm:** `amicus models --check` — audits your configured aliases against the live catalog and flags drift/staleness. A model that still resolves locally but has quietly been retired upstream is one possible explanation, but a static audit only compares against the catalog's *listing* — it can't tell you whether a still-listed model actually answers. For that, run `amicus models --check --live` on demand: it sends one tiny real request to every stored alias and reports `SERVED` / `SILENT` / `ERROR` per alias, where `SILENT` (`accepted-but-silent`) is this exact `NO_OUTPUT_BACKSTOP` failure, caught deliberately instead of by accident. It spends real money (one tiny leg per stored alias) — see [docs/usage.md § `amicus models`](./usage.md#amicus-models--the-model-catalog) for cost, scope, and exit-code details.
|
|
243
250
|
|
|
244
|
-
**Fix:**
|
|
251
|
+
**Fix:** Read the ` — engine log: …` clause on the failure message first, if it has one — that is the engine's own account of this session (see #133 above). If it has none, look yourself: OpenCode writes its logs to `$XDG_DATA_HOME/opencode/log` when that variable is set, otherwise `~/.local/share/opencode/log` (that path holds on Windows too) — check **both** when `XDG_DATA_HOME` is set, since a leftover value can point at a directory the running engine never writes to (amicus searches all of them, newest file first, and answers from the most recently written line that both is an ERROR and names your session). Current engine builds write **one timestamped file per process** there, e.g. `2026-08-25T185532.log`; older ones append to a single `opencode.log` in the same directory, and both layouts turn up on real machines — so check the newest files by modification time, not one fixed filename. The session's `ses_…` id is the correlation key. Then check the alias's target (`amicus models --search <term>` to find the current id, then re-point the alias) — a stale alias is a common case, but not the only one. If the failing leg was a Stage-1 retry (the window read `600s`, or generally double your configured/default value), `AMICUS_NO_OUTPUT_BACKSTOP_MS` **is** the lever — raising it raises both the first attempt's window and the retry's doubled one, unless your `--timeout` is low enough that the doubled window is clamped to the leg timeout. On an ordinary (non-retry) leg, raise it only if a model legitimately needs more than 300 s to produce its first token. The live probe's fixed 30 s window is the one case the env var genuinely cannot touch. See [docs/configuration.md § Headless Poller Tuning](./configuration.md#headless-poller-tuning).
|
|
245
252
|
|
|
246
253
|
---
|
|
247
254
|
|
package/docs/usage.md
CHANGED
|
@@ -203,6 +203,7 @@ amicus council run --prompt-file briefing.md --models gemini,glm --chair deepsee
|
|
|
203
203
|
| `--artifact <file>` | File whose content fills `{{artifact}}`/`{{artifact_path}}` (256 KB cap). Requires `--template`. |
|
|
204
204
|
| `--var <k=v>` | Set `{{var.<key>}}`; repeatable. Requires `--template`. |
|
|
205
205
|
| `--tag <t>` | Label this run for `list`/`--search`/`spend --group-by tag` (1-64 chars, `[A-Za-z0-9_-]`; rejected, not cleaned). Every stage's sub-waves (Stage-1, critic/lens solos, Stage-2, chair, debate) carry the same tag on their wave metadata. |
|
|
206
|
+
| `--intent <review\|task>` | The run's intent (v4.9). `review` — the default — is never stored; `task` marks a task-mode run, recorded as `intent: "task"` on `run.json`/`verdict.json` and kept out of the reliability ledger. Over MCP: the `intent` param on `amicus_council_run`, and a hand-assembled `amicus_council_tally` input may carry `meta.intent` the same emit-when-`task` way. What forks stage by stage — and what stays byte-identical — is in [docs/council.md § Task mode](./council.md#task-mode---intent-task). |
|
|
206
207
|
|
|
207
208
|
**Exit codes:** `0` full run · `2` degraded but reportable (fewer than 2 judges, chair failure —
|
|
208
209
|
`overallVerdict: null` — a cost ceiling hit after the tally, or a `--max-cost` ceiling set over a
|
|
@@ -403,7 +404,7 @@ Each stored alias resolves to one of three outcomes:
|
|
|
403
404
|
| Outcome | Example line | Meaning |
|
|
404
405
|
|---------|--------------|---------|
|
|
405
406
|
| `SERVED` | `SERVED: gemini -> openrouter/google/gemini-3.6-flash ($0.0004)` | The model answered; cost shown in parens. |
|
|
406
|
-
| `SILENT` (`accepted-but-silent`) | `SILENT: probetest -> anthropic/claude-opus-4-8 — NO_OUTPUT_BACKSTOP: … (
|
|
407
|
+
| `SILENT` (`accepted-but-silent`) | `SILENT: probetest -> anthropic/claude-opus-4-8 — NO_OUTPUT_BACKSTOP: … (no output within the probe window)` | Nothing arrived at all within the probe's 30 s backstop window — shorter than the ordinary 300 s leg default, and not tunable. ⚠️ `accepted-but-silent` is the CLASSIFICATION's name, not a proven fact about the endpoint: a fired backstop shows only that no output, reasoning or tool call arrived in the window, which a stalled gateway or a dropped connection produces just as readily as a model that accepted and then said nothing. Either way it is the "listed but not actually serving" failure this check exists to catch. |
|
|
407
408
|
| `ERROR` | `ERROR: gpt -> openai/gpt-5.6-terra — 402 Payment Required` | Routing, auth, or provider failure; the raw error is printed. |
|
|
408
409
|
|
|
409
410
|
**Exit code.** The probe's non-served count folds into the same exit code as the static audit — `max(existing exit, min(nonServedCount, 100))` — so a single `SILENT` or `ERROR` fails the check even when every alias is otherwise catalog-fresh. No stored aliases prints `Live probe: no stored aliases to probe` and never affects the exit code. `--json` adds `probe` (the per-alias array) and `probeCount` (its length) to the `alias-audit` document — both additive, `[]`/`0` when `--live` wasn't passed.
|
|
@@ -464,12 +465,31 @@ amicus setup --add-alias fast=google/gemini-2.5-flash # Add/override one alias
|
|
|
464
465
|
the same core fields everywhere: `id`, `model`, `status`, `mode` (`interactive`/`headless`),
|
|
465
466
|
`type` (`run` by default), `parentWave` (`null` unless the row is a fan-out leg), `legCount`
|
|
466
467
|
(`null` unless the row is a wave), and `tag` — the one field that's omitted, not `null`, when the
|
|
467
|
-
session has none. The human-readable CLI table adds a `TAG` column. Council-run rows
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
`briefing` preview plus a `stage` field naming
|
|
468
|
+
session has none. The human-readable CLI table adds a `TAG` column. Council-run rows join the
|
|
469
|
+
listing on **both surfaces** through a separate merge (`src/sidecar/list-council.js`): the
|
|
470
|
+
directory scan itself still skips their `council-<runId>.json` pointer files (a pointer's
|
|
471
|
+
filename fails the session-ID pattern every other row's directory name must match), which is
|
|
472
|
+
exactly why the merge exists. Each merged row carries `type: 'council-run'`, a fixed
|
|
473
|
+
`mode: 'headless'`, and its own 80-char sanitized `briefing` preview plus a `stage` field naming
|
|
474
|
+
whichever stage is currently running; the CLI table renders `council(<stage>)` in its MODEL
|
|
475
|
+
column and re-truncates that 80-char preview to its usual 30-char slice. That merge is scoped
|
|
476
|
+
to the CURRENT project on both surfaces — `--all` included, so under `--all` the session rows
|
|
477
|
+
span every known project while the council rows do not: council runs are found through
|
|
478
|
+
per-project pointer files and there is no cross-project council index to walk. The CLI says
|
|
479
|
+
so at runtime rather than only here — every human-readable `--all` listing ends with
|
|
480
|
+
`council runs: current project only (no cross-project index).`, under the table or right after
|
|
481
|
+
`No amicus sessions found.` when there is no table, and after the `--limit` elision notice when
|
|
482
|
+
one is printed. That line is human-surface only: `--json`
|
|
483
|
+
keeps its shape, on stdout and stderr alike, because no flag can widen the scope it reports.
|
|
484
|
+
If the merge itself cannot run — an unreadable or corrupt council pointer, say — the listing
|
|
485
|
+
still prints the sessions it already had and adds `council runs: unavailable (<reason>)`,
|
|
486
|
+
rather than dropping every council row in silence. It appears with or without `--all` (under
|
|
487
|
+
`--all` it sits just above the scope note), and the reason is the underlying error message,
|
|
488
|
+
sanitised and capped to one line. This one is human-surface only for a *different* reason than
|
|
489
|
+
the scope note: not that nothing could widen it, but that `--json`'s shape is a contract and
|
|
490
|
+
this is prose. The residual that leaves — a `--json` caller reads a well-formed document that
|
|
491
|
+
is silently short — is recorded at the pins in `tests/list-council-merge.test.js`.
|
|
492
|
+
The MCP tool
|
|
473
493
|
also re-sanitizes every other row's `briefing` to that same 80-char cap and, for any row still
|
|
474
494
|
`status: 'running'`, adds live-progress fields (`phase`, `messageCount`, `lastActivityAt`,
|
|
475
495
|
`latestPreview`) — enrichments the CLI table doesn't apply, since it prints the raw 30-char slice
|
|
@@ -481,10 +501,10 @@ at a missing or unreadable project is skipped rather than surfaced as an error.
|
|
|
481
501
|
fan-out wave row reads its full `briefing.md` off disk (falling back to the row's 200-char excerpt
|
|
482
502
|
if that file isn't readable), and a leg row (one spawned by a wave) matches on `id`/`tag` only —
|
|
483
503
|
its briefing is the parent wave's, and matching it there would surface the same wave once per leg
|
|
484
|
-
it spawned.
|
|
485
|
-
written at MCP launch time, or falls back to the portion of
|
|
486
|
-
`--- MATERIAL / BRIEFING ---` (CLI-launched runs only ever have the
|
|
487
|
-
|
|
504
|
+
it spawned. A council-run row's search material (both surfaces, now that the CLI merges council
|
|
505
|
+
rows too) is `briefing.md` written at MCP launch time, or falls back to the portion of
|
|
506
|
+
`briefing-stage1.md` after `--- MATERIAL / BRIEFING ---` (CLI-launched runs only ever have the
|
|
507
|
+
latter file). A bare `--search`
|
|
488
508
|
with no value is a usage error on the CLI. Tag itself is set at launch with `--tag <t>` on
|
|
489
509
|
`start`/`fanout`/`council run` (see those sections above), and is also a dimension for
|
|
490
510
|
`amicus spend --group-by tag`. `amicus continue` and `amicus resume` don't take a `--tag` of
|
|
@@ -518,7 +538,7 @@ $ amicus status demo123 --json
|
|
|
518
538
|
"taskId": "demo123",
|
|
519
539
|
"status": "complete",
|
|
520
540
|
"elapsed": "5m 0s",
|
|
521
|
-
"version": "4.
|
|
541
|
+
"version": "4.9.1",
|
|
522
542
|
"model": "google/gemini-2.5-flash",
|
|
523
543
|
"phase": "terminal"
|
|
524
544
|
}
|
|
@@ -849,6 +869,102 @@ The async pattern is **start → status → read**: `amicus_start` (or `amicus_f
|
|
|
849
869
|
|
|
850
870
|
Session statuses: `running`, `complete`, `aborted`, `crashed`, `error`, `timed-out`, `idle-timeout`
|
|
851
871
|
|
|
872
|
+
### MCP tool parameters
|
|
873
|
+
|
|
874
|
+
Every tool below also takes an optional `project` — an absolute path naming the working directory the call resolves against, auto-detected from the caller's cwd when omitted. `amicus_setup` and `amicus_guide` take no parameters at all; `amicus_council_stats` takes only `project`; and `amicus_spend`'s nine filters are described in the paragraph above. A handful of parameters not listed here are filled in by the calling agent from facts only it can see (the Cowork VM process name, the parent session UUID) rather than chosen by you — they are documented in the tool schema the agent reads, and pinned as such in `tests/mcp-tool-params-docs.test.js`.
|
|
875
|
+
|
|
876
|
+
**`amicus_start`** — spawn a session with another model.
|
|
877
|
+
|
|
878
|
+
- `model` — short alias (`gemini`, `gpt`, `opus`, `deepseek`, …) or a full `provider/model` id. Omitted uses your configured default.
|
|
879
|
+
- `gateway` — routing preference: `auto` (direct-first, the default), `direct`, or `openrouter`.
|
|
880
|
+
- `prompt` — the task briefing: objective, background, files of interest, success criteria.
|
|
881
|
+
- `agent` — OpenCode agent mode (`Chat`, `Plan`, `Build`); see [OpenCode Agent Types](#opencode-agent-types) below.
|
|
882
|
+
- `noUi` — run headless instead of opening the Electron window. Default `false`.
|
|
883
|
+
- `thinking` — reasoning effort (`low` | `medium` | `high`). Default `medium`.
|
|
884
|
+
- `timeout` — headless timeout in minutes (default 15); applies only when `noUi` is true.
|
|
885
|
+
- `contextTurns` — max parent-conversation turns to include. Default 50; the MCP twin of `--context-turns`.
|
|
886
|
+
- `contextSince` — time window for parent context (`30m`, `2h`, `1d`); overrides `contextTurns` when set. The MCP twin of `--context-since`.
|
|
887
|
+
- `contextMaxTokens` — cap on the included context, in tokens. Default 80000.
|
|
888
|
+
- `includeContext` — include the parent conversation at all. Default `true`; set `false` for a self-contained briefing (the MCP twin of `--no-context`).
|
|
889
|
+
- `summaryLength` — fold-summary verbosity: `brief`, `normal` (default), or `verbose`.
|
|
890
|
+
- `windowPosition` — where the interactive window lands: `right` (default), `left`, or `center`.
|
|
891
|
+
- `pack` — [policy pack](#policy-packs) name or path supplying defaults; explicit params still win.
|
|
892
|
+
- `tag` — a 1-64 character label (letters, digits, `_`, `-`) for `list`/`search`/`spend` grouping.
|
|
893
|
+
|
|
894
|
+
**`amicus_status`** — one-shot status. Takes `taskId` (a session or wave id).
|
|
895
|
+
|
|
896
|
+
**`amicus_abort`** — stop a running session. Takes `taskId`.
|
|
897
|
+
|
|
898
|
+
**`amicus_wait`** — block until a run reaches a terminal state, replacing a sleep+poll loop.
|
|
899
|
+
|
|
900
|
+
- `taskId` — the session or wave id to wait on.
|
|
901
|
+
- `waveId` — alias for `taskId` when waiting on a fan-out wave.
|
|
902
|
+
- `timeoutMs` — max wait in milliseconds. Default 50000, capped at 110000 so the call returns before typical MCP client kill windows; on expiry you get `{timedOut: true}`, not an error, and re-call.
|
|
903
|
+
|
|
904
|
+
**`amicus_read`** — read a finished session. Every mode is capped at ~50KB.
|
|
905
|
+
|
|
906
|
+
- `taskId` — the task to read.
|
|
907
|
+
- `mode` — `summary` (default), `conversation`, or `metadata`. Paging is ignored in `metadata`.
|
|
908
|
+
- `offset` — byte offset to start from (0-based). Suppresses the cap and the truncation notice, and takes precedence over `tail`.
|
|
909
|
+
- `limit` — max bytes to return (1-51200). Defaults to the cap.
|
|
910
|
+
- `tail` — return the LAST `limit` bytes instead of the first. Ignored when `offset` is given; it is also the implicit behaviour when content exceeds the cap and neither is set.
|
|
911
|
+
|
|
912
|
+
**`amicus_list`** — list sessions.
|
|
913
|
+
|
|
914
|
+
- `status` — filter by status (`all`, `running`, `complete`, `error`, `aborted`, …). Default: everything.
|
|
915
|
+
- `search` — case-insensitive substring filter over id, tag, and briefing material.
|
|
916
|
+
|
|
917
|
+
**`amicus_resume`** — reopen a finished session. Takes `taskId`, plus `noUi` and `timeout` with the same meanings as on `amicus_start`.
|
|
918
|
+
|
|
919
|
+
**`amicus_continue`** — send a follow-up turn into a previous session.
|
|
920
|
+
|
|
921
|
+
- `taskId` — the session to continue from; `prompt` — the new task description.
|
|
922
|
+
- `model` / `gateway` — override the model or routing for the continuation.
|
|
923
|
+
- `noUi` / `timeout` — headless mode and its timeout, as on `amicus_start`.
|
|
924
|
+
- `contextTurns` / `contextMaxTokens` — how much of the previous session's conversation rides along.
|
|
925
|
+
|
|
926
|
+
**`amicus_fanout`** — same briefing, many models, one wave.
|
|
927
|
+
|
|
928
|
+
- `models` — 1-10 aliases or full ids (2+ for a genuine fan-out). Mutually exclusive with `council`.
|
|
929
|
+
- `council` — a saved council or a built-in bench (`free`, `budget`, `frontier`) instead of `models`.
|
|
930
|
+
- `prompt` — the briefing every leg receives; `agent`, `thinking`, `timeout`, `summaryLength`, `gateway`, `includeContext`, `pack` and `tag` all mean what they do on `amicus_start`, applied to every leg (`timeout` is per-leg).
|
|
931
|
+
- `onComplete` — `mcp-notify`: send an MCP info notification carrying the terminal event doc when the wave finishes. Advisory and best-effort — `amicus_wait` stays the reliable completion mechanism, and exec commands are never accepted over MCP.
|
|
932
|
+
|
|
933
|
+
**`amicus_council_tally`** — deterministic tiers + street-cred from an assembled council record.
|
|
934
|
+
|
|
935
|
+
- `meta` — run metadata; `meta.models` lists every reviewed model, and `meta.intent` marks a task run.
|
|
936
|
+
- `findings` — the run-global findings, ids already `A1`/`B2`/`C3`-prefixed.
|
|
937
|
+
- `adjudications` — one row per (judge × finding): `judge`, `findingId`, `verdict`.
|
|
938
|
+
- `rankings` — each judge's preference order over the reviews; ties are a nested array.
|
|
939
|
+
- `runStats` — optional per-model run stats (status/duration/usage).
|
|
940
|
+
|
|
941
|
+
**`amicus_verdict`** — merge a tally record with Stage-4 decisions into `verdict.json`.
|
|
942
|
+
|
|
943
|
+
- `record` — a tally output record (from `amicus_council_tally`).
|
|
944
|
+
- `decisions` — per-finding Stage-4 decisions; defaults to `[]`.
|
|
945
|
+
- `overallVerdict` — the chair's terminal line, carried through from the engine-written `verdict.json`. It is the only copy; omit it when the chair was skipped, and never author one yourself.
|
|
946
|
+
- `seatLoss` / `degrades` — the engine-written critic-seating block and the "what was lost" list, carried through the same way. Omitted means absent, never fabricated.
|
|
947
|
+
- `render` — also return the markdown rendering of the decided verdict.
|
|
948
|
+
- `outDir` — where to refresh `report.html` when `render` is true; rejected if it escapes the project dir. Omit to write nothing.
|
|
949
|
+
|
|
950
|
+
**`amicus_council_run`** — the full headless council engine, no orchestrating agent required.
|
|
951
|
+
|
|
952
|
+
- `briefingFile` — path to the briefing (self-contained material + criteria). Councils always brief via file; the file is copied into the run dir.
|
|
953
|
+
- `models` — 2-10 bench seats, or `council` to name a saved council or a built-in bench instead.
|
|
954
|
+
- `chair` — the synthesizing model (default `deepseek`). Must NOT be a bench seat.
|
|
955
|
+
- `critic` — swap one bench seat to an adversarial brief. Must BE a bench seat; mutually exclusive with `lenses`.
|
|
956
|
+
- `lenses` — one expert lens per seat (count must equal seat count). Forces no-ledger; mutually exclusive with `critic`.
|
|
957
|
+
- `outDir` — the run directory. Default `<project>/council-<runId>/`.
|
|
958
|
+
- `maxCost` — whole-run USD ceiling, checked before each paid stage launch.
|
|
959
|
+
- `noCostGate` — disable the per-leg price gate for the WHOLE run, repairs and chair included. Independent of `maxCost`, which still caps the total. See [Cost gate](configuration.md#cost-gate).
|
|
960
|
+
- `timeoutMinutes` — per-leg timeout in minutes (fanout semantics). Default 15.
|
|
961
|
+
- `gateway` — routing preference, as on `amicus_start`.
|
|
962
|
+
- `debate` — add a Stage-2.5 rebuttal round before the chair synthesizes.
|
|
963
|
+
- `claudeReviewFile` — path to Claude's own review, included as a judged entry. Claude is reviewed and ranked like a seat, but never judges or chairs.
|
|
964
|
+
- `intent` — `task` marks a task-mode run (recorded on `run.json`/`verdict.json`, kept out of the reliability ledger): seats produce the deliverable and the chair closes with `ANSWER:` on a disjoint scale. `review` is the default and is never stored. See [docs/council.md § Task mode](./council.md#task-mode---intent-task).
|
|
965
|
+
- `ui` — auto-open the Council Workspace window for this run. Default: opens under Claude Code (local) when Electron and a display exist and `workspace.autoOpen` is not `false`.
|
|
966
|
+
- `onComplete`, `pack`, `tag` — as on `amicus_fanout`.
|
|
967
|
+
|
|
852
968
|
> Legacy `sidecar_*` tool names were removed entirely in v2.0.0 — the tool surface is `amicus_*` only, always. `AMICUS_LEGACY_ALIASES=1` (the v1.8.0 opt-in switch that used to restore the `sidecar_*` twins) is now a no-op: setting it on the MCP server entry changes nothing. See [docs/SHIMS.md](./SHIMS.md) for the removal record.
|
|
853
969
|
|
|
854
970
|
> The MCP server auto-detects whether it's running under Claude Code or Claude Desktop/Cowork (from the MCP `initialize` handshake) and passes the right `--client` value downstream — this drives context inclusion, MCP discovery, and session-dir resolution. If detection ever picks the wrong one, force it with `"env": {"AMICUS_MCP_CLIENT": "code-local"}` (or `code-web` / `cowork`) on the MCP server entry.
|
package/electron/ipc-setup.js
CHANGED
|
@@ -25,6 +25,10 @@ const { registerLocalProviderHandlers } = require('./ipc-setup-local');
|
|
|
25
25
|
* resolves through this local binding instead.
|
|
26
26
|
*/
|
|
27
27
|
function registerSetupHandlers(getMainWindow, { ipcMain = require('electron').ipcMain } = {}) {
|
|
28
|
+
// Offer-session catalog snapshots (V17/A4 + PR 199 B1/D2/A1) — the full
|
|
29
|
+
// lifetime contract lives in electron/offer-session.js.
|
|
30
|
+
const offerCatalogs = require('./offer-session').createOfferSessions();
|
|
31
|
+
|
|
28
32
|
ipcMain.handle('sidecar:validate-key', async (_event, provider, key) => {
|
|
29
33
|
try {
|
|
30
34
|
const { validateApiKey } = require('../src/utils/api-key-store');
|
|
@@ -62,6 +66,7 @@ function registerSetupHandlers(getMainWindow, { ipcMain = require('electron').ip
|
|
|
62
66
|
const { buildProviderDefaultChoices } = require('../src/utils/provider-default-picker');
|
|
63
67
|
const catalog = await getCatalog();
|
|
64
68
|
result.providerDefault = buildProviderDefaultChoices(provider, { catalog });
|
|
69
|
+
offerCatalogs.set(_event, provider, catalog);
|
|
65
70
|
} catch (err) {
|
|
66
71
|
logger.error('save-key providerDefault error', { error: err.message });
|
|
67
72
|
result.providerDefault = null;
|
|
@@ -80,22 +85,34 @@ function registerSetupHandlers(getMainWindow, { ipcMain = require('electron').ip
|
|
|
80
85
|
// Task 8: apply a per-provider default picker choice. Read-modify-write,
|
|
81
86
|
// no-clobber -- applyProviderDefault only ever writes aliases[vendor] and
|
|
82
87
|
// seeds config.default when absent (see provider-default-picker.js).
|
|
83
|
-
//
|
|
84
|
-
// uses directFormIfProven (model-canonicalization.js)
|
|
85
|
-
// strip an OpenRouter prefix off chosenId, and needs
|
|
88
|
+
// Applies against the save-key offer's catalog snapshot (V17 / issue 195):
|
|
89
|
+
// applyProviderDefault uses directFormIfProven (model-canonicalization.js)
|
|
90
|
+
// to decide whether to strip an OpenRouter prefix off chosenId, and needs
|
|
91
|
+
// the catalog the offer was built from to do it. Fetching fresh only when
|
|
92
|
+
// no snapshot exists (no prior offer, or the offer session already ended
|
|
93
|
+
// via setup-done).
|
|
86
94
|
ipcMain.handle('sidecar:set-provider-default', async (_event, provider, chosenId) => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
// Reads WITHOUT consuming (PR 199 B1/D2 re-ruled after review F1): the
|
|
96
|
+
// user's pick is routinely the second-or-later apply for one offer
|
|
97
|
+
// (auto-apply on render, re-apply per radio change), and each must see
|
|
98
|
+
// the catalog the visible rows were built from. Staleness is bounded by
|
|
99
|
+
// the offer session instead: setup-done clears the map, a re-offer
|
|
100
|
+
// overwrites the entry.
|
|
101
|
+
let catalog = offerCatalogs.get(_event, provider);
|
|
102
|
+
if (!catalog) {
|
|
103
|
+
catalog = [];
|
|
104
|
+
try {
|
|
105
|
+
const { getCatalog } = require('../src/utils/model-catalog');
|
|
106
|
+
catalog = await getCatalog();
|
|
107
|
+
} catch (err) {
|
|
108
|
+
// Best-effort only -- a fetch failure leaves `catalog` empty, which
|
|
109
|
+
// directFormIfProven (F1, council review of PR 198) reads as NO
|
|
110
|
+
// evidence, never as license to strip: chosenId is persisted exactly
|
|
111
|
+
// as given, not re-derived. Applying an already-made picker choice
|
|
112
|
+
// must never abort on a catalog hiccup, and must never fabricate an
|
|
113
|
+
// id on one either -- that was the exact bug issue 195 fixed.
|
|
114
|
+
logger.error('set-provider-default catalog fetch error', { error: err.message });
|
|
115
|
+
}
|
|
99
116
|
}
|
|
100
117
|
try {
|
|
101
118
|
const { applyProviderDefault } = require('../src/utils/provider-default-picker');
|
|
@@ -135,6 +152,10 @@ function registerSetupHandlers(getMainWindow, { ipcMain = require('electron').ip
|
|
|
135
152
|
});
|
|
136
153
|
|
|
137
154
|
ipcMain.handle('sidecar:setup-done', (_event, defaultModel, keyCount) => {
|
|
155
|
+
// The offer session ends with the wizard: any apply after this fetches
|
|
156
|
+
// fresh evidence rather than reusing a closed offer's catalog (B1/D2).
|
|
157
|
+
// A1: only THIS sender's offer sessions end here — never another window's.
|
|
158
|
+
offerCatalogs.endSession(_event);
|
|
138
159
|
const { BrowserWindow } = require('electron');
|
|
139
160
|
const senderWindow = BrowserWindow.fromWebContents(_event.sender);
|
|
140
161
|
const mainWin = getMainWindow();
|
|
@@ -163,11 +184,13 @@ function registerSetupHandlers(getMainWindow, { ipcMain = require('electron').ip
|
|
|
163
184
|
let cfg = loadConfig();
|
|
164
185
|
if (!cfg) {
|
|
165
186
|
const { toLiveSeedAliases } = require('../src/utils/quick-picks');
|
|
166
|
-
|
|
187
|
+
// issue 214: getCatalogInfo, not getCatalog -- toLiveSeedAliases PERSISTS
|
|
188
|
+
// these routes, so it must see which namespaces were rejected.
|
|
189
|
+
let catalogInfo = { models: [] };
|
|
167
190
|
try {
|
|
168
|
-
|
|
191
|
+
catalogInfo = await require('../src/utils/model-catalog').getCatalogInfo();
|
|
169
192
|
} catch (_err) { /* offline: pinned seeds */ }
|
|
170
|
-
cfg = { aliases: toLiveSeedAliases(
|
|
193
|
+
cfg = { aliases: toLiveSeedAliases(catalogInfo) };
|
|
171
194
|
}
|
|
172
195
|
if (!cfg.aliases) { cfg.aliases = {}; }
|
|
173
196
|
if (defaultModel) { cfg.default = defaultModel; }
|
package/electron/main.js
CHANGED
|
@@ -327,12 +327,20 @@ function createAmicusWindow() {
|
|
|
327
327
|
async function createSetupWindow() {
|
|
328
328
|
// Lazy-load setup UI to avoid loading it for sidecar mode
|
|
329
329
|
const { buildSetupHTML } = require('./setup-ui');
|
|
330
|
-
const { resolveQuickPicks, toStorableRoute } = require('../src/utils/quick-picks');
|
|
330
|
+
const { resolveQuickPicks, canonicalRoutesFor, toStorableRoute } = require('../src/utils/quick-picks');
|
|
331
331
|
let quickPicks;
|
|
332
332
|
const shortlists = {};
|
|
333
333
|
try {
|
|
334
|
-
|
|
334
|
+
// issue 208: getCatalogInfo (not getCatalog) -- the shortlist needs the
|
|
335
|
+
// per-provider fetch outcomes, or directFormIfSafe's namespace-failure
|
|
336
|
+
// gate cannot fire and a rejected namespace still yields bare direct ids.
|
|
337
|
+
const catalogInfo = await require('../src/utils/model-catalog').getCatalogInfo();
|
|
338
|
+
const catalog = catalogInfo.models;
|
|
335
339
|
quickPicks = resolveQuickPicks(catalog);
|
|
340
|
+
// issue 214: decide each pick's safe storable form HERE, with the catalog in
|
|
341
|
+
// hand, and ship it as data. The page cannot require() the canonicalisation
|
|
342
|
+
// primitives, and its hand-copy of them dropped their guards.
|
|
343
|
+
for (const p of quickPicks) { p.canonicalRoutes = canonicalRoutesFor(p, catalogInfo); }
|
|
336
344
|
|
|
337
345
|
// issue 138: one vendor shortlist per family card, resolved server-side from
|
|
338
346
|
// the same catalog the quick picks came from (no extra IPC round-trip).
|
|
@@ -341,7 +349,8 @@ async function createSetupWindow() {
|
|
|
341
349
|
try {
|
|
342
350
|
shortlists[p.alias] = buildModelShortlist(p.vendorPath, {
|
|
343
351
|
catalog,
|
|
344
|
-
|
|
352
|
+
providerFailures: catalogInfo.providerFailures,
|
|
353
|
+
recommendedId: toStorableRoute(p, catalogInfo),
|
|
345
354
|
});
|
|
346
355
|
} catch (_e) { /* a shortlist failure must never block the wizard */ }
|
|
347
356
|
}
|
|
@@ -520,21 +529,26 @@ function createSettingsChildWindow() {
|
|
|
520
529
|
// createSetupWindow awaits. A missing or corrupt cache reads back as null
|
|
521
530
|
// and degrades to the same pinned fallback buildSetupHTML already applies
|
|
522
531
|
// when no quickPicks are given.
|
|
523
|
-
const { resolveQuickPicks, toStorableRoute } = require('../src/utils/quick-picks');
|
|
532
|
+
const { resolveQuickPicks, canonicalRoutesFor, toStorableRoute } = require('../src/utils/quick-picks');
|
|
524
533
|
const { readCache } = require('../src/utils/model-catalog');
|
|
525
534
|
let quickPicks;
|
|
526
535
|
const shortlists = {};
|
|
527
536
|
try {
|
|
528
537
|
const cacheDoc = readCache();
|
|
529
538
|
const catalog = cacheDoc ? cacheDoc.models : [];
|
|
539
|
+
// issue 208: the cache doc carries the fetch outcomes for THESE rows.
|
|
540
|
+
const providerFailures = (cacheDoc && cacheDoc.providerFailures) || [];
|
|
530
541
|
quickPicks = resolveQuickPicks(catalog);
|
|
542
|
+
// issue 214: see the note at the other resolveQuickPicks site.
|
|
543
|
+
for (const p of quickPicks) { p.canonicalRoutes = canonicalRoutesFor(p, { models: catalog, providerFailures }); }
|
|
531
544
|
|
|
532
545
|
const { buildModelShortlist } = require('../src/utils/model-shortlist');
|
|
533
546
|
for (const p of quickPicks) {
|
|
534
547
|
try {
|
|
535
548
|
shortlists[p.alias] = buildModelShortlist(p.vendorPath, {
|
|
536
549
|
catalog,
|
|
537
|
-
|
|
550
|
+
providerFailures,
|
|
551
|
+
recommendedId: toStorableRoute(p, { models: catalog, providerFailures }),
|
|
538
552
|
});
|
|
539
553
|
} catch (_e) { /* a shortlist failure must never block the wizard */ }
|
|
540
554
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offer-session catalog snapshots for the setup wizard's provider-default
|
|
3
|
+
* flow (extracted from ipc-setup.js at the 300-line gate, v4.9 / PR 199).
|
|
4
|
+
*
|
|
5
|
+
* V17 (council A4): each save-key offer snapshots the catalog it was built
|
|
6
|
+
* from, so set-provider-default applies against the SAME catalog the picker
|
|
7
|
+
* offered — a re-fetch there could return a different catalog and flip
|
|
8
|
+
* directFormIfProven's evidence (TOCTOU).
|
|
9
|
+
*
|
|
10
|
+
* Lifetime = the OFFER SESSION (PR 199 B1/D2, re-ruled after review F1):
|
|
11
|
+
* the wizard auto-applies on render and re-applies on every radio change,
|
|
12
|
+
* so EVERY apply while the offer is on screen must see the offer's own
|
|
13
|
+
* catalog — a one-shot delete-on-read handed every human pick a fresh
|
|
14
|
+
* fetch, which is the original A4 race. A re-offer overwrites the entry;
|
|
15
|
+
* setup-done ends the sender's sessions.
|
|
16
|
+
*
|
|
17
|
+
* Keyed per SENDER + provider (PR 199 round-2 council A1): two Settings
|
|
18
|
+
* windows are independent offer sessions — one window's offer, apply, or
|
|
19
|
+
* completion must never alter another's. A harness event with no sender id
|
|
20
|
+
* keys by provider alone (also the single-window behavior), and its
|
|
21
|
+
* endSession ends every session.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
function createOfferSessions() {
|
|
27
|
+
const snapshots = new Map();
|
|
28
|
+
const senderId = (event) => {
|
|
29
|
+
const sid = event && event.sender ? event.sender.id : undefined;
|
|
30
|
+
return (sid === undefined || sid === null) ? null : sid;
|
|
31
|
+
};
|
|
32
|
+
const key = (event, provider) => {
|
|
33
|
+
const sid = senderId(event);
|
|
34
|
+
return (sid !== null ? sid + ':' : '') + provider;
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
/** Arm (or re-arm) the sender's offer session for a provider. */
|
|
38
|
+
set(event, provider, catalog) { snapshots.set(key(event, provider), catalog); },
|
|
39
|
+
/** The sender's live offer catalog for a provider, or undefined. */
|
|
40
|
+
get(event, provider) { return snapshots.get(key(event, provider)); },
|
|
41
|
+
/** End every offer session belonging to this sender (setup-done). */
|
|
42
|
+
endSession(event) {
|
|
43
|
+
const sid = senderId(event);
|
|
44
|
+
for (const k of [...snapshots.keys()]) {
|
|
45
|
+
if (sid === null || k.startsWith(sid + ':')) { snapshots.delete(k); }
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { createOfferSessions };
|