@volter/supercode-orchestrator 0.5.17 → 0.5.19

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.
@@ -1543,10 +1543,31 @@ export function completeAdoptions(board, fleet, settings, report, lanes = null)
1543
1543
  }
1544
1544
  }
1545
1545
 
1546
+ /**
1547
+ * A session on this machine that Teams never published (one outside every synced root: a profile's own directory) is
1548
+ * found through this machine's own doors: its panes, and the native session each pane's process runs. Only a full
1549
+ * address is looked up here; liveness is still verified below as for any other session.
1550
+ */
1551
+ function localListing(who) {
1552
+ const m = /^sc:([^:]+):([^:]+):(.+)$/.exec(String(who));
1553
+ if (!m || normalMachine(m[1]) !== localMachine()) return null;
1554
+ const [, , harness, sessionId] = m;
1555
+ const listed = runtimeDoor(null, 'harness.v1.panes.list', {}, { timeout: 60_000 });
1556
+ const panes = (listed.code === 0 ? json(listed.stdout.trim(), []) : []) ?? [];
1557
+ const live = panes.filter((p) => !p.dead && p.pid);
1558
+ if (!live.length) return null;
1559
+ const r = runtimeDoor(null, 'harness.v1.sessions.activity_under', { pids: live.map((p) => p.pid) }, { timeout: 30_000 });
1560
+ const row = (r.code === 0 ? json(r.stdout.trim(), {})?.activities ?? [] : []).find((a) => a.activity?.session_id === sessionId && (a.activity?.harness ?? harness) === harness);
1561
+ // Not found in a pane of this machine: not listed, so a session opened moments ago is looked for again next tick.
1562
+ const pane = row ? live.find((p) => p.pid === row.pid)?.pane ?? null : null;
1563
+ if (!pane) return null;
1564
+ return { address: `sc:${localMachine()}:${harness}:${sessionId}`, machine: localMachine(), harness, session_id: sessionId, pane, name: null };
1565
+ }
1566
+
1546
1567
  function adoptSession(board, id, who, { workspace = null, ttl, assignee = null, role = null, fleet = null, lanes = null } = {}) {
1547
1568
  fleet ??= observeFleet();
1548
1569
  if (!fleet.ok) throw new Error(`supercode could not read the fleet: ${fleet.error}`);
1549
- const found = fleet.sessions.find((s) => s.address === who || s.address.endsWith(`:${who}`) || s.name === who);
1570
+ const found = fleet.sessions.find((s) => s.address === who || s.address.endsWith(`:${who}`) || s.name === who) ?? localListing(who);
1550
1571
  if (!found) throw Object.assign(new Error(`the fleet lists no live session ${who}`), { notListed: true });
1551
1572
  const [, host, harness, sessionId] = found.address.split(':');
1552
1573
  if (!HARNESSES[harness]) throw new Error(`${found.address} is ${harness}; the board keeps and resumes ${Object.keys(HARNESSES).join(' and ')} sessions`);
@@ -1797,9 +1818,17 @@ async function notify(board, fleet, report, entry) {
1797
1818
  try {
1798
1819
  // an event older than the workflow's notice_max_age_seconds is history, not news: it is passed over, never mailed
1799
1820
  const maxAge = Number(entry.params.notice_max_age_seconds) || null;
1821
+ // A backlog (a restart, a recipient that could not be reached) is told as it stands now: of one kind of news about
1822
+ // the card, only the latest (a later adoption_failed, blocked or moved says what the earlier one did); comments
1823
+ // and answers, each its own message, in full. A notice the board already recorded as taken by this address is never sent again.
1824
+ const latest = new Map(events.filter((ev) => !['commented', 'answered'].includes(ev.kind)).map((ev) => [ev.kind, ev.id]));
1825
+ 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");
1826
+ 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
1827
  for (const ev of events) {
1801
1828
  const p = json(ev.payload, {}) ?? {};
1802
1829
  if (maxAge && now() - Number(ev.created_at) > maxAge) { cursor = ev.id; (report.stale_notices_passed ??= 0); report.stale_notices_passed += 1; continue; }
1830
+ 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; }
1831
+ if (sentBefore.get(sub.task_id, ev.id, sub.chat_id)) { cursor = ev.id; continue; }
1803
1832
  // nobody is told of their own act
1804
1833
  if (p.by && p.by === sub.chat_id) { cursor = ev.id; continue; }
1805
1834
  let text;
@@ -1816,6 +1845,8 @@ async function notify(board, fleet, report, entry) {
1816
1845
  if (!receipt.delivered && !receipt.queued) throw new Error(receipt.reason ?? 'notice was not filed');
1817
1846
  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
1847
  report.notified.push([sub.task_id, ev.kind, sub.chat_id]);
1848
+ // the cursor follows each notice sent, so a restart mid-backlog resumes after it
1849
+ advance(ev.id);
1819
1850
  }
1820
1851
  cursor = ev.id;
1821
1852
  }
@@ -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.19",
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": {