@volter/supercode-orchestrator 0.4.127 → 0.4.129

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/cli.mjs CHANGED
@@ -302,6 +302,7 @@ export async function runWorkflow(argv) {
302
302
  }), true);
303
303
  let answer = { id: created.id, existing: created.existing };
304
304
  if (args.adopt) {
305
+ withBoard(b => { if (!rolesFor(b.db, created.id, sid, workflowFor(root).params).includes('manager')) throw new Error('Only the manager adopts an existing session.'); });
305
306
  const adopted = withBoard(b => adoptSession(b, created.id, args.adopt, { ttl: settings.ttl }));
306
307
  answer = { ...answer, started: true, session: adopted.address, mode: adopted.mode };
307
308
  } else if (!args['no-start'] && !created.existing && !args['subtask-of']) {
@@ -827,6 +828,7 @@ export async function runWorkflow(argv) {
827
828
  }
828
829
  case 'adopt': {
829
830
  need(2, 'a card id and a session');
831
+ withBoard(b => { if (!rolesFor(b.db, id, sid, workflowFor(root).params).includes('manager')) throw new Error('Only the manager adopts an existing session.'); });
830
832
  const { settings } = await laneState(root);
831
833
  const got = withBoard((b) => adoptSession(b, id, rest[1], { workspace: args.workspace ?? null, ttl: settings.ttl, assignee: args.assignee ?? null, role: args.role ?? null }));
832
834
  say(args, `${id} is worked by ${got.address} (pane ${got.pane ?? '?'} in ${got.workspace ?? 'its card\'s directory'})`, { ok: true, ...got });
@@ -1212,6 +1212,12 @@ function resolveLocalWorkspace(board, task) {
1212
1212
  * its card. `who` is an address, a session id, or a `name@machine` the fleet lists.
1213
1213
  */
1214
1214
  export function adoptSession(board, id, who, { workspace = null, ttl, assignee = null, role = null } = {}) {
1215
+ // Hold before discovery or any other process I/O: a failed attachment remains
1216
+ // visible and cannot race a dispatcher into launching a replacement worker.
1217
+ tx(board, db => {
1218
+ if (!getTask(db, id)) throw new Error(`no card ${id}`);
1219
+ event(db, id, 'dispatch_held', { session: who, reason: 'adoption requested' });
1220
+ });
1215
1221
  const fleet = observeFleet();
1216
1222
  if (!fleet.ok) throw new Error(`supercode could not read the fleet: ${fleet.error}`);
1217
1223
  const found = fleet.sessions.find((s) => s.address === who || s.address.endsWith(`:${who}`) || s.name === who);
package/board/engine.mjs CHANGED
@@ -170,6 +170,7 @@ export function createCard(board, spec) {
170
170
  ws.path = declared.path;
171
171
  spec = { ...spec, branch: declared.branch ?? spec.branch };
172
172
  const initial = spec.initialStatus ?? null;
173
+ if (initial === 'running' && (spec.noStart || spec.adopt)) throw new Error('A held or awaiting-adoption card cannot be created initially running.');
173
174
  if (initial && !['blocked', 'running'].includes(initial)) throw new Error('initial status is blocked or running');
174
175
  if (spec.provider && !spec.model) throw new Error('a provider needs a model');
175
176
  return tx(board, (db) => {
package/board/ztrack.mjs CHANGED
@@ -19,6 +19,14 @@ const CARD_MAIN = new Set(['id', 'title', 'body', 'status', 'assignee', 'priorit
19
19
  const EPHEMERAL = new Set(['tasks', 'supercode_cards', 'task_links', 'sqlite_sequence']);
20
20
  const atomic = (path, text) => { const tmp = `${path}.${process.pid}.tmp`; writeFileSync(tmp, text, { mode: 0o600 }); renameSync(tmp, path); };
21
21
 
22
+ // Only SQLite's actual contention codes retry. Rule refusals can contain words
23
+ // such as "blocked" or "busy" and must reach the caller unchanged.
24
+ const sqliteContention = error => [5, 6].includes(Number(error?.errcode) & 255)
25
+ || ['SQLITE_BUSY', 'SQLITE_LOCKED'].includes(error?.code)
26
+ || /^database (?:table |schema )?is locked$/i.test(error?.message ?? '');
27
+ const exportConflict = error => error?.message === 'Hermes wrote during board export; retained its events for the next read';
28
+ const recoveryConflict = error => error?.message === 'document changed during recovery; retry';
29
+
22
30
  export function openZtrackBoard(marker, { create = false, document = null, seed = null, init, root, slug, compatPath }) {
23
31
  if (!existsSync(marker)) {
24
32
  if (!create) throw new Error(`no ztrack board ${marker}`);
@@ -60,7 +68,7 @@ class ZtrackBoard {
60
68
  const fileStamp = (path) => { try { const s = statSync(path); return `${s.ino}:${s.size}:${s.mtimeMs}`; } catch { return 'missing'; } };
61
69
  return [fileStamp(this.path), fileStamp(this.archive), fileStamp(this.compatPath), this.get('version', '')].join('|');
62
70
  }
63
- readOnly(fn) { for (;;) { try { return fn(); } catch(error) { if(!/busy|locked/i.test(error.message))throw error; } } }
71
+ readOnly(fn) { for (;;) { try { return fn(); } catch(error) { if(!sqliteContention(error))throw error; } } }
64
72
  access(fn, reading = false) {
65
73
  if (reading && !this.reading) { this.reading=true; try {return this.readOnly(()=>this.access(fn,true));}finally{this.reading=false;} }
66
74
  if (this.active || (reading && this.mem && this.loadedStamp === this.stamp())) return fn();
@@ -112,7 +120,7 @@ class ZtrackBoard {
112
120
  this.readOnly(()=>{ if(this.runtime.prepare('PRAGMA journal_mode').get().journal_mode!=='wal')this.runtime.exec('PRAGMA journal_mode=WAL'); });
113
121
  for (;;) {
114
122
  try { this.runtime.exec('BEGIN'); }
115
- catch (error) { if (!/busy|locked/i.test(error.message)) throw error; continue; }
123
+ catch (error) { if (!sqliteContention(error)) throw error; continue; }
116
124
  this.active = true;
117
125
  let committed = false;
118
126
  try {
@@ -173,14 +181,14 @@ class ZtrackBoard {
173
181
  this.put('export_pending', [...this.get('export_pending', []), ...changes]);
174
182
  this.put('version', randomBytes(8).toString('hex'));
175
183
  this.runtime.exec('COMMIT');
176
- try { this.flushExport(); } catch (error) { if (!/busy|locked|Hermes wrote/i.test(error.message)) throw error; }
184
+ try { this.flushExport(); } catch (error) { if (!sqliteContention(error) && !exportConflict(error)) throw error; }
177
185
  this.loadedStamp = this.stamp();
178
186
  this.seed = null;
179
187
  return result;
180
188
  } catch (error) {
181
189
  try { this.runtime.exec('ROLLBACK'); } catch { /* intent already durable */ }
182
190
  this.invalidate();
183
- if (/busy|locked|Hermes wrote|document changed during recovery/i.test(error.message)) continue;
191
+ if (sqliteContention(error) || exportConflict(error) || recoveryConflict(error)) continue;
184
192
  if (committed) error.message += ' (the operation reached its durable intent; inspect its recorded outcome before retrying)';
185
193
  throw error;
186
194
  } finally { this.active = false; this.computing = false; this.pendingValues = null; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.4.127",
3
+ "version": "0.4.129",
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": {