@volter/supercode-orchestrator 0.5.34 → 0.5.36
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 +8 -8
- package/board/dispatch.mjs +113 -11
- package/package.json +1 -1
package/board/cli.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { actorOf, view, backingPaths, layOut, backingOf, boardExists, boardPath,
|
|
|
15
15
|
import { isLocalWorkspace } from './workspace.mjs';
|
|
16
16
|
import { openZtrackBoard } from './ztrack.mjs';
|
|
17
17
|
import * as engine from './engine.mjs';
|
|
18
|
-
import { requestAdoption, alertManagers, boardAddress, boardLabel, boardSettings, callerSession, commandEnv, reconcileCallerLaunch, closeRequested, lanesOf, localMachine, observeFleet, paused, plan, startCard, supercodeBin, tick } from './dispatch.mjs';
|
|
18
|
+
import { requestAdoption, alertManagers, boardAddress, boardLabel, boardSettings, callerSession, commandEnv, reconcileCallerLaunch, closeRequested, lanesOf, localMachine, observeFleet, paused, plan, startCard, stopDispatching, supercodeBin, tick } from './dispatch.mjs';
|
|
19
19
|
import { watchBoard } from './wake.mjs';
|
|
20
20
|
import { streamBoardPublications } from './event-stream.mjs';
|
|
21
21
|
import { completionOwnerOf, isLatestReviewer, loadWorkflow, requestSessionClose, scopeOf, statusOf, useWorkflow, workflowEntry, workflowFor } from './workflow.mjs';
|
|
@@ -1358,7 +1358,7 @@ function closeSession(board, id) {
|
|
|
1358
1358
|
export function summary(r) {
|
|
1359
1359
|
const parts = [`board ${r.board}:`];
|
|
1360
1360
|
if (r.error) parts.push(`error ${r.error}`);
|
|
1361
|
-
for (const [k, label] of [['mailed', 'mailed updates'], ['started', 'started'], ['queued', 'waiting'], ['failed', 'failed'], ['lost', 'lost'], ['crashed', 'crashed'], ['timed_out', 'timed out'], ['reclaimed', 'reclaimed'], ['auto_blocked', 'gave up'], ['notified', 'notices'], ['closed', 'closed sessions'], ['undelivered', 'undelivered (retrying)']]) {
|
|
1361
|
+
for (const [k, label] of [['mailed', 'mailed updates'], ['started', 'started'], ['queued', 'waiting'], ['failed', 'failed'], ['lost', 'lost'], ['crashed', 'crashed'], ['timed_out', 'timed out'], ['reclaimed', 'reclaimed'], ['auto_blocked', 'gave up'], ['notified', 'notices'], ['closed', 'closed sessions'], ['undelivered', 'undelivered (retrying)'], ['subscriptions_ended', 'worker subscriptions to ended sessions ended']]) {
|
|
1362
1362
|
if (r[k]?.length) parts.push(`${label} ${r[k].length}`);
|
|
1363
1363
|
}
|
|
1364
1364
|
if (r.promoted) parts.push(`promoted ${r.promoted}`);
|
|
@@ -1379,9 +1379,9 @@ async function serve(root, args = {}) {
|
|
|
1379
1379
|
if (args.pidfile) writeFileSync(args.pidfile, `${process.pid}\n`);
|
|
1380
1380
|
let stopped = false;
|
|
1381
1381
|
let events;
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1382
|
+
// a signal ends the round in progress with nothing new started (dispatch.mjs stopDispatching) and no round after it
|
|
1383
|
+
const stop = () => { stopped = true; stopDispatching(); events?.close(); };
|
|
1384
|
+
for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) process.on(signal, stop);
|
|
1385
1385
|
// what the serve says is kept in the home too (logs/workflow-serve.log), so why it stopped outlives its pane
|
|
1386
1386
|
const kept = join(root, 'logs', 'workflow-serve.log');
|
|
1387
1387
|
const log = (text) => {
|
|
@@ -1488,7 +1488,7 @@ async function serve(root, args = {}) {
|
|
|
1488
1488
|
interval = settings.interval;
|
|
1489
1489
|
const reports = await tick({ root, lanes: state.lanes, settings, params, deploymentReceipt: true });
|
|
1490
1490
|
round += 1;
|
|
1491
|
-
await watchRounds(reports);
|
|
1491
|
+
if (!stopped) await watchRounds(reports);
|
|
1492
1492
|
for (const r of reports) {
|
|
1493
1493
|
nextMailAt = Math.min(nextMailAt, r.next_mail_at ?? Infinity);
|
|
1494
1494
|
const said = summary(r);
|
|
@@ -1509,7 +1509,7 @@ async function serve(root, args = {}) {
|
|
|
1509
1509
|
}
|
|
1510
1510
|
} catch (error) {
|
|
1511
1511
|
log(`tick failed: ${error.message}`);
|
|
1512
|
-
await watchRounds([{ board: '*', error: `tick failed: ${error.message}` }]);
|
|
1512
|
+
if (!stopped) await watchRounds([{ board: '*', error: `tick failed: ${error.message}` }]);
|
|
1513
1513
|
}
|
|
1514
1514
|
// each round is a line of its own in the slow log, with the steps it spent its time on
|
|
1515
1515
|
recordSlow({ kind: 'call', name: 'dispatch round', ms: Date.now() - began, slowest: null, extra: { steps: takeSteps() } });
|
|
@@ -1529,7 +1529,7 @@ async function serve(root, args = {}) {
|
|
|
1529
1529
|
}
|
|
1530
1530
|
} finally {
|
|
1531
1531
|
events?.close();
|
|
1532
|
-
|
|
1532
|
+
for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) process.off(signal, stop);
|
|
1533
1533
|
if (args.pidfile) { try { unlinkSync(args.pidfile); } catch { /* gone */ } }
|
|
1534
1534
|
}
|
|
1535
1535
|
log(stopped ? 'stopped (a signal)' : 'stopped');
|
package/board/dispatch.mjs
CHANGED
|
@@ -910,9 +910,15 @@ export function closeRequested(board, report, { taskId = null, caller = null, tt
|
|
|
910
910
|
const { close_error: _error, ...rest } = current;
|
|
911
911
|
setRunSession(db, row.id, { ...rest, close_error: null, closed_at: at, close_confirmed_at: at });
|
|
912
912
|
const address = addressOf(session);
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
913
|
+
// The closed session stops hearing every card it heard as their worker: its own and each subtask (a launch
|
|
914
|
+
// subscribes it to all of them), unless another unended run still works in it, read in this transaction.
|
|
915
|
+
// Removing only the card's own left every subtask notifying a closed session, each attempt a failed delivery,
|
|
916
|
+
// every round. A later run that resumes it by id subscribes it again at its launch (and each round while it is
|
|
917
|
+
// the card's current run).
|
|
918
|
+
const subscribed = db.prepare('SELECT r.id, r.metadata FROM task_runs r WHERE r.ended_at IS NULL OR EXISTS (SELECT 1 FROM tasks t WHERE t.current_run_id = r.id)').all()
|
|
919
|
+
.some((r) => addressOf(json(r.metadata, {})?.supercode) === address && r.id !== row.id);
|
|
920
|
+
const unsubscribed = subscribed ? 0 : db.prepare("DELETE FROM kanban_notify_subs WHERE platform = 'supercode' AND chat_id = ? AND (task_id = ? OR json_extract(delivery_metadata, '$.role') = 'worker')").run(address, row.task_id).changes;
|
|
921
|
+
event(db, row.task_id, 'session_closed', { session_id: session.session_id, pane: session.pane, machine: session.machine, unsubscribed }, row.id);
|
|
916
922
|
}
|
|
917
923
|
});
|
|
918
924
|
if (error) report.undelivered.push([row.task_id, addressOf(session), error, 'close']);
|
|
@@ -1966,7 +1972,12 @@ async function send(board, from, label, to, body, key, { queue = false, ask = fa
|
|
|
1966
1972
|
// killed after it answered `code: 0` delivered)
|
|
1967
1973
|
const r = await scAsync(['message', 'remote-deliver'], { input: JSON.stringify(request), timeout: 180_000 });
|
|
1968
1974
|
const answer = lastJson(r.stdout) ?? {};
|
|
1969
|
-
if (answer.code !== 0)
|
|
1975
|
+
if (answer.code !== 0) {
|
|
1976
|
+
const error = new Error((answer.text || r.stderr || `no answer (exit ${r.code})`).trim().slice(-300));
|
|
1977
|
+
// the delivery door's own answer that the recipient runs nowhere (mail_route / mail_send), never a local failure
|
|
1978
|
+
if (typeof answer.code === 'number' && / is no longer running\. Nothing was sent\./.test(String(answer.text ?? ''))) error.notRunning = true;
|
|
1979
|
+
throw error;
|
|
1980
|
+
}
|
|
1970
1981
|
return answer.receipt ?? { delivered: false, reason: 'mail door returned no delivery receipt' };
|
|
1971
1982
|
}
|
|
1972
1983
|
|
|
@@ -1998,6 +2009,7 @@ export async function sendBoardMessages(board, report, taskId = null) {
|
|
|
1998
2009
|
const planned = tx(board, db => pendingRequestMail(db, taskId));
|
|
1999
2010
|
if (Number.isFinite(planned.nextAt)) report.next_mail_at = Math.min(report.next_mail_at ?? Infinity, planned.nextAt);
|
|
2000
2011
|
for (const mail of planned.ready) {
|
|
2012
|
+
if (stopping) break;
|
|
2001
2013
|
const first = mail.members[0];
|
|
2002
2014
|
try {
|
|
2003
2015
|
const task = view(board, db => getTask(db, first.task_id));
|
|
@@ -2064,8 +2076,73 @@ function ensureSubscriptions(board, entry) {
|
|
|
2064
2076
|
/** The event kinds an audience hears (the workflow's notify). */
|
|
2065
2077
|
const heardBy = (entry, audience) => Object.keys(entry.workflow.notify?.[audience] ?? {});
|
|
2066
2078
|
|
|
2079
|
+
/**
|
|
2080
|
+
* The board's record of each session address: `held`, every address an unended or current run works in, and `latest`,
|
|
2081
|
+
* the latest run that names it. A Claude or Codex session resumed by id keeps its address, so a run that resumes it
|
|
2082
|
+
* holds it again.
|
|
2083
|
+
*/
|
|
2084
|
+
function sessionRecord(db) {
|
|
2085
|
+
const held = new Set(), latest = new Map();
|
|
2086
|
+
for (const r of db.prepare('SELECT r.id, r.task_id, r.ended_at, r.metadata, t.id AS current FROM task_runs r LEFT JOIN tasks t ON t.current_run_id = r.id ORDER BY r.id').all()) {
|
|
2087
|
+
const session = json(r.metadata, {})?.supercode, address = addressOf(session);
|
|
2088
|
+
if (!address) continue;
|
|
2089
|
+
if (!r.ended_at || r.current) held.add(address);
|
|
2090
|
+
latest.set(address, { run: r.id, task_id: r.task_id, session });
|
|
2091
|
+
}
|
|
2092
|
+
return { held, latest };
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
/**
|
|
2096
|
+
* Whether the recipient's machine says, through its native discovery, that the session is not running (`persisted`:
|
|
2097
|
+
* its transcript is on disk and nothing runs it). Asked only after the delivery door answered that the session is no
|
|
2098
|
+
* longer running, once per machine and harness a round; null when the machine cannot answer.
|
|
2099
|
+
*/
|
|
2100
|
+
function nativeEnded(address, discovered) {
|
|
2101
|
+
const m = String(address).match(/^sc:([^:]+):([^:]+):(.+)$/);
|
|
2102
|
+
if (!m) return null;
|
|
2103
|
+
const [, machine, harness, session_id] = m, key = `${normalMachine(machine)}\u0000${harness}`;
|
|
2104
|
+
if (!discovered.has(key)) { try { discovered.set(key, nativeSessions(machine, harness)); } catch { discovered.set(key, null); } }
|
|
2105
|
+
const rows = discovered.get(key);
|
|
2106
|
+
if (!rows) return null;
|
|
2107
|
+
return nativeReading(rows, { session_id, harness }).alive === false ? { by: 'native_discovery', presence: 'persisted', machine } : null;
|
|
2108
|
+
}
|
|
2109
|
+
|
|
2110
|
+
/**
|
|
2111
|
+
* The worker subscriptions of sessions that have ended, removed in one transaction, with one notice_unsubscribed event
|
|
2112
|
+
* per session naming its cards. A session has ended when no unended or current run works in it, the fleet does not list
|
|
2113
|
+
* it running, its latest run is not a release (a released session is a person's own, left running), and either the
|
|
2114
|
+
* board closed it (that run's confirmed close) or its machine's native discovery read it `persisted` this round
|
|
2115
|
+
* (`natively`). Everything is read inside the transaction, so a run that took the session meanwhile keeps it. A run
|
|
2116
|
+
* that resumes the session subscribes it again at its launch and each round while it is the card's current run.
|
|
2117
|
+
* A creator's or manager's subscription is never ended here: its notices wait for the session to come back.
|
|
2118
|
+
*/
|
|
2119
|
+
function endWorkerSubscriptions(board, fleet, report, natively = new Map()) {
|
|
2120
|
+
tx(board, (db) => {
|
|
2121
|
+
const { held, latest } = sessionRecord(db);
|
|
2122
|
+
const remove = db.prepare("DELETE FROM kanban_notify_subs WHERE task_id = ? AND platform = 'supercode' AND chat_id = ? AND thread_id = ?");
|
|
2123
|
+
const ended = new Map();
|
|
2124
|
+
for (const row of db.prepare("SELECT task_id, chat_id, thread_id FROM kanban_notify_subs WHERE platform = 'supercode' AND json_extract(delivery_metadata, '$.role') = 'worker'").all()) {
|
|
2125
|
+
const address = row.chat_id, record = latest.get(address);
|
|
2126
|
+
if (!record || record.session.released || held.has(address) || (fleet.ok && fleet.alive.has(address))) continue;
|
|
2127
|
+
const evidence = record.session.close_confirmed_at ? { by: 'board_close', run: record.run, closed_at: record.session.close_confirmed_at } : natively.get(address);
|
|
2128
|
+
if (!evidence || !remove.run(row.task_id, address, row.thread_id).changes) continue;
|
|
2129
|
+
if (!ended.has(address)) ended.set(address, { record, evidence, cards: [] });
|
|
2130
|
+
ended.get(address).cards.push(row.task_id);
|
|
2131
|
+
}
|
|
2132
|
+
for (const [address, { record, evidence, cards }] of ended) {
|
|
2133
|
+
event(db, record.task_id, 'notice_unsubscribed', { address, role: 'worker', reason: 'the session has ended', evidence, cards, count: cards.length }, record.run);
|
|
2134
|
+
for (const card of cards) (report.subscriptions_ended ??= []).push([card, address]);
|
|
2135
|
+
}
|
|
2136
|
+
});
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2067
2139
|
async function notify(board, fleet, report, entry) {
|
|
2068
2140
|
const from = boardAddress(board.label);
|
|
2141
|
+
endWorkerSubscriptions(board, fleet, report);
|
|
2142
|
+
const { held, latest } = view(board, sessionRecord);
|
|
2143
|
+
// A recipient whose delivery failed this round is not asked again this round for its other cards: those are left for
|
|
2144
|
+
// the next round untried (a closed session subscribed to 21 subtasks was a delivery process for each, every round)
|
|
2145
|
+
const refused = new Map(), discovered = new Map(), natively = new Map();
|
|
2069
2146
|
// each subscription's notices go in order; different subscribers are told side by side
|
|
2070
2147
|
await pool(subscriptions(board.db).filter((sub) => sub.platform === 'supercode'), SEND_CONCURRENCY, async (sub) => {
|
|
2071
2148
|
if (sub.delivery_metadata?.dependent_card && sub.delivery_metadata.waiting_recipient) return;
|
|
@@ -2085,7 +2162,7 @@ async function notify(board, fleet, report, entry) {
|
|
|
2085
2162
|
// the one the next run gets): the notice is filed in its mailbox, read when it next works
|
|
2086
2163
|
const working = role !== 'worker' || view(board, (db) => addressOf(currentRun(db, sub.task_id)?.metadata?.supercode) === sub.chat_id);
|
|
2087
2164
|
let cursor = Number(sub.last_event_id);
|
|
2088
|
-
let failed = null;
|
|
2165
|
+
let failed = null, untried = false;
|
|
2089
2166
|
try {
|
|
2090
2167
|
// an event older than the workflow's notice_max_age_seconds is history, not news: it is passed over, never mailed
|
|
2091
2168
|
const maxAge = Number(entry.params.notice_max_age_seconds) || null;
|
|
@@ -2113,10 +2190,17 @@ async function notify(board, fleet, report, entry) {
|
|
|
2113
2190
|
} else text = noticeOf(entry, role, ev.kind, task, p);
|
|
2114
2191
|
if (text) {
|
|
2115
2192
|
const body = ['worker', 'dependency'].includes(role) ? identifyNotice(ev, text) : text;
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2193
|
+
if (refused.has(sub.chat_id) || stopping) { untried = true; break; }
|
|
2194
|
+
let receipt;
|
|
2195
|
+
try {
|
|
2196
|
+
receipt = await send(board, from, board.label, sub.chat_id, body, `board-${sub.task_id}-${ev.id}-${sub.chat_id.slice(-12)}`, { queue: String(sub.chat_id).includes(':claude-code:') ? false : !working || (!entry.workflow.run && sub.delivery_mode === 'notify'), subject: `${ev.kind.replaceAll('_', ' ')} · run ${ev.run_id ?? 'none'} · event ${ev.id} · ${task?.title || sub.task_id}` });
|
|
2197
|
+
// A notice the recipient's mailbox took is filed: the machine delivers it at the session's next turn, so the
|
|
2198
|
+
// board does not send it again.
|
|
2199
|
+
if (!receipt.delivered && !receipt.queued) throw new Error(receipt.reason ?? 'notice was not filed');
|
|
2200
|
+
} catch (error) {
|
|
2201
|
+
if (!refused.has(sub.chat_id)) refused.set(sub.chat_id, error);
|
|
2202
|
+
throw error;
|
|
2203
|
+
}
|
|
2120
2204
|
tx(board, (db) => event(db, sub.task_id, receipt.delivered ? 'notice_delivered' : 'notice_filed', { source_event: ev.id, kind: ev.kind, address: sub.chat_id, receipt }));
|
|
2121
2205
|
report.notified.push([sub.task_id, ev.kind, sub.chat_id]);
|
|
2122
2206
|
// the cursor follows each notice sent, so a restart mid-backlog resumes after it
|
|
@@ -2125,6 +2209,14 @@ async function notify(board, fleet, report, entry) {
|
|
|
2125
2209
|
cursor = ev.id;
|
|
2126
2210
|
}
|
|
2127
2211
|
} catch (error) {
|
|
2212
|
+
// One "no longer running" answer is not an ended session (a Claude session is resumed under the same id): for a
|
|
2213
|
+
// worker no run holds, and that is not a released session, the machine's native discovery is asked, and a
|
|
2214
|
+
// `persisted` reading ends its worker subscriptions after this round's deliveries.
|
|
2215
|
+
const record = latest.get(sub.chat_id);
|
|
2216
|
+
if (error.notRunning && role === 'worker' && record && !record.session.released && !held.has(sub.chat_id)) {
|
|
2217
|
+
if (!natively.has(sub.chat_id)) natively.set(sub.chat_id, nativeEnded(sub.chat_id, discovered));
|
|
2218
|
+
if (natively.get(sub.chat_id)) return;
|
|
2219
|
+
}
|
|
2128
2220
|
failed = String(error.message).slice(-200);
|
|
2129
2221
|
tx(board, (db) => {
|
|
2130
2222
|
const kind = 'notice_failed';
|
|
@@ -2140,12 +2232,16 @@ async function notify(board, fleet, report, entry) {
|
|
|
2140
2232
|
const where = "WHERE task_id = ? AND platform = 'supercode' AND chat_id = ? AND thread_id = ?";
|
|
2141
2233
|
const identity = [sub.task_id, sub.chat_id, sub.thread_id ?? ''];
|
|
2142
2234
|
db.prepare(`UPDATE kanban_notify_subs SET last_event_id = ? ${where} AND last_event_id < ?`).run(cursor, ...identity, cursor);
|
|
2235
|
+
// a round that left this subscription untried (a delivery to its recipient failed on another card) says nothing of it
|
|
2236
|
+
if (untried) return;
|
|
2143
2237
|
const failures = failed ? Number(sub.delivery_metadata?.failures ?? 0) + 1 : 0;
|
|
2144
2238
|
if (failures >= MAX_SEND_FAILURES) { db.prepare(`DELETE FROM kanban_notify_subs ${where}`).run(...identity); return; }
|
|
2145
2239
|
if (failures !== Number(sub.delivery_metadata?.failures ?? 0)) db.prepare(`UPDATE kanban_notify_subs SET delivery_metadata = ? ${where}`).run(JSON.stringify({ ...sub.delivery_metadata, failures }), ...identity);
|
|
2146
2240
|
if (task?.status === 'archived' && cursor >= events.at(-1).id) db.prepare(`DELETE FROM kanban_notify_subs ${where}`).run(...identity);
|
|
2147
2241
|
});
|
|
2148
2242
|
});
|
|
2243
|
+
const persisted = new Map([...natively].filter(([, reading]) => reading));
|
|
2244
|
+
if (persisted.size) endWorkerSubscriptions(board, fleet, report, persisted);
|
|
2149
2245
|
}
|
|
2150
2246
|
|
|
2151
2247
|
// Hermes's terminal kinds for a chat subscriber, in its order of silence: archived and unblocked move the cursor and say nothing
|
|
@@ -2359,6 +2455,11 @@ function scAsync(args, { input = null, timeout = 120_000 } = {}) {
|
|
|
2359
2455
|
});
|
|
2360
2456
|
}
|
|
2361
2457
|
|
|
2458
|
+
// A dispatcher told to stop (its serve's SIGTERM, SIGINT or SIGHUP) starts nothing more in the round it is in: no further
|
|
2459
|
+
// board, card start, board message or notice. What it leaves untried is not a failure; the next dispatcher sends it.
|
|
2460
|
+
let stopping = false;
|
|
2461
|
+
export function stopDispatching() { stopping = true; }
|
|
2462
|
+
|
|
2362
2463
|
// how many deliveries a tick has in flight at once (each is a `supercode message` process of a few seconds)
|
|
2363
2464
|
const SEND_CONCURRENCY = 8;
|
|
2364
2465
|
|
|
@@ -2366,7 +2467,7 @@ const SEND_CONCURRENCY = 8;
|
|
|
2366
2467
|
async function pool(items, n, fn) {
|
|
2367
2468
|
let next = 0;
|
|
2368
2469
|
await Promise.all(Array.from({ length: Math.min(n, items.length) }, async () => {
|
|
2369
|
-
while (next < items.length) { const i = next; next += 1; await fn(items[i]); }
|
|
2470
|
+
while (next < items.length && !stopping) { const i = next; next += 1; await fn(items[i]); }
|
|
2370
2471
|
}));
|
|
2371
2472
|
}
|
|
2372
2473
|
|
|
@@ -2503,6 +2604,7 @@ export async function tick({ root, lanes, settings, only = null, slugs = null, d
|
|
|
2503
2604
|
const held = dispatch ? homeHoldings(root, boards, lanes, fleet, entry) : [];
|
|
2504
2605
|
const out = [];
|
|
2505
2606
|
for (const slug of boards) {
|
|
2607
|
+
if (stopping) break;
|
|
2506
2608
|
const report = { board: slug, ms: { fleet: fleetMs }, mailed: [], started: [], queued: [], failed: [], lost: [], crashed: [], timed_out: [], reclaimed: [], auto_blocked: [], promoted: 0, unassigned: [], nonspawnable: [], notified: [], undelivered: [], fleet: fleet.ok ? 'read' : fleet.error };
|
|
2507
2609
|
// the mailbox's answers in flight (not part of the report the caller prints)
|
|
2508
2610
|
Object.defineProperty(report, 'answers', { value: [], enumerable: false });
|
|
@@ -2533,7 +2635,7 @@ export async function tick({ root, lanes, settings, only = null, slugs = null, d
|
|
|
2533
2635
|
tx(board,db=>auditEndedRuns(db,board,entry));
|
|
2534
2636
|
if (dispatch) await activatePullRequestReviews(board, entry, report, effects, { only });
|
|
2535
2637
|
await sendBoardMessages(board, report);
|
|
2536
|
-
if (dispatch) startCards(board, fleet, lanes, settings, report, entry, effects, { only, held });
|
|
2638
|
+
if (dispatch && !stopping) startCards(board, fleet, lanes, settings, report, entry, effects, { only, held });
|
|
2537
2639
|
phase('start');
|
|
2538
2640
|
if (dispatch && !only && !report.paused) observeIdle(board, fleet, settings, report, entry, effects);
|
|
2539
2641
|
// what the moves asked for, once they are committed (messages to actors, closed panes, cleaned workspaces)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.36",
|
|
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": {
|