@swestash/agent-lens 0.13.2 → 0.14.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.
@@ -591,7 +591,7 @@ var CAC = class extends EventEmitter {
591
591
  var cac = (name = "") => new CAC(name);
592
592
 
593
593
  // ../core/dist/schema.js
594
- var SCHEMA_VERSION = 14;
594
+ var SCHEMA_VERSION = 15;
595
595
  var SCHEMA_SQL = (
596
596
  /* sql */
597
597
  `
@@ -688,7 +688,8 @@ CREATE TABLE IF NOT EXISTS events (
688
688
  model TEXT,
689
689
  is_sidechain INTEGER NOT NULL DEFAULT 0,
690
690
  is_meta INTEGER NOT NULL DEFAULT 0,
691
- text TEXT, -- flattened text/thinking for search + preview
691
+ text TEXT, -- visible message text, for search + preview
692
+ thinking TEXT, -- reasoning blocks, kept apart so the read path needs no parsing
692
693
  raw_json BLOB NOT NULL, -- original line, verbatim, gzip-compressed (ADR-011); read via unpackRaw()
693
694
  source_file TEXT -- archive path the canonical copy came from
694
695
  );
@@ -900,23 +901,27 @@ CREATE TABLE IF NOT EXISTS ingest_state (
900
901
  );
901
902
 
902
903
  -- Full-text search over event text (contentless external-content FTS). ------
904
+ -- Both columns are indexed: search covered reasoning back when text carried it inline, and the
905
+ -- queries are unqualified MATCHes, which span every column \u2014 so indexing thinking here is what makes
906
+ -- splitting the columns invisible to search.
903
907
  CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
904
908
  text,
909
+ thinking,
905
910
  content='events',
906
911
  content_rowid='rowid'
907
912
  );
908
913
 
909
914
  -- Keep FTS in sync with events. --------------------------------------------
910
915
  CREATE TRIGGER IF NOT EXISTS events_ai AFTER INSERT ON events BEGIN
911
- INSERT INTO events_fts(rowid, text) VALUES (new.rowid, new.text);
916
+ INSERT INTO events_fts(rowid, text, thinking) VALUES (new.rowid, new.text, new.thinking);
912
917
  END;
913
918
  CREATE TRIGGER IF NOT EXISTS events_ad AFTER DELETE ON events BEGIN
914
- INSERT INTO events_fts(events_fts, rowid, text) VALUES ('delete', old.rowid, old.text);
919
+ INSERT INTO events_fts(events_fts, rowid, text, thinking) VALUES ('delete', old.rowid, old.text, old.thinking);
915
920
  END;
916
921
  CREATE TRIGGER IF NOT EXISTS events_au AFTER UPDATE ON events
917
- WHEN old.text IS NOT new.text BEGIN
918
- INSERT INTO events_fts(events_fts, rowid, text) VALUES ('delete', old.rowid, old.text);
919
- INSERT INTO events_fts(rowid, text) VALUES (new.rowid, new.text);
922
+ WHEN old.text IS NOT new.text OR old.thinking IS NOT new.thinking BEGIN
923
+ INSERT INTO events_fts(events_fts, rowid, text, thinking) VALUES ('delete', old.rowid, old.text, old.thinking);
924
+ INSERT INTO events_fts(rowid, text, thinking) VALUES (new.rowid, new.text, new.thinking);
920
925
  END;
921
926
  `
922
927
  );
@@ -1371,14 +1376,6 @@ import { gzipSync, gunzipSync } from "node:zlib";
1371
1376
  function packRaw(json) {
1372
1377
  return gzipSync(Buffer.from(json, "utf8"));
1373
1378
  }
1374
- function unpackRaw(value) {
1375
- if (typeof value === "string")
1376
- return value;
1377
- if (value.length >= 2 && value[0] === 31 && value[1] === 139) {
1378
- return gunzipSync(value).toString("utf8");
1379
- }
1380
- return Buffer.from(value).toString("utf8");
1381
- }
1382
1379
 
1383
1380
  // ../core/dist/paths.js
1384
1381
  import { existsSync, readFileSync } from "node:fs";
@@ -2338,25 +2335,36 @@ function resetSchema(db) {
2338
2335
  // ../ingest/dist/adapters/claude-code.js
2339
2336
  import { readdirSync as readdirSync2 } from "node:fs";
2340
2337
  import { basename as basename2, join as join6 } from "node:path";
2341
- function flattenText(content) {
2338
+ function extractParts(content) {
2342
2339
  if (content == null)
2343
- return null;
2340
+ return { text: null, thinking: null };
2344
2341
  if (typeof content === "string")
2345
- return content.trim() || null;
2342
+ return { text: content.trim() || null, thinking: null };
2346
2343
  if (!Array.isArray(content))
2347
- return null;
2348
- const parts = [];
2344
+ return { text: null, thinking: null };
2345
+ const texts = [];
2346
+ const thoughts = [];
2349
2347
  for (const block of content) {
2350
2348
  if (!block || typeof block !== "object")
2351
2349
  continue;
2352
2350
  const b = block;
2353
2351
  if (b.type === "text" && typeof b.text === "string")
2354
- parts.push(b.text);
2352
+ texts.push(b.text);
2355
2353
  else if (b.type === "thinking" && typeof b.thinking === "string")
2356
- parts.push(b.thinking);
2354
+ thoughts.push(b.thinking);
2357
2355
  }
2358
- const text = parts.join("\n").trim();
2359
- return text || null;
2356
+ return { text: texts.join("\n").trim() || null, thinking: thoughts.join("\n").trim() || null };
2357
+ }
2358
+ function flattenText(content) {
2359
+ return extractParts(content).text;
2360
+ }
2361
+ function queuedPromptText(attachment) {
2362
+ if (!attachment || typeof attachment !== "object")
2363
+ return null;
2364
+ const a = attachment;
2365
+ if (a.type !== "queued_command" || a.commandMode !== "prompt")
2366
+ return null;
2367
+ return flattenText(a.prompt);
2360
2368
  }
2361
2369
  var RESULT_SUMMARY_MAX_LINES = 40;
2362
2370
  var RESULT_SUMMARY_MAX_CHARS = 2e3;
@@ -2484,7 +2492,9 @@ var ClaudeCodeAdapter = class {
2484
2492
  if (!asString(r.uuid))
2485
2493
  return { meta };
2486
2494
  const message = r.message ?? {};
2487
- const text = flattenText(message.content);
2495
+ const queued = queuedPromptText(r.attachment);
2496
+ const parts = extractParts(message.content);
2497
+ const text = parts.text ?? queued;
2488
2498
  const result = {
2489
2499
  meta,
2490
2500
  event: {
@@ -2494,12 +2504,15 @@ var ClaudeCodeAdapter = class {
2494
2504
  parent_uuid: asString(r.parentUuid),
2495
2505
  seq,
2496
2506
  type: asString(r.type) ?? "unknown",
2497
- role: asString(message.role),
2507
+ // A recovered queued prompt has no `message` to take a role from, but it *is* the user
2508
+ // speaking — without this the transcript labels their own words as a generic attachment.
2509
+ role: asString(message.role) ?? (queued ? "user" : null),
2498
2510
  timestamp: asString(r.timestamp),
2499
2511
  model: asString(message.model),
2500
2512
  is_sidechain: r.isSidechain ? 1 : 0,
2501
2513
  is_meta: r.isMeta ? 1 : 0,
2502
2514
  text,
2515
+ thinking: parts.thinking,
2503
2516
  raw_json: JSON.stringify(raw),
2504
2517
  source_file: file.path
2505
2518
  }
@@ -3596,6 +3609,24 @@ function canonicalizeProjects(db, opts = {}) {
3596
3609
 
3597
3610
  // ../ingest/dist/pipeline.js
3598
3611
  import { createHash as createHash5 } from "node:crypto";
3612
+
3613
+ // ../transcript-format/dist/index.js
3614
+ var COMMAND_OUTPUT_TAGS = ["local-command-stdout", "local-command-stderr", "command-output"];
3615
+ var TASK_NOTIFICATION_TAG = "task-notification";
3616
+ var TASK_STATUS_TAG = "status";
3617
+ var TASK_SUMMARY_TAG = "summary";
3618
+ var TASK_RESULT_TAG = "result";
3619
+ var TASK_FAILURES_TAG = "failures";
3620
+ var COMMAND_OUTPUT_ALTERNATION = COMMAND_OUTPUT_TAGS.join("|");
3621
+ var COMMAND_RESULT_CARRIER = new RegExp(`^<(?:${COMMAND_OUTPUT_ALTERNATION})>`);
3622
+ function isCommandResultCarrier(text) {
3623
+ return COMMAND_RESULT_CARRIER.test(text.trimStart());
3624
+ }
3625
+ function xmlTag(text, tag) {
3626
+ return text.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`))?.[1]?.trim() ?? null;
3627
+ }
3628
+
3629
+ // ../ingest/dist/pipeline.js
3599
3630
  function newStats() {
3600
3631
  return { files: 0, skipped: 0, malformed: 0, newEvents: 0 };
3601
3632
  }
@@ -3689,13 +3720,14 @@ function durationMs(start, end) {
3689
3720
  return Math.max(0, b - a);
3690
3721
  }
3691
3722
  function buildTurns(sessionId, rows) {
3692
- rows.sort((x, y) => (x.timestamp ?? "").localeCompare(y.timestamp ?? "") || x.uuid.localeCompare(y.uuid));
3723
+ rows.sort((x, y) => (x.timestamp ?? "").localeCompare(y.timestamp ?? "") || (x.seq ?? 0) - (y.seq ?? 0) || x.uuid.localeCompare(y.uuid));
3693
3724
  const turns = [];
3694
3725
  const eventTurn = /* @__PURE__ */ new Map();
3695
3726
  let cur = null;
3696
3727
  let seq = 0;
3697
3728
  for (const e of rows) {
3698
- const isPrompt = e.type === "user" && e.is_meta === 0 && !!e.text && e.text.trim().length > 0;
3729
+ const body = e.text?.trim() ?? "";
3730
+ const isPrompt = e.type === "user" && e.is_meta === 0 && body.length > 0 && !isCommandResultCarrier(body);
3699
3731
  if (isPrompt) {
3700
3732
  cur = {
3701
3733
  id: `${sessionId}:${seq}`,
@@ -3753,7 +3785,7 @@ function expandDirtyScope(db, dirty) {
3753
3785
  }
3754
3786
  }
3755
3787
  function rebuildTurns(db, scope, sessionIds) {
3756
- const selEvents = db.prepare("SELECT uuid, type, is_sidechain, is_meta, timestamp, model, text FROM events WHERE session_id = ?");
3788
+ const selEvents = db.prepare("SELECT uuid, seq, type, is_sidechain, is_meta, timestamp, model, text FROM events WHERE session_id = ?");
3757
3789
  const insTurn = db.prepare(`INSERT INTO turns (id, session_id, seq, user_event_uuid, prompt_preview, model, started_at, ended_at, duration_ms)
3758
3790
  VALUES (@id, @session_id, @seq, @user_event_uuid, @prompt_preview, @model, @started_at, @ended_at, @duration_ms)`);
3759
3791
  const updEventTurn = db.prepare("UPDATE events SET turn_id = ? WHERE uuid = ?");
@@ -3932,8 +3964,8 @@ function prepareStatements(db) {
3932
3964
  ON CONFLICT(file_path) DO UPDATE SET
3933
3965
  size=excluded.size, mtime_ms=excluded.mtime_ms, sha256=excluded.sha256,
3934
3966
  events_ingested=excluded.events_ingested, ingested_at=excluded.ingested_at`),
3935
- insEvent: db.prepare(`INSERT INTO events (uuid, session_id, turn_id, parent_uuid, seq, type, role, timestamp, model, is_sidechain, is_meta, text, raw_json, source_file)
3936
- VALUES (@uuid, @session_id, @turn_id, @parent_uuid, @seq, @type, @role, @timestamp, @model, @is_sidechain, @is_meta, @text, @raw_json, @source_file)
3967
+ insEvent: db.prepare(`INSERT INTO events (uuid, session_id, turn_id, parent_uuid, seq, type, role, timestamp, model, is_sidechain, is_meta, text, thinking, raw_json, source_file)
3968
+ VALUES (@uuid, @session_id, @turn_id, @parent_uuid, @seq, @type, @role, @timestamp, @model, @is_sidechain, @is_meta, @text, @thinking, @raw_json, @source_file)
3937
3969
  ON CONFLICT(uuid) DO NOTHING`),
3938
3970
  insTokens: db.prepare(
3939
3971
  // Bare ON CONFLICT (no target) so the insert is skipped on EITHER the event_uuid PK or the
@@ -4614,25 +4646,6 @@ function lastIngested(db) {
4614
4646
  const row = db.prepare("SELECT MAX(ingested_at) AS last FROM ingest_state").get();
4615
4647
  return row?.last ?? null;
4616
4648
  }
4617
- function extractParts(rawJson) {
4618
- let text = "";
4619
- let thinking = "";
4620
- try {
4621
- const content = JSON.parse(unpackRaw(rawJson))?.message?.content;
4622
- if (typeof content === "string")
4623
- text = content;
4624
- else if (Array.isArray(content)) {
4625
- for (const b of content) {
4626
- if (b?.type === "text" && typeof b.text === "string")
4627
- text += (text ? "\n" : "") + b.text;
4628
- else if (b?.type === "thinking" && typeof b.thinking === "string")
4629
- thinking += (thinking ? "\n" : "") + b.thinking;
4630
- }
4631
- }
4632
- } catch {
4633
- }
4634
- return { text: text || null, thinking: thinking || null };
4635
- }
4636
4649
  function listSources(db) {
4637
4650
  return queryAll(db, `SELECT s.id, s.label,
4638
4651
  (SELECT COUNT(*) FROM sessions x WHERE x.source_id = s.id AND x.is_sidechain = 0) AS session_count
@@ -4815,7 +4828,7 @@ function toResponseToolCall({ event_uuid: _e, error_type: _t, ...call }) {
4815
4828
  return call;
4816
4829
  }
4817
4830
  function loadEvents(db, id, toolRows) {
4818
- const eventRows = queryAll(db, `SELECT uuid, type, role, timestamp, model, is_sidechain, turn_id, raw_json
4831
+ const eventRows = queryAll(db, `SELECT uuid, type, role, timestamp, model, is_sidechain, turn_id, text, thinking
4819
4832
  FROM events WHERE session_id = ? ORDER BY timestamp, seq`, id);
4820
4833
  const toolsByEvent = /* @__PURE__ */ new Map();
4821
4834
  for (const t of toolRows) {
@@ -4823,21 +4836,18 @@ function loadEvents(db, id, toolRows) {
4823
4836
  continue;
4824
4837
  pushGrouped(toolsByEvent, t.event_uuid, t);
4825
4838
  }
4826
- return eventRows.map((e) => {
4827
- const { text, thinking } = extractParts(e.raw_json);
4828
- return {
4829
- uuid: e.uuid,
4830
- type: e.type,
4831
- role: e.role,
4832
- timestamp: e.timestamp,
4833
- model: e.model,
4834
- is_sidechain: e.is_sidechain,
4835
- turn_id: e.turn_id,
4836
- text,
4837
- thinking,
4838
- toolCalls: (toolsByEvent.get(e.uuid) ?? []).map(toResponseToolCall)
4839
- };
4840
- });
4839
+ return eventRows.map((e) => ({
4840
+ uuid: e.uuid,
4841
+ type: e.type,
4842
+ role: e.role,
4843
+ timestamp: e.timestamp,
4844
+ model: e.model,
4845
+ is_sidechain: e.is_sidechain,
4846
+ turn_id: e.turn_id,
4847
+ text: e.text,
4848
+ thinking: e.thinking,
4849
+ toolCalls: (toolsByEvent.get(e.uuid) ?? []).map(toResponseToolCall)
4850
+ }));
4841
4851
  }
4842
4852
  function loadClassification(db, id) {
4843
4853
  const cls = queryGet(db, `SELECT category, complexity_score, complexity_band, signals_json, classifier_version
@@ -4932,9 +4942,6 @@ function getSession(db, id) {
4932
4942
  file_changes: loadFileChanges(db, id)
4933
4943
  };
4934
4944
  }
4935
- function xmlTag(text, tag) {
4936
- return text.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`))?.[1]?.trim() ?? null;
4937
- }
4938
4945
  function safeJson(s) {
4939
4946
  if (!s)
4940
4947
  return null;
@@ -4975,14 +4982,18 @@ function loadAgents(db, runId) {
4975
4982
  function parseCompletion(db, wf) {
4976
4983
  if (!wf.tool_use_id || !wf.parent_session_id)
4977
4984
  return null;
4978
- const ev = queryGet(db, `SELECT raw_json FROM events
4979
- WHERE session_id = ? AND text LIKE '%<task-notification>%' AND text LIKE ?
4985
+ const ev = queryGet(db, `SELECT text FROM events
4986
+ WHERE session_id = ? AND text LIKE '%<${TASK_NOTIFICATION_TAG}>%' AND text LIKE ?
4980
4987
  ORDER BY timestamp DESC LIMIT 1`, wf.parent_session_id, `%${wf.tool_use_id}%`);
4981
4988
  if (!ev)
4982
4989
  return null;
4983
- const { text } = extractParts(ev.raw_json);
4984
- const t = text ?? "";
4985
- return { status: xmlTag(t, "status"), summary: xmlTag(t, "summary"), result: xmlTag(t, "result"), failures: xmlTag(t, "failures") };
4990
+ const t = ev.text ?? "";
4991
+ return {
4992
+ status: xmlTag(t, TASK_STATUS_TAG),
4993
+ summary: xmlTag(t, TASK_SUMMARY_TAG),
4994
+ result: xmlTag(t, TASK_RESULT_TAG),
4995
+ failures: xmlTag(t, TASK_FAILURES_TAG)
4996
+ };
4986
4997
  }
4987
4998
  function loadRunSidecar(db, runId, completion) {
4988
4999
  const wr = tableExists(db, "workflow_results") ? queryGet(db, `SELECT status, summary, default_model, result_json, phases_json, logs_json, progress_json,