@songsid/agend 2.2.0-alpha.9 → 2.2.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +21 -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 +2 -2
- package/dist/auth-api.js +4 -2
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/antigravity.d.ts +3 -0
- package/dist/backend/antigravity.js +9 -0
- 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 +1 -1
- package/dist/backend/effort-metadata.js +3 -2
- 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 +33 -0
- package/dist/backend/launch-effort.js +123 -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 +33 -4
- package/dist/backend/types.js +2 -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 +26 -1
- package/dist/channel/adapters/discord.js +98 -3
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +26 -2
- package/dist/channel/adapters/telegram.js +123 -19
- 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 +24 -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.js +9 -3
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +41 -3
- package/dist/cli.js +133 -28
- 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 +764 -89
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +62 -7
- package/dist/delivery-outbox.js +309 -22
- 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 +352 -10
- package/dist/fleet-manager.js +1925 -175
- 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 +9 -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 +204 -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 +263 -96
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +26 -15
- package/dist/outbound-schemas.js +24 -7
- 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 +11 -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 +3 -0
- package/dist/reply-buttons-controller.js +3 -2
- package/dist/reply-buttons-controller.js.map +1 -1
- 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-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 +92 -7
- 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 +42 -2
- package/dist/ui/chat-store.js +2 -2
- package/dist/ui/chat-strings.js +38 -20
- package/dist/ui/chat-thread.js +69 -12
- package/dist/ui/chat-time.js +80 -0
- package/dist/ui/panel-chat.js +39 -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 +137 -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 +46 -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 +23 -5
- package/dist/ui/shared/app.css +159 -23
- 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 +10 -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-install.d.ts +28 -2
- package/dist/update-install.js +22 -4
- package/dist/update-install.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 +49 -2
- package/dist/web-api.js +421 -31
- 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-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-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 +63 -0
- package/dist/web-prefs.js +265 -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-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 +36 -1
- package/dist/web-session.js +53 -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. */
|
|
@@ -1058,6 +1089,10 @@ export class Daemon extends EventEmitter {
|
|
|
1058
1089
|
ipcServer = null;
|
|
1059
1090
|
messageBus;
|
|
1060
1091
|
transcriptMonitor = null;
|
|
1092
|
+
/** #1218: the transcript this instance's monitor follows now (the Log tab reads it on demand; nothing runs for it here). */
|
|
1093
|
+
logTranscriptPath() { return this.transcriptMonitor?.currentPath() ?? null; }
|
|
1094
|
+
/** #1218 (kiro): the launch this daemon's backend last built, for the Log's reason only (kiro-log-gate.ts). */
|
|
1095
|
+
logLaunch() { return this.backend?.logLaunch?.() ?? null; }
|
|
1061
1096
|
guardian = null;
|
|
1062
1097
|
adapter = null;
|
|
1063
1098
|
pendingIpcRequests = new Map();
|
|
@@ -1144,6 +1179,34 @@ export class Daemon extends EventEmitter {
|
|
|
1144
1179
|
dialogParkedKey = null;
|
|
1145
1180
|
dialogParkedInteractionEpisode = null;
|
|
1146
1181
|
dialogParkedReported = false;
|
|
1182
|
+
/**
|
|
1183
|
+
* #1691: the prompt at the bottom of the pane that no backend table knows — its block, since when (performance.now)
|
|
1184
|
+
* it has been unchanged, and whether that has lasted UNRECOGNISED_PROMPT_STABLE_MS. Updated by the runtime scan.
|
|
1185
|
+
*/
|
|
1186
|
+
unrecognisedPrompt = null;
|
|
1187
|
+
/**
|
|
1188
|
+
* #1691: when (performance.now) the newest pane read that decided the evidence above was taken — a scan or a fresh
|
|
1189
|
+
* read, confirming, restarting or ending it. A read taken before it finishes late and says nothing about now: it may
|
|
1190
|
+
* neither clear, restart nor confirm (#1695 r4).
|
|
1191
|
+
*/
|
|
1192
|
+
unrecognisedPromptReadAt = -Infinity;
|
|
1193
|
+
/**
|
|
1194
|
+
* #1691: the catch-all for that prompt, as a hold-only runtime dialog — so the delivery hold, awaiting_input, the
|
|
1195
|
+
* Needs-you item and the single parked report are the existing ones. Never answered (hold-only, no keys).
|
|
1196
|
+
*/
|
|
1197
|
+
unrecognisedPromptDialog = {
|
|
1198
|
+
pattern: /(?!)unrecognised-prompt-1691/,
|
|
1199
|
+
keys: [],
|
|
1200
|
+
description: "a prompt AgEnD does not recognise",
|
|
1201
|
+
holdOnly: true,
|
|
1202
|
+
isActive: (pane) => this.unrecognisedPromptActive(pane),
|
|
1203
|
+
requestIdentity: () => this.unrecognisedPrompt?.block ?? null,
|
|
1204
|
+
};
|
|
1205
|
+
/**
|
|
1206
|
+
* #1690: AgEnD's own runtime `/model` in flight — from the moment its paste went in until its "Switch model?" dialog
|
|
1207
|
+
* (if any) is answered. Every delivery is held meanwhile: the dialog's default cursor is Yes, a stray Enter confirms it.
|
|
1208
|
+
*/
|
|
1209
|
+
ownSwitch = null;
|
|
1147
1210
|
/**
|
|
1148
1211
|
* The runtime dialog the monitor is answering, and how many of its answers in a row did nothing (the dialog was
|
|
1149
1212
|
* still on screen right after the keys). Not the on-screen duration: a burst of FRESH prompts, each answered, has a
|
|
@@ -1237,6 +1300,11 @@ export class Daemon extends EventEmitter {
|
|
|
1237
1300
|
turnReplyGuard = new TurnReplyGuard();
|
|
1238
1301
|
/** #1510: turn boundaries from the CLI's transcript, for a backend whose guard reads its turn end there. */
|
|
1239
1302
|
transcriptTurns = new TranscriptTurnLedger();
|
|
1303
|
+
/** #1641: when this launch's transcript monitor last read anything (a turn boundary, a tool, assistant text); performance.now(). */
|
|
1304
|
+
transcriptActivityAt = 0;
|
|
1305
|
+
/** #1641: the launch and composer a stuck pane's idle evidence is about, and since when (performance.now()); see recoverStuck. */
|
|
1306
|
+
stuckRecoveryKey = null;
|
|
1307
|
+
stuckRecoverySince = 0;
|
|
1240
1308
|
/** #1510: since when (monotonic) this generation has been held as running in the transcript. */
|
|
1241
1309
|
transcriptRunningSince = null;
|
|
1242
1310
|
/** Prevent a visible stale XML fragment from being recovered on later turns. */
|
|
@@ -1388,6 +1456,8 @@ export class Daemon extends EventEmitter {
|
|
|
1388
1456
|
recentPastes = [];
|
|
1389
1457
|
pendingInstructionsUpdate;
|
|
1390
1458
|
pendingInstructionsNotice = false;
|
|
1459
|
+
/** #1633: the one-time "what's new for you" notice, pasted before the next real delivery; recorded once pasted. */
|
|
1460
|
+
pendingWhatsNew = null;
|
|
1391
1461
|
// Whether the warmup steering-reload notice should be injected after spawn.
|
|
1392
1462
|
// Set in trySpawn by comparing the freshly-built instructions against the
|
|
1393
1463
|
// last value the agent was told about (prev-instructions). Skipped when
|
|
@@ -1856,8 +1926,14 @@ export class Daemon extends EventEmitter {
|
|
|
1856
1926
|
this.finishDurableDelivery(delivery, "delivered", OUTPUT_EDGE_ONLY_EVIDENCE);
|
|
1857
1927
|
return;
|
|
1858
1928
|
}
|
|
1929
|
+
// #1647: a raw paste on a pane with no transcript (kiro), taken by the CLI as its own input row shows.
|
|
1930
|
+
if (verdict.proof === RAW_PASTE_LEFT_INPUT) {
|
|
1931
|
+
this.finishDurableDelivery(delivery, "delivered", verdict.proof, verdict);
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1859
1934
|
this.finishDurableDelivery(delivery, "delivered", verdict.transcriptProof
|
|
1860
|
-
? `positive submission proof; ${transcriptMarkerEvidence(verdict.transcriptProof
|
|
1935
|
+
? `positive submission proof; ${transcriptMarkerEvidence(verdict.transcriptProof, verdict.submissionMode === "raw_paste")}`
|
|
1936
|
+
: "positive submission proof", verdict);
|
|
1861
1937
|
}
|
|
1862
1938
|
/**
|
|
1863
1939
|
* Finish a delivery that only the output edge vouched for, by the delivery's own marker in the CLI's transcript (#758).
|
|
@@ -1873,23 +1949,28 @@ export class Daemon extends EventEmitter {
|
|
|
1873
1949
|
* TRANSCRIPT_PROOF_WINDOW_MS). It never finishes a row that stop has handed to reconciliation; a pause or respawn
|
|
1874
1950
|
* only ends the wait, falling back to today's outcome.
|
|
1875
1951
|
*/
|
|
1876
|
-
async proveDeliveryFromTranscript(delivery, checkpoint) {
|
|
1952
|
+
async proveDeliveryFromTranscript(delivery, checkpoint, rawPaste) {
|
|
1877
1953
|
const spawn = this.spawnGeneration;
|
|
1878
1954
|
const fence = this.launchFenceEpoch;
|
|
1879
1955
|
let last = "unavailable";
|
|
1880
1956
|
let outcome = "delivered";
|
|
1881
1957
|
let evidence = OUTPUT_EDGE_ONLY_EVIDENCE;
|
|
1958
|
+
// #1647: a raw paste has no marker; its text is the needle, and its pane proof already failed — so without the turn
|
|
1959
|
+
// in the transcript it stays uncertain (with the pane's reason), never the output-edge outcome.
|
|
1960
|
+
const needle = rawPaste ? { rawText: rawPaste.text } : delivery.deliveryId;
|
|
1961
|
+
let found = false;
|
|
1882
1962
|
try {
|
|
1883
1963
|
for (let polls = 0; polls <= TRANSCRIPT_PROOF_WINDOW_MS / TRANSCRIPT_PROOF_POLL_MS; polls++) {
|
|
1884
1964
|
if (this.deliveryWritesStopping)
|
|
1885
1965
|
return; // reconciliation owns the row now
|
|
1886
|
-
const
|
|
1887
|
-
if (
|
|
1966
|
+
const look = await scanTranscriptForDeliveryMarker(checkpoint.path, checkpoint.offset, checkpoint.backend, needle);
|
|
1967
|
+
if (look === "user" || look === "absorbed" || look === "queued") {
|
|
1968
|
+
found = true;
|
|
1888
1969
|
outcome = "delivered";
|
|
1889
|
-
evidence = transcriptMarkerEvidence(
|
|
1970
|
+
evidence = transcriptMarkerEvidence(look, !!rawPaste);
|
|
1890
1971
|
break;
|
|
1891
1972
|
}
|
|
1892
|
-
last =
|
|
1973
|
+
last = look;
|
|
1893
1974
|
if (spawn !== this.spawnGeneration || fence !== this.launchFenceEpoch) {
|
|
1894
1975
|
last = "unavailable";
|
|
1895
1976
|
break;
|
|
@@ -1909,6 +1990,11 @@ export class Daemon extends EventEmitter {
|
|
|
1909
1990
|
catch (err) {
|
|
1910
1991
|
this.logger.debug({ err }, "Transcript proof of a delivery failed — keeping the output-edge outcome");
|
|
1911
1992
|
}
|
|
1993
|
+
if (rawPaste && !found) {
|
|
1994
|
+
outcome = "uncertain";
|
|
1995
|
+
evidence = `${rawPaste.unproven}; no-transcript-turn (${last})`;
|
|
1996
|
+
this.logger.warn({ deliveryId: delivery.deliveryId, cause: evidence }, "Raw paste outcome uncertain — not re-pasting");
|
|
1997
|
+
}
|
|
1912
1998
|
// The settlement itself can throw (e.g. the outbox store is unhealthy), and this runs on a detached `void`
|
|
1913
1999
|
// promise: an uncaught throw is an unhandled rejection, which the fleet restart path turns into a global fault.
|
|
1914
2000
|
// Settle observably and leave the row for reconciliation — never a false completion, never another paste (#1207 P2).
|
|
@@ -1922,6 +2008,7 @@ export class Daemon extends EventEmitter {
|
|
|
1922
2008
|
}
|
|
1923
2009
|
}
|
|
1924
2010
|
deferDurableDelivery(meta, reason) {
|
|
2011
|
+
this.finishHumanReceipt(meta, { reached: false });
|
|
1925
2012
|
const delivery = this.durableDeliveryAttempt(meta);
|
|
1926
2013
|
if (delivery === null || delivery === false)
|
|
1927
2014
|
return;
|
|
@@ -3174,7 +3261,6 @@ export class Daemon extends EventEmitter {
|
|
|
3174
3261
|
if (this.runtimeMonitorsFrozen || this.errorMonitorTimer)
|
|
3175
3262
|
return;
|
|
3176
3263
|
const patterns = this.backend?.getErrorPatterns?.() ?? [];
|
|
3177
|
-
const dialogs = this.backend?.getRuntimeDialogs?.() ?? [];
|
|
3178
3264
|
if (!this.tmux)
|
|
3179
3265
|
return;
|
|
3180
3266
|
if (!this.backend)
|
|
@@ -3210,6 +3296,9 @@ export class Daemon extends EventEmitter {
|
|
|
3210
3296
|
scanStartedAt = performance.now();
|
|
3211
3297
|
if (stale())
|
|
3212
3298
|
return;
|
|
3299
|
+
this.trackUnrecognisedPrompt(pane, captureMono, this.instanceStateLastOutputAt > 0 && this.instanceStateLastOutputAt >= captureAt);
|
|
3300
|
+
// The backend's dialogs, and #1691's catch-all while an unrecognised prompt is active (decided just above).
|
|
3301
|
+
const dialogs = this.runtimeDialogs();
|
|
3213
3302
|
if (this.instanceStateLastOutputAt > 0 && this.instanceStateLastOutputAt >= captureAt) {
|
|
3214
3303
|
this.unverifyInteraction(captureMono, captureOrder);
|
|
3215
3304
|
}
|
|
@@ -3689,14 +3778,21 @@ export class Daemon extends EventEmitter {
|
|
|
3689
3778
|
isDeliveryEpochCurrent(epoch) {
|
|
3690
3779
|
return epoch === this.deliveryEpoch;
|
|
3691
3780
|
}
|
|
3692
|
-
queueRawPaste(rawText, deliveryEpoch = this.deliveryEpoch, confirmClear = false, durableAttempt) {
|
|
3781
|
+
queueRawPaste(rawText, deliveryEpoch = this.deliveryEpoch, confirmClear = false, durableAttempt, ownSwitch) {
|
|
3782
|
+
const reportSwitch = (result) => {
|
|
3783
|
+
if (!ownSwitch)
|
|
3784
|
+
return;
|
|
3785
|
+
this.ipcServer?.broadcast({ type: "runtime_switch_result", id: ownSwitch.id, ...result });
|
|
3786
|
+
};
|
|
3693
3787
|
if (!this.tmux || !this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
3694
3788
|
if (durableAttempt)
|
|
3695
3789
|
this.retryDurableDeliveryBeforeBegin(durableAttempt, "raw_paste target unavailable before pane queue");
|
|
3790
|
+
reportSwitch({ outcome: "not_sent" });
|
|
3696
3791
|
return;
|
|
3697
3792
|
}
|
|
3698
3793
|
this.pasteLock = this.pasteLock.then(async () => {
|
|
3699
3794
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
3795
|
+
reportSwitch({ outcome: "not_sent" });
|
|
3700
3796
|
this.logger.info("Pending raw delivery dropped by user cancel");
|
|
3701
3797
|
if (durableAttempt)
|
|
3702
3798
|
this.retryDurableDeliveryBeforeBegin(durableAttempt, "raw_paste epoch changed while queued");
|
|
@@ -3708,6 +3804,7 @@ export class Daemon extends EventEmitter {
|
|
|
3708
3804
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
3709
3805
|
if (durableAttempt)
|
|
3710
3806
|
this.retryDurableDeliveryBeforeBegin(durableAttempt, "raw_paste epoch changed while waking");
|
|
3807
|
+
reportSwitch({ outcome: "not_sent" }); // #1690: cancelled before anything was typed — said at once, nothing saved
|
|
3711
3808
|
return;
|
|
3712
3809
|
}
|
|
3713
3810
|
const delivered = await this.deliverMessage(rawText, undefined, {
|
|
@@ -3715,11 +3812,25 @@ export class Daemon extends EventEmitter {
|
|
|
3715
3812
|
verdict,
|
|
3716
3813
|
durableAttempt,
|
|
3717
3814
|
preserveExactBytes: true,
|
|
3815
|
+
// #1690: our own /model holds every other delivery from its write on — before the lock is released, so a steer
|
|
3816
|
+
// or /btw waiting on it cannot type into the gap before claude paints "Switch model?" (default cursor: Yes).
|
|
3817
|
+
...(ownSwitch ? { onPaneWritten: () => { this.ownSwitch = ownSwitch; } } : {}),
|
|
3718
3818
|
});
|
|
3719
3819
|
if (delivered)
|
|
3720
3820
|
this.finishDurableSubmission(durableAttempt ?? null, verdict);
|
|
3721
3821
|
else if (durableAttempt && verdict.paneWriteStarted) {
|
|
3722
|
-
|
|
3822
|
+
const unproven = `${verdict.phase ?? "raw-paste"}:${verdict.proof ?? "unverified"}`;
|
|
3823
|
+
const checkpoint = verdict.transcriptCheckpoint;
|
|
3824
|
+
if (checkpoint && RAW_TEXT_TRANSCRIPT_BACKENDS.includes(checkpoint.backend)) {
|
|
3825
|
+
// #1647: the pane could not show it, but the CLI's transcript records the turn the paste started — later
|
|
3826
|
+
// than the pane window sometimes. Wait for it there (bounded), off the paste lock.
|
|
3827
|
+
this.logger.info({ deliveryId: durableAttempt.deliveryId, unproven }, "Raw paste not proven on the pane — asking the CLI's transcript for the turn it started");
|
|
3828
|
+
void this.proveDeliveryFromTranscript(durableAttempt, checkpoint, { text: rawText, unproven });
|
|
3829
|
+
}
|
|
3830
|
+
else {
|
|
3831
|
+
this.logger.warn({ deliveryId: durableAttempt.deliveryId, cause: unproven }, "Raw paste outcome uncertain — not re-pasting");
|
|
3832
|
+
this.finishDurableDelivery(durableAttempt, "uncertain", unproven);
|
|
3833
|
+
}
|
|
3723
3834
|
}
|
|
3724
3835
|
else if (durableAttempt && verdict.durableBeginCommitted) {
|
|
3725
3836
|
this.abortDurableDelivery(durableAttempt, `${verdict.phase ?? "raw-paste"}:${verdict.proof ?? "no-pane-write"}`);
|
|
@@ -3736,10 +3847,31 @@ export class Daemon extends EventEmitter {
|
|
|
3736
3847
|
else if (durableAttempt)
|
|
3737
3848
|
this.retryDurableDeliveryBeforeBegin(durableAttempt, err.message);
|
|
3738
3849
|
this.logger.warn({ err: err.message }, "raw_paste delivery error");
|
|
3850
|
+
if (ownSwitch) {
|
|
3851
|
+
if (this.ownSwitch === ownSwitch)
|
|
3852
|
+
this.ownSwitch = null;
|
|
3853
|
+
reportSwitch({ outcome: "not_confirmed" });
|
|
3854
|
+
}
|
|
3739
3855
|
return;
|
|
3740
3856
|
}
|
|
3741
3857
|
if (confirmClear)
|
|
3742
3858
|
await this.confirmBackendClearDialog();
|
|
3859
|
+
if (ownSwitch) {
|
|
3860
|
+
if (this.ownSwitch !== ownSwitch)
|
|
3861
|
+
reportSwitch({ outcome: "not_sent" }); // its write never began
|
|
3862
|
+
else {
|
|
3863
|
+
try {
|
|
3864
|
+
reportSwitch(await this.answerOwnSwitchDialog(ownSwitch));
|
|
3865
|
+
}
|
|
3866
|
+
catch (err) {
|
|
3867
|
+
this.logger.warn({ err: err.message }, "Answering our own model switch failed");
|
|
3868
|
+
reportSwitch({ outcome: "not_confirmed" });
|
|
3869
|
+
}
|
|
3870
|
+
finally {
|
|
3871
|
+
this.ownSwitch = null;
|
|
3872
|
+
}
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3743
3875
|
this.logger.debug({ text: rawText.slice(0, 100) }, "Raw paste delivered");
|
|
3744
3876
|
});
|
|
3745
3877
|
this.pasteLock = this.pasteLock.catch(err => {
|
|
@@ -3758,7 +3890,93 @@ export class Daemon extends EventEmitter {
|
|
|
3758
3890
|
this.deferDurableDelivery(deliveryMeta, "invalid raw_paste delivery attempt metadata");
|
|
3759
3891
|
return;
|
|
3760
3892
|
}
|
|
3761
|
-
|
|
3893
|
+
const sw = msg.runtime_switch;
|
|
3894
|
+
const ownSwitch = sw && typeof sw.id === "string" && sw.kind === "model" && typeof sw.requested === "string"
|
|
3895
|
+
? { id: sw.id, kind: "model", requested: sw.requested } : undefined;
|
|
3896
|
+
this.queueRawPaste(msg.content, this.captureDeliveryEpoch(msg.delivery_epoch), msg.confirm_clear === true, durableAttempt ? { ...durableAttempt, submissionMode: "raw_paste" } : undefined, ownSwitch);
|
|
3897
|
+
}
|
|
3898
|
+
/**
|
|
3899
|
+
* #1690 (the user's choice A): after AgEnD's own `/model <name>`, claude may ask "Switch model?" (a warm cache for
|
|
3900
|
+
* another model). When the model it names is the one asked for, answer Yes; otherwise No (Escape — `Kept model as …`),
|
|
3901
|
+
* and say so. Every key goes on a freshly read, verified dialog under the pane write lock: the same target, and for
|
|
3902
|
+
* Enter the cursor on "1. Yes"; the cursor is walked there with Up, one verified step at a time, never a blind key.
|
|
3903
|
+
* Without the dialog, claude's own "Set model to …" line after the command is the result.
|
|
3904
|
+
*/
|
|
3905
|
+
async answerOwnSwitchDialog(spec) {
|
|
3906
|
+
const tmux = this.tmux;
|
|
3907
|
+
if (!tmux)
|
|
3908
|
+
return { outcome: "not_confirmed" };
|
|
3909
|
+
const read = () => tmux.capturePane().catch(() => "");
|
|
3910
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
3911
|
+
/** One key on a dialog re-read under the lock: still the same target, and (when given) the cursor where expected. */
|
|
3912
|
+
const press = (target, cursor, key) => this.paneWriteLock.run(async () => {
|
|
3913
|
+
if (this.tmux !== tmux)
|
|
3914
|
+
return false;
|
|
3915
|
+
const now = claudeSwitchDialog(await read());
|
|
3916
|
+
if (!now || now.kind !== "model" || now.target !== target || (cursor !== null && now.cursor !== cursor))
|
|
3917
|
+
return false;
|
|
3918
|
+
return tmux.sendSpecialKey(key);
|
|
3919
|
+
});
|
|
3920
|
+
const gone = async () => {
|
|
3921
|
+
const until = performance.now() + OWN_SWITCH_SETTLE_MS;
|
|
3922
|
+
while (performance.now() < until) {
|
|
3923
|
+
if (!claudeSwitchDialog(await read()))
|
|
3924
|
+
return true;
|
|
3925
|
+
await sleep(150);
|
|
3926
|
+
}
|
|
3927
|
+
return false;
|
|
3928
|
+
};
|
|
3929
|
+
const deadline = performance.now() + OWN_SWITCH_WAIT_MS;
|
|
3930
|
+
while (performance.now() < deadline && this.tmux === tmux) {
|
|
3931
|
+
const pane = await read();
|
|
3932
|
+
const dialog = claudeSwitchDialog(pane);
|
|
3933
|
+
if (dialog && dialog.kind === "model") {
|
|
3934
|
+
const shown = dialog.target;
|
|
3935
|
+
if (!claudeSwitchTargetMatches(spec.requested, shown)) {
|
|
3936
|
+
const answered = await press(shown, null, "Escape") && await gone();
|
|
3937
|
+
this.logger.warn({ requested: spec.requested, shown, answered }, "claude would switch to another model than asked — answered No");
|
|
3938
|
+
return { outcome: "mismatch", shown, answered };
|
|
3939
|
+
}
|
|
3940
|
+
// Walk the cursor to "1. Yes" (it starts there; a human may have moved it): Up from row 2, re-read each time.
|
|
3941
|
+
let current = dialog;
|
|
3942
|
+
for (let step = 0; step < 2 && current.cursor === 2; step++) {
|
|
3943
|
+
if (!await press(shown, 2, "Up"))
|
|
3944
|
+
break;
|
|
3945
|
+
await sleep(120);
|
|
3946
|
+
const next = claudeSwitchDialog(await read());
|
|
3947
|
+
if (!next || next.target !== shown)
|
|
3948
|
+
break;
|
|
3949
|
+
current = next;
|
|
3950
|
+
}
|
|
3951
|
+
if (current.cursor !== 1 || !await press(shown, 1, "Enter") || !await gone()) {
|
|
3952
|
+
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");
|
|
3953
|
+
return { outcome: "not_confirmed", shown };
|
|
3954
|
+
}
|
|
3955
|
+
this.logger.info({ requested: spec.requested, shown }, "Confirmed our own model switch (claude: the next reply re-reads the history)");
|
|
3956
|
+
return { outcome: "switched", confirmed: true, shown };
|
|
3957
|
+
}
|
|
3958
|
+
if (Daemon.switchResultAfterCommand(pane, spec.requested))
|
|
3959
|
+
return { outcome: "switched", confirmed: false };
|
|
3960
|
+
await sleep(200);
|
|
3961
|
+
}
|
|
3962
|
+
return { outcome: "not_confirmed" };
|
|
3963
|
+
}
|
|
3964
|
+
/**
|
|
3965
|
+
* #1690: claude's "Set model to …" line under our `/model <requested>` (no dialog was needed). Only the newest command
|
|
3966
|
+
* on the pane counts: an older identical `/model` with its result further up (switched away and back) must not pass
|
|
3967
|
+
* for this one before it has even painted — that left its dialog unanswered (live, 2.1.296).
|
|
3968
|
+
*/
|
|
3969
|
+
static switchResultAfterCommand(pane, requested) {
|
|
3970
|
+
const rows = pane.replace(/\r/g, "").split("\n");
|
|
3971
|
+
let last = -1;
|
|
3972
|
+
for (let i = rows.length - 1; i >= 0; i--)
|
|
3973
|
+
if (/^❯[ \t\u00a0]+\S/.test(rows[i])) {
|
|
3974
|
+
last = i;
|
|
3975
|
+
break;
|
|
3976
|
+
}
|
|
3977
|
+
if (last < 0 || rows[last].replace(/^❯[ \t\u00a0]+/, "").trimEnd() !== `/model ${requested}`)
|
|
3978
|
+
return false;
|
|
3979
|
+
return rows.slice(last + 1).some(row => /^\s*⎿?\s*Set model to /.test(row));
|
|
3762
3980
|
}
|
|
3763
3981
|
/**
|
|
3764
3982
|
* Confirm a backend's terminal-level /clear prompt after the channel-level
|
|
@@ -4021,6 +4239,13 @@ export class Daemon extends EventEmitter {
|
|
|
4021
4239
|
if (!backend.quitBlockedByDialog(pane))
|
|
4022
4240
|
return false;
|
|
4023
4241
|
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");
|
|
4242
|
+
// #1716: the background work is about to be ended — record it where the
|
|
4243
|
+
// stop/restart reply and the instance topic notice can report it, so the
|
|
4244
|
+
// kill is never silent.
|
|
4245
|
+
try {
|
|
4246
|
+
writeBackgroundWorkEndedMarker(this.instanceDir, reason);
|
|
4247
|
+
}
|
|
4248
|
+
catch { /* reporting only */ }
|
|
4024
4249
|
await this.tmux.sendSpecialKey("Escape").catch(() => false);
|
|
4025
4250
|
return true;
|
|
4026
4251
|
}
|
|
@@ -4222,7 +4447,9 @@ export class Daemon extends EventEmitter {
|
|
|
4222
4447
|
// Bare backend regexes are not proof of a live modal. The canonical
|
|
4223
4448
|
// isActive predicate replaces pattern.test(), preserving table order.
|
|
4224
4449
|
const dialog = dialogs.find(d => d.isActive && (d.inputBlocked || d.blocksDelivery || d.holdOnly)
|
|
4225
|
-
&& Daemon.dialogMatches(d, pane)
|
|
4450
|
+
&& Daemon.dialogMatches(d, pane)
|
|
4451
|
+
// #1690: AgEnD's own switch dialog, being answered right now, is nobody's to answer.
|
|
4452
|
+
&& !(this.ownSwitch && claudeSwitchDialog(pane) !== null));
|
|
4226
4453
|
if (dialog) {
|
|
4227
4454
|
const identity = dialog.requestIdentity?.(pane) ?? pane.replace(/\r/g, "")
|
|
4228
4455
|
.split("\n").map(row => row.replace(/^([ \t]*)[❯›]([ \t]+)(?=\d\.|Yes\b|No\b|Resume\b)/, "$1$2").trimEnd())
|
|
@@ -4238,7 +4465,7 @@ export class Daemon extends EventEmitter {
|
|
|
4238
4465
|
// No captured text leaves this function or the private observer identity.
|
|
4239
4466
|
return evidence ? { ...evidence, identity: createHash("sha256").update(evidence.identity).digest("hex") } : null;
|
|
4240
4467
|
}
|
|
4241
|
-
observeInteractionPane(pane, observedAt, monotonicAt, dialogs = this.
|
|
4468
|
+
observeInteractionPane(pane, observedAt, monotonicAt, dialogs = this.runtimeDialogs(), order = ++this.interactionCaptureSerial, allowConfirmation = true) {
|
|
4242
4469
|
const owner = this.interactionOwner();
|
|
4243
4470
|
const evidence = this.interactionEvidence(pane, dialogs);
|
|
4244
4471
|
if (!this.interactionObservation.observe(evidence, owner, observedAt, monotonicAt, this.interactionOutputRevision !== this.interactionObservedOutputRevision, order))
|
|
@@ -4351,6 +4578,39 @@ export class Daemon extends EventEmitter {
|
|
|
4351
4578
|
this.pauseRequested = false;
|
|
4352
4579
|
return;
|
|
4353
4580
|
}
|
|
4581
|
+
// #1717: an idle pause is AgEnD-initiated and silent — it must not kill
|
|
4582
|
+
// the user's running background work the way an explicit stop may. Defer
|
|
4583
|
+
// this cycle while the backend reports background work; the next idle
|
|
4584
|
+
// tick retries once the task ends. A pane-capture failure proceeds (the
|
|
4585
|
+
// quit-menu handler below stays the backstop), never wedges unpaused.
|
|
4586
|
+
if (this.backend?.backgroundWorkRunning && this.tmux) {
|
|
4587
|
+
const tmux = this.tmux;
|
|
4588
|
+
const windowId = tmux.getWindowId?.();
|
|
4589
|
+
const generation = this.spawnGeneration;
|
|
4590
|
+
let pane = null;
|
|
4591
|
+
try {
|
|
4592
|
+
pane = await tmux.capturePane();
|
|
4593
|
+
}
|
|
4594
|
+
catch { /* capture failed — pause as before, after revalidation */ }
|
|
4595
|
+
// #1730 P2-2: the await yields — the pane read is only valid for the
|
|
4596
|
+
// world it was taken from. Revalidate owner, phase, state and queue
|
|
4597
|
+
// before acting on it; anything that moved defers this cycle.
|
|
4598
|
+
if (this.tmux !== tmux
|
|
4599
|
+
|| this.tmux?.getWindowId?.() !== windowId
|
|
4600
|
+
|| this.spawnGeneration !== generation
|
|
4601
|
+
|| this.getPauseWakeState() !== "active"
|
|
4602
|
+
|| !(this.instanceState === "idle" || (allowStuck && this.instanceState === "stuck"))
|
|
4603
|
+
|| this.pasteQueueDepth > 0) {
|
|
4604
|
+
this.logger.info("Deferring auto-pause: pause preconditions changed during the pane read");
|
|
4605
|
+
this.pauseRequested = false;
|
|
4606
|
+
return;
|
|
4607
|
+
}
|
|
4608
|
+
if (pane !== null && this.backend?.backgroundWorkRunning?.(pane)) {
|
|
4609
|
+
this.logger.info("Deferring auto-pause: background work is still running");
|
|
4610
|
+
this.pauseRequested = false;
|
|
4611
|
+
return;
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4354
4614
|
this.pauseWakeState = "pausing";
|
|
4355
4615
|
this.healthCheckPaused = true;
|
|
4356
4616
|
this.freezeRuntimeMonitors();
|
|
@@ -4723,10 +4983,7 @@ export class Daemon extends EventEmitter {
|
|
|
4723
4983
|
backend: resumeBackend ?? "unknown",
|
|
4724
4984
|
cwd: resumeCwd,
|
|
4725
4985
|
armedAt: Date.now(),
|
|
4726
|
-
before: readResumeFingerprint(normalizeResumeBackend(resumeBackend),
|
|
4727
|
-
instanceDir: this.instanceDir,
|
|
4728
|
-
workingDirectory: resumeCwd,
|
|
4729
|
-
}),
|
|
4986
|
+
before: readResumeFingerprint(normalizeResumeBackend(resumeBackend), this.resumeRoots(resumeCwd)),
|
|
4730
4987
|
});
|
|
4731
4988
|
}
|
|
4732
4989
|
}
|
|
@@ -4772,7 +5029,7 @@ export class Daemon extends EventEmitter {
|
|
|
4772
5029
|
&& this.spawnGeneration === gateSpawn
|
|
4773
5030
|
&& this.launchFenceEpoch === gateFence;
|
|
4774
5031
|
const backend = this.resolveResumeBackend() ?? normalizeResumeBackend(marker.backend);
|
|
4775
|
-
const roots =
|
|
5032
|
+
const roots = this.resumeRoots(this.config.working_directory ?? "");
|
|
4776
5033
|
// This boot's CLI spawn is what the daemon itself caused: pre-restart
|
|
4777
5034
|
// writes (including pre-crash progress) are excluded, and the flush
|
|
4778
5035
|
// grace holds the decision until post-spawn writes are observable.
|
|
@@ -5041,9 +5298,68 @@ export class Daemon extends EventEmitter {
|
|
|
5041
5298
|
buildTranscriptMonitor(source) {
|
|
5042
5299
|
const monitor = new TranscriptMonitor(this.instanceDir, this.logger, source);
|
|
5043
5300
|
this.transcriptTurns.reset();
|
|
5044
|
-
|
|
5301
|
+
this.transcriptActivityAt = 0;
|
|
5302
|
+
// Monotonic: a wall-clock correction must neither end nor extend the open-turn hold (#1641 R1).
|
|
5303
|
+
const touch = () => { this.transcriptActivityAt = performance.now(); };
|
|
5304
|
+
monitor.on("turns", (turns) => { this.transcriptTurns.observe(turns, performance.now()); touch(); });
|
|
5305
|
+
// #1641 R2: a re-baselined source starts over, and so does what it said (an open turn read before is withdrawn).
|
|
5306
|
+
monitor.on("reset", () => { if (this.transcriptMonitor === monitor) {
|
|
5307
|
+
this.transcriptTurns.reset();
|
|
5308
|
+
this.transcriptActivityAt = 0;
|
|
5309
|
+
} });
|
|
5310
|
+
for (const event of ["tool_use", "tool_result", "assistant_text"])
|
|
5311
|
+
monitor.on(event, touch);
|
|
5045
5312
|
return monitor;
|
|
5046
5313
|
}
|
|
5314
|
+
/**
|
|
5315
|
+
* #1641: what the CLI's own transcript (codex's rollout) says about its turn. `open`: a turn started and has not ended,
|
|
5316
|
+
* and the transcript moved within TRANSCRIPT_OPEN_TURN_QUIET_MS. `closed`: every turn read since this launch's monitor
|
|
5317
|
+
* began has ended. `unknown`: not a transcript backend, nothing followed, or an open turn gone quiet (a missed end).
|
|
5318
|
+
*/
|
|
5319
|
+
transcriptTurnState(now = performance.now()) {
|
|
5320
|
+
if (this.backend?.turnEndFromTranscript !== true || this.transcriptMonitor?.following() !== true)
|
|
5321
|
+
return "unknown";
|
|
5322
|
+
// #1641 R2: an empty ledger after a baseline that could not see the current turn's start is not "closed".
|
|
5323
|
+
if (this.transcriptMonitor.turnStateKnown?.() !== true)
|
|
5324
|
+
return "unknown";
|
|
5325
|
+
const since = this.transcriptTurns.openSince();
|
|
5326
|
+
if (since === null)
|
|
5327
|
+
return "closed";
|
|
5328
|
+
return now - Math.max(since, this.transcriptActivityAt) < TRANSCRIPT_OPEN_TURN_QUIET_MS ? "open" : "unknown";
|
|
5329
|
+
}
|
|
5330
|
+
/**
|
|
5331
|
+
* #1641: a stuck pane that is in fact an idle CLI (a footer this build cannot read) must not stay stuck for hours with
|
|
5332
|
+
* every delivery parked behind it. It recovers when the backend sees its own live, EMPTY composer with no busy, queued
|
|
5333
|
+
* or dialog marker (isStableUnknownLayoutIdlePane) AND the transcript shows no open turn — or, with no transcript to
|
|
5334
|
+
* ask, that same composer held unchanged for UNKNOWN_LAYOUT_STABLE_MS (#978's rule). An open turn never recovers.
|
|
5335
|
+
*/
|
|
5336
|
+
recoverStuck(pane, snapshot, changeAt, turn) {
|
|
5337
|
+
const machine = this.instanceStateMachine;
|
|
5338
|
+
const idleComposer = this.backend?.isStableUnknownLayoutIdlePane?.(pane) === true;
|
|
5339
|
+
// The resume load (#1031: `Resuming session…` over the same composer) is input that is not open yet (#1641 R1).
|
|
5340
|
+
if (!machine || !idleComposer || turn === "open" || this.inputTransientInPane(pane)) {
|
|
5341
|
+
this.stuckRecoveryKey = null;
|
|
5342
|
+
return snapshot;
|
|
5343
|
+
}
|
|
5344
|
+
// The evidence is about this launch and this screen. Once it holds, it keeps holding — a still pane reads stuck
|
|
5345
|
+
// again at the next sweep (its footer is still unreadable) and must not start the window over (#1641 R1).
|
|
5346
|
+
const mono = performance.now();
|
|
5347
|
+
const key = `${this.spawnGeneration}:${this.launchAttempt}\n${pane.replace(/⋆/gu, "").replace(/[ \t]+$/gmu, "")}`;
|
|
5348
|
+
if (key !== this.stuckRecoveryKey) {
|
|
5349
|
+
this.stuckRecoveryKey = key;
|
|
5350
|
+
this.stuckRecoverySince = mono;
|
|
5351
|
+
}
|
|
5352
|
+
if (turn === "unknown" && mono - this.stuckRecoverySince < UNKNOWN_LAYOUT_STABLE_MS)
|
|
5353
|
+
return snapshot;
|
|
5354
|
+
// A footer this build cannot read stays unreadable: the same still pane reads stuck again at every sweep and is
|
|
5355
|
+
// recovered again. Only the edge into idle is news.
|
|
5356
|
+
const fields = { evidence: turn === "closed" ? "transcript_no_open_turn" : "stable_empty_composer", unchangedForMs: snapshot.unchangedForMs };
|
|
5357
|
+
if (this.instanceState !== "idle")
|
|
5358
|
+
this.logger.info(fields, "Stuck instance recovered — the CLI shows its idle composer and is not running a turn (#1641)");
|
|
5359
|
+
else
|
|
5360
|
+
this.logger.debug(fields, "Still idle on its unreadable footer — the stuck reading was recovered again (#1641)");
|
|
5361
|
+
return machine.observe(pane, Date.now(), { settled: true, changeAt, forceReady: true });
|
|
5362
|
+
}
|
|
5047
5363
|
replyCompletionGuardEnabled() {
|
|
5048
5364
|
return this.config.reply_completion_guard !== false
|
|
5049
5365
|
&& this.backend?.replyCompletionGuard === true;
|
|
@@ -5425,7 +5741,9 @@ export class Daemon extends EventEmitter {
|
|
|
5425
5741
|
// a pair every ~1.6s for as long as the repaint lasted (#1116). Real
|
|
5426
5742
|
// work cannot hide behind this: it changes the layout, the next
|
|
5427
5743
|
// output arms another probe, and the debounce capture still fires.
|
|
5428
|
-
|
|
5744
|
+
// #1641: never while the transcript shows a turn running — that idle-looking screen is mid-turn.
|
|
5745
|
+
if (this.instanceState === "idle" && this.backend?.isPeriodicRedrawIdlePane?.(pane) === true
|
|
5746
|
+
&& this.transcriptTurnState() !== "open")
|
|
5429
5747
|
return;
|
|
5430
5748
|
// A second burst overtook the probe. Do not bless its stale capture as
|
|
5431
5749
|
// idle: continuous output is the safer working signal, while the next
|
|
@@ -5440,6 +5758,8 @@ export class Daemon extends EventEmitter {
|
|
|
5440
5758
|
return;
|
|
5441
5759
|
if (!this.observeInteractionPane(pane, captureStartedAt, interactionCaptureAt, undefined, interactionCaptureOrder, reason !== "interaction_confirmation"))
|
|
5442
5760
|
return;
|
|
5761
|
+
// #1691: an accepted capture of this launch is a fresh read for the unrecognised-prompt evidence too (#1695 r3).
|
|
5762
|
+
this.noteFreshPaneRead(pane, interactionCaptureAt);
|
|
5443
5763
|
this.updateInputBlockedState(pane);
|
|
5444
5764
|
// Advance the shared 10s timer only for a fresh capture of this launch.
|
|
5445
5765
|
const deliveryFallback = deliveryCandidate === true
|
|
@@ -5458,6 +5778,10 @@ export class Daemon extends EventEmitter {
|
|
|
5458
5778
|
// decisions about *now*. The old double-observe expressed that by calling
|
|
5459
5779
|
// observe twice, which silently disabled the "content moved" branch.
|
|
5460
5780
|
const structuredPeriodicIdle = this.backend?.isPeriodicRedrawIdlePane;
|
|
5781
|
+
// #1641: the transcript outranks the pane: while it shows the turn running, no reading of this pane is idle (codex
|
|
5782
|
+
// hides its status row while streaming, leaving the idle composer and footer in view mid-turn).
|
|
5783
|
+
const transcriptTurn = this.transcriptTurnState();
|
|
5784
|
+
const transcriptBusy = transcriptTurn === "open";
|
|
5461
5785
|
let snapshot;
|
|
5462
5786
|
if (deliveryFallback !== null) {
|
|
5463
5787
|
// A missing footer cannot fall through to a broad ready match. All
|
|
@@ -5466,7 +5790,7 @@ export class Daemon extends EventEmitter {
|
|
|
5466
5790
|
settled: true,
|
|
5467
5791
|
changeAt: observedChangeAt,
|
|
5468
5792
|
forceReady: deliveryFallback,
|
|
5469
|
-
forceBusy: !deliveryFallback || paneActivity !== null,
|
|
5793
|
+
forceBusy: !deliveryFallback || paneActivity !== null || transcriptBusy,
|
|
5470
5794
|
});
|
|
5471
5795
|
}
|
|
5472
5796
|
else if (structuredPeriodicIdle && reason === "output_probe") {
|
|
@@ -5488,6 +5812,7 @@ export class Daemon extends EventEmitter {
|
|
|
5488
5812
|
snapshot = this.instanceStateMachine.observe(pane, Date.now(), {
|
|
5489
5813
|
settled: true,
|
|
5490
5814
|
changeAt: observedChangeAt,
|
|
5815
|
+
forceBusy: transcriptBusy,
|
|
5491
5816
|
});
|
|
5492
5817
|
}
|
|
5493
5818
|
}
|
|
@@ -5521,6 +5846,7 @@ export class Daemon extends EventEmitter {
|
|
|
5521
5846
|
settled: true,
|
|
5522
5847
|
changeAt: observedChangeAt,
|
|
5523
5848
|
forceReady: true,
|
|
5849
|
+
forceBusy: transcriptBusy,
|
|
5524
5850
|
});
|
|
5525
5851
|
}
|
|
5526
5852
|
else {
|
|
@@ -5569,6 +5895,7 @@ export class Daemon extends EventEmitter {
|
|
|
5569
5895
|
snapshot = this.instanceStateMachine.observe(pane, Date.now(), {
|
|
5570
5896
|
settled: true,
|
|
5571
5897
|
changeAt: observedChangeAt,
|
|
5898
|
+
forceBusy: transcriptBusy,
|
|
5572
5899
|
});
|
|
5573
5900
|
}
|
|
5574
5901
|
else {
|
|
@@ -5586,9 +5913,11 @@ export class Daemon extends EventEmitter {
|
|
|
5586
5913
|
snapshot = this.instanceStateMachine.observe(pane, Date.now(), {
|
|
5587
5914
|
settled,
|
|
5588
5915
|
changeAt: observedChangeAt,
|
|
5589
|
-
forceBusy: paneActivity !== null,
|
|
5916
|
+
forceBusy: paneActivity !== null || transcriptBusy,
|
|
5590
5917
|
});
|
|
5591
5918
|
}
|
|
5919
|
+
if (snapshot.state === "stuck")
|
|
5920
|
+
snapshot = this.recoverStuck(pane, snapshot, observedChangeAt, transcriptTurn);
|
|
5592
5921
|
this.applyInstanceStateSnapshot(snapshot, pane);
|
|
5593
5922
|
// Backends without a transcript feed can still say what they are doing, if
|
|
5594
5923
|
// their TUI names it. Free-riding on a capture that already happened: no
|
|
@@ -5999,11 +6328,7 @@ export class Daemon extends EventEmitter {
|
|
|
5999
6328
|
* is a durable delivery) must LEAD the persisted user message — restart reconciliation recognises a delivery only by a
|
|
6000
6329
|
* marker at the start of the body (#1197) — so the banner goes right after it, not in front of it.
|
|
6001
6330
|
*/
|
|
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
|
-
}
|
|
6331
|
+
static steerEnvelope(inbound) { return steerInboundEnvelope(inbound); }
|
|
6007
6332
|
/**
|
|
6008
6333
|
* The one place an inbound message grows its metadata wrapper ([user:]/
|
|
6009
6334
|
* [from:] prefix, pending reactions, handoff metadata, reply instructions).
|
|
@@ -6011,33 +6336,68 @@ export class Daemon extends EventEmitter {
|
|
|
6011
6336
|
* here — a steered message must read EXACTLY like a queued one to the
|
|
6012
6337
|
* agent, or the two drift apart in what the agent is told about replying.
|
|
6013
6338
|
*/
|
|
6339
|
+
/** #1650: how this instance talks to the fleet — MCP tools, or agend-agent commands (agent_mode: cli). */
|
|
6340
|
+
agentMode() { return this.config.agent_mode === "cli" ? "cli" : "mcp"; }
|
|
6014
6341
|
formatInboundMessage(content, meta) {
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
let formatted;
|
|
6018
|
-
if (
|
|
6019
|
-
formatted
|
|
6342
|
+
if (!meta.from_instance)
|
|
6343
|
+
return formatHumanInboundMessage(content, meta, this.agentMode());
|
|
6344
|
+
let formatted = formatCrossInstanceInboundMessage(content, meta, this.agentMode());
|
|
6345
|
+
if (meta.reply_to_text)
|
|
6346
|
+
formatted += '\n(reply_to: "' + meta.reply_to_text + '")';
|
|
6347
|
+
if (meta.delivery_id)
|
|
6348
|
+
formatted = "[agend-delivery-id:" + meta.delivery_id + "]\n" + formatted;
|
|
6349
|
+
return formatted;
|
|
6350
|
+
}
|
|
6351
|
+
/** Pure cached owner: no pane/process query on ingress. */
|
|
6352
|
+
captureHumanReceiptOwner() {
|
|
6353
|
+
try {
|
|
6354
|
+
if (this.deliveryWritesStopping || this.spawning || !this.tmux)
|
|
6355
|
+
return null;
|
|
6356
|
+
const o = this.interactionOwner();
|
|
6357
|
+
const window = this.tmux.getWindowId();
|
|
6358
|
+
if (!window || !o.bootId || ![o.spawnGeneration, o.launchAttempt, o.launchFenceEpoch, this.deliveryEpoch]
|
|
6359
|
+
.every(n => Number.isSafeInteger(n) && n >= 0))
|
|
6360
|
+
return null;
|
|
6361
|
+
return JSON.stringify([o.bootId, o.spawnGeneration, o.launchAttempt, o.launchFenceEpoch,
|
|
6362
|
+
this.deliveryEpoch, humanOwnerObjectId(this.tmux), window]);
|
|
6020
6363
|
}
|
|
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)";
|
|
6364
|
+
catch {
|
|
6365
|
+
return null;
|
|
6033
6366
|
}
|
|
6034
|
-
|
|
6035
|
-
|
|
6367
|
+
}
|
|
6368
|
+
/** The same formatter/sanitizer used by the actual write; snapshot only. */
|
|
6369
|
+
humanReceiptWire(content, meta, kind) {
|
|
6370
|
+
let wire = kind === "raw_paste" ? content
|
|
6371
|
+
: !meta.from_instance && content.startsWith("/raw ") && kind === "fleet_inbound"
|
|
6372
|
+
? content.slice(5) : this.formatInboundMessage(content, meta);
|
|
6373
|
+
if (kind === "steer")
|
|
6374
|
+
wire = Daemon.steerEnvelope(wire);
|
|
6375
|
+
else if (kind === "btw")
|
|
6376
|
+
wire = BTW_INBOUND_PREFIX + wire;
|
|
6377
|
+
return prepareDeliveryWire(wire, kind === "raw_paste");
|
|
6378
|
+
}
|
|
6379
|
+
humanReceiptAttempt(meta, wire) {
|
|
6380
|
+
const id = meta.human_receipt_id;
|
|
6381
|
+
const daemonOwner = meta.human_daemon_owner;
|
|
6382
|
+
if (!id || !/^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/.test(id) || meta.verification_id !== "hmsg-" + id
|
|
6383
|
+
|| !daemonOwner || !meta.human_owner_key || !/^[a-f0-9]{64}$/.test(meta.human_wire_sha256 ?? "")
|
|
6384
|
+
|| humanBodyHash(prepareDeliveryWire(wire)) !== meta.human_wire_sha256)
|
|
6385
|
+
return undefined;
|
|
6386
|
+
return { receiptId: id, verificationId: meta.verification_id, expectedWireHash: meta.human_wire_sha256,
|
|
6387
|
+
ownerKey: meta.human_owner_key, current: () => this.captureHumanReceiptOwner() === daemonOwner };
|
|
6388
|
+
}
|
|
6389
|
+
finishHumanReceipt(meta, verdict) {
|
|
6390
|
+
if (!meta.human_receipt_id || !meta.human_owner_key)
|
|
6391
|
+
return;
|
|
6392
|
+
const attempt = verdict.humanReceipt;
|
|
6393
|
+
const proof = attempt?.current() && !["unverified", "output-edge", "unverifiable"].includes(verdict.proof ?? "")
|
|
6394
|
+
? attempt.proof ?? null : null;
|
|
6395
|
+
try {
|
|
6396
|
+
this.emit("human_receipt_result", { receipt_id: meta.human_receipt_id, owner_key: meta.human_owner_key, proof });
|
|
6036
6397
|
}
|
|
6037
|
-
|
|
6038
|
-
|
|
6398
|
+
catch (err) {
|
|
6399
|
+
this.logger.error({ err }, "Could not publish human delivery evidence");
|
|
6039
6400
|
}
|
|
6040
|
-
return formatted;
|
|
6041
6401
|
}
|
|
6042
6402
|
/**
|
|
6043
6403
|
* Whether ONE delivery reached a verdict, carried by that delivery.
|
|
@@ -6130,12 +6490,13 @@ export class Daemon extends EventEmitter {
|
|
|
6130
6490
|
this.deferDurableDelivery(meta, "delivery epoch changed while waking for steer");
|
|
6131
6491
|
return;
|
|
6132
6492
|
}
|
|
6133
|
-
const verdict = { reached: false };
|
|
6493
|
+
const verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
|
|
6134
6494
|
try {
|
|
6135
6495
|
if (await this.deliverMessage(formatted, status, {
|
|
6136
6496
|
steer: true,
|
|
6137
6497
|
deliveryEpoch,
|
|
6138
6498
|
submissionId: meta.message_id,
|
|
6499
|
+
...(meta.verification_id ? { humanVerificationId: meta.verification_id } : {}),
|
|
6139
6500
|
verdict,
|
|
6140
6501
|
durableAttempt: durableAttempt || undefined,
|
|
6141
6502
|
})) {
|
|
@@ -6166,6 +6527,9 @@ export class Daemon extends EventEmitter {
|
|
|
6166
6527
|
this.retryDurableDeliveryBeforeBegin(durableAttempt, err.message);
|
|
6167
6528
|
throw err;
|
|
6168
6529
|
}
|
|
6530
|
+
finally {
|
|
6531
|
+
this.finishHumanReceipt(meta, verdict);
|
|
6532
|
+
}
|
|
6169
6533
|
}).catch(err => {
|
|
6170
6534
|
this.deferDurableDelivery(meta, `target wake/steer failed before pane submission: ${err.message}`);
|
|
6171
6535
|
this.logger.warn({ err: err.message }, "steer delivery error");
|
|
@@ -6181,24 +6545,37 @@ export class Daemon extends EventEmitter {
|
|
|
6181
6545
|
* so channel delivery intentionally uses a normal wrapped inbound instead.
|
|
6182
6546
|
*/
|
|
6183
6547
|
btwMessage(content, meta, deliveryEpoch = this.deliveryEpoch) {
|
|
6184
|
-
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
6548
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
6549
|
+
this.finishHumanReceipt(meta, { reached: false });
|
|
6185
6550
|
return;
|
|
6551
|
+
}
|
|
6186
6552
|
this.updateLastChat(meta.chat_id, meta.thread_id, meta.adapter_id);
|
|
6187
6553
|
this.pendingWork.recordInbound();
|
|
6188
6554
|
this.recordRecentUserMessage(content, meta);
|
|
6189
|
-
const formatted =
|
|
6190
|
-
+ this.formatInboundMessage(content, meta);
|
|
6555
|
+
const formatted = BTW_INBOUND_PREFIX + this.formatInboundMessage(content, meta);
|
|
6191
6556
|
const status = channelStatus(meta);
|
|
6192
6557
|
this.steerLock = this.steerLock.then(async () => {
|
|
6193
|
-
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
6558
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
6559
|
+
this.finishHumanReceipt(meta, { reached: false });
|
|
6194
6560
|
return;
|
|
6561
|
+
}
|
|
6195
6562
|
await this.wake();
|
|
6196
|
-
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
6563
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
6564
|
+
this.finishHumanReceipt(meta, { reached: false });
|
|
6197
6565
|
return;
|
|
6198
|
-
|
|
6199
|
-
|
|
6566
|
+
}
|
|
6567
|
+
const verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
|
|
6568
|
+
try {
|
|
6569
|
+
if (await this.deliverMessage(formatted, status, { steer: true, deliveryEpoch, submissionId: meta.message_id,
|
|
6570
|
+
...(meta.verification_id ? { humanVerificationId: meta.verification_id } : {}), verdict })) {
|
|
6571
|
+
this.markTurnStarted(meta, formatted, deliveryEpoch);
|
|
6572
|
+
}
|
|
6573
|
+
}
|
|
6574
|
+
finally {
|
|
6575
|
+
this.finishHumanReceipt(meta, verdict);
|
|
6200
6576
|
}
|
|
6201
6577
|
}).catch(err => {
|
|
6578
|
+
this.finishHumanReceipt(meta, { reached: false });
|
|
6202
6579
|
this.logger.warn({ err: err.message }, "btw delivery error");
|
|
6203
6580
|
});
|
|
6204
6581
|
}
|
|
@@ -6291,11 +6668,28 @@ export class Daemon extends EventEmitter {
|
|
|
6291
6668
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
6292
6669
|
return;
|
|
6293
6670
|
}
|
|
6671
|
+
const whatsNew = this.pendingWhatsNew ? this.currentWhatsNew(this.pendingWhatsNew) : null;
|
|
6672
|
+
if (whatsNew) {
|
|
6673
|
+
// #1633: here, inside the delivery lock and ahead of a real delivery — never into a busy turn, never to an
|
|
6674
|
+
// instance nobody writes to. Recorded only once pasted: a failed or cancelled paste stays pending.
|
|
6675
|
+
const pasted = await this.deliverMessage(whatsNew.notice, undefined, { deliveryEpoch });
|
|
6676
|
+
if (pasted && this.pendingWhatsNew === whatsNew) {
|
|
6677
|
+
this.pendingWhatsNew = null;
|
|
6678
|
+
try {
|
|
6679
|
+
recordWhatsNew(this.instanceDir, whatsNew.ids, whatsNew.version);
|
|
6680
|
+
}
|
|
6681
|
+
catch (err) {
|
|
6682
|
+
this.logger.warn({ err }, "what's-new record not written — the notice may repeat once");
|
|
6683
|
+
}
|
|
6684
|
+
}
|
|
6685
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
6686
|
+
return;
|
|
6687
|
+
}
|
|
6294
6688
|
const status = channelStatus(meta);
|
|
6295
6689
|
// A fresh delivery begins a fresh turn — its bubble must not inherit
|
|
6296
6690
|
// the previous turn's tool list.
|
|
6297
6691
|
this.resetToolProgress();
|
|
6298
|
-
verdict = { reached: false };
|
|
6692
|
+
verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
|
|
6299
6693
|
const durableAttempt = this.durableDeliveryAttempt(meta, content);
|
|
6300
6694
|
if (durableAttempt === false) {
|
|
6301
6695
|
this.deferDurableDelivery(meta, "invalid durable delivery attempt metadata");
|
|
@@ -6305,6 +6699,7 @@ export class Daemon extends EventEmitter {
|
|
|
6305
6699
|
if (await this.deliverMessage(formatted, status, {
|
|
6306
6700
|
deliveryEpoch,
|
|
6307
6701
|
submissionId: meta.message_id,
|
|
6702
|
+
...(meta.verification_id ? { humanVerificationId: meta.verification_id } : {}),
|
|
6308
6703
|
verdict,
|
|
6309
6704
|
durableAttempt: durableAttempt || undefined,
|
|
6310
6705
|
})) {
|
|
@@ -6352,6 +6747,7 @@ export class Daemon extends EventEmitter {
|
|
|
6352
6747
|
this.logger.warn({ err: err.message }, "Durable pane delivery deferred after exception");
|
|
6353
6748
|
}
|
|
6354
6749
|
finally {
|
|
6750
|
+
this.finishHumanReceipt(meta, verdict);
|
|
6355
6751
|
this.pasteQueueDepth--;
|
|
6356
6752
|
}
|
|
6357
6753
|
}).catch(err => {
|
|
@@ -6388,12 +6784,7 @@ export class Daemon extends EventEmitter {
|
|
|
6388
6784
|
&& !this.isDeliveryEpochCurrent(opts.deliveryEpoch);
|
|
6389
6785
|
if (cancelled() || this.stormWindow?.isStopped())
|
|
6390
6786
|
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
|
-
}
|
|
6787
|
+
formatted = prepareDeliveryWire(formatted, opts?.preserveExactBytes);
|
|
6397
6788
|
// Direct paths such as /steer, /btw, /raw and schedules do not pass the
|
|
6398
6789
|
// FleetManager idle gate. Hold them at the final common pane-write path so
|
|
6399
6790
|
// a tmux storm cannot force any text into a half-booted CLI.
|
|
@@ -6566,7 +6957,10 @@ export class Daemon extends EventEmitter {
|
|
|
6566
6957
|
return stale();
|
|
6567
6958
|
if (residue !== "clear")
|
|
6568
6959
|
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);
|
|
6960
|
+
const written = await this.writeMessageToPane(formatted, windowId, handingOffToNativeQueue, status, opts?.submissionId, verdict, opts?.durableAttempt, opts?.steer === true, writeCurrent, opts?.humanVerificationId);
|
|
6961
|
+
// #1690: still under the lock — whatever waits on it next must already see what this write started.
|
|
6962
|
+
if (verdict.paneWriteStarted)
|
|
6963
|
+
opts?.onPaneWritten?.();
|
|
6570
6964
|
// The hand-off's last capture had no box: nothing was begun or written — wait for readiness, then try again.
|
|
6571
6965
|
if (written === "handoff-box-unread")
|
|
6572
6966
|
return "dialog";
|
|
@@ -6614,7 +7008,148 @@ export class Daemon extends EventEmitter {
|
|
|
6614
7008
|
* on purpose ("I trust", "Yes, continue") and would match ordinary prose.
|
|
6615
7009
|
*/
|
|
6616
7010
|
deliveryBlockingDialogs() {
|
|
6617
|
-
return
|
|
7011
|
+
return this.runtimeDialogs().filter(d => d.blocksDelivery || d.holdOnly);
|
|
7012
|
+
}
|
|
7013
|
+
/**
|
|
7014
|
+
* The backend's runtime dialogs, then #1691's catch-all for a prompt none of them knows (last: known ones win). The
|
|
7015
|
+
* catch-all is listed only while such a prompt is active: otherwise it could match nothing, and a backend with no
|
|
7016
|
+
* dialogs keeps its empty list (its delivery path skips the dialog probe, as before).
|
|
7017
|
+
*/
|
|
7018
|
+
runtimeDialogs() {
|
|
7019
|
+
const dialogs = this.backend?.getRuntimeDialogs?.() ?? [];
|
|
7020
|
+
return this.unrecognisedPromptCurrent()?.active ? [...dialogs, this.unrecognisedPromptDialog] : dialogs;
|
|
7021
|
+
}
|
|
7022
|
+
/**
|
|
7023
|
+
* #1691: is the pane at the CLI's own idle composer? Then whatever looks like a prompt above it is transcript output
|
|
7024
|
+
* (an agent's numbered list, a quoted "[y/n]"), not a prompt. The backend's own reading, most specific first.
|
|
7025
|
+
*/
|
|
7026
|
+
cliComposerShowing(pane, shape) {
|
|
7027
|
+
const backend = this.backend;
|
|
7028
|
+
if (!backend)
|
|
7029
|
+
return false;
|
|
7030
|
+
if (backend.readInputRow)
|
|
7031
|
+
return backend.readInputRow(pane) !== null;
|
|
7032
|
+
// muse: its live input box — null when the bottom of the screen is not its layout (#829), e.g. a selection menu.
|
|
7033
|
+
if (backend.inputDraft)
|
|
7034
|
+
return backend.inputDraft(pane) !== null;
|
|
7035
|
+
if (backend.isDeliveryInputReadyPane)
|
|
7036
|
+
return backend.isDeliveryInputReadyPane(pane);
|
|
7037
|
+
const bottom = backend.getBottomReadyPattern?.();
|
|
7038
|
+
if (bottom) {
|
|
7039
|
+
bottom.lastIndex = 0;
|
|
7040
|
+
return bottom.test(pane);
|
|
7041
|
+
}
|
|
7042
|
+
// No reader of its own (opencode, grok, …): its ready chrome BELOW the prompt rows — a status bar or prompt hint drawn
|
|
7043
|
+
// under its input box (opencode's "ctrl+p commands"), which a prompt shown in place of the input has nothing of. Not
|
|
7044
|
+
// the whole pane (a reply quoting "ctrl+p commands" higher up), and never the prompt's own rows: a ready pattern that
|
|
7045
|
+
// is just a cursor glyph would otherwise read the menu's own "❯ 1." as the CLI's prompt (#1695 r2).
|
|
7046
|
+
const rows = pane.replace(/\r/g, "").split("\n").map(r => r.trimEnd());
|
|
7047
|
+
const lastOwn = (shape.block.split("\n").at(-1) ?? "").trim();
|
|
7048
|
+
let end = -1;
|
|
7049
|
+
for (let i = rows.length - 1; i >= 0; i--)
|
|
7050
|
+
if (rows[i].trim() === lastOwn) {
|
|
7051
|
+
end = i;
|
|
7052
|
+
break;
|
|
7053
|
+
}
|
|
7054
|
+
if (end < 0)
|
|
7055
|
+
return false;
|
|
7056
|
+
const below = rows.slice(end + 1).filter(r => r.trim() !== "").slice(0, COMPOSER_TAIL_ROWS).join("\n");
|
|
7057
|
+
const ready = backend.getReadyPattern();
|
|
7058
|
+
ready.lastIndex = 0;
|
|
7059
|
+
return below !== "" && ready.test(below);
|
|
7060
|
+
}
|
|
7061
|
+
/**
|
|
7062
|
+
* #1691: the prompt shape this pane shows, when nothing else explains it (no known dialog, login, idle composer, and
|
|
7063
|
+
* the CLI is not showing its busy indicator).
|
|
7064
|
+
*/
|
|
7065
|
+
unrecognisedPromptCandidate(pane) {
|
|
7066
|
+
const shape = unrecognisedPromptShape(pane);
|
|
7067
|
+
if (!shape)
|
|
7068
|
+
return null;
|
|
7069
|
+
const busy = this.backend?.getBusyPattern?.() ?? null;
|
|
7070
|
+
if (busy) {
|
|
7071
|
+
busy.lastIndex = 0;
|
|
7072
|
+
if (busy.test(pane))
|
|
7073
|
+
return null;
|
|
7074
|
+
}
|
|
7075
|
+
if ((this.backend?.getRuntimeDialogs?.() ?? []).some(dialog => Daemon.dialogMatches(dialog, pane)))
|
|
7076
|
+
return null;
|
|
7077
|
+
if (LOGIN_FLOWS[this.config.backend ?? "claude-code"]?.loginScreenActive?.(pane))
|
|
7078
|
+
return null;
|
|
7079
|
+
if (this.cliComposerShowing(pane, shape))
|
|
7080
|
+
return null;
|
|
7081
|
+
return shape;
|
|
7082
|
+
}
|
|
7083
|
+
/**
|
|
7084
|
+
* #1691, from each runtime scan: the same unexplained prompt block unchanged for UNRECOGNISED_PROMPT_STABLE_MS
|
|
7085
|
+
* becomes active (held, awaiting_input, reported once by trackDialogParked). Any other screen ends it. A turn keeps
|
|
7086
|
+
* changing the pane (spinner, timer, streamed text), so its clock never completes mid-turn.
|
|
7087
|
+
*/
|
|
7088
|
+
trackUnrecognisedPrompt(pane, mono, outputMoved) {
|
|
7089
|
+
if (mono < this.unrecognisedPromptReadAt)
|
|
7090
|
+
return;
|
|
7091
|
+
this.unrecognisedPromptReadAt = mono;
|
|
7092
|
+
const shape = this.unrecognisedPromptCandidate(pane);
|
|
7093
|
+
if (!shape) {
|
|
7094
|
+
this.unrecognisedPrompt = null;
|
|
7095
|
+
return;
|
|
7096
|
+
}
|
|
7097
|
+
const screen = Daemon.stableScreen(pane);
|
|
7098
|
+
const owner = this.interactionOwner();
|
|
7099
|
+
const current = this.unrecognisedPromptCurrent();
|
|
7100
|
+
// Restart the minute on any change: another prompt, motion anywhere on the screen, output that arrived while this
|
|
7101
|
+
// capture was taken, or evidence from another launch (#1695 review).
|
|
7102
|
+
if (!current || current.block !== shape.block || current.screen !== screen || outputMoved) {
|
|
7103
|
+
this.unrecognisedPrompt = { block: shape.block, shape, since: mono, active: false, reportedElsewhere: false, screen, owner };
|
|
7104
|
+
return;
|
|
7105
|
+
}
|
|
7106
|
+
if (!current.active && mono - current.since >= UNRECOGNISED_PROMPT_STABLE_MS) {
|
|
7107
|
+
current.active = true;
|
|
7108
|
+
// A [y/n] the interactive-prompt detector knows was already told (interactive_prompt, after 10 s): held and
|
|
7109
|
+
// awaiting input from here on, but not reported a second time.
|
|
7110
|
+
current.reportedElsewhere = shape.kind === "yes-no" && this.interactivePromptDetector.match(pane) !== null;
|
|
7111
|
+
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)");
|
|
7112
|
+
}
|
|
7113
|
+
}
|
|
7114
|
+
/** #1691: the tracked evidence, if it still belongs to this launch (else it is dropped). */
|
|
7115
|
+
unrecognisedPromptCurrent() {
|
|
7116
|
+
const current = this.unrecognisedPrompt;
|
|
7117
|
+
if (current && (this.runtimeMonitorsFrozen || !sameInteractionOwner(current.owner, this.interactionOwner()))) {
|
|
7118
|
+
this.unrecognisedPrompt = null;
|
|
7119
|
+
return null;
|
|
7120
|
+
}
|
|
7121
|
+
return current;
|
|
7122
|
+
}
|
|
7123
|
+
/**
|
|
7124
|
+
* #1691: the catch-all's predicate — the tracked prompt is active and this pane still shows exactly it (any reader:
|
|
7125
|
+
* the scan, a probe, an interaction re-capture). Ending the evidence on a read without it is noteFreshPaneRead's.
|
|
7126
|
+
*/
|
|
7127
|
+
unrecognisedPromptActive(pane) {
|
|
7128
|
+
const current = this.unrecognisedPromptCurrent();
|
|
7129
|
+
return !!current?.active && this.unrecognisedPromptCandidate(pane)?.block === current.block && Daemon.stableScreen(pane) === current.screen;
|
|
7130
|
+
}
|
|
7131
|
+
/**
|
|
7132
|
+
* #1691: a fresh pane read outside the scan (a delivery probe, an accepted state re-capture), taken at `mono`. When it
|
|
7133
|
+
* no longer shows exactly the tracked prompt, the evidence ends — counting or active, so a return needs a fresh minute
|
|
7134
|
+
* (#1695 r2). When it does, it is the newest confirmation. Either way only a read newer than the newest read that
|
|
7135
|
+
* decided the evidence counts: an older one, finishing late, says nothing about now (#1695 r3, r4).
|
|
7136
|
+
*/
|
|
7137
|
+
noteFreshPaneRead(pane, mono) {
|
|
7138
|
+
if (mono < this.unrecognisedPromptReadAt)
|
|
7139
|
+
return;
|
|
7140
|
+
// Advanced even with nothing tracked: a scan whose capture is older must not start evidence this read contradicts
|
|
7141
|
+
// (#1695 r5).
|
|
7142
|
+
this.unrecognisedPromptReadAt = mono;
|
|
7143
|
+
const current = this.unrecognisedPromptCurrent();
|
|
7144
|
+
if (!current)
|
|
7145
|
+
return;
|
|
7146
|
+
if (this.unrecognisedPromptCandidate(pane)?.block === current.block && Daemon.stableScreen(pane) === current.screen)
|
|
7147
|
+
return;
|
|
7148
|
+
this.unrecognisedPrompt = null;
|
|
7149
|
+
}
|
|
7150
|
+
/** #1691: the screen as the stability clock compares it (trailing blanks are not motion). */
|
|
7151
|
+
static stableScreen(pane) {
|
|
7152
|
+
return pane.replace(/\r/g, "").split("\n").map(r => r.trimEnd()).join("\n").trimEnd();
|
|
6618
7153
|
}
|
|
6619
7154
|
/** Passive input-unavailable phases relevant to the current spawn only. */
|
|
6620
7155
|
guardedInputTransients() {
|
|
@@ -6782,7 +7317,7 @@ export class Daemon extends EventEmitter {
|
|
|
6782
7317
|
return dialog.isActive ? dialog.isActive(pane) : dialog.pattern.test(pane);
|
|
6783
7318
|
}
|
|
6784
7319
|
/** Refresh the separate stdin-blocked observation from one pane capture. */
|
|
6785
|
-
updateInputBlockedState(pane, dialogs = this.
|
|
7320
|
+
updateInputBlockedState(pane, dialogs = this.runtimeDialogs()) {
|
|
6786
7321
|
const active = dialogs.find(dialog => dialog.inputBlocked && Daemon.dialogMatches(dialog, pane)) ?? null;
|
|
6787
7322
|
const nextKey = active ? Daemon.dialogKey(active) : null;
|
|
6788
7323
|
if (nextKey !== this.inputBlockedDialogKey) {
|
|
@@ -6870,9 +7405,15 @@ export class Daemon extends EventEmitter {
|
|
|
6870
7405
|
async probeBlockingDialog() {
|
|
6871
7406
|
if (!this.tmux || !this.backend)
|
|
6872
7407
|
return { state: "clear" };
|
|
7408
|
+
// #1691: a probe is a fresh read — it may end unrecognised-prompt evidence even when no dialog is listed.
|
|
7409
|
+
const tracking = this.unrecognisedPrompt !== null;
|
|
6873
7410
|
const dialogs = this.deliveryBlockingDialogs();
|
|
6874
|
-
|
|
7411
|
+
// #1690: our own switch is being answered — nothing else types into the pane until it has settled.
|
|
7412
|
+
if (this.ownSwitch && dialogs.length > 0)
|
|
7413
|
+
return { state: "dialog", dialog: dialogs[0] };
|
|
7414
|
+
if (dialogs.length === 0 && !tracking)
|
|
6875
7415
|
return { state: "clear" };
|
|
7416
|
+
const probeMono = performance.now();
|
|
6876
7417
|
let pane;
|
|
6877
7418
|
try {
|
|
6878
7419
|
pane = await this.tmux.capturePane();
|
|
@@ -6881,6 +7422,7 @@ export class Daemon extends EventEmitter {
|
|
|
6881
7422
|
this.logger.debug({ err }, "capture-pane failed during the dialog probe — pane state unknown");
|
|
6882
7423
|
return { state: "unknown" };
|
|
6883
7424
|
}
|
|
7425
|
+
this.noteFreshPaneRead(pane, probeMono);
|
|
6884
7426
|
for (const dialog of dialogs) {
|
|
6885
7427
|
if (!Daemon.dialogMatches(dialog, pane))
|
|
6886
7428
|
continue;
|
|
@@ -7090,11 +7632,16 @@ export class Daemon extends EventEmitter {
|
|
|
7090
7632
|
this.dialogParkedReported = false;
|
|
7091
7633
|
return;
|
|
7092
7634
|
}
|
|
7093
|
-
|
|
7635
|
+
// #1691: the catch-all waited its minute before it became active; it is reported on the next scan.
|
|
7636
|
+
const unrecognised = dialog === this.unrecognisedPromptDialog;
|
|
7637
|
+
if (unrecognised && this.unrecognisedPrompt?.reportedElsewhere)
|
|
7638
|
+
this.dialogParkedReported = true;
|
|
7639
|
+
if (!this.dialogParkedReported && Date.now() - this.dialogParkedSince >= (unrecognised ? 0 : DIALOG_PARKED_NOTIFY_MS)) {
|
|
7094
7640
|
this.dialogParkedReported = true;
|
|
7095
7641
|
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
7642
|
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" } : {})
|
|
7643
|
+
...(episode !== null && interaction.kind ? { kind: interaction.kind, episode, backend: this.config.backend ?? "claude-code" } : {}),
|
|
7644
|
+
...(unrecognised ? { unrecognised: true, question: this.unrecognisedPrompt?.shape.question ?? null } : {}) });
|
|
7098
7645
|
}
|
|
7099
7646
|
}
|
|
7100
7647
|
/**
|
|
@@ -7205,7 +7752,7 @@ export class Daemon extends EventEmitter {
|
|
|
7205
7752
|
return false;
|
|
7206
7753
|
const row = norm(shown);
|
|
7207
7754
|
// The marker line holds nothing but the marker: the row is exactly it.
|
|
7208
|
-
if (/^\[agend-delivery-id:[^\]\s]+\]$/.test(first))
|
|
7755
|
+
if (/^\[agend-(?:delivery|human)-id:[^\]\s]+\]$/.test(first))
|
|
7209
7756
|
return row === first;
|
|
7210
7757
|
return row === first || row.startsWith(`${first} `);
|
|
7211
7758
|
}
|
|
@@ -7663,6 +8210,33 @@ export class Daemon extends EventEmitter {
|
|
|
7663
8210
|
this.logger.warn("Could not read the pane after Enter — not confirming the submission");
|
|
7664
8211
|
return sawOutput && residue === "absent";
|
|
7665
8212
|
}
|
|
8213
|
+
/**
|
|
8214
|
+
* #1647: whether a raw paste on a pane with no transcript (kiro) was taken: its text gone from the input row (or the
|
|
8215
|
+
* CLI's own busy marker shown instead — pasteResidue's rule) with output after the Enter. That is the confirmation's
|
|
8216
|
+
* own rule, looked at more than once: kiro repaints its composer late, so the single look right after Enter misread
|
|
8217
|
+
* pastes it took. A generic `working` reading is not evidence (#1663 P1-3: a repaint looks like work).
|
|
8218
|
+
*
|
|
8219
|
+
* Bounded by the budget that confirmation already has, measured from the Enter (#1663 P2-1): the busy wait plus the
|
|
8220
|
+
* post-Enter proof window. Every reading is asked again after its await — still this write's owner (`current`: the
|
|
8221
|
+
* caller's fence, no stop, the same tmux and spawn, #1663 P2-2) and still inside the budget — before it counts; a late
|
|
8222
|
+
* or fenced answer proves nothing. Null: not shown — the caller settles uncertain, never a second Enter.
|
|
8223
|
+
*/
|
|
8224
|
+
async rawPasteTakenOnPane(windowId, enterAt, enterMono, formatted, current) {
|
|
8225
|
+
const deadline = enterMono + CONFIRM_BUSY_POLLS * CONFIRM_BUSY_POLL_MS + POST_ENTER_PROOF_WINDOW_MS;
|
|
8226
|
+
const live = () => current() && performance.now() <= deadline;
|
|
8227
|
+
for (;;) {
|
|
8228
|
+
if (!live())
|
|
8229
|
+
return null;
|
|
8230
|
+
if (this.controlClient?.hasOutputSince?.(windowId, enterAt)) {
|
|
8231
|
+
const residue = await this.pasteResidue(formatted);
|
|
8232
|
+
if (!live())
|
|
8233
|
+
return null;
|
|
8234
|
+
if (residue === "absent")
|
|
8235
|
+
return RAW_PASTE_LEFT_INPUT;
|
|
8236
|
+
}
|
|
8237
|
+
await new Promise(r => setTimeout(r, POST_ENTER_PROOF_POLL_MS));
|
|
8238
|
+
}
|
|
8239
|
+
}
|
|
7666
8240
|
/** Once Kiro returns to its prompt, its unique envelope id in history proves submission. */
|
|
7667
8241
|
async kiroSubmissionEvidence(windowId, formatted, signature) {
|
|
7668
8242
|
if (!this.tmux)
|
|
@@ -7720,6 +8294,8 @@ export class Daemon extends EventEmitter {
|
|
|
7720
8294
|
if (!current())
|
|
7721
8295
|
return false;
|
|
7722
8296
|
if (evidence === "submitted") {
|
|
8297
|
+
if (pending.signature.unique)
|
|
8298
|
+
pending.signature.humanPositive?.("unique-id-pane");
|
|
7723
8299
|
if (status)
|
|
7724
8300
|
this.emit("message_confirmed", status);
|
|
7725
8301
|
return true;
|
|
@@ -7755,6 +8331,8 @@ export class Daemon extends EventEmitter {
|
|
|
7755
8331
|
if (retry === "cancelled")
|
|
7756
8332
|
return false;
|
|
7757
8333
|
if (retry === "submitted") {
|
|
8334
|
+
if (pending.signature.unique)
|
|
8335
|
+
pending.signature.humanPositive?.("unique-id-pane");
|
|
7758
8336
|
if (status)
|
|
7759
8337
|
this.emit("message_confirmed", status);
|
|
7760
8338
|
return true;
|
|
@@ -7895,9 +8473,24 @@ export class Daemon extends EventEmitter {
|
|
|
7895
8473
|
verdict = { reached: false }, durableAttempt, steer = false,
|
|
7896
8474
|
// #829: the caller's fence, asked once more after the last await before the
|
|
7897
8475
|
// first side effect. Stale means unwritten: false, with no failure verdict.
|
|
7898
|
-
stillCurrent) {
|
|
7899
|
-
const signature = this.submissionSignature(formatted, submissionId);
|
|
8476
|
+
stillCurrent, humanVerificationId) {
|
|
8477
|
+
const signature = this.submissionSignature(formatted, humanVerificationId ?? submissionId, humanVerificationId !== undefined);
|
|
7900
8478
|
const rawPaste = durableAttempt?.submissionMode === "raw_paste";
|
|
8479
|
+
const humanAttempt = verdict.humanReceipt;
|
|
8480
|
+
const humanMode = rawPaste ? "raw_paste" : steer ? "steer"
|
|
8481
|
+
: handingOffToNativeQueue ? "native_queue_handoff" : "idle_submit";
|
|
8482
|
+
if (humanAttempt && signature.unique && signature.value === "agend-human-id:" + humanAttempt.verificationId
|
|
8483
|
+
&& humanBodyHash(formatted) === humanAttempt.expectedWireHash) {
|
|
8484
|
+
signature.humanPositive = tag => {
|
|
8485
|
+
if (!humanAttempt.current() || (stillCurrent && !stillCurrent()))
|
|
8486
|
+
return;
|
|
8487
|
+
humanAttempt.proof = {
|
|
8488
|
+
receipt_id: humanAttempt.receiptId, verification_id: humanAttempt.verificationId,
|
|
8489
|
+
wire_sha256: humanBodyHash(formatted), owner_key: humanAttempt.ownerKey,
|
|
8490
|
+
mode: humanMode, tag,
|
|
8491
|
+
};
|
|
8492
|
+
};
|
|
8493
|
+
}
|
|
7901
8494
|
let windowId = initialWindowId;
|
|
7902
8495
|
// The spawn this write belongs to: acceptance evidence is only read while it is still the current one (#1197).
|
|
7903
8496
|
const spawnAtWrite = this.spawnGeneration;
|
|
@@ -7975,9 +8568,11 @@ export class Daemon extends EventEmitter {
|
|
|
7975
8568
|
offset: attemptEvidence.transcriptOffset,
|
|
7976
8569
|
};
|
|
7977
8570
|
// 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
|
-
|
|
8571
|
+
// reply, or still be painting the paste) — this delivery's exact marker only, or a raw paste's exact text (#1647).
|
|
8572
|
+
// A raw paste's text only where the transcript is this instance's alone (RAW_TEXT_TRANSCRIPT_BACKENDS).
|
|
8573
|
+
if (["claude-code", "codex"].includes(attemptEvidence.backend)
|
|
8574
|
+
&& (!rawPaste || RAW_TEXT_TRANSCRIPT_BACKENDS.includes(attemptEvidence.backend))) {
|
|
8575
|
+
signature.transcript = this.transcriptProofFor(verdict.transcriptCheckpoint, rawPaste ? { rawText: formatted } : durableAttempt.deliveryId, spawnAtWrite, stillCurrent, kind => { verdict.transcriptProof = kind; });
|
|
7981
8576
|
}
|
|
7982
8577
|
}
|
|
7983
8578
|
}
|
|
@@ -8041,6 +8636,7 @@ export class Daemon extends EventEmitter {
|
|
|
8041
8636
|
// positively hold typed text, and after it be the empty placeholder again. The empty placeholder here means the
|
|
8042
8637
|
// paste never reached the box; an unreadable box (a failed or late capture, a dialog) proves nothing: no Enter.
|
|
8043
8638
|
const steerComposerProof = handingOffToNativeQueue && steer && !!this.backend?.readSteerComposer && tmuxAtWrite !== null;
|
|
8639
|
+
let humanComposerOwned = false;
|
|
8044
8640
|
let steerEnterDeadline = Infinity;
|
|
8045
8641
|
if (steerComposerProof) {
|
|
8046
8642
|
const deadline = performance.now() + STEER_PASTE_VISIBLE_MS;
|
|
@@ -8064,10 +8660,13 @@ export class Daemon extends EventEmitter {
|
|
|
8064
8660
|
return this.failDelivery(verdict, status, "steer-paste", before === null ? "box-unread" : `box-still-${before.mode}`);
|
|
8065
8661
|
if (!this.steerPasteIsOurs(before.pane, formatted))
|
|
8066
8662
|
return this.failDelivery(verdict, status, "steer-paste", "box-not-this-paste");
|
|
8663
|
+
const shown = this.backend?.readSteerComposerText?.(before.pane)?.replace(/\s+/g, " ").trim();
|
|
8664
|
+
humanComposerOwned = !!humanAttempt && shown === "[agend-human-id:" + humanAttempt.verificationId + "]";
|
|
8067
8665
|
// …and so is the Enter: the key is sent only while the reading is still within it.
|
|
8068
8666
|
steerEnterDeadline = deadline;
|
|
8069
8667
|
}
|
|
8070
8668
|
let enterAt = Date.now();
|
|
8669
|
+
const enterMono = performance.now();
|
|
8071
8670
|
const steerEnterCurrent = () => steerCurrent() && performance.now() <= steerEnterDeadline;
|
|
8072
8671
|
if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof ? steerEnterCurrent : undefined, durableAttempt))) {
|
|
8073
8672
|
if (steerComposerProof && !steerCurrent()) {
|
|
@@ -8134,6 +8733,9 @@ export class Daemon extends EventEmitter {
|
|
|
8134
8733
|
if (after === "steer" || after === "queue" || after === "idle") {
|
|
8135
8734
|
const probe = await withinMs(this.probeBlockingDialog(), deadline - performance.now());
|
|
8136
8735
|
if (probe?.state === "clear" && performance.now() <= deadline && steerCurrent() && this.getWindowId() === windowId) {
|
|
8736
|
+
if (humanComposerOwned)
|
|
8737
|
+
signature.humanPositive?.(after === "steer" ? "composer-empty-steer"
|
|
8738
|
+
: after === "queue" ? "composer-empty-queue" : "composer-empty-idle");
|
|
8137
8739
|
verdict.phase = "steer-accepted";
|
|
8138
8740
|
verdict.proof = `steer-composer-${after}`;
|
|
8139
8741
|
if (status)
|
|
@@ -8313,12 +8915,20 @@ export class Daemon extends EventEmitter {
|
|
|
8313
8915
|
return { kind: "kiro-pending", windowId, spawnGeneration: this.spawnGeneration, signature };
|
|
8314
8916
|
}
|
|
8315
8917
|
}
|
|
8316
|
-
if (!submitted) {
|
|
8317
|
-
|
|
8918
|
+
if (!submitted && rawPaste) {
|
|
8919
|
+
// #1647: kiro paints output at once but its composer late, so one residue look right after Enter misreads a
|
|
8920
|
+
// paste it took. Keep looking within the confirmation's own budget from the Enter, under this write's fence;
|
|
8921
|
+
// never a second Enter (a raw command must not run twice).
|
|
8922
|
+
const taken = await this.rawPasteTakenOnPane(windowId, enterAt, enterMono, formatted, steerCurrent);
|
|
8923
|
+
if (!taken) {
|
|
8318
8924
|
verdict.phase = "raw-paste-submit-proof";
|
|
8319
8925
|
verdict.proof = "no-positive-acceptance-proof";
|
|
8320
8926
|
return false;
|
|
8321
8927
|
}
|
|
8928
|
+
verdict.proof = taken;
|
|
8929
|
+
submitted = true;
|
|
8930
|
+
}
|
|
8931
|
+
if (!submitted) {
|
|
8322
8932
|
// Legacy system pastes have no unique envelope id. Preserve their
|
|
8323
8933
|
// existing bounded retry until they can be tied to a specific turn.
|
|
8324
8934
|
this.logger.warn("Message not submitted after Enter — waiting for the prompt, then re-sending Enter once");
|
|
@@ -8631,10 +9241,10 @@ export class Daemon extends EventEmitter {
|
|
|
8631
9241
|
* A write's transcript proof: bounded looks from its checkpoint, fenced to the write — its spawn and launch, a stop,
|
|
8632
9242
|
* and the caller's own fence — so a look that lands after any of them is never used (#1380 review).
|
|
8633
9243
|
*/
|
|
8634
|
-
transcriptProofFor(checkpoint,
|
|
9244
|
+
transcriptProofFor(checkpoint, needle, spawnAtWrite, stillCurrent, onProof) {
|
|
8635
9245
|
const launchAtWrite = this.launchFenceEpoch;
|
|
8636
9246
|
return {
|
|
8637
|
-
reader: new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend,
|
|
9247
|
+
reader: new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend, needle),
|
|
8638
9248
|
current: () => !this.deliveryWritesStopping && this.spawnGeneration === spawnAtWrite
|
|
8639
9249
|
&& this.launchFenceEpoch === launchAtWrite && (stillCurrent?.() ?? true),
|
|
8640
9250
|
onProof,
|
|
@@ -8712,8 +9322,11 @@ export class Daemon extends EventEmitter {
|
|
|
8712
9322
|
// the input row is proof in itself — which also means a momentary
|
|
8713
9323
|
// failure to read the pane BEFORE pasting cannot turn a delivered
|
|
8714
9324
|
// message into a re-paste.
|
|
8715
|
-
if (signature.unique && after.payload > 0)
|
|
9325
|
+
if (signature.unique && after.payload > 0) {
|
|
9326
|
+
if (after.inputReadable)
|
|
9327
|
+
signature.humanPositive?.("unique-id-pane");
|
|
8716
9328
|
return this.submittedProof();
|
|
9329
|
+
}
|
|
8717
9330
|
if (unattributedResidue)
|
|
8718
9331
|
return "unverifiable";
|
|
8719
9332
|
// 3. Otherwise the evidence must be NEW relative to the pane as it was
|
|
@@ -8850,7 +9463,7 @@ export class Daemon extends EventEmitter {
|
|
|
8850
9463
|
* paste that never landed. System pastes carry no envelope and fall back to
|
|
8851
9464
|
* the body signature, which the baseline comparison still ties to this paste.
|
|
8852
9465
|
*/
|
|
8853
|
-
submissionSignature(formatted, trustedId) {
|
|
9466
|
+
submissionSignature(formatted, trustedId, verification = false) {
|
|
8854
9467
|
// The id must come from the message's own metadata, never from scanning the
|
|
8855
9468
|
// rendered text: agents and users discuss message ids in the body all the
|
|
8856
9469
|
// time ("check message_id: abc"), and a body-scanned value would match an
|
|
@@ -8859,7 +9472,7 @@ export class Daemon extends EventEmitter {
|
|
|
8859
9472
|
// the handoff metadata identifies THIS delivery.
|
|
8860
9473
|
const id = trustedId?.trim();
|
|
8861
9474
|
if (id)
|
|
8862
|
-
return { value:
|
|
9475
|
+
return { value: (verification ? "agend-human-id:" : "message_id:") + id, unique: true };
|
|
8863
9476
|
return { value: pastedTextSignature(formatted), unique: false };
|
|
8864
9477
|
}
|
|
8865
9478
|
/**
|
|
@@ -9966,7 +10579,7 @@ export class Daemon extends EventEmitter {
|
|
|
9966
10579
|
* the same tmux window, not aborted, writes not stopping — re-checked after every await, and between the paste and
|
|
9967
10580
|
* its Enter. The whole step, every await included, ends at one monotonic deadline: a late capture never confirms
|
|
9968
10581
|
* 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
|
|
10582
|
+
* 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
10583
|
* silently, confirming and removing nothing. Never throws into the spawn.
|
|
9971
10584
|
*/
|
|
9972
10585
|
async ensureBackendAgent() {
|
|
@@ -10012,8 +10625,10 @@ export class Daemon extends EventEmitter {
|
|
|
10012
10625
|
if (active === sw.agent && (wroteAt === null || capturedAt > wroteAt)) {
|
|
10013
10626
|
if (sw.alreadyConfirmed && wroteAt === null)
|
|
10014
10627
|
return; // already ours, already on record
|
|
10015
|
-
for (const
|
|
10016
|
-
this.
|
|
10628
|
+
for (const { message, quiet } of sw.confirm()) {
|
|
10629
|
+
this.logger.warn({ agent: sw.agent, warning: message }, "The agent switch was confirmed with a warning");
|
|
10630
|
+
this.emit("backend_launch_warning", { name: this.name, message, quiet: quiet === true });
|
|
10631
|
+
}
|
|
10017
10632
|
this.logger.info({ agent: sw.agent, switched: wroteAt !== null }, "The resumed conversation runs as this instance's agent");
|
|
10018
10633
|
return;
|
|
10019
10634
|
}
|
|
@@ -10062,7 +10677,9 @@ export class Daemon extends EventEmitter {
|
|
|
10062
10677
|
if (!fenced())
|
|
10063
10678
|
return; // a stop, pause or respawn: nothing to say, nothing to do
|
|
10064
10679
|
this.logger.warn({ agent: sw.agent, typed: wroteAt !== null }, "The resumed conversation did not switch to this instance's agent in time");
|
|
10065
|
-
|
|
10680
|
+
// #1794: quiet — the conversation keeps running as its saved agent with the shared entries, and the next launch
|
|
10681
|
+
// tries again; the user loses nothing and has nothing to do. Logged above and kept on the Log tab only.
|
|
10682
|
+
this.emit("backend_launch_warning", { name: this.name, message: t("kiro.switch_timeout", Daemon.AGENT_SWITCH_BUDGET_MS / 1000), quiet: true });
|
|
10066
10683
|
}
|
|
10067
10684
|
catch (err) {
|
|
10068
10685
|
this.startupAdmission?.();
|
|
@@ -10426,6 +11043,49 @@ export class Daemon extends EventEmitter {
|
|
|
10426
11043
|
}
|
|
10427
11044
|
catch { /* non-fatal */ }
|
|
10428
11045
|
}
|
|
11046
|
+
/**
|
|
11047
|
+
* #1633 (#1639 review): the pending notice as the record stands NOW, just before the paste — it was decided at launch,
|
|
11048
|
+
* and the record may have changed while the delivery waited. Unreadable → nothing is sent (and nothing pending);
|
|
11049
|
+
* entries recorded meanwhile are dropped from it; none left → nothing.
|
|
11050
|
+
*/
|
|
11051
|
+
currentWhatsNew(pending) {
|
|
11052
|
+
const record = readWhatsNewRecord(this.instanceDir);
|
|
11053
|
+
if (record === "unreadable") {
|
|
11054
|
+
this.pendingWhatsNew = null;
|
|
11055
|
+
return null;
|
|
11056
|
+
}
|
|
11057
|
+
const known = new Set(record?.announced ?? []);
|
|
11058
|
+
const ids = pending.ids.filter(id => !known.has(id));
|
|
11059
|
+
if (!ids.length) {
|
|
11060
|
+
this.pendingWhatsNew = null;
|
|
11061
|
+
return null;
|
|
11062
|
+
}
|
|
11063
|
+
if (ids.length === pending.ids.length)
|
|
11064
|
+
return pending;
|
|
11065
|
+
const narrowed = { notice: whatsNewNotice(pending.version, AGENT_WHATS_NEW.filter(e => ids.includes(e.id))), ids, version: pending.version };
|
|
11066
|
+
this.pendingWhatsNew = narrowed;
|
|
11067
|
+
return narrowed;
|
|
11068
|
+
}
|
|
11069
|
+
/** #1633: what this instance has not been told about its AgEnD version yet (see agent-whats-new.ts). */
|
|
11070
|
+
prepareWhatsNew(fresh, version = agendVersion()) {
|
|
11071
|
+
try {
|
|
11072
|
+
const plan = planWhatsNew({ record: readWhatsNewRecord(this.instanceDir), fresh, version });
|
|
11073
|
+
if (!plan) {
|
|
11074
|
+
this.pendingWhatsNew = null;
|
|
11075
|
+
return;
|
|
11076
|
+
}
|
|
11077
|
+
if ("silent" in plan) {
|
|
11078
|
+
recordWhatsNew(this.instanceDir, plan.silent, plan.version);
|
|
11079
|
+
this.pendingWhatsNew = null;
|
|
11080
|
+
return;
|
|
11081
|
+
}
|
|
11082
|
+
this.pendingWhatsNew = plan;
|
|
11083
|
+
this.logger.info({ entries: plan.ids }, "what's-new notice pending for the next delivery");
|
|
11084
|
+
}
|
|
11085
|
+
catch (err) {
|
|
11086
|
+
this.logger.debug({ err }, "what's-new not prepared");
|
|
11087
|
+
}
|
|
11088
|
+
}
|
|
10429
11089
|
async trySpawnInsideGate(reuseWindow = false, startupTimeoutMs) {
|
|
10430
11090
|
this.startupAdmission?.();
|
|
10431
11091
|
const launchFence = this.launchFenceEpoch;
|
|
@@ -10435,6 +11095,8 @@ export class Daemon extends EventEmitter {
|
|
|
10435
11095
|
// instructionsReloadedOnResume) so the warmup steering-reload can be
|
|
10436
11096
|
// skipped when nothing changed.
|
|
10437
11097
|
this.lastBuiltInstructions = backendConfig.instructions ?? "";
|
|
11098
|
+
// #1633: decided before prev-instructions is first written — its absence is what says "never launched before".
|
|
11099
|
+
this.prepareWhatsNew(!existsSync(join(this.instanceDir, "prev-instructions")));
|
|
10438
11100
|
{
|
|
10439
11101
|
const prevFile = join(this.instanceDir, "prev-instructions");
|
|
10440
11102
|
let prev = "";
|
|
@@ -10476,7 +11138,7 @@ export class Daemon extends EventEmitter {
|
|
|
10476
11138
|
if (launchFence !== this.launchFenceEpoch)
|
|
10477
11139
|
throw new Error("Launch cancelled: the instance was stopped or paused while the launch was being prepared");
|
|
10478
11140
|
this.backend.writeConfig(backendConfig);
|
|
10479
|
-
this.backend.preTrust?.(this.config.working_directory);
|
|
11141
|
+
this.backend.preTrust?.(this.config.working_directory, backendConfig);
|
|
10480
11142
|
// Resolve working directory (e.g. symlink for hidden paths)
|
|
10481
11143
|
const resolvedCwd = this.backend.resolveWorkingDirectory?.(this.config.working_directory, this.name) ?? this.config.working_directory;
|
|
10482
11144
|
// Generate a fresh per-instance agent token each spawn. agent-cli reads
|
|
@@ -10546,6 +11208,11 @@ export class Daemon extends EventEmitter {
|
|
|
10546
11208
|
this.logger.warn({ warning: launchWarning }, "Backend launch fell back");
|
|
10547
11209
|
this.emit("backend_launch_warning", { name: this.name, message: launchWarning });
|
|
10548
11210
|
}
|
|
11211
|
+
// #1811: what the launch left out and why (an effort this CLI or model does not take) — the Log tab, not the chat.
|
|
11212
|
+
for (const note of this.backend.consumeQuietLaunchNotes?.() ?? []) {
|
|
11213
|
+
this.logger.info({ note }, "Launch note");
|
|
11214
|
+
this.emit("backend_launch_warning", { name: this.name, message: note, quiet: true });
|
|
11215
|
+
}
|
|
10549
11216
|
// Ensure tmux session exists (may have been destroyed if all windows died)
|
|
10550
11217
|
await TmuxManager.ensureSession(this.tmuxSessionName);
|
|
10551
11218
|
if (this.stormWindow?.observeServerAlive(await TmuxManager.getServerPid(this.tmuxSessionName))) {
|
|
@@ -11116,6 +11783,14 @@ export class Daemon extends EventEmitter {
|
|
|
11116
11783
|
}, "Snapshot written");
|
|
11117
11784
|
return snapshot;
|
|
11118
11785
|
}
|
|
11786
|
+
/** #1699: the claude config dir this instance's CLI uses — its credential profile's, else exactly as before. */
|
|
11787
|
+
claudeConfigDir() {
|
|
11788
|
+
return claudeConfigDirFor(claudeProfileConfigDir(this.config.backend_options?.["claude-code"]));
|
|
11789
|
+
}
|
|
11790
|
+
/** Where this instance's CLI keeps the conversation a turn resume is judged on (#1699: claude's follows the profile). */
|
|
11791
|
+
resumeRoots(workingDirectory) {
|
|
11792
|
+
return { instanceDir: this.instanceDir, workingDirectory, claudeConfigDir: this.claudeConfigDir() };
|
|
11793
|
+
}
|
|
11119
11794
|
/**
|
|
11120
11795
|
* The credential profile's store this instance's CLI writes to, if any.
|
|
11121
11796
|
*
|