Repository navigation
chore(models): refresh stale model defaults (NodeVoice) - #16
Merged
Merged
Conversation
Remove the gpt-4.1-nano router row (OpenAI retirement 2026-10-23), migrate stored rooms via rooms:migrateRetiredRoomModels, guard the coordinator with validModel(room.model), mark gpt-5-nano/gpt-5-mini as retiring 2026-12-11, prune scripts/model-eval.mjs, and import DEFAULT_MODEL in the two literal copies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
…ost-deploy-only runbook Codex review round 1: the web-research branch wrapped a stored worker.model in validModel, which collapsed a valid out-of-picker id (e.g. an OPENAI_WEB_MODEL override stored by completeV3Worker and copied by retryV3Worker) to gpt-5.4-mini. Restore the caller argument; guard only the room.model fallback. Add the create -> migrate -> audit -> next-turn test for one room and a worker argument-preservation test. Runbook: run the migration once after the deploy. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ape tests; picker retirement note; pricing as-of Review round 1 on PR 16. - convex/rooms.ts: spawnAgentOsGoal stored room.model on the execution_plan worker, so a steer in an unmigrated room created a worker that sent gpt-4.1-nano. Store validModel(room.model). - tests: request-shape tests for the human-steer interpreter and for a planning worker spawned through applyHumanIntent then run by runV3Worker (and the runV3Worker fallback); reverting each validModel guard now fails a test. Picker test renders the real <ModelSelect> options. - LiveRoom: dropdown options carry "retires 2026-12-11" for rows that have it. - scripts/model-eval.mjs: PRICING_AS_OF stamp (date typed, not re-verified) + id-coverage test. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ement test
Review round 2 on PR 16.
- convex/rooms.ts: migrateRetiredRoomModels takes {table: "rooms"|"workers"}. The workers pass
rewrites execution_plan rows whose model is not in ROUTER_MODELS to DEFAULT_MODEL and writes a
roomModelMigrations audit row (new optional workerId). web_research rows are untouched so the
OPENAI_WEB_MODEL caller argument is preserved. retryV3Worker copies worker.model forward, so
without this a retry of a pre-change planning worker resent gpt-4.1-nano after 2026-10-23.
- tests: retryV3Worker -> runV3Worker on a migrated planning worker requests gpt-5.4-mini (and,
unmigrated, the retired id: proves the defect); a web_research retry keeps gpt-5.4.
- SPEC A.0.6: the pricing test now recomputes every recorded call's cost from the eval script's
per-M table and checks it against docs/model-eval-results.json byModel and expectedCostUsd.
ROUTER_PRICING_AS_OF (2026-07-04, the measured run date) added to routerModels.ts.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ution_plan rows An in-flight planning call echoes the id it was started with; when the workers migration ran meanwhile, completion wrote the retired id back and retryV3Worker copied it. web_research rows keep the echoed model (caller argument). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…A; changelog current Review round 3 on PR 16. - tests: new test reads README.md ($/turn table) and scripts/generate-chart.mjs (DATA rows) read-only and asserts each equals docs/model-eval-results.json byModel.costPerTurn at the 2 significant figures printed. Mutating either file to 999 now fails it (checked). - CHANGELOG: test count 13 -> 16, add the completeV3Worker execution_plan guard from 8ee4eed and the new pricing test. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…argeted exhaustive runbook script Pages were 200 rows with no byte bound. createRoom stores goal uncapped, a document can be 1 MiB, and Convex limits a mutation to 16 MiB read / 8 MiB written, so a page of large rooms (read once by paginate, again by patch) threw and the same cursor failed again. Pages are now MIGRATION_PAGE_SIZE = 6 (12 MiB read, 6 MiB written worst case); a test models the limits and walks 40 x 1 MiB rooms and workers. scripts/migrate-retired-models.mjs walks every page of both tables, refuses to run without a deployment selector (convex run defaults to dev), and a second run reporting migrated: 0 is the exhaustive stale-row check (convex data lists only the newest 100 rows). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…tion_plan model ids on read
migrate-retired-models.mjs spawned npx through cmd.exe on Windows, which strips the JSON quotes
so convex run (JSON5) rejected {table:rooms,cursor:null}. It now spawns node on the resolved
convex bin with no shell; a test crosses the real spawn boundary with a stub CLI that echoes argv.
coordinator runV3Worker sent a stored retired worker.model unchanged on execution_plan rows
(retry copies it). Now validModel(worker.model ?? room.model); execution_plan models are never
caller-supplied, and the web_research branch is untouched. The expectation that asserted the
retired id on the wire is rewritten to expect DEFAULT_MODEL and the gpt-5 request shape.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ow the effective model; align docs with code migrate-retired-models.mjs only checked that some argument existed, so `--typecheck disable` passed the guard and convex run migrated the DEV deployment. convexRun now refuses, before spawning, any argument list without --prod, --deployment-name, --preview-name, or --url with --admin-key. watchRoom serialized the raw room.model; an unmigrated room still storing gpt-4.1-nano showed a model the picker could not find and $0 cost panels while the coordinator sent gpt-5.4-mini. It now serializes validModel(room.model). CHANGELOG: the retry no longer sends the retired id (read-side guard), only pipeline.ts and providers/openai.ts import DEFAULT_MODEL, test count corrected, the live-room pre-deploy record is a required runbook step, and the voice/Realtime prices at README.md:1206-1209 carry their 2026-07-04 provenance (commit 40cd6b6). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nvex CLI CLI 1.42.1 reads a deploy key from --env-file, prints only a warning, ignores --prod and runs against the key's deployment, so one 6-row page could land on the wrong deployment before the post-hoc stderr check fired. assertNoDeployKey now refuses --env-file / --env-file= up front; the test proves a stub CLI is never executed. CHANGELOG test count refreshed (23 tests in roomModelMigration.test.ts). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… variables blanked The Convex CLI loads .env.local and .env with dotenv, whose grammar also accepts `KEY: value`; the script's own `KEY=` regex missed it, so the stub/real CLI ran one page on the key's deployment before the stderr backstop threw. Root cause: the script re-implemented the CLI's env-file parsing. convexRun now spawns with CONVEX_DEPLOY_KEY and CONVEX_DEPLOYMENT_TOKEN set to "" (dotenv never overrides an existing variable, readDeployKeyFromEnv treats "" as unset), so a file key cannot take effect whatever its syntax; the preflight also matches `KEY: value` and stays as the fail-fast message. Test: colon syntax in .env.local and .env refuses and the stub never runs; the child env sees both variables as "" (verified through spawnSync on Windows). Knockouts: old regex fails the colon assertion, unblanked env fails the shadow assertion. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…live-room check lists all rooms as jsonl Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nown model id instead of storing the paid default
A.0.10 binds the public API boundary: validModel mapped every non-router id (":free" ones included) to the paid DEFAULT_MODEL, so a request for a free lane or a typo became gpt-5.4-mini on every later coordinator turn. Root cause: one function served two jobs, the read-side guard for STORED stale ids (A.2.5(b), correct to fall back) and the write-side check on NEW caller input (must fail closed). requestedModel() is now the write-side check: absent/empty keeps the default (UI passes undefined to createRoom, http.ts sends "" to setModel), a router id passes, anything else throws UnknownModelError. Used in convex createRoom/setModel and roomServer createRoom/POST model (400 on refusal); validModel stays on every read path. The roomServer local copy of validModel is deleted; its DEFAULT_LLM_MODEL (OPENAI_MODEL override) default is kept.
Test: tests/modelBoundary.test.ts, 21 tests, 18 fail on the previous source (":free", unknown, retired and case/space-variant ids are refused with nothing written at both Convex mutations and both Node routes; undefined/"" default; every router id is stored as asked). CITATION anchors in two .tours files and ARCHITECTURE.md moved with the roomServer.ts lines they point at.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…Convex routes answer 400 on an unknown model id
requestedModel took raw request JSON typed as `string | undefined`. `0`, `false` and `null` hit `if (!m)` and took the paid default; `123`, `{}` and `true` made UnknownModelError's own `id.slice` throw, which the local server turned into a 500. It is now `unknown`: only `undefined` and "" keep the fallback, everything else throws UnknownModelError(String(m)).
convex/http.ts create and rooms POST /model let that throw escape (a Convex 500 with no CORS headers, or the 502 catch-all). A shared modelOrRefusal() in the httpAction applies the same rule and returns json({ok:false,error},400) with CORS headers before any mutation runs; the mutations still enforce it for client callers.
Docs: ARCHITECTURE invariant 4, the create-route comment and the CHANGELOG entry now describe the boundary and the public API change (explicit non-router id is an error); one citation anchor moved.
Tests: REFUSED gains 0, false, null, 123, {}, true and runs against the Convex mutations, the local server and the new hosted-route test (52 tests; 24 fail with routerModels.ts reverted, 13 with http.ts reverted).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… (D1) SPEC-v3 D1 (line 4, overrides conflicting text): NodeVoice's existing write-path normalization of unknown ids to DEFAULT_MODEL stays. 596fd7e and 868f788 made createRoom/setModel, the hosted Convex routes and the local server reject unknown ids, changing caller-argument behaviour (A.0 rule 4) on routes A.2.5 does not list. They also let createRoom's evictIfNeeded() evict the oldest room before the request was refused. Reverting both removes the rejection and the eviction ordering defect. Migration, read guards, spawn guard and completeV3Worker filter are untouched. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… Convex and the local server Knockout: making Convex createRoom throw on an unknown id fails 4 of the 8 tests. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…vex write limit Review round 2: `npx convex data rooms --prod` shares the CLI's deployment selection, so a visible CONVEX_DEPLOY_KEY made it silently list another deployment's running rooms. The live-room check is now `node scripts/migrate-retired-models.mjs --live-rooms --prod`, which reuses assertDeploymentSelector, assertNoDeployKey, the blanked child env and the "Ignoring --prod" backstop (one shared convexExec) and refuses a result that fills --limit. Convex's per-mutation write limit is 16 MiB (docs.convex.dev, read 2026-10-16), not 8 MiB; JSDoc and CHANGELOG say so and the test labels its 8 MiB cap a stricter margin. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Owner
Author
|
Production migration done (Homen approved running it, 2026-10-14):
|
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
OpenAI retires
gpt-4.1-nanoon 2026-10-23. A room host who picked it still has it stored onrooms.model, and the coordinator sent that stored value straight to OpenAI (validModelonly ran on writes). This PR removes the row, rewrites stored rooms and stored planning workers, and makes every coordinator request built fromroom.modelrefuse stale ids, including the planning worker a host steer spawns.Runtime sites (27) and class E/R sites (3)
One row per site (SPEC 1.4 / A.0 rule 9).
src/core/routerModels.ts:19DEFAULT_MODELgpt-5.4-minisrc/core/routerModels.ts:11gpt-5.4-minirowsrc/core/routerModels.ts:12gpt-4.1-nanorowsrc/core/routerModels.ts:13gpt-4.1-minigpt-4.1-mini-2025-04-14shuts down 2027-01-14 (recommended replacementgpt-6-luna; source in "For Homen"). The replacement is held in SPEC 1.2 and a router row needs the paidscripts/model-eval.mjsrun, so frozen heresrc/core/routerModels.ts:14gpt-4o-minigpt-4o-mini-2024-07-18shuts down 2027-01-14 (recommended replacementgpt-6-luna, held in SPEC 1.2); same blocker asgpt-4.1-mini; see "For Homen"src/core/routerModels.ts:15gpt-5-nanoLiveRoom.tsxModelSelect); row removed in a follow-up PR before 2026-12-11 via the same migration pathsrc/core/routerModels.ts:16gpt-5-minisrc/live/pipeline.ts:18OPENAI_MODELdefaultDEFAULT_MODEL; net line count beforekeyOrThrow/withTimeoutunchanged so thedocs/START_HERE.md:302-303anchors still hold (check:citations 56/0)src/providers/openai.ts:14fallbackDEFAULT_MODELconvex/openai.ts:10gpt-4.1-miniweb model (OPENAI_WEB_MODEL)OPENAI_WEB_MODELoverride keeps its precedenceconvex/rooms.ts:178gpt-4.1-miniconvex/openai.ts:9gpt-4o-mini-ttssrc/live/pipeline.ts:23gpt-4o-mini-ttsconvex/openai.ts:325tts-1src/live/pipeline.ts:253tts-1convex/openai.ts:8whisper-1gpt-transcribekeeps the shape; ship with the tts-1 PRsrc/live/pipeline.ts:13whisper-1src/live/pipeline.ts:24ElevenLabssrc/providers/ollama.ts:21src/voice/runVoiceMvp.ts:12src/nodeagents/runNodeAgentMvp.ts:9src/compare/badGoodDemo.ts:82src/providers/localModels.ts:717src/providers/localModels.ts:718src/providers/localModels.ts:24.github/workflows/ci.yml:47nodeagent.yaml:26gpt-5.4-minisrc/client/live/LiveRoom.tsx:49(R)WEB_RESEARCH_MODEL_IDdisplay lookup,gpt-4.1-miniactive until the 2027-01-14 shutdown above; file region untouchedscripts/model-eval.mjs:25(E)gpt-4.1-nanodropped fromMODELS; no new row added (a new row needs a paid, manual run)scripts/model-eval.mjs:32(E)gpt-5.4active; comparability with the 2026-07-04 resultsDisposition counts: migrate 3 (+1 E), freeze 20 (+2), defer 4 (27 runtime rows + 3 E/R rows).
Coordinates in this table are main-baseline (SPEC 1.4). Where this PR moved a line, head is:
routerModels.tsDEFAULT_MODEL:26 (main :19),scripts/model-eval.mjsjudge :37 (main :32).Class N: 12 entries (SPEC 1.4). One names an id this PR changes: the test assertion
tests/liveSteering.test.ts:209-210(theROUTER_MODELSlength, 6 -> 5, with a comment citing the OpenAI deprecations page; listed again under Stored per-room values). The other 11 are untouched: the docs and state entries name no changed id, andREADME.md:1164(table row :1175),scripts/generate-chart.mjs:15(DATA row :16) anddocs/model-eval-results.json(gpt-4.1-nanoat :38, :136, ...) name the removed id but are frozen recorded measurements. Docs-loop entries, listed here per 1.4:README.md:1175-1179andscripts/generate-chart.mjs(the recorded 2026-07-04 measurement table and chart inputs),docs/codebase/INTEGRATIONS.md:32,docs/local-models.md:3,docs/media/nodevoice-v3-state.json:59(captured UI state, never rewritten) andpromotion/PROMOTION_LOG.md:80. Eval fixtures:docs/model-eval-results.json,docs/model-chart.svgandpromotion/evidence/**are hash-bound recorded evidence and are never edited.Stored per-room values (convex)
convex/rooms.tsmigrateRetiredRoomModels(internalMutation, paged 6 rows per call (MIGRATION_PAGE_SIZE, see the byte bound below), argumenttable:"rooms"(default) or"workers"): any room whosemodelis not inROUTER_MODELSis patched toDEFAULT_MODELand a{roomId, from, to, at}row is written to the newroomModelMigrationstable (convex/schema.ts). Theworkerspass does the same forkind === "execution_plan"rows whose storedmodelis not inROUTER_MODELS(the audit row also carriesworkerId, a new optional field). Why workers need it:retryV3Worker(public,rooms.ts) copiesworker.modelinto the retry row andrunV3Workerresolves anexecution_planrow throughvalidModel(worker.model ?? room.model)(coordinator.ts:194), so a retry before the migration already requestsgpt-5.4-mini, but the stored rows would keep the retired id;execution_planmodels always come fromroom.model(spawnAgentOsGoal;runPlanningWorkerechoes its input).web_researchrows are never touched, so theOPENAI_WEB_MODELcaller argument is preserved.completeV3Worker(rooms.ts) does not write a model outsideROUTER_MODELSonto anexecution_planrow: an in-flight planning call echoes the id it started with, and writing it back after the migration undid the migration (a retry then resent the retired id).web_researchrows still record the echoed model. Idempotent, and no longer sensitive to a planning worker running during the migration. Runnode scripts/migrate-retired-models.mjs --prodonce AFTER the deploy that removes the row: it walks every page of both tables and prints the totals (a run before the deploy is impossible: the function is not deployed yet). Not run by this PR; needs the deployment owner (runbook below).createRoomstoresgoalwith no length cap, so a stale room can be a full 1 MiB document, and each page reads a document twice (the page, thenpatch). The old 200-row page could throw, roll back, and fail identically on the same cursor.MIGRATION_PAGE_SIZE = 6is derived from those limits (6 x 1 MiB x 2 = 12 MiB read, 6 MiB written: under the vendor 16 MiB write limit and under the test's 8 MiB margin, with headroom for audit inserts).maximumBytesReadwas not used on purpose: a byte-limited page can return incomplete (pageStatus: SplitRequired) and this migration must never skip a row. The test models both limits, shows a 200-row page of 1 MiB documents throws, then walks 40 stale rooms and 40 stale workers (all 1 MiB) page by page throughcontinueCursorwith every transaction inside the limits.scripts/migrate-retired-models.mjs <convex run selector>followscontinueCursortoisDoneforroomsthenworkersand prints{table, migrated, pages}. It exits 2 unless the arguments name a deployment (--prod,--deployment-name,--preview-name, or--urlwith--admin-key; checked byassertDeploymentSelectorbefore anything is spawned, so--typecheck disablealone is refused), becauseconvex rundefaults to the dev deployment (installed CLI 1.42.1cli/run.js: "By default, this runs on your dev deployment"; the README uses--prodat :1145-1146). It spawnsnode <convex/bin/main.js> run ...directly with no shell (round 6: the earliernpxcall went through cmd.exe on Windows, which strips the JSON quotes, andconvex runparses its argument as JSON5, so{table:rooms,cursor:null}is rejected). Its loop and output parser are tested offline against the real migration handler, and a test crosses the real spawn boundary with a stub CLI that echoes its argv; the real Convex CLI against a deployment has not been exercised (no deployment was touched).validModel(room.model), so a stale stored id falls back toDEFAULT_MODELinstead of reaching OpenAI while rooms are unmigrated: the turn (coordinator.ts:41produceTurn), the human-steer interpreter (coordinator.ts:127), the planning worker (coordinator.ts:194,validModel(worker.model ?? room.model)since round 6, so a stale storedworker.modelalso falls back), and, new in this round, the worker row written at spawn (rooms.ts:180,spawnAgentOsGoal->execution_plan). Without the spawn-time guard every new planning worker stored the room's stale id, the oldworker.model ?? validModel(room.model)never fell back, andrunPlanningWorkersent the id as-is (observed:PLANNING_WORKER_REQUEST_MODEL=gpt-4.1-nano).spawnAgentOsGoalis reached from the profile switch (rooms.ts:831, viaensureInitialAgentOsGoal) and from human steer (rooms.ts:912); both now store a valid id. (Line numbers re-checked at head40bca69; on main the same calls arerooms.ts:771and:852.) Theweb_researchbranch passes its storedworker.modelunchanged (that one can be a valid out-of-pickerOPENAI_WEB_MODELid);execution_planmodels are never caller-supplied (spawn usesvalidModel(room.model), retry copies,completeV3Workerrefuses non-router ids), so resolving them on read touches no caller argument.watchRoom(rooms.ts:311) serializesvalidModel(room.model), so until the migration runs the picker and the expected-cost/budget panels show the model the coordinator sends instead of finding no match for a storedgpt-4.1-nano.tests/liveSteering.test.ts:209-210toHaveLength(6)->toHaveLength(5)with a comment citing the OpenAI deprecations page.Deployment-owner runbook (not run by this PR)
Every command names the deployment and runs through
scripts/migrate-retired-models.mjs, which refuses a visibleCONVEX_DEPLOY_KEY/CONVEX_DEPLOYMENT_TOKEN(env,.env.local,.env) and--env-file, blanks both variables in the child env, and fails on the CLI's "Ignoring--prod" warning.convex runandconvex datadefault to the DEV deployment and both obey a deploy key over--prod(installed CLI 1.42.1), so a bare command can silently act on another deployment.node scripts/migrate-retired-models.mjs --live-rooms --prod(runsconvex data rooms --format jsonl --limit 100000through the same guards as step 3), and record the printed count and room codes of the"running":truerooms in the deploy notes before step 2. This is the live-room check, not the stale-row check:convex datalists only the newest rows, so zero there proves nothing about older rows. Rooms are never deleted andjsonlprints no truncation warning, so the script refuses a result that fills the limit (raise it inliveRooms). It readsjsonl, not the defaultprettyformat (which cuts lines to the terminal width and can hide therunningcolumn). The read-side guards cover any stale room until the migration runs; the exhaustive stale-row check is step 4.node scripts/migrate-retired-models.mjs --prod. It walks every page ofroomsandworkers(6 rows per call) and prints one line per table, e.g.{"table":"rooms","migrated":N,"pages":P}."migrated":0for BOTH tables: that second run visits every page, so it is the exhaustive stale-row check.roomModelMigrationsholds one audit row per rewrite from step 3.gpt-4.1-nanoon 2026-10-23, so steps 2-3 must land before that date. Productionexecution_planworker rows keep the retired id in storage until step 3 runs, but since round 6runV3Workerresolves it on read, so a retry before step 3 still requestsgpt-5.4-mini.Breaking parameters
None land now (no gpt-6 id is introduced). Recorded precondition: the
isNextGenregex/^(gpt-5|o[1-9])/(convex/openai.ts:11,src/live/pipeline.ts:44) must become a per-model capability field inROUTER_MODELSbefore any gpt-6 row is added. No change here.Test proof (re-run at head
2d555a0)tests/roomModelMigration.test.ts(24 tests) builds real requests through a fakefetchfor each changed coordinator role and assertsbody.model === "gpt-5.4-mini",temperatureandmax_tokensabsent,max_completion_tokenspresent:produceTurn)coordinator.ts:41interpretHumanSteer)coordinator.ts:127(observed:expected 'gpt-4.1-nano' to be 'gpt-5.4-mini')applyHumanIntent->spawnAgentOsGoal-> worker row ->runV3Worker)rooms.ts:180(observed: same assertion)runV3Worker), row with no stored model or a stored retired idcoordinator.ts:194(observed: same assertions)gpt-5.4kept as passedvalidModelretryV3WorkerthenrunV3Workergpt-5.4-mini; unmigrated, the retry row still storesgpt-4.1-nano(retryV3Workercopies it) but the request sendsgpt-5.4-miniin the gpt-5 shape (runV3Workerresolves on read)execution_planrows from the migrationretryV3WorkerthenrunV3Worker, after the workers migrationgpt-5.4web_researchrowscompleteV3Workerwith the echoed retired id ->retryV3Worker->runV3Worker)gpt-5.4-mini; the retry request isgpt-5.4-miniin the gpt-5 shape (observed before the fix: row came backgpt-4.1-nano)execution_plancondition incompleteV3WorkercompleteV3Workerstill records the model a worker ran onweb_researchkeeps any idtests/modelWritePath.test.ts(8 tests, added in round 12) pins SPEC-v3 D1: an unknown id (not-a-model,some-model:free, retiredgpt-4.1-nano, wrong-caseGPT-5.4-MINI) sent to ConvexcreateRoom/setModelis stored asDEFAULT_MODEL, and sent to the local server'sPOST /live/roomsand/modelis stored asDEFAULT_LLM_MODELwith status 200. Knockout: making ConvexcreateRoomthrow on an unknown id fails 4 of the 8; restored, 8 of 8 pass.Also: the round 5 byte-limit, bound-derivation and runbook-script tests (above); migration against a fake db (stale rooms patched and recorded, valid rooms untouched, second run a no-op; the workers pass rewrites only stale
execution_planrows, leavesweb_researchand rows with no model alone, and its audit row carriesworkerId); the dropdown test renders the real<ModelSelect>and asserts thegpt-5-nano/gpt-5-minioption text containsretires 2026-12-11and the other options do not; the pricing agreement test (below); the runbook-script selector test (convexRunrefuses[],--typecheck disable,--urlwithout--admin-key, and accepts--prod,--deployment-name,--preview-name,--urlwith--admin-key; knockout: removing the guard fails it); thewatchRoomtest (a room storinggpt-4.1-nanoserializesgpt-5.4-mini,gpt-4.1-miniis unchanged; knockout: serializing the rawroom.modelfails it).Fail-before (nano row), re-run at head
2d555a0: restoring only thegpt-4.1-nanorow (the main-baseline line) insrc/core/routerModels.ts(everything else at head) makes 17 of the 97 tests in the whole suite fail: 14 of the 24 intests/roomModelMigration.test.ts(10 pass), 1 inliveSteering, 2 inmodelWritePath; reverted, 97 of 97 pass. Round 5 mutation check: settingMIGRATION_PAGE_SIZEback to 200 fails the byte-limit test (Transaction exceeded read limit: 17825792 > 16777216), the bound-derivation test and the script paging test. Round-2 mutation checks (not re-run for round 4), run here and reverted: replacing thegpt-5.4-minirate with[999, 999], or itsexpectedCostUsdwith another number, fails the pricing test; dropping theexecution_planfilter fails the worker tests.Review round 13 dispositions (head
2d555a0)npx convex data rooms --prod ...bare, andconvex datasharesrun's deployment selection (CLI 1.42.1), so a visible deploy key made it list another deployment's rooms and the A.2.5 live-room check could record the wrong ones. It is nownode scripts/migrate-retired-models.mjs --live-rooms --prod:liveRoomsandconvexRunshare oneconvexExec(selector required,assertNoDeployKey, blanked child env, "Ignoring--prod" backstop), andliveRoomsrefuses a result that fills--limit. New test intests/roomModelMigration.test.ts; knockout: removingassertNoDeployKeyfromconvexExecfails 2 tests (the existing deploy-key test and the new one).MIGRATION_PAGE_SIZE = 6is unchanged and safe under either figure.151162eis superseded; the counts in Test proof are the ones taken at2d555a0.2d555a0: check, check:client, check:convex exit 0; check:citations 56/0; npm test 14 files, 97/97; build exit 0. No provider calls.Review round 12 dispositions (head
5584776)596fd7eand868f788made ConvexcreateRoom/setModel, the hosted routes inconvex/http.tsand the local server'screateRoom/POST /modelreject an unknown id (400 /UnknownModelError) and deleted the localvalidModel. SPEC-v3 D1 says write-path normalization stays, so both are reverted (git revert, squashed into one new commita258f81; no force-push,868f788is still an ancestor).requestedModel,UnknownModelError,modelOrRefusalandtests/modelBoundary.test.tsare gone;docs/codebase/ARCHITECTURE.mdinvariant 4 and the CHANGELOG are back to the pre-round-10 text and make no rejection claim. The migration, read guards, spawn guard andcompleteV3Workerfilter are untouched.evictIfNeeded()ran, so a refused create no longer evicts the oldest room. Root cause was a rejecting check placed after the eviction; removing the check removes the ordering defect.tests/modelWritePath.test.tspins D1 (see Test proof).5584776: check, check:client, check:convex exit 0; check:citations 56/0; npm test 14 files, 96/96; build exit 0. No provider calls.No provider calls were made;
scripts/model-eval.mjs(paid, manual) was not run and no new router row was added.Notes for review
tsccannot compileconvex/*.ts(module resolution), so the test loadsconvex/rooms.ts,convex/coordinator.tsandLiveRoom.tsxby a non-literal dynamic import;npm run check:convexandcheck:clienttype-check them.produceTurnis exported fromconvex/coordinator.tsandModelSelectfromLiveRoom.tsxso tests can call them (plain function / component, not registered Convex functions).Pricing sources (SPEC A.0 rule 6)
scripts/model-eval.mjsper-M list prices: now carryPRICING_AS_OF = "2026-07-04"(also written intodocs/model-eval-results.jsonon the next run). Provenance: the day the prices were typed for the measured run (commit40cd6b6). They have not been re-read from the vendor since, and this PR does not claim they were; SPEC A.2 says per-row prices are re-read on the day of the next paid run, which is when the date moves. Not re-read now because that needs the paid run.src/core/routerModels.tsexpectedCostUsd: measured per-turn cost, now dated byROUTER_PRICING_AS_OF = "2026-07-04"and tied to the per-M table by the agreement test below.README.md:1175-1179($/turn table) andscripts/generate-chart.mjsDATA rows (per-turn costs): frozen, read-only, and now covered by the agreement test (see below).README.md:1206-1209: voice/Realtime unit prices, pricingAsOf 2026-07-04 (typed in commit40cd6b6, found withgit log -L1206,1209:README.md; the same day as the router prices; never re-read from the vendor since, and not re-read now) (gpt-realtime$32/$64 per 1M,gpt-4o-mini-tts~$12/1M,whisper-1$0.006/min,gpt-4o-mini-transcribe$0.003/min). Frozen voice-class docs-loop source, outside the router (these ids are not inROUTER_MODELS). The repo has no second source for them (no per-M voice table, no recorded voice run), so there is nothing to agree against and no test covers them; they are neither edited nor re-verified here. Recorded as a SPEC §C item for Homen below.Agreement test (
tests/roomModelMigration.test.ts, offline): loads the per-M table inscripts/model-eval.mjs, every recorded call indocs/model-eval-results.json(promptTokens,completionTokens,costPerTurn) andROUTER_MODELS, and asserts (1) the table's ids are exactly the router ids, (2)(promptTokens*in + completionTokens*out)/1e6equals each recordedcostPerTurnfor all 20 calls of the 5 live ids (the 4gpt-4.1-nanocalls have no price any more), (3) the per-model mean equalsbyModel.costPerTurn, and (4)ROUTER_MODELS[].expectedCostUsdequals it.routerModels.tsnow exportsROUTER_PRICING_AS_OF = "2026-07-04"(the measured run date), asserted equal to the eval script'sPRICING_AS_OFand to the date ofranAtin the results file. A second test readsREADME.mdandscripts/generate-chart.mjsread-only and asserts all 12 displayed per-turn costs (6 models in each,gpt-4.1-nanoincluded) equalbyModel.costPerTurnat the 2 significant figures printed; mutating either file to 999 fails it (checked). This is the {in, out} agreement the spec asks for between the sources the repo has. The earlier id-only test was not an agreement test, and the earlier "nothing to compare" / "explicit spec exception" wording is withdrawn.Review round 1 dispositions
Fixed (round 1, narrowed in round 6):
runV3Workerdoes not wrap a storedworker.modelof aweb_researchrow invalidModel(it collapsed a valid out-of-picker id such as anOPENAI_WEB_MODELoverride on retry); that branch stays byte-identical to main. Theexecution_planbranch resolves on read (round 6).Fixed (round 2): spawn-time guard at
rooms.ts:180(planning workers bypassed the stale-model guard); regression test goes throughapplyHumanIntent/spawnAgentOsGoalthenrunV3Worker.Fixed (round 2): per-role request-shape tests for the steer interpreter and the planning worker (table above).
Fixed (round 2): this body refreshed from a run at head; rows split one per site.
Fixed (round 2):
PRICING_AS_OFstamp + provenance, id-coverage test, explicit exception above.Fixed (round 2): dropdown option text carries the retirement date; the test renders the real options.
Fixed (round 2, Codex feat(demo): per-agent private-state panels + model provenance badges #1): pre-change
execution_planworker rows storinggpt-4.1-nanoare migrated (workers pass above), soretryV3Workerno longer copies a retired id;web_researchrows are untouched. Test:retryV3WorkerthenrunV3Worker. The round-1 "known residual" is closed, including for a planning worker that is still running when the migration runs (round 3 below).Fixed (round 2, Codex chore: adopt Node Platform P0 contract #3): real offline pricing agreement test plus
ROUTER_PRICING_AS_OF; Class N paragraph lists all docs-loop entries and the count of 12.Fixed (round 3, Codex r2 feat(demo): per-agent private-state panels + model provenance badges #1): a planning worker running during the workers migration no longer undoes it.
completeV3Workerskips a non-router model onexecution_planrows; test reproduces the ordering (fails without the fix).Fixed (round 3): this body's line references re-derived at head; Class N count corrected (the
liveSteeringassertion 6 -> 5 is the one changed entry); disposition-table coordinates labelled main-baseline.Superseded in round 12 (D1): public
createRoom/setModel, the hosted Convex routes and the local Node server keep normalizing an unknown id to the default, as on main. The round-10/11 change that made them throw was reverted (see Review round 12).Fixed (round 4): the test-proof block was refreshed from a run at head
aa271b1(76 tests in total, 16 inroomModelMigration.test.ts) and the fail-before count re-run (11 of 16); head references updated. No code change.Fixed (round 4): the pre-merge live-room check from SPEC §A.2.5 is now a deployment-owner step in the runbook above. No code change.
Fixed (round 5, merge-readiness): the branch was one commit behind
main(b2edaef, fix(trace): preserve active rows and honest failures on current SDK #17).origin/mainis merged in (no force-push, so the six branch commits keep their SHAs); the branch is now 0 behind. At that headnpm ci,check,check:client,check:convex,check:citations(56/0) andbuildexit 0 (suite counts at the current head are in Test proof).Fixed (round 5, Codex r2 feat(demo): per-agent private-state panels + model provenance badges #1, BOUND_READ): migration pages are 6 rows, derived from Convex's read/write limits; the test models the limits (see "Byte bound").
Fixed (round 5, Codex r2 fix(demo): derive bad-panel footer from agent state (no more false 'stuck at 1') #2 and docs(nodekit): reconcile the voice handoff and verify owned CSS #4): every runbook command names its deployment (
--prod), and the stale-row check is the exhaustive second run of the paging script, notconvex data's newest-100 list; the JSDoc inconvex/rooms.tsand the CHANGELOG say the same.Superseded in round 12: the round-10 rejection of an unknown explicit id was reverted; write-path normalization stays (D1).
Superseded (round 5): an earlier implementer claim described
32c14de(66 tests; "pricing agreement test not added"; "workers are not covered"). Those statements were true of that commit only; at the current head the pricing agreement test, the workers migration, the spawn guard and thecompleteV3Workerfilter all exist and are tested (this body).Spec discrepancies
§A.2.5(a) says run the migration before the removing deploy and again after. The function does not exist until that deploy, so it runs once after (the read-side guards cover unmigrated rooms meanwhile).
Rule 6, router and docs-table sources: the agreement tests cover them (above). Rule 6, voice-class prices at
README.md:1206-1209: enumerated above but not testable from this repo; raised for Homen under SPEC §C (accept as a frozen, unverified docs-loop figure, or supply a source). Write-path normalization: SPEC-v3 D1 (SPEC-v3.md line 4, which overrides conflicting text) says NodeVoice's existing write-path normalization of unknown ids toDEFAULT_MODELstays. SocreateRoom,setModel, the hosted Convex routes and the local server keep mapping an unknown id to the default (unchanged from main), and the read-sidevalidModelguards for stored stale values also stay (§A.2.5(b)). A.0 rule 4 (caller arguments unchanged) is honoured and no route outside §A.2.5 changed. The round-10/11 rejection (an A.0.10 reading) violated D1 and is reverted.Fixed (round 6, review r3 feat(demo): per-agent private-state panels + model provenance badges #1): the runbook script failed on Windows because
npxran through cmd.exe and lost the JSON quotes ({table:rooms,cursor:null}, rejected by JSON5 inconvex run). It now runsnode <convex bin>with no shell;convexRun(selector, cli)is exported and a new test spawns a stub CLI and asserts the selector and the quoted JSON argument arrive intact. Knockout (shell: true +npx-style spawn) fails that test.Fixed (round 6, review r3 fix(demo): derive bad-panel footer from agent state (no more false 'stuck at 1') #2):
runV3Workersent a stored retiredworker.modelunchanged onexecution_planrows (a retry before the workers migration, or after a failed runbook). NowvalidModel(worker.model ?? room.model)on that branch only;web_researchis byte-identical. TheretryThenRunexpectation that assertedgpt-4.1-nanoon the wire was written in this PR and is rewritten toDEFAULT_MODELplus the gpt-5 shape, for the reason above. Knockout (restoringworker.model ?? validModel(room.model)) fails it.Fixed (round 6, review r3 chore: adopt Node Platform P0 contract #3): Class N wording (above). Claim record (r3 docs(nodekit): reconcile the voice handoff and verify owned CSS #4): the
32c14declaims remain superseded; the only suite counts in this body are the ones in Test proof, taken at the current head.Review round 7 dispositions (head
354adf7; round 8 below)CHANGELOG.mdrewritten to match the code: the unmigrated retry row keepsgpt-4.1-nanobut the request sendsgpt-5.4-mini;runV3WorkerisvalidModel(worker.model ?? room.model); onlypipeline.tsandproviders/openai.tsimportDEFAULT_MODEL; one set of counts, the ones in Test proof above (re-run at the current head; round 8 added assertions to an existing test, so the counts did not change).40cd6b6) recorded forREADME.md:1206-1209(Pricing sources, CHANGELOG). Whether a single-source agreement test is required stays with Homen under SPEC section C; no test added (one source compares nothing).scripts/migrate-retired-models.mjsrefuses to spawn without an explicit deployment selector; test added.watchRoomserializesvalidModel(room.model); test added.CONVEX_DEPLOY_KEY/CONVEX_DEPLOYMENT_TOKENfrom the env,.env.localand.env, then ignores--prod/--deployment-name/--preview-name(stderr warning, exit 0), andconvexRundropped stderr on success, so both passes could migrate another deployment and printmigrated: 0.convexRunrefuses before spawning when it finds a key in the environment or in.env.local/.env(not for--url+--admin-key, which skips the key lookup); round 8 below corrects the file-syntax coverage of that scan and adds the actual guarantee, throws on the CLI's "Ignoring--prod" warning as a backstop, and prints any other stderr. New offline test (a stub CLI writing the warning to stderr must make the run throw; env,.env.localand.envkeys refuse; empty or commented keys and--url+--admin-keydo not). Knockout: the pre-fix script fails that test. Not done: the real CLI against a deployment (none touched); the optionalCONVEX_CLOUD_URLecho was skipped because the refusal already prevents the mismatch. Superseded in part by the round 3 bullet below (head 37f29d5 was bypassable via--env-file).--env-file <file>was a fourth deploy-key source the key check never looked at: Convex CLI 1.42.1 reads a key from it, only warns, ignores--prodand runs against the key's deployment, so one 6-row page landed on the wrong deployment before the stderr backstop threw (offline probe: stub CLI executed, then the error). Root cause:convexRunforwarded the caller's selector argument list unchecked.assertNoDeployKeynow refuses--env-fileand--env-file=before anything is spawned (not parsed: the file's contents do not matter, it is simply not a supported input to this runbook script). The test uses a stub CLI that writes a marker file and arelease.envholding a key: both spellings throw and the marker is absent. Knockout: with the guard stashed that test fails (expected [Function] to throw). Head 354adf7.Review round 8 dispositions (head
91c3fd3)KEY=value, but the Convex CLI 1.42.1 reads.env.local/.envwith dotenv, whose line grammar also acceptsKEY: value. Offline repro at 354adf7:CONVEX_DEPLOY_KEY: dev:other|kin.env.localplus--prodpassed the preflight, the stub CLI ran, and only the stderr backstop threw afterwards (with the real CLI, one 6-row page would have run on the key's deployment). Root cause: the script re-implemented the CLI's env-file parsing, so any grammar difference is a hole. Fix:convexRunnow spawns the CLI withCONVEX_DEPLOY_KEYandCONVEX_DEPLOYMENT_TOKENset to""in the child env. dotenv never overrides a variable that already exists andreadDeployKeyFromEnvtreats""as unset, so a file key cannot take effect whatever its syntax; the empty values survivespawnSyncon Windows (the new assertion reads them inside a stub CLI). The preflight scan stays as the fail-fast message and now also matchesKEY: value. Corrected claim: the earlier text said it refuses whenever a key is visible; the accurate statement is that it refuses on=and:syntax in the env and both files, and that the blanked child env is the guarantee for anything the scan misses. Tests (inside the existing runbook-script test, so counts are unchanged): colon syntax in.env.localand in.enveach throw and the stub CLI never runs; a stub CLI run with both variables set in the caller env sees["",""]. Knockouts, run and reverted: the old=-only regex fails the colon assertion (.env.local: expected [Function] to throw an error); spawning with the unmodified env fails the shadow assertion (expected '["dev:x|k","t"]' to be '["",""]'). Not done: the real Convex CLI against a deployment (none touched); the dotenv no-override behaviour is taken from its documented semantics and the CLI source read above, not re-run end to end.87 / 22in the round 7Fixed (r1 #1)bullet replaced by a pointer to the Test proof counts; Test proof and fail-before headings now say91c3fd3, and the nano-row knockout was re-run at that head (14 of 23 inroomModelMigration.test.ts, 15 of 88 overall; reverted, all green).Review round 9 dispositions (head
c5d0cc0)cli.bundle.cjs) line by line and flags only non-empty values, soCONVEX_DEPLOY_KEY="",CONVEX_DEPLOY_KEY=followed by another line, andCONVEX_DEPLOY_KEY= # noneno longer block the required runbook; a later non-empty line (last wins) and quoted values still refuse. The three cases (plus a''/DEPLOYMENT_TOKEN=variant, in both.env.localand.env) are added to the existing runbook test; no assertion was weakened. Knockout: the old regex fails the new assertions. The blanked child env stays the guarantee.--format jsonl --limit 100000and says how to detect truncation, becauseconvex datakeeps only the newest--limitrooms and rooms are never deleted.createRoom/setModelunknown id toDEFAULT_MODEL) is not a defect; SPEC-v3 D1 says that normalization stays.c5d0cc0: check, check:client, check:convex exit 0; check:citations 56/0; npm test 13 files, 88/88; build exit 0; demo:compare exit 0. No provider calls.For Homen (SPEC §C)
gpt-4.1-mini-2025-04-14andgpt-4o-mini-2024-07-18with shutdown 2027-01-14 and recommended replacementgpt-6-luna. This replaces the earlier freeze reason "not on the 2026-10-23 list; active", which is out of date. Affected frozen sites:src/core/routerModels.ts:13-14,convex/openai.ts:10(OPENAI_WEB_MODELdefault),convex/rooms.ts:178,src/client/live/LiveRoom.tsx:49, and thegpt-4o-mini/gpt-4.1-minirows inscripts/model-eval.mjsMODELS. They stay frozen here: SPEC 1.2 holdsgpt-6-luna(no evidence) and a router row needs the paidscripts/model-eval.mjsrun (not run; no provider calls).gpt-5-nano/gpt-5-minifollow-up PR. That PR must land before 2026-12-11 and in any case before 2027-01-14. It also needs a replacement for theOPENAI_WEB_MODELdefault; without one the router is left with onlygpt-5.4-mini. ExplicitOPENAI_MODEL/OPENAI_WEB_MODELoverrides keep their precedence.README.md:1206-1209(see Pricing sources): accept as a frozen, unverified docs-loop figure, or supply a source.Spec: model-defaults SPEC Part A §A.2 (NodeVoice)
🤖 Generated with Claude Code