@volter/supercode-orchestrator 0.4.129 → 0.4.130
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/dispatch.mjs +31 -13
- package/board/engine.mjs +1 -1
- package/board/wake.mjs +20 -3
- package/board/workflow.mjs +14 -2
- package/package.json +1 -1
package/board/dispatch.mjs
CHANGED
|
@@ -450,16 +450,23 @@ function startCards(board, fleet, lanes, settings, report, entry, effects, { onl
|
|
|
450
450
|
return { target, runSpec: spec, profile, lane: profile ? lanes[profile] ?? null : null };
|
|
451
451
|
};
|
|
452
452
|
const candidates = [];
|
|
453
|
+
const waiting = (task, why) => {
|
|
454
|
+
report.queued.push([task.id, why]);
|
|
455
|
+
if (!dry) tx(board, db => noteGuard(db, task.id, why));
|
|
456
|
+
};
|
|
453
457
|
for (const task of everyCard) {
|
|
454
458
|
if (only && task.id !== only) continue;
|
|
455
459
|
const state = scopeFor(task).card;
|
|
456
|
-
if (state.
|
|
457
|
-
if (
|
|
458
|
-
if (
|
|
460
|
+
if (task.subtask_of || state.archived || task.current_run_id || ['done', 'cancelled'].includes(state.status)) continue;
|
|
461
|
+
if (state.dispatch_hold) { waiting(task, 'dispatch held: an explicit start or verified adoption must release it'); continue; }
|
|
462
|
+
if (workflow.run && state.paused) { waiting(task, `paused: ${state.paused.reason ?? 'manager pause'}`); continue; }
|
|
463
|
+
if (workflow.run && state.blocked_by.length) { waiting(task, `blocked by ${state.blocked_by.map(b => b.id).join(', ')}`); continue; }
|
|
464
|
+
if (workflow.run && state.every && state.next_run_at > now()) { waiting(task, `next scheduled run at ${state.next_run_at}`); continue; }
|
|
465
|
+
if (!startable.has(state.status)) { waiting(task, `no ${d.start} transition from ${state.status}`); continue; }
|
|
459
466
|
const plan = planOf(task, scopeFor(task));
|
|
460
|
-
if (!plan) continue;
|
|
467
|
+
if (!plan) { waiting(task, `start guard refused: ${transitionsFor(workflow, state.status, d.start).map(t => t.guard ?? 'sender role').join(' | ')}`); continue; }
|
|
461
468
|
const scope = scopeFor(task, { lane: plan.lane ?? {}, run: { surface: plan.lane?.surface ?? null, local: plan.lane?.surface !== 'pane' || normalMachine(task.machine ?? '') === localMachine() } });
|
|
462
|
-
if (!holds(d.eligible ?? null, scope)) continue;
|
|
469
|
+
if (!holds(d.eligible ?? null, scope)) { waiting(task, `dispatch eligibility refused (memory ${pressure}, local starts ${tickState.local_started}): ${d.eligible}`); continue; }
|
|
463
470
|
candidates.push({ task, plan, scope });
|
|
464
471
|
}
|
|
465
472
|
const keyOf = (c) => (d.order ?? []).map((o) => { const desc = o.startsWith('-'); const v = evaluate(desc ? o.slice(1) : o, c.scope); return { v, desc }; });
|
|
@@ -770,16 +777,17 @@ export async function sessionNotice(board, e) {
|
|
|
770
777
|
return true;
|
|
771
778
|
}
|
|
772
779
|
e = { ...e, address: e.address ?? addressOf(prior) };
|
|
773
|
-
let error = null;
|
|
780
|
+
let error = null, receipt = null;
|
|
774
781
|
try {
|
|
775
782
|
if (!e.address) throw new Error('lifecycle notice awaits the owned launch session identity');
|
|
776
|
-
await send(boardAddress(board.label), board.label, e.address, e.text, `board-run-notice-${e.id}-${e.durableId}
|
|
783
|
+
receipt = await send(boardAddress(board.label), board.label, e.address, e.text, `board-run-notice-${e.id}-${e.durableId}`);
|
|
784
|
+
if (!receipt.delivered) error = receipt.reason ?? 'notice queued; native transcript confirmation pending'; }
|
|
777
785
|
catch (failure) { error = String(failure.message).slice(-500); }
|
|
778
786
|
tx(board, (db) => {
|
|
779
787
|
const session = json(db.prepare('SELECT metadata FROM task_runs WHERE id = ?').get(e.runId)?.metadata, {})?.supercode;
|
|
780
788
|
if (session) setRunSession(db, e.runId, { ...session,
|
|
781
789
|
...(error ? { lifecycle_notice_error: error } : { lifecycle_notice_error: null, ...(session.lifecycle_notice_pending === e.durableId ? { lifecycle_notice_pending: null } : {}) }) });
|
|
782
|
-
if (!error || session?.lifecycle_notice_error !== error) event(db, e.id, error ? 'session_notice_failed' : 'session_notice_delivered', { address: e.address, verb: e.verb, source_event: e.sourceEvent, notice: e.durableId, ...(error ? { error, retry: true } : {}) }, e.runId);
|
|
790
|
+
if (!error || session?.lifecycle_notice_error !== error) event(db, e.id, error ? (receipt?.queued ? 'session_notice_queued' : 'session_notice_failed') : 'session_notice_delivered', { address: e.address, verb: e.verb, source_event: e.sourceEvent, notice: e.durableId, receipt, ...(error ? { error, retry: true } : {}) }, e.runId);
|
|
783
791
|
});
|
|
784
792
|
return !error;
|
|
785
793
|
}
|
|
@@ -787,12 +795,21 @@ export async function sessionNotice(board, e) {
|
|
|
787
795
|
/** A session effect a committed move asked for: a message to the card's current actor, or closing a slot's pane. */
|
|
788
796
|
export async function sessionEffect(board, e) {
|
|
789
797
|
if (e.kind === 'send_to_actor') {
|
|
790
|
-
const
|
|
791
|
-
const session =
|
|
798
|
+
const row = view(board, db => e.runId ? db.prepare('SELECT metadata FROM task_runs WHERE id=?').get(e.runId) : null);
|
|
799
|
+
const session = json(row?.metadata, {})?.supercode;
|
|
792
800
|
const address = addressOf(session);
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
801
|
+
let receipt;
|
|
802
|
+
try {
|
|
803
|
+
if (!address) throw new Error('actor session identity unavailable');
|
|
804
|
+
receipt = await send(boardAddress(board.label), board.label, address, e.text, `board-actor-${e.id}-${e.durableId}`);
|
|
805
|
+
} catch (error) { receipt = { delivered: false, reason: error.message }; }
|
|
806
|
+
const kind = receipt.delivered ? 'actor_notice_delivered' : receipt.queued ? 'actor_notice_queued' : 'actor_notice_failed';
|
|
807
|
+
tx(board, db => {
|
|
808
|
+
const previous = db.prepare("SELECT kind,payload FROM task_events WHERE task_id=? AND kind IN ('actor_notice_delivered','actor_notice_queued','actor_notice_failed') ORDER BY id DESC LIMIT 1").get(e.id);
|
|
809
|
+
const payload = { notice: e.durableId, address, receipt };
|
|
810
|
+
if (previous?.kind !== kind || JSON.stringify(json(previous?.payload, {})?.receipt) !== JSON.stringify(receipt)) event(db, e.id, kind, payload, e.runId);
|
|
811
|
+
});
|
|
812
|
+
return receipt.delivered;
|
|
796
813
|
}
|
|
797
814
|
if (e.kind === 'close_session' || e.kind === 'run_ended') {
|
|
798
815
|
tx(board, (db) => requestSessionClose(db, e.id, e.kind === 'run_ended' ? { runId: e.runId } : { slot: e.slot, through: e.runId }));
|
|
@@ -1299,6 +1316,7 @@ async function send(from, label, to, body, key, { queue = false, ask = false } =
|
|
|
1299
1316
|
const r = await scAsync(['message', 'remote-deliver'], { input: JSON.stringify(request), timeout: 180_000 });
|
|
1300
1317
|
const answer = lastJson(r.stdout) ?? {};
|
|
1301
1318
|
if (answer.code !== 0) throw new Error((answer.text || r.stderr || `no answer (exit ${r.code})`).trim().slice(-300));
|
|
1319
|
+
return answer.receipt ?? { delivered: false, reason: 'mail door returned no delivery receipt' };
|
|
1302
1320
|
}
|
|
1303
1321
|
|
|
1304
1322
|
/**
|
package/board/engine.mjs
CHANGED
|
@@ -397,7 +397,7 @@ export async function runEffects(board, effects = [], { retryNotices = false } =
|
|
|
397
397
|
else if (e.kind === 'close_session' || e.kind === 'send_to_actor' || e.kind === 'run_ended') {
|
|
398
398
|
if (e.kind === 'run_ended' && e.outcome === 'reclaimed') stopWorker(board, e.runId);
|
|
399
399
|
const { sessionEffect } = await import('./dispatch.mjs');
|
|
400
|
-
await sessionEffect(board, e);
|
|
400
|
+
if (await sessionEffect(board, e) === false) continue;
|
|
401
401
|
}
|
|
402
402
|
if (e.durableId) tx(board, (db) => db.prepare('DELETE FROM supercode_effects WHERE id = ?').run(e.durableId));
|
|
403
403
|
}
|
package/board/wake.mjs
CHANGED
|
@@ -20,7 +20,7 @@ export function signalBoardWrite(root, actor) {
|
|
|
20
20
|
/** One coalescing queue shared by standalone serve and the hosted dispatcher. */
|
|
21
21
|
export function watchBoard({ root, binary, env, log = () => {} }) {
|
|
22
22
|
const watchers = new Map();
|
|
23
|
-
let pending = null, waiter = null, closed = false, child = null, retry = null, lastError = null;
|
|
23
|
+
let pending = null, waiter = null, closed = false, child = null, retry = null, lastError = null, catalogue = null;
|
|
24
24
|
const wake = (source, at = Date.now()) => {
|
|
25
25
|
if (closed) return;
|
|
26
26
|
if (!pending) pending = { at, sources: new Set() };
|
|
@@ -83,7 +83,7 @@ export function watchBoard({ root, binary, env, log = () => {} }) {
|
|
|
83
83
|
if (closed) return;
|
|
84
84
|
let buffer = '', error = '';
|
|
85
85
|
const command = env.SUPERCODE_TEAMS_ENTRY ? process.execPath : binary;
|
|
86
|
-
const args = [...(env.SUPERCODE_TEAMS_ENTRY ? [env.SUPERCODE_TEAMS_ENTRY] : []), 'sessions', 'list', '--watch', '--json'];
|
|
86
|
+
const args = [...(env.SUPERCODE_TEAMS_ENTRY ? [env.SUPERCODE_TEAMS_ENTRY] : []), 'sessions', 'list', '--watch', '--json', '--limit', '200'];
|
|
87
87
|
child = spawn(command, args, { cwd: tmpdir(), env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
88
88
|
child.stdout.setEncoding('utf8').on('data', chunk => {
|
|
89
89
|
buffer += chunk;
|
|
@@ -94,7 +94,24 @@ export function watchBoard({ root, binary, env, log = () => {} }) {
|
|
|
94
94
|
try {
|
|
95
95
|
const event = JSON.parse(line);
|
|
96
96
|
lastError = null;
|
|
97
|
-
if (
|
|
97
|
+
if (['heartbeat', 'ping', 'keepalive'].includes(event.type)) continue;
|
|
98
|
+
if (event.type === 'catalog_reset' && Array.isArray(event.data?.items)) {
|
|
99
|
+
// Transcript revisions and sort order are not scheduling events.
|
|
100
|
+
// The safety sweep reads quiet timestamps; wake for session identity,
|
|
101
|
+
// availability and available delivery/control doors changing now.
|
|
102
|
+
const next = JSON.stringify(event.data.items.map(item => ({
|
|
103
|
+
id:item.id, harness:item.harness, machines:[...(item.machine_ids ?? [])].sort(),
|
|
104
|
+
bindings:(item.bindings ?? []).map(binding => ({ id:binding.id, machine:binding.machine_id,
|
|
105
|
+
incarnation:binding.incarnation_id, runtime:binding.native_runtime_id, session:binding.native_session_id,
|
|
106
|
+
harness:binding.harness, state:binding.state, capabilities:[...(binding.native_capabilities ?? [])].sort(),
|
|
107
|
+
})).sort((a,b)=>String(a.id).localeCompare(String(b.id))), actions:item.actions,
|
|
108
|
+
presence:item.activity?.presence, turn:item.activity?.turn,
|
|
109
|
+
status:item.live_status, delivery:item.delivery,
|
|
110
|
+
})).sort((a,b)=>String(a.id).localeCompare(String(b.id))));
|
|
111
|
+
if (next === catalogue) continue;
|
|
112
|
+
catalogue = next;
|
|
113
|
+
}
|
|
114
|
+
wake('session-catalogue');
|
|
98
115
|
} catch { /* CLI diagnostics are not events */ }
|
|
99
116
|
}
|
|
100
117
|
});
|
package/board/workflow.mjs
CHANGED
|
@@ -359,7 +359,15 @@ export function fire(db, board, entry, id, name, { roles = [], payload = {}, ext
|
|
|
359
359
|
const allowed = candidates.filter((t) => !t.by?.length || t.by.some((r) => roles.includes(r)));
|
|
360
360
|
if (!allowed.length) throw new Error(`${id} is ${from}; ${name.replace(/_/g, '-')} there is for ${[...new Set(candidates.flatMap((t) => t.by ?? []))].join(' or ')}. ${candidates.map((t) => t.description).filter(Boolean).join(' ')}`);
|
|
361
361
|
const taken = allowed.find((t) => holds(t.guard ?? null, scope));
|
|
362
|
-
if (!taken) {
|
|
362
|
+
if (!taken) {
|
|
363
|
+
const state = scope.card;
|
|
364
|
+
const reasons = [state.paused && `paused: ${state.paused.reason ?? 'manager pause'}`,
|
|
365
|
+
state.blocked_by.length && `blocked by ${state.blocked_by.map(b => b.id).join(', ')}`,
|
|
366
|
+
state.dispatch_hold && 'dispatch held', state.current_run_id && `current run ${state.current_run_id}`,
|
|
367
|
+
state.archived && 'archived'].filter(Boolean);
|
|
368
|
+
const none = new Error(`${id} is ${from}; ${name.replace(/_/g, '-')} cannot proceed: ${reasons.join('; ') || `guard ${allowed.map(t => t.guard).join(' | ')}`}`);
|
|
369
|
+
none.noTransition = true; throw none;
|
|
370
|
+
}
|
|
363
371
|
for (const v of taken.validate ?? []) {
|
|
364
372
|
if (!holds(v.expr, scope)) { const refusal = new Error(v.message); refusal.refusal = v.message; throw refusal; }
|
|
365
373
|
}
|
|
@@ -763,7 +771,11 @@ const ACTIONS = {
|
|
|
763
771
|
record(ctx.db, ctx.id, 'board_message', { to: String(to), text: `${String(text ?? '')}\n\nAnswer with reply_to ${id}.`, message_id: id }, null);
|
|
764
772
|
ctx.effects.push({ kind: 'board_message', id: ctx.id });
|
|
765
773
|
},
|
|
766
|
-
send_to_actor(ctx, text) {
|
|
774
|
+
send_to_actor(ctx, text) {
|
|
775
|
+
const effect = { kind: 'send_to_actor', id: ctx.id, runId: ctx.runId, text };
|
|
776
|
+
const inserted = run(ctx.db, 'INSERT INTO supercode_effects (effect) VALUES (?)', JSON.stringify(effect));
|
|
777
|
+
ctx.effects.push({ ...effect, durableId: Number(inserted.lastInsertRowid) });
|
|
778
|
+
},
|
|
767
779
|
};
|
|
768
780
|
|
|
769
781
|
/** Binding a newer run transfers ownership; stale run references cannot veto its later stop. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.130",
|
|
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": {
|