@songsid/agend 2.1.4-beta.17 → 2.1.4-beta.19
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/backend/antigravity.d.ts +1 -0
- package/dist/backend/antigravity.js +4 -0
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/codex.d.ts +17 -0
- package/dist/backend/codex.js +51 -0
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/types.d.ts +10 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.js +54 -20
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.js +5 -3
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/types.d.ts +1 -1
- package/dist/cli.js +31 -21
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +3 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +5 -1
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +58 -2
- package/dist/daemon.js +263 -29
- package/dist/daemon.js.map +1 -1
- package/dist/doctor.js +34 -27
- package/dist/doctor.js.map +1 -1
- package/dist/fleet-context.d.ts +4 -0
- package/dist/fleet-manager.d.ts +25 -0
- package/dist/fleet-manager.js +451 -88
- package/dist/fleet-manager.js.map +1 -1
- package/dist/locale.js +656 -320
- package/dist/locale.js.map +1 -1
- package/dist/outbound-handlers.js +5 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/restart-progress.d.ts +2 -0
- package/dist/restart-progress.js +3 -0
- package/dist/restart-progress.js.map +1 -1
- package/dist/scheduler/db.d.ts +5 -0
- package/dist/scheduler/db.js +35 -0
- package/dist/scheduler/db.js.map +1 -1
- package/dist/tips.d.ts +34 -0
- package/dist/tips.js +554 -0
- package/dist/tips.js.map +1 -0
- package/dist/topic-commands.d.ts +9 -2
- package/dist/topic-commands.js +144 -59
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/ui/dashboard.html +55 -32
- package/dist/ui/settings.html +46 -37
- package/dist/ui/view.html +23 -15
- package/package.json +1 -1
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAC9D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,IAAI,MAAM,SAAS,CAAC;AAC3B,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAGlF,SAAS,gBAAgB,CAAmB,MAAS,EAAE,MAAkB;IACvE,MAAM,MAAM,GAAG,EAAE,GAAG,MAAM,EAA6B,CAAC;IACxD,MAAM,YAAY,GAAG,MAAiC,CAAC;IAEvD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;QAC5C,MAAM,SAAS,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;QACpC,MAAM,SAAS,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QAC9B,IACE,SAAS,KAAK,IAAI;YAClB,OAAO,SAAS,KAAK,QAAQ;YAC7B,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC;YACzB,OAAO,SAAS,KAAK,QAAQ;YAC7B,SAAS,KAAK,IAAI;YAClB,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,EACzB,CAAC;YACD,MAAM,CAAC,GAAG,CAAC,GAAG,gBAAgB,CAC5B,SAAmB,EACnB,SAA4B,CAC7B,CAAC;QACJ,CAAC;aAAM,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YACnC,MAAM,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;QAC1B,CAAC;IACH,CAAC;IACD,OAAO,MAAsB,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,cAAc;IAC5B,OAAO,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,kBAAkB,EAAE,CAAC;AAChE,CAAC;AAED,MAAM,CAAC,MAAM,kBAAkB,GAAoB;IACjD,eAAe,EAAE,CAAC,EAAE,eAAe;IACnC,kBAAkB,EAAE,EAAE;IACtB,QAAQ,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC,eAAe,EAAE,CAAC,QAAQ;CAC3D,CAAC;AAEF,MAAM,CAAC,MAAM,qBAAqB,GAAuB;IACvD,OAAO,EAAE,IAAI;IACb,eAAe,EAAE,EAAE;CACpB,CAAC;AAEF,MAAM,CAAC,MAAM,qBAAqB,GAAuB;IACvD,OAAO,EAAE,IAAI;IACb,IAAI,EAAE,EAAE;IACR,MAAM,EAAE,CAAC;CACV,CAAC;AAEF,MAAM,CAAC,MAAM,uBAAuB,GAA8C;IAChF,gBAAgB,EAAE,CAAC,EAAE,iCAAiC;IACtD,gBAAgB,EAAE,IAAI,EAAE,gDAAgD;IACxE,eAAe,EAAE,KAAK,EAAE,4EAA4E;IACpG,aAAa,EAAE,KAAK,EAAE,oEAAoE;IAE1F,OAAO,EAAE,QAAQ;IACjB,QAAQ,EAAE;QACR,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,GAAG;QACZ,IAAI,EAAE,EAAE;KACT;IACD,cAAc,EAAE;QACd,WAAW,EAAE,EAAE;QACf,OAAO,EAAE,aAAa;QACtB,WAAW,EAAE,GAAG;KACjB;IACD,gBAAgB,EAAE;QAChB,eAAe,EAAE,OAAO;QACxB,aAAa,EAAE,CAAC,EAAE,gEAAgE;KACnF;IACD,SAAS,EAAE,MAAM;CAClB,CAAC;AAEF,kFAAkF;AAClF,MAAM,UAAU,4BAA4B,CAC1C,gBAAyC,EAAE;IAE3C,OAAO,gBAAgB,CACrB,uBAAkD,EAClD,
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/daemon.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export declare function buildInstructionReloadNotice(binaryName: string, instanc
|
|
|
11
11
|
export declare const DEFAULT_STUCK_TIMEOUT_MS: number;
|
|
12
12
|
export declare const DEFAULT_STATE_IDLE_DEBOUNCE_MS = 2000;
|
|
13
13
|
export declare const DEFAULT_STATE_SAFETY_SWEEP_MS = 60000;
|
|
14
|
+
/** Coalesce one cosmetic-redraw burst before comparing the visible pane. */
|
|
15
|
+
export declare const PERIODIC_REDRAW_PROBE_MS = 25;
|
|
14
16
|
/** A foreground server/port-forward should hand control back or be acknowledged. */
|
|
15
17
|
export declare const DEFAULT_BLOCKING_PROCESS_GRACE_MS: number;
|
|
16
18
|
/**
|
|
@@ -348,6 +350,7 @@ export declare class Daemon extends EventEmitter {
|
|
|
348
350
|
/** Fallback safety sweep used only when no shared tmux control client exists. */
|
|
349
351
|
private instanceStateMonitorTimer;
|
|
350
352
|
private instanceStateIdleTimer;
|
|
353
|
+
private instanceStateOutputProbeTimer;
|
|
351
354
|
private instanceStateStuckTimer;
|
|
352
355
|
private instanceStateOutputListener;
|
|
353
356
|
private instanceStateOutputEventName;
|
|
@@ -374,6 +377,20 @@ export declare class Daemon extends EventEmitter {
|
|
|
374
377
|
* would silently no-op exactly when we most need to stop feeding the CLI.
|
|
375
378
|
*/
|
|
376
379
|
private pausePending;
|
|
380
|
+
/**
|
|
381
|
+
* An auth failure was detected and nothing has resolved it yet.
|
|
382
|
+
*
|
|
383
|
+
* Separate from `pausePending` because it must outlive it: the deferred pause
|
|
384
|
+
* clears `pausePending` as soon as it fires, but if that pause is refused the
|
|
385
|
+
* instance is still auth-broken and must not resume raising alarms.
|
|
386
|
+
*/
|
|
387
|
+
private authFailureUnresolved;
|
|
388
|
+
/**
|
|
389
|
+
* Let the next pause() proceed from a stuck pane. Set only for the auth-deferred
|
|
390
|
+
* pause — see the pausePending consumption site for why waiting for idle there
|
|
391
|
+
* is waiting forever.
|
|
392
|
+
*/
|
|
393
|
+
private pauseAllowStuck;
|
|
377
394
|
private pauseWakeState;
|
|
378
395
|
private pauseWakeTransition;
|
|
379
396
|
private modelOverride;
|
|
@@ -385,6 +402,12 @@ export declare class Daemon extends EventEmitter {
|
|
|
385
402
|
* and ⏳/👀/✅ reactions live here). It does NOT cover the other writers that
|
|
386
403
|
* reach the pane — {@link paneWriteLock} does. */
|
|
387
404
|
private pasteLock;
|
|
405
|
+
/**
|
|
406
|
+
* Fleet cancellation epoch. Queue entries retain the epoch they arrived with;
|
|
407
|
+
* advancing it invalidates only work received before the cancel, never a new
|
|
408
|
+
* user message which arrives immediately afterwards.
|
|
409
|
+
*/
|
|
410
|
+
private deliveryEpoch;
|
|
388
411
|
/** Mutual exclusion for *every* write into the pane, whichever subsystem it
|
|
389
412
|
* comes from. See PaneWriteLock for why interleaving is destructive. */
|
|
390
413
|
private readonly paneWriteLock;
|
|
@@ -533,6 +556,16 @@ export declare class Daemon extends EventEmitter {
|
|
|
533
556
|
* that message, which is what the user asked for anyway.
|
|
534
557
|
*/
|
|
535
558
|
sendEscape(): Promise<void>;
|
|
559
|
+
/**
|
|
560
|
+
* Drop every not-yet-started pane delivery which predates a user cancel.
|
|
561
|
+
* Promise chains cannot be removed, so entries become generation-checked
|
|
562
|
+
* no-ops. An entry already past its check is allowed to finish its current PTY
|
|
563
|
+
* transaction; the interrupt key still cancels the CLI generation.
|
|
564
|
+
*/
|
|
565
|
+
clearPendingDeliveries(fleetEpoch?: number): void;
|
|
566
|
+
private captureDeliveryEpoch;
|
|
567
|
+
private isDeliveryEpochCurrent;
|
|
568
|
+
private queueRawPaste;
|
|
536
569
|
/** Send the backend-specific graceful quit command/key sequence. */
|
|
537
570
|
private sendQuitSequence;
|
|
538
571
|
stop(): Promise<void>;
|
|
@@ -576,6 +609,13 @@ export declare class Daemon extends EventEmitter {
|
|
|
576
609
|
/** Relay the pane's final text to the channel, marked as a daemon proxy reply. */
|
|
577
610
|
private sendProxyReply;
|
|
578
611
|
private clearInstanceStateIdleTimer;
|
|
612
|
+
private clearInstanceStateOutputProbeTimer;
|
|
613
|
+
/**
|
|
614
|
+
* Periodic status-line hooks can generate `%output` without changing a single
|
|
615
|
+
* visible cell. Probe the pane once per burst: a changed capture is real work;
|
|
616
|
+
* an identical ready capture remains idle and emits no state edge.
|
|
617
|
+
*/
|
|
618
|
+
private scheduleInstanceStateOutputProbe;
|
|
579
619
|
private clearInstanceStateStuckTimer;
|
|
580
620
|
private scheduleInstanceStateStuckDeadline;
|
|
581
621
|
private scheduleInstanceStateIdleCapture;
|
|
@@ -670,13 +710,20 @@ export declare class Daemon extends EventEmitter {
|
|
|
670
710
|
* The steered text goes through formatInboundMessage so the agent sees a
|
|
671
711
|
* normal inbound message, with a steering notice prepended for context.
|
|
672
712
|
*/
|
|
673
|
-
steerMessage(content: string, meta: Record<string, string
|
|
713
|
+
steerMessage(content: string, meta: Record<string, string>, deliveryEpoch?: number): void;
|
|
714
|
+
/**
|
|
715
|
+
* /btw: submit a separately-labelled side question immediately while asking
|
|
716
|
+
* the agent not to change the active task. Claude's native `/btw` opens a
|
|
717
|
+
* branchAndResume panel whose answer never reaches the channel or transcript,
|
|
718
|
+
* so channel delivery intentionally uses a normal wrapped inbound instead.
|
|
719
|
+
*/
|
|
720
|
+
btwMessage(content: string, meta: Record<string, string>, deliveryEpoch?: number): void;
|
|
674
721
|
/**
|
|
675
722
|
* Push an inbound channel message to a specific MCP session.
|
|
676
723
|
* If targetSession is provided, only send to the matching socket.
|
|
677
724
|
* Otherwise send to the instance's own session (this.name).
|
|
678
725
|
*/
|
|
679
|
-
pushChannelMessage(content: string, meta: Record<string, string>, _targetSession?: string): void;
|
|
726
|
+
pushChannelMessage(content: string, meta: Record<string, string>, _targetSession?: string, deliveryEpoch?: number): void;
|
|
680
727
|
/**
|
|
681
728
|
* Deliver a single message and drive its status reactions:
|
|
682
729
|
* ⏳ message_queued — CLI busy; queued locally or by the backend
|
|
@@ -694,6 +741,15 @@ export declare class Daemon extends EventEmitter {
|
|
|
694
741
|
* serial in both cases so separate PTY writes can never overlap.
|
|
695
742
|
*/
|
|
696
743
|
private deliverMessage;
|
|
744
|
+
/**
|
|
745
|
+
* A raw `%output` silence check is authoritative for ordinary TUIs. It is not
|
|
746
|
+
* for a backend such as agy whose status-line hook periodically redraws an
|
|
747
|
+
* identical screen. In that case trust an idle pane-state snapshot only when
|
|
748
|
+
* it was captured after both the latest output and the latest control-client
|
|
749
|
+
* observation reset.
|
|
750
|
+
*/
|
|
751
|
+
private isPaneIdleForDelivery;
|
|
752
|
+
private waitForPaneIdleForDelivery;
|
|
697
753
|
/**
|
|
698
754
|
* The write half of a delivery: paste → Enter → confirm, with retries.
|
|
699
755
|
*
|
package/dist/daemon.js
CHANGED
|
@@ -47,6 +47,8 @@ export function buildInstructionReloadNotice(binaryName, instanceName, instanceD
|
|
|
47
47
|
export const DEFAULT_STUCK_TIMEOUT_MS = 10 * 60_000;
|
|
48
48
|
export const DEFAULT_STATE_IDLE_DEBOUNCE_MS = 2_000;
|
|
49
49
|
export const DEFAULT_STATE_SAFETY_SWEEP_MS = 60_000;
|
|
50
|
+
/** Coalesce one cosmetic-redraw burst before comparing the visible pane. */
|
|
51
|
+
export const PERIODIC_REDRAW_PROBE_MS = 25;
|
|
50
52
|
/** A foreground server/port-forward should hand control back or be acknowledged. */
|
|
51
53
|
export const DEFAULT_BLOCKING_PROCESS_GRACE_MS = 2 * 60_000;
|
|
52
54
|
const LAST_INBOUND_FILE = "last-inbound-at";
|
|
@@ -732,6 +734,7 @@ export class Daemon extends EventEmitter {
|
|
|
732
734
|
/** Fallback safety sweep used only when no shared tmux control client exists. */
|
|
733
735
|
instanceStateMonitorTimer = null;
|
|
734
736
|
instanceStateIdleTimer = null;
|
|
737
|
+
instanceStateOutputProbeTimer = null;
|
|
735
738
|
instanceStateStuckTimer = null;
|
|
736
739
|
instanceStateOutputListener = null;
|
|
737
740
|
instanceStateOutputEventName = null;
|
|
@@ -762,6 +765,20 @@ export class Daemon extends EventEmitter {
|
|
|
762
765
|
* would silently no-op exactly when we most need to stop feeding the CLI.
|
|
763
766
|
*/
|
|
764
767
|
pausePending = false;
|
|
768
|
+
/**
|
|
769
|
+
* An auth failure was detected and nothing has resolved it yet.
|
|
770
|
+
*
|
|
771
|
+
* Separate from `pausePending` because it must outlive it: the deferred pause
|
|
772
|
+
* clears `pausePending` as soon as it fires, but if that pause is refused the
|
|
773
|
+
* instance is still auth-broken and must not resume raising alarms.
|
|
774
|
+
*/
|
|
775
|
+
authFailureUnresolved = false;
|
|
776
|
+
/**
|
|
777
|
+
* Let the next pause() proceed from a stuck pane. Set only for the auth-deferred
|
|
778
|
+
* pause — see the pausePending consumption site for why waiting for idle there
|
|
779
|
+
* is waiting forever.
|
|
780
|
+
*/
|
|
781
|
+
pauseAllowStuck = false;
|
|
765
782
|
pauseWakeState = "active";
|
|
766
783
|
pauseWakeTransition = null;
|
|
767
784
|
// Model failover: override model on next spawn when rate-limited
|
|
@@ -775,6 +792,12 @@ export class Daemon extends EventEmitter {
|
|
|
775
792
|
* and ⏳/👀/✅ reactions live here). It does NOT cover the other writers that
|
|
776
793
|
* reach the pane — {@link paneWriteLock} does. */
|
|
777
794
|
pasteLock = Promise.resolve();
|
|
795
|
+
/**
|
|
796
|
+
* Fleet cancellation epoch. Queue entries retain the epoch they arrived with;
|
|
797
|
+
* advancing it invalidates only work received before the cancel, never a new
|
|
798
|
+
* user message which arrives immediately afterwards.
|
|
799
|
+
*/
|
|
800
|
+
deliveryEpoch = 0;
|
|
778
801
|
/** Mutual exclusion for *every* write into the pane, whichever subsystem it
|
|
779
802
|
* comes from. See PaneWriteLock for why interleaving is destructive. */
|
|
780
803
|
paneWriteLock = new PaneWriteLock();
|
|
@@ -1000,36 +1023,33 @@ export class Daemon extends EventEmitter {
|
|
|
1000
1023
|
// Fleet manager routed a message to us (topic mode)
|
|
1001
1024
|
const meta = msg.meta;
|
|
1002
1025
|
const targetSession = msg.targetSession;
|
|
1026
|
+
const deliveryEpoch = this.captureDeliveryEpoch(msg.delivery_epoch);
|
|
1003
1027
|
void this.wake().then(() => {
|
|
1004
|
-
this.pushChannelMessage(msg.content, meta, targetSession);
|
|
1028
|
+
this.pushChannelMessage(msg.content, meta, targetSession, deliveryEpoch);
|
|
1005
1029
|
}).catch(err => {
|
|
1006
1030
|
this.logger.error({ err: err.message }, "Wake failed for inbound delivery");
|
|
1007
1031
|
});
|
|
1008
1032
|
}
|
|
1009
1033
|
else if (msg.type === "raw_paste") {
|
|
1010
1034
|
// Paste raw text directly to CLI without [user:] wrapping.
|
|
1011
|
-
|
|
1012
|
-
const rawText = msg.content;
|
|
1013
|
-
this.pasteLock = this.pasteLock.then(async () => {
|
|
1014
|
-
await this.wake();
|
|
1015
|
-
await this.deliverMessage(rawText);
|
|
1016
|
-
this.logger.debug({ text: rawText.slice(0, 100) }, "Raw paste delivered");
|
|
1017
|
-
}).catch(err => {
|
|
1018
|
-
this.logger.warn({ err: err.message }, "raw_paste delivery error");
|
|
1019
|
-
});
|
|
1020
|
-
}
|
|
1035
|
+
this.queueRawPaste(msg.content, this.captureDeliveryEpoch(msg.delivery_epoch));
|
|
1021
1036
|
}
|
|
1022
1037
|
else if (msg.type === "config_update") {
|
|
1023
1038
|
this.applyConfigUpdate(msg.config);
|
|
1024
1039
|
}
|
|
1025
1040
|
else if (msg.type === "steer") {
|
|
1026
1041
|
const meta = (msg.meta ?? {});
|
|
1027
|
-
this.steerMessage(msg.content, meta);
|
|
1042
|
+
this.steerMessage(msg.content, meta, this.captureDeliveryEpoch(msg.delivery_epoch));
|
|
1043
|
+
}
|
|
1044
|
+
else if (msg.type === "btw") {
|
|
1045
|
+
const meta = (msg.meta ?? {});
|
|
1046
|
+
this.btwMessage(msg.content, meta, this.captureDeliveryEpoch(msg.delivery_epoch));
|
|
1028
1047
|
}
|
|
1029
1048
|
else if (msg.type === "fleet_schedule_trigger") {
|
|
1030
1049
|
const payload = msg.payload;
|
|
1031
1050
|
const meta = msg.meta;
|
|
1032
|
-
|
|
1051
|
+
const deliveryEpoch = this.captureDeliveryEpoch(msg.delivery_epoch);
|
|
1052
|
+
void this.wake().then(() => this.pushChannelMessage(payload.message, meta, undefined, deliveryEpoch)).catch(err => {
|
|
1033
1053
|
this.logger.error({ err: err.message }, "Wake failed for scheduled delivery");
|
|
1034
1054
|
});
|
|
1035
1055
|
}
|
|
@@ -1767,6 +1787,7 @@ export class Daemon extends EventEmitter {
|
|
|
1767
1787
|
if (seen > 0)
|
|
1768
1788
|
this.lastErrorCount.set(Daemon.errorPatternKey(ep), seen);
|
|
1769
1789
|
}
|
|
1790
|
+
this.authFailureUnresolved = false;
|
|
1770
1791
|
this.clearErrorRecoveryGate();
|
|
1771
1792
|
this.logger.info({ downtime_s: downtime }, "PTY error recovered — agent is ready again");
|
|
1772
1793
|
this.emit("pty_recovered", { name: this.name, downtime_s: downtime });
|
|
@@ -1828,6 +1849,11 @@ export class Daemon extends EventEmitter {
|
|
|
1828
1849
|
if (ep.action === "failover")
|
|
1829
1850
|
this.lastFailoverAt = now;
|
|
1830
1851
|
const message = this.resolveErrorMessage(pane, ep);
|
|
1852
|
+
// Remember an auth failure for as long as it is unresolved. An auth-broken
|
|
1853
|
+
// CLI cannot finish the turn it is holding, so every other detector in this
|
|
1854
|
+
// scan would otherwise keep reporting the same dead instance forever.
|
|
1855
|
+
if (ep.type === "auth_error")
|
|
1856
|
+
this.authFailureUnresolved = true;
|
|
1831
1857
|
this.logger.warn({ errorType: ep.type, action: ep.action }, `PTY error detected: ${message}`);
|
|
1832
1858
|
this.emit("pty_error", { name: this.name, ...ep, message });
|
|
1833
1859
|
break; // Only handle first unsuppressed new error per scan
|
|
@@ -1871,6 +1897,46 @@ export class Daemon extends EventEmitter {
|
|
|
1871
1897
|
const cancelKey = this.backend?.getCancelKey() ?? "Escape";
|
|
1872
1898
|
await this.tmux?.sendSpecialKey(cancelKey);
|
|
1873
1899
|
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Drop every not-yet-started pane delivery which predates a user cancel.
|
|
1902
|
+
* Promise chains cannot be removed, so entries become generation-checked
|
|
1903
|
+
* no-ops. An entry already past its check is allowed to finish its current PTY
|
|
1904
|
+
* transaction; the interrupt key still cancels the CLI generation.
|
|
1905
|
+
*/
|
|
1906
|
+
clearPendingDeliveries(fleetEpoch) {
|
|
1907
|
+
this.deliveryEpoch = fleetEpoch === undefined
|
|
1908
|
+
? this.deliveryEpoch + 1
|
|
1909
|
+
: Math.max(this.deliveryEpoch, fleetEpoch);
|
|
1910
|
+
this.logger.info({ deliveryEpoch: this.deliveryEpoch }, "Pending delivery queue cleared by user cancel");
|
|
1911
|
+
}
|
|
1912
|
+
captureDeliveryEpoch(value) {
|
|
1913
|
+
const epoch = typeof value === "number" && Number.isSafeInteger(value) && value >= 0
|
|
1914
|
+
? value
|
|
1915
|
+
: this.deliveryEpoch;
|
|
1916
|
+
if (epoch > this.deliveryEpoch)
|
|
1917
|
+
this.deliveryEpoch = epoch;
|
|
1918
|
+
return epoch;
|
|
1919
|
+
}
|
|
1920
|
+
isDeliveryEpochCurrent(epoch) {
|
|
1921
|
+
return epoch === this.deliveryEpoch;
|
|
1922
|
+
}
|
|
1923
|
+
queueRawPaste(rawText, deliveryEpoch = this.deliveryEpoch) {
|
|
1924
|
+
if (!this.tmux || !this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
1925
|
+
return;
|
|
1926
|
+
this.pasteLock = this.pasteLock.then(async () => {
|
|
1927
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
1928
|
+
this.logger.info("Pending raw delivery dropped by user cancel");
|
|
1929
|
+
return;
|
|
1930
|
+
}
|
|
1931
|
+
await this.wake();
|
|
1932
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
1933
|
+
return;
|
|
1934
|
+
await this.deliverMessage(rawText, undefined, { deliveryEpoch });
|
|
1935
|
+
this.logger.debug({ text: rawText.slice(0, 100) }, "Raw paste delivered");
|
|
1936
|
+
}).catch(err => {
|
|
1937
|
+
this.logger.warn({ err: err.message }, "raw_paste delivery error");
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1874
1940
|
/** Send the backend-specific graceful quit command/key sequence. */
|
|
1875
1941
|
async sendQuitSequence() {
|
|
1876
1942
|
if (!this.tmux || !this.backend)
|
|
@@ -2018,7 +2084,14 @@ export class Daemon extends EventEmitter {
|
|
|
2018
2084
|
if (this.getPauseWakeState() !== "active")
|
|
2019
2085
|
return;
|
|
2020
2086
|
}
|
|
2021
|
-
|
|
2087
|
+
// `pauseAllowStuck` is consumed here, not merely read: it authorises exactly
|
|
2088
|
+
// one pause from a stuck pane (the auth-deferred one) and must never leak
|
|
2089
|
+
// into an ordinary idle-timeout pause of a busy instance.
|
|
2090
|
+
const allowStuck = this.pauseAllowStuck;
|
|
2091
|
+
this.pauseAllowStuck = false;
|
|
2092
|
+
const pausableState = this.instanceState === "idle"
|
|
2093
|
+
|| (allowStuck && this.instanceState === "stuck");
|
|
2094
|
+
if (!pausableState || this.pasteQueueDepth > 0) {
|
|
2022
2095
|
this.pauseRequested = false;
|
|
2023
2096
|
return;
|
|
2024
2097
|
}
|
|
@@ -2082,6 +2155,7 @@ export class Daemon extends EventEmitter {
|
|
|
2082
2155
|
// An explicit wake (e.g. after the user re-logs in) cancels a deferred
|
|
2083
2156
|
// auth pause — otherwise the instance would pause again the moment it idles.
|
|
2084
2157
|
this.pausePending = false;
|
|
2158
|
+
this.authFailureUnresolved = false;
|
|
2085
2159
|
if (this.pauseWakeState === "active")
|
|
2086
2160
|
return;
|
|
2087
2161
|
if (this.pauseWakeState === "pausing")
|
|
@@ -2178,8 +2252,13 @@ export class Daemon extends EventEmitter {
|
|
|
2178
2252
|
if (snapshot.state !== "idle")
|
|
2179
2253
|
this.pauseRequested = false;
|
|
2180
2254
|
// A pause deferred by an auth failure: retry the moment the pane settles.
|
|
2181
|
-
|
|
2255
|
+
// "stuck" counts as settled here, and that is the whole point — an expired
|
|
2256
|
+
// session cannot complete the turn it is holding, so the pane never returns
|
|
2257
|
+
// to idle and waiting for idle meant waiting forever. The instance stayed
|
|
2258
|
+
// live, kept its warm slot, and re-raised a hang alert on every scan.
|
|
2259
|
+
if (this.pausePending && (snapshot.state === "idle" || snapshot.state === "stuck") && this.pasteQueueDepth === 0) {
|
|
2182
2260
|
this.pausePending = false;
|
|
2261
|
+
this.pauseAllowStuck = snapshot.state === "stuck";
|
|
2183
2262
|
this.emit("auto_pause_requested", { name: this.name, idleSince: snapshot.stateChangedAt });
|
|
2184
2263
|
return;
|
|
2185
2264
|
}
|
|
@@ -2306,6 +2385,25 @@ export class Daemon extends EventEmitter {
|
|
|
2306
2385
|
clearTimeout(this.instanceStateIdleTimer);
|
|
2307
2386
|
this.instanceStateIdleTimer = null;
|
|
2308
2387
|
}
|
|
2388
|
+
clearInstanceStateOutputProbeTimer() {
|
|
2389
|
+
if (this.instanceStateOutputProbeTimer)
|
|
2390
|
+
clearTimeout(this.instanceStateOutputProbeTimer);
|
|
2391
|
+
this.instanceStateOutputProbeTimer = null;
|
|
2392
|
+
}
|
|
2393
|
+
/**
|
|
2394
|
+
* Periodic status-line hooks can generate `%output` without changing a single
|
|
2395
|
+
* visible cell. Probe the pane once per burst: a changed capture is real work;
|
|
2396
|
+
* an identical ready capture remains idle and emits no state edge.
|
|
2397
|
+
*/
|
|
2398
|
+
scheduleInstanceStateOutputProbe() {
|
|
2399
|
+
if (this.instanceStateOutputProbeTimer)
|
|
2400
|
+
return;
|
|
2401
|
+
this.instanceStateOutputProbeTimer = setTimeout(() => {
|
|
2402
|
+
this.instanceStateOutputProbeTimer = null;
|
|
2403
|
+
void this.captureAndEvaluateInstanceState("output_probe", this.instanceStateLastOutputAt);
|
|
2404
|
+
}, PERIODIC_REDRAW_PROBE_MS);
|
|
2405
|
+
this.instanceStateOutputProbeTimer.unref?.();
|
|
2406
|
+
}
|
|
2309
2407
|
clearInstanceStateStuckTimer() {
|
|
2310
2408
|
if (this.instanceStateStuckTimer)
|
|
2311
2409
|
clearTimeout(this.instanceStateStuckTimer);
|
|
@@ -2350,6 +2448,8 @@ export class Daemon extends EventEmitter {
|
|
|
2350
2448
|
if (this.statePollInFlight) {
|
|
2351
2449
|
if (reason === "idle_debounce")
|
|
2352
2450
|
this.scheduleInstanceStateIdleCapture();
|
|
2451
|
+
if (reason === "output_probe")
|
|
2452
|
+
this.scheduleInstanceStateOutputProbe();
|
|
2353
2453
|
return;
|
|
2354
2454
|
}
|
|
2355
2455
|
this.statePollInFlight = true;
|
|
@@ -2358,9 +2458,19 @@ export class Daemon extends EventEmitter {
|
|
|
2358
2458
|
const pane = await this.tmux.capturePane();
|
|
2359
2459
|
// Output received while capture-pane was in flight makes this snapshot
|
|
2360
2460
|
// stale. Its output handler has already armed a new debounce.
|
|
2361
|
-
|
|
2362
|
-
|| (this.instanceStateLastOutputAt > 0 && this.instanceStateLastOutputAt >= captureStartedAt)
|
|
2461
|
+
const outputMovedDuringCapture = (expectedOutputAt > 0 && this.instanceStateLastOutputAt > expectedOutputAt)
|
|
2462
|
+
|| (this.instanceStateLastOutputAt > 0 && this.instanceStateLastOutputAt >= captureStartedAt);
|
|
2463
|
+
if (outputMovedDuringCapture) {
|
|
2464
|
+
if (reason === "output_probe") {
|
|
2465
|
+
// A second burst overtook the probe. Do not bless its stale capture as
|
|
2466
|
+
// idle: continuous output is the safer working signal, while the next
|
|
2467
|
+
// quiet probe/debounce can still prove that it was only repainting.
|
|
2468
|
+
this.applyInstanceStateSnapshot(this.instanceStateMachine.recordOutput(this.instanceStateLastOutputAt));
|
|
2469
|
+
this.scheduleInstanceStateIdleCapture();
|
|
2470
|
+
this.scheduleInstanceStateStuckDeadline(this.instanceStateLastOutputAt);
|
|
2471
|
+
}
|
|
2363
2472
|
return;
|
|
2473
|
+
}
|
|
2364
2474
|
const observedChangeAt = expectedOutputAt || captureStartedAt;
|
|
2365
2475
|
// One observation per capture. This used to call observe() twice with the
|
|
2366
2476
|
// same pane: the second call always saw an unchanged pane, so the
|
|
@@ -2412,6 +2522,14 @@ export class Daemon extends EventEmitter {
|
|
|
2412
2522
|
if (!windowId || event.windowId !== windowId || !this.instanceStateMachine)
|
|
2413
2523
|
return;
|
|
2414
2524
|
this.instanceStateLastOutputAt = event.at;
|
|
2525
|
+
if (this.backend?.hasPeriodicPaneRedraw?.() === true && this.instanceState === "idle") {
|
|
2526
|
+
// Do not emit idle→working solely because agy repainted an identical
|
|
2527
|
+
// footer. The short capture probe below still observes genuine pane
|
|
2528
|
+
// changes promptly and moves the state to working.
|
|
2529
|
+
this.scheduleInstanceStateOutputProbe();
|
|
2530
|
+
this.scheduleInstanceStateIdleCapture();
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2415
2533
|
const snapshot = this.instanceStateMachine.recordOutput(event.at);
|
|
2416
2534
|
this.applyInstanceStateSnapshot(snapshot);
|
|
2417
2535
|
this.scheduleInstanceStateIdleCapture();
|
|
@@ -2462,6 +2580,7 @@ export class Daemon extends EventEmitter {
|
|
|
2462
2580
|
stopInstanceStateMonitor() {
|
|
2463
2581
|
this.instanceStateMonitorActive = false;
|
|
2464
2582
|
this.clearInstanceStateIdleTimer();
|
|
2583
|
+
this.clearInstanceStateOutputProbeTimer();
|
|
2465
2584
|
this.clearInstanceStateStuckTimer();
|
|
2466
2585
|
if (this.instanceStateMonitorTimer)
|
|
2467
2586
|
clearInterval(this.instanceStateMonitorTimer);
|
|
@@ -2496,6 +2615,15 @@ export class Daemon extends EventEmitter {
|
|
|
2496
2615
|
this.logger.debug(diagnostic, "Suppressing stuck notification without pending work");
|
|
2497
2616
|
return;
|
|
2498
2617
|
}
|
|
2618
|
+
// An auth-broken instance IS stuck, and stays stuck: it cannot finish the
|
|
2619
|
+
// turn it is holding, so pendingWork never clears and this fired on every
|
|
2620
|
+
// scan forever. The notification is also useless — it offers Restart/Wait
|
|
2621
|
+
// and asks General for help, but only the user re-logging in fixes this, and
|
|
2622
|
+
// they have already been told exactly that by the auth alert.
|
|
2623
|
+
if (this.authFailureUnresolved) {
|
|
2624
|
+
this.logger.debug(diagnostic, "Suppressing stuck notification — unresolved auth failure (re-login required)");
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2499
2627
|
this.logger.warn(diagnostic, "Instance pane stuck with pending work");
|
|
2500
2628
|
this.hangDetector?.emit("hang", { unchangedForMs: snapshot.unchangedForMs });
|
|
2501
2629
|
}
|
|
@@ -2785,7 +2913,9 @@ export class Daemon extends EventEmitter {
|
|
|
2785
2913
|
* The steered text goes through formatInboundMessage so the agent sees a
|
|
2786
2914
|
* normal inbound message, with a steering notice prepended for context.
|
|
2787
2915
|
*/
|
|
2788
|
-
steerMessage(content, meta) {
|
|
2916
|
+
steerMessage(content, meta, deliveryEpoch = this.deliveryEpoch) {
|
|
2917
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
2918
|
+
return;
|
|
2789
2919
|
this.updateLastChat(meta.chat_id, meta.thread_id, meta.adapter_id);
|
|
2790
2920
|
this.pendingWork.recordInbound();
|
|
2791
2921
|
this.recordRecentUserMessage(content, meta);
|
|
@@ -2797,20 +2927,60 @@ export class Daemon extends EventEmitter {
|
|
|
2797
2927
|
? { chatId: meta.thread_id || chatId, messageId }
|
|
2798
2928
|
: undefined;
|
|
2799
2929
|
this.steerLock = this.steerLock.then(async () => {
|
|
2930
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
2931
|
+
return;
|
|
2800
2932
|
await this.wake();
|
|
2801
|
-
if (
|
|
2933
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
2934
|
+
return;
|
|
2935
|
+
if (await this.deliverMessage(formatted, status, { steer: true, deliveryEpoch })) {
|
|
2802
2936
|
this.markTurnStarted(meta, formatted);
|
|
2803
2937
|
}
|
|
2804
2938
|
}).catch(err => {
|
|
2805
2939
|
this.logger.warn({ err: err.message }, "steer delivery error");
|
|
2806
2940
|
});
|
|
2807
2941
|
}
|
|
2942
|
+
/**
|
|
2943
|
+
* /btw: submit a separately-labelled side question immediately while asking
|
|
2944
|
+
* the agent not to change the active task. Claude's native `/btw` opens a
|
|
2945
|
+
* branchAndResume panel whose answer never reaches the channel or transcript,
|
|
2946
|
+
* so channel delivery intentionally uses a normal wrapped inbound instead.
|
|
2947
|
+
*/
|
|
2948
|
+
btwMessage(content, meta, deliveryEpoch = this.deliveryEpoch) {
|
|
2949
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
2950
|
+
return;
|
|
2951
|
+
this.updateLastChat(meta.chat_id, meta.thread_id, meta.adapter_id);
|
|
2952
|
+
this.pendingWork.recordInbound();
|
|
2953
|
+
this.recordRecentUserMessage(content, meta);
|
|
2954
|
+
const formatted = "[BTW — side question from the user. Answer it separately without changing or interrupting the CURRENT work.]\n"
|
|
2955
|
+
+ this.formatInboundMessage(content, meta);
|
|
2956
|
+
const chatId = meta.chat_id;
|
|
2957
|
+
const messageId = meta.message_id;
|
|
2958
|
+
const status = (chatId && messageId)
|
|
2959
|
+
? { chatId: meta.thread_id || chatId, messageId }
|
|
2960
|
+
: undefined;
|
|
2961
|
+
this.steerLock = this.steerLock.then(async () => {
|
|
2962
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
2963
|
+
return;
|
|
2964
|
+
await this.wake();
|
|
2965
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
2966
|
+
return;
|
|
2967
|
+
if (await this.deliverMessage(formatted, status, { steer: true, deliveryEpoch })) {
|
|
2968
|
+
this.markTurnStarted(meta, formatted);
|
|
2969
|
+
}
|
|
2970
|
+
}).catch(err => {
|
|
2971
|
+
this.logger.warn({ err: err.message }, "btw delivery error");
|
|
2972
|
+
});
|
|
2973
|
+
}
|
|
2808
2974
|
/**
|
|
2809
2975
|
* Push an inbound channel message to a specific MCP session.
|
|
2810
2976
|
* If targetSession is provided, only send to the matching socket.
|
|
2811
2977
|
* Otherwise send to the instance's own session (this.name).
|
|
2812
2978
|
*/
|
|
2813
|
-
pushChannelMessage(content, meta, _targetSession) {
|
|
2979
|
+
pushChannelMessage(content, meta, _targetSession, deliveryEpoch = this.deliveryEpoch) {
|
|
2980
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
2981
|
+
this.logger.info("Pending channel delivery dropped by user cancel");
|
|
2982
|
+
return;
|
|
2983
|
+
}
|
|
2814
2984
|
if (!this.tmux) {
|
|
2815
2985
|
this.logger.warn("Cannot push channel message: tmux not running");
|
|
2816
2986
|
return;
|
|
@@ -2844,7 +3014,9 @@ export class Daemon extends EventEmitter {
|
|
|
2844
3014
|
const rawText = content.slice(5);
|
|
2845
3015
|
this.logger.info({ user }, "Raw paste from topic mode user");
|
|
2846
3016
|
this.pasteLock = this.pasteLock.then(async () => {
|
|
2847
|
-
if (
|
|
3017
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
3018
|
+
return;
|
|
3019
|
+
if (await this.deliverMessage(rawText, undefined, { deliveryEpoch }))
|
|
2848
3020
|
this.markTurnStarted(meta, rawText);
|
|
2849
3021
|
}).catch(err => {
|
|
2850
3022
|
this.logger.warn({ err: err.message }, "pasteLock raw delivery error");
|
|
@@ -2854,8 +3026,7 @@ export class Daemon extends EventEmitter {
|
|
|
2854
3026
|
const formatted = this.formatInboundMessage(content, meta);
|
|
2855
3027
|
// Serialize deliveries: each message waits for the previous to complete,
|
|
2856
3028
|
// and each waits for the CLI to be idle before pasting. Messages are never
|
|
2857
|
-
// dropped for age —
|
|
2858
|
-
// (the user can press Cancel to interrupt and let the queue drain sooner).
|
|
3029
|
+
// dropped for age — only an explicit user Cancel invalidates queued work.
|
|
2859
3030
|
const chatId = meta.chat_id;
|
|
2860
3031
|
const messageId = meta.message_id;
|
|
2861
3032
|
const wasQueued = this.pasteQueueDepth > 0;
|
|
@@ -2868,12 +3039,20 @@ export class Daemon extends EventEmitter {
|
|
|
2868
3039
|
}
|
|
2869
3040
|
this.pasteLock = this.pasteLock.then(async () => {
|
|
2870
3041
|
try {
|
|
3042
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
3043
|
+
this.logger.info("Pending channel delivery dropped by user cancel");
|
|
3044
|
+
return;
|
|
3045
|
+
}
|
|
2871
3046
|
if (this.config.pre_task_command) {
|
|
2872
|
-
await this.deliverMessage(this.config.pre_task_command);
|
|
3047
|
+
await this.deliverMessage(this.config.pre_task_command, undefined, { deliveryEpoch });
|
|
3048
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
3049
|
+
return;
|
|
2873
3050
|
}
|
|
2874
3051
|
if (this.pendingInstructionsNotice) {
|
|
2875
3052
|
this.pendingInstructionsNotice = false;
|
|
2876
|
-
await this.deliverMessage(buildInstructionReloadNotice(this.backend?.binaryName ?? "unknown", this.name, this.instanceDir));
|
|
3053
|
+
await this.deliverMessage(buildInstructionReloadNotice(this.backend?.binaryName ?? "unknown", this.name, this.instanceDir), undefined, { deliveryEpoch });
|
|
3054
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
3055
|
+
return;
|
|
2877
3056
|
}
|
|
2878
3057
|
const status = (chatId && messageId)
|
|
2879
3058
|
? { chatId: meta.thread_id || chatId, messageId }
|
|
@@ -2881,7 +3060,7 @@ export class Daemon extends EventEmitter {
|
|
|
2881
3060
|
// A fresh delivery begins a fresh turn — its bubble must not inherit
|
|
2882
3061
|
// the previous turn's tool list.
|
|
2883
3062
|
this.resetToolProgress();
|
|
2884
|
-
if (await this.deliverMessage(formatted, status))
|
|
3063
|
+
if (await this.deliverMessage(formatted, status, { deliveryEpoch }))
|
|
2885
3064
|
this.markTurnStarted(meta, formatted);
|
|
2886
3065
|
}
|
|
2887
3066
|
finally {
|
|
@@ -2909,6 +3088,10 @@ export class Daemon extends EventEmitter {
|
|
|
2909
3088
|
* serial in both cases so separate PTY writes can never overlap.
|
|
2910
3089
|
*/
|
|
2911
3090
|
async deliverMessage(formatted, status, opts) {
|
|
3091
|
+
const cancelled = () => opts?.deliveryEpoch !== undefined
|
|
3092
|
+
&& !this.isDeliveryEpochCurrent(opts.deliveryEpoch);
|
|
3093
|
+
if (cancelled())
|
|
3094
|
+
return false;
|
|
2912
3095
|
// Sanitize unclosed code fences — they cause CLI to wait for closure on Enter
|
|
2913
3096
|
const fenceCount = (formatted.match(/```/g) || []).length;
|
|
2914
3097
|
if (fenceCount % 2 !== 0) {
|
|
@@ -2917,13 +3100,15 @@ export class Daemon extends EventEmitter {
|
|
|
2917
3100
|
}
|
|
2918
3101
|
// Before anything reads the window id: a spawn in progress is about to change it.
|
|
2919
3102
|
await this.waitForSpawnToSettle();
|
|
3103
|
+
if (cancelled())
|
|
3104
|
+
return false;
|
|
2920
3105
|
let windowId = this.getWindowId();
|
|
2921
3106
|
const supportsQueuedInput = this.backend?.supportsQueuedInput?.() === true;
|
|
2922
3107
|
let handingOffToNativeQueue = false;
|
|
2923
3108
|
// If the CLI is busy, either hand the complete submission to its native input
|
|
2924
3109
|
// queue or wait for idle. Native queue support is an explicit backend capability:
|
|
2925
3110
|
// normal Enter input has steering/interrupt semantics in several other CLIs.
|
|
2926
|
-
if (windowId && this.controlClient && !this.
|
|
3111
|
+
if (windowId && this.controlClient && !this.isPaneIdleForDelivery(windowId)) {
|
|
2927
3112
|
if (status)
|
|
2928
3113
|
this.emit("message_queued", status);
|
|
2929
3114
|
if (supportsQueuedInput || opts?.steer) {
|
|
@@ -2937,7 +3122,9 @@ export class Daemon extends EventEmitter {
|
|
|
2937
3122
|
}
|
|
2938
3123
|
else {
|
|
2939
3124
|
this.logger.debug("CLI busy — queuing message until idle");
|
|
2940
|
-
const becameIdle = await this.
|
|
3125
|
+
const becameIdle = await this.waitForPaneIdleForDelivery(windowId);
|
|
3126
|
+
if (cancelled())
|
|
3127
|
+
return false;
|
|
2941
3128
|
if (!becameIdle) {
|
|
2942
3129
|
// The pane never freed up. Report the failure instead of pasting into a
|
|
2943
3130
|
// wedged CLI (where the text would sit unsubmitted and the next message
|
|
@@ -2953,7 +3140,54 @@ export class Daemon extends EventEmitter {
|
|
|
2953
3140
|
// held under the pane lock — holding it across the idle wait (up to 30 min)
|
|
2954
3141
|
// would starve the runtime-dialog dismisser, which is often the very thing
|
|
2955
3142
|
// that would let the pane go idle again.
|
|
2956
|
-
return this.paneWriteLock.run(() =>
|
|
3143
|
+
return this.paneWriteLock.run(async () => {
|
|
3144
|
+
if (cancelled())
|
|
3145
|
+
return false;
|
|
3146
|
+
return this.writeMessageToPane(formatted, windowId, handingOffToNativeQueue, status);
|
|
3147
|
+
});
|
|
3148
|
+
}
|
|
3149
|
+
/**
|
|
3150
|
+
* A raw `%output` silence check is authoritative for ordinary TUIs. It is not
|
|
3151
|
+
* for a backend such as agy whose status-line hook periodically redraws an
|
|
3152
|
+
* identical screen. In that case trust an idle pane-state snapshot only when
|
|
3153
|
+
* it was captured after both the latest output and the latest control-client
|
|
3154
|
+
* observation reset.
|
|
3155
|
+
*/
|
|
3156
|
+
isPaneIdleForDelivery(windowId) {
|
|
3157
|
+
if (!this.controlClient)
|
|
3158
|
+
return true;
|
|
3159
|
+
if (this.controlClient.isIdle(windowId))
|
|
3160
|
+
return true;
|
|
3161
|
+
if (this.backend?.hasPeriodicPaneRedraw?.() !== true || this.instanceState !== "idle")
|
|
3162
|
+
return false;
|
|
3163
|
+
const lastOutputAt = this.controlClient.getLastOutputAt(windowId);
|
|
3164
|
+
if (lastOutputAt === undefined || !this.instanceStateMachine)
|
|
3165
|
+
return false;
|
|
3166
|
+
const observedAt = this.instanceStateMachine.snapshot().observedAt;
|
|
3167
|
+
return observedAt >= lastOutputAt
|
|
3168
|
+
&& observedAt >= this.controlClient.getObservationResetAt();
|
|
3169
|
+
}
|
|
3170
|
+
waitForPaneIdleForDelivery(windowId, timeoutMs = 30 * 60_000) {
|
|
3171
|
+
if (!this.controlClient)
|
|
3172
|
+
return Promise.resolve(true);
|
|
3173
|
+
if (this.backend?.hasPeriodicPaneRedraw?.() !== true) {
|
|
3174
|
+
return this.controlClient.waitUntilIdle(windowId, timeoutMs);
|
|
3175
|
+
}
|
|
3176
|
+
if (this.isPaneIdleForDelivery(windowId))
|
|
3177
|
+
return Promise.resolve(true);
|
|
3178
|
+
return new Promise(resolve => {
|
|
3179
|
+
const check = setInterval(() => {
|
|
3180
|
+
if (!this.isPaneIdleForDelivery(windowId))
|
|
3181
|
+
return;
|
|
3182
|
+
clearInterval(check);
|
|
3183
|
+
clearTimeout(timer);
|
|
3184
|
+
resolve(true);
|
|
3185
|
+
}, 50);
|
|
3186
|
+
const timer = setTimeout(() => {
|
|
3187
|
+
clearInterval(check);
|
|
3188
|
+
resolve(false);
|
|
3189
|
+
}, timeoutMs);
|
|
3190
|
+
});
|
|
2957
3191
|
}
|
|
2958
3192
|
/**
|
|
2959
3193
|
* The write half of a delivery: paste → Enter → confirm, with retries.
|