@ccmsg/cli 0.14.1 → 0.15.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.
@@ -1,4 +1,4 @@
1
- import { readdirSync, type Stats, statSync } from "node:fs";
1
+ import type { Stats } from "node:fs";
2
2
  import { readdir, readFile, stat } from "node:fs/promises";
3
3
  import { basename, dirname, join } from "node:path";
4
4
  import type { Sid, TranscriptSubject } from "@ccmsg/protocol";
@@ -95,16 +95,16 @@ export class TranscriptFiles {
95
95
  * greeted or is no longer running. Both stay inside this harness's own
96
96
  * transcript tree — the announced path because it was taken only if it was
97
97
  * inside it, the walk because that tree is what it walks (M6). */
98
- path(sid: Sid): string | undefined {
98
+ async path(sid: Sid): Promise<string | undefined> {
99
99
  const announced = this.deps.announced(sid);
100
- if (announced !== undefined && isFile(announced)) return announced;
101
- return this.find(sid);
100
+ if (announced !== undefined && (await stated(announced)) !== undefined) return announced;
101
+ return await this.find(sid);
102
102
  }
103
103
 
104
104
  /** The session's own transcript, for an op that has nothing to answer
105
105
  * without one. */
106
- session(sid: Sid): string {
107
- const found = this.path(sid);
106
+ async session(sid: Sid): Promise<string> {
107
+ const found = await this.path(sid);
108
108
  if (found === undefined) throw new OpError("not_found", `no transcript is held for ${sid}`);
109
109
  return found;
110
110
  }
@@ -116,7 +116,7 @@ export class TranscriptFiles {
116
116
  * cannot be combined — a request carrying both names two files and is a
117
117
  * caller's mistake rather than a choice this makes for them. */
118
118
  async locate(sid: Sid, names: AgentNames = {}): Promise<string> {
119
- const file = this.session(sid);
119
+ const file = await this.session(sid);
120
120
  if (names.agent_id !== undefined && names.teammate !== undefined) {
121
121
  throw new OpError("invalid_args", "agent_id and teammate name two different transcripts");
122
122
  }
@@ -238,21 +238,23 @@ export class TranscriptFiles {
238
238
  * which is one `stat` per directory instead of a listing; one whose name
239
239
  * carries more than the sid is walked, because the rest of the name is
240
240
  * exactly what this does not know. */
241
- private find(sid: Sid): string | undefined {
241
+ private async find(sid: Sid): Promise<string | undefined> {
242
242
  const layout = LAYOUTS[this.deps.harness];
243
- const dirs = directories(this.#root(), layout.depth);
243
+ const dirs = await walked(this.#root(), layout.depth);
244
244
  const nameOf = layout.nameOf;
245
245
  if (nameOf !== undefined) {
246
246
  if (!SID.test(sid)) return undefined;
247
247
  for (const dir of dirs) {
248
248
  const file = join(dir, nameOf(sid));
249
- if (isFile(file)) return file;
249
+ if ((await stated(file)) !== undefined) return file;
250
250
  }
251
251
  return undefined;
252
252
  }
253
253
  for (const dir of dirs) {
254
- for (const entry of names(dir)) {
255
- if (layout.sidOf(entry) === sid && isFile(join(dir, entry))) return join(dir, entry);
254
+ for (const entry of await listed(dir)) {
255
+ if (layout.sidOf(entry) !== sid) continue;
256
+ const file = join(dir, entry);
257
+ if ((await stated(file)) !== undefined) return file;
256
258
  }
257
259
  }
258
260
  return undefined;
@@ -265,13 +267,9 @@ export class TranscriptFiles {
265
267
  * wrote, and a tree with a directory nobody expected is one whose files are
266
268
  * still found.
267
269
  *
268
- * There are two of this walk, and of the two readings below it, because the
269
- * two callers are not alike: `all()` runs from an op and reads the tree
270
- * without holding the instance, while `path()` also runs from a subscription
271
- * being opened, which states its value in the turn it is opened in and so
272
- * cannot wait (DESIGN §6.2). The one that cannot wait is the one CT-Q8 and the
273
- * issue `fold-from-head-with-versioned-cache` settle, and the two become one
274
- * reading again when they do. */
270
+ * One walk for every caller: enumerating the tree and finding one session's
271
+ * file in it are the same reading, and a subscription that opens on it waits
272
+ * for it like any other caller (CT-Q8). */
275
273
  async function walked(root: string, depth: number): Promise<string[]> {
276
274
  let level = [root];
277
275
  for (let step = 0; step < depth; step += 1) {
@@ -284,14 +282,6 @@ async function walked(root: string, depth: number): Promise<string[]> {
284
282
  return level;
285
283
  }
286
284
 
287
- function directories(root: string, depth: number): string[] {
288
- let level = [root];
289
- for (let step = 0; step < depth; step += 1) {
290
- level = level.flatMap((dir) => names(dir).map((entry) => join(dir, entry)));
291
- }
292
- return level;
293
- }
294
-
295
285
  export interface AgentNames {
296
286
  readonly agent_id?: string;
297
287
  readonly run_id?: string;
@@ -343,14 +333,6 @@ async function listed(dir: string): Promise<string[]> {
343
333
  }
344
334
  }
345
335
 
346
- function names(dir: string): string[] {
347
- try {
348
- return readdirSync(dir);
349
- } catch {
350
- return [];
351
- }
352
- }
353
-
354
336
  async function stated(file: string): Promise<Stats | undefined> {
355
337
  try {
356
338
  const known = await stat(file);
@@ -359,16 +341,3 @@ async function stated(file: string): Promise<Stats | undefined> {
359
341
  return undefined;
360
342
  }
361
343
  }
362
-
363
- function statOf(file: string) {
364
- try {
365
- const known = statSync(file);
366
- return known.isFile() ? known : undefined;
367
- } catch {
368
- return undefined;
369
- }
370
- }
371
-
372
- function isFile(file: string): boolean {
373
- return statOf(file) !== undefined;
374
- }
@@ -61,6 +61,29 @@ export const NO_FACTS: TranscriptFacts = {
61
61
  agent_tree: { teammates: [], agents: [], workflows: [] },
62
62
  };
63
63
 
64
+ /** Everything the fold carries from one record to the next, as data.
65
+ *
66
+ * The facts are what a consumer reads; this is what reading further needs. The
67
+ * two are not the same — a todo is held under the key a later record updates it
68
+ * by, a call is held until its result arrives — so a fold resumed from its
69
+ * facts alone would answer the next record differently from one that had read
70
+ * every record before it. What is kept here is the whole of that difference,
71
+ * which is what makes resuming from it the same reading rather than a similar
72
+ * one. */
73
+ export interface FoldState {
74
+ readonly api_error?: SessionApiError;
75
+ readonly last_user_input_at?: Timestamp;
76
+ readonly model?: string;
77
+ readonly effort?: string;
78
+ readonly files: readonly (readonly [string, ExternalFile])[];
79
+ readonly todos: readonly (readonly [string, SessionTodo])[];
80
+ readonly teammates: readonly (readonly [string, Teammate])[];
81
+ readonly background: readonly (readonly [string, SessionBackgroundStatus])[];
82
+ readonly workflows: readonly (readonly [string, SessionWorkflowStatus])[];
83
+ readonly agents: readonly (readonly [string, AgentTreeNode])[];
84
+ readonly calls: readonly (readonly [string, PendingCall])[];
85
+ }
86
+
64
87
  /** The one place a transcript line is interpreted.
65
88
  *
66
89
  * Nothing outside this module parses a transcript record. A line arrives, the
@@ -104,6 +127,43 @@ export class TranscriptFold {
104
127
  };
105
128
  }
106
129
 
130
+ /** Everything the reading carries forward, so that it can be taken up again
131
+ * where it stopped. */
132
+ get held(): FoldState {
133
+ return {
134
+ ...(this.#apiError === undefined ? {} : { api_error: this.#apiError }),
135
+ ...(this.#lastUserInputAt === undefined ? {} : { last_user_input_at: this.#lastUserInputAt }),
136
+ ...(this.#model === undefined ? {} : { model: this.#model }),
137
+ ...(this.#effort === undefined ? {} : { effort: this.#effort }),
138
+ files: [...this.#files],
139
+ todos: [...this.#todos],
140
+ teammates: [...this.#teammates],
141
+ background: [...this.#background],
142
+ workflows: [...this.#workflows],
143
+ agents: [...this.#agents],
144
+ calls: [...this.#calls],
145
+ };
146
+ }
147
+
148
+ /** Take up a reading somebody else left off, replacing whatever this fold
149
+ * held. The state is trusted as the record of records already read — what
150
+ * makes it trustworthy is decided where it is kept (`FOLD_CACHE_VERSION`),
151
+ * not here. */
152
+ restore(state: FoldState): void {
153
+ this.reset();
154
+ this.#apiError = state.api_error;
155
+ this.#lastUserInputAt = state.last_user_input_at;
156
+ this.#model = state.model;
157
+ this.#effort = state.effort;
158
+ for (const [key, value] of state.files) this.#files.set(key, value);
159
+ for (const [key, value] of state.todos) this.#todos.set(key, value);
160
+ for (const [key, value] of state.teammates) this.#teammates.set(key, value);
161
+ for (const [key, value] of state.background) this.#background.set(key, { ...value });
162
+ for (const [key, value] of state.workflows) this.#workflows.set(key, { ...value });
163
+ for (const [key, value] of state.agents) this.#agents.set(key, { ...value });
164
+ for (const [key, value] of state.calls) this.#calls.set(key, value);
165
+ }
166
+
107
167
  reset(): void {
108
168
  this.#apiError = undefined;
109
169
  this.#lastUserInputAt = undefined;
@@ -648,13 +708,13 @@ type Mutable<T> = { -readonly [K in keyof T]: T[K] };
648
708
 
649
709
  /** A teammate as the fold holds it: what the contract states about it, and the
650
710
  * two facts only the tree needs. */
651
- interface Teammate {
711
+ export interface Teammate {
652
712
  readonly status: Mutable<SessionTeammate>;
653
713
  agent_id?: string;
654
714
  team_name?: string;
655
715
  }
656
716
 
657
- interface PendingCall {
717
+ export interface PendingCall {
658
718
  readonly name: string;
659
719
  readonly input: Record<string, unknown>;
660
720
  readonly at?: Timestamp;
@@ -4,7 +4,9 @@ export {
4
4
  TranscriptFiles,
5
5
  type TranscriptFilesDeps,
6
6
  } from "./files.ts";
7
+ export { FOLD_CACHE_VERSION, FoldCache, type FoldCacheEntry } from "./cache.ts";
7
8
  export {
9
+ type FoldState,
8
10
  NO_FACTS,
9
11
  readRecord,
10
12
  type TranscriptFacts,
@@ -12,5 +14,5 @@ export {
12
14
  type TranscriptRecord,
13
15
  } from "./fold.ts";
14
16
  export { READ_LIMIT, readSlice } from "./read.ts";
15
- export { type Appended, FOLD_TAIL_BYTES, TranscriptTail } from "./tail.ts";
17
+ export { type Appended, READ_CHUNK_BYTES, TranscriptTail } from "./tail.ts";
16
18
  export { ITEMS_SNAPSHOT, Transcripts, type TranscriptsDeps } from "./transcripts.ts";
@@ -97,6 +97,27 @@ type Draft = Record<string, unknown> & {
97
97
  at: number;
98
98
  };
99
99
 
100
+ /** Everything a reading carries from one record to the next, as data.
101
+ *
102
+ * The items already handed over are not here — a reader holds those — but what
103
+ * decides how the next record reads is: which turn the file is in, which slash
104
+ * command output belongs to, whose file it is, and the calls still waiting for
105
+ * an answer. A reading taken up from this answers the next record exactly as
106
+ * the reading that produced it would have.
107
+ *
108
+ * The outstanding calls name their items by id rather than carrying them: the
109
+ * items are what a reader already holds, and naming them is what lets a result
110
+ * point back at the call in the very list the reader has. */
111
+ export interface ClassificationState {
112
+ readonly turn: number;
113
+ readonly slash?: string;
114
+ readonly subject: TranscriptSubject;
115
+ readonly calls: readonly (readonly [
116
+ string,
117
+ { readonly tool: string; readonly message?: string; readonly name: string },
118
+ ])[];
119
+ }
120
+
100
121
  /** A whole transcript read as items, in the order the file holds them.
101
122
  *
102
123
  * The file is read through once and the links are filled in as the answers
@@ -144,6 +165,62 @@ export class Classification {
144
165
  this.#subject = subject;
145
166
  }
146
167
 
168
+ /** What this reading would carry into the next record. */
169
+ get held(): ClassificationState {
170
+ const calls: ClassificationState["calls"] = [...this.#calls].map(([id, call]) => [
171
+ id,
172
+ {
173
+ tool: call.tool.id,
174
+ name: call.name,
175
+ ...optional("message", call.message?.id),
176
+ },
177
+ ]);
178
+ return {
179
+ turn: this.#turn,
180
+ subject: this.#subject,
181
+ calls,
182
+ ...optional("slash", this.#slash),
183
+ };
184
+ }
185
+
186
+ /** Take up a reading somebody else left off.
187
+ *
188
+ * `known` is the items that reading handed over and this one still holds, so
189
+ * an outstanding call is matched back to the item a reader has rather than to
190
+ * a copy of it: what the answer writes into the call then lands where the
191
+ * reader will look for it. A call whose item the reader no longer holds is
192
+ * kept by name and id alone — the result still says which tool answered and
193
+ * which item it belongs to, and there is no item left to write into. */
194
+ restore(state: ClassificationState, known: Iterable<Item>): void {
195
+ this.#items = [];
196
+ this.#calls.clear();
197
+ this.#turn = state.turn;
198
+ this.#subject = state.subject;
199
+ this.#slash = state.slash;
200
+ const held = new Map<string, Draft>();
201
+ for (const item of known) {
202
+ const draft = item as unknown as Draft;
203
+ if (typeof draft.id === "string") held.set(draft.id, draft);
204
+ }
205
+ const detached = (id: string): Draft =>
206
+ held.get(id) ??
207
+ ({
208
+ id,
209
+ uuid: "",
210
+ subject: this.#subject,
211
+ source: { offset: 0, bytes: 0 },
212
+ type: "",
213
+ at: 0,
214
+ } as Draft);
215
+ for (const [id, call] of state.calls) {
216
+ this.#calls.set(id, {
217
+ tool: detached(call.tool),
218
+ name: call.name,
219
+ ...optional("message", call.message === undefined ? undefined : detached(call.message)),
220
+ });
221
+ }
222
+ }
223
+
147
224
  /** The records of one chunk as the items they were read as, oldest first.
148
225
  *
149
226
  * What comes back is this chunk's items alone. A call the chunk before it
@@ -1,4 +1,4 @@
1
- export { classify, Classification } from "./classify.ts";
1
+ export { classify, Classification, type ClassificationState } from "./classify.ts";
2
2
  export { document, type DumpView } from "./document.ts";
3
3
  export { fields, type Item } from "./item.ts";
4
4
  export { ledger } from "./ids.ts";
@@ -1,22 +1,19 @@
1
- import { closeSync, type FSWatcher, openSync, readSync, statSync, watch } from "node:fs";
1
+ import { type FSWatcher, watch } from "node:fs";
2
2
  import { open, stat } from "node:fs/promises";
3
3
  import { CONFIRM_POLL_MS } from "../sessions/harness.ts";
4
+ import { breathe } from "./scan.ts";
4
5
 
5
- /** How much of an existing transcript is read when a tail starts.
6
+ /** How much of a transcript one read takes at a time.
6
7
  *
7
- * The fold's two values both describe the present the error the latest turn
8
- * ended on, and the last time a person spoke so what a tail needs on opening
9
- * is the recent end of the file, not its history. A megabyte is a few hundred
10
- * records at the sizes the harness writes, which reaches back past the current
11
- * turn by a wide margin while costing one read of fixed size however large the
12
- * file has grown (DESIGN §2.3: a transcript of any size is read from its end).
13
- *
14
- * A person who has not spoken within it is reported as having no known input
15
- * rather than as having spoken long ago, which is what the contract's absent
16
- * `last_user_input_at` already means. */
17
- export const FOLD_TAIL_BYTES = 1024 * 1024;
18
-
19
- /** What the tail found appended, with the offsets that place it.
8
+ * The reading of a whole file is cut into reads of this size and the loop is
9
+ * handed back between them, so following a transcript of any size costs the
10
+ * instance a read of fixed size rather than a pause proportional to the file.
11
+ * A megabyte is a few hundred records at the sizes the harness writes, which
12
+ * is large enough that the turns themselves cost nothing measurable against
13
+ * the parsing they carry. */
14
+ export const READ_CHUNK_BYTES = 1024 * 1024;
15
+
16
+ /** What the tail read, with the offsets that place it.
20
17
  *
21
18
  * The offsets are the ones a transcript read pages by, so what arrives live
22
19
  * and what was read stitch together without reading anything twice. `end` is
@@ -32,10 +29,10 @@ export interface Appended {
32
29
  export interface TailDeps {
33
30
  /** Complete lines only; a record still being written waits for its end. */
34
31
  readonly onAppended: (appended: Appended) => void;
35
- /** The end of the file as it stood when the tail opened, oldest first, with
36
- * the offsets that place it. The seed of what is folded and of what is
32
+ /** What was already in the file when the tail opened, oldest first, in the
33
+ * reads it arrived in. The input of what is folded and of what is
37
34
  * classified, not something a subscriber is sent. */
38
- readonly onSeed: (seeded: Appended) => void;
35
+ readonly onExisting: (read: Appended) => void;
39
36
  /** The file is not the one the tail was reading: it shrank, so what was
40
37
  * folded out of the old contents no longer describes it. */
41
38
  readonly onTruncated: () => void;
@@ -63,17 +60,7 @@ export class TranscriptTail {
63
60
  constructor(
64
61
  private readonly path: string,
65
62
  private readonly deps: TailDeps,
66
- ) {
67
- // Where the file ends, read before anything can ask. A subscription's
68
- // snapshot states this and is answered in the same turn the tail is
69
- // created, so a size that only the awaited seed had filled in would be
70
- // reported as zero and every byte already written would look appended.
71
- // Reading it here also fixes what the seed reads: the seed takes this size
72
- // rather than stating a newer one, so nothing lands between the size the
73
- // subscriber was given and the first frame it is sent.
74
- this.#size = sizeNow(path);
75
- this.#offset = this.#size;
76
- }
63
+ ) {}
77
64
 
78
65
  /** The transcript's size as last observed, which is what a subscription's
79
66
  * snapshot states and where the frames after it begin. */
@@ -81,21 +68,29 @@ export class TranscriptTail {
81
68
  return this.#size;
82
69
  }
83
70
 
71
+ /** Just past the last record read, which is what a reading resumed later
72
+ * starts from. */
73
+ get offset(): number {
74
+ return this.#offset;
75
+ }
76
+
84
77
  get running(): boolean {
85
78
  return this.#watcher !== undefined || this.#timer !== undefined;
86
79
  }
87
80
 
88
- /** Begin following, seeding the fold from the end of what is already there.
81
+ /** Read what is already there, then begin following.
82
+ *
83
+ * `from` is where a reading of this same file left off — nothing for a file
84
+ * being read for the first time, which is read from its beginning. Everything
85
+ * before the tail's own first append is therefore accounted for, whether it
86
+ * was read now or read once before and remembered.
89
87
  *
90
- * The seed is read before this returns rather than awaited, for the reason
91
- * the size is read in the constructor: a subscription's snapshot is answered
92
- * in the same turn the tail is started, and what the seed settles — the
93
- * fold's values, and the items a subscriber opens on — would otherwise be
94
- * stated as empty and the whole existing end of the file would arrive later
95
- * as though it had just been appended. */
96
- async start(): Promise<void> {
88
+ * This is awaited by whoever states a value derived from the file, so a
89
+ * subscriber is told what the whole transcript says rather than what its end
90
+ * says (CT-Q8). */
91
+ async start(from = 0): Promise<void> {
97
92
  if (this.running) return;
98
- this.#seed();
93
+ await this.#catchUp(from);
99
94
  try {
100
95
  this.#watcher = watch(this.path, () => void this.refresh());
101
96
  } catch {
@@ -121,20 +116,17 @@ export class TranscriptTail {
121
116
  return this.#reading;
122
117
  }
123
118
 
124
- #seed(): void {
125
- const size = this.#size;
126
- if (size === 0) return;
127
- const from = Math.max(0, size - FOLD_TAIL_BYTES);
128
- const complete = whole(this.#sliceSync(from, size));
129
- this.#offset = from + complete.byteLength;
130
- // The first line is half a record whenever the read began mid-file, so it
131
- // is dropped: what is read are whole records or nothing.
132
- const at = from === 0 ? 0 : complete.indexOf(NEWLINE) + 1;
133
- this.deps.onSeed({ lines: split(complete, at), start: from + at, end: this.#offset, size });
119
+ /** The file as it already stands, read to the end it had when this began.
120
+ * What is written while it runs is left to the first append, so the size a
121
+ * subscription opens on and the first frame after it meet exactly. */
122
+ async #catchUp(from: number): Promise<void> {
123
+ this.#offset = from;
124
+ this.#size = await this.#stat(from);
125
+ await this.#consume(this.#size, this.deps.onExisting);
134
126
  }
135
127
 
136
128
  async #read(): Promise<void> {
137
- const size = await this.#stat();
129
+ const size = await this.#stat(this.#offset);
138
130
  if (size < this.#offset) {
139
131
  // Shorter than what was already consumed: the file was replaced, so what
140
132
  // was folded out of it describes nothing, and reading resumes from its
@@ -143,21 +135,36 @@ export class TranscriptTail {
143
135
  this.deps.onTruncated();
144
136
  }
145
137
  this.#size = size;
146
- if (size === this.#offset) return;
147
- const complete = whole(await this.#slice(this.#offset, size));
148
- if (complete.byteLength === 0) return;
149
- const start = this.#offset;
150
- const end = start + complete.byteLength;
151
- this.#offset = end;
152
- this.deps.onAppended({ lines: split(complete, 0), start, end, size });
138
+ await this.#consume(size, this.deps.onAppended);
153
139
  }
154
140
 
155
- async #stat(): Promise<number> {
141
+ /** Everything up to `size`, in reads of a fixed size with the loop handed
142
+ * back between them. A record still being written ends the pass: it is left
143
+ * for the read that finds its end. */
144
+ async #consume(size: number, state: (read: Appended) => void): Promise<void> {
145
+ while (this.#offset < size) {
146
+ // A record longer than one read is taken in one piece rather than in
147
+ // halves, so the window grows until it holds a whole one.
148
+ let to = Math.min(this.#offset + READ_CHUNK_BYTES, size);
149
+ let complete = whole(await this.#slice(this.#offset, to));
150
+ while (complete.byteLength === 0 && to < size) {
151
+ to = Math.min(to + READ_CHUNK_BYTES, size);
152
+ complete = whole(await this.#slice(this.#offset, to));
153
+ }
154
+ if (complete.byteLength === 0) return;
155
+ const start = this.#offset;
156
+ this.#offset = start + complete.byteLength;
157
+ state({ lines: split(complete), start, end: this.#offset, size });
158
+ if (this.#offset < size) await breathe();
159
+ }
160
+ }
161
+
162
+ async #stat(fallback: number): Promise<number> {
156
163
  try {
157
164
  return (await stat(this.path)).size;
158
165
  } catch {
159
166
  // Not there. Nothing was appended, and the poll keeps looking.
160
- return this.#offset;
167
+ return fallback;
161
168
  }
162
169
  }
163
170
 
@@ -169,26 +176,6 @@ export class TranscriptTail {
169
176
  * arithmetic puts it, inside a character as readily as before one, and
170
177
  * decoding first would turn those bytes into a replacement character of a
171
178
  * different length and move every offset derived from it. */
172
- /** The same range, read without yielding, which is what the seed is read
173
- * through: the turn that starts a tail is the turn that answers a
174
- * subscription, and it has to hold the end of the file by then. Bounded by
175
- * `FOLD_TAIL_BYTES` however large the transcript is. */
176
- #sliceSync(from: number, to: number): Buffer {
177
- if (to <= from) return Buffer.alloc(0);
178
- let handle: number;
179
- try {
180
- handle = openSync(this.path, "r");
181
- } catch {
182
- return Buffer.alloc(0);
183
- }
184
- try {
185
- const buffer = Buffer.alloc(to - from);
186
- return buffer.subarray(0, readSync(handle, buffer, 0, buffer.length, from));
187
- } finally {
188
- closeSync(handle);
189
- }
190
- }
191
-
192
179
  async #slice(from: number, to: number): Promise<Buffer> {
193
180
  if (to <= from) return Buffer.alloc(0);
194
181
  const handle = await open(this.path, "r").catch(() => undefined);
@@ -213,19 +200,13 @@ function whole(bytes: Buffer): Buffer {
213
200
  return last < 0 ? Buffer.alloc(0) : bytes.subarray(0, last + 1);
214
201
  }
215
202
 
216
- /** The records in what was read, from a byte that begins one. */
217
- function split(complete: Buffer, at: number): string[] {
203
+ /** The records in what was read, which begins at a record. */
204
+ function split(complete: Buffer): string[] {
218
205
  const lines: string[] = [];
219
- for (let from = at; from < complete.byteLength;) {
206
+ for (let from = 0; from < complete.byteLength;) {
220
207
  const newline = complete.indexOf(NEWLINE, from);
221
208
  lines.push(complete.toString("utf8", from, newline));
222
209
  from = newline + 1;
223
210
  }
224
211
  return lines;
225
212
  }
226
-
227
- /** How large the file is right now, or zero for one that is not there yet.
228
- * Synchronous because the value is wanted before the first await. */
229
- function sizeNow(path: string): number {
230
- return statSync(path, { throwIfNoEntry: false })?.size ?? 0;
231
- }