@volter/supercode-teams 0.2.54 → 0.2.55

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.
Files changed (2) hide show
  1. package/machine.mjs +17 -14
  2. package/package.json +1 -1
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.55",
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",