@volter/supercode-orchestrator 0.5.228 → 0.5.230

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 (80) hide show
  1. package/README.md +18 -2
  2. package/activation.mjs +1 -1
  3. package/adapters/agent-mailbox.mjs +10 -0
  4. package/adapters/api-server.mjs +6 -0
  5. package/agents.mjs +149 -40
  6. package/apply/doors.mjs +3 -1
  7. package/bin/agent-channel.mjs +7 -2
  8. package/bin/connection-runner.mjs +60 -19
  9. package/bin/hermes-worker.mjs +2 -1
  10. package/bin/orchestrator.mjs +3 -3
  11. package/bin/runtime.mjs +3 -2
  12. package/board/agent-current.mjs +82 -0
  13. package/board/agent-declarations.mjs +10 -3
  14. package/board/args.mjs +137 -6
  15. package/board/call.mjs +16 -1
  16. package/board/caller-session.mjs +3 -1
  17. package/board/capacity.mjs +59 -45
  18. package/board/card-state.mjs +14 -5
  19. package/board/ci.mjs +521 -178
  20. package/board/cli.mjs +614 -209
  21. package/board/client.mjs +64 -21
  22. package/board/close-current.mjs +36 -0
  23. package/board/current-records.mjs +44 -0
  24. package/board/custody.mjs +2 -1
  25. package/board/dispatch.mjs +1973 -846
  26. package/board/doors.mjs +2 -2
  27. package/board/engine.mjs +623 -85
  28. package/board/event-stream.mjs +10 -5
  29. package/board/fence.mjs +2 -0
  30. package/board/files.mjs +93 -62
  31. package/board/generation-owner.mjs +188 -53
  32. package/board/git-document.mjs +3 -1
  33. package/board/github-rest.mjs +29 -7
  34. package/board/incidents.mjs +5 -17
  35. package/board/listing.mjs +14 -8
  36. package/board/local-admission.mjs +59 -0
  37. package/board/lock-holder.mjs +18 -6
  38. package/board/machine-fleet.mjs +66 -13
  39. package/board/owner-door-worker.mjs +3 -3
  40. package/board/owner-door.mjs +17 -7
  41. package/board/owner-health.mjs +12 -15
  42. package/board/owner-interface.mjs +6 -0
  43. package/board/publication-reader.mjs +50 -0
  44. package/board/published.mjs +40 -12
  45. package/board/queries.mjs +24 -4
  46. package/board/record-view.mjs +30 -3
  47. package/board/request-mail.mjs +37 -6
  48. package/board/resting-admission.mjs +318 -0
  49. package/board/resting-codec-qualifications.json +33 -0
  50. package/board/resting-protocol.mjs +71 -0
  51. package/board/resting-state-protocol.json +120 -0
  52. package/board/resting-state.mjs +113 -0
  53. package/board/runtime-door.mjs +2 -2
  54. package/board/session-continuity.mjs +127 -0
  55. package/board/session-work.mjs +25 -13
  56. package/board/standalone-declaration.mjs +24 -0
  57. package/board/storage-contract.mjs +129 -7
  58. package/board/store.mjs +73 -11
  59. package/board/tools.mjs +56 -13
  60. package/board/wake.mjs +18 -5
  61. package/board/workflow.mjs +284 -82
  62. package/board/workspace.mjs +86 -9
  63. package/board/ztrack.mjs +344 -91
  64. package/connection.mjs +72 -17
  65. package/gates.mjs +2 -1
  66. package/hand-off.mjs +4 -3
  67. package/hermes-cli.mjs +5 -3
  68. package/hermes-locks.mjs +5 -2
  69. package/hosted.mjs +2 -1
  70. package/index.d.ts +2 -0
  71. package/index.mjs +1 -1
  72. package/kanban-worker.mjs +63 -13
  73. package/loop.mjs +33 -0
  74. package/mcp/hermes-kanban-tools.json +109 -50
  75. package/mcp/server.mjs +7 -1
  76. package/ontology.d.ts +26 -0
  77. package/orchestration.mjs +15 -4
  78. package/package.json +19 -12
  79. package/session-recovery.mjs +2 -1
  80. package/worker-route.mjs +37 -19
@@ -2,11 +2,12 @@
2
2
  // caps; each names where it keeps one count (`scope`), what it counts (`counts`), which cards and sessions it is about
3
3
  // (`where`, less `unless`) and its `max`. This module counts what a home holds now and answers whether one more card may start.
4
4
 
5
- import { json } from './store.mjs';
6
- import { detachedSession } from './custody.mjs';
7
- import { statusOf } from './workflow.mjs';
5
+ import { json, RUN_SLOT, runField } from './store.mjs';
6
+ import { currentRecordChanges, currentRecordConnection } from './current-records.mjs';
7
+ import { sessionAddress } from './custody.mjs';
8
8
  import { setImmediate as yieldToOwner } from 'node:timers/promises';
9
- import { listRevision } from './listing.mjs';
9
+ import { listRevision, readBoundary } from './listing.mjs';
10
+ import { backlogQualification } from './resting-state.mjs';
10
11
 
11
12
  /** The scope's group a counted thing or a card falls in. */
12
13
  const GROUP = {
@@ -38,26 +39,49 @@ export function matches(filter, thing) {
38
39
  /** Whether a cap is about `thing`: its `where` matches and its `unless` (when it names anything) does not. */
39
40
  const applies = (cap, thing) => matches(cap.where, thing) && !(Object.keys(cap.unless ?? {}).length && matches(cap.unless, thing));
40
41
 
41
- /** Each slot's latest recorded session on a card (implementer `worker`, `reviewer`, …), newest run first. */
42
- async function sessionsOf(db, taskId, unchanged) {
43
- const out = new Map();
44
- let cursor = null;
45
- for (;;) {
46
- if (!unchanged()) return null;
47
- // Page by the indexed run id, not an offset. Include null metadata so even a long empty history
48
- // reads only 64 records per statement. Every page is visited: an older, distinct slot still counts.
49
- const rows = db.prepare(`SELECT id, metadata FROM task_runs WHERE task_id = ?${cursor === null ? '' : ' AND id < ?'} ORDER BY id DESC LIMIT 64`)
50
- .all(taskId, ...(cursor === null ? [] : [cursor]));
51
- for (const row of rows) {
52
- const s = json(row.metadata, {})?.supercode;
53
- const slot = s?.slot ?? 'worker';
54
- if (s?.session_id && !out.has(slot)) out.set(slot, detachedSession(db, s) ? null : { ...s, run: row.id });
42
+ // Held card rows and latest retained role slots, never a cached fleet/capacity verdict.
43
+ const projections = new WeakMap();
44
+ const CARD_ROWS = `SELECT t.id,t.status,COALESCE(c.stage,t.status) AS stage,t.assignee,t.current_run_id,t.model_override,
45
+ c.machine,c.subtask_of,c.paused,c.backlog,c.blocked_by,c.archived,c.detached FROM tasks t LEFT JOIN supercode_cards c ON c.task_id=t.id`;
46
+ async function currentHoldings(db, unchanged) {
47
+ const projection = projections.get(currentRecordConnection(db)) ?? { token: null, cards: new Map() };
48
+ projections.set(currentRecordConnection(db), projection);
49
+ const change = currentRecordChanges(db, projection.token);
50
+ const refresh = (row) => {
51
+ if (!row || row.status === 'archived' || row.detached != null) return;
52
+ // A flag cannot erase unresolved work, even if an external SQLite writer bypassed import admission.
53
+ if (json(row.backlog, null) && backlogQualification(db, row.id).qualified) return;
54
+ // The index groups a card's slot names; only their newest session-bearing run is decoded.
55
+ const slots = db.prepare(`SELECT id,metadata FROM task_runs WHERE id IN (
56
+ SELECT MAX(id) FROM task_runs WHERE task_id=? AND ${runField('session_id')} IS NOT NULL
57
+ AND ${runField('session_id')} NOT IN ('',0) GROUP BY ${RUN_SLOT}) ORDER BY id DESC`).all(row.id)
58
+ .map(run => ({ ...json(run.metadata, {})?.supercode, run: run.id }));
59
+ const current = row.current_run_id ? { ...json(db.prepare('SELECT metadata FROM task_runs WHERE id=?').get(row.current_run_id)?.metadata, {})?.supercode, run: row.current_run_id } : null;
60
+ projection.cards.set(row.id, { row, slots, current });
61
+ };
62
+ if (change.reset) {
63
+ projection.cards.clear();
64
+ let cursor = '';
65
+ for (;;) {
66
+ await yieldToOwner(); if (!unchanged()) return null;
67
+ const rows = readBoundary(db, () => db.prepare(`${CARD_ROWS} WHERE t.id>? ORDER BY t.id LIMIT 32`).all(cursor));
68
+ if (!rows.length) break;
69
+ readBoundary(db, () => { for (const row of rows) refresh(row); });
70
+ cursor = rows.at(-1).id;
71
+ if (rows.length < 32) break;
72
+ }
73
+ } else {
74
+ for (let at = 0; at < change.cards.length; at += 32) {
75
+ await yieldToOwner(); if (!unchanged()) return null;
76
+ readBoundary(db, () => { for (const id of change.cards.slice(at, at + 32)) {
77
+ projection.cards.delete(id);
78
+ refresh(db.prepare(`${CARD_ROWS} WHERE t.id=?`).get(id));
79
+ } });
55
80
  }
56
- if (rows.length < 64) break;
57
- cursor = rows.at(-1).id;
58
- await yieldToOwner();
59
81
  }
60
- return new Map([...out].filter(([, s]) => s));
82
+ if (change.reset || change.cards.length) projection.cards = new Map([...projection.cards].sort(([a],[b]) => a < b ? -1 : a > b ? 1 : 0));
83
+ projection.token = change.token;
84
+ return projection.cards;
61
85
  }
62
86
 
63
87
  const sameRevision = (a, b) => Object.keys(a).every(key => a[key] === b[key]);
@@ -89,28 +113,18 @@ export async function holdings(board, options) {
89
113
  */
90
114
  async function readHoldings(board, { lanes, fleetOk, alive, liveRecord, modelOf = () => null, storedOf = null, machineOf, home = null, runLaneOf = null }, unchanged) {
91
115
  const out = [];
92
- let cursor = '';
93
- const high = board.db.prepare('SELECT MAX(id) AS id FROM tasks').get()?.id ?? '';
94
- for (;;) {
95
- await yieldToOwner();
96
- if (!unchanged()) return null;
97
- // Limit candidates before filtering or joining: even a page containing only archived/detached cards is bounded.
98
- const candidates = board.db.prepare('SELECT id FROM tasks WHERE id>? AND id<=? ORDER BY id LIMIT 32').all(cursor, high);
99
- if (!candidates.length) break;
100
- cursor = candidates.at(-1).id;
101
- const rows = board.db.prepare(`SELECT t.id,t.status,t.assignee,t.current_run_id,t.model_override,c.machine,c.subtask_of
102
- FROM tasks t LEFT JOIN supercode_cards c ON c.task_id=t.id
103
- WHERE t.id IN (SELECT value FROM json_each(?)) AND t.status!='archived' AND c.detached IS NULL`)
104
- .all(JSON.stringify(candidates.map(row => row.id)));
105
- for (const row of rows) {
106
- await yieldToOwner();
107
- if (!unchanged()) return null;
116
+ const rows = await currentHoldings(board.db, unchanged);
117
+ if (rows === null) return null;
118
+ const custody = new Set(board.db.prepare('SELECT address FROM supercode_session_custody').all().map(row => row.address));
119
+ let at = 0;
120
+ for (const { row, slots, current: retainedCurrent } of rows.values()) {
121
+ if (at++ % 32 === 0) { await yieldToOwner(); if (!unchanged()) return null; }
108
122
  const lane = lanes[row.assignee] ?? {};
109
123
  const card = {
110
- card: row.id, board: board.slug, assignee: row.assignee ?? null, status: statusOf(board.db, row),
124
+ card: row.id, board: board.slug, assignee: row.assignee ?? null, status: row.stage,
111
125
  // what the board stores for an arc is read through its workflow's stored_as rules now, never the column a write has
112
126
  // not yet brought up to date (a paused card counted as running until its row caught up)
113
- stored: !row.subtask_of && storedOf ? storedOf(row.id) : row.status,
127
+ stored: json(row.backlog, null) ? row.stage : !row.subtask_of && storedOf ? storedOf(row.id, row) : row.status,
114
128
  // a card that names no machine runs on this home's: a cap on this machine counts it
115
129
  machine: row.machine ? machineOf(row.machine) : home,
116
130
  model: row.model_override ?? lane.model ?? null, harness: lane.harness ?? null,
@@ -124,15 +138,16 @@ async function readHoldings(board, { lanes, fleetOk, alive, liveRecord, modelOf
124
138
  : executionHost(runLaneOf ? runLaneOf(row) : lane, row.machine, home, machineOf), harness: s.harness ?? card.harness ?? 'claude-code',
125
139
  model: (s.session_id ? modelOf(s) : null) ?? configured };
126
140
  };
127
- const sessions = fleetOk ? await sessionsOf(board.db, row.id, unchanged) : new Map();
128
- if (sessions === null) return null;
141
+ const sessions = fleetOk ? slots : [];
129
142
  const held = [];
130
143
  let current = null;
131
144
  if (row.current_run_id) {
132
- current = { ...json(board.db.prepare('SELECT metadata FROM task_runs WHERE id = ?').get(row.current_run_id)?.metadata, {})?.supercode, run: row.current_run_id };
145
+ current = retainedCurrent;
133
146
  out.push({ ...sessionOf(current, current.slot ?? 'worker'), session: undefined, slot: undefined, counts: 'runs' });
134
147
  }
135
- for (const [slot, s] of sessions) {
148
+ for (const s of sessions) {
149
+ const slot = s.slot ?? 'worker';
150
+ if (s.detached_at || s.close_confirmed_at || s.released || custody.has(sessionAddress(s))) continue;
136
151
  if (!alive(s)) continue;
137
152
  const session = sessionOf(s, slot);
138
153
  held.push(session);
@@ -145,7 +160,6 @@ async function readHoldings(board, { lanes, fleetOk, alive, liveRecord, modelOf
145
160
  // until it is done); its model and harness are its run's session's, else its newest live session's, else its own
146
161
  // a subtask is a task of its arc: it never runs or holds a slot of its own
147
162
  if (!row.subtask_of) out.push({ ...(held[0] ?? { ...card, unconfigured: !card.model }), session: undefined, slot: undefined, counts: 'cards' });
148
- }
149
163
  }
150
164
  return out;
151
165
  }
@@ -11,19 +11,28 @@ export const cardStateSql = ({ nativeHolds = false } = {}) => nativeHolds ? `CAS
11
11
  WHEN COALESCE(c.stage,t.status) IN ('done','cancelled','archived') OR t.status = 'archived' THEN t.status
12
12
  WHEN c.paused IS NOT NULL AND c.paused != 'null' THEN 'paused'
13
13
  WHEN CASE WHEN json_valid(c.blocked_by) THEN json_array_length(c.blocked_by) ELSE 0 END > 0 THEN 'blocked'
14
+ WHEN c.backlog IS NOT NULL AND c.backlog != 'null' THEN 'todo'
14
15
  WHEN t.status IN ('paused','blocked') THEN CASE WHEN COALESCE(c.stage,t.status) IN ('paused','blocked') THEN 'todo' ELSE COALESCE(c.stage,t.status) END
15
16
  ELSE t.status END` : 't.status';
16
17
 
17
18
  export function cardState(task, { nativeHolds = false } = {}) {
18
19
  const paused = parse(task.paused, null);
20
+ const backlog = parse(task.backlog, null);
19
21
  const blocked_by = parse(task.blocked_by, []);
20
22
  const lane = task.stage ?? task.workflow_status ?? task.status;
21
23
  const ended = ['done', 'cancelled', 'archived'].includes(lane) || task.status === 'archived';
22
24
  const status = !nativeHolds ? task.status : ended ? task.status : paused ? 'paused' : blocked_by.length ? 'blocked'
23
- : task.status === 'blocked' || task.status === 'paused' ? (['blocked', 'paused'].includes(lane) ? 'todo' : lane) : task.status;
24
- return { ...task, status, paused, blocked_by };
25
+ : backlog ? 'todo' : task.status === 'blocked' || task.status === 'paused' ? (['blocked', 'paused'].includes(lane) ? 'todo' : lane) : task.status;
26
+ return { ...task, status, paused, backlog, blocked_by };
25
27
  }
26
28
 
27
- export const holdDescription = (task) => task.status === 'paused' && task.paused
28
- ? `paused by ${task.paused.by ?? 'unknown'}: ${task.paused.reason ?? 'reason not recorded'}`
29
- : task.status === 'blocked' && task.blocked_by?.length ? `blocked by ${task.blocked_by.map(b => `${b.id}${b.reason ? `: ${b.reason}` : ''}`).join(', ')}` : null;
29
+ const deadlineText = (until) => {
30
+ const date = new Date(Number.isFinite(until) ? until * 1000 : NaN);
31
+ return Number.isFinite(date.getTime()) ? date.toISOString() : null;
32
+ };
33
+
34
+ export const holdDescription = (task) => [
35
+ task.backlog && `backlog by ${task.backlog.by ?? 'unknown'}: ${task.backlog.reason ?? 'reason not recorded'}`,
36
+ task.status === 'paused' && task.paused && `paused by ${task.paused.by ?? 'unknown'}${deadlineText(task.paused.until) ? ` until ${deadlineText(task.paused.until)}` : ' (deadline not recorded)'}: ${task.paused.reason ?? 'reason not recorded'}`,
37
+ task.status === 'blocked' && task.blocked_by?.length && `blocked by ${task.blocked_by.map(b => `${b.id}${b.reason ? `: ${b.reason}` : ''}`).join(', ')}`,
38
+ ].filter(Boolean).join('; ') || null;