@faithfulalabi/agent-lens 0.4.1 → 0.5.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
  }
@@ -181,6 +181,7 @@ const SPILL_SNIPPET = `snippet(spill_fts, 3, '<mark>', '</mark>', '…', 12)`;
181
181
  function ftsPhrase(q) {
182
182
  return `"${q.replaceAll('"', '""')}"`;
183
183
  }
184
+ const SEARCHABLE = `e.kind <> 'unknown'`;
184
185
  function searchSql(scoped) {
185
186
  const scope = scoped ? ' AND e.session_id = :session' : '';
186
187
  const events = `SELECT e.session_id AS session_id, s.title AS session_title, s.project_path AS project_path,` +
@@ -188,7 +189,7 @@ function searchSql(scoped) {
188
189
  ` e.ts AS ts, ${SNIPPET} AS snippet, rank AS score` +
189
190
  ` FROM events_fts JOIN events e ON e.rowid = events_fts.rowid` +
190
191
  ` JOIN sessions s ON s.id = e.session_id` +
191
- ` WHERE events_fts MATCH :q${scope}`;
192
+ ` WHERE events_fts MATCH :q AND ${SEARCHABLE}${scope}`;
192
193
  const spills = `SELECT e.session_id, s.title, s.project_path, e.turn_id, e.id, e.seq, e.kind, e.name,` +
193
194
  ` e.ts, ${SPILL_SNIPPET}, rank` +
194
195
  ` FROM spill_fts JOIN events e ON e.id = spill_fts.event_id` +
@@ -1,4 +1,5 @@
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';
@@ -75,7 +76,9 @@ function rawTypeOf(line) {
75
76
  function rawSubtypeOf(line) {
76
77
  if (line.kind === 'unknown')
77
78
  return line.raw_subtype;
78
- return line.kind === 'system' ? line.subtype : undefined;
79
+ if (line.kind === 'system')
80
+ return line.subtype;
81
+ return attachmentRecord(line)?.subtype;
79
82
  }
80
83
  function turnKind(lead, human, text, compacted) {
81
84
  if (human)
@@ -176,6 +179,7 @@ export function runPipeline(lines, ctx) {
176
179
  const blocks = blocksAt[index];
177
180
  const human = humanAt[index];
178
181
  const usage = usageAt.get(index);
182
+ const lineText = blocks.length === 0 ? attachmentRecord(line)?.text : undefined;
179
183
  let stamped = false;
180
184
  const emit = (block, blockIndex) => {
181
185
  if (block?.kind === 'unknown_block')
@@ -201,7 +205,7 @@ export function runPipeline(lines, ctx) {
201
205
  child_session_id: undefined,
202
206
  agent_type: undefined,
203
207
  ...(block?.kind === 'tool_use' ? toolInput(block.input) : NO_INPUT),
204
- text: kind === 'tool_call' ? undefined : blockText(block),
208
+ text: kind === 'tool_call' ? undefined : (blockText(block) ?? lineText),
205
209
  text_bytes: undefined,
206
210
  output_storage: kind === 'tool_call' ? 'absent' : undefined,
207
211
  spill_path: undefined,
@@ -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 = 12;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faithfulalabi/agent-lens",
3
- "version": "0.4.1",
3
+ "version": "0.5.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",