@bitkyc08/opencodex 2.40.0 → 2.42.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/gui/dist/assets/index-BU1tE0sr.js +112 -0
- package/gui/dist/assets/index-DL9-iS6J.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/meta.svg +1 -0
- package/package.json +4 -3
- package/src/adapters/cursor/catalog.ts +71 -29
- package/src/adapters/cursor/claude-id.ts +76 -0
- package/src/adapters/cursor/discovery.ts +16 -3
- package/src/adapters/cursor/effort-map.ts +27 -12
- package/src/adapters/cursor/protobuf-request.ts +41 -21
- package/src/adapters/google.ts +39 -2
- package/src/adapters/identity.ts +8 -2
- package/src/adapters/openai-responses.ts +57 -4
- package/src/bridge.ts +25 -3
- package/src/cli/account-auth.ts +28 -3
- package/src/cli/account-extended.ts +7 -1
- package/src/cli/capabilities.ts +2 -2
- package/src/cli/claude.ts +11 -2
- package/src/cli/connect.ts +7 -1
- package/src/cli/observe.ts +3 -1
- package/src/cli/registry.ts +1 -1
- package/src/cli/status.ts +19 -4
- package/src/client/connect.ts +5 -1
- package/src/client/hub-client.ts +29 -5
- package/src/clients/config-export.ts +12 -2
- package/src/codex/auth-api.ts +102 -9
- package/src/codex/catalog/aggregation.ts +8 -0
- package/src/codex/catalog/effort.ts +15 -2
- package/src/codex/catalog/metadata.ts +119 -9
- package/src/codex/catalog/native-models.ts +71 -0
- package/src/codex/catalog/parsing.ts +5 -3
- package/src/codex/catalog/provider-fetch.ts +166 -28
- package/src/codex/catalog.ts +1 -1
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/data/upstream-models.json +169 -0
- package/src/codex/desired-state.ts +18 -11
- package/src/codex/inject.ts +96 -6
- package/src/codex/injected-marker.ts +30 -4
- package/src/codex/journal.ts +14 -0
- package/src/combos/failover.ts +185 -6
- package/src/combos/index.ts +6 -0
- package/src/combos/resolve.ts +43 -6
- package/src/config.ts +5 -1
- package/src/generated/compatibility-version.json +115 -83
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/sync.ts +10 -2
- package/src/integrations/cursor-effort-table.ts +143 -0
- package/src/integrations/state.ts +1 -1
- package/src/integrations/writer.ts +2 -2
- package/src/lib/app-owned-memory-stores.ts +27 -8
- package/src/lib/bounded-body.ts +16 -1
- package/src/oauth/account-quota-rank.ts +40 -1
- package/src/oauth/chatgpt-device.ts +187 -0
- package/src/oauth/chatgpt.ts +31 -4
- package/src/oauth/generic-account-failover.ts +2 -2
- package/src/oauth/index.ts +24 -3
- package/src/oauth/log.ts +3 -0
- package/src/oauth/meta-muse.ts +235 -0
- package/src/providers/antigravity-models.ts +71 -13
- package/src/providers/command-code-efforts.ts +15 -0
- package/src/providers/free-directory.ts +4 -1
- package/src/providers/muse-subscription-usage.ts +95 -0
- package/src/providers/quota.ts +96 -0
- package/src/providers/registry.ts +116 -8
- package/src/responses/code-mode-helper-compat.ts +4 -1
- package/src/responses/state.ts +5 -4
- package/src/server/auth-cors.ts +241 -56
- package/src/server/chat-completions.ts +11 -2
- package/src/server/chat-native.ts +30 -4
- package/src/server/claude-messages.ts +17 -3
- package/src/server/effort-row.ts +131 -0
- package/src/server/index.ts +82 -45
- package/src/server/live.ts +18 -4
- package/src/server/management/api-key-rotation.ts +2 -1
- package/src/server/management/api-key-usage.ts +97 -43
- package/src/server/management/context.ts +3 -0
- package/src/server/management/cursor-integration-routes.ts +36 -7
- package/src/server/management/logs-usage-routes.ts +64 -87
- package/src/server/management/oauth-account-routes.ts +10 -3
- package/src/server/management/provider-routes.ts +218 -1
- package/src/server/management/route-registry.ts +1 -0
- package/src/server/management/usage-aggregate-cache.ts +464 -0
- package/src/server/management/usage-summary-cache.ts +4 -0
- package/src/server/models-capabilities.ts +60 -5
- package/src/server/responses/core.ts +95 -7
- package/src/server/responses/empty-completion-guard.ts +4 -0
- package/src/types/config.ts +10 -1
- package/src/types/request.ts +8 -0
- package/src/types/tools.ts +12 -9
- package/src/usage/expected-prices.ts +43 -7
- package/src/usage/ledger-scanner.ts +448 -0
- package/src/usage/log.ts +1 -1
- package/src/usage/summary.ts +915 -655
- package/src/web-search/index.ts +1 -1
- package/gui/dist/assets/index-BHe2rl_C.js +0 -112
- package/gui/dist/assets/index-CJSb3HPe.css +0 -1
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { closeSync, fstatSync, openSync, readSync } from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
currentUsageLogRevision,
|
|
5
|
+
normalizePersistedUsageRow,
|
|
6
|
+
usageLogIdentityKey,
|
|
7
|
+
usageLogPath,
|
|
8
|
+
usageLogRevisionKey,
|
|
9
|
+
type PersistedUsageEntry,
|
|
10
|
+
type UsageLogRevision,
|
|
11
|
+
} from "./log";
|
|
12
|
+
|
|
13
|
+
export const USAGE_LEDGER_READ_CHUNK_BYTES = 1024 * 1024;
|
|
14
|
+
// Normalized writer rows carry a <=16 KiB route trace and <=500-character captured
|
|
15
|
+
// upstream error, so 1 MiB leaves wide headroom even for an extreme multi-attempt row.
|
|
16
|
+
// Hand-edited rows can still exceed it; those are reported separately instead of making
|
|
17
|
+
// one unterminated line an unbounded allocation.
|
|
18
|
+
export const USAGE_LEDGER_MAX_LINE_BYTES = 1024 * 1024;
|
|
19
|
+
export const USAGE_LEDGER_BOUNDARY_DIGEST_BYTES = 64 * 1024;
|
|
20
|
+
|
|
21
|
+
export interface ScanUsageLedgerOptions {
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
onEntry: (entry: PersistedUsageEntry) => void;
|
|
24
|
+
/** Absolute LF boundary returned by a previous scan. Defaults to byte zero. */
|
|
25
|
+
startAtBytes?: number;
|
|
26
|
+
/** Stable path/dev/ino/birthtime identity; required when startAtBytes is nonzero. */
|
|
27
|
+
expectedIdentityKey?: string;
|
|
28
|
+
/** Trailing digest at the previous boundary; required when startAtBytes is nonzero. */
|
|
29
|
+
expectedProcessedThroughDigest?: string;
|
|
30
|
+
/** Test seam for forcing byte boundaries; production always uses the 1 MiB default. */
|
|
31
|
+
chunkBytes?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface UsageLedgerScanResult {
|
|
35
|
+
/** Revision whose EOF was captured when the scan opened the ledger. */
|
|
36
|
+
revision: UsageLogRevision | null;
|
|
37
|
+
parsedRows: number;
|
|
38
|
+
/** LF-complete malformed/schema-invalid rows plus a non-empty bounded torn suffix. */
|
|
39
|
+
invalidRows: number;
|
|
40
|
+
/** Rows skipped after exceeding USAGE_LEDGER_MAX_LINE_BYTES. */
|
|
41
|
+
oversizedRows: number;
|
|
42
|
+
bytesRead: number;
|
|
43
|
+
/** Absolute byte offset immediately after the last handled LF. */
|
|
44
|
+
processedThroughBytes: number;
|
|
45
|
+
/** SHA-256 over at most the last 64 KiB ending at processedThroughBytes. */
|
|
46
|
+
processedThroughDigest: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type UsageLedgerRebuildReason =
|
|
50
|
+
| "identity_mismatch"
|
|
51
|
+
| "shrink"
|
|
52
|
+
| "boundary_mismatch"
|
|
53
|
+
| "content_changed";
|
|
54
|
+
|
|
55
|
+
export class UsageLedgerRebuildRequiredError extends Error {
|
|
56
|
+
readonly code = "usage_ledger_rebuild_required";
|
|
57
|
+
|
|
58
|
+
constructor(readonly reason: UsageLedgerRebuildReason) {
|
|
59
|
+
super(`usage ledger rebuild required: ${reason}`);
|
|
60
|
+
this.name = "UsageLedgerRebuildRequiredError";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function revisionFromStat(
|
|
65
|
+
path: string,
|
|
66
|
+
stat: ReturnType<typeof fstatSync>,
|
|
67
|
+
): UsageLogRevision {
|
|
68
|
+
if (!stat.isFile()) throw new Error("usage log is not a regular file");
|
|
69
|
+
return {
|
|
70
|
+
path,
|
|
71
|
+
dev: Number(stat.dev),
|
|
72
|
+
ino: Number(stat.ino),
|
|
73
|
+
birthtimeMs: Number(stat.birthtimeMs),
|
|
74
|
+
size: Number(stat.size),
|
|
75
|
+
mtimeMs: Number(stat.mtimeMs),
|
|
76
|
+
ctimeMs: Number(stat.ctimeMs),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function throwIfAborted(signal: AbortSignal | undefined): void {
|
|
81
|
+
if (!signal?.aborted) return;
|
|
82
|
+
throw signal.reason ?? new Error("usage ledger scan aborted");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isMissingFileError(error: unknown): boolean {
|
|
86
|
+
return error !== null
|
|
87
|
+
&& typeof error === "object"
|
|
88
|
+
&& "code" in error
|
|
89
|
+
&& error.code === "ENOENT";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isJsonWhitespace(bytes: Buffer, length: number): boolean {
|
|
93
|
+
for (let index = 0; index < length; index += 1) {
|
|
94
|
+
const byte = bytes[index];
|
|
95
|
+
if (byte !== 0x20 && byte !== 0x09 && byte !== 0x0d) return false;
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
class RollingByteWindow {
|
|
101
|
+
private readonly bytes: Buffer;
|
|
102
|
+
private start = 0;
|
|
103
|
+
private length = 0;
|
|
104
|
+
|
|
105
|
+
constructor(private readonly capacity: number) {
|
|
106
|
+
this.bytes = Buffer.allocUnsafe(capacity);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
append(source: Buffer, from = 0, to = source.byteLength): void {
|
|
110
|
+
const sourceLength = to - from;
|
|
111
|
+
if (sourceLength <= 0) return;
|
|
112
|
+
if (sourceLength >= this.capacity) {
|
|
113
|
+
source.copy(this.bytes, 0, to - this.capacity, to);
|
|
114
|
+
this.start = 0;
|
|
115
|
+
this.length = this.capacity;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const overflow = Math.max(0, this.length + sourceLength - this.capacity);
|
|
120
|
+
this.start = (this.start + overflow) % this.capacity;
|
|
121
|
+
this.length -= overflow;
|
|
122
|
+
const writeAt = (this.start + this.length) % this.capacity;
|
|
123
|
+
const firstLength = Math.min(sourceLength, this.capacity - writeAt);
|
|
124
|
+
source.copy(this.bytes, writeAt, from, from + firstLength);
|
|
125
|
+
if (firstLength < sourceLength) {
|
|
126
|
+
source.copy(this.bytes, 0, from + firstLength, to);
|
|
127
|
+
}
|
|
128
|
+
this.length += sourceLength;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
appendByte(byte: number): void {
|
|
132
|
+
if (this.length < this.capacity) {
|
|
133
|
+
this.bytes[(this.start + this.length) % this.capacity] = byte;
|
|
134
|
+
this.length += 1;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
this.bytes[this.start] = byte;
|
|
138
|
+
this.start = (this.start + 1) % this.capacity;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
appendWindow(source: RollingByteWindow): void {
|
|
142
|
+
if (source.length === 0) return;
|
|
143
|
+
const firstLength = Math.min(source.length, source.capacity - source.start);
|
|
144
|
+
this.append(source.bytes, source.start, source.start + firstLength);
|
|
145
|
+
if (firstLength < source.length) {
|
|
146
|
+
this.append(source.bytes, 0, source.length - firstLength);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
reset(): void {
|
|
151
|
+
this.start = 0;
|
|
152
|
+
this.length = 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
digest(): string {
|
|
156
|
+
const hash = createHash("sha256");
|
|
157
|
+
if (this.length === 0) return hash.digest("hex");
|
|
158
|
+
const firstLength = Math.min(this.length, this.capacity - this.start);
|
|
159
|
+
hash.update(this.bytes.subarray(this.start, this.start + firstLength));
|
|
160
|
+
if (firstLength < this.length) {
|
|
161
|
+
hash.update(this.bytes.subarray(0, this.length - firstLength));
|
|
162
|
+
}
|
|
163
|
+
return hash.digest("hex");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function rebuildRequired(reason: UsageLedgerRebuildReason): UsageLedgerRebuildRequiredError {
|
|
168
|
+
return new UsageLedgerRebuildRequiredError(reason);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function captureRangeIntoWindow(
|
|
172
|
+
fd: number,
|
|
173
|
+
from: number,
|
|
174
|
+
to: number,
|
|
175
|
+
scratch: Buffer,
|
|
176
|
+
window: RollingByteWindow,
|
|
177
|
+
signal: AbortSignal | undefined,
|
|
178
|
+
): void {
|
|
179
|
+
for (let position = from; position < to;) {
|
|
180
|
+
throwIfAborted(signal);
|
|
181
|
+
const requested = Math.min(scratch.byteLength, to - position);
|
|
182
|
+
const read = readSync(fd, scratch, 0, requested, position);
|
|
183
|
+
if (read === 0) throw rebuildRequired("shrink");
|
|
184
|
+
window.append(scratch, 0, read);
|
|
185
|
+
position += read;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function digestRangeCooperatively(
|
|
190
|
+
fd: number,
|
|
191
|
+
from: number,
|
|
192
|
+
to: number,
|
|
193
|
+
buffer: Buffer,
|
|
194
|
+
signal: AbortSignal | undefined,
|
|
195
|
+
): Promise<string> {
|
|
196
|
+
const hash = createHash("sha256");
|
|
197
|
+
for (let position = from; position < to;) {
|
|
198
|
+
throwIfAborted(signal);
|
|
199
|
+
const requested = Math.min(buffer.byteLength, to - position);
|
|
200
|
+
const read = readSync(fd, buffer, 0, requested, position);
|
|
201
|
+
if (read === 0) throw rebuildRequired("shrink");
|
|
202
|
+
hash.update(buffer.subarray(0, read));
|
|
203
|
+
position += read;
|
|
204
|
+
if (position < to) await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
205
|
+
}
|
|
206
|
+
return hash.digest("hex");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Cooperatively scans a full ledger or validated append suffix with bounded memory.
|
|
211
|
+
*
|
|
212
|
+
* The opened EOF is the snapshot boundary: bytes appended after the initial fstat are
|
|
213
|
+
* deliberately left for the next scan. Rows are framed as raw bytes before UTF-8 decoding,
|
|
214
|
+
* so a multi-byte character may safely cross any read boundary. Only LF-terminated rows are
|
|
215
|
+
* published; a torn final write is skipped rather than accepted prematurely.
|
|
216
|
+
*/
|
|
217
|
+
export async function scanUsageLedgerCooperatively(
|
|
218
|
+
options: ScanUsageLedgerOptions,
|
|
219
|
+
): Promise<UsageLedgerScanResult> {
|
|
220
|
+
const chunkBytes = options.chunkBytes ?? USAGE_LEDGER_READ_CHUNK_BYTES;
|
|
221
|
+
if (!Number.isSafeInteger(chunkBytes) || chunkBytes <= 0 || chunkBytes > USAGE_LEDGER_READ_CHUNK_BYTES) {
|
|
222
|
+
throw new RangeError(`usage ledger chunk bytes must be between 1 and ${USAGE_LEDGER_READ_CHUNK_BYTES}`);
|
|
223
|
+
}
|
|
224
|
+
const startAtBytes = options.startAtBytes ?? 0;
|
|
225
|
+
if (!Number.isSafeInteger(startAtBytes) || startAtBytes < 0) {
|
|
226
|
+
throw new RangeError("usage ledger start offset must be a non-negative safe integer");
|
|
227
|
+
}
|
|
228
|
+
if (startAtBytes > 0
|
|
229
|
+
&& (options.expectedIdentityKey === undefined
|
|
230
|
+
|| options.expectedProcessedThroughDigest === undefined)) {
|
|
231
|
+
throw new TypeError(
|
|
232
|
+
"usage ledger append scan requires expectedIdentityKey and expectedProcessedThroughDigest",
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
throwIfAborted(options.signal);
|
|
236
|
+
|
|
237
|
+
const path = usageLogPath();
|
|
238
|
+
let fd: number;
|
|
239
|
+
try {
|
|
240
|
+
fd = openSync(path, "r");
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (isMissingFileError(error)) {
|
|
243
|
+
if (startAtBytes > 0
|
|
244
|
+
|| (options.expectedIdentityKey
|
|
245
|
+
&& options.expectedIdentityKey !== usageLogIdentityKey(null))) {
|
|
246
|
+
throw rebuildRequired("identity_mismatch");
|
|
247
|
+
}
|
|
248
|
+
return {
|
|
249
|
+
revision: null,
|
|
250
|
+
parsedRows: 0,
|
|
251
|
+
invalidRows: 0,
|
|
252
|
+
oversizedRows: 0,
|
|
253
|
+
bytesRead: 0,
|
|
254
|
+
processedThroughBytes: 0,
|
|
255
|
+
processedThroughDigest: createHash("sha256").digest("hex"),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
262
|
+
const chunk = Buffer.allocUnsafe(chunkBytes);
|
|
263
|
+
const line = Buffer.allocUnsafe(USAGE_LEDGER_MAX_LINE_BYTES);
|
|
264
|
+
let lineLength = 0;
|
|
265
|
+
let droppingOversizedLine = false;
|
|
266
|
+
let parsedRows = 0;
|
|
267
|
+
let invalidRows = 0;
|
|
268
|
+
let oversizedRows = 0;
|
|
269
|
+
let bytesRead = 0;
|
|
270
|
+
let bytesSinceYield = 0;
|
|
271
|
+
let linesSinceYield = 0;
|
|
272
|
+
let processedThroughBytes = startAtBytes;
|
|
273
|
+
const capturedHash = createHash("sha256");
|
|
274
|
+
const committedTail = new RollingByteWindow(USAGE_LEDGER_BOUNDARY_DIGEST_BYTES);
|
|
275
|
+
const pendingTail = new RollingByteWindow(USAGE_LEDGER_BOUNDARY_DIGEST_BYTES);
|
|
276
|
+
let pendingLineBytes = 0;
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const openedRevision = revisionFromStat(path, fstatSync(fd));
|
|
280
|
+
const scanEnd = openedRevision.size;
|
|
281
|
+
const openedIdentityKey = usageLogIdentityKey(openedRevision);
|
|
282
|
+
if (options.expectedIdentityKey && options.expectedIdentityKey !== openedIdentityKey) {
|
|
283
|
+
throw rebuildRequired("identity_mismatch");
|
|
284
|
+
}
|
|
285
|
+
if (scanEnd < startAtBytes) throw rebuildRequired("shrink");
|
|
286
|
+
if (startAtBytes > 0) {
|
|
287
|
+
const preceding = Buffer.allocUnsafe(1);
|
|
288
|
+
const read = readSync(fd, preceding, 0, 1, startAtBytes - 1);
|
|
289
|
+
if (read !== 1) throw rebuildRequired("shrink");
|
|
290
|
+
if (preceding[0] !== 0x0a) throw rebuildRequired("boundary_mismatch");
|
|
291
|
+
}
|
|
292
|
+
if (options.expectedProcessedThroughDigest !== undefined) {
|
|
293
|
+
captureRangeIntoWindow(
|
|
294
|
+
fd,
|
|
295
|
+
Math.max(0, startAtBytes - USAGE_LEDGER_BOUNDARY_DIGEST_BYTES),
|
|
296
|
+
startAtBytes,
|
|
297
|
+
line,
|
|
298
|
+
committedTail,
|
|
299
|
+
options.signal,
|
|
300
|
+
);
|
|
301
|
+
if (committedTail.digest() !== options.expectedProcessedThroughDigest) {
|
|
302
|
+
throw rebuildRequired("content_changed");
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const publishLine = (): void => {
|
|
307
|
+
if (lineLength === 0 || isJsonWhitespace(line, lineLength)) return;
|
|
308
|
+
let entry: PersistedUsageEntry | undefined;
|
|
309
|
+
try {
|
|
310
|
+
const text = decoder.decode(line.subarray(0, lineLength));
|
|
311
|
+
entry = normalizePersistedUsageRow(JSON.parse(text));
|
|
312
|
+
} catch {
|
|
313
|
+
invalidRows += 1;
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (!entry) {
|
|
317
|
+
invalidRows += 1;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
options.onEntry(entry);
|
|
321
|
+
parsedRows += 1;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
for (let position = startAtBytes; position < scanEnd;) {
|
|
325
|
+
throwIfAborted(options.signal);
|
|
326
|
+
const chunkStart = position;
|
|
327
|
+
const requested = Math.min(chunk.byteLength, scanEnd - position);
|
|
328
|
+
const read = readSync(fd, chunk, 0, requested, position);
|
|
329
|
+
if (read === 0) throw rebuildRequired("shrink");
|
|
330
|
+
position += read;
|
|
331
|
+
bytesRead += read;
|
|
332
|
+
bytesSinceYield += read;
|
|
333
|
+
capturedHash.update(chunk.subarray(0, read));
|
|
334
|
+
|
|
335
|
+
let cursor = 0;
|
|
336
|
+
while (cursor < read) {
|
|
337
|
+
const newline = chunk.indexOf(0x0a, cursor);
|
|
338
|
+
const segmentEnd = newline >= 0 && newline < read ? newline : read;
|
|
339
|
+
const segmentLength = segmentEnd - cursor;
|
|
340
|
+
|
|
341
|
+
if (segmentLength > 0) {
|
|
342
|
+
pendingTail.append(chunk, cursor, segmentEnd);
|
|
343
|
+
pendingLineBytes = Math.min(
|
|
344
|
+
USAGE_LEDGER_BOUNDARY_DIGEST_BYTES,
|
|
345
|
+
pendingLineBytes + segmentLength,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (!droppingOversizedLine) {
|
|
350
|
+
if (lineLength + segmentLength > USAGE_LEDGER_MAX_LINE_BYTES) {
|
|
351
|
+
oversizedRows += 1;
|
|
352
|
+
droppingOversizedLine = true;
|
|
353
|
+
lineLength = 0;
|
|
354
|
+
} else if (segmentLength > 0) {
|
|
355
|
+
chunk.copy(line, lineLength, cursor, segmentEnd);
|
|
356
|
+
lineLength += segmentLength;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (newline < 0 || newline >= read) break;
|
|
361
|
+
linesSinceYield += 1;
|
|
362
|
+
processedThroughBytes = chunkStart + newline + 1;
|
|
363
|
+
if (pendingLineBytes + 1 >= USAGE_LEDGER_BOUNDARY_DIGEST_BYTES) {
|
|
364
|
+
committedTail.reset();
|
|
365
|
+
}
|
|
366
|
+
committedTail.appendWindow(pendingTail);
|
|
367
|
+
committedTail.appendByte(0x0a);
|
|
368
|
+
pendingTail.reset();
|
|
369
|
+
pendingLineBytes = 0;
|
|
370
|
+
if (droppingOversizedLine) {
|
|
371
|
+
droppingOversizedLine = false;
|
|
372
|
+
} else {
|
|
373
|
+
publishLine();
|
|
374
|
+
}
|
|
375
|
+
lineLength = 0;
|
|
376
|
+
cursor = newline + 1;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (position < scanEnd
|
|
380
|
+
&& (bytesSinceYield >= USAGE_LEDGER_READ_CHUNK_BYTES || linesSinceYield >= 1_000)) {
|
|
381
|
+
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
|
382
|
+
bytesSinceYield = 0;
|
|
383
|
+
linesSinceYield = 0;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// A non-empty suffix without LF is not a committed JSONL row, even when it happens
|
|
388
|
+
// to contain valid JSON. Count it as invalid and leave it out of the aggregate.
|
|
389
|
+
if (!droppingOversizedLine && lineLength > 0 && !isJsonWhitespace(line, lineLength)) {
|
|
390
|
+
invalidRows += 1;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
throwIfAborted(options.signal);
|
|
394
|
+
const endingRevision = revisionFromStat(path, fstatSync(fd));
|
|
395
|
+
if (usageLogIdentityKey(endingRevision) !== openedIdentityKey) {
|
|
396
|
+
throw rebuildRequired("identity_mismatch");
|
|
397
|
+
}
|
|
398
|
+
if (endingRevision.size < scanEnd) throw rebuildRequired("shrink");
|
|
399
|
+
const pathRevision = currentUsageLogRevision();
|
|
400
|
+
if (!pathRevision || usageLogIdentityKey(pathRevision) !== openedIdentityKey) {
|
|
401
|
+
throw rebuildRequired("identity_mismatch");
|
|
402
|
+
}
|
|
403
|
+
if (pathRevision.size < scanEnd) throw rebuildRequired("shrink");
|
|
404
|
+
|
|
405
|
+
// A pure append changes size/mtime/ctime but leaves the captured prefix intact and
|
|
406
|
+
// is safe to ignore until the next scan. Re-read only that prefix when a mutation
|
|
407
|
+
// was observed, so a same-inode rewrite (including rewrite + growth) cannot publish
|
|
408
|
+
// a mixture of old and new rows after a cooperative yield.
|
|
409
|
+
const mutationObserved = usageLogRevisionKey(endingRevision) !== usageLogRevisionKey(openedRevision)
|
|
410
|
+
|| usageLogRevisionKey(pathRevision) !== usageLogRevisionKey(openedRevision);
|
|
411
|
+
if (mutationObserved) {
|
|
412
|
+
const capturedDigest = capturedHash.digest("hex");
|
|
413
|
+
const verifiedDigest = await digestRangeCooperatively(
|
|
414
|
+
fd,
|
|
415
|
+
startAtBytes,
|
|
416
|
+
scanEnd,
|
|
417
|
+
line,
|
|
418
|
+
options.signal,
|
|
419
|
+
);
|
|
420
|
+
const verifiedFdRevision = revisionFromStat(path, fstatSync(fd));
|
|
421
|
+
const verifiedPathRevision = currentUsageLogRevision();
|
|
422
|
+
if (capturedDigest !== verifiedDigest) throw rebuildRequired("content_changed");
|
|
423
|
+
if (!verifiedPathRevision
|
|
424
|
+
|| usageLogIdentityKey(verifiedFdRevision) !== openedIdentityKey
|
|
425
|
+
|| usageLogIdentityKey(verifiedPathRevision) !== openedIdentityKey) {
|
|
426
|
+
throw rebuildRequired("identity_mismatch");
|
|
427
|
+
}
|
|
428
|
+
if (verifiedFdRevision.size < scanEnd || verifiedPathRevision.size < scanEnd) {
|
|
429
|
+
throw rebuildRequired("shrink");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
throwIfAborted(options.signal);
|
|
434
|
+
const processedThroughDigest = committedTail.digest();
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
revision: openedRevision,
|
|
438
|
+
parsedRows,
|
|
439
|
+
invalidRows,
|
|
440
|
+
oversizedRows,
|
|
441
|
+
bytesRead,
|
|
442
|
+
processedThroughBytes,
|
|
443
|
+
processedThroughDigest,
|
|
444
|
+
};
|
|
445
|
+
} finally {
|
|
446
|
+
closeSync(fd);
|
|
447
|
+
}
|
|
448
|
+
}
|
package/src/usage/log.ts
CHANGED
|
@@ -1190,7 +1190,7 @@ export async function readUsageEntriesForManagement(): Promise<PersistedUsageEnt
|
|
|
1190
1190
|
}
|
|
1191
1191
|
|
|
1192
1192
|
/** Keep legacy optional fields permissive, but reject rows that cannot be safely attributed. */
|
|
1193
|
-
function normalizePersistedUsageRow(value: unknown): PersistedUsageEntry | undefined {
|
|
1193
|
+
export function normalizePersistedUsageRow(value: unknown): PersistedUsageEntry | undefined {
|
|
1194
1194
|
if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
1195
1195
|
const row = value as Record<string, unknown>;
|
|
1196
1196
|
if (typeof row.requestId !== "string" || typeof row.provider !== "string") return undefined;
|