@volter/supercode-orchestrator 0.4.110 → 0.4.112

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.
@@ -15,6 +15,7 @@
15
15
  // Every side effect follows a committed transition: a card is claimed before its session is launched, and a
16
16
  // subscriber's cursor moves only after its notice was delivered, so two dispatchers over one board never both
17
17
  // launch and a crash between the two steps is answered by the next tick.
18
+ import { createRequire } from 'node:module';
18
19
  import { spawn as spawnChild, spawnSync } from 'node:child_process';
19
20
  import { existsSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
20
21
  import { hostname, tmpdir } from 'node:os';
@@ -54,10 +55,22 @@ export function localMachine() {
54
55
  /** The supercode command the dispatcher drives. */
55
56
  export const supercodeBin = () => process.env.SUPERCODE_BIN || 'supercode';
56
57
 
58
+ // Installed board workers use their sibling Teams package, not a CLI build's retained checkout.
59
+ // An explicit operator override remains authoritative for source development.
60
+ let teamsEntry;
61
+ function commandEnv(as = null) {
62
+ if (teamsEntry === undefined) {
63
+ try { teamsEntry = createRequire(import.meta.url).resolve('@volter/supercode-teams/bin'); }
64
+ catch { teamsEntry = null; }
65
+ }
66
+ return { ...process.env, ...(!process.env.SUPERCODE_TEAMS_ENTRY && teamsEntry ? { SUPERCODE_TEAMS_ENTRY: teamsEntry } : {}),
67
+ ...(as ? { SUPERCODE_CALLER: as } : {}) };
68
+ }
69
+
57
70
  function sc(args, { input = null, timeout = 120_000, as = null } = {}) {
58
71
  // a neutral directory: `supercode teams …` resolves its entry from a checkout at the cwd. `as`: the board this
59
72
  // command acts for, so a card's pane is started and closed by its board, whichever session ran the board's verb
60
- const env = as ? { ...process.env, SUPERCODE_CALLER: as } : process.env;
73
+ const env = commandEnv(as);
61
74
  const r = spawnSync(supercodeBin(), args, { cwd: tmpdir(), input: input ?? undefined, encoding: 'utf8', timeout, env, maxBuffer: 512 * 1024 * 1024 });
62
75
  return { code: r.status, stdout: r.stdout ?? '', stderr: (r.stderr ?? '') + (r.error ? r.error.message : '') };
63
76
  }
@@ -166,6 +179,7 @@ export function boardSettings(params = {}) {
166
179
  ttl: claimTtl(params.claim_ttl_seconds),
167
180
  grace: pos(params.session_grace_seconds) ?? 120,
168
181
  interval: Math.max(Number(params.dispatch_interval_seconds) || 60, 1),
182
+ idleQuiet: pos(params.session_idle_quiet_seconds) ?? 600,
169
183
  logRotateBytes: pos(params.worker_log_rotate_bytes) ?? 2 * 1024 * 1024,
170
184
  logBackups: Number.isInteger(Number(params.worker_log_backup_count)) && Number(params.worker_log_backup_count) >= 0 ? Number(params.worker_log_backup_count) : 1,
171
185
  hooks: params.hooks && typeof params.hooks === 'object' ? params.hooks : {},
@@ -525,10 +539,11 @@ export function closeRequested(board, report, { taskId = null, caller = null } =
525
539
  for (const row of rows) {
526
540
  let session = json(board.db.prepare('SELECT metadata FROM task_runs WHERE id = ?').get(row.id)?.metadata, {})?.supercode;
527
541
  if (session?.launch_key && !session.pane && (row.ended_at || session.close_requested)) {
528
- const receipt = launchReceipt(board, session);
542
+ let receipt = null, receiptError = null;
543
+ try { receipt = launchReceipt(board, session); } catch (error) { receiptError = error.message; }
529
544
  if (receipt?.pane) { bindLaunch(board, row, session, receipt.pane, session.session_id); session = { ...session, pane: receipt.pane }; }
530
545
  else {
531
- const error = `close deferred: awaiting launch receipt ${session.launch_key}`;
546
+ const error = `close deferred: ${receiptError ?? `awaiting launch receipt ${session.launch_key}`}`;
532
547
  tx(board, (db) => {
533
548
  setRunSession(db, row.id, { close_error: error, close_requested: session.close_requested ?? now() });
534
549
  if (session.close_error !== error) event(db, row.task_id, 'session_close_failed', { error, retry: true }, row.id);
@@ -596,9 +611,9 @@ function observeIdle(board, fleet, settings, report, entry, effects) {
596
611
  // a supercode that reports no last message time gives no reading, and no reading gives no verdict
597
612
  const lastAt = Math.floor(Number(live?.last_message_at_ms) / 1000);
598
613
  if (!(lastAt > 0)) return;
599
- if (now() - lastAt < settings.interval) return;
600
- if (session.goal_nudged_at && now() - session.goal_nudged_at < settings.interval * 2) return;
601
- const worked = Boolean(session.goal_nudged_at) && lastAt >= session.goal_nudged_at + settings.interval;
614
+ if (now() - lastAt < settings.idleQuiet) return;
615
+ if (session.goal_nudged_at && now() - session.goal_nudged_at < settings.idleQuiet) return;
616
+ const worked = Boolean(session.goal_nudged_at) && lastAt > session.goal_nudged_at;
602
617
  try {
603
618
  const where = whereOf(board, task);
604
619
  // the ask names what the session may do, as its run's prompt did
@@ -732,9 +747,13 @@ const getTaskNow = (board, id) => view(board, (db) => getTask(db, id));
732
747
 
733
748
  /** A transport error is not a negative launch receipt. The key remains owned by its run. */
734
749
  function launchReceipt(board, session) {
735
- const r = sc(['teams', 'rpc', 'launches.get', JSON.stringify({ key: session.launch_key }), '--machine', session.machine],
750
+ const r = sc(['teams', 'launches', 'get', session.launch_key, '--machine', session.machine],
736
751
  { timeout: 30_000, as: boardAddress(board.label) });
737
- return r.code === 0 ? lastJson(r.stdout) : null;
752
+ if (r.code !== 0) throw new Error(`launch receipt unavailable on ${session.machine}: install the matching Teams CLI and machine capability (teams launches get): ${(r.stderr || r.stdout).trim().slice(-300)}`);
753
+ if (r.stdout.trim() === 'null') return null;
754
+ const receipt = lastJson(r.stdout);
755
+ if (!receipt || receipt.key !== session.launch_key) throw new Error(`invalid launch receipt for ${session.launch_key} on ${session.machine}`);
756
+ return receipt;
738
757
  }
739
758
 
740
759
  function bindLaunch(board, row, session, pane, sid) {
@@ -758,6 +777,8 @@ function bindLaunch(board, row, session, pane, sid) {
758
777
 
759
778
  /** A fast worker can reach its first board door before open's reply. Bind only the daemon's keyed pane. */
760
779
  export function reconcileCallerLaunch(board, sid) {
780
+ if (view(board, (db) => db.prepare(`SELECT 1 FROM task_runs r JOIN tasks t ON t.current_run_id=r.id
781
+ WHERE json_extract(r.metadata, '$.supercode.session_id')=? AND COALESCE(json_extract(r.metadata, '$.supercode.launch_pending'),0)=0`).get(sid))) return;
761
782
  const rows = view(board, (db) => db.prepare(`SELECT r.id, r.task_id, r.metadata FROM task_runs r
762
783
  JOIN tasks t ON t.current_run_id=r.id WHERE r.ended_at IS NULL
763
784
  AND json_extract(r.metadata, '$.supercode.launch_pending') = 1`).all());
@@ -846,10 +867,12 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
846
867
  if (prev?.close_requested && !prev.closed_at) throw new Error(`the slot's session ${prev.session_id} asked to be closed and its pane ${prev.pane} could not be: ${report.undelivered.map((u) => u[2]).join('; ') || 'no reason given'}`);
847
868
  }
848
869
  const openOwned = (key, sid, mode, args) => {
870
+ launchReceipt(board, { machine, launch_key: key }); // Prove the exact receipt door before starting a process.
849
871
  tx(board, (db) => setRunSession(db, runId, { ...meta, surface: 'pane', machine, harness, slot,
850
872
  session_id: sid, mode, launch_key: key, launch_pending: true, launched_at: now() }));
851
873
  const result = sc(args, { timeout: 180_000, as: boardAddress(board.label) });
852
- const facts = launchReceipt(board, { machine, launch_key: key });
874
+ let facts = null, receiptError = null;
875
+ try { facts = launchReceipt(board, { machine, launch_key: key }); } catch (error) { receiptError = error.message; }
853
876
  const pane = facts?.pane ?? lastJson(result.stdout)?.pane ?? null;
854
877
  // The daemon's keyed receipt is authoritative even when the stream carrying open's reply closed.
855
878
  if (pane) tx(board, (db) => {
@@ -863,7 +886,7 @@ function launchPane(board, task, runId, lane, fleet, { input = null, slot = 'wor
863
886
  launch_pending: false, launch_error: null, launched_at: now() };
864
887
  }
865
888
  const reason = facts?.live === false && pane ? `launch pane ${pane} ended` :
866
- `launch ${key} is awaiting its machine receipt or session identity: ${(result.stderr || result.stdout).trim().slice(-300)}`;
889
+ `launch ${key} is awaiting its machine receipt or session identity: ${receiptError ?? (result.stderr || result.stdout).trim().slice(-300)}`;
867
890
  tx(board, (db) => {
868
891
  setRunSession(db, runId, { launch_pending: true, launch_error: reason });
869
892
  event(db, task.id, 'session_launch_pending', { key, pane, reason }, runId);
@@ -1101,7 +1124,7 @@ export function adoptSession(board, id, who, { workspace = null, ttl, assignee =
1101
1124
 
1102
1125
  // a notice sent within a transaction's caller that cannot await (the launcher): the same door, synchronously
1103
1126
  function deliverNow(board, to, body, key) {
1104
- const request = { op: 'send', from: boardAddress(board.label), from_name: `${board.label} board`, to, queue: false, id: key,
1127
+ const request = { op: 'send', from: boardAddress(board.label), from_name: `${board.label} board`, to, queue: false, id: key, subject: String(body).split('\n')[0].slice(0, 160),
1105
1128
  body: `[Board notice from the ${board.label} board] ${body}\n\n(An automated notice from the board's dispatcher; replies to this address go nowhere.)` };
1106
1129
  const r = sc(['message', 'remote-deliver'], { input: JSON.stringify(request), timeout: 120_000 });
1107
1130
  const answer = lastJson(r.stdout);
@@ -1110,7 +1133,7 @@ function deliverNow(board, to, body, key) {
1110
1133
 
1111
1134
  async function send(from, label, to, body, key, { queue = false, ask = false } = {}) {
1112
1135
  const request = {
1113
- op: 'send', from, from_name: `${label} board`, to, queue, id: key,
1136
+ op: 'send', from, from_name: `${label} board`, to, queue, id: key, subject: String(body).split('\n')[0].slice(0, 160),
1114
1137
  // a question the board waits on is answered by a reply; a notice is not
1115
1138
  body: ask ? `[A question from the ${label} board] ${body}` : `[Board notice from the ${label} board] ${body}\n\n(An automated notice from the board's dispatcher; replies to this address go nowhere.)`,
1116
1139
  };
@@ -1423,7 +1446,7 @@ export function boardLabel(root, slug) {
1423
1446
  /** `sc` without blocking the tick: notices to many sessions go out side by side. */
1424
1447
  function scAsync(args, { input = null, timeout = 120_000 } = {}) {
1425
1448
  return new Promise((done) => {
1426
- const child = spawnChild(supercodeBin(), args, { cwd: tmpdir(), env: process.env, stdio: ['pipe', 'pipe', 'pipe'] });
1449
+ const child = spawnChild(supercodeBin(), args, { cwd: tmpdir(), env: commandEnv(), stdio: ['pipe', 'pipe', 'pipe'] });
1427
1450
  let stdout = '';
1428
1451
  let stderr = '';
1429
1452
  const timer = setTimeout(() => { stderr += ` timed out after ${timeout}ms`; child.kill('SIGKILL'); }, timeout);
@@ -344,7 +344,7 @@ export function fire(db, board, entry, id, name, { roles = [], payload = {}, ext
344
344
  // `sender`: the roles the event was sent with (a validator can ask who is asking)
345
345
  const scope = scopeOf(db, board, task, { params, event: payload, extra: { lanes: lanesIn(entry), ...extra, sender: roles } });
346
346
  const allowed = candidates.filter((t) => !t.by?.length || t.by.some((r) => roles.includes(r)));
347
- if (!allowed.length) throw new Error(`${id} is ${from}; ${name.replace(/_/g, '-')} there is for ${[...new Set(candidates.flatMap((t) => t.by ?? []))].join(' or ')}`);
347
+ if (!allowed.length) throw new Error(`${id} is ${from}; ${name.replace(/_/g, '-')} there is for ${[...new Set(candidates.flatMap((t) => t.by ?? []))].join(' or ')}. ${candidates.map((t) => t.description).filter(Boolean).join(' ')}`);
348
348
  const taken = allowed.find((t) => holds(t.guard ?? null, scope));
349
349
  if (!taken) { const none = new Error(`${id} is ${from}; ${name.replace(/_/g, '-')} does not apply to it now`); none.noTransition = true; throw none; }
350
350
  for (const v of taken.validate ?? []) {
@@ -557,7 +557,8 @@ const ACTIONS = {
557
557
  if (!['paused', 'archived'].includes(name)) throw new Error(`unknown flag ${name}`);
558
558
  if (name === 'paused') {
559
559
  const was = json(one(ctx.db, 'SELECT paused FROM supercode_cards WHERE task_id = ?', ctx.id)?.paused, null);
560
- if (!value && was && was.by !== actorOf() && !ctx.scope.sender?.includes('owner') && !(ctx.scope.sender?.includes('hermes') && ctx.scope.event?.hermes)) throw new Error('only the pauser or owner may resume this card');
560
+ const clearBy = ctx.entry.workflow.flags?.paused?.clear_by ?? ['pauser', 'owner'];
561
+ if (!value && was && !clearBy.some((role) => role === 'pauser' ? was.by === actorOf() : ctx.scope.sender?.includes(role))) throw new Error(`only ${clearBy.join(' or ')} may resume this card; ask the manager with your reason`);
561
562
  if (value && !String(reason ?? '').trim()) throw new Error('pause needs a reason');
562
563
  cardField(ctx, name, value ? JSON.stringify({ by: actorOf(), reason }) : null);
563
564
  } else cardField(ctx, name, Number(Boolean(value)));
package/board/ztrack.mjs CHANGED
@@ -136,6 +136,7 @@ class ZtrackBoard {
136
136
  if(source.name==='supercode_cards')return JSON.stringify(canonical(source.row,new Set(['since','after_fired','next_run_at'])))!==JSON.stringify(canonical(this.rows.cachedCards[JSON.parse(source.key)[0]],new Set(['since','after_fired','next_run_at'])));
137
137
  return false;
138
138
  });
139
+ this.publishedFingerprints = {};
139
140
  const documents = this.seed || documentChanges.length ? this.documents(this.seed ? null : documentChanges) : {open:this.text, archived:this.archiveText};
140
141
  const files = [{ path: this.path, before: hash(this.text), base: this.text, text: documents.open }, { path: this.archive, before: hash(this.archiveText), base: this.archiveText, text: documents.archived }];
141
142
  const snapshot = this.snapshot(documents);
@@ -330,18 +331,7 @@ class ZtrackBoard {
330
331
  if (this.originals.has(id)) throw new Error(`duplicate board card ${id}`);
331
332
  this.originals.set(id, card); this.fileIds.set(id, card.id);
332
333
  this.loadCard(id, card);
333
- for (const task of card.acceptanceCriteria) {
334
- const tid = task.metadata?.id ?? this.saved.taskIds?.[`${id}:${task.id}`] ?? `t_${randomBytes(4).toString('hex')}`;
335
- this.taskKeys.set(tid, `${id}:${task.id}`);
336
- const body = task.metadata?.body ?? '';
337
- const taskCard = { ...task.metadata?.card, id: tid, title: `${task.id} ${task.text}`, body,
338
- status: task.status === 'passed' ? (task.metadata?.status === 'cancelled' ? 'cancelled' : 'done') : (['done','cancelled'].includes(task.metadata?.status) ? 'todo' : task.metadata?.status ?? 'todo'),
339
- metadata: task.metadata?.properties ?? {}, assignee: task.metadata?.assignee ?? card.assignee,
340
- machine: task.metadata?.machine ?? card.machine, acceptanceCriteria: [],
341
- acceptance: task.lines ?? [], relations: (task.blockedBy ?? []).map((b) => ({ type: 'blocked-by', issueId: b.ac ? `${nativeId(b.issue)}:${b.ac}` : nativeId(b.issue) })) };
342
- if (task.waitingOn) taskCard.relations.push({ type: 'blocked-by', issueId: task.waitingOn });
343
- this.loadCard(tid, taskCard, id);
344
- }
334
+ for (const [tid, taskCard] of this.taskCards(id, card)) this.loadCard(tid, taskCard, id);
345
335
  }
346
336
  // Keep the last readable projection only for the affected malformed cards.
347
337
  // Their raw document spans remain untouched and their verbs refuse with this reason.
@@ -368,24 +358,48 @@ class ZtrackBoard {
368
358
  }
369
359
  if (observe) this.documentEvents();
370
360
  }
361
+ taskCards(id, card) {
362
+ return card.acceptanceCriteria.map((task) => {
363
+ const tid = task.metadata?.id ?? this.saved.taskIds?.[`${id}:${task.id}`] ?? `t_${randomBytes(4).toString('hex')}`;
364
+ this.taskKeys.set(tid, `${id}:${task.id}`);
365
+ const taskCard = { ...task.metadata?.card, id: tid, title: `${task.id} ${task.text}`, body: task.metadata?.body ?? '',
366
+ status: task.status === 'passed' ? (task.metadata?.status === 'cancelled' ? 'cancelled' : 'done') : (['done','cancelled'].includes(task.metadata?.status) ? 'todo' : task.metadata?.status ?? 'todo'),
367
+ metadata: task.metadata?.properties ?? {}, assignee: task.metadata?.assignee ?? card.assignee,
368
+ machine: task.metadata?.machine ?? card.machine, acceptanceCriteria: [], acceptance: task.lines ?? [],
369
+ relations: (task.blockedBy ?? []).map((b) => ({ type: 'blocked-by', issueId: b.ac ? `${nativeId(b.issue)}:${b.ac}` : nativeId(b.issue) })) };
370
+ if (task.waitingOn) taskCard.relations.push({ type: 'blocked-by', issueId: task.waitingOn });
371
+ return [tid, taskCard];
372
+ });
373
+ }
371
374
  loadCard(id, card, parent = null) {
375
+ const rows = this.cardRows(id, card, parent);
376
+ this.insert('tasks', rows.task); this.insert('supercode_cards', rows.state);
377
+ }
378
+ cardRows(id, card, parent = null) {
372
379
  const meta = card.metadata ?? {};
373
380
  const stored = this.preset.serializeKanbanCard({ ...card, acceptanceCriteria: [], relations: [], metadata: undefined, paused: undefined, every: undefined, workspace: undefined, branch: undefined, machine: undefined, priority: undefined });
374
381
  const workspace = /^(scratch|dir|worktree)(?::(.*))?$/.exec(card.workspace ?? 'scratch');
375
- this.insert('tasks', { ...meta, ...this.saved.runtime?.[id], id, title: card.title, body: stored.body,
382
+ const task = { ...meta, ...this.saved.runtime?.[id], id, title: card.title, body: stored.body,
376
383
  status: card.status, assignee: card.assignee ?? null, created_at: meta.created_at ?? now(), priority: card.priority ?? 0,
377
- workspace_kind: workspace?.[1] ?? 'scratch', workspace_path: workspace?.[1] === 'scratch' ? (this.saved.runtime?.[id]?.workspace_path ?? meta.workspace_path ?? null) : workspace?.[2] ?? null, branch_name: card.branch ?? null });
378
- this.insert('supercode_cards', { ...this.saved.clocks?.[id], task_id: id, stage: card.status, machine: card.machine ?? null, subtask_of: parent,
384
+ workspace_kind: workspace?.[1] ?? 'scratch', workspace_path: workspace?.[1] === 'scratch' ? (this.saved.runtime?.[id]?.workspace_path ?? meta.workspace_path ?? null) : workspace?.[2] ?? null, branch_name: card.branch ?? null };
385
+ const state = { ...this.saved.clocks?.[id], task_id: id, stage: card.status, machine: card.machine ?? null, subtask_of: parent,
379
386
  blocked_by: JSON.stringify((card.relations ?? []).map((r) => ({ ...(meta.blocked_by ?? []).find((b) => b.id === nativeId(r.issueId)), id: nativeId(r.issueId), kind: /^[ma]-/.test(r.issueId) ? 'message' : 'card' }))),
380
- paused: card.paused ? JSON.stringify(card.paused) : null, archived: meta.archived ? 1 : 0, every: card.every ?? null });
387
+ paused: card.paused ? JSON.stringify(card.paused) : null, archived: meta.archived ? 1 : 0, every: card.every ?? null };
388
+ return { task, state };
389
+ }
390
+ fingerprint(t, c) {
391
+ const status = c.stage ?? t.status;
392
+ return { hash: hash(JSON.stringify([t.title, t.body, status, t.assignee, c.machine, c.blocked_by, c.paused, c.every])), title: hash(t.title), body: hash(t.body ?? ''), status, assignee: t.assignee, machine: c.machine, parent: c.subtask_of };
381
393
  }
382
394
  fingerprints() {
383
395
  return Object.fromEntries(this.mem.prepare('SELECT * FROM tasks').all().map((t) => {
384
396
  const c = this.mem.prepare('SELECT * FROM supercode_cards WHERE task_id = ?').get(t.id) ?? {};
385
- return [t.id, { hash: hash(JSON.stringify([t.title, t.body, c.stage, t.assignee, c.machine, c.blocked_by, c.paused, c.every])), title: hash(t.title), body: hash(t.body ?? ''), status: c.stage, assignee: t.assignee, machine: c.machine, parent: c.subtask_of }];
397
+ return [t.id, this.fingerprint(t, c)];
386
398
  }));
387
399
  }
388
400
  documentEvents() {
401
+ // A published native document is already represented by its authored events.
402
+ if (this.saved.documentHashes?.open === hash(this.text) && this.saved.documentHashes?.archived === hash(this.archiveText)) return;
389
403
  const previous = this.saved.fingerprints ?? {};
390
404
  const current = this.fingerprints();
391
405
  if (!Object.keys(previous).length) return;
@@ -412,6 +426,7 @@ class ZtrackBoard {
412
426
  return 'document-editor';
413
427
  }
414
428
  documents(changes=null) {
429
+ this.publishedFingerprints = {};
415
430
  const allRows = this.mem.prepare('SELECT * FROM tasks ORDER BY created_at, id').all();
416
431
  const states = new Map(this.mem.prepare('SELECT * FROM supercode_cards').all().map((c) => [c.task_id, c]));
417
432
  const affected=changes?new Set(changes.map(({source})=>{const id=JSON.parse(source.key)[0];return states.get(id)?.subtask_of??this.rows.cachedCards[id]?.subtask_of??id;})):null;
@@ -456,6 +471,15 @@ class ZtrackBoard {
456
471
  for (const [id, card] of cards) {
457
472
  const serialized = this.preset.serializeKanbanCard(card);
458
473
  const record = { id: card.id, title: card.title, status: card.status, ...(card.assignee ? { assignee: card.assignee } : {}), body: serialized.body };
474
+ // Fingerprint the codec's persisted projection, not the pre-serialization input.
475
+ // The codec normalizes bodies (including their trailing newline) and subtask titles.
476
+ const persisted = this.preset.KanbanRootSchema.parse(this.preset.parseKanban([record])).issues[0];
477
+ const rows = this.cardRows(id, persisted);
478
+ this.publishedFingerprints[id] = this.fingerprint(rows.task, rows.state);
479
+ for (const [tid, taskCard] of this.taskCards(id, persisted)) {
480
+ const rows = this.cardRows(tid, taskCard, id);
481
+ this.publishedFingerprints[tid] = this.fingerprint(rows.task, rows.state);
482
+ }
459
483
  (states.get(id)?.archived || ['done', 'cancelled'].includes(card.status) ? archived : open).push(record);
460
484
  }
461
485
  const original=this.documentRecords;
@@ -463,7 +487,11 @@ class ZtrackBoard {
463
487
  return { open: !changedPath(this.path)?this.text:this.codec.renderBoardDocument(this.text, open, this.path, [...(this.preserve.get(this.path)??[])]), archived: !changedPath(this.archive)?this.archiveText:this.codec.renderBoardDocument(this.archiveText, archived, this.archive, [...(this.preserve.get(this.archive)??[])]) };
464
488
  }
465
489
  snapshot(documents) {
466
- return { documentHashes:{open:hash(documents.open),archived:hash(documents.archived)}, errors:[...this.errors], fingerprints: this.fingerprints(), taskIds: Object.fromEntries([...this.taskKeys].map(([id, key]) => [key, id])) };
490
+ const fingerprints = this.fingerprints();
491
+ if (this.saved.documentHashes?.open === hash(this.text) && this.saved.documentHashes?.archived === hash(this.archiveText)) {
492
+ for (const id of Object.keys(fingerprints)) if (this.saved.fingerprints?.[id]) fingerprints[id] = this.saved.fingerprints[id];
493
+ }
494
+ return { documentHashes:{open:hash(documents.open),archived:hash(documents.archived)}, errors:[...this.errors], fingerprints: { ...fingerprints, ...this.publishedFingerprints }, taskIds: Object.fromEntries([...this.taskKeys].map(([id, key]) => [key, id])) };
467
495
  }
468
496
 
469
497
  lift() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.4.110",
3
+ "version": "0.4.112",
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": {