@faithfulalabi/agent-lens 0.7.0 → 0.8.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.
@@ -1,7 +1,7 @@
1
1
  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
- import { readDriftRows, readHealthCounts, readMeta } from '../../db/read.js';
4
+ import { hasStructuralDrift, readDriftRows, readHealthCounts, readMeta } from '../../db/read.js';
5
5
  import { aggregateDrift, mergeBucket } from '../../server/drift-report.js';
6
6
  import { parseStringFlag } from './archive.js';
7
7
  const MAX_LISTED = 20;
@@ -40,7 +40,8 @@ export function readCacheStats(dataDir) {
40
40
  const row = census.get(session.harness_version ?? 'unknown');
41
41
  if (row === undefined)
42
42
  continue;
43
- row.drifting += 1;
43
+ if (hasStructuralDrift(session.counts))
44
+ row.drifting += 1;
44
45
  mergeNamed(row, 'unknown_line_types', session.counts.unknown_line_types);
45
46
  mergeNamed(row, 'unknown_top_level_fields', session.counts.unknown_top_level_fields);
46
47
  }
@@ -8,7 +8,7 @@ import { classifyLine } from '../transcript/line.js';
8
8
  function byteLength(text) {
9
9
  return Buffer.byteLength(text, 'utf8');
10
10
  }
11
- function lineAt(env, archivePath, byteOffset, len) {
11
+ function lineAt(env, archivePath, byteOffset, len, drift) {
12
12
  let bytes;
13
13
  try {
14
14
  bytes = env.reader.read(archivePath, byteOffset, len);
@@ -22,7 +22,7 @@ function lineAt(env, archivePath, byteOffset, len) {
22
22
  return classifyLine(JSON.parse(bytes.toString('utf8')), {
23
23
  byteOffset,
24
24
  byteLength: len,
25
- drift: new DriftCounter(),
25
+ drift,
26
26
  });
27
27
  }
28
28
  catch {
@@ -30,8 +30,42 @@ function lineAt(env, archivePath, byteOffset, len) {
30
30
  }
31
31
  }
32
32
  function blockAt(env, archivePath, byteOffset, len, blockIndex) {
33
- const line = lineAt(env, archivePath, byteOffset, len);
34
- return line === undefined ? undefined : contentBlocks(line)[blockIndex];
33
+ const line = lineAt(env, archivePath, byteOffset, len, new DriftCounter());
34
+ if (line === undefined)
35
+ return undefined;
36
+ try {
37
+ return contentBlocks(line)[blockIndex];
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ }
43
+ function unrecognisedFieldsAt(env, archivePath, byteOffset, len, tag) {
44
+ const drift = new DriftCounter();
45
+ const line = lineAt(env, archivePath, byteOffset, len, drift);
46
+ if (line === undefined)
47
+ return [];
48
+ try {
49
+ const counted = JSON.parse(drift.serialize()).unknown_top_level_fields;
50
+ if (typeof counted !== 'object' || counted === null)
51
+ return [];
52
+ const names = new Set(Object.keys(counted));
53
+ return Object.entries(line.raw)
54
+ .filter(([name]) => names.has(name))
55
+ .map(([name, value]) => ({ line: tag, name, value: JSON.stringify(value) }));
56
+ }
57
+ catch {
58
+ return [];
59
+ }
60
+ }
61
+ export function resolveUnrecognisedFields(row, archivePath, env) {
62
+ if (archivePath === undefined)
63
+ return [];
64
+ const own = unrecognisedFieldsAt(env, archivePath, row.src_offset, row.src_len, 'emitting');
65
+ const { result_offset, result_len } = row;
66
+ if (result_offset === null || result_len === null)
67
+ return own;
68
+ return [...own, ...unrecognisedFieldsAt(env, archivePath, result_offset, result_len, 'result')];
35
69
  }
36
70
  function stored(row, field) {
37
71
  const text = (field === 'text' ? row.text : row.input) ?? '';
@@ -164,7 +198,9 @@ export function resolveImage(row, ordinal, archivePath, env) {
164
198
  return { ok: false, reason: 'gone' };
165
199
  if (at.len > IMAGE_LINE_MAX)
166
200
  return { ok: false, reason: 'too_large' };
167
- const line = archivePath === undefined ? undefined : lineAt(env, archivePath, at.offset, at.len);
201
+ const line = archivePath === undefined
202
+ ? undefined
203
+ : lineAt(env, archivePath, at.offset, at.len, new DriftCounter());
168
204
  const source = line === undefined ? undefined : imageSourceAt(line, at.block, ordinal);
169
205
  if (source === undefined)
170
206
  return { ok: false, reason: 'gone' };
@@ -185,6 +221,9 @@ export function createContentEnv(reader, roots) {
185
221
  export function createContentResolver(archivePathOf, env) {
186
222
  return (row, field) => resolveContent(row, field, archivePathOf(row.session_id), env);
187
223
  }
224
+ export function createUnrecognisedFieldsResolver(archivePathOf, env) {
225
+ return (row) => resolveUnrecognisedFields(row, archivePathOf(row.session_id), env);
226
+ }
188
227
  export function createSpillLocator(archivePathOf, env) {
189
228
  return (row) => spillSource(row, archivePathOf(row.session_id), env);
190
229
  }
@@ -25,6 +25,10 @@ const SESSION_LIST_COLUMNS = `id, title, preview, project_path, git_branch, mode
25
25
  sub_tokens_cache_read, sub_tokens_cache_write, sub_est_cost, sub_models, rollup_state, drift_json`;
26
26
  const TOP_LEVEL_ONLY = 'parent_session_id IS NULL';
27
27
  const NO_DRIFT = '{}';
28
+ export const FIELD_TIER_BUCKETS = new Set(['unknown_top_level_fields']);
29
+ export function hasStructuralDrift(counts) {
30
+ return Object.keys(counts).some((bucket) => !FIELD_TIER_BUCKETS.has(bucket));
31
+ }
28
32
  const Q_PREDICATE = "(COALESCE(title,'') || ' ' || COALESCE(preview,'') || ' ' || project_path) LIKE ? ESCAPE '\\'";
29
33
  function likeContains(q) {
30
34
  return `%${q.replace(/[\\%_]/g, (ch) => `\\${ch}`)}%`;
@@ -38,7 +42,7 @@ function toSessionRow(row) {
38
42
  ...rest,
39
43
  models: modelIdsOf(models),
40
44
  sub_models: modelIdsOf(sub_models),
41
- has_drift: drift_json !== NO_DRIFT,
45
+ has_drift: drift_json !== NO_DRIFT && hasStructuralDrift(JSON.parse(drift_json)),
42
46
  };
43
47
  }
44
48
  export function buildSessionListSql(query) {
@@ -276,4 +276,10 @@ export function registerApi(app, deps) {
276
276
  schema_version: readMeta(db, 'schema_version') ?? null,
277
277
  projector_version: readMeta(db, 'projector_version') ?? null,
278
278
  }));
279
+ app.get('/api/events/:id/unrecognised-fields', (c) => {
280
+ const row = readEventContentRow(db, c.req.param('id'));
281
+ if (row === undefined)
282
+ return jsonNotFound(c);
283
+ return c.json({ id: row.id, fields: deps.resolveUnrecognisedFields?.(row) ?? [] });
284
+ });
279
285
  }
@@ -5,7 +5,7 @@ import { mkdirSync } from 'node:fs';
5
5
  import { readOrCreateToken } from '../shared/index.js';
6
6
  import { resolveArchiveRoot, resolveTranscriptRoot } from '../archive/index.js';
7
7
  import { createArchiveReader } from '../archive/read.js';
8
- import { createContentEnv, createContentResolver, createImageResolver, } from '../content/resolve.js';
8
+ import { createContentEnv, createContentResolver, createImageResolver, createUnrecognisedFieldsResolver, } from '../content/resolve.js';
9
9
  import { createCorpusSweep } from '../corpus/watch.js';
10
10
  import { createProjectionEnv } from '../corpus/env.js';
11
11
  import { openDb } from '../db/open.js';
@@ -77,6 +77,7 @@ export async function startServer(options = {}) {
77
77
  warm,
78
78
  resolveContent: createContentResolver(archivePathOf, contentEnv),
79
79
  resolveImage: createImageResolver(archivePathOf, contentEnv),
80
+ resolveUnrecognisedFields: createUnrecognisedFieldsResolver(archivePathOf, contentEnv),
80
81
  });
81
82
  const fetch = app.fetch;
82
83
  const explicit = options.port !== undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faithfulalabi/agent-lens",
3
- "version": "0.7.0",
3
+ "version": "0.8.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",