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

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 (98) 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 +37 -9
  12. package/dist/backend/codex.js +204 -38
  13. package/dist/backend/codex.js.map +1 -1
  14. package/dist/backend/grok.js +51 -0
  15. package/dist/backend/grok.js.map +1 -1
  16. package/dist/backend/types.d.ts +12 -0
  17. package/dist/backend/types.js.map +1 -1
  18. package/dist/channel/mcp-tools.js +2 -0
  19. package/dist/channel/mcp-tools.js.map +1 -1
  20. package/dist/cli.js +137 -124
  21. package/dist/cli.js.map +1 -1
  22. package/dist/completion-install.d.ts +32 -0
  23. package/dist/completion-install.js +125 -1
  24. package/dist/completion-install.js.map +1 -1
  25. package/dist/config-validator.js +4 -0
  26. package/dist/config-validator.js.map +1 -1
  27. package/dist/daemon.d.ts +12 -1
  28. package/dist/daemon.js +186 -27
  29. package/dist/daemon.js.map +1 -1
  30. package/dist/delivery-outbox.d.ts +199 -6
  31. package/dist/delivery-outbox.js +602 -44
  32. package/dist/delivery-outbox.js.map +1 -1
  33. package/dist/delivery-outbox.test.js +268 -14
  34. package/dist/delivery-outbox.test.js.map +1 -1
  35. package/dist/delivery-queue-evidence.d.ts +15 -0
  36. package/dist/delivery-queue-evidence.js +35 -0
  37. package/dist/delivery-queue-evidence.js.map +1 -0
  38. package/dist/delivery-queue-evidence.test.d.ts +1 -0
  39. package/dist/delivery-queue-evidence.test.js +23 -0
  40. package/dist/delivery-queue-evidence.test.js.map +1 -0
  41. package/dist/delivery-reconciliation.d.ts +49 -0
  42. package/dist/delivery-reconciliation.js +296 -0
  43. package/dist/delivery-reconciliation.js.map +1 -0
  44. package/dist/delivery-reconciliation.test.d.ts +1 -0
  45. package/dist/delivery-reconciliation.test.js +411 -0
  46. package/dist/delivery-reconciliation.test.js.map +1 -0
  47. package/dist/fleet-manager.d.ts +10 -1
  48. package/dist/fleet-manager.js +109 -32
  49. package/dist/fleet-manager.js.map +1 -1
  50. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +10 -0
  51. package/dist/instance-lifecycle.d.ts +13 -0
  52. package/dist/instance-lifecycle.js +105 -21
  53. package/dist/instance-lifecycle.js.map +1 -1
  54. package/dist/instructions.d.ts +6 -0
  55. package/dist/instructions.js +9 -0
  56. package/dist/instructions.js.map +1 -1
  57. package/dist/locale.js +8 -0
  58. package/dist/locale.js.map +1 -1
  59. package/dist/ls-rows.d.ts +50 -0
  60. package/dist/ls-rows.js +152 -0
  61. package/dist/ls-rows.js.map +1 -0
  62. package/dist/outbound-handlers.d.ts +3 -0
  63. package/dist/outbound-handlers.js +38 -2
  64. package/dist/outbound-handlers.js.map +1 -1
  65. package/dist/outbound-schemas.d.ts +8 -0
  66. package/dist/outbound-schemas.js +15 -0
  67. package/dist/outbound-schemas.js.map +1 -1
  68. package/dist/pane-write-lock.d.ts +2 -0
  69. package/dist/pane-write-lock.js +19 -0
  70. package/dist/pane-write-lock.js.map +1 -1
  71. package/dist/quickstart.js +9 -3
  72. package/dist/quickstart.js.map +1 -1
  73. package/dist/remote-credential-check.d.ts +26 -7
  74. package/dist/remote-credential-check.js +157 -10
  75. package/dist/remote-credential-check.js.map +1 -1
  76. package/dist/scheduler/scheduler.d.ts +5 -1
  77. package/dist/scheduler/scheduler.js +60 -20
  78. package/dist/scheduler/scheduler.js.map +1 -1
  79. package/dist/scheduler/scheduler.test.js +41 -2
  80. package/dist/scheduler/scheduler.test.js.map +1 -1
  81. package/dist/scheduler/types.d.ts +1 -1
  82. package/dist/tmux-manager.d.ts +3 -3
  83. package/dist/tmux-manager.js +6 -6
  84. package/dist/tmux-manager.js.map +1 -1
  85. package/dist/tool-permissions.js +6 -6
  86. package/dist/tool-permissions.js.map +1 -1
  87. package/dist/transcript-monitor.d.ts +3 -1
  88. package/dist/transcript-monitor.js +15 -0
  89. package/dist/transcript-monitor.js.map +1 -1
  90. package/dist/transcript-sources.d.ts +9 -0
  91. package/dist/transcript-sources.js +14 -0
  92. package/dist/transcript-sources.js.map +1 -1
  93. package/dist/types.d.ts +6 -0
  94. package/dist/ui/dashboard.html +7 -5
  95. package/dist/ui/view.html +138 -45
  96. package/dist/usage/usage-api.js +30 -0
  97. package/dist/usage/usage-api.js.map +1 -1
  98. package/package.json +1 -1
package/dist/daemon.js CHANGED
@@ -30,10 +30,12 @@ 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 { deliveryContentDigest } from "./delivery-outbox.js";
34
+ import { queueResumePolicyForAttempt } from "./delivery-queue-evidence.js";
33
35
  const __filename = fileURLToPath(import.meta.url);
34
36
  const __dirname = dirname(__filename);
35
37
  // 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"]);
38
+ 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
39
  const SCHEDULE_TOOLS = new Set(["create_schedule", "list_schedules", "update_schedule", "delete_schedule"]);
38
40
  const DECISION_TOOLS = new Set(["post_decision", "list_decisions", "update_decision"]);
39
41
  const TASK_TOOL = "task";
@@ -1095,6 +1097,7 @@ export class Daemon extends EventEmitter {
1095
1097
  /** Mutual exclusion for *every* write into the pane, whichever subsystem it
1096
1098
  * comes from. See PaneWriteLock for why interleaving is destructive. */
1097
1099
  paneWriteLock = new PaneWriteLock();
1100
+ deliveryWritesStopping = false;
1098
1101
  pendingInstructionsUpdate;
1099
1102
  pendingInstructionsNotice = false;
1100
1103
  // Whether the warmup steering-reload notice should be injected after spawn.
@@ -1331,25 +1334,71 @@ export class Daemon extends EventEmitter {
1331
1334
  setDeliveryOutboxPort(port) {
1332
1335
  this.deliveryOutbox = port;
1333
1336
  }
1334
- durableDeliveryAttempt(meta) {
1337
+ /** The fleet's other same-backend instances' working directories, read at spawn (#984). */
1338
+ peerWorkingDirectories;
1339
+ setPeerWorkingDirectories(peers) {
1340
+ this.peerWorkingDirectories = peers;
1341
+ }
1342
+ /** Fence pane writers before lifecycle captures pre-kill reconciliation evidence. */
1343
+ fenceDeliveryWritesForStop() {
1344
+ if (this.deliveryWritesStopping)
1345
+ return;
1346
+ this.deliveryWritesStopping = true;
1347
+ this.deliveryEpoch++;
1348
+ }
1349
+ /** Bounded drain: timeout remains safe because Enter has a write-ahead fence. */
1350
+ waitForDeliveryWritesToDrain(timeoutMs) {
1351
+ return this.paneWriteLock.waitForIdle(timeoutMs);
1352
+ }
1353
+ durableDeliveryAttempt(meta, content) {
1335
1354
  const deliveryId = meta.delivery_id;
1336
1355
  if (!deliveryId)
1337
1356
  return null;
1338
1357
  const attemptNo = Number(meta.delivery_attempt);
1339
1358
  if (!Number.isSafeInteger(attemptNo) || attemptNo < 1)
1340
1359
  return false;
1341
- return { deliveryId, attemptNo };
1360
+ // Hashed on arrival, before any formatting, so the outbox can compare it
1361
+ // with the digest taken at admission (#856).
1362
+ return { deliveryId, attemptNo, ...(content !== undefined ? { contentSha256: deliveryContentDigest(content) } : {}) };
1342
1363
  }
1343
- beginDurableDelivery(delivery) {
1364
+ beginDurableDelivery(delivery, evidence) {
1344
1365
  if (!this.deliveryOutbox) {
1345
1366
  this.emit("durable_delivery_deferred", { ...delivery, targetBootId: this.bootId, reason: "outbox port unavailable before pane write" });
1346
1367
  return false;
1347
1368
  }
1348
- const outcome = this.deliveryOutbox.begin(delivery.deliveryId, this.bootId, delivery.attemptNo);
1369
+ const outcome = this.deliveryOutbox.begin(delivery.deliveryId, this.bootId, delivery.attemptNo, evidence);
1349
1370
  // A repeated begin is idempotent at the store but must not authorize a
1350
1371
  // second pane write. Only the first caller that acquired the permit acts.
1351
1372
  return outcome === "begun";
1352
1373
  }
1374
+ async durableAttemptEvidence(windowId, handingOffToNativeQueue, steer, rawPaste = false) {
1375
+ let checkpoint = null;
1376
+ try {
1377
+ checkpoint = await this.transcriptMonitor?.reconciliationCheckpoint() ?? null;
1378
+ }
1379
+ catch (err) {
1380
+ this.logger.debug({ err }, "Could not checkpoint transcript before durable delivery");
1381
+ }
1382
+ const submissionMode = rawPaste ? "raw_paste" : steer
1383
+ ? "steer"
1384
+ : handingOffToNativeQueue ? "native_queue_handoff" : "idle_submit";
1385
+ const backend = this.config.backend ?? this.backend?.binaryName ?? "unknown";
1386
+ // The daemon does not synchronously probe CLI versions on the delivery
1387
+ // path. Until lifecycle supplies a version tied to this exact process,
1388
+ // queue handoffs persist an unknown resume contract and reconciliation
1389
+ // must fail closed rather than infer loss from transcript absence.
1390
+ const backendVersion = null;
1391
+ return {
1392
+ backend,
1393
+ backendVersion,
1394
+ windowId: windowId ?? this.tmux?.getWindowId() ?? null,
1395
+ transcriptPath: checkpoint?.path ?? null,
1396
+ transcriptOffset: checkpoint?.offset ?? null,
1397
+ transcriptSessionId: checkpoint?.sessionId ?? null,
1398
+ submissionMode,
1399
+ queueResumePolicy: queueResumePolicyForAttempt(backend, backendVersion, submissionMode),
1400
+ };
1401
+ }
1353
1402
  abortDurableDelivery(delivery, reason) {
1354
1403
  if (!delivery)
1355
1404
  return;
@@ -1489,8 +1538,7 @@ export class Daemon extends EventEmitter {
1489
1538
  });
1490
1539
  }
1491
1540
  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);
1541
+ this.handleRawPasteIpcMessage(msg);
1494
1542
  }
1495
1543
  else if (msg.type === "config_update") {
1496
1544
  this.applyConfigUpdate(msg.config);
@@ -2765,25 +2813,77 @@ export class Daemon extends EventEmitter {
2765
2813
  isDeliveryEpochCurrent(epoch) {
2766
2814
  return epoch === this.deliveryEpoch;
2767
2815
  }
2768
- queueRawPaste(rawText, deliveryEpoch = this.deliveryEpoch, confirmClear = false) {
2769
- if (!this.tmux || !this.isDeliveryEpochCurrent(deliveryEpoch))
2816
+ queueRawPaste(rawText, deliveryEpoch = this.deliveryEpoch, confirmClear = false, durableAttempt) {
2817
+ if (!this.tmux || !this.isDeliveryEpochCurrent(deliveryEpoch)) {
2818
+ if (durableAttempt)
2819
+ this.retryDurableDeliveryBeforeBegin(durableAttempt, "raw_paste target unavailable before pane queue");
2770
2820
  return;
2821
+ }
2771
2822
  this.pasteLock = this.pasteLock.then(async () => {
2772
2823
  if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
2773
2824
  this.logger.info("Pending raw delivery dropped by user cancel");
2825
+ if (durableAttempt)
2826
+ this.retryDurableDeliveryBeforeBegin(durableAttempt, "raw_paste epoch changed while queued");
2774
2827
  return;
2775
2828
  }
2776
- await this.wake();
2777
- if (!this.isDeliveryEpochCurrent(deliveryEpoch))
2829
+ const verdict = { reached: false };
2830
+ try {
2831
+ await this.wake();
2832
+ if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
2833
+ if (durableAttempt)
2834
+ this.retryDurableDeliveryBeforeBegin(durableAttempt, "raw_paste epoch changed while waking");
2835
+ return;
2836
+ }
2837
+ const delivered = await this.deliverMessage(rawText, undefined, {
2838
+ deliveryEpoch,
2839
+ verdict,
2840
+ durableAttempt,
2841
+ preserveExactBytes: true,
2842
+ });
2843
+ if (delivered)
2844
+ this.finishDurableSubmission(durableAttempt ?? null, verdict);
2845
+ else if (durableAttempt && verdict.paneWriteStarted) {
2846
+ this.finishDurableDelivery(durableAttempt, "uncertain", `${verdict.phase ?? "raw-paste"}:${verdict.proof ?? "unverified"}`);
2847
+ }
2848
+ else if (durableAttempt && verdict.durableBeginCommitted) {
2849
+ this.abortDurableDelivery(durableAttempt, `${verdict.phase ?? "raw-paste"}:${verdict.proof ?? "no-pane-write"}`);
2850
+ }
2851
+ else if (durableAttempt) {
2852
+ this.retryDurableDeliveryBeforeBegin(durableAttempt, `${verdict.phase ?? "raw-paste"}:${verdict.proof ?? "deferred"}`);
2853
+ }
2854
+ }
2855
+ catch (err) {
2856
+ if (durableAttempt && verdict.paneWriteStarted)
2857
+ this.finishDurableDelivery(durableAttempt, "uncertain", err.message);
2858
+ else if (durableAttempt && verdict.durableBeginCommitted)
2859
+ this.abortDurableDelivery(durableAttempt, err.message);
2860
+ else if (durableAttempt)
2861
+ this.retryDurableDeliveryBeforeBegin(durableAttempt, err.message);
2862
+ this.logger.warn({ err: err.message }, "raw_paste delivery error");
2778
2863
  return;
2779
- await this.deliverMessage(rawText, undefined, { deliveryEpoch });
2864
+ }
2780
2865
  if (confirmClear)
2781
2866
  await this.confirmBackendClearDialog();
2782
2867
  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");
2868
+ });
2869
+ this.pasteLock = this.pasteLock.catch(err => {
2870
+ this.logger.warn({ err: err.message }, "raw_paste delivery queue error");
2785
2871
  });
2786
2872
  }
2873
+ /** Keep the IPC raw route's durable mode attached to the parsed attempt. */
2874
+ handleRawPasteIpcMessage(msg) {
2875
+ // Paste raw text directly to CLI without [user:] wrapping.
2876
+ const deliveryMeta = {
2877
+ delivery_id: typeof msg.delivery_id === "string" ? msg.delivery_id : "",
2878
+ delivery_attempt: typeof msg.delivery_attempt === "string" ? msg.delivery_attempt : "",
2879
+ };
2880
+ const durableAttempt = this.durableDeliveryAttempt(deliveryMeta, typeof msg.content === "string" ? msg.content : undefined);
2881
+ if (durableAttempt === false) {
2882
+ this.deferDurableDelivery(deliveryMeta, "invalid raw_paste delivery attempt metadata");
2883
+ return;
2884
+ }
2885
+ this.queueRawPaste(msg.content, this.captureDeliveryEpoch(msg.delivery_epoch), msg.confirm_clear === true, durableAttempt ? { ...durableAttempt, submissionMode: "raw_paste" } : undefined);
2886
+ }
2787
2887
  /**
2788
2888
  * Confirm a backend's terminal-level /clear prompt after the channel-level
2789
2889
  * admin confirmation has already succeeded. Poll instead of pasting `/clear\ny`
@@ -2930,6 +3030,7 @@ export class Daemon extends EventEmitter {
2930
3030
  }
2931
3031
  async stop() {
2932
3032
  this.logger.info("Stopping daemon instance");
3033
+ this.fenceDeliveryWritesForStop();
2933
3034
  this.turnReplyGuard.reset();
2934
3035
  // Invalidate any bounded pre-Enter wait from the process generation being
2935
3036
  // stopped. It must fail closed, not press Enter in a replacement pane.
@@ -4368,7 +4469,7 @@ export class Daemon extends EventEmitter {
4368
4469
  const formatted = "[STEERING — mid-task course correction. Fold this into the CURRENT work if one is active.]\n"
4369
4470
  + this.formatInboundMessage(content, meta);
4370
4471
  const status = channelStatus(meta);
4371
- const durableAttempt = this.durableDeliveryAttempt(meta);
4472
+ const durableAttempt = this.durableDeliveryAttempt(meta, content);
4372
4473
  if (durableAttempt === false) {
4373
4474
  this.deferDurableDelivery(meta, "invalid durable delivery attempt metadata");
4374
4475
  return;
@@ -4548,7 +4649,7 @@ export class Daemon extends EventEmitter {
4548
4649
  // the previous turn's tool list.
4549
4650
  this.resetToolProgress();
4550
4651
  verdict = { reached: false };
4551
- const durableAttempt = this.durableDeliveryAttempt(meta);
4652
+ const durableAttempt = this.durableDeliveryAttempt(meta, content);
4552
4653
  if (durableAttempt === false) {
4553
4654
  this.deferDurableDelivery(meta, "invalid durable delivery attempt metadata");
4554
4655
  return;
@@ -4641,7 +4742,7 @@ export class Daemon extends EventEmitter {
4641
4742
  return false;
4642
4743
  // Sanitize unclosed code fences — they cause CLI to wait for closure on Enter
4643
4744
  const fenceCount = (formatted.match(/```/g) || []).length;
4644
- if (fenceCount % 2 !== 0) {
4745
+ if (!opts?.preserveExactBytes && fenceCount % 2 !== 0) {
4645
4746
  // Odd number of fences = unclosed. Remove all code fences from the message.
4646
4747
  formatted = formatted.replace(/```/g, "");
4647
4748
  }
@@ -4789,7 +4890,7 @@ export class Daemon extends EventEmitter {
4789
4890
  }
4790
4891
  if (!(await this.hasPositiveDeliveryInput()))
4791
4892
  return "dialog";
4792
- return this.writeMessageToPane(formatted, windowId, handingOffToNativeQueue, status, opts?.submissionId, verdict, opts?.durableAttempt);
4893
+ return this.writeMessageToPane(formatted, windowId, handingOffToNativeQueue, status, opts?.submissionId, verdict, opts?.durableAttempt, opts?.steer === true);
4793
4894
  });
4794
4895
  if (outcome === "spawn-started") {
4795
4896
  // The pane changed under this delivery: its queued paste must not land
@@ -5543,13 +5644,20 @@ export class Daemon extends EventEmitter {
5543
5644
  * precisely so the lock's scope is visible at the call site rather than being
5544
5645
  * an invariant maintained by comments.
5545
5646
  */
5546
- async sendDeliveryEnter(phase, stillCurrent) {
5647
+ async sendDeliveryEnter(phase, stillCurrent, durableAttempt) {
5547
5648
  if (!(await this.waitForInputTransientToClear(phase)))
5548
5649
  return false;
5549
5650
  // A recovery Enter may have waited for a transient while cancel or spawn
5550
5651
  // replaced the delivery. Check at the last point before the tmux write.
5551
- if (stillCurrent && !stillCurrent())
5652
+ if (this.deliveryWritesStopping || (stillCurrent && !stillCurrent()))
5653
+ return false;
5654
+ // W1: persist the "Enter may have happened" boundary before tmux receives
5655
+ // the key. A crash after this commit is never auto-replayed without a
5656
+ // positive transcript marker, even if the stale pane still shows composer.
5657
+ if (durableAttempt && !this.deliveryOutbox?.markEnterStarted(durableAttempt.deliveryId, this.bootId, durableAttempt.attemptNo)) {
5658
+ this.logger.error({ phase, deliveryId: durableAttempt.deliveryId }, "Could not commit durable Enter write-ahead fence");
5552
5659
  return false;
5660
+ }
5553
5661
  const sent = await this.tmux.sendSpecialKey("Enter");
5554
5662
  if (!sent) {
5555
5663
  this.logger.error({
@@ -5562,8 +5670,9 @@ export class Daemon extends EventEmitter {
5562
5670
  async writeMessageToPane(formatted, initialWindowId, handingOffToNativeQueue, status, submissionId,
5563
5671
  // Last and defaulted so the positional callers that ignore the outcome stay
5564
5672
  // readable; deliverMessage, the only one that reports, always passes its own.
5565
- verdict = { reached: false }, durableAttempt) {
5673
+ verdict = { reached: false }, durableAttempt, steer = false) {
5566
5674
  const signature = this.submissionSignature(formatted, submissionId);
5675
+ const rawPaste = durableAttempt?.submissionMode === "raw_paste";
5567
5676
  let windowId = initialWindowId;
5568
5677
  // Bug A: paste with backoff. Transient failures are usually a stale window id
5569
5678
  // after a crash/respawn — recover by name and retry (max 3 attempts, 2s apart).
@@ -5579,7 +5688,14 @@ export class Daemon extends EventEmitter {
5579
5688
  // first side effect; a crash during those waits remains safely replayable.
5580
5689
  if (durableAttempt && !beginChecked) {
5581
5690
  beginChecked = true;
5582
- if (!this.beginDurableDelivery(durableAttempt)) {
5691
+ const attemptEvidence = {
5692
+ ...await this.durableAttemptEvidence(windowId, handingOffToNativeQueue, steer, durableAttempt.submissionMode === "raw_paste"),
5693
+ // #856: what arrived vs what is about to be written, taken at the
5694
+ // fence that already precedes the first pane side effect.
5695
+ pastedContentSha256: durableAttempt.contentSha256 ?? null,
5696
+ pastedBytesSha256: deliveryContentDigest(formatted),
5697
+ };
5698
+ if (!this.beginDurableDelivery(durableAttempt, attemptEvidence)) {
5583
5699
  verdict.durableBeginRejected = true;
5584
5700
  verdict.phase = "submission-begin";
5585
5701
  verdict.proof = "permit-rejected";
@@ -5587,8 +5703,19 @@ export class Daemon extends EventEmitter {
5587
5703
  }
5588
5704
  verdict.durableBeginCommitted = true;
5589
5705
  }
5706
+ if (rawPaste)
5707
+ verdict.paneWriteStarted = true;
5708
+ // For raw commands the paste itself may alter the live composer. Treat a
5709
+ // failed/throwing write as uncertain rather than retrying bytes into the
5710
+ // same pane; the write-ahead Enter marker later separates safe pre-Enter
5711
+ // recovery from a possibly executed command.
5590
5712
  const pasted = await this.tmux.pasteBuffer(formatted);
5591
5713
  if (!pasted) {
5714
+ if (rawPaste) {
5715
+ verdict.phase = "raw-paste-write";
5716
+ verdict.proof = "paste-result-unknown";
5717
+ return false;
5718
+ }
5592
5719
  const tmuxError = this.tmux.getLastPasteError?.() ?? "unknown tmux paste failure";
5593
5720
  const recoverable = this.tmux.isLastPasteFailureRecoverable?.() ?? true;
5594
5721
  this.logger[recoverable ? "warn" : "error"]({ attempt, tmuxError, recoverable }, recoverable
@@ -5630,7 +5757,7 @@ export class Daemon extends EventEmitter {
5630
5757
  settle = { settleMs: fallbackMs, observedPostPasteOutput: false, capHit: false, usedFallback: true };
5631
5758
  }
5632
5759
  let enterAt = Date.now();
5633
- if (!(await this.sendDeliveryEnter("initial-submit"))) {
5760
+ if (!(await this.sendDeliveryEnter("initial-submit", undefined, durableAttempt))) {
5634
5761
  return this.failDelivery(verdict, status, "submit-enter", "tmux-send-keys-failed");
5635
5762
  }
5636
5763
  // Kiro's legacy TUI can swallow Enter while it is still processing a large
@@ -5646,7 +5773,7 @@ export class Daemon extends EventEmitter {
5646
5773
  // re-baselined to the second Enter so leftover paste-render output between
5647
5774
  // the two cannot be what "confirms" the submission.
5648
5775
  let enterRetry = false;
5649
- if (this.backend?.requiresDeliveryEnterRetry?.() === true) {
5776
+ if (!rawPaste && this.backend?.requiresDeliveryEnterRetry?.() === true) {
5650
5777
  await new Promise(r => setTimeout(r, 1_000));
5651
5778
  const retryAt = Date.now();
5652
5779
  if (await this.sendDeliveryEnter("queue-less-defensive-retry")) {
@@ -5697,6 +5824,13 @@ export class Daemon extends EventEmitter {
5697
5824
  this.emit("message_confirmed", status); // ✅ (best-effort)
5698
5825
  return true;
5699
5826
  }
5827
+ // raw_paste has no unique marker and is not allowed to re-paste or
5828
+ // issue another Enter after its one committed submission attempt.
5829
+ if (rawPaste) {
5830
+ verdict.phase = "raw-paste-native-queue-proof";
5831
+ verdict.proof = proof;
5832
+ return false;
5833
+ }
5700
5834
  // Not submitted (or not provably submitted): fall back once to the
5701
5835
  // normal idle-gated path, which is the only one with a full
5702
5836
  // confirmation ladder. "stranded" is the case that used to be a silent
@@ -5806,6 +5940,11 @@ export class Daemon extends EventEmitter {
5806
5940
  }
5807
5941
  }
5808
5942
  if (!submitted) {
5943
+ if (rawPaste) {
5944
+ verdict.phase = "raw-paste-submit-proof";
5945
+ verdict.proof = "no-positive-acceptance-proof";
5946
+ return false;
5947
+ }
5809
5948
  // Legacy system pastes have no unique envelope id. Preserve their
5810
5949
  // existing bounded retry until they can be tied to a specific turn.
5811
5950
  this.logger.warn("Message not submitted after Enter — waiting for the prompt, then re-sending Enter once");
@@ -5836,7 +5975,7 @@ export class Daemon extends EventEmitter {
5836
5975
  // is disqualifying and no amount of output may override it. Where it
5837
5976
  // cannot (a backend with no prompt pattern), the output signal remains
5838
5977
  // 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");
5978
+ const becameBusy = await this.confirmAfterEnter(windowId, enterAt, signature, pasteBaseline, "idle-to-busy-retry", !rawPaste);
5840
5979
  if (becameBusy) {
5841
5980
  if (status)
5842
5981
  this.emit("message_confirmed", status); // ✅
@@ -5865,6 +6004,14 @@ export class Daemon extends EventEmitter {
5865
6004
  }
5866
6005
  }
5867
6006
  else {
6007
+ // No control client means a raw command has no acceptance proof. The
6008
+ // write-ahead Enter fence keeps it uncertain after this point; never
6009
+ // send a second Enter that could execute a command twice.
6010
+ if (rawPaste) {
6011
+ verdict.phase = "raw-paste-submit-proof";
6012
+ verdict.proof = "unverified";
6013
+ return true;
6014
+ }
5868
6015
  // No control client to observe output: fall back to the legacy double-Enter.
5869
6016
  await new Promise(r => setTimeout(r, 1000));
5870
6017
  await this.sendDeliveryEnter("unobserved-defensive-retry");
@@ -5920,10 +6067,12 @@ export class Daemon extends EventEmitter {
5920
6067
  * cannot reappear in just one of them (it did: three copies, two of which
5921
6068
  * still confirmed on output alone).
5922
6069
  */
5923
- async confirmAfterEnter(windowId, enterAt, signature, baseline, retryPhase) {
6070
+ async confirmAfterEnter(windowId, enterAt, signature, baseline, retryPhase, allowRecoveryEnter = true) {
5924
6071
  if (!this.canProveSubmission()) {
5925
6072
  let busy = await this.confirmBusyAfterEnter(windowId, enterAt);
5926
6073
  if (!busy) {
6074
+ if (!allowRecoveryEnter)
6075
+ return false;
5927
6076
  this.logger.warn("No idle→busy transition after Enter — re-sending Enter once");
5928
6077
  const retryAt = Date.now();
5929
6078
  if (!(await this.sendDeliveryEnter(retryPhase)))
@@ -5947,7 +6096,7 @@ export class Daemon extends EventEmitter {
5947
6096
  break;
5948
6097
  await new Promise(r => setTimeout(r, POST_ENTER_PROOF_POLL_MS));
5949
6098
  }
5950
- if (proof !== "stranded")
6099
+ if (proof !== "stranded" || !allowRecoveryEnter)
5951
6100
  return false;
5952
6101
  if (!(await this.waitForPaneReadyForDelivery(windowId, STRANDED_RETRY_READY_WAIT_MS)))
5953
6102
  return false;
@@ -5974,6 +6123,8 @@ export class Daemon extends EventEmitter {
5974
6123
  // how we got here. Then poll briefly rather than judging on one capture:
5975
6124
  // the key takes a moment to be digested, and a single early look would
5976
6125
  // report a failure for a message that did go out.
6126
+ if (!allowRecoveryEnter)
6127
+ return false;
5977
6128
  this.logger.warn("Enter did not submit — waiting for the prompt, then re-sending once");
5978
6129
  if (!(await this.waitForPaneReadyForDelivery(windowId, STRANDED_RETRY_READY_WAIT_MS)))
5979
6130
  return false;
@@ -6705,6 +6856,7 @@ export class Daemon extends EventEmitter {
6705
6856
  agentMode: isCliMode ? "cli" : "mcp",
6706
6857
  agentPort: isCliMode ? agentPort : undefined,
6707
6858
  backendOptions,
6859
+ peerWorkingDirectories: this.peerWorkingDirectories,
6708
6860
  };
6709
6861
  }
6710
6862
  /**
@@ -7410,6 +7562,13 @@ export class Daemon extends EventEmitter {
7410
7562
  }
7411
7563
  }
7412
7564
  const cmd = `${envPrefix} ` + this.backend.buildCommand(launchConfig);
7565
+ // e.g. Codex could not read its session DB and chose a fallback (#984):
7566
+ // the operator should know which conversation this launch continues.
7567
+ const launchWarning = this.backend.consumeLaunchWarning?.() ?? null;
7568
+ if (launchWarning) {
7569
+ this.logger.warn({ warning: launchWarning }, "Backend launch fell back");
7570
+ this.emit("backend_launch_warning", { name: this.name, message: launchWarning });
7571
+ }
7413
7572
  // Ensure tmux session exists (may have been destroyed if all windows died)
7414
7573
  await TmuxManager.ensureSession(this.tmuxSessionName);
7415
7574
  if (this.stormWindow?.observeServerAlive(await TmuxManager.getServerPid(this.tmuxSessionName))) {