@phnx-labs/agents-cli 1.22.29 → 1.22.30
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 +82 -0
- package/README.md +39 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/accounts.d.ts +13 -0
- package/dist/commands/accounts.js +32 -0
- package/dist/commands/daemon.d.ts +18 -0
- package/dist/commands/daemon.js +581 -0
- package/dist/commands/exec.js +66 -20
- package/dist/commands/routines.js +29 -11
- package/dist/commands/secrets.d.ts +17 -0
- package/dist/commands/secrets.js +30 -15
- package/dist/commands/sessions-browser.js +6 -6
- package/dist/commands/sessions-favorite.d.ts +7 -7
- package/dist/commands/sessions-favorite.js +30 -30
- package/dist/commands/sessions-picker.d.ts +33 -1
- package/dist/commands/sessions-picker.js +102 -27
- package/dist/commands/sessions.d.ts +12 -1
- package/dist/commands/sessions.js +259 -20
- package/dist/commands/view.d.ts +11 -0
- package/dist/commands/view.js +56 -29
- package/dist/index.js +37 -2
- package/dist/lib/account-labels.d.ts +24 -0
- package/dist/lib/account-labels.js +72 -0
- package/dist/lib/agents.d.ts +32 -1
- package/dist/lib/agents.js +96 -31
- package/dist/lib/daemon-health.d.ts +24 -0
- package/dist/lib/daemon-health.js +84 -0
- package/dist/lib/daemon-ticks.d.ts +81 -0
- package/dist/lib/daemon-ticks.js +190 -0
- package/dist/lib/daemon.d.ts +68 -18
- package/dist/lib/daemon.js +303 -338
- package/dist/lib/device-config.d.ts +10 -0
- package/dist/lib/device-config.js +27 -0
- package/dist/lib/exec.d.ts +27 -0
- package/dist/lib/exec.js +49 -2
- package/dist/lib/hosts/dispatch.d.ts +4 -0
- package/dist/lib/hosts/dispatch.js +4 -0
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/hosts/run-target.d.ts +1 -0
- package/dist/lib/hosts/run-target.js +1 -0
- package/dist/lib/import.js +7 -6
- package/dist/lib/memory-cache.d.ts +19 -0
- package/dist/lib/memory-cache.js +31 -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/migrate.d.ts +1 -1
- package/dist/lib/migrate.js +7 -2
- package/dist/lib/routine-activation.d.ts +2 -0
- package/dist/lib/routine-activation.js +16 -0
- package/dist/lib/runner.d.ts +18 -0
- package/dist/lib/runner.js +52 -0
- 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.d.ts +19 -0
- package/dist/lib/secrets/agent.js +32 -2
- package/dist/lib/secrets/scope.d.ts +3 -3
- package/dist/lib/secrets/scope.js +3 -3
- package/dist/lib/session/db.d.ts +15 -0
- package/dist/lib/session/db.js +90 -15
- package/dist/lib/session/discover.js +91 -39
- package/dist/lib/session/favorites.d.ts +2 -2
- package/dist/lib/session/favorites.js +2 -2
- package/dist/lib/session/parse.d.ts +63 -0
- package/dist/lib/session/parse.js +165 -20
- package/dist/lib/session/session-cache.d.ts +9 -6
- package/dist/lib/session/session-cache.js +23 -6
- package/dist/lib/shims.js +12 -0
- package/dist/lib/startup/command-registry.d.ts +15 -1
- package/dist/lib/startup/command-registry.js +49 -0
- package/dist/lib/usage-refresh.js +3 -2
- package/dist/lib/usage.d.ts +12 -10
- package/dist/lib/usage.js +63 -144
- package/package.json +4 -1
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -28,6 +28,7 @@ export declare const RESOURCE_INDEX_VERSION = 1;
|
|
|
28
28
|
*/
|
|
29
29
|
/** Bump when facet extraction changes so cached rows recompute (stalls-by-model v6). */
|
|
30
30
|
export declare const INSIGHTS_EXTRACTOR_VERSION = 6;
|
|
31
|
+
export declare const PREVIEW_EXTRACTOR_VERSION = 1;
|
|
31
32
|
/** Raw row shape returned from the sessions table. */
|
|
32
33
|
export interface SessionRow {
|
|
33
34
|
id: string;
|
|
@@ -390,6 +391,20 @@ export declare function writeSessionInsights<T>(entries: Array<{
|
|
|
390
391
|
}>): void;
|
|
391
392
|
/** Drop every cached facet row. Backs `agents insights --refresh`. */
|
|
392
393
|
export declare function clearSessionInsights(): void;
|
|
394
|
+
/** Read one derived preview only when it matches the transcript bytes on disk. */
|
|
395
|
+
export declare function readSessionPreviewCache<T>(id: string, sourceStamp: {
|
|
396
|
+
fileMtimeMs: number | null;
|
|
397
|
+
fileSize: number | null;
|
|
398
|
+
}): T | undefined;
|
|
399
|
+
/** Persist normalized preview data against the exact transcript bytes parsed. */
|
|
400
|
+
export declare function writeSessionPreviewCache<T>(entry: {
|
|
401
|
+
id: string;
|
|
402
|
+
fileMtimeMs: number | null;
|
|
403
|
+
fileSize: number | null;
|
|
404
|
+
preview: T;
|
|
405
|
+
}): void;
|
|
406
|
+
/** Plugin provenance already indexed for resources used by one session. */
|
|
407
|
+
export declare function getSessionPlugins(id: string): string[];
|
|
393
408
|
export type UsageRollupGroup = 'agent' | 'project' | 'day' | 'account';
|
|
394
409
|
/**
|
|
395
410
|
* Smart-launch affinity priors: group sessions by origin machine, harness, or
|
package/dist/lib/session/db.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import Database from '../sqlite.js';
|
|
12
|
-
import { parseSession } from './parse.js';
|
|
12
|
+
import { parseSession, sessionFilePathContainer } from './parse.js';
|
|
13
13
|
import { extractRecentDirectoriesTouched, extractTodoProgressFromEvents } from './state.js';
|
|
14
14
|
import { getSessionsDir, getSessionsDbPath } from '../state.js';
|
|
15
15
|
import { query as queryEvents, queryToolUsageForSessions } from '../events.js';
|
|
@@ -305,6 +305,18 @@ CREATE TABLE IF NOT EXISTS session_insights (
|
|
|
305
305
|
computed_at INTEGER NOT NULL,
|
|
306
306
|
facets TEXT NOT NULL
|
|
307
307
|
);
|
|
308
|
+
|
|
309
|
+
-- Normalized data behind sessions preview. Like session_insights this is a
|
|
310
|
+
-- lazy, stamp-validated cache: opening one session parses only that transcript,
|
|
311
|
+
-- while subsequent processes reuse the derived preview until its bytes change.
|
|
312
|
+
CREATE TABLE IF NOT EXISTS session_preview_cache (
|
|
313
|
+
session_id TEXT PRIMARY KEY,
|
|
314
|
+
file_mtime_ms INTEGER,
|
|
315
|
+
file_size INTEGER,
|
|
316
|
+
extractor_version INTEGER NOT NULL,
|
|
317
|
+
computed_at INTEGER NOT NULL,
|
|
318
|
+
preview_json TEXT NOT NULL
|
|
319
|
+
);
|
|
308
320
|
`;
|
|
309
321
|
/**
|
|
310
322
|
* Bumping this invalidates every cached facet row without touching the schema
|
|
@@ -314,6 +326,7 @@ CREATE TABLE IF NOT EXISTS session_insights (
|
|
|
314
326
|
*/
|
|
315
327
|
/** Bump when facet extraction changes so cached rows recompute (stalls-by-model v6). */
|
|
316
328
|
export const INSIGHTS_EXTRACTOR_VERSION = 6;
|
|
329
|
+
export const PREVIEW_EXTRACTOR_VERSION = 1;
|
|
317
330
|
let dbInstance = null;
|
|
318
331
|
/**
|
|
319
332
|
* Apply schema migrations from `fromVersion` → SCHEMA_VERSION. The new
|
|
@@ -2291,18 +2304,36 @@ function clearSessionExistenceCache() {
|
|
|
2291
2304
|
directoryMembershipSweepCount = 0;
|
|
2292
2305
|
}
|
|
2293
2306
|
function findMissingFilePaths(filePaths) {
|
|
2307
|
+
// Existence is decided on the CONTAINER file, never the raw stored path. A
|
|
2308
|
+
// composite `file_path` (`<container>#<id>`, e.g. OpenCode's `opencode.db#ses_…`)
|
|
2309
|
+
// names a row INSIDE a shared file — its basename is never a directory entry,
|
|
2310
|
+
// so a dirname/basename membership check on the composite string classified
|
|
2311
|
+
// every such row as deleted and pruned it (RUSH-2357). Group the original
|
|
2312
|
+
// paths under the container we actually stat, and map the verdict back so the
|
|
2313
|
+
// returned set still holds the original `file_path` strings the caller keys on.
|
|
2294
2314
|
const byDir = new Map();
|
|
2295
2315
|
for (const p of filePaths) {
|
|
2296
|
-
const
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2316
|
+
const container = sessionFilePathContainer(p);
|
|
2317
|
+
const dir = path.dirname(container);
|
|
2318
|
+
const base = path.basename(container);
|
|
2319
|
+
let bases = byDir.get(dir);
|
|
2320
|
+
if (!bases) {
|
|
2321
|
+
bases = new Map();
|
|
2322
|
+
byDir.set(dir, bases);
|
|
2301
2323
|
}
|
|
2302
|
-
|
|
2324
|
+
let originals = bases.get(base);
|
|
2325
|
+
if (!originals) {
|
|
2326
|
+
originals = [];
|
|
2327
|
+
bases.set(base, originals);
|
|
2328
|
+
}
|
|
2329
|
+
originals.push(p);
|
|
2303
2330
|
}
|
|
2304
2331
|
const missing = new Set();
|
|
2305
|
-
|
|
2332
|
+
const markMissing = (originals) => {
|
|
2333
|
+
for (const original of originals)
|
|
2334
|
+
missing.add(original);
|
|
2335
|
+
};
|
|
2336
|
+
for (const [dir, bases] of byDir) {
|
|
2306
2337
|
let entries;
|
|
2307
2338
|
try {
|
|
2308
2339
|
const stat = fs.statSync(dir);
|
|
@@ -2324,17 +2355,16 @@ function findMissingFilePaths(filePaths) {
|
|
|
2324
2355
|
// Directory itself is gone (or unreadable) — every file in it is missing.
|
|
2325
2356
|
// Also covers the race where readdir loses to a concurrent delete: fall
|
|
2326
2357
|
// back to a direct stat rather than assuming existence.
|
|
2327
|
-
for (const base of
|
|
2358
|
+
for (const [base, originals] of bases) {
|
|
2328
2359
|
const filePath = path.join(dir, base);
|
|
2329
2360
|
if (!fs.existsSync(filePath))
|
|
2330
|
-
|
|
2361
|
+
markMissing(originals);
|
|
2331
2362
|
}
|
|
2332
2363
|
continue;
|
|
2333
2364
|
}
|
|
2334
|
-
for (const base of
|
|
2335
|
-
const filePath = path.join(dir, base);
|
|
2365
|
+
for (const [base, originals] of bases) {
|
|
2336
2366
|
if (!entries.has(base))
|
|
2337
|
-
|
|
2367
|
+
markMissing(originals);
|
|
2338
2368
|
}
|
|
2339
2369
|
}
|
|
2340
2370
|
return missing;
|
|
@@ -2471,6 +2501,49 @@ export function writeSessionInsights(entries) {
|
|
|
2471
2501
|
export function clearSessionInsights() {
|
|
2472
2502
|
getDB().exec(`DELETE FROM session_insights`);
|
|
2473
2503
|
}
|
|
2504
|
+
/** Read one derived preview only when it matches the transcript bytes on disk. */
|
|
2505
|
+
export function readSessionPreviewCache(id, sourceStamp) {
|
|
2506
|
+
const row = getDB().prepare(`
|
|
2507
|
+
SELECT pc.preview_json AS previewJson
|
|
2508
|
+
FROM session_preview_cache pc
|
|
2509
|
+
WHERE pc.session_id = ?
|
|
2510
|
+
AND pc.extractor_version = ?
|
|
2511
|
+
AND pc.file_mtime_ms IS ?
|
|
2512
|
+
AND pc.file_size IS ?
|
|
2513
|
+
`).get(id, PREVIEW_EXTRACTOR_VERSION, sourceStamp.fileMtimeMs, sourceStamp.fileSize);
|
|
2514
|
+
if (!row)
|
|
2515
|
+
return undefined;
|
|
2516
|
+
try {
|
|
2517
|
+
return JSON.parse(row.previewJson);
|
|
2518
|
+
}
|
|
2519
|
+
catch {
|
|
2520
|
+
return undefined;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
/** Persist normalized preview data against the exact transcript bytes parsed. */
|
|
2524
|
+
export function writeSessionPreviewCache(entry) {
|
|
2525
|
+
getDB().prepare(`
|
|
2526
|
+
INSERT INTO session_preview_cache
|
|
2527
|
+
(session_id, file_mtime_ms, file_size, extractor_version, computed_at, preview_json)
|
|
2528
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
2529
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
2530
|
+
file_mtime_ms = excluded.file_mtime_ms,
|
|
2531
|
+
file_size = excluded.file_size,
|
|
2532
|
+
extractor_version = excluded.extractor_version,
|
|
2533
|
+
computed_at = excluded.computed_at,
|
|
2534
|
+
preview_json = excluded.preview_json
|
|
2535
|
+
`).run(entry.id, entry.fileMtimeMs, entry.fileSize, PREVIEW_EXTRACTOR_VERSION, Date.now(), JSON.stringify(entry.preview));
|
|
2536
|
+
}
|
|
2537
|
+
/** Plugin provenance already indexed for resources used by one session. */
|
|
2538
|
+
export function getSessionPlugins(id) {
|
|
2539
|
+
const rows = getDB().prepare(`
|
|
2540
|
+
SELECT DISTINCT plugin
|
|
2541
|
+
FROM session_resource_usage
|
|
2542
|
+
WHERE session_id = ? AND plugin IS NOT NULL AND plugin <> ''
|
|
2543
|
+
ORDER BY plugin COLLATE NOCASE
|
|
2544
|
+
`).all(id);
|
|
2545
|
+
return rows.map(row => row.plugin);
|
|
2546
|
+
}
|
|
2474
2547
|
export function queryAffinityRollup(options) {
|
|
2475
2548
|
const db = getDB();
|
|
2476
2549
|
const where = [];
|
|
@@ -2708,7 +2781,9 @@ export function backfillResourceUsage(filter = {}, onProgress) {
|
|
|
2708
2781
|
result.scanned++;
|
|
2709
2782
|
let stamp;
|
|
2710
2783
|
try {
|
|
2711
|
-
|
|
2784
|
+
// Composite rows (`<container>#<id>`) stat their container file — the
|
|
2785
|
+
// per-session bytes/mtime stamp is derived during discovery, not here.
|
|
2786
|
+
const st = fs.statSync(sessionFilePathContainer(meta.filePath));
|
|
2712
2787
|
stamp = { fileMtimeMs: st.mtimeMs, fileSize: st.size };
|
|
2713
2788
|
}
|
|
2714
2789
|
catch {
|
|
@@ -2787,7 +2862,7 @@ export function topSessionsByCost(n, options = {}) {
|
|
|
2787
2862
|
// Over-fetch a small buffer to survive the on-disk liveness filter below.
|
|
2788
2863
|
const sql = `SELECT * FROM sessions ${whereCost} ORDER BY cost_usd DESC, timestamp DESC LIMIT ${limit + 16}`;
|
|
2789
2864
|
const rows = db.prepare(sql).all(...params);
|
|
2790
|
-
const live = rows.filter(r => !r.file_path || fs.existsSync(r.file_path));
|
|
2865
|
+
const live = rows.filter(r => !r.file_path || fs.existsSync(sessionFilePathContainer(r.file_path)));
|
|
2791
2866
|
return live.slice(0, limit).map(r => ({
|
|
2792
2867
|
meta: rowToMeta(r),
|
|
2793
2868
|
costUsd: r.cost_usd ?? 0,
|
|
@@ -18,7 +18,7 @@ import { getAgentsDir, getUserAgentsDir, getHistoryDir, getRunsDir } from '../st
|
|
|
18
18
|
import { shortCodexHome } from '../codex-home.js';
|
|
19
19
|
import { parseTimeFilter } from './relative-time.js';
|
|
20
20
|
const execFileAsync = promisify(execFile);
|
|
21
|
-
import { AGENTS, agentConfigDirName, getCliVersion } from '../agents.js';
|
|
21
|
+
import { AGENTS, agentConfigDirName, getCliVersion, resolveOpenCodeAccountId } from '../agents.js';
|
|
22
22
|
import { walkForFilesWithStat } from '../fs-walk.js';
|
|
23
23
|
import { hasCommand } from '../cli-resources.js';
|
|
24
24
|
import { execFileShellSpec } from '../platform/exec.js';
|
|
@@ -28,7 +28,7 @@ import { deriveShortId } from './short-id.js';
|
|
|
28
28
|
import { buildClaudeAccountIndex, resolveClaudeAccount } from './claude-accounts.js';
|
|
29
29
|
import { extractSessionTopic, extractSlashCommandName, extractSlashCommandFromToolInput } from './prompt.js';
|
|
30
30
|
import { isSkillInvocation, extractSkills, extractSlashCommands } from './highlights.js';
|
|
31
|
-
import { parseAntigravity, parseCursor } from './parse.js';
|
|
31
|
+
import { parseAntigravity, parseCursor, splitSessionFilePath } from './parse.js';
|
|
32
32
|
import { extractPrUrl, detectWorktree, detectTicket, isPrCreateCommand, detectSpawnedTeam, isTicketCreateTool, extractCreatedTicket, extractRecentDirectoriesTouched, extractTodoProgressFromEvents } from './state.js';
|
|
33
33
|
import { costOfUsage, costOfUsageNoCache } from '../pricing/index.js';
|
|
34
34
|
import { machineForSessionFile } from './origin-machine.js';
|
|
@@ -335,6 +335,15 @@ export function isManagedSessionFile(filePath) {
|
|
|
335
335
|
// someone's dotfile dir, so scoping must not silently swallow them.
|
|
336
336
|
if (!filePath || !path.isAbsolute(filePath))
|
|
337
337
|
return true;
|
|
338
|
+
// A composite file_path (`<container>#<id>`) names a row inside a single shared
|
|
339
|
+
// DB the scanner reads from one fixed location (OpenCode's `opencode.db`) — that
|
|
340
|
+
// store is never a per-install dotfile under a version home, so the managed-vs-
|
|
341
|
+
// unmanaged split does not apply: there is exactly one store, not a "your own"
|
|
342
|
+
// copy to hide. Classifying it as unmanaged hid every OpenCode row from default
|
|
343
|
+
// listings once any agent was managed (RUSH-2357). Keyed off the composite FORM,
|
|
344
|
+
// so any future single-DB harness inherits this.
|
|
345
|
+
if (splitSessionFilePath(filePath).fragment !== undefined)
|
|
346
|
+
return true;
|
|
338
347
|
const roots = [
|
|
339
348
|
...VERSIONS_ROOTS.map((root) => path.join(root, 'versions')),
|
|
340
349
|
path.join(getHistoryDir(), 'backups'),
|
|
@@ -1838,39 +1847,24 @@ function readAntigravityMeta(filePath, currentVersion) {
|
|
|
1838
1847
|
const OPENCODE_DB = path.join(HOME, '.local', 'share', 'opencode', 'opencode.db');
|
|
1839
1848
|
let cachedOpenCodeAccount;
|
|
1840
1849
|
/**
|
|
1841
|
-
*
|
|
1850
|
+
* The active OpenCode account: the provider ids with a valid credential in
|
|
1851
|
+
* `auth.json`, joined (e.g. `"anthropic+muse-spark"`) — see
|
|
1852
|
+
* `resolveOpenCodeAccountId` in `../agents.js`, the single source of truth
|
|
1853
|
+
* `agents view`/`agents doctor` also read.
|
|
1842
1854
|
*
|
|
1843
|
-
*
|
|
1844
|
-
*
|
|
1855
|
+
* OpenCode's `opencode.db` also carries `account`/`account_state`/
|
|
1856
|
+
* `control_account` tables that look like the obvious source, but on a real,
|
|
1857
|
+
* actively-used install (yosemite-s1, 1.16.0, 35 applied migrations) all
|
|
1858
|
+
* three are permanently empty — no migration populates them and no session
|
|
1859
|
+
* has ever written a row. Querying them (the pre-fix behavior here) always
|
|
1860
|
+
* returned undefined, credential or not; `auth.json` is what OpenCode
|
|
1861
|
+
* actually reads at runtime.
|
|
1845
1862
|
*/
|
|
1846
|
-
function getOpenCodeAccount(
|
|
1863
|
+
function getOpenCodeAccount() {
|
|
1847
1864
|
if (cachedOpenCodeAccount !== undefined)
|
|
1848
1865
|
return cachedOpenCodeAccount || undefined;
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
let owned;
|
|
1852
|
-
try {
|
|
1853
|
-
const db = handle ?? (fs.existsSync(OPENCODE_DB) ? (owned = new Database(OPENCODE_DB)) : undefined);
|
|
1854
|
-
if (db) {
|
|
1855
|
-
const row = db
|
|
1856
|
-
.prepare('SELECT email FROM control_account WHERE active=1 LIMIT 1;')
|
|
1857
|
-
.get();
|
|
1858
|
-
const out = typeof row?.email === 'string' ? row.email.trim() : '';
|
|
1859
|
-
if (out) {
|
|
1860
|
-
cachedOpenCodeAccount = out;
|
|
1861
|
-
return out;
|
|
1862
|
-
}
|
|
1863
|
-
}
|
|
1864
|
-
}
|
|
1865
|
-
catch { /* DB not accessible, sqlite module unavailable, or query failed */ }
|
|
1866
|
-
finally {
|
|
1867
|
-
try {
|
|
1868
|
-
owned?.close();
|
|
1869
|
-
}
|
|
1870
|
-
catch { /* best-effort close */ }
|
|
1871
|
-
}
|
|
1872
|
-
cachedOpenCodeAccount = '';
|
|
1873
|
-
return undefined;
|
|
1866
|
+
cachedOpenCodeAccount = resolveOpenCodeAccountId(HOME) ?? '';
|
|
1867
|
+
return cachedOpenCodeAccount || undefined;
|
|
1874
1868
|
}
|
|
1875
1869
|
/**
|
|
1876
1870
|
* The per-session ledger stamp for an OpenCode row: the newest write time across
|
|
@@ -1945,12 +1939,28 @@ async function scanOpenCodeIncremental() {
|
|
|
1945
1939
|
// works on every OS — the CLI is absent on Windows.
|
|
1946
1940
|
let db;
|
|
1947
1941
|
try {
|
|
1942
|
+
db = new Database(OPENCODE_DB);
|
|
1943
|
+
// OpenCode's `session` schema varies by version: `cost` and `model` are newer
|
|
1944
|
+
// columns. Probe once and select NULL where absent, so an older opencode.db
|
|
1945
|
+
// still scans instead of throwing "no such column" and dropping every session.
|
|
1946
|
+
const sessionCols = new Set(db.prepare('PRAGMA table_info(session);').all()
|
|
1947
|
+
.map(c => (typeof c.name === 'string' ? c.name : '')));
|
|
1948
|
+
const costExpr = sessionCols.has('cost') ? 's.cost' : 'NULL';
|
|
1949
|
+
const modelExpr = sessionCols.has('model') ? 's.model' : 'NULL';
|
|
1950
|
+
// Every `json_extract` / `json_type` below is guarded by `json_valid`.
|
|
1951
|
+
// SQLite raises "malformed JSON" on a non-JSON value, and that aborts the
|
|
1952
|
+
// WHOLE query — so a single unparseable `part`/`message` row anywhere in the
|
|
1953
|
+
// shared DB would drop EVERY OpenCode session from the index, silently in a
|
|
1954
|
+
// non-TTY run (the handler below only prints when stderr is a TTY). A
|
|
1955
|
+
// poisoned row must cost that row, not the harness.
|
|
1948
1956
|
const query = `
|
|
1949
1957
|
SELECT
|
|
1950
1958
|
s.id AS id,
|
|
1951
1959
|
s.title AS title,
|
|
1952
1960
|
s.directory AS directory,
|
|
1953
1961
|
s.version AS version,
|
|
1962
|
+
${costExpr} AS cost,
|
|
1963
|
+
${modelExpr} AS model,
|
|
1954
1964
|
s.time_created AS time_created,
|
|
1955
1965
|
s.time_updated AS time_updated,
|
|
1956
1966
|
COALESCE(stats.message_count, 0) AS message_count,
|
|
@@ -1958,15 +1968,20 @@ async function scanOpenCodeIncremental() {
|
|
|
1958
1968
|
COALESCE(stats.message_bytes, 0) AS message_bytes,
|
|
1959
1969
|
COALESCE(parts.last_part_at, 0) AS last_part_at,
|
|
1960
1970
|
COALESCE(parts.part_bytes, 0) AS part_bytes,
|
|
1971
|
+
COALESCE(parts.tool_call_count, 0) AS tool_call_count,
|
|
1961
1972
|
stats.token_count AS token_count,
|
|
1962
1973
|
stats.output_tokens AS output_tokens,
|
|
1974
|
+
stats.input_tokens AS input_tokens,
|
|
1975
|
+
stats.cache_read_tokens AS cache_read_tokens,
|
|
1976
|
+
stats.cache_write_tokens AS cache_write_tokens,
|
|
1963
1977
|
COALESCE(stats.has_token_data, 0) AS has_token_data
|
|
1964
1978
|
FROM session s
|
|
1965
1979
|
LEFT JOIN (
|
|
1966
1980
|
SELECT
|
|
1967
1981
|
session_id,
|
|
1968
1982
|
MAX(time_created) AS last_part_at,
|
|
1969
|
-
SUM(LENGTH(CAST(data AS BLOB))) AS part_bytes
|
|
1983
|
+
SUM(LENGTH(CAST(data AS BLOB))) AS part_bytes,
|
|
1984
|
+
SUM(CASE WHEN json_valid(data) AND json_extract(data, '$.type') = 'tool' THEN 1 ELSE 0 END) AS tool_call_count
|
|
1970
1985
|
FROM part
|
|
1971
1986
|
GROUP BY session_id
|
|
1972
1987
|
) parts ON parts.session_id = s.id
|
|
@@ -1976,15 +1991,18 @@ async function scanOpenCodeIncremental() {
|
|
|
1976
1991
|
COUNT(*) AS message_count,
|
|
1977
1992
|
MAX(time_created) AS last_message_at,
|
|
1978
1993
|
SUM(LENGTH(CAST(data AS BLOB))) AS message_bytes,
|
|
1979
|
-
SUM(
|
|
1994
|
+
SUM(CASE WHEN json_valid(data) THEN
|
|
1980
1995
|
COALESCE(json_extract(data, '$.tokens.input'), 0) +
|
|
1981
1996
|
COALESCE(json_extract(data, '$.tokens.output'), 0) +
|
|
1982
1997
|
COALESCE(json_extract(data, '$.tokens.reasoning'), 0) +
|
|
1983
1998
|
COALESCE(json_extract(data, '$.tokens.cache.read'), 0) +
|
|
1984
1999
|
COALESCE(json_extract(data, '$.tokens.cache.write'), 0)
|
|
1985
|
-
) AS token_count,
|
|
1986
|
-
SUM(COALESCE(json_extract(data, '$.tokens.output'), 0)) AS output_tokens,
|
|
1987
|
-
|
|
2000
|
+
ELSE 0 END) AS token_count,
|
|
2001
|
+
SUM(CASE WHEN json_valid(data) THEN COALESCE(json_extract(data, '$.tokens.output'), 0) ELSE 0 END) AS output_tokens,
|
|
2002
|
+
SUM(CASE WHEN json_valid(data) THEN COALESCE(json_extract(data, '$.tokens.input'), 0) ELSE 0 END) AS input_tokens,
|
|
2003
|
+
SUM(CASE WHEN json_valid(data) THEN COALESCE(json_extract(data, '$.tokens.cache.read'), 0) ELSE 0 END) AS cache_read_tokens,
|
|
2004
|
+
SUM(CASE WHEN json_valid(data) THEN COALESCE(json_extract(data, '$.tokens.cache.write'), 0) ELSE 0 END) AS cache_write_tokens,
|
|
2005
|
+
MAX(CASE WHEN json_valid(data) AND json_type(data, '$.tokens') IS NOT NULL THEN 1 ELSE 0 END) AS has_token_data
|
|
1988
2006
|
FROM message
|
|
1989
2007
|
GROUP BY session_id
|
|
1990
2008
|
) stats ON stats.session_id = s.id
|
|
@@ -1992,8 +2010,7 @@ async function scanOpenCodeIncremental() {
|
|
|
1992
2010
|
ORDER BY time_created DESC
|
|
1993
2011
|
LIMIT 1000;
|
|
1994
2012
|
`.replace(/\n/g, ' ');
|
|
1995
|
-
|
|
1996
|
-
const account = getOpenCodeAccount(db);
|
|
2013
|
+
const account = getOpenCodeAccount();
|
|
1997
2014
|
const rows = db.prepare(query).all();
|
|
1998
2015
|
// Two passes. First derive each row's identity + per-session stamp, then
|
|
1999
2016
|
// bulk-load the prior stamps in ONE query and keep only the rows that
|
|
@@ -2034,13 +2051,40 @@ async function scanOpenCodeIncremental() {
|
|
|
2034
2051
|
const messageCount = asInt(row.message_count);
|
|
2035
2052
|
const tokenCount = asInt(row.token_count);
|
|
2036
2053
|
const outputTokens = asInt(row.output_tokens);
|
|
2054
|
+
const inputTokens = asInt(row.input_tokens);
|
|
2055
|
+
const cacheReadTokens = asInt(row.cache_read_tokens);
|
|
2056
|
+
const cacheWriteTokens = asInt(row.cache_write_tokens);
|
|
2037
2057
|
const hasTokenData = asInt(row.has_token_data) === 1;
|
|
2058
|
+
const toolCallCount = asInt(row.tool_call_count);
|
|
2038
2059
|
const timestamp = isNaN(timeCreated) ? new Date().toISOString() : new Date(timeCreated).toISOString();
|
|
2039
2060
|
// OpenCode is one shared DB, not one file per session — its row carries a
|
|
2040
2061
|
// per-session updated time. Set lastActivity explicitly (falling back to
|
|
2041
2062
|
// creation, never the whole-DB mtime the ScanStamp would otherwise supply).
|
|
2042
2063
|
const lastActivity = Number.isNaN(timeUpdated) ? timestamp : new Date(timeUpdated).toISOString();
|
|
2043
2064
|
const topic = title || undefined;
|
|
2065
|
+
// Duration is the session-row span; a missing/degenerate pair yields no value
|
|
2066
|
+
// rather than a negative or NaN.
|
|
2067
|
+
const durationMs = Number.isFinite(timeCreated) && Number.isFinite(timeUpdated) && timeUpdated > timeCreated
|
|
2068
|
+
? timeUpdated - timeCreated
|
|
2069
|
+
: undefined;
|
|
2070
|
+
// OpenCode stores the model as JSON (`{"id":"…","providerID":"…"}`); the
|
|
2071
|
+
// index tracks the model id.
|
|
2072
|
+
let model;
|
|
2073
|
+
if (typeof row.model === 'string' && row.model.trim()) {
|
|
2074
|
+
try {
|
|
2075
|
+
const parsed = JSON.parse(row.model);
|
|
2076
|
+
if (typeof parsed?.id === 'string' && parsed.id.trim())
|
|
2077
|
+
model = parsed.id;
|
|
2078
|
+
}
|
|
2079
|
+
catch { /* non-JSON model string — leave unset */ }
|
|
2080
|
+
}
|
|
2081
|
+
// `cost` is a REAL rollup OpenCode maintains on the session row (0 for a
|
|
2082
|
+
// zero-priced provider, which is a real value, not "unknown").
|
|
2083
|
+
const costUsd = typeof row.cost === 'number' ? row.cost : undefined;
|
|
2084
|
+
// Worktree slug is a pure function of cwd (`.agents/worktrees/<slug>/`),
|
|
2085
|
+
// so it derives for OpenCode exactly as it does for every other harness.
|
|
2086
|
+
const cwd = directory ? normalizeCwd(directory) : undefined;
|
|
2087
|
+
const worktreeSlug = detectWorktree(cwd)?.slug;
|
|
2044
2088
|
const meta = {
|
|
2045
2089
|
id,
|
|
2046
2090
|
shortId: deriveShortId(id, /^ses_/),
|
|
@@ -2048,14 +2092,22 @@ async function scanOpenCodeIncremental() {
|
|
|
2048
2092
|
timestamp,
|
|
2049
2093
|
lastActivity,
|
|
2050
2094
|
project: directory ? path.basename(directory) : undefined,
|
|
2051
|
-
cwd
|
|
2095
|
+
cwd,
|
|
2052
2096
|
filePath,
|
|
2053
2097
|
version: resolveSessionVersion('opencode', OPENCODE_DB, version || undefined, currentVersion),
|
|
2054
2098
|
account,
|
|
2055
2099
|
topic,
|
|
2100
|
+
model,
|
|
2101
|
+
costUsd,
|
|
2102
|
+
durationMs,
|
|
2103
|
+
worktreeSlug,
|
|
2104
|
+
toolCallCount: Number.isNaN(toolCallCount) ? undefined : toolCallCount,
|
|
2056
2105
|
messageCount: Number.isNaN(messageCount) ? undefined : messageCount,
|
|
2057
2106
|
tokenCount: hasTokenData && !Number.isNaN(tokenCount) ? tokenCount : undefined,
|
|
2058
2107
|
outputTokens: hasTokenData && !Number.isNaN(outputTokens) ? outputTokens : undefined,
|
|
2108
|
+
inputTokens: hasTokenData && !Number.isNaN(inputTokens) ? inputTokens : undefined,
|
|
2109
|
+
cacheReadTokens: hasTokenData && !Number.isNaN(cacheReadTokens) ? cacheReadTokens : undefined,
|
|
2110
|
+
cacheWriteTokens: hasTokenData && !Number.isNaN(cacheWriteTokens) ? cacheWriteTokens : undefined,
|
|
2059
2111
|
};
|
|
2060
2112
|
entries.push({ meta, content: topic || '', scan });
|
|
2061
2113
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Favorited
|
|
3
|
-
*
|
|
2
|
+
* Favorited sessions — the durable "keep this one handy" mark a human puts on a
|
|
3
|
+
* session, deliberately kept OUT of the session index.
|
|
4
4
|
*
|
|
5
5
|
* `sessions.db` is a rebuildable CACHE: a reindex or a schema bump throws its
|
|
6
6
|
* rows away and re-derives them from the transcripts on disk. A favorite is not
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Favorited
|
|
3
|
-
*
|
|
2
|
+
* Favorited sessions — the durable "keep this one handy" mark a human puts on a
|
|
3
|
+
* session, deliberately kept OUT of the session index.
|
|
4
4
|
*
|
|
5
5
|
* `sessions.db` is a rebuildable CACHE: a reindex or a schema bump throws its
|
|
6
6
|
* rows away and re-derives them from the transcripts on disk. A favorite is not
|
|
@@ -103,6 +103,33 @@ export declare function parseGemini(filePath: string): SessionEvent[];
|
|
|
103
103
|
* request step and a completion step that share the id).
|
|
104
104
|
*/
|
|
105
105
|
export declare function parseAntigravity(dbPath: string): SessionEvent[];
|
|
106
|
+
/**
|
|
107
|
+
* Separator between a container file and the id it holds inside a *composite*
|
|
108
|
+
* session `file_path`. Some harnesses keep every session in ONE file (OpenCode's
|
|
109
|
+
* single `opencode.db`), so the index stores `<container>#<session-id>` rather
|
|
110
|
+
* than one path per session — e.g.
|
|
111
|
+
* `/home/u/.local/share/opencode/opencode.db#ses_02410a2c…`.
|
|
112
|
+
*/
|
|
113
|
+
export declare const SESSION_FILE_PATH_SEP = "#";
|
|
114
|
+
/**
|
|
115
|
+
* Split a stored session `file_path` into its on-disk container and the optional
|
|
116
|
+
* in-container fragment. For a plain per-session file the container is the path
|
|
117
|
+
* itself and `fragment` is undefined; for a composite path it is the part before
|
|
118
|
+
* the first `#` and the id after it.
|
|
119
|
+
*/
|
|
120
|
+
export declare function splitSessionFilePath(filePath: string): {
|
|
121
|
+
container: string;
|
|
122
|
+
fragment: string | undefined;
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* The filesystem path whose existence/stat decides whether a session row is
|
|
126
|
+
* stale. For a composite `file_path` this is the CONTAINER file — the row is a
|
|
127
|
+
* record inside it, never a filesystem entry of its own — so a composite session
|
|
128
|
+
* is stale only when its container is gone. For a plain path it is the path
|
|
129
|
+
* itself. Keying off the composite FORM (a `#` fragment), not any harness name,
|
|
130
|
+
* means any future single-file-DB harness inherits the correct behavior.
|
|
131
|
+
*/
|
|
132
|
+
export declare function sessionFilePathContainer(filePath: string): string;
|
|
106
133
|
/**
|
|
107
134
|
* Parse an OpenCode session from its SQLite database.
|
|
108
135
|
* filePath format: "/path/to/opencode.db#session_id"
|
|
@@ -130,6 +157,42 @@ export declare function parseAntigravity(dbPath: string): SessionEvent[];
|
|
|
130
157
|
* (from summary.json), falling back to the transcript's mtime.
|
|
131
158
|
*/
|
|
132
159
|
export declare function parseGrok(filePath: string): SessionEvent[];
|
|
160
|
+
/**
|
|
161
|
+
* The transcript query {@link parseOpenCode} runs, exported so a test can assert
|
|
162
|
+
* the projection's real cost against a database instead of re-typing the SQL.
|
|
163
|
+
*
|
|
164
|
+
* A tool part is PROJECTED to exactly the fields the `case 'tool'` branch below
|
|
165
|
+
* reads — `tool`, `callID`, `state.status`, `state.input`, `state.output` —
|
|
166
|
+
* rather than carried whole. Two things this has to get right at once:
|
|
167
|
+
*
|
|
168
|
+
* - Keep `state.input`. It carries `filePath` / `command`, and losing it is
|
|
169
|
+
* what left `recentDirectoriesTouched` empty (RUSH-2358).
|
|
170
|
+
* - Stay bounded. A tool part is not bounded by its output: on a real
|
|
171
|
+
* `opencode.db` the largest part is 1,346,068 bytes of which
|
|
172
|
+
* `state.attachments` (a base64 data URL from `read`) is 1,345,674, while
|
|
173
|
+
* `state.output` is 23. Truncating only `state.output` therefore bounded
|
|
174
|
+
* nothing — it took one session's loaded tool payload from 299,365 to
|
|
175
|
+
* 1,911,209 bytes. The projection drops `attachments` (and every other
|
|
176
|
+
* unread key) outright, caps `state.output`, and collapses an oversized
|
|
177
|
+
* `state.input` to just its addressing fields. Those are the keys the
|
|
178
|
+
* enrichment reads — `filePath`/`path` for an edit and `cwd`/`workdir`/
|
|
179
|
+
* `working_directory` for a shell (`extractRecentDirectoriesTouched` in
|
|
180
|
+
* state.ts), plus `command`/`description`, themselves capped since a command
|
|
181
|
+
* can be arbitrarily long. Above the cap every other input key (an `edit`'s
|
|
182
|
+
* `oldString`/`newString`) is gone, deliberately — nothing downstream reads
|
|
183
|
+
* them, and they are the weight.
|
|
184
|
+
*
|
|
185
|
+
* `json_valid` guards every `json_extract`: SQLite raises "malformed JSON" on a
|
|
186
|
+
* non-JSON value, which aborts the WHOLE query, so one bad `part` row would
|
|
187
|
+
* otherwise cost the entire transcript. Note the single-argument form is
|
|
188
|
+
* RFC-8259-strict — it rejects a JSONB blob that `json_extract` would accept.
|
|
189
|
+
* That is correct for today's schema (`part.data` / `message.data` are TEXT on
|
|
190
|
+
* a real database); if OpenCode ever migrates them to JSONB, these guards must
|
|
191
|
+
* move to `json_valid(data, 6)` or they will drop every row.
|
|
192
|
+
*
|
|
193
|
+
* The session id is bound as a parameter; the two caps are numeric literals.
|
|
194
|
+
*/
|
|
195
|
+
export declare const OPENCODE_TRANSCRIPT_QUERY: string;
|
|
133
196
|
export declare function parseOpenCode(filePath: string): SessionEvent[];
|
|
134
197
|
/** Parse a Rush JSONL session file into normalized events. */
|
|
135
198
|
export declare function parseRush(filePath: string): SessionEvent[];
|