@volter/supercode-teams 0.3.114 → 0.3.116

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
@@ -126,6 +126,13 @@ export function claudeUsageLimitWait(screen) {
126
126
  /** The settle floor for a pane whose kind has no composer pattern: the screen
127
127
  * must have stopped changing for this long before the pane counts as ready. */
128
128
  const INITIAL_INPUT_SETTLE_MS = 1_000;
129
+ /** A Windows Codex's typed first input waits, before pasting, for an empty composer on a screen unchanged this long
130
+ * (CODEX_PASTE_SETTLE_DEADLINE_MS at most); gives the paste CODEX_PASTE_SHOWN_MS to show, and watches
131
+ * CODEX_PASTE_LATE_MS more to clear one shown after that. */
132
+ const CODEX_PASTE_SETTLE_MS = 1_500;
133
+ const CODEX_PASTE_SETTLE_DEADLINE_MS = 30_000;
134
+ const CODEX_PASTE_SHOWN_MS = 5_000;
135
+ const CODEX_PASTE_LATE_MS = 15_000;
129
136
 
130
137
  /**
131
138
  * Claude asks whether to trust a folder it has not opened before, whatever its permission mode, and has no flag that
@@ -500,6 +507,17 @@ export class TeamsMachine extends MachineCore {
500
507
  // Set by a service-run connector: how `machine.restart` ends this daemon so its service manager starts it again.
501
508
  this.restartHandler = null;
502
509
  this.launches = new Launches(this.p.launches);
510
+ // The release this daemon runs, in its machine record (`supercode teams machine status`): its Teams package and its
511
+ // native. A daemon an upgrade did not restart runs the release it started with, whatever is installed, and a
512
+ // client a release behind or ahead of it is refused, so status says which it is (2026-10-06: version skew between
513
+ // daemons and their installs behind much of the day's trouble).
514
+ this.once('listening', () => {
515
+ try {
516
+ const record = readJson(this.p.machine, null);
517
+ if (record?.pid !== process.pid) return;
518
+ writeJson(this.p.machine, { ...record, release: this.#release() });
519
+ } catch { /* its record says no release */ }
520
+ });
503
521
  // the panes' stable porthole hook and the machine's stable supercode brought up to this release's at start, under
504
522
  // every pane already running
505
523
  this.#portholeHook();
@@ -1805,6 +1823,17 @@ export class TeamsMachine extends MachineCore {
1805
1823
  return this.stableSupercodePath ?? this.supercodeBin;
1806
1824
  }
1807
1825
 
1826
+ /** This daemon's release: its Teams package's version, and its native's (`--version`), as it started. */
1827
+ #release() {
1828
+ let teams = null, native = null;
1829
+ try { teams = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version ?? null; } catch { /* unread */ }
1830
+ try {
1831
+ const answer = spawnSync(this.supercodeBin, ['--version'], { env: this.env, encoding: 'utf8', timeout: 5000, stdio: ['ignore', 'pipe', 'ignore'] });
1832
+ native = answer.status === 0 ? answer.stdout.trim() || null : null;
1833
+ } catch { /* unread */ }
1834
+ return { teams, native };
1835
+ }
1836
+
1808
1837
  #portholeHook() {
1809
1838
  if (this.portholeHookPath) return this.portholeHookPath;
1810
1839
  const stable = join(supercodeHome(this.env), 'porthole-hook.cjs');
@@ -2367,18 +2396,47 @@ export class TeamsMachine extends MachineCore {
2367
2396
  const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2368
2397
  // Styled, so the composer's dim placeholder ("Ask Codex to do anything") reads as empty.
2369
2398
  const composer = async () => terminal.composerText?.('codex', await this.host.capture(session.id, { preserveEscapes: true })) ?? null;
2370
- if (await composer() !== '') return { delivered: false, reason: 'composer-not-empty' };
2399
+ // A composer that reads ready can still be mid-layout: a fresh Codex redraws its screen for about a second more
2400
+ // (measured on volter-desktop: ready from the first frame, redrawn at ~1.1 s and ~1.3 s), and a paste sent into
2401
+ // that is shown late or not at all. Paste only into an empty composer on a screen that has stopped changing.
2402
+ let settled = false;
2403
+ for (const until = Date.now() + CODEX_PASTE_SETTLE_DEADLINE_MS; !settled && !signal.aborted && Date.now() < until;) {
2404
+ const capture = await this.host.capture(session.id, { preserveEscapes: true });
2405
+ settled = terminal.composerText?.('codex', capture) === '' &&
2406
+ this.#screenUnchangedMs(session.id, capture, Date.now()) >= CODEX_PASTE_SETTLE_MS;
2407
+ if (!settled) await pause(150);
2408
+ }
2409
+ if (signal.aborted) return { delivered: false, reason: 'aborted' };
2410
+ if (!settled) return { delivered: false, reason: await composer() === '' ? 'screen-unsettled' : 'composer-not-empty' };
2371
2411
  for (const paste of codexWindowsPastes(body)) {
2372
2412
  if (signal.aborted) return { delivered: false, reason: 'aborted' };
2373
2413
  await this.host.sendInput(session.id, paste, { submitMode: 'none' });
2374
2414
  await pause(300);
2375
2415
  }
2376
2416
  let held = false;
2377
- for (let attempt = 0; attempt < 20 && !held && !signal.aborted; attempt += 1) {
2417
+ for (const until = Date.now() + CODEX_PASTE_SHOWN_MS; !held && !signal.aborted && Date.now() < until;) {
2378
2418
  held = Boolean(await composer());
2379
2419
  if (!held) await pause(150);
2380
2420
  }
2381
- if (!held) return { delivered: false, reason: 'paste-unconfirmed' };
2421
+ if (!held) {
2422
+ // A paste shown after this gave up would sit in the composer unsent: watch a while longer and clear it, so the
2423
+ // pane is left as it was found (the launch records the input as undelivered either way).
2424
+ for (const until = Date.now() + CODEX_PASTE_LATE_MS; !signal.aborted && Date.now() < until;) {
2425
+ const shown = await composer();
2426
+ if (!shown) { await pause(250); continue; }
2427
+ // Only this launch's paste is cleared: Codex shows a large paste as its placeholder, a small one as its
2428
+ // text. Anything else (a person typing meanwhile) is theirs and stays.
2429
+ // Compared with whitespace removed on both sides: the composer's text joins its screen rows with line breaks,
2430
+ // so a first line wider than one row never matches as it was sent.
2431
+ const squash = (value) => value.replace(/\s+/gu, '');
2432
+ const firstLine = squash(body.split('\n')[0]);
2433
+ if (/^\[Pasted Content \d+ chars\]/u.test(shown) || (firstLine && squash(shown).startsWith(firstLine))) {
2434
+ await this.#clearCodexComposer(session, composer, pause);
2435
+ }
2436
+ break;
2437
+ }
2438
+ return { delivered: false, reason: 'paste-unconfirmed' };
2439
+ }
2382
2440
  await this.host.sendInput(session.id, '\r', { submitMode: 'single' });
2383
2441
  return { delivered: true };
2384
2442
  } catch (error) {
@@ -2388,6 +2446,17 @@ export class TeamsMachine extends MachineCore {
2388
2446
  }
2389
2447
  }
2390
2448
 
2449
+ /** Empty a Windows Codex composer this launch pasted into: line clear, then deletes until it reads empty (a paste
2450
+ * placeholder goes with one delete). Never Enter, Escape or Ctrl-C. */
2451
+ async #clearCodexComposer(session, composer, pause) {
2452
+ await this.host.sendInput(session.id, '\u0015', { submitMode: 'none' });
2453
+ await pause(300);
2454
+ for (let round = 0; round < 40 && await composer(); round += 1) {
2455
+ await this.host.sendInput(session.id, '\u007f'.repeat(200), { submitMode: 'none' });
2456
+ await pause(200);
2457
+ }
2458
+ }
2459
+
2391
2460
  async #deliverPaneText(session, contextKey, text, options = {}) {
2392
2461
  const kind = this.#paneKind(contextKey, session);
2393
2462
  if (kind === 'codex' && codexUpdatePrompt(await this.host.capture(session.id, { lines: 120 }))) return { delivered: false, reason: 'codex-update-prompt: delivery refused; restart Codex with check_for_update_on_startup=false' };
package/mail/relay.mjs CHANGED
@@ -283,11 +283,13 @@ export class Relays {
283
283
  }
284
284
  // why it is not reused, said in the timing line: a relay that should have been warm and was not shows here
285
285
  step(`runtime not reused (${stale ? 'it held a stale turn' : !runtime ? 'no live receipt' : !alive(this.#pid(spec, runtime)) ? 'its process ended' : !record.connection ? 'no connection' : 'endpoint moved'})`);
286
- // closed through the owner's door even after its receipt went. One whose receipt is gone, or whose process
287
- // ended, is only serve's record of it: let go without holding this send (yuerans, 2026-10-06: 2.3-18 s of every
288
- // send under load); one still running is closed before its successor starts, as before
286
+ // closed through the owner's door even after its receipt went. One whose receipt is gone, or whose process is
287
+ // known and ended, is only serve's record of it: let go without holding this send (yuerans, 2026-10-06: 2.3-18 s
288
+ // of every send under load). One that may still run, its process unknown (no child pid, a record of another
289
+ // runtime) or alive, is closed before its successor starts in the same folder, as before.
289
290
  const close = this.serve('harness.v1.runtimes.close', { connection: record.connection, runtime_id: record.runtime_id }, CLOSE_MS);
290
- if (runtime && alive(this.#pid(spec, runtime))) {
291
+ const pid = runtime ? this.#pid(spec, runtime) : null;
292
+ if (runtime && !(Number.isInteger(pid) && !alive(pid))) {
291
293
  try { await close; }
292
294
  catch (error) { if (this.runtimeOf('claude-code', record.runtime_id)) throw new Error(`could not retire the old relay: ${error.message}`); }
293
295
  step('old runtime closed (runtimes.close)');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.3.114",
3
+ "version": "0.3.116",
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",