@volter/supercode-orchestrator 0.5.116 → 0.5.118

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/board/cli.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  // `serve`, `--at`) are added beside them. The Rust CLI hands every verb but `load` to `bin/orchestrator.mjs workflow`,
5
5
  // which lands here. The home is `--root`, else `SUPERCODE_ORCHESTRATOR_HOME`; the board is
6
6
  // `--board <slug>`, else `HERMES_KANBAN_BOARD`, else the home's current board (`kanban/current`), else `default`.
7
- import { machineCall, closeDoors } from './doors.mjs';
7
+ import { machineCall, closeDoors, onLocalReopen } from './doors.mjs';
8
8
  import { createHash } from 'node:crypto';
9
9
  import { DatabaseSync } from 'node:sqlite';
10
10
  import { homeOf, parse } from './args.mjs';
@@ -1699,14 +1699,21 @@ async function serve(root, args = {}) {
1699
1699
  try { const { PerformanceObserver } = await import('node:perf_hooks'); new PerformanceObserver((list) => { for (const entry of list.getEntries()) noteStep('gc', entry.duration); }).observe({ entryTypes: ['gc'] }); } catch { /* no gc entries here */ }
1700
1700
  // The board's events are published by this machine's connector, not by the dispatcher (D137, D138 step 4): the home is
1701
1701
  // named to the daemon once, and the connector follows its `workflow events` from then on, with or without this serve.
1702
- { let named = null, refusal = null;
1703
- // this process is the home's owner: it names its pid (its claim), which the daemon takes only if it holds the home's lock
1702
+ // This process is the home's owner: it names its pid (its claim), which the daemon takes only if it holds the home's
1703
+ // lock. It names it again to each daemon that starts while it serves: the owner a restarted daemon restores from its
1704
+ // file stands only if the kernel probe at its start agreed, and an owner it dropped lost the board's authority over the
1705
+ // panes the board launched (a close refused as "not started by this command").
1706
+ const registerOwner = async () => {
1707
+ let named = null, refusal = null;
1704
1708
  try { named = await machineCall(null, 'harness.v1.boards.register', { root, claim: { pid: process.pid, ppid: process.ppid } }, { timeoutMs: 30_000 }); } catch (error) { refusal = error.message; }
1705
1709
  const refused = named?.refused ?? null;
1706
1710
  log(refusal ? `board events: not published; the machine daemon did not take this home: ${refusal}`
1707
1711
  : refused ? `board events: registered, but the team refused this machine's credential (${refused.status}); nothing is published until it is re-enrolled`
1708
1712
  : named?.published === false ? `board ownership: registered locally; this machine has no team connector to publish events`
1709
- : `board events: this home is published by the machine's connector`); }
1713
+ : `board events: this home is published by the machine's connector`);
1714
+ };
1715
+ await registerOwner();
1716
+ onLocalReopen(registerOwner);
1710
1717
  log('dispatch wakes on board writes, document edits and board mail; the interval sweep reads each machine\'s own door');
1711
1718
  const stuck = {};
1712
1719
  const warned = {};
@@ -2053,9 +2053,23 @@ export async function completeAdoptions(board, fleet, settings, report, lanes =
2053
2053
  report.queued.push([row.task_id, `adoption of ${asked.session} waits: ${message}`]);
2054
2054
  continue;
2055
2055
  }
2056
+ // A reading that was not taken (the fleet or the machine's harness door did not answer) is no answer about the
2057
+ // session, before this round wrote anything outside the board: the request stays open and the next round reads again
2058
+ if ((error.unread || error.launchDeferred) && !progress.claimed) {
2059
+ report.queued.push([row.task_id, `adoption of ${asked.session} waits for a reading: ${message}`]);
2060
+ continue;
2061
+ }
2056
2062
  // the failure answers the request only while it is still open, or claimed by this round: an adoption that bound and
2057
2063
  // then failed after (its agent run not recorded) is bound, and a request another round claimed is that round's
2058
- const answered = !error.superseded && tx(board, db => adoptionOpenTo(db, row.task_id, request.id, row.id) && (event(db, row.task_id, 'adoption_failed', { session: asked.session, error: message }), true));
2064
+ // A failed adoption answers its request and ends the hold the request put on the card: the card is held again
2065
+ // only if it was held before the request (an incident's hold, kept for its own release), and is otherwise the
2066
+ // dispatcher's again. A hold that outlived its failed request held cards nobody adopted until a person released them.
2067
+ const answered = !error.superseded && tx(board, db => {
2068
+ if (!adoptionOpenTo(db, row.task_id, request.id, row.id)) return false;
2069
+ event(db, row.task_id, 'adoption_failed', { session: asked.session, error: message });
2070
+ restoreHoldBefore(db, row.task_id, request.id);
2071
+ return true;
2072
+ });
2059
2073
  // A request another round answered or claimed first is that round's, not a failure of this one: it is noted where
2060
2074
  // no round prints it. A failure after this round's own claim (bound, then its agent run not recorded) is one.
2061
2075
  if (!answered && !progress.claimed) { (report.adoption_skipped ??= []).push([row.task_id, message]); continue; }
@@ -2064,6 +2078,19 @@ export async function completeAdoptions(board, fleet, settings, report, lanes =
2064
2078
  }
2065
2079
  }
2066
2080
 
2081
+ /**
2082
+ * The card's hold as it stood before adoption request `request` (an event id), restored once that request failed: a
2083
+ * hold the request found (an incident's) is written again as it was, so its own release still finds it; a card that was
2084
+ * not held, or held only for this adoption (`create --adopt`'s hold, an earlier request this one answers), is released.
2085
+ */
2086
+ const ADOPTION_HOLDS = new Set(['adoption requested', 'awaiting adoption']);
2087
+ export function restoreHoldBefore(db, id, request) {
2088
+ const before = db.prepare("SELECT kind, payload FROM task_events WHERE task_id = ? AND id < ? AND kind IN ('dispatch_held','dispatch_released','session_adopted') ORDER BY id DESC LIMIT 1").get(id, request);
2089
+ const held = before?.kind === 'dispatch_held' ? json(before.payload, {}) ?? {} : null;
2090
+ if (held && !ADOPTION_HOLDS.has(held.reason)) { const { by: _by, ...kept } = held; event(db, id, 'dispatch_held', { ...kept, restored_after: request }); }
2091
+ else event(db, id, 'dispatch_released', { reason: 'adoption failed', request });
2092
+ }
2093
+
2067
2094
  /** Whether request `request` is still this round's to answer: the card's latest answer is `open`, or this process's claim of it. */
2068
2095
  function adoptionOpenTo(db, id, request, open) {
2069
2096
  const last = latestAnswer(db, id);
@@ -2078,8 +2105,9 @@ async function nativeListing(who) {
2078
2105
  const m = /^sc:([^:]+):([^:]+):(.+)$/.exec(String(who));
2079
2106
  if (!m || !HARNESSES[m[2]]) return null;
2080
2107
  const [, host, harness, session_id] = m;
2108
+ // a discovery its machine did not answer is unknown, not absent: it is thrown (unread), never taken as no listing
2081
2109
  try { return nativeReading(await readNative(host, harness, session_id), { session_id, harness }).alive === true ? { address: who, machine: normalMachine(host), harness, session_id, pane: null, name: null } : null; }
2082
- catch { return null; }
2110
+ catch (error) { if (error.launchDeferred) throw error; return null; }
2083
2111
  }
2084
2112
 
2085
2113
  /** The verb an adoption with no current run fires: the workflow's own `adopt` where it declares one, else its dispatch start. */
@@ -2100,7 +2128,8 @@ function adoptedRole(db, board, entry, task, run) {
2100
2128
 
2101
2129
  async function adoptSession(board, id, who, { request = null, open = request, progress = {}, workspace = null, ttl, assignee = null, role = null, fleet = null, lanes = null } = {}) {
2102
2130
  fleet ??= await observeFleet();
2103
- if (!fleet.ok) throw new Error(`supercode could not read the fleet: ${fleet.error}`);
2131
+ // a fleet that could not be read says nothing of the session: the request stays open for the next reading
2132
+ if (!fleet.ok) throw Object.assign(new Error(`supercode could not read the fleet: ${fleet.error}`), { unread: true });
2104
2133
  const found = fleet.sessions.find((s) => s.address === who || s.address.endsWith(`:${who}`) || s.name === who) ?? await nativeListing(who);
2105
2134
  if (!found) throw Object.assign(new Error(`the fleet lists no live session ${who}`), { notListed: true });
2106
2135
  const [, host, harness, sessionId] = found.address.split(':');
package/board/doors.mjs CHANGED
@@ -9,6 +9,12 @@ import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
9
9
  import { noteStep } from '@volter/supercode-harness-sdk/slow-log';
10
10
 
11
11
  const held = new Map(); // machine name, '' for this one → Promise<{ channel, node, harness }>
12
+ // Called each time a channel to this machine's daemon opens again after one closed (the daemon restarted): what this
13
+ // process told the daemon it had held only while that daemon ran.
14
+ const reopened = new Set();
15
+ let localOpened = false;
16
+ /** Run `fn` each time the channel to this machine's daemon opens again (a daemon restart). */
17
+ export function onLocalReopen(fn) { reopened.add(fn); }
12
18
 
13
19
  async function opened(machine) {
14
20
  const key = machine ?? '';
@@ -25,6 +31,7 @@ async function opened(machine) {
25
31
  return door;
26
32
  })();
27
33
  held.set(key, opening);
34
+ if (!key) opening.then(() => { if (localOpened) for (const fn of reopened) Promise.resolve().then(fn).catch(() => {}); localOpened = true; }, () => {});
28
35
  try { return await opening; } catch (error) { held.delete(key); throw error; }
29
36
  }
30
37
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.116",
3
+ "version": "0.5.118",
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": {