@dmsdc-ai/aigentry-telepty 0.6.3 → 0.6.4
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/CHANGELOG.md +14 -0
- package/cli.js +15 -1
- package/daemon.js +27 -0
- package/package.json +5 -5
- package/src/submit-gate.js +80 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@dmsdc-ai/aigentry-telepty` are documented here.
|
|
4
4
|
|
|
5
|
+
## [0.6.4] - 2026-06-13
|
|
6
|
+
|
|
7
|
+
### Added — inject consumption-evidence: consumed | queued | unknown (#53)
|
|
8
|
+
|
|
9
|
+
- **`telepty inject` now distinguishes "delivered" from "consumed".** After the CR
|
|
10
|
+
(`pty_cr`), the daemon captures an output-ring watermark and `classifyInjectConsumption()`
|
|
11
|
+
/ `verifyBodyConsumed()` (reusing the #52 echo-watermark technique) classify the result:
|
|
12
|
+
**consumed** (composer cleared + new turn rendered), **queued** (injected text persists
|
|
13
|
+
in a busy TUI composer), or **unknown** (conservative). The `/submit` response and CLI
|
|
14
|
+
output now carry this status; a `queued` result on a busy orchestrator TUI prints a
|
|
15
|
+
pull-fallback hint. Closes the "`Submitted` reads as success but the busy recipient never
|
|
16
|
+
consumed it" gap (observed 3+ times in a single orchestration wave). Backward-compatible
|
|
17
|
+
(accepted/retryable semantics and exit codes unchanged; response is a superset).
|
|
18
|
+
|
|
5
19
|
## [0.6.3] - 2026-06-13
|
|
6
20
|
|
|
7
21
|
### ⚠️ BREAKING — daemon binds 127.0.0.1 by default (#50)
|
package/cli.js
CHANGED
|
@@ -2423,7 +2423,21 @@ async function main() {
|
|
|
2423
2423
|
: '';
|
|
2424
2424
|
const attemptsNote = submitData.attempts > 1 ? ` (${submitData.attempts} attempts)` : '';
|
|
2425
2425
|
const forcedNote = submitData.forced ? ' [forced]' : '';
|
|
2426
|
-
|
|
2426
|
+
const tail = `${attemptsNote}${gateNote}${lateNote}${forcedNote}`;
|
|
2427
|
+
// #53: distinguish CONSUMED-as-a-turn from QUEUED-in-a-busy-composer. A bare
|
|
2428
|
+
// "Submitted via pty_cr" only proves bytes reached the PTY; a busy recipient TUI
|
|
2429
|
+
// parks the text without firing a turn, so report that instead of a false success.
|
|
2430
|
+
const consumption = submitData.consumption
|
|
2431
|
+
|| (submitData.verify && submitData.verify.consumption) || null;
|
|
2432
|
+
if (consumption === 'queued') {
|
|
2433
|
+
console.log(`⚠️ Submitted via ${submitData.strategy}${tail}, but recipient is BUSY — text QUEUED, NOT consumed as a new turn. It will be processed after the current turn ends; if a reply is expected, fall back to pulling the recipient's state.`);
|
|
2434
|
+
} else if (consumption === 'consumed') {
|
|
2435
|
+
console.log(`✅ Submitted via ${submitData.strategy}${tail} — consumed as a new turn.`);
|
|
2436
|
+
} else if (consumption === 'unknown') {
|
|
2437
|
+
console.log(`✅ Submitted via ${submitData.strategy}${tail} (consumption=unknown — delivered to PTY; turn-consumption not observable).`);
|
|
2438
|
+
} else {
|
|
2439
|
+
console.log(`✅ Submitted via ${submitData.strategy}${tail}.`);
|
|
2440
|
+
}
|
|
2427
2441
|
} else if (submitRes && submitRes.status === 504) {
|
|
2428
2442
|
// Soft failure: REPL never readied. Orchestrator scripts depend on
|
|
2429
2443
|
// exit 0 here — surface a clear remediation hint but do not exit
|
package/daemon.js
CHANGED
|
@@ -2815,6 +2815,9 @@ app.post('/api/sessions/:id/submit', async (req, res) => {
|
|
|
2815
2815
|
const settleEnabled = req.body?.input_settle_gate !== false;
|
|
2816
2816
|
let strategy = await gatedTerminalSubmit(id, session, injectedBody, settleEnabled);
|
|
2817
2817
|
let submittedAtMs = Date.now();
|
|
2818
|
+
// #53: outputRing watermark at the CR — scopes consumption-evidence matching to frames
|
|
2819
|
+
// appended AFTER this submit (composer redraw / new-turn render), surviving ring trimming.
|
|
2820
|
+
let ringBytesAtSubmit = session.outputRingTotalBytes || 0;
|
|
2818
2821
|
let attempts = strategy ? 1 : 0;
|
|
2819
2822
|
if (!strategy) {
|
|
2820
2823
|
if (injectedBody) {
|
|
@@ -2836,12 +2839,15 @@ app.post('/api/sessions/:id/submit', async (req, res) => {
|
|
|
2836
2839
|
// shot is enough. A retry is idempotent only when the body is still visible.
|
|
2837
2840
|
let verify = null;
|
|
2838
2841
|
let confirm = null;
|
|
2842
|
+
let consumption = null; // #53: 'consumed' | 'queued' | 'unknown'
|
|
2843
|
+
let consumptionReason = null;
|
|
2839
2844
|
if (injectedBody && injectedBody.length > 0) {
|
|
2840
2845
|
confirm = await confirmSubmitAfterDispatch(id, session, injectedBody, submittedAtMs, verifyTimeoutMs);
|
|
2841
2846
|
while (confirm && !confirm.accepted && confirm.retryable && attempts <= retries) {
|
|
2842
2847
|
await new Promise(resolve => setTimeout(resolve, retryDelayMs));
|
|
2843
2848
|
const retryStrategy = await gatedTerminalSubmit(id, session, injectedBody, settleEnabled);
|
|
2844
2849
|
submittedAtMs = Date.now();
|
|
2850
|
+
ringBytesAtSubmit = session.outputRingTotalBytes || 0;
|
|
2845
2851
|
if (!retryStrategy) break;
|
|
2846
2852
|
strategy = retryStrategy;
|
|
2847
2853
|
attempts++;
|
|
@@ -2849,6 +2855,25 @@ app.post('/api/sessions/:id/submit', async (req, res) => {
|
|
|
2849
2855
|
}
|
|
2850
2856
|
verify = buildSubmitVerify(confirm);
|
|
2851
2857
|
|
|
2858
|
+
// #53: consumption-evidence on the DELIVERY path. `confirm.accepted` can read a BUSY
|
|
2859
|
+
// recipient's mid-turn output as success (the isAcceptedSubmitState last_output_at leak),
|
|
2860
|
+
// so additionally classify whether the body was CONSUMED as a fresh turn vs QUEUED in a
|
|
2861
|
+
// busy composer vs UNKNOWN — and surface it to the caller. Advisory + additive: it does
|
|
2862
|
+
// NOT change accepted/retryable (back-compat); it only tells the sender what telepty can
|
|
2863
|
+
// actually observe past the PTY layer. Conservative (never-false-consumed).
|
|
2864
|
+
const consumptionResult = await submitGate.classifyInjectConsumption(session, injectedBody, {
|
|
2865
|
+
submittedAtMs,
|
|
2866
|
+
sinceBytes: ringBytesAtSubmit,
|
|
2867
|
+
getState: () => sessionStateManager.getState(id),
|
|
2868
|
+
stripAnsi: stripAnsiState,
|
|
2869
|
+
});
|
|
2870
|
+
consumption = consumptionResult.status;
|
|
2871
|
+
consumptionReason = consumptionResult.reason;
|
|
2872
|
+
if (verify) {
|
|
2873
|
+
verify.consumption = consumptionResult.status;
|
|
2874
|
+
verify.consumption_reason = consumptionResult.reason;
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2852
2877
|
if (confirm && !confirm.accepted) {
|
|
2853
2878
|
const reason = gatedDispatchAfterTimeout ? 'gated_dispatch_unconsumed' : 'submit_unconfirmed';
|
|
2854
2879
|
const failBody = {
|
|
@@ -2863,6 +2888,7 @@ app.post('/api/sessions/:id/submit', async (req, res) => {
|
|
|
2863
2888
|
gate_wait_ms: gateResult.waited_ms,
|
|
2864
2889
|
verify,
|
|
2865
2890
|
confirm,
|
|
2891
|
+
...(consumption ? { consumption, consumption_reason: consumptionReason } : {}),
|
|
2866
2892
|
gated_dispatch_after_timeout: true,
|
|
2867
2893
|
...(promptSymbol ? { prompt_symbol: promptSymbol } : {}),
|
|
2868
2894
|
};
|
|
@@ -2885,6 +2911,7 @@ app.post('/api/sessions/:id/submit', async (req, res) => {
|
|
|
2885
2911
|
gate_wait_ms: gateResult.waited_ms,
|
|
2886
2912
|
verify,
|
|
2887
2913
|
confirm,
|
|
2914
|
+
...(consumption ? { consumption, consumption_reason: consumptionReason } : {}),
|
|
2888
2915
|
...(gatedDispatchAfterTimeout ? { gated_dispatch_after_timeout: true } : {}),
|
|
2889
2916
|
...(promptSymbol ? { prompt_symbol: promptSymbol } : {}),
|
|
2890
2917
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dmsdc-ai/aigentry-telepty",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"main": "daemon.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"aigentry-telepty": "install.js",
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
"scripts": {
|
|
38
38
|
"postinstall": "node scripts/postinstall.js",
|
|
39
39
|
"preuninstall": "node scripts/preuninstall.js",
|
|
40
|
-
"test": "node --require ./test-support/setup-env.js --test test/auth.test.js test/http-auth.test.js test/broker-protocol.test.js test/broker-auth.test.js test/broker-server.test.js test/broker-client.test.js test/daemon-broker-wiring.test.js test/broker-cli.test.js test/broker-integration.test.js test/daemon.test.js test/daemon-singleton.test.js test/daemon-harness-port.test.js test/daemon-bind-default.test.js test/integration/daemon-launch.test.js test/cli.test.js test/subcommand-help.test.js test/telepty-kill.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/session-store-persistence.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/peer-inject-validator.test.js test/enforce-submit-gate.test.js test/submit-gate.test.js test/submit-via-pty.test.js test/submit-render-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/install-service-generation.test.js test/install-broker-service.test.js test/uninstall.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/ensure-daemon-running.test.js test/daemon-restart-fallback-15.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js test/release-0.4.5-bugfixes.test.js test/idle-unconfirmed-settle.test.js test/idle-unconfirmed-consumption.test.js test/provenance.test.js test/inject-audit-broker-seam.test.js test/inject-provenance-daemon.test.js test/inject-audit-log.test.js test/inject-audit-daemon.test.js test/inject-audit-cli.test.js && git diff --exit-code tests/snippet-protocol/v1/",
|
|
41
|
-
"test:watch": "node --require ./test-support/setup-env.js --test --watch test/auth.test.js test/http-auth.test.js test/broker-protocol.test.js test/broker-auth.test.js test/broker-server.test.js test/broker-client.test.js test/daemon-broker-wiring.test.js test/broker-cli.test.js test/broker-integration.test.js test/daemon.test.js test/daemon-singleton.test.js test/daemon-harness-port.test.js test/daemon-bind-default.test.js test/integration/daemon-launch.test.js test/cli.test.js test/subcommand-help.test.js test/telepty-kill.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/session-store-persistence.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/peer-inject-validator.test.js test/enforce-submit-gate.test.js test/submit-gate.test.js test/submit-via-pty.test.js test/submit-render-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/install-service-generation.test.js test/install-broker-service.test.js test/uninstall.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/ensure-daemon-running.test.js test/daemon-restart-fallback-15.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js test/release-0.4.5-bugfixes.test.js test/idle-unconfirmed-settle.test.js test/idle-unconfirmed-consumption.test.js test/provenance.test.js test/inject-audit-broker-seam.test.js test/inject-provenance-daemon.test.js test/inject-audit-log.test.js test/inject-audit-daemon.test.js test/inject-audit-cli.test.js",
|
|
42
|
-
"test:ci": "node --require ./test-support/setup-env.js --test --test-reporter=spec test/auth.test.js test/http-auth.test.js test/broker-protocol.test.js test/broker-auth.test.js test/broker-server.test.js test/broker-client.test.js test/daemon-broker-wiring.test.js test/broker-cli.test.js test/broker-integration.test.js test/daemon.test.js test/daemon-singleton.test.js test/daemon-harness-port.test.js test/daemon-bind-default.test.js test/integration/daemon-launch.test.js test/cli.test.js test/subcommand-help.test.js test/telepty-kill.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/session-store-persistence.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/peer-inject-validator.test.js test/enforce-submit-gate.test.js test/submit-gate.test.js test/submit-via-pty.test.js test/submit-render-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/install-service-generation.test.js test/install-broker-service.test.js test/uninstall.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/ensure-daemon-running.test.js test/daemon-restart-fallback-15.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js test/release-0.4.5-bugfixes.test.js test/idle-unconfirmed-settle.test.js test/idle-unconfirmed-consumption.test.js test/provenance.test.js test/inject-audit-broker-seam.test.js test/inject-provenance-daemon.test.js test/inject-audit-log.test.js test/inject-audit-daemon.test.js test/inject-audit-cli.test.js && git diff --exit-code tests/snippet-protocol/v1/",
|
|
40
|
+
"test": "node --require ./test-support/setup-env.js --test test/auth.test.js test/http-auth.test.js test/broker-protocol.test.js test/broker-auth.test.js test/broker-server.test.js test/broker-client.test.js test/daemon-broker-wiring.test.js test/broker-cli.test.js test/broker-integration.test.js test/daemon.test.js test/daemon-singleton.test.js test/daemon-harness-port.test.js test/daemon-bind-default.test.js test/integration/daemon-launch.test.js test/cli.test.js test/subcommand-help.test.js test/telepty-kill.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/session-store-persistence.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/peer-inject-validator.test.js test/enforce-submit-gate.test.js test/submit-gate.test.js test/submit-via-pty.test.js test/submit-render-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/inject-consumption-evidence.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/install-service-generation.test.js test/install-broker-service.test.js test/uninstall.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/ensure-daemon-running.test.js test/daemon-restart-fallback-15.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js test/release-0.4.5-bugfixes.test.js test/idle-unconfirmed-settle.test.js test/idle-unconfirmed-consumption.test.js test/provenance.test.js test/inject-audit-broker-seam.test.js test/inject-provenance-daemon.test.js test/inject-audit-log.test.js test/inject-audit-daemon.test.js test/inject-audit-cli.test.js && git diff --exit-code tests/snippet-protocol/v1/",
|
|
41
|
+
"test:watch": "node --require ./test-support/setup-env.js --test --watch test/auth.test.js test/http-auth.test.js test/broker-protocol.test.js test/broker-auth.test.js test/broker-server.test.js test/broker-client.test.js test/daemon-broker-wiring.test.js test/broker-cli.test.js test/broker-integration.test.js test/daemon.test.js test/daemon-singleton.test.js test/daemon-harness-port.test.js test/daemon-bind-default.test.js test/integration/daemon-launch.test.js test/cli.test.js test/subcommand-help.test.js test/telepty-kill.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/session-store-persistence.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/peer-inject-validator.test.js test/enforce-submit-gate.test.js test/submit-gate.test.js test/submit-via-pty.test.js test/submit-render-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/inject-consumption-evidence.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/install-service-generation.test.js test/install-broker-service.test.js test/uninstall.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/ensure-daemon-running.test.js test/daemon-restart-fallback-15.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js test/release-0.4.5-bugfixes.test.js test/idle-unconfirmed-settle.test.js test/idle-unconfirmed-consumption.test.js test/provenance.test.js test/inject-audit-broker-seam.test.js test/inject-provenance-daemon.test.js test/inject-audit-log.test.js test/inject-audit-daemon.test.js test/inject-audit-cli.test.js",
|
|
42
|
+
"test:ci": "node --require ./test-support/setup-env.js --test --test-reporter=spec test/auth.test.js test/http-auth.test.js test/broker-protocol.test.js test/broker-auth.test.js test/broker-server.test.js test/broker-client.test.js test/daemon-broker-wiring.test.js test/broker-cli.test.js test/broker-integration.test.js test/daemon.test.js test/daemon-singleton.test.js test/daemon-harness-port.test.js test/daemon-bind-default.test.js test/integration/daemon-launch.test.js test/cli.test.js test/subcommand-help.test.js test/telepty-kill.test.js test/idle-ttl.test.js test/telepty-clean-older-than.test.js test/lifecycle-transport-agnostic.test.js test/skill-installer.test.js test/interactive-terminal.test.js test/runtime-info.test.js test/session-routing.test.js test/session-state.test.js test/session-store-persistence.test.js test/mailbox-lock.test.js test/report-enforcement.test.js test/enforce-report.test.js test/peer-inject-validator.test.js test/enforce-submit-gate.test.js test/submit-gate.test.js test/submit-via-pty.test.js test/submit-render-gate.test.js test/prompt-symbol-registry.test.js test/inject-submit-flags.test.js test/inject-submit-force-env.test.js test/inject-consumption-evidence.test.js test/host-spec.test.js test/cross-host-inject.test.js test/cross-machine-ssh-routing.test.js test/init.test.js test/install-service-generation.test.js test/install-broker-service.test.js test/uninstall.test.js test/win-resolve-executable.test.js test/version-handshake.test.js test/ensure-daemon-running.test.js test/daemon-restart-fallback-15.test.js test/win-kill-process.test.js test/daemon-control-port-owner.test.js test/banner-stderr-jq-safety.test.js test/bridge-supervisor-ipc.test.js test/bridge-j3-shim.test.js test/bridge-e2e.test.js test/release-0.4.5-bugfixes.test.js test/idle-unconfirmed-settle.test.js test/idle-unconfirmed-consumption.test.js test/provenance.test.js test/inject-audit-broker-seam.test.js test/inject-provenance-daemon.test.js test/inject-audit-log.test.js test/inject-audit-daemon.test.js test/inject-audit-cli.test.js && git diff --exit-code tests/snippet-protocol/v1/",
|
|
43
43
|
"typecheck": "tsc --noEmit",
|
|
44
44
|
"regen-fixtures": "node scripts/regen-snippet-fixtures.js"
|
|
45
45
|
},
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
],
|
|
62
62
|
"author": "dmsdc-ai",
|
|
63
63
|
"license": "ISC",
|
|
64
|
-
"description": "Universal terminal session bridge
|
|
64
|
+
"description": "Universal terminal session bridge — connect any terminal to any terminal, any machine",
|
|
65
65
|
"repository": {
|
|
66
66
|
"type": "git",
|
|
67
67
|
"url": "git+https://github.com/dmsdc-ai/aigentry-telepty.git"
|
package/src/submit-gate.js
CHANGED
|
@@ -438,6 +438,85 @@ function observeInjectEcho(session, bodyText, opts = {}) {
|
|
|
438
438
|
return { observed: false, reason: 'no_echo', windows_matched: hits };
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
+
// #53: classify whether an injected+submitted body was CONSUMED as a new turn by the
|
|
442
|
+
// recipient TUI, vs QUEUED in a busy composer, vs UNKNOWN. This is the DELIVERY-side dual
|
|
443
|
+
// of #52 (which gates the IDLE signal on consumption evidence). A bare `Submitted via
|
|
444
|
+
// pty_cr` only proves bytes reached the PTY master; a BUSY Claude Code TUI parks the CR'd
|
|
445
|
+
// text in its composer ("Press up to edit queued messages") and never starts a turn, so the
|
|
446
|
+
// sender must be able to tell `queued` from `consumed`.
|
|
447
|
+
//
|
|
448
|
+
// Hard boundary (#53): telepty cannot see inside the TUI's turn loop, so `consumed` is only
|
|
449
|
+
// claimable from OBSERVABLE evidence, and never-false-consumed is conservative:
|
|
450
|
+
//
|
|
451
|
+
// consumed — the recipient was NOT already busy at the CR and then began a FRESH turn:
|
|
452
|
+
// an idle→working/thinking transition whose since_ms ≥ submittedAtMs (a genuine
|
|
453
|
+
// new turn, NOT mere continued output from a turn already running — the leak
|
|
454
|
+
// that made `last_output_at ≥ submittedAtMs` read a busy queue as success).
|
|
455
|
+
// queued — the injected body is still observably PARKED on screen after a short settle
|
|
456
|
+
// (windowed echo match — #52 technique — tolerates composer line-wrap/borders).
|
|
457
|
+
// A recipient already busy at the CR can only ever land here (fact 1: busy CR
|
|
458
|
+
// queues, never fires), never in `consumed`.
|
|
459
|
+
// unknown — neither positive signal within the window (conservative default).
|
|
460
|
+
//
|
|
461
|
+
// Pure: DI getState + outputRing-only, DI now/sleep/stripAnsi — no I/O, no daemon coupling.
|
|
462
|
+
async function classifyInjectConsumption(session, bodyText, opts = {}) {
|
|
463
|
+
const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : 1200;
|
|
464
|
+
const settleMs = Number.isFinite(opts.settleMs) ? opts.settleMs : 250;
|
|
465
|
+
const intervalMs = Number.isFinite(opts.intervalMs) ? opts.intervalMs : 80;
|
|
466
|
+
const minChars = Number.isFinite(opts.minChars) ? opts.minChars : 24;
|
|
467
|
+
const stripAnsi = typeof opts.stripAnsi === 'function' ? opts.stripAnsi : (s) => s;
|
|
468
|
+
const now = typeof opts.now === 'function' ? opts.now : () => Date.now();
|
|
469
|
+
const sleep = typeof opts.sleep === 'function' ? opts.sleep : (ms) => new Promise((r) => setTimeout(r, ms));
|
|
470
|
+
const getState = typeof opts.getState === 'function' ? opts.getState : null;
|
|
471
|
+
const submittedAtMs = Number.isFinite(opts.submittedAtMs) ? opts.submittedAtMs : now();
|
|
472
|
+
const sinceBytes = Number.isFinite(opts.sinceBytes) ? opts.sinceBytes : null;
|
|
473
|
+
|
|
474
|
+
const body = normalize(bodyText);
|
|
475
|
+
if (body.length === 0) return { status: 'unknown', reason: 'empty_body', waited_ms: 0 };
|
|
476
|
+
if (body.length < minChars) return { status: 'unknown', reason: 'body_too_short', waited_ms: 0 };
|
|
477
|
+
if (!session || !Array.isArray(session.outputRing)) {
|
|
478
|
+
return { status: 'unknown', reason: 'no_ring', waited_ms: 0 };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Was the recipient ALREADY busy when the CR was written? A busy claude-code TUI parks the
|
|
482
|
+
// CR'd text and never starts a turn (#53 fact 1), so we may observe `queued` there but must
|
|
483
|
+
// NEVER claim `consumed` — that is the hard boundary that produced the false success.
|
|
484
|
+
const initial = getState ? getState() : null;
|
|
485
|
+
const startedBusy = !!(initial && ACCEPTED_AFTER_SUBMIT_STATES.has(initial.state)
|
|
486
|
+
&& Number.isFinite(initial.since_ms) && initial.since_ms < submittedAtMs);
|
|
487
|
+
|
|
488
|
+
const observeParked = () => observeInjectEcho(session, bodyText, { stripAnsi, sinceBytes, minChars });
|
|
489
|
+
|
|
490
|
+
const start = now();
|
|
491
|
+
while (true) {
|
|
492
|
+
// consumed — only from a non-busy start that produced a fresh idle→working/thinking turn.
|
|
493
|
+
if (!startedBusy && getState) {
|
|
494
|
+
const st = getState();
|
|
495
|
+
if (st && ACCEPTED_AFTER_SUBMIT_STATES.has(st.state)
|
|
496
|
+
&& Number.isFinite(st.since_ms) && st.since_ms >= submittedAtMs) {
|
|
497
|
+
return { status: 'consumed', reason: `turn_started_${st.state}`, waited_ms: now() - start };
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const elapsed = now() - start;
|
|
502
|
+
// Busy recipient cannot consume — short-circuit to `queued` as soon as the parked body
|
|
503
|
+
// settles, instead of waiting out the full window for a turn that will never come.
|
|
504
|
+
if (startedBusy && elapsed >= settleMs) {
|
|
505
|
+
const echo = observeParked();
|
|
506
|
+
if (echo.observed) return { status: 'queued', reason: 'busy_parked', waited_ms: elapsed };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (elapsed >= timeoutMs) {
|
|
510
|
+
const echo = observeParked();
|
|
511
|
+
if (echo.observed) {
|
|
512
|
+
return { status: 'queued', reason: startedBusy ? 'busy_parked' : 'body_parked', waited_ms: elapsed };
|
|
513
|
+
}
|
|
514
|
+
return { status: 'unknown', reason: startedBusy ? 'busy_no_evidence' : 'no_turn', waited_ms: elapsed };
|
|
515
|
+
}
|
|
516
|
+
await sleep(intervalMs);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
441
520
|
function isAcceptedSubmitState(state, submittedAtMs) {
|
|
442
521
|
if (!state || !ACCEPTED_AFTER_SUBMIT_STATES.has(state.state)) return false;
|
|
443
522
|
if (!Number.isFinite(submittedAtMs)) {
|
|
@@ -613,6 +692,7 @@ module.exports = {
|
|
|
613
692
|
confirmSubmitAccepted,
|
|
614
693
|
observeBodyVisibility,
|
|
615
694
|
observeInjectEcho,
|
|
695
|
+
classifyInjectConsumption,
|
|
616
696
|
awaitPromptSymbol,
|
|
617
697
|
defaultReadScreen,
|
|
618
698
|
isReady,
|