@volter/supercode-teams 0.2.104 → 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.
- package/machine.mjs +53 -35
- 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));
|
|
@@ -596,7 +597,11 @@ export class TeamsMachine extends MachineCore {
|
|
|
596
597
|
let out = '';
|
|
597
598
|
let child;
|
|
598
599
|
try {
|
|
599
|
-
|
|
600
|
+
// The descriptor's number, not the socket: a stream handed to spawn stops being read here, and the
|
|
601
|
+
// connection's next request would never be answered. The child gets its own duplicate of the descriptor.
|
|
602
|
+
const fd = entry.socket._handle?.fd;
|
|
603
|
+
if (!Number.isInteger(fd) || fd < 0) { resolve({ unsupported: true }); return; }
|
|
604
|
+
child = spawn(this.supercodeBin, ['teams', 'caller', '--peer-fd', '3'], { stdio: ['ignore', 'pipe', 'ignore', fd], env: this.env, windowsHide: true });
|
|
600
605
|
} catch { resolve({ unknown: true }); return; }
|
|
601
606
|
child.stdout.on('data', (chunk) => { out += chunk; });
|
|
602
607
|
child.on('error', () => resolve({ unknown: true }));
|
|
@@ -748,30 +753,16 @@ export class TeamsMachine extends MachineCore {
|
|
|
748
753
|
const task = (async () => {
|
|
749
754
|
const readyBy = await this.#awaitInputReady(session, contextKey, kind, abort.signal);
|
|
750
755
|
if (readyBy === 'aborted') return;
|
|
751
|
-
if (readyBy === 'pane-gone') {
|
|
756
|
+
if (readyBy === 'pane-gone' || readyBy === 'deadline') {
|
|
752
757
|
this.launches.record({ key, pane: contextKey, kind, by, status: 'ready', inputDelivered: false, inputReadyBy: readyBy });
|
|
753
|
-
act('launch', `pane:${contextKey}`, 'failed',
|
|
758
|
+
act('launch', `pane:${contextKey}`, 'failed', `first input undelivered: ${readyBy}`);
|
|
754
759
|
return;
|
|
755
760
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
try {
|
|
762
|
-
await this.host.sendInput(session.id, submission(input), { submitMode: 'single' });
|
|
763
|
-
} catch (error) {
|
|
764
|
-
this.launches.record({ key, pane: contextKey, kind, by, status: 'ready', inputDelivered: false, inputReadyBy: readyBy, inputError: error.message });
|
|
765
|
-
act('launch', `pane:${contextKey}`, 'failed', `first input undelivered: ${error.code ?? error.message}`);
|
|
766
|
-
return;
|
|
767
|
-
}
|
|
768
|
-
// `inputDeliveredLate` is the deadline's own confession: the pane never
|
|
769
|
-
// showed us it was ready, and the message went in on a screen nobody
|
|
770
|
-
// vouched for. A launch that reads `ready` without it was given its
|
|
771
|
-
// message by an input box that existed.
|
|
772
|
-
const late = readyBy === 'deadline';
|
|
773
|
-
this.launches.record({ key, pane: contextKey, kind, by, status: 'ready', inputDelivered: true, inputReadyBy: readyBy, ...(late ? { inputDeliveredLate: true } : {}) });
|
|
774
|
-
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}`);
|
|
775
766
|
})().catch((error) => {
|
|
776
767
|
// Nothing above is expected to throw; if something does, the launch record
|
|
777
768
|
// is where it is answerable, not a crashed daemon.
|
|
@@ -782,6 +773,24 @@ export class TeamsMachine extends MachineCore {
|
|
|
782
773
|
this.pendingInitialInputs.set(key, { abort, task });
|
|
783
774
|
}
|
|
784
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
|
+
|
|
785
794
|
/** The program a pane runs, as the terminal package names kinds: what it was
|
|
786
795
|
* launched as, else what its foreground command is. */
|
|
787
796
|
#paneKind(contextKey, session) {
|
|
@@ -809,13 +818,13 @@ export class TeamsMachine extends MachineCore {
|
|
|
809
818
|
* the foreground command (`awaiting_me`'s evidence: a prompt,
|
|
810
819
|
* turn over).
|
|
811
820
|
* deadline neither, for `INITIAL_INPUT_READY_DEADLINE_MS`. The caller
|
|
812
|
-
*
|
|
821
|
+
* leaves input undelivered without typing into that screen.
|
|
813
822
|
*/
|
|
814
823
|
async #awaitInputReady(session, contextKey, kind, signal) {
|
|
815
|
-
const composerKind =
|
|
824
|
+
const composerKind = this.#paneKind(contextKey, session);
|
|
816
825
|
const until = Date.now() + INITIAL_INPUT_READY_DEADLINE_MS;
|
|
817
826
|
while (!signal.aborted && Date.now() < until) {
|
|
818
|
-
const capture = await this
|
|
827
|
+
const capture = await Promise.resolve().then(() => this.host.capture(session.id, { preserveEscapes: true })).catch(() => '');
|
|
819
828
|
const unchangedMs = this.#screenUnchangedMs(session.id, capture, Date.now());
|
|
820
829
|
const blank = capture.trim() === '';
|
|
821
830
|
// A blank screen is the one reading that can mean the pane is GONE rather
|
|
@@ -1301,13 +1310,22 @@ export class TeamsMachine extends MachineCore {
|
|
|
1301
1310
|
const session = await this.#paneByContext(params.pane);
|
|
1302
1311
|
if (!session) throw Object.assign(new Error(`no pane with context ${params.pane}`), { code: 404 });
|
|
1303
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 });
|
|
1304
|
-
//
|
|
1305
|
-
//
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
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;
|
|
1311
1329
|
}
|
|
1312
1330
|
case 'panes.turn': {
|
|
1313
1331
|
// A user's own turn, as its own submitted turn: typed only into an empty
|
|
@@ -1328,9 +1346,9 @@ export class TeamsMachine extends MachineCore {
|
|
|
1328
1346
|
if (first !== false || second !== false) {
|
|
1329
1347
|
return { delivered: false, reason: first === true || second === true ? 'draft' : 'no-composer' };
|
|
1330
1348
|
}
|
|
1331
|
-
const
|
|
1332
|
-
act('control', `pane:${params.pane}`, 'input');
|
|
1333
|
-
return
|
|
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;
|
|
1334
1352
|
}
|
|
1335
1353
|
case 'panes.capture': {
|
|
1336
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.
|
|
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",
|