@volter/supercode-teams 0.2.105 → 0.2.106

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 +48 -34
  2. package/package.json +1 -1
package/machine.mjs CHANGED
@@ -187,6 +187,7 @@ export class TeamsMachine extends MachineCore {
187
187
  //. The launch reply does not wait for delivery — a cold TUI can take
188
188
  // longer than the caller's own RPC timeout — so the wait lives here, where
189
189
  // `stop()` can abandon it rather than leave a timer running past the daemon.
190
+ this.inputInFlight = new Set(); // one input transaction per pane
190
191
  this.pendingInitialInputs = new Map(); // launch key → { abort, task }
191
192
  this.registerDoor('harness', (entry, stream, accept, refuse, index) => this.#serveHarness(index, entry, stream, accept, refuse));
192
193
  this.registerDoor('terminal', (entry, stream, accept, refuse) => this.#attachTerminal(entry, stream, accept, refuse));
@@ -752,30 +753,16 @@ export class TeamsMachine extends MachineCore {
752
753
  const task = (async () => {
753
754
  const readyBy = await this.#awaitInputReady(session, contextKey, kind, abort.signal);
754
755
  if (readyBy === 'aborted') return;
755
- if (readyBy === 'pane-gone') {
756
+ if (readyBy === 'pane-gone' || readyBy === 'deadline') {
756
757
  this.launches.record({ key, pane: contextKey, kind, by, status: 'ready', inputDelivered: false, inputReadyBy: readyBy });
757
- act('launch', `pane:${contextKey}`, 'failed', 'first input undelivered: the pane ended before it was ready');
758
+ act('launch', `pane:${contextKey}`, 'failed', `first input undelivered: ${readyBy}`);
758
759
  return;
759
760
  }
760
- // `single` stays `single`: one paste, one submit. The repeated Enter of
761
- // `retry` exists for a composer that swallowed the first one, and pressing
762
- // Enter again at an agent that already took the message sends an empty
763
- // turn. The fix for a lost submit is waiting until there is something to
764
- // submit into, which is what happened above.
765
- try {
766
- await this.host.sendInput(session.id, submission(input), { submitMode: 'single' });
767
- } catch (error) {
768
- this.launches.record({ key, pane: contextKey, kind, by, status: 'ready', inputDelivered: false, inputReadyBy: readyBy, inputError: error.message });
769
- act('launch', `pane:${contextKey}`, 'failed', `first input undelivered: ${error.code ?? error.message}`);
770
- return;
771
- }
772
- // `inputDeliveredLate` is the deadline's own confession: the pane never
773
- // showed us it was ready, and the message went in on a screen nobody
774
- // vouched for. A launch that reads `ready` without it was given its
775
- // message by an input box that existed.
776
- const late = readyBy === 'deadline';
777
- this.launches.record({ key, pane: contextKey, kind, by, status: 'ready', inputDelivered: true, inputReadyBy: readyBy, ...(late ? { inputDeliveredLate: true } : {}) });
778
- act('launch', `pane:${contextKey}`, 'ok', `first input delivered (${readyBy})`);
761
+ const result = await this.#deliverPaneText(session, contextKey, input, { signal: abort.signal });
762
+ this.launches.record({ key, pane: contextKey, kind, by, status: 'ready', inputDelivered: result.delivered,
763
+ inputReadyBy: readyBy, ...(!result.delivered ? { inputError: result.reason } : {}) });
764
+ act('launch', `pane:${contextKey}`, result.delivered ? 'ok' : 'failed',
765
+ result.delivered ? `first input delivered (${readyBy})` : `first input unconfirmed: ${result.reason}`);
779
766
  })().catch((error) => {
780
767
  // Nothing above is expected to throw; if something does, the launch record
781
768
  // is where it is answerable, not a crashed daemon.
@@ -786,6 +773,24 @@ export class TeamsMachine extends MachineCore {
786
773
  this.pendingInitialInputs.set(key, { abort, task });
787
774
  }
788
775
 
776
+ async #deliverPaneText(session, contextKey, text, options = {}) {
777
+ if (this.inputInFlight.has(session.id)) return { delivered: false, reason: 'input-in-flight' };
778
+ this.inputInFlight.add(session.id);
779
+ try {
780
+ const kind = this.#paneKind(contextKey, session);
781
+ if (kind === 'claude-code' || kind === 'codex') {
782
+ if (typeof terminal.deliverComposerInput !== 'function') return { delivered: false, reason: 'terminal-package-too-old' };
783
+ return await terminal.deliverComposerInput(this.host, session.id, kind, submission(text, false), options);
784
+ }
785
+ const delivery = await this.host.sendInput(session.id, submission(text), { ...options, submitMode: 'single' });
786
+ return { delivered: true, delivery };
787
+ } catch (error) {
788
+ return { delivered: false, reason: error?.message ?? String(error) };
789
+ } finally {
790
+ this.inputInFlight.delete(session.id);
791
+ }
792
+ }
793
+
789
794
  /** The program a pane runs, as the terminal package names kinds: what it was
790
795
  * launched as, else what its foreground command is. */
791
796
  #paneKind(contextKey, session) {
@@ -813,13 +818,13 @@ export class TeamsMachine extends MachineCore {
813
818
  * the foreground command (`awaiting_me`'s evidence: a prompt,
814
819
  * turn over).
815
820
  * deadline neither, for `INITIAL_INPUT_READY_DEADLINE_MS`. The caller
816
- * delivers anyway and says so.
821
+ * leaves input undelivered without typing into that screen.
817
822
  */
818
823
  async #awaitInputReady(session, contextKey, kind, signal) {
819
- const composerKind = typeof kind === 'string' ? kind.trim().toLowerCase() : '';
824
+ const composerKind = this.#paneKind(contextKey, session);
820
825
  const until = Date.now() + INITIAL_INPUT_READY_DEADLINE_MS;
821
826
  while (!signal.aborted && Date.now() < until) {
822
- const capture = await this.#safeCapture(session.id);
827
+ const capture = await Promise.resolve().then(() => this.host.capture(session.id, { preserveEscapes: true })).catch(() => '');
823
828
  const unchangedMs = this.#screenUnchangedMs(session.id, capture, Date.now());
824
829
  const blank = capture.trim() === '';
825
830
  // A blank screen is the one reading that can mean the pane is GONE rather
@@ -1305,13 +1310,22 @@ export class TeamsMachine extends MachineCore {
1305
1310
  const session = await this.#paneByContext(params.pane);
1306
1311
  if (!session) throw Object.assign(new Error(`no pane with context ${params.pane}`), { code: 404 });
1307
1312
  if (this.suspended.has(params.pane)) throw Object.assign(new Error(`pane ${params.pane} is suspended: remote input is refused until resume`), { code: 423 });
1308
- // `deadlineMs` is how long the caller will wait (default 10 s, which covers `retry`'s 5.5 s of
1309
- // submit presses; at most 30 s); the terminal host takes an absolute deadline.
1310
- // `keys`: the bytes of named keys, delivered as they are (a trailing Enter is a key, not a line ending).
1311
- const data = params.keys ? String(params.text ?? '') : submission(params.text, params.submit !== false);
1312
- const delivery = await this.host.sendInput(session.id, data, { submitMode: params.submit === false || params.keys ? 'single' : 'retry', deadlineMs: Date.now() + Math.min(Number(params.deadlineMs) || 10_000, 30_000) });
1313
- act('control', `pane:${params.pane}`, 'input');
1314
- return { delivered: true, delivery };
1313
+ // Named keys and no-submit input are raw terminal writes. A submitted
1314
+ // Claude/Codex turn needs evidence from the composer before claiming delivery.
1315
+ const deadlineMs = Date.now() + Math.min(Number(params.deadlineMs) || 10_000, 30_000);
1316
+ if (this.inputInFlight.has(session.id)) return { delivered: false, reason: 'input-in-flight' };
1317
+ let result;
1318
+ if (params.keys || params.submit === false) {
1319
+ this.inputInFlight.add(session.id);
1320
+ try {
1321
+ const delivery = await this.host.sendInput(session.id, params.keys ? String(params.text ?? '') : submission(params.text, false), { submitMode: 'single', deadlineMs });
1322
+ result = { delivered: true, delivery };
1323
+ } finally { this.inputInFlight.delete(session.id); }
1324
+ } else {
1325
+ result = await this.#deliverPaneText(session, params.pane, params.text, { deadlineMs });
1326
+ }
1327
+ act('control', `pane:${params.pane}`, result.delivered ? 'input' : 'failed', result.reason);
1328
+ return result;
1315
1329
  }
1316
1330
  case 'panes.turn': {
1317
1331
  // A user's own turn, as its own submitted turn: typed only into an empty
@@ -1332,9 +1346,9 @@ export class TeamsMachine extends MachineCore {
1332
1346
  if (first !== false || second !== false) {
1333
1347
  return { delivered: false, reason: first === true || second === true ? 'draft' : 'no-composer' };
1334
1348
  }
1335
- const delivery = await this.host.sendInput(session.id, submission(params.text, true), { submitMode: 'single', deadlineMs: Date.now() + 10_000 });
1336
- act('control', `pane:${params.pane}`, 'input');
1337
- return { delivered: true, delivery };
1349
+ const result = await this.#deliverPaneText(session, params.pane, params.text, { deadlineMs: Date.now() + 10_000 });
1350
+ act('control', `pane:${params.pane}`, result.delivered ? 'input' : 'failed', result.reason);
1351
+ return result;
1338
1352
  }
1339
1353
  case 'panes.capture': {
1340
1354
  need('observe', `pane:${params.pane}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.2.105",
3
+ "version": "0.2.106",
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",