@dmsdc-ai/aigentry-telepty 0.6.9 → 0.6.10

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
@@ -4,6 +4,8 @@ All notable changes to `@dmsdc-ai/aigentry-telepty` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.6.10] - 2026-07-05
8
+
7
9
  ### Added — seamless cross-machine on Tailscale (auto bind + auto trust) (#672)
8
10
 
9
11
  - **On a Tailscale host, a fresh install is cross-machine-ready with zero manual env.** At
@@ -31,6 +33,22 @@ All notable changes to `@dmsdc-ai/aigentry-telepty` are documented here.
31
33
  Zero-config cross-machine is Tailscale-specific; other topologies use the manual
32
34
  `TELEPTY_BIND` path. Addressing stays IP-free — use MagicDNS names with `<id>@<host>`.
33
35
 
36
+ ### Fixed — busy-target `--submit` latency (busy-dispatch fast-path) (#694)
37
+
38
+ - **A gated `--submit` to a BUSY (mid-turn) recipient no longer burns the full gate timeout.**
39
+ A busy claude sits in `working`/`thinking` — neither is a ready state — so the render-gate
40
+ (`awaitReplReady`) could never pass mid-turn and waited out the entire `gate_timeout_ms`
41
+ (default 10s) before dispatching best-effort. A new busy-dispatch fast-path detects a
42
+ **genuine ongoing turn** (`working`/`thinking` held ≥ `TELEPTY_SUBMIT_BUSY_GRACE_MS`, default
43
+ 250ms — which excludes the transient `working` a target emits while echoing our own
44
+ just-injected text) and dispatches after only the existing echo+micro-quiet settle
45
+ (`awaitInputSettled`), cutting the busy path from ~10s to **sub-second**.
46
+ - **Never fires blindly, idle path unchanged.** The downstream `gatedTerminalSubmit` still
47
+ runs its own echo+quiet gate before the `\r`; a CR into a busy composer merely queues and the
48
+ #617 hold-and-redeliver loop re-fires it on idle (delivery reliability unchanged). `idle`/
49
+ `waiting` targets never match the fast-path, so the proven idle behavior is byte-for-byte
50
+ identical. Rollback via `TELEPTY_SUBMIT_BUSY_DISPATCH=off`.
51
+
34
52
  ## [0.6.6] - 2026-06-14
35
53
 
36
54
  ### Fixed — duplicate same-id `allow` flap loop + `kill` doesn't stick (#56)
package/README.md CHANGED
@@ -299,4 +299,4 @@ npm run test:watch # Watch mode
299
299
 
300
300
  ## License
301
301
 
302
- ISC
302
+ MIT
package/cli.js CHANGED
@@ -2375,7 +2375,14 @@ async function main() {
2375
2375
  const target = await resolveSessionTarget(sessionId);
2376
2376
  if (!target) {
2377
2377
  console.error(`❌ Session '${sessionId}' was not found on any discovered host.`);
2378
- process.exit(1);
2378
+ // #691: drain-exit, do NOT process.exit() here. resolveSessionTarget →
2379
+ // discoverSessions() leaves undici keep-alive sockets and AbortSignal.timeout
2380
+ // timers still mid-close; a hard process.exit() races that teardown and trips a
2381
+ // libuv double-close assertion (src/win/async.c:76 UV_HANDLE_CLOSING) on Windows,
2382
+ // which wedges the session. Setting exitCode + returning lets the event loop close
2383
+ // each handle exactly once. Same exit code (1); success path unchanged.
2384
+ process.exitCode = 1;
2385
+ return;
2379
2386
  }
2380
2387
 
2381
2388
  let injectPrompt = prompt;
@@ -2388,7 +2395,8 @@ async function main() {
2388
2395
  const ent = checkEntitlement({ feature: 'telepty.remote_sessions' });
2389
2396
  if (!ent.allowed) {
2390
2397
  console.error(`⚠️ ${ent.reason}\n Upgrade: ${ent.upgrade_url}`);
2391
- process.exit(1);
2398
+ process.exitCode = 1; // #691: drain-exit (see not-found above), not process.exit() mid-teardown
2399
+ return;
2392
2400
  }
2393
2401
 
2394
2402
  if (useRef) {
package/daemon.js CHANGED
@@ -53,6 +53,15 @@ const REDELIVER_ENABLED = String(process.env.TELEPTY_REDELIVER || '').toLowerCas
53
53
  const REDELIVER_MAX_ATTEMPTS = Math.max(1, Number(process.env.TELEPTY_REDELIVER_MAX_ATTEMPTS || 3));
54
54
  const REDELIVER_TOTAL_TIMEOUT_MS = Math.max(1000, Number(process.env.TELEPTY_REDELIVER_TOTAL_TIMEOUT_MS || 600000));
55
55
  const REDELIVER_IDLE_WAIT_MS = Math.max(1000, Number(process.env.TELEPTY_REDELIVER_IDLE_WAIT_MS || 120000));
56
+ // #694: busy-dispatch fast-path — on a busy (mid-turn) recipient the render-gate (awaitReplReady)
57
+ // can never reach idle/waiting, so it burns the full gate timeout before best-effort dispatch.
58
+ // When enabled, a genuine ongoing turn (working/thinking held ≥ grace) dispatches after only the
59
+ // echo+micro-quiet settle instead of the full timeout. `off` restores the pure-idle-gate behavior.
60
+ const SUBMIT_BUSY_DISPATCH_ENABLED = String(process.env.TELEPTY_SUBMIT_BUSY_DISPATCH || '').toLowerCase() !== 'off';
61
+ // Grace floor separating a REAL ongoing turn from the transient `working` a target emits while
62
+ // echoing our OWN just-injected text (duration_ms ≈ 0). Default 250ms: above the echo transient,
63
+ // below any human-perceptible latency.
64
+ const SUBMIT_BUSY_GRACE_MS = Math.max(0, Number(process.env.TELEPTY_SUBMIT_BUSY_GRACE_MS || 250));
56
65
 
57
66
  // Session state machine manager — auto-detects session state from PTY output
58
67
  const sessionStateManager = new SessionStateManager({
@@ -1798,7 +1807,9 @@ function forceSubmitDeliveredToSurface(session, strategy) {
1798
1807
  // PTY/ownerWs really is gone, submitViaPty returns false → a clean 503, not a false success).
1799
1808
  // Only genuinely-terminal PTY states — session_dead / session_error / session_restarting — still
1800
1809
  // short-circuit, because writing a CR to a dead/errored/restarting PTY is pointless.
1801
- const NON_TERMINAL_GATE_REASONS = new Set(['timeout', 'no_state', 'no_state_manager']);
1810
+ // #694: `busy_settled` (busy-dispatch fast-path) joins the best-effort DISPATCH set — the target is
1811
+ // alive and mid-turn, so firing the CR is correct (it queues + #617 redelivers), never a hard-fail.
1812
+ const NON_TERMINAL_GATE_REASONS = new Set(['timeout', 'no_state', 'no_state_manager', 'busy_settled']);
1802
1813
  function isTerminalGateFailure(gateResult) {
1803
1814
  return !!(
1804
1815
  gateResult &&
@@ -2843,8 +2854,10 @@ function submitViaOsascript(sessionId, keyCombo) {
2843
2854
  // POST /api/sessions/:id/submit — render-gated CLI-aware submit
2844
2855
  //
2845
2856
  // Default behavior (0.3.0+): wait for the target REPL to be ready (sessionStateManager
2846
- // reports `idle`/`waiting` with confidence ≥ 0.85) before firing Enter. When the
2847
- // caller passes `injected_body`, also verify the body has been consumed (i.e.
2857
+ // reports `idle`/`waiting` with confidence ≥ 0.5, the awaitReplReady default) before firing
2858
+ // Enter. #694: a BUSY recipient (working/thinking) is never idle/waiting, so the busy-dispatch
2859
+ // fast-path below dispatches best-effort after an echo+quiet settle instead of burning the gate
2860
+ // timeout. When the caller passes `injected_body`, also verify the body has been consumed (i.e.
2848
2861
  // disappeared from the input box) by polling the session output ring; if still
2849
2862
  // visible, perform one bounded retry.
2850
2863
  //
@@ -3044,10 +3057,38 @@ app.post('/api/sessions/:id/submit', async (req, res) => {
3044
3057
  // Hard-fail reasons (session_dead/error/restarting/no_state/no_state_manager)
3045
3058
  // still short-circuit to 504 because dispatching to a dead/missing PTY is
3046
3059
  // pointless. See spec §1.3 / §3.3.
3047
- const gateResult = await submitGate.awaitReplReady(id, sessionStateManager, {
3048
- timeoutMs: gateTimeoutMs,
3049
- ...(minConfidence !== undefined ? { minConfidence } : {}),
3050
- });
3060
+ //
3061
+ // #694 busy-dispatch fast-path: a BUSY (mid-turn) recipient sits in working/thinking — neither
3062
+ // is a READY_STATE so awaitReplReady can never pass mid-turn and would burn the FULL
3063
+ // gateTimeoutMs (up to 10s) before best-effort dispatch. When the target is a GENUINE ongoing
3064
+ // turn (isBusyDispatchState: working/thinking held ≥ SUBMIT_BUSY_GRACE_MS, which excludes the
3065
+ // transient `working` from echoing our OWN just-injected text, duration_ms ≈ 0), wait ONLY for
3066
+ // the input to settle (body echoed + micro-quiet, via awaitInputSettled) and dispatch
3067
+ // best-effort — the same downstream path as a gate timeout, but in ~hundreds of ms. idle/waiting
3068
+ // never match isBusyDispatchState, so the idle path is byte-unchanged. gatedTerminalSubmit below
3069
+ // still runs its OWN echo+quiet gate before the \r (never fires blindly), and a CR into a busy
3070
+ // composer merely queues → #617 redeliver fires it on idle. Rollback: TELEPTY_SUBMIT_BUSY_DISPATCH=off.
3071
+ let gateResult = null;
3072
+ if (injectedBody && SUBMIT_BUSY_DISPATCH_ENABLED) {
3073
+ const cur = sessionStateManager.getState(id);
3074
+ if (submitGate.isBusyDispatchState(cur, SUBMIT_BUSY_GRACE_MS)) {
3075
+ const settle = await submitGate.awaitInputSettled(session, injectedBody, {
3076
+ timeoutMs: 1200,
3077
+ quietWindowMs: 100,
3078
+ echoGraceMs: 400,
3079
+ pollIntervalMs: 30,
3080
+ stripAnsi: stripAnsiState,
3081
+ });
3082
+ gateResult = { ready: false, reason: 'busy_settled', last_state: cur.state, waited_ms: settle.waited_ms };
3083
+ console.log(`[SUBMIT] busy-dispatch fast-path ${id}: state=${cur.state} (${cur.duration_ms}ms) settle=${settle.reason} (${settle.waited_ms}ms) — dispatching without idle-gate burn`);
3084
+ }
3085
+ }
3086
+ if (!gateResult) {
3087
+ gateResult = await submitGate.awaitReplReady(id, sessionStateManager, {
3088
+ timeoutMs: gateTimeoutMs,
3089
+ ...(minConfidence !== undefined ? { minConfidence } : {}),
3090
+ });
3091
+ }
3051
3092
  const gatedDispatchAfterTimeout = !gateResult.ready;
3052
3093
  if (isTerminalGateFailure(gateResult)) {
3053
3094
  console.log(`[SUBMIT] gate hard-fail ${id}: ${gateResult.reason} (last_state=${gateResult.last_state})`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-telepty",
3
- "version": "0.6.9",
3
+ "version": "0.6.10",
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/tailnet-autobind.test.js test/win-firewall.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/dupid-flap-kill-stick-56.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/submit-gate-restore-register-678.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/daemon-lifecycle-55.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/idle-unconfirmed-decayed-619.test.js test/inject-redeliver.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/tailnet-autobind.test.js test/win-firewall.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/dupid-flap-kill-stick-56.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/submit-gate-restore-register-678.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/daemon-lifecycle-55.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/idle-unconfirmed-decayed-619.test.js test/inject-redeliver.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/tailnet-autobind.test.js test/win-firewall.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/dupid-flap-kill-stick-56.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/submit-gate-restore-register-678.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/daemon-lifecycle-55.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/idle-unconfirmed-decayed-619.test.js test/inject-redeliver.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/tailnet-autobind.test.js test/win-firewall.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/dupid-flap-kill-stick-56.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/submit-gate-restore-register-678.test.js test/submit-busy-dispatch-694.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/daemon-lifecycle-55.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/idle-unconfirmed-decayed-619.test.js test/inject-redeliver.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 test/inject-notfound-teardown-691.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/tailnet-autobind.test.js test/win-firewall.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/dupid-flap-kill-stick-56.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/submit-gate-restore-register-678.test.js test/submit-busy-dispatch-694.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/daemon-lifecycle-55.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/idle-unconfirmed-decayed-619.test.js test/inject-redeliver.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 test/inject-notfound-teardown-691.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/tailnet-autobind.test.js test/win-firewall.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/dupid-flap-kill-stick-56.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/submit-gate-restore-register-678.test.js test/submit-busy-dispatch-694.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/daemon-lifecycle-55.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/idle-unconfirmed-decayed-619.test.js test/inject-redeliver.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 test/inject-notfound-teardown-691.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
  },
@@ -35,6 +35,19 @@ function isFailed(state) {
35
35
  return !!(state && FAIL_STATES.has(state.state));
36
36
  }
37
37
 
38
+ // #694: a "busy-dispatch" state — the recipient is mid-turn (working/thinking, the
39
+ // ACCEPTED_AFTER_SUBMIT_STATES) and has been for at least graceMs. Used by the submit gate to
40
+ // short-circuit the idle-wait on a genuinely-busy target: working/thinking are NOT READY_STATES,
41
+ // so awaitReplReady can never pass mid-turn and would burn its full timeout before best-effort
42
+ // dispatch. The graceMs floor is what distinguishes a REAL ongoing turn from the transient
43
+ // `working` a target emits while echoing our OWN just-injected text (duration_ms ≈ 0) — so a
44
+ // fresh inject never trips the fast-path prematurely, and idle/waiting never match at all.
45
+ function isBusyDispatchState(state, graceMs) {
46
+ if (!state || !ACCEPTED_AFTER_SUBMIT_STATES.has(state.state)) return false;
47
+ const grace = Number.isFinite(graceMs) ? graceMs : 0;
48
+ return Number.isFinite(state.duration_ms) && state.duration_ms >= grace;
49
+ }
50
+
38
51
  /**
39
52
  * Wait until the session's REPL is ready to accept Enter.
40
53
  *
@@ -784,6 +797,7 @@ module.exports = {
784
797
  defaultReadScreen,
785
798
  isReady,
786
799
  isFailed,
800
+ isBusyDispatchState,
787
801
  READY_STATES,
788
802
  FAIL_STATES,
789
803
  ACCEPTED_AFTER_SUBMIT_STATES,