@volter/supercode-orchestrator 0.5.17 → 0.5.18
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 +10 -0
- package/board/workflow.mjs +34 -2
- package/package.json +1 -1
package/board/dispatch.mjs
CHANGED
|
@@ -1797,9 +1797,17 @@ async function notify(board, fleet, report, entry) {
|
|
|
1797
1797
|
try {
|
|
1798
1798
|
// an event older than the workflow's notice_max_age_seconds is history, not news: it is passed over, never mailed
|
|
1799
1799
|
const maxAge = Number(entry.params.notice_max_age_seconds) || null;
|
|
1800
|
+
// A backlog (a restart, a recipient that could not be reached) is told as it stands now: of one kind of news about
|
|
1801
|
+
// the card, only the latest (a later adoption_failed, blocked or moved says what the earlier one did); comments
|
|
1802
|
+
// and answers, each its own message, in full. A notice the board already recorded as taken by this address is never sent again.
|
|
1803
|
+
const latest = new Map(events.filter((ev) => !['commented', 'answered'].includes(ev.kind)).map((ev) => [ev.kind, ev.id]));
|
|
1804
|
+
const sentBefore = board.db.prepare("SELECT 1 FROM task_events WHERE task_id = ? AND kind IN ('notice_delivered', 'notice_filed') AND json_extract(payload, '$.source_event') = ? AND json_extract(payload, '$.address') = ? LIMIT 1");
|
|
1805
|
+
const advance = (id) => tx(board, (db) => db.prepare("UPDATE kanban_notify_subs SET last_event_id = ? WHERE task_id = ? AND platform = 'supercode' AND chat_id = ? AND thread_id = ? AND last_event_id < ?").run(id, sub.task_id, sub.chat_id, sub.thread_id ?? '', id));
|
|
1800
1806
|
for (const ev of events) {
|
|
1801
1807
|
const p = json(ev.payload, {}) ?? {};
|
|
1802
1808
|
if (maxAge && now() - Number(ev.created_at) > maxAge) { cursor = ev.id; (report.stale_notices_passed ??= 0); report.stale_notices_passed += 1; continue; }
|
|
1809
|
+
if (latest.has(ev.kind) && latest.get(ev.kind) !== ev.id) { cursor = ev.id; (report.superseded_notices_passed ??= 0); report.superseded_notices_passed += 1; continue; }
|
|
1810
|
+
if (sentBefore.get(sub.task_id, ev.id, sub.chat_id)) { cursor = ev.id; continue; }
|
|
1803
1811
|
// nobody is told of their own act
|
|
1804
1812
|
if (p.by && p.by === sub.chat_id) { cursor = ev.id; continue; }
|
|
1805
1813
|
let text;
|
|
@@ -1816,6 +1824,8 @@ async function notify(board, fleet, report, entry) {
|
|
|
1816
1824
|
if (!receipt.delivered && !receipt.queued) throw new Error(receipt.reason ?? 'notice was not filed');
|
|
1817
1825
|
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 }));
|
|
1818
1826
|
report.notified.push([sub.task_id, ev.kind, sub.chat_id]);
|
|
1827
|
+
// the cursor follows each notice sent, so a restart mid-backlog resumes after it
|
|
1828
|
+
advance(ev.id);
|
|
1819
1829
|
}
|
|
1820
1830
|
cursor = ev.id;
|
|
1821
1831
|
}
|
package/board/workflow.mjs
CHANGED
|
@@ -396,6 +396,39 @@ function queueTransitionNotice(db, board, entry, task, from, name, payload, befo
|
|
|
396
396
|
run(db, 'UPDATE task_runs SET metadata = ? WHERE id = ?', JSON.stringify(current), beforeRun.id);
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
+
/**
|
|
400
|
+
* A card's status, read through its workflow. A card carried from a board whose workflow named other statuses (an
|
|
401
|
+
* imported `ready`) is in none this workflow declares, and no transition leaves it: every verb refused it, the
|
|
402
|
+
* manager's included. It enters the way a new card does (the workflow's `initial`), recorded on the card, and the verb
|
|
403
|
+
* then applies from there. No status is named here: the workflow's own start rule decides.
|
|
404
|
+
*/
|
|
405
|
+
function declaredStatus(db, board, entry, task) {
|
|
406
|
+
const from = statusOf(db, task);
|
|
407
|
+
if (entry.workflow.statuses[from]) return from;
|
|
408
|
+
// A name the workflow stores a status as (kanban.db's `archived` for cancelled, Hermes's `blocked`, a `ready` its
|
|
409
|
+
// stored_as rules write) is that status, never a new card: a stopped card entering at the start would be dispatched
|
|
410
|
+
// again. Which status is read through the workflow's own rules: each declared status is tried on this card, in the
|
|
411
|
+
// caller's transaction, and the ones it would be stored as `from` are the candidates. One is the card's status;
|
|
412
|
+
// none or several say nothing certain, and the verb is refused.
|
|
413
|
+
const stores = Object.values(entry.workflow.statuses).some((status) => status.stored_as === from) || (entry.workflow.stored_as ?? []).some((rule) => rule.status === from);
|
|
414
|
+
if (stores) {
|
|
415
|
+
const stage = (name) => run(db, 'INSERT INTO supercode_cards (task_id, stage) VALUES (?, ?) ON CONFLICT(task_id) DO UPDATE SET stage = excluded.stage', task.id, name);
|
|
416
|
+
const fits = Object.keys(entry.workflow.statuses).filter((name) => { stage(name); return storedStatus(db, board, entry, task.id).stored === from; });
|
|
417
|
+
stage(from);
|
|
418
|
+
if (fits.length !== 1) throw new Error(`${task.id} is ${from}, which the board's workflow does not declare; it stores ${fits.length ? fits.join(' and ') : 'no status of this card'} as ${from}, so which it means is not certain: move it with that status's verb`);
|
|
419
|
+
setStatus(db, entry.workflow, task.id, fits[0]);
|
|
420
|
+
record(db, task.id, 'status_adopted', { from, to: fits[0], reason: `${from} is how this board's workflow stores ${fits[0]} for this card` });
|
|
421
|
+
return fits[0];
|
|
422
|
+
}
|
|
423
|
+
const scope = scopeOf(db, board, task, { params: entry.params, event: {}, extra: { lanes: lanesIn(entry) } });
|
|
424
|
+
const start = entry.workflow.initial?.find((x) => holds(x.guard ?? null, scope));
|
|
425
|
+
const to = start?.target ? String(templateValue(start.target, scope)) : null;
|
|
426
|
+
if (!to || !entry.workflow.statuses[to]) throw new Error(`${task.id} is ${from}, which the board's workflow does not declare, and its start rule names no declared status to enter`);
|
|
427
|
+
setStatus(db, entry.workflow, task.id, to);
|
|
428
|
+
record(db, task.id, 'status_adopted', { from, to, reason: `${from} is no status of this board's workflow; entered as a new card enters` });
|
|
429
|
+
return to;
|
|
430
|
+
}
|
|
431
|
+
|
|
399
432
|
/**
|
|
400
433
|
* Apply event `name` to card `id` in the caller's transaction. `roles` are who sends it (a transition with `by`
|
|
401
434
|
* takes only a sender in one of its roles; `platform` for the dispatcher's own observations). Answers
|
|
@@ -407,8 +440,7 @@ export function fire(db, board, entry, id, name, { roles = [], payload = {}, ext
|
|
|
407
440
|
if (workflow.run && (workflow.run.on?.[name] || Object.values(workflow.run.states ?? {}).some((s) => s.on?.[name]))) return fireRun(db, board, entry, id, name, { roles, payload, extra });
|
|
408
441
|
const task = cardOf(db, id);
|
|
409
442
|
const beforeRun = noticeRun(db, task);
|
|
410
|
-
const from =
|
|
411
|
-
if (!workflow.statuses[from]) throw new Error(`${id} is ${from}, which the board's workflow does not declare`);
|
|
443
|
+
const from = declaredStatus(db, board, entry, task);
|
|
412
444
|
const candidates = transitionsFor(workflow, from, name);
|
|
413
445
|
if (!candidates.length) throw new Error(`${id} is ${from}; ${name.replace(/_/g, '-')} does not apply there`);
|
|
414
446
|
// `sender`: the roles the event was sent with (a validator can ask who is asking)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-orchestrator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.18",
|
|
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": {
|