The business file can name the account claude-code launches use,
`launch.account` = {name, credential}. `name` is an identifier and
`credential` names one login set's environment variables, never a value.
`launch.limits.tokens.daily.<account>` = {limit, since, reset, timeZone}
sets a daily quota for that account. A daily key that isn't the account's
name refuses the business read.
The new plug-in limit-check/daily-quota (interface 2, pure) starts a day at
the first instant at or after `reset` on that local date in `timeZone`. Across
DST that makes a 23- or 25-hour day, and a reset in a skipped hour starts at
the gap's end. It counts the account's known records of all four kinds since
the later of the day start and `since`, and counts unknown turns apart.
The launcher refuses account-credential-missing, account-credential-ambiguous
and account-is-credential (an account name equal to a session credential's
value). Messages name variables, never values. The account rides mayLaunch,
bindLaunch, the usage records, mustStop and the launch record. A pi launch
has no account, records NULL, and no daily quota covers it. `mosaic limits`
and the Console's Settings show one fixed line when a daily row shows:
"Daily quotas count only this stack's own use of the account, not its use
anywhere else."
Bus schema 3h to 3i: 3h reserved usage_records.account as
CHECK (account IS NULL). The new step 3h-3i.sql rebuilds the table in one
transaction, keeping every row's seq and the sqlite_sequence value, and
recreates the three append-only triggers. All of sqlite_master equals 3h's
apart from the account line. A 3h engine refuses a 3i store.
packages/bus/tests/account-migration.test.mjs covers all three, with a
step that fails partway and leaves 3h intact. 3i goes live only through
`stack-release.sh activate ... --migrate` with Jason present (P19 371); the
activate path takes the pre-migrate backup, and the way back is restoring
it (agents/dewey/work/queue-126/MIGRATION.md).
Mos's go mos-0835, six conditions: (1) one transaction, account-migration
test 2 and mutant D29; (2) sqlite_master against 3h, test 1 and mutant D28;
(3) account is never a credential, the launcher's account-is-credential
refusal (launch-account.test.mjs) and the business name pattern
(token-limit-settings.test.mjs); (4) MIGRATION.md, accepted by Mos in
mos-0837; (5) the packet's DESIGN.md "no schema change" line corrected;
(6) the public/ digest below.
The public/ change in bus.js (the daily line under the token-limit table)
carries Mos's decision 123 go mos-0833 on the +/- digest 03e6d9da...ba78,
unchanged at landing.
Owner Dewey, brief docs/plans/2026-10-14_prd-1.2-build.md, "Daily quota for
the credential the stack uses today". Review round 2 rebases the round 4
build onto 1df04f52f (row 167). Reviewer: Filbert approve 29068 on round 1
(manifest d18314f7...8e65); Filbert approve 29080 on
round 2 (manifest f332db76...6fa6).
Seat run on round 2: touched 158/0, singles 1388/0 with 1 esbuild skip,
test-land-work.sh 86/0, 30/30 mutants killed.
Sage gate on 54319f552: GATE_PASSED, 2474+422/0, all 15 packages, the queue
suite and 10 script selftests.
Nothing live.
66 lines
4.4 KiB
JavaScript
66 lines
4.4 KiB
JavaScript
// Row 125 (#1619, REQ-LIMIT-3, REQ-LIMIT-4): `mosaic limits` shows each token limit with its
|
|
// count, or why it isn't counted, and `mosaic limits reset PATH...` restarts a limit's count.
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {main} from '../src/cli.mjs';
|
|
import {formatTokens,DAILY_NOTE} from '../src/limits.mjs';
|
|
import {io,tmp} from './helpers.mjs';
|
|
|
|
const SINCE='2026-10-18T07:00:00.000Z';
|
|
const TOKENS=[
|
|
{path:['tokens','instances','coder'],limit:100,since:SINCE,status:'counted',check:'token-limit',count:120,kinds:{input:10,output:20,cacheWrite:40,cacheRead:50},unknownTurns:2},
|
|
{path:['tokens','keys','k1'],limit:5,since:SINCE,status:'not-enforced',reason:'no records name a key'},
|
|
{path:['tokens','providers','zai'],limit:9,since:SINCE,status:'not-enforced',reason:null},
|
|
{path:['tokens','models','anthropic','claude-opus-5-5'],limit:7,since:SINCE,status:'check-failed',check:'token-limit',code:'check-threw'},
|
|
{path:['tokens','instances','pm'],limit:1,since:SINCE,status:'check-failed',check:null,code:'records-unreadable'},
|
|
];
|
|
|
|
test('REQ-LIMIT-3 REQ-LIMIT-4 formatTokens shows each limit counted, not enforced, or check failed, in reset path words',()=>{
|
|
assert.equal(formatTokens(TOKENS),'token limits:\n'+[
|
|
` tokens instances coder: limit 100 since ${SINCE}, count 120 (input 10, output 20, cacheWrite 40, cacheRead 50), unknown turns 2`,
|
|
` tokens keys k1: limit 5 since ${SINCE}, not enforced: no records name a key`,
|
|
` tokens providers zai: limit 9 since ${SINCE}, not enforced`,
|
|
` tokens models anthropic claude-opus-5-5: limit 7 since ${SINCE}, check failed: check-threw (token-limit)`,
|
|
` tokens instances pm: limit 1 since ${SINCE}, check failed: records-unreadable`,
|
|
].join('\n')+'\n');
|
|
for(const none of [[],undefined])assert.equal(formatTokens(none),'token limits: none\n');
|
|
});
|
|
|
|
test('REQ-LIMIT-2 row 126: any daily quota row adds one line after the list saying the count is this stack\'s own use',()=>{
|
|
const daily={path:['tokens','daily','jason-max'],limit:40,since:SINCE,status:'counted',check:'daily-quota',count:3,kinds:{input:1,output:1,cacheWrite:0,cacheRead:1},unknownTurns:0};
|
|
const out=formatTokens([TOKENS[0],daily,{...daily,path:['tokens','daily','other'],status:'not-enforced',reason:null}]);
|
|
assert.ok(out.endsWith(` tokens daily other: limit 40 since ${SINCE}, not enforced\n${DAILY_NOTE}\n`),out);
|
|
assert.equal(out.split(DAILY_NOTE).length,2,'said once');
|
|
assert.equal(DAILY_NOTE,"Daily quotas count only this stack's own use of the account, not its use anywhere else.");
|
|
assert.ok(!formatTokens(TOKENS).includes(DAILY_NOTE));
|
|
});
|
|
|
|
test('REQ-LIMIT-3 the view prints the limits then the token lines; --json keeps tokens in the result',async t=>{
|
|
const root=tmp(t),deps={system:{dataRoot:root},transport:()=>()=>({revision:'b'.repeat(64),limits:{total:3},tokens:TOKENS})};
|
|
const x=io();await main(['limits','--business','acme'],x,deps);
|
|
const [json,lines]=x.out.text.split('token limits:\n');
|
|
assert.deepEqual(JSON.parse(json),{revision:'b'.repeat(64),limits:{total:3}});
|
|
assert.match(lines,/tokens keys k1: limit 5 since \S+, not enforced: no records name a key/);
|
|
const y=io();await main(['limits','--business','acme','--json'],y,deps);
|
|
assert.deepEqual(JSON.parse(y.out.text).tokens,TOKENS);
|
|
});
|
|
|
|
test('REQ-LIMIT-3 reset reads the revision, then sends limits.set with one path per limit, a word per level',async t=>{
|
|
const root=tmp(t),calls=[];
|
|
const deps={system:{dataRoot:root},transport:business=>(verb,args)=>{calls.push({business,verb,args});return {revision:'c'.repeat(64),limits:{},tokens:[]};}};
|
|
await main(['limits','reset','tokens','models','anthropic','claude-opus-5-5','--business','acme'],io(),deps);
|
|
assert.deepEqual(calls,[
|
|
{business:'acme',verb:'limits',args:{}},
|
|
{business:'acme',verb:'limits.set',args:{revision:'c'.repeat(64),reset:[['tokens','models','anthropic','claude-opus-5-5']]}},
|
|
]);
|
|
});
|
|
|
|
test('REQ-LIMIT-3 reset with no path, or with --file, is refused before the bus',async t=>{
|
|
const root=tmp(t),calls=[];
|
|
const deps={system:{dataRoot:root},transport:()=>verb=>{calls.push(verb);return {};}};
|
|
await assert.rejects(main(['limits','reset','--business','acme'],io(),deps),/mosaic limits/);
|
|
await assert.rejects(main(['limits','reset','tokens','--file','x.json','--business','acme'],io(),deps),/mosaic limits/);
|
|
await assert.rejects(main(['limits','view','extra','--business','acme'],io(),deps),/mosaic limits/);
|
|
assert.deepEqual(calls,[]);
|
|
});
|