@phnx-labs/agents-cli 1.20.74 → 1.20.77
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 +1055 -0
- package/README.md +36 -10
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.js +21 -3
- package/dist/commands/attach.d.ts +10 -0
- package/dist/commands/attach.js +41 -0
- package/dist/commands/computer.js +3 -3
- package/dist/commands/detach-core.d.ts +46 -0
- package/dist/commands/detach-core.js +61 -0
- package/dist/commands/detach.d.ts +5 -0
- package/dist/commands/detach.js +171 -0
- package/dist/commands/doctor.js +18 -0
- package/dist/commands/exec.js +83 -13
- package/dist/commands/feed.d.ts +7 -1
- package/dist/commands/feed.js +47 -4
- package/dist/commands/go.d.ts +9 -2
- package/dist/commands/go.js +16 -6
- package/dist/commands/harness.d.ts +14 -0
- package/dist/commands/harness.js +145 -0
- package/dist/commands/import.js +38 -8
- package/dist/commands/inspect.js +5 -2
- package/dist/commands/profiles.d.ts +20 -0
- package/dist/commands/profiles.js +68 -21
- package/dist/commands/repo.js +14 -0
- package/dist/commands/routines.js +286 -15
- package/dist/commands/secrets.js +102 -37
- package/dist/commands/sessions-browser.js +12 -2
- package/dist/commands/sessions-export.js +19 -5
- package/dist/commands/sessions-migrate.d.ts +29 -0
- package/dist/commands/sessions-migrate.js +596 -0
- package/dist/commands/sessions-picker.d.ts +14 -1
- package/dist/commands/sessions-picker.js +184 -16
- package/dist/commands/sessions.d.ts +56 -14
- package/dist/commands/sessions.js +322 -62
- package/dist/commands/setup-computer.js +2 -2
- package/dist/commands/ssh.d.ts +13 -0
- package/dist/commands/ssh.js +110 -37
- package/dist/commands/status.js +10 -2
- package/dist/commands/versions.js +15 -6
- package/dist/commands/view.d.ts +5 -0
- package/dist/commands/view.js +24 -4
- package/dist/commands/watchdog.d.ts +7 -2
- package/dist/commands/watchdog.js +73 -57
- package/dist/index.js +59 -7
- package/dist/lib/activity.d.ts +17 -6
- package/dist/lib/activity.js +466 -40
- package/dist/lib/actor.d.ts +50 -0
- package/dist/lib/actor.js +166 -0
- package/dist/lib/agent-spec/provider.js +2 -1
- package/dist/lib/agent-spec/resolve.js +19 -5
- package/dist/lib/agent-spec/types.d.ts +9 -1
- package/dist/lib/agents.d.ts +21 -0
- package/dist/lib/agents.js +70 -6
- package/dist/lib/cloud/codex.d.ts +2 -0
- package/dist/lib/cloud/codex.js +14 -3
- package/dist/lib/cloud/session-index.d.ts +32 -0
- package/dist/lib/cloud/session-index.js +58 -0
- package/dist/lib/cloud/store.d.ts +7 -0
- package/dist/lib/cloud/store.js +25 -0
- package/dist/lib/config-transfer.js +4 -0
- package/dist/lib/daemon.d.ts +10 -1
- package/dist/lib/daemon.js +114 -11
- package/dist/lib/devices/connect.d.ts +15 -1
- package/dist/lib/devices/connect.js +15 -1
- package/dist/lib/devices/fleet.d.ts +37 -1
- package/dist/lib/devices/fleet.js +36 -2
- package/dist/lib/devices/health-report.d.ts +38 -0
- package/dist/lib/devices/health-report.js +214 -0
- package/dist/lib/devices/health.js +4 -1
- package/dist/lib/devices/reachability.d.ts +31 -0
- package/dist/lib/devices/reachability.js +40 -0
- package/dist/lib/devices/registry.d.ts +33 -0
- package/dist/lib/devices/registry.js +37 -0
- package/dist/lib/devices/resolve-target.d.ts +15 -27
- package/dist/lib/devices/resolve-target.js +63 -102
- package/dist/lib/devices/sync.d.ts +18 -0
- package/dist/lib/devices/sync.js +23 -1
- package/dist/lib/devices/tailscale.d.ts +3 -0
- package/dist/lib/devices/tailscale.js +1 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +82 -0
- package/dist/lib/exec.js +218 -6
- package/dist/lib/feed-post.d.ts +63 -0
- package/dist/lib/feed-post.js +204 -0
- package/dist/lib/feed.d.ts +7 -1
- package/dist/lib/feed.js +96 -6
- package/dist/lib/fleet/apply.d.ts +32 -2
- package/dist/lib/fleet/apply.js +97 -10
- package/dist/lib/fleet/types.d.ts +11 -0
- package/dist/lib/fs-walk.d.ts +13 -0
- package/dist/lib/fs-walk.js +16 -7
- package/dist/lib/heal.d.ts +7 -4
- package/dist/lib/heal.js +10 -22
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +156 -0
- package/dist/lib/hosts/dispatch.d.ts +16 -0
- package/dist/lib/hosts/dispatch.js +26 -4
- package/dist/lib/hosts/passthrough.js +9 -2
- package/dist/lib/hosts/reconnect.d.ts +78 -0
- package/dist/lib/hosts/reconnect.js +127 -0
- package/dist/lib/hosts/registry.d.ts +75 -14
- package/dist/lib/hosts/registry.js +205 -30
- package/dist/lib/hosts/remote-cmd.d.ts +11 -0
- package/dist/lib/hosts/remote-cmd.js +32 -8
- package/dist/lib/hosts/remote-session-id.d.ts +45 -0
- package/dist/lib/hosts/remote-session-id.js +84 -0
- package/dist/lib/hosts/run-target.d.ts +17 -5
- package/dist/lib/hosts/run-target.js +30 -10
- package/dist/lib/hosts/session-index.d.ts +18 -1
- package/dist/lib/hosts/session-index.js +40 -5
- package/dist/lib/hosts/session-marker.d.ts +33 -0
- package/dist/lib/hosts/session-marker.js +51 -0
- package/dist/lib/hosts/tasks.d.ts +8 -4
- package/dist/lib/import.d.ts +2 -0
- package/dist/lib/import.js +35 -2
- package/dist/lib/menubar/install-menubar.d.ts +29 -0
- package/dist/lib/menubar/install-menubar.js +59 -1
- package/dist/lib/menubar/notify-desktop.d.ts +44 -0
- package/dist/lib/menubar/notify-desktop.js +78 -0
- package/dist/lib/migrate.d.ts +16 -0
- package/dist/lib/migrate.js +36 -0
- package/dist/lib/models.d.ts +27 -0
- package/dist/lib/models.js +54 -1
- package/dist/lib/overdue.d.ts +6 -2
- package/dist/lib/overdue.js +10 -36
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/process.d.ts +17 -0
- package/dist/lib/platform/process.js +70 -0
- package/dist/lib/profiles-presets.js +9 -7
- package/dist/lib/profiles.d.ts +31 -0
- package/dist/lib/profiles.js +70 -0
- package/dist/lib/pty-server.d.ts +2 -10
- package/dist/lib/pty-server.js +4 -38
- package/dist/lib/registry.d.ts +1 -1
- package/dist/lib/registry.js +48 -8
- package/dist/lib/rotate.d.ts +18 -0
- package/dist/lib/rotate.js +28 -0
- package/dist/lib/routine-notify.d.ts +76 -0
- package/dist/lib/routine-notify.js +190 -0
- package/dist/lib/routines-placement.d.ts +38 -0
- package/dist/lib/routines-placement.js +79 -0
- package/dist/lib/routines-project.d.ts +97 -0
- package/dist/lib/routines-project.js +349 -0
- package/dist/lib/routines.d.ts +94 -0
- package/dist/lib/routines.js +93 -9
- package/dist/lib/runner.d.ts +12 -2
- package/dist/lib/runner.js +242 -20
- package/dist/lib/sandbox.js +10 -0
- package/dist/lib/secrets/account-token.d.ts +20 -0
- package/dist/lib/secrets/account-token.js +64 -0
- package/dist/lib/secrets/agent.d.ts +74 -4
- package/dist/lib/secrets/agent.js +181 -107
- package/dist/lib/secrets/bundles.d.ts +54 -6
- package/dist/lib/secrets/bundles.js +230 -34
- package/dist/lib/secrets/index.d.ts +26 -3
- package/dist/lib/secrets/index.js +42 -12
- package/dist/lib/secrets/rc-hygiene.d.ts +55 -0
- package/dist/lib/secrets/rc-hygiene.js +154 -0
- package/dist/lib/secrets/remote.d.ts +7 -4
- package/dist/lib/secrets/remote.js +7 -4
- package/dist/lib/secrets/session-store.d.ts +6 -2
- package/dist/lib/secrets/session-store.js +65 -15
- package/dist/lib/secrets/sync-commands.d.ts +21 -0
- package/dist/lib/secrets/sync-commands.js +21 -0
- package/dist/lib/secrets/unlock-hints.d.ts +27 -0
- package/dist/lib/secrets/unlock-hints.js +36 -0
- package/dist/lib/self-update.d.ts +23 -2
- package/dist/lib/self-update.js +86 -5
- package/dist/lib/session/active.d.ts +124 -7
- package/dist/lib/session/active.js +390 -56
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +54 -0
- package/dist/lib/session/db.js +320 -19
- package/dist/lib/session/detached.d.ts +30 -0
- package/dist/lib/session/detached.js +92 -0
- package/dist/lib/session/discover.d.ts +478 -3
- package/dist/lib/session/discover.js +1740 -472
- package/dist/lib/session/fork.js +2 -1
- package/dist/lib/session/hook-sessions.d.ts +43 -0
- package/dist/lib/session/hook-sessions.js +135 -0
- package/dist/lib/session/linear.d.ts +6 -0
- package/dist/lib/session/linear.js +55 -0
- package/dist/lib/session/migrate-targets.d.ts +65 -0
- package/dist/lib/session/migrate-targets.js +94 -0
- package/dist/lib/session/migrations.d.ts +37 -0
- package/dist/lib/session/migrations.js +60 -0
- package/dist/lib/session/parse.d.ts +18 -0
- package/dist/lib/session/parse.js +141 -32
- package/dist/lib/session/pid-registry.d.ts +34 -2
- package/dist/lib/session/pid-registry.js +49 -2
- package/dist/lib/session/prompt.d.ts +7 -0
- package/dist/lib/session/prompt.js +37 -0
- package/dist/lib/session/provenance.d.ts +7 -0
- package/dist/lib/session/render.d.ts +7 -2
- package/dist/lib/session/render.js +40 -26
- package/dist/lib/session/short-id.d.ts +17 -0
- package/dist/lib/session/short-id.js +20 -0
- package/dist/lib/session/state.d.ts +4 -0
- package/dist/lib/session/state.js +99 -13
- package/dist/lib/session/types.d.ts +25 -1
- package/dist/lib/session/types.js +8 -0
- package/dist/lib/shims.d.ts +1 -1
- package/dist/lib/shims.js +20 -6
- package/dist/lib/sqlite.d.ts +3 -2
- package/dist/lib/sqlite.js +27 -4
- package/dist/lib/staleness/detectors/commands.js +1 -1
- package/dist/lib/staleness/detectors/workflows.js +13 -2
- package/dist/lib/staleness/writers/commands.js +7 -7
- package/dist/lib/staleness/writers/workflows.d.ts +4 -2
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +3 -0
- package/dist/lib/state.d.ts +8 -4
- package/dist/lib/state.js +21 -37
- package/dist/lib/teams/agents.d.ts +19 -10
- package/dist/lib/teams/agents.js +40 -39
- package/dist/lib/types.d.ts +47 -2
- package/dist/lib/usage.d.ts +33 -1
- package/dist/lib/usage.js +172 -12
- package/dist/lib/versions.d.ts +9 -2
- package/dist/lib/versions.js +37 -8
- package/dist/lib/watchdog/log.d.ts +43 -0
- package/dist/lib/watchdog/log.js +69 -0
- package/dist/lib/watchdog/routine.d.ts +44 -0
- package/dist/lib/watchdog/routine.js +69 -0
- package/dist/lib/watchdog/runner.d.ts +51 -7
- package/dist/lib/watchdog/runner.js +239 -64
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +31 -16
- package/dist/lib/workflows.d.ts +16 -0
- package/dist/lib/workflows.js +110 -1
- package/package.json +1 -1
|
@@ -13,6 +13,7 @@ import * as fs from 'fs';
|
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import * as os from 'os';
|
|
15
15
|
import * as yaml from 'yaml';
|
|
16
|
+
import { deriveShortId } from './short-id.js';
|
|
16
17
|
import { getCacheDir } from '../state.js';
|
|
17
18
|
const PROXY_BASE = process.env.RUSH_PROXY_BASE ?? 'https://api.prix.dev';
|
|
18
19
|
const USER_YAML = path.join(os.homedir(), '.rush', 'user.yaml');
|
|
@@ -94,7 +95,7 @@ export async function discoverCloudSessions(options) {
|
|
|
94
95
|
const filePath = cachePathForExecution(id, agent);
|
|
95
96
|
out.push({
|
|
96
97
|
id,
|
|
97
|
-
shortId: id
|
|
98
|
+
shortId: deriveShortId(id),
|
|
98
99
|
agent,
|
|
99
100
|
timestamp,
|
|
100
101
|
project,
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -40,6 +40,10 @@ export interface SessionRow {
|
|
|
40
40
|
worktree_slug: string | null;
|
|
41
41
|
ticket_id: string | null;
|
|
42
42
|
plan: string | null;
|
|
43
|
+
todos: string | null;
|
|
44
|
+
recent_directories_touched: string | null;
|
|
45
|
+
linear_project: string | null;
|
|
46
|
+
linear_project_url: string | null;
|
|
43
47
|
}
|
|
44
48
|
/** File stat snapshot used to detect changes between scan runs. */
|
|
45
49
|
export interface ScanStamp {
|
|
@@ -109,6 +113,29 @@ export declare function getScanStampByPath(filePath: string): ScanStamp | null;
|
|
|
109
113
|
* This is the fast path used by the incremental scanner — avoids N+1 queries.
|
|
110
114
|
*/
|
|
111
115
|
export declare function getScanStampsForPaths(filePaths: string[]): Map<string, ScanStamp>;
|
|
116
|
+
/**
|
|
117
|
+
* A file's persisted resumable-parse continuation, read back from scan_ledger.
|
|
118
|
+
* `parserState` is the serialized {@link ClaudeParserState} JSON blob (offset +
|
|
119
|
+
* accumulator snapshot); `contentText` is the accumulated user doc. Both are
|
|
120
|
+
* written by the Claude scan (B-2) and consumed on the next scan to decide
|
|
121
|
+
* full-vs-incremental and to hydrate the resume.
|
|
122
|
+
*/
|
|
123
|
+
export interface ParserStateRow {
|
|
124
|
+
parserState: string | null;
|
|
125
|
+
contentText: string | null;
|
|
126
|
+
fileMtimeMs: number;
|
|
127
|
+
fileSize: number;
|
|
128
|
+
scannedAt: number;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Bulk-load the resumable-parse continuation (parser_state + content_text) plus
|
|
132
|
+
* the stamp for a set of file paths in a single chunked query. Mirrors
|
|
133
|
+
* {@link getScanStampsForPaths}: keys by canonical path and fans results back to
|
|
134
|
+
* every original alias so callers can `.get(filePath)` with the path they passed.
|
|
135
|
+
* The Claude incremental scan uses this to fetch each changed file's prior
|
|
136
|
+
* continuation without an N+1 of {@link getScanStampByPath}.
|
|
137
|
+
*/
|
|
138
|
+
export declare function getParserStatesForPaths(filePaths: string[]): Map<string, ParserStateRow>;
|
|
112
139
|
/**
|
|
113
140
|
* Record scan stamps for files we've looked at. Covers both files that produced
|
|
114
141
|
* a session and files we looked at but chose not to index (e.g. malformed).
|
|
@@ -117,6 +144,29 @@ export declare function recordScans(entries: Array<{
|
|
|
117
144
|
filePath: string;
|
|
118
145
|
scan: ScanStamp;
|
|
119
146
|
}>): void;
|
|
147
|
+
/** Snapshot of a leaf transcript directory used to detect create/delete/rename. */
|
|
148
|
+
export interface DirStamp {
|
|
149
|
+
dirMtimeMs: number;
|
|
150
|
+
entryCount: number;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Bulk-load the dir ledger for a set of leaf directories in a single SQL query.
|
|
154
|
+
* Mirrors {@link getScanStampsForPaths}: keys by canonical path (so a dir
|
|
155
|
+
* reachable via a symlinked version home and its realpath collapse to one row)
|
|
156
|
+
* and fans the result back out to every original alias so callers can
|
|
157
|
+
* `.get(dirPath)` with the path they passed in.
|
|
158
|
+
*/
|
|
159
|
+
export declare function getDirLedgerForPaths(dirs: string[]): Map<string, DirStamp>;
|
|
160
|
+
/**
|
|
161
|
+
* Upsert dir-scan stamps. Recorded after a full readdir of a leaf transcript
|
|
162
|
+
* dir so the next scan can skip that dir when its (mtime, entry_count) is
|
|
163
|
+
* unchanged. Mirrors {@link recordScans}.
|
|
164
|
+
*/
|
|
165
|
+
export declare function recordDirScans(entries: Array<{
|
|
166
|
+
dirPath: string;
|
|
167
|
+
dirMtimeMs: number;
|
|
168
|
+
entryCount: number;
|
|
169
|
+
}>): void;
|
|
120
170
|
/**
|
|
121
171
|
* Upsert a session row and replace its FTS5 content in a single transaction.
|
|
122
172
|
* `content` is the tokenizable user-prompt text; pass '' to leave the row unsearchable.
|
|
@@ -127,6 +177,8 @@ export declare function upsertSessionsBatch(entries: Array<{
|
|
|
127
177
|
meta: SessionMeta;
|
|
128
178
|
content: string;
|
|
129
179
|
scan?: ScanStamp;
|
|
180
|
+
parserState?: string;
|
|
181
|
+
contentText?: string;
|
|
130
182
|
}>): void;
|
|
131
183
|
/**
|
|
132
184
|
* Sync labels for a set of sessions. For each id in the map, if the stored
|
|
@@ -167,6 +219,8 @@ export declare function isSessionActivityFresh(row: {
|
|
|
167
219
|
timestamp: string;
|
|
168
220
|
file_mtime_ms: number | null;
|
|
169
221
|
}, maxAgeMs: number, nowMs: number): boolean;
|
|
222
|
+
/** Persist a lazily resolved Linear project without reparsing the transcript. */
|
|
223
|
+
export declare function cacheLinearProject(sessionId: string, project: string, projectUrl: string): void;
|
|
170
224
|
/**
|
|
171
225
|
* Newest indexed session file for an agent working in `cwd`. Lets the live
|
|
172
226
|
* `--active` scanner locate a Codex transcript (whose files are date-partitioned,
|
package/dist/lib/session/db.js
CHANGED
|
@@ -9,11 +9,13 @@
|
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as path from 'path';
|
|
11
11
|
import Database from '../sqlite.js';
|
|
12
|
+
import { parseSession } from './parse.js';
|
|
13
|
+
import { extractRecentDirectoriesTouched, extractTodoProgressFromEvents } from './state.js';
|
|
12
14
|
import { getSessionsDir, getSessionsDbPath } from '../state.js';
|
|
13
15
|
const SESSIONS_DIR = getSessionsDir();
|
|
14
16
|
const DB_PATH = getSessionsDbPath();
|
|
15
17
|
/** Current schema version; bumped when migrations are added. */
|
|
16
|
-
const SCHEMA_VERSION =
|
|
18
|
+
const SCHEMA_VERSION = 17;
|
|
17
19
|
/**
|
|
18
20
|
* Canonicalize a file path for use as a scan_ledger key. The same physical
|
|
19
21
|
* session file is reachable via multiple aliases — `~/.claude/projects/x.jsonl`
|
|
@@ -69,7 +71,11 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
69
71
|
pr_number INTEGER,
|
|
70
72
|
worktree_slug TEXT,
|
|
71
73
|
ticket_id TEXT,
|
|
72
|
-
plan TEXT
|
|
74
|
+
plan TEXT,
|
|
75
|
+
todos TEXT,
|
|
76
|
+
recent_directories_touched TEXT,
|
|
77
|
+
linear_project TEXT,
|
|
78
|
+
linear_project_url TEXT
|
|
73
79
|
);
|
|
74
80
|
CREATE INDEX IF NOT EXISTS idx_sessions_timestamp ON sessions(timestamp DESC);
|
|
75
81
|
CREATE INDEX IF NOT EXISTS idx_sessions_cwd ON sessions(cwd);
|
|
@@ -99,6 +105,27 @@ CREATE TABLE IF NOT EXISTS scan_ledger (
|
|
|
99
105
|
file_path TEXT PRIMARY KEY,
|
|
100
106
|
file_mtime_ms INTEGER NOT NULL,
|
|
101
107
|
file_size INTEGER NOT NULL,
|
|
108
|
+
scanned_at INTEGER NOT NULL,
|
|
109
|
+
-- Resumable-parse cursor + continuation (B-1). parser_state is a JSON
|
|
110
|
+
-- ClaudeParserState blob (offset + accumulator snapshot) so a scan can pick
|
|
111
|
+
-- up where the last one stopped; content_text caches the accumulated user
|
|
112
|
+
-- doc so detectTicket + FTS can rebuild on append without re-reading the file.
|
|
113
|
+
-- Written by B-2; B-1 only defines + round-trips them.
|
|
114
|
+
parser_state TEXT,
|
|
115
|
+
content_text TEXT
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
-- Tracks the mtime + entry-count of every LEAF directory that directly holds
|
|
119
|
+
-- transcripts (a Claude project dir, a Gemini chats dir). A dir's mtime bumps
|
|
120
|
+
-- on create/delete/rename of its entries but NOT on an in-place append, so a
|
|
121
|
+
-- match here means the dir gained/lost/renamed no files: we can skip the
|
|
122
|
+
-- readdir + per-file stat and serve unchanged files from the DB (append-safety
|
|
123
|
+
-- is preserved by re-stat'ing only the "hot set" — see discover.ts). Keyed by
|
|
124
|
+
-- canonicalLedgerKey, same as scan_ledger.
|
|
125
|
+
CREATE TABLE IF NOT EXISTS dir_ledger (
|
|
126
|
+
dir_path TEXT PRIMARY KEY,
|
|
127
|
+
dir_mtime_ms INTEGER NOT NULL,
|
|
128
|
+
entry_count INTEGER NOT NULL,
|
|
102
129
|
scanned_at INTEGER NOT NULL
|
|
103
130
|
);
|
|
104
131
|
`;
|
|
@@ -252,6 +279,61 @@ function migrateSchema(db, fromVersion) {
|
|
|
252
279
|
db.exec(`UPDATE sessions SET origin = 'cli' WHERE origin IS NULL OR origin = ''`);
|
|
253
280
|
db.exec(`DELETE FROM scan_ledger;`);
|
|
254
281
|
}
|
|
282
|
+
if (fromVersion < 14) {
|
|
283
|
+
// v13 → v14: the discovery scan now short-circuits the readdir + per-file
|
|
284
|
+
// stat of leaf transcript dirs whose (mtime, entry_count) is unchanged,
|
|
285
|
+
// caching that snapshot in the new `dir_ledger` table. Create it, and clear
|
|
286
|
+
// scan_ledger so the first post-upgrade scan does a clean full walk — that
|
|
287
|
+
// walk seeds BOTH ledgers correctly, so a cold/empty dir_ledger degrades to
|
|
288
|
+
// today's full behavior.
|
|
289
|
+
db.exec(`
|
|
290
|
+
CREATE TABLE IF NOT EXISTS dir_ledger (
|
|
291
|
+
dir_path TEXT PRIMARY KEY,
|
|
292
|
+
dir_mtime_ms INTEGER NOT NULL,
|
|
293
|
+
entry_count INTEGER NOT NULL,
|
|
294
|
+
scanned_at INTEGER NOT NULL
|
|
295
|
+
);
|
|
296
|
+
DELETE FROM scan_ledger;
|
|
297
|
+
`);
|
|
298
|
+
}
|
|
299
|
+
if (fromVersion < 15) {
|
|
300
|
+
// v14 → v15: the Claude scan becomes resumable (B-1). scan_ledger gains a
|
|
301
|
+
// `parser_state` continuation blob (offset + accumulator snapshot) and a
|
|
302
|
+
// `content_text` cache of the accumulated user doc. Add both columns, then
|
|
303
|
+
// clear scan_ledger so the first post-upgrade scan does a clean full walk
|
|
304
|
+
// that reseeds the cursor from byte 0.
|
|
305
|
+
const cols = db.prepare(`PRAGMA table_info(scan_ledger)`).all();
|
|
306
|
+
if (!cols.some(c => c.name === 'parser_state'))
|
|
307
|
+
db.exec(`ALTER TABLE scan_ledger ADD COLUMN parser_state TEXT`);
|
|
308
|
+
if (!cols.some(c => c.name === 'content_text'))
|
|
309
|
+
db.exec(`ALTER TABLE scan_ledger ADD COLUMN content_text TEXT`);
|
|
310
|
+
db.exec(`DELETE FROM scan_ledger;`);
|
|
311
|
+
}
|
|
312
|
+
if (fromVersion < 16) {
|
|
313
|
+
// v15 → v16: repair rows poisoned by the empty-shortId bug (now fixed at the
|
|
314
|
+
// source in deriveShortId). A session id that was only a known prefix — a bare
|
|
315
|
+
// `session_` dir, an id of exactly `api-` or `ses_` — derived to '' via
|
|
316
|
+
// `id.replace(prefix, '').slice(0, 8)`, passed the `short_id TEXT NOT NULL`
|
|
317
|
+
// column (empty string is not NULL), yet matched nothing in the
|
|
318
|
+
// `short_id LIKE ?` picker lookups. The parser no longer produces '', but
|
|
319
|
+
// existing rows don't self-heal: an empty short_id is not re-parsed unless its
|
|
320
|
+
// file changes, and an orphaned row (file gone) never re-parses at all. Repair
|
|
321
|
+
// in place — `substr(id, 1, 8)` is non-empty because `id` is the non-empty
|
|
322
|
+
// primary key — so every corrupt row becomes addressable. No rescan needed.
|
|
323
|
+
db.exec(`UPDATE sessions SET short_id = substr(id, 1, 8) WHERE short_id IS NULL OR short_id = ''`);
|
|
324
|
+
}
|
|
325
|
+
if (fromVersion < 17) {
|
|
326
|
+
const cols = db.prepare(`PRAGMA table_info(sessions)`).all();
|
|
327
|
+
if (!cols.some(c => c.name === 'todos'))
|
|
328
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN todos TEXT`);
|
|
329
|
+
if (!cols.some(c => c.name === 'recent_directories_touched'))
|
|
330
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN recent_directories_touched TEXT`);
|
|
331
|
+
if (!cols.some(c => c.name === 'linear_project'))
|
|
332
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN linear_project TEXT`);
|
|
333
|
+
if (!cols.some(c => c.name === 'linear_project_url'))
|
|
334
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN linear_project_url TEXT`);
|
|
335
|
+
db.exec(`DELETE FROM scan_ledger; DELETE FROM dir_ledger;`);
|
|
336
|
+
}
|
|
255
337
|
}
|
|
256
338
|
/** Open (or return the cached) sessions database, applying migrations as needed. */
|
|
257
339
|
export function getDB() {
|
|
@@ -310,6 +392,11 @@ export function closeDB() {
|
|
|
310
392
|
if (dbInstance) {
|
|
311
393
|
dbInstance.close();
|
|
312
394
|
dbInstance = null;
|
|
395
|
+
// Closing the connection finalizes every prepared statement it owns. Drop
|
|
396
|
+
// the cached upsert/FTS statements too, so the next getDB() rebuilds them
|
|
397
|
+
// against the fresh connection instead of re-running a finalized statement
|
|
398
|
+
// (which throws "statement has been finalized" on the first upsert).
|
|
399
|
+
cachedStmts = {};
|
|
313
400
|
}
|
|
314
401
|
}
|
|
315
402
|
// ---------------------------------------------------------------------------
|
|
@@ -440,6 +527,55 @@ export function getScanStampsForPaths(filePaths) {
|
|
|
440
527
|
}
|
|
441
528
|
return result;
|
|
442
529
|
}
|
|
530
|
+
/**
|
|
531
|
+
* Bulk-load the resumable-parse continuation (parser_state + content_text) plus
|
|
532
|
+
* the stamp for a set of file paths in a single chunked query. Mirrors
|
|
533
|
+
* {@link getScanStampsForPaths}: keys by canonical path and fans results back to
|
|
534
|
+
* every original alias so callers can `.get(filePath)` with the path they passed.
|
|
535
|
+
* The Claude incremental scan uses this to fetch each changed file's prior
|
|
536
|
+
* continuation without an N+1 of {@link getScanStampByPath}.
|
|
537
|
+
*/
|
|
538
|
+
export function getParserStatesForPaths(filePaths) {
|
|
539
|
+
const result = new Map();
|
|
540
|
+
if (filePaths.length === 0)
|
|
541
|
+
return result;
|
|
542
|
+
const db = getDB();
|
|
543
|
+
const canonicalToOriginals = new Map();
|
|
544
|
+
for (const fp of filePaths) {
|
|
545
|
+
const canonical = canonicalLedgerKey(fp);
|
|
546
|
+
const aliases = canonicalToOriginals.get(canonical);
|
|
547
|
+
if (aliases)
|
|
548
|
+
aliases.push(fp);
|
|
549
|
+
else
|
|
550
|
+
canonicalToOriginals.set(canonical, [fp]);
|
|
551
|
+
}
|
|
552
|
+
const canonicalKeys = [...canonicalToOriginals.keys()];
|
|
553
|
+
const CHUNK = 500;
|
|
554
|
+
for (let i = 0; i < canonicalKeys.length; i += CHUNK) {
|
|
555
|
+
const chunk = canonicalKeys.slice(i, i + CHUNK);
|
|
556
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
557
|
+
const rows = db
|
|
558
|
+
.prepare(`
|
|
559
|
+
SELECT file_path, file_mtime_ms, file_size, scanned_at, parser_state, content_text
|
|
560
|
+
FROM scan_ledger
|
|
561
|
+
WHERE file_path IN (${placeholders})
|
|
562
|
+
`)
|
|
563
|
+
.all(...chunk);
|
|
564
|
+
for (const row of rows) {
|
|
565
|
+
const state = {
|
|
566
|
+
parserState: row.parser_state,
|
|
567
|
+
contentText: row.content_text,
|
|
568
|
+
fileMtimeMs: row.file_mtime_ms,
|
|
569
|
+
fileSize: row.file_size,
|
|
570
|
+
scannedAt: row.scanned_at,
|
|
571
|
+
};
|
|
572
|
+
for (const original of canonicalToOriginals.get(row.file_path) || []) {
|
|
573
|
+
result.set(original, state);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return result;
|
|
578
|
+
}
|
|
443
579
|
/**
|
|
444
580
|
* Record scan stamps for files we've looked at. Covers both files that produced
|
|
445
581
|
* a session and files we looked at but chose not to index (e.g. malformed).
|
|
@@ -464,6 +600,73 @@ export function recordScans(entries) {
|
|
|
464
600
|
});
|
|
465
601
|
txn(entries);
|
|
466
602
|
}
|
|
603
|
+
/**
|
|
604
|
+
* Bulk-load the dir ledger for a set of leaf directories in a single SQL query.
|
|
605
|
+
* Mirrors {@link getScanStampsForPaths}: keys by canonical path (so a dir
|
|
606
|
+
* reachable via a symlinked version home and its realpath collapse to one row)
|
|
607
|
+
* and fans the result back out to every original alias so callers can
|
|
608
|
+
* `.get(dirPath)` with the path they passed in.
|
|
609
|
+
*/
|
|
610
|
+
export function getDirLedgerForPaths(dirs) {
|
|
611
|
+
const result = new Map();
|
|
612
|
+
if (dirs.length === 0)
|
|
613
|
+
return result;
|
|
614
|
+
const db = getDB();
|
|
615
|
+
const canonicalToOriginals = new Map();
|
|
616
|
+
for (const d of dirs) {
|
|
617
|
+
const canonical = canonicalLedgerKey(d);
|
|
618
|
+
const aliases = canonicalToOriginals.get(canonical);
|
|
619
|
+
if (aliases)
|
|
620
|
+
aliases.push(d);
|
|
621
|
+
else
|
|
622
|
+
canonicalToOriginals.set(canonical, [d]);
|
|
623
|
+
}
|
|
624
|
+
const canonicalKeys = [...canonicalToOriginals.keys()];
|
|
625
|
+
const CHUNK = 500;
|
|
626
|
+
for (let i = 0; i < canonicalKeys.length; i += CHUNK) {
|
|
627
|
+
const chunk = canonicalKeys.slice(i, i + CHUNK);
|
|
628
|
+
const placeholders = chunk.map(() => '?').join(',');
|
|
629
|
+
const rows = db
|
|
630
|
+
.prepare(`
|
|
631
|
+
SELECT dir_path, dir_mtime_ms, entry_count
|
|
632
|
+
FROM dir_ledger
|
|
633
|
+
WHERE dir_path IN (${placeholders})
|
|
634
|
+
`)
|
|
635
|
+
.all(...chunk);
|
|
636
|
+
for (const row of rows) {
|
|
637
|
+
const stamp = { dirMtimeMs: row.dir_mtime_ms, entryCount: row.entry_count };
|
|
638
|
+
for (const original of canonicalToOriginals.get(row.dir_path) || []) {
|
|
639
|
+
result.set(original, stamp);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return result;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Upsert dir-scan stamps. Recorded after a full readdir of a leaf transcript
|
|
647
|
+
* dir so the next scan can skip that dir when its (mtime, entry_count) is
|
|
648
|
+
* unchanged. Mirrors {@link recordScans}.
|
|
649
|
+
*/
|
|
650
|
+
export function recordDirScans(entries) {
|
|
651
|
+
if (entries.length === 0)
|
|
652
|
+
return;
|
|
653
|
+
const db = getDB();
|
|
654
|
+
const stmt = db.prepare(`
|
|
655
|
+
INSERT INTO dir_ledger (dir_path, dir_mtime_ms, entry_count, scanned_at)
|
|
656
|
+
VALUES (?, ?, ?, ?)
|
|
657
|
+
ON CONFLICT(dir_path) DO UPDATE SET
|
|
658
|
+
dir_mtime_ms = excluded.dir_mtime_ms,
|
|
659
|
+
entry_count = excluded.entry_count,
|
|
660
|
+
scanned_at = excluded.scanned_at
|
|
661
|
+
`);
|
|
662
|
+
const now = Date.now();
|
|
663
|
+
const txn = db.transaction((items) => {
|
|
664
|
+
for (const { dirPath, dirMtimeMs, entryCount } of items) {
|
|
665
|
+
stmt.run(canonicalLedgerKey(dirPath), dirMtimeMs, entryCount, now);
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
txn(entries);
|
|
669
|
+
}
|
|
467
670
|
const upsertSessionStmt = (db) => db.prepare(`
|
|
468
671
|
INSERT INTO sessions (
|
|
469
672
|
id, short_id, agent, origin, routine_name, routine_run_id,
|
|
@@ -471,14 +674,16 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
471
674
|
project, cwd, git_branch, topic, label, message_count, token_count,
|
|
472
675
|
output_tokens, cost_usd, duration_ms,
|
|
473
676
|
file_path, file_mtime_ms, file_size, scanned_at, is_team_origin,
|
|
474
|
-
pr_url, pr_number, worktree_slug, ticket_id, plan
|
|
677
|
+
pr_url, pr_number, worktree_slug, ticket_id, plan, todos,
|
|
678
|
+
recent_directories_touched, linear_project, linear_project_url
|
|
475
679
|
) VALUES (
|
|
476
680
|
@id, @short_id, @agent, @origin, @routine_name, @routine_run_id,
|
|
477
681
|
@version, @account, @timestamp, @last_activity,
|
|
478
682
|
@project, @cwd, @git_branch, @topic, @label, @message_count, @token_count,
|
|
479
683
|
@output_tokens, @cost_usd, @duration_ms,
|
|
480
684
|
@file_path, @file_mtime_ms, @file_size, @scanned_at, @is_team_origin,
|
|
481
|
-
@pr_url, @pr_number, @worktree_slug, @ticket_id, @plan
|
|
685
|
+
@pr_url, @pr_number, @worktree_slug, @ticket_id, @plan, @todos,
|
|
686
|
+
@recent_directories_touched, @linear_project, @linear_project_url
|
|
482
687
|
)
|
|
483
688
|
ON CONFLICT(id) DO UPDATE SET
|
|
484
689
|
short_id = excluded.short_id,
|
|
@@ -494,7 +699,15 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
494
699
|
cwd = excluded.cwd,
|
|
495
700
|
git_branch = excluded.git_branch,
|
|
496
701
|
topic = excluded.topic,
|
|
497
|
-
label
|
|
702
|
+
-- Never let an empty/placeholder incoming label clobber a good stored one.
|
|
703
|
+
-- A real incoming label (non-empty after trim) still wins; a blank one keeps
|
|
704
|
+
-- the label seeded by --name (seedLabelsFromNames) or refined by an agent
|
|
705
|
+
-- title / rename (syncLabels). A bare rescan carries no label, so it must
|
|
706
|
+
-- preserve, not erase, the good one already stored.
|
|
707
|
+
label = CASE
|
|
708
|
+
WHEN excluded.label IS NULL OR trim(excluded.label) = '' THEN sessions.label
|
|
709
|
+
ELSE excluded.label
|
|
710
|
+
END,
|
|
498
711
|
message_count = excluded.message_count,
|
|
499
712
|
token_count = excluded.token_count,
|
|
500
713
|
output_tokens = excluded.output_tokens,
|
|
@@ -509,10 +722,40 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
509
722
|
pr_number = excluded.pr_number,
|
|
510
723
|
worktree_slug = excluded.worktree_slug,
|
|
511
724
|
ticket_id = excluded.ticket_id,
|
|
512
|
-
plan = excluded.plan
|
|
725
|
+
plan = excluded.plan,
|
|
726
|
+
todos = excluded.todos,
|
|
727
|
+
recent_directories_touched = excluded.recent_directories_touched,
|
|
728
|
+
linear_project = CASE
|
|
729
|
+
WHEN excluded.ticket_id IS NOT sessions.ticket_id THEN excluded.linear_project
|
|
730
|
+
ELSE COALESCE(excluded.linear_project, sessions.linear_project)
|
|
731
|
+
END,
|
|
732
|
+
linear_project_url = CASE
|
|
733
|
+
WHEN excluded.ticket_id IS NOT sessions.ticket_id THEN excluded.linear_project_url
|
|
734
|
+
ELSE COALESCE(excluded.linear_project_url, sessions.linear_project_url)
|
|
735
|
+
END
|
|
513
736
|
`);
|
|
737
|
+
function enrichCachedSessionMeta(meta) {
|
|
738
|
+
if (!meta.filePath)
|
|
739
|
+
return meta;
|
|
740
|
+
try {
|
|
741
|
+
const events = parseSession(meta.filePath, meta.agent);
|
|
742
|
+
return {
|
|
743
|
+
...meta,
|
|
744
|
+
todos: extractTodoProgressFromEvents(events),
|
|
745
|
+
recentDirectoriesTouched: extractRecentDirectoriesTouched(events, meta.cwd),
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
catch {
|
|
749
|
+
// Synthetic/cloud rows can intentionally name a transcript that is not local.
|
|
750
|
+
return meta;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
514
753
|
const deleteTextStmt = (db) => db.prepare(`DELETE FROM session_text WHERE session_id = ?`);
|
|
515
754
|
const insertTextStmt = (db) => db.prepare(`INSERT INTO session_text (session_id, label, topic, project, content) VALUES (?, ?, ?, ?, ?)`);
|
|
755
|
+
// Read back the label the upsert actually stored (which may be the preserved
|
|
756
|
+
// one, not the incoming blank) so the FTS label column stays consistent with
|
|
757
|
+
// sessions.label after a bare rescan.
|
|
758
|
+
const readLabelStmt = (db) => db.prepare(`SELECT label FROM sessions WHERE id = ?`);
|
|
516
759
|
let cachedStmts = {};
|
|
517
760
|
function stmts(db) {
|
|
518
761
|
if (!cachedStmts.upsert) {
|
|
@@ -520,17 +763,29 @@ function stmts(db) {
|
|
|
520
763
|
upsert: upsertSessionStmt(db),
|
|
521
764
|
delText: deleteTextStmt(db),
|
|
522
765
|
insText: insertTextStmt(db),
|
|
766
|
+
readLabel: readLabelStmt(db),
|
|
523
767
|
};
|
|
524
768
|
}
|
|
525
769
|
return cachedStmts;
|
|
526
770
|
}
|
|
771
|
+
/**
|
|
772
|
+
* Return the label stored for a session, as text for the FTS index (never NULL).
|
|
773
|
+
* Called inside the upsert transaction, AFTER the row upsert, so it reflects the
|
|
774
|
+
* preserve-non-empty-label rule in the ON CONFLICT clause rather than the raw
|
|
775
|
+
* incoming label.
|
|
776
|
+
*/
|
|
777
|
+
function storedFtsLabel(readLabel, id) {
|
|
778
|
+
const row = readLabel.get(id);
|
|
779
|
+
return row?.label ?? '';
|
|
780
|
+
}
|
|
527
781
|
/**
|
|
528
782
|
* Upsert a session row and replace its FTS5 content in a single transaction.
|
|
529
783
|
* `content` is the tokenizable user-prompt text; pass '' to leave the row unsearchable.
|
|
530
784
|
*/
|
|
531
785
|
export function upsertSession(meta, content, scan) {
|
|
786
|
+
meta = enrichCachedSessionMeta(meta);
|
|
532
787
|
const db = getDB();
|
|
533
|
-
const { upsert, delText, insText } = stmts(db);
|
|
788
|
+
const { upsert, delText, insText, readLabel } = stmts(db);
|
|
534
789
|
const row = {
|
|
535
790
|
id: meta.id,
|
|
536
791
|
short_id: meta.shortId,
|
|
@@ -562,11 +817,18 @@ export function upsertSession(meta, content, scan) {
|
|
|
562
817
|
worktree_slug: meta.worktreeSlug ?? null,
|
|
563
818
|
ticket_id: meta.ticketId ?? null,
|
|
564
819
|
plan: meta.plan ?? null,
|
|
820
|
+
todos: meta.todos ? JSON.stringify(meta.todos) : null,
|
|
821
|
+
recent_directories_touched: meta.recentDirectoriesTouched ? JSON.stringify(meta.recentDirectoriesTouched) : null,
|
|
822
|
+
linear_project: meta.linearProject ?? null,
|
|
823
|
+
linear_project_url: meta.linearProjectUrl ?? null,
|
|
565
824
|
};
|
|
566
825
|
const txn = db.transaction(() => {
|
|
567
826
|
upsert.run(row);
|
|
568
827
|
delText.run(meta.id);
|
|
569
|
-
insText.run(meta.id,
|
|
828
|
+
insText.run(meta.id,
|
|
829
|
+
// Use the label the upsert actually stored (preserve-non-empty rule),
|
|
830
|
+
// not the raw incoming one, so FTS label ranking survives a bare rescan.
|
|
831
|
+
storedFtsLabel(readLabel, meta.id), meta.topic ?? '', meta.project ?? '', content ?? '');
|
|
570
832
|
});
|
|
571
833
|
txn();
|
|
572
834
|
}
|
|
@@ -575,15 +837,22 @@ export function upsertSessionsBatch(entries) {
|
|
|
575
837
|
if (entries.length === 0)
|
|
576
838
|
return;
|
|
577
839
|
const db = getDB();
|
|
578
|
-
const { upsert, delText, insText } = stmts(db);
|
|
840
|
+
const { upsert, delText, insText, readLabel } = stmts(db);
|
|
579
841
|
const now = Date.now();
|
|
842
|
+
// Persist the Claude resumable-parse continuation (parser_state + content_text)
|
|
843
|
+
// alongside the stamp. On a full/incremental Claude parse the caller passes the
|
|
844
|
+
// serialized newState + accumulated user doc so the NEXT scan can resume from
|
|
845
|
+
// the persisted offset (B-2). Other scanners pass neither, leaving both columns
|
|
846
|
+
// NULL exactly as before — their ledger rows are unaffected.
|
|
580
847
|
const ledger = db.prepare(`
|
|
581
|
-
INSERT INTO scan_ledger (file_path, file_mtime_ms, file_size, scanned_at)
|
|
582
|
-
VALUES (?, ?, ?, ?)
|
|
848
|
+
INSERT INTO scan_ledger (file_path, file_mtime_ms, file_size, scanned_at, parser_state, content_text)
|
|
849
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
583
850
|
ON CONFLICT(file_path) DO UPDATE SET
|
|
584
851
|
file_mtime_ms = excluded.file_mtime_ms,
|
|
585
852
|
file_size = excluded.file_size,
|
|
586
|
-
scanned_at = excluded.scanned_at
|
|
853
|
+
scanned_at = excluded.scanned_at,
|
|
854
|
+
parser_state = excluded.parser_state,
|
|
855
|
+
content_text = excluded.content_text
|
|
587
856
|
`);
|
|
588
857
|
// Build a lookup from canonical file path → entry, used inside the write
|
|
589
858
|
// transaction to re-check the ledger AFTER acquiring the lock. When a
|
|
@@ -594,6 +863,9 @@ export function upsertSessionsBatch(entries) {
|
|
|
594
863
|
const byPath = new Map(entries
|
|
595
864
|
.filter(e => e.scan && e.meta.filePath)
|
|
596
865
|
.map(e => [canonicalLedgerKey(e.meta.filePath), e]));
|
|
866
|
+
const enrichedEntries = entries.map(entry => entry.meta.agent === 'claude' || entry.meta.agent === 'codex'
|
|
867
|
+
? entry
|
|
868
|
+
: { ...entry, meta: enrichCachedSessionMeta(entry.meta) });
|
|
597
869
|
const txn = db.transaction((items) => {
|
|
598
870
|
// Re-read the ledger now that we hold the write lock. Any file committed
|
|
599
871
|
// by a concurrent process since our pre-scan is visible here.
|
|
@@ -613,7 +885,7 @@ export function upsertSessionsBatch(entries) {
|
|
|
613
885
|
}
|
|
614
886
|
}
|
|
615
887
|
}
|
|
616
|
-
for (const { meta, content, scan } of items) {
|
|
888
|
+
for (const { meta, content, scan, parserState, contentText } of items) {
|
|
617
889
|
if (alreadyIndexed.has(meta.id))
|
|
618
890
|
continue;
|
|
619
891
|
// Per-row guard: one malformed session (e.g. a required field that resolves to
|
|
@@ -655,11 +927,18 @@ export function upsertSessionsBatch(entries) {
|
|
|
655
927
|
worktree_slug: meta.worktreeSlug ?? null,
|
|
656
928
|
ticket_id: meta.ticketId ?? null,
|
|
657
929
|
plan: meta.plan ?? null,
|
|
930
|
+
todos: meta.todos ? JSON.stringify(meta.todos) : null,
|
|
931
|
+
recent_directories_touched: meta.recentDirectoriesTouched ? JSON.stringify(meta.recentDirectoriesTouched) : null,
|
|
932
|
+
linear_project: meta.linearProject ?? null,
|
|
933
|
+
linear_project_url: meta.linearProjectUrl ?? null,
|
|
658
934
|
});
|
|
659
935
|
delText.run(meta.id);
|
|
660
|
-
insText.run(meta.id,
|
|
936
|
+
insText.run(meta.id,
|
|
937
|
+
// Mirror upsertSession: index the label the upsert actually stored
|
|
938
|
+
// (preserve-non-empty rule), not the raw incoming one.
|
|
939
|
+
storedFtsLabel(readLabel, meta.id), meta.topic ?? '', meta.project ?? '', content ?? '');
|
|
661
940
|
if (scan && meta.filePath) {
|
|
662
|
-
ledger.run(canonicalLedgerKey(meta.filePath), scan.fileMtimeMs, scan.fileSize, now);
|
|
941
|
+
ledger.run(canonicalLedgerKey(meta.filePath), scan.fileMtimeMs, scan.fileSize, now, parserState ?? null, contentText ?? null);
|
|
663
942
|
}
|
|
664
943
|
}
|
|
665
944
|
catch (err) {
|
|
@@ -669,7 +948,7 @@ export function upsertSessionsBatch(entries) {
|
|
|
669
948
|
}
|
|
670
949
|
}
|
|
671
950
|
});
|
|
672
|
-
txn(
|
|
951
|
+
txn(enrichedEntries);
|
|
673
952
|
}
|
|
674
953
|
/**
|
|
675
954
|
* Sync labels for a set of sessions. For each id in the map, if the stored
|
|
@@ -832,8 +1111,22 @@ function rowToMeta(row) {
|
|
|
832
1111
|
worktreeSlug: row.worktree_slug ?? undefined,
|
|
833
1112
|
ticketId: row.ticket_id ?? undefined,
|
|
834
1113
|
plan: row.plan ?? undefined,
|
|
1114
|
+
todos: parseJsonColumn(row.todos),
|
|
1115
|
+
recentDirectoriesTouched: parseJsonColumn(row.recent_directories_touched),
|
|
1116
|
+
linearProject: row.linear_project ?? undefined,
|
|
1117
|
+
linearProjectUrl: row.linear_project_url ?? undefined,
|
|
835
1118
|
};
|
|
836
1119
|
}
|
|
1120
|
+
function parseJsonColumn(value) {
|
|
1121
|
+
if (!value)
|
|
1122
|
+
return undefined;
|
|
1123
|
+
try {
|
|
1124
|
+
return JSON.parse(value);
|
|
1125
|
+
}
|
|
1126
|
+
catch {
|
|
1127
|
+
return undefined;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
837
1130
|
/**
|
|
838
1131
|
* The recency signal used to sort and label the listing: last-message time when
|
|
839
1132
|
* a parser computed it (`meta.lastActivity` from `lastTsMs`), else the file's
|
|
@@ -854,6 +1147,11 @@ export function isSessionActivityFresh(row, maxAgeMs, nowMs) {
|
|
|
854
1147
|
const activityMs = Number.isFinite(parsedActivityMs) ? parsedActivityMs : row.file_mtime_ms ?? undefined;
|
|
855
1148
|
return activityMs != null && nowMs - activityMs <= maxAgeMs;
|
|
856
1149
|
}
|
|
1150
|
+
/** Persist a lazily resolved Linear project without reparsing the transcript. */
|
|
1151
|
+
export function cacheLinearProject(sessionId, project, projectUrl) {
|
|
1152
|
+
getDB().prepare(`UPDATE sessions SET linear_project = ?, linear_project_url = ? WHERE id = ?`)
|
|
1153
|
+
.run(project, projectUrl, sessionId);
|
|
1154
|
+
}
|
|
857
1155
|
/**
|
|
858
1156
|
* Newest indexed session file for an agent working in `cwd`. Lets the live
|
|
859
1157
|
* `--active` scanner locate a Codex transcript (whose files are date-partitioned,
|
|
@@ -909,9 +1207,12 @@ function buildSessionWhere(options) {
|
|
|
909
1207
|
params.push(options.cwd);
|
|
910
1208
|
}
|
|
911
1209
|
if (options.cwdPrefix) {
|
|
912
|
-
//
|
|
913
|
-
// the subdir wildcard must too — a hardcoded '/'
|
|
914
|
-
// `C:\a\b` subpath and the listing comes back empty.
|
|
1210
|
+
// A LOCAL stored cwd uses the host path separator (normalizeCwd runs
|
|
1211
|
+
// path.normalize on it), so the subdir wildcard must too — a hardcoded '/'
|
|
1212
|
+
// never matches a Windows `C:\a\b` subpath and the listing comes back empty.
|
|
1213
|
+
// A cwd recorded on another machine keeps its own separators, so this
|
|
1214
|
+
// wildcard does not match foreign subpaths; both sides go through
|
|
1215
|
+
// normalizeCwd, so the exact `cwd = ?` comparison still holds for them.
|
|
915
1216
|
where.push('(cwd = ? OR cwd LIKE ?)');
|
|
916
1217
|
params.push(options.cwdPrefix, options.cwdPrefix + path.sep + '%');
|
|
917
1218
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** A session's foreground/background presence. */
|
|
2
|
+
export type Presence = 'attached' | 'background' | 'parked';
|
|
3
|
+
export interface DetachRecord {
|
|
4
|
+
sessionId: string;
|
|
5
|
+
agent: string;
|
|
6
|
+
cwd?: string;
|
|
7
|
+
/** pid of the detached headless continuation `agents sessions detach` spawned. */
|
|
8
|
+
headlessPid: number;
|
|
9
|
+
/**
|
|
10
|
+
* Start-time fingerprint of {@link headlessPid} at spawn, so a liveness check
|
|
11
|
+
* survives PID reuse: the pid is only "our" continuation if it still occupies
|
|
12
|
+
* the process we launched. Null when the platform capture failed.
|
|
13
|
+
*/
|
|
14
|
+
headlessStartTime: string | null;
|
|
15
|
+
detachedAtMs: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function writeDetachRecord(rec: DetachRecord): void;
|
|
18
|
+
export declare function readDetachRecord(sessionId: string): DetachRecord | undefined;
|
|
19
|
+
export declare function clearDetachRecord(sessionId: string): void;
|
|
20
|
+
export declare function listDetachRecords(): DetachRecord[];
|
|
21
|
+
/** True while the recorded headless continuation is still the live process we spawned. */
|
|
22
|
+
export declare function isHeadlessAlive(rec: DetachRecord): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Presence for a session id from the detach store alone:
|
|
25
|
+
* - no record -> undefined (caller decides: `attached` for a live
|
|
26
|
+
* interactive row, nothing for cloud/team rows)
|
|
27
|
+
* - record + pid alive -> `background` (headless continuation running)
|
|
28
|
+
* - record + pid exited -> `parked` (the run finished; transcript is durable)
|
|
29
|
+
*/
|
|
30
|
+
export declare function presenceFromStore(sessionId: string): Presence | undefined;
|