@phnx-labs/agents-cli 1.22.25 → 1.22.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +427 -0
- package/README.md +35 -3
- package/dist/bin/agents +0 -0
- package/dist/browser.js +14 -4
- package/dist/commands/apply.js +52 -8
- package/dist/commands/bench.d.ts +2 -0
- package/dist/commands/bench.js +101 -0
- package/dist/commands/browser.js +35 -0
- package/dist/commands/doctor.js +32 -10
- package/dist/commands/exec.js +17 -16
- package/dist/commands/insights.d.ts +25 -19
- package/dist/commands/insights.js +146 -38
- package/dist/commands/output.js +100 -25
- package/dist/commands/reconnect.d.ts +46 -0
- package/dist/commands/reconnect.js +109 -0
- package/dist/commands/resume.d.ts +11 -0
- package/dist/commands/resume.js +51 -0
- package/dist/commands/routines.js +2 -2
- package/dist/commands/secrets.d.ts +2 -8
- package/dist/commands/secrets.js +29 -105
- package/dist/commands/sessions-picker.js +12 -7
- package/dist/commands/sessions.d.ts +47 -1
- package/dist/commands/sessions.js +349 -37
- package/dist/commands/setup-secrets.d.ts +1 -0
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/setup.d.ts +26 -3
- package/dist/commands/setup.js +105 -46
- package/dist/commands/ssh.js +184 -11
- package/dist/commands/teams.d.ts +6 -0
- package/dist/commands/teams.js +46 -3
- package/dist/commands/trends.d.ts +8 -0
- package/dist/commands/trends.js +10 -156
- package/dist/index.js +17 -21
- package/dist/lib/agents.d.ts +11 -0
- package/dist/lib/agents.js +29 -2
- package/dist/lib/analytics/dashboard.d.ts +10 -6
- package/dist/lib/analytics/dashboard.js +6 -4
- package/dist/lib/analytics/mix-commands.d.ts +53 -0
- package/dist/lib/analytics/mix-commands.js +229 -0
- package/dist/lib/analytics/recipes.d.ts +19 -14
- package/dist/lib/analytics/recipes.js +4 -2
- package/dist/lib/auth-health.d.ts +47 -3
- package/dist/lib/auth-health.js +74 -15
- package/dist/lib/bench/index.d.ts +4 -0
- package/dist/lib/bench/index.js +4 -0
- package/dist/lib/bench/runner.d.ts +16 -0
- package/dist/lib/bench/runner.js +111 -0
- package/dist/lib/bench/schema.d.ts +5 -0
- package/dist/lib/bench/schema.js +91 -0
- package/dist/lib/bench/storage.d.ts +5 -0
- package/dist/lib/bench/storage.js +32 -0
- package/dist/lib/bench/types.d.ts +40 -0
- package/dist/lib/bench/types.js +1 -0
- package/dist/lib/browser/ipc.d.ts +26 -0
- package/dist/lib/browser/ipc.js +139 -24
- package/dist/lib/browser/profiles.d.ts +11 -0
- package/dist/lib/browser/profiles.js +1 -1
- package/dist/lib/browser/stream.d.ts +14 -0
- package/dist/lib/browser/stream.js +71 -0
- package/dist/lib/channels/owner-sink.d.ts +27 -0
- package/dist/lib/channels/owner-sink.js +93 -0
- package/dist/lib/claude-account-token.d.ts +2 -0
- package/dist/lib/claude-account-token.js +27 -1
- package/dist/lib/crabbox/cli.d.ts +2 -0
- package/dist/lib/crabbox/cli.js +2 -0
- package/dist/lib/crabbox/lease.js +7 -1
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/devices/connect.d.ts +2 -0
- package/dist/lib/devices/connect.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +10 -1
- package/dist/lib/devices/doctor-findings.js +47 -1
- package/dist/lib/devices/harness-inventory.d.ts +97 -0
- package/dist/lib/devices/harness-inventory.js +0 -0
- package/dist/lib/devices/registry.d.ts +2 -0
- package/dist/lib/devices/ssh-config.js +3 -0
- package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
- package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
- package/dist/lib/exec.bench.d.ts +1 -0
- package/dist/lib/exec.bench.js +186 -0
- package/dist/lib/exec.js +18 -8
- package/dist/lib/fleet/apply.d.ts +59 -3
- package/dist/lib/fleet/apply.js +191 -12
- package/dist/lib/fleet/remote-login.d.ts +4 -3
- package/dist/lib/fleet/remote-login.js +11 -9
- package/dist/lib/fleet/types.d.ts +21 -2
- package/dist/lib/gemini-settings.d.ts +0 -1
- package/dist/lib/gemini-settings.js +12 -7
- package/dist/lib/hooks/cache.js +15 -0
- package/dist/lib/hooks.d.ts +8 -5
- package/dist/lib/hooks.js +10 -6
- package/dist/lib/hosts/dispatch.js +7 -3
- package/dist/lib/hosts/passthrough.d.ts +45 -0
- package/dist/lib/hosts/passthrough.js +59 -8
- package/dist/lib/hosts/progress.d.ts +2 -1
- package/dist/lib/hosts/progress.js +4 -3
- package/dist/lib/hosts/providers/devices.js +1 -0
- package/dist/lib/hosts/ready.d.ts +6 -2
- package/dist/lib/hosts/ready.js +33 -8
- package/dist/lib/hosts/reconcile.d.ts +1 -1
- package/dist/lib/hosts/reconcile.js +16 -7
- package/dist/lib/hosts/reconnect.d.ts +56 -21
- package/dist/lib/hosts/reconnect.js +128 -71
- package/dist/lib/hosts/registry.d.ts +2 -2
- package/dist/lib/hosts/registry.js +3 -5
- package/dist/lib/hosts/remote-cmd.d.ts +17 -0
- package/dist/lib/hosts/remote-cmd.js +29 -0
- package/dist/lib/hosts/tasks.d.ts +2 -0
- package/dist/lib/hosts/types.d.ts +1 -0
- package/dist/lib/hosts/types.js +3 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +9 -6
- package/dist/lib/menubar/install-menubar.js +20 -9
- package/dist/lib/models.d.ts +30 -1
- package/dist/lib/models.js +42 -144
- package/dist/lib/pricing/cost.d.ts +9 -0
- package/dist/lib/pricing/cost.js +24 -0
- package/dist/lib/pricing/index.d.ts +1 -1
- package/dist/lib/pricing/index.js +1 -1
- package/dist/lib/redact.js +8 -3
- package/dist/lib/remote-agents-json.d.ts +32 -0
- package/dist/lib/remote-agents-json.js +47 -16
- package/dist/lib/resource-profiles.js +1 -2
- package/dist/lib/routine-notify-owner.d.ts +102 -0
- package/dist/lib/routine-notify-owner.js +232 -0
- package/dist/lib/routines.d.ts +10 -0
- package/dist/lib/routines.js +14 -2
- package/dist/lib/runner.d.ts +9 -4
- package/dist/lib/runner.js +182 -30
- package/dist/lib/sandbox.d.ts +0 -2
- package/dist/lib/sandbox.js +2 -19
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.js +3 -2
- package/dist/lib/secrets/lease.d.ts +25 -0
- package/dist/lib/secrets/lease.js +44 -0
- package/dist/lib/secrets/push.d.ts +94 -0
- package/dist/lib/secrets/push.js +145 -0
- package/dist/lib/secrets/reaper.d.ts +15 -1
- package/dist/lib/secrets/reaper.js +30 -3
- package/dist/lib/self-update.d.ts +20 -5
- package/dist/lib/self-update.js +93 -16
- package/dist/lib/session/db.d.ts +38 -3
- package/dist/lib/session/db.js +273 -16
- package/dist/lib/session/discover.d.ts +22 -1
- package/dist/lib/session/discover.js +239 -51
- package/dist/lib/session/insights.d.ts +37 -0
- package/dist/lib/session/insights.js +219 -9
- package/dist/lib/session/recovery.d.ts +17 -1
- package/dist/lib/session/recovery.js +111 -4
- package/dist/lib/session/remote-list.d.ts +10 -9
- package/dist/lib/session/remote-list.js +25 -23
- package/dist/lib/session/resume-owner.d.ts +55 -0
- package/dist/lib/session/resume-owner.js +69 -0
- package/dist/lib/session/team-filter.d.ts +65 -0
- package/dist/lib/session/team-filter.js +98 -3
- package/dist/lib/session/tool-index.js +133 -22
- package/dist/lib/session/tool-store.d.ts +26 -2
- package/dist/lib/session/tool-store.js +36 -17
- package/dist/lib/session/types.d.ts +33 -0
- package/dist/lib/smart-launch.d.ts +31 -5
- package/dist/lib/smart-launch.js +43 -7
- package/dist/lib/ssh-exec.d.ts +27 -0
- package/dist/lib/ssh-exec.js +42 -3
- package/dist/lib/ssh-tunnel.d.ts +3 -2
- package/dist/lib/ssh-tunnel.js +25 -16
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +6 -0
- package/dist/lib/teams/agents.d.ts +13 -0
- package/dist/lib/teams/agents.js +75 -7
- package/dist/lib/teams/placement-probe.d.ts +21 -0
- package/dist/lib/teams/placement-probe.js +135 -0
- package/dist/lib/teams/scheduler.d.ts +74 -1
- package/dist/lib/teams/scheduler.js +187 -10
- package/dist/lib/tmux/session.d.ts +8 -0
- package/dist/lib/tmux/session.js +22 -0
- package/dist/lib/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -20,6 +20,8 @@ import { parseTimeFilter } from './relative-time.js';
|
|
|
20
20
|
const execFileAsync = promisify(execFile);
|
|
21
21
|
import { AGENTS, agentConfigDirName, getCliVersion } from '../agents.js';
|
|
22
22
|
import { walkForFilesWithStat } from '../fs-walk.js';
|
|
23
|
+
import { hasCommand } from '../cli-resources.js';
|
|
24
|
+
import { execFileShellSpec } from '../platform/exec.js';
|
|
23
25
|
import { getConfigSymlinkVersion } from '../shims.js';
|
|
24
26
|
import { SESSION_AGENTS } from './types.js';
|
|
25
27
|
import { deriveShortId } from './short-id.js';
|
|
@@ -28,7 +30,7 @@ import { extractSessionTopic, extractSlashCommandName, extractSlashCommandFromTo
|
|
|
28
30
|
import { isSkillInvocation, extractSkills, extractSlashCommands } from './highlights.js';
|
|
29
31
|
import { parseAntigravity, parseCursor } from './parse.js';
|
|
30
32
|
import { extractPrUrl, detectWorktree, detectTicket, isPrCreateCommand, detectSpawnedTeam, isTicketCreateTool, extractCreatedTicket, extractRecentDirectoriesTouched, extractTodoProgressFromEvents } from './state.js';
|
|
31
|
-
import { costOfUsage } from '../pricing/index.js';
|
|
33
|
+
import { costOfUsage, costOfUsageNoCache } from '../pricing/index.js';
|
|
32
34
|
import { machineForSessionFile } from './origin-machine.js';
|
|
33
35
|
export { machineForSessionFile } from './origin-machine.js';
|
|
34
36
|
import { mapBounded } from '../concurrency.js';
|
|
@@ -877,6 +879,7 @@ async function scanRoutineArchivesIncremental(agent, onProgress) {
|
|
|
877
879
|
meta: result.meta,
|
|
878
880
|
content: result.content,
|
|
879
881
|
scan,
|
|
882
|
+
events: result.events,
|
|
880
883
|
toolCalls: result.toolCalls,
|
|
881
884
|
toolIndexMode: result.toolIndexMode,
|
|
882
885
|
});
|
|
@@ -1095,7 +1098,11 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, label) {
|
|
|
1095
1098
|
toolCallCount: scan.toolCallCount,
|
|
1096
1099
|
tokenCount: scan.tokenCount,
|
|
1097
1100
|
outputTokens: scan.outputTokens,
|
|
1101
|
+
inputTokens: scan.inputTokens,
|
|
1102
|
+
cacheReadTokens: scan.cacheReadTokens,
|
|
1103
|
+
cacheWriteTokens: scan.cacheWriteTokens,
|
|
1098
1104
|
costUsd: scan.costUsd,
|
|
1105
|
+
costUsdNoCache: scan.costUsdNoCache,
|
|
1099
1106
|
durationMs: scan.durationMs,
|
|
1100
1107
|
isTeamOrigin,
|
|
1101
1108
|
prUrl: scan.prUrl,
|
|
@@ -1129,7 +1136,11 @@ async function readClaudeMeta(filePath, sessionId, scanStamp, priorRow, label) {
|
|
|
1129
1136
|
toolCallCount: scan.toolCallCount,
|
|
1130
1137
|
tokenCount: scan.tokenCount,
|
|
1131
1138
|
outputTokens: scan.outputTokens,
|
|
1139
|
+
inputTokens: scan.inputTokens,
|
|
1140
|
+
cacheReadTokens: scan.cacheReadTokens,
|
|
1141
|
+
cacheWriteTokens: scan.cacheWriteTokens,
|
|
1132
1142
|
costUsd: scan.costUsd,
|
|
1143
|
+
costUsdNoCache: scan.costUsdNoCache,
|
|
1133
1144
|
durationMs: scan.durationMs,
|
|
1134
1145
|
topic: scan.topic,
|
|
1135
1146
|
isTeamOrigin,
|
|
@@ -1437,7 +1448,11 @@ export async function readCodexMeta(filePath, resolveAccount, currentVersion, sc
|
|
|
1437
1448
|
messageCount: scan.messageCount,
|
|
1438
1449
|
tokenCount: scan.tokenCount,
|
|
1439
1450
|
outputTokens: scan.outputTokens,
|
|
1451
|
+
inputTokens: scan.inputTokens,
|
|
1452
|
+
cacheReadTokens: scan.cacheReadTokens,
|
|
1453
|
+
cacheWriteTokens: scan.cacheWriteTokens,
|
|
1440
1454
|
costUsd: scan.costUsd,
|
|
1455
|
+
costUsdNoCache: scan.costUsdNoCache,
|
|
1441
1456
|
durationMs: scan.durationMs,
|
|
1442
1457
|
account: resolveAccount?.(),
|
|
1443
1458
|
prUrl: scan.prUrl,
|
|
@@ -1575,8 +1590,11 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
|
|
|
1575
1590
|
let messageCount = 0;
|
|
1576
1591
|
let tokenCount = 0;
|
|
1577
1592
|
let outputTokens = 0;
|
|
1593
|
+
let inputTokens = 0;
|
|
1594
|
+
let cacheReadTokens = 0;
|
|
1578
1595
|
let sawTokenCount = false;
|
|
1579
1596
|
let costUsd = 0;
|
|
1597
|
+
let costUsdNoCache = 0;
|
|
1580
1598
|
let sawCost = false;
|
|
1581
1599
|
let firstTsMs;
|
|
1582
1600
|
let lastTsMs;
|
|
@@ -1621,21 +1639,28 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
|
|
|
1621
1639
|
(typeof gtk.output === 'number' ? gtk.output : 0) +
|
|
1622
1640
|
(typeof gtk.thoughts === 'number' ? gtk.thoughts : 0) +
|
|
1623
1641
|
(typeof gtk.tool === 'number' ? gtk.tool : 0);
|
|
1642
|
+
// Burn split (RUSH-2287): Gemini has uncached input + cache-read, no cache-write.
|
|
1643
|
+
if (typeof gtk.input === 'number')
|
|
1644
|
+
inputTokens += gtk.input;
|
|
1645
|
+
if (typeof gtk.cached === 'number')
|
|
1646
|
+
cacheReadTokens += gtk.cached;
|
|
1624
1647
|
}
|
|
1625
1648
|
// Per-message cost: directional tokens × this message's model price.
|
|
1626
1649
|
const msgModel = (typeof message.model === 'string' ? message.model : undefined) || sessionModel;
|
|
1627
1650
|
const tk = message.tokens;
|
|
1628
1651
|
if (msgModel && tk && typeof tk === 'object') {
|
|
1629
|
-
const
|
|
1652
|
+
const usage = {
|
|
1630
1653
|
model: msgModel,
|
|
1631
1654
|
inputTokens: typeof tk.input === 'number' ? tk.input : undefined,
|
|
1632
1655
|
outputTokens: (typeof tk.output === 'number' ? tk.output : 0) +
|
|
1633
1656
|
(typeof tk.thoughts === 'number' ? tk.thoughts : 0) +
|
|
1634
1657
|
(typeof tk.tool === 'number' ? tk.tool : 0),
|
|
1635
1658
|
cacheReadTokens: typeof tk.cached === 'number' ? tk.cached : undefined,
|
|
1636
|
-
}
|
|
1659
|
+
};
|
|
1660
|
+
const c = costOfUsage(usage);
|
|
1637
1661
|
if (c > 0) {
|
|
1638
1662
|
costUsd += c;
|
|
1663
|
+
costUsdNoCache += costOfUsageNoCache(usage);
|
|
1639
1664
|
sawCost = true;
|
|
1640
1665
|
}
|
|
1641
1666
|
}
|
|
@@ -1658,7 +1683,10 @@ function readGeminiMeta(filePath, hashDir, projectMap, currentVersion) {
|
|
|
1658
1683
|
messageCount,
|
|
1659
1684
|
tokenCount: sawTokenCount ? tokenCount : undefined,
|
|
1660
1685
|
outputTokens: sawTokenCount ? outputTokens : undefined,
|
|
1686
|
+
inputTokens: sawTokenCount ? inputTokens : undefined,
|
|
1687
|
+
cacheReadTokens: sawTokenCount ? cacheReadTokens : undefined,
|
|
1661
1688
|
costUsd: sawCost ? costUsd : undefined,
|
|
1689
|
+
costUsdNoCache: sawCost ? costUsdNoCache : undefined,
|
|
1662
1690
|
durationMs,
|
|
1663
1691
|
};
|
|
1664
1692
|
return { meta, content: userTexts.join('\n') };
|
|
@@ -1756,7 +1784,7 @@ async function scanAntigravityIncremental(onProgress) {
|
|
|
1756
1784
|
const result = readAntigravityMeta(filePath, currentVersion);
|
|
1757
1785
|
if (result && !seen.has(result.meta.id)) {
|
|
1758
1786
|
seen.add(result.meta.id);
|
|
1759
|
-
entries.push({ meta: result.meta, content: result.content, scan });
|
|
1787
|
+
entries.push({ meta: result.meta, content: result.content, scan, events: result.events });
|
|
1760
1788
|
}
|
|
1761
1789
|
else {
|
|
1762
1790
|
touched.push({ filePath, scan });
|
|
@@ -1802,23 +1830,28 @@ function readAntigravityMeta(filePath, currentVersion) {
|
|
|
1802
1830
|
topic: topic ? topic.slice(0, 120) : undefined,
|
|
1803
1831
|
messageCount: events.length,
|
|
1804
1832
|
};
|
|
1805
|
-
return { meta, content: contentParts.join('\n') };
|
|
1833
|
+
return { meta, content: contentParts.join('\n'), events };
|
|
1806
1834
|
}
|
|
1807
1835
|
// ---------------------------------------------------------------------------
|
|
1808
1836
|
// OpenCode
|
|
1809
1837
|
// ---------------------------------------------------------------------------
|
|
1810
1838
|
const OPENCODE_DB = path.join(HOME, '.local', 'share', 'opencode', 'opencode.db');
|
|
1811
1839
|
let cachedOpenCodeAccount;
|
|
1812
|
-
/**
|
|
1813
|
-
|
|
1840
|
+
/**
|
|
1841
|
+
* Query the active OpenCode account email from its SQLite database.
|
|
1842
|
+
*
|
|
1843
|
+
* Takes the scan's already-open handle so a scan opens `opencode.db` exactly
|
|
1844
|
+
* once (RUSH-2210); opens its own only when called without one.
|
|
1845
|
+
*/
|
|
1846
|
+
function getOpenCodeAccount(handle) {
|
|
1814
1847
|
if (cachedOpenCodeAccount !== undefined)
|
|
1815
1848
|
return cachedOpenCodeAccount || undefined;
|
|
1816
1849
|
// Read through the node/bun SQLite wrapper (not the `sqlite3` CLI) so this
|
|
1817
1850
|
// works on every OS — the CLI is absent on Windows.
|
|
1818
|
-
let
|
|
1851
|
+
let owned;
|
|
1819
1852
|
try {
|
|
1820
|
-
|
|
1821
|
-
|
|
1853
|
+
const db = handle ?? (fs.existsSync(OPENCODE_DB) ? (owned = new Database(OPENCODE_DB)) : undefined);
|
|
1854
|
+
if (db) {
|
|
1822
1855
|
const row = db
|
|
1823
1856
|
.prepare('SELECT email FROM control_account WHERE active=1 LIMIT 1;')
|
|
1824
1857
|
.get();
|
|
@@ -1832,13 +1865,63 @@ async function getOpenCodeAccount() {
|
|
|
1832
1865
|
catch { /* DB not accessible, sqlite module unavailable, or query failed */ }
|
|
1833
1866
|
finally {
|
|
1834
1867
|
try {
|
|
1835
|
-
|
|
1868
|
+
owned?.close();
|
|
1836
1869
|
}
|
|
1837
1870
|
catch { /* best-effort close */ }
|
|
1838
1871
|
}
|
|
1839
1872
|
cachedOpenCodeAccount = '';
|
|
1840
1873
|
return undefined;
|
|
1841
1874
|
}
|
|
1875
|
+
/**
|
|
1876
|
+
* The per-session ledger stamp for an OpenCode row: the newest write time across
|
|
1877
|
+
* the session's own row, its messages, and its parts, paired with the total byte
|
|
1878
|
+
* length of that session's message + part payloads.
|
|
1879
|
+
*
|
|
1880
|
+
* OpenCode keeps every session in ONE shared SQLite file, so that file's
|
|
1881
|
+
* mtime/size changes whenever *any* session is written. Stamping each session
|
|
1882
|
+
* with the whole-DB stat therefore invalidated every indexed session on every
|
|
1883
|
+
* scan: up to `LIMIT 1000` sessions were re-emitted into `upsertSessionsBatch`,
|
|
1884
|
+
* and its enrichment step re-opened `opencode.db` once per re-emitted entry via
|
|
1885
|
+
* `parseSession` (RUSH-2210).
|
|
1886
|
+
*
|
|
1887
|
+
* `s.time_updated` alone is NOT that session's change signal, which is the trap
|
|
1888
|
+
* this shape exists to avoid. On a real database, parts land long after the
|
|
1889
|
+
* session row was last touched — e.g. `ses_3955202dfffe…` carried
|
|
1890
|
+
* `time_updated = 1771316403087` with its newest part at `1771331512162`, over
|
|
1891
|
+
* four hours later. So the stamp maxes `time_updated` with the newest message
|
|
1892
|
+
* and part times, and pairs it with a byte total that also moves when an
|
|
1893
|
+
* existing part's `data` is rewritten in place (a streaming turn) without any
|
|
1894
|
+
* new row or timestamp.
|
|
1895
|
+
*
|
|
1896
|
+
* The byte total is a REAL size, not a repurposed counter, because
|
|
1897
|
+
* `sessions.file_size` is read back as bytes elsewhere: `ensureToolIndex` uses
|
|
1898
|
+
* it as the tool-backfill byte budget and `toolCallsForBackfill` as the 16 MiB
|
|
1899
|
+
* in-memory parser cap (`tool-index.ts`). A message/part byte total is the
|
|
1900
|
+
* honest cost of parsing that session — strictly better than the whole-DB size
|
|
1901
|
+
* this column used to hold for every OpenCode row.
|
|
1902
|
+
*
|
|
1903
|
+
* The size is `LENGTH(CAST(data AS BLOB))`, not `LENGTH(data)`: SQLite's
|
|
1904
|
+
* `LENGTH()` on a TEXT column counts CHARACTERS, so a CJK/emoji-heavy transcript
|
|
1905
|
+
* would under-report its real size by up to 4x — and this number is a byte
|
|
1906
|
+
* budget downstream.
|
|
1907
|
+
*
|
|
1908
|
+
* A degenerate row — nothing but zeros/NaN for every time AND no payload at all
|
|
1909
|
+
* — falls back to the whole-DB stat, keeping the pre-RUSH-2210 always-rescan
|
|
1910
|
+
* behavior for exactly those rows rather than pinning them to a stamp that never
|
|
1911
|
+
* changes.
|
|
1912
|
+
*/
|
|
1913
|
+
function openCodeSessionStamp(times, bytes, dbScan) {
|
|
1914
|
+
// A positive time, not merely a finite one: the SQL COALESCEs the aggregates
|
|
1915
|
+
// to 0, so `Number.isFinite` alone would accept a row that told us nothing and
|
|
1916
|
+
// this branch could never fire.
|
|
1917
|
+
const known = [times.timeUpdated, times.lastMessageAt, times.lastPartAt, times.timeCreated]
|
|
1918
|
+
.filter(t => Number.isFinite(t) && t > 0);
|
|
1919
|
+
const size = (Number.isFinite(bytes.messageBytes) ? bytes.messageBytes : 0)
|
|
1920
|
+
+ (Number.isFinite(bytes.partBytes) ? bytes.partBytes : 0);
|
|
1921
|
+
if (known.length === 0 && size === 0)
|
|
1922
|
+
return dbScan;
|
|
1923
|
+
return { fileMtimeMs: known.length === 0 ? 0 : Math.floor(Math.max(...known)), fileSize: size };
|
|
1924
|
+
}
|
|
1842
1925
|
/** Scan OpenCode sessions from its SQLite database when the DB file has changed. */
|
|
1843
1926
|
async function scanOpenCodeIncremental() {
|
|
1844
1927
|
if (!fs.existsSync(OPENCODE_DB))
|
|
@@ -1846,8 +1929,9 @@ async function scanOpenCodeIncremental() {
|
|
|
1846
1929
|
const stat = safeStatSync(OPENCODE_DB);
|
|
1847
1930
|
if (!stat)
|
|
1848
1931
|
return;
|
|
1849
|
-
// OpenCode is one big DB
|
|
1850
|
-
//
|
|
1932
|
+
// OpenCode is one big DB. Its mtime/size is the cheap "did ANYTHING change"
|
|
1933
|
+
// short-circuit for the whole harness — not the per-session stamp, which each
|
|
1934
|
+
// row carries itself (see openCodeSessionStamp, RUSH-2210).
|
|
1851
1935
|
const currentScan = {
|
|
1852
1936
|
fileMtimeMs: Math.floor(stat.mtimeMs),
|
|
1853
1937
|
fileSize: stat.size,
|
|
@@ -1856,7 +1940,6 @@ async function scanOpenCodeIncremental() {
|
|
|
1856
1940
|
if (prev && prev.fileMtimeMs === currentScan.fileMtimeMs && prev.fileSize === currentScan.fileSize) {
|
|
1857
1941
|
return;
|
|
1858
1942
|
}
|
|
1859
|
-
const account = await getOpenCodeAccount();
|
|
1860
1943
|
const currentVersion = await getCurrentAgentVersion('opencode');
|
|
1861
1944
|
// Read through the node/bun SQLite wrapper (not the `sqlite3` CLI) so this
|
|
1862
1945
|
// works on every OS — the CLI is absent on Windows.
|
|
@@ -1871,14 +1954,28 @@ async function scanOpenCodeIncremental() {
|
|
|
1871
1954
|
s.time_created AS time_created,
|
|
1872
1955
|
s.time_updated AS time_updated,
|
|
1873
1956
|
COALESCE(stats.message_count, 0) AS message_count,
|
|
1957
|
+
COALESCE(stats.last_message_at, 0) AS last_message_at,
|
|
1958
|
+
COALESCE(stats.message_bytes, 0) AS message_bytes,
|
|
1959
|
+
COALESCE(parts.last_part_at, 0) AS last_part_at,
|
|
1960
|
+
COALESCE(parts.part_bytes, 0) AS part_bytes,
|
|
1874
1961
|
stats.token_count AS token_count,
|
|
1875
1962
|
stats.output_tokens AS output_tokens,
|
|
1876
1963
|
COALESCE(stats.has_token_data, 0) AS has_token_data
|
|
1877
1964
|
FROM session s
|
|
1965
|
+
LEFT JOIN (
|
|
1966
|
+
SELECT
|
|
1967
|
+
session_id,
|
|
1968
|
+
MAX(time_created) AS last_part_at,
|
|
1969
|
+
SUM(LENGTH(CAST(data AS BLOB))) AS part_bytes
|
|
1970
|
+
FROM part
|
|
1971
|
+
GROUP BY session_id
|
|
1972
|
+
) parts ON parts.session_id = s.id
|
|
1878
1973
|
LEFT JOIN (
|
|
1879
1974
|
SELECT
|
|
1880
1975
|
session_id,
|
|
1881
1976
|
COUNT(*) AS message_count,
|
|
1977
|
+
MAX(time_created) AS last_message_at,
|
|
1978
|
+
SUM(LENGTH(CAST(data AS BLOB))) AS message_bytes,
|
|
1882
1979
|
SUM(
|
|
1883
1980
|
COALESCE(json_extract(data, '$.tokens.input'), 0) +
|
|
1884
1981
|
COALESCE(json_extract(data, '$.tokens.output'), 0) +
|
|
@@ -1896,16 +1993,42 @@ async function scanOpenCodeIncremental() {
|
|
|
1896
1993
|
LIMIT 1000;
|
|
1897
1994
|
`.replace(/\n/g, ' ');
|
|
1898
1995
|
db = new Database(OPENCODE_DB);
|
|
1996
|
+
const account = getOpenCodeAccount(db);
|
|
1899
1997
|
const rows = db.prepare(query).all();
|
|
1900
|
-
|
|
1901
|
-
|
|
1998
|
+
// Two passes. First derive each row's identity + per-session stamp, then
|
|
1999
|
+
// bulk-load the prior stamps in ONE query and keep only the rows that
|
|
2000
|
+
// actually changed. Skipped rows never reach upsertSessionsBatch, so they
|
|
2001
|
+
// never pay its per-entry `parseSession` re-open of this same DB.
|
|
2002
|
+
const asInt = (v) => typeof v === 'number' ? v : parseInt(String(v), 10);
|
|
2003
|
+
const candidates = rows.flatMap(row => {
|
|
1902
2004
|
const id = typeof row.id === 'string' ? row.id : '';
|
|
1903
2005
|
if (!id)
|
|
1904
|
-
|
|
2006
|
+
return [];
|
|
2007
|
+
const filePath = `${OPENCODE_DB}#${id}`;
|
|
2008
|
+
return [{
|
|
2009
|
+
row,
|
|
2010
|
+
id,
|
|
2011
|
+
filePath,
|
|
2012
|
+
scan: openCodeSessionStamp({
|
|
2013
|
+
timeUpdated: asInt(row.time_updated),
|
|
2014
|
+
timeCreated: asInt(row.time_created),
|
|
2015
|
+
lastMessageAt: asInt(row.last_message_at),
|
|
2016
|
+
lastPartAt: asInt(row.last_part_at),
|
|
2017
|
+
}, { messageBytes: asInt(row.message_bytes), partBytes: asInt(row.part_bytes) }, currentScan),
|
|
2018
|
+
}];
|
|
2019
|
+
});
|
|
2020
|
+
const priorStamps = getScanStampsForPaths(candidates.map(c => c.filePath));
|
|
2021
|
+
const changed = candidates.filter(c => {
|
|
2022
|
+
const prevStamp = priorStamps.get(c.filePath);
|
|
2023
|
+
return !prevStamp
|
|
2024
|
+
|| prevStamp.fileMtimeMs !== c.scan.fileMtimeMs
|
|
2025
|
+
|| prevStamp.fileSize !== c.scan.fileSize;
|
|
2026
|
+
});
|
|
2027
|
+
const entries = [];
|
|
2028
|
+
for (const { row, id, filePath, scan } of changed) {
|
|
1905
2029
|
const title = typeof row.title === 'string' ? row.title : '';
|
|
1906
2030
|
const directory = typeof row.directory === 'string' ? row.directory : '';
|
|
1907
2031
|
const version = typeof row.version === 'string' ? row.version : '';
|
|
1908
|
-
const asInt = (v) => typeof v === 'number' ? v : parseInt(String(v), 10);
|
|
1909
2032
|
const timeCreated = asInt(row.time_created);
|
|
1910
2033
|
const timeUpdated = asInt(row.time_updated);
|
|
1911
2034
|
const messageCount = asInt(row.message_count);
|
|
@@ -1926,7 +2049,7 @@ async function scanOpenCodeIncremental() {
|
|
|
1926
2049
|
lastActivity,
|
|
1927
2050
|
project: directory ? path.basename(directory) : undefined,
|
|
1928
2051
|
cwd: directory ? normalizeCwd(directory) : undefined,
|
|
1929
|
-
filePath
|
|
2052
|
+
filePath,
|
|
1930
2053
|
version: resolveSessionVersion('opencode', OPENCODE_DB, version || undefined, currentVersion),
|
|
1931
2054
|
account,
|
|
1932
2055
|
topic,
|
|
@@ -1934,7 +2057,7 @@ async function scanOpenCodeIncremental() {
|
|
|
1934
2057
|
tokenCount: hasTokenData && !Number.isNaN(tokenCount) ? tokenCount : undefined,
|
|
1935
2058
|
outputTokens: hasTokenData && !Number.isNaN(outputTokens) ? outputTokens : undefined,
|
|
1936
2059
|
};
|
|
1937
|
-
entries.push({ meta, content: topic || '', scan
|
|
2060
|
+
entries.push({ meta, content: topic || '', scan });
|
|
1938
2061
|
}
|
|
1939
2062
|
upsertSessionsBatch(entries);
|
|
1940
2063
|
// Stamp the OpenCode DB itself so we can short-circuit on the next run.
|
|
@@ -1957,13 +2080,12 @@ async function scanOpenCodeIncremental() {
|
|
|
1957
2080
|
// ---------------------------------------------------------------------------
|
|
1958
2081
|
/** Scan active OpenClaw channels and cron jobs via the openclaw CLI. */
|
|
1959
2082
|
async function scanOpenClawIncremental() {
|
|
1960
|
-
// Check if openclaw is installed — silently skip if not.
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
2083
|
+
// Check if openclaw is installed — silently skip if not. `which` is POSIX-only
|
|
2084
|
+
// (Windows resolves PATH with `where`), so a bare `which` throws ENOENT on every
|
|
2085
|
+
// Windows run and silently disabled the entire OpenClaw scan there — its sessions
|
|
2086
|
+
// never reached the index (RUSH-2286). hasCommand() probes cross-platform.
|
|
2087
|
+
if (!hasCommand('openclaw'))
|
|
1965
2088
|
return;
|
|
1966
|
-
}
|
|
1967
2089
|
// TTL cache: skip subprocess calls if we scanned recently. Stored in the
|
|
1968
2090
|
// meta table so we skip even when no channels/cron exist to produce rows.
|
|
1969
2091
|
const db = getDB();
|
|
@@ -1977,8 +2099,13 @@ async function scanOpenClawIncremental() {
|
|
|
1977
2099
|
const scan = { fileMtimeMs: now, fileSize: 0 };
|
|
1978
2100
|
const entries = [];
|
|
1979
2101
|
try {
|
|
1980
|
-
|
|
2102
|
+
// On Windows `openclaw` resolves to a .cmd/.ps1 shim that execFile can't launch
|
|
2103
|
+
// directly; execFileShellSpec composes a shell-safe invocation there and is a
|
|
2104
|
+
// no-op passthrough on POSIX (RUSH-2286).
|
|
2105
|
+
const channelsSpec = execFileShellSpec('openclaw', ['channels', 'status']);
|
|
2106
|
+
const { stdout: output } = await execFileAsync(channelsSpec.command, channelsSpec.args, {
|
|
1981
2107
|
encoding: 'utf-8',
|
|
2108
|
+
shell: channelsSpec.shell,
|
|
1982
2109
|
});
|
|
1983
2110
|
for (const line of output.split('\n')) {
|
|
1984
2111
|
const match = line.match(/^-\s+\w+\s+(\S+)\s+\((\w+)\):\s*(.+)/);
|
|
@@ -2007,8 +2134,10 @@ async function scanOpenClawIncremental() {
|
|
|
2007
2134
|
/* channels command failed */
|
|
2008
2135
|
}
|
|
2009
2136
|
try {
|
|
2010
|
-
const
|
|
2137
|
+
const cronSpec = execFileShellSpec('openclaw', ['cron', 'list']);
|
|
2138
|
+
const { stdout: output } = await execFileAsync(cronSpec.command, cronSpec.args, {
|
|
2011
2139
|
encoding: 'utf-8',
|
|
2140
|
+
shell: cronSpec.shell,
|
|
2012
2141
|
});
|
|
2013
2142
|
const lines = output.split('\n');
|
|
2014
2143
|
for (let i = 1; i < lines.length; i++) {
|
|
@@ -2514,15 +2643,19 @@ async function readDroidMeta(filePath, currentVersion) {
|
|
|
2514
2643
|
const settings = readDroidSettings(filePath.replace(/\.jsonl$/, '.settings.json'));
|
|
2515
2644
|
const model = settings.model || scan.model;
|
|
2516
2645
|
const tokenCount = settings.tokenCount;
|
|
2517
|
-
|
|
2518
|
-
|
|
2646
|
+
// Droid records a full split (input / cache-read / cache-write / output) in its
|
|
2647
|
+
// settings sidecar, so both the actual and no-cache cost are derivable (RUSH-2287).
|
|
2648
|
+
const usageForCost = model && settings.usage
|
|
2649
|
+
? {
|
|
2519
2650
|
model,
|
|
2520
2651
|
inputTokens: settings.usage.inputTokens,
|
|
2521
2652
|
outputTokens: settings.usage.outputTokens,
|
|
2522
2653
|
cacheReadTokens: settings.usage.cacheReadTokens,
|
|
2523
2654
|
cacheCreationTokens: settings.usage.cacheCreationTokens,
|
|
2524
|
-
}
|
|
2525
|
-
:
|
|
2655
|
+
}
|
|
2656
|
+
: undefined;
|
|
2657
|
+
const costUsd = usageForCost ? costOfUsage(usageForCost) : 0;
|
|
2658
|
+
const costUsdNoCache = usageForCost ? costOfUsageNoCache(usageForCost) : 0;
|
|
2526
2659
|
const stat = safeStatSync(filePath);
|
|
2527
2660
|
const cwd = normalizeCwd(scan.cwd || '');
|
|
2528
2661
|
const meta = {
|
|
@@ -2540,7 +2673,11 @@ async function readDroidMeta(filePath, currentVersion) {
|
|
|
2540
2673
|
messageCount: scan.messageCount,
|
|
2541
2674
|
tokenCount,
|
|
2542
2675
|
outputTokens: settings.usage?.outputTokens,
|
|
2676
|
+
inputTokens: settings.usage?.inputTokens,
|
|
2677
|
+
cacheReadTokens: settings.usage?.cacheReadTokens,
|
|
2678
|
+
cacheWriteTokens: settings.usage?.cacheCreationTokens,
|
|
2543
2679
|
costUsd: costUsd > 0 ? costUsd : undefined,
|
|
2680
|
+
costUsdNoCache: costUsd > 0 ? costUsdNoCache : undefined,
|
|
2544
2681
|
durationMs: scan.durationMs,
|
|
2545
2682
|
};
|
|
2546
2683
|
return { meta, content: scan.contentText || '' };
|
|
@@ -2688,8 +2825,12 @@ export function initClaudeParseState() {
|
|
|
2688
2825
|
toolCallCount: 0,
|
|
2689
2826
|
tokenCount: 0,
|
|
2690
2827
|
outputTokens: 0,
|
|
2828
|
+
inputTokens: 0,
|
|
2829
|
+
cacheReadTokens: 0,
|
|
2830
|
+
cacheWriteTokens: 0,
|
|
2691
2831
|
sawTokenCount: false,
|
|
2692
2832
|
costUsd: 0,
|
|
2833
|
+
costUsdNoCache: 0,
|
|
2693
2834
|
sawCost: false,
|
|
2694
2835
|
firstTsMs: undefined,
|
|
2695
2836
|
lastTsMs: undefined,
|
|
@@ -2886,21 +3027,34 @@ export function applyClaudeLine(state, parsed) {
|
|
|
2886
3027
|
}
|
|
2887
3028
|
if (typeof usageObj?.output_tokens === 'number')
|
|
2888
3029
|
state.outputTokens += usageObj.output_tokens;
|
|
3030
|
+
// Burn split (RUSH-2287): accumulate the raw directional counts so `agents
|
|
3031
|
+
// output` can report uncached-input / cache-read / cache-write separately.
|
|
3032
|
+
if (usageObj && typeof usageObj === 'object') {
|
|
3033
|
+
if (typeof usageObj.input_tokens === 'number')
|
|
3034
|
+
state.inputTokens += usageObj.input_tokens;
|
|
3035
|
+
if (typeof usageObj.cache_read_input_tokens === 'number')
|
|
3036
|
+
state.cacheReadTokens += usageObj.cache_read_input_tokens;
|
|
3037
|
+
if (typeof usageObj.cache_creation_input_tokens === 'number')
|
|
3038
|
+
state.cacheWriteTokens += usageObj.cache_creation_input_tokens;
|
|
3039
|
+
}
|
|
2889
3040
|
// Per-assistant-message cost: each event carries its own model, so we
|
|
2890
3041
|
// multiply that event's raw token directions by that model's price.
|
|
2891
3042
|
const model = parsed.message?.model;
|
|
2892
3043
|
if (typeof model === 'string' && model)
|
|
2893
3044
|
state.model = model;
|
|
2894
3045
|
if (model && usageObj && typeof usageObj === 'object') {
|
|
2895
|
-
const
|
|
3046
|
+
const usageForCost = {
|
|
2896
3047
|
model,
|
|
2897
3048
|
inputTokens: usageObj.input_tokens,
|
|
2898
3049
|
outputTokens: usageObj.output_tokens,
|
|
2899
3050
|
cacheReadTokens: usageObj.cache_read_input_tokens,
|
|
2900
3051
|
cacheCreationTokens: usageObj.cache_creation_input_tokens,
|
|
2901
|
-
}
|
|
3052
|
+
};
|
|
3053
|
+
const eventCost = costOfUsage(usageForCost);
|
|
2902
3054
|
if (eventCost > 0) {
|
|
2903
3055
|
state.costUsd += eventCost;
|
|
3056
|
+
// No-cache scenario: reprice this same event's cache tokens at the input rate.
|
|
3057
|
+
state.costUsdNoCache += costOfUsageNoCache(usageForCost);
|
|
2904
3058
|
state.sawCost = true;
|
|
2905
3059
|
}
|
|
2906
3060
|
}
|
|
@@ -2930,7 +3084,11 @@ export function finalizeClaudeScan(state) {
|
|
|
2930
3084
|
toolCallCount: state.toolCallCount,
|
|
2931
3085
|
tokenCount: state.sawTokenCount ? state.tokenCount : undefined,
|
|
2932
3086
|
outputTokens: state.sawTokenCount ? state.outputTokens : undefined,
|
|
3087
|
+
inputTokens: state.sawTokenCount ? state.inputTokens : undefined,
|
|
3088
|
+
cacheReadTokens: state.sawTokenCount ? state.cacheReadTokens : undefined,
|
|
3089
|
+
cacheWriteTokens: state.sawTokenCount ? state.cacheWriteTokens : undefined,
|
|
2933
3090
|
costUsd: state.sawCost ? state.costUsd : undefined,
|
|
3091
|
+
costUsdNoCache: state.sawCost ? state.costUsdNoCache : undefined,
|
|
2934
3092
|
durationMs,
|
|
2935
3093
|
lastActivity: state.lastTsMs !== undefined ? new Date(state.lastTsMs).toISOString() : undefined,
|
|
2936
3094
|
contentText: state.userTexts.length > 0 ? state.userTexts.join('\n') : undefined,
|
|
@@ -2986,7 +3144,7 @@ export function serializeClaudeParserState(state, offset, jsonlDroppingOversized
|
|
|
2986
3144
|
: allIds;
|
|
2987
3145
|
const ticket = detectTicket(state.userTexts.join('\n') || undefined, state.gitBranch);
|
|
2988
3146
|
return {
|
|
2989
|
-
v:
|
|
3147
|
+
v: 3,
|
|
2990
3148
|
offset,
|
|
2991
3149
|
jsonlDroppingOversizedLine: jsonlDroppingOversizedLine || undefined,
|
|
2992
3150
|
timestamp: state.timestamp,
|
|
@@ -3005,9 +3163,13 @@ export function serializeClaudeParserState(state, offset, jsonlDroppingOversized
|
|
|
3005
3163
|
toolCallCount: state.toolCallCount,
|
|
3006
3164
|
tokenCount: state.tokenCount,
|
|
3007
3165
|
outputTokens: state.outputTokens,
|
|
3166
|
+
inputTokens: state.inputTokens,
|
|
3167
|
+
cacheReadTokens: state.cacheReadTokens,
|
|
3168
|
+
cacheWriteTokens: state.cacheWriteTokens,
|
|
3008
3169
|
sawTokenCount: state.sawTokenCount,
|
|
3009
3170
|
sawCost: state.sawCost,
|
|
3010
3171
|
costUsd: state.costUsd,
|
|
3172
|
+
costUsdNoCache: state.costUsdNoCache,
|
|
3011
3173
|
seenIdsSize: state.seenAssistantIds.size,
|
|
3012
3174
|
seenIdsRecent,
|
|
3013
3175
|
sawPrCreate: state.sawPrCreate,
|
|
@@ -3067,8 +3229,12 @@ export function hydrateClaudeParseState(prior) {
|
|
|
3067
3229
|
toolCallCount: prior.toolCallCount ?? 0,
|
|
3068
3230
|
tokenCount: prior.tokenCount,
|
|
3069
3231
|
outputTokens: prior.outputTokens,
|
|
3232
|
+
inputTokens: prior.inputTokens ?? 0,
|
|
3233
|
+
cacheReadTokens: prior.cacheReadTokens ?? 0,
|
|
3234
|
+
cacheWriteTokens: prior.cacheWriteTokens ?? 0,
|
|
3070
3235
|
sawTokenCount: prior.sawTokenCount,
|
|
3071
3236
|
costUsd: prior.costUsd,
|
|
3237
|
+
costUsdNoCache: prior.costUsdNoCache ?? 0,
|
|
3072
3238
|
sawCost: prior.sawCost,
|
|
3073
3239
|
firstTsMs: prior.firstTsMs,
|
|
3074
3240
|
lastTsMs: prior.lastTsMs,
|
|
@@ -3217,7 +3383,7 @@ function parsePriorClaudeState(row) {
|
|
|
3217
3383
|
return null;
|
|
3218
3384
|
try {
|
|
3219
3385
|
const parsed = JSON.parse(row.parserState);
|
|
3220
|
-
if (parsed?.v !==
|
|
3386
|
+
if (parsed?.v !== 3 || typeof parsed.offset !== 'number' || parsed.toolCalls?.v !== 1)
|
|
3221
3387
|
return null;
|
|
3222
3388
|
return parsed;
|
|
3223
3389
|
}
|
|
@@ -3373,17 +3539,34 @@ export function applyCodexLine(state, parsed) {
|
|
|
3373
3539
|
* exact return-building {@link scanCodexSession} used to run inline.
|
|
3374
3540
|
*/
|
|
3375
3541
|
export function finalizeCodexScan(state) {
|
|
3376
|
-
//
|
|
3542
|
+
// Codex reports one cumulative snapshot: uncached input, cached (cache-read)
|
|
3543
|
+
// input, and output+reasoning. It has no cache-write bucket. Derive the burn
|
|
3544
|
+
// split and both costs (actual + no-cache) from that final snapshot (RUSH-2287).
|
|
3545
|
+
const snap = state.lastTotalTokenUsage;
|
|
3546
|
+
const outputTokens = snap
|
|
3547
|
+
? (snap.output_tokens ?? 0) + (snap.reasoning_output_tokens ?? 0)
|
|
3548
|
+
: undefined;
|
|
3377
3549
|
let costUsd;
|
|
3378
|
-
|
|
3379
|
-
|
|
3550
|
+
let costUsdNoCache;
|
|
3551
|
+
let inputTokens;
|
|
3552
|
+
let cacheReadTokens;
|
|
3553
|
+
if (snap) {
|
|
3554
|
+
inputTokens = typeof snap.input_tokens === 'number' ? snap.input_tokens : undefined;
|
|
3555
|
+
cacheReadTokens = typeof snap.cached_input_tokens === 'number' ? snap.cached_input_tokens : undefined;
|
|
3556
|
+
}
|
|
3557
|
+
// Price the final cumulative token snapshot once, against the session model.
|
|
3558
|
+
if (state.model && snap) {
|
|
3559
|
+
const usage = {
|
|
3380
3560
|
model: state.model,
|
|
3381
|
-
inputTokens:
|
|
3382
|
-
outputTokens
|
|
3383
|
-
cacheReadTokens:
|
|
3384
|
-
}
|
|
3385
|
-
|
|
3561
|
+
inputTokens: snap.input_tokens,
|
|
3562
|
+
outputTokens,
|
|
3563
|
+
cacheReadTokens: snap.cached_input_tokens,
|
|
3564
|
+
};
|
|
3565
|
+
const c = costOfUsage(usage);
|
|
3566
|
+
if (c > 0) {
|
|
3386
3567
|
costUsd = c;
|
|
3568
|
+
costUsdNoCache = costOfUsageNoCache(usage);
|
|
3569
|
+
}
|
|
3387
3570
|
}
|
|
3388
3571
|
const durationMs = state.firstTsMs !== undefined && state.lastTsMs !== undefined && state.lastTsMs > state.firstTsMs
|
|
3389
3572
|
? state.lastTsMs - state.firstTsMs
|
|
@@ -3400,10 +3583,12 @@ export function finalizeCodexScan(state) {
|
|
|
3400
3583
|
topic: state.topic,
|
|
3401
3584
|
messageCount: state.messageCount,
|
|
3402
3585
|
tokenCount: state.tokenCount,
|
|
3403
|
-
outputTokens
|
|
3404
|
-
|
|
3405
|
-
|
|
3586
|
+
outputTokens,
|
|
3587
|
+
inputTokens,
|
|
3588
|
+
cacheReadTokens,
|
|
3589
|
+
cacheWriteTokens: undefined,
|
|
3406
3590
|
costUsd,
|
|
3591
|
+
costUsdNoCache,
|
|
3407
3592
|
durationMs,
|
|
3408
3593
|
lastActivity: state.lastTsMs !== undefined ? new Date(state.lastTsMs).toISOString() : undefined,
|
|
3409
3594
|
contentText: state.userTexts.length > 0 ? state.userTexts.join('\n') : undefined,
|
|
@@ -3902,7 +4087,7 @@ async function scanCursorIncremental(onProgress) {
|
|
|
3902
4087
|
const result = readCursorMeta(filePath, currentVersion);
|
|
3903
4088
|
if (result && !seen.has(result.meta.id)) {
|
|
3904
4089
|
seen.add(result.meta.id);
|
|
3905
|
-
entries.push({ meta: result.meta, content: result.content, scan });
|
|
4090
|
+
entries.push({ meta: result.meta, content: result.content, scan, events: result.events });
|
|
3906
4091
|
}
|
|
3907
4092
|
else {
|
|
3908
4093
|
touched.push({ filePath, scan });
|
|
@@ -3998,7 +4183,7 @@ export function readCursorMeta(filePath, currentVersion) {
|
|
|
3998
4183
|
messageCount: events.filter((event) => event.type === 'message').length,
|
|
3999
4184
|
todos: extractTodoProgressFromEvents(events),
|
|
4000
4185
|
};
|
|
4001
|
-
return { meta, content: userTexts.join('\n') };
|
|
4186
|
+
return { meta, content: userTexts.join('\n'), events };
|
|
4002
4187
|
}
|
|
4003
4188
|
// ---------------------------------------------------------------------------
|
|
4004
4189
|
// Kimi
|
|
@@ -4361,10 +4546,13 @@ export function readGrokMeta(filePath, currentVersion) {
|
|
|
4361
4546
|
? summary.num_messages
|
|
4362
4547
|
: undefined;
|
|
4363
4548
|
// Grok records its managed home in summary.grok_home
|
|
4364
|
-
// (…/versions/grok/<version>/home/.grok) — recover the version from it.
|
|
4549
|
+
// (…/versions/grok/<version>/home/.grok) — recover the version from it. The
|
|
4550
|
+
// value is written by the Grok CLI in the writing host's native separators, so
|
|
4551
|
+
// a Windows-authored summary is backslash-separated; normalize to `/` before
|
|
4552
|
+
// matching or the version never resolves on Windows (RUSH-2286).
|
|
4365
4553
|
let embeddedVersion;
|
|
4366
4554
|
if (typeof summary?.grok_home === 'string') {
|
|
4367
|
-
embeddedVersion = summary.grok_home.match(/versions\/grok\/([^/]+)\//)?.[1];
|
|
4555
|
+
embeddedVersion = summary.grok_home.replace(/\\/g, '/').match(/versions\/grok\/([^/]+)\//)?.[1];
|
|
4368
4556
|
}
|
|
4369
4557
|
const meta = {
|
|
4370
4558
|
id: sessionId,
|