From 58a80d996bb88959a964a87f19b34d1655f4da53 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Sun, 20 Sep 2026 23:42:00 -0400 Subject: [PATCH 1/3] test(runtime): expect the agent's own submit delay in the PTY timing policy case (#21874) #21665 gave antigravity a per-line settle before Enter, so the delay the test computed from bytes alone is 45 ms short of what the runtime waits. Under fake timers that leaves the submit pending until the real 30 s timeout, which is what every PR's node shard 8/8 has been failing on since it landed. The case now derives the expected delay from the agent's policy, so a future per-agent term moves the expectation with it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb --- .../terminal-creation-and-readiness-part-07.spec.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-07.spec.ts b/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-07.spec.ts index 3ae648ab0ef..5e1de184c9c 100644 --- a/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-07.spec.ts +++ b/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-07.spec.ts @@ -3,7 +3,7 @@ import { AGENT_PROMPT_BRACKETED_PASTE_END, AGENT_PROMPT_BRACKETED_PASTE_START, buildAgentPromptPasteBytes, - getAgentPromptSubmitDelayMs + resolveAgentPromptSubmitDelayForAgent } from '../../../shared/agent-prompt-injection' import { TUI_AGENT_CONFIG } from '../../../shared/tui-agent-config' import type { TuiAgent } from '../../../shared/tui-agent' @@ -610,9 +610,13 @@ describe('OrcaRuntimeService', () => { launchAgent: agent }) - const submitDelayMs = getAgentPromptSubmitDelayMs( + // The agent's own policy, not the byte-only delay: antigravity adds a per-line settle + // (#21665), and advancing fake timers by less than the policy waits leaves the submit + // pending until the real 30 s timeout. + const submitDelayMs = resolveAgentPromptSubmitDelayForAgent( process.platform, - Buffer.byteLength(buildAgentPromptPasteBytes('review this change'), 'utf8') + 'review this change', + agent ) const sendPromise = runtime.sendTerminalAgentPrompt(handle, 'review this change') if (agent === 'omp') { From 169bccc544decade78add849b22f340561b94867 Mon Sep 17 00:00:00 2001 From: Jinwoo Hong <73622457+Jinwoo-H@users.noreply.github.com> Date: Sun, 20 Sep 2026 23:45:38 -0400 Subject: [PATCH 2/3] fix(mobile): restore query-string's named exports under the 9.5.1 override (#21727) #21652 overrode query-string to 9.5.1 for GHSA-vcc3-ghjq-m6fr (decode-uri-component <= 0.4.2). 9.x's entry exports only `default`, while expo-router's linking forks, @react-navigation/core and @react-navigation/native all `import * as queryString`, so `stringify` and `parse` became undefined: any push carrying a param outside the path pattern and any href with a query threw. Patch the entry to re-export the named API; the override and the advisory fix stay. The lockfile carries the patch hash only; regenerated by hand because a non-frozen install re-resolves peer suffixes across the file. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb --- mobile/patches/query-string@9.5.1.patch | 10 +++++++ mobile/pnpm-lock.yaml | 9 ++++-- mobile/pnpm-workspace.yaml | 1 + .../query-string-named-exports.test.ts | 28 +++++++++++++++++++ 4 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 mobile/patches/query-string@9.5.1.patch create mode 100644 mobile/src/navigation/query-string-named-exports.test.ts diff --git a/mobile/patches/query-string@9.5.1.patch b/mobile/patches/query-string@9.5.1.patch new file mode 100644 index 00000000000..eb8c575d5ec --- /dev/null +++ b/mobile/patches/query-string@9.5.1.patch @@ -0,0 +1,10 @@ +diff --git a/index.js b/index.js +index 0480a96d718c997b0a5a54b775e635e2e048e796..85f71b13d3f0f31c3c2140581d1e4051afb21dc3 100644 +--- a/index.js ++++ b/index.js +@@ -1,3 +1,5 @@ + import * as queryString from './base.js'; + + export default queryString; ++ ++export * from './base.js'; diff --git a/mobile/pnpm-lock.yaml b/mobile/pnpm-lock.yaml index 201d564805c..63a1f5dec01 100644 --- a/mobile/pnpm-lock.yaml +++ b/mobile/pnpm-lock.yaml @@ -12,6 +12,9 @@ patchedDependencies: expo-notifications@55.0.27: hash: ce20843a3daad4185d7e8571788fa323ba4d11984936188790858650a61749c0 path: patches/expo-notifications@55.0.27.patch + query-string@9.5.1: + hash: 8624f1beedd0705bfdaf287519ed6363662a0c96f01fd594508b34583bf30d8b + path: patches/query-string@9.5.1.patch react-native-webview@13.16.2: hash: de6761dfa76a5491a23e49f1262566a8831cab26736a336fdffc5d4e7d05ff27 path: patches/react-native-webview@13.16.2.patch @@ -9966,7 +9969,7 @@ snapshots: escape-string-regexp: 4.0.0 fast-deep-equal: 3.1.3 nanoid: 3.3.18 - query-string: 9.5.1 + query-string: 9.5.1(patch_hash=8624f1beedd0705bfdaf287519ed6363662a0c96f01fd594508b34583bf30d8b) react: 19.2.8 react-is: 19.2.6 use-latest-callback: 0.2.6(react@19.2.8) @@ -12230,7 +12233,7 @@ snapshots: fast-deep-equal: 3.1.3 invariant: 2.2.4 nanoid: 3.3.18 - query-string: 9.5.1 + query-string: 9.5.1(patch_hash=8624f1beedd0705bfdaf287519ed6363662a0c96f01fd594508b34583bf30d8b) react: 19.2.8 react-fast-compare: 3.2.2 react-native: 0.83.10(patch_hash=44876634a8efbb0f2c3f66cd4332be170ec821d1cbfc0264ac80680983e8513d)(@babel/core@7.29.7)(@react-native/metro-config@0.85.2(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.8) @@ -14283,7 +14286,7 @@ snapshots: pure-rand@6.1.0: {} - query-string@9.5.1: + query-string@9.5.1(patch_hash=8624f1beedd0705bfdaf287519ed6363662a0c96f01fd594508b34583bf30d8b): dependencies: decode-uri-component: 0.5.0 filter-obj: 5.1.0 diff --git a/mobile/pnpm-workspace.yaml b/mobile/pnpm-workspace.yaml index 9f0a43c71fe..4e4bb54fc3d 100644 --- a/mobile/pnpm-workspace.yaml +++ b/mobile/pnpm-workspace.yaml @@ -9,5 +9,6 @@ overrides: patchedDependencies: expo-notifications@55.0.27: patches/expo-notifications@55.0.27.patch + query-string@9.5.1: patches/query-string@9.5.1.patch react-native-webview@13.16.2: patches/react-native-webview@13.16.2.patch react-native@0.83.10: patches/react-native@0.83.10.patch diff --git a/mobile/src/navigation/query-string-named-exports.test.ts b/mobile/src/navigation/query-string-named-exports.test.ts new file mode 100644 index 00000000000..9cd34aedaea --- /dev/null +++ b/mobile/src/navigation/query-string-named-exports.test.ts @@ -0,0 +1,28 @@ +import { execFileSync } from 'node:child_process' +import { createRequire } from 'node:module' +import { pathToFileURL } from 'node:url' +import { describe, expect, it } from 'vitest' + +/** + * expo-router and React Navigation serialise route params through `import * as queryString from + * 'query-string'`. The lockfile overrides `query-string` to 9.x for a `decode-uri-component` + * advisory, and 9.x's entry has a default export only, so without `patches/query-string@9.5.1.patch` + * every push carrying a param outside the path pattern throws `queryString.stringify is not a + * function` and every href with a query throws on `parse`. Resolved from expo-router's own location, + * the way Metro and the web bundler resolve it for that consumer, and imported by a plain Node + * child: vitest's default-export interop would paper over the missing names in-process. + */ +describe('query-string, as expo-router resolves it', () => { + it('exposes the named API the namespace import needs', () => { + const requireFromHere = createRequire(import.meta.url) + const requireFromExpoRouter = createRequire(requireFromHere.resolve('expo-router/package.json')) + const entry = pathToFileURL(requireFromExpoRouter.resolve('query-string')).href + const script = `const ns = await import(${JSON.stringify(entry)}); process.stdout.write(JSON.stringify({ names: Object.keys(ns).sort(), stringified: typeof ns.stringify === 'function' ? ns.stringify({ from: 'worktrees' }) : null }))` + const output = execFileSync(process.execPath, ['--input-type=module', '-e', script], { + encoding: 'utf8' + }) + const { names, stringified } = JSON.parse(output) + expect(names).toEqual(expect.arrayContaining(['parse', 'stringify'])) + expect(stringified).toBe('from=worktrees') + }) +}) From 87a22db25af1bbcd56b966c02d65344e53c43eea Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 20 Sep 2026 21:02:24 -0700 Subject: [PATCH 3/3] fix(opencode): include cache read and write usage totals (#21886) * fix(opencode): include cache read and write usage totals * fix(opencode): satisfy aggregate query safety checks * chore(i18n): refresh runtime English catalog --- .../opencode-usage-row-queries.ts | 34 ++++++++++++++++--- src/main/opencode-usage/scanner.test.ts | 16 +++++---- .../src/i18n/en-runtime-required.json | 12 +++++++ 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/src/main/opencode-usage/opencode-usage-row-queries.ts b/src/main/opencode-usage/opencode-usage-row-queries.ts index 2e633d7cda2..db8e84010b2 100644 --- a/src/main/opencode-usage/opencode-usage-row-queries.ts +++ b/src/main/opencode-usage/opencode-usage-row-queries.ts @@ -27,6 +27,7 @@ type OpenCodeSessionUsageRow = { tokens_output: number tokens_reasoning: number tokens_cache_read: number + tokens_cache_write: number } function getProjectJoin(db: Database.Database): string { @@ -46,6 +47,7 @@ function getAssistantSessionMessageCount(db: Database.Database): number { const assistantPredicate = columnExists(db, 'session_message', 'type') ? "type = 'assistant' AND json_extract(data, '$.tokens.input') IS NOT NULL" : "json_extract(data, '$.tokens.input') IS NOT NULL" + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: SQLite aggregate rows are validated by the typed count field below. const row = db .prepare(`SELECT COUNT(*) AS count FROM session_message WHERE ${assistantPredicate}`) .get() as { count?: number } | undefined @@ -61,16 +63,29 @@ function canReadSessionUsageRows(db: Database.Database): boolean { ) } +function getSessionCacheWriteSelect(db: Database.Database): string { + return columnExists(db, 'session', 'tokens_cache_write') ? 's.tokens_cache_write' : '0' +} + +function getSessionTokenTotalExpression(db: Database.Database): string { + const cacheWrite = columnExists(db, 'session', 'tokens_cache_write') + ? ' + tokens_cache_write' + : '' + return `tokens_input + tokens_output + tokens_reasoning + tokens_cache_read${cacheWrite}` +} + function getSessionUsageRowCount(db: Database.Database): number { if (!canReadSessionUsageRows(db)) { return 0 } + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: SQLite aggregate rows are validated by the typed count field below. const row = db .prepare( `SELECT COUNT(*) AS count FROM session - WHERE tokens_input + tokens_output + tokens_reasoning + tokens_cache_read > 0` + WHERE ${getSessionTokenTotalExpression(db)} > 0` ) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: SQLite aggregate rows are validated by the typed count field below. .get() as { count?: number } | undefined return row?.count ?? 0 } @@ -78,14 +93,18 @@ function getSessionUsageRowCount(db: Database.Database): number { function selectSessionUsageRows(db: Database.Database): OpenCodeUsageRow[] { const projectJoin = getProjectJoin(db) const sessionModelSelect = getSessionModelSelect(db) + const cacheWriteSelect = getSessionCacheWriteSelect(db) + const tokenTotalExpression = getSessionTokenTotalExpression(db) + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: SELECT aliases match OpenCodeSessionUsageRow across supported schemas. const rows = db .prepare( `SELECT s.id, s.id AS session_id, s.time_created, s.time_updated, s.directory, s.title, p.worktree, ${sessionModelSelect}, - s.cost, s.tokens_input, s.tokens_output, s.tokens_reasoning, s.tokens_cache_read + s.cost, s.tokens_input, s.tokens_output, s.tokens_reasoning, s.tokens_cache_read, + ${cacheWriteSelect} AS tokens_cache_write FROM session s ${projectJoin} - WHERE s.tokens_input + s.tokens_output + s.tokens_reasoning + s.tokens_cache_read > 0 + WHERE ${tokenTotalExpression.replaceAll('tokens_', 's.tokens_')} > 0 ORDER BY s.time_created, s.id` ) .all() as OpenCodeSessionUsageRow[] @@ -105,10 +124,15 @@ function selectSessionUsageRows(db: Database.Database): OpenCodeUsageRow[] { input: row.tokens_input, output: row.tokens_output, reasoning: row.tokens_reasoning, - total: row.tokens_input + row.tokens_output + row.tokens_reasoning, + total: + row.tokens_input + + row.tokens_output + + row.tokens_reasoning + + row.tokens_cache_read + + row.tokens_cache_write, cache: { read: row.tokens_cache_read, - write: 0 + write: row.tokens_cache_write } } }) diff --git a/src/main/opencode-usage/scanner.test.ts b/src/main/opencode-usage/scanner.test.ts index 4188a0f93a9..88559b0ec7d 100644 --- a/src/main/opencode-usage/scanner.test.ts +++ b/src/main/opencode-usage/scanner.test.ts @@ -43,6 +43,7 @@ function createSessionTotalsSchema(db: Database.Database): void { tokens_output INTEGER, tokens_reasoning INTEGER, tokens_cache_read INTEGER, + tokens_cache_write INTEGER, time_created INTEGER, time_updated INTEGER ); @@ -57,9 +58,9 @@ function insertSessionTotalsRow( db.prepare( `INSERT INTO session ( id, directory, title, model, cost, - tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, + tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write, time_created, time_updated - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ).run( sessionId, `${WORKTREE}/packages/app`, @@ -70,6 +71,7 @@ function insertSessionTotalsRow( 100, 0, 0, + 0, 1_777_777_700_000, 1_777_777_800_000 ) @@ -235,6 +237,7 @@ describe('parseOpenCodeUsageDatabase', () => { tokens_output INTEGER, tokens_reasoning INTEGER, tokens_cache_read INTEGER, + tokens_cache_write INTEGER, time_created INTEGER, time_updated INTEGER ); @@ -243,9 +246,9 @@ describe('parseOpenCodeUsageDatabase', () => { db.prepare( `INSERT INTO session ( id, project_id, directory, title, model, cost, - tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, + tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write, time_created, time_updated - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ).run( 'session-1', 'project-1', @@ -257,6 +260,7 @@ describe('parseOpenCodeUsageDatabase', () => { 500, 100, 250, + 75, 1_777_777_700_000, 1_777_777_800_000 ) @@ -274,7 +278,7 @@ describe('parseOpenCodeUsageDatabase', () => { totalCachedInputTokens: 250, totalOutputTokens: 500, totalReasoningOutputTokens: 100, - totalTokens: 1850, + totalTokens: 1925, estimatedCostUsd: 0.06 }) expect(parsed.dailyAggregates).toEqual([ @@ -284,7 +288,7 @@ describe('parseOpenCodeUsageDatabase', () => { cachedInputTokens: 250, outputTokens: 500, reasoningOutputTokens: 100, - totalTokens: 1850, + totalTokens: 1925, estimatedCostUsd: 0.06 }) ]) diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index f28c32e0b8a..a915bd8e132 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -1074,17 +1074,24 @@ }, "settings": { "AccountsPane": { + "0023cc336e": "Paste either the raw token value (e.g.", "15e831350e": "Configure MiniMax usage tracking from platform.minimax.io.", "1fd1b1b6b4": "Cookie not set", + "338820326a": ") or the full cookie header (e.g.", "3455cf43fa": "Claude login.", "350b2a1aa7": "Use your current", "4e32e030b2": "Stored locally. Orca sends it only to platform.minimax.io for usage refreshes.", "566d9a99ab": "_token=…; minimax_group_id_v2=…", "5e08b0fe57": "Stored locally and sent only to platform.minimax.io for usage refreshes.", "79418c782a": "Open platform.minimax.io/console/usage in your browser, sign in, then copy the Cookie request header from DevTools (Network → any remains request → Cookie).", + "7ce0e1907c": "). Find it in your browser's DevTools → Network → any opencode.ai request → Cookie header. OpenCode Go auth is web-based and shared across Windows and WSL terminals.", + "8951c5309f": "auth=Fe26.2**…", "9107406589": "Could not load Claude accounts.", + "922b51e02d": "Fe26.2**…", + "a7e38affcd": "Fe26.2**… token or auth=Fe26.2**… header", "b10cb4f696": "adding", "b11078a9c2": "wsl", + "b2b1aa936d": "Paste your opencode.ai session cookie for rate limit fetching.", "b43e761fe5": "MiniMax cookie update failed.", "b8c2905c2b": "Could not load Codex accounts.", "f5d8d2a6a1": "Open platform.minimax.io/console/usage in your browser and sign in." @@ -1557,6 +1564,11 @@ "7c3bb36706": "remove", "e2b0ee267f": "stale" }, + "accounts": { + "search": { + "d1d2ae383c": "Paste your opencode.ai session cookie for rate limit fetching." + } + }, "agent-awake-copy": { "95d3031db2": "Keeps this computer and display awake while agents are working. Lid-close behavior follows this device's power settings.", "a42f6fbdd8": "Keeps this computer and display awake while agents are working. Orca also asks this device to stay awake when the lid is closed, subject to its power policy.",