@volter/supercode-orchestrator 0.5.47 → 0.5.48

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
@@ -525,10 +525,12 @@ failed/timeout fires without replaying them; a restart advances a stale claim
525
525
  once. `callOperator` addresses the live socket, with no cold-write fallback.
526
526
  `Orchestrator({executeJob})` is the corresponding in-process API for embedders.
527
527
 
528
- The daemon acquires its exclusive lease before loading state. A crash during the
529
- short acquisition critical section leaves `orchestrator.acquire`; subsequent
530
- starts fail closed until an operator inspects and removes that guard. Normal
531
- stops remove only their own lease. Native `jobs.resume` computes the first fire
528
+ The daemon serves a home only while it holds the home's claim, Hermes's
529
+ `gateway.lock` (an flock the kernel releases when its holder dies). A second
530
+ daemon on the home waits for the claim and serves the home as soon as the first
531
+ is gone, however it ended; nothing fails closed or waits for a person. The
532
+ serving daemon records itself in `orchestrator.lock` for `supercode orchestrator
533
+ status` and the operator verbs; a normal stop removes only its own record. Native `jobs.resume` computes the first fire
532
534
  for a job that was created paused with no next-run timestamp.
533
535
 
534
536
  ## Hosted daemon lifecycle
package/bin/runtime.mjs CHANGED
@@ -53,7 +53,7 @@
53
53
  // Everything it prints is one JSON object per line on stdout.
54
54
 
55
55
  import { SenderIdentityRecord } from '@volter/teams/sender-identity';
56
- import { existsSync, mkdirSync, rmdirSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';
56
+ import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'node:fs';
57
57
  import { basename, dirname, join, resolve } from 'node:path';
58
58
  import { spawn, spawnSync } from 'node:child_process';
59
59
  import { fileURLToPath } from 'node:url';
@@ -75,7 +75,8 @@ import { httpJobExecutor } from '../hosted.mjs';
75
75
  import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
76
76
 
77
77
  /** How long a starting daemon waits for a stopping predecessor to release the home's lease. */
78
- const LEASE_HANDOFF_MS = 20_000;
78
+ /** How often a daemon waiting for the home's claim asks for it again. */
79
+ const CLAIM_RETRY_MS = 2_000;
79
80
 
80
81
  /// Adapter selector -> constructor. A `platforms:` block picks its adapter by
81
82
  /// `extra.adapter` when it has one and by the block's own name otherwise, so
@@ -388,16 +389,16 @@ function profileReport(name, profile) {
388
389
  };
389
390
  }
390
391
 
391
- // ---------------------------------------------------------------- the lease
392
+ // ---------------------------------------------------------------- who serves the home
392
393
 
393
- /** The lease file, shared with `supercode orchestrator status|stop` (Rust `Lease`). */
394
+ /** The record of the daemon serving the home, shared with `supercode orchestrator status|stop` (Rust `Lease`). */
394
395
  const readLock = readLease;
395
396
 
396
397
  function writeLock(root, lease) {
397
398
  writeFileSync(join(root, LOCK_FILE), `${JSON.stringify(lease, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
398
399
  }
399
400
 
400
- /** Only ever removes a lease this process holds. */
401
+ /** Only ever removes the record this process wrote. */
401
402
  function clearLock(root) {
402
403
  const held = readLock(root);
403
404
  if (held && held.pid !== process.pid) return;
@@ -677,35 +678,26 @@ async function main() {
677
678
  let crashed = false;
678
679
  let gatewayLock = null;
679
680
  if (!args.once) {
680
- // One daemon per home: its lease (and the acquisition guard serializing a lease's replacement) keeps a second daemon
681
- // from firing the home's cron jobs, holding its channel adapters and driving its bindings twice, and tells a restart
682
- // that its predecessor crashed (§4.7). None of it guards the board, whose one dispatcher is `workflow serve`. An
683
- // acquisition guard left by a crash fails closed; inspect it before removing it manually.
684
- // A service manager's restart starts this one while the last is still stopping (launchd's kickstart -k did, and
685
- // this one exited 'already serving'): a live predecessor gets a moment to let go before it counts as serving.
686
- for (let waited = 0; waited < LEASE_HANDOFF_MS; waited += 250) {
687
- const previous = readLock(root);
688
- if (!previous || previous.pid === process.pid || !leaseIsLive(previous)) break;
689
- await new Promise((resolve) => setTimeout(resolve, 250));
690
- }
691
- const guard = join(root, 'orchestrator.acquire');
692
- mkdirSync(guard);
693
- try {
694
- const previous = readLock(root);
695
- if (previous && leaseIsLive(previous)) {
696
- say('error', { root, message: `already serving ${root} (pid ${previous.pid})` });
697
- throw new Error(`already serving ${root}`);
681
+ // One runtime serves a home: whoever holds its claim, Hermes's own gateway lock (`<home>/gateway.lock`, an flock the
682
+ // kernel releases the moment its holder dies, however it died). So its cron jobs fire once, its channel adapters
683
+ // are held once and its bindings are driven once, and a Hermes gateway on the home refuses while this daemon
684
+ // serves it. A second daemon does not exit and nothing waits for a person: it waits for the claim and serves the
685
+ // home as soon as the first is gone (a service manager's restart, a crash, kill -9).
686
+ for (let said = false; ;) {
687
+ try { gatewayLock = await holdGatewayLock(root); break; }
688
+ catch (error) {
689
+ if (!/ is served by /.test(error.message)) { say('error', { root, message: error.message }); process.exit(1); }
690
+ if (!said) { say('log', { level: 'info', message: `${error.message}; this daemon serves the home when that one ends` }); said = true; }
691
+ await new Promise((resolve) => setTimeout(resolve, CLAIM_RETRY_MS));
698
692
  }
699
- crashed = Boolean(previous);
700
- if (previous) unlinkSync(join(root, LOCK_FILE));
701
- writeLock(root, { pid: process.pid, started_at: new Date().toISOString(), root, ...thisHost(), executor_url: args.executorUrl ?? null });
702
- } finally {
703
- rmdirSync(guard);
704
693
  }
705
- // One runtime per home: Hermes's own gateway lock, held for as long as this daemon lives, so a Hermes
706
- // gateway on this home refuses to start, and this daemon refuses a home a Hermes gateway serves.
694
+ // Who serves the home now (`orchestrator.lock`), for `supercode orchestrator status` and the operator verbs' live
695
+ // door. A record left behind is a predecessor that never stopped cleanly: this start replays for it (§4.7).
696
+ const previous = readLock(root);
697
+ crashed = Boolean(previous && previous.pid !== process.pid);
698
+ if (previous) unlinkSync(join(root, LOCK_FILE));
699
+ writeLock(root, { pid: process.pid, started_at: new Date().toISOString(), root, ...thisHost(), executor_url: args.executorUrl ?? null });
707
700
  try {
708
- gatewayLock = await holdGatewayLock(root);
709
701
  // the home in both forms before it is loaded: a Hermes home is adopted as it stands
710
702
  for (const change of adoptHome(root)) say('log', { level: 'info', message: `home: ${change}` });
711
703
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.47",
3
+ "version": "0.5.48",
4
4
  "type": "module",
5
5
  "description": "The orchestrator runtime over the Volter Harness ontology: one typed operational model whose folder is its serialization, read and written through the harness orchestration doors (docs/ORCHESTRATOR-IR.md)",
6
6
  "exports": {