@volter/supercode-orchestrator 0.4.105 → 0.4.106

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.
@@ -299,7 +299,7 @@ function stopSession(board, session) {
299
299
  }
300
300
  if (session.pane) {
301
301
  const r = sc(['teams', 'panes', 'close', session.pane, ...(session.machine ? ['--machine', session.machine] : [])], { timeout: 60_000, as: boardAddress(board.label) });
302
- if (r.code !== 0 || lastJson(r.stdout)?.closed !== session.pane) throw new Error(`closing pane ${session.pane}: ${(r.stderr || r.stdout || `exit ${r.code}`).trim().slice(-500)}`);
302
+ if (r.code !== 0 || json(r.stdout.trim(), null)?.closed !== session.pane) throw new Error(`closing pane ${session.pane}: ${(r.stderr || r.stdout || `exit ${r.code}`).trim().slice(-500)}`);
303
303
  }
304
304
  }
305
305
 
@@ -484,10 +484,10 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
484
484
  const session = json(row.metadata, {})?.supercode;
485
485
  if (!session?.pane || session.close_confirmed_at || (!row.ended_at && !session.close_requested)) continue;
486
486
  if (caller && session.session_id === caller) continue; // let the command that ended its own run return
487
- const active = view(board, (db) => db.prepare(`SELECT r.id, r.metadata FROM tasks t JOIN task_runs r ON r.id = t.current_run_id`).all());
487
+ const active = view(board, (db) => 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());
488
488
  if (active.some((r) => {
489
489
  const s = json(r.metadata, {})?.supercode;
490
- return r.id !== row.id && s?.pane === session.pane && normalMachine(s.machine) === normalMachine(session.machine);
490
+ return s?.pane === session.pane && normalMachine(s.machine) === normalMachine(session.machine);
491
491
  })) continue;
492
492
  // Persist retries even for historical ends that did not have a close_session action.
493
493
  tx(board, (db) => requestSessionClose(db, row.task_id, { runId: row.id }));
@@ -501,7 +501,12 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
501
501
  }
502
502
  const observed = panes.get(machine);
503
503
  if (observed.error) throw new Error(observed.error);
504
- if (observed.live.has(session.pane)) { stopSession(board, session); observed.live.delete(session.pane); }
504
+ if (observed.live.has(session.pane)) {
505
+ const protectedNow = view(board, (db) => db.prepare(`SELECT 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())
506
+ .some((r) => { const s = json(r.metadata, {})?.supercode; return s?.pane === session.pane && normalMachine(s.machine) === normalMachine(session.machine); });
507
+ if (protectedNow) continue;
508
+ stopSession(board, session); observed.live.delete(session.pane);
509
+ }
505
510
  } catch (e) { error = String(e.message).slice(-500); }
506
511
  tx(board, (db) => {
507
512
  const current = json(db.prepare('SELECT metadata FROM task_runs WHERE id = ?').get(row.id)?.metadata, {})?.supercode;
package/board/engine.mjs CHANGED
@@ -12,7 +12,7 @@ import { existsSync, rmSync } from 'node:fs';
12
12
  import { join, resolve } from 'node:path';
13
13
  import { DatabaseSync } from 'node:sqlite';
14
14
  import { view, claimer, event, json, newTaskId, now, tx } from './store.mjs';
15
- import { refreshCardInputs, requestSessionClose, eventTo, fire, loadWorkflow, setStatus, settle, startNew, statusOf, runEvent, workflowFor } from './workflow.mjs';
15
+ import { addDependency, refreshCardInputs, requestSessionClose, eventTo, fire, loadWorkflow, setStatus, settle, startNew, statusOf, runEvent, workflowFor } from './workflow.mjs';
16
16
 
17
17
  export const STATUSES = ['triage', 'todo', 'scheduled', 'ready', 'running', 'blocked', 'review', 'done', 'archived'];
18
18
  export const WORKSPACE_KINDS = ['scratch', 'dir', 'worktree'];
@@ -191,9 +191,9 @@ export function createCard(board, spec) {
191
191
  spec.reasoning ?? null, spec.sessionId ?? null, spec.project ?? null, spec.goal ? 1 : 0, spec.goal?.maxTurns ?? null);
192
192
  const machine = spec.machine ?? arc?.machine ?? null;
193
193
  if (machine || arc) run(db, 'INSERT OR REPLACE INTO supercode_cards (task_id, machine, subtask_of) VALUES (?, ?, ?)', id, machine, arc?.id ?? null);
194
- for (const p of parents) run(db, 'INSERT OR IGNORE INTO task_links (parent_id, child_id) VALUES (?, ?)', p, id);
195
- // where it starts is the workflow's `initial` (Hermes: triage, blocked, todo behind open parents, else ready)
196
194
  const entry = workflowFor(board.root);
195
+ for (const p of parents) addDependency(db, entry, id, p);
196
+ // where it starts is the workflow's `initial` (Hermes: triage, blocked, todo behind open parents, else ready)
197
197
  // the create's own words: whether it asked to be a goal card (null: the workflow decides), triage, its arc
198
198
  const status = startNew(db, board, entry, getTask(db, id), { triage: Boolean(spec.triage), initial_status: initial,
199
199
  every: spec.every ?? null, goal: spec.goal === undefined || spec.goal === null ? null : spec.goal !== false, subtask_of: arc?.id ?? null });
@@ -521,6 +521,9 @@ const ACTIONS = {
521
521
  blocks = blocks.filter((b) => remove === 'messages' ? b.kind !== 'message' : remove !== '*' && b.id !== remove);
522
522
  }
523
523
  cardField(ctx, 'blocked_by', JSON.stringify(blocks));
524
+ // task_links is the Hermes projection of the canonical card relations, including within this transaction.
525
+ run(ctx.db, 'DELETE FROM task_links WHERE child_id = ?', ctx.id);
526
+ for (const block of blocks.filter((b) => b.kind === 'card')) run(ctx.db, 'INSERT OR IGNORE INTO task_links (parent_id, child_id) VALUES (?, ?)', block.id, ctx.id);
524
527
  },
525
528
  flag(ctx, { name, value, reason = null }) {
526
529
  if (!['paused', 'archived'].includes(name)) throw new Error(`unknown flag ${name}`);
@@ -712,6 +715,12 @@ const ACTIONS = {
712
715
  send_to_actor(ctx, text) { ctx.effects.push({ kind: 'send_to_actor', id: ctx.id, text }); },
713
716
  };
714
717
 
718
+ /** The create door uses the instance's canonical dependency representation. */
719
+ export function addDependency(db, entry, id, parent) {
720
+ if (entry.workflow.relations?.blocked_by) ACTIONS.relation({ db, id }, { add: parent, reason: 'declared dependency' });
721
+ else run(db, 'INSERT OR IGNORE INTO task_links (parent_id, child_id) VALUES (?, ?)', parent, id);
722
+ }
723
+
715
724
  /** Persist close intent in the same transaction as the transition; I/O can fail or never run. */
716
725
  export function requestSessionClose(db, id, { runId = null, slot = null, through = null } = {}) {
717
726
  for (const row of all(db, 'SELECT id, metadata FROM task_runs WHERE task_id = ?', id)) {
package/board/ztrack.mjs CHANGED
@@ -523,6 +523,12 @@ class ZtrackBoard {
523
523
  const compat = new DatabaseSync(this.compatPath);
524
524
  try {
525
525
  compat.exec('BEGIN'); this.init(compat, { fresh: true });
526
+ // Native events and Hermes events share an export database, not an id sequence.
527
+ // Bootstrap the old identity mapping once; new exports allocate independent Hermes ids.
528
+ if (!compat.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='supercode_export_events'").get()) {
529
+ compat.exec(`CREATE TABLE supercode_export_events (source_id INTEGER PRIMARY KEY, event_id INTEGER NOT NULL UNIQUE)`);
530
+ compat.exec(`INSERT INTO supercode_export_events SELECT id, id FROM task_events WHERE json_extract(payload, '$.__supercode_export') = 1`);
531
+ }
526
532
  // Every import is event-based. Existing unconsumed acts must never be erased by an export race.
527
533
  const cursor = this.get('hermes_cursor', 0);
528
534
  const unseen = compat.prepare('SELECT payload FROM task_events WHERE id > ?').all(cursor).some((r) => !json(r.payload, {})?.__supercode_export);
@@ -535,14 +541,49 @@ class ZtrackBoard {
535
541
  for (const {name,key,row:source} of sources.values()) {
536
542
  if (!compat.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(name)) continue;
537
543
  const table = this.rows.tables.get(name);
544
+ if (name === 'task_events') {
545
+ const sourceId = JSON.parse(key)[0];
546
+ const mapped = compat.prepare('SELECT event_id FROM supercode_export_events WHERE source_id = ?').get(sourceId);
547
+ if (!source) {
548
+ if (mapped) compat.prepare('DELETE FROM task_events WHERE id = ?').run(mapped.event_id);
549
+ compat.prepare('DELETE FROM supercode_export_events WHERE source_id = ?').run(sourceId);
550
+ continue;
551
+ }
552
+ const row = { ...source, payload: JSON.stringify({ ...json(source.payload, {}), __supercode_export: true }) };
553
+ delete row.id;
554
+ const keys = Object.keys(row);
555
+ if (mapped) {
556
+ const previous = compat.prepare('SELECT * FROM task_events WHERE id = ?').get(mapped.event_id);
557
+ if (previous && keys.every((k) => previous[k] === row[k])) continue;
558
+ compat.prepare(`UPDATE task_events SET ${keys.map((k) => `${quote(k)} = ?`).join(',')} WHERE id = ?`).run(...keys.map((k) => row[k]), mapped.event_id);
559
+ } else {
560
+ const inserted = compat.prepare(`INSERT INTO task_events (${keys.map(quote)}) VALUES (${keys.map(() => '?')})`).run(...keys.map((k) => row[k]));
561
+ compat.prepare('INSERT INTO supercode_export_events (source_id, event_id) VALUES (?, ?)').run(sourceId, inserted.lastInsertRowid);
562
+ }
563
+ continue;
564
+ }
538
565
  if (!source) { compat.prepare(`DELETE FROM ${quote(name)} WHERE ${table.keys.map(k=>`${quote(k)} IS ?`).join(' AND ')}`).run(...JSON.parse(key)); continue; }
539
566
  const row = {...source};
540
- if (name === 'task_events') row.payload = JSON.stringify({...json(row.payload,{}),__supercode_export:true});
541
567
  const keys=Object.keys(row);
542
568
  const previous=compat.prepare(`SELECT * FROM ${quote(name)} WHERE ${table.keys.map(k=>`${quote(k)} IS ?`).join(' AND ')}`).get(...JSON.parse(key));
543
569
  if (previous && keys.every(k=>previous[k]===row[k]))continue;
544
570
  compat.prepare(`INSERT OR REPLACE INTO ${quote(name)} (${keys.map(quote)}) VALUES (${keys.map(()=>'?')})`).run(...keys.map(k=>row[k]));
545
571
  }
572
+ // Hermes recomputes ready on read. Its sticky block is the last blocked/unblocked event,
573
+ // so lowering the status alone cannot preserve a pause (including one made by a document edit).
574
+ for (const id of projected) {
575
+ const state = compat.prepare('SELECT t.status, c.stage, c.paused, c.blocked_by FROM tasks t JOIN supercode_cards c ON c.task_id = t.id WHERE t.id = ?').get(id);
576
+ if (!state) continue;
577
+ const sticky = state.status === 'blocked' && (json(state.paused, null) || json(state.blocked_by, []).some((b) => b.kind === 'message'));
578
+ const last = compat.prepare("SELECT kind, payload FROM task_events WHERE task_id = ? AND kind IN ('blocked','unblocked') ORDER BY id DESC LIMIT 1").get(id);
579
+ if (sticky) compat.prepare("UPDATE tasks SET block_kind = 'needs_input' WHERE id = ?").run(id);
580
+ const cleared = !sticky && json(last?.payload, {})?.__supercode_sticky;
581
+ if ((sticky && last?.kind !== 'blocked') || (cleared && last?.kind === 'blocked')) {
582
+ const payload = { __supercode_export: true, __supercode_sticky: true, block_kind: 'needs_input', source_status: state.stage,
583
+ reason: json(state.paused, null)?.reason ?? (sticky ? 'waiting on a message' : 'lowered hold cleared') };
584
+ compat.prepare('INSERT INTO task_events (task_id, kind, payload, created_at) VALUES (?, ?, ?, ?)').run(id, sticky ? 'blocked' : 'unblocked', JSON.stringify(payload), now());
585
+ }
586
+ }
546
587
  compat.exec('COMMIT');
547
588
  // Exported events carry an origin marker and never advance the Hermes input cursor.
548
589
  } catch (error) { try { compat.exec('ROLLBACK'); } catch {} throw error; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.4.105",
3
+ "version": "0.4.106",
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": {