sidekick-shared 0.28.3 → 0.29.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.
- package/CHANGELOG.md +86 -0
- package/README.md +188 -27
- package/dist/aggregation/EventAggregator.d.ts +19 -0
- package/dist/aggregation/EventAggregator.js +174 -35
- package/dist/aggregation/snapshot.d.ts +37 -8
- package/dist/aggregation/snapshot.js +31 -9
- package/dist/aggregation/types.d.ts +13 -0
- package/dist/authStatus.d.ts +54 -0
- package/dist/authStatus.js +91 -0
- package/dist/browser.d.ts +16 -6
- package/dist/browser.js +24 -2
- package/dist/codex.d.ts +23 -0
- package/dist/codex.js +44 -0
- package/dist/codexProfiles.d.ts +1 -0
- package/dist/codexProfiles.js +8 -11
- package/dist/context/sessionContext.js +4 -0
- package/dist/esm/aggregation/EventAggregator.js +174 -35
- package/dist/esm/aggregation/snapshot.js +31 -9
- package/dist/esm/authStatus.js +87 -0
- package/dist/esm/browser.js +5 -2
- package/dist/esm/codex.js +18 -0
- package/dist/esm/codexProfiles.js +8 -11
- package/dist/esm/context/sessionContext.js +4 -0
- package/dist/esm/formatters/toolSummary.js +4 -1
- package/dist/esm/formatting.js +10 -1
- package/dist/esm/index.js +11 -6
- package/dist/esm/modelCatalog.js +7 -5
- package/dist/esm/modelContext.js +4 -3
- package/dist/esm/modelInfo.js +425 -47
- package/dist/esm/node.js +9 -3
- package/dist/esm/observedContextWindows.js +2 -1
- package/dist/esm/ownKey.js +26 -0
- package/dist/esm/parsers/codexParser.js +351 -54
- package/dist/esm/parsers/codexRolloutOrigin.js +107 -0
- package/dist/esm/parsers/openCodeParser.js +85 -22
- package/dist/esm/parsers/subagentScanner.js +33 -12
- package/dist/esm/parsers/subagentTraceParser.js +97 -37
- package/dist/esm/pricing.js +18 -0
- package/dist/esm/pricingCatalog.js +17 -9
- package/dist/esm/promptHistory.js +66 -4
- package/dist/esm/providerFailure.js +124 -3
- package/dist/esm/providers/claudeCode.js +10 -2
- package/dist/esm/providers/codex.js +84 -62
- package/dist/esm/providers/codexDatabase.js +3 -19
- package/dist/esm/providers/codexRollouts.js +24 -1
- package/dist/esm/providers/openCode.js +9 -17
- package/dist/esm/report/htmlReportGenerator.js +42 -5
- package/dist/esm/report/transcriptParser.js +13 -11
- package/dist/esm/schemas/providerFailure.js +3 -1
- package/dist/esm/schemas/sessionEvent.js +35 -14
- package/dist/esm/schemas/transcript.js +4 -0
- package/dist/esm/sessionPromptHistory.js +2 -2
- package/dist/esm/statusline/payload.js +9 -0
- package/dist/esm/subagentSpawn.js +163 -0
- package/dist/esm/testing/credentialGuard.js +87 -5
- package/dist/esm/toolCallOrigins.js +310 -0
- package/dist/esm/transcript.js +46 -3
- package/dist/esm/turns/assistantTurn.js +3 -2
- package/dist/esm/usage/claudeUsageDedupe.js +68 -0
- package/dist/esm/usage/usageEvents.js +146 -17
- package/dist/esm/usage/usageReports.js +39 -6
- package/dist/esm/watchers/eventBridge.js +15 -0
- package/dist/esm/watchers/factory.js +5 -3
- package/dist/esm/watchers/jsonlTail.js +442 -58
- package/dist/esm/watchers/jsonlWatcher.js +51 -4
- package/dist/esm/watchers/providerReaderWatcher.js +13 -0
- package/dist/esm/zaiQuota.js +2 -1
- package/dist/formatters/toolSummary.js +4 -1
- package/dist/formatting.d.ts +26 -1
- package/dist/formatting.js +11 -1
- package/dist/index.d.ts +22 -11
- package/dist/index.js +44 -12
- package/dist/modelCatalog.js +7 -5
- package/dist/modelContext.js +4 -3
- package/dist/modelInfo.d.ts +185 -16
- package/dist/modelInfo.js +434 -49
- package/dist/node.d.ts +16 -5
- package/dist/node.js +33 -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 +118 -0
- package/dist/parsers/codexParser.js +352 -54
- package/dist/parsers/codexRolloutOrigin.d.ts +49 -0
- package/dist/parsers/codexRolloutOrigin.js +111 -0
- package/dist/parsers/openCodeParser.d.ts +38 -0
- package/dist/parsers/openCodeParser.js +88 -22
- package/dist/parsers/subagentScanner.d.ts +19 -6
- package/dist/parsers/subagentScanner.js +33 -12
- package/dist/parsers/subagentTraceParser.d.ts +25 -5
- package/dist/parsers/subagentTraceParser.js +97 -37
- package/dist/pricing.d.ts +22 -0
- package/dist/pricing.js +50 -0
- package/dist/pricingCatalog.d.ts +10 -0
- package/dist/pricingCatalog.js +16 -8
- package/dist/promptHistory.d.ts +65 -4
- package/dist/promptHistory.js +68 -5
- package/dist/providerFailure.d.ts +44 -0
- package/dist/providerFailure.js +126 -4
- package/dist/providers/claudeCode.js +9 -1
- package/dist/providers/codex.d.ts +3 -22
- package/dist/providers/codex.js +85 -64
- package/dist/providers/codexDatabase.d.ts +3 -2
- package/dist/providers/codexDatabase.js +4 -22
- package/dist/providers/codexRollouts.d.ts +22 -0
- package/dist/providers/codexRollouts.js +24 -0
- package/dist/providers/openCode.js +8 -16
- package/dist/providers/types.d.ts +14 -0
- package/dist/report/htmlReportGenerator.js +42 -5
- package/dist/report/transcriptParser.d.ts +10 -1
- package/dist/report/transcriptParser.js +13 -11
- package/dist/schemas/index.d.ts +1 -0
- package/dist/schemas/providerFailure.d.ts +39 -0
- package/dist/schemas/providerFailure.js +2 -0
- package/dist/schemas/sessionEvent.d.ts +27 -10
- package/dist/schemas/sessionEvent.js +35 -14
- package/dist/schemas/transcript.d.ts +2 -0
- package/dist/schemas/transcript.js +4 -0
- package/dist/sessionPromptHistory.d.ts +7 -1
- package/dist/sessionPromptHistory.js +1 -1
- package/dist/statusline/payload.d.ts +10 -0
- package/dist/statusline/payload.js +14 -0
- package/dist/subagentSpawn.d.ts +80 -0
- package/dist/subagentSpawn.js +174 -0
- package/dist/testing/credentialGuard.d.ts +14 -0
- package/dist/testing/credentialGuard.js +87 -5
- package/dist/toolCallOrigins.d.ts +70 -0
- package/dist/toolCallOrigins.js +346 -0
- package/dist/transcript.d.ts +12 -0
- package/dist/transcript.js +46 -3
- package/dist/turns/assistantTurn.d.ts +1 -0
- package/dist/turns/assistantTurn.js +3 -2
- package/dist/types/opencode.d.ts +2 -0
- package/dist/usage/claudeUsageDedupe.d.ts +31 -0
- package/dist/usage/claudeUsageDedupe.js +68 -0
- package/dist/usage/usageEvents.d.ts +54 -6
- package/dist/usage/usageEvents.js +146 -17
- package/dist/usage/usageReports.d.ts +35 -4
- package/dist/usage/usageReports.js +39 -6
- package/dist/watchers/eventBridge.js +15 -0
- package/dist/watchers/factory.d.ts +6 -0
- package/dist/watchers/factory.js +5 -3
- package/dist/watchers/jsonlTail.d.ts +124 -4
- package/dist/watchers/jsonlTail.js +446 -59
- package/dist/watchers/jsonlWatcher.d.ts +18 -1
- package/dist/watchers/jsonlWatcher.js +50 -3
- package/dist/watchers/providerReaderWatcher.d.ts +7 -0
- package/dist/watchers/providerReaderWatcher.js +13 -0
- package/dist/watchers/types.d.ts +12 -0
- package/dist/zaiQuota.js +2 -1
- package/package.json +41 -1
|
@@ -37,8 +37,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
37
37
|
};
|
|
38
38
|
})();
|
|
39
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
-
exports.DEFAULT_JSONL_TAIL_MAX_LINE_BYTES = exports.DEFAULT_JSONL_TAIL_READ_BYTES = void 0;
|
|
40
|
+
exports.JsonlTailSymlinkError = exports.JSONL_TAIL_CHECKPOINT_VERSION = exports.DEFAULT_JSONL_TAIL_MAX_LINE_BYTES = exports.DEFAULT_JSONL_TAIL_READ_BYTES = void 0;
|
|
41
41
|
exports.createJsonlTail = createJsonlTail;
|
|
42
|
+
exports.captureJsonlCheckpoint = captureJsonlCheckpoint;
|
|
43
|
+
exports.verifyJsonlCheckpoint = verifyJsonlCheckpoint;
|
|
42
44
|
const fs = __importStar(require("fs"));
|
|
43
45
|
const fileHead_1 = require("../fileHead");
|
|
44
46
|
const jsonl_1 = require("../parsers/jsonl");
|
|
@@ -48,29 +50,113 @@ const DEFAULT_CATCHUP_INTERVAL_MS = 30_000;
|
|
|
48
50
|
exports.DEFAULT_JSONL_TAIL_READ_BYTES = 1024 * 1024;
|
|
49
51
|
/** Default longest line parsed; longer lines are skipped and reported. */
|
|
50
52
|
exports.DEFAULT_JSONL_TAIL_MAX_LINE_BYTES = 16 * 1024 * 1024;
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
53
|
+
/** The `version` written into, and accepted from, {@link JsonlTailCheckpoint}. */
|
|
54
|
+
exports.JSONL_TAIL_CHECKPOINT_VERSION = 1;
|
|
55
|
+
const BACKWARD_SCAN_BYTES = 64 * 1024;
|
|
56
|
+
/**
|
|
57
|
+
* Skip old complete events while retaining an in-progress final line: the
|
|
58
|
+
* offset just after the last newline in the first `size` bytes, or 0.
|
|
59
|
+
*/
|
|
60
|
+
function lastCompleteLineOffset(fd, size) {
|
|
61
|
+
let end = size;
|
|
62
|
+
const buffer = Buffer.alloc(BACKWARD_SCAN_BYTES);
|
|
63
|
+
while (end > 0) {
|
|
64
|
+
const start = Math.max(0, end - buffer.length);
|
|
65
|
+
const count = fs.readSync(fd, buffer, 0, end - start, start);
|
|
66
|
+
const newline = buffer.subarray(0, count).lastIndexOf(0x0a);
|
|
67
|
+
if (newline >= 0)
|
|
68
|
+
return start + newline + 1;
|
|
69
|
+
end = start;
|
|
66
70
|
}
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
async function lastCompleteLineOffsetAsync(handle, size) {
|
|
74
|
+
let end = size;
|
|
75
|
+
const buffer = Buffer.alloc(BACKWARD_SCAN_BYTES);
|
|
76
|
+
while (end > 0) {
|
|
77
|
+
const start = Math.max(0, end - buffer.length);
|
|
78
|
+
const { bytesRead } = await handle.read(buffer, 0, end - start, start);
|
|
79
|
+
const newline = buffer.subarray(0, bytesRead).lastIndexOf(0x0a);
|
|
80
|
+
if (newline >= 0)
|
|
81
|
+
return start + newline + 1;
|
|
82
|
+
end = start;
|
|
83
|
+
}
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Reported to `onError` (and nothing is read) when `noFollow` is set and the
|
|
88
|
+
* tailed path is a symbolic link.
|
|
89
|
+
*/
|
|
90
|
+
class JsonlTailSymlinkError extends Error {
|
|
91
|
+
path;
|
|
92
|
+
code = 'JSONL_TAIL_SYMLINK';
|
|
93
|
+
constructor(path) {
|
|
94
|
+
super(`Refusing to follow the symbolic link at ${path} (noFollow is set).`);
|
|
95
|
+
this.path = path;
|
|
96
|
+
this.name = 'JsonlTailSymlinkError';
|
|
69
97
|
}
|
|
70
98
|
}
|
|
99
|
+
exports.JsonlTailSymlinkError = JsonlTailSymlinkError;
|
|
71
100
|
function createJsonlTail(options) {
|
|
72
101
|
return new JsonlTailReader(options);
|
|
73
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Records a {@link JsonlTailCheckpoint} for byte `offset` of `filePath`
|
|
105
|
+
* without a tail, for readers that keep their own offset (a snapshot that
|
|
106
|
+
* stores a reader position, for example). The checkpoint carries the file's
|
|
107
|
+
* current device and inode and a digest of its first bytes, so
|
|
108
|
+
* {@link verifyJsonlCheckpoint} (or {@link JsonlTail.resumeFrom}) can later
|
|
109
|
+
* tell whether `offset` still refers to the same, append-only content.
|
|
110
|
+
*
|
|
111
|
+
* Returns `null` when the file is missing or unreadable, or when `offset` is
|
|
112
|
+
* not a resume-safe position in it: negative, past the end, or not 0 and not
|
|
113
|
+
* just after a newline. Follows symbolic links.
|
|
114
|
+
*/
|
|
115
|
+
function captureJsonlCheckpoint(filePath, offset) {
|
|
116
|
+
if (!Number.isSafeInteger(offset) || offset < 0)
|
|
117
|
+
return null;
|
|
118
|
+
let fd = null;
|
|
119
|
+
try {
|
|
120
|
+
fd = fs.openSync(filePath, 'r');
|
|
121
|
+
const stat = fs.fstatSync(fd);
|
|
122
|
+
if (offset > stat.size)
|
|
123
|
+
return null;
|
|
124
|
+
if (offset > 0 && readAt(fd, offset - 1, 1)[0] !== 0x0a)
|
|
125
|
+
return null;
|
|
126
|
+
return checkpointOf(offset, stat, (0, fileHead_1.fileHeadOf)(readPrefix(fd, stat)));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
closeQuietly(fd);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Whether `checkpoint` (any value, typically parsed from JSON) is a valid
|
|
137
|
+
* {@link JsonlTailCheckpoint} that still describes `filePath`: same device
|
|
138
|
+
* and inode, the recorded first bytes unchanged, and its offset within the
|
|
139
|
+
* file and just after a newline (or 0). This is the check
|
|
140
|
+
* {@link JsonlTail.resumeFrom} applies. `false` on any error, including a
|
|
141
|
+
* missing file. Follows symbolic links.
|
|
142
|
+
*/
|
|
143
|
+
function verifyJsonlCheckpoint(filePath, checkpoint) {
|
|
144
|
+
const parsed = parseCheckpoint(checkpoint);
|
|
145
|
+
if (parsed === null)
|
|
146
|
+
return false;
|
|
147
|
+
let fd = null;
|
|
148
|
+
try {
|
|
149
|
+
fd = fs.openSync(filePath, 'r');
|
|
150
|
+
const stat = fs.fstatSync(fd);
|
|
151
|
+
return checkpointMatches(parsed, fd, stat, readPrefix(fd, stat));
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
closeQuietly(fd);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
74
160
|
class JsonlTailReader {
|
|
75
161
|
options;
|
|
76
162
|
active = false;
|
|
@@ -88,20 +174,34 @@ class JsonlTailReader {
|
|
|
88
174
|
lastMtimeMs = -1;
|
|
89
175
|
maxReadBytes;
|
|
90
176
|
maxLineBytes;
|
|
177
|
+
rewritePolicy;
|
|
91
178
|
fsWatcher = null;
|
|
92
179
|
debounceTimer = null;
|
|
93
180
|
catchupTimer = null;
|
|
94
181
|
eventsInCurrentBatch = 0;
|
|
95
182
|
parser;
|
|
183
|
+
/** Collects parsed events while a `read()` is running. */
|
|
184
|
+
collector = null;
|
|
185
|
+
/** Serializes `read()` calls. */
|
|
186
|
+
readChain = Promise.resolve();
|
|
187
|
+
/** `read()` calls queued or running; `readNow()` waits for them. */
|
|
188
|
+
queuedReads = 0;
|
|
189
|
+
readNowDeferred = false;
|
|
190
|
+
/** Bumped by repositioning calls so an in-flight `read()` stops early. */
|
|
191
|
+
generation = 0;
|
|
192
|
+
/** `resumeFrom()` chose the position; `start()` must not move it to the end. */
|
|
193
|
+
positioned = false;
|
|
96
194
|
constructor(options) {
|
|
97
195
|
this.options = options;
|
|
98
196
|
this.readOffset = options.startOffset ?? 0;
|
|
99
197
|
this.committedOffset = this.readOffset;
|
|
100
198
|
this.maxReadBytes = positiveInteger(options.maxReadBytes, exports.DEFAULT_JSONL_TAIL_READ_BYTES);
|
|
101
199
|
this.maxLineBytes = positiveInteger(options.maxLineBytes, exports.DEFAULT_JSONL_TAIL_MAX_LINE_BYTES);
|
|
200
|
+
this.rewritePolicy = options.onRewrite === 'end' ? 'end' : 'restart';
|
|
102
201
|
this.parser = new jsonl_1.JsonlParser({
|
|
103
202
|
onEvent: (event) => {
|
|
104
203
|
this.eventsInCurrentBatch += 1;
|
|
204
|
+
this.collector?.push({ event, line: this.currentLine });
|
|
105
205
|
this.options.onEvent(event, this.currentLine);
|
|
106
206
|
},
|
|
107
207
|
onError: (error, line) => this.options.onError?.(error, line),
|
|
@@ -114,13 +214,18 @@ class JsonlTailReader {
|
|
|
114
214
|
if (this.active)
|
|
115
215
|
return;
|
|
116
216
|
this.active = true;
|
|
117
|
-
if (this.options.startAtEnd && this.options.startOffset === undefined) {
|
|
217
|
+
if (this.options.startAtEnd && this.options.startOffset === undefined && !this.positioned) {
|
|
218
|
+
let fd = null;
|
|
118
219
|
try {
|
|
119
|
-
|
|
220
|
+
fd = this.openSync();
|
|
221
|
+
this.readOffset = lastCompleteLineOffset(fd, fs.fstatSync(fd).size);
|
|
120
222
|
}
|
|
121
223
|
catch {
|
|
122
224
|
this.readOffset = 0;
|
|
123
225
|
}
|
|
226
|
+
finally {
|
|
227
|
+
closeQuietly(fd);
|
|
228
|
+
}
|
|
124
229
|
this.committedOffset = this.readOffset;
|
|
125
230
|
}
|
|
126
231
|
this.readNow();
|
|
@@ -152,11 +257,20 @@ class JsonlTailReader {
|
|
|
152
257
|
this.stop();
|
|
153
258
|
}
|
|
154
259
|
readNow() {
|
|
260
|
+
if (this.queuedReads > 0) {
|
|
261
|
+
this.readNowDeferred = true;
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
155
264
|
let fd = null;
|
|
156
265
|
try {
|
|
157
|
-
fd =
|
|
266
|
+
fd = this.openSync();
|
|
158
267
|
const stat = fs.fstatSync(fd);
|
|
159
|
-
this.
|
|
268
|
+
const probe = this.probeNeeded(stat) ? this.probeSync(fd, stat) : null;
|
|
269
|
+
const reason = this.resetReason(stat, probe);
|
|
270
|
+
if (reason !== null) {
|
|
271
|
+
this.reset(reason, this.rewritePolicy === 'end' ? lastCompleteLineOffset(fd, stat.size) : 0);
|
|
272
|
+
}
|
|
273
|
+
this.adoptFile(stat, probe);
|
|
160
274
|
if (stat.size <= this.readOffset)
|
|
161
275
|
return;
|
|
162
276
|
this.eventsInCurrentBatch = 0;
|
|
@@ -180,61 +294,166 @@ class JsonlTailReader {
|
|
|
180
294
|
});
|
|
181
295
|
}
|
|
182
296
|
catch (error) {
|
|
183
|
-
this.options.onError?.(
|
|
297
|
+
this.options.onError?.(toError(error));
|
|
184
298
|
}
|
|
185
299
|
finally {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
300
|
+
closeQuietly(fd);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
read() {
|
|
304
|
+
this.queuedReads += 1;
|
|
305
|
+
const run = this.readChain.then(() => this.readAsync());
|
|
306
|
+
this.readChain = run.catch(() => undefined);
|
|
307
|
+
return run;
|
|
308
|
+
}
|
|
309
|
+
async readAsync() {
|
|
310
|
+
const generation = this.generation;
|
|
311
|
+
const events = [];
|
|
312
|
+
let reset;
|
|
313
|
+
let bytesRead = 0;
|
|
314
|
+
let eventsRead = 0;
|
|
315
|
+
let handle = null;
|
|
316
|
+
try {
|
|
317
|
+
handle = await this.openAsync();
|
|
318
|
+
const stat = await handle.stat();
|
|
319
|
+
const probe = this.probeNeeded(stat) ? await this.probeAsync(handle, stat) : null;
|
|
320
|
+
if (generation === this.generation) {
|
|
321
|
+
const reason = this.resetReason(stat, probe);
|
|
322
|
+
const offset = reason !== null && this.rewritePolicy === 'end'
|
|
323
|
+
? await lastCompleteLineOffsetAsync(handle, stat.size)
|
|
324
|
+
: 0;
|
|
325
|
+
if (generation === this.generation) {
|
|
326
|
+
if (reason !== null)
|
|
327
|
+
reset = this.reset(reason, offset);
|
|
328
|
+
this.adoptFile(stat, probe);
|
|
329
|
+
if (stat.size > this.readOffset) {
|
|
330
|
+
this.eventsInCurrentBatch = 0;
|
|
331
|
+
const chunk = Buffer.alloc(Math.min(this.maxReadBytes, stat.size - this.readOffset));
|
|
332
|
+
while (this.readOffset < stat.size) {
|
|
333
|
+
const want = Math.min(chunk.length, stat.size - this.readOffset);
|
|
334
|
+
const position = this.readOffset;
|
|
335
|
+
const { bytesRead: count } = await handle.read(chunk, 0, want, position);
|
|
336
|
+
if (count <= 0 || generation !== this.generation)
|
|
337
|
+
break;
|
|
338
|
+
this.collector = events;
|
|
339
|
+
try {
|
|
340
|
+
this.consume(chunk.subarray(0, count), position);
|
|
341
|
+
}
|
|
342
|
+
finally {
|
|
343
|
+
this.collector = null;
|
|
344
|
+
}
|
|
345
|
+
this.readOffset += count;
|
|
346
|
+
bytesRead += count;
|
|
347
|
+
}
|
|
348
|
+
eventsRead = this.eventsInCurrentBatch;
|
|
349
|
+
if (bytesRead > 0) {
|
|
350
|
+
this.options.onBatchComplete?.({
|
|
351
|
+
bytesRead,
|
|
352
|
+
eventsRead,
|
|
353
|
+
offset: this.committedOffset,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
192
357
|
}
|
|
193
358
|
}
|
|
194
359
|
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
this.options.onError?.(toError(error));
|
|
362
|
+
}
|
|
363
|
+
finally {
|
|
364
|
+
if (handle)
|
|
365
|
+
await handle.close().catch(() => undefined);
|
|
366
|
+
this.queuedReads -= 1;
|
|
367
|
+
if (this.queuedReads === 0 && this.readNowDeferred) {
|
|
368
|
+
this.readNowDeferred = false;
|
|
369
|
+
this.readNow();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
bytesRead,
|
|
374
|
+
eventsRead,
|
|
375
|
+
offset: this.committedOffset,
|
|
376
|
+
events,
|
|
377
|
+
...(reset ? { reset } : {}),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
/** Whether the continuity check needs bytes from the file. */
|
|
381
|
+
probeNeeded(stat) {
|
|
382
|
+
return (this.identity === null ||
|
|
383
|
+
stat.dev !== this.identity.dev ||
|
|
384
|
+
stat.ino !== this.identity.ino ||
|
|
385
|
+
stat.size < this.readOffset ||
|
|
386
|
+
stat.mtimeMs !== this.lastMtimeMs ||
|
|
387
|
+
this.head === null ||
|
|
388
|
+
this.head.bytes < Math.min(fileHead_1.HEAD_BYTES, stat.size));
|
|
389
|
+
}
|
|
390
|
+
probeSync(fd, stat) {
|
|
391
|
+
return {
|
|
392
|
+
prefix: readPrefix(fd, stat),
|
|
393
|
+
before: this.committedOffset > 0 && this.committedOffset <= stat.size
|
|
394
|
+
? readAt(fd, this.committedOffset - 1, 1)
|
|
395
|
+
: null,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
async probeAsync(handle, stat) {
|
|
399
|
+
const committedOffset = this.committedOffset;
|
|
400
|
+
return {
|
|
401
|
+
prefix: await readAtAsync(handle, 0, Math.min(fileHead_1.HEAD_BYTES, stat.size)),
|
|
402
|
+
before: committedOffset > 0 && committedOffset <= stat.size
|
|
403
|
+
? await readAtAsync(handle, committedOffset - 1, 1)
|
|
404
|
+
: null,
|
|
405
|
+
};
|
|
195
406
|
}
|
|
196
407
|
/**
|
|
197
|
-
*
|
|
198
|
-
* first bytes changed, or the resume point no longer follows a
|
|
408
|
+
* Why the tail must reposition: the path now names another file, the file
|
|
409
|
+
* shrank, its first bytes changed, or the resume point no longer follows a
|
|
410
|
+
* newline. `null` when it can continue.
|
|
199
411
|
*/
|
|
200
|
-
|
|
201
|
-
let reason = null;
|
|
412
|
+
resetReason(stat, probe) {
|
|
202
413
|
if (this.identity && (stat.dev !== this.identity.dev || stat.ino !== this.identity.ino)) {
|
|
203
|
-
|
|
414
|
+
return 'replaced';
|
|
204
415
|
}
|
|
205
|
-
|
|
206
|
-
|
|
416
|
+
if (stat.size < this.readOffset)
|
|
417
|
+
return 'truncated';
|
|
418
|
+
if (this.identity && stat.mtimeMs !== this.lastMtimeMs && probe) {
|
|
419
|
+
if (this.head && !(0, fileHead_1.headMatches)(this.head, probe.prefix))
|
|
420
|
+
return 'rewritten';
|
|
421
|
+
if (probe.before && probe.before[0] !== 0x0a)
|
|
422
|
+
return 'rewritten';
|
|
207
423
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
reason = 'rewritten';
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
if (reason !== null)
|
|
216
|
-
this.reset(reason);
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
/** Records the identity and prefix of the file the offsets now refer to. */
|
|
427
|
+
adoptFile(stat, probe) {
|
|
217
428
|
this.identity = { dev: stat.dev, ino: stat.ino };
|
|
218
429
|
this.lastMtimeMs = stat.mtimeMs;
|
|
219
|
-
if (!this.head || this.head.bytes < Math.min(fileHead_1.HEAD_BYTES, stat.size)) {
|
|
220
|
-
this.head = (0, fileHead_1.fileHeadOf)(
|
|
430
|
+
if (probe && (!this.head || this.head.bytes < Math.min(fileHead_1.HEAD_BYTES, stat.size))) {
|
|
431
|
+
this.head = (0, fileHead_1.fileHeadOf)(probe.prefix);
|
|
221
432
|
}
|
|
222
433
|
}
|
|
223
|
-
reset(reason) {
|
|
434
|
+
reset(reason, offset) {
|
|
224
435
|
const previousOffset = this.committedOffset;
|
|
225
|
-
this.
|
|
226
|
-
this.committedOffset = 0;
|
|
227
|
-
this.pendingBytes = Buffer.alloc(0);
|
|
228
|
-
this.skippingLine = false;
|
|
436
|
+
this.position(offset);
|
|
229
437
|
this.head = null;
|
|
230
|
-
this.parser.reset();
|
|
231
438
|
if (reason === 'replaced' && this.active) {
|
|
232
439
|
// fs.watch follows the old inode; watch the file now at the path.
|
|
233
440
|
this.fsWatcher?.close();
|
|
234
441
|
this.fsWatcher = null;
|
|
235
442
|
this.watchFile();
|
|
236
443
|
}
|
|
237
|
-
this.
|
|
444
|
+
const reset = this.rewritePolicy === 'end'
|
|
445
|
+
? { reason, previousOffset, offset }
|
|
446
|
+
: { reason, previousOffset };
|
|
447
|
+
this.options.onReset?.(reset);
|
|
448
|
+
return reset;
|
|
449
|
+
}
|
|
450
|
+
/** Moves both offsets to `offset` and drops any partial-line state. */
|
|
451
|
+
position(offset) {
|
|
452
|
+
this.readOffset = offset;
|
|
453
|
+
this.committedOffset = offset;
|
|
454
|
+
this.pendingBytes = Buffer.alloc(0);
|
|
455
|
+
this.skippingLine = false;
|
|
456
|
+
this.parser.reset();
|
|
238
457
|
}
|
|
239
458
|
/** Parses the complete lines in `bytes`, which start at file offset `position`. */
|
|
240
459
|
consume(bytes, position) {
|
|
@@ -286,13 +505,93 @@ class JsonlTailReader {
|
|
|
286
505
|
return this.committedOffset;
|
|
287
506
|
}
|
|
288
507
|
seekTo(offset) {
|
|
289
|
-
this.
|
|
290
|
-
this.
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
this.
|
|
508
|
+
this.generation += 1;
|
|
509
|
+
this.position(Math.max(0, offset));
|
|
510
|
+
}
|
|
511
|
+
checkpoint() {
|
|
512
|
+
return checkpointOf(this.committedOffset, this.identity, this.head);
|
|
513
|
+
}
|
|
514
|
+
resumeFrom(checkpoint) {
|
|
515
|
+
this.generation += 1;
|
|
516
|
+
this.positioned = true;
|
|
517
|
+
const parsed = parseCheckpoint(checkpoint);
|
|
518
|
+
let fd = null;
|
|
519
|
+
try {
|
|
520
|
+
fd = this.openSync();
|
|
521
|
+
const stat = fs.fstatSync(fd);
|
|
522
|
+
const prefix = readPrefix(fd, stat);
|
|
523
|
+
const matches = parsed !== null && checkpointMatches(parsed, fd, stat, prefix);
|
|
524
|
+
const offset = matches
|
|
525
|
+
? parsed.offset
|
|
526
|
+
: this.rewritePolicy === 'end'
|
|
527
|
+
? lastCompleteLineOffset(fd, stat.size)
|
|
528
|
+
: 0;
|
|
529
|
+
this.position(offset);
|
|
530
|
+
this.identity = { dev: stat.dev, ino: stat.ino };
|
|
531
|
+
this.lastMtimeMs = stat.mtimeMs;
|
|
532
|
+
this.head = (0, fileHead_1.fileHeadOf)(prefix);
|
|
533
|
+
return matches;
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
this.position(0);
|
|
537
|
+
this.identity = null;
|
|
538
|
+
this.head = null;
|
|
539
|
+
this.lastMtimeMs = -1;
|
|
540
|
+
this.options.onError?.(toError(error));
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
finally {
|
|
544
|
+
closeQuietly(fd);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
openFlags() {
|
|
548
|
+
const noFollow = this.options.noFollow ? fs.constants.O_NOFOLLOW : undefined;
|
|
549
|
+
return typeof noFollow === 'number' ? fs.constants.O_RDONLY | noFollow : 'r';
|
|
550
|
+
}
|
|
551
|
+
openSync() {
|
|
552
|
+
try {
|
|
553
|
+
return fs.openSync(this.options.path, this.openFlags());
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
throw this.refusedSymlink(error) ?? error;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
async openAsync() {
|
|
560
|
+
try {
|
|
561
|
+
return await fs.promises.open(this.options.path, this.openFlags());
|
|
562
|
+
}
|
|
563
|
+
catch (error) {
|
|
564
|
+
throw this.refusedSymlink(error) ?? error;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
/** The typed error for an open that `O_NOFOLLOW` refused because the path is a symlink. */
|
|
568
|
+
refusedSymlink(error) {
|
|
569
|
+
if (!this.options.noFollow)
|
|
570
|
+
return null;
|
|
571
|
+
const code = error?.code;
|
|
572
|
+
// ELOOP on Linux and macOS; EMLINK on FreeBSD.
|
|
573
|
+
if (code !== 'ELOOP' && code !== 'EMLINK')
|
|
574
|
+
return null;
|
|
575
|
+
try {
|
|
576
|
+
return fs.lstatSync(this.options.path).isSymbolicLink()
|
|
577
|
+
? new JsonlTailSymlinkError(this.options.path)
|
|
578
|
+
: null;
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
294
583
|
}
|
|
295
584
|
watchFile() {
|
|
585
|
+
if (this.options.noFollow) {
|
|
586
|
+
try {
|
|
587
|
+
// fs.watch follows links; a refused symlink is not watched either.
|
|
588
|
+
if (fs.lstatSync(this.options.path).isSymbolicLink())
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
// Missing: fs.watch below fails the same way and polling covers it.
|
|
593
|
+
}
|
|
594
|
+
}
|
|
296
595
|
try {
|
|
297
596
|
this.fsWatcher = fs.watch(this.options.path, { persistent: false }, () => {
|
|
298
597
|
this.debouncedRead();
|
|
@@ -314,13 +613,101 @@ class JsonlTailReader {
|
|
|
314
613
|
this.debounceTimer = setTimeout(() => this.readNow(), debounceMs);
|
|
315
614
|
}
|
|
316
615
|
}
|
|
616
|
+
/** The checkpoint when `value` has the documented shape, else `null`. */
|
|
617
|
+
function parseCheckpoint(value) {
|
|
618
|
+
if (!value || typeof value !== 'object')
|
|
619
|
+
return null;
|
|
620
|
+
const candidate = value;
|
|
621
|
+
if (candidate.version !== exports.JSONL_TAIL_CHECKPOINT_VERSION)
|
|
622
|
+
return null;
|
|
623
|
+
const { offset, identity, prefixDigest } = candidate;
|
|
624
|
+
if (typeof offset !== 'number' || !Number.isSafeInteger(offset) || offset < 0)
|
|
625
|
+
return null;
|
|
626
|
+
let parsedIdentity = null;
|
|
627
|
+
if (identity !== null) {
|
|
628
|
+
if (!identity || typeof identity !== 'object')
|
|
629
|
+
return null;
|
|
630
|
+
const { dev, ino } = identity;
|
|
631
|
+
if (!isFiniteNumber(dev) || !isFiniteNumber(ino))
|
|
632
|
+
return null;
|
|
633
|
+
parsedIdentity = { dev, ino };
|
|
634
|
+
}
|
|
635
|
+
let parsedDigest = null;
|
|
636
|
+
if (prefixDigest !== null) {
|
|
637
|
+
if (!prefixDigest || typeof prefixDigest !== 'object')
|
|
638
|
+
return null;
|
|
639
|
+
const { bytes, hash } = prefixDigest;
|
|
640
|
+
if (typeof bytes !== 'number' || !Number.isInteger(bytes) || bytes < 0 || bytes > fileHead_1.HEAD_BYTES) {
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
if (typeof hash !== 'string' || hash.length === 0)
|
|
644
|
+
return null;
|
|
645
|
+
parsedDigest = { bytes, hash };
|
|
646
|
+
}
|
|
647
|
+
return {
|
|
648
|
+
version: exports.JSONL_TAIL_CHECKPOINT_VERSION,
|
|
649
|
+
offset,
|
|
650
|
+
identity: parsedIdentity,
|
|
651
|
+
prefixDigest: parsedDigest,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
/** A fresh checkpoint for `offset` in the file with `identity` and recorded `head`. */
|
|
655
|
+
function checkpointOf(offset, identity, head) {
|
|
656
|
+
return {
|
|
657
|
+
version: exports.JSONL_TAIL_CHECKPOINT_VERSION,
|
|
658
|
+
offset,
|
|
659
|
+
identity: identity ? { dev: identity.dev, ino: identity.ino } : null,
|
|
660
|
+
prefixDigest: head ? { bytes: head.bytes, hash: head.hash } : null,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
/** Whether `checkpoint` still describes the open file. */
|
|
664
|
+
function checkpointMatches(checkpoint, fd, stat, prefix) {
|
|
665
|
+
const { offset, identity, prefixDigest } = checkpoint;
|
|
666
|
+
// Taken before the first read: nothing was consumed, so nothing can be stale.
|
|
667
|
+
if (identity === null && prefixDigest === null)
|
|
668
|
+
return offset === 0;
|
|
669
|
+
if (identity === null || prefixDigest === null)
|
|
670
|
+
return false;
|
|
671
|
+
if (identity.dev !== stat.dev || identity.ino !== stat.ino)
|
|
672
|
+
return false;
|
|
673
|
+
if (!(0, fileHead_1.headMatches)(prefixDigest, prefix))
|
|
674
|
+
return false;
|
|
675
|
+
if (offset > stat.size)
|
|
676
|
+
return false;
|
|
677
|
+
return offset === 0 || readAt(fd, offset - 1, 1)[0] === 0x0a;
|
|
678
|
+
}
|
|
679
|
+
function isFiniteNumber(value) {
|
|
680
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
681
|
+
}
|
|
317
682
|
function positiveInteger(value, fallback) {
|
|
318
683
|
return typeof value === 'number' && Number.isFinite(value) && value >= 1
|
|
319
684
|
? Math.floor(value)
|
|
320
685
|
: fallback;
|
|
321
686
|
}
|
|
687
|
+
function toError(error) {
|
|
688
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
689
|
+
}
|
|
690
|
+
function closeQuietly(fd) {
|
|
691
|
+
if (fd === null)
|
|
692
|
+
return;
|
|
693
|
+
try {
|
|
694
|
+
fs.closeSync(fd);
|
|
695
|
+
}
|
|
696
|
+
catch {
|
|
697
|
+
/* ignore close errors */
|
|
698
|
+
}
|
|
699
|
+
}
|
|
322
700
|
function readAt(fd, position, length) {
|
|
323
701
|
const buffer = Buffer.alloc(length);
|
|
324
702
|
const count = length > 0 ? fs.readSync(fd, buffer, 0, length, position) : 0;
|
|
325
703
|
return buffer.subarray(0, count);
|
|
326
704
|
}
|
|
705
|
+
/** The file's first `min(HEAD_BYTES, size)` bytes: what a checkpoint digests. */
|
|
706
|
+
function readPrefix(fd, stat) {
|
|
707
|
+
return readAt(fd, 0, Math.min(fileHead_1.HEAD_BYTES, stat.size));
|
|
708
|
+
}
|
|
709
|
+
async function readAtAsync(handle, position, length) {
|
|
710
|
+
const buffer = Buffer.alloc(length);
|
|
711
|
+
const count = length > 0 ? (await handle.read(buffer, 0, length, position)).bytesRead : 0;
|
|
712
|
+
return buffer.subarray(0, count);
|
|
713
|
+
}
|
|
@@ -4,22 +4,39 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { ProviderId } from '../providers/types';
|
|
6
6
|
import type { SessionWatcher, SessionWatcherCallbacks } from './types';
|
|
7
|
+
/** Options for {@link JsonlSessionWatcher}. */
|
|
8
|
+
export interface JsonlSessionWatcherOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Emit a `system` event (summary from the notification, `raw` the line) for
|
|
11
|
+
* each Claude Code mid-turn task-notification attachment. Default `false`.
|
|
12
|
+
*/
|
|
13
|
+
includeTaskNotifications?: boolean;
|
|
14
|
+
}
|
|
7
15
|
export declare class JsonlSessionWatcher implements SessionWatcher {
|
|
8
16
|
private readonly providerId;
|
|
9
17
|
private readonly sessionPath;
|
|
10
18
|
private readonly callbacks;
|
|
19
|
+
private readonly options;
|
|
11
20
|
private tail;
|
|
12
21
|
private startOffset;
|
|
13
22
|
private codexInPlanMode;
|
|
14
23
|
private readonly claudeUsageDeduper;
|
|
15
24
|
/** Last Codex cumulative usage seen; repeated `token_count` events carry the same value. */
|
|
16
25
|
private codexPrevTotal;
|
|
17
|
-
constructor(providerId: ProviderId, sessionPath: string, callbacks: SessionWatcherCallbacks);
|
|
26
|
+
constructor(providerId: ProviderId, sessionPath: string, callbacks: SessionWatcherCallbacks, options?: JsonlSessionWatcherOptions);
|
|
18
27
|
get isActive(): boolean;
|
|
19
28
|
/** Seek to a committed byte offset before starting replay. */
|
|
20
29
|
seekTo(position: number): void;
|
|
21
30
|
/** Position after the last complete JSONL line. */
|
|
22
31
|
getPosition(): number;
|
|
32
|
+
/**
|
|
33
|
+
* Parse state for {@link getPosition}: for Claude Code, the usage deduper's
|
|
34
|
+
* recent message ids (a `ClaudeUsageDedupeState`); for Codex, the previous
|
|
35
|
+
* cumulative token total and plan mode.
|
|
36
|
+
*/
|
|
37
|
+
getResumeState(): unknown;
|
|
38
|
+
/** Restores state from {@link getResumeState} after `seekTo()` and before `start()`. */
|
|
39
|
+
restoreResumeState(state: unknown): boolean;
|
|
23
40
|
start(replay: boolean): void;
|
|
24
41
|
stop(): void;
|
|
25
42
|
private handleRawEvent;
|