From 11bd7ca135ba44a8373bdd15f27b53375f9c7ed5 Mon Sep 17 00:00:00 2001 From: Alex TYRODE Date: Mon, 12 Oct 2026 10:29:57 +0000 Subject: [PATCH 1/6] Verification: a model whose check got no clear answer is left out, not every model measured beside it A benchmark result OMP reports as unresolved or unmatched made deriveCatalog refuse code_probe_inconclusive_probe for the whole list, so one provider that could not be checked (a key without credit, a timeout, an unclassified wording) discarded every model measured beside it and nothing was saved. Such a model is now excluded with the reason inconclusive, exactly as a refused one is, and the Models sheet names it "no answer". The derive refusals that remain have words instead of "Code refused the step". --- docs/configuration.md | 18 +++++++++++------- plugins/code/generator/sheets-model.ts | 1 + plugins/code/workflow.ts | 3 +++ plugins/domain/probe.test.ts | 17 +++++++++++++---- plugins/domain/probe.ts | 20 ++++++++++++++------ 5 files changed, 42 insertions(+), 17 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index c45c6a6..f1d63d3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -152,11 +152,14 @@ rungs any other family's listing ladders (DeepSeek, OpenRouter, any other provid such a rung that does not answer leaves that family shorter. A family Code requires is laddered on whatever of it answers, short below three rungs, and one none of whose models answers is left out as a family with no account is; a selection that needs it is refused -where it is reviewed. `deriveCatalog` returns the document and every other offered model -with its reason; the document's compiled catalog names its short families. It refuses only a -family Code requires whose ladder would put a model above one that costs no more and thinks deeper -(`code_ladder_regression`, naming both), a benchmark that does not answer the charge exactly, -and nothing laddered at all (`code_probe_insufficient_ladder`). Only the returned `confirm` +where it is reviewed. A model whose probe got no answer the benchmark could read (OMP's +`unresolved` or `unmatched`) is left out as `inconclusive`, as one that does not answer is, +rather than discarding every model measured beside it. `deriveCatalog` returns the document +and every other offered model with its reason; the document's compiled catalog names its +short families. It refuses only a family Code requires whose ladder would put a model above +one that costs no more and thinks deeper (`code_ladder_regression`, naming both), a benchmark +that does not answer the charge exactly, and nothing laddered at all +(`code_probe_insufficient_ladder`). Only the returned `confirm` spends: one benchmark job per provider, then derive, stage, review, promote and save the selection narrowed to what the verified catalog hosts. Every step re-observes the revision, the exact account pool, OMP defaults and OMP itself, and stops, naming itself, @@ -780,8 +783,9 @@ verifying, a staged list's changes (field by field, a model added or dropped, th value struck), or, after a verification in this panel, the models it left out, each with its reason in a word: a model the accounts' plan, their settings or this client excludes (`client_blocked`) reads **not served**, one no account finds (`not_found`) **not -reachable**, a model in OMP's `spark` quota class **retired** and any other class of its -own **own quota**. Ids, prices, speeds, thinking ranges and reasons +reachable**, one whose check got no clear answer (`inconclusive`) **no answer**, a model in +OMP's `spark` quota class **retired** and any other class of its own **own quota**. Ids, +prices, speeds, thinking ranges and reasons appear only in the readout line while a cell, a provider or a left-out model is pointed or focused. One next action sits where the launch sits and follows the step: **verify models**, diff --git a/plugins/code/generator/sheets-model.ts b/plugins/code/generator/sheets-model.ts index e82d5ab..640e04f 100644 --- a/plugins/code/generator/sheets-model.ts +++ b/plugins/code/generator/sheets-model.ts @@ -93,6 +93,7 @@ const EXCLUSION_WORDS: Readonly> = { code_invalid_catalog: "The model list could not be used.", code_preview_changed: "The reviewed model list changed. Review it again.", code_starter_candidate_limit: "The bundled model list is too large to verify at once.", + code_probe_missing_probe: "OMP's check left a model without an answer. Verify again.", + code_probe_insufficient_ladder: "No model answered its check with these accounts.", + code_ladder_regression: "Among the models that answered, a dearer one thinks less than a cheaper one.", code_verification_cancelled: "Verification cancelled.", code_verification_changed: "The machine, workspace or edit access changed, so the verification stopped.", code_verification_confirmed: "That charge was already confirmed.", diff --git a/plugins/domain/probe.test.ts b/plugins/domain/probe.test.ts index cbf0f32..ce81e49 100644 --- a/plugins/domain/probe.test.ts +++ b/plugins/domain/probe.test.ts @@ -61,7 +61,7 @@ function receiptOf(models: Row[]): InventoryReceipt { return { schemaVersion: 1, kind: "inventory", ompVersion: OMP_VERSION, observedAt: 100, models }; } /** A benchmark of every candidate: reachable, except the ids given another verdict. */ -function observed(inv: InventoryReceipt, verdicts: Record = {}, +function observed(inv: InventoryReceipt, verdicts: Record = {}, options: ScaffoldOptions = { budget: "any" }) { const input = benchmarkCandidates(inv, options), receipt = parseBenchmarkObservation(report(input), input, 101, 200); return { ...receipt, results: receipt.results.map(result => verdicts[result.id] @@ -232,13 +232,22 @@ describe("pure typed scaffolding", () => { expect([tiers(short, "anthropic"), compileCatalog(short).short]).toEqual([[[1, "claude-sonnet-5"], [2, "claude-opus-5"]], ["anthropic"]]); expect(compileCatalog(derived(inv).document).short).toEqual([]); }); - test("missing, stale, wrong API and inconclusive probes refuse promotion", () => { + test("missing, stale and wrong-API probes refuse promotion", () => { const inv = fullInventory(), input = benchmarkCandidates(inv), receipt = parseBenchmarkObservation(report(input), input, 101, 200); expect(() => catalogFromObservations(inv, { ...receipt, results: receipt.results.slice(1) })).toThrow("probe_missing_probe"); expect(() => catalogFromObservations(inv, { ...receipt, inventoryObservedAt: 99 })).toThrow("probe_missing_probe"); expect(() => catalogFromObservations(inv, { ...receipt, results: receipt.results.map((result, i) => i ? result : { ...result, api: "other" }) })).toThrow("probe_missing_probe"); - for (const status of ["unresolved", "unmatched"]) { - expect(() => catalogFromObservations(inv, { ...receipt, results: receipt.results.map((result, i) => i ? result : { ...result, status, tokensPerSecond: null, timeToFirstTokenMs: null }) })).toThrow("probe_inconclusive_probe"); + }); + test("an inconclusive probe leaves only its own model out, named, as a refused one is", () => { + const inv = fullInventory(); + inv.models.push({ ...inv.models[0]!, provider: "deepseek", api: "openai-completions", id: "deepseek-v4" }); + const refused = catalogFromObservations(inv, observed(inv, { "gpt-pro-6": "client_blocked", "deepseek-v4": "not_found" })); + for (const status of ["unresolved", "unmatched"] as const) { + // One model of a provider that answered otherwise, and the whole of a provider that could not be checked at all. + const derived = catalogFromObservations(inv, observed(inv, { "gpt-pro-6": status, "deepseek-v4": status })); + expect(derived.document).toEqual(refused.document); + expect(derived.exclusions).toEqual(refused.exclusions.map(exclusion => + exclusion.id === "gpt-pro-6" || exclusion.id === "deepseek-v4" ? { ...exclusion, reason: "inconclusive" } : exclusion)); } }); test("every old version is probed before supersession, blocked newest cannot displace callable older", () => { diff --git a/plugins/domain/probe.ts b/plugins/domain/probe.ts index 469210c..b10573e 100644 --- a/plugins/domain/probe.ts +++ b/plugins/domain/probe.ts @@ -33,15 +33,17 @@ export type ScaffoldOptions = z.infer; * `superseded`: a newer version of its family is used instead. `unstable_id`: a rolling alias, * preview or experiment, never probed or laddered. `not_found` and `client_blocked`: the * benchmark's own verdicts; a blocked model is the operator's to fix, so it is named, as the old - * scaffold's warnings named it. `regression`: as a rung it would think less than a cheaper rung - * below it. `separate_quota`: it draws a quota of its own, which Code does not spend, named - * by OMP's class for it (`quotaTier`), so a reader can tell Spark's retired class from another. + * scaffold's warnings named it. `inconclusive`: its probe got no answer the benchmark could read + * (OMP's `unresolved` or `unmatched`), so it is left out until a later verification settles it. + * `regression`: as a rung it would think less than a cheaper rung below it. `separate_quota`: it + * draws a quota of its own, which Code does not spend, named by OMP's class for it (`quotaTier`), + * so a reader can tell Spark's retired class from another. * A model that is merely not chosen (a third middle, a model priced above the top, a model whose * id names none of its provider's tier names) has no such reason and is not listed. */ const excludedIdentity = { provider: ProbeIdentitySchema.shape.provider, id: ProbeIdentitySchema.shape.id }; export const ExclusionSchema = z.discriminatedUnion("reason", [ - z.strictObject({ ...excludedIdentity, reason: z.enum(["superseded", "unstable_id", "not_found", "client_blocked", "regression"]) }), + z.strictObject({ ...excludedIdentity, reason: z.enum(["superseded", "unstable_id", "not_found", "client_blocked", "inconclusive", "regression"]) }), z.strictObject({ ...excludedIdentity, reason: z.literal("separate_quota"), quotaTier: QuotaTierSchema }), ]); export type Exclusion = z.infer; @@ -411,7 +413,13 @@ export function inventoryDraft(inventoryValue: unknown, budget: Selection["budge return { schemaVersion: 1, kind: "draft", inventoryObservedAt: inventory.observedAt, benchmark: benchmarkInput(inventory, models), exclusions: [...unstableExclusions(inventory, options.budget), ...unprobed].sort((a, b) => compare(probeAddress(a), probeAddress(b))) }; } -/** Every probed candidate (`probeSet`) must have an exact probe, before superseding older versions. */ +/** + * Every probed candidate (`probeSet`) must have an exact probe, before superseding older versions. + * Only a reachable model enters the catalog. One whose probe was inconclusive is left out and named, + * like a refused one, rather than refusing the whole catalog: a provider that could not be checked + * (a key without credit, a timeout, a wording OMP does not classify) must not discard every model + * measured beside it. + */ export function catalogFromObservations(inventoryValue: unknown, benchmarkValue: unknown, optionsValue: unknown = { budget: "any" }): DerivedCatalog { const inventory = parse(InventoryReceiptSchema, inventoryValue), benchmark = parse(BenchmarkReceiptSchema, benchmarkValue); const options = parse(ScaffoldOptionsSchema, optionsValue, "invalid_input"); @@ -424,12 +432,12 @@ export function catalogFromObservations(inventoryValue: unknown, benchmarkValue: for (const candidate of candidates) { const fact = facts.get(probeAddress(candidate)); if (!fact || fact.api !== candidate.api || fact.key !== candidate.key) throw new ProbeError("missing_probe"); - if (fact.status === "unmatched" || fact.status === "unresolved") throw new ProbeError("inconclusive_probe"); } const refused: Exclusion[] = []; for (const model of offered) { const status = facts.get(probeAddress(model))!.status; if (status === "not_found" || status === "client_blocked") refused.push(exclusion(model, status)); + else if (status === "unresolved" || status === "unmatched") refused.push(exclusion(model, "inconclusive")); } return scaffold(offered.filter(model => facts.get(probeAddress(model))!.status === "reachable"), [...unstableExclusions(inventory, options.budget), ...unprobed, ...refused], facts); From 0f76c42e991980bf4cb358766e5ae5c43291c9fe Mon Sep 17 00:00:00 2001 From: Alex TYRODE Date: Mon, 12 Oct 2026 13:01:36 +0000 Subject: [PATCH 2/6] Verification: a model with no clear answer never shortens the list in use or moves the team off its family A benchmark reads an outage, a rate limit or a broken credential as unresolved, the same as a model the account cannot use. Leaving such a model out is right for a first list, but a verification again during an outage would have shortened the list in use and narrowed the saved selection with it. The run now stops at derive, naming the models (code_models_unanswered), when an inconclusive model is in the list in use or when narrowing would move the selection off the provider lane it names because none of that family answered; the list and the saved selection stay as they were. The derive refusals read as the receipts and ladders they describe, and a ladder regression keeps its identifiers as written. --- docs/configuration.md | 7 ++++- plugins/code/generator/sheets-model.ts | 2 +- plugins/code/workflow.ts | 37 +++++++++++++++++++++----- plugins/domain/probe.test.ts | 2 +- plugins/test/workflow.test.ts | 24 ++++++++++++++++- 5 files changed, 62 insertions(+), 10 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index f1d63d3..9b6f693 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -161,7 +161,12 @@ one that costs no more and thinks deeper (`code_ladder_regression`, naming both) that does not answer the charge exactly, and nothing laddered at all (`code_probe_insufficient_ladder`). Only the returned `confirm` spends: one benchmark job per provider, then derive, stage, review, promote and save -the selection narrowed to what the verified catalog hosts. Every step re-observes the +the selection narrowed to what the verified catalog hosts. An `inconclusive` model may be +left out only where nothing already chosen needs it: when it is in the list in use, or when +narrowing would move the selection off the provider lane it names because none of that +family answered, the run stops at derive (`code_models_unanswered`, naming the models) +with the list and the saved selection unchanged, since an outage or a rate limit reads the +same as a model the account cannot use. Every step re-observes the revision, the exact account pool, OMP defaults and OMP itself, and stops, naming itself, when one moved, on a refusal or on cancellation. A stop never undoes: probe jobs stay in OMP's history and a staged catalog stays staged. A confirmation is used once, and a failed diff --git a/plugins/code/generator/sheets-model.ts b/plugins/code/generator/sheets-model.ts index 640e04f..5470917 100644 --- a/plugins/code/generator/sheets-model.ts +++ b/plugins/code/generator/sheets-model.ts @@ -93,7 +93,7 @@ const EXCLUSION_WORDS: Readonly> = { code_invalid_catalog: "The model list could not be used.", code_preview_changed: "The reviewed model list changed. Review it again.", code_starter_candidate_limit: "The bundled model list is too large to verify at once.", - code_probe_missing_probe: "OMP's check left a model without an answer. Verify again.", - code_probe_insufficient_ladder: "No model answered its check with these accounts.", - code_ladder_regression: "Among the models that answered, a dearer one thinks less than a cheaper one.", + code_probe_missing_probe: "OMP's results do not match the models it was asked to check. Verify again.", + code_probe_insufficient_ladder: "No model answered its check, so there is no list to build.", + code_ladder_regression: "Among the models that answered, a higher tier thinks less than a lower one that costs no more.", + code_models_unanswered: "Models this profile relies on gave no clear answer, so nothing was changed. Verify again later.", code_verification_cancelled: "Verification cancelled.", code_verification_changed: "The machine, workspace or edit access changed, so the verification stopped.", code_verification_confirmed: "That charge was already confirmed.", @@ -92,6 +94,8 @@ const agentRefusals: Readonly> = { /** Refusals whose detail is a list of identifiers, with the label that introduces it; the list stays exactly as written. */ const listedDetails: Readonly> = { code_model_unpublished: "Not listed:", + code_models_unanswered: "No answer:", + code_ladder_regression: "In", }; /** How a browser's dispatch reports a refused door (machine-web.ts `codeWorkflow`): `.: . No approval or readiness is assumed.` */ const DOOR_DENIAL = /^[a-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+: ([^]*?)(?:\. No approval or readiness is assumed\.)?$/; @@ -120,6 +124,20 @@ export function failureWords(message: string): string { return Object.hasOwn(listedDetails, token!) ? `${words} ${listedDetails[token!]} ${detail}` : `${words} ${detail.charAt(0).toUpperCase()}${detail.slice(1)}`; } +/** + * The left-out models a verification may not lose merely because they gave no clear answer + * (`inconclusive`): one the list in use has, and every one of the family whose provider lane the + * wanted selection named when narrowing moved off that family. An outage, a rate limit or a + * wording OMP cannot read would otherwise shorten the list in use, or move the team off the family + * it chose, which only a provider's own refusal of a model may do. + */ +function unansweredNeeds(exclusions: readonly Exclusion[], inUse: ReadonlySet, wanted: Selection | null, narrowed: Selection): string[] { + const want = wanted?.lane; + const lost = want?.kind === "provider" && (narrowed.lane.kind !== "provider" || narrowed.lane.family !== want.family) ? want.family : null; + return exclusions.filter(exclusion => exclusion.reason === "inconclusive" && + (inUse.has(probeAddress(exclusion)) || providerPolicy(exclusion.provider).family === lost)).map(probeAddress); +} + /** The steps of a model verification, in the order they run; a failure names the one it stopped at. */ export type VerificationStep = "observe" | "initialize" | "inventory" | "draft" | "benchmark" | "derive" | "stage" | "review" | "promote" | "select"; /** Requests a provider's benchmark has answered out of those it was charged. */ @@ -519,6 +537,8 @@ export function createCodeWorkflowClient(dispatch: Dispatch) { evidence.revision = record.revision; } const revision = record.revision, replacesDraft = record.draft !== null; + // The models the list in use has, which a model that merely gives no clear answer may not remove. + const inUse = new Set(record.active?.document.models.map(probeAddress) ?? []); run.enter("inventory"); // The model list OMP bundles now is the one the inventory picks its models from when it starts, so its revision is read // before the start: one republished while the inventory runs is then a moved OMP, refused before the charge is answered. @@ -549,11 +569,11 @@ export function createCodeWorkflowClient(dispatch: Dispatch) { confirm: (selection, confirmOptions = options) => { if (confirmed) return Promise.reject(new WorkflowError("code_verification_confirmed")); confirmed = true; - return confirmVerification(charge, selection, verificationRun(confirmOptions, { ...evidence, benchmarkJobIds: [] })); + return confirmVerification(charge, inUse, selection, verificationRun(confirmOptions, { ...evidence, benchmarkJobIds: [] })); }, }; } - async function confirmVerification(charge: ChargeReview, wanted: Selection | null, run: VerificationRun): Promise { + async function confirmVerification(charge: ChargeReview, inUse: ReadonlySet, wanted: Selection | null, run: VerificationRun): Promise { const { target } = charge; run.enter("benchmark"); await unchangedCharge(charge, run); @@ -583,6 +603,11 @@ export function createCodeWorkflowClient(dispatch: Dispatch) { let selection: Selection; try { selection = clampSelection(compileCatalog(derived.document), wanted); } catch (error) { throw run.fail(error instanceof Error ? error.message : "code_invalid_selection"); } + // A model with no clear answer stays out of the list only where nothing already chosen needs it, so a stop + // here keeps the list in use and the saved team exactly as they were. + const unanswered = unansweredNeeds(derived.exclusions, inUse, wanted, selection); + if (unanswered.length > 0) + throw run.fail(`code_models_unanswered: ${unanswered.slice(0, 8).join(", ")}${unanswered.length > 8 ? ` and ${unanswered.length - 8} more` : ""}`); run.enter("stage"); const pool = await unchangedCharge(charge, run); const staged = await run.call(() => code("stageCatalog", { containerId: target.containerId, expectedRevision: charge.revision, document: derived.document, diff --git a/plugins/domain/probe.test.ts b/plugins/domain/probe.test.ts index ce81e49..93e31c3 100644 --- a/plugins/domain/probe.test.ts +++ b/plugins/domain/probe.test.ts @@ -243,7 +243,7 @@ describe("pure typed scaffolding", () => { inv.models.push({ ...inv.models[0]!, provider: "deepseek", api: "openai-completions", id: "deepseek-v4" }); const refused = catalogFromObservations(inv, observed(inv, { "gpt-pro-6": "client_blocked", "deepseek-v4": "not_found" })); for (const status of ["unresolved", "unmatched"] as const) { - // One model of a provider that answered otherwise, and the whole of a provider that could not be checked at all. + // The run leaves one model of a provider that otherwise answered without an answer, and the whole of a provider that could not be checked at all. const derived = catalogFromObservations(inv, observed(inv, { "gpt-pro-6": status, "deepseek-v4": status })); expect(derived.document).toEqual(refused.document); expect(derived.exclusions).toEqual(refused.exclusions.map(exclusion => diff --git a/plugins/test/workflow.test.ts b/plugins/test/workflow.test.ts index 26e7def..87aabe1 100644 --- a/plugins/test/workflow.test.ts +++ b/plugins/test/workflow.test.ts @@ -47,7 +47,7 @@ function verificationFixture() { const hooks: Partial void | Promise>> = {}; const states = new Map(); const exitCodes = new Map(); - const verdicts: Record = {}; + const verdicts: Record = {}; const ctx: CodeContext = { now: () => 10_000, emit() {}, outsideScope: async () => null, auth: { principal: { id: "writer", kind: "human", name: "Writer", color: "#123456" }, @@ -311,6 +311,28 @@ test("a family Code requires that the accounts reach only in part is verified sh expect(verified.configuration.selection).toEqual({ ...preview, lane: { kind: "provider", family: "anthropic", blend: "only" }, capability: 3, spark: false }); }); +test("a model with no clear answer is left out of a first list, but never shortens the list in use or moves the team off its family", async () => { + // A first list: a provider that could not be checked at all is left out and named, and what answered is saved. + const f = verificationFixture(); + f.verdicts["deepseek-flash"] = "unresolved"; f.verdicts["deepseek-pro"] = "unmatched"; + const first = await (await f.workflow.verifyModels(target, { expectedRevision: 0, budget: "any" })).confirm(preview); + expect(first.configuration.active?.document.models.map(model => model.id)).toEqual(["claude-haiku-5", "claude-sonnet-5", "claude-opus-5"]); + expect(first.exclusions.filter(exclusion => exclusion.reason === "inconclusive").map(exclusion => exclusion.id)).toEqual(["deepseek-flash", "deepseek-pro"]); + // Verifying again while a model of the list in use gives no clear answer stops before anything is staged; the list and team stay. + f.verdicts["claude-opus-5"] = "unresolved"; + const before = await f.configuration(); + const again = await stopOf((await f.workflow.verifyModels(target, { expectedRevision: before.revision, budget: "any" })).confirm(first.configuration.selection)); + expect([again.step, again.reason]).toEqual(["derive", "code_models_unanswered: anthropic/claude-opus-5"]); + expect(await f.configuration()).toEqual(before); + // A first list for a team that chose a family none of whose models answered stops too, rather than saving another family's team. + const chosen = verificationFixture(); + chosen.verdicts["deepseek-flash"] = "unresolved"; chosen.verdicts["deepseek-pro"] = "unresolved"; + const deepseekOnly: Selection = { ...preview, lane: { kind: "provider", family: "deepseek", blend: "only" } }; + const refused = await stopOf((await chosen.workflow.verifyModels(target, { expectedRevision: 0, budget: "any" })).confirm(deepseekOnly)); + expect([refused.step, refused.reason]).toEqual(["derive", "code_models_unanswered: deepseek/deepseek-flash, deepseek/deepseek-pro"]); + expect(chosen.calls).not.toContain("atyrode.code.stageCatalog"); +}); + test("a derivation or selection the verified catalog cannot serve stops before anything is staged", async () => { // Nothing the accounts offer answers, so there is no catalog to stage. const blocked = verificationFixture(); From 7c03aa75bc7095cc2dd9212f4233c781d8fff384 Mon Sep 17 00:00:00 2001 From: Alex TYRODE Date: Mon, 12 Oct 2026 13:15:04 +0000 Subject: [PATCH 3/6] Verification: protect only what these accounts verified, both mixed-lane families, and nothing a newer version replaces The list in use is protected from a non-answer only when a verification with this exact account pool made it: a list verified with other accounts, or imported, vouches for nothing these accounts could keep, and a model that never answers would otherwise block every verification again. A lane names its families through laneFamilies, so the mixed lane protects both OpenAI and Anthropic, and the rule is judged before the selection is narrowed. A model with no clear answer that a newer, answering version supersedes, or that draws a quota of its own, is named as it would be had it answered, since its answer could not change the list. --- docs/configuration.md | 17 +++++++++------ plugins/code/workflow.ts | 40 ++++++++++++++++++++--------------- plugins/domain/probe.test.ts | 13 ++++++++++++ plugins/domain/probe.ts | 16 ++++++++++---- plugins/domain/routing.ts | 11 ++++++++-- plugins/test/workflow.test.ts | 25 ++++++++++++++++++++-- 6 files changed, 90 insertions(+), 32 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 9b6f693..9b4e6ce 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -153,8 +153,9 @@ such a rung that does not answer leaves that family shorter. A family Code requi laddered on whatever of it answers, short below three rungs, and one none of whose models answers is left out as a family with no account is; a selection that needs it is refused where it is reviewed. A model whose probe got no answer the benchmark could read (OMP's -`unresolved` or `unmatched`) is left out as `inconclusive`, as one that does not answer is, -rather than discarding every model measured beside it. `deriveCatalog` returns the document +`unresolved` or `unmatched`) is left out rather than discarding every model measured beside +it: as `inconclusive`, or as it would be had it answered where its answer could not change +the list (`superseded` by a newer version that answered, `separate_quota`). `deriveCatalog` returns the document and every other offered model with its reason; the document's compiled catalog names its short families. It refuses only a family Code requires whose ladder would put a model above one that costs no more and thinks deeper (`code_ladder_regression`, naming both), a benchmark @@ -162,11 +163,13 @@ that does not answer the charge exactly, and nothing laddered at all (`code_probe_insufficient_ladder`). Only the returned `confirm` spends: one benchmark job per provider, then derive, stage, review, promote and save the selection narrowed to what the verified catalog hosts. An `inconclusive` model may be -left out only where nothing already chosen needs it: when it is in the list in use, or when -narrowing would move the selection off the provider lane it names because none of that -family answered, the run stops at derive (`code_models_unanswered`, naming the models) -with the list and the saved selection unchanged, since an outage or a rate limit reads the -same as a model the account cannot use. Every step re-observes the +left out only where nothing already chosen needs it. When it is in the list in use as a +verification with this exact account pool made it, or belongs to a family the wanted lane +names (one provider, or OpenAI and Anthropic for the mixed lane) that the new list would not +have at all, the run stops at derive (`code_models_unanswered`, naming the models) before the +selection is narrowed, with the list and the saved selection unchanged, since an outage or a +rate limit reads the same as a model the account cannot use. A list verified with other +accounts, or never verified, holds no such claim. Every step re-observes the revision, the exact account pool, OMP defaults and OMP itself, and stops, naming itself, when one moved, on a refusal or on cancellation. A stop never undoes: probe jobs stay in OMP's history and a staged catalog stays staged. A confirmation is used once, and a failed diff --git a/plugins/code/workflow.ts b/plugins/code/workflow.ts index 3e873b9..fd0561b 100644 --- a/plugins/code/workflow.ts +++ b/plugins/code/workflow.ts @@ -15,7 +15,7 @@ import { compileCatalog } from "../domain/catalog.ts"; import { projectUsage } from "../domain/usage.ts"; import { providerPolicy } from "../domain/providers.ts"; import type { Exclusion } from "../domain/probe.ts"; -import { clampSelection } from "../domain/routing.ts"; +import { clampSelection, laneFamilies } from "../domain/routing.ts"; import { DomainError, type AccountChoices, type Selection } from "../domain/contracts.ts"; export type Dispatch = (door: string, input: unknown) => Promise; @@ -39,7 +39,7 @@ const messages: Readonly> = { code_probe_missing_probe: "OMP's results do not match the models it was asked to check. Verify again.", code_probe_insufficient_ladder: "No model answered its check, so there is no list to build.", code_ladder_regression: "Among the models that answered, a higher tier thinks less than a lower one that costs no more.", - code_models_unanswered: "Models this profile relies on gave no clear answer, so nothing was changed. Verify again later.", + code_models_unanswered: "Models the list in use or this team needs gave no clear answer, so the list and team were left as they were. Verify again later.", code_verification_cancelled: "Verification cancelled.", code_verification_changed: "The machine, workspace or edit access changed, so the verification stopped.", code_verification_confirmed: "That charge was already confirmed.", @@ -126,16 +126,17 @@ export function failureWords(message: string): string { /** * The left-out models a verification may not lose merely because they gave no clear answer - * (`inconclusive`): one the list in use has, and every one of the family whose provider lane the - * wanted selection named when narrowing moved off that family. An outage, a rate limit or a - * wording OMP cannot read would otherwise shorten the list in use, or move the team off the family - * it chose, which only a provider's own refusal of a model may do. + * (`inconclusive`): one the list in use has, and every one of a family the wanted selection's lane + * names that the new list (`families`) would not have at all. An outage, a rate limit or a wording + * OMP cannot read would otherwise shorten the list in use, or move the team off a family it chose, + * which only a provider's own refusal of a model may do. `inUse` is empty where no verification + * with this exact pool vouched for the list in use: evidence from other accounts, or a list never + * verified, holds nothing these accounts could keep. */ -function unansweredNeeds(exclusions: readonly Exclusion[], inUse: ReadonlySet, wanted: Selection | null, narrowed: Selection): string[] { - const want = wanted?.lane; - const lost = want?.kind === "provider" && (narrowed.lane.kind !== "provider" || narrowed.lane.family !== want.family) ? want.family : null; +function unansweredNeeds(exclusions: readonly Exclusion[], inUse: ReadonlySet, wanted: Selection | null, families: readonly string[]): string[] { + const lost = wanted ? laneFamilies(wanted.lane).filter(family => !families.includes(family)) : []; return exclusions.filter(exclusion => exclusion.reason === "inconclusive" && - (inUse.has(probeAddress(exclusion)) || providerPolicy(exclusion.provider).family === lost)).map(probeAddress); + (inUse.has(probeAddress(exclusion)) || lost.includes(providerPolicy(exclusion.provider).family))).map(probeAddress); } /** The steps of a model verification, in the order they run; a failure names the one it stopped at. */ @@ -537,13 +538,16 @@ export function createCodeWorkflowClient(dispatch: Dispatch) { evidence.revision = record.revision; } const revision = record.revision, replacesDraft = record.draft !== null; - // The models the list in use has, which a model that merely gives no clear answer may not remove. - const inUse = new Set(record.active?.document.models.map(probeAddress) ?? []); run.enter("inventory"); // The model list OMP bundles now is the one the inventory picks its models from when it starts, so its revision is read // before the start: one republished while the inventory runs is then a moved OMP, refused before the charge is answered. const [defaults, pool, catalogRevision] = await run.call(() => Promise.all([omp("readDefaults", {}), observePool(target.containerId, revision), readCatalogRevision()])); + // The models of the list in use, when it was verified with this exact pool: what a model that merely gives no clear + // answer may not remove. Spark's retired rung (tier 0) is never routed, so nothing needs it. + const active = record.active; + const inUse = new Set(active && active.provenance?.poolIdentityDigest === pool.poolIdentityDigest + ? active.document.models.filter(model => model.tier !== 0).map(probeAddress) : []); // The start is checked by the job's own wait, so a stop that lands now cancels the job rather than abandoning it. const job = await run.call(() => omp("startInventory", { ...target, expectedDefaultsRevision: defaults.revision, accountPool: pool.accountPool }), false); evidence.inventoryJobId = job.jobId; @@ -598,16 +602,18 @@ export function createCodeWorkflowClient(dispatch: Dispatch) { inventoryObservedAt: charge.inventory.observedAt, startedAt: Math.min(...receipts.map(receipt => receipt.startedAt)), completedAt: Math.max(...receipts.map(receipt => receipt.completedAt)), results: receipts.flatMap(receipt => receipt.results) }; const derived = await run.call(() => code("deriveCatalog", { inventory: charge.inventory, benchmark, budget: charge.budget })); + // A model with no clear answer stays out of the list only where nothing already chosen needs it, so a stop + // here keeps the list in use and the saved team exactly as they were. It is judged before the selection is + // narrowed, since a team whose family gave no answer could not be formed from this list either. + const families = [...new Set(derived.document.models.map(model => providerPolicy(model.provider).family))]; + const unanswered = unansweredNeeds(derived.exclusions, inUse, wanted, families); + if (unanswered.length > 0) + throw run.fail(`code_models_unanswered: ${unanswered.slice(0, 8).join(", ")}${unanswered.length > 8 ? ` and ${unanswered.length - 8} more` : ""}`); // Narrowed before anything is written, so a selection the verified catalog cannot serve stops // here with nothing staged, rather than after a promotion saved its default instead. let selection: Selection; try { selection = clampSelection(compileCatalog(derived.document), wanted); } catch (error) { throw run.fail(error instanceof Error ? error.message : "code_invalid_selection"); } - // A model with no clear answer stays out of the list only where nothing already chosen needs it, so a stop - // here keeps the list in use and the saved team exactly as they were. - const unanswered = unansweredNeeds(derived.exclusions, inUse, wanted, selection); - if (unanswered.length > 0) - throw run.fail(`code_models_unanswered: ${unanswered.slice(0, 8).join(", ")}${unanswered.length > 8 ? ` and ${unanswered.length - 8} more` : ""}`); run.enter("stage"); const pool = await unchangedCharge(charge, run); const staged = await run.call(() => code("stageCatalog", { containerId: target.containerId, expectedRevision: charge.revision, document: derived.document, diff --git a/plugins/domain/probe.test.ts b/plugins/domain/probe.test.ts index 93e31c3..59c54ce 100644 --- a/plugins/domain/probe.test.ts +++ b/plugins/domain/probe.test.ts @@ -250,6 +250,19 @@ describe("pure typed scaffolding", () => { exclusion.id === "gpt-pro-6" || exclusion.id === "deepseek-v4" ? { ...exclusion, reason: "inconclusive" } : exclusion)); } }); + test("a model with no clear answer is named as it would be had it answered wherever its answer could not change the list", () => { + const inv = fullInventory(); + inv.models.push({ ...inv.models.find(model => model.id === "claude-opus-5")!, id: "claude-opus-6" }); + inv.models.push({ ...inv.models[0]!, provider: "openai-codex", api: "openai-codex-responses", id: "gpt-spark-6", inputCostPerMillion: 0.01, quotaTier: "future-resource" }); + // A newer version that answered replaces it whatever it answers, and a quota Code does not spend never takes a rung. + const replaced = catalogFromObservations(inv, observed(inv, { "claude-opus-5": "unresolved", "gpt-spark-6": "unmatched" })); + expect(replaced.exclusions).toContainEqual({ provider: "anthropic", id: "claude-opus-5", reason: "superseded" }); + expect(replaced.exclusions).toContainEqual({ provider: "openai-codex", id: "gpt-spark-6", reason: "separate_quota", quotaTier: "future-resource" }); + // The newest version is the one whose answer decides the rung, so it is left out as inconclusive and its predecessor takes the rung. + const newest = catalogFromObservations(inv, observed(inv, { "claude-opus-6": "unresolved" })); + expect(newest.exclusions).toContainEqual({ provider: "anthropic", id: "claude-opus-6", reason: "inconclusive" }); + expect(tiers(newest.document, "anthropic")).toEqual([[1, "claude-haiku-5"], [2, "claude-sonnet-5"], [3, "claude-opus-5"]]); + }); test("every old version is probed before supersession, blocked newest cannot displace callable older", () => { const inv = fullInventory(); inv.models.push({ ...inv.models.find(model => model.id === "claude-opus-5")!, id: "claude-opus-6" }); diff --git a/plugins/domain/probe.ts b/plugins/domain/probe.ts index b10573e..e45f6c7 100644 --- a/plugins/domain/probe.ts +++ b/plugins/domain/probe.ts @@ -418,7 +418,9 @@ export function inventoryDraft(inventoryValue: unknown, budget: Selection["budge * Only a reachable model enters the catalog. One whose probe was inconclusive is left out and named, * like a refused one, rather than refusing the whole catalog: a provider that could not be checked * (a key without credit, a timeout, a wording OMP does not classify) must not discard every model - * measured beside it. + * measured beside it. Whether leaving it out may replace what is already chosen is the caller's to + * judge (workflow.ts `unansweredNeeds`), so it is named `inconclusive` only where its answer could + * have changed the list. */ export function catalogFromObservations(inventoryValue: unknown, benchmarkValue: unknown, optionsValue: unknown = { budget: "any" }): DerivedCatalog { const inventory = parse(InventoryReceiptSchema, inventoryValue), benchmark = parse(BenchmarkReceiptSchema, benchmarkValue); @@ -433,12 +435,18 @@ export function catalogFromObservations(inventoryValue: unknown, benchmarkValue: const fact = facts.get(probeAddress(candidate)); if (!fact || fact.api !== candidate.api || fact.key !== candidate.key) throw new ProbeError("missing_probe"); } + const reachable = offered.filter(model => facts.get(probeAddress(model))!.status === "reachable"); + // What replaces a model in the ladder (`scaffold`): a newer version of its family that answered and draws no quota of its own. + const laddered = reachable.filter(model => !separateClass(model)); const refused: Exclusion[] = []; for (const model of offered) { const status = facts.get(probeAddress(model))!.status; if (status === "not_found" || status === "client_blocked") refused.push(exclusion(model, status)); - else if (status === "unresolved" || status === "unmatched") refused.push(exclusion(model, "inconclusive")); + // A model with no clear answer is named as it would be had it answered wherever its answer could not change + // the list: one drawing a quota of its own, and one a newer, answering version replaces anyway. + else if (status === "unresolved" || status === "unmatched") refused.push(separateClass(model) + ? { provider: model.provider, id: model.id, reason: "separate_quota", quotaTier: model.quotaTier } + : exclusion(model, supersede([...laddered, model]).superseded.includes(model) ? "superseded" : "inconclusive")); } - return scaffold(offered.filter(model => facts.get(probeAddress(model))!.status === "reachable"), - [...unstableExclusions(inventory, options.budget), ...unprobed, ...refused], facts); + return scaffold(reachable, [...unstableExclusions(inventory, options.budget), ...unprobed, ...refused], facts); } diff --git a/plugins/domain/routing.ts b/plugins/domain/routing.ts index bf35f25..ab0e406 100644 --- a/plugins/domain/routing.ts +++ b/plugins/domain/routing.ts @@ -46,13 +46,20 @@ const weights: Record = { const thinkingCost: Record = { minimal: 0.6, low: 0.8, medium: 1, high: 1.3, xhigh: 1.6, max: 2 }; const thinkingSpeed: Record = { minimal: 1.4, low: 1.2, medium: 1, high: 0.8, xhigh: 0.65, max: 0.5 }; +/** The two families the mixed lane blends; it exists only where a catalog has both. */ +const MIXED_FAMILIES: readonly string[] = Object.freeze(["openai", "anthropic"]); +/** The families a lane names: its own, or the two the mixed lane blends. */ +export function laneFamilies(lane: Lane): readonly string[] { + return lane.kind === "mixed" ? MIXED_FAMILIES : [lane.family]; +} + function lanesFor(catalog: CompiledCatalog): Lane[] { const lanes: Lane[] = []; for (const family of catalog.families) { lanes.push({ kind: "provider", family, blend: "only" }); if (catalog.families.length > 1 && familyPolicy(family).crossTo !== null) lanes.push({ kind: "provider", family, blend: "led" }); } - if (catalog.families.includes("openai") && catalog.families.includes("anthropic")) lanes.push({ kind: "mixed" }); + if (MIXED_FAMILIES.every(family => catalog.families.includes(family))) lanes.push({ kind: "mixed" }); return lanes; } @@ -99,7 +106,7 @@ function selectionFacts(catalog: CompiledCatalog, input: Selection): { selection * refuses a capability the lead family lacks. */ export function defaultSelection(catalog: CompiledCatalog): Selection { - const lane: Lane = catalog.families.includes("openai") && catalog.families.includes("anthropic") + const lane: Lane = MIXED_FAMILIES.every(family => catalog.families.includes(family)) ? { kind: "mixed" } : { kind: "provider", family: catalog.families[0]!, blend: "only" }; const admitted: Selection = { lane, capability: 1, thinking: "medium", advisor: "glance", spark: false, priority: false, diff --git a/plugins/test/workflow.test.ts b/plugins/test/workflow.test.ts index 87aabe1..912b60b 100644 --- a/plugins/test/workflow.test.ts +++ b/plugins/test/workflow.test.ts @@ -311,7 +311,7 @@ test("a family Code requires that the accounts reach only in part is verified sh expect(verified.configuration.selection).toEqual({ ...preview, lane: { kind: "provider", family: "anthropic", blend: "only" }, capability: 3, spark: false }); }); -test("a model with no clear answer is left out of a first list, but never shortens the list in use or moves the team off its family", async () => { +test("a model with no clear answer is left out of a first list, but never shortens a list these accounts verified", async () => { // A first list: a provider that could not be checked at all is left out and named, and what answered is saved. const f = verificationFixture(); f.verdicts["deepseek-flash"] = "unresolved"; f.verdicts["deepseek-pro"] = "unmatched"; @@ -324,7 +324,28 @@ test("a model with no clear answer is left out of a first list, but never shorte const again = await stopOf((await f.workflow.verifyModels(target, { expectedRevision: before.revision, budget: "any" })).confirm(first.configuration.selection)); expect([again.step, again.reason]).toEqual(["derive", "code_models_unanswered: anthropic/claude-opus-5"]); expect(await f.configuration()).toEqual(before); - // A first list for a team that chose a family none of whose models answered stops too, rather than saving another family's team. + // Another pool's verification vouches for nothing these accounts could keep, so the same non-answer only leaves the model out. + f.omp.accounts.accounts.push(f.slot("anthropic", 3)); + const otherPool = await (await f.workflow.verifyModels(target, { expectedRevision: before.revision, budget: "any" })).confirm(first.configuration.selection); + expect(otherPool.configuration.active?.document.models.map(model => model.id)).toEqual(["claude-haiku-5", "claude-sonnet-5"]); + // So does a list in use that no verification made, such as an imported one. + const imported = await f.workflow.code("stageCatalog", { containerId: target.containerId, expectedRevision: otherPool.configuration.revision, + document: first.configuration.active!.document }); + const review = await f.workflow.code("reviewCatalog", { containerId: target.containerId, expectedRevision: imported.revision, source: "draft" }); + const unverified = await f.workflow.code("promoteCatalog", { containerId: target.containerId, expectedRevision: imported.revision, source: "draft", reviewDigest: review.reviewDigest }); + expect(unverified.active?.provenance).toBeNull(); + const replaced = await (await f.workflow.verifyModels(target, { expectedRevision: unverified.revision, budget: "any" })).confirm(unverified.selection); + expect(replaced.configuration.active?.document.models.map(model => model.id)).toEqual(["claude-haiku-5", "claude-sonnet-5"]); +}); + +test("a first list stops rather than move the team off a family it chose that gave no clear answer at all", async () => { + // The mixed lane names Claude: with no Claude model answering, saving would leave the team on another family. + const mixed = verificationFixture(); + for (const id of ["claude-haiku-5", "claude-sonnet-5", "claude-opus-5"]) mixed.verdicts[id] = "unresolved"; + const blended = await stopOf((await mixed.workflow.verifyModels(target, { expectedRevision: 0, budget: "any" })).confirm(preview)); + expect([blended.step, blended.reason]).toEqual(["derive", "code_models_unanswered: anthropic/claude-haiku-5, anthropic/claude-opus-5, anthropic/claude-sonnet-5"]); + expect(mixed.calls).not.toContain("atyrode.code.stageCatalog"); + // A team that chose DeepSeek alone stops the same way when DeepSeek does not answer. const chosen = verificationFixture(); chosen.verdicts["deepseek-flash"] = "unresolved"; chosen.verdicts["deepseek-pro"] = "unresolved"; const deepseekOnly: Selection = { ...preview, lane: { kind: "provider", family: "deepseek", blend: "only" } }; From 698a134523e388d4d5d29843ab6b2431a0494277 Mon Sep 17 00:00:00 2001 From: Alex TYRODE Date: Mon, 12 Oct 2026 13:28:08 +0000 Subject: [PATCH 4/6] Verification: a stop for models with no answer says how to get past it A non-answer can be permanent, so the stop also offers choosing a team or accounts that do without the named models, and the derivation comment names exactly when a model with no answer is named superseded or separate_quota. --- plugins/code/workflow.ts | 2 +- plugins/domain/probe.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/code/workflow.ts b/plugins/code/workflow.ts index fd0561b..8ceaf6c 100644 --- a/plugins/code/workflow.ts +++ b/plugins/code/workflow.ts @@ -39,7 +39,7 @@ const messages: Readonly> = { code_probe_missing_probe: "OMP's results do not match the models it was asked to check. Verify again.", code_probe_insufficient_ladder: "No model answered its check, so there is no list to build.", code_ladder_regression: "Among the models that answered, a higher tier thinks less than a lower one that costs no more.", - code_models_unanswered: "Models the list in use or this team needs gave no clear answer, so the list and team were left as they were. Verify again later.", + code_models_unanswered: "Models the list in use or this team needs gave no clear answer, so the list and team were left as they were. Verify again later, or choose a team or accounts that do without them.", code_verification_cancelled: "Verification cancelled.", code_verification_changed: "The machine, workspace or edit access changed, so the verification stopped.", code_verification_confirmed: "That charge was already confirmed.", diff --git a/plugins/domain/probe.ts b/plugins/domain/probe.ts index e45f6c7..4a0478f 100644 --- a/plugins/domain/probe.ts +++ b/plugins/domain/probe.ts @@ -419,8 +419,8 @@ export function inventoryDraft(inventoryValue: unknown, budget: Selection["budge * like a refused one, rather than refusing the whole catalog: a provider that could not be checked * (a key without credit, a timeout, a wording OMP does not classify) must not discard every model * measured beside it. Whether leaving it out may replace what is already chosen is the caller's to - * judge (workflow.ts `unansweredNeeds`), so it is named `inconclusive` only where its answer could - * have changed the list. + * judge (workflow.ts `unansweredNeeds`), so it is named `superseded` or `separate_quota` instead + * wherever a newer version that answered, or its own quota class, keeps it out whatever it answers. */ export function catalogFromObservations(inventoryValue: unknown, benchmarkValue: unknown, optionsValue: unknown = { budget: "any" }): DerivedCatalog { const inventory = parse(InventoryReceiptSchema, inventoryValue), benchmark = parse(BenchmarkReceiptSchema, benchmarkValue); From 9c82d829c79560fba16a2508e29cde14048e405b Mon Sep 17 00:00:00 2001 From: Alex TYRODE Date: Mon, 12 Oct 2026 13:46:58 +0000 Subject: [PATCH 5/6] Models: a verification again that stopped says so beside back to code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A stop after a current verification was only announced: the sheet kept reading "back to code · the generator routes on this list", so a verify again that kept the list in use looked like nothing had happened. The verified step now reads "verification stopped" beside its action, with the reason in the readout, as the unverified step already did; the stop message is shorter. --- docs/configuration.md | 4 +++- plugins/code/generator/models-sheet.tsx | 14 ++++++++++---- plugins/code/workflow.ts | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 9b4e6ce..17c05f6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -800,7 +800,9 @@ One next action sits where the launch sits and follows the step: **verify models **checking models…** (drawn with the checking hold the charge keeps), **confirm charge** (a single deliberate press of the exact charge shown, as in the launch line), **verifying…** with the requests measured, then **back to code** with **verify again** beside it, the -same Verify a current verification can always renew, its charge confirmed the same way. +same Verify a current verification can always renew, its charge confirmed the same way; +a verification again that stopped reads **verification stopped** beside it, with its +reason in the readout, since the list it would have replaced stays in use. A staged list offers **use staged list**, which reviews the staged list at its revision and promotes exactly that review with the list's own default team, and **discard**. An unsaved edit made on the list a write would replace holds it, saying what the write does diff --git a/plugins/code/generator/models-sheet.tsx b/plugins/code/generator/models-sheet.tsx index b7afa64..ae977c1 100644 --- a/plugins/code/generator/models-sheet.tsx +++ b/plugins/code/generator/models-sheet.tsx @@ -284,12 +284,18 @@ export function ModelsSheet({ host, model, machine, onBack, backRef, onPlace }: : { value: "use staged list", text: draft?.provenance ? "replaces the list in use with this verified one" : "replaces the list in use; it needs verifying after" }, press: () => { if (writeRefusal) return writeRefusal; useStaged(); return null; } }; } - case "verified": + case "verified": { // The head already says when the list was verified; beside the action, only its alternative: verifying again, through - // the same gate as a first verification, whose charge still waits on its own confirmation. - return { label: "back to code", parts: refused, fixes: verifyGate.open ? [{ label: "verify again", run: () => model.actions.verify() }] : [], - said: failure ? { value: "back to code", text: failure, warn: true } : { value: "back to code", text: "the generator routes on this list" }, + // the same gate as a first verification, whose charge still waits on its own confirmation. A verification again that + // stopped says so, as a first one does: the list it would have replaced stays in use, which the head alone cannot show. + const stopped = verification.failure; + const note: readonly Part[] = stopped ? [[stopped.cancelled ? "verification cancelled" : "verification stopped", stopped.cancelled ? null : "attention"]] : []; + return { label: "back to code", parts: [...note, ...refused], fixes: verifyGate.open ? [{ label: "verify again", run: () => model.actions.verify() }] : [], + said: failure ? { value: "back to code", text: failure, warn: true } + : stopped && !stopped.cancelled ? { value: "back to code", text: stopped.reason, warn: true } + : { value: "back to code", text: "the generator routes on this list" }, press: () => { leave(onBack); return null; } }; + } case "refused": return { label: "enable in setup", parts: [["Discovery is not enabled", "attention"], ["verifying needs it", null]], fixes: [], said: { value: "enable in setup", text: `Setup turns on model discovery for ${where}` }, press: () => { leave(() => onPlace("setup")); return null; } }; case "unverified": { diff --git a/plugins/code/workflow.ts b/plugins/code/workflow.ts index 8ceaf6c..40c403e 100644 --- a/plugins/code/workflow.ts +++ b/plugins/code/workflow.ts @@ -39,7 +39,7 @@ const messages: Readonly> = { code_probe_missing_probe: "OMP's results do not match the models it was asked to check. Verify again.", code_probe_insufficient_ladder: "No model answered its check, so there is no list to build.", code_ladder_regression: "Among the models that answered, a higher tier thinks less than a lower one that costs no more.", - code_models_unanswered: "Models the list in use or this team needs gave no clear answer, so the list and team were left as they were. Verify again later, or choose a team or accounts that do without them.", + code_models_unanswered: "Models the list in use or this team needs gave no clear answer, so the list and team stayed as they were. Verify again later, or choose a team or accounts without them.", code_verification_cancelled: "Verification cancelled.", code_verification_changed: "The machine, workspace or edit access changed, so the verification stopped.", code_verification_confirmed: "That charge was already confirmed.", From 44229261ca7731d17c7fcf4362d0f940044a1b18 Mon Sep 17 00:00:00 2001 From: Alex TYRODE Date: Mon, 12 Oct 2026 14:00:08 +0000 Subject: [PATCH 6/6] Models: the stopped note names what stays as it was, the list or the team --- docs/configuration.md | 3 ++- plugins/code/generator/models-sheet.tsx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 17c05f6..c6fec2e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -802,7 +802,8 @@ One next action sits where the launch sits and follows the step: **verify models with the requests measured, then **back to code** with **verify again** beside it, the same Verify a current verification can always renew, its charge confirmed the same way; a verification again that stopped reads **verification stopped** beside it, with its -reason in the readout, since the list it would have replaced stays in use. +reason in the readout, since what it would have changed (the list, or for a stop while +saving the profile, the team) stays as it was. A staged list offers **use staged list**, which reviews the staged list at its revision and promotes exactly that review with the list's own default team, and **discard**. An unsaved edit made on the list a write would replace holds it, saying what the write does diff --git a/plugins/code/generator/models-sheet.tsx b/plugins/code/generator/models-sheet.tsx index ae977c1..6781bb6 100644 --- a/plugins/code/generator/models-sheet.tsx +++ b/plugins/code/generator/models-sheet.tsx @@ -287,7 +287,8 @@ export function ModelsSheet({ host, model, machine, onBack, backRef, onPlace }: case "verified": { // The head already says when the list was verified; beside the action, only its alternative: verifying again, through // the same gate as a first verification, whose charge still waits on its own confirmation. A verification again that - // stopped says so, as a first one does: the list it would have replaced stays in use, which the head alone cannot show. + // stopped says so, as a first one does: what it would have changed (the list, or for a stop while saving the profile, + // the team) stays as it was, which the head alone cannot show. const stopped = verification.failure; const note: readonly Part[] = stopped ? [[stopped.cancelled ? "verification cancelled" : "verification stopped", stopped.cancelled ? null : "attention"]] : []; return { label: "back to code", parts: [...note, ...refused], fixes: verifyGate.open ? [{ label: "verify again", run: () => model.actions.verify() }] : [],