botmux 3.4.0 → 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/create-group-resolver.d.ts +19 -0
- package/dist/cli/create-group-resolver.d.ts.map +1 -1
- package/dist/cli/create-group-resolver.js +30 -0
- package/dist/cli/create-group-resolver.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +27 -6
- 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/services/group-creator.d.ts +8 -0
- package/dist/services/group-creator.d.ts.map +1 -1
- package/dist/services/group-creator.js +42 -0
- package/dist/services/group-creator.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 +60 -0
- package/dist/utils/input-gate.d.ts.map +1 -1
- package/dist/utils/input-gate.js +60 -0
- 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 +114 -23
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
package/dist/utils/input-gate.js
CHANGED
|
@@ -24,6 +24,24 @@ export function shouldWriteNow(state) {
|
|
|
24
24
|
// Type-ahead is only safe after the TUI has booted at least once.
|
|
25
25
|
return state.supportsTypeAhead && !state.awaitingFirstPrompt;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Claude runs every matching SessionStart hook in parallel and waits for all of
|
|
29
|
+
* them before it renders the real input prompt. Botmux's own hook can therefore
|
|
30
|
+
* finish while a slower project hook is still running. During the first prompt,
|
|
31
|
+
* treat the signal as an outer-selector boundary only and wait for fresh prompt
|
|
32
|
+
* evidence emitted after that boundary.
|
|
33
|
+
*
|
|
34
|
+
* Other ready-integrated CLIs (notably Hermes) emit their signal only once their
|
|
35
|
+
* prompt is usable, so their established authoritative-signal behavior stays
|
|
36
|
+
* unchanged.
|
|
37
|
+
*/
|
|
38
|
+
export function shouldWaitForPostSessionStartPromptEvidence(state) {
|
|
39
|
+
return state.isClaudeFamily
|
|
40
|
+
&& state.hasReadyPattern
|
|
41
|
+
&& state.awaitingFirstPrompt
|
|
42
|
+
&& !state.isPromptReady
|
|
43
|
+
&& !state.alreadyWaiting;
|
|
44
|
+
}
|
|
27
45
|
export function shouldReleaseFirstPromptTimeout(state) {
|
|
28
46
|
if (!state.deferFirstPromptTimeoutUntilReady)
|
|
29
47
|
return true;
|
|
@@ -31,4 +49,46 @@ export function shouldReleaseFirstPromptTimeout(state) {
|
|
|
31
49
|
return true;
|
|
32
50
|
return state.elapsedMs >= state.hardTimeoutMs;
|
|
33
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* After the ready-gate releases (SessionStart/direct-ready signal OR the timeout
|
|
54
|
+
* fallback), the worker settles for PTY quiescence and then decides whether to
|
|
55
|
+
* mark the prompt ready (which flushes for ALL adapters) vs. just calling
|
|
56
|
+
* flushPending() (which only flushes for type-ahead adapters). Marking ready is
|
|
57
|
+
* correct when ANY of these hold:
|
|
58
|
+
* - promptReadyAfterSettle — an authoritative direct ready command
|
|
59
|
+
* fired (Hermes). Claude passes false here
|
|
60
|
+
* and waits for post-hook PTY evidence.
|
|
61
|
+
* - promptReadyDetectedDuringSettle — the idle detector fired during the
|
|
62
|
+
* settle (a readyPattern/idle proved readiness).
|
|
63
|
+
* - readyPatternSeenDuringHold — a readyPattern fired WHILE the gate was
|
|
64
|
+
* holding (markPromptReady was blocked by
|
|
65
|
+
* readyGate.shouldHold()). The input box
|
|
66
|
+
* exists; the gate only deferred delivery.
|
|
67
|
+
*
|
|
68
|
+
* Pins the Hermes regression: a non-type-ahead adapter that renders its prompt
|
|
69
|
+
* (❯) during the hold but never fires the SessionStart signal must be marked
|
|
70
|
+
* ready at settle — otherwise settle calls flushPending(), which bails on
|
|
71
|
+
* !isPromptReady && !typeAheadAllowed and leaves the first message queued until
|
|
72
|
+
* the hard timeout (and, before the hard-timeout fix, forever).
|
|
73
|
+
*/
|
|
74
|
+
export function decideSettleMarkReady(state) {
|
|
75
|
+
return state.promptReadyAfterSettle || state.promptReadyDetectedDuringSettle || state.readyPatternSeenDuringHold;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* At the first-prompt hard timeout the worker has waited the full cap. For
|
|
79
|
+
* type-ahead adapters flushPending() drains the queue even while !isPromptReady
|
|
80
|
+
* (the TUI parks input in its own queue). For non-type-ahead adapters
|
|
81
|
+
* flushPending() bails on !isPromptReady && !typeAheadAllowed, so the worker
|
|
82
|
+
* must mark the prompt ready first (markPromptReady() then flushes).
|
|
83
|
+
* Returns the action the worker must take:
|
|
84
|
+
* - 'flush' — call flushPending() (type-ahead adapters).
|
|
85
|
+
* - 'mark-ready' — call markPromptReady() (non-type-ahead adapters).
|
|
86
|
+
*
|
|
87
|
+
* Pins the regression where non-type-ahead adapters only logged "forcing
|
|
88
|
+
* queued message flush" at the hard timeout without actually delivering the
|
|
89
|
+
* held first message.
|
|
90
|
+
*/
|
|
91
|
+
export function decideHardTimeoutAction(supportsTypeAhead) {
|
|
92
|
+
return supportsTypeAhead ? 'flush' : 'mark-ready';
|
|
93
|
+
}
|
|
34
94
|
//# sourceMappingURL=input-gate.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"input-gate.js","sourceRoot":"","sources":["../../src/utils/input-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,cAAc,CAAC,KAS9B;IACC,IAAI,KAAK,CAAC,aAAa,IAAI,KAAK,CAAC,UAAU;QAAE,OAAO,IAAI,CAAC;IACzD,kEAAkE;IAClE,OAAO,KAAK,CAAC,iBAAiB,IAAI,CAAC,KAAK,CAAC,mBAAmB,CAAC;AAC/D,CAAC;AAED,MAAM,UAAU,+BAA+B,CAAC,KAS/C;IACC,IAAI,CAAC,KAAK,CAAC,iCAAiC;QAAE,OAAO,IAAI,CAAC;IAC1D,IAAI,CAAC,KAAK,CAAC,eAAe;QAAE,OAAO,IAAI,CAAC;IACxC,OAAO,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,aAAa,CAAC;AAChD,CAAC"}
|
|
1
|
+
{"version":3,"file":"input-gate.js","sourceRoot":"","sources":["../../src/utils/input-gate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,cAAc,CAAC,KAS9B;IACC,IAAI,KAAK,CAAC,aAAa,IAAI,KAAK,CAAC,UAAU;QAAE,OAAO,IAAI,CAAC;IACzD,kEAAkE;IAClE,OAAO,KAAK,CAAC,iBAAiB,IAAI,CAAC,KAAK,CAAC,mBAAmB,CAAC;AAC/D,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,2CAA2C,CAAC,KAM3D;IACC,OAAO,KAAK,CAAC,cAAc;WACtB,KAAK,CAAC,eAAe;WACrB,KAAK,CAAC,mBAAmB;WACzB,CAAC,KAAK,CAAC,aAAa;WACpB,CAAC,KAAK,CAAC,cAAc,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,+BAA+B,CAAC,KAS/C;IACC,IAAI,CAAC,KAAK,CAAC,iCAAiC;QAAE,OAAO,IAAI,CAAC;IAC1D,IAAI,CAAC,KAAK,CAAC,eAAe;QAAE,OAAO,IAAI,CAAC;IACxC,OAAO,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,aAAa,CAAC;AAChD,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAIrC;IACC,OAAO,KAAK,CAAC,sBAAsB,IAAI,KAAK,CAAC,+BAA+B,IAAI,KAAK,CAAC,0BAA0B,CAAC;AACnH,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,uBAAuB,CAAC,iBAA0B;IAChE,OAAO,iBAAiB,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,YAAY,CAAC;AACpD,CAAC"}
|
|
@@ -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/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 { 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. */
|
|
@@ -837,19 +836,40 @@ let isSettlingFirstFlush = false;
|
|
|
837
836
|
* ready yet, or flushPending will be blocked by isSettlingFirstFlush and a
|
|
838
837
|
* later markPromptReady call would return early with the first prompt stranded. */
|
|
839
838
|
let promptReadyDetectedDuringSettle = false;
|
|
839
|
+
/** While the ready-gate is holding, the IdleDetector may still fire on a real
|
|
840
|
+
* readyPattern (e.g. Hermes's ❯) — proving the input box exists — but
|
|
841
|
+
* markPromptReady() returns early because the gate is armed. Record that the
|
|
842
|
+
* pattern was seen so the gate's timeout-fallback settle can mark the prompt
|
|
843
|
+
* ready immediately instead of delivering into a !isPromptReady state that
|
|
844
|
+
* flushPending() rejects for non-type-ahead adapters. Without this, a Hermes
|
|
845
|
+
* spawn that renders ❯ but never fires BOTMUX_READY_COMMAND waits the full
|
|
846
|
+
* hard timeout (and previously never delivered at all). */
|
|
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;
|
|
840
855
|
/** Wait until the PTY has been quiet for READY_FLUSH_SETTLE_MS (Ink render
|
|
841
856
|
* drained), capped at READY_FLUSH_SETTLE_CAP_MS, then flush the held prompt.
|
|
842
|
-
*
|
|
843
|
-
*
|
|
844
|
-
*
|
|
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. */
|
|
845
860
|
function settleThenFlush(startedAtMs, promptReadyAfterSettle) {
|
|
846
861
|
readyFlushSettleTimer = null;
|
|
847
862
|
const now = Date.now();
|
|
848
863
|
const quietForMs = now - lastPtyOutputAtMs;
|
|
849
864
|
if (quietForMs >= READY_FLUSH_SETTLE_MS || now - startedAtMs >= READY_FLUSH_SETTLE_CAP_MS) {
|
|
850
865
|
isSettlingFirstFlush = false;
|
|
851
|
-
const shouldMarkPromptReady =
|
|
866
|
+
const shouldMarkPromptReady = decideSettleMarkReady({
|
|
867
|
+
promptReadyAfterSettle,
|
|
868
|
+
promptReadyDetectedDuringSettle,
|
|
869
|
+
readyPatternSeenDuringHold,
|
|
870
|
+
});
|
|
852
871
|
promptReadyDetectedDuringSettle = false;
|
|
872
|
+
readyPatternSeenDuringHold = false;
|
|
853
873
|
log(`Ready-gate settle done (quiet ${quietForMs}ms); ${shouldMarkPromptReady ? 'marking prompt ready' : 'delivering held first prompt'}`);
|
|
854
874
|
if (shouldMarkPromptReady) {
|
|
855
875
|
markPromptReady();
|
|
@@ -4513,6 +4533,15 @@ function releaseRawInputRestartGate() {
|
|
|
4513
4533
|
rawInputRestartGate = false;
|
|
4514
4534
|
log('Replacement CLI prompt ready — releasing deferred passthrough commands');
|
|
4515
4535
|
}
|
|
4536
|
+
function markPromptReadyFromPty() {
|
|
4537
|
+
postSessionStartPromptEvidenceInFlight = true;
|
|
4538
|
+
try {
|
|
4539
|
+
markPromptReady();
|
|
4540
|
+
}
|
|
4541
|
+
finally {
|
|
4542
|
+
postSessionStartPromptEvidenceInFlight = false;
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4516
4545
|
function markPromptReady() {
|
|
4517
4546
|
if (isPromptReady)
|
|
4518
4547
|
return; // guard against duplicate calls
|
|
@@ -4524,9 +4553,22 @@ function markPromptReady() {
|
|
|
4524
4553
|
// releaseReadyGate() drives flushPending() once the real signal lands, and a
|
|
4525
4554
|
// later genuine idle then runs this fully. No-op for non-armed gates.
|
|
4526
4555
|
if (readyGate.shouldHold()) {
|
|
4556
|
+
// A real readyPattern fired while the gate was holding — the input box
|
|
4557
|
+
// exists. Remember it so the gate's timeout-fallback settle can mark the
|
|
4558
|
+
// prompt ready (see settleThenFlush) instead of letting flushPending()
|
|
4559
|
+
// reject the held message for non-type-ahead adapters.
|
|
4560
|
+
readyPatternSeenDuringHold = true;
|
|
4527
4561
|
log('Idle detected but holding for SessionStart ready signal (startup selector guard)');
|
|
4528
4562
|
return;
|
|
4529
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
|
+
}
|
|
4530
4572
|
if (isSettlingFirstFlush) {
|
|
4531
4573
|
promptReadyDetectedDuringSettle = true;
|
|
4532
4574
|
log('Idle detected during ready-gate settle; deferring prompt-ready until settle completes');
|
|
@@ -4558,6 +4600,7 @@ function markPromptReady() {
|
|
|
4558
4600
|
maybeEmitWorkflowTranscriptOutput();
|
|
4559
4601
|
if (awaitingFirstPrompt) {
|
|
4560
4602
|
awaitingFirstPrompt = false;
|
|
4603
|
+
awaitingPostSessionStartPromptEvidence = false;
|
|
4561
4604
|
renderer?.markNewTurn(); // exclude history replay from streaming card
|
|
4562
4605
|
}
|
|
4563
4606
|
send({ type: 'prompt_ready' });
|
|
@@ -4897,6 +4940,10 @@ async function flushPending() {
|
|
|
4897
4940
|
log(`Holding ${pendingMessages.length} pending message(s) until ready-gate settle completes`);
|
|
4898
4941
|
return;
|
|
4899
4942
|
}
|
|
4943
|
+
if (awaitingPostSessionStartPromptEvidence) {
|
|
4944
|
+
log(`Holding ${pendingMessages.length} pending message(s) until post-SessionStart prompt evidence`);
|
|
4945
|
+
return;
|
|
4946
|
+
}
|
|
4900
4947
|
// Type-ahead adapters flush even while the CLI is busy; others wait for
|
|
4901
4948
|
// idle. Claude bridge fallback used to also disable type-ahead because
|
|
4902
4949
|
// BridgeTurnQueue.ingest didn't recognise the `attachment(queued_command)`
|
|
@@ -7349,6 +7396,8 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
7349
7396
|
}
|
|
7350
7397
|
isSettlingFirstFlush = false;
|
|
7351
7398
|
promptReadyDetectedDuringSettle = false;
|
|
7399
|
+
readyPatternSeenDuringHold = false;
|
|
7400
|
+
awaitingPostSessionStartPromptEvidence = false;
|
|
7352
7401
|
// Reset quiescence baseline so the settle measures silence from THIS spawn.
|
|
7353
7402
|
lastPtyOutputAtMs = Date.now();
|
|
7354
7403
|
const readyHookAvailable = effectiveReadyHookInstall
|
|
@@ -7391,7 +7440,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
7391
7440
|
// quiescence, repeatedly triggering markPromptReady() and duplicate cards.
|
|
7392
7441
|
if (effectiveBackendType !== 'riff') {
|
|
7393
7442
|
idleDetector = new IdleDetector(cliAdapter);
|
|
7394
|
-
idleDetector.onIdle(async () => {
|
|
7443
|
+
idleDetector.onIdle(async (evidenceSource) => {
|
|
7395
7444
|
log('Prompt detected (idle)');
|
|
7396
7445
|
// Bridge drain MUST run before markPromptReady() — the latter calls
|
|
7397
7446
|
// flushPending() which can immediately fire the next queued message
|
|
@@ -7413,7 +7462,12 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
7413
7462
|
log(`Codex bridge emit error: ${err.message}`);
|
|
7414
7463
|
}
|
|
7415
7464
|
}
|
|
7416
|
-
|
|
7465
|
+
if (evidenceSource === 'screen') {
|
|
7466
|
+
markPromptReadyFromPty();
|
|
7467
|
+
}
|
|
7468
|
+
else {
|
|
7469
|
+
markPromptReady();
|
|
7470
|
+
}
|
|
7417
7471
|
});
|
|
7418
7472
|
}
|
|
7419
7473
|
backend.onData(onPtyData);
|
|
@@ -7562,6 +7616,7 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
7562
7616
|
return;
|
|
7563
7617
|
}
|
|
7564
7618
|
awaitingFirstPrompt = false;
|
|
7619
|
+
awaitingPostSessionStartPromptEvidence = false;
|
|
7565
7620
|
renderer?.markNewTurn();
|
|
7566
7621
|
log(forced
|
|
7567
7622
|
? `WARN First prompt hard timeout — ${cliName()} readyPattern did not arrive; forcing queued message flush`
|
|
@@ -7574,8 +7629,23 @@ async function spawnCli(cfg, opts = {}) {
|
|
|
7574
7629
|
// invoking markPromptReady() would claim the CLI is idle while it's still
|
|
7575
7630
|
// mid-boot, so flushPending() alone is safer — it respects typeAheadAllowed
|
|
7576
7631
|
// and drains pendingMessages now.
|
|
7577
|
-
|
|
7632
|
+
//
|
|
7633
|
+
// Non-type-ahead adapters (Hermes etc.) flushPending() rejects the held
|
|
7634
|
+
// message while isPromptReady is false — it bails on
|
|
7635
|
+
// `!isPromptReady && !typeAheadAllowed`. The hard cap means we've waited
|
|
7636
|
+
// long enough. By now the ready gate's 45s fallback has already released
|
|
7637
|
+
// the gate (READY_SIGNAL_TIMEOUT_MS < this 90s hard cap) and the post-
|
|
7638
|
+
// release settle has drained, so markPromptReady() proceeds: it sets
|
|
7639
|
+
// isPromptReady and drains the held first prompt. Without this, a spawn
|
|
7640
|
+
// that never fires the ready signal (and whose readyPattern the idle
|
|
7641
|
+
// detector never matched) would hold the first queued message forever —
|
|
7642
|
+
// the previous code only logged "forcing flush" without actually flushing
|
|
7643
|
+
// for non-type-ahead adapters.
|
|
7644
|
+
if (decideHardTimeoutAction(cliAdapter?.supportsTypeAhead === true) === 'flush') {
|
|
7578
7645
|
flushPending();
|
|
7646
|
+
return;
|
|
7647
|
+
}
|
|
7648
|
+
markPromptReady();
|
|
7579
7649
|
};
|
|
7580
7650
|
setTimeout(() => releaseFirstPromptTimeout(FIRST_PROMPT_TIMEOUT_MS, false), FIRST_PROMPT_TIMEOUT_MS);
|
|
7581
7651
|
// Riff (and other remote HTTP backends) have no local boot process — the
|
|
@@ -7607,6 +7677,8 @@ function killCli(opts = {}) {
|
|
|
7607
7677
|
}
|
|
7608
7678
|
isSettlingFirstFlush = false;
|
|
7609
7679
|
promptReadyDetectedDuringSettle = false;
|
|
7680
|
+
readyPatternSeenDuringHold = false;
|
|
7681
|
+
awaitingPostSessionStartPromptEvidence = false;
|
|
7610
7682
|
stopScreenAnalyzer();
|
|
7611
7683
|
stopScreenUpdates();
|
|
7612
7684
|
backend?.kill();
|
|
@@ -9418,24 +9490,43 @@ process.on('message', async (raw) => {
|
|
|
9418
9490
|
break;
|
|
9419
9491
|
}
|
|
9420
9492
|
case 'session_ready': {
|
|
9421
|
-
// Claude-family SessionStart
|
|
9422
|
-
//
|
|
9423
|
-
//
|
|
9424
|
-
//
|
|
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.
|
|
9425
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
|
+
}
|
|
9426
9514
|
// 先记下 gate 是否已被 45s fallback 释放:ReadyGate.receive() 是一次性
|
|
9427
9515
|
// 语义,fallback 抢先后 releaseReadyGate 会整块跳过迟到的真信号。
|
|
9428
9516
|
const lateAfterFallback = readyGate.isArmed && readyGate.isReceived;
|
|
9429
|
-
releaseReadyGate('SessionStart hook', { promptReadyAfterSettle:
|
|
9517
|
+
releaseReadyGate('SessionStart hook', { promptReadyAfterSettle: !waitForPostHookPrompt });
|
|
9430
9518
|
// 冷启动超过 READY_SIGNAL_TIMEOUT_MS 的 CLI(Hermes 常态是 2-3 分钟)恰好
|
|
9431
9519
|
// 总落在 fallback 之后:fallback 只开闸不投递(非 type-ahead 的
|
|
9432
9520
|
// flushPending 是 no-op),真信号依然是权威就绪,这里直接兑现。仅限首轮
|
|
9433
9521
|
// (awaitingFirstPrompt)——首条 prompt 交付后 clear/compact 来源的
|
|
9434
9522
|
// SessionStart 保持原有 no-op 语义,绝不在会话中途误标就绪。
|
|
9435
|
-
if (lateAfterFallback && awaitingFirstPrompt && !isPromptReady) {
|
|
9523
|
+
if (lateAfterFallback && awaitingFirstPrompt && !isPromptReady && !waitForPostHookPrompt) {
|
|
9436
9524
|
log('Late ready signal after timeout fallback — marking prompt ready now');
|
|
9437
9525
|
markPromptReady();
|
|
9438
9526
|
}
|
|
9527
|
+
if (msg.requestId) {
|
|
9528
|
+
send({ type: 'session_ready_ack', requestId: msg.requestId });
|
|
9529
|
+
}
|
|
9439
9530
|
break;
|
|
9440
9531
|
}
|
|
9441
9532
|
case 'set_display_mode': {
|