Skip to content

chore(models): refresh stale model defaults (NodeVoice) - #16

Merged
HomenShum merged 21 commits into
mainfrom
claude/model-refresh-nodevoice
Oct 16, 2026
Merged

HomenShum merged 21 commits into
mainfrom
claude/model-refresh-nodevoice

Conversation

@HomenShum

@HomenShum HomenShum commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner

What this does

OpenAI retires gpt-4.1-nano on 2026-10-23. A room host who picked it still has it stored on rooms.model, and the coordinator sent that stored value straight to OpenAI (validModel only ran on writes). This PR removes the row, rewrites stored rooms and stored planning workers, and makes every coordinator request built from room.model refuse 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).

Site Disposition Reason / what changed
src/core/routerModels.ts:19 DEFAULT_MODEL gpt-5.4-mini freeze active; measured default (docs/model-eval-results.json 2026-07-04)
src/core/routerModels.ts:11 gpt-5.4-mini row freeze same
src/core/routerModels.ts:12 gpt-4.1-nano row migrate row removed (OpenAI retirement 2026-10-23); room migration + read-side guards below
src/core/routerModels.ts:13 gpt-4.1-mini freeze active, but gpt-4.1-mini-2025-04-14 shuts down 2027-01-14 (recommended replacement gpt-6-luna; source in "For Homen"). The replacement is held in SPEC 1.2 and a router row needs the paid scripts/model-eval.mjs run, so frozen here
src/core/routerModels.ts:14 gpt-4o-mini freeze active, but gpt-4o-mini-2024-07-18 shuts down 2027-01-14 (recommended replacement gpt-6-luna, held in SPEC 1.2); same blocker as gpt-4.1-mini; see "For Homen"
src/core/routerModels.ts:15 gpt-5-nano migrate (partial) note now says "retires 2026-12-11" and the dropdown option text carries it (LiveRoom.tsx ModelSelect); row removed in a follow-up PR before 2026-12-11 via the same migration path
src/core/routerModels.ts:16 gpt-5-mini migrate (partial) same as above
src/live/pipeline.ts:18 OPENAI_MODEL default freeze (id) literal now imports DEFAULT_MODEL; net line count before keyOrThrow/withTimeout unchanged so the docs/START_HERE.md:302-303 anchors still hold (check:citations 56/0)
src/providers/openai.ts:14 fallback freeze (id) literal replaced by imported DEFAULT_MODEL
convex/openai.ts:10 gpt-4.1-mini web model (OPENAI_WEB_MODEL) freeze active until the 2027-01-14 shutdown above; the follow-up needs a replacement default for it; the OPENAI_WEB_MODEL override keeps its precedence
convex/rooms.ts:178 gpt-4.1-mini freeze active until the 2027-01-14 shutdown above
convex/openai.ts:9 gpt-4o-mini-tts freeze voice; snapshots retire 2027-01-06, alias target unknown
src/live/pipeline.ts:23 gpt-4o-mini-tts freeze same
convex/openai.ts:325 tts-1 defer retires 2027-01-06; replacement is a different API shape; scheduled PR before 2026-12-01
src/live/pipeline.ts:253 tts-1 defer same
convex/openai.ts:8 whisper-1 defer retires 2027-02-26; gpt-transcribe keeps the shape; ship with the tts-1 PR
src/live/pipeline.ts:13 whisper-1 defer same
src/live/pipeline.ts:24 ElevenLabs freeze third-party voice, out of scope
src/providers/ollama.ts:21 freeze local model, out of scope
src/voice/runVoiceMvp.ts:12 freeze local model, out of scope
src/nodeagents/runNodeAgentMvp.ts:9 freeze local model, out of scope
src/compare/badGoodDemo.ts:82 freeze local/demo model, out of scope
src/providers/localModels.ts:717 freeze local model, out of scope
src/providers/localModels.ts:718 freeze local model, out of scope
src/providers/localModels.ts:24 freeze local model, out of scope
.github/workflows/ci.yml:47 freeze CI-local model, out of scope
nodeagent.yaml:26 gpt-5.4-mini freeze active; measured default
src/client/live/LiveRoom.tsx:49 (R) freeze WEB_RESEARCH_MODEL_ID display lookup, gpt-4.1-mini active until the 2027-01-14 shutdown above; file region untouched
scripts/model-eval.mjs:25 (E) migrate gpt-4.1-nano dropped from MODELS; no new row added (a new row needs a paid, manual run)
scripts/model-eval.mjs:32 (E) freeze judge gpt-5.4 active; comparability with the 2026-07-04 results

Disposition 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.ts DEFAULT_MODEL :26 (main :19), scripts/model-eval.mjs judge :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 (the ROUTER_MODELS length, 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, and README.md:1164 (table row :1175), scripts/generate-chart.mjs:15 (DATA row :16) and docs/model-eval-results.json (gpt-4.1-nano at :38, :136, ...) name the removed id but are frozen recorded measurements. Docs-loop entries, listed here per 1.4: README.md:1175-1179 and scripts/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) and promotion/PROMOTION_LOG.md:80. Eval fixtures: docs/model-eval-results.json, docs/model-chart.svg and promotion/evidence/** are hash-bound recorded evidence and are never edited.

Stored per-room values (convex)

  • convex/rooms.ts migrateRetiredRoomModels (internalMutation, paged 6 rows per call (MIGRATION_PAGE_SIZE, see the byte bound below), argument table: "rooms" (default) or "workers"): any room whose model is not in ROUTER_MODELS is patched to DEFAULT_MODEL and a {roomId, from, to, at} row is written to the new roomModelMigrations table (convex/schema.ts). The workers pass does the same for kind === "execution_plan" rows whose stored model is not in ROUTER_MODELS (the audit row also carries workerId, a new optional field). Why workers need it: retryV3Worker (public, rooms.ts) copies worker.model into the retry row and runV3Worker resolves an execution_plan row through validModel(worker.model ?? room.model) (coordinator.ts:194), so a retry before the migration already requests gpt-5.4-mini, but the stored rows would keep the retired id; execution_plan models always come from room.model (spawnAgentOsGoal; runPlanningWorker echoes its input). web_research rows are never touched, so the OPENAI_WEB_MODEL caller argument is preserved. completeV3Worker (rooms.ts) does not write a model outside ROUTER_MODELS onto an execution_plan row: 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_research rows still record the echoed model. Idempotent, and no longer sensitive to a planning worker running during the migration. Run node scripts/migrate-retired-models.mjs --prod once 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).
  • Byte bound (round 5). Convex limits a mutation to 16 MiB read and 16 MiB written and a document to 1 MiB (docs.convex.dev/production/state/limits, read 2026-10-16; the test keeps a stricter 8 MiB write cap as a deliberate margin, not the vendor figure); createRoom stores goal with no length cap, so a stale room can be a full 1 MiB document, and each page reads a document twice (the page, then patch). The old 200-row page could throw, roll back, and fail identically on the same cursor. MIGRATION_PAGE_SIZE = 6 is 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). maximumBytesRead was 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 through continueCursor with every transaction inside the limits.
  • Runbook script (round 5). scripts/migrate-retired-models.mjs <convex run selector> follows continueCursor to isDone for rooms then workers and prints {table, migrated, pages}. It exits 2 unless the arguments name a deployment (--prod, --deployment-name, --preview-name, or --url with --admin-key; checked by assertDeploymentSelector before anything is spawned, so --typecheck disable alone is refused), because convex run defaults to the dev deployment (installed CLI 1.42.1 cli/run.js: "By default, this runs on your dev deployment"; the README uses --prod at :1145-1146). It spawns node <convex/bin/main.js> run ... directly with no shell (round 6: the earlier npx call went through cmd.exe on Windows, which strips the JSON quotes, and convex run parses 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).
  • Read-side guards, all validModel(room.model), so a stale stored id falls back to DEFAULT_MODEL instead of reaching OpenAI while rooms are unmigrated: the turn (coordinator.ts:41 produceTurn), the human-steer interpreter (coordinator.ts:127), the planning worker (coordinator.ts:194, validModel(worker.model ?? room.model) since round 6, so a stale stored worker.model also 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 old worker.model ?? validModel(room.model) never fell back, and runPlanningWorker sent the id as-is (observed: PLANNING_WORKER_REQUEST_MODEL=gpt-4.1-nano). spawnAgentOsGoal is reached from the profile switch (rooms.ts:831, via ensureInitialAgentOsGoal) and from human steer (rooms.ts:912); both now store a valid id. (Line numbers re-checked at head 40bca69; on main the same calls are rooms.ts:771 and :852.) The web_research branch passes its stored worker.model unchanged (that one can be a valid out-of-picker OPENAI_WEB_MODEL id); execution_plan models are never caller-supplied (spawn uses validModel(room.model), retry copies, completeV3Worker refuses non-router ids), so resolving them on read touches no caller argument.
  • watchRoom (rooms.ts:311) serializes validModel(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 stored gpt-4.1-nano.
  • tests/liveSteering.test.ts:209-210 toHaveLength(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 visible CONVEX_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 run and convex data default 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.

  1. BEFORE the removing deploy, REQUIRED (SPEC A.2.5: live rooms are checked before removal): node scripts/migrate-retired-models.mjs --live-rooms --prod (runs convex data rooms --format jsonl --limit 100000 through the same guards as step 3), and record the printed count and room codes of the "running":true rooms in the deploy notes before step 2. This is the live-room check, not the stale-row check: convex data lists only the newest rows, so zero there proves nothing about older rows. Rooms are never deleted and jsonl prints no truncation warning, so the script refuses a result that fills the limit (raise it in liveRooms). It reads jsonl, not the default pretty format (which cuts lines to the terminal width and can hide the running column). The read-side guards cover any stale room until the migration runs; the exhaustive stale-row check is step 4.
  2. Deploy the change that removes the row.
  3. AFTER deploy: node scripts/migrate-retired-models.mjs --prod. It walks every page of rooms and workers (6 rows per call) and prints one line per table, e.g. {"table":"rooms","migrated":N,"pages":P}.
  4. Run the same command again. Expect "migrated":0 for BOTH tables: that second run visits every page, so it is the exhaustive stale-row check. roomModelMigrations holds one audit row per rewrite from step 3.
  5. OpenAI retires gpt-4.1-nano on 2026-10-23, so steps 2-3 must land before that date. Production execution_plan worker rows keep the retired id in storage until step 3 runs, but since round 6 runV3Worker resolves it on read, so a retry before step 3 still requests gpt-5.4-mini.

Breaking parameters

None land now (no gpt-6 id is introduced). Recorded precondition: the isNextGen regex /^(gpt-5|o[1-9])/ (convex/openai.ts:11, src/live/pipeline.ts:44) must become a per-model capability field in ROUTER_MODELS before 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 fake fetch for each changed coordinator role and asserts body.model === "gpt-5.4-mini", temperature and max_tokens absent, max_completion_tokens present:

Role Test Reverting this guard fails it
turn (produceTurn) stale room next turn; one-room create -> migrate -> audit -> turn coordinator.ts:41
human-steer interpreter (interpretHumanSteer) stale room steer coordinator.ts:127 (observed: expected 'gpt-4.1-nano' to be 'gpt-5.4-mini')
planning worker (real path: applyHumanIntent -> spawnAgentOsGoal -> worker row -> runV3Worker) stale room, unmigrated rooms.ts:180 (observed: same assertion)
planning worker (runV3Worker), row with no stored model or a stored retired id stale room, or retired stored id on a migrated room coordinator.ts:194 (observed: same assertions)
web-research worker (caller argument) stored gpt-5.4 kept as passed wrapping it in validModel
pre-change planning worker, retryV3Worker then runV3Worker after the workers migration the retry row and the request carry gpt-5.4-mini; unmigrated, the retry row still stores gpt-4.1-nano (retryV3Worker copies it) but the request sends gpt-5.4-mini in the gpt-5 shape (runV3Worker resolves on read) removing the execution_plan rows from the migration
web-research worker, retryV3Worker then runV3Worker, after the workers migration stays gpt-5.4 migrating web_research rows
planning worker still running when the migration runs (migrate -> completeV3Worker with the echoed retired id -> retryV3Worker -> runV3Worker) row stays gpt-5.4-mini; the retry request is gpt-5.4-mini in the gpt-5 shape (observed before the fix: row came back gpt-4.1-nano) the execution_plan condition in completeV3Worker
completeV3Worker still records the model a worker ran on valid planning id kept; web_research keeps any id over-broad guard

tests/modelWritePath.test.ts (8 tests, added in round 12) pins SPEC-v3 D1: an unknown id (not-a-model, some-model:free, retired gpt-4.1-nano, wrong-case GPT-5.4-MINI) sent to Convex createRoom / setModel is stored as DEFAULT_MODEL, and sent to the local server's POST /live/rooms and /model is stored as DEFAULT_LLM_MODEL with status 200. Knockout: making Convex createRoom throw 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_plan rows, leaves web_research and rows with no model alone, and its audit row carries workerId); the dropdown test renders the real <ModelSelect> and asserts the gpt-5-nano / gpt-5-mini option text contains retires 2026-12-11 and the other options do not; the pricing agreement test (below); the runbook-script selector test (convexRun refuses [], --typecheck disable, --url without --admin-key, and accepts --prod, --deployment-name, --preview-name, --url with --admin-key; knockout: removing the guard fails it); the watchRoom test (a room storing gpt-4.1-nano serializes gpt-5.4-mini, gpt-4.1-mini is unchanged; knockout: serializing the raw room.model fails it).

Fail-before (nano row), re-run at head 2d555a0: restoring only the gpt-4.1-nano row (the main-baseline line) in src/core/routerModels.ts (everything else at head) makes 17 of the 97 tests in the whole suite fail: 14 of the 24 in tests/roomModelMigration.test.ts (10 pass), 1 in liveSteering, 2 in modelWritePath; reverted, 97 of 97 pass. Round 5 mutation check: setting MIGRATION_PAGE_SIZE back 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 the gpt-5.4-mini rate with [999, 999], or its expectedCostUsd with another number, fails the pricing test; dropping the execution_plan filter fails the worker tests.

> npm run check            tsc --noEmit                    exit 0
> npm run check:client     tsc --noEmit -p tsconfig.client.json   exit 0
> npm run check:convex     tsc --noEmit -p convex/tsconfig.json   exit 0
> npm run check:citations  56 citations checked, 0 broken
> npm test                 Test Files 14 passed (14) | Tests 97 passed (97)
> npm run build            built, exit 0

Review round 13 dispositions (head 2d555a0)

  • Fixed (Codex r2 feat(demo): per-agent private-state panels + model provenance badges #1, MAJOR): runbook step 1 ran npx convex data rooms --prod ... bare, and convex data shares run'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 now node scripts/migrate-retired-models.mjs --live-rooms --prod: liveRooms and convexRun share one convexExec (selector required, assertNoDeployKey, blanked child env, "Ignoring --prod" backstop), and liveRooms refuses a result that fills --limit. New test in tests/roomModelMigration.test.ts; knockout: removing assertNoDeployKey from convexExec fails 2 tests (the existing deploy-key test and the new one).
  • Fixed (Codex r2 fix(demo): derive bad-panel footer from agent state (no more false 'stuck at 1') #2, nit): Convex's per-mutation write limit is 16 MiB (docs.convex.dev/production/state/limits, read 2026-10-16), not 8 MiB. JSDoc, CHANGELOG and this body corrected; the test's 8 MiB write cap stays and is labelled a deliberate margin. MIGRATION_PAGE_SIZE = 6 is unchanged and safe under either figure.
  • Fixed (Codex r2 chore: adopt Node Platform P0 contract #3, nit): the nano-row knockout figure is refreshed to the current head (see Fail-before).
  • Claim record (r2 docs(nodekit): reconcile the voice handoff and verify owned CSS #4): the earlier implementer claim naming 151162e is superseded; the counts in Test proof are the ones taken at 2d555a0.
  • Re-run at 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)

  • Fixed (BLOCKER, D1): commits 596fd7e and 868f788 made Convex createRoom / setModel, the hosted routes in convex/http.ts and the local server's createRoom / POST /model reject an unknown id (400 / UnknownModelError) and deleted the local validModel. SPEC-v3 D1 says write-path normalization stays, so both are reverted (git revert, squashed into one new commit a258f81; no force-push, 868f788 is still an ancestor). requestedModel, UnknownModelError, modelOrRefusal and tests/modelBoundary.test.ts are gone; docs/codebase/ARCHITECTURE.md invariant 4 and the CHANGELOG are back to the pre-round-10 text and make no rejection claim. The migration, read guards, spawn guard and completeV3Worker filter are untouched.
  • Fixed (MAJOR, Codex fix(demo): derive bad-panel footer from agent state (no more false 'stuck at 1') #2): a create request can no longer be refused after 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.
  • Added: tests/modelWritePath.test.ts pins D1 (see Test proof).
  • Superseded: the round 10 and round 11 dispositions (fail-closed boundary, the 21 and 52 boundary tests, the 109 and 140 suite counts) described the reverted change and are removed from this body.
  • Re-run at 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

  • Root tsc cannot compile convex/*.ts (module resolution), so the test loads convex/rooms.ts, convex/coordinator.ts and LiveRoom.tsx by a non-literal dynamic import; npm run check:convex and check:client type-check them.
  • produceTurn is exported from convex/coordinator.ts and ModelSelect from LiveRoom.tsx so tests can call them (plain function / component, not registered Convex functions).

Pricing sources (SPEC A.0 rule 6)

  • scripts/model-eval.mjs per-M list prices: now carry PRICING_AS_OF = "2026-07-04" (also written into docs/model-eval-results.json on the next run). Provenance: the day the prices were typed for the measured run (commit 40cd6b6). 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.ts expectedCostUsd: measured per-turn cost, now dated by ROUTER_PRICING_AS_OF = "2026-07-04" and tied to the per-M table by the agreement test below.
  • README.md:1175-1179 ($/turn table) and scripts/generate-chart.mjs DATA 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 commit 40cd6b6, found with git 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 in ROUTER_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 in scripts/model-eval.mjs, every recorded call in docs/model-eval-results.json (promptTokens, completionTokens, costPerTurn) and ROUTER_MODELS, and asserts (1) the table's ids are exactly the router ids, (2) (promptTokens*in + completionTokens*out)/1e6 equals each recorded costPerTurn for all 20 calls of the 5 live ids (the 4 gpt-4.1-nano calls have no price any more), (3) the per-model mean equals byModel.costPerTurn, and (4) ROUTER_MODELS[].expectedCostUsd equals it. routerModels.ts now exports ROUTER_PRICING_AS_OF = "2026-07-04" (the measured run date), asserted equal to the eval script's PRICING_AS_OF and to the date of ranAt in the results file. A second test reads README.md and scripts/generate-chart.mjs read-only and asserts all 12 displayed per-turn costs (6 models in each, gpt-4.1-nano included) equal byModel.costPerTurn at 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): runV3Worker does not wrap a stored worker.model of a web_research row in validModel (it collapsed a valid out-of-picker id such as an OPENAI_WEB_MODEL override on retry); that branch stays byte-identical to main. The execution_plan branch 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 through applyHumanIntent/spawnAgentOsGoal then runV3Worker.

  • 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_OF stamp + 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_plan worker rows storing gpt-4.1-nano are migrated (workers pass above), so retryV3Worker no longer copies a retired id; web_research rows are untouched. Test: retryV3Worker then runV3Worker. 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. completeV3Worker skips a non-router model on execution_plan rows; 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 liveSteering assertion 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 in roomModelMigration.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/main is merged in (no force-push, so the six branch commits keep their SHAs); the branch is now 0 behind. At that head npm ci, check, check:client, check:convex, check:citations (56/0) and build exit 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, not convex data's newest-100 list; the JSDoc in convex/rooms.ts and 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 the completeV3Worker filter 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 to DEFAULT_MODEL stays. So createRoom, setModel, the hosted Convex routes and the local server keep mapping an unknown id to the default (unchanged from main), and the read-side validModel guards 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 npx ran through cmd.exe and lost the JSON quotes ({table:rooms,cursor:null}, rejected by JSON5 in convex run). It now runs node <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): runV3Worker sent a stored retired worker.model unchanged on execution_plan rows (a retry before the workers migration, or after a failed runbook). Now validModel(worker.model ?? room.model) on that branch only; web_research is byte-identical. The retryThenRun expectation that asserted gpt-4.1-nano on the wire was written in this PR and is rewritten to DEFAULT_MODEL plus the gpt-5 shape, for the reason above. Knockout (restoring worker.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 32c14de claims 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)

  • Fixed (r1 feat(demo): per-agent private-state panels + model provenance badges #1): this body and CHANGELOG.md rewritten to match the code: the unmigrated retry row keeps gpt-4.1-nano but the request sends gpt-5.4-mini; runV3Worker is validModel(worker.model ?? room.model); only pipeline.ts and providers/openai.ts import DEFAULT_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).
  • Fixed (r1 fix(demo): derive bad-panel footer from agent state (no more false 'stuck at 1') #2): the live-room check before the removing deploy is a required runbook step 1 (above), recorded before step 2.
  • Fixed (r1 chore: adopt Node Platform P0 contract #3): pricingAsOf 2026-07-04 and its provenance (40cd6b6) recorded for README.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).
  • Fixed (r1 docs(nodekit): reconcile the voice handoff and verify owned CSS #4): scripts/migrate-retired-models.mjs refuses to spawn without an explicit deployment selector; test added.
  • Fixed (r1 Sync model-table latency to the eval receipt; surface Quickstart at the top #5): watchRoom serializes validModel(room.model); test added.
  • Fixed (round 2 review, Codex MAJOR): the runbook script's "never defaults to DEV" promise was false when a deploy key was visible: the Convex CLI 1.42.1 reads CONVEX_DEPLOY_KEY / CONVEX_DEPLOYMENT_TOKEN from the env, .env.local and .env, then ignores --prod / --deployment-name / --preview-name (stderr warning, exit 0), and convexRun dropped stderr on success, so both passes could migrate another deployment and print migrated: 0. convexRun refuses 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.local and .env keys refuse; empty or commented keys and --url + --admin-key do not). Knockout: the pre-fix script fails that test. Not done: the real CLI against a deployment (none touched); the optional CONVEX_CLOUD_URL echo 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).
  • Fixed (round 3 review, Codex MAJOR): --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 --prod and 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: convexRun forwarded the caller's selector argument list unchecked. assertNoDeployKey now refuses --env-file and --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 a release.env holding a key: both spellings throw and the marker is absent. Knockout: with the guard stashed that test fails (expected [Function] to throw). Head 354adf7.
  • Fixed (round 3 review, minor): quoted test numbers refreshed from runs at head 354adf7 (Test proof, fail-before, CHANGELOG count), and the stray bullet that had been appended after the footer moved above it.
  • Still disclosed for Homen, unchanged: the before/after migration-ordering discrepancy (Spec discrepancies above).

Review round 8 dispositions (head 91c3fd3)

  • Fixed (round 4 review, Codex MAJOR): the file scan only recognised KEY=value, but the Convex CLI 1.42.1 reads .env.local / .env with dotenv, whose line grammar also accepts KEY: value. Offline repro at 354adf7: CONVEX_DEPLOY_KEY: dev:other|k in .env.local plus --prod passed 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: convexRun now spawns the CLI with CONVEX_DEPLOY_KEY and CONVEX_DEPLOYMENT_TOKEN set to "" in the child env. dotenv never overrides a variable that already exists and readDeployKeyFromEnv treats "" as unset, so a file key cannot take effect whatever its syntax; the empty values survive spawnSync on Windows (the new assertion reads them inside a stub CLI). The preflight scan stays as the fail-fast message and now also matches KEY: 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.local and in .env each 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.
  • Fixed (round 4 review, minor): the stale 87 / 22 in the round 7 Fixed (r1 #1) bullet replaced by a pointer to the Test proof counts; Test proof and fail-before headings now say 91c3fd3, and the nano-row knockout was re-run at that head (14 of 23 in roomModelMigration.test.ts, 15 of 88 overall; reverted, all green).

Review round 9 dispositions (head c5d0cc0)

For Homen (SPEC §C)

  • OpenAI GPT-4 series shutdown, new 2026-10-14. OpenAI deprecations page (https://developers.openai.com/api/docs/deprecations, fetched 2026-10-16), section 2026-10-14 GPT-4 series model deprecations, lists gpt-4.1-mini-2025-04-14 and gpt-4o-mini-2024-07-18 with shutdown 2027-01-14 and recommended replacement gpt-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_MODEL default), convex/rooms.ts:178, src/client/live/LiveRoom.tsx:49, and the gpt-4o-mini / gpt-4.1-mini rows in scripts/model-eval.mjs MODELS. They stay frozen here: SPEC 1.2 holds gpt-6-luna (no evidence) and a router row needs the paid scripts/model-eval.mjs run (not run; no provider calls).
  • Schedule it into the gpt-5-nano / gpt-5-mini follow-up PR. That PR must land before 2026-12-11 and in any case before 2027-01-14. It also needs a replacement for the OPENAI_WEB_MODEL default; without one the router is left with only gpt-5.4-mini. Explicit OPENAI_MODEL / OPENAI_WEB_MODEL overrides keep their precedence.
  • Voice-class prices at 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

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>
@vercel

vercel Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
local-collab-mvp Ready Ready Preview Oct 16, 2026 4:10pm UTC
nodevoice Ready Ready Preview Oct 16, 2026 4:10pm UTC

Request Review

…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>
HomenShum and others added 2 commits October 15, 2026 04:56
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>
HomenShum and others added 2 commits October 16, 2026 04:10
… (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>
Review round 3: the 23 was written at 354adf7; 2d555a0 added the --live-rooms guard test without updating this line.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@HomenShum
HomenShum merged commit 70de9ab into main Oct 16, 2026
8 checks passed
@HomenShum
HomenShum deleted the claude/model-refresh-nodevoice branch October 16, 2026 20:20
@HomenShum

Copy link
Copy Markdown
Owner Author

Production migration done (Homen approved running it, 2026-10-14):

  • Before: production room-os-live / enduring-echidna-527 had 96 rooms, 2 on gpt-4.1-nano; 64 workers, none on gpt-4.1-nano.
  • The production backend had not been redeployed after merge (Convex deploys are manual per README); function-spec showed no migrateRetiredRoomModels. Only chore(models): refresh stale model defaults (NodeVoice) #16 changed convex/ since 2026-08-13, so npx convex deploy of main 70de9ab shipped only this PR (dry run first: no index deleted, new table indexes added).
  • node scripts/migrate-retired-models.mjs --deployment-name enduring-echidna-527: run 1 rooms migrated 2, workers 0; run 2 rooms 0, workers 0.
  • After: all 96 rooms on gpt-5.4-mini; roomModelMigrations holds 2 rows gpt-4.1-nano -> gpt-5.4-mini. OpenAI retires gpt-4.1-nano on 2026-10-23.
  • Not in this PR: 29 workers still on gpt-4.1-mini (retires 2027-01-14), handled by the second NodeVoice PR.

This branch was successfully deployed

2 active deployments
Preview – nodevoice — e15b20c6 Deployed Oct 16, 2026 by vercel[bot]
Preview – local-collab-mvp — e15b20c6 Deployed Oct 16, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant