sidekick-shared 0.29.0 → 0.30.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/CHANGELOG.md +48 -0
- package/README.md +198 -56
- package/dist/aggregation/EventAggregator.d.ts +96 -2
- package/dist/aggregation/EventAggregator.js +535 -42
- package/dist/aggregation/snapshot.d.ts +37 -8
- package/dist/aggregation/snapshot.js +31 -9
- package/dist/aggregation/types.d.ts +38 -1
- package/dist/browser.d.ts +9 -1
- package/dist/browser.js +29 -3
- package/dist/context/sessionContext.js +8 -0
- package/dist/esm/aggregation/EventAggregator.js +535 -42
- package/dist/esm/aggregation/snapshot.js +31 -9
- package/dist/esm/browser.js +3 -0
- package/dist/esm/context/sessionContext.js +8 -0
- package/dist/esm/fileContinuity.js +149 -0
- package/dist/esm/formatters/toolSummary.js +4 -1
- package/dist/esm/index.js +7 -2
- package/dist/esm/modelCatalog.js +5 -4
- package/dist/esm/modelContext.js +4 -3
- package/dist/esm/modelInfo.js +4 -3
- package/dist/esm/node.js +3 -1
- package/dist/esm/observedContextWindows.js +2 -1
- package/dist/esm/ownKey.js +26 -0
- package/dist/esm/parsers/codexParser.js +372 -7
- package/dist/esm/parsers/openCodeParser.js +2 -1
- package/dist/esm/parsers/subagentEndState.js +61 -0
- package/dist/esm/parsers/subagentScanner.js +37 -12
- package/dist/esm/parsers/subagentTraceParser.js +103 -37
- package/dist/esm/pricingCatalog.js +3 -2
- package/dist/esm/promptHistory.js +22 -11
- package/dist/esm/providers/claudeCode.js +60 -29
- package/dist/esm/providers/codex.js +235 -201
- package/dist/esm/providers/codexCompressedReader.js +184 -0
- package/dist/esm/providers/codexDatabase.js +47 -2
- package/dist/esm/providers/codexRollouts.js +20 -2
- package/dist/esm/providers/codexSubagents.js +456 -0
- package/dist/esm/providers/compressedRollouts.js +330 -0
- package/dist/esm/providers/factory.js +4 -1
- package/dist/esm/providers/rolloutWalker.js +32 -4
- package/dist/esm/schemas/sessionEvent.js +63 -14
- package/dist/esm/sessionMonitor.js +7 -1
- package/dist/esm/sessionPreviews.js +9 -0
- package/dist/esm/sessionPromptHistory.js +5 -3
- package/dist/esm/subagentActivity.js +125 -0
- package/dist/esm/subagentDisplayState.js +81 -0
- package/dist/esm/subagentSpawn.js +363 -0
- package/dist/esm/testing/codexSubagentFixtures.js +336 -0
- package/dist/esm/toolCallOrigins.js +17 -10
- package/dist/esm/toolCategory.js +6 -2
- package/dist/esm/transcript.js +8 -0
- package/dist/esm/turns/assistantTurn.js +6 -2
- package/dist/esm/usage/claudeUsageDedupe.js +68 -0
- package/dist/esm/usage/usageEvents.js +4 -1
- package/dist/esm/watchers/eventBridge.js +45 -0
- package/dist/esm/watchers/factory.js +5 -3
- package/dist/esm/watchers/jsonlTail.js +82 -48
- package/dist/esm/watchers/jsonlWatcher.js +61 -4
- package/dist/esm/watchers/providerReaderWatcher.js +18 -4
- package/dist/esm/zaiQuota.js +2 -1
- package/dist/fileContinuity.d.ts +95 -0
- package/dist/fileContinuity.js +187 -0
- package/dist/formatters/toolSummary.js +4 -1
- package/dist/index.d.ts +19 -7
- package/dist/index.js +48 -10
- package/dist/modelCatalog.js +5 -4
- package/dist/modelContext.js +4 -3
- package/dist/modelInfo.js +4 -3
- package/dist/node.d.ts +5 -1
- package/dist/node.js +18 -2
- package/dist/observedContextWindows.js +2 -1
- package/dist/ownKey.d.ts +16 -0
- package/dist/ownKey.js +30 -0
- package/dist/parsers/codexParser.d.ts +107 -0
- package/dist/parsers/codexParser.js +373 -7
- package/dist/parsers/openCodeParser.js +2 -1
- package/dist/parsers/subagentEndState.d.ts +20 -0
- package/dist/parsers/subagentEndState.js +65 -0
- package/dist/parsers/subagentScanner.d.ts +19 -6
- package/dist/parsers/subagentScanner.js +37 -12
- package/dist/parsers/subagentTraceParser.d.ts +33 -5
- package/dist/parsers/subagentTraceParser.js +103 -37
- package/dist/pricingCatalog.js +3 -2
- package/dist/promptHistory.d.ts +13 -3
- package/dist/promptHistory.js +22 -11
- package/dist/providers/claudeCode.js +59 -28
- package/dist/providers/codex.d.ts +35 -5
- package/dist/providers/codex.js +232 -198
- package/dist/providers/codexCompressedReader.d.ts +58 -0
- package/dist/providers/codexCompressedReader.js +221 -0
- package/dist/providers/codexDatabase.d.ts +25 -0
- package/dist/providers/codexDatabase.js +48 -2
- package/dist/providers/codexRollouts.d.ts +16 -2
- package/dist/providers/codexRollouts.js +20 -2
- package/dist/providers/codexSubagents.d.ts +66 -0
- package/dist/providers/codexSubagents.js +495 -0
- package/dist/providers/compressedRollouts.d.ts +114 -0
- package/dist/providers/compressedRollouts.js +380 -0
- package/dist/providers/factory.d.ts +5 -0
- package/dist/providers/factory.js +4 -1
- package/dist/providers/rolloutWalker.d.ts +15 -2
- package/dist/providers/rolloutWalker.js +33 -4
- package/dist/providers/types.d.ts +36 -1
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/sessionEvent.d.ts +51 -10
- package/dist/schemas/sessionEvent.js +63 -14
- package/dist/sessionMonitor.d.ts +6 -0
- package/dist/sessionMonitor.js +7 -1
- package/dist/sessionPreviews.js +9 -0
- package/dist/sessionPromptHistory.d.ts +5 -0
- package/dist/sessionPromptHistory.js +5 -36
- package/dist/subagentActivity.d.ts +91 -0
- package/dist/subagentActivity.js +132 -0
- package/dist/subagentDisplayState.d.ts +81 -0
- package/dist/subagentDisplayState.js +86 -0
- package/dist/subagentSpawn.d.ts +175 -0
- package/dist/subagentSpawn.js +385 -0
- package/dist/testing/codexSubagentFixtures.d.ts +48 -0
- package/dist/testing/codexSubagentFixtures.js +344 -0
- package/dist/toolCallOrigins.d.ts +5 -0
- package/dist/toolCallOrigins.js +17 -10
- package/dist/toolCategory.js +6 -2
- package/dist/transcript.js +8 -0
- package/dist/turns/assistantTurn.d.ts +4 -0
- package/dist/turns/assistantTurn.js +6 -2
- package/dist/types/codex.d.ts +71 -2
- package/dist/types/sessionEvent.d.ts +38 -3
- package/dist/usage/claudeUsageDedupe.d.ts +31 -0
- package/dist/usage/claudeUsageDedupe.js +68 -0
- package/dist/usage/usageEvents.d.ts +4 -1
- package/dist/usage/usageEvents.js +4 -1
- package/dist/watchers/eventBridge.js +45 -0
- package/dist/watchers/factory.d.ts +6 -0
- package/dist/watchers/factory.js +5 -3
- package/dist/watchers/jsonlTail.d.ts +27 -4
- package/dist/watchers/jsonlTail.js +84 -48
- package/dist/watchers/jsonlWatcher.d.ts +22 -1
- package/dist/watchers/jsonlWatcher.js +60 -3
- package/dist/watchers/providerReaderWatcher.d.ts +7 -0
- package/dist/watchers/providerReaderWatcher.js +18 -4
- package/dist/watchers/types.d.ts +12 -0
- package/dist/zaiQuota.js +2 -1
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import { fileHeadOf, HEAD_BYTES, headMatches } from '../fileHead';
|
|
7
|
+
import { FileContinuity, probeFileContinuity } from '../fileContinuity';
|
|
7
8
|
import { JsonlParser } from '../parsers/jsonl';
|
|
8
9
|
const DEFAULT_DEBOUNCE_MS = 100;
|
|
9
10
|
const DEFAULT_CATCHUP_INTERVAL_MS = 30_000;
|
|
@@ -60,6 +61,63 @@ export class JsonlTailSymlinkError extends Error {
|
|
|
60
61
|
export function createJsonlTail(options) {
|
|
61
62
|
return new JsonlTailReader(options);
|
|
62
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Records a {@link JsonlTailCheckpoint} for byte `offset` of `filePath`
|
|
66
|
+
* without a tail, for readers that keep their own offset (a snapshot that
|
|
67
|
+
* stores a reader position, for example). The checkpoint carries the file's
|
|
68
|
+
* current device and inode and a digest of its first bytes, so
|
|
69
|
+
* {@link verifyJsonlCheckpoint} (or {@link JsonlTail.resumeFrom}) can later
|
|
70
|
+
* tell whether `offset` still refers to the same, append-only content.
|
|
71
|
+
*
|
|
72
|
+
* Returns `null` when the file is missing or unreadable, or when `offset` is
|
|
73
|
+
* not a resume-safe position in it: negative, past the end, or not 0 and not
|
|
74
|
+
* just after a newline. Follows symbolic links.
|
|
75
|
+
*/
|
|
76
|
+
export function captureJsonlCheckpoint(filePath, offset) {
|
|
77
|
+
if (!Number.isSafeInteger(offset) || offset < 0)
|
|
78
|
+
return null;
|
|
79
|
+
let fd = null;
|
|
80
|
+
try {
|
|
81
|
+
fd = fs.openSync(filePath, 'r');
|
|
82
|
+
const stat = fs.fstatSync(fd);
|
|
83
|
+
if (offset > stat.size)
|
|
84
|
+
return null;
|
|
85
|
+
if (offset > 0 && readAt(fd, offset - 1, 1)[0] !== 0x0a)
|
|
86
|
+
return null;
|
|
87
|
+
return checkpointOf(offset, stat, fileHeadOf(readPrefix(fd, stat)));
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
closeQuietly(fd);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Whether `checkpoint` (any value, typically parsed from JSON) is a valid
|
|
98
|
+
* {@link JsonlTailCheckpoint} that still describes `filePath`: same device
|
|
99
|
+
* and inode, the recorded first bytes unchanged, and its offset within the
|
|
100
|
+
* file and just after a newline (or 0). This is the check
|
|
101
|
+
* {@link JsonlTail.resumeFrom} applies. `false` on any error, including a
|
|
102
|
+
* missing file. Follows symbolic links.
|
|
103
|
+
*/
|
|
104
|
+
export function verifyJsonlCheckpoint(filePath, checkpoint) {
|
|
105
|
+
const parsed = parseCheckpoint(checkpoint);
|
|
106
|
+
if (parsed === null)
|
|
107
|
+
return false;
|
|
108
|
+
let fd = null;
|
|
109
|
+
try {
|
|
110
|
+
fd = fs.openSync(filePath, 'r');
|
|
111
|
+
const stat = fs.fstatSync(fd);
|
|
112
|
+
return checkpointMatches(parsed, fd, stat, readPrefix(fd, stat));
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
closeQuietly(fd);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
63
121
|
class JsonlTailReader {
|
|
64
122
|
options;
|
|
65
123
|
active = false;
|
|
@@ -71,10 +129,8 @@ class JsonlTailReader {
|
|
|
71
129
|
/** Inside a line over `maxLineBytes`: drop bytes until its newline. */
|
|
72
130
|
skippingLine = false;
|
|
73
131
|
currentLine = { offset: 0, length: 0 };
|
|
74
|
-
/** Identity and prefix of the file the offsets refer to, once read. */
|
|
75
|
-
|
|
76
|
-
head = null;
|
|
77
|
-
lastMtimeMs = -1;
|
|
132
|
+
/** Identity, mtime, and prefix of the file the offsets refer to, once read. */
|
|
133
|
+
continuity = new FileContinuity();
|
|
78
134
|
maxReadBytes;
|
|
79
135
|
maxLineBytes;
|
|
80
136
|
rewritePolicy;
|
|
@@ -282,21 +338,10 @@ class JsonlTailReader {
|
|
|
282
338
|
}
|
|
283
339
|
/** Whether the continuity check needs bytes from the file. */
|
|
284
340
|
probeNeeded(stat) {
|
|
285
|
-
return (this.
|
|
286
|
-
stat.dev !== this.identity.dev ||
|
|
287
|
-
stat.ino !== this.identity.ino ||
|
|
288
|
-
stat.size < this.readOffset ||
|
|
289
|
-
stat.mtimeMs !== this.lastMtimeMs ||
|
|
290
|
-
this.head === null ||
|
|
291
|
-
this.head.bytes < Math.min(HEAD_BYTES, stat.size));
|
|
341
|
+
return this.continuity.needsProbe(stat, this.readOffset);
|
|
292
342
|
}
|
|
293
343
|
probeSync(fd, stat) {
|
|
294
|
-
return
|
|
295
|
-
prefix: readAt(fd, 0, Math.min(HEAD_BYTES, stat.size)),
|
|
296
|
-
before: this.committedOffset > 0 && this.committedOffset <= stat.size
|
|
297
|
-
? readAt(fd, this.committedOffset - 1, 1)
|
|
298
|
-
: null,
|
|
299
|
-
};
|
|
344
|
+
return probeFileContinuity(fd, stat, this.committedOffset);
|
|
300
345
|
}
|
|
301
346
|
async probeAsync(handle, stat) {
|
|
302
347
|
const committedOffset = this.committedOffset;
|
|
@@ -313,31 +358,16 @@ class JsonlTailReader {
|
|
|
313
358
|
* newline. `null` when it can continue.
|
|
314
359
|
*/
|
|
315
360
|
resetReason(stat, probe) {
|
|
316
|
-
|
|
317
|
-
return 'replaced';
|
|
318
|
-
}
|
|
319
|
-
if (stat.size < this.readOffset)
|
|
320
|
-
return 'truncated';
|
|
321
|
-
if (this.identity && stat.mtimeMs !== this.lastMtimeMs && probe) {
|
|
322
|
-
if (this.head && !headMatches(this.head, probe.prefix))
|
|
323
|
-
return 'rewritten';
|
|
324
|
-
if (probe.before && probe.before[0] !== 0x0a)
|
|
325
|
-
return 'rewritten';
|
|
326
|
-
}
|
|
327
|
-
return null;
|
|
361
|
+
return this.continuity.reasonFor(stat, this.readOffset, probe);
|
|
328
362
|
}
|
|
329
363
|
/** Records the identity and prefix of the file the offsets now refer to. */
|
|
330
364
|
adoptFile(stat, probe) {
|
|
331
|
-
this.
|
|
332
|
-
this.lastMtimeMs = stat.mtimeMs;
|
|
333
|
-
if (probe && (!this.head || this.head.bytes < Math.min(HEAD_BYTES, stat.size))) {
|
|
334
|
-
this.head = fileHeadOf(probe.prefix);
|
|
335
|
-
}
|
|
365
|
+
this.continuity.record(stat, probe);
|
|
336
366
|
}
|
|
337
367
|
reset(reason, offset) {
|
|
338
368
|
const previousOffset = this.committedOffset;
|
|
339
369
|
this.position(offset);
|
|
340
|
-
this.
|
|
370
|
+
this.continuity.discardHead();
|
|
341
371
|
if (reason === 'replaced' && this.active) {
|
|
342
372
|
// fs.watch follows the old inode; watch the file now at the path.
|
|
343
373
|
this.fsWatcher?.close();
|
|
@@ -412,12 +442,7 @@ class JsonlTailReader {
|
|
|
412
442
|
this.position(Math.max(0, offset));
|
|
413
443
|
}
|
|
414
444
|
checkpoint() {
|
|
415
|
-
return
|
|
416
|
-
version: JSONL_TAIL_CHECKPOINT_VERSION,
|
|
417
|
-
offset: this.committedOffset,
|
|
418
|
-
identity: this.identity ? { dev: this.identity.dev, ino: this.identity.ino } : null,
|
|
419
|
-
prefixDigest: this.head ? { bytes: this.head.bytes, hash: this.head.hash } : null,
|
|
420
|
-
};
|
|
445
|
+
return checkpointOf(this.committedOffset, this.continuity.knownIdentity, this.continuity.knownHead);
|
|
421
446
|
}
|
|
422
447
|
resumeFrom(checkpoint) {
|
|
423
448
|
this.generation += 1;
|
|
@@ -427,7 +452,7 @@ class JsonlTailReader {
|
|
|
427
452
|
try {
|
|
428
453
|
fd = this.openSync();
|
|
429
454
|
const stat = fs.fstatSync(fd);
|
|
430
|
-
const prefix =
|
|
455
|
+
const prefix = readPrefix(fd, stat);
|
|
431
456
|
const matches = parsed !== null && checkpointMatches(parsed, fd, stat, prefix);
|
|
432
457
|
const offset = matches
|
|
433
458
|
? parsed.offset
|
|
@@ -435,16 +460,12 @@ class JsonlTailReader {
|
|
|
435
460
|
? lastCompleteLineOffset(fd, stat.size)
|
|
436
461
|
: 0;
|
|
437
462
|
this.position(offset);
|
|
438
|
-
this.
|
|
439
|
-
this.lastMtimeMs = stat.mtimeMs;
|
|
440
|
-
this.head = fileHeadOf(prefix);
|
|
463
|
+
this.continuity.adoptPrefix(stat, prefix);
|
|
441
464
|
return matches;
|
|
442
465
|
}
|
|
443
466
|
catch (error) {
|
|
444
467
|
this.position(0);
|
|
445
|
-
this.
|
|
446
|
-
this.head = null;
|
|
447
|
-
this.lastMtimeMs = -1;
|
|
468
|
+
this.continuity.forget();
|
|
448
469
|
this.options.onError?.(toError(error));
|
|
449
470
|
return false;
|
|
450
471
|
}
|
|
@@ -559,6 +580,15 @@ function parseCheckpoint(value) {
|
|
|
559
580
|
prefixDigest: parsedDigest,
|
|
560
581
|
};
|
|
561
582
|
}
|
|
583
|
+
/** A fresh checkpoint for `offset` in the file with `identity` and recorded `head`. */
|
|
584
|
+
function checkpointOf(offset, identity, head) {
|
|
585
|
+
return {
|
|
586
|
+
version: JSONL_TAIL_CHECKPOINT_VERSION,
|
|
587
|
+
offset,
|
|
588
|
+
identity: identity ? { dev: identity.dev, ino: identity.ino } : null,
|
|
589
|
+
prefixDigest: head ? { bytes: head.bytes, hash: head.hash } : null,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
562
592
|
/** Whether `checkpoint` still describes the open file. */
|
|
563
593
|
function checkpointMatches(checkpoint, fd, stat, prefix) {
|
|
564
594
|
const { offset, identity, prefixDigest } = checkpoint;
|
|
@@ -601,6 +631,10 @@ function readAt(fd, position, length) {
|
|
|
601
631
|
const count = length > 0 ? fs.readSync(fd, buffer, 0, length, position) : 0;
|
|
602
632
|
return buffer.subarray(0, count);
|
|
603
633
|
}
|
|
634
|
+
/** The file's first `min(HEAD_BYTES, size)` bytes: what a checkpoint digests. */
|
|
635
|
+
function readPrefix(fd, stat) {
|
|
636
|
+
return readAt(fd, 0, Math.min(HEAD_BYTES, stat.size));
|
|
637
|
+
}
|
|
604
638
|
async function readAtAsync(handle, position, length) {
|
|
605
639
|
const buffer = Buffer.alloc(length);
|
|
606
640
|
const count = length > 0 ? (await handle.read(buffer, 0, length, position)).bytesRead : 0;
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* Uses byte-offset tracking + fs.watch with debounce + catch-up polling.
|
|
4
4
|
*/
|
|
5
5
|
import { createJsonlTail } from './jsonlTail';
|
|
6
|
-
import { normalizeCodexToolName, resolveCodexCallUsage } from '../parsers/codexParser';
|
|
6
|
+
import { codexTokenUsageFrom, normalizeCodexToolName, resolveCodexCallUsage, } from '../parsers/codexParser';
|
|
7
7
|
import { formatToolSummary } from '../formatters/toolSummary';
|
|
8
8
|
import { ClaudeUsageDeduper, normalizeClaudeUsage } from '../usage/claudeUsageDedupe';
|
|
9
|
+
import { describeAgentsStopped, describeTaskStatus, isAgentsKilledSystemEvent, isFinalTaskStatus, parseAgentsStoppedNotice, parseTaskNotification, taskNotificationPromptFromAttachment, taskStatusFromAttachment, } from '../subagentSpawn';
|
|
9
10
|
// ── Normalizers ──
|
|
10
11
|
/**
|
|
11
12
|
* Split lines of one Claude response repeat its usage; run the raw usage
|
|
@@ -21,7 +22,7 @@ function dedupedClaudeUsage(raw, deduper) {
|
|
|
21
22
|
}));
|
|
22
23
|
return { usage: event.message?.usage, usageKind: event.message?.usageKind };
|
|
23
24
|
}
|
|
24
|
-
function normalizeClaudeCodeEvent(raw, deduper) {
|
|
25
|
+
function normalizeClaudeCodeEvent(raw, deduper, includeTaskNotifications) {
|
|
25
26
|
const events = [];
|
|
26
27
|
const ts = raw.timestamp || new Date().toISOString();
|
|
27
28
|
const permissionMode = raw.permissionMode;
|
|
@@ -133,6 +134,29 @@ function normalizeClaudeCodeEvent(raw, deduper) {
|
|
|
133
134
|
raw,
|
|
134
135
|
});
|
|
135
136
|
}
|
|
137
|
+
else if (includeTaskNotifications) {
|
|
138
|
+
// A task notification that landed mid-turn is only an attachment line.
|
|
139
|
+
const prompt = taskNotificationPromptFromAttachment(raw);
|
|
140
|
+
const notification = parseTaskNotification(prompt);
|
|
141
|
+
const stopped = notification ? null : parseAgentsStoppedNotice(prompt);
|
|
142
|
+
const taskStatus = taskStatusFromAttachment(raw);
|
|
143
|
+
let summary = null;
|
|
144
|
+
if (notification) {
|
|
145
|
+
summary = notification.summary ?? `Background task ${notification.status ?? 'finished'}`;
|
|
146
|
+
}
|
|
147
|
+
else if (stopped) {
|
|
148
|
+
summary = describeAgentsStopped(stopped);
|
|
149
|
+
}
|
|
150
|
+
else if (taskStatus && isFinalTaskStatus(taskStatus.status)) {
|
|
151
|
+
summary = describeTaskStatus(taskStatus);
|
|
152
|
+
}
|
|
153
|
+
else if (isAgentsKilledSystemEvent(raw)) {
|
|
154
|
+
summary = describeAgentsStopped(null);
|
|
155
|
+
}
|
|
156
|
+
if (summary !== null) {
|
|
157
|
+
events.push({ providerId: 'claude-code', type: 'system', timestamp: ts, summary, raw });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
136
160
|
}
|
|
137
161
|
// Propagate permission mode to all generated events
|
|
138
162
|
if (permissionMode) {
|
|
@@ -204,16 +228,18 @@ export class JsonlSessionWatcher {
|
|
|
204
228
|
providerId;
|
|
205
229
|
sessionPath;
|
|
206
230
|
callbacks;
|
|
231
|
+
options;
|
|
207
232
|
tail;
|
|
208
233
|
startOffset;
|
|
209
234
|
codexInPlanMode = false;
|
|
210
235
|
claudeUsageDeduper = new ClaudeUsageDeduper();
|
|
211
236
|
/** Last Codex cumulative usage seen; repeated `token_count` events carry the same value. */
|
|
212
237
|
codexPrevTotal;
|
|
213
|
-
constructor(providerId, sessionPath, callbacks) {
|
|
238
|
+
constructor(providerId, sessionPath, callbacks, options = {}) {
|
|
214
239
|
this.providerId = providerId;
|
|
215
240
|
this.sessionPath = sessionPath;
|
|
216
241
|
this.callbacks = callbacks;
|
|
242
|
+
this.options = options;
|
|
217
243
|
}
|
|
218
244
|
get isActive() {
|
|
219
245
|
return this.tail?.isActive ?? false;
|
|
@@ -227,6 +253,37 @@ export class JsonlSessionWatcher {
|
|
|
227
253
|
getPosition() {
|
|
228
254
|
return this.tail?.getOffset() ?? this.startOffset ?? 0;
|
|
229
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* Parse state for {@link getPosition}: for Claude Code, the usage deduper's
|
|
258
|
+
* recent message ids (a `ClaudeUsageDedupeState`); for Codex, the previous
|
|
259
|
+
* cumulative token total and plan mode.
|
|
260
|
+
*/
|
|
261
|
+
getResumeState() {
|
|
262
|
+
if (this.providerId === 'codex') {
|
|
263
|
+
return {
|
|
264
|
+
version: 1,
|
|
265
|
+
prevTotalUsage: this.codexPrevTotal ? { ...this.codexPrevTotal } : null,
|
|
266
|
+
inPlanMode: this.codexInPlanMode,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return this.claudeUsageDeduper.exportState();
|
|
270
|
+
}
|
|
271
|
+
/** Restores state from {@link getResumeState} after `seekTo()` and before `start()`. */
|
|
272
|
+
restoreResumeState(state) {
|
|
273
|
+
if (this.providerId !== 'codex')
|
|
274
|
+
return this.claudeUsageDeduper.importState(state);
|
|
275
|
+
if (!state || typeof state !== 'object')
|
|
276
|
+
return false;
|
|
277
|
+
const { version, prevTotalUsage, inPlanMode } = state;
|
|
278
|
+
if (version !== 1 || typeof inPlanMode !== 'boolean')
|
|
279
|
+
return false;
|
|
280
|
+
const total = codexTokenUsageFrom(prevTotalUsage);
|
|
281
|
+
if (total === undefined)
|
|
282
|
+
return false;
|
|
283
|
+
this.codexPrevTotal = total ?? undefined;
|
|
284
|
+
this.codexInPlanMode = inPlanMode;
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
230
287
|
start(replay) {
|
|
231
288
|
if (this.isActive)
|
|
232
289
|
return;
|
|
@@ -258,7 +315,7 @@ export class JsonlSessionWatcher {
|
|
|
258
315
|
// JsonlParser parses any JSON object. Cast to Record for codex normalizer.
|
|
259
316
|
const followEvents = this.providerId === 'codex'
|
|
260
317
|
? this.normalizeCodexEvent(event)
|
|
261
|
-
: normalizeClaudeCodeEvent(event, this.claudeUsageDeduper);
|
|
318
|
+
: normalizeClaudeCodeEvent(event, this.claudeUsageDeduper, this.options.includeTaskNotifications === true);
|
|
262
319
|
for (const fe of followEvents) {
|
|
263
320
|
this.callbacks.onEvent(fe);
|
|
264
321
|
}
|
|
@@ -32,6 +32,19 @@ export class ProviderReaderSessionWatcher {
|
|
|
32
32
|
getPosition() {
|
|
33
33
|
return this.reader.getPosition();
|
|
34
34
|
}
|
|
35
|
+
/** The reader's resume state, if it keeps any. */
|
|
36
|
+
getResumeState() {
|
|
37
|
+
return this.reader.getResumeState?.();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Restores the reader's resume state after `seekTo()`. A reader that keeps
|
|
41
|
+
* none accepts only `undefined`.
|
|
42
|
+
*/
|
|
43
|
+
restoreResumeState(state) {
|
|
44
|
+
if (!this.reader.restoreResumeState)
|
|
45
|
+
return state === undefined;
|
|
46
|
+
return this.reader.restoreResumeState(state);
|
|
47
|
+
}
|
|
35
48
|
start(replay) {
|
|
36
49
|
if (this._isActive)
|
|
37
50
|
return;
|
|
@@ -99,11 +112,12 @@ export class ProviderReaderSessionWatcher {
|
|
|
99
112
|
if (!this._isActive)
|
|
100
113
|
return;
|
|
101
114
|
try {
|
|
102
|
-
if (this.reader.wasTruncated()) {
|
|
103
|
-
this.reader.reset();
|
|
104
|
-
this.callbacks.onReset?.();
|
|
105
|
-
}
|
|
106
115
|
const events = this.reader.readNew();
|
|
116
|
+
// The reader started over (truncated, rewritten, or replaced file):
|
|
117
|
+
// these events are the session from the beginning, so consumers must
|
|
118
|
+
// drop what they built before receiving them.
|
|
119
|
+
if (this.reader.wasTruncated())
|
|
120
|
+
this.callbacks.onReset?.();
|
|
107
121
|
for (const event of events) {
|
|
108
122
|
for (const followEvent of toFollowEvents(event, this.provider.id)) {
|
|
109
123
|
this.callbacks.onEvent(followEvent);
|
package/dist/esm/zaiQuota.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ownValue } from './ownKey';
|
|
1
2
|
// ── Tier budgets ──
|
|
2
3
|
/**
|
|
3
4
|
* Published z.ai coding-plan prompt budgets.
|
|
@@ -215,7 +216,7 @@ export function parseZaiQuotaError(error) {
|
|
|
215
216
|
if (!error)
|
|
216
217
|
return null;
|
|
217
218
|
const code = String(error.code ?? '');
|
|
218
|
-
const kind = ZAI_ERROR_CODES
|
|
219
|
+
const kind = ownValue(ZAI_ERROR_CODES, code);
|
|
219
220
|
if (!kind)
|
|
220
221
|
return null;
|
|
221
222
|
const message = error.message ?? '';
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Continuity of a file read incrementally by byte offset: whether the file
|
|
3
|
+
* now at a path is still the content earlier reads consumed, or was
|
|
4
|
+
* truncated, rewritten, or replaced since.
|
|
5
|
+
*
|
|
6
|
+
* {@link FileContinuity} holds the rules `createJsonlTail` applies, so every
|
|
7
|
+
* incremental reader detects the same cases the same way:
|
|
8
|
+
*
|
|
9
|
+
* - `replaced`: another file (by device and inode, read from the open
|
|
10
|
+
* descriptor, so a rename between a stat and an open cannot mix two files)
|
|
11
|
+
* now has the path.
|
|
12
|
+
* - `truncated`: the file is shorter than the read position.
|
|
13
|
+
* - `rewritten`: the mtime changed and either the first {@link HEAD_BYTES}
|
|
14
|
+
* bytes differ from those recorded, or the byte before the committed
|
|
15
|
+
* (resume-safe) offset is no longer a newline.
|
|
16
|
+
*
|
|
17
|
+
* Replacement and truncation are checked on every read; the content probe
|
|
18
|
+
* only when the mtime changed or the recorded head is still short.
|
|
19
|
+
*
|
|
20
|
+
* Known limits: a rewrite that leaves the mtime unchanged (coarse mtime
|
|
21
|
+
* resolution, or an mtime restored on purpose) or that changes only bytes
|
|
22
|
+
* past the first 4 KiB while the resume point still follows a newline goes
|
|
23
|
+
* unnoticed; on filesystems without stable inode numbers (some SMB mounts,
|
|
24
|
+
* Windows) an unchanged file can read as replaced, which costs a replay.
|
|
25
|
+
*
|
|
26
|
+
* @module fileContinuity
|
|
27
|
+
*/
|
|
28
|
+
import * as fs from 'fs';
|
|
29
|
+
import { type FileHead } from './fileHead';
|
|
30
|
+
import type { JsonlTailResetReason } from './watchers/jsonlTail';
|
|
31
|
+
/** The fields of `fs.Stats` the continuity rules read. */
|
|
32
|
+
export type FileContinuityStat = Pick<fs.Stats, 'dev' | 'ino' | 'size' | 'mtimeMs'>;
|
|
33
|
+
/** The bytes the continuity check needs, read before any state changes. */
|
|
34
|
+
export interface FileContinuityProbe {
|
|
35
|
+
/** The file's first `min(HEAD_BYTES, size)` bytes. */
|
|
36
|
+
prefix: Buffer;
|
|
37
|
+
/** The byte before the committed offset, when there is one. */
|
|
38
|
+
before: Buffer | null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* What one reader knows about the file its offsets refer to: identity,
|
|
42
|
+
* mtime, and a digest of the first bytes, recorded by {@link adopt} after
|
|
43
|
+
* each read.
|
|
44
|
+
*
|
|
45
|
+
* Synchronous readers call {@link check} on the open descriptor, restart
|
|
46
|
+
* from offset 0 when it returns a reason, then call {@link adopt}. A reader
|
|
47
|
+
* that repositions without reading (a seek to a stored offset, a reset)
|
|
48
|
+
* calls {@link forget}, so its next read adopts the file instead of
|
|
49
|
+
* comparing it with the old one. The lower-level methods ({@link needsProbe},
|
|
50
|
+
* {@link reasonFor}, {@link record}) serve asynchronous readers that probe
|
|
51
|
+
* on their own.
|
|
52
|
+
*/
|
|
53
|
+
export declare class FileContinuity {
|
|
54
|
+
private identity;
|
|
55
|
+
private head;
|
|
56
|
+
private lastMtimeMs;
|
|
57
|
+
/** What the last {@link check} probed and decided, for {@link adopt}. */
|
|
58
|
+
private checked;
|
|
59
|
+
/** Device and inode of the adopted file; `null` before the first read and after {@link forget}. */
|
|
60
|
+
get knownIdentity(): {
|
|
61
|
+
dev: number;
|
|
62
|
+
ino: number;
|
|
63
|
+
} | null;
|
|
64
|
+
/** Length and digest of the adopted file's first bytes, once probed. */
|
|
65
|
+
get knownHead(): FileHead | null;
|
|
66
|
+
/** Whether {@link reasonFor} needs bytes from the file to decide. */
|
|
67
|
+
needsProbe(stat: FileContinuityStat, readOffset: number): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Why the reader must start over: the path now names another file, the
|
|
70
|
+
* file shrank below `readOffset`, its first bytes changed, or the resume
|
|
71
|
+
* point no longer follows a newline. `null` when it can continue.
|
|
72
|
+
*/
|
|
73
|
+
reasonFor(stat: FileContinuityStat, readOffset: number, probe: FileContinuityProbe | null): JsonlTailResetReason | null;
|
|
74
|
+
/** Records the identity, mtime, and (from `probe`) first bytes of the file the offsets now refer to. */
|
|
75
|
+
record(stat: FileContinuityStat, probe: FileContinuityProbe | null): void;
|
|
76
|
+
/** Drops the recorded first bytes (the reader restarted); the next {@link record} with a probe sets them again. */
|
|
77
|
+
discardHead(): void;
|
|
78
|
+
/** Records the file as known from `prefix`, its first `min(HEAD_BYTES, size)` bytes. */
|
|
79
|
+
adoptPrefix(stat: FileContinuityStat, prefix: Buffer): void;
|
|
80
|
+
/** Forgets the file, so the next {@link check} adopts whatever file has the path. */
|
|
81
|
+
forget(): void;
|
|
82
|
+
/**
|
|
83
|
+
* Applies the rules to the file open on `fd` (`stat` from `fstat` on that
|
|
84
|
+
* descriptor), reading the bytes they need. `readOffset` is how far the
|
|
85
|
+
* reader has read; `committedOffset` (default `readOffset`) is its
|
|
86
|
+
* resume-safe position just after a newline. Returns why the reader must
|
|
87
|
+
* restart from offset 0, or `null`. Call {@link adopt} with the same
|
|
88
|
+
* `stat` once the reader has acted on the answer.
|
|
89
|
+
*/
|
|
90
|
+
check(fd: number, stat: FileContinuityStat, readOffset: number, committedOffset?: number): JsonlTailResetReason | null;
|
|
91
|
+
/** Records the file just checked as the one the reader's offsets now refer to. */
|
|
92
|
+
adopt(stat: FileContinuityStat): void;
|
|
93
|
+
}
|
|
94
|
+
/** Reads the bytes {@link FileContinuity.reasonFor} compares, from the file open on `fd`. */
|
|
95
|
+
export declare function probeFileContinuity(fd: number, stat: FileContinuityStat, committedOffset: number): FileContinuityProbe;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Continuity of a file read incrementally by byte offset: whether the file
|
|
4
|
+
* now at a path is still the content earlier reads consumed, or was
|
|
5
|
+
* truncated, rewritten, or replaced since.
|
|
6
|
+
*
|
|
7
|
+
* {@link FileContinuity} holds the rules `createJsonlTail` applies, so every
|
|
8
|
+
* incremental reader detects the same cases the same way:
|
|
9
|
+
*
|
|
10
|
+
* - `replaced`: another file (by device and inode, read from the open
|
|
11
|
+
* descriptor, so a rename between a stat and an open cannot mix two files)
|
|
12
|
+
* now has the path.
|
|
13
|
+
* - `truncated`: the file is shorter than the read position.
|
|
14
|
+
* - `rewritten`: the mtime changed and either the first {@link HEAD_BYTES}
|
|
15
|
+
* bytes differ from those recorded, or the byte before the committed
|
|
16
|
+
* (resume-safe) offset is no longer a newline.
|
|
17
|
+
*
|
|
18
|
+
* Replacement and truncation are checked on every read; the content probe
|
|
19
|
+
* only when the mtime changed or the recorded head is still short.
|
|
20
|
+
*
|
|
21
|
+
* Known limits: a rewrite that leaves the mtime unchanged (coarse mtime
|
|
22
|
+
* resolution, or an mtime restored on purpose) or that changes only bytes
|
|
23
|
+
* past the first 4 KiB while the resume point still follows a newline goes
|
|
24
|
+
* unnoticed; on filesystems without stable inode numbers (some SMB mounts,
|
|
25
|
+
* Windows) an unchanged file can read as replaced, which costs a replay.
|
|
26
|
+
*
|
|
27
|
+
* @module fileContinuity
|
|
28
|
+
*/
|
|
29
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
30
|
+
if (k2 === undefined) k2 = k;
|
|
31
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
32
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
33
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
34
|
+
}
|
|
35
|
+
Object.defineProperty(o, k2, desc);
|
|
36
|
+
}) : (function(o, m, k, k2) {
|
|
37
|
+
if (k2 === undefined) k2 = k;
|
|
38
|
+
o[k2] = m[k];
|
|
39
|
+
}));
|
|
40
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
41
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
42
|
+
}) : function(o, v) {
|
|
43
|
+
o["default"] = v;
|
|
44
|
+
});
|
|
45
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
46
|
+
var ownKeys = function(o) {
|
|
47
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
48
|
+
var ar = [];
|
|
49
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
50
|
+
return ar;
|
|
51
|
+
};
|
|
52
|
+
return ownKeys(o);
|
|
53
|
+
};
|
|
54
|
+
return function (mod) {
|
|
55
|
+
if (mod && mod.__esModule) return mod;
|
|
56
|
+
var result = {};
|
|
57
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
58
|
+
__setModuleDefault(result, mod);
|
|
59
|
+
return result;
|
|
60
|
+
};
|
|
61
|
+
})();
|
|
62
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
63
|
+
exports.FileContinuity = void 0;
|
|
64
|
+
exports.probeFileContinuity = probeFileContinuity;
|
|
65
|
+
const fs = __importStar(require("fs"));
|
|
66
|
+
const fileHead_1 = require("./fileHead");
|
|
67
|
+
/**
|
|
68
|
+
* What one reader knows about the file its offsets refer to: identity,
|
|
69
|
+
* mtime, and a digest of the first bytes, recorded by {@link adopt} after
|
|
70
|
+
* each read.
|
|
71
|
+
*
|
|
72
|
+
* Synchronous readers call {@link check} on the open descriptor, restart
|
|
73
|
+
* from offset 0 when it returns a reason, then call {@link adopt}. A reader
|
|
74
|
+
* that repositions without reading (a seek to a stored offset, a reset)
|
|
75
|
+
* calls {@link forget}, so its next read adopts the file instead of
|
|
76
|
+
* comparing it with the old one. The lower-level methods ({@link needsProbe},
|
|
77
|
+
* {@link reasonFor}, {@link record}) serve asynchronous readers that probe
|
|
78
|
+
* on their own.
|
|
79
|
+
*/
|
|
80
|
+
class FileContinuity {
|
|
81
|
+
identity = null;
|
|
82
|
+
head = null;
|
|
83
|
+
lastMtimeMs = -1;
|
|
84
|
+
/** What the last {@link check} probed and decided, for {@link adopt}. */
|
|
85
|
+
checked = null;
|
|
86
|
+
/** Device and inode of the adopted file; `null` before the first read and after {@link forget}. */
|
|
87
|
+
get knownIdentity() {
|
|
88
|
+
return this.identity;
|
|
89
|
+
}
|
|
90
|
+
/** Length and digest of the adopted file's first bytes, once probed. */
|
|
91
|
+
get knownHead() {
|
|
92
|
+
return this.head;
|
|
93
|
+
}
|
|
94
|
+
/** Whether {@link reasonFor} needs bytes from the file to decide. */
|
|
95
|
+
needsProbe(stat, readOffset) {
|
|
96
|
+
return (this.identity === null ||
|
|
97
|
+
stat.dev !== this.identity.dev ||
|
|
98
|
+
stat.ino !== this.identity.ino ||
|
|
99
|
+
stat.size < readOffset ||
|
|
100
|
+
stat.mtimeMs !== this.lastMtimeMs ||
|
|
101
|
+
this.head === null ||
|
|
102
|
+
this.head.bytes < Math.min(fileHead_1.HEAD_BYTES, stat.size));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Why the reader must start over: the path now names another file, the
|
|
106
|
+
* file shrank below `readOffset`, its first bytes changed, or the resume
|
|
107
|
+
* point no longer follows a newline. `null` when it can continue.
|
|
108
|
+
*/
|
|
109
|
+
reasonFor(stat, readOffset, probe) {
|
|
110
|
+
if (this.identity && (stat.dev !== this.identity.dev || stat.ino !== this.identity.ino)) {
|
|
111
|
+
return 'replaced';
|
|
112
|
+
}
|
|
113
|
+
if (stat.size < readOffset)
|
|
114
|
+
return 'truncated';
|
|
115
|
+
if (this.identity && stat.mtimeMs !== this.lastMtimeMs && probe) {
|
|
116
|
+
if (this.head && !(0, fileHead_1.headMatches)(this.head, probe.prefix))
|
|
117
|
+
return 'rewritten';
|
|
118
|
+
if (probe.before && probe.before[0] !== 0x0a)
|
|
119
|
+
return 'rewritten';
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
/** Records the identity, mtime, and (from `probe`) first bytes of the file the offsets now refer to. */
|
|
124
|
+
record(stat, probe) {
|
|
125
|
+
this.identity = { dev: stat.dev, ino: stat.ino };
|
|
126
|
+
this.lastMtimeMs = stat.mtimeMs;
|
|
127
|
+
if (probe && (!this.head || this.head.bytes < Math.min(fileHead_1.HEAD_BYTES, stat.size))) {
|
|
128
|
+
this.head = (0, fileHead_1.fileHeadOf)(probe.prefix);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/** Drops the recorded first bytes (the reader restarted); the next {@link record} with a probe sets them again. */
|
|
132
|
+
discardHead() {
|
|
133
|
+
this.head = null;
|
|
134
|
+
}
|
|
135
|
+
/** Records the file as known from `prefix`, its first `min(HEAD_BYTES, size)` bytes. */
|
|
136
|
+
adoptPrefix(stat, prefix) {
|
|
137
|
+
this.identity = { dev: stat.dev, ino: stat.ino };
|
|
138
|
+
this.lastMtimeMs = stat.mtimeMs;
|
|
139
|
+
this.head = (0, fileHead_1.fileHeadOf)(prefix);
|
|
140
|
+
}
|
|
141
|
+
/** Forgets the file, so the next {@link check} adopts whatever file has the path. */
|
|
142
|
+
forget() {
|
|
143
|
+
this.identity = null;
|
|
144
|
+
this.head = null;
|
|
145
|
+
this.lastMtimeMs = -1;
|
|
146
|
+
this.checked = null;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Applies the rules to the file open on `fd` (`stat` from `fstat` on that
|
|
150
|
+
* descriptor), reading the bytes they need. `readOffset` is how far the
|
|
151
|
+
* reader has read; `committedOffset` (default `readOffset`) is its
|
|
152
|
+
* resume-safe position just after a newline. Returns why the reader must
|
|
153
|
+
* restart from offset 0, or `null`. Call {@link adopt} with the same
|
|
154
|
+
* `stat` once the reader has acted on the answer.
|
|
155
|
+
*/
|
|
156
|
+
check(fd, stat, readOffset, committedOffset = readOffset) {
|
|
157
|
+
const probe = this.needsProbe(stat, readOffset)
|
|
158
|
+
? probeFileContinuity(fd, stat, committedOffset)
|
|
159
|
+
: null;
|
|
160
|
+
const reason = this.reasonFor(stat, readOffset, probe);
|
|
161
|
+
this.checked = { probe, reset: reason !== null };
|
|
162
|
+
return reason;
|
|
163
|
+
}
|
|
164
|
+
/** Records the file just checked as the one the reader's offsets now refer to. */
|
|
165
|
+
adopt(stat) {
|
|
166
|
+
const checked = this.checked;
|
|
167
|
+
this.checked = null;
|
|
168
|
+
if (checked?.reset)
|
|
169
|
+
this.head = null;
|
|
170
|
+
this.record(stat, checked?.probe ?? null);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
exports.FileContinuity = FileContinuity;
|
|
174
|
+
/** Reads the bytes {@link FileContinuity.reasonFor} compares, from the file open on `fd`. */
|
|
175
|
+
function probeFileContinuity(fd, stat, committedOffset) {
|
|
176
|
+
return {
|
|
177
|
+
prefix: readAt(fd, 0, Math.min(fileHead_1.HEAD_BYTES, stat.size)),
|
|
178
|
+
before: committedOffset > 0 && committedOffset <= stat.size
|
|
179
|
+
? readAt(fd, committedOffset - 1, 1)
|
|
180
|
+
: null,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function readAt(fd, position, length) {
|
|
184
|
+
const buffer = Buffer.alloc(length);
|
|
185
|
+
const count = length > 0 ? fs.readSync(fd, buffer, 0, length, position) : 0;
|
|
186
|
+
return buffer.subarray(0, count);
|
|
187
|
+
}
|