@titan-design/session-graph 0.9.1 → 0.11.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
@@ -625,8 +667,91 @@ function advanceFacet(db, transcriptId, fromByteOffset, indexedTo, version = EXT
625
667
  db.prepare(`UPDATE ${FACET_TABLE} SET indexed_to = ? WHERE transcript_id = ? AND facet = ? AND version = ?`).run(indexedTo, transcriptId, AUDIT_FACET, version);
626
668
  }
627
669
 
670
+ // src/injected-text.ts
671
+ import { hasMarker, INJECTED_MARKERS } from "@titan-design/session-read";
672
+ var INJECTED_CAUSES = /* @__PURE__ */ new Set([
673
+ "compaction",
674
+ "channel_message",
675
+ "channel_system",
676
+ "task_notification",
677
+ "scheduled_wakeup",
678
+ "usage_limit_resume",
679
+ "harness_resume",
680
+ "local_command",
681
+ "image_meta",
682
+ "hook_or_reminder"
683
+ ]);
684
+ function isInjectedCause(cause) {
685
+ return INJECTED_CAUSES.has(cause);
686
+ }
687
+ function isUntypedPrompt(inbound, indexSdkPrompts = false) {
688
+ return isInjectedCause(inbound.cause) || !indexSdkPrompts && inbound.promptSource === "sdk";
689
+ }
690
+ var CUT_TAGS = [
691
+ ...Object.values(INJECTED_MARKERS).flatMap((m) => m.close ? [m.close.slice(2, -1)] : []),
692
+ "user-prompt-submit-hook",
693
+ "command-name",
694
+ "command-message",
695
+ "local-command-caveat",
696
+ "local-command-stdout",
697
+ "local-command-stderr",
698
+ "bash-stdout",
699
+ "bash-stderr"
700
+ ].join("|");
701
+ var CUT_OPEN = new RegExp(`<(${CUT_TAGS})(?=[\\s>])[^>]*>`, "g");
702
+ var LINE_END_OR_BLOCK = new RegExp(`^[ \\t]*(?:\\r?\\n|$|<(?:${CUT_TAGS})(?=[\\s>]))`);
703
+ function opensBlock(text, at, keptFrom) {
704
+ const lead = text.slice(Math.max(text.lastIndexOf("\n", at - 1) + 1, keptFrom), at);
705
+ return lead.trim() === "";
706
+ }
707
+ function balancedClose(text, tag, from) {
708
+ const tags = new RegExp(`<(/?)${tag}(?=[\\s>])[^>]*>`, "g");
709
+ tags.lastIndex = from;
710
+ let depth = 1;
711
+ for (let m = tags.exec(text); m; m = tags.exec(text)) {
712
+ depth += m[1] ? -1 : 1;
713
+ if (depth === 0) return tags.lastIndex;
714
+ }
715
+ return null;
716
+ }
717
+ function closesLine(text, at) {
718
+ return LINE_END_OR_BLOCK.test(text.slice(at));
719
+ }
720
+ function cutBlocks(text) {
721
+ let kept = "";
722
+ let keptFrom = 0;
723
+ CUT_OPEN.lastIndex = 0;
724
+ for (let open = CUT_OPEN.exec(text); open; open = CUT_OPEN.exec(text)) {
725
+ if (!opensBlock(text, open.index, keptFrom)) continue;
726
+ const end = balancedClose(text, open[1], CUT_OPEN.lastIndex);
727
+ if (end === null || !closesLine(text, end)) continue;
728
+ kept += text.slice(keptFrom, open.index);
729
+ keptFrom = end;
730
+ CUT_OPEN.lastIndex = end;
731
+ }
732
+ return kept + text.slice(keptFrom);
733
+ }
734
+ var INTERRUPTION = /\[Request interrupted by user[^\]\n]*\]/g;
735
+ var HUMAN_WRAPPERS = ["command-args", "bash-input"].map((tag) => new RegExp(`</?${tag}>`, "g"));
736
+ var WHOLE_TURN_MARKERS = Object.keys(INJECTED_MARKERS).filter((name) => INJECTED_MARKERS[name].close === null);
737
+ var SPAWN_BRIEF_FRAMING = [
738
+ /^# Predecessor: .*\n\nYou are taking over from /,
739
+ /Commit your work there; nothing outside it is yours to change\.\s*$/,
740
+ /You may be sharing it with other agents, so stay inside the paths you were given\.\s*$/
741
+ ];
742
+ function isWholeTurnInjected(text) {
743
+ const trimmed = text.trim();
744
+ return WHOLE_TURN_MARKERS.some((name) => hasMarker(trimmed, name)) || SPAWN_BRIEF_FRAMING.some((re) => re.test(trimmed));
745
+ }
746
+ function stripInjected(text) {
747
+ let kept = cutBlocks(text);
748
+ for (const re of [INTERRUPTION, ...HUMAN_WRAPPERS]) kept = kept.replace(re, "");
749
+ if (isWholeTurnInjected(kept)) return "";
750
+ return kept.replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
751
+ }
752
+
628
753
  // src/apply.ts
629
- import { sessionRef } from "@titan-design/session-read";
754
+ import { RELATIONS, sessionRef } from "@titan-design/session-read";
630
755
  function applyDelta(graph, transcriptId, delta, source = {}) {
631
756
  graph.db.transaction(() => {
632
757
  applyFacts(graph.db, transcriptId, delta);
@@ -635,6 +760,8 @@ function applyDelta(graph, transcriptId, delta, source = {}) {
635
760
  applyAssets(graph.db, delta);
636
761
  applyPhases(graph.db, transcriptId, delta);
637
762
  applyLinkedRows(graph, transcriptId, delta);
763
+ applySpans(graph, transcriptId, delta, source.indexSdkPrompts ?? false);
764
+ applyReviewVerdicts(graph.db, transcriptId, delta);
638
765
  applyAudit(graph.db, transcriptId, delta);
639
766
  })();
640
767
  }
@@ -702,6 +829,43 @@ function pick(row, sql) {
702
829
  const names = new Set([...sql.matchAll(/@(\w+)/g)].map((m) => m[1]));
703
830
  return Object.fromEntries([...names].map((name) => [name, row[name] ?? null]));
704
831
  }
832
+ var UPSERT_CHAT_VERDICT = `
833
+ INSERT INTO ${REVIEW_TABLE} (source_key, surface, verdict, ts, session_id, transcript_id, repo, repo_hint, cwd_repo, number)
834
+ VALUES (@sourceKey, 'chat', @verdict, @ts, @sessionId, @transcriptId, @repo, @repoHint, @cwdRepo, @number)
835
+ ON CONFLICT (source_key) DO UPDATE SET verdict = excluded.verdict, ts = excluded.ts, session_id = excluded.session_id,
836
+ transcript_id = excluded.transcript_id, repo = excluded.repo, repo_hint = excluded.repo_hint,
837
+ cwd_repo = excluded.cwd_repo, number = excluded.number`;
838
+ function applyReviewVerdicts(db, transcriptId, delta) {
839
+ const upsert = db.prepare(UPSERT_CHAT_VERDICT);
840
+ const keys = chatVerdictKeys(delta.reviewVerdicts);
841
+ delta.reviewVerdicts.forEach((v, i) => {
842
+ upsert.run({
843
+ sourceKey: keys[i],
844
+ verdict: v.verdict,
845
+ ts: v.ts,
846
+ sessionId: v.sessionId,
847
+ transcriptId,
848
+ repo: v.repo,
849
+ repoHint: v.repoHint,
850
+ cwdRepo: v.cwdRepo,
851
+ number: v.number
852
+ });
853
+ });
854
+ }
855
+ var hasOrdinal = (v) => Number.isInteger(v.ordinal) && v.ordinal >= 0;
856
+ function chatVerdictKeys(verdicts) {
857
+ const used = /* @__PURE__ */ new Map();
858
+ const usedBy = (id) => used.get(id) ?? used.set(id, /* @__PURE__ */ new Set()).get(id);
859
+ for (const v of verdicts) if (hasOrdinal(v)) usedBy(v.toolUseId).add(v.ordinal);
860
+ return verdicts.map((v) => {
861
+ if (hasOrdinal(v)) return `chat:${v.toolUseId}:${v.ordinal}`;
862
+ const taken = usedBy(v.toolUseId);
863
+ let n = 0;
864
+ while (taken.has(n)) n += 1;
865
+ taken.add(n);
866
+ return `chat:${v.toolUseId}:${n}`;
867
+ });
868
+ }
705
869
  function applyPhases(db, transcriptId, delta) {
706
870
  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
871
  const close = db.prepare("UPDATE permission_phase SET t_invalid = ? WHERE phase_id = ?");
@@ -734,11 +898,22 @@ function applyLinkedRows(graph, transcriptId, delta) {
734
898
  for (const s of delta.subagents) upsertSubagent.run(s.agentRef, s.sessionId, s.agentType, s.label, s.ts || null, factId(s.byteOffset));
735
899
  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
900
  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) });
901
+ for (const e of delta.edges) assertTranscriptEdge(graph, { sourceRef: e.sourceRef, relation: e.relation, targetRef: e.targetRef, tValid: e.ts, factId: factId(e.byteOffset) });
902
+ }
903
+ function applySpans(graph, transcriptId, delta, indexSdkPrompts) {
904
+ const injectedLines = new Set(delta.inbound.filter((i) => isUntypedPrompt(i, indexSdkPrompts)).map((i) => i.byteOffset));
738
905
  for (const span of delta.spans) {
739
- graph.spans.index({ ownerRef: sessionRef(span.sessionId), field: span.field, sourceId: transcriptId, byteOffset: span.byteOffset, byteLength: span.byteLength }, span.text);
906
+ const isPrompt = span.field === "prompt";
907
+ const text = !isPrompt ? span.text : injectedLines.has(span.byteOffset) ? "" : stripInjected(span.text);
908
+ if (isPrompt && !text) continue;
909
+ graph.spans.index({ ownerRef: sessionRef(span.sessionId), field: span.field, sourceId: transcriptId, byteOffset: span.byteOffset, byteLength: span.byteLength }, text);
740
910
  }
741
911
  }
912
+ function assertTranscriptEdge(graph, edge) {
913
+ const existing = edge.relation === RELATIONS.RAN ? graph.edges.current(edge.sourceRef, edge.relation, edge.targetRef) : void 0;
914
+ if (existing?.attrs?.via === "origin") graph.edges.supersede(edge);
915
+ else graph.edges.assert(edge);
916
+ }
742
917
  function factIdFor(db, transcriptId, byteOffset) {
743
918
  const row = db.prepare("SELECT fact_id FROM fact WHERE transcript_id = ? AND byte_offset = ?").get(transcriptId, byteOffset);
744
919
  return row?.fact_id ?? null;
@@ -755,7 +930,7 @@ function purgeTranscript(graph, transcriptId) {
755
930
  graph.db.prepare(`DELETE FROM "${table}" WHERE session_id IN (SELECT session_id FROM session WHERE transcript_id = ?)`).run(transcriptId);
756
931
  }
757
932
  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]) {
933
+ for (const table of [...AUDIT_TABLES, FACET_TABLE, REVIEW_TABLE]) {
759
934
  graph.db.prepare(`DELETE FROM "${table}" WHERE transcript_id = ?`).run(transcriptId);
760
935
  }
761
936
  graph.db.prepare("DELETE FROM fact WHERE transcript_id = ?").run(transcriptId);
@@ -921,18 +1096,126 @@ import { promises as fs } from "fs";
921
1096
  import { contentHash, resumePoint } from "@titan-design/locator";
922
1097
  import { TranscriptParseError as TranscriptParseError2, extractTranscript as extractTranscript2 } from "@titan-design/session-read";
923
1098
 
1099
+ // src/review-rounds.ts
1100
+ import { RELATIONS as RELATIONS2 } from "@titan-design/session-read";
1101
+ var isReviewerProfile = (profile) => profile === "reviewer" || profile.endsWith("-reviewer");
1102
+ function countRounds(changesRequestedAt, commitTimes) {
1103
+ const commits = parseCommitTimes(commitTimes);
1104
+ return commits ? answeredHeads(changesRequestedAt, commits).heads.size : null;
1105
+ }
1106
+ function parseCommitTimes(commitTimes) {
1107
+ const commits = commitTimes.map(Date.parse);
1108
+ return commits.some(Number.isNaN) ? null : commits;
1109
+ }
1110
+ function answeredHeads(reviewTimes, commits) {
1111
+ const heads = /* @__PURE__ */ new Set();
1112
+ let ignored = 0;
1113
+ for (const ts of reviewTimes) {
1114
+ const at = Date.parse(ts);
1115
+ if (Number.isNaN(at)) {
1116
+ ignored += 1;
1117
+ continue;
1118
+ }
1119
+ const head = commits.filter((c) => c <= at).length;
1120
+ if (head < commits.length) heads.add(head);
1121
+ }
1122
+ return { heads, ignored };
1123
+ }
1124
+ function projectReviewRounds(graph, options = {}) {
1125
+ const isReviewer = options.isReviewerProfile ?? isReviewerProfile;
1126
+ return graph.db.transaction(() => {
1127
+ const resolved = resolveChatVerdicts(graph.db);
1128
+ const invalidTimes = writeRounds(graph.db, isReviewer);
1129
+ const { n } = graph.db.prepare(`SELECT count(*) AS n FROM ${REVIEW_TABLE} WHERE surface = 'chat' AND pr_ref IS NULL`).get();
1130
+ return { resolved, unresolved: n, invalidTimes };
1131
+ })();
1132
+ }
1133
+ function resolveChatVerdicts(db) {
1134
+ 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();
1135
+ const update = db.prepare(`UPDATE ${REVIEW_TABLE} SET pr_ref = ? WHERE source_key = ?`);
1136
+ let resolved = 0;
1137
+ for (const row of rows) {
1138
+ const prRef = resolvePrRef(db, row);
1139
+ if (prRef) resolved += update.run(prRef, row.source_key).changes;
1140
+ }
1141
+ return resolved;
1142
+ }
1143
+ function resolvePrRef(db, row) {
1144
+ const candidates = db.prepare("SELECT pr_ref, repo FROM pr WHERE number = ? AND repo IS NOT NULL").all(row.number);
1145
+ if (row.repo) return candidates.find((c) => c.repo === row.repo)?.pr_ref ?? null;
1146
+ const hinted = candidates.filter((c) => sameRepoName(c.repo, row.repo_hint));
1147
+ if (hinted.length === 1) return hinted[0].pr_ref;
1148
+ const pool = hinted.length > 0 ? hinted : candidates;
1149
+ const linked = familyLinks(db, row.session_id);
1150
+ const bySibling = pool.filter((c) => linked.has(c.pr_ref));
1151
+ if (bySibling.length === 1) return bySibling[0].pr_ref;
1152
+ const byCwd = (bySibling.length > 0 ? bySibling : pool).filter((c) => sameRepoName(c.repo, row.cwd_repo));
1153
+ return byCwd.length === 1 ? byCwd[0].pr_ref : null;
1154
+ }
1155
+ function sameRepoName(repo, name) {
1156
+ return name !== null && repo.split("/").pop().toLowerCase() === name.toLowerCase();
1157
+ }
1158
+ var FAMILY_LINKS = `
1159
+ WITH parent(id) AS (
1160
+ SELECT parent_session_id FROM session_origin WHERE session_id = @sessionId AND parent_session_id IS NOT NULL
1161
+ UNION SELECT session_id FROM subagent WHERE child_session_id = @sessionId AND session_id IS NOT NULL
1162
+ ), family(id) AS (
1163
+ SELECT @sessionId UNION SELECT id FROM parent
1164
+ UNION SELECT session_id FROM session_origin WHERE parent_session_id IN (SELECT id FROM parent)
1165
+ UNION SELECT child_session_id FROM subagent WHERE session_id IN (SELECT id FROM parent) AND child_session_id IS NOT NULL
1166
+ UNION SELECT substr(target_ref, 9) FROM "${KIT.edge}" WHERE relation = '${RELATIONS2.SPAWNED}' AND t_expired IS NULL
1167
+ AND target_ref LIKE 'session:%' AND source_ref IN (SELECT 'session:' || id FROM parent)
1168
+ )
1169
+ SELECT DISTINCT target_ref FROM "${KIT.edge}" WHERE relation = '${RELATIONS2.LINKED}' AND t_expired IS NULL
1170
+ AND source_ref IN (SELECT 'session:' || id FROM family)`;
1171
+ function familyLinks(db, sessionId) {
1172
+ if (!sessionId) return /* @__PURE__ */ new Set();
1173
+ const rows = db.prepare(FAMILY_LINKS).all({ sessionId });
1174
+ return new Set(rows.map((r) => r.target_ref));
1175
+ }
1176
+ var CHANGES_REQUESTED = `
1177
+ 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
1178
+ WHERE r.verdict = 'changes_requested' AND r.pr_ref IS NOT NULL`;
1179
+ function writeRounds(db, isReviewer) {
1180
+ const byPr = /* @__PURE__ */ new Map();
1181
+ for (const review of db.prepare(CHANGES_REQUESTED).all()) {
1182
+ if (review.surface === "chat" && !(review.profile && isReviewer(review.profile))) continue;
1183
+ byPr.set(review.pr_ref, [...byPr.get(review.pr_ref) ?? [], review]);
1184
+ }
1185
+ const update = db.prepare("UPDATE pr SET review_rounds_chat = ?, review_rounds = ? WHERE pr_ref = ?");
1186
+ let invalidTimes = 0;
1187
+ for (const pr of db.prepare("SELECT pr_ref, commit_times, review_rounds_gh FROM pr").all()) {
1188
+ const { chat, total, invalid } = roundsFor(pr, byPr.get(pr.pr_ref) ?? []);
1189
+ update.run(chat, total, pr.pr_ref);
1190
+ invalidTimes += invalid;
1191
+ }
1192
+ return invalidTimes;
1193
+ }
1194
+ function roundsFor(pr, reviews) {
1195
+ const commits = pr.commit_times === null ? null : parseCommitTimes(JSON.parse(pr.commit_times));
1196
+ if (!commits) return { chat: null, total: pr.review_rounds_gh, invalid: pr.commit_times === null ? 0 : 1 };
1197
+ const timesOn = (surface) => reviews.filter((r) => r.surface === surface).map((r) => r.ts);
1198
+ const forge = answeredHeads(timesOn("github"), commits);
1199
+ const chat = answeredHeads(timesOn("chat"), commits);
1200
+ const chatOnly = [...chat.heads].filter((head) => !forge.heads.has(head)).length;
1201
+ return { chat: chat.heads.size, total: (pr.review_rounds_gh ?? 0) + chatOnly, invalid: forge.ignored + chat.ignored };
1202
+ }
1203
+
924
1204
  // src/outcomes.ts
925
1205
  var NO_PR_OUTCOMES = Object.freeze({ requested: 0, applied: 0, failed: false });
926
1206
  var PRS_NEEDING_OUTCOME = `
927
1207
  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`;
1208
+ WHERE repo IS NOT NULL AND number IS NOT NULL
1209
+ AND (outcome_checked_at IS NULL OR state IS NOT 'merged' OR commit_times IS NULL)
1210
+ ORDER BY CASE WHEN outcome_checked_at IS NULL THEN 0 WHEN state IS NOT 'merged' THEN 1 ELSE 2 END, pr_ref`;
930
1211
  var APPLY_OUTCOME = `
931
1212
  UPDATE pr SET
932
1213
  state = CASE WHEN state = 'merged' THEN state ELSE COALESCE(lower(@state), state) END,
933
1214
  merged_at = COALESCE(@mergedAt, merged_at),
934
1215
  closed_at = COALESCE(@closedAt, closed_at),
935
1216
  review_rounds = COALESCE(@reviewRounds, review_rounds),
1217
+ review_rounds_gh = COALESCE(@reviewRounds, review_rounds_gh),
1218
+ commit_times = COALESCE(@commitTimes, commit_times),
936
1219
  outcome_checked_at = @checkedAt
937
1220
  WHERE pr_ref = @prRef`;
938
1221
  function prsNeedingOutcome(graph) {
@@ -952,6 +1235,7 @@ async function enrichPrs(graph, resolver) {
952
1235
  }
953
1236
  function write(graph, resolved, checkedAt) {
954
1237
  const apply = graph.db.prepare(APPLY_OUTCOME);
1238
+ const stored = graph.db.prepare("SELECT commit_times FROM pr WHERE pr_ref = ?");
955
1239
  return graph.db.transaction(() => {
956
1240
  let applied = 0;
957
1241
  for (const [prRef, fields] of resolved) {
@@ -962,27 +1246,79 @@ function write(graph, resolved, checkedAt) {
962
1246
  state: fields.state ?? null,
963
1247
  mergedAt: fields.mergedAt ?? null,
964
1248
  closedAt: fields.closedAt ?? null,
965
- reviewRounds: fields.reviewRounds ?? null
1249
+ reviewRounds: forgeRounds(fields, () => storedCommitTimes(stored.get(prRef))),
1250
+ commitTimes: fields.commitTimes ? JSON.stringify(fields.commitTimes) : null
966
1251
  }).changes;
1252
+ if (fields.reviews) replaceForgeReviews(graph, prRef, fields.reviews);
967
1253
  }
968
1254
  return applied;
969
1255
  })();
970
1256
  }
1257
+ var isChangesRequested = (review) => review.state.toUpperCase() === "CHANGES_REQUESTED";
1258
+ function forgeRounds(fields, storedTimes) {
1259
+ if (!fields.reviews) return fields.reviewRounds ?? null;
1260
+ const commitTimes = fields.commitTimes ?? storedTimes();
1261
+ const counted = commitTimes ? countRounds(fields.reviews.filter(isChangesRequested).map((r) => r.submittedAt), commitTimes) : null;
1262
+ return counted ?? fields.reviewRounds ?? null;
1263
+ }
1264
+ function storedCommitTimes(row) {
1265
+ const times = row?.commit_times;
1266
+ return times ? JSON.parse(times) : null;
1267
+ }
1268
+ var FORGE_VERDICTS = { APPROVED: "approve", CHANGES_REQUESTED: "changes_requested" };
1269
+ var INSERT_FORGE_REVIEW = `
1270
+ INSERT INTO ${REVIEW_TABLE} (source_key, surface, verdict, ts, repo, number, pr_ref)
1271
+ SELECT 'gh:' || pr_ref || ':' || @ts, 'github', @verdict, @ts, repo, number, pr_ref FROM pr WHERE pr_ref = @prRef
1272
+ ON CONFLICT (source_key) DO UPDATE SET verdict = CASE WHEN verdict = 'changes_requested' THEN verdict ELSE excluded.verdict END`;
1273
+ function replaceForgeReviews(graph, prRef, reviews) {
1274
+ graph.db.prepare(`DELETE FROM ${REVIEW_TABLE} WHERE surface = 'github' AND pr_ref = ?`).run(prRef);
1275
+ const insert = graph.db.prepare(INSERT_FORGE_REVIEW);
1276
+ for (const review of reviews) {
1277
+ const verdict = FORGE_VERDICTS[review.state.toUpperCase()];
1278
+ if (verdict) insert.run({ prRef, verdict, ts: review.submittedAt });
1279
+ }
1280
+ }
971
1281
 
972
1282
  // src/origin.ts
973
- import { RELATIONS, sessionRef as sessionRef3 } from "@titan-design/session-read";
1283
+ import { RELATIONS as RELATIONS3, sessionRef as sessionRef3, taskRef } from "@titan-design/session-read";
1284
+ var NO_TASK_LINK = "none";
974
1285
  var NO_ORIGINS = Object.freeze({ requested: 0, applied: 0, events: 0, failed: false });
975
1286
  var SESSIONS_NEEDING_ORIGIN = `
976
1287
  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
1288
+ WHERE o.session_id IS NULL OR o.resolved_at < s.ended_at OR o.task_source IS NULL
978
1289
  ORDER BY s.session_id`;
1290
+ var ORIGIN_COLUMNS = [
1291
+ "origin_system",
1292
+ "agent_id",
1293
+ "agent_name",
1294
+ "parent_name",
1295
+ "parent_session_id",
1296
+ "profile",
1297
+ "model_alias",
1298
+ "surface",
1299
+ "isolation",
1300
+ "depth",
1301
+ "origin_kind",
1302
+ "config_dir",
1303
+ "spawn_cwd",
1304
+ "spawned_at",
1305
+ "brief_chars",
1306
+ "brief_excerpt",
1307
+ "brief_path",
1308
+ "launch_args",
1309
+ "resolved_at"
1310
+ ];
1311
+ var TASK_LINK_UPDATE = `task_source = COALESCE(excluded.task_source, task_source),
1312
+ task_ids = CASE WHEN excluded.task_source IS NOT NULL THEN excluded.task_ids ELSE task_ids END`;
979
1313
  var UPSERT_ORIGIN = `
980
- INSERT OR REPLACE INTO session_origin (session_id, origin_system, agent_id, agent_name, parent_name, parent_session_id,
1314
+ INSERT INTO session_origin (session_id, origin_system, agent_id, agent_name, parent_name, parent_session_id,
981
1315
  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)
1316
+ brief_chars, brief_excerpt, brief_path, launch_args, task_ids, task_source, resolved_at)
983
1317
  VALUES (@sessionId, @originSystem, @agentId, @agentName, @parentName, @parentSessionId,
984
1318
  @profile, @modelAlias, @surface, @isolation, @depth, @originKind, @configDir, @spawnCwd, @spawnedAt,
985
- @briefChars, @briefExcerpt, @briefPath, @launchArgs, @resolvedAt)`;
1319
+ @briefChars, @briefExcerpt, @briefPath, @launchArgs, @taskIds, @taskSource, @resolvedAt)
1320
+ ON CONFLICT (session_id) DO UPDATE SET ${ORIGIN_COLUMNS.map((c) => `${c} = excluded.${c}`).join(", ")},
1321
+ ${TASK_LINK_UPDATE}`;
986
1322
  var UPSERT_EVENT = `
987
1323
  INSERT INTO session_external_event (session_id, ts, origin_system, kind, detail)
988
1324
  VALUES (@sessionId, @ts, COALESCE(@originSystem, (SELECT origin_system FROM session_origin WHERE session_id = @sessionId), 'unknown'), @kind, @detail)
@@ -999,14 +1335,16 @@ function sessionsNeedingOrigin(graph) {
999
1335
  async function resolveOrigins(graph, resolver) {
1000
1336
  if (!resolver) return NO_ORIGINS;
1001
1337
  const sessionIds = sessionsNeedingOrigin(graph);
1338
+ let resolved = [];
1002
1339
  try {
1003
1340
  const resolution = await resolver(sessionIds);
1004
1341
  const written = writeResolution(graph, resolution, (/* @__PURE__ */ new Date()).toISOString());
1342
+ resolved = Object.keys(resolution.origins);
1005
1343
  return { requested: sessionIds.length, ...written, failed: false };
1006
1344
  } catch (err) {
1007
1345
  return { requested: sessionIds.length, applied: 0, events: 0, failed: true, error: err instanceof Error ? err.message : String(err) };
1008
1346
  } finally {
1009
- projectOrigins(graph);
1347
+ projectOrigins(graph, resolved);
1010
1348
  }
1011
1349
  }
1012
1350
  function writeResolution(graph, resolution, resolvedAt) {
@@ -1045,21 +1383,54 @@ function originRow(sessionId, o, resolvedAt) {
1045
1383
  briefChars: o.briefChars ?? null,
1046
1384
  briefExcerpt: o.briefExcerpt ?? null,
1047
1385
  briefPath: o.briefPath ?? null,
1048
- launchArgs: o.launchArgs ?? null
1386
+ launchArgs: o.launchArgs ?? null,
1387
+ ...taskLinkColumns(o)
1049
1388
  };
1050
1389
  }
1051
- function projectOrigins(graph) {
1390
+ function taskLinkColumns(o) {
1391
+ if (o.taskIds === void 0) return { taskIds: null, taskSource: null };
1392
+ if (!o.taskIds?.length) return { taskIds: null, taskSource: NO_TASK_LINK };
1393
+ return { taskIds: JSON.stringify(o.taskIds), taskSource: o.taskSource ?? NO_TASK_LINK };
1394
+ }
1395
+ function projectOrigins(graph, resolved) {
1052
1396
  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
1397
  graph.db.transaction(() => {
1054
1398
  graph.db.prepare(PROJECT_SUBAGENTS).run();
1055
1399
  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 });
1400
+ 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
1401
  }
1402
+ projectTaskLinks(graph, resolved);
1058
1403
  })();
1059
1404
  }
1405
+ var LINK_CONFIDENCE = { name: 1, "name-over-brief": 1, "brief-anchor": 0.9, "brief-paragraph": 0.6 };
1406
+ var LOWEST_CONFIDENCE = 0.6;
1407
+ function projectTaskLinks(graph, resolved) {
1408
+ const insertTask = graph.db.prepare("INSERT OR IGNORE INTO task (task_ref, task_id) VALUES (?, ?)");
1409
+ const select = graph.db.prepare("SELECT session_id, task_ids, task_source, spawned_at, resolved_at FROM session_origin WHERE session_id = ?");
1410
+ for (const sessionId of resolved) {
1411
+ const o = select.get(sessionId);
1412
+ if (!o) continue;
1413
+ const taskIds = o.task_ids ? JSON.parse(o.task_ids) : [];
1414
+ const source = sessionRef3(o.session_id);
1415
+ const attrs = { via: "origin", source: o.task_source };
1416
+ const confidence = LINK_CONFIDENCE[o.task_source ?? ""] ?? LOWEST_CONFIDENCE;
1417
+ for (const taskId of taskIds) {
1418
+ insertTask.run(taskRef(taskId), taskId);
1419
+ graph.edges.assert({ sourceRef: source, relation: RELATIONS3.RAN, targetRef: taskRef(taskId), tValid: o.spawned_at ?? o.resolved_at, attrs, confidence });
1420
+ }
1421
+ expireStaleTaskLinks(graph, source, new Set(taskIds.map(taskRef)));
1422
+ }
1423
+ }
1424
+ function expireStaleTaskLinks(graph, source, linked) {
1425
+ for (const edge of graph.edges.from(source)) {
1426
+ if (edge.relation === RELATIONS3.RAN && edge.attrs?.via === "origin" && !linked.has(edge.targetRef)) {
1427
+ graph.edges.expire(source, RELATIONS3.RAN, edge.targetRef);
1428
+ }
1429
+ }
1430
+ }
1060
1431
 
1061
1432
  // src/tasks.ts
1062
- import { taskRef } from "@titan-design/session-read";
1433
+ import { taskRef as taskRef2 } from "@titan-design/session-read";
1063
1434
  var NO_ENRICHMENT = Object.freeze({ requested: 0, applied: 0, failed: false });
1064
1435
  var UPSERT_TASK = `
1065
1436
  INSERT INTO task (task_ref, task_id, initiative, title, status, estimate) VALUES (@taskRef, @taskId, @initiative, @title, @status, @estimate)
@@ -1088,7 +1459,7 @@ function write2(graph, resolved) {
1088
1459
  for (const [taskId, fields] of resolved) {
1089
1460
  if (!fields) continue;
1090
1461
  applied += upsert.run({
1091
- taskRef: taskRef(taskId),
1462
+ taskRef: taskRef2(taskId),
1092
1463
  taskId,
1093
1464
  initiative: fields.initiative ?? null,
1094
1465
  title: fields.title ?? null,
@@ -1122,7 +1493,7 @@ async function indexTranscript(graph, transcript, options = {}) {
1122
1493
  });
1123
1494
  const rewound = point.state === "rewritten" || delta.restartedFromZero;
1124
1495
  if (rewound) purgeTranscript(graph, row.sourceId);
1125
- applyDelta(graph, row.sourceId, delta, { account: transcript.account });
1496
+ applyDelta(graph, row.sourceId, delta, { account: transcript.account, indexSdkPrompts: options.indexSdkPrompts });
1126
1497
  advanceFacet(graph.db, row.sourceId, rewound ? 0 : point.start, delta.lastByteOffset);
1127
1498
  const stat2 = await fs.stat(transcript.absolutePath);
1128
1499
  graph.transcripts.advance(transcript.displayPath, {
@@ -1143,7 +1514,7 @@ async function indexTranscript(graph, transcript, options = {}) {
1143
1514
  }
1144
1515
  }
1145
1516
  async function refreshCorpus(graph, transcripts, options = {}) {
1146
- const { resolveTasks, resolveOrigins: originResolver, resolvePrs, full, facetLimit, ...perTranscript } = options;
1517
+ const { resolveTasks, resolveOrigins: originResolver, resolvePrs, isReviewerProfile: isReviewerProfile2, full, facetLimit, ...perTranscript } = options;
1147
1518
  const counts = { indexed: 0, unchanged: 0, rewound: 0, missing: 0, quarantined: 0 };
1148
1519
  const touched = [];
1149
1520
  let facts = 0;
@@ -1159,10 +1530,11 @@ async function refreshCorpus(graph, transcripts, options = {}) {
1159
1530
  const origins = await resolveOrigins(graph, originResolver);
1160
1531
  const reconciled = reconcile(graph);
1161
1532
  const prs = await enrichPrs(graph, resolvePrs);
1533
+ const reviews = projectReviewRounds(graph, { isReviewerProfile: isReviewerProfile2 });
1162
1534
  const tasks = await enrichTasks(graph, resolveTasks, allTaskIds(graph));
1163
1535
  const markedMissing = await markMissing(graph, transcripts);
1164
1536
  const facetSummary = { facetsBackfilled: facets.backfilled, facetBacklog: facets.backlog };
1165
- return { transcripts: transcripts.length, ...counts, facts, turnsRolledUp, reconciled, tasks, origins, prs, markedMissing, ...facetSummary };
1537
+ return { transcripts: transcripts.length, ...counts, facts, turnsRolledUp, reconciled, tasks, origins, prs, reviews, markedMissing, ...facetSummary };
1166
1538
  }
1167
1539
  async function markMissing(graph, discovered) {
1168
1540
  const present = new Set(discovered.map((t) => t.displayPath));
@@ -1274,7 +1646,8 @@ function insertObservation(graph, transcriptId, o) {
1274
1646
  );
1275
1647
  }
1276
1648
  function observationText(o) {
1277
- if (o.kind === "message") return o.content.map((p) => ({ field: o.role === "user" ? "prompt" : "assistant_response", text: p.text.slice(0, SPAN_TEXT_CAP), locator: p.locator }));
1649
+ if (o.kind === "message" && o.role === "user") return o.content.map((p) => ({ field: "prompt", text: stripInjected(p.text).slice(0, SPAN_TEXT_CAP), locator: p.locator })).filter((p) => p.text);
1650
+ if (o.kind === "message") return o.content.map((p) => ({ field: "assistant_response", text: p.text.slice(0, SPAN_TEXT_CAP), locator: p.locator }));
1278
1651
  if (o.kind === "tool_call" && o.inputLocator) return [{ field: "tool_input", text: normalizedSearchText(o.input), locator: o.inputLocator }];
1279
1652
  if (o.kind === "tool_result" && o.outputLocator) return [{ field: "tool_result", text: normalizedSearchText(o.output), locator: o.outputLocator }];
1280
1653
  return [];
@@ -1377,6 +1750,10 @@ function* stagedRows(graph, table) {
1377
1750
  import { contentHash as contentHash3, prefixHash } from "@titan-design/locator";
1378
1751
  import { readSessionSourceText, readSessionText, SessionUsageAccumulator } from "@titan-design/session-read";
1379
1752
  async function readIndexedText(graph, span) {
1753
+ const text = await readSourceText(graph, span);
1754
+ return text !== null && span.field === "prompt" ? stripInjected(text) : text;
1755
+ }
1756
+ async function readSourceText(graph, span) {
1380
1757
  const row = graph.db.prepare(`SELECT n.locators FROM normalized_span n JOIN search_span s USING(span_id)
1381
1758
  WHERE s.source_id = ? AND s.byte_offset = ? AND s.field = ?`).get(span.sourceId, span.byteOffset, span.field);
1382
1759
  if (row) {
@@ -1465,22 +1842,32 @@ export {
1465
1842
  NO_ENRICHMENT,
1466
1843
  NO_ORIGINS,
1467
1844
  NO_PR_OUTCOMES,
1845
+ NO_TASK_LINK,
1468
1846
  ORIGIN_DDL,
1469
1847
  ORIGIN_MIGRATION_NAME,
1470
1848
  ORIGIN_TABLES,
1849
+ ORIGIN_TASK_LINK_MIGRATION_NAME,
1471
1850
  ORIGIN_VIEWS,
1851
+ REVIEW_DDL,
1852
+ REVIEW_TABLE,
1853
+ REVIEW_VERDICT_MIGRATION_NAME,
1472
1854
  allSessionIds,
1473
1855
  allTaskIds,
1474
1856
  applyAudit,
1475
1857
  applyDelta,
1476
1858
  backfillFacets,
1859
+ countRounds,
1477
1860
  enrichPrs,
1478
1861
  enrichTasks,
1479
1862
  indexCodexSource,
1480
1863
  indexTranscript,
1864
+ isInjectedCause,
1865
+ isReviewerProfile,
1866
+ isUntypedPrompt,
1481
1867
  normalizedSessions,
1482
1868
  normalizedUsage,
1483
1869
  openSessionGraph,
1870
+ projectReviewRounds,
1484
1871
  prsNeedingOutcome,
1485
1872
  purgeTranscript,
1486
1873
  readIndexedText,
@@ -1492,6 +1879,7 @@ export {
1492
1879
  resolveOrigins,
1493
1880
  rollupSessions,
1494
1881
  sessionsNeedingOrigin,
1882
+ stripInjected,
1495
1883
  syncPrices
1496
1884
  };
1497
1885
  //# sourceMappingURL=index.js.map