@gigaai/newton 0.3.0 → 0.4.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 CHANGED
@@ -61,19 +61,26 @@ script body. The body runs as an async function with these in scope:
61
61
 
62
62
  | Primitive | What it does |
63
63
  |---|---|
64
- | `agent(prompt, opts?)` | Run one agent turn; returns its final text, or a schema-validated object when `opts.schema` is set. Returns `null` on agent failure — filter with `.filter(Boolean)`. |
64
+ | `agent(prompt, opts?)` | Run one agent turn; returns its final text, or a schema-validated object when `opts.schema` is set. Transient failures (provider 5xx/overload, rate limits, dropped connections) retry up to twice with backoff; returns `null` on lasting failure — filter with `.filter(Boolean)`. |
65
65
  | `parallel(thunks)` | Run `() => …` tasks concurrently; a failed task resolves to `null`. Barrier: waits for all. |
66
66
  | `pipeline(items, ...stages)` | Each item flows through the stages independently — no barrier between stages. Stage signature: `(prev, item, index)`. |
67
67
  | `phase(title)` | Group subsequent agents in progress output. |
68
68
  | `log(message)` | Narrator line in progress output and the journal. |
69
+ | `workflow(nameOrPath, args?)` | Run one child workflow. Bare names resolve from `.newton/workflows/<name>.js`; paths resolve relative to the calling workflow. Nesting is limited to one child level. |
69
70
  | `args` | Whatever you passed via `--args` (JSON-parsed when possible). |
70
71
  | `budget` | `{ total, spent(), remaining() }` for the `--budget` output-token ceiling. |
71
72
 
72
73
  `agent()` options: `provider` (see below), `model`, `effort` (`low`–`xhigh`),
73
74
  `access` (`read` | `write` | `full` — sandbox posture), `schema` (JSON Schema
74
75
  for structured output, validated with automatic retries), `label`, `phase`,
75
- `cwd`, `timeoutMs`, and `command` an escape hatch to run any ACP agent,
76
- e.g. `command: ['my-agent', '--acp']`.
76
+ `cwd`, `timeoutMs`, `isolation: 'worktree'` for disposable per-agent git
77
+ worktrees, `mcp` (`true` for all `.mcp.json` servers, or a list of names/inline
78
+ servers), `config` (ACP `configId` to string value), and `command` — an escape
79
+ hatch to run any ACP agent, e.g. `command: ['my-agent', '--acp']`.
80
+
81
+ Workflow scripts run in a sandboxed JavaScript realm: Node globals such as
82
+ `process`, `fetch`, and `setTimeout` are not available, and `console.log()`
83
+ routes through `log()` into progress output and the journal.
77
84
 
78
85
  ## Providers
79
86
 
@@ -98,7 +105,7 @@ the agent advertises over ACP (session config options) and selects it
98
105
  protocol-side. `newton models <provider>` shows what an agent exposes;
99
106
  requesting a model an agent doesn't offer fails with the available list.
100
107
 
101
- `Date.now()`, argless `new Date()`, and `Math.random()` throw inside
108
+ `Date.now()`, argless `new Date()`, and `Math.random()` also throw inside
102
109
  workflows: nondeterminism would silently break `--resume` replay. Pass
103
110
  timestamps in via `--args`.
104
111
 
@@ -113,15 +120,32 @@ workspace:
113
120
  chunk, tool call, and permission decision.
114
121
 
115
122
  `newton run workflow.js --resume <runId>` replays successful agent results
116
- from a previous run's journal identical calls return instantly and only new
117
- or changed calls hit real agents.
123
+ from a previous run's journal. The default `--resume-mode prefix` replays by
124
+ call position until the first changed agent call, then runs that call and
125
+ everything after it live. `--resume-mode hash` uses the legacy hash replay:
126
+ identical calls return instantly no matter where they appear.
127
+
128
+ Long runs can be detached and controlled from another shell:
129
+
130
+ - `newton run workflow.js --detach` starts in the background and prints
131
+ `{ runId, pid }`.
132
+ - `newton runs [--json]` lists `.newton/runs/*` with status, agent count,
133
+ output tokens, and pid.
134
+ - `newton watch <runId> [--json]` replays the journal from the beginning and
135
+ keeps tailing until the run ends.
136
+ - `newton skip <runId> <agentId>` asks a queued or live agent to cancel;
137
+ `agent()` returns `null` and the workflow continues.
118
138
 
119
139
  ## CLI
120
140
 
121
141
  ```
122
142
  newton run <workflow.js> --args <json> --cwd <dir> --resume <runId> --json
143
+ --resume-mode <prefix|hash> --detach
123
144
  --budget <tokens> --max-agents <n> --timeout <seconds>
124
145
  --concurrency <n>
146
+ newton runs [--json] list runs under .newton/runs
147
+ newton watch <runId> [--json] replay/tail a run's journal
148
+ newton skip <runId> <agentId> cancel one agent; workflow continues
125
149
  newton validate <workflow.js> parse + compile without spawning agents
126
150
  newton agents [--json] supported agents: installed? signed in? (alias: doctor)
127
151
  newton models <provider> ask an agent which models/modes it exposes
package/dist/acp.js CHANGED
@@ -50,8 +50,14 @@ export class AcpAgentSession {
50
50
  toolCalls = 0;
51
51
  usage = null;
52
52
  stderrTail = [];
53
+ aborted;
54
+ abortSignal;
55
+ resolveAbortSignal;
53
56
  constructor(request) {
54
57
  this.request = request;
58
+ this.abortSignal = new Promise((resolveAbort) => {
59
+ this.resolveAbortSignal = resolveAbort;
60
+ });
55
61
  }
56
62
  async prompt() {
57
63
  try {
@@ -72,6 +78,15 @@ export class AcpAgentSession {
72
78
  }
73
79
  async promptAgain(prompt) {
74
80
  try {
81
+ if (this.aborted) {
82
+ return {
83
+ text: this.text,
84
+ stopReason: "cancelled",
85
+ usage: this.usage,
86
+ toolCalls: this.toolCalls,
87
+ error: this.aborted,
88
+ };
89
+ }
75
90
  const connection = this.connection;
76
91
  const sessionId = this.sessionId;
77
92
  this.text = "";
@@ -84,7 +99,7 @@ export class AcpAgentSession {
84
99
  sessionId,
85
100
  prompt: [{ type: "text", text: prompt }],
86
101
  });
87
- const outcome = await Promise.race([turn, timedOut]);
102
+ const outcome = await Promise.race([turn, timedOut, this.abortSignal]);
88
103
  clearTimeout(timer);
89
104
  if (outcome === "timeout") {
90
105
  await connection.cancel({ sessionId }).catch(() => { });
@@ -99,6 +114,19 @@ export class AcpAgentSession {
99
114
  error: `agent timed out after ${this.request.timeoutMs}ms`,
100
115
  };
101
116
  }
117
+ if (outcome === "aborted") {
118
+ await connection.cancel({ sessionId }).catch(() => { });
119
+ // Give the agent a moment to flush final updates, then hard-stop.
120
+ await Promise.race([turn.catch(() => { }), new Promise((r) => setTimeout(r, 3000))]);
121
+ this.close();
122
+ return {
123
+ text: this.text,
124
+ stopReason: "cancelled",
125
+ usage: this.usage,
126
+ toolCalls: this.toolCalls,
127
+ error: this.aborted,
128
+ };
129
+ }
102
130
  this.usage = outcome.usage ?? this.usage;
103
131
  return {
104
132
  text: this.text,
@@ -111,6 +139,10 @@ export class AcpAgentSession {
111
139
  return this.failure(error);
112
140
  }
113
141
  }
142
+ abort(reason) {
143
+ this.aborted = reason;
144
+ this.resolveAbortSignal("aborted");
145
+ }
114
146
  close() {
115
147
  if (this.child && !this.child.killed)
116
148
  this.child.kill("SIGTERM");
@@ -187,13 +219,17 @@ export class AcpAgentSession {
187
219
  session: { configOptions: {} },
188
220
  },
189
221
  });
190
- const session = await connection.newSession({ cwd: resolve(cwd), mcpServers: [] });
222
+ const session = await connection.newSession({ cwd: resolve(cwd), mcpServers: this.request.mcpServers ?? [] });
191
223
  this.sessionId = session.sessionId;
192
224
  this.sessionInfo = session;
193
225
  trace({ type: "session", sessionId: session.sessionId, configOptions: session.configOptions ?? null });
194
226
  if (this.request.spec.model && !resolved.modelHandled) {
195
227
  await this.selectModelViaProtocol(session, this.request.spec.model);
196
228
  }
229
+ for (const [configId, value] of Object.entries(this.request.config ?? {})) {
230
+ await connection.setSessionConfigOption({ sessionId: this.sessionId, configId, value });
231
+ trace({ type: "config_set", configId, value });
232
+ }
197
233
  }
198
234
  /**
199
235
  * Providers without a CLI/env model knob may still expose model choice as an
package/dist/cli.js CHANGED
@@ -1,7 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { accessSync, constants } from "node:fs";
2
+ import { spawn } from "node:child_process";
3
+ import { accessSync, closeSync, constants, mkdirSync, openSync } from "node:fs";
4
+ import { dirname, join, resolve } from "node:path";
3
5
  import { AcpAgentSession } from "./acp.js";
6
+ import { listRuns, readStatus, requestSkip, tailJournal } from "./control.js";
4
7
  import { compileWorkflow, loadWorkflow, runWorkflow } from "./executor.js";
8
+ import { newRunId } from "./journal.js";
5
9
  import { HumanReporter, JsonReporter } from "./progress.js";
6
10
  import { describeProviders } from "./providers.js";
7
11
  import { VERSION } from "./version.js";
@@ -9,6 +13,10 @@ const USAGE = `newton — deterministic multi-agent workflows over ACP
9
13
 
10
14
  Usage:
11
15
  newton run <workflow.js> [options] Run a workflow in the current directory
16
+ newton run <workflow.js> --detach Start in the background; prints { runId, pid }
17
+ newton runs [--json] List runs under .newton/runs with status
18
+ newton watch <runId> [--json] Tail a run's journal as live progress
19
+ newton skip <runId> <agentId> Cancel one agent; agent() returns null, run continues
12
20
  newton validate <workflow.js> Parse + compile a workflow without running agents
13
21
  newton agents [--json] Supported agents: installed? signed in? (alias: doctor)
14
22
  newton models <provider> [--json] Ask an agent which models it exposes
@@ -17,6 +25,8 @@ Run options:
17
25
  --args <json> Value exposed to the script as \`args\`
18
26
  --cwd <dir> Workspace agents operate in (default: current directory)
19
27
  --resume <runId> Replay cached agent results from a previous run
28
+ --resume-mode <prefix|hash> Resume matching: positional prefix (default) or hash replay
29
+ --detach Start the run in the background
20
30
  --json NDJSON events on stdout instead of human output
21
31
  --budget <tokens> Output-token ceiling; agent() throws once exhausted
22
32
  --max-agents <n> Agent-call cap (default 200)
@@ -28,7 +38,11 @@ followed by a script body using agent()/parallel()/pipeline()/phase()/log(),
28
38
  with \`args\` and \`budget\` in scope. agent() opts: provider (codex | claude |
29
39
  gemini | qwen | cursor | opencode | goose | copilot), model, effort, access
30
40
  (read | write | full), schema, label, phase, cwd, command (any ACP agent,
31
- e.g. ["my-agent", "--acp"]).
41
+ e.g. ["my-agent", "--acp"]), isolation ('worktree'), mcp (true | names/servers
42
+ from .mcp.json), config ({configId: value}).
43
+ Workflows may call workflow(nameOrPath, args) one level deep; bare names resolve
44
+ from .newton/workflows/<name>.js.
45
+ Scripts run sandboxed: no process/fetch/setTimeout; console.log routes to log().
32
46
 
33
47
  Runs are recorded under .newton/runs/<runId>/ — journal.jsonl plus a full
34
48
  per-agent ACP trace.`;
@@ -66,10 +80,47 @@ function intFlag(flags, name) {
66
80
  fail(`--${name} requires a positive integer`);
67
81
  return value;
68
82
  }
83
+ function resumeModeFlag(flags) {
84
+ const raw = flags["resume-mode"];
85
+ if (raw === undefined)
86
+ return undefined;
87
+ if (raw === "prefix" || raw === "hash")
88
+ return raw;
89
+ fail("--resume-mode requires prefix or hash");
90
+ }
91
+ function commandRunDetached(file, flags) {
92
+ const cwd = typeof flags.cwd === "string" ? resolve(flags.cwd) : process.cwd();
93
+ const runId = newRunId();
94
+ const runDir = join(cwd, ".newton", "runs", runId);
95
+ mkdirSync(runDir, { recursive: true });
96
+ const fd = openSync(join(runDir, "detach.log"), "a");
97
+ const argv = [process.argv[1], "run", resolve(file), "--json", "--run-id", runId, "--cwd", cwd];
98
+ for (const name of ["args", "resume", "resume-mode", "budget", "max-agents", "timeout", "concurrency"]) {
99
+ const value = flags[name];
100
+ if (typeof value === "string")
101
+ argv.push(`--${name}`, value);
102
+ }
103
+ let child;
104
+ try {
105
+ child = spawn(process.execPath, argv, { detached: true, stdio: ["ignore", fd, fd] });
106
+ }
107
+ finally {
108
+ closeSync(fd);
109
+ }
110
+ if (!child)
111
+ fail("failed to start detached run");
112
+ child.unref();
113
+ process.stdout.write(`${JSON.stringify({ runId, pid: child.pid }, null, 2)}\n`);
114
+ process.exit(0);
115
+ }
69
116
  async function commandRun(positional, flags) {
70
117
  const file = positional[0];
71
118
  if (!file)
72
119
  fail("newton run requires a workflow file");
120
+ const resumeMode = resumeModeFlag(flags);
121
+ if (flags.detach === true) {
122
+ commandRunDetached(file, flags);
123
+ }
73
124
  let args;
74
125
  if (typeof flags.args === "string") {
75
126
  try {
@@ -87,6 +138,9 @@ async function commandRun(positional, flags) {
87
138
  args,
88
139
  cwd: typeof flags.cwd === "string" ? flags.cwd : process.cwd(),
89
140
  resumeRunId: typeof flags.resume === "string" ? flags.resume : undefined,
141
+ resumeMode,
142
+ runId: typeof flags["run-id"] === "string" ? flags["run-id"] : undefined,
143
+ detached: typeof flags["run-id"] === "string",
90
144
  budgetTokens: intFlag(flags, "budget"),
91
145
  maxAgents: intFlag(flags, "max-agents"),
92
146
  agentTimeoutMs: timeoutSeconds ? timeoutSeconds * 1000 : undefined,
@@ -99,6 +153,126 @@ async function commandRun(positional, flags) {
99
153
  }
100
154
  process.exit(result.status === "ok" ? 0 : 1);
101
155
  }
156
+ function commandRuns(flags) {
157
+ const workspace = process.cwd();
158
+ const rows = listRuns(workspace);
159
+ if (flags.json === true) {
160
+ process.stdout.write(`${JSON.stringify(rows.map(({ runId, stale, status }) => ({ runId, stale, ...(status ?? {}) })), null, 2)}\n`);
161
+ return;
162
+ }
163
+ if (rows.length === 0) {
164
+ process.stdout.write("no runs under .newton/runs\n");
165
+ return;
166
+ }
167
+ const table = [
168
+ ["runId", "status", "agents", "out-tok", "pid", "name"],
169
+ ...rows.map(({ runId, stale, status }) => [
170
+ runId,
171
+ stale ? "stale" : (status?.status ?? "—"),
172
+ status ? String(status.agents) : "",
173
+ status ? String(status.outputTokens) : "",
174
+ status ? String(status.pid) : "",
175
+ status?.name ?? "",
176
+ ]),
177
+ ];
178
+ const widths = table[0].map((_, index) => Math.max(...table.map((row) => row[index].length)));
179
+ for (const row of table) {
180
+ process.stdout.write(`${row.map((value, index) => value.padEnd(widths[index])).join(" ")}\n`);
181
+ }
182
+ }
183
+ async function commandWatch(positional, flags) {
184
+ const runId = positional[0];
185
+ if (!runId)
186
+ fail("newton watch requires a run id");
187
+ const runDir = join(process.cwd(), ".newton", "runs", runId);
188
+ try {
189
+ accessSync(runDir, constants.F_OK);
190
+ }
191
+ catch {
192
+ fail(`no run "${runId}"`);
193
+ }
194
+ const journalPath = join(runDir, "journal.jsonl");
195
+ const json = flags.json === true;
196
+ const reporter = new HumanReporter();
197
+ let startT;
198
+ let exitCode = 0;
199
+ const keepAlive = setInterval(() => { }, 1 << 30);
200
+ let handle;
201
+ handle = tailJournal(journalPath, (entry) => {
202
+ if (json) {
203
+ process.stdout.write(`${JSON.stringify(entry)}\n`);
204
+ }
205
+ else {
206
+ switch (entry.type) {
207
+ case "workflow_start":
208
+ startT = Date.parse(String(entry.t));
209
+ reporter.start({ name: String(entry.name), description: "" }, String(entry.runId), dirname(journalPath));
210
+ break;
211
+ case "phase":
212
+ reporter.phase(String(entry.title));
213
+ break;
214
+ case "log":
215
+ reporter.log(String(entry.message));
216
+ break;
217
+ case "agent_start":
218
+ reporter.agentStart(Number(entry.id), String(entry.label), entry.spec, entry.phase, false);
219
+ break;
220
+ case "agent_end": {
221
+ if (entry.cached === true) {
222
+ reporter.agentStart(Number(entry.id), String(entry.label), {}, entry.phase, true);
223
+ }
224
+ const ok = entry.status === "ok";
225
+ reporter.agentEnd(Number(entry.id), ok, Number(entry.seconds ?? 0), ok ? (entry.cached ? "cached" : `${entry.toolCalls ?? 0} tools`) : String(entry.error ?? "error"));
226
+ break;
227
+ }
228
+ case "workflow_child_start":
229
+ reporter.log(`▸ workflow ${entry.name}`);
230
+ break;
231
+ case "workflow_child_end":
232
+ reporter.log(`${entry.status === "ok" ? "✔" : "✖"} workflow ${entry.name}`);
233
+ break;
234
+ }
235
+ }
236
+ if (entry.type === "workflow_end") {
237
+ const endT = Date.parse(String(entry.t));
238
+ exitCode = entry.status === "ok" ? 0 : 1;
239
+ if (!json) {
240
+ reporter.done({
241
+ runId,
242
+ status: entry.status,
243
+ result: entry.result,
244
+ error: entry.error,
245
+ agents: Number(entry.agents ?? 0),
246
+ outputTokens: Number(entry.outputTokens ?? 0),
247
+ durationMs: startT ? Math.max(0, endT - startT) : 0,
248
+ });
249
+ }
250
+ clearInterval(keepAlive);
251
+ handle.stop();
252
+ process.exit(exitCode);
253
+ }
254
+ });
255
+ }
256
+ function commandSkip(positional) {
257
+ const runId = positional[0];
258
+ const rawAgentId = positional[1];
259
+ if (!runId || !rawAgentId)
260
+ fail("newton skip requires a run id and agent id");
261
+ const agentId = Number(rawAgentId);
262
+ if (!Number.isInteger(agentId) || agentId <= 0)
263
+ fail("agentId requires a positive integer");
264
+ try {
265
+ requestSkip(process.cwd(), runId, agentId);
266
+ }
267
+ catch (error) {
268
+ fail(error.message);
269
+ }
270
+ process.stdout.write(`skip requested: run ${runId} agent #${agentId}\n`);
271
+ const status = readStatus(join(process.cwd(), ".newton", "runs", runId));
272
+ if (status?.status !== "running") {
273
+ process.stderr.write(`warning: run ${runId} status is ${status?.status ?? "unknown"}; skip may have no effect\n`);
274
+ }
275
+ }
102
276
  function commandValidate(positional) {
103
277
  const file = positional[0];
104
278
  if (!file)
@@ -184,6 +358,15 @@ async function main() {
184
358
  case "run":
185
359
  await commandRun(positional, flags);
186
360
  break;
361
+ case "runs":
362
+ commandRuns(flags);
363
+ break;
364
+ case "watch":
365
+ await commandWatch(positional, flags);
366
+ break;
367
+ case "skip":
368
+ commandSkip(positional);
369
+ break;
187
370
  case "validate":
188
371
  try {
189
372
  commandValidate(positional);
@@ -0,0 +1,169 @@
1
+ import { closeSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, statSync, writeFileSync, } from "node:fs";
2
+ import { join } from "node:path";
3
+ export function writeStatus(runDir, status) {
4
+ const tmpPath = join(runDir, "status.json.tmp");
5
+ const statusPath = join(runDir, "status.json");
6
+ writeFileSync(tmpPath, `${JSON.stringify(status, null, 2)}\n`, "utf8");
7
+ renameSync(tmpPath, statusPath);
8
+ }
9
+ export function readStatus(runDir) {
10
+ try {
11
+ return JSON.parse(readFileSync(join(runDir, "status.json"), "utf8"));
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ }
17
+ export function pidAlive(pid) {
18
+ try {
19
+ process.kill(pid, 0);
20
+ return true;
21
+ }
22
+ catch (error) {
23
+ return errorCode(error) === "EPERM";
24
+ }
25
+ }
26
+ export function listRuns(workspace) {
27
+ const runsDir = join(workspace, ".newton", "runs");
28
+ let runIds;
29
+ try {
30
+ runIds = readdirSync(runsDir, { withFileTypes: true })
31
+ .filter((entry) => entry.isDirectory())
32
+ .map((entry) => entry.name)
33
+ .sort()
34
+ .reverse();
35
+ }
36
+ catch {
37
+ return [];
38
+ }
39
+ return runIds.map((runId) => {
40
+ const status = readStatus(join(runsDir, runId));
41
+ return {
42
+ runId,
43
+ status,
44
+ stale: status?.status === "running" && !pidAlive(status.pid),
45
+ };
46
+ });
47
+ }
48
+ export function requestSkip(workspace, runId, agentId) {
49
+ const runDir = join(workspace, ".newton", "runs", runId);
50
+ try {
51
+ if (!statSync(runDir).isDirectory()) {
52
+ throw new Error("not a directory");
53
+ }
54
+ }
55
+ catch {
56
+ throw new Error(`no run "${runId}" under .newton/runs`);
57
+ }
58
+ const controlDir = join(runDir, "control");
59
+ mkdirSync(controlDir, { recursive: true });
60
+ writeFileSync(join(controlDir, `skip-${agentId}`), "", "utf8");
61
+ }
62
+ export function readSkipRequests(runDir) {
63
+ const ids = new Set();
64
+ let entries;
65
+ try {
66
+ entries = readdirSync(join(runDir, "control"));
67
+ }
68
+ catch {
69
+ return ids;
70
+ }
71
+ for (const entry of entries) {
72
+ const match = /^skip-(\d+)$/.exec(entry);
73
+ if (!match)
74
+ continue;
75
+ const id = Number(match[1]);
76
+ if (Number.isInteger(id))
77
+ ids.add(id);
78
+ }
79
+ return ids;
80
+ }
81
+ export function tailJournal(journalPath, onEntry, pollMs = 300) {
82
+ let offset = 0;
83
+ let partial = Buffer.alloc(0);
84
+ let stopped = false;
85
+ function emitLine(line) {
86
+ const text = line.toString("utf8");
87
+ if (!text.trim())
88
+ return;
89
+ let entry;
90
+ try {
91
+ entry = JSON.parse(text);
92
+ }
93
+ catch {
94
+ // Ignore incomplete/corrupt journal lines; later valid lines still stream.
95
+ return;
96
+ }
97
+ if (entry !== null && typeof entry === "object" && !Array.isArray(entry)) {
98
+ onEntry(entry);
99
+ }
100
+ }
101
+ function consume(chunk) {
102
+ const data = partial.length === 0 ? chunk : Buffer.concat([partial, chunk]);
103
+ let lineStart = 0;
104
+ for (let i = 0; i < data.length; i++) {
105
+ if (data[i] !== 10)
106
+ continue;
107
+ emitLine(data.subarray(lineStart, i));
108
+ lineStart = i + 1;
109
+ }
110
+ partial = lineStart < data.length ? data.subarray(lineStart) : Buffer.alloc(0);
111
+ }
112
+ function tick() {
113
+ if (stopped)
114
+ return;
115
+ let size;
116
+ try {
117
+ const stat = statSync(journalPath);
118
+ if (!stat.isFile())
119
+ return;
120
+ size = stat.size;
121
+ }
122
+ catch {
123
+ return;
124
+ }
125
+ if (size < offset) {
126
+ offset = 0;
127
+ partial = Buffer.alloc(0);
128
+ }
129
+ if (size === offset)
130
+ return;
131
+ let fd;
132
+ try {
133
+ fd = openSync(journalPath, "r");
134
+ const length = size - offset;
135
+ const chunk = Buffer.alloc(length);
136
+ const bytesRead = readSync(fd, chunk, 0, length, offset);
137
+ if (bytesRead <= 0)
138
+ return;
139
+ offset += bytesRead;
140
+ consume(bytesRead === chunk.length ? chunk : chunk.subarray(0, bytesRead));
141
+ }
142
+ catch {
143
+ return;
144
+ }
145
+ finally {
146
+ if (fd !== undefined) {
147
+ try {
148
+ closeSync(fd);
149
+ }
150
+ catch {
151
+ // A close failure should not stop journal polling.
152
+ }
153
+ }
154
+ }
155
+ }
156
+ const timer = setInterval(tick, pollMs);
157
+ timer.unref();
158
+ return {
159
+ stop() {
160
+ stopped = true;
161
+ clearInterval(timer);
162
+ },
163
+ };
164
+ }
165
+ function errorCode(error) {
166
+ return typeof error === "object" && error !== null && "code" in error
167
+ ? error.code
168
+ : undefined;
169
+ }