@phnx-labs/agents-cli 1.22.52 → 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 +336 -0
- package/README.md +42 -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 +220 -174
- 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 +26 -17
- package/dist/commands/fleet-capture.js +7 -0
- package/dist/commands/focus.d.ts +1 -0
- package/dist/commands/focus.js +4 -2
- package/dist/commands/go.d.ts +5 -4
- package/dist/commands/go.js +8 -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-inject.js +8 -3
- package/dist/commands/sessions-picker.js +2 -1
- package/dist/commands/sessions.d.ts +20 -12
- package/dist/commands/sessions.js +94 -40
- 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 +53 -14
- package/dist/commands/status.js +14 -0
- package/dist/commands/sync.js +44 -0
- package/dist/commands/view.d.ts +3 -1
- package/dist/commands/view.js +5 -4
- package/dist/lib/account-registry.d.ts +15 -5
- package/dist/lib/account-registry.js +165 -53
- 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/answer-router.js +2 -1
- package/dist/lib/auth-mint.d.ts +150 -0
- package/dist/lib/auth-mint.js +434 -0
- package/dist/lib/browser/profiles.d.ts +18 -0
- package/dist/lib/browser/profiles.js +26 -1
- package/dist/lib/browser/registry.d.ts +44 -14
- package/dist/lib/browser/registry.js +141 -45
- 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/runner.js +10 -2
- 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 +8 -7
- package/dist/lib/devices/config-migration.js +147 -1
- package/dist/lib/devices/connect.d.ts +26 -0
- package/dist/lib/devices/connect.js +48 -1
- package/dist/lib/devices/device-docs.d.ts +35 -0
- package/dist/lib/devices/device-docs.js +163 -0
- package/dist/lib/devices/discovery-policy.d.ts +14 -2
- package/dist/lib/devices/discovery-policy.js +31 -21
- package/dist/lib/devices/doctor-findings.d.ts +5 -1
- package/dist/lib/devices/doctor-findings.js +19 -1
- package/dist/lib/devices/registry.d.ts +11 -5
- package/dist/lib/devices/registry.js +46 -18
- package/dist/lib/exec.d.ts +88 -30
- package/dist/lib/exec.js +138 -34
- package/dist/lib/feed/feed.d.ts +10 -2
- package/dist/lib/feed/feed.js +35 -2
- 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/dispatch.d.ts +4 -3
- package/dist/lib/hosts/dispatch.js +12 -8
- package/dist/lib/hosts/passthrough.d.ts +1 -10
- package/dist/lib/hosts/passthrough.js +1 -13
- package/dist/lib/hosts/providers/local.d.ts +9 -3
- package/dist/lib/hosts/providers/local.js +23 -12
- package/dist/lib/hosts/reconnect.d.ts +7 -4
- package/dist/lib/hosts/reconnect.js +29 -25
- package/dist/lib/hosts/registry.js +4 -1
- package/dist/lib/hosts/remote-os.js +3 -1
- 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.d.ts +10 -1
- package/dist/lib/session/active.js +8 -5
- package/dist/lib/session/actor-sidecar.d.ts +7 -0
- package/dist/lib/session/actor-sidecar.js +2 -0
- package/dist/lib/session/db.d.ts +39 -4
- package/dist/lib/session/db.js +168 -31
- package/dist/lib/session/discover.d.ts +32 -4
- package/dist/lib/session/discover.js +126 -37
- 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/live-metadata.js +1 -0
- package/dist/lib/session/pid-registry.d.ts +7 -0
- package/dist/lib/session/prompt.d.ts +15 -0
- package/dist/lib/session/prompt.js +21 -0
- 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 +25 -0
- package/dist/lib/session/types.js +10 -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 +594 -64
- package/dist/lib/startup/root-command.js +2 -1
- package/dist/lib/state.d.ts +24 -0
- package/dist/lib/state.js +318 -54
- package/dist/lib/sync-status.d.ts +4 -0
- package/dist/lib/sync-status.js +3 -0
- package/dist/lib/terminal/resolve.d.ts +7 -0
- package/dist/lib/terminal/resolve.js +41 -2
- package/dist/lib/traces/classify.js +24 -19
- package/dist/lib/traces/insights.d.ts +67 -0
- package/dist/lib/traces/insights.js +178 -0
- package/dist/lib/traces/phenotype.d.ts +67 -0
- package/dist/lib/traces/phenotype.js +437 -0
- package/dist/lib/traces/segments.d.ts +133 -0
- package/dist/lib/traces/segments.js +301 -0
- package/dist/lib/traces/sync.d.ts +33 -0
- package/dist/lib/traces/sync.js +11 -2
- package/dist/lib/types.d.ts +47 -1
- package/dist/lib/usage-refresh.js +2 -1
- package/dist/lib/view-types.d.ts +2 -0
- package/dist/lib/watchdog/runner.js +18 -4
- package/package.json +2 -1
|
@@ -62,7 +62,7 @@ export declare function attributedSetLostPids(prev: Set<number>, next: Set<numbe
|
|
|
62
62
|
export declare function filterCachedUnattributed(sessions: ActiveSession[], attributed: Set<number>, alive: (pid: number, startedAtMs?: number) => boolean): ActiveSession[];
|
|
63
63
|
type ActiveContext = 'terminal' | 'teams' | 'cloud' | 'headless';
|
|
64
64
|
/** The SessionMeta fields the live-row backfill reads — the enrichment a running process cannot report. */
|
|
65
|
-
export type BackfillMeta = Pick<SessionMeta, 'version' | 'timestamp' | 'label' | 'ticketId' | 'prUrl' | 'prNumber' | 'origin' | 'routineName'>;
|
|
65
|
+
export type BackfillMeta = Pick<SessionMeta, 'version' | 'timestamp' | 'label' | 'ticketId' | 'prUrl' | 'prNumber' | 'origin' | 'routineName' | 'harness'>;
|
|
66
66
|
export declare function backfillActiveRowsFromMeta(sessions: ActiveSession[], metaById: Map<string, BackfillMeta>): void;
|
|
67
67
|
export declare function backfillActiveRowsFromIndex(sessions: ActiveSession[]): void;
|
|
68
68
|
export declare function isRunningLiveSession(s: ActiveSession): boolean;
|
|
@@ -112,6 +112,13 @@ export type RecapSource = 'label' | 'last' | 'prompt';
|
|
|
112
112
|
export interface ActiveSession {
|
|
113
113
|
context: ActiveContext;
|
|
114
114
|
kind: string;
|
|
115
|
+
/**
|
|
116
|
+
* Custom harness / profile name when this live process was launched via
|
|
117
|
+
* `agents run <profile>` (e.g. `deepseek`). `kind` stays the HOST process
|
|
118
|
+
* (claude) so transcript lookup and live-signal parsers keep working.
|
|
119
|
+
* `sessions --active` displays this when set (PHNX-2935).
|
|
120
|
+
*/
|
|
121
|
+
harness?: string;
|
|
115
122
|
/** Specific host app — 'code', 'cursor', 'codium', 'iterm', 'terminal', 'warp', 'tmux', etc. */
|
|
116
123
|
host?: string;
|
|
117
124
|
pid?: number;
|
|
@@ -652,6 +659,8 @@ export declare function listUnattributedActive(attributed: Set<number>): Promise
|
|
|
652
659
|
/** One tmux pane's resolved agent identity for the authoritative source. */
|
|
653
660
|
interface PaneIdentity {
|
|
654
661
|
agent: string;
|
|
662
|
+
/** Custom harness/profile name from the launch registry, when set. */
|
|
663
|
+
harness?: string;
|
|
655
664
|
/** Exact session id when resolvable (launch registry, or the hook join). */
|
|
656
665
|
sessionId?: string;
|
|
657
666
|
/** The agent's OS pid from the launch registry (may differ from `pane_pid`). */
|
|
@@ -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
|
|
@@ -166,6 +167,8 @@ export function backfillActiveRowsFromMeta(sessions, metaById) {
|
|
|
166
167
|
s.origin = m.origin;
|
|
167
168
|
if (!s.routineName && m.routineName)
|
|
168
169
|
s.routineName = m.routineName;
|
|
170
|
+
if (!s.harness && m.harness)
|
|
171
|
+
s.harness = m.harness;
|
|
169
172
|
}
|
|
170
173
|
}
|
|
171
174
|
function loadBackfillMetaFor(sessions) {
|
|
@@ -528,10 +531,6 @@ function readLiveTerminals() {
|
|
|
528
531
|
}
|
|
529
532
|
return Array.from(merged.values());
|
|
530
533
|
}
|
|
531
|
-
/** Convert an absolute cwd to the Claude-project folder name (slashes and dots → dashes). */
|
|
532
|
-
function claudeProjectDirName(cwd) {
|
|
533
|
-
return cwd.replace(/[/.]/g, '-');
|
|
534
|
-
}
|
|
535
534
|
/**
|
|
536
535
|
* Process-local memo for Claude transcript path resolution. Each active-session
|
|
537
536
|
* poll re-walks every Claude version-home `projects/` tree for every live pid
|
|
@@ -1010,6 +1009,7 @@ export async function listTeamsActive(opts = {}) {
|
|
|
1010
1009
|
return applyState({
|
|
1011
1010
|
context: 'teams',
|
|
1012
1011
|
kind: a.agentType,
|
|
1012
|
+
harness: a.profileName ?? undefined,
|
|
1013
1013
|
pid: a.pid ?? undefined,
|
|
1014
1014
|
sessionId: resolvedId,
|
|
1015
1015
|
machine: offloaded ? execHost : undefined,
|
|
@@ -1071,6 +1071,7 @@ export async function listTerminalsActive() {
|
|
|
1071
1071
|
return applyState({
|
|
1072
1072
|
context: 'terminal',
|
|
1073
1073
|
kind: t.kind,
|
|
1074
|
+
harness: pidEntry?.harness,
|
|
1074
1075
|
host: detectHost(t.pid, procByPid),
|
|
1075
1076
|
tty: procByPid.get(t.pid)?.tty,
|
|
1076
1077
|
pid: t.pid,
|
|
@@ -1557,6 +1558,7 @@ async function listUnattributedActiveLive(attributed) {
|
|
|
1557
1558
|
out.push(applyState({
|
|
1558
1559
|
context,
|
|
1559
1560
|
kind,
|
|
1561
|
+
harness: entry?.harness,
|
|
1560
1562
|
host,
|
|
1561
1563
|
tty: procByPid.get(pid)?.tty,
|
|
1562
1564
|
pid,
|
|
@@ -1621,7 +1623,7 @@ export function resolvePaneIdentity(pane, sessName, meta, liveEntry, getHookInde
|
|
|
1621
1623
|
terminalId: liveEntry.terminalId,
|
|
1622
1624
|
})?.session_id
|
|
1623
1625
|
?? nameSessionId;
|
|
1624
|
-
return { agent: liveEntry.agent, sessionId, pid: liveEntry.pid };
|
|
1626
|
+
return { agent: liveEntry.agent, harness: liveEntry.harness, sessionId, pid: liveEntry.pid };
|
|
1625
1627
|
}
|
|
1626
1628
|
// No live-registry entry. Session-meta labels are the wrapped-origin fallback;
|
|
1627
1629
|
// prefer them, then fall back to the name so a pane with neither a registry
|
|
@@ -1771,6 +1773,7 @@ export async function listTmuxAgentSessions() {
|
|
|
1771
1773
|
out.push(applyState({
|
|
1772
1774
|
context: 'terminal',
|
|
1773
1775
|
kind: id.agent,
|
|
1776
|
+
harness: id.harness,
|
|
1774
1777
|
host: 'tmux',
|
|
1775
1778
|
pid,
|
|
1776
1779
|
sessionId: id.sessionId ?? sessionIdFromFile(sessionFile),
|
|
@@ -7,6 +7,13 @@ export interface SessionActorRecord {
|
|
|
7
7
|
initiatedBy?: 'human' | 'agent';
|
|
8
8
|
/** Effective permissions mode used by the launcher. */
|
|
9
9
|
mode?: SessionRunMode;
|
|
10
|
+
/**
|
|
11
|
+
* Custom harness / profile name when launched via `agents run <profile>`
|
|
12
|
+
* (e.g. `deepseek`). Joined onto the session index at scan time so a
|
|
13
|
+
* durable listing can distinguish the profile from its host agent
|
|
14
|
+
* (PHNX-2935).
|
|
15
|
+
*/
|
|
16
|
+
harness?: string;
|
|
10
17
|
/** Stable wrapper names that resolve to this native session id. */
|
|
11
18
|
aliases?: string[];
|
|
12
19
|
startedAtMs: number;
|
|
@@ -40,6 +40,7 @@ function isSafeAlias(alias) {
|
|
|
40
40
|
function hasRecordData(record) {
|
|
41
41
|
return typeof record.actor === 'string'
|
|
42
42
|
|| typeof record.mode === 'string'
|
|
43
|
+
|| typeof record.harness === 'string'
|
|
43
44
|
|| (Array.isArray(record.aliases) && record.aliases.some(alias => typeof alias === 'string'));
|
|
44
45
|
}
|
|
45
46
|
function normalizedAliases(aliases) {
|
|
@@ -83,6 +84,7 @@ export function writeSessionAliasRecord(sessionId, alias) {
|
|
|
83
84
|
actor: previous?.actor,
|
|
84
85
|
initiatedBy: previous?.initiatedBy,
|
|
85
86
|
mode: previous?.mode,
|
|
87
|
+
harness: previous?.harness,
|
|
86
88
|
aliases: normalizedAliases([...(previous?.aliases ?? []), alias]),
|
|
87
89
|
startedAtMs: previous?.startedAtMs ?? Date.now(),
|
|
88
90
|
});
|
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,16 +66,22 @@ 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 (
|
|
64
81
|
id TEXT PRIMARY KEY,
|
|
65
82
|
short_id TEXT NOT NULL,
|
|
66
83
|
agent TEXT NOT NULL,
|
|
84
|
+
harness TEXT,
|
|
67
85
|
origin TEXT DEFAULT 'cli',
|
|
68
86
|
routine_name TEXT,
|
|
69
87
|
routine_run_id TEXT,
|
|
@@ -134,6 +152,7 @@ CREATE VIRTUAL TABLE IF NOT EXISTS session_text USING fts5(
|
|
|
134
152
|
topic,
|
|
135
153
|
project,
|
|
136
154
|
content,
|
|
155
|
+
assistant,
|
|
137
156
|
tokenize = 'unicode61 remove_diacritics 2'
|
|
138
157
|
);
|
|
139
158
|
|
|
@@ -157,7 +176,13 @@ CREATE TABLE IF NOT EXISTS scan_ledger (
|
|
|
157
176
|
-- doc so detectTicket + FTS can rebuild on append without re-reading the file.
|
|
158
177
|
-- Written by B-2; B-1 only defines + round-trips them.
|
|
159
178
|
parser_state TEXT,
|
|
160
|
-
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
|
|
161
186
|
);
|
|
162
187
|
|
|
163
188
|
-- Tracks the mtime + entry-count of every LEAF directory that directly holds
|
|
@@ -408,8 +433,8 @@ CREATE INDEX IF NOT EXISTS idx_computer_sessions_started ON computer_sessions(st
|
|
|
408
433
|
* re-derives on the next `agents insights` instead of silently reporting stale
|
|
409
434
|
* numbers alongside fresh ones. Same role as RESOURCE_INDEX_VERSION.
|
|
410
435
|
*/
|
|
411
|
-
/** Bump when facet extraction changes so cached rows recompute (
|
|
412
|
-
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;
|
|
413
438
|
export const SESSION_TOPIC_EXTRACTOR_VERSION = 1;
|
|
414
439
|
const PREVIEW_EXTRACTOR_VERSION = 1;
|
|
415
440
|
let dbInstance = null;
|
|
@@ -1072,6 +1097,60 @@ function migrateSchema(db, fromVersion) {
|
|
|
1072
1097
|
db.exec(`ALTER TABLE sessions ADD COLUMN background_shell_count INTEGER`);
|
|
1073
1098
|
}
|
|
1074
1099
|
}
|
|
1100
|
+
if (fromVersion < 41) {
|
|
1101
|
+
// v40 -> v41: persist the custom-harness / profile name a run was launched
|
|
1102
|
+
// as (PHNX-2935). Transcript discovery still keys `agent` on the HOST
|
|
1103
|
+
// (the file lives under the host's session dir), so without this column
|
|
1104
|
+
// `agents sessions` cannot tell `agents run deepseek` from a native claude
|
|
1105
|
+
// run. Additive, no ledger flush — the name is launch metadata joined from
|
|
1106
|
+
// the actor sidecar, not parsed from the transcript. Pre-upgrade rows stay
|
|
1107
|
+
// NULL (native / unknown) until a sidecar-backed rescan fills them.
|
|
1108
|
+
const cols = new Set(db.prepare(`PRAGMA table_info(sessions)`).all().map((c) => c.name));
|
|
1109
|
+
if (!cols.has('harness'))
|
|
1110
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN harness TEXT`);
|
|
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
|
+
}
|
|
1075
1154
|
}
|
|
1076
1155
|
/**
|
|
1077
1156
|
* Stamp `account_key` / `account_org` / `account` on every Claude row from its
|
|
@@ -1194,6 +1273,21 @@ export function getDB() {
|
|
|
1194
1273
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_machine_ts ON sessions(machine, timestamp DESC)`);
|
|
1195
1274
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_sessions_agent_ts ON sessions(agent, timestamp DESC)`);
|
|
1196
1275
|
}
|
|
1276
|
+
// harness column: only after the column is guaranteed present.
|
|
1277
|
+
// Fresh SCHEMA (v41) includes the column; older DBs get it from migrate v41.
|
|
1278
|
+
// schema_version can be stamped at SCHEMA_VERSION without the column existing
|
|
1279
|
+
// — getDB writes the marker for any DB whose meta has no row (a hand-built
|
|
1280
|
+
// or partially-created index), and migrateSchema never runs in that path
|
|
1281
|
+
// (`currentVersion === undefined`). If a partial upgrade left schema_version
|
|
1282
|
+
// ahead of the column, repair here so the next upsertSession INSERT naming
|
|
1283
|
+
// `harness` does not throw (PHNX-2935). Same shape as the `machine` repair
|
|
1284
|
+
// above, for the same reason.
|
|
1285
|
+
{
|
|
1286
|
+
const cols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
1287
|
+
if (!cols.some((c) => c.name === 'harness')) {
|
|
1288
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN harness TEXT`);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1197
1291
|
// One-shot cleanup of the pre-SQLite JSONL indexes. Safe — nothing reads
|
|
1198
1292
|
// them anymore. Guarded by a meta flag so we only try once.
|
|
1199
1293
|
const cleaned = db.prepare(`SELECT value FROM meta WHERE key = 'legacy_indexes_removed'`).get();
|
|
@@ -1427,13 +1521,18 @@ export function getScanStampsForPaths(filePaths) {
|
|
|
1427
1521
|
const placeholders = chunk.map(() => '?').join(',');
|
|
1428
1522
|
const rows = db
|
|
1429
1523
|
.prepare(`
|
|
1430
|
-
SELECT file_path, file_mtime_ms, file_size, scanned_at
|
|
1524
|
+
SELECT file_path, file_mtime_ms, file_size, scanned_at, extractor_version
|
|
1431
1525
|
FROM scan_ledger
|
|
1432
1526
|
WHERE file_path IN (${placeholders})
|
|
1433
1527
|
`)
|
|
1434
1528
|
.all(...chunk);
|
|
1435
1529
|
for (const row of rows) {
|
|
1436
|
-
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
|
+
};
|
|
1437
1536
|
for (const original of canonicalToOriginals.get(row.file_path) || []) {
|
|
1438
1537
|
result.set(original, stamp);
|
|
1439
1538
|
}
|
|
@@ -1470,7 +1569,7 @@ export function getParserStatesForPaths(filePaths) {
|
|
|
1470
1569
|
const placeholders = chunk.map(() => '?').join(',');
|
|
1471
1570
|
const rows = db
|
|
1472
1571
|
.prepare(`
|
|
1473
|
-
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
|
|
1474
1573
|
FROM scan_ledger
|
|
1475
1574
|
WHERE file_path IN (${placeholders})
|
|
1476
1575
|
`)
|
|
@@ -1482,6 +1581,7 @@ export function getParserStatesForPaths(filePaths) {
|
|
|
1482
1581
|
fileMtimeMs: row.file_mtime_ms,
|
|
1483
1582
|
fileSize: row.file_size,
|
|
1484
1583
|
scannedAt: row.scanned_at,
|
|
1584
|
+
extractorVersion: row.extractor_version,
|
|
1485
1585
|
};
|
|
1486
1586
|
for (const original of canonicalToOriginals.get(row.file_path) || []) {
|
|
1487
1587
|
result.set(original, state);
|
|
@@ -1499,17 +1599,23 @@ export function recordScans(entries) {
|
|
|
1499
1599
|
return;
|
|
1500
1600
|
const db = getDB();
|
|
1501
1601
|
const stmt = db.prepare(`
|
|
1502
|
-
INSERT INTO scan_ledger (file_path, file_mtime_ms, file_size, scanned_at)
|
|
1503
|
-
VALUES (?, ?, ?, ?)
|
|
1602
|
+
INSERT INTO scan_ledger (file_path, file_mtime_ms, file_size, scanned_at, extractor_version)
|
|
1603
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1504
1604
|
ON CONFLICT(file_path) DO UPDATE SET
|
|
1505
1605
|
file_mtime_ms = excluded.file_mtime_ms,
|
|
1506
1606
|
file_size = excluded.file_size,
|
|
1507
|
-
scanned_at = excluded.scanned_at
|
|
1607
|
+
scanned_at = excluded.scanned_at,
|
|
1608
|
+
extractor_version = excluded.extractor_version
|
|
1508
1609
|
`);
|
|
1509
1610
|
const now = Date.now();
|
|
1510
1611
|
const txn = db.transaction((items) => {
|
|
1511
1612
|
for (const { filePath, scan } of items) {
|
|
1512
|
-
|
|
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);
|
|
1513
1619
|
}
|
|
1514
1620
|
});
|
|
1515
1621
|
txn(entries);
|
|
@@ -1583,7 +1689,7 @@ export function recordDirScans(entries) {
|
|
|
1583
1689
|
}
|
|
1584
1690
|
const upsertSessionStmt = (db) => db.prepare(`
|
|
1585
1691
|
INSERT INTO sessions (
|
|
1586
|
-
id, short_id, agent, origin, routine_name, routine_run_id,
|
|
1692
|
+
id, short_id, agent, harness, origin, routine_name, routine_run_id,
|
|
1587
1693
|
version, account, account_key, account_org, mode, timestamp, last_activity,
|
|
1588
1694
|
project, cwd, git_branch, topic, label, message_count, token_count,
|
|
1589
1695
|
output_tokens, input_tokens, cache_read_tokens, cache_write_tokens,
|
|
@@ -1594,7 +1700,7 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
1594
1700
|
recent_directories_touched, linear_project, linear_project_url, machine,
|
|
1595
1701
|
actor, initiated_by, used_browser, used_computer
|
|
1596
1702
|
) VALUES (
|
|
1597
|
-
@id, @short_id, @agent, @origin, @routine_name, @routine_run_id,
|
|
1703
|
+
@id, @short_id, @agent, @harness, @origin, @routine_name, @routine_run_id,
|
|
1598
1704
|
@version, @account, @account_key, @account_org, @mode, @timestamp, @last_activity,
|
|
1599
1705
|
@project, @cwd, @git_branch, @topic, @label, @message_count, @token_count,
|
|
1600
1706
|
@output_tokens, @input_tokens, @cache_read_tokens, @cache_write_tokens,
|
|
@@ -1608,6 +1714,11 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
1608
1714
|
ON CONFLICT(id) DO UPDATE SET
|
|
1609
1715
|
short_id = excluded.short_id,
|
|
1610
1716
|
agent = excluded.agent,
|
|
1717
|
+
-- Custom harness/profile name is launch metadata, not transcript-derived.
|
|
1718
|
+
-- COALESCE(existing, incoming) keeps a stored stamp on rescan (the scanner
|
|
1719
|
+
-- carries none) and backfills a NULL-first row once the sidecar lands —
|
|
1720
|
+
-- the same write-once pattern as actor/initiated_by (PHNX-2935).
|
|
1721
|
+
harness = COALESCE(sessions.harness, excluded.harness),
|
|
1611
1722
|
origin = excluded.origin,
|
|
1612
1723
|
routine_name = excluded.routine_name,
|
|
1613
1724
|
routine_run_id = excluded.routine_run_id,
|
|
@@ -1834,7 +1945,7 @@ function enrichCachedSessionMeta(meta) {
|
|
|
1834
1945
|
}
|
|
1835
1946
|
}
|
|
1836
1947
|
const deleteTextStmt = (db) => db.prepare(`DELETE FROM session_text WHERE session_id = ?`);
|
|
1837
|
-
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 (?, ?, ?, ?, ?, ?)`);
|
|
1838
1949
|
// Read back the label the upsert actually stored (which may be the preserved
|
|
1839
1950
|
// one, not the incoming blank) so the FTS label column stays consistent with
|
|
1840
1951
|
// sessions.label after a bare rescan.
|
|
@@ -1871,7 +1982,7 @@ function resolveMachine(meta) {
|
|
|
1871
1982
|
* Upsert a session row and replace its FTS5 content in a single transaction.
|
|
1872
1983
|
* `content` is the tokenizable user-prompt text; pass '' to leave the row unsearchable.
|
|
1873
1984
|
*/
|
|
1874
|
-
export function upsertSession(meta, content, scan) {
|
|
1985
|
+
export function upsertSession(meta, content, scan, assistantContent = '') {
|
|
1875
1986
|
meta = enrichCachedSessionMeta(meta);
|
|
1876
1987
|
// Join the durable sessionId -> actor sidecar (RUSH-2019) when the caller
|
|
1877
1988
|
// didn't already carry an actor, so a scanned transcript still attributes to a
|
|
@@ -1886,6 +1997,7 @@ export function upsertSession(meta, content, scan) {
|
|
|
1886
1997
|
id: meta.id,
|
|
1887
1998
|
short_id: meta.shortId,
|
|
1888
1999
|
agent: meta.agent,
|
|
2000
|
+
harness: meta.harness ?? actorRec?.harness ?? null,
|
|
1889
2001
|
origin: meta.origin ?? 'cli',
|
|
1890
2002
|
routine_name: meta.routineName ?? null,
|
|
1891
2003
|
routine_run_id: meta.routineRunId ?? null,
|
|
@@ -1941,7 +2053,7 @@ export function upsertSession(meta, content, scan) {
|
|
|
1941
2053
|
insText.run(meta.id,
|
|
1942
2054
|
// Use the label the upsert actually stored (preserve-non-empty rule),
|
|
1943
2055
|
// not the raw incoming one, so FTS label ranking survives a bare rescan.
|
|
1944
|
-
storedFtsLabel(readLabel, meta.id), meta.topic ?? '', meta.project ?? '', content ?? '');
|
|
2056
|
+
storedFtsLabel(readLabel, meta.id), meta.topic ?? '', meta.project ?? '', content ?? '', assistantContent ?? '');
|
|
1945
2057
|
});
|
|
1946
2058
|
txn();
|
|
1947
2059
|
}
|
|
@@ -1959,19 +2071,23 @@ export function upsertSessionsBatch(entries) {
|
|
|
1959
2071
|
// stored owner on rescan.
|
|
1960
2072
|
const actorIndex = loadSessionActorIndex();
|
|
1961
2073
|
// Persist the Claude resumable-parse continuation (parser_state + content_text)
|
|
1962
|
-
// 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
|
|
1963
2078
|
// serialized newState + accumulated user doc so the NEXT scan can resume from
|
|
1964
2079
|
// the persisted offset (B-2). Other scanners pass neither, leaving both columns
|
|
1965
2080
|
// NULL exactly as before — their ledger rows are unaffected.
|
|
1966
2081
|
const ledger = db.prepare(`
|
|
1967
|
-
INSERT INTO scan_ledger (file_path, file_mtime_ms, file_size, scanned_at, parser_state, content_text)
|
|
1968
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
2082
|
+
INSERT INTO scan_ledger (file_path, file_mtime_ms, file_size, scanned_at, parser_state, content_text, extractor_version)
|
|
2083
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1969
2084
|
ON CONFLICT(file_path) DO UPDATE SET
|
|
1970
2085
|
file_mtime_ms = excluded.file_mtime_ms,
|
|
1971
2086
|
file_size = excluded.file_size,
|
|
1972
2087
|
scanned_at = excluded.scanned_at,
|
|
1973
2088
|
parser_state = excluded.parser_state,
|
|
1974
|
-
content_text = excluded.content_text
|
|
2089
|
+
content_text = excluded.content_text,
|
|
2090
|
+
extractor_version = excluded.extractor_version
|
|
1975
2091
|
`);
|
|
1976
2092
|
// Build a lookup from canonical file path → entry, used inside the write
|
|
1977
2093
|
// transaction to re-check the ledger AFTER acquiring the lock. When a
|
|
@@ -2042,17 +2158,26 @@ export function upsertSessionsBatch(entries) {
|
|
|
2042
2158
|
const chunk = paths.slice(i, i + CHUNK);
|
|
2043
2159
|
const phs = chunk.map(() => '?').join(',');
|
|
2044
2160
|
const rows = db
|
|
2045
|
-
.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})`)
|
|
2046
2162
|
.all(...chunk);
|
|
2047
2163
|
for (const row of rows) {
|
|
2048
2164
|
const entry = byPath.get(row.file_path);
|
|
2049
|
-
|
|
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) {
|
|
2050
2175
|
alreadyIndexed.add(entry.meta.id);
|
|
2051
2176
|
}
|
|
2052
2177
|
}
|
|
2053
2178
|
}
|
|
2054
2179
|
for (const entry of items) {
|
|
2055
|
-
const { meta, content, scan, parserState, contentText } = entry;
|
|
2180
|
+
const { meta, content, assistantContent, scan, parserState, contentText } = entry;
|
|
2056
2181
|
if (alreadyIndexed.has(meta.id))
|
|
2057
2182
|
continue;
|
|
2058
2183
|
// Per-row guard: one malformed session (e.g. a required field that resolves to
|
|
@@ -2081,6 +2206,7 @@ export function upsertSessionsBatch(entries) {
|
|
|
2081
2206
|
id: meta.id,
|
|
2082
2207
|
short_id: meta.shortId,
|
|
2083
2208
|
agent: meta.agent,
|
|
2209
|
+
harness: meta.harness ?? actorIndex.get(meta.id)?.harness ?? null,
|
|
2084
2210
|
origin: meta.origin ?? 'cli',
|
|
2085
2211
|
routine_name: meta.routineName ?? null,
|
|
2086
2212
|
routine_run_id: meta.routineRunId ?? null,
|
|
@@ -2135,9 +2261,9 @@ export function upsertSessionsBatch(entries) {
|
|
|
2135
2261
|
insText.run(meta.id,
|
|
2136
2262
|
// Mirror upsertSession: index the label the upsert actually stored
|
|
2137
2263
|
// (preserve-non-empty rule), not the raw incoming one.
|
|
2138
|
-
storedFtsLabel(readLabel, meta.id), meta.topic ?? '', meta.project ?? '', content ?? '');
|
|
2264
|
+
storedFtsLabel(readLabel, meta.id), meta.topic ?? '', meta.project ?? '', content ?? '', assistantContent ?? '');
|
|
2139
2265
|
if (scan && meta.filePath) {
|
|
2140
|
-
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);
|
|
2141
2267
|
}
|
|
2142
2268
|
writtenEntries.push(entry);
|
|
2143
2269
|
}
|
|
@@ -2309,6 +2435,7 @@ function rowToMeta(row) {
|
|
|
2309
2435
|
id: row.id,
|
|
2310
2436
|
shortId: row.short_id,
|
|
2311
2437
|
agent: row.agent,
|
|
2438
|
+
harness: row.harness ?? undefined,
|
|
2312
2439
|
origin: (row.origin === 'routine' ? 'routine' : 'cli'),
|
|
2313
2440
|
routineName: row.routine_name ?? undefined,
|
|
2314
2441
|
routineRunId: row.routine_run_id ?? undefined,
|
|
@@ -3474,11 +3601,16 @@ export function ftsSearch(input, limit = 200) {
|
|
|
3474
3601
|
return hits.slice(0, limit);
|
|
3475
3602
|
}
|
|
3476
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.
|
|
3477
3608
|
if (expr) {
|
|
3478
3609
|
try {
|
|
3479
3610
|
const rows = db
|
|
3480
3611
|
.prepare(`
|
|
3481
|
-
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
|
|
3482
3614
|
FROM session_text
|
|
3483
3615
|
WHERE session_text MATCH ?
|
|
3484
3616
|
ORDER BY rank ASC
|
|
@@ -3488,7 +3620,12 @@ export function ftsSearch(input, limit = 200) {
|
|
|
3488
3620
|
for (const r of rows) {
|
|
3489
3621
|
if (seen.has(r.session_id))
|
|
3490
3622
|
continue;
|
|
3491
|
-
hits.push({
|
|
3623
|
+
hits.push({
|
|
3624
|
+
sessionId: r.session_id,
|
|
3625
|
+
score: -r.rank,
|
|
3626
|
+
matchedTerms: terms,
|
|
3627
|
+
snippet: r.snip?.trim() || undefined,
|
|
3628
|
+
});
|
|
3492
3629
|
seen.add(r.session_id);
|
|
3493
3630
|
}
|
|
3494
3631
|
}
|