@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/dist/index.js CHANGED
@@ -254,6 +254,45 @@ function applyEpisodeTranscriptSchema(db) {
254
254
  addColumnIfMissing(db, EPISODE_TABLE, "end_transcript_id", "INTEGER");
255
255
  }
256
256
 
257
+ // src/audit-schema-v7.ts
258
+ var ORIGIN_TASK_LINK_MIGRATION_NAME = "origin task link";
259
+ function applyOriginTaskLinkSchema(db) {
260
+ addColumnIfMissing(db, "session_origin", "task_ids", "TEXT");
261
+ addColumnIfMissing(db, "session_origin", "task_source", "TEXT");
262
+ }
263
+
264
+ // src/audit-schema-v8.ts
265
+ var REVIEW_VERDICT_MIGRATION_NAME = "review verdicts";
266
+ var REVIEW_TABLE = "pr_review";
267
+ var REVIEW_DDL = `
268
+ CREATE TABLE IF NOT EXISTS ${REVIEW_TABLE} (
269
+ source_key TEXT PRIMARY KEY,
270
+ surface TEXT NOT NULL,
271
+ verdict TEXT NOT NULL,
272
+ ts TEXT NOT NULL,
273
+ session_id TEXT,
274
+ transcript_id INTEGER,
275
+ repo TEXT,
276
+ repo_hint TEXT,
277
+ cwd_repo TEXT,
278
+ number INTEGER NOT NULL,
279
+ pr_ref TEXT
280
+ );
281
+ CREATE INDEX IF NOT EXISTS idx_${REVIEW_TABLE}_pr_ref ON ${REVIEW_TABLE}(pr_ref);
282
+ CREATE INDEX IF NOT EXISTS idx_${REVIEW_TABLE}_transcript ON ${REVIEW_TABLE}(transcript_id);
283
+ `;
284
+ var PR_COLUMNS = [
285
+ ["review_rounds_gh", "INTEGER"],
286
+ ["review_rounds_chat", "INTEGER"],
287
+ ["commit_times", "TEXT"]
288
+ ];
289
+ function applyReviewVerdictSchema(db) {
290
+ db.exec(REVIEW_DDL);
291
+ for (const [column, type] of PR_COLUMNS) addColumnIfMissing(db, "pr", column, type);
292
+ db.exec("UPDATE pr SET review_rounds_gh = review_rounds");
293
+ db.exec("UPDATE pr SET outcome_checked_at = NULL");
294
+ }
295
+
257
296
  // src/normalized-schema.ts
258
297
  import { conversationRef } from "@titan-design/agent-protocol";
259
298
  var NORMALIZED_DDL = `
@@ -451,6 +490,7 @@ var DERIVED_TABLES = [
451
490
  "pr",
452
491
  "pr_merge_observation",
453
492
  "pr_create_observation",
493
+ REVIEW_TABLE,
454
494
  "branch",
455
495
  "file",
456
496
  "task",
@@ -465,7 +505,9 @@ var MIGRATIONS = [
465
505
  } },
466
506
  { version: 4, name: AUDIT_MIGRATION_NAME, up: applyAuditSchema },
467
507
  { version: 5, name: ORIGIN_MIGRATION_NAME, up: applyOriginSchema },
468
- { version: 6, name: EPISODE_TRANSCRIPT_MIGRATION_NAME, up: applyEpisodeTranscriptSchema }
508
+ { version: 6, name: EPISODE_TRANSCRIPT_MIGRATION_NAME, up: applyEpisodeTranscriptSchema },
509
+ { version: 7, name: ORIGIN_TASK_LINK_MIGRATION_NAME, up: applyOriginTaskLinkSchema },
510
+ { version: 8, name: REVIEW_VERDICT_MIGRATION_NAME, up: applyReviewVerdictSchema }
469
511
  ];
470
512
 
471
513
  // src/graph.ts
@@ -626,7 +668,7 @@ function advanceFacet(db, transcriptId, fromByteOffset, indexedTo, version = EXT
626
668
  }
627
669
 
628
670
  // src/apply.ts
629
- import { sessionRef } from "@titan-design/session-read";
671
+ import { RELATIONS, sessionRef } from "@titan-design/session-read";
630
672
  function applyDelta(graph, transcriptId, delta, source = {}) {
631
673
  graph.db.transaction(() => {
632
674
  applyFacts(graph.db, transcriptId, delta);
@@ -635,6 +677,7 @@ function applyDelta(graph, transcriptId, delta, source = {}) {
635
677
  applyAssets(graph.db, delta);
636
678
  applyPhases(graph.db, transcriptId, delta);
637
679
  applyLinkedRows(graph, transcriptId, delta);
680
+ applyReviewVerdicts(graph.db, transcriptId, delta);
638
681
  applyAudit(graph.db, transcriptId, delta);
639
682
  })();
640
683
  }
@@ -702,6 +745,43 @@ function pick(row, sql) {
702
745
  const names = new Set([...sql.matchAll(/@(\w+)/g)].map((m) => m[1]));
703
746
  return Object.fromEntries([...names].map((name) => [name, row[name] ?? null]));
704
747
  }
748
+ var UPSERT_CHAT_VERDICT = `
749
+ INSERT INTO ${REVIEW_TABLE} (source_key, surface, verdict, ts, session_id, transcript_id, repo, repo_hint, cwd_repo, number)
750
+ VALUES (@sourceKey, 'chat', @verdict, @ts, @sessionId, @transcriptId, @repo, @repoHint, @cwdRepo, @number)
751
+ ON CONFLICT (source_key) DO UPDATE SET verdict = excluded.verdict, ts = excluded.ts, session_id = excluded.session_id,
752
+ transcript_id = excluded.transcript_id, repo = excluded.repo, repo_hint = excluded.repo_hint,
753
+ cwd_repo = excluded.cwd_repo, number = excluded.number`;
754
+ function applyReviewVerdicts(db, transcriptId, delta) {
755
+ const upsert = db.prepare(UPSERT_CHAT_VERDICT);
756
+ const keys = chatVerdictKeys(delta.reviewVerdicts);
757
+ delta.reviewVerdicts.forEach((v, i) => {
758
+ upsert.run({
759
+ sourceKey: keys[i],
760
+ verdict: v.verdict,
761
+ ts: v.ts,
762
+ sessionId: v.sessionId,
763
+ transcriptId,
764
+ repo: v.repo,
765
+ repoHint: v.repoHint,
766
+ cwdRepo: v.cwdRepo,
767
+ number: v.number
768
+ });
769
+ });
770
+ }
771
+ var hasOrdinal = (v) => Number.isInteger(v.ordinal) && v.ordinal >= 0;
772
+ function chatVerdictKeys(verdicts) {
773
+ const used = /* @__PURE__ */ new Map();
774
+ const usedBy = (id) => used.get(id) ?? used.set(id, /* @__PURE__ */ new Set()).get(id);
775
+ for (const v of verdicts) if (hasOrdinal(v)) usedBy(v.toolUseId).add(v.ordinal);
776
+ return verdicts.map((v) => {
777
+ if (hasOrdinal(v)) return `chat:${v.toolUseId}:${v.ordinal}`;
778
+ const taken = usedBy(v.toolUseId);
779
+ let n = 0;
780
+ while (taken.has(n)) n += 1;
781
+ taken.add(n);
782
+ return `chat:${v.toolUseId}:${n}`;
783
+ });
784
+ }
705
785
  function applyPhases(db, transcriptId, delta) {
706
786
  const current = db.prepare("SELECT phase_id, to_mode FROM permission_phase WHERE session_id = ? AND trigger = ? AND t_invalid IS NULL ORDER BY phase_id DESC LIMIT 1");
707
787
  const close = db.prepare("UPDATE permission_phase SET t_invalid = ? WHERE phase_id = ?");
@@ -734,11 +814,16 @@ function applyLinkedRows(graph, transcriptId, delta) {
734
814
  for (const s of delta.subagents) upsertSubagent.run(s.agentRef, s.sessionId, s.agentType, s.label, s.ts || null, factId(s.byteOffset));
735
815
  const linkTranscript = db.prepare("INSERT INTO subagent (agent_ref, child_session_id) VALUES (?, ?) ON CONFLICT (agent_ref) DO UPDATE SET child_session_id = excluded.child_session_id");
736
816
  for (const l of delta.subagentTranscripts) linkTranscript.run(l.agentRef, l.childSessionId);
737
- for (const e of delta.edges) graph.edges.assert({ sourceRef: e.sourceRef, relation: e.relation, targetRef: e.targetRef, tValid: e.ts, factId: factId(e.byteOffset) });
817
+ for (const e of delta.edges) assertTranscriptEdge(graph, { sourceRef: e.sourceRef, relation: e.relation, targetRef: e.targetRef, tValid: e.ts, factId: factId(e.byteOffset) });
738
818
  for (const span of delta.spans) {
739
819
  graph.spans.index({ ownerRef: sessionRef(span.sessionId), field: span.field, sourceId: transcriptId, byteOffset: span.byteOffset, byteLength: span.byteLength }, span.text);
740
820
  }
741
821
  }
822
+ function assertTranscriptEdge(graph, edge) {
823
+ const existing = edge.relation === RELATIONS.RAN ? graph.edges.current(edge.sourceRef, edge.relation, edge.targetRef) : void 0;
824
+ if (existing?.attrs?.via === "origin") graph.edges.supersede(edge);
825
+ else graph.edges.assert(edge);
826
+ }
742
827
  function factIdFor(db, transcriptId, byteOffset) {
743
828
  const row = db.prepare("SELECT fact_id FROM fact WHERE transcript_id = ? AND byte_offset = ?").get(transcriptId, byteOffset);
744
829
  return row?.fact_id ?? null;
@@ -755,7 +840,7 @@ function purgeTranscript(graph, transcriptId) {
755
840
  graph.db.prepare(`DELETE FROM "${table}" WHERE session_id IN (SELECT session_id FROM session WHERE transcript_id = ?)`).run(transcriptId);
756
841
  }
757
842
  graph.db.prepare(`DELETE FROM "${KIT.edge}" WHERE fact_id IN (SELECT fact_id FROM fact WHERE transcript_id = ?)`).run(transcriptId);
758
- for (const table of [...AUDIT_TABLES, FACET_TABLE]) {
843
+ for (const table of [...AUDIT_TABLES, FACET_TABLE, REVIEW_TABLE]) {
759
844
  graph.db.prepare(`DELETE FROM "${table}" WHERE transcript_id = ?`).run(transcriptId);
760
845
  }
761
846
  graph.db.prepare("DELETE FROM fact WHERE transcript_id = ?").run(transcriptId);
@@ -921,18 +1006,126 @@ import { promises as fs } from "fs";
921
1006
  import { contentHash, resumePoint } from "@titan-design/locator";
922
1007
  import { TranscriptParseError as TranscriptParseError2, extractTranscript as extractTranscript2 } from "@titan-design/session-read";
923
1008
 
1009
+ // src/review-rounds.ts
1010
+ import { RELATIONS as RELATIONS2 } from "@titan-design/session-read";
1011
+ var isReviewerProfile = (profile) => profile === "reviewer" || profile.endsWith("-reviewer");
1012
+ function countRounds(changesRequestedAt, commitTimes) {
1013
+ const commits = parseCommitTimes(commitTimes);
1014
+ return commits ? answeredHeads(changesRequestedAt, commits).heads.size : null;
1015
+ }
1016
+ function parseCommitTimes(commitTimes) {
1017
+ const commits = commitTimes.map(Date.parse);
1018
+ return commits.some(Number.isNaN) ? null : commits;
1019
+ }
1020
+ function answeredHeads(reviewTimes, commits) {
1021
+ const heads = /* @__PURE__ */ new Set();
1022
+ let ignored = 0;
1023
+ for (const ts of reviewTimes) {
1024
+ const at = Date.parse(ts);
1025
+ if (Number.isNaN(at)) {
1026
+ ignored += 1;
1027
+ continue;
1028
+ }
1029
+ const head = commits.filter((c) => c <= at).length;
1030
+ if (head < commits.length) heads.add(head);
1031
+ }
1032
+ return { heads, ignored };
1033
+ }
1034
+ function projectReviewRounds(graph, options = {}) {
1035
+ const isReviewer = options.isReviewerProfile ?? isReviewerProfile;
1036
+ return graph.db.transaction(() => {
1037
+ const resolved = resolveChatVerdicts(graph.db);
1038
+ const invalidTimes = writeRounds(graph.db, isReviewer);
1039
+ const { n } = graph.db.prepare(`SELECT count(*) AS n FROM ${REVIEW_TABLE} WHERE surface = 'chat' AND pr_ref IS NULL`).get();
1040
+ return { resolved, unresolved: n, invalidTimes };
1041
+ })();
1042
+ }
1043
+ function resolveChatVerdicts(db) {
1044
+ const rows = db.prepare(`SELECT source_key, session_id, repo, repo_hint, cwd_repo, number FROM ${REVIEW_TABLE} WHERE surface = 'chat' AND pr_ref IS NULL`).all();
1045
+ const update = db.prepare(`UPDATE ${REVIEW_TABLE} SET pr_ref = ? WHERE source_key = ?`);
1046
+ let resolved = 0;
1047
+ for (const row of rows) {
1048
+ const prRef = resolvePrRef(db, row);
1049
+ if (prRef) resolved += update.run(prRef, row.source_key).changes;
1050
+ }
1051
+ return resolved;
1052
+ }
1053
+ function resolvePrRef(db, row) {
1054
+ const candidates = db.prepare("SELECT pr_ref, repo FROM pr WHERE number = ? AND repo IS NOT NULL").all(row.number);
1055
+ if (row.repo) return candidates.find((c) => c.repo === row.repo)?.pr_ref ?? null;
1056
+ const hinted = candidates.filter((c) => sameRepoName(c.repo, row.repo_hint));
1057
+ if (hinted.length === 1) return hinted[0].pr_ref;
1058
+ const pool = hinted.length > 0 ? hinted : candidates;
1059
+ const linked = familyLinks(db, row.session_id);
1060
+ const bySibling = pool.filter((c) => linked.has(c.pr_ref));
1061
+ if (bySibling.length === 1) return bySibling[0].pr_ref;
1062
+ const byCwd = (bySibling.length > 0 ? bySibling : pool).filter((c) => sameRepoName(c.repo, row.cwd_repo));
1063
+ return byCwd.length === 1 ? byCwd[0].pr_ref : null;
1064
+ }
1065
+ function sameRepoName(repo, name) {
1066
+ return name !== null && repo.split("/").pop().toLowerCase() === name.toLowerCase();
1067
+ }
1068
+ var FAMILY_LINKS = `
1069
+ WITH parent(id) AS (
1070
+ SELECT parent_session_id FROM session_origin WHERE session_id = @sessionId AND parent_session_id IS NOT NULL
1071
+ UNION SELECT session_id FROM subagent WHERE child_session_id = @sessionId AND session_id IS NOT NULL
1072
+ ), family(id) AS (
1073
+ SELECT @sessionId UNION SELECT id FROM parent
1074
+ UNION SELECT session_id FROM session_origin WHERE parent_session_id IN (SELECT id FROM parent)
1075
+ UNION SELECT child_session_id FROM subagent WHERE session_id IN (SELECT id FROM parent) AND child_session_id IS NOT NULL
1076
+ UNION SELECT substr(target_ref, 9) FROM "${KIT.edge}" WHERE relation = '${RELATIONS2.SPAWNED}' AND t_expired IS NULL
1077
+ AND target_ref LIKE 'session:%' AND source_ref IN (SELECT 'session:' || id FROM parent)
1078
+ )
1079
+ SELECT DISTINCT target_ref FROM "${KIT.edge}" WHERE relation = '${RELATIONS2.LINKED}' AND t_expired IS NULL
1080
+ AND source_ref IN (SELECT 'session:' || id FROM family)`;
1081
+ function familyLinks(db, sessionId) {
1082
+ if (!sessionId) return /* @__PURE__ */ new Set();
1083
+ const rows = db.prepare(FAMILY_LINKS).all({ sessionId });
1084
+ return new Set(rows.map((r) => r.target_ref));
1085
+ }
1086
+ var CHANGES_REQUESTED = `
1087
+ SELECT r.pr_ref, r.surface, r.ts, o.profile FROM ${REVIEW_TABLE} r LEFT JOIN session_origin o ON o.session_id = r.session_id
1088
+ WHERE r.verdict = 'changes_requested' AND r.pr_ref IS NOT NULL`;
1089
+ function writeRounds(db, isReviewer) {
1090
+ const byPr = /* @__PURE__ */ new Map();
1091
+ for (const review of db.prepare(CHANGES_REQUESTED).all()) {
1092
+ if (review.surface === "chat" && !(review.profile && isReviewer(review.profile))) continue;
1093
+ byPr.set(review.pr_ref, [...byPr.get(review.pr_ref) ?? [], review]);
1094
+ }
1095
+ const update = db.prepare("UPDATE pr SET review_rounds_chat = ?, review_rounds = ? WHERE pr_ref = ?");
1096
+ let invalidTimes = 0;
1097
+ for (const pr of db.prepare("SELECT pr_ref, commit_times, review_rounds_gh FROM pr").all()) {
1098
+ const { chat, total, invalid } = roundsFor(pr, byPr.get(pr.pr_ref) ?? []);
1099
+ update.run(chat, total, pr.pr_ref);
1100
+ invalidTimes += invalid;
1101
+ }
1102
+ return invalidTimes;
1103
+ }
1104
+ function roundsFor(pr, reviews) {
1105
+ const commits = pr.commit_times === null ? null : parseCommitTimes(JSON.parse(pr.commit_times));
1106
+ if (!commits) return { chat: null, total: pr.review_rounds_gh, invalid: pr.commit_times === null ? 0 : 1 };
1107
+ const timesOn = (surface) => reviews.filter((r) => r.surface === surface).map((r) => r.ts);
1108
+ const forge = answeredHeads(timesOn("github"), commits);
1109
+ const chat = answeredHeads(timesOn("chat"), commits);
1110
+ const chatOnly = [...chat.heads].filter((head) => !forge.heads.has(head)).length;
1111
+ return { chat: chat.heads.size, total: (pr.review_rounds_gh ?? 0) + chatOnly, invalid: forge.ignored + chat.ignored };
1112
+ }
1113
+
924
1114
  // src/outcomes.ts
925
1115
  var NO_PR_OUTCOMES = Object.freeze({ requested: 0, applied: 0, failed: false });
926
1116
  var PRS_NEEDING_OUTCOME = `
927
1117
  SELECT pr_ref, repo, number FROM pr
928
- WHERE repo IS NOT NULL AND number IS NOT NULL AND (outcome_checked_at IS NULL OR state IS NOT 'merged')
929
- ORDER BY pr_ref`;
1118
+ WHERE repo IS NOT NULL AND number IS NOT NULL
1119
+ AND (outcome_checked_at IS NULL OR state IS NOT 'merged' OR commit_times IS NULL)
1120
+ ORDER BY CASE WHEN outcome_checked_at IS NULL THEN 0 WHEN state IS NOT 'merged' THEN 1 ELSE 2 END, pr_ref`;
930
1121
  var APPLY_OUTCOME = `
931
1122
  UPDATE pr SET
932
1123
  state = CASE WHEN state = 'merged' THEN state ELSE COALESCE(lower(@state), state) END,
933
1124
  merged_at = COALESCE(@mergedAt, merged_at),
934
1125
  closed_at = COALESCE(@closedAt, closed_at),
935
1126
  review_rounds = COALESCE(@reviewRounds, review_rounds),
1127
+ review_rounds_gh = COALESCE(@reviewRounds, review_rounds_gh),
1128
+ commit_times = COALESCE(@commitTimes, commit_times),
936
1129
  outcome_checked_at = @checkedAt
937
1130
  WHERE pr_ref = @prRef`;
938
1131
  function prsNeedingOutcome(graph) {
@@ -952,6 +1145,7 @@ async function enrichPrs(graph, resolver) {
952
1145
  }
953
1146
  function write(graph, resolved, checkedAt) {
954
1147
  const apply = graph.db.prepare(APPLY_OUTCOME);
1148
+ const stored = graph.db.prepare("SELECT commit_times FROM pr WHERE pr_ref = ?");
955
1149
  return graph.db.transaction(() => {
956
1150
  let applied = 0;
957
1151
  for (const [prRef, fields] of resolved) {
@@ -962,27 +1156,79 @@ function write(graph, resolved, checkedAt) {
962
1156
  state: fields.state ?? null,
963
1157
  mergedAt: fields.mergedAt ?? null,
964
1158
  closedAt: fields.closedAt ?? null,
965
- reviewRounds: fields.reviewRounds ?? null
1159
+ reviewRounds: forgeRounds(fields, () => storedCommitTimes(stored.get(prRef))),
1160
+ commitTimes: fields.commitTimes ? JSON.stringify(fields.commitTimes) : null
966
1161
  }).changes;
1162
+ if (fields.reviews) replaceForgeReviews(graph, prRef, fields.reviews);
967
1163
  }
968
1164
  return applied;
969
1165
  })();
970
1166
  }
1167
+ var isChangesRequested = (review) => review.state.toUpperCase() === "CHANGES_REQUESTED";
1168
+ function forgeRounds(fields, storedTimes) {
1169
+ if (!fields.reviews) return fields.reviewRounds ?? null;
1170
+ const commitTimes = fields.commitTimes ?? storedTimes();
1171
+ const counted = commitTimes ? countRounds(fields.reviews.filter(isChangesRequested).map((r) => r.submittedAt), commitTimes) : null;
1172
+ return counted ?? fields.reviewRounds ?? null;
1173
+ }
1174
+ function storedCommitTimes(row) {
1175
+ const times = row?.commit_times;
1176
+ return times ? JSON.parse(times) : null;
1177
+ }
1178
+ var FORGE_VERDICTS = { APPROVED: "approve", CHANGES_REQUESTED: "changes_requested" };
1179
+ var INSERT_FORGE_REVIEW = `
1180
+ INSERT INTO ${REVIEW_TABLE} (source_key, surface, verdict, ts, repo, number, pr_ref)
1181
+ SELECT 'gh:' || pr_ref || ':' || @ts, 'github', @verdict, @ts, repo, number, pr_ref FROM pr WHERE pr_ref = @prRef
1182
+ ON CONFLICT (source_key) DO UPDATE SET verdict = CASE WHEN verdict = 'changes_requested' THEN verdict ELSE excluded.verdict END`;
1183
+ function replaceForgeReviews(graph, prRef, reviews) {
1184
+ graph.db.prepare(`DELETE FROM ${REVIEW_TABLE} WHERE surface = 'github' AND pr_ref = ?`).run(prRef);
1185
+ const insert = graph.db.prepare(INSERT_FORGE_REVIEW);
1186
+ for (const review of reviews) {
1187
+ const verdict = FORGE_VERDICTS[review.state.toUpperCase()];
1188
+ if (verdict) insert.run({ prRef, verdict, ts: review.submittedAt });
1189
+ }
1190
+ }
971
1191
 
972
1192
  // src/origin.ts
973
- import { RELATIONS, sessionRef as sessionRef3 } from "@titan-design/session-read";
1193
+ import { RELATIONS as RELATIONS3, sessionRef as sessionRef3, taskRef } from "@titan-design/session-read";
1194
+ var NO_TASK_LINK = "none";
974
1195
  var NO_ORIGINS = Object.freeze({ requested: 0, applied: 0, events: 0, failed: false });
975
1196
  var SESSIONS_NEEDING_ORIGIN = `
976
1197
  SELECT s.session_id FROM session s LEFT JOIN session_origin o ON o.session_id = s.session_id
977
- WHERE o.session_id IS NULL OR o.resolved_at < s.ended_at
1198
+ WHERE o.session_id IS NULL OR o.resolved_at < s.ended_at OR o.task_source IS NULL
978
1199
  ORDER BY s.session_id`;
1200
+ var ORIGIN_COLUMNS = [
1201
+ "origin_system",
1202
+ "agent_id",
1203
+ "agent_name",
1204
+ "parent_name",
1205
+ "parent_session_id",
1206
+ "profile",
1207
+ "model_alias",
1208
+ "surface",
1209
+ "isolation",
1210
+ "depth",
1211
+ "origin_kind",
1212
+ "config_dir",
1213
+ "spawn_cwd",
1214
+ "spawned_at",
1215
+ "brief_chars",
1216
+ "brief_excerpt",
1217
+ "brief_path",
1218
+ "launch_args",
1219
+ "resolved_at"
1220
+ ];
1221
+ var TASK_LINK_UPDATE = `task_source = COALESCE(excluded.task_source, task_source),
1222
+ task_ids = CASE WHEN excluded.task_source IS NOT NULL THEN excluded.task_ids ELSE task_ids END`;
979
1223
  var UPSERT_ORIGIN = `
980
- INSERT OR REPLACE INTO session_origin (session_id, origin_system, agent_id, agent_name, parent_name, parent_session_id,
1224
+ INSERT INTO session_origin (session_id, origin_system, agent_id, agent_name, parent_name, parent_session_id,
981
1225
  profile, model_alias, surface, isolation, depth, origin_kind, config_dir, spawn_cwd, spawned_at,
982
- brief_chars, brief_excerpt, brief_path, launch_args, resolved_at)
1226
+ brief_chars, brief_excerpt, brief_path, launch_args, task_ids, task_source, resolved_at)
983
1227
  VALUES (@sessionId, @originSystem, @agentId, @agentName, @parentName, @parentSessionId,
984
1228
  @profile, @modelAlias, @surface, @isolation, @depth, @originKind, @configDir, @spawnCwd, @spawnedAt,
985
- @briefChars, @briefExcerpt, @briefPath, @launchArgs, @resolvedAt)`;
1229
+ @briefChars, @briefExcerpt, @briefPath, @launchArgs, @taskIds, @taskSource, @resolvedAt)
1230
+ ON CONFLICT (session_id) DO UPDATE SET ${ORIGIN_COLUMNS.map((c) => `${c} = excluded.${c}`).join(", ")},
1231
+ ${TASK_LINK_UPDATE}`;
986
1232
  var UPSERT_EVENT = `
987
1233
  INSERT INTO session_external_event (session_id, ts, origin_system, kind, detail)
988
1234
  VALUES (@sessionId, @ts, COALESCE(@originSystem, (SELECT origin_system FROM session_origin WHERE session_id = @sessionId), 'unknown'), @kind, @detail)
@@ -999,14 +1245,16 @@ function sessionsNeedingOrigin(graph) {
999
1245
  async function resolveOrigins(graph, resolver) {
1000
1246
  if (!resolver) return NO_ORIGINS;
1001
1247
  const sessionIds = sessionsNeedingOrigin(graph);
1248
+ let resolved = [];
1002
1249
  try {
1003
1250
  const resolution = await resolver(sessionIds);
1004
1251
  const written = writeResolution(graph, resolution, (/* @__PURE__ */ new Date()).toISOString());
1252
+ resolved = Object.keys(resolution.origins);
1005
1253
  return { requested: sessionIds.length, ...written, failed: false };
1006
1254
  } catch (err) {
1007
1255
  return { requested: sessionIds.length, applied: 0, events: 0, failed: true, error: err instanceof Error ? err.message : String(err) };
1008
1256
  } finally {
1009
- projectOrigins(graph);
1257
+ projectOrigins(graph, resolved);
1010
1258
  }
1011
1259
  }
1012
1260
  function writeResolution(graph, resolution, resolvedAt) {
@@ -1045,21 +1293,54 @@ function originRow(sessionId, o, resolvedAt) {
1045
1293
  briefChars: o.briefChars ?? null,
1046
1294
  briefExcerpt: o.briefExcerpt ?? null,
1047
1295
  briefPath: o.briefPath ?? null,
1048
- launchArgs: o.launchArgs ?? null
1296
+ launchArgs: o.launchArgs ?? null,
1297
+ ...taskLinkColumns(o)
1049
1298
  };
1050
1299
  }
1051
- function projectOrigins(graph) {
1300
+ function taskLinkColumns(o) {
1301
+ if (o.taskIds === void 0) return { taskIds: null, taskSource: null };
1302
+ if (!o.taskIds?.length) return { taskIds: null, taskSource: NO_TASK_LINK };
1303
+ return { taskIds: JSON.stringify(o.taskIds), taskSource: o.taskSource ?? NO_TASK_LINK };
1304
+ }
1305
+ function projectOrigins(graph, resolved) {
1052
1306
  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();
1053
1307
  graph.db.transaction(() => {
1054
1308
  graph.db.prepare(PROJECT_SUBAGENTS).run();
1055
1309
  for (const o of spawned) {
1056
- graph.edges.assert({ sourceRef: sessionRef3(o.parent_session_id), relation: RELATIONS.SPAWNED, targetRef: sessionRef3(o.session_id), tValid: o.spawned_at ?? o.resolved_at });
1310
+ graph.edges.assert({ sourceRef: sessionRef3(o.parent_session_id), relation: RELATIONS3.SPAWNED, targetRef: sessionRef3(o.session_id), tValid: o.spawned_at ?? o.resolved_at });
1057
1311
  }
1312
+ projectTaskLinks(graph, resolved);
1058
1313
  })();
1059
1314
  }
1315
+ var LINK_CONFIDENCE = { name: 1, "name-over-brief": 1, "brief-anchor": 0.9, "brief-paragraph": 0.6 };
1316
+ var LOWEST_CONFIDENCE = 0.6;
1317
+ function projectTaskLinks(graph, resolved) {
1318
+ const insertTask = graph.db.prepare("INSERT OR IGNORE INTO task (task_ref, task_id) VALUES (?, ?)");
1319
+ const select = graph.db.prepare("SELECT session_id, task_ids, task_source, spawned_at, resolved_at FROM session_origin WHERE session_id = ?");
1320
+ for (const sessionId of resolved) {
1321
+ const o = select.get(sessionId);
1322
+ if (!o) continue;
1323
+ const taskIds = o.task_ids ? JSON.parse(o.task_ids) : [];
1324
+ const source = sessionRef3(o.session_id);
1325
+ const attrs = { via: "origin", source: o.task_source };
1326
+ const confidence = LINK_CONFIDENCE[o.task_source ?? ""] ?? LOWEST_CONFIDENCE;
1327
+ for (const taskId of taskIds) {
1328
+ insertTask.run(taskRef(taskId), taskId);
1329
+ graph.edges.assert({ sourceRef: source, relation: RELATIONS3.RAN, targetRef: taskRef(taskId), tValid: o.spawned_at ?? o.resolved_at, attrs, confidence });
1330
+ }
1331
+ expireStaleTaskLinks(graph, source, new Set(taskIds.map(taskRef)));
1332
+ }
1333
+ }
1334
+ function expireStaleTaskLinks(graph, source, linked) {
1335
+ for (const edge of graph.edges.from(source)) {
1336
+ if (edge.relation === RELATIONS3.RAN && edge.attrs?.via === "origin" && !linked.has(edge.targetRef)) {
1337
+ graph.edges.expire(source, RELATIONS3.RAN, edge.targetRef);
1338
+ }
1339
+ }
1340
+ }
1060
1341
 
1061
1342
  // src/tasks.ts
1062
- import { taskRef } from "@titan-design/session-read";
1343
+ import { taskRef as taskRef2 } from "@titan-design/session-read";
1063
1344
  var NO_ENRICHMENT = Object.freeze({ requested: 0, applied: 0, failed: false });
1064
1345
  var UPSERT_TASK = `
1065
1346
  INSERT INTO task (task_ref, task_id, initiative, title, status, estimate) VALUES (@taskRef, @taskId, @initiative, @title, @status, @estimate)
@@ -1088,7 +1369,7 @@ function write2(graph, resolved) {
1088
1369
  for (const [taskId, fields] of resolved) {
1089
1370
  if (!fields) continue;
1090
1371
  applied += upsert.run({
1091
- taskRef: taskRef(taskId),
1372
+ taskRef: taskRef2(taskId),
1092
1373
  taskId,
1093
1374
  initiative: fields.initiative ?? null,
1094
1375
  title: fields.title ?? null,
@@ -1143,7 +1424,7 @@ async function indexTranscript(graph, transcript, options = {}) {
1143
1424
  }
1144
1425
  }
1145
1426
  async function refreshCorpus(graph, transcripts, options = {}) {
1146
- const { resolveTasks, resolveOrigins: originResolver, resolvePrs, full, facetLimit, ...perTranscript } = options;
1427
+ const { resolveTasks, resolveOrigins: originResolver, resolvePrs, isReviewerProfile: isReviewerProfile2, full, facetLimit, ...perTranscript } = options;
1147
1428
  const counts = { indexed: 0, unchanged: 0, rewound: 0, missing: 0, quarantined: 0 };
1148
1429
  const touched = [];
1149
1430
  let facts = 0;
@@ -1159,10 +1440,11 @@ async function refreshCorpus(graph, transcripts, options = {}) {
1159
1440
  const origins = await resolveOrigins(graph, originResolver);
1160
1441
  const reconciled = reconcile(graph);
1161
1442
  const prs = await enrichPrs(graph, resolvePrs);
1443
+ const reviews = projectReviewRounds(graph, { isReviewerProfile: isReviewerProfile2 });
1162
1444
  const tasks = await enrichTasks(graph, resolveTasks, allTaskIds(graph));
1163
1445
  const markedMissing = await markMissing(graph, transcripts);
1164
1446
  const facetSummary = { facetsBackfilled: facets.backfilled, facetBacklog: facets.backlog };
1165
- return { transcripts: transcripts.length, ...counts, facts, turnsRolledUp, reconciled, tasks, origins, prs, markedMissing, ...facetSummary };
1447
+ return { transcripts: transcripts.length, ...counts, facts, turnsRolledUp, reconciled, tasks, origins, prs, reviews, markedMissing, ...facetSummary };
1166
1448
  }
1167
1449
  async function markMissing(graph, discovered) {
1168
1450
  const present = new Set(discovered.map((t) => t.displayPath));
@@ -1465,22 +1747,30 @@ export {
1465
1747
  NO_ENRICHMENT,
1466
1748
  NO_ORIGINS,
1467
1749
  NO_PR_OUTCOMES,
1750
+ NO_TASK_LINK,
1468
1751
  ORIGIN_DDL,
1469
1752
  ORIGIN_MIGRATION_NAME,
1470
1753
  ORIGIN_TABLES,
1754
+ ORIGIN_TASK_LINK_MIGRATION_NAME,
1471
1755
  ORIGIN_VIEWS,
1756
+ REVIEW_DDL,
1757
+ REVIEW_TABLE,
1758
+ REVIEW_VERDICT_MIGRATION_NAME,
1472
1759
  allSessionIds,
1473
1760
  allTaskIds,
1474
1761
  applyAudit,
1475
1762
  applyDelta,
1476
1763
  backfillFacets,
1764
+ countRounds,
1477
1765
  enrichPrs,
1478
1766
  enrichTasks,
1479
1767
  indexCodexSource,
1480
1768
  indexTranscript,
1769
+ isReviewerProfile,
1481
1770
  normalizedSessions,
1482
1771
  normalizedUsage,
1483
1772
  openSessionGraph,
1773
+ projectReviewRounds,
1484
1774
  prsNeedingOutcome,
1485
1775
  purgeTranscript,
1486
1776
  readIndexedText,