From 0380c9d206457e971ea490d101c12407f8aa2ea0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 01:20:02 +0000 Subject: [PATCH 1/5] relay fresh counts the POSIX argument limit in bytes, starts a home-folder project from its trusted folder, and relay cloud finds the shim behind where Three things a review of 1.43.0 found. The inline hand-off on Linux was checked against 120,000 characters, but the kernel's limit on one argument is 128 KiB of bytes. 70,000 two-byte characters (140,000 bytes) passed the check and the exec failed E2BIG here, so a hand-off heavy in accented text or dashes would have died in the window the same way on every retry. The POSIX cost is now UTF-8 bytes, and past it the first prompt points at the hand-off file, as it already did on Windows. The POSIX launcher cd'd into record.cwd even when the relay had chosen a trusted launch folder for a project in the home folder. The preflight answers the trust question for the launch folder only, and the new session's opening says it was started from one, so the window would have sat at the trust question. It now starts where the Windows launcher does, launchCwd, with --add-dir back to the project; the resume launcher shares the line. relay cloud took the first line of `where claude` as the claude a Windows shell runs, and on an npm install that line is the extensionless sh script beside claude.cmd, so the batch-shim warning never fired for the install it was written for. It now takes the first match with an extension. UNVERIFIED on a real Windows machine: the where output order is the documented one, fed to the function in the test. defer.md and relay.md also say the file is used when the hand-off is too long for one argument. The three new tests fail without the script changes. Suite 989/990, the skip being Task Scheduler, which is Windows only. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CPrhYxAHgTrw4QZVtWaHV8 --- commands/defer.md | 4 +++- commands/relay.md | 4 +++- skills/usage-limits/scripts/relay.js | 13 +++++++---- skills/usage-limits/scripts/wake.js | 17 +++++++++----- test/relay-cloud.test.js | 17 ++++++++++++++ test/relay-fresh.test.js | 33 ++++++++++++++++++++++++++++ 6 files changed, 77 insertions(+), 11 deletions(-) diff --git a/commands/defer.md b/commands/defer.md index ec2ec12..0ad6c82 100644 --- a/commands/defer.md +++ b/commands/defer.md @@ -57,7 +57,9 @@ will be: the relay's `permission` mode and `model`. It needs no session id. On Windows the hand-off goes inline only through the native `claude.exe`; with only the npm `claude.cmd` shim, the first prompt points at the hand-off file instead, - because cmd.exe cuts an argument at its first line break. + because cmd.exe cuts an argument at its first line break. It does the same + anywhere when the hand-off is too long for one argument (30,000 characters on + Windows, 120,000 bytes elsewhere). - `relay show off` - either kind runs headless (`claude -p`, the prompt on stdin) with no window and no Remote Control; its output is kept in `relay log --run`. diff --git a/commands/relay.md b/commands/relay.md index 86641f7..da6f196 100644 --- a/commands/relay.md +++ b/commands/relay.md @@ -39,7 +39,9 @@ The rest: and model - for a session that has grown long, so the next window starts clean instead of carrying all of it. The old conversation is left as it was. On Windows the inline hand-off needs the native `claude.exe`; with only the - npm `claude.cmd` shim the first prompt points at the file instead. Both + npm `claude.cmd` shim the first prompt points at the file instead, and so it + does anywhere for a hand-off too long for one argument (30,000 characters on + Windows, 120,000 bytes elsewhere). Both modes keep one terminal: if the old session is open and working again at the wake, the wake stands down; if it is open but quiet for three minutes, the wake opens its one window anyway. Under `mode notify` neither applies, and diff --git a/skills/usage-limits/scripts/relay.js b/skills/usage-limits/scripts/relay.js index fca84ea..a2c7ab0 100644 --- a/skills/usage-limits/scripts/relay.js +++ b/skills/usage-limits/scripts/relay.js @@ -1836,12 +1836,17 @@ function cloudCli(env, platform) { } // The claude that `claude` typed into a Windows shell runs: the first match on -// PATH, which is what `where` lists first. Only asked on Windows. -function shellClaude(env) { +// PATH that has an extension. `where claude` also lists the extensionless sh +// script npm writes beside claude.cmd, and lists it first, but neither shell +// runs a file with no extension - so taking the first line named the sh +// script, and the shim warning never fired for the npm install it is about. +// Only asked on Windows. `run` stands in for spawnSync in the tests. +function shellClaude(env, run) { const environment = env || process.env; if (environment.USAGE_LIMITS_CLAUDE_CLI) return firstExisting([environment.USAGE_LIMITS_CLAUDE_CLI]); - const where = spawnSync('where', ['claude'], { encoding: 'utf8', timeout: 10000, windowsHide: true }); - const first = where.status === 0 ? String(where.stdout || '').split(/\r?\n/)[0].trim() : ''; + const where = (run || spawnSync)('where', ['claude'], { encoding: 'utf8', timeout: 10000, windowsHide: true }); + const lines = where && where.status === 0 ? String(where.stdout || '').split(/\r?\n/).map((line) => line.trim()) : []; + const first = lines.find((line) => line && path.win32.extname(line)) || ''; return first || findClaude(environment); } diff --git a/skills/usage-limits/scripts/wake.js b/skills/usage-limits/scripts/wake.js index e265ef6..584e2e3 100644 --- a/skills/usage-limits/scripts/wake.js +++ b/skills/usage-limits/scripts/wake.js @@ -245,12 +245,14 @@ function freshFor(config) { } // One argument has to hold the hand-off. Linux refuses a single argument past -// 128 KiB; Windows caps a whole command line at 32,767 characters, and there -// each double quote and backslash can cost one more when it is quoted. +// 128 KiB - bytes, not characters, so an accented letter or a dash costs two +// or three (70,000 two-byte characters measured E2BIG here); Windows caps a +// whole command line at 32,767 UTF-16 characters, and there each double quote +// and backslash can cost one more when it is quoted. const FRESH_ARG_MAX = { win32: 30000, posix: 120000 }; function argCost(text, platform) { - return platform === 'win32' ? text.length + (text.match(/["\\]/g) || []).length : text.length; + return platform === 'win32' ? text.length + (text.match(/["\\]/g) || []).length : Buffer.byteLength(text, 'utf8'); } function isShim(cli) { @@ -291,7 +293,8 @@ function freshLaunch(record, config, cli, prompt, promptFile, platform) { if (platform === 'win32' && isShim(cli)) { why = cli + ' is a batch shim and cmd.exe ends a command at the first line break'; } else if (argCost(text, platform) > limit) { - why = 'the hand-off is ' + text.length + ' characters, more than one argument holds here (' + limit + ')'; + why = 'the hand-off is ' + argCost(text, platform) + (platform === 'win32' ? ' characters' : ' bytes') + + ', more than one argument holds here (' + limit + ')'; } return { cli, args: freshArgs(record, config, why ? freshPointerPrompt(promptFile) : text), inline: !why, why }; } @@ -550,7 +553,11 @@ function shQuote(text) { function launcherScriptPosix(record, config, cli, promptFile, exitPath, args) { return [ '#!/bin/sh', - 'cd ' + shQuote(record.cwd) + ' || exit 1', + // The folder the start-up questions were answered for, as on Windows: for + // a project in the home folder that is the trusted launch folder, with + // --add-dir back to the project. Started in the home folder itself, the + // window sat at the trust question the preflight never answered. + 'cd ' + shQuote(record.launchCwd || record.cwd) + ' || exit 1', 'unset CLAUDE_CODE_CHILD_SESSION CLAUDE_CODE_SESSION_ID', shQuote(cli) + ' ' + (args || visibleArgs(record, config, promptFile)).map(shQuote).join(' '), 'code=$?', diff --git a/test/relay-cloud.test.js b/test/relay-cloud.test.js index 56a476a..58f517b 100644 --- a/test/relay-cloud.test.js +++ b/test/relay-cloud.test.js @@ -345,3 +345,20 @@ test('a Windows command meant for a batch shim warns that cmd.exe cuts the hand- assert.doesNotMatch(r.cloud(['--session', 'sess-9', 'x'], { git: cleanGit, platform: 'linux', cwd: '/repo', account: {}, cli: '/x/claude.cmd' }), /batch shim/); assert.deepStrictEqual(r.cloudWarnings(r.gitState('/x', cleanGit), 'short', 'win32', 'C:\\bin\\claude.exe'), []); })); + +// `where claude` on an npm install lists the extensionless sh script before +// claude.cmd. Neither shell runs a file with no extension, and taking the +// first line meant the shim warning above never fired for the install it is +// about. +test('the claude a Windows shell runs is the first match with an extension, not the sh script npm writes beside the shim', () => { + const r = relay(); + const calls = []; + const where = (cmd, args) => { + calls.push([cmd].concat(args)); + return { status: 0, stdout: 'C:\\Users\\me\\AppData\\Roaming\\npm\\claude\r\nC:\\Users\\me\\AppData\\Roaming\\npm\\claude.cmd\r\nC:\\Users\\me\\.local\\bin\\claude.exe\r\n' }; + }; + assert.strictEqual(r.shellClaude({}, where), 'C:\\Users\\me\\AppData\\Roaming\\npm\\claude.cmd'); + assert.deepStrictEqual(calls, [['where', 'claude']]); + const native = () => ({ status: 0, stdout: 'C:\\Users\\me\\.local\\bin\\claude.exe\r\nC:\\Users\\me\\AppData\\Roaming\\npm\\claude\r\n' }); + assert.strictEqual(r.shellClaude({}, native), 'C:\\Users\\me\\.local\\bin\\claude.exe'); +}); diff --git a/test/relay-fresh.test.js b/test/relay-fresh.test.js index 6b770ef..e6be46a 100644 --- a/test/relay-fresh.test.js +++ b/test/relay-fresh.test.js @@ -364,3 +364,36 @@ test('defer marks its own record, not whichever relay was armed first', () => assert.strictEqual(relay.armedFor(held, 'sess-defer-own').deferred, true); assert.strictEqual(relay.armedFor(held, 'someone-else').deferred, undefined, 'the other relay is left as it was'); })); + +// --- found in review ---------------------------------------------------------- + +// Linux's limit on one argument is 128 KiB of BYTES. Counting characters let +// 70,000 two-byte characters (140,000 bytes) through as inline, and the exec +// then failed E2BIG in the window, the same way on every retry. +test('the POSIX inline limit counts bytes, so a hand-off heavy in non-ASCII text goes by file instead of failing E2BIG', () => { + const record = { id: ID, cwd: '/work/proj', project: 'proj' }; + const wide = '\u00e9'.repeat(70000); + const plan = wake.freshLaunch(record, {}, '/bin/claude', wide, '/cfg/p.md', 'linux'); + assert.strictEqual(plan.inline, false); + assert.match(plan.why, /140000 bytes, more than one argument holds here \(120000\)/); + assert.ok(plan.args[plan.args.length - 1].includes('/cfg/p.md')); + assert.strictEqual(wake.freshLaunch(record, {}, '/bin/claude', 'x'.repeat(70000), '/cfg/p.md', 'linux').inline, true); +}); + +// A project in the home folder is launched from the trusted folder the +// preflight answered for, with --add-dir back to it, as on Windows - and as +// the new session's own opening says. The POSIX launcher cd'd into the home +// folder itself, where the trust question waits for nobody. +test('the POSIX window starts a home-folder project from its trusted launch folder, with --add-dir back to it', () => + isolated((dir) => { + const launch = path.join(dir, 'relay-cwd'); + const record = { id: ID, cwd: '/home/me', launchCwd: launch, project: 'me' }; + const opened = []; + const result = wake.deliverClaude(record, PROMPT, { show: true, fresh: true }, '/usr/local/bin/claude', [], clockIo({ platform: 'linux', open: (launcher) => opened.push(launcher) })); + assert.strictEqual(result.ok, true); + const script = fs.readFileSync(opened[0], 'utf8'); + assert.ok(script.includes('cd ' + wake.shQuote(launch) + ' || exit 1'), script); + assert.ok(script.includes("'--add-dir' '/home/me'"), script); + const resume = wake.launcherScriptPosix(record, {}, '/usr/local/bin/claude', '/cfg/p.md', '/cfg/p.exit'); + assert.ok(resume.includes('cd ' + wake.shQuote(launch) + ' || exit 1'), 'the resume launches from the same folder'); + })); From 36f4cf5bf130503e04f85599400f5f82a1b15673 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 01:20:21 +0000 Subject: [PATCH 2/5] 1.43.1 across the manifests, and the packaged copies rebuilt at it package.json, both plugin manifests and the VS Code package say 1.43.1, and vscode/lib is rebuilt from the scripts at that version - 32 scripts. 989/990, the one skip being Task Scheduler, which is Windows only; no usage-limits temp folder left behind. Patch, because these are fixes to 1.43.0. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CPrhYxAHgTrw4QZVtWaHV8 --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- package.json | 2 +- vscode/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index cd5446b..8877241 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "usage-limits", "displayName": "Usage Limits", - "version": "1.43.0", + "version": "1.43.1", "description": "Puts your remaining Claude Code usage limit into Claude's context before every prompt, so it opens with what fits in the budget instead of starting work that gets cut off. Reports headroom as turns rather than percentages, prices a job before you start it, and detects your plan tier.", "author": { "name": "Ridelink", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 589f643..5db1951 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "usage-limits", - "version": "1.43.0", + "version": "1.43.1", "description": "Reports how much of your Codex usage limit is left as turns of work rather than a percentage, prices a job before you start it, and counts the other agents sharing the same budget.", "author": { "name": "Ridelink", diff --git a/package.json b/package.json index 5ccbec9..a662486 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-usage-limits", - "version": "1.43.0", + "version": "1.43.1", "description": "Puts your remaining Claude Code usage limit into Claude's context before every prompt, so it opens with what fits in the budget instead of starting work that gets cut off. Reports headroom as turns rather than percentages, prices a job before you start it, and detects your plan tier.", "keywords": [ "claude", diff --git a/vscode/package.json b/vscode/package.json index b5dcc6c..80f2139 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -2,7 +2,7 @@ "name": "claude-usage-limits", "displayName": "Claude Usage Limits", "description": "Your Claude Code session, weekly and per-model limits as live bars in a view beside the Claude Code chat, drawn in Claude's own colours from Claude's own numbers.", - "version": "1.43.0", + "version": "1.43.1", "publisher": "ridelink", "license": "MIT", "icon": "media/icon.png", From 1a46c4f408c15322534058888e161d122c5b1135 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 01:58:34 +0000 Subject: [PATCH 3/5] Sonnet 5.5 is priced from the table: $2 in, $10 out, reads at the tenth rule claude-sonnet-5-5 was missing from RATES, so its calls fell to the sonnet family average, $2.50/$12.50 from Sonnet 5 and 4.6 together - 25% over on every Sonnet 5.5 turn, and the model listed as assumed rather than known. It keeps Sonnet 5's prices (claude-api model table, cached 2026-09-25), and its $0.20 reads are a tenth of input, so no cacheRead override. Because nothing is cheaper, Sonnet 5 is not pointed at 5.5 as a newer sibling. One new test: rate, [1m] suffix, cost of a million in, out and read, and the parsed version. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CPrhYxAHgTrw4QZVtWaHV8 --- skills/usage-limits/scripts/usage.js | 3 +++ test/model-version.test.js | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/skills/usage-limits/scripts/usage.js b/skills/usage-limits/scripts/usage.js index 27a071c..c03082a 100644 --- a/skills/usage-limits/scripts/usage.js +++ b/skills/usage-limits/scripts/usage.js @@ -71,6 +71,9 @@ const RATES = { 'claude-opus-4-8': { input: 5, output: 25 }, 'claude-opus-4-7': { input: 5, output: 25 }, 'claude-opus-4-6': { input: 5, output: 25 }, + // Sonnet 5.5 keeps Sonnet 5's $2/$10, and its $0.20 reads are the tenth rule + // (claude-api model table, cached 2026-09-25). + 'claude-sonnet-5-5': { input: 2, output: 10 }, 'claude-sonnet-5': { input: 2, output: 10 }, 'claude-sonnet-4-6': { input: 3, output: 15 }, 'claude-haiku-4-5': { input: 1, output: 5 }, diff --git a/test/model-version.test.js b/test/model-version.test.js index fc03186..54ec787 100644 --- a/test/model-version.test.js +++ b/test/model-version.test.js @@ -57,6 +57,15 @@ test('model ids come apart into family and version; a bare alias has no version' assert.strictEqual(usage.parseModelId(null), null); }); +test('Sonnet 5.5 is priced from the table, not the family average', () => { + assert.strictEqual(usage.isKnownModel('claude-sonnet-5-5'), true); + assert.deepStrictEqual(usage.rateFor('claude-sonnet-5-5'), { input: 2, output: 10 }); + assert.deepStrictEqual(usage.rateFor('claude-sonnet-5-5[1m]'), usage.rateFor('claude-sonnet-5-5')); + assert.strictEqual(usage.costOf({ input_tokens: 1e6, output_tokens: 1e6 }, 'claude-sonnet-5-5'), 12); + assert.ok(Math.abs(usage.costOf({ cache_read_input_tokens: 1e6 }, 'claude-sonnet-5-5') - 0.2) < 1e-12); + assert.deepStrictEqual(usage.parseModelId('claude-sonnet-5-5'), { family: 'sonnet', version: [5, 5] }); +}); + test('the newer sibling is same family, newer, no dearer on any rate, and cheaper on one', () => { const opus5 = usage.newerSibling('claude-opus-5'); assert.ok(opus5); @@ -77,6 +86,8 @@ test('the newer sibling is same family, newer, no dearer on any rate, and cheape // so Sonnet 4.6 is not told Sonnet 5 is cheaper and Opus 4.6 is not pointed // at 5.5. assert.strictEqual(usage.newerSibling('claude-sonnet-4-6'), null); + // Sonnet 5.5 prices every rate the same as Sonnet 5, so it is not cheaper. + assert.strictEqual(usage.newerSibling('claude-sonnet-5'), null); assert.strictEqual(usage.newerSibling('claude-opus-4-6'), null); // A bare alias names no release, and a model with no row of its own is not // compared against a guess. From f243ec88a5fc655921dda61d420b82526688b3ee Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 01:58:34 +0000 Subject: [PATCH 4/5] 1.43.2 across the manifests package.json, both plugin manifests and the VS Code package say 1.43.2. Patch: a price row for a model the table did not know. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CPrhYxAHgTrw4QZVtWaHV8 --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- package.json | 2 +- vscode/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 8877241..e2bdd2c 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "usage-limits", "displayName": "Usage Limits", - "version": "1.43.1", + "version": "1.43.2", "description": "Puts your remaining Claude Code usage limit into Claude's context before every prompt, so it opens with what fits in the budget instead of starting work that gets cut off. Reports headroom as turns rather than percentages, prices a job before you start it, and detects your plan tier.", "author": { "name": "Ridelink", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 5db1951..dfbdb4f 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "usage-limits", - "version": "1.43.1", + "version": "1.43.2", "description": "Reports how much of your Codex usage limit is left as turns of work rather than a percentage, prices a job before you start it, and counts the other agents sharing the same budget.", "author": { "name": "Ridelink", diff --git a/package.json b/package.json index a662486..a915d17 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "claude-usage-limits", - "version": "1.43.1", + "version": "1.43.2", "description": "Puts your remaining Claude Code usage limit into Claude's context before every prompt, so it opens with what fits in the budget instead of starting work that gets cut off. Reports headroom as turns rather than percentages, prices a job before you start it, and detects your plan tier.", "keywords": [ "claude", diff --git a/vscode/package.json b/vscode/package.json index 80f2139..731f121 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -2,7 +2,7 @@ "name": "claude-usage-limits", "displayName": "Claude Usage Limits", "description": "Your Claude Code session, weekly and per-model limits as live bars in a view beside the Claude Code chat, drawn in Claude's own colours from Claude's own numbers.", - "version": "1.43.1", + "version": "1.43.2", "publisher": "ridelink", "license": "MIT", "icon": "media/icon.png", From 76643ab580998af89b11f105bfe72e7a82db2dca Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 01:59:23 +0000 Subject: [PATCH 5/5] The Sonnet family average now counts Sonnet 5.5 An unknown Sonnet id falls back to the average of the Sonnet rows, and that is three rows now: $7/3 in and $35/3 out, not $2.50/$12.50. The expected value was written for two. 990/991, the one skip being Task Scheduler. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CPrhYxAHgTrw4QZVtWaHV8 --- test/usage.test.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/usage.test.js b/test/usage.test.js index 391b952..5158a97 100644 --- a/test/usage.test.js +++ b/test/usage.test.js @@ -62,7 +62,11 @@ test('cache reads use the per-model rate when one is stated', () => { }); test('rateFor falls back to the family when the id is unknown', () => { - assert.deepStrictEqual(usage.rateFor('claude-sonnet-9-9'), { input: 2.5, output: 12.5 }); + // The Sonnet average takes in Sonnet 5.5 and 5 at $2/$10 and 4.6 at $3/$15, + // so (2 + 2 + 3) / 3 in and (10 + 10 + 15) / 3 out. + const sonnet = usage.rateFor('claude-sonnet-9-9'); + near(sonnet.input, 7 / 3, 1e-9); + near(sonnet.output, 35 / 3, 1e-9); // The Opus average takes in every Opus row: four at $5/$25 and Opus 5.5 at // $4/$20, so (4 x 5 + 4) / 5 = 4.8 in and (4 x 25 + 20) / 5 = 24 out. assert.deepStrictEqual(usage.rateFor('CLAUDE-OPUS-9'), { input: 4.8, output: 24 });