agent-trace 0.2.9 → 0.2.11

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.
Files changed (2) hide show
  1. package/agent-trace.cjs +158 -7
  2. package/package.json +1 -1
package/agent-trace.cjs CHANGED
@@ -24122,6 +24122,10 @@ th{color:var(--text-dim);font-size:10px;text-transform:uppercase;letter-spacing:
24122
24122
  .outcome-prs{border-top:1px solid var(--panel-border);padding-top:6px;margin-top:4px}
24123
24123
  .outcome-pr{display:flex;align-items:center;gap:8px;padding:3px 0;font-size:12px}
24124
24124
  .pr-badge{font-size:10px;padding:1px 5px;border-radius:3px;font-weight:600;text-transform:uppercase;border:1px solid rgba(74,222,128,.3);color:var(--green)}
24125
+ .pr-badge.open{border-color:rgba(34,211,238,.3);color:var(--cyan)}
24126
+ .pr-badge.merged{border-color:rgba(192,132,252,.3);color:var(--purple)}
24127
+ .pr-badge.closed{border-color:rgba(248,113,113,.3);color:var(--red)}
24128
+ .pr-badge.draft{border-color:rgba(102,102,102,.3);color:var(--text-muted)}
24125
24129
  .pr-label{color:var(--cyan);font-weight:600}
24126
24130
  .pr-repo{color:var(--text-muted)}
24127
24131
  .pr-link{color:var(--text-dim);text-decoration:none;font-size:11px}
@@ -24470,7 +24474,7 @@ function renderReplay() {
24470
24474
  }
24471
24475
  if (replay.pullRequests.length > 0) {
24472
24476
  h += '<div class="outcome-prs">';
24473
- replay.pullRequests.forEach(function(pr){ h += '<div class="outcome-pr"><span class="pr-badge">' + esc(pr.state) + '</span><span class="pr-label">PR #' + pr.prNumber + '</span><span class="pr-repo">' + esc(pr.repo) + '</span>' + (pr.url ? '<a class="pr-link" href="' + esc(pr.url) + '" target="_blank" rel="noopener noreferrer">' + esc(pr.url) + '</a>' : '') + '</div>'; });
24477
+ replay.pullRequests.forEach(function(pr){ h += '<div class="outcome-pr"><span class="pr-badge ' + esc(pr.state) + '">' + esc(pr.state) + '</span><span class="pr-label">PR #' + pr.prNumber + '</span><span class="pr-repo">' + esc(pr.repo) + '</span>' + (pr.url ? '<a class="pr-link" href="' + esc(pr.url) + '" target="_blank" rel="noopener noreferrer">' + esc(pr.url) + '</a>' : '') + '</div>'; });
24474
24478
  h += '</div>';
24475
24479
  }
24476
24480
  h += '</div>';
@@ -25073,6 +25077,18 @@ CREATE TABLE IF NOT EXISTS commits (
25073
25077
  );
25074
25078
 
25075
25079
  CREATE INDEX IF NOT EXISTS idx_commits_session ON commits(session_id);
25080
+
25081
+ CREATE TABLE IF NOT EXISTS pull_requests (
25082
+ session_id TEXT NOT NULL,
25083
+ repo TEXT NOT NULL,
25084
+ pr_number INTEGER NOT NULL,
25085
+ state TEXT NOT NULL DEFAULT 'open',
25086
+ url TEXT,
25087
+ merged_at TEXT,
25088
+ PRIMARY KEY (session_id, repo, pr_number)
25089
+ );
25090
+
25091
+ CREATE INDEX IF NOT EXISTS idx_pull_requests_session ON pull_requests(session_id);
25076
25092
  `;
25077
25093
  function toJsonArray(value) {
25078
25094
  return JSON.stringify(value);
@@ -25255,6 +25271,37 @@ var SqliteClient = class {
25255
25271
  });
25256
25272
  transaction(rows);
25257
25273
  }
25274
+ async upsertPullRequests(rows) {
25275
+ if (rows.length === 0) return;
25276
+ const upsert = this.db.prepare(`
25277
+ INSERT INTO pull_requests
25278
+ (session_id, repo, pr_number, state, url, merged_at)
25279
+ VALUES (?, ?, ?, ?, ?, ?)
25280
+ ON CONFLICT(session_id, repo, pr_number) DO UPDATE SET
25281
+ state = excluded.state,
25282
+ url = COALESCE(excluded.url, pull_requests.url),
25283
+ merged_at = COALESCE(excluded.merged_at, pull_requests.merged_at)
25284
+ `);
25285
+ const transaction = this.db.transaction((prRows) => {
25286
+ for (const row of prRows) {
25287
+ upsert.run(
25288
+ row.session_id,
25289
+ row.repo,
25290
+ row.pr_number,
25291
+ row.state,
25292
+ row.url,
25293
+ row.merged_at
25294
+ );
25295
+ }
25296
+ });
25297
+ transaction(rows);
25298
+ }
25299
+ listPullRequestsBySessionId(sessionId) {
25300
+ const rows = this.db.prepare(
25301
+ "SELECT session_id, repo, pr_number, state, url, merged_at FROM pull_requests WHERE session_id = ? ORDER BY pr_number ASC"
25302
+ ).all(sessionId);
25303
+ return rows;
25304
+ }
25258
25305
  listCommitsBySessionId(sessionId) {
25259
25306
  const rows = this.db.prepare(
25260
25307
  "SELECT sha, session_id, prompt_id, message, lines_added, lines_removed, committed_at FROM commits WHERE session_id = ? ORDER BY committed_at ASC"
@@ -25972,6 +26019,19 @@ function toPostgresCommitRow(trace, commit) {
25972
26019
  function toPostgresCommitRows(trace) {
25973
26020
  return trace.git.commits.map((commit) => toPostgresCommitRow(trace, commit));
25974
26021
  }
26022
+ function toPostgresPullRequestRow(trace, pr) {
26023
+ return {
26024
+ session_id: trace.sessionId,
26025
+ repo: pr.repo,
26026
+ pr_number: pr.prNumber,
26027
+ state: pr.state,
26028
+ url: toNullableString(pr.url),
26029
+ merged_at: toNullableString(pr.mergedAt)
26030
+ };
26031
+ }
26032
+ function toPostgresPullRequestRows(trace) {
26033
+ return trace.git.pullRequests.map((pr) => toPostgresPullRequestRow(trace, pr));
26034
+ }
25975
26035
  function dedupeBySessionId(rows) {
25976
26036
  const bySession = /* @__PURE__ */ new Map();
25977
26037
  rows.forEach((row) => {
@@ -25986,6 +26046,13 @@ function dedupeBySha(rows) {
25986
26046
  });
25987
26047
  return [...bySha.values()];
25988
26048
  }
26049
+ function dedupePullRequests(rows) {
26050
+ const byKey = /* @__PURE__ */ new Map();
26051
+ rows.forEach((row) => {
26052
+ byKey.set(`${row.session_id}:${row.repo}:${String(row.pr_number)}`, row);
26053
+ });
26054
+ return [...byKey.values()];
26055
+ }
25989
26056
  var PostgresSessionWriter = class {
25990
26057
  client;
25991
26058
  constructor(client) {
@@ -26003,9 +26070,11 @@ var PostgresSessionWriter = class {
26003
26070
  }
26004
26071
  const sessions = dedupeBySessionId(traces.map(toPostgresSessionRow));
26005
26072
  const commitRows = dedupeBySha(traces.flatMap((trace) => toPostgresCommitRows(trace)));
26073
+ const prRows = dedupePullRequests(traces.flatMap((trace) => toPostgresPullRequestRows(trace)));
26006
26074
  const sessionsPromise = this.client.upsertSessions(sessions);
26007
26075
  const commitsPromise = this.client.upsertCommits(commitRows);
26008
- await Promise.all([sessionsPromise, commitsPromise]);
26076
+ const prsPromise = this.client.upsertPullRequests(prRows);
26077
+ await Promise.all([sessionsPromise, commitsPromise, prsPromise]);
26009
26078
  return {
26010
26079
  writtenSessions: sessions.length,
26011
26080
  writtenCommits: commitRows.length
@@ -28222,6 +28291,7 @@ var InMemoryRuntimeClickHouseClient = class {
28222
28291
  var InMemoryRuntimePostgresClient = class {
28223
28292
  sessionsById = /* @__PURE__ */ new Map();
28224
28293
  commitsBySha = /* @__PURE__ */ new Map();
28294
+ pullRequestsByKey = /* @__PURE__ */ new Map();
28225
28295
  async upsertSessions(rows) {
28226
28296
  rows.forEach((row) => {
28227
28297
  this.sessionsById.set(row.session_id, row);
@@ -28232,12 +28302,20 @@ var InMemoryRuntimePostgresClient = class {
28232
28302
  this.commitsBySha.set(row.sha, row);
28233
28303
  });
28234
28304
  }
28305
+ async upsertPullRequests(rows) {
28306
+ rows.forEach((row) => {
28307
+ this.pullRequestsByKey.set(`${row.session_id}:${row.repo}:${String(row.pr_number)}`, row);
28308
+ });
28309
+ }
28235
28310
  listSessions() {
28236
28311
  return [...this.sessionsById.values()];
28237
28312
  }
28238
28313
  listCommits() {
28239
28314
  return [...this.commitsBySha.values()];
28240
28315
  }
28316
+ listPullRequests() {
28317
+ return [...this.pullRequestsByKey.values()];
28318
+ }
28241
28319
  };
28242
28320
  var InMemoryRuntimeSessionTraceClient = class {
28243
28321
  rows = [];
@@ -28545,15 +28623,35 @@ function toUpdatedTrace(existing, envelope) {
28545
28623
  const prUrl = readString4(payload, ["pr_url", "prUrl"]);
28546
28624
  const prRepo = readString4(payload, ["pr_repo", "prRepo"]);
28547
28625
  const prNumberRaw = readNumber3(payload, ["pr_number", "prNumber"]);
28626
+ const prState = readString4(payload, ["pr_state", "prState"]) ?? "open";
28627
+ const prMergedAt = readString4(payload, ["pr_merged_at", "prMergedAt"]);
28548
28628
  if (prUrl !== void 0 && prRepo !== void 0 && prNumberRaw !== void 0) {
28549
- const alreadyTracked = existingPullRequests.some((pr) => pr.prNumber === prNumberRaw && pr.repo === prRepo);
28550
- if (!alreadyTracked) {
28629
+ const trackedIndex = existingPullRequests.findIndex((pr) => pr.prNumber === prNumberRaw && pr.repo === prRepo);
28630
+ if (trackedIndex === -1) {
28551
28631
  existingPullRequests.push({
28552
28632
  repo: prRepo,
28553
28633
  prNumber: prNumberRaw,
28554
- state: "open",
28555
- url: prUrl
28634
+ state: prState,
28635
+ url: prUrl,
28636
+ ...prState === "merged" && prMergedAt !== void 0 ? { mergedAt: prMergedAt } : {}
28556
28637
  });
28638
+ } else if (prState !== "open") {
28639
+ const tracked = existingPullRequests[trackedIndex];
28640
+ existingPullRequests[trackedIndex] = {
28641
+ ...tracked,
28642
+ state: prState,
28643
+ ...prState === "merged" && prMergedAt !== void 0 ? { mergedAt: prMergedAt } : {}
28644
+ };
28645
+ }
28646
+ } else if (prState !== "open" && prRepo !== void 0 && prNumberRaw !== void 0) {
28647
+ const trackedIndex = existingPullRequests.findIndex((pr) => pr.prNumber === prNumberRaw && pr.repo === prRepo);
28648
+ if (trackedIndex !== -1) {
28649
+ const tracked = existingPullRequests[trackedIndex];
28650
+ existingPullRequests[trackedIndex] = {
28651
+ ...tracked,
28652
+ state: prState,
28653
+ ...prState === "merged" && prMergedAt !== void 0 ? { mergedAt: prMergedAt } : {}
28654
+ };
28557
28655
  }
28558
28656
  }
28559
28657
  return {
@@ -28827,10 +28925,21 @@ function hydrateFromSqlite(runtime, sqlite, limit, eventLimit) {
28827
28925
  const extra = commits.filter((c) => !pgShas.has(c.sha));
28828
28926
  commits = [...mapped, ...extra];
28829
28927
  }
28928
+ let pullRequests = trace.git.pullRequests;
28929
+ const pgPrs = sqlite.listPullRequestsBySessionId(row.session_id);
28930
+ if (pgPrs.length > 0) {
28931
+ pullRequests = pgPrs.map((pr) => ({
28932
+ repo: pr.repo,
28933
+ prNumber: pr.pr_number,
28934
+ state: pr.state,
28935
+ ...pr.url !== null ? { url: pr.url } : {},
28936
+ ...pr.merged_at !== null ? { mergedAt: pr.merged_at } : {}
28937
+ }));
28938
+ }
28830
28939
  let hydratedTrace = {
28831
28940
  ...trace,
28832
28941
  timeline,
28833
- git: { ...trace.git, commits }
28942
+ git: { ...trace.git, commits, pullRequests }
28834
28943
  };
28835
28944
  if (hydratedTrace.metrics.totalCostUsd === 0 && (hydratedTrace.metrics.totalInputTokens > 0 || hydratedTrace.metrics.totalOutputTokens > 0) && hydratedTrace.metrics.modelsUsed.length > 0) {
28836
28945
  const model = String(hydratedTrace.metrics.modelsUsed[0]);
@@ -29426,6 +29535,40 @@ function extractPrUrl(payload) {
29426
29535
  if (prUrlMatch !== null && prUrlMatch[0] !== void 0) return prUrlMatch[0];
29427
29536
  return void 0;
29428
29537
  }
29538
+ function extractPrState(payload) {
29539
+ const record = payload;
29540
+ const explicit = readString5(record, "pr_state") ?? readString5(record, "prState");
29541
+ if (explicit !== void 0) return explicit;
29542
+ const output = readString5(record, "tool_response") ?? readString5(record, "toolResponse") ?? readString5(record, "stdout") ?? readString5(record, "output");
29543
+ const command = pickCommand2(payload);
29544
+ const combined = [command, output].filter((s) => s !== void 0).join("\n");
29545
+ if (combined.length === 0) return void 0;
29546
+ const isGhPr = /\bgh\s+pr\b/.test(combined);
29547
+ if (!isGhPr) return void 0;
29548
+ const jsonState = combined.match(/"state"\s*:\s*"(MERGED|CLOSED|OPEN|DRAFT)"/i);
29549
+ if (jsonState?.[1] !== void 0) {
29550
+ const s = jsonState[1].toLowerCase();
29551
+ return s === "open" ? "open" : s;
29552
+ }
29553
+ const isDraft = /"isDraft"\s*:\s*true/i.test(combined);
29554
+ if (isDraft) return "draft";
29555
+ if (/\bgh\s+pr\s+merge\b/.test(combined) && output !== void 0 && !/error|failed|not merged/i.test(output)) {
29556
+ return "merged";
29557
+ }
29558
+ if (/\bMerged\b/.test(combined)) return "merged";
29559
+ if (/\bClosed\b/.test(combined)) return "closed";
29560
+ if (/\bDraft\b/.test(combined)) return "draft";
29561
+ return void 0;
29562
+ }
29563
+ function extractPrMergedAt(payload) {
29564
+ const record = payload;
29565
+ const explicit = readString5(record, "pr_merged_at") ?? readString5(record, "prMergedAt");
29566
+ if (explicit !== void 0) return explicit;
29567
+ const output = readString5(record, "tool_response") ?? readString5(record, "toolResponse") ?? readString5(record, "stdout") ?? readString5(record, "output");
29568
+ if (output === void 0) return void 0;
29569
+ const match = output.match(/"mergedAt"\s*:\s*"([^"]+)"/);
29570
+ return match?.[1] ?? void 0;
29571
+ }
29429
29572
  function parsePrFromUrl(url) {
29430
29573
  const match = url.match(/https:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)/);
29431
29574
  if (match === null || match[1] === void 0 || match[2] === void 0) return void 0;
@@ -29831,6 +29974,14 @@ function enrichHookPayloadWithGitContext(payload, provider, baselineStore, now)
29831
29974
  }
29832
29975
  }
29833
29976
  }
29977
+ const prState = extractPrState(payload);
29978
+ if (prState !== void 0) {
29979
+ patch["pr_state"] = prState;
29980
+ }
29981
+ const prMergedAt = extractPrMergedAt(payload);
29982
+ if (prMergedAt !== void 0) {
29983
+ patch["pr_merged_at"] = prMergedAt;
29984
+ }
29834
29985
  if (Object.keys(patch).length === 0) {
29835
29986
  return {
29836
29987
  payload,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-trace",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "Self-hosted observability for AI coding agents. One command, zero config.",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {