@volter/supercode-teams 0.2.105 → 0.2.107
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 -41
- package/package.json +1 -1
package/machine.mjs
CHANGED
|
@@ -51,12 +51,8 @@ import { JobOwner, executionCapabilities, streamProgram, serveWorkspace } from '
|
|
|
51
51
|
|
|
52
52
|
/** The preload every launched pane carries: it announces the ports a pane's node processes listen on. */
|
|
53
53
|
const PORTHOLE_HOOK = fileURLToPath(new URL('./porthole-hook.cjs', import.meta.url));
|
|
54
|
-
/** How long
|
|
55
|
-
*
|
|
56
|
-
* start anyone has reported (a first-run agent TUI fetching its config over a
|
|
57
|
-
* slow link); past it the input goes in regardless and the launch record says
|
|
58
|
-
* so, because silently dropping a caller's first message is worse than an
|
|
59
|
-
* imperfectly-timed one. */
|
|
54
|
+
/** How long launch input waits for a ready composer. At the deadline it stays
|
|
55
|
+
* undelivered; startup dialogs must never receive the queued message. */
|
|
60
56
|
const INITIAL_INPUT_READY_DEADLINE_MS = 90_000;
|
|
61
57
|
/** How often the readiness evidence is re-read while waiting. Same cadence the
|
|
62
58
|
* terminal controller polls its own composer at: fast enough that a ready pane
|
|
@@ -187,6 +183,7 @@ export class TeamsMachine extends MachineCore {
|
|
|
187
183
|
//. The launch reply does not wait for delivery — a cold TUI can take
|
|
188
184
|
// longer than the caller's own RPC timeout — so the wait lives here, where
|
|
189
185
|
// `stop()` can abandon it rather than leave a timer running past the daemon.
|
|
186
|
+
this.inputInFlight = new Set(); // one input transaction per pane
|
|
190
187
|
this.pendingInitialInputs = new Map(); // launch key → { abort, task }
|
|
191
188
|
this.registerDoor('harness', (entry, stream, accept, refuse, index) => this.#serveHarness(index, entry, stream, accept, refuse));
|
|
192
189
|
this.registerDoor('terminal', (entry, stream, accept, refuse) => this.#attachTerminal(entry, stream, accept, refuse));
|
|
@@ -752,30 +749,16 @@ export class TeamsMachine extends MachineCore {
|
|
|
752
749
|
const task = (async () => {
|
|
753
750
|
const readyBy = await this.#awaitInputReady(session, contextKey, kind, abort.signal);
|
|
754
751
|
if (readyBy === 'aborted') return;
|
|
755
|
-
if (readyBy === 'pane-gone') {
|
|
752
|
+
if (readyBy === 'pane-gone' || readyBy === 'deadline') {
|
|
756
753
|
this.launches.record({ key, pane: contextKey, kind, by, status: 'ready', inputDelivered: false, inputReadyBy: readyBy });
|
|
757
|
-
act('launch', `pane:${contextKey}`, 'failed',
|
|
754
|
+
act('launch', `pane:${contextKey}`, 'failed', `first input undelivered: ${readyBy}`);
|
|
758
755
|
return;
|
|
759
756
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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})`);
|
|
757
|
+
const result = await this.#deliverPaneText(session, contextKey, input, { signal: abort.signal });
|
|
758
|
+
this.launches.record({ key, pane: contextKey, kind, by, status: 'ready', inputDelivered: result.delivered,
|
|
759
|
+
inputReadyBy: readyBy, ...(!result.delivered ? { inputError: result.reason } : {}) });
|
|
760
|
+
act('launch', `pane:${contextKey}`, result.delivered ? 'ok' : 'failed',
|
|
761
|
+
result.delivered ? `first input delivered (${readyBy})` : `first input unconfirmed: ${result.reason}`);
|
|
779
762
|
})().catch((error) => {
|
|
780
763
|
// Nothing above is expected to throw; if something does, the launch record
|
|
781
764
|
// is where it is answerable, not a crashed daemon.
|
|
@@ -786,6 +769,24 @@ export class TeamsMachine extends MachineCore {
|
|
|
786
769
|
this.pendingInitialInputs.set(key, { abort, task });
|
|
787
770
|
}
|
|
788
771
|
|
|
772
|
+
async #deliverPaneText(session, contextKey, text, options = {}) {
|
|
773
|
+
if (this.inputInFlight.has(session.id)) return { delivered: false, reason: 'input-in-flight' };
|
|
774
|
+
this.inputInFlight.add(session.id);
|
|
775
|
+
try {
|
|
776
|
+
const kind = this.#paneKind(contextKey, session);
|
|
777
|
+
if (kind === 'claude-code' || kind === 'codex') {
|
|
778
|
+
if (typeof terminal.deliverComposerInput !== 'function') return { delivered: false, reason: 'terminal-package-too-old' };
|
|
779
|
+
return await terminal.deliverComposerInput(this.host, session.id, kind, submission(text, false), options);
|
|
780
|
+
}
|
|
781
|
+
const delivery = await this.host.sendInput(session.id, submission(text), { ...options, submitMode: 'single' });
|
|
782
|
+
return { delivered: true, delivery };
|
|
783
|
+
} catch (error) {
|
|
784
|
+
return { delivered: false, reason: error?.message ?? String(error) };
|
|
785
|
+
} finally {
|
|
786
|
+
this.inputInFlight.delete(session.id);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
|
|
789
790
|
/** The program a pane runs, as the terminal package names kinds: what it was
|
|
790
791
|
* launched as, else what its foreground command is. */
|
|
791
792
|
#paneKind(contextKey, session) {
|
|
@@ -813,13 +814,14 @@ export class TeamsMachine extends MachineCore {
|
|
|
813
814
|
* the foreground command (`awaiting_me`'s evidence: a prompt,
|
|
814
815
|
* turn over).
|
|
815
816
|
* deadline neither, for `INITIAL_INPUT_READY_DEADLINE_MS`. The caller
|
|
816
|
-
*
|
|
817
|
+
* leaves input undelivered without typing into that screen.
|
|
817
818
|
*/
|
|
818
819
|
async #awaitInputReady(session, contextKey, kind, signal) {
|
|
819
|
-
const composerKind =
|
|
820
|
+
const composerKind = this.#paneKind(contextKey, session);
|
|
820
821
|
const until = Date.now() + INITIAL_INPUT_READY_DEADLINE_MS;
|
|
822
|
+
let readySince = null;
|
|
821
823
|
while (!signal.aborted && Date.now() < until) {
|
|
822
|
-
const capture = await this
|
|
824
|
+
const capture = await Promise.resolve().then(() => this.host.capture(session.id, { preserveEscapes: true })).catch(() => '');
|
|
823
825
|
const unchangedMs = this.#screenUnchangedMs(session.id, capture, Date.now());
|
|
824
826
|
const blank = capture.trim() === '';
|
|
825
827
|
// A blank screen is the one reading that can mean the pane is GONE rather
|
|
@@ -832,7 +834,8 @@ export class TeamsMachine extends MachineCore {
|
|
|
832
834
|
// never "not yet", or an unrecognized kind would wait out the whole
|
|
833
835
|
// deadline on a screen nobody is reading.
|
|
834
836
|
const composer = composerReadyForInput(composerKind, capture);
|
|
835
|
-
|
|
837
|
+
readySince = composer === true ? (readySince ?? Date.now()) : null;
|
|
838
|
+
if (composer === true && Date.now() - readySince >= INITIAL_INPUT_SETTLE_MS) return 'composer';
|
|
836
839
|
if (composer === null && unchangedMs >= INITIAL_INPUT_SETTLE_MS && (!blank || livenessOf(live.activeCommand) === 'waiting')) return 'settled';
|
|
837
840
|
await new Promise((resolve) => setTimeout(resolve, INITIAL_INPUT_POLL_MS));
|
|
838
841
|
}
|
|
@@ -1305,13 +1308,22 @@ export class TeamsMachine extends MachineCore {
|
|
|
1305
1308
|
const session = await this.#paneByContext(params.pane);
|
|
1306
1309
|
if (!session) throw Object.assign(new Error(`no pane with context ${params.pane}`), { code: 404 });
|
|
1307
1310
|
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
|
-
//
|
|
1309
|
-
//
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1311
|
+
// Named keys and no-submit input are raw terminal writes. A submitted
|
|
1312
|
+
// Claude/Codex turn needs evidence from the composer before claiming delivery.
|
|
1313
|
+
const deadlineMs = Date.now() + Math.min(Number(params.deadlineMs) || 10_000, 30_000);
|
|
1314
|
+
if (this.inputInFlight.has(session.id)) return { delivered: false, reason: 'input-in-flight' };
|
|
1315
|
+
let result;
|
|
1316
|
+
if (params.keys || params.submit === false) {
|
|
1317
|
+
this.inputInFlight.add(session.id);
|
|
1318
|
+
try {
|
|
1319
|
+
const delivery = await this.host.sendInput(session.id, params.keys ? String(params.text ?? '') : submission(params.text, false), { submitMode: 'single', deadlineMs });
|
|
1320
|
+
result = { delivered: true, delivery };
|
|
1321
|
+
} finally { this.inputInFlight.delete(session.id); }
|
|
1322
|
+
} else {
|
|
1323
|
+
result = await this.#deliverPaneText(session, params.pane, params.text, { deadlineMs });
|
|
1324
|
+
}
|
|
1325
|
+
act('control', `pane:${params.pane}`, result.delivered ? 'input' : 'failed', result.reason);
|
|
1326
|
+
return result;
|
|
1315
1327
|
}
|
|
1316
1328
|
case 'panes.turn': {
|
|
1317
1329
|
// A user's own turn, as its own submitted turn: typed only into an empty
|
|
@@ -1332,9 +1344,9 @@ export class TeamsMachine extends MachineCore {
|
|
|
1332
1344
|
if (first !== false || second !== false) {
|
|
1333
1345
|
return { delivered: false, reason: first === true || second === true ? 'draft' : 'no-composer' };
|
|
1334
1346
|
}
|
|
1335
|
-
const
|
|
1336
|
-
act('control', `pane:${params.pane}`, 'input');
|
|
1337
|
-
return
|
|
1347
|
+
const result = await this.#deliverPaneText(session, params.pane, params.text, { deadlineMs: Date.now() + 10_000 });
|
|
1348
|
+
act('control', `pane:${params.pane}`, result.delivered ? 'input' : 'failed', result.reason);
|
|
1349
|
+
return result;
|
|
1338
1350
|
}
|
|
1339
1351
|
case 'panes.capture': {
|
|
1340
1352
|
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.107",
|
|
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",
|