llm-relay 0.85.0 → 0.86.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.
Files changed (185) hide show
  1. package/README.md +39 -62
  2. package/THIRD_PARTY_NOTICES.md +5 -4
  3. package/dist/accounting-state.js.map +1 -1
  4. package/dist/accounting-store-schema.js.map +1 -1
  5. package/dist/accounting-store.js.map +1 -1
  6. package/dist/accounting.js.map +1 -1
  7. package/dist/authEnv.js.map +1 -1
  8. package/dist/availability-snapshot.js.map +1 -1
  9. package/dist/availability.js.map +1 -1
  10. package/dist/backend.js.map +1 -1
  11. package/dist/benchmarks.js.map +1 -1
  12. package/dist/breaker-persistence.js.map +1 -1
  13. package/dist/candidate-runner.d.ts +6 -12
  14. package/dist/candidate-runner.js +16 -4
  15. package/dist/candidate-runner.js.map +1 -1
  16. package/dist/candidates.js.map +1 -1
  17. package/dist/catalog.js.map +1 -1
  18. package/dist/circuit-breaker.d.ts +14 -17
  19. package/dist/circuit-breaker.js +46 -6
  20. package/dist/circuit-breaker.js.map +1 -1
  21. package/dist/claude-hook.js.map +1 -1
  22. package/dist/cli.d.ts +4 -1
  23. package/dist/cli.js +127 -12
  24. package/dist/cli.js.map +1 -1
  25. package/dist/config/routing-parser.d.ts +3 -13
  26. package/dist/config/routing-parser.js +16 -10
  27. package/dist/config/routing-parser.js.map +1 -1
  28. package/dist/config-reload.d.ts +34 -0
  29. package/dist/config-reload.js +180 -0
  30. package/dist/config-reload.js.map +1 -0
  31. package/dist/config-types.d.ts +26 -65
  32. package/dist/config-types.js.map +1 -1
  33. package/dist/config.d.ts +1 -1
  34. package/dist/config.js +1 -1
  35. package/dist/config.js.map +1 -1
  36. package/dist/configured-lane-execution-launcher.d.ts +36 -0
  37. package/dist/configured-lane-execution-launcher.js +133 -0
  38. package/dist/configured-lane-execution-launcher.js.map +1 -0
  39. package/dist/context-limits.js.map +1 -1
  40. package/dist/control-authorization.js.map +1 -1
  41. package/dist/cooldown-clear.js.map +1 -1
  42. package/dist/credential-fleet.js.map +1 -1
  43. package/dist/credential-select.js.map +1 -1
  44. package/dist/daemon-dispatch-view.d.ts +21 -0
  45. package/dist/daemon-dispatch-view.js +16 -0
  46. package/dist/daemon-dispatch-view.js.map +1 -0
  47. package/dist/dashboard/.vite/dashboard-bundle-graph.json +2 -1
  48. package/dist/dashboard/.vite/manifest.json +2 -2
  49. package/dist/dashboard/assets/index-BoSmrF3q.js +123 -0
  50. package/dist/dashboard/assets/index-DKPeE07T.css +1 -0
  51. package/dist/dashboard/index.html +2 -2
  52. package/dist/dashboard-auth.js.map +1 -1
  53. package/dist/dashboard-contract.d.ts +2 -2
  54. package/dist/dashboard-routes.js.map +1 -1
  55. package/dist/dashboard-snapshot.js.map +1 -1
  56. package/dist/dashboard-static.js.map +1 -1
  57. package/dist/delegate-gate/cli.js.map +1 -1
  58. package/dist/dispatch-exhaustion-persistence.d.ts +1 -0
  59. package/dist/dispatch-exhaustion-persistence.js +7 -2
  60. package/dist/dispatch-exhaustion-persistence.js.map +1 -1
  61. package/dist/dispatch-lane-stats.d.ts +10 -17
  62. package/dist/dispatch-lane-stats.js +0 -5
  63. package/dist/dispatch-lane-stats.js.map +1 -1
  64. package/dist/dispatch.d.ts +32 -78
  65. package/dist/dispatch.js +51 -54
  66. package/dist/dispatch.js.map +1 -1
  67. package/dist/documents.js.map +1 -1
  68. package/dist/dotenv.js.map +1 -1
  69. package/dist/dynamic-pools.js.map +1 -1
  70. package/dist/executable-lookup.d.ts +2 -0
  71. package/dist/executable-lookup.js +7 -4
  72. package/dist/executable-lookup.js.map +1 -1
  73. package/dist/host-routing.js.map +1 -1
  74. package/dist/installed-hosts.js.map +1 -1
  75. package/dist/json-shape.js.map +1 -1
  76. package/dist/key-checker.js.map +1 -1
  77. package/dist/key-import.js.map +1 -1
  78. package/dist/keys-cli.js.map +1 -1
  79. package/dist/keystore.js.map +1 -1
  80. package/dist/lane-activity.d.ts +8 -0
  81. package/dist/lane-activity.js +27 -2
  82. package/dist/lane-activity.js.map +1 -1
  83. package/dist/lane-affinity.d.ts +4 -4
  84. package/dist/lane-affinity.js.map +1 -1
  85. package/dist/lane-cadence.js.map +1 -1
  86. package/dist/lane-execution-broker.d.ts +124 -0
  87. package/dist/lane-execution-broker.js +479 -0
  88. package/dist/lane-execution-broker.js.map +1 -0
  89. package/dist/lane-launch-env.d.ts +14 -0
  90. package/dist/lane-launch-env.js +52 -0
  91. package/dist/lane-launch-env.js.map +1 -0
  92. package/dist/lane-manifest.js.map +1 -1
  93. package/dist/lane-probe.js.map +1 -1
  94. package/dist/lane-quota-probe.js.map +1 -1
  95. package/dist/log.js.map +1 -1
  96. package/dist/mcp/job-archive.d.ts +12 -9
  97. package/dist/mcp/job-archive.js +44 -37
  98. package/dist/mcp/job-archive.js.map +1 -1
  99. package/dist/mcp/job-journal.d.ts +42 -2
  100. package/dist/mcp/job-journal.js +212 -32
  101. package/dist/mcp/job-journal.js.map +1 -1
  102. package/dist/mcp/lane-execution-client.d.ts +26 -0
  103. package/dist/mcp/lane-execution-client.js +117 -0
  104. package/dist/mcp/lane-execution-client.js.map +1 -0
  105. package/dist/mcp/lane-runner.d.ts +140 -25
  106. package/dist/mcp/lane-runner.js +361 -49
  107. package/dist/mcp/lane-runner.js.map +1 -1
  108. package/dist/mcp/persistence-lock.d.ts +13 -0
  109. package/dist/mcp/persistence-lock.js +4 -0
  110. package/dist/mcp/persistence-lock.js.map +1 -0
  111. package/dist/mcp/process-cpu.d.ts +18 -0
  112. package/dist/mcp/process-cpu.js +133 -0
  113. package/dist/mcp/process-cpu.js.map +1 -0
  114. package/dist/mcp/readonly-boundary.js.map +1 -1
  115. package/dist/mcp/server.d.ts +97 -3
  116. package/dist/mcp/server.js +547 -61
  117. package/dist/mcp/server.js.map +1 -1
  118. package/dist/mcp/tree-delta.js.map +1 -1
  119. package/dist/mcp/windows-npm-shim.d.ts +32 -0
  120. package/dist/mcp/windows-npm-shim.js +92 -0
  121. package/dist/mcp/windows-npm-shim.js.map +1 -0
  122. package/dist/metadata.js.map +1 -1
  123. package/dist/openai-dialect.js.map +1 -1
  124. package/dist/openai-request.js.map +1 -1
  125. package/dist/os-keyring.js.map +1 -1
  126. package/dist/ping/cadence.js.map +1 -1
  127. package/dist/ping/metrics.js.map +1 -1
  128. package/dist/ping/ping.js.map +1 -1
  129. package/dist/ping/probe-cache.js.map +1 -1
  130. package/dist/ping/quota.js.map +1 -1
  131. package/dist/ping/runtime-telemetry.js.map +1 -1
  132. package/dist/pool-health.js.map +1 -1
  133. package/dist/process-safety-net.js +18 -10
  134. package/dist/process-safety-net.js.map +1 -1
  135. package/dist/quota-observation.js.map +1 -1
  136. package/dist/rate-limits.js.map +1 -1
  137. package/dist/refusal-interpretation.js.map +1 -1
  138. package/dist/registry.js.map +1 -1
  139. package/dist/request-log.js.map +1 -1
  140. package/dist/reshaper.js.map +1 -1
  141. package/dist/resolved-attempt.js.map +1 -1
  142. package/dist/responses-request.js.map +1 -1
  143. package/dist/routes/admin.d.ts +8 -0
  144. package/dist/routes/admin.js +54 -8
  145. package/dist/routes/admin.js.map +1 -1
  146. package/dist/routes/messages.js +10 -10
  147. package/dist/routes/messages.js.map +1 -1
  148. package/dist/routes/openai-front.js +4 -4
  149. package/dist/routes/openai-front.js.map +1 -1
  150. package/dist/secret-file-acl.js.map +1 -1
  151. package/dist/self-update.js.map +1 -1
  152. package/dist/server.d.ts +19 -0
  153. package/dist/server.js +70 -4
  154. package/dist/server.js.map +1 -1
  155. package/dist/session-pin.js.map +1 -1
  156. package/dist/setup-claude.d.ts +6 -1
  157. package/dist/setup-claude.js +3 -3
  158. package/dist/setup-claude.js.map +1 -1
  159. package/dist/spend-headroom.js.map +1 -1
  160. package/dist/sse.js.map +1 -1
  161. package/dist/state-paths.js.map +1 -1
  162. package/dist/storage/file-lock.d.ts +26 -0
  163. package/dist/storage/file-lock.js +127 -0
  164. package/dist/storage/file-lock.js.map +1 -0
  165. package/dist/storage/json-store.d.ts +33 -0
  166. package/dist/storage/json-store.js +68 -1
  167. package/dist/storage/json-store.js.map +1 -1
  168. package/dist/stream-commit.js.map +1 -1
  169. package/dist/stream-pipeline.js.map +1 -1
  170. package/dist/target-facts.js.map +1 -1
  171. package/dist/telemetry.d.ts +3 -1
  172. package/dist/telemetry.js +2 -1
  173. package/dist/telemetry.js.map +1 -1
  174. package/dist/tier-data.js.map +1 -1
  175. package/dist/tool-dialects.js.map +1 -1
  176. package/dist/usage-observer.js.map +1 -1
  177. package/dist/winenv.js.map +1 -1
  178. package/docs/README.md +4 -2
  179. package/docs/dashboard-bundle-inventory.json +5 -4
  180. package/package.json +15 -14
  181. package/scripts/install-skill.mjs +4 -2
  182. package/skills/llm-relay/SKILL.md +10 -8
  183. package/skills/llm-relay/references/dispatch-lanes.md +18 -1
  184. package/dist/dashboard/assets/index-C-I6hPeY.js +0 -403
  185. package/dist/dashboard/assets/index-CrSNlLMD.css +0 -1
@@ -1,9 +1,10 @@
1
- import { exec, execFile, execSync } from "node:child_process";
1
+ import { exec, execFile, execSync, spawn } from "node:child_process";
2
+ import { randomBytes } from "node:crypto";
2
3
  import { existsSync, statSync } from "node:fs";
3
4
  import { resolve as resolvePath } from "node:path";
4
- import { quoteCmdArg } from "../lane-probe.js";
5
- import { nullJobJournal } from "./job-journal.js";
6
- import { jobSeqOf, nullJobArchive } from "./job-archive.js";
5
+ import { resolveWindowsNpmShim } from "./windows-npm-shim.js";
6
+ import { nullJobJournal, } from "./job-journal.js";
7
+ import { nullJobArchive } from "./job-archive.js";
7
8
  import { classifyLaneProbeOutput } from "../lane-quota-probe.js";
8
9
  import { laneOfRung } from "../lane-manifest.js";
9
10
  export const DEPTH_ENV = "LLM_RELAY_DISPATCH_DEPTH";
@@ -227,12 +228,120 @@ function observeOutput(child, onOutput) {
227
228
  watch("stdout");
228
229
  watch("stderr");
229
230
  }
231
+ function spawnPosixLane(processApi, command, args, opts) {
232
+ const spawnProcess = processApi.spawn;
233
+ if (!spawnProcess) {
234
+ return {
235
+ result: Promise.resolve({
236
+ code: null,
237
+ stdout: "",
238
+ stderr: "POSIX lane spawning requires the injected spawn boundary",
239
+ timedOut: false,
240
+ }),
241
+ kill: () => { },
242
+ pids: () => [],
243
+ };
244
+ }
245
+ let child;
246
+ let timedOut = false;
247
+ let bufferExceeded = null;
248
+ let timeoutTimer;
249
+ const stdoutChunks = [];
250
+ const stderrChunks = [];
251
+ let stdoutBytes = 0;
252
+ let stderrBytes = 0;
253
+ const killOwned = () => {
254
+ if (child?.pid)
255
+ terminateProcessTree(child.pid, processApi.platform);
256
+ child?.kill();
257
+ };
258
+ const clearTimer = () => {
259
+ if (timeoutTimer !== undefined)
260
+ clearTimeout(timeoutTimer);
261
+ timeoutTimer = undefined;
262
+ };
263
+ const append = (stream, chunk) => {
264
+ const data = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
265
+ const isStdout = stream === "stdout";
266
+ const used = isStdout ? stdoutBytes : stderrBytes;
267
+ const remaining = Math.max(0, MAX_OUTPUT_BYTES - used);
268
+ if (remaining > 0) {
269
+ const kept = data.length <= remaining ? data : data.subarray(0, remaining);
270
+ (isStdout ? stdoutChunks : stderrChunks).push(kept);
271
+ if (isStdout)
272
+ stdoutBytes += kept.length;
273
+ else
274
+ stderrBytes += kept.length;
275
+ }
276
+ if (data.length > remaining && bufferExceeded === null) {
277
+ bufferExceeded = stream;
278
+ killOwned();
279
+ }
280
+ };
281
+ const result = new Promise((resolve) => {
282
+ let settled = false;
283
+ const settle = (code, error) => {
284
+ if (settled)
285
+ return;
286
+ settled = true;
287
+ clearTimer();
288
+ const stdout = Buffer.concat(stdoutChunks).toString("utf8");
289
+ let stderr = Buffer.concat(stderrChunks).toString("utf8");
290
+ if (!stderr && error)
291
+ stderr = error.message;
292
+ if (bufferExceeded !== null) {
293
+ const msg = `${bufferExceeded} exceeded ${MAX_OUTPUT_BYTES} byte lane output limit`;
294
+ stderr = stderr ? `${stderr}\n${msg}` : msg;
295
+ }
296
+ resolve({
297
+ code: bufferExceeded === null ? code : null,
298
+ stdout,
299
+ stderr,
300
+ timedOut,
301
+ });
302
+ };
303
+ try {
304
+ child = spawnProcess(command, args, {
305
+ cwd: opts.cwd,
306
+ env: opts.env,
307
+ windowsHide: true,
308
+ detached: true,
309
+ });
310
+ }
311
+ catch (e) {
312
+ settle(null, e);
313
+ return;
314
+ }
315
+ child.stdout?.on("data", (chunk) => append("stdout", chunk));
316
+ child.stderr?.on("data", (chunk) => append("stderr", chunk));
317
+ observeOutput(child, opts.onOutput);
318
+ child.on("error", (err) => settle(null, err));
319
+ child.on("close", (code) => settle(code));
320
+ child.stdin?.end();
321
+ if (opts.timeoutMs > 0) {
322
+ timeoutTimer = setTimeout(() => {
323
+ timedOut = true;
324
+ killOwned();
325
+ }, opts.timeoutMs);
326
+ timeoutTimer.unref?.();
327
+ }
328
+ });
329
+ return {
330
+ result,
331
+ kill: () => {
332
+ clearTimer();
333
+ killOwned();
334
+ },
335
+ pids: () => (child?.pid === undefined ? [] : [child.pid]),
336
+ };
337
+ }
230
338
  const nodeProcessApi = {
231
339
  platform: process.platform,
232
340
  execFile: (command, args, opts, callback) => execFile(command, args, opts, callback),
233
341
  exec: (command, opts, callback) => exec(command, opts, callback),
342
+ spawn: (command, args, opts) => spawn(command, args, opts),
234
343
  };
235
- export function createLaneSpawner(processApi, hostEnv = process.env) {
344
+ export function createLaneSpawner(processApi, hostEnv = process.env, resolveNpmShim = resolveWindowsNpmShim) {
236
345
  return (command, args, opts) => {
237
346
  if (hostEnv["VITEST"]) {
238
347
  return {
@@ -245,6 +354,8 @@ export function createLaneSpawner(processApi, hostEnv = process.env) {
245
354
  kill: () => { },
246
355
  };
247
356
  }
357
+ if (processApi.platform !== "win32")
358
+ return spawnPosixLane(processApi, command, args, opts);
248
359
  const execOpts = {
249
360
  encoding: "utf8",
250
361
  maxBuffer: MAX_OUTPUT_BYTES,
@@ -269,15 +380,17 @@ export function createLaneSpawner(processApi, hostEnv = process.env) {
269
380
  });
270
381
  };
271
382
  child = processApi.execFile(command, args, execOpts, (err, stdout, stderr) => {
272
- if (!err) {
273
- resolve({ code: 0, stdout, stderr, timedOut: false });
274
- return;
275
- }
276
- if (processApi.platform === "win32" && err.code === "ENOENT" && !killed) {
277
- const line = `${quoteCmdArg(command)} ${args.map(quoteCmdArg).join(" ")}`;
278
- const fallback = processApi.exec(line, execOpts, (err2, stdout2, stderr2) => {
279
- settle(err2, stdout2 ?? "", stderr2 ?? "");
383
+ if (err && processApi.platform === "win32" && err.code === "ENOENT" && !killed) {
384
+ const resolved = resolveNpmShim(command, args, {
385
+ env: opts.env,
386
+ cwd: opts.cwd,
387
+ nodeExecutable: process.execPath,
280
388
  });
389
+ if (!resolved.ok) {
390
+ settle(Object.assign(new Error(resolved.error), { code: "ENOENT" }), stdout ?? "", stderr ?? "");
391
+ return;
392
+ }
393
+ const fallback = processApi.execFile(resolved.command, resolved.args, execOpts, (err2, stdout2, stderr2) => settle(err2, stdout2 ?? "", stderr2 ?? ""));
281
394
  child = fallback;
282
395
  fallback.stdin?.end();
283
396
  observeOutput(fallback, opts.onOutput);
@@ -292,9 +405,8 @@ export function createLaneSpawner(processApi, hostEnv = process.env) {
292
405
  result,
293
406
  kill: () => {
294
407
  killed = true;
295
- if (child?.pid) {
408
+ if (child?.pid)
296
409
  terminateProcessTree(child.pid, processApi.platform);
297
- }
298
410
  child?.kill();
299
411
  },
300
412
  pids: () => (child?.pid === undefined ? [] : [child.pid]),
@@ -355,17 +467,20 @@ function recentOf(job, elsewhere) {
355
467
  };
356
468
  }
357
469
  function newestFirst(a, b) {
358
- return b.startedAt - a.startedAt || (jobSeqOf(b.id) ?? 0) - (jobSeqOf(a.id) ?? 0);
359
- }
360
- let jobCounter = 0;
361
- function nextJobId() {
362
- jobCounter += 1;
363
- return `job-${String(jobCounter).padStart(4, "0")}`;
470
+ return b.startedAt - a.startedAt || b.id.localeCompare(a.id);
364
471
  }
365
- export function seedJobCounter(seq) {
366
- if (Number.isSafeInteger(seq) && seq > jobCounter)
367
- jobCounter = seq;
472
+ export function createJobIdFactory(entropy = randomBytes(16)) {
473
+ const bytes = Buffer.from(entropy);
474
+ if (bytes.length !== 16)
475
+ throw new Error("job id entropy must be exactly 16 bytes");
476
+ const instance = BigInt(`0x${bytes.toString("hex")}`).toString(10).padStart(39, "0");
477
+ let local = 0n;
478
+ return () => {
479
+ local += 1n;
480
+ return `job-9${instance}${local.toString(10).padStart(8, "0")}`;
481
+ };
368
482
  }
483
+ const defaultJobIdFactory = createJobIdFactory();
369
484
  export class LaneJobStore {
370
485
  jobs = new Map();
371
486
  kills = new Map();
@@ -381,22 +496,23 @@ export class LaneJobStore {
381
496
  };
382
497
  journal;
383
498
  archive;
384
- constructor(journal = nullJobJournal, archive = nullJobArchive) {
499
+ adoptedStartingTrees = new Map();
500
+ brokerOrphans = new Map();
501
+ brokerExecutions = new Map();
502
+ nextJobId;
503
+ constructor(journal = nullJobJournal, archive = nullJobArchive, nextJobId = defaultJobIdFactory) {
385
504
  this.journal = journal;
386
505
  this.archive = archive;
506
+ this.nextJobId = nextJobId;
387
507
  this.restoreArchive();
388
508
  this.adoptOrphans();
389
509
  }
390
510
  restoreArchive() {
391
- const { jobs, lastSeq } = this.archive.restore();
392
- seedJobCounter(lastSeq);
511
+ const { jobs } = this.archive.restore();
393
512
  for (const row of jobs) {
394
513
  if (this.jobs.has(row.id))
395
514
  continue;
396
515
  this.jobs.set(row.id, { ...row, restored: true });
397
- const seq = jobSeqOf(row.id);
398
- if (seq !== null)
399
- seedJobCounter(seq);
400
516
  }
401
517
  }
402
518
  flush() {
@@ -406,9 +522,12 @@ export class LaneJobStore {
406
522
  for (const row of this.journal.orphans()) {
407
523
  if (this.jobs.has(row.jobId))
408
524
  continue;
409
- const seq = jobSeqOf(row.jobId);
410
- if (seq !== null)
411
- seedJobCounter(seq);
525
+ if (row.brokerExecution !== undefined) {
526
+ const claimed = this.journal.claimBrokerOrphan?.(row.jobId);
527
+ if (claimed !== undefined)
528
+ this.brokerOrphans.set(row.jobId, claimed);
529
+ continue;
530
+ }
412
531
  const killed = {
413
532
  id: row.jobId,
414
533
  status: "killed",
@@ -428,14 +547,16 @@ export class LaneJobStore {
428
547
  process: { pids: [], survivors: [], terminated: false },
429
548
  };
430
549
  this.jobs.set(row.jobId, killed);
431
- this.archive.record(killed);
550
+ if (row.startingTree !== undefined) {
551
+ this.adoptedStartingTrees.set(row.jobId, { cwd: row.cwd, startingTree: row.startingTree });
552
+ }
553
+ if (this.archive.record(killed))
554
+ this.journal.clearOrphan?.(row);
432
555
  }
433
556
  }
434
557
  create(laneId, spec, cwd, dispatchSource, label) {
435
- seedJobCounter(this.archive.lastSeqOnDisk());
436
- seedJobCounter(this.journal.maxSeqOnDisk());
437
558
  const job = {
438
- id: nextJobId(),
559
+ id: this.nextJobId(),
439
560
  status: "running",
440
561
  laneId,
441
562
  spec,
@@ -452,9 +573,6 @@ export class LaneJobStore {
452
573
  ...(label ? { label } : {}),
453
574
  };
454
575
  this.jobs.set(job.id, job);
455
- const seq = jobSeqOf(job.id);
456
- if (seq !== null)
457
- this.archive.noteSeq(seq);
458
576
  this.journal.note({
459
577
  jobId: job.id,
460
578
  laneId: job.laneId,
@@ -469,6 +587,12 @@ export class LaneJobStore {
469
587
  this.kills.set(id, handle.kill);
470
588
  this.owned.set(id, handle);
471
589
  }
590
+ runningPids(id) {
591
+ const job = this.jobs.get(id);
592
+ if (!job || job.status !== "running")
593
+ return [];
594
+ return readOwnedPids(this.owned.get(id));
595
+ }
472
596
  registerKill(id, kill) {
473
597
  this.kills.set(id, kill);
474
598
  this.owned.set(id, { kill });
@@ -479,14 +603,25 @@ export class LaneJobStore {
479
603
  const kill = this.kills.get(id);
480
604
  this.kills.delete(id);
481
605
  this.owned.delete(id);
482
- this.journal.clear(id);
483
- if (!job)
606
+ const brokerOwned = this.brokerExecutions.has(id);
607
+ this.brokerExecutions.delete(id);
608
+ if (!job) {
609
+ this.journal.clear(id);
484
610
  return;
611
+ }
485
612
  delete job.activity;
613
+ delete job.liveness;
486
614
  const pids = readOwnedPids(handle);
615
+ if (brokerOwned) {
616
+ job.process = { pids: [], survivors: [], terminated: false };
617
+ if (this.archive.record(job))
618
+ this.journal.clear(id);
619
+ return;
620
+ }
487
621
  if (kill === undefined) {
488
622
  job.process = { pids, survivors: [], terminated: false };
489
- this.archive.record(job);
623
+ if (this.archive.record(job))
624
+ this.journal.clear(id);
490
625
  return;
491
626
  }
492
627
  try {
@@ -495,7 +630,8 @@ export class LaneJobStore {
495
630
  catch {
496
631
  }
497
632
  job.process = { pids, survivors: pids.filter((pid) => this.isAlive(pid)), terminated: true };
498
- this.archive.record(job);
633
+ if (this.archive.record(job))
634
+ this.journal.clear(id);
499
635
  }
500
636
  ownedProcesses() {
501
637
  const rows = [];
@@ -524,7 +660,17 @@ export class LaneJobStore {
524
660
  delete job.activity;
525
661
  delete job.readOnly;
526
662
  delete job.launch;
527
- delete job.lastActive;
663
+ delete job.liveness;
664
+ this.brokerExecutions.delete(id);
665
+ delete job.executionOwner;
666
+ this.journal.note({
667
+ jobId: job.id,
668
+ laneId: job.laneId,
669
+ ...(job.spec === undefined ? {} : { spec: job.spec }),
670
+ cwd: job.cwd,
671
+ startedAt: job.startedAt,
672
+ ...(job.label === undefined ? {} : { label: job.label }),
673
+ });
528
674
  }
529
675
  beginAttemptActivity(id, now) {
530
676
  const job = this.jobs.get(id);
@@ -542,6 +688,167 @@ export class LaneJobStore {
542
688
  else
543
689
  activity.stderrBytes += bytes;
544
690
  }
691
+ noteStartingTree(id, tree, scope) {
692
+ const job = this.jobs.get(id);
693
+ if (!job || job.status !== "running")
694
+ return;
695
+ this.journal.noteStartingTree?.(id, tree, scope);
696
+ }
697
+ takeBrokerOrphans() {
698
+ const rows = [...this.brokerOrphans.values()].map((row) => ({ ...row }));
699
+ this.brokerOrphans.clear();
700
+ return rows;
701
+ }
702
+ brokerExecution(id) {
703
+ return this.brokerExecutions.get(id);
704
+ }
705
+ noteExecutionOwner(id, owner) {
706
+ const job = this.jobs.get(id);
707
+ if (job === undefined || job.status !== "running")
708
+ return;
709
+ if (owner === undefined)
710
+ delete job.executionOwner;
711
+ else
712
+ job.executionOwner = owner;
713
+ }
714
+ clearBrokerExecution(id) {
715
+ const job = this.jobs.get(id);
716
+ if (job === undefined || job.status !== "running")
717
+ return;
718
+ this.brokerExecutions.delete(id);
719
+ if (job.executionOwner === "relay-daemon")
720
+ delete job.executionOwner;
721
+ this.journal.note({
722
+ jobId: job.id,
723
+ laneId: job.laneId,
724
+ ...(job.spec === undefined ? {} : { spec: job.spec }),
725
+ cwd: job.cwd,
726
+ startedAt: job.startedAt,
727
+ ...(job.label === undefined ? {} : { label: job.label }),
728
+ });
729
+ }
730
+ noteBrokerExecution(id, executionId) {
731
+ const job = this.jobs.get(id);
732
+ if (job === undefined || job.status !== "running")
733
+ return;
734
+ const execution = { kind: "daemon-v1", executionId };
735
+ this.brokerExecutions.set(id, executionId);
736
+ this.journal.noteBrokerExecution?.(id, execution);
737
+ }
738
+ adoptBrokerRunning(row, snapshot) {
739
+ if (row.brokerExecution === undefined)
740
+ return undefined;
741
+ const existing = this.jobs.get(row.jobId);
742
+ if (existing !== undefined)
743
+ return existing;
744
+ const job = {
745
+ id: row.jobId,
746
+ status: "running",
747
+ laneId: row.laneId,
748
+ spec: row.spec,
749
+ startedAt: row.startedAt,
750
+ endedAt: undefined,
751
+ exitCode: null,
752
+ stdout: "",
753
+ stderr: "",
754
+ timedOut: false,
755
+ cwd: row.cwd,
756
+ error: undefined,
757
+ attempts: [],
758
+ restored: true,
759
+ executionOwner: "relay-daemon",
760
+ ...(row.label === undefined ? {} : { label: row.label }),
761
+ };
762
+ this.jobs.set(job.id, job);
763
+ this.brokerExecutions.set(job.id, row.brokerExecution.executionId);
764
+ if (snapshot !== undefined)
765
+ this.applyBrokerSnapshot(job.id, snapshot);
766
+ return job;
767
+ }
768
+ applyBrokerSnapshot(id, snapshot) {
769
+ const job = this.jobs.get(id);
770
+ const executionId = this.brokerExecutions.get(id);
771
+ if (job === undefined ||
772
+ executionId === undefined ||
773
+ snapshot.executionId !== executionId ||
774
+ snapshot.jobId !== id ||
775
+ snapshot.laneId !== job.laneId) {
776
+ return false;
777
+ }
778
+ if (snapshot.launchNotes !== undefined)
779
+ job.launch = [...snapshot.launchNotes];
780
+ job.activity = {
781
+ attemptStartedAt: snapshot.startedAt,
782
+ lastOutputAt: snapshot.lastOutputAt,
783
+ stdoutBytes: snapshot.stdoutBytes,
784
+ stderrBytes: snapshot.stderrBytes,
785
+ };
786
+ if (snapshot.status === "running")
787
+ return true;
788
+ if (snapshot.status === "completed") {
789
+ if (snapshot.code !== 0 || snapshot.timedOut !== false || snapshot.stdout === undefined)
790
+ return false;
791
+ if (isContentEmpty(snapshot.stdout)) {
792
+ job.status = "failed";
793
+ job.error = EMPTY_OUTPUT_REASON;
794
+ }
795
+ else {
796
+ job.status = "completed";
797
+ }
798
+ }
799
+ else if (snapshot.status === "failed") {
800
+ if (snapshot.timedOut !== false || snapshot.code === 0)
801
+ return false;
802
+ job.status = "failed";
803
+ }
804
+ else if (snapshot.status === "timed_out") {
805
+ if (snapshot.timedOut !== true)
806
+ return false;
807
+ job.status = "timed_out";
808
+ job.error = "the daemon-owned lane exceeded its configured timeout and was stopped before it finished";
809
+ }
810
+ else if (snapshot.status === "cancelled") {
811
+ job.status = "cancelled";
812
+ }
813
+ else {
814
+ const _never = snapshot.status;
815
+ return _never;
816
+ }
817
+ if (snapshot.code !== undefined)
818
+ job.exitCode = snapshot.code;
819
+ if (snapshot.stdout !== undefined)
820
+ job.stdout = snapshot.stdout;
821
+ if (snapshot.stderr !== undefined)
822
+ job.stderr = snapshot.stderr;
823
+ if (snapshot.timedOut !== undefined)
824
+ job.timedOut = snapshot.timedOut;
825
+ job.endedAt = snapshot.endedAt ?? Date.now();
826
+ delete job.activity;
827
+ delete job.liveness;
828
+ this.brokerExecutions.delete(id);
829
+ if (this.archive.record(job))
830
+ this.journal.clear(id);
831
+ return true;
832
+ }
833
+ markBrokerKilled(id, message) {
834
+ const job = this.jobs.get(id);
835
+ if (job === undefined || !this.brokerExecutions.has(id) || job.status !== "running")
836
+ return false;
837
+ job.status = "killed";
838
+ job.endedAt = Date.now();
839
+ job.error = message;
840
+ delete job.activity;
841
+ delete job.liveness;
842
+ this.brokerExecutions.delete(id);
843
+ if (this.archive.record(job))
844
+ this.journal.clear(id);
845
+ return true;
846
+ }
847
+ takeAdoptedStartingTrees() {
848
+ const adopted = [...this.adoptedStartingTrees].map(([jobId, value]) => ({ jobId, ...value }));
849
+ this.adoptedStartingTrees.clear();
850
+ return adopted;
851
+ }
545
852
  noteReadOnly(id, laneId, binding) {
546
853
  const job = this.jobs.get(id);
547
854
  if (!job || job.status !== "running")
@@ -553,14 +860,15 @@ export class LaneJobStore {
553
860
  if (!job)
554
861
  return;
555
862
  job.treeDelta = text;
556
- if (job.status !== "running")
557
- this.archive.record(job);
863
+ if (job.status !== "running" && this.archive.record(job)) {
864
+ this.journal.clear(id);
865
+ }
558
866
  }
559
- noteLastActivity(id, at, source) {
867
+ noteLiveness(id, liveness) {
560
868
  const job = this.jobs.get(id);
561
869
  if (!job || job.status !== "running")
562
870
  return;
563
- job.lastActive = { at, source };
871
+ job.liveness = { ...liveness };
564
872
  }
565
873
  noteLaunch(id, notes) {
566
874
  const job = this.jobs.get(id);
@@ -687,6 +995,8 @@ export class LaneJobStore {
687
995
  return false;
688
996
  if (job.status !== "running")
689
997
  return false;
998
+ if (this.brokerExecutions.has(id))
999
+ return false;
690
1000
  job.status = "cancelled";
691
1001
  job.endedAt = Date.now();
692
1002
  this.reap(id);
@@ -694,6 +1004,8 @@ export class LaneJobStore {
694
1004
  }
695
1005
  cancelAll() {
696
1006
  for (const id of [...this.jobs.values()].filter((j) => j.status === "running").map((j) => j.id)) {
1007
+ if (this.brokerExecutions.has(id))
1008
+ continue;
697
1009
  this.cancel(id);
698
1010
  }
699
1011
  }