@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.
@@ -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
  }
@@ -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 = statusOf(db, task);
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.17",
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": {