@faithfulalabi/agent-lens 0.1.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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/bin/agent-lens.js +40 -0
  4. package/bin/package.json +4 -0
  5. package/dist/src/archive/cron-log.js +41 -0
  6. package/dist/src/archive/discover.js +89 -0
  7. package/dist/src/archive/index.js +7 -0
  8. package/dist/src/archive/lock.js +119 -0
  9. package/dist/src/archive/log.js +15 -0
  10. package/dist/src/archive/mirror.js +366 -0
  11. package/dist/src/archive/paths.js +159 -0
  12. package/dist/src/archive/read.js +133 -0
  13. package/dist/src/archive/report.js +272 -0
  14. package/dist/src/archive/seal.js +76 -0
  15. package/dist/src/archive/sidecar.js +39 -0
  16. package/dist/src/cli/args.js +47 -0
  17. package/dist/src/cli/commands/archive.js +65 -0
  18. package/dist/src/cli/commands/doctor.js +191 -0
  19. package/dist/src/cli/commands/prune.js +159 -0
  20. package/dist/src/cli/commands/rebuild.js +98 -0
  21. package/dist/src/cli/commands/schedule.js +325 -0
  22. package/dist/src/cli/commands/start.js +83 -0
  23. package/dist/src/cli/commands/warm.js +96 -0
  24. package/dist/src/cli/index.js +102 -0
  25. package/dist/src/content/resolve.js +163 -0
  26. package/dist/src/corpus/env.js +32 -0
  27. package/dist/src/corpus/paths.js +70 -0
  28. package/dist/src/corpus/scan.js +85 -0
  29. package/dist/src/corpus/watch.js +189 -0
  30. package/dist/src/db/freshness.js +82 -0
  31. package/dist/src/db/open.js +74 -0
  32. package/dist/src/db/read.js +266 -0
  33. package/dist/src/db/schema.js +312 -0
  34. package/dist/src/db/sidecars.js +216 -0
  35. package/dist/src/db/spill-index.js +68 -0
  36. package/dist/src/db/write.js +279 -0
  37. package/dist/src/project/pipeline.js +307 -0
  38. package/dist/src/project/subagents.js +41 -0
  39. package/dist/src/project/tools.js +94 -0
  40. package/dist/src/server/api.js +249 -0
  41. package/dist/src/server/app.js +28 -0
  42. package/dist/src/server/config.js +24 -0
  43. package/dist/src/server/drift-report.js +35 -0
  44. package/dist/src/server/index.js +1 -0
  45. package/dist/src/server/live.js +109 -0
  46. package/dist/src/server/middleware/host-guard.js +42 -0
  47. package/dist/src/server/middleware/token-auth.js +19 -0
  48. package/dist/src/server/start.js +150 -0
  49. package/dist/src/server/static-ui.js +97 -0
  50. package/dist/src/server/stream.js +50 -0
  51. package/dist/src/server/warm.js +48 -0
  52. package/dist/src/shared/api.js +1 -0
  53. package/dist/src/shared/entities.js +1 -0
  54. package/dist/src/shared/index.js +2 -0
  55. package/dist/src/shared/pricing.js +68 -0
  56. package/dist/src/shared/token.js +39 -0
  57. package/dist/src/transcript/accessors.js +28 -0
  58. package/dist/src/transcript/agents.js +44 -0
  59. package/dist/src/transcript/blocks.js +75 -0
  60. package/dist/src/transcript/drift.js +42 -0
  61. package/dist/src/transcript/human.js +65 -0
  62. package/dist/src/transcript/line.js +251 -0
  63. package/dist/src/transcript/raw-types.js +1 -0
  64. package/dist/src/transcript/spill.js +122 -0
  65. package/dist/src/transcript/usage.js +63 -0
  66. package/dist/src/transcript/version.js +1 -0
  67. package/package.json +70 -0
  68. package/ui/dist/assets/index-CKKoKUCq.js +254 -0
  69. package/ui/dist/assets/index-Chza4fL6.css +1 -0
  70. package/ui/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  71. package/ui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  72. package/ui/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  73. package/ui/dist/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
  74. package/ui/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  75. package/ui/dist/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
  76. package/ui/dist/index.html +21 -0
@@ -0,0 +1,94 @@
1
+ import { claimsPersistedOutput, } from '../transcript/agents.js';
2
+ import { epochMs } from './pipeline.js';
3
+ export const INLINE_MAX = 65536;
4
+ export const PREVIEW_MAX = 8192;
5
+ export const NO_INPUT = Object.freeze({
6
+ input: undefined,
7
+ input_bytes: undefined,
8
+ input_storage: undefined,
9
+ });
10
+ function byteLength(text) {
11
+ return Buffer.byteLength(text, 'utf8');
12
+ }
13
+ function headPreview(text) {
14
+ const bytes = Buffer.from(text, 'utf8');
15
+ if (bytes.byteLength <= PREVIEW_MAX)
16
+ return text;
17
+ let cut = PREVIEW_MAX;
18
+ while (cut > 0 && (bytes[cut] & 0xc0) === 0x80)
19
+ cut -= 1;
20
+ return bytes.subarray(0, cut).toString('utf8');
21
+ }
22
+ export function toolInput(input) {
23
+ if (input === undefined) {
24
+ return { input: undefined, input_bytes: undefined, input_storage: 'absent' };
25
+ }
26
+ const text = JSON.stringify(input);
27
+ const bytes = byteLength(text);
28
+ return bytes > INLINE_MAX
29
+ ? { input: headPreview(text), input_bytes: bytes, input_storage: 'line_ref' }
30
+ : { input: text, input_bytes: bytes, input_storage: 'inline' };
31
+ }
32
+ function elapsedMs(call, result) {
33
+ const ms = epochMs(result) - epochMs(call);
34
+ return ms < 0 ? 0 : ms;
35
+ }
36
+ function statusOf(result) {
37
+ if (result.denial !== undefined)
38
+ return 'denied';
39
+ return result.is_error ? 'error' : 'ok';
40
+ }
41
+ function storeOutput(event, text) {
42
+ const bytes = byteLength(text);
43
+ event.text_bytes = bytes;
44
+ if (claimsPersistedOutput(text)) {
45
+ event.output_storage = 'spill';
46
+ event.text = undefined;
47
+ return;
48
+ }
49
+ if (bytes > INLINE_MAX) {
50
+ event.output_storage = 'line_ref';
51
+ event.text = headPreview(text);
52
+ return;
53
+ }
54
+ event.output_storage = 'inline';
55
+ event.text = text;
56
+ }
57
+ function backPatch(event, notification) {
58
+ const payload = notification?.result;
59
+ if (payload === undefined) {
60
+ event.text = undefined;
61
+ event.text_bytes = undefined;
62
+ event.output_storage = 'absent';
63
+ event.status = 'running';
64
+ event.agent_status = 'running';
65
+ return;
66
+ }
67
+ event.text = payload;
68
+ event.text_bytes = byteLength(payload);
69
+ event.output_storage = 'inline';
70
+ event.agent_status = notification?.status;
71
+ }
72
+ export function joinToolCalls(events, results, notifications, drift) {
73
+ const answers = new Map(notifications.flatMap((entry) => entry.toolCallId === undefined ? [] : [[entry.toolCallId, entry]]));
74
+ for (const event of events) {
75
+ if (event.kind !== 'tool_call')
76
+ continue;
77
+ const result = results.get(event.id);
78
+ if (result === undefined) {
79
+ drift.noteUnjoinedToolUse();
80
+ continue;
81
+ }
82
+ event.result_offset = result.result_offset;
83
+ event.result_len = result.result_len;
84
+ event.result_block = result.result_block;
85
+ event.duration_ms = elapsedMs(event.ts, result.ts);
86
+ event.duration_source = 'elapsed';
87
+ event.status = statusOf(result);
88
+ if (event.name === 'Agent' && result.launch !== undefined) {
89
+ backPatch(event, answers.get(event.id));
90
+ continue;
91
+ }
92
+ storeOutput(event, result.text);
93
+ }
94
+ }
@@ -0,0 +1,249 @@
1
+ import { streamSSE } from 'hono/streaming';
2
+ import { ensureProjectedFold, fingerprint } from '../db/freshness.js';
3
+ import { countUnprojected, readDriftRows, readEventContentRow, readEventCount, readEventPage, readHealthCounts, readMeta, readProjects, readSessionHeader, readSessionList, readTurns, searchEvents, } from '../db/read.js';
4
+ import { deleteSessionProjection, projectSession } from '../db/write.js';
5
+ import { aggregateDrift } from './drift-report.js';
6
+ export { aggregateDrift } from './drift-report.js';
7
+ export const DEFAULT_LIMIT = 50;
8
+ export const EVENT_PAGE_LIMIT = 1000;
9
+ export const MAX_LIMIT = 10_000;
10
+ const NON_NEGATIVE_INT = /^\d+$/;
11
+ const RANGE = /^(\d+)-(\d*)$/;
12
+ export function parsePageParams(query, fallbackLimit = DEFAULT_LIMIT) {
13
+ let limit = fallbackLimit;
14
+ const rawLimit = query.limit;
15
+ if (rawLimit !== undefined && rawLimit !== '') {
16
+ if (!NON_NEGATIVE_INT.test(rawLimit))
17
+ return { ok: false, error: 'invalid limit' };
18
+ const parsed = Number(rawLimit);
19
+ if (parsed === 0)
20
+ return { ok: false, error: 'invalid limit' };
21
+ limit = Math.min(parsed, MAX_LIMIT);
22
+ }
23
+ let offset = 0;
24
+ const rawOffset = query.offset;
25
+ if (rawOffset !== undefined && rawOffset !== '') {
26
+ if (!NON_NEGATIVE_INT.test(rawOffset))
27
+ return { ok: false, error: 'invalid offset' };
28
+ offset = Number(rawOffset);
29
+ if (!Number.isSafeInteger(offset))
30
+ return { ok: false, error: 'invalid offset' };
31
+ }
32
+ return { ok: true, value: { limit, offset } };
33
+ }
34
+ export function parseFromSeq(raw) {
35
+ if (raw === undefined || raw === '')
36
+ return { ok: true, value: 0 };
37
+ if (!NON_NEGATIVE_INT.test(raw))
38
+ return { ok: false, error: 'invalid from_seq' };
39
+ const value = Number(raw);
40
+ if (!Number.isSafeInteger(value))
41
+ return { ok: false, error: 'invalid from_seq' };
42
+ return { ok: true, value };
43
+ }
44
+ const SORTS = ['recent', 'cost', 'tokens', 'errors'];
45
+ export function parseSort(raw) {
46
+ if (raw === undefined || raw === '')
47
+ return { ok: true, value: 'recent' };
48
+ const sort = SORTS.find((known) => known === raw);
49
+ return sort === undefined ? { ok: false, error: 'invalid sort' } : { ok: true, value: sort };
50
+ }
51
+ const FIELDS = ['text', 'input'];
52
+ export function parseField(raw) {
53
+ if (raw === undefined || raw === '')
54
+ return { ok: true, value: 'text' };
55
+ const field = FIELDS.find((known) => known === raw);
56
+ return field === undefined ? { ok: false, error: 'invalid field' } : { ok: true, value: field };
57
+ }
58
+ export function parseRange(raw) {
59
+ if (raw === undefined || raw === '')
60
+ return { ok: true, value: undefined };
61
+ const match = RANGE.exec(raw);
62
+ if (match === null)
63
+ return { ok: false, error: 'invalid range' };
64
+ const start = Number(match[1]);
65
+ if (!Number.isSafeInteger(start))
66
+ return { ok: false, error: 'invalid range' };
67
+ if (match[2] === '')
68
+ return { ok: true, value: { start } };
69
+ const end = Number(match[2]);
70
+ if (!Number.isSafeInteger(end))
71
+ return { ok: false, error: 'invalid range' };
72
+ if (end < start)
73
+ return { ok: false, error: 'invalid range' };
74
+ return { ok: true, value: { start, end } };
75
+ }
76
+ export function clampRange(range, byteSize) {
77
+ const start = Math.min(range?.start ?? 0, byteSize);
78
+ const requestedEnd = range?.end ?? byteSize - 1;
79
+ const end = Math.max(start - 1, Math.min(requestedEnd, byteSize - 1));
80
+ return { start, end, length: end - start + 1 };
81
+ }
82
+ export function parseSearchQuery(query) {
83
+ const q = query.q;
84
+ if (q === undefined || q === '' || q.includes('\0'))
85
+ return { ok: false, error: 'invalid q' };
86
+ const page = parsePageParams(query);
87
+ if (!page.ok)
88
+ return page;
89
+ const value = { q, limit: page.value.limit };
90
+ if (query.session !== undefined && query.session !== '')
91
+ value.session = query.session;
92
+ return { ok: true, value };
93
+ }
94
+ export const LIVE_WINDOW_MS = 60_000;
95
+ export function isLive(last_activity_at, now) {
96
+ const at = Date.parse(last_activity_at);
97
+ return !Number.isNaN(at) && now - at < LIVE_WINDOW_MS;
98
+ }
99
+ const PROJECTION_STATES = ['none', 'ready', 'failed', 'empty'];
100
+ const ARCHIVE_UNREADABLE = 'archive unreadable: the projection could not be verified';
101
+ export function toDetailProjection(header, outcome) {
102
+ const stored = header.projection;
103
+ if (outcome === 'failed') {
104
+ return { ...stored, state: 'failed', error: stored.error ?? ARCHIVE_UNREADABLE };
105
+ }
106
+ return PROJECTION_STATES.includes(stored.state) ? stored : { ...stored, state: 'none' };
107
+ }
108
+ function storedContent(row, field) {
109
+ const isText = field === 'text';
110
+ const resolved = {
111
+ storage: (isText ? row.output_storage : row.input_storage) ?? 'absent',
112
+ content: (isText ? row.text : row.input) ?? '',
113
+ byte_size: (isText ? row.text_bytes : row.input_bytes) ?? 0,
114
+ };
115
+ if (isText && row.spill_path !== null)
116
+ resolved.spill_path = row.spill_path;
117
+ return resolved;
118
+ }
119
+ export function jsonNotFound(c) {
120
+ return c.json({ error: 'not found' }, 404);
121
+ }
122
+ export function registerApi(app, deps) {
123
+ const { db, env } = deps;
124
+ app.get('/api/sessions', (c) => {
125
+ const query = c.req.query();
126
+ const page = parsePageParams(query);
127
+ if (!page.ok)
128
+ return c.json({ error: page.error }, 400);
129
+ const sort = parseSort(query.sort);
130
+ if (!sort.ok)
131
+ return c.json({ error: sort.error }, 400);
132
+ const filter = { ...page.value, sort: sort.value };
133
+ if (query.project !== undefined && query.project !== '')
134
+ filter.project = query.project;
135
+ if (query.q !== undefined && query.q !== '')
136
+ filter.q = query.q;
137
+ const listed = readSessionList(db, filter);
138
+ const now = Date.now();
139
+ return c.json({
140
+ ...listed,
141
+ items: listed.items.map((row) => ({ ...row, live: isLive(row.last_activity_at, now) })),
142
+ });
143
+ });
144
+ app.get('/api/projects', (c) => c.json(readProjects(db)));
145
+ app.get('/api/sessions/:id', (c) => {
146
+ const query = c.req.query();
147
+ const page = parsePageParams(query, EVENT_PAGE_LIMIT);
148
+ if (!page.ok)
149
+ return c.json({ error: page.error }, 400);
150
+ const from_seq = parseFromSeq(query.from_seq);
151
+ if (!from_seq.ok)
152
+ return c.json({ error: from_seq.error }, 400);
153
+ const id = c.req.param('id');
154
+ const gate = ensureProjectedFold(db, id, env);
155
+ if (gate.outcome === 'unindexed')
156
+ return jsonNotFound(c);
157
+ const header = readSessionHeader(db, id);
158
+ if (header === undefined)
159
+ return jsonNotFound(c);
160
+ const events = readEventPage(db, id, { from_seq: from_seq.value, limit: page.value.limit });
161
+ return c.json({
162
+ session: {
163
+ ...header,
164
+ live: isLive(header.last_activity_at, Date.now()),
165
+ projection: toDetailProjection(header, gate.outcome),
166
+ },
167
+ turns: readTurns(db, id),
168
+ events: events.items,
169
+ next_seq: events.next_seq,
170
+ has_more: events.has_more,
171
+ fingerprint: gate.fold === undefined ? '' : fingerprint(gate.fold),
172
+ });
173
+ });
174
+ app.get('/api/events/:id/content', (c) => {
175
+ const field = parseField(c.req.query('field'));
176
+ if (!field.ok)
177
+ return c.json({ error: field.error }, 400);
178
+ const range = parseRange(c.req.query('range'));
179
+ if (!range.ok)
180
+ return c.json({ error: range.error }, 400);
181
+ const row = readEventContentRow(db, c.req.param('id'));
182
+ if (row === undefined)
183
+ return jsonNotFound(c);
184
+ const stored = storedContent(row, field.value);
185
+ const resolved = stored.storage === 'inline' || stored.storage === 'absent'
186
+ ? stored
187
+ : (deps.resolveContent?.(row, field.value) ?? { ...stored, storage: 'missing' });
188
+ const bytes = Buffer.from(resolved.content, 'utf8');
189
+ const clamped = clampRange(range.value, bytes.length);
190
+ const body = {
191
+ id: row.id,
192
+ field: field.value,
193
+ storage: resolved.storage,
194
+ byte_size: resolved.byte_size,
195
+ range: { start: clamped.start, end: clamped.end },
196
+ content: bytes.subarray(clamped.start, clamped.start + clamped.length).toString('utf8'),
197
+ truncated: clamped.length < resolved.byte_size,
198
+ };
199
+ if (resolved.spill_path !== undefined)
200
+ body.spill_path = resolved.spill_path;
201
+ return c.json(body);
202
+ });
203
+ app.get('/api/search', (c) => {
204
+ const parsed = parseSearchQuery(c.req.query());
205
+ if (!parsed.ok)
206
+ return c.json({ error: parsed.error }, 400);
207
+ return c.json({
208
+ items: searchEvents(db, parsed.value),
209
+ scope: parsed.value.session === undefined ? 'projected' : 'session',
210
+ unprojected_count: countUnprojected(db),
211
+ });
212
+ });
213
+ app.get('/api/stream', (c) => streamSSE(c, (stream) => deps.hub.attach(stream)));
214
+ app.post('/api/sessions/:id/reproject', (c) => {
215
+ const id = c.req.param('id');
216
+ const started = Date.now();
217
+ const gate = ensureProjectedFold(db, id, env);
218
+ if (gate.outcome === 'unindexed')
219
+ return jsonNotFound(c);
220
+ if (gate.outcome === 'hit' && gate.fold !== undefined) {
221
+ deleteSessionProjection(db, id);
222
+ projectSession(db, id, env, gate.fold);
223
+ }
224
+ const header = readSessionHeader(db, id);
225
+ if (header === undefined)
226
+ return jsonNotFound(c);
227
+ return c.json({
228
+ session: { ...header, live: isLive(header.last_activity_at, Date.now()) },
229
+ event_count: readEventCount(db, id),
230
+ turn_count: header.turn_count,
231
+ took_ms: Date.now() - started,
232
+ });
233
+ });
234
+ app.post('/api/warm', (c) => c.json({ queued: deps.warm.start() }, 202));
235
+ app.get('/api/drift', (c) => c.json({
236
+ projector_version: readMeta(db, 'projector_version') ?? null,
237
+ schema_version: readMeta(db, 'schema_version') ?? null,
238
+ ...aggregateDrift(readDriftRows(db)),
239
+ }));
240
+ app.get('/api/health', (c) => c.json({
241
+ ok: true,
242
+ projects_root: readMeta(db, 'projects_root') ?? null,
243
+ index_built_at: readMeta(db, 'index_built_at') ?? null,
244
+ files_indexed: deps.sweep?.report().walked ?? null,
245
+ ...readHealthCounts(db),
246
+ schema_version: readMeta(db, 'schema_version') ?? null,
247
+ projector_version: readMeta(db, 'projector_version') ?? null,
248
+ }));
249
+ }
@@ -0,0 +1,28 @@
1
+ import { Hono } from 'hono';
2
+ import { hostGuard, resolveBindHosts } from './middleware/host-guard.js';
3
+ import { tokenAuth } from './middleware/token-auth.js';
4
+ import { jsonNotFound as apiNotFound, registerApi } from './api.js';
5
+ import { makeServeIndex, registerUi } from './static-ui.js';
6
+ const jsonErrorOnApiPaths = (err, c) => {
7
+ if ('getResponse' in err) {
8
+ const res = err.getResponse();
9
+ return c.newResponse(res.body, res);
10
+ }
11
+ console.error(err);
12
+ if (c.req.path.startsWith('/api/')) {
13
+ return c.json({ error: 'internal error' }, 500);
14
+ }
15
+ return c.text('Internal Server Error', 500);
16
+ };
17
+ export function buildApiApp(deps) {
18
+ const { token, host, uiDir } = deps;
19
+ const app = new Hono();
20
+ app.use('*', hostGuard(host === undefined ? [] : resolveBindHosts(host)));
21
+ app.get('/', makeServeIndex({ token, uiDir }));
22
+ app.use('/api/*', tokenAuth(token));
23
+ registerApi(app, deps);
24
+ app.all('/api/*', apiNotFound);
25
+ registerUi(app, { token, uiDir });
26
+ app.onError(jsonErrorOnApiPaths);
27
+ return app;
28
+ }
@@ -0,0 +1,24 @@
1
+ import { chmodSync, readFileSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ const CONFIG_FILE = 'config.json';
4
+ export function writeConfig(dataDir, config) {
5
+ const target = join(dataDir, CONFIG_FILE);
6
+ const temp = join(dataDir, `${CONFIG_FILE}.tmp.${process.pid}`);
7
+ writeFileSync(temp, JSON.stringify(config, null, 2), { mode: 0o600 });
8
+ chmodSync(temp, 0o600);
9
+ renameSync(temp, target);
10
+ }
11
+ export function readConfig(dataDir) {
12
+ try {
13
+ return JSON.parse(readFileSync(join(dataDir, CONFIG_FILE), 'utf8'));
14
+ }
15
+ catch (err) {
16
+ if (err.code === 'ENOENT') {
17
+ return null;
18
+ }
19
+ throw err;
20
+ }
21
+ }
22
+ export function clearConfig(dataDir) {
23
+ rmSync(join(dataDir, CONFIG_FILE), { force: true });
24
+ }
@@ -0,0 +1,35 @@
1
+ const NO_DRIFT = '{}';
2
+ function mergeBucket(into, from) {
3
+ for (const [key, n] of Object.entries(from ?? {}))
4
+ into[key] = (into[key] ?? 0) + n;
5
+ }
6
+ export function aggregateDrift(rows) {
7
+ const report = {
8
+ harness_versions: {},
9
+ unknown_line_types: {},
10
+ unknown_block_types: {},
11
+ unknown_top_level_fields: {},
12
+ unjoined_tool_uses: 0,
13
+ unresolved_spills: 0,
14
+ sessions_with_drift: [],
15
+ };
16
+ for (const row of rows) {
17
+ const version = row.harness_version ?? 'unknown';
18
+ report.harness_versions[version] = (report.harness_versions[version] ?? 0) + 1;
19
+ if (row.drift_json === NO_DRIFT)
20
+ continue;
21
+ const counts = JSON.parse(row.drift_json);
22
+ mergeBucket(report.unknown_line_types, counts.unknown_line_types);
23
+ mergeBucket(report.unknown_block_types, counts.unknown_block_types);
24
+ mergeBucket(report.unknown_top_level_fields, counts.unknown_top_level_fields);
25
+ report.unjoined_tool_uses += counts.unjoined_tool_uses ?? 0;
26
+ report.unresolved_spills += counts.unresolved_spills ?? 0;
27
+ report.sessions_with_drift.push({
28
+ id: row.id,
29
+ title: row.title,
30
+ harness_version: row.harness_version,
31
+ counts,
32
+ });
33
+ }
34
+ return report;
35
+ }
@@ -0,0 +1 @@
1
+ export { startServer } from './start.js';
@@ -0,0 +1,109 @@
1
+ import { ensureProjectedFold, fingerprint } from '../db/freshness.js';
2
+ import { readEventsByIds, readRunningEventIds, readSessionHeader, readTurns, } from '../db/read.js';
3
+ const INTERVAL_MS = 1000;
4
+ const SLOW_MS = 100;
5
+ const SLOW_INTERVAL_MS = 5000;
6
+ const DEADLINE_MS = 250;
7
+ function ownRollups(header) {
8
+ return {
9
+ last_activity_at: header.last_activity_at,
10
+ turn_count: header.turn_count,
11
+ tool_call_count: header.tool_call_count,
12
+ error_count: header.error_count,
13
+ tokens_in: header.tokens_in,
14
+ tokens_out: header.tokens_out,
15
+ tokens_cache_read: header.tokens_cache_read,
16
+ tokens_cache_write: header.tokens_cache_write,
17
+ est_cost: header.est_cost,
18
+ };
19
+ }
20
+ export function startLiveTick(options) {
21
+ const { db, env, sweep, hub } = options;
22
+ const now = options.now ?? Date.now;
23
+ const intervalMs = options.intervalMs ?? INTERVAL_MS;
24
+ const backoffAt = new Map();
25
+ const pending = new Set();
26
+ const announced = new Set();
27
+ const tick = async () => {
28
+ const startedAt = now();
29
+ const report = sweep.wave1();
30
+ for (const id of report.indexed_ids) {
31
+ if (announced.has(id))
32
+ continue;
33
+ announced.add(id);
34
+ const frame = { session_id: id };
35
+ await hub.publish('session_indexed', frame);
36
+ }
37
+ const candidates = [...pending, ...report.indexed_ids.filter((id) => !pending.has(id))];
38
+ let visited = 0;
39
+ for (const id of candidates) {
40
+ if ((backoffAt.get(id) ?? 0) > now()) {
41
+ pending.add(id);
42
+ continue;
43
+ }
44
+ if (visited > 0 && now() - startedAt >= DEADLINE_MS) {
45
+ pending.add(id);
46
+ continue;
47
+ }
48
+ visited += 1;
49
+ const before = new Set(readRunningEventIds(db, id));
50
+ const startedProjection = now();
51
+ const gate = ensureProjectedFold(db, id, env);
52
+ if (now() - startedProjection > SLOW_MS)
53
+ backoffAt.set(id, now() + SLOW_INTERVAL_MS);
54
+ if (gate.outcome === 'failed') {
55
+ pending.add(id);
56
+ continue;
57
+ }
58
+ if (gate.outcome !== 'projected' || gate.fold === undefined) {
59
+ pending.delete(id);
60
+ continue;
61
+ }
62
+ pending.delete(id);
63
+ const header = readSessionHeader(db, id);
64
+ if (header === undefined)
65
+ continue;
66
+ const after = new Set(readRunningEventIds(db, id));
67
+ const stopped = [...before].filter((eventId) => !after.has(eventId));
68
+ const turns = readTurns(db, id);
69
+ const last = turns[turns.length - 1];
70
+ const frame = {
71
+ session_id: id,
72
+ fingerprint: fingerprint(gate.fold),
73
+ from_seq: last === undefined ? 0 : last.first_seq,
74
+ patched: readEventsByIds(db, id, stopped),
75
+ rollups: ownRollups(header),
76
+ };
77
+ await hub.publish('session_changed', frame);
78
+ }
79
+ sweep.wave2();
80
+ };
81
+ let inFlight = false;
82
+ const fire = async () => {
83
+ if (inFlight)
84
+ return;
85
+ inFlight = true;
86
+ try {
87
+ await tick();
88
+ }
89
+ catch (error) {
90
+ console.error(error);
91
+ }
92
+ finally {
93
+ inFlight = false;
94
+ }
95
+ };
96
+ let timer;
97
+ if (intervalMs !== 0) {
98
+ timer = setInterval(() => void fire(), intervalMs);
99
+ timer.unref?.();
100
+ }
101
+ return {
102
+ tick,
103
+ close: () => {
104
+ if (timer !== undefined)
105
+ clearInterval(timer);
106
+ timer = undefined;
107
+ },
108
+ };
109
+ }
@@ -0,0 +1,42 @@
1
+ import { networkInterfaces } from 'node:os';
2
+ const ALLOWED_HOSTNAMES = new Set([
3
+ 'localhost',
4
+ '127.0.0.1',
5
+ '[::1]',
6
+ '::1',
7
+ ]);
8
+ function isLoopbackBind(host) {
9
+ return ALLOWED_HOSTNAMES.has(host);
10
+ }
11
+ export function resolveBindHosts(host) {
12
+ if (isLoopbackBind(host)) {
13
+ return [];
14
+ }
15
+ const hosts = [];
16
+ for (const addrs of Object.values(networkInterfaces())) {
17
+ for (const addr of addrs ?? []) {
18
+ if (addr.internal)
19
+ continue;
20
+ hosts.push(addr.family === 'IPv6' ? `[${addr.address}]` : addr.address);
21
+ }
22
+ }
23
+ return hosts;
24
+ }
25
+ function hostnameOf(host) {
26
+ if (host.startsWith('[')) {
27
+ const end = host.indexOf(']');
28
+ return end === -1 ? host : host.slice(0, end + 1);
29
+ }
30
+ const colon = host.indexOf(':');
31
+ return colon === -1 ? host : host.slice(0, colon);
32
+ }
33
+ export function hostGuard(extraHosts = []) {
34
+ const allowed = new Set([...ALLOWED_HOSTNAMES, ...extraHosts]);
35
+ return async (c, next) => {
36
+ const host = c.req.header('host');
37
+ if (host === undefined || !allowed.has(hostnameOf(host))) {
38
+ return c.text('Forbidden', 403);
39
+ }
40
+ return next();
41
+ };
42
+ }
@@ -0,0 +1,19 @@
1
+ import { timingSafeEqual } from 'node:crypto';
2
+ import { TOKEN_HEADER } from '../../shared/index.js';
3
+ function safeEqual(a, b) {
4
+ const ab = Buffer.from(a);
5
+ const bb = Buffer.from(b);
6
+ if (ab.length !== bb.length) {
7
+ return false;
8
+ }
9
+ return timingSafeEqual(ab, bb);
10
+ }
11
+ export function tokenAuth(expected) {
12
+ return async (c, next) => {
13
+ const provided = c.req.header(TOKEN_HEADER);
14
+ if (provided === undefined || !safeEqual(provided, expected)) {
15
+ return c.text('Unauthorized', 401);
16
+ }
17
+ return next();
18
+ };
19
+ }