@phnx-labs/agents-cli 1.22.53 → 1.22.55
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 +208 -0
- package/README.md +59 -9
- package/dist/bootstrap.js +55 -154
- package/dist/cli/command-registry.d.ts +5 -0
- package/dist/cli/command-registry.js +8 -1
- package/dist/commands/accounts.js +219 -173
- package/dist/commands/apply.js +6 -3
- package/dist/commands/auth-mint.d.ts +8 -0
- package/dist/commands/auth-mint.js +96 -0
- package/dist/commands/auth.js +5 -1
- package/dist/commands/browser.js +1 -1
- package/dist/commands/cost.js +8 -2
- package/dist/commands/daemon.js +2 -2
- package/dist/commands/doctor.js +6 -1
- package/dist/commands/exec.js +10 -8
- package/dist/commands/focus.d.ts +1 -0
- package/dist/commands/focus.js +2 -2
- package/dist/commands/go.d.ts +5 -4
- package/dist/commands/go.js +7 -7
- package/dist/commands/insights.js +9 -0
- package/dist/commands/monitors.js +85 -30
- package/dist/commands/output.js +8 -2
- package/dist/commands/repo.js +18 -0
- package/dist/commands/routines.js +31 -2
- package/dist/commands/secrets.js +33 -14
- package/dist/commands/sessions.d.ts +20 -12
- package/dist/commands/sessions.js +64 -20
- package/dist/commands/setup-accounts.d.ts +8 -0
- package/dist/commands/setup-accounts.js +47 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +11 -2
- package/dist/commands/share.d.ts +79 -3
- package/dist/commands/share.js +347 -18
- package/dist/commands/ssh.d.ts +7 -0
- package/dist/commands/ssh.js +18 -2
- package/dist/commands/status.js +14 -0
- package/dist/commands/view.d.ts +11 -1
- package/dist/commands/view.js +35 -7
- package/dist/lib/account-registry.js +15 -3
- package/dist/lib/accounting/rotate.d.ts +20 -6
- package/dist/lib/accounting/rotate.js +38 -7
- package/dist/lib/accounting/usage.d.ts +68 -1
- package/dist/lib/accounting/usage.js +116 -10
- package/dist/lib/agent-spec/agents.d.ts +5 -2
- package/dist/lib/agent-spec/agents.js +25 -7
- package/dist/lib/analytics/mix-commands.js +12 -6
- package/dist/lib/auth-mint.d.ts +150 -0
- package/dist/lib/auth-mint.js +434 -0
- package/dist/lib/browser/cdp.d.ts +1 -1
- package/dist/lib/browser/cdp.js +1 -1
- package/dist/lib/browser/ffmpeg.d.ts +12 -0
- package/dist/lib/browser/ffmpeg.js +184 -0
- package/dist/lib/browser/remote-control.d.ts +9 -7
- package/dist/lib/browser/remote-control.js +9 -7
- package/dist/lib/browser/service.js +119 -25
- package/dist/lib/claude-account-token.d.ts +10 -0
- package/dist/lib/claude-account-token.js +14 -4
- package/dist/lib/config-drift.d.ts +37 -0
- package/dist/lib/config-drift.js +72 -0
- package/dist/lib/daemon/auth-sync-service.d.ts +19 -0
- package/dist/lib/daemon/auth-sync-service.js +34 -0
- package/dist/lib/daemon/browser-task-reap-service.d.ts +14 -0
- package/dist/lib/daemon/browser-task-reap-service.js +26 -0
- package/dist/lib/daemon/daemon.js +87 -176
- package/dist/lib/daemon/heartbeat-service.d.ts +13 -0
- package/dist/lib/daemon/heartbeat-service.js +26 -0
- package/dist/lib/daemon/monitor-engine-service.d.ts +9 -5
- package/dist/lib/daemon/monitor-engine-service.js +15 -7
- package/dist/lib/daemon/runner.d.ts +15 -0
- package/dist/lib/daemon/runner.js +23 -0
- package/dist/lib/daemon/secrets-broker-service.d.ts +5 -4
- package/dist/lib/daemon/secrets-broker-service.js +17 -32
- package/dist/lib/daemon/service.d.ts +2 -2
- package/dist/lib/daemon/service.js +1 -1
- package/dist/lib/daemon/session-state-service.d.ts +21 -0
- package/dist/lib/daemon/session-state-service.js +34 -0
- package/dist/lib/daemon/supervisor.d.ts +17 -7
- package/dist/lib/daemon/supervisor.js +87 -14
- package/dist/lib/daemon/tmux-reap-service.d.ts +11 -0
- package/dist/lib/daemon/tmux-reap-service.js +28 -0
- package/dist/lib/daemon/webhook-receiver-service.d.ts +9 -0
- package/dist/lib/daemon/webhook-receiver-service.js +17 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +25 -0
- package/dist/lib/device-config.d.ts +3 -3
- package/dist/lib/device-config.js +5 -5
- package/dist/lib/devices/connect.d.ts +26 -0
- package/dist/lib/devices/connect.js +48 -1
- package/dist/lib/devices/doctor-findings.d.ts +5 -1
- package/dist/lib/devices/doctor-findings.js +19 -1
- package/dist/lib/devices/harness-inventory.js +5 -2
- package/dist/lib/exec.d.ts +28 -0
- package/dist/lib/exec.js +73 -7
- package/dist/lib/feed/feed.d.ts +1 -1
- package/dist/lib/feed/feed.js +23 -1
- package/dist/lib/feed-broadcast.js +1 -1
- package/dist/lib/fleet/apply.d.ts +11 -0
- package/dist/lib/fleet/apply.js +23 -3
- package/dist/lib/fleet/auth-sync.js +5 -3
- package/dist/lib/help.d.ts +9 -0
- package/dist/lib/help.js +29 -1
- package/dist/lib/hosts/passthrough.d.ts +1 -10
- package/dist/lib/hosts/passthrough.js +1 -13
- package/dist/lib/installations/versions.js +9 -1
- package/dist/lib/linux-userns.d.ts +58 -0
- package/dist/lib/linux-userns.js +116 -0
- package/dist/lib/memory.d.ts +26 -0
- package/dist/lib/memory.js +80 -1
- package/dist/lib/monitors/config.d.ts +11 -0
- package/dist/lib/monitors/config.js +8 -0
- package/dist/lib/monitors/engine.d.ts +5 -1
- package/dist/lib/monitors/engine.js +13 -4
- package/dist/lib/monitors/state.d.ts +37 -1
- package/dist/lib/monitors/state.js +79 -4
- package/dist/lib/permissions-registry.d.ts +2 -0
- package/dist/lib/permissions-registry.js +116 -14
- package/dist/lib/permissions.d.ts +5 -3
- package/dist/lib/permissions.js +25 -27
- package/dist/lib/profiles.d.ts +8 -7
- package/dist/lib/profiles.js +12 -0
- package/dist/lib/project-key.d.ts +9 -0
- package/dist/lib/project-key.js +11 -0
- package/dist/lib/scheduling/routines.d.ts +47 -0
- package/dist/lib/scheduling/routines.js +70 -1
- package/dist/lib/secrets/bundles.d.ts +35 -0
- package/dist/lib/secrets/bundles.js +78 -1
- package/dist/lib/secrets/push.d.ts +3 -8
- package/dist/lib/secrets/push.js +18 -14
- package/dist/lib/secrets/remote.d.ts +9 -18
- package/dist/lib/secrets/remote.js +11 -26
- package/dist/lib/secrets/reserved-sync.d.ts +65 -0
- package/dist/lib/secrets/reserved-sync.js +129 -0
- package/dist/lib/self-heal/checks/hook-manifest.d.ts +2 -0
- package/dist/lib/self-heal/checks/hook-manifest.js +56 -0
- package/dist/lib/self-heal/registry.js +4 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/session/active.js +1 -4
- package/dist/lib/session/db.d.ts +41 -5
- package/dist/lib/session/db.js +132 -30
- package/dist/lib/session/discover.d.ts +32 -4
- package/dist/lib/session/discover.js +119 -25
- package/dist/lib/session/insights.d.ts +14 -0
- package/dist/lib/session/insights.js +25 -2
- package/dist/lib/session/linear.js +1 -1
- package/dist/lib/session/shell-programs.d.ts +17 -0
- package/dist/lib/session/shell-programs.js +21 -0
- package/dist/lib/session/state.js +2 -1
- package/dist/lib/session/stream-render.js +2 -1
- package/dist/lib/session/tool-calls.js +2 -5
- package/dist/lib/session/trajectory-html.js +2 -1
- package/dist/lib/session/trajectory.js +3 -12
- package/dist/lib/session/types.d.ts +8 -0
- package/dist/lib/share/capture.js +11 -2
- package/dist/lib/share/publish.d.ts +56 -5
- package/dist/lib/share/publish.js +126 -18
- package/dist/lib/share/worker-template.d.ts +3 -12
- package/dist/lib/share/worker-template.js +860 -59
- package/dist/lib/startup/root-command.js +2 -1
- package/dist/lib/state.d.ts +16 -0
- package/dist/lib/state.js +178 -46
- package/dist/lib/sync-status.d.ts +4 -0
- package/dist/lib/sync-status.js +3 -0
- package/dist/lib/traces/classify.js +24 -19
- package/dist/lib/usage-refresh.js +2 -1
- package/dist/lib/view-types.d.ts +7 -0
- package/package.json +9 -2
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -12,21 +12,42 @@ import { type IndexedToolCall } from './tool-calls.js';
|
|
|
12
12
|
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
13
13
|
* assert against the constant instead of hardcoding a number that every bump
|
|
14
14
|
* then has to chase (docs/sessions.md calls the constant the source of truth). */
|
|
15
|
-
export declare const SCHEMA_VERSION =
|
|
15
|
+
export declare const SCHEMA_VERSION = 42;
|
|
16
|
+
/**
|
|
17
|
+
* Bump to force the content extractor (assistant-answer text, alongside the
|
|
18
|
+
* user-prompt text every harness already accumulates) to re-derive on every
|
|
19
|
+
* session's next scan. Unlike RESOURCE_INDEX_VERSION this is read by the
|
|
20
|
+
* change-detector itself (`filterChangedEntries`, discover.ts) via
|
|
21
|
+
* `scan_ledger.extractor_version` — a stored version below this one is treated
|
|
22
|
+
* as "changed" even when the file's (mtime, size) are unchanged, so bumping it
|
|
23
|
+
* here backfills every existing session's assistant text on its next scan
|
|
24
|
+
* without a `DELETE FROM scan_ledger` (which would also throw away the
|
|
25
|
+
* resumable parser_state/content_text for Claude/Codex).
|
|
26
|
+
*/
|
|
27
|
+
export declare const CONTENT_INDEX_VERSION = 1;
|
|
16
28
|
/**
|
|
17
29
|
* Bumping this invalidates every cached facet row without touching the schema
|
|
18
30
|
* version, so a change to the extraction logic (a new metric, a corrected bucket)
|
|
19
31
|
* re-derives on the next `agents insights` instead of silently reporting stale
|
|
20
32
|
* numbers alongside fresh ones. Same role as RESOURCE_INDEX_VERSION.
|
|
21
33
|
*/
|
|
22
|
-
/** Bump when facet extraction changes so cached rows recompute (
|
|
23
|
-
export declare const INSIGHTS_EXTRACTOR_VERSION =
|
|
24
|
-
|
|
34
|
+
/** Bump when facet extraction changes so cached rows recompute (shell-command-by-binary v7). */
|
|
35
|
+
export declare const INSIGHTS_EXTRACTOR_VERSION = 7;
|
|
36
|
+
/** Bump when classifyTopic's output changes so cached topics recompute (human task taxonomy v2). */
|
|
37
|
+
export declare const SESSION_TOPIC_EXTRACTOR_VERSION = 2;
|
|
25
38
|
/** File stat snapshot used to detect changes between scan runs. */
|
|
26
39
|
export interface ScanStamp {
|
|
27
40
|
fileMtimeMs: number;
|
|
28
41
|
fileSize: number;
|
|
29
42
|
scannedAt?: number;
|
|
43
|
+
/**
|
|
44
|
+
* `scan_ledger.extractor_version` as of the last scan, when read from the
|
|
45
|
+
* ledger (undefined for a freshly-computed stamp that hasn't been persisted
|
|
46
|
+
* yet). Compared against {@link CONTENT_INDEX_VERSION} by
|
|
47
|
+
* `filterChangedEntries` (discover.ts) to force a re-extract independent of
|
|
48
|
+
* (mtime, size).
|
|
49
|
+
*/
|
|
50
|
+
extractorVersion?: number | null;
|
|
30
51
|
}
|
|
31
52
|
/** Filter and pagination options for querying the sessions table. */
|
|
32
53
|
export interface QueryOptions {
|
|
@@ -163,6 +184,10 @@ interface ParserStateRow {
|
|
|
163
184
|
fileMtimeMs: number;
|
|
164
185
|
fileSize: number;
|
|
165
186
|
scannedAt: number;
|
|
187
|
+
/** See {@link ScanStamp.extractorVersion}. A mismatch vs CONTENT_INDEX_VERSION
|
|
188
|
+
* means this continuation predates the current content extractor and MUST
|
|
189
|
+
* be treated as absent (forcing a full re-parse) rather than resumed from. */
|
|
190
|
+
extractorVersion: number | null;
|
|
166
191
|
}
|
|
167
192
|
/**
|
|
168
193
|
* Bulk-load the resumable-parse continuation (parser_state + content_text) plus
|
|
@@ -208,11 +233,15 @@ export declare function recordDirScans(entries: Array<{
|
|
|
208
233
|
* Upsert a session row and replace its FTS5 content in a single transaction.
|
|
209
234
|
* `content` is the tokenizable user-prompt text; pass '' to leave the row unsearchable.
|
|
210
235
|
*/
|
|
211
|
-
export declare function upsertSession(meta: SessionMeta, content: string, scan?: ScanStamp): void;
|
|
236
|
+
export declare function upsertSession(meta: SessionMeta, content: string, scan?: ScanStamp, assistantContent?: string): void;
|
|
212
237
|
/** Batch-upsert sessions with their FTS5 content and scan stamps in a single transaction. */
|
|
213
238
|
export declare function upsertSessionsBatch(entries: Array<{
|
|
214
239
|
meta: SessionMeta;
|
|
215
240
|
content: string;
|
|
241
|
+
/** Assistant-answer text, accumulated the same way as `content` (the
|
|
242
|
+
* user-prompt text) but stored in session_text's own `assistant` column
|
|
243
|
+
* with a lower BM25 weight — see BM25_WEIGHTS. */
|
|
244
|
+
assistantContent?: string;
|
|
216
245
|
scan?: ScanStamp;
|
|
217
246
|
parserState?: string;
|
|
218
247
|
contentText?: string;
|
|
@@ -584,6 +613,13 @@ interface FtsHit {
|
|
|
584
613
|
sessionId: string;
|
|
585
614
|
score: number;
|
|
586
615
|
matchedTerms: string[];
|
|
616
|
+
/**
|
|
617
|
+
* A short bm25 `snippet()` excerpt around the best-matching column (label,
|
|
618
|
+
* topic, project, user content, or assistant answer), with the matched
|
|
619
|
+
* term(s) wrapped in `**…**`. Absent for a handle/label-tier hit (tiers 1-3
|
|
620
|
+
* below), which has no excerpt to show — the label itself IS the match.
|
|
621
|
+
*/
|
|
622
|
+
snippet?: string;
|
|
587
623
|
}
|
|
588
624
|
/**
|
|
589
625
|
* Escape a raw user query into a safe FTS5 MATCH expression.
|
package/dist/lib/session/db.js
CHANGED
|
@@ -27,7 +27,19 @@ const DB_PATH = getSessionsDbPath();
|
|
|
27
27
|
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
28
28
|
* assert against the constant instead of hardcoding a number that every bump
|
|
29
29
|
* then has to chase (docs/sessions.md calls the constant the source of truth). */
|
|
30
|
-
export const SCHEMA_VERSION =
|
|
30
|
+
export const SCHEMA_VERSION = 42;
|
|
31
|
+
/**
|
|
32
|
+
* Bump to force the content extractor (assistant-answer text, alongside the
|
|
33
|
+
* user-prompt text every harness already accumulates) to re-derive on every
|
|
34
|
+
* session's next scan. Unlike RESOURCE_INDEX_VERSION this is read by the
|
|
35
|
+
* change-detector itself (`filterChangedEntries`, discover.ts) via
|
|
36
|
+
* `scan_ledger.extractor_version` — a stored version below this one is treated
|
|
37
|
+
* as "changed" even when the file's (mtime, size) are unchanged, so bumping it
|
|
38
|
+
* here backfills every existing session's assistant text on its next scan
|
|
39
|
+
* without a `DELETE FROM scan_ledger` (which would also throw away the
|
|
40
|
+
* resumable parser_state/content_text for Claude/Codex).
|
|
41
|
+
*/
|
|
42
|
+
export const CONTENT_INDEX_VERSION = 1;
|
|
31
43
|
/**
|
|
32
44
|
* Bump to force `agents sessions backfill resources` to re-derive every
|
|
33
45
|
* session's skill/slash-command tallies on its next run (resource_scan_ledger
|
|
@@ -54,10 +66,15 @@ function canonicalLedgerKey(filePath) {
|
|
|
54
66
|
return filePath;
|
|
55
67
|
}
|
|
56
68
|
}
|
|
57
|
-
// BM25 column weights for session_text: label > topic > project > content
|
|
58
|
-
// Higher weights make matches in that column rank higher.
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
// BM25 column weights for session_text: label > topic > project > content >
|
|
70
|
+
// assistant. Higher weights make matches in that column rank higher.
|
|
71
|
+
// `assistant` (the agent's own answers) is weighted BELOW `content` (the
|
|
72
|
+
// user's prompts): a user's own words are a stronger signal of "this is the
|
|
73
|
+
// session I meant" than the agent echoing/paraphrasing them back, so an
|
|
74
|
+
// assistant-only match still surfaces but ranks behind an equivalent
|
|
75
|
+
// user-prompt match.
|
|
76
|
+
/** BM25 column weights for FTS5: label > topic > project > content > assistant. */
|
|
77
|
+
const BM25_WEIGHTS = [5.0, 2.0, 1.5, 1.0, 0.5];
|
|
61
78
|
/** DDL for the sessions database (tables, indexes, FTS5 virtual table). */
|
|
62
79
|
const SCHEMA = `
|
|
63
80
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
@@ -135,6 +152,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS session_text USING fts5(
|
|
|
135
152
|
topic,
|
|
136
153
|
project,
|
|
137
154
|
content,
|
|
155
|
+
assistant,
|
|
138
156
|
tokenize = 'unicode61 remove_diacritics 2'
|
|
139
157
|
);
|
|
140
158
|
|
|
@@ -158,7 +176,13 @@ CREATE TABLE IF NOT EXISTS scan_ledger (
|
|
|
158
176
|
-- doc so detectTicket + FTS can rebuild on append without re-reading the file.
|
|
159
177
|
-- Written by B-2; B-1 only defines + round-trips them.
|
|
160
178
|
parser_state TEXT,
|
|
161
|
-
content_text TEXT
|
|
179
|
+
content_text TEXT,
|
|
180
|
+
-- CONTENT_INDEX_VERSION this row's session_text content was last extracted
|
|
181
|
+
-- at. NULL (a pre-v42 row) never equals the current constant, so the
|
|
182
|
+
-- change-detector (filterChangedEntries) treats it as changed even when
|
|
183
|
+
-- (mtime, size) match — the lever that backfills assistant text into
|
|
184
|
+
-- existing sessions without wiping scan_ledger outright.
|
|
185
|
+
extractor_version INTEGER
|
|
162
186
|
);
|
|
163
187
|
|
|
164
188
|
-- Tracks the mtime + entry-count of every LEAF directory that directly holds
|
|
@@ -409,9 +433,10 @@ CREATE INDEX IF NOT EXISTS idx_computer_sessions_started ON computer_sessions(st
|
|
|
409
433
|
* re-derives on the next `agents insights` instead of silently reporting stale
|
|
410
434
|
* numbers alongside fresh ones. Same role as RESOURCE_INDEX_VERSION.
|
|
411
435
|
*/
|
|
412
|
-
/** Bump when facet extraction changes so cached rows recompute (
|
|
413
|
-
export const INSIGHTS_EXTRACTOR_VERSION =
|
|
414
|
-
|
|
436
|
+
/** Bump when facet extraction changes so cached rows recompute (shell-command-by-binary v7). */
|
|
437
|
+
export const INSIGHTS_EXTRACTOR_VERSION = 7;
|
|
438
|
+
/** Bump when classifyTopic's output changes so cached topics recompute (human task taxonomy v2). */
|
|
439
|
+
export const SESSION_TOPIC_EXTRACTOR_VERSION = 2;
|
|
415
440
|
const PREVIEW_EXTRACTOR_VERSION = 1;
|
|
416
441
|
let dbInstance = null;
|
|
417
442
|
/**
|
|
@@ -1085,6 +1110,48 @@ function migrateSchema(db, fromVersion) {
|
|
|
1085
1110
|
if (!cols.has('harness'))
|
|
1086
1111
|
db.exec(`ALTER TABLE sessions ADD COLUMN harness TEXT`);
|
|
1087
1112
|
}
|
|
1113
|
+
if (fromVersion < 42) {
|
|
1114
|
+
// v41 -> v42: index the agent's ANSWERS, not just the user's prompts.
|
|
1115
|
+
// session_text gains an `assistant` column (own FTS5 column, own lower
|
|
1116
|
+
// BM25 weight — see BM25_WEIGHTS) and scan_ledger gains `extractor_version`
|
|
1117
|
+
// so the change-detector can force a re-extract independent of
|
|
1118
|
+
// (mtime, size). FTS5 can't ALTER a virtual table's column set, but unlike
|
|
1119
|
+
// v1->v2 (which dropped the table and forced a blind full rescan of
|
|
1120
|
+
// everything), the existing label/topic/project/content in every row is
|
|
1121
|
+
// still exactly right — only `assistant` is missing. Rename the old table
|
|
1122
|
+
// out of the way, create the new 6-column one, and copy the old rows back
|
|
1123
|
+
// in (assistant defaults to '' until the extractor_version lever backfills
|
|
1124
|
+
// it on that row's next scan) — search over label/topic/project/content
|
|
1125
|
+
// never blacks out for the transient window it takes existing sessions to
|
|
1126
|
+
// get rescanned, which can be a long time for a session whose transcript
|
|
1127
|
+
// is otherwise cold.
|
|
1128
|
+
db.exec(`ALTER TABLE session_text RENAME TO session_text_v41`);
|
|
1129
|
+
db.exec(`
|
|
1130
|
+
CREATE VIRTUAL TABLE session_text USING fts5(
|
|
1131
|
+
session_id UNINDEXED,
|
|
1132
|
+
label,
|
|
1133
|
+
topic,
|
|
1134
|
+
project,
|
|
1135
|
+
content,
|
|
1136
|
+
assistant,
|
|
1137
|
+
tokenize = 'unicode61 remove_diacritics 2'
|
|
1138
|
+
);
|
|
1139
|
+
`);
|
|
1140
|
+
db.exec(`
|
|
1141
|
+
INSERT INTO session_text (session_id, label, topic, project, content, assistant)
|
|
1142
|
+
SELECT session_id, label, topic, project, content, '' FROM session_text_v41
|
|
1143
|
+
`);
|
|
1144
|
+
db.exec(`DROP TABLE session_text_v41`);
|
|
1145
|
+
const ledgerCols = db.prepare(`PRAGMA table_info(scan_ledger)`).all();
|
|
1146
|
+
if (!ledgerCols.some(c => c.name === 'extractor_version')) {
|
|
1147
|
+
db.exec(`ALTER TABLE scan_ledger ADD COLUMN extractor_version INTEGER`);
|
|
1148
|
+
}
|
|
1149
|
+
// No `DELETE FROM scan_ledger` — the new column is NULL on every existing
|
|
1150
|
+
// row, which already never equals CONTENT_INDEX_VERSION, so every session
|
|
1151
|
+
// re-extracts on its next scan while parser_state/content_text (the
|
|
1152
|
+
// Claude/Codex resumable continuation) stay intact for rows that don't
|
|
1153
|
+
// need a full reparse for any OTHER reason.
|
|
1154
|
+
}
|
|
1088
1155
|
}
|
|
1089
1156
|
/**
|
|
1090
1157
|
* Stamp `account_key` / `account_org` / `account` on every Claude row from its
|
|
@@ -1455,13 +1522,18 @@ export function getScanStampsForPaths(filePaths) {
|
|
|
1455
1522
|
const placeholders = chunk.map(() => '?').join(',');
|
|
1456
1523
|
const rows = db
|
|
1457
1524
|
.prepare(`
|
|
1458
|
-
SELECT file_path, file_mtime_ms, file_size, scanned_at
|
|
1525
|
+
SELECT file_path, file_mtime_ms, file_size, scanned_at, extractor_version
|
|
1459
1526
|
FROM scan_ledger
|
|
1460
1527
|
WHERE file_path IN (${placeholders})
|
|
1461
1528
|
`)
|
|
1462
1529
|
.all(...chunk);
|
|
1463
1530
|
for (const row of rows) {
|
|
1464
|
-
const stamp = {
|
|
1531
|
+
const stamp = {
|
|
1532
|
+
fileMtimeMs: row.file_mtime_ms,
|
|
1533
|
+
fileSize: row.file_size,
|
|
1534
|
+
scannedAt: row.scanned_at,
|
|
1535
|
+
extractorVersion: row.extractor_version,
|
|
1536
|
+
};
|
|
1465
1537
|
for (const original of canonicalToOriginals.get(row.file_path) || []) {
|
|
1466
1538
|
result.set(original, stamp);
|
|
1467
1539
|
}
|
|
@@ -1498,7 +1570,7 @@ export function getParserStatesForPaths(filePaths) {
|
|
|
1498
1570
|
const placeholders = chunk.map(() => '?').join(',');
|
|
1499
1571
|
const rows = db
|
|
1500
1572
|
.prepare(`
|
|
1501
|
-
SELECT file_path, file_mtime_ms, file_size, scanned_at, parser_state, content_text
|
|
1573
|
+
SELECT file_path, file_mtime_ms, file_size, scanned_at, parser_state, content_text, extractor_version
|
|
1502
1574
|
FROM scan_ledger
|
|
1503
1575
|
WHERE file_path IN (${placeholders})
|
|
1504
1576
|
`)
|
|
@@ -1510,6 +1582,7 @@ export function getParserStatesForPaths(filePaths) {
|
|
|
1510
1582
|
fileMtimeMs: row.file_mtime_ms,
|
|
1511
1583
|
fileSize: row.file_size,
|
|
1512
1584
|
scannedAt: row.scanned_at,
|
|
1585
|
+
extractorVersion: row.extractor_version,
|
|
1513
1586
|
};
|
|
1514
1587
|
for (const original of canonicalToOriginals.get(row.file_path) || []) {
|
|
1515
1588
|
result.set(original, state);
|
|
@@ -1527,17 +1600,23 @@ export function recordScans(entries) {
|
|
|
1527
1600
|
return;
|
|
1528
1601
|
const db = getDB();
|
|
1529
1602
|
const stmt = db.prepare(`
|
|
1530
|
-
INSERT INTO scan_ledger (file_path, file_mtime_ms, file_size, scanned_at)
|
|
1531
|
-
VALUES (?, ?, ?, ?)
|
|
1603
|
+
INSERT INTO scan_ledger (file_path, file_mtime_ms, file_size, scanned_at, extractor_version)
|
|
1604
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1532
1605
|
ON CONFLICT(file_path) DO UPDATE SET
|
|
1533
1606
|
file_mtime_ms = excluded.file_mtime_ms,
|
|
1534
1607
|
file_size = excluded.file_size,
|
|
1535
|
-
scanned_at = excluded.scanned_at
|
|
1608
|
+
scanned_at = excluded.scanned_at,
|
|
1609
|
+
extractor_version = excluded.extractor_version
|
|
1536
1610
|
`);
|
|
1537
1611
|
const now = Date.now();
|
|
1538
1612
|
const txn = db.transaction((items) => {
|
|
1539
1613
|
for (const { filePath, scan } of items) {
|
|
1540
|
-
|
|
1614
|
+
// Stamped at the CURRENT content extractor version even for a file that
|
|
1615
|
+
// yielded no session (malformed / no id): we ran today's extractor over
|
|
1616
|
+
// it and it produced nothing, so it is current, not stale — otherwise a
|
|
1617
|
+
// permanently-unparseable file would re-trigger "changed" on every scan
|
|
1618
|
+
// forever once CONTENT_INDEX_VERSION is bumped.
|
|
1619
|
+
stmt.run(canonicalLedgerKey(filePath), scan.fileMtimeMs, scan.fileSize, now, CONTENT_INDEX_VERSION);
|
|
1541
1620
|
}
|
|
1542
1621
|
});
|
|
1543
1622
|
txn(entries);
|
|
@@ -1867,7 +1946,7 @@ function enrichCachedSessionMeta(meta) {
|
|
|
1867
1946
|
}
|
|
1868
1947
|
}
|
|
1869
1948
|
const deleteTextStmt = (db) => db.prepare(`DELETE FROM session_text WHERE session_id = ?`);
|
|
1870
|
-
const insertTextStmt = (db) => db.prepare(`INSERT INTO session_text (session_id, label, topic, project, content) VALUES (?, ?, ?, ?, ?)`);
|
|
1949
|
+
const insertTextStmt = (db) => db.prepare(`INSERT INTO session_text (session_id, label, topic, project, content, assistant) VALUES (?, ?, ?, ?, ?, ?)`);
|
|
1871
1950
|
// Read back the label the upsert actually stored (which may be the preserved
|
|
1872
1951
|
// one, not the incoming blank) so the FTS label column stays consistent with
|
|
1873
1952
|
// sessions.label after a bare rescan.
|
|
@@ -1904,7 +1983,7 @@ function resolveMachine(meta) {
|
|
|
1904
1983
|
* Upsert a session row and replace its FTS5 content in a single transaction.
|
|
1905
1984
|
* `content` is the tokenizable user-prompt text; pass '' to leave the row unsearchable.
|
|
1906
1985
|
*/
|
|
1907
|
-
export function upsertSession(meta, content, scan) {
|
|
1986
|
+
export function upsertSession(meta, content, scan, assistantContent = '') {
|
|
1908
1987
|
meta = enrichCachedSessionMeta(meta);
|
|
1909
1988
|
// Join the durable sessionId -> actor sidecar (RUSH-2019) when the caller
|
|
1910
1989
|
// didn't already carry an actor, so a scanned transcript still attributes to a
|
|
@@ -1975,7 +2054,7 @@ export function upsertSession(meta, content, scan) {
|
|
|
1975
2054
|
insText.run(meta.id,
|
|
1976
2055
|
// Use the label the upsert actually stored (preserve-non-empty rule),
|
|
1977
2056
|
// not the raw incoming one, so FTS label ranking survives a bare rescan.
|
|
1978
|
-
storedFtsLabel(readLabel, meta.id), meta.topic ?? '', meta.project ?? '', content ?? '');
|
|
2057
|
+
storedFtsLabel(readLabel, meta.id), meta.topic ?? '', meta.project ?? '', content ?? '', assistantContent ?? '');
|
|
1979
2058
|
});
|
|
1980
2059
|
txn();
|
|
1981
2060
|
}
|
|
@@ -1993,19 +2072,23 @@ export function upsertSessionsBatch(entries) {
|
|
|
1993
2072
|
// stored owner on rescan.
|
|
1994
2073
|
const actorIndex = loadSessionActorIndex();
|
|
1995
2074
|
// Persist the Claude resumable-parse continuation (parser_state + content_text)
|
|
1996
|
-
// alongside the stamp
|
|
2075
|
+
// alongside the stamp, plus the CURRENT content extractor version — a caller
|
|
2076
|
+
// that reached this batch write ran today's extractor over the file, so the
|
|
2077
|
+
// ledger row is current regardless of which branch (full/incremental)
|
|
2078
|
+
// produced it. On a full/incremental Claude parse the caller passes the
|
|
1997
2079
|
// serialized newState + accumulated user doc so the NEXT scan can resume from
|
|
1998
2080
|
// the persisted offset (B-2). Other scanners pass neither, leaving both columns
|
|
1999
2081
|
// NULL exactly as before — their ledger rows are unaffected.
|
|
2000
2082
|
const ledger = db.prepare(`
|
|
2001
|
-
INSERT INTO scan_ledger (file_path, file_mtime_ms, file_size, scanned_at, parser_state, content_text)
|
|
2002
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
2083
|
+
INSERT INTO scan_ledger (file_path, file_mtime_ms, file_size, scanned_at, parser_state, content_text, extractor_version)
|
|
2084
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2003
2085
|
ON CONFLICT(file_path) DO UPDATE SET
|
|
2004
2086
|
file_mtime_ms = excluded.file_mtime_ms,
|
|
2005
2087
|
file_size = excluded.file_size,
|
|
2006
2088
|
scanned_at = excluded.scanned_at,
|
|
2007
2089
|
parser_state = excluded.parser_state,
|
|
2008
|
-
content_text = excluded.content_text
|
|
2090
|
+
content_text = excluded.content_text,
|
|
2091
|
+
extractor_version = excluded.extractor_version
|
|
2009
2092
|
`);
|
|
2010
2093
|
// Build a lookup from canonical file path → entry, used inside the write
|
|
2011
2094
|
// transaction to re-check the ledger AFTER acquiring the lock. When a
|
|
@@ -2076,17 +2159,26 @@ export function upsertSessionsBatch(entries) {
|
|
|
2076
2159
|
const chunk = paths.slice(i, i + CHUNK);
|
|
2077
2160
|
const phs = chunk.map(() => '?').join(',');
|
|
2078
2161
|
const rows = db
|
|
2079
|
-
.prepare(`SELECT file_path, file_mtime_ms, file_size FROM scan_ledger WHERE file_path IN (${phs})`)
|
|
2162
|
+
.prepare(`SELECT file_path, file_mtime_ms, file_size, extractor_version FROM scan_ledger WHERE file_path IN (${phs})`)
|
|
2080
2163
|
.all(...chunk);
|
|
2081
2164
|
for (const row of rows) {
|
|
2082
2165
|
const entry = byPath.get(row.file_path);
|
|
2083
|
-
|
|
2166
|
+
// A concurrent writer's row only makes THIS entry redundant when it is
|
|
2167
|
+
// current at CONTENT_INDEX_VERSION too — otherwise a (mtime, size) match
|
|
2168
|
+
// alone would make the version lever a no-op: the very reason this batch
|
|
2169
|
+
// was scheduled (a stale extractor_version) would be silently skipped as
|
|
2170
|
+
// "someone else already indexed it", when what they indexed predates the
|
|
2171
|
+
// current extractor.
|
|
2172
|
+
if (entry &&
|
|
2173
|
+
row.file_mtime_ms === entry.scan.fileMtimeMs &&
|
|
2174
|
+
row.file_size === entry.scan.fileSize &&
|
|
2175
|
+
row.extractor_version === CONTENT_INDEX_VERSION) {
|
|
2084
2176
|
alreadyIndexed.add(entry.meta.id);
|
|
2085
2177
|
}
|
|
2086
2178
|
}
|
|
2087
2179
|
}
|
|
2088
2180
|
for (const entry of items) {
|
|
2089
|
-
const { meta, content, scan, parserState, contentText } = entry;
|
|
2181
|
+
const { meta, content, assistantContent, scan, parserState, contentText } = entry;
|
|
2090
2182
|
if (alreadyIndexed.has(meta.id))
|
|
2091
2183
|
continue;
|
|
2092
2184
|
// Per-row guard: one malformed session (e.g. a required field that resolves to
|
|
@@ -2170,9 +2262,9 @@ export function upsertSessionsBatch(entries) {
|
|
|
2170
2262
|
insText.run(meta.id,
|
|
2171
2263
|
// Mirror upsertSession: index the label the upsert actually stored
|
|
2172
2264
|
// (preserve-non-empty rule), not the raw incoming one.
|
|
2173
|
-
storedFtsLabel(readLabel, meta.id), meta.topic ?? '', meta.project ?? '', content ?? '');
|
|
2265
|
+
storedFtsLabel(readLabel, meta.id), meta.topic ?? '', meta.project ?? '', content ?? '', assistantContent ?? '');
|
|
2174
2266
|
if (scan && meta.filePath) {
|
|
2175
|
-
ledger.run(canonicalLedgerKey(meta.filePath), scan.fileMtimeMs, scan.fileSize, now, parserState ?? null, contentText ?? null);
|
|
2267
|
+
ledger.run(canonicalLedgerKey(meta.filePath), scan.fileMtimeMs, scan.fileSize, now, parserState ?? null, contentText ?? null, CONTENT_INDEX_VERSION);
|
|
2176
2268
|
}
|
|
2177
2269
|
writtenEntries.push(entry);
|
|
2178
2270
|
}
|
|
@@ -3510,11 +3602,16 @@ export function ftsSearch(input, limit = 200) {
|
|
|
3510
3602
|
return hits.slice(0, limit);
|
|
3511
3603
|
}
|
|
3512
3604
|
// Tier 4: FTS5 content match, skipping anything already surfaced via label.
|
|
3605
|
+
// `snippet(session_text, -1, ...)` lets FTS5 pick the best-matching column
|
|
3606
|
+
// itself (label/topic/project/content/assistant) rather than us guessing —
|
|
3607
|
+
// a query that only matched in `assistant` (an agent-only answer) still gets
|
|
3608
|
+
// an excerpt from the right column instead of an empty content snippet.
|
|
3513
3609
|
if (expr) {
|
|
3514
3610
|
try {
|
|
3515
3611
|
const rows = db
|
|
3516
3612
|
.prepare(`
|
|
3517
|
-
SELECT session_id, bm25(session_text, ${BM25_WEIGHTS.join(', ')}) AS rank
|
|
3613
|
+
SELECT session_id, bm25(session_text, ${BM25_WEIGHTS.join(', ')}) AS rank,
|
|
3614
|
+
snippet(session_text, -1, '**', '**', '…', 12) AS snip
|
|
3518
3615
|
FROM session_text
|
|
3519
3616
|
WHERE session_text MATCH ?
|
|
3520
3617
|
ORDER BY rank ASC
|
|
@@ -3524,7 +3621,12 @@ export function ftsSearch(input, limit = 200) {
|
|
|
3524
3621
|
for (const r of rows) {
|
|
3525
3622
|
if (seen.has(r.session_id))
|
|
3526
3623
|
continue;
|
|
3527
|
-
hits.push({
|
|
3624
|
+
hits.push({
|
|
3625
|
+
sessionId: r.session_id,
|
|
3626
|
+
score: -r.rank,
|
|
3627
|
+
matchedTerms: terms,
|
|
3628
|
+
snippet: r.snip?.trim() || undefined,
|
|
3629
|
+
});
|
|
3528
3630
|
seen.add(r.session_id);
|
|
3529
3631
|
}
|
|
3530
3632
|
}
|
|
@@ -104,6 +104,8 @@ interface ClaudeSessionScan {
|
|
|
104
104
|
entrypoint?: string;
|
|
105
105
|
/** Concatenated user message text, ready to hand to FTS5. */
|
|
106
106
|
contentText?: string;
|
|
107
|
+
/** Concatenated assistant-answer text, ready to hand to FTS5's `assistant` column. */
|
|
108
|
+
assistantText?: string;
|
|
107
109
|
/** Durable state signals persisted to the index by the session-state engine. */
|
|
108
110
|
prUrl?: string;
|
|
109
111
|
prNumber?: number;
|
|
@@ -154,6 +156,7 @@ interface CodexSessionScan {
|
|
|
154
156
|
durationMs?: number;
|
|
155
157
|
lastActivity?: string;
|
|
156
158
|
contentText?: string;
|
|
159
|
+
assistantText?: string;
|
|
157
160
|
prUrl?: string;
|
|
158
161
|
prNumber?: number;
|
|
159
162
|
worktreeSlug?: string;
|
|
@@ -347,8 +350,15 @@ export declare function looksLikeSessionId(query: string): boolean;
|
|
|
347
350
|
*/
|
|
348
351
|
export declare function resolveSessionById(sessions: SessionMeta[], idQuery: string): SessionMeta[];
|
|
349
352
|
/**
|
|
350
|
-
* Run an FTS5 search over the DB and
|
|
351
|
-
*
|
|
353
|
+
* Run an FTS5 search over the DB and union hits with the given session list.
|
|
354
|
+
*
|
|
355
|
+
* The listing pool is a minority of the index — cwd-scoped, default-capped at
|
|
356
|
+
* 50, and skipping whole classes of indexed transcript — so intersecting FTS
|
|
357
|
+
* hits with it dropped grep-visible sessions that the index already found
|
|
358
|
+
* (PHNX-2767: `agents sessions "tmux pane"` returned 0 while the project
|
|
359
|
+
* transcripts matched). Hits already in the pool keep the caller's SessionMeta;
|
|
360
|
+
* hits the pool missed are hydrated from the index so a content query returns
|
|
361
|
+
* the transcript FTS matched.
|
|
352
362
|
*/
|
|
353
363
|
export declare function searchContentIndex(sessions: SessionMeta[], query: string): Map<string, SessionMeta>;
|
|
354
364
|
/**
|
|
@@ -375,6 +385,13 @@ interface PreStatEntry {
|
|
|
375
385
|
* stat. Same debounce and change-detection as filterChangedFiles; the raw
|
|
376
386
|
* mtime is floored here so warm files match the ledger exactly as the stat path
|
|
377
387
|
* does (Math.floor(stat.mtimeMs)).
|
|
388
|
+
*
|
|
389
|
+
* A file is also treated as "changed" — independent of (mtime, size) — when
|
|
390
|
+
* its ledger row's `extractor_version` is behind {@link CONTENT_INDEX_VERSION}.
|
|
391
|
+
* This is the lever that backfills a content-extractor improvement (e.g.
|
|
392
|
+
* indexing assistant answers, not just user prompts) into every already-scanned
|
|
393
|
+
* session on its next pass, without a `DELETE FROM scan_ledger` that would also
|
|
394
|
+
* discard the Claude/Codex resumable parser_state.
|
|
378
395
|
*/
|
|
379
396
|
export declare function filterChangedEntries(entries: PreStatEntry[]): Array<{
|
|
380
397
|
filePath: string;
|
|
@@ -429,9 +446,11 @@ export declare function parseCodexThreadNameIndex(raw: string): Map<string, stri
|
|
|
429
446
|
export declare function readCodexMeta(filePath: string, resolveAccount?: () => string | undefined, currentVersion?: string, scanStamp?: ScanStamp, priorRow?: {
|
|
430
447
|
parserState: string | null;
|
|
431
448
|
fileMtimeMs: number;
|
|
449
|
+
extractorVersion?: number | null;
|
|
432
450
|
}): Promise<{
|
|
433
451
|
meta: SessionMeta;
|
|
434
452
|
content: string;
|
|
453
|
+
assistantContent: string;
|
|
435
454
|
parserState?: string;
|
|
436
455
|
contentText?: string;
|
|
437
456
|
toolCalls?: IndexedToolCall[];
|
|
@@ -469,6 +488,10 @@ interface ClaudeParseState {
|
|
|
469
488
|
lastTsMs?: number;
|
|
470
489
|
seenAssistantIds: Set<string>;
|
|
471
490
|
userTexts: string[];
|
|
491
|
+
/** Assistant-answer text (#PHNX content-search), accumulated the same way as
|
|
492
|
+
* `userTexts` but indexed into session_text's own lower-weighted `assistant`
|
|
493
|
+
* column instead of `content` — see BM25_WEIGHTS. */
|
|
494
|
+
assistantTexts: string[];
|
|
472
495
|
sawPrCreate: boolean;
|
|
473
496
|
prUrl?: string;
|
|
474
497
|
prNumber?: number;
|
|
@@ -521,7 +544,7 @@ export declare function scanClaudeSession(filePath: string): Promise<ClaudeSessi
|
|
|
521
544
|
* exact even when the recent window is smaller than the true count.
|
|
522
545
|
*/
|
|
523
546
|
export interface ClaudeParserState {
|
|
524
|
-
v:
|
|
547
|
+
v: 5;
|
|
525
548
|
/**
|
|
526
549
|
* Fan-out tallies carried across a RESUMED parse (RUSH-3091/3095). They must
|
|
527
550
|
* live in the durable blob: the resumable parser reads only new bytes, so
|
|
@@ -566,6 +589,7 @@ export interface ClaudeParserState {
|
|
|
566
589
|
spawnedTeam?: string;
|
|
567
590
|
ticketId?: string;
|
|
568
591
|
contentText?: string;
|
|
592
|
+
assistantContentText?: string;
|
|
569
593
|
checklistEvents: SessionEvent[];
|
|
570
594
|
recentDirectoriesTouched: string[];
|
|
571
595
|
toolCalls: ToolCallCollectorSnapshot;
|
|
@@ -663,6 +687,8 @@ interface CodexParseState {
|
|
|
663
687
|
firstTsMs?: number;
|
|
664
688
|
lastTsMs?: number;
|
|
665
689
|
userTexts: string[];
|
|
690
|
+
/** See ClaudeParseState.assistantTexts — same accumulate-and-lower-weight FTS treatment. */
|
|
691
|
+
assistantTexts: string[];
|
|
666
692
|
sawPrCreate: boolean;
|
|
667
693
|
prUrl?: string;
|
|
668
694
|
prNumber?: number;
|
|
@@ -693,7 +719,7 @@ export declare function initCodexParseState(): CodexParseState;
|
|
|
693
719
|
* finalize is identical after a resume.
|
|
694
720
|
*/
|
|
695
721
|
export interface CodexParserState {
|
|
696
|
-
v:
|
|
722
|
+
v: 3;
|
|
697
723
|
offset: number;
|
|
698
724
|
jsonlDroppingOversizedLine?: boolean;
|
|
699
725
|
sessionId?: string;
|
|
@@ -716,6 +742,7 @@ export interface CodexParserState {
|
|
|
716
742
|
spawnedTeam?: string;
|
|
717
743
|
ticketId?: string;
|
|
718
744
|
contentText?: string;
|
|
745
|
+
assistantContentText?: string;
|
|
719
746
|
checklistEvents: SessionEvent[];
|
|
720
747
|
recentDirectoriesTouched: string[];
|
|
721
748
|
toolCalls: ToolCallCollectorSnapshot;
|
|
@@ -772,6 +799,7 @@ export declare function __resetCodexScanBranchCountsForTest(): void;
|
|
|
772
799
|
export declare function readCursorMeta(filePath: string, currentVersion?: string): {
|
|
773
800
|
meta: SessionMeta;
|
|
774
801
|
content: string;
|
|
802
|
+
assistantContent: string;
|
|
775
803
|
events: SessionEvent[];
|
|
776
804
|
} | null;
|
|
777
805
|
/** Parse a single Kimi session state.json file to extract session metadata. */
|