@titan-design/session-graph 0.9.0 → 0.10.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 +94 -11
- package/dist/index.d.ts +72 -7
- package/dist/index.js +310 -20
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@ buckets, permission phases, touched files, branches, PRs, subagents, and the edg
|
|
|
5
5
|
them, all in one SQLite file built from `@titan-design/store-sqlite` kit tables and kept
|
|
6
6
|
current incrementally.
|
|
7
7
|
|
|
8
|
-
Tier 2 of the titan-platform DAG. Depends on `session-read`, `store-sqlite`,
|
|
9
|
-
`cluster`. Extracted from active-work's session index (AW-23, TP-6).
|
|
8
|
+
Tier 2 of the titan-platform DAG. Depends on `session-read`, `store-sqlite`,
|
|
9
|
+
`cluster`, `locator`, and `agent-protocol`. Extracted from active-work's session index (AW-23, TP-6).
|
|
10
10
|
|
|
11
11
|
```ts
|
|
12
12
|
import { discoverTranscripts } from "@titan-design/session-read";
|
|
@@ -46,9 +46,10 @@ passes 1002.
|
|
|
46
46
|
complete `gh pr create` sightings into new PR rows, subagent end times and parentage
|
|
47
47
|
from child sessions.
|
|
48
48
|
6. `enrichPrs` runs if the caller passed a `resolvePrs` resolver. See below.
|
|
49
|
-
7. `
|
|
49
|
+
7. `projectReviewRounds` resolves chat verdicts and writes review rounds. See below.
|
|
50
|
+
8. `enrichTasks` runs if the caller passed a `resolveTasks` resolver, once over the whole
|
|
50
51
|
task table. See below.
|
|
51
|
-
|
|
52
|
+
9. Rows whose source file has vanished are marked `missing`. Their facts stay: surviving
|
|
52
53
|
Claude Code's own pruning is much of the point.
|
|
53
54
|
|
|
54
55
|
`resetIndex` clears every derived table and rewinds watermarks; the next refresh rebuilds
|
|
@@ -93,15 +94,61 @@ await refreshCorpus(graph, transcripts, {
|
|
|
93
94
|
- **Merged is sticky.** A resolver's `open` or `closed` never replaces a `merged` state, so a
|
|
94
95
|
stale forge cache cannot reopen a PR. States are stored lower-case.
|
|
95
96
|
- **Batching.** One call per refresh with every PR that has a repo and number and whose
|
|
96
|
-
outcome may still change: never checked,
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
97
|
+
outcome may still change: never checked, not yet merged, or merged with no commit times
|
|
98
|
+
stored. PRs never checked come first, then open and closed PRs, then merged PRs offered only
|
|
99
|
+
for their commit times, so a resolver that caps its batch reaches open PRs before the
|
|
100
|
+
backlog. The resolver only updates rows; a PR enters the graph from a transcript.
|
|
101
|
+
- **`commitTimes`** is stored as a JSON array in `commit_times`. Send an empty array for a PR
|
|
102
|
+
with no commits, so it is not offered again; omitting the field leaves the stored value.
|
|
103
|
+
- **`reviews`** replaces the PR's forge rows in `pr_review` (`APPROVED` and
|
|
104
|
+
`CHANGES_REQUESTED`, keyed `gh:<pr_ref>:<submittedAt>`); omitting it leaves them. With
|
|
105
|
+
`reviews`, the round rule below counts `review_rounds_gh` against the sent `commitTimes`, or
|
|
106
|
+
the stored ones when none are sent. With no usable commit times, `reviewRounds` is stored
|
|
107
|
+
there as the forge counted it, and omitting that too leaves the stored value.
|
|
108
|
+
|
|
109
|
+
## Review rounds
|
|
110
|
+
|
|
111
|
+
`projectReviewRounds` runs after `enrichPrs` on every pass. It first resolves each chat
|
|
112
|
+
verdict's `pr_ref`, then writes `review_rounds_chat` and `review_rounds` for every PR.
|
|
113
|
+
|
|
114
|
+
**The rule.** A review's head is the number of the PR's commits at or before it. A
|
|
115
|
+
changes-requested review counts when its head is below the commit count, which means a later
|
|
116
|
+
commit answered it. A PR's rounds are the distinct heads among its counting reviews. A second
|
|
117
|
+
changes-requested review on one head adds nothing, whether it comes from the same reviewer, a
|
|
118
|
+
second reviewer, or the other surface. An approval is stored and never adds a round.
|
|
119
|
+
|
|
120
|
+
- **Senders.** A chat verdict counts only when the sender's `session_origin.profile` passes
|
|
121
|
+
`isReviewerProfile`, a `refreshCorpus` option. The default accepts `reviewer` and any
|
|
122
|
+
profile ending in `-reviewer`.
|
|
123
|
+
- **Totals.** `review_rounds` is `review_rounds_gh` plus the chat heads no forge review
|
|
124
|
+
already sits on. A resolver that sends only a `reviewRounds` count therefore still adds to
|
|
125
|
+
the chat rounds unchanged.
|
|
126
|
+
- **Count-only resolvers overcount.** Without `reviews`, the forge reviews' heads are unknown,
|
|
127
|
+
so a chat verdict and a forge review on the same head both count. `review_rounds` is then
|
|
128
|
+
an upper bound. `review_rounds_gh` and `review_rounds_chat` are each exact, and the total
|
|
129
|
+
is exact once the resolver sends `reviews` and `commitTimes`.
|
|
130
|
+
- **Unknown commits.** While `commit_times` is null, `review_rounds_chat` stays null and
|
|
131
|
+
`review_rounds` equals `review_rounds_gh`.
|
|
132
|
+
- **Unparseable times.** A review whose time does not parse is ignored by the rule and stays
|
|
133
|
+
stored. If any of a PR's commit times does not parse, its commit times count as unknown,
|
|
134
|
+
since a dropped commit would shift every later head. `countRounds` returns null in that
|
|
135
|
+
case. `summary.reviews.invalidTimes` counts the ignored reviews plus the PRs with unusable
|
|
136
|
+
commit times.
|
|
137
|
+
|
|
138
|
+
**Resolving a chat verdict.** An exact `owner/name` must match a `pr` row, or the verdict
|
|
139
|
+
stays unresolved. Otherwise the candidates are the `pr` rows with that number, narrowed in
|
|
140
|
+
order. A repo hint keeps the rows whose repo's last path segment matches it, in any case; a
|
|
141
|
+
hint that matches none is ignored. Then the sender's family links keep the PRs `linked` from
|
|
142
|
+
the sender, its parent session, or any session that parent spawned. Last, the sender's
|
|
143
|
+
working directory repo, a bare name, is compared with each repo's last path segment. The first
|
|
144
|
+
step that leaves exactly one row resolves the verdict. A resolved `pr_ref` is kept on later
|
|
145
|
+
passes, and `summary.reviews` reports `resolved` (this pass) and `unresolved` (still null).
|
|
100
146
|
- **Failure.** Same as the task resolver: the pass completes, the rows stand, and
|
|
101
147
|
`summary.prs` carries `failed` plus `error`.
|
|
102
148
|
|
|
103
|
-
`reconcile`
|
|
104
|
-
|
|
149
|
+
`reconcile` sets `merged_at` from merge sightings only for a PR the resolver has not yet
|
|
150
|
+
checked. Once the resolver has answered for a PR, its `merged_at` is the forge's and a later
|
|
151
|
+
pass never overwrites it with a sighting time.
|
|
105
152
|
|
|
106
153
|
## Tables
|
|
107
154
|
|
|
@@ -109,7 +156,19 @@ Kit tables: `transcript` (watermark), `edge` (bi-temporal, `session:… touched
|
|
|
109
156
|
`search_span` + `search_fts` (contentless full-text over prompts, responses, tool inputs
|
|
110
157
|
and results, keyed by the session ref). Domain tables: `fact`, `session`,
|
|
111
158
|
`session_model_usage`, `turn`, `permission_phase`, `human_edit`, `file_checkpoint`, `pr`,
|
|
112
|
-
`branch`, `file`, `task`, `subagent`, `artifact`,
|
|
159
|
+
`branch`, `file`, `task`, `subagent`, `artifact`, the two PR observation tables, and
|
|
160
|
+
`pr_review`.
|
|
161
|
+
|
|
162
|
+
`pr_review` holds one row per review verdict. A chat row comes from a `chat_send` tool use
|
|
163
|
+
that session-read parsed a verdict from, keyed `chat:<tool_use_id>:<n>` where `n` is the
|
|
164
|
+
verdict's `ordinal` in that message. An event from a session-read that predates `ordinal`
|
|
165
|
+
takes the next index its tool use has not used in that `applyDelta` call; that session-read
|
|
166
|
+
emits all of one tool use's verdicts from one line, so the keys cannot collide across calls. It keeps only the parsed fields (verdict, repo, repo hint,
|
|
167
|
+
PR number, and the repo of the sender's working directory), never message text, and is purged
|
|
168
|
+
with its transcript. `pr_ref` stays null until a later pass resolves it.
|
|
169
|
+
|
|
170
|
+
`resetIndex` drops `pr` rows and forge review rows with everything else, so `commit_times`,
|
|
171
|
+
`review_rounds_gh` and the forge reviews return only when a resolver answers again.
|
|
113
172
|
|
|
114
173
|
Everything except `transcript` is derivable, which is what makes the schema safe to evolve
|
|
115
174
|
by drop-and-rederive.
|
|
@@ -169,6 +228,30 @@ empty tables, so existing rows are untouched.
|
|
|
169
228
|
and latest `effective_from`; an unmatched model reads `priced = 0` and costs 0.
|
|
170
229
|
`context_contribution` attributes each request's context growth to the blocks before it.
|
|
171
230
|
|
|
231
|
+
Migration 6, `episode transcript ids`, adds nullable `start_transcript_id` and
|
|
232
|
+
`end_transcript_id` columns to `episode`, surfaced as `EpisodeRow.startTranscriptId` and
|
|
233
|
+
`endTranscriptId`. A session resumed across two transcripts then orders by timestamp,
|
|
234
|
+
transcript id, and byte offset, because byte offsets reset with the new file.
|
|
235
|
+
|
|
236
|
+
Migration 7, `origin task link`, adds nullable `task_ids` (a JSON array, primary id first)
|
|
237
|
+
and `task_source` columns to `session_origin`, surfaced as `ResolvedOrigin.taskIds` and
|
|
238
|
+
`taskSource`. It changes no existing row or edge. A row whose `task_source` is null is
|
|
239
|
+
offered to the resolver again on every pass, so the first pass with a task-aware resolver
|
|
240
|
+
is the backfill. A resolver that sets `taskIds`, even to an empty array or null, always
|
|
241
|
+
leaves `task_source` non-null: the given source, or `none` (`NO_TASK_LINK`) when there are
|
|
242
|
+
no ids. A resolver that leaves `taskIds` undefined leaves a stored link as it was, and a
|
|
243
|
+
row without one stays on offer. A task-aware resolver therefore returns an entry for every
|
|
244
|
+
requested session it examined, with `taskIds` empty when nothing links; a session it leaves
|
|
245
|
+
out keeps a null `task_source` and is offered again on every pass. The
|
|
246
|
+
upsert updates only the columns it names, so a later column keeps its value. Each linked id
|
|
247
|
+
projects a `task` row and a `session ran task` edge with `attrs = { via: "origin", source }`
|
|
248
|
+
and a confidence of 1.0 for `name` or `name-over-brief`, 0.9 for `brief-anchor` and 0.6 for
|
|
249
|
+
`brief-paragraph`. Only rows resolved in the current pass are projected. When a
|
|
250
|
+
re-resolution drops an id, only that origin-made edge expires. A transcript's `ran` edge
|
|
251
|
+
carries no `via`, and when a transcript claims an edge the origin made first, the edge is
|
|
252
|
+
superseded without `via`, so origin expiry never removes a transcript's claim.
|
|
253
|
+
session-graph stores and projects the ids a resolver hands it; it does not compute them.
|
|
254
|
+
|
|
172
255
|
For snapshot-only usage across multiple physical sources, queries select one source
|
|
173
256
|
by latest native usage timestamp, then greatest usage-record coverage and stable
|
|
174
257
|
source ID. Source-local reset epochs cannot safely be summed across copies. This is
|
package/dist/index.d.ts
CHANGED
|
@@ -39,7 +39,7 @@ declare const KIT: {
|
|
|
39
39
|
*/
|
|
40
40
|
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";
|
|
41
41
|
/** Every derived table, in an order safe to clear. The watermark table is not derived. */
|
|
42
|
-
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", "branch", "file", "task", "artifact"];
|
|
42
|
+
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", "artifact"];
|
|
43
43
|
declare const MIGRATIONS: Migration[];
|
|
44
44
|
|
|
45
45
|
/** Recorded in `_migration`; store-sqlite refuses a database whose applied name differs, so never rename it. */
|
|
@@ -59,6 +59,18 @@ declare const EPISODE_TABLE = "episode";
|
|
|
59
59
|
declare const ORIGIN_DDL = "\n CREATE TABLE IF NOT EXISTS session_origin ( -- not derived from transcripts\n session_id TEXT PRIMARY KEY,\n origin_system TEXT NOT NULL, -- 'agent-chat'\n agent_id TEXT, agent_name TEXT, parent_name TEXT, parent_session_id TEXT,\n profile TEXT, model_alias TEXT, surface TEXT, isolation TEXT, depth INTEGER,\n origin_kind TEXT, -- spawned | adopted | inherited | human\n config_dir TEXT, spawn_cwd TEXT, spawned_at TEXT,\n brief_chars INTEGER, brief_excerpt TEXT, brief_path TEXT, launch_args TEXT,\n resolved_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS idx_origin_parent ON session_origin(parent_session_id);\n\n CREATE TABLE IF NOT EXISTS session_external_event ( -- teleport, handoff, retire, exit\n session_id TEXT NOT NULL, ts TEXT NOT NULL, origin_system TEXT NOT NULL,\n kind TEXT NOT NULL, detail TEXT,\n PRIMARY KEY (session_id, ts, kind)\n ) WITHOUT ROWID;\n\n CREATE TABLE IF NOT EXISTS episode ( -- provisional, see section 9\n session_id TEXT NOT NULL, episode_index INTEGER NOT NULL,\n heuristic TEXT NOT NULL, heuristic_version INTEGER NOT NULL,\n started_at TEXT NOT NULL, ended_at TEXT NOT NULL,\n start_offset INTEGER NOT NULL, end_offset INTEGER NOT NULL,\n opened_by TEXT NOT NULL, -- brief | channel_followup | idle_gap | compaction\n assignment_offset INTEGER, first_deliverable_offset INTEGER, first_deliverable_signal TEXT,\n first_status_offset INTEGER,\n PRIMARY KEY (session_id, heuristic, episode_index)\n ) WITHOUT ROWID;\n\n CREATE TABLE IF NOT EXISTS price (\n model TEXT NOT NULL, effective_from TEXT NOT NULL, table_version INTEGER NOT NULL,\n input_usd_mtok REAL NOT NULL, cache_read_usd_mtok REAL NOT NULL,\n cache_write_5m_usd_mtok REAL NOT NULL, cache_write_1h_usd_mtok REAL NOT NULL,\n output_usd_mtok REAL NOT NULL, source TEXT,\n PRIMARY KEY (model, effective_from)\n ) WITHOUT ROWID;\n";
|
|
60
60
|
declare const ORIGIN_VIEWS: readonly ["\n CREATE VIEW IF NOT EXISTS request_dedup AS\n SELECT transcript_id, request_id, byte_offset, session_id, message_id, ts, model,\n input_tokens, cache_read_tokens, cache_creation_tokens, cache_creation_5m, cache_creation_1h,\n output_tokens, thinking_tokens, context_tokens, service_tier, is_sidechain,\n seq_in_session, gap_ms, ctx_delta, wake_cause, wake_delivery, wake_detail,\n wake_tool_family, wake_mcp_server, wake_offset FROM (\n SELECT r.*, ROW_NUMBER() OVER (PARTITION BY r.request_id ORDER BY r.ts, r.transcript_id) AS copy_rank\n FROM request r\n ) WHERE copy_rank = 1;\n", "\n CREATE VIEW IF NOT EXISTS request_cost AS\n WITH candidate AS (\n SELECT d.request_id, p.model AS price_model, p.effective_from AS price_effective_from,\n 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,\n ROW_NUMBER() OVER (PARTITION BY d.request_id ORDER BY length(p.model) DESC, p.effective_from DESC) AS match_rank\n FROM request_dedup d\n JOIN price p ON substr(d.model, 1, length(p.model)) = p.model AND p.effective_from <= d.ts\n ), component AS (\n SELECT d.*, c.price_model, c.price_effective_from, c.price_model IS NOT NULL AS priced,\n COALESCE(d.input_tokens * c.input_usd_mtok, 0) / 1e6 AS input_cost_usd,\n COALESCE(d.cache_read_tokens * c.cache_read_usd_mtok, 0) / 1e6 AS cache_read_cost_usd,\n COALESCE(CASE WHEN d.cache_creation_5m + d.cache_creation_1h = 0 THEN d.cache_creation_tokens ELSE d.cache_creation_5m END\n * c.cache_write_5m_usd_mtok, 0) / 1e6 AS cache_write_5m_cost_usd,\n COALESCE(d.cache_creation_1h * c.cache_write_1h_usd_mtok, 0) / 1e6 AS cache_write_1h_cost_usd,\n COALESCE(d.output_tokens * c.output_usd_mtok, 0) / 1e6 AS output_cost_usd\n FROM request_dedup d\n LEFT JOIN candidate c ON c.request_id = d.request_id AND c.match_rank = 1\n )\n SELECT *,\n input_cost_usd + cache_read_cost_usd + cache_write_5m_cost_usd + cache_write_1h_cost_usd + output_cost_usd AS cost_usd,\n (cache_read_tokens < 0.2 * context_tokens AND cache_creation_tokens >= 20000) AS is_cold,\n CASE WHEN context_tokens < 50000 THEN '<50k' WHEN context_tokens < 100000 THEN '50-100k'\n WHEN context_tokens < 200000 THEN '100-200k' ELSE '200k+' END AS context_band,\n CASE WHEN gap_ms IS NULL THEN NULL WHEN gap_ms < 300000 THEN '<5m'\n WHEN gap_ms < 3600000 THEN '5-60m' ELSE '>60m' END AS gap_band\n FROM component;\n", "\n CREATE VIEW IF NOT EXISTS context_contribution AS\n WITH stream AS (\n SELECT transcript_id, byte_offset, 0 AS lane, block_index, NULL AS request_id FROM context_block\n UNION ALL\n SELECT transcript_id, byte_offset, -1 AS lane, 0, request_id FROM request\n ), ordered AS (\n SELECT *, COUNT(request_id) OVER (PARTITION BY transcript_id ORDER BY byte_offset, lane, block_index ROWS UNBOUNDED PRECEDING) AS requests_before\n FROM stream\n ), owner AS (\n SELECT b.transcript_id, b.byte_offset, b.block_index, r.request_id\n 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\n WHERE b.lane = 0\n ), tool AS (\n SELECT tool_use_id, name, family, mcp_server,\n ROW_NUMBER() OVER (PARTITION BY tool_use_id ORDER BY transcript_id, byte_offset, block_index) AS copy_rank\n FROM tool_call\n )\n SELECT cb.transcript_id, cb.byte_offset, cb.block_index, cb.session_id, cb.ts, cb.source,\n cb.tool_use_id, cb.attachment_type, cb.chars, cb.is_media, o.request_id, d.ctx_delta,\n CASE WHEN cb.source IN ('assistant_text', 'assistant_thinking', 'assistant_tool_input') THEN NULL\n ELSE d.ctx_delta * cb.chars * 1.0 / NULLIF(SUM(CASE WHEN cb.source IN ('assistant_text', 'assistant_thinking', 'assistant_tool_input') THEN 0 ELSE cb.chars END)\n OVER (PARTITION BY o.transcript_id, o.request_id), 0) END AS est_tokens,\n t.name AS tool_name, t.family AS tool_family, t.mcp_server\n FROM context_block cb\n JOIN owner o USING (transcript_id, byte_offset, block_index)\n JOIN request_dedup d ON d.transcript_id = o.transcript_id AND d.request_id = o.request_id\n LEFT JOIN tool t ON t.tool_use_id = cb.tool_use_id AND t.copy_rank = 1;\n"];
|
|
61
61
|
|
|
62
|
+
/** Recorded in `_migration`; store-sqlite refuses a database whose applied name differs, so never rename it. */
|
|
63
|
+
declare const ORIGIN_TASK_LINK_MIGRATION_NAME = "origin task link";
|
|
64
|
+
|
|
65
|
+
/** Recorded in `_migration`; store-sqlite refuses a database whose applied name differs, so never rename it. */
|
|
66
|
+
declare const REVIEW_VERDICT_MIGRATION_NAME = "review verdicts";
|
|
67
|
+
declare const REVIEW_TABLE = "pr_review";
|
|
68
|
+
/**
|
|
69
|
+
* One row per review verdict, from either surface. Only parsed fields are kept, never message text.
|
|
70
|
+
* `source_key` is `chat:<tool_use_id>:<n>` or `gh:<pr_ref>:<submitted_at>`; `pr_ref` is resolved later.
|
|
71
|
+
*/
|
|
72
|
+
declare const REVIEW_DDL = "\n CREATE TABLE IF NOT EXISTS pr_review (\n source_key TEXT PRIMARY KEY,\n surface TEXT NOT NULL,\n verdict TEXT NOT NULL,\n ts TEXT NOT NULL,\n session_id TEXT,\n transcript_id INTEGER,\n repo TEXT,\n repo_hint TEXT,\n cwd_repo TEXT,\n number INTEGER NOT NULL,\n pr_ref TEXT\n );\n CREATE INDEX IF NOT EXISTS idx_pr_review_pr_ref ON pr_review(pr_ref);\n CREATE INDEX IF NOT EXISTS idx_pr_review_transcript ON pr_review(transcript_id);\n";
|
|
73
|
+
|
|
62
74
|
/** Write one delta's audit events. Runs inside `applyDelta`'s transaction, so it opens none of its own. */
|
|
63
75
|
declare function applyAudit(db: Db, transcriptId: number, delta: TranscriptDelta): void;
|
|
64
76
|
|
|
@@ -119,8 +131,8 @@ interface DeltaSource {
|
|
|
119
131
|
* the only source. The two observation tables are left for the same reason —
|
|
120
132
|
* `reconcile` folds them, and re-reading re-asserts the same rows.
|
|
121
133
|
*
|
|
122
|
-
* Audit rows
|
|
123
|
-
* directly rather than through `session`.
|
|
134
|
+
* Audit rows, facet state and chat review verdicts carry `transcript_id`
|
|
135
|
+
* themselves, so they go by it directly rather than through `session`.
|
|
124
136
|
*/
|
|
125
137
|
declare function purgeTranscript(graph: SessionGraph, transcriptId: number): void;
|
|
126
138
|
|
|
@@ -150,8 +162,17 @@ interface ResolvedPr {
|
|
|
150
162
|
state?: string | null;
|
|
151
163
|
mergedAt?: string | null;
|
|
152
164
|
closedAt?: string | null;
|
|
153
|
-
/** Stored as the
|
|
165
|
+
/** Stored as the forge's round count when `reviews` or `commitTimes` is absent; otherwise the round rule counts it. */
|
|
154
166
|
reviewRounds?: number | null;
|
|
167
|
+
/** The PR's commit times; an empty array is stored as known-empty, an omitted field leaves the stored value. */
|
|
168
|
+
commitTimes?: readonly string[];
|
|
169
|
+
/** Replaces the PR's stored forge reviews; an omitted field leaves them. */
|
|
170
|
+
reviews?: readonly ResolvedReview[];
|
|
171
|
+
}
|
|
172
|
+
/** One forge review. `APPROVED` and `CHANGES_REQUESTED`, in any case, are stored; other states are ignored. */
|
|
173
|
+
interface ResolvedReview {
|
|
174
|
+
state: string;
|
|
175
|
+
submittedAt: string;
|
|
155
176
|
}
|
|
156
177
|
/** Resolutions keyed by `pr_ref` (`pr:acme/demo#7`). */
|
|
157
178
|
type PrResolution = ReadonlyMap<string, ResolvedPr | null | undefined>;
|
|
@@ -206,7 +227,18 @@ interface ResolvedOrigin {
|
|
|
206
227
|
briefExcerpt?: string | null;
|
|
207
228
|
briefPath?: string | null;
|
|
208
229
|
launchArgs?: string | null;
|
|
230
|
+
/**
|
|
231
|
+
* Task ids the spawn record assigned, primary first; each projects a `ran` edge. Leave it
|
|
232
|
+
* undefined only if the resolver does not look for tasks: a stored link then stands and a
|
|
233
|
+
* row without one is offered again. A task-aware resolver sets it, empty when nothing links.
|
|
234
|
+
*/
|
|
235
|
+
taskIds?: readonly string[] | null;
|
|
236
|
+
/** How the ids were found. Stored as `none` when `taskIds` is given but empty. */
|
|
237
|
+
taskSource?: TaskLinkSource | null;
|
|
209
238
|
}
|
|
239
|
+
/** Stored when a task-aware resolver found no id, so the row is not offered again. */
|
|
240
|
+
declare const NO_TASK_LINK = "none";
|
|
241
|
+
type TaskLinkSource = "name" | "name-over-brief" | "brief-anchor" | "brief-paragraph" | typeof NO_TASK_LINK;
|
|
210
242
|
/** A lifecycle event the launcher saw outside the transcript: `teleport`, `handoff`, `retired`, `exited`. */
|
|
211
243
|
interface ExternalEvent {
|
|
212
244
|
sessionId: string;
|
|
@@ -224,7 +256,9 @@ interface OriginResolution {
|
|
|
224
256
|
/**
|
|
225
257
|
* Supplied by the caller, never by this package: session-graph is tier 2 and
|
|
226
258
|
* must not learn where a launcher keeps its records. Called once per pass with
|
|
227
|
-
* every session that has no origin row or a
|
|
259
|
+
* every session that has no origin row, a stale one, or a null `task_source`.
|
|
260
|
+
* A task-aware resolver returns an entry for every requested session it examined,
|
|
261
|
+
* with `taskIds` empty when nothing links; a session left out is offered again.
|
|
228
262
|
*/
|
|
229
263
|
type OriginResolver = (sessionIds: readonly string[]) => PromiseLike<OriginResolution> | OriginResolution;
|
|
230
264
|
interface OriginEnrichment {
|
|
@@ -249,6 +283,34 @@ declare function sessionsNeedingOrigin(graph: SessionGraph): string[];
|
|
|
249
283
|
*/
|
|
250
284
|
declare function resolveOrigins(graph: SessionGraph, resolver: OriginResolver | undefined): Promise<OriginEnrichment>;
|
|
251
285
|
|
|
286
|
+
/** Decides from a sender's `session_origin.profile` whether its chat verdicts count. */
|
|
287
|
+
type ReviewerProfilePredicate = (profile: string) => boolean;
|
|
288
|
+
/** The default role set: `reviewer`, or any profile ending in `-reviewer`. */
|
|
289
|
+
declare const isReviewerProfile: ReviewerProfilePredicate;
|
|
290
|
+
interface ReviewRoundOptions {
|
|
291
|
+
isReviewerProfile?: ReviewerProfilePredicate;
|
|
292
|
+
}
|
|
293
|
+
interface ReviewProjection {
|
|
294
|
+
/** Chat verdicts given a `pr_ref` this pass. */
|
|
295
|
+
resolved: number;
|
|
296
|
+
/** Chat verdicts whose PR is still unknown. */
|
|
297
|
+
unresolved: number;
|
|
298
|
+
/** Reviews ignored for a time that does not parse, plus PRs whose commit times were unusable. */
|
|
299
|
+
invalidTimes: number;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* The number of distinct heads among changes-requested reviews that a later commit answered.
|
|
303
|
+
* A review's head is the count of commits at or before it, so two reviews on one head count once.
|
|
304
|
+
* A review whose time does not parse is ignored. Returns null when any commit time does not
|
|
305
|
+
* parse, because a missing commit would shift every later head.
|
|
306
|
+
*/
|
|
307
|
+
declare function countRounds(changesRequestedAt: readonly string[], commitTimes: readonly string[]): number | null;
|
|
308
|
+
/**
|
|
309
|
+
* Resolve each unresolved chat verdict to a PR, then write `review_rounds_chat` and `review_rounds`
|
|
310
|
+
* for every PR. Runs after `enrichPrs`, so forge reviews and commit times are current.
|
|
311
|
+
*/
|
|
312
|
+
declare function projectReviewRounds(graph: SessionGraph, options?: ReviewRoundOptions): ReviewProjection;
|
|
313
|
+
|
|
252
314
|
/**
|
|
253
315
|
* What a product's task store knows and a transcript cannot state: the task's
|
|
254
316
|
* present title, its initiative, and the status it holds right now rather than
|
|
@@ -305,6 +367,8 @@ interface IndexOptions {
|
|
|
305
367
|
resolvePrs?: PrResolver;
|
|
306
368
|
}
|
|
307
369
|
interface RefreshOptions extends IndexOptions {
|
|
370
|
+
/** Which sender profiles' chat verdicts count toward review rounds. Defaults to `reviewer` and `*-reviewer`. */
|
|
371
|
+
isReviewerProfile?: ReviewerProfilePredicate;
|
|
308
372
|
/** Roll up every session, not just the ones this pass touched. */
|
|
309
373
|
full?: boolean;
|
|
310
374
|
/** Stale audit facets re-extracted per pass (default 40). `Infinity` clears the backlog. */
|
|
@@ -343,6 +407,7 @@ interface RefreshSummary {
|
|
|
343
407
|
tasks: TaskEnrichment;
|
|
344
408
|
origins: OriginEnrichment;
|
|
345
409
|
prs: PrEnrichment;
|
|
410
|
+
reviews: ReviewProjection;
|
|
346
411
|
markedMissing: number;
|
|
347
412
|
facetsBackfilled: number;
|
|
348
413
|
/** Transcripts whose audit facet is still stale after this pass. */
|
|
@@ -352,7 +417,7 @@ interface RefreshSummary {
|
|
|
352
417
|
* One pass over a corpus: index every transcript, re-extract a bounded batch of
|
|
353
418
|
* stale audit facets, roll up the sessions that changed, resolve session
|
|
354
419
|
* origins, reconcile cross-transcript observations, resolve PR outcomes,
|
|
355
|
-
* enrich tasks, and mark rows whose source file is gone. Idempotent: a second pass over unchanged
|
|
420
|
+
* project review rounds, enrich tasks, and mark rows whose source file is gone. Idempotent: a second pass over unchanged
|
|
356
421
|
* files changes nothing.
|
|
357
422
|
*
|
|
358
423
|
* The resolver is hoisted out of the per-transcript loop and run once over the
|
|
@@ -455,4 +520,4 @@ declare function normalizedUsage(graph: SessionGraph, ref: string): NormalizedUs
|
|
|
455
520
|
/** Ambiguity is explicit; workspace session bodies remain in their original namespace. */
|
|
456
521
|
declare function resolveConversationAlias(db: Db, legacyRef: string): string | null;
|
|
457
522
|
|
|
458
|
-
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, type NormalizedIndexResult, type NormalizedUsageSummary, ORIGIN_DDL, ORIGIN_MIGRATION_NAME, ORIGIN_TABLES, ORIGIN_VIEWS, type OpenSessionGraphOptions, type OriginEnrichment, type OriginResolution, type OriginResolver, type PrEnrichment, type PrKey, type PrResolution, type PrResolver, type PriceInput, type ReconcileCounts, type RefreshOptions, type RefreshSummary, type ResolvedOrigin, type ResolvedPr, type ResolvedTask, type SessionGraph, type SyncPricesOptions, type TaskEnrichment, type TaskResolution, type TaskResolver, type TranscriptOutcome, allSessionIds, allTaskIds, applyAudit, applyDelta, backfillFacets, enrichPrs, enrichTasks, indexCodexSource, indexTranscript, normalizedSessions, normalizedUsage, openSessionGraph, prsNeedingOutcome, purgeTranscript, readIndexedText, reconcile, refreshCorpus, replaceEpisodes, resetIndex, resolveConversationAlias, resolveOrigins, rollupSessions, sessionsNeedingOrigin, syncPrices };
|
|
523
|
+
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 RefreshOptions, type RefreshSummary, type ResolvedOrigin, type ResolvedPr, type ResolvedReview, type ResolvedTask, type ReviewProjection, type ReviewRoundOptions, type ReviewerProfilePredicate, type SessionGraph, type SyncPricesOptions, type TaskEnrichment, type TaskLinkSource, type TaskResolution, type TaskResolver, type TranscriptOutcome, allSessionIds, allTaskIds, applyAudit, applyDelta, backfillFacets, countRounds, enrichPrs, enrichTasks, indexCodexSource, indexTranscript, isReviewerProfile, normalizedSessions, normalizedUsage, openSessionGraph, projectReviewRounds, prsNeedingOutcome, purgeTranscript, readIndexedText, reconcile, refreshCorpus, replaceEpisodes, resetIndex, resolveConversationAlias, resolveOrigins, rollupSessions, sessionsNeedingOrigin, syncPrices };
|