@songsid/agend 2.3.4-beta.2 → 2.3.4-beta.4
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/dist/auth-api.js +3 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/claude-code.js +1 -1
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-session-lookup.d.ts +35 -0
- package/dist/backend/codex-session-lookup.js +93 -0
- package/dist/backend/codex-session-lookup.js.map +1 -1
- package/dist/backend/codex.d.ts +27 -0
- package/dist/backend/codex.js +116 -20
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok.d.ts +2 -0
- package/dist/backend/grok.js +22 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/types.d.ts +2 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.js +3 -1
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.js +3 -1
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/inbox-temp-name.d.ts +9 -0
- package/dist/channel/inbox-temp-name.js +13 -0
- package/dist/channel/inbox-temp-name.js.map +1 -0
- package/dist/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/daemon.d.ts +10 -1
- package/dist/daemon.js +196 -26
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.test.js +2 -2
- package/dist/delivery-outbox.test.js.map +1 -1
- package/dist/dialog-screen-text.d.ts +16 -0
- package/dist/dialog-screen-text.js +51 -0
- package/dist/dialog-screen-text.js.map +1 -0
- package/dist/fleet-manager.d.ts +4 -0
- package/dist/fleet-manager.js +46 -12
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +3 -2
- package/dist/install-stream.d.ts +53 -0
- package/dist/install-stream.js +134 -0
- package/dist/install-stream.js.map +1 -0
- package/dist/instance-lifecycle.d.ts +12 -0
- package/dist/instance-lifecycle.js +31 -2
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +26 -6
- package/dist/locale.js.map +1 -1
- package/dist/log-service.d.ts +2 -0
- package/dist/log-service.js +7 -3
- package/dist/log-service.js.map +1 -1
- package/dist/login-manager.d.ts +6 -0
- package/dist/login-manager.js +10 -1
- package/dist/login-manager.js.map +1 -1
- package/dist/office-share-page.d.ts +1 -1
- package/dist/office-share-page.js +6 -2
- package/dist/office-share-page.js.map +1 -1
- package/dist/outbound-handlers.js +56 -0
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +1 -0
- package/dist/outbound-schemas.js +4 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/selection-card.d.ts +2 -3
- package/dist/selection-card.js +99 -26
- package/dist/selection-card.js.map +1 -1
- package/dist/tmux-manager.d.ts +6 -0
- package/dist/tmux-manager.js +17 -0
- package/dist/tmux-manager.js.map +1 -1
- package/dist/ui/chat-store.js +20 -3
- package/dist/ui/fleet-canvas.js +4 -3
- package/dist/ui/fleet-office-model.js +55 -22
- package/dist/ui/fleet-office-overview.js +6 -38
- package/dist/ui/fleet-office-pack.js +4 -3
- package/dist/ui/fleet-office-skin.js +114 -21
- package/dist/ui/fleet-office-store.js +5 -0
- package/dist/ui/fleet-office-zones.js +2 -2
- package/dist/ui/fleet-office.js +41 -9
- package/dist/ui/panel-log.js +7 -1
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-shell.js +4 -2
- package/dist/ui/shared/app-stream.js +26 -0
- package/dist/ui/shared/app.css +21 -1
- package/dist/ui/shared/office/guild.png +0 -0
- package/dist/ui/shared/office/school.png +0 -0
- package/dist/ui/shared/panel-view.js +185 -10
- package/dist/ui/shared/view-strings.js +23 -0
- package/dist/unattended-install.d.ts +6 -0
- package/dist/unattended-install.js +65 -0
- package/dist/unattended-install.js.map +1 -0
- package/dist/view-api.d.ts +15 -0
- package/dist/view-api.js +94 -33
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.js +7 -2
- package/dist/web-api.js.map +1 -1
- package/dist/web-chat-history.d.ts +20 -0
- package/dist/web-chat-history.js +21 -0
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-guest-access.js +5 -1
- package/dist/web-guest-access.js.map +1 -1
- package/dist/web-office-pack-tools.d.ts +1 -1
- package/dist/web-office-pack-tools.js +1 -1
- package/dist/web-office-pack-tools.js.map +1 -1
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dialogScreenText } from "./dialog-screen-text.js";
|
|
1
2
|
import { readActiveKiroModel } from "./backend/kiro.js";
|
|
2
3
|
import { dashboardAccessOf } from "./dashboard-permissions.js";
|
|
3
4
|
import { sendPackSources } from "./web-office-pack-read.js";
|
|
@@ -531,6 +532,9 @@ function countOccurrences(haystack, needle) {
|
|
|
531
532
|
}
|
|
532
533
|
/** Bottom-ready re-poll cadence for Enter-dropping TUIs once the pane is quiet. */
|
|
533
534
|
const BOTTOM_READY_POLL_MS = 250;
|
|
535
|
+
/** M-2267: how often a pane left in a tmux mode is looked at again, and how long the delivery Enter waits for it to end. */
|
|
536
|
+
const PANE_MODE_POLL_MS = 500;
|
|
537
|
+
const PANE_MODE_ENTER_WAIT_MS = 10 * 60_000;
|
|
534
538
|
/** Consecutive unreadable pane probes tolerated by the delivery gate before the delivery is failed (≈10s at the poll cadence). */
|
|
535
539
|
const DIALOG_PROBE_UNKNOWN_MAX = 40;
|
|
536
540
|
/** Startup dialog scan: poll cadence and wall-clock budget (keys and waits included). */
|
|
@@ -3706,7 +3710,9 @@ export class Daemon extends EventEmitter {
|
|
|
3706
3710
|
const interactivePrompt = this.interactivePromptDetector.observe(pane, Date.now(), this.instanceStateLastOutputAt);
|
|
3707
3711
|
if (interactivePrompt) {
|
|
3708
3712
|
this.logger.warn(interactivePrompt, "Interactive terminal prompt is waiting for human input");
|
|
3709
|
-
|
|
3713
|
+
// M-2269: the prompt as it was on screen, from this capture, for the Log.
|
|
3714
|
+
const screen = dialogScreenText(pane);
|
|
3715
|
+
this.emit("interactive_prompt", { name: this.name, ...interactivePrompt, ...(screen ? { screen } : {}) });
|
|
3710
3716
|
// A prompt is not an error and must not enter the PTY recovery gate.
|
|
3711
3717
|
// Continue scanning real errors in this same snapshot.
|
|
3712
3718
|
}
|
|
@@ -3901,7 +3907,7 @@ export class Daemon extends EventEmitter {
|
|
|
3901
3907
|
else if (dialog.verifyAfterKeys && !resolved) {
|
|
3902
3908
|
this.logger.warn({ dialog: dialog.description }, "Safety dialog choice was sent but follow-up verification or notice submission failed");
|
|
3903
3909
|
}
|
|
3904
|
-
this.trackDialogParked(blockingSeen);
|
|
3910
|
+
this.trackDialogParked(blockingSeen, pane);
|
|
3905
3911
|
return; // Dialog handled (or deliberately deferred): skip error checks this cycle
|
|
3906
3912
|
}
|
|
3907
3913
|
// Reached only when no dialog was answered this poll (none on screen, a fenced one, or one waiting out its
|
|
@@ -3911,7 +3917,7 @@ export class Daemon extends EventEmitter {
|
|
|
3911
3917
|
this.dialogAnswers = null;
|
|
3912
3918
|
this.retireParkedNotice("answer");
|
|
3913
3919
|
}
|
|
3914
|
-
this.trackDialogParked(blockingSeen);
|
|
3920
|
+
this.trackDialogParked(blockingSeen, pane);
|
|
3915
3921
|
this.trackRemoteCard(blockingSeen, pane, captureMono);
|
|
3916
3922
|
if (blockingSeen)
|
|
3917
3923
|
return; // held dialog: skip error checks this cycle
|
|
@@ -7768,6 +7774,9 @@ export class Daemon extends EventEmitter {
|
|
|
7768
7774
|
// The hand-off's last capture had no box: nothing was begun or written — wait for readiness, then try again.
|
|
7769
7775
|
if (written === "handoff-box-unread")
|
|
7770
7776
|
return "dialog";
|
|
7777
|
+
// M-2267: the pane is in a tmux mode: nothing written — wait for readiness (it holds while the mode lasts), retry.
|
|
7778
|
+
if (written === "pane-in-mode")
|
|
7779
|
+
return "dialog";
|
|
7771
7780
|
// Fenced before its first write: not attempted — redo after a spawn, else drop without a ❌.
|
|
7772
7781
|
return written === false && verdict.fenced ? stale() : written;
|
|
7773
7782
|
});
|
|
@@ -8241,7 +8250,7 @@ export class Daemon extends EventEmitter {
|
|
|
8241
8250
|
&& this.hasAutoDialogClaim(dialog)
|
|
8242
8251
|
&& dialogs.some(other => other !== dialog && Daemon.dialogMatches(other, pane)))
|
|
8243
8252
|
continue;
|
|
8244
|
-
this.trackDialogParked(dialog);
|
|
8253
|
+
this.trackDialogParked(dialog, pane);
|
|
8245
8254
|
this.trackRemoteCard(dialog, pane, probeMono);
|
|
8246
8255
|
return { state: "dialog", dialog };
|
|
8247
8256
|
}
|
|
@@ -8620,7 +8629,8 @@ export class Daemon extends EventEmitter {
|
|
|
8620
8629
|
this.parkedNoticeOut = null;
|
|
8621
8630
|
this.emit("dialog_unparked", { name: this.name });
|
|
8622
8631
|
}
|
|
8623
|
-
|
|
8632
|
+
/** `pane`: the capture the caller already holds — M-2269 keeps the dialog's text from it for the Log (no new capture). */
|
|
8633
|
+
trackDialogParked(dialog, pane = "") {
|
|
8624
8634
|
if (!dialog) {
|
|
8625
8635
|
if (this.dialogParkedSince !== 0)
|
|
8626
8636
|
this.logger.info({ dialog: this.dialogParkedKey }, "Blocking dialog is gone from the pane");
|
|
@@ -8658,7 +8668,9 @@ export class Daemon extends EventEmitter {
|
|
|
8658
8668
|
this.dialogParkedReported = true;
|
|
8659
8669
|
this.parkedNoticeOut = "parked";
|
|
8660
8670
|
this.logger.warn({ dialog: dialog.description, parkedForMs: Date.now() - this.dialogParkedSince }, "CLI dialog is still on screen — not auto-answering, reporting for a human");
|
|
8671
|
+
const screen = dialogScreenText(pane);
|
|
8661
8672
|
this.emit("dialog_parked", { name: this.name, description: dialog.description, holdOnly: dialog.holdOnly === true,
|
|
8673
|
+
...(screen ? { screen } : {}),
|
|
8662
8674
|
...(dialog.parkedAdvice ? { advice: dialog.parkedAdvice } : {}),
|
|
8663
8675
|
...(episode !== null && interaction.kind ? { kind: interaction.kind, episode, backend: this.config.backend ?? "claude-code" } : {}),
|
|
8664
8676
|
...(unrecognised ? { unrecognised: true, question: this.unrecognisedPrompt?.shape.question ?? null } : {}) });
|
|
@@ -9018,6 +9030,14 @@ export class Daemon extends EventEmitter {
|
|
|
9018
9030
|
: await this.waitForPaneIdleForDelivery(windowId, remaining);
|
|
9019
9031
|
if (idle !== true)
|
|
9020
9032
|
return idle;
|
|
9033
|
+
// M-2267: a person left the pane in a tmux mode (copy-mode): its keys would go to the mode. Hold — never take the
|
|
9034
|
+
// pane out of it — and look again; the delivery goes once the mode ends (or this wait's own deadline).
|
|
9035
|
+
if (await this.paneInTmuxMode("readiness")) {
|
|
9036
|
+
if (stop && await stop())
|
|
9037
|
+
return "stopped";
|
|
9038
|
+
await new Promise(r => setTimeout(r, BOTTOM_READY_POLL_MS));
|
|
9039
|
+
continue;
|
|
9040
|
+
}
|
|
9021
9041
|
// The idle wait just resolved, so re-reading the silence gate would only
|
|
9022
9042
|
// add a stale-cache race; ask the pane the remaining questions directly.
|
|
9023
9043
|
const transient = await this.probeInputTransient();
|
|
@@ -9563,17 +9583,133 @@ export class Daemon extends EventEmitter {
|
|
|
9563
9583
|
* precisely so the lock's scope is visible at the call site rather than being
|
|
9564
9584
|
* an invariant maintained by comments.
|
|
9565
9585
|
*/
|
|
9566
|
-
async sendDeliveryEnter(phase, stillCurrent, durableAttempt) {
|
|
9586
|
+
async sendDeliveryEnter(phase, stillCurrent, durableAttempt, onInputHeld) {
|
|
9567
9587
|
if (!(await this.waitForInputTransientToClear(phase)))
|
|
9568
9588
|
return false;
|
|
9569
|
-
return this.sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt);
|
|
9589
|
+
return this.sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt, onInputHeld);
|
|
9590
|
+
}
|
|
9591
|
+
/**
|
|
9592
|
+
* M-2267 (hmsg-b8a170c0): is the pane in a tmux mode (a person left it in copy-mode)? Logged once per stretch. Unknown
|
|
9593
|
+
* (unreadable) is not a mode: the delivery's own reads decide then, as before.
|
|
9594
|
+
*/
|
|
9595
|
+
paneModeSince = 0;
|
|
9596
|
+
async paneInTmuxMode(phase) {
|
|
9597
|
+
const inMode = await this.tmux?.paneInMode?.();
|
|
9598
|
+
if (inMode !== true) {
|
|
9599
|
+
this.paneModeSince = 0;
|
|
9600
|
+
return false;
|
|
9601
|
+
}
|
|
9602
|
+
if (!this.paneModeSince) {
|
|
9603
|
+
this.paneModeSince = performance.now();
|
|
9604
|
+
this.logger.warn({ phase }, "The pane is in a tmux mode (copy-mode) — delivery waits for it to end; no key is sent to it");
|
|
9605
|
+
}
|
|
9606
|
+
return true;
|
|
9607
|
+
}
|
|
9608
|
+
/** M-2267: wait while the pane is in a tmux mode, until `deadline` (PANE_MODE_ENTER_WAIT_MS) — "clear", "stale" (fenced), "held". */
|
|
9609
|
+
async waitWhilePaneInMode(phase, current, deadline = performance.now() + PANE_MODE_ENTER_WAIT_MS) {
|
|
9610
|
+
for (;;) {
|
|
9611
|
+
const inMode = await this.paneInTmuxMode(phase);
|
|
9612
|
+
if (!current())
|
|
9613
|
+
return "stale";
|
|
9614
|
+
if (!inMode)
|
|
9615
|
+
return "clear";
|
|
9616
|
+
if (performance.now() >= deadline)
|
|
9617
|
+
return "held";
|
|
9618
|
+
await new Promise(r => setTimeout(r, PANE_MODE_POLL_MS));
|
|
9619
|
+
if (!current())
|
|
9620
|
+
return "stale";
|
|
9621
|
+
}
|
|
9570
9622
|
}
|
|
9571
9623
|
/** Write half only: callers must finish the availability wait before any final recovery ownership proof. */
|
|
9572
|
-
async sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt) {
|
|
9624
|
+
async sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt, onInputHeld) {
|
|
9573
9625
|
// A recovery Enter may have waited for a transient while cancel or spawn
|
|
9574
9626
|
// replaced the delivery. Check at the last point before the tmux write.
|
|
9575
|
-
|
|
9627
|
+
const tmux = this.tmux;
|
|
9628
|
+
const backend = this.backend;
|
|
9629
|
+
const generation = this.spawnGeneration;
|
|
9630
|
+
const fence = this.launchFenceEpoch;
|
|
9631
|
+
const windowId = tmux?.getWindowId();
|
|
9632
|
+
const current = () => !!tmux && !!windowId && this.tmux === tmux && this.backend === backend
|
|
9633
|
+
&& this.spawnGeneration === generation && this.launchFenceEpoch === fence && tmux.getWindowId() === windowId
|
|
9634
|
+
&& !this.deliveryWritesStopping && (!stillCurrent || stillCurrent());
|
|
9635
|
+
if (!current())
|
|
9636
|
+
return false;
|
|
9637
|
+
// A paste-settle wait or retry can outlive the composer it saw. Passive startup-transient checks do not prove
|
|
9638
|
+
// that a permission card has not replaced it. Read this exact pane again, without resolving any dialog.
|
|
9639
|
+
const hold = () => {
|
|
9640
|
+
onInputHeld?.();
|
|
9641
|
+
this.logger.warn({ phase }, "Delivery input is not a verified composer; Enter withheld");
|
|
9576
9642
|
return false;
|
|
9643
|
+
};
|
|
9644
|
+
// M-2267 (#2971 review, Prism P2): a pane in a tmux mode would take this Enter. Wait for the mode to end FIRST, then
|
|
9645
|
+
// prove the composer (and that no dialog took its place) on a capture taken after it — and again whenever the pane
|
|
9646
|
+
// went back into a mode meanwhile: the Enter only ever goes on a proof newer than the last mode. Never ended for the
|
|
9647
|
+
// person, never answering a dialog that appeared during the wait (hold() then, as for any).
|
|
9648
|
+
const modeDeadline = performance.now() + PANE_MODE_ENTER_WAIT_MS;
|
|
9649
|
+
for (;;) {
|
|
9650
|
+
const mode = await this.waitWhilePaneInMode(phase, current, modeDeadline);
|
|
9651
|
+
if (mode === "stale")
|
|
9652
|
+
return false;
|
|
9653
|
+
if (mode === "held")
|
|
9654
|
+
return hold();
|
|
9655
|
+
let pane;
|
|
9656
|
+
try {
|
|
9657
|
+
pane = await tmux.capturePane();
|
|
9658
|
+
}
|
|
9659
|
+
catch {
|
|
9660
|
+
return current() ? hold() : false;
|
|
9661
|
+
}
|
|
9662
|
+
if (!current())
|
|
9663
|
+
return false;
|
|
9664
|
+
let composer = false;
|
|
9665
|
+
try {
|
|
9666
|
+
if (this.runtimeDialogs().some(dialog => Daemon.dialogMatches(dialog, pane)))
|
|
9667
|
+
return hold();
|
|
9668
|
+
if (backend?.readInputRow)
|
|
9669
|
+
composer = backend.readInputRow(pane) != null;
|
|
9670
|
+
else if (backend?.inputDraft)
|
|
9671
|
+
composer = backend.inputDraft(pane) != null;
|
|
9672
|
+
else if (backend?.isDeliveryInputReadyPane)
|
|
9673
|
+
composer = this.deliveryInputReadyPane(pane);
|
|
9674
|
+
else if (backend?.readSteerComposer?.(pane) != null)
|
|
9675
|
+
composer = true;
|
|
9676
|
+
else if (backend) {
|
|
9677
|
+
// Legacy backends still need their own current ready chrome, never a menu's cursor or old transcript row.
|
|
9678
|
+
const shape = unrecognisedPromptShape(pane);
|
|
9679
|
+
const ownComposer = backend.isDeliveryComposerPane?.(pane) === true;
|
|
9680
|
+
if ((!selectionCard(pane, "CLI") || ownComposer) && (!shape || ownComposer || this.cliComposerShowing(pane, shape))) {
|
|
9681
|
+
const bottom = backend.getBottomReadyPattern?.();
|
|
9682
|
+
const ready = backend.getReadyPattern();
|
|
9683
|
+
ready.lastIndex = 0;
|
|
9684
|
+
composer = ownComposer || (bottom ? bottomRowIsReady(pane, bottom)
|
|
9685
|
+
: ready.test(pane.trimEnd().split("\n").slice(-COMPOSER_TAIL_ROWS).join("\n")));
|
|
9686
|
+
}
|
|
9687
|
+
}
|
|
9688
|
+
}
|
|
9689
|
+
catch {
|
|
9690
|
+
return hold();
|
|
9691
|
+
}
|
|
9692
|
+
if (!composer)
|
|
9693
|
+
return hold();
|
|
9694
|
+
if (!current())
|
|
9695
|
+
return false;
|
|
9696
|
+
// The proof stands only if the pane did not go into a mode while it was made: else wait, and prove again — within
|
|
9697
|
+
// the same deadline (#2971 re-review, Prism P2: a pane flickering in and out of a mode must not hold the write lock
|
|
9698
|
+
// past it), and never spinning: one poll interval before the next look.
|
|
9699
|
+
if (await this.paneInTmuxMode(phase)) {
|
|
9700
|
+
if (!current())
|
|
9701
|
+
return false;
|
|
9702
|
+
if (performance.now() >= modeDeadline)
|
|
9703
|
+
return hold();
|
|
9704
|
+
await new Promise(r => setTimeout(r, PANE_MODE_POLL_MS));
|
|
9705
|
+
if (!current())
|
|
9706
|
+
return false;
|
|
9707
|
+
continue;
|
|
9708
|
+
}
|
|
9709
|
+
if (!current())
|
|
9710
|
+
return false;
|
|
9711
|
+
break;
|
|
9712
|
+
}
|
|
9577
9713
|
// W1: persist the "Enter may have happened" boundary before tmux receives
|
|
9578
9714
|
// the key. A crash after this commit is never auto-replayed without a
|
|
9579
9715
|
// positive transcript marker, even if the stale pane still shows composer.
|
|
@@ -9581,7 +9717,7 @@ export class Daemon extends EventEmitter {
|
|
|
9581
9717
|
this.logger.error({ phase, deliveryId: durableAttempt.deliveryId }, "Could not commit durable Enter write-ahead fence");
|
|
9582
9718
|
return false;
|
|
9583
9719
|
}
|
|
9584
|
-
const sent = await
|
|
9720
|
+
const sent = await tmux.sendSpecialKey("Enter");
|
|
9585
9721
|
if (!sent) {
|
|
9586
9722
|
this.logger.error({
|
|
9587
9723
|
phase,
|
|
@@ -9641,6 +9777,10 @@ export class Daemon extends EventEmitter {
|
|
|
9641
9777
|
verdict.fenced = !verdict.durableBeginCommitted;
|
|
9642
9778
|
return false;
|
|
9643
9779
|
}
|
|
9780
|
+
// M-2267: the pane went into a tmux mode after the readiness wait. Nothing begun or written: the caller waits for
|
|
9781
|
+
// readiness (which holds while it lasts) and tries again — as for a dialog that appeared before the write.
|
|
9782
|
+
if (!verdict.durableBeginCommitted && await this.paneInTmuxMode("paste"))
|
|
9783
|
+
return "pane-in-mode";
|
|
9644
9784
|
// A hand-off pastes and presses Enter into a busy pane: the box must still be readable on this, the last capture
|
|
9645
9785
|
// before the write (#1169 review) — a modal that appeared after the under-lock check would take that Enter. Before
|
|
9646
9786
|
// the durable begin the caller waits for readiness outside the lock and tries again; after it (a paste retry),
|
|
@@ -9737,6 +9877,22 @@ export class Daemon extends EventEmitter {
|
|
|
9737
9877
|
// failed/throwing write as uncertain rather than retrying bytes into the
|
|
9738
9878
|
// same pane; the write-ahead Enter marker later separates safe pre-Enter
|
|
9739
9879
|
// recovery from a possibly executed command.
|
|
9880
|
+
// M-2267 (#2971 review, Prism P2): EVERY paste attempt — a retry after a failed paste, a first paste whose durable
|
|
9881
|
+
// begin already went — asks too. Before the begin: nothing begun, the caller waits for readiness and tries again.
|
|
9882
|
+
// After it: this attempt waits here, keeping its durable attempt; a mode outlasting the wait fails it unwritten.
|
|
9883
|
+
if (!verdict.durableBeginCommitted) {
|
|
9884
|
+
if (await this.paneInTmuxMode("paste"))
|
|
9885
|
+
return "pane-in-mode";
|
|
9886
|
+
}
|
|
9887
|
+
else {
|
|
9888
|
+
const mode = await this.waitWhilePaneInMode("paste", () => (!stillCurrent || stillCurrent()) && !this.deliveryWritesStopping);
|
|
9889
|
+
if (mode === "stale") {
|
|
9890
|
+
verdict.fenced = false;
|
|
9891
|
+
return false;
|
|
9892
|
+
}
|
|
9893
|
+
if (mode === "held")
|
|
9894
|
+
return this.failDelivery(verdict, status, "paste", "pane-in-tmux-mode");
|
|
9895
|
+
}
|
|
9740
9896
|
const pasteGeneration = this.spawnGeneration;
|
|
9741
9897
|
beforePaste?.();
|
|
9742
9898
|
const pasted = await this.tmux.pasteBuffer(formatted);
|
|
@@ -9825,15 +9981,22 @@ export class Daemon extends EventEmitter {
|
|
|
9825
9981
|
let enterAt = Date.now();
|
|
9826
9982
|
const enterMono = performance.now();
|
|
9827
9983
|
const steerEnterCurrent = () => steerCurrent() && performance.now() <= steerEnterDeadline;
|
|
9828
|
-
|
|
9829
|
-
|
|
9984
|
+
let inputHeld = false;
|
|
9985
|
+
if (!(await this.sendDeliveryEnter("initial-submit", steerEnterCurrent, durableAttempt, () => { inputHeld = true; }))) {
|
|
9986
|
+
if (inputHeld) {
|
|
9987
|
+
// The paste may still be in the CLI. Keep the existing uncertain path; no false failure or blind replay.
|
|
9988
|
+
verdict.phase = "submit-enter";
|
|
9989
|
+
verdict.proof = "input-not-verified";
|
|
9990
|
+
return false;
|
|
9991
|
+
}
|
|
9992
|
+
if (!steerCurrent()) {
|
|
9830
9993
|
verdict.phase = "submit-enter";
|
|
9831
9994
|
verdict.proof = "fenced-before-enter";
|
|
9832
9995
|
return false;
|
|
9833
9996
|
}
|
|
9834
9997
|
if (steerComposerProof && performance.now() > steerEnterDeadline)
|
|
9835
9998
|
return this.failDelivery(verdict, status, "submit-enter", "pre-enter-deadline");
|
|
9836
|
-
return this.failDelivery(verdict, status, "submit-enter", "
|
|
9999
|
+
return this.failDelivery(verdict, status, "submit-enter", "enter-not-sent");
|
|
9837
10000
|
}
|
|
9838
10001
|
// The proof's whole budget, from the Enter: every capture and dialog probe inside it, a late answer dropped.
|
|
9839
10002
|
const steerProofDeadline = performance.now() + NATIVE_QUEUE_PASTE_VERIFY_MS + POST_ENTER_PROOF_WINDOW_MS;
|
|
@@ -9855,7 +10018,7 @@ export class Daemon extends EventEmitter {
|
|
|
9855
10018
|
if (!rawPaste && !steerComposerProof && this.backend?.requiresDeliveryEnterRetry?.() === true) {
|
|
9856
10019
|
await new Promise(r => setTimeout(r, 1_000));
|
|
9857
10020
|
const retryAt = Date.now();
|
|
9858
|
-
if (await this.sendDeliveryEnter("queue-less-defensive-retry")) {
|
|
10021
|
+
if (await this.sendDeliveryEnter("queue-less-defensive-retry", steerCurrent)) {
|
|
9859
10022
|
enterAt = retryAt;
|
|
9860
10023
|
enterRetry = true;
|
|
9861
10024
|
this.logger.debug("Sent defensive Enter retry (queue-less TUI can swallow the first)");
|
|
@@ -9986,7 +10149,7 @@ export class Daemon extends EventEmitter {
|
|
|
9986
10149
|
// again. The prompt is back now, so an Enter can land.
|
|
9987
10150
|
this.logger.warn("Message still in the input row after idle — submitting the existing text instead of pasting it again");
|
|
9988
10151
|
const strandedAt = Date.now();
|
|
9989
|
-
if (!(await this.sendDeliveryEnter("native-queue-stranded-submit"))) {
|
|
10152
|
+
if (!(await this.sendDeliveryEnter("native-queue-stranded-submit", steerCurrent))) {
|
|
9990
10153
|
return this.failDelivery(verdict, status, "native-queue-submit", "tmux-send-keys-failed");
|
|
9991
10154
|
}
|
|
9992
10155
|
// The recovery Enter can be accepted before Codex paints its new
|
|
@@ -10039,7 +10202,7 @@ export class Daemon extends EventEmitter {
|
|
|
10039
10202
|
}
|
|
10040
10203
|
await new Promise(r => setTimeout(r, NORMAL_ENTER_SETTLE_MS));
|
|
10041
10204
|
const retryAt = Date.now();
|
|
10042
|
-
if (!(await this.sendDeliveryEnter("native-queue-idle-redelivery"))) {
|
|
10205
|
+
if (!(await this.sendDeliveryEnter("native-queue-idle-redelivery", steerCurrent))) {
|
|
10043
10206
|
return this.failDelivery(verdict, status, "native-queue-redelivery", "tmux-send-keys-failed");
|
|
10044
10207
|
}
|
|
10045
10208
|
if (windowId && this.controlClient) {
|
|
@@ -10091,7 +10254,7 @@ export class Daemon extends EventEmitter {
|
|
|
10091
10254
|
this.logger.warn("Message not submitted after Enter — waiting for the prompt, then re-sending Enter once");
|
|
10092
10255
|
const promptBack = await this.waitForPaneReadyForDelivery(windowId, STRANDED_RETRY_READY_WAIT_MS);
|
|
10093
10256
|
const retryAt = Date.now();
|
|
10094
|
-
if (promptBack && !(await this.sendDeliveryEnter("stranded-text-retry"))) {
|
|
10257
|
+
if (promptBack && !(await this.sendDeliveryEnter("stranded-text-retry", steerCurrent))) {
|
|
10095
10258
|
return this.failDelivery(verdict, status, "stranded-text-retry", "tmux-send-keys-failed");
|
|
10096
10259
|
}
|
|
10097
10260
|
submitted = promptBack && await this.confirmSubmittedAfterEnter(windowId, retryAt, formatted);
|
|
@@ -10179,7 +10342,7 @@ export class Daemon extends EventEmitter {
|
|
|
10179
10342
|
}
|
|
10180
10343
|
// No control client to observe output: fall back to the legacy double-Enter.
|
|
10181
10344
|
await new Promise(r => setTimeout(r, 1000));
|
|
10182
|
-
await this.sendDeliveryEnter("unobserved-defensive-retry");
|
|
10345
|
+
await this.sendDeliveryEnter("unobserved-defensive-retry", steerCurrent);
|
|
10183
10346
|
if (status)
|
|
10184
10347
|
this.emit("message_confirmed", status); // ✅ (best-effort)
|
|
10185
10348
|
}
|
|
@@ -10647,20 +10810,27 @@ export class Daemon extends EventEmitter {
|
|
|
10647
10810
|
* `guard` is for a paste that must not outlive the reason it was made (the continue nudge): `accept` judges the ONE
|
|
10648
10811
|
* capture taken here — the same picture is the baseline, the expected-screen check and the idle check — and `current`
|
|
10649
10812
|
* is asked again after every await that precedes a write, so a cancelled paste adds no further paste and no key
|
|
10650
|
-
* (the retries included).
|
|
10813
|
+
* (the retries included). Every system paste also keeps the pane owner from before its first await.
|
|
10651
10814
|
*/
|
|
10652
10815
|
async submitSystemPaste(text, label, guard, admission) {
|
|
10653
|
-
const
|
|
10654
|
-
|
|
10816
|
+
const tmux = this.tmux, generation = this.spawnGeneration, fence = this.launchFenceEpoch;
|
|
10817
|
+
const windowId = tmux?.getWindowId();
|
|
10818
|
+
const current = () => {
|
|
10819
|
+
admission?.();
|
|
10820
|
+
return !!tmux && !!windowId && this.tmux === tmux && tmux.getWindowId() === windowId
|
|
10821
|
+
&& this.spawnGeneration === generation && this.launchFenceEpoch === fence
|
|
10822
|
+
&& !this.deliveryWritesStopping && (guard?.current() ?? true);
|
|
10823
|
+
};
|
|
10824
|
+
if (!current())
|
|
10655
10825
|
return false;
|
|
10656
|
-
if (!
|
|
10826
|
+
if (!tmux)
|
|
10657
10827
|
return false;
|
|
10658
10828
|
const signature = this.submissionSignature(text);
|
|
10659
10829
|
let baseline;
|
|
10660
10830
|
if (guard) {
|
|
10661
10831
|
let pane;
|
|
10662
10832
|
try {
|
|
10663
|
-
pane = await
|
|
10833
|
+
pane = await tmux.capturePane();
|
|
10664
10834
|
}
|
|
10665
10835
|
catch {
|
|
10666
10836
|
return false;
|
|
@@ -10675,7 +10845,7 @@ export class Daemon extends EventEmitter {
|
|
|
10675
10845
|
if (current && !current())
|
|
10676
10846
|
return false;
|
|
10677
10847
|
const pasteGeneration = this.spawnGeneration;
|
|
10678
|
-
const pasted = current ? await
|
|
10848
|
+
const pasted = current ? await tmux.pasteBuffer(text, { guard: current }) : await tmux.pasteBuffer(text);
|
|
10679
10849
|
if (!pasted) {
|
|
10680
10850
|
this.logger.warn({ label }, "System paste failed to reach the pane");
|
|
10681
10851
|
return false;
|
|
@@ -12890,7 +13060,7 @@ export class Daemon extends EventEmitter {
|
|
|
12890
13060
|
// ones included: if the auto-dismiss keeps failing here, the human
|
|
12891
13061
|
// report should count from first sight, not from the runtime scan.
|
|
12892
13062
|
if (!dialog.fatal && this.deliveryBlockingDialogs().some(d => Daemon.dialogKey(d) === Daemon.dialogKey(dialog))) {
|
|
12893
|
-
this.trackDialogParked(dialog);
|
|
13063
|
+
this.trackDialogParked(dialog, pane);
|
|
12894
13064
|
}
|
|
12895
13065
|
// A fatal startup screen cannot be dismissed by keypresses (e.g. a
|
|
12896
13066
|
// corrupt claude.json modal offering only "exit" / "reset config").
|
|
@@ -12920,7 +13090,7 @@ export class Daemon extends EventEmitter {
|
|
|
12920
13090
|
this.heldReasonLoggedKey = Daemon.dialogKey(dialog);
|
|
12921
13091
|
this.logger.warn({ description: dialog.description, ...reason }, "Startup dialog held — not auto-answering, deliveries stay blocked");
|
|
12922
13092
|
}
|
|
12923
|
-
this.trackDialogParked(dialog);
|
|
13093
|
+
this.trackDialogParked(dialog, pane);
|
|
12924
13094
|
matched = true;
|
|
12925
13095
|
break;
|
|
12926
13096
|
}
|