botmux 3.2.0 → 3.2.1
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/README.en.md +1 -0
- package/README.md +1 -0
- package/dist/adapters/cli/codex.d.ts.map +1 -1
- package/dist/adapters/cli/codex.js +13 -1
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/adapters/cli/traex.d.ts.map +1 -1
- package/dist/adapters/cli/traex.js +11 -1
- package/dist/adapters/cli/traex.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +6 -0
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/bot-registry.d.ts +7 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +1 -0
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli/daemon-lifecycle-env.d.ts +11 -0
- package/dist/cli/daemon-lifecycle-env.d.ts.map +1 -0
- package/dist/cli/daemon-lifecycle-env.js +25 -0
- package/dist/cli/daemon-lifecycle-env.js.map +1 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +11 -4
- package/dist/cli.js.map +1 -1
- package/dist/codex-rpc-engine.d.ts +110 -0
- package/dist/codex-rpc-engine.d.ts.map +1 -0
- package/dist/codex-rpc-engine.js +496 -0
- package/dist/codex-rpc-engine.js.map +1 -0
- package/dist/codex-rpc-lifecycle.d.ts +157 -0
- package/dist/codex-rpc-lifecycle.d.ts.map +1 -0
- package/dist/codex-rpc-lifecycle.js +210 -0
- package/dist/codex-rpc-lifecycle.js.map +1 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +14 -2
- package/dist/config.js.map +1 -1
- package/dist/core/attachment-path.d.ts +3 -0
- package/dist/core/attachment-path.d.ts.map +1 -0
- package/dist/core/attachment-path.js +8 -0
- package/dist/core/attachment-path.js.map +1 -0
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +6 -1
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/dashboard-images.d.ts +29 -0
- package/dist/core/dashboard-images.d.ts.map +1 -0
- package/dist/core/dashboard-images.js +113 -0
- package/dist/core/dashboard-images.js.map +1 -0
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +13 -1
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/github-auth.d.ts +5 -0
- package/dist/core/github-auth.d.ts.map +1 -1
- package/dist/core/github-auth.js +38 -0
- package/dist/core/github-auth.js.map +1 -1
- package/dist/core/maintenance.d.ts +1 -0
- package/dist/core/maintenance.d.ts.map +1 -1
- package/dist/core/maintenance.js +9 -1
- package/dist/core/maintenance.js.map +1 -1
- package/dist/core/session-create.d.ts +10 -0
- package/dist/core/session-create.d.ts.map +1 -1
- package/dist/core/session-create.js +21 -0
- package/dist/core/session-create.js.map +1 -1
- package/dist/core/session-manager.d.ts +4 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +25 -13
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/trigger-session.d.ts +3 -0
- package/dist/core/trigger-session.d.ts.map +1 -1
- package/dist/core/trigger-session.js +17 -3
- package/dist/core/trigger-session.js.map +1 -1
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +2 -0
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +19 -3
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/connector-api.d.ts.map +1 -1
- package/dist/dashboard/connector-api.js +19 -0
- package/dist/dashboard/connector-api.js.map +1 -1
- package/dist/dashboard/settings-write-applier.d.ts +2 -1
- package/dist/dashboard/settings-write-applier.d.ts.map +1 -1
- package/dist/dashboard/settings-write-applier.js +6 -0
- package/dist/dashboard/settings-write-applier.js.map +1 -1
- package/dist/dashboard/web/connectors-page.d.ts.map +1 -1
- package/dist/dashboard/web/connectors-page.js +23 -2
- package/dist/dashboard/web/connectors-page.js.map +1 -1
- package/dist/dashboard/web/create-session-composer.d.ts +35 -0
- package/dist/dashboard/web/create-session-composer.d.ts.map +1 -0
- package/dist/dashboard/web/create-session-composer.js +70 -0
- package/dist/dashboard/web/create-session-composer.js.map +1 -0
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +65 -5
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard/web/sessions-page.d.ts.map +1 -1
- package/dist/dashboard/web/sessions-page.js +147 -1
- package/dist/dashboard/web/sessions-page.js.map +1 -1
- package/dist/dashboard/web/settings-page.d.ts.map +1 -1
- package/dist/dashboard/web/settings-page.js +2 -1
- package/dist/dashboard/web/settings-page.js.map +1 -1
- package/dist/dashboard/web/skills-page.d.ts.map +1 -1
- package/dist/dashboard/web/skills-page.js +5 -2
- package/dist/dashboard/web/skills-page.js.map +1 -1
- package/dist/dashboard/webhook-routes.d.ts +4 -0
- package/dist/dashboard/webhook-routes.d.ts.map +1 -1
- package/dist/dashboard/webhook-routes.js +19 -0
- package/dist/dashboard/webhook-routes.js.map +1 -1
- package/dist/dashboard-web/app.js +1 -1
- package/dist/dashboard-web/chunks/{bot-defaults-page-4PM4T6QQ.js → bot-defaults-page-O37SAQD5.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-RXVA6XW3.js → chunk-6QHMLMIM.js} +6 -6
- package/dist/dashboard-web/chunks/{chunk-IKCZ677K.js → chunk-B42PCL5P.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-3PMN55Z6.js → chunk-FYALHCPG.js} +1 -1
- package/dist/dashboard-web/chunks/{chunk-AIDN2WZB.js → chunk-O2DPQL7C.js} +1 -1
- package/dist/dashboard-web/chunks/connectors-page-DZBQ42MH.js +1 -0
- package/dist/dashboard-web/chunks/{groups-page-TAU2UIVS.js → groups-page-3IEGRZKQ.js} +1 -1
- package/dist/dashboard-web/chunks/{insights-page-PJ2XVM6T.js → insights-page-X3ADRON3.js} +1 -1
- package/dist/dashboard-web/chunks/{monitor-room-D6445HJO.js → monitor-room-IV36RGQY.js} +1 -1
- package/dist/dashboard-web/chunks/{monitoring-page-GUHMCKSO.js → monitoring-page-ADFKVON4.js} +1 -1
- package/dist/dashboard-web/chunks/{office-page-6K6U2MS5.js → office-page-CALAOWNS.js} +1 -1
- package/dist/dashboard-web/chunks/{overview-page-USDYU7U5.js → overview-page-SLMGLK3M.js} +1 -1
- package/dist/dashboard-web/chunks/{roles-page-O4OUA7ZH.js → roles-page-L7JJ4FFV.js} +1 -1
- package/dist/dashboard-web/chunks/{schedules-page-LNIBXJSZ.js → schedules-page-Z3672POL.js} +1 -1
- package/dist/dashboard-web/chunks/sessions-page-OKJ5YWUZ.js +3 -0
- package/dist/dashboard-web/chunks/settings-page-P6MI2ZUE.js +2 -0
- package/dist/dashboard-web/chunks/{skills-page-M5KYNXBA.js → skills-page-FT7BVZ6X.js} +4 -4
- package/dist/dashboard-web/chunks/{team-federation-page-GT7UFQMN.js → team-federation-page-DEYCPUFB.js} +1 -1
- package/dist/dashboard-web/chunks/{v3-page-UYZIRJ2C.js → v3-page-4IOWREZG.js} +1 -1
- package/dist/dashboard-web/chunks/{whiteboards-page-WAR77DQE.js → whiteboards-page-2VMBETZH.js} +1 -1
- package/dist/dashboard-web/style.css +191 -0
- package/dist/dashboard.js +33 -6
- package/dist/dashboard.js.map +1 -1
- package/dist/global-config.d.ts +7 -0
- package/dist/global-config.d.ts.map +1 -1
- package/dist/global-config.js +2 -0
- package/dist/global-config.js.map +1 -1
- package/dist/im/lark/ask-card.d.ts.map +1 -1
- package/dist/im/lark/ask-card.js +52 -4
- package/dist/im/lark/ask-card.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts +2 -0
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +19 -2
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts +3 -0
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +706 -693
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/grant-pending.d.ts +8 -4
- package/dist/im/lark/grant-pending.d.ts.map +1 -1
- package/dist/im/lark/grant-pending.js +13 -6
- package/dist/im/lark/grant-pending.js.map +1 -1
- package/dist/services/connector-store.d.ts +8 -0
- package/dist/services/connector-store.d.ts.map +1 -1
- package/dist/services/connector-store.js.map +1 -1
- package/dist/services/session-store.d.ts +2 -1
- package/dist/services/session-store.d.ts.map +1 -1
- package/dist/services/session-store.js +60 -2
- package/dist/services/session-store.js.map +1 -1
- package/dist/services/skill-registry-store.d.ts +16 -0
- package/dist/services/skill-registry-store.d.ts.map +1 -1
- package/dist/services/skill-registry-store.js +343 -47
- package/dist/services/skill-registry-store.js.map +1 -1
- package/dist/services/trigger-types.d.ts +5 -0
- package/dist/services/trigger-types.d.ts.map +1 -1
- package/dist/services/trigger-types.js +12 -0
- package/dist/services/trigger-types.js.map +1 -1
- package/dist/types.d.ts +13 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/worker.js +440 -34
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
- package/dist/dashboard-web/chunks/connectors-page-2MKF4YTN.js +0 -1
- package/dist/dashboard-web/chunks/sessions-page-B6U6TCFF.js +0 -3
- package/dist/dashboard-web/chunks/settings-page-PXLRFF7I.js +0 -2
package/dist/worker.js
CHANGED
|
@@ -74,6 +74,8 @@ import { createCliAdapterSync, locateOnPath } from './adapters/cli/registry.js';
|
|
|
74
74
|
import { buildWrappedLaunch, parseWrapperCli, isTtadkWrapper } from './setup/cli-selection.js';
|
|
75
75
|
import { cliUnavailableMessage } from './setup/cli-availability.js';
|
|
76
76
|
import { findLaunchedCliPid, scheduleWrapperRealCliPid, readComm, isBareShellComm, bareShellLaunchKind } from './core/session-discovery.js';
|
|
77
|
+
import { codexRpcEligible, paneRunsRemoteTui, orchestrateCodexRpcInit, rolloutUserTurnMatches, decideStartupDialogAction, shouldQueueInitialPrompt, shouldPreMarkFirstTurn, killAndVerifyPersistentPane } from './codex-rpc-lifecycle.js';
|
|
78
|
+
import { delay } from './utils/timing.js';
|
|
77
79
|
import { claudeJsonlPathForSession, resolveJsonlFromPid, findOpenClaudeSessionIds, syncClaudeResumeTargetToCwd, DEFAULT_CLAUDE_DATA_DIR } from './adapters/cli/claude-code.js';
|
|
78
80
|
import { sessionReadyHookCommand } from './adapters/hook-command.js';
|
|
79
81
|
import { mtrSessionIdForBotmuxSession } from './adapters/cli/mtr.js';
|
|
@@ -111,10 +113,273 @@ import { withCodexAppContext } from './utils/codex-app-context.js';
|
|
|
111
113
|
import { resolveCodexAppFinalTurnIdentity } from './adapters/cli/codex-app-turn.js';
|
|
112
114
|
import { RunnerControlDecoder } from './adapters/cli/runner-control-channel.js';
|
|
113
115
|
import { hasMatchingManagedOriginCapability, managedOriginCapabilityPath, RELAY_ORIGIN_CAPABILITY_BASENAME, replaceManagedOriginCapabilityFile, } from './core/managed-origin-capability.js';
|
|
116
|
+
import { CodexRpcEngine } from './codex-rpc-engine.js';
|
|
114
117
|
// ─── State ───────────────────────────────────────────────────────────────────
|
|
115
118
|
let cliAdapter = null;
|
|
116
119
|
let backend = null;
|
|
117
120
|
let intentionalRestartBackend = null;
|
|
121
|
+
// Hybrid codex RPC input mode (opt-in per bot, cfg.codexRpcInput). When active,
|
|
122
|
+
// user input is delivered to codex via the app-server JSON-RPC channel
|
|
123
|
+
// (turn/start) instead of a tmux paste, and the pane runs `codex --remote`
|
|
124
|
+
// attached to this engine's thread. All three are set together or none.
|
|
125
|
+
let codexRpcEngine;
|
|
126
|
+
let remoteWsUrl;
|
|
127
|
+
let remoteThreadId;
|
|
128
|
+
let rpcDialogDismissTimer = null;
|
|
129
|
+
let rpcEnginePidMarker = null;
|
|
130
|
+
function stopCodexRpcEngine() {
|
|
131
|
+
const engine = codexRpcEngine;
|
|
132
|
+
codexRpcEngine = undefined;
|
|
133
|
+
remoteWsUrl = undefined;
|
|
134
|
+
remoteThreadId = undefined;
|
|
135
|
+
clearRpcEnginePidMarker();
|
|
136
|
+
try {
|
|
137
|
+
engine?.stop();
|
|
138
|
+
}
|
|
139
|
+
catch { /* best effort */ }
|
|
140
|
+
}
|
|
141
|
+
/** Resolve the persistent pane name + liveness for a backend type, mirroring
|
|
142
|
+
* spawnCli's willReattachPersistent probe (worker.ts) but callable from the init
|
|
143
|
+
* handler BEFORE spawnCli runs (effectiveBackendType is stale there). A wrong
|
|
144
|
+
* guess is safe: an assumed-tmux session that is really pty just has no live
|
|
145
|
+
* session → treated as fresh. Returns null for non-persistent backends. */
|
|
146
|
+
function persistentPaneInfo(backendType, sessionId) {
|
|
147
|
+
let name;
|
|
148
|
+
if (backendType === 'tmux')
|
|
149
|
+
name = TmuxBackend.sessionName(sessionId);
|
|
150
|
+
else if (backendType === 'herdr')
|
|
151
|
+
name = HerdrBackend.sessionName(sessionId);
|
|
152
|
+
else if (backendType === 'zellij')
|
|
153
|
+
name = ZellijBackend.sessionName(sessionId);
|
|
154
|
+
if (!name)
|
|
155
|
+
return null;
|
|
156
|
+
const live = backendType === 'tmux' ? TmuxBackend.hasSession(name)
|
|
157
|
+
: backendType === 'zellij' ? ZellijBackend.hasSession(name)
|
|
158
|
+
: HerdrBackend.hasSession(name);
|
|
159
|
+
return { name, live };
|
|
160
|
+
}
|
|
161
|
+
function persistentPaneLive(backendType, name) {
|
|
162
|
+
if (backendType === 'tmux')
|
|
163
|
+
return TmuxBackend.hasSession(name);
|
|
164
|
+
if (backendType === 'zellij')
|
|
165
|
+
return ZellijBackend.hasSession(name);
|
|
166
|
+
if (backendType === 'herdr')
|
|
167
|
+
return HerdrBackend.hasSession(name);
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
/** Kill a persistent session and VERIFY it is gone (bounded retry). killSession
|
|
171
|
+
* swallows exceptions and killPersistentSession returns void, so a plain
|
|
172
|
+
* try/catch can never observe a failed kill — and a surviving (dead-`--remote`)
|
|
173
|
+
* pane would then be reattached against the NEW app-server's fresh port,
|
|
174
|
+
* re-triggering the exact P0 freeze. Returns true only when the session is
|
|
175
|
+
* confirmed absent (Codex delta point 1). */
|
|
176
|
+
async function killPersistentSessionVerified(backendType, name) {
|
|
177
|
+
return killAndVerifyPersistentPane(name, {
|
|
178
|
+
kill: (resolvedName) => killPersistentSession(backendType, resolvedName),
|
|
179
|
+
isLive: (resolvedName) => persistentPaneLive(backendType, resolvedName),
|
|
180
|
+
wait: delay,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
/** Poll the codex/traex rollout for POSITIVE evidence that the fresh first turn's
|
|
184
|
+
* user message was persisted (P1-1). Used only to resolve a dispatched-but-
|
|
185
|
+
* unacked first turn: a hit means the app-server accepted the turn → 'accepted'
|
|
186
|
+
* (never resend); no hit within the window stays 'ambiguous' (NOT downgraded to
|
|
187
|
+
* safe). A wrong sessions root (custom CODEX_HOME) simply fails to find → stays
|
|
188
|
+
* ambiguous, which is the safe direction. */
|
|
189
|
+
async function codexRolloutProbe(cliId, threadId, promptText, timeoutMs) {
|
|
190
|
+
const findPath = cliId === 'traex' ? findTraexRolloutBySessionId : findCodexRolloutBySessionId;
|
|
191
|
+
const deadline = Date.now() + timeoutMs;
|
|
192
|
+
do {
|
|
193
|
+
try {
|
|
194
|
+
const path = findPath(threadId);
|
|
195
|
+
if (path) {
|
|
196
|
+
const { events } = drainCodexRollout(path, 0);
|
|
197
|
+
if (rolloutUserTurnMatches(events, promptText))
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch { /* keep polling */ }
|
|
202
|
+
await delay(400);
|
|
203
|
+
} while (Date.now() < deadline);
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
/** Stand up (or re-establish) the per-session codex app-server + botmux-owned
|
|
207
|
+
* thread and point remote{WsUrl,ThreadId} at it, so the next spawnCli launches
|
|
208
|
+
* `codex --remote <ws> resume <thread>` and input flows over JSON-RPC. Fully
|
|
209
|
+
* fail-closed: on ANY failure the LOCAL engine is stopped and the vars cleared,
|
|
210
|
+
* so spawnCli falls back to paste (boundary #1 — nothing else is touched).
|
|
211
|
+
* Rebuilds from scratch each call (tears down a prior engine) so every
|
|
212
|
+
* incarnation binds the TUI to the CURRENT app-server (a fresh port each time),
|
|
213
|
+
* never a dead one — called from init AND the in-worker restart path (P1-3b).
|
|
214
|
+
*
|
|
215
|
+
* Ordering: engine.start() (/readyz) AND thread start/resume BOTH complete
|
|
216
|
+
* before returning true. For a FRESH session the first turn is ALSO sent here —
|
|
217
|
+
* an empty thread has no rollout, so `--remote resume` can't attach to it
|
|
218
|
+
* (verified), hence the first turn must persist the rollout BEFORE the pane
|
|
219
|
+
* spawns; it renders as history and later turns stream live. "thread ready" is
|
|
220
|
+
* thus a distinct step from "first turn sent". */
|
|
221
|
+
async function engageCodexRpc(cfg) {
|
|
222
|
+
if (!codexRpcEligible(cfg, { sandboxForced: sandboxEnabled() }))
|
|
223
|
+
return 'not-engaged';
|
|
224
|
+
const wantResume = cfg.resume === true && !!cfg.cliSessionId;
|
|
225
|
+
stopCodexRpcEngine();
|
|
226
|
+
let engine;
|
|
227
|
+
try {
|
|
228
|
+
const cliBin = createCliAdapterSync(cfg.cliId, cfg.cliPathOverride).resolvedBin;
|
|
229
|
+
const engineEnv = { ...redactChildEnv(process.env) };
|
|
230
|
+
engineEnv.PATH = `${join(homedir(), '.botmux', 'bin')}:${engineEnv.PATH ?? ''}`;
|
|
231
|
+
engineEnv.BOTMUX_SESSION_ID = cfg.sessionId;
|
|
232
|
+
engineEnv.BOTMUX_LARK_APP_ID = cfg.larkAppId;
|
|
233
|
+
// The app-server owns model execution in RPC mode. Its MCP gateway child
|
|
234
|
+
// must inherit the trusted host socket just like a native CLI process does.
|
|
235
|
+
if (sessionMcpGatewayHost) {
|
|
236
|
+
engineEnv[MCP_GATEWAY_SOCKET_ENV] = sessionMcpGatewayHost.socketPath;
|
|
237
|
+
engineEnv[MCP_GATEWAY_REQUIRED_ENV] = '1';
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
delete engineEnv[MCP_GATEWAY_SOCKET_ENV];
|
|
241
|
+
delete engineEnv[MCP_GATEWAY_REQUIRED_ENV];
|
|
242
|
+
}
|
|
243
|
+
// P1-2: the app-server is where the model actually runs, so it needs the SAME
|
|
244
|
+
// per-bot provider env (base-url + token, proxy, feature flags) spawnCli
|
|
245
|
+
// injects into the TUI — else a 3rd-party-provider bot's app-server silently
|
|
246
|
+
// falls back to the default provider. Re-sanitized (crossed IPC).
|
|
247
|
+
Object.assign(engineEnv, sanitizePerBotEnv(cfg.env));
|
|
248
|
+
engine = new CodexRpcEngine({
|
|
249
|
+
cliBin, cwd: cfg.workingDir, env: engineEnv, sessionId: cfg.sessionId,
|
|
250
|
+
model: cfg.model, log: (m) => log(m),
|
|
251
|
+
onDead: () => {
|
|
252
|
+
if (codexRpcEngine === engine) {
|
|
253
|
+
log('Codex RPC app-server died; replacing the tmux session and re-engaging the thread');
|
|
254
|
+
// failAll() rejects the active sendTurn promise immediately after this
|
|
255
|
+
// callback returns. Let that microtask classify/notify the ambiguous
|
|
256
|
+
// submit while the old backend still exists, then replace the paired
|
|
257
|
+
// viewer on the next timer turn. Restarting synchronously here nulls
|
|
258
|
+
// backend first and suppresses the submit-failure notice.
|
|
259
|
+
const restartTimer = setTimeout(() => {
|
|
260
|
+
if (codexRpcEngine !== engine)
|
|
261
|
+
return; // close/restart won the race
|
|
262
|
+
void restartCliProcess('Codex RPC app-server died', { immediate: true, preservePending: true });
|
|
263
|
+
}, 0);
|
|
264
|
+
restartTimer.unref?.();
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
await engine.start();
|
|
269
|
+
// Shell tools run under the app-server process tree, not the viewer TUI.
|
|
270
|
+
// Mark its pid before the first turn so `botmux send` resolves the current
|
|
271
|
+
// per-turn identity instead of falling back to a stale/session-only env.
|
|
272
|
+
registerRpcEnginePidMarker(engine.appServerPid);
|
|
273
|
+
const threadId = wantResume ? await engine.resumeThread(cfg.cliSessionId) : await engine.startThread();
|
|
274
|
+
let outcome = wantResume ? 'resumed' : 'accepted';
|
|
275
|
+
if (!wantResume && cfg.prompt) {
|
|
276
|
+
// Three-state delivery (P1-1, exactly-once priority): 'accepted' (ack or
|
|
277
|
+
// rollout evidence), 'not-sent' (frame never dispatched → safe paste), or
|
|
278
|
+
// 'ambiguous' (dispatched, unconfirmed → engaged but NEVER resend).
|
|
279
|
+
const first = await engine.sendFirstTurn(cfg.prompt, cfg.turnId, (tid) => codexRolloutProbe(cfg.cliId, tid, cfg.prompt, 12_000));
|
|
280
|
+
if (first === 'not-sent') {
|
|
281
|
+
// The turn/start frame never left → the turn cannot have run → tear the
|
|
282
|
+
// engine down and fall back to paste. flushPending marks the bridge once
|
|
283
|
+
// on the paste path — we must NOT pre-mark here or that would double-mark
|
|
284
|
+
// the same turnId and leave a stale, never-consumed queue head (Codex P1).
|
|
285
|
+
log('Codex RPC fresh first turn: frame not dispatched → falling back to paste (safe, single execution)');
|
|
286
|
+
clearRpcEnginePidMarker();
|
|
287
|
+
try {
|
|
288
|
+
engine.stop();
|
|
289
|
+
}
|
|
290
|
+
catch { /* best effort */ }
|
|
291
|
+
return 'not-engaged';
|
|
292
|
+
}
|
|
293
|
+
// Bridge mark ONLY for a confirmed-accepted turn — so the structured
|
|
294
|
+
// fallback can attribute the reply even if the model skips `botmux send`.
|
|
295
|
+
// Marked here (after the outcome, before persistCliSessionId/attach — late
|
|
296
|
+
// attach from offset 0 still matches, timing-safe). 'ambiguous' does NOT
|
|
297
|
+
// mark: there is no positive evidence the turn ran, and an unstarted head
|
|
298
|
+
// would permanently block every later turn's drain — the notify + Web
|
|
299
|
+
// terminal are the authoritative recovery surface (Codex P1).
|
|
300
|
+
if (shouldPreMarkFirstTurn(first))
|
|
301
|
+
codexBridgeMarkPendingTurn(cfg.prompt, cfg.turnId);
|
|
302
|
+
outcome = first; // 'accepted' | 'ambiguous' — both stay engaged, prompt never re-queued
|
|
303
|
+
}
|
|
304
|
+
persistCliSessionId(threadId);
|
|
305
|
+
codexRpcEngine = engine;
|
|
306
|
+
remoteWsUrl = engine.wsUrl;
|
|
307
|
+
remoteThreadId = threadId;
|
|
308
|
+
log(`Codex RPC input engaged (${outcome}${wantResume ? '/resume' : '/fresh'}): app-server ${engine.wsUrl} thread ${threadId}`);
|
|
309
|
+
return outcome;
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
log(`Codex RPC input failed to start (${err?.message ?? err}); falling back to paste mode`);
|
|
313
|
+
clearRpcEnginePidMarker();
|
|
314
|
+
try {
|
|
315
|
+
engine?.stop();
|
|
316
|
+
}
|
|
317
|
+
catch { /* best effort */ } // P1-3a: stop the LOCAL ref (codexRpcEngine may be unassigned)
|
|
318
|
+
codexRpcEngine = undefined;
|
|
319
|
+
remoteWsUrl = undefined;
|
|
320
|
+
remoteThreadId = undefined;
|
|
321
|
+
return 'not-engaged';
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/** RPC panes have NO terminal input path (turns go via JSON-RPC), so codex's
|
|
325
|
+
* interactive startup dialogs — most notably "Update available … Press enter to
|
|
326
|
+
* continue" right after a codex release — would block the `--remote resume` TUI
|
|
327
|
+
* from ever attaching, freezing the Web terminal even though turns still
|
|
328
|
+
* process. Bounded startup hygiene (boundary #4): for RPC panes only, watch the
|
|
329
|
+
* pane briefly and dismiss such a dialog with a single Enter, self-cancelling
|
|
330
|
+
* once the composer (readyPattern) is up or a hard cap elapses. This is startup
|
|
331
|
+
* handling, NOT ongoing screen-scraping — an Enter at the ready composer is a
|
|
332
|
+
* harmless empty submit codex ignores, and it never runs for paste panes. */
|
|
333
|
+
function armRpcStartupDialogDismiss() {
|
|
334
|
+
if (rpcDialogDismissTimer) {
|
|
335
|
+
clearTimeout(rpcDialogDismissTimer);
|
|
336
|
+
rpcDialogDismissTimer = null;
|
|
337
|
+
}
|
|
338
|
+
const deadline = Date.now() + 30_000;
|
|
339
|
+
// The update dialog is disabled at the source via `-c check_for_update_on_startup=false`
|
|
340
|
+
// (codex.ts/traex.ts buildArgs). This watcher is only a fail-safe. It must NEVER
|
|
341
|
+
// blind-press keys on an update menu — the default selection can be "Update now",
|
|
342
|
+
// which would trigger a self-update. So: if an update dialog is somehow present
|
|
343
|
+
// (config not honored), WARN and do nothing. Only a plain "Press enter to
|
|
344
|
+
// continue" with NO update/menu options gets a single safe Enter.
|
|
345
|
+
let warnedUpdate = false;
|
|
346
|
+
const tick = () => {
|
|
347
|
+
rpcDialogDismissTimer = null;
|
|
348
|
+
if (!codexRpcEngine || !backend)
|
|
349
|
+
return; // RPC torn down / pane gone
|
|
350
|
+
if (Date.now() > deadline) {
|
|
351
|
+
log('Codex RPC: startup-dialog watch timed out');
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
let screen = '';
|
|
355
|
+
try {
|
|
356
|
+
screen = renderer?.snapshot().content ?? '';
|
|
357
|
+
}
|
|
358
|
+
catch { /* renderer not ready yet */ }
|
|
359
|
+
const action = decideStartupDialogAction(screen, cliAdapter?.readyPattern);
|
|
360
|
+
if (action === 'warn-update') {
|
|
361
|
+
// Update menu present despite the config disable → NEVER auto-press (default
|
|
362
|
+
// may be "Update now"); warn the user so they dismiss it manually (P2).
|
|
363
|
+
if (!warnedUpdate) {
|
|
364
|
+
warnedUpdate = true;
|
|
365
|
+
log('Codex RPC: update dialog appeared despite check_for_update_on_startup=false — NOT auto-pressing; asking user to dismiss');
|
|
366
|
+
send({ type: 'user_notify', message: '⚠️ Codex 更新弹窗挡住了网页终端渲染。为避免误触自更新未自动处理——请在网页终端手动选「Skip」;消息仍经 RPC 正常处理,不影响回复。', turnId: currentBotmuxTurnId });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
else if (action === 'dismiss-safe') {
|
|
370
|
+
log('Codex RPC: dismissing a safe "press enter to continue" prompt on the --remote pane');
|
|
371
|
+
try {
|
|
372
|
+
backend.write('\r');
|
|
373
|
+
}
|
|
374
|
+
catch { /* best effort */ }
|
|
375
|
+
}
|
|
376
|
+
else if (action === 'ready') {
|
|
377
|
+
return; // composer reached with no blocking dialog → done
|
|
378
|
+
}
|
|
379
|
+
rpcDialogDismissTimer = setTimeout(tick, 2000);
|
|
380
|
+
};
|
|
381
|
+
rpcDialogDismissTimer = setTimeout(tick, 2500);
|
|
382
|
+
}
|
|
118
383
|
let cliPidMarker = null; // path to .botmux-cli-pids/<pid>
|
|
119
384
|
let seatbeltProfilePath = null; // per-session Seatbelt .sb profile to rm at exit (external-wrapper read isolation)
|
|
120
385
|
let sandboxStopWatcher = null; // stop fn for the sandbox outbox watcher
|
|
@@ -189,6 +454,25 @@ function refreshCliPluginGeneration(cfg, adapter) {
|
|
|
189
454
|
deferredPluginSkillCatalog = generation.deferredSkillCatalog ?? null;
|
|
190
455
|
log(`Plugin generation refreshed: ${generation.pluginManifest.pluginIds.join(', ') || '(none)'}`);
|
|
191
456
|
}
|
|
457
|
+
/** Refresh the process-scoped Skill/MCP snapshot and bring up the trusted MCP
|
|
458
|
+
* host before the process that owns model execution starts. Native paste mode
|
|
459
|
+
* calls this from spawnCli; RPC mode calls it before starting the app-server so
|
|
460
|
+
* the fresh first turn includes the catalog and the gateway socket already
|
|
461
|
+
* exists when Codex reads its MCP config. */
|
|
462
|
+
async function prepareCliPluginGenerationAndGateway(cfg, adapter) {
|
|
463
|
+
refreshCliPluginGeneration(cfg, adapter);
|
|
464
|
+
const manifest = readSessionMcpRuntimeManifest(cfg.sessionId, config.session.dataDir);
|
|
465
|
+
stopSessionMcpGatewayHost();
|
|
466
|
+
if (adapter.mcpGateway && manifest?.entries.length) {
|
|
467
|
+
sessionMcpGatewayHost = await startSessionMcpGatewayHost({
|
|
468
|
+
sessionId: cfg.sessionId,
|
|
469
|
+
dataDir: config.session.dataDir,
|
|
470
|
+
onError: error => log(`[mcp-gateway] host error: ${error.message}`),
|
|
471
|
+
});
|
|
472
|
+
log(`[mcp-gateway] trusted host listening for ${manifest.entries.length} plugin server(s)`);
|
|
473
|
+
}
|
|
474
|
+
return manifest;
|
|
475
|
+
}
|
|
192
476
|
/** v2 read isolation — provision a bot's PER-BOT config dir under its BOT_HOME so the
|
|
193
477
|
* CLI (redirected there via CLAUDE_CONFIG_DIR/CODEX_HOME) starts fully set up despite
|
|
194
478
|
* the global ~/.claude|~/.codex being Seatbelt-denied. Idempotent (guards on
|
|
@@ -987,24 +1271,53 @@ function authorizeManagedSend(claim) {
|
|
|
987
1271
|
}
|
|
988
1272
|
: { ok: false, error: `${decision.errorCode}: ${decision.error}` };
|
|
989
1273
|
}
|
|
990
|
-
function
|
|
991
|
-
if (!
|
|
1274
|
+
function clearRpcEnginePidMarker() {
|
|
1275
|
+
if (!rpcEnginePidMarker)
|
|
992
1276
|
return;
|
|
993
1277
|
try {
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1278
|
+
unlinkSync(rpcEnginePidMarker);
|
|
1279
|
+
}
|
|
1280
|
+
catch { /* already gone */ }
|
|
1281
|
+
rpcEnginePidMarker = null;
|
|
1282
|
+
}
|
|
1283
|
+
function registerRpcEnginePidMarker(pid) {
|
|
1284
|
+
clearRpcEnginePidMarker();
|
|
1285
|
+
if (!pid || !process.env.SESSION_DATA_DIR)
|
|
1286
|
+
return;
|
|
1287
|
+
try {
|
|
1288
|
+
const markersDir = join(process.env.SESSION_DATA_DIR, '.botmux-cli-pids');
|
|
1289
|
+
mkdirSync(markersDir, { recursive: true });
|
|
1290
|
+
rpcEnginePidMarker = join(markersDir, String(pid));
|
|
1291
|
+
writeCliPidMarker();
|
|
1292
|
+
log(`Codex RPC app-server PID marker written: ${pid}`);
|
|
1005
1293
|
}
|
|
1006
1294
|
catch (err) {
|
|
1007
|
-
|
|
1295
|
+
rpcEnginePidMarker = null;
|
|
1296
|
+
log(`Failed to write Codex RPC app-server PID marker: ${err?.message ?? err}`);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
function writeCliPidMarker() {
|
|
1300
|
+
if (!sessionId)
|
|
1301
|
+
return;
|
|
1302
|
+
for (const markerPath of [cliPidMarker, rpcEnginePidMarker]) {
|
|
1303
|
+
if (!markerPath)
|
|
1304
|
+
continue;
|
|
1305
|
+
try {
|
|
1306
|
+
// 原子写:daemon 侧(killStalePids 等)随时读这个 marker JSON。
|
|
1307
|
+
const markerPid = Number(basename(markerPath));
|
|
1308
|
+
const procStart = Number.isInteger(markerPid) && markerPid > 0
|
|
1309
|
+
? readProcessStartIdentity(markerPid)
|
|
1310
|
+
: undefined;
|
|
1311
|
+
atomicWriteFileSync(markerPath, JSON.stringify({
|
|
1312
|
+
sessionId,
|
|
1313
|
+
turnId: currentBotmuxTurnId ?? null,
|
|
1314
|
+
dispatchAttempt: currentBotmuxDispatchAttempt ?? null,
|
|
1315
|
+
...(procStart ? { procStart } : {}),
|
|
1316
|
+
}));
|
|
1317
|
+
}
|
|
1318
|
+
catch (err) {
|
|
1319
|
+
log(`Failed to update CLI PID marker ${markerPath}: ${err?.message ?? err}`);
|
|
1320
|
+
}
|
|
1008
1321
|
}
|
|
1009
1322
|
}
|
|
1010
1323
|
let lastStructuredBridgeActivityAtMs = 0;
|
|
@@ -4635,7 +4948,15 @@ async function flushPending() {
|
|
|
4635
4948
|
durableTurnInFlight = true;
|
|
4636
4949
|
// Track as in-flight until the CLI returns to idle (markPromptReady).
|
|
4637
4950
|
// If the CLI exits first, onExit stashes these for re-queue on respawn.
|
|
4638
|
-
|
|
4951
|
+
// EXCEPT in RPC mode: the app-server (not the pane) owns turn execution, so
|
|
4952
|
+
// once turn/start is sent the turn runs regardless of the pane. Re-queuing
|
|
4953
|
+
// it on a pane restart would RE-SEND an already-accepted turn → double
|
|
4954
|
+
// execution (same class as #443's double-submit). So RPC turns are never
|
|
4955
|
+
// auto-re-queued: a lost/late ack surfaces as a submit-failure the user can
|
|
4956
|
+
// resend manually, and the stable clientUserMessageId lets codex dedupe a
|
|
4957
|
+
// resend (Codex delta P1-1).
|
|
4958
|
+
if (!codexRpcEngine)
|
|
4959
|
+
inflightInputs.onWrite(item);
|
|
4639
4960
|
const msg = item.content;
|
|
4640
4961
|
currentBotmuxTurnId = item.turnId;
|
|
4641
4962
|
currentBotmuxDispatchAttempt = item.dispatchAttempt;
|
|
@@ -4687,9 +5008,21 @@ async function flushPending() {
|
|
|
4687
5008
|
// worker — exactly the failure mode this change is closing. Contain it.
|
|
4688
5009
|
let result;
|
|
4689
5010
|
try {
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
5011
|
+
if (codexRpcEngine) {
|
|
5012
|
+
// RPC input mode: deliver via JSON-RPC turn/start (its ack IS the
|
|
5013
|
+
// submit confirmation), which the attached `codex --remote` TUI
|
|
5014
|
+
// renders. No tmux paste → the history.jsonl verify/retry/recover
|
|
5015
|
+
// machinery is bypassed. A throw here falls into the catch below and
|
|
5016
|
+
// surfaces as a normal submit-failure notice.
|
|
5017
|
+
await codexRpcEngine.sendTurn(msg, item.turnId);
|
|
5018
|
+
result = { submitted: true };
|
|
5019
|
+
}
|
|
5020
|
+
else if (item.codexAppInput && cliAdapter.writeStructuredInput) {
|
|
5021
|
+
result = await cliAdapter.writeStructuredInput(backend, msg, item.codexAppInput);
|
|
5022
|
+
}
|
|
5023
|
+
else {
|
|
5024
|
+
result = await cliAdapter.writeInput(backend, msg);
|
|
5025
|
+
}
|
|
4693
5026
|
scheduleBusyPatternIdleProbe(`${cliName()} post-submit`);
|
|
4694
5027
|
}
|
|
4695
5028
|
catch (err) {
|
|
@@ -5188,7 +5521,7 @@ function scheduleBusyPatternIdleProbe(source) {
|
|
|
5188
5521
|
busyPatternIdleProbeTimer = setTimeout(tick, IDLE_PROBE_INTERVAL_MS);
|
|
5189
5522
|
busyPatternIdleProbeTimer.unref?.();
|
|
5190
5523
|
}
|
|
5191
|
-
async function spawnCli(cfg) {
|
|
5524
|
+
async function spawnCli(cfg, opts = {}) {
|
|
5192
5525
|
clearSessionRenameInFlight();
|
|
5193
5526
|
// (startupCommands one-shot is re-armed below, AFTER the reattach-vs-fresh
|
|
5194
5527
|
// prediction — only a genuinely fresh CLI process replays them; see
|
|
@@ -5675,17 +6008,9 @@ async function spawnCli(cfg) {
|
|
|
5675
6008
|
// A warm worker reattach keeps the existing Gateway and catalog untouched;
|
|
5676
6009
|
// every fresh/resumed CLI spawn atomically refreshes both from current Bot config.
|
|
5677
6010
|
if (!willReattachPersistent) {
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
if (cliAdapter.mcpGateway && mcpRuntimeManifest?.entries.length) {
|
|
5682
|
-
sessionMcpGatewayHost = await startSessionMcpGatewayHost({
|
|
5683
|
-
sessionId: cfg.sessionId,
|
|
5684
|
-
dataDir: config.session.dataDir,
|
|
5685
|
-
onError: error => log(`[mcp-gateway] host error: ${error.message}`),
|
|
5686
|
-
});
|
|
5687
|
-
log(`[mcp-gateway] trusted host listening for ${mcpRuntimeManifest.entries.length} plugin server(s)`);
|
|
5688
|
-
}
|
|
6011
|
+
mcpRuntimeManifest = opts.pluginGenerationPrepared
|
|
6012
|
+
? readSessionMcpRuntimeManifest(cfg.sessionId, config.session.dataDir)
|
|
6013
|
+
: await prepareCliPluginGenerationAndGateway(cfg, cliAdapter);
|
|
5689
6014
|
}
|
|
5690
6015
|
// Re-arm the startup-commands one-shot ONLY for a genuinely fresh CLI process.
|
|
5691
6016
|
// A reattach to a LIVE persistent pane (daemon-restart recovery) is the SAME
|
|
@@ -5902,6 +6227,11 @@ async function spawnCli(cfg) {
|
|
|
5902
6227
|
disableCliBypass: cfg.disableCliBypass === true,
|
|
5903
6228
|
skillPluginDir: cfg.skillPluginDir,
|
|
5904
6229
|
readIsolation: willReadIsolate,
|
|
6230
|
+
// Set (as a pair) by the init handler when hybrid RPC input is engaged;
|
|
6231
|
+
// makes buildArgs launch `codex --remote <ws> resume <thread>` instead of
|
|
6232
|
+
// the normal paste-mode codex. Undefined for every other bot/CLI.
|
|
6233
|
+
remoteWsUrl,
|
|
6234
|
+
remoteThreadId,
|
|
5905
6235
|
});
|
|
5906
6236
|
// Extra args from env (CLI_DISABLE_DEFAULT_ARGS is removed — adapters own their defaults)
|
|
5907
6237
|
const extra = (process.env.CLI_EXTRA_ARGS ?? '').trim();
|
|
@@ -6813,6 +7143,16 @@ async function spawnCli(cfg) {
|
|
|
6813
7143
|
emitTurnTerminal(exitedTurnId, 'ambiguous', 'cli_exit', exitedDispatchAttempt);
|
|
6814
7144
|
}
|
|
6815
7145
|
durableTurnInFlight = false;
|
|
7146
|
+
// Hybrid RPC mode: the `codex --remote` viewer just died — tear down the
|
|
7147
|
+
// paired app-server so it can't outlive the pane. A fresh spawn re-engages
|
|
7148
|
+
// the engine from scratch (or falls back to paste mode).
|
|
7149
|
+
if (codexRpcEngine) {
|
|
7150
|
+
stopCodexRpcEngine();
|
|
7151
|
+
if (rpcDialogDismissTimer) {
|
|
7152
|
+
clearTimeout(rpcDialogDismissTimer);
|
|
7153
|
+
rpcDialogDismissTimer = null;
|
|
7154
|
+
}
|
|
7155
|
+
}
|
|
6816
7156
|
const logTail = recentTerminalLogTail();
|
|
6817
7157
|
// Don't park a diagnostic shell here: most exits are immediately
|
|
6818
7158
|
// auto-restarted by the daemon, so an inline park would just be torn down
|
|
@@ -6902,6 +7242,7 @@ async function spawnCli(cfg) {
|
|
|
6902
7242
|
}
|
|
6903
7243
|
function killCli(opts = {}) {
|
|
6904
7244
|
stopSessionMcpGatewayHost();
|
|
7245
|
+
stopCodexRpcEngine();
|
|
6905
7246
|
destroyCrashDiagnosticTerminal('killCli');
|
|
6906
7247
|
idleDetector?.dispose();
|
|
6907
7248
|
idleDetector = null;
|
|
@@ -7006,6 +7347,10 @@ async function restartCliProcess(reason, opts = {}) {
|
|
|
7006
7347
|
const restart = async () => {
|
|
7007
7348
|
try {
|
|
7008
7349
|
tmuxRestartTimer = null;
|
|
7350
|
+
// Capture the RPC thread id before destroySession()/killCli() tears down
|
|
7351
|
+
// the engine and clears remoteThreadId. The replacement app-server must
|
|
7352
|
+
// resume this exact thread on its fresh port.
|
|
7353
|
+
const rpcThreadId = remoteThreadId;
|
|
7009
7354
|
const restartingBackend = backend;
|
|
7010
7355
|
if (restartingBackend)
|
|
7011
7356
|
intentionalRestartBackend = restartingBackend;
|
|
@@ -7032,7 +7377,20 @@ async function restartCliProcess(reason, opts = {}) {
|
|
|
7032
7377
|
startScreenUpdates();
|
|
7033
7378
|
startScreenAnalyzer();
|
|
7034
7379
|
try {
|
|
7035
|
-
|
|
7380
|
+
const restartCfg = { ...lastInitConfig, resume: true, prompt: '', cliSessionId: rpcThreadId ?? lastInitConfig.cliSessionId };
|
|
7381
|
+
// Re-engage RPC so the new --remote pane binds to the CURRENT app-server
|
|
7382
|
+
// (a fresh port), not the dead prior one. engageCodexRpc only sets
|
|
7383
|
+
// remote* on success, else spawnCli falls back to paste.
|
|
7384
|
+
let rpcPluginGenerationPrepared = false;
|
|
7385
|
+
if (codexRpcEligible(restartCfg, { sandboxForced: sandboxEnabled() })) {
|
|
7386
|
+
const adapter = createCliAdapterSync(restartCfg.cliId, restartCfg.cliPathOverride);
|
|
7387
|
+
await prepareCliPluginGenerationAndGateway(restartCfg, adapter);
|
|
7388
|
+
rpcPluginGenerationPrepared = true;
|
|
7389
|
+
await engageCodexRpc(restartCfg);
|
|
7390
|
+
}
|
|
7391
|
+
await spawnCli(restartCfg, { pluginGenerationPrepared: rpcPluginGenerationPrepared });
|
|
7392
|
+
if (codexRpcEngine)
|
|
7393
|
+
armRpcStartupDialogDismiss();
|
|
7036
7394
|
}
|
|
7037
7395
|
catch (err) {
|
|
7038
7396
|
cliRestartInProgress = false;
|
|
@@ -8297,7 +8655,42 @@ process.on('message', async (raw) => {
|
|
|
8297
8655
|
port = await startWebServer(config.web.workerHost, msg.webPort);
|
|
8298
8656
|
log('Workflow worker mode: web terminal enabled; skipping screen updates and screen analyzer');
|
|
8299
8657
|
}
|
|
8300
|
-
|
|
8658
|
+
// Hybrid codex RPC input (opt-in): bind the pane to a botmux-owned
|
|
8659
|
+
// app-server thread; input flows via JSON-RPC instead of a drop-prone
|
|
8660
|
+
// paste. The pure orchestrator (codex-rpc-lifecycle) decides fresh vs
|
|
8661
|
+
// resume vs stale-pane-replace vs fail-closed; the worker wires the real
|
|
8662
|
+
// effects and acts on the decision. On a kill-verify failure it ABORTS
|
|
8663
|
+
// init (throws) rather than let spawnCli reattach a dead `--remote` pane
|
|
8664
|
+
// to the fresh-port engine (Codex delta P0-2). engageCodexRpc readies the
|
|
8665
|
+
// engine + thread BEFORE any pane is touched (boundary #1); the pane is
|
|
8666
|
+
// then launched/respawned as `codex --remote resume` (codex.ts buildArgs)
|
|
8667
|
+
// against the CURRENT app-server (a fresh port each incarnation).
|
|
8668
|
+
const rpcBackendType = msg.backendType ?? config.daemon.backendType;
|
|
8669
|
+
let rpcPluginGenerationPrepared = false;
|
|
8670
|
+
const rpcDecision = await orchestrateCodexRpcInit(msg, {
|
|
8671
|
+
paneInfo: (sid) => persistentPaneInfo(rpcBackendType, sid),
|
|
8672
|
+
paneIsRemote: (name) => paneRunsRemoteTui(name),
|
|
8673
|
+
prepare: async () => {
|
|
8674
|
+
const adapter = createCliAdapterSync(msg.cliId, msg.cliPathOverride);
|
|
8675
|
+
await prepareCliPluginGenerationAndGateway(msg, adapter);
|
|
8676
|
+
rpcPluginGenerationPrepared = true;
|
|
8677
|
+
},
|
|
8678
|
+
engage: () => engageCodexRpc(msg),
|
|
8679
|
+
killVerify: (name) => killPersistentSessionVerified(rpcBackendType, name),
|
|
8680
|
+
teardownEngine: () => stopCodexRpcEngine(),
|
|
8681
|
+
log: (m) => log(m),
|
|
8682
|
+
notify: (m) => send({ type: 'user_notify', message: m, turnId: msg.turnId }),
|
|
8683
|
+
}, { sandboxForced: sandboxEnabled() });
|
|
8684
|
+
if (rpcDecision.engaged)
|
|
8685
|
+
log(`Codex RPC resume: pane bound to ${remoteWsUrl}`);
|
|
8686
|
+
if (rpcDecision.abortSpawn) {
|
|
8687
|
+
// Stale --remote pane couldn't be replaced — refuse to attach it. Abort
|
|
8688
|
+
// init so the daemon retries a clean incarnation instead of freezing.
|
|
8689
|
+
throw new Error('codex RPC resume: could not replace stale --remote pane; aborting init');
|
|
8690
|
+
}
|
|
8691
|
+
await spawnCli(msg, { pluginGenerationPrepared: rpcPluginGenerationPrepared });
|
|
8692
|
+
if (codexRpcEngine)
|
|
8693
|
+
armRpcStartupDialogDismiss(); // boundary #4: keep the --remote pane from freezing on a startup dialog
|
|
8301
8694
|
// Queue the initial prompt — flushed when CLI shows idle.
|
|
8302
8695
|
// Adapters with passesInitialPromptViaArgs (e.g. Gemini -i) bake the
|
|
8303
8696
|
// prompt into CLI args, so we normally skip queuing to avoid double-send.
|
|
@@ -8328,7 +8721,20 @@ process.on('message', async (raw) => {
|
|
|
8328
8721
|
// transcript file.
|
|
8329
8722
|
codexBridgeMarkPendingTurn(msg.prompt, msg.turnId, msg.dispatchAttempt);
|
|
8330
8723
|
}
|
|
8331
|
-
|
|
8724
|
+
// Queue the initial prompt for flushPending (pure decision, unit-tested):
|
|
8725
|
+
// - paste (no engine): normal queue.
|
|
8726
|
+
// - RPC FRESH accepted/ambiguous: engine set + queuePrompt=false → NEVER
|
|
8727
|
+
// queue (the turn was pre-sent or is ambiguous; re-queuing = double
|
|
8728
|
+
// execution — exactly-once, Codex P1-1). Ambiguous never reaches here.
|
|
8729
|
+
// - RPC RESUME: queuePrompt=true → queue for post-ready flush (bridge
|
|
8730
|
+
// marked at flush time, P0-1).
|
|
8731
|
+
if (shouldQueueInitialPrompt({
|
|
8732
|
+
hasPrompt: !!msg.prompt,
|
|
8733
|
+
rpcEngineActive: !!codexRpcEngine,
|
|
8734
|
+
queuePrompt: rpcDecision.queuePrompt,
|
|
8735
|
+
passesInitialPromptViaArgs: cliAdapter?.passesInitialPromptViaArgs === true,
|
|
8736
|
+
deferInitialPrompt,
|
|
8737
|
+
})) {
|
|
8332
8738
|
pendingMessages.push({
|
|
8333
8739
|
content: msg.prompt,
|
|
8334
8740
|
turnId: msg.turnId,
|
|
@@ -8876,7 +9282,7 @@ function teardownSandboxBestEffort() {
|
|
|
8876
9282
|
// and process.exit(1), killing a live session over a dropped log write. Install
|
|
8877
9283
|
// the guard before any further stdout writes (log() writes to process.stdout).
|
|
8878
9284
|
installStdioEpipeGuard();
|
|
8879
|
-
process.on('exit', () => { teardownSandboxBestEffort(); });
|
|
9285
|
+
process.on('exit', () => { teardownSandboxBestEffort(); stopCodexRpcEngine(); });
|
|
8880
9286
|
process.on('uncaughtException', (err) => {
|
|
8881
9287
|
// A broken pipe on stdout/stderr (or any socket) must not tear down a live
|
|
8882
9288
|
// session — the stdio guard handles those it can; this is the backstop.
|