botmux 3.4.1 → 3.4.2

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.
Files changed (37) hide show
  1. package/dist/adapters/cli/claude-code.d.ts.map +1 -1
  2. package/dist/adapters/cli/claude-code.js +6 -7
  3. package/dist/adapters/cli/claude-code.js.map +1 -1
  4. package/dist/cli.js +4 -5
  5. package/dist/cli.js.map +1 -1
  6. package/dist/core/session-ready-handshake.d.ts +11 -0
  7. package/dist/core/session-ready-handshake.d.ts.map +1 -0
  8. package/dist/core/session-ready-handshake.js +41 -0
  9. package/dist/core/session-ready-handshake.js.map +1 -0
  10. package/dist/core/worker-pool.d.ts.map +1 -1
  11. package/dist/core/worker-pool.js +9 -0
  12. package/dist/core/worker-pool.js.map +1 -1
  13. package/dist/daemon.d.ts.map +1 -1
  14. package/dist/daemon.js +10 -2
  15. package/dist/daemon.js.map +1 -1
  16. package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
  17. package/dist/im/lark/event-dispatcher.js +1 -3
  18. package/dist/im/lark/event-dispatcher.js.map +1 -1
  19. package/dist/setup/open-platform-automation.d.ts +1 -1
  20. package/dist/setup/open-platform-automation.d.ts.map +1 -1
  21. package/dist/setup/open-platform-automation.js +0 -1
  22. package/dist/setup/open-platform-automation.js.map +1 -1
  23. package/dist/types.d.ts +7 -0
  24. package/dist/types.d.ts.map +1 -1
  25. package/dist/utils/idle-detector.d.ts +9 -1
  26. package/dist/utils/idle-detector.d.ts.map +1 -1
  27. package/dist/utils/idle-detector.js +18 -5
  28. package/dist/utils/idle-detector.js.map +1 -1
  29. package/dist/utils/input-gate.d.ts +22 -3
  30. package/dist/utils/input-gate.d.ts.map +1 -1
  31. package/dist/utils/input-gate.js +22 -3
  32. package/dist/utils/input-gate.js.map +1 -1
  33. package/dist/utils/ready-gate.d.ts +6 -6
  34. package/dist/utils/ready-gate.js +6 -6
  35. package/dist/worker.js +76 -21
  36. package/dist/worker.js.map +1 -1
  37. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Ready-gate state machine — holds the FIRST prompt for Claude-family CLIs until
3
- * a SessionStart hook fires a "真就绪" (true-ready) signal.
3
+ * a SessionStart hook proves the outer startup selector has been passed.
4
4
  *
5
5
  * Why this exists: a custom launcher (e.g. `cjadk claude`) shows an interactive
6
6
  * model/session selector at startup whose cursor is `❯` (U+276F). That glyph
@@ -10,11 +10,11 @@
10
10
  * trailing Enter mis-selects a menu item). The selector clears before Claude's
11
11
  * real input box renders, so the message is silently lost.
12
12
  *
13
- * Claude Code's `SessionStart` hook fires within ~3ms of the real input box
14
- * rendering, and crucially does NOT fire while the launcher is still on its
15
- * selector so it's an unambiguous "the CLI is genuinely ready" signal. This
16
- * gate suspends the first flush until that signal (or a fallback timeout) so the
17
- * selector can never eat the first message.
13
+ * Claude Code's `SessionStart` hook crucially does NOT fire while the launcher
14
+ * is still on its selector. Claude can run multiple matching hooks in parallel,
15
+ * though, and renders the real prompt only after all finish. This gate therefore
16
+ * establishes the anti-selector boundary; the worker separately waits for fresh
17
+ * post-signal prompt evidence before it flushes Claude input.
18
18
  *
19
19
  * Lifecycle (recreated per CLI spawn in the worker):
20
20
  * - `arm()` — call at spawn when the adapter injects the SessionStart
package/dist/worker.js CHANGED
@@ -24,7 +24,7 @@ import { readProcessStartIdentity } from './core/session-marker.js';
24
24
  import { drainTranscript, joinAssistantText, trailingAssistantText, findJsonlContainingFingerprint, findJsonlsContainingExactContent, findLatestJsonl, extractLastAssistantTurn, extractTurnStartText, splitTranscriptEventsByCutoff } from './services/claude-transcript.js';
25
25
  import { BridgeTurnQueue, makeFingerprint, normaliseForFingerprint } from './services/bridge-turn-queue.js';
26
26
  import { shouldSuppressBridgeEmit } from './services/bridge-fallback-gate.js';
27
- import { decideHardTimeoutAction, decideSettleMarkReady, shouldReleaseFirstPromptTimeout, shouldWriteNow } from './utils/input-gate.js';
27
+ import { decideHardTimeoutAction, decideSettleMarkReady, shouldReleaseFirstPromptTimeout, shouldWaitForPostSessionStartPromptEvidence, shouldWriteNow, } from './utils/input-gate.js';
28
28
  import { canStartInjectionFlush, shouldDeferUserFlush, shouldFlushInjectionsFirst } from './core/inject-queue-policy.js';
29
29
  import { stripAnsiForLog, tailChars } from './utils/crash-log.js';
30
30
  import { CodexUpdateDialogGuard } from './utils/codex-update-dialog.js';
@@ -794,17 +794,16 @@ let bareShellLaunchBlocked = false;
794
794
  * shell. Reset per spawn in spawnCli. */
795
795
  let bareShellChecked = false;
796
796
  /** Ready-gate (Claude-family): holds the first prompt until the SessionStart
797
- * hook fires a true-ready signal, so a cjadk-style startup selector's (which
798
- * falsely matches readyPattern) can't eat the first message. Recreated + armed
799
- * per spawn in spawnCli; disarmed on signal or fallback timeout. */
797
+ * hook proves a cjadk-style startup selector is behind us. Claude then needs
798
+ * fresh post-hook prompt evidence because sibling hooks may still be running.
799
+ * Recreated + armed per spawn; disarmed on signal or fallback timeout. */
800
800
  let readyGate = new ReadyGate();
801
801
  /** Fallback timer: if the SessionStart signal never arrives (hook injection
802
802
  * failed / old CLI / launcher didn't pass --settings / adopt) release the gate
803
803
  * and fall back to readyPattern + quiescence. */
804
804
  let readySignalTimer = null;
805
805
  /** How long the ready-gate waits for the SessionStart signal before falling
806
- * back. The real signal lands within ~ms of the input box rendering, so this is
807
- * pure insurance against a missing/failed hook — generous but bounded. */
806
+ * back. This is insurance against a missing/failed hook generous but bounded. */
808
807
  const READY_SIGNAL_TIMEOUT_MS = 45_000;
809
808
  /** Soft fallback for CLIs that never emit an idle/ready signal during startup.
810
809
  * Legacy adapters release queued first input here. Adapters that opt into
@@ -817,13 +816,13 @@ const FIRST_PROMPT_HARD_TIMEOUT_MS = 90_000;
817
816
  /** Epoch ms of the most recent PTY output — used to settle for quiescence
818
817
  * before the first flush (see settleThenFlush). */
819
818
  let lastPtyOutputAtMs = 0;
820
- /** After the SessionStart signal fires, the input box has appeared but Ink's
821
- * startup render isn't fully drained yet — typing immediately trips Claude's
819
+ /** After the SessionStart signal fires, Ink's startup rendering or sibling
820
+ * hooks may still be active — typing immediately can trip Claude's
822
821
  * paste-burst heuristic and the `\` soft-newline markers (claude-code
823
822
  * writeInput) get kept literally. This is pronounced under wrapperCli launchers
824
823
  * (e.g. `aiden x claude`) whose Claude renders more at startup. So we wait for
825
- * the PTY to fall quiet for SETTLE_MS before the first flush — the signal still
826
- * gates readiness (anti-selector), the settle just lets the render drain. */
824
+ * PTY quiescence, while Claude additionally requires fresh prompt evidence
825
+ * after the SessionStart boundary. */
827
826
  const READY_FLUSH_SETTLE_MS = 1_000;
828
827
  /** Upper bound on the settle so a chatty startup (spinners, periodic redraw)
829
828
  * can't stall the first prompt indefinitely. */
@@ -846,11 +845,18 @@ let promptReadyDetectedDuringSettle = false;
846
845
  * spawn that renders ❯ but never fires BOTMUX_READY_COMMAND waits the full
847
846
  * hard timeout (and previously never delivered at all). */
848
847
  let readyPatternSeenDuringHold = false;
848
+ /** Claude's SessionStart hooks run in parallel. Its botmux hook proves the
849
+ * startup selector is behind us, but sibling project hooks may still be
850
+ * running. Hold type-ahead until a fresh PTY prompt is observed after the
851
+ * SessionStart signal. */
852
+ let awaitingPostSessionStartPromptEvidence = false;
853
+ /** Scoped marker set only by IdleDetector's screen-driven callback. */
854
+ let postSessionStartPromptEvidenceInFlight = false;
849
855
  /** Wait until the PTY has been quiet for READY_FLUSH_SETTLE_MS (Ink render
850
856
  * drained), capped at READY_FLUSH_SETTLE_CAP_MS, then flush the held prompt.
851
- * A real SessionStart/BOTMUX_READY_COMMAND signal is itself authoritative
852
- * prompt readiness; the timeout fallback only opens the gate and lets the
853
- * regular readyPattern/idle path prove readiness later. */
857
+ * An authoritative direct ready command (Hermes) can mark prompt readiness;
858
+ * Claude's SessionStart only opens the anti-selector boundary and its regular
859
+ * readyPattern/idle path must prove readiness afterward. */
854
860
  function settleThenFlush(startedAtMs, promptReadyAfterSettle) {
855
861
  readyFlushSettleTimer = null;
856
862
  const now = Date.now();
@@ -4527,6 +4533,15 @@ function releaseRawInputRestartGate() {
4527
4533
  rawInputRestartGate = false;
4528
4534
  log('Replacement CLI prompt ready — releasing deferred passthrough commands');
4529
4535
  }
4536
+ function markPromptReadyFromPty() {
4537
+ postSessionStartPromptEvidenceInFlight = true;
4538
+ try {
4539
+ markPromptReady();
4540
+ }
4541
+ finally {
4542
+ postSessionStartPromptEvidenceInFlight = false;
4543
+ }
4544
+ }
4530
4545
  function markPromptReady() {
4531
4546
  if (isPromptReady)
4532
4547
  return; // guard against duplicate calls
@@ -4546,6 +4561,14 @@ function markPromptReady() {
4546
4561
  log('Idle detected but holding for SessionStart ready signal (startup selector guard)');
4547
4562
  return;
4548
4563
  }
4564
+ if (awaitingPostSessionStartPromptEvidence) {
4565
+ if (!postSessionStartPromptEvidenceInFlight) {
4566
+ log('Ignoring non-PTY ready source while waiting for post-SessionStart prompt evidence');
4567
+ return;
4568
+ }
4569
+ awaitingPostSessionStartPromptEvidence = false;
4570
+ log('Fresh prompt evidence observed after SessionStart hooks');
4571
+ }
4549
4572
  if (isSettlingFirstFlush) {
4550
4573
  promptReadyDetectedDuringSettle = true;
4551
4574
  log('Idle detected during ready-gate settle; deferring prompt-ready until settle completes');
@@ -4577,6 +4600,7 @@ function markPromptReady() {
4577
4600
  maybeEmitWorkflowTranscriptOutput();
4578
4601
  if (awaitingFirstPrompt) {
4579
4602
  awaitingFirstPrompt = false;
4603
+ awaitingPostSessionStartPromptEvidence = false;
4580
4604
  renderer?.markNewTurn(); // exclude history replay from streaming card
4581
4605
  }
4582
4606
  send({ type: 'prompt_ready' });
@@ -4916,6 +4940,10 @@ async function flushPending() {
4916
4940
  log(`Holding ${pendingMessages.length} pending message(s) until ready-gate settle completes`);
4917
4941
  return;
4918
4942
  }
4943
+ if (awaitingPostSessionStartPromptEvidence) {
4944
+ log(`Holding ${pendingMessages.length} pending message(s) until post-SessionStart prompt evidence`);
4945
+ return;
4946
+ }
4919
4947
  // Type-ahead adapters flush even while the CLI is busy; others wait for
4920
4948
  // idle. Claude bridge fallback used to also disable type-ahead because
4921
4949
  // BridgeTurnQueue.ingest didn't recognise the `attachment(queued_command)`
@@ -7369,6 +7397,7 @@ async function spawnCli(cfg, opts = {}) {
7369
7397
  isSettlingFirstFlush = false;
7370
7398
  promptReadyDetectedDuringSettle = false;
7371
7399
  readyPatternSeenDuringHold = false;
7400
+ awaitingPostSessionStartPromptEvidence = false;
7372
7401
  // Reset quiescence baseline so the settle measures silence from THIS spawn.
7373
7402
  lastPtyOutputAtMs = Date.now();
7374
7403
  const readyHookAvailable = effectiveReadyHookInstall
@@ -7411,7 +7440,7 @@ async function spawnCli(cfg, opts = {}) {
7411
7440
  // quiescence, repeatedly triggering markPromptReady() and duplicate cards.
7412
7441
  if (effectiveBackendType !== 'riff') {
7413
7442
  idleDetector = new IdleDetector(cliAdapter);
7414
- idleDetector.onIdle(async () => {
7443
+ idleDetector.onIdle(async (evidenceSource) => {
7415
7444
  log('Prompt detected (idle)');
7416
7445
  // Bridge drain MUST run before markPromptReady() — the latter calls
7417
7446
  // flushPending() which can immediately fire the next queued message
@@ -7433,7 +7462,12 @@ async function spawnCli(cfg, opts = {}) {
7433
7462
  log(`Codex bridge emit error: ${err.message}`);
7434
7463
  }
7435
7464
  }
7436
- markPromptReady();
7465
+ if (evidenceSource === 'screen') {
7466
+ markPromptReadyFromPty();
7467
+ }
7468
+ else {
7469
+ markPromptReady();
7470
+ }
7437
7471
  });
7438
7472
  }
7439
7473
  backend.onData(onPtyData);
@@ -7582,6 +7616,7 @@ async function spawnCli(cfg, opts = {}) {
7582
7616
  return;
7583
7617
  }
7584
7618
  awaitingFirstPrompt = false;
7619
+ awaitingPostSessionStartPromptEvidence = false;
7585
7620
  renderer?.markNewTurn();
7586
7621
  log(forced
7587
7622
  ? `WARN First prompt hard timeout — ${cliName()} readyPattern did not arrive; forcing queued message flush`
@@ -7643,6 +7678,7 @@ function killCli(opts = {}) {
7643
7678
  isSettlingFirstFlush = false;
7644
7679
  promptReadyDetectedDuringSettle = false;
7645
7680
  readyPatternSeenDuringHold = false;
7681
+ awaitingPostSessionStartPromptEvidence = false;
7646
7682
  stopScreenAnalyzer();
7647
7683
  stopScreenUpdates();
7648
7684
  backend?.kill();
@@ -9454,24 +9490,43 @@ process.on('message', async (raw) => {
9454
9490
  break;
9455
9491
  }
9456
9492
  case 'session_ready': {
9457
- // Claude-family SessionStart hook fired (via `botmux session-ready`
9458
- // daemon). The CLI's input box is genuinely rendered release the
9459
- // ready-gate and deliver any held first prompt. Idempotent: a later
9460
- // duplicate (clear/compact source) is a no-op.
9493
+ // Claude-family SessionStart hooks run in parallel. This signal proves
9494
+ // the startup selector is behind us, but a slower project hook can still
9495
+ // be running and Claude does not render its real prompt until ALL hooks
9496
+ // finish. Clear selector-era evidence and require a fresh PTY prompt after
9497
+ // the signal. Hermes keeps its authoritative ready-command behavior.
9461
9498
  log(`SessionStart ready signal received (source=${msg.source ?? '?'})`);
9499
+ const waitForPostHookPrompt = shouldWaitForPostSessionStartPromptEvidence({
9500
+ isClaudeFamily: !!cliAdapter?.claudeDataDir,
9501
+ hasReadyPattern: !!cliAdapter?.readyPattern,
9502
+ awaitingFirstPrompt,
9503
+ isPromptReady,
9504
+ alreadyWaiting: awaitingPostSessionStartPromptEvidence,
9505
+ });
9506
+ if (waitForPostHookPrompt) {
9507
+ awaitingPostSessionStartPromptEvidence = true;
9508
+ promptReadyDetectedDuringSettle = false;
9509
+ readyPatternSeenDuringHold = false;
9510
+ idleDetector?.resetReadyEvidence();
9511
+ lastPtyOutputAtMs = Date.now();
9512
+ log('SessionStart boundary recorded — waiting for fresh post-hook prompt evidence');
9513
+ }
9462
9514
  // 先记下 gate 是否已被 45s fallback 释放:ReadyGate.receive() 是一次性
9463
9515
  // 语义,fallback 抢先后 releaseReadyGate 会整块跳过迟到的真信号。
9464
9516
  const lateAfterFallback = readyGate.isArmed && readyGate.isReceived;
9465
- releaseReadyGate('SessionStart hook', { promptReadyAfterSettle: true });
9517
+ releaseReadyGate('SessionStart hook', { promptReadyAfterSettle: !waitForPostHookPrompt });
9466
9518
  // 冷启动超过 READY_SIGNAL_TIMEOUT_MS 的 CLI(Hermes 常态是 2-3 分钟)恰好
9467
9519
  // 总落在 fallback 之后:fallback 只开闸不投递(非 type-ahead 的
9468
9520
  // flushPending 是 no-op),真信号依然是权威就绪,这里直接兑现。仅限首轮
9469
9521
  // (awaitingFirstPrompt)——首条 prompt 交付后 clear/compact 来源的
9470
9522
  // SessionStart 保持原有 no-op 语义,绝不在会话中途误标就绪。
9471
- if (lateAfterFallback && awaitingFirstPrompt && !isPromptReady) {
9523
+ if (lateAfterFallback && awaitingFirstPrompt && !isPromptReady && !waitForPostHookPrompt) {
9472
9524
  log('Late ready signal after timeout fallback — marking prompt ready now');
9473
9525
  markPromptReady();
9474
9526
  }
9527
+ if (msg.requestId) {
9528
+ send({ type: 'session_ready_ack', requestId: msg.requestId });
9529
+ }
9475
9530
  break;
9476
9531
  }
9477
9532
  case 'set_display_mode': {