@titan-design/session-graph 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 +47 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +460 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Henry Jewkes
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# @titan-design/session-graph
|
|
2
|
+
|
|
3
|
+
The activity graph behind a corpus of Claude Code transcripts: sessions, turns, token
|
|
4
|
+
buckets, permission phases, touched files, branches, PRs, subagents, and the edges between
|
|
5
|
+
them, all in one SQLite file built from `@titan-design/store-sqlite` kit tables and kept
|
|
6
|
+
current incrementally.
|
|
7
|
+
|
|
8
|
+
Tier 2 of the titan-platform DAG. Depends on `session-read`, `store-sqlite`, and
|
|
9
|
+
`cluster`. Extracted from active-work's session index (AW-23, TP-6).
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { discoverTranscripts } from "@titan-design/session-read";
|
|
13
|
+
import { openSessionGraph, refreshCorpus } from "@titan-design/session-graph";
|
|
14
|
+
|
|
15
|
+
const graph = openSessionGraph("~/.local/state/miner/index.sqlite3");
|
|
16
|
+
const summary = await refreshCorpus(graph, await discoverTranscripts());
|
|
17
|
+
// summary.indexed, summary.unchanged, summary.rewound, summary.quarantined, summary.missing
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## What a refresh does
|
|
21
|
+
|
|
22
|
+
1. For every transcript, `indexTranscript` asks the watermark table where it stopped,
|
|
23
|
+
checks the file (`unchanged`, `appended`, `rewritten`, `missing`), reads the delta with
|
|
24
|
+
`extractTranscript`, applies it in one transaction, and advances the watermark with the
|
|
25
|
+
new prefix hash. A malformed line quarantines that transcript only.
|
|
26
|
+
2. `rollupSessions` recomputes turn aggregates (index, end, duration, tool calls, thinking
|
|
27
|
+
time) for the sessions that changed. Recompute, never accumulate, so incremental and
|
|
28
|
+
full passes converge.
|
|
29
|
+
3. `reconcile` folds cross-transcript observations: `gh pr merge` sightings onto PRs,
|
|
30
|
+
complete `gh pr create` sightings into new PR rows, subagent end times and parentage
|
|
31
|
+
from child sessions.
|
|
32
|
+
4. Rows whose source file has vanished are marked `missing`. Their facts stay: surviving
|
|
33
|
+
Claude Code's own pruning is much of the point.
|
|
34
|
+
|
|
35
|
+
`resetIndex` clears every derived table and rewinds watermarks; the next refresh rebuilds
|
|
36
|
+
from byte 0 to the same rows a chunked history produced.
|
|
37
|
+
|
|
38
|
+
## Tables
|
|
39
|
+
|
|
40
|
+
Kit tables: `transcript` (watermark), `edge` (bi-temporal, `session:… touched file:…`),
|
|
41
|
+
`search_span` + `search_fts` (contentless full-text over prompts, responses, tool inputs
|
|
42
|
+
and results, keyed by the session ref). Domain tables: `fact`, `session`,
|
|
43
|
+
`session_model_usage`, `turn`, `permission_phase`, `human_edit`, `file_checkpoint`, `pr`,
|
|
44
|
+
`branch`, `file`, `task`, `subagent`, `artifact`, and the two PR observation tables.
|
|
45
|
+
|
|
46
|
+
Everything except `transcript` is derivable, which is what makes the schema safe to evolve
|
|
47
|
+
by drop-and-rederive.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { Db, WatermarkTable, EdgeTable, SpanFtsTables, Migration } from '@titan-design/store-sqlite';
|
|
2
|
+
import { TranscriptDelta, DiscoveredTranscript } from '@titan-design/session-read';
|
|
3
|
+
|
|
4
|
+
/** One open session graph: the connection plus the kit helpers bound to its tables. */
|
|
5
|
+
interface SessionGraph {
|
|
6
|
+
db: Db;
|
|
7
|
+
transcripts: WatermarkTable;
|
|
8
|
+
edges: EdgeTable;
|
|
9
|
+
spans: SpanFtsTables;
|
|
10
|
+
}
|
|
11
|
+
declare function openSessionGraph(dbPath: string): SessionGraph;
|
|
12
|
+
/**
|
|
13
|
+
* Clear every derived row and rewind all watermarks so the next refresh
|
|
14
|
+
* rebuilds from byte 0. Rewinding alone is not enough: the accumulating
|
|
15
|
+
* upserts would add a second copy of every count.
|
|
16
|
+
*/
|
|
17
|
+
declare function resetIndex(graph: SessionGraph): void;
|
|
18
|
+
declare function allSessionIds(graph: SessionGraph): string[];
|
|
19
|
+
|
|
20
|
+
/** Kit table names this graph uses. `watermark` rows are transcripts; `search_*` are the FTS spans. */
|
|
21
|
+
declare const KIT: {
|
|
22
|
+
readonly watermark: "transcript";
|
|
23
|
+
readonly edge: "edge";
|
|
24
|
+
readonly spanFts: "search";
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Domain tables. Everything here is derivable from the transcripts plus the
|
|
28
|
+
* watermark table, which is what makes drop-and-rederive a safe rebuild.
|
|
29
|
+
* Locators are `(transcript_id, byte_offset, byte_length)`; `fact_id` rows
|
|
30
|
+
* resolve through the facts table's unique `(transcript_id, byte_offset)`.
|
|
31
|
+
*/
|
|
32
|
+
declare const DOMAIN_DDL = "\n CREATE TABLE IF NOT EXISTS fact (\n fact_id INTEGER PRIMARY KEY,\n transcript_id INTEGER NOT NULL,\n byte_offset INTEGER NOT NULL,\n byte_length INTEGER NOT NULL,\n event_type TEXT NOT NULL,\n ts TEXT NOT NULL,\n seq INTEGER NOT NULL,\n session_id TEXT NOT NULL,\n prompt_id TEXT,\n tool_use_id TEXT,\n t_indexed TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),\n UNIQUE (transcript_id, byte_offset)\n );\n CREATE INDEX IF NOT EXISTS idx_fact_session_ts ON fact(session_id, ts);\n CREATE INDEX IF NOT EXISTS idx_fact_prompt ON fact(prompt_id);\n CREATE INDEX IF NOT EXISTS idx_fact_tool_use ON fact(tool_use_id);\n\n CREATE TABLE IF NOT EXISTS session (\n session_id TEXT PRIMARY KEY,\n transcript_id INTEGER,\n started_at TEXT,\n ended_at TEXT,\n start_type TEXT,\n cwd TEXT,\n git_branch TEXT,\n ai_title TEXT,\n seed_prompt TEXT,\n cli_version TEXT,\n turn_count INTEGER NOT NULL DEFAULT 0,\n commit_count INTEGER NOT NULL DEFAULT 0,\n push_count INTEGER NOT NULL DEFAULT 0\n );\n CREATE INDEX IF NOT EXISTS idx_session_started ON session(started_at);\n\n CREATE TABLE IF NOT EXISTS session_model_usage (\n session_id TEXT NOT NULL,\n model TEXT NOT NULL,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n cache_read_tokens INTEGER NOT NULL DEFAULT 0,\n cache_creation_tokens INTEGER NOT NULL DEFAULT 0,\n thinking_tokens INTEGER NOT NULL DEFAULT 0,\n request_count INTEGER NOT NULL DEFAULT 0,\n PRIMARY KEY (session_id, model)\n );\n\n CREATE TABLE IF NOT EXISTS turn (\n prompt_id TEXT PRIMARY KEY,\n session_id TEXT NOT NULL,\n turn_index INTEGER NOT NULL,\n started_at TEXT NOT NULL,\n ended_at TEXT,\n duration_ms INTEGER,\n tool_call_count INTEGER NOT NULL DEFAULT 0,\n thinking_ms INTEGER NOT NULL DEFAULT 0,\n fact_id_start INTEGER\n );\n CREATE INDEX IF NOT EXISTS idx_turn_session ON turn(session_id, turn_index);\n\n CREATE TABLE IF NOT EXISTS permission_phase (\n phase_id INTEGER PRIMARY KEY,\n session_id TEXT NOT NULL,\n from_mode TEXT,\n to_mode TEXT NOT NULL,\n trigger TEXT NOT NULL,\n t_valid TEXT NOT NULL,\n t_invalid TEXT,\n fact_id INTEGER\n );\n CREATE INDEX IF NOT EXISTS idx_phase_session ON permission_phase(session_id, t_valid);\n\n CREATE TABLE IF NOT EXISTS human_edit (\n edit_id INTEGER PRIMARY KEY,\n session_id TEXT NOT NULL,\n file_path TEXT NOT NULL,\n ts TEXT NOT NULL,\n fact_id INTEGER,\n UNIQUE (session_id, file_path, ts)\n );\n\n CREATE TABLE IF NOT EXISTS file_checkpoint (\n checkpoint_id INTEGER PRIMARY KEY,\n session_id TEXT NOT NULL,\n file_path TEXT NOT NULL,\n backup_file_name TEXT NOT NULL,\n version INTEGER NOT NULL,\n backup_time TEXT NOT NULL,\n fact_id INTEGER,\n UNIQUE (session_id, file_path, backup_file_name)\n );\n\n CREATE TABLE IF NOT EXISTS pr (\n pr_ref TEXT PRIMARY KEY, number INTEGER, repo TEXT, title TEXT, state TEXT, url TEXT, merged_at TEXT\n );\n CREATE TABLE IF NOT EXISTS branch (\n branch_ref TEXT PRIMARY KEY, repo TEXT, name TEXT NOT NULL, base TEXT, created_at TEXT, deleted_at TEXT\n );\n CREATE TABLE IF NOT EXISTS file (\n file_ref TEXT PRIMARY KEY, repo TEXT, path TEXT NOT NULL\n );\n CREATE TABLE IF NOT EXISTS task (\n task_ref TEXT PRIMARY KEY, task_id TEXT NOT NULL, initiative TEXT, title TEXT, status TEXT\n );\n CREATE TABLE IF NOT EXISTS subagent (\n agent_ref TEXT PRIMARY KEY,\n session_id TEXT,\n child_session_id TEXT,\n parent_agent_ref TEXT,\n agent_type TEXT,\n label TEXT,\n started_at TEXT,\n ended_at TEXT,\n fact_id INTEGER\n );\n CREATE INDEX IF NOT EXISTS idx_subagent_child ON subagent(child_session_id);\n CREATE TABLE IF NOT EXISTS artifact (\n artifact_ref TEXT PRIMARY KEY, kind TEXT, title TEXT, url TEXT, path TEXT, created_at TEXT\n );\n CREATE TABLE IF NOT EXISTS pr_merge_observation (\n number INTEGER NOT NULL, repo_hint TEXT, merged_at TEXT NOT NULL,\n PRIMARY KEY (number, repo_hint, merged_at)\n );\n CREATE TABLE IF NOT EXISTS pr_create_observation (\n tool_use_id TEXT PRIMARY KEY, title TEXT, number INTEGER, repo TEXT, url TEXT\n );\n";
|
|
33
|
+
/** Every derived table, in an order safe to clear. The watermark table is not derived. */
|
|
34
|
+
declare const DERIVED_TABLES: readonly ["search_span", "edge", "turn", "permission_phase", "human_edit", "file_checkpoint", "subagent", "session_model_usage", "session", "fact", "pr", "pr_merge_observation", "pr_create_observation", "branch", "file", "task", "artifact"];
|
|
35
|
+
declare const MIGRATIONS: Migration[];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Apply one transcript chunk's delta in a single transaction, so a crash can
|
|
39
|
+
* never leave rows committed behind a stale watermark or ahead of their rows.
|
|
40
|
+
* Append-only tables are idempotent through unique indexes; session and usage
|
|
41
|
+
* rows accumulate; ref-keyed assets insert-if-absent with COALESCE merges that
|
|
42
|
+
* mirror `EventFolder`'s rules.
|
|
43
|
+
*/
|
|
44
|
+
declare function applyDelta(graph: SessionGraph, transcriptId: number, delta: TranscriptDelta): void;
|
|
45
|
+
|
|
46
|
+
/** Recompute turn aggregates for the given sessions in one transaction. */
|
|
47
|
+
declare function rollupSessions(graph: SessionGraph, sessionIds: readonly string[]): number;
|
|
48
|
+
interface ReconcileCounts {
|
|
49
|
+
prMerges: number;
|
|
50
|
+
prCreates: number;
|
|
51
|
+
subagents: number;
|
|
52
|
+
}
|
|
53
|
+
/** Whole-table reconciliations, run once at the end of a refresh pass after the rollup. */
|
|
54
|
+
declare function reconcile(graph: SessionGraph): ReconcileCounts;
|
|
55
|
+
|
|
56
|
+
interface IndexOptions {
|
|
57
|
+
/** Re-hash the already-read prefix to catch same-length rewrites. O(file), so opt in. */
|
|
58
|
+
verifyHash?: boolean;
|
|
59
|
+
/** Store a whole-file content hash for durability reporting. O(file), so opt in. */
|
|
60
|
+
withContentHash?: boolean;
|
|
61
|
+
}
|
|
62
|
+
type TranscriptOutcome = {
|
|
63
|
+
status: "unchanged" | "indexed" | "rewound";
|
|
64
|
+
transcriptId: number;
|
|
65
|
+
sessionIds: string[];
|
|
66
|
+
facts: number;
|
|
67
|
+
} | {
|
|
68
|
+
status: "missing" | "quarantined";
|
|
69
|
+
transcriptId: number;
|
|
70
|
+
sessionIds: string[];
|
|
71
|
+
facts: 0;
|
|
72
|
+
reason: string;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Bring one transcript up to date: read from its watermark (or byte 0 after a
|
|
76
|
+
* rewrite), apply the delta, advance the watermark. A malformed line
|
|
77
|
+
* quarantines the transcript instead of poisoning the pass.
|
|
78
|
+
*/
|
|
79
|
+
declare function indexTranscript(graph: SessionGraph, transcript: DiscoveredTranscript, options?: IndexOptions): Promise<TranscriptOutcome>;
|
|
80
|
+
interface RefreshSummary {
|
|
81
|
+
transcripts: number;
|
|
82
|
+
indexed: number;
|
|
83
|
+
unchanged: number;
|
|
84
|
+
rewound: number;
|
|
85
|
+
missing: number;
|
|
86
|
+
quarantined: number;
|
|
87
|
+
facts: number;
|
|
88
|
+
turnsRolledUp: number;
|
|
89
|
+
reconciled: ReconcileCounts;
|
|
90
|
+
markedMissing: number;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* One pass over a corpus: index every transcript, roll up the sessions that
|
|
94
|
+
* changed, reconcile cross-transcript observations, and mark rows whose source
|
|
95
|
+
* file is gone. Idempotent: a second pass over unchanged files changes nothing.
|
|
96
|
+
*/
|
|
97
|
+
declare function refreshCorpus(graph: SessionGraph, transcripts: readonly DiscoveredTranscript[], options?: IndexOptions & {
|
|
98
|
+
full?: boolean;
|
|
99
|
+
}): Promise<RefreshSummary>;
|
|
100
|
+
|
|
101
|
+
export { DERIVED_TABLES, DOMAIN_DDL, type IndexOptions, KIT, MIGRATIONS, type ReconcileCounts, type RefreshSummary, type SessionGraph, type TranscriptOutcome, allSessionIds, applyDelta, indexTranscript, openSessionGraph, reconcile, refreshCorpus, resetIndex, rollupSessions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
// src/graph.ts
|
|
2
|
+
import { EdgeTable, SpanFtsTables, WatermarkTable, openDatabase, runMigrations } from "@titan-design/store-sqlite";
|
|
3
|
+
|
|
4
|
+
// src/schema.ts
|
|
5
|
+
import { SQL_NOW, kitMigration } from "@titan-design/store-sqlite";
|
|
6
|
+
var KIT = { watermark: "transcript", edge: "edge", spanFts: "search" };
|
|
7
|
+
var DOMAIN_DDL = `
|
|
8
|
+
CREATE TABLE IF NOT EXISTS fact (
|
|
9
|
+
fact_id INTEGER PRIMARY KEY,
|
|
10
|
+
transcript_id INTEGER NOT NULL,
|
|
11
|
+
byte_offset INTEGER NOT NULL,
|
|
12
|
+
byte_length INTEGER NOT NULL,
|
|
13
|
+
event_type TEXT NOT NULL,
|
|
14
|
+
ts TEXT NOT NULL,
|
|
15
|
+
seq INTEGER NOT NULL,
|
|
16
|
+
session_id TEXT NOT NULL,
|
|
17
|
+
prompt_id TEXT,
|
|
18
|
+
tool_use_id TEXT,
|
|
19
|
+
t_indexed TEXT NOT NULL DEFAULT (${SQL_NOW}),
|
|
20
|
+
UNIQUE (transcript_id, byte_offset)
|
|
21
|
+
);
|
|
22
|
+
CREATE INDEX IF NOT EXISTS idx_fact_session_ts ON fact(session_id, ts);
|
|
23
|
+
CREATE INDEX IF NOT EXISTS idx_fact_prompt ON fact(prompt_id);
|
|
24
|
+
CREATE INDEX IF NOT EXISTS idx_fact_tool_use ON fact(tool_use_id);
|
|
25
|
+
|
|
26
|
+
CREATE TABLE IF NOT EXISTS session (
|
|
27
|
+
session_id TEXT PRIMARY KEY,
|
|
28
|
+
transcript_id INTEGER,
|
|
29
|
+
started_at TEXT,
|
|
30
|
+
ended_at TEXT,
|
|
31
|
+
start_type TEXT,
|
|
32
|
+
cwd TEXT,
|
|
33
|
+
git_branch TEXT,
|
|
34
|
+
ai_title TEXT,
|
|
35
|
+
seed_prompt TEXT,
|
|
36
|
+
cli_version TEXT,
|
|
37
|
+
turn_count INTEGER NOT NULL DEFAULT 0,
|
|
38
|
+
commit_count INTEGER NOT NULL DEFAULT 0,
|
|
39
|
+
push_count INTEGER NOT NULL DEFAULT 0
|
|
40
|
+
);
|
|
41
|
+
CREATE INDEX IF NOT EXISTS idx_session_started ON session(started_at);
|
|
42
|
+
|
|
43
|
+
CREATE TABLE IF NOT EXISTS session_model_usage (
|
|
44
|
+
session_id TEXT NOT NULL,
|
|
45
|
+
model TEXT NOT NULL,
|
|
46
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
47
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
48
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
49
|
+
cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
|
|
50
|
+
thinking_tokens INTEGER NOT NULL DEFAULT 0,
|
|
51
|
+
request_count INTEGER NOT NULL DEFAULT 0,
|
|
52
|
+
PRIMARY KEY (session_id, model)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
CREATE TABLE IF NOT EXISTS turn (
|
|
56
|
+
prompt_id TEXT PRIMARY KEY,
|
|
57
|
+
session_id TEXT NOT NULL,
|
|
58
|
+
turn_index INTEGER NOT NULL,
|
|
59
|
+
started_at TEXT NOT NULL,
|
|
60
|
+
ended_at TEXT,
|
|
61
|
+
duration_ms INTEGER,
|
|
62
|
+
tool_call_count INTEGER NOT NULL DEFAULT 0,
|
|
63
|
+
thinking_ms INTEGER NOT NULL DEFAULT 0,
|
|
64
|
+
fact_id_start INTEGER
|
|
65
|
+
);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_turn_session ON turn(session_id, turn_index);
|
|
67
|
+
|
|
68
|
+
CREATE TABLE IF NOT EXISTS permission_phase (
|
|
69
|
+
phase_id INTEGER PRIMARY KEY,
|
|
70
|
+
session_id TEXT NOT NULL,
|
|
71
|
+
from_mode TEXT,
|
|
72
|
+
to_mode TEXT NOT NULL,
|
|
73
|
+
trigger TEXT NOT NULL,
|
|
74
|
+
t_valid TEXT NOT NULL,
|
|
75
|
+
t_invalid TEXT,
|
|
76
|
+
fact_id INTEGER
|
|
77
|
+
);
|
|
78
|
+
CREATE INDEX IF NOT EXISTS idx_phase_session ON permission_phase(session_id, t_valid);
|
|
79
|
+
|
|
80
|
+
CREATE TABLE IF NOT EXISTS human_edit (
|
|
81
|
+
edit_id INTEGER PRIMARY KEY,
|
|
82
|
+
session_id TEXT NOT NULL,
|
|
83
|
+
file_path TEXT NOT NULL,
|
|
84
|
+
ts TEXT NOT NULL,
|
|
85
|
+
fact_id INTEGER,
|
|
86
|
+
UNIQUE (session_id, file_path, ts)
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
CREATE TABLE IF NOT EXISTS file_checkpoint (
|
|
90
|
+
checkpoint_id INTEGER PRIMARY KEY,
|
|
91
|
+
session_id TEXT NOT NULL,
|
|
92
|
+
file_path TEXT NOT NULL,
|
|
93
|
+
backup_file_name TEXT NOT NULL,
|
|
94
|
+
version INTEGER NOT NULL,
|
|
95
|
+
backup_time TEXT NOT NULL,
|
|
96
|
+
fact_id INTEGER,
|
|
97
|
+
UNIQUE (session_id, file_path, backup_file_name)
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
CREATE TABLE IF NOT EXISTS pr (
|
|
101
|
+
pr_ref TEXT PRIMARY KEY, number INTEGER, repo TEXT, title TEXT, state TEXT, url TEXT, merged_at TEXT
|
|
102
|
+
);
|
|
103
|
+
CREATE TABLE IF NOT EXISTS branch (
|
|
104
|
+
branch_ref TEXT PRIMARY KEY, repo TEXT, name TEXT NOT NULL, base TEXT, created_at TEXT, deleted_at TEXT
|
|
105
|
+
);
|
|
106
|
+
CREATE TABLE IF NOT EXISTS file (
|
|
107
|
+
file_ref TEXT PRIMARY KEY, repo TEXT, path TEXT NOT NULL
|
|
108
|
+
);
|
|
109
|
+
CREATE TABLE IF NOT EXISTS task (
|
|
110
|
+
task_ref TEXT PRIMARY KEY, task_id TEXT NOT NULL, initiative TEXT, title TEXT, status TEXT
|
|
111
|
+
);
|
|
112
|
+
CREATE TABLE IF NOT EXISTS subagent (
|
|
113
|
+
agent_ref TEXT PRIMARY KEY,
|
|
114
|
+
session_id TEXT,
|
|
115
|
+
child_session_id TEXT,
|
|
116
|
+
parent_agent_ref TEXT,
|
|
117
|
+
agent_type TEXT,
|
|
118
|
+
label TEXT,
|
|
119
|
+
started_at TEXT,
|
|
120
|
+
ended_at TEXT,
|
|
121
|
+
fact_id INTEGER
|
|
122
|
+
);
|
|
123
|
+
CREATE INDEX IF NOT EXISTS idx_subagent_child ON subagent(child_session_id);
|
|
124
|
+
CREATE TABLE IF NOT EXISTS artifact (
|
|
125
|
+
artifact_ref TEXT PRIMARY KEY, kind TEXT, title TEXT, url TEXT, path TEXT, created_at TEXT
|
|
126
|
+
);
|
|
127
|
+
CREATE TABLE IF NOT EXISTS pr_merge_observation (
|
|
128
|
+
number INTEGER NOT NULL, repo_hint TEXT, merged_at TEXT NOT NULL,
|
|
129
|
+
PRIMARY KEY (number, repo_hint, merged_at)
|
|
130
|
+
);
|
|
131
|
+
CREATE TABLE IF NOT EXISTS pr_create_observation (
|
|
132
|
+
tool_use_id TEXT PRIMARY KEY, title TEXT, number INTEGER, repo TEXT, url TEXT
|
|
133
|
+
);
|
|
134
|
+
`;
|
|
135
|
+
var DERIVED_TABLES = [
|
|
136
|
+
`${KIT.spanFts}_span`,
|
|
137
|
+
KIT.edge,
|
|
138
|
+
"turn",
|
|
139
|
+
"permission_phase",
|
|
140
|
+
"human_edit",
|
|
141
|
+
"file_checkpoint",
|
|
142
|
+
"subagent",
|
|
143
|
+
"session_model_usage",
|
|
144
|
+
"session",
|
|
145
|
+
"fact",
|
|
146
|
+
"pr",
|
|
147
|
+
"pr_merge_observation",
|
|
148
|
+
"pr_create_observation",
|
|
149
|
+
"branch",
|
|
150
|
+
"file",
|
|
151
|
+
"task",
|
|
152
|
+
"artifact"
|
|
153
|
+
];
|
|
154
|
+
var MIGRATIONS = [
|
|
155
|
+
kitMigration(1, { watermark: KIT.watermark, edge: KIT.edge, spanFts: KIT.spanFts }, "kit tables"),
|
|
156
|
+
{ version: 2, name: "session graph tables", up: (db) => db.exec(DOMAIN_DDL) }
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
// src/graph.ts
|
|
160
|
+
function openSessionGraph(dbPath) {
|
|
161
|
+
const db = openDatabase(dbPath);
|
|
162
|
+
runMigrations(db, MIGRATIONS);
|
|
163
|
+
return {
|
|
164
|
+
db,
|
|
165
|
+
transcripts: new WatermarkTable(db, { name: KIT.watermark }),
|
|
166
|
+
edges: new EdgeTable(db, { name: KIT.edge }),
|
|
167
|
+
spans: new SpanFtsTables(db, { name: KIT.spanFts })
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function resetIndex(graph) {
|
|
171
|
+
graph.db.transaction(() => {
|
|
172
|
+
for (const table of DERIVED_TABLES) graph.db.exec(`DELETE FROM "${table}"`);
|
|
173
|
+
graph.spans.clearIndex();
|
|
174
|
+
for (const row of graph.transcripts.list()) graph.transcripts.rewind(row.sourceKey);
|
|
175
|
+
})();
|
|
176
|
+
}
|
|
177
|
+
function allSessionIds(graph) {
|
|
178
|
+
return graph.db.prepare("SELECT session_id FROM session").all().map((r) => r.session_id);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/apply.ts
|
|
182
|
+
import { sessionRef } from "@titan-design/session-read";
|
|
183
|
+
function applyDelta(graph, transcriptId, delta) {
|
|
184
|
+
graph.db.transaction(() => {
|
|
185
|
+
applyFacts(graph.db, transcriptId, delta);
|
|
186
|
+
applySessions(graph.db, transcriptId, delta);
|
|
187
|
+
applyAssets(graph.db, delta);
|
|
188
|
+
applyPhases(graph.db, transcriptId, delta);
|
|
189
|
+
applyLinkedRows(graph, transcriptId, delta);
|
|
190
|
+
})();
|
|
191
|
+
}
|
|
192
|
+
var INSERT_FACT = `
|
|
193
|
+
INSERT INTO fact (transcript_id, byte_offset, byte_length, event_type, ts, seq, session_id, prompt_id, tool_use_id)
|
|
194
|
+
VALUES (@transcriptId, @byteOffset, @byteLength, @eventType, @ts, @seq, @sessionId, @promptId, @toolUseId)
|
|
195
|
+
ON CONFLICT (transcript_id, byte_offset) DO NOTHING`;
|
|
196
|
+
function applyFacts(db, transcriptId, delta) {
|
|
197
|
+
const next = db.prepare("SELECT COALESCE(MAX(seq), -1) + 1 AS next FROM fact WHERE transcript_id = ?").get(transcriptId);
|
|
198
|
+
const insert = db.prepare(INSERT_FACT);
|
|
199
|
+
let seq = next.next;
|
|
200
|
+
for (const f of delta.facts) {
|
|
201
|
+
insert.run({ transcriptId, byteOffset: f.byteOffset, byteLength: f.byteLength, eventType: f.eventType, ts: f.ts, seq: seq++, sessionId: f.sessionId, promptId: f.promptId, toolUseId: f.toolUseId });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
var UPSERT_SESSION = `
|
|
205
|
+
INSERT INTO session (session_id, transcript_id, started_at, ended_at, start_type, cwd, git_branch, ai_title, seed_prompt, cli_version, turn_count, commit_count, push_count)
|
|
206
|
+
VALUES (@sessionId, @transcriptId, @startedAt, @endedAt, @startType, @cwd, @gitBranch, @aiTitle, @seedPrompt, @cliVersion, @turnDelta, @commitDelta, @pushDelta)
|
|
207
|
+
ON CONFLICT (session_id) DO UPDATE SET
|
|
208
|
+
started_at = MIN(COALESCE(started_at, excluded.started_at), COALESCE(excluded.started_at, started_at)),
|
|
209
|
+
ended_at = MAX(COALESCE(ended_at, excluded.ended_at), COALESCE(excluded.ended_at, ended_at)),
|
|
210
|
+
start_type = CASE
|
|
211
|
+
WHEN excluded.start_type IS NULL THEN start_type
|
|
212
|
+
WHEN start_type IS NULL THEN excluded.start_type
|
|
213
|
+
WHEN COALESCE(excluded.started_at, started_at) < COALESCE(started_at, excluded.started_at) THEN excluded.start_type
|
|
214
|
+
ELSE start_type END,
|
|
215
|
+
cwd = COALESCE(excluded.cwd, cwd),
|
|
216
|
+
git_branch = COALESCE(git_branch, excluded.git_branch),
|
|
217
|
+
ai_title = COALESCE(excluded.ai_title, ai_title),
|
|
218
|
+
seed_prompt = COALESCE(seed_prompt, excluded.seed_prompt),
|
|
219
|
+
cli_version = COALESCE(excluded.cli_version, cli_version),
|
|
220
|
+
turn_count = turn_count + excluded.turn_count,
|
|
221
|
+
commit_count = commit_count + excluded.commit_count,
|
|
222
|
+
push_count = push_count + excluded.push_count`;
|
|
223
|
+
var UPSERT_USAGE = `
|
|
224
|
+
INSERT INTO session_model_usage (session_id, model, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, thinking_tokens, request_count)
|
|
225
|
+
VALUES (@sessionId, @model, @inputTokens, @outputTokens, @cacheReadTokens, @cacheCreationTokens, @thinkingTokens, @requestCount)
|
|
226
|
+
ON CONFLICT (session_id, model) DO UPDATE SET
|
|
227
|
+
input_tokens = input_tokens + excluded.input_tokens, output_tokens = output_tokens + excluded.output_tokens,
|
|
228
|
+
cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
|
|
229
|
+
cache_creation_tokens = cache_creation_tokens + excluded.cache_creation_tokens,
|
|
230
|
+
thinking_tokens = thinking_tokens + excluded.thinking_tokens, request_count = request_count + excluded.request_count`;
|
|
231
|
+
function applySessions(db, transcriptId, delta) {
|
|
232
|
+
const upsertSession = db.prepare(UPSERT_SESSION);
|
|
233
|
+
for (const s of delta.sessions) upsertSession.run({ ...s, transcriptId });
|
|
234
|
+
const upsertUsage = db.prepare(UPSERT_USAGE);
|
|
235
|
+
for (const u of delta.usage) upsertUsage.run(u);
|
|
236
|
+
}
|
|
237
|
+
var ASSET_UPSERTS = {
|
|
238
|
+
prs: `INSERT INTO pr (pr_ref, number, repo, title, url) VALUES (@prRef, @number, @repo, @title, @url)
|
|
239
|
+
ON CONFLICT (pr_ref) DO UPDATE SET title = COALESCE(title, excluded.title), url = COALESCE(url, excluded.url)`,
|
|
240
|
+
branches: `INSERT INTO branch (branch_ref, repo, name, base, created_at, deleted_at) VALUES (@branchRef, @repo, @name, @base, @createdAt, @deletedAt)
|
|
241
|
+
ON CONFLICT (branch_ref) DO UPDATE SET base = COALESCE(base, excluded.base),
|
|
242
|
+
created_at = MIN(COALESCE(created_at, excluded.created_at), COALESCE(excluded.created_at, created_at)),
|
|
243
|
+
deleted_at = MAX(COALESCE(deleted_at, excluded.deleted_at), COALESCE(excluded.deleted_at, deleted_at))`,
|
|
244
|
+
files: `INSERT INTO file (file_ref, repo, path) VALUES (@fileRef, @repo, @path) ON CONFLICT (file_ref) DO NOTHING`,
|
|
245
|
+
tasks: `INSERT INTO task (task_ref, task_id) VALUES (@taskRef, @taskId) ON CONFLICT (task_ref) DO NOTHING`,
|
|
246
|
+
artifacts: `INSERT INTO artifact (artifact_ref, kind, title, url, path, created_at) VALUES (@artifactRef, @artifactKind, @title, @url, @path, @ts)
|
|
247
|
+
ON CONFLICT (artifact_ref) DO NOTHING`,
|
|
248
|
+
prMerges: `INSERT INTO pr_merge_observation (number, repo_hint, merged_at) VALUES (@number, @repoHint, @ts)
|
|
249
|
+
ON CONFLICT (number, repo_hint, merged_at) DO NOTHING`,
|
|
250
|
+
prCreates: `INSERT INTO pr_create_observation (tool_use_id, title, number, repo, url) VALUES (@toolUseId, @title, @number, @repo, @url)
|
|
251
|
+
ON CONFLICT (tool_use_id) DO UPDATE SET title = COALESCE(title, excluded.title), number = COALESCE(number, excluded.number),
|
|
252
|
+
repo = COALESCE(repo, excluded.repo), url = COALESCE(url, excluded.url)`
|
|
253
|
+
};
|
|
254
|
+
function applyAssets(db, delta) {
|
|
255
|
+
for (const [kind, sql] of Object.entries(ASSET_UPSERTS)) {
|
|
256
|
+
const statement = db.prepare(sql);
|
|
257
|
+
for (const row of delta[kind]) statement.run(pick(row, sql));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function pick(row, sql) {
|
|
261
|
+
const names = new Set([...sql.matchAll(/@(\w+)/g)].map((m) => m[1]));
|
|
262
|
+
return Object.fromEntries([...names].map((name) => [name, row[name] ?? null]));
|
|
263
|
+
}
|
|
264
|
+
function applyPhases(db, transcriptId, delta) {
|
|
265
|
+
const current = db.prepare("SELECT phase_id, to_mode FROM permission_phase WHERE session_id = ? AND trigger = ? AND t_invalid IS NULL ORDER BY phase_id DESC LIMIT 1");
|
|
266
|
+
const close = db.prepare("UPDATE permission_phase SET t_invalid = ? WHERE phase_id = ?");
|
|
267
|
+
const insert = db.prepare("INSERT INTO permission_phase (session_id, from_mode, to_mode, trigger, t_valid, fact_id) VALUES (?, ?, ?, ?, ?, ?)");
|
|
268
|
+
for (const phase of delta.phases) {
|
|
269
|
+
const open = current.get(phase.sessionId, phase.trigger);
|
|
270
|
+
if (open?.to_mode === phase.toMode) continue;
|
|
271
|
+
if (open) close.run(phase.ts, open.phase_id);
|
|
272
|
+
insert.run(phase.sessionId, open?.to_mode ?? null, phase.toMode, phase.trigger, phase.ts, factIdFor(db, transcriptId, phase.byteOffset));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function applyLinkedRows(graph, transcriptId, delta) {
|
|
276
|
+
const { db } = graph;
|
|
277
|
+
const factId = (byteOffset) => factIdFor(db, transcriptId, byteOffset);
|
|
278
|
+
const upsertTurn = db.prepare(`
|
|
279
|
+
INSERT INTO turn (prompt_id, session_id, turn_index, started_at, fact_id_start)
|
|
280
|
+
VALUES (?, ?, (SELECT COALESCE(MAX(turn_index), -1) + 1 FROM turn WHERE session_id = ?), ?, ?)
|
|
281
|
+
ON CONFLICT (prompt_id) DO UPDATE SET session_id = MIN(session_id, excluded.session_id)`);
|
|
282
|
+
for (const t of delta.turns) upsertTurn.run(t.promptId, t.sessionId, t.sessionId, t.ts, factId(t.byteOffset));
|
|
283
|
+
const insertEdit = db.prepare("INSERT INTO human_edit (session_id, file_path, ts, fact_id) VALUES (?, ?, ?, ?) ON CONFLICT (session_id, file_path, ts) DO NOTHING");
|
|
284
|
+
for (const e of delta.humanEdits) insertEdit.run(e.sessionId, e.filePath, e.ts, factId(e.byteOffset));
|
|
285
|
+
const insertCheckpoint = db.prepare(`
|
|
286
|
+
INSERT INTO file_checkpoint (session_id, file_path, backup_file_name, version, backup_time, fact_id) VALUES (?, ?, ?, ?, ?, ?)
|
|
287
|
+
ON CONFLICT (session_id, file_path, backup_file_name) DO NOTHING`);
|
|
288
|
+
for (const c of delta.fileCheckpoints) insertCheckpoint.run(c.sessionId, c.filePath, c.backupFileName, c.version, c.backupTime, factId(c.byteOffset));
|
|
289
|
+
const upsertSubagent = db.prepare(`
|
|
290
|
+
INSERT INTO subagent (agent_ref, session_id, agent_type, label, started_at, fact_id) VALUES (?, ?, ?, ?, ?, ?)
|
|
291
|
+
ON CONFLICT (agent_ref) DO UPDATE SET session_id = COALESCE(session_id, excluded.session_id),
|
|
292
|
+
agent_type = COALESCE(agent_type, excluded.agent_type), label = COALESCE(label, excluded.label), started_at = COALESCE(started_at, excluded.started_at)`);
|
|
293
|
+
for (const s of delta.subagents) upsertSubagent.run(s.agentRef, s.sessionId, s.agentType, s.label, s.ts || null, factId(s.byteOffset));
|
|
294
|
+
const linkTranscript = db.prepare("INSERT INTO subagent (agent_ref, child_session_id) VALUES (?, ?) ON CONFLICT (agent_ref) DO UPDATE SET child_session_id = excluded.child_session_id");
|
|
295
|
+
for (const l of delta.subagentTranscripts) linkTranscript.run(l.agentRef, l.childSessionId);
|
|
296
|
+
for (const e of delta.edges) graph.edges.assert({ sourceRef: e.sourceRef, relation: e.relation, targetRef: e.targetRef, tValid: e.ts, factId: factId(e.byteOffset) });
|
|
297
|
+
for (const span of delta.spans) {
|
|
298
|
+
graph.spans.index({ ownerRef: sessionRef(span.sessionId), field: span.field, sourceId: transcriptId, byteOffset: span.byteOffset, byteLength: span.byteLength }, span.text);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function factIdFor(db, transcriptId, byteOffset) {
|
|
302
|
+
const row = db.prepare("SELECT fact_id FROM fact WHERE transcript_id = ? AND byte_offset = ?").get(transcriptId, byteOffset);
|
|
303
|
+
return row?.fact_id ?? null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/rollup.ts
|
|
307
|
+
var BATCH = 400;
|
|
308
|
+
var ROLLUP = `
|
|
309
|
+
WITH ordered AS (
|
|
310
|
+
SELECT fact_id, session_id, event_type, ts, prompt_id,
|
|
311
|
+
COUNT(CASE WHEN event_type = 'user_prompt' THEN 1 END) OVER (
|
|
312
|
+
PARTITION BY session_id ORDER BY ts, transcript_id, seq ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS turn_no,
|
|
313
|
+
LAG(event_type) OVER (PARTITION BY session_id ORDER BY ts, transcript_id, seq) AS prev_type,
|
|
314
|
+
LAG(ts) OVER (PARTITION BY session_id ORDER BY ts, transcript_id, seq) AS prev_ts
|
|
315
|
+
FROM fact WHERE session_id IN (SELECT value FROM json_each(@sessionIds))
|
|
316
|
+
),
|
|
317
|
+
owners AS (SELECT session_id, turn_no, prompt_id FROM ordered WHERE event_type = 'user_prompt'),
|
|
318
|
+
agg AS (
|
|
319
|
+
SELECT owners.prompt_id AS prompt_id, MAX(ordered.ts) AS ended_at,
|
|
320
|
+
CAST(ROUND((julianday(MAX(ordered.ts)) - julianday(MIN(ordered.ts))) * 86400000) AS INTEGER) AS duration_ms,
|
|
321
|
+
SUM(CASE WHEN ordered.event_type = 'tool_decision' THEN 1 ELSE 0 END) AS tool_call_count,
|
|
322
|
+
CAST(COALESCE(SUM(CASE
|
|
323
|
+
WHEN ordered.event_type IN ('assistant_response', 'tool_decision') AND ordered.prev_type IN ('user_prompt', 'tool_result', 'tool_result_error')
|
|
324
|
+
THEN ROUND((julianday(ordered.ts) - julianday(ordered.prev_ts)) * 86400000) END), 0) AS INTEGER) AS thinking_ms
|
|
325
|
+
FROM ordered JOIN owners ON owners.session_id = ordered.session_id AND owners.turn_no = ordered.turn_no
|
|
326
|
+
GROUP BY owners.prompt_id
|
|
327
|
+
)
|
|
328
|
+
UPDATE turn SET ended_at = agg.ended_at, duration_ms = COALESCE(agg.duration_ms, 0),
|
|
329
|
+
tool_call_count = agg.tool_call_count, thinking_ms = COALESCE(agg.thinking_ms, 0)
|
|
330
|
+
FROM agg WHERE turn.prompt_id = agg.prompt_id`;
|
|
331
|
+
var RENUMBER_TURNS = `
|
|
332
|
+
WITH ordered AS (
|
|
333
|
+
SELECT prompt_id, ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY started_at, prompt_id) - 1 AS idx
|
|
334
|
+
FROM turn WHERE session_id IN (SELECT value FROM json_each(@sessionIds))
|
|
335
|
+
)
|
|
336
|
+
UPDATE turn SET turn_index = ordered.idx FROM ordered WHERE turn.prompt_id = ordered.prompt_id`;
|
|
337
|
+
function rollupSessions(graph, sessionIds) {
|
|
338
|
+
const unique = [...new Set(sessionIds)];
|
|
339
|
+
if (unique.length === 0) return 0;
|
|
340
|
+
const aggregate = graph.db.prepare(ROLLUP);
|
|
341
|
+
const renumber = graph.db.prepare(RENUMBER_TURNS);
|
|
342
|
+
return graph.db.transaction(() => {
|
|
343
|
+
let updated = 0;
|
|
344
|
+
for (let i = 0; i < unique.length; i += BATCH) {
|
|
345
|
+
const batch = JSON.stringify(unique.slice(i, i + BATCH));
|
|
346
|
+
renumber.run({ sessionIds: batch });
|
|
347
|
+
updated += aggregate.run({ sessionIds: batch }).changes;
|
|
348
|
+
}
|
|
349
|
+
return updated;
|
|
350
|
+
})();
|
|
351
|
+
}
|
|
352
|
+
var RECONCILE_PR_MERGES = `
|
|
353
|
+
UPDATE pr SET state = 'merged',
|
|
354
|
+
merged_at = (SELECT MIN(o.merged_at) FROM pr_merge_observation o WHERE o.number = pr.number AND (o.repo_hint IS NULL OR pr.repo LIKE '%/' || o.repo_hint))
|
|
355
|
+
WHERE EXISTS (SELECT 1 FROM pr_merge_observation o WHERE o.number = pr.number AND (o.repo_hint IS NULL OR pr.repo LIKE '%/' || o.repo_hint))`;
|
|
356
|
+
var RECONCILE_PR_CREATES = `
|
|
357
|
+
INSERT INTO pr (pr_ref, number, repo, title, url)
|
|
358
|
+
SELECT 'pr:' || repo || '#' || number, number, repo, title, url FROM pr_create_observation
|
|
359
|
+
WHERE number IS NOT NULL AND repo IS NOT NULL AND title IS NOT NULL
|
|
360
|
+
ON CONFLICT (pr_ref) DO UPDATE SET title = COALESCE(pr.title, excluded.title), url = COALESCE(pr.url, excluded.url)`;
|
|
361
|
+
var RECONCILE_SUBAGENTS = `
|
|
362
|
+
UPDATE subagent SET
|
|
363
|
+
ended_at = COALESCE((SELECT s.ended_at FROM session s WHERE s.session_id = subagent.child_session_id), ended_at),
|
|
364
|
+
parent_agent_ref = COALESCE((SELECT p.agent_ref FROM subagent p WHERE p.child_session_id = subagent.session_id), parent_agent_ref)`;
|
|
365
|
+
function reconcile(graph) {
|
|
366
|
+
return {
|
|
367
|
+
prCreates: graph.db.prepare(RECONCILE_PR_CREATES).run().changes,
|
|
368
|
+
prMerges: graph.db.prepare(RECONCILE_PR_MERGES).run().changes,
|
|
369
|
+
subagents: graph.db.prepare(RECONCILE_SUBAGENTS).run().changes
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/refresh.ts
|
|
374
|
+
import { promises as fs } from "fs";
|
|
375
|
+
import { contentHash, resumePoint } from "@titan-design/locator";
|
|
376
|
+
import { TranscriptParseError, extractTranscript } from "@titan-design/session-read";
|
|
377
|
+
async function indexTranscript(graph, transcript, options = {}) {
|
|
378
|
+
const row = graph.transcripts.ensure(transcript.displayPath);
|
|
379
|
+
const entry = { path: transcript.displayPath, lastByteOffset: row.lastOffset, prefixHash: row.prefixHash };
|
|
380
|
+
const point = await resumePoint(entry, transcript.absolutePath, { verifyHash: options.verifyHash });
|
|
381
|
+
const base = { transcriptId: row.sourceId, sessionIds: [] };
|
|
382
|
+
if (point.state === "missing") {
|
|
383
|
+
graph.transcripts.markStatus(transcript.displayPath, "missing", "source file no longer exists");
|
|
384
|
+
return { ...base, status: "missing", facts: 0, reason: "source file no longer exists" };
|
|
385
|
+
}
|
|
386
|
+
if (point.state === "unchanged") return { ...base, status: "unchanged", facts: 0 };
|
|
387
|
+
try {
|
|
388
|
+
const delta = await extractTranscript(transcript.absolutePath, {
|
|
389
|
+
fromByteOffset: point.start,
|
|
390
|
+
priorPrefixHash: point.state === "rewritten" ? null : row.prefixHash,
|
|
391
|
+
subagentId: transcript.subagentId
|
|
392
|
+
});
|
|
393
|
+
applyDelta(graph, row.sourceId, delta);
|
|
394
|
+
const stat = await fs.stat(transcript.absolutePath);
|
|
395
|
+
graph.transcripts.advance(transcript.displayPath, {
|
|
396
|
+
lastOffset: delta.lastByteOffset,
|
|
397
|
+
prefixHash: delta.prefixHash,
|
|
398
|
+
fileSize: stat.size,
|
|
399
|
+
fileMtime: stat.mtime.toISOString(),
|
|
400
|
+
contentHash: options.withContentHash ? await contentHash(transcript.absolutePath) : null
|
|
401
|
+
});
|
|
402
|
+
const sessionIds = delta.sessions.map((s) => s.sessionId);
|
|
403
|
+
return { ...base, sessionIds, status: point.state === "rewritten" ? "rewound" : "indexed", facts: delta.facts.length };
|
|
404
|
+
} catch (err) {
|
|
405
|
+
if (!(err instanceof TranscriptParseError)) throw err;
|
|
406
|
+
graph.transcripts.markStatus(transcript.displayPath, "quarantined", err.message);
|
|
407
|
+
return { ...base, status: "quarantined", facts: 0, reason: err.message };
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async function refreshCorpus(graph, transcripts, options = {}) {
|
|
411
|
+
const counts = { indexed: 0, unchanged: 0, rewound: 0, missing: 0, quarantined: 0 };
|
|
412
|
+
const touched = [];
|
|
413
|
+
let facts = 0;
|
|
414
|
+
for (const transcript of transcripts) {
|
|
415
|
+
const outcome = await indexTranscript(graph, transcript, options);
|
|
416
|
+
counts[outcome.status] += 1;
|
|
417
|
+
facts += outcome.facts;
|
|
418
|
+
touched.push(...outcome.sessionIds);
|
|
419
|
+
}
|
|
420
|
+
const turnsRolledUp = rollupSessions(graph, options.full ? allSessionIds(graph) : touched);
|
|
421
|
+
const reconciled = reconcile(graph);
|
|
422
|
+
const markedMissing = await markMissing(graph, transcripts);
|
|
423
|
+
return { transcripts: transcripts.length, ...counts, facts, turnsRolledUp, reconciled, markedMissing };
|
|
424
|
+
}
|
|
425
|
+
async function markMissing(graph, discovered) {
|
|
426
|
+
const present = new Set(discovered.map((t) => t.displayPath));
|
|
427
|
+
const byPath = new Map(discovered.map((t) => [t.displayPath, t.absolutePath]));
|
|
428
|
+
let marked = 0;
|
|
429
|
+
for (const row of graph.transcripts.list()) {
|
|
430
|
+
if (row.status === "missing" || present.has(row.sourceKey)) continue;
|
|
431
|
+
const absolute = byPath.get(row.sourceKey) ?? row.sourceKey;
|
|
432
|
+
if (await exists(absolute)) continue;
|
|
433
|
+
graph.transcripts.markStatus(row.sourceKey, "missing", "source file no longer exists");
|
|
434
|
+
marked += 1;
|
|
435
|
+
}
|
|
436
|
+
return marked;
|
|
437
|
+
}
|
|
438
|
+
async function exists(absolutePath) {
|
|
439
|
+
try {
|
|
440
|
+
await fs.stat(absolutePath);
|
|
441
|
+
return true;
|
|
442
|
+
} catch {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
export {
|
|
447
|
+
DERIVED_TABLES,
|
|
448
|
+
DOMAIN_DDL,
|
|
449
|
+
KIT,
|
|
450
|
+
MIGRATIONS,
|
|
451
|
+
allSessionIds,
|
|
452
|
+
applyDelta,
|
|
453
|
+
indexTranscript,
|
|
454
|
+
openSessionGraph,
|
|
455
|
+
reconcile,
|
|
456
|
+
refreshCorpus,
|
|
457
|
+
resetIndex,
|
|
458
|
+
rollupSessions
|
|
459
|
+
};
|
|
460
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/graph.ts","../src/schema.ts","../src/apply.ts","../src/rollup.ts","../src/refresh.ts"],"sourcesContent":["import { EdgeTable, SpanFtsTables, WatermarkTable, openDatabase, runMigrations, type Db } from \"@titan-design/store-sqlite\";\nimport { DERIVED_TABLES, KIT, MIGRATIONS } from \"./schema.js\";\n\n/** One open session graph: the connection plus the kit helpers bound to its tables. */\nexport interface SessionGraph {\n db: Db;\n transcripts: WatermarkTable;\n edges: EdgeTable;\n spans: SpanFtsTables;\n}\n\nexport function openSessionGraph(dbPath: string): SessionGraph {\n const db = openDatabase(dbPath);\n runMigrations(db, MIGRATIONS);\n return {\n db,\n transcripts: new WatermarkTable(db, { name: KIT.watermark }),\n edges: new EdgeTable(db, { name: KIT.edge }),\n spans: new SpanFtsTables(db, { name: KIT.spanFts }),\n };\n}\n\n/**\n * Clear every derived row and rewind all watermarks so the next refresh\n * rebuilds from byte 0. Rewinding alone is not enough: the accumulating\n * upserts would add a second copy of every count.\n */\nexport function resetIndex(graph: SessionGraph): void {\n graph.db.transaction(() => {\n for (const table of DERIVED_TABLES) graph.db.exec(`DELETE FROM \"${table}\"`);\n graph.spans.clearIndex();\n for (const row of graph.transcripts.list()) graph.transcripts.rewind(row.sourceKey);\n })();\n}\n\nexport function allSessionIds(graph: SessionGraph): string[] {\n return (graph.db.prepare(\"SELECT session_id FROM session\").all() as { session_id: string }[]).map((r) => r.session_id);\n}\n","import { SQL_NOW, kitMigration, type Migration } from \"@titan-design/store-sqlite\";\n\n/** Kit table names this graph uses. `watermark` rows are transcripts; `search_*` are the FTS spans. */\nexport const KIT = { watermark: \"transcript\", edge: \"edge\", spanFts: \"search\" } as const;\n\n/**\n * Domain tables. Everything here is derivable from the transcripts plus the\n * watermark table, which is what makes drop-and-rederive a safe rebuild.\n * Locators are `(transcript_id, byte_offset, byte_length)`; `fact_id` rows\n * resolve through the facts table's unique `(transcript_id, byte_offset)`.\n */\nexport const DOMAIN_DDL = `\n CREATE TABLE IF NOT EXISTS fact (\n fact_id INTEGER PRIMARY KEY,\n transcript_id INTEGER NOT NULL,\n byte_offset INTEGER NOT NULL,\n byte_length INTEGER NOT NULL,\n event_type TEXT NOT NULL,\n ts TEXT NOT NULL,\n seq INTEGER NOT NULL,\n session_id TEXT NOT NULL,\n prompt_id TEXT,\n tool_use_id TEXT,\n t_indexed TEXT NOT NULL DEFAULT (${SQL_NOW}),\n UNIQUE (transcript_id, byte_offset)\n );\n CREATE INDEX IF NOT EXISTS idx_fact_session_ts ON fact(session_id, ts);\n CREATE INDEX IF NOT EXISTS idx_fact_prompt ON fact(prompt_id);\n CREATE INDEX IF NOT EXISTS idx_fact_tool_use ON fact(tool_use_id);\n\n CREATE TABLE IF NOT EXISTS session (\n session_id TEXT PRIMARY KEY,\n transcript_id INTEGER,\n started_at TEXT,\n ended_at TEXT,\n start_type TEXT,\n cwd TEXT,\n git_branch TEXT,\n ai_title TEXT,\n seed_prompt TEXT,\n cli_version TEXT,\n turn_count INTEGER NOT NULL DEFAULT 0,\n commit_count INTEGER NOT NULL DEFAULT 0,\n push_count INTEGER NOT NULL DEFAULT 0\n );\n CREATE INDEX IF NOT EXISTS idx_session_started ON session(started_at);\n\n CREATE TABLE IF NOT EXISTS session_model_usage (\n session_id TEXT NOT NULL,\n model TEXT NOT NULL,\n input_tokens INTEGER NOT NULL DEFAULT 0,\n output_tokens INTEGER NOT NULL DEFAULT 0,\n cache_read_tokens INTEGER NOT NULL DEFAULT 0,\n cache_creation_tokens INTEGER NOT NULL DEFAULT 0,\n thinking_tokens INTEGER NOT NULL DEFAULT 0,\n request_count INTEGER NOT NULL DEFAULT 0,\n PRIMARY KEY (session_id, model)\n );\n\n CREATE TABLE IF NOT EXISTS turn (\n prompt_id TEXT PRIMARY KEY,\n session_id TEXT NOT NULL,\n turn_index INTEGER NOT NULL,\n started_at TEXT NOT NULL,\n ended_at TEXT,\n duration_ms INTEGER,\n tool_call_count INTEGER NOT NULL DEFAULT 0,\n thinking_ms INTEGER NOT NULL DEFAULT 0,\n fact_id_start INTEGER\n );\n CREATE INDEX IF NOT EXISTS idx_turn_session ON turn(session_id, turn_index);\n\n CREATE TABLE IF NOT EXISTS permission_phase (\n phase_id INTEGER PRIMARY KEY,\n session_id TEXT NOT NULL,\n from_mode TEXT,\n to_mode TEXT NOT NULL,\n trigger TEXT NOT NULL,\n t_valid TEXT NOT NULL,\n t_invalid TEXT,\n fact_id INTEGER\n );\n CREATE INDEX IF NOT EXISTS idx_phase_session ON permission_phase(session_id, t_valid);\n\n CREATE TABLE IF NOT EXISTS human_edit (\n edit_id INTEGER PRIMARY KEY,\n session_id TEXT NOT NULL,\n file_path TEXT NOT NULL,\n ts TEXT NOT NULL,\n fact_id INTEGER,\n UNIQUE (session_id, file_path, ts)\n );\n\n CREATE TABLE IF NOT EXISTS file_checkpoint (\n checkpoint_id INTEGER PRIMARY KEY,\n session_id TEXT NOT NULL,\n file_path TEXT NOT NULL,\n backup_file_name TEXT NOT NULL,\n version INTEGER NOT NULL,\n backup_time TEXT NOT NULL,\n fact_id INTEGER,\n UNIQUE (session_id, file_path, backup_file_name)\n );\n\n CREATE TABLE IF NOT EXISTS pr (\n pr_ref TEXT PRIMARY KEY, number INTEGER, repo TEXT, title TEXT, state TEXT, url TEXT, merged_at TEXT\n );\n CREATE TABLE IF NOT EXISTS branch (\n branch_ref TEXT PRIMARY KEY, repo TEXT, name TEXT NOT NULL, base TEXT, created_at TEXT, deleted_at TEXT\n );\n CREATE TABLE IF NOT EXISTS file (\n file_ref TEXT PRIMARY KEY, repo TEXT, path TEXT NOT NULL\n );\n CREATE TABLE IF NOT EXISTS task (\n task_ref TEXT PRIMARY KEY, task_id TEXT NOT NULL, initiative TEXT, title TEXT, status TEXT\n );\n CREATE TABLE IF NOT EXISTS subagent (\n agent_ref TEXT PRIMARY KEY,\n session_id TEXT,\n child_session_id TEXT,\n parent_agent_ref TEXT,\n agent_type TEXT,\n label TEXT,\n started_at TEXT,\n ended_at TEXT,\n fact_id INTEGER\n );\n CREATE INDEX IF NOT EXISTS idx_subagent_child ON subagent(child_session_id);\n CREATE TABLE IF NOT EXISTS artifact (\n artifact_ref TEXT PRIMARY KEY, kind TEXT, title TEXT, url TEXT, path TEXT, created_at TEXT\n );\n CREATE TABLE IF NOT EXISTS pr_merge_observation (\n number INTEGER NOT NULL, repo_hint TEXT, merged_at TEXT NOT NULL,\n PRIMARY KEY (number, repo_hint, merged_at)\n );\n CREATE TABLE IF NOT EXISTS pr_create_observation (\n tool_use_id TEXT PRIMARY KEY, title TEXT, number INTEGER, repo TEXT, url TEXT\n );\n`;\n\n/** Every derived table, in an order safe to clear. The watermark table is not derived. */\nexport const DERIVED_TABLES = [\n `${KIT.spanFts}_span`,\n KIT.edge,\n \"turn\",\n \"permission_phase\",\n \"human_edit\",\n \"file_checkpoint\",\n \"subagent\",\n \"session_model_usage\",\n \"session\",\n \"fact\",\n \"pr\",\n \"pr_merge_observation\",\n \"pr_create_observation\",\n \"branch\",\n \"file\",\n \"task\",\n \"artifact\",\n] as const;\n\nexport const MIGRATIONS: Migration[] = [\n kitMigration(1, { watermark: KIT.watermark, edge: KIT.edge, spanFts: KIT.spanFts }, \"kit tables\"),\n { version: 2, name: \"session graph tables\", up: (db) => db.exec(DOMAIN_DDL) },\n];\n","import { sessionRef, type TranscriptDelta } from \"@titan-design/session-read\";\nimport type { Db } from \"@titan-design/store-sqlite\";\nimport type { SessionGraph } from \"./graph.js\";\n\n/**\n * Apply one transcript chunk's delta in a single transaction, so a crash can\n * never leave rows committed behind a stale watermark or ahead of their rows.\n * Append-only tables are idempotent through unique indexes; session and usage\n * rows accumulate; ref-keyed assets insert-if-absent with COALESCE merges that\n * mirror `EventFolder`'s rules.\n */\nexport function applyDelta(graph: SessionGraph, transcriptId: number, delta: TranscriptDelta): void {\n graph.db.transaction(() => {\n applyFacts(graph.db, transcriptId, delta);\n applySessions(graph.db, transcriptId, delta);\n applyAssets(graph.db, delta);\n applyPhases(graph.db, transcriptId, delta);\n applyLinkedRows(graph, transcriptId, delta);\n })();\n}\n\nconst INSERT_FACT = `\n INSERT INTO fact (transcript_id, byte_offset, byte_length, event_type, ts, seq, session_id, prompt_id, tool_use_id)\n VALUES (@transcriptId, @byteOffset, @byteLength, @eventType, @ts, @seq, @sessionId, @promptId, @toolUseId)\n ON CONFLICT (transcript_id, byte_offset) DO NOTHING`;\n\n/** Facts carry a per-transcript `seq` continuing from whatever is stored. */\nfunction applyFacts(db: Db, transcriptId: number, delta: TranscriptDelta): void {\n const next = db.prepare(\"SELECT COALESCE(MAX(seq), -1) + 1 AS next FROM fact WHERE transcript_id = ?\").get(transcriptId) as { next: number };\n const insert = db.prepare(INSERT_FACT);\n let seq = next.next;\n for (const f of delta.facts) {\n insert.run({ transcriptId, byteOffset: f.byteOffset, byteLength: f.byteLength, eventType: f.eventType, ts: f.ts, seq: seq++, sessionId: f.sessionId, promptId: f.promptId, toolUseId: f.toolUseId });\n }\n}\n\nconst UPSERT_SESSION = `\n INSERT INTO session (session_id, transcript_id, started_at, ended_at, start_type, cwd, git_branch, ai_title, seed_prompt, cli_version, turn_count, commit_count, push_count)\n VALUES (@sessionId, @transcriptId, @startedAt, @endedAt, @startType, @cwd, @gitBranch, @aiTitle, @seedPrompt, @cliVersion, @turnDelta, @commitDelta, @pushDelta)\n ON CONFLICT (session_id) DO UPDATE SET\n started_at = MIN(COALESCE(started_at, excluded.started_at), COALESCE(excluded.started_at, started_at)),\n ended_at = MAX(COALESCE(ended_at, excluded.ended_at), COALESCE(excluded.ended_at, ended_at)),\n start_type = CASE\n WHEN excluded.start_type IS NULL THEN start_type\n WHEN start_type IS NULL THEN excluded.start_type\n WHEN COALESCE(excluded.started_at, started_at) < COALESCE(started_at, excluded.started_at) THEN excluded.start_type\n ELSE start_type END,\n cwd = COALESCE(excluded.cwd, cwd),\n git_branch = COALESCE(git_branch, excluded.git_branch),\n ai_title = COALESCE(excluded.ai_title, ai_title),\n seed_prompt = COALESCE(seed_prompt, excluded.seed_prompt),\n cli_version = COALESCE(excluded.cli_version, cli_version),\n turn_count = turn_count + excluded.turn_count,\n commit_count = commit_count + excluded.commit_count,\n push_count = push_count + excluded.push_count`;\n\nconst UPSERT_USAGE = `\n INSERT INTO session_model_usage (session_id, model, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, thinking_tokens, request_count)\n VALUES (@sessionId, @model, @inputTokens, @outputTokens, @cacheReadTokens, @cacheCreationTokens, @thinkingTokens, @requestCount)\n ON CONFLICT (session_id, model) DO UPDATE SET\n input_tokens = input_tokens + excluded.input_tokens, output_tokens = output_tokens + excluded.output_tokens,\n cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,\n cache_creation_tokens = cache_creation_tokens + excluded.cache_creation_tokens,\n thinking_tokens = thinking_tokens + excluded.thinking_tokens, request_count = request_count + excluded.request_count`;\n\nfunction applySessions(db: Db, transcriptId: number, delta: TranscriptDelta): void {\n const upsertSession = db.prepare(UPSERT_SESSION);\n for (const s of delta.sessions) upsertSession.run({ ...s, transcriptId });\n const upsertUsage = db.prepare(UPSERT_USAGE);\n for (const u of delta.usage) upsertUsage.run(u);\n}\n\nconst ASSET_UPSERTS = {\n prs: `INSERT INTO pr (pr_ref, number, repo, title, url) VALUES (@prRef, @number, @repo, @title, @url)\n ON CONFLICT (pr_ref) DO UPDATE SET title = COALESCE(title, excluded.title), url = COALESCE(url, excluded.url)`,\n branches: `INSERT INTO branch (branch_ref, repo, name, base, created_at, deleted_at) VALUES (@branchRef, @repo, @name, @base, @createdAt, @deletedAt)\n ON CONFLICT (branch_ref) DO UPDATE SET base = COALESCE(base, excluded.base),\n created_at = MIN(COALESCE(created_at, excluded.created_at), COALESCE(excluded.created_at, created_at)),\n deleted_at = MAX(COALESCE(deleted_at, excluded.deleted_at), COALESCE(excluded.deleted_at, deleted_at))`,\n files: `INSERT INTO file (file_ref, repo, path) VALUES (@fileRef, @repo, @path) ON CONFLICT (file_ref) DO NOTHING`,\n tasks: `INSERT INTO task (task_ref, task_id) VALUES (@taskRef, @taskId) ON CONFLICT (task_ref) DO NOTHING`,\n artifacts: `INSERT INTO artifact (artifact_ref, kind, title, url, path, created_at) VALUES (@artifactRef, @artifactKind, @title, @url, @path, @ts)\n ON CONFLICT (artifact_ref) DO NOTHING`,\n prMerges: `INSERT INTO pr_merge_observation (number, repo_hint, merged_at) VALUES (@number, @repoHint, @ts)\n ON CONFLICT (number, repo_hint, merged_at) DO NOTHING`,\n prCreates: `INSERT INTO pr_create_observation (tool_use_id, title, number, repo, url) VALUES (@toolUseId, @title, @number, @repo, @url)\n ON CONFLICT (tool_use_id) DO UPDATE SET title = COALESCE(title, excluded.title), number = COALESCE(number, excluded.number),\n repo = COALESCE(repo, excluded.repo), url = COALESCE(url, excluded.url)`,\n} as const;\n\nfunction applyAssets(db: Db, delta: TranscriptDelta): void {\n for (const [kind, sql] of Object.entries(ASSET_UPSERTS) as [keyof typeof ASSET_UPSERTS, string][]) {\n const statement = db.prepare(sql);\n for (const row of delta[kind] as unknown as Record<string, unknown>[]) statement.run(pick(row, sql));\n }\n}\n\n/** better-sqlite3 rejects unused named parameters, so pass only the ones the statement binds. */\nfunction pick(row: Record<string, unknown>, sql: string): Record<string, unknown> {\n const names = new Set([...sql.matchAll(/@(\\w+)/g)].map((m) => m[1]!));\n return Object.fromEntries([...names].map((name) => [name, row[name] ?? null]));\n}\n\n/**\n * Bi-temporal phase collapse: a candidate matching the session's open phase is a\n * no-op; otherwise the open phase closes at the new `t_valid` and a replacement\n * row is inserted. Done here so a chunk boundary mid-run cannot add a phase.\n */\nfunction applyPhases(db: Db, transcriptId: number, delta: TranscriptDelta): void {\n const current = db.prepare(\"SELECT phase_id, to_mode FROM permission_phase WHERE session_id = ? AND trigger = ? AND t_invalid IS NULL ORDER BY phase_id DESC LIMIT 1\");\n const close = db.prepare(\"UPDATE permission_phase SET t_invalid = ? WHERE phase_id = ?\");\n const insert = db.prepare(\"INSERT INTO permission_phase (session_id, from_mode, to_mode, trigger, t_valid, fact_id) VALUES (?, ?, ?, ?, ?, ?)\");\n for (const phase of delta.phases) {\n const open = current.get(phase.sessionId, phase.trigger) as { phase_id: number; to_mode: string } | undefined;\n if (open?.to_mode === phase.toMode) continue;\n if (open) close.run(phase.ts, open.phase_id);\n insert.run(phase.sessionId, open?.to_mode ?? null, phase.toMode, phase.trigger, phase.ts, factIdFor(db, transcriptId, phase.byteOffset));\n }\n}\n\nfunction applyLinkedRows(graph: SessionGraph, transcriptId: number, delta: TranscriptDelta): void {\n const { db } = graph;\n const factId = (byteOffset: number) => factIdFor(db, transcriptId, byteOffset);\n\n const upsertTurn = db.prepare(`\n INSERT INTO turn (prompt_id, session_id, turn_index, started_at, fact_id_start)\n VALUES (?, ?, (SELECT COALESCE(MAX(turn_index), -1) + 1 FROM turn WHERE session_id = ?), ?, ?)\n ON CONFLICT (prompt_id) DO UPDATE SET session_id = MIN(session_id, excluded.session_id)`);\n for (const t of delta.turns) upsertTurn.run(t.promptId, t.sessionId, t.sessionId, t.ts, factId(t.byteOffset));\n\n const insertEdit = db.prepare(\"INSERT INTO human_edit (session_id, file_path, ts, fact_id) VALUES (?, ?, ?, ?) ON CONFLICT (session_id, file_path, ts) DO NOTHING\");\n for (const e of delta.humanEdits) insertEdit.run(e.sessionId, e.filePath, e.ts, factId(e.byteOffset));\n\n const insertCheckpoint = db.prepare(`\n INSERT INTO file_checkpoint (session_id, file_path, backup_file_name, version, backup_time, fact_id) VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT (session_id, file_path, backup_file_name) DO NOTHING`);\n for (const c of delta.fileCheckpoints) insertCheckpoint.run(c.sessionId, c.filePath, c.backupFileName, c.version, c.backupTime, factId(c.byteOffset));\n\n const upsertSubagent = db.prepare(`\n INSERT INTO subagent (agent_ref, session_id, agent_type, label, started_at, fact_id) VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT (agent_ref) DO UPDATE SET session_id = COALESCE(session_id, excluded.session_id),\n agent_type = COALESCE(agent_type, excluded.agent_type), label = COALESCE(label, excluded.label), started_at = COALESCE(started_at, excluded.started_at)`);\n for (const s of delta.subagents) upsertSubagent.run(s.agentRef, s.sessionId, s.agentType, s.label, s.ts || null, factId(s.byteOffset));\n\n // The dispatch row and this bridge come from different lines; an upsert survives a chunk boundary between them.\n const linkTranscript = db.prepare(\"INSERT INTO subagent (agent_ref, child_session_id) VALUES (?, ?) ON CONFLICT (agent_ref) DO UPDATE SET child_session_id = excluded.child_session_id\");\n for (const l of delta.subagentTranscripts) linkTranscript.run(l.agentRef, l.childSessionId);\n\n for (const e of delta.edges) graph.edges.assert({ sourceRef: e.sourceRef, relation: e.relation, targetRef: e.targetRef, tValid: e.ts, factId: factId(e.byteOffset) });\n\n for (const span of delta.spans) {\n graph.spans.index({ ownerRef: sessionRef(span.sessionId), field: span.field, sourceId: transcriptId, byteOffset: span.byteOffset, byteLength: span.byteLength }, span.text);\n }\n}\n\nfunction factIdFor(db: Db, transcriptId: number, byteOffset: number): number | null {\n const row = db.prepare(\"SELECT fact_id FROM fact WHERE transcript_id = ? AND byte_offset = ?\").get(transcriptId, byteOffset) as { fact_id: number } | undefined;\n return row?.fact_id ?? null;\n}\n","import type { SessionGraph } from \"./graph.js\";\n\nconst BATCH = 400;\n\n/**\n * Turn aggregates that no single line can supply. Contract: recompute, never\n * accumulate. Every value is a pure function of the fact rows, so incremental\n * refreshes and full rebuilds converge regardless of chunk boundaries.\n */\nconst ROLLUP = `\n WITH ordered AS (\n SELECT fact_id, session_id, event_type, ts, prompt_id,\n COUNT(CASE WHEN event_type = 'user_prompt' THEN 1 END) OVER (\n PARTITION BY session_id ORDER BY ts, transcript_id, seq ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS turn_no,\n LAG(event_type) OVER (PARTITION BY session_id ORDER BY ts, transcript_id, seq) AS prev_type,\n LAG(ts) OVER (PARTITION BY session_id ORDER BY ts, transcript_id, seq) AS prev_ts\n FROM fact WHERE session_id IN (SELECT value FROM json_each(@sessionIds))\n ),\n owners AS (SELECT session_id, turn_no, prompt_id FROM ordered WHERE event_type = 'user_prompt'),\n agg AS (\n SELECT owners.prompt_id AS prompt_id, MAX(ordered.ts) AS ended_at,\n CAST(ROUND((julianday(MAX(ordered.ts)) - julianday(MIN(ordered.ts))) * 86400000) AS INTEGER) AS duration_ms,\n SUM(CASE WHEN ordered.event_type = 'tool_decision' THEN 1 ELSE 0 END) AS tool_call_count,\n CAST(COALESCE(SUM(CASE\n WHEN ordered.event_type IN ('assistant_response', 'tool_decision') AND ordered.prev_type IN ('user_prompt', 'tool_result', 'tool_result_error')\n THEN ROUND((julianday(ordered.ts) - julianday(ordered.prev_ts)) * 86400000) END), 0) AS INTEGER) AS thinking_ms\n FROM ordered JOIN owners ON owners.session_id = ordered.session_id AND owners.turn_no = ordered.turn_no\n GROUP BY owners.prompt_id\n )\n UPDATE turn SET ended_at = agg.ended_at, duration_ms = COALESCE(agg.duration_ms, 0),\n tool_call_count = agg.tool_call_count, thinking_ms = COALESCE(agg.thinking_ms, 0)\n FROM agg WHERE turn.prompt_id = agg.prompt_id`;\n\n/** `turn_index` is insert-order at write time; recomputing it from `started_at` makes it a property of the corpus. */\nconst RENUMBER_TURNS = `\n WITH ordered AS (\n SELECT prompt_id, ROW_NUMBER() OVER (PARTITION BY session_id ORDER BY started_at, prompt_id) - 1 AS idx\n FROM turn WHERE session_id IN (SELECT value FROM json_each(@sessionIds))\n )\n UPDATE turn SET turn_index = ordered.idx FROM ordered WHERE turn.prompt_id = ordered.prompt_id`;\n\n/** Recompute turn aggregates for the given sessions in one transaction. */\nexport function rollupSessions(graph: SessionGraph, sessionIds: readonly string[]): number {\n const unique = [...new Set(sessionIds)];\n if (unique.length === 0) return 0;\n const aggregate = graph.db.prepare(ROLLUP);\n const renumber = graph.db.prepare(RENUMBER_TURNS);\n return graph.db.transaction(() => {\n let updated = 0;\n for (let i = 0; i < unique.length; i += BATCH) {\n const batch = JSON.stringify(unique.slice(i, i + BATCH));\n renumber.run({ sessionIds: batch });\n updated += aggregate.run({ sessionIds: batch }).changes;\n }\n return updated;\n })();\n}\n\n/** A merge sighting and the `pr-link` naming the same PR routinely live in different transcripts; fold at pass end. */\nconst RECONCILE_PR_MERGES = `\n UPDATE pr SET state = 'merged',\n merged_at = (SELECT MIN(o.merged_at) FROM pr_merge_observation o WHERE o.number = pr.number AND (o.repo_hint IS NULL OR pr.repo LIKE '%/' || o.repo_hint))\n WHERE EXISTS (SELECT 1 FROM pr_merge_observation o WHERE o.number = pr.number AND (o.repo_hint IS NULL OR pr.repo LIKE '%/' || o.repo_hint))`;\n\n/** Both halves of a `gh pr create` make a real PR whether or not a `pr-link` ever mentioned it. */\nconst RECONCILE_PR_CREATES = `\n INSERT INTO pr (pr_ref, number, repo, title, url)\n SELECT 'pr:' || repo || '#' || number, number, repo, title, url FROM pr_create_observation\n WHERE number IS NOT NULL AND repo IS NOT NULL AND title IS NOT NULL\n ON CONFLICT (pr_ref) DO UPDATE SET title = COALESCE(pr.title, excluded.title), url = COALESCE(pr.url, excluded.url)`;\n\n/**\n * A subagent ends when its child session's last line does, not when the\n * dispatch's tool_result returns (most dispatches are async and return at\n * launch). Parentage comes from the dispatch that ran inside a child transcript.\n */\nconst RECONCILE_SUBAGENTS = `\n UPDATE subagent SET\n ended_at = COALESCE((SELECT s.ended_at FROM session s WHERE s.session_id = subagent.child_session_id), ended_at),\n parent_agent_ref = COALESCE((SELECT p.agent_ref FROM subagent p WHERE p.child_session_id = subagent.session_id), parent_agent_ref)`;\n\nexport interface ReconcileCounts {\n prMerges: number;\n prCreates: number;\n subagents: number;\n}\n\n/** Whole-table reconciliations, run once at the end of a refresh pass after the rollup. */\nexport function reconcile(graph: SessionGraph): ReconcileCounts {\n return {\n prCreates: graph.db.prepare(RECONCILE_PR_CREATES).run().changes,\n prMerges: graph.db.prepare(RECONCILE_PR_MERGES).run().changes,\n subagents: graph.db.prepare(RECONCILE_SUBAGENTS).run().changes,\n };\n}\n","import { promises as fs } from \"node:fs\";\nimport { contentHash, resumePoint } from \"@titan-design/locator\";\nimport { TranscriptParseError, extractTranscript, type DiscoveredTranscript } from \"@titan-design/session-read\";\nimport { applyDelta } from \"./apply.js\";\nimport { allSessionIds, type SessionGraph } from \"./graph.js\";\nimport { reconcile, rollupSessions, type ReconcileCounts } from \"./rollup.js\";\n\nexport interface IndexOptions {\n /** Re-hash the already-read prefix to catch same-length rewrites. O(file), so opt in. */\n verifyHash?: boolean;\n /** Store a whole-file content hash for durability reporting. O(file), so opt in. */\n withContentHash?: boolean;\n}\n\nexport type TranscriptOutcome =\n | { status: \"unchanged\" | \"indexed\" | \"rewound\"; transcriptId: number; sessionIds: string[]; facts: number }\n | { status: \"missing\" | \"quarantined\"; transcriptId: number; sessionIds: string[]; facts: 0; reason: string };\n\n/**\n * Bring one transcript up to date: read from its watermark (or byte 0 after a\n * rewrite), apply the delta, advance the watermark. A malformed line\n * quarantines the transcript instead of poisoning the pass.\n */\nexport async function indexTranscript(graph: SessionGraph, transcript: DiscoveredTranscript, options: IndexOptions = {}): Promise<TranscriptOutcome> {\n const row = graph.transcripts.ensure(transcript.displayPath);\n const entry = { path: transcript.displayPath, lastByteOffset: row.lastOffset, prefixHash: row.prefixHash };\n const point = await resumePoint(entry, transcript.absolutePath, { verifyHash: options.verifyHash });\n const base = { transcriptId: row.sourceId, sessionIds: [] as string[] };\n if (point.state === \"missing\") {\n graph.transcripts.markStatus(transcript.displayPath, \"missing\", \"source file no longer exists\");\n return { ...base, status: \"missing\", facts: 0, reason: \"source file no longer exists\" };\n }\n if (point.state === \"unchanged\") return { ...base, status: \"unchanged\", facts: 0 };\n\n try {\n const delta = await extractTranscript(transcript.absolutePath, {\n fromByteOffset: point.start,\n priorPrefixHash: point.state === \"rewritten\" ? null : row.prefixHash,\n subagentId: transcript.subagentId,\n });\n applyDelta(graph, row.sourceId, delta);\n const stat = await fs.stat(transcript.absolutePath);\n graph.transcripts.advance(transcript.displayPath, {\n lastOffset: delta.lastByteOffset,\n prefixHash: delta.prefixHash,\n fileSize: stat.size,\n fileMtime: stat.mtime.toISOString(),\n contentHash: options.withContentHash ? await contentHash(transcript.absolutePath) : null,\n });\n const sessionIds = delta.sessions.map((s) => s.sessionId);\n return { ...base, sessionIds, status: point.state === \"rewritten\" ? \"rewound\" : \"indexed\", facts: delta.facts.length };\n } catch (err) {\n if (!(err instanceof TranscriptParseError)) throw err;\n graph.transcripts.markStatus(transcript.displayPath, \"quarantined\", err.message);\n return { ...base, status: \"quarantined\", facts: 0, reason: err.message };\n }\n}\n\nexport interface RefreshSummary {\n transcripts: number;\n indexed: number;\n unchanged: number;\n rewound: number;\n missing: number;\n quarantined: number;\n facts: number;\n turnsRolledUp: number;\n reconciled: ReconcileCounts;\n markedMissing: number;\n}\n\n/**\n * One pass over a corpus: index every transcript, roll up the sessions that\n * changed, reconcile cross-transcript observations, and mark rows whose source\n * file is gone. Idempotent: a second pass over unchanged files changes nothing.\n */\nexport async function refreshCorpus(graph: SessionGraph, transcripts: readonly DiscoveredTranscript[], options: IndexOptions & { full?: boolean } = {}): Promise<RefreshSummary> {\n const counts = { indexed: 0, unchanged: 0, rewound: 0, missing: 0, quarantined: 0 };\n const touched: string[] = [];\n let facts = 0;\n for (const transcript of transcripts) {\n const outcome = await indexTranscript(graph, transcript, options);\n counts[outcome.status] += 1;\n facts += outcome.facts;\n touched.push(...outcome.sessionIds);\n }\n const turnsRolledUp = rollupSessions(graph, options.full ? allSessionIds(graph) : touched);\n const reconciled = reconcile(graph);\n const markedMissing = await markMissing(graph, transcripts);\n return { transcripts: transcripts.length, ...counts, facts, turnsRolledUp, reconciled, markedMissing };\n}\n\n/** Rows absent from discovery are only nominated; an `fs.stat` decides, so an empty scan cannot condemn the corpus. */\nasync function markMissing(graph: SessionGraph, discovered: readonly DiscoveredTranscript[]): Promise<number> {\n const present = new Set(discovered.map((t) => t.displayPath));\n const byPath = new Map(discovered.map((t) => [t.displayPath, t.absolutePath]));\n let marked = 0;\n for (const row of graph.transcripts.list()) {\n if (row.status === \"missing\" || present.has(row.sourceKey)) continue;\n const absolute = byPath.get(row.sourceKey) ?? row.sourceKey;\n if (await exists(absolute)) continue;\n graph.transcripts.markStatus(row.sourceKey, \"missing\", \"source file no longer exists\");\n marked += 1;\n }\n return marked;\n}\n\nasync function exists(absolutePath: string): Promise<boolean> {\n try {\n await fs.stat(absolutePath);\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@titan-design/session-graph",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Fold session events into the activity graph on store-sqlite",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"author": "Henry Jewkes",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"homepage": "https://github.com/HJewkes/titan-platform#readme",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/HJewkes/titan-platform.git",
|
|
24
|
+
"directory": "packages/session-graph"
|
|
25
|
+
},
|
|
26
|
+
"bugs": "https://github.com/HJewkes/titan-platform/issues",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@titan-design/store-sqlite": "^0.1.0",
|
|
35
|
+
"@titan-design/cluster": "^0.1.0",
|
|
36
|
+
"@titan-design/session-read": "^0.1.0",
|
|
37
|
+
"@titan-design/locator": "^0.1.0"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup",
|
|
41
|
+
"typecheck": "tsc --noEmit",
|
|
42
|
+
"lint": "eslint src"
|
|
43
|
+
}
|
|
44
|
+
}
|