@songsid/agend 2.3.2 → 2.3.3-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/dist/agent-cli-instructions.md +2 -2
- package/dist/agent-cli.js +13 -112
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +0 -1
- package/dist/agent-endpoint.js +1 -9
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.d.ts +23 -5
- package/dist/agent-whats-new.js +58 -40
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.d.ts +0 -7
- package/dist/auth-api.js +1 -15
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/authority-state.js +1 -1
- package/dist/authority/authority-state.js.map +1 -1
- package/dist/authority/web-authority.d.ts +9 -4
- package/dist/authority/web-authority.js +10 -5
- package/dist/authority/web-authority.js.map +1 -1
- package/dist/backend/antigravity.js +6 -1
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +20 -1
- package/dist/backend/claude-code.js +54 -29
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/claude-transcript-errors.d.ts +18 -0
- package/dist/backend/claude-transcript-errors.js +19 -0
- package/dist/backend/claude-transcript-errors.js.map +1 -0
- package/dist/backend/codex-session-lookup.d.ts +16 -3
- package/dist/backend/codex-session-lookup.js +45 -11
- package/dist/backend/codex-session-lookup.js.map +1 -1
- package/dist/backend/codex.d.ts +16 -0
- package/dist/backend/codex.js +184 -35
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok.js +58 -12
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro-reconcile-store.js +8 -1
- package/dist/backend/kiro-reconcile-store.js.map +1 -1
- package/dist/backend/kiro.d.ts +0 -5
- package/dist/backend/kiro.js +2 -10
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.js +2 -1
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +53 -4
- package/dist/backend/types.js.map +1 -1
- package/dist/bundled-skills.d.ts +4 -6
- package/dist/bundled-skills.js +10 -14
- package/dist/bundled-skills.js.map +1 -1
- package/dist/channel/adapters/discord.js +60 -17
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-server.js +5 -5
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.d.ts +1 -3
- package/dist/channel/mcp-tools.js +4 -58
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/classic-channel-manager.d.ts +4 -1
- package/dist/classic-channel-manager.js +21 -7
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +23 -6
- package/dist/cli.js.map +1 -1
- package/dist/config-history.d.ts +50 -0
- package/dist/config-history.js +263 -0
- package/dist/config-history.js.map +1 -0
- package/dist/config-validator.js +10 -8
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.js +11 -4
- package/dist/config.js.map +1 -1
- package/dist/cross-instance-envelope.d.ts +10 -5
- package/dist/cross-instance-envelope.js +20 -17
- package/dist/cross-instance-envelope.js.map +1 -1
- package/dist/daemon.d.ts +130 -64
- package/dist/daemon.js +603 -428
- package/dist/daemon.js.map +1 -1
- package/dist/daily-summary.d.ts +8 -1
- package/dist/daily-summary.js +8 -5
- package/dist/daily-summary.js.map +1 -1
- package/dist/dashboard-live.d.ts +1 -1
- package/dist/dashboard-live.js +3 -3
- package/dist/dashboard-live.js.map +1 -1
- package/dist/dashboard-permissions.d.ts +1 -1
- package/dist/dashboard-permissions.js +1 -3
- package/dist/dashboard-permissions.js.map +1 -1
- package/dist/delivery-outbox.d.ts +31 -22
- package/dist/delivery-outbox.js +119 -161
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-outbox.test.js +10 -25
- package/dist/delivery-outbox.test.js.map +1 -1
- package/dist/doctor.d.ts +4 -0
- package/dist/doctor.js +16 -1
- package/dist/doctor.js.map +1 -1
- package/dist/fleet-level-config.js +1 -1
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +74 -66
- package/dist/fleet-manager.js +285 -414
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-process-handlers.d.ts +5 -0
- package/dist/fleet-process-handlers.js +21 -0
- package/dist/fleet-process-handlers.js.map +1 -1
- package/dist/fleet-service-owner.d.ts +11 -0
- package/dist/fleet-service-owner.js +3 -0
- package/dist/fleet-service-owner.js.map +1 -0
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +11 -1
- package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +1 -1
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -1
- package/dist/general-knowledge/skills/session-management/SKILL.md +7 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +13 -204
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +30 -68
- package/dist/instance-lifecycle.d.ts +19 -0
- package/dist/instance-lifecycle.js +73 -15
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.js +6 -7
- package/dist/instructions.js.map +1 -1
- package/dist/kiro-reconcile.js +3 -3
- package/dist/kiro-reconcile.js.map +1 -1
- package/dist/locale.js +96 -58
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +1 -1
- package/dist/log-service.js.map +1 -1
- package/dist/needs-you-hub.d.ts +1 -0
- package/dist/needs-you-hub.js +6 -3
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you-live.d.ts +3 -1
- package/dist/needs-you-live.js +24 -2
- package/dist/needs-you-live.js.map +1 -1
- package/dist/needs-you.d.ts +13 -2
- package/dist/needs-you.js +11 -1
- package/dist/needs-you.js.map +1 -1
- package/dist/notify-socket.d.ts +10 -0
- package/dist/notify-socket.js +45 -0
- package/dist/notify-socket.js.map +1 -0
- package/dist/outbound-handlers.js +20 -8
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +0 -62
- package/dist/outbound-schemas.js +4 -43
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/parked-notices.d.ts +87 -0
- package/dist/parked-notices.js +194 -0
- package/dist/parked-notices.js.map +1 -0
- package/dist/public-link-progress.d.ts +5 -2
- package/dist/public-link-progress.js +4 -3
- package/dist/public-link-progress.js.map +1 -1
- package/dist/public-web-gateway.js +0 -9
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +12 -0
- package/dist/public-web-link.js +29 -5
- package/dist/public-web-link.js.map +1 -1
- package/dist/quota-notice.d.ts +23 -0
- package/dist/quota-notice.js +22 -0
- package/dist/quota-notice.js.map +1 -0
- package/dist/scheduler/task-board.d.ts +17 -25
- package/dist/scheduler/task-board.js +29 -39
- package/dist/scheduler/task-board.js.map +1 -1
- package/dist/sd-notify.d.ts +23 -15
- package/dist/sd-notify.js +88 -29
- package/dist/sd-notify.js.map +1 -1
- package/dist/search/search-docs.d.ts +1 -1
- package/dist/search/search-docs.js +2 -2
- package/dist/search/search-docs.js.map +1 -1
- package/dist/settings-api.d.ts +16 -1
- package/dist/settings-api.js +56 -16
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +3 -1
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-confirmation.d.ts +17 -3
- package/dist/settings-confirmation.js +8 -5
- package/dist/settings-confirmation.js.map +1 -1
- package/dist/settings-effect.js +0 -2
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +31 -1
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/setup-wizard.d.ts +3 -0
- package/dist/setup-wizard.js +4 -2
- package/dist/setup-wizard.js.map +1 -1
- package/dist/task-board-waker.d.ts +43 -1
- package/dist/task-board-waker.js +121 -9
- package/dist/task-board-waker.js.map +1 -1
- package/dist/tmux-control.d.ts +1 -1
- package/dist/tmux-control.js +3 -2
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-manager.js +18 -16
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tmux-read.d.ts +2 -0
- package/dist/tmux-read.js +7 -1
- package/dist/tmux-read.js.map +1 -1
- package/dist/tool-permissions.js +3 -10
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +29 -7
- package/dist/topic-commands.js +97 -37
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.d.ts +7 -0
- package/dist/transcript-monitor.js +48 -3
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/tunnel/cloudflared.d.ts +5 -0
- package/dist/tunnel/cloudflared.js +32 -1
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/tunnel/named-diagnostics.d.ts +2 -1
- package/dist/tunnel/named-diagnostics.js +4 -1
- package/dist/tunnel/named-diagnostics.js.map +1 -1
- package/dist/tunnel/named-link-test.d.ts +90 -0
- package/dist/tunnel/named-link-test.js +270 -0
- package/dist/tunnel/named-link-test.js.map +1 -0
- package/dist/tunnel/purpose-lane.d.ts +3 -1
- package/dist/tunnel/purpose-lane.js +2 -0
- package/dist/tunnel/purpose-lane.js.map +1 -1
- package/dist/types.d.ts +9 -11
- package/dist/ui/chat-ambient.js +4 -4
- package/dist/ui/chat-commands.js +3 -2
- package/dist/ui/chat-store.js +4 -1
- package/dist/ui/chat-strings.js +12 -46
- package/dist/ui/chat-thread.js +51 -59
- package/dist/ui/fleet-office-skin.js +37 -14
- package/dist/ui/fleet-office.js +31 -7
- package/dist/ui/html-attachment.js +22 -8
- package/dist/ui/panel-chat.js +21 -50
- package/dist/ui/panel-log.js +2 -2
- package/dist/ui/panel-needs.js +11 -9
- package/dist/ui/panel-settings.js +5 -3
- package/dist/ui/preview-panel.js +12 -21
- package/dist/ui/preview.js +10 -3
- package/dist/ui/search-dialog.js +30 -14
- package/dist/ui/settings-appearance.js +3 -37
- package/dist/ui/settings-confirm.js +18 -0
- package/dist/ui/settings-guests.js +13 -4
- package/dist/ui/settings-learn.js +1 -1
- package/dist/ui/settings-mods.js +5 -12
- package/dist/ui/settings-named-test.js +144 -0
- package/dist/ui/settings-registry.js +0 -2
- package/dist/ui/settings-strings.js +16 -16
- package/dist/ui/settings-web-authority.js +4 -3
- package/dist/ui/shared/app-i18n.js +15 -4
- package/dist/ui/shared/app-needs.js +5 -1
- package/dist/ui/shared/app-prefs.js +17 -17
- package/dist/ui/shared/app-shell.js +47 -9
- package/dist/ui/shared/app-store.js +6 -2
- package/dist/ui/shared/app.css +33 -39
- package/dist/ui/shared/app.js +5 -4
- package/dist/ui/shared/instance-nav.js +9 -9
- package/dist/ui/shared/instance-switch.js +8 -2
- package/dist/ui/shared/panel-view.js +22 -8
- package/dist/ui/shared/view-strings.js +6 -0
- package/dist/ui/shared/web-authorize.js +35 -13
- package/dist/update-check.d.ts +13 -4
- package/dist/update-check.js +17 -4
- package/dist/update-check.js.map +1 -1
- package/dist/usage/usage-api.d.ts +3 -0
- package/dist/usage/usage-api.js +5 -1
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/view-api.d.ts +2 -0
- package/dist/view-api.js +64 -0
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +9 -17
- package/dist/web-api.js +257 -371
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-tools.d.ts +0 -2
- package/dist/web-appearance-tools.js +2 -5
- package/dist/web-appearance-tools.js.map +1 -1
- package/dist/web-appearance-view.d.ts +0 -6
- package/dist/web-appearance-view.js +0 -1
- package/dist/web-appearance-view.js.map +1 -1
- package/dist/web-appearance.d.ts +14 -7
- package/dist/web-appearance.js +5 -6
- package/dist/web-appearance.js.map +1 -1
- package/dist/web-asset-parts.d.ts +1 -17
- package/dist/web-asset-parts.js +1 -15
- package/dist/web-asset-parts.js.map +1 -1
- package/dist/web-auth.js +1 -7
- package/dist/web-auth.js.map +1 -1
- package/dist/web-chat-history.d.ts +3 -7
- package/dist/web-chat-history.js +10 -17
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +9 -1
- package/dist/web-commands.js +13 -0
- package/dist/web-commands.js.map +1 -1
- package/dist/web-experimental.d.ts +3 -8
- package/dist/web-experimental.js +9 -33
- package/dist/web-experimental.js.map +1 -1
- package/dist/web-guest-access.d.ts +40 -2
- package/dist/web-guest-access.js +50 -14
- package/dist/web-guest-access.js.map +1 -1
- package/dist/web-mod-bundle.d.ts +2 -45
- package/dist/web-mod-bundle.js +15 -63
- package/dist/web-mod-bundle.js.map +1 -1
- package/dist/web-mod-store.d.ts +3 -48
- package/dist/web-mod-store.js +30 -97
- package/dist/web-mod-store.js.map +1 -1
- package/dist/web-prefs.d.ts +13 -0
- package/dist/web-prefs.js +38 -0
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +6 -13
- package/dist/web-preview.js +12 -23
- package/dist/web-preview.js.map +1 -1
- package/dist/web-session.d.ts +11 -3
- package/dist/web-session.js +22 -11
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +1 -1
- package/dist/web-settings-patch.js +9 -7
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-settings-tools.d.ts +0 -13
- package/dist/web-settings-tools.js +12 -84
- package/dist/web-settings-tools.js.map +1 -1
- package/dist/workflow-templates/default.md +2 -2
- package/dist/working-recovery.d.ts +0 -2
- package/dist/working-recovery.js +2 -23
- package/dist/working-recovery.js.map +1 -1
- package/launcher/agend-install.cjs +7 -1
- package/launcher/fleet-service-owner.cjs +150 -0
- package/package.json +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +0 -376
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +0 -673
- package/dist/task-board-summary.d.ts +0 -101
- package/dist/task-board-summary.js +0 -177
- package/dist/task-board-summary.js.map +0 -1
- package/dist/ui/app-effects.js +0 -251
- package/dist/ui/chat-effect-posters.js +0 -42
- package/dist/ui/chat-effects.js +0 -167
- package/dist/ui/effect-trial-controller.js +0 -124
- package/dist/ui/effect-trial-offer.js +0 -2
- package/dist/ui/effect-trial-panel.js +0 -40
- package/dist/ui/effects-waker.js +0 -2
- package/dist/ui/settings-effects.js +0 -202
- package/dist/ui/settings-owner-effects.js +0 -82
- package/dist/ui/shared/effect-app-runtime.js +0 -299
- package/dist/ui/shared/effect-baseline.js +0 -64
- package/dist/ui/shared/effect-chat-runtime.js +0 -162
- package/dist/ui/shared/effect-cursor.js +0 -126
- package/dist/ui/shared/effect-display-metadata.js +0 -39
- package/dist/ui/shared/effect-frame.js +0 -146
- package/dist/ui/shared/effect-geometry.js +0 -137
- package/dist/ui/shared/effect-host-style.js +0 -10
- package/dist/ui/shared/effect-owner-regions.js +0 -74
- package/dist/ui/shared/effect-owner-source.js +0 -122
- package/dist/ui/shared/effect-policy.js +0 -151
- package/dist/ui/shared/effect-poster-reader.js +0 -98
- package/dist/ui/shared/effect-posters.js +0 -117
- package/dist/ui/shared/effect-profiler.js +0 -184
- package/dist/ui/shared/effect-profiles.js +0 -65
- package/dist/ui/shared/effect-registry.js +0 -545
- package/dist/ui/shared/effect-runtime-loader.js +0 -236
- package/dist/ui/shared/effect-safety.js +0 -153
- package/dist/ui/shared/effect-stock-snapshot.js +0 -43
- package/dist/ui/shared/effect-stock-trust.js +0 -217
- package/dist/ui/shared/effect-trial-shadow.js +0 -62
- package/dist/ui/shared/effect-trial-source.js +0 -103
- package/dist/ui/shared/effect-trial-stage.js +0 -170
- package/dist/ui/shared/effect-trial.css +0 -40
- package/dist/ui/shared/feature-flags.js +0 -127
- package/dist/ui/shared/owner-effect-prefs.js +0 -117
- package/dist/upgrade-notices.d.ts +0 -17
- package/dist/upgrade-notices.js +0 -93
- package/dist/upgrade-notices.js.map +0 -1
- package/dist/web-effect-bootstrap.d.ts +0 -12
- package/dist/web-effect-bootstrap.js +0 -49
- package/dist/web-effect-bootstrap.js.map +0 -1
- package/dist/web-effect-bundle.d.ts +0 -28
- package/dist/web-effect-bundle.js +0 -171
- package/dist/web-effect-bundle.js.map +0 -1
- package/dist/web-effect-canonical.d.ts +0 -8
- package/dist/web-effect-canonical.js +0 -27
- package/dist/web-effect-canonical.js.map +0 -1
- package/dist/web-effect-library.d.ts +0 -114
- package/dist/web-effect-library.js +0 -530
- package/dist/web-effect-library.js.map +0 -1
- package/dist/web-effect-playback.d.ts +0 -30
- package/dist/web-effect-playback.js +0 -71
- package/dist/web-effect-playback.js.map +0 -1
- package/dist/web-effect-poster.d.ts +0 -8
- package/dist/web-effect-poster.js +0 -52
- package/dist/web-effect-poster.js.map +0 -1
- package/dist/web-effect-profiles.d.ts +0 -118
- package/dist/web-effect-profiles.js +0 -181
- package/dist/web-effect-profiles.js.map +0 -1
- package/dist/web-effect-service.d.ts +0 -93
- package/dist/web-effect-service.js +0 -335
- package/dist/web-effect-service.js.map +0 -1
- package/dist/web-effect-tools.d.ts +0 -48
- package/dist/web-effect-tools.js +0 -129
- package/dist/web-effect-tools.js.map +0 -1
- package/dist/web-effect-trials.d.ts +0 -86
- package/dist/web-effect-trials.js +0 -204
- package/dist/web-effect-trials.js.map +0 -1
- package/dist/web-effect-upload.d.ts +0 -27
- package/dist/web-effect-upload.js +0 -185
- package/dist/web-effect-upload.js.map +0 -1
- package/dist/web-effect-worker.d.ts +0 -8
- package/dist/web-effect-worker.js +0 -170
- package/dist/web-effect-worker.js.map +0 -1
- package/dist/web-effect.d.ts +0 -103
- package/dist/web-effect.js +0 -153
- package/dist/web-effect.js.map +0 -1
- package/dist/web-experimental-http.d.ts +0 -2
- package/dist/web-experimental-http.js +0 -12
- package/dist/web-experimental-http.js.map +0 -1
- package/dist/web-mod-agent.d.ts +0 -50
- package/dist/web-mod-agent.js +0 -207
- package/dist/web-mod-agent.js.map +0 -1
package/dist/daemon.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readActiveKiroModel } from "./backend/kiro.js";
|
|
2
2
|
import { dashboardAccessOf } from "./dashboard-permissions.js";
|
|
3
3
|
import { sendPackSources } from "./web-office-pack-read.js";
|
|
4
|
-
import {
|
|
4
|
+
import { agendVersion, planWhatsNew, readWhatsNewRecord, recordWhatsNew, withWhatsNew } from "./agent-whats-new.js";
|
|
5
5
|
import { join, dirname, resolve } from "node:path";
|
|
6
6
|
import { checkoutTarget, readCheckouts, releaseCandidate, reposRoot, verifyWorktree, writeCheckouts } from "./repo-worktree-guard.js";
|
|
7
7
|
import { mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, unlinkSync, rmSync, appendFileSync, statSync, renameSync } from "node:fs";
|
|
@@ -24,13 +24,13 @@ import { TranscriptMonitor } from "./transcript-monitor.js";
|
|
|
24
24
|
import { createTranscriptSource } from "./transcript-sources.js";
|
|
25
25
|
import { TranscriptTurnLedger } from "./transcript-turns.js";
|
|
26
26
|
import { credentialProfileStoreHome, resolveCredentialProfile } from "./backend/credential-profile.js";
|
|
27
|
-
import { resolveToolSet } from "./tool-permissions.js";
|
|
28
|
-
import {
|
|
27
|
+
import { resolveToolSet, toolsFor } from "./tool-permissions.js";
|
|
28
|
+
import { TOOLS } from "./channel/mcp-tools.js";
|
|
29
|
+
import { dashboardAccessProblem, dashboardFeaturesOff, withoutFeatures } from "./dashboard-permissions.js";
|
|
29
30
|
import { getAgendHome } from "./paths.js";
|
|
30
|
-
import {
|
|
31
|
-
import { readAgentEffect, sendEffectInParts } from "./web-effect-upload.js";
|
|
31
|
+
import { scopedFleetConfigEnvironment } from "./web-experimental.js";
|
|
32
32
|
import { readAgentFile } from "./agent-file-read.js";
|
|
33
|
-
import { ASSET_UPLOAD_MAX_BYTES,
|
|
33
|
+
import { ASSET_UPLOAD_MAX_BYTES, receivePreview, sendAssetInParts, writePreview } from "./web-asset-parts.js";
|
|
34
34
|
import { LOGIN_FLOWS } from "./login-flows.js";
|
|
35
35
|
import { ProgressAccumulator, summarizeProgress } from "./tool-progress.js";
|
|
36
36
|
import { assembleSystemPrompt, resolveWorkflowText } from "./prompt-file-ref.js";
|
|
@@ -52,7 +52,9 @@ import { formatHumanInboundMessage, prepareDeliveryWire, steerInboundEnvelope, B
|
|
|
52
52
|
import { formatCrossInstanceInboundMessage, renderCrossInstanceHandoffMetadata, } from "./cross-instance-envelope.js";
|
|
53
53
|
import { agendMessageInInput, bottomRowIsReady, inputAreaText, inputShowsPastedText, pastedTextSignature, pasteLeftInInput, strandedAgendMessageInInput } from "./pane-input-residue.js";
|
|
54
54
|
import { TurnReplyGuard, formatReplyDropDetection, turnExpectsPersonReply } from "./turn-reply-guard.js";
|
|
55
|
+
import { CLAUDE_CREDIT_BALANCE_LOW, CLAUDE_MODEL_USAGE_LIMIT, CLAUDE_USAGE_LIMIT_PAUSE } from "./backend/claude-transcript-errors.js";
|
|
55
56
|
import { t } from "./locale.js";
|
|
57
|
+
import { claudeResetTime, quotaPausedLine } from "./quota-notice.js";
|
|
56
58
|
import { MuseUsageRelay, clearMuseUsageSnapshot } from "./muse-usage-relay.js";
|
|
57
59
|
import { deliveryContentDigest } from "./delivery-outbox.js";
|
|
58
60
|
import { consumedWatches } from "./delivery-consumed-watch.js";
|
|
@@ -67,8 +69,6 @@ import { claudeConfigDirFor, claudeProfileConfigDir, claudeSwitchDialog, claudeS
|
|
|
67
69
|
import { MuseBackend } from "./backend/muse.js";
|
|
68
70
|
import { InteractionObservation, INTERACTION_CONFIRM_MS, sameInteractionOwner } from "./interaction-observation.js";
|
|
69
71
|
import { unrecognisedPromptShape } from "./unrecognised-prompt.js";
|
|
70
|
-
/** #2140: deliveries that may carry an unwritten what's-new notice before it is recorded anyway. */
|
|
71
|
-
const WHATS_NEW_MAX_UNWRITTEN = 3;
|
|
72
72
|
const __filename = fileURLToPath(import.meta.url);
|
|
73
73
|
const __dirname = dirname(__filename);
|
|
74
74
|
// Tool routing sets — module-level to avoid re-creation on every handleToolCall
|
|
@@ -82,15 +82,6 @@ const WEB_THEME_IPC = Object.freeze({
|
|
|
82
82
|
set_web_widget: "fleet_set_web_widget", remove_web_widget: "fleet_remove_web_widget",
|
|
83
83
|
// #2117: its own declarative ambient.
|
|
84
84
|
set_web_ambient: "fleet_set_web_ambient", clear_web_ambient: "fleet_clear_web_ambient",
|
|
85
|
-
// r4-5b: mods — an export's file is written here, a proposal's file read here (on the agent's side).
|
|
86
|
-
export_web_mod: "fleet_export_web_mod", propose_web_mod: "fleet_propose_web_mod",
|
|
87
|
-
upload_web_effect: "fleet_upload_web_effect",
|
|
88
|
-
list_web_effects: "fleet_list_web_effects",
|
|
89
|
-
preview_web_effect: "fleet_preview_web_effect",
|
|
90
|
-
profile_web_effect: "fleet_profile_web_effect",
|
|
91
|
-
apply_web_effect: "fleet_apply_web_effect",
|
|
92
|
-
remove_web_effect: "fleet_remove_web_effect",
|
|
93
|
-
delete_web_effect: "fleet_delete_web_effect",
|
|
94
85
|
// #2578: its own character in the Office view — the args as they are (the fleet checks them).
|
|
95
86
|
office_view: "fleet_office_view", office_set: "fleet_office_set",
|
|
96
87
|
// #2692: the skin packs — check and upload read the pack here (web-office-pack-read.ts) and send its bytes in parts.
|
|
@@ -113,7 +104,6 @@ const TURN_OUTBOUND_TOOLS = new Set([
|
|
|
113
104
|
// These actions reach the human-facing channel and satisfy reply-completion:
|
|
114
105
|
// a deliberate reaction or edit is a valid response even without new text.
|
|
115
106
|
const TURN_COMPLETION_TOOLS = new Set(["reply", "react", "edit_message"]);
|
|
116
|
-
const REPLY_DROP_WARNING_COOLDOWN_MS = 5 * 60_000;
|
|
117
107
|
/**
|
|
118
108
|
* #1241: how long an idle edge must persist — with no reply and no further
|
|
119
109
|
* observed work — before the reply guard declares the turn a miss. A claude
|
|
@@ -136,50 +126,12 @@ const TRANSCRIPT_RUNNING_HOLD_MAX_MS = 30 * 60_000;
|
|
|
136
126
|
*/
|
|
137
127
|
export const TRANSCRIPT_OPEN_TURN_QUIET_MS = 15 * 60_000;
|
|
138
128
|
const REPLY_RECOVERY_PROMPT = "[system:reply-required] The previous human-facing turn ended without a successfully delivered reply. Do not redo the work. React with an emoji or use the reply tool exactly once now to send the user a concise conclusion. If no substantive answer is needed, a brief react is sufficient. If you already replied to this message, do nothing. Do not reply to this system instruction except through the react or reply tool.";
|
|
139
|
-
/** Point a resumed CLI at its one backend-native instruction source. */
|
|
140
129
|
/** The outbox evidence for a delivery the CLI's transcript shows taken (#758; #1201 absorbed; #1647 a raw paste's text). */
|
|
141
130
|
function transcriptMarkerEvidence(kind, rawPaste = false) {
|
|
142
131
|
if (rawPaste)
|
|
143
132
|
return "transcript-turn-user-entry-raw-paste-text";
|
|
144
133
|
return kind === "user" ? "transcript-marker" : kind === "absorbed" ? "transcript-marker-absorbed" : "transcript-marker-queued";
|
|
145
134
|
}
|
|
146
|
-
export function buildInstructionReloadNotice(binaryName, instanceName, instanceDir, backendSource) {
|
|
147
|
-
const source = backendSource ? backendSource : binaryName === "codex" || binaryName === "grok" || binaryName === "muse"
|
|
148
|
-
? "AGENTS.md"
|
|
149
|
-
: binaryName === "kiro-cli"
|
|
150
|
-
? `.kiro/steering/agend-${instanceName}.md`
|
|
151
|
-
: binaryName === "agy"
|
|
152
|
-
? ".agents/AGENTS.md"
|
|
153
|
-
: join(instanceDir, "fleet-instructions.md");
|
|
154
|
-
return `[system] Your AgEnD instructions have been updated. Reload only ${source}; do not scan other instruction directories. Do not reply to this message.`;
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Whether this backend has to be told in its pane that its instructions moved.
|
|
158
|
-
*
|
|
159
|
-
* A backend that re-reads them on resume already received the new ones from the
|
|
160
|
-
* resume itself, so the notice is pure interruption. Both places that can send
|
|
161
|
-
* it — the warmup paste and the deferred pendingInstructionsNotice — ask through
|
|
162
|
-
* here, so the two cannot drift apart again: the warmup path used to omit this
|
|
163
|
-
* check entirely and told claude-code to reload instructions it had just read.
|
|
164
|
-
*/
|
|
165
|
-
export function backendNeedsPaneReloadNotice(backend) {
|
|
166
|
-
return !backend?.instructionsReloadedOnResume;
|
|
167
|
-
}
|
|
168
|
-
/**
|
|
169
|
-
* What the warmup should do about changed instructions.
|
|
170
|
-
*
|
|
171
|
-
* `skip` — nothing to say, or this backend learns of the change by itself.
|
|
172
|
-
* `defer` — worth saying, but nobody is talking to the instance; say it when a
|
|
173
|
-
* real message next arrives rather than provoking an unsolicited reply.
|
|
174
|
-
* `paste` — say it now, into a pane that already has a delivery in flight.
|
|
175
|
-
*/
|
|
176
|
-
export function warmupNoticeAction(opts) {
|
|
177
|
-
if (!opts.warmupNeeded)
|
|
178
|
-
return "skip";
|
|
179
|
-
if (!backendNeedsPaneReloadNotice(opts.backend))
|
|
180
|
-
return "skip";
|
|
181
|
-
return opts.pasteQueueDepth === 0 ? "defer" : "paste";
|
|
182
|
-
}
|
|
183
135
|
export const DEFAULT_STUCK_TIMEOUT_MS = 10 * 60_000;
|
|
184
136
|
export const DEFAULT_STATE_IDLE_DEBOUNCE_MS = 2_000;
|
|
185
137
|
export const DEFAULT_STATE_SAFETY_SWEEP_MS = 60_000;
|
|
@@ -189,6 +141,14 @@ export const PERIODIC_REDRAW_PROBE_MS = 25;
|
|
|
189
141
|
const KIRO_COMPACT_PANE_POLL_MS = 1_000;
|
|
190
142
|
/** Do not turn a streaming Codex turn into one capture-pane per burst. */
|
|
191
143
|
export const STRUCTURED_IDLE_PROBE_MS = 500;
|
|
144
|
+
/** #2770 (ii): how often output may schedule a look for a layout that repaints while idle (CliBackend.periodicRedrawFrom). */
|
|
145
|
+
export const PERIODIC_LAYOUT_LOOK_MS = 5_000;
|
|
146
|
+
/** #2770: how often a delivery waiting for idle asks again whether it can be handed off instead. */
|
|
147
|
+
export const HANDOFF_RECHECK_MS = 1_000;
|
|
148
|
+
/** #2770: how often a wait for idle asks its stop condition (a cancel, a hand-off that became possible). */
|
|
149
|
+
export const WAIT_STOP_POLL_MS = 250;
|
|
150
|
+
/** #2770: human messages a cancel dropped are told in one notice, this long after the last of them. */
|
|
151
|
+
export const CANCELLED_HUMANS_NOTICE_MS = 1_000;
|
|
192
152
|
/**
|
|
193
153
|
* An instance publishing more than this many execution-state edges inside the
|
|
194
154
|
* window is flapping; its edges stop being logged one by one (#1116). One
|
|
@@ -599,6 +559,8 @@ const DIALOG_ANSWER_IGNORED_MAX = 3;
|
|
|
599
559
|
* normal pace (and a fresh count).
|
|
600
560
|
*/
|
|
601
561
|
const DIALOG_ANSWER_BACKOFF_MS = 30_000;
|
|
562
|
+
/** #2844: how long a dialog AgEnD answers itself (`answeredByAgend`) stays nobody's interaction while it is on screen. */
|
|
563
|
+
const AGEND_ANSWER_GRACE_MS = 20_000;
|
|
602
564
|
/** How many "dialog painted just before the write → wait → retry" rounds a delivery tolerates. */
|
|
603
565
|
const LATE_DIALOG_WRITE_ROUNDS = 3;
|
|
604
566
|
/** How many times a delivery redoes itself when a spawn starts between its settle wait and its pane write. */
|
|
@@ -1295,13 +1257,26 @@ export class Daemon extends EventEmitter {
|
|
|
1295
1257
|
bootSkippedResume = false;
|
|
1296
1258
|
recoveryCrashLoopFresh = false;
|
|
1297
1259
|
recoveryFreshChoice = false;
|
|
1260
|
+
/** The current boot found a crash-loop marker and is attempting resume rather than going fresh immediately. */
|
|
1261
|
+
bootCrashLoopResume = false;
|
|
1298
1262
|
startupAborted = false;
|
|
1299
1263
|
/** First time the current on-screen blocking dialog was seen (0 = none); drives the parked report. */
|
|
1300
1264
|
dialogParkedSince = 0;
|
|
1301
1265
|
/** Identity of that dialog: its pattern, not its description (two tables may describe one screen differently). */
|
|
1302
1266
|
dialogParkedKey = null;
|
|
1267
|
+
/**
|
|
1268
|
+
* #2893: the held dialog whose heldReason was last logged. Its own state, not the parked clock's: the delivery probe
|
|
1269
|
+
* starts that clock too, and must not use up the one log line. Cleared with the hold, so a later hold logs again.
|
|
1270
|
+
*/
|
|
1271
|
+
heldReasonLoggedKey = null;
|
|
1303
1272
|
dialogParkedInteractionEpisode = null;
|
|
1304
1273
|
dialogParkedReported = false;
|
|
1274
|
+
/**
|
|
1275
|
+
* #2897: a "needs a human" notice is out for a dialog, and what it lives with: "parked" — the parked clock (retired when
|
|
1276
|
+
* that dialog leaves the pane or another replaces it); "answer" — an answer episode of a dialog the parked clock does
|
|
1277
|
+
* not track (a non-blocking one, whose scans pass no blocking dialog): retired when that episode ends.
|
|
1278
|
+
*/
|
|
1279
|
+
parkedNoticeOut = null;
|
|
1305
1280
|
/** #2675: the card on screen that a person can answer from the chat — what it asks, since when, and whether it was posted. */
|
|
1306
1281
|
remoteCardShown = null;
|
|
1307
1282
|
/** #2675: when (performance.now, taken before its capture) the newest read the card tracker used was started. */
|
|
@@ -1352,7 +1327,7 @@ export class Daemon extends EventEmitter {
|
|
|
1352
1327
|
* cancels it when anything changed, and it fires at most once. The due time is on the monotonic clock.
|
|
1353
1328
|
*/
|
|
1354
1329
|
/**
|
|
1355
|
-
* How many occurrences of each `nudge_continue` pattern were already on screen when this spawn first showed a live
|
|
1330
|
+
* How many occurrences of each `nudge_continue` (or `historyUntilLiveComposer`) pattern were already on screen when this spawn first showed a live
|
|
1356
1331
|
* composer (null until then): resumed scrollback, i.e. HISTORY. An occurrence is a new episode only beyond that — a
|
|
1357
1332
|
* resumed session whose scrollback holds an old capacity line must not be told to "keep going" for it (#949).
|
|
1358
1333
|
*/
|
|
@@ -1366,6 +1341,8 @@ export class Daemon extends EventEmitter {
|
|
|
1366
1341
|
interactionObservedOutputRevision = 0;
|
|
1367
1342
|
interactionCaptureSerial = 0;
|
|
1368
1343
|
interactionDialogKey = null;
|
|
1344
|
+
/** Since when (monotonic) the `answeredByAgend` dialog now on screen has been withheld from the interaction state. */
|
|
1345
|
+
agendAnswerGrace = null;
|
|
1369
1346
|
acceptedInstanceSnapshot = null;
|
|
1370
1347
|
/** Generation/signature fence for safety dialogs while their screen is still visible. */
|
|
1371
1348
|
autoResolvedDialogKey = null;
|
|
@@ -1398,6 +1375,10 @@ export class Daemon extends EventEmitter {
|
|
|
1398
1375
|
/** Consecutive structural idle captures for a noisy, periodically-redrawing
|
|
1399
1376
|
* pane. Two are required before a working -> idle transition. */
|
|
1400
1377
|
instanceStatePeriodicIdleConfirmations = 0;
|
|
1378
|
+
/** #2770 (ii): the latest capture showed a layout that repaints while idle (CliBackend.periodicRedrawFrom). */
|
|
1379
|
+
periodicRedrawLayout = false;
|
|
1380
|
+
/** When output last scheduled a look for that layout (monotonic ms), so a busy pane is not captured on every chunk. */
|
|
1381
|
+
periodicLayoutLookAt = -Infinity;
|
|
1401
1382
|
/** Recent execution-state edge times, for the flap log limiter (#1116). */
|
|
1402
1383
|
stateEdgeTimes = [];
|
|
1403
1384
|
stateEdgesNotLogged = 0;
|
|
@@ -1439,7 +1420,6 @@ export class Daemon extends EventEmitter {
|
|
|
1439
1420
|
/** Prevent a visible stale XML fragment from being recovered on later turns. */
|
|
1440
1421
|
lastMalformedToolCallSignature;
|
|
1441
1422
|
proxyReplySeq = 0;
|
|
1442
|
-
lastReplyDropWarningAt = 0;
|
|
1443
1423
|
/**
|
|
1444
1424
|
* #1241: an idle edge that has not yet proven the turn ended. Armed instead
|
|
1445
1425
|
* of recovering immediately; a steady idle snapshot past confirmAt promotes
|
|
@@ -1525,6 +1505,8 @@ export class Daemon extends EventEmitter {
|
|
|
1525
1505
|
authFailureUnresolved = false;
|
|
1526
1506
|
/** One login-screen auth report per spawn — the screen persists across polls. */
|
|
1527
1507
|
loginScreenReported = false;
|
|
1508
|
+
/** #2795: the auth pattern match the token-free check is still to judge; it arms nothing until confirmAuthFailure(). */
|
|
1509
|
+
unverifiedAuthMatch = null;
|
|
1528
1510
|
/**
|
|
1529
1511
|
* While the lifecycle is still starting this daemon, `pty_error` has no
|
|
1530
1512
|
* listener and `InstanceLifecycle.daemons` has no entry — so an incident the
|
|
@@ -1583,19 +1565,12 @@ export class Daemon extends EventEmitter {
|
|
|
1583
1565
|
deliveryWritesStopping = false;
|
|
1584
1566
|
/** #829: the latest pastes, by spawn generation (see ownedInputDraft). */
|
|
1585
1567
|
recentPastes = [];
|
|
1586
|
-
pendingInstructionsUpdate;
|
|
1587
|
-
pendingInstructionsNotice = false;
|
|
1588
1568
|
/** #1633: the one-time "what's new for you" notice, pasted before the next real delivery; recorded once pasted. */
|
|
1589
1569
|
pendingWhatsNew = null;
|
|
1590
|
-
/** #2140: deliveries tried for the pending what's-new notice that never reached the pane (capped: never before every message). */
|
|
1591
|
-
whatsNewUnwritten = 0;
|
|
1592
|
-
// Whether the warmup steering-reload notice should be injected after spawn.
|
|
1593
|
-
// Set in trySpawn by comparing the freshly-built instructions against the
|
|
1594
|
-
// last value the agent was told about (prev-instructions). Skipped when
|
|
1595
|
-
// unchanged so agents don't waste 10-30s re-reading identical steering.
|
|
1596
|
-
warmupNeeded = false;
|
|
1597
|
-
lastBuiltInstructions = "";
|
|
1598
1570
|
pasteQueueDepth = 0;
|
|
1571
|
+
/** #2770: a person's messages a cancel dropped before they reached the CLI, told together (noteCancelledHuman). */
|
|
1572
|
+
cancelledHumans = [];
|
|
1573
|
+
cancelledHumansTimer = null;
|
|
1599
1574
|
firstDeliveryDelay = new FirstDeliveryDelay();
|
|
1600
1575
|
/** Orders /steer pastes against each other. Deliberately NOT pasteLock:
|
|
1601
1576
|
* a steer must not queue behind the normal deliveries it exists to
|
|
@@ -1799,6 +1774,77 @@ export class Daemon extends EventEmitter {
|
|
|
1799
1774
|
// old transcript, so pre-ready scans must not lower this seeded baseline.
|
|
1800
1775
|
seededErrorCountBaselines = new Map();
|
|
1801
1776
|
lastDetectedErrorType = null;
|
|
1777
|
+
/**
|
|
1778
|
+
* #2822: Claude's per-model usage limit (`You've reached your <model> limit.`), known only from Claude's own record of
|
|
1779
|
+
* the failed call in its transcript (`isApiErrorMessage`, a 429) — never from pane text, which an agent can print. It
|
|
1780
|
+
* names no reset time. While it holds it is told once, and the reply guard stays quiet while the transcript's newest
|
|
1781
|
+
* event is that error; any later assistant output (of any turn, from a person or not) ends it. The order is Claude's
|
|
1782
|
+
* own: a 429 is recorded within a second, often before AgEnD has armed the turn, so no AgEnD-side clock is used.
|
|
1783
|
+
*/
|
|
1784
|
+
usageLimitHold = null;
|
|
1785
|
+
/** Transcript events in Claude's order (prompts, output, every API error): the newest, and the latest limit error. */
|
|
1786
|
+
transcriptEventSeq = 0;
|
|
1787
|
+
usageLimitErrorSeq = 0;
|
|
1788
|
+
static MODEL_USAGE_LIMIT = CLAUDE_MODEL_USAGE_LIMIT;
|
|
1789
|
+
/** #2822: the usage-limit hold, while it lasts — Needs you lists it. */
|
|
1790
|
+
usageHold() {
|
|
1791
|
+
return this.usageLimitHold ? { ...this.usageLimitHold } : null;
|
|
1792
|
+
}
|
|
1793
|
+
/** #2834: Claude's account is out of API credits — its own record; the same pause as the pane pattern it replaces. */
|
|
1794
|
+
static CREDIT_BALANCE_LOW = CLAUDE_CREDIT_BALANCE_LOW;
|
|
1795
|
+
/** #2834 (#816): Claude's usage-limit auto-continue note, as it records it (`system` / `informational`). */
|
|
1796
|
+
static USAGE_LIMIT_PAUSE = CLAUDE_USAGE_LIMIT_PAUSE;
|
|
1797
|
+
/** #2834 (#816): Claude paused itself until its usage window opens — the same notice the screen pattern gave. */
|
|
1798
|
+
noteClaudeInformational(text) {
|
|
1799
|
+
const match = Daemon.USAGE_LIMIT_PAUSE.exec(text.trim());
|
|
1800
|
+
if (!match)
|
|
1801
|
+
return;
|
|
1802
|
+
const when = match[1].trim();
|
|
1803
|
+
this.emit("pty_error", { name: this.name, type: "quota", action: "notify", verifyQuota: true, skipCooldown: true, skipRecoveryWait: true,
|
|
1804
|
+
message: `Claude Code usage limit reached — paused; continuing automatically ${when}`,
|
|
1805
|
+
notice: quotaPausedLine("Claude", "usage", claudeResetTime(when)) });
|
|
1806
|
+
}
|
|
1807
|
+
/** #2822: Claude recorded a failed API call. */
|
|
1808
|
+
noteClaudeApiError(text, status, errorKind = null) {
|
|
1809
|
+
const seq = ++this.transcriptEventSeq;
|
|
1810
|
+
if (errorKind === "billing_error" || (status === 400 && Daemon.CREDIT_BALANCE_LOW.test(text))) {
|
|
1811
|
+
this.emit("pty_error", { name: this.name, type: "quota", action: "pause", message: "Insufficient API credits", notice: quotaPausedLine("Claude", "api_credits") });
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
const match = status === 429 ? Daemon.MODEL_USAGE_LIMIT.exec(text.trim()) : null;
|
|
1815
|
+
if (!match)
|
|
1816
|
+
return;
|
|
1817
|
+
this.usageLimitErrorSeq = seq;
|
|
1818
|
+
if (this.usageLimitHold)
|
|
1819
|
+
return; // told already: one notice per hold
|
|
1820
|
+
const model = match[1].trim();
|
|
1821
|
+
this.usageLimitHold = { model, since: Date.now() };
|
|
1822
|
+
const message = t("inst.model_usage_limit", model);
|
|
1823
|
+
this.logger.warn({ model }, "Usage-limit hold started (reset time unknown)");
|
|
1824
|
+
this.emit("usage_hold", { name: this.name, held: true });
|
|
1825
|
+
this.emit("pty_error", { name: this.name, type: "quota", action: "notify", message, notice: t("inst.quota_model_hold", model), skipCooldown: true, skipRecoveryWait: true });
|
|
1826
|
+
}
|
|
1827
|
+
/** #2822: Claude produced output (text or a tool call): the model answers again, so a hold ends. */
|
|
1828
|
+
noteClaudeOutput() {
|
|
1829
|
+
++this.transcriptEventSeq;
|
|
1830
|
+
if (!this.usageLimitHold)
|
|
1831
|
+
return;
|
|
1832
|
+
this.logger.info({ model: this.usageLimitHold.model }, "Usage-limit hold cleared — Claude answered again");
|
|
1833
|
+
this.usageLimitHold = null;
|
|
1834
|
+
this.emit("usage_hold", { name: this.name, held: false });
|
|
1835
|
+
}
|
|
1836
|
+
/** #2822: a prompt reached Claude (recorded in its transcript). */
|
|
1837
|
+
noteClaudePrompt() {
|
|
1838
|
+
++this.transcriptEventSeq;
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* #2822: the turn ended on the limit error — the transcript's newest event of any kind. A prompt after it (a new
|
|
1842
|
+
* message the error is older than), output after it (the model answered) or another API error after it (a 500, a
|
|
1843
|
+
* different 429) means the limit error is not this turn's end.
|
|
1844
|
+
*/
|
|
1845
|
+
personTurnEndedOnUsageLimit() {
|
|
1846
|
+
return this.usageLimitErrorSeq > 0 && this.usageLimitErrorSeq === this.transcriptEventSeq;
|
|
1847
|
+
}
|
|
1802
1848
|
static errorPatternKey(ep) {
|
|
1803
1849
|
return `${ep.type}:${ep.pattern.source}`;
|
|
1804
1850
|
}
|
|
@@ -1838,21 +1884,14 @@ export class Daemon extends EventEmitter {
|
|
|
1838
1884
|
return super.emit(event, ...args);
|
|
1839
1885
|
}
|
|
1840
1886
|
/**
|
|
1841
|
-
* Undo
|
|
1842
|
-
* probe
|
|
1843
|
-
*
|
|
1844
|
-
*
|
|
1845
|
-
*
|
|
1846
|
-
* daemon permanently suspicious of an auth failure that never existed. That
|
|
1847
|
-
* state is not inert: authFailureUnresolved suppresses the stuck/hang
|
|
1848
|
-
* notification and holds MCP auto-restart (a later real MCP death is then read
|
|
1849
|
-
* as already-confirmed auth trouble rather than re-verified), and the recovery
|
|
1850
|
-
* gate suppresses further error detection until a ready pattern shows up.
|
|
1851
|
-
*
|
|
1852
|
-
* Only what an auth match armed is rolled back. If some other pattern has
|
|
1853
|
-
* since armed the recovery gate, that one is still live and stays.
|
|
1887
|
+
* Undo an auth suspicion once the backend's token-free probe has said the credentials are fine. Since #2795 a pane
|
|
1888
|
+
* pattern match arms nothing before that probe (its pending match is just dropped); what is left to undo is a
|
|
1889
|
+
* login-screen report, which arms authFailureUnresolved itself. That state is not inert: it suppresses the
|
|
1890
|
+
* stuck/hang notification and holds MCP auto-restart. If some other pattern has armed the recovery gate, that one
|
|
1891
|
+
* is still live and stays.
|
|
1854
1892
|
*/
|
|
1855
1893
|
clearSuspectedAuthFailure() {
|
|
1894
|
+
this.unverifiedAuthMatch = null;
|
|
1856
1895
|
if (!this.authFailureUnresolved && !this.loginScreenReported)
|
|
1857
1896
|
return false;
|
|
1858
1897
|
this.authFailureUnresolved = false;
|
|
@@ -1864,6 +1903,29 @@ export class Daemon extends EventEmitter {
|
|
|
1864
1903
|
this.logger.info("Auth suspicion withdrawn — the token-free probe reports valid credentials");
|
|
1865
1904
|
return true;
|
|
1866
1905
|
}
|
|
1906
|
+
armErrorRecoveryGate(key, type, now) {
|
|
1907
|
+
this.errorWaitingForRecovery = true;
|
|
1908
|
+
this.errorDetectedAt = now;
|
|
1909
|
+
this.errorRecoveryDeadlineAt = now + Daemon.ERROR_RECOVERY_TIMEOUT_MS;
|
|
1910
|
+
this.activeErrorPatternKey = key;
|
|
1911
|
+
this.lastDetectedErrorType = type;
|
|
1912
|
+
}
|
|
1913
|
+
/**
|
|
1914
|
+
* #2795: the token-free check did not vouch for the credentials behind an auth pattern match. Only now does the
|
|
1915
|
+
* match become an auth failure: the state the detector used to arm at match time. Remembered for as long as it is
|
|
1916
|
+
* unresolved — an auth-broken CLI cannot finish the turn it is holding, so every other detector would otherwise
|
|
1917
|
+
* keep reporting the same dead instance. A login-screen report arms its own state and has no match to confirm.
|
|
1918
|
+
*/
|
|
1919
|
+
confirmAuthFailure() {
|
|
1920
|
+
const match = this.unverifiedAuthMatch;
|
|
1921
|
+
if (!match)
|
|
1922
|
+
return false;
|
|
1923
|
+
this.unverifiedAuthMatch = null;
|
|
1924
|
+
if (match.gate)
|
|
1925
|
+
this.armErrorRecoveryGate(match.key, "auth_error", match.at);
|
|
1926
|
+
this.authFailureUnresolved = true;
|
|
1927
|
+
return true;
|
|
1928
|
+
}
|
|
1867
1929
|
clearErrorRecoveryGate() {
|
|
1868
1930
|
this.errorWaitingForRecovery = false;
|
|
1869
1931
|
this.errorDetectedAt = 0;
|
|
@@ -2283,19 +2345,40 @@ export class Daemon extends EventEmitter {
|
|
|
2283
2345
|
this.bootSkippedResume = false;
|
|
2284
2346
|
this.recoveryCrashLoopFresh = false;
|
|
2285
2347
|
this.recoveryFreshChoice = false;
|
|
2348
|
+
this.bootCrashLoopResume = false;
|
|
2286
2349
|
try {
|
|
2287
2350
|
if (existsSync(crashStatePath)) {
|
|
2288
2351
|
const content = readFileSync(crashStatePath, "utf-8");
|
|
2289
2352
|
try {
|
|
2290
2353
|
const state = JSON.parse(content);
|
|
2291
|
-
if (state?.
|
|
2354
|
+
if (state?.resumeFailed === true) {
|
|
2355
|
+
// The previous start tried resume after a crash loop but the resumed
|
|
2356
|
+
// session died before reaching ready. Go fresh this time.
|
|
2292
2357
|
this.skipResume = true;
|
|
2293
2358
|
this.bootSkippedResume = true;
|
|
2294
|
-
this.recoveryCrashLoopFresh =
|
|
2359
|
+
this.recoveryCrashLoopFresh = true;
|
|
2360
|
+
this.recoveryFreshChoice = false;
|
|
2361
|
+
this.logger.warn("Previously resumed session died quickly after a crash loop — starting fresh");
|
|
2362
|
+
}
|
|
2363
|
+
else if (state?.resumeDisabled === true) {
|
|
2364
|
+
// Crash loop detected, but the session is not proven broken.
|
|
2365
|
+
// Try resume first; if it dies before reaching ready, the next
|
|
2366
|
+
// boot will see resumeFailed and go fresh (PR #2809).
|
|
2367
|
+
const isCrashLoop = (state.reason === undefined || state.reason === "crash_loop")
|
|
2295
2368
|
&& Number.isInteger(state.crashesInWindow) && state.crashesInWindow >= 3
|
|
2296
2369
|
&& Number.isFinite(state.lastCrashAt) && state.lastCrashAt > 0;
|
|
2297
|
-
|
|
2298
|
-
|
|
2370
|
+
if (isCrashLoop) {
|
|
2371
|
+
this.bootCrashLoopResume = true;
|
|
2372
|
+
this.logger.warn("Previous crash loop detected — attempting resume (will fall back to fresh if session is broken)");
|
|
2373
|
+
}
|
|
2374
|
+
else {
|
|
2375
|
+
// Explicit fresh-start request (not a crash loop); honour it.
|
|
2376
|
+
this.skipResume = true;
|
|
2377
|
+
this.bootSkippedResume = true;
|
|
2378
|
+
this.recoveryCrashLoopFresh = false;
|
|
2379
|
+
this.recoveryFreshChoice = true;
|
|
2380
|
+
this.logger.warn("Previous crash loop detected — starting without resume");
|
|
2381
|
+
}
|
|
2299
2382
|
}
|
|
2300
2383
|
}
|
|
2301
2384
|
finally {
|
|
@@ -2441,16 +2524,11 @@ export class Daemon extends EventEmitter {
|
|
|
2441
2524
|
catch { /* may not exist */ }
|
|
2442
2525
|
}
|
|
2443
2526
|
// #1209: conditional continuation for the turn interrupted by the last
|
|
2444
|
-
// restart, if the gates allow. Detached
|
|
2527
|
+
// restart, if the gates allow. Detached; the gate
|
|
2445
2528
|
// consumes its one-shot marker first and never throws into boot.
|
|
2446
2529
|
const legacyResume = this.maybeResumeInterruptedTurn().catch(() => "unknown");
|
|
2447
|
-
// Warmup: wait for CLI idle, then trigger steering reload — but only when
|
|
2448
|
-
// the instructions actually changed since the agent last saw them.
|
|
2449
|
-
// Skipping the no-op reload saves 10-30s of agent time on every restart
|
|
2450
|
-
// where instructions are unchanged.
|
|
2451
|
-
const warmup = this.runWarmupInstructionNotice();
|
|
2452
2530
|
const recoveryOwner = JSON.stringify(this.interactionOwner());
|
|
2453
|
-
void Promise.allSettled([legacyResume
|
|
2531
|
+
void Promise.allSettled([legacyResume]).then(() => {
|
|
2454
2532
|
if (JSON.stringify(this.interactionOwner()) === recoveryOwner) {
|
|
2455
2533
|
this.recoveryBootWorkPending = false;
|
|
2456
2534
|
this.recoveryBootSettled = true;
|
|
@@ -2488,6 +2566,7 @@ export class Daemon extends EventEmitter {
|
|
|
2488
2566
|
.catch(e => this.logger.debug({ err: e.message }, "Ack react failed"));
|
|
2489
2567
|
};
|
|
2490
2568
|
this.transcriptMonitor.on("tool_use", (name, input) => {
|
|
2569
|
+
this.noteClaudeOutput();
|
|
2491
2570
|
this.logger.debug({ tool: name }, "Tool use");
|
|
2492
2571
|
ackIfPending();
|
|
2493
2572
|
this.recordRecentEvent({ type: "tool_use", name, preview: this.summarizeTool(name, input) });
|
|
@@ -2501,7 +2580,11 @@ export class Daemon extends EventEmitter {
|
|
|
2501
2580
|
// show only elapsed time than to leave a stale "Bash: npm test" on screen.
|
|
2502
2581
|
this.publishActivity(null);
|
|
2503
2582
|
});
|
|
2583
|
+
this.transcriptMonitor.on("api_error", (text, status, _at, errorKind) => this.noteClaudeApiError(text, status, errorKind));
|
|
2584
|
+
this.transcriptMonitor.on("system_informational", (text) => this.noteClaudeInformational(text));
|
|
2585
|
+
this.transcriptMonitor.on("user_prompt", () => this.noteClaudePrompt());
|
|
2504
2586
|
this.transcriptMonitor.on("assistant_text", (text) => {
|
|
2587
|
+
this.noteClaudeOutput();
|
|
2505
2588
|
this.logger.debug({ text: text.slice(0, 200) }, "Claude response");
|
|
2506
2589
|
ackIfPending();
|
|
2507
2590
|
this.recordRecentEvent({ type: "assistant_text", preview: text.slice(0, 100) });
|
|
@@ -3649,10 +3732,21 @@ export class Daemon extends EventEmitter {
|
|
|
3649
3732
|
if (!Daemon.dialogMatches(dialog, pane))
|
|
3650
3733
|
continue;
|
|
3651
3734
|
this.noteCodexReserveDialog(dialog, pane);
|
|
3735
|
+
this.noteLiveIncidentDialog(dialog, pane);
|
|
3652
3736
|
if (dialog.blocksDelivery || dialog.holdOnly)
|
|
3653
3737
|
blockingSeen = dialog;
|
|
3654
|
-
if (dialog.holdOnly)
|
|
3655
|
-
|
|
3738
|
+
if (dialog.holdOnly) {
|
|
3739
|
+
// Recognised, deliberately not answered; trackDialogParked reports it. Why an automatic answer passed it by
|
|
3740
|
+
// is logged once, when this hold begins (#2893).
|
|
3741
|
+
if (this.heldReasonLoggedKey !== Daemon.dialogKey(dialog)) {
|
|
3742
|
+
const reason = Daemon.heldReasonOf(dialog, pane);
|
|
3743
|
+
if (Object.keys(reason).length) {
|
|
3744
|
+
this.heldReasonLoggedKey = Daemon.dialogKey(dialog);
|
|
3745
|
+
this.logger.warn({ description: dialog.description, ...reason }, "Runtime dialog held — not auto-answering");
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
break;
|
|
3749
|
+
}
|
|
3656
3750
|
// Reported as ignoring the daemon: the SAME request (the identity the count was made on — verbatim, the
|
|
3657
3751
|
// backend's own when it names one) is answered only once per DIALOG_ANSWER_BACKOFF_MS. Anything else — another
|
|
3658
3752
|
// request, an unexplained difference, the dialog gone — is not held back here; the answer path below ends or
|
|
@@ -3715,7 +3809,7 @@ export class Daemon extends EventEmitter {
|
|
|
3715
3809
|
return;
|
|
3716
3810
|
if (dialog.verifyAfterKeys && autoKey)
|
|
3717
3811
|
this.claimAutoDialog(dialog);
|
|
3718
|
-
this.logger.info(`Auto-dismissing runtime dialog: ${dialog.description}`);
|
|
3812
|
+
this.logger.info(Daemon.answerDetailOf(dialog, currentPane), `Auto-dismissing runtime dialog: ${dialog.description}`);
|
|
3719
3813
|
const SPECIAL_KEYS = new Set(["Up", "Down", "Enter", "Escape", "Right", "Left"]);
|
|
3720
3814
|
for (const key of dialog.keys) {
|
|
3721
3815
|
if (stale())
|
|
@@ -3724,6 +3818,9 @@ export class Daemon extends EventEmitter {
|
|
|
3724
3818
|
if (SPECIAL_KEYS.has(key)) {
|
|
3725
3819
|
sent = await pollTmux.sendSpecialKey(key);
|
|
3726
3820
|
}
|
|
3821
|
+
else if (dialog.literalKeys) {
|
|
3822
|
+
sent = await pollTmux.sendKeys(key); // typed, no Enter (#2872)
|
|
3823
|
+
}
|
|
3727
3824
|
else {
|
|
3728
3825
|
sent = await pollTmux.pasteText(key, this.systemPasteOptions());
|
|
3729
3826
|
}
|
|
@@ -3768,6 +3865,8 @@ export class Daemon extends EventEmitter {
|
|
|
3768
3865
|
else {
|
|
3769
3866
|
resolved = true;
|
|
3770
3867
|
}
|
|
3868
|
+
if (dialog.dismissIncident)
|
|
3869
|
+
this.emitDismissIncident(dialog);
|
|
3771
3870
|
}
|
|
3772
3871
|
else if (dialog.keys.length > 0) {
|
|
3773
3872
|
// Did the keys take? Read the pane again. The same request still there is an answer that did nothing. A request
|
|
@@ -3801,8 +3900,10 @@ export class Daemon extends EventEmitter {
|
|
|
3801
3900
|
// Reached only when no dialog was answered this poll (none on screen, a fenced one, or one waiting out its
|
|
3802
3901
|
// backoff): unless it is the one backing off, the screen that was being answered is gone, and the next dialog is
|
|
3803
3902
|
// a new one.
|
|
3804
|
-
if (!answersBackedOff)
|
|
3903
|
+
if (!answersBackedOff && this.dialogAnswers) {
|
|
3805
3904
|
this.dialogAnswers = null;
|
|
3905
|
+
this.retireParkedNotice("answer");
|
|
3906
|
+
}
|
|
3806
3907
|
this.trackDialogParked(blockingSeen);
|
|
3807
3908
|
this.trackRemoteCard(blockingSeen, pane, captureMono);
|
|
3808
3909
|
if (blockingSeen)
|
|
@@ -3848,6 +3949,14 @@ export class Daemon extends EventEmitter {
|
|
|
3848
3949
|
rebasedSeededBaselines.add(key);
|
|
3849
3950
|
}
|
|
3850
3951
|
}
|
|
3952
|
+
// #2795: an auth match the token-free check has not judged yet holds the other detectors, as the armed gate did
|
|
3953
|
+
// before: a real auth failure's text also matches generic patterns (kiro's "having trouble responding"). It is not
|
|
3954
|
+
// an error state — nobody is warned — and it is bounded like the gate.
|
|
3955
|
+
if (this.unverifiedAuthMatch?.gate) {
|
|
3956
|
+
if (now - this.unverifiedAuthMatch.at < Daemon.ERROR_RECOVERY_TIMEOUT_MS)
|
|
3957
|
+
return;
|
|
3958
|
+
this.unverifiedAuthMatch = null;
|
|
3959
|
+
}
|
|
3851
3960
|
// State: waiting for recovery. A missing/outdated ready pattern must not
|
|
3852
3961
|
// suppress every future error forever, so the gate has a hard deadline.
|
|
3853
3962
|
if (this.errorWaitingForRecovery) {
|
|
@@ -3912,9 +4021,10 @@ export class Daemon extends EventEmitter {
|
|
|
3912
4021
|
const count = countMatches(ep.pattern);
|
|
3913
4022
|
const seen = this.lastErrorCount.get(key) ?? 0;
|
|
3914
4023
|
// #2282: a resumed exhausted429 row is history, not even a new
|
|
3915
|
-
// notification/failover incident
|
|
3916
|
-
//
|
|
3917
|
-
|
|
4024
|
+
// notification/failover incident; so is a resumed codex workspace-limit row
|
|
4025
|
+
// (#2844: a wake must not pause again on it). Wait for the first live
|
|
4026
|
+
// composer to establish its baseline; later appended occurrences remain eligible.
|
|
4027
|
+
if ((ep.type === "rate_limit" && ep.action === "nudge_continue") || ep.historyUntilLiveComposer) {
|
|
3918
4028
|
const baseline = this.nudgeBaselines?.get(key);
|
|
3919
4029
|
if (baseline === undefined || count <= baseline) {
|
|
3920
4030
|
if (baseline !== undefined)
|
|
@@ -3991,30 +4101,38 @@ export class Daemon extends EventEmitter {
|
|
|
3991
4101
|
break; // Only handle first unsuppressed new error per scan
|
|
3992
4102
|
}
|
|
3993
4103
|
}
|
|
4104
|
+
/**
|
|
4105
|
+
* #2872: the incident a dialog stood for, once AgEnD's own keys closed it (RuntimeDialog.dismissIncident). It skips
|
|
4106
|
+
* the usage check (the modal was the proof) and arms no pane gate: nothing on screen is its evidence, so nothing
|
|
4107
|
+
* stale can raise it again.
|
|
4108
|
+
*/
|
|
4109
|
+
emitDismissIncident(dialog) {
|
|
4110
|
+
const inc = dialog.dismissIncident;
|
|
4111
|
+
this.logger.warn({ errorType: inc.type, action: inc.action, dialog: dialog.description }, `Dialog incident: ${inc.message}`);
|
|
4112
|
+
this.emit("pty_error", { name: this.name, type: inc.type, action: inc.action, message: inc.message, skipUsageVerify: true,
|
|
4113
|
+
...(inc.notice ? { notice: inc.notice } : {}) });
|
|
4114
|
+
}
|
|
3994
4115
|
/** Arm the recovery gates and emit one detected error pattern. */
|
|
3995
4116
|
emitErrorPattern(ep, key, pane, now) {
|
|
4117
|
+
// #2795: auth text on the pane proves nothing about the credentials (22 of 22 such matches on the main host were
|
|
4118
|
+
// conversation text). It arms nothing here; it only asks the lifecycle's token-free check, and a failing check
|
|
4119
|
+
// arms the auth state through confirmAuthFailure().
|
|
4120
|
+
if (ep.type === "auth_error")
|
|
4121
|
+
this.unverifiedAuthMatch = { key, at: now, gate: !ep.skipRecoveryWait };
|
|
3996
4122
|
// skipRecoveryWait: this error self-recovers (e.g. a timeout — Kiro is
|
|
3997
4123
|
// back at its prompt immediately). Keep monitoring so the next occurrence
|
|
3998
4124
|
// can fire without waiting for a possibly startup-only ready pattern.
|
|
3999
|
-
if (!ep.skipRecoveryWait)
|
|
4000
|
-
this.
|
|
4001
|
-
this.errorDetectedAt = now;
|
|
4002
|
-
this.errorRecoveryDeadlineAt = now + Daemon.ERROR_RECOVERY_TIMEOUT_MS;
|
|
4003
|
-
this.activeErrorPatternKey = key;
|
|
4004
|
-
this.lastDetectedErrorType = ep.type;
|
|
4005
|
-
}
|
|
4125
|
+
else if (!ep.skipRecoveryWait)
|
|
4126
|
+
this.armErrorRecoveryGate(key, ep.type, now);
|
|
4006
4127
|
this.lastErrorNotifiedAt.set(key, now);
|
|
4007
4128
|
if (ep.action === "failover")
|
|
4008
4129
|
this.lastFailoverAt = now;
|
|
4009
4130
|
const message = this.resolveErrorMessage(pane, ep);
|
|
4010
|
-
// Remember an auth failure for as long as it is unresolved. An auth-broken
|
|
4011
|
-
// CLI cannot finish the turn it is holding, so every other detector in this
|
|
4012
|
-
// scan would otherwise keep reporting the same dead instance forever.
|
|
4013
|
-
if (ep.type === "auth_error")
|
|
4014
|
-
this.authFailureUnresolved = true;
|
|
4015
4131
|
this.logger.warn({ errorType: ep.type, action: ep.action }, `PTY error detected: ${message}`);
|
|
4016
4132
|
this.lastErrorEpisode = { key, pattern: ep.pattern, pane, spawn: this.spawnGeneration, fence: this.launchFenceEpoch };
|
|
4017
|
-
|
|
4133
|
+
const { notice: noticeOf, ...rest } = ep;
|
|
4134
|
+
const notice = noticeOf ? this.resolveErrorNotice(pane, ep, noticeOf) : undefined;
|
|
4135
|
+
this.emit("pty_error", { name: this.name, ...rest, message, ...(notice ? { notice } : {}) });
|
|
4018
4136
|
}
|
|
4019
4137
|
/**
|
|
4020
4138
|
* Notification text for a detected pattern. `formatMessage` patterns build it
|
|
@@ -4024,6 +4142,18 @@ export class Daemon extends EventEmitter {
|
|
|
4024
4142
|
* Uses the LAST match in the pane: an older warning may still be scrolled up
|
|
4025
4143
|
* (10% earlier, 5% now), and the newest one is the one worth reporting.
|
|
4026
4144
|
*/
|
|
4145
|
+
/** The pattern's short notice from its last match on the pane (no match: from none); undefined if it throws. */
|
|
4146
|
+
resolveErrorNotice(pane, ep, noticeOf) {
|
|
4147
|
+
try {
|
|
4148
|
+
const flags = ep.pattern.flags.includes("g") ? ep.pattern.flags : ep.pattern.flags + "g";
|
|
4149
|
+
const matches = [...pane.matchAll(new RegExp(ep.pattern.source, flags))];
|
|
4150
|
+
return noticeOf(matches[matches.length - 1] ?? null);
|
|
4151
|
+
}
|
|
4152
|
+
catch (err) {
|
|
4153
|
+
this.logger.debug({ err, errorType: ep.type }, "notice failed — posting the standard notification");
|
|
4154
|
+
return undefined;
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4027
4157
|
resolveErrorMessage(pane, ep) {
|
|
4028
4158
|
if (!ep.formatMessage)
|
|
4029
4159
|
return ep.message;
|
|
@@ -4954,6 +5084,22 @@ export class Daemon extends EventEmitter {
|
|
|
4954
5084
|
&& Daemon.dialogMatches(d, pane)
|
|
4955
5085
|
// #1690: AgEnD's own switch dialog, being answered right now, is nobody's to answer.
|
|
4956
5086
|
&& !(this.ownSwitch && claudeSwitchDialog(pane) !== null));
|
|
5087
|
+
// #2844: a dialog AgEnD is answering itself is nobody's to answer — for AGEND_ANSWER_GRACE_MS of being on screen,
|
|
5088
|
+
// and only until its answers are reported as ignored. No evidence at all meanwhile: the catch-alls further down
|
|
5089
|
+
// the table match the same screen. A dialog still there after the grace (an Escape that did not take, keys that
|
|
5090
|
+
// could not be sent) is a person's again.
|
|
5091
|
+
if (dialog?.answeredByAgend) {
|
|
5092
|
+
const key = Daemon.answerKey(dialog);
|
|
5093
|
+
const now = performance.now();
|
|
5094
|
+
if (this.agendAnswerGrace?.key !== key)
|
|
5095
|
+
this.agendAnswerGrace = { key, since: now };
|
|
5096
|
+
if (now - this.agendAnswerGrace.since < AGEND_ANSWER_GRACE_MS
|
|
5097
|
+
&& !(this.dialogAnswers?.reported && this.dialogAnswers.key === key))
|
|
5098
|
+
return null;
|
|
5099
|
+
}
|
|
5100
|
+
else {
|
|
5101
|
+
this.agendAnswerGrace = null;
|
|
5102
|
+
}
|
|
4957
5103
|
if (dialog) {
|
|
4958
5104
|
const identity = dialog.requestIdentity?.(pane) ?? pane.replace(/\r/g, "")
|
|
4959
5105
|
.split("\n").map(row => row.replace(/^([ \t]*)[❯›]([ \t]+)(?=\d\.|Yes\b|No\b|Resume\b)/, "$1$2").trimEnd())
|
|
@@ -5186,6 +5332,7 @@ export class Daemon extends EventEmitter {
|
|
|
5186
5332
|
this.pausePending = false;
|
|
5187
5333
|
this.pauseReconfirmAuth = false;
|
|
5188
5334
|
this.authFailureUnresolved = false;
|
|
5335
|
+
this.unverifiedAuthMatch = null;
|
|
5189
5336
|
this.loginScreenReported = false;
|
|
5190
5337
|
this.fatalStartupReported = false;
|
|
5191
5338
|
this.fatalStartupBlocked = false;
|
|
@@ -5681,6 +5828,15 @@ export class Daemon extends EventEmitter {
|
|
|
5681
5828
|
this.turnReplyGuard.complete(turn.generation);
|
|
5682
5829
|
return;
|
|
5683
5830
|
}
|
|
5831
|
+
// #2822: a turn whose newest transcript event is Claude's usage-limit error could not have replied — no recovery
|
|
5832
|
+
// turn, no drop report: the limit is told once (the notice and Needs you). Only that record counts: pane text and
|
|
5833
|
+
// an error from before this turn began do not.
|
|
5834
|
+
if (this.personTurnEndedOnUsageLimit()) {
|
|
5835
|
+
this.logger.info({ correlationId: turn.target.correlationId }, "Turn ended on Claude's usage-limit error — no reply recovery");
|
|
5836
|
+
this.clearReplyGuardConfirm();
|
|
5837
|
+
this.turnReplyGuard.complete(turn.generation);
|
|
5838
|
+
return;
|
|
5839
|
+
}
|
|
5684
5840
|
// A second idle edge ends the one permitted recovery turn. Never create a
|
|
5685
5841
|
// third turn or guess at terminal text; make the failure visible instead.
|
|
5686
5842
|
if (turn.phase === "recovering") {
|
|
@@ -6030,20 +6186,7 @@ export class Daemon extends EventEmitter {
|
|
|
6030
6186
|
reason,
|
|
6031
6187
|
message: formatReplyDropDetection(turn, reason),
|
|
6032
6188
|
});
|
|
6033
|
-
|
|
6034
|
-
}
|
|
6035
|
-
/** Channel notice for a reply-drop. `rateLimit` shares the per-instance cooldown (#2544). */
|
|
6036
|
-
deliverReplyDropNotice(key, kind, title, turn, reason, rateLimit) {
|
|
6037
|
-
if (rateLimit) {
|
|
6038
|
-
const now = Date.now();
|
|
6039
|
-
if (this.lastReplyDropWarningAt !== 0
|
|
6040
|
-
&& now - this.lastReplyDropWarningAt < REPLY_DROP_WARNING_COOLDOWN_MS) {
|
|
6041
|
-
this.logger.debug({ reason, kind }, "Reply-drop channel notice rate-limited");
|
|
6042
|
-
return;
|
|
6043
|
-
}
|
|
6044
|
-
this.lastReplyDropWarningAt = now;
|
|
6045
|
-
}
|
|
6046
|
-
void this.deliverDaemonReply(t(key), kind, title, turn.target, true);
|
|
6189
|
+
// #2802: the event stays (Log tab); the person is no longer told in the topic or the web chat.
|
|
6047
6190
|
}
|
|
6048
6191
|
/** Relay the pane's final text to the channel, marked as a daemon proxy reply. */
|
|
6049
6192
|
async sendProxyReply(pane, target) {
|
|
@@ -6076,7 +6219,7 @@ export class Daemon extends EventEmitter {
|
|
|
6076
6219
|
}
|
|
6077
6220
|
}
|
|
6078
6221
|
/** Context-bound reply path shared by dead-MCP and malformed-call recovery. */
|
|
6079
|
-
deliverDaemonReply(body, requestPrefix, logLabel, target
|
|
6222
|
+
deliverDaemonReply(body, requestPrefix, logLabel, target) {
|
|
6080
6223
|
const args = { text: body };
|
|
6081
6224
|
const chatId = target?.chatId ?? this.lastChatId;
|
|
6082
6225
|
const threadId = target?.threadId ?? this.lastThreadId;
|
|
@@ -6153,7 +6296,6 @@ export class Daemon extends EventEmitter {
|
|
|
6153
6296
|
args,
|
|
6154
6297
|
fleetRequestId: fleetReqId,
|
|
6155
6298
|
adapterId,
|
|
6156
|
-
statusOnly,
|
|
6157
6299
|
});
|
|
6158
6300
|
}
|
|
6159
6301
|
catch {
|
|
@@ -6329,7 +6471,9 @@ export class Daemon extends EventEmitter {
|
|
|
6329
6471
|
// changed when tmux reported output (observedChangeAt), but idle/stuck are
|
|
6330
6472
|
// decisions about *now*. The old double-observe expressed that by calling
|
|
6331
6473
|
// observe twice, which silently disabled the "content moved" branch.
|
|
6332
|
-
|
|
6474
|
+
if (this.backend?.periodicRedrawFrom)
|
|
6475
|
+
this.periodicRedrawLayout = this.backend.periodicRedrawFrom(pane);
|
|
6476
|
+
const structuredPeriodicIdle = this.periodicIdleProof();
|
|
6333
6477
|
// #1641: the transcript outranks the pane: while it shows the turn running, no reading of this pane is idle (codex
|
|
6334
6478
|
// hides its status row while streaming, leaving the idle composer and footer in view mid-turn).
|
|
6335
6479
|
const transcriptTurn = this.transcriptTurnState();
|
|
@@ -6521,7 +6665,7 @@ export class Daemon extends EventEmitter {
|
|
|
6521
6665
|
return;
|
|
6522
6666
|
this.instanceStateLastOutputAt = event.at;
|
|
6523
6667
|
this.interactionOutputRevision++;
|
|
6524
|
-
const canProvePeriodicIdle = !!this.
|
|
6668
|
+
const canProvePeriodicIdle = !!this.periodicIdleProof();
|
|
6525
6669
|
if (canProvePeriodicIdle || (this.backend?.hasPeriodicPaneRedraw?.() === true && this.instanceState === "idle")) {
|
|
6526
6670
|
// Do not emit idle→working solely because agy repainted an identical
|
|
6527
6671
|
// footer. Codex additionally uses a structural probe while working: two
|
|
@@ -6539,6 +6683,25 @@ export class Daemon extends EventEmitter {
|
|
|
6539
6683
|
this.applyInstanceStateSnapshot(snapshot);
|
|
6540
6684
|
this.scheduleInstanceStateIdleCapture();
|
|
6541
6685
|
this.scheduleInstanceStateStuckDeadline(event.at);
|
|
6686
|
+
// #2770 (ii): a layout that repaints while idle (Claude's background-agent list) never leaves the quiet window the
|
|
6687
|
+
// idle capture waits for, so nothing would look at the pane again before the 60 s sweep. Look now and then.
|
|
6688
|
+
if (this.backend?.periodicRedrawFrom && performance.now() - this.periodicLayoutLookAt >= PERIODIC_LAYOUT_LOOK_MS) {
|
|
6689
|
+
this.periodicLayoutLookAt = performance.now();
|
|
6690
|
+
this.scheduleInstanceStateOutputProbe(STRUCTURED_IDLE_PROBE_MS);
|
|
6691
|
+
}
|
|
6692
|
+
}
|
|
6693
|
+
/**
|
|
6694
|
+
* The backend's structural idle proof for periodically repainting panes, when it applies to the current layout: always
|
|
6695
|
+
* for a backend that repaints in every layout (Codex, Muse), only while the latest capture showed such a layout for one
|
|
6696
|
+
* that names it (CliBackend.periodicRedrawFrom — Claude's background-agent list, #2770 (ii)).
|
|
6697
|
+
*/
|
|
6698
|
+
periodicIdleProof() {
|
|
6699
|
+
const proof = this.backend?.isPeriodicRedrawIdlePane;
|
|
6700
|
+
if (!proof)
|
|
6701
|
+
return undefined;
|
|
6702
|
+
if (this.backend?.periodicRedrawFrom && !this.periodicRedrawLayout)
|
|
6703
|
+
return undefined;
|
|
6704
|
+
return proof;
|
|
6542
6705
|
}
|
|
6543
6706
|
bindInstanceStateOutputListener(windowId) {
|
|
6544
6707
|
if (!this.controlClient || !this.instanceStateMonitorActive)
|
|
@@ -6555,6 +6718,7 @@ export class Daemon extends EventEmitter {
|
|
|
6555
6718
|
startInstanceStateMonitor() {
|
|
6556
6719
|
if (this.runtimeMonitorsFrozen || !this.tmux || !this.backend || this.instanceStateMonitorActive)
|
|
6557
6720
|
return;
|
|
6721
|
+
this.periodicRedrawLayout = false; // a new launch's layout is not the old one's
|
|
6558
6722
|
const rawConfig = this.config.hang_detector;
|
|
6559
6723
|
const timeoutMinutes = rawConfig?.timeout_minutes;
|
|
6560
6724
|
this.instanceStateStuckTimeoutMs = typeof timeoutMinutes === "number" && timeoutMinutes > 0
|
|
@@ -6971,6 +7135,26 @@ export class Daemon extends EventEmitter {
|
|
|
6971
7135
|
return { receiptId: id, verificationId: meta.verification_id, expectedWireHash: meta.human_wire_sha256,
|
|
6972
7136
|
ownerKey: meta.human_owner_key, current: () => this.captureHumanReceiptOwner() === daemonOwner };
|
|
6973
7137
|
}
|
|
7138
|
+
/**
|
|
7139
|
+
* #2770: a person's message a cancel dropped before it reached the CLI is told, not left as a silent "uncertain":
|
|
7140
|
+
* the messages dropped together go out as one `human_messages_cancelled` (the fleet marks each ❌ and says so once).
|
|
7141
|
+
* Agents' messages are not a person's, and a durable row (delivery_id) is retried by the outbox: neither is ❌'d. A
|
|
7142
|
+
* message with no chat to mark has nowhere to say it.
|
|
7143
|
+
*/
|
|
7144
|
+
noteCancelledHuman(meta) {
|
|
7145
|
+
if (meta.from_instance || meta.delivery_id || !meta.chat_id || !meta.message_id)
|
|
7146
|
+
return;
|
|
7147
|
+
this.cancelledHumans.push({ chatId: meta.chat_id, messageId: meta.message_id, ...(meta.thread_id ? { threadId: meta.thread_id } : {}) });
|
|
7148
|
+
if (this.cancelledHumansTimer)
|
|
7149
|
+
clearTimeout(this.cancelledHumansTimer);
|
|
7150
|
+
this.cancelledHumansTimer = setTimeout(() => {
|
|
7151
|
+
this.cancelledHumansTimer = null;
|
|
7152
|
+
const messages = this.cancelledHumans.splice(0);
|
|
7153
|
+
if (messages.length)
|
|
7154
|
+
this.emit("human_messages_cancelled", { messages });
|
|
7155
|
+
}, CANCELLED_HUMANS_NOTICE_MS);
|
|
7156
|
+
this.cancelledHumansTimer.unref?.();
|
|
7157
|
+
}
|
|
6974
7158
|
finishHumanReceipt(meta, verdict) {
|
|
6975
7159
|
if (!meta.human_receipt_id || !meta.human_owner_key)
|
|
6976
7160
|
return;
|
|
@@ -7045,6 +7229,7 @@ export class Daemon extends EventEmitter {
|
|
|
7045
7229
|
steerMessage(content, meta, deliveryEpoch = this.deliveryEpoch) {
|
|
7046
7230
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
7047
7231
|
this.deferDurableDelivery(meta, "delivery epoch changed before steer began");
|
|
7232
|
+
this.noteCancelledHuman(meta);
|
|
7048
7233
|
return;
|
|
7049
7234
|
}
|
|
7050
7235
|
// The fleet facade prevents a steer from overtaking an item still in its
|
|
@@ -7068,14 +7253,17 @@ export class Daemon extends EventEmitter {
|
|
|
7068
7253
|
this.steerLock = this.steerLock.then(async () => {
|
|
7069
7254
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
7070
7255
|
this.deferDurableDelivery(meta, "delivery epoch changed before steer began");
|
|
7256
|
+
this.noteCancelledHuman(meta);
|
|
7071
7257
|
return;
|
|
7072
7258
|
}
|
|
7073
7259
|
await this.wake();
|
|
7074
7260
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
7075
7261
|
this.deferDurableDelivery(meta, "delivery epoch changed while waking for steer");
|
|
7262
|
+
this.noteCancelledHuman(meta);
|
|
7076
7263
|
return;
|
|
7077
7264
|
}
|
|
7078
7265
|
const verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
|
|
7266
|
+
let delivered = false;
|
|
7079
7267
|
try {
|
|
7080
7268
|
if (await this.deliverMessage(formatted, status, {
|
|
7081
7269
|
steer: true,
|
|
@@ -7085,6 +7273,7 @@ export class Daemon extends EventEmitter {
|
|
|
7085
7273
|
verdict,
|
|
7086
7274
|
durableAttempt: durableAttempt || undefined,
|
|
7087
7275
|
})) {
|
|
7276
|
+
delivered = true;
|
|
7088
7277
|
this.finishDurableSubmission(durableAttempt, verdict);
|
|
7089
7278
|
this.markTurnStarted(meta, formatted, deliveryEpoch);
|
|
7090
7279
|
}
|
|
@@ -7114,6 +7303,8 @@ export class Daemon extends EventEmitter {
|
|
|
7114
7303
|
}
|
|
7115
7304
|
finally {
|
|
7116
7305
|
this.finishHumanReceipt(meta, verdict);
|
|
7306
|
+
if (!delivered && !verdict.paneWriteStarted && !this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
7307
|
+
this.noteCancelledHuman(meta);
|
|
7117
7308
|
}
|
|
7118
7309
|
}).catch(err => {
|
|
7119
7310
|
this.deferDurableDelivery(meta, `target wake/steer failed before pane submission: ${err.message}`);
|
|
@@ -7132,6 +7323,7 @@ export class Daemon extends EventEmitter {
|
|
|
7132
7323
|
btwMessage(content, meta, deliveryEpoch = this.deliveryEpoch) {
|
|
7133
7324
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
7134
7325
|
this.finishHumanReceipt(meta, { reached: false });
|
|
7326
|
+
this.noteCancelledHuman(meta);
|
|
7135
7327
|
return;
|
|
7136
7328
|
}
|
|
7137
7329
|
this.updateLastChat(meta.chat_id, meta.thread_id, meta.adapter_id);
|
|
@@ -7142,22 +7334,28 @@ export class Daemon extends EventEmitter {
|
|
|
7142
7334
|
this.steerLock = this.steerLock.then(async () => {
|
|
7143
7335
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
7144
7336
|
this.finishHumanReceipt(meta, { reached: false });
|
|
7337
|
+
this.noteCancelledHuman(meta);
|
|
7145
7338
|
return;
|
|
7146
7339
|
}
|
|
7147
7340
|
await this.wake();
|
|
7148
7341
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
7149
7342
|
this.finishHumanReceipt(meta, { reached: false });
|
|
7343
|
+
this.noteCancelledHuman(meta);
|
|
7150
7344
|
return;
|
|
7151
7345
|
}
|
|
7152
7346
|
const verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
|
|
7347
|
+
let delivered = false;
|
|
7153
7348
|
try {
|
|
7154
7349
|
if (await this.deliverMessage(formatted, status, { steer: true, deliveryEpoch, submissionId: meta.message_id,
|
|
7155
7350
|
...(meta.verification_id ? { humanVerificationId: meta.verification_id } : {}), verdict })) {
|
|
7351
|
+
delivered = true;
|
|
7156
7352
|
this.markTurnStarted(meta, formatted, deliveryEpoch);
|
|
7157
7353
|
}
|
|
7158
7354
|
}
|
|
7159
7355
|
finally {
|
|
7160
7356
|
this.finishHumanReceipt(meta, verdict);
|
|
7357
|
+
if (!delivered && !verdict.paneWriteStarted && !this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
7358
|
+
this.noteCancelledHuman(meta);
|
|
7161
7359
|
}
|
|
7162
7360
|
}).catch(err => {
|
|
7163
7361
|
this.finishHumanReceipt(meta, { reached: false });
|
|
@@ -7173,6 +7371,7 @@ export class Daemon extends EventEmitter {
|
|
|
7173
7371
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
7174
7372
|
this.logger.info("Pending channel delivery dropped by user cancel");
|
|
7175
7373
|
this.deferDurableDelivery(meta, "delivery epoch changed before queue admission");
|
|
7374
|
+
this.noteCancelledHuman(meta);
|
|
7176
7375
|
return;
|
|
7177
7376
|
}
|
|
7178
7377
|
if (!this.tmux) {
|
|
@@ -7194,10 +7393,6 @@ export class Daemon extends EventEmitter {
|
|
|
7194
7393
|
this.logger.warn({ err: err.message }, "Failed to persist last inbound timestamp");
|
|
7195
7394
|
}
|
|
7196
7395
|
}
|
|
7197
|
-
if (this.pendingInstructionsUpdate) {
|
|
7198
|
-
writeFileSync(join(this.instanceDir, "prev-instructions"), this.pendingInstructionsUpdate);
|
|
7199
|
-
this.pendingInstructionsUpdate = undefined;
|
|
7200
|
-
}
|
|
7201
7396
|
this.pendingWork.recordInbound();
|
|
7202
7397
|
// v3: record user messages for rotation snapshot
|
|
7203
7398
|
this.recordRecentUserMessage(content, meta);
|
|
@@ -7236,6 +7431,7 @@ export class Daemon extends EventEmitter {
|
|
|
7236
7431
|
this.pasteLock = this.pasteLock.then(async () => {
|
|
7237
7432
|
let durable = null;
|
|
7238
7433
|
let verdict = { reached: false };
|
|
7434
|
+
let delivered = false;
|
|
7239
7435
|
try {
|
|
7240
7436
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
7241
7437
|
this.logger.info("Pending channel delivery dropped by user cancel");
|
|
@@ -7247,44 +7443,11 @@ export class Daemon extends EventEmitter {
|
|
|
7247
7443
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
7248
7444
|
return;
|
|
7249
7445
|
}
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
}
|
|
7256
|
-
const whatsNew = this.pendingWhatsNew ? this.currentWhatsNew(this.pendingWhatsNew) : null;
|
|
7257
|
-
if (whatsNew) {
|
|
7258
|
-
// #1633: here, inside the delivery lock and ahead of a real delivery — never into a busy turn, never to an
|
|
7259
|
-
// instance nobody writes to. #2140: told at most once. Once its paste reached the pane it is recorded, proven
|
|
7260
|
-
// submitted or not. A system paste has no unique id: its only pane proof is its opening line on the screen, and
|
|
7261
|
-
// a long notice (every entry since an old session began) scrolls that line above the captured rows once Claude
|
|
7262
|
-
// paints it — unproven, though the agent read it. One that never reached the pane (cancelled, never idle) is
|
|
7263
|
-
// tried again, at most WHATS_NEW_MAX_UNWRITTEN times, then recorded with a warning.
|
|
7264
|
-
const attempt = { reached: false };
|
|
7265
|
-
const pasted = await this.deliverMessage(whatsNew.notice, undefined, { deliveryEpoch, verdict: attempt });
|
|
7266
|
-
if (this.pendingWhatsNew === whatsNew) {
|
|
7267
|
-
const written = pasted || attempt.paneWriteStarted === true;
|
|
7268
|
-
if (!written)
|
|
7269
|
-
this.whatsNewUnwritten++;
|
|
7270
|
-
if (written || this.whatsNewUnwritten >= WHATS_NEW_MAX_UNWRITTEN) {
|
|
7271
|
-
if (!pasted) {
|
|
7272
|
-
this.logger.warn({ entries: whatsNew.ids, written, attempts: this.whatsNewUnwritten, phase: attempt.phase, proof: attempt.proof }, written ? "what's-new notice pasted but its submission could not be proven — recorded so it is not sent again"
|
|
7273
|
-
: "what's-new notice did not reach the pane after several deliveries — recorded so it is not sent again");
|
|
7274
|
-
}
|
|
7275
|
-
this.pendingWhatsNew = null;
|
|
7276
|
-
this.whatsNewUnwritten = 0;
|
|
7277
|
-
try {
|
|
7278
|
-
recordWhatsNew(this.instanceDir, whatsNew.ids, whatsNew.version);
|
|
7279
|
-
}
|
|
7280
|
-
catch (err) {
|
|
7281
|
-
this.logger.warn({ err }, "what's-new record not written — the notice may repeat once");
|
|
7282
|
-
}
|
|
7283
|
-
}
|
|
7284
|
-
}
|
|
7285
|
-
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
7286
|
-
return;
|
|
7287
|
-
}
|
|
7446
|
+
// #2800: with this real message, in the same paste — never a paste or turn of its own: after its delivery marker
|
|
7447
|
+
// (transcript proof anchors on it), before the message. A person's message with a verified receipt is pasted
|
|
7448
|
+
// byte for byte (its proof is the wire's hash), so the notice waits for the next delivery without one.
|
|
7449
|
+
const whatsNew = this.pendingWhatsNew && !meta.human_receipt_id ? this.currentWhatsNew(this.pendingWhatsNew) : null;
|
|
7450
|
+
const pasted = whatsNew ? withWhatsNew(formatted, whatsNew.notice) : formatted;
|
|
7288
7451
|
const status = channelStatus(meta);
|
|
7289
7452
|
// A fresh delivery begins a fresh turn — its bubble must not inherit
|
|
7290
7453
|
// the previous turn's tool list.
|
|
@@ -7296,13 +7459,24 @@ export class Daemon extends EventEmitter {
|
|
|
7296
7459
|
return;
|
|
7297
7460
|
}
|
|
7298
7461
|
durable = durableAttempt;
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
|
|
7304
|
-
|
|
7305
|
-
|
|
7462
|
+
let sent = false;
|
|
7463
|
+
try {
|
|
7464
|
+
sent = await this.deliverMessage(pasted, status, {
|
|
7465
|
+
deliveryEpoch,
|
|
7466
|
+
submissionId: meta.message_id,
|
|
7467
|
+
...(meta.verification_id ? { humanVerificationId: meta.verification_id } : {}),
|
|
7468
|
+
verdict,
|
|
7469
|
+
durableAttempt: durableAttempt || undefined,
|
|
7470
|
+
});
|
|
7471
|
+
}
|
|
7472
|
+
finally {
|
|
7473
|
+
// #2800: told once — recorded as soon as it went out with this message, proven submitted or not, also when
|
|
7474
|
+
// a later step of the write threw; a paste that never reached the pane keeps it for the next message.
|
|
7475
|
+
if (whatsNew && (sent || verdict.paneWriteStarted))
|
|
7476
|
+
this.recordWhatsNewSent(whatsNew);
|
|
7477
|
+
}
|
|
7478
|
+
if (sent) {
|
|
7479
|
+
delivered = true;
|
|
7306
7480
|
this.finishDurableSubmission(durable, verdict);
|
|
7307
7481
|
this.markTurnStarted(meta, formatted, deliveryEpoch);
|
|
7308
7482
|
}
|
|
@@ -7348,6 +7522,8 @@ export class Daemon extends EventEmitter {
|
|
|
7348
7522
|
}
|
|
7349
7523
|
finally {
|
|
7350
7524
|
this.finishHumanReceipt(meta, verdict);
|
|
7525
|
+
if (!delivered && !verdict.paneWriteStarted && !this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
7526
|
+
this.noteCancelledHuman(meta);
|
|
7351
7527
|
this.pasteQueueDepth--;
|
|
7352
7528
|
}
|
|
7353
7529
|
}).catch(err => {
|
|
@@ -7419,6 +7595,22 @@ export class Daemon extends EventEmitter {
|
|
|
7419
7595
|
// normal Enter input has steering/interrupt semantics in several other CLIs.
|
|
7420
7596
|
let readiness = "ready";
|
|
7421
7597
|
const gateWindowId = windowId;
|
|
7598
|
+
const handOffEligible = !!(supportsQueuedInput || opts?.steer || composerQueue);
|
|
7599
|
+
const handOffAdmissible = async () => (await this.probeBlockingDialog()).state === "clear"
|
|
7600
|
+
&& await this.hasPositiveDeliveryInput(true)
|
|
7601
|
+
&& ((!opts?.steer && !composerQueue) || (this.launchTakesSteer() && await this.steerComposerAdmits(false, opts?.steer === true)));
|
|
7602
|
+
// #2770: a delivery waiting for idle holds the paste queue, and every message behind it. It stops waiting at once
|
|
7603
|
+
// on a cancel, and as soon as the busy pane can take it the way it would have on arrival (a box that reads again, a
|
|
7604
|
+
// dialog gone) — rather than for the end of whatever keeps the pane moving (2026-10-27: 11 minutes).
|
|
7605
|
+
let handOffCheckedAt = -Infinity;
|
|
7606
|
+
const waitStop = async () => {
|
|
7607
|
+
if (cancelled())
|
|
7608
|
+
return true;
|
|
7609
|
+
if (!handOffEligible || !gateWindowId || performance.now() - handOffCheckedAt < HANDOFF_RECHECK_MS)
|
|
7610
|
+
return false;
|
|
7611
|
+
handOffCheckedAt = performance.now();
|
|
7612
|
+
return (await this.paneReadinessForDelivery(gateWindowId)) === "busy" && await handOffAdmissible() && !cancelled();
|
|
7613
|
+
};
|
|
7422
7614
|
if (gateWindowId && this.controlClient)
|
|
7423
7615
|
readiness = await this.paneReadinessForDelivery(gateWindowId);
|
|
7424
7616
|
if (readiness !== "ready") {
|
|
@@ -7431,11 +7623,7 @@ export class Daemon extends EventEmitter {
|
|
|
7431
7623
|
// fall through to the wait.
|
|
7432
7624
|
// #2676: an ordinary message goes into a mode-switched composer (kiro TUI) mid-turn too, on a launch whose busy
|
|
7433
7625
|
// composer AgEnD drives, by the steer path's own gates and proof — when its box can show it is this paste.
|
|
7434
|
-
const canHandOff =
|
|
7435
|
-
&& readiness === "busy"
|
|
7436
|
-
&& (await this.probeBlockingDialog()).state === "clear"
|
|
7437
|
-
&& await this.hasPositiveDeliveryInput(true)
|
|
7438
|
-
&& ((!opts?.steer && !composerQueue) || (this.launchTakesSteer() && await this.steerComposerAdmits(false, opts?.steer === true)));
|
|
7626
|
+
const canHandOff = handOffEligible && readiness === "busy" && await handOffAdmissible();
|
|
7439
7627
|
if (canHandOff) {
|
|
7440
7628
|
// Native queue (codex), or an explicit /steer: hand the complete
|
|
7441
7629
|
// paste+Enter transaction to the busy CLI now. For steer this is the
|
|
@@ -7449,10 +7637,14 @@ export class Daemon extends EventEmitter {
|
|
|
7449
7637
|
}
|
|
7450
7638
|
else {
|
|
7451
7639
|
this.logger.debug("CLI busy — queuing message until idle");
|
|
7452
|
-
const becameIdle = await this.waitForPaneReadyForDelivery(gateWindowId);
|
|
7640
|
+
const becameIdle = await this.waitForPaneReadyForDelivery(gateWindowId, undefined, waitStop);
|
|
7453
7641
|
if (cancelled())
|
|
7454
7642
|
return false;
|
|
7455
|
-
if (
|
|
7643
|
+
if (becameIdle === "stopped") {
|
|
7644
|
+
handingOffToNativeQueue = true;
|
|
7645
|
+
this.logger.info("The busy CLI can take this delivery now — handing it off instead of waiting for idle (#2770)");
|
|
7646
|
+
}
|
|
7647
|
+
else if (!becameIdle) {
|
|
7456
7648
|
// The pane never freed up. Report the failure instead of pasting into a
|
|
7457
7649
|
// wedged CLI (where the text would sit unsubmitted and the next message
|
|
7458
7650
|
// would land on top of it) — and instead of holding the queue silently.
|
|
@@ -7597,9 +7789,13 @@ export class Daemon extends EventEmitter {
|
|
|
7597
7789
|
return this.failDelivery(verdict, status, "pre-write", "dialog-or-input-not-ready");
|
|
7598
7790
|
}
|
|
7599
7791
|
this.logger.info("Dialog or input transition appeared before the pane write — waiting for readiness");
|
|
7600
|
-
const clear = gateWindowId ? await this.waitForPaneReadyForDelivery(gateWindowId) : false;
|
|
7792
|
+
const clear = gateWindowId ? await this.waitForPaneReadyForDelivery(gateWindowId, undefined, waitStop) : false;
|
|
7601
7793
|
if (cancelled())
|
|
7602
7794
|
return false;
|
|
7795
|
+
if (clear === "stopped") {
|
|
7796
|
+
handingOffToNativeQueue = true;
|
|
7797
|
+
continue;
|
|
7798
|
+
}
|
|
7603
7799
|
if (!clear) {
|
|
7604
7800
|
return this.failDelivery(verdict, status, "pre-write", "readiness-timeout");
|
|
7605
7801
|
}
|
|
@@ -8149,13 +8345,36 @@ export class Daemon extends EventEmitter {
|
|
|
8149
8345
|
&& !this.backend?.backgroundWorkRunning?.(pane) && codexLiveRowMatches(pane, nudge.pattern)
|
|
8150
8346
|
&& Daemon.countOccurrences(nudge.pattern, pane) === Daemon.countOccurrences(nudge.pattern, nudge.pane);
|
|
8151
8347
|
}
|
|
8348
|
+
/**
|
|
8349
|
+
* #2844: a dialog that only paints for a live incident proves the newest such error row on screen is current: its
|
|
8350
|
+
* baseline is held below it, so neither a first poll that sees the dialog before any live composer nor the composer
|
|
8351
|
+
* after it can take that row for resumed history. Older rows, of any kind, stay history.
|
|
8352
|
+
*/
|
|
8353
|
+
noteLiveIncidentDialog(dialog, pane) {
|
|
8354
|
+
if (!dialog.marksLiveIncident)
|
|
8355
|
+
return;
|
|
8356
|
+
const baselines = this.nudgeBaselines ?? new Map();
|
|
8357
|
+
// Only the row nearest the dialog is the incident; every other kind on screen (an older credits row above a new
|
|
8358
|
+
// spend cap) stays history, so the first matching pattern cannot claim the live incident under its own name.
|
|
8359
|
+
const rows = (this.backend?.getErrorPatterns?.() ?? []).filter(ep => ep.historyUntilLiveComposer).map(ep => {
|
|
8360
|
+
const flags = ep.pattern.flags.includes("g") ? ep.pattern.flags : ep.pattern.flags + "g";
|
|
8361
|
+
const matches = [...pane.matchAll(new RegExp(ep.pattern.source, flags))];
|
|
8362
|
+
return { key: Daemon.errorPatternKey(ep), count: matches.length, last: matches.at(-1)?.index ?? -1 };
|
|
8363
|
+
}).filter(row => row.count > 0);
|
|
8364
|
+
const newest = rows.reduce((best, row) => !best || row.last > best.last ? row : best, null);
|
|
8365
|
+
for (const row of rows) {
|
|
8366
|
+
const floor = row === newest ? row.count - 1 : row.count;
|
|
8367
|
+
baselines.set(row.key, Math.min(baselines.get(row.key) ?? floor, floor));
|
|
8368
|
+
}
|
|
8369
|
+
this.nudgeBaselines = baselines;
|
|
8370
|
+
}
|
|
8152
8371
|
/** The first live composer of a spawn fixes what is history (see {@link nudgeBaselines}). */
|
|
8153
8372
|
takeNudgeBaselines(pane, patterns) {
|
|
8154
8373
|
if (!this.isCodexLivePaneSnapshot(pane))
|
|
8155
8374
|
return;
|
|
8156
8375
|
const baselines = this.nudgeBaselines ?? new Map();
|
|
8157
8376
|
for (const ep of patterns) {
|
|
8158
|
-
if (ep.action !== "nudge_continue")
|
|
8377
|
+
if (ep.action !== "nudge_continue" && !ep.historyUntilLiveComposer)
|
|
8159
8378
|
continue;
|
|
8160
8379
|
const key = Daemon.errorPatternKey(ep);
|
|
8161
8380
|
const count = Daemon.countOccurrences(ep.pattern, pane);
|
|
@@ -8239,6 +8458,7 @@ export class Daemon extends EventEmitter {
|
|
|
8239
8458
|
*/
|
|
8240
8459
|
endDialogEpisode(clearParked = true) {
|
|
8241
8460
|
this.dialogAnswers = null;
|
|
8461
|
+
this.retireParkedNotice("answer");
|
|
8242
8462
|
if (clearParked)
|
|
8243
8463
|
this.trackDialogParked(null);
|
|
8244
8464
|
}
|
|
@@ -8265,8 +8485,11 @@ export class Daemon extends EventEmitter {
|
|
|
8265
8485
|
*/
|
|
8266
8486
|
noteDialogAnswer(dialog, outcome, screen) {
|
|
8267
8487
|
const key = Daemon.answerKey(dialog);
|
|
8268
|
-
if (!this.dialogAnswers || this.dialogAnswers.key !== key)
|
|
8269
|
-
this.dialogAnswers
|
|
8488
|
+
if (!this.dialogAnswers || this.dialogAnswers.key !== key) {
|
|
8489
|
+
if (this.dialogAnswers)
|
|
8490
|
+
this.retireParkedNotice("answer"); // #2897: a different screen: the reported one is over
|
|
8491
|
+
this.dialogAnswers = { key, ignored: 0, reported: false, screen, lastAnswerAt: 0 };
|
|
8492
|
+
} // the exact screen being counted: every answer in the episode saw this same one
|
|
8270
8493
|
const episode = this.dialogAnswers;
|
|
8271
8494
|
if (outcome !== "ignored") {
|
|
8272
8495
|
this.endDialogEpisode(outcome === "took");
|
|
@@ -8282,6 +8505,8 @@ export class Daemon extends EventEmitter {
|
|
|
8282
8505
|
this.dialogParkedReported = true;
|
|
8283
8506
|
const holdsDeliveries = dialog.blocksDelivery === true || dialog.holdOnly === true;
|
|
8284
8507
|
this.logger.warn({ dialog: dialog.description, attempts: episode.ignored, holdsDeliveries }, "Runtime dialog is ignoring AgEnD's answer — reporting for a human");
|
|
8508
|
+
// #2897: a dialog the parked clock tracks keeps its notice until it leaves the pane; any other, until this episode ends.
|
|
8509
|
+
this.parkedNoticeOut = this.dialogParkedSince !== 0 && this.dialogParkedKey === Daemon.dialogKey(dialog) ? "parked" : "answer";
|
|
8285
8510
|
this.emit("dialog_answer_ignored", { name: this.name, description: dialog.description, attempts: episode.ignored, holdsDeliveries });
|
|
8286
8511
|
}
|
|
8287
8512
|
/**
|
|
@@ -8355,6 +8580,24 @@ export class Daemon extends EventEmitter {
|
|
|
8355
8580
|
this.remoteCardAnswering = false;
|
|
8356
8581
|
}
|
|
8357
8582
|
}
|
|
8583
|
+
/** #2893: a held dialog's own account of why no automatic answer took it (RuntimeDialog.heldReason); {} when none. */
|
|
8584
|
+
static heldReasonOf(dialog, pane) {
|
|
8585
|
+
try {
|
|
8586
|
+
return dialog.heldReason?.(pane) ?? {};
|
|
8587
|
+
}
|
|
8588
|
+
catch {
|
|
8589
|
+
return {};
|
|
8590
|
+
}
|
|
8591
|
+
}
|
|
8592
|
+
/** What an answered dialog says about the answer (RuntimeDialog.answerDetail); {} when it gives none. */
|
|
8593
|
+
static answerDetailOf(dialog, pane) {
|
|
8594
|
+
try {
|
|
8595
|
+
return dialog.answerDetail?.(pane) ?? {};
|
|
8596
|
+
}
|
|
8597
|
+
catch {
|
|
8598
|
+
return {};
|
|
8599
|
+
}
|
|
8600
|
+
}
|
|
8358
8601
|
/**
|
|
8359
8602
|
* Remember how long a blocking dialog has been on screen. Once it has
|
|
8360
8603
|
* outlived DIALOG_PARKED_NOTIFY_MS — the auto-dismiss did not take, or the
|
|
@@ -8363,14 +8606,24 @@ export class Daemon extends EventEmitter {
|
|
|
8363
8606
|
* is safe. Identity is the pattern, so the delivery gate and the runtime
|
|
8364
8607
|
* scanner (same canonical list) never reset each other's clock.
|
|
8365
8608
|
*/
|
|
8609
|
+
/** #2897: the notice out for a dialog is out of date (its `owner` ended): the fleet takes it back. */
|
|
8610
|
+
retireParkedNotice(owner) {
|
|
8611
|
+
if (this.parkedNoticeOut !== owner)
|
|
8612
|
+
return;
|
|
8613
|
+
this.parkedNoticeOut = null;
|
|
8614
|
+
this.emit("dialog_unparked", { name: this.name });
|
|
8615
|
+
}
|
|
8366
8616
|
trackDialogParked(dialog) {
|
|
8367
8617
|
if (!dialog) {
|
|
8368
8618
|
if (this.dialogParkedSince !== 0)
|
|
8369
8619
|
this.logger.info({ dialog: this.dialogParkedKey }, "Blocking dialog is gone from the pane");
|
|
8370
8620
|
this.dialogParkedSince = 0;
|
|
8371
8621
|
this.dialogParkedKey = null;
|
|
8622
|
+
this.heldReasonLoggedKey = null;
|
|
8372
8623
|
this.dialogParkedInteractionEpisode = null;
|
|
8373
8624
|
this.dialogParkedReported = false;
|
|
8625
|
+
// #2897: whatever it posted is out of date — the fleet takes its notices back.
|
|
8626
|
+
this.retireParkedNotice("parked");
|
|
8374
8627
|
return;
|
|
8375
8628
|
}
|
|
8376
8629
|
const key = Daemon.dialogKey(dialog);
|
|
@@ -8382,6 +8635,9 @@ export class Daemon extends EventEmitter {
|
|
|
8382
8635
|
if (episode !== null)
|
|
8383
8636
|
this.dialogParkedInteractionEpisode = episode;
|
|
8384
8637
|
if (this.dialogParkedSince === 0 || this.dialogParkedKey !== key) {
|
|
8638
|
+
// #2897: a different dialog replaced the one being tracked — that one's notices are out of date.
|
|
8639
|
+
if (this.dialogParkedSince !== 0)
|
|
8640
|
+
this.retireParkedNotice("parked");
|
|
8385
8641
|
this.dialogParkedSince = Date.now();
|
|
8386
8642
|
this.dialogParkedKey = key;
|
|
8387
8643
|
this.dialogParkedReported = false;
|
|
@@ -8393,8 +8649,10 @@ export class Daemon extends EventEmitter {
|
|
|
8393
8649
|
this.dialogParkedReported = true;
|
|
8394
8650
|
if (!this.dialogParkedReported && Date.now() - this.dialogParkedSince >= (unrecognised ? 0 : DIALOG_PARKED_NOTIFY_MS)) {
|
|
8395
8651
|
this.dialogParkedReported = true;
|
|
8652
|
+
this.parkedNoticeOut = "parked";
|
|
8396
8653
|
this.logger.warn({ dialog: dialog.description, parkedForMs: Date.now() - this.dialogParkedSince }, "CLI dialog is still on screen — not auto-answering, reporting for a human");
|
|
8397
8654
|
this.emit("dialog_parked", { name: this.name, description: dialog.description, holdOnly: dialog.holdOnly === true,
|
|
8655
|
+
...(dialog.parkedAdvice ? { advice: dialog.parkedAdvice } : {}),
|
|
8398
8656
|
...(episode !== null && interaction.kind ? { kind: interaction.kind, episode, backend: this.config.backend ?? "claude-code" } : {}),
|
|
8399
8657
|
...(unrecognised ? { unrecognised: true, question: this.unrecognisedPrompt?.shape.question ?? null } : {}) });
|
|
8400
8658
|
}
|
|
@@ -8736,13 +8994,7 @@ export class Daemon extends EventEmitter {
|
|
|
8736
8994
|
return "busy";
|
|
8737
8995
|
}
|
|
8738
8996
|
}
|
|
8739
|
-
|
|
8740
|
-
* Like waitForPaneIdleForDelivery, plus the bottom-anchored prompt check for
|
|
8741
|
-
* Enter-dropping TUIs. Polls at a coarse cadence (each poll is a capture-pane)
|
|
8742
|
-
* only after the pane is already quiet, so the common fast-idle delivery still
|
|
8743
|
-
* costs a single capture.
|
|
8744
|
-
*/
|
|
8745
|
-
async waitForPaneReadyForDelivery(windowId, timeoutMs = 30 * 60_000) {
|
|
8997
|
+
async waitForPaneReadyForDelivery(windowId, timeoutMs = 30 * 60_000, stop) {
|
|
8746
8998
|
// Every backend polls: readiness now also means "no blocking dialog on
|
|
8747
8999
|
// screen", and a dialog disappears without any output edge the silence
|
|
8748
9000
|
// gate could see.
|
|
@@ -8755,9 +9007,10 @@ export class Daemon extends EventEmitter {
|
|
|
8755
9007
|
const remaining = deadline - Date.now();
|
|
8756
9008
|
if (remaining <= 0)
|
|
8757
9009
|
return false;
|
|
8758
|
-
const idle = await this.waitForPaneIdleForDelivery(windowId, remaining)
|
|
8759
|
-
|
|
8760
|
-
|
|
9010
|
+
const idle = stop ? await this.waitForPaneIdleForDelivery(windowId, remaining, stop)
|
|
9011
|
+
: await this.waitForPaneIdleForDelivery(windowId, remaining);
|
|
9012
|
+
if (idle !== true)
|
|
9013
|
+
return idle;
|
|
8761
9014
|
// The idle wait just resolved, so re-reading the silence gate would only
|
|
8762
9015
|
// add a stale-cache race; ask the pane the remaining questions directly.
|
|
8763
9016
|
const transient = await this.probeInputTransient();
|
|
@@ -8777,6 +9030,8 @@ export class Daemon extends EventEmitter {
|
|
|
8777
9030
|
this.retireInputTransients("transient stalled");
|
|
8778
9031
|
return false;
|
|
8779
9032
|
}
|
|
9033
|
+
if (stop && await stop())
|
|
9034
|
+
return "stopped";
|
|
8780
9035
|
await new Promise(r => setTimeout(r, BOTTOM_READY_POLL_MS));
|
|
8781
9036
|
continue;
|
|
8782
9037
|
}
|
|
@@ -8786,6 +9041,8 @@ export class Daemon extends EventEmitter {
|
|
|
8786
9041
|
this.logger.error({ probes: unknownStreak }, "Pane stayed unreadable during the input-transient probe — refusing to deliver blind");
|
|
8787
9042
|
return false;
|
|
8788
9043
|
}
|
|
9044
|
+
if (stop && await stop())
|
|
9045
|
+
return "stopped";
|
|
8789
9046
|
await new Promise(r => setTimeout(r, BOTTOM_READY_POLL_MS));
|
|
8790
9047
|
continue;
|
|
8791
9048
|
}
|
|
@@ -8807,6 +9064,8 @@ export class Daemon extends EventEmitter {
|
|
|
8807
9064
|
unknownStreak = 0;
|
|
8808
9065
|
}
|
|
8809
9066
|
}
|
|
9067
|
+
if (stop && await stop())
|
|
9068
|
+
return "stopped";
|
|
8810
9069
|
await new Promise(r => setTimeout(r, BOTTOM_READY_POLL_MS));
|
|
8811
9070
|
}
|
|
8812
9071
|
}
|
|
@@ -9214,9 +9473,11 @@ export class Daemon extends EventEmitter {
|
|
|
9214
9473
|
return observedAt >= lastOutputAt
|
|
9215
9474
|
&& observedAt >= this.controlClient.getObservationResetAt();
|
|
9216
9475
|
}
|
|
9217
|
-
waitForPaneIdleForDelivery(windowId, timeoutMs = 30 * 60_000) {
|
|
9476
|
+
waitForPaneIdleForDelivery(windowId, timeoutMs = 30 * 60_000, stop) {
|
|
9218
9477
|
if (!this.controlClient)
|
|
9219
9478
|
return Promise.resolve(true);
|
|
9479
|
+
if (stop)
|
|
9480
|
+
return this.waitForPaneIdleOrStop(windowId, timeoutMs, stop);
|
|
9220
9481
|
if (!this.canTrustRedrawIdleState()) {
|
|
9221
9482
|
return this.controlClient.waitUntilIdle(windowId, timeoutMs);
|
|
9222
9483
|
}
|
|
@@ -9236,6 +9497,44 @@ export class Daemon extends EventEmitter {
|
|
|
9236
9497
|
}, timeoutMs);
|
|
9237
9498
|
});
|
|
9238
9499
|
}
|
|
9500
|
+
/**
|
|
9501
|
+
* #2770: waitForPaneIdleForDelivery that also ends as soon as `stop` says so ("stopped"). The same idle readings
|
|
9502
|
+
* (tmux silence, or the state machine where its idle is trusted), polled here so nothing outlives a stopped wait.
|
|
9503
|
+
*/
|
|
9504
|
+
waitForPaneIdleOrStop(windowId, timeoutMs, stop) {
|
|
9505
|
+
const control = this.controlClient;
|
|
9506
|
+
const trusted = this.canTrustRedrawIdleState();
|
|
9507
|
+
if (trusted && this.isPaneIdleForDelivery(windowId))
|
|
9508
|
+
return Promise.resolve(true);
|
|
9509
|
+
return new Promise(resolve => {
|
|
9510
|
+
let done = false, asking = false, askedAt = -Infinity;
|
|
9511
|
+
const finish = (outcome) => {
|
|
9512
|
+
if (done)
|
|
9513
|
+
return;
|
|
9514
|
+
done = true;
|
|
9515
|
+
clearInterval(check);
|
|
9516
|
+
clearTimeout(timer);
|
|
9517
|
+
resolve(outcome);
|
|
9518
|
+
};
|
|
9519
|
+
// The same idle readings as the plain wait: tmux silence (abandoned once this wait ends), or the state machine.
|
|
9520
|
+
if (!trusted) {
|
|
9521
|
+
control.waitUntilIdle(windowId, timeoutMs, () => done).then(idle => finish(idle), () => finish(false));
|
|
9522
|
+
}
|
|
9523
|
+
const check = setInterval(() => {
|
|
9524
|
+
if (trusted && this.isPaneIdleForDelivery(windowId)) {
|
|
9525
|
+
finish(true);
|
|
9526
|
+
return;
|
|
9527
|
+
}
|
|
9528
|
+
if (asking || performance.now() - askedAt < WAIT_STOP_POLL_MS)
|
|
9529
|
+
return;
|
|
9530
|
+
asking = true;
|
|
9531
|
+
askedAt = performance.now();
|
|
9532
|
+
stop().then(stopNow => { asking = false; if (stopNow)
|
|
9533
|
+
finish("stopped"); }, err => { asking = false; this.logger.debug({ err }, "Delivery wait stop check failed — still waiting"); });
|
|
9534
|
+
}, 50);
|
|
9535
|
+
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
9536
|
+
});
|
|
9537
|
+
}
|
|
9239
9538
|
/**
|
|
9240
9539
|
* Antigravity has always opted into pane-state delivery readiness because its
|
|
9241
9540
|
* footer redraws while idle. Codex's structural idle proof is useful only
|
|
@@ -9248,7 +9547,7 @@ export class Daemon extends EventEmitter {
|
|
|
9248
9547
|
canTrustRedrawIdleState() {
|
|
9249
9548
|
if (this.backend?.hasPeriodicPaneRedraw?.() === true)
|
|
9250
9549
|
return true;
|
|
9251
|
-
return this.instanceStateMonitorActive && !!this.
|
|
9550
|
+
return this.instanceStateMonitorActive && !!this.periodicIdleProof();
|
|
9252
9551
|
}
|
|
9253
9552
|
/**
|
|
9254
9553
|
* The write half of a delivery: paste → Enter → confirm, with retries.
|
|
@@ -10723,96 +11022,12 @@ export class Daemon extends EventEmitter {
|
|
|
10723
11022
|
reply(written, undefined);
|
|
10724
11023
|
return;
|
|
10725
11024
|
}
|
|
10726
|
-
if (tool === "export_web_mod") {
|
|
10727
|
-
// The fleet builds the file (whole, or in parts); it is written here, in the working directory.
|
|
10728
|
-
const first = await ask(args);
|
|
10729
|
-
if (first.error) {
|
|
10730
|
-
reply(null, first.error);
|
|
10731
|
-
return;
|
|
10732
|
-
}
|
|
10733
|
-
let failed;
|
|
10734
|
-
const firstResult = (first.result && typeof first.result === "object" ? first.result : {});
|
|
10735
|
-
const got = await receiveFiles(firstResult, async (payload) => {
|
|
10736
|
-
const r = await ask(payload);
|
|
10737
|
-
if (r.error) {
|
|
10738
|
-
failed = r.error;
|
|
10739
|
-
return { error: r.error };
|
|
10740
|
-
}
|
|
10741
|
-
return (r.result && typeof r.result === "object" ? r.result : {});
|
|
10742
|
-
}, { name: MOD_FILE, idKey: "transfer", what: "export", maxParts: MOD_MAX_PARTS });
|
|
10743
|
-
if (failed) {
|
|
10744
|
-
reply(null, failed);
|
|
10745
|
-
return;
|
|
10746
|
-
}
|
|
10747
|
-
if ("error" in got) {
|
|
10748
|
-
reply(got, undefined);
|
|
10749
|
-
return;
|
|
10750
|
-
}
|
|
10751
|
-
const written = await writeModFile(got.files, { workspace: this.config.working_directory, configuredWorkspace: this.config.working_directory, stateDir: getAgendHome(), instance: this.name });
|
|
10752
|
-
if ("error" in written) {
|
|
10753
|
-
reply(null, `export_web_mod: ${written.error}`);
|
|
10754
|
-
return;
|
|
10755
|
-
}
|
|
10756
|
-
reply({ ...written, ...(firstResult.effects_omitted ? { effects_omitted: firstResult.effects_omitted } : {}) }, undefined);
|
|
10757
|
-
return;
|
|
10758
|
-
}
|
|
10759
|
-
if (tool === "propose_web_mod") {
|
|
10760
|
-
const read = await readAgentFile(args.file, { workspace: this.config.working_directory, configuredWorkspace: this.config.working_directory, stateDir: getAgendHome(), instance: this.name, maxBytes: MOD_UPLOAD_MAX_BYTES });
|
|
10761
|
-
const named = typeof args.file === "string" ? args.file.slice(0, 200) : "the file argument";
|
|
10762
|
-
if (!read.ok) {
|
|
10763
|
-
reply(null, `propose_web_mod: ${named} — ${read.reason}`);
|
|
10764
|
-
return;
|
|
10765
|
-
}
|
|
10766
|
-
let failed;
|
|
10767
|
-
const answer = await sendAssetInParts("mod", read.bytes, async (payload) => {
|
|
10768
|
-
const r = await ask(payload);
|
|
10769
|
-
if (r.error) {
|
|
10770
|
-
failed = r.error;
|
|
10771
|
-
return { error: r.error };
|
|
10772
|
-
}
|
|
10773
|
-
return (r.result && typeof r.result === "object" ? r.result : {});
|
|
10774
|
-
});
|
|
10775
|
-
reply(failed ? null : answer, failed);
|
|
10776
|
-
return;
|
|
10777
|
-
}
|
|
10778
11025
|
if (tool === "office_pack_check" || tool === "office_pack_upload") {
|
|
10779
11026
|
// #2692: the folder (or the manifest's files) is read here, on the agent's side; the fleet gets bytes in parts.
|
|
10780
11027
|
const answer = await sendPackSources(args, { workspace: this.config.working_directory, configuredWorkspace: this.config.working_directory, stateDir: getAgendHome(), instance: this.name }, async (payload) => { const r = await ask(payload); return r.error ? { error: "fleet", message: r.error } : (r.result && typeof r.result === "object" ? r.result : {}); });
|
|
10781
11028
|
reply(answer, undefined);
|
|
10782
11029
|
return;
|
|
10783
11030
|
}
|
|
10784
|
-
if (tool === "upload_web_effect") {
|
|
10785
|
-
const owner = { fence: this.launchFenceEpoch, spawn: this.spawnGeneration, boot: this.bootId, ipc: this.ipcServer };
|
|
10786
|
-
const current = () => !!owner.ipc && owner.ipc === this.ipcServer && owner.fence === this.launchFenceEpoch && owner.spawn === this.spawnGeneration && owner.boot === this.bootId && !this.deliveryWritesStopping && !this.startupAborted;
|
|
10787
|
-
if (!current()) {
|
|
10788
|
-
reply(null, "upload_web_effect: launch no longer current");
|
|
10789
|
-
return;
|
|
10790
|
-
}
|
|
10791
|
-
const read = await readAgentEffect(args.dir, { workspace: this.config.working_directory, configuredWorkspace: this.config.working_directory, stateDir: getAgendHome(), instance: this.name });
|
|
10792
|
-
if (!current()) {
|
|
10793
|
-
reply(null, "upload_web_effect: launch no longer current");
|
|
10794
|
-
return;
|
|
10795
|
-
}
|
|
10796
|
-
if (!read.ok) {
|
|
10797
|
-
reply(null, `upload_web_effect: ${read.reason}`);
|
|
10798
|
-
return;
|
|
10799
|
-
}
|
|
10800
|
-
let failed;
|
|
10801
|
-
const answer = await sendEffectInParts(read.files, async (payload) => {
|
|
10802
|
-
if (!current()) {
|
|
10803
|
-
failed = "upload_web_effect: launch no longer current";
|
|
10804
|
-
return { error: failed };
|
|
10805
|
-
}
|
|
10806
|
-
const r = await ask(payload);
|
|
10807
|
-
if (r.error) {
|
|
10808
|
-
failed = r.error;
|
|
10809
|
-
return { error: r.error };
|
|
10810
|
-
}
|
|
10811
|
-
return (r.result && typeof r.result === "object" ? r.result : {});
|
|
10812
|
-
});
|
|
10813
|
-
reply(failed ? null : answer, failed);
|
|
10814
|
-
return;
|
|
10815
|
-
}
|
|
10816
11031
|
if (tool !== "set_web_theme_asset") {
|
|
10817
11032
|
const r = await ask(args);
|
|
10818
11033
|
reply(r.result, r.error);
|
|
@@ -11229,10 +11444,6 @@ export class Daemon extends EventEmitter {
|
|
|
11229
11444
|
const snapshot = this.buildSnapshotPrompt();
|
|
11230
11445
|
if (!snapshot || !this.tmux)
|
|
11231
11446
|
return;
|
|
11232
|
-
if (this.pendingInstructionsUpdate) {
|
|
11233
|
-
writeFileSync(join(this.instanceDir, "prev-instructions"), this.pendingInstructionsUpdate);
|
|
11234
|
-
this.pendingInstructionsUpdate = undefined;
|
|
11235
|
-
}
|
|
11236
11447
|
// Small delay to let the CLI fully render its ready prompt
|
|
11237
11448
|
await new Promise(r => setTimeout(r, 1_000));
|
|
11238
11449
|
admission?.();
|
|
@@ -11277,6 +11488,13 @@ export class Daemon extends EventEmitter {
|
|
|
11277
11488
|
this.recoveryNativeWork = false;
|
|
11278
11489
|
this.recoveryCancelled = false;
|
|
11279
11490
|
this.dialogAnswers = null;
|
|
11491
|
+
// #2897: a relaunched CLI shows none of the old dialogs — whatever notice was out for one is out of date.
|
|
11492
|
+
// A dialog still tracked when the new CLI shows it again is reported afresh (its old notice is gone).
|
|
11493
|
+
if (this.parkedNoticeOut) {
|
|
11494
|
+
this.parkedNoticeOut = null;
|
|
11495
|
+
this.dialogParkedReported = false;
|
|
11496
|
+
this.emit("dialog_unparked", { name: this.name });
|
|
11497
|
+
}
|
|
11280
11498
|
this.cancelCapacityNudge("the instance was stopped, paused or respawned");
|
|
11281
11499
|
this.lastErrorEpisode = null;
|
|
11282
11500
|
this.nudgeBaselines = null;
|
|
@@ -11505,6 +11723,8 @@ export class Daemon extends EventEmitter {
|
|
|
11505
11723
|
// as this backend's and resumed by the next start.
|
|
11506
11724
|
this.startupAdmission?.();
|
|
11507
11725
|
this.setAsideForeignSession();
|
|
11726
|
+
// A crash-loop-resume boot should attempt resume even though skipResume is false:
|
|
11727
|
+
// bootCrashLoopResume set at boot means we had a crash loop but try resume first.
|
|
11508
11728
|
const attemptedResume = !this.skipResume;
|
|
11509
11729
|
// A resume launch may get a longer budget than a fresh one (kiro: the
|
|
11510
11730
|
// conversation must come back from the backend before anything paints).
|
|
@@ -11589,6 +11809,20 @@ export class Daemon extends EventEmitter {
|
|
|
11589
11809
|
}
|
|
11590
11810
|
this.setSessionAside(proven ? "cli_reported_no_conversation" : "resume_failed_repeatedly");
|
|
11591
11811
|
this.skipResume = true;
|
|
11812
|
+
// If this was a crash-loop-resume attempt, the session is now proven
|
|
11813
|
+
// broken. Persist resumeFailed so the next restart goes fresh without
|
|
11814
|
+
// attempting resume again.
|
|
11815
|
+
if (this.bootCrashLoopResume) {
|
|
11816
|
+
try {
|
|
11817
|
+
writeFileSync(join(this.instanceDir, "crash-state.json"), JSON.stringify({
|
|
11818
|
+
resumeFailed: true,
|
|
11819
|
+
failedAt: Date.now(),
|
|
11820
|
+
}));
|
|
11821
|
+
}
|
|
11822
|
+
catch { /* best effort */ }
|
|
11823
|
+
this.logger.warn("Crash-loop-resume: session abandoned — next boot will go fresh");
|
|
11824
|
+
this.bootCrashLoopResume = false;
|
|
11825
|
+
}
|
|
11592
11826
|
}
|
|
11593
11827
|
// A fresh start that also failed retries once, as before. It never clears
|
|
11594
11828
|
// a session: nothing about a failed fresh launch says the stored
|
|
@@ -11606,9 +11840,37 @@ export class Daemon extends EventEmitter {
|
|
|
11606
11840
|
this.startupAdmission?.();
|
|
11607
11841
|
throw new Error("CLI failed to start after retry");
|
|
11608
11842
|
}
|
|
11843
|
+
// Fresh fallback succeeded. Clear any resumeFailed marker written during this
|
|
11844
|
+
// boot's setSessionAside path, so the next normal restart is not forced fresh.
|
|
11845
|
+
// NOTE: bootCrashLoopResume is already false here (cleared by setSessionAside path),
|
|
11846
|
+
// so check the marker directly rather than relying on the flag.
|
|
11847
|
+
try {
|
|
11848
|
+
const p = join(this.instanceDir, "crash-state.json");
|
|
11849
|
+
if (existsSync(p)) {
|
|
11850
|
+
const s = JSON.parse(readFileSync(p, "utf-8"));
|
|
11851
|
+
if (s?.resumeFailed === true)
|
|
11852
|
+
unlinkSync(p);
|
|
11853
|
+
}
|
|
11854
|
+
}
|
|
11855
|
+
catch { /* best effort */ }
|
|
11609
11856
|
}
|
|
11610
11857
|
else if (attemptedResume) {
|
|
11611
11858
|
resumedSuccessfully = true;
|
|
11859
|
+
if (this.bootCrashLoopResume) {
|
|
11860
|
+
// Resume succeeded after a crash loop — clear any stale resumeFailed marker
|
|
11861
|
+
// that might have been written by a concurrent write or a previous attempt,
|
|
11862
|
+
// so the next restart is not forced fresh.
|
|
11863
|
+
this.bootCrashLoopResume = false;
|
|
11864
|
+
try {
|
|
11865
|
+
const p = join(this.instanceDir, "crash-state.json");
|
|
11866
|
+
if (existsSync(p)) {
|
|
11867
|
+
const s = JSON.parse(readFileSync(p, "utf-8"));
|
|
11868
|
+
if (s?.resumeFailed === true)
|
|
11869
|
+
unlinkSync(p);
|
|
11870
|
+
}
|
|
11871
|
+
}
|
|
11872
|
+
catch { /* best effort */ }
|
|
11873
|
+
}
|
|
11612
11874
|
this.clearUnprovenResumeFailures();
|
|
11613
11875
|
// A resume needs the backend: its success is positive proof the backend
|
|
11614
11876
|
// is reachable again (a fresh prompt is local and proves nothing).
|
|
@@ -12064,10 +12326,13 @@ export class Daemon extends EventEmitter {
|
|
|
12064
12326
|
}
|
|
12065
12327
|
/** Kill the entire process tree of the current tmux pane (CLI + MCP server). */
|
|
12066
12328
|
async killProcessTree(signal = "SIGTERM", reason = "unspecified") {
|
|
12067
|
-
|
|
12329
|
+
const windowId = this.tmux?.getWindowId();
|
|
12330
|
+
// Before createWindow succeeds there is no pane owned by this daemon.
|
|
12331
|
+
// tmux interprets an empty selector as the session's active window.
|
|
12332
|
+
if (!windowId?.trim())
|
|
12068
12333
|
return;
|
|
12069
12334
|
try {
|
|
12070
|
-
const pid = await TmuxManager.getPanePid(this.tmuxSessionName,
|
|
12335
|
+
const pid = await TmuxManager.getPanePid(this.tmuxSessionName, windowId);
|
|
12071
12336
|
if (pid) {
|
|
12072
12337
|
this.noteStopRequest(signal, reason);
|
|
12073
12338
|
process.kill(-pid, signal);
|
|
@@ -12239,113 +12504,40 @@ export class Daemon extends EventEmitter {
|
|
|
12239
12504
|
return this.museRelayFallbackInFlight;
|
|
12240
12505
|
}
|
|
12241
12506
|
/**
|
|
12242
|
-
*
|
|
12243
|
-
*
|
|
12244
|
-
*
|
|
12245
|
-
* testable: warmupNoticeAction's own tests pass even if this call site stops
|
|
12246
|
-
* consulting it, which is exactly the bug this path had — the decision was
|
|
12247
|
-
* made inline and never asked whether the backend needed telling at all.
|
|
12248
|
-
*
|
|
12249
|
-
* Never throws: a missed reload notice must not fail a spawn.
|
|
12250
|
-
*/
|
|
12251
|
-
async runWarmupInstructionNotice() {
|
|
12252
|
-
try {
|
|
12253
|
-
const action = warmupNoticeAction({
|
|
12254
|
-
warmupNeeded: this.warmupNeeded,
|
|
12255
|
-
backend: this.backend ?? undefined,
|
|
12256
|
-
pasteQueueDepth: this.pasteQueueDepth,
|
|
12257
|
-
});
|
|
12258
|
-
if (action === "skip") {
|
|
12259
|
-
this.logger.debug(this.warmupNeeded
|
|
12260
|
-
? "Warmup skipped — this backend reloads instructions on resume"
|
|
12261
|
-
: "Warmup skipped — instructions unchanged");
|
|
12262
|
-
// When the backend learns of the change on its own, record what it now
|
|
12263
|
-
// holds: leaving prev-instructions behind would keep every later
|
|
12264
|
-
// restart comparing against a value the agent has long since replaced.
|
|
12265
|
-
if (this.warmupNeeded) {
|
|
12266
|
-
try {
|
|
12267
|
-
writeFileSync(join(this.instanceDir, "prev-instructions"), this.lastBuiltInstructions);
|
|
12268
|
-
}
|
|
12269
|
-
catch { /* best effort */ }
|
|
12270
|
-
}
|
|
12271
|
-
return;
|
|
12272
|
-
}
|
|
12273
|
-
if (action === "defer") {
|
|
12274
|
-
this.logger.debug("Warmup deferred — no pending inbound messages");
|
|
12275
|
-
// Convert to pendingInstructionsNotice so it fires on next real message.
|
|
12276
|
-
this.pendingInstructionsNotice = true;
|
|
12277
|
-
try {
|
|
12278
|
-
writeFileSync(join(this.instanceDir, "prev-instructions"), this.lastBuiltInstructions);
|
|
12279
|
-
}
|
|
12280
|
-
catch { }
|
|
12281
|
-
return;
|
|
12282
|
-
}
|
|
12283
|
-
const wid = existsSync(join(this.instanceDir, "window-id"))
|
|
12284
|
-
? readFileSync(join(this.instanceDir, "window-id"), "utf-8").trim() : "";
|
|
12285
|
-
if (wid && this.controlClient) {
|
|
12286
|
-
await this.controlClient.waitForIdle(wid, 120_000);
|
|
12287
|
-
}
|
|
12288
|
-
else {
|
|
12289
|
-
await new Promise(r => setTimeout(r, 5000));
|
|
12290
|
-
}
|
|
12291
|
-
// This path only runs when pasteQueueDepth > 0 — i.e. exactly when a real
|
|
12292
|
-
// delivery is already in flight or queued. Without the lock the notice and
|
|
12293
|
-
// that delivery race into the same pane.
|
|
12294
|
-
const told = await this.paneWriteLock.run(() => this.submitSystemPaste(buildInstructionReloadNotice(this.backend?.binaryName ?? "unknown", this.name, this.instanceDir, this.backend?.instructionsSource?.()), "instruction-reload-notice"));
|
|
12295
|
-
if (!told) {
|
|
12296
|
-
// Recording the new instructions here would mark the agent as told about
|
|
12297
|
-
// a notice it never received, and every later restart would then skip
|
|
12298
|
-
// the reload. Leave the snapshot behind and hand it to the deferred
|
|
12299
|
-
// notice, which fires on the next real message.
|
|
12300
|
-
this.logger.warn("Instruction reload notice not confirmed — deferring it to the next message");
|
|
12301
|
-
this.pendingInstructionsNotice = true;
|
|
12302
|
-
return;
|
|
12303
|
-
}
|
|
12304
|
-
// Record the value the agent has now been told about so the next
|
|
12305
|
-
// unchanged restart skips the reload.
|
|
12306
|
-
try {
|
|
12307
|
-
writeFileSync(join(this.instanceDir, "prev-instructions"), this.lastBuiltInstructions);
|
|
12308
|
-
}
|
|
12309
|
-
catch { /* best effort */ }
|
|
12310
|
-
this.logger.debug("Warmup sent after idle");
|
|
12311
|
-
}
|
|
12312
|
-
catch { /* non-fatal */ }
|
|
12313
|
-
}
|
|
12314
|
-
/**
|
|
12315
|
-
* #1633 (#1639 review): the pending notice as the record stands NOW, just before the paste — it was decided at launch,
|
|
12316
|
-
* and the record may have changed while the delivery waited. Unreadable → nothing is sent (and nothing pending);
|
|
12317
|
-
* entries recorded meanwhile are dropped from it; none left → nothing.
|
|
12507
|
+
* #1633 (#1639 review): the pending notice as things stand NOW, just before the paste — it was decided at launch, and
|
|
12508
|
+
* the record, a switch or the instance's tools may have changed while the delivery waited. Planned again from the
|
|
12509
|
+
* record (#2800: the running release only, tools it has now); nothing left to tell → recorded, nothing pending.
|
|
12318
12510
|
*/
|
|
12319
12511
|
currentWhatsNew(pending) {
|
|
12320
|
-
const
|
|
12321
|
-
|
|
12512
|
+
const plan = planWhatsNew({ record: readWhatsNewRecord(this.instanceDir), fresh: false, version: pending.version,
|
|
12513
|
+
roles: this.whatsNewRoles(), tools: this.whatsNewTools() });
|
|
12514
|
+
if (!plan || "silent" in plan) {
|
|
12322
12515
|
this.pendingWhatsNew = null;
|
|
12516
|
+
if (plan)
|
|
12517
|
+
this.recordWhatsNewSent({ ids: plan.silent, version: plan.version });
|
|
12323
12518
|
return null;
|
|
12324
12519
|
}
|
|
12325
|
-
|
|
12326
|
-
|
|
12327
|
-
|
|
12520
|
+
this.pendingWhatsNew = plan;
|
|
12521
|
+
return plan;
|
|
12522
|
+
}
|
|
12523
|
+
recordWhatsNewSent(told) {
|
|
12524
|
+
if (this.pendingWhatsNew && told.ids.every(id => this.pendingWhatsNew.ids.includes(id)))
|
|
12328
12525
|
this.pendingWhatsNew = null;
|
|
12329
|
-
|
|
12526
|
+
try {
|
|
12527
|
+
recordWhatsNew(this.instanceDir, told.ids, told.version);
|
|
12330
12528
|
}
|
|
12331
|
-
|
|
12332
|
-
|
|
12333
|
-
// #2163: rebuilt as planned — only the entries this instance's roles are told; the rest are recorded with them.
|
|
12334
|
-
const roles = this.whatsNewRoles();
|
|
12335
|
-
const shown = agentWhatsNew(readCanvasWorkerEnabled(join(this.instanceDir, "..", ".."))).filter(e => ids.includes(e.id) && (!e.audience || e.audience.some(r => roles.includes(r))));
|
|
12336
|
-
if (!shown.length) {
|
|
12337
|
-
this.pendingWhatsNew = null;
|
|
12338
|
-
try {
|
|
12339
|
-
recordWhatsNew(this.instanceDir, ids, pending.version);
|
|
12340
|
-
}
|
|
12341
|
-
catch (err) {
|
|
12342
|
-
this.logger.warn({ err }, "what's-new record not written — the notice may repeat once");
|
|
12343
|
-
}
|
|
12344
|
-
return null;
|
|
12529
|
+
catch (err) {
|
|
12530
|
+
this.logger.warn({ err }, "what's-new record not written — the notice may repeat once");
|
|
12345
12531
|
}
|
|
12346
|
-
|
|
12347
|
-
|
|
12348
|
-
|
|
12532
|
+
}
|
|
12533
|
+
/**
|
|
12534
|
+
* #2800: the tools this instance's agend MCP server lists now — the same filters (tool set, dashboard switches) — so a
|
|
12535
|
+
* notice names only changes to tools it has.
|
|
12536
|
+
*/
|
|
12537
|
+
whatsNewTools() {
|
|
12538
|
+
const allowed = toolsFor(resolveToolSet(this.config, this.name));
|
|
12539
|
+
const listed = withoutFeatures(TOOLS.filter(t => allowed.has(t.name)), dashboardFeaturesOff(dashboardAccessOf(this.config)));
|
|
12540
|
+
return new Set(listed.map(t => t.name));
|
|
12349
12541
|
}
|
|
12350
12542
|
/**
|
|
12351
12543
|
* #2163: who this instance is to the what's-new notices — a ClassicBot room's agent, General, or a fleet worker (with
|
|
@@ -12362,7 +12554,8 @@ export class Daemon extends EventEmitter {
|
|
|
12362
12554
|
/** #1633: what this instance has not been told about its AgEnD version yet (see agent-whats-new.ts). */
|
|
12363
12555
|
prepareWhatsNew(fresh, version = agendVersion()) {
|
|
12364
12556
|
try {
|
|
12365
|
-
const plan = planWhatsNew({ record: readWhatsNewRecord(this.instanceDir), fresh, version, roles: this.whatsNewRoles(),
|
|
12557
|
+
const plan = planWhatsNew({ record: readWhatsNewRecord(this.instanceDir), fresh, version, roles: this.whatsNewRoles(),
|
|
12558
|
+
tools: this.whatsNewTools() });
|
|
12366
12559
|
if (!plan) {
|
|
12367
12560
|
this.pendingWhatsNew = null;
|
|
12368
12561
|
return;
|
|
@@ -12373,8 +12566,7 @@ export class Daemon extends EventEmitter {
|
|
|
12373
12566
|
return;
|
|
12374
12567
|
}
|
|
12375
12568
|
this.pendingWhatsNew = plan;
|
|
12376
|
-
this.
|
|
12377
|
-
this.logger.info({ entries: plan.ids }, "what's-new notice pending for the next delivery");
|
|
12569
|
+
this.logger.debug({ entries: plan.ids }, "what's-new notice pending for the next delivery");
|
|
12378
12570
|
}
|
|
12379
12571
|
catch (err) {
|
|
12380
12572
|
this.logger.debug({ err }, "what's-new not prepared");
|
|
@@ -12385,41 +12577,15 @@ export class Daemon extends EventEmitter {
|
|
|
12385
12577
|
this.startupAdmission?.();
|
|
12386
12578
|
const launchFence = this.launchFenceEpoch;
|
|
12387
12579
|
const backendConfig = this.buildBackendConfig();
|
|
12388
|
-
// Compare freshly-built instructions against the last value the agent was
|
|
12389
|
-
// told about. Computed for ALL backends (not gated by
|
|
12390
|
-
// instructionsReloadedOnResume) so the warmup steering-reload can be
|
|
12391
|
-
// skipped when nothing changed.
|
|
12392
|
-
this.lastBuiltInstructions = backendConfig.instructions ?? "";
|
|
12393
12580
|
// #1633: decided before prev-instructions is first written — its absence is what says "never launched before".
|
|
12394
12581
|
this.prepareWhatsNew(!existsSync(join(this.instanceDir, "prev-instructions")));
|
|
12395
|
-
|
|
12396
|
-
|
|
12397
|
-
|
|
12582
|
+
// Record what this launch writes. #2787: a change is never announced in the pane; the CLI takes the new
|
|
12583
|
+
// instructions whenever it next reads its own file.
|
|
12584
|
+
if (backendConfig.instructions) {
|
|
12398
12585
|
try {
|
|
12399
|
-
|
|
12400
|
-
}
|
|
12401
|
-
catch { }
|
|
12402
|
-
if (!prev && backendConfig.instructions) {
|
|
12403
|
-
// First time (no prev-instructions file): write current hash, skip warmup.
|
|
12404
|
-
try {
|
|
12405
|
-
writeFileSync(prevFile, backendConfig.instructions);
|
|
12406
|
-
}
|
|
12407
|
-
catch { }
|
|
12408
|
-
this.warmupNeeded = false;
|
|
12409
|
-
}
|
|
12410
|
-
else {
|
|
12411
|
-
this.warmupNeeded = !!backendConfig.instructions && prev !== backendConfig.instructions;
|
|
12412
|
-
}
|
|
12413
|
-
// For backends that don't re-read instructions on resume (kiro/codex),
|
|
12414
|
-
// also notify the agent on next message instead of forcing a new
|
|
12415
|
-
// session. Resume is preserved so context isn't lost.
|
|
12416
|
-
if (!backendConfig.skipResume && backendNeedsPaneReloadNotice(this.backend) && this.warmupNeeded) {
|
|
12417
|
-
if (prev) {
|
|
12418
|
-
this.logger.info("Instructions changed — will notify agent on next message");
|
|
12419
|
-
this.pendingInstructionsNotice = true;
|
|
12420
|
-
}
|
|
12421
|
-
this.pendingInstructionsUpdate = backendConfig.instructions;
|
|
12586
|
+
writeFileSync(join(this.instanceDir, "prev-instructions"), backendConfig.instructions);
|
|
12422
12587
|
}
|
|
12588
|
+
catch { /* best effort */ }
|
|
12423
12589
|
}
|
|
12424
12590
|
// A backend that has to ask its CLI something before the launch command exists (OpenCode: does
|
|
12425
12591
|
// this binary take --auto?) does it here, off the event loop, bounded, and never fails the launch.
|
|
@@ -12709,7 +12875,10 @@ export class Daemon extends EventEmitter {
|
|
|
12709
12875
|
continue;
|
|
12710
12876
|
lastDialog = dialog;
|
|
12711
12877
|
this.noteCodexReserveDialog(dialog, pane);
|
|
12878
|
+
this.noteLiveIncidentDialog(dialog, pane);
|
|
12712
12879
|
cleanReadyPolls = 0;
|
|
12880
|
+
// Before the parked clock below claims it: a hold seen for the first time is said once, with its reason (#2893).
|
|
12881
|
+
const firstSight = this.dialogParkedKey !== Daemon.dialogKey(dialog);
|
|
12713
12882
|
// Start the parked clock for every delivery-blocking dialog, exact
|
|
12714
12883
|
// ones included: if the auto-dismiss keeps failing here, the human
|
|
12715
12884
|
// report should count from first sight, not from the runtime scan.
|
|
@@ -12738,14 +12907,16 @@ export class Daemon extends EventEmitter {
|
|
|
12738
12907
|
}
|
|
12739
12908
|
if (dialog.holdOnly) {
|
|
12740
12909
|
// Recognised but deliberately not answered (see RuntimeDialog.holdOnly).
|
|
12741
|
-
if (
|
|
12742
|
-
this.
|
|
12910
|
+
if (firstSight) {
|
|
12911
|
+
const reason = this.heldReasonLoggedKey !== Daemon.dialogKey(dialog) ? Daemon.heldReasonOf(dialog, pane) : {};
|
|
12912
|
+
if (Object.keys(reason).length)
|
|
12913
|
+
this.heldReasonLoggedKey = Daemon.dialogKey(dialog);
|
|
12914
|
+
this.logger.warn({ description: dialog.description, ...reason }, "Startup dialog held — not auto-answering, deliveries stay blocked");
|
|
12743
12915
|
}
|
|
12744
12916
|
this.trackDialogParked(dialog);
|
|
12745
12917
|
matched = true;
|
|
12746
12918
|
break;
|
|
12747
12919
|
}
|
|
12748
|
-
this.logger.info(`Dismissing startup dialog: ${dialog.description}`);
|
|
12749
12920
|
// Restart is exactly when inbound messages pile up, and nothing gates
|
|
12750
12921
|
// delivery on `spawning`. Take the pane lock for the key sequence so a
|
|
12751
12922
|
// queued message cannot be pasted into a half-dismissed trust dialog.
|
|
@@ -12758,6 +12929,7 @@ export class Daemon extends EventEmitter {
|
|
|
12758
12929
|
// a pane that is no longer this launch's is not read or keyed.
|
|
12759
12930
|
if (!owned())
|
|
12760
12931
|
return false;
|
|
12932
|
+
let answeredPane = pane;
|
|
12761
12933
|
if (dialog.inputBlocked) {
|
|
12762
12934
|
const currentPane = await this.tmux.capturePane();
|
|
12763
12935
|
this.startupAdmission?.();
|
|
@@ -12766,7 +12938,10 @@ export class Daemon extends EventEmitter {
|
|
|
12766
12938
|
return false;
|
|
12767
12939
|
if (!Daemon.dialogMatches(dialog, currentPane))
|
|
12768
12940
|
return false;
|
|
12941
|
+
answeredPane = currentPane;
|
|
12769
12942
|
}
|
|
12943
|
+
// Said of the screen actually answered — the one re-read under the lock (#2896 review).
|
|
12944
|
+
this.logger.info(Daemon.answerDetailOf(dialog, answeredPane), `Dismissing startup dialog: ${dialog.description}`);
|
|
12770
12945
|
if (dialog.autoResolutionKey) {
|
|
12771
12946
|
attemptedSafetyChoices.add(dialog.autoResolutionKey);
|
|
12772
12947
|
this.claimAutoDialog(dialog);
|