@songsid/agend 2.2.0-beta.1 → 2.2.0-beta.11
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/README.md +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/agent-cli-instructions.md +16 -9
- package/dist/agent-cli.js +15 -7
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +24 -9
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.d.ts +47 -0
- package/dist/agent-whats-new.js +147 -0
- package/dist/agent-whats-new.js.map +1 -0
- package/dist/auth-api.d.ts +5 -3
- package/dist/auth-api.js +19 -3
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/antigravity.d.ts +8 -0
- package/dist/backend/antigravity.js +32 -9
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-carry.d.ts +22 -0
- package/dist/backend/claude-carry.js +170 -0
- package/dist/backend/claude-carry.js.map +1 -0
- package/dist/backend/claude-code.d.ts +65 -2
- package/dist/backend/claude-code.js +226 -33
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-metadata.d.ts +33 -0
- package/dist/backend/codex-metadata.js +122 -18
- package/dist/backend/codex-metadata.js.map +1 -1
- package/dist/backend/codex.d.ts +3 -0
- package/dist/backend/codex.js +17 -1
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/credential-profile.d.ts +40 -3
- package/dist/backend/credential-profile.js +113 -5
- package/dist/backend/credential-profile.js.map +1 -1
- package/dist/backend/effort-metadata.d.ts +37 -1
- package/dist/backend/effort-metadata.js +86 -15
- package/dist/backend/effort-metadata.js.map +1 -1
- package/dist/backend/grok.d.ts +4 -0
- package/dist/backend/grok.js +14 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro-engine-ledger.d.ts +5 -0
- package/dist/backend/kiro-engine-ledger.js +20 -0
- package/dist/backend/kiro-engine-ledger.js.map +1 -1
- package/dist/backend/kiro-identity.d.ts +6 -0
- package/dist/backend/kiro-identity.js +19 -5
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro.d.ts +39 -3
- package/dist/backend/kiro.js +143 -19
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/launch-effort.d.ts +61 -0
- package/dist/backend/launch-effort.js +170 -0
- package/dist/backend/launch-effort.js.map +1 -0
- package/dist/backend/muse.d.ts +84 -0
- package/dist/backend/muse.js +449 -53
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +47 -4
- package/dist/backend/types.js +12 -1
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-command.d.ts +91 -0
- package/dist/backend-command.js +122 -0
- package/dist/backend-command.js.map +1 -0
- package/dist/background-work-ended.d.ts +18 -0
- package/dist/background-work-ended.js +30 -0
- package/dist/background-work-ended.js.map +1 -0
- package/dist/cache-service.d.ts +2 -0
- package/dist/cache-service.js +2 -1
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +36 -1
- package/dist/channel/adapters/discord.js +126 -4
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +42 -2
- package/dist/channel/adapters/telegram.js +142 -23
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-server.js +9 -1
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.d.ts +11 -0
- package/dist/channel/mcp-tools.js +26 -13
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/message-edit-errors.d.ts +5 -0
- package/dist/channel/message-edit-errors.js +24 -0
- package/dist/channel/message-edit-errors.js.map +1 -0
- package/dist/channel/telegram-markdown.d.ts +6 -0
- package/dist/channel/telegram-markdown.js +104 -0
- package/dist/channel/telegram-markdown.js.map +1 -0
- package/dist/channel/tool-router.d.ts +2 -0
- package/dist/channel/tool-router.js +10 -4
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +55 -3
- package/dist/cli.js +167 -39
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +2 -2
- package/dist/command-table.js +6 -3
- package/dist/command-table.js.map +1 -1
- package/dist/config-validator.js +59 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/cross-instance-envelope.d.ts +15 -2
- package/dist/cross-instance-envelope.js +29 -5
- package/dist/cross-instance-envelope.js.map +1 -1
- package/dist/daemon.d.ts +156 -1
- package/dist/daemon.js +773 -89
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +70 -8
- package/dist/delivery-outbox.js +315 -24
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +18 -1
- package/dist/delivery-reconciliation.js +67 -2
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/fleet-context.d.ts +10 -1
- package/dist/fleet-manager.d.ts +404 -13
- package/dist/fleet-manager.js +2039 -210
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/backend-providers/SKILL.md +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +92 -21
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +27 -3
- package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +8 -8
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -5
- package/dist/general-knowledge/skills/fleet-health/SKILL.md +4 -4
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +13 -13
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -2
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +6 -0
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +12 -9
- package/dist/general-knowledge/skills/multi-channel/SKILL.md +8 -2
- package/dist/general-knowledge/skills/scheduling/SKILL.md +5 -3
- package/dist/general-knowledge/skills/session-management/SKILL.md +6 -5
- package/dist/general-knowledge/skills/tui-effort/SKILL.md +2 -2
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +8 -3
- package/dist/general-knowledge/steering/core-rules.md +8 -7
- package/dist/host-memory.d.ts +10 -0
- package/dist/host-memory.js +33 -1
- package/dist/host-memory.js.map +1 -1
- package/dist/human-delivery-receipts.d.ts +83 -0
- package/dist/human-delivery-receipts.js +194 -0
- package/dist/human-delivery-receipts.js.map +1 -0
- package/dist/instance-avatar.d.ts +36 -0
- package/dist/instance-avatar.js +57 -0
- package/dist/instance-avatar.js.map +1 -0
- package/dist/instance-config-impact.js +6 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +29 -6
- package/dist/instance-lifecycle.js +79 -28
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +3 -1
- package/dist/instructions.js +11 -8
- package/dist/instructions.js.map +1 -1
- package/dist/kiro-log-gate.d.ts +42 -0
- package/dist/kiro-log-gate.js +39 -0
- package/dist/kiro-log-gate.js.map +1 -0
- package/dist/locale.js +226 -44
- package/dist/locale.js.map +1 -1
- package/dist/log-service.d.ts +99 -0
- package/dist/log-service.js +243 -0
- package/dist/log-service.js.map +1 -0
- package/dist/log-steps.d.ts +169 -0
- package/dist/log-steps.js +858 -0
- package/dist/log-steps.js.map +1 -0
- package/dist/login-controller.d.ts +20 -3
- package/dist/login-controller.js +71 -19
- package/dist/login-controller.js.map +1 -1
- package/dist/login-flows.d.ts +5 -2
- package/dist/login-flows.js +5 -4
- package/dist/login-flows.js.map +1 -1
- package/dist/memory-pressure.d.ts +13 -1
- package/dist/memory-pressure.js +50 -10
- package/dist/memory-pressure.js.map +1 -1
- package/dist/needs-you-hub.js +2 -0
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you-live.d.ts +6 -0
- package/dist/needs-you-live.js +23 -3
- package/dist/needs-you-live.js.map +1 -1
- package/dist/next-steps.js +1 -1
- package/dist/next-steps.js.map +1 -1
- package/dist/outbound-handlers.d.ts +100 -0
- package/dist/outbound-handlers.js +284 -97
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +38 -15
- package/dist/outbound-schemas.js +36 -8
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-link-progress.d.ts +1 -1
- package/dist/public-link-progress.js +8 -2
- package/dist/public-link-progress.js.map +1 -1
- package/dist/public-preview.d.ts +101 -0
- package/dist/public-preview.js +236 -0
- package/dist/public-preview.js.map +1 -0
- package/dist/public-web-gateway.js +14 -4
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +24 -1
- package/dist/public-web-link.js +63 -9
- package/dist/public-web-link.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +89 -5
- package/dist/reply-buttons-controller.js +273 -8
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/reply-buttons.d.ts +48 -3
- package/dist/reply-buttons.js +105 -10
- package/dist/reply-buttons.js.map +1 -1
- package/dist/reply-polls.d.ts +85 -0
- package/dist/reply-polls.js +154 -0
- package/dist/reply-polls.js.map +1 -0
- package/dist/restart-guard.d.ts +6 -3
- package/dist/restart-guard.js +6 -3
- package/dist/restart-guard.js.map +1 -1
- package/dist/restart-progress.d.ts +33 -0
- package/dist/restart-progress.js +30 -0
- package/dist/restart-progress.js.map +1 -1
- package/dist/service-activation.d.ts +15 -7
- package/dist/service-activation.js +46 -29
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.d.ts +31 -0
- package/dist/service-installer.js +41 -2
- package/dist/service-installer.js.map +1 -1
- package/dist/settings-api.js +85 -25
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +72 -1
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +28 -4
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +7 -0
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/setup-form.d.ts +1 -1
- package/dist/setup-form.js +1 -1
- package/dist/spawn-gate.js +5 -1
- package/dist/spawn-gate.js.map +1 -1
- package/dist/spawn-hold-reply.d.ts +12 -0
- package/dist/spawn-hold-reply.js +39 -0
- package/dist/spawn-hold-reply.js.map +1 -0
- package/dist/status-emojis.d.ts +10 -3
- package/dist/status-emojis.js +11 -2
- package/dist/status-emojis.js.map +1 -1
- package/dist/systemd-peer-bus.d.ts +95 -0
- package/dist/systemd-peer-bus.js +430 -0
- package/dist/systemd-peer-bus.js.map +1 -0
- package/dist/systemd-runtime.d.ts +27 -1
- package/dist/systemd-runtime.js +69 -7
- package/dist/systemd-runtime.js.map +1 -1
- package/dist/tips.js +4 -4
- package/dist/tips.js.map +1 -1
- package/dist/tool-permissions.js +3 -3
- package/dist/tool-permissions.js.map +1 -1
- package/dist/tool-progress.js +3 -2
- package/dist/tool-progress.js.map +1 -1
- package/dist/topic-commands.d.ts +42 -2
- package/dist/topic-commands.js +101 -11
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-delta-reader.d.ts +3 -3
- package/dist/transcript-delta-reader.js +8 -8
- package/dist/transcript-delta-reader.js.map +1 -1
- package/dist/transcript-jsonl.d.ts +80 -3
- package/dist/transcript-jsonl.js +147 -10
- package/dist/transcript-jsonl.js.map +1 -1
- package/dist/transcript-monitor.d.ts +19 -0
- package/dist/transcript-monitor.js +87 -15
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +78 -0
- package/dist/transcript-sources.js +375 -64
- package/dist/transcript-sources.js.map +1 -1
- package/dist/transcript-turns.d.ts +15 -2
- package/dist/transcript-turns.js +15 -2
- package/dist/transcript-turns.js.map +1 -1
- package/dist/tunnel/cloudflared.d.ts +7 -15
- package/dist/tunnel/cloudflared.js +36 -1
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/tunnel/purpose-lane.d.ts +5 -1
- package/dist/tunnel/purpose-lane.js +14 -7
- package/dist/tunnel/purpose-lane.js.map +1 -1
- package/dist/tunnel/types.d.ts +5 -1
- package/dist/tunnel/types.js.map +1 -1
- package/dist/turn-resume.d.ts +2 -0
- package/dist/turn-resume.js +1 -1
- package/dist/turn-resume.js.map +1 -1
- package/dist/types.d.ts +25 -0
- package/dist/ui/chat-commands.js +1 -0
- package/dist/ui/chat-emojis.js +33 -0
- package/dist/ui/chat-mentions.js +63 -0
- package/dist/ui/chat-render.js +61 -4
- package/dist/ui/chat-store.js +37 -3
- package/dist/ui/chat-strings.js +40 -20
- package/dist/ui/chat-thread.js +111 -12
- package/dist/ui/chat-time.js +80 -0
- package/dist/ui/panel-chat.js +41 -10
- package/dist/ui/panel-details.js +2 -0
- package/dist/ui/panel-fleet.js +199 -57
- package/dist/ui/panel-log.js +428 -0
- package/dist/ui/panel-needs.js +6 -2
- package/dist/ui/panel-settings.js +184 -32
- package/dist/ui/preview-panel.js +55 -15
- package/dist/ui/preview-status.js +28 -0
- package/dist/ui/preview.js +44 -3
- package/dist/ui/settings-dialogs.js +7 -2
- package/dist/ui/settings-model.js +2 -1
- package/dist/ui/settings-strings.js +56 -4
- package/dist/ui/settings-token.js +1 -1
- package/dist/ui/shared/app-i18n.js +43 -17
- package/dist/ui/shared/app-prefs.js +250 -0
- package/dist/ui/shared/app-route.js +7 -2
- package/dist/ui/shared/app-shell.js +165 -8
- package/dist/ui/shared/app-store.js +5 -1
- package/dist/ui/shared/app-stream.js +26 -5
- package/dist/ui/shared/app.css +163 -22
- package/dist/ui/shared/app.js +11 -1
- package/dist/ui/shared/fonts/README.md +1 -1
- package/dist/ui/shared/header-tools.js +55 -5
- package/dist/ui/shared/instance-nav.js +241 -45
- package/dist/ui/shared/instance-switch.js +4 -4
- package/dist/ui/shared/panel-view.js +6 -0
- package/dist/ui/shared/signin.js +7 -7
- package/dist/ui/shared/theme.js +39 -7
- package/dist/ui/shared/tokens.css +1 -1
- package/dist/ui/shared/ui-icons.js +1 -0
- package/dist/ui/shared/usage-dialog.js +23 -5
- package/dist/ui/shared/view-strings.js +32 -4
- package/dist/ui/signin.html +1 -1
- package/dist/unrecognised-prompt.d.ts +23 -0
- package/dist/unrecognised-prompt.js +74 -0
- package/dist/unrecognised-prompt.js.map +1 -0
- package/dist/update-check.d.ts +31 -0
- package/dist/update-check.js +47 -0
- package/dist/update-check.js.map +1 -1
- package/dist/update-install.d.ts +28 -2
- package/dist/update-install.js +22 -4
- package/dist/update-install.js.map +1 -1
- package/dist/update-launch.d.ts +46 -1
- package/dist/update-launch.js +146 -3
- package/dist/update-launch.js.map +1 -1
- package/dist/update-marker.d.ts +51 -0
- package/dist/update-marker.js +61 -0
- package/dist/update-marker.js.map +1 -1
- package/dist/update-preflight.d.ts +100 -0
- package/dist/update-preflight.js +264 -0
- package/dist/update-preflight.js.map +1 -0
- package/dist/updater-caller.d.ts +31 -0
- package/dist/updater-caller.js +109 -0
- package/dist/updater-caller.js.map +1 -0
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +1 -1
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/providers.d.ts +34 -5
- package/dist/usage/providers.js +160 -27
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/statusline-usage.d.ts +6 -2
- package/dist/usage/statusline-usage.js +10 -4
- package/dist/usage/statusline-usage.js.map +1 -1
- package/dist/view-api.d.ts +1 -1
- package/dist/view-api.js +38 -8
- package/dist/view-api.js.map +1 -1
- package/dist/wake-coordinator.d.ts +11 -0
- package/dist/wake-coordinator.js +38 -4
- package/dist/wake-coordinator.js.map +1 -1
- package/dist/web-api.d.ts +89 -4
- package/dist/web-api.js +551 -45
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +2 -2
- package/dist/web-auth.js +9 -4
- package/dist/web-auth.js.map +1 -1
- package/dist/web-chat-history.d.ts +12 -1
- package/dist/web-chat-history.js +29 -7
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +27 -4
- package/dist/web-commands.js +19 -4
- package/dist/web-commands.js.map +1 -1
- package/dist/web-host-guard.d.ts +3 -11
- package/dist/web-host-guard.js +3 -12
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-images.d.ts +47 -0
- package/dist/web-images.js +169 -0
- package/dist/web-images.js.map +1 -0
- package/dist/web-login.d.ts +14 -0
- package/dist/web-login.js +4 -2
- package/dist/web-login.js.map +1 -1
- package/dist/web-mentions.d.ts +44 -0
- package/dist/web-mentions.js +75 -0
- package/dist/web-mentions.js.map +1 -0
- package/dist/web-prefs.d.ts +81 -0
- package/dist/web-prefs.js +297 -0
- package/dist/web-prefs.js.map +1 -0
- package/dist/web-preview.d.ts +14 -1
- package/dist/web-preview.js +35 -13
- package/dist/web-preview.js.map +1 -1
- package/dist/web-profile.d.ts +32 -0
- package/dist/web-profile.js +132 -0
- package/dist/web-profile.js.map +1 -0
- package/dist/web-reply-files.d.ts +45 -0
- package/dist/web-reply-files.js +223 -0
- package/dist/web-reply-files.js.map +1 -0
- package/dist/web-schedules.d.ts +12 -0
- package/dist/web-schedules.js +34 -0
- package/dist/web-schedules.js.map +1 -0
- package/dist/web-server-drain.d.ts +10 -0
- package/dist/web-server-drain.js +50 -0
- package/dist/web-server-drain.js.map +1 -0
- package/dist/web-session.d.ts +50 -1
- package/dist/web-session.js +63 -6
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +23 -0
- package/dist/web-settings-patch.js +77 -0
- package/dist/web-settings-patch.js.map +1 -0
- package/dist/web-shell-routes.d.ts +5 -1
- package/dist/web-shell-routes.js +10 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-upload.d.ts +17 -4
- package/dist/web-upload.js +64 -13
- package/dist/web-upload.js.map +1 -1
- package/dist/workflow-templates/default.md +1 -1
- package/package.json +1 -1
- package/templates/launchd.plist.ejs +3 -1
- package/templates/systemd.service.ejs +2 -1
package/dist/daemon.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AGENT_WHATS_NEW, agendVersion, planWhatsNew, readWhatsNewRecord, recordWhatsNew, whatsNewNotice } from "./agent-whats-new.js";
|
|
1
2
|
import { join, dirname, basename, resolve } from "node:path";
|
|
2
3
|
import { mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, unlinkSync, rmSync, appendFileSync, statSync, renameSync } from "node:fs";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -8,6 +9,7 @@ import { rotateLogIfNeededAsync } from "./logger.js";
|
|
|
8
9
|
import { coredumpFilterLaunchPrefix } from "./coredump-filter.js";
|
|
9
10
|
import { mcpServerState } from "./mcp-liveness.js";
|
|
10
11
|
import { clearPausedMarker, writePausedMarker } from "./pause-marker.js";
|
|
12
|
+
import { writeBackgroundWorkEndedMarker } from "./background-work-ended.js";
|
|
11
13
|
import { TmuxManager, resolveTmuxLogicalSize } from "./tmux-manager.js";
|
|
12
14
|
import { TranscriptMonitor } from "./transcript-monitor.js";
|
|
13
15
|
import { createTranscriptSource } from "./transcript-sources.js";
|
|
@@ -31,6 +33,7 @@ import { HangDetector } from "./hang-detector.js";
|
|
|
31
33
|
import { writeSecretFile } from "./secret-file.js";
|
|
32
34
|
import { PaneWriteLock } from "./pane-write-lock.js";
|
|
33
35
|
import { buildFleetInstructions } from "./instructions.js";
|
|
36
|
+
import { formatHumanInboundMessage, prepareDeliveryWire, steerInboundEnvelope, BTW_INBOUND_PREFIX, humanBodyHash, humanOwnerObjectId } from "./human-delivery-receipts.js";
|
|
34
37
|
import { formatCrossInstanceInboundMessage, renderCrossInstanceHandoffMetadata, } from "./cross-instance-envelope.js";
|
|
35
38
|
import { agendMessageInInput, bottomRowIsReady, inputAreaText, inputShowsPastedText, pastedTextSignature, pasteLeftInInput, strandedAgendMessageInInput } from "./pane-input-residue.js";
|
|
36
39
|
import { TurnReplyGuard } from "./turn-reply-guard.js";
|
|
@@ -44,7 +47,9 @@ import { measureSyncWork, noteSyncWork } from "./sync-work-attribution.js";
|
|
|
44
47
|
import { TranscriptDeltaReader } from "./transcript-delta-reader.js";
|
|
45
48
|
import { buildResumeContinuation, clearInFlightTurnMarker, consumeInFlightTurnMarker, decideTurnResume, normalizeResumeBackend, readResumeFingerprint, RESUME_BLOCKING_DELIVERY_STATES, writeInFlightTurnMarker, } from "./turn-resume.js";
|
|
46
49
|
import { codexLiveRowMatches } from "./backend/codex.js";
|
|
50
|
+
import { claudeConfigDirFor, claudeProfileConfigDir, claudeSwitchDialog, claudeSwitchTargetMatches } from "./backend/claude-code.js";
|
|
47
51
|
import { InteractionObservation, INTERACTION_CONFIRM_MS, sameInteractionOwner } from "./interaction-observation.js";
|
|
52
|
+
import { unrecognisedPromptShape } from "./unrecognised-prompt.js";
|
|
48
53
|
const __filename = fileURLToPath(import.meta.url);
|
|
49
54
|
const __dirname = dirname(__filename);
|
|
50
55
|
// Tool routing sets — module-level to avoid re-creation on every handleToolCall
|
|
@@ -77,10 +82,18 @@ const REPLY_GUARD_IDLE_CONFIRM_MS = 60_000;
|
|
|
77
82
|
* down like a turn the transcript cannot place — never a recovery.
|
|
78
83
|
*/
|
|
79
84
|
const TRANSCRIPT_RUNNING_HOLD_MAX_MS = 30 * 60_000;
|
|
85
|
+
/**
|
|
86
|
+
* #1641: an open turn in the CLI's transcript holds the instance busy (no paste, whatever the pane shows) only while the
|
|
87
|
+
* transcript is still moving: a turn whose end was missed must not pin the instance busy forever. A live turn writes
|
|
88
|
+
* often; a long silent tool run shows its own busy row on the pane.
|
|
89
|
+
*/
|
|
90
|
+
export const TRANSCRIPT_OPEN_TURN_QUIET_MS = 15 * 60_000;
|
|
80
91
|
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.";
|
|
81
92
|
/** Point a resumed CLI at its one backend-native instruction source. */
|
|
82
|
-
/** The outbox evidence for a delivery the CLI's transcript shows taken (#758; #1201 absorbed). */
|
|
83
|
-
function transcriptMarkerEvidence(kind) {
|
|
93
|
+
/** The outbox evidence for a delivery the CLI's transcript shows taken (#758; #1201 absorbed; #1647 a raw paste's text). */
|
|
94
|
+
function transcriptMarkerEvidence(kind, rawPaste = false) {
|
|
95
|
+
if (rawPaste)
|
|
96
|
+
return "transcript-turn-user-entry-raw-paste-text";
|
|
84
97
|
return kind === "user" ? "transcript-marker" : kind === "absorbed" ? "transcript-marker-absorbed" : "transcript-marker-queued";
|
|
85
98
|
}
|
|
86
99
|
export function buildInstructionReloadNotice(binaryName, instanceName, instanceDir, backendSource) {
|
|
@@ -472,6 +485,16 @@ const POST_ENTER_PROOF_WINDOW_MS = 3_000;
|
|
|
472
485
|
/** A final bounded observation before an ambiguous Codex submit can be classified. */
|
|
473
486
|
const CODEX_LATE_PROOF_MS = 5_000;
|
|
474
487
|
const POST_ENTER_PROOF_POLL_MS = 250;
|
|
488
|
+
/** #1647: a raw paste a pane with no transcript showed taken — its text left the input row (or the CLI's own busy marker
|
|
489
|
+
* replaced it), with output after the Enter. */
|
|
490
|
+
const RAW_PASTE_LEFT_INPUT = "raw-paste-left-input-after-output";
|
|
491
|
+
/**
|
|
492
|
+
* #1647/#1663 P1-2: backends whose transcript AgEnD holds for this instance alone, so a raw paste's exact text in it is
|
|
493
|
+
* this paste's turn. claude-code: the path is its own statusline.json's transcript_path (written by this instance's
|
|
494
|
+
* CLI). Not codex: its rollout is chosen by working directory, which another instance can share, and AgEnD holds no
|
|
495
|
+
* session id to bind it — so a codex raw paste the pane could not prove stays uncertain.
|
|
496
|
+
*/
|
|
497
|
+
const RAW_TEXT_TRANSCRIPT_BACKENDS = ["claude-code"];
|
|
475
498
|
/**
|
|
476
499
|
* A readerless CLI's delivery is proven by its own transcript, which lags the pane by a flush. Only a delta that was
|
|
477
500
|
* read and carried no marker for this long is held against the delivery — a late flush must not raise a false ⚠️.
|
|
@@ -503,8 +526,16 @@ const DIALOG_PROBE_UNKNOWN_MAX = 40;
|
|
|
503
526
|
/** Startup dialog scan: poll cadence and wall-clock budget (keys and waits included). */
|
|
504
527
|
const STARTUP_DIALOG_POLL_MS = 500;
|
|
505
528
|
const STARTUP_DIALOG_BUDGET_MS = 30_000;
|
|
529
|
+
/** #1690: after AgEnD's own `/model`, how long the pane is read for claude's "Switch model?" or its result. */
|
|
530
|
+
export const OWN_SWITCH_WAIT_MS = 6_000;
|
|
531
|
+
/** #1690: after an answer to that dialog, how long it gets to go away. */
|
|
532
|
+
const OWN_SWITCH_SETTLE_MS = 3_000;
|
|
506
533
|
/** A blocking dialog still on screen this long after first being seen is reported for a human. */
|
|
507
534
|
const DIALOG_PARKED_NOTIFY_MS = 60_000;
|
|
535
|
+
/** #1691: a prompt no table knows must sit unchanged this long (monotonic) before it holds deliveries and is reported. */
|
|
536
|
+
export const UNRECOGNISED_PROMPT_STABLE_MS = 60_000;
|
|
537
|
+
/** #1691: the rows under a prompt a backend without a composer reader shows its ready chrome in (status bar, hint). */
|
|
538
|
+
const COMPOSER_TAIL_ROWS = 4;
|
|
508
539
|
/** After a runtime dialog's keys, how long the pane gets to repaint before it is read again to see whether they took. */
|
|
509
540
|
const DIALOG_ANSWER_SETTLE_MS = 300;
|
|
510
541
|
/** Consecutive answers one runtime dialog ignored (still on screen right after the keys) before it is reported for a human. */
|
|
@@ -614,6 +645,10 @@ export function channelStatus(meta) {
|
|
|
614
645
|
// they are the dashboard's ticks (the fleet routes them by the message id, never to a platform).
|
|
615
646
|
if (!messageId || (!chatId && meta.source !== "web"))
|
|
616
647
|
return undefined;
|
|
648
|
+
// #1823: a notice from AgEnD itself (a poll's results) names its chat so the answer goes there, but its message id
|
|
649
|
+
// is AgEnD's own, not a platform message: there is nothing to react to.
|
|
650
|
+
if (meta.source === "agend-system")
|
|
651
|
+
return undefined;
|
|
617
652
|
if (!chatId)
|
|
618
653
|
return { chatId: "", messageId };
|
|
619
654
|
const threadId = meta.thread_id || undefined;
|
|
@@ -1058,6 +1093,10 @@ export class Daemon extends EventEmitter {
|
|
|
1058
1093
|
ipcServer = null;
|
|
1059
1094
|
messageBus;
|
|
1060
1095
|
transcriptMonitor = null;
|
|
1096
|
+
/** #1218: the transcript this instance's monitor follows now (the Log tab reads it on demand; nothing runs for it here). */
|
|
1097
|
+
logTranscriptPath() { return this.transcriptMonitor?.currentPath() ?? null; }
|
|
1098
|
+
/** #1218 (kiro): the launch this daemon's backend last built, for the Log's reason only (kiro-log-gate.ts). */
|
|
1099
|
+
logLaunch() { return this.backend?.logLaunch?.() ?? null; }
|
|
1061
1100
|
guardian = null;
|
|
1062
1101
|
adapter = null;
|
|
1063
1102
|
pendingIpcRequests = new Map();
|
|
@@ -1144,6 +1183,34 @@ export class Daemon extends EventEmitter {
|
|
|
1144
1183
|
dialogParkedKey = null;
|
|
1145
1184
|
dialogParkedInteractionEpisode = null;
|
|
1146
1185
|
dialogParkedReported = false;
|
|
1186
|
+
/**
|
|
1187
|
+
* #1691: the prompt at the bottom of the pane that no backend table knows — its block, since when (performance.now)
|
|
1188
|
+
* it has been unchanged, and whether that has lasted UNRECOGNISED_PROMPT_STABLE_MS. Updated by the runtime scan.
|
|
1189
|
+
*/
|
|
1190
|
+
unrecognisedPrompt = null;
|
|
1191
|
+
/**
|
|
1192
|
+
* #1691: when (performance.now) the newest pane read that decided the evidence above was taken — a scan or a fresh
|
|
1193
|
+
* read, confirming, restarting or ending it. A read taken before it finishes late and says nothing about now: it may
|
|
1194
|
+
* neither clear, restart nor confirm (#1695 r4).
|
|
1195
|
+
*/
|
|
1196
|
+
unrecognisedPromptReadAt = -Infinity;
|
|
1197
|
+
/**
|
|
1198
|
+
* #1691: the catch-all for that prompt, as a hold-only runtime dialog — so the delivery hold, awaiting_input, the
|
|
1199
|
+
* Needs-you item and the single parked report are the existing ones. Never answered (hold-only, no keys).
|
|
1200
|
+
*/
|
|
1201
|
+
unrecognisedPromptDialog = {
|
|
1202
|
+
pattern: /(?!)unrecognised-prompt-1691/,
|
|
1203
|
+
keys: [],
|
|
1204
|
+
description: "a prompt AgEnD does not recognise",
|
|
1205
|
+
holdOnly: true,
|
|
1206
|
+
isActive: (pane) => this.unrecognisedPromptActive(pane),
|
|
1207
|
+
requestIdentity: () => this.unrecognisedPrompt?.block ?? null,
|
|
1208
|
+
};
|
|
1209
|
+
/**
|
|
1210
|
+
* #1690: AgEnD's own runtime `/model` in flight — from the moment its paste went in until its "Switch model?" dialog
|
|
1211
|
+
* (if any) is answered. Every delivery is held meanwhile: the dialog's default cursor is Yes, a stray Enter confirms it.
|
|
1212
|
+
*/
|
|
1213
|
+
ownSwitch = null;
|
|
1147
1214
|
/**
|
|
1148
1215
|
* The runtime dialog the monitor is answering, and how many of its answers in a row did nothing (the dialog was
|
|
1149
1216
|
* still on screen right after the keys). Not the on-screen duration: a burst of FRESH prompts, each answered, has a
|
|
@@ -1237,6 +1304,11 @@ export class Daemon extends EventEmitter {
|
|
|
1237
1304
|
turnReplyGuard = new TurnReplyGuard();
|
|
1238
1305
|
/** #1510: turn boundaries from the CLI's transcript, for a backend whose guard reads its turn end there. */
|
|
1239
1306
|
transcriptTurns = new TranscriptTurnLedger();
|
|
1307
|
+
/** #1641: when this launch's transcript monitor last read anything (a turn boundary, a tool, assistant text); performance.now(). */
|
|
1308
|
+
transcriptActivityAt = 0;
|
|
1309
|
+
/** #1641: the launch and composer a stuck pane's idle evidence is about, and since when (performance.now()); see recoverStuck. */
|
|
1310
|
+
stuckRecoveryKey = null;
|
|
1311
|
+
stuckRecoverySince = 0;
|
|
1240
1312
|
/** #1510: since when (monotonic) this generation has been held as running in the transcript. */
|
|
1241
1313
|
transcriptRunningSince = null;
|
|
1242
1314
|
/** Prevent a visible stale XML fragment from being recovered on later turns. */
|
|
@@ -1388,6 +1460,8 @@ export class Daemon extends EventEmitter {
|
|
|
1388
1460
|
recentPastes = [];
|
|
1389
1461
|
pendingInstructionsUpdate;
|
|
1390
1462
|
pendingInstructionsNotice = false;
|
|
1463
|
+
/** #1633: the one-time "what's new for you" notice, pasted before the next real delivery; recorded once pasted. */
|
|
1464
|
+
pendingWhatsNew = null;
|
|
1391
1465
|
// Whether the warmup steering-reload notice should be injected after spawn.
|
|
1392
1466
|
// Set in trySpawn by comparing the freshly-built instructions against the
|
|
1393
1467
|
// last value the agent was told about (prev-instructions). Skipped when
|
|
@@ -1856,8 +1930,14 @@ export class Daemon extends EventEmitter {
|
|
|
1856
1930
|
this.finishDurableDelivery(delivery, "delivered", OUTPUT_EDGE_ONLY_EVIDENCE);
|
|
1857
1931
|
return;
|
|
1858
1932
|
}
|
|
1933
|
+
// #1647: a raw paste on a pane with no transcript (kiro), taken by the CLI as its own input row shows.
|
|
1934
|
+
if (verdict.proof === RAW_PASTE_LEFT_INPUT) {
|
|
1935
|
+
this.finishDurableDelivery(delivery, "delivered", verdict.proof, verdict);
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1859
1938
|
this.finishDurableDelivery(delivery, "delivered", verdict.transcriptProof
|
|
1860
|
-
? `positive submission proof; ${transcriptMarkerEvidence(verdict.transcriptProof
|
|
1939
|
+
? `positive submission proof; ${transcriptMarkerEvidence(verdict.transcriptProof, verdict.submissionMode === "raw_paste")}`
|
|
1940
|
+
: "positive submission proof", verdict);
|
|
1861
1941
|
}
|
|
1862
1942
|
/**
|
|
1863
1943
|
* Finish a delivery that only the output edge vouched for, by the delivery's own marker in the CLI's transcript (#758).
|
|
@@ -1873,23 +1953,28 @@ export class Daemon extends EventEmitter {
|
|
|
1873
1953
|
* TRANSCRIPT_PROOF_WINDOW_MS). It never finishes a row that stop has handed to reconciliation; a pause or respawn
|
|
1874
1954
|
* only ends the wait, falling back to today's outcome.
|
|
1875
1955
|
*/
|
|
1876
|
-
async proveDeliveryFromTranscript(delivery, checkpoint) {
|
|
1956
|
+
async proveDeliveryFromTranscript(delivery, checkpoint, rawPaste) {
|
|
1877
1957
|
const spawn = this.spawnGeneration;
|
|
1878
1958
|
const fence = this.launchFenceEpoch;
|
|
1879
1959
|
let last = "unavailable";
|
|
1880
1960
|
let outcome = "delivered";
|
|
1881
1961
|
let evidence = OUTPUT_EDGE_ONLY_EVIDENCE;
|
|
1962
|
+
// #1647: a raw paste has no marker; its text is the needle, and its pane proof already failed — so without the turn
|
|
1963
|
+
// in the transcript it stays uncertain (with the pane's reason), never the output-edge outcome.
|
|
1964
|
+
const needle = rawPaste ? { rawText: rawPaste.text } : delivery.deliveryId;
|
|
1965
|
+
let found = false;
|
|
1882
1966
|
try {
|
|
1883
1967
|
for (let polls = 0; polls <= TRANSCRIPT_PROOF_WINDOW_MS / TRANSCRIPT_PROOF_POLL_MS; polls++) {
|
|
1884
1968
|
if (this.deliveryWritesStopping)
|
|
1885
1969
|
return; // reconciliation owns the row now
|
|
1886
|
-
const
|
|
1887
|
-
if (
|
|
1970
|
+
const look = await scanTranscriptForDeliveryMarker(checkpoint.path, checkpoint.offset, checkpoint.backend, needle);
|
|
1971
|
+
if (look === "user" || look === "absorbed" || look === "queued") {
|
|
1972
|
+
found = true;
|
|
1888
1973
|
outcome = "delivered";
|
|
1889
|
-
evidence = transcriptMarkerEvidence(
|
|
1974
|
+
evidence = transcriptMarkerEvidence(look, !!rawPaste);
|
|
1890
1975
|
break;
|
|
1891
1976
|
}
|
|
1892
|
-
last =
|
|
1977
|
+
last = look;
|
|
1893
1978
|
if (spawn !== this.spawnGeneration || fence !== this.launchFenceEpoch) {
|
|
1894
1979
|
last = "unavailable";
|
|
1895
1980
|
break;
|
|
@@ -1909,6 +1994,11 @@ export class Daemon extends EventEmitter {
|
|
|
1909
1994
|
catch (err) {
|
|
1910
1995
|
this.logger.debug({ err }, "Transcript proof of a delivery failed — keeping the output-edge outcome");
|
|
1911
1996
|
}
|
|
1997
|
+
if (rawPaste && !found) {
|
|
1998
|
+
outcome = "uncertain";
|
|
1999
|
+
evidence = `${rawPaste.unproven}; no-transcript-turn (${last})`;
|
|
2000
|
+
this.logger.warn({ deliveryId: delivery.deliveryId, cause: evidence }, "Raw paste outcome uncertain — not re-pasting");
|
|
2001
|
+
}
|
|
1912
2002
|
// The settlement itself can throw (e.g. the outbox store is unhealthy), and this runs on a detached `void`
|
|
1913
2003
|
// promise: an uncaught throw is an unhandled rejection, which the fleet restart path turns into a global fault.
|
|
1914
2004
|
// Settle observably and leave the row for reconciliation — never a false completion, never another paste (#1207 P2).
|
|
@@ -1922,6 +2012,7 @@ export class Daemon extends EventEmitter {
|
|
|
1922
2012
|
}
|
|
1923
2013
|
}
|
|
1924
2014
|
deferDurableDelivery(meta, reason) {
|
|
2015
|
+
this.finishHumanReceipt(meta, { reached: false });
|
|
1925
2016
|
const delivery = this.durableDeliveryAttempt(meta);
|
|
1926
2017
|
if (delivery === null || delivery === false)
|
|
1927
2018
|
return;
|
|
@@ -3174,7 +3265,6 @@ export class Daemon extends EventEmitter {
|
|
|
3174
3265
|
if (this.runtimeMonitorsFrozen || this.errorMonitorTimer)
|
|
3175
3266
|
return;
|
|
3176
3267
|
const patterns = this.backend?.getErrorPatterns?.() ?? [];
|
|
3177
|
-
const dialogs = this.backend?.getRuntimeDialogs?.() ?? [];
|
|
3178
3268
|
if (!this.tmux)
|
|
3179
3269
|
return;
|
|
3180
3270
|
if (!this.backend)
|
|
@@ -3210,6 +3300,9 @@ export class Daemon extends EventEmitter {
|
|
|
3210
3300
|
scanStartedAt = performance.now();
|
|
3211
3301
|
if (stale())
|
|
3212
3302
|
return;
|
|
3303
|
+
this.trackUnrecognisedPrompt(pane, captureMono, this.instanceStateLastOutputAt > 0 && this.instanceStateLastOutputAt >= captureAt);
|
|
3304
|
+
// The backend's dialogs, and #1691's catch-all while an unrecognised prompt is active (decided just above).
|
|
3305
|
+
const dialogs = this.runtimeDialogs();
|
|
3213
3306
|
if (this.instanceStateLastOutputAt > 0 && this.instanceStateLastOutputAt >= captureAt) {
|
|
3214
3307
|
this.unverifyInteraction(captureMono, captureOrder);
|
|
3215
3308
|
}
|
|
@@ -3689,14 +3782,21 @@ export class Daemon extends EventEmitter {
|
|
|
3689
3782
|
isDeliveryEpochCurrent(epoch) {
|
|
3690
3783
|
return epoch === this.deliveryEpoch;
|
|
3691
3784
|
}
|
|
3692
|
-
queueRawPaste(rawText, deliveryEpoch = this.deliveryEpoch, confirmClear = false, durableAttempt) {
|
|
3785
|
+
queueRawPaste(rawText, deliveryEpoch = this.deliveryEpoch, confirmClear = false, durableAttempt, ownSwitch) {
|
|
3786
|
+
const reportSwitch = (result) => {
|
|
3787
|
+
if (!ownSwitch)
|
|
3788
|
+
return;
|
|
3789
|
+
this.ipcServer?.broadcast({ type: "runtime_switch_result", id: ownSwitch.id, ...result });
|
|
3790
|
+
};
|
|
3693
3791
|
if (!this.tmux || !this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
3694
3792
|
if (durableAttempt)
|
|
3695
3793
|
this.retryDurableDeliveryBeforeBegin(durableAttempt, "raw_paste target unavailable before pane queue");
|
|
3794
|
+
reportSwitch({ outcome: "not_sent" });
|
|
3696
3795
|
return;
|
|
3697
3796
|
}
|
|
3698
3797
|
this.pasteLock = this.pasteLock.then(async () => {
|
|
3699
3798
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
3799
|
+
reportSwitch({ outcome: "not_sent" });
|
|
3700
3800
|
this.logger.info("Pending raw delivery dropped by user cancel");
|
|
3701
3801
|
if (durableAttempt)
|
|
3702
3802
|
this.retryDurableDeliveryBeforeBegin(durableAttempt, "raw_paste epoch changed while queued");
|
|
@@ -3708,6 +3808,7 @@ export class Daemon extends EventEmitter {
|
|
|
3708
3808
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
3709
3809
|
if (durableAttempt)
|
|
3710
3810
|
this.retryDurableDeliveryBeforeBegin(durableAttempt, "raw_paste epoch changed while waking");
|
|
3811
|
+
reportSwitch({ outcome: "not_sent" }); // #1690: cancelled before anything was typed — said at once, nothing saved
|
|
3711
3812
|
return;
|
|
3712
3813
|
}
|
|
3713
3814
|
const delivered = await this.deliverMessage(rawText, undefined, {
|
|
@@ -3715,11 +3816,25 @@ export class Daemon extends EventEmitter {
|
|
|
3715
3816
|
verdict,
|
|
3716
3817
|
durableAttempt,
|
|
3717
3818
|
preserveExactBytes: true,
|
|
3819
|
+
// #1690: our own /model holds every other delivery from its write on — before the lock is released, so a steer
|
|
3820
|
+
// or /btw waiting on it cannot type into the gap before claude paints "Switch model?" (default cursor: Yes).
|
|
3821
|
+
...(ownSwitch ? { onPaneWritten: () => { this.ownSwitch = ownSwitch; } } : {}),
|
|
3718
3822
|
});
|
|
3719
3823
|
if (delivered)
|
|
3720
3824
|
this.finishDurableSubmission(durableAttempt ?? null, verdict);
|
|
3721
3825
|
else if (durableAttempt && verdict.paneWriteStarted) {
|
|
3722
|
-
|
|
3826
|
+
const unproven = `${verdict.phase ?? "raw-paste"}:${verdict.proof ?? "unverified"}`;
|
|
3827
|
+
const checkpoint = verdict.transcriptCheckpoint;
|
|
3828
|
+
if (checkpoint && RAW_TEXT_TRANSCRIPT_BACKENDS.includes(checkpoint.backend)) {
|
|
3829
|
+
// #1647: the pane could not show it, but the CLI's transcript records the turn the paste started — later
|
|
3830
|
+
// than the pane window sometimes. Wait for it there (bounded), off the paste lock.
|
|
3831
|
+
this.logger.info({ deliveryId: durableAttempt.deliveryId, unproven }, "Raw paste not proven on the pane — asking the CLI's transcript for the turn it started");
|
|
3832
|
+
void this.proveDeliveryFromTranscript(durableAttempt, checkpoint, { text: rawText, unproven });
|
|
3833
|
+
}
|
|
3834
|
+
else {
|
|
3835
|
+
this.logger.warn({ deliveryId: durableAttempt.deliveryId, cause: unproven }, "Raw paste outcome uncertain — not re-pasting");
|
|
3836
|
+
this.finishDurableDelivery(durableAttempt, "uncertain", unproven);
|
|
3837
|
+
}
|
|
3723
3838
|
}
|
|
3724
3839
|
else if (durableAttempt && verdict.durableBeginCommitted) {
|
|
3725
3840
|
this.abortDurableDelivery(durableAttempt, `${verdict.phase ?? "raw-paste"}:${verdict.proof ?? "no-pane-write"}`);
|
|
@@ -3736,10 +3851,31 @@ export class Daemon extends EventEmitter {
|
|
|
3736
3851
|
else if (durableAttempt)
|
|
3737
3852
|
this.retryDurableDeliveryBeforeBegin(durableAttempt, err.message);
|
|
3738
3853
|
this.logger.warn({ err: err.message }, "raw_paste delivery error");
|
|
3854
|
+
if (ownSwitch) {
|
|
3855
|
+
if (this.ownSwitch === ownSwitch)
|
|
3856
|
+
this.ownSwitch = null;
|
|
3857
|
+
reportSwitch({ outcome: "not_confirmed" });
|
|
3858
|
+
}
|
|
3739
3859
|
return;
|
|
3740
3860
|
}
|
|
3741
3861
|
if (confirmClear)
|
|
3742
3862
|
await this.confirmBackendClearDialog();
|
|
3863
|
+
if (ownSwitch) {
|
|
3864
|
+
if (this.ownSwitch !== ownSwitch)
|
|
3865
|
+
reportSwitch({ outcome: "not_sent" }); // its write never began
|
|
3866
|
+
else {
|
|
3867
|
+
try {
|
|
3868
|
+
reportSwitch(await this.answerOwnSwitchDialog(ownSwitch));
|
|
3869
|
+
}
|
|
3870
|
+
catch (err) {
|
|
3871
|
+
this.logger.warn({ err: err.message }, "Answering our own model switch failed");
|
|
3872
|
+
reportSwitch({ outcome: "not_confirmed" });
|
|
3873
|
+
}
|
|
3874
|
+
finally {
|
|
3875
|
+
this.ownSwitch = null;
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3743
3879
|
this.logger.debug({ text: rawText.slice(0, 100) }, "Raw paste delivered");
|
|
3744
3880
|
});
|
|
3745
3881
|
this.pasteLock = this.pasteLock.catch(err => {
|
|
@@ -3758,7 +3894,93 @@ export class Daemon extends EventEmitter {
|
|
|
3758
3894
|
this.deferDurableDelivery(deliveryMeta, "invalid raw_paste delivery attempt metadata");
|
|
3759
3895
|
return;
|
|
3760
3896
|
}
|
|
3761
|
-
|
|
3897
|
+
const sw = msg.runtime_switch;
|
|
3898
|
+
const ownSwitch = sw && typeof sw.id === "string" && sw.kind === "model" && typeof sw.requested === "string"
|
|
3899
|
+
? { id: sw.id, kind: "model", requested: sw.requested } : undefined;
|
|
3900
|
+
this.queueRawPaste(msg.content, this.captureDeliveryEpoch(msg.delivery_epoch), msg.confirm_clear === true, durableAttempt ? { ...durableAttempt, submissionMode: "raw_paste" } : undefined, ownSwitch);
|
|
3901
|
+
}
|
|
3902
|
+
/**
|
|
3903
|
+
* #1690 (the user's choice A): after AgEnD's own `/model <name>`, claude may ask "Switch model?" (a warm cache for
|
|
3904
|
+
* another model). When the model it names is the one asked for, answer Yes; otherwise No (Escape — `Kept model as …`),
|
|
3905
|
+
* and say so. Every key goes on a freshly read, verified dialog under the pane write lock: the same target, and for
|
|
3906
|
+
* Enter the cursor on "1. Yes"; the cursor is walked there with Up, one verified step at a time, never a blind key.
|
|
3907
|
+
* Without the dialog, claude's own "Set model to …" line after the command is the result.
|
|
3908
|
+
*/
|
|
3909
|
+
async answerOwnSwitchDialog(spec) {
|
|
3910
|
+
const tmux = this.tmux;
|
|
3911
|
+
if (!tmux)
|
|
3912
|
+
return { outcome: "not_confirmed" };
|
|
3913
|
+
const read = () => tmux.capturePane().catch(() => "");
|
|
3914
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
3915
|
+
/** One key on a dialog re-read under the lock: still the same target, and (when given) the cursor where expected. */
|
|
3916
|
+
const press = (target, cursor, key) => this.paneWriteLock.run(async () => {
|
|
3917
|
+
if (this.tmux !== tmux)
|
|
3918
|
+
return false;
|
|
3919
|
+
const now = claudeSwitchDialog(await read());
|
|
3920
|
+
if (!now || now.kind !== "model" || now.target !== target || (cursor !== null && now.cursor !== cursor))
|
|
3921
|
+
return false;
|
|
3922
|
+
return tmux.sendSpecialKey(key);
|
|
3923
|
+
});
|
|
3924
|
+
const gone = async () => {
|
|
3925
|
+
const until = performance.now() + OWN_SWITCH_SETTLE_MS;
|
|
3926
|
+
while (performance.now() < until) {
|
|
3927
|
+
if (!claudeSwitchDialog(await read()))
|
|
3928
|
+
return true;
|
|
3929
|
+
await sleep(150);
|
|
3930
|
+
}
|
|
3931
|
+
return false;
|
|
3932
|
+
};
|
|
3933
|
+
const deadline = performance.now() + OWN_SWITCH_WAIT_MS;
|
|
3934
|
+
while (performance.now() < deadline && this.tmux === tmux) {
|
|
3935
|
+
const pane = await read();
|
|
3936
|
+
const dialog = claudeSwitchDialog(pane);
|
|
3937
|
+
if (dialog && dialog.kind === "model") {
|
|
3938
|
+
const shown = dialog.target;
|
|
3939
|
+
if (!claudeSwitchTargetMatches(spec.requested, shown)) {
|
|
3940
|
+
const answered = await press(shown, null, "Escape") && await gone();
|
|
3941
|
+
this.logger.warn({ requested: spec.requested, shown, answered }, "claude would switch to another model than asked — answered No");
|
|
3942
|
+
return { outcome: "mismatch", shown, answered };
|
|
3943
|
+
}
|
|
3944
|
+
// Walk the cursor to "1. Yes" (it starts there; a human may have moved it): Up from row 2, re-read each time.
|
|
3945
|
+
let current = dialog;
|
|
3946
|
+
for (let step = 0; step < 2 && current.cursor === 2; step++) {
|
|
3947
|
+
if (!await press(shown, 2, "Up"))
|
|
3948
|
+
break;
|
|
3949
|
+
await sleep(120);
|
|
3950
|
+
const next = claudeSwitchDialog(await read());
|
|
3951
|
+
if (!next || next.target !== shown)
|
|
3952
|
+
break;
|
|
3953
|
+
current = next;
|
|
3954
|
+
}
|
|
3955
|
+
if (current.cursor !== 1 || !await press(shown, 1, "Enter") || !await gone()) {
|
|
3956
|
+
this.logger.warn({ requested: spec.requested, shown, cursor: current.cursor }, "Our own model switch dialog could not be confirmed on a verified cursor — left for a human");
|
|
3957
|
+
return { outcome: "not_confirmed", shown };
|
|
3958
|
+
}
|
|
3959
|
+
this.logger.info({ requested: spec.requested, shown }, "Confirmed our own model switch (claude: the next reply re-reads the history)");
|
|
3960
|
+
return { outcome: "switched", confirmed: true, shown };
|
|
3961
|
+
}
|
|
3962
|
+
if (Daemon.switchResultAfterCommand(pane, spec.requested))
|
|
3963
|
+
return { outcome: "switched", confirmed: false };
|
|
3964
|
+
await sleep(200);
|
|
3965
|
+
}
|
|
3966
|
+
return { outcome: "not_confirmed" };
|
|
3967
|
+
}
|
|
3968
|
+
/**
|
|
3969
|
+
* #1690: claude's "Set model to …" line under our `/model <requested>` (no dialog was needed). Only the newest command
|
|
3970
|
+
* on the pane counts: an older identical `/model` with its result further up (switched away and back) must not pass
|
|
3971
|
+
* for this one before it has even painted — that left its dialog unanswered (live, 2.1.296).
|
|
3972
|
+
*/
|
|
3973
|
+
static switchResultAfterCommand(pane, requested) {
|
|
3974
|
+
const rows = pane.replace(/\r/g, "").split("\n");
|
|
3975
|
+
let last = -1;
|
|
3976
|
+
for (let i = rows.length - 1; i >= 0; i--)
|
|
3977
|
+
if (/^❯[ \t\u00a0]+\S/.test(rows[i])) {
|
|
3978
|
+
last = i;
|
|
3979
|
+
break;
|
|
3980
|
+
}
|
|
3981
|
+
if (last < 0 || rows[last].replace(/^❯[ \t\u00a0]+/, "").trimEnd() !== `/model ${requested}`)
|
|
3982
|
+
return false;
|
|
3983
|
+
return rows.slice(last + 1).some(row => /^\s*⎿?\s*Set model to /.test(row));
|
|
3762
3984
|
}
|
|
3763
3985
|
/**
|
|
3764
3986
|
* Confirm a backend's terminal-level /clear prompt after the channel-level
|
|
@@ -4021,6 +4243,13 @@ export class Daemon extends EventEmitter {
|
|
|
4021
4243
|
if (!backend.quitBlockedByDialog(pane))
|
|
4022
4244
|
return false;
|
|
4023
4245
|
this.logger.warn({ reason }, "The CLI asked to confirm its exit (background work is running) — cancelling that prompt and stopping the process, not answering it");
|
|
4246
|
+
// #1716: the background work is about to be ended — record it where the
|
|
4247
|
+
// stop/restart reply and the instance topic notice can report it, so the
|
|
4248
|
+
// kill is never silent.
|
|
4249
|
+
try {
|
|
4250
|
+
writeBackgroundWorkEndedMarker(this.instanceDir, reason);
|
|
4251
|
+
}
|
|
4252
|
+
catch { /* reporting only */ }
|
|
4024
4253
|
await this.tmux.sendSpecialKey("Escape").catch(() => false);
|
|
4025
4254
|
return true;
|
|
4026
4255
|
}
|
|
@@ -4222,7 +4451,9 @@ export class Daemon extends EventEmitter {
|
|
|
4222
4451
|
// Bare backend regexes are not proof of a live modal. The canonical
|
|
4223
4452
|
// isActive predicate replaces pattern.test(), preserving table order.
|
|
4224
4453
|
const dialog = dialogs.find(d => d.isActive && (d.inputBlocked || d.blocksDelivery || d.holdOnly)
|
|
4225
|
-
&& Daemon.dialogMatches(d, pane)
|
|
4454
|
+
&& Daemon.dialogMatches(d, pane)
|
|
4455
|
+
// #1690: AgEnD's own switch dialog, being answered right now, is nobody's to answer.
|
|
4456
|
+
&& !(this.ownSwitch && claudeSwitchDialog(pane) !== null));
|
|
4226
4457
|
if (dialog) {
|
|
4227
4458
|
const identity = dialog.requestIdentity?.(pane) ?? pane.replace(/\r/g, "")
|
|
4228
4459
|
.split("\n").map(row => row.replace(/^([ \t]*)[❯›]([ \t]+)(?=\d\.|Yes\b|No\b|Resume\b)/, "$1$2").trimEnd())
|
|
@@ -4238,7 +4469,7 @@ export class Daemon extends EventEmitter {
|
|
|
4238
4469
|
// No captured text leaves this function or the private observer identity.
|
|
4239
4470
|
return evidence ? { ...evidence, identity: createHash("sha256").update(evidence.identity).digest("hex") } : null;
|
|
4240
4471
|
}
|
|
4241
|
-
observeInteractionPane(pane, observedAt, monotonicAt, dialogs = this.
|
|
4472
|
+
observeInteractionPane(pane, observedAt, monotonicAt, dialogs = this.runtimeDialogs(), order = ++this.interactionCaptureSerial, allowConfirmation = true) {
|
|
4242
4473
|
const owner = this.interactionOwner();
|
|
4243
4474
|
const evidence = this.interactionEvidence(pane, dialogs);
|
|
4244
4475
|
if (!this.interactionObservation.observe(evidence, owner, observedAt, monotonicAt, this.interactionOutputRevision !== this.interactionObservedOutputRevision, order))
|
|
@@ -4351,6 +4582,39 @@ export class Daemon extends EventEmitter {
|
|
|
4351
4582
|
this.pauseRequested = false;
|
|
4352
4583
|
return;
|
|
4353
4584
|
}
|
|
4585
|
+
// #1717: an idle pause is AgEnD-initiated and silent — it must not kill
|
|
4586
|
+
// the user's running background work the way an explicit stop may. Defer
|
|
4587
|
+
// this cycle while the backend reports background work; the next idle
|
|
4588
|
+
// tick retries once the task ends. A pane-capture failure proceeds (the
|
|
4589
|
+
// quit-menu handler below stays the backstop), never wedges unpaused.
|
|
4590
|
+
if (this.backend?.backgroundWorkRunning && this.tmux) {
|
|
4591
|
+
const tmux = this.tmux;
|
|
4592
|
+
const windowId = tmux.getWindowId?.();
|
|
4593
|
+
const generation = this.spawnGeneration;
|
|
4594
|
+
let pane = null;
|
|
4595
|
+
try {
|
|
4596
|
+
pane = await tmux.capturePane();
|
|
4597
|
+
}
|
|
4598
|
+
catch { /* capture failed — pause as before, after revalidation */ }
|
|
4599
|
+
// #1730 P2-2: the await yields — the pane read is only valid for the
|
|
4600
|
+
// world it was taken from. Revalidate owner, phase, state and queue
|
|
4601
|
+
// before acting on it; anything that moved defers this cycle.
|
|
4602
|
+
if (this.tmux !== tmux
|
|
4603
|
+
|| this.tmux?.getWindowId?.() !== windowId
|
|
4604
|
+
|| this.spawnGeneration !== generation
|
|
4605
|
+
|| this.getPauseWakeState() !== "active"
|
|
4606
|
+
|| !(this.instanceState === "idle" || (allowStuck && this.instanceState === "stuck"))
|
|
4607
|
+
|| this.pasteQueueDepth > 0) {
|
|
4608
|
+
this.logger.info("Deferring auto-pause: pause preconditions changed during the pane read");
|
|
4609
|
+
this.pauseRequested = false;
|
|
4610
|
+
return;
|
|
4611
|
+
}
|
|
4612
|
+
if (pane !== null && this.backend?.backgroundWorkRunning?.(pane)) {
|
|
4613
|
+
this.logger.info("Deferring auto-pause: background work is still running");
|
|
4614
|
+
this.pauseRequested = false;
|
|
4615
|
+
return;
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
4354
4618
|
this.pauseWakeState = "pausing";
|
|
4355
4619
|
this.healthCheckPaused = true;
|
|
4356
4620
|
this.freezeRuntimeMonitors();
|
|
@@ -4723,10 +4987,7 @@ export class Daemon extends EventEmitter {
|
|
|
4723
4987
|
backend: resumeBackend ?? "unknown",
|
|
4724
4988
|
cwd: resumeCwd,
|
|
4725
4989
|
armedAt: Date.now(),
|
|
4726
|
-
before: readResumeFingerprint(normalizeResumeBackend(resumeBackend),
|
|
4727
|
-
instanceDir: this.instanceDir,
|
|
4728
|
-
workingDirectory: resumeCwd,
|
|
4729
|
-
}),
|
|
4990
|
+
before: readResumeFingerprint(normalizeResumeBackend(resumeBackend), this.resumeRoots(resumeCwd)),
|
|
4730
4991
|
});
|
|
4731
4992
|
}
|
|
4732
4993
|
}
|
|
@@ -4772,7 +5033,7 @@ export class Daemon extends EventEmitter {
|
|
|
4772
5033
|
&& this.spawnGeneration === gateSpawn
|
|
4773
5034
|
&& this.launchFenceEpoch === gateFence;
|
|
4774
5035
|
const backend = this.resolveResumeBackend() ?? normalizeResumeBackend(marker.backend);
|
|
4775
|
-
const roots =
|
|
5036
|
+
const roots = this.resumeRoots(this.config.working_directory ?? "");
|
|
4776
5037
|
// This boot's CLI spawn is what the daemon itself caused: pre-restart
|
|
4777
5038
|
// writes (including pre-crash progress) are excluded, and the flush
|
|
4778
5039
|
// grace holds the decision until post-spawn writes are observable.
|
|
@@ -5041,9 +5302,68 @@ export class Daemon extends EventEmitter {
|
|
|
5041
5302
|
buildTranscriptMonitor(source) {
|
|
5042
5303
|
const monitor = new TranscriptMonitor(this.instanceDir, this.logger, source);
|
|
5043
5304
|
this.transcriptTurns.reset();
|
|
5044
|
-
|
|
5305
|
+
this.transcriptActivityAt = 0;
|
|
5306
|
+
// Monotonic: a wall-clock correction must neither end nor extend the open-turn hold (#1641 R1).
|
|
5307
|
+
const touch = () => { this.transcriptActivityAt = performance.now(); };
|
|
5308
|
+
monitor.on("turns", (turns) => { this.transcriptTurns.observe(turns, performance.now()); touch(); });
|
|
5309
|
+
// #1641 R2: a re-baselined source starts over, and so does what it said (an open turn read before is withdrawn).
|
|
5310
|
+
monitor.on("reset", () => { if (this.transcriptMonitor === monitor) {
|
|
5311
|
+
this.transcriptTurns.reset();
|
|
5312
|
+
this.transcriptActivityAt = 0;
|
|
5313
|
+
} });
|
|
5314
|
+
for (const event of ["tool_use", "tool_result", "assistant_text"])
|
|
5315
|
+
monitor.on(event, touch);
|
|
5045
5316
|
return monitor;
|
|
5046
5317
|
}
|
|
5318
|
+
/**
|
|
5319
|
+
* #1641: what the CLI's own transcript (codex's rollout) says about its turn. `open`: a turn started and has not ended,
|
|
5320
|
+
* and the transcript moved within TRANSCRIPT_OPEN_TURN_QUIET_MS. `closed`: every turn read since this launch's monitor
|
|
5321
|
+
* began has ended. `unknown`: not a transcript backend, nothing followed, or an open turn gone quiet (a missed end).
|
|
5322
|
+
*/
|
|
5323
|
+
transcriptTurnState(now = performance.now()) {
|
|
5324
|
+
if (this.backend?.turnEndFromTranscript !== true || this.transcriptMonitor?.following() !== true)
|
|
5325
|
+
return "unknown";
|
|
5326
|
+
// #1641 R2: an empty ledger after a baseline that could not see the current turn's start is not "closed".
|
|
5327
|
+
if (this.transcriptMonitor.turnStateKnown?.() !== true)
|
|
5328
|
+
return "unknown";
|
|
5329
|
+
const since = this.transcriptTurns.openSince();
|
|
5330
|
+
if (since === null)
|
|
5331
|
+
return "closed";
|
|
5332
|
+
return now - Math.max(since, this.transcriptActivityAt) < TRANSCRIPT_OPEN_TURN_QUIET_MS ? "open" : "unknown";
|
|
5333
|
+
}
|
|
5334
|
+
/**
|
|
5335
|
+
* #1641: a stuck pane that is in fact an idle CLI (a footer this build cannot read) must not stay stuck for hours with
|
|
5336
|
+
* every delivery parked behind it. It recovers when the backend sees its own live, EMPTY composer with no busy, queued
|
|
5337
|
+
* or dialog marker (isStableUnknownLayoutIdlePane) AND the transcript shows no open turn — or, with no transcript to
|
|
5338
|
+
* ask, that same composer held unchanged for UNKNOWN_LAYOUT_STABLE_MS (#978's rule). An open turn never recovers.
|
|
5339
|
+
*/
|
|
5340
|
+
recoverStuck(pane, snapshot, changeAt, turn) {
|
|
5341
|
+
const machine = this.instanceStateMachine;
|
|
5342
|
+
const idleComposer = this.backend?.isStableUnknownLayoutIdlePane?.(pane) === true;
|
|
5343
|
+
// The resume load (#1031: `Resuming session…` over the same composer) is input that is not open yet (#1641 R1).
|
|
5344
|
+
if (!machine || !idleComposer || turn === "open" || this.inputTransientInPane(pane)) {
|
|
5345
|
+
this.stuckRecoveryKey = null;
|
|
5346
|
+
return snapshot;
|
|
5347
|
+
}
|
|
5348
|
+
// The evidence is about this launch and this screen. Once it holds, it keeps holding — a still pane reads stuck
|
|
5349
|
+
// again at the next sweep (its footer is still unreadable) and must not start the window over (#1641 R1).
|
|
5350
|
+
const mono = performance.now();
|
|
5351
|
+
const key = `${this.spawnGeneration}:${this.launchAttempt}\n${pane.replace(/⋆/gu, "").replace(/[ \t]+$/gmu, "")}`;
|
|
5352
|
+
if (key !== this.stuckRecoveryKey) {
|
|
5353
|
+
this.stuckRecoveryKey = key;
|
|
5354
|
+
this.stuckRecoverySince = mono;
|
|
5355
|
+
}
|
|
5356
|
+
if (turn === "unknown" && mono - this.stuckRecoverySince < UNKNOWN_LAYOUT_STABLE_MS)
|
|
5357
|
+
return snapshot;
|
|
5358
|
+
// A footer this build cannot read stays unreadable: the same still pane reads stuck again at every sweep and is
|
|
5359
|
+
// recovered again. Only the edge into idle is news.
|
|
5360
|
+
const fields = { evidence: turn === "closed" ? "transcript_no_open_turn" : "stable_empty_composer", unchangedForMs: snapshot.unchangedForMs };
|
|
5361
|
+
if (this.instanceState !== "idle")
|
|
5362
|
+
this.logger.info(fields, "Stuck instance recovered — the CLI shows its idle composer and is not running a turn (#1641)");
|
|
5363
|
+
else
|
|
5364
|
+
this.logger.debug(fields, "Still idle on its unreadable footer — the stuck reading was recovered again (#1641)");
|
|
5365
|
+
return machine.observe(pane, Date.now(), { settled: true, changeAt, forceReady: true });
|
|
5366
|
+
}
|
|
5047
5367
|
replyCompletionGuardEnabled() {
|
|
5048
5368
|
return this.config.reply_completion_guard !== false
|
|
5049
5369
|
&& this.backend?.replyCompletionGuard === true;
|
|
@@ -5425,7 +5745,9 @@ export class Daemon extends EventEmitter {
|
|
|
5425
5745
|
// a pair every ~1.6s for as long as the repaint lasted (#1116). Real
|
|
5426
5746
|
// work cannot hide behind this: it changes the layout, the next
|
|
5427
5747
|
// output arms another probe, and the debounce capture still fires.
|
|
5428
|
-
|
|
5748
|
+
// #1641: never while the transcript shows a turn running — that idle-looking screen is mid-turn.
|
|
5749
|
+
if (this.instanceState === "idle" && this.backend?.isPeriodicRedrawIdlePane?.(pane) === true
|
|
5750
|
+
&& this.transcriptTurnState() !== "open")
|
|
5429
5751
|
return;
|
|
5430
5752
|
// A second burst overtook the probe. Do not bless its stale capture as
|
|
5431
5753
|
// idle: continuous output is the safer working signal, while the next
|
|
@@ -5440,6 +5762,8 @@ export class Daemon extends EventEmitter {
|
|
|
5440
5762
|
return;
|
|
5441
5763
|
if (!this.observeInteractionPane(pane, captureStartedAt, interactionCaptureAt, undefined, interactionCaptureOrder, reason !== "interaction_confirmation"))
|
|
5442
5764
|
return;
|
|
5765
|
+
// #1691: an accepted capture of this launch is a fresh read for the unrecognised-prompt evidence too (#1695 r3).
|
|
5766
|
+
this.noteFreshPaneRead(pane, interactionCaptureAt);
|
|
5443
5767
|
this.updateInputBlockedState(pane);
|
|
5444
5768
|
// Advance the shared 10s timer only for a fresh capture of this launch.
|
|
5445
5769
|
const deliveryFallback = deliveryCandidate === true
|
|
@@ -5458,6 +5782,10 @@ export class Daemon extends EventEmitter {
|
|
|
5458
5782
|
// decisions about *now*. The old double-observe expressed that by calling
|
|
5459
5783
|
// observe twice, which silently disabled the "content moved" branch.
|
|
5460
5784
|
const structuredPeriodicIdle = this.backend?.isPeriodicRedrawIdlePane;
|
|
5785
|
+
// #1641: the transcript outranks the pane: while it shows the turn running, no reading of this pane is idle (codex
|
|
5786
|
+
// hides its status row while streaming, leaving the idle composer and footer in view mid-turn).
|
|
5787
|
+
const transcriptTurn = this.transcriptTurnState();
|
|
5788
|
+
const transcriptBusy = transcriptTurn === "open";
|
|
5461
5789
|
let snapshot;
|
|
5462
5790
|
if (deliveryFallback !== null) {
|
|
5463
5791
|
// A missing footer cannot fall through to a broad ready match. All
|
|
@@ -5466,7 +5794,7 @@ export class Daemon extends EventEmitter {
|
|
|
5466
5794
|
settled: true,
|
|
5467
5795
|
changeAt: observedChangeAt,
|
|
5468
5796
|
forceReady: deliveryFallback,
|
|
5469
|
-
forceBusy: !deliveryFallback || paneActivity !== null,
|
|
5797
|
+
forceBusy: !deliveryFallback || paneActivity !== null || transcriptBusy,
|
|
5470
5798
|
});
|
|
5471
5799
|
}
|
|
5472
5800
|
else if (structuredPeriodicIdle && reason === "output_probe") {
|
|
@@ -5488,6 +5816,7 @@ export class Daemon extends EventEmitter {
|
|
|
5488
5816
|
snapshot = this.instanceStateMachine.observe(pane, Date.now(), {
|
|
5489
5817
|
settled: true,
|
|
5490
5818
|
changeAt: observedChangeAt,
|
|
5819
|
+
forceBusy: transcriptBusy,
|
|
5491
5820
|
});
|
|
5492
5821
|
}
|
|
5493
5822
|
}
|
|
@@ -5521,6 +5850,7 @@ export class Daemon extends EventEmitter {
|
|
|
5521
5850
|
settled: true,
|
|
5522
5851
|
changeAt: observedChangeAt,
|
|
5523
5852
|
forceReady: true,
|
|
5853
|
+
forceBusy: transcriptBusy,
|
|
5524
5854
|
});
|
|
5525
5855
|
}
|
|
5526
5856
|
else {
|
|
@@ -5569,6 +5899,7 @@ export class Daemon extends EventEmitter {
|
|
|
5569
5899
|
snapshot = this.instanceStateMachine.observe(pane, Date.now(), {
|
|
5570
5900
|
settled: true,
|
|
5571
5901
|
changeAt: observedChangeAt,
|
|
5902
|
+
forceBusy: transcriptBusy,
|
|
5572
5903
|
});
|
|
5573
5904
|
}
|
|
5574
5905
|
else {
|
|
@@ -5586,9 +5917,11 @@ export class Daemon extends EventEmitter {
|
|
|
5586
5917
|
snapshot = this.instanceStateMachine.observe(pane, Date.now(), {
|
|
5587
5918
|
settled,
|
|
5588
5919
|
changeAt: observedChangeAt,
|
|
5589
|
-
forceBusy: paneActivity !== null,
|
|
5920
|
+
forceBusy: paneActivity !== null || transcriptBusy,
|
|
5590
5921
|
});
|
|
5591
5922
|
}
|
|
5923
|
+
if (snapshot.state === "stuck")
|
|
5924
|
+
snapshot = this.recoverStuck(pane, snapshot, observedChangeAt, transcriptTurn);
|
|
5592
5925
|
this.applyInstanceStateSnapshot(snapshot, pane);
|
|
5593
5926
|
// Backends without a transcript feed can still say what they are doing, if
|
|
5594
5927
|
// their TUI names it. Free-riding on a capture that already happened: no
|
|
@@ -5999,11 +6332,7 @@ export class Daemon extends EventEmitter {
|
|
|
5999
6332
|
* is a durable delivery) must LEAD the persisted user message — restart reconciliation recognises a delivery only by a
|
|
6000
6333
|
* marker at the start of the body (#1197) — so the banner goes right after it, not in front of it.
|
|
6001
6334
|
*/
|
|
6002
|
-
static steerEnvelope(inbound) {
|
|
6003
|
-
const banner = "[STEERING — mid-task course correction. Fold this into the CURRENT work if one is active.]\n";
|
|
6004
|
-
const marker = inbound.match(/^\[agend-delivery-id:[^\]\r\n]+\]\r?\n/);
|
|
6005
|
-
return marker ? marker[0] + banner + inbound.slice(marker[0].length) : banner + inbound;
|
|
6006
|
-
}
|
|
6335
|
+
static steerEnvelope(inbound) { return steerInboundEnvelope(inbound); }
|
|
6007
6336
|
/**
|
|
6008
6337
|
* The one place an inbound message grows its metadata wrapper ([user:]/
|
|
6009
6338
|
* [from:] prefix, pending reactions, handoff metadata, reply instructions).
|
|
@@ -6011,33 +6340,68 @@ export class Daemon extends EventEmitter {
|
|
|
6011
6340
|
* here — a steered message must read EXACTLY like a queued one to the
|
|
6012
6341
|
* agent, or the two drift apart in what the agent is told about replying.
|
|
6013
6342
|
*/
|
|
6343
|
+
/** #1650: how this instance talks to the fleet — MCP tools, or agend-agent commands (agent_mode: cli). */
|
|
6344
|
+
agentMode() { return this.config.agent_mode === "cli" ? "cli" : "mcp"; }
|
|
6014
6345
|
formatInboundMessage(content, meta) {
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
let formatted;
|
|
6018
|
-
if (
|
|
6019
|
-
formatted
|
|
6346
|
+
if (!meta.from_instance)
|
|
6347
|
+
return formatHumanInboundMessage(content, meta, this.agentMode());
|
|
6348
|
+
let formatted = formatCrossInstanceInboundMessage(content, meta, this.agentMode());
|
|
6349
|
+
if (meta.reply_to_text)
|
|
6350
|
+
formatted += '\n(reply_to: "' + meta.reply_to_text + '")';
|
|
6351
|
+
if (meta.delivery_id)
|
|
6352
|
+
formatted = "[agend-delivery-id:" + meta.delivery_id + "]\n" + formatted;
|
|
6353
|
+
return formatted;
|
|
6354
|
+
}
|
|
6355
|
+
/** Pure cached owner: no pane/process query on ingress. */
|
|
6356
|
+
captureHumanReceiptOwner() {
|
|
6357
|
+
try {
|
|
6358
|
+
if (this.deliveryWritesStopping || this.spawning || !this.tmux)
|
|
6359
|
+
return null;
|
|
6360
|
+
const o = this.interactionOwner();
|
|
6361
|
+
const window = this.tmux.getWindowId();
|
|
6362
|
+
if (!window || !o.bootId || ![o.spawnGeneration, o.launchAttempt, o.launchFenceEpoch, this.deliveryEpoch]
|
|
6363
|
+
.every(n => Number.isSafeInteger(n) && n >= 0))
|
|
6364
|
+
return null;
|
|
6365
|
+
return JSON.stringify([o.bootId, o.spawnGeneration, o.launchAttempt, o.launchFenceEpoch,
|
|
6366
|
+
this.deliveryEpoch, humanOwnerObjectId(this.tmux), window]);
|
|
6020
6367
|
}
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
const idTag = meta.user_id ? `, id:${meta.user_id}` : "";
|
|
6024
|
-
formatted = `[user:${user}${via}${idTag}] ${content}`;
|
|
6025
|
-
// Reactions queued since the last real message (#432). One leading line of
|
|
6026
|
-
// context, present only when something is pending — a reaction no longer
|
|
6027
|
-
// costs a turn of its own.
|
|
6028
|
-
if (meta.pending_reactions) {
|
|
6029
|
-
formatted = `[Recent reactions: ${meta.pending_reactions}]\n${formatted}`;
|
|
6030
|
-
}
|
|
6031
|
-
formatted += renderHandoffMetadata(meta);
|
|
6032
|
-
formatted += "\n(Reply using the reply tool — do NOT respond with direct text)";
|
|
6368
|
+
catch {
|
|
6369
|
+
return null;
|
|
6033
6370
|
}
|
|
6034
|
-
|
|
6035
|
-
|
|
6371
|
+
}
|
|
6372
|
+
/** The same formatter/sanitizer used by the actual write; snapshot only. */
|
|
6373
|
+
humanReceiptWire(content, meta, kind) {
|
|
6374
|
+
let wire = kind === "raw_paste" ? content
|
|
6375
|
+
: !meta.from_instance && content.startsWith("/raw ") && kind === "fleet_inbound"
|
|
6376
|
+
? content.slice(5) : this.formatInboundMessage(content, meta);
|
|
6377
|
+
if (kind === "steer")
|
|
6378
|
+
wire = Daemon.steerEnvelope(wire);
|
|
6379
|
+
else if (kind === "btw")
|
|
6380
|
+
wire = BTW_INBOUND_PREFIX + wire;
|
|
6381
|
+
return prepareDeliveryWire(wire, kind === "raw_paste");
|
|
6382
|
+
}
|
|
6383
|
+
humanReceiptAttempt(meta, wire) {
|
|
6384
|
+
const id = meta.human_receipt_id;
|
|
6385
|
+
const daemonOwner = meta.human_daemon_owner;
|
|
6386
|
+
if (!id || !/^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/.test(id) || meta.verification_id !== "hmsg-" + id
|
|
6387
|
+
|| !daemonOwner || !meta.human_owner_key || !/^[a-f0-9]{64}$/.test(meta.human_wire_sha256 ?? "")
|
|
6388
|
+
|| humanBodyHash(prepareDeliveryWire(wire)) !== meta.human_wire_sha256)
|
|
6389
|
+
return undefined;
|
|
6390
|
+
return { receiptId: id, verificationId: meta.verification_id, expectedWireHash: meta.human_wire_sha256,
|
|
6391
|
+
ownerKey: meta.human_owner_key, current: () => this.captureHumanReceiptOwner() === daemonOwner };
|
|
6392
|
+
}
|
|
6393
|
+
finishHumanReceipt(meta, verdict) {
|
|
6394
|
+
if (!meta.human_receipt_id || !meta.human_owner_key)
|
|
6395
|
+
return;
|
|
6396
|
+
const attempt = verdict.humanReceipt;
|
|
6397
|
+
const proof = attempt?.current() && !["unverified", "output-edge", "unverifiable"].includes(verdict.proof ?? "")
|
|
6398
|
+
? attempt.proof ?? null : null;
|
|
6399
|
+
try {
|
|
6400
|
+
this.emit("human_receipt_result", { receipt_id: meta.human_receipt_id, owner_key: meta.human_owner_key, proof });
|
|
6036
6401
|
}
|
|
6037
|
-
|
|
6038
|
-
|
|
6402
|
+
catch (err) {
|
|
6403
|
+
this.logger.error({ err }, "Could not publish human delivery evidence");
|
|
6039
6404
|
}
|
|
6040
|
-
return formatted;
|
|
6041
6405
|
}
|
|
6042
6406
|
/**
|
|
6043
6407
|
* Whether ONE delivery reached a verdict, carried by that delivery.
|
|
@@ -6130,12 +6494,13 @@ export class Daemon extends EventEmitter {
|
|
|
6130
6494
|
this.deferDurableDelivery(meta, "delivery epoch changed while waking for steer");
|
|
6131
6495
|
return;
|
|
6132
6496
|
}
|
|
6133
|
-
const verdict = { reached: false };
|
|
6497
|
+
const verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
|
|
6134
6498
|
try {
|
|
6135
6499
|
if (await this.deliverMessage(formatted, status, {
|
|
6136
6500
|
steer: true,
|
|
6137
6501
|
deliveryEpoch,
|
|
6138
6502
|
submissionId: meta.message_id,
|
|
6503
|
+
...(meta.verification_id ? { humanVerificationId: meta.verification_id } : {}),
|
|
6139
6504
|
verdict,
|
|
6140
6505
|
durableAttempt: durableAttempt || undefined,
|
|
6141
6506
|
})) {
|
|
@@ -6166,6 +6531,9 @@ export class Daemon extends EventEmitter {
|
|
|
6166
6531
|
this.retryDurableDeliveryBeforeBegin(durableAttempt, err.message);
|
|
6167
6532
|
throw err;
|
|
6168
6533
|
}
|
|
6534
|
+
finally {
|
|
6535
|
+
this.finishHumanReceipt(meta, verdict);
|
|
6536
|
+
}
|
|
6169
6537
|
}).catch(err => {
|
|
6170
6538
|
this.deferDurableDelivery(meta, `target wake/steer failed before pane submission: ${err.message}`);
|
|
6171
6539
|
this.logger.warn({ err: err.message }, "steer delivery error");
|
|
@@ -6181,24 +6549,37 @@ export class Daemon extends EventEmitter {
|
|
|
6181
6549
|
* so channel delivery intentionally uses a normal wrapped inbound instead.
|
|
6182
6550
|
*/
|
|
6183
6551
|
btwMessage(content, meta, deliveryEpoch = this.deliveryEpoch) {
|
|
6184
|
-
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
6552
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
6553
|
+
this.finishHumanReceipt(meta, { reached: false });
|
|
6185
6554
|
return;
|
|
6555
|
+
}
|
|
6186
6556
|
this.updateLastChat(meta.chat_id, meta.thread_id, meta.adapter_id);
|
|
6187
6557
|
this.pendingWork.recordInbound();
|
|
6188
6558
|
this.recordRecentUserMessage(content, meta);
|
|
6189
|
-
const formatted =
|
|
6190
|
-
+ this.formatInboundMessage(content, meta);
|
|
6559
|
+
const formatted = BTW_INBOUND_PREFIX + this.formatInboundMessage(content, meta);
|
|
6191
6560
|
const status = channelStatus(meta);
|
|
6192
6561
|
this.steerLock = this.steerLock.then(async () => {
|
|
6193
|
-
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
6562
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
6563
|
+
this.finishHumanReceipt(meta, { reached: false });
|
|
6194
6564
|
return;
|
|
6565
|
+
}
|
|
6195
6566
|
await this.wake();
|
|
6196
|
-
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
6567
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
6568
|
+
this.finishHumanReceipt(meta, { reached: false });
|
|
6197
6569
|
return;
|
|
6198
|
-
|
|
6199
|
-
|
|
6570
|
+
}
|
|
6571
|
+
const verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
|
|
6572
|
+
try {
|
|
6573
|
+
if (await this.deliverMessage(formatted, status, { steer: true, deliveryEpoch, submissionId: meta.message_id,
|
|
6574
|
+
...(meta.verification_id ? { humanVerificationId: meta.verification_id } : {}), verdict })) {
|
|
6575
|
+
this.markTurnStarted(meta, formatted, deliveryEpoch);
|
|
6576
|
+
}
|
|
6577
|
+
}
|
|
6578
|
+
finally {
|
|
6579
|
+
this.finishHumanReceipt(meta, verdict);
|
|
6200
6580
|
}
|
|
6201
6581
|
}).catch(err => {
|
|
6582
|
+
this.finishHumanReceipt(meta, { reached: false });
|
|
6202
6583
|
this.logger.warn({ err: err.message }, "btw delivery error");
|
|
6203
6584
|
});
|
|
6204
6585
|
}
|
|
@@ -6291,11 +6672,28 @@ export class Daemon extends EventEmitter {
|
|
|
6291
6672
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
6292
6673
|
return;
|
|
6293
6674
|
}
|
|
6675
|
+
const whatsNew = this.pendingWhatsNew ? this.currentWhatsNew(this.pendingWhatsNew) : null;
|
|
6676
|
+
if (whatsNew) {
|
|
6677
|
+
// #1633: here, inside the delivery lock and ahead of a real delivery — never into a busy turn, never to an
|
|
6678
|
+
// instance nobody writes to. Recorded only once pasted: a failed or cancelled paste stays pending.
|
|
6679
|
+
const pasted = await this.deliverMessage(whatsNew.notice, undefined, { deliveryEpoch });
|
|
6680
|
+
if (pasted && this.pendingWhatsNew === whatsNew) {
|
|
6681
|
+
this.pendingWhatsNew = null;
|
|
6682
|
+
try {
|
|
6683
|
+
recordWhatsNew(this.instanceDir, whatsNew.ids, whatsNew.version);
|
|
6684
|
+
}
|
|
6685
|
+
catch (err) {
|
|
6686
|
+
this.logger.warn({ err }, "what's-new record not written — the notice may repeat once");
|
|
6687
|
+
}
|
|
6688
|
+
}
|
|
6689
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
6690
|
+
return;
|
|
6691
|
+
}
|
|
6294
6692
|
const status = channelStatus(meta);
|
|
6295
6693
|
// A fresh delivery begins a fresh turn — its bubble must not inherit
|
|
6296
6694
|
// the previous turn's tool list.
|
|
6297
6695
|
this.resetToolProgress();
|
|
6298
|
-
verdict = { reached: false };
|
|
6696
|
+
verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
|
|
6299
6697
|
const durableAttempt = this.durableDeliveryAttempt(meta, content);
|
|
6300
6698
|
if (durableAttempt === false) {
|
|
6301
6699
|
this.deferDurableDelivery(meta, "invalid durable delivery attempt metadata");
|
|
@@ -6305,6 +6703,7 @@ export class Daemon extends EventEmitter {
|
|
|
6305
6703
|
if (await this.deliverMessage(formatted, status, {
|
|
6306
6704
|
deliveryEpoch,
|
|
6307
6705
|
submissionId: meta.message_id,
|
|
6706
|
+
...(meta.verification_id ? { humanVerificationId: meta.verification_id } : {}),
|
|
6308
6707
|
verdict,
|
|
6309
6708
|
durableAttempt: durableAttempt || undefined,
|
|
6310
6709
|
})) {
|
|
@@ -6352,6 +6751,7 @@ export class Daemon extends EventEmitter {
|
|
|
6352
6751
|
this.logger.warn({ err: err.message }, "Durable pane delivery deferred after exception");
|
|
6353
6752
|
}
|
|
6354
6753
|
finally {
|
|
6754
|
+
this.finishHumanReceipt(meta, verdict);
|
|
6355
6755
|
this.pasteQueueDepth--;
|
|
6356
6756
|
}
|
|
6357
6757
|
}).catch(err => {
|
|
@@ -6388,12 +6788,7 @@ export class Daemon extends EventEmitter {
|
|
|
6388
6788
|
&& !this.isDeliveryEpochCurrent(opts.deliveryEpoch);
|
|
6389
6789
|
if (cancelled() || this.stormWindow?.isStopped())
|
|
6390
6790
|
return false;
|
|
6391
|
-
|
|
6392
|
-
const fenceCount = (formatted.match(/```/g) || []).length;
|
|
6393
|
-
if (!opts?.preserveExactBytes && fenceCount % 2 !== 0) {
|
|
6394
|
-
// Odd number of fences = unclosed. Remove all code fences from the message.
|
|
6395
|
-
formatted = formatted.replace(/```/g, "");
|
|
6396
|
-
}
|
|
6791
|
+
formatted = prepareDeliveryWire(formatted, opts?.preserveExactBytes);
|
|
6397
6792
|
// Direct paths such as /steer, /btw, /raw and schedules do not pass the
|
|
6398
6793
|
// FleetManager idle gate. Hold them at the final common pane-write path so
|
|
6399
6794
|
// a tmux storm cannot force any text into a half-booted CLI.
|
|
@@ -6566,7 +6961,10 @@ export class Daemon extends EventEmitter {
|
|
|
6566
6961
|
return stale();
|
|
6567
6962
|
if (residue !== "clear")
|
|
6568
6963
|
return this.failDelivery(verdict, status, "input-residue", residue);
|
|
6569
|
-
const written = await this.writeMessageToPane(formatted, windowId, handingOffToNativeQueue, status, opts?.submissionId, verdict, opts?.durableAttempt, opts?.steer === true, writeCurrent);
|
|
6964
|
+
const written = await this.writeMessageToPane(formatted, windowId, handingOffToNativeQueue, status, opts?.submissionId, verdict, opts?.durableAttempt, opts?.steer === true, writeCurrent, opts?.humanVerificationId);
|
|
6965
|
+
// #1690: still under the lock — whatever waits on it next must already see what this write started.
|
|
6966
|
+
if (verdict.paneWriteStarted)
|
|
6967
|
+
opts?.onPaneWritten?.();
|
|
6570
6968
|
// The hand-off's last capture had no box: nothing was begun or written — wait for readiness, then try again.
|
|
6571
6969
|
if (written === "handoff-box-unread")
|
|
6572
6970
|
return "dialog";
|
|
@@ -6614,7 +7012,148 @@ export class Daemon extends EventEmitter {
|
|
|
6614
7012
|
* on purpose ("I trust", "Yes, continue") and would match ordinary prose.
|
|
6615
7013
|
*/
|
|
6616
7014
|
deliveryBlockingDialogs() {
|
|
6617
|
-
return
|
|
7015
|
+
return this.runtimeDialogs().filter(d => d.blocksDelivery || d.holdOnly);
|
|
7016
|
+
}
|
|
7017
|
+
/**
|
|
7018
|
+
* The backend's runtime dialogs, then #1691's catch-all for a prompt none of them knows (last: known ones win). The
|
|
7019
|
+
* catch-all is listed only while such a prompt is active: otherwise it could match nothing, and a backend with no
|
|
7020
|
+
* dialogs keeps its empty list (its delivery path skips the dialog probe, as before).
|
|
7021
|
+
*/
|
|
7022
|
+
runtimeDialogs() {
|
|
7023
|
+
const dialogs = this.backend?.getRuntimeDialogs?.() ?? [];
|
|
7024
|
+
return this.unrecognisedPromptCurrent()?.active ? [...dialogs, this.unrecognisedPromptDialog] : dialogs;
|
|
7025
|
+
}
|
|
7026
|
+
/**
|
|
7027
|
+
* #1691: is the pane at the CLI's own idle composer? Then whatever looks like a prompt above it is transcript output
|
|
7028
|
+
* (an agent's numbered list, a quoted "[y/n]"), not a prompt. The backend's own reading, most specific first.
|
|
7029
|
+
*/
|
|
7030
|
+
cliComposerShowing(pane, shape) {
|
|
7031
|
+
const backend = this.backend;
|
|
7032
|
+
if (!backend)
|
|
7033
|
+
return false;
|
|
7034
|
+
if (backend.readInputRow)
|
|
7035
|
+
return backend.readInputRow(pane) !== null;
|
|
7036
|
+
// muse: its live input box — null when the bottom of the screen is not its layout (#829), e.g. a selection menu.
|
|
7037
|
+
if (backend.inputDraft)
|
|
7038
|
+
return backend.inputDraft(pane) !== null;
|
|
7039
|
+
if (backend.isDeliveryInputReadyPane)
|
|
7040
|
+
return backend.isDeliveryInputReadyPane(pane);
|
|
7041
|
+
const bottom = backend.getBottomReadyPattern?.();
|
|
7042
|
+
if (bottom) {
|
|
7043
|
+
bottom.lastIndex = 0;
|
|
7044
|
+
return bottom.test(pane);
|
|
7045
|
+
}
|
|
7046
|
+
// No reader of its own (opencode, grok, …): its ready chrome BELOW the prompt rows — a status bar or prompt hint drawn
|
|
7047
|
+
// under its input box (opencode's "ctrl+p commands"), which a prompt shown in place of the input has nothing of. Not
|
|
7048
|
+
// the whole pane (a reply quoting "ctrl+p commands" higher up), and never the prompt's own rows: a ready pattern that
|
|
7049
|
+
// is just a cursor glyph would otherwise read the menu's own "❯ 1." as the CLI's prompt (#1695 r2).
|
|
7050
|
+
const rows = pane.replace(/\r/g, "").split("\n").map(r => r.trimEnd());
|
|
7051
|
+
const lastOwn = (shape.block.split("\n").at(-1) ?? "").trim();
|
|
7052
|
+
let end = -1;
|
|
7053
|
+
for (let i = rows.length - 1; i >= 0; i--)
|
|
7054
|
+
if (rows[i].trim() === lastOwn) {
|
|
7055
|
+
end = i;
|
|
7056
|
+
break;
|
|
7057
|
+
}
|
|
7058
|
+
if (end < 0)
|
|
7059
|
+
return false;
|
|
7060
|
+
const below = rows.slice(end + 1).filter(r => r.trim() !== "").slice(0, COMPOSER_TAIL_ROWS).join("\n");
|
|
7061
|
+
const ready = backend.getReadyPattern();
|
|
7062
|
+
ready.lastIndex = 0;
|
|
7063
|
+
return below !== "" && ready.test(below);
|
|
7064
|
+
}
|
|
7065
|
+
/**
|
|
7066
|
+
* #1691: the prompt shape this pane shows, when nothing else explains it (no known dialog, login, idle composer, and
|
|
7067
|
+
* the CLI is not showing its busy indicator).
|
|
7068
|
+
*/
|
|
7069
|
+
unrecognisedPromptCandidate(pane) {
|
|
7070
|
+
const shape = unrecognisedPromptShape(pane);
|
|
7071
|
+
if (!shape)
|
|
7072
|
+
return null;
|
|
7073
|
+
const busy = this.backend?.getBusyPattern?.() ?? null;
|
|
7074
|
+
if (busy) {
|
|
7075
|
+
busy.lastIndex = 0;
|
|
7076
|
+
if (busy.test(pane))
|
|
7077
|
+
return null;
|
|
7078
|
+
}
|
|
7079
|
+
if ((this.backend?.getRuntimeDialogs?.() ?? []).some(dialog => Daemon.dialogMatches(dialog, pane)))
|
|
7080
|
+
return null;
|
|
7081
|
+
if (LOGIN_FLOWS[this.config.backend ?? "claude-code"]?.loginScreenActive?.(pane))
|
|
7082
|
+
return null;
|
|
7083
|
+
if (this.cliComposerShowing(pane, shape))
|
|
7084
|
+
return null;
|
|
7085
|
+
return shape;
|
|
7086
|
+
}
|
|
7087
|
+
/**
|
|
7088
|
+
* #1691, from each runtime scan: the same unexplained prompt block unchanged for UNRECOGNISED_PROMPT_STABLE_MS
|
|
7089
|
+
* becomes active (held, awaiting_input, reported once by trackDialogParked). Any other screen ends it. A turn keeps
|
|
7090
|
+
* changing the pane (spinner, timer, streamed text), so its clock never completes mid-turn.
|
|
7091
|
+
*/
|
|
7092
|
+
trackUnrecognisedPrompt(pane, mono, outputMoved) {
|
|
7093
|
+
if (mono < this.unrecognisedPromptReadAt)
|
|
7094
|
+
return;
|
|
7095
|
+
this.unrecognisedPromptReadAt = mono;
|
|
7096
|
+
const shape = this.unrecognisedPromptCandidate(pane);
|
|
7097
|
+
if (!shape) {
|
|
7098
|
+
this.unrecognisedPrompt = null;
|
|
7099
|
+
return;
|
|
7100
|
+
}
|
|
7101
|
+
const screen = Daemon.stableScreen(pane);
|
|
7102
|
+
const owner = this.interactionOwner();
|
|
7103
|
+
const current = this.unrecognisedPromptCurrent();
|
|
7104
|
+
// Restart the minute on any change: another prompt, motion anywhere on the screen, output that arrived while this
|
|
7105
|
+
// capture was taken, or evidence from another launch (#1695 review).
|
|
7106
|
+
if (!current || current.block !== shape.block || current.screen !== screen || outputMoved) {
|
|
7107
|
+
this.unrecognisedPrompt = { block: shape.block, shape, since: mono, active: false, reportedElsewhere: false, screen, owner };
|
|
7108
|
+
return;
|
|
7109
|
+
}
|
|
7110
|
+
if (!current.active && mono - current.since >= UNRECOGNISED_PROMPT_STABLE_MS) {
|
|
7111
|
+
current.active = true;
|
|
7112
|
+
// A [y/n] the interactive-prompt detector knows was already told (interactive_prompt, after 10 s): held and
|
|
7113
|
+
// awaiting input from here on, but not reported a second time.
|
|
7114
|
+
current.reportedElsewhere = shape.kind === "yes-no" && this.interactivePromptDetector.match(pane) !== null;
|
|
7115
|
+
this.logger.warn({ shape: shape.kind, options: shape.options, question: shape.question, unchangedForMs: Math.round(mono - current.since) }, "CLI is waiting on a prompt AgEnD does not recognise — holding deliveries, reporting for a human (capture this pane for the per-version audit, ffb8104e)");
|
|
7116
|
+
}
|
|
7117
|
+
}
|
|
7118
|
+
/** #1691: the tracked evidence, if it still belongs to this launch (else it is dropped). */
|
|
7119
|
+
unrecognisedPromptCurrent() {
|
|
7120
|
+
const current = this.unrecognisedPrompt;
|
|
7121
|
+
if (current && (this.runtimeMonitorsFrozen || !sameInteractionOwner(current.owner, this.interactionOwner()))) {
|
|
7122
|
+
this.unrecognisedPrompt = null;
|
|
7123
|
+
return null;
|
|
7124
|
+
}
|
|
7125
|
+
return current;
|
|
7126
|
+
}
|
|
7127
|
+
/**
|
|
7128
|
+
* #1691: the catch-all's predicate — the tracked prompt is active and this pane still shows exactly it (any reader:
|
|
7129
|
+
* the scan, a probe, an interaction re-capture). Ending the evidence on a read without it is noteFreshPaneRead's.
|
|
7130
|
+
*/
|
|
7131
|
+
unrecognisedPromptActive(pane) {
|
|
7132
|
+
const current = this.unrecognisedPromptCurrent();
|
|
7133
|
+
return !!current?.active && this.unrecognisedPromptCandidate(pane)?.block === current.block && Daemon.stableScreen(pane) === current.screen;
|
|
7134
|
+
}
|
|
7135
|
+
/**
|
|
7136
|
+
* #1691: a fresh pane read outside the scan (a delivery probe, an accepted state re-capture), taken at `mono`. When it
|
|
7137
|
+
* no longer shows exactly the tracked prompt, the evidence ends — counting or active, so a return needs a fresh minute
|
|
7138
|
+
* (#1695 r2). When it does, it is the newest confirmation. Either way only a read newer than the newest read that
|
|
7139
|
+
* decided the evidence counts: an older one, finishing late, says nothing about now (#1695 r3, r4).
|
|
7140
|
+
*/
|
|
7141
|
+
noteFreshPaneRead(pane, mono) {
|
|
7142
|
+
if (mono < this.unrecognisedPromptReadAt)
|
|
7143
|
+
return;
|
|
7144
|
+
// Advanced even with nothing tracked: a scan whose capture is older must not start evidence this read contradicts
|
|
7145
|
+
// (#1695 r5).
|
|
7146
|
+
this.unrecognisedPromptReadAt = mono;
|
|
7147
|
+
const current = this.unrecognisedPromptCurrent();
|
|
7148
|
+
if (!current)
|
|
7149
|
+
return;
|
|
7150
|
+
if (this.unrecognisedPromptCandidate(pane)?.block === current.block && Daemon.stableScreen(pane) === current.screen)
|
|
7151
|
+
return;
|
|
7152
|
+
this.unrecognisedPrompt = null;
|
|
7153
|
+
}
|
|
7154
|
+
/** #1691: the screen as the stability clock compares it (trailing blanks are not motion). */
|
|
7155
|
+
static stableScreen(pane) {
|
|
7156
|
+
return pane.replace(/\r/g, "").split("\n").map(r => r.trimEnd()).join("\n").trimEnd();
|
|
6618
7157
|
}
|
|
6619
7158
|
/** Passive input-unavailable phases relevant to the current spawn only. */
|
|
6620
7159
|
guardedInputTransients() {
|
|
@@ -6782,7 +7321,7 @@ export class Daemon extends EventEmitter {
|
|
|
6782
7321
|
return dialog.isActive ? dialog.isActive(pane) : dialog.pattern.test(pane);
|
|
6783
7322
|
}
|
|
6784
7323
|
/** Refresh the separate stdin-blocked observation from one pane capture. */
|
|
6785
|
-
updateInputBlockedState(pane, dialogs = this.
|
|
7324
|
+
updateInputBlockedState(pane, dialogs = this.runtimeDialogs()) {
|
|
6786
7325
|
const active = dialogs.find(dialog => dialog.inputBlocked && Daemon.dialogMatches(dialog, pane)) ?? null;
|
|
6787
7326
|
const nextKey = active ? Daemon.dialogKey(active) : null;
|
|
6788
7327
|
if (nextKey !== this.inputBlockedDialogKey) {
|
|
@@ -6870,9 +7409,15 @@ export class Daemon extends EventEmitter {
|
|
|
6870
7409
|
async probeBlockingDialog() {
|
|
6871
7410
|
if (!this.tmux || !this.backend)
|
|
6872
7411
|
return { state: "clear" };
|
|
7412
|
+
// #1691: a probe is a fresh read — it may end unrecognised-prompt evidence even when no dialog is listed.
|
|
7413
|
+
const tracking = this.unrecognisedPrompt !== null;
|
|
6873
7414
|
const dialogs = this.deliveryBlockingDialogs();
|
|
6874
|
-
|
|
7415
|
+
// #1690: our own switch is being answered — nothing else types into the pane until it has settled.
|
|
7416
|
+
if (this.ownSwitch && dialogs.length > 0)
|
|
7417
|
+
return { state: "dialog", dialog: dialogs[0] };
|
|
7418
|
+
if (dialogs.length === 0 && !tracking)
|
|
6875
7419
|
return { state: "clear" };
|
|
7420
|
+
const probeMono = performance.now();
|
|
6876
7421
|
let pane;
|
|
6877
7422
|
try {
|
|
6878
7423
|
pane = await this.tmux.capturePane();
|
|
@@ -6881,6 +7426,7 @@ export class Daemon extends EventEmitter {
|
|
|
6881
7426
|
this.logger.debug({ err }, "capture-pane failed during the dialog probe — pane state unknown");
|
|
6882
7427
|
return { state: "unknown" };
|
|
6883
7428
|
}
|
|
7429
|
+
this.noteFreshPaneRead(pane, probeMono);
|
|
6884
7430
|
for (const dialog of dialogs) {
|
|
6885
7431
|
if (!Daemon.dialogMatches(dialog, pane))
|
|
6886
7432
|
continue;
|
|
@@ -7090,11 +7636,16 @@ export class Daemon extends EventEmitter {
|
|
|
7090
7636
|
this.dialogParkedReported = false;
|
|
7091
7637
|
return;
|
|
7092
7638
|
}
|
|
7093
|
-
|
|
7639
|
+
// #1691: the catch-all waited its minute before it became active; it is reported on the next scan.
|
|
7640
|
+
const unrecognised = dialog === this.unrecognisedPromptDialog;
|
|
7641
|
+
if (unrecognised && this.unrecognisedPrompt?.reportedElsewhere)
|
|
7642
|
+
this.dialogParkedReported = true;
|
|
7643
|
+
if (!this.dialogParkedReported && Date.now() - this.dialogParkedSince >= (unrecognised ? 0 : DIALOG_PARKED_NOTIFY_MS)) {
|
|
7094
7644
|
this.dialogParkedReported = true;
|
|
7095
7645
|
this.logger.warn({ dialog: dialog.description, parkedForMs: Date.now() - this.dialogParkedSince }, "CLI dialog is still on screen — not auto-answering, reporting for a human");
|
|
7096
7646
|
this.emit("dialog_parked", { name: this.name, description: dialog.description, holdOnly: dialog.holdOnly === true,
|
|
7097
|
-
...(episode !== null && interaction.kind ? { kind: interaction.kind, episode, backend: this.config.backend ?? "claude-code" } : {})
|
|
7647
|
+
...(episode !== null && interaction.kind ? { kind: interaction.kind, episode, backend: this.config.backend ?? "claude-code" } : {}),
|
|
7648
|
+
...(unrecognised ? { unrecognised: true, question: this.unrecognisedPrompt?.shape.question ?? null } : {}) });
|
|
7098
7649
|
}
|
|
7099
7650
|
}
|
|
7100
7651
|
/**
|
|
@@ -7205,7 +7756,7 @@ export class Daemon extends EventEmitter {
|
|
|
7205
7756
|
return false;
|
|
7206
7757
|
const row = norm(shown);
|
|
7207
7758
|
// The marker line holds nothing but the marker: the row is exactly it.
|
|
7208
|
-
if (/^\[agend-delivery-id:[^\]\s]+\]$/.test(first))
|
|
7759
|
+
if (/^\[agend-(?:delivery|human)-id:[^\]\s]+\]$/.test(first))
|
|
7209
7760
|
return row === first;
|
|
7210
7761
|
return row === first || row.startsWith(`${first} `);
|
|
7211
7762
|
}
|
|
@@ -7663,6 +8214,33 @@ export class Daemon extends EventEmitter {
|
|
|
7663
8214
|
this.logger.warn("Could not read the pane after Enter — not confirming the submission");
|
|
7664
8215
|
return sawOutput && residue === "absent";
|
|
7665
8216
|
}
|
|
8217
|
+
/**
|
|
8218
|
+
* #1647: whether a raw paste on a pane with no transcript (kiro) was taken: its text gone from the input row (or the
|
|
8219
|
+
* CLI's own busy marker shown instead — pasteResidue's rule) with output after the Enter. That is the confirmation's
|
|
8220
|
+
* own rule, looked at more than once: kiro repaints its composer late, so the single look right after Enter misread
|
|
8221
|
+
* pastes it took. A generic `working` reading is not evidence (#1663 P1-3: a repaint looks like work).
|
|
8222
|
+
*
|
|
8223
|
+
* Bounded by the budget that confirmation already has, measured from the Enter (#1663 P2-1): the busy wait plus the
|
|
8224
|
+
* post-Enter proof window. Every reading is asked again after its await — still this write's owner (`current`: the
|
|
8225
|
+
* caller's fence, no stop, the same tmux and spawn, #1663 P2-2) and still inside the budget — before it counts; a late
|
|
8226
|
+
* or fenced answer proves nothing. Null: not shown — the caller settles uncertain, never a second Enter.
|
|
8227
|
+
*/
|
|
8228
|
+
async rawPasteTakenOnPane(windowId, enterAt, enterMono, formatted, current) {
|
|
8229
|
+
const deadline = enterMono + CONFIRM_BUSY_POLLS * CONFIRM_BUSY_POLL_MS + POST_ENTER_PROOF_WINDOW_MS;
|
|
8230
|
+
const live = () => current() && performance.now() <= deadline;
|
|
8231
|
+
for (;;) {
|
|
8232
|
+
if (!live())
|
|
8233
|
+
return null;
|
|
8234
|
+
if (this.controlClient?.hasOutputSince?.(windowId, enterAt)) {
|
|
8235
|
+
const residue = await this.pasteResidue(formatted);
|
|
8236
|
+
if (!live())
|
|
8237
|
+
return null;
|
|
8238
|
+
if (residue === "absent")
|
|
8239
|
+
return RAW_PASTE_LEFT_INPUT;
|
|
8240
|
+
}
|
|
8241
|
+
await new Promise(r => setTimeout(r, POST_ENTER_PROOF_POLL_MS));
|
|
8242
|
+
}
|
|
8243
|
+
}
|
|
7666
8244
|
/** Once Kiro returns to its prompt, its unique envelope id in history proves submission. */
|
|
7667
8245
|
async kiroSubmissionEvidence(windowId, formatted, signature) {
|
|
7668
8246
|
if (!this.tmux)
|
|
@@ -7720,6 +8298,8 @@ export class Daemon extends EventEmitter {
|
|
|
7720
8298
|
if (!current())
|
|
7721
8299
|
return false;
|
|
7722
8300
|
if (evidence === "submitted") {
|
|
8301
|
+
if (pending.signature.unique)
|
|
8302
|
+
pending.signature.humanPositive?.("unique-id-pane");
|
|
7723
8303
|
if (status)
|
|
7724
8304
|
this.emit("message_confirmed", status);
|
|
7725
8305
|
return true;
|
|
@@ -7755,6 +8335,8 @@ export class Daemon extends EventEmitter {
|
|
|
7755
8335
|
if (retry === "cancelled")
|
|
7756
8336
|
return false;
|
|
7757
8337
|
if (retry === "submitted") {
|
|
8338
|
+
if (pending.signature.unique)
|
|
8339
|
+
pending.signature.humanPositive?.("unique-id-pane");
|
|
7758
8340
|
if (status)
|
|
7759
8341
|
this.emit("message_confirmed", status);
|
|
7760
8342
|
return true;
|
|
@@ -7895,9 +8477,24 @@ export class Daemon extends EventEmitter {
|
|
|
7895
8477
|
verdict = { reached: false }, durableAttempt, steer = false,
|
|
7896
8478
|
// #829: the caller's fence, asked once more after the last await before the
|
|
7897
8479
|
// first side effect. Stale means unwritten: false, with no failure verdict.
|
|
7898
|
-
stillCurrent) {
|
|
7899
|
-
const signature = this.submissionSignature(formatted, submissionId);
|
|
8480
|
+
stillCurrent, humanVerificationId) {
|
|
8481
|
+
const signature = this.submissionSignature(formatted, humanVerificationId ?? submissionId, humanVerificationId !== undefined);
|
|
7900
8482
|
const rawPaste = durableAttempt?.submissionMode === "raw_paste";
|
|
8483
|
+
const humanAttempt = verdict.humanReceipt;
|
|
8484
|
+
const humanMode = rawPaste ? "raw_paste" : steer ? "steer"
|
|
8485
|
+
: handingOffToNativeQueue ? "native_queue_handoff" : "idle_submit";
|
|
8486
|
+
if (humanAttempt && signature.unique && signature.value === "agend-human-id:" + humanAttempt.verificationId
|
|
8487
|
+
&& humanBodyHash(formatted) === humanAttempt.expectedWireHash) {
|
|
8488
|
+
signature.humanPositive = tag => {
|
|
8489
|
+
if (!humanAttempt.current() || (stillCurrent && !stillCurrent()))
|
|
8490
|
+
return;
|
|
8491
|
+
humanAttempt.proof = {
|
|
8492
|
+
receipt_id: humanAttempt.receiptId, verification_id: humanAttempt.verificationId,
|
|
8493
|
+
wire_sha256: humanBodyHash(formatted), owner_key: humanAttempt.ownerKey,
|
|
8494
|
+
mode: humanMode, tag,
|
|
8495
|
+
};
|
|
8496
|
+
};
|
|
8497
|
+
}
|
|
7901
8498
|
let windowId = initialWindowId;
|
|
7902
8499
|
// The spawn this write belongs to: acceptance evidence is only read while it is still the current one (#1197).
|
|
7903
8500
|
const spawnAtWrite = this.spawnGeneration;
|
|
@@ -7975,9 +8572,11 @@ export class Daemon extends EventEmitter {
|
|
|
7975
8572
|
offset: attemptEvidence.transcriptOffset,
|
|
7976
8573
|
};
|
|
7977
8574
|
// From here every proof of this write may also ask the CLI's transcript (the pane can lose the echo to the
|
|
7978
|
-
// reply, or still be painting the paste) — this delivery's exact marker only.
|
|
7979
|
-
|
|
7980
|
-
|
|
8575
|
+
// reply, or still be painting the paste) — this delivery's exact marker only, or a raw paste's exact text (#1647).
|
|
8576
|
+
// A raw paste's text only where the transcript is this instance's alone (RAW_TEXT_TRANSCRIPT_BACKENDS).
|
|
8577
|
+
if (["claude-code", "codex"].includes(attemptEvidence.backend)
|
|
8578
|
+
&& (!rawPaste || RAW_TEXT_TRANSCRIPT_BACKENDS.includes(attemptEvidence.backend))) {
|
|
8579
|
+
signature.transcript = this.transcriptProofFor(verdict.transcriptCheckpoint, rawPaste ? { rawText: formatted } : durableAttempt.deliveryId, spawnAtWrite, stillCurrent, kind => { verdict.transcriptProof = kind; });
|
|
7981
8580
|
}
|
|
7982
8581
|
}
|
|
7983
8582
|
}
|
|
@@ -8041,6 +8640,7 @@ export class Daemon extends EventEmitter {
|
|
|
8041
8640
|
// positively hold typed text, and after it be the empty placeholder again. The empty placeholder here means the
|
|
8042
8641
|
// paste never reached the box; an unreadable box (a failed or late capture, a dialog) proves nothing: no Enter.
|
|
8043
8642
|
const steerComposerProof = handingOffToNativeQueue && steer && !!this.backend?.readSteerComposer && tmuxAtWrite !== null;
|
|
8643
|
+
let humanComposerOwned = false;
|
|
8044
8644
|
let steerEnterDeadline = Infinity;
|
|
8045
8645
|
if (steerComposerProof) {
|
|
8046
8646
|
const deadline = performance.now() + STEER_PASTE_VISIBLE_MS;
|
|
@@ -8064,10 +8664,13 @@ export class Daemon extends EventEmitter {
|
|
|
8064
8664
|
return this.failDelivery(verdict, status, "steer-paste", before === null ? "box-unread" : `box-still-${before.mode}`);
|
|
8065
8665
|
if (!this.steerPasteIsOurs(before.pane, formatted))
|
|
8066
8666
|
return this.failDelivery(verdict, status, "steer-paste", "box-not-this-paste");
|
|
8667
|
+
const shown = this.backend?.readSteerComposerText?.(before.pane)?.replace(/\s+/g, " ").trim();
|
|
8668
|
+
humanComposerOwned = !!humanAttempt && shown === "[agend-human-id:" + humanAttempt.verificationId + "]";
|
|
8067
8669
|
// …and so is the Enter: the key is sent only while the reading is still within it.
|
|
8068
8670
|
steerEnterDeadline = deadline;
|
|
8069
8671
|
}
|
|
8070
8672
|
let enterAt = Date.now();
|
|
8673
|
+
const enterMono = performance.now();
|
|
8071
8674
|
const steerEnterCurrent = () => steerCurrent() && performance.now() <= steerEnterDeadline;
|
|
8072
8675
|
if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof ? steerEnterCurrent : undefined, durableAttempt))) {
|
|
8073
8676
|
if (steerComposerProof && !steerCurrent()) {
|
|
@@ -8134,6 +8737,9 @@ export class Daemon extends EventEmitter {
|
|
|
8134
8737
|
if (after === "steer" || after === "queue" || after === "idle") {
|
|
8135
8738
|
const probe = await withinMs(this.probeBlockingDialog(), deadline - performance.now());
|
|
8136
8739
|
if (probe?.state === "clear" && performance.now() <= deadline && steerCurrent() && this.getWindowId() === windowId) {
|
|
8740
|
+
if (humanComposerOwned)
|
|
8741
|
+
signature.humanPositive?.(after === "steer" ? "composer-empty-steer"
|
|
8742
|
+
: after === "queue" ? "composer-empty-queue" : "composer-empty-idle");
|
|
8137
8743
|
verdict.phase = "steer-accepted";
|
|
8138
8744
|
verdict.proof = `steer-composer-${after}`;
|
|
8139
8745
|
if (status)
|
|
@@ -8313,12 +8919,20 @@ export class Daemon extends EventEmitter {
|
|
|
8313
8919
|
return { kind: "kiro-pending", windowId, spawnGeneration: this.spawnGeneration, signature };
|
|
8314
8920
|
}
|
|
8315
8921
|
}
|
|
8316
|
-
if (!submitted) {
|
|
8317
|
-
|
|
8922
|
+
if (!submitted && rawPaste) {
|
|
8923
|
+
// #1647: kiro paints output at once but its composer late, so one residue look right after Enter misreads a
|
|
8924
|
+
// paste it took. Keep looking within the confirmation's own budget from the Enter, under this write's fence;
|
|
8925
|
+
// never a second Enter (a raw command must not run twice).
|
|
8926
|
+
const taken = await this.rawPasteTakenOnPane(windowId, enterAt, enterMono, formatted, steerCurrent);
|
|
8927
|
+
if (!taken) {
|
|
8318
8928
|
verdict.phase = "raw-paste-submit-proof";
|
|
8319
8929
|
verdict.proof = "no-positive-acceptance-proof";
|
|
8320
8930
|
return false;
|
|
8321
8931
|
}
|
|
8932
|
+
verdict.proof = taken;
|
|
8933
|
+
submitted = true;
|
|
8934
|
+
}
|
|
8935
|
+
if (!submitted) {
|
|
8322
8936
|
// Legacy system pastes have no unique envelope id. Preserve their
|
|
8323
8937
|
// existing bounded retry until they can be tied to a specific turn.
|
|
8324
8938
|
this.logger.warn("Message not submitted after Enter — waiting for the prompt, then re-sending Enter once");
|
|
@@ -8631,10 +9245,10 @@ export class Daemon extends EventEmitter {
|
|
|
8631
9245
|
* A write's transcript proof: bounded looks from its checkpoint, fenced to the write — its spawn and launch, a stop,
|
|
8632
9246
|
* and the caller's own fence — so a look that lands after any of them is never used (#1380 review).
|
|
8633
9247
|
*/
|
|
8634
|
-
transcriptProofFor(checkpoint,
|
|
9248
|
+
transcriptProofFor(checkpoint, needle, spawnAtWrite, stillCurrent, onProof) {
|
|
8635
9249
|
const launchAtWrite = this.launchFenceEpoch;
|
|
8636
9250
|
return {
|
|
8637
|
-
reader: new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend,
|
|
9251
|
+
reader: new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend, needle),
|
|
8638
9252
|
current: () => !this.deliveryWritesStopping && this.spawnGeneration === spawnAtWrite
|
|
8639
9253
|
&& this.launchFenceEpoch === launchAtWrite && (stillCurrent?.() ?? true),
|
|
8640
9254
|
onProof,
|
|
@@ -8712,8 +9326,11 @@ export class Daemon extends EventEmitter {
|
|
|
8712
9326
|
// the input row is proof in itself — which also means a momentary
|
|
8713
9327
|
// failure to read the pane BEFORE pasting cannot turn a delivered
|
|
8714
9328
|
// message into a re-paste.
|
|
8715
|
-
if (signature.unique && after.payload > 0)
|
|
9329
|
+
if (signature.unique && after.payload > 0) {
|
|
9330
|
+
if (after.inputReadable)
|
|
9331
|
+
signature.humanPositive?.("unique-id-pane");
|
|
8716
9332
|
return this.submittedProof();
|
|
9333
|
+
}
|
|
8717
9334
|
if (unattributedResidue)
|
|
8718
9335
|
return "unverifiable";
|
|
8719
9336
|
// 3. Otherwise the evidence must be NEW relative to the pane as it was
|
|
@@ -8850,7 +9467,7 @@ export class Daemon extends EventEmitter {
|
|
|
8850
9467
|
* paste that never landed. System pastes carry no envelope and fall back to
|
|
8851
9468
|
* the body signature, which the baseline comparison still ties to this paste.
|
|
8852
9469
|
*/
|
|
8853
|
-
submissionSignature(formatted, trustedId) {
|
|
9470
|
+
submissionSignature(formatted, trustedId, verification = false) {
|
|
8854
9471
|
// The id must come from the message's own metadata, never from scanning the
|
|
8855
9472
|
// rendered text: agents and users discuss message ids in the body all the
|
|
8856
9473
|
// time ("check message_id: abc"), and a body-scanned value would match an
|
|
@@ -8859,7 +9476,7 @@ export class Daemon extends EventEmitter {
|
|
|
8859
9476
|
// the handoff metadata identifies THIS delivery.
|
|
8860
9477
|
const id = trustedId?.trim();
|
|
8861
9478
|
if (id)
|
|
8862
|
-
return { value:
|
|
9479
|
+
return { value: (verification ? "agend-human-id:" : "message_id:") + id, unique: true };
|
|
8863
9480
|
return { value: pastedTextSignature(formatted), unique: false };
|
|
8864
9481
|
}
|
|
8865
9482
|
/**
|
|
@@ -9381,6 +9998,11 @@ export class Daemon extends EventEmitter {
|
|
|
9381
9998
|
respond(null, `reply: ${buttons.error}`);
|
|
9382
9999
|
return;
|
|
9383
10000
|
}
|
|
10001
|
+
// #1823: a poll's results need the fleet's store and outbox; as text it would never close.
|
|
10002
|
+
if (args.poll !== undefined && args.poll !== null) {
|
|
10003
|
+
respond(null, "reply: a poll needs AgEnD's fleet; send the question without poll");
|
|
10004
|
+
return;
|
|
10005
|
+
}
|
|
9384
10006
|
if (buttons) {
|
|
9385
10007
|
routed = { ...args, text: `${String(args.text)}\n\n${replyButtonsFallbackText(buttons.buttons)}` };
|
|
9386
10008
|
delete routed.buttons;
|
|
@@ -9966,7 +10588,7 @@ export class Daemon extends EventEmitter {
|
|
|
9966
10588
|
* the same tmux window, not aborted, writes not stopping — re-checked after every await, and between the paste and
|
|
9967
10589
|
* its Enter. The whole step, every await included, ends at one monotonic deadline: a late capture never confirms
|
|
9968
10590
|
* and a late admission never writes; a paste whose Enter would land past it is taken back. Not confirmed in time:
|
|
9969
|
-
* the old setup stays, a launch warning
|
|
10591
|
+
* the old setup stays, the next launch tries again, and a quiet launch warning (#1794: logged and on the Log tab, not posted) says so. A stop, pause or respawn ends it
|
|
9970
10592
|
* silently, confirming and removing nothing. Never throws into the spawn.
|
|
9971
10593
|
*/
|
|
9972
10594
|
async ensureBackendAgent() {
|
|
@@ -10012,8 +10634,10 @@ export class Daemon extends EventEmitter {
|
|
|
10012
10634
|
if (active === sw.agent && (wroteAt === null || capturedAt > wroteAt)) {
|
|
10013
10635
|
if (sw.alreadyConfirmed && wroteAt === null)
|
|
10014
10636
|
return; // already ours, already on record
|
|
10015
|
-
for (const
|
|
10016
|
-
this.
|
|
10637
|
+
for (const { message, quiet } of sw.confirm()) {
|
|
10638
|
+
this.logger.warn({ agent: sw.agent, warning: message }, "The agent switch was confirmed with a warning");
|
|
10639
|
+
this.emit("backend_launch_warning", { name: this.name, message, quiet: quiet === true });
|
|
10640
|
+
}
|
|
10017
10641
|
this.logger.info({ agent: sw.agent, switched: wroteAt !== null }, "The resumed conversation runs as this instance's agent");
|
|
10018
10642
|
return;
|
|
10019
10643
|
}
|
|
@@ -10062,7 +10686,9 @@ export class Daemon extends EventEmitter {
|
|
|
10062
10686
|
if (!fenced())
|
|
10063
10687
|
return; // a stop, pause or respawn: nothing to say, nothing to do
|
|
10064
10688
|
this.logger.warn({ agent: sw.agent, typed: wroteAt !== null }, "The resumed conversation did not switch to this instance's agent in time");
|
|
10065
|
-
|
|
10689
|
+
// #1794: quiet — the conversation keeps running as its saved agent with the shared entries, and the next launch
|
|
10690
|
+
// tries again; the user loses nothing and has nothing to do. Logged above and kept on the Log tab only.
|
|
10691
|
+
this.emit("backend_launch_warning", { name: this.name, message: t("kiro.switch_timeout", Daemon.AGENT_SWITCH_BUDGET_MS / 1000), quiet: true });
|
|
10066
10692
|
}
|
|
10067
10693
|
catch (err) {
|
|
10068
10694
|
this.startupAdmission?.();
|
|
@@ -10426,6 +11052,49 @@ export class Daemon extends EventEmitter {
|
|
|
10426
11052
|
}
|
|
10427
11053
|
catch { /* non-fatal */ }
|
|
10428
11054
|
}
|
|
11055
|
+
/**
|
|
11056
|
+
* #1633 (#1639 review): the pending notice as the record stands NOW, just before the paste — it was decided at launch,
|
|
11057
|
+
* and the record may have changed while the delivery waited. Unreadable → nothing is sent (and nothing pending);
|
|
11058
|
+
* entries recorded meanwhile are dropped from it; none left → nothing.
|
|
11059
|
+
*/
|
|
11060
|
+
currentWhatsNew(pending) {
|
|
11061
|
+
const record = readWhatsNewRecord(this.instanceDir);
|
|
11062
|
+
if (record === "unreadable") {
|
|
11063
|
+
this.pendingWhatsNew = null;
|
|
11064
|
+
return null;
|
|
11065
|
+
}
|
|
11066
|
+
const known = new Set(record?.announced ?? []);
|
|
11067
|
+
const ids = pending.ids.filter(id => !known.has(id));
|
|
11068
|
+
if (!ids.length) {
|
|
11069
|
+
this.pendingWhatsNew = null;
|
|
11070
|
+
return null;
|
|
11071
|
+
}
|
|
11072
|
+
if (ids.length === pending.ids.length)
|
|
11073
|
+
return pending;
|
|
11074
|
+
const narrowed = { notice: whatsNewNotice(pending.version, AGENT_WHATS_NEW.filter(e => ids.includes(e.id))), ids, version: pending.version };
|
|
11075
|
+
this.pendingWhatsNew = narrowed;
|
|
11076
|
+
return narrowed;
|
|
11077
|
+
}
|
|
11078
|
+
/** #1633: what this instance has not been told about its AgEnD version yet (see agent-whats-new.ts). */
|
|
11079
|
+
prepareWhatsNew(fresh, version = agendVersion()) {
|
|
11080
|
+
try {
|
|
11081
|
+
const plan = planWhatsNew({ record: readWhatsNewRecord(this.instanceDir), fresh, version });
|
|
11082
|
+
if (!plan) {
|
|
11083
|
+
this.pendingWhatsNew = null;
|
|
11084
|
+
return;
|
|
11085
|
+
}
|
|
11086
|
+
if ("silent" in plan) {
|
|
11087
|
+
recordWhatsNew(this.instanceDir, plan.silent, plan.version);
|
|
11088
|
+
this.pendingWhatsNew = null;
|
|
11089
|
+
return;
|
|
11090
|
+
}
|
|
11091
|
+
this.pendingWhatsNew = plan;
|
|
11092
|
+
this.logger.info({ entries: plan.ids }, "what's-new notice pending for the next delivery");
|
|
11093
|
+
}
|
|
11094
|
+
catch (err) {
|
|
11095
|
+
this.logger.debug({ err }, "what's-new not prepared");
|
|
11096
|
+
}
|
|
11097
|
+
}
|
|
10429
11098
|
async trySpawnInsideGate(reuseWindow = false, startupTimeoutMs) {
|
|
10430
11099
|
this.startupAdmission?.();
|
|
10431
11100
|
const launchFence = this.launchFenceEpoch;
|
|
@@ -10435,6 +11104,8 @@ export class Daemon extends EventEmitter {
|
|
|
10435
11104
|
// instructionsReloadedOnResume) so the warmup steering-reload can be
|
|
10436
11105
|
// skipped when nothing changed.
|
|
10437
11106
|
this.lastBuiltInstructions = backendConfig.instructions ?? "";
|
|
11107
|
+
// #1633: decided before prev-instructions is first written — its absence is what says "never launched before".
|
|
11108
|
+
this.prepareWhatsNew(!existsSync(join(this.instanceDir, "prev-instructions")));
|
|
10438
11109
|
{
|
|
10439
11110
|
const prevFile = join(this.instanceDir, "prev-instructions");
|
|
10440
11111
|
let prev = "";
|
|
@@ -10476,7 +11147,7 @@ export class Daemon extends EventEmitter {
|
|
|
10476
11147
|
if (launchFence !== this.launchFenceEpoch)
|
|
10477
11148
|
throw new Error("Launch cancelled: the instance was stopped or paused while the launch was being prepared");
|
|
10478
11149
|
this.backend.writeConfig(backendConfig);
|
|
10479
|
-
this.backend.preTrust?.(this.config.working_directory);
|
|
11150
|
+
this.backend.preTrust?.(this.config.working_directory, backendConfig);
|
|
10480
11151
|
// Resolve working directory (e.g. symlink for hidden paths)
|
|
10481
11152
|
const resolvedCwd = this.backend.resolveWorkingDirectory?.(this.config.working_directory, this.name) ?? this.config.working_directory;
|
|
10482
11153
|
// Generate a fresh per-instance agent token each spawn. agent-cli reads
|
|
@@ -10546,6 +11217,11 @@ export class Daemon extends EventEmitter {
|
|
|
10546
11217
|
this.logger.warn({ warning: launchWarning }, "Backend launch fell back");
|
|
10547
11218
|
this.emit("backend_launch_warning", { name: this.name, message: launchWarning });
|
|
10548
11219
|
}
|
|
11220
|
+
// #1811: what the launch left out and why (an effort this CLI or model does not take) — the Log tab, not the chat.
|
|
11221
|
+
for (const note of this.backend.consumeQuietLaunchNotes?.() ?? []) {
|
|
11222
|
+
this.logger.info({ note }, "Launch note");
|
|
11223
|
+
this.emit("backend_launch_warning", { name: this.name, message: note, quiet: true });
|
|
11224
|
+
}
|
|
10549
11225
|
// Ensure tmux session exists (may have been destroyed if all windows died)
|
|
10550
11226
|
await TmuxManager.ensureSession(this.tmuxSessionName);
|
|
10551
11227
|
if (this.stormWindow?.observeServerAlive(await TmuxManager.getServerPid(this.tmuxSessionName))) {
|
|
@@ -11116,6 +11792,14 @@ export class Daemon extends EventEmitter {
|
|
|
11116
11792
|
}, "Snapshot written");
|
|
11117
11793
|
return snapshot;
|
|
11118
11794
|
}
|
|
11795
|
+
/** #1699: the claude config dir this instance's CLI uses — its credential profile's, else exactly as before. */
|
|
11796
|
+
claudeConfigDir() {
|
|
11797
|
+
return claudeConfigDirFor(claudeProfileConfigDir(this.config.backend_options?.["claude-code"]));
|
|
11798
|
+
}
|
|
11799
|
+
/** Where this instance's CLI keeps the conversation a turn resume is judged on (#1699: claude's follows the profile). */
|
|
11800
|
+
resumeRoots(workingDirectory) {
|
|
11801
|
+
return { instanceDir: this.instanceDir, workingDirectory, claudeConfigDir: this.claudeConfigDir() };
|
|
11802
|
+
}
|
|
11119
11803
|
/**
|
|
11120
11804
|
* The credential profile's store this instance's CLI writes to, if any.
|
|
11121
11805
|
*
|