@volter/supercode-orchestrator 0.5.115 → 0.5.117
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 +11 -4
- package/board/dispatch.mjs +17 -6
- package/board/doors.mjs +7 -0
- package/package.json +1 -1
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
|
-
|
|
1703
|
-
|
|
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 = {};
|
package/board/dispatch.mjs
CHANGED
|
@@ -172,6 +172,8 @@ const HARNESSES = {
|
|
|
172
172
|
// machine_offline; a stream that timed out, 504), says nothing about what it was asked: it is tried again next round.
|
|
173
173
|
const doorTransient = (r) => r.code === null || ['unavailable', 'machine_offline', 'rate_limited', 504, '504', 'ETIMEDOUT', 'ECONNRESET', 'EAI_AGAIN'].includes(json(String(r.stderr ?? '').trim().split('\n').pop(), null)?.code);
|
|
174
174
|
const adoptedOnly = (s) => s?.mode === 'adopt' && !s.launch_key && s.surface !== 'headless';
|
|
175
|
+
/** A close the machine refused for good (not deferred): the board will not try it again, and the session runs on. */
|
|
176
|
+
const closeRefused = (s) => Boolean(s?.close_failed_at && s.close_error && !/^close deferred/.test(s.close_error) && !s.close_confirmed_at);
|
|
175
177
|
const addressOf = (s) => s?.address ?? (s?.session_id && s.machine ? `sc:${s.machine}:${s.harness ?? 'claude-code'}:${s.session_id}` : null);
|
|
176
178
|
|
|
177
179
|
// A session in a live pane of its machine is the fleet's reading; any other run is read on its own machine: a headless
|
|
@@ -1059,7 +1061,7 @@ export async function closeRequested(board, report, { taskId = null, caller = nu
|
|
|
1059
1061
|
}
|
|
1060
1062
|
// A final refusal stops further close attempts. Only an authorized observation of this exact pane may establish
|
|
1061
1063
|
// absence: panes.list can be filtered or carry missing context keys. Unknown/invisible keeps the original failure.
|
|
1062
|
-
const finalFailure =
|
|
1064
|
+
const finalFailure = closeRefused(session);
|
|
1063
1065
|
if (finalFailure) {
|
|
1064
1066
|
if (!session.pane) continue;
|
|
1065
1067
|
const r = await doorOn(session.machine ?? localMachine(), 'harness.v1.panes.presence', { pane: session.pane }, { timeout: 10_000, as: boardAddress(board.label) });
|
|
@@ -1724,9 +1726,13 @@ async function launchPane(board, task, runId, lane, fleet, { input = null, slot
|
|
|
1724
1726
|
// session starts (never two processes on one session)
|
|
1725
1727
|
// Every old pane in the slot must be settled, including a failed launch with no session id.
|
|
1726
1728
|
await closeRequested(board, { undelivered: [] }, { taskId: task.id });
|
|
1729
|
+
// The slot's own last session whose close was refused for good is not waited on: it runs on, and is continued below
|
|
1730
|
+
// (continuationOf), never started beside.
|
|
1731
|
+
const last = view(board, (db) => lastSession(db, task.id, slot));
|
|
1727
1732
|
const pending = view(board, (db) => db.prepare('SELECT id, ended_at, metadata FROM task_runs WHERE task_id = ? AND id != ?').all(task.id, runId))
|
|
1728
1733
|
.map((r) => ({ ...r, session: json(r.metadata, {})?.supercode }))
|
|
1729
|
-
.find((r) => (r.session?.pane || r.session?.launch_key || r.session?.surface === 'native') && !r.session.close_transferred_to && (r.session.slot ?? 'worker') === slot && (r.ended_at || r.session.close_requested) && !r.session.close_confirmed_at
|
|
1734
|
+
.find((r) => (r.session?.pane || r.session?.launch_key || r.session?.surface === 'native') && !r.session.close_transferred_to && (r.session.slot ?? 'worker') === slot && (r.ended_at || r.session.close_requested) && !r.session.close_confirmed_at
|
|
1735
|
+
&& !(closeRefused(r.session) && last?.session_id && r.session.session_id === last.session_id));
|
|
1730
1736
|
if (pending) throw unavailable(`slot ${slot} still owns pane ${pending.session.pane} from run ${pending.id}; closure must be confirmed before another session starts: ${pending.session.close_error ?? 'close pending'}`);
|
|
1731
1737
|
let prev = view(board, (db) => lastSession(db, task.id, slot));
|
|
1732
1738
|
if (prev?.session_id && !prev.forgotten && (prev.harness ?? 'claude-code') === harness && prev.machine === machine) {
|
|
@@ -1740,7 +1746,7 @@ async function launchPane(board, task, runId, lane, fleet, { input = null, slot
|
|
|
1740
1746
|
if (pane && pane !== prev.pane) prev = { ...prev, pane, close_requested: null, closed_at: null, close_confirmed_at: null, close_error: null };
|
|
1741
1747
|
fleet = fresh;
|
|
1742
1748
|
}
|
|
1743
|
-
if (prev?.close_requested && !prev.closed_at) {
|
|
1749
|
+
if (prev?.close_requested && !prev.closed_at && !closeRefused(prev)) {
|
|
1744
1750
|
const report = { undelivered: [] };
|
|
1745
1751
|
await closeRequested(board, report, { taskId: task.id });
|
|
1746
1752
|
prev = view(board, (db) => lastSession(db, task.id, slot));
|
|
@@ -1810,13 +1816,18 @@ async function launchPane(board, task, runId, lane, fleet, { input = null, slot
|
|
|
1810
1816
|
if (next === 'refuse') throw new Error(`session ${prev.session_id} still has live pane ${prev.pane}; cannot resume or replace it`);
|
|
1811
1817
|
if (next === 'keep') {
|
|
1812
1818
|
// the session is kept; the heartbeat's count belongs to the run that ended, so the new run starts it again
|
|
1813
|
-
const { missing_since: _m, goal_turns: _t, goal_nudged_at: _n, idle_since: _is, unknown_since: _us, unknown_notice: _un, work_progress: _wp, idle_observation: _io, idle_below_threshold: _ib, idle_eligible: _ie, close_requested: _q, closed_at: _c, close_confirmed_at: _cc, close_error: _ce, close_attempts: _ca, close_attempted_at: _ct, monitor_held: _mh, released: _rl, ...kept } = prev;
|
|
1819
|
+
const { missing_since: _m, goal_turns: _t, goal_nudged_at: _n, idle_since: _is, unknown_since: _us, unknown_notice: _un, work_progress: _wp, idle_observation: _io, idle_below_threshold: _ib, idle_eligible: _ie, close_requested: _q, closed_at: _c, close_confirmed_at: _cc, close_error: _ce, close_attempts: _ca, close_attempted_at: _ct, close_failed_at: _cf, close_claim: _cl, monitor_held: _mh, released: _rl, ...kept } = prev;
|
|
1814
1820
|
session = { ...kept, mode: 'adopt', launched_at: now(), seen_at: now() };
|
|
1815
1821
|
// a kept session is told what this run is for (its card came back to it, with what happened since), only while
|
|
1816
1822
|
// this run is still the card's current one (its pane and native reads were awaited)
|
|
1817
1823
|
tx(board, (db) => {
|
|
1818
1824
|
if (!runCurrent(db, task.id, runId)) throw Object.assign(new Error(`run ${runId} of ${task.id} ended or was replaced while its session was read; nothing was told to it`), { launchSuperseded: true });
|
|
1819
1825
|
setRunSession(db, runId, { ...session, ...meta });
|
|
1826
|
+
// an earlier run's close of this pane is this run's now: the pane is the kept session's, not a close to retry
|
|
1827
|
+
if (prev.pane) for (const r of db.prepare('SELECT id, metadata FROM task_runs WHERE task_id = ? AND id != ?').all(task.id, runId)) {
|
|
1828
|
+
const s = json(r.metadata, {})?.supercode;
|
|
1829
|
+
if (s?.pane === prev.pane && s.machine === prev.machine && !s.close_confirmed_at && !s.close_transferred_to) setRunSession(db, r.id, { ...s, close_transferred_to: runId, close_requested: null });
|
|
1830
|
+
}
|
|
1820
1831
|
});
|
|
1821
1832
|
// filed in its mailbox, which delivers it
|
|
1822
1833
|
if (input) { assertRunCurrent(board, task.id, runId); await fileMail(board, addressOf(prev), input, `board-run-${task.id}-r${runId}`); }
|
|
@@ -1888,10 +1899,10 @@ async function launchPane(board, task, runId, lane, fleet, { input = null, slot
|
|
|
1888
1899
|
* What a slot's last session on the card's machine is to the card's next run, given whether it is live (its pane, else
|
|
1889
1900
|
* its native process): an open session that is live is kept, and the run is delivered into it; a live pane holding a
|
|
1890
1901
|
* session the board closed refuses the launch; anything else is resumed. A released session is open: its release only
|
|
1891
|
-
* freed the slot, and the session was left running.
|
|
1902
|
+
* freed the slot, and the session was left running. So is one whose close the machine refused for good.
|
|
1892
1903
|
*/
|
|
1893
1904
|
export function continuationOf(prev, live) {
|
|
1894
|
-
const open = Boolean(prev.released) || (!prev.closed_at && !prev.close_requested);
|
|
1905
|
+
const open = Boolean(prev.released) || closeRefused(prev) || (!prev.closed_at && !prev.close_requested);
|
|
1895
1906
|
if (open) return live ? 'keep' : 'resume';
|
|
1896
1907
|
return live && prev.pane ? 'refuse' : 'resume';
|
|
1897
1908
|
}
|
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.
|
|
3
|
+
"version": "0.5.117",
|
|
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": {
|