@volter/supercode-orchestrator 0.5.205 → 0.5.207

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 (52) hide show
  1. package/README.md +138 -4
  2. package/activation.mjs +93 -2
  3. package/adapters/rh2.mjs +7 -2
  4. package/agents.mjs +24 -2
  5. package/bin/agent-channel.mjs +41 -22
  6. package/bin/connection-runner.mjs +141 -0
  7. package/bin/orchestrator.mjs +11 -2
  8. package/bin/runtime.mjs +16 -9
  9. package/board/args.mjs +3 -2
  10. package/board/attachment-read.mjs +77 -0
  11. package/board/call.mjs +4 -2
  12. package/board/capacity.mjs +46 -9
  13. package/board/ci.mjs +157 -53
  14. package/board/cli.mjs +278 -97
  15. package/board/client.mjs +18 -2
  16. package/board/custody.mjs +109 -0
  17. package/board/dispatch.mjs +305 -80
  18. package/board/doors.mjs +47 -17
  19. package/board/engine.mjs +92 -27
  20. package/board/files.mjs +17 -3
  21. package/board/fleet-listeners.mjs +268 -0
  22. package/board/fleet-observer.mjs +79 -0
  23. package/board/fleet-targets.mjs +82 -0
  24. package/board/incidents.mjs +7 -0
  25. package/board/listing.mjs +8 -2
  26. package/board/machine-fleet.mjs +7 -0
  27. package/board/notice-receipts.mjs +67 -0
  28. package/board/notices.mjs +5 -0
  29. package/board/owner-door-worker.mjs +61 -0
  30. package/board/owner-door.mjs +46 -0
  31. package/board/owner-health.mjs +8 -2
  32. package/board/published.mjs +2 -2
  33. package/board/record-view.mjs +2 -0
  34. package/board/store.mjs +84 -3
  35. package/board/tools.mjs +11 -4
  36. package/board/workflow.mjs +96 -31
  37. package/board/ztrack.mjs +207 -32
  38. package/board-input.d.ts +2 -0
  39. package/board-input.mjs +16 -0
  40. package/connection.mjs +153 -0
  41. package/index.d.ts +7 -5
  42. package/index.mjs +1 -1
  43. package/installed-apps.mjs +31 -20
  44. package/loop.mjs +91 -36
  45. package/mcp/bridge.mjs +12 -0
  46. package/mcp/hermes-kanban-tools.json +22 -14
  47. package/mcp/mount.mjs +10 -9
  48. package/ontology.d.ts +37 -0
  49. package/operator.mjs +72 -4
  50. package/orchestration.mjs +7 -2
  51. package/package.json +9 -5
  52. package/reducer.mjs +74 -5
package/board/client.mjs CHANGED
@@ -7,6 +7,7 @@ import { mkdirSync, readFileSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
8
  import { homeOf, parse } from './args.mjs';
9
9
  import { processSession } from './caller-session.mjs';
10
+ import { noteCommandError, noteStep } from '@volter/supercode-harness-sdk/slow-log';
10
11
 
11
12
  /**
12
13
  * `supercode workflow <verb>`: the CLI holds nothing. A card verb is sent to the home's board owner (`workflow serve`,
@@ -38,6 +39,7 @@ export async function runWorkflow(argv) {
38
39
  if (writes) {
39
40
  try { lock = await takeHome(root); }
40
41
  catch (error) {
42
+ noteCommandError(error, { phase: 'board ownership' });
41
43
  process.stderr.write(`supercode workflow: ${verb} writes the boards of ${root}, which have a running owner (${error.message}); stop \`supercode workflow serve\` to run it\n`);
42
44
  return 1;
43
45
  }
@@ -68,7 +70,10 @@ async function takeHome(root) {
68
70
  }
69
71
 
70
72
  async function sendToOwner(root, argv, args) {
73
+ const began = performance.now();
71
74
  const sid = processSession();
75
+ noteStep('board caller session', performance.now() - began);
76
+ const preparing = performance.now();
72
77
  const harness = sid ? (sid === process.env.CODEX_THREAD_ID ? 'codex' : 'claude-code') : null;
73
78
  // a file named `-` is the caller's standard input, read here and sent with the verb
74
79
  const stdin = [args.file, args['body-file']].includes('-') ? readFileSync(0, 'utf8') : null;
@@ -76,9 +81,20 @@ async function sendToOwner(root, argv, args) {
76
81
  // board it names, else the one its environment names) and its Teams context (named, else the one selected here).
77
82
  const board = args.board ?? process.env.HERMES_KANBAN_BOARD ?? null;
78
83
  const facts = { ...callerFacts(), SUPERCODE_CONTEXT: process.env.SUPERCODE_CONTEXT || new WorkspaceContexts(supercodeHome()).read().current || 'local' };
79
- const answer = await askBoardOwner(root, { argv: withoutHome(argv), board, caller: null, session: sid ? { id: sid, harness } : null, facts, cwd: process.cwd(), stdin });
84
+ noteStep('board request preparation', performance.now() - preparing);
85
+ const asking = performance.now();
86
+ let answer;
87
+ try {
88
+ answer = await askBoardOwner(root, { argv: withoutHome(argv), board, caller: null, session: sid ? { id: sid, harness } : null, facts, cwd: process.cwd(), stdin });
89
+ } finally { noteStep('board owner answer', performance.now() - asking); }
80
90
  // the owner took the verb and the call ended with no answer: its outcome is unknown, never a plain failure
81
- if (answer.unanswered === true) { process.stderr.write(`supercode workflow: ${unansweredText(answer, `\`${withoutHome(argv).join(' ')}\``)}`); return 1; }
91
+ if (answer.unanswered === true) {
92
+ const cause = unansweredText(answer, 'the workflow request');
93
+ noteCommandError({ code: 'owner_unanswered', exitCode: 1 }, { phase: 'board owner answer' });
94
+ process.stderr.write(`supercode workflow: ${cause}`);
95
+ return 1;
96
+ }
97
+ if (answer.code !== 0) noteCommandError({ code: 'owner_rejected', exitCode: answer.code }, { phase: 'board owner answer' });
82
98
  if (answer.stdout) process.stdout.write(answer.stdout);
83
99
  if (answer.stderr) process.stderr.write(answer.stderr);
84
100
  return answer.code ?? 1;
@@ -0,0 +1,109 @@
1
+ // Native identity survives a handback. Custody decides what this board may do with it.
2
+ import { randomUUID } from 'node:crypto';
3
+ import { hostname } from 'node:os';
4
+ import { actorOf, claimer, event, json, now, tx } from './store.mjs';
5
+
6
+ export const sessionAddress = s => s?.address ?? (s?.machine && s?.harness && s?.session_id ? `sc:${s.machine}:${s.harness}:${s.session_id}` : null);
7
+ export const detachedCard = (db, id) => json(db.prepare('SELECT detached FROM supercode_cards WHERE task_id=?').get(id)?.detached, null);
8
+ export const personCustody = (db, address) => address ? db.prepare('SELECT * FROM supercode_session_custody WHERE address=?').get(address) ?? null : null;
9
+ export const detachedSession = (db, session) => Boolean(session?.detached_at || personCustody(db, sessionAddress(session)));
10
+
11
+ // No timer releases an operation that may still be inside a native door. Only its
12
+ // finally, or positive absence of its process on this host, releases the reservation.
13
+ function operationAlive(row) {
14
+ const at = row.owner.lastIndexOf(':');
15
+ if (row.owner.slice(0, at) !== hostname()) return true;
16
+ const pid = Number(row.owner.slice(at + 1));
17
+ if (!Number.isInteger(pid) || pid <= 0) return true;
18
+ try { process.kill(pid, 0); return true; }
19
+ catch (error) { return error.code !== 'ESRCH'; }
20
+ }
21
+
22
+ export function assertNoSessionOperation(db, id, address) {
23
+ for (const row of db.prepare('SELECT * FROM supercode_session_operations WHERE card_ref=? OR address=?').all(id, address)) {
24
+ if (operationAlive(row)) throw new Error(`cannot detach while ${row.kind} is in progress (${row.owner}); wait for that native operation to finish`);
25
+ db.prepare('DELETE FROM supercode_session_operations WHERE token=?').run(row.token);
26
+ }
27
+ }
28
+
29
+ /** Reserve ownership before an await. Detach and a native mutation cannot both win. */
30
+ export async function sessionOperation(board, { taskId = null, address = null, kind }, perform) {
31
+ const token = randomUUID();
32
+ tx(board, db => {
33
+ const custody = personCustody(db, address);
34
+ if ((taskId && detachedCard(db, taskId)) || custody) throw Object.assign(new Error(`session belongs to ${custody?.person ?? 'a person'}; verified adoption is required before ${kind}`), { sessionDetached: true });
35
+ db.prepare('INSERT INTO supercode_session_operations(token,card_ref,address,kind,owner,started_at) VALUES(?,?,?,?,?,?)').run(token, taskId, address, kind, claimer(), now());
36
+ });
37
+ try { return await perform(); }
38
+ finally { tx(board, db => db.prepare('DELETE FROM supercode_session_operations WHERE token=?').run(token)); }
39
+ }
40
+
41
+ /** Give the card's one held native session to a person, without stopping it. */
42
+ export function detachSession(board, id, person) {
43
+ if (!/^person:\S+$/.test(String(person ?? ''))) throw new Error('detach requires --to person:<id>, or a signed-in person caller');
44
+ return tx(board, db => {
45
+ const task = db.prepare('SELECT * FROM tasks WHERE id=?').get(id);
46
+ if (!task) throw new Error(`no card ${id}`);
47
+ const prior = detachedCard(db, id);
48
+ if (prior) {
49
+ if (prior.person !== person) throw new Error(`${id} already belongs to ${prior.person}; adopt it before another handback`);
50
+ return prior;
51
+ }
52
+ const rows = db.prepare('SELECT * FROM task_runs WHERE task_id=? ORDER BY id DESC').all(id);
53
+ const latest = new Map();
54
+ for (const row of rows) {
55
+ const session = json(row.metadata, {})?.supercode;
56
+ if (session?.launch_pending && !session.close_confirmed_at && !session.close_transferred_to) throw new Error('an unresolved launch remains on this card; settle its receipt before detaching');
57
+ if (!session || latest.has(session.slot ?? 'worker')) continue;
58
+ latest.set(session.slot ?? 'worker', { row, session });
59
+ }
60
+ const held = [...latest.values()].filter(({ session: s }) => sessionAddress(s) && !s.forgotten && !s.close_transferred_to && !(s.close_confirmed_at && !s.released) && !detachedSession(db, s));
61
+ const chosen = held.find(({ row }) => row.id === task.current_run_id) ?? held[0];
62
+ if (!chosen) throw new Error(`${id} has no bound native session to detach`);
63
+ const { row, session } = chosen, address = sessionAddress(session);
64
+ if (!session.session_id || !session.machine || !session.harness || session.launch_pending) throw new Error('a pending or unidentified launch cannot be detached');
65
+ if (task.current_run_id && task.current_run_id !== row.id) throw new Error('the current run has no bound native session; wait for its launch to settle');
66
+ if (held.some(item => sessionAddress(item.session) !== address)) throw new Error(`${id} holds other session slots; close those through the board before detaching its remaining session`);
67
+ if (rows.some(reference => {
68
+ const s = json(reference.metadata, {})?.supercode;
69
+ return sessionAddress(s) && sessionAddress(s) !== address && s.close_requested && !s.close_confirmed_at && !s.close_transferred_to && !s.detached_at;
70
+ })) throw new Error('another native session on this card has an unresolved close; settle that receipt before detaching');
71
+ assertNoSessionOperation(db, id, address);
72
+ const adoption = db.prepare("SELECT id,kind,payload FROM task_events WHERE task_id=? AND kind IN ('dispatch_held','dispatch_released','session_adopted','adoption_failed','adoption_claimed') ORDER BY id DESC LIMIT 1").get(id);
73
+ const request = json(adoption?.payload, {});
74
+ if (adoption?.kind === 'adoption_claimed' && request.lock && operationAlive({ owner: request.lock })) throw new Error('an adoption is already being bound; wait for its answer before detaching');
75
+ for (const other of db.prepare('SELECT r.id,r.task_id,r.metadata FROM task_runs r JOIN tasks t ON t.current_run_id=r.id WHERE t.id<>?').all(id)) {
76
+ if (sessionAddress(json(other.metadata, {})?.supercode) === address) throw new Error(`${address} also belongs to ${other.task_id} run ${other.id}; its custody must be resolved first`);
77
+ }
78
+ const detached = { person, at: now(), by: actorOf(), run: row.id, session: { address, session_id: session.session_id, machine: session.machine, harness: session.harness, pane: session.pane ?? null }, slot: session.slot ?? 'worker' };
79
+ // Every historical reference on this board loses custody, including old
80
+ // unended rows; those cannot veto a later, verified adoption's close.
81
+ const references = db.prepare(`SELECT id,metadata FROM task_runs
82
+ WHERE json_extract(metadata,'$.supercode.address')=? OR
83
+ (json_extract(metadata,'$.supercode.machine')=? AND json_extract(metadata,'$.supercode.harness')=? AND json_extract(metadata,'$.supercode.session_id')=?)`)
84
+ .all(address, session.machine, session.harness, session.session_id);
85
+ for (const reference of references) {
86
+ const metadata = json(reference.metadata, {}), s = metadata.supercode;
87
+ if (sessionAddress(s) !== address) continue;
88
+ if (s.close_claim?.lock && Number(s.close_claim.expires) > now()) throw new Error(`a close of ${address} is claimed; wait for its closer to finish`);
89
+ metadata.supercode = { ...s, detached_at: detached.at, detached_to: person, resumable: false, lifecycle_notice_pending: null };
90
+ db.prepare("UPDATE task_runs SET metadata=?,ended_at=COALESCE(ended_at,?),status=CASE WHEN ended_at IS NULL THEN 'detached' ELSE status END,outcome=CASE WHEN ended_at IS NULL THEN 'detached' ELSE outcome END,claim_lock=NULL,claim_expires=NULL,worker_pid=NULL WHERE id=?").run(JSON.stringify(metadata), detached.at, reference.id);
91
+ }
92
+ db.prepare('INSERT INTO supercode_cards(task_id,detached) VALUES(?,?) ON CONFLICT(task_id) DO UPDATE SET detached=excluded.detached').run(id, JSON.stringify(detached));
93
+ db.prepare('INSERT INTO supercode_session_custody(address,task_id,person,detached_at,detached_by,run_id) VALUES(?,?,?,?,?,?)').run(address, id, person, detached.at, detached.by, row.id);
94
+ db.prepare('UPDATE tasks SET current_run_id=NULL,session_id=NULL,claim_lock=NULL,claim_expires=NULL,worker_pid=NULL WHERE id=?').run(id);
95
+ // No automatic subscription on this board may keep waking the person's session.
96
+ db.prepare("DELETE FROM kanban_notify_subs WHERE platform='supercode' AND chat_id=?").run(address);
97
+ if ((adoption?.kind === 'dispatch_held' && request.reason === 'adoption requested') || adoption?.kind === 'adoption_claimed') event(db, id, 'adoption_failed', { request: request.request ?? adoption.id, session: request.session, reason: 'session detached to a person' });
98
+ event(db, id, 'session_detached', detached, row.id);
99
+ return detached;
100
+ });
101
+ }
102
+
103
+ /** Called only inside the transaction that binds a verified native adoption. */
104
+ export function takeSessionCustody(db, id, address) {
105
+ const custody = personCustody(db, address);
106
+ if (custody && custody.task_id !== id) throw new Error(`${address} was detached from ${custody.task_id}; adopt it there before moving its board ownership`);
107
+ db.prepare('DELETE FROM supercode_session_custody WHERE address=?').run(address);
108
+ db.prepare('UPDATE supercode_cards SET detached=NULL WHERE task_id=?').run(id);
109
+ }