@titan-design/session-graph 0.5.0 → 0.7.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/README.md +25 -6
- package/dist/index.d.ts +146 -14
- package/dist/index.js +603 -26
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,6 +1,252 @@
|
|
|
1
1
|
// src/graph.ts
|
|
2
2
|
import { EdgeTable, SpanFtsTables, WatermarkTable, openDatabase, runMigrations } from "@titan-design/store-sqlite";
|
|
3
3
|
|
|
4
|
+
// src/audit-schema.ts
|
|
5
|
+
var AUDIT_MIGRATION_NAME = "audit tables";
|
|
6
|
+
var AUDIT_TABLES = [
|
|
7
|
+
"request",
|
|
8
|
+
"tool_call",
|
|
9
|
+
"inbound",
|
|
10
|
+
"context_block",
|
|
11
|
+
"compaction",
|
|
12
|
+
"queue_op",
|
|
13
|
+
"session_signal",
|
|
14
|
+
"cost_state_observation"
|
|
15
|
+
];
|
|
16
|
+
var FACET_TABLE = "transcript_facet";
|
|
17
|
+
var AUDIT_DDL = `
|
|
18
|
+
CREATE TABLE IF NOT EXISTS request (
|
|
19
|
+
transcript_id INTEGER NOT NULL,
|
|
20
|
+
request_id TEXT NOT NULL,
|
|
21
|
+
byte_offset INTEGER NOT NULL, -- first line seen for this request
|
|
22
|
+
session_id TEXT NOT NULL,
|
|
23
|
+
message_id TEXT,
|
|
24
|
+
ts TEXT NOT NULL,
|
|
25
|
+
model TEXT NOT NULL,
|
|
26
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
27
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
28
|
+
cache_creation_tokens INTEGER NOT NULL DEFAULT 0,
|
|
29
|
+
cache_creation_5m INTEGER NOT NULL DEFAULT 0,
|
|
30
|
+
cache_creation_1h INTEGER NOT NULL DEFAULT 0,
|
|
31
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
32
|
+
thinking_tokens INTEGER NOT NULL DEFAULT 0,
|
|
33
|
+
context_tokens INTEGER NOT NULL DEFAULT 0, -- input + cache_read + cache_creation
|
|
34
|
+
service_tier TEXT,
|
|
35
|
+
is_sidechain INTEGER NOT NULL DEFAULT 0,
|
|
36
|
+
-- rollup-owned, recomputed for touched sessions:
|
|
37
|
+
seq_in_session INTEGER,
|
|
38
|
+
gap_ms INTEGER, -- since previous request in the session
|
|
39
|
+
ctx_delta INTEGER, -- context_tokens - (prev.context_tokens + prev.output_tokens)
|
|
40
|
+
wake_cause TEXT,
|
|
41
|
+
wake_delivery TEXT,
|
|
42
|
+
wake_detail TEXT, -- tool name, channel sender, or NULL
|
|
43
|
+
wake_tool_family TEXT,
|
|
44
|
+
wake_mcp_server TEXT,
|
|
45
|
+
wake_offset INTEGER, -- byte_offset of the inbound row
|
|
46
|
+
PRIMARY KEY (transcript_id, request_id)
|
|
47
|
+
) WITHOUT ROWID;
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_request_session_ts ON request(session_id, ts);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_request_ts ON request(ts);
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_request_id ON request(request_id);
|
|
51
|
+
|
|
52
|
+
CREATE TABLE IF NOT EXISTS tool_call (
|
|
53
|
+
transcript_id INTEGER NOT NULL, byte_offset INTEGER NOT NULL, block_index INTEGER NOT NULL,
|
|
54
|
+
session_id TEXT NOT NULL, ts TEXT NOT NULL, tool_use_id TEXT NOT NULL,
|
|
55
|
+
name TEXT NOT NULL, family TEXT NOT NULL, mcp_server TEXT, input_chars INTEGER NOT NULL,
|
|
56
|
+
PRIMARY KEY (transcript_id, byte_offset, block_index)
|
|
57
|
+
) WITHOUT ROWID;
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_tool_call_use ON tool_call(tool_use_id);
|
|
59
|
+
|
|
60
|
+
CREATE TABLE IF NOT EXISTS inbound (
|
|
61
|
+
transcript_id INTEGER NOT NULL, byte_offset INTEGER NOT NULL, block_index INTEGER NOT NULL,
|
|
62
|
+
session_id TEXT NOT NULL, ts TEXT NOT NULL,
|
|
63
|
+
cause TEXT NOT NULL, delivery TEXT NOT NULL, detail TEXT,
|
|
64
|
+
origin_server TEXT, from_name TEXT, msg_id TEXT, tool_use_id TEXT,
|
|
65
|
+
is_error INTEGER NOT NULL DEFAULT 0, content_hash TEXT NOT NULL, chars INTEGER NOT NULL,
|
|
66
|
+
queued_ms INTEGER, -- rollup-owned
|
|
67
|
+
PRIMARY KEY (transcript_id, byte_offset, block_index)
|
|
68
|
+
) WITHOUT ROWID;
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_inbound_session ON inbound(session_id, byte_offset);
|
|
70
|
+
|
|
71
|
+
CREATE TABLE IF NOT EXISTS context_block (
|
|
72
|
+
transcript_id INTEGER NOT NULL, byte_offset INTEGER NOT NULL, block_index INTEGER NOT NULL,
|
|
73
|
+
session_id TEXT NOT NULL, ts TEXT NOT NULL, source TEXT NOT NULL,
|
|
74
|
+
tool_use_id TEXT, attachment_type TEXT, chars INTEGER NOT NULL, is_media INTEGER NOT NULL DEFAULT 0,
|
|
75
|
+
PRIMARY KEY (transcript_id, byte_offset, block_index)
|
|
76
|
+
) WITHOUT ROWID;
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_context_block_session ON context_block(session_id, source);
|
|
78
|
+
|
|
79
|
+
CREATE TABLE IF NOT EXISTS compaction (
|
|
80
|
+
transcript_id INTEGER NOT NULL, byte_offset INTEGER NOT NULL,
|
|
81
|
+
session_id TEXT NOT NULL, ts TEXT NOT NULL, trigger TEXT,
|
|
82
|
+
pre_tokens INTEGER, post_tokens INTEGER, dropped_tokens INTEGER, duration_ms INTEGER,
|
|
83
|
+
mid_loop INTEGER, -- rollup-owned: previous inbound was a tool_result
|
|
84
|
+
PRIMARY KEY (transcript_id, byte_offset)
|
|
85
|
+
) WITHOUT ROWID;
|
|
86
|
+
|
|
87
|
+
CREATE TABLE IF NOT EXISTS queue_op (
|
|
88
|
+
transcript_id INTEGER NOT NULL, byte_offset INTEGER NOT NULL,
|
|
89
|
+
session_id TEXT NOT NULL, ts TEXT NOT NULL, operation TEXT NOT NULL,
|
|
90
|
+
content_hash TEXT, origin_server TEXT,
|
|
91
|
+
PRIMARY KEY (transcript_id, byte_offset)
|
|
92
|
+
) WITHOUT ROWID;
|
|
93
|
+
CREATE INDEX IF NOT EXISTS idx_queue_op_hash ON queue_op(session_id, content_hash);
|
|
94
|
+
|
|
95
|
+
CREATE TABLE IF NOT EXISTS session_signal (
|
|
96
|
+
transcript_id INTEGER NOT NULL, byte_offset INTEGER NOT NULL, block_index INTEGER NOT NULL,
|
|
97
|
+
session_id TEXT NOT NULL, ts TEXT NOT NULL, signal TEXT NOT NULL, detail TEXT, tool_use_id TEXT,
|
|
98
|
+
-- one Bash block can emit up to three signals (commit, push, pr_create); the signal name disambiguates them
|
|
99
|
+
PRIMARY KEY (transcript_id, byte_offset, block_index, signal)
|
|
100
|
+
) WITHOUT ROWID;
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_signal_session ON session_signal(session_id, ts);
|
|
102
|
+
|
|
103
|
+
CREATE TABLE IF NOT EXISTS cost_state_observation (
|
|
104
|
+
transcript_id INTEGER NOT NULL, byte_offset INTEGER NOT NULL,
|
|
105
|
+
session_id TEXT NOT NULL, ts TEXT, total_cost_usd REAL NOT NULL, model_usage TEXT NOT NULL,
|
|
106
|
+
PRIMARY KEY (transcript_id, byte_offset)
|
|
107
|
+
) WITHOUT ROWID;
|
|
108
|
+
|
|
109
|
+
CREATE TABLE IF NOT EXISTS transcript_facet (
|
|
110
|
+
transcript_id INTEGER NOT NULL, facet TEXT NOT NULL,
|
|
111
|
+
version INTEGER NOT NULL, indexed_to INTEGER NOT NULL,
|
|
112
|
+
PRIMARY KEY (transcript_id, facet)
|
|
113
|
+
) WITHOUT ROWID;
|
|
114
|
+
`;
|
|
115
|
+
var AUDIT_COLUMNS = [
|
|
116
|
+
["session", "account", "TEXT"],
|
|
117
|
+
["turn", "wake_cause", "TEXT"],
|
|
118
|
+
["task", "estimate", "REAL"],
|
|
119
|
+
["pr", "review_rounds", "INTEGER"],
|
|
120
|
+
["pr", "closed_at", "TEXT"],
|
|
121
|
+
["pr", "outcome_checked_at", "TEXT"]
|
|
122
|
+
];
|
|
123
|
+
function applyAuditSchema(db) {
|
|
124
|
+
db.exec(AUDIT_DDL);
|
|
125
|
+
for (const [table, column, type] of AUDIT_COLUMNS) addColumnIfMissing(db, table, column, type);
|
|
126
|
+
}
|
|
127
|
+
function addColumnIfMissing(db, table, column, type) {
|
|
128
|
+
const present = db.prepare("SELECT 1 FROM pragma_table_info(?) WHERE name = ?").get(table, column);
|
|
129
|
+
if (!present) db.exec(`ALTER TABLE "${table}" ADD COLUMN "${column}" ${type}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/audit-schema-v5.ts
|
|
133
|
+
var ORIGIN_MIGRATION_NAME = "origin, episodes, prices";
|
|
134
|
+
var ORIGIN_TABLES = ["session_origin", "session_external_event"];
|
|
135
|
+
var EPISODE_TABLE = "episode";
|
|
136
|
+
var ORIGIN_DDL = `
|
|
137
|
+
CREATE TABLE IF NOT EXISTS session_origin ( -- not derived from transcripts
|
|
138
|
+
session_id TEXT PRIMARY KEY,
|
|
139
|
+
origin_system TEXT NOT NULL, -- 'agent-chat'
|
|
140
|
+
agent_id TEXT, agent_name TEXT, parent_name TEXT, parent_session_id TEXT,
|
|
141
|
+
profile TEXT, model_alias TEXT, surface TEXT, isolation TEXT, depth INTEGER,
|
|
142
|
+
origin_kind TEXT, -- spawned | adopted | inherited | human
|
|
143
|
+
config_dir TEXT, spawn_cwd TEXT, spawned_at TEXT,
|
|
144
|
+
brief_chars INTEGER, brief_excerpt TEXT, brief_path TEXT, launch_args TEXT,
|
|
145
|
+
resolved_at TEXT NOT NULL
|
|
146
|
+
);
|
|
147
|
+
CREATE INDEX IF NOT EXISTS idx_origin_parent ON session_origin(parent_session_id);
|
|
148
|
+
|
|
149
|
+
CREATE TABLE IF NOT EXISTS session_external_event ( -- teleport, handoff, retire, exit
|
|
150
|
+
session_id TEXT NOT NULL, ts TEXT NOT NULL, origin_system TEXT NOT NULL,
|
|
151
|
+
kind TEXT NOT NULL, detail TEXT,
|
|
152
|
+
PRIMARY KEY (session_id, ts, kind)
|
|
153
|
+
) WITHOUT ROWID;
|
|
154
|
+
|
|
155
|
+
CREATE TABLE IF NOT EXISTS episode ( -- provisional, see section 9
|
|
156
|
+
session_id TEXT NOT NULL, episode_index INTEGER NOT NULL,
|
|
157
|
+
heuristic TEXT NOT NULL, heuristic_version INTEGER NOT NULL,
|
|
158
|
+
started_at TEXT NOT NULL, ended_at TEXT NOT NULL,
|
|
159
|
+
start_offset INTEGER NOT NULL, end_offset INTEGER NOT NULL,
|
|
160
|
+
opened_by TEXT NOT NULL, -- brief | channel_followup | idle_gap | compaction
|
|
161
|
+
assignment_offset INTEGER, first_deliverable_offset INTEGER, first_deliverable_signal TEXT,
|
|
162
|
+
first_status_offset INTEGER,
|
|
163
|
+
PRIMARY KEY (session_id, heuristic, episode_index)
|
|
164
|
+
) WITHOUT ROWID;
|
|
165
|
+
|
|
166
|
+
CREATE TABLE IF NOT EXISTS price (
|
|
167
|
+
model TEXT NOT NULL, effective_from TEXT NOT NULL, table_version INTEGER NOT NULL,
|
|
168
|
+
input_usd_mtok REAL NOT NULL, cache_read_usd_mtok REAL NOT NULL,
|
|
169
|
+
cache_write_5m_usd_mtok REAL NOT NULL, cache_write_1h_usd_mtok REAL NOT NULL,
|
|
170
|
+
output_usd_mtok REAL NOT NULL, source TEXT,
|
|
171
|
+
PRIMARY KEY (model, effective_from)
|
|
172
|
+
) WITHOUT ROWID;
|
|
173
|
+
`;
|
|
174
|
+
var REQUEST_COLUMNS = `transcript_id, request_id, byte_offset, session_id, message_id, ts, model,
|
|
175
|
+
input_tokens, cache_read_tokens, cache_creation_tokens, cache_creation_5m, cache_creation_1h,
|
|
176
|
+
output_tokens, thinking_tokens, context_tokens, service_tier, is_sidechain,
|
|
177
|
+
seq_in_session, gap_ms, ctx_delta, wake_cause, wake_delivery, wake_detail,
|
|
178
|
+
wake_tool_family, wake_mcp_server, wake_offset`;
|
|
179
|
+
var REQUEST_DEDUP = `
|
|
180
|
+
CREATE VIEW IF NOT EXISTS request_dedup AS
|
|
181
|
+
SELECT ${REQUEST_COLUMNS} FROM (
|
|
182
|
+
SELECT r.*, ROW_NUMBER() OVER (PARTITION BY r.request_id ORDER BY r.ts, r.transcript_id) AS copy_rank
|
|
183
|
+
FROM request r
|
|
184
|
+
) WHERE copy_rank = 1;
|
|
185
|
+
`;
|
|
186
|
+
var REQUEST_COST = `
|
|
187
|
+
CREATE VIEW IF NOT EXISTS request_cost AS
|
|
188
|
+
WITH candidate AS (
|
|
189
|
+
SELECT d.request_id, p.model AS price_model, p.effective_from AS price_effective_from,
|
|
190
|
+
p.input_usd_mtok, p.cache_read_usd_mtok, p.cache_write_5m_usd_mtok, p.cache_write_1h_usd_mtok, p.output_usd_mtok,
|
|
191
|
+
ROW_NUMBER() OVER (PARTITION BY d.request_id ORDER BY length(p.model) DESC, p.effective_from DESC) AS match_rank
|
|
192
|
+
FROM request_dedup d
|
|
193
|
+
JOIN price p ON substr(d.model, 1, length(p.model)) = p.model AND p.effective_from <= d.ts
|
|
194
|
+
), component AS (
|
|
195
|
+
SELECT d.*, c.price_model, c.price_effective_from, c.price_model IS NOT NULL AS priced,
|
|
196
|
+
COALESCE(d.input_tokens * c.input_usd_mtok, 0) / 1e6 AS input_cost_usd,
|
|
197
|
+
COALESCE(d.cache_read_tokens * c.cache_read_usd_mtok, 0) / 1e6 AS cache_read_cost_usd,
|
|
198
|
+
COALESCE(CASE WHEN d.cache_creation_5m + d.cache_creation_1h = 0 THEN d.cache_creation_tokens ELSE d.cache_creation_5m END
|
|
199
|
+
* c.cache_write_5m_usd_mtok, 0) / 1e6 AS cache_write_5m_cost_usd,
|
|
200
|
+
COALESCE(d.cache_creation_1h * c.cache_write_1h_usd_mtok, 0) / 1e6 AS cache_write_1h_cost_usd,
|
|
201
|
+
COALESCE(d.output_tokens * c.output_usd_mtok, 0) / 1e6 AS output_cost_usd
|
|
202
|
+
FROM request_dedup d
|
|
203
|
+
LEFT JOIN candidate c ON c.request_id = d.request_id AND c.match_rank = 1
|
|
204
|
+
)
|
|
205
|
+
SELECT *,
|
|
206
|
+
input_cost_usd + cache_read_cost_usd + cache_write_5m_cost_usd + cache_write_1h_cost_usd + output_cost_usd AS cost_usd,
|
|
207
|
+
(cache_read_tokens < 0.2 * context_tokens AND cache_creation_tokens >= 20000) AS is_cold,
|
|
208
|
+
CASE WHEN context_tokens < 50000 THEN '<50k' WHEN context_tokens < 100000 THEN '50-100k'
|
|
209
|
+
WHEN context_tokens < 200000 THEN '100-200k' ELSE '200k+' END AS context_band,
|
|
210
|
+
CASE WHEN gap_ms IS NULL THEN NULL WHEN gap_ms < 300000 THEN '<5m'
|
|
211
|
+
WHEN gap_ms < 3600000 THEN '5-60m' ELSE '>60m' END AS gap_band
|
|
212
|
+
FROM component;
|
|
213
|
+
`;
|
|
214
|
+
var ASSISTANT_OUTPUT = "'assistant_text', 'assistant_thinking', 'assistant_tool_input'";
|
|
215
|
+
var CONTEXT_CONTRIBUTION = `
|
|
216
|
+
CREATE VIEW IF NOT EXISTS context_contribution AS
|
|
217
|
+
WITH stream AS (
|
|
218
|
+
SELECT transcript_id, byte_offset, 0 AS lane, block_index, NULL AS request_id FROM context_block
|
|
219
|
+
UNION ALL
|
|
220
|
+
SELECT transcript_id, byte_offset, -1 AS lane, 0, request_id FROM request
|
|
221
|
+
), ordered AS (
|
|
222
|
+
SELECT *, COUNT(request_id) OVER (PARTITION BY transcript_id ORDER BY byte_offset, lane, block_index ROWS UNBOUNDED PRECEDING) AS requests_before
|
|
223
|
+
FROM stream
|
|
224
|
+
), owner AS (
|
|
225
|
+
SELECT b.transcript_id, b.byte_offset, b.block_index, r.request_id
|
|
226
|
+
FROM ordered b JOIN ordered r ON r.transcript_id = b.transcript_id AND r.lane = -1 AND r.requests_before = b.requests_before + 1
|
|
227
|
+
WHERE b.lane = 0
|
|
228
|
+
), tool AS (
|
|
229
|
+
SELECT tool_use_id, name, family, mcp_server,
|
|
230
|
+
ROW_NUMBER() OVER (PARTITION BY tool_use_id ORDER BY transcript_id, byte_offset, block_index) AS copy_rank
|
|
231
|
+
FROM tool_call
|
|
232
|
+
)
|
|
233
|
+
SELECT cb.transcript_id, cb.byte_offset, cb.block_index, cb.session_id, cb.ts, cb.source,
|
|
234
|
+
cb.tool_use_id, cb.attachment_type, cb.chars, cb.is_media, o.request_id, d.ctx_delta,
|
|
235
|
+
CASE WHEN cb.source IN (${ASSISTANT_OUTPUT}) THEN NULL
|
|
236
|
+
ELSE d.ctx_delta * cb.chars * 1.0 / NULLIF(SUM(CASE WHEN cb.source IN (${ASSISTANT_OUTPUT}) THEN 0 ELSE cb.chars END)
|
|
237
|
+
OVER (PARTITION BY o.transcript_id, o.request_id), 0) END AS est_tokens,
|
|
238
|
+
t.name AS tool_name, t.family AS tool_family, t.mcp_server
|
|
239
|
+
FROM context_block cb
|
|
240
|
+
JOIN owner o USING (transcript_id, byte_offset, block_index)
|
|
241
|
+
JOIN request_dedup d ON d.transcript_id = o.transcript_id AND d.request_id = o.request_id
|
|
242
|
+
LEFT JOIN tool t ON t.tool_use_id = cb.tool_use_id AND t.copy_rank = 1;
|
|
243
|
+
`;
|
|
244
|
+
var ORIGIN_VIEWS = [REQUEST_DEDUP, REQUEST_COST, CONTEXT_CONTRIBUTION];
|
|
245
|
+
function applyOriginSchema(db) {
|
|
246
|
+
db.exec(ORIGIN_DDL);
|
|
247
|
+
for (const view of ORIGIN_VIEWS) db.exec(view);
|
|
248
|
+
}
|
|
249
|
+
|
|
4
250
|
// src/normalized-schema.ts
|
|
5
251
|
import { conversationRef } from "@titan-design/agent-protocol";
|
|
6
252
|
var NORMALIZED_DDL = `
|
|
@@ -188,6 +434,10 @@ var DERIVED_TABLES = [
|
|
|
188
434
|
"human_edit",
|
|
189
435
|
"file_checkpoint",
|
|
190
436
|
"subagent",
|
|
437
|
+
...AUDIT_TABLES,
|
|
438
|
+
FACET_TABLE,
|
|
439
|
+
...ORIGIN_TABLES,
|
|
440
|
+
EPISODE_TABLE,
|
|
191
441
|
"session_model_usage",
|
|
192
442
|
"session",
|
|
193
443
|
"fact",
|
|
@@ -205,7 +455,9 @@ var MIGRATIONS = [
|
|
|
205
455
|
{ version: 3, name: "normalized conversations and source evidence", up: (db) => {
|
|
206
456
|
db.exec(NORMALIZED_DDL);
|
|
207
457
|
backfillClaudeAliases(db);
|
|
208
|
-
} }
|
|
458
|
+
} },
|
|
459
|
+
{ version: 4, name: AUDIT_MIGRATION_NAME, up: applyAuditSchema },
|
|
460
|
+
{ version: 5, name: ORIGIN_MIGRATION_NAME, up: applyOriginSchema }
|
|
209
461
|
];
|
|
210
462
|
|
|
211
463
|
// src/graph.ts
|
|
@@ -230,16 +482,152 @@ function allSessionIds(graph) {
|
|
|
230
482
|
return graph.db.prepare("SELECT session_id FROM session").all().map((r) => r.session_id);
|
|
231
483
|
}
|
|
232
484
|
|
|
485
|
+
// src/audit-apply.ts
|
|
486
|
+
var UPSERT_REQUEST = `
|
|
487
|
+
INSERT INTO request (transcript_id, request_id, byte_offset, session_id, message_id, ts, model,
|
|
488
|
+
input_tokens, cache_read_tokens, cache_creation_tokens, cache_creation_5m, cache_creation_1h,
|
|
489
|
+
output_tokens, thinking_tokens, context_tokens, service_tier, is_sidechain)
|
|
490
|
+
VALUES (@transcriptId, @requestId, @byteOffset, @sessionId, @messageId, @ts, @model,
|
|
491
|
+
@inputTokens, @cacheReadTokens, @cacheCreationTokens, @cacheCreation5mTokens, @cacheCreation1hTokens,
|
|
492
|
+
@outputTokens, @thinkingTokens, @inputTokens + @cacheReadTokens + @cacheCreationTokens, @serviceTier, @isSidechain)
|
|
493
|
+
ON CONFLICT (transcript_id, request_id) DO UPDATE SET
|
|
494
|
+
session_id = CASE WHEN excluded.byte_offset < byte_offset THEN excluded.session_id ELSE session_id END,
|
|
495
|
+
message_id = CASE WHEN excluded.byte_offset < byte_offset THEN excluded.message_id ELSE message_id END,
|
|
496
|
+
model = CASE WHEN excluded.byte_offset < byte_offset THEN excluded.model ELSE model END,
|
|
497
|
+
service_tier = CASE WHEN excluded.byte_offset < byte_offset THEN excluded.service_tier ELSE service_tier END,
|
|
498
|
+
is_sidechain = CASE WHEN excluded.byte_offset < byte_offset THEN excluded.is_sidechain ELSE is_sidechain END,
|
|
499
|
+
byte_offset = MIN(byte_offset, excluded.byte_offset),
|
|
500
|
+
ts = MIN(ts, excluded.ts),
|
|
501
|
+
input_tokens = MAX(input_tokens, excluded.input_tokens),
|
|
502
|
+
cache_read_tokens = MAX(cache_read_tokens, excluded.cache_read_tokens),
|
|
503
|
+
cache_creation_tokens = MAX(cache_creation_tokens, excluded.cache_creation_tokens),
|
|
504
|
+
cache_creation_5m = MAX(cache_creation_5m, excluded.cache_creation_5m),
|
|
505
|
+
cache_creation_1h = MAX(cache_creation_1h, excluded.cache_creation_1h),
|
|
506
|
+
output_tokens = MAX(output_tokens, excluded.output_tokens),
|
|
507
|
+
thinking_tokens = MAX(thinking_tokens, excluded.thinking_tokens),
|
|
508
|
+
context_tokens = MAX(input_tokens, excluded.input_tokens) + MAX(cache_read_tokens, excluded.cache_read_tokens)
|
|
509
|
+
+ MAX(cache_creation_tokens, excluded.cache_creation_tokens)`;
|
|
510
|
+
var INSERTS = {
|
|
511
|
+
toolCalls: `INSERT INTO tool_call (transcript_id, byte_offset, block_index, session_id, ts, tool_use_id, name, family, mcp_server, input_chars)
|
|
512
|
+
VALUES (@transcriptId, @byteOffset, @blockIndex, @sessionId, @ts, @toolUseId, @name, @family, @mcpServer, @inputChars)
|
|
513
|
+
ON CONFLICT DO NOTHING`,
|
|
514
|
+
inbound: `INSERT INTO inbound (transcript_id, byte_offset, block_index, session_id, ts, cause, delivery, detail, origin_server, from_name, msg_id, tool_use_id, is_error, content_hash, chars)
|
|
515
|
+
VALUES (@transcriptId, @byteOffset, @blockIndex, @sessionId, @ts, @cause, @delivery, @detail, @originServer, @fromName, @msgId, @toolUseId, @isError, @contentHash, @chars)
|
|
516
|
+
ON CONFLICT DO NOTHING`,
|
|
517
|
+
contextBlocks: `INSERT INTO context_block (transcript_id, byte_offset, block_index, session_id, ts, source, tool_use_id, attachment_type, chars, is_media)
|
|
518
|
+
VALUES (@transcriptId, @byteOffset, @blockIndex, @sessionId, @ts, @source, @toolUseId, @attachmentType, @chars, @isMedia)
|
|
519
|
+
ON CONFLICT DO NOTHING`,
|
|
520
|
+
compactions: `INSERT INTO compaction (transcript_id, byte_offset, session_id, ts, trigger, pre_tokens, post_tokens, dropped_tokens, duration_ms)
|
|
521
|
+
VALUES (@transcriptId, @byteOffset, @sessionId, @ts, @trigger, @preTokens, @postTokens, @droppedTokens, @durationMs)
|
|
522
|
+
ON CONFLICT DO NOTHING`,
|
|
523
|
+
queueOps: `INSERT INTO queue_op (transcript_id, byte_offset, session_id, ts, operation, content_hash, origin_server)
|
|
524
|
+
VALUES (@transcriptId, @byteOffset, @sessionId, @ts, @operation, @contentHash, @originServer)
|
|
525
|
+
ON CONFLICT DO NOTHING`,
|
|
526
|
+
signals: `INSERT INTO session_signal (transcript_id, byte_offset, block_index, session_id, ts, signal, detail, tool_use_id)
|
|
527
|
+
VALUES (@transcriptId, @byteOffset, @blockIndex, @sessionId, @ts, @signal, @detail, @toolUseId)
|
|
528
|
+
ON CONFLICT DO NOTHING`,
|
|
529
|
+
costStates: `INSERT INTO cost_state_observation (transcript_id, byte_offset, session_id, ts, total_cost_usd, model_usage)
|
|
530
|
+
VALUES (@transcriptId, @byteOffset, @sessionId, @ts, @totalCostUsd, @modelUsageJson)
|
|
531
|
+
ON CONFLICT DO NOTHING`
|
|
532
|
+
};
|
|
533
|
+
function applyAudit(db, transcriptId, delta) {
|
|
534
|
+
run(db, UPSERT_REQUEST, transcriptId, delta.requests);
|
|
535
|
+
for (const [list, sql] of Object.entries(INSERTS)) {
|
|
536
|
+
run(db, sql, transcriptId, delta[list]);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
function run(db, sql, transcriptId, events) {
|
|
540
|
+
if (events.length === 0) return;
|
|
541
|
+
const statement = db.prepare(sql);
|
|
542
|
+
const names = [...new Set([...sql.matchAll(/@(\w+)/g)].map((m) => m[1]))];
|
|
543
|
+
for (const event of events) statement.run(bind(names, { ...event, transcriptId }));
|
|
544
|
+
}
|
|
545
|
+
function bind(names, row) {
|
|
546
|
+
return Object.fromEntries(names.map((name) => {
|
|
547
|
+
const value = row[name] ?? null;
|
|
548
|
+
return [name, typeof value === "boolean" ? Number(value) : value];
|
|
549
|
+
}));
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/facet.ts
|
|
553
|
+
import { EXTRACT_VERSION, TranscriptParseError, extractTranscript } from "@titan-design/session-read";
|
|
554
|
+
var AUDIT_FACET = "audit";
|
|
555
|
+
var DEFAULT_FACET_LIMIT = 40;
|
|
556
|
+
async function backfillFacets(graph, transcripts, options = {}) {
|
|
557
|
+
const version = options.version ?? EXTRACT_VERSION;
|
|
558
|
+
const stale = staleTranscripts(graph, transcripts, version);
|
|
559
|
+
const visit = stale.slice(0, options.limit ?? DEFAULT_FACET_LIMIT);
|
|
560
|
+
const sessionIds = /* @__PURE__ */ new Set();
|
|
561
|
+
let backfilled = 0;
|
|
562
|
+
for (const candidate of visit) {
|
|
563
|
+
const touched = await backfillOne(graph, candidate, version);
|
|
564
|
+
if (!touched) continue;
|
|
565
|
+
backfilled += 1;
|
|
566
|
+
for (const id of touched) sessionIds.add(id);
|
|
567
|
+
}
|
|
568
|
+
return { backfilled, backlog: stale.length - visit.length, sessionIds: [...sessionIds] };
|
|
569
|
+
}
|
|
570
|
+
function staleTranscripts(graph, transcripts, version) {
|
|
571
|
+
const byPath = new Map(transcripts.map((t) => [t.displayPath, t]));
|
|
572
|
+
const versions = facetVersions(graph.db);
|
|
573
|
+
const stale = [];
|
|
574
|
+
for (const row of graph.transcripts.list()) {
|
|
575
|
+
const transcript = byPath.get(row.sourceKey);
|
|
576
|
+
if (!transcript || row.status !== "ok" || (versions.get(row.sourceId) ?? -1) >= version) continue;
|
|
577
|
+
stale.push({ row, transcript });
|
|
578
|
+
}
|
|
579
|
+
return stale.sort((a, b) => (b.row.fileMtime ?? "").localeCompare(a.row.fileMtime ?? ""));
|
|
580
|
+
}
|
|
581
|
+
function facetVersions(db) {
|
|
582
|
+
const rows = db.prepare(`SELECT transcript_id, version FROM ${FACET_TABLE} WHERE facet = ?`).all(AUDIT_FACET);
|
|
583
|
+
return new Map(rows.map((r) => [r.transcript_id, r.version]));
|
|
584
|
+
}
|
|
585
|
+
async function backfillOne(graph, { row, transcript }, version) {
|
|
586
|
+
let delta;
|
|
587
|
+
try {
|
|
588
|
+
delta = await extractTranscript(transcript.absolutePath, { untilByteOffset: row.lastOffset, subagentId: transcript.subagentId });
|
|
589
|
+
} catch (err) {
|
|
590
|
+
const status = unreadableStatus(err);
|
|
591
|
+
if (!status) throw err;
|
|
592
|
+
graph.transcripts.markStatus(row.sourceKey, status, err instanceof Error ? err.message : String(err));
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
graph.db.transaction(() => {
|
|
596
|
+
for (const table of AUDIT_TABLES) graph.db.prepare(`DELETE FROM "${table}" WHERE transcript_id = ?`).run(row.sourceId);
|
|
597
|
+
applyAudit(graph.db, row.sourceId, delta);
|
|
598
|
+
writeFacet(graph.db, row.sourceId, version, delta.lastByteOffset);
|
|
599
|
+
})();
|
|
600
|
+
return delta.sessions.map((s) => s.sessionId);
|
|
601
|
+
}
|
|
602
|
+
function unreadableStatus(err) {
|
|
603
|
+
if (err instanceof TranscriptParseError) return "quarantined";
|
|
604
|
+
if (err?.code === "ENOENT") return "missing";
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
function writeFacet(db, transcriptId, version, indexedTo) {
|
|
608
|
+
db.prepare(`
|
|
609
|
+
INSERT INTO ${FACET_TABLE} (transcript_id, facet, version, indexed_to) VALUES (?, ?, ?, ?)
|
|
610
|
+
ON CONFLICT (transcript_id, facet) DO UPDATE SET version = excluded.version, indexed_to = excluded.indexed_to`).run(transcriptId, AUDIT_FACET, version, indexedTo);
|
|
611
|
+
}
|
|
612
|
+
function advanceFacet(db, transcriptId, fromByteOffset, indexedTo, version = EXTRACT_VERSION) {
|
|
613
|
+
if (fromByteOffset === 0) {
|
|
614
|
+
writeFacet(db, transcriptId, version, indexedTo);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
db.prepare(`UPDATE ${FACET_TABLE} SET indexed_to = ? WHERE transcript_id = ? AND facet = ? AND version = ?`).run(indexedTo, transcriptId, AUDIT_FACET, version);
|
|
618
|
+
}
|
|
619
|
+
|
|
233
620
|
// src/apply.ts
|
|
234
621
|
import { sessionRef } from "@titan-design/session-read";
|
|
235
|
-
function applyDelta(graph, transcriptId, delta) {
|
|
622
|
+
function applyDelta(graph, transcriptId, delta, source = {}) {
|
|
236
623
|
graph.db.transaction(() => {
|
|
237
624
|
applyFacts(graph.db, transcriptId, delta);
|
|
238
|
-
applySessions(graph.db, transcriptId, delta);
|
|
625
|
+
applySessions(graph.db, transcriptId, delta, source.account ?? null);
|
|
239
626
|
backfillClaudeAliases(graph.db, delta.sessions.map((s) => s.sessionId));
|
|
240
627
|
applyAssets(graph.db, delta);
|
|
241
628
|
applyPhases(graph.db, transcriptId, delta);
|
|
242
629
|
applyLinkedRows(graph, transcriptId, delta);
|
|
630
|
+
applyAudit(graph.db, transcriptId, delta);
|
|
243
631
|
})();
|
|
244
632
|
}
|
|
245
633
|
var INSERT_FACT = `
|
|
@@ -255,8 +643,8 @@ function applyFacts(db, transcriptId, delta) {
|
|
|
255
643
|
}
|
|
256
644
|
}
|
|
257
645
|
var UPSERT_SESSION = `
|
|
258
|
-
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)
|
|
259
|
-
VALUES (@sessionId, @transcriptId, @startedAt, @endedAt, @startType, @cwd, @gitBranch, @aiTitle, @seedPrompt, @cliVersion, @turnDelta, @commitDelta, @pushDelta)
|
|
646
|
+
INSERT INTO session (session_id, transcript_id, started_at, ended_at, start_type, cwd, git_branch, ai_title, seed_prompt, cli_version, account, turn_count, commit_count, push_count)
|
|
647
|
+
VALUES (@sessionId, @transcriptId, @startedAt, @endedAt, @startType, @cwd, @gitBranch, @aiTitle, @seedPrompt, @cliVersion, @account, @turnDelta, @commitDelta, @pushDelta)
|
|
260
648
|
ON CONFLICT (session_id) DO UPDATE SET
|
|
261
649
|
started_at = MIN(COALESCE(started_at, excluded.started_at), COALESCE(excluded.started_at, started_at)),
|
|
262
650
|
ended_at = MAX(COALESCE(ended_at, excluded.ended_at), COALESCE(excluded.ended_at, ended_at)),
|
|
@@ -270,22 +658,13 @@ var UPSERT_SESSION = `
|
|
|
270
658
|
ai_title = COALESCE(excluded.ai_title, ai_title),
|
|
271
659
|
seed_prompt = COALESCE(seed_prompt, excluded.seed_prompt),
|
|
272
660
|
cli_version = COALESCE(excluded.cli_version, cli_version),
|
|
661
|
+
account = COALESCE(excluded.account, account),
|
|
273
662
|
turn_count = turn_count + excluded.turn_count,
|
|
274
663
|
commit_count = commit_count + excluded.commit_count,
|
|
275
664
|
push_count = push_count + excluded.push_count`;
|
|
276
|
-
|
|
277
|
-
INSERT INTO session_model_usage (session_id, model, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, thinking_tokens, request_count)
|
|
278
|
-
VALUES (@sessionId, @model, @inputTokens, @outputTokens, @cacheReadTokens, @cacheCreationTokens, @thinkingTokens, @requestCount)
|
|
279
|
-
ON CONFLICT (session_id, model) DO UPDATE SET
|
|
280
|
-
input_tokens = input_tokens + excluded.input_tokens, output_tokens = output_tokens + excluded.output_tokens,
|
|
281
|
-
cache_read_tokens = cache_read_tokens + excluded.cache_read_tokens,
|
|
282
|
-
cache_creation_tokens = cache_creation_tokens + excluded.cache_creation_tokens,
|
|
283
|
-
thinking_tokens = thinking_tokens + excluded.thinking_tokens, request_count = request_count + excluded.request_count`;
|
|
284
|
-
function applySessions(db, transcriptId, delta) {
|
|
665
|
+
function applySessions(db, transcriptId, delta, account) {
|
|
285
666
|
const upsertSession = db.prepare(UPSERT_SESSION);
|
|
286
|
-
for (const s of delta.sessions) upsertSession.run({ ...s, transcriptId });
|
|
287
|
-
const upsertUsage = db.prepare(UPSERT_USAGE);
|
|
288
|
-
for (const u of delta.usage) upsertUsage.run(u);
|
|
667
|
+
for (const s of delta.sessions) upsertSession.run({ ...s, transcriptId, account });
|
|
289
668
|
}
|
|
290
669
|
var ASSET_UPSERTS = {
|
|
291
670
|
prs: `INSERT INTO pr (pr_ref, number, repo, title, url) VALUES (@prRef, @number, @repo, @title, @url)
|
|
@@ -359,7 +738,7 @@ function factIdFor(db, transcriptId, byteOffset) {
|
|
|
359
738
|
|
|
360
739
|
// src/purge.ts
|
|
361
740
|
import { sessionRef as sessionRef2 } from "@titan-design/session-read";
|
|
362
|
-
var SESSION_SCOPED = ["turn", "permission_phase", "human_edit", "file_checkpoint", "subagent", "session_model_usage"];
|
|
741
|
+
var SESSION_SCOPED = ["turn", "permission_phase", "human_edit", "file_checkpoint", "subagent", "session_model_usage", EPISODE_TABLE];
|
|
363
742
|
function purgeTranscript(graph, transcriptId) {
|
|
364
743
|
graph.db.transaction(() => {
|
|
365
744
|
const owned = graph.db.prepare("SELECT session_id FROM session WHERE transcript_id = ?").all(transcriptId);
|
|
@@ -368,11 +747,97 @@ function purgeTranscript(graph, transcriptId) {
|
|
|
368
747
|
graph.db.prepare(`DELETE FROM "${table}" WHERE session_id IN (SELECT session_id FROM session WHERE transcript_id = ?)`).run(transcriptId);
|
|
369
748
|
}
|
|
370
749
|
graph.db.prepare(`DELETE FROM "${KIT.edge}" WHERE fact_id IN (SELECT fact_id FROM fact WHERE transcript_id = ?)`).run(transcriptId);
|
|
750
|
+
for (const table of [...AUDIT_TABLES, FACET_TABLE]) {
|
|
751
|
+
graph.db.prepare(`DELETE FROM "${table}" WHERE transcript_id = ?`).run(transcriptId);
|
|
752
|
+
}
|
|
371
753
|
graph.db.prepare("DELETE FROM fact WHERE transcript_id = ?").run(transcriptId);
|
|
372
754
|
graph.db.prepare("DELETE FROM session WHERE transcript_id = ?").run(transcriptId);
|
|
373
755
|
})();
|
|
374
756
|
}
|
|
375
757
|
|
|
758
|
+
// src/audit-rollup.ts
|
|
759
|
+
var IN_BATCH = "SELECT value FROM json_each(@sessionIds)";
|
|
760
|
+
var REQUEST_ORDER = `
|
|
761
|
+
WITH ordered AS (
|
|
762
|
+
SELECT transcript_id, request_id, ts, context_tokens,
|
|
763
|
+
ROW_NUMBER() OVER w - 1 AS seq,
|
|
764
|
+
LAG(ts) OVER w AS prev_ts,
|
|
765
|
+
LAG(context_tokens) OVER w AS prev_context,
|
|
766
|
+
LAG(output_tokens) OVER w AS prev_output
|
|
767
|
+
FROM request WHERE session_id IN (${IN_BATCH})
|
|
768
|
+
WINDOW w AS (PARTITION BY session_id ORDER BY ts, byte_offset, transcript_id, request_id)
|
|
769
|
+
)
|
|
770
|
+
UPDATE request SET seq_in_session = ordered.seq,
|
|
771
|
+
gap_ms = CAST(ROUND((julianday(ordered.ts) - julianday(ordered.prev_ts)) * 86400000) AS INTEGER),
|
|
772
|
+
ctx_delta = ordered.context_tokens - (ordered.prev_context + ordered.prev_output)
|
|
773
|
+
FROM ordered WHERE request.transcript_id = ordered.transcript_id AND request.request_id = ordered.request_id`;
|
|
774
|
+
var REQUEST_WAKE = `
|
|
775
|
+
WITH hit AS (
|
|
776
|
+
SELECT r.transcript_id, r.request_id,
|
|
777
|
+
(SELECT MAX(i.byte_offset) FROM inbound i
|
|
778
|
+
WHERE i.transcript_id = r.transcript_id AND i.session_id = r.session_id AND i.byte_offset < r.byte_offset) AS wake_offset
|
|
779
|
+
FROM request r WHERE r.session_id IN (${IN_BATCH})
|
|
780
|
+
),
|
|
781
|
+
tools AS (
|
|
782
|
+
SELECT transcript_id, tool_use_id, name, family, mcp_server,
|
|
783
|
+
ROW_NUMBER() OVER (PARTITION BY transcript_id, tool_use_id ORDER BY byte_offset, block_index) AS pick
|
|
784
|
+
FROM tool_call WHERE session_id IN (${IN_BATCH})
|
|
785
|
+
),
|
|
786
|
+
wake AS (
|
|
787
|
+
SELECT hit.transcript_id, hit.request_id, hit.wake_offset, i.cause, i.delivery, i.detail, i.from_name,
|
|
788
|
+
t.name AS tool_name, t.family, t.mcp_server
|
|
789
|
+
FROM hit
|
|
790
|
+
LEFT JOIN inbound i ON i.transcript_id = hit.transcript_id AND i.byte_offset = hit.wake_offset
|
|
791
|
+
AND i.block_index = (SELECT MAX(block_index) FROM inbound j WHERE j.transcript_id = hit.transcript_id AND j.byte_offset = hit.wake_offset)
|
|
792
|
+
LEFT JOIN tools t ON t.transcript_id = i.transcript_id AND t.tool_use_id = i.tool_use_id AND t.pick = 1
|
|
793
|
+
)
|
|
794
|
+
UPDATE request SET
|
|
795
|
+
wake_cause = CASE WHEN wake.cause IS NULL THEN 'session_start'
|
|
796
|
+
WHEN wake.tool_name = 'AskUserQuestion' THEN 'ask_user_answer' ELSE wake.cause END,
|
|
797
|
+
wake_delivery = wake.delivery,
|
|
798
|
+
wake_detail = COALESCE(wake.tool_name, wake.from_name, wake.detail),
|
|
799
|
+
wake_tool_family = wake.family,
|
|
800
|
+
wake_mcp_server = wake.mcp_server,
|
|
801
|
+
wake_offset = wake.wake_offset
|
|
802
|
+
FROM wake WHERE request.transcript_id = wake.transcript_id AND request.request_id = wake.request_id`;
|
|
803
|
+
var INBOUND_QUEUED = `
|
|
804
|
+
UPDATE inbound SET queued_ms = (
|
|
805
|
+
SELECT CAST(ROUND((julianday(inbound.ts) - julianday(q.ts)) * 86400000) AS INTEGER)
|
|
806
|
+
FROM queue_op q
|
|
807
|
+
WHERE q.transcript_id = inbound.transcript_id AND q.session_id = inbound.session_id
|
|
808
|
+
AND q.operation = 'enqueue' AND q.content_hash = inbound.content_hash AND q.byte_offset < inbound.byte_offset
|
|
809
|
+
ORDER BY q.byte_offset DESC LIMIT 1)
|
|
810
|
+
WHERE session_id IN (${IN_BATCH})`;
|
|
811
|
+
var COMPACTION_MID_LOOP = `
|
|
812
|
+
UPDATE compaction SET mid_loop = COALESCE((
|
|
813
|
+
SELECT i.delivery = 'tool_result' FROM inbound i
|
|
814
|
+
WHERE i.transcript_id = compaction.transcript_id AND i.session_id = compaction.session_id AND i.byte_offset < compaction.byte_offset
|
|
815
|
+
ORDER BY i.byte_offset DESC, i.block_index DESC LIMIT 1), 0)
|
|
816
|
+
WHERE session_id IN (${IN_BATCH})`;
|
|
817
|
+
var TURN_WAKE = `
|
|
818
|
+
UPDATE turn SET wake_cause = (
|
|
819
|
+
SELECT i.cause FROM fact f JOIN inbound i ON i.transcript_id = f.transcript_id AND i.byte_offset = f.byte_offset
|
|
820
|
+
WHERE f.fact_id = turn.fact_id_start ORDER BY i.block_index LIMIT 1)
|
|
821
|
+
WHERE session_id IN (${IN_BATCH})`;
|
|
822
|
+
var CLEAR_USAGE = `
|
|
823
|
+
DELETE FROM session_model_usage
|
|
824
|
+
WHERE session_id IN (${IN_BATCH}) AND session_id IN (SELECT session_id FROM request WHERE session_id IN (${IN_BATCH}))`;
|
|
825
|
+
var RECOMPUTE_USAGE = `
|
|
826
|
+
WITH ranked AS (
|
|
827
|
+
SELECT *, ROW_NUMBER() OVER (PARTITION BY session_id, request_id ORDER BY ts, transcript_id) AS pick
|
|
828
|
+
FROM request WHERE session_id IN (${IN_BATCH})
|
|
829
|
+
)
|
|
830
|
+
INSERT INTO session_model_usage (session_id, model, input_tokens, output_tokens, cache_read_tokens, cache_creation_tokens, thinking_tokens, request_count)
|
|
831
|
+
SELECT session_id, model, SUM(input_tokens), SUM(output_tokens), SUM(cache_read_tokens), SUM(cache_creation_tokens), SUM(thinking_tokens), COUNT(*)
|
|
832
|
+
FROM ranked WHERE pick = 1 GROUP BY session_id, model`;
|
|
833
|
+
var STEPS = [REQUEST_ORDER, REQUEST_WAKE, INBOUND_QUEUED, COMPACTION_MID_LOOP, TURN_WAKE, CLEAR_USAGE, RECOMPUTE_USAGE];
|
|
834
|
+
function prepareAuditRollup(db) {
|
|
835
|
+
const statements = STEPS.map((sql) => db.prepare(sql));
|
|
836
|
+
return (sessionIds) => {
|
|
837
|
+
for (const statement of statements) statement.run({ sessionIds });
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
376
841
|
// src/rollup.ts
|
|
377
842
|
var BATCH = 400;
|
|
378
843
|
var ROLLUP = `
|
|
@@ -409,12 +874,14 @@ function rollupSessions(graph, sessionIds) {
|
|
|
409
874
|
if (unique.length === 0) return 0;
|
|
410
875
|
const aggregate = graph.db.prepare(ROLLUP);
|
|
411
876
|
const renumber = graph.db.prepare(RENUMBER_TURNS);
|
|
877
|
+
const audit = prepareAuditRollup(graph.db);
|
|
412
878
|
return graph.db.transaction(() => {
|
|
413
879
|
let updated = 0;
|
|
414
880
|
for (let i = 0; i < unique.length; i += BATCH) {
|
|
415
881
|
const batch = JSON.stringify(unique.slice(i, i + BATCH));
|
|
416
882
|
renumber.run({ sessionIds: batch });
|
|
417
883
|
updated += aggregate.run({ sessionIds: batch }).changes;
|
|
884
|
+
audit(batch);
|
|
418
885
|
}
|
|
419
886
|
return updated;
|
|
420
887
|
})();
|
|
@@ -443,7 +910,96 @@ function reconcile(graph) {
|
|
|
443
910
|
// src/refresh.ts
|
|
444
911
|
import { promises as fs } from "fs";
|
|
445
912
|
import { contentHash, resumePoint } from "@titan-design/locator";
|
|
446
|
-
import { TranscriptParseError, extractTranscript } from "@titan-design/session-read";
|
|
913
|
+
import { TranscriptParseError as TranscriptParseError2, extractTranscript as extractTranscript2 } from "@titan-design/session-read";
|
|
914
|
+
|
|
915
|
+
// src/origin.ts
|
|
916
|
+
import { RELATIONS, sessionRef as sessionRef3 } from "@titan-design/session-read";
|
|
917
|
+
var NO_ORIGINS = Object.freeze({ requested: 0, applied: 0, events: 0, failed: false });
|
|
918
|
+
var SESSIONS_NEEDING_ORIGIN = `
|
|
919
|
+
SELECT s.session_id FROM session s LEFT JOIN session_origin o ON o.session_id = s.session_id
|
|
920
|
+
WHERE o.session_id IS NULL OR o.resolved_at < s.ended_at
|
|
921
|
+
ORDER BY s.session_id`;
|
|
922
|
+
var UPSERT_ORIGIN = `
|
|
923
|
+
INSERT OR REPLACE INTO session_origin (session_id, origin_system, agent_id, agent_name, parent_name, parent_session_id,
|
|
924
|
+
profile, model_alias, surface, isolation, depth, origin_kind, config_dir, spawn_cwd, spawned_at,
|
|
925
|
+
brief_chars, brief_excerpt, brief_path, launch_args, resolved_at)
|
|
926
|
+
VALUES (@sessionId, @originSystem, @agentId, @agentName, @parentName, @parentSessionId,
|
|
927
|
+
@profile, @modelAlias, @surface, @isolation, @depth, @originKind, @configDir, @spawnCwd, @spawnedAt,
|
|
928
|
+
@briefChars, @briefExcerpt, @briefPath, @launchArgs, @resolvedAt)`;
|
|
929
|
+
var UPSERT_EVENT = `
|
|
930
|
+
INSERT INTO session_external_event (session_id, ts, origin_system, kind, detail)
|
|
931
|
+
VALUES (@sessionId, @ts, COALESCE(@originSystem, (SELECT origin_system FROM session_origin WHERE session_id = @sessionId), 'unknown'), @kind, @detail)
|
|
932
|
+
ON CONFLICT (session_id, ts, kind) DO UPDATE SET origin_system = excluded.origin_system, detail = excluded.detail`;
|
|
933
|
+
var PROJECT_SUBAGENTS = `
|
|
934
|
+
INSERT INTO subagent (agent_ref, session_id, child_session_id, agent_type, label, started_at)
|
|
935
|
+
SELECT 'agent:' || origin_system || ':' || agent_id, parent_session_id, session_id, profile, agent_name, spawned_at
|
|
936
|
+
FROM session_origin WHERE parent_session_id IS NOT NULL AND agent_id IS NOT NULL
|
|
937
|
+
ON CONFLICT (agent_ref) DO UPDATE SET session_id = excluded.session_id, child_session_id = excluded.child_session_id,
|
|
938
|
+
agent_type = excluded.agent_type, label = excluded.label, started_at = COALESCE(excluded.started_at, started_at)`;
|
|
939
|
+
function sessionsNeedingOrigin(graph) {
|
|
940
|
+
return graph.db.prepare(SESSIONS_NEEDING_ORIGIN).all().map((r) => r.session_id);
|
|
941
|
+
}
|
|
942
|
+
async function resolveOrigins(graph, resolver) {
|
|
943
|
+
if (!resolver) return NO_ORIGINS;
|
|
944
|
+
const sessionIds = sessionsNeedingOrigin(graph);
|
|
945
|
+
try {
|
|
946
|
+
const resolution = await resolver(sessionIds);
|
|
947
|
+
const written = writeResolution(graph, resolution, (/* @__PURE__ */ new Date()).toISOString());
|
|
948
|
+
return { requested: sessionIds.length, ...written, failed: false };
|
|
949
|
+
} catch (err) {
|
|
950
|
+
return { requested: sessionIds.length, applied: 0, events: 0, failed: true, error: err instanceof Error ? err.message : String(err) };
|
|
951
|
+
} finally {
|
|
952
|
+
projectOrigins(graph);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
function writeResolution(graph, resolution, resolvedAt) {
|
|
956
|
+
const upsertOrigin = graph.db.prepare(UPSERT_ORIGIN);
|
|
957
|
+
const upsertEvent = graph.db.prepare(UPSERT_EVENT);
|
|
958
|
+
return graph.db.transaction(() => {
|
|
959
|
+
let applied = 0;
|
|
960
|
+
for (const [sessionId, origin] of Object.entries(resolution.origins)) {
|
|
961
|
+
applied += upsertOrigin.run(originRow(sessionId, origin, resolvedAt)).changes;
|
|
962
|
+
}
|
|
963
|
+
let events = 0;
|
|
964
|
+
for (const e of resolution.externalEvents ?? []) {
|
|
965
|
+
events += upsertEvent.run({ sessionId: e.sessionId, ts: e.ts, kind: e.kind, detail: e.detail ?? null, originSystem: e.originSystem ?? null }).changes;
|
|
966
|
+
}
|
|
967
|
+
return { applied, events };
|
|
968
|
+
})();
|
|
969
|
+
}
|
|
970
|
+
function originRow(sessionId, o, resolvedAt) {
|
|
971
|
+
return {
|
|
972
|
+
sessionId,
|
|
973
|
+
originSystem: o.originSystem,
|
|
974
|
+
resolvedAt,
|
|
975
|
+
agentId: o.agentId ?? null,
|
|
976
|
+
agentName: o.agentName ?? null,
|
|
977
|
+
parentName: o.parentName ?? null,
|
|
978
|
+
parentSessionId: o.parentSessionId ?? null,
|
|
979
|
+
profile: o.profile ?? null,
|
|
980
|
+
modelAlias: o.modelAlias ?? null,
|
|
981
|
+
surface: o.surface ?? null,
|
|
982
|
+
isolation: o.isolation ?? null,
|
|
983
|
+
depth: o.depth ?? null,
|
|
984
|
+
originKind: o.originKind ?? null,
|
|
985
|
+
configDir: o.configDir ?? null,
|
|
986
|
+
spawnCwd: o.spawnCwd ?? null,
|
|
987
|
+
spawnedAt: o.spawnedAt ?? null,
|
|
988
|
+
briefChars: o.briefChars ?? null,
|
|
989
|
+
briefExcerpt: o.briefExcerpt ?? null,
|
|
990
|
+
briefPath: o.briefPath ?? null,
|
|
991
|
+
launchArgs: o.launchArgs ?? null
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
function projectOrigins(graph) {
|
|
995
|
+
const spawned = graph.db.prepare("SELECT session_id, parent_session_id, spawned_at, resolved_at FROM session_origin WHERE parent_session_id IS NOT NULL").all();
|
|
996
|
+
graph.db.transaction(() => {
|
|
997
|
+
graph.db.prepare(PROJECT_SUBAGENTS).run();
|
|
998
|
+
for (const o of spawned) {
|
|
999
|
+
graph.edges.assert({ sourceRef: sessionRef3(o.parent_session_id), relation: RELATIONS.SPAWNED, targetRef: sessionRef3(o.session_id), tValid: o.spawned_at ?? o.resolved_at });
|
|
1000
|
+
}
|
|
1001
|
+
})();
|
|
1002
|
+
}
|
|
447
1003
|
|
|
448
1004
|
// src/tasks.ts
|
|
449
1005
|
import { taskRef } from "@titan-design/session-read";
|
|
@@ -500,14 +1056,15 @@ async function indexTranscript(graph, transcript, options = {}) {
|
|
|
500
1056
|
return { ...base, status: "unchanged", facts: 0 };
|
|
501
1057
|
}
|
|
502
1058
|
try {
|
|
503
|
-
const delta = await
|
|
1059
|
+
const delta = await extractTranscript2(transcript.absolutePath, {
|
|
504
1060
|
fromByteOffset: point.start,
|
|
505
1061
|
priorPrefixHash: point.state === "rewritten" ? null : row.prefixHash,
|
|
506
1062
|
subagentId: transcript.subagentId
|
|
507
1063
|
});
|
|
508
1064
|
const rewound = point.state === "rewritten" || delta.restartedFromZero;
|
|
509
1065
|
if (rewound) purgeTranscript(graph, row.sourceId);
|
|
510
|
-
applyDelta(graph, row.sourceId, delta);
|
|
1066
|
+
applyDelta(graph, row.sourceId, delta, { account: transcript.account });
|
|
1067
|
+
advanceFacet(graph.db, row.sourceId, rewound ? 0 : point.start, delta.lastByteOffset);
|
|
511
1068
|
const stat2 = await fs.stat(transcript.absolutePath);
|
|
512
1069
|
graph.transcripts.advance(transcript.displayPath, {
|
|
513
1070
|
lastOffset: delta.lastByteOffset,
|
|
@@ -521,13 +1078,13 @@ async function indexTranscript(graph, transcript, options = {}) {
|
|
|
521
1078
|
const tasks = taskIds.length > 0 ? await enrichTasks(graph, options.resolveTasks, taskIds) : NO_ENRICHMENT;
|
|
522
1079
|
return { ...base, sessionIds, tasks, status: rewound ? "rewound" : "indexed", facts: delta.facts.length };
|
|
523
1080
|
} catch (err) {
|
|
524
|
-
if (!(err instanceof
|
|
1081
|
+
if (!(err instanceof TranscriptParseError2)) throw err;
|
|
525
1082
|
graph.transcripts.markStatus(transcript.displayPath, "quarantined", err.message);
|
|
526
1083
|
return { ...base, status: "quarantined", facts: 0, reason: err.message };
|
|
527
1084
|
}
|
|
528
1085
|
}
|
|
529
1086
|
async function refreshCorpus(graph, transcripts, options = {}) {
|
|
530
|
-
const { resolveTasks, ...perTranscript } = options;
|
|
1087
|
+
const { resolveTasks, resolveOrigins: originResolver, full, facetLimit, ...perTranscript } = options;
|
|
531
1088
|
const counts = { indexed: 0, unchanged: 0, rewound: 0, missing: 0, quarantined: 0 };
|
|
532
1089
|
const touched = [];
|
|
533
1090
|
let facts = 0;
|
|
@@ -537,11 +1094,15 @@ async function refreshCorpus(graph, transcripts, options = {}) {
|
|
|
537
1094
|
facts += outcome.facts;
|
|
538
1095
|
touched.push(...outcome.sessionIds);
|
|
539
1096
|
}
|
|
540
|
-
const
|
|
1097
|
+
const facets = await backfillFacets(graph, transcripts, { limit: facetLimit });
|
|
1098
|
+
touched.push(...facets.sessionIds);
|
|
1099
|
+
const turnsRolledUp = rollupSessions(graph, full ? allSessionIds(graph) : touched);
|
|
1100
|
+
const origins = await resolveOrigins(graph, originResolver);
|
|
541
1101
|
const reconciled = reconcile(graph);
|
|
542
1102
|
const tasks = await enrichTasks(graph, resolveTasks, allTaskIds(graph));
|
|
543
1103
|
const markedMissing = await markMissing(graph, transcripts);
|
|
544
|
-
|
|
1104
|
+
const facetSummary = { facetsBackfilled: facets.backfilled, facetBacklog: facets.backlog };
|
|
1105
|
+
return { transcripts: transcripts.length, ...counts, facts, turnsRolledUp, reconciled, tasks, origins, markedMissing, ...facetSummary };
|
|
545
1106
|
}
|
|
546
1107
|
async function markMissing(graph, discovered) {
|
|
547
1108
|
const present = new Set(discovered.map((t) => t.displayPath));
|
|
@@ -775,14 +1336,28 @@ function normalizedUsage(graph, ref) {
|
|
|
775
1336
|
return accumulator.summaries();
|
|
776
1337
|
}
|
|
777
1338
|
export {
|
|
1339
|
+
AUDIT_DDL,
|
|
1340
|
+
AUDIT_FACET,
|
|
1341
|
+
AUDIT_MIGRATION_NAME,
|
|
1342
|
+
AUDIT_TABLES,
|
|
1343
|
+
DEFAULT_FACET_LIMIT,
|
|
778
1344
|
DERIVED_TABLES,
|
|
779
1345
|
DOMAIN_DDL,
|
|
1346
|
+
EPISODE_TABLE,
|
|
1347
|
+
FACET_TABLE,
|
|
780
1348
|
KIT,
|
|
781
1349
|
MIGRATIONS,
|
|
782
1350
|
NO_ENRICHMENT,
|
|
1351
|
+
NO_ORIGINS,
|
|
1352
|
+
ORIGIN_DDL,
|
|
1353
|
+
ORIGIN_MIGRATION_NAME,
|
|
1354
|
+
ORIGIN_TABLES,
|
|
1355
|
+
ORIGIN_VIEWS,
|
|
783
1356
|
allSessionIds,
|
|
784
1357
|
allTaskIds,
|
|
1358
|
+
applyAudit,
|
|
785
1359
|
applyDelta,
|
|
1360
|
+
backfillFacets,
|
|
786
1361
|
enrichTasks,
|
|
787
1362
|
indexCodexSource,
|
|
788
1363
|
indexTranscript,
|
|
@@ -795,6 +1370,8 @@ export {
|
|
|
795
1370
|
refreshCorpus,
|
|
796
1371
|
resetIndex,
|
|
797
1372
|
resolveConversationAlias,
|
|
798
|
-
|
|
1373
|
+
resolveOrigins,
|
|
1374
|
+
rollupSessions,
|
|
1375
|
+
sessionsNeedingOrigin
|
|
799
1376
|
};
|
|
800
1377
|
//# sourceMappingURL=index.js.map
|