@phnx-labs/agents-cli 1.22.53 → 1.22.54
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 +190 -0
- package/README.md +41 -8
- 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/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 +52 -3
- package/dist/commands/share.js +262 -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 +3 -1
- package/dist/commands/view.js +5 -4
- 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 +37 -1
- package/dist/lib/accounting/usage.js +71 -6
- 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/remote-control.d.ts +9 -7
- package/dist/lib/browser/remote-control.js +9 -7
- 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/daemon.js +30 -4
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -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/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.js +8 -1
- 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/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 +39 -4
- package/dist/lib/session/db.js +130 -29
- 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/publish.d.ts +53 -5
- package/dist/lib/share/publish.js +99 -17
- package/dist/lib/share/worker-template.js +582 -57
- 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 +2 -0
- package/package.json +2 -1
|
@@ -45,6 +45,7 @@ import { classifyHostLink, HOST_HEARTBEAT_STALE_MS } from './host-link.js';
|
|
|
45
45
|
import { mapBounded } from '../concurrency.js';
|
|
46
46
|
import { linearIssueUrl } from './linear.js';
|
|
47
47
|
import { viewingInLabel } from './viewing-in.js';
|
|
48
|
+
import { claudeProjectDirName } from '../project-key.js';
|
|
48
49
|
const execFileAsync = promisify(execFile);
|
|
49
50
|
/**
|
|
50
51
|
* The owner (actor id) to show for a session in `--active`. Prefers the actor
|
|
@@ -530,10 +531,6 @@ function readLiveTerminals() {
|
|
|
530
531
|
}
|
|
531
532
|
return Array.from(merged.values());
|
|
532
533
|
}
|
|
533
|
-
/** Convert an absolute cwd to the Claude-project folder name (slashes and dots → dashes). */
|
|
534
|
-
function claudeProjectDirName(cwd) {
|
|
535
|
-
return cwd.replace(/[/.]/g, '-');
|
|
536
|
-
}
|
|
537
534
|
/**
|
|
538
535
|
* Process-local memo for Claude transcript path resolution. Each active-session
|
|
539
536
|
* poll re-walks every Claude version-home `projects/` tree for every live pid
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -12,21 +12,41 @@ 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 =
|
|
34
|
+
/** Bump when facet extraction changes so cached rows recompute (shell-command-by-binary v7). */
|
|
35
|
+
export declare const INSIGHTS_EXTRACTOR_VERSION = 7;
|
|
24
36
|
export declare const SESSION_TOPIC_EXTRACTOR_VERSION = 1;
|
|
25
37
|
/** File stat snapshot used to detect changes between scan runs. */
|
|
26
38
|
export interface ScanStamp {
|
|
27
39
|
fileMtimeMs: number;
|
|
28
40
|
fileSize: number;
|
|
29
41
|
scannedAt?: number;
|
|
42
|
+
/**
|
|
43
|
+
* `scan_ledger.extractor_version` as of the last scan, when read from the
|
|
44
|
+
* ledger (undefined for a freshly-computed stamp that hasn't been persisted
|
|
45
|
+
* yet). Compared against {@link CONTENT_INDEX_VERSION} by
|
|
46
|
+
* `filterChangedEntries` (discover.ts) to force a re-extract independent of
|
|
47
|
+
* (mtime, size).
|
|
48
|
+
*/
|
|
49
|
+
extractorVersion?: number | null;
|
|
30
50
|
}
|
|
31
51
|
/** Filter and pagination options for querying the sessions table. */
|
|
32
52
|
export interface QueryOptions {
|
|
@@ -163,6 +183,10 @@ interface ParserStateRow {
|
|
|
163
183
|
fileMtimeMs: number;
|
|
164
184
|
fileSize: number;
|
|
165
185
|
scannedAt: number;
|
|
186
|
+
/** See {@link ScanStamp.extractorVersion}. A mismatch vs CONTENT_INDEX_VERSION
|
|
187
|
+
* means this continuation predates the current content extractor and MUST
|
|
188
|
+
* be treated as absent (forcing a full re-parse) rather than resumed from. */
|
|
189
|
+
extractorVersion: number | null;
|
|
166
190
|
}
|
|
167
191
|
/**
|
|
168
192
|
* Bulk-load the resumable-parse continuation (parser_state + content_text) plus
|
|
@@ -208,11 +232,15 @@ export declare function recordDirScans(entries: Array<{
|
|
|
208
232
|
* Upsert a session row and replace its FTS5 content in a single transaction.
|
|
209
233
|
* `content` is the tokenizable user-prompt text; pass '' to leave the row unsearchable.
|
|
210
234
|
*/
|
|
211
|
-
export declare function upsertSession(meta: SessionMeta, content: string, scan?: ScanStamp): void;
|
|
235
|
+
export declare function upsertSession(meta: SessionMeta, content: string, scan?: ScanStamp, assistantContent?: string): void;
|
|
212
236
|
/** Batch-upsert sessions with their FTS5 content and scan stamps in a single transaction. */
|
|
213
237
|
export declare function upsertSessionsBatch(entries: Array<{
|
|
214
238
|
meta: SessionMeta;
|
|
215
239
|
content: string;
|
|
240
|
+
/** Assistant-answer text, accumulated the same way as `content` (the
|
|
241
|
+
* user-prompt text) but stored in session_text's own `assistant` column
|
|
242
|
+
* with a lower BM25 weight — see BM25_WEIGHTS. */
|
|
243
|
+
assistantContent?: string;
|
|
216
244
|
scan?: ScanStamp;
|
|
217
245
|
parserState?: string;
|
|
218
246
|
contentText?: string;
|
|
@@ -584,6 +612,13 @@ interface FtsHit {
|
|
|
584
612
|
sessionId: string;
|
|
585
613
|
score: number;
|
|
586
614
|
matchedTerms: string[];
|
|
615
|
+
/**
|
|
616
|
+
* A short bm25 `snippet()` excerpt around the best-matching column (label,
|
|
617
|
+
* topic, project, user content, or assistant answer), with the matched
|
|
618
|
+
* term(s) wrapped in `**…**`. Absent for a handle/label-tier hit (tiers 1-3
|
|
619
|
+
* below), which has no excerpt to show — the label itself IS the match.
|
|
620
|
+
*/
|
|
621
|
+
snippet?: string;
|
|
587
622
|
}
|
|
588
623
|
/**
|
|
589
624
|
* 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,8 +433,8 @@ 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 =
|
|
436
|
+
/** Bump when facet extraction changes so cached rows recompute (shell-command-by-binary v7). */
|
|
437
|
+
export const INSIGHTS_EXTRACTOR_VERSION = 7;
|
|
414
438
|
export const SESSION_TOPIC_EXTRACTOR_VERSION = 1;
|
|
415
439
|
const PREVIEW_EXTRACTOR_VERSION = 1;
|
|
416
440
|
let dbInstance = null;
|
|
@@ -1085,6 +1109,48 @@ function migrateSchema(db, fromVersion) {
|
|
|
1085
1109
|
if (!cols.has('harness'))
|
|
1086
1110
|
db.exec(`ALTER TABLE sessions ADD COLUMN harness TEXT`);
|
|
1087
1111
|
}
|
|
1112
|
+
if (fromVersion < 42) {
|
|
1113
|
+
// v41 -> v42: index the agent's ANSWERS, not just the user's prompts.
|
|
1114
|
+
// session_text gains an `assistant` column (own FTS5 column, own lower
|
|
1115
|
+
// BM25 weight — see BM25_WEIGHTS) and scan_ledger gains `extractor_version`
|
|
1116
|
+
// so the change-detector can force a re-extract independent of
|
|
1117
|
+
// (mtime, size). FTS5 can't ALTER a virtual table's column set, but unlike
|
|
1118
|
+
// v1->v2 (which dropped the table and forced a blind full rescan of
|
|
1119
|
+
// everything), the existing label/topic/project/content in every row is
|
|
1120
|
+
// still exactly right — only `assistant` is missing. Rename the old table
|
|
1121
|
+
// out of the way, create the new 6-column one, and copy the old rows back
|
|
1122
|
+
// in (assistant defaults to '' until the extractor_version lever backfills
|
|
1123
|
+
// it on that row's next scan) — search over label/topic/project/content
|
|
1124
|
+
// never blacks out for the transient window it takes existing sessions to
|
|
1125
|
+
// get rescanned, which can be a long time for a session whose transcript
|
|
1126
|
+
// is otherwise cold.
|
|
1127
|
+
db.exec(`ALTER TABLE session_text RENAME TO session_text_v41`);
|
|
1128
|
+
db.exec(`
|
|
1129
|
+
CREATE VIRTUAL TABLE session_text USING fts5(
|
|
1130
|
+
session_id UNINDEXED,
|
|
1131
|
+
label,
|
|
1132
|
+
topic,
|
|
1133
|
+
project,
|
|
1134
|
+
content,
|
|
1135
|
+
assistant,
|
|
1136
|
+
tokenize = 'unicode61 remove_diacritics 2'
|
|
1137
|
+
);
|
|
1138
|
+
`);
|
|
1139
|
+
db.exec(`
|
|
1140
|
+
INSERT INTO session_text (session_id, label, topic, project, content, assistant)
|
|
1141
|
+
SELECT session_id, label, topic, project, content, '' FROM session_text_v41
|
|
1142
|
+
`);
|
|
1143
|
+
db.exec(`DROP TABLE session_text_v41`);
|
|
1144
|
+
const ledgerCols = db.prepare(`PRAGMA table_info(scan_ledger)`).all();
|
|
1145
|
+
if (!ledgerCols.some(c => c.name === 'extractor_version')) {
|
|
1146
|
+
db.exec(`ALTER TABLE scan_ledger ADD COLUMN extractor_version INTEGER`);
|
|
1147
|
+
}
|
|
1148
|
+
// No `DELETE FROM scan_ledger` — the new column is NULL on every existing
|
|
1149
|
+
// row, which already never equals CONTENT_INDEX_VERSION, so every session
|
|
1150
|
+
// re-extracts on its next scan while parser_state/content_text (the
|
|
1151
|
+
// Claude/Codex resumable continuation) stay intact for rows that don't
|
|
1152
|
+
// need a full reparse for any OTHER reason.
|
|
1153
|
+
}
|
|
1088
1154
|
}
|
|
1089
1155
|
/**
|
|
1090
1156
|
* Stamp `account_key` / `account_org` / `account` on every Claude row from its
|
|
@@ -1455,13 +1521,18 @@ export function getScanStampsForPaths(filePaths) {
|
|
|
1455
1521
|
const placeholders = chunk.map(() => '?').join(',');
|
|
1456
1522
|
const rows = db
|
|
1457
1523
|
.prepare(`
|
|
1458
|
-
SELECT file_path, file_mtime_ms, file_size, scanned_at
|
|
1524
|
+
SELECT file_path, file_mtime_ms, file_size, scanned_at, extractor_version
|
|
1459
1525
|
FROM scan_ledger
|
|
1460
1526
|
WHERE file_path IN (${placeholders})
|
|
1461
1527
|
`)
|
|
1462
1528
|
.all(...chunk);
|
|
1463
1529
|
for (const row of rows) {
|
|
1464
|
-
const stamp = {
|
|
1530
|
+
const stamp = {
|
|
1531
|
+
fileMtimeMs: row.file_mtime_ms,
|
|
1532
|
+
fileSize: row.file_size,
|
|
1533
|
+
scannedAt: row.scanned_at,
|
|
1534
|
+
extractorVersion: row.extractor_version,
|
|
1535
|
+
};
|
|
1465
1536
|
for (const original of canonicalToOriginals.get(row.file_path) || []) {
|
|
1466
1537
|
result.set(original, stamp);
|
|
1467
1538
|
}
|
|
@@ -1498,7 +1569,7 @@ export function getParserStatesForPaths(filePaths) {
|
|
|
1498
1569
|
const placeholders = chunk.map(() => '?').join(',');
|
|
1499
1570
|
const rows = db
|
|
1500
1571
|
.prepare(`
|
|
1501
|
-
SELECT file_path, file_mtime_ms, file_size, scanned_at, parser_state, content_text
|
|
1572
|
+
SELECT file_path, file_mtime_ms, file_size, scanned_at, parser_state, content_text, extractor_version
|
|
1502
1573
|
FROM scan_ledger
|
|
1503
1574
|
WHERE file_path IN (${placeholders})
|
|
1504
1575
|
`)
|
|
@@ -1510,6 +1581,7 @@ export function getParserStatesForPaths(filePaths) {
|
|
|
1510
1581
|
fileMtimeMs: row.file_mtime_ms,
|
|
1511
1582
|
fileSize: row.file_size,
|
|
1512
1583
|
scannedAt: row.scanned_at,
|
|
1584
|
+
extractorVersion: row.extractor_version,
|
|
1513
1585
|
};
|
|
1514
1586
|
for (const original of canonicalToOriginals.get(row.file_path) || []) {
|
|
1515
1587
|
result.set(original, state);
|
|
@@ -1527,17 +1599,23 @@ export function recordScans(entries) {
|
|
|
1527
1599
|
return;
|
|
1528
1600
|
const db = getDB();
|
|
1529
1601
|
const stmt = db.prepare(`
|
|
1530
|
-
INSERT INTO scan_ledger (file_path, file_mtime_ms, file_size, scanned_at)
|
|
1531
|
-
VALUES (?, ?, ?, ?)
|
|
1602
|
+
INSERT INTO scan_ledger (file_path, file_mtime_ms, file_size, scanned_at, extractor_version)
|
|
1603
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1532
1604
|
ON CONFLICT(file_path) DO UPDATE SET
|
|
1533
1605
|
file_mtime_ms = excluded.file_mtime_ms,
|
|
1534
1606
|
file_size = excluded.file_size,
|
|
1535
|
-
scanned_at = excluded.scanned_at
|
|
1607
|
+
scanned_at = excluded.scanned_at,
|
|
1608
|
+
extractor_version = excluded.extractor_version
|
|
1536
1609
|
`);
|
|
1537
1610
|
const now = Date.now();
|
|
1538
1611
|
const txn = db.transaction((items) => {
|
|
1539
1612
|
for (const { filePath, scan } of items) {
|
|
1540
|
-
|
|
1613
|
+
// Stamped at the CURRENT content extractor version even for a file that
|
|
1614
|
+
// yielded no session (malformed / no id): we ran today's extractor over
|
|
1615
|
+
// it and it produced nothing, so it is current, not stale — otherwise a
|
|
1616
|
+
// permanently-unparseable file would re-trigger "changed" on every scan
|
|
1617
|
+
// forever once CONTENT_INDEX_VERSION is bumped.
|
|
1618
|
+
stmt.run(canonicalLedgerKey(filePath), scan.fileMtimeMs, scan.fileSize, now, CONTENT_INDEX_VERSION);
|
|
1541
1619
|
}
|
|
1542
1620
|
});
|
|
1543
1621
|
txn(entries);
|
|
@@ -1867,7 +1945,7 @@ function enrichCachedSessionMeta(meta) {
|
|
|
1867
1945
|
}
|
|
1868
1946
|
}
|
|
1869
1947
|
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 (?, ?, ?, ?, ?)`);
|
|
1948
|
+
const insertTextStmt = (db) => db.prepare(`INSERT INTO session_text (session_id, label, topic, project, content, assistant) VALUES (?, ?, ?, ?, ?, ?)`);
|
|
1871
1949
|
// Read back the label the upsert actually stored (which may be the preserved
|
|
1872
1950
|
// one, not the incoming blank) so the FTS label column stays consistent with
|
|
1873
1951
|
// sessions.label after a bare rescan.
|
|
@@ -1904,7 +1982,7 @@ function resolveMachine(meta) {
|
|
|
1904
1982
|
* Upsert a session row and replace its FTS5 content in a single transaction.
|
|
1905
1983
|
* `content` is the tokenizable user-prompt text; pass '' to leave the row unsearchable.
|
|
1906
1984
|
*/
|
|
1907
|
-
export function upsertSession(meta, content, scan) {
|
|
1985
|
+
export function upsertSession(meta, content, scan, assistantContent = '') {
|
|
1908
1986
|
meta = enrichCachedSessionMeta(meta);
|
|
1909
1987
|
// Join the durable sessionId -> actor sidecar (RUSH-2019) when the caller
|
|
1910
1988
|
// didn't already carry an actor, so a scanned transcript still attributes to a
|
|
@@ -1975,7 +2053,7 @@ export function upsertSession(meta, content, scan) {
|
|
|
1975
2053
|
insText.run(meta.id,
|
|
1976
2054
|
// Use the label the upsert actually stored (preserve-non-empty rule),
|
|
1977
2055
|
// not the raw incoming one, so FTS label ranking survives a bare rescan.
|
|
1978
|
-
storedFtsLabel(readLabel, meta.id), meta.topic ?? '', meta.project ?? '', content ?? '');
|
|
2056
|
+
storedFtsLabel(readLabel, meta.id), meta.topic ?? '', meta.project ?? '', content ?? '', assistantContent ?? '');
|
|
1979
2057
|
});
|
|
1980
2058
|
txn();
|
|
1981
2059
|
}
|
|
@@ -1993,19 +2071,23 @@ export function upsertSessionsBatch(entries) {
|
|
|
1993
2071
|
// stored owner on rescan.
|
|
1994
2072
|
const actorIndex = loadSessionActorIndex();
|
|
1995
2073
|
// Persist the Claude resumable-parse continuation (parser_state + content_text)
|
|
1996
|
-
// alongside the stamp
|
|
2074
|
+
// alongside the stamp, plus the CURRENT content extractor version — a caller
|
|
2075
|
+
// that reached this batch write ran today's extractor over the file, so the
|
|
2076
|
+
// ledger row is current regardless of which branch (full/incremental)
|
|
2077
|
+
// produced it. On a full/incremental Claude parse the caller passes the
|
|
1997
2078
|
// serialized newState + accumulated user doc so the NEXT scan can resume from
|
|
1998
2079
|
// the persisted offset (B-2). Other scanners pass neither, leaving both columns
|
|
1999
2080
|
// NULL exactly as before — their ledger rows are unaffected.
|
|
2000
2081
|
const ledger = db.prepare(`
|
|
2001
|
-
INSERT INTO scan_ledger (file_path, file_mtime_ms, file_size, scanned_at, parser_state, content_text)
|
|
2002
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
2082
|
+
INSERT INTO scan_ledger (file_path, file_mtime_ms, file_size, scanned_at, parser_state, content_text, extractor_version)
|
|
2083
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2003
2084
|
ON CONFLICT(file_path) DO UPDATE SET
|
|
2004
2085
|
file_mtime_ms = excluded.file_mtime_ms,
|
|
2005
2086
|
file_size = excluded.file_size,
|
|
2006
2087
|
scanned_at = excluded.scanned_at,
|
|
2007
2088
|
parser_state = excluded.parser_state,
|
|
2008
|
-
content_text = excluded.content_text
|
|
2089
|
+
content_text = excluded.content_text,
|
|
2090
|
+
extractor_version = excluded.extractor_version
|
|
2009
2091
|
`);
|
|
2010
2092
|
// Build a lookup from canonical file path → entry, used inside the write
|
|
2011
2093
|
// transaction to re-check the ledger AFTER acquiring the lock. When a
|
|
@@ -2076,17 +2158,26 @@ export function upsertSessionsBatch(entries) {
|
|
|
2076
2158
|
const chunk = paths.slice(i, i + CHUNK);
|
|
2077
2159
|
const phs = chunk.map(() => '?').join(',');
|
|
2078
2160
|
const rows = db
|
|
2079
|
-
.prepare(`SELECT file_path, file_mtime_ms, file_size FROM scan_ledger WHERE file_path IN (${phs})`)
|
|
2161
|
+
.prepare(`SELECT file_path, file_mtime_ms, file_size, extractor_version FROM scan_ledger WHERE file_path IN (${phs})`)
|
|
2080
2162
|
.all(...chunk);
|
|
2081
2163
|
for (const row of rows) {
|
|
2082
2164
|
const entry = byPath.get(row.file_path);
|
|
2083
|
-
|
|
2165
|
+
// A concurrent writer's row only makes THIS entry redundant when it is
|
|
2166
|
+
// current at CONTENT_INDEX_VERSION too — otherwise a (mtime, size) match
|
|
2167
|
+
// alone would make the version lever a no-op: the very reason this batch
|
|
2168
|
+
// was scheduled (a stale extractor_version) would be silently skipped as
|
|
2169
|
+
// "someone else already indexed it", when what they indexed predates the
|
|
2170
|
+
// current extractor.
|
|
2171
|
+
if (entry &&
|
|
2172
|
+
row.file_mtime_ms === entry.scan.fileMtimeMs &&
|
|
2173
|
+
row.file_size === entry.scan.fileSize &&
|
|
2174
|
+
row.extractor_version === CONTENT_INDEX_VERSION) {
|
|
2084
2175
|
alreadyIndexed.add(entry.meta.id);
|
|
2085
2176
|
}
|
|
2086
2177
|
}
|
|
2087
2178
|
}
|
|
2088
2179
|
for (const entry of items) {
|
|
2089
|
-
const { meta, content, scan, parserState, contentText } = entry;
|
|
2180
|
+
const { meta, content, assistantContent, scan, parserState, contentText } = entry;
|
|
2090
2181
|
if (alreadyIndexed.has(meta.id))
|
|
2091
2182
|
continue;
|
|
2092
2183
|
// Per-row guard: one malformed session (e.g. a required field that resolves to
|
|
@@ -2170,9 +2261,9 @@ export function upsertSessionsBatch(entries) {
|
|
|
2170
2261
|
insText.run(meta.id,
|
|
2171
2262
|
// Mirror upsertSession: index the label the upsert actually stored
|
|
2172
2263
|
// (preserve-non-empty rule), not the raw incoming one.
|
|
2173
|
-
storedFtsLabel(readLabel, meta.id), meta.topic ?? '', meta.project ?? '', content ?? '');
|
|
2264
|
+
storedFtsLabel(readLabel, meta.id), meta.topic ?? '', meta.project ?? '', content ?? '', assistantContent ?? '');
|
|
2174
2265
|
if (scan && meta.filePath) {
|
|
2175
|
-
ledger.run(canonicalLedgerKey(meta.filePath), scan.fileMtimeMs, scan.fileSize, now, parserState ?? null, contentText ?? null);
|
|
2266
|
+
ledger.run(canonicalLedgerKey(meta.filePath), scan.fileMtimeMs, scan.fileSize, now, parserState ?? null, contentText ?? null, CONTENT_INDEX_VERSION);
|
|
2176
2267
|
}
|
|
2177
2268
|
writtenEntries.push(entry);
|
|
2178
2269
|
}
|
|
@@ -3510,11 +3601,16 @@ export function ftsSearch(input, limit = 200) {
|
|
|
3510
3601
|
return hits.slice(0, limit);
|
|
3511
3602
|
}
|
|
3512
3603
|
// Tier 4: FTS5 content match, skipping anything already surfaced via label.
|
|
3604
|
+
// `snippet(session_text, -1, ...)` lets FTS5 pick the best-matching column
|
|
3605
|
+
// itself (label/topic/project/content/assistant) rather than us guessing —
|
|
3606
|
+
// a query that only matched in `assistant` (an agent-only answer) still gets
|
|
3607
|
+
// an excerpt from the right column instead of an empty content snippet.
|
|
3513
3608
|
if (expr) {
|
|
3514
3609
|
try {
|
|
3515
3610
|
const rows = db
|
|
3516
3611
|
.prepare(`
|
|
3517
|
-
SELECT session_id, bm25(session_text, ${BM25_WEIGHTS.join(', ')}) AS rank
|
|
3612
|
+
SELECT session_id, bm25(session_text, ${BM25_WEIGHTS.join(', ')}) AS rank,
|
|
3613
|
+
snippet(session_text, -1, '**', '**', '…', 12) AS snip
|
|
3518
3614
|
FROM session_text
|
|
3519
3615
|
WHERE session_text MATCH ?
|
|
3520
3616
|
ORDER BY rank ASC
|
|
@@ -3524,7 +3620,12 @@ export function ftsSearch(input, limit = 200) {
|
|
|
3524
3620
|
for (const r of rows) {
|
|
3525
3621
|
if (seen.has(r.session_id))
|
|
3526
3622
|
continue;
|
|
3527
|
-
hits.push({
|
|
3623
|
+
hits.push({
|
|
3624
|
+
sessionId: r.session_id,
|
|
3625
|
+
score: -r.rank,
|
|
3626
|
+
matchedTerms: terms,
|
|
3627
|
+
snippet: r.snip?.trim() || undefined,
|
|
3628
|
+
});
|
|
3528
3629
|
seen.add(r.session_id);
|
|
3529
3630
|
}
|
|
3530
3631
|
}
|
|
@@ -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. */
|