@songsid/agend 2.1.7-beta.4 → 2.1.7-beta.5

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 (76) hide show
  1. package/README.md +2 -0
  2. package/README.zh-TW.md +2 -0
  3. package/dist/agent-cli-instructions.md +5 -0
  4. package/dist/agent-cli.js +13 -0
  5. package/dist/agent-cli.js.map +1 -1
  6. package/dist/agent-endpoint.js +1 -0
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/backend/codex-session-lookup.d.ts +59 -0
  9. package/dist/backend/codex-session-lookup.js +139 -0
  10. package/dist/backend/codex-session-lookup.js.map +1 -0
  11. package/dist/backend/codex.d.ts +11 -0
  12. package/dist/backend/codex.js +115 -7
  13. package/dist/backend/codex.js.map +1 -1
  14. package/dist/backend/types.d.ts +12 -0
  15. package/dist/backend/types.js.map +1 -1
  16. package/dist/channel/mcp-tools.js +2 -0
  17. package/dist/channel/mcp-tools.js.map +1 -1
  18. package/dist/cli.js +68 -2
  19. package/dist/cli.js.map +1 -1
  20. package/dist/daemon.d.ts +11 -0
  21. package/dist/daemon.js +173 -23
  22. package/dist/daemon.js.map +1 -1
  23. package/dist/delivery-outbox.d.ts +90 -6
  24. package/dist/delivery-outbox.js +301 -39
  25. package/dist/delivery-outbox.js.map +1 -1
  26. package/dist/delivery-outbox.test.js +268 -14
  27. package/dist/delivery-outbox.test.js.map +1 -1
  28. package/dist/delivery-queue-evidence.d.ts +15 -0
  29. package/dist/delivery-queue-evidence.js +35 -0
  30. package/dist/delivery-queue-evidence.js.map +1 -0
  31. package/dist/delivery-queue-evidence.test.d.ts +1 -0
  32. package/dist/delivery-queue-evidence.test.js +23 -0
  33. package/dist/delivery-queue-evidence.test.js.map +1 -0
  34. package/dist/delivery-reconciliation.d.ts +49 -0
  35. package/dist/delivery-reconciliation.js +296 -0
  36. package/dist/delivery-reconciliation.js.map +1 -0
  37. package/dist/delivery-reconciliation.test.d.ts +1 -0
  38. package/dist/delivery-reconciliation.test.js +411 -0
  39. package/dist/delivery-reconciliation.test.js.map +1 -0
  40. package/dist/fleet-manager.d.ts +2 -1
  41. package/dist/fleet-manager.js +61 -32
  42. package/dist/fleet-manager.js.map +1 -1
  43. package/dist/instance-lifecycle.d.ts +13 -0
  44. package/dist/instance-lifecycle.js +85 -10
  45. package/dist/instance-lifecycle.js.map +1 -1
  46. package/dist/instructions.js +1 -0
  47. package/dist/instructions.js.map +1 -1
  48. package/dist/locale.js +6 -0
  49. package/dist/locale.js.map +1 -1
  50. package/dist/outbound-handlers.d.ts +3 -0
  51. package/dist/outbound-handlers.js +38 -1
  52. package/dist/outbound-handlers.js.map +1 -1
  53. package/dist/outbound-schemas.d.ts +7 -0
  54. package/dist/outbound-schemas.js +14 -0
  55. package/dist/outbound-schemas.js.map +1 -1
  56. package/dist/pane-write-lock.d.ts +2 -0
  57. package/dist/pane-write-lock.js +19 -0
  58. package/dist/pane-write-lock.js.map +1 -1
  59. package/dist/scheduler/scheduler.d.ts +5 -1
  60. package/dist/scheduler/scheduler.js +60 -20
  61. package/dist/scheduler/scheduler.js.map +1 -1
  62. package/dist/scheduler/scheduler.test.js +41 -2
  63. package/dist/scheduler/scheduler.test.js.map +1 -1
  64. package/dist/scheduler/types.d.ts +1 -1
  65. package/dist/tmux-manager.d.ts +3 -3
  66. package/dist/tmux-manager.js +6 -6
  67. package/dist/tmux-manager.js.map +1 -1
  68. package/dist/tool-permissions.js +6 -6
  69. package/dist/tool-permissions.js.map +1 -1
  70. package/dist/transcript-monitor.d.ts +3 -1
  71. package/dist/transcript-monitor.js +15 -0
  72. package/dist/transcript-monitor.js.map +1 -1
  73. package/dist/transcript-sources.d.ts +9 -0
  74. package/dist/transcript-sources.js +14 -0
  75. package/dist/transcript-sources.js.map +1 -1
  76. package/package.json +1 -1
package/dist/daemon.js CHANGED
@@ -30,10 +30,11 @@ import { bottomRowIsReady, inputAreaText, inputShowsPastedText, pastedTextSignat
30
30
  import { TurnReplyGuard } from "./turn-reply-guard.js";
31
31
  import { t } from "./locale.js";
32
32
  import { MuseUsageRelay, clearMuseUsageSnapshot } from "./muse-usage-relay.js";
33
+ import { queueResumePolicyForAttempt } from "./delivery-queue-evidence.js";
33
34
  const __filename = fileURLToPath(import.meta.url);
34
35
  const __dirname = dirname(__filename);
35
36
  // Tool routing sets — module-level to avoid re-creation on every handleToolCall
36
- const CROSS_INSTANCE_TOOLS = new Set(["send_to_instance", "broadcast", "list_instances", "start_instance", "restart_instance", "create_instance", "delete_instance", "replace_instance", "request_information", "delegate_task", "report_result", "describe_instance"]);
37
+ const CROSS_INSTANCE_TOOLS = new Set(["send_to_instance", "broadcast", "list_instances", "start_instance", "restart_instance", "create_instance", "delete_instance", "replace_instance", "request_information", "delegate_task", "report_result", "delivery_status", "describe_instance"]);
37
38
  const SCHEDULE_TOOLS = new Set(["create_schedule", "list_schedules", "update_schedule", "delete_schedule"]);
38
39
  const DECISION_TOOLS = new Set(["post_decision", "list_decisions", "update_decision"]);
39
40
  const TASK_TOOL = "task";
@@ -1095,6 +1096,7 @@ export class Daemon extends EventEmitter {
1095
1096
  /** Mutual exclusion for *every* write into the pane, whichever subsystem it
1096
1097
  * comes from. See PaneWriteLock for why interleaving is destructive. */
1097
1098
  paneWriteLock = new PaneWriteLock();
1099
+ deliveryWritesStopping = false;
1098
1100
  pendingInstructionsUpdate;
1099
1101
  pendingInstructionsNotice = false;
1100
1102
  // Whether the warmup steering-reload notice should be injected after spawn.
@@ -1331,6 +1333,22 @@ export class Daemon extends EventEmitter {
1331
1333
  setDeliveryOutboxPort(port) {
1332
1334
  this.deliveryOutbox = port;
1333
1335
  }
1336
+ /** The fleet's other same-backend instances' working directories, read at spawn (#984). */
1337
+ peerWorkingDirectories;
1338
+ setPeerWorkingDirectories(peers) {
1339
+ this.peerWorkingDirectories = peers;
1340
+ }
1341
+ /** Fence pane writers before lifecycle captures pre-kill reconciliation evidence. */
1342
+ fenceDeliveryWritesForStop() {
1343
+ if (this.deliveryWritesStopping)
1344
+ return;
1345
+ this.deliveryWritesStopping = true;
1346
+ this.deliveryEpoch++;
1347
+ }
1348
+ /** Bounded drain: timeout remains safe because Enter has a write-ahead fence. */
1349
+ waitForDeliveryWritesToDrain(timeoutMs) {
1350
+ return this.paneWriteLock.waitForIdle(timeoutMs);
1351
+ }
1334
1352
  durableDeliveryAttempt(meta) {
1335
1353
  const deliveryId = meta.delivery_id;
1336
1354
  if (!deliveryId)
@@ -1340,16 +1358,44 @@ export class Daemon extends EventEmitter {
1340
1358
  return false;
1341
1359
  return { deliveryId, attemptNo };
1342
1360
  }
1343
- beginDurableDelivery(delivery) {
1361
+ beginDurableDelivery(delivery, evidence) {
1344
1362
  if (!this.deliveryOutbox) {
1345
1363
  this.emit("durable_delivery_deferred", { ...delivery, targetBootId: this.bootId, reason: "outbox port unavailable before pane write" });
1346
1364
  return false;
1347
1365
  }
1348
- const outcome = this.deliveryOutbox.begin(delivery.deliveryId, this.bootId, delivery.attemptNo);
1366
+ const outcome = this.deliveryOutbox.begin(delivery.deliveryId, this.bootId, delivery.attemptNo, evidence);
1349
1367
  // A repeated begin is idempotent at the store but must not authorize a
1350
1368
  // second pane write. Only the first caller that acquired the permit acts.
1351
1369
  return outcome === "begun";
1352
1370
  }
1371
+ async durableAttemptEvidence(windowId, handingOffToNativeQueue, steer, rawPaste = false) {
1372
+ let checkpoint = null;
1373
+ try {
1374
+ checkpoint = await this.transcriptMonitor?.reconciliationCheckpoint() ?? null;
1375
+ }
1376
+ catch (err) {
1377
+ this.logger.debug({ err }, "Could not checkpoint transcript before durable delivery");
1378
+ }
1379
+ const submissionMode = rawPaste ? "raw_paste" : steer
1380
+ ? "steer"
1381
+ : handingOffToNativeQueue ? "native_queue_handoff" : "idle_submit";
1382
+ const backend = this.config.backend ?? this.backend?.binaryName ?? "unknown";
1383
+ // The daemon does not synchronously probe CLI versions on the delivery
1384
+ // path. Until lifecycle supplies a version tied to this exact process,
1385
+ // queue handoffs persist an unknown resume contract and reconciliation
1386
+ // must fail closed rather than infer loss from transcript absence.
1387
+ const backendVersion = null;
1388
+ return {
1389
+ backend,
1390
+ backendVersion,
1391
+ windowId: windowId ?? this.tmux?.getWindowId() ?? null,
1392
+ transcriptPath: checkpoint?.path ?? null,
1393
+ transcriptOffset: checkpoint?.offset ?? null,
1394
+ transcriptSessionId: checkpoint?.sessionId ?? null,
1395
+ submissionMode,
1396
+ queueResumePolicy: queueResumePolicyForAttempt(backend, backendVersion, submissionMode),
1397
+ };
1398
+ }
1353
1399
  abortDurableDelivery(delivery, reason) {
1354
1400
  if (!delivery)
1355
1401
  return;
@@ -1489,8 +1535,7 @@ export class Daemon extends EventEmitter {
1489
1535
  });
1490
1536
  }
1491
1537
  else if (msg.type === "raw_paste") {
1492
- // Paste raw text directly to CLI without [user:] wrapping.
1493
- this.queueRawPaste(msg.content, this.captureDeliveryEpoch(msg.delivery_epoch), msg.confirm_clear === true);
1538
+ this.handleRawPasteIpcMessage(msg);
1494
1539
  }
1495
1540
  else if (msg.type === "config_update") {
1496
1541
  this.applyConfigUpdate(msg.config);
@@ -2765,25 +2810,77 @@ export class Daemon extends EventEmitter {
2765
2810
  isDeliveryEpochCurrent(epoch) {
2766
2811
  return epoch === this.deliveryEpoch;
2767
2812
  }
2768
- queueRawPaste(rawText, deliveryEpoch = this.deliveryEpoch, confirmClear = false) {
2769
- if (!this.tmux || !this.isDeliveryEpochCurrent(deliveryEpoch))
2813
+ queueRawPaste(rawText, deliveryEpoch = this.deliveryEpoch, confirmClear = false, durableAttempt) {
2814
+ if (!this.tmux || !this.isDeliveryEpochCurrent(deliveryEpoch)) {
2815
+ if (durableAttempt)
2816
+ this.retryDurableDeliveryBeforeBegin(durableAttempt, "raw_paste target unavailable before pane queue");
2770
2817
  return;
2818
+ }
2771
2819
  this.pasteLock = this.pasteLock.then(async () => {
2772
2820
  if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
2773
2821
  this.logger.info("Pending raw delivery dropped by user cancel");
2822
+ if (durableAttempt)
2823
+ this.retryDurableDeliveryBeforeBegin(durableAttempt, "raw_paste epoch changed while queued");
2774
2824
  return;
2775
2825
  }
2776
- await this.wake();
2777
- if (!this.isDeliveryEpochCurrent(deliveryEpoch))
2826
+ const verdict = { reached: false };
2827
+ try {
2828
+ await this.wake();
2829
+ if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
2830
+ if (durableAttempt)
2831
+ this.retryDurableDeliveryBeforeBegin(durableAttempt, "raw_paste epoch changed while waking");
2832
+ return;
2833
+ }
2834
+ const delivered = await this.deliverMessage(rawText, undefined, {
2835
+ deliveryEpoch,
2836
+ verdict,
2837
+ durableAttempt,
2838
+ preserveExactBytes: true,
2839
+ });
2840
+ if (delivered)
2841
+ this.finishDurableSubmission(durableAttempt ?? null, verdict);
2842
+ else if (durableAttempt && verdict.paneWriteStarted) {
2843
+ this.finishDurableDelivery(durableAttempt, "uncertain", `${verdict.phase ?? "raw-paste"}:${verdict.proof ?? "unverified"}`);
2844
+ }
2845
+ else if (durableAttempt && verdict.durableBeginCommitted) {
2846
+ this.abortDurableDelivery(durableAttempt, `${verdict.phase ?? "raw-paste"}:${verdict.proof ?? "no-pane-write"}`);
2847
+ }
2848
+ else if (durableAttempt) {
2849
+ this.retryDurableDeliveryBeforeBegin(durableAttempt, `${verdict.phase ?? "raw-paste"}:${verdict.proof ?? "deferred"}`);
2850
+ }
2851
+ }
2852
+ catch (err) {
2853
+ if (durableAttempt && verdict.paneWriteStarted)
2854
+ this.finishDurableDelivery(durableAttempt, "uncertain", err.message);
2855
+ else if (durableAttempt && verdict.durableBeginCommitted)
2856
+ this.abortDurableDelivery(durableAttempt, err.message);
2857
+ else if (durableAttempt)
2858
+ this.retryDurableDeliveryBeforeBegin(durableAttempt, err.message);
2859
+ this.logger.warn({ err: err.message }, "raw_paste delivery error");
2778
2860
  return;
2779
- await this.deliverMessage(rawText, undefined, { deliveryEpoch });
2861
+ }
2780
2862
  if (confirmClear)
2781
2863
  await this.confirmBackendClearDialog();
2782
2864
  this.logger.debug({ text: rawText.slice(0, 100) }, "Raw paste delivered");
2783
- }).catch(err => {
2784
- this.logger.warn({ err: err.message }, "raw_paste delivery error");
2865
+ });
2866
+ this.pasteLock = this.pasteLock.catch(err => {
2867
+ this.logger.warn({ err: err.message }, "raw_paste delivery queue error");
2785
2868
  });
2786
2869
  }
2870
+ /** Keep the IPC raw route's durable mode attached to the parsed attempt. */
2871
+ handleRawPasteIpcMessage(msg) {
2872
+ // Paste raw text directly to CLI without [user:] wrapping.
2873
+ const deliveryMeta = {
2874
+ delivery_id: typeof msg.delivery_id === "string" ? msg.delivery_id : "",
2875
+ delivery_attempt: typeof msg.delivery_attempt === "string" ? msg.delivery_attempt : "",
2876
+ };
2877
+ const durableAttempt = this.durableDeliveryAttempt(deliveryMeta);
2878
+ if (durableAttempt === false) {
2879
+ this.deferDurableDelivery(deliveryMeta, "invalid raw_paste delivery attempt metadata");
2880
+ return;
2881
+ }
2882
+ this.queueRawPaste(msg.content, this.captureDeliveryEpoch(msg.delivery_epoch), msg.confirm_clear === true, durableAttempt ? { ...durableAttempt, submissionMode: "raw_paste" } : undefined);
2883
+ }
2787
2884
  /**
2788
2885
  * Confirm a backend's terminal-level /clear prompt after the channel-level
2789
2886
  * admin confirmation has already succeeded. Poll instead of pasting `/clear\ny`
@@ -2930,6 +3027,7 @@ export class Daemon extends EventEmitter {
2930
3027
  }
2931
3028
  async stop() {
2932
3029
  this.logger.info("Stopping daemon instance");
3030
+ this.fenceDeliveryWritesForStop();
2933
3031
  this.turnReplyGuard.reset();
2934
3032
  // Invalidate any bounded pre-Enter wait from the process generation being
2935
3033
  // stopped. It must fail closed, not press Enter in a replacement pane.
@@ -4641,7 +4739,7 @@ export class Daemon extends EventEmitter {
4641
4739
  return false;
4642
4740
  // Sanitize unclosed code fences — they cause CLI to wait for closure on Enter
4643
4741
  const fenceCount = (formatted.match(/```/g) || []).length;
4644
- if (fenceCount % 2 !== 0) {
4742
+ if (!opts?.preserveExactBytes && fenceCount % 2 !== 0) {
4645
4743
  // Odd number of fences = unclosed. Remove all code fences from the message.
4646
4744
  formatted = formatted.replace(/```/g, "");
4647
4745
  }
@@ -4789,7 +4887,7 @@ export class Daemon extends EventEmitter {
4789
4887
  }
4790
4888
  if (!(await this.hasPositiveDeliveryInput()))
4791
4889
  return "dialog";
4792
- return this.writeMessageToPane(formatted, windowId, handingOffToNativeQueue, status, opts?.submissionId, verdict, opts?.durableAttempt);
4890
+ return this.writeMessageToPane(formatted, windowId, handingOffToNativeQueue, status, opts?.submissionId, verdict, opts?.durableAttempt, opts?.steer === true);
4793
4891
  });
4794
4892
  if (outcome === "spawn-started") {
4795
4893
  // The pane changed under this delivery: its queued paste must not land
@@ -5543,13 +5641,20 @@ export class Daemon extends EventEmitter {
5543
5641
  * precisely so the lock's scope is visible at the call site rather than being
5544
5642
  * an invariant maintained by comments.
5545
5643
  */
5546
- async sendDeliveryEnter(phase, stillCurrent) {
5644
+ async sendDeliveryEnter(phase, stillCurrent, durableAttempt) {
5547
5645
  if (!(await this.waitForInputTransientToClear(phase)))
5548
5646
  return false;
5549
5647
  // A recovery Enter may have waited for a transient while cancel or spawn
5550
5648
  // replaced the delivery. Check at the last point before the tmux write.
5551
- if (stillCurrent && !stillCurrent())
5649
+ if (this.deliveryWritesStopping || (stillCurrent && !stillCurrent()))
5650
+ return false;
5651
+ // W1: persist the "Enter may have happened" boundary before tmux receives
5652
+ // the key. A crash after this commit is never auto-replayed without a
5653
+ // positive transcript marker, even if the stale pane still shows composer.
5654
+ if (durableAttempt && !this.deliveryOutbox?.markEnterStarted(durableAttempt.deliveryId, this.bootId, durableAttempt.attemptNo)) {
5655
+ this.logger.error({ phase, deliveryId: durableAttempt.deliveryId }, "Could not commit durable Enter write-ahead fence");
5552
5656
  return false;
5657
+ }
5553
5658
  const sent = await this.tmux.sendSpecialKey("Enter");
5554
5659
  if (!sent) {
5555
5660
  this.logger.error({
@@ -5562,8 +5667,9 @@ export class Daemon extends EventEmitter {
5562
5667
  async writeMessageToPane(formatted, initialWindowId, handingOffToNativeQueue, status, submissionId,
5563
5668
  // Last and defaulted so the positional callers that ignore the outcome stay
5564
5669
  // readable; deliverMessage, the only one that reports, always passes its own.
5565
- verdict = { reached: false }, durableAttempt) {
5670
+ verdict = { reached: false }, durableAttempt, steer = false) {
5566
5671
  const signature = this.submissionSignature(formatted, submissionId);
5672
+ const rawPaste = durableAttempt?.submissionMode === "raw_paste";
5567
5673
  let windowId = initialWindowId;
5568
5674
  // Bug A: paste with backoff. Transient failures are usually a stale window id
5569
5675
  // after a crash/respawn — recover by name and retry (max 3 attempts, 2s apart).
@@ -5579,7 +5685,8 @@ export class Daemon extends EventEmitter {
5579
5685
  // first side effect; a crash during those waits remains safely replayable.
5580
5686
  if (durableAttempt && !beginChecked) {
5581
5687
  beginChecked = true;
5582
- if (!this.beginDurableDelivery(durableAttempt)) {
5688
+ const attemptEvidence = await this.durableAttemptEvidence(windowId, handingOffToNativeQueue, steer, durableAttempt.submissionMode === "raw_paste");
5689
+ if (!this.beginDurableDelivery(durableAttempt, attemptEvidence)) {
5583
5690
  verdict.durableBeginRejected = true;
5584
5691
  verdict.phase = "submission-begin";
5585
5692
  verdict.proof = "permit-rejected";
@@ -5587,8 +5694,19 @@ export class Daemon extends EventEmitter {
5587
5694
  }
5588
5695
  verdict.durableBeginCommitted = true;
5589
5696
  }
5697
+ if (rawPaste)
5698
+ verdict.paneWriteStarted = true;
5699
+ // For raw commands the paste itself may alter the live composer. Treat a
5700
+ // failed/throwing write as uncertain rather than retrying bytes into the
5701
+ // same pane; the write-ahead Enter marker later separates safe pre-Enter
5702
+ // recovery from a possibly executed command.
5590
5703
  const pasted = await this.tmux.pasteBuffer(formatted);
5591
5704
  if (!pasted) {
5705
+ if (rawPaste) {
5706
+ verdict.phase = "raw-paste-write";
5707
+ verdict.proof = "paste-result-unknown";
5708
+ return false;
5709
+ }
5592
5710
  const tmuxError = this.tmux.getLastPasteError?.() ?? "unknown tmux paste failure";
5593
5711
  const recoverable = this.tmux.isLastPasteFailureRecoverable?.() ?? true;
5594
5712
  this.logger[recoverable ? "warn" : "error"]({ attempt, tmuxError, recoverable }, recoverable
@@ -5630,7 +5748,7 @@ export class Daemon extends EventEmitter {
5630
5748
  settle = { settleMs: fallbackMs, observedPostPasteOutput: false, capHit: false, usedFallback: true };
5631
5749
  }
5632
5750
  let enterAt = Date.now();
5633
- if (!(await this.sendDeliveryEnter("initial-submit"))) {
5751
+ if (!(await this.sendDeliveryEnter("initial-submit", undefined, durableAttempt))) {
5634
5752
  return this.failDelivery(verdict, status, "submit-enter", "tmux-send-keys-failed");
5635
5753
  }
5636
5754
  // Kiro's legacy TUI can swallow Enter while it is still processing a large
@@ -5646,7 +5764,7 @@ export class Daemon extends EventEmitter {
5646
5764
  // re-baselined to the second Enter so leftover paste-render output between
5647
5765
  // the two cannot be what "confirms" the submission.
5648
5766
  let enterRetry = false;
5649
- if (this.backend?.requiresDeliveryEnterRetry?.() === true) {
5767
+ if (!rawPaste && this.backend?.requiresDeliveryEnterRetry?.() === true) {
5650
5768
  await new Promise(r => setTimeout(r, 1_000));
5651
5769
  const retryAt = Date.now();
5652
5770
  if (await this.sendDeliveryEnter("queue-less-defensive-retry")) {
@@ -5697,6 +5815,13 @@ export class Daemon extends EventEmitter {
5697
5815
  this.emit("message_confirmed", status); // ✅ (best-effort)
5698
5816
  return true;
5699
5817
  }
5818
+ // raw_paste has no unique marker and is not allowed to re-paste or
5819
+ // issue another Enter after its one committed submission attempt.
5820
+ if (rawPaste) {
5821
+ verdict.phase = "raw-paste-native-queue-proof";
5822
+ verdict.proof = proof;
5823
+ return false;
5824
+ }
5700
5825
  // Not submitted (or not provably submitted): fall back once to the
5701
5826
  // normal idle-gated path, which is the only one with a full
5702
5827
  // confirmation ladder. "stranded" is the case that used to be a silent
@@ -5806,6 +5931,11 @@ export class Daemon extends EventEmitter {
5806
5931
  }
5807
5932
  }
5808
5933
  if (!submitted) {
5934
+ if (rawPaste) {
5935
+ verdict.phase = "raw-paste-submit-proof";
5936
+ verdict.proof = "no-positive-acceptance-proof";
5937
+ return false;
5938
+ }
5809
5939
  // Legacy system pastes have no unique envelope id. Preserve their
5810
5940
  // existing bounded retry until they can be tied to a specific turn.
5811
5941
  this.logger.warn("Message not submitted after Enter — waiting for the prompt, then re-sending Enter once");
@@ -5836,7 +5966,7 @@ export class Daemon extends EventEmitter {
5836
5966
  // is disqualifying and no amount of output may override it. Where it
5837
5967
  // cannot (a backend with no prompt pattern), the output signal remains
5838
5968
  // exactly as before — this must not regress backends we cannot read.
5839
- const becameBusy = await this.confirmAfterEnter(windowId, enterAt, signature, pasteBaseline, "idle-to-busy-retry");
5969
+ const becameBusy = await this.confirmAfterEnter(windowId, enterAt, signature, pasteBaseline, "idle-to-busy-retry", !rawPaste);
5840
5970
  if (becameBusy) {
5841
5971
  if (status)
5842
5972
  this.emit("message_confirmed", status); // ✅
@@ -5865,6 +5995,14 @@ export class Daemon extends EventEmitter {
5865
5995
  }
5866
5996
  }
5867
5997
  else {
5998
+ // No control client means a raw command has no acceptance proof. The
5999
+ // write-ahead Enter fence keeps it uncertain after this point; never
6000
+ // send a second Enter that could execute a command twice.
6001
+ if (rawPaste) {
6002
+ verdict.phase = "raw-paste-submit-proof";
6003
+ verdict.proof = "unverified";
6004
+ return true;
6005
+ }
5868
6006
  // No control client to observe output: fall back to the legacy double-Enter.
5869
6007
  await new Promise(r => setTimeout(r, 1000));
5870
6008
  await this.sendDeliveryEnter("unobserved-defensive-retry");
@@ -5920,10 +6058,12 @@ export class Daemon extends EventEmitter {
5920
6058
  * cannot reappear in just one of them (it did: three copies, two of which
5921
6059
  * still confirmed on output alone).
5922
6060
  */
5923
- async confirmAfterEnter(windowId, enterAt, signature, baseline, retryPhase) {
6061
+ async confirmAfterEnter(windowId, enterAt, signature, baseline, retryPhase, allowRecoveryEnter = true) {
5924
6062
  if (!this.canProveSubmission()) {
5925
6063
  let busy = await this.confirmBusyAfterEnter(windowId, enterAt);
5926
6064
  if (!busy) {
6065
+ if (!allowRecoveryEnter)
6066
+ return false;
5927
6067
  this.logger.warn("No idle→busy transition after Enter — re-sending Enter once");
5928
6068
  const retryAt = Date.now();
5929
6069
  if (!(await this.sendDeliveryEnter(retryPhase)))
@@ -5947,7 +6087,7 @@ export class Daemon extends EventEmitter {
5947
6087
  break;
5948
6088
  await new Promise(r => setTimeout(r, POST_ENTER_PROOF_POLL_MS));
5949
6089
  }
5950
- if (proof !== "stranded")
6090
+ if (proof !== "stranded" || !allowRecoveryEnter)
5951
6091
  return false;
5952
6092
  if (!(await this.waitForPaneReadyForDelivery(windowId, STRANDED_RETRY_READY_WAIT_MS)))
5953
6093
  return false;
@@ -5974,6 +6114,8 @@ export class Daemon extends EventEmitter {
5974
6114
  // how we got here. Then poll briefly rather than judging on one capture:
5975
6115
  // the key takes a moment to be digested, and a single early look would
5976
6116
  // report a failure for a message that did go out.
6117
+ if (!allowRecoveryEnter)
6118
+ return false;
5977
6119
  this.logger.warn("Enter did not submit — waiting for the prompt, then re-sending once");
5978
6120
  if (!(await this.waitForPaneReadyForDelivery(windowId, STRANDED_RETRY_READY_WAIT_MS)))
5979
6121
  return false;
@@ -6705,6 +6847,7 @@ export class Daemon extends EventEmitter {
6705
6847
  agentMode: isCliMode ? "cli" : "mcp",
6706
6848
  agentPort: isCliMode ? agentPort : undefined,
6707
6849
  backendOptions,
6850
+ peerWorkingDirectories: this.peerWorkingDirectories,
6708
6851
  };
6709
6852
  }
6710
6853
  /**
@@ -7410,6 +7553,13 @@ export class Daemon extends EventEmitter {
7410
7553
  }
7411
7554
  }
7412
7555
  const cmd = `${envPrefix} ` + this.backend.buildCommand(launchConfig);
7556
+ // e.g. Codex could not read its session DB and chose a fallback (#984):
7557
+ // the operator should know which conversation this launch continues.
7558
+ const launchWarning = this.backend.consumeLaunchWarning?.() ?? null;
7559
+ if (launchWarning) {
7560
+ this.logger.warn({ warning: launchWarning }, "Backend launch fell back");
7561
+ this.emit("backend_launch_warning", { name: this.name, message: launchWarning });
7562
+ }
7413
7563
  // Ensure tmux session exists (may have been destroyed if all windows died)
7414
7564
  await TmuxManager.ensureSession(this.tmuxSessionName);
7415
7565
  if (this.stormWindow?.observeServerAlive(await TmuxManager.getServerPid(this.tmuxSessionName))) {