@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 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
- console.log(`✅ Submitted via ${submitData.strategy}${attemptsNote}${gateNote}${lateNote}${forcedNote}.`);
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",
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 \u2014 connect any terminal to any terminal, any machine",
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"
@@ -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,