@phnx-labs/agents-cli 1.21.2 → 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 +101 -0
- package/README.md +32 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/computer-actions.d.ts +4 -0
- package/dist/commands/computer-actions.js +35 -0
- package/dist/commands/computer.js +4 -2
- 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/perf.d.ts +16 -0
- package/dist/commands/perf.js +11 -1
- package/dist/commands/projects.d.ts +11 -1
- package/dist/commands/projects.js +38 -4
- package/dist/commands/sessions-backfill.d.ts +32 -0
- package/dist/commands/sessions-backfill.js +186 -0
- package/dist/commands/sessions-picker.js +17 -2
- package/dist/commands/sessions.d.ts +22 -1
- package/dist/commands/sessions.js +331 -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/index.js +8 -0
- package/dist/lib/browser/service.js +13 -0
- package/dist/lib/computer/dispatch.d.ts +3 -1
- package/dist/lib/computer/dispatch.js +10 -2
- 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/event-stream.d.ts +2 -0
- package/dist/lib/event-stream.js +3 -0
- package/dist/lib/events.d.ts +3 -1
- package/dist/lib/events.js +4 -2
- package/dist/lib/exec.js +39 -8
- package/dist/lib/git.d.ts +14 -0
- package/dist/lib/git.js +36 -0
- package/dist/lib/hooks/profile.js +1 -14
- 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/percentile.d.ts +12 -0
- package/dist/lib/percentile.js +24 -0
- package/dist/lib/perf/db.d.ts +1 -2
- package/dist/lib/perf/db.js +2 -14
- package/dist/lib/plugins.js +12 -1
- 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/resources.d.ts +16 -0
- package/dist/lib/resources.js +25 -14
- package/dist/lib/rotate.d.ts +84 -1
- package/dist/lib/rotate.js +155 -5
- package/dist/lib/routines.js +1 -14
- package/dist/lib/runner.d.ts +4 -2
- package/dist/lib/runner.js +21 -5
- 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 +22 -1
- package/dist/lib/session/db.js +516 -24
- package/dist/lib/session/discover.d.ts +68 -7
- package/dist/lib/session/discover.js +186 -84
- package/dist/lib/session/highlights.d.ts +24 -4
- package/dist/lib/session/highlights.js +52 -7
- package/dist/lib/session/parse.d.ts +8 -1
- package/dist/lib/session/parse.js +102 -35
- package/dist/lib/session/prompt.d.ts +19 -0
- package/dist/lib/session/prompt.js +43 -0
- 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 +49 -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 +31 -0
- package/dist/lib/types.js +17 -0
- package/package.json +3 -1
package/dist/lib/session/db.js
CHANGED
|
@@ -12,14 +12,20 @@ import Database from '../sqlite.js';
|
|
|
12
12
|
import { parseSession } from './parse.js';
|
|
13
13
|
import { extractRecentDirectoriesTouched, extractTodoProgressFromEvents } from './state.js';
|
|
14
14
|
import { getSessionsDir, getSessionsDbPath } from '../state.js';
|
|
15
|
+
import { query as queryEvents } from '../events.js';
|
|
15
16
|
import { machineForSessionFile } from './origin-machine.js';
|
|
16
17
|
import { loadSessionActorIndex, readSessionActorRecord } from './actor-sidecar.js';
|
|
18
|
+
import { toolCallsFromEvents } from './tool-calls.js';
|
|
19
|
+
import { persistToolCalls, purgeToolCalls, toolEvidenceSourcePath } from './tool-store.js';
|
|
20
|
+
import { extractSkills, extractSlashCommands } from './highlights.js';
|
|
21
|
+
import { resolveResource } from '../resources.js';
|
|
22
|
+
import { discoverPlugins } from '../plugins.js';
|
|
17
23
|
const SESSIONS_DIR = getSessionsDir();
|
|
18
24
|
const DB_PATH = getSessionsDbPath();
|
|
19
25
|
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
20
26
|
* assert against the constant instead of hardcoding a number that every bump
|
|
21
27
|
* then has to chase (docs/05-sessions.md calls the constant the source of truth). */
|
|
22
|
-
export const SCHEMA_VERSION =
|
|
28
|
+
export const SCHEMA_VERSION = 30;
|
|
23
29
|
/**
|
|
24
30
|
* Canonicalize a file path for use as a scan_ledger key. The same physical
|
|
25
31
|
* session file is reachable via multiple aliases — `~/.claude/projects/x.jsonl`
|
|
@@ -85,7 +91,9 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
85
91
|
linear_project TEXT,
|
|
86
92
|
linear_project_url TEXT,
|
|
87
93
|
actor TEXT,
|
|
88
|
-
initiated_by TEXT
|
|
94
|
+
initiated_by TEXT,
|
|
95
|
+
used_browser INTEGER,
|
|
96
|
+
used_computer INTEGER
|
|
89
97
|
);
|
|
90
98
|
CREATE INDEX IF NOT EXISTS idx_sessions_timestamp ON sessions(timestamp DESC);
|
|
91
99
|
CREATE INDEX IF NOT EXISTS idx_sessions_cwd ON sessions(cwd);
|
|
@@ -140,6 +148,96 @@ CREATE TABLE IF NOT EXISTS dir_ledger (
|
|
|
140
148
|
entry_count INTEGER NOT NULL,
|
|
141
149
|
scanned_at INTEGER NOT NULL
|
|
142
150
|
);
|
|
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
|
+
|
|
219
|
+
-- Skill/slash-command usage per session (#12), computed from a session's
|
|
220
|
+
-- parsed transcript (extractSkills / extractSlashCommands, session/highlights.ts)
|
|
221
|
+
-- and joined at write time against the currently-installed resource/plugin
|
|
222
|
+
-- (resolveResource / discoverPlugins) for provenance — repo_root + snapshot_sha
|
|
223
|
+
-- answer "which repo, which commit installed this skill/command", plugin/source
|
|
224
|
+
-- answer "which plugin, which DotAgents layer". A resource renamed or uninstalled
|
|
225
|
+
-- since the session ran leaves plugin/source/repo_root/snapshot_sha NULL rather
|
|
226
|
+
-- than a stale guess. One row per (session, kind, name); count is how many
|
|
227
|
+
-- times that skill/command fired in the session.
|
|
228
|
+
CREATE TABLE IF NOT EXISTS session_resource_usage (
|
|
229
|
+
session_id TEXT NOT NULL,
|
|
230
|
+
kind TEXT NOT NULL,
|
|
231
|
+
name TEXT NOT NULL,
|
|
232
|
+
plugin TEXT,
|
|
233
|
+
source TEXT,
|
|
234
|
+
repo_root TEXT,
|
|
235
|
+
snapshot_sha TEXT,
|
|
236
|
+
count INTEGER NOT NULL DEFAULT 1,
|
|
237
|
+
PRIMARY KEY (session_id, kind, name)
|
|
238
|
+
);
|
|
239
|
+
CREATE INDEX IF NOT EXISTS idx_session_resource_usage_kind_name ON session_resource_usage(kind, name);
|
|
240
|
+
CREATE INDEX IF NOT EXISTS idx_session_resource_usage_plugin ON session_resource_usage(plugin);
|
|
143
241
|
`;
|
|
144
242
|
let dbInstance = null;
|
|
145
243
|
/**
|
|
@@ -361,12 +459,11 @@ function migrateSchema(db, fromVersion) {
|
|
|
361
459
|
.prepare(`SELECT id, agent, file_path FROM sessions WHERE machine IS NULL OR machine = ''`)
|
|
362
460
|
.all();
|
|
363
461
|
const upd = db.prepare(`UPDATE sessions SET machine = ? WHERE id = ?`);
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
369
|
-
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
|
+
}
|
|
370
467
|
}
|
|
371
468
|
if (fromVersion < 19) {
|
|
372
469
|
// v18 → v19: actor provenance (RUSH-2018) — who initiated the session and
|
|
@@ -401,11 +498,186 @@ function migrateSchema(db, fromVersion) {
|
|
|
401
498
|
db.exec(`DELETE FROM scan_ledger; DELETE FROM dir_ledger;`);
|
|
402
499
|
}
|
|
403
500
|
if (fromVersion < 22) {
|
|
501
|
+
// v21 → v22: persist the transcript's aggregate tool-call count.
|
|
404
502
|
const cols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
405
503
|
if (!cols.some(c => c.name === 'tool_call_count'))
|
|
406
504
|
db.exec(`ALTER TABLE sessions ADD COLUMN tool_call_count INTEGER`);
|
|
407
505
|
db.exec(`DELETE FROM scan_ledger; DELETE FROM dir_ledger;`);
|
|
408
506
|
}
|
|
507
|
+
if (fromVersion < 23) {
|
|
508
|
+
// v22 → v23: persist usedBrowser/usedComputer (#11) so the sessions picker
|
|
509
|
+
// preview can trust a positive detection instead of re-deriving it from a
|
|
510
|
+
// transcript regex on every render. Computed from a sessionId-scoped read
|
|
511
|
+
// of the events log (see detectToolUsage), not from the parsed transcript,
|
|
512
|
+
// so no ledger wipe is needed here — a rescan re-derives them regardless.
|
|
513
|
+
//
|
|
514
|
+
// Deliberately NO DEFAULT (NULL on ALTER, for every pre-existing row):
|
|
515
|
+
// NULL means "not yet computed by this scanner" (a legacy row), distinct
|
|
516
|
+
// from a real, computed 0/false.
|
|
517
|
+
const cols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
518
|
+
if (!cols.some(c => c.name === 'used_browser'))
|
|
519
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN used_browser INTEGER`);
|
|
520
|
+
if (!cols.some(c => c.name === 'used_computer'))
|
|
521
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN used_computer INTEGER`);
|
|
522
|
+
}
|
|
523
|
+
if (fromVersion < 24) {
|
|
524
|
+
// v23 → v24: session_resource_usage (#12) — skill/slash-command usage per
|
|
525
|
+
// session, joined against the currently-installed resource/plugin for
|
|
526
|
+
// provenance. No ledger wipe: writeResourceUsage() owns this table.
|
|
527
|
+
db.exec(`
|
|
528
|
+
CREATE TABLE IF NOT EXISTS session_resource_usage (
|
|
529
|
+
session_id TEXT NOT NULL,
|
|
530
|
+
kind TEXT NOT NULL,
|
|
531
|
+
name TEXT NOT NULL,
|
|
532
|
+
plugin TEXT,
|
|
533
|
+
source TEXT,
|
|
534
|
+
repo_root TEXT,
|
|
535
|
+
snapshot_sha TEXT,
|
|
536
|
+
count INTEGER NOT NULL DEFAULT 1,
|
|
537
|
+
PRIMARY KEY (session_id, kind, name)
|
|
538
|
+
);
|
|
539
|
+
CREATE INDEX IF NOT EXISTS idx_session_resource_usage_kind_name ON session_resource_usage(kind, name);
|
|
540
|
+
CREATE INDEX IF NOT EXISTS idx_session_resource_usage_plugin ON session_resource_usage(plugin);
|
|
541
|
+
`);
|
|
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
|
+
}
|
|
409
681
|
}
|
|
410
682
|
/** Open (or return the cached) sessions database, applying migrations as needed. */
|
|
411
683
|
export function getDB() {
|
|
@@ -413,24 +685,37 @@ export function getDB() {
|
|
|
413
685
|
return dbInstance;
|
|
414
686
|
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
415
687
|
const db = new Database(DB_PATH);
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
// 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
|
|
420
691
|
// agents (CLIs, skills, hooks) open this DB concurrently. The first scan of
|
|
421
692
|
// a new version home can take longer than 10s; concurrent callers need enough
|
|
422
693
|
// headroom to wait. The ledger-recheck in upsertSessionsBatch makes
|
|
423
694
|
// subsequent writers near-instant, so 30s is a rarely-reached safety net.
|
|
424
695
|
db.pragma('busy_timeout = 30000');
|
|
696
|
+
db.pragma('journal_mode = WAL');
|
|
697
|
+
db.pragma('synchronous = NORMAL');
|
|
698
|
+
db.pragma('temp_store = MEMORY');
|
|
425
699
|
db.exec(SCHEMA);
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
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) {
|
|
429
706
|
db.prepare(`INSERT OR IGNORE INTO meta(key, value) VALUES ('schema_version', ?)`).run(String(SCHEMA_VERSION));
|
|
430
707
|
}
|
|
431
708
|
else if (currentVersion < SCHEMA_VERSION) {
|
|
432
|
-
|
|
433
|
-
|
|
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();
|
|
434
719
|
}
|
|
435
720
|
// Index last_activity only after the column is guaranteed to exist — fresh DBs
|
|
436
721
|
// get it from CREATE TABLE above, existing pre-v8 DBs from the migration just
|
|
@@ -769,7 +1054,7 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
769
1054
|
file_path, file_mtime_ms, file_size, scanned_at, is_team_origin,
|
|
770
1055
|
pr_url, pr_number, worktree_slug, ticket_id, spawned_team, plan, todos,
|
|
771
1056
|
recent_directories_touched, linear_project, linear_project_url, machine,
|
|
772
|
-
actor, initiated_by
|
|
1057
|
+
actor, initiated_by, used_browser, used_computer
|
|
773
1058
|
) VALUES (
|
|
774
1059
|
@id, @short_id, @agent, @origin, @routine_name, @routine_run_id,
|
|
775
1060
|
@version, @account, @timestamp, @last_activity,
|
|
@@ -778,7 +1063,7 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
778
1063
|
@file_path, @file_mtime_ms, @file_size, @scanned_at, @is_team_origin,
|
|
779
1064
|
@pr_url, @pr_number, @worktree_slug, @ticket_id, @spawned_team, @plan, @todos,
|
|
780
1065
|
@recent_directories_touched, @linear_project, @linear_project_url, @machine,
|
|
781
|
-
@actor, @initiated_by
|
|
1066
|
+
@actor, @initiated_by, @used_browser, @used_computer
|
|
782
1067
|
)
|
|
783
1068
|
ON CONFLICT(id) DO UPDATE SET
|
|
784
1069
|
short_id = excluded.short_id,
|
|
@@ -823,6 +1108,8 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
823
1108
|
plan = excluded.plan,
|
|
824
1109
|
todos = excluded.todos,
|
|
825
1110
|
recent_directories_touched = excluded.recent_directories_touched,
|
|
1111
|
+
used_browser = excluded.used_browser,
|
|
1112
|
+
used_computer = excluded.used_computer,
|
|
826
1113
|
linear_project = CASE
|
|
827
1114
|
WHEN excluded.ticket_id IS NOT sessions.ticket_id THEN excluded.linear_project
|
|
828
1115
|
ELSE COALESCE(excluded.linear_project, sessions.linear_project)
|
|
@@ -841,11 +1128,124 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
841
1128
|
actor = COALESCE(sessions.actor, excluded.actor),
|
|
842
1129
|
initiated_by = COALESCE(sessions.initiated_by, excluded.initiated_by)
|
|
843
1130
|
`);
|
|
1131
|
+
/**
|
|
1132
|
+
* Did this session emit at least one browser/computer-automation event?
|
|
1133
|
+
* A scoped, sessionId-filtered read of the events log — NOT a re-scan of the
|
|
1134
|
+
* (potentially huge) transcript — since browser.navigate / browser.screenshot
|
|
1135
|
+
* / computer.action are recorded there via events.ts's emit(), keyed by the
|
|
1136
|
+
* same session id that is this row's `meta.id` (the provenance floor stamps
|
|
1137
|
+
* AGENT_SESSION_ID/AGENTS_SESSION_ID on every such event at emit time).
|
|
1138
|
+
*
|
|
1139
|
+
* Called independently of {@link enrichCachedSessionMeta} (which SKIPS
|
|
1140
|
+
* claude/codex entries in the batch path because their caller already parsed
|
|
1141
|
+
* the transcript for todos/recentDirectoriesTouched) — this never touches the
|
|
1142
|
+
* transcript, so it must run for every agent, every time.
|
|
1143
|
+
*/
|
|
1144
|
+
function detectToolUsage(sessionId) {
|
|
1145
|
+
const usedBrowser = queryEvents({ sessionId, eventTypes: ['browser.navigate', 'browser.screenshot'], limit: 1 }).length > 0;
|
|
1146
|
+
const usedComputer = queryEvents({ sessionId, eventTypes: ['computer.action'], limit: 1 }).length > 0;
|
|
1147
|
+
return { usedBrowser, usedComputer };
|
|
1148
|
+
}
|
|
1149
|
+
const deleteResourceUsageStmt = (db) => db.prepare(`DELETE FROM session_resource_usage WHERE session_id = ?`);
|
|
1150
|
+
const insertResourceUsageStmt = (db) => db.prepare(`
|
|
1151
|
+
INSERT INTO session_resource_usage (session_id, kind, name, plugin, source, repo_root, snapshot_sha, count)
|
|
1152
|
+
VALUES (@session_id, @kind, @name, @plugin, @source, @repo_root, @snapshot_sha, @count)
|
|
1153
|
+
`);
|
|
1154
|
+
/**
|
|
1155
|
+
* Resolve a skill/slash-command's provenance for `session_resource_usage`
|
|
1156
|
+
* (#12). A flat (non-namespaced) resource resolves via resolveResource()'s
|
|
1157
|
+
* project/user/system/extra-repo scan. A namespaced one (`plugin:name`, e.g.
|
|
1158
|
+
* `rush:design` — the shape both a plugin skill's `args.skill` and a plugin
|
|
1159
|
+
* command's SessionEvent.slashCommand carry) is plugin-owned and lives under
|
|
1160
|
+
* the plugin's own directory, invisible to resolveResource()'s flat scan —
|
|
1161
|
+
* resolved instead against the already-discovered plugin list. Neither found
|
|
1162
|
+
* (renamed or uninstalled since the session ran) returns all-undefined
|
|
1163
|
+
* rather than a stale guess.
|
|
1164
|
+
*/
|
|
1165
|
+
function resolveResourceProvenance(kind, name, cwd, plugins) {
|
|
1166
|
+
const listOf = (p) => (kind === 'skills' ? p.skills : p.commands);
|
|
1167
|
+
const colonIdx = name.indexOf(':');
|
|
1168
|
+
if (colonIdx > 0) {
|
|
1169
|
+
const pluginName = name.slice(0, colonIdx);
|
|
1170
|
+
const shortName = name.slice(colonIdx + 1);
|
|
1171
|
+
const plugin = plugins.find((p) => p.name === pluginName && listOf(p).includes(shortName));
|
|
1172
|
+
if (!plugin)
|
|
1173
|
+
return {};
|
|
1174
|
+
return { plugin: plugin.name, source: plugin.marketplace, repoRoot: plugin.repoRoot, snapshotSha: plugin.snapshotSha };
|
|
1175
|
+
}
|
|
1176
|
+
const resolved = resolveResource(kind, name, cwd);
|
|
1177
|
+
if (resolved)
|
|
1178
|
+
return { source: resolved.source, repoRoot: resolved.repoRoot, snapshotSha: resolved.snapshotSha };
|
|
1179
|
+
const plugin = plugins.find((p) => listOf(p).includes(name));
|
|
1180
|
+
if (!plugin)
|
|
1181
|
+
return {};
|
|
1182
|
+
return { plugin: plugin.name, source: plugin.marketplace, repoRoot: plugin.repoRoot, snapshotSha: plugin.snapshotSha };
|
|
1183
|
+
}
|
|
1184
|
+
/**
|
|
1185
|
+
* Persist already-computed skill/slash-command tallies for a session (#12)
|
|
1186
|
+
* into `session_resource_usage`, replacing any prior rows for it (a rescan's
|
|
1187
|
+
* usage supersedes the old — same replace-on-upsert shape as the FTS text
|
|
1188
|
+
* below). `discoverPlugins()` (real I/O: manifest + directory reads) only
|
|
1189
|
+
* runs when there is actually a skill/command to resolve, so a session with
|
|
1190
|
+
* neither pays nothing beyond the DELETE.
|
|
1191
|
+
*
|
|
1192
|
+
* Split from {@link writeResourceUsage} so a caller that already has the
|
|
1193
|
+
* tallies (claude/codex's incremental accumulator — see
|
|
1194
|
+
* ClaudeParseState.skillEvents/slashCommandEvents, threaded onto
|
|
1195
|
+
* SessionMeta.skillsUsed/slashCommandsUsed) can write without re-parsing the
|
|
1196
|
+
* transcript to re-derive them.
|
|
1197
|
+
*
|
|
1198
|
+
* Deliberately NOT wrapped in its own `db.transaction()`: better-sqlite3
|
|
1199
|
+
* (this repo's wrapper included) does not support nested transactions, and
|
|
1200
|
+
* `upsertSessionsBatch` calls this from INSIDE its own outer transaction. A
|
|
1201
|
+
* standalone caller (enrichCachedSessionMeta, outside any transaction) still
|
|
1202
|
+
* gets each statement committed individually — a crash between the DELETE
|
|
1203
|
+
* and an INSERT self-heals on the next rescan, the same risk profile as any
|
|
1204
|
+
* other un-batched write in this file.
|
|
1205
|
+
*/
|
|
1206
|
+
function writeResourceUsageFromTallies(sessionId, skills, commands, cwd) {
|
|
1207
|
+
const db = getDB();
|
|
1208
|
+
const del = deleteResourceUsageStmt(db);
|
|
1209
|
+
const ins = insertResourceUsageStmt(db);
|
|
1210
|
+
del.run(sessionId);
|
|
1211
|
+
if (skills.length === 0 && commands.length === 0)
|
|
1212
|
+
return;
|
|
1213
|
+
const plugins = discoverPlugins({ cwd });
|
|
1214
|
+
for (const { name, count } of skills) {
|
|
1215
|
+
const prov = resolveResourceProvenance('skills', name, cwd, plugins);
|
|
1216
|
+
ins.run({
|
|
1217
|
+
session_id: sessionId, kind: 'skill', name, count,
|
|
1218
|
+
plugin: prov.plugin ?? null, source: prov.source ?? null,
|
|
1219
|
+
repo_root: prov.repoRoot ?? null, snapshot_sha: prov.snapshotSha ?? null,
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
for (const { name, count } of commands) {
|
|
1223
|
+
const bare = name.replace(/^\//, '');
|
|
1224
|
+
const prov = resolveResourceProvenance('commands', bare, cwd, plugins);
|
|
1225
|
+
ins.run({
|
|
1226
|
+
session_id: sessionId, kind: 'command', name: bare, count,
|
|
1227
|
+
plugin: prov.plugin ?? null, source: prov.source ?? null,
|
|
1228
|
+
repo_root: prov.repoRoot ?? null, snapshot_sha: prov.snapshotSha ?? null,
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
/**
|
|
1233
|
+
* Persist skill/slash-command usage for a session (#12) by deriving the
|
|
1234
|
+
* tallies from a full parsed transcript. Used by {@link enrichCachedSessionMeta}
|
|
1235
|
+
* (every `upsertSession()` call, and `upsertSessionsBatch` for every harness
|
|
1236
|
+
* EXCEPT claude/codex, which pre-compute skillsUsed/slashCommandsUsed via
|
|
1237
|
+
* their incremental accumulator instead — see writeResourceUsageFromTallies
|
|
1238
|
+
* and the call site in upsertSessionsBatch).
|
|
1239
|
+
*/
|
|
1240
|
+
function writeResourceUsage(sessionId, events, cwd) {
|
|
1241
|
+
writeResourceUsageFromTallies(sessionId, extractSkills(events), extractSlashCommands(events), cwd);
|
|
1242
|
+
}
|
|
844
1243
|
function enrichCachedSessionMeta(meta) {
|
|
845
1244
|
if (!meta.filePath)
|
|
846
1245
|
return meta;
|
|
847
1246
|
try {
|
|
848
1247
|
const events = parseSession(meta.filePath, meta.agent);
|
|
1248
|
+
writeResourceUsage(meta.id, events, meta.cwd);
|
|
849
1249
|
return {
|
|
850
1250
|
...meta,
|
|
851
1251
|
todos: extractTodoProgressFromEvents(events),
|
|
@@ -903,6 +1303,7 @@ export function upsertSession(meta, content, scan) {
|
|
|
903
1303
|
// row AND backfills one indexed null-first (before its sidecar existed), while a
|
|
904
1304
|
// rescan carrying no actor still keeps the stored owner.
|
|
905
1305
|
const actorRec = meta.actor ? undefined : readSessionActorRecord(meta.id);
|
|
1306
|
+
const toolUsage = detectToolUsage(meta.id);
|
|
906
1307
|
const db = getDB();
|
|
907
1308
|
const { upsert, delText, insText, readLabel } = stmts(db);
|
|
908
1309
|
const row = {
|
|
@@ -946,6 +1347,8 @@ export function upsertSession(meta, content, scan) {
|
|
|
946
1347
|
machine: resolveMachine(meta),
|
|
947
1348
|
actor: meta.actor ?? actorRec?.actor ?? null,
|
|
948
1349
|
initiated_by: meta.initiatedBy ?? actorRec?.initiatedBy ?? null,
|
|
1350
|
+
used_browser: toolUsage.usedBrowser ? 1 : 0,
|
|
1351
|
+
used_computer: toolUsage.usedComputer ? 1 : 0,
|
|
949
1352
|
};
|
|
950
1353
|
const txn = db.transaction(() => {
|
|
951
1354
|
upsert.run(row);
|
|
@@ -994,9 +1397,38 @@ export function upsertSessionsBatch(entries) {
|
|
|
994
1397
|
const byPath = new Map(entries
|
|
995
1398
|
.filter(e => e.scan && e.meta.filePath)
|
|
996
1399
|
.map(e => [canonicalLedgerKey(e.meta.filePath), e]));
|
|
997
|
-
const enrichedEntries = entries.map(entry =>
|
|
998
|
-
|
|
999
|
-
|
|
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 = [];
|
|
1000
1432
|
const txn = db.transaction((items) => {
|
|
1001
1433
|
// Re-read the ledger now that we hold the write lock. Any file committed
|
|
1002
1434
|
// by a concurrent process since our pre-scan is visible here.
|
|
@@ -1016,7 +1448,8 @@ export function upsertSessionsBatch(entries) {
|
|
|
1016
1448
|
}
|
|
1017
1449
|
}
|
|
1018
1450
|
}
|
|
1019
|
-
for (const
|
|
1451
|
+
for (const entry of items) {
|
|
1452
|
+
const { meta, content, scan, parserState, contentText } = entry;
|
|
1020
1453
|
if (alreadyIndexed.has(meta.id))
|
|
1021
1454
|
continue;
|
|
1022
1455
|
// Per-row guard: one malformed session (e.g. a required field that resolves to
|
|
@@ -1026,6 +1459,15 @@ export function upsertSessionsBatch(entries) {
|
|
|
1026
1459
|
// back when the error escapes `fn`, so catching + skipping here leaves the txn valid
|
|
1027
1460
|
// and committable. We deliberately do NOT stamp the ledger for a skipped row, so the
|
|
1028
1461
|
// next scan re-tries it (self-healing once the underlying parser is fixed).
|
|
1462
|
+
const toolUsage = detectToolUsage(meta.id);
|
|
1463
|
+
// claude/codex skip enrichCachedSessionMeta above (preserving their
|
|
1464
|
+
// resumable-parse optimization) — write their pre-computed
|
|
1465
|
+
// skillsUsed/slashCommandsUsed (folded incrementally by discover.ts's
|
|
1466
|
+
// accumulator) here instead. Every other harness already got this
|
|
1467
|
+
// write from enrichCachedSessionMeta() in the .map() above.
|
|
1468
|
+
if (meta.agent === 'claude' || meta.agent === 'codex') {
|
|
1469
|
+
writeResourceUsageFromTallies(meta.id, meta.skillsUsed ?? [], meta.slashCommandsUsed ?? [], meta.cwd);
|
|
1470
|
+
}
|
|
1029
1471
|
try {
|
|
1030
1472
|
upsert.run({
|
|
1031
1473
|
id: meta.id,
|
|
@@ -1068,6 +1510,8 @@ export function upsertSessionsBatch(entries) {
|
|
|
1068
1510
|
machine: resolveMachine(meta),
|
|
1069
1511
|
actor: meta.actor ?? actorIndex.get(meta.id)?.actor ?? null,
|
|
1070
1512
|
initiated_by: meta.initiatedBy ?? actorIndex.get(meta.id)?.initiatedBy ?? null,
|
|
1513
|
+
used_browser: toolUsage.usedBrowser ? 1 : 0,
|
|
1514
|
+
used_computer: toolUsage.usedComputer ? 1 : 0,
|
|
1071
1515
|
});
|
|
1072
1516
|
delText.run(meta.id);
|
|
1073
1517
|
insText.run(meta.id,
|
|
@@ -1077,6 +1521,7 @@ export function upsertSessionsBatch(entries) {
|
|
|
1077
1521
|
if (scan && meta.filePath) {
|
|
1078
1522
|
ledger.run(canonicalLedgerKey(meta.filePath), scan.fileMtimeMs, scan.fileSize, now, parserState ?? null, contentText ?? null);
|
|
1079
1523
|
}
|
|
1524
|
+
writtenEntries.push(entry);
|
|
1080
1525
|
}
|
|
1081
1526
|
catch (err) {
|
|
1082
1527
|
if (process.stderr.isTTY) {
|
|
@@ -1086,6 +1531,20 @@ export function upsertSessionsBatch(entries) {
|
|
|
1086
1531
|
}
|
|
1087
1532
|
});
|
|
1088
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
|
+
}
|
|
1089
1548
|
}
|
|
1090
1549
|
/**
|
|
1091
1550
|
* Sync labels for a set of sessions. For each id in the map, if the stored
|
|
@@ -1260,6 +1719,11 @@ function rowToMeta(row) {
|
|
|
1260
1719
|
// Narrow the free-text column to the known kinds; an unexpected value maps
|
|
1261
1720
|
// to undefined rather than being asserted as a valid kind.
|
|
1262
1721
|
initiatedBy: row.initiated_by === 'human' || row.initiated_by === 'agent' ? row.initiated_by : undefined,
|
|
1722
|
+
// NULL = never computed by this scanner (legacy row) — leave undefined so
|
|
1723
|
+
// the sessions picker knows to fall back to the transcript-regex detection
|
|
1724
|
+
// instead of trusting a false "never used browser/computer".
|
|
1725
|
+
usedBrowser: row.used_browser === null ? undefined : row.used_browser === 1,
|
|
1726
|
+
usedComputer: row.used_computer === null ? undefined : row.used_computer === 1,
|
|
1263
1727
|
};
|
|
1264
1728
|
}
|
|
1265
1729
|
function parseJsonColumn(value) {
|
|
@@ -1392,6 +1856,21 @@ function buildSessionWhere(options) {
|
|
|
1392
1856
|
if (options.onlyTeamOrigin) {
|
|
1393
1857
|
where.push('IFNULL(is_team_origin, 0) = 1');
|
|
1394
1858
|
}
|
|
1859
|
+
// #12: join against session_resource_usage. A subquery IN, not a real JOIN
|
|
1860
|
+
// on the base SELECT, keeps `SELECT * FROM sessions` untouched for every
|
|
1861
|
+
// other caller of buildSessionWhere() (countSessions, the usage rollup, …)
|
|
1862
|
+
// that never wants a skill/plugin filter.
|
|
1863
|
+
if (options.skill) {
|
|
1864
|
+
where.push(`id IN (
|
|
1865
|
+
SELECT session_id FROM session_resource_usage
|
|
1866
|
+
WHERE kind = 'skill' AND (name = ? COLLATE NOCASE OR name LIKE ? COLLATE NOCASE)
|
|
1867
|
+
)`);
|
|
1868
|
+
params.push(options.skill, `%:${options.skill}`);
|
|
1869
|
+
}
|
|
1870
|
+
if (options.plugin) {
|
|
1871
|
+
where.push(`id IN (SELECT session_id FROM session_resource_usage WHERE plugin = ? COLLATE NOCASE)`);
|
|
1872
|
+
params.push(options.plugin);
|
|
1873
|
+
}
|
|
1395
1874
|
const clause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
|
|
1396
1875
|
return { clause, params };
|
|
1397
1876
|
}
|
|
@@ -1414,13 +1893,26 @@ export function querySessions(options = {}) {
|
|
|
1414
1893
|
: 'ORDER BY IFNULL(last_activity, timestamp) DESC, timestamp DESC';
|
|
1415
1894
|
const sql = `SELECT * FROM sessions ${clause} ${orderClause} ${limitClause}`;
|
|
1416
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
|
+
}
|
|
1417
1900
|
// Belt-and-suspenders: drop rows whose JSONL no longer exists on disk. The
|
|
1418
1901
|
// authoritative fix is to keep file_path in sync (see updateSessionFilePaths
|
|
1419
1902
|
// callers), but skipping vanished rows here prevents phantom sessions from
|
|
1420
1903
|
// surfacing in the Factory UI if any code path forgets to rewrite (#136).
|
|
1421
1904
|
// Synthetic rows (OpenClaw channels/cron — see scanOpenClawIncremental) carry
|
|
1422
1905
|
// an empty file_path and are exempt; they're keyed by CLI output, not files.
|
|
1423
|
-
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));
|
|
1424
1916
|
const trimmed = options.limit ? live.slice(0, options.limit) : live;
|
|
1425
1917
|
return trimmed.map(rowToMeta);
|
|
1426
1918
|
}
|