@titan-design/session-graph 0.12.1 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -3
- package/dist/index.d.ts +19 -6
- package/dist/index.js +138 -70
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -9,10 +9,12 @@ Tier 2 of the titan-platform DAG. Depends on `session-read`, `store-sqlite`,
|
|
|
9
9
|
`cluster`, `locator`, and `agent-protocol`. Extracted from active-work's session index (AW-23, TP-6).
|
|
10
10
|
|
|
11
11
|
```ts
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
12
14
|
import { discoverTranscripts } from "@titan-design/session-read";
|
|
13
15
|
import { openSessionGraph, refreshCorpus } from "@titan-design/session-graph";
|
|
14
16
|
|
|
15
|
-
const graph = openSessionGraph("
|
|
17
|
+
const graph = openSessionGraph(path.join(os.homedir(), ".local/state/miner/index.sqlite3"));
|
|
16
18
|
const summary = await refreshCorpus(graph, await discoverTranscripts());
|
|
17
19
|
// summary.indexed, summary.unchanged, summary.rewound, summary.quarantined, summary.missing,
|
|
18
20
|
// summary.facetsBackfilled, summary.facetBacklog
|
|
@@ -174,8 +176,14 @@ with its transcript. `pr_ref` stays null until a later pass resolves it.
|
|
|
174
176
|
`resetIndex` drops `pr` rows and forge review rows with everything else, so `commit_times`,
|
|
175
177
|
`review_rounds_gh` and the forge reviews return only when a resolver answers again.
|
|
176
178
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
+
Not everything can be rebuilt from transcripts. Original sources can be pruned, so schema
|
|
180
|
+
migrations must preserve `fact` and `session` rows rather than assuming a replay is possible.
|
|
181
|
+
`resetIndex` clears the tables in `DERIVED_TABLES`, including `fact` and `session`. That is a
|
|
182
|
+
deliberate reset, not a migration step. It keeps the `transcript` watermark table (rewound, not
|
|
183
|
+
deleted), `price` and `session_state`, which nothing derives from transcripts, and `conversation`
|
|
184
|
+
and `conversation_alias`, which are derived from `session` rows or sources but are not in
|
|
185
|
+
`DERIVED_TABLES`. `session_origin` and `session_external_event` are not derived from transcripts
|
|
186
|
+
either: `resetIndex` clears them and the next pass refills them from their own sources.
|
|
179
187
|
|
|
180
188
|
|
|
181
189
|
## Injected context
|
|
@@ -277,6 +285,8 @@ empty tables, so existing rows are untouched.
|
|
|
277
285
|
- `request_dedup` collapses fan-out copies of a request to the earliest one. Every cost
|
|
278
286
|
query reads it, never `request`. `request_cost` prices each row by longest model prefix
|
|
279
287
|
and latest `effective_from`; an unmatched model reads `priced = 0` and costs 0.
|
|
288
|
+
A price row matches only when the model id equals its prefix or continues with `[..]` or
|
|
289
|
+
`-YYYYMMDD` (optionally followed by `[..]`), so `claude-opus-5` never prices `claude-opus-5-9`.
|
|
280
290
|
`context_contribution` attributes each request's context growth to the blocks before it.
|
|
281
291
|
|
|
282
292
|
Migration 6, `episode transcript ids`, adds nullable `start_transcript_id` and
|
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ interface OpenSessionGraphOptions {
|
|
|
20
20
|
* the graph must already carry every migration this package declares.
|
|
21
21
|
*/
|
|
22
22
|
readonly?: boolean;
|
|
23
|
+
/** Keep the `normalized_*` tables the Codex path reads and writes. Off by default: a graph holds none unless asked. Ignored when read-only. */
|
|
24
|
+
normalized?: boolean;
|
|
23
25
|
}
|
|
24
26
|
declare class SessionGraphNotMigratedError extends Error {
|
|
25
27
|
readonly missing: readonly string[];
|
|
@@ -46,9 +48,12 @@ declare const KIT: {
|
|
|
46
48
|
* Locators are `(transcript_id, byte_offset, byte_length)`; `fact_id` rows
|
|
47
49
|
* resolve through the facts table's unique `(transcript_id, byte_offset)`.
|
|
48
50
|
*/
|
|
49
|
-
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
|
|
50
|
-
/**
|
|
51
|
-
declare const
|
|
51
|
+
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 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";
|
|
52
|
+
/** Opt-in tables: absent from a graph opened without `normalized: true`. */
|
|
53
|
+
declare const NORMALIZED_TABLES: readonly ["normalized_span", "normalized_event", "normalized_source"];
|
|
54
|
+
/** Derived tables present in this database; the opt-in normalized ones come first so they clear first. */
|
|
55
|
+
declare function derivedTables(db: Db): string[];
|
|
56
|
+
declare const DERIVED_TABLES: readonly ["normalized_span", "normalized_event", "normalized_source", "search_span", "edge", "turn", "permission_phase", "human_edit", "file_checkpoint", "subagent", "request", "tool_call", "inbound", "context_block", "compaction", "queue_op", "session_signal", "cost_state_observation", "transcript_facet", "session_origin", "session_external_event", "episode", "session_model_usage", "session", "fact", "pr", "pr_merge_observation", "pr_create_observation", "pr_review", "branch", "file", "task"];
|
|
52
57
|
declare const MIGRATIONS: Migration[];
|
|
53
58
|
|
|
54
59
|
/** Recorded in `_migration`; store-sqlite refuses a database whose applied name differs, so never rename it. */
|
|
@@ -386,6 +391,10 @@ interface RefreshOptions extends IndexOptions {
|
|
|
386
391
|
full?: boolean;
|
|
387
392
|
/** Stale audit facets re-extracted per pass (default 40). `Infinity` clears the backlog. */
|
|
388
393
|
facetLimit?: number;
|
|
394
|
+
/** What a leading `~` in a stored source key means when checking for vanished files. Defaults to the OS home directory. */
|
|
395
|
+
homeDir?: string;
|
|
396
|
+
/** Source keys known to exist this pass even when not visited, such as a sealed transcript the caller skipped. */
|
|
397
|
+
present?: Iterable<string>;
|
|
389
398
|
}
|
|
390
399
|
type TranscriptOutcome = {
|
|
391
400
|
status: "unchanged" | "indexed" | "rewound";
|
|
@@ -471,7 +480,7 @@ declare function replaceEpisodes(graph: SessionGraph, sessionId: string, heurist
|
|
|
471
480
|
|
|
472
481
|
/** USD per million tokens for one model prefix from one date. Shaped so session-analytics' `PRICE_TABLE` passes straight through. */
|
|
473
482
|
interface PriceInput {
|
|
474
|
-
/** Matched against `request.model` by longest prefix
|
|
483
|
+
/** Matched against `request.model` in `request_cost` by longest prefix at an id boundary: the id itself, or followed by -YYYYMMDD or [..]. */
|
|
475
484
|
modelPrefix: string;
|
|
476
485
|
/** ISO date or timestamp; compared as text against `request.ts`. */
|
|
477
486
|
effectiveFrom: string;
|
|
@@ -523,7 +532,9 @@ interface IndexedSpan {
|
|
|
523
532
|
spanId?: number;
|
|
524
533
|
}
|
|
525
534
|
/** One resolver for search and error clustering; never display a raw JSON line. */
|
|
526
|
-
declare function readIndexedText(graph: SessionGraph, span: IndexedSpan
|
|
535
|
+
declare function readIndexedText(graph: SessionGraph, span: IndexedSpan, options?: {
|
|
536
|
+
homeDir?: string;
|
|
537
|
+
}): Promise<string | null>;
|
|
527
538
|
interface ConversationSummary {
|
|
528
539
|
sessionId: string;
|
|
529
540
|
harness: string;
|
|
@@ -547,7 +558,9 @@ type NormalizedUsageSummary = SessionUsageSummary;
|
|
|
547
558
|
/** Shared storage-free usage policy keeps graph and direct readers consistent. */
|
|
548
559
|
declare function normalizedUsage(graph: SessionGraph, ref: string): NormalizedUsageSummary[];
|
|
549
560
|
|
|
561
|
+
/** Re-creates the opt-in tables migration 9 drops from a graph that held none of their rows. */
|
|
562
|
+
declare function ensureNormalizedSchema(db: Db): void;
|
|
550
563
|
/** Ambiguity is explicit; workspace session bodies remain in their original namespace. */
|
|
551
564
|
declare function resolveConversationAlias(db: Db, legacyRef: string): string | null;
|
|
552
565
|
|
|
553
|
-
export { AUDIT_DDL, AUDIT_FACET, AUDIT_MIGRATION_NAME, AUDIT_TABLES, type BackfillOptions, type BackfillSummary, type ConversationSummary, DEFAULT_FACET_LIMIT, DERIVED_TABLES, DOMAIN_DDL, type DeltaSource, EPISODE_TABLE, type EpisodeRow, type ExternalEvent, FACET_TABLE, type IndexOptions, type IndexedSpan, KIT, MIGRATIONS, NO_ENRICHMENT, NO_ORIGINS, NO_PR_OUTCOMES, NO_TASK_LINK, type NormalizedIndexResult, type NormalizedUsageSummary, ORIGIN_DDL, ORIGIN_MIGRATION_NAME, ORIGIN_TABLES, ORIGIN_TASK_LINK_MIGRATION_NAME, ORIGIN_VIEWS, type OpenSessionGraphOptions, type OriginEnrichment, type OriginResolution, type OriginResolver, type PrEnrichment, type PrKey, type PrResolution, type PrResolver, type PriceInput, REVIEW_DDL, REVIEW_TABLE, REVIEW_VERDICT_MIGRATION_NAME, type ReconcileCounts, type ReconcileResult, type RefreshOptions, type RefreshSummary, type ResolvedOrigin, type ResolvedPr, type ResolvedReview, type ResolvedTask, type ReviewProjection, type ReviewRoundOptions, type ReviewerProfilePredicate, type SessionGraph, SessionGraphNotMigratedError, type SyncPricesOptions, type TaskEnrichment, type TaskLinkSource, type TaskResolution, type TaskResolver, type TranscriptOutcome, allSessionIds, allTaskIds, applyAudit, applyDelta, backfillFacets, countRounds, enrichPrs, enrichTasks, indexCodexSource, indexTranscript, isInjectedCause, isReviewerProfile, isUntypedPrompt, normalizedSessions, normalizedUsage, openSessionGraph, projectReviewRounds, prsNeedingOutcome, purgeTranscript, readIndexedText, reconcile, reconcilePrices, refreshCorpus, replaceEpisodes, resetIndex, resolveConversationAlias, resolveOrigins, rollupSessions, sessionsNeedingOrigin, stripInjected, syncPrices };
|
|
566
|
+
export { AUDIT_DDL, AUDIT_FACET, AUDIT_MIGRATION_NAME, AUDIT_TABLES, type BackfillOptions, type BackfillSummary, type ConversationSummary, DEFAULT_FACET_LIMIT, DERIVED_TABLES, DOMAIN_DDL, type DeltaSource, EPISODE_TABLE, type EpisodeRow, type ExternalEvent, FACET_TABLE, type IndexOptions, type IndexedSpan, KIT, MIGRATIONS, NORMALIZED_TABLES, NO_ENRICHMENT, NO_ORIGINS, NO_PR_OUTCOMES, NO_TASK_LINK, type NormalizedIndexResult, type NormalizedUsageSummary, ORIGIN_DDL, ORIGIN_MIGRATION_NAME, ORIGIN_TABLES, ORIGIN_TASK_LINK_MIGRATION_NAME, ORIGIN_VIEWS, type OpenSessionGraphOptions, type OriginEnrichment, type OriginResolution, type OriginResolver, type PrEnrichment, type PrKey, type PrResolution, type PrResolver, type PriceInput, REVIEW_DDL, REVIEW_TABLE, REVIEW_VERDICT_MIGRATION_NAME, type ReconcileCounts, type ReconcileResult, type RefreshOptions, type RefreshSummary, type ResolvedOrigin, type ResolvedPr, type ResolvedReview, type ResolvedTask, type ReviewProjection, type ReviewRoundOptions, type ReviewerProfilePredicate, type SessionGraph, SessionGraphNotMigratedError, type SyncPricesOptions, type TaskEnrichment, type TaskLinkSource, type TaskResolution, type TaskResolver, type TranscriptOutcome, allSessionIds, allTaskIds, applyAudit, applyDelta, backfillFacets, countRounds, derivedTables, enrichPrs, enrichTasks, ensureNormalizedSchema, indexCodexSource, indexTranscript, isInjectedCause, isReviewerProfile, isUntypedPrompt, normalizedSessions, normalizedUsage, openSessionGraph, projectReviewRounds, prsNeedingOutcome, purgeTranscript, readIndexedText, reconcile, reconcilePrices, refreshCorpus, replaceEpisodes, resetIndex, resolveConversationAlias, resolveOrigins, rollupSessions, sessionsNeedingOrigin, stripInjected, syncPrices };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
// src/graph.ts
|
|
2
|
-
import { EdgeTable, MIGRATION_TABLE_NAME, SpanFtsTables, WatermarkTable, hasTable, openDatabase, runMigrations } from "@titan-design/store-sqlite";
|
|
2
|
+
import { EdgeTable, MIGRATION_TABLE_NAME, SpanFtsTables, WatermarkTable, hasTable as hasTable2, openDatabase, runMigrations } from "@titan-design/store-sqlite";
|
|
3
|
+
|
|
4
|
+
// src/normalized-schema.ts
|
|
5
|
+
import { conversationRef } from "@titan-design/agent-protocol";
|
|
6
|
+
var NORMALIZED_DDL = `
|
|
7
|
+
CREATE TABLE IF NOT EXISTS conversation (
|
|
8
|
+
ref TEXT PRIMARY KEY, harness TEXT NOT NULL, namespace TEXT NOT NULL,
|
|
9
|
+
native_id TEXT NOT NULL, legacy_session_id TEXT,
|
|
10
|
+
UNIQUE(harness, namespace, native_id)
|
|
11
|
+
);
|
|
12
|
+
CREATE TABLE IF NOT EXISTS conversation_alias (
|
|
13
|
+
legacy_ref TEXT NOT NULL, conversation_ref TEXT NOT NULL,
|
|
14
|
+
PRIMARY KEY(legacy_ref, conversation_ref)
|
|
15
|
+
);
|
|
16
|
+
CREATE TABLE IF NOT EXISTS normalized_source (
|
|
17
|
+
transcript_id INTEGER PRIMARY KEY, source_id TEXT NOT NULL UNIQUE,
|
|
18
|
+
conversation_ref TEXT NOT NULL, descriptor TEXT NOT NULL
|
|
19
|
+
);
|
|
20
|
+
CREATE TABLE IF NOT EXISTS normalized_event (
|
|
21
|
+
transcript_id INTEGER NOT NULL, byte_offset INTEGER NOT NULL,
|
|
22
|
+
subrecord_index INTEGER NOT NULL, byte_length INTEGER NOT NULL,
|
|
23
|
+
conversation_ref TEXT NOT NULL, kind TEXT NOT NULL, ts TEXT,
|
|
24
|
+
turn_ref TEXT, call_ref TEXT, item_ref TEXT, phase TEXT, is_error INTEGER,
|
|
25
|
+
usage TEXT, metadata TEXT, history_origin TEXT, related_ref TEXT, relationship TEXT,
|
|
26
|
+
PRIMARY KEY(transcript_id, byte_offset, subrecord_index)
|
|
27
|
+
);
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_normalized_event_conversation ON normalized_event(conversation_ref, ts);
|
|
29
|
+
CREATE TABLE IF NOT EXISTS normalized_span (
|
|
30
|
+
span_id INTEGER PRIMARY KEY, locators TEXT NOT NULL
|
|
31
|
+
);
|
|
32
|
+
`;
|
|
33
|
+
function ensureNormalizedSchema(db) {
|
|
34
|
+
db.exec(NORMALIZED_DDL);
|
|
35
|
+
}
|
|
36
|
+
function backfillClaudeAliases(db, sessionIds) {
|
|
37
|
+
const insert = db.prepare("INSERT OR IGNORE INTO conversation(ref,harness,namespace,native_id,legacy_session_id) VALUES (?,?,?,?,?)");
|
|
38
|
+
const alias = db.prepare("INSERT OR IGNORE INTO conversation_alias VALUES (?,?)");
|
|
39
|
+
const rows = sessionIds?.map((session_id) => ({ session_id })) ?? db.prepare("SELECT session_id FROM session").all();
|
|
40
|
+
for (const row of rows) {
|
|
41
|
+
const ref = conversationRef({ harness: "claude-code", namespace: "legacy", nativeId: row.session_id });
|
|
42
|
+
insert.run(ref, "claude-code", "legacy", row.session_id, row.session_id);
|
|
43
|
+
alias.run(`session:${row.session_id}`, ref);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function resolveConversationAlias(db, legacyRef) {
|
|
47
|
+
const rows = db.prepare("SELECT conversation_ref FROM conversation_alias WHERE legacy_ref = ?").all(legacyRef);
|
|
48
|
+
if (rows.length > 1) throw new Error(`ambiguous conversation alias: ${legacyRef}`);
|
|
49
|
+
return rows[0]?.conversation_ref ?? null;
|
|
50
|
+
}
|
|
3
51
|
|
|
4
52
|
// src/audit-schema.ts
|
|
5
53
|
var AUDIT_MIGRATION_NAME = "audit tables";
|
|
@@ -293,53 +341,48 @@ function applyReviewVerdictSchema(db) {
|
|
|
293
341
|
db.exec("UPDATE pr SET outcome_checked_at = NULL");
|
|
294
342
|
}
|
|
295
343
|
|
|
296
|
-
// src/
|
|
297
|
-
|
|
298
|
-
var
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
CREATE
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
)
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
344
|
+
// src/audit-schema-v10.ts
|
|
345
|
+
var REQUEST_COST_BOUNDARY_MIGRATION_NAME = "request_cost model-id boundary";
|
|
346
|
+
var DIGITS_8 = "[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]";
|
|
347
|
+
var REST = "substr(d.model, length(p.model) + 1)";
|
|
348
|
+
var AT_BOUNDARY = `substr(d.model, 1, length(p.model)) = p.model AND (
|
|
349
|
+
${REST} = '' OR ${REST} GLOB '[[]*'
|
|
350
|
+
OR ${REST} GLOB '-${DIGITS_8}' OR ${REST} GLOB '-${DIGITS_8}[[]*]')`;
|
|
351
|
+
var REQUEST_COST2 = `
|
|
352
|
+
CREATE VIEW request_cost AS
|
|
353
|
+
WITH candidate AS (
|
|
354
|
+
SELECT d.request_id, p.model AS price_model, p.effective_from AS price_effective_from,
|
|
355
|
+
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,
|
|
356
|
+
ROW_NUMBER() OVER (PARTITION BY d.request_id ORDER BY length(p.model) DESC, p.effective_from DESC) AS match_rank
|
|
357
|
+
FROM request_dedup d
|
|
358
|
+
JOIN price p ON ${AT_BOUNDARY} AND p.effective_from <= d.ts
|
|
359
|
+
), component AS (
|
|
360
|
+
SELECT d.*, c.price_model, c.price_effective_from, c.price_model IS NOT NULL AS priced,
|
|
361
|
+
COALESCE(d.input_tokens * c.input_usd_mtok, 0) / 1e6 AS input_cost_usd,
|
|
362
|
+
COALESCE(d.cache_read_tokens * c.cache_read_usd_mtok, 0) / 1e6 AS cache_read_cost_usd,
|
|
363
|
+
COALESCE(CASE WHEN d.cache_creation_5m + d.cache_creation_1h = 0 THEN d.cache_creation_tokens ELSE d.cache_creation_5m END
|
|
364
|
+
* c.cache_write_5m_usd_mtok, 0) / 1e6 AS cache_write_5m_cost_usd,
|
|
365
|
+
COALESCE(d.cache_creation_1h * c.cache_write_1h_usd_mtok, 0) / 1e6 AS cache_write_1h_cost_usd,
|
|
366
|
+
COALESCE(d.output_tokens * c.output_usd_mtok, 0) / 1e6 AS output_cost_usd
|
|
367
|
+
FROM request_dedup d
|
|
368
|
+
LEFT JOIN candidate c ON c.request_id = d.request_id AND c.match_rank = 1
|
|
369
|
+
)
|
|
370
|
+
SELECT *,
|
|
371
|
+
input_cost_usd + cache_read_cost_usd + cache_write_5m_cost_usd + cache_write_1h_cost_usd + output_cost_usd AS cost_usd,
|
|
372
|
+
(cache_read_tokens < 0.2 * context_tokens AND cache_creation_tokens >= 20000) AS is_cold,
|
|
373
|
+
CASE WHEN context_tokens < 50000 THEN '<50k' WHEN context_tokens < 100000 THEN '50-100k'
|
|
374
|
+
WHEN context_tokens < 200000 THEN '100-200k' ELSE '200k+' END AS context_band,
|
|
375
|
+
CASE WHEN gap_ms IS NULL THEN NULL WHEN gap_ms < 300000 THEN '<5m'
|
|
376
|
+
WHEN gap_ms < 3600000 THEN '5-60m' ELSE '>60m' END AS gap_band
|
|
377
|
+
FROM component;
|
|
324
378
|
`;
|
|
325
|
-
function
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const rows = sessionIds?.map((session_id) => ({ session_id })) ?? db.prepare("SELECT session_id FROM session").all();
|
|
329
|
-
for (const row of rows) {
|
|
330
|
-
const ref = conversationRef({ harness: "claude-code", namespace: "legacy", nativeId: row.session_id });
|
|
331
|
-
insert.run(ref, "claude-code", "legacy", row.session_id, row.session_id);
|
|
332
|
-
alias.run(`session:${row.session_id}`, ref);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
function resolveConversationAlias(db, legacyRef) {
|
|
336
|
-
const rows = db.prepare("SELECT conversation_ref FROM conversation_alias WHERE legacy_ref = ?").all(legacyRef);
|
|
337
|
-
if (rows.length > 1) throw new Error(`ambiguous conversation alias: ${legacyRef}`);
|
|
338
|
-
return rows[0]?.conversation_ref ?? null;
|
|
379
|
+
function applyRequestCostBoundary(db) {
|
|
380
|
+
db.exec("DROP VIEW IF EXISTS request_cost");
|
|
381
|
+
db.exec(REQUEST_COST2);
|
|
339
382
|
}
|
|
340
383
|
|
|
341
384
|
// src/schema.ts
|
|
342
|
-
import { SQL_NOW, kitMigration } from "@titan-design/store-sqlite";
|
|
385
|
+
import { SQL_NOW, hasTable, kitMigration } from "@titan-design/store-sqlite";
|
|
343
386
|
var KIT = { watermark: "transcript", edge: "edge", spanFts: "search" };
|
|
344
387
|
var DOMAIN_DDL = `
|
|
345
388
|
CREATE TABLE IF NOT EXISTS fact (
|
|
@@ -458,9 +501,6 @@ var DOMAIN_DDL = `
|
|
|
458
501
|
fact_id INTEGER
|
|
459
502
|
);
|
|
460
503
|
CREATE INDEX IF NOT EXISTS idx_subagent_child ON subagent(child_session_id);
|
|
461
|
-
CREATE TABLE IF NOT EXISTS artifact (
|
|
462
|
-
artifact_ref TEXT PRIMARY KEY, kind TEXT, title TEXT, url TEXT, path TEXT, created_at TEXT
|
|
463
|
-
);
|
|
464
504
|
CREATE TABLE IF NOT EXISTS pr_merge_observation (
|
|
465
505
|
number INTEGER NOT NULL, repo_hint TEXT, merged_at TEXT NOT NULL,
|
|
466
506
|
PRIMARY KEY (number, repo_hint, merged_at)
|
|
@@ -469,10 +509,9 @@ var DOMAIN_DDL = `
|
|
|
469
509
|
tool_use_id TEXT PRIMARY KEY, title TEXT, number INTEGER, repo TEXT, url TEXT
|
|
470
510
|
);
|
|
471
511
|
`;
|
|
472
|
-
var
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
"normalized_source",
|
|
512
|
+
var NORMALIZED_TABLES = ["normalized_span", "normalized_event", "normalized_source"];
|
|
513
|
+
var ALWAYS_DERIVED = [
|
|
514
|
+
// Dropped too, not only FTS rows: spans of a deleted or rewritten transcript are never re-streamed and would survive.
|
|
476
515
|
`${KIT.spanFts}_span`,
|
|
477
516
|
KIT.edge,
|
|
478
517
|
"turn",
|
|
@@ -493,9 +532,19 @@ var DERIVED_TABLES = [
|
|
|
493
532
|
REVIEW_TABLE,
|
|
494
533
|
"branch",
|
|
495
534
|
"file",
|
|
496
|
-
"task"
|
|
497
|
-
"artifact"
|
|
535
|
+
"task"
|
|
498
536
|
];
|
|
537
|
+
function derivedTables(db) {
|
|
538
|
+
return [...NORMALIZED_TABLES.filter((t) => hasTable(db, t)), ...ALWAYS_DERIVED];
|
|
539
|
+
}
|
|
540
|
+
var DERIVED_TABLES = [...NORMALIZED_TABLES, ...ALWAYS_DERIVED];
|
|
541
|
+
function stopStoringBulkClasses(db) {
|
|
542
|
+
db.exec("DROP TABLE IF EXISTS artifact");
|
|
543
|
+
const present = NORMALIZED_TABLES.filter((t) => hasTable(db, t));
|
|
544
|
+
const empty = present.every((t) => db.prepare(`SELECT 1 FROM ${t} LIMIT 1`).get() === void 0);
|
|
545
|
+
if (empty) for (const t of present) db.exec(`DROP TABLE ${t}`);
|
|
546
|
+
db.exec("CREATE TABLE IF NOT EXISTS session_state (session_id TEXT NOT NULL, key TEXT NOT NULL, value TEXT, PRIMARY KEY (session_id, key))");
|
|
547
|
+
}
|
|
499
548
|
var MIGRATIONS = [
|
|
500
549
|
kitMigration(1, { watermark: KIT.watermark, edge: KIT.edge, spanFts: KIT.spanFts }, "kit tables"),
|
|
501
550
|
{ version: 2, name: "session graph tables", up: (db) => db.exec(DOMAIN_DDL) },
|
|
@@ -507,7 +556,9 @@ var MIGRATIONS = [
|
|
|
507
556
|
{ version: 5, name: ORIGIN_MIGRATION_NAME, up: applyOriginSchema },
|
|
508
557
|
{ version: 6, name: EPISODE_TRANSCRIPT_MIGRATION_NAME, up: applyEpisodeTranscriptSchema },
|
|
509
558
|
{ version: 7, name: ORIGIN_TASK_LINK_MIGRATION_NAME, up: applyOriginTaskLinkSchema },
|
|
510
|
-
{ version: 8, name: REVIEW_VERDICT_MIGRATION_NAME, up: applyReviewVerdictSchema }
|
|
559
|
+
{ version: 8, name: REVIEW_VERDICT_MIGRATION_NAME, up: applyReviewVerdictSchema },
|
|
560
|
+
{ version: 9, name: "stop storing bulk classes", up: stopStoringBulkClasses },
|
|
561
|
+
{ version: 10, name: REQUEST_COST_BOUNDARY_MIGRATION_NAME, up: applyRequestCostBoundary }
|
|
511
562
|
];
|
|
512
563
|
|
|
513
564
|
// src/graph.ts
|
|
@@ -523,6 +574,7 @@ function openSessionGraph(dbPath, options = {}) {
|
|
|
523
574
|
const db = openDatabase(dbPath, { schemaVersion: options.schemaVersion, readonly: options.readonly });
|
|
524
575
|
if (options.readonly) assertMigratedOrClose(db);
|
|
525
576
|
else runMigrations(db, MIGRATIONS);
|
|
577
|
+
if (options.normalized && !options.readonly) ensureNormalizedSchema(db);
|
|
526
578
|
return {
|
|
527
579
|
db,
|
|
528
580
|
transcripts: new WatermarkTable(db, { name: KIT.watermark }),
|
|
@@ -538,13 +590,13 @@ function assertMigratedOrClose(db) {
|
|
|
538
590
|
throw new SessionGraphNotMigratedError(missing);
|
|
539
591
|
}
|
|
540
592
|
function appliedNames(db) {
|
|
541
|
-
if (!
|
|
593
|
+
if (!hasTable2(db, MIGRATION_TABLE_NAME)) return /* @__PURE__ */ new Map();
|
|
542
594
|
const rows = db.prepare(`SELECT version, name FROM ${MIGRATION_TABLE_NAME}`).all();
|
|
543
595
|
return new Map(rows.map((r) => [r.version, r.name]));
|
|
544
596
|
}
|
|
545
597
|
function resetIndex(graph) {
|
|
546
598
|
graph.db.transaction(() => {
|
|
547
|
-
for (const table of
|
|
599
|
+
for (const table of derivedTables(graph.db)) graph.db.exec(`DELETE FROM "${table}"`);
|
|
548
600
|
graph.spans.clearIndex();
|
|
549
601
|
for (const row of graph.transcripts.list()) graph.transcripts.rewind(row.sourceKey);
|
|
550
602
|
})();
|
|
@@ -832,8 +884,6 @@ var ASSET_UPSERTS = {
|
|
|
832
884
|
files: `INSERT INTO file (file_ref, repo, path) VALUES (@fileRef, @repo, @path) ON CONFLICT (file_ref) DO NOTHING`,
|
|
833
885
|
tasks: `INSERT INTO task (task_ref, task_id, status) VALUES (@taskRef, @taskId, @status)
|
|
834
886
|
ON CONFLICT (task_ref) DO UPDATE SET status = COALESCE(excluded.status, status)`,
|
|
835
|
-
artifacts: `INSERT INTO artifact (artifact_ref, kind, title, url, path, created_at) VALUES (@artifactRef, @artifactKind, @title, @url, @path, @ts)
|
|
836
|
-
ON CONFLICT (artifact_ref) DO NOTHING`,
|
|
837
887
|
prMerges: `INSERT INTO pr_merge_observation (number, repo_hint, merged_at) VALUES (@number, @repoHint, @ts)
|
|
838
888
|
ON CONFLICT (number, repo_hint, merged_at) DO NOTHING`,
|
|
839
889
|
prCreates: `INSERT INTO pr_create_observation (tool_use_id, title, number, repo, url) VALUES (@toolUseId, @title, @number, @repo, @url)
|
|
@@ -1114,9 +1164,18 @@ function reconcile(graph) {
|
|
|
1114
1164
|
|
|
1115
1165
|
// src/refresh.ts
|
|
1116
1166
|
import { promises as fs } from "fs";
|
|
1167
|
+
import { hasTable as hasTable3 } from "@titan-design/store-sqlite";
|
|
1168
|
+
import os from "os";
|
|
1117
1169
|
import { contentHash, resumePoint } from "@titan-design/locator";
|
|
1118
1170
|
import { TranscriptParseError as TranscriptParseError2, extractTranscript as extractTranscript2 } from "@titan-design/session-read";
|
|
1119
1171
|
|
|
1172
|
+
// src/expand-home.ts
|
|
1173
|
+
import path from "path";
|
|
1174
|
+
function expandHome(file, homeDir) {
|
|
1175
|
+
if (file === "~") return homeDir;
|
|
1176
|
+
return file.startsWith("~/") ? path.join(homeDir, file.slice(2)) : file;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1120
1179
|
// src/review-rounds.ts
|
|
1121
1180
|
import { RELATIONS as RELATIONS2 } from "@titan-design/session-read";
|
|
1122
1181
|
var isReviewerProfile = (profile) => profile === "reviewer" || profile.endsWith("-reviewer");
|
|
@@ -1535,7 +1594,7 @@ async function indexTranscript(graph, transcript, options = {}) {
|
|
|
1535
1594
|
}
|
|
1536
1595
|
}
|
|
1537
1596
|
async function refreshCorpus(graph, transcripts, options = {}) {
|
|
1538
|
-
const { resolveTasks, resolveOrigins: originResolver, resolvePrs, isReviewerProfile: isReviewerProfile2, full, facetLimit, ...perTranscript } = options;
|
|
1597
|
+
const { resolveTasks, resolveOrigins: originResolver, resolvePrs, isReviewerProfile: isReviewerProfile2, full, facetLimit, homeDir, present, ...perTranscript } = options;
|
|
1539
1598
|
const counts = { indexed: 0, unchanged: 0, rewound: 0, missing: 0, quarantined: 0 };
|
|
1540
1599
|
const touched = [];
|
|
1541
1600
|
let facts = 0;
|
|
@@ -1553,19 +1612,20 @@ async function refreshCorpus(graph, transcripts, options = {}) {
|
|
|
1553
1612
|
const prs = await enrichPrs(graph, resolvePrs);
|
|
1554
1613
|
const reviews = projectReviewRounds(graph, { isReviewerProfile: isReviewerProfile2 });
|
|
1555
1614
|
const tasks = await enrichTasks(graph, resolveTasks, allTaskIds(graph));
|
|
1556
|
-
const markedMissing = await markMissing(graph, transcripts);
|
|
1615
|
+
const markedMissing = await markMissing(graph, transcripts, homeDir ?? os.homedir(), present);
|
|
1557
1616
|
const facetSummary = { facetsBackfilled: facets.backfilled, facetBacklog: facets.backlog };
|
|
1558
1617
|
return { transcripts: transcripts.length, ...counts, facts, turnsRolledUp, reconciled, tasks, origins, prs, reviews, markedMissing, ...facetSummary };
|
|
1559
1618
|
}
|
|
1560
|
-
async function markMissing(graph, discovered) {
|
|
1561
|
-
const present = new Set(discovered.map((t) => t.displayPath));
|
|
1619
|
+
async function markMissing(graph, discovered, homeDir, alsoPresent = []) {
|
|
1620
|
+
const present = /* @__PURE__ */ new Set([...discovered.map((t) => t.displayPath), ...alsoPresent]);
|
|
1621
|
+
const hasNormalized = hasTable3(graph.db, "normalized_source");
|
|
1562
1622
|
const byPath = new Map(discovered.map((t) => [t.displayPath, t.absolutePath]));
|
|
1563
1623
|
let marked = 0;
|
|
1564
1624
|
for (const row of graph.transcripts.list()) {
|
|
1565
1625
|
if (row.status === "missing" || present.has(row.sourceKey)) continue;
|
|
1566
|
-
const normalized = graph.db.prepare("SELECT descriptor FROM normalized_source WHERE transcript_id = ?").get(row.sourceId);
|
|
1626
|
+
const normalized = hasNormalized ? graph.db.prepare("SELECT descriptor FROM normalized_source WHERE transcript_id = ?").get(row.sourceId) : void 0;
|
|
1567
1627
|
const absolute = normalized ? JSON.parse(normalized.descriptor).path : byPath.get(row.sourceKey) ?? row.sourceKey;
|
|
1568
|
-
if (await exists(absolute)) continue;
|
|
1628
|
+
if (await exists(expandHome(absolute, homeDir))) continue;
|
|
1569
1629
|
graph.transcripts.markStatus(row.sourceKey, "missing", "source file no longer exists");
|
|
1570
1630
|
marked += 1;
|
|
1571
1631
|
}
|
|
@@ -1718,6 +1778,7 @@ function observationText(o) {
|
|
|
1718
1778
|
|
|
1719
1779
|
// src/normalized-index.ts
|
|
1720
1780
|
async function indexCodexSource(graph, source) {
|
|
1781
|
+
ensureNormalizedSchema(graph.db);
|
|
1721
1782
|
const ref = conversationRef3(source.conversation);
|
|
1722
1783
|
const base = { conversationRef: ref, observations: 0 };
|
|
1723
1784
|
const row = graph.transcripts.ensure(source.sourceId);
|
|
@@ -1812,12 +1873,14 @@ function* stagedRows(graph, table) {
|
|
|
1812
1873
|
// src/normalized-query.ts
|
|
1813
1874
|
import { contentHash as contentHash3, prefixHash } from "@titan-design/locator";
|
|
1814
1875
|
import { readSessionSourceText, readSessionText, SessionUsageAccumulator } from "@titan-design/session-read";
|
|
1815
|
-
|
|
1816
|
-
|
|
1876
|
+
import { hasTable as hasTable4 } from "@titan-design/store-sqlite";
|
|
1877
|
+
import os2 from "os";
|
|
1878
|
+
async function readIndexedText(graph, span, options = {}) {
|
|
1879
|
+
const text = await readSourceText(graph, span, options.homeDir ?? os2.homedir());
|
|
1817
1880
|
return text !== null && span.field === "prompt" ? stripInjected(text) : text;
|
|
1818
1881
|
}
|
|
1819
|
-
async function readSourceText(graph, span) {
|
|
1820
|
-
const row = graph.db.prepare(`SELECT n.locators FROM normalized_span n JOIN search_span s USING(span_id)
|
|
1882
|
+
async function readSourceText(graph, span, homeDir) {
|
|
1883
|
+
const row = !hasTable4(graph.db, "normalized_span") ? void 0 : graph.db.prepare(`SELECT n.locators FROM normalized_span n JOIN search_span s USING(span_id)
|
|
1821
1884
|
WHERE s.source_id = ? AND s.byte_offset = ? AND s.field = ?`).get(span.sourceId, span.byteOffset, span.field);
|
|
1822
1885
|
if (row) {
|
|
1823
1886
|
const source = graph.db.prepare("SELECT descriptor FROM normalized_source WHERE transcript_id = ?").get(span.sourceId);
|
|
@@ -1839,12 +1902,13 @@ async function readSourceText(graph, span) {
|
|
|
1839
1902
|
const texts = await Promise.all(JSON.parse(row.locators).map((locator) => readSessionSourceText(locator, { sources: [JSON.parse(source.descriptor)] })));
|
|
1840
1903
|
return texts.some((text) => text === null) ? null : texts.join("\n");
|
|
1841
1904
|
}
|
|
1842
|
-
const normalized = graph.db.prepare("SELECT 1 FROM normalized_source WHERE transcript_id = ?").get(span.sourceId);
|
|
1905
|
+
const normalized = hasTable4(graph.db, "normalized_source") && graph.db.prepare("SELECT 1 FROM normalized_source WHERE transcript_id = ?").get(span.sourceId);
|
|
1843
1906
|
if (normalized) return null;
|
|
1844
1907
|
const legacy = graph.transcripts.list().find((t) => t.sourceId === span.sourceId);
|
|
1845
|
-
return legacy ? readSessionText({ path: legacy.sourceKey, byteOffset: span.byteOffset, byteLength: span.byteLength, field: span.field }) : null;
|
|
1908
|
+
return legacy ? readSessionText({ path: expandHome(legacy.sourceKey, homeDir), byteOffset: span.byteOffset, byteLength: span.byteLength, field: span.field }) : null;
|
|
1846
1909
|
}
|
|
1847
1910
|
function normalizedSessions(graph, options = {}) {
|
|
1911
|
+
if (!hasTable4(graph.db, "normalized_event")) return [];
|
|
1848
1912
|
const rows = graph.db.prepare(`SELECT c.ref,c.harness,c.native_id,c.namespace,MIN(e.ts) AS started,
|
|
1849
1913
|
COUNT(DISTINCT CASE WHEN e.kind = 'native_turn' THEN e.turn_ref END) AS turns
|
|
1850
1914
|
FROM conversation c JOIN (SELECT DISTINCT conversation_ref FROM normalized_source) s ON s.conversation_ref = c.ref
|
|
@@ -1877,6 +1941,7 @@ function normalizedSessions(graph, options = {}) {
|
|
|
1877
1941
|
});
|
|
1878
1942
|
}
|
|
1879
1943
|
function normalizedUsage(graph, ref) {
|
|
1944
|
+
if (!hasTable4(graph.db, "normalized_event")) return [];
|
|
1880
1945
|
const preferred = graph.db.prepare(`SELECT transcript_id FROM normalized_event
|
|
1881
1946
|
WHERE conversation_ref = ? AND usage IS NOT NULL AND history_origin IS NULL
|
|
1882
1947
|
GROUP BY transcript_id ORDER BY MAX(ts) DESC, COUNT(*) DESC, transcript_id ASC LIMIT 1`).get(ref);
|
|
@@ -1902,6 +1967,7 @@ export {
|
|
|
1902
1967
|
FACET_TABLE,
|
|
1903
1968
|
KIT,
|
|
1904
1969
|
MIGRATIONS,
|
|
1970
|
+
NORMALIZED_TABLES,
|
|
1905
1971
|
NO_ENRICHMENT,
|
|
1906
1972
|
NO_ORIGINS,
|
|
1907
1973
|
NO_PR_OUTCOMES,
|
|
@@ -1921,8 +1987,10 @@ export {
|
|
|
1921
1987
|
applyDelta,
|
|
1922
1988
|
backfillFacets,
|
|
1923
1989
|
countRounds,
|
|
1990
|
+
derivedTables,
|
|
1924
1991
|
enrichPrs,
|
|
1925
1992
|
enrichTasks,
|
|
1993
|
+
ensureNormalizedSchema,
|
|
1926
1994
|
indexCodexSource,
|
|
1927
1995
|
indexTranscript,
|
|
1928
1996
|
isInjectedCause,
|