@overmux/pi 0.0.1

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 (70) hide show
  1. package/README.md +118 -0
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +40 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/config-2jNv4tol.d.ts +34 -0
  6. package/dist/config-2jNv4tol.d.ts.map +1 -0
  7. package/dist/config.d.ts +2 -0
  8. package/dist/config.js +47 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/extension.d.ts +19 -0
  11. package/dist/extension.d.ts.map +1 -0
  12. package/dist/extension.js +265 -0
  13. package/dist/extension.js.map +1 -0
  14. package/dist/index.d.ts +10 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +16 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/jsonl-tail.d.ts +13 -0
  19. package/dist/jsonl-tail.d.ts.map +1 -0
  20. package/dist/jsonl-tail.js +83 -0
  21. package/dist/jsonl-tail.js.map +1 -0
  22. package/dist/live-events-ClhFOMGW.js +497 -0
  23. package/dist/live-events-ClhFOMGW.js.map +1 -0
  24. package/dist/live-events-DAmf6RRx.d.ts +107 -0
  25. package/dist/live-events-DAmf6RRx.d.ts.map +1 -0
  26. package/dist/live-events.d.ts +2 -0
  27. package/dist/live-events.js +2 -0
  28. package/dist/notification-DzOd9cRc.d.ts +20 -0
  29. package/dist/notification-DzOd9cRc.d.ts.map +1 -0
  30. package/dist/notification.d.ts +2 -0
  31. package/dist/notification.js +54 -0
  32. package/dist/notification.js.map +1 -0
  33. package/dist/plugin.d.ts +90 -0
  34. package/dist/plugin.d.ts.map +1 -0
  35. package/dist/plugin.js +213 -0
  36. package/dist/plugin.js.map +1 -0
  37. package/dist/projection.d.ts +101 -0
  38. package/dist/projection.d.ts.map +1 -0
  39. package/dist/projection.js +550 -0
  40. package/dist/projection.js.map +1 -0
  41. package/dist/protocol-CsrnSPOv.d.ts +115 -0
  42. package/dist/protocol-CsrnSPOv.d.ts.map +1 -0
  43. package/dist/protocol.d.ts +2 -0
  44. package/dist/protocol.js +365 -0
  45. package/dist/protocol.js.map +1 -0
  46. package/dist/react.d.ts +139 -0
  47. package/dist/react.d.ts.map +1 -0
  48. package/dist/react.js +796 -0
  49. package/dist/react.js.map +1 -0
  50. package/dist/server.d.ts +5 -0
  51. package/dist/server.js +4 -0
  52. package/dist/session-status-CZLTo8Km.d.ts +71 -0
  53. package/dist/session-status-CZLTo8Km.d.ts.map +1 -0
  54. package/dist/styles.css +619 -0
  55. package/docs/index.md +10 -0
  56. package/package.json +115 -0
  57. package/src/cli.ts +59 -0
  58. package/src/config.ts +87 -0
  59. package/src/extension.ts +486 -0
  60. package/src/index.ts +14 -0
  61. package/src/jsonl-tail.ts +110 -0
  62. package/src/live-events.ts +578 -0
  63. package/src/notification.ts +109 -0
  64. package/src/plugin.ts +369 -0
  65. package/src/projection.ts +995 -0
  66. package/src/protocol.ts +805 -0
  67. package/src/react.tsx +1293 -0
  68. package/src/server.ts +15 -0
  69. package/src/session-status.ts +379 -0
  70. package/src/styles.css +619 -0
@@ -0,0 +1,486 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ } from "@earendil-works/pi-coding-agent";
5
+
6
+ import {
7
+ OvermuxPiConfigSchema,
8
+ type OvermuxPiConfig,
9
+ type OvermuxPiConfigInput,
10
+ } from "./config.js";
11
+ import {
12
+ compactAssistantMessageEvent,
13
+ createLiveEventWriter,
14
+ liveEventSessionDir,
15
+ liveEventStreamPath,
16
+ PROCESS_INSTANCE_ID,
17
+ registerLiveEventCapture,
18
+ type CompactAssistantMessageEvent,
19
+ type LiveEventPayload,
20
+ type LiveEventRecord,
21
+ type LiveEventWriter,
22
+ } from "./live-events.js";
23
+ import {
24
+ inspectDelegate,
25
+ OVERMUX_PI_DELEGATE_SETTLED_MESSAGE_TYPE,
26
+ notificationMessage,
27
+ notificationRenderer,
28
+ type DelegateNotificationDetails,
29
+ } from "./notification.js";
30
+ import {
31
+ CHILD_ENV,
32
+ DEFAULT_OVERMUX_PI_RUNTIME_DIR,
33
+ ensureRuntimeDir,
34
+ eventKey,
35
+ PARENT_SESSION_ENV,
36
+ parseEnvelope,
37
+ sendEnvelope,
38
+ sendUserMessage,
39
+ socketPathForSession,
40
+ startReceiver,
41
+ TASK_SLUG_ENV,
42
+ TASK_SLUG_PATTERN,
43
+ type AbortRequest,
44
+ type AbortResponse,
45
+ type DelegateSettledEnvelope,
46
+ type SetModelRequest,
47
+ type SetModelResponse,
48
+ type SetThinkingLevelRequest,
49
+ type SetThinkingLevelResponse,
50
+ type UserMessageRequest,
51
+ type UserMessageResponse,
52
+ } from "./protocol.js";
53
+
54
+ export {
55
+ CHILD_ENV,
56
+ eventKey,
57
+ PARENT_SESSION_ENV,
58
+ TASK_SLUG_ENV,
59
+ } from "./protocol.js";
60
+ export {
61
+ OVERMUX_PI_DELEGATE_SETTLED_MESSAGE_TYPE,
62
+ truncatePiJqOutput,
63
+ } from "./notification.js";
64
+ export {
65
+ createPiSessionStatusWriter,
66
+ isLocalPidAlive,
67
+ piSessionStatusPath,
68
+ piSessionStatusSchema,
69
+ parsePiSessionStatus,
70
+ type PiSessionStatus,
71
+ type PiSessionStatusUpdate,
72
+ type PiSessionStatusWriter,
73
+ } from "./session-status.js";
74
+ export {
75
+ compactAssistantMessageEvent,
76
+ createLiveEventWriter,
77
+ liveEventSessionDir,
78
+ liveEventStreamPath,
79
+ parseLiveEventRecord,
80
+ PROCESS_INSTANCE_ID,
81
+ type CompactAssistantMessageEvent,
82
+ type LiveEventEnvelope,
83
+ type LiveEventPayload,
84
+ type LiveEventRecord,
85
+ type LiveEventWireRecord,
86
+ type LiveEventWriter,
87
+ } from "./live-events.js";
88
+ export {
89
+ parseEnvelope,
90
+ probePiSession,
91
+ sendEnvelope,
92
+ sendUserMessage,
93
+ socketPathForSession,
94
+ startReceiver,
95
+ parseUserMessageRequest,
96
+ type DelegateSettledEnvelope,
97
+ type ProbeOptions,
98
+ type Receiver,
99
+ type ReceiverOptions,
100
+ type SenderOptions,
101
+ type UserMessageInput,
102
+ type UserMessageRequest,
103
+ type UserMessageResponse,
104
+ } from "./protocol.js";
105
+
106
+ export const OVERMUX_PI_DELEGATE_SETTLED_RECEIPT_TYPE =
107
+ "overmux-pi.delegate-settled-receipt";
108
+
109
+ export type OvermuxPiOptions = OvermuxPiConfigInput & {
110
+ env?: NodeJS.ProcessEnv;
111
+ runtimeDir?: string;
112
+ ackTimeoutMs?: number;
113
+ retryDelaysMs?: readonly number[];
114
+ onLiveEventError?: (error: unknown) => void;
115
+ };
116
+
117
+ const notificationEnvelope = (details: unknown) => {
118
+ if (typeof details !== "object" || details === null) {
119
+ return undefined;
120
+ }
121
+ const value = details as Record<string, unknown>;
122
+ return parseEnvelope(
123
+ JSON.stringify({
124
+ childSessionId: value.childSessionId,
125
+ taskSlug: value.taskSlug,
126
+ leafId: value.leafId,
127
+ cwd: value.cwd,
128
+ timestamp: value.timestamp,
129
+ }),
130
+ );
131
+ };
132
+
133
+ const restoreReceipts = (ctx: ExtensionContext) => {
134
+ const receipts = new Map<string, DelegateSettledEnvelope>();
135
+ const delivered = new Set<string>();
136
+ ctx.sessionManager.getEntries().forEach((entry) => {
137
+ if (
138
+ entry.type === "custom" &&
139
+ entry.customType === OVERMUX_PI_DELEGATE_SETTLED_RECEIPT_TYPE
140
+ ) {
141
+ const envelope = parseEnvelope(JSON.stringify(entry.data));
142
+ if (envelope) {
143
+ receipts.set(eventKey(envelope), envelope);
144
+ }
145
+ }
146
+ if (
147
+ entry.type === "custom_message" &&
148
+ entry.customType === OVERMUX_PI_DELEGATE_SETTLED_MESSAGE_TYPE
149
+ ) {
150
+ const envelope = notificationEnvelope(entry.details);
151
+ if (envelope) {
152
+ delivered.add(eventKey(envelope));
153
+ }
154
+ }
155
+ });
156
+ return {
157
+ eventKeys: new Set(receipts.keys()),
158
+ pending: [...receipts.values()].filter(
159
+ (envelope) => !delivered.has(eventKey(envelope)),
160
+ ),
161
+ };
162
+ };
163
+
164
+ const requiredTaskSlug = (taskSlug: string | undefined): string => {
165
+ if (!taskSlug || !TASK_SLUG_PATTERN.test(taskSlug)) {
166
+ throw new Error(`Child Pi requires a valid ${TASK_SLUG_ENV}`);
167
+ }
168
+ return taskSlug;
169
+ };
170
+
171
+ const rememberResponse = <Response extends { requestId: string }>(
172
+ responses: Map<string, Response>,
173
+ response: Response,
174
+ ): Response => {
175
+ responses.set(response.requestId, response);
176
+ if (responses.size > 128) {
177
+ responses.delete(responses.keys().next().value!);
178
+ }
179
+ return response;
180
+ };
181
+
182
+ const injectNotification = async (
183
+ pi: ExtensionAPI,
184
+ ctx: ExtensionContext,
185
+ envelope: DelegateSettledEnvelope,
186
+ signal: AbortSignal,
187
+ config: OvermuxPiConfig,
188
+ ): Promise<void> => {
189
+ if (signal.aborted) {
190
+ return;
191
+ }
192
+ const details = await inspectDelegate(pi, envelope, signal, config);
193
+ if (signal.aborted) {
194
+ return;
195
+ }
196
+ pi.sendMessage(
197
+ notificationMessage(envelope, details, config),
198
+ ctx.isIdle() ? { triggerTurn: true } : { deliverAs: "steer" },
199
+ );
200
+ };
201
+
202
+ export const overmuxPi = (
203
+ pi: ExtensionAPI,
204
+ options: OvermuxPiOptions = {},
205
+ ): void => {
206
+ pi.registerMessageRenderer<DelegateNotificationDetails>(
207
+ OVERMUX_PI_DELEGATE_SETTLED_MESSAGE_TYPE,
208
+ notificationRenderer,
209
+ );
210
+
211
+ const {
212
+ inspectionCommand,
213
+ inspectionTimeoutMs,
214
+ supervisionPrompt,
215
+ liveEventsDir,
216
+ } = options;
217
+ const config = OvermuxPiConfigSchema.parse({
218
+ inspectionCommand,
219
+ inspectionTimeoutMs,
220
+ supervisionPrompt,
221
+ liveEventsDir,
222
+ });
223
+ const env = options.env ?? process.env;
224
+ const isChild = env[CHILD_ENV] === "1";
225
+ const parentSessionId = isChild ? env[PARENT_SESSION_ENV] : undefined;
226
+ const childTaskSlug = isChild
227
+ ? requiredTaskSlug(env[TASK_SLUG_ENV])
228
+ : undefined;
229
+ const runtimeDir = options.runtimeDir ?? DEFAULT_OVERMUX_PI_RUNTIME_DIR;
230
+ const parentSocket = parentSessionId
231
+ ? socketPathForSession(parentSessionId, runtimeDir)
232
+ : undefined;
233
+ let receiver: Awaited<ReturnType<typeof startReceiver>> | undefined;
234
+ let activeSessionId: string | undefined;
235
+ let sessionController: AbortController | undefined;
236
+ let eventKeys = new Set<string>();
237
+ let abortResponses = new Map<string, AbortResponse>();
238
+ let modelResponses = new Map<string, SetModelResponse>();
239
+ let thinkingResponses = new Map<string, SetThinkingLevelResponse>();
240
+ let userMessageResponses = new Map<string, UserMessageResponse>();
241
+ const liveEvents = registerLiveEventCapture(pi, {
242
+ rootDir: config.liveEventsDir,
243
+ env,
244
+ onError: options.onLiveEventError,
245
+ });
246
+
247
+ pi.on("session_start", async (event, ctx) => {
248
+ await liveEvents.start(event, ctx);
249
+ const previousReceiver = receiver;
250
+ receiver = undefined;
251
+ sessionController?.abort();
252
+ const controller = new AbortController();
253
+ sessionController = controller;
254
+ abortResponses = new Map();
255
+ modelResponses = new Map();
256
+ thinkingResponses = new Map();
257
+ userMessageResponses = new Map();
258
+ await previousReceiver?.close();
259
+ const ownSessionId = ctx.sessionManager.getSessionId();
260
+ activeSessionId = ownSessionId;
261
+ const restored = restoreReceipts(ctx);
262
+ eventKeys = restored.eventKeys;
263
+ await ensureRuntimeDir(runtimeDir);
264
+ const startedReceiver = await startReceiver(
265
+ socketPathForSession(ownSessionId, runtimeDir),
266
+ (envelope) => {
267
+ if (controller.signal.aborted || sessionController !== controller) {
268
+ return;
269
+ }
270
+ const key = eventKey(envelope);
271
+ if (eventKeys.has(key)) {
272
+ return;
273
+ }
274
+ pi.appendEntry(OVERMUX_PI_DELEGATE_SETTLED_RECEIPT_TYPE, envelope);
275
+ eventKeys.add(key);
276
+ void injectNotification(
277
+ pi,
278
+ ctx,
279
+ envelope,
280
+ controller.signal,
281
+ config,
282
+ ).catch(() => undefined);
283
+ },
284
+ {
285
+ onSetModel: async (
286
+ request: SetModelRequest,
287
+ ): Promise<SetModelResponse> => {
288
+ const previous = modelResponses.get(request.requestId);
289
+ if (previous) {
290
+ return previous;
291
+ }
292
+ if (controller.signal.aborted || sessionController !== controller) {
293
+ return rememberResponse(modelResponses, {
294
+ version: 1,
295
+ requestId: request.requestId,
296
+ ok: false,
297
+ error: "shutting_down",
298
+ });
299
+ }
300
+ const model = ctx.modelRegistry.find(request.provider, request.id);
301
+ if (!model) {
302
+ return rememberResponse(modelResponses, {
303
+ version: 1,
304
+ requestId: request.requestId,
305
+ ok: false,
306
+ error: "not_found",
307
+ });
308
+ }
309
+ try {
310
+ if (!(await pi.setModel(model))) {
311
+ return rememberResponse(modelResponses, {
312
+ version: 1,
313
+ requestId: request.requestId,
314
+ ok: false,
315
+ error: "no_key",
316
+ });
317
+ }
318
+ return rememberResponse(modelResponses, {
319
+ version: 1,
320
+ requestId: request.requestId,
321
+ ok: true,
322
+ });
323
+ } catch {
324
+ return rememberResponse(modelResponses, {
325
+ version: 1,
326
+ requestId: request.requestId,
327
+ ok: false,
328
+ error: "unavailable",
329
+ });
330
+ }
331
+ },
332
+ onSetThinkingLevel: (
333
+ request: SetThinkingLevelRequest,
334
+ ): SetThinkingLevelResponse => {
335
+ const previous = thinkingResponses.get(request.requestId);
336
+ if (previous) {
337
+ return previous;
338
+ }
339
+ if (controller.signal.aborted || sessionController !== controller) {
340
+ return rememberResponse(thinkingResponses, {
341
+ version: 1,
342
+ requestId: request.requestId,
343
+ ok: false,
344
+ error: "shutting_down",
345
+ });
346
+ }
347
+ try {
348
+ pi.setThinkingLevel(request.level);
349
+ return rememberResponse(thinkingResponses, {
350
+ version: 1,
351
+ requestId: request.requestId,
352
+ ok: true,
353
+ });
354
+ } catch {
355
+ return rememberResponse(thinkingResponses, {
356
+ version: 1,
357
+ requestId: request.requestId,
358
+ ok: false,
359
+ error: "unavailable",
360
+ });
361
+ }
362
+ },
363
+ onAbort: (request: AbortRequest): AbortResponse => {
364
+ const previous = abortResponses.get(request.requestId);
365
+ if (previous) {
366
+ return previous;
367
+ }
368
+ if (controller.signal.aborted || sessionController !== controller) {
369
+ return rememberResponse(abortResponses, {
370
+ version: 1,
371
+ requestId: request.requestId,
372
+ ok: false,
373
+ error: "shutting_down",
374
+ });
375
+ }
376
+ if (ctx.isIdle()) {
377
+ return rememberResponse(abortResponses, {
378
+ version: 1,
379
+ requestId: request.requestId,
380
+ ok: false,
381
+ error: "unavailable",
382
+ });
383
+ }
384
+ try {
385
+ ctx.abort();
386
+ return rememberResponse(abortResponses, {
387
+ version: 1,
388
+ requestId: request.requestId,
389
+ ok: true,
390
+ });
391
+ } catch {
392
+ return rememberResponse(abortResponses, {
393
+ version: 1,
394
+ requestId: request.requestId,
395
+ ok: false,
396
+ error: "unavailable",
397
+ });
398
+ }
399
+ },
400
+ onUserMessage: (request: UserMessageRequest): UserMessageResponse => {
401
+ const previous = userMessageResponses.get(request.requestId);
402
+ if (previous) {
403
+ return previous;
404
+ }
405
+ if (controller.signal.aborted || sessionController !== controller) {
406
+ return rememberResponse(userMessageResponses, {
407
+ version: 1,
408
+ requestId: request.requestId,
409
+ ok: false,
410
+ error: "shutting_down",
411
+ });
412
+ }
413
+ const isIdle = ctx.isIdle();
414
+ const delivery = isIdle ? "immediate" : request.deliverAs;
415
+ const userMessageOptions = isIdle
416
+ ? { expandPromptTemplates: request.expandPromptTemplates }
417
+ : {
418
+ deliverAs: request.deliverAs,
419
+ expandPromptTemplates: request.expandPromptTemplates,
420
+ };
421
+ try {
422
+ pi.sendUserMessage(request.message, userMessageOptions);
423
+ return rememberResponse(userMessageResponses, {
424
+ version: 1,
425
+ requestId: request.requestId,
426
+ ok: true,
427
+ delivery,
428
+ });
429
+ } catch {
430
+ return rememberResponse(userMessageResponses, {
431
+ version: 1,
432
+ requestId: request.requestId,
433
+ ok: false,
434
+ error: "unavailable",
435
+ });
436
+ }
437
+ },
438
+ },
439
+ );
440
+ if (controller.signal.aborted || sessionController !== controller) {
441
+ await startedReceiver.close();
442
+ return;
443
+ }
444
+ receiver = startedReceiver;
445
+ await Promise.all(
446
+ restored.pending.map((envelope) =>
447
+ injectNotification(pi, ctx, envelope, controller.signal, config),
448
+ ),
449
+ );
450
+ });
451
+
452
+ pi.on("agent_settled", async (_event, ctx) => {
453
+ const childSessionId = ctx.sessionManager.getSessionId();
454
+ const leafId = ctx.sessionManager.getLeafId();
455
+ if (!parentSocket || !leafId || !childTaskSlug) {
456
+ return;
457
+ }
458
+
459
+ await sendEnvelope(
460
+ parentSocket,
461
+ {
462
+ childSessionId,
463
+ taskSlug: childTaskSlug,
464
+ leafId,
465
+ cwd: ctx.cwd,
466
+ timestamp: Date.now(),
467
+ },
468
+ {
469
+ ackTimeoutMs: options.ackTimeoutMs,
470
+ retryDelaysMs: options.retryDelaysMs,
471
+ },
472
+ );
473
+ });
474
+
475
+ pi.on("session_shutdown", async (event, ctx) => {
476
+ if (ctx.sessionManager.getSessionId() !== activeSessionId) {
477
+ return;
478
+ }
479
+ const ownedReceiver = receiver;
480
+ receiver = undefined;
481
+ activeSessionId = undefined;
482
+ sessionController?.abort();
483
+ sessionController = undefined;
484
+ await Promise.all([ownedReceiver?.close(), liveEvents.stop(event)]);
485
+ });
486
+ };
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { loadConfigOrDefault } from "@richardgill/pi-config";
2
+
3
+ import { OvermuxPiConfigSchema } from "./config.js";
4
+ import { overmuxPi } from "./extension.js";
5
+
6
+ export * from "./extension.js";
7
+
8
+ const config = loadConfigOrDefault({
9
+ filename: "overmux-pi.jsonc",
10
+ schema: OvermuxPiConfigSchema,
11
+ });
12
+
13
+ export default (pi: Parameters<typeof overmuxPi>[0]): void =>
14
+ overmuxPi(pi, config);
@@ -0,0 +1,110 @@
1
+ import { open, stat } from "node:fs/promises";
2
+
3
+ const ANCHOR_BYTES = 512;
4
+
5
+ export type JsonlRead = { records: unknown[]; reset: boolean };
6
+ export type JsonlTail = { read: (path: string) => Promise<JsonlRead> };
7
+
8
+ export const parseCompleteJsonlBytes = (bytes: Buffer): unknown[] => {
9
+ const records: unknown[] = [];
10
+ let offset = 0;
11
+ bytes.forEach((byte, index) => {
12
+ if (byte !== 10) {
13
+ return;
14
+ }
15
+ const line = bytes.subarray(offset, index);
16
+ offset = index + 1;
17
+ if (!line.length) {
18
+ return;
19
+ }
20
+ try {
21
+ records.push(JSON.parse(line.toString("utf8")) as unknown);
22
+ } catch {}
23
+ });
24
+ return records;
25
+ };
26
+
27
+ const parseJsonlLines = (bytes: Buffer) => {
28
+ const newline = bytes.lastIndexOf(10);
29
+ if (newline < 0) {
30
+ return { records: [] as unknown[], remainder: bytes };
31
+ }
32
+ return {
33
+ records: parseCompleteJsonlBytes(bytes.subarray(0, newline + 1)),
34
+ remainder: bytes.subarray(newline + 1),
35
+ };
36
+ };
37
+
38
+ const readBytes = async (
39
+ path: string,
40
+ offset: number,
41
+ length: number,
42
+ ): Promise<Buffer<ArrayBufferLike>> => {
43
+ const handle = await open(path, "r");
44
+ try {
45
+ const bytes = Buffer.alloc(length);
46
+ const { bytesRead } = await handle.read(bytes, 0, length, offset);
47
+ return bytes.subarray(0, bytesRead);
48
+ } finally {
49
+ await handle.close();
50
+ }
51
+ };
52
+
53
+ const anchorMatches = async (
54
+ path: string,
55
+ cursor: number,
56
+ anchor: Buffer<ArrayBufferLike>,
57
+ ) => {
58
+ if (!anchor.length) {
59
+ return true;
60
+ }
61
+ const committed = await readBytes(
62
+ path,
63
+ cursor - anchor.length,
64
+ anchor.length,
65
+ );
66
+ return committed.equals(anchor);
67
+ };
68
+
69
+ const nextAnchor = (
70
+ previous: Buffer<ArrayBufferLike>,
71
+ appended: Buffer<ArrayBufferLike>,
72
+ ) => Buffer.concat([previous, appended]).subarray(-ANCHOR_BYTES);
73
+
74
+ export const createJsonlTail = (): JsonlTail => {
75
+ let anchor: Buffer<ArrayBufferLike> = Buffer.alloc(0);
76
+ let cursor = 0;
77
+ let identity: string | undefined;
78
+ let remainder: Buffer<ArrayBufferLike> = Buffer.alloc(0);
79
+
80
+ return {
81
+ read: async (path) => {
82
+ const metadata = await stat(path).catch(() => undefined);
83
+ if (!metadata?.isFile()) {
84
+ return { records: [], reset: false };
85
+ }
86
+ const nextIdentity = `${metadata.dev}:${metadata.ino}`;
87
+ const replaced = identity !== undefined && identity !== nextIdentity;
88
+ const truncated = metadata.size < cursor;
89
+ const rewritten =
90
+ !replaced && !truncated && !(await anchorMatches(path, cursor, anchor));
91
+ const reset = replaced || truncated || rewritten;
92
+ if (reset) {
93
+ anchor = Buffer.alloc(0);
94
+ cursor = 0;
95
+ remainder = Buffer.alloc(0);
96
+ }
97
+ identity = nextIdentity;
98
+ if (metadata.size === cursor) {
99
+ return { records: [], reset };
100
+ }
101
+
102
+ const bytes = await readBytes(path, cursor, metadata.size - cursor);
103
+ cursor += bytes.length;
104
+ anchor = nextAnchor(anchor, bytes);
105
+ const parsed = parseJsonlLines(Buffer.concat([remainder, bytes]));
106
+ remainder = parsed.remainder;
107
+ return { records: parsed.records, reset };
108
+ },
109
+ };
110
+ };