@neta-art/cohub-cli 10.7.0 → 10.8.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/README.md +4 -4
- package/dist/board-kernel.js +276 -214
- package/dist/commands/boards/examples.js +16 -16
- package/dist/commands/boards.js +1 -1
- package/dist/commands/runtime.js +3 -14
- package/dist/runtime/connection.js +161 -54
- package/dist/runtime/harnesses/chained-transcript.d.ts +24 -0
- package/dist/runtime/harnesses/chained-transcript.js +62 -0
- package/dist/runtime/harnesses/claude-transcript.d.ts +8 -0
- package/dist/runtime/harnesses/claude-transcript.js +138 -0
- package/dist/runtime/harnesses/claude.js +20 -2
- package/dist/runtime/harnesses/codex.js +1 -1
- package/dist/runtime/harnesses/cursor-transcript.d.ts +37 -0
- package/dist/runtime/harnesses/cursor-transcript.js +183 -0
- package/dist/runtime/harnesses/cursor.d.ts +24 -0
- package/dist/runtime/harnesses/cursor.js +620 -0
- package/dist/runtime/harnesses/index.d.ts +1 -3
- package/dist/runtime/harnesses/index.js +4 -10
- package/dist/runtime/harnesses/pi.js +1 -1
- package/dist/runtime/harnesses/stream-json.d.ts +5 -1
- package/dist/runtime/harnesses/stream-json.js +31 -9
- package/dist/runtime/harnesses/types.d.ts +10 -5
- package/dist/runtime/harnesses/workbuddy-transcript.d.ts +8 -0
- package/dist/runtime/harnesses/workbuddy-transcript.js +127 -0
- package/dist/runtime/harnesses/workbuddy.d.ts +1 -1
- package/dist/runtime/harnesses/workbuddy.js +31 -7
- package/dist/runtime/json-rpc.d.ts +2 -0
- package/dist/runtime/json-rpc.js +9 -0
- package/dist/runtime/native/attach.d.ts +1 -1
- package/dist/runtime/native/attach.js +1 -4
- package/dist/runtime/native/execution.js +14 -3
- package/dist/runtime/native/ingest.d.ts +24 -2
- package/dist/runtime/native/ingest.js +76 -10
- package/dist/runtime/native/sessions.js +6 -1
- package/dist/runtime/native/transcript.d.ts +23 -0
- package/dist/runtime/native/transcript.js +40 -5
- package/package.json +3 -3
|
@@ -2,8 +2,9 @@ import { appendFile, mkdir, open, readFile, readdir, rename, stat } from "node:f
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { NATIVE_INGEST_MAX_TURNS, SETTLED_TURN_STATUSES } from "@neta-art/cohub";
|
|
4
4
|
import { acceptsDirectory, acceptsTranscript, readSessionHeader, readTranscript, transcriptRoots } from "./adapters.js";
|
|
5
|
+
import { SizeLimitError } from "./transcript.js";
|
|
5
6
|
import { nativeTurnId } from "./identity.js";
|
|
6
|
-
import { transcriptFormat } from "../harnesses/index.js";
|
|
7
|
+
import { harnessAdapter, transcriptFormat } from "../harnesses/index.js";
|
|
7
8
|
import { containsNativePath } from "./paths.js";
|
|
8
9
|
const missing = (error) => error?.code === "ENOENT";
|
|
9
10
|
/** Transcripts read in parallel per lane. The live lane never waits behind a backfill. */
|
|
@@ -27,6 +28,7 @@ const RETRY_MAX_MS = 5 * 60_000;
|
|
|
27
28
|
*/
|
|
28
29
|
const INGEST_BATCH_TURNS = Math.min(50, NATIVE_INGEST_MAX_TURNS);
|
|
29
30
|
const IMPORT_FAILURES_KEPT = 100;
|
|
31
|
+
const fileVersion = (info) => `${info.ino}:${info.size}:${info.mtimeMs}`;
|
|
30
32
|
/** Headers are read this many at a time; each costs one small read. */
|
|
31
33
|
const HEADER_CONCURRENCY = 16;
|
|
32
34
|
/** Whether a session in `cwd` belongs to the directory bound at `root`. */
|
|
@@ -105,8 +107,12 @@ export class NativeIngest {
|
|
|
105
107
|
reads = new Set();
|
|
106
108
|
timers = new Map();
|
|
107
109
|
failures = new Map();
|
|
110
|
+
/** File versions that hit a size limit; they are not read again until the file changes. */
|
|
111
|
+
oversized = new Map();
|
|
108
112
|
aliveAt = new Map();
|
|
109
113
|
runningPaths = new Set();
|
|
114
|
+
/** Files a live Cohub execution is writing in a format that cannot mark its Turn. */
|
|
115
|
+
executing = new Set();
|
|
110
116
|
writing = Promise.resolve();
|
|
111
117
|
foreign = new Set();
|
|
112
118
|
importQueue = [];
|
|
@@ -194,18 +200,49 @@ export class NativeIngest {
|
|
|
194
200
|
}
|
|
195
201
|
/**
|
|
196
202
|
* Record that Cohub created or chose this transcript for a Session, before any Turn of it runs.
|
|
203
|
+
* `through` says the server has the file's Turns up to `turnKey` (none when null) and `turnId` heads them.
|
|
197
204
|
*/
|
|
198
205
|
adopt(session, sessionId, through) {
|
|
199
206
|
const state = this.states.get(session.path) ?? this.blank(session.path, session.nativeSessionId, session.cwd, session.harness, "user", { size: -1, mtimeMs: 0, ino: 0 });
|
|
200
207
|
Object.assign(state, { sessionId, nativeSessionId: session.nativeSessionId });
|
|
201
|
-
if (through
|
|
208
|
+
if (through)
|
|
202
209
|
Object.assign(state, { throughTurnKey: through.turnKey, throughTurnId: through.turnId, headTurnId: through.turnId });
|
|
203
210
|
if (!this.states.has(session.path))
|
|
204
211
|
state.origin = "cohub";
|
|
212
|
+
// The harness wrote elsewhere than expected; the execution follows the file.
|
|
213
|
+
for (const other of this.states.values()) {
|
|
214
|
+
if (other === state || other.harness !== session.harness || other.nativeSessionId !== session.nativeSessionId || !other.executingTurnId)
|
|
215
|
+
continue;
|
|
216
|
+
state.executingTurnId = other.executingTurnId;
|
|
217
|
+
other.executingTurnId = null;
|
|
218
|
+
if (this.executing.delete(other.path))
|
|
219
|
+
this.executing.add(session.path);
|
|
220
|
+
this.markDirty(other.path);
|
|
221
|
+
}
|
|
205
222
|
this.states.set(session.path, state);
|
|
206
223
|
this.markDirty(session.path);
|
|
207
224
|
}
|
|
225
|
+
/** A Cohub Turn starts writing this file; a format that cannot mark it is not read until it ends. */
|
|
226
|
+
begin(session, sessionId, turnId) {
|
|
227
|
+
if (transcriptFormat(session.harness).marksCohubTurns)
|
|
228
|
+
return;
|
|
229
|
+
this.adopt(session, sessionId);
|
|
230
|
+
const state = this.states.get(session.path);
|
|
231
|
+
state.executingTurnId = turnId;
|
|
232
|
+
this.executing.add(session.path);
|
|
233
|
+
this.markDirty(session.path);
|
|
234
|
+
}
|
|
235
|
+
/** A Cohub Turn ended, whether or not it produced a result. */
|
|
236
|
+
end(turnId) {
|
|
237
|
+
for (const state of this.states.values()) {
|
|
238
|
+
if (state.executingTurnId !== turnId)
|
|
239
|
+
continue;
|
|
240
|
+
this.executing.delete(state.path);
|
|
241
|
+
this.touch(state.path);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
208
244
|
advance(path, turnId) {
|
|
245
|
+
this.end(turnId);
|
|
209
246
|
const state = this.states.get(path);
|
|
210
247
|
if (!state)
|
|
211
248
|
return;
|
|
@@ -287,21 +324,23 @@ export class NativeIngest {
|
|
|
287
324
|
}
|
|
288
325
|
this.drainLive();
|
|
289
326
|
if (this.job.state === "running")
|
|
290
|
-
await this.startImport(this.job.filter, this.job.concurrency);
|
|
327
|
+
await this.startImport(this.job.filter, this.job.concurrency, { resume: true });
|
|
291
328
|
}
|
|
292
|
-
|
|
329
|
+
/** `resume` is automatic resumption: it skips file versions that already hit a size limit, which only an explicit import retries. */
|
|
330
|
+
async plan(filter = {}, signal, options = {}) {
|
|
293
331
|
const harnesses = this.harnesses.filter((harness) => !filter.harnesses?.length || filter.harnesses.includes(harness));
|
|
294
332
|
const candidates = await discoverTranscripts(this.options.cwd, harnesses, { headers: true, nested: this.nested, signal });
|
|
295
333
|
// A transcript whose first read failed is still untracked (size -1), so a later import retries it.
|
|
296
334
|
return candidates.filter((candidate) => (this.states.get(candidate.path)?.size ?? -1) < 0 && !this.live.has(candidate.path)
|
|
335
|
+
&& !(options.resume && this.oversized.get(candidate.path) === fileVersion(candidate))
|
|
297
336
|
&& (!filter.nativeSessionId || candidate.nativeSessionId === filter.nativeSessionId));
|
|
298
337
|
}
|
|
299
338
|
/**
|
|
300
339
|
* Start or resume a backfill. What was imported is tracked, so resuming simply plans again; the
|
|
301
340
|
* server recognizes any Turn it already has, so an interrupted batch is never duplicated.
|
|
302
341
|
*/
|
|
303
|
-
async startImport(filter = {}, concurrency = DEFAULT_IMPORT_CONCURRENCY) {
|
|
304
|
-
const queue = await this.plan(filter);
|
|
342
|
+
async startImport(filter = {}, concurrency = DEFAULT_IMPORT_CONCURRENCY, options = {}) {
|
|
343
|
+
const queue = await this.plan(filter, undefined, options);
|
|
305
344
|
const resuming = this.job.state === "running" || this.job.state === "paused";
|
|
306
345
|
const same = resuming && JSON.stringify(this.job.filter) === JSON.stringify(filter);
|
|
307
346
|
const base = same ? this.job : { ...this.job, done: 0, turns: 0, bytes: 0, failed: [], startedAt: new Date().toISOString() };
|
|
@@ -417,7 +456,7 @@ export class NativeIngest {
|
|
|
417
456
|
}
|
|
418
457
|
retryLater(path, error) {
|
|
419
458
|
this.report(path, error);
|
|
420
|
-
if (this.stopped || !this.transport)
|
|
459
|
+
if (this.stopped || !this.transport || error instanceof SizeLimitError)
|
|
421
460
|
return;
|
|
422
461
|
const attempts = (this.failures.get(path) ?? 0) + 1;
|
|
423
462
|
if (attempts > RETRY_ATTEMPTS) {
|
|
@@ -469,12 +508,26 @@ export class NativeIngest {
|
|
|
469
508
|
}
|
|
470
509
|
}
|
|
471
510
|
async read(path, lane, force) {
|
|
472
|
-
if (this.foreign.has(path))
|
|
511
|
+
if (this.foreign.has(path) || this.executing.has(path))
|
|
473
512
|
return;
|
|
474
513
|
const info = await stat(path).catch((error) => { if (missing(error))
|
|
475
514
|
return null; throw error; });
|
|
476
515
|
if (!info?.isFile())
|
|
477
516
|
return;
|
|
517
|
+
const version = fileVersion(info);
|
|
518
|
+
if (lane === "live" && this.oversized.get(path) === version)
|
|
519
|
+
return;
|
|
520
|
+
this.oversized.delete(path);
|
|
521
|
+
try {
|
|
522
|
+
await this.readVersion(path, lane, force, info);
|
|
523
|
+
}
|
|
524
|
+
catch (error) {
|
|
525
|
+
if (error instanceof SizeLimitError)
|
|
526
|
+
this.oversized.set(path, version);
|
|
527
|
+
throw error;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
async readVersion(path, lane, force, info) {
|
|
478
531
|
const previous = this.states.get(path);
|
|
479
532
|
// Unchanged bytes can still hold a Turn whose end the control plane has only now reported.
|
|
480
533
|
if (previous && this.unchanged(previous, info) && !(force && previous.runningTurnId))
|
|
@@ -565,6 +618,8 @@ export class NativeIngest {
|
|
|
565
618
|
const certain = this.options.hooks?.settled?.(state, mtimeMs);
|
|
566
619
|
if (certain != null)
|
|
567
620
|
return certain;
|
|
621
|
+
if (tail.ended)
|
|
622
|
+
return true;
|
|
568
623
|
if (!format.replyEnded?.(tail))
|
|
569
624
|
return false;
|
|
570
625
|
const idleMs = Date.now() - mtimeMs;
|
|
@@ -598,9 +653,20 @@ export class NativeIngest {
|
|
|
598
653
|
const known = pending.length ? await this.known(transport, pending.flatMap((turn) => turn.cloudTurnId ? [turn.cloudTurnId, derived(turn)] : [derived(turn)])) : new Map();
|
|
599
654
|
const cohub = (turn) => turn.cloudTurnId && known.has(turn.cloudTurnId) ? turn.cloudTurnId : null;
|
|
600
655
|
const tail = turns.at(-1);
|
|
656
|
+
// A Cohub file whose latest Cohub Turn the file cannot name (its format marks none, or it predates
|
|
657
|
+
// the marker) holds only Turns the server already has, up to its current end.
|
|
658
|
+
const cohubHead = state.executingTurnId ?? state.headTurnId;
|
|
659
|
+
const unmarked = !transcriptFormat(state.harness).marksCohubTurns || state.size < 0;
|
|
660
|
+
if (state.origin === "cohub" && cohubHead && unmarked && state.throughTurnId !== cohubHead && !turns.some((turn) => turn.cloudTurnId === cohubHead)) {
|
|
661
|
+
const through = tail ? cohub(tail) ?? cohubHead : cohubHead;
|
|
662
|
+
Object.assign(state, { throughTurnKey: tail?.key ?? state.throughTurnKey, throughTurnId: through, headTurnId: through, executingTurnId: null });
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
state.executingTurnId = null;
|
|
601
666
|
const head = !tail ? null : pending.length ? cohub(tail) ?? derived(tail) : state.throughTurnId;
|
|
602
667
|
const candidates = [];
|
|
603
|
-
|
|
668
|
+
// A head adopted without a transcript key (Cohub's Turn left no record) still parents what follows.
|
|
669
|
+
let parentTurnId = from > 0 || !state.throughTurnKey ? state.throughTurnId : null;
|
|
604
670
|
for (const [offset, turn] of pending.entries()) {
|
|
605
671
|
const marked = cohub(turn);
|
|
606
672
|
if (marked) {
|
|
@@ -726,7 +792,7 @@ export class NativeIngest {
|
|
|
726
792
|
return Date.parse(turn.startedAt) < Date.parse(this.enabledAt);
|
|
727
793
|
}
|
|
728
794
|
async archive(state, turn, turnId) {
|
|
729
|
-
if (!state.sessionId)
|
|
795
|
+
if (!state.sessionId || !harnessAdapter(state.harness).session.archive)
|
|
730
796
|
return;
|
|
731
797
|
try {
|
|
732
798
|
const source = turn.path ?? state.path;
|
|
@@ -31,7 +31,10 @@ export class NativeSessions {
|
|
|
31
31
|
if (restored) {
|
|
32
32
|
// A restored copy is history the server already has, even where it carries no markers.
|
|
33
33
|
const through = (await readTranscript(restored.path, restored.harness).catch(() => null))?.turns.at(-1)?.key ?? null;
|
|
34
|
-
|
|
34
|
+
if (through)
|
|
35
|
+
this.options.ingest.adopt(restored, input.sessionId, { turnKey: through, turnId: input.context.throughTurnId });
|
|
36
|
+
else
|
|
37
|
+
this.options.ingest.adopt(restored, input.sessionId);
|
|
35
38
|
return { session: restored, resume: "restored" };
|
|
36
39
|
}
|
|
37
40
|
}
|
|
@@ -45,6 +48,8 @@ export class NativeSessions {
|
|
|
45
48
|
const id = randomUUID();
|
|
46
49
|
const raw = join(this.options.root, "native", "restored", `${id}.jsonl`);
|
|
47
50
|
const format = harnessAdapter(input.harness).session.archive;
|
|
51
|
+
if (!format)
|
|
52
|
+
throw new ArchiveNotRestorableError(`${input.harness} archives are not restorable`);
|
|
48
53
|
try {
|
|
49
54
|
const restored = await this.options.archives.restore(archive, raw, signal);
|
|
50
55
|
const path = format.target({ id, nativeSessionId: restored.nativeSessionId, cwd: input.cwd });
|
|
@@ -18,6 +18,8 @@ export type NativeTranscriptTurn = {
|
|
|
18
18
|
boundaries: Record<number, string>;
|
|
19
19
|
sha256: string;
|
|
20
20
|
result: NativeTurnComplete | null;
|
|
21
|
+
/** The file itself recorded this Turn's end, so it settles without waiting for a quiet period. */
|
|
22
|
+
ended?: boolean;
|
|
21
23
|
path?: string;
|
|
22
24
|
rolloutId?: string;
|
|
23
25
|
sequence?: number;
|
|
@@ -31,6 +33,18 @@ export type NativeTranscript = {
|
|
|
31
33
|
lineagePaths?: string[];
|
|
32
34
|
lineageRolloutIds?: string[];
|
|
33
35
|
};
|
|
36
|
+
export type TranscriptLine = {
|
|
37
|
+
value: JsonRecord;
|
|
38
|
+
startBytes: number;
|
|
39
|
+
endBytes: number;
|
|
40
|
+
sha256: string;
|
|
41
|
+
};
|
|
42
|
+
type Line = TranscriptLine;
|
|
43
|
+
export declare const iso: (value: unknown) => string;
|
|
44
|
+
/** A size limit that the same bytes will hit again; retrying before the file changes is wasted work. */
|
|
45
|
+
export declare class SizeLimitError extends Error {
|
|
46
|
+
name: string;
|
|
47
|
+
}
|
|
34
48
|
export declare const NATIVE_MAX_RECORD_BYTES: number;
|
|
35
49
|
export declare const NATIVE_MAX_TRANSCRIPT_BYTES: number;
|
|
36
50
|
export declare const codexSessionsDirectory: () => string;
|
|
@@ -50,6 +64,8 @@ export type NativeTranscriptHeader = {
|
|
|
50
64
|
};
|
|
51
65
|
/** Read exactly the first JSONL record of a stream without re-concatenating per chunk. */
|
|
52
66
|
export declare function readFirstRecord(stream: AsyncIterable<Buffer>): Promise<JsonRecord>;
|
|
67
|
+
/** The first record a format without a header line identifies its session by; `null` when none is found early. */
|
|
68
|
+
export declare function readFirstMatchingRecord(stream: AsyncIterable<Buffer>, match: (value: JsonRecord) => boolean): Promise<JsonRecord | null>;
|
|
53
69
|
/** Read only the session header before deciding whether a global Codex file belongs to this project. */
|
|
54
70
|
export declare function readNativeTranscriptHeader(path: string, harness: "pi" | "codex", signal?: AbortSignal): Promise<NativeTranscriptHeader>;
|
|
55
71
|
/** Raw JSONL bytes of a rollout, transparently decompressing `.zst` and honoring a lineage cutoff. */
|
|
@@ -58,9 +74,16 @@ export declare function codexRolloutBytes(path: string, endBytes: number | null,
|
|
|
58
74
|
export declare function codexRolloutCachePath(source: string, cacheDirectory: string): string;
|
|
59
75
|
/** Archives need plain JSONL bytes; decompress a cold `.zst` rollout into a cache once. */
|
|
60
76
|
export declare function ensurePlainCodexRollout(path: string, cacheDirectory: string, signal?: AbortSignal, maxBytes?: number): Promise<string>;
|
|
77
|
+
type TranscriptLines = {
|
|
78
|
+
lines: Line[];
|
|
79
|
+
prefixes: Map<number, string>;
|
|
80
|
+
};
|
|
81
|
+
/** Newline framing with exact byte offsets and rolling checksums; a partial tail stays unparsed. */
|
|
82
|
+
export declare function readTranscriptLines(stream: AsyncIterable<Buffer>): Promise<TranscriptLines>;
|
|
61
83
|
/** Partial trailing records are retried, never parsed or acknowledged as complete. */
|
|
62
84
|
export declare function readNativeTranscript(path: string, harness: "pi" | "codex", options?: {
|
|
63
85
|
settled?: boolean;
|
|
64
86
|
leafId?: string | null;
|
|
65
87
|
signal?: AbortSignal;
|
|
66
88
|
}): Promise<NativeTranscript>;
|
|
89
|
+
export {};
|
|
@@ -12,16 +12,22 @@ import { codexTokenTotals, codexUsage } from "../codex-usage.js";
|
|
|
12
12
|
import { record } from "../json-rpc.js";
|
|
13
13
|
const text = (value) => typeof value === "string" ? value : "";
|
|
14
14
|
const list = (value) => Array.isArray(value) ? value : [];
|
|
15
|
-
const iso = (value) => {
|
|
15
|
+
export const iso = (value) => {
|
|
16
16
|
const date = new Date(typeof value === "number" || typeof value === "string" ? value : 0);
|
|
17
17
|
if (!Number.isFinite(date.getTime()))
|
|
18
18
|
throw new Error("Invalid native timestamp");
|
|
19
19
|
return date.toISOString();
|
|
20
20
|
};
|
|
21
21
|
const missing = (error) => error?.code === "ENOENT";
|
|
22
|
+
/** A size limit that the same bytes will hit again; retrying before the file changes is wasted work. */
|
|
23
|
+
export class SizeLimitError extends Error {
|
|
24
|
+
name = "SizeLimitError";
|
|
25
|
+
}
|
|
22
26
|
export const NATIVE_MAX_RECORD_BYTES = 256 * 1024 * 1024;
|
|
23
27
|
export const NATIVE_MAX_TRANSCRIPT_BYTES = 1024 * 1024 * 1024;
|
|
24
28
|
const NATIVE_HEADER_MAX_BYTES = 1024 * 1024;
|
|
29
|
+
/** Formats without a header line are searched this far for the record naming the session's project. */
|
|
30
|
+
const NATIVE_HEADER_SCAN_MAX_BYTES = 16 * 1024 * 1024;
|
|
25
31
|
const CODEX_HISTORY_MAX_DEPTH = 64;
|
|
26
32
|
const CODEX_ROLLOUT_INDEX_TTL_MS = 2_000;
|
|
27
33
|
const CODEX_ROLLOUT_NAME = /^rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-([0-9a-f-]{36})(?:_([0-9a-f-]{36}))?\.(?:jsonl|jsonl\.zst)$/;
|
|
@@ -64,6 +70,35 @@ export async function readFirstRecord(stream) {
|
|
|
64
70
|
}
|
|
65
71
|
throw new Error("Native transcript is empty");
|
|
66
72
|
}
|
|
73
|
+
/** The first record a format without a header line identifies its session by; `null` when none is found early. */
|
|
74
|
+
export async function readFirstMatchingRecord(stream, match) {
|
|
75
|
+
let fragments = [], scanned = 0;
|
|
76
|
+
for await (const chunk of stream) {
|
|
77
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
78
|
+
let start = 0;
|
|
79
|
+
for (let end = bytes.indexOf(10, start); end >= 0; end = bytes.indexOf(10, start)) {
|
|
80
|
+
const line = Buffer.concat([...fragments, bytes.subarray(start, end)]);
|
|
81
|
+
fragments = [];
|
|
82
|
+
start = end + 1;
|
|
83
|
+
if (!line.length)
|
|
84
|
+
continue;
|
|
85
|
+
let value;
|
|
86
|
+
try {
|
|
87
|
+
value = record(JSON.parse(line.toString("utf8")));
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (match(value))
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
fragments.push(bytes.subarray(start));
|
|
96
|
+
scanned += bytes.length;
|
|
97
|
+
if (scanned > NATIVE_HEADER_SCAN_MAX_BYTES)
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
67
102
|
async function readCodexHeader(path, signal) {
|
|
68
103
|
const value = await readFirstRecord(codexRolloutBytes(path, null, signal));
|
|
69
104
|
const payload = record(value.payload);
|
|
@@ -172,7 +207,7 @@ export async function ensurePlainCodexRollout(path, cacheDirectory, signal, maxB
|
|
|
172
207
|
transform(chunk, _encoding, push) {
|
|
173
208
|
written += chunk.length;
|
|
174
209
|
if (written > maxBytes) {
|
|
175
|
-
push(new
|
|
210
|
+
push(new SizeLimitError("Decompressed rollout exceeds the local limit"));
|
|
176
211
|
return;
|
|
177
212
|
}
|
|
178
213
|
push(null, chunk);
|
|
@@ -188,7 +223,7 @@ export async function ensurePlainCodexRollout(path, cacheDirectory, signal, maxB
|
|
|
188
223
|
}
|
|
189
224
|
}
|
|
190
225
|
/** Newline framing with exact byte offsets and rolling checksums; a partial tail stays unparsed. */
|
|
191
|
-
async function readTranscriptLines(stream) {
|
|
226
|
+
export async function readTranscriptLines(stream) {
|
|
192
227
|
const lines = [];
|
|
193
228
|
const prefixes = new Map();
|
|
194
229
|
let fragments = [], pendingBytes = 0, offset = 0;
|
|
@@ -200,9 +235,9 @@ async function readTranscriptLines(stream) {
|
|
|
200
235
|
pendingBytes += bytes.length;
|
|
201
236
|
checksum.update(bytes);
|
|
202
237
|
if (pendingBytes > NATIVE_MAX_RECORD_BYTES)
|
|
203
|
-
throw new
|
|
238
|
+
throw new SizeLimitError("Native record exceeds the 256 MiB local parse limit; original retained");
|
|
204
239
|
if (offset + pendingBytes > NATIVE_MAX_TRANSCRIPT_BYTES)
|
|
205
|
-
throw new
|
|
240
|
+
throw new SizeLimitError("Native transcript exceeds the 1 GiB local parse limit; original retained");
|
|
206
241
|
};
|
|
207
242
|
for await (const chunk of stream) {
|
|
208
243
|
let start = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neta-art/cohub-cli",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.8.1",
|
|
4
4
|
"description": "CLI for Cohub — spaces, sessions, and agent collaboration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
"NOTICE"
|
|
19
19
|
],
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@neta-art/cohub": "
|
|
22
|
-
"@neta-art/generation": "^0.1.
|
|
21
|
+
"@neta-art/cohub": "12.1.0",
|
|
22
|
+
"@neta-art/generation": "^0.1.40",
|
|
23
23
|
"commander": "^15.0.0",
|
|
24
24
|
"sharp": "^0.35.4",
|
|
25
25
|
"ws": "^8.21.3"
|