@faithfulalabi/agent-lens 0.4.1 → 0.6.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.
@@ -2,7 +2,7 @@ import { join } from 'node:path';
2
2
  import { buildDoctorReport, readCronLogStatus, resolveDataDir, } from '../../archive/index.js';
3
3
  import { CACHE_DB_FILE, openReadOnlyDb } from '../../db/open.js';
4
4
  import { readDriftRows, readHealthCounts, readMeta } from '../../db/read.js';
5
- import { aggregateDrift } from '../../server/drift-report.js';
5
+ import { aggregateDrift, mergeBucket } from '../../server/drift-report.js';
6
6
  import { parseStringFlag } from './archive.js';
7
7
  const MAX_LISTED = 20;
8
8
  export const COVERAGE_GAP_STATEMENT = 'agent-lens can only archive what exists while it runs — a gap in uptime is a gap in the record.';
@@ -17,6 +17,11 @@ function listCapped(items, indent) {
17
17
  function files(n) {
18
18
  return n === 1 ? '1 file' : `${n} files`;
19
19
  }
20
+ function mergeNamed(row, key, from) {
21
+ if (from === undefined || Object.keys(from).length === 0)
22
+ return;
23
+ mergeBucket((row[key] ??= {}), from);
24
+ }
20
25
  export function readCacheStats(dataDir) {
21
26
  const dir = resolveDataDir(dataDir);
22
27
  const path = join(dir, CACHE_DB_FILE);
@@ -27,10 +32,17 @@ export function readCacheStats(dataDir) {
27
32
  return { state: 'absent', path };
28
33
  const counts = readHealthCounts(db);
29
34
  const drift = aggregateDrift(readDriftRows(db));
30
- const drifting = new Map();
35
+ const census = new Map();
36
+ for (const [version, projected] of Object.entries(drift.harness_versions)) {
37
+ census.set(version, { version, projected, drifting: 0 });
38
+ }
31
39
  for (const session of drift.sessions_with_drift) {
32
- const version = session.harness_version ?? 'unknown';
33
- drifting.set(version, (drifting.get(version) ?? 0) + 1);
40
+ const row = census.get(session.harness_version ?? 'unknown');
41
+ if (row === undefined)
42
+ continue;
43
+ row.drifting += 1;
44
+ mergeNamed(row, 'unknown_line_types', session.counts.unknown_line_types);
45
+ mergeNamed(row, 'unknown_top_level_fields', session.counts.unknown_top_level_fields);
34
46
  }
35
47
  return {
36
48
  state: 'ready',
@@ -41,13 +53,7 @@ export function readCacheStats(dataDir) {
41
53
  sessions_projected: counts.sessions_projected,
42
54
  schema_version: readMeta(db, 'schema_version'),
43
55
  projector_version: readMeta(db, 'projector_version'),
44
- harness: Object.entries(drift.harness_versions)
45
- .map(([version, projected]) => ({
46
- version,
47
- projected,
48
- drifting: drifting.get(version) ?? 0,
49
- }))
50
- .sort((a, b) => (a.version < b.version ? -1 : 1)),
56
+ harness: [...census.values()].sort((a, b) => (a.version < b.version ? -1 : 1)),
51
57
  },
52
58
  };
53
59
  }
@@ -59,6 +65,16 @@ export function readCacheStats(dataDir) {
59
65
  db.close();
60
66
  }
61
67
  }
68
+ const MAX_NAMED_DRIFT = 3;
69
+ function namedDrift(label, bucket) {
70
+ const names = Object.entries(bucket ?? {}).sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1));
71
+ if (names.length === 0)
72
+ return [];
73
+ const shown = names.slice(0, MAX_NAMED_DRIFT).map(([name, n]) => `${name} ${n}`);
74
+ if (names.length > MAX_NAMED_DRIFT)
75
+ shown.push(`… and ${names.length - MAX_NAMED_DRIFT} more`);
76
+ return [` ${label} (${names.length}): ${shown.join(', ')}`];
77
+ }
62
78
  export function formatCacheSection(cache) {
63
79
  if (cache.state === 'absent') {
64
80
  return ['', `cache: ${cache.path} — not created yet; \`agent-lens start\` builds it`];
@@ -86,6 +102,8 @@ export function formatCacheSection(cache) {
86
102
  lines.push(' drift by harness version (the census counts clean rows too):');
87
103
  for (const row of stats.harness) {
88
104
  lines.push(` ${row.version}: ${row.projected} projected, ${row.drifting} drifting`);
105
+ lines.push(...namedDrift('unknown record types', row.unknown_line_types));
106
+ lines.push(...namedDrift('unknown top-level fields', row.unknown_top_level_fields));
89
107
  }
90
108
  return lines;
91
109
  }
@@ -128,6 +128,7 @@ export function readTurns(db, session_id) {
128
128
  }
129
129
  const EVENT_COLUMNS = `id, turn_id, seq, kind, ts, request_id, block_index, name, status,
130
130
  duration_ms, duration_source, input, input_bytes, input_storage,
131
+ wire_input, wire_input_bytes, wire_input_storage,
131
132
  text, text_bytes, output_storage, spill_path, spill_bytes,
132
133
  model, tokens_in, tokens_out, tokens_cache_read, tokens_cache_write, est_cost,
133
134
  child_session_id, agent_type, agent_status, raw_type, raw_subtype`;
@@ -181,6 +182,7 @@ const SPILL_SNIPPET = `snippet(spill_fts, 3, '<mark>', '</mark>', '…', 12)`;
181
182
  function ftsPhrase(q) {
182
183
  return `"${q.replaceAll('"', '""')}"`;
183
184
  }
185
+ const SEARCHABLE = `e.kind <> 'unknown'`;
184
186
  function searchSql(scoped) {
185
187
  const scope = scoped ? ' AND e.session_id = :session' : '';
186
188
  const events = `SELECT e.session_id AS session_id, s.title AS session_title, s.project_path AS project_path,` +
@@ -188,7 +190,7 @@ function searchSql(scoped) {
188
190
  ` e.ts AS ts, ${SNIPPET} AS snippet, rank AS score` +
189
191
  ` FROM events_fts JOIN events e ON e.rowid = events_fts.rowid` +
190
192
  ` JOIN sessions s ON s.id = e.session_id` +
191
- ` WHERE events_fts MATCH :q${scope}`;
193
+ ` WHERE events_fts MATCH :q AND ${SEARCHABLE}${scope}`;
192
194
  const spills = `SELECT e.session_id, s.title, s.project_path, e.turn_id, e.id, e.seq, e.kind, e.name,` +
193
195
  ` e.ts, ${SPILL_SNIPPET}, rank` +
194
196
  ` FROM spill_fts JOIN events e ON e.id = spill_fts.event_id` +
@@ -1,4 +1,4 @@
1
- export const SCHEMA_VERSION = 3;
1
+ export const SCHEMA_VERSION = 4;
2
2
  export const SCHEMA_DDL = `-- ~/.agent-lens/cache.db
3
3
  --
4
4
  -- THIS FILE IS A CACHE. Every byte is a pure function of ~/.agent-lens/archive/** --
@@ -21,7 +21,7 @@ export const SCHEMA_DDL = `-- ~/.agent-lens/cache.db
21
21
  PRAGMA journal_mode = WAL;
22
22
  PRAGMA synchronous = NORMAL;
23
23
  PRAGMA foreign_keys = OFF; -- projections are dropped wholesale, per session, by hand
24
- PRAGMA user_version = 3; -- SCHEMA_VERSION; mismatch => unlink + recreate
24
+ PRAGMA user_version = 4; -- SCHEMA_VERSION; mismatch => unlink + recreate
25
25
 
26
26
  -- ===================================================================== sessions
27
27
  -- The file index AND the session-list row AND the projection freshness header,
@@ -227,6 +227,15 @@ CREATE TABLE events (
227
227
  -- tools.ts:118, unwitnessed by its own comment.
228
228
  -- 'line_ref' is live at exactly 1 row. Keep the
229
229
  -- arms; see task 0.12.
230
+
231
+ -- wireToolInputs[tool_use.id]: the form the harness SENT, which is not always the
232
+ -- form the model asked for. NULL means THE TWO AGREED -- never that nothing was
233
+ -- recorded. Storage words and the cap are input_storage's, written by the same
234
+ -- tools.ts:toolInput.
235
+ wire_input TEXT,
236
+ wire_input_bytes INTEGER,
237
+ wire_input_storage TEXT,
238
+
230
239
  text TEXT, -- prose / thinking / prompt / tool output
231
240
  text_bytes INTEGER,
232
241
  output_storage TEXT, -- inline | line_ref | spill | missing | absent
@@ -63,12 +63,13 @@ const INSERT_TURN_SQL = `INSERT INTO turns
63
63
  VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`;
64
64
  const INSERT_EVENT_SQL = `INSERT INTO events
65
65
  (id, session_id, turn_id, seq, kind, ts, request_id, block_index, name, status,
66
- duration_ms, duration_source, input, input_bytes, input_storage, text, text_bytes,
66
+ duration_ms, duration_source, input, input_bytes, input_storage,
67
+ wire_input, wire_input_bytes, wire_input_storage, text, text_bytes,
67
68
  output_storage, spill_path, spill_bytes, src_offset, src_len, result_offset,
68
69
  result_len, result_block, model, tokens_in, tokens_out, tokens_cache_read,
69
70
  tokens_cache_write, est_cost, child_session_id, agent_type, agent_status, raw_type,
70
71
  raw_subtype, attrs)
71
- VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`;
72
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`;
72
73
  const POPULATE_FTS_SQL = `INSERT INTO events_fts(rowid, text, input)
73
74
  SELECT rowid, text, input FROM events WHERE session_id = ?`;
74
75
  const WRITE_HEADER_SQL = `UPDATE sessions SET
@@ -178,7 +179,7 @@ function insertEvents(db, events, spills) {
178
179
  cache_read: event.tokens_cache_read,
179
180
  cache_write: event.tokens_cache_write,
180
181
  });
181
- insert.run(event.id, event.session_id, event.turn_id, event.seq, event.kind, event.ts, event.request_id ?? null, event.block_index, event.name ?? null, event.status ?? null, event.duration_ms ?? null, event.duration_source ?? null, event.input ?? null, event.input_bytes ?? null, event.input_storage ?? null, event.text ?? null, event.text_bytes ?? null, output.output_storage, output.spill_path, output.spill_bytes, event.src_offset, event.src_len, event.result_offset ?? null, event.result_len ?? null, event.result_block ?? null, event.model ?? null, event.tokens_in ?? null, event.tokens_out ?? null, event.tokens_cache_read ?? null, event.tokens_cache_write ?? null, est_cost, event.child_session_id ?? null, event.agent_type ?? null, event.agent_status ?? null, event.raw_type, event.raw_subtype ?? null, event.attrs);
182
+ insert.run(event.id, event.session_id, event.turn_id, event.seq, event.kind, event.ts, event.request_id ?? null, event.block_index, event.name ?? null, event.status ?? null, event.duration_ms ?? null, event.duration_source ?? null, event.input ?? null, event.input_bytes ?? null, event.input_storage ?? null, event.wire_input ?? null, event.wire_input_bytes ?? null, event.wire_input_storage ?? null, event.text ?? null, event.text_bytes ?? null, output.output_storage, output.spill_path, output.spill_bytes, event.src_offset, event.src_len, event.result_offset ?? null, event.result_len ?? null, event.result_block ?? null, event.model ?? null, event.tokens_in ?? null, event.tokens_out ?? null, event.tokens_cache_read ?? null, event.tokens_cache_write ?? null, est_cost, event.child_session_id ?? null, event.agent_type ?? null, event.agent_status ?? null, event.raw_type, event.raw_subtype ?? null, event.attrs);
182
183
  }
183
184
  }
184
185
  function spillColumns(event, spills) {
@@ -1,9 +1,11 @@
1
1
  import { asyncAgentLaunch, taskNotification } from '../transcript/agents.js';
2
+ import { attachmentRecord } from '../transcript/attachments.js';
2
3
  import { contentBlocks } from '../transcript/blocks.js';
3
4
  import { isHumanPrompt, MACHINERY_TAGS } from '../transcript/human.js';
4
5
  import { foldControlLines, foldSessionEnvelope, promptGroupId, toolDenialKind, turnDurationMs, } from '../transcript/line.js';
5
6
  import { foldRequestGroup, groupByRequestId, modelOfRequestGroup, requestIdOf, } from '../transcript/usage.js';
6
- import { joinToolCalls, NO_INPUT, toolInput, } from './tools.js';
7
+ import { joinToolCalls, NO_INPUT, NO_WIRE_INPUT, toolInput, wireInput, } from './tools.js';
8
+ import { wireInputOf } from '../transcript/wire.js';
7
9
  const MAX_TITLE_CHARS = 200;
8
10
  const NO_BLOCKS = Object.freeze([]);
9
11
  const MS_PER_DAY = 86_400_000;
@@ -75,7 +77,9 @@ function rawTypeOf(line) {
75
77
  function rawSubtypeOf(line) {
76
78
  if (line.kind === 'unknown')
77
79
  return line.raw_subtype;
78
- return line.kind === 'system' ? line.subtype : undefined;
80
+ if (line.kind === 'system')
81
+ return line.subtype;
82
+ return attachmentRecord(line)?.subtype;
79
83
  }
80
84
  function turnKind(lead, human, text, compacted) {
81
85
  if (human)
@@ -176,6 +180,7 @@ export function runPipeline(lines, ctx) {
176
180
  const blocks = blocksAt[index];
177
181
  const human = humanAt[index];
178
182
  const usage = usageAt.get(index);
183
+ const lineText = blocks.length === 0 ? attachmentRecord(line)?.text : undefined;
179
184
  let stamped = false;
180
185
  const emit = (block, blockIndex) => {
181
186
  if (block?.kind === 'unknown_block')
@@ -201,7 +206,10 @@ export function runPipeline(lines, ctx) {
201
206
  child_session_id: undefined,
202
207
  agent_type: undefined,
203
208
  ...(block?.kind === 'tool_use' ? toolInput(block.input) : NO_INPUT),
204
- text: kind === 'tool_call' ? undefined : blockText(block),
209
+ ...(block?.kind === 'tool_use'
210
+ ? wireInput(wireInputOf(line.raw, block.id), block.input)
211
+ : NO_WIRE_INPUT),
212
+ text: kind === 'tool_call' ? undefined : (blockText(block) ?? lineText),
205
213
  text_bytes: undefined,
206
214
  output_storage: kind === 'tool_call' ? 'absent' : undefined,
207
215
  spill_path: undefined,
@@ -1,4 +1,5 @@
1
1
  import { claimsPersistedOutput, } from '../transcript/agents.js';
2
+ import { sameValue } from '../transcript/wire.js';
2
3
  import { epochMs } from './pipeline.js';
3
4
  export const INLINE_MAX = 65536;
4
5
  export const PREVIEW_MAX = 8192;
@@ -7,6 +8,11 @@ export const NO_INPUT = Object.freeze({
7
8
  input_bytes: undefined,
8
9
  input_storage: undefined,
9
10
  });
11
+ export const NO_WIRE_INPUT = Object.freeze({
12
+ wire_input: undefined,
13
+ wire_input_bytes: undefined,
14
+ wire_input_storage: undefined,
15
+ });
10
16
  function byteLength(text) {
11
17
  return Buffer.byteLength(text, 'utf8');
12
18
  }
@@ -29,6 +35,16 @@ export function toolInput(input) {
29
35
  ? { input: headPreview(text), input_bytes: bytes, input_storage: 'line_ref' }
30
36
  : { input: text, input_bytes: bytes, input_storage: 'inline' };
31
37
  }
38
+ export function wireInput(wire, request) {
39
+ if (wire === undefined || sameValue(wire, request))
40
+ return NO_WIRE_INPUT;
41
+ const stored = toolInput(wire);
42
+ return {
43
+ wire_input: stored.input,
44
+ wire_input_bytes: stored.input_bytes,
45
+ wire_input_storage: stored.input_storage === 'line_ref' ? 'line_ref' : 'inline',
46
+ };
47
+ }
32
48
  function elapsedMs(call, result) {
33
49
  const ms = epochMs(result) - epochMs(call);
34
50
  return ms < 0 ? 0 : ms;
@@ -1,4 +1,4 @@
1
- import { chmodSync, readFileSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
1
+ import { chmodSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  const CONFIG_FILE = 'config.json';
4
4
  export function writeConfig(dataDir, config) {
@@ -1,5 +1,5 @@
1
1
  const NO_DRIFT = '{}';
2
- function mergeBucket(into, from) {
2
+ export function mergeBucket(into, from) {
3
3
  for (const [key, n] of Object.entries(from ?? {}))
4
4
  into[key] = (into[key] ?? 0) + n;
5
5
  }
@@ -1,10 +1,5 @@
1
1
  import { networkInterfaces } from 'node:os';
2
- const ALLOWED_HOSTNAMES = new Set([
3
- 'localhost',
4
- '127.0.0.1',
5
- '[::1]',
6
- '::1',
7
- ]);
2
+ const ALLOWED_HOSTNAMES = new Set(['localhost', '127.0.0.1', '[::1]', '::1']);
8
3
  function isLoopbackBind(host) {
9
4
  return ALLOWED_HOSTNAMES.has(host);
10
5
  }
@@ -1,5 +1,5 @@
1
1
  import { randomBytes } from 'node:crypto';
2
- import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
2
+ import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { join } from 'node:path';
5
5
  export const TOKEN_HEADER = 'x-agentlens-token';
@@ -0,0 +1,13 @@
1
+ import { arr, obj, str } from './accessors.js';
2
+ const NO_ELEMENTS = Object.freeze([]);
3
+ function placeholder(subtype) {
4
+ return subtype === '' ? '[attachment]' : `[attachment ${subtype}]`;
5
+ }
6
+ export function attachmentRecord(line) {
7
+ if (line.kind !== 'attachment')
8
+ return undefined;
9
+ const subtype = str(obj(line.raw.attachment, undefined)?.type, '');
10
+ const first = arr(line.raw.rendered, NO_ELEMENTS)[0];
11
+ const rendered = str(obj(first, undefined)?.content, undefined);
12
+ return { subtype, text: rendered ?? placeholder(subtype) };
13
+ }
@@ -1 +1 @@
1
- export const PROJECTOR_VERSION = 11;
1
+ export const PROJECTOR_VERSION = 13;
@@ -0,0 +1,39 @@
1
+ import { arr, obj } from './accessors.js';
2
+ export function wireInputOf(raw, toolUseId) {
3
+ try {
4
+ const field = obj(obj(raw, undefined)?.wireToolInputs, undefined);
5
+ if (field === undefined || !Object.hasOwn(field, toolUseId))
6
+ return undefined;
7
+ return obj(field[toolUseId], undefined);
8
+ }
9
+ catch {
10
+ return undefined;
11
+ }
12
+ }
13
+ export function sameValue(a, b) {
14
+ try {
15
+ return same(a, b);
16
+ }
17
+ catch {
18
+ return true;
19
+ }
20
+ }
21
+ function same(a, b) {
22
+ if (a === b)
23
+ return true;
24
+ const aList = arr(a, undefined);
25
+ const bList = arr(b, undefined);
26
+ if (aList !== undefined || bList !== undefined) {
27
+ if (aList === undefined || bList === undefined || aList.length !== bList.length)
28
+ return false;
29
+ return aList.every((item, index) => same(item, bList[index]));
30
+ }
31
+ const aObj = obj(a, undefined);
32
+ const bObj = obj(b, undefined);
33
+ if (aObj === undefined || bObj === undefined)
34
+ return false;
35
+ const aKeys = Object.keys(aObj);
36
+ if (aKeys.length !== Object.keys(bObj).length)
37
+ return false;
38
+ return aKeys.every((key) => Object.hasOwn(bObj, key) && same(aObj[key], bObj[key]));
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faithfulalabi/agent-lens",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "Local-first agentic tracing platform: inspect everything your agent did in a session.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -47,6 +47,7 @@
47
47
  "lint": "eslint .",
48
48
  "prepublishOnly": "npm run build",
49
49
  "format": "prettier --write .",
50
+ "format:check": "prettier --check .",
50
51
  "prepare": "npm --prefix ui install"
51
52
  },
52
53
  "dependencies": {
@@ -62,7 +63,7 @@
62
63
  "fast-check": "^3.23.2",
63
64
  "globals": "^15.15.0",
64
65
  "playwright-core": "^1.62.1",
65
- "prettier": "^3.3.0",
66
+ "prettier": "3.9.6",
66
67
  "tsx": "^4.19.0",
67
68
  "typescript": "^5.6.0",
68
69
  "typescript-eslint": "^8.0.0",