@songsid/agend 2.3.3-beta.1 → 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.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 +3 -6
- package/dist/agent-whats-new.js +7 -18
- 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 +7 -0
- package/dist/backend/claude-code.js +34 -28
- 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.js +2 -0
- 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 +46 -0
- 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 +21 -4
- 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 +7 -7
- 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/daemon.d.ts +73 -3
- package/dist/daemon.js +334 -121
- 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 +4 -0
- package/dist/delivery-outbox.js +12 -2
- package/dist/delivery-outbox.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 +70 -54
- package/dist/fleet-manager.js +257 -322
- 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/session-management/SKILL.md +6 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +13 -204
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +29 -67
- package/dist/instance-lifecycle.d.ts +19 -0
- package/dist/instance-lifecycle.js +52 -10
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +92 -42
- package/dist/locale.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 +19 -7
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +0 -62
- package/dist/outbound-schemas.js +3 -42
- 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 +0 -13
- package/dist/scheduler/task-board.js +0 -14
- package/dist/scheduler/task-board.js.map +1 -1
- package/dist/sd-notify.d.ts +10 -1
- package/dist/sd-notify.js +27 -2
- 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 -0
- package/dist/settings-api.js +55 -2
- 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-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 +4 -1
- package/dist/task-board-waker.js +13 -2
- package/dist/task-board-waker.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 +27 -7
- package/dist/topic-commands.js +95 -36
- 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 +8 -6
- 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-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 +1 -3
- 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 -1
- package/dist/ui/settings-strings.js +14 -14
- 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/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 +2 -11
- package/dist/web-experimental.js +8 -36
- 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 -0
- package/dist/web-settings-patch.js +6 -3
- 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/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/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
|
@@ -25,13 +25,12 @@ 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
27
|
import { resolveToolSet, toolsFor } from "./tool-permissions.js";
|
|
28
|
-
import { TOOLS
|
|
28
|
+
import { TOOLS } from "./channel/mcp-tools.js";
|
|
29
29
|
import { dashboardAccessProblem, dashboardFeaturesOff, withoutFeatures } from "./dashboard-permissions.js";
|
|
30
30
|
import { getAgendHome } from "./paths.js";
|
|
31
|
-
import {
|
|
32
|
-
import { readAgentEffect, sendEffectInParts } from "./web-effect-upload.js";
|
|
31
|
+
import { scopedFleetConfigEnvironment } from "./web-experimental.js";
|
|
33
32
|
import { readAgentFile } from "./agent-file-read.js";
|
|
34
|
-
import { ASSET_UPLOAD_MAX_BYTES,
|
|
33
|
+
import { ASSET_UPLOAD_MAX_BYTES, receivePreview, sendAssetInParts, writePreview } from "./web-asset-parts.js";
|
|
35
34
|
import { LOGIN_FLOWS } from "./login-flows.js";
|
|
36
35
|
import { ProgressAccumulator, summarizeProgress } from "./tool-progress.js";
|
|
37
36
|
import { assembleSystemPrompt, resolveWorkflowText } from "./prompt-file-ref.js";
|
|
@@ -53,7 +52,9 @@ import { formatHumanInboundMessage, prepareDeliveryWire, steerInboundEnvelope, B
|
|
|
53
52
|
import { formatCrossInstanceInboundMessage, renderCrossInstanceHandoffMetadata, } from "./cross-instance-envelope.js";
|
|
54
53
|
import { agendMessageInInput, bottomRowIsReady, inputAreaText, inputShowsPastedText, pastedTextSignature, pasteLeftInInput, strandedAgendMessageInInput } from "./pane-input-residue.js";
|
|
55
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";
|
|
56
56
|
import { t } from "./locale.js";
|
|
57
|
+
import { claudeResetTime, quotaPausedLine } from "./quota-notice.js";
|
|
57
58
|
import { MuseUsageRelay, clearMuseUsageSnapshot } from "./muse-usage-relay.js";
|
|
58
59
|
import { deliveryContentDigest } from "./delivery-outbox.js";
|
|
59
60
|
import { consumedWatches } from "./delivery-consumed-watch.js";
|
|
@@ -81,15 +82,6 @@ const WEB_THEME_IPC = Object.freeze({
|
|
|
81
82
|
set_web_widget: "fleet_set_web_widget", remove_web_widget: "fleet_remove_web_widget",
|
|
82
83
|
// #2117: its own declarative ambient.
|
|
83
84
|
set_web_ambient: "fleet_set_web_ambient", clear_web_ambient: "fleet_clear_web_ambient",
|
|
84
|
-
// r4-5b: mods — an export's file is written here, a proposal's file read here (on the agent's side).
|
|
85
|
-
export_web_mod: "fleet_export_web_mod", propose_web_mod: "fleet_propose_web_mod",
|
|
86
|
-
upload_web_effect: "fleet_upload_web_effect",
|
|
87
|
-
list_web_effects: "fleet_list_web_effects",
|
|
88
|
-
preview_web_effect: "fleet_preview_web_effect",
|
|
89
|
-
profile_web_effect: "fleet_profile_web_effect",
|
|
90
|
-
apply_web_effect: "fleet_apply_web_effect",
|
|
91
|
-
remove_web_effect: "fleet_remove_web_effect",
|
|
92
|
-
delete_web_effect: "fleet_delete_web_effect",
|
|
93
85
|
// #2578: its own character in the Office view — the args as they are (the fleet checks them).
|
|
94
86
|
office_view: "fleet_office_view", office_set: "fleet_office_set",
|
|
95
87
|
// #2692: the skin packs — check and upload read the pack here (web-office-pack-read.ts) and send its bytes in parts.
|
|
@@ -567,6 +559,8 @@ const DIALOG_ANSWER_IGNORED_MAX = 3;
|
|
|
567
559
|
* normal pace (and a fresh count).
|
|
568
560
|
*/
|
|
569
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;
|
|
570
564
|
/** How many "dialog painted just before the write → wait → retry" rounds a delivery tolerates. */
|
|
571
565
|
const LATE_DIALOG_WRITE_ROUNDS = 3;
|
|
572
566
|
/** How many times a delivery redoes itself when a spawn starts between its settle wait and its pane write. */
|
|
@@ -1263,13 +1257,26 @@ export class Daemon extends EventEmitter {
|
|
|
1263
1257
|
bootSkippedResume = false;
|
|
1264
1258
|
recoveryCrashLoopFresh = false;
|
|
1265
1259
|
recoveryFreshChoice = false;
|
|
1260
|
+
/** The current boot found a crash-loop marker and is attempting resume rather than going fresh immediately. */
|
|
1261
|
+
bootCrashLoopResume = false;
|
|
1266
1262
|
startupAborted = false;
|
|
1267
1263
|
/** First time the current on-screen blocking dialog was seen (0 = none); drives the parked report. */
|
|
1268
1264
|
dialogParkedSince = 0;
|
|
1269
1265
|
/** Identity of that dialog: its pattern, not its description (two tables may describe one screen differently). */
|
|
1270
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;
|
|
1271
1272
|
dialogParkedInteractionEpisode = null;
|
|
1272
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;
|
|
1273
1280
|
/** #2675: the card on screen that a person can answer from the chat — what it asks, since when, and whether it was posted. */
|
|
1274
1281
|
remoteCardShown = null;
|
|
1275
1282
|
/** #2675: when (performance.now, taken before its capture) the newest read the card tracker used was started. */
|
|
@@ -1320,7 +1327,7 @@ export class Daemon extends EventEmitter {
|
|
|
1320
1327
|
* cancels it when anything changed, and it fires at most once. The due time is on the monotonic clock.
|
|
1321
1328
|
*/
|
|
1322
1329
|
/**
|
|
1323
|
-
* 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
|
|
1324
1331
|
* composer (null until then): resumed scrollback, i.e. HISTORY. An occurrence is a new episode only beyond that — a
|
|
1325
1332
|
* resumed session whose scrollback holds an old capacity line must not be told to "keep going" for it (#949).
|
|
1326
1333
|
*/
|
|
@@ -1334,6 +1341,8 @@ export class Daemon extends EventEmitter {
|
|
|
1334
1341
|
interactionObservedOutputRevision = 0;
|
|
1335
1342
|
interactionCaptureSerial = 0;
|
|
1336
1343
|
interactionDialogKey = null;
|
|
1344
|
+
/** Since when (monotonic) the `answeredByAgend` dialog now on screen has been withheld from the interaction state. */
|
|
1345
|
+
agendAnswerGrace = null;
|
|
1337
1346
|
acceptedInstanceSnapshot = null;
|
|
1338
1347
|
/** Generation/signature fence for safety dialogs while their screen is still visible. */
|
|
1339
1348
|
autoResolvedDialogKey = null;
|
|
@@ -1765,6 +1774,77 @@ export class Daemon extends EventEmitter {
|
|
|
1765
1774
|
// old transcript, so pre-ready scans must not lower this seeded baseline.
|
|
1766
1775
|
seededErrorCountBaselines = new Map();
|
|
1767
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
|
+
}
|
|
1768
1848
|
static errorPatternKey(ep) {
|
|
1769
1849
|
return `${ep.type}:${ep.pattern.source}`;
|
|
1770
1850
|
}
|
|
@@ -2265,19 +2345,40 @@ export class Daemon extends EventEmitter {
|
|
|
2265
2345
|
this.bootSkippedResume = false;
|
|
2266
2346
|
this.recoveryCrashLoopFresh = false;
|
|
2267
2347
|
this.recoveryFreshChoice = false;
|
|
2348
|
+
this.bootCrashLoopResume = false;
|
|
2268
2349
|
try {
|
|
2269
2350
|
if (existsSync(crashStatePath)) {
|
|
2270
2351
|
const content = readFileSync(crashStatePath, "utf-8");
|
|
2271
2352
|
try {
|
|
2272
2353
|
const state = JSON.parse(content);
|
|
2273
|
-
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.
|
|
2274
2357
|
this.skipResume = true;
|
|
2275
2358
|
this.bootSkippedResume = true;
|
|
2276
|
-
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")
|
|
2277
2368
|
&& Number.isInteger(state.crashesInWindow) && state.crashesInWindow >= 3
|
|
2278
2369
|
&& Number.isFinite(state.lastCrashAt) && state.lastCrashAt > 0;
|
|
2279
|
-
|
|
2280
|
-
|
|
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
|
+
}
|
|
2281
2382
|
}
|
|
2282
2383
|
}
|
|
2283
2384
|
finally {
|
|
@@ -2465,6 +2566,7 @@ export class Daemon extends EventEmitter {
|
|
|
2465
2566
|
.catch(e => this.logger.debug({ err: e.message }, "Ack react failed"));
|
|
2466
2567
|
};
|
|
2467
2568
|
this.transcriptMonitor.on("tool_use", (name, input) => {
|
|
2569
|
+
this.noteClaudeOutput();
|
|
2468
2570
|
this.logger.debug({ tool: name }, "Tool use");
|
|
2469
2571
|
ackIfPending();
|
|
2470
2572
|
this.recordRecentEvent({ type: "tool_use", name, preview: this.summarizeTool(name, input) });
|
|
@@ -2478,7 +2580,11 @@ export class Daemon extends EventEmitter {
|
|
|
2478
2580
|
// show only elapsed time than to leave a stale "Bash: npm test" on screen.
|
|
2479
2581
|
this.publishActivity(null);
|
|
2480
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());
|
|
2481
2586
|
this.transcriptMonitor.on("assistant_text", (text) => {
|
|
2587
|
+
this.noteClaudeOutput();
|
|
2482
2588
|
this.logger.debug({ text: text.slice(0, 200) }, "Claude response");
|
|
2483
2589
|
ackIfPending();
|
|
2484
2590
|
this.recordRecentEvent({ type: "assistant_text", preview: text.slice(0, 100) });
|
|
@@ -3626,10 +3732,21 @@ export class Daemon extends EventEmitter {
|
|
|
3626
3732
|
if (!Daemon.dialogMatches(dialog, pane))
|
|
3627
3733
|
continue;
|
|
3628
3734
|
this.noteCodexReserveDialog(dialog, pane);
|
|
3735
|
+
this.noteLiveIncidentDialog(dialog, pane);
|
|
3629
3736
|
if (dialog.blocksDelivery || dialog.holdOnly)
|
|
3630
3737
|
blockingSeen = dialog;
|
|
3631
|
-
if (dialog.holdOnly)
|
|
3632
|
-
|
|
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
|
+
}
|
|
3633
3750
|
// Reported as ignoring the daemon: the SAME request (the identity the count was made on — verbatim, the
|
|
3634
3751
|
// backend's own when it names one) is answered only once per DIALOG_ANSWER_BACKOFF_MS. Anything else — another
|
|
3635
3752
|
// request, an unexplained difference, the dialog gone — is not held back here; the answer path below ends or
|
|
@@ -3692,7 +3809,7 @@ export class Daemon extends EventEmitter {
|
|
|
3692
3809
|
return;
|
|
3693
3810
|
if (dialog.verifyAfterKeys && autoKey)
|
|
3694
3811
|
this.claimAutoDialog(dialog);
|
|
3695
|
-
this.logger.info(`Auto-dismissing runtime dialog: ${dialog.description}`);
|
|
3812
|
+
this.logger.info(Daemon.answerDetailOf(dialog, currentPane), `Auto-dismissing runtime dialog: ${dialog.description}`);
|
|
3696
3813
|
const SPECIAL_KEYS = new Set(["Up", "Down", "Enter", "Escape", "Right", "Left"]);
|
|
3697
3814
|
for (const key of dialog.keys) {
|
|
3698
3815
|
if (stale())
|
|
@@ -3701,6 +3818,9 @@ export class Daemon extends EventEmitter {
|
|
|
3701
3818
|
if (SPECIAL_KEYS.has(key)) {
|
|
3702
3819
|
sent = await pollTmux.sendSpecialKey(key);
|
|
3703
3820
|
}
|
|
3821
|
+
else if (dialog.literalKeys) {
|
|
3822
|
+
sent = await pollTmux.sendKeys(key); // typed, no Enter (#2872)
|
|
3823
|
+
}
|
|
3704
3824
|
else {
|
|
3705
3825
|
sent = await pollTmux.pasteText(key, this.systemPasteOptions());
|
|
3706
3826
|
}
|
|
@@ -3745,6 +3865,8 @@ export class Daemon extends EventEmitter {
|
|
|
3745
3865
|
else {
|
|
3746
3866
|
resolved = true;
|
|
3747
3867
|
}
|
|
3868
|
+
if (dialog.dismissIncident)
|
|
3869
|
+
this.emitDismissIncident(dialog);
|
|
3748
3870
|
}
|
|
3749
3871
|
else if (dialog.keys.length > 0) {
|
|
3750
3872
|
// Did the keys take? Read the pane again. The same request still there is an answer that did nothing. A request
|
|
@@ -3778,8 +3900,10 @@ export class Daemon extends EventEmitter {
|
|
|
3778
3900
|
// Reached only when no dialog was answered this poll (none on screen, a fenced one, or one waiting out its
|
|
3779
3901
|
// backoff): unless it is the one backing off, the screen that was being answered is gone, and the next dialog is
|
|
3780
3902
|
// a new one.
|
|
3781
|
-
if (!answersBackedOff)
|
|
3903
|
+
if (!answersBackedOff && this.dialogAnswers) {
|
|
3782
3904
|
this.dialogAnswers = null;
|
|
3905
|
+
this.retireParkedNotice("answer");
|
|
3906
|
+
}
|
|
3783
3907
|
this.trackDialogParked(blockingSeen);
|
|
3784
3908
|
this.trackRemoteCard(blockingSeen, pane, captureMono);
|
|
3785
3909
|
if (blockingSeen)
|
|
@@ -3897,9 +4021,10 @@ export class Daemon extends EventEmitter {
|
|
|
3897
4021
|
const count = countMatches(ep.pattern);
|
|
3898
4022
|
const seen = this.lastErrorCount.get(key) ?? 0;
|
|
3899
4023
|
// #2282: a resumed exhausted429 row is history, not even a new
|
|
3900
|
-
// notification/failover incident
|
|
3901
|
-
//
|
|
3902
|
-
|
|
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) {
|
|
3903
4028
|
const baseline = this.nudgeBaselines?.get(key);
|
|
3904
4029
|
if (baseline === undefined || count <= baseline) {
|
|
3905
4030
|
if (baseline !== undefined)
|
|
@@ -3976,6 +4101,17 @@ export class Daemon extends EventEmitter {
|
|
|
3976
4101
|
break; // Only handle first unsuppressed new error per scan
|
|
3977
4102
|
}
|
|
3978
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
|
+
}
|
|
3979
4115
|
/** Arm the recovery gates and emit one detected error pattern. */
|
|
3980
4116
|
emitErrorPattern(ep, key, pane, now) {
|
|
3981
4117
|
// #2795: auth text on the pane proves nothing about the credentials (22 of 22 such matches on the main host were
|
|
@@ -3994,7 +4130,9 @@ export class Daemon extends EventEmitter {
|
|
|
3994
4130
|
const message = this.resolveErrorMessage(pane, ep);
|
|
3995
4131
|
this.logger.warn({ errorType: ep.type, action: ep.action }, `PTY error detected: ${message}`);
|
|
3996
4132
|
this.lastErrorEpisode = { key, pattern: ep.pattern, pane, spawn: this.spawnGeneration, fence: this.launchFenceEpoch };
|
|
3997
|
-
|
|
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 } : {}) });
|
|
3998
4136
|
}
|
|
3999
4137
|
/**
|
|
4000
4138
|
* Notification text for a detected pattern. `formatMessage` patterns build it
|
|
@@ -4004,6 +4142,18 @@ export class Daemon extends EventEmitter {
|
|
|
4004
4142
|
* Uses the LAST match in the pane: an older warning may still be scrolled up
|
|
4005
4143
|
* (10% earlier, 5% now), and the newest one is the one worth reporting.
|
|
4006
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
|
+
}
|
|
4007
4157
|
resolveErrorMessage(pane, ep) {
|
|
4008
4158
|
if (!ep.formatMessage)
|
|
4009
4159
|
return ep.message;
|
|
@@ -4934,6 +5084,22 @@ export class Daemon extends EventEmitter {
|
|
|
4934
5084
|
&& Daemon.dialogMatches(d, pane)
|
|
4935
5085
|
// #1690: AgEnD's own switch dialog, being answered right now, is nobody's to answer.
|
|
4936
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
|
+
}
|
|
4937
5103
|
if (dialog) {
|
|
4938
5104
|
const identity = dialog.requestIdentity?.(pane) ?? pane.replace(/\r/g, "")
|
|
4939
5105
|
.split("\n").map(row => row.replace(/^([ \t]*)[❯›]([ \t]+)(?=\d\.|Yes\b|No\b|Resume\b)/, "$1$2").trimEnd())
|
|
@@ -5662,6 +5828,15 @@ export class Daemon extends EventEmitter {
|
|
|
5662
5828
|
this.turnReplyGuard.complete(turn.generation);
|
|
5663
5829
|
return;
|
|
5664
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
|
+
}
|
|
5665
5840
|
// A second idle edge ends the one permitted recovery turn. Never create a
|
|
5666
5841
|
// third turn or guess at terminal text; make the failure visible instead.
|
|
5667
5842
|
if (turn.phase === "recovering") {
|
|
@@ -8170,13 +8345,36 @@ export class Daemon extends EventEmitter {
|
|
|
8170
8345
|
&& !this.backend?.backgroundWorkRunning?.(pane) && codexLiveRowMatches(pane, nudge.pattern)
|
|
8171
8346
|
&& Daemon.countOccurrences(nudge.pattern, pane) === Daemon.countOccurrences(nudge.pattern, nudge.pane);
|
|
8172
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
|
+
}
|
|
8173
8371
|
/** The first live composer of a spawn fixes what is history (see {@link nudgeBaselines}). */
|
|
8174
8372
|
takeNudgeBaselines(pane, patterns) {
|
|
8175
8373
|
if (!this.isCodexLivePaneSnapshot(pane))
|
|
8176
8374
|
return;
|
|
8177
8375
|
const baselines = this.nudgeBaselines ?? new Map();
|
|
8178
8376
|
for (const ep of patterns) {
|
|
8179
|
-
if (ep.action !== "nudge_continue")
|
|
8377
|
+
if (ep.action !== "nudge_continue" && !ep.historyUntilLiveComposer)
|
|
8180
8378
|
continue;
|
|
8181
8379
|
const key = Daemon.errorPatternKey(ep);
|
|
8182
8380
|
const count = Daemon.countOccurrences(ep.pattern, pane);
|
|
@@ -8260,6 +8458,7 @@ export class Daemon extends EventEmitter {
|
|
|
8260
8458
|
*/
|
|
8261
8459
|
endDialogEpisode(clearParked = true) {
|
|
8262
8460
|
this.dialogAnswers = null;
|
|
8461
|
+
this.retireParkedNotice("answer");
|
|
8263
8462
|
if (clearParked)
|
|
8264
8463
|
this.trackDialogParked(null);
|
|
8265
8464
|
}
|
|
@@ -8286,8 +8485,11 @@ export class Daemon extends EventEmitter {
|
|
|
8286
8485
|
*/
|
|
8287
8486
|
noteDialogAnswer(dialog, outcome, screen) {
|
|
8288
8487
|
const key = Daemon.answerKey(dialog);
|
|
8289
|
-
if (!this.dialogAnswers || this.dialogAnswers.key !== key)
|
|
8290
|
-
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
|
|
8291
8493
|
const episode = this.dialogAnswers;
|
|
8292
8494
|
if (outcome !== "ignored") {
|
|
8293
8495
|
this.endDialogEpisode(outcome === "took");
|
|
@@ -8303,6 +8505,8 @@ export class Daemon extends EventEmitter {
|
|
|
8303
8505
|
this.dialogParkedReported = true;
|
|
8304
8506
|
const holdsDeliveries = dialog.blocksDelivery === true || dialog.holdOnly === true;
|
|
8305
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";
|
|
8306
8510
|
this.emit("dialog_answer_ignored", { name: this.name, description: dialog.description, attempts: episode.ignored, holdsDeliveries });
|
|
8307
8511
|
}
|
|
8308
8512
|
/**
|
|
@@ -8376,6 +8580,24 @@ export class Daemon extends EventEmitter {
|
|
|
8376
8580
|
this.remoteCardAnswering = false;
|
|
8377
8581
|
}
|
|
8378
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
|
+
}
|
|
8379
8601
|
/**
|
|
8380
8602
|
* Remember how long a blocking dialog has been on screen. Once it has
|
|
8381
8603
|
* outlived DIALOG_PARKED_NOTIFY_MS — the auto-dismiss did not take, or the
|
|
@@ -8384,14 +8606,24 @@ export class Daemon extends EventEmitter {
|
|
|
8384
8606
|
* is safe. Identity is the pattern, so the delivery gate and the runtime
|
|
8385
8607
|
* scanner (same canonical list) never reset each other's clock.
|
|
8386
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
|
+
}
|
|
8387
8616
|
trackDialogParked(dialog) {
|
|
8388
8617
|
if (!dialog) {
|
|
8389
8618
|
if (this.dialogParkedSince !== 0)
|
|
8390
8619
|
this.logger.info({ dialog: this.dialogParkedKey }, "Blocking dialog is gone from the pane");
|
|
8391
8620
|
this.dialogParkedSince = 0;
|
|
8392
8621
|
this.dialogParkedKey = null;
|
|
8622
|
+
this.heldReasonLoggedKey = null;
|
|
8393
8623
|
this.dialogParkedInteractionEpisode = null;
|
|
8394
8624
|
this.dialogParkedReported = false;
|
|
8625
|
+
// #2897: whatever it posted is out of date — the fleet takes its notices back.
|
|
8626
|
+
this.retireParkedNotice("parked");
|
|
8395
8627
|
return;
|
|
8396
8628
|
}
|
|
8397
8629
|
const key = Daemon.dialogKey(dialog);
|
|
@@ -8403,6 +8635,9 @@ export class Daemon extends EventEmitter {
|
|
|
8403
8635
|
if (episode !== null)
|
|
8404
8636
|
this.dialogParkedInteractionEpisode = episode;
|
|
8405
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");
|
|
8406
8641
|
this.dialogParkedSince = Date.now();
|
|
8407
8642
|
this.dialogParkedKey = key;
|
|
8408
8643
|
this.dialogParkedReported = false;
|
|
@@ -8414,8 +8649,10 @@ export class Daemon extends EventEmitter {
|
|
|
8414
8649
|
this.dialogParkedReported = true;
|
|
8415
8650
|
if (!this.dialogParkedReported && Date.now() - this.dialogParkedSince >= (unrecognised ? 0 : DIALOG_PARKED_NOTIFY_MS)) {
|
|
8416
8651
|
this.dialogParkedReported = true;
|
|
8652
|
+
this.parkedNoticeOut = "parked";
|
|
8417
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");
|
|
8418
8654
|
this.emit("dialog_parked", { name: this.name, description: dialog.description, holdOnly: dialog.holdOnly === true,
|
|
8655
|
+
...(dialog.parkedAdvice ? { advice: dialog.parkedAdvice } : {}),
|
|
8419
8656
|
...(episode !== null && interaction.kind ? { kind: interaction.kind, episode, backend: this.config.backend ?? "claude-code" } : {}),
|
|
8420
8657
|
...(unrecognised ? { unrecognised: true, question: this.unrecognisedPrompt?.shape.question ?? null } : {}) });
|
|
8421
8658
|
}
|
|
@@ -10785,96 +11022,12 @@ export class Daemon extends EventEmitter {
|
|
|
10785
11022
|
reply(written, undefined);
|
|
10786
11023
|
return;
|
|
10787
11024
|
}
|
|
10788
|
-
if (tool === "export_web_mod") {
|
|
10789
|
-
// The fleet builds the file (whole, or in parts); it is written here, in the working directory.
|
|
10790
|
-
const first = await ask(args);
|
|
10791
|
-
if (first.error) {
|
|
10792
|
-
reply(null, first.error);
|
|
10793
|
-
return;
|
|
10794
|
-
}
|
|
10795
|
-
let failed;
|
|
10796
|
-
const firstResult = (first.result && typeof first.result === "object" ? first.result : {});
|
|
10797
|
-
const got = await receiveFiles(firstResult, async (payload) => {
|
|
10798
|
-
const r = await ask(payload);
|
|
10799
|
-
if (r.error) {
|
|
10800
|
-
failed = r.error;
|
|
10801
|
-
return { error: r.error };
|
|
10802
|
-
}
|
|
10803
|
-
return (r.result && typeof r.result === "object" ? r.result : {});
|
|
10804
|
-
}, { name: MOD_FILE, idKey: "transfer", what: "export", maxParts: MOD_MAX_PARTS });
|
|
10805
|
-
if (failed) {
|
|
10806
|
-
reply(null, failed);
|
|
10807
|
-
return;
|
|
10808
|
-
}
|
|
10809
|
-
if ("error" in got) {
|
|
10810
|
-
reply(got, undefined);
|
|
10811
|
-
return;
|
|
10812
|
-
}
|
|
10813
|
-
const written = await writeModFile(got.files, { workspace: this.config.working_directory, configuredWorkspace: this.config.working_directory, stateDir: getAgendHome(), instance: this.name });
|
|
10814
|
-
if ("error" in written) {
|
|
10815
|
-
reply(null, `export_web_mod: ${written.error}`);
|
|
10816
|
-
return;
|
|
10817
|
-
}
|
|
10818
|
-
reply({ ...written, ...(firstResult.effects_omitted ? { effects_omitted: firstResult.effects_omitted } : {}) }, undefined);
|
|
10819
|
-
return;
|
|
10820
|
-
}
|
|
10821
|
-
if (tool === "propose_web_mod") {
|
|
10822
|
-
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 });
|
|
10823
|
-
const named = typeof args.file === "string" ? args.file.slice(0, 200) : "the file argument";
|
|
10824
|
-
if (!read.ok) {
|
|
10825
|
-
reply(null, `propose_web_mod: ${named} — ${read.reason}`);
|
|
10826
|
-
return;
|
|
10827
|
-
}
|
|
10828
|
-
let failed;
|
|
10829
|
-
const answer = await sendAssetInParts("mod", read.bytes, async (payload) => {
|
|
10830
|
-
const r = await ask(payload);
|
|
10831
|
-
if (r.error) {
|
|
10832
|
-
failed = r.error;
|
|
10833
|
-
return { error: r.error };
|
|
10834
|
-
}
|
|
10835
|
-
return (r.result && typeof r.result === "object" ? r.result : {});
|
|
10836
|
-
});
|
|
10837
|
-
reply(failed ? null : answer, failed);
|
|
10838
|
-
return;
|
|
10839
|
-
}
|
|
10840
11025
|
if (tool === "office_pack_check" || tool === "office_pack_upload") {
|
|
10841
11026
|
// #2692: the folder (or the manifest's files) is read here, on the agent's side; the fleet gets bytes in parts.
|
|
10842
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 : {}); });
|
|
10843
11028
|
reply(answer, undefined);
|
|
10844
11029
|
return;
|
|
10845
11030
|
}
|
|
10846
|
-
if (tool === "upload_web_effect") {
|
|
10847
|
-
const owner = { fence: this.launchFenceEpoch, spawn: this.spawnGeneration, boot: this.bootId, ipc: this.ipcServer };
|
|
10848
|
-
const current = () => !!owner.ipc && owner.ipc === this.ipcServer && owner.fence === this.launchFenceEpoch && owner.spawn === this.spawnGeneration && owner.boot === this.bootId && !this.deliveryWritesStopping && !this.startupAborted;
|
|
10849
|
-
if (!current()) {
|
|
10850
|
-
reply(null, "upload_web_effect: launch no longer current");
|
|
10851
|
-
return;
|
|
10852
|
-
}
|
|
10853
|
-
const read = await readAgentEffect(args.dir, { workspace: this.config.working_directory, configuredWorkspace: this.config.working_directory, stateDir: getAgendHome(), instance: this.name });
|
|
10854
|
-
if (!current()) {
|
|
10855
|
-
reply(null, "upload_web_effect: launch no longer current");
|
|
10856
|
-
return;
|
|
10857
|
-
}
|
|
10858
|
-
if (!read.ok) {
|
|
10859
|
-
reply(null, `upload_web_effect: ${read.reason}`);
|
|
10860
|
-
return;
|
|
10861
|
-
}
|
|
10862
|
-
let failed;
|
|
10863
|
-
const answer = await sendEffectInParts(read.files, async (payload) => {
|
|
10864
|
-
if (!current()) {
|
|
10865
|
-
failed = "upload_web_effect: launch no longer current";
|
|
10866
|
-
return { error: failed };
|
|
10867
|
-
}
|
|
10868
|
-
const r = await ask(payload);
|
|
10869
|
-
if (r.error) {
|
|
10870
|
-
failed = r.error;
|
|
10871
|
-
return { error: r.error };
|
|
10872
|
-
}
|
|
10873
|
-
return (r.result && typeof r.result === "object" ? r.result : {});
|
|
10874
|
-
});
|
|
10875
|
-
reply(failed ? null : answer, failed);
|
|
10876
|
-
return;
|
|
10877
|
-
}
|
|
10878
11031
|
if (tool !== "set_web_theme_asset") {
|
|
10879
11032
|
const r = await ask(args);
|
|
10880
11033
|
reply(r.result, r.error);
|
|
@@ -11335,6 +11488,13 @@ export class Daemon extends EventEmitter {
|
|
|
11335
11488
|
this.recoveryNativeWork = false;
|
|
11336
11489
|
this.recoveryCancelled = false;
|
|
11337
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
|
+
}
|
|
11338
11498
|
this.cancelCapacityNudge("the instance was stopped, paused or respawned");
|
|
11339
11499
|
this.lastErrorEpisode = null;
|
|
11340
11500
|
this.nudgeBaselines = null;
|
|
@@ -11563,6 +11723,8 @@ export class Daemon extends EventEmitter {
|
|
|
11563
11723
|
// as this backend's and resumed by the next start.
|
|
11564
11724
|
this.startupAdmission?.();
|
|
11565
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.
|
|
11566
11728
|
const attemptedResume = !this.skipResume;
|
|
11567
11729
|
// A resume launch may get a longer budget than a fresh one (kiro: the
|
|
11568
11730
|
// conversation must come back from the backend before anything paints).
|
|
@@ -11647,6 +11809,20 @@ export class Daemon extends EventEmitter {
|
|
|
11647
11809
|
}
|
|
11648
11810
|
this.setSessionAside(proven ? "cli_reported_no_conversation" : "resume_failed_repeatedly");
|
|
11649
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
|
+
}
|
|
11650
11826
|
}
|
|
11651
11827
|
// A fresh start that also failed retries once, as before. It never clears
|
|
11652
11828
|
// a session: nothing about a failed fresh launch says the stored
|
|
@@ -11664,9 +11840,37 @@ export class Daemon extends EventEmitter {
|
|
|
11664
11840
|
this.startupAdmission?.();
|
|
11665
11841
|
throw new Error("CLI failed to start after retry");
|
|
11666
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 */ }
|
|
11667
11856
|
}
|
|
11668
11857
|
else if (attemptedResume) {
|
|
11669
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
|
+
}
|
|
11670
11874
|
this.clearUnprovenResumeFailures();
|
|
11671
11875
|
// A resume needs the backend: its success is positive proof the backend
|
|
11672
11876
|
// is reachable again (a fresh prompt is local and proves nothing).
|
|
@@ -12306,7 +12510,7 @@ export class Daemon extends EventEmitter {
|
|
|
12306
12510
|
*/
|
|
12307
12511
|
currentWhatsNew(pending) {
|
|
12308
12512
|
const plan = planWhatsNew({ record: readWhatsNewRecord(this.instanceDir), fresh: false, version: pending.version,
|
|
12309
|
-
roles: this.whatsNewRoles(), tools: this.whatsNewTools()
|
|
12513
|
+
roles: this.whatsNewRoles(), tools: this.whatsNewTools() });
|
|
12310
12514
|
if (!plan || "silent" in plan) {
|
|
12311
12515
|
this.pendingWhatsNew = null;
|
|
12312
12516
|
if (plan)
|
|
@@ -12327,13 +12531,13 @@ export class Daemon extends EventEmitter {
|
|
|
12327
12531
|
}
|
|
12328
12532
|
}
|
|
12329
12533
|
/**
|
|
12330
|
-
* #2800: the tools this instance's agend MCP server lists now — the same filters (tool set, dashboard switches
|
|
12331
|
-
*
|
|
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.
|
|
12332
12536
|
*/
|
|
12333
12537
|
whatsNewTools() {
|
|
12334
12538
|
const allowed = toolsFor(resolveToolSet(this.config, this.name));
|
|
12335
12539
|
const listed = withoutFeatures(TOOLS.filter(t => allowed.has(t.name)), dashboardFeaturesOff(dashboardAccessOf(this.config)));
|
|
12336
|
-
return new Set(
|
|
12540
|
+
return new Set(listed.map(t => t.name));
|
|
12337
12541
|
}
|
|
12338
12542
|
/**
|
|
12339
12543
|
* #2163: who this instance is to the what's-new notices — a ClassicBot room's agent, General, or a fleet worker (with
|
|
@@ -12351,7 +12555,7 @@ export class Daemon extends EventEmitter {
|
|
|
12351
12555
|
prepareWhatsNew(fresh, version = agendVersion()) {
|
|
12352
12556
|
try {
|
|
12353
12557
|
const plan = planWhatsNew({ record: readWhatsNewRecord(this.instanceDir), fresh, version, roles: this.whatsNewRoles(),
|
|
12354
|
-
tools: this.whatsNewTools()
|
|
12558
|
+
tools: this.whatsNewTools() });
|
|
12355
12559
|
if (!plan) {
|
|
12356
12560
|
this.pendingWhatsNew = null;
|
|
12357
12561
|
return;
|
|
@@ -12671,7 +12875,10 @@ export class Daemon extends EventEmitter {
|
|
|
12671
12875
|
continue;
|
|
12672
12876
|
lastDialog = dialog;
|
|
12673
12877
|
this.noteCodexReserveDialog(dialog, pane);
|
|
12878
|
+
this.noteLiveIncidentDialog(dialog, pane);
|
|
12674
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);
|
|
12675
12882
|
// Start the parked clock for every delivery-blocking dialog, exact
|
|
12676
12883
|
// ones included: if the auto-dismiss keeps failing here, the human
|
|
12677
12884
|
// report should count from first sight, not from the runtime scan.
|
|
@@ -12700,14 +12907,16 @@ export class Daemon extends EventEmitter {
|
|
|
12700
12907
|
}
|
|
12701
12908
|
if (dialog.holdOnly) {
|
|
12702
12909
|
// Recognised but deliberately not answered (see RuntimeDialog.holdOnly).
|
|
12703
|
-
if (
|
|
12704
|
-
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");
|
|
12705
12915
|
}
|
|
12706
12916
|
this.trackDialogParked(dialog);
|
|
12707
12917
|
matched = true;
|
|
12708
12918
|
break;
|
|
12709
12919
|
}
|
|
12710
|
-
this.logger.info(`Dismissing startup dialog: ${dialog.description}`);
|
|
12711
12920
|
// Restart is exactly when inbound messages pile up, and nothing gates
|
|
12712
12921
|
// delivery on `spawning`. Take the pane lock for the key sequence so a
|
|
12713
12922
|
// queued message cannot be pasted into a half-dismissed trust dialog.
|
|
@@ -12720,6 +12929,7 @@ export class Daemon extends EventEmitter {
|
|
|
12720
12929
|
// a pane that is no longer this launch's is not read or keyed.
|
|
12721
12930
|
if (!owned())
|
|
12722
12931
|
return false;
|
|
12932
|
+
let answeredPane = pane;
|
|
12723
12933
|
if (dialog.inputBlocked) {
|
|
12724
12934
|
const currentPane = await this.tmux.capturePane();
|
|
12725
12935
|
this.startupAdmission?.();
|
|
@@ -12728,7 +12938,10 @@ export class Daemon extends EventEmitter {
|
|
|
12728
12938
|
return false;
|
|
12729
12939
|
if (!Daemon.dialogMatches(dialog, currentPane))
|
|
12730
12940
|
return false;
|
|
12941
|
+
answeredPane = currentPane;
|
|
12731
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}`);
|
|
12732
12945
|
if (dialog.autoResolutionKey) {
|
|
12733
12946
|
attemptedSafetyChoices.add(dialog.autoResolutionKey);
|
|
12734
12947
|
this.claimAutoDialog(dialog);
|