@volter/supercode-orchestrator 0.5.208 → 0.5.209

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.
@@ -125,7 +125,11 @@ function agentFacts(db,taskId){
125
125
  const agents=new Map(parse(checkpoint?.facts_json,[]).map(agent=>[JSON.stringify([agent.org,agent.assignment,agent.id]),agent]));
126
126
  let scope=db.prepare('SELECT from_org,org,changed_at FROM supercode_card_agent_scopes WHERE task_id=?').get(taskId)??null;
127
127
  let receipt=checkpoint?.receipt_sequence??0,declared=checkpoint?.declared_event_id??0;
128
- for(const event of db.prepare(`SELECT id,kind,payload FROM task_events WHERE task_id=? AND kind IN ${AGENT_KINDS} ORDER BY id`).all(taskId)){
128
+ // Match idx_events_task_kind's order. ORDER BY id alone lets SQLite choose idx_events_task_id and fetch every
129
+ // event's row just to reject its kind (including cards with no agent events). Those synchronous page reads held
130
+ // the owner's loop inside synchronizeAgents and expired unrelated manager lookups. Sort only the matching events.
131
+ const events=db.prepare(`SELECT id,kind,payload FROM task_events WHERE task_id=? AND kind IN ${AGENT_KINDS} ORDER BY kind,id`).all(taskId);
132
+ for(const event of events.sort((a,b)=>a.id-b.id)){
129
133
  if(event.kind==='agent_declared')declared=Math.max(declared,event.id);
130
134
  const value=parse(event.payload,{});
131
135
  if(Number.isSafeInteger(value.receipt))receipt=Math.max(receipt,value.receipt);
package/board/ztrack.mjs CHANGED
@@ -830,7 +830,6 @@ class ZtrackBoard {
830
830
  if (this.get('rebuild_from_documents') || !this.one('SELECT 1 FROM main.tasks LIMIT 1')) return;
831
831
  let fields = [];
832
832
  try { fields = workflowFor(this.root).params.document_control_fields ?? []; } catch { /* no home policy */ }
833
- if (!fields.length) return;
834
833
  const value = (task, state, field) => field === 'status' ? state?.stage ?? task?.status ?? 'todo'
835
834
  : field === 'blocked_by' ? json(state?.blocked_by, []).map((b) => [b.kind, b.id]).sort()
836
835
  : field === 'paused' ? json(state?.paused, null)
@@ -862,15 +861,20 @@ class ZtrackBoard {
862
861
  };
863
862
  for (const [id, rows] of plan.rows) {
864
863
  const task = this.one('SELECT * FROM main.tasks WHERE id = ?', id), state = this.one('SELECT * FROM main.supercode_cards WHERE task_id = ?', id);
864
+ // Stale status recovery is independent of a home's document-control policy. In particular, render() re-reads a
865
+ // refused span after a verb completes its card: its unchanged running line must not undo that completion.
866
+ // Reopening an ended card requires its workflow verb, even in a home that otherwise permits document edits.
867
+ const controlled = new Set(fields);
868
+ if (task && ENDED.has(value(task, state, 'status'))) controlled.add('status');
865
869
  // A new display author is not an authenticated creator.
866
- const differ = fields.filter((field) => (task || field !== 'created_by') && !same(value(rows.task, rows.state, field), value(task, state, field)));
870
+ const differ = [...new Set(['status', ...controlled])].filter((field) => (task || field !== 'created_by') && !same(value(rows.task, rows.state, field), value(task, state, field)));
867
871
  const prior = task && differ.length ? rowsBefore(rows.state.subtask_of ?? id).get(id) : null;
868
872
  const stale = prior ? differ.filter((field) => same(value(prior.task, prior.state, field), value(rows.task, rows.state, field))) : [];
869
873
  for (const field of stale) adopt(rows, task, state, field);
870
- const forbidden = differ.filter((field) => !stale.includes(field));
874
+ const forbidden = differ.filter((field) => controlled.has(field) && !stale.includes(field));
871
875
  if (forbidden.length) plan.errors.set(id, `Document edit cannot change ${forbidden.join(', ')}; use this card's role-checked workflow verbs or request-state.`);
872
876
  }
873
- for (const id of plan.remove) {
877
+ for (const id of fields.length ? plan.remove : []) {
874
878
  // A task line already absent from the replaced span was not removed by this edit: the store keeps it.
875
879
  const parent = this.one('SELECT subtask_of FROM main.supercode_cards WHERE task_id = ?', id)?.subtask_of;
876
880
  const prior = parent && !plan.remove.has(parent) ? rowsBefore(parent) : null;
package/connection.mjs CHANGED
@@ -101,14 +101,22 @@ export async function connectWorkplace(flags) {
101
101
  if(Boolean(flags['webhook-url'])!==Boolean(flags['webhook-listen']))throw new Error('--webhook-url and --webhook-listen go together: where the Workplace sends the agents registration\'s events, and the loopback address the service listens on');
102
102
  const moduleRoot=flags['modules-root']?resolve(flags['modules-root']):null;
103
103
  // An explicit local credential input is useful for an owned stack; a normal connect uses its existing Teams context.
104
- let teams,owner;
104
+ let teams,owner,teamsContext=flags.context??process.env.SUPERCODE_CONTEXT;
105
105
  if(flags['teams-token-file']){
106
106
  if(!flags['teams-url']||!flags.team)throw new Error('--teams-token-file needs --teams-url and --team');
107
107
  teams={server:new URL(flags['teams-url']).origin,team:flags.team};owner=readFileSync(flags['teams-token-file'],'utf8').trim();
108
108
  }else{
109
- const {context}=fromContext({supercodeHome:supercodeHome(),name:flags.context});
109
+ // An agent session connects as its agent: its delegated context, never the person's who signed the box in (the
110
+ // Teams verbs' rule, agentContextName). A person's context named from an agent session is refused, as a grant is.
110
111
  const {WorkspaceContexts}=await import('@volter/teams/context');
111
- teams={server:context.origin,team:context.team_id};owner=new WorkspaceContexts(supercodeHome()).credential(context.credential_ref).token;
112
+ const {agentSessionEnvironment,agentSessionHint,agentContextName}=await import('@volter/supercode-teams/agent-session');
113
+ const contexts=new WorkspaceContexts(supercodeHome());
114
+ // Asked of the daemon whatever the environment says: a resumed session can lack its launch's SUPERCODE_AGENT_ID,
115
+ // and the daemon's retained binding is what names its agent (workspace.mjs does the same for every Teams verb).
116
+ const env=await agentSessionEnvironment(process.env,agentSessionHint(process.env));
117
+ teamsContext=agentContextName(contexts,env,flags.context??null);
118
+ const {context}=fromContext({supercodeHome:supercodeHome(),name:teamsContext});
119
+ teams={server:context.origin,team:context.team_id};owner=contexts.credential(context.credential_ref).token;
112
120
  }
113
121
  for(const origin of [ticket.origin,ticket.issuer,teams.server]){
114
122
  const url=new URL(origin);if(url.origin!==origin||url.username||url.password||(url.protocol!=='https:'&&!(url.protocol==='http:'&&['127.0.0.1','localhost','[::1]'].includes(url.hostname))))throw new Error('connection endpoints must be exact HTTPS origins or loopback HTTP');
@@ -116,7 +124,7 @@ export async function connectWorkplace(flags) {
116
124
  const id=createHash('sha256').update(`${ticket.scope}\0${ticket.issuer}\0${ticket.organization}\0${teams.server}\0${teams.team}\0${flags.source}`).digest('hex').slice(0,24);
117
125
  const path=connectionPath(root,id),prior=existsSync(path)?JSON.parse(readFileSync(path,'utf8')):null;
118
126
  if(!prior&&!(Date.parse(ticket.expiresAt)>Date.now()))throw new Error('connection ticket expired; apply the organization file again');
119
- const record=prior??{version:1,id,scope:ticket.scope,teamsContext:flags.context??process.env.SUPERCODE_CONTEXT,origin:ticket.origin,issuer:ticket.issuer,organization:ticket.organization,teams,source:flags.source,
127
+ const record=prior??{version:1,id,scope:ticket.scope,teamsContext,origin:ticket.origin,issuer:ticket.issuer,organization:ticket.organization,teams,source:flags.source,
120
128
  selection:ticket.grants.map(g=>g.name),moduleRoot,...(flags['webhook-url']?{webhook:{url:flags['webhook-url'],listen:flags['webhook-listen']}}:{}),floor:flags.floor??ticket.floor??'Work',alertRole:flags['alert-role']??'admin',grants:[]};
121
129
  record.root=root;
122
130
  if(prior?.disconnectedAt)throw new Error('this connection was disconnected; apply the organization file again for a new ticket');
@@ -187,7 +195,7 @@ export async function connectWorkplace(flags) {
187
195
  }
188
196
 
189
197
  /** Undo a connection with its own credentials, no admin needed (runhuman-2 ADR 0055 decision 3): its runner stops, every
190
- * installation it holds leaves both sides by itself (each Workplace installation through its own uninstall door, each
198
+ * installation it holds leaves both sides by itself (each Workplace installation through its own leave door, each
191
199
  * Teams installation and the issuer's by deleting itself), and the home's registry entries go. The registrations stay, as
192
200
  * consented apps with no installation; an admin deregisters them. The custody record stays, marked disconnected, for
193
201
  * audit; it can never run again. */
@@ -211,10 +219,11 @@ export async function disconnectWorkplace(flags) {
211
219
  };
212
220
  for(const grant of record.grants){
213
221
  if(grant.teams?.installation?.id)await leave(grant.name,'teams','DELETE',`${team}/app-installations/${encodeURIComponent(grant.teams.installation.id)}`,grant.teams.credential.token);
214
- if(grant.rh2?.installationId)await leave(grant.name,'rh2','POST',`/api/v3/automation/installations/${encodeURIComponent(grant.rh2.installationId)}/uninstall`,grant.rh2.accessToken);
222
+ if(grant.rh2?.installationId)await leave(grant.name,'rh2','POST',`/api/v3/automation/installations/${encodeURIComponent(grant.rh2.installationId)}/leave`,grant.rh2.accessToken);
215
223
  const entry=join(root,'apps',`${grant.name}.${record.id}.json`);if(existsSync(entry))unlinkSync(entry);
216
224
  }
217
- if(record.issuerCredential?.installation?.id)await leave('issuer','teams','DELETE',`${team}/app-installations/${encodeURIComponent(record.issuerCredential.installation.id)}`,record.issuerCredential.token);
225
+ // The token exchange answers {installation, credential: {token, …}} (renewTeams reads the same shape).
226
+ if(record.issuerCredential?.installation?.id)await leave('issuer','teams','DELETE',`${team}/app-installations/${encodeURIComponent(record.issuerCredential.installation.id)}`,record.issuerCredential.credential?.token);
218
227
  record.disconnectedAt=new Date().toISOString();record.left=left;privateJson(path,record);
219
228
  return {connection:record.id,root,left,registrations:[...record.grants.map(g=>g.registration?.id).filter(Boolean),record.issuerRegistration?.id].filter(Boolean)};
220
229
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-orchestrator",
3
- "version": "0.5.208",
3
+ "version": "0.5.209",
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": {
@@ -32,7 +32,7 @@
32
32
  "check-types": "node scripts/generate-types.mjs --check"
33
33
  },
34
34
  "dependencies": {
35
- "@volter/supercode-teams": "^0.3.345",
35
+ "@volter/supercode-teams": "^0.3.344",
36
36
  "@volter/supercode-terminal": "^0.3.10",
37
37
  "@volter/supercode-harness-sdk": "^0.3.104",
38
38
  "@volter/teams": "0.2.98",
@@ -61,6 +61,5 @@
61
61
  "board",
62
62
  "apply",
63
63
  "README.md"
64
- ],
65
- "gitHead": "db4a88b4d2f670654fa8e57dbc3e93c7daa8d2bb"
64
+ ]
66
65
  }
package/source-lock.mjs CHANGED
@@ -10,6 +10,8 @@ if (lock) {
10
10
  enforceSourceLock(import.meta.resolve('@volter/supercode-teams/source-lock'), 'the orchestrator Teams package');
11
11
  }
12
12
 
13
+ let driftSaid = false;
14
+
13
15
  /** The locked tree's release native, proved against the same lock before any codec door opens. */
14
16
  export function lockedNative() {
15
17
  const lock = enforceSourceLock(import.meta.url, 'the orchestrator');
@@ -27,8 +29,11 @@ export function lockedNative() {
27
29
  const result = spawnSync(binary, ['source', 'show', '--json'], { encoding: 'utf8', timeout: 5000 });
28
30
  if (result.error || result.status !== 0) throw new SourceLockError(`supercode source lock: the orchestrator native ${binary} cannot be verified: ${result.error?.message || result.stderr}`);
29
31
  const receipt = JSON.parse(result.stdout);
30
- if (receipt.verdict !== 'matches' || receipt.native.version !== lock.supercode || receipt.native.commit !== lock.commit || receipt.native.dirty) {
31
- throw new SourceLockError(`supercode source lock: the orchestrator native ${binary} does not match ${lock.path}: ${receipt.verdict}`);
32
+ // The native is judged by its binding alone: a retired copy that came back (drift) is said, and never stops the board.
33
+ const binding = receipt.binding ?? receipt.verdict;
34
+ if (binding !== 'matches' || receipt.native.version !== lock.supercode || receipt.native.commit !== lock.commit || receipt.native.dirty) {
35
+ throw new SourceLockError(`supercode source lock: the orchestrator native ${binary} does not match ${lock.path}: ${binding}`);
32
36
  }
37
+ if (Array.isArray(receipt.drift) && receipt.drift.length && !driftSaid) { driftSaid = true; process.stderr.write(`supercode source lock: ${receipt.drift.join('; ')}\n`); }
33
38
  return binary;
34
39
  }