botmux 3.4.2 → 3.4.3

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 (36) hide show
  1. package/dist/adapters/cli/pi-initial-prompt-extension.d.ts +27 -0
  2. package/dist/adapters/cli/pi-initial-prompt-extension.d.ts.map +1 -0
  3. package/dist/adapters/cli/pi-initial-prompt-extension.js +38 -0
  4. package/dist/adapters/cli/pi-initial-prompt-extension.js.map +1 -0
  5. package/dist/adapters/cli/pi-initial-prompt.d.ts +21 -0
  6. package/dist/adapters/cli/pi-initial-prompt.d.ts.map +1 -0
  7. package/dist/adapters/cli/pi-initial-prompt.js +55 -0
  8. package/dist/adapters/cli/pi-initial-prompt.js.map +1 -0
  9. package/dist/adapters/cli/pi.d.ts.map +1 -1
  10. package/dist/adapters/cli/pi.js +15 -4
  11. package/dist/adapters/cli/pi.js.map +1 -1
  12. package/dist/adapters/cli/read-isolation.d.ts.map +1 -1
  13. package/dist/adapters/cli/read-isolation.js +5 -0
  14. package/dist/adapters/cli/read-isolation.js.map +1 -1
  15. package/dist/adapters/cli/shared-hints.d.ts.map +1 -1
  16. package/dist/adapters/cli/shared-hints.js +8 -0
  17. package/dist/adapters/cli/shared-hints.js.map +1 -1
  18. package/dist/adapters/cli/types.d.ts +20 -0
  19. package/dist/adapters/cli/types.d.ts.map +1 -1
  20. package/dist/core/inflight-input-tracker.d.ts +1 -0
  21. package/dist/core/inflight-input-tracker.d.ts.map +1 -1
  22. package/dist/core/inflight-input-tracker.js.map +1 -1
  23. package/dist/platform/secure-host-file.d.ts.map +1 -1
  24. package/dist/platform/secure-host-file.js +173 -54
  25. package/dist/platform/secure-host-file.js.map +1 -1
  26. package/dist/utils/child-env.d.ts +1 -1
  27. package/dist/utils/child-env.d.ts.map +1 -1
  28. package/dist/utils/child-env.js +2 -0
  29. package/dist/utils/child-env.js.map +1 -1
  30. package/dist/utils/pending-input-queue.d.ts +19 -0
  31. package/dist/utils/pending-input-queue.d.ts.map +1 -1
  32. package/dist/utils/pending-input-queue.js +21 -1
  33. package/dist/utils/pending-input-queue.js.map +1 -1
  34. package/dist/worker.js +102 -24
  35. package/dist/worker.js.map +1 -1
  36. package/package.json +1 -1
package/dist/worker.js CHANGED
@@ -13,7 +13,7 @@
13
13
  * 7. On 'restart', kills CLI and re-spawns with --resume
14
14
  */
15
15
  import { randomBytes } from 'node:crypto';
16
- import { mkdirSync, writeFileSync, unlinkSync, existsSync, statSync, lstatSync, readdirSync, readlinkSync, readFileSync, realpathSync, copyFileSync, watch as fsWatch, createWriteStream, openSync, closeSync, fstatSync, constants as fsConstants } from 'node:fs';
16
+ import { mkdirSync, writeFileSync, unlinkSync, rmdirSync, existsSync, statSync, lstatSync, readdirSync, readlinkSync, readFileSync, realpathSync, copyFileSync, watch as fsWatch, createWriteStream, openSync, closeSync, fstatSync, constants as fsConstants } from 'node:fs';
17
17
  import { atomicWriteFileSync } from './utils/atomic-write.js';
18
18
  import { join, basename, dirname } from 'node:path';
19
19
  import { homedir, tmpdir } from 'node:os';
@@ -29,7 +29,7 @@ import { canStartInjectionFlush, shouldDeferUserFlush, shouldFlushInjectionsFirs
29
29
  import { stripAnsiForLog, tailChars } from './utils/crash-log.js';
30
30
  import { CodexUpdateDialogGuard } from './utils/codex-update-dialog.js';
31
31
  import { installStdioEpipeGuard, isIgnorableStreamError } from './utils/stdio-epipe-guard.js';
32
- import { mergeQueuedCliInput, pendingInputMayFlush, pendingInputAllowsTypeAhead, shouldDeferArgsBakedDurablePrompt, shouldDeferInitialPromptForArgLimit, shouldStopPendingBatch, terminalReleasesDurableTurn, } from './utils/pending-input-queue.js';
32
+ import { mergeQueuedCliInput, pendingInputMayFlush, pendingInputAllowsTypeAhead, resolveInitialPromptDelivery, shouldDeferArgsBakedDurablePrompt, shouldDeferInitialPromptForArgLimit, shouldStopPendingBatch, terminalReleasesDurableTurn, } from './utils/pending-input-queue.js';
33
33
  import { ReadyGate, shouldArmReadyGate } from './utils/ready-gate.js';
34
34
  import { shouldRunStartupCommandsOnSpawn, shouldDeferInitialPromptForStartup } from './core/startup-commands.js';
35
35
  import { sanitizePerBotEnv } from './core/per-bot-env.js';
@@ -144,6 +144,36 @@ let remoteWsUrl;
144
144
  let remoteThreadId;
145
145
  let rpcDialogDismissTimer = null;
146
146
  let rpcEnginePidMarker = null;
147
+ const piInitialPromptCleanupPaths = [];
148
+ const piInitialPromptCleanupDirs = [];
149
+ let piInitialPromptReadonlyRoots = [];
150
+ let piInitialPromptAdditionalArgs = [];
151
+ let piInitialPromptEnv = {};
152
+ function cleanupPiInitialPromptFiles() {
153
+ while (piInitialPromptCleanupPaths.length > 0) {
154
+ const p = piInitialPromptCleanupPaths.pop();
155
+ if (!p)
156
+ continue;
157
+ try {
158
+ unlinkSync(p);
159
+ }
160
+ catch { /* best effort */ }
161
+ }
162
+ while (piInitialPromptCleanupDirs.length > 0) {
163
+ const dir = piInitialPromptCleanupDirs.pop();
164
+ if (!dir)
165
+ continue;
166
+ // Non-recursive on purpose: remove only an empty session-owned directory,
167
+ // never a shared root or a path that unexpectedly gained other content.
168
+ try {
169
+ rmdirSync(dir);
170
+ }
171
+ catch { /* best effort */ }
172
+ }
173
+ piInitialPromptReadonlyRoots = [];
174
+ piInitialPromptAdditionalArgs = [];
175
+ piInitialPromptEnv = {};
176
+ }
147
177
  function stopCodexRpcEngine() {
148
178
  const engine = codexRpcEngine;
149
179
  codexRpcEngine = undefined;
@@ -672,6 +702,9 @@ let reattachIdleProbeTimer = null;
672
702
  * adapter's hermesBridgeAttach reads the correct mode. */
673
703
  let lastSpawnEffectiveResume = false;
674
704
  let lastSpawnEffectiveCliSessionId;
705
+ let lastSpawnDeferInitialPrompt = false;
706
+ let lastSpawnQueuedInitialPrompt;
707
+ let lastSpawnQueuedInitialPromptLogicalContent;
675
708
  let idleDetector = null;
676
709
  let isTmuxMode = false;
677
710
  /** True once a crash diagnostic tmux shell (bmx-diag-<sid>) is live. */
@@ -5068,6 +5101,7 @@ async function flushPending() {
5068
5101
  if (!codexRpcEngine)
5069
5102
  inflightInputs.onWrite(item);
5070
5103
  const msg = item.content;
5104
+ const logicalMsg = item.logicalContent ?? msg;
5071
5105
  currentBotmuxTurnId = item.turnId;
5072
5106
  currentBotmuxDispatchAttempt = item.dispatchAttempt;
5073
5107
  currentVcMeetingImTurnOrigin = item.vcMeetingImTurnOrigin;
@@ -5087,14 +5121,14 @@ async function flushPending() {
5087
5121
  bridgeIngest();
5088
5122
  }
5089
5123
  catch { /* best-effort */ }
5090
- bridgeTurnId = bridgeMarkPendingTurn(msg, item.turnId, item.dispatchAttempt);
5124
+ bridgeTurnId = bridgeMarkPendingTurn(logicalMsg, item.turnId, item.dispatchAttempt);
5091
5125
  }
5092
5126
  else if (codexBridgeActive) {
5093
5127
  // Codex mark works even before the rollout path is known: the
5094
5128
  // queue is path-agnostic, and the late-attach below will start
5095
5129
  // ingest from offset 0 so the user_message that lands shortly
5096
5130
  // after still fingerprint-matches this turn.
5097
- codexBridgeMarkPendingTurn(msg, item.turnId, item.dispatchAttempt);
5131
+ codexBridgeMarkPendingTurn(logicalMsg, item.turnId, item.dispatchAttempt);
5098
5132
  }
5099
5133
  if (durableWrite
5100
5134
  && cliAdapter.reliableTurnTerminal === true
@@ -5148,7 +5182,7 @@ async function flushPending() {
5148
5182
  // do. Otherwise surface it as a submit failure so the message isn't
5149
5183
  // silently lost.
5150
5184
  if (backend)
5151
- scheduleSubmitFailureNotify(msg, undefined, '会话 JSONL', bridgeTurnId, undefined, turnSeq, item);
5185
+ scheduleSubmitFailureNotify(logicalMsg, undefined, '会话 JSONL', bridgeTurnId, undefined, turnSeq, item);
5152
5186
  break;
5153
5187
  }
5154
5188
  // Persist any sessionId the adapter observed via authoritative sources
@@ -5167,7 +5201,7 @@ async function flushPending() {
5167
5201
  // nulled backend) the user already got a "CLI exited" notice; don't also
5168
5202
  // nag that the submit wasn't confirmed.
5169
5203
  if (result && result.submitted === false && backend) {
5170
- scheduleSubmitFailureNotify(msg, result.recheck, '会话 JSONL', bridgeTurnId, result.failureReason, turnSeq, item);
5204
+ scheduleSubmitFailureNotify(logicalMsg, result.recheck, '会话 JSONL', bridgeTurnId, result.failureReason, turnSeq, item);
5171
5205
  }
5172
5206
  // All structured bridges now drain every pending message in one flush:
5173
5207
  // Claude's BridgeTurnQueue handles `attachment(queued_command)` events
@@ -6340,6 +6374,30 @@ async function spawnCli(cfg, opts = {}) {
6340
6374
  // baking it into args would drop the message that triggered the resume.
6341
6375
  // Finally, defer adapter-declared over-limit prompts to avoid backend command
6342
6376
  // string limits (tmux "command too long") while preserving short argv prompts.
6377
+ let preparedInitialPrompt;
6378
+ let promptArgPreparationChanged = false;
6379
+ let preparedDeferredInput;
6380
+ if (cfg.prompt) {
6381
+ const prepared = cliAdapter.prepareInitialPromptArg?.({
6382
+ initialPrompt: cfg.prompt,
6383
+ sessionId: effectiveAdapterSessionId,
6384
+ sessionDataDir: process.env.SESSION_DATA_DIR,
6385
+ });
6386
+ if (prepared?.readonlyRoots?.length) {
6387
+ piInitialPromptReadonlyRoots = [
6388
+ ...new Set([...piInitialPromptReadonlyRoots, ...prepared.readonlyRoots]),
6389
+ ];
6390
+ }
6391
+ if (prepared?.cleanupPaths?.length) {
6392
+ piInitialPromptCleanupPaths.push(...prepared.cleanupPaths);
6393
+ }
6394
+ if (prepared?.cleanupDirs?.length) {
6395
+ piInitialPromptCleanupDirs.push(...prepared.cleanupDirs);
6396
+ }
6397
+ preparedDeferredInput = prepared?.deferredInput;
6398
+ preparedInitialPrompt = prepared?.initialPrompt ?? cfg.prompt;
6399
+ promptArgPreparationChanged = preparedInitialPrompt !== cfg.prompt;
6400
+ }
6343
6401
  const deferInitialPrompt = shouldDeferInitialPromptForStartup({
6344
6402
  hasStartupCommands: !!cfg.startupCommands?.length,
6345
6403
  adoptMode: cfg.adoptMode === true,
@@ -6349,11 +6407,25 @@ async function spawnCli(cfg, opts = {}) {
6349
6407
  adoptMode: cfg.adoptMode === true,
6350
6408
  dispatchAttempt: cfg.dispatchAttempt,
6351
6409
  }) || (effectiveResume && cliAdapter.initialPromptArgsIgnoredOnResume === true)
6352
- || shouldDeferInitialPromptForArgLimit({
6410
+ || (!promptArgPreparationChanged && shouldDeferInitialPromptForArgLimit({
6353
6411
  passesInitialPromptViaArgs: cliAdapter.passesInitialPromptViaArgs === true,
6354
6412
  prompt: cfg.prompt,
6355
6413
  maxInitialPromptArgBytes: cliAdapter.maxInitialPromptArgBytes,
6356
- });
6414
+ }));
6415
+ const initialPromptDelivery = resolveInitialPromptDelivery({
6416
+ originalPrompt: cfg.prompt,
6417
+ preparedArg: preparedInitialPrompt,
6418
+ preparedDeferredContent: preparedDeferredInput?.content,
6419
+ defer: deferInitialPrompt,
6420
+ });
6421
+ preparedInitialPrompt = initialPromptDelivery.argvPrompt;
6422
+ lastSpawnQueuedInitialPrompt = initialPromptDelivery.queuedContent;
6423
+ lastSpawnQueuedInitialPromptLogicalContent = initialPromptDelivery.logicalContent;
6424
+ if (deferInitialPrompt && preparedDeferredInput) {
6425
+ piInitialPromptAdditionalArgs = [...(preparedDeferredInput.additionalArgs ?? [])];
6426
+ piInitialPromptEnv = { ...(preparedDeferredInput.env ?? {}) };
6427
+ }
6428
+ lastSpawnDeferInitialPrompt = deferInitialPrompt;
6357
6429
  kiroSessionIdCaptureArmed = cfg.cliId === 'kiro-cli' && !effectiveCliSessionId && !willReattachPersistent;
6358
6430
  kiroSessionIdCaptureBuffer = '';
6359
6431
  // Per-bot local read isolation: assemble the Seatbelt profile context (the gate
@@ -6432,7 +6504,7 @@ async function spawnCli(cfg, opts = {}) {
6432
6504
  resume: effectiveResume,
6433
6505
  workingDir: cfg.workingDir,
6434
6506
  resumeSessionId: effectiveCliSessionId,
6435
- initialPrompt: deferInitialPrompt ? undefined : (cfg.prompt || undefined),
6507
+ initialPrompt: preparedInitialPrompt,
6436
6508
  botName: cfg.botName,
6437
6509
  botOpenId: cfg.botOpenId,
6438
6510
  larkAppId: cfg.larkAppId,
@@ -6447,6 +6519,12 @@ async function spawnCli(cfg, opts = {}) {
6447
6519
  remoteWsUrl,
6448
6520
  remoteThreadId,
6449
6521
  });
6522
+ // Pi's deferred long-first-prompt command is implemented by a session-scoped
6523
+ // extension. Keep its launch args across owned process restarts while the
6524
+ // queued/in-flight command may still need replay.
6525
+ if (piInitialPromptAdditionalArgs.length > 0) {
6526
+ args.unshift(...piInitialPromptAdditionalArgs);
6527
+ }
6450
6528
  // Extra args from env (CLI_DISABLE_DEFAULT_ARGS is removed — adapters own their defaults)
6451
6529
  const extra = (process.env.CLI_EXTRA_ARGS ?? '').trim();
6452
6530
  if (extra)
@@ -6576,6 +6654,9 @@ async function spawnCli(cfg, opts = {}) {
6576
6654
  // them past the server's global env.
6577
6655
  if (cliAdapter.spawnEnv)
6578
6656
  Object.assign(childEnv, cliAdapter.spawnEnv);
6657
+ if (Object.keys(piInitialPromptEnv).length > 0) {
6658
+ Object.assign(childEnv, piInitialPromptEnv);
6659
+ }
6579
6660
  // v2 read isolation: point the CLI at its PER-BOT config dir (set AFTER spawnEnv
6580
6661
  // so it overrides any adapter default). claude → CLAUDE_CONFIG_DIR, codex →
6581
6662
  // CODEX_HOME. Both are in BOTMUX_INJECTED_ENV_KEYS so the tmux backend forwards
@@ -6662,6 +6743,9 @@ async function spawnCli(cfg, opts = {}) {
6662
6743
  cliId: cliAdapter.id,
6663
6744
  resolvedBin: canonical(cliAdapter.resolvedBin),
6664
6745
  }),
6746
+ // buildV2DenyPaths masks the shared pi-initial-prompts root. Re-open
6747
+ // only this session's private child directory for Pi's @file/extension.
6748
+ ...piInitialPromptReadonlyRoots.map(canonical),
6665
6749
  ];
6666
6750
  finalDenyPaths = carve.finalDenyPaths.map(canonical);
6667
6751
  traverseDirs = carve.traverseDirs.map(canonical);
@@ -6908,6 +6992,7 @@ async function spawnCli(cfg, opts = {}) {
6908
6992
  extraExecPaths: cliAdapter.sandboxExtraExecPaths?.(),
6909
6993
  readonlyRoots: [
6910
6994
  ...(cfg.skillReadonlyRoots ?? []),
6995
+ ...piInitialPromptReadonlyRoots,
6911
6996
  ...(readIsoLinuxMasks?.ownReadOnlyPaths ?? []),
6912
6997
  ],
6913
6998
  mcpGatewaySocketPath: sessionMcpGatewayHost?.socketPath,
@@ -7661,6 +7746,8 @@ function killCli(opts = {}) {
7661
7746
  currentCliCredentialIsolated = false;
7662
7747
  stopSessionMcpGatewayHost();
7663
7748
  stopCodexRpcEngine();
7749
+ if (!opts.preservePending)
7750
+ cleanupPiInitialPromptFiles();
7664
7751
  destroyCrashDiagnosticTerminal('killCli');
7665
7752
  idleDetector?.dispose();
7666
7753
  idleDetector = null;
@@ -9133,20 +9220,7 @@ process.on('message', async (raw) => {
9133
9220
  // Tier-1/Tier-2 fresh demotion, which clears the flag). Adopt spawns
9134
9221
  // return from spawnCli before that write — exclude them explicitly so
9135
9222
  // the stale module-level value can't leak in.
9136
- const deferInitialPrompt = shouldDeferInitialPromptForStartup({
9137
- hasStartupCommands: !!msg.startupCommands?.length,
9138
- adoptMode: msg.adoptMode === true,
9139
- passesInitialPromptViaArgs: cliAdapter?.passesInitialPromptViaArgs === true,
9140
- }) || shouldDeferArgsBakedDurablePrompt({
9141
- passesInitialPromptViaArgs: cliAdapter?.passesInitialPromptViaArgs === true,
9142
- adoptMode: msg.adoptMode === true,
9143
- dispatchAttempt: msg.dispatchAttempt,
9144
- }) || (msg.adoptMode !== true && lastSpawnEffectiveResume && cliAdapter?.initialPromptArgsIgnoredOnResume === true)
9145
- || shouldDeferInitialPromptForArgLimit({
9146
- passesInitialPromptViaArgs: cliAdapter?.passesInitialPromptViaArgs === true,
9147
- prompt: msg.prompt,
9148
- maxInitialPromptArgBytes: cliAdapter?.maxInitialPromptArgBytes,
9149
- });
9223
+ const deferInitialPrompt = lastSpawnDeferInitialPrompt;
9150
9224
  if (msg.prompt && cliAdapter?.passesInitialPromptViaArgs && !deferInitialPrompt && codexBridgeFallbackActive()) {
9151
9225
  // Args-baked first prompts (notably Pi) never pass through the normal
9152
9226
  // 'message' IPC path, so the structured bridge would otherwise see the
@@ -9171,7 +9245,10 @@ process.on('message', async (raw) => {
9171
9245
  deferInitialPrompt,
9172
9246
  })) {
9173
9247
  pendingMessages.push({
9174
- content: msg.prompt,
9248
+ content: lastSpawnQueuedInitialPrompt ?? msg.prompt,
9249
+ ...(lastSpawnQueuedInitialPromptLogicalContent
9250
+ ? { logicalContent: lastSpawnQueuedInitialPromptLogicalContent }
9251
+ : {}),
9175
9252
  turnId: msg.turnId,
9176
9253
  dispatchAttempt: msg.dispatchAttempt,
9177
9254
  vcMeetingImTurnOrigin: msg.vcMeetingImTurnOrigin,
@@ -9633,6 +9710,7 @@ process.on('message', async (raw) => {
9633
9710
  });
9634
9711
  // ─── Cleanup ─────────────────────────────────────────────────────────────────
9635
9712
  function cleanup() {
9713
+ cleanupPiInitialPromptFiles();
9636
9714
  stopSessionMcpGatewayHost();
9637
9715
  if (tmuxRestartTimer) {
9638
9716
  clearTimeout(tmuxRestartTimer);