@1agents/session-reader 0.1.1
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/README.md +347 -0
- package/dist/bin/1session.d.ts +2 -0
- package/dist/bin/1session.js +408 -0
- package/dist/src/aggregator.d.ts +12 -0
- package/dist/src/aggregator.js +136 -0
- package/dist/src/classify.d.ts +2 -0
- package/dist/src/classify.js +11 -0
- package/dist/src/distiller.d.ts +7 -0
- package/dist/src/distiller.js +122 -0
- package/dist/src/index.d.ts +19 -0
- package/dist/src/index.js +17 -0
- package/dist/src/ledger.d.ts +19 -0
- package/dist/src/ledger.js +192 -0
- package/dist/src/overview.d.ts +6 -0
- package/dist/src/overview.js +324 -0
- package/dist/src/parsers/antigravity.d.ts +4 -0
- package/dist/src/parsers/antigravity.js +297 -0
- package/dist/src/parsers/claude.d.ts +2 -0
- package/dist/src/parsers/claude.js +214 -0
- package/dist/src/parsers/codex.d.ts +2 -0
- package/dist/src/parsers/codex.js +316 -0
- package/dist/src/parsers/provider.d.ts +24 -0
- package/dist/src/parsers/provider.js +5 -0
- package/dist/src/resolver.d.ts +42 -0
- package/dist/src/resolver.js +145 -0
- package/dist/src/search.d.ts +59 -0
- package/dist/src/search.js +257 -0
- package/dist/src/store/db.d.ts +10 -0
- package/dist/src/store/db.js +89 -0
- package/dist/src/store/edges.d.ts +46 -0
- package/dist/src/store/edges.js +153 -0
- package/dist/src/store/facts.d.ts +8 -0
- package/dist/src/store/facts.js +39 -0
- package/dist/src/store/indexer.d.ts +40 -0
- package/dist/src/store/indexer.js +62 -0
- package/dist/src/store/read.d.ts +29 -0
- package/dist/src/store/read.js +70 -0
- package/dist/src/store/rows.d.ts +35 -0
- package/dist/src/store/rows.js +55 -0
- package/dist/src/store/schema.d.ts +19 -0
- package/dist/src/store/schema.js +142 -0
- package/dist/src/store/write.d.ts +22 -0
- package/dist/src/store/write.js +72 -0
- package/dist/src/turns.d.ts +14 -0
- package/dist/src/turns.js +156 -0
- package/dist/src/types.d.ts +294 -0
- package/dist/src/types.js +13 -0
- package/dist/src/util/jsonl.d.ts +6 -0
- package/dist/src/util/jsonl.js +32 -0
- package/dist/src/util/paths.d.ts +15 -0
- package/dist/src/util/paths.js +69 -0
- package/dist/src/util/text.d.ts +7 -0
- package/dist/src/util/text.js +48 -0
- package/dist/src/writes.d.ts +35 -0
- package/dist/src/writes.js +204 -0
- package/package.json +58 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { analyzableCommand } from '../writes.js';
|
|
2
|
+
import { findSessionRow } from './read.js';
|
|
3
|
+
import { EDGE_VERSION } from './schema.js';
|
|
4
|
+
/** Subcommands that address another session, and what reading one means. */
|
|
5
|
+
const VERB_RELATION = {
|
|
6
|
+
overview: 'references',
|
|
7
|
+
turns: 'references',
|
|
8
|
+
turn: 'references',
|
|
9
|
+
files: 'references',
|
|
10
|
+
commands: 'references',
|
|
11
|
+
errors: 'references',
|
|
12
|
+
jobs: 'references',
|
|
13
|
+
digest: 'references',
|
|
14
|
+
graph: 'references',
|
|
15
|
+
related: 'references',
|
|
16
|
+
handoff: 'handoff_from',
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* `1session <verb> <target>` in any of the shapes it actually gets typed:
|
|
20
|
+
* the bare alias, `node …/1session.js`, `tsx bin/1session.ts`. Flags between
|
|
21
|
+
* verb and target are skipped; the target must look like a session id.
|
|
22
|
+
*/
|
|
23
|
+
const INVOCATION = /(?:^|[\s;&|(])(?:[^\s;&|()]*\b1session(?:\.[jt]s)?)\s+([a-z-]+)((?:\s+--?[\w-]+(?:[= ][^\s;&|()]+)?)*)\s+([A-Za-z0-9][\w-]{5,})/g;
|
|
24
|
+
/** Reads the session-reader invocations out of one tool call. */
|
|
25
|
+
export function invocationsOf(event) {
|
|
26
|
+
// Here-doc payloads are stripped first: this module's own development
|
|
27
|
+
// sessions are full of `cat > x.ts <<'EOF'` blocks quoting example
|
|
28
|
+
// commands, and counting those would invent edges that never happened.
|
|
29
|
+
const command = analyzableCommand(event);
|
|
30
|
+
if (!command)
|
|
31
|
+
return [];
|
|
32
|
+
const found = [];
|
|
33
|
+
INVOCATION.lastIndex = 0;
|
|
34
|
+
for (const match of command.matchAll(INVOCATION)) {
|
|
35
|
+
const verb = match[1];
|
|
36
|
+
const relation = VERB_RELATION[verb];
|
|
37
|
+
if (!relation)
|
|
38
|
+
continue;
|
|
39
|
+
found.push({
|
|
40
|
+
relation,
|
|
41
|
+
target: match[3],
|
|
42
|
+
eventIndex: event.index,
|
|
43
|
+
operation: verb,
|
|
44
|
+
command: command.slice(match.index, match.index + 160).trim(),
|
|
45
|
+
...(event.timestamp ? { timestamp: event.timestamp } : {}),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return found;
|
|
49
|
+
}
|
|
50
|
+
function upsertEdge(db, from, to, relation, candidate, provenance, extractor) {
|
|
51
|
+
db.prepare(`INSERT INTO session_edges (from_session, to_session, relation, first_seen_at, last_seen_at)
|
|
52
|
+
VALUES (?, ?, ?, ?, ?)
|
|
53
|
+
ON CONFLICT (from_session, to_session, relation) DO UPDATE SET
|
|
54
|
+
first_seen_at = MIN(COALESCE(first_seen_at, excluded.first_seen_at), COALESCE(excluded.first_seen_at, first_seen_at)),
|
|
55
|
+
last_seen_at = MAX(COALESCE(last_seen_at, excluded.last_seen_at), COALESCE(excluded.last_seen_at, last_seen_at))`).run(from, to, relation, candidate.timestamp ?? null, candidate.timestamp ?? null);
|
|
56
|
+
const edge = db
|
|
57
|
+
.prepare('SELECT id FROM session_edges WHERE from_session = ? AND to_session = ? AND relation = ?')
|
|
58
|
+
.get(from, to, relation);
|
|
59
|
+
db.prepare(`INSERT OR IGNORE INTO edge_evidence
|
|
60
|
+
(edge_id, source_session, event_index, operation, command, ts, provenance, extractor)
|
|
61
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(edge.id, from, candidate.eventIndex, candidate.operation, candidate.command, candidate.timestamp ?? null, provenance, extractor);
|
|
62
|
+
// Counted, never incremented: a re-index must not inflate the tally.
|
|
63
|
+
db.prepare('UPDATE session_edges SET evidence_count = (SELECT count(*) FROM edge_evidence WHERE edge_id = ?) WHERE id = ?').run(edge.id, edge.id);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Derives L3 for one session from its stored events. Targets that do not
|
|
67
|
+
* resolve to an indexed session are dropped — a dangling edge is worse than
|
|
68
|
+
* no edge.
|
|
69
|
+
*/
|
|
70
|
+
export function deriveEdges(db, id, session) {
|
|
71
|
+
db.exec('BEGIN IMMEDIATE');
|
|
72
|
+
try {
|
|
73
|
+
const stale = db
|
|
74
|
+
.prepare('SELECT edge_id FROM edge_evidence WHERE source_session = ?')
|
|
75
|
+
.all(id);
|
|
76
|
+
db.prepare('DELETE FROM edge_evidence WHERE source_session = ?').run(id);
|
|
77
|
+
let added = 0;
|
|
78
|
+
for (const event of session.turns) {
|
|
79
|
+
for (const candidate of invocationsOf(event)) {
|
|
80
|
+
const row = findSessionRow(db, candidate.target);
|
|
81
|
+
if (!row || row.id === id)
|
|
82
|
+
continue;
|
|
83
|
+
upsertEdge(db, id, row.id, candidate.relation, candidate, 'derived', 'tool:Bash+1session');
|
|
84
|
+
added++;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Edges whose last evidence just disappeared must go with it.
|
|
88
|
+
for (const { edge_id } of stale) {
|
|
89
|
+
db.prepare('DELETE FROM session_edges WHERE id = ? AND NOT EXISTS (SELECT 1 FROM edge_evidence WHERE edge_id = ?)').run(edge_id, edge_id);
|
|
90
|
+
}
|
|
91
|
+
db.prepare('UPDATE sessions SET edge_version = ? WHERE id = ?').run(EDGE_VERSION, id);
|
|
92
|
+
db.exec('COMMIT');
|
|
93
|
+
return added;
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
db.exec('ROLLBACK');
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Records an edge at the moment it happens, when the caller session is known.
|
|
102
|
+
* Injected by a hook or the ACP context as `SESSION_READER_CALLER_SESSION`;
|
|
103
|
+
* absent everywhere else, in which case nothing is written.
|
|
104
|
+
*/
|
|
105
|
+
export function captureRuntimeEdge(db, verb, target) {
|
|
106
|
+
const caller = process.env.SESSION_READER_CALLER_SESSION?.trim();
|
|
107
|
+
const relation = VERB_RELATION[verb];
|
|
108
|
+
if (!caller || !relation)
|
|
109
|
+
return;
|
|
110
|
+
const from = findSessionRow(db, caller);
|
|
111
|
+
const to = findSessionRow(db, target);
|
|
112
|
+
if (!from || !to || from.id === to.id)
|
|
113
|
+
return;
|
|
114
|
+
const now = new Date().toISOString();
|
|
115
|
+
db.exec('BEGIN IMMEDIATE');
|
|
116
|
+
try {
|
|
117
|
+
upsertEdge(db, from.id, to.id, relation, { relation, target, eventIndex: -1, operation: verb, command: `1session ${verb} ${target}`, timestamp: now }, 'observed', 'runtime:caller-env');
|
|
118
|
+
db.exec('COMMIT');
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
db.exec('ROLLBACK');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export function edgesOf(db, id) {
|
|
125
|
+
const rows = db
|
|
126
|
+
.prepare(`SELECT from_session, to_session, relation, evidence_count, first_seen_at, last_seen_at
|
|
127
|
+
FROM session_edges WHERE from_session = ? OR to_session = ?
|
|
128
|
+
ORDER BY evidence_count DESC`)
|
|
129
|
+
.all(id, id);
|
|
130
|
+
return rows.map((row) => ({
|
|
131
|
+
from: row.from_session,
|
|
132
|
+
to: row.to_session,
|
|
133
|
+
relation: row.relation,
|
|
134
|
+
evidenceCount: row.evidence_count,
|
|
135
|
+
...(row.first_seen_at ? { firstSeenAt: row.first_seen_at } : {}),
|
|
136
|
+
...(row.last_seen_at ? { lastSeenAt: row.last_seen_at } : {}),
|
|
137
|
+
direction: row.from_session === id ? 'out' : 'in',
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
export function edgeEvidence(db, from, to, relation) {
|
|
141
|
+
const rows = db
|
|
142
|
+
.prepare(`SELECT e.event_index, e.operation, e.ts, e.extractor
|
|
143
|
+
FROM edge_evidence e JOIN session_edges s ON s.id = e.edge_id
|
|
144
|
+
WHERE s.from_session = ? AND s.to_session = ? AND s.relation = ?
|
|
145
|
+
ORDER BY e.event_index`)
|
|
146
|
+
.all(from, to, relation);
|
|
147
|
+
return rows.map((row) => ({
|
|
148
|
+
eventIndex: row.event_index,
|
|
149
|
+
operation: row.operation,
|
|
150
|
+
...(row.ts ? { ts: row.ts } : {}),
|
|
151
|
+
extractor: row.extractor,
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import type { NormalizedSession } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Materializes L2. The rules themselves stay in `ledger.ts` — this only moves
|
|
5
|
+
* their output into rows, so a stored fact and a freshly computed one can
|
|
6
|
+
* never disagree.
|
|
7
|
+
*/
|
|
8
|
+
export declare function deriveFacts(db: DatabaseSync, id: string, session: NormalizedSession): void;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { commandLedger, fileLedger, jobLedger } from '../ledger.js';
|
|
2
|
+
import { EXTRACTOR_VERSION } from './schema.js';
|
|
3
|
+
/**
|
|
4
|
+
* Materializes L2. The rules themselves stay in `ledger.ts` — this only moves
|
|
5
|
+
* their output into rows, so a stored fact and a freshly computed one can
|
|
6
|
+
* never disagree.
|
|
7
|
+
*/
|
|
8
|
+
export function deriveFacts(db, id, session) {
|
|
9
|
+
db.exec('BEGIN IMMEDIATE');
|
|
10
|
+
try {
|
|
11
|
+
for (const table of ['file_ops', 'commands', 'jobs']) {
|
|
12
|
+
db.prepare(`DELETE FROM ${table} WHERE session_id = ?`).run(id);
|
|
13
|
+
}
|
|
14
|
+
const file = db.prepare(`INSERT INTO file_ops (session_id, path, host, operation, turn, event_index, ts,
|
|
15
|
+
provenance, extractor, file_group)
|
|
16
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
17
|
+
for (const record of fileLedger(session)) {
|
|
18
|
+
file.run(id, record.path, record.host ?? null, record.operation, record.turn, record.eventIndex, record.timestamp ?? null, record.provenance, record.extractor, record.group);
|
|
19
|
+
}
|
|
20
|
+
const command = db.prepare(`INSERT INTO commands (session_id, event_index, turn, command, host, cwd, exit_code,
|
|
21
|
+
duration_ms, pid, stderr, ts, provenance, extractor)
|
|
22
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
23
|
+
for (const record of commandLedger(session)) {
|
|
24
|
+
command.run(id, record.eventIndex, record.turn, record.command, record.host ?? null, record.cwd ?? null, record.exitCode ?? null, record.durationMs ?? null, record.pid ?? null, record.stderr ?? null, record.timestamp ?? null, record.provenance, record.extractor);
|
|
25
|
+
}
|
|
26
|
+
const job = db.prepare(`INSERT INTO jobs (session_id, job_id, command, pid, host, log, started_at,
|
|
27
|
+
discovered_from, status, provenance, extractor, evidence_json)
|
|
28
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
29
|
+
for (const record of jobLedger(session)) {
|
|
30
|
+
job.run(id, record.id, record.command ?? null, record.pid ?? null, record.host ?? null, record.log ?? null, record.startedAt ?? null, record.discoveredFrom ?? null, record.status, record.provenance, record.extractor, JSON.stringify(record.evidence));
|
|
31
|
+
}
|
|
32
|
+
db.prepare('UPDATE sessions SET extractor_version = ? WHERE id = ?').run(EXTRACTOR_VERSION, id);
|
|
33
|
+
db.exec('COMMIT');
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
db.exec('ROLLBACK');
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import type { ProviderAdapter, SessionCandidate } from '../parsers/provider.js';
|
|
3
|
+
import type { NormalizedSession } from '../types.js';
|
|
4
|
+
export type IndexAction = 'indexed' | 'reused' | 'facts-rederived' | 'edges-rederived';
|
|
5
|
+
export interface IndexResult {
|
|
6
|
+
id: string;
|
|
7
|
+
action: IndexAction;
|
|
8
|
+
session: NormalizedSession;
|
|
9
|
+
}
|
|
10
|
+
export interface IndexHandle {
|
|
11
|
+
adapter: ProviderAdapter;
|
|
12
|
+
candidate: SessionCandidate;
|
|
13
|
+
}
|
|
14
|
+
export interface IndexOptions {
|
|
15
|
+
/** Re-read the source even when the fingerprint says nothing changed. */
|
|
16
|
+
force?: boolean;
|
|
17
|
+
/** Skip L3 — a bulk backfill derives edges once at the end instead. */
|
|
18
|
+
edges?: boolean;
|
|
19
|
+
}
|
|
20
|
+
export interface RefreshResult {
|
|
21
|
+
id: string;
|
|
22
|
+
action: IndexAction;
|
|
23
|
+
/** Present only when the work required materializing it anyway. */
|
|
24
|
+
session?: NormalizedSession;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Brings one session's index up to date and returns it.
|
|
28
|
+
*
|
|
29
|
+
* The three layers are invalidated independently: only a changed fingerprint
|
|
30
|
+
* or a new `PARSER_VERSION` costs a file read. A new `EXTRACTOR_VERSION` or
|
|
31
|
+
* `EDGE_VERSION` re-derives from the stored events alone.
|
|
32
|
+
*/
|
|
33
|
+
export declare function indexSession(db: DatabaseSync, handle: IndexHandle, options?: IndexOptions): Promise<IndexResult>;
|
|
34
|
+
/**
|
|
35
|
+
* The same work without materializing the session.
|
|
36
|
+
*
|
|
37
|
+
* A caller that only needs the index to be current — search, for one — pays a
|
|
38
|
+
* stat and a 64KB read per session instead of rebuilding every event object.
|
|
39
|
+
*/
|
|
40
|
+
export declare function refreshSession(db: DatabaseSync, handle: IndexHandle, options?: IndexOptions): Promise<RefreshResult>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { summarizeTurns } from '../turns.js';
|
|
2
|
+
import { deriveEdges } from './edges.js';
|
|
3
|
+
import { deriveFacts } from './facts.js';
|
|
4
|
+
import { readSession, sessionRow } from './read.js';
|
|
5
|
+
import { EDGE_VERSION, EXTRACTOR_VERSION, PARSER_VERSION } from './schema.js';
|
|
6
|
+
import { canonicalId, fingerprintOf, writeSession } from './write.js';
|
|
7
|
+
/**
|
|
8
|
+
* Brings one session's index up to date and returns it.
|
|
9
|
+
*
|
|
10
|
+
* The three layers are invalidated independently: only a changed fingerprint
|
|
11
|
+
* or a new `PARSER_VERSION` costs a file read. A new `EXTRACTOR_VERSION` or
|
|
12
|
+
* `EDGE_VERSION` re-derives from the stored events alone.
|
|
13
|
+
*/
|
|
14
|
+
export async function indexSession(db, handle, options = {}) {
|
|
15
|
+
const result = await refreshSession(db, handle, options);
|
|
16
|
+
const session = result.session ?? readSession(db, sessionRow(db, result.id));
|
|
17
|
+
return { id: result.id, action: result.action, session };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The same work without materializing the session.
|
|
21
|
+
*
|
|
22
|
+
* A caller that only needs the index to be current — search, for one — pays a
|
|
23
|
+
* stat and a 64KB read per session instead of rebuilding every event object.
|
|
24
|
+
*/
|
|
25
|
+
export async function refreshSession(db, handle, options = {}) {
|
|
26
|
+
const { adapter, candidate } = handle;
|
|
27
|
+
const id = canonicalId(adapter.provider, candidate.id);
|
|
28
|
+
const aux = await adapter.auxFingerprint?.(candidate);
|
|
29
|
+
const fingerprint = fingerprintOf(candidate, aux);
|
|
30
|
+
const row = sessionRow(db, id);
|
|
31
|
+
const l1Valid = !!row &&
|
|
32
|
+
!options.force &&
|
|
33
|
+
row.parser_version === PARSER_VERSION &&
|
|
34
|
+
row.source_size === fingerprint.sourceSize &&
|
|
35
|
+
row.source_mtime_ms === fingerprint.sourceMtimeMs &&
|
|
36
|
+
row.head_hash === fingerprint.headHash &&
|
|
37
|
+
(row.aux_fingerprint ?? undefined) === fingerprint.auxFingerprint;
|
|
38
|
+
if (!l1Valid) {
|
|
39
|
+
const session = await adapter.parse(candidate);
|
|
40
|
+
writeSession(db, session, fingerprint, summarizeTurns(session).length);
|
|
41
|
+
deriveFacts(db, id, session);
|
|
42
|
+
if (options.edges !== false)
|
|
43
|
+
deriveEdges(db, id, session);
|
|
44
|
+
return { id, action: 'indexed', session };
|
|
45
|
+
}
|
|
46
|
+
// Nothing to redo: the caller gets an id and no object was ever built.
|
|
47
|
+
if (row.extractor_version === EXTRACTOR_VERSION &&
|
|
48
|
+
(options.edges === false || row.edge_version === EDGE_VERSION)) {
|
|
49
|
+
return { id, action: 'reused' };
|
|
50
|
+
}
|
|
51
|
+
const session = readSession(db, row);
|
|
52
|
+
let action = 'reused';
|
|
53
|
+
if (row.extractor_version !== EXTRACTOR_VERSION) {
|
|
54
|
+
deriveFacts(db, id, session);
|
|
55
|
+
action = 'facts-rederived';
|
|
56
|
+
}
|
|
57
|
+
if (options.edges !== false && row.edge_version !== EDGE_VERSION) {
|
|
58
|
+
deriveEdges(db, id, session);
|
|
59
|
+
action = action === 'reused' ? 'edges-rederived' : action;
|
|
60
|
+
}
|
|
61
|
+
return { id, action, session };
|
|
62
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { type NormalizedSession, type SessionRef } from '../types.js';
|
|
3
|
+
export interface SessionRow {
|
|
4
|
+
id: string;
|
|
5
|
+
provider: string;
|
|
6
|
+
native_id: string;
|
|
7
|
+
source_path: string;
|
|
8
|
+
workspace: string | null;
|
|
9
|
+
title: string | null;
|
|
10
|
+
started_at: string | null;
|
|
11
|
+
ended_at: string | null;
|
|
12
|
+
event_count: number;
|
|
13
|
+
turn_count: number;
|
|
14
|
+
source_size: number;
|
|
15
|
+
source_mtime_ms: number;
|
|
16
|
+
head_hash: string | null;
|
|
17
|
+
aux_fingerprint: string | null;
|
|
18
|
+
parser_version: number;
|
|
19
|
+
extractor_version: number;
|
|
20
|
+
edge_version: number;
|
|
21
|
+
indexed_at: string | null;
|
|
22
|
+
artifacts_json: string | null;
|
|
23
|
+
stats_json: string | null;
|
|
24
|
+
}
|
|
25
|
+
export declare function sessionRow(db: DatabaseSync, id: string): SessionRow | undefined;
|
|
26
|
+
/** Resolves a full id, an id prefix, or a native id against the index. */
|
|
27
|
+
export declare function findSessionRow(db: DatabaseSync, needle: string): SessionRow | undefined;
|
|
28
|
+
export declare function refOf(row: SessionRow): SessionRef;
|
|
29
|
+
export declare function readSession(db: DatabaseSync, row: SessionRow): NormalizedSession;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { emptyProviderStats, } from '../types.js';
|
|
2
|
+
export function sessionRow(db, id) {
|
|
3
|
+
return db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
|
|
4
|
+
}
|
|
5
|
+
/** Resolves a full id, an id prefix, or a native id against the index. */
|
|
6
|
+
export function findSessionRow(db, needle) {
|
|
7
|
+
const exact = db
|
|
8
|
+
.prepare('SELECT * FROM sessions WHERE id = ? OR native_id = ? LIMIT 1')
|
|
9
|
+
.get(needle, needle);
|
|
10
|
+
if (exact)
|
|
11
|
+
return exact;
|
|
12
|
+
if (needle.length < 6)
|
|
13
|
+
return undefined;
|
|
14
|
+
return db
|
|
15
|
+
.prepare('SELECT * FROM sessions WHERE native_id LIKE ? ORDER BY ended_at DESC LIMIT 1')
|
|
16
|
+
.get(`${needle}%`);
|
|
17
|
+
}
|
|
18
|
+
export function refOf(row) {
|
|
19
|
+
return {
|
|
20
|
+
id: row.native_id,
|
|
21
|
+
provider: row.provider,
|
|
22
|
+
path: row.source_path,
|
|
23
|
+
...(row.title === null ? {} : { title: row.title }),
|
|
24
|
+
...(row.workspace === null ? {} : { workspace: row.workspace }),
|
|
25
|
+
...(row.started_at === null ? {} : { createdAt: row.started_at }),
|
|
26
|
+
...(row.ended_at === null ? {} : { updatedAt: row.ended_at }),
|
|
27
|
+
sizeBytes: row.source_size,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Rebuilds events exactly as the parser emitted them — absent optionals stay
|
|
32
|
+
* absent, so the result deep-equals a fresh `parse()`.
|
|
33
|
+
*/
|
|
34
|
+
function eventsOf(db, id, nativeId) {
|
|
35
|
+
const rows = db
|
|
36
|
+
.prepare('SELECT * FROM events WHERE session_id = ? ORDER BY idx')
|
|
37
|
+
.all(id);
|
|
38
|
+
return rows.map((row) => ({
|
|
39
|
+
id: `${nativeId}#${row.idx}`,
|
|
40
|
+
index: row.idx,
|
|
41
|
+
kind: row.kind,
|
|
42
|
+
...(row.text === null ? {} : { text: row.text }),
|
|
43
|
+
...(row.tool_name === null ? {} : { toolName: row.tool_name }),
|
|
44
|
+
...(row.tool_args_json === null
|
|
45
|
+
? {}
|
|
46
|
+
: { toolArgs: JSON.parse(row.tool_args_json) }),
|
|
47
|
+
...(row.tool_result === null ? {} : { toolResult: row.tool_result }),
|
|
48
|
+
...(row.is_error === null ? {} : { isError: row.is_error === 1 }),
|
|
49
|
+
...(row.ts === null ? {} : { timestamp: row.ts }),
|
|
50
|
+
...(row.source_index === null ? {} : { sourceIndex: row.source_index }),
|
|
51
|
+
...(row.provider_truncated === null ? {} : { truncated: row.provider_truncated === 1 }),
|
|
52
|
+
...(row.pid === null ? {} : { processId: row.pid }),
|
|
53
|
+
...(row.exit_code === null ? {} : { exitCode: row.exit_code }),
|
|
54
|
+
...(row.duration_ms === null ? {} : { durationMs: row.duration_ms }),
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
export function readSession(db, row) {
|
|
58
|
+
const artifacts = row.artifacts_json
|
|
59
|
+
? JSON.parse(row.artifacts_json)
|
|
60
|
+
: [];
|
|
61
|
+
const stats = row.stats_json
|
|
62
|
+
? JSON.parse(row.stats_json)
|
|
63
|
+
: emptyProviderStats();
|
|
64
|
+
return {
|
|
65
|
+
ref: refOf(row),
|
|
66
|
+
turns: eventsOf(db, row.id, row.native_id),
|
|
67
|
+
artifacts,
|
|
68
|
+
stats,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
/** One indexed event, reduced to the columns a text search needs. */
|
|
3
|
+
export interface TextRow {
|
|
4
|
+
session_id: string;
|
|
5
|
+
idx: number;
|
|
6
|
+
kind: string;
|
|
7
|
+
tool_name: string | null;
|
|
8
|
+
ts: string | null;
|
|
9
|
+
text: string | null;
|
|
10
|
+
tool_result: string | null;
|
|
11
|
+
tool_args_json: string | null;
|
|
12
|
+
}
|
|
13
|
+
export interface RowQuery {
|
|
14
|
+
/** Sessions in scope, already refreshed against disk. */
|
|
15
|
+
ids: string[];
|
|
16
|
+
/** Canonical workspace root; matches the directory itself and everything under it. */
|
|
17
|
+
workspace?: string;
|
|
18
|
+
kinds?: string[];
|
|
19
|
+
/**
|
|
20
|
+
* A substring every match must contain. Pushing it into SQL is only sound
|
|
21
|
+
* when it is genuinely mandatory — see `mandatoryLiteral` in `search.ts`.
|
|
22
|
+
*/
|
|
23
|
+
literal?: string;
|
|
24
|
+
/** Compare the literal case-insensitively (ASCII folding, as SQLite does). */
|
|
25
|
+
fold?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Streams candidate rows. The filters are structural — session scope,
|
|
29
|
+
* workspace, kind — plus an optional literal prefilter; deciding whether a row
|
|
30
|
+
* really matches stays with the regex matcher, which remains the only
|
|
31
|
+
* authority on search semantics.
|
|
32
|
+
*/
|
|
33
|
+
export declare function searchRows(db: DatabaseSync, query: RowQuery): Generator<TextRow>;
|
|
34
|
+
/** Session rows for the ids a search ended up with. */
|
|
35
|
+
export declare function scopedSessions(db: DatabaseSync, ids: string[]): Record<string, import("node:sqlite").SQLOutputValue>[];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const TEXT_COLUMNS = ['text', 'tool_result', 'tool_args_json'];
|
|
2
|
+
/**
|
|
3
|
+
* Streams candidate rows. The filters are structural — session scope,
|
|
4
|
+
* workspace, kind — plus an optional literal prefilter; deciding whether a row
|
|
5
|
+
* really matches stays with the regex matcher, which remains the only
|
|
6
|
+
* authority on search semantics.
|
|
7
|
+
*/
|
|
8
|
+
export function* searchRows(db, query) {
|
|
9
|
+
if (!query.ids.length)
|
|
10
|
+
return;
|
|
11
|
+
db.exec('DROP TABLE IF EXISTS temp.search_scope');
|
|
12
|
+
db.exec('CREATE TEMP TABLE search_scope (id TEXT PRIMARY KEY)');
|
|
13
|
+
const insert = db.prepare('INSERT OR IGNORE INTO temp.search_scope (id) VALUES (?)');
|
|
14
|
+
for (const id of query.ids)
|
|
15
|
+
insert.run(id);
|
|
16
|
+
const where = [];
|
|
17
|
+
const params = [];
|
|
18
|
+
if (query.workspace) {
|
|
19
|
+
// Plain prefix comparison, not LIKE: real paths contain `_`, which LIKE
|
|
20
|
+
// would treat as a wildcard.
|
|
21
|
+
where.push('(s.workspace = ? OR substr(s.workspace, 1, ?) = ?)');
|
|
22
|
+
params.push(query.workspace, query.workspace.length + 1, `${query.workspace}/`);
|
|
23
|
+
}
|
|
24
|
+
if (query.kinds?.length) {
|
|
25
|
+
where.push(`e.kind IN (${query.kinds.map(() => '?').join(', ')})`);
|
|
26
|
+
params.push(...query.kinds);
|
|
27
|
+
}
|
|
28
|
+
if (query.literal) {
|
|
29
|
+
const column = (name) => query.fold ? `lower(coalesce(e.${name}, ''))` : `coalesce(e.${name}, '')`;
|
|
30
|
+
// Per column, never across the joined haystack: a literal that straddles
|
|
31
|
+
// two fields would be missed, so callers reject literals containing a
|
|
32
|
+
// newline before they get here.
|
|
33
|
+
where.push(`(${TEXT_COLUMNS.map((name) => `instr(${column(name)}, ?) > 0`).join(' OR ')})`);
|
|
34
|
+
params.push(...TEXT_COLUMNS.map(() => query.literal));
|
|
35
|
+
}
|
|
36
|
+
const sql = `SELECT e.session_id, e.idx, e.kind, e.tool_name, e.ts, e.text, e.tool_result, e.tool_args_json
|
|
37
|
+
FROM events e
|
|
38
|
+
JOIN temp.search_scope sc ON sc.id = e.session_id
|
|
39
|
+
JOIN sessions s ON s.id = e.session_id` +
|
|
40
|
+
(where.length ? `\n WHERE ${where.join(' AND ')}` : '') +
|
|
41
|
+
'\n ORDER BY e.session_id, e.idx';
|
|
42
|
+
try {
|
|
43
|
+
yield* db.prepare(sql).iterate(...params);
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
db.exec('DROP TABLE IF EXISTS temp.search_scope');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Session rows for the ids a search ended up with. */
|
|
50
|
+
export function scopedSessions(db, ids) {
|
|
51
|
+
if (!ids.length)
|
|
52
|
+
return [];
|
|
53
|
+
const placeholders = ids.map(() => '?').join(', ');
|
|
54
|
+
return db.prepare(`SELECT * FROM sessions WHERE id IN (${placeholders})`).all(...ids);
|
|
55
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Index store schema and the version constants that decide what is stale.
|
|
3
|
+
*
|
|
4
|
+
* The four versions are independent on purpose: a change to fact extraction
|
|
5
|
+
* must not force a re-read of 600MB of JSONL, it only invalidates the layer it
|
|
6
|
+
* actually touched. Bump the matching constant in the same commit that changes
|
|
7
|
+
* the behaviour — never derive them from a git hash, `dist` ships without git.
|
|
8
|
+
*/
|
|
9
|
+
/** DDL layout. A bump drops and rebuilds the whole database. */
|
|
10
|
+
export declare const SCHEMA_VERSION = 1;
|
|
11
|
+
/** L1 semantics — anything in `parsers/` that changes normalized events. */
|
|
12
|
+
export declare const PARSER_VERSION = 1;
|
|
13
|
+
/** L2 rules — `writes.ts` / `ledger.ts`. Re-derives facts from stored events. */
|
|
14
|
+
export declare const EXTRACTOR_VERSION = 1;
|
|
15
|
+
/** L3 rules — `store/edges.ts`. Re-derives edges from stored events. */
|
|
16
|
+
export declare const EDGE_VERSION = 1;
|
|
17
|
+
export declare const DDL = "\nCREATE TABLE IF NOT EXISTS meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS sessions (\n id TEXT PRIMARY KEY, -- canonical \"provider:native_id\"\n provider TEXT NOT NULL,\n native_id TEXT NOT NULL,\n source_path TEXT NOT NULL,\n workspace TEXT,\n title TEXT,\n started_at TEXT,\n ended_at TEXT,\n event_count INTEGER NOT NULL DEFAULT 0,\n turn_count INTEGER NOT NULL DEFAULT 0,\n source_size INTEGER NOT NULL DEFAULT 0,\n source_mtime_ms INTEGER NOT NULL DEFAULT 0,\n head_hash TEXT,\n aux_fingerprint TEXT,\n indexed_bytes INTEGER NOT NULL DEFAULT 0, -- reserved for resumable parse\n parser_version INTEGER NOT NULL DEFAULT 0,\n extractor_version INTEGER NOT NULL DEFAULT 0,\n edge_version INTEGER NOT NULL DEFAULT 0,\n indexed_at TEXT,\n artifacts_json TEXT,\n stats_json TEXT\n);\n\nCREATE TABLE IF NOT EXISTS events (\n session_id TEXT NOT NULL,\n idx INTEGER NOT NULL,\n kind TEXT NOT NULL,\n text TEXT,\n tool_name TEXT,\n tool_args_json TEXT,\n tool_result TEXT,\n is_error INTEGER,\n ts TEXT,\n source_index INTEGER,\n exit_code INTEGER,\n pid TEXT,\n duration_ms INTEGER,\n provider_truncated INTEGER, -- the provider's own \"shortened copy\" flag\n PRIMARY KEY (session_id, idx)\n) WITHOUT ROWID;\n\nCREATE TABLE IF NOT EXISTS file_ops (\n session_id TEXT NOT NULL,\n path TEXT NOT NULL,\n host TEXT,\n operation TEXT NOT NULL,\n turn INTEGER NOT NULL,\n event_index INTEGER NOT NULL,\n ts TEXT,\n provenance TEXT NOT NULL,\n extractor TEXT NOT NULL,\n file_group TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS commands (\n session_id TEXT NOT NULL,\n event_index INTEGER NOT NULL,\n turn INTEGER NOT NULL,\n command TEXT NOT NULL,\n host TEXT,\n cwd TEXT,\n exit_code INTEGER,\n duration_ms INTEGER,\n pid TEXT,\n stderr TEXT,\n ts TEXT,\n provenance TEXT NOT NULL,\n extractor TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS jobs (\n session_id TEXT NOT NULL,\n job_id TEXT NOT NULL,\n command TEXT,\n pid TEXT,\n host TEXT,\n log TEXT,\n started_at TEXT,\n discovered_from INTEGER,\n status TEXT NOT NULL,\n provenance TEXT NOT NULL,\n extractor TEXT NOT NULL,\n evidence_json TEXT\n);\n\nCREATE TABLE IF NOT EXISTS session_edges (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n from_session TEXT NOT NULL,\n to_session TEXT NOT NULL,\n relation TEXT NOT NULL,\n first_seen_at TEXT,\n last_seen_at TEXT,\n evidence_count INTEGER NOT NULL DEFAULT 0,\n UNIQUE (from_session, to_session, relation)\n);\n\nCREATE TABLE IF NOT EXISTS edge_evidence (\n edge_id INTEGER NOT NULL,\n source_session TEXT NOT NULL,\n event_index INTEGER NOT NULL,\n operation TEXT NOT NULL,\n command TEXT,\n ts TEXT,\n provenance TEXT NOT NULL,\n extractor TEXT NOT NULL,\n UNIQUE (edge_id, source_session, event_index)\n);\n\nCREATE INDEX IF NOT EXISTS idx_events_kind ON events (session_id, kind);\nCREATE INDEX IF NOT EXISTS idx_fileops_path ON file_ops (path);\nCREATE INDEX IF NOT EXISTS idx_fileops_session ON file_ops (session_id);\nCREATE INDEX IF NOT EXISTS idx_commands_session ON commands (session_id, exit_code);\nCREATE INDEX IF NOT EXISTS idx_jobs_session ON jobs (session_id);\nCREATE INDEX IF NOT EXISTS idx_sessions_ws ON sessions (workspace, ended_at);\nCREATE INDEX IF NOT EXISTS idx_sessions_native ON sessions (native_id);\nCREATE INDEX IF NOT EXISTS idx_edges_to ON session_edges (to_session);\n";
|
|
18
|
+
/** Tables holding derived rows — dropped wholesale when a layer is rebuilt. */
|
|
19
|
+
export declare const L2_TABLES: readonly ["file_ops", "commands", "jobs"];
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Index store schema and the version constants that decide what is stale.
|
|
3
|
+
*
|
|
4
|
+
* The four versions are independent on purpose: a change to fact extraction
|
|
5
|
+
* must not force a re-read of 600MB of JSONL, it only invalidates the layer it
|
|
6
|
+
* actually touched. Bump the matching constant in the same commit that changes
|
|
7
|
+
* the behaviour — never derive them from a git hash, `dist` ships without git.
|
|
8
|
+
*/
|
|
9
|
+
/** DDL layout. A bump drops and rebuilds the whole database. */
|
|
10
|
+
export const SCHEMA_VERSION = 1;
|
|
11
|
+
/** L1 semantics — anything in `parsers/` that changes normalized events. */
|
|
12
|
+
export const PARSER_VERSION = 1;
|
|
13
|
+
/** L2 rules — `writes.ts` / `ledger.ts`. Re-derives facts from stored events. */
|
|
14
|
+
export const EXTRACTOR_VERSION = 1;
|
|
15
|
+
/** L3 rules — `store/edges.ts`. Re-derives edges from stored events. */
|
|
16
|
+
export const EDGE_VERSION = 1;
|
|
17
|
+
export const DDL = `
|
|
18
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
19
|
+
key TEXT PRIMARY KEY,
|
|
20
|
+
value TEXT NOT NULL
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
24
|
+
id TEXT PRIMARY KEY, -- canonical "provider:native_id"
|
|
25
|
+
provider TEXT NOT NULL,
|
|
26
|
+
native_id TEXT NOT NULL,
|
|
27
|
+
source_path TEXT NOT NULL,
|
|
28
|
+
workspace TEXT,
|
|
29
|
+
title TEXT,
|
|
30
|
+
started_at TEXT,
|
|
31
|
+
ended_at TEXT,
|
|
32
|
+
event_count INTEGER NOT NULL DEFAULT 0,
|
|
33
|
+
turn_count INTEGER NOT NULL DEFAULT 0,
|
|
34
|
+
source_size INTEGER NOT NULL DEFAULT 0,
|
|
35
|
+
source_mtime_ms INTEGER NOT NULL DEFAULT 0,
|
|
36
|
+
head_hash TEXT,
|
|
37
|
+
aux_fingerprint TEXT,
|
|
38
|
+
indexed_bytes INTEGER NOT NULL DEFAULT 0, -- reserved for resumable parse
|
|
39
|
+
parser_version INTEGER NOT NULL DEFAULT 0,
|
|
40
|
+
extractor_version INTEGER NOT NULL DEFAULT 0,
|
|
41
|
+
edge_version INTEGER NOT NULL DEFAULT 0,
|
|
42
|
+
indexed_at TEXT,
|
|
43
|
+
artifacts_json TEXT,
|
|
44
|
+
stats_json TEXT
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
48
|
+
session_id TEXT NOT NULL,
|
|
49
|
+
idx INTEGER NOT NULL,
|
|
50
|
+
kind TEXT NOT NULL,
|
|
51
|
+
text TEXT,
|
|
52
|
+
tool_name TEXT,
|
|
53
|
+
tool_args_json TEXT,
|
|
54
|
+
tool_result TEXT,
|
|
55
|
+
is_error INTEGER,
|
|
56
|
+
ts TEXT,
|
|
57
|
+
source_index INTEGER,
|
|
58
|
+
exit_code INTEGER,
|
|
59
|
+
pid TEXT,
|
|
60
|
+
duration_ms INTEGER,
|
|
61
|
+
provider_truncated INTEGER, -- the provider's own "shortened copy" flag
|
|
62
|
+
PRIMARY KEY (session_id, idx)
|
|
63
|
+
) WITHOUT ROWID;
|
|
64
|
+
|
|
65
|
+
CREATE TABLE IF NOT EXISTS file_ops (
|
|
66
|
+
session_id TEXT NOT NULL,
|
|
67
|
+
path TEXT NOT NULL,
|
|
68
|
+
host TEXT,
|
|
69
|
+
operation TEXT NOT NULL,
|
|
70
|
+
turn INTEGER NOT NULL,
|
|
71
|
+
event_index INTEGER NOT NULL,
|
|
72
|
+
ts TEXT,
|
|
73
|
+
provenance TEXT NOT NULL,
|
|
74
|
+
extractor TEXT NOT NULL,
|
|
75
|
+
file_group TEXT NOT NULL
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
CREATE TABLE IF NOT EXISTS commands (
|
|
79
|
+
session_id TEXT NOT NULL,
|
|
80
|
+
event_index INTEGER NOT NULL,
|
|
81
|
+
turn INTEGER NOT NULL,
|
|
82
|
+
command TEXT NOT NULL,
|
|
83
|
+
host TEXT,
|
|
84
|
+
cwd TEXT,
|
|
85
|
+
exit_code INTEGER,
|
|
86
|
+
duration_ms INTEGER,
|
|
87
|
+
pid TEXT,
|
|
88
|
+
stderr TEXT,
|
|
89
|
+
ts TEXT,
|
|
90
|
+
provenance TEXT NOT NULL,
|
|
91
|
+
extractor TEXT NOT NULL
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
CREATE TABLE IF NOT EXISTS jobs (
|
|
95
|
+
session_id TEXT NOT NULL,
|
|
96
|
+
job_id TEXT NOT NULL,
|
|
97
|
+
command TEXT,
|
|
98
|
+
pid TEXT,
|
|
99
|
+
host TEXT,
|
|
100
|
+
log TEXT,
|
|
101
|
+
started_at TEXT,
|
|
102
|
+
discovered_from INTEGER,
|
|
103
|
+
status TEXT NOT NULL,
|
|
104
|
+
provenance TEXT NOT NULL,
|
|
105
|
+
extractor TEXT NOT NULL,
|
|
106
|
+
evidence_json TEXT
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
CREATE TABLE IF NOT EXISTS session_edges (
|
|
110
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
111
|
+
from_session TEXT NOT NULL,
|
|
112
|
+
to_session TEXT NOT NULL,
|
|
113
|
+
relation TEXT NOT NULL,
|
|
114
|
+
first_seen_at TEXT,
|
|
115
|
+
last_seen_at TEXT,
|
|
116
|
+
evidence_count INTEGER NOT NULL DEFAULT 0,
|
|
117
|
+
UNIQUE (from_session, to_session, relation)
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
CREATE TABLE IF NOT EXISTS edge_evidence (
|
|
121
|
+
edge_id INTEGER NOT NULL,
|
|
122
|
+
source_session TEXT NOT NULL,
|
|
123
|
+
event_index INTEGER NOT NULL,
|
|
124
|
+
operation TEXT NOT NULL,
|
|
125
|
+
command TEXT,
|
|
126
|
+
ts TEXT,
|
|
127
|
+
provenance TEXT NOT NULL,
|
|
128
|
+
extractor TEXT NOT NULL,
|
|
129
|
+
UNIQUE (edge_id, source_session, event_index)
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
CREATE INDEX IF NOT EXISTS idx_events_kind ON events (session_id, kind);
|
|
133
|
+
CREATE INDEX IF NOT EXISTS idx_fileops_path ON file_ops (path);
|
|
134
|
+
CREATE INDEX IF NOT EXISTS idx_fileops_session ON file_ops (session_id);
|
|
135
|
+
CREATE INDEX IF NOT EXISTS idx_commands_session ON commands (session_id, exit_code);
|
|
136
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_session ON jobs (session_id);
|
|
137
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_ws ON sessions (workspace, ended_at);
|
|
138
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_native ON sessions (native_id);
|
|
139
|
+
CREATE INDEX IF NOT EXISTS idx_edges_to ON session_edges (to_session);
|
|
140
|
+
`;
|
|
141
|
+
/** Tables holding derived rows — dropped wholesale when a layer is rebuilt. */
|
|
142
|
+
export const L2_TABLES = ['file_ops', 'commands', 'jobs'];
|