@volter/supercode-orchestrator 0.5.27 → 0.5.29

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.
@@ -1304,8 +1304,21 @@ function reconcileLaunches(board, fleet, report, entry, effects, settings, lanes
1304
1304
  event(db,row.task_id,mismatch.differences.length?'agent_profile_drift':'agent_profile_recorded',{agent:session.agent_id,session:session.session_id,comparison:mismatch},row.id);
1305
1305
  });
1306
1306
  }
1307
+ const ref=`${session.harness}:${session.session_id}`;
1308
+ // An ended run whose session the agent store already holds, published, for another agent lost that claim when it
1309
+ // ran: no round can change an ended run or a published owner, so the disagreement is history. It is said once on
1310
+ // its card (agent_ownership_conflict) and the run is not recorded again. A live run is never history: it is
1311
+ // recorded below every round, and a conflict there is refused and said each round as before.
1312
+ if(row.ended_at){
1313
+ let owner=null;try{owner=agentRunForSession(board.root,[ref]);}catch{/* conflicting owners: recorded below, refused there */}
1314
+ if(owner?.published===1&&owner.org===session.agent_org&&owner.agent_id!==session.agent_id){
1315
+ const said=db=>db.prepare("SELECT 1 FROM task_events WHERE task_id=? AND run_id=? AND kind='agent_ownership_conflict' LIMIT 1").get(row.task_id,row.id);
1316
+ if(!view(board,said))tx(board,db=>{if(!said(db))event(db,row.task_id,'agent_ownership_conflict',{session:ref,run_agent:session.agent_id,owner:owner.agent_id,owner_run:owner.run_id},row.id);});
1317
+ continue;
1318
+ }
1319
+ }
1307
1320
  // One run whose session another agent already owns is that card's problem, said on it; the round goes on for the rest.
1308
- try{recordAgentRun(board.root,session.agent_org,session.agent_id,{session:`${session.harness}:${session.session_id}`,machine:session.machine,run:row.id});}
1321
+ try{recordAgentRun(board.root,session.agent_org,session.agent_id,{session:ref,machine:session.machine,run:row.id});}
1309
1322
  catch(error){report.queued.push([row.task_id,`run ${row.id}: ${error.message}`]);continue;}
1310
1323
  orgs.set(session.agent_org,session.agent_context);
1311
1324
  }
@@ -1719,6 +1732,22 @@ function localListing(who) {
1719
1732
  return { address: `sc:${localMachine()}:${harness}:${sessionId}`, machine: localMachine(), harness, session_id: sessionId, pane, name: null };
1720
1733
  }
1721
1734
 
1735
+ /** The verb an adoption with no current run fires: the workflow's own `adopt` where it declares one, else its dispatch start. */
1736
+ const adoptionVerb = (entry) => (entry.workflow.verbs?.adopt || !entry.workflow.run ? 'adopt' : entry.workflow.dispatch.start);
1737
+
1738
+ /**
1739
+ * The role an adopted session works on its card: a rebound run's own, else the role of the status the adoption's verb
1740
+ * moves the card to, as its claim names it. A card in review adopts its reviewer, never its implementer.
1741
+ */
1742
+ function adoptedRole(db, board, entry, task, run) {
1743
+ const kept = run?.metadata?.supercode?.role;
1744
+ if (kept) return kept;
1745
+ const verb = adoptionVerb(entry), by = verb === 'adopt' ? 'manager' : 'platform';
1746
+ const status = statusOf(db, task), scope = scopeOf(db, board, task, { params: entry.params });
1747
+ const t = transitionsFor(entry.workflow, status, verb).find((x) => !x.by?.length || x.by.includes(by));
1748
+ return runSpec(entry, t?.target ? String(templateValue(t.target, scope)) : status, task)?.role ?? null;
1749
+ }
1750
+
1722
1751
  function adoptSession(board, id, who, { workspace = null, ttl, assignee = null, role = null, fleet = null, lanes = null } = {}) {
1723
1752
  fleet ??= observeFleet();
1724
1753
  if (!fleet.ok) throw new Error(`supercode could not read the fleet: ${fleet.error}`);
@@ -1745,12 +1774,12 @@ function adoptSession(board, id, who, { workspace = null, ttl, assignee = null,
1745
1774
  // Rule 2: a session working a card role is that role's agent, however it started; adopted, it joins the agent as a
1746
1775
  // launch does, and its recorded profile is compared with the role's intent (rule 4).
1747
1776
  const entry0 = workflowFor(board.root), card0 = view(board, (db) => getTask(db, id));
1748
- const spec0 = view(board, (db) => runSpec(entry0, entry0.workflow.verbs?.adopt?.[0]?.target ?? 'running', card0));
1749
- const laneName = view(board, (db) => evaluate(entry0.workflow.roles?.[spec0?.role] ?? 'card.assignee', scopeOf(db, board, card0, { params: entry0.params }))) ?? assignee ?? card0?.assignee;
1777
+ const agentRole = view(board, (db) => adoptedRole(db, board, entry0, card0, beforeRun)) ?? 'implementer';
1778
+ const laneName = view(board, (db) => evaluate(entry0.workflow.roles?.[agentRole] ?? 'card.assignee', scopeOf(db, board, card0, { params: entry0.params }))) ?? assignee ?? card0?.assignee;
1750
1779
  const lane = laneName ? lanes?.[laneName] : null, context = process.env.SUPERCODE_CONTEXT, team = agentScope(context);
1751
1780
  let agent = null;
1752
1781
  if (lane && !team.offline) {
1753
- try { agent = ensureCardAgent({ root: board.root, org: team.org, server: team.server, profiles: entry0.profiles, profile: lane.name, card: id, role: spec0?.role ?? 'implementer', sponsor: card0?.sponsor ?? null, machine: normalMachine(host) === localMachine() ? null : host, context }); }
1782
+ try { agent = ensureCardAgent({ root: board.root, org: team.org, server: team.server, profiles: entry0.profiles, profile: lane.name, card: id, role: agentRole, sponsor: card0?.sponsor ?? null, machine: normalMachine(host) === localMachine() ? null : host, context }); }
1754
1783
  catch (error) { if (!teamsUnavailable(error)) throw error; }
1755
1784
  }
1756
1785
  // A session another agent already owns is refused before anything is recorded: the run would name one agent and the
@@ -1782,10 +1811,13 @@ function adoptSession(board, id, who, { workspace = null, ttl, assignee = null,
1782
1811
  event(db, id, 'dispatch_released', { reason: 'verified adoption', session: found.address });
1783
1812
  // the manager's adoption is the workflow's own `adopt` verb where it declares one, sent as the manager; else its
1784
1813
  // dispatch start, which the platform sends for a verified adoption
1785
- if (!beforeRun) { const verb = entry.workflow.verbs?.adopt || !entry.workflow.run ? 'adopt' : entry.workflow.dispatch.start;
1814
+ if (!beforeRun) { const verb = adoptionVerb(entry);
1786
1815
  fire(db, board, entry, id, verb, { roles: [verb === 'adopt' ? 'manager' : 'platform'], payload: { lock: claimer() } }); }
1787
1816
  const runId = getTask(db, id).current_run_id;
1788
1817
  if (!runId) throw new Error(`${id} waits on parents that are not done`);
1818
+ // the agent joined above is the one of the run's own role; a run of another role would name the wrong agent
1819
+ const runRole = currentRun(db, id)?.metadata?.supercode?.role;
1820
+ if (agent && runRole && runRole !== agentRole) throw new Error(`${id}'s run is its ${runRole}'s, and ${found.address} was joined as its ${agentRole}; adopt again`);
1789
1821
  const session = { surface: found.pane ? 'pane' : 'native', session_id: sessionId, harness, machine: host, pane: found.pane ?? null, address: found.address, mode: 'adopt', launched_at: now(), seen_at: now(),
1790
1822
  ...(agent ? { agent_id: agent.identity.id, agent_org: team.org, agent_context: context ?? null,
1791
1823
  agent_profile_intent: { ...structuredClone(agent.intended.resolved), worker: { ...agent.intended.resolved?.worker, harness: lane.harness, model: lane.model ?? null, cwd: dir ?? task.workspace_path ?? null } } } : {}) };
@@ -2207,7 +2239,13 @@ function readMailbox(board, address, report) {
2207
2239
  try { renameSync(path, join(box, 'cur', name)); } catch { continue; } // another reader took it
2208
2240
  // an answer to a question the board asked is what a card waits on (read by the card's workflow), not a verb
2209
2241
  if (envelope.in_reply_to) continue;
2210
- if (envelope.from === `sc:${localMachine()}:operator:teams-events` && envelope.kind === 'notice') { report.mailed.push([envelope.from, 'machine_event']); continue; }
2242
+ // A notice is news for the board, never a card update: a machine's event, or Claude Code's delivery notice about a
2243
+ // message the board sent (filed by the relay from `sc:<machine>:claude-code:notice`, which is no session). Its sender
2244
+ // asked for no reply (reply_via none), so it is filed as read and never answered.
2245
+ if (envelope.kind === 'notice' || envelope.reply_via?.mode === 'none') {
2246
+ report.mailed.push([envelope.from, envelope.from === `sc:${localMachine()}:operator:teams-events` ? 'machine_event' : 'notice']);
2247
+ continue;
2248
+ }
2211
2249
  const argv = json(envelope.body, {})?.workflow;
2212
2250
  let answer;
2213
2251
  if (!Array.isArray(argv) || !argv.length || NOT_BY_MAIL.has(argv[0])) answer = `not applied: a mailed card update is one \`supercode workflow\` verb other than ${[...NOT_BY_MAIL].join(', ')}`;
package/board/ztrack.mjs CHANGED
@@ -339,12 +339,19 @@ class ZtrackBoard {
339
339
  }
340
340
 
341
341
  // A document whose hash is neither the published one nor the one a pending publication replaces
342
- // was edited by hand.
342
+ // was edited by hand. A write also reads again a document its index does not measure: spans whose lengths do not
343
+ // add up to the text cannot place an edit in it (a span's stored text changed outside the board, its length not).
343
344
  staleDocuments({ pending }) {
344
345
  const published = this.get('published', {});
345
346
  const inFlight = pending ? this.get('pending_hashes', null) : null;
347
+ // the repair is said, so a span length written wrong stays visible
348
+ const measured = (doc) => {
349
+ const gap = lf(doc.raw).length - this.entries(doc.role).reduce((sum, e) => sum + e.len, 0);
350
+ if (gap) process.stderr.write(`board: warning: ${doc.path}: its index spans measure ${Math.abs(gap)} character${Math.abs(gap) === 1 ? '' : 's'} ${gap > 0 ? 'short of' : 'past'} its text; this write reads it again\n`);
351
+ return !gap;
352
+ };
346
353
  return this.docs.map((doc) => ({ ...doc, ...this.fileHash(doc.path) }))
347
- .filter((doc) => doc.hash !== published[doc.role] && !(inFlight?.[doc.role] && doc.hash === inFlight[doc.role].before))
354
+ .filter((doc) => (doc.hash !== published[doc.role] && !(inFlight?.[doc.role] && doc.hash === inFlight[doc.role].before)) || (!pending && !measured(doc)))
348
355
  .map((doc) => ({ ...doc, text: lf(doc.raw) }));
349
356
  }
350
357
 
@@ -493,9 +500,13 @@ class ZtrackBoard {
493
500
  const edit = stale.find((s) => s.role === doc.role);
494
501
  if (!edit) { plan.layout.set(doc.role, entries); continue; }
495
502
  let base = this.get('published', {})[doc.role] !== undefined && entries.length ? entries : [];
503
+ // Spans are compared by their stored text and spliced by their recorded length: an index whose lengths are not
504
+ // its texts' (a stored span rewritten outside the board) is no base to re-read a region against.
505
+ let texts = this.texts(base);
506
+ if (texts.some((t, i) => t.text.length !== base[i].len)) { base = []; texts = []; }
496
507
  let result;
497
508
  // A region that cannot be re-read on its own is read with the whole document.
498
- try { result = this.codec.rechunkBoardDocument(this.texts(base), edit.text, doc.path); }
509
+ try { result = this.codec.rechunkBoardDocument(texts, edit.text, doc.path); }
499
510
  catch { base = []; result = this.codec.rechunkBoardDocument([], edit.text, doc.path); }
500
511
  const added = result.chunks.slice(result.from, result.to).map((c) => ({ cid: null, id: c.id, level: c.level, len: c.text.length, rec: null, text: c.text }));
501
512
  plan.layout.set(doc.role, [...base.slice(0, result.from), ...added, ...base.slice(result.oldTo)]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.27",
3
+ "version": "0.5.29",
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": {