@faithfulalabi/agent-lens 0.2.0 → 0.4.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.
package/README.md CHANGED
@@ -55,15 +55,27 @@ only by the archive.
55
55
  agent-lens schedule install
56
56
  ```
57
57
 
58
- On macOS that sets up a launchd job that runs `agent-lens archive` every 15 minutes and logs each
59
- pass where `agent-lens doctor` reads it. Run it again any time — it replaces its own job rather
60
- than duplicating it. `agent-lens schedule status` reports the job and its last successful pass;
61
- `agent-lens schedule disable` turns it off cleanly, leaving the archive and its logs untouched. On
62
- other platforms, run `agent-lens archive` every ~15 minutes yourself via a systemd timer or cron.
58
+ That sets up a recurring job that runs `agent-lens archive` every 15 minutes and logs each pass
59
+ where `agent-lens doctor` reads it. Both supported platforms are handled for you: macOS gets a
60
+ launchd agent, Linux gets a systemd **user** timer under `~/.config/systemd/user`. Run it again any
61
+ time — it replaces its own job rather than duplicating it. `agent-lens schedule status` reports the
62
+ job and its last successful pass; `agent-lens schedule disable` turns it off cleanly, leaving the
63
+ archive and its logs untouched. On any other platform, run `agent-lens archive` every ~15 minutes
64
+ yourself from whatever scheduler you have, such as a cron entry.
63
65
 
64
66
  One caveat worth knowing before you rely on an interval: a wall-clock schedule does not fire while
65
67
  the machine is asleep. Treat the interval as a bound on _wake_ time, not on elapsed time.
66
68
 
69
+ One more on Linux, and it is the difference between a green report and a real archive:
70
+
71
+ > **a systemd user timer does not run while you are logged out unless lingering is on for your user**
72
+
73
+ `agent-lens schedule status` reports which state your user is in, and `schedule install` prints the
74
+ one command that changes it — `loginctl enable-linger <uid>`. The command is printed and never run:
75
+ it writes outside everything this tool owns. The two platforms also differ deliberately on catch-up.
76
+ launchd runs a pass at load; the systemd timer uses `OnCalendar` with `Persistent=true`, so a slot
77
+ missed while the machine was off runs once on resume.
78
+
67
79
  The pass is safe to run often: an advisory lock means a second concurrent pass copies nothing and
68
80
  exits 0, and an unchanged corpus copies zero bytes. When a source has been rewritten, the archived
69
81
  bytes are **kept** and the file is marked `diverged` rather than overwritten. `agent-lens archive
@@ -53,6 +53,18 @@ export function resolveLaunchAgentsDir(homeDir = homedir()) {
53
53
  export function resolvePlistPath(label, homeDir = homedir()) {
54
54
  return join(resolveLaunchAgentsDir(homeDir), `${label}.plist`);
55
55
  }
56
+ export function resolveSystemdUserDir(homeDir = homedir(), configHome) {
57
+ const base = configHome !== undefined && configHome !== '' && isAbsolute(configHome)
58
+ ? configHome
59
+ : join(homeDir, '.config');
60
+ return join(base, 'systemd', 'user');
61
+ }
62
+ export function resolveSystemdUnitPath(name, homeDir, configHome) {
63
+ return join(resolveSystemdUserDir(homeDir, configHome), name);
64
+ }
65
+ export function resolveSystemdWantsPath(name, homeDir, configHome) {
66
+ return join(resolveSystemdUserDir(homeDir, configHome), 'timers.target.wants', name);
67
+ }
56
68
  export function resolveScheduleWrapperPath(dataDir) {
57
69
  return join(resolveDataDir(dataDir), 'schedule', 'archive.sh');
58
70
  }
@@ -3,23 +3,40 @@ import { chmodSync, existsSync, mkdirSync, renameSync, rmdirSync, unlinkSync, wr
3
3
  import { homedir } from 'node:os';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { assertNotUnderRoot, lstatSafe, resolveCronLogPath, resolveDataDir, resolvePlistPath, resolveScheduleWrapperPath, resolveTranscriptRoot, TRANSCRIPT_ROOT_LABEL, } from '../../archive/paths.js';
6
+ import { assertNotUnderRoot, lstatSafe, resolveCronLogPath, resolveDataDir, resolvePlistPath, resolveScheduleWrapperPath, resolveSystemdUnitPath, resolveSystemdUserDir, resolveSystemdWantsPath, resolveTranscriptRoot, TRANSCRIPT_ROOT_LABEL, } from '../../archive/paths.js';
7
7
  import { parseStringFlag } from './archive.js';
8
8
  export const SCHEDULE_LABEL = 'com.agent-lens.archive';
9
9
  export const LEGACY_LABEL = 'com.faithful.agent-lens.archive';
10
10
  const START_INTERVAL_SECONDS = 900;
11
11
  export const CRON_STAMP_FORMAT = '+%Y-%m-%dT%H:%M:%S%z';
12
12
  export const WAKE_TIME_CAVEAT = 'a wall-clock schedule does not fire while the machine is asleep — treat the interval as a bound on wake time, not on elapsed time';
13
+ export const LINGER_CAVEAT = 'a systemd user timer does not run while you are logged out unless lingering is on for your user';
14
+ export const SYSTEMD_UNIT_BASE = 'agent-lens-archive';
15
+ const SYSTEMD_SERVICE = `${SYSTEMD_UNIT_BASE}.service`;
16
+ const SYSTEMD_TIMER = `${SYSTEMD_UNIT_BASE}.timer`;
17
+ const SYSTEMD_CALENDAR = '*:0/15';
18
+ const SYSTEMD_TIMER_PROPERTIES = [
19
+ 'ActiveState',
20
+ 'UnitFileState',
21
+ 'NextElapseUSecRealtime',
22
+ 'LastTriggerUSec',
23
+ 'Result',
24
+ ];
13
25
  const ACTIONS = 'install | status | disable';
14
- const NON_MACOS_POINTER = 'the recurring job uses launchd, which is macOS-only. Elsewhere, run `agent-lens archive` ' +
15
- 'every ~15 minutes yourself — a systemd timer or cron entry — per the README section ' +
16
- '"Keeping the archive current".';
17
- function realLaunchctl(args) {
18
- const result = spawnSync('launchctl', args, { encoding: 'utf8' });
19
- return {
20
- status: result.status,
21
- stdout: result.stdout ?? '',
22
- stderr: result.error === undefined ? (result.stderr ?? '') : String(result.error.message),
26
+ const UNSUPPORTED_PLATFORM_POINTER = 'the recurring job uses launchd on macOS and a systemd user timer on Linux, and this platform ' +
27
+ 'is neither. Run `agent-lens archive` every ~15 minutes from whatever scheduler it does have — ' +
28
+ 'a cron entry, for instance — per the README section "Keeping the archive current". No cron ' +
29
+ 'fallback ships here on purpose: the two backends above are the two this command sets up for you.';
30
+ const BUS_HINT = ' hint: run this from a logged-in session — `systemctl --user` needs a user D-Bus, with ' +
31
+ 'XDG_RUNTIME_DIR pointing at /run/user/<uid>';
32
+ function realRunner(bin) {
33
+ return (args) => {
34
+ const result = spawnSync(bin, args, { encoding: 'utf8' });
35
+ return {
36
+ status: result.status,
37
+ stdout: result.stdout ?? '',
38
+ stderr: result.error === undefined ? (result.stderr ?? '') : String(result.error.message),
39
+ };
23
40
  };
24
41
  }
25
42
  function realDeps() {
@@ -29,7 +46,10 @@ function realDeps() {
29
46
  homeDir: homedir(),
30
47
  uid: process.getuid?.() ?? 0,
31
48
  moduleUrl: import.meta.url,
32
- launchctl: realLaunchctl,
49
+ launchctl: realRunner('launchctl'),
50
+ systemctl: realRunner('systemctl'),
51
+ loginctl: realRunner('loginctl'),
52
+ configHome: process.env.XDG_CONFIG_HOME,
33
53
  now: Date.now,
34
54
  };
35
55
  }
@@ -184,6 +204,56 @@ export function buildPlist(opts) {
184
204
  </plist>
185
205
  `;
186
206
  }
207
+ export function escapeUnitValue(value) {
208
+ if (/[\n\r]/.test(value)) {
209
+ throw new Error('refusing to write a systemd unit: a line break in a unit value would split the ' +
210
+ `directive — ${JSON.stringify(value)}`);
211
+ }
212
+ return value
213
+ .replace(/\\/g, '\\\\')
214
+ .replace(/"/g, '\\"')
215
+ .replace(/%/g, '%%')
216
+ .replace(/\$/g, '$$$$');
217
+ }
218
+ export function buildSystemdService(opts) {
219
+ return `[Unit]
220
+ Description=agent-lens archive — unattended pass, every ${START_INTERVAL_SECONDS / 60} minutes
221
+ #
222
+ # GENERATED by \`agent-lens schedule\` — do not edit. Turning the job on again
223
+ # rewrites this file in full, which is what keeps it current with the package
224
+ # that shipped it.
225
+ #
226
+ # Coverage is wake-time-bounded: ${escapeUnitValue(WAKE_TIME_CAVEAT)}.
227
+ #
228
+ # Exit codes are the archive command's own: 0 clean, 1 usage error / crash,
229
+ # 3 archive-side errors. 2 is RESERVED product-wide and must never appear.
230
+
231
+ [Service]
232
+ Type=oneshot
233
+ ExecStart=/bin/sh "${escapeUnitValue(opts.wrapperPath)}"
234
+ `;
235
+ }
236
+ export function buildSystemdTimer() {
237
+ return `[Unit]
238
+ Description=agent-lens archive — timer for ${escapeUnitValue(SYSTEMD_SERVICE)}
239
+ #
240
+ # GENERATED by \`agent-lens schedule\` — do not edit. Turning the job on again
241
+ # rewrites this file in full.
242
+ #
243
+ # Coverage is wake-time-bounded: ${escapeUnitValue(WAKE_TIME_CAVEAT)}.
244
+ #
245
+ # And ${escapeUnitValue(LINGER_CAVEAT)} — \`agent-lens schedule status\` reports which.
246
+
247
+ [Timer]
248
+ OnCalendar=${escapeUnitValue(SYSTEMD_CALENDAR)}
249
+ Persistent=true
250
+ AccuracySec=1min
251
+ Unit=${escapeUnitValue(SYSTEMD_SERVICE)}
252
+
253
+ [Install]
254
+ WantedBy=timers.target
255
+ `;
256
+ }
187
257
  function ensureDirOutsideCorpus(dir, mode, transcriptRoot) {
188
258
  assertNotUnderRoot(dir, transcriptRoot, TRANSCRIPT_ROOT_LABEL);
189
259
  mkdirSync(dir, { recursive: true, mode });
@@ -202,12 +272,22 @@ function removeIfPresent(path, transcriptRoot) {
202
272
  unlinkSync(path);
203
273
  return true;
204
274
  }
275
+ function removeEmptyDir(dir, transcriptRoot) {
276
+ assertNotUnderRoot(dir, transcriptRoot, TRANSCRIPT_ROOT_LABEL);
277
+ try {
278
+ rmdirSync(dir);
279
+ }
280
+ catch {
281
+ }
282
+ }
205
283
  function gui(deps, label) {
206
284
  return `gui/${deps.uid}/${label}`;
207
285
  }
208
286
  function install(dataDirFlag, deps) {
287
+ if (deps.platform === 'linux')
288
+ return installSystemd(dataDirFlag, deps);
209
289
  if (deps.platform !== 'darwin') {
210
- console.error(`agent-lens schedule: nothing was set up — ${NON_MACOS_POINTER}`);
290
+ console.error(`agent-lens schedule: nothing was set up — ${UNSUPPORTED_PLATFORM_POINTER}`);
211
291
  return 1;
212
292
  }
213
293
  const dataDir = resolveDataDir(dataDirFlag);
@@ -247,8 +327,10 @@ function install(dataDirFlag, deps) {
247
327
  return 0;
248
328
  }
249
329
  async function status(dataDirFlag, deps) {
330
+ if (deps.platform === 'linux')
331
+ return statusSystemd(dataDirFlag, deps);
250
332
  if (deps.platform !== 'darwin') {
251
- console.log(`agent-lens schedule: ${NON_MACOS_POINTER}`);
333
+ console.log(`agent-lens schedule: ${UNSUPPORTED_PLATFORM_POINTER}`);
252
334
  console.log(`note: ${WAKE_TIME_CAVEAT}`);
253
335
  return 0;
254
336
  }
@@ -275,8 +357,10 @@ async function status(dataDirFlag, deps) {
275
357
  return 0;
276
358
  }
277
359
  function disable(dataDirFlag, deps) {
360
+ if (deps.platform === 'linux')
361
+ return disableSystemd(dataDirFlag, deps);
278
362
  if (deps.platform !== 'darwin') {
279
- console.error(`agent-lens schedule: nothing was removed — ${NON_MACOS_POINTER}`);
363
+ console.error(`agent-lens schedule: nothing was removed — ${UNSUPPORTED_PLATFORM_POINTER}`);
280
364
  return 1;
281
365
  }
282
366
  const transcriptRoot = resolveTranscriptRoot();
@@ -291,11 +375,7 @@ function disable(dataDirFlag, deps) {
291
375
  removed.push(plistPath);
292
376
  if (removeIfPresent(wrapperPath, transcriptRoot))
293
377
  removed.push(wrapperPath);
294
- try {
295
- rmdirSync(dirname(wrapperPath));
296
- }
297
- catch {
298
- }
378
+ removeEmptyDir(dirname(wrapperPath), transcriptRoot);
299
379
  if (removed.length === 0) {
300
380
  console.log('recurring archive job: not installed — nothing to remove');
301
381
  }
@@ -305,6 +385,155 @@ function disable(dataDirFlag, deps) {
305
385
  }
306
386
  return 0;
307
387
  }
388
+ function systemdPaths(deps) {
389
+ return {
390
+ unitDir: resolveSystemdUserDir(deps.homeDir, deps.configHome),
391
+ servicePath: resolveSystemdUnitPath(SYSTEMD_SERVICE, deps.homeDir, deps.configHome),
392
+ timerPath: resolveSystemdUnitPath(SYSTEMD_TIMER, deps.homeDir, deps.configHome),
393
+ wantsPath: resolveSystemdWantsPath(SYSTEMD_TIMER, deps.homeDir, deps.configHome),
394
+ };
395
+ }
396
+ function detailOf(result) {
397
+ return (result.stderr || result.stdout).trim() || `systemctl exited ${String(result.status)}`;
398
+ }
399
+ function parseProperties(stdout) {
400
+ const properties = new Map();
401
+ for (const line of stdout.split('\n')) {
402
+ const split = line.indexOf('=');
403
+ if (split > 0)
404
+ properties.set(line.slice(0, split).trim(), line.slice(split + 1).trim());
405
+ }
406
+ return properties;
407
+ }
408
+ function reported(value) {
409
+ return value === undefined || value === '' ? 'unknown — systemd reported nothing' : value;
410
+ }
411
+ function readLingerState(deps) {
412
+ const result = deps.loginctl(['show-user', String(deps.uid), '--property=Linger']);
413
+ if (result.status !== 0)
414
+ return 'unknown';
415
+ const value = parseProperties(result.stdout).get('Linger');
416
+ if (value === 'yes')
417
+ return 'on';
418
+ if (value === 'no')
419
+ return 'off';
420
+ return 'unknown';
421
+ }
422
+ function lingerLines(deps) {
423
+ const state = readLingerState(deps);
424
+ if (state === 'on') {
425
+ return [' lingering: on — the timer keeps running while you are logged out'];
426
+ }
427
+ return [
428
+ ` lingering: ${state} — ${LINGER_CAVEAT}`,
429
+ ` turn lingering on with: loginctl enable-linger ${deps.uid}`,
430
+ ];
431
+ }
432
+ function installSystemd(dataDirFlag, deps) {
433
+ const dataDir = resolveDataDir(dataDirFlag);
434
+ const transcriptRoot = resolveTranscriptRoot();
435
+ const wrapperPath = resolveScheduleWrapperPath(dataDir);
436
+ const cronLogPath = resolveCronLogPath(dataDir);
437
+ const { unitDir, servicePath, timerPath } = systemdPaths(deps);
438
+ for (const path of [wrapperPath, cronLogPath, unitDir, servicePath, timerPath]) {
439
+ assertNotUnderRoot(path, transcriptRoot, TRANSCRIPT_ROOT_LABEL);
440
+ }
441
+ const invocation = resolveArchiveInvocation(deps.moduleUrl);
442
+ const wrapperText = buildWrapperScript({
443
+ nodePath: deps.execPath,
444
+ invocation,
445
+ dataDir,
446
+ cronLogPath,
447
+ });
448
+ const serviceText = buildSystemdService({ wrapperPath });
449
+ const timerText = buildSystemdTimer();
450
+ ensureDirOutsideCorpus(dirname(wrapperPath), 0o700, transcriptRoot);
451
+ ensureDirOutsideCorpus(dirname(cronLogPath), 0o700, transcriptRoot);
452
+ ensureDirOutsideCorpus(unitDir, 0o700, transcriptRoot);
453
+ atomicWrite(wrapperPath, wrapperText, 0o700, transcriptRoot);
454
+ atomicWrite(servicePath, serviceText, 0o644, transcriptRoot);
455
+ atomicWrite(timerPath, timerText, 0o644, transcriptRoot);
456
+ const armed = [
457
+ ['--user', 'daemon-reload'],
458
+ ['--user', 'enable', SYSTEMD_TIMER],
459
+ ['--user', 'restart', SYSTEMD_TIMER],
460
+ ];
461
+ for (const argv of armed) {
462
+ const result = deps.systemctl(argv);
463
+ if (result.status !== 0) {
464
+ console.error(`agent-lens schedule: \`systemctl ${argv.join(' ')}\` failed — ${detailOf(result)}`);
465
+ if (/Failed to connect to bus/.test(result.stderr))
466
+ console.error(BUS_HINT);
467
+ return 1;
468
+ }
469
+ }
470
+ const firstPass = deps.systemctl(['--user', 'start', '--no-block', SYSTEMD_SERVICE]);
471
+ const lines = [];
472
+ if (firstPass.status !== 0) {
473
+ lines.push(`warning: the first pass could not be started — ${detailOf(firstPass)}`);
474
+ }
475
+ lines.push(`recurring archive job on — ${SYSTEMD_TIMER}, every ` +
476
+ `${START_INTERVAL_SECONDS / 60} minutes (${invocation.kind} layout)`, ` timer ${timerPath}`, ` service ${servicePath}`, ` wrapper ${wrapperPath}`, ` passes ${cronLogPath}`, ` unit log journalctl --user -u ${SYSTEMD_SERVICE}`, `note: ${WAKE_TIME_CAVEAT}`, ...lingerLines(deps));
477
+ console.log(lines.join('\n'));
478
+ return 0;
479
+ }
480
+ async function statusSystemd(dataDirFlag, deps) {
481
+ const { servicePath, timerPath } = systemdPaths(deps);
482
+ const lines = [];
483
+ if (lstatSafe(timerPath)?.isFile() === true) {
484
+ lines.push(`recurring archive job: installed — ${timerPath}`);
485
+ lines.push(` service ${servicePath}`);
486
+ const shown = deps.systemctl([
487
+ '--user',
488
+ 'show',
489
+ SYSTEMD_TIMER,
490
+ `--property=${SYSTEMD_TIMER_PROPERTIES.join(',')}`,
491
+ ]);
492
+ const properties = parseProperties(shown.stdout);
493
+ lines.push(` timer state: ${reported(properties.get('ActiveState'))}`, ` unit file: ${reported(properties.get('UnitFileState'))}`, ` next trigger: ${reported(properties.get('NextElapseUSecRealtime'))}`, ` last trigger: ${reported(properties.get('LastTriggerUSec'))}`, ` last result: ${reported(properties.get('Result'))}`);
494
+ }
495
+ else {
496
+ lines.push('recurring archive job: not installed');
497
+ lines.push(' turn it on with: agent-lens schedule install');
498
+ }
499
+ lines.push(...lingerLines(deps));
500
+ const [{ formatLastPassSection }, { readCronLogStatus }] = await Promise.all([
501
+ import('./doctor.js'),
502
+ import('../../archive/cron-log.js'),
503
+ ]);
504
+ lines.push(...formatLastPassSection(readCronLogStatus(dataDirFlag), deps.now()));
505
+ lines.push('', `note: ${WAKE_TIME_CAVEAT}`);
506
+ console.log(lines.join('\n'));
507
+ return 0;
508
+ }
509
+ function disableSystemd(dataDirFlag, deps) {
510
+ const transcriptRoot = resolveTranscriptRoot();
511
+ const wrapperPath = resolveScheduleWrapperPath(resolveDataDir(dataDirFlag));
512
+ const { servicePath, timerPath, wantsPath } = systemdPaths(deps);
513
+ const wasInstalled = lstatSafe(timerPath) !== undefined || lstatSafe(servicePath) !== undefined;
514
+ const stopped = deps.systemctl(['--user', 'stop', SYSTEMD_TIMER]);
515
+ const disabled = deps.systemctl(['--user', 'disable', SYSTEMD_TIMER]);
516
+ const removed = [];
517
+ for (const path of [timerPath, servicePath, wantsPath, wrapperPath]) {
518
+ if (removeIfPresent(path, transcriptRoot))
519
+ removed.push(path);
520
+ }
521
+ deps.systemctl(['--user', 'daemon-reload']);
522
+ removeEmptyDir(dirname(wrapperPath), transcriptRoot);
523
+ const lines = [];
524
+ const refused = [stopped, disabled].find((result) => result.status !== 0);
525
+ if (refused !== undefined && wasInstalled) {
526
+ lines.push(`warning: systemctl --user could not stop the timer — ${detailOf(refused)}`, ' the unit files were removed anyway, so nothing is left to fire');
527
+ }
528
+ if (removed.length === 0) {
529
+ lines.push('recurring archive job: not installed — nothing to remove');
530
+ }
531
+ else {
532
+ lines.push('recurring archive job off — removed:', ...removed.map((path) => ` ${path}`), ' cron.log and the archive itself are untouched');
533
+ }
534
+ console.log(lines.join('\n'));
535
+ return 0;
536
+ }
308
537
  export async function schedule(args = [], deps = realDeps()) {
309
538
  const action = parseScheduleAction(args);
310
539
  const dataDir = parseStringFlag(args, 'dataDir');
@@ -46,6 +46,14 @@ const LINE_TYPES = new Map([
46
46
  'attributionPlugin',
47
47
  'isApiErrorMessage',
48
48
  'error',
49
+ 'perTurnEffort',
50
+ 'apiBlockIndex',
51
+ 'advisorModel',
52
+ 'serverClassifierRequest',
53
+ 'wireToolInputs',
54
+ 'wireIngestContext',
55
+ 'apiErrorStatus',
56
+ 'quotaLimits',
49
57
  ]),
50
58
  },
51
59
  ],
@@ -68,6 +76,11 @@ const LINE_TYPES = new Map([
68
76
  'toolEndsTurn',
69
77
  'toolDenialKind',
70
78
  'imagePasteIds',
79
+ 'serverClassifierContext',
80
+ 'turnOrigin',
81
+ 'queueSkipAttachments',
82
+ 'turnPosition',
83
+ 'turnCompanion',
71
84
  ]),
72
85
  },
73
86
  ],
@@ -96,13 +109,23 @@ const LINE_TYPES = new Map([
96
109
  ]),
97
110
  },
98
111
  ],
99
- ['attachment', { kind: 'attachment', knownFields: fields(ENVELOPE, ['attachment']) }],
112
+ [
113
+ 'attachment',
114
+ {
115
+ kind: 'attachment',
116
+ knownFields: fields(ENVELOPE, [
117
+ 'attachment',
118
+ 'rendered',
119
+ 'renderedInHumanTurn',
120
+ ]),
121
+ },
122
+ ],
100
123
  ['mode', { kind: 'mode', knownFields: fields(['type', 'mode', 'sessionId']) }],
101
124
  [
102
125
  'last-prompt',
103
126
  {
104
127
  kind: 'last-prompt',
105
- knownFields: fields(['type', 'lastPrompt', 'leafUuid', 'sessionId']),
128
+ knownFields: fields(['type', 'lastPrompt', 'leafUuid', 'sessionId', 'explicit']),
106
129
  },
107
130
  ],
108
131
  [
@@ -138,7 +161,7 @@ const LINE_TYPES = new Map([
138
161
  'queue-operation',
139
162
  {
140
163
  kind: 'queue-operation',
141
- knownFields: fields(['type', 'operation', 'content', 'timestamp', 'sessionId']),
164
+ knownFields: fields(['type', 'operation', 'content', 'timestamp', 'sessionId', 'reason']),
142
165
  },
143
166
  ],
144
167
  [
@@ -150,6 +173,40 @@ const LINE_TYPES = new Map([
150
173
  ],
151
174
  ['started', { kind: 'started', knownFields: fields(['type', 'key', 'agentId']) }],
152
175
  ['result', { kind: 'result', knownFields: fields(['type', 'key', 'agentId', 'result']) }],
176
+ ['atis-latch', { kind: 'atis-latch', knownFields: fields(['type', 'atis', 'sessionId']) }],
177
+ [
178
+ 'cost-state',
179
+ {
180
+ kind: 'cost-state',
181
+ knownFields: fields([
182
+ 'type',
183
+ 'sessionId',
184
+ 'startTime',
185
+ 'totalCostUSD',
186
+ 'totalDuration',
187
+ 'totalAPIDuration',
188
+ 'totalAPIDurationWithoutRetries',
189
+ 'totalToolDuration',
190
+ 'totalLinesAdded',
191
+ 'totalLinesRemoved',
192
+ 'modelUsage',
193
+ 'hasUnknownModelCost',
194
+ ]),
195
+ },
196
+ ],
197
+ [
198
+ 'fork-context-ref',
199
+ {
200
+ kind: 'fork-context-ref',
201
+ knownFields: fields([
202
+ 'type',
203
+ 'agentId',
204
+ 'parentSessionId',
205
+ 'parentLastUuid',
206
+ 'contextLength',
207
+ ]),
208
+ },
209
+ ],
153
210
  ]);
154
211
  const EMPTY_RECORD = Object.freeze({});
155
212
  function rawTypeKey(value) {
@@ -1 +1 @@
1
- export const PROJECTOR_VERSION = 10;
1
+ export const PROJECTOR_VERSION = 11;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@faithfulalabi/agent-lens",
3
- "version": "0.2.0",
3
+ "version": "0.4.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",