@volter/supercode-teams 0.3.10 → 0.3.12

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/bin/teams.mjs CHANGED
@@ -52,7 +52,7 @@ import { NODE_PROTOCOL } from '@volter/teams/channel';
52
52
 
53
53
  const MACHINE_VERBS = new Set(['machine', 'slow', 'describe', 'panes', 'input', 'suspend', 'resume', 'files', 'ports', 'port', 'browser', 'stream', 'jobs', 'workspace', 'title', 'attention', 'watch', 'rpc', 'launches', 'log', 'mail', 'health']);
54
54
  // Every flag a machine verb reads; any other is refused rather than silently ignored.
55
- const KNOWN_FLAGS = new Set(['home', 'since', 'machine', 'context', 'floor', 'supercode', 'tmux-socket', 'verbose', 'lines', 'input', 'name', 'for', 'urgency', 'reason', 'interrupt', 'query', 'out', 'in', 'interval', 'no-enter', 'when-composer-empty', 'key', 'listen', 'list', 'from', 'to', 'path', 'to-path', 'repo', 'branch', 'solo', 'card', 'create', 'presence-only', 'clear-draft', 'message-ids', 'json', 'all']);
55
+ const KNOWN_FLAGS = new Set(['home', 'since', 'machine', 'context', 'floor', 'supercode', 'tmux-socket', 'verbose', 'lines', 'input', 'name', 'for', 'urgency', 'reason', 'interrupt', 'query', 'out', 'in', 'interval', 'no-enter', 'when-composer-empty', 'key', 'listen', 'list', 'from', 'to', 'path', 'to-path', 'repo', 'branch', 'solo', 'card', 'create', 'presence-only', 'processes', 'panes', 'clear-draft', 'message-ids', 'json', 'all']);
56
56
 
57
57
  const argv = process.argv.slice(2);
58
58
  const flags = {};
@@ -218,7 +218,11 @@ async function main() {
218
218
  case 'describe':
219
219
  return withNode(async (rpc) => out(await rpc.call('describe')));
220
220
  case 'panes': {
221
- if (sub === 'ls') return withNode(async (rpc) => out(await rpc.call(flags['presence-only'] ? 'panes.present' : 'panes.list')));
221
+ // `--processes`: each pane and its root pid (no screens read); `--presence-only [--panes a,b]`: whether each pane's
222
+ // composer is idle, for the named panes only when named.
223
+ if (sub === 'ls') return withNode(async (rpc) => out(await rpc.call(
224
+ flags.processes ? 'panes.processes' : flags['presence-only'] ? 'panes.present' : 'panes.list',
225
+ flags['presence-only'] && typeof flags.panes === 'string' ? { panes: flags.panes.split(',').filter(Boolean) } : {})));
222
226
  if (sub === 'capture') return withNode(async (rpc) => out((await rpc.call('panes.capture', { pane: rest[0], lines: Number(flags.lines ?? 200) })).text));
223
227
  if (sub === 'close') return withNode(async (rpc) => out(await rpc.call('panes.close', { pane: rest[0] })));
224
228
  die('panes ls|capture|close');
package/machine.mjs CHANGED
@@ -521,10 +521,12 @@ export class TeamsMachine extends MachineCore {
521
521
  async #checkEnds() {
522
522
  const pending = this.#pendingNotices();
523
523
  if (!pending.length) { clearInterval(this.endWatch); this.endWatch = null; return; }
524
+ // One pane listing for every pending notice, not one per notice.
525
+ const panes = await this.host.listSessions();
524
526
  for (const ask of pending) {
525
527
  // A pane has ended when it is gone, or when its program has exited and the host keeps it for its last screen
526
528
  // (a ConPTY pane whose program failed).
527
- const session = await this.#paneByContext(ask.pane);
529
+ const session = panes.find((s) => s.contextKey === ask.pane) ?? null;
528
530
  if (session && !session.dead) continue;
529
531
  appendFileSync(this.#noticesPath(), JSON.stringify({ t: Date.now(), pane: ask.pane, told: true }) + '\n', { mode: 0o600 });
530
532
  const exit = session?.dead ? deadPaneExit(session) : paneExit(ask.pane);
@@ -1725,7 +1727,9 @@ export class TeamsMachine extends MachineCore {
1725
1727
  // No harness callback: discovery may be the harness's own caller.
1726
1728
  const whole = operator || holds(admitted, 'view', '*');
1727
1729
  if (!whole && !observesPanes(admitted)) need('view', '*');
1728
- const sessions = (await this.host.listSessions()).filter(s => s.contextKey && !s.dead && (whole || holds(admitted, 'observe', `pane:${s.contextKey}`)));
1730
+ // `panes`: only these are read (a caller that needs some panes' composers does not pay a capture of every pane).
1731
+ const asked = Array.isArray(params.panes) ? new Set(params.panes.map(String)) : null;
1732
+ const sessions = (await this.host.listSessions()).filter(s => s.contextKey && !s.dead && (!asked || asked.has(s.contextKey)) && (whole || holds(admitted, 'observe', `pane:${s.contextKey}`)));
1729
1733
  return Promise.all(sessions.map(async session => {
1730
1734
  const screen = await Promise.resolve().then(() => this.host.capture(session.id, { lines: 100, preserveEscapes: true })).catch(() => null);
1731
1735
  const kind = this.#paneKind(session.contextKey, session);
@@ -1733,6 +1737,15 @@ export class TeamsMachine extends MachineCore {
1733
1737
  return { pane: session.contextKey, idleComposer: composer != null && !/\besc to (?:interrupt|cancel)\b/iu.test(screen) };
1734
1738
  }));
1735
1739
  }
1740
+ case 'panes.processes': {
1741
+ // Each pane and its root process, from the pane listing alone: what a caller that maps processes to panes
1742
+ // needs, without panes.list's screen capture of every pane (message list asked for that, t_23e47b73).
1743
+ const whole = operator || holds(admitted, 'view', '*');
1744
+ if (!whole && !observesPanes(admitted)) need('view', '*');
1745
+ return (await this.host.listSessions())
1746
+ .filter(s => s.contextKey && !s.dead && (whole || holds(admitted, 'observe', `pane:${s.contextKey}`)))
1747
+ .map(s => ({ pane: s.contextKey, pid: s.rootPid ?? null }));
1748
+ }
1736
1749
  case 'harness.v1.panes.list':
1737
1750
  case 'panes.list': {
1738
1751
  // A participant lists the panes its grants let it observe; view on the whole
@@ -1876,9 +1889,15 @@ export class TeamsMachine extends MachineCore {
1876
1889
  // Everything this daemon launched, by key, with whether the pane still lives: how an
1877
1890
  // operator tells its own launches from a person's panes without reading labels.
1878
1891
  if (!operator) throw Object.assign(new Error('operators only'), { code: 403 });
1892
+ // One pane listing for the whole journal: one per launch (thousands of them) ran past the door's 30 s bound,
1893
+ // the slowest door the slow log named on yuerans-macbook-pro (t_23e47b73).
1894
+ // The journal first, then the panes: a launch that becomes ready while the panes are listed is in a listing
1895
+ // taken after it, so it never reads as not live.
1879
1896
  const rows = [];
1880
- for (const found of this.launches.all()) {
1881
- const session = found.pane ? await this.#paneByContext(found.pane) : null;
1897
+ const launched = this.launches.all();
1898
+ const panes = await this.host.listSessions();
1899
+ for (const found of launched) {
1900
+ const session = found.pane ? panes.find((s) => s.contextKey === found.pane) ?? null : null;
1882
1901
  rows.push({ key: found.key, pane: found.pane ?? null, kind: found.kind ?? null, by: found.by ?? null, status: found.status ?? null, at: found.t, live: Boolean(session && !session.dead), ...initialInputFacts(found) });
1883
1902
  }
1884
1903
  return rows;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.3.10",
3
+ "version": "0.3.12",
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",