@songsid/agend 2.3.4-beta.1 → 2.3.4-beta.10
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/THIRD_PARTY_NOTICES.md +37 -0
- package/dist/agend-home.d.ts +13 -0
- package/dist/agend-home.js +42 -0
- package/dist/agend-home.js.map +1 -1
- package/dist/agent-cli-instructions.md +1 -1
- package/dist/agent-credentials.d.ts +24 -0
- package/dist/agent-credentials.js +66 -0
- package/dist/agent-credentials.js.map +1 -0
- package/dist/agent-endpoint.d.ts +3 -1
- package/dist/agent-endpoint.js +13 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/auth-api.js +10 -2
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/session-authorizations.d.ts +28 -4
- package/dist/authority/session-authorizations.js +63 -17
- package/dist/authority/session-authorizations.js.map +1 -1
- package/dist/backend/antigravity.js +3 -1
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +3 -0
- package/dist/backend/claude-code.js +89 -4
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-footer-hints.d.ts +4 -0
- package/dist/backend/codex-footer-hints.js +27 -0
- package/dist/backend/codex-footer-hints.js.map +1 -0
- 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 +124 -23
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro-identity.d.ts +2 -0
- package/dist/backend/kiro-identity.js +13 -6
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro.d.ts +3 -0
- package/dist/backend/kiro.js +68 -5
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.js +5 -3
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +6 -1
- package/dist/backend/types.js +3 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-availability.d.ts +6 -0
- package/dist/backend-availability.js +13 -5
- package/dist/backend-availability.js.map +1 -1
- package/dist/cache-ledger.d.ts +85 -0
- package/dist/cache-ledger.js +236 -1
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-service.d.ts +5 -0
- package/dist/cache-service.js +16 -4
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.js +27 -4
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +51 -0
- package/dist/channel/adapters/telegram.js +238 -18
- 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 +3 -3
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +2 -0
- package/dist/chat-log-ids.d.ts +48 -0
- package/dist/chat-log-ids.js +182 -0
- package/dist/chat-log-ids.js.map +1 -0
- package/dist/classic-channel-manager.d.ts +8 -1
- package/dist/classic-channel-manager.js +24 -2
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +4 -6
- package/dist/cli.js.map +1 -1
- package/dist/cross-instance-notice.d.ts +10 -0
- package/dist/cross-instance-notice.js +67 -5
- package/dist/cross-instance-notice.js.map +1 -1
- package/dist/daemon.d.ts +52 -0
- package/dist/daemon.js +530 -34
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-consumed-watch.d.ts +23 -1
- package/dist/delivery-consumed-watch.js +30 -3
- package/dist/delivery-consumed-watch.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +7 -0
- package/dist/delivery-reconciliation.js +9 -1
- package/dist/delivery-reconciliation.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/export-import.d.ts +7 -0
- package/dist/export-import.js +44 -10
- package/dist/export-import.js.map +1 -1
- package/dist/fleet-context.d.ts +2 -0
- package/dist/fleet-manager.d.ts +147 -11
- package/dist/fleet-manager.js +747 -118
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-yaml-patch.d.ts +3 -0
- package/dist/fleet-yaml-patch.js +103 -0
- package/dist/fleet-yaml-patch.js.map +1 -0
- package/dist/general-knowledge/skills/concepts/SKILL.md +393 -0
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -0
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -1
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +41 -4
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +2 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +10 -4
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
- 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 +20 -2
- package/dist/instance-lifecycle.js +80 -12
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instance-topic-delete.d.ts +77 -0
- package/dist/instance-topic-delete.js +123 -0
- package/dist/instance-topic-delete.js.map +1 -0
- package/dist/instructions.d.ts +1 -1
- package/dist/instructions.js +1 -1
- package/dist/instructions.js.map +1 -1
- package/dist/kiro-engine-status.d.ts +11 -0
- package/dist/kiro-engine-status.js +6 -0
- package/dist/kiro-engine-status.js.map +1 -1
- package/dist/kiro-resume-summary.d.ts +37 -0
- package/dist/kiro-resume-summary.js +85 -0
- package/dist/kiro-resume-summary.js.map +1 -0
- package/dist/locale.js +82 -6
- package/dist/locale.js.map +1 -1
- package/dist/log-service.d.ts +30 -0
- package/dist/log-service.js +36 -3
- package/dist/log-service.js.map +1 -1
- package/dist/log-steps.d.ts +4 -3
- package/dist/log-steps.js +32 -7
- package/dist/log-steps.js.map +1 -1
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +43 -1
- package/dist/logger.js.map +1 -1
- package/dist/login-controller.js +9 -1
- package/dist/login-controller.js.map +1 -1
- package/dist/login-flows.d.ts +2 -0
- package/dist/login-flows.js +5 -2
- package/dist/login-flows.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/missed-offline.d.ts +25 -0
- package/dist/missed-offline.js +79 -0
- package/dist/missed-offline.js.map +1 -0
- package/dist/needs-you-hub.d.ts +6 -1
- package/dist/needs-you-hub.js +31 -8
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +22 -2
- package/dist/needs-you.js +17 -3
- package/dist/needs-you.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 +59 -3
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +1 -0
- package/dist/outbound-schemas.js +9 -3
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/package-preimage.js +27 -2
- package/dist/package-preimage.js.map +1 -1
- package/dist/queued-handoffs.d.ts +33 -0
- package/dist/queued-handoffs.js +75 -0
- package/dist/queued-handoffs.js.map +1 -0
- package/dist/reply-buttons-controller.js +10 -0
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/scheduler/db.d.ts +2 -1
- package/dist/scheduler/db.js +23 -9
- package/dist/scheduler/db.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/settings-api.d.ts +13 -0
- package/dist/settings-api.js +12 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/stall-reporter.d.ts +21 -0
- package/dist/stall-reporter.js +82 -0
- package/dist/stall-reporter.js.map +1 -0
- package/dist/tmux-control.d.ts +2 -2
- package/dist/tmux-control.js +4 -2
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-exec-helper.d.ts +6 -3
- package/dist/tmux-exec-helper.js +115 -53
- package/dist/tmux-exec-helper.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/tool-permissions.d.ts +5 -2
- package/dist/tool-permissions.js +6 -5
- package/dist/tool-permissions.js.map +1 -1
- package/dist/tool-progress.d.ts +2 -0
- package/dist/tool-progress.js +19 -3
- package/dist/tool-progress.js.map +1 -1
- package/dist/topic-commands.js +4 -0
- package/dist/topic-commands.js.map +1 -1
- package/dist/tunnel/cloudflared-install.d.ts +4 -0
- package/dist/tunnel/cloudflared-install.js +20 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/cloudflared.d.ts +8 -1
- package/dist/tunnel/cloudflared.js +34 -6
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/tunnel/lease.d.ts +3 -3
- package/dist/tunnel/lease.js +23 -19
- package/dist/tunnel/lease.js.map +1 -1
- package/dist/tunnel/manager.js +6 -7
- package/dist/tunnel/manager.js.map +1 -1
- package/dist/ui/chat-store.js +20 -3
- package/dist/ui/chat-strings.js +4 -4
- package/dist/ui/chat-thread.js +52 -10
- 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/fleet-usage.js +90 -27
- package/dist/ui/panel-chat.js +2 -0
- package/dist/ui/panel-log.js +7 -1
- package/dist/ui/panel-needs.js +17 -8
- package/dist/ui/settings-strings.js +2 -2
- package/dist/ui/shared/app-i18n.js +4 -4
- package/dist/ui/shared/app-needs.js +1 -1
- package/dist/ui/shared/app-shell.js +4 -2
- package/dist/ui/shared/app-stream.js +26 -0
- package/dist/ui/shared/app.css +51 -3
- 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 +339 -17
- package/dist/ui/shared/usage-dialog.js +18 -1
- package/dist/ui/shared/view-history.css +17 -0
- package/dist/ui/shared/view-history.js +438 -0
- package/dist/ui/shared/view-strings.js +42 -0
- package/dist/unattended-install.d.ts +6 -0
- package/dist/unattended-install.js +79 -0
- package/dist/unattended-install.js.map +1 -0
- package/dist/usage/capacity.d.ts +69 -0
- package/dist/usage/capacity.js +119 -0
- package/dist/usage/capacity.js.map +1 -0
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +3 -0
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/profile-timeline.d.ts +16 -0
- package/dist/usage/profile-timeline.js +58 -0
- package/dist/usage/profile-timeline.js.map +1 -0
- package/dist/usage/quota-history.d.ts +43 -0
- package/dist/usage/quota-history.js +168 -0
- package/dist/usage/quota-history.js.map +1 -0
- package/dist/usage/usage-api.d.ts +31 -0
- package/dist/usage/usage-api.js +80 -1
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/usage-report.d.ts +25 -1
- package/dist/usage-report.js +55 -4
- package/dist/usage-report.js.map +1 -1
- package/dist/usage-rollup.d.ts +4 -1
- package/dist/usage-rollup.js +12 -3
- package/dist/usage-rollup.js.map +1 -1
- package/dist/view-api.d.ts +31 -0
- package/dist/view-api.js +186 -41
- package/dist/view-api.js.map +1 -1
- package/dist/view-history.d.ts +84 -0
- package/dist/view-history.js +116 -0
- package/dist/view-history.js.map +1 -0
- package/dist/web-api.d.ts +6 -0
- package/dist/web-api.js +15 -4
- 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-login.d.ts +4 -3
- package/dist/web-login.js +23 -14
- package/dist/web-login.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/dist/web-session-authorization.d.ts +13 -2
- package/dist/web-session-authorization.js +10 -3
- package/dist/web-session-authorization.js.map +1 -1
- package/dist/web-session.d.ts +5 -0
- package/dist/web-session.js +20 -8
- package/dist/web-session.js.map +1 -1
- package/launcher/agend-install.cjs +16 -2
- package/launcher/private-home.cjs +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";
|
|
@@ -32,14 +33,14 @@ import { scopedFleetConfigEnvironment } from "./web-experimental.js";
|
|
|
32
33
|
import { readAgentFile } from "./agent-file-read.js";
|
|
33
34
|
import { ASSET_UPLOAD_MAX_BYTES, receivePreview, sendAssetInParts, writePreview } from "./web-asset-parts.js";
|
|
34
35
|
import { LOGIN_FLOWS } from "./login-flows.js";
|
|
35
|
-
import { ProgressAccumulator, summarizeProgress } from "./tool-progress.js";
|
|
36
|
+
import { ProgressAccumulator, isSearchProgram, summarizeProgress } from "./tool-progress.js";
|
|
36
37
|
import { assembleSystemPrompt, resolveWorkflowText } from "./prompt-file-ref.js";
|
|
37
38
|
import { isCrossInstanceVisibility } from "./cross-instance-notice.js";
|
|
38
39
|
import { ContextGuardian } from "./context-guardian.js";
|
|
39
40
|
import { IpcServer } from "./channel/ipc-bridge.js";
|
|
40
41
|
import { daemonBudgetMs } from "./channel/ipc-timeouts.js";
|
|
41
42
|
import { MessageBus } from "./channel/message-bus.js";
|
|
42
|
-
import { shellQuote, UnsupportedCliError, ResumeBlockedError } from "./backend/types.js";
|
|
43
|
+
import { shellQuote, KiroCompatibilityError, UnsupportedCliError, ResumeBlockedError } from "./backend/types.js";
|
|
43
44
|
import { getTmuxSession } from "./config.js";
|
|
44
45
|
import { routeToolCall } from "./channel/tool-router.js";
|
|
45
46
|
import { parseReplyButtons, replyButtonsFallbackText } from "./reply-buttons.js";
|
|
@@ -59,7 +60,8 @@ import { MuseUsageRelay, clearMuseUsageSnapshot } from "./muse-usage-relay.js";
|
|
|
59
60
|
import { deliveryContentDigest } from "./delivery-outbox.js";
|
|
60
61
|
import { consumedWatches } from "./delivery-consumed-watch.js";
|
|
61
62
|
import { queueResumePolicyForAttempt } from "./delivery-queue-evidence.js";
|
|
62
|
-
import { scanTranscriptForDeliveryMarker } from "./delivery-reconciliation.js";
|
|
63
|
+
import { markerConsumed, scanTranscriptForDeliveryMarker } from "./delivery-reconciliation.js";
|
|
64
|
+
import { QUEUED_HANDOFF_MAX_REDELIVERIES, QueuedHandoffStore } from "./queued-handoffs.js";
|
|
63
65
|
import { measureSyncWork, noteSyncWork } from "./sync-work-attribution.js";
|
|
64
66
|
import { TranscriptDeltaReader } from "./transcript-delta-reader.js";
|
|
65
67
|
import { buildResumeContinuation, clearInFlightTurnMarker, consumeInFlightTurnMarker, decideTurnResume, normalizeResumeBackend, readResumeFingerprint, RESUME_BLOCKING_DELIVERY_STATES, writeInFlightTurnMarker, } from "./turn-resume.js";
|
|
@@ -150,6 +152,8 @@ export const HANDOFF_RECHECK_MS = 1_000;
|
|
|
150
152
|
export const WAIT_STOP_POLL_MS = 250;
|
|
151
153
|
/** #2770: human messages a cancel dropped are told in one notice, this long after the last of them. */
|
|
152
154
|
export const CANCELLED_HUMANS_NOTICE_MS = 1_000;
|
|
155
|
+
/** #3030: a queued hand-off's watch outlives a long turn (the record outlives the watch in any case). */
|
|
156
|
+
export const QUEUED_HANDOFF_WATCH_LIFETIME_MS = 12 * 60 * 60_000;
|
|
153
157
|
/**
|
|
154
158
|
* An instance publishing more than this many execution-state edges inside the
|
|
155
159
|
* window is flapping; its edges stop being logged one by one (#1116). One
|
|
@@ -342,7 +346,8 @@ export function shellCommandLabel(command) {
|
|
|
342
346
|
if (!program)
|
|
343
347
|
return "";
|
|
344
348
|
const isBareWord = (t) => !!t && /^[A-Za-z][A-Za-z0-9_-]*$/.test(t);
|
|
345
|
-
|
|
349
|
+
// A search program's next word is what it searches for, not a subcommand: `grep <token>` keeps only `grep`.
|
|
350
|
+
const label = isBareWord(tokens[1]) && !isSearchProgram(program) ? `${program} ${tokens[1]}` : program;
|
|
346
351
|
return label.length > 40 ? `${label.slice(0, 39)}…` : label;
|
|
347
352
|
}
|
|
348
353
|
export class PaneStateMachine {
|
|
@@ -531,6 +536,9 @@ function countOccurrences(haystack, needle) {
|
|
|
531
536
|
}
|
|
532
537
|
/** Bottom-ready re-poll cadence for Enter-dropping TUIs once the pane is quiet. */
|
|
533
538
|
const BOTTOM_READY_POLL_MS = 250;
|
|
539
|
+
/** 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. */
|
|
540
|
+
const PANE_MODE_POLL_MS = 500;
|
|
541
|
+
const PANE_MODE_ENTER_WAIT_MS = 10 * 60_000;
|
|
534
542
|
/** Consecutive unreadable pane probes tolerated by the delivery gate before the delivery is failed (≈10s at the poll cadence). */
|
|
535
543
|
const DIALOG_PROBE_UNKNOWN_MAX = 40;
|
|
536
544
|
/** Startup dialog scan: poll cadence and wall-clock budget (keys and waits included). */
|
|
@@ -1576,6 +1584,12 @@ export class Daemon extends EventEmitter {
|
|
|
1576
1584
|
pendingWhatsNew = null;
|
|
1577
1585
|
pasteQueueDepth = 0;
|
|
1578
1586
|
/** #2770: a person's messages a cancel dropped before they reached the CLI, told together (noteCancelledHuman). */
|
|
1587
|
+
/** #3030: people's messages a CLI never took (or that a restart left unverifiable), told once per batch. */
|
|
1588
|
+
unsettledHandoffs = { "not-taken": [], unverifiable: [] };
|
|
1589
|
+
unsettledHandoffsTimer = null;
|
|
1590
|
+
/** #3030: since when (monotonic) the CLI has been idle without a break; null while it is not. */
|
|
1591
|
+
instanceIdleSinceMono = null;
|
|
1592
|
+
queuedHandoffStore = null;
|
|
1579
1593
|
cancelledHumans = [];
|
|
1580
1594
|
cancelledHumansTimer = null;
|
|
1581
1595
|
firstDeliveryDelay = new FirstDeliveryDelay();
|
|
@@ -1987,6 +2001,9 @@ export class Daemon extends EventEmitter {
|
|
|
1987
2001
|
this.publishWorkingState("turn_closed");
|
|
1988
2002
|
};
|
|
1989
2003
|
}
|
|
2004
|
+
/** Register each issued HTTP credential with the same-process lifecycle owner. */
|
|
2005
|
+
agentCredentialIssuer;
|
|
2006
|
+
setAgentCredentialIssuer(issue) { this.agentCredentialIssuer = issue; }
|
|
1990
2007
|
/** FleetManager and this daemon share a process, so durable begin/ACK is a direct synchronous store call. */
|
|
1991
2008
|
setDeliveryOutboxPort(port) {
|
|
1992
2009
|
this.deliveryOutbox = port;
|
|
@@ -2123,6 +2140,243 @@ export class Daemon extends EventEmitter {
|
|
|
2123
2140
|
logger: this.logger,
|
|
2124
2141
|
});
|
|
2125
2142
|
}
|
|
2143
|
+
handoffStore() {
|
|
2144
|
+
return this.queuedHandoffStore ??= new QueuedHandoffStore(this.instanceDir);
|
|
2145
|
+
}
|
|
2146
|
+
/** #3030: the CLI that holds a queued hand-off; a record is settled only by a later one. */
|
|
2147
|
+
handoffHolder() { return `${this.bootId}:${this.spawnGeneration}`; }
|
|
2148
|
+
/**
|
|
2149
|
+
* #3030: before the paste. A hand-off into a busy Claude Code / Codex queue gets its record — the transcript
|
|
2150
|
+
* checkpoint before the paste, the unique leading marker and what a redelivery needs — or, for a redelivery written
|
|
2151
|
+
* as an ordinary idle submission, its record moves its checkpoint here, so a crash right after this paste is still
|
|
2152
|
+
* judged from the transcript that will show it. A write that did not keep a record runs as before.
|
|
2153
|
+
*/
|
|
2154
|
+
async recordQueuedHandoff(formatted, handingOff, verdict, durableAttempt, humanVerificationId) {
|
|
2155
|
+
const track = verdict.trackHandoff;
|
|
2156
|
+
if (!track)
|
|
2157
|
+
return;
|
|
2158
|
+
if (!handingOff && !track.id) {
|
|
2159
|
+
// An earlier attempt of this delivery was a hand-off; this one is an idle submission.
|
|
2160
|
+
if (verdict.handoff)
|
|
2161
|
+
this.dropQueuedHandoff(verdict);
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
const backend = this.config.backend ?? this.backend?.binaryName ?? "";
|
|
2165
|
+
if (!["claude-code", "codex"].includes(backend))
|
|
2166
|
+
return;
|
|
2167
|
+
const marker = durableAttempt ? `[agend-delivery-id:${durableAttempt.deliveryId}]`
|
|
2168
|
+
: humanVerificationId ? `[agend-human-id:${humanVerificationId}]` : null;
|
|
2169
|
+
if (!marker || !formatted.startsWith(marker))
|
|
2170
|
+
return;
|
|
2171
|
+
let checkpoint = verdict.transcriptCheckpoint ?? null;
|
|
2172
|
+
if (!checkpoint) {
|
|
2173
|
+
try {
|
|
2174
|
+
checkpoint = await this.transcriptMonitor?.reconciliationCheckpoint() ?? null;
|
|
2175
|
+
}
|
|
2176
|
+
catch (err) {
|
|
2177
|
+
this.logger.debug({ err }, "Could not checkpoint the transcript before a queued hand-off");
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
if (!checkpoint)
|
|
2181
|
+
return; // nothing could judge it later: it runs as before (✅ at the hand-off)
|
|
2182
|
+
const entry = {
|
|
2183
|
+
id: track.id ?? randomUUID(), marker, backend, path: checkpoint.path, offset: checkpoint.offset,
|
|
2184
|
+
content: track.content, meta: track.meta, holder: this.handoffHolder(), createdAt: Date.now(),
|
|
2185
|
+
redeliveries: track.redeliveries ?? 0,
|
|
2186
|
+
};
|
|
2187
|
+
try {
|
|
2188
|
+
const dropped = this.handoffStore().put(entry);
|
|
2189
|
+
if (handingOff)
|
|
2190
|
+
verdict.handoff = entry;
|
|
2191
|
+
for (const old of dropped)
|
|
2192
|
+
this.logger.warn({ handoff: old.id, createdAt: old.createdAt }, "Queued hand-off record dropped: too many on record");
|
|
2193
|
+
}
|
|
2194
|
+
catch (err) {
|
|
2195
|
+
this.logger.warn({ err }, "Could not record a queued hand-off; it runs as before (✅ at the hand-off)");
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
/**
|
|
2199
|
+
* #3030: after pushChannelMessage's write. A hand-off on record whose paste went out is watched (✅ when a turn
|
|
2200
|
+
* takes it); one that was never written goes. A redelivery that ended as an ordinary submission is done; one that a
|
|
2201
|
+
* cancel dropped goes too (a cancel is the person's); one stopped before its write stays for the next CLI.
|
|
2202
|
+
*/
|
|
2203
|
+
afterTrackedWrite(meta, verdict, sent, deliveryEpoch) {
|
|
2204
|
+
try {
|
|
2205
|
+
// A stale epoch with writes still allowed is a cancel (the person's); a stop fences writes too, and keeps records.
|
|
2206
|
+
const cancelled = !this.isDeliveryEpochCurrent(deliveryEpoch) && !this.deliveryWritesStopping;
|
|
2207
|
+
if (verdict.handoff) {
|
|
2208
|
+
if (sent || verdict.paneWriteStarted)
|
|
2209
|
+
this.watchQueuedHandoff(verdict.handoff);
|
|
2210
|
+
else if (!verdict.trackHandoff?.id || cancelled)
|
|
2211
|
+
this.dropQueuedHandoff(verdict);
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
const id = meta.handoff_redelivery;
|
|
2215
|
+
if (!id)
|
|
2216
|
+
return;
|
|
2217
|
+
if (sent || verdict.paneWriteStarted || cancelled) {
|
|
2218
|
+
this.handoffStore().remove(id);
|
|
2219
|
+
this.logger.info({ handoff: id, sent, proof: verdict.proof ?? null }, "Queued hand-off redelivered as an ordinary submission");
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
catch (err) {
|
|
2223
|
+
this.logger.warn({ err }, "Could not update the queued hand-off record after its write");
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
redeliveryStillOurs(meta) {
|
|
2227
|
+
const holder = this.handoffHolder();
|
|
2228
|
+
if (meta.handoff_holder !== holder)
|
|
2229
|
+
return false;
|
|
2230
|
+
try {
|
|
2231
|
+
return this.handoffStore().get(meta.handoff_redelivery)?.holder === holder;
|
|
2232
|
+
}
|
|
2233
|
+
catch {
|
|
2234
|
+
return false;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
dropQueuedHandoff(verdict) {
|
|
2238
|
+
const entry = verdict.handoff;
|
|
2239
|
+
verdict.handoff = undefined;
|
|
2240
|
+
if (!entry)
|
|
2241
|
+
return;
|
|
2242
|
+
try {
|
|
2243
|
+
this.handoffStore().remove(entry.id);
|
|
2244
|
+
}
|
|
2245
|
+
catch (err) {
|
|
2246
|
+
this.logger.warn({ err, handoff: entry.id }, "Could not remove a queued hand-off record");
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* #3030: the hand-off's CLI is the current one: watch its transcript for the turn that takes it (✅ then), and for an
|
|
2251
|
+
* idle CLI that never did (reported). A stop ends the watch; the record stays for the next CLI to settle.
|
|
2252
|
+
*/
|
|
2253
|
+
watchQueuedHandoff(entry) {
|
|
2254
|
+
const spawn = this.spawnGeneration, fence = this.launchFenceEpoch, epoch = this.deliveryEpoch;
|
|
2255
|
+
const started = consumedWatches.start({
|
|
2256
|
+
deliveryId: `handoff:${entry.id}`,
|
|
2257
|
+
attemptNo: entry.redeliveries,
|
|
2258
|
+
backend: entry.backend,
|
|
2259
|
+
path: entry.path,
|
|
2260
|
+
offset: entry.offset,
|
|
2261
|
+
marker: entry.marker,
|
|
2262
|
+
// A cancel or stop moves the epoch: the watch ends quietly (a cancel cleared the record, a stop leaves it).
|
|
2263
|
+
current: () => !this.deliveryWritesStopping && spawn === this.spawnGeneration && fence === this.launchFenceEpoch
|
|
2264
|
+
&& epoch === this.deliveryEpoch,
|
|
2265
|
+
consumed: (via) => this.settleQueuedHandoff(entry, "taken", `transcript:${via}`),
|
|
2266
|
+
idleForMs: () => this.instanceIdleSinceMono === null ? null : performance.now() - this.instanceIdleSinceMono,
|
|
2267
|
+
notTaken: () => this.settleQueuedHandoff(entry, "not-taken", "transcript:absent-while-idle"),
|
|
2268
|
+
// A queued message waits for the running turn, however long that is.
|
|
2269
|
+
lifetimeMs: QUEUED_HANDOFF_WATCH_LIFETIME_MS,
|
|
2270
|
+
logger: this.logger,
|
|
2271
|
+
});
|
|
2272
|
+
if (!started) {
|
|
2273
|
+
// No watch (the fleet's cap): ✅ as before; the record still lets a restart judge it.
|
|
2274
|
+
const status = channelStatus(entry.meta);
|
|
2275
|
+
if (status)
|
|
2276
|
+
this.emit("message_confirmed", status);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
/** #3030: a record's end — taken (✅), or never taken / not judgeable (❌, told once per batch). */
|
|
2280
|
+
settleQueuedHandoff(entry, outcome, proof) {
|
|
2281
|
+
try {
|
|
2282
|
+
this.handoffStore().remove(entry.id);
|
|
2283
|
+
}
|
|
2284
|
+
catch (err) {
|
|
2285
|
+
this.logger.warn({ err, handoff: entry.id }, "Could not remove a settled queued hand-off record");
|
|
2286
|
+
}
|
|
2287
|
+
const meta = entry.meta;
|
|
2288
|
+
const status = channelStatus(meta);
|
|
2289
|
+
if (outcome === "taken") {
|
|
2290
|
+
this.logger.info({ handoff: entry.id, proof, redeliveries: entry.redeliveries }, "Queued hand-off taken by the CLI");
|
|
2291
|
+
if (status)
|
|
2292
|
+
this.emit("message_confirmed", status);
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
this.logger.warn({ handoff: entry.id, outcome, proof, redeliveries: entry.redeliveries }, "Queued hand-off was not taken by the CLI");
|
|
2296
|
+
if (meta.from_instance) {
|
|
2297
|
+
this.reportCrossInstanceDeliveryFailure(meta, { reached: true, phase: "handoff-not-consumed", proof: outcome === "not-taken" ? "not-taken" : "unverifiable" });
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
if (!status)
|
|
2301
|
+
return;
|
|
2302
|
+
this.unsettledHandoffs[outcome].push({ chatId: status.chatId, messageId: status.messageId, ...(status.threadId ? { threadId: status.threadId } : {}) });
|
|
2303
|
+
if (this.unsettledHandoffsTimer)
|
|
2304
|
+
clearTimeout(this.unsettledHandoffsTimer);
|
|
2305
|
+
this.unsettledHandoffsTimer = setTimeout(() => {
|
|
2306
|
+
this.unsettledHandoffsTimer = null;
|
|
2307
|
+
for (const reason of ["not-taken", "unverifiable"]) {
|
|
2308
|
+
const messages = this.unsettledHandoffs[reason].splice(0);
|
|
2309
|
+
if (messages.length)
|
|
2310
|
+
this.emit("human_handoffs_unsettled", { reason, messages });
|
|
2311
|
+
}
|
|
2312
|
+
}, CANCELLED_HUMANS_NOTICE_MS);
|
|
2313
|
+
this.unsettledHandoffsTimer.unref?.();
|
|
2314
|
+
}
|
|
2315
|
+
/**
|
|
2316
|
+
* #3030: right after a CLI came up — the records left by CLIs that are gone. Their queue went with them, so each is
|
|
2317
|
+
* judged by its own transcript from its checkpoint: taken there (✅, nothing pasted); proven not taken (the whole
|
|
2318
|
+
* delta read, the marker absent or only enqueued) → redelivered once, with the same marker, through the ordinary
|
|
2319
|
+
* queue; anything that cannot be judged → never pasted, reported. Ownership is evidence: never pasted twice.
|
|
2320
|
+
*/
|
|
2321
|
+
async reconcileQueuedHandoffs() {
|
|
2322
|
+
let entries;
|
|
2323
|
+
try {
|
|
2324
|
+
entries = this.handoffStore().list().filter(e => e.holder !== this.handoffHolder());
|
|
2325
|
+
}
|
|
2326
|
+
catch (err) {
|
|
2327
|
+
this.logger.warn({ err }, "Could not read the queued hand-off records");
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
if (!entries.length)
|
|
2331
|
+
return;
|
|
2332
|
+
// Captured before any await: a reconciliation that outlives its CLI can never claim the next CLI's holder.
|
|
2333
|
+
const holder = this.handoffHolder();
|
|
2334
|
+
const spawn = this.spawnGeneration, fence = this.launchFenceEpoch, epoch = this.deliveryEpoch;
|
|
2335
|
+
const fresh = () => spawn === this.spawnGeneration && fence === this.launchFenceEpoch && epoch === this.deliveryEpoch
|
|
2336
|
+
&& !this.deliveryWritesStopping;
|
|
2337
|
+
let redelivered = 0, taken = 0, unverifiable = 0;
|
|
2338
|
+
for (const entry of entries) {
|
|
2339
|
+
const kind = await scanTranscriptForDeliveryMarker(entry.path, entry.offset, entry.backend, { marker: entry.marker });
|
|
2340
|
+
// This CLI is gone already, or a cancel came: the next CLI settles them (a cancel cleared them).
|
|
2341
|
+
if (!fresh())
|
|
2342
|
+
return;
|
|
2343
|
+
// …and the record is still the one that was read (another reconciliation or a cancel did not change it meanwhile).
|
|
2344
|
+
let current;
|
|
2345
|
+
try {
|
|
2346
|
+
current = this.handoffStore().get(entry.id);
|
|
2347
|
+
}
|
|
2348
|
+
catch {
|
|
2349
|
+
current = undefined;
|
|
2350
|
+
}
|
|
2351
|
+
if (!current || current.holder !== entry.holder || current.redeliveries !== entry.redeliveries)
|
|
2352
|
+
continue;
|
|
2353
|
+
if (markerConsumed(kind)) {
|
|
2354
|
+
taken++;
|
|
2355
|
+
this.settleQueuedHandoff(entry, "taken", `restart-transcript:${kind}`);
|
|
2356
|
+
continue;
|
|
2357
|
+
}
|
|
2358
|
+
if (kind === "unavailable" || entry.redeliveries >= QUEUED_HANDOFF_MAX_REDELIVERIES) {
|
|
2359
|
+
unverifiable++;
|
|
2360
|
+
this.settleQueuedHandoff(entry, "unverifiable", kind === "unavailable" ? "restart-transcript:unavailable" : "redelivery-limit");
|
|
2361
|
+
continue;
|
|
2362
|
+
}
|
|
2363
|
+
// Proven not taken: the CLI that queued it is gone and its transcript, read to the end, never took it.
|
|
2364
|
+
const next = { ...entry, holder, redeliveries: entry.redeliveries + 1 };
|
|
2365
|
+
try {
|
|
2366
|
+
this.handoffStore().put(next);
|
|
2367
|
+
}
|
|
2368
|
+
catch (err) {
|
|
2369
|
+
this.logger.warn({ err, handoff: entry.id }, "Could not update a queued hand-off record; not redelivered now");
|
|
2370
|
+
continue;
|
|
2371
|
+
}
|
|
2372
|
+
redelivered++;
|
|
2373
|
+
// The person's receipt was settled by the first write: the redelivery carries its marker, not its receipt binding.
|
|
2374
|
+
const { human_receipt_id: _r, human_owner_key: _o, human_daemon_owner: _d, human_wire_sha256: _w, ...meta } = entry.meta;
|
|
2375
|
+
this.pushChannelMessage(entry.content, { ...meta, handoff_redelivery: entry.id,
|
|
2376
|
+
handoff_redeliveries: String(next.redeliveries), handoff_holder: next.holder }, undefined, epoch);
|
|
2377
|
+
}
|
|
2378
|
+
this.logger.info({ instance: this.name, redelivered, taken, unverifiable, proof: "transcript" }, "Queued hand-offs left by the previous CLI settled");
|
|
2379
|
+
}
|
|
2126
2380
|
finishDurableSubmission(delivery, verdict) {
|
|
2127
2381
|
// Legacy backends may accept a pane write without enough pane evidence to
|
|
2128
2382
|
// claim submission. Keep that row inspectable instead of recording a false
|
|
@@ -2631,6 +2885,8 @@ export class Daemon extends EventEmitter {
|
|
|
2631
2885
|
this.startErrorMonitor();
|
|
2632
2886
|
}
|
|
2633
2887
|
this.startInstanceStateMonitor();
|
|
2888
|
+
// #3030: messages a gone CLI still had queued — judged by its transcript, redelivered once or reported.
|
|
2889
|
+
void this.reconcileQueuedHandoffs().catch(err => this.logger.warn({ err }, "Queued hand-off reconciliation failed"));
|
|
2634
2890
|
this.logger.info(`${this.name} ready`);
|
|
2635
2891
|
}
|
|
2636
2892
|
/**
|
|
@@ -3105,8 +3361,6 @@ export class Daemon extends EventEmitter {
|
|
|
3105
3361
|
scheduleNext();
|
|
3106
3362
|
return;
|
|
3107
3363
|
}
|
|
3108
|
-
// The CLI owns the MCP server process, so the daemon can only observe it.
|
|
3109
|
-
measureSyncWork("daemon.healthCheck", () => this.checkMcpServerAlive());
|
|
3110
3364
|
// Human-readable backend label for logs (e.g. "claude", "kiro-cli")
|
|
3111
3365
|
const cliLabel = this.backend?.binaryName ?? "CLI";
|
|
3112
3366
|
const healthTmux = this.tmux;
|
|
@@ -3127,6 +3381,9 @@ export class Daemon extends EventEmitter {
|
|
|
3127
3381
|
if (discardStaleHealth())
|
|
3128
3382
|
return;
|
|
3129
3383
|
if (paneStatus?.alive) {
|
|
3384
|
+
// A CLI shutdown also closes its MCP children. Revive tools only for
|
|
3385
|
+
// a positively live pane, never while its exit is being classified.
|
|
3386
|
+
measureSyncWork("daemon.healthCheck", () => this.checkMcpServerAlive());
|
|
3130
3387
|
// Capture only a known primary while it lives. No extra tmux call: PID comes
|
|
3131
3388
|
// from this same status read. Fast/unobserved deaths remain ordinary crashes.
|
|
3132
3389
|
const key = this.oomSessionKey();
|
|
@@ -3173,6 +3430,29 @@ export class Daemon extends EventEmitter {
|
|
|
3173
3430
|
return;
|
|
3174
3431
|
}
|
|
3175
3432
|
}
|
|
3433
|
+
// tmux marks the PTY dead when its fd closes, before SIGCHLD can fill
|
|
3434
|
+
// pane_dead_status. Claude /exit (and any other CLI) can hit this gap.
|
|
3435
|
+
// Give the status four bounded re-probes; missing is never exit zero.
|
|
3436
|
+
if (paneStatus && !paneStatus.alive && paneStatus.exitCode === undefined) {
|
|
3437
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
3438
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
3439
|
+
if (discardStaleHealth())
|
|
3440
|
+
return;
|
|
3441
|
+
const observed = await healthTmux.getPaneStatus();
|
|
3442
|
+
if (discardStaleHealth())
|
|
3443
|
+
return;
|
|
3444
|
+
if (observed)
|
|
3445
|
+
paneStatus = observed;
|
|
3446
|
+
if (paneStatus.alive || paneStatus.exitCode !== undefined)
|
|
3447
|
+
break;
|
|
3448
|
+
}
|
|
3449
|
+
if (paneStatus.alive) {
|
|
3450
|
+
this.windowQueryFailureTicks = 0;
|
|
3451
|
+
this.stormWindow?.noteWindowAlive(this.name);
|
|
3452
|
+
scheduleNext();
|
|
3453
|
+
return;
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3176
3456
|
// paneStatus === null → window gone entirely (e.g. tmux server crash)
|
|
3177
3457
|
// paneStatus.alive === false → pane dead, exit code available
|
|
3178
3458
|
const exitCode = paneStatus?.exitCode;
|
|
@@ -3356,6 +3636,8 @@ export class Daemon extends EventEmitter {
|
|
|
3356
3636
|
try {
|
|
3357
3637
|
await this.spawnClaudeWindow();
|
|
3358
3638
|
this.setProcessStatus("running");
|
|
3639
|
+
// #3030: messages a gone CLI still had queued — judged by its transcript, redelivered once or reported.
|
|
3640
|
+
void this.reconcileQueuedHandoffs().catch(err => this.logger.warn({ err }, "Queued hand-off reconciliation failed"));
|
|
3359
3641
|
this.logger.info("Recovered from background session conflict");
|
|
3360
3642
|
this.emit("crash_respawn", this.name);
|
|
3361
3643
|
}
|
|
@@ -3515,6 +3797,8 @@ export class Daemon extends EventEmitter {
|
|
|
3515
3797
|
catch { /* may not exist */ }
|
|
3516
3798
|
}
|
|
3517
3799
|
this.setProcessStatus("running");
|
|
3800
|
+
// #3030: messages a gone CLI still had queued — judged by its transcript, redelivered once or reported.
|
|
3801
|
+
void this.reconcileQueuedHandoffs().catch(err => this.logger.warn({ err }, "Queued hand-off reconciliation failed"));
|
|
3518
3802
|
this.logger.info({ resumed }, `Respawned ${cliLabel} window after crash`);
|
|
3519
3803
|
this.emit("crash_respawn", this.name);
|
|
3520
3804
|
// Emit the per-instance audit event while the storm is still active
|
|
@@ -3706,7 +3990,9 @@ export class Daemon extends EventEmitter {
|
|
|
3706
3990
|
const interactivePrompt = this.interactivePromptDetector.observe(pane, Date.now(), this.instanceStateLastOutputAt);
|
|
3707
3991
|
if (interactivePrompt) {
|
|
3708
3992
|
this.logger.warn(interactivePrompt, "Interactive terminal prompt is waiting for human input");
|
|
3709
|
-
|
|
3993
|
+
// M-2269: the prompt as it was on screen, from this capture, for the Log.
|
|
3994
|
+
const screen = dialogScreenText(pane);
|
|
3995
|
+
this.emit("interactive_prompt", { name: this.name, ...interactivePrompt, ...(screen ? { screen } : {}) });
|
|
3710
3996
|
// A prompt is not an error and must not enter the PTY recovery gate.
|
|
3711
3997
|
// Continue scanning real errors in this same snapshot.
|
|
3712
3998
|
}
|
|
@@ -3901,7 +4187,7 @@ export class Daemon extends EventEmitter {
|
|
|
3901
4187
|
else if (dialog.verifyAfterKeys && !resolved) {
|
|
3902
4188
|
this.logger.warn({ dialog: dialog.description }, "Safety dialog choice was sent but follow-up verification or notice submission failed");
|
|
3903
4189
|
}
|
|
3904
|
-
this.trackDialogParked(blockingSeen);
|
|
4190
|
+
this.trackDialogParked(blockingSeen, pane);
|
|
3905
4191
|
return; // Dialog handled (or deliberately deferred): skip error checks this cycle
|
|
3906
4192
|
}
|
|
3907
4193
|
// Reached only when no dialog was answered this poll (none on screen, a fenced one, or one waiting out its
|
|
@@ -3911,7 +4197,7 @@ export class Daemon extends EventEmitter {
|
|
|
3911
4197
|
this.dialogAnswers = null;
|
|
3912
4198
|
this.retireParkedNotice("answer");
|
|
3913
4199
|
}
|
|
3914
|
-
this.trackDialogParked(blockingSeen);
|
|
4200
|
+
this.trackDialogParked(blockingSeen, pane);
|
|
3915
4201
|
this.trackRemoteCard(blockingSeen, pane, captureMono);
|
|
3916
4202
|
if (blockingSeen)
|
|
3917
4203
|
return; // held dialog: skip error checks this cycle
|
|
@@ -4209,6 +4495,14 @@ export class Daemon extends EventEmitter {
|
|
|
4209
4495
|
// (Completion paths clear via the guard's onComplete; a cancel with no
|
|
4210
4496
|
// later idle edge would otherwise leave the marker behind.)
|
|
4211
4497
|
clearInFlightTurnMarker(this.instanceDir);
|
|
4498
|
+
// #3030: what the person cancelled is not kept for a redelivery (nor reported as not taken).
|
|
4499
|
+
try {
|
|
4500
|
+
for (const entry of this.handoffStore().list())
|
|
4501
|
+
this.handoffStore().remove(entry.id);
|
|
4502
|
+
}
|
|
4503
|
+
catch (err) {
|
|
4504
|
+
this.logger.warn({ err }, "Could not clear queued hand-off records on cancel");
|
|
4505
|
+
}
|
|
4212
4506
|
this.deliveryEpoch = fleetEpoch === undefined
|
|
4213
4507
|
? this.deliveryEpoch + 1
|
|
4214
4508
|
: Math.max(this.deliveryEpoch, fleetEpoch);
|
|
@@ -5505,6 +5799,10 @@ export class Daemon extends EventEmitter {
|
|
|
5505
5799
|
if (snapshot.state === "working" && this.recoveryBootSettled
|
|
5506
5800
|
&& this.workingRecoveryInputOwner !== JSON.stringify(this.interactionOwner()))
|
|
5507
5801
|
this.recoveryNativeWork = true;
|
|
5802
|
+
if (snapshot.state !== "idle")
|
|
5803
|
+
this.instanceIdleSinceMono = null;
|
|
5804
|
+
else if (this.instanceState !== "idle" || this.instanceIdleSinceMono === null)
|
|
5805
|
+
this.instanceIdleSinceMono = performance.now();
|
|
5508
5806
|
this.instanceState = snapshot.state;
|
|
5509
5807
|
this.acceptedInstanceSnapshot = { ...snapshot };
|
|
5510
5808
|
// Reuse an already-captured pane and the backend's pure parser, never capture for the journal.
|
|
@@ -5616,7 +5914,7 @@ export class Daemon extends EventEmitter {
|
|
|
5616
5914
|
* message can arrive mid-turn and sit queued, and marking at arrival would let
|
|
5617
5915
|
* the PREVIOUS turn's idle edge consume (and reset) the new turn's state.
|
|
5618
5916
|
*/
|
|
5619
|
-
markTurnStarted(meta, deliveredText, deliveryEpoch = this.deliveryEpoch) {
|
|
5917
|
+
markTurnStarted(meta, deliveredText, deliveryEpoch = this.deliveryEpoch, queuedHandoff = false) {
|
|
5620
5918
|
// Cancel may have interrupted an in-flight paste before its promise settled.
|
|
5621
5919
|
// Its late success must not re-arm the turn that the user just cancelled.
|
|
5622
5920
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
@@ -5664,7 +5962,9 @@ export class Daemon extends EventEmitter {
|
|
|
5664
5962
|
// during the continuation boots clean instead of chaining resumes.
|
|
5665
5963
|
// #2544: a reply that settled before this arm already answered the person —
|
|
5666
5964
|
// writing a marker would re-inject a continuation for a finished turn.
|
|
5667
|
-
|
|
5965
|
+
// #3030: a message only queued in the busy CLI has not started a turn: the interrupted turn is still the running one,
|
|
5966
|
+
// and a queued message lost to a restart is the hand-off record's to redeliver, not a continuation's.
|
|
5967
|
+
if (!meta.resumedContinuationOf && !armed.completionDelivered && !queuedHandoff) {
|
|
5668
5968
|
const resumeBackend = this.config.backend ?? this.backend?.binaryName ?? this.runtimeIdentity?.backend;
|
|
5669
5969
|
const resumeCwd = this.config.working_directory ?? "";
|
|
5670
5970
|
writeInFlightTurnMarker(this.instanceDir, {
|
|
@@ -7069,10 +7369,12 @@ export class Daemon extends EventEmitter {
|
|
|
7069
7369
|
// Command name only — never the arguments. See shellCommandLabel.
|
|
7070
7370
|
if (name === "Bash")
|
|
7071
7371
|
return `$ ${shellCommandLabel(String(inp.command ?? ""))}`;
|
|
7372
|
+
// The tool name only — never the search pattern: it can be a literal secret someone is looking for, and this label
|
|
7373
|
+
// reaches the channel's progress line.
|
|
7072
7374
|
if (name === "Glob")
|
|
7073
|
-
return
|
|
7375
|
+
return "Glob";
|
|
7074
7376
|
if (name === "Grep")
|
|
7075
|
-
return
|
|
7377
|
+
return "Grep";
|
|
7076
7378
|
if (name === "Agent")
|
|
7077
7379
|
return "Agent (subagent)";
|
|
7078
7380
|
if (name.startsWith("mcp__agend__"))
|
|
@@ -7443,6 +7745,13 @@ export class Daemon extends EventEmitter {
|
|
|
7443
7745
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
7444
7746
|
this.logger.info("Pending channel delivery dropped by user cancel");
|
|
7445
7747
|
this.deferDurableDelivery(meta, "delivery epoch changed while queued");
|
|
7748
|
+
// #3030: a cancel is the person's — a queued redelivery it dropped is not redelivered later.
|
|
7749
|
+
if (meta.handoff_redelivery && !this.deliveryWritesStopping) {
|
|
7750
|
+
try {
|
|
7751
|
+
this.handoffStore().remove(meta.handoff_redelivery);
|
|
7752
|
+
}
|
|
7753
|
+
catch { /* the next CLI judges it */ }
|
|
7754
|
+
}
|
|
7446
7755
|
return;
|
|
7447
7756
|
}
|
|
7448
7757
|
if (this.config.pre_task_command) {
|
|
@@ -7453,13 +7762,26 @@ export class Daemon extends EventEmitter {
|
|
|
7453
7762
|
// #2800: with this real message, in the same paste — never a paste or turn of its own: after its delivery marker
|
|
7454
7763
|
// (transcript proof anchors on it), before the message. A person's message with a verified receipt is pasted
|
|
7455
7764
|
// byte for byte (its proof is the wire's hash), so the notice waits for the next delivery without one.
|
|
7456
|
-
const whatsNew = this.pendingWhatsNew && !meta.human_receipt_id
|
|
7765
|
+
const whatsNew = this.pendingWhatsNew && !meta.human_receipt_id && !meta.handoff_redelivery
|
|
7766
|
+
? this.currentWhatsNew(this.pendingWhatsNew) : null;
|
|
7457
7767
|
const pasted = whatsNew ? withWhatsNew(formatted, whatsNew.notice) : formatted;
|
|
7458
7768
|
const status = channelStatus(meta);
|
|
7459
7769
|
// A fresh delivery begins a fresh turn — its bubble must not inherit
|
|
7460
7770
|
// the previous turn's tool list.
|
|
7461
7771
|
this.resetToolProgress();
|
|
7462
7772
|
verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
|
|
7773
|
+
// #3030: a hand-off into the CLI's own queue stays on record until a turn takes it (redeliveries included).
|
|
7774
|
+
if (!content.startsWith("/raw ") || meta.from_instance) {
|
|
7775
|
+
const { handoff_redelivery: redeliveryOf, handoff_redeliveries: redeliveries, handoff_holder: _holder, ...kept } = meta;
|
|
7776
|
+
verdict.trackHandoff = { content, meta: kept,
|
|
7777
|
+
...(redeliveryOf ? { id: redeliveryOf, redeliveries: Number(redeliveries) || 0 } : {}) };
|
|
7778
|
+
}
|
|
7779
|
+
// #3030: a redelivery is written only by the CLI it was issued for, and only while its record still says so —
|
|
7780
|
+
// one queued for a CLI that was replaced meanwhile is that next CLI's reconciliation's to judge again.
|
|
7781
|
+
if (meta.handoff_redelivery && !this.redeliveryStillOurs(meta)) {
|
|
7782
|
+
this.logger.info({ handoff: meta.handoff_redelivery }, "Queued hand-off redelivery dropped: its CLI was replaced before the write");
|
|
7783
|
+
return;
|
|
7784
|
+
}
|
|
7463
7785
|
const durableAttempt = this.durableDeliveryAttempt(meta, content);
|
|
7464
7786
|
if (durableAttempt === false) {
|
|
7465
7787
|
this.deferDurableDelivery(meta, "invalid durable delivery attempt metadata");
|
|
@@ -7482,10 +7804,11 @@ export class Daemon extends EventEmitter {
|
|
|
7482
7804
|
if (whatsNew && (sent || verdict.paneWriteStarted))
|
|
7483
7805
|
this.recordWhatsNewSent(whatsNew);
|
|
7484
7806
|
}
|
|
7807
|
+
this.afterTrackedWrite(meta, verdict, !!sent, deliveryEpoch);
|
|
7485
7808
|
if (sent) {
|
|
7486
7809
|
delivered = true;
|
|
7487
7810
|
this.finishDurableSubmission(durable, verdict);
|
|
7488
|
-
this.markTurnStarted(meta, formatted, deliveryEpoch);
|
|
7811
|
+
this.markTurnStarted(meta, formatted, deliveryEpoch, !!verdict.handoff);
|
|
7489
7812
|
}
|
|
7490
7813
|
else if (durableAttempt && verdict.paneWriteStarted) {
|
|
7491
7814
|
this.finishDurableDelivery(durable, "uncertain", `${verdict.phase ?? "post-submit"}:${verdict.proof ?? "unproven"}`, verdict);
|
|
@@ -7768,6 +8091,9 @@ export class Daemon extends EventEmitter {
|
|
|
7768
8091
|
// The hand-off's last capture had no box: nothing was begun or written — wait for readiness, then try again.
|
|
7769
8092
|
if (written === "handoff-box-unread")
|
|
7770
8093
|
return "dialog";
|
|
8094
|
+
// M-2267: the pane is in a tmux mode: nothing written — wait for readiness (it holds while the mode lasts), retry.
|
|
8095
|
+
if (written === "pane-in-mode")
|
|
8096
|
+
return "dialog";
|
|
7771
8097
|
// Fenced before its first write: not attempted — redo after a spawn, else drop without a ❌.
|
|
7772
8098
|
return written === false && verdict.fenced ? stale() : written;
|
|
7773
8099
|
});
|
|
@@ -8241,7 +8567,7 @@ export class Daemon extends EventEmitter {
|
|
|
8241
8567
|
&& this.hasAutoDialogClaim(dialog)
|
|
8242
8568
|
&& dialogs.some(other => other !== dialog && Daemon.dialogMatches(other, pane)))
|
|
8243
8569
|
continue;
|
|
8244
|
-
this.trackDialogParked(dialog);
|
|
8570
|
+
this.trackDialogParked(dialog, pane);
|
|
8245
8571
|
this.trackRemoteCard(dialog, pane, probeMono);
|
|
8246
8572
|
return { state: "dialog", dialog };
|
|
8247
8573
|
}
|
|
@@ -8620,7 +8946,8 @@ export class Daemon extends EventEmitter {
|
|
|
8620
8946
|
this.parkedNoticeOut = null;
|
|
8621
8947
|
this.emit("dialog_unparked", { name: this.name });
|
|
8622
8948
|
}
|
|
8623
|
-
|
|
8949
|
+
/** `pane`: the capture the caller already holds — M-2269 keeps the dialog's text from it for the Log (no new capture). */
|
|
8950
|
+
trackDialogParked(dialog, pane = "") {
|
|
8624
8951
|
if (!dialog) {
|
|
8625
8952
|
if (this.dialogParkedSince !== 0)
|
|
8626
8953
|
this.logger.info({ dialog: this.dialogParkedKey }, "Blocking dialog is gone from the pane");
|
|
@@ -8658,7 +8985,9 @@ export class Daemon extends EventEmitter {
|
|
|
8658
8985
|
this.dialogParkedReported = true;
|
|
8659
8986
|
this.parkedNoticeOut = "parked";
|
|
8660
8987
|
this.logger.warn({ dialog: dialog.description, parkedForMs: Date.now() - this.dialogParkedSince }, "CLI dialog is still on screen — not auto-answering, reporting for a human");
|
|
8988
|
+
const screen = dialogScreenText(pane);
|
|
8661
8989
|
this.emit("dialog_parked", { name: this.name, description: dialog.description, holdOnly: dialog.holdOnly === true,
|
|
8990
|
+
...(screen ? { screen } : {}),
|
|
8662
8991
|
...(dialog.parkedAdvice ? { advice: dialog.parkedAdvice } : {}),
|
|
8663
8992
|
...(episode !== null && interaction.kind ? { kind: interaction.kind, episode, backend: this.config.backend ?? "claude-code" } : {}),
|
|
8664
8993
|
...(unrecognised ? { unrecognised: true, question: this.unrecognisedPrompt?.shape.question ?? null } : {}) });
|
|
@@ -9018,6 +9347,14 @@ export class Daemon extends EventEmitter {
|
|
|
9018
9347
|
: await this.waitForPaneIdleForDelivery(windowId, remaining);
|
|
9019
9348
|
if (idle !== true)
|
|
9020
9349
|
return idle;
|
|
9350
|
+
// M-2267: a person left the pane in a tmux mode (copy-mode): its keys would go to the mode. Hold — never take the
|
|
9351
|
+
// pane out of it — and look again; the delivery goes once the mode ends (or this wait's own deadline).
|
|
9352
|
+
if (await this.paneInTmuxMode("readiness")) {
|
|
9353
|
+
if (stop && await stop())
|
|
9354
|
+
return "stopped";
|
|
9355
|
+
await new Promise(r => setTimeout(r, BOTTOM_READY_POLL_MS));
|
|
9356
|
+
continue;
|
|
9357
|
+
}
|
|
9021
9358
|
// The idle wait just resolved, so re-reading the silence gate would only
|
|
9022
9359
|
// add a stale-cache race; ask the pane the remaining questions directly.
|
|
9023
9360
|
const transient = await this.probeInputTransient();
|
|
@@ -9563,17 +9900,119 @@ export class Daemon extends EventEmitter {
|
|
|
9563
9900
|
* precisely so the lock's scope is visible at the call site rather than being
|
|
9564
9901
|
* an invariant maintained by comments.
|
|
9565
9902
|
*/
|
|
9566
|
-
async sendDeliveryEnter(phase, stillCurrent, durableAttempt) {
|
|
9903
|
+
async sendDeliveryEnter(phase, stillCurrent, durableAttempt, onInputHeld) {
|
|
9567
9904
|
if (!(await this.waitForInputTransientToClear(phase)))
|
|
9568
9905
|
return false;
|
|
9569
|
-
return this.sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt);
|
|
9906
|
+
return this.sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt, onInputHeld);
|
|
9907
|
+
}
|
|
9908
|
+
/**
|
|
9909
|
+
* M-2267 (hmsg-b8a170c0): is the pane in a tmux mode (a person left it in copy-mode)? Logged once per stretch. Unknown
|
|
9910
|
+
* (unreadable) is not a mode: the delivery's own reads decide then, as before.
|
|
9911
|
+
*/
|
|
9912
|
+
paneModeSince = 0;
|
|
9913
|
+
async paneInTmuxMode(phase) {
|
|
9914
|
+
const inMode = await this.tmux?.paneInMode?.();
|
|
9915
|
+
if (inMode !== true) {
|
|
9916
|
+
this.paneModeSince = 0;
|
|
9917
|
+
return false;
|
|
9918
|
+
}
|
|
9919
|
+
if (!this.paneModeSince) {
|
|
9920
|
+
this.paneModeSince = performance.now();
|
|
9921
|
+
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");
|
|
9922
|
+
}
|
|
9923
|
+
return true;
|
|
9924
|
+
}
|
|
9925
|
+
/** M-2267: wait while the pane is in a tmux mode, until `deadline` (PANE_MODE_ENTER_WAIT_MS) — "clear", "stale" (fenced), "held". */
|
|
9926
|
+
async waitWhilePaneInMode(phase, current, deadline = performance.now() + PANE_MODE_ENTER_WAIT_MS) {
|
|
9927
|
+
for (;;) {
|
|
9928
|
+
const inMode = await this.paneInTmuxMode(phase);
|
|
9929
|
+
if (!current())
|
|
9930
|
+
return "stale";
|
|
9931
|
+
if (!inMode)
|
|
9932
|
+
return "clear";
|
|
9933
|
+
if (performance.now() >= deadline)
|
|
9934
|
+
return "held";
|
|
9935
|
+
await new Promise(r => setTimeout(r, PANE_MODE_POLL_MS));
|
|
9936
|
+
if (!current())
|
|
9937
|
+
return "stale";
|
|
9938
|
+
}
|
|
9570
9939
|
}
|
|
9571
9940
|
/** Write half only: callers must finish the availability wait before any final recovery ownership proof. */
|
|
9572
|
-
async sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt) {
|
|
9573
|
-
// A recovery Enter may have waited for a transient while cancel or spawn
|
|
9574
|
-
//
|
|
9575
|
-
|
|
9941
|
+
async sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt, onInputHeld) {
|
|
9942
|
+
// A recovery Enter may have waited for a transient while cancel or spawn replaced the delivery: check here, and
|
|
9943
|
+
// again after every wait below, at the last point before the tmux write.
|
|
9944
|
+
// M-2290 (#2985 review, Fable): the mode wait can last up to 10 minutes, and a respawn or relaunch reuses the same
|
|
9945
|
+
// tmux object and window — so the spawn generation and launch fence are part of the fence too: an Enter meant for
|
|
9946
|
+
// the CLI this paste went to is never sent to its successor.
|
|
9947
|
+
const tmux = this.tmux;
|
|
9948
|
+
const generation = this.spawnGeneration;
|
|
9949
|
+
const fence = this.launchFenceEpoch;
|
|
9950
|
+
const current = () => this.tmux === tmux && this.spawnGeneration === generation && this.launchFenceEpoch === fence
|
|
9951
|
+
&& !this.deliveryWritesStopping && (!stillCurrent || stillCurrent());
|
|
9952
|
+
if (!current())
|
|
9576
9953
|
return false;
|
|
9954
|
+
// The pasted text may still be in the box: the caller keeps it uncertain (no false failure, no second paste).
|
|
9955
|
+
const hold = () => {
|
|
9956
|
+
onInputHeld?.("pane-in-mode");
|
|
9957
|
+
this.logger.warn({ phase }, "The pane stayed in a tmux mode; Enter withheld");
|
|
9958
|
+
return false;
|
|
9959
|
+
};
|
|
9960
|
+
// M-2267 (#2971): a pane in a tmux mode (a person left it in copy-mode) would take this Enter. Wait for the mode to
|
|
9961
|
+
// end, then look once more within the same deadline — a pane flickering back into a mode is waited out again, never
|
|
9962
|
+
// past the bound. (beta.5: #2958's composer proof that used to sit between the two looks is reverted, decision
|
|
9963
|
+
// f57474aa: it rejected Codex's footer after a resume and while busy. #2957 is open again.)
|
|
9964
|
+
const modeDeadline = performance.now() + PANE_MODE_ENTER_WAIT_MS;
|
|
9965
|
+
for (;;) {
|
|
9966
|
+
const mode = await this.waitWhilePaneInMode(phase, current, modeDeadline);
|
|
9967
|
+
if (mode === "stale")
|
|
9968
|
+
return false;
|
|
9969
|
+
if (mode === "held")
|
|
9970
|
+
return hold();
|
|
9971
|
+
if (await this.paneInTmuxMode(phase)) {
|
|
9972
|
+
if (!current())
|
|
9973
|
+
return false;
|
|
9974
|
+
if (performance.now() >= modeDeadline)
|
|
9975
|
+
return hold();
|
|
9976
|
+
await new Promise(r => setTimeout(r, PANE_MODE_POLL_MS));
|
|
9977
|
+
if (!current())
|
|
9978
|
+
return false;
|
|
9979
|
+
continue;
|
|
9980
|
+
}
|
|
9981
|
+
if (!current())
|
|
9982
|
+
return false;
|
|
9983
|
+
break;
|
|
9984
|
+
}
|
|
9985
|
+
// M-2290 (the simple half of the #2957 redesign, leader xmsg-1793712561468): a dialog the backend RECOGNISES
|
|
9986
|
+
// (runtimeDialogs: a permission card, the dangerous-command card, …) that painted between the paste and this Enter
|
|
9987
|
+
// would take the key as its answer. Read the pane once more: such a dialog withholds the Enter, the caller keeps the
|
|
9988
|
+
// delivery uncertain (Needs-you), and the dialog scanner parks the dialog for a person. Nothing else is judged here
|
|
9989
|
+
// — an unknown or unreadable screen sends, exactly as before: #2958's composer proof (decision f57474aa) is NOT back.
|
|
9990
|
+
// Only a caller that can hold (onInputHeld: the delivery's initial submit, settled uncertain → Needs-you) asks: one
|
|
9991
|
+
// that cannot would read `false` as a failure and paste again. Only the backend's OWN dialogs count — not AgEnD's
|
|
9992
|
+
// unrecognised-prompt guess — so an unknown screen still sends.
|
|
9993
|
+
if (onInputHeld) {
|
|
9994
|
+
let finalPane = null;
|
|
9995
|
+
try {
|
|
9996
|
+
finalPane = await tmux.capturePane();
|
|
9997
|
+
}
|
|
9998
|
+
catch {
|
|
9999
|
+
finalPane = null;
|
|
10000
|
+
}
|
|
10001
|
+
if (!current())
|
|
10002
|
+
return false;
|
|
10003
|
+
let dialog;
|
|
10004
|
+
try {
|
|
10005
|
+
dialog = finalPane === null ? undefined : (this.backend?.getRuntimeDialogs?.() ?? []).find(d => Daemon.dialogMatches(d, finalPane))?.description;
|
|
10006
|
+
}
|
|
10007
|
+
catch {
|
|
10008
|
+
dialog = undefined;
|
|
10009
|
+
}
|
|
10010
|
+
if (dialog !== undefined) {
|
|
10011
|
+
onInputHeld?.("dialog");
|
|
10012
|
+
this.logger.warn({ phase, dialog }, "A dialog is on the pane; the delivery's Enter is withheld (it would answer it)");
|
|
10013
|
+
return false;
|
|
10014
|
+
}
|
|
10015
|
+
}
|
|
9577
10016
|
// W1: persist the "Enter may have happened" boundary before tmux receives
|
|
9578
10017
|
// the key. A crash after this commit is never auto-replayed without a
|
|
9579
10018
|
// positive transcript marker, even if the stale pane still shows composer.
|
|
@@ -9581,7 +10020,7 @@ export class Daemon extends EventEmitter {
|
|
|
9581
10020
|
this.logger.error({ phase, deliveryId: durableAttempt.deliveryId }, "Could not commit durable Enter write-ahead fence");
|
|
9582
10021
|
return false;
|
|
9583
10022
|
}
|
|
9584
|
-
const sent = await
|
|
10023
|
+
const sent = await tmux.sendSpecialKey("Enter");
|
|
9585
10024
|
if (!sent) {
|
|
9586
10025
|
this.logger.error({
|
|
9587
10026
|
phase,
|
|
@@ -9641,6 +10080,10 @@ export class Daemon extends EventEmitter {
|
|
|
9641
10080
|
verdict.fenced = !verdict.durableBeginCommitted;
|
|
9642
10081
|
return false;
|
|
9643
10082
|
}
|
|
10083
|
+
// M-2267: the pane went into a tmux mode after the readiness wait. Nothing begun or written: the caller waits for
|
|
10084
|
+
// readiness (which holds while it lasts) and tries again — as for a dialog that appeared before the write.
|
|
10085
|
+
if (!verdict.durableBeginCommitted && await this.paneInTmuxMode("paste"))
|
|
10086
|
+
return "pane-in-mode";
|
|
9644
10087
|
// A hand-off pastes and presses Enter into a busy pane: the box must still be readable on this, the last capture
|
|
9645
10088
|
// before the write (#1169 review) — a modal that appeared after the under-lock check would take that Enter. Before
|
|
9646
10089
|
// the durable begin the caller waits for readiness outside the lock and tries again; after it (a paste retry),
|
|
@@ -9704,6 +10147,17 @@ export class Daemon extends EventEmitter {
|
|
|
9704
10147
|
}
|
|
9705
10148
|
}
|
|
9706
10149
|
}
|
|
10150
|
+
// #3030: a hand-off into a busy CLI's own queue is on record before its paste (write-ahead): that queue lives only
|
|
10151
|
+
// in the CLI process, and a stop before a turn takes it would lose it without a trace.
|
|
10152
|
+
if (verdict.trackHandoff && !rawPaste) {
|
|
10153
|
+
await this.recordQueuedHandoff(formatted, handingOffToNativeQueue, verdict, durableAttempt, humanVerificationId);
|
|
10154
|
+
if (stillCurrent && !stillCurrent()) {
|
|
10155
|
+
if (!verdict.trackHandoff.id)
|
|
10156
|
+
this.dropQueuedHandoff(verdict);
|
|
10157
|
+
verdict.fenced = !verdict.durableBeginCommitted;
|
|
10158
|
+
return false;
|
|
10159
|
+
}
|
|
10160
|
+
}
|
|
9707
10161
|
if (lookBeforePaste) {
|
|
9708
10162
|
await lookBeforePaste();
|
|
9709
10163
|
// The look awaited: this attempt is still this write's — the same fence, tmux and launch — or nothing is written.
|
|
@@ -9737,6 +10191,22 @@ export class Daemon extends EventEmitter {
|
|
|
9737
10191
|
// failed/throwing write as uncertain rather than retrying bytes into the
|
|
9738
10192
|
// same pane; the write-ahead Enter marker later separates safe pre-Enter
|
|
9739
10193
|
// recovery from a possibly executed command.
|
|
10194
|
+
// M-2267 (#2971 review, Prism P2): EVERY paste attempt — a retry after a failed paste, a first paste whose durable
|
|
10195
|
+
// begin already went — asks too. Before the begin: nothing begun, the caller waits for readiness and tries again.
|
|
10196
|
+
// After it: this attempt waits here, keeping its durable attempt; a mode outlasting the wait fails it unwritten.
|
|
10197
|
+
if (!verdict.durableBeginCommitted) {
|
|
10198
|
+
if (await this.paneInTmuxMode("paste"))
|
|
10199
|
+
return "pane-in-mode";
|
|
10200
|
+
}
|
|
10201
|
+
else {
|
|
10202
|
+
const mode = await this.waitWhilePaneInMode("paste", () => (!stillCurrent || stillCurrent()) && !this.deliveryWritesStopping);
|
|
10203
|
+
if (mode === "stale") {
|
|
10204
|
+
verdict.fenced = false;
|
|
10205
|
+
return false;
|
|
10206
|
+
}
|
|
10207
|
+
if (mode === "held")
|
|
10208
|
+
return this.failDelivery(verdict, status, "paste", "pane-in-tmux-mode");
|
|
10209
|
+
}
|
|
9740
10210
|
const pasteGeneration = this.spawnGeneration;
|
|
9741
10211
|
beforePaste?.();
|
|
9742
10212
|
const pasted = await this.tmux.pasteBuffer(formatted);
|
|
@@ -9825,7 +10295,15 @@ export class Daemon extends EventEmitter {
|
|
|
9825
10295
|
let enterAt = Date.now();
|
|
9826
10296
|
const enterMono = performance.now();
|
|
9827
10297
|
const steerEnterCurrent = () => steerCurrent() && performance.now() <= steerEnterDeadline;
|
|
9828
|
-
|
|
10298
|
+
let inputHeld = null;
|
|
10299
|
+
if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof ? steerEnterCurrent : undefined, durableAttempt, (why) => { inputHeld = why; }))) {
|
|
10300
|
+
if (inputHeld) {
|
|
10301
|
+
// #2971: held in a tmux mode past the bound; M-2290: a recognised dialog on the pane. The paste may still be in
|
|
10302
|
+
// the CLI: keep it uncertain (Needs-you), never re-paste, never answer the dialog.
|
|
10303
|
+
verdict.phase = "submit-enter";
|
|
10304
|
+
verdict.proof = inputHeld === "dialog" ? "dialog-on-pane" : "pane-in-mode-held";
|
|
10305
|
+
return false;
|
|
10306
|
+
}
|
|
9829
10307
|
if (steerComposerProof && !steerCurrent()) {
|
|
9830
10308
|
verdict.phase = "submit-enter";
|
|
9831
10309
|
verdict.proof = "fenced-before-enter";
|
|
@@ -9916,8 +10394,9 @@ export class Daemon extends EventEmitter {
|
|
|
9916
10394
|
}
|
|
9917
10395
|
const proof = await this.confirmSubmitted(signature, pasteBaseline);
|
|
9918
10396
|
if (proof === "submitted") {
|
|
9919
|
-
|
|
9920
|
-
|
|
10397
|
+
// ✅ native queue accepted — unless it is on record (#3030): then ✅ waits for the turn that takes it.
|
|
10398
|
+
if (status && !verdict.handoff)
|
|
10399
|
+
this.emit("message_confirmed", status);
|
|
9921
10400
|
return true;
|
|
9922
10401
|
}
|
|
9923
10402
|
if (proof === "unverifiable") {
|
|
@@ -9949,8 +10428,8 @@ export class Daemon extends EventEmitter {
|
|
|
9949
10428
|
this.logger.warn("Paste reached the pane but could not be verified as submitted — accepting without proof");
|
|
9950
10429
|
verdict.phase = "best-effort-submission";
|
|
9951
10430
|
verdict.proof = "unverified";
|
|
9952
|
-
if (status)
|
|
9953
|
-
this.emit("message_confirmed", status); // ✅ (best-effort)
|
|
10431
|
+
if (status && !verdict.handoff)
|
|
10432
|
+
this.emit("message_confirmed", status); // ✅ (best-effort; on record: when taken, #3030)
|
|
9954
10433
|
return true;
|
|
9955
10434
|
}
|
|
9956
10435
|
// raw_paste has no unique marker and is not allowed to re-paste or
|
|
@@ -9965,8 +10444,14 @@ export class Daemon extends EventEmitter {
|
|
|
9965
10444
|
// confirmation ladder. "stranded" is the case that used to be a silent
|
|
9966
10445
|
// ✅ — the text was visible, so the old check called it delivered.
|
|
9967
10446
|
this.logger.warn({ proof }, "Native-queue paste not confirmed as submitted — retrying via idle-gated delivery");
|
|
9968
|
-
|
|
9969
|
-
|
|
10447
|
+
// #3030: what follows is an idle submission (or a failure the caller reports): no longer a queued hand-off.
|
|
10448
|
+
if (verdict.handoff && !verdict.trackHandoff?.id)
|
|
10449
|
+
this.dropQueuedHandoff(verdict);
|
|
10450
|
+
verdict.handoff = undefined;
|
|
10451
|
+
if (windowId && this.controlClient && !(await this.controlClient.waitUntilIdle(windowId))) {
|
|
10452
|
+
verdict.phase = "native-queue-idle-wait";
|
|
10453
|
+
verdict.proof = "idle-wait-failed";
|
|
10454
|
+
return false;
|
|
9970
10455
|
}
|
|
9971
10456
|
// Re-read before doing anything: the CLI had a whole turn to flush its
|
|
9972
10457
|
// queue while we waited, and the right recovery differs per outcome.
|
|
@@ -12361,7 +12846,12 @@ export class Daemon extends EventEmitter {
|
|
|
12361
12846
|
|| this.startupAborted || this.deliveryWritesStopping || this.runtimeMonitorsFrozen || this.healthCheckPaused)) {
|
|
12362
12847
|
throw new Error("OOM recovery cancelled: the instance was stopped, paused or replaced while memory was unavailable");
|
|
12363
12848
|
}
|
|
12364
|
-
return this.trySpawnInsideGate(reuseWindow, startupTimeoutMs)
|
|
12849
|
+
return this.trySpawnInsideGate(reuseWindow, startupTimeoutMs).catch(err => {
|
|
12850
|
+
// Includes an in-place binary replacement during asynchronous launch preparation.
|
|
12851
|
+
if (err instanceof KiroCompatibilityError)
|
|
12852
|
+
this.emit("kiro_compatibility_hold", err.message);
|
|
12853
|
+
throw err;
|
|
12854
|
+
});
|
|
12365
12855
|
};
|
|
12366
12856
|
const ready = !this.spawnGate ? await enter() : await this.spawnGate.run({
|
|
12367
12857
|
instanceName: this.name,
|
|
@@ -12584,6 +13074,11 @@ export class Daemon extends EventEmitter {
|
|
|
12584
13074
|
this.startupAdmission?.();
|
|
12585
13075
|
const launchFence = this.launchFenceEpoch;
|
|
12586
13076
|
const backendConfig = this.buildBackendConfig();
|
|
13077
|
+
// A compatibility refusal is not a failed resume: no configuration, pin, history or pane may change.
|
|
13078
|
+
if (this.backend?.assertLaunchSupported) {
|
|
13079
|
+
this.backend.assertLaunchSupported(backendConfig);
|
|
13080
|
+
this.emit("kiro_compatibility_hold", null);
|
|
13081
|
+
}
|
|
12587
13082
|
// #1633: decided before prev-instructions is first written — its absence is what says "never launched before".
|
|
12588
13083
|
this.prepareWhatsNew(!existsSync(join(this.instanceDir, "prev-instructions")));
|
|
12589
13084
|
// Record what this launch writes. #2787: a change is never announced in the pane; the CLI takes the new
|
|
@@ -12620,6 +13115,7 @@ export class Daemon extends EventEmitter {
|
|
|
12620
13115
|
const agentTokenPath = join(this.instanceDir, "agent.token");
|
|
12621
13116
|
const agentToken = randomBytes(32).toString("hex");
|
|
12622
13117
|
const tokenWrite = writeSecretFile(agentTokenPath, agentToken);
|
|
13118
|
+
this.agentCredentialIssuer?.(agentToken);
|
|
12623
13119
|
if (!tokenWrite.ok) {
|
|
12624
13120
|
// Do not fail the spawn over it — an instance that cannot start is worse
|
|
12625
13121
|
// than one whose token is readable. But say so, loudly: the whole point of
|
|
@@ -12890,7 +13386,7 @@ export class Daemon extends EventEmitter {
|
|
|
12890
13386
|
// ones included: if the auto-dismiss keeps failing here, the human
|
|
12891
13387
|
// report should count from first sight, not from the runtime scan.
|
|
12892
13388
|
if (!dialog.fatal && this.deliveryBlockingDialogs().some(d => Daemon.dialogKey(d) === Daemon.dialogKey(dialog))) {
|
|
12893
|
-
this.trackDialogParked(dialog);
|
|
13389
|
+
this.trackDialogParked(dialog, pane);
|
|
12894
13390
|
}
|
|
12895
13391
|
// A fatal startup screen cannot be dismissed by keypresses (e.g. a
|
|
12896
13392
|
// corrupt claude.json modal offering only "exit" / "reset config").
|
|
@@ -12920,7 +13416,7 @@ export class Daemon extends EventEmitter {
|
|
|
12920
13416
|
this.heldReasonLoggedKey = Daemon.dialogKey(dialog);
|
|
12921
13417
|
this.logger.warn({ description: dialog.description, ...reason }, "Startup dialog held — not auto-answering, deliveries stay blocked");
|
|
12922
13418
|
}
|
|
12923
|
-
this.trackDialogParked(dialog);
|
|
13419
|
+
this.trackDialogParked(dialog, pane);
|
|
12924
13420
|
matched = true;
|
|
12925
13421
|
break;
|
|
12926
13422
|
}
|