@phnx-labs/agents-cli 1.21.3 → 1.22.0
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 +67 -0
- package/README.md +32 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/computer-actions.js +1 -0
- package/dist/commands/exec.d.ts +27 -0
- package/dist/commands/exec.js +123 -6
- package/dist/commands/models.js +36 -1
- package/dist/commands/projects.js +22 -2
- package/dist/commands/sessions-backfill.d.ts +32 -0
- package/dist/commands/sessions-backfill.js +186 -0
- package/dist/commands/sessions.d.ts +17 -1
- package/dist/commands/sessions.js +317 -18
- package/dist/commands/teams.js +1 -1
- package/dist/commands/worktree.d.ts +3 -3
- package/dist/commands/worktree.js +35 -4
- package/dist/lib/daemon.d.ts +5 -1
- package/dist/lib/daemon.js +63 -14
- package/dist/lib/devices/resolve-target.d.ts +6 -0
- package/dist/lib/devices/resolve-target.js +9 -3
- package/dist/lib/exec.js +39 -8
- package/dist/lib/hosts/dispatch.d.ts +12 -0
- package/dist/lib/hosts/dispatch.js +23 -6
- package/dist/lib/hosts/reconnect.d.ts +38 -0
- package/dist/lib/hosts/reconnect.js +85 -4
- package/dist/lib/hosts/run-target.js +14 -2
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/model-tiers.d.ts +54 -0
- package/dist/lib/model-tiers.js +229 -0
- package/dist/lib/models.d.ts +3 -0
- package/dist/lib/models.js +44 -7
- package/dist/lib/pricing/prices.json +16 -1
- package/dist/lib/project-focus.d.ts +42 -0
- package/dist/lib/project-focus.js +80 -0
- package/dist/lib/project-schedule.d.ts +75 -0
- package/dist/lib/project-schedule.js +110 -0
- package/dist/lib/redact.d.ts +2 -0
- package/dist/lib/redact.js +22 -0
- package/dist/lib/remote-agents-json.d.ts +2 -0
- package/dist/lib/remote-agents-json.js +3 -3
- package/dist/lib/rotate.d.ts +84 -1
- package/dist/lib/rotate.js +155 -5
- package/dist/lib/runner.d.ts +4 -2
- package/dist/lib/runner.js +13 -4
- 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/session/bash-command.js +60 -9
- package/dist/lib/session/db.d.ts +7 -1
- package/dist/lib/session/db.js +301 -32
- package/dist/lib/session/discover.d.ts +40 -7
- package/dist/lib/session/discover.js +144 -83
- package/dist/lib/session/parse.d.ts +8 -1
- package/dist/lib/session/parse.js +83 -32
- package/dist/lib/session/remote-list.d.ts +71 -0
- package/dist/lib/session/remote-list.js +410 -2
- package/dist/lib/session/shell-programs.d.ts +15 -0
- package/dist/lib/session/shell-programs.js +359 -0
- package/dist/lib/session/tool-calls.d.ts +88 -0
- package/dist/lib/session/tool-calls.js +612 -0
- package/dist/lib/session/tool-index.d.ts +100 -0
- package/dist/lib/session/tool-index.js +773 -0
- package/dist/lib/session/tool-store.d.ts +15 -0
- package/dist/lib/session/tool-store.js +198 -0
- package/dist/lib/session/types.d.ts +7 -0
- package/dist/lib/state.d.ts +10 -1
- package/dist/lib/state.js +11 -2
- package/dist/lib/teams/remoteWorktree.d.ts +3 -4
- package/dist/lib/teams/remoteWorktree.js +3 -4
- package/dist/lib/teams/worktree.d.ts +11 -1
- package/dist/lib/teams/worktree.js +42 -4
- package/dist/lib/types.d.ts +17 -0
- package/dist/lib/types.js +17 -0
- package/package.json +3 -1
package/dist/lib/session/db.js
CHANGED
|
@@ -15,6 +15,8 @@ import { getSessionsDir, getSessionsDbPath } from '../state.js';
|
|
|
15
15
|
import { query as queryEvents } from '../events.js';
|
|
16
16
|
import { machineForSessionFile } from './origin-machine.js';
|
|
17
17
|
import { loadSessionActorIndex, readSessionActorRecord } from './actor-sidecar.js';
|
|
18
|
+
import { toolCallsFromEvents } from './tool-calls.js';
|
|
19
|
+
import { persistToolCalls, purgeToolCalls, toolEvidenceSourcePath } from './tool-store.js';
|
|
18
20
|
import { extractSkills, extractSlashCommands } from './highlights.js';
|
|
19
21
|
import { resolveResource } from '../resources.js';
|
|
20
22
|
import { discoverPlugins } from '../plugins.js';
|
|
@@ -23,7 +25,7 @@ const DB_PATH = getSessionsDbPath();
|
|
|
23
25
|
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
24
26
|
* assert against the constant instead of hardcoding a number that every bump
|
|
25
27
|
* then has to chase (docs/05-sessions.md calls the constant the source of truth). */
|
|
26
|
-
export const SCHEMA_VERSION =
|
|
28
|
+
export const SCHEMA_VERSION = 30;
|
|
27
29
|
/**
|
|
28
30
|
* Canonicalize a file path for use as a scan_ledger key. The same physical
|
|
29
31
|
* session file is reachable via multiple aliases — `~/.claude/projects/x.jsonl`
|
|
@@ -147,6 +149,73 @@ CREATE TABLE IF NOT EXISTS dir_ledger (
|
|
|
147
149
|
scanned_at INTEGER NOT NULL
|
|
148
150
|
);
|
|
149
151
|
|
|
152
|
+
-- One redacted evidence row per tool call. The ordinal is assigned in
|
|
153
|
+
-- transcript order and is stable across incremental appends.
|
|
154
|
+
CREATE TABLE IF NOT EXISTS tool_calls (
|
|
155
|
+
call_key TEXT PRIMARY KEY,
|
|
156
|
+
session_id TEXT NOT NULL,
|
|
157
|
+
ordinal INTEGER NOT NULL,
|
|
158
|
+
source_call_id TEXT,
|
|
159
|
+
timestamp TEXT NOT NULL,
|
|
160
|
+
tool TEXT NOT NULL,
|
|
161
|
+
input TEXT NOT NULL,
|
|
162
|
+
outcome TEXT NOT NULL,
|
|
163
|
+
exit_code INTEGER,
|
|
164
|
+
status_code INTEGER,
|
|
165
|
+
error_code TEXT,
|
|
166
|
+
output TEXT,
|
|
167
|
+
error TEXT,
|
|
168
|
+
parse_error TEXT,
|
|
169
|
+
evidence_bytes INTEGER NOT NULL,
|
|
170
|
+
UNIQUE(session_id, ordinal)
|
|
171
|
+
);
|
|
172
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id, ordinal);
|
|
173
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_tool ON tool_calls(tool COLLATE NOCASE);
|
|
174
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_outcome ON tool_calls(outcome);
|
|
175
|
+
|
|
176
|
+
CREATE TABLE IF NOT EXISTS tool_call_programs (
|
|
177
|
+
call_key TEXT NOT NULL,
|
|
178
|
+
program TEXT NOT NULL COLLATE NOCASE,
|
|
179
|
+
PRIMARY KEY(call_key, program)
|
|
180
|
+
);
|
|
181
|
+
CREATE INDEX IF NOT EXISTS idx_tool_call_programs_program ON tool_call_programs(program, call_key);
|
|
182
|
+
|
|
183
|
+
-- Ordered static program sites retain repeated commands within one Bash call.
|
|
184
|
+
-- The complete redacted command stays on tool_calls.input; these rows contain
|
|
185
|
+
-- only the normalized program and whether it is a wrapper or effective target.
|
|
186
|
+
CREATE TABLE IF NOT EXISTS tool_program_occurrences (
|
|
187
|
+
call_key TEXT NOT NULL,
|
|
188
|
+
occurrence_ordinal INTEGER NOT NULL,
|
|
189
|
+
program TEXT NOT NULL COLLATE NOCASE,
|
|
190
|
+
role TEXT NOT NULL CHECK(role IN ('wrapper', 'effective')),
|
|
191
|
+
PRIMARY KEY(call_key, occurrence_ordinal)
|
|
192
|
+
);
|
|
193
|
+
CREATE INDEX IF NOT EXISTS idx_tool_program_occurrences_program
|
|
194
|
+
ON tool_program_occurrences(program, call_key);
|
|
195
|
+
|
|
196
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS tool_call_text USING fts5(
|
|
197
|
+
call_key UNINDEXED,
|
|
198
|
+
tool,
|
|
199
|
+
input,
|
|
200
|
+
output,
|
|
201
|
+
error,
|
|
202
|
+
tokenize = 'trigram'
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
-- Independent of scan_ledger: schema migration never forces the normal
|
|
206
|
+
-- session index to reread history. Existing transcripts are backfilled only
|
|
207
|
+
-- by the explicit, bounded agents sessions backfill tools command.
|
|
208
|
+
CREATE TABLE IF NOT EXISTS tool_scan_ledger (
|
|
209
|
+
session_id TEXT PRIMARY KEY,
|
|
210
|
+
file_path TEXT NOT NULL UNIQUE,
|
|
211
|
+
file_mtime_ms INTEGER NOT NULL,
|
|
212
|
+
file_size INTEGER NOT NULL,
|
|
213
|
+
extractor_version INTEGER NOT NULL,
|
|
214
|
+
indexed_at INTEGER NOT NULL,
|
|
215
|
+
call_count INTEGER NOT NULL,
|
|
216
|
+
evidence_bytes INTEGER NOT NULL
|
|
217
|
+
);
|
|
218
|
+
|
|
150
219
|
-- Skill/slash-command usage per session (#12), computed from a session's
|
|
151
220
|
-- parsed transcript (extractSkills / extractSlashCommands, session/highlights.ts)
|
|
152
221
|
-- and joined at write time against the currently-installed resource/plugin
|
|
@@ -390,12 +459,11 @@ function migrateSchema(db, fromVersion) {
|
|
|
390
459
|
.prepare(`SELECT id, agent, file_path FROM sessions WHERE machine IS NULL OR machine = ''`)
|
|
391
460
|
.all();
|
|
392
461
|
const upd = db.prepare(`UPDATE sessions SET machine = ? WHERE id = ?`);
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
txn(rows);
|
|
462
|
+
// migrateSchema runs inside getDB's schema transaction, so these writes
|
|
463
|
+
// deliberately share that transaction instead of opening a nested one.
|
|
464
|
+
for (const row of rows) {
|
|
465
|
+
upd.run(machineForSessionFile(row.file_path, row.agent), row.id);
|
|
466
|
+
}
|
|
399
467
|
}
|
|
400
468
|
if (fromVersion < 19) {
|
|
401
469
|
// v18 → v19: actor provenance (RUSH-2018) — who initiated the session and
|
|
@@ -430,7 +498,7 @@ function migrateSchema(db, fromVersion) {
|
|
|
430
498
|
db.exec(`DELETE FROM scan_ledger; DELETE FROM dir_ledger;`);
|
|
431
499
|
}
|
|
432
500
|
if (fromVersion < 22) {
|
|
433
|
-
// v21 → v22
|
|
501
|
+
// v21 → v22: persist the transcript's aggregate tool-call count.
|
|
434
502
|
const cols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
435
503
|
if (!cols.some(c => c.name === 'tool_call_count'))
|
|
436
504
|
db.exec(`ALTER TABLE sessions ADD COLUMN tool_call_count INTEGER`);
|
|
@@ -445,10 +513,7 @@ function migrateSchema(db, fromVersion) {
|
|
|
445
513
|
//
|
|
446
514
|
// Deliberately NO DEFAULT (NULL on ALTER, for every pre-existing row):
|
|
447
515
|
// NULL means "not yet computed by this scanner" (a legacy row), distinct
|
|
448
|
-
// from a real, computed 0/false.
|
|
449
|
-
// make every un-rescanned row look like a definite "never used
|
|
450
|
-
// browser/computer" and permanently blind the picker's fallback path for
|
|
451
|
-
// any row that isn't rescanned before it's next viewed.
|
|
516
|
+
// from a real, computed 0/false.
|
|
452
517
|
const cols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
453
518
|
if (!cols.some(c => c.name === 'used_browser'))
|
|
454
519
|
db.exec(`ALTER TABLE sessions ADD COLUMN used_browser INTEGER`);
|
|
@@ -458,12 +523,7 @@ function migrateSchema(db, fromVersion) {
|
|
|
458
523
|
if (fromVersion < 24) {
|
|
459
524
|
// v23 → v24: session_resource_usage (#12) — skill/slash-command usage per
|
|
460
525
|
// session, joined against the currently-installed resource/plugin for
|
|
461
|
-
// provenance (
|
|
462
|
-
// needs only CREATE TABLE IF NOT EXISTS, already present in SCHEMA for a
|
|
463
|
-
// fresh DB — this repeats it for an existing DB whose SCHEMA exec ran
|
|
464
|
-
// before the table definition was added. No ledger wipe: this table is
|
|
465
|
-
// populated by writeResourceUsage() at upsert time, independent of the
|
|
466
|
-
// scan ledgers.
|
|
526
|
+
// provenance. No ledger wipe: writeResourceUsage() owns this table.
|
|
467
527
|
db.exec(`
|
|
468
528
|
CREATE TABLE IF NOT EXISTS session_resource_usage (
|
|
469
529
|
session_id TEXT NOT NULL,
|
|
@@ -480,6 +540,144 @@ function migrateSchema(db, fromVersion) {
|
|
|
480
540
|
CREATE INDEX IF NOT EXISTS idx_session_resource_usage_plugin ON session_resource_usage(plugin);
|
|
481
541
|
`);
|
|
482
542
|
}
|
|
543
|
+
if (fromVersion < 25) {
|
|
544
|
+
// v24 → v25: tool-call evidence uses an independent ledger. Do not clear
|
|
545
|
+
// scan_ledger or dir_ledger: normal session listing stays warm, while tool
|
|
546
|
+
// history is filled once on demand.
|
|
547
|
+
db.exec(`
|
|
548
|
+
CREATE TABLE IF NOT EXISTS tool_calls (
|
|
549
|
+
call_key TEXT PRIMARY KEY,
|
|
550
|
+
session_id TEXT NOT NULL,
|
|
551
|
+
ordinal INTEGER NOT NULL,
|
|
552
|
+
source_call_id TEXT,
|
|
553
|
+
timestamp TEXT NOT NULL,
|
|
554
|
+
tool TEXT NOT NULL,
|
|
555
|
+
input TEXT NOT NULL,
|
|
556
|
+
outcome TEXT NOT NULL,
|
|
557
|
+
exit_code INTEGER,
|
|
558
|
+
status_code INTEGER,
|
|
559
|
+
error_code TEXT,
|
|
560
|
+
output TEXT,
|
|
561
|
+
error TEXT,
|
|
562
|
+
parse_error TEXT,
|
|
563
|
+
evidence_bytes INTEGER NOT NULL,
|
|
564
|
+
UNIQUE(session_id, ordinal)
|
|
565
|
+
);
|
|
566
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_session ON tool_calls(session_id, ordinal);
|
|
567
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_tool ON tool_calls(tool COLLATE NOCASE);
|
|
568
|
+
CREATE INDEX IF NOT EXISTS idx_tool_calls_outcome ON tool_calls(outcome);
|
|
569
|
+
CREATE TABLE IF NOT EXISTS tool_call_programs (
|
|
570
|
+
call_key TEXT NOT NULL,
|
|
571
|
+
program TEXT NOT NULL COLLATE NOCASE,
|
|
572
|
+
PRIMARY KEY(call_key, program)
|
|
573
|
+
);
|
|
574
|
+
CREATE INDEX IF NOT EXISTS idx_tool_call_programs_program ON tool_call_programs(program, call_key);
|
|
575
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS tool_call_text USING fts5(
|
|
576
|
+
call_key UNINDEXED,
|
|
577
|
+
tool,
|
|
578
|
+
input,
|
|
579
|
+
output,
|
|
580
|
+
error,
|
|
581
|
+
tokenize = 'unicode61 remove_diacritics 2'
|
|
582
|
+
);
|
|
583
|
+
CREATE TABLE IF NOT EXISTS tool_scan_ledger (
|
|
584
|
+
file_path TEXT PRIMARY KEY,
|
|
585
|
+
file_mtime_ms INTEGER NOT NULL,
|
|
586
|
+
file_size INTEGER NOT NULL,
|
|
587
|
+
extractor_version INTEGER NOT NULL,
|
|
588
|
+
indexed_at INTEGER NOT NULL,
|
|
589
|
+
call_count INTEGER NOT NULL,
|
|
590
|
+
evidence_bytes INTEGER NOT NULL
|
|
591
|
+
);
|
|
592
|
+
`);
|
|
593
|
+
}
|
|
594
|
+
if (fromVersion < 26) {
|
|
595
|
+
// v25 → v26: make append accounting O(changed calls), including empty
|
|
596
|
+
// deltas, instead of reading every historical evidence row per append.
|
|
597
|
+
const callCols = db.prepare(`PRAGMA table_info(tool_calls)`).all();
|
|
598
|
+
if (!callCols.some((column) => column.name === 'evidence_bytes')) {
|
|
599
|
+
db.exec(`ALTER TABLE tool_calls ADD COLUMN evidence_bytes INTEGER NOT NULL DEFAULT 0`);
|
|
600
|
+
}
|
|
601
|
+
const ledgerCols = db.prepare(`PRAGMA table_info(tool_scan_ledger)`).all();
|
|
602
|
+
if (!ledgerCols.some((column) => column.name === 'evidence_bytes')) {
|
|
603
|
+
db.exec(`ALTER TABLE tool_scan_ledger ADD COLUMN evidence_bytes INTEGER NOT NULL DEFAULT 0`);
|
|
604
|
+
}
|
|
605
|
+
// The first tool schema existed only in prerelease development builds. Force its tool
|
|
606
|
+
// evidence through one bounded rebuild rather than trusting zeroed totals.
|
|
607
|
+
db.exec(`DELETE FROM tool_scan_ledger`);
|
|
608
|
+
}
|
|
609
|
+
if (fromVersion < 27) {
|
|
610
|
+
// v26 → v27: the original unicode word tokenizer could not prefilter the
|
|
611
|
+
// substring semantics promised by input/output/error queries. Rebuild only
|
|
612
|
+
// the derived FTS table from already-redacted call rows; transcripts and
|
|
613
|
+
// both scan ledgers remain warm.
|
|
614
|
+
db.exec(`
|
|
615
|
+
DROP TABLE IF EXISTS tool_call_text;
|
|
616
|
+
CREATE VIRTUAL TABLE tool_call_text USING fts5(
|
|
617
|
+
call_key UNINDEXED,
|
|
618
|
+
tool,
|
|
619
|
+
input,
|
|
620
|
+
output,
|
|
621
|
+
error,
|
|
622
|
+
tokenize = 'trigram'
|
|
623
|
+
);
|
|
624
|
+
INSERT INTO tool_call_text (call_key, tool, input, output, error)
|
|
625
|
+
SELECT call_key, tool, input, coalesce(output, ''), coalesce(error, '')
|
|
626
|
+
FROM tool_calls;
|
|
627
|
+
`);
|
|
628
|
+
}
|
|
629
|
+
if (fromVersion < 28) {
|
|
630
|
+
// v27 → v28: retain every static program occurrence instead of only the
|
|
631
|
+
// distinct program set. The source transcript is rebuilt only by the
|
|
632
|
+
// explicit tools backfill; normal session and directory ledgers stay warm.
|
|
633
|
+
db.exec(`
|
|
634
|
+
CREATE TABLE IF NOT EXISTS tool_program_occurrences (
|
|
635
|
+
call_key TEXT NOT NULL,
|
|
636
|
+
occurrence_ordinal INTEGER NOT NULL,
|
|
637
|
+
program TEXT NOT NULL COLLATE NOCASE,
|
|
638
|
+
role TEXT NOT NULL CHECK(role IN ('wrapper', 'effective')),
|
|
639
|
+
PRIMARY KEY(call_key, occurrence_ordinal)
|
|
640
|
+
);
|
|
641
|
+
CREATE INDEX IF NOT EXISTS idx_tool_program_occurrences_program
|
|
642
|
+
ON tool_program_occurrences(program, call_key);
|
|
643
|
+
DELETE FROM tool_scan_ledger;
|
|
644
|
+
`);
|
|
645
|
+
}
|
|
646
|
+
if (fromVersion < 29) {
|
|
647
|
+
// v28 → v29: coverage and query planning address the independent tool
|
|
648
|
+
// ledger by session id. Rebuild only this derived ledger so a tool query
|
|
649
|
+
// never has to resolve or stat transcript paths.
|
|
650
|
+
db.exec(`
|
|
651
|
+
DROP TABLE tool_scan_ledger;
|
|
652
|
+
CREATE TABLE tool_scan_ledger (
|
|
653
|
+
session_id TEXT PRIMARY KEY,
|
|
654
|
+
file_path TEXT NOT NULL UNIQUE,
|
|
655
|
+
file_mtime_ms INTEGER NOT NULL,
|
|
656
|
+
file_size INTEGER NOT NULL,
|
|
657
|
+
extractor_version INTEGER NOT NULL,
|
|
658
|
+
indexed_at INTEGER NOT NULL,
|
|
659
|
+
call_count INTEGER NOT NULL,
|
|
660
|
+
evidence_bytes INTEGER NOT NULL
|
|
661
|
+
);
|
|
662
|
+
`);
|
|
663
|
+
}
|
|
664
|
+
if (fromVersion < 30) {
|
|
665
|
+
// v29 → v30: prerelease tool-index builds temporarily used schema versions
|
|
666
|
+
// later owned by independent main migrations. Repair from the physical
|
|
667
|
+
// schema because a v29 marker alone cannot prove these columns are present.
|
|
668
|
+
const cols = new Set(db.prepare(`PRAGMA table_info(sessions)`).all().map((column) => column.name));
|
|
669
|
+
if (!cols.has('tool_call_count')) {
|
|
670
|
+
db.exec(`
|
|
671
|
+
ALTER TABLE sessions ADD COLUMN tool_call_count INTEGER;
|
|
672
|
+
DELETE FROM scan_ledger;
|
|
673
|
+
DELETE FROM dir_ledger;
|
|
674
|
+
`);
|
|
675
|
+
}
|
|
676
|
+
if (!cols.has('used_browser'))
|
|
677
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN used_browser INTEGER`);
|
|
678
|
+
if (!cols.has('used_computer'))
|
|
679
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN used_computer INTEGER`);
|
|
680
|
+
}
|
|
483
681
|
}
|
|
484
682
|
/** Open (or return the cached) sessions database, applying migrations as needed. */
|
|
485
683
|
export function getDB() {
|
|
@@ -487,24 +685,37 @@ export function getDB() {
|
|
|
487
685
|
return dbInstance;
|
|
488
686
|
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
489
687
|
const db = new Database(DB_PATH);
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
// Wait up to 30s instead of failing immediately on SQLITE_BUSY. Multiple
|
|
688
|
+
// Wait up to 30s instead of failing immediately on SQLITE_BUSY. Install the
|
|
689
|
+
// handler before journal_mode: concurrent first opens can race for its schema
|
|
690
|
+
// lock, before a later busy_timeout would have a chance to wait. Multiple
|
|
494
691
|
// agents (CLIs, skills, hooks) open this DB concurrently. The first scan of
|
|
495
692
|
// a new version home can take longer than 10s; concurrent callers need enough
|
|
496
693
|
// headroom to wait. The ledger-recheck in upsertSessionsBatch makes
|
|
497
694
|
// subsequent writers near-instant, so 30s is a rarely-reached safety net.
|
|
498
695
|
db.pragma('busy_timeout = 30000');
|
|
696
|
+
db.pragma('journal_mode = WAL');
|
|
697
|
+
db.pragma('synchronous = NORMAL');
|
|
698
|
+
db.pragma('temp_store = MEMORY');
|
|
499
699
|
db.exec(SCHEMA);
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
700
|
+
const readSchemaVersion = () => {
|
|
701
|
+
const row = db.prepare(`SELECT value FROM meta WHERE key = 'schema_version'`).get();
|
|
702
|
+
return row ? parseInt(row.value, 10) : undefined;
|
|
703
|
+
};
|
|
704
|
+
const currentVersion = readSchemaVersion();
|
|
705
|
+
if (currentVersion === undefined) {
|
|
503
706
|
db.prepare(`INSERT OR IGNORE INTO meta(key, value) VALUES ('schema_version', ?)`).run(String(SCHEMA_VERSION));
|
|
504
707
|
}
|
|
505
708
|
else if (currentVersion < SCHEMA_VERSION) {
|
|
506
|
-
|
|
507
|
-
|
|
709
|
+
// Re-read after BEGIN IMMEDIATE acquires the writer lock. A second process
|
|
710
|
+
// may have completed the migration while this connection was waiting.
|
|
711
|
+
const migrate = db.transaction(() => {
|
|
712
|
+
const lockedVersion = readSchemaVersion();
|
|
713
|
+
if (lockedVersion === undefined || lockedVersion >= SCHEMA_VERSION)
|
|
714
|
+
return;
|
|
715
|
+
migrateSchema(db, lockedVersion);
|
|
716
|
+
db.prepare(`INSERT OR REPLACE INTO meta(key, value) VALUES ('schema_version', ?)`).run(String(SCHEMA_VERSION));
|
|
717
|
+
});
|
|
718
|
+
migrate();
|
|
508
719
|
}
|
|
509
720
|
// Index last_activity only after the column is guaranteed to exist — fresh DBs
|
|
510
721
|
// get it from CREATE TABLE above, existing pre-v8 DBs from the migration just
|
|
@@ -1186,9 +1397,38 @@ export function upsertSessionsBatch(entries) {
|
|
|
1186
1397
|
const byPath = new Map(entries
|
|
1187
1398
|
.filter(e => e.scan && e.meta.filePath)
|
|
1188
1399
|
.map(e => [canonicalLedgerKey(e.meta.filePath), e]));
|
|
1189
|
-
const enrichedEntries = entries.map(entry =>
|
|
1190
|
-
|
|
1191
|
-
|
|
1400
|
+
const enrichedEntries = entries.map(entry => {
|
|
1401
|
+
if (entry.meta.agent === 'claude' || entry.meta.agent === 'codex' || !entry.meta.filePath)
|
|
1402
|
+
return entry;
|
|
1403
|
+
try {
|
|
1404
|
+
const toolSourcePath = toolEvidenceSourcePath(entry.meta.filePath, entry.meta.agent);
|
|
1405
|
+
const toolScan = toolSourcePath === entry.meta.filePath
|
|
1406
|
+
? entry.scan
|
|
1407
|
+
: (() => {
|
|
1408
|
+
const stat = fs.statSync(toolSourcePath);
|
|
1409
|
+
return { fileMtimeMs: stat.mtimeMs, fileSize: stat.size };
|
|
1410
|
+
})();
|
|
1411
|
+
// Non-resumable harnesses already parse here for todos/recent dirs. Derive
|
|
1412
|
+
// the tool index from those same in-memory events: no second file read.
|
|
1413
|
+
const events = parseSession(entry.meta.filePath, entry.meta.agent);
|
|
1414
|
+
writeResourceUsage(entry.meta.id, events, entry.meta.cwd);
|
|
1415
|
+
return {
|
|
1416
|
+
...entry,
|
|
1417
|
+
meta: {
|
|
1418
|
+
...entry.meta,
|
|
1419
|
+
todos: extractTodoProgressFromEvents(events),
|
|
1420
|
+
recentDirectoriesTouched: extractRecentDirectoriesTouched(events, entry.meta.cwd),
|
|
1421
|
+
},
|
|
1422
|
+
toolCalls: toolCallsFromEvents(events),
|
|
1423
|
+
toolScan,
|
|
1424
|
+
toolIndexMode: 'replace',
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
catch {
|
|
1428
|
+
return entry;
|
|
1429
|
+
}
|
|
1430
|
+
});
|
|
1431
|
+
const writtenEntries = [];
|
|
1192
1432
|
const txn = db.transaction((items) => {
|
|
1193
1433
|
// Re-read the ledger now that we hold the write lock. Any file committed
|
|
1194
1434
|
// by a concurrent process since our pre-scan is visible here.
|
|
@@ -1208,7 +1448,8 @@ export function upsertSessionsBatch(entries) {
|
|
|
1208
1448
|
}
|
|
1209
1449
|
}
|
|
1210
1450
|
}
|
|
1211
|
-
for (const
|
|
1451
|
+
for (const entry of items) {
|
|
1452
|
+
const { meta, content, scan, parserState, contentText } = entry;
|
|
1212
1453
|
if (alreadyIndexed.has(meta.id))
|
|
1213
1454
|
continue;
|
|
1214
1455
|
// Per-row guard: one malformed session (e.g. a required field that resolves to
|
|
@@ -1280,6 +1521,7 @@ export function upsertSessionsBatch(entries) {
|
|
|
1280
1521
|
if (scan && meta.filePath) {
|
|
1281
1522
|
ledger.run(canonicalLedgerKey(meta.filePath), scan.fileMtimeMs, scan.fileSize, now, parserState ?? null, contentText ?? null);
|
|
1282
1523
|
}
|
|
1524
|
+
writtenEntries.push(entry);
|
|
1283
1525
|
}
|
|
1284
1526
|
catch (err) {
|
|
1285
1527
|
if (process.stderr.isTTY) {
|
|
@@ -1289,6 +1531,20 @@ export function upsertSessionsBatch(entries) {
|
|
|
1289
1531
|
}
|
|
1290
1532
|
});
|
|
1291
1533
|
txn(enrichedEntries);
|
|
1534
|
+
// Tool evidence shares the transcript parse above but owns an independent
|
|
1535
|
+
// transaction/ledger. If this write fails, the normal session row remains
|
|
1536
|
+
// valid and ensureToolIndex retries from the missing tool ledger later.
|
|
1537
|
+
for (const entry of writtenEntries) {
|
|
1538
|
+
const toolScan = entry.toolScan ?? entry.scan;
|
|
1539
|
+
if (!toolScan || !entry.toolCalls)
|
|
1540
|
+
continue;
|
|
1541
|
+
try {
|
|
1542
|
+
persistToolCalls(db, entry.meta, entry.toolCalls, toolScan, entry.toolIndexMode ?? 'replace');
|
|
1543
|
+
}
|
|
1544
|
+
catch {
|
|
1545
|
+
// Boundary is intentionally retryable via tool_scan_ledger.
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1292
1548
|
}
|
|
1293
1549
|
/**
|
|
1294
1550
|
* Sync labels for a set of sessions. For each id in the map, if the stored
|
|
@@ -1637,13 +1893,26 @@ export function querySessions(options = {}) {
|
|
|
1637
1893
|
: 'ORDER BY IFNULL(last_activity, timestamp) DESC, timestamp DESC';
|
|
1638
1894
|
const sql = `SELECT * FROM sessions ${clause} ${orderClause} ${limitClause}`;
|
|
1639
1895
|
const rows = db.prepare(sql).all(...params);
|
|
1896
|
+
if (options.skipExistenceCheck) {
|
|
1897
|
+
const trimmed = options.limit ? rows.slice(0, options.limit) : rows;
|
|
1898
|
+
return trimmed.map(rowToMeta);
|
|
1899
|
+
}
|
|
1640
1900
|
// Belt-and-suspenders: drop rows whose JSONL no longer exists on disk. The
|
|
1641
1901
|
// authoritative fix is to keep file_path in sync (see updateSessionFilePaths
|
|
1642
1902
|
// callers), but skipping vanished rows here prevents phantom sessions from
|
|
1643
1903
|
// surfacing in the Factory UI if any code path forgets to rewrite (#136).
|
|
1644
1904
|
// Synthetic rows (OpenClaw channels/cron — see scanOpenClawIncremental) carry
|
|
1645
1905
|
// an empty file_path and are exempt; they're keyed by CLI output, not files.
|
|
1646
|
-
const
|
|
1906
|
+
const missing = rows.filter(r => r.file_path && !fs.existsSync(r.file_path));
|
|
1907
|
+
if (missing.length > 0) {
|
|
1908
|
+
const purge = db.transaction(() => {
|
|
1909
|
+
for (const row of missing)
|
|
1910
|
+
purgeToolCalls(db, row.id);
|
|
1911
|
+
});
|
|
1912
|
+
purge();
|
|
1913
|
+
}
|
|
1914
|
+
const missingIds = new Set(missing.map((row) => row.id));
|
|
1915
|
+
const live = rows.filter(r => !r.file_path || !missingIds.has(r.id));
|
|
1647
1916
|
const trimmed = options.limit ? live.slice(0, options.limit) : live;
|
|
1648
1917
|
return trimmed.map(rowToMeta);
|
|
1649
1918
|
}
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
import type { SessionAgentId, SessionEvent, SessionMeta, TodoProgress } from './types.js';
|
|
10
10
|
export { machineForSessionFile } from './origin-machine.js';
|
|
11
11
|
import { type ScanStamp } from './db.js';
|
|
12
|
+
import { ToolCallCollector, type IndexedToolCall, type ToolCallCollectorSnapshot } from './tool-calls.js';
|
|
13
|
+
/** One JSONL record may not force an unbounded string allocation. */
|
|
14
|
+
export declare const SESSION_JSONL_LINE_MAX_BYTES: number;
|
|
12
15
|
/** Options controlling which sessions to discover and how to report progress. */
|
|
13
16
|
export interface DiscoverOptions {
|
|
14
17
|
agent?: SessionAgentId;
|
|
@@ -27,6 +30,8 @@ export interface DiscoverOptions {
|
|
|
27
30
|
/** Match any session whose cwd equals this or is a descendant. Overrides `cwd`. */
|
|
28
31
|
cwdPrefix?: string;
|
|
29
32
|
limit?: number;
|
|
33
|
+
/** Internal indexed-query path: return every row in scope instead of the default page. */
|
|
34
|
+
unbounded?: boolean;
|
|
30
35
|
/** Filter sessions newer than this (ISO timestamp or "7d", "30d", "90d") */
|
|
31
36
|
since?: string;
|
|
32
37
|
/** Filter sessions older than this (ISO timestamp) */
|
|
@@ -39,6 +44,8 @@ export interface DiscoverOptions {
|
|
|
39
44
|
origin?: 'cli' | 'routine';
|
|
40
45
|
/** Column to order results by (all descending): 'timestamp' (default), 'cost', or 'duration'. */
|
|
41
46
|
sortBy?: 'timestamp' | 'cost' | 'duration';
|
|
47
|
+
/** Trust scan-ledger rows without stat'ing every returned transcript. */
|
|
48
|
+
skipExistenceCheck?: boolean;
|
|
42
49
|
/** Called as each agent makes parsing progress. Totals count only files that need re-parsing (cache misses). */
|
|
43
50
|
onProgress?: (progress: ScanProgress) => void;
|
|
44
51
|
/** Only sessions that invoked this skill (#12) — see QueryOptions.skill in session/db.ts. */
|
|
@@ -139,6 +146,22 @@ interface CodexSessionScan {
|
|
|
139
146
|
* TTL, no external lock files needed.
|
|
140
147
|
*/
|
|
141
148
|
export declare function discoverSessions(options?: DiscoverOptions): Promise<SessionMeta[]>;
|
|
149
|
+
/** Read the current SQLite snapshot without scanning or parsing transcript files. */
|
|
150
|
+
export declare function queryIndexedSessions(options?: DiscoverOptions, indexedOptions?: {
|
|
151
|
+
resolveLinear?: boolean;
|
|
152
|
+
skipExistenceCheck?: boolean;
|
|
153
|
+
}): Promise<SessionMeta[]>;
|
|
154
|
+
/**
|
|
155
|
+
* Drop unmanaged rows for agents that HAVE managed versions.
|
|
156
|
+
*
|
|
157
|
+
* Scoping happens here, at query time, rather than by narrowing the scan: the index
|
|
158
|
+
* stays complete, so `--unmanaged` needs no re-scan and every other consumer of the
|
|
159
|
+
* DB (watchdog, the Factory watcher, `--roots`) is unaffected.
|
|
160
|
+
*
|
|
161
|
+
* An agent with no managed versions is left alone entirely — someone who has never
|
|
162
|
+
* run `agents add` sees exactly what they saw before.
|
|
163
|
+
*/
|
|
164
|
+
export declare function scopeToManaged(sessions: SessionMeta[], agents: readonly SessionAgentId[], options?: DiscoverOptions): SessionMeta[];
|
|
142
165
|
/**
|
|
143
166
|
* How many agents' dotfile dirs we scan at once, and the minimum spacing between
|
|
144
167
|
* successive scan starts. A small bound + stagger turns a simultaneous bulk
|
|
@@ -370,6 +393,8 @@ export declare function readCodexMeta(filePath: string, resolveAccount?: () => s
|
|
|
370
393
|
content: string;
|
|
371
394
|
parserState?: string;
|
|
372
395
|
contentText?: string;
|
|
396
|
+
toolCalls?: IndexedToolCall[];
|
|
397
|
+
toolIndexMode: 'replace' | 'append';
|
|
373
398
|
} | null>;
|
|
374
399
|
/**
|
|
375
400
|
* Mutable accumulator for the Claude transcript reducer. One field per local
|
|
@@ -408,6 +433,7 @@ export interface ClaudeParseState {
|
|
|
408
433
|
plan?: string;
|
|
409
434
|
checklistEvents: SessionEvent[];
|
|
410
435
|
recentDirectoriesTouched: string[];
|
|
436
|
+
toolCollector: ToolCallCollector;
|
|
411
437
|
/** Skill-invocation tool_use events, held for extractSkills() at finalize (#12). */
|
|
412
438
|
skillEvents: SessionEvent[];
|
|
413
439
|
/** Slash-command events (user-typed <command-name> wrapper OR a SlashCommand
|
|
@@ -442,8 +468,9 @@ export declare function scanClaudeSession(filePath: string): Promise<ClaudeSessi
|
|
|
442
468
|
* exact even when the recent window is smaller than the true count.
|
|
443
469
|
*/
|
|
444
470
|
export interface ClaudeParserState {
|
|
445
|
-
v:
|
|
471
|
+
v: 2;
|
|
446
472
|
offset: number;
|
|
473
|
+
jsonlDroppingOversizedLine?: boolean;
|
|
447
474
|
timestamp?: string;
|
|
448
475
|
cwd?: string;
|
|
449
476
|
gitBranch?: string;
|
|
@@ -475,6 +502,7 @@ export interface ClaudeParserState {
|
|
|
475
502
|
contentText?: string;
|
|
476
503
|
checklistEvents: SessionEvent[];
|
|
477
504
|
recentDirectoriesTouched: string[];
|
|
505
|
+
toolCalls: ToolCallCollectorSnapshot;
|
|
478
506
|
/** #12: see ClaudeParseState.skillEvents/slashCommandEvents. */
|
|
479
507
|
skillEvents: SessionEvent[];
|
|
480
508
|
slashCommandEvents: SessionEvent[];
|
|
@@ -484,7 +512,7 @@ export interface ClaudeParserState {
|
|
|
484
512
|
* `offset` bytes consumed. Round-trips through {@link hydrateClaudeParseState}
|
|
485
513
|
* so incremental replay equals a full parse.
|
|
486
514
|
*/
|
|
487
|
-
export declare function serializeClaudeParserState(state: ClaudeParseState, offset: number): ClaudeParserState;
|
|
515
|
+
export declare function serializeClaudeParserState(state: ClaudeParseState, offset: number, jsonlDroppingOversizedLine?: boolean): ClaudeParserState;
|
|
488
516
|
/**
|
|
489
517
|
* Rebuild a live {@link ClaudeParseState} from a persisted continuation so that
|
|
490
518
|
* applying the appended lines yields the same accumulator a full parse would.
|
|
@@ -517,6 +545,7 @@ export declare function scanClaudeSessionIncremental(filePath: string, fromOffse
|
|
|
517
545
|
scan: ClaudeSessionScan;
|
|
518
546
|
newState: ClaudeParserState;
|
|
519
547
|
newOffset: number;
|
|
548
|
+
toolCalls: IndexedToolCall[];
|
|
520
549
|
}>;
|
|
521
550
|
/** Test seam: read the (incremental, full) Claude parse counters. */
|
|
522
551
|
export declare function __claudeScanBranchCountsForTest(): {
|
|
@@ -553,6 +582,7 @@ export interface CodexParseState {
|
|
|
553
582
|
spawnedTeam?: string;
|
|
554
583
|
checklistEvents: SessionEvent[];
|
|
555
584
|
recentDirectoriesTouched: string[];
|
|
585
|
+
toolCollector: ToolCallCollector;
|
|
556
586
|
/** #12: see ClaudeParseState.skillEvents. Empty in practice today — no
|
|
557
587
|
* verified Codex skill-invocation tool name — but wired for parity so a
|
|
558
588
|
* future confirmed tool name (or a literal 'SlashCommand' function_call)
|
|
@@ -586,8 +616,9 @@ export declare function finalizeCodexScan(state: CodexParseState): CodexSessionS
|
|
|
586
616
|
* finalize is identical after a resume.
|
|
587
617
|
*/
|
|
588
618
|
export interface CodexParserState {
|
|
589
|
-
v:
|
|
619
|
+
v: 2;
|
|
590
620
|
offset: number;
|
|
621
|
+
jsonlDroppingOversizedLine?: boolean;
|
|
591
622
|
sessionId?: string;
|
|
592
623
|
timestamp?: string;
|
|
593
624
|
cwd?: string;
|
|
@@ -610,13 +641,14 @@ export interface CodexParserState {
|
|
|
610
641
|
contentText?: string;
|
|
611
642
|
checklistEvents: SessionEvent[];
|
|
612
643
|
recentDirectoriesTouched: string[];
|
|
644
|
+
toolCalls: ToolCallCollectorSnapshot;
|
|
613
645
|
}
|
|
614
646
|
/**
|
|
615
647
|
* Snapshot a live {@link CodexParseState} into its serializable form at `offset`
|
|
616
648
|
* bytes consumed. Round-trips through {@link hydrateCodexParseState} so
|
|
617
649
|
* incremental replay equals a full parse.
|
|
618
650
|
*/
|
|
619
|
-
export declare function serializeCodexParserState(state: CodexParseState, offset: number): CodexParserState;
|
|
651
|
+
export declare function serializeCodexParserState(state: CodexParseState, offset: number, jsonlDroppingOversizedLine?: boolean): CodexParserState;
|
|
620
652
|
/**
|
|
621
653
|
* Rebuild a live {@link CodexParseState} from a persisted continuation so that
|
|
622
654
|
* applying the appended lines yields the same accumulator a full parse would.
|
|
@@ -635,9 +667,9 @@ export declare function hydrateCodexParseState(prior: CodexParserState): CodexPa
|
|
|
635
667
|
*
|
|
636
668
|
* Same trailing-line discipline as {@link scanClaudeSessionIncremental}: apply
|
|
637
669
|
* ONLY the run of newline-terminated lines (slice at the last `'\n'`), and set
|
|
638
|
-
* `newOffset = fromOffset + consumedBytes`.
|
|
639
|
-
*
|
|
640
|
-
*
|
|
670
|
+
* `newOffset = fromOffset + consumedBytes`. An ordinary tail after the last
|
|
671
|
+
* `'\n'` is deferred; an oversized one advances under a persisted discard bit.
|
|
672
|
+
* Codex `messageCount` is additive with
|
|
641
673
|
* NO dedup, so re-reading a still-unterminated complete line would double-count
|
|
642
674
|
* it; deferring prevents that (the bug class prix-cloud caught for Claude).
|
|
643
675
|
*/
|
|
@@ -645,6 +677,7 @@ export declare function scanCodexSessionIncremental(filePath: string, fromOffset
|
|
|
645
677
|
scan: CodexSessionScan;
|
|
646
678
|
newState: CodexParserState;
|
|
647
679
|
newOffset: number;
|
|
680
|
+
toolCalls: IndexedToolCall[];
|
|
648
681
|
}>;
|
|
649
682
|
/** Test seam: read the (incremental, full) Codex parse counters. */
|
|
650
683
|
export declare function __codexScanBranchCountsForTest(): {
|