@faithfulalabi/agent-lens 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/bin/agent-lens.js +40 -0
- package/bin/package.json +4 -0
- package/dist/src/archive/cron-log.js +41 -0
- package/dist/src/archive/discover.js +89 -0
- package/dist/src/archive/index.js +7 -0
- package/dist/src/archive/lock.js +119 -0
- package/dist/src/archive/log.js +15 -0
- package/dist/src/archive/mirror.js +366 -0
- package/dist/src/archive/paths.js +159 -0
- package/dist/src/archive/read.js +133 -0
- package/dist/src/archive/report.js +272 -0
- package/dist/src/archive/seal.js +76 -0
- package/dist/src/archive/sidecar.js +39 -0
- package/dist/src/cli/args.js +47 -0
- package/dist/src/cli/commands/archive.js +65 -0
- package/dist/src/cli/commands/doctor.js +191 -0
- package/dist/src/cli/commands/prune.js +159 -0
- package/dist/src/cli/commands/rebuild.js +98 -0
- package/dist/src/cli/commands/schedule.js +325 -0
- package/dist/src/cli/commands/start.js +83 -0
- package/dist/src/cli/commands/warm.js +96 -0
- package/dist/src/cli/index.js +102 -0
- package/dist/src/content/resolve.js +163 -0
- package/dist/src/corpus/env.js +32 -0
- package/dist/src/corpus/paths.js +70 -0
- package/dist/src/corpus/scan.js +85 -0
- package/dist/src/corpus/watch.js +189 -0
- package/dist/src/db/freshness.js +82 -0
- package/dist/src/db/open.js +74 -0
- package/dist/src/db/read.js +266 -0
- package/dist/src/db/schema.js +312 -0
- package/dist/src/db/sidecars.js +216 -0
- package/dist/src/db/spill-index.js +68 -0
- package/dist/src/db/write.js +279 -0
- package/dist/src/project/pipeline.js +307 -0
- package/dist/src/project/subagents.js +41 -0
- package/dist/src/project/tools.js +94 -0
- package/dist/src/server/api.js +249 -0
- package/dist/src/server/app.js +28 -0
- package/dist/src/server/config.js +24 -0
- package/dist/src/server/drift-report.js +35 -0
- package/dist/src/server/index.js +1 -0
- package/dist/src/server/live.js +109 -0
- package/dist/src/server/middleware/host-guard.js +42 -0
- package/dist/src/server/middleware/token-auth.js +19 -0
- package/dist/src/server/start.js +150 -0
- package/dist/src/server/static-ui.js +97 -0
- package/dist/src/server/stream.js +50 -0
- package/dist/src/server/warm.js +48 -0
- package/dist/src/shared/api.js +1 -0
- package/dist/src/shared/entities.js +1 -0
- package/dist/src/shared/index.js +2 -0
- package/dist/src/shared/pricing.js +68 -0
- package/dist/src/shared/token.js +39 -0
- package/dist/src/transcript/accessors.js +28 -0
- package/dist/src/transcript/agents.js +44 -0
- package/dist/src/transcript/blocks.js +75 -0
- package/dist/src/transcript/drift.js +42 -0
- package/dist/src/transcript/human.js +65 -0
- package/dist/src/transcript/line.js +251 -0
- package/dist/src/transcript/raw-types.js +1 -0
- package/dist/src/transcript/spill.js +122 -0
- package/dist/src/transcript/usage.js +63 -0
- package/dist/src/transcript/version.js +1 -0
- package/package.json +70 -0
- package/ui/dist/assets/index-CKKoKUCq.js +254 -0
- package/ui/dist/assets/index-Chza4fL6.css +1 -0
- package/ui/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/ui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/ui/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/ui/dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/ui/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/ui/dist/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
- package/ui/dist/index.html +21 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { createArchiveReader } from '../archive/read.js';
|
|
2
|
+
import { resolveArchiveRoot, resolveTranscriptRoot } from '../archive/paths.js';
|
|
3
|
+
import { createContentEnv, createContentResolver, createSpillLocator } from '../content/resolve.js';
|
|
4
|
+
import { ensureProjected } from '../db/freshness.js';
|
|
5
|
+
import { readChildSessionIds, readEventArchivePath, readMeta, readTreeRoots } from '../db/read.js';
|
|
6
|
+
import { readSessionEnvelope } from '../db/sidecars.js';
|
|
7
|
+
import { indexSpills } from '../db/spill-index.js';
|
|
8
|
+
import { markRollupComplete, recomputeSubagentRollups, setParentSession, upsertSessionIndex, writeMeta, } from '../db/write.js';
|
|
9
|
+
import { createProjectionEnv } from './env.js';
|
|
10
|
+
import { decodeProjectDir, projectSlugOf, rowIdOf, workflowParentOf } from './paths.js';
|
|
11
|
+
import { scanCorpus } from './scan.js';
|
|
12
|
+
const INTERVAL_MS = 1000;
|
|
13
|
+
const WAVE2_DEADLINE_MS = 250;
|
|
14
|
+
export function emptyReport() {
|
|
15
|
+
return {
|
|
16
|
+
walked: 0,
|
|
17
|
+
indexed: 0,
|
|
18
|
+
indexed_ids: [],
|
|
19
|
+
deferred: 0,
|
|
20
|
+
deferred_wf_sidecars: 0,
|
|
21
|
+
unchanged: 0,
|
|
22
|
+
excluded: [],
|
|
23
|
+
ignored: 0,
|
|
24
|
+
unkeyable: [],
|
|
25
|
+
envelope_incomplete: [],
|
|
26
|
+
projection_failed: [],
|
|
27
|
+
spills_indexed: 0,
|
|
28
|
+
spills_removed: 0,
|
|
29
|
+
spills_skipped: [],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function createSpillIndexEnv(db, reader, roots) {
|
|
33
|
+
const archivePathOf = (id) => readEventArchivePath(db, id)?.archive_path;
|
|
34
|
+
const env = createContentEnv(reader, roots);
|
|
35
|
+
return {
|
|
36
|
+
resolve: createContentResolver(archivePathOf, env),
|
|
37
|
+
locate: createSpillLocator(archivePathOf, env),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function conservationOf(report) {
|
|
41
|
+
return {
|
|
42
|
+
walked: report.walked,
|
|
43
|
+
accounted: report.indexed +
|
|
44
|
+
report.deferred +
|
|
45
|
+
report.unchanged +
|
|
46
|
+
report.ignored +
|
|
47
|
+
report.excluded.length +
|
|
48
|
+
report.unkeyable.length +
|
|
49
|
+
report.envelope_incomplete.length,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function startCorpusSweep(options) {
|
|
53
|
+
const sweep = createCorpusSweep(options);
|
|
54
|
+
sweep.tick();
|
|
55
|
+
return sweep.bind();
|
|
56
|
+
}
|
|
57
|
+
export function createCorpusSweep(options) {
|
|
58
|
+
const { db } = options;
|
|
59
|
+
const now = options.now ?? Date.now;
|
|
60
|
+
const intervalMs = options.intervalMs ?? INTERVAL_MS;
|
|
61
|
+
const deadlineMs = options.wave2DeadlineMs ?? WAVE2_DEADLINE_MS;
|
|
62
|
+
const archiveRoot = resolveArchiveRoot(options.dataDir);
|
|
63
|
+
const sourceRoot = resolveTranscriptRoot(options.transcriptRoot);
|
|
64
|
+
const reader = options.reader ?? createArchiveReader();
|
|
65
|
+
const env = createProjectionEnv(reader, { archiveRoot, transcriptRoot: sourceRoot });
|
|
66
|
+
const spillEnv = createSpillIndexEnv(db, reader, [archiveRoot, sourceRoot]);
|
|
67
|
+
let last = emptyReport();
|
|
68
|
+
const runWave1 = (report) => {
|
|
69
|
+
const scan = scanCorpus(db, archiveRoot, sourceRoot);
|
|
70
|
+
report.walked += scan.walked;
|
|
71
|
+
report.unchanged += scan.unchanged;
|
|
72
|
+
report.deferred += scan.deferred;
|
|
73
|
+
report.ignored += scan.ignored;
|
|
74
|
+
report.excluded.push(...scan.excluded);
|
|
75
|
+
report.unkeyable.push(...scan.unkeyable);
|
|
76
|
+
const queue = [...scan.changed].sort((a, b) => b.fold.mtime_ms - a.fold.mtime_ms);
|
|
77
|
+
for (const entry of queue) {
|
|
78
|
+
const seed = decodeProjectDir(projectSlugOf(entry.relPath));
|
|
79
|
+
const envelope = readSessionEnvelope(reader, entry.archivePath, seed);
|
|
80
|
+
if (envelope === undefined) {
|
|
81
|
+
report.envelope_incomplete.push(entry.relPath);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const id = rowIdOf(entry.relPath);
|
|
85
|
+
upsertSessionIndex(db, {
|
|
86
|
+
id,
|
|
87
|
+
source_path: entry.sourcePath,
|
|
88
|
+
archive_path: entry.archivePath,
|
|
89
|
+
file_mtime_ms: entry.fold.mtime_ms,
|
|
90
|
+
file_size: entry.fold.size,
|
|
91
|
+
project_path: envelope.project_path,
|
|
92
|
+
started_at: envelope.started_at,
|
|
93
|
+
last_activity_at: envelope.last_activity_at,
|
|
94
|
+
});
|
|
95
|
+
report.indexed += 1;
|
|
96
|
+
report.indexed_ids.push(id);
|
|
97
|
+
const parent = workflowParentOf(entry.relPath);
|
|
98
|
+
if (parent !== undefined) {
|
|
99
|
+
setParentSession(db, id, parent);
|
|
100
|
+
report.deferred_wf_sidecars += 1;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
stampIndexMeta(db, sourceRoot, report, now);
|
|
104
|
+
};
|
|
105
|
+
const projectTree = (root, report) => {
|
|
106
|
+
const done = [];
|
|
107
|
+
const seen = new Set();
|
|
108
|
+
let frontier = [root];
|
|
109
|
+
while (frontier.length > 0) {
|
|
110
|
+
const next = [];
|
|
111
|
+
for (const id of frontier) {
|
|
112
|
+
if (seen.has(id))
|
|
113
|
+
continue;
|
|
114
|
+
seen.add(id);
|
|
115
|
+
done.push(id);
|
|
116
|
+
if (ensureProjected(db, id, env) === 'failed')
|
|
117
|
+
report.projection_failed.push(id);
|
|
118
|
+
for (const child of readChildSessionIds(db, id)) {
|
|
119
|
+
if (!seen.has(child))
|
|
120
|
+
next.push(child);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
frontier = next;
|
|
124
|
+
}
|
|
125
|
+
for (const id of done.reverse())
|
|
126
|
+
recomputeSubagentRollups(db, id);
|
|
127
|
+
markRollupComplete(db, root);
|
|
128
|
+
};
|
|
129
|
+
const runWave2 = (report, startedAt) => {
|
|
130
|
+
let processed = 0;
|
|
131
|
+
for (const root of readTreeRoots(db)) {
|
|
132
|
+
if (processed > 0 && now() - startedAt >= deadlineMs)
|
|
133
|
+
break;
|
|
134
|
+
projectTree(root, report);
|
|
135
|
+
processed += 1;
|
|
136
|
+
}
|
|
137
|
+
const spills = indexSpills(db, spillEnv, { now, startedAt, deadlineMs });
|
|
138
|
+
report.spills_indexed += spills.indexed;
|
|
139
|
+
report.spills_removed += spills.removed;
|
|
140
|
+
report.spills_skipped.push(...spills.skipped);
|
|
141
|
+
};
|
|
142
|
+
const tick = () => {
|
|
143
|
+
const startedAt = now();
|
|
144
|
+
const report = emptyReport();
|
|
145
|
+
runWave1(report);
|
|
146
|
+
runWave2(report, startedAt);
|
|
147
|
+
last = report;
|
|
148
|
+
return report;
|
|
149
|
+
};
|
|
150
|
+
const wave1 = () => {
|
|
151
|
+
const report = emptyReport();
|
|
152
|
+
runWave1(report);
|
|
153
|
+
last = report;
|
|
154
|
+
return report;
|
|
155
|
+
};
|
|
156
|
+
const wave2 = () => {
|
|
157
|
+
const report = emptyReport();
|
|
158
|
+
runWave2(report, now());
|
|
159
|
+
last = report;
|
|
160
|
+
return report;
|
|
161
|
+
};
|
|
162
|
+
let timer;
|
|
163
|
+
const handle = {
|
|
164
|
+
tick,
|
|
165
|
+
wave1,
|
|
166
|
+
wave2,
|
|
167
|
+
report: () => last,
|
|
168
|
+
close: () => {
|
|
169
|
+
if (timer !== undefined)
|
|
170
|
+
clearInterval(timer);
|
|
171
|
+
timer = undefined;
|
|
172
|
+
},
|
|
173
|
+
bind: () => {
|
|
174
|
+
timer = setInterval(tick, intervalMs);
|
|
175
|
+
timer.unref?.();
|
|
176
|
+
return handle;
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
return handle;
|
|
180
|
+
}
|
|
181
|
+
function stampIndexMeta(db, sourceRoot, report, now) {
|
|
182
|
+
if (readMeta(db, 'projects_root') !== sourceRoot)
|
|
183
|
+
writeMeta(db, 'projects_root', sourceRoot);
|
|
184
|
+
if (report.unkeyable.length > 0 || report.envelope_incomplete.length > 0)
|
|
185
|
+
return;
|
|
186
|
+
if (readMeta(db, 'index_built_at') !== undefined && report.indexed === 0)
|
|
187
|
+
return;
|
|
188
|
+
writeMeta(db, 'index_built_at', new Date(now()).toISOString());
|
|
189
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { readdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { statSafe } from '../archive/paths.js';
|
|
4
|
+
import { PROJECTOR_VERSION } from '../transcript/version.js';
|
|
5
|
+
import { SCHEMA_VERSION } from './schema.js';
|
|
6
|
+
import { projectSession, writeMeta } from './write.js';
|
|
7
|
+
const TRANSCRIPT_EXT = '.jsonl';
|
|
8
|
+
const SEALED_SUFFIX = '.zst';
|
|
9
|
+
export function foldArchive(archivePath) {
|
|
10
|
+
const parent = statSafe(archivePath) ?? statSafe(`${archivePath}${SEALED_SUFFIX}`);
|
|
11
|
+
if (parent === undefined)
|
|
12
|
+
return undefined;
|
|
13
|
+
const fold = {
|
|
14
|
+
mtime_ms: Math.floor(parent.mtimeMs),
|
|
15
|
+
size: parent.size,
|
|
16
|
+
sidecar_count: 0,
|
|
17
|
+
};
|
|
18
|
+
if (archivePath.endsWith(TRANSCRIPT_EXT)) {
|
|
19
|
+
walk(archivePath.slice(0, -TRANSCRIPT_EXT.length), fold);
|
|
20
|
+
}
|
|
21
|
+
return fold;
|
|
22
|
+
}
|
|
23
|
+
function walk(dir, fold) {
|
|
24
|
+
let entries;
|
|
25
|
+
try {
|
|
26
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
const path = join(dir, entry.name);
|
|
33
|
+
if (entry.isDirectory()) {
|
|
34
|
+
walk(path, fold);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (!entry.isFile())
|
|
38
|
+
continue;
|
|
39
|
+
const stat = statSafe(path);
|
|
40
|
+
if (stat === undefined)
|
|
41
|
+
continue;
|
|
42
|
+
fold.mtime_ms = Math.max(fold.mtime_ms, Math.floor(stat.mtimeMs));
|
|
43
|
+
fold.size += stat.size;
|
|
44
|
+
if (entry.name.endsWith(TRANSCRIPT_EXT))
|
|
45
|
+
fold.sidecar_count += 1;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function fingerprint(fold) {
|
|
49
|
+
return `${fold.mtime_ms}:${fold.size}:${fold.sidecar_count}`;
|
|
50
|
+
}
|
|
51
|
+
export function seedMeta(db) {
|
|
52
|
+
writeMeta(db, 'schema_version', String(SCHEMA_VERSION));
|
|
53
|
+
writeMeta(db, 'projector_version', String(PROJECTOR_VERSION));
|
|
54
|
+
}
|
|
55
|
+
const GATE_SQL = `SELECT archive_path, projected_mtime_ms, projected_size, projector_version
|
|
56
|
+
FROM sessions WHERE id = ?`;
|
|
57
|
+
export function ensureProjectedFold(db, id, env) {
|
|
58
|
+
const row = db.prepare(GATE_SQL).get(id);
|
|
59
|
+
if (row === undefined) {
|
|
60
|
+
seedMeta(db);
|
|
61
|
+
return { outcome: 'unindexed' };
|
|
62
|
+
}
|
|
63
|
+
const fold = foldArchive(row.archive_path);
|
|
64
|
+
if (fold === undefined)
|
|
65
|
+
return { outcome: 'failed' };
|
|
66
|
+
if (row.projected_mtime_ms === fold.mtime_ms &&
|
|
67
|
+
row.projected_size === fold.size &&
|
|
68
|
+
row.projector_version === PROJECTOR_VERSION) {
|
|
69
|
+
return { outcome: 'hit', fold };
|
|
70
|
+
}
|
|
71
|
+
seedMeta(db);
|
|
72
|
+
try {
|
|
73
|
+
projectSession(db, id, env, fold);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return { outcome: 'failed', fold };
|
|
77
|
+
}
|
|
78
|
+
return { outcome: 'projected', fold };
|
|
79
|
+
}
|
|
80
|
+
export function ensureProjected(db, id, env) {
|
|
81
|
+
return ensureProjectedFold(db, id, env).outcome;
|
|
82
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { existsSync, rmSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
4
|
+
import { acquireLock } from '../archive/lock.js';
|
|
5
|
+
import { SCHEMA_DDL, SCHEMA_VERSION } from './schema.js';
|
|
6
|
+
export const CACHE_DB_FILE = 'cache.db';
|
|
7
|
+
export const CACHE_LOCK_FILE = 'cache.db.lock';
|
|
8
|
+
export class DbLockedError extends Error {
|
|
9
|
+
holderPid;
|
|
10
|
+
constructor(message, holderPid) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'DbLockedError';
|
|
13
|
+
this.holderPid = holderPid;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function applyConnectionPragmas(db) {
|
|
17
|
+
db.exec('PRAGMA busy_timeout = 5000');
|
|
18
|
+
db.exec('PRAGMA journal_mode = WAL');
|
|
19
|
+
db.exec('PRAGMA synchronous = NORMAL');
|
|
20
|
+
}
|
|
21
|
+
function journalModeOf(db) {
|
|
22
|
+
return String(db.prepare('PRAGMA journal_mode').get()?.journal_mode);
|
|
23
|
+
}
|
|
24
|
+
function userVersionOf(db) {
|
|
25
|
+
return Number(db.prepare('PRAGMA user_version').get()?.user_version);
|
|
26
|
+
}
|
|
27
|
+
function connect(path) {
|
|
28
|
+
const db = new DatabaseSync(path);
|
|
29
|
+
applyConnectionPragmas(db);
|
|
30
|
+
return db;
|
|
31
|
+
}
|
|
32
|
+
function shutdown(db, lock) {
|
|
33
|
+
if (db?.isOpen === true)
|
|
34
|
+
db.close();
|
|
35
|
+
lock.release();
|
|
36
|
+
}
|
|
37
|
+
export function openReadOnlyDb(dataDir) {
|
|
38
|
+
const path = join(dataDir, CACHE_DB_FILE);
|
|
39
|
+
if (!existsSync(path))
|
|
40
|
+
return undefined;
|
|
41
|
+
return new DatabaseSync(path, { readOnly: true });
|
|
42
|
+
}
|
|
43
|
+
export function openDb(options) {
|
|
44
|
+
const { dataDir, port, identity, readJournalMode = journalModeOf } = options;
|
|
45
|
+
const lockPath = join(dataDir, CACHE_LOCK_FILE);
|
|
46
|
+
const lock = acquireLock(dataDir, identity, lockPath);
|
|
47
|
+
if (lock.state.state === 'held') {
|
|
48
|
+
const holderPid = lock.state.holder_pid;
|
|
49
|
+
const who = `agent-lens is already running (pid ${holderPid ?? 'unknown'})`;
|
|
50
|
+
throw new DbLockedError(port === undefined ? `${who}; ${lockPath} is held` : `${who}; port ${port} was requested`, holderPid);
|
|
51
|
+
}
|
|
52
|
+
const path = join(dataDir, CACHE_DB_FILE);
|
|
53
|
+
let handle;
|
|
54
|
+
try {
|
|
55
|
+
handle = connect(path);
|
|
56
|
+
if (userVersionOf(handle) !== SCHEMA_VERSION) {
|
|
57
|
+
handle.close();
|
|
58
|
+
for (const suffix of ['', '-wal', '-shm'])
|
|
59
|
+
rmSync(path + suffix, { force: true });
|
|
60
|
+
handle = connect(path);
|
|
61
|
+
handle.exec(SCHEMA_DDL);
|
|
62
|
+
}
|
|
63
|
+
const mode = readJournalMode(handle);
|
|
64
|
+
if (mode !== 'wal') {
|
|
65
|
+
throw new Error(`refusing to use ${path}: journal_mode read back '${mode}', not 'wal'`);
|
|
66
|
+
}
|
|
67
|
+
const db = handle;
|
|
68
|
+
return { db, close: () => shutdown(db, lock) };
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
shutdown(handle, lock);
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { PROJECTOR_VERSION } from '../transcript/version.js';
|
|
2
|
+
const PAGE_TAIL = ' LIMIT ? OFFSET ?';
|
|
3
|
+
function pushPageParams(params, page) {
|
|
4
|
+
params.push(page.limit + 1, page.offset);
|
|
5
|
+
}
|
|
6
|
+
function pageOf(db, query, map, { limit, offset }) {
|
|
7
|
+
const rows = db.prepare(query.sql).all(...query.params);
|
|
8
|
+
return {
|
|
9
|
+
items: rows.slice(0, limit).map(map),
|
|
10
|
+
limit,
|
|
11
|
+
offset,
|
|
12
|
+
has_more: rows.length > limit,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
const SORT_EXPRESSIONS = {
|
|
16
|
+
recent: 'last_activity_at',
|
|
17
|
+
cost: 'est_cost',
|
|
18
|
+
tokens: '(tokens_in + tokens_out)',
|
|
19
|
+
errors: 'error_count',
|
|
20
|
+
};
|
|
21
|
+
const SESSION_LIST_COLUMNS = `id, title, preview, project_path, git_branch, model, harness_version,
|
|
22
|
+
started_at, last_activity_at, turn_count, tool_call_count, error_count,
|
|
23
|
+
tokens_in, tokens_out, tokens_cache_read, tokens_cache_write, est_cost,
|
|
24
|
+
agent_count, sub_tool_call_count, sub_error_count, sub_tokens_in, sub_tokens_out,
|
|
25
|
+
sub_tokens_cache_read, sub_tokens_cache_write, sub_est_cost, rollup_state, drift_json`;
|
|
26
|
+
const TOP_LEVEL_ONLY = 'parent_session_id IS NULL';
|
|
27
|
+
const NO_DRIFT = '{}';
|
|
28
|
+
const Q_PREDICATE = "(COALESCE(title,'') || ' ' || COALESCE(preview,'') || ' ' || project_path) LIKE ? ESCAPE '\\'";
|
|
29
|
+
function likeContains(q) {
|
|
30
|
+
return `%${q.replace(/[\\%_]/g, (ch) => `\\${ch}`)}%`;
|
|
31
|
+
}
|
|
32
|
+
function toSessionRow(row) {
|
|
33
|
+
const { drift_json, ...rest } = row;
|
|
34
|
+
return { ...rest, has_drift: drift_json !== NO_DRIFT };
|
|
35
|
+
}
|
|
36
|
+
export function buildSessionListSql(query) {
|
|
37
|
+
const where = [TOP_LEVEL_ONLY];
|
|
38
|
+
const params = [];
|
|
39
|
+
if (query.project !== undefined) {
|
|
40
|
+
where.push('project_path = ?');
|
|
41
|
+
params.push(query.project);
|
|
42
|
+
}
|
|
43
|
+
if (query.q !== undefined) {
|
|
44
|
+
where.push(Q_PREDICATE);
|
|
45
|
+
params.push(likeContains(query.q));
|
|
46
|
+
}
|
|
47
|
+
const sort = query.sort ?? 'recent';
|
|
48
|
+
const order = SORT_EXPRESSIONS[sort];
|
|
49
|
+
if (order === undefined)
|
|
50
|
+
throw new Error(`unknown sort: ${String(sort)}`);
|
|
51
|
+
const sql = `SELECT ${SESSION_LIST_COLUMNS} FROM sessions WHERE ${where.join(' AND ')}` +
|
|
52
|
+
` ORDER BY ${order} DESC, id DESC${PAGE_TAIL}`;
|
|
53
|
+
pushPageParams(params, query);
|
|
54
|
+
return { sql, params };
|
|
55
|
+
}
|
|
56
|
+
export function readSessionList(db, query) {
|
|
57
|
+
return pageOf(db, buildSessionListSql(query), toSessionRow, query);
|
|
58
|
+
}
|
|
59
|
+
export function buildProjectsSql() {
|
|
60
|
+
return {
|
|
61
|
+
sql: `SELECT project_path AS path, count(*) AS session_count,` +
|
|
62
|
+
` max(last_activity_at) AS last_activity_at FROM sessions WHERE ${TOP_LEVEL_ONLY}` +
|
|
63
|
+
` GROUP BY project_path ORDER BY last_activity_at DESC, path ASC`,
|
|
64
|
+
params: [],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function readProjects(db) {
|
|
68
|
+
const query = buildProjectsSql();
|
|
69
|
+
return { items: db.prepare(query.sql).all() };
|
|
70
|
+
}
|
|
71
|
+
const SESSION_HEADER_COLUMNS = `${SESSION_LIST_COLUMNS},
|
|
72
|
+
agent_type, agent_description, spawn_depth, parent_session_id,
|
|
73
|
+
projection_state, projection_error, projector_version, projected_at`;
|
|
74
|
+
function parseDrift(text) {
|
|
75
|
+
const raw = JSON.parse(text);
|
|
76
|
+
return {
|
|
77
|
+
unknown_line_types: raw.unknown_line_types ?? {},
|
|
78
|
+
unknown_block_types: raw.unknown_block_types ?? {},
|
|
79
|
+
unjoined_tool_uses: raw.unjoined_tool_uses ?? 0,
|
|
80
|
+
unresolved_spills: raw.unresolved_spills ?? 0,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function toDetailHeader(row) {
|
|
84
|
+
const { agent_type, agent_description, spawn_depth, parent_session_id, projection_state, projection_error, projector_version, projected_at, ...listRow } = row;
|
|
85
|
+
const header = {
|
|
86
|
+
...toSessionRow(listRow),
|
|
87
|
+
cwd: listRow.project_path,
|
|
88
|
+
projection: {
|
|
89
|
+
state: projection_state,
|
|
90
|
+
projector_version,
|
|
91
|
+
projected_at,
|
|
92
|
+
drift: parseDrift(listRow.drift_json),
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
if (projection_error !== null)
|
|
96
|
+
header.projection.error = projection_error;
|
|
97
|
+
if (agent_type !== null)
|
|
98
|
+
header.agent_type = agent_type;
|
|
99
|
+
if (agent_description !== null)
|
|
100
|
+
header.agent_description = agent_description;
|
|
101
|
+
if (spawn_depth !== null)
|
|
102
|
+
header.spawn_depth = spawn_depth;
|
|
103
|
+
if (parent_session_id !== null)
|
|
104
|
+
header.parent_session_id = parent_session_id;
|
|
105
|
+
return header;
|
|
106
|
+
}
|
|
107
|
+
export function readSessionHeader(db, id) {
|
|
108
|
+
const row = db
|
|
109
|
+
.prepare(`SELECT ${SESSION_HEADER_COLUMNS} FROM sessions WHERE id = ?`)
|
|
110
|
+
.get(id);
|
|
111
|
+
return row === undefined ? undefined : toDetailHeader(row);
|
|
112
|
+
}
|
|
113
|
+
const TURN_COLUMNS = `id, seq, kind, parent_event_id, title, started_at, ended_at, duration_ms, duration_source,
|
|
114
|
+
tokens_in, tokens_out, tokens_cache_read, tokens_cache_write, est_cost,
|
|
115
|
+
tool_call_count, error_count, first_seq, last_seq`;
|
|
116
|
+
export function readTurns(db, session_id) {
|
|
117
|
+
return db
|
|
118
|
+
.prepare(`SELECT ${TURN_COLUMNS} FROM turns WHERE session_id = ? ORDER BY seq`)
|
|
119
|
+
.all(session_id);
|
|
120
|
+
}
|
|
121
|
+
const EVENT_COLUMNS = `id, turn_id, seq, kind, ts, request_id, block_index, name, status,
|
|
122
|
+
duration_ms, duration_source, input, input_bytes, input_storage,
|
|
123
|
+
text, text_bytes, output_storage, spill_path, spill_bytes,
|
|
124
|
+
model, tokens_in, tokens_out, tokens_cache_read, tokens_cache_write, est_cost,
|
|
125
|
+
child_session_id, agent_type, agent_status, raw_type, raw_subtype`;
|
|
126
|
+
export function buildEventPageSql(session_id, query) {
|
|
127
|
+
return {
|
|
128
|
+
sql: `SELECT ${EVENT_COLUMNS} FROM events WHERE session_id = ? AND seq >= ?` +
|
|
129
|
+
` ORDER BY seq LIMIT ?`,
|
|
130
|
+
params: [session_id, query.from_seq, query.limit + 1],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export function readEventPage(db, session_id, query) {
|
|
134
|
+
const built = buildEventPageSql(session_id, query);
|
|
135
|
+
const rows = db.prepare(built.sql).all(...built.params);
|
|
136
|
+
const items = rows.slice(0, query.limit);
|
|
137
|
+
const last = items[items.length - 1];
|
|
138
|
+
return {
|
|
139
|
+
items,
|
|
140
|
+
next_seq: last === undefined ? query.from_seq : last.seq + 1,
|
|
141
|
+
has_more: rows.length > query.limit,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
export function readRunningEventIds(db, session_id) {
|
|
145
|
+
const rows = db
|
|
146
|
+
.prepare(`SELECT id FROM events WHERE session_id = ? AND status = 'running' ORDER BY seq`)
|
|
147
|
+
.all(session_id);
|
|
148
|
+
return rows.map((row) => row.id);
|
|
149
|
+
}
|
|
150
|
+
export function readEventsByIds(db, session_id, ids) {
|
|
151
|
+
if (ids.length === 0)
|
|
152
|
+
return [];
|
|
153
|
+
const holes = ids.map(() => '?').join(', ');
|
|
154
|
+
return db
|
|
155
|
+
.prepare(`SELECT ${EVENT_COLUMNS} FROM events WHERE session_id = ? AND id IN (${holes}) ORDER BY seq`)
|
|
156
|
+
.all(session_id, ...ids);
|
|
157
|
+
}
|
|
158
|
+
export const EVENT_CONTENT_COLUMNS = `id, session_id, block_index, input, input_bytes, input_storage,
|
|
159
|
+
text, text_bytes, output_storage, spill_path, spill_bytes,
|
|
160
|
+
src_offset, src_len, result_offset, result_len, result_block`;
|
|
161
|
+
export function readEventContentRow(db, event_id) {
|
|
162
|
+
return db
|
|
163
|
+
.prepare(`SELECT ${EVENT_CONTENT_COLUMNS} FROM events WHERE id = ?`)
|
|
164
|
+
.get(event_id);
|
|
165
|
+
}
|
|
166
|
+
export function readEventArchivePath(db, session_id) {
|
|
167
|
+
return db
|
|
168
|
+
.prepare(`SELECT archive_path, parent_session_id FROM sessions WHERE id = ?`)
|
|
169
|
+
.get(session_id);
|
|
170
|
+
}
|
|
171
|
+
const SNIPPET = `snippet(events_fts, -1, '<mark>', '</mark>', '…', 12)`;
|
|
172
|
+
const SPILL_SNIPPET = `snippet(spill_fts, 3, '<mark>', '</mark>', '…', 12)`;
|
|
173
|
+
function ftsPhrase(q) {
|
|
174
|
+
return `"${q.replaceAll('"', '""')}"`;
|
|
175
|
+
}
|
|
176
|
+
function searchSql(scoped) {
|
|
177
|
+
const scope = scoped ? ' AND e.session_id = :session' : '';
|
|
178
|
+
const events = `SELECT e.session_id AS session_id, s.title AS session_title, s.project_path AS project_path,` +
|
|
179
|
+
` e.turn_id AS turn_id, e.id AS event_id, e.seq AS seq, e.kind AS kind, e.name AS name,` +
|
|
180
|
+
` e.ts AS ts, ${SNIPPET} AS snippet, rank AS score` +
|
|
181
|
+
` FROM events_fts JOIN events e ON e.rowid = events_fts.rowid` +
|
|
182
|
+
` JOIN sessions s ON s.id = e.session_id` +
|
|
183
|
+
` WHERE events_fts MATCH :q${scope}`;
|
|
184
|
+
const spills = `SELECT e.session_id, s.title, s.project_path, e.turn_id, e.id, e.seq, e.kind, e.name,` +
|
|
185
|
+
` e.ts, ${SPILL_SNIPPET}, rank` +
|
|
186
|
+
` FROM spill_fts JOIN events e ON e.id = spill_fts.event_id` +
|
|
187
|
+
` AND e.output_storage = 'spill' AND e.spill_path = spill_fts.spill_path` +
|
|
188
|
+
` JOIN sessions s ON s.id = e.session_id` +
|
|
189
|
+
` WHERE spill_fts MATCH :q${scope}`;
|
|
190
|
+
return (`SELECT session_id, session_title, project_path, turn_id, event_id, seq, kind, name, ts,` +
|
|
191
|
+
` snippet FROM (${events} UNION ALL ${spills}) ORDER BY score LIMIT :limit`);
|
|
192
|
+
}
|
|
193
|
+
export function searchEvents(db, query) {
|
|
194
|
+
const scoped = query.session !== undefined;
|
|
195
|
+
const sql = searchSql(scoped);
|
|
196
|
+
const run = (q) => db.prepare(sql).all({
|
|
197
|
+
q,
|
|
198
|
+
limit: query.limit,
|
|
199
|
+
...(scoped && { session: query.session }),
|
|
200
|
+
});
|
|
201
|
+
try {
|
|
202
|
+
return run(query.q);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return run(ftsPhrase(query.q));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
export function countUnprojected(db) {
|
|
209
|
+
const row = db
|
|
210
|
+
.prepare(`SELECT count(*) AS n FROM sessions WHERE projection_state NOT IN ('ready', 'empty')`)
|
|
211
|
+
.get();
|
|
212
|
+
return row.n;
|
|
213
|
+
}
|
|
214
|
+
const WARMABLE_SQL = `SELECT id FROM sessions
|
|
215
|
+
WHERE projection_state NOT IN ('ready', 'empty')
|
|
216
|
+
OR projector_version IS NOT :version
|
|
217
|
+
ORDER BY last_activity_at DESC, id DESC`;
|
|
218
|
+
export function readWarmableIds(db) {
|
|
219
|
+
const rows = db.prepare(WARMABLE_SQL).all({ version: PROJECTOR_VERSION });
|
|
220
|
+
return rows.map((row) => row.id);
|
|
221
|
+
}
|
|
222
|
+
export function readDriftRows(db) {
|
|
223
|
+
return db
|
|
224
|
+
.prepare(`SELECT id, title, harness_version, drift_json FROM sessions
|
|
225
|
+
WHERE projection_state = 'ready'`)
|
|
226
|
+
.all();
|
|
227
|
+
}
|
|
228
|
+
export function readHealthCounts(db) {
|
|
229
|
+
const sessions = db
|
|
230
|
+
.prepare(`SELECT count(*) AS sessions_indexed,
|
|
231
|
+
coalesce(sum(projection_state IN ('ready', 'empty')), 0) AS sessions_projected
|
|
232
|
+
FROM sessions`)
|
|
233
|
+
.get();
|
|
234
|
+
const pages = db.prepare('PRAGMA page_count').get();
|
|
235
|
+
const size = db.prepare('PRAGMA page_size').get();
|
|
236
|
+
return { ...sessions, db_bytes: pages.page_count * size.page_size };
|
|
237
|
+
}
|
|
238
|
+
export function readEventCount(db, session_id) {
|
|
239
|
+
const row = db
|
|
240
|
+
.prepare('SELECT count(*) AS n FROM events WHERE session_id = ?')
|
|
241
|
+
.get(session_id);
|
|
242
|
+
return row.n;
|
|
243
|
+
}
|
|
244
|
+
export function readIndexedFolds(db) {
|
|
245
|
+
const rows = db
|
|
246
|
+
.prepare('SELECT archive_path, file_mtime_ms, file_size FROM sessions')
|
|
247
|
+
.all();
|
|
248
|
+
return new Map(rows.map((row) => [row.archive_path, row]));
|
|
249
|
+
}
|
|
250
|
+
export function readMeta(db, key) {
|
|
251
|
+
const row = db.prepare('SELECT value FROM meta WHERE key = ?').get(key);
|
|
252
|
+
return row?.value;
|
|
253
|
+
}
|
|
254
|
+
const TREE_ROOTS_SQL = `SELECT id FROM sessions
|
|
255
|
+
WHERE parent_session_id IS NULL AND rollup_state = 'own'
|
|
256
|
+
ORDER BY last_activity_at DESC, id DESC`;
|
|
257
|
+
export function readTreeRoots(db) {
|
|
258
|
+
const rows = db.prepare(TREE_ROOTS_SQL).all();
|
|
259
|
+
return rows.map((row) => row.id);
|
|
260
|
+
}
|
|
261
|
+
export function readChildSessionIds(db, session_id) {
|
|
262
|
+
const rows = db
|
|
263
|
+
.prepare('SELECT id FROM sessions WHERE parent_session_id = ?')
|
|
264
|
+
.all(session_id);
|
|
265
|
+
return rows.map((row) => row.id);
|
|
266
|
+
}
|