@volter/supercode-teams 0.2.54 → 0.2.56

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/machine.mjs CHANGED
@@ -144,9 +144,10 @@ export class TeamsMachine extends MachineCore {
144
144
  // Each pane launched here opens a terminal window on this machine's screen (popupWindow).
145
145
  this.popups = Boolean(popups);
146
146
  this.launches = new Launches(this.p.launches);
147
- // Windows has no tmux: its panes are ConPTY children of this daemon, and end with it.
148
- if (process.platform === 'win32' && !terminal.PtyTerminalHost) throw new Error('panes on Windows need a newer @volter/supercode-terminal (one with PtyTerminalHost)');
149
- this.host = process.platform === 'win32' ? new terminal.PtyTerminalHost() : new TmuxTerminalHost({
147
+ // Windows has no tmux: its panes live in the machine's pane server, a process of their own that outlives this
148
+ // daemon (a restart or an upgrade never ends a pane), as tmux panes live in tmux's server.
149
+ if (process.platform === 'win32' && !terminal.PaneServerHost) throw new Error('panes on Windows need a newer @volter/supercode-terminal (one with PaneServerHost)');
150
+ this.host = process.platform === 'win32' ? new terminal.PaneServerHost({ pipe: `\\\\.\\pipe\\supercode-panes-${fingerprint(this.identity.publicKeyPem)}` }) : new TmuxTerminalHost({
150
151
  // Stable owner id so panes this daemon launched stay "owned" across restarts.
151
152
  ownerId: `supercode-teams-${fingerprint(this.identity.publicKeyPem)}`,
152
153
  ...(tmuxSocket ? { socket: tmuxSocket } : {}),
@@ -459,7 +460,7 @@ export class TeamsMachine extends MachineCore {
459
460
  let release;
460
461
  try {
461
462
  release = this.claimAttachment(principal);
462
- grant = this.host.issueAttachment(session.id, { mode });
463
+ grant = await this.host.issueAttachment(session.id, { mode });
463
464
  } catch (error) {
464
465
  release?.();
465
466
  this.log.append({ principal, cap, target: `pane:${pane}`, outcome: 'refused', detail: error.message });
@@ -571,9 +572,11 @@ export class TeamsMachine extends MachineCore {
571
572
  } catch { /* no harness evidence this tick: the process tree still answers */ }
572
573
  }
573
574
  const live = new Set();
574
- const rows = sessions.map((session) => {
575
+ // Every pane's screen, read before the rows are built (a pane server answers asynchronously).
576
+ const captures = await Promise.all(sessions.map((session) => this.#safeCapture(session.id)));
577
+ const rows = sessions.map((session, index) => {
575
578
  if (session.contextKey) live.add(session.contextKey);
576
- const capture = this.#safeCapture(session.id);
579
+ const capture = captures[index];
577
580
  const requested = session.contextKey ? doc.requests[session.contextKey] ?? null : null;
578
581
  const own = harnessLiveness(harness.get(session.rootPid));
579
582
  const evidence = {
@@ -595,10 +598,10 @@ export class TeamsMachine extends MachineCore {
595
598
 
596
599
  /** One capture, never fatal: a pane that died between the listing and the read
597
600
  * is evidence of nothing, not a failed door. */
598
- #safeCapture(sessionId) {
601
+ async #safeCapture(sessionId) {
599
602
  if (!sessionId) return '';
600
603
  try {
601
- return this.host.capture(sessionId, { lines: 120 });
604
+ return await this.host.capture(sessionId, { lines: 120 });
602
605
  } catch {
603
606
  return '';
604
607
  }
@@ -687,7 +690,7 @@ export class TeamsMachine extends MachineCore {
687
690
  const composerKind = typeof kind === 'string' ? kind.trim().toLowerCase() : '';
688
691
  const until = Date.now() + INITIAL_INPUT_READY_DEADLINE_MS;
689
692
  while (!signal.aborted && Date.now() < until) {
690
- const capture = this.#safeCapture(session.id);
693
+ const capture = await this.#safeCapture(session.id);
691
694
  const unchangedMs = this.#screenUnchangedMs(session.id, capture, Date.now());
692
695
  const blank = capture.trim() === '';
693
696
  // A blank screen is the one reading that can mean the pane is GONE rather
@@ -1119,7 +1122,7 @@ export class TeamsMachine extends MachineCore {
1119
1122
  need(operator ? 'control' : 'control', `pane:${params.pane}`);
1120
1123
  const session = await this.#paneByContext(params.pane);
1121
1124
  if (!session) throw new Error(`no pane with context ${params.pane}`);
1122
- this.host.closeSession(session.id);
1125
+ await this.host.closeSession(session.id);
1123
1126
  act('close', `pane:${params.pane}`);
1124
1127
  return { closed: params.pane };
1125
1128
  }
@@ -1170,10 +1173,10 @@ export class TeamsMachine extends MachineCore {
1170
1173
  if (!session) throw Object.assign(new Error(`no pane with context ${params.pane}`), { code: 404 });
1171
1174
  if (this.suspended.has(params.pane)) throw Object.assign(new Error(`pane ${params.pane} is suspended: remote input is refused until resume`), { code: 423 });
1172
1175
  const kind = this.#paneKind(params.pane, session);
1173
- const draftOf = () => composerHoldsDraft(kind, this.host.capture(session.id, { lines: 200, preserveEscapes: true }));
1174
- const first = draftOf();
1176
+ const draftOf = async () => composerHoldsDraft(kind, await this.host.capture(session.id, { lines: 200, preserveEscapes: true }));
1177
+ const first = await draftOf();
1175
1178
  await new Promise((resolve) => setTimeout(resolve, COMPOSER_RECHECK_MS));
1176
- const second = draftOf();
1179
+ const second = await draftOf();
1177
1180
  if (first !== false || second !== false) {
1178
1181
  return { delivered: false, reason: first === true || second === true ? 'draft' : 'no-composer' };
1179
1182
  }
@@ -1185,7 +1188,7 @@ export class TeamsMachine extends MachineCore {
1185
1188
  need('observe', `pane:${params.pane}`);
1186
1189
  const session = await this.#paneByContext(params.pane);
1187
1190
  if (!session) throw new Error(`no pane with context ${params.pane}`);
1188
- return { text: this.host.capture(session.id, { lines: params.lines ?? 200 }) };
1191
+ return { text: await this.host.capture(session.id, { lines: params.lines ?? 200 }) };
1189
1192
  }
1190
1193
  case 'files.stat': case 'files.list': case 'files.read': case 'files.write': case 'files.mkdir': case 'files.rename': case 'files.delete': case 'files.search': {
1191
1194
  // Authorize the REAL path: a symlink under a granted root must not reach outside it. Reading (stat, list,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.2.54",
3
+ "version": "0.2.56",
4
4
  "type": "module",
5
5
  "description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
6
6
  "license": "MIT",
@@ -284,7 +284,7 @@ async function captureCommit(api,req,res,auth,sourceId,captureId,requestId){cons
284
284
  if(!origin)statements.push({sql:'INSERT INTO sessions(team_id,id,harness,custodian_id,visibility,revision,created_at) VALUES (?,?,?,?,?,?,?)',params:[auth.team_id,sid,cap.harness,source.custodian_id,lineage?'restricted':begin.visibility,revisionParam,boundaryNow]},{sql:'INSERT INTO origins(team_id,source_id,harness,native_id,session_id,generation,sequence,digest) VALUES (?,?,?,?,?,?,?,?)',params:[auth.team_id,sourceId,cap.harness,cap.native_id,sid,cap.generation,cap.sequence,commit.digest]});else statements.push({sql:'UPDATE sessions SET revision=? WHERE team_id=? AND id=?',params:[revisionParam,auth.team_id,sid]},{sql:'UPDATE origins SET generation=?,sequence=?,digest=? WHERE team_id=? AND source_id=? AND harness=? AND native_id=?',params:[cap.generation,cap.sequence,commit.digest,auth.team_id,sourceId,cap.harness,cap.native_id]});
285
285
  if(lineage){lineageValue.new_session_id=sid;const operationRow=await api.store.one('SELECT value_json FROM operations WHERE team_id=? AND id=?',[auth.team_id,lineageValue.operation_id]),operationValue=JSON.parse(operationRow.value_json);operationValue.result={...operationValue.result,new_session_id:sid};statements.push({sql:"UPDATE continuations SET value_json=? WHERE team_id=? AND id=? AND state='running' AND value_json=?",params:[JSON.stringify(lineageValue),auth.team_id,lineage.id,lineage.value_json]},{sql:'UPDATE operations SET value_json=? WHERE team_id=? AND id=?',params:[JSON.stringify(operationValue),auth.team_id,lineageValue.operation_id]});lineageGuards.push({sql:"SELECT 1 ok FROM continuations WHERE team_id=? AND id=? AND state='running' AND value_json=?",params:[auth.team_id,lineage.id,lineage.value_json],code:'conflict',status:409,message:'continuation lineage changed'});}
286
286
  statements.push({sql:'INSERT INTO session_versions(team_id,session_id,revision,descriptor_json,source_projection_json,search_title,sort_at,created_at) VALUES (?,?,?,?,?,?,?,?)',params:[auth.team_id,sid,revisionParam,{$journalJsonValue:descriptor},JSON.stringify({source_id:sourceId}),normalizeSearch(commit.title??''),sortAt,boundaryNow]});let ordinal=0;for(const m of messages){let mid=(await api.store.one('SELECT id FROM message_ids WHERE team_id=? AND source_id=? AND harness=? AND native_id=? AND native_key=?',[auth.team_id,sourceId,cap.harness,cap.native_id,m.native_key]))?.id;if(!mid){mid=api.ids.next('message');statements.push({sql:'INSERT INTO message_ids(team_id,source_id,harness,native_id,native_key,id) VALUES (?,?,?,?,?,?)',params:[auth.team_id,sourceId,cap.harness,cap.native_id,m.native_key,mid]});}statements.push({sql:'INSERT INTO messages(team_id,session_id,revision,ordinal,id,source_id,native_key,recorded_at,value_json,search_text) VALUES (?,?,?,?,?,?,?,?,?,?)',params:[auth.team_id,sid,revisionParam,ordinal++,mid,sourceId,m.native_key,m.recorded_at,JSON.stringify(m.value),normalizeSearch(textFromContent(m.value.content))]});}
287
- const bundle=commit.native_bundle??null,legacyBundle=commit.artifacts.length?captureId:null;for(let index=0;index<commit.artifacts.length;index++){const a=commit.artifacts[index],file=bundle?.files.find(x=>x.upload_id===a.upload_id);statements.push({sql:'UPDATE uploads SET manifest_json=?,committed=1 WHERE team_id=? AND id=?',params:[JSON.stringify(a),auth.team_id,a.upload_id]},{sql:'INSERT INTO session_artifacts(team_id,session_id,revision,bundle_id,upload_id,format,provenance,file_role,fidelity,residue_json) VALUES (?,?,?,?,?,?,?,?,?,?)',params:[auth.team_id,sid,revisionParam,bundle?.id??legacyBundle,a.upload_id,bundle?.harness??cap.harness,'capture',file?.role??(index===0?'primary':'bundle'),bundle?.fidelity??'semantic',JSON.stringify(bundle?.residue??['native_bundle_metadata_unavailable'])]});}const storedReceipt={source_id:sourceId,capture_id:captureId,session_id:sid,generation:cap.generation,sequence:cap.sequence,digest:commit.digest,revision:'__JOURNAL_REVISION__',duplicate:false};statements.push({sql:"UPDATE captures SET state='committed',commit_json=?,receipt_json=? WHERE team_id=? AND id=?",params:[JSON.stringify(commit),{$journalJsonValue:storedReceipt},auth.team_id,captureId]},{sql:'UPDATE sources SET last_received_at=? WHERE team_id=? AND id=?',params:[boundaryNow,auth.team_id,sourceId]},{sql:'UPDATE teams SET journal_revision=? WHERE id=?',params:[revisionParam,auth.team_id]},{sql:'INSERT INTO journal(team_id,revision,session_id,kind,created_at) VALUES (?,?,?,?,?)',params:[auth.team_id,revisionParam,sid,'capture',boundaryNow]});const guards=[{sql:'SELECT 1 ok FROM credentials WHERE id=? AND source_id=? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at>?)',params:[auth.credential_id,sourceId,boundaryNow],code:'authentication_required',status:401,message:'source credential expired or revoked'},...lineageGuards,{sql:"SELECT 1 ok FROM captures c JOIN sources s ON s.team_id=c.team_id AND s.id=c.source_id LEFT JOIN machines m ON m.team_id=s.team_id AND m.id=s.machine_id LEFT JOIN sync_rules r ON r.team_id=c.team_id AND r.id=json_extract(c.begin_json,'$.rule_id') WHERE c.team_id=? AND c.id=? AND c.source_id=? AND c.state='staging' AND s.state='active' AND (s.kind!='connector' OR (m.revoked_at IS NULL AND r.machine_id=m.id AND r.source_id=s.id AND r.state='active' AND r.revision=json_extract(c.begin_json,'$.rule_revision') AND r.visibility=json_extract(c.begin_json,'$.visibility')))",params:[auth.team_id,captureId,sourceId],code:'conflict',status:409,message:'capture authorization changed'}],storedResponse=result(api,auth,storedReceipt),{revision}=await guardedMutation(api,res,auth,guards,statements,{status:200,body:storedResponse,targetId:captureId,journal:true}),receipt={...storedReceipt,revision};api.events.publish(auth.team_id,{type:'session_changed',cursor:String(revision),revision,session_id:sid,reason:'capture',read_required:true});send(res,200,result(api,auth,receipt),requestId);
287
+ const bundle=commit.native_bundle??null,legacyBundle=commit.artifacts.length?captureId:null;for(let index=0;index<commit.artifacts.length;index++){const a=commit.artifacts[index],file=bundle?.files.find(x=>x.upload_id===a.upload_id);statements.push({sql:'UPDATE uploads SET manifest_json=?,committed=1 WHERE team_id=? AND id=?',params:[JSON.stringify(a),auth.team_id,a.upload_id]},{sql:'INSERT INTO session_artifacts(team_id,session_id,revision,bundle_id,upload_id,format,provenance,file_role,fidelity,residue_json) VALUES (?,?,?,?,?,?,?,?,?,?)',params:[auth.team_id,sid,revisionParam,bundle?.id??legacyBundle,a.upload_id,bundle?.harness??cap.harness,'capture',file?.role??(index===0?'primary':'bundle'),bundle?.fidelity??'semantic',JSON.stringify(bundle?.residue??['native_bundle_metadata_unavailable'])]});}const storedReceipt={source_id:sourceId,capture_id:captureId,session_id:sid,generation:cap.generation,sequence:cap.sequence,digest:commit.digest,revision:'__JOURNAL_REVISION__',duplicate:false};statements.push({sql:"UPDATE captures SET state='committed',commit_json=?,receipt_json=? WHERE team_id=? AND id=?",params:[JSON.stringify(commit),{$journalJsonValue:storedReceipt},auth.team_id,captureId]},{sql:'DELETE FROM capture_parts WHERE team_id=? AND capture_id=?',params:[auth.team_id,captureId]},{sql:'UPDATE sources SET last_received_at=? WHERE team_id=? AND id=?',params:[boundaryNow,auth.team_id,sourceId]},{sql:'UPDATE teams SET journal_revision=? WHERE id=?',params:[revisionParam,auth.team_id]},{sql:'INSERT INTO journal(team_id,revision,session_id,kind,created_at) VALUES (?,?,?,?,?)',params:[auth.team_id,revisionParam,sid,'capture',boundaryNow]});const guards=[{sql:'SELECT 1 ok FROM credentials WHERE id=? AND source_id=? AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at>?)',params:[auth.credential_id,sourceId,boundaryNow],code:'authentication_required',status:401,message:'source credential expired or revoked'},...lineageGuards,{sql:"SELECT 1 ok FROM captures c JOIN sources s ON s.team_id=c.team_id AND s.id=c.source_id LEFT JOIN machines m ON m.team_id=s.team_id AND m.id=s.machine_id LEFT JOIN sync_rules r ON r.team_id=c.team_id AND r.id=json_extract(c.begin_json,'$.rule_id') WHERE c.team_id=? AND c.id=? AND c.source_id=? AND c.state='staging' AND s.state='active' AND (s.kind!='connector' OR (m.revoked_at IS NULL AND r.machine_id=m.id AND r.source_id=s.id AND r.state='active' AND r.revision=json_extract(c.begin_json,'$.rule_revision') AND r.visibility=json_extract(c.begin_json,'$.visibility')))",params:[auth.team_id,captureId,sourceId],code:'conflict',status:409,message:'capture authorization changed'}],storedResponse=result(api,auth,storedReceipt),{revision}=await guardedMutation(api,res,auth,guards,statements,{status:200,body:storedResponse,targetId:captureId,journal:true}),receipt={...storedReceipt,revision};api.events.publish(auth.team_id,{type:'session_changed',cursor:String(revision),revision,session_id:sid,reason:'capture',read_required:true});send(res,200,result(api,auth,receipt),requestId);
288
288
  }
289
289
 
290
290
  /**
package/worker/index.mjs CHANGED
@@ -117,8 +117,9 @@ export class TeamsServer extends DurableObject {
117
117
 
118
118
  async alarm() {
119
119
  await this.ready;
120
- if (this.api.serverId) await maintain(this.api).catch(() => {});
121
- await this.ctx.storage.setAlarm(Date.now() + MAINTAIN_MS);
120
+ const done = this.api.serverId ? await maintain(this.api).catch(() => null) : null;
121
+ // A backlog (versions or parts left over) is worked again in seconds, not a minute.
122
+ await this.ctx.storage.setAlarm(Date.now() + (done?.backlog ? 5_000 : MAINTAIN_MS));
122
123
  }
123
124
  }
124
125