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.
- package/dist/adapters/cli/claude-code.d.ts.map +1 -1
- package/dist/adapters/cli/claude-code.js +6 -7
- package/dist/adapters/cli/claude-code.js.map +1 -1
- package/dist/cli.js +4 -5
- package/dist/cli.js.map +1 -1
- package/dist/core/session-ready-handshake.d.ts +11 -0
- package/dist/core/session-ready-handshake.d.ts.map +1 -0
- package/dist/core/session-ready-handshake.js +41 -0
- package/dist/core/session-ready-handshake.js.map +1 -0
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +9 -0
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +10 -2
- package/dist/daemon.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +1 -3
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/setup/open-platform-automation.d.ts +1 -1
- package/dist/setup/open-platform-automation.d.ts.map +1 -1
- package/dist/setup/open-platform-automation.js +0 -1
- package/dist/setup/open-platform-automation.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/idle-detector.d.ts +9 -1
- package/dist/utils/idle-detector.d.ts.map +1 -1
- package/dist/utils/idle-detector.js +18 -5
- package/dist/utils/idle-detector.js.map +1 -1
- package/dist/utils/input-gate.d.ts +22 -3
- package/dist/utils/input-gate.d.ts.map +1 -1
- package/dist/utils/input-gate.js +22 -3
- package/dist/utils/input-gate.js.map +1 -1
- package/dist/utils/ready-gate.d.ts +6 -6
- package/dist/utils/ready-gate.js +6 -6
- package/dist/worker.js +76 -21
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
package/dist/utils/ready-gate.js
CHANGED
|
@@ -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
|
|
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
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
798
|
-
*
|
|
799
|
-
* per spawn
|
|
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.
|
|
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,
|
|
821
|
-
*
|
|
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
|
-
*
|
|
826
|
-
*
|
|
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
|
-
*
|
|
852
|
-
*
|
|
853
|
-
*
|
|
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
|
-
|
|
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
|
|
9458
|
-
//
|
|
9459
|
-
//
|
|
9460
|
-
//
|
|
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:
|
|
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': {
|