@volter/supercode-orchestrator 0.5.73 → 0.5.74

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/ztrack.mjs CHANGED
@@ -132,7 +132,9 @@ class ZtrackBoard {
132
132
  exec(sql) { return self.active ? self.noting(() => self.conn.exec(sql)) : self.tx(() => self.conn.exec(sql)); },
133
133
  close() { self.close(); },
134
134
  };
135
- if (seed) this.tx(() => {});
135
+ // A board's first open in this process recovers what its store accepted and derives what git owes for it (tx), so
136
+ // a board that is only read, or whose owner runs no round, still finishes an earlier process's commit.
137
+ if (seed || this.git?.underived) this.tx(() => {});
136
138
  }
137
139
  // The board's documents: the open one, the current archive, and each rotated archive (recorded in the board's own
138
140
  // state, so a rotation committed but not yet written is still a document of the board).
@@ -400,7 +402,7 @@ class ZtrackBoard {
400
402
  this.conn.exec('DELETE FROM temp.board_dirty; DELETE FROM temp.board_touched');
401
403
  this.refused = new Set();
402
404
  timedStep('board tx errors', () => this.loadErrors());
403
- timedStep('board tx recover', () => this.recover());
405
+ const recovered = timedStep('board tx recover', () => this.recover());
404
406
  if (this.seed) this.indexForSeed();
405
407
  else {
406
408
  const stale = timedStep('board tx stale check', () => this.staleDocuments({ pending: false }));
@@ -422,7 +424,7 @@ class ZtrackBoard {
422
424
  const held = performance.now() - txStarted;
423
425
  noteStep('board transaction', held);
424
426
  recordSlow({ kind: 'call', name: 'board transaction', ms: held, slowest: null, extra: { board: this.path } });
425
- outcome = { result, files, intent };
427
+ outcome = { result, files, intent, recovered };
426
428
  } catch (error) {
427
429
  try { this.conn.exec('ROLLBACK'); } catch { /* not begun */ }
428
430
  this.active = false; this.seen = null; this.loadErrors();
@@ -430,6 +432,13 @@ class ZtrackBoard {
430
432
  throw error;
431
433
  } finally { this.active = false; }
432
434
  this.seed = null;
435
+ // What an earlier process left owed in git is derived once this board's recovery has written what its store
436
+ // accepted, with every document the board has; a recovery in any later write is committed the same way.
437
+ if (this.git && (this.git.underived || outcome.recovered)) {
438
+ this.git.paths = this.docs.map((doc) => doc.path);
439
+ try { timedStep('board git derive', () => outcome.recovered && !this.git.underived ? this.git.commitDirty('board: documents another write accepted') : this.git.derive()); }
440
+ catch (error) { current().err(`board: ${error.message}\n`); }
441
+ }
433
442
  if (outcome.intent) {
434
443
  this.publish(outcome);
435
444
  // Committed as the session (or person) whose act this write was, and pushed.
@@ -471,7 +480,7 @@ class ZtrackBoard {
471
480
  // document it replaced; merge it into a document edited since (the hand edit wins its cards).
472
481
  recover() {
473
482
  const pending = this.get('pending');
474
- if (!pending) return;
483
+ if (!pending) return false;
475
484
  for (const file of pending.files) {
476
485
  const current = this.fileHash(file.path);
477
486
  if (current.hash === file.after) continue;
@@ -483,6 +492,7 @@ class ZtrackBoard {
483
492
  if (merged) atomic(file.path, merged.text);
484
493
  }
485
494
  this.drop('pending'); this.drop('pending_hashes');
495
+ return true;
486
496
  }
487
497
 
488
498
  // ------------------------------------------------------------------ the document index
package/hermes-cli.mjs CHANGED
@@ -4,8 +4,8 @@
4
4
  // pinned `hermes` on its `PATH`; and what Hermes's scheduler records about a failed run (its incidents,
5
5
  // in `cron/executions.db`) the orchestrator records through Hermes's own function.
6
6
  import { spawn, spawnSync } from 'node:child_process';
7
- import { existsSync, readFileSync } from 'node:fs';
8
- import { dirname, join } from 'node:path';
7
+ import { accessSync, constants, existsSync, readFileSync, statSync } from 'node:fs';
8
+ import { delimiter, dirname, join } from 'node:path';
9
9
  import { parseDotenv } from './orchestration.mjs';
10
10
  import { locateHermes } from './apply/doors.mjs';
11
11
 
@@ -28,7 +28,8 @@ let located;
28
28
  export function hermesInstall() {
29
29
  if (located !== undefined) return located;
30
30
  try {
31
- const which = spawnSync('sh', ['-c', 'command -v hermes'], { encoding: 'utf8' }).stdout.trim();
31
+ // `hermes` on PATH, found by the file system (no shell started for it)
32
+ const which = String(process.env.PATH ?? '').split(delimiter).filter(Boolean).map((dir) => join(dir, 'hermes')).find((path) => { try { accessSync(path, constants.X_OK); return statSync(path).isFile(); } catch { return false; } }) ?? '';
32
33
  located = which ? { bin: dirname(which), ...locateHermes() } : null;
33
34
  } catch {
34
35
  located = null;
@@ -350,15 +351,6 @@ export async function memorySnapshot(profileDir) {
350
351
  return runHermes(profileDir, { op: 'snapshot' });
351
352
  }
352
353
 
353
- /** memorySnapshot for a synchronous caller (the board's launch). An unreadable store contributes nothing. */
354
- export function memorySnapshotSync(profileDir) {
355
- const install = hermesInstall();
356
- if (!install?.python) return '';
357
- const run = spawnSync(install.python, ['-c', HERMES_TOOL], { input: JSON.stringify({ op: 'snapshot' }), encoding: 'utf8', timeout: 30_000, cwd: install.source, env: hermesEnv(install, profileDir) });
358
- try { const out = hermesAnswer('snapshot', { stdout: run.stdout ?? '', stderr: run.stderr || run.error?.message || '' }); return typeof out === 'string' ? out : ''; }
359
- catch { return ''; }
360
- }
361
-
362
354
  /**
363
355
  * A schedule string as Hermes parses it (`{ kind: once|interval|cron, … }`), or null when no Hermes is
364
356
  * installed; throws Hermes's own refusal. Synchronous: schedules are parsed where a job is built.
package/loop.mjs CHANGED
@@ -180,16 +180,16 @@ export class Orchestrator {
180
180
  tick(at = this.clock()) {
181
181
  this.queue = this.queue.then(() => this.adoptOutsideJobs()).catch((error) => this.log('warn', `cron/jobs.json not read back: ${error.message}`));
182
182
  return this.dispatch(events.tick(at, cronInactivitySeconds(), cronMaxParallel())).then(
183
- (result) => { this.tickerHeartbeat(true); this.declareLocalAgents();return result; },
183
+ (result) => { this.tickerHeartbeat(true); void this.declareLocalAgents().catch((error) => this.log('warn', `agent hand-off: ${error.message}`));return result; },
184
184
  (error) => { this.tickerHeartbeat(false); throw error; },
185
185
  );
186
186
  }
187
187
 
188
188
  /** The home's agent declarations and ends a launch could not hand to this machine's daemon (none was running) are
189
189
  * handed on. The daemon declares and ends them in Teams and binds their sessions; the gateway makes no Teams call. */
190
- declareLocalAgents(){
190
+ async declareLocalAgents(){
191
191
  if(!existsSync(join(this.state.root,'agent-state.sqlite')))return;
192
- try{for(const pending of handOffAgents(this.state.root))this.log('warn',`agent ${pending.agent}: ${pending.error}`);}
192
+ try{for(const pending of await handOffAgents(this.state.root))this.log('warn',`agent ${pending.agent}: ${pending.error}`);}
193
193
  catch(error){this.log('warn',`agent hand-off: ${error.message}`);}
194
194
  }
195
195
 
@@ -416,8 +416,8 @@ export class Orchestrator {
416
416
  if(!record)throw new Error('launch as an agent requires its retained profile intent');
417
417
  agent=record;
418
418
  }
419
- agent??=localProfileAgent({root:this.state.root,org:scope.org,server:scope.server,profiles:this.state.profiles,profile:profileName,agent:agentName,declarations:this.state.agents});
420
- if(!scope.offline)for(const pending of handOffAgents(this.state.root,{context}))this.log('warn',`agent ${pending.agent}: ${pending.error}`);
419
+ agent??=await localProfileAgent({root:this.state.root,org:scope.org,server:scope.server,profiles:this.state.profiles,profile:profileName,agent:agentName,declarations:this.state.agents});
420
+ if(!scope.offline)for(const pending of await handOffAgents(this.state.root,{context}))this.log('warn',`agent ${pending.agent}: ${pending.error}`);
421
421
  agent.intended.resolved.worker={...agent.intended.resolved.worker,...worker,cwd};
422
422
  main=new_fork||!agent.identity.main_session;
423
423
  if(resume_runtime_id&&!new_fork){
@@ -507,8 +507,8 @@ export class Orchestrator {
507
507
  if(this.runtimes.has(leaseId(profileName,keyString)))throw new Error('fork requires an unoccupied target conversation');
508
508
  if(!(await this.descriptorFor(locator.harness))?.runtime?.capabilities?.resume_session)throw new Error('this harness cannot resume a materialized fork');
509
509
  const context=process.env.SUPERCODE_CONTEXT,scope=agentScope(context),session=`${locator.harness}:${locator.session_id}`;
510
- const child=localForkAgent({root:this.state.root,org:scope.org,server:scope.server,profiles:this.state.profiles,parent,session,name,key});
511
- if(!scope.offline)for(const pending of handOffAgents(this.state.root,{context}))this.log('warn',`agent ${pending.agent}: ${pending.error}`);
510
+ const child=await localForkAgent({root:this.state.root,org:scope.org,server:scope.server,profiles:this.state.profiles,parent,session,name,key});
511
+ if(!scope.offline)for(const pending of await handOffAgents(this.state.root,{context}))this.log('warn',`agent ${pending.agent}: ${pending.error}`);
512
512
  const runKey=`fork:${key}`,prior=agentRunForKey(this.state.root,scope.org,child.identity.id,runKey);
513
513
  let target;
514
514
  if(prior) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.73",
3
+ "version": "0.5.74",
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": {
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@volter/supercode-harness-sdk": "^0.3.60",
28
- "@volter/teams": "0.2.24",
28
+ "@volter/teams": "0.2.25",
29
29
  "@volter/ztrack": "^2.4.23",
30
30
  "yaml": "^2.9.0"
31
31
  },
@@ -1,38 +0,0 @@
1
- #!/usr/bin/env node
2
- // One fleet reading for a synchronous board process: each named machine's panes and the harness session under each live
3
- // pane, every machine read at once through its own door, each bounded by its own deadline. Prints one row per machine:
4
- // { machine, panes, activities } when its door answered, { machine, error, code } when it did not.
5
- import { openMachine } from '@volter/teams/machine-door';
6
- import { StreamRpc, harnessTransport } from '@volter/teams/client';
7
- import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
8
- let input = ''; for await (const chunk of process.stdin) input += chunk;
9
- const { machines, deadlineMs } = JSON.parse(input);
10
- const within = (promise, ms) => Promise.race([promise, new Promise((_, reject) => setTimeout(() => reject(Object.assign(new Error(`no answer within ${ms} ms`), { code: 'timeout' })), ms))]);
11
- async function read({ name, at }) {
12
- let channel;
13
- try {
14
- channel = await openMachine({ machine: at });
15
- const node = new StreamRpc(await channel.open('node'));
16
- const panes = await node.call('harness.v1.panes.list', {});
17
- // What a board launch needs of this machine: its board process guard (describe), and its keyed launch door, asked
18
- // for a key no launch has (an answer of null is the door; an unknown method is its absence)
19
- const description = await node.call('harness.v1.machine.describe', {}).catch(() => null);
20
- const keyed = await node.call('harness.v1.launches.get', { key: 'board-capability-reading' }).then(() => null, (error) => String(error?.message ?? error).slice(0, 200));
21
- const capabilities = { guard: description?.boardProcessPolicy?.version === 1 ? description.boardProcessPolicy.harnesses ?? [] : [], launches: keyed === null, launchesRefused: keyed };
22
- const live = (Array.isArray(panes) ? panes : []).filter((pane) => !pane.dead && pane.pid);
23
- // the session under each live pane: the harness stream, an operator's; a caller the machine admits only to its panes
24
- // (a box another custodian enrolled, granting view and control) reads its panes and no sessions
25
- let activities = [], sessionsUnread = null;
26
- if (live.length) {
27
- const client = new SupercodeHarnessClient({ transport: harnessTransport(channel) });
28
- try { await client.start?.(); activities = (await client.request('harness.v1.sessions.activity_under', { pids: live.map((pane) => pane.pid) }))?.activities ?? []; }
29
- catch (error) { sessionsUnread = String(error?.message ?? error).slice(0, 200); }
30
- finally { await client.close?.().catch(() => {}); }
31
- }
32
- return { machine: name, panes, activities, capabilities, sessionsUnread };
33
- } finally { channel?.close(); }
34
- }
35
- const rows = await Promise.all(machines.map((machine) => within(read(machine), deadlineMs)
36
- .catch((error) => ({ machine: machine.name, error: String(error?.message ?? error).slice(0, 300), code: error?.code ?? 'unavailable' }))));
37
- process.stdout.write(`${JSON.stringify(rows)}\n`);
38
- process.exit(0);
@@ -1,23 +0,0 @@
1
- #!/usr/bin/env node
2
- // A synchronous board process delegates one request to the public daemon door.
3
- // This helper owns neither credentials nor machine state; the SDK opens the admitted channel.
4
- import { openMachine } from '@volter/teams/machine-door';
5
- import { StreamRpc, harnessTransport } from '@volter/teams/client';
6
- import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
7
- let input = ''; for await (const chunk of process.stdin) input += chunk;
8
- let channel;
9
- try {
10
- const { machine, method, params = {} } = JSON.parse(input);
11
- if (!method.startsWith('harness.v1.')) throw new Error('the runtime door accepts harness.v1 methods');
12
- channel = await openMachine({machine});
13
- let result;
14
- if (method.startsWith('harness.v1.sessions.')) {
15
- // A session's records are the harness's own service, spoken by its SDK over the machine's harness stream.
16
- const client = new SupercodeHarnessClient({ transport: harnessTransport(channel) });
17
- try { await client.start?.(); result = await client.request(method, params); } finally { await client.close?.(); }
18
- } else result = await new StreamRpc(await channel.open('node')).call(method,params);
19
- process.stdout.write(JSON.stringify(result)+'\n');
20
- } catch (error) {
21
- process.stderr.write(JSON.stringify({code:error.code??'unavailable',message:error.message})+'\n');
22
- process.exitCode = 1;
23
- } finally { channel?.close(); }