@volter/supercode-orchestrator 0.5.179 → 0.5.181

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.
@@ -142,7 +142,9 @@ log('info', `${values.agent}: the account Room ${roomId} is carried to its mailb
142
142
 
143
143
  const supercode = process.env.SUPERCODE_BIN || 'supercode';
144
144
  const sc = (args) => JSON.parse(execFileSync(supercode, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }));
145
- let boundTo = null;
145
+ let boundTo = null, failedAs = null;
146
+ const lastLine = (error) => String(error.stderr || error.message).trim().split('\n').at(-1);
147
+ const warnOnce = (as, line) => { if (failedAs !== as) { failedAs = as; log('warn', line); } };
146
148
  /** The agent's own seat takes its session at RH2's own-seat door (PUT …/members/:principalId/session, a Room
147
149
  * reader's): the session alone changes, never the seat's roles or who may read and post to it, which stay a
148
150
  * members.manage holder's. A refusal throws with its status. */
@@ -151,15 +153,21 @@ async function seatSession(session) {
151
153
  const res = await adapter.fetch(`${adapter.apiUrl()}/api/v3${path}`, { method: 'PUT', headers: adapter.headers(), body: JSON.stringify({ session }) });
152
154
  if (!res.ok) throw new Error(`PUT ${path} -> HTTP ${res.status}: ${(await res.text()).slice(0, 200)}`);
153
155
  }
154
- /** The participant's seat bound to the agent's current main session; a new main session rebinds it. */
156
+ /** The participant's seat bound to the agent's current main session; a new main session rebinds it. The agent's record
157
+ * is read again on every tick, so a re-declared main is bound at the next one. A bind that fails is said once per main
158
+ * and reason (a main that is not in the team's catalog, or one no session ever ran as), never only at debug level,
159
+ * which this process does not print. */
155
160
  async function bindDm() {
156
161
  let main;
157
- try { main = [sc(['agent', 'show', values.agent, '--json'])].flat().find((a) => a.name === values.agent)?.main_session; } catch { return; }
162
+ try { main = [sc(['agent', 'show', values.agent, '--json'])].flat().find((a) => a.name === values.agent)?.main_session; }
163
+ catch (error) { warnOnce(`agent show: ${lastLine(error)}`, `${values.agent}: its agent record was not read (${lastLine(error)}); its DM stays bound to ${boundTo ?? 'nothing'}`); return; }
158
164
  const session = typeof main === 'string' ? main.split(':').pop() : null;
159
- if (!session || session === boundTo) return;
165
+ if (!session) { warnOnce('no main', `${values.agent}: its agent record names no main session; its DM stays bound to ${boundTo ?? 'nothing'}`); return; }
166
+ if (session === boundTo) return;
160
167
  let bound;
161
168
  try { bound = sc(['sessions', 'bind', session, '--room', roomId, '--agent', adapter.principalId(), '--rh2', adapter.apiUrl(), '--json']).data; }
162
- catch (error) { log('debug', `${values.agent}: its DM is not bound yet (${String(error.stderr ?? error.message).trim().split('\n').at(-1)})`); return; }
169
+ catch (error) { warnOnce(`${session}: ${lastLine(error)}`, `${values.agent}: its DM is not bound to its main session ${main} (exit ${error.status ?? '?'}: ${lastLine(error)}); retried every 15 s with the record read again`); return; }
170
+ failedAs = null;
163
171
  await seatSession({ harness: bound.harness, sessionId: bound.sessionId, team: bound.team, teamsUrl: bound.teamsUrl });
164
172
  boundTo = session;
165
173
  log('info', `${values.agent}: its DM in the account Room is its main session ${session}`);
@@ -2107,7 +2107,7 @@ async function launchPane(board, task, runId, lane, fleet, { input = null, slot
2107
2107
  if (next === 'refuse') throw new Error(`session ${prev.session_id} still has live pane ${prev.pane}; cannot resume or replace it`);
2108
2108
  if (next === 'keep') {
2109
2109
  // the session is kept; the heartbeat's count belongs to the run that ended, so the new run starts it again
2110
- const { missing_since: _m, goal_turns: _t, goal_nudged_at: _n, idle_since: _is, unknown_since: _us, unknown_notice: _un, work_progress: _wp, idle_observation: _io, idle_below_threshold: _ib, idle_eligible: _ie, close_requested: _q, closed_at: _c, close_confirmed_at: _cc, close_error: _ce, close_attempts: _ca, close_attempted_at: _ct, close_failed_at: _cf, close_unread: _cu, close_claim: _cl, monitor_held: _mh, released: _rl, ...kept } = prev;
2110
+ const { missing_since: _m, goal_turns: _t, goal_nudged_at: _n, idle_since: _is, unknown_since: _us, unknown_notice: _un, work_progress: _wp, idle_observation: _io, idle_below_threshold: _ib, idle_eligible: _ie, close_requested: _q, closed_at: _c, close_confirmed_at: _cc, close_error: _ce, close_attempts: _ca, close_attempted_at: _ct, close_failed_at: _cf, close_unread: _cu, close_claim: _cl, monitor_held: _mh, released: _rl, review_held: _rh, ...kept } = prev;
2111
2111
  session = { ...kept, mode: 'adopt', launched_at: now(), seen_at: now() };
2112
2112
  // a kept session is told what this run is for (its card came back to it, with what happened since), only while
2113
2113
  // this run is still the card's current one (its pane and native reads were awaited)
@@ -3070,16 +3070,42 @@ function outcomeTicked(body) {
3070
3070
  /** An opened PR asks for the declared review transition once the arc's tasks are closed. */
3071
3071
  // A task written in the card's own Tasks section that did not become a subtask holds its review too.
3072
3072
  /** The arc's session still in a turn: its review, and the close of that session the request brings (end_run), wait for
3073
- * the turn to end, so work in flight is never cut (t_f34710bc's coder had a proof take running when its review was
3074
- * requested at 15:01:13Z on 2026-10-04, and the close ended it). A session the reading cannot place waits on nothing. */
3075
- async function turnInFlight(board, id, fleet) {
3073
+ * the turn to end, so work in flight is never cut and the reviewer never starts in the coder's checkout beside a turn
3074
+ * still running there (t_f34710bc's coder had a proof take running when its review was requested at 15:01:13Z on
3075
+ * 2026-10-04, and the close ended it). A session the reading cannot place waits on nothing.
3076
+ *
3077
+ * A Codex session with an active thread goal is never read between turns: the goal starts the next turn the moment one
3078
+ * ends (t_374d7f70, codex 01a108b7, 2026-10-14). For it the turn boundary is the one its harness records: the open turn
3079
+ * was started by the goal (Codex's own `turn_trigger`, published as the activity's `turn_trigger`), not by a person or
3080
+ * by mail, and it started after `heldSince`, the first round that found the review due with nothing but this turn
3081
+ * holding it. So the turn that was running when the review fell due, the one that closed the tasks and pushed the
3082
+ * heads, has ended with the coder's own completion; a goal turn that began before that round, which may be the one
3083
+ * running a proof, still holds the review. */
3084
+ async function turnInFlight(board, id, fleet, heldSince = null) {
3076
3085
  if (!fleet?.ok) return null;
3077
3086
  const session = currentRun(board.db, id)?.metadata?.supercode;
3078
3087
  if (!session?.session_id || session.surface === 'headless' || session.close_requested) return null;
3079
- let turn = fleet.sessions?.find((x) => x.address === addressOf(session))?.turn ?? null;
3080
- if (turn == null && session.surface === 'native') try { turn = nativeReading(await readNative(session.machine, session.harness ?? 'claude-code', session.session_id), session).row?.activity?.turn ?? null; } catch { /* unread: waits on nothing */ }
3088
+ let live = fleet.sessions?.find((x) => x.address === addressOf(session)) ?? null;
3089
+ if (live?.turn == null && session.surface === 'native') try {
3090
+ const activity = nativeReading(await readNative(session.machine, session.harness ?? 'claude-code', session.session_id), session).row?.activity;
3091
+ live = activity ? { turn: activity.turn ?? null, turn_trigger: activity.evidence?.turn_trigger ?? null, turn_since_ms: activity.evidence?.native_state_since_ms ?? null } : live;
3092
+ } catch { /* unread: waits on nothing */ }
3093
+ const turn = live?.turn ?? null;
3081
3094
  // a turn stopped on a permission or a question (needs_input) is a turn not finished, as one still working is
3082
- return ['working', 'needs_input'].includes(turn) ? `its session ${addressOf(session)} is in a turn (${turn}); the review is requested once the turn ends` : null;
3095
+ if (!['working', 'needs_input'].includes(turn)) return null;
3096
+ const started = Number(live.turn_since_ms) / 1000;
3097
+ if (turn === 'working' && live.turn_trigger === 'goal' && heldSince != null && started > heldSince) return null;
3098
+ return { why: `its session ${addressOf(session)} is in a turn (${turn}${live.turn_trigger ? `, started by ${live.turn_trigger}` : ''}); the review is requested once the turn ends${live.turn_trigger === 'goal' ? ', or once a goal turn starts after this round' : ''}` };
3099
+ }
3100
+
3101
+ /** What a review held on its session's turn was due on: the arc's PR heads (or its no-code act) and its last task close.
3102
+ * A hold is kept only while this stays the same; anything else due is a new hold from its own round. */
3103
+ function reviewDueKey(board, id, payload) {
3104
+ const closed = board.db.prepare(`SELECT MAX(id) AS id FROM task_events WHERE kind IN ('completed', 'cancelled')
3105
+ AND task_id IN (SELECT task_id FROM supercode_cards WHERE subtask_of = ?)`).get(id)?.id ?? null;
3106
+ const source = payload.no_code ? `no_code:${payload.no_code.kind ?? ''}:${payload.no_code.repository ?? ''}:${payload.no_code.base ?? ''}`
3107
+ : payload.pr.map((url, index) => `${url}@${payload.pr_head[index]}`).join(',');
3108
+ return `${source}|${closed ?? ''}`;
3083
3109
  }
3084
3110
 
3085
3111
  async function activatePullRequestReviews(board, entry, report, effects, { only = null, dry = false, fleet = null } = {}) {
@@ -3101,8 +3127,6 @@ async function activatePullRequestReviews(board, entry, report, effects, { only
3101
3127
  const ready = view(board, eligible);
3102
3128
  if (!ready) continue;
3103
3129
  if (ready !== true) { report.queued.push([id, `review waits: ${ready.length} open task(s) in the card's Tasks: ${ready[0].id} ${ready[0].title}`]); continue; }
3104
- const inTurn = await turnInFlight(board, id, fleet);
3105
- if (inTurn) { report.queued.push([id, `review waits: ${inTurn}`]); continue; }
3106
3130
  let payload = null;
3107
3131
  try {
3108
3132
  payload = await openReviewPullRequests(board, id);
@@ -3121,6 +3145,16 @@ async function activatePullRequestReviews(board, entry, report, effects, { only
3121
3145
  }
3122
3146
  const evidence = payload.no_code ? { required: false, ready: true, reason: null, phase: 'source', no_code: payload.no_code } : await inspectCompletionEvidence(board, id, entry, payload, 'request_review');
3123
3147
  if (!evidence.ready) throw new Error(evidence.reason);
3148
+ // Due in every other respect: only the session's turn may still hold it. The first round that finds it so is
3149
+ // recorded on the run (`review_held`), with what it was due on, so a goal turn can be told to have started after.
3150
+ const heldRun = currentRun(board.db, id), dueKey = reviewDueKey(board, id, payload);
3151
+ const held = heldRun?.metadata?.supercode?.review_held?.key === dueKey ? heldRun.metadata.supercode.review_held : null;
3152
+ const inTurn = await turnInFlight(board, id, fleet, held ? Number(held.since) : null);
3153
+ if (inTurn) {
3154
+ if (!held && !dry && heldRun) tx(board, (db) => { if (currentRun(db, id)?.id === heldRun.id) setRunSession(db, heldRun.id, { review_held: { key: dueKey, since: now() } }); });
3155
+ report.queued.push([id, `review waits: ${inTurn.why}`]);
3156
+ continue;
3157
+ }
3124
3158
  // a dry run names the review a tick would request and writes nothing
3125
3159
  if (dry) { (report.would_review ??= []).push({ id, prs: payload.pr, heads: payload.pr_head, no_code: payload.no_code ?? null }); continue; }
3126
3160
  await refreshCardInputs(board, id, { commits: true });
@@ -100,8 +100,10 @@ export async function readMachineFleet(machines, local) {
100
100
  sessions.push({ address, machine: row.machine, harness: activity.harness, session_id: activity.session_id, pane: pane.pane, name: pane.title ?? pane.label ?? null,
101
101
  starter_session: pane.starter_session ?? null,
102
102
  state: running ? 'running' : 'unknown', turn: activity.turn ?? null, native_state: activity.evidence?.native_state ?? null,
103
- // what a session that needs input says it waits for and since when, where its harness publishes them
104
- waiting_for: activity.evidence?.waiting_for ?? null, turn_source: activity.evidence?.source ?? null, turn_since_ms: activity.evidence?.native_state_since_ms ?? null });
103
+ // what a session that needs input says it waits for, what started its open turn (Codex: `goal` for a thread
104
+ // goal's continuation) and since when, where its harness publishes them
105
+ waiting_for: activity.evidence?.waiting_for ?? null, turn_source: activity.evidence?.source ?? null, turn_since_ms: activity.evidence?.native_state_since_ms ?? null,
106
+ turn_trigger: activity.evidence?.turn_trigger ?? null });
105
107
  }
106
108
  }
107
109
  const ok = online.has(local);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.179",
3
+ "version": "0.5.181",
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": {
@@ -24,7 +24,7 @@
24
24
  "check-types": "node scripts/generate-types.mjs --check"
25
25
  },
26
26
  "dependencies": {
27
- "@volter/supercode-harness-sdk": "^0.3.100",
27
+ "@volter/supercode-harness-sdk": "^0.3.101",
28
28
  "@volter/teams": "0.2.79",
29
29
  "@volter/ztrack": "^2.4.23",
30
30
  "yaml": "^2.9.0"