@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.
- package/README.md +2 -0
- package/README.zh-TW.md +2 -0
- package/dist/agent-cli-instructions.md +5 -0
- package/dist/agent-cli.js +13 -0
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +1 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/backend/codex-session-lookup.d.ts +59 -0
- package/dist/backend/codex-session-lookup.js +139 -0
- package/dist/backend/codex-session-lookup.js.map +1 -0
- package/dist/backend/codex.d.ts +11 -0
- package/dist/backend/codex.js +115 -7
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/types.d.ts +12 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/mcp-tools.js +2 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/cli.js +68 -2
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts +11 -0
- package/dist/daemon.js +173 -23
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +90 -6
- package/dist/delivery-outbox.js +301 -39
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-outbox.test.js +268 -14
- package/dist/delivery-outbox.test.js.map +1 -1
- package/dist/delivery-queue-evidence.d.ts +15 -0
- package/dist/delivery-queue-evidence.js +35 -0
- package/dist/delivery-queue-evidence.js.map +1 -0
- package/dist/delivery-queue-evidence.test.d.ts +1 -0
- package/dist/delivery-queue-evidence.test.js +23 -0
- package/dist/delivery-queue-evidence.test.js.map +1 -0
- package/dist/delivery-reconciliation.d.ts +49 -0
- package/dist/delivery-reconciliation.js +296 -0
- package/dist/delivery-reconciliation.js.map +1 -0
- package/dist/delivery-reconciliation.test.d.ts +1 -0
- package/dist/delivery-reconciliation.test.js +411 -0
- package/dist/delivery-reconciliation.test.js.map +1 -0
- package/dist/fleet-manager.d.ts +2 -1
- package/dist/fleet-manager.js +61 -32
- package/dist/fleet-manager.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +13 -0
- package/dist/instance-lifecycle.js +85 -10
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.js +1 -0
- package/dist/instructions.js.map +1 -1
- package/dist/locale.js +6 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-handlers.d.ts +3 -0
- package/dist/outbound-handlers.js +38 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +7 -0
- package/dist/outbound-schemas.js +14 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/pane-write-lock.d.ts +2 -0
- package/dist/pane-write-lock.js +19 -0
- package/dist/pane-write-lock.js.map +1 -1
- package/dist/scheduler/scheduler.d.ts +5 -1
- package/dist/scheduler/scheduler.js +60 -20
- package/dist/scheduler/scheduler.js.map +1 -1
- package/dist/scheduler/scheduler.test.js +41 -2
- package/dist/scheduler/scheduler.test.js.map +1 -1
- package/dist/scheduler/types.d.ts +1 -1
- package/dist/tmux-manager.d.ts +3 -3
- package/dist/tmux-manager.js +6 -6
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tool-permissions.js +6 -6
- package/dist/tool-permissions.js.map +1 -1
- package/dist/transcript-monitor.d.ts +3 -1
- package/dist/transcript-monitor.js +15 -0
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +9 -0
- package/dist/transcript-sources.js +14 -0
- package/dist/transcript-sources.js.map +1 -1
- 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
|
-
|
|
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
|
-
|
|
2777
|
-
|
|
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
|
-
|
|
2861
|
+
}
|
|
2780
2862
|
if (confirmClear)
|
|
2781
2863
|
await this.confirmBackendClearDialog();
|
|
2782
2864
|
this.logger.debug({ text: rawText.slice(0, 100) }, "Raw paste delivered");
|
|
2783
|
-
})
|
|
2784
|
-
|
|
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
|
-
|
|
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))) {
|