# Row 126 (#1620): design Dewey, 2026-10-18T22:27Z. Base origin/refactor 70c9131f0 (sage-0940). Brief: docs/plans/2026-10-14_prd-1.2-build.md:480-547, blob 6b473b93044e, with the sage-0910 rulings in DESIGN-NOTES.md. REQ-LIMIT-2, REQ-LIMIT-4. Nothing here has been run; there is no slot yet. Line numbers are on 70c9131f0. ## Two questions for Sage before the build **Q1. The named set and the other credential variables.** `sessionEnv` (packages/seat/src/session.mjs:101) passes every `ENV_ALLOW` credential variable into the session, whichever set the organization file names. If the file names `CLAUDE_CODE_OAUTH_TOKEN` and the bus environment also holds `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN`, Claude Code logs in with those first, as far as I know its precedence (`ANTHROPIC_AUTH_TOKEN`, then `ANTHROPIC_API_KEY`, then `CLAUDE_CODE_OAUTH_TOKEN`). The usage records would then name an account that isn't the one in use, which is the false record sage-0910 refused to allow for a missing set. My recommendation is that the launcher refuses, before anything starts, when any `SESSION_CREDENTIALS` variable outside the named set is set, `ANTHROPIC_BASE_URL` included. That's a second code beside the missing-set one. The other fix is to pass only the named set in `sessionEnv`, but that changes what enters the sandbox, and the brief says no sandbox change. Is the refusal right? **Q2. Pi launches.** `SESSION_CREDENTIALS` are Claude Code's login sets (session.mjs:94-98). My reading is that the account applies only to a `claude-code` launch. A `pi` launch would log `account: null`, its records keep `NULL`, and the credential checks don't run for it, so a daily quota never counts Pi turns. Is that right? Unless Sage says otherwise, I build with both of these recommendations. ### Sage's answers (sage-0942) 1. **Q1: refuse `account-credential-ambiguous`.** - It applies only when `launch.account` is set. With no account, the launch behaves exactly as it does today. - The message names variable names, never values. - `ANTHROPIC_BASE_URL` is included. - The design doesn't rest on Claude Code's credential precedence. The refusal stands whatever that order is, so the precedence remark above is background only. - Narrowing `sessionEnv` stays out of the row. 2. **Q2: yes.** Only a `claude-code` launch carries the account. A `pi` launch logs `account: null`, its records stay `NULL`, and the credential checks don't run for it. The cli README says so, and a test asserts the pi-null case. The other design choices stand. The public/bus.js line needs Mos's decision 123 go before it lands; `public.diff` goes in this packet. 126 is rebased onto 156's landing before I ask for its slot, and nothing runs before then. ## Item 1: the account **Organization file.** A new optional `launch.account`, written by hand (Settings doesn't edit it, so `SETTINGS_KEYS` doesn't change): ```json "launch": { "by": "chief", "instances": [...], "models": {...}, "account": { "name": "jason-max", "credential": "CLAUDE_CODE_OAUTH_TOKEN" } } ``` - `business.mjs:121` gains `account` in its key list. A new `checkAccount` in launch.mjs uses `rejectUnknownKeys(["name","credential"])`. `name` uses `requireId`, the same rule as a `tokens.keys` key. `credential` must be variable names joined with "+" (sage-0910), checked by `/^[A-Z][A-Z0-9_]*(\+[A-Z][A-Z0-9_]*)*$/`. A secret can't pass that check. - The organization-file read checks only the shape. Business can't import `SESSION_CREDENTIALS` from seat, because seat already imports business (session.mjs:37, sandbox.mjs:40). So the launcher checks that the name is one of the three sets, under the same refusal code. - With no `account`, everything stays as it is today: records are `NULL` and keys are not enforced. Existing files still load, and no migration is needed. - A `tokens.daily.` leaf whose `` isn't `launch.account.name` makes the read refuse, in the same way as the `tokens.instances` check at business.mjs:135. Without that check, the quota would match no record and still show as counted, a limit that never applies. **Launcher** (packages/cli/src/launcher.mjs `check()`, after the `no-session-credential` check at :213): - `account-credential-missing`: the file names a set that isn't one of `SESSION_CREDENTIALS`, or whose variables the bus environment doesn't all hold. The message gives the set name, never a value. - `account-credential-ambiguous` (if Q1 is accepted): the environment holds a credential variable outside the named set. - Both go in the refusal table at packages/cli/README.md:332-347, after `no-session-credential`. - `account = model.harness === "claude-code" ? b.launch.account?.name ?? null : null` (Q2). It goes to the `mayLaunch` op (:225), to `bindLaunch` (:337), and to the `launch` log record (:367), as `account`. **Bus:** - runtime.mjs:187 passes `account` through to `limits.mayLaunch`. - broker.mjs `bindLaunch` (:410) takes an optional `account`, which must be an identifier. The session keeps it. `session.launched`'s body doesn't change, so `launch-record-mismatch` on a rebind is unaffected. - broker.mjs:1231 `mustStop` passes `account: s.account ?? null`. - usage.mjs:65 reads `account = null` from the session. The `NULL` at :94 becomes a bound `?`. - Corrected in round 4 (sage-0970, sage-0972). This line used to say "The column already exists, so there is no schema change and no migration". That was wrong: 3h made the column `account TEXT CHECK (account IS NULL)` (`3g-3h.sql:10`), and the first slot run on 81f26551b went red on that CHECK. Row 126 needs schema 3i, a rebuild of usage_records without the CHECK. See MIGRATION.md and BUILD.md "Round 4". ## Item 2: `tokens.daily.` - launch.mjs `checkTokenLimits` gains `daily`. The keys use `requireId`. Each leaf is `{limit, since, reset, timeZone}`, all required: - `limit` and `since` follow `tokenLimit`'s rules. - `reset` must match `/^([01]\d|2[0-3]):[0-5]\d$/`. - `timeZone` must match an IANA name shape (`/^[A-Za-z][A-Za-z0-9_+-]*(\/[A-Za-z0-9_+-]+)*$/`, so `+05:00` is out), and `new Intl.DateTimeFormat("en-US", {timeZone})` must accept it. - Anything else refuses the read. - `daily: 1` is added to the `DEPTH` tables in bus limits.mjs:32 and business launch-settings.mjs:24. The writer stamps a new daily leaf's `since` and `reset` moves it, as for the other kinds. - token-limit's `leaves()` doesn't change: it walks named kinds and never reads `daily`. ## Item 3: packages/plugins/limit-check/daily-quota/ The module is self-contained (index.mjs, README.md), `interfaceVersion = 2`, pure, with the same answer shapes and thrown `{code}` errors as token-limit. It copies token-limit's small tally helpers instead of importing a sibling check, so each check loads alone. The cost is about 20 duplicated lines. **The day window.** A local date D's reset is the earliest instant whose date in `timeZone` is D and whose wall-clock time there is at or after `reset`. The window start is today's reset if it is at or before `now`; otherwise it is yesterday's. That one rule covers both DST cases: - *Skipped* (America/Chicago 2026-03-08, `reset` 02:30): the reset is the first instant after the gap, 03:00 CDT (08:00Z). Temporal's `compatible` rule would give 03:30 instead, which leaves the old day running for half an hour after the wall clock passed the reset. - *Repeated* (2026-11-01, `reset` 01:30): the reset is the first 01:30, CDT (06:30Z). The second 01:30, CST (07:30Z), doesn't start another window. The implementation: - `offset(t)` comes from `Intl.DateTimeFormat` `formatToParts` with `hourCycle: "h23"`. - A candidate is `Date.UTC(D, reset)` minus the offset in force a day before and minus the offset a day after. - If some candidates read back as `(D, reset)`, the earliest wins. - If none does, `reset` is in a gap, and the reset is found by binary search between the two candidates for the first instant whose local reading is at or past `(D, reset)`. - The count runs from `max(windowStart, since)` over records with `account === `, summing every kind. Unknown turns add nothing and are counted apart, the same as token-limit. - `mayLaunch` and `mustStop` match on `subject.account === `. A hit is `{path, limit, count, unknownTurns}`. - `report` gives one counted entry per daily leaf, with `since` being the leaf's `since` (core's validEntry requires that), not the window start. ## Item 4: refusal and stop Both use core's existing `mayLaunch`/`mustStop`, logged as `limit-reached` with `check: "daily-quota"`. Nothing in core changes beyond the DEPTH entry and the account passing. ## Item 5: the fixed line Both lines are keyed on `path[1] === "daily"` and print once when any daily row is present. The text is a draft for review: > Daily quotas count only this stack's own use of the account, not its use > anywhere else. - **CLI** (packages/cli/src/limits.mjs `formatTokens`): the line goes after the list. - **webui** (packages/webui/src/public/bus.js `tokenLimits`, :593-603): a `

` under the table. Its test is packages/webui/tests/limits-browser.test.mjs. - **This is a public/ change, so it needs Mos's decision 123 go before landing.** The diff goes in this packet as public.diff. - serve.mjs doesn't change; it passes `tokens` through (:78-79). ## Files **New:** - packages/plugins/limit-check/daily-quota/index.mjs - packages/plugins/limit-check/daily-quota/README.md - packages/plugins/tests/daily-quota.test.mjs **Changed source:** - packages/business/src/launch.mjs - packages/business/src/business.mjs - packages/business/src/launch-settings.mjs - packages/bus/src/limits.mjs - packages/bus/src/usage.mjs - packages/bus/src/broker.mjs - packages/bus/src/runtime.mjs - packages/cli/src/launcher.mjs - packages/cli/src/limits.mjs - packages/webui/src/public/bus.js **Tests and docs:** the tests below; packages/plugins/README.md, packages/business/README.md, packages/cli/README.md. **The org-file change for Jason** (the packet gives it; I don't edit the file): add `"account": {"name": "", "credential": ""}` to `launch`. `` is whichever of `CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_API_KEY` or `ANTHROPIC_AUTH_TOKEN+ANTHROPIC_BASE_URL` the bus unit's EnvironmentFile sets. I won't read that file. If Q1 is accepted, the file must set only that set. **No sandbox change.** session.mjs, sandbox.mjs and `ENV_ALLOW` stay untouched. ## Paths shared with other rows - **Row 165** (Darkwing, on the slot; paths from agents/darkwing/work/queue-165/BUILD.md): - packages/bus/src/broker.mjs - packages/bus/src/runtime.mjs - packages/webui/src/public/bus.js - packages/cli/README.md - possibly packages/bus/README.md, if 126 touches it (not planned) - **Row 156** (mine, in review, candidate afea1df3): - packages/bus/src/broker.mjs - packages/bus/src/runtime.mjs - packages/cli/src/launcher.mjs - packages/cli/tests/launcher.test.mjs - packages/cli/README.md - Whichever lands first, 126 rebases onto it and reruns the whole gate on the new base (candidate-base-drift rule). ## Tests (each run singly) **New, packages/plugins/tests/daily-quota.test.mjs (REQ-LIMIT-2, REQ-LIMIT-4):** - the count: every kind; only this account; from `max(start, since)`; unknown turns kept apart - the America/Chicago boundary at 05:00: a record at 04:59 CDT is outside today's window, and one at 05:00 is inside - the spring DST change with a 00:00 reset: the window is 23 h - the fall DST change with a 00:00 reset: the window is 25 h - a reset DST skips (02:30 on 03-08), starting at 08:00Z - a reset DST repeats (01:30 on 11-01): one window, from 06:30Z - a refusal at the quota (`count === limit`) - `mustStop` once the turn's records reach it - an allow after the next reset - bad leaves throw a code (fail closed) **Changed:** - token-limit.test.mjs: token-limit ignores `tokens.daily`. - packages/business/tests (the launch schema test file, picked at build time): - a bad `reset` refuses the read - a bad `timeZone` refuses the read (a non-IANA name, an offset, an unknown zone) - a bad `account` refuses the read - a daily key that isn't the account refuses the read - launch-settings tests: a daily leaf gets its `since` stamped and reset. - bus: - limits.test.mjs: `limitLeaves` walks `daily`. - limit-checks.test.mjs or limits-runtime.test.mjs: daily-quota loaded through the loader refuses at the quota and allows after the reset. - limit-stop.test.mjs: a stop at the end of the turn that crosses it, with `account` on the subject. - usage-broker.test.mjs: records carry the bound account. - launcher.test.mjs: - the launch log names the account - `account-credential-missing` - `account-credential-ambiguous` - a pi launch logs `account: null` - `mayLaunch` gets the account - cli limits test: the fixed line. - webui limits-browser.test.mjs: the fixed line. - Hash pins and path-reading tests: I grep tests for every manifest path and for 64-hex pins on the changed files before in-review. ## Mutants (one each, with the test that kills it) | # | Mutant | Killed by | |---|---|---| | D01 | never steps back to yesterday | the boundary test | | D02 | window computed in UTC | the Chicago boundary test | | D03 | offset taken once from `now` | the two DST-change tests | | D04 | skipped reset uses Temporal-style +gap (03:30) | the skip test | | D05 | repeated reset takes the later 01:30 | the repeat test | | D06 | ignores `since` | the count test | | D07 | ignores account | the count test | | D08 | `>=` becomes `>` | the refusal-at-quota test | | D09 | token-limit walks `daily` | the ignores-daily test | | D10 | schema drops the `reset` check | the bad reset test | | D11 | schema drops the `timeZone` check | the bad zone test | | D12 | no cross-check of the daily key against the account | the cross-check test | | D13 | launcher skips the missing-set refusal | the missing-set refusal test | | D14 | launcher skips the ambiguity refusal | the ambiguity refusal test | | D15 | launch log omits the account | the log test | | D16 | usage inserts `NULL` | the usage-broker test | | D17 | broker `mustStop` omits the account | the limit-stop test | | D18 | runtime drops the account from `mayLaunch` | the bus refusal test | | D19 | bus DEPTH lacks `daily` | the limitLeaves and bus refusal tests | | D20 | settings DEPTH lacks `daily` | the stamp test | | D21 | CLI line removed | the CLI test | | D22 | webui line removed | the browser test |