arisa 5.1.68 → 5.2.7

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.
Files changed (77) hide show
  1. package/README.md +7 -4
  2. package/package.json +1 -1
  3. package/src/core/agent/agent-manager.js +46 -6
  4. package/src/core/agent/agent-session-lifecycle.js +80 -3
  5. package/src/core/agent/core-tools.js +1 -1
  6. package/src/core/agent/pi-auth-login.js +1 -1
  7. package/src/core/agent/pi-runtime.js +1 -1
  8. package/src/core/agent/runtime-context.js +1 -1
  9. package/src/core/agent/worker-heap-circuit-breaker.js +122 -0
  10. package/src/core/artifacts/artifact-store.js +1 -1
  11. package/src/core/capabilities/capability-service.js +1 -1
  12. package/src/core/config/config-defaults.js +30 -1
  13. package/src/core/config/config-store.js +1 -1
  14. package/src/core/conversation/session-seed-store.js +1 -1
  15. package/src/core/tasks/task-store.js +1 -1
  16. package/src/core/tools/daemon-client.js +180 -0
  17. package/src/core/tools/daemon-processes.js +19 -3
  18. package/src/core/tools/daemon-protocol.js +72 -0
  19. package/src/core/tools/daemon-runtime.js +13 -490
  20. package/src/core/tools/daemon-worker.js +310 -0
  21. package/src/core/tools/ipc-client.js +2 -2
  22. package/src/core/tools/memory-pressure.js +56 -0
  23. package/src/core/tools/official-tool-installer.js +1 -1
  24. package/src/core/tools/tool-config.js +1 -1
  25. package/src/core/tools/tool-process-output.js +100 -0
  26. package/src/core/tools/tool-process-runner.js +175 -0
  27. package/src/core/tools/tool-registry.js +99 -187
  28. package/src/core/tools/tool-resource-note-store.js +1 -1
  29. package/src/core/tools/tool-usage-store.js +1 -1
  30. package/src/core/tools/weighted-resource-governor.js +188 -38
  31. package/src/index.js +14 -2
  32. package/src/official-tools.lock.json +424 -50
  33. package/src/platform/paths.js +152 -0
  34. package/src/runtime/bootstrap-cli.js +121 -0
  35. package/src/runtime/bootstrap-config.js +97 -0
  36. package/src/runtime/bootstrap-telegram.js +325 -0
  37. package/src/runtime/bootstrap.js +6 -543
  38. package/src/runtime/doctor.js +6 -3
  39. package/src/runtime/flush.js +1 -1
  40. package/src/runtime/ipc/ipc-server.js +1 -1
  41. package/src/runtime/log-viewer.js +1 -1
  42. package/src/runtime/oom-protection.js +20 -0
  43. package/src/runtime/paths.js +3 -151
  44. package/src/runtime/restart-receipt.js +1 -1
  45. package/src/runtime/service-manager.js +1 -1
  46. package/src/runtime/service-supervisor.js +14 -0
  47. package/src/runtime/slave-cli.js +1 -1
  48. package/src/runtime/tool-process-supervisor.js +1 -1
  49. package/src/runtime/tui.js +200 -0
  50. package/src/runtime/update-manager.js +1 -1
  51. package/src/runtime/worker-recovery-report.js +142 -0
  52. package/src/transport/telegram/bot.js +42 -320
  53. package/src/transport/telegram/prompt-builders.js +8 -3
  54. package/src/transport/telegram/telegram-prompt-controller.js +346 -0
  55. package/src/transport/telegram/workspace-topic-store.js +1 -1
  56. package/test/agent-session-lifecycle.test.js +92 -0
  57. package/test/architecture-boundaries.test.js +29 -0
  58. package/test/bootstrap.test.js +65 -0
  59. package/test/daemon-process-invocation.test.js +27 -0
  60. package/test/daemon-runtime.test.js +36 -1
  61. package/test/doctor.test.js +22 -0
  62. package/test/memory-pressure.test.js +36 -0
  63. package/test/model-selection.test.js +11 -1
  64. package/test/official-tool-dependencies.test.js +1 -1
  65. package/test/official-tool-installer.test.js +18 -1
  66. package/test/oom-protection.test.js +32 -0
  67. package/test/paths.test.js +7 -0
  68. package/test/pi-compaction.test.js +9 -0
  69. package/test/service-manager.test.js +6 -1
  70. package/test/telegram-prompt-controller.test.js +81 -0
  71. package/test/telegram-text-artifact.test.js +30 -0
  72. package/test/tool-registry-run.test.js +108 -4
  73. package/test/tui.test.js +41 -0
  74. package/test/weighted-resource-governor.test.js +97 -5
  75. package/test/worker-heap-circuit-breaker.test.js +79 -0
  76. package/test/worker-recovery-report.test.js +69 -0
  77. package/test-fixtures/fake-daemon.js +5 -0
@@ -1,244 +1,21 @@
1
- import crypto from "node:crypto";
2
- import net from "node:net";
3
- import { chmod, mkdir, readFile, readdir, rename, rm, unlink } from "node:fs/promises";
4
- import path from "node:path";
5
1
  import {
6
2
  daemonPaths,
7
- ensureDaemonCapability,
8
3
  isProcessAlive,
9
4
  readJson,
10
5
  startManagedDaemon,
11
- stopManagedDaemon,
12
- writeDaemonStatus,
13
- writeJson
6
+ stopManagedDaemon
14
7
  } from "./daemon-processes.js";
15
8
  import { loadDaemonPolicy } from "./daemon-policy.js";
9
+ import { enqueueDaemonJob, submitDaemonControl } from "./daemon-client.js";
10
+ import { createDaemonWorker } from "./daemon-worker.js";
16
11
 
17
- const CONTROL_FIELD = "__daemon";
18
- export const DAEMON_PROTOCOL_VERSION = 1;
19
- export const DAEMON_EVENT_TYPES = Object.freeze(["accepted", "progress", "chunk", "completed", "failed"]);
20
- const TERMINAL_EVENT_TYPES = new Set(["completed", "failed"]);
12
+ export { submitDaemonControl } from "./daemon-client.js";
13
+ export { DAEMON_EVENT_TYPES, DAEMON_PROTOCOL_VERSION } from "./daemon-protocol.js";
21
14
 
22
15
  function sleep(ms) {
23
16
  return new Promise((resolve) => setTimeout(resolve, ms));
24
17
  }
25
18
 
26
- async function withTimeout(work, timeoutMs, message) {
27
- let timer;
28
- try {
29
- return await Promise.race([
30
- Promise.resolve().then(work),
31
- new Promise((_, reject) => {
32
- timer = setTimeout(() => {
33
- const error = new Error(message);
34
- error.code = "DAEMON_OPERATION_TIMEOUT";
35
- reject(error);
36
- }, timeoutMs);
37
- })
38
- ]);
39
- } finally {
40
- clearTimeout(timer);
41
- }
42
- }
43
-
44
- function jobPaths(paths, id) {
45
- return {
46
- request: path.join(paths.commandsDir, `${id}.request.json`),
47
- processing: path.join(paths.commandsDir, `${id}.processing.json`),
48
- result: path.join(paths.commandsDir, `${id}.result.json`)
49
- };
50
- }
51
-
52
- function daemonFrame(jobId, type, sequence, payload = {}) {
53
- return { version: DAEMON_PROTOCOL_VERSION, jobId, type, sequence, payload };
54
- }
55
-
56
- function terminalResult(frame) {
57
- if (frame.type === "failed") {
58
- const error = new Error(frame.payload?.error || "Daemon job failed");
59
- if (frame.payload?.code) error.code = frame.payload.code;
60
- throw error;
61
- }
62
- return frame.payload?.output || {};
63
- }
64
-
65
- async function readCapability(paths) {
66
- const token = (await readFile(paths.capabilityFile, "utf8")).trim();
67
- if (!token) throw new Error(`Invalid daemon capability for ${paths.toolName}`);
68
- return token;
69
- }
70
-
71
- async function writeSocketFrame(socket, frame, { maxFrameBytes = 1_048_576, streamBufferBytes = 1_048_576 } = {}) {
72
- if (socket.destroyed || !socket.writable) return false;
73
- const encoded = `${JSON.stringify(frame)}\n`;
74
- if (Buffer.byteLength(encoded, "utf8") > maxFrameBytes) {
75
- throw new Error(`Daemon IPC frame exceeds ${maxFrameBytes} bytes`);
76
- }
77
- if (socket.writableLength >= streamBufferBytes) {
78
- await new Promise((resolve) => {
79
- const finish = () => {
80
- socket.off("drain", finish);
81
- socket.off("close", finish);
82
- socket.off("error", finish);
83
- resolve();
84
- };
85
- socket.once("drain", finish);
86
- socket.once("close", finish);
87
- socket.once("error", finish);
88
- });
89
- if (socket.destroyed || !socket.writable) return false;
90
- }
91
- if (socket.write(encoded)) return true;
92
- await new Promise((resolve) => {
93
- const finish = () => {
94
- socket.off("drain", finish);
95
- socket.off("close", finish);
96
- socket.off("error", finish);
97
- resolve();
98
- };
99
- socket.once("drain", finish);
100
- socket.once("close", finish);
101
- socket.once("error", finish);
102
- });
103
- return !socket.destroyed;
104
- }
105
-
106
- function validateDaemonEvent(frame, { jobId, previousSequence = 0, terminalSeen = false } = {}) {
107
- if (!frame || frame.version !== DAEMON_PROTOCOL_VERSION || frame.jobId !== jobId) {
108
- throw new Error("Invalid daemon event identity or protocol version");
109
- }
110
- if (!DAEMON_EVENT_TYPES.includes(frame.type)) throw new Error(`Invalid daemon event type: ${frame.type}`);
111
- if (!Number.isSafeInteger(frame.sequence) || frame.sequence <= previousSequence) {
112
- throw new Error(`Invalid daemon event sequence for ${jobId}: ${frame.sequence}`);
113
- }
114
- if (terminalSeen) throw new Error(`Daemon job ${jobId} emitted more than one terminal event`);
115
- return { sequence: frame.sequence, terminal: TERMINAL_EVENT_TYPES.has(frame.type) };
116
- }
117
-
118
- async function connectDaemon(paths, request, { timeoutMs, onEvent, maxFrameBytes }) {
119
- const startedAt = Date.now();
120
- const token = await readCapability(paths);
121
- let lastError;
122
- while (Date.now() - startedAt < timeoutMs) {
123
- try {
124
- return await new Promise((resolve, reject) => {
125
- const socket = net.createConnection(paths.socketFile);
126
- let buffer = "";
127
- let sequence = 0;
128
- let terminalSeen = false;
129
- let settled = false;
130
- let observerChain = Promise.resolve();
131
- const remainingMs = Math.max(1, timeoutMs - (Date.now() - startedAt));
132
- const timer = setTimeout(() => finish(reject, Object.assign(
133
- new Error(`${paths.toolName} daemon job timed out after ${timeoutMs}ms`),
134
- { code: "DAEMON_JOB_TIMEOUT" }
135
- )), remainingMs);
136
- timer.unref?.();
137
-
138
- function finish(fn, value) {
139
- if (settled) return;
140
- settled = true;
141
- clearTimeout(timer);
142
- socket.destroy();
143
- fn(value);
144
- }
145
-
146
- socket.setEncoding("utf8");
147
- socket.once("connect", () => {
148
- socket.write(`${JSON.stringify({
149
- version: DAEMON_PROTOCOL_VERSION,
150
- type: "submit",
151
- jobId: request.jobId,
152
- capabilityToken: token
153
- })}\n`);
154
- });
155
- socket.on("data", (chunk) => {
156
- buffer += chunk;
157
- if (Buffer.byteLength(buffer, "utf8") > maxFrameBytes && !buffer.includes("\n")) {
158
- finish(reject, new Error(`Daemon IPC frame exceeds ${maxFrameBytes} bytes`));
159
- return;
160
- }
161
- let newlineIndex = buffer.indexOf("\n");
162
- while (newlineIndex !== -1) {
163
- const line = buffer.slice(0, newlineIndex).trim();
164
- buffer = buffer.slice(newlineIndex + 1);
165
- newlineIndex = buffer.indexOf("\n");
166
- if (!line) continue;
167
- let frame;
168
- try {
169
- frame = JSON.parse(line);
170
- const validated = validateDaemonEvent(frame, {
171
- jobId: request.jobId,
172
- previousSequence: sequence,
173
- terminalSeen
174
- });
175
- sequence = validated.sequence;
176
- terminalSeen = validated.terminal;
177
- } catch (error) {
178
- finish(reject, error);
179
- return;
180
- }
181
- observerChain = observerChain.then(() => onEvent?.(frame));
182
- if (terminalSeen) {
183
- observerChain.then(() => {
184
- try {
185
- finish(resolve, terminalResult(frame));
186
- } catch (error) {
187
- finish(reject, error);
188
- }
189
- }, (error) => finish(reject, error));
190
- }
191
- }
192
- });
193
- socket.once("error", (error) => finish(reject, error));
194
- socket.once("close", () => {
195
- if (!settled) finish(reject, new Error("Daemon IPC connection closed before terminal result"));
196
- });
197
- });
198
- } catch (error) {
199
- lastError = error;
200
- if (!["ENOENT", "ECONNREFUSED"].includes(error?.code)) throw error;
201
- await sleep(Math.min(25, Math.max(1, timeoutMs - (Date.now() - startedAt))));
202
- }
203
- }
204
- const error = new Error(`${paths.toolName} daemon IPC was unavailable after ${timeoutMs}ms`);
205
- error.code = lastError?.code || "DAEMON_IPC_UNAVAILABLE";
206
- throw error;
207
- }
208
-
209
- async function enqueue(paths, payload, { control = false, timeoutMs, onEvent, jobId, maxFrameBytes } = {}) {
210
- await mkdir(paths.commandsDir, { recursive: true });
211
- const id = jobId || `${control ? "control" : "job"}-${crypto.randomUUID()}`;
212
- const files = jobPaths(paths, id);
213
- const existingResult = await readJson(files.result, null);
214
- if (existingResult?.terminal) {
215
- await onEvent?.(existingResult.terminal);
216
- return terminalResult(existingResult.terminal);
217
- }
218
- const existingRequest = await readJson(files.request, null);
219
- const existingAccepted = await readJson(files.processing, null);
220
- if (!existingRequest && !existingAccepted) {
221
- await writeJson(files.request, {
222
- id,
223
- status: "queued",
224
- queuedAt: new Date().toISOString(),
225
- payload
226
- });
227
- }
228
- return connectDaemon(paths, { jobId: id }, { timeoutMs, onEvent, maxFrameBytes });
229
- }
230
-
231
- export async function submitDaemonControl(record, operation, { timeoutMs, onEvent } = {}) {
232
- const paths = daemonPaths({ toolName: record.toolName, scope: record.scope });
233
- const policy = await loadDaemonPolicy();
234
- return enqueue(paths, { [CONTROL_FIELD]: { operation } }, {
235
- control: true,
236
- timeoutMs: timeoutMs ?? policy.healthTimeoutMs,
237
- onEvent,
238
- maxFrameBytes: policy.ipcFrameBytes || 1_048_576
239
- });
240
- }
241
-
242
19
  export function isDaemonReady(status, pid, policy, now = Date.now()) {
243
20
  if (status.state !== "ready" || !isProcessAlive(pid)) return false;
244
21
  const heartbeatAt = new Date(status.heartbeatAt || 0).getTime();
@@ -258,20 +35,7 @@ export function createDaemonRuntime({
258
35
  }) {
259
36
  const paths = daemonPaths({ toolName, scope });
260
37
  const registration = { toolName, entryPath, scope: paths.scope, startupContext, autoStart };
261
- let statusWrite = Promise.resolve();
262
-
263
- async function ensure() {
264
- await mkdir(paths.commandsDir, { recursive: true });
265
- }
266
-
267
- async function getPid() {
268
- return (await readJson(paths.pidFile, {})).pid;
269
- }
270
-
271
- async function writeStatus(patch) {
272
- statusWrite = statusWrite.catch(() => {}).then(() => writeDaemonStatus(paths, patch));
273
- return statusWrite;
274
- }
38
+ const worker = createDaemonWorker({ toolName, paths });
275
39
 
276
40
  async function start() {
277
41
  return startManagedDaemon({ ...registration, beforeStart });
@@ -287,7 +51,7 @@ export function createDaemonRuntime({
287
51
  const startTime = Date.now();
288
52
  while (Date.now() - startTime < effectiveTimeoutMs) {
289
53
  const status = await readJson(paths.statusFile, {});
290
- const pid = await getPid();
54
+ const pid = await worker.getPid();
291
55
  if (isDaemonReady(status, pid, policy)) return status;
292
56
  if (status.state === "failed") throw new Error(status.message || `${toolName} daemon failed`);
293
57
  await sleep(policy.queuePollIntervalMs);
@@ -298,7 +62,7 @@ export function createDaemonRuntime({
298
62
  async function ensureReady({ timeoutMs } = {}) {
299
63
  const policy = await loadDaemonPolicy();
300
64
  const status = await readJson(paths.statusFile, {});
301
- const pid = await getPid();
65
+ const pid = await worker.getPid();
302
66
  if (isDaemonReady(status, pid, policy)) return status;
303
67
  await submitDaemonControl(registration, "health", { timeoutMs: timeoutMs ?? policy.healthTimeoutMs });
304
68
  return waitReady({ timeoutMs: timeoutMs ?? policy.startupTimeoutMs });
@@ -314,7 +78,7 @@ export function createDaemonRuntime({
314
78
  const policy = await loadDaemonPolicy();
315
79
  await start();
316
80
  if (requireReady) await ensureReady({ timeoutMs: readyTimeoutMs });
317
- return enqueue(paths, payload, {
81
+ return enqueueDaemonJob(paths, payload, {
318
82
  timeoutMs: timeoutMs ?? policy.startupTimeoutMs,
319
83
  onEvent,
320
84
  jobId,
@@ -322,258 +86,17 @@ export function createDaemonRuntime({
322
86
  });
323
87
  }
324
88
 
325
- async function claimNext() {
326
- await ensure();
327
- const files = await readdir(paths.commandsDir);
328
- const pending = files
329
- .filter((file) => file.endsWith(".request.json") || file.endsWith(".processing.json"))
330
- .sort((a, b) => {
331
- const aControl = a.startsWith("control-") ? 0 : 1;
332
- const bControl = b.startsWith("control-") ? 0 : 1;
333
- return aControl - bControl || a.localeCompare(b);
334
- });
335
- for (const file of pending) {
336
- const id = file.replace(/\.(?:request|processing)\.json$/, "");
337
- const item = jobPaths(paths, id);
338
- if (await readJson(item.result, null)) {
339
- await Promise.all([unlink(item.request).catch(() => {}), unlink(item.processing).catch(() => {})]);
340
- continue;
341
- }
342
- try {
343
- if (file.endsWith(".request.json")) await rename(item.request, item.processing);
344
- const record = await readJson(item.processing, null);
345
- if (!record) continue;
346
- const accepted = {
347
- ...record,
348
- status: "accepted",
349
- acceptedAt: record.acceptedAt || new Date().toISOString()
350
- };
351
- await writeJson(item.processing, accepted);
352
- return { id, ...item, payload: accepted.payload };
353
- } catch {}
354
- }
355
- return null;
356
- }
357
-
358
- async function workLoop({
359
- processJob,
360
- healthCheck,
361
- recover = null,
362
- beforeExit = null,
363
- idleTimeoutMs = 0
364
- }) {
365
- if (typeof healthCheck !== "function") throw new Error(`${toolName} daemon must declare healthCheck`);
366
- const policy = await loadDaemonPolicy();
367
- const ipcLimits = {
368
- maxFrameBytes: policy.ipcFrameBytes || 1_048_576,
369
- streamBufferBytes: policy.streamBufferBytes || 1_048_576
370
- };
371
- const subscribers = new Map();
372
- let lastActivity = Date.now();
373
- let processing = false;
374
- let exiting = false;
375
- let acceptingWork = true;
376
- let processRequested = false;
377
-
378
- await ensure();
379
- const capabilityToken = process.env.ARISA_DAEMON_CAPABILITY || await ensureDaemonCapability(paths);
380
- await writeStatus({
381
- state: "starting",
382
- pid: process.pid,
383
- heartbeatAt: new Date().toISOString(),
384
- supportsRecovery: typeof recover === "function",
385
- message: "Daemon work loop started; waiting for health check"
386
- });
387
-
388
- async function publish(frame) {
389
- const sockets = [...(subscribers.get(frame.jobId) || [])];
390
- for (const socket of sockets) {
391
- if (!(await writeSocketFrame(socket, frame, ipcLimits))) subscribers.get(frame.jobId)?.delete(socket);
392
- }
393
- }
394
-
395
- async function persistTerminal(job, frame) {
396
- await writeJson(job.result, {
397
- id: job.id,
398
- status: frame.type,
399
- completedAt: new Date().toISOString(),
400
- terminal: frame
401
- });
402
- await unlink(job.processing).catch(() => {});
403
- await publish(frame);
404
- }
405
-
406
- async function execute(job) {
407
- let sequence = 1;
408
- await publish(daemonFrame(job.id, "accepted", sequence, {}));
409
- const emit = async (type, payload = {}) => {
410
- if (!['progress', 'chunk'].includes(type)) throw new Error(`Invalid non-terminal daemon event type: ${type}`);
411
- sequence += 1;
412
- await publish(daemonFrame(job.id, type, sequence, payload));
413
- };
414
- const operation = job.payload?.[CONTROL_FIELD]?.operation;
415
- try {
416
- let output;
417
- if (operation === "health") {
418
- const checkedAt = new Date().toISOString();
419
- await writeStatus({ lastHealthCheckAt: checkedAt });
420
- output = await withTimeout(
421
- healthCheck,
422
- policy.healthTimeoutMs,
423
- `${toolName} health check timed out after ${policy.healthTimeoutMs}ms`
424
- );
425
- await writeStatus({
426
- state: "ready",
427
- lastHealthSuccessAt: new Date().toISOString(),
428
- consecutiveHealthFailures: 0,
429
- restartAttempts: 0,
430
- restartRequested: false,
431
- nextRestartAt: null,
432
- message: output?.message || "Daemon health check passed"
433
- });
434
- output ||= { ok: true };
435
- } else if (operation === "recover") {
436
- const recovered = typeof recover === "function"
437
- ? await withTimeout(recover, policy.healthTimeoutMs, `${toolName} recovery timed out after ${policy.healthTimeoutMs}ms`)
438
- : false;
439
- output = { recovered: recovered !== false };
440
- } else {
441
- lastActivity = Date.now();
442
- output = await processJob(job.payload, { emit, jobId: job.id });
443
- await writeStatus({ lastSuccessfulJobAt: new Date().toISOString() });
444
- lastActivity = Date.now();
445
- }
446
- sequence += 1;
447
- await persistTerminal(job, daemonFrame(job.id, "completed", sequence, { output }));
448
- } catch (error) {
449
- if (error?.code === "DAEMON_OPERATION_TIMEOUT") acceptingWork = false;
450
- const current = await readJson(paths.statusFile, {});
451
- await writeStatus({
452
- ...(operation === "health" ? {
453
- state: error?.code === "DAEMON_OPERATION_TIMEOUT" ? "unhealthy" : "degraded",
454
- consecutiveHealthFailures: Number(current.consecutiveHealthFailures || 0) + 1
455
- } : {}),
456
- lastError: {
457
- at: new Date().toISOString(),
458
- phase: operation || "job",
459
- message: error?.message || String(error),
460
- ...(error?.code ? { code: error.code } : {})
461
- },
462
- message: error?.message || String(error)
463
- });
464
- sequence += 1;
465
- await persistTerminal(job, daemonFrame(job.id, "failed", sequence, {
466
- error: error?.message || String(error),
467
- code: error?.code || null
468
- }));
469
- }
470
- }
471
-
472
- async function processQueue() {
473
- if (processing || exiting || !acceptingWork) {
474
- processRequested = true;
475
- return;
476
- }
477
- processing = true;
478
- try {
479
- do {
480
- processRequested = false;
481
- const job = await claimNext();
482
- if (!job) break;
483
- await execute(job);
484
- } while (!exiting && acceptingWork);
485
- } catch (error) {
486
- await writeStatus({
487
- state: "degraded",
488
- lastError: { at: new Date().toISOString(), phase: "work-loop", message: error?.message || String(error), ...(error?.code ? { code: error.code } : {}) },
489
- message: error?.message || String(error)
490
- });
491
- } finally {
492
- processing = false;
493
- if (processRequested && !exiting && acceptingWork) queueMicrotask(() => processQueue().catch(() => {}));
494
- }
495
- }
496
-
497
- if (process.platform !== "win32") await rm(paths.socketFile, { force: true });
498
- const server = net.createServer((socket) => {
499
- socket.setEncoding("utf8");
500
- let buffer = "";
501
- const subscribedJobs = new Set();
502
- const removeSocket = () => {
503
- for (const jobId of subscribedJobs) subscribers.get(jobId)?.delete(socket);
504
- };
505
- socket.on("data", (chunk) => {
506
- buffer += chunk;
507
- if (Buffer.byteLength(buffer, "utf8") > ipcLimits.maxFrameBytes && !buffer.includes("\n")) {
508
- socket.destroy();
509
- return;
510
- }
511
- let newlineIndex = buffer.indexOf("\n");
512
- while (newlineIndex !== -1) {
513
- const line = buffer.slice(0, newlineIndex).trim();
514
- buffer = buffer.slice(newlineIndex + 1);
515
- newlineIndex = buffer.indexOf("\n");
516
- if (!line) continue;
517
- let notification;
518
- try {
519
- notification = JSON.parse(line);
520
- } catch {
521
- socket.destroy();
522
- return;
523
- }
524
- if (notification.version !== DAEMON_PROTOCOL_VERSION
525
- || notification.type !== "submit"
526
- || typeof notification.jobId !== "string"
527
- || notification.capabilityToken !== capabilityToken) {
528
- socket.destroy();
529
- return;
530
- }
531
- const { jobId } = notification;
532
- if (!subscribers.has(jobId)) subscribers.set(jobId, new Set());
533
- subscribers.get(jobId).add(socket);
534
- subscribedJobs.add(jobId);
535
- readJson(jobPaths(paths, jobId).result, null).then((result) => {
536
- if (result?.terminal) return writeSocketFrame(socket, result.terminal, ipcLimits);
537
- return processQueue();
538
- }).catch(() => socket.destroy());
539
- }
540
- });
541
- socket.once("close", removeSocket);
542
- socket.once("error", removeSocket);
543
- });
544
- await new Promise((resolve, reject) => {
545
- server.once("error", reject);
546
- server.listen(paths.socketFile, resolve);
547
- });
548
- if (process.platform !== "win32") await chmod(paths.socketFile, 0o600);
549
-
550
- const heartbeatTimer = setInterval(() => {
551
- writeStatus({ heartbeatAt: new Date().toISOString() }).catch(() => {});
552
- }, policy.heartbeatIntervalMs);
553
- const idleTimer = idleTimeoutMs > 0 ? setInterval(async () => {
554
- if (processing || exiting || Date.now() - lastActivity <= idleTimeoutMs) return;
555
- exiting = true;
556
- clearInterval(heartbeatTimer);
557
- clearInterval(idleTimer);
558
- await beforeExit?.();
559
- await writeStatus({ state: "stopped", restartRequested: false, nextRestartAt: null, message: "Idle timeout reached" });
560
- server.close(() => process.exit(0));
561
- }, Math.min(idleTimeoutMs, 1_000)) : null;
562
-
563
- processQueue().catch(() => {});
564
- }
565
-
566
89
  return {
567
90
  paths,
568
91
  registration,
569
- ensure,
570
- getPid,
571
- writeStatus,
92
+ ensure: worker.ensure,
93
+ getPid: worker.getPid,
94
+ writeStatus: worker.writeStatus,
572
95
  start,
573
96
  stop,
574
97
  waitReady,
575
98
  ensureReady,
576
99
  submit,
577
- workLoop
100
+ workLoop: worker.workLoop
578
101
  };
579
102
  }