parallel-codex-tui 0.3.1 → 0.4.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 +19 -8
- package/dist/cli.js +22 -2
- package/dist/core/session-index.js +123 -10
- package/dist/core/session-manager.js +15 -4
- package/dist/core/task-report.js +510 -0
- package/dist/core/task-search.js +142 -0
- package/dist/orchestrator/orchestrator.js +26 -4
- package/dist/supervisor/client.js +442 -0
- package/dist/supervisor/protocol.js +177 -0
- package/dist/supervisor/runner.js +238 -0
- package/dist/supervisor/store.js +213 -0
- package/dist/tui/App.js +169 -36
- package/dist/tui/AppShell.js +7 -3
- package/dist/tui/InputBar.js +20 -6
- package/dist/tui/TaskSessionsView.js +30 -9
- package/dist/version.js +1 -1
- package/dist/workers/mock-adapter.js +18 -0
- package/package.json +1 -1
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { createRuntime } from "../bootstrap.js";
|
|
2
|
+
import { readProcessStartToken } from "../core/process-identity.js";
|
|
3
|
+
import { appendSupervisorEvent, readSupervisorCommands, readSupervisorRunRequest, readSupervisorRunState, supervisorRunFiles, writeSupervisorRunState } from "./store.js";
|
|
4
|
+
const COMMAND_POLL_MS = 100;
|
|
5
|
+
export async function runSupervisorJob(runDir) {
|
|
6
|
+
const files = supervisorRunFiles(runDir);
|
|
7
|
+
const request = await readSupervisorRunRequest(files);
|
|
8
|
+
let state = await readSupervisorRunState(files);
|
|
9
|
+
let runtime = null;
|
|
10
|
+
const controller = new AbortController();
|
|
11
|
+
let commandLoopStopped = false;
|
|
12
|
+
let writeQueue = Promise.resolve();
|
|
13
|
+
let eventSequence = 0;
|
|
14
|
+
const enqueue = (operation) => {
|
|
15
|
+
writeQueue = writeQueue.then(operation, operation);
|
|
16
|
+
};
|
|
17
|
+
const replaceState = (update) => {
|
|
18
|
+
state = update(state);
|
|
19
|
+
const snapshot = state;
|
|
20
|
+
enqueue(() => writeSupervisorRunState(files, snapshot));
|
|
21
|
+
};
|
|
22
|
+
const emit = (type, payload) => {
|
|
23
|
+
const event = {
|
|
24
|
+
version: 1,
|
|
25
|
+
sequence: eventSequence,
|
|
26
|
+
at: new Date().toISOString(),
|
|
27
|
+
type,
|
|
28
|
+
payload
|
|
29
|
+
};
|
|
30
|
+
eventSequence += 1;
|
|
31
|
+
enqueue(() => appendSupervisorEvent(files, event));
|
|
32
|
+
};
|
|
33
|
+
const stopFromSignal = () => controller.abort();
|
|
34
|
+
process.once("SIGTERM", stopFromSignal);
|
|
35
|
+
process.once("SIGINT", stopFromSignal);
|
|
36
|
+
try {
|
|
37
|
+
const startedAt = new Date().toISOString();
|
|
38
|
+
const processStartToken = await readProcessStartToken(process.pid);
|
|
39
|
+
replaceState((current) => ({
|
|
40
|
+
...current,
|
|
41
|
+
status: "running",
|
|
42
|
+
updated_at: startedAt,
|
|
43
|
+
started_at: startedAt,
|
|
44
|
+
pid: process.pid,
|
|
45
|
+
...(processStartToken ? { process_start_token: processStartToken } : {})
|
|
46
|
+
}));
|
|
47
|
+
await writeQueue;
|
|
48
|
+
runtime = await createRuntime(request.app_root, request.workspace_root);
|
|
49
|
+
const commandLoop = consumeCommands(files, runtime, controller, () => commandLoopStopped, () => {
|
|
50
|
+
replaceState((current) => ({
|
|
51
|
+
...current,
|
|
52
|
+
status: "cancelling",
|
|
53
|
+
updated_at: new Date().toISOString()
|
|
54
|
+
}));
|
|
55
|
+
});
|
|
56
|
+
try {
|
|
57
|
+
const result = await executeRequest(request, runtime, controller.signal, {
|
|
58
|
+
onRouteStart: (value) => emit("route-start", value),
|
|
59
|
+
onRouteProgress: (value) => emit("route-progress", value),
|
|
60
|
+
onRoute: (value) => emit("route", value),
|
|
61
|
+
onStatus: (value) => {
|
|
62
|
+
emit("status", value);
|
|
63
|
+
if (value.taskId !== "main") {
|
|
64
|
+
replaceState((current) => ({
|
|
65
|
+
...current,
|
|
66
|
+
task_id: value.taskId,
|
|
67
|
+
updated_at: new Date().toISOString()
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
onWorker: (value) => emit("worker", value)
|
|
72
|
+
});
|
|
73
|
+
await writeQueue;
|
|
74
|
+
await persistRunMessage(runtime, request, result.summary, result.taskId);
|
|
75
|
+
const finishedAt = new Date().toISOString();
|
|
76
|
+
state = {
|
|
77
|
+
...state,
|
|
78
|
+
status: "completed",
|
|
79
|
+
updated_at: finishedAt,
|
|
80
|
+
finished_at: finishedAt,
|
|
81
|
+
task_id: result.taskId ?? requestedTaskId(request),
|
|
82
|
+
result
|
|
83
|
+
};
|
|
84
|
+
delete state.error;
|
|
85
|
+
await writeSupervisorRunState(files, state);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
await writeQueue;
|
|
89
|
+
const cancelled = controller.signal.aborted || isAbortError(error);
|
|
90
|
+
const summary = cancelled
|
|
91
|
+
? cancellationSummary(request)
|
|
92
|
+
: error instanceof Error ? error.message : String(error);
|
|
93
|
+
try {
|
|
94
|
+
await persistRunMessage(runtime, request, summary, requestedTaskId(request));
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// The terminal state remains recoverable even when chat persistence fails.
|
|
98
|
+
}
|
|
99
|
+
const finishedAt = new Date().toISOString();
|
|
100
|
+
state = {
|
|
101
|
+
...state,
|
|
102
|
+
status: cancelled ? "cancelled" : "failed",
|
|
103
|
+
updated_at: finishedAt,
|
|
104
|
+
finished_at: finishedAt,
|
|
105
|
+
task_id: state.task_id ?? requestedTaskId(request),
|
|
106
|
+
error: summary
|
|
107
|
+
};
|
|
108
|
+
delete state.result;
|
|
109
|
+
await writeSupervisorRunState(files, state);
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
commandLoopStopped = true;
|
|
113
|
+
await commandLoop;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
const finishedAt = new Date().toISOString();
|
|
118
|
+
const summary = error instanceof Error ? error.message : String(error);
|
|
119
|
+
state = {
|
|
120
|
+
...state,
|
|
121
|
+
status: controller.signal.aborted ? "cancelled" : "failed",
|
|
122
|
+
updated_at: finishedAt,
|
|
123
|
+
finished_at: finishedAt,
|
|
124
|
+
error: controller.signal.aborted ? cancellationSummary(request) : summary
|
|
125
|
+
};
|
|
126
|
+
delete state.result;
|
|
127
|
+
try {
|
|
128
|
+
await writeQueue;
|
|
129
|
+
await writeSupervisorRunState(files, state);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// The CLI-level failure handler is the last resort when the run directory is unavailable.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
commandLoopStopped = true;
|
|
137
|
+
process.off("SIGTERM", stopFromSignal);
|
|
138
|
+
process.off("SIGINT", stopFromSignal);
|
|
139
|
+
try {
|
|
140
|
+
runtime?.index.close();
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Process exit closes a damaged or already-closed SQLite handle.
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function executeRequest(request, runtime, signal, callbacks) {
|
|
148
|
+
const common = {
|
|
149
|
+
cwd: request.cwd,
|
|
150
|
+
signal,
|
|
151
|
+
...callbacks
|
|
152
|
+
};
|
|
153
|
+
switch (request.kind) {
|
|
154
|
+
case "handle-request":
|
|
155
|
+
return runtime.orchestrator.handleRequest({
|
|
156
|
+
...common,
|
|
157
|
+
request: request.request,
|
|
158
|
+
route: request.route,
|
|
159
|
+
roleSelection: request.role_selection
|
|
160
|
+
});
|
|
161
|
+
case "handle-task-turn":
|
|
162
|
+
return runtime.orchestrator.handleTaskTurn({
|
|
163
|
+
...common,
|
|
164
|
+
request: request.request,
|
|
165
|
+
taskId: request.task_id,
|
|
166
|
+
route: request.route,
|
|
167
|
+
roleSelection: request.role_selection
|
|
168
|
+
});
|
|
169
|
+
case "answer-task-question":
|
|
170
|
+
return runtime.orchestrator.answerTaskQuestion({
|
|
171
|
+
...common,
|
|
172
|
+
request: request.request,
|
|
173
|
+
taskId: request.task_id,
|
|
174
|
+
route: request.route,
|
|
175
|
+
roleSelection: request.role_selection
|
|
176
|
+
});
|
|
177
|
+
case "retry-task":
|
|
178
|
+
return runtime.orchestrator.retryTask({
|
|
179
|
+
...common,
|
|
180
|
+
taskId: request.task_id
|
|
181
|
+
});
|
|
182
|
+
case "resume-feature":
|
|
183
|
+
return runtime.orchestrator.resumeFeature({
|
|
184
|
+
...common,
|
|
185
|
+
taskId: request.task_id,
|
|
186
|
+
featureId: request.feature_id
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function consumeCommands(files, runtime, controller, stopped, onCancelling) {
|
|
191
|
+
const handled = new Set();
|
|
192
|
+
while (!stopped()) {
|
|
193
|
+
try {
|
|
194
|
+
const commands = await readSupervisorCommands(files);
|
|
195
|
+
for (const command of commands) {
|
|
196
|
+
if (handled.has(command.id)) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
handled.add(command.id);
|
|
200
|
+
if (command.type === "cancel-run") {
|
|
201
|
+
onCancelling();
|
|
202
|
+
controller.abort();
|
|
203
|
+
}
|
|
204
|
+
else if (command.type === "cancel-feature") {
|
|
205
|
+
await runtime.orchestrator.cancelFeature(command.task_id, command.feature_id);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
await runtime.orchestrator.pauseFeature(command.task_id, command.feature_id);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// A concurrent append can expose a partial final JSONL row; the next poll retries it.
|
|
214
|
+
}
|
|
215
|
+
await delay(COMMAND_POLL_MS);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function persistRunMessage(runtime, request, text, resultTaskId) {
|
|
219
|
+
await runtime.sessions.appendChatMessage({
|
|
220
|
+
from: "system",
|
|
221
|
+
text,
|
|
222
|
+
taskId: resultTaskId ?? requestedTaskId(request) ?? undefined
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
function requestedTaskId(request) {
|
|
226
|
+
return request.kind === "handle-request" ? null : request.task_id;
|
|
227
|
+
}
|
|
228
|
+
function cancellationSummary(request) {
|
|
229
|
+
return request.kind === "retry-task" || request.kind === "resume-feature"
|
|
230
|
+
? "cancelled · retry stopped"
|
|
231
|
+
: "cancelled · request stopped";
|
|
232
|
+
}
|
|
233
|
+
function isAbortError(error) {
|
|
234
|
+
return error instanceof Error && error.name === "AbortError";
|
|
235
|
+
}
|
|
236
|
+
function delay(ms) {
|
|
237
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
238
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { appendJsonLine, ensureDir, pathExists, readJson, readTextIfExists, removeIfExists, writeJson } from "../core/file-store.js";
|
|
5
|
+
import { processIsAlive, readProcessStartToken } from "../core/process-identity.js";
|
|
6
|
+
import { SupervisorCommandSchema, SupervisorControllerSchema, SupervisorRunEventSchema, SupervisorRunRequestSchema, SupervisorRunStateSchema } from "./protocol.js";
|
|
7
|
+
const SUPERVISOR_DIR = "supervisor";
|
|
8
|
+
const RUNS_DIR = "runs";
|
|
9
|
+
const REQUEST_FILE = "request.json";
|
|
10
|
+
const STATE_FILE = "state.json";
|
|
11
|
+
const EVENTS_FILE = "events.jsonl";
|
|
12
|
+
const COMMANDS_FILE = "commands.jsonl";
|
|
13
|
+
const CONTROLLER_FILE = "controller.json";
|
|
14
|
+
const ACKNOWLEDGED_FILE = "acknowledged.json";
|
|
15
|
+
export function supervisorRoot(workspaceRoot, dataDir) {
|
|
16
|
+
return join(workspaceRoot, dataDir, SUPERVISOR_DIR);
|
|
17
|
+
}
|
|
18
|
+
export function supervisorRunsRoot(workspaceRoot, dataDir) {
|
|
19
|
+
return join(supervisorRoot(workspaceRoot, dataDir), RUNS_DIR);
|
|
20
|
+
}
|
|
21
|
+
export function supervisorRunFiles(runDir) {
|
|
22
|
+
return {
|
|
23
|
+
dir: runDir,
|
|
24
|
+
requestPath: join(runDir, REQUEST_FILE),
|
|
25
|
+
statePath: join(runDir, STATE_FILE),
|
|
26
|
+
eventsPath: join(runDir, EVENTS_FILE),
|
|
27
|
+
commandsPath: join(runDir, COMMANDS_FILE),
|
|
28
|
+
controllerPath: join(runDir, CONTROLLER_FILE),
|
|
29
|
+
acknowledgedPath: join(runDir, ACKNOWLEDGED_FILE)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function createSupervisorRunId(now = new Date()) {
|
|
33
|
+
const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
34
|
+
return `run-${stamp}-${randomUUID().slice(0, 8)}`;
|
|
35
|
+
}
|
|
36
|
+
export async function createSupervisorRun(workspaceRoot, dataDir, request) {
|
|
37
|
+
const runsRoot = supervisorRunsRoot(workspaceRoot, dataDir);
|
|
38
|
+
const files = supervisorRunFiles(join(runsRoot, request.run_id));
|
|
39
|
+
const stagingFiles = supervisorRunFiles(join(runsRoot, `.${request.run_id}.${process.pid}.${randomUUID().slice(0, 8)}.pending`));
|
|
40
|
+
const now = request.created_at;
|
|
41
|
+
await ensureDir(stagingFiles.dir);
|
|
42
|
+
try {
|
|
43
|
+
await writeJson(stagingFiles.requestPath, SupervisorRunRequestSchema.parse(request));
|
|
44
|
+
await writeSupervisorRunState(stagingFiles, {
|
|
45
|
+
version: 1,
|
|
46
|
+
run_id: request.run_id,
|
|
47
|
+
kind: request.kind,
|
|
48
|
+
status: "queued",
|
|
49
|
+
app_root: request.app_root,
|
|
50
|
+
workspace_root: request.workspace_root,
|
|
51
|
+
created_at: now,
|
|
52
|
+
updated_at: now,
|
|
53
|
+
...(request.kind === "handle-request" ? {} : { task_id: request.task_id })
|
|
54
|
+
});
|
|
55
|
+
await rename(stagingFiles.dir, files.dir);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
await rm(stagingFiles.dir, { recursive: true, force: true });
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
return files;
|
|
62
|
+
}
|
|
63
|
+
export async function readSupervisorRunRequest(files) {
|
|
64
|
+
return readJson(files.requestPath, SupervisorRunRequestSchema);
|
|
65
|
+
}
|
|
66
|
+
export async function readSupervisorRunState(files) {
|
|
67
|
+
return readJson(files.statePath, SupervisorRunStateSchema);
|
|
68
|
+
}
|
|
69
|
+
export async function writeSupervisorRunState(files, state) {
|
|
70
|
+
await writeJson(files.statePath, SupervisorRunStateSchema.parse(state));
|
|
71
|
+
}
|
|
72
|
+
export async function appendSupervisorEvent(files, event) {
|
|
73
|
+
await appendJsonLine(files.eventsPath, SupervisorRunEventSchema.parse(event));
|
|
74
|
+
}
|
|
75
|
+
export async function readSupervisorEvents(files) {
|
|
76
|
+
return readJsonLines(files.eventsPath, SupervisorRunEventSchema);
|
|
77
|
+
}
|
|
78
|
+
export async function appendSupervisorCommand(files, command) {
|
|
79
|
+
await appendJsonLine(files.commandsPath, SupervisorCommandSchema.parse(command));
|
|
80
|
+
}
|
|
81
|
+
export async function readSupervisorCommands(files) {
|
|
82
|
+
return readJsonLines(files.commandsPath, SupervisorCommandSchema);
|
|
83
|
+
}
|
|
84
|
+
export async function acknowledgeSupervisorRun(files) {
|
|
85
|
+
await writeJson(files.acknowledgedPath, {
|
|
86
|
+
version: 1,
|
|
87
|
+
acknowledged_at: new Date().toISOString(),
|
|
88
|
+
pid: process.pid
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
export async function supervisorRunIsAcknowledged(files) {
|
|
92
|
+
return pathExists(files.acknowledgedPath);
|
|
93
|
+
}
|
|
94
|
+
export async function listSupervisorRuns(workspaceRoot, dataDir) {
|
|
95
|
+
const root = supervisorRunsRoot(workspaceRoot, dataDir);
|
|
96
|
+
if (!(await pathExists(root))) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
const names = await readdir(root);
|
|
100
|
+
const records = [];
|
|
101
|
+
for (const name of names) {
|
|
102
|
+
if (!name.startsWith("run-")) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const files = supervisorRunFiles(join(root, name));
|
|
106
|
+
const state = await readValidJson(files.statePath, SupervisorRunStateSchema);
|
|
107
|
+
if (state) {
|
|
108
|
+
records.push({ files, state });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return records.sort((left, right) => left.state.created_at.localeCompare(right.state.created_at));
|
|
112
|
+
}
|
|
113
|
+
export function supervisorRunIsTerminal(state) {
|
|
114
|
+
return state.status === "completed" || state.status === "failed" || state.status === "cancelled";
|
|
115
|
+
}
|
|
116
|
+
export async function supervisorRunProcessIsActive(state) {
|
|
117
|
+
if (!state.pid || !processIsAlive(state.pid)) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
if (!state.process_start_token) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
return await readProcessStartToken(state.pid) === state.process_start_token;
|
|
124
|
+
}
|
|
125
|
+
export async function claimSupervisorController(files, options = {}) {
|
|
126
|
+
const pid = options.pid ?? process.pid;
|
|
127
|
+
const owner = SupervisorControllerSchema.parse({
|
|
128
|
+
version: 1,
|
|
129
|
+
controller_id: options.controllerId ?? randomUUID(),
|
|
130
|
+
pid,
|
|
131
|
+
acquired_at: (options.now ?? (() => new Date()))().toISOString(),
|
|
132
|
+
...await optionalProcessStartToken(pid)
|
|
133
|
+
});
|
|
134
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
135
|
+
if (await writeJsonExclusive(files.controllerPath, owner)) {
|
|
136
|
+
return {
|
|
137
|
+
owner,
|
|
138
|
+
release: () => releaseSupervisorController(files, owner.controller_id)
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const current = await readValidJson(files.controllerPath, SupervisorControllerSchema);
|
|
142
|
+
if (current && await supervisorControllerIsActive(current)) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
await removeControllerIfOwned(files.controllerPath, current?.controller_id);
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
async function releaseSupervisorController(files, controllerId) {
|
|
150
|
+
await removeControllerIfOwned(files.controllerPath, controllerId);
|
|
151
|
+
}
|
|
152
|
+
async function removeControllerIfOwned(path, controllerId) {
|
|
153
|
+
if (controllerId) {
|
|
154
|
+
const current = await readValidJson(path, SupervisorControllerSchema);
|
|
155
|
+
if (!current || current.controller_id !== controllerId) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
await removeIfExists(path);
|
|
160
|
+
}
|
|
161
|
+
async function supervisorControllerIsActive(controller) {
|
|
162
|
+
if (!processIsAlive(controller.pid)) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
if (!controller.process_start_token) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
return await readProcessStartToken(controller.pid) === controller.process_start_token;
|
|
169
|
+
}
|
|
170
|
+
async function optionalProcessStartToken(pid) {
|
|
171
|
+
const token = await readProcessStartToken(pid);
|
|
172
|
+
return token ? { process_start_token: token } : {};
|
|
173
|
+
}
|
|
174
|
+
async function writeJsonExclusive(path, value) {
|
|
175
|
+
try {
|
|
176
|
+
await ensureDir(dirname(path));
|
|
177
|
+
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", flag: "wx" });
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (error.code === "EEXIST") {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async function readJsonLines(path, schema) {
|
|
188
|
+
const text = await readTextIfExists(path);
|
|
189
|
+
const records = [];
|
|
190
|
+
for (const line of text.split("\n")) {
|
|
191
|
+
if (!line.trim()) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
const parsed = schema.safeParse(JSON.parse(line));
|
|
196
|
+
if (parsed.success) {
|
|
197
|
+
records.push(parsed.data);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// A partial final line is ignored until the next poll completes it.
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return records;
|
|
205
|
+
}
|
|
206
|
+
async function readValidJson(path, schema) {
|
|
207
|
+
try {
|
|
208
|
+
return await readJson(path, schema);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
}
|