@songsid/agend 2.2.0-beta.2 → 2.2.0-beta.20
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 +40 -24
- package/README.zh-TW.md +40 -24
- package/dist/agent-cli-instructions.md +6 -2
- package/dist/agent-cli.js +15 -7
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +3 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.js +16 -4
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.d.ts +3 -1
- package/dist/auth-api.js +17 -1
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/antigravity.d.ts +33 -0
- package/dist/backend/antigravity.js +126 -18
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-carry.d.ts +22 -0
- package/dist/backend/claude-carry.js +170 -0
- package/dist/backend/claude-carry.js.map +1 -0
- package/dist/backend/claude-code.d.ts +65 -2
- package/dist/backend/claude-code.js +226 -33
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-metadata.d.ts +33 -0
- package/dist/backend/codex-metadata.js +122 -18
- package/dist/backend/codex-metadata.js.map +1 -1
- package/dist/backend/codex.d.ts +3 -0
- package/dist/backend/codex.js +17 -1
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/credential-profile.d.ts +40 -3
- package/dist/backend/credential-profile.js +113 -5
- package/dist/backend/credential-profile.js.map +1 -1
- package/dist/backend/effort-metadata.d.ts +37 -1
- package/dist/backend/effort-metadata.js +86 -15
- package/dist/backend/effort-metadata.js.map +1 -1
- package/dist/backend/grok.d.ts +4 -0
- package/dist/backend/grok.js +14 -0
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +27 -1
- package/dist/backend/kiro.js +63 -10
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/launch-effort.d.ts +61 -0
- package/dist/backend/launch-effort.js +170 -0
- package/dist/backend/launch-effort.js.map +1 -0
- package/dist/backend/muse.d.ts +84 -0
- package/dist/backend/muse.js +449 -53
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +48 -2
- package/dist/backend/types.js +12 -1
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-command.d.ts +91 -0
- package/dist/backend-command.js +122 -0
- package/dist/backend-command.js.map +1 -0
- package/dist/background-work-ended.d.ts +18 -0
- package/dist/background-work-ended.js +30 -0
- package/dist/background-work-ended.js.map +1 -0
- package/dist/cache-service.d.ts +2 -0
- package/dist/cache-service.js +2 -1
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +26 -1
- package/dist/channel/adapters/discord.js +103 -3
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +35 -3
- package/dist/channel/adapters/telegram.js +122 -23
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +5 -3
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/message-edit-errors.d.ts +5 -0
- package/dist/channel/message-edit-errors.js +24 -0
- package/dist/channel/message-edit-errors.js.map +1 -0
- package/dist/channel/telegram-markdown.d.ts +6 -0
- package/dist/channel/telegram-markdown.js +104 -0
- package/dist/channel/telegram-markdown.js.map +1 -0
- package/dist/channel/tool-router.d.ts +2 -0
- package/dist/channel/tool-router.js +6 -5
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +46 -4
- package/dist/cli.js +184 -34
- package/dist/cli.js.map +1 -1
- package/dist/command-table.d.ts +2 -2
- package/dist/command-table.js +5 -2
- package/dist/command-table.js.map +1 -1
- package/dist/compact-watch.d.ts +86 -0
- package/dist/compact-watch.js +288 -0
- package/dist/compact-watch.js.map +1 -0
- package/dist/config-validator.js +55 -5
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +2 -1
- package/dist/config.js.map +1 -1
- package/dist/cross-instance-envelope.js +2 -0
- package/dist/cross-instance-envelope.js.map +1 -1
- package/dist/daemon.d.ts +129 -1
- package/dist/daemon.js +754 -94
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +64 -8
- package/dist/delivery-outbox.js +315 -24
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +18 -1
- package/dist/delivery-reconciliation.js +67 -2
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/doctor.js +7 -0
- package/dist/doctor.js.map +1 -1
- package/dist/fleet-context.d.ts +12 -1
- package/dist/fleet-manager.d.ts +392 -19
- package/dist/fleet-manager.js +2030 -275
- package/dist/fleet-manager.js.map +1 -1
- package/dist/forged-envelope-scan.d.ts +4 -3
- package/dist/forged-envelope-scan.js +4 -3
- package/dist/forged-envelope-scan.js.map +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +84 -16
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +16 -1
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +4 -1
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +4 -0
- 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/install-lock.d.ts +18 -1
- package/dist/install-lock.js +30 -3
- package/dist/install-lock.js.map +1 -1
- package/dist/instance-avatar.d.ts +5 -4
- package/dist/instance-avatar.js +12 -8
- package/dist/instance-avatar.js.map +1 -1
- package/dist/instance-config-impact.js +1 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +25 -6
- package/dist/instance-lifecycle.js +76 -27
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.d.ts +1 -1
- package/dist/instructions.js +3 -3
- package/dist/instructions.js.map +1 -1
- package/dist/kiro-compact-pane.d.ts +38 -0
- package/dist/kiro-compact-pane.js +57 -0
- package/dist/kiro-compact-pane.js.map +1 -0
- 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/launchd-handoff.d.ts +43 -0
- package/dist/launchd-handoff.js +56 -0
- package/dist/launchd-handoff.js.map +1 -0
- package/dist/locale.js +268 -42
- 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/login-shell.d.ts +21 -0
- package/dist/login-shell.js +98 -0
- package/dist/login-shell.js.map +1 -0
- 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/outbound-handlers.d.ts +100 -0
- package/dist/outbound-handlers.js +277 -93
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +30 -15
- package/dist/outbound-schemas.js +23 -5
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/provider-probe.d.ts +29 -0
- package/dist/provider-probe.js +198 -2
- package/dist/provider-probe.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.d.ts +2 -0
- package/dist/public-web-gateway.js +20 -6
- 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/quickstart-api.d.ts +1 -1
- package/dist/quickstart-api.js +8 -6
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +12 -0
- package/dist/quickstart.js +36 -6
- package/dist/quickstart.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +89 -5
- package/dist/reply-buttons-controller.js +273 -8
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/reply-buttons.d.ts +48 -3
- package/dist/reply-buttons.js +105 -10
- package/dist/reply-buttons.js.map +1 -1
- package/dist/reply-polls.d.ts +85 -0
- package/dist/reply-polls.js +154 -0
- package/dist/reply-polls.js.map +1 -0
- package/dist/restart-guard.d.ts +6 -3
- package/dist/restart-guard.js +6 -3
- package/dist/restart-guard.js.map +1 -1
- package/dist/restart-progress.d.ts +33 -2
- package/dist/restart-progress.js +30 -2
- package/dist/restart-progress.js.map +1 -1
- package/dist/sd-notify.d.ts +22 -1
- package/dist/sd-notify.js +63 -10
- package/dist/sd-notify.js.map +1 -1
- package/dist/service-activation.d.ts +15 -7
- package/dist/service-activation.js +42 -26
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.d.ts +55 -0
- package/dist/service-installer.js +90 -3
- package/dist/service-installer.js.map +1 -1
- package/dist/service-plan.d.ts +11 -0
- package/dist/service-plan.js +23 -0
- package/dist/service-plan.js.map +1 -1
- package/dist/settings-api.js +14 -4
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +4 -2
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +2 -0
- package/dist/settings-effect.js.map +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/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 +1 -1
- package/dist/tips.js.map +1 -1
- package/dist/tool-permissions.js +3 -3
- package/dist/tool-permissions.js.map +1 -1
- package/dist/tool-progress.js +3 -2
- package/dist/tool-progress.js.map +1 -1
- package/dist/topic-commands.d.ts +71 -4
- package/dist/topic-commands.js +167 -34
- 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 +85 -3
- package/dist/transcript-jsonl.js +156 -13
- package/dist/transcript-jsonl.js.map +1 -1
- package/dist/transcript-monitor.d.ts +11 -0
- package/dist/transcript-monitor.js +61 -13
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +24 -0
- package/dist/transcript-sources.js +211 -56
- package/dist/transcript-sources.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 +22 -0
- package/dist/ui/chat-commands.js +1 -0
- package/dist/ui/chat-mentions.js +63 -0
- package/dist/ui/chat-render.js +80 -10
- package/dist/ui/chat-store.js +70 -6
- package/dist/ui/chat-strings.js +62 -32
- package/dist/ui/chat-thread.js +199 -24
- package/dist/ui/chat-time.js +80 -0
- package/dist/ui/chat-tour.js +17 -14
- package/dist/ui/panel-chat.js +45 -21
- package/dist/ui/panel-details.js +2 -0
- package/dist/ui/panel-fleet.js +213 -58
- package/dist/ui/panel-log.js +428 -0
- package/dist/ui/panel-needs.js +14 -10
- package/dist/ui/panel-settings.js +82 -17
- 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 +11 -6
- package/dist/ui/settings-model.js +3 -1
- package/dist/ui/settings-strings.js +32 -12
- package/dist/ui/settings-token.js +8 -1
- package/dist/ui/settings-wizard.js +5 -3
- package/dist/ui/shared/app-i18n.js +41 -15
- package/dist/ui/shared/app-prefs.js +250 -0
- package/dist/ui/shared/app-route.js +6 -1
- package/dist/ui/shared/app-shell.js +165 -8
- package/dist/ui/shared/app-store.js +5 -1
- package/dist/ui/shared/app-stream.js +26 -5
- package/dist/ui/shared/app.css +170 -30
- package/dist/ui/shared/app.js +11 -1
- package/dist/ui/shared/fonts/README.md +1 -1
- package/dist/ui/shared/header-tools.js +83 -13
- package/dist/ui/shared/instance-nav.js +178 -51
- package/dist/ui/shared/instance-switch.js +8 -5
- package/dist/ui/shared/panel-view.js +21 -4
- package/dist/ui/shared/signin.js +15 -9
- package/dist/ui/shared/theme.js +39 -7
- package/dist/ui/shared/tokens.css +1 -1
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/ui/shared/usage-dialog.js +23 -5
- package/dist/ui/shared/view-strings.js +40 -4
- package/dist/ui/signin.html +8 -3
- package/dist/unrecognised-prompt.d.ts +23 -0
- package/dist/unrecognised-prompt.js +74 -0
- package/dist/unrecognised-prompt.js.map +1 -0
- package/dist/update-check.d.ts +31 -0
- package/dist/update-check.js +47 -0
- package/dist/update-check.js.map +1 -1
- package/dist/update-install.d.ts +13 -1
- package/dist/update-install.js +16 -4
- package/dist/update-install.js.map +1 -1
- package/dist/update-launch.d.ts +48 -1
- package/dist/update-launch.js +159 -5
- package/dist/update-launch.js.map +1 -1
- package/dist/update-marker.d.ts +51 -0
- package/dist/update-marker.js +61 -0
- package/dist/update-marker.js.map +1 -1
- package/dist/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 +62 -13
- package/dist/usage/providers.js +256 -68
- 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.js +31 -9
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +82 -4
- package/dist/web-api.js +632 -88
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +5 -2
- package/dist/web-auth.js.map +1 -1
- package/dist/web-chat-history.d.ts +26 -1
- package/dist/web-chat-history.js +47 -8
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +41 -5
- package/dist/web-commands.js +25 -7
- package/dist/web-commands.js.map +1 -1
- package/dist/web-login.d.ts +14 -0
- package/dist/web-login.js +4 -2
- package/dist/web-login.js.map +1 -1
- package/dist/web-mentions.d.ts +44 -0
- package/dist/web-mentions.js +75 -0
- package/dist/web-mentions.js.map +1 -0
- package/dist/web-prefs.d.ts +81 -0
- package/dist/web-prefs.js +297 -0
- package/dist/web-prefs.js.map +1 -0
- package/dist/web-preview.d.ts +14 -1
- package/dist/web-preview.js +35 -13
- package/dist/web-preview.js.map +1 -1
- package/dist/web-profile.d.ts +32 -0
- package/dist/web-profile.js +132 -0
- package/dist/web-profile.js.map +1 -0
- package/dist/web-reply-files.d.ts +45 -0
- package/dist/web-reply-files.js +223 -0
- package/dist/web-reply-files.js.map +1 -0
- package/dist/web-schedules.d.ts +12 -0
- package/dist/web-schedules.js +34 -0
- package/dist/web-schedules.js.map +1 -0
- package/dist/web-server-drain.d.ts +10 -0
- package/dist/web-server-drain.js +50 -0
- package/dist/web-server-drain.js.map +1 -0
- package/dist/web-session.d.ts +14 -0
- package/dist/web-session.js +11 -1
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +3 -1
- package/dist/web-settings-patch.js +4 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-shell-routes.d.ts +4 -0
- package/dist/web-shell-routes.js +9 -0
- package/dist/web-shell-routes.js.map +1 -1
- package/dist/web-upload.d.ts +135 -7
- package/dist/web-upload.js +315 -51
- package/dist/web-upload.js.map +1 -1
- package/launcher/install-admission.cjs +61 -6
- package/package.json +3 -3
- package/templates/launchd.plist.ejs +3 -1
- package/templates/systemd.service.ejs +2 -1
package/dist/daemon.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { AGENT_WHATS_NEW, agendVersion, planWhatsNew, readWhatsNewRecord, recordWhatsNew, whatsNewNotice } from "./agent-whats-new.js";
|
|
2
2
|
import { join, dirname, basename, resolve } from "node:path";
|
|
3
3
|
import { mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, unlinkSync, rmSync, appendFileSync, statSync, renameSync } from "node:fs";
|
|
4
|
+
import { COMPACT_WATCH_TIMEOUT_MS, launchIdentity } from "./compact-watch.js";
|
|
5
|
+
import { KIRO_COMPACT_PANE_LINES, KiroCompactPaneFollower } from "./kiro-compact-pane.js";
|
|
4
6
|
import { fileURLToPath } from "node:url";
|
|
5
7
|
import { ensureInstanceDir } from "./private-dir.js";
|
|
6
8
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
@@ -9,6 +11,7 @@ import { rotateLogIfNeededAsync } from "./logger.js";
|
|
|
9
11
|
import { coredumpFilterLaunchPrefix } from "./coredump-filter.js";
|
|
10
12
|
import { mcpServerState } from "./mcp-liveness.js";
|
|
11
13
|
import { clearPausedMarker, writePausedMarker } from "./pause-marker.js";
|
|
14
|
+
import { writeBackgroundWorkEndedMarker } from "./background-work-ended.js";
|
|
12
15
|
import { TmuxManager, resolveTmuxLogicalSize } from "./tmux-manager.js";
|
|
13
16
|
import { TranscriptMonitor } from "./transcript-monitor.js";
|
|
14
17
|
import { createTranscriptSource } from "./transcript-sources.js";
|
|
@@ -32,6 +35,7 @@ import { HangDetector } from "./hang-detector.js";
|
|
|
32
35
|
import { writeSecretFile } from "./secret-file.js";
|
|
33
36
|
import { PaneWriteLock } from "./pane-write-lock.js";
|
|
34
37
|
import { buildFleetInstructions } from "./instructions.js";
|
|
38
|
+
import { formatHumanInboundMessage, prepareDeliveryWire, steerInboundEnvelope, BTW_INBOUND_PREFIX, humanBodyHash, humanOwnerObjectId } from "./human-delivery-receipts.js";
|
|
35
39
|
import { formatCrossInstanceInboundMessage, renderCrossInstanceHandoffMetadata, } from "./cross-instance-envelope.js";
|
|
36
40
|
import { agendMessageInInput, bottomRowIsReady, inputAreaText, inputShowsPastedText, pastedTextSignature, pasteLeftInInput, strandedAgendMessageInInput } from "./pane-input-residue.js";
|
|
37
41
|
import { TurnReplyGuard } from "./turn-reply-guard.js";
|
|
@@ -45,7 +49,9 @@ import { measureSyncWork, noteSyncWork } from "./sync-work-attribution.js";
|
|
|
45
49
|
import { TranscriptDeltaReader } from "./transcript-delta-reader.js";
|
|
46
50
|
import { buildResumeContinuation, clearInFlightTurnMarker, consumeInFlightTurnMarker, decideTurnResume, normalizeResumeBackend, readResumeFingerprint, RESUME_BLOCKING_DELIVERY_STATES, writeInFlightTurnMarker, } from "./turn-resume.js";
|
|
47
51
|
import { codexLiveRowMatches } from "./backend/codex.js";
|
|
52
|
+
import { claudeConfigDirFor, claudeProfileConfigDir, claudeSwitchDialog, claudeSwitchTargetMatches } from "./backend/claude-code.js";
|
|
48
53
|
import { InteractionObservation, INTERACTION_CONFIRM_MS, sameInteractionOwner } from "./interaction-observation.js";
|
|
54
|
+
import { unrecognisedPromptShape } from "./unrecognised-prompt.js";
|
|
49
55
|
const __filename = fileURLToPath(import.meta.url);
|
|
50
56
|
const __dirname = dirname(__filename);
|
|
51
57
|
// Tool routing sets — module-level to avoid re-creation on every handleToolCall
|
|
@@ -86,8 +92,10 @@ const TRANSCRIPT_RUNNING_HOLD_MAX_MS = 30 * 60_000;
|
|
|
86
92
|
export const TRANSCRIPT_OPEN_TURN_QUIET_MS = 15 * 60_000;
|
|
87
93
|
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.";
|
|
88
94
|
/** Point a resumed CLI at its one backend-native instruction source. */
|
|
89
|
-
/** The outbox evidence for a delivery the CLI's transcript shows taken (#758; #1201 absorbed). */
|
|
90
|
-
function transcriptMarkerEvidence(kind) {
|
|
95
|
+
/** The outbox evidence for a delivery the CLI's transcript shows taken (#758; #1201 absorbed; #1647 a raw paste's text). */
|
|
96
|
+
function transcriptMarkerEvidence(kind, rawPaste = false) {
|
|
97
|
+
if (rawPaste)
|
|
98
|
+
return "transcript-turn-user-entry-raw-paste-text";
|
|
91
99
|
return kind === "user" ? "transcript-marker" : kind === "absorbed" ? "transcript-marker-absorbed" : "transcript-marker-queued";
|
|
92
100
|
}
|
|
93
101
|
export function buildInstructionReloadNotice(binaryName, instanceName, instanceDir, backendSource) {
|
|
@@ -96,7 +104,7 @@ export function buildInstructionReloadNotice(binaryName, instanceName, instanceD
|
|
|
96
104
|
: binaryName === "kiro-cli"
|
|
97
105
|
? `.kiro/steering/agend-${instanceName}.md`
|
|
98
106
|
: binaryName === "agy"
|
|
99
|
-
? ".agents/
|
|
107
|
+
? ".agents/AGENTS.md"
|
|
100
108
|
: join(instanceDir, "fleet-instructions.md");
|
|
101
109
|
return `[system] Your AgEnD instructions have been updated. Reload only ${source}; do not scan other instruction directories. Do not reply to this message.`;
|
|
102
110
|
}
|
|
@@ -132,6 +140,8 @@ export const DEFAULT_STATE_IDLE_DEBOUNCE_MS = 2_000;
|
|
|
132
140
|
export const DEFAULT_STATE_SAFETY_SWEEP_MS = 60_000;
|
|
133
141
|
/** Coalesce one cosmetic-redraw burst before comparing the visible pane. */
|
|
134
142
|
export const PERIODIC_REDRAW_PROBE_MS = 25;
|
|
143
|
+
/** Board 907c7899: how often a kiro /compact's pane is read (TUI V2's "too short" shows for about ten seconds). */
|
|
144
|
+
const KIRO_COMPACT_PANE_POLL_MS = 1_000;
|
|
135
145
|
/** Do not turn a streaming Codex turn into one capture-pane per burst. */
|
|
136
146
|
export const STRUCTURED_IDLE_PROBE_MS = 500;
|
|
137
147
|
/**
|
|
@@ -479,6 +489,16 @@ const POST_ENTER_PROOF_WINDOW_MS = 3_000;
|
|
|
479
489
|
/** A final bounded observation before an ambiguous Codex submit can be classified. */
|
|
480
490
|
const CODEX_LATE_PROOF_MS = 5_000;
|
|
481
491
|
const POST_ENTER_PROOF_POLL_MS = 250;
|
|
492
|
+
/** #1647: a raw paste a pane with no transcript showed taken — its text left the input row (or the CLI's own busy marker
|
|
493
|
+
* replaced it), with output after the Enter. */
|
|
494
|
+
const RAW_PASTE_LEFT_INPUT = "raw-paste-left-input-after-output";
|
|
495
|
+
/**
|
|
496
|
+
* #1647/#1663 P1-2: backends whose transcript AgEnD holds for this instance alone, so a raw paste's exact text in it is
|
|
497
|
+
* this paste's turn. claude-code: the path is its own statusline.json's transcript_path (written by this instance's
|
|
498
|
+
* CLI). Not codex: its rollout is chosen by working directory, which another instance can share, and AgEnD holds no
|
|
499
|
+
* session id to bind it — so a codex raw paste the pane could not prove stays uncertain.
|
|
500
|
+
*/
|
|
501
|
+
const RAW_TEXT_TRANSCRIPT_BACKENDS = ["claude-code"];
|
|
482
502
|
/**
|
|
483
503
|
* A readerless CLI's delivery is proven by its own transcript, which lags the pane by a flush. Only a delta that was
|
|
484
504
|
* read and carried no marker for this long is held against the delivery — a late flush must not raise a false ⚠️.
|
|
@@ -510,8 +530,16 @@ const DIALOG_PROBE_UNKNOWN_MAX = 40;
|
|
|
510
530
|
/** Startup dialog scan: poll cadence and wall-clock budget (keys and waits included). */
|
|
511
531
|
const STARTUP_DIALOG_POLL_MS = 500;
|
|
512
532
|
const STARTUP_DIALOG_BUDGET_MS = 30_000;
|
|
533
|
+
/** #1690: after AgEnD's own `/model`, how long the pane is read for claude's "Switch model?" or its result. */
|
|
534
|
+
export const OWN_SWITCH_WAIT_MS = 6_000;
|
|
535
|
+
/** #1690: after an answer to that dialog, how long it gets to go away. */
|
|
536
|
+
const OWN_SWITCH_SETTLE_MS = 3_000;
|
|
513
537
|
/** A blocking dialog still on screen this long after first being seen is reported for a human. */
|
|
514
538
|
const DIALOG_PARKED_NOTIFY_MS = 60_000;
|
|
539
|
+
/** #1691: a prompt no table knows must sit unchanged this long (monotonic) before it holds deliveries and is reported. */
|
|
540
|
+
export const UNRECOGNISED_PROMPT_STABLE_MS = 60_000;
|
|
541
|
+
/** #1691: the rows under a prompt a backend without a composer reader shows its ready chrome in (status bar, hint). */
|
|
542
|
+
const COMPOSER_TAIL_ROWS = 4;
|
|
515
543
|
/** After a runtime dialog's keys, how long the pane gets to repaint before it is read again to see whether they took. */
|
|
516
544
|
const DIALOG_ANSWER_SETTLE_MS = 300;
|
|
517
545
|
/** Consecutive answers one runtime dialog ignored (still on screen right after the keys) before it is reported for a human. */
|
|
@@ -621,6 +649,10 @@ export function channelStatus(meta) {
|
|
|
621
649
|
// they are the dashboard's ticks (the fleet routes them by the message id, never to a platform).
|
|
622
650
|
if (!messageId || (!chatId && meta.source !== "web"))
|
|
623
651
|
return undefined;
|
|
652
|
+
// #1823: a notice from AgEnD itself (a poll's results) names its chat so the answer goes there, but its message id
|
|
653
|
+
// is AgEnD's own, not a platform message: there is nothing to react to.
|
|
654
|
+
if (meta.source === "agend-system")
|
|
655
|
+
return undefined;
|
|
624
656
|
if (!chatId)
|
|
625
657
|
return { chatId: "", messageId };
|
|
626
658
|
const threadId = meta.thread_id || undefined;
|
|
@@ -1065,6 +1097,10 @@ export class Daemon extends EventEmitter {
|
|
|
1065
1097
|
ipcServer = null;
|
|
1066
1098
|
messageBus;
|
|
1067
1099
|
transcriptMonitor = null;
|
|
1100
|
+
/** #1218: the transcript this instance's monitor follows now (the Log tab reads it on demand; nothing runs for it here). */
|
|
1101
|
+
logTranscriptPath() { return this.transcriptMonitor?.currentPath() ?? null; }
|
|
1102
|
+
/** #1218 (kiro): the launch this daemon's backend last built, for the Log's reason only (kiro-log-gate.ts). */
|
|
1103
|
+
logLaunch() { return this.backend?.logLaunch?.() ?? null; }
|
|
1068
1104
|
guardian = null;
|
|
1069
1105
|
adapter = null;
|
|
1070
1106
|
pendingIpcRequests = new Map();
|
|
@@ -1151,6 +1187,34 @@ export class Daemon extends EventEmitter {
|
|
|
1151
1187
|
dialogParkedKey = null;
|
|
1152
1188
|
dialogParkedInteractionEpisode = null;
|
|
1153
1189
|
dialogParkedReported = false;
|
|
1190
|
+
/**
|
|
1191
|
+
* #1691: the prompt at the bottom of the pane that no backend table knows — its block, since when (performance.now)
|
|
1192
|
+
* it has been unchanged, and whether that has lasted UNRECOGNISED_PROMPT_STABLE_MS. Updated by the runtime scan.
|
|
1193
|
+
*/
|
|
1194
|
+
unrecognisedPrompt = null;
|
|
1195
|
+
/**
|
|
1196
|
+
* #1691: when (performance.now) the newest pane read that decided the evidence above was taken — a scan or a fresh
|
|
1197
|
+
* read, confirming, restarting or ending it. A read taken before it finishes late and says nothing about now: it may
|
|
1198
|
+
* neither clear, restart nor confirm (#1695 r4).
|
|
1199
|
+
*/
|
|
1200
|
+
unrecognisedPromptReadAt = -Infinity;
|
|
1201
|
+
/**
|
|
1202
|
+
* #1691: the catch-all for that prompt, as a hold-only runtime dialog — so the delivery hold, awaiting_input, the
|
|
1203
|
+
* Needs-you item and the single parked report are the existing ones. Never answered (hold-only, no keys).
|
|
1204
|
+
*/
|
|
1205
|
+
unrecognisedPromptDialog = {
|
|
1206
|
+
pattern: /(?!)unrecognised-prompt-1691/,
|
|
1207
|
+
keys: [],
|
|
1208
|
+
description: "a prompt AgEnD does not recognise",
|
|
1209
|
+
holdOnly: true,
|
|
1210
|
+
isActive: (pane) => this.unrecognisedPromptActive(pane),
|
|
1211
|
+
requestIdentity: () => this.unrecognisedPrompt?.block ?? null,
|
|
1212
|
+
};
|
|
1213
|
+
/**
|
|
1214
|
+
* #1690: AgEnD's own runtime `/model` in flight — from the moment its paste went in until its "Switch model?" dialog
|
|
1215
|
+
* (if any) is answered. Every delivery is held meanwhile: the dialog's default cursor is Yes, a stray Enter confirms it.
|
|
1216
|
+
*/
|
|
1217
|
+
ownSwitch = null;
|
|
1154
1218
|
/**
|
|
1155
1219
|
* The runtime dialog the monitor is answering, and how many of its answers in a row did nothing (the dialog was
|
|
1156
1220
|
* still on screen right after the keys). Not the on-screen duration: a burst of FRESH prompts, each answered, has a
|
|
@@ -1870,8 +1934,14 @@ export class Daemon extends EventEmitter {
|
|
|
1870
1934
|
this.finishDurableDelivery(delivery, "delivered", OUTPUT_EDGE_ONLY_EVIDENCE);
|
|
1871
1935
|
return;
|
|
1872
1936
|
}
|
|
1937
|
+
// #1647: a raw paste on a pane with no transcript (kiro), taken by the CLI as its own input row shows.
|
|
1938
|
+
if (verdict.proof === RAW_PASTE_LEFT_INPUT) {
|
|
1939
|
+
this.finishDurableDelivery(delivery, "delivered", verdict.proof, verdict);
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1873
1942
|
this.finishDurableDelivery(delivery, "delivered", verdict.transcriptProof
|
|
1874
|
-
? `positive submission proof; ${transcriptMarkerEvidence(verdict.transcriptProof
|
|
1943
|
+
? `positive submission proof; ${transcriptMarkerEvidence(verdict.transcriptProof, verdict.submissionMode === "raw_paste")}`
|
|
1944
|
+
: "positive submission proof", verdict);
|
|
1875
1945
|
}
|
|
1876
1946
|
/**
|
|
1877
1947
|
* Finish a delivery that only the output edge vouched for, by the delivery's own marker in the CLI's transcript (#758).
|
|
@@ -1887,23 +1957,28 @@ export class Daemon extends EventEmitter {
|
|
|
1887
1957
|
* TRANSCRIPT_PROOF_WINDOW_MS). It never finishes a row that stop has handed to reconciliation; a pause or respawn
|
|
1888
1958
|
* only ends the wait, falling back to today's outcome.
|
|
1889
1959
|
*/
|
|
1890
|
-
async proveDeliveryFromTranscript(delivery, checkpoint) {
|
|
1960
|
+
async proveDeliveryFromTranscript(delivery, checkpoint, rawPaste) {
|
|
1891
1961
|
const spawn = this.spawnGeneration;
|
|
1892
1962
|
const fence = this.launchFenceEpoch;
|
|
1893
1963
|
let last = "unavailable";
|
|
1894
1964
|
let outcome = "delivered";
|
|
1895
1965
|
let evidence = OUTPUT_EDGE_ONLY_EVIDENCE;
|
|
1966
|
+
// #1647: a raw paste has no marker; its text is the needle, and its pane proof already failed — so without the turn
|
|
1967
|
+
// in the transcript it stays uncertain (with the pane's reason), never the output-edge outcome.
|
|
1968
|
+
const needle = rawPaste ? { rawText: rawPaste.text } : delivery.deliveryId;
|
|
1969
|
+
let found = false;
|
|
1896
1970
|
try {
|
|
1897
1971
|
for (let polls = 0; polls <= TRANSCRIPT_PROOF_WINDOW_MS / TRANSCRIPT_PROOF_POLL_MS; polls++) {
|
|
1898
1972
|
if (this.deliveryWritesStopping)
|
|
1899
1973
|
return; // reconciliation owns the row now
|
|
1900
|
-
const
|
|
1901
|
-
if (
|
|
1974
|
+
const look = await scanTranscriptForDeliveryMarker(checkpoint.path, checkpoint.offset, checkpoint.backend, needle);
|
|
1975
|
+
if (look === "user" || look === "absorbed" || look === "queued") {
|
|
1976
|
+
found = true;
|
|
1902
1977
|
outcome = "delivered";
|
|
1903
|
-
evidence = transcriptMarkerEvidence(
|
|
1978
|
+
evidence = transcriptMarkerEvidence(look, !!rawPaste);
|
|
1904
1979
|
break;
|
|
1905
1980
|
}
|
|
1906
|
-
last =
|
|
1981
|
+
last = look;
|
|
1907
1982
|
if (spawn !== this.spawnGeneration || fence !== this.launchFenceEpoch) {
|
|
1908
1983
|
last = "unavailable";
|
|
1909
1984
|
break;
|
|
@@ -1923,6 +1998,11 @@ export class Daemon extends EventEmitter {
|
|
|
1923
1998
|
catch (err) {
|
|
1924
1999
|
this.logger.debug({ err }, "Transcript proof of a delivery failed — keeping the output-edge outcome");
|
|
1925
2000
|
}
|
|
2001
|
+
if (rawPaste && !found) {
|
|
2002
|
+
outcome = "uncertain";
|
|
2003
|
+
evidence = `${rawPaste.unproven}; no-transcript-turn (${last})`;
|
|
2004
|
+
this.logger.warn({ deliveryId: delivery.deliveryId, cause: evidence }, "Raw paste outcome uncertain — not re-pasting");
|
|
2005
|
+
}
|
|
1926
2006
|
// The settlement itself can throw (e.g. the outbox store is unhealthy), and this runs on a detached `void`
|
|
1927
2007
|
// promise: an uncaught throw is an unhandled rejection, which the fleet restart path turns into a global fault.
|
|
1928
2008
|
// Settle observably and leave the row for reconciliation — never a false completion, never another paste (#1207 P2).
|
|
@@ -1936,6 +2016,7 @@ export class Daemon extends EventEmitter {
|
|
|
1936
2016
|
}
|
|
1937
2017
|
}
|
|
1938
2018
|
deferDurableDelivery(meta, reason) {
|
|
2019
|
+
this.finishHumanReceipt(meta, { reached: false });
|
|
1939
2020
|
const delivery = this.durableDeliveryAttempt(meta);
|
|
1940
2021
|
if (delivery === null || delivery === false)
|
|
1941
2022
|
return;
|
|
@@ -3188,7 +3269,6 @@ export class Daemon extends EventEmitter {
|
|
|
3188
3269
|
if (this.runtimeMonitorsFrozen || this.errorMonitorTimer)
|
|
3189
3270
|
return;
|
|
3190
3271
|
const patterns = this.backend?.getErrorPatterns?.() ?? [];
|
|
3191
|
-
const dialogs = this.backend?.getRuntimeDialogs?.() ?? [];
|
|
3192
3272
|
if (!this.tmux)
|
|
3193
3273
|
return;
|
|
3194
3274
|
if (!this.backend)
|
|
@@ -3224,6 +3304,9 @@ export class Daemon extends EventEmitter {
|
|
|
3224
3304
|
scanStartedAt = performance.now();
|
|
3225
3305
|
if (stale())
|
|
3226
3306
|
return;
|
|
3307
|
+
this.trackUnrecognisedPrompt(pane, captureMono, this.instanceStateLastOutputAt > 0 && this.instanceStateLastOutputAt >= captureAt);
|
|
3308
|
+
// The backend's dialogs, and #1691's catch-all while an unrecognised prompt is active (decided just above).
|
|
3309
|
+
const dialogs = this.runtimeDialogs();
|
|
3227
3310
|
if (this.instanceStateLastOutputAt > 0 && this.instanceStateLastOutputAt >= captureAt) {
|
|
3228
3311
|
this.unverifyInteraction(captureMono, captureOrder);
|
|
3229
3312
|
}
|
|
@@ -3692,6 +3775,42 @@ export class Daemon extends EventEmitter {
|
|
|
3692
3775
|
: Math.max(this.deliveryEpoch, fleetEpoch);
|
|
3693
3776
|
this.logger.info({ deliveryEpoch: this.deliveryEpoch }, "Pending delivery queue cleared by user cancel");
|
|
3694
3777
|
}
|
|
3778
|
+
/** Board 907c7899: the layout a web /compact's end can be read from on this pane (kiro classic or TUI V2), or null. */
|
|
3779
|
+
compactPaneUi() {
|
|
3780
|
+
return this.backend?.compactPaneUi?.() ?? null;
|
|
3781
|
+
}
|
|
3782
|
+
/**
|
|
3783
|
+
* Board 907c7899: follow a /compact written to this pane until the pane says how it ended, then say so once
|
|
3784
|
+
* (`compact_pane_result`). Bound to the launch it was written under: another launch, or no pane, ends the look
|
|
3785
|
+
* silently (the fleet's watch sees the launch change and calls it lost); the fleet's deadline is the same.
|
|
3786
|
+
*/
|
|
3787
|
+
async followCompactPane(id, owner, follower) {
|
|
3788
|
+
const tmux = this.tmux;
|
|
3789
|
+
if (!tmux || owner === null)
|
|
3790
|
+
return;
|
|
3791
|
+
// Monotonic, and asked after every await: a result past the deadline is not said (the fleet's watch has timed out).
|
|
3792
|
+
const deadline = performance.now() + COMPACT_WATCH_TIMEOUT_MS;
|
|
3793
|
+
const standing = () => this.tmux === tmux && launchIdentity(this.interactionOwner()) === owner && performance.now() < deadline;
|
|
3794
|
+
while (standing()) {
|
|
3795
|
+
await new Promise(resolve => { setTimeout(resolve, KIRO_COMPACT_PANE_POLL_MS).unref?.(); });
|
|
3796
|
+
if (!standing())
|
|
3797
|
+
return;
|
|
3798
|
+
let pane;
|
|
3799
|
+
try {
|
|
3800
|
+
pane = await tmux.capturePaneWithHistory(KIRO_COMPACT_PANE_LINES);
|
|
3801
|
+
}
|
|
3802
|
+
catch {
|
|
3803
|
+
continue;
|
|
3804
|
+
}
|
|
3805
|
+
if (!standing())
|
|
3806
|
+
return;
|
|
3807
|
+
const outcome = follower.look(pane);
|
|
3808
|
+
if (outcome) {
|
|
3809
|
+
this.ipcServer?.broadcast({ type: "compact_pane_result", id, owner, outcome });
|
|
3810
|
+
return;
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3695
3814
|
captureDeliveryEpoch(value) {
|
|
3696
3815
|
const epoch = typeof value === "number" && Number.isSafeInteger(value) && value >= 0
|
|
3697
3816
|
? value
|
|
@@ -3703,14 +3822,55 @@ export class Daemon extends EventEmitter {
|
|
|
3703
3822
|
isDeliveryEpochCurrent(epoch) {
|
|
3704
3823
|
return epoch === this.deliveryEpoch;
|
|
3705
3824
|
}
|
|
3706
|
-
queueRawPaste(rawText, deliveryEpoch = this.deliveryEpoch, confirmClear = false, durableAttempt) {
|
|
3825
|
+
queueRawPaste(rawText, deliveryEpoch = this.deliveryEpoch, confirmClear = false, durableAttempt, ownSwitch, notifyWritten) {
|
|
3826
|
+
const reportSwitch = (result) => {
|
|
3827
|
+
if (!ownSwitch)
|
|
3828
|
+
return;
|
|
3829
|
+
this.ipcServer?.broadcast({ type: "runtime_switch_result", id: ownSwitch.id, ...result });
|
|
3830
|
+
};
|
|
3831
|
+
// Board 003648ac: the fleet follows a web /compact from just before its paste is written: the transcript's end then
|
|
3832
|
+
// (taken here, under this launch, before the write) and the launch it was written under ride with the notice — once.
|
|
3833
|
+
// The checkpoint and the launch are taken together, right before the paste (each attempt takes them again); the
|
|
3834
|
+
// notice carries those — never the launch at notice time, which a respawn in between would relabel.
|
|
3835
|
+
let told = false;
|
|
3836
|
+
let checkpoint = null;
|
|
3837
|
+
let owner = null;
|
|
3838
|
+
// Board 907c7899: kiro says how a /compact ended only on its own pane; read from the pane just before this write.
|
|
3839
|
+
let paneFollower = null;
|
|
3840
|
+
const tellWritten = (written) => {
|
|
3841
|
+
if (!notifyWritten || told)
|
|
3842
|
+
return;
|
|
3843
|
+
told = true;
|
|
3844
|
+
this.ipcServer?.broadcast({ type: "raw_paste_written", id: notifyWritten, outcome: written ? "written" : "not_written",
|
|
3845
|
+
...(written ? { owner, checkpoint, ...(paneFollower ? { pane: true } : {}) } : {}) });
|
|
3846
|
+
if (written && paneFollower)
|
|
3847
|
+
void this.followCompactPane(notifyWritten, owner, paneFollower);
|
|
3848
|
+
};
|
|
3849
|
+
const takeCheckpoint = () => {
|
|
3850
|
+
owner = launchIdentity(this.interactionOwner());
|
|
3851
|
+
const path = this.logTranscriptPath();
|
|
3852
|
+
checkpoint = null;
|
|
3853
|
+
if (!path)
|
|
3854
|
+
return;
|
|
3855
|
+
try {
|
|
3856
|
+
const st = statSync(path);
|
|
3857
|
+
checkpoint = { path, ino: st.ino, size: st.size };
|
|
3858
|
+
}
|
|
3859
|
+
catch {
|
|
3860
|
+
checkpoint = null;
|
|
3861
|
+
}
|
|
3862
|
+
};
|
|
3707
3863
|
if (!this.tmux || !this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
3708
3864
|
if (durableAttempt)
|
|
3709
3865
|
this.retryDurableDeliveryBeforeBegin(durableAttempt, "raw_paste target unavailable before pane queue");
|
|
3866
|
+
reportSwitch({ outcome: "not_sent" });
|
|
3867
|
+
tellWritten(false);
|
|
3710
3868
|
return;
|
|
3711
3869
|
}
|
|
3712
3870
|
this.pasteLock = this.pasteLock.then(async () => {
|
|
3713
3871
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
3872
|
+
reportSwitch({ outcome: "not_sent" });
|
|
3873
|
+
tellWritten(false);
|
|
3714
3874
|
this.logger.info("Pending raw delivery dropped by user cancel");
|
|
3715
3875
|
if (durableAttempt)
|
|
3716
3876
|
this.retryDurableDeliveryBeforeBegin(durableAttempt, "raw_paste epoch changed while queued");
|
|
@@ -3722,18 +3882,44 @@ export class Daemon extends EventEmitter {
|
|
|
3722
3882
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
3723
3883
|
if (durableAttempt)
|
|
3724
3884
|
this.retryDurableDeliveryBeforeBegin(durableAttempt, "raw_paste epoch changed while waking");
|
|
3885
|
+
reportSwitch({ outcome: "not_sent" }); // #1690: cancelled before anything was typed — said at once, nothing saved
|
|
3886
|
+
tellWritten(false);
|
|
3725
3887
|
return;
|
|
3726
3888
|
}
|
|
3889
|
+
// Board 907c7899: the pane just before this attempt's paste — after its readiness wait, last capture and fence,
|
|
3890
|
+
// under the paste lock (no other input lands in between). Each attempt looks again.
|
|
3891
|
+
const lookBefore = async () => {
|
|
3892
|
+
const ui = this.backend?.compactPaneUi?.() ?? null;
|
|
3893
|
+
const before = ui ? await this.tmux?.capturePaneWithHistory(KIRO_COMPACT_PANE_LINES).catch(() => null) : null;
|
|
3894
|
+
paneFollower = ui && typeof before === "string" ? new KiroCompactPaneFollower(ui, before) : null;
|
|
3895
|
+
};
|
|
3727
3896
|
const delivered = await this.deliverMessage(rawText, undefined, {
|
|
3728
3897
|
deliveryEpoch,
|
|
3729
3898
|
verdict,
|
|
3730
3899
|
durableAttempt,
|
|
3731
3900
|
preserveExactBytes: true,
|
|
3901
|
+
// #1690: our own /model holds every other delivery from its write on — before the lock is released, so a steer
|
|
3902
|
+
// or /btw waiting on it cannot type into the gap before claude paints "Switch model?" (default cursor: Yes).
|
|
3903
|
+
...(ownSwitch || notifyWritten ? { onPaneWritten: () => { if (ownSwitch)
|
|
3904
|
+
this.ownSwitch = ownSwitch; tellWritten(true); } } : {}),
|
|
3905
|
+
...(notifyWritten ? { beforePaneWrite: takeCheckpoint, ...(this.backend?.compactPaneUi?.() ? { lookBeforePaneWrite: lookBefore } : {}) } : {}),
|
|
3732
3906
|
});
|
|
3907
|
+
tellWritten(false); // never written (once told, this says nothing)
|
|
3733
3908
|
if (delivered)
|
|
3734
3909
|
this.finishDurableSubmission(durableAttempt ?? null, verdict);
|
|
3735
3910
|
else if (durableAttempt && verdict.paneWriteStarted) {
|
|
3736
|
-
|
|
3911
|
+
const unproven = `${verdict.phase ?? "raw-paste"}:${verdict.proof ?? "unverified"}`;
|
|
3912
|
+
const checkpoint = verdict.transcriptCheckpoint;
|
|
3913
|
+
if (checkpoint && RAW_TEXT_TRANSCRIPT_BACKENDS.includes(checkpoint.backend)) {
|
|
3914
|
+
// #1647: the pane could not show it, but the CLI's transcript records the turn the paste started — later
|
|
3915
|
+
// than the pane window sometimes. Wait for it there (bounded), off the paste lock.
|
|
3916
|
+
this.logger.info({ deliveryId: durableAttempt.deliveryId, unproven }, "Raw paste not proven on the pane — asking the CLI's transcript for the turn it started");
|
|
3917
|
+
void this.proveDeliveryFromTranscript(durableAttempt, checkpoint, { text: rawText, unproven });
|
|
3918
|
+
}
|
|
3919
|
+
else {
|
|
3920
|
+
this.logger.warn({ deliveryId: durableAttempt.deliveryId, cause: unproven }, "Raw paste outcome uncertain — not re-pasting");
|
|
3921
|
+
this.finishDurableDelivery(durableAttempt, "uncertain", unproven);
|
|
3922
|
+
}
|
|
3737
3923
|
}
|
|
3738
3924
|
else if (durableAttempt && verdict.durableBeginCommitted) {
|
|
3739
3925
|
this.abortDurableDelivery(durableAttempt, `${verdict.phase ?? "raw-paste"}:${verdict.proof ?? "no-pane-write"}`);
|
|
@@ -3750,10 +3936,32 @@ export class Daemon extends EventEmitter {
|
|
|
3750
3936
|
else if (durableAttempt)
|
|
3751
3937
|
this.retryDurableDeliveryBeforeBegin(durableAttempt, err.message);
|
|
3752
3938
|
this.logger.warn({ err: err.message }, "raw_paste delivery error");
|
|
3939
|
+
if (ownSwitch) {
|
|
3940
|
+
if (this.ownSwitch === ownSwitch)
|
|
3941
|
+
this.ownSwitch = null;
|
|
3942
|
+
reportSwitch({ outcome: "not_confirmed" });
|
|
3943
|
+
}
|
|
3944
|
+
tellWritten(false);
|
|
3753
3945
|
return;
|
|
3754
3946
|
}
|
|
3755
3947
|
if (confirmClear)
|
|
3756
3948
|
await this.confirmBackendClearDialog();
|
|
3949
|
+
if (ownSwitch) {
|
|
3950
|
+
if (this.ownSwitch !== ownSwitch)
|
|
3951
|
+
reportSwitch({ outcome: "not_sent" }); // its write never began
|
|
3952
|
+
else {
|
|
3953
|
+
try {
|
|
3954
|
+
reportSwitch(await this.answerOwnSwitchDialog(ownSwitch));
|
|
3955
|
+
}
|
|
3956
|
+
catch (err) {
|
|
3957
|
+
this.logger.warn({ err: err.message }, "Answering our own model switch failed");
|
|
3958
|
+
reportSwitch({ outcome: "not_confirmed" });
|
|
3959
|
+
}
|
|
3960
|
+
finally {
|
|
3961
|
+
this.ownSwitch = null;
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3757
3965
|
this.logger.debug({ text: rawText.slice(0, 100) }, "Raw paste delivered");
|
|
3758
3966
|
});
|
|
3759
3967
|
this.pasteLock = this.pasteLock.catch(err => {
|
|
@@ -3772,7 +3980,94 @@ export class Daemon extends EventEmitter {
|
|
|
3772
3980
|
this.deferDurableDelivery(deliveryMeta, "invalid raw_paste delivery attempt metadata");
|
|
3773
3981
|
return;
|
|
3774
3982
|
}
|
|
3775
|
-
|
|
3983
|
+
const sw = msg.runtime_switch;
|
|
3984
|
+
const ownSwitch = sw && typeof sw.id === "string" && sw.kind === "model" && typeof sw.requested === "string"
|
|
3985
|
+
? { id: sw.id, kind: "model", requested: sw.requested } : undefined;
|
|
3986
|
+
const notifyWritten = typeof msg.notify_written === "string" && /^[0-9a-f]{16}$/.test(msg.notify_written) ? msg.notify_written : undefined;
|
|
3987
|
+
this.queueRawPaste(msg.content, this.captureDeliveryEpoch(msg.delivery_epoch), msg.confirm_clear === true, durableAttempt ? { ...durableAttempt, submissionMode: "raw_paste", silentSchedule: msg.silent_schedule === true } : undefined, ownSwitch, notifyWritten);
|
|
3988
|
+
}
|
|
3989
|
+
/**
|
|
3990
|
+
* #1690 (the user's choice A): after AgEnD's own `/model <name>`, claude may ask "Switch model?" (a warm cache for
|
|
3991
|
+
* another model). When the model it names is the one asked for, answer Yes; otherwise No (Escape — `Kept model as …`),
|
|
3992
|
+
* and say so. Every key goes on a freshly read, verified dialog under the pane write lock: the same target, and for
|
|
3993
|
+
* Enter the cursor on "1. Yes"; the cursor is walked there with Up, one verified step at a time, never a blind key.
|
|
3994
|
+
* Without the dialog, claude's own "Set model to …" line after the command is the result.
|
|
3995
|
+
*/
|
|
3996
|
+
async answerOwnSwitchDialog(spec) {
|
|
3997
|
+
const tmux = this.tmux;
|
|
3998
|
+
if (!tmux)
|
|
3999
|
+
return { outcome: "not_confirmed" };
|
|
4000
|
+
const read = () => tmux.capturePane().catch(() => "");
|
|
4001
|
+
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
4002
|
+
/** One key on a dialog re-read under the lock: still the same target, and (when given) the cursor where expected. */
|
|
4003
|
+
const press = (target, cursor, key) => this.paneWriteLock.run(async () => {
|
|
4004
|
+
if (this.tmux !== tmux)
|
|
4005
|
+
return false;
|
|
4006
|
+
const now = claudeSwitchDialog(await read());
|
|
4007
|
+
if (!now || now.kind !== "model" || now.target !== target || (cursor !== null && now.cursor !== cursor))
|
|
4008
|
+
return false;
|
|
4009
|
+
return tmux.sendSpecialKey(key);
|
|
4010
|
+
});
|
|
4011
|
+
const gone = async () => {
|
|
4012
|
+
const until = performance.now() + OWN_SWITCH_SETTLE_MS;
|
|
4013
|
+
while (performance.now() < until) {
|
|
4014
|
+
if (!claudeSwitchDialog(await read()))
|
|
4015
|
+
return true;
|
|
4016
|
+
await sleep(150);
|
|
4017
|
+
}
|
|
4018
|
+
return false;
|
|
4019
|
+
};
|
|
4020
|
+
const deadline = performance.now() + OWN_SWITCH_WAIT_MS;
|
|
4021
|
+
while (performance.now() < deadline && this.tmux === tmux) {
|
|
4022
|
+
const pane = await read();
|
|
4023
|
+
const dialog = claudeSwitchDialog(pane);
|
|
4024
|
+
if (dialog && dialog.kind === "model") {
|
|
4025
|
+
const shown = dialog.target;
|
|
4026
|
+
if (!claudeSwitchTargetMatches(spec.requested, shown)) {
|
|
4027
|
+
const answered = await press(shown, null, "Escape") && await gone();
|
|
4028
|
+
this.logger.warn({ requested: spec.requested, shown, answered }, "claude would switch to another model than asked — answered No");
|
|
4029
|
+
return { outcome: "mismatch", shown, answered };
|
|
4030
|
+
}
|
|
4031
|
+
// Walk the cursor to "1. Yes" (it starts there; a human may have moved it): Up from row 2, re-read each time.
|
|
4032
|
+
let current = dialog;
|
|
4033
|
+
for (let step = 0; step < 2 && current.cursor === 2; step++) {
|
|
4034
|
+
if (!await press(shown, 2, "Up"))
|
|
4035
|
+
break;
|
|
4036
|
+
await sleep(120);
|
|
4037
|
+
const next = claudeSwitchDialog(await read());
|
|
4038
|
+
if (!next || next.target !== shown)
|
|
4039
|
+
break;
|
|
4040
|
+
current = next;
|
|
4041
|
+
}
|
|
4042
|
+
if (current.cursor !== 1 || !await press(shown, 1, "Enter") || !await gone()) {
|
|
4043
|
+
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");
|
|
4044
|
+
return { outcome: "not_confirmed", shown };
|
|
4045
|
+
}
|
|
4046
|
+
this.logger.info({ requested: spec.requested, shown }, "Confirmed our own model switch (claude: the next reply re-reads the history)");
|
|
4047
|
+
return { outcome: "switched", confirmed: true, shown };
|
|
4048
|
+
}
|
|
4049
|
+
if (Daemon.switchResultAfterCommand(pane, spec.requested))
|
|
4050
|
+
return { outcome: "switched", confirmed: false };
|
|
4051
|
+
await sleep(200);
|
|
4052
|
+
}
|
|
4053
|
+
return { outcome: "not_confirmed" };
|
|
4054
|
+
}
|
|
4055
|
+
/**
|
|
4056
|
+
* #1690: claude's "Set model to …" line under our `/model <requested>` (no dialog was needed). Only the newest command
|
|
4057
|
+
* on the pane counts: an older identical `/model` with its result further up (switched away and back) must not pass
|
|
4058
|
+
* for this one before it has even painted — that left its dialog unanswered (live, 2.1.296).
|
|
4059
|
+
*/
|
|
4060
|
+
static switchResultAfterCommand(pane, requested) {
|
|
4061
|
+
const rows = pane.replace(/\r/g, "").split("\n");
|
|
4062
|
+
let last = -1;
|
|
4063
|
+
for (let i = rows.length - 1; i >= 0; i--)
|
|
4064
|
+
if (/^❯[ \t\u00a0]+\S/.test(rows[i])) {
|
|
4065
|
+
last = i;
|
|
4066
|
+
break;
|
|
4067
|
+
}
|
|
4068
|
+
if (last < 0 || rows[last].replace(/^❯[ \t\u00a0]+/, "").trimEnd() !== `/model ${requested}`)
|
|
4069
|
+
return false;
|
|
4070
|
+
return rows.slice(last + 1).some(row => /^\s*⎿?\s*Set model to /.test(row));
|
|
3776
4071
|
}
|
|
3777
4072
|
/**
|
|
3778
4073
|
* Confirm a backend's terminal-level /clear prompt after the channel-level
|
|
@@ -4035,6 +4330,13 @@ export class Daemon extends EventEmitter {
|
|
|
4035
4330
|
if (!backend.quitBlockedByDialog(pane))
|
|
4036
4331
|
return false;
|
|
4037
4332
|
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");
|
|
4333
|
+
// #1716: the background work is about to be ended — record it where the
|
|
4334
|
+
// stop/restart reply and the instance topic notice can report it, so the
|
|
4335
|
+
// kill is never silent.
|
|
4336
|
+
try {
|
|
4337
|
+
writeBackgroundWorkEndedMarker(this.instanceDir, reason);
|
|
4338
|
+
}
|
|
4339
|
+
catch { /* reporting only */ }
|
|
4038
4340
|
await this.tmux.sendSpecialKey("Escape").catch(() => false);
|
|
4039
4341
|
return true;
|
|
4040
4342
|
}
|
|
@@ -4236,7 +4538,9 @@ export class Daemon extends EventEmitter {
|
|
|
4236
4538
|
// Bare backend regexes are not proof of a live modal. The canonical
|
|
4237
4539
|
// isActive predicate replaces pattern.test(), preserving table order.
|
|
4238
4540
|
const dialog = dialogs.find(d => d.isActive && (d.inputBlocked || d.blocksDelivery || d.holdOnly)
|
|
4239
|
-
&& Daemon.dialogMatches(d, pane)
|
|
4541
|
+
&& Daemon.dialogMatches(d, pane)
|
|
4542
|
+
// #1690: AgEnD's own switch dialog, being answered right now, is nobody's to answer.
|
|
4543
|
+
&& !(this.ownSwitch && claudeSwitchDialog(pane) !== null));
|
|
4240
4544
|
if (dialog) {
|
|
4241
4545
|
const identity = dialog.requestIdentity?.(pane) ?? pane.replace(/\r/g, "")
|
|
4242
4546
|
.split("\n").map(row => row.replace(/^([ \t]*)[❯›]([ \t]+)(?=\d\.|Yes\b|No\b|Resume\b)/, "$1$2").trimEnd())
|
|
@@ -4252,7 +4556,7 @@ export class Daemon extends EventEmitter {
|
|
|
4252
4556
|
// No captured text leaves this function or the private observer identity.
|
|
4253
4557
|
return evidence ? { ...evidence, identity: createHash("sha256").update(evidence.identity).digest("hex") } : null;
|
|
4254
4558
|
}
|
|
4255
|
-
observeInteractionPane(pane, observedAt, monotonicAt, dialogs = this.
|
|
4559
|
+
observeInteractionPane(pane, observedAt, monotonicAt, dialogs = this.runtimeDialogs(), order = ++this.interactionCaptureSerial, allowConfirmation = true) {
|
|
4256
4560
|
const owner = this.interactionOwner();
|
|
4257
4561
|
const evidence = this.interactionEvidence(pane, dialogs);
|
|
4258
4562
|
if (!this.interactionObservation.observe(evidence, owner, observedAt, monotonicAt, this.interactionOutputRevision !== this.interactionObservedOutputRevision, order))
|
|
@@ -4365,6 +4669,39 @@ export class Daemon extends EventEmitter {
|
|
|
4365
4669
|
this.pauseRequested = false;
|
|
4366
4670
|
return;
|
|
4367
4671
|
}
|
|
4672
|
+
// #1717: an idle pause is AgEnD-initiated and silent — it must not kill
|
|
4673
|
+
// the user's running background work the way an explicit stop may. Defer
|
|
4674
|
+
// this cycle while the backend reports background work; the next idle
|
|
4675
|
+
// tick retries once the task ends. A pane-capture failure proceeds (the
|
|
4676
|
+
// quit-menu handler below stays the backstop), never wedges unpaused.
|
|
4677
|
+
if (this.backend?.backgroundWorkRunning && this.tmux) {
|
|
4678
|
+
const tmux = this.tmux;
|
|
4679
|
+
const windowId = tmux.getWindowId?.();
|
|
4680
|
+
const generation = this.spawnGeneration;
|
|
4681
|
+
let pane = null;
|
|
4682
|
+
try {
|
|
4683
|
+
pane = await tmux.capturePane();
|
|
4684
|
+
}
|
|
4685
|
+
catch { /* capture failed — pause as before, after revalidation */ }
|
|
4686
|
+
// #1730 P2-2: the await yields — the pane read is only valid for the
|
|
4687
|
+
// world it was taken from. Revalidate owner, phase, state and queue
|
|
4688
|
+
// before acting on it; anything that moved defers this cycle.
|
|
4689
|
+
if (this.tmux !== tmux
|
|
4690
|
+
|| this.tmux?.getWindowId?.() !== windowId
|
|
4691
|
+
|| this.spawnGeneration !== generation
|
|
4692
|
+
|| this.getPauseWakeState() !== "active"
|
|
4693
|
+
|| !(this.instanceState === "idle" || (allowStuck && this.instanceState === "stuck"))
|
|
4694
|
+
|| this.pasteQueueDepth > 0) {
|
|
4695
|
+
this.logger.info("Deferring auto-pause: pause preconditions changed during the pane read");
|
|
4696
|
+
this.pauseRequested = false;
|
|
4697
|
+
return;
|
|
4698
|
+
}
|
|
4699
|
+
if (pane !== null && this.backend?.backgroundWorkRunning?.(pane)) {
|
|
4700
|
+
this.logger.info("Deferring auto-pause: background work is still running");
|
|
4701
|
+
this.pauseRequested = false;
|
|
4702
|
+
return;
|
|
4703
|
+
}
|
|
4704
|
+
}
|
|
4368
4705
|
this.pauseWakeState = "pausing";
|
|
4369
4706
|
this.healthCheckPaused = true;
|
|
4370
4707
|
this.freezeRuntimeMonitors();
|
|
@@ -4737,10 +5074,7 @@ export class Daemon extends EventEmitter {
|
|
|
4737
5074
|
backend: resumeBackend ?? "unknown",
|
|
4738
5075
|
cwd: resumeCwd,
|
|
4739
5076
|
armedAt: Date.now(),
|
|
4740
|
-
before: readResumeFingerprint(normalizeResumeBackend(resumeBackend),
|
|
4741
|
-
instanceDir: this.instanceDir,
|
|
4742
|
-
workingDirectory: resumeCwd,
|
|
4743
|
-
}),
|
|
5077
|
+
before: readResumeFingerprint(normalizeResumeBackend(resumeBackend), this.resumeRoots(resumeCwd)),
|
|
4744
5078
|
});
|
|
4745
5079
|
}
|
|
4746
5080
|
}
|
|
@@ -4786,7 +5120,7 @@ export class Daemon extends EventEmitter {
|
|
|
4786
5120
|
&& this.spawnGeneration === gateSpawn
|
|
4787
5121
|
&& this.launchFenceEpoch === gateFence;
|
|
4788
5122
|
const backend = this.resolveResumeBackend() ?? normalizeResumeBackend(marker.backend);
|
|
4789
|
-
const roots =
|
|
5123
|
+
const roots = this.resumeRoots(this.config.working_directory ?? "");
|
|
4790
5124
|
// This boot's CLI spawn is what the daemon itself caused: pre-restart
|
|
4791
5125
|
// writes (including pre-crash progress) are excluded, and the flush
|
|
4792
5126
|
// grace holds the decision until post-spawn writes are observable.
|
|
@@ -5089,8 +5423,9 @@ export class Daemon extends EventEmitter {
|
|
|
5089
5423
|
* every delivery parked behind it. It recovers when the backend sees its own live, EMPTY composer with no busy, queued
|
|
5090
5424
|
* or dialog marker (isStableUnknownLayoutIdlePane) AND the transcript shows no open turn — or, with no transcript to
|
|
5091
5425
|
* ask, that same composer held unchanged for UNKNOWN_LAYOUT_STABLE_MS (#978's rule). An open turn never recovers.
|
|
5426
|
+
* #1865: `hold` makes the closed-turn case wait out that window too (a `working` reading, recovered before the stuck deadline).
|
|
5092
5427
|
*/
|
|
5093
|
-
recoverStuck(pane, snapshot, changeAt, turn) {
|
|
5428
|
+
recoverStuck(pane, snapshot, changeAt, turn, hold = false) {
|
|
5094
5429
|
const machine = this.instanceStateMachine;
|
|
5095
5430
|
const idleComposer = this.backend?.isStableUnknownLayoutIdlePane?.(pane) === true;
|
|
5096
5431
|
// The resume load (#1031: `Resuming session…` over the same composer) is input that is not open yet (#1641 R1).
|
|
@@ -5106,7 +5441,7 @@ export class Daemon extends EventEmitter {
|
|
|
5106
5441
|
this.stuckRecoveryKey = key;
|
|
5107
5442
|
this.stuckRecoverySince = mono;
|
|
5108
5443
|
}
|
|
5109
|
-
if (turn === "unknown" && mono - this.stuckRecoverySince < UNKNOWN_LAYOUT_STABLE_MS)
|
|
5444
|
+
if ((turn === "unknown" || hold) && mono - this.stuckRecoverySince < UNKNOWN_LAYOUT_STABLE_MS)
|
|
5110
5445
|
return snapshot;
|
|
5111
5446
|
// A footer this build cannot read stays unreadable: the same still pane reads stuck again at every sweep and is
|
|
5112
5447
|
// recovered again. Only the edge into idle is news.
|
|
@@ -5515,6 +5850,8 @@ export class Daemon extends EventEmitter {
|
|
|
5515
5850
|
return;
|
|
5516
5851
|
if (!this.observeInteractionPane(pane, captureStartedAt, interactionCaptureAt, undefined, interactionCaptureOrder, reason !== "interaction_confirmation"))
|
|
5517
5852
|
return;
|
|
5853
|
+
// #1691: an accepted capture of this launch is a fresh read for the unrecognised-prompt evidence too (#1695 r3).
|
|
5854
|
+
this.noteFreshPaneRead(pane, interactionCaptureAt);
|
|
5518
5855
|
this.updateInputBlockedState(pane);
|
|
5519
5856
|
// Advance the shared 10s timer only for a fresh capture of this launch.
|
|
5520
5857
|
const deliveryFallback = deliveryCandidate === true
|
|
@@ -5673,6 +6010,13 @@ export class Daemon extends EventEmitter {
|
|
|
5673
6010
|
}
|
|
5674
6011
|
if (snapshot.state === "stuck")
|
|
5675
6012
|
snapshot = this.recoverStuck(pane, snapshot, observedChangeAt, transcriptTurn);
|
|
6013
|
+
// #1865: a resumed codex can paint its footer without the Context item (sol after the beta.13 restart), so its
|
|
6014
|
+
// idle composer reads `working` for the whole stuck window. Its own rollout says no turn is open: the sweep needs
|
|
6015
|
+
// no deadline for that — the same composer held still for UNKNOWN_LAYOUT_STABLE_MS is enough.
|
|
6016
|
+
else if (snapshot.state === "working" && reason === "safety_sweep" && transcriptTurn === "closed"
|
|
6017
|
+
&& this.backend?.binaryName === "codex") {
|
|
6018
|
+
snapshot = this.recoverStuck(pane, snapshot, observedChangeAt, transcriptTurn, true);
|
|
6019
|
+
}
|
|
5676
6020
|
this.applyInstanceStateSnapshot(snapshot, pane);
|
|
5677
6021
|
// Backends without a transcript feed can still say what they are doing, if
|
|
5678
6022
|
// their TUI names it. Free-riding on a capture that already happened: no
|
|
@@ -6083,11 +6427,7 @@ export class Daemon extends EventEmitter {
|
|
|
6083
6427
|
* is a durable delivery) must LEAD the persisted user message — restart reconciliation recognises a delivery only by a
|
|
6084
6428
|
* marker at the start of the body (#1197) — so the banner goes right after it, not in front of it.
|
|
6085
6429
|
*/
|
|
6086
|
-
static steerEnvelope(inbound) {
|
|
6087
|
-
const banner = "[STEERING — mid-task course correction. Fold this into the CURRENT work if one is active.]\n";
|
|
6088
|
-
const marker = inbound.match(/^\[agend-delivery-id:[^\]\r\n]+\]\r?\n/);
|
|
6089
|
-
return marker ? marker[0] + banner + inbound.slice(marker[0].length) : banner + inbound;
|
|
6090
|
-
}
|
|
6430
|
+
static steerEnvelope(inbound) { return steerInboundEnvelope(inbound); }
|
|
6091
6431
|
/**
|
|
6092
6432
|
* The one place an inbound message grows its metadata wrapper ([user:]/
|
|
6093
6433
|
* [from:] prefix, pending reactions, handoff metadata, reply instructions).
|
|
@@ -6098,35 +6438,65 @@ export class Daemon extends EventEmitter {
|
|
|
6098
6438
|
/** #1650: how this instance talks to the fleet — MCP tools, or agend-agent commands (agent_mode: cli). */
|
|
6099
6439
|
agentMode() { return this.config.agent_mode === "cli" ? "cli" : "mcp"; }
|
|
6100
6440
|
formatInboundMessage(content, meta) {
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
let formatted;
|
|
6104
|
-
if (
|
|
6105
|
-
formatted
|
|
6441
|
+
if (!meta.from_instance)
|
|
6442
|
+
return formatHumanInboundMessage(content, meta, this.agentMode());
|
|
6443
|
+
let formatted = formatCrossInstanceInboundMessage(content, meta, this.agentMode());
|
|
6444
|
+
if (meta.reply_to_text)
|
|
6445
|
+
formatted += '\n(reply_to: "' + meta.reply_to_text + '")';
|
|
6446
|
+
if (meta.delivery_id)
|
|
6447
|
+
formatted = "[agend-delivery-id:" + meta.delivery_id + "]\n" + formatted;
|
|
6448
|
+
return formatted;
|
|
6449
|
+
}
|
|
6450
|
+
/** Pure cached owner: no pane/process query on ingress. */
|
|
6451
|
+
captureHumanReceiptOwner() {
|
|
6452
|
+
try {
|
|
6453
|
+
if (this.deliveryWritesStopping || this.spawning || !this.tmux)
|
|
6454
|
+
return null;
|
|
6455
|
+
const o = this.interactionOwner();
|
|
6456
|
+
const window = this.tmux.getWindowId();
|
|
6457
|
+
if (!window || !o.bootId || ![o.spawnGeneration, o.launchAttempt, o.launchFenceEpoch, this.deliveryEpoch]
|
|
6458
|
+
.every(n => Number.isSafeInteger(n) && n >= 0))
|
|
6459
|
+
return null;
|
|
6460
|
+
return JSON.stringify([o.bootId, o.spawnGeneration, o.launchAttempt, o.launchFenceEpoch,
|
|
6461
|
+
this.deliveryEpoch, humanOwnerObjectId(this.tmux), window]);
|
|
6106
6462
|
}
|
|
6107
|
-
|
|
6108
|
-
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
}
|
|
6126
|
-
|
|
6127
|
-
|
|
6463
|
+
catch {
|
|
6464
|
+
return null;
|
|
6465
|
+
}
|
|
6466
|
+
}
|
|
6467
|
+
/** The same formatter/sanitizer used by the actual write; snapshot only. */
|
|
6468
|
+
humanReceiptWire(content, meta, kind) {
|
|
6469
|
+
let wire = kind === "raw_paste" ? content
|
|
6470
|
+
: !meta.from_instance && content.startsWith("/raw ") && kind === "fleet_inbound"
|
|
6471
|
+
? content.slice(5) : this.formatInboundMessage(content, meta);
|
|
6472
|
+
if (kind === "steer")
|
|
6473
|
+
wire = Daemon.steerEnvelope(wire);
|
|
6474
|
+
else if (kind === "btw")
|
|
6475
|
+
wire = BTW_INBOUND_PREFIX + wire;
|
|
6476
|
+
return prepareDeliveryWire(wire, kind === "raw_paste");
|
|
6477
|
+
}
|
|
6478
|
+
humanReceiptAttempt(meta, wire) {
|
|
6479
|
+
const id = meta.human_receipt_id;
|
|
6480
|
+
const daemonOwner = meta.human_daemon_owner;
|
|
6481
|
+
if (!id || !/^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/.test(id) || meta.verification_id !== "hmsg-" + id
|
|
6482
|
+
|| !daemonOwner || !meta.human_owner_key || !/^[a-f0-9]{64}$/.test(meta.human_wire_sha256 ?? "")
|
|
6483
|
+
|| humanBodyHash(prepareDeliveryWire(wire)) !== meta.human_wire_sha256)
|
|
6484
|
+
return undefined;
|
|
6485
|
+
return { receiptId: id, verificationId: meta.verification_id, expectedWireHash: meta.human_wire_sha256,
|
|
6486
|
+
ownerKey: meta.human_owner_key, current: () => this.captureHumanReceiptOwner() === daemonOwner };
|
|
6487
|
+
}
|
|
6488
|
+
finishHumanReceipt(meta, verdict) {
|
|
6489
|
+
if (!meta.human_receipt_id || !meta.human_owner_key)
|
|
6490
|
+
return;
|
|
6491
|
+
const attempt = verdict.humanReceipt;
|
|
6492
|
+
const proof = attempt?.current() && !["unverified", "output-edge", "unverifiable"].includes(verdict.proof ?? "")
|
|
6493
|
+
? attempt.proof ?? null : null;
|
|
6494
|
+
try {
|
|
6495
|
+
this.emit("human_receipt_result", { receipt_id: meta.human_receipt_id, owner_key: meta.human_owner_key, proof });
|
|
6496
|
+
}
|
|
6497
|
+
catch (err) {
|
|
6498
|
+
this.logger.error({ err }, "Could not publish human delivery evidence");
|
|
6128
6499
|
}
|
|
6129
|
-
return formatted;
|
|
6130
6500
|
}
|
|
6131
6501
|
/**
|
|
6132
6502
|
* Whether ONE delivery reached a verdict, carried by that delivery.
|
|
@@ -6219,12 +6589,13 @@ export class Daemon extends EventEmitter {
|
|
|
6219
6589
|
this.deferDurableDelivery(meta, "delivery epoch changed while waking for steer");
|
|
6220
6590
|
return;
|
|
6221
6591
|
}
|
|
6222
|
-
const verdict = { reached: false };
|
|
6592
|
+
const verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
|
|
6223
6593
|
try {
|
|
6224
6594
|
if (await this.deliverMessage(formatted, status, {
|
|
6225
6595
|
steer: true,
|
|
6226
6596
|
deliveryEpoch,
|
|
6227
6597
|
submissionId: meta.message_id,
|
|
6598
|
+
...(meta.verification_id ? { humanVerificationId: meta.verification_id } : {}),
|
|
6228
6599
|
verdict,
|
|
6229
6600
|
durableAttempt: durableAttempt || undefined,
|
|
6230
6601
|
})) {
|
|
@@ -6255,6 +6626,9 @@ export class Daemon extends EventEmitter {
|
|
|
6255
6626
|
this.retryDurableDeliveryBeforeBegin(durableAttempt, err.message);
|
|
6256
6627
|
throw err;
|
|
6257
6628
|
}
|
|
6629
|
+
finally {
|
|
6630
|
+
this.finishHumanReceipt(meta, verdict);
|
|
6631
|
+
}
|
|
6258
6632
|
}).catch(err => {
|
|
6259
6633
|
this.deferDurableDelivery(meta, `target wake/steer failed before pane submission: ${err.message}`);
|
|
6260
6634
|
this.logger.warn({ err: err.message }, "steer delivery error");
|
|
@@ -6270,24 +6644,37 @@ export class Daemon extends EventEmitter {
|
|
|
6270
6644
|
* so channel delivery intentionally uses a normal wrapped inbound instead.
|
|
6271
6645
|
*/
|
|
6272
6646
|
btwMessage(content, meta, deliveryEpoch = this.deliveryEpoch) {
|
|
6273
|
-
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
6647
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
6648
|
+
this.finishHumanReceipt(meta, { reached: false });
|
|
6274
6649
|
return;
|
|
6650
|
+
}
|
|
6275
6651
|
this.updateLastChat(meta.chat_id, meta.thread_id, meta.adapter_id);
|
|
6276
6652
|
this.pendingWork.recordInbound();
|
|
6277
6653
|
this.recordRecentUserMessage(content, meta);
|
|
6278
|
-
const formatted =
|
|
6279
|
-
+ this.formatInboundMessage(content, meta);
|
|
6654
|
+
const formatted = BTW_INBOUND_PREFIX + this.formatInboundMessage(content, meta);
|
|
6280
6655
|
const status = channelStatus(meta);
|
|
6281
6656
|
this.steerLock = this.steerLock.then(async () => {
|
|
6282
|
-
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
6657
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
6658
|
+
this.finishHumanReceipt(meta, { reached: false });
|
|
6283
6659
|
return;
|
|
6660
|
+
}
|
|
6284
6661
|
await this.wake();
|
|
6285
|
-
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
6662
|
+
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
6663
|
+
this.finishHumanReceipt(meta, { reached: false });
|
|
6286
6664
|
return;
|
|
6287
|
-
|
|
6288
|
-
|
|
6665
|
+
}
|
|
6666
|
+
const verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
|
|
6667
|
+
try {
|
|
6668
|
+
if (await this.deliverMessage(formatted, status, { steer: true, deliveryEpoch, submissionId: meta.message_id,
|
|
6669
|
+
...(meta.verification_id ? { humanVerificationId: meta.verification_id } : {}), verdict })) {
|
|
6670
|
+
this.markTurnStarted(meta, formatted, deliveryEpoch);
|
|
6671
|
+
}
|
|
6672
|
+
}
|
|
6673
|
+
finally {
|
|
6674
|
+
this.finishHumanReceipt(meta, verdict);
|
|
6289
6675
|
}
|
|
6290
6676
|
}).catch(err => {
|
|
6677
|
+
this.finishHumanReceipt(meta, { reached: false });
|
|
6291
6678
|
this.logger.warn({ err: err.message }, "btw delivery error");
|
|
6292
6679
|
});
|
|
6293
6680
|
}
|
|
@@ -6401,7 +6788,7 @@ export class Daemon extends EventEmitter {
|
|
|
6401
6788
|
// A fresh delivery begins a fresh turn — its bubble must not inherit
|
|
6402
6789
|
// the previous turn's tool list.
|
|
6403
6790
|
this.resetToolProgress();
|
|
6404
|
-
verdict = { reached: false };
|
|
6791
|
+
verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
|
|
6405
6792
|
const durableAttempt = this.durableDeliveryAttempt(meta, content);
|
|
6406
6793
|
if (durableAttempt === false) {
|
|
6407
6794
|
this.deferDurableDelivery(meta, "invalid durable delivery attempt metadata");
|
|
@@ -6411,6 +6798,7 @@ export class Daemon extends EventEmitter {
|
|
|
6411
6798
|
if (await this.deliverMessage(formatted, status, {
|
|
6412
6799
|
deliveryEpoch,
|
|
6413
6800
|
submissionId: meta.message_id,
|
|
6801
|
+
...(meta.verification_id ? { humanVerificationId: meta.verification_id } : {}),
|
|
6414
6802
|
verdict,
|
|
6415
6803
|
durableAttempt: durableAttempt || undefined,
|
|
6416
6804
|
})) {
|
|
@@ -6458,6 +6846,7 @@ export class Daemon extends EventEmitter {
|
|
|
6458
6846
|
this.logger.warn({ err: err.message }, "Durable pane delivery deferred after exception");
|
|
6459
6847
|
}
|
|
6460
6848
|
finally {
|
|
6849
|
+
this.finishHumanReceipt(meta, verdict);
|
|
6461
6850
|
this.pasteQueueDepth--;
|
|
6462
6851
|
}
|
|
6463
6852
|
}).catch(err => {
|
|
@@ -6494,12 +6883,7 @@ export class Daemon extends EventEmitter {
|
|
|
6494
6883
|
&& !this.isDeliveryEpochCurrent(opts.deliveryEpoch);
|
|
6495
6884
|
if (cancelled() || this.stormWindow?.isStopped())
|
|
6496
6885
|
return false;
|
|
6497
|
-
|
|
6498
|
-
const fenceCount = (formatted.match(/```/g) || []).length;
|
|
6499
|
-
if (!opts?.preserveExactBytes && fenceCount % 2 !== 0) {
|
|
6500
|
-
// Odd number of fences = unclosed. Remove all code fences from the message.
|
|
6501
|
-
formatted = formatted.replace(/```/g, "");
|
|
6502
|
-
}
|
|
6886
|
+
formatted = prepareDeliveryWire(formatted, opts?.preserveExactBytes);
|
|
6503
6887
|
// Direct paths such as /steer, /btw, /raw and schedules do not pass the
|
|
6504
6888
|
// FleetManager idle gate. Hold them at the final common pane-write path so
|
|
6505
6889
|
// a tmux storm cannot force any text into a half-booted CLI.
|
|
@@ -6672,7 +7056,10 @@ export class Daemon extends EventEmitter {
|
|
|
6672
7056
|
return stale();
|
|
6673
7057
|
if (residue !== "clear")
|
|
6674
7058
|
return this.failDelivery(verdict, status, "input-residue", residue);
|
|
6675
|
-
const written = await this.writeMessageToPane(formatted, windowId, handingOffToNativeQueue, status, opts?.submissionId, verdict, opts?.durableAttempt, opts?.steer === true, writeCurrent);
|
|
7059
|
+
const written = await this.writeMessageToPane(formatted, windowId, handingOffToNativeQueue, status, opts?.submissionId, verdict, opts?.durableAttempt, opts?.steer === true, writeCurrent, opts?.humanVerificationId, opts?.beforePaneWrite, opts?.lookBeforePaneWrite);
|
|
7060
|
+
// #1690: still under the lock — whatever waits on it next must already see what this write started.
|
|
7061
|
+
if (verdict.paneWriteStarted)
|
|
7062
|
+
opts?.onPaneWritten?.();
|
|
6676
7063
|
// The hand-off's last capture had no box: nothing was begun or written — wait for readiness, then try again.
|
|
6677
7064
|
if (written === "handoff-box-unread")
|
|
6678
7065
|
return "dialog";
|
|
@@ -6720,7 +7107,148 @@ export class Daemon extends EventEmitter {
|
|
|
6720
7107
|
* on purpose ("I trust", "Yes, continue") and would match ordinary prose.
|
|
6721
7108
|
*/
|
|
6722
7109
|
deliveryBlockingDialogs() {
|
|
6723
|
-
return
|
|
7110
|
+
return this.runtimeDialogs().filter(d => d.blocksDelivery || d.holdOnly);
|
|
7111
|
+
}
|
|
7112
|
+
/**
|
|
7113
|
+
* The backend's runtime dialogs, then #1691's catch-all for a prompt none of them knows (last: known ones win). The
|
|
7114
|
+
* catch-all is listed only while such a prompt is active: otherwise it could match nothing, and a backend with no
|
|
7115
|
+
* dialogs keeps its empty list (its delivery path skips the dialog probe, as before).
|
|
7116
|
+
*/
|
|
7117
|
+
runtimeDialogs() {
|
|
7118
|
+
const dialogs = this.backend?.getRuntimeDialogs?.() ?? [];
|
|
7119
|
+
return this.unrecognisedPromptCurrent()?.active ? [...dialogs, this.unrecognisedPromptDialog] : dialogs;
|
|
7120
|
+
}
|
|
7121
|
+
/**
|
|
7122
|
+
* #1691: is the pane at the CLI's own idle composer? Then whatever looks like a prompt above it is transcript output
|
|
7123
|
+
* (an agent's numbered list, a quoted "[y/n]"), not a prompt. The backend's own reading, most specific first.
|
|
7124
|
+
*/
|
|
7125
|
+
cliComposerShowing(pane, shape) {
|
|
7126
|
+
const backend = this.backend;
|
|
7127
|
+
if (!backend)
|
|
7128
|
+
return false;
|
|
7129
|
+
if (backend.readInputRow)
|
|
7130
|
+
return backend.readInputRow(pane) !== null;
|
|
7131
|
+
// muse: its live input box — null when the bottom of the screen is not its layout (#829), e.g. a selection menu.
|
|
7132
|
+
if (backend.inputDraft)
|
|
7133
|
+
return backend.inputDraft(pane) !== null;
|
|
7134
|
+
if (backend.isDeliveryInputReadyPane)
|
|
7135
|
+
return backend.isDeliveryInputReadyPane(pane);
|
|
7136
|
+
const bottom = backend.getBottomReadyPattern?.();
|
|
7137
|
+
if (bottom) {
|
|
7138
|
+
bottom.lastIndex = 0;
|
|
7139
|
+
return bottom.test(pane);
|
|
7140
|
+
}
|
|
7141
|
+
// No reader of its own (opencode, grok, …): its ready chrome BELOW the prompt rows — a status bar or prompt hint drawn
|
|
7142
|
+
// under its input box (opencode's "ctrl+p commands"), which a prompt shown in place of the input has nothing of. Not
|
|
7143
|
+
// the whole pane (a reply quoting "ctrl+p commands" higher up), and never the prompt's own rows: a ready pattern that
|
|
7144
|
+
// is just a cursor glyph would otherwise read the menu's own "❯ 1." as the CLI's prompt (#1695 r2).
|
|
7145
|
+
const rows = pane.replace(/\r/g, "").split("\n").map(r => r.trimEnd());
|
|
7146
|
+
const lastOwn = (shape.block.split("\n").at(-1) ?? "").trim();
|
|
7147
|
+
let end = -1;
|
|
7148
|
+
for (let i = rows.length - 1; i >= 0; i--)
|
|
7149
|
+
if (rows[i].trim() === lastOwn) {
|
|
7150
|
+
end = i;
|
|
7151
|
+
break;
|
|
7152
|
+
}
|
|
7153
|
+
if (end < 0)
|
|
7154
|
+
return false;
|
|
7155
|
+
const below = rows.slice(end + 1).filter(r => r.trim() !== "").slice(0, COMPOSER_TAIL_ROWS).join("\n");
|
|
7156
|
+
const ready = backend.getReadyPattern();
|
|
7157
|
+
ready.lastIndex = 0;
|
|
7158
|
+
return below !== "" && ready.test(below);
|
|
7159
|
+
}
|
|
7160
|
+
/**
|
|
7161
|
+
* #1691: the prompt shape this pane shows, when nothing else explains it (no known dialog, login, idle composer, and
|
|
7162
|
+
* the CLI is not showing its busy indicator).
|
|
7163
|
+
*/
|
|
7164
|
+
unrecognisedPromptCandidate(pane) {
|
|
7165
|
+
const shape = unrecognisedPromptShape(pane);
|
|
7166
|
+
if (!shape)
|
|
7167
|
+
return null;
|
|
7168
|
+
const busy = this.backend?.getBusyPattern?.() ?? null;
|
|
7169
|
+
if (busy) {
|
|
7170
|
+
busy.lastIndex = 0;
|
|
7171
|
+
if (busy.test(pane))
|
|
7172
|
+
return null;
|
|
7173
|
+
}
|
|
7174
|
+
if ((this.backend?.getRuntimeDialogs?.() ?? []).some(dialog => Daemon.dialogMatches(dialog, pane)))
|
|
7175
|
+
return null;
|
|
7176
|
+
if (LOGIN_FLOWS[this.config.backend ?? "claude-code"]?.loginScreenActive?.(pane))
|
|
7177
|
+
return null;
|
|
7178
|
+
if (this.cliComposerShowing(pane, shape))
|
|
7179
|
+
return null;
|
|
7180
|
+
return shape;
|
|
7181
|
+
}
|
|
7182
|
+
/**
|
|
7183
|
+
* #1691, from each runtime scan: the same unexplained prompt block unchanged for UNRECOGNISED_PROMPT_STABLE_MS
|
|
7184
|
+
* becomes active (held, awaiting_input, reported once by trackDialogParked). Any other screen ends it. A turn keeps
|
|
7185
|
+
* changing the pane (spinner, timer, streamed text), so its clock never completes mid-turn.
|
|
7186
|
+
*/
|
|
7187
|
+
trackUnrecognisedPrompt(pane, mono, outputMoved) {
|
|
7188
|
+
if (mono < this.unrecognisedPromptReadAt)
|
|
7189
|
+
return;
|
|
7190
|
+
this.unrecognisedPromptReadAt = mono;
|
|
7191
|
+
const shape = this.unrecognisedPromptCandidate(pane);
|
|
7192
|
+
if (!shape) {
|
|
7193
|
+
this.unrecognisedPrompt = null;
|
|
7194
|
+
return;
|
|
7195
|
+
}
|
|
7196
|
+
const screen = Daemon.stableScreen(pane);
|
|
7197
|
+
const owner = this.interactionOwner();
|
|
7198
|
+
const current = this.unrecognisedPromptCurrent();
|
|
7199
|
+
// Restart the minute on any change: another prompt, motion anywhere on the screen, output that arrived while this
|
|
7200
|
+
// capture was taken, or evidence from another launch (#1695 review).
|
|
7201
|
+
if (!current || current.block !== shape.block || current.screen !== screen || outputMoved) {
|
|
7202
|
+
this.unrecognisedPrompt = { block: shape.block, shape, since: mono, active: false, reportedElsewhere: false, screen, owner };
|
|
7203
|
+
return;
|
|
7204
|
+
}
|
|
7205
|
+
if (!current.active && mono - current.since >= UNRECOGNISED_PROMPT_STABLE_MS) {
|
|
7206
|
+
current.active = true;
|
|
7207
|
+
// A [y/n] the interactive-prompt detector knows was already told (interactive_prompt, after 10 s): held and
|
|
7208
|
+
// awaiting input from here on, but not reported a second time.
|
|
7209
|
+
current.reportedElsewhere = shape.kind === "yes-no" && this.interactivePromptDetector.match(pane) !== null;
|
|
7210
|
+
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)");
|
|
7211
|
+
}
|
|
7212
|
+
}
|
|
7213
|
+
/** #1691: the tracked evidence, if it still belongs to this launch (else it is dropped). */
|
|
7214
|
+
unrecognisedPromptCurrent() {
|
|
7215
|
+
const current = this.unrecognisedPrompt;
|
|
7216
|
+
if (current && (this.runtimeMonitorsFrozen || !sameInteractionOwner(current.owner, this.interactionOwner()))) {
|
|
7217
|
+
this.unrecognisedPrompt = null;
|
|
7218
|
+
return null;
|
|
7219
|
+
}
|
|
7220
|
+
return current;
|
|
7221
|
+
}
|
|
7222
|
+
/**
|
|
7223
|
+
* #1691: the catch-all's predicate — the tracked prompt is active and this pane still shows exactly it (any reader:
|
|
7224
|
+
* the scan, a probe, an interaction re-capture). Ending the evidence on a read without it is noteFreshPaneRead's.
|
|
7225
|
+
*/
|
|
7226
|
+
unrecognisedPromptActive(pane) {
|
|
7227
|
+
const current = this.unrecognisedPromptCurrent();
|
|
7228
|
+
return !!current?.active && this.unrecognisedPromptCandidate(pane)?.block === current.block && Daemon.stableScreen(pane) === current.screen;
|
|
7229
|
+
}
|
|
7230
|
+
/**
|
|
7231
|
+
* #1691: a fresh pane read outside the scan (a delivery probe, an accepted state re-capture), taken at `mono`. When it
|
|
7232
|
+
* no longer shows exactly the tracked prompt, the evidence ends — counting or active, so a return needs a fresh minute
|
|
7233
|
+
* (#1695 r2). When it does, it is the newest confirmation. Either way only a read newer than the newest read that
|
|
7234
|
+
* decided the evidence counts: an older one, finishing late, says nothing about now (#1695 r3, r4).
|
|
7235
|
+
*/
|
|
7236
|
+
noteFreshPaneRead(pane, mono) {
|
|
7237
|
+
if (mono < this.unrecognisedPromptReadAt)
|
|
7238
|
+
return;
|
|
7239
|
+
// Advanced even with nothing tracked: a scan whose capture is older must not start evidence this read contradicts
|
|
7240
|
+
// (#1695 r5).
|
|
7241
|
+
this.unrecognisedPromptReadAt = mono;
|
|
7242
|
+
const current = this.unrecognisedPromptCurrent();
|
|
7243
|
+
if (!current)
|
|
7244
|
+
return;
|
|
7245
|
+
if (this.unrecognisedPromptCandidate(pane)?.block === current.block && Daemon.stableScreen(pane) === current.screen)
|
|
7246
|
+
return;
|
|
7247
|
+
this.unrecognisedPrompt = null;
|
|
7248
|
+
}
|
|
7249
|
+
/** #1691: the screen as the stability clock compares it (trailing blanks are not motion). */
|
|
7250
|
+
static stableScreen(pane) {
|
|
7251
|
+
return pane.replace(/\r/g, "").split("\n").map(r => r.trimEnd()).join("\n").trimEnd();
|
|
6724
7252
|
}
|
|
6725
7253
|
/** Passive input-unavailable phases relevant to the current spawn only. */
|
|
6726
7254
|
guardedInputTransients() {
|
|
@@ -6888,7 +7416,7 @@ export class Daemon extends EventEmitter {
|
|
|
6888
7416
|
return dialog.isActive ? dialog.isActive(pane) : dialog.pattern.test(pane);
|
|
6889
7417
|
}
|
|
6890
7418
|
/** Refresh the separate stdin-blocked observation from one pane capture. */
|
|
6891
|
-
updateInputBlockedState(pane, dialogs = this.
|
|
7419
|
+
updateInputBlockedState(pane, dialogs = this.runtimeDialogs()) {
|
|
6892
7420
|
const active = dialogs.find(dialog => dialog.inputBlocked && Daemon.dialogMatches(dialog, pane)) ?? null;
|
|
6893
7421
|
const nextKey = active ? Daemon.dialogKey(active) : null;
|
|
6894
7422
|
if (nextKey !== this.inputBlockedDialogKey) {
|
|
@@ -6976,9 +7504,15 @@ export class Daemon extends EventEmitter {
|
|
|
6976
7504
|
async probeBlockingDialog() {
|
|
6977
7505
|
if (!this.tmux || !this.backend)
|
|
6978
7506
|
return { state: "clear" };
|
|
7507
|
+
// #1691: a probe is a fresh read — it may end unrecognised-prompt evidence even when no dialog is listed.
|
|
7508
|
+
const tracking = this.unrecognisedPrompt !== null;
|
|
6979
7509
|
const dialogs = this.deliveryBlockingDialogs();
|
|
6980
|
-
|
|
7510
|
+
// #1690: our own switch is being answered — nothing else types into the pane until it has settled.
|
|
7511
|
+
if (this.ownSwitch && dialogs.length > 0)
|
|
7512
|
+
return { state: "dialog", dialog: dialogs[0] };
|
|
7513
|
+
if (dialogs.length === 0 && !tracking)
|
|
6981
7514
|
return { state: "clear" };
|
|
7515
|
+
const probeMono = performance.now();
|
|
6982
7516
|
let pane;
|
|
6983
7517
|
try {
|
|
6984
7518
|
pane = await this.tmux.capturePane();
|
|
@@ -6987,6 +7521,7 @@ export class Daemon extends EventEmitter {
|
|
|
6987
7521
|
this.logger.debug({ err }, "capture-pane failed during the dialog probe — pane state unknown");
|
|
6988
7522
|
return { state: "unknown" };
|
|
6989
7523
|
}
|
|
7524
|
+
this.noteFreshPaneRead(pane, probeMono);
|
|
6990
7525
|
for (const dialog of dialogs) {
|
|
6991
7526
|
if (!Daemon.dialogMatches(dialog, pane))
|
|
6992
7527
|
continue;
|
|
@@ -7196,11 +7731,16 @@ export class Daemon extends EventEmitter {
|
|
|
7196
7731
|
this.dialogParkedReported = false;
|
|
7197
7732
|
return;
|
|
7198
7733
|
}
|
|
7199
|
-
|
|
7734
|
+
// #1691: the catch-all waited its minute before it became active; it is reported on the next scan.
|
|
7735
|
+
const unrecognised = dialog === this.unrecognisedPromptDialog;
|
|
7736
|
+
if (unrecognised && this.unrecognisedPrompt?.reportedElsewhere)
|
|
7737
|
+
this.dialogParkedReported = true;
|
|
7738
|
+
if (!this.dialogParkedReported && Date.now() - this.dialogParkedSince >= (unrecognised ? 0 : DIALOG_PARKED_NOTIFY_MS)) {
|
|
7200
7739
|
this.dialogParkedReported = true;
|
|
7201
7740
|
this.logger.warn({ dialog: dialog.description, parkedForMs: Date.now() - this.dialogParkedSince }, "CLI dialog is still on screen — not auto-answering, reporting for a human");
|
|
7202
7741
|
this.emit("dialog_parked", { name: this.name, description: dialog.description, holdOnly: dialog.holdOnly === true,
|
|
7203
|
-
...(episode !== null && interaction.kind ? { kind: interaction.kind, episode, backend: this.config.backend ?? "claude-code" } : {})
|
|
7742
|
+
...(episode !== null && interaction.kind ? { kind: interaction.kind, episode, backend: this.config.backend ?? "claude-code" } : {}),
|
|
7743
|
+
...(unrecognised ? { unrecognised: true, question: this.unrecognisedPrompt?.shape.question ?? null } : {}) });
|
|
7204
7744
|
}
|
|
7205
7745
|
}
|
|
7206
7746
|
/**
|
|
@@ -7311,7 +7851,7 @@ export class Daemon extends EventEmitter {
|
|
|
7311
7851
|
return false;
|
|
7312
7852
|
const row = norm(shown);
|
|
7313
7853
|
// The marker line holds nothing but the marker: the row is exactly it.
|
|
7314
|
-
if (/^\[agend-delivery-id:[^\]\s]+\]$/.test(first))
|
|
7854
|
+
if (/^\[agend-(?:delivery|human)-id:[^\]\s]+\]$/.test(first))
|
|
7315
7855
|
return row === first;
|
|
7316
7856
|
return row === first || row.startsWith(`${first} `);
|
|
7317
7857
|
}
|
|
@@ -7769,6 +8309,33 @@ export class Daemon extends EventEmitter {
|
|
|
7769
8309
|
this.logger.warn("Could not read the pane after Enter — not confirming the submission");
|
|
7770
8310
|
return sawOutput && residue === "absent";
|
|
7771
8311
|
}
|
|
8312
|
+
/**
|
|
8313
|
+
* #1647: whether a raw paste on a pane with no transcript (kiro) was taken: its text gone from the input row (or the
|
|
8314
|
+
* CLI's own busy marker shown instead — pasteResidue's rule) with output after the Enter. That is the confirmation's
|
|
8315
|
+
* own rule, looked at more than once: kiro repaints its composer late, so the single look right after Enter misread
|
|
8316
|
+
* pastes it took. A generic `working` reading is not evidence (#1663 P1-3: a repaint looks like work).
|
|
8317
|
+
*
|
|
8318
|
+
* Bounded by the budget that confirmation already has, measured from the Enter (#1663 P2-1): the busy wait plus the
|
|
8319
|
+
* post-Enter proof window. Every reading is asked again after its await — still this write's owner (`current`: the
|
|
8320
|
+
* caller's fence, no stop, the same tmux and spawn, #1663 P2-2) and still inside the budget — before it counts; a late
|
|
8321
|
+
* or fenced answer proves nothing. Null: not shown — the caller settles uncertain, never a second Enter.
|
|
8322
|
+
*/
|
|
8323
|
+
async rawPasteTakenOnPane(windowId, enterAt, enterMono, formatted, current) {
|
|
8324
|
+
const deadline = enterMono + CONFIRM_BUSY_POLLS * CONFIRM_BUSY_POLL_MS + POST_ENTER_PROOF_WINDOW_MS;
|
|
8325
|
+
const live = () => current() && performance.now() <= deadline;
|
|
8326
|
+
for (;;) {
|
|
8327
|
+
if (!live())
|
|
8328
|
+
return null;
|
|
8329
|
+
if (this.controlClient?.hasOutputSince?.(windowId, enterAt)) {
|
|
8330
|
+
const residue = await this.pasteResidue(formatted);
|
|
8331
|
+
if (!live())
|
|
8332
|
+
return null;
|
|
8333
|
+
if (residue === "absent")
|
|
8334
|
+
return RAW_PASTE_LEFT_INPUT;
|
|
8335
|
+
}
|
|
8336
|
+
await new Promise(r => setTimeout(r, POST_ENTER_PROOF_POLL_MS));
|
|
8337
|
+
}
|
|
8338
|
+
}
|
|
7772
8339
|
/** Once Kiro returns to its prompt, its unique envelope id in history proves submission. */
|
|
7773
8340
|
async kiroSubmissionEvidence(windowId, formatted, signature) {
|
|
7774
8341
|
if (!this.tmux)
|
|
@@ -7826,6 +8393,8 @@ export class Daemon extends EventEmitter {
|
|
|
7826
8393
|
if (!current())
|
|
7827
8394
|
return false;
|
|
7828
8395
|
if (evidence === "submitted") {
|
|
8396
|
+
if (pending.signature.unique)
|
|
8397
|
+
pending.signature.humanPositive?.("unique-id-pane");
|
|
7829
8398
|
if (status)
|
|
7830
8399
|
this.emit("message_confirmed", status);
|
|
7831
8400
|
return true;
|
|
@@ -7861,6 +8430,8 @@ export class Daemon extends EventEmitter {
|
|
|
7861
8430
|
if (retry === "cancelled")
|
|
7862
8431
|
return false;
|
|
7863
8432
|
if (retry === "submitted") {
|
|
8433
|
+
if (pending.signature.unique)
|
|
8434
|
+
pending.signature.humanPositive?.("unique-id-pane");
|
|
7864
8435
|
if (status)
|
|
7865
8436
|
this.emit("message_confirmed", status);
|
|
7866
8437
|
return true;
|
|
@@ -8001,9 +8572,28 @@ export class Daemon extends EventEmitter {
|
|
|
8001
8572
|
verdict = { reached: false }, durableAttempt, steer = false,
|
|
8002
8573
|
// #829: the caller's fence, asked once more after the last await before the
|
|
8003
8574
|
// first side effect. Stale means unwritten: false, with no failure verdict.
|
|
8004
|
-
stillCurrent
|
|
8005
|
-
|
|
8575
|
+
stillCurrent, humanVerificationId,
|
|
8576
|
+
// Board 003648ac: called synchronously right before each paste — after the last await and fence of that attempt.
|
|
8577
|
+
beforePaste,
|
|
8578
|
+
// Board 907c7899: awaited each attempt after its last capture, begin and fence; the fence is asked again after it.
|
|
8579
|
+
lookBeforePaste) {
|
|
8580
|
+
const signature = this.submissionSignature(formatted, humanVerificationId ?? submissionId, humanVerificationId !== undefined);
|
|
8006
8581
|
const rawPaste = durableAttempt?.submissionMode === "raw_paste";
|
|
8582
|
+
const humanAttempt = verdict.humanReceipt;
|
|
8583
|
+
const humanMode = rawPaste ? "raw_paste" : steer ? "steer"
|
|
8584
|
+
: handingOffToNativeQueue ? "native_queue_handoff" : "idle_submit";
|
|
8585
|
+
if (humanAttempt && signature.unique && signature.value === "agend-human-id:" + humanAttempt.verificationId
|
|
8586
|
+
&& humanBodyHash(formatted) === humanAttempt.expectedWireHash) {
|
|
8587
|
+
signature.humanPositive = tag => {
|
|
8588
|
+
if (!humanAttempt.current() || (stillCurrent && !stillCurrent()))
|
|
8589
|
+
return;
|
|
8590
|
+
humanAttempt.proof = {
|
|
8591
|
+
receipt_id: humanAttempt.receiptId, verification_id: humanAttempt.verificationId,
|
|
8592
|
+
wire_sha256: humanBodyHash(formatted), owner_key: humanAttempt.ownerKey,
|
|
8593
|
+
mode: humanMode, tag,
|
|
8594
|
+
};
|
|
8595
|
+
};
|
|
8596
|
+
}
|
|
8007
8597
|
let windowId = initialWindowId;
|
|
8008
8598
|
// The spawn this write belongs to: acceptance evidence is only read while it is still the current one (#1197).
|
|
8009
8599
|
const spawnAtWrite = this.spawnGeneration;
|
|
@@ -8081,19 +8671,49 @@ export class Daemon extends EventEmitter {
|
|
|
8081
8671
|
offset: attemptEvidence.transcriptOffset,
|
|
8082
8672
|
};
|
|
8083
8673
|
// From here every proof of this write may also ask the CLI's transcript (the pane can lose the echo to the
|
|
8084
|
-
// reply, or still be painting the paste) — this delivery's exact marker only.
|
|
8085
|
-
|
|
8086
|
-
|
|
8674
|
+
// reply, or still be painting the paste) — this delivery's exact marker only, or a raw paste's exact text (#1647).
|
|
8675
|
+
// A raw paste's text only where the transcript is this instance's alone (RAW_TEXT_TRANSCRIPT_BACKENDS).
|
|
8676
|
+
if (["claude-code", "codex"].includes(attemptEvidence.backend)
|
|
8677
|
+
&& (!rawPaste || RAW_TEXT_TRANSCRIPT_BACKENDS.includes(attemptEvidence.backend))) {
|
|
8678
|
+
signature.transcript = this.transcriptProofFor(verdict.transcriptCheckpoint, rawPaste ? { rawText: formatted } : durableAttempt.deliveryId, spawnAtWrite, stillCurrent, kind => { verdict.transcriptProof = kind; });
|
|
8087
8679
|
}
|
|
8088
8680
|
}
|
|
8089
8681
|
}
|
|
8090
|
-
if (
|
|
8682
|
+
if (lookBeforePaste) {
|
|
8683
|
+
await lookBeforePaste();
|
|
8684
|
+
// The look awaited: this attempt is still this write's — the same fence, tmux and launch — or nothing is written.
|
|
8685
|
+
if (!steerCurrent()) {
|
|
8686
|
+
verdict.fenced = !verdict.durableBeginCommitted;
|
|
8687
|
+
return false;
|
|
8688
|
+
}
|
|
8689
|
+
}
|
|
8690
|
+
if (rawPaste) {
|
|
8091
8691
|
verdict.paneWriteStarted = true;
|
|
8692
|
+
// #1843: this write starts silent work even if its submission proof later stays uncertain.
|
|
8693
|
+
// Retire the preceding human guard before the first write, not after proof: its idle callback must never
|
|
8694
|
+
// turn a silent heartbeat into a channel reply or a reply-required recovery prompt. This may also retire a
|
|
8695
|
+
// genuine missed human reply still awaiting confirmation/recovery; log that tradeoff instead of notifying.
|
|
8696
|
+
if (durableAttempt?.silentSchedule) {
|
|
8697
|
+
this.clearReplyGuardConfirm();
|
|
8698
|
+
const previous = this.turnReplyGuard.snapshot();
|
|
8699
|
+
if (previous) {
|
|
8700
|
+
this.turnReplyGuard.complete(previous.generation);
|
|
8701
|
+
this.logger.info({
|
|
8702
|
+
correlationId: previous.target.correlationId ?? null,
|
|
8703
|
+
generation: previous.generation,
|
|
8704
|
+
phase: previous.phase,
|
|
8705
|
+
reason: "silent_schedule_delivery",
|
|
8706
|
+
deliveryId: durableAttempt.deliveryId,
|
|
8707
|
+
}, "Reply guard retired before silent schedule write");
|
|
8708
|
+
}
|
|
8709
|
+
}
|
|
8710
|
+
}
|
|
8092
8711
|
// For raw commands the paste itself may alter the live composer. Treat a
|
|
8093
8712
|
// failed/throwing write as uncertain rather than retrying bytes into the
|
|
8094
8713
|
// same pane; the write-ahead Enter marker later separates safe pre-Enter
|
|
8095
8714
|
// recovery from a possibly executed command.
|
|
8096
8715
|
const pasteGeneration = this.spawnGeneration;
|
|
8716
|
+
beforePaste?.();
|
|
8097
8717
|
const pasted = await this.tmux.pasteBuffer(formatted);
|
|
8098
8718
|
if (pasted)
|
|
8099
8719
|
this.rememberPaste(formatted, pasteGeneration);
|
|
@@ -8147,6 +8767,7 @@ export class Daemon extends EventEmitter {
|
|
|
8147
8767
|
// positively hold typed text, and after it be the empty placeholder again. The empty placeholder here means the
|
|
8148
8768
|
// paste never reached the box; an unreadable box (a failed or late capture, a dialog) proves nothing: no Enter.
|
|
8149
8769
|
const steerComposerProof = handingOffToNativeQueue && steer && !!this.backend?.readSteerComposer && tmuxAtWrite !== null;
|
|
8770
|
+
let humanComposerOwned = false;
|
|
8150
8771
|
let steerEnterDeadline = Infinity;
|
|
8151
8772
|
if (steerComposerProof) {
|
|
8152
8773
|
const deadline = performance.now() + STEER_PASTE_VISIBLE_MS;
|
|
@@ -8170,10 +8791,13 @@ export class Daemon extends EventEmitter {
|
|
|
8170
8791
|
return this.failDelivery(verdict, status, "steer-paste", before === null ? "box-unread" : `box-still-${before.mode}`);
|
|
8171
8792
|
if (!this.steerPasteIsOurs(before.pane, formatted))
|
|
8172
8793
|
return this.failDelivery(verdict, status, "steer-paste", "box-not-this-paste");
|
|
8794
|
+
const shown = this.backend?.readSteerComposerText?.(before.pane)?.replace(/\s+/g, " ").trim();
|
|
8795
|
+
humanComposerOwned = !!humanAttempt && shown === "[agend-human-id:" + humanAttempt.verificationId + "]";
|
|
8173
8796
|
// …and so is the Enter: the key is sent only while the reading is still within it.
|
|
8174
8797
|
steerEnterDeadline = deadline;
|
|
8175
8798
|
}
|
|
8176
8799
|
let enterAt = Date.now();
|
|
8800
|
+
const enterMono = performance.now();
|
|
8177
8801
|
const steerEnterCurrent = () => steerCurrent() && performance.now() <= steerEnterDeadline;
|
|
8178
8802
|
if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof ? steerEnterCurrent : undefined, durableAttempt))) {
|
|
8179
8803
|
if (steerComposerProof && !steerCurrent()) {
|
|
@@ -8240,6 +8864,9 @@ export class Daemon extends EventEmitter {
|
|
|
8240
8864
|
if (after === "steer" || after === "queue" || after === "idle") {
|
|
8241
8865
|
const probe = await withinMs(this.probeBlockingDialog(), deadline - performance.now());
|
|
8242
8866
|
if (probe?.state === "clear" && performance.now() <= deadline && steerCurrent() && this.getWindowId() === windowId) {
|
|
8867
|
+
if (humanComposerOwned)
|
|
8868
|
+
signature.humanPositive?.(after === "steer" ? "composer-empty-steer"
|
|
8869
|
+
: after === "queue" ? "composer-empty-queue" : "composer-empty-idle");
|
|
8243
8870
|
verdict.phase = "steer-accepted";
|
|
8244
8871
|
verdict.proof = `steer-composer-${after}`;
|
|
8245
8872
|
if (status)
|
|
@@ -8419,12 +9046,20 @@ export class Daemon extends EventEmitter {
|
|
|
8419
9046
|
return { kind: "kiro-pending", windowId, spawnGeneration: this.spawnGeneration, signature };
|
|
8420
9047
|
}
|
|
8421
9048
|
}
|
|
8422
|
-
if (!submitted) {
|
|
8423
|
-
|
|
9049
|
+
if (!submitted && rawPaste) {
|
|
9050
|
+
// #1647: kiro paints output at once but its composer late, so one residue look right after Enter misreads a
|
|
9051
|
+
// paste it took. Keep looking within the confirmation's own budget from the Enter, under this write's fence;
|
|
9052
|
+
// never a second Enter (a raw command must not run twice).
|
|
9053
|
+
const taken = await this.rawPasteTakenOnPane(windowId, enterAt, enterMono, formatted, steerCurrent);
|
|
9054
|
+
if (!taken) {
|
|
8424
9055
|
verdict.phase = "raw-paste-submit-proof";
|
|
8425
9056
|
verdict.proof = "no-positive-acceptance-proof";
|
|
8426
9057
|
return false;
|
|
8427
9058
|
}
|
|
9059
|
+
verdict.proof = taken;
|
|
9060
|
+
submitted = true;
|
|
9061
|
+
}
|
|
9062
|
+
if (!submitted) {
|
|
8428
9063
|
// Legacy system pastes have no unique envelope id. Preserve their
|
|
8429
9064
|
// existing bounded retry until they can be tied to a specific turn.
|
|
8430
9065
|
this.logger.warn("Message not submitted after Enter — waiting for the prompt, then re-sending Enter once");
|
|
@@ -8737,10 +9372,10 @@ export class Daemon extends EventEmitter {
|
|
|
8737
9372
|
* A write's transcript proof: bounded looks from its checkpoint, fenced to the write — its spawn and launch, a stop,
|
|
8738
9373
|
* and the caller's own fence — so a look that lands after any of them is never used (#1380 review).
|
|
8739
9374
|
*/
|
|
8740
|
-
transcriptProofFor(checkpoint,
|
|
9375
|
+
transcriptProofFor(checkpoint, needle, spawnAtWrite, stillCurrent, onProof) {
|
|
8741
9376
|
const launchAtWrite = this.launchFenceEpoch;
|
|
8742
9377
|
return {
|
|
8743
|
-
reader: new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend,
|
|
9378
|
+
reader: new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend, needle),
|
|
8744
9379
|
current: () => !this.deliveryWritesStopping && this.spawnGeneration === spawnAtWrite
|
|
8745
9380
|
&& this.launchFenceEpoch === launchAtWrite && (stillCurrent?.() ?? true),
|
|
8746
9381
|
onProof,
|
|
@@ -8818,8 +9453,11 @@ export class Daemon extends EventEmitter {
|
|
|
8818
9453
|
// the input row is proof in itself — which also means a momentary
|
|
8819
9454
|
// failure to read the pane BEFORE pasting cannot turn a delivered
|
|
8820
9455
|
// message into a re-paste.
|
|
8821
|
-
if (signature.unique && after.payload > 0)
|
|
9456
|
+
if (signature.unique && after.payload > 0) {
|
|
9457
|
+
if (after.inputReadable)
|
|
9458
|
+
signature.humanPositive?.("unique-id-pane");
|
|
8822
9459
|
return this.submittedProof();
|
|
9460
|
+
}
|
|
8823
9461
|
if (unattributedResidue)
|
|
8824
9462
|
return "unverifiable";
|
|
8825
9463
|
// 3. Otherwise the evidence must be NEW relative to the pane as it was
|
|
@@ -8956,7 +9594,7 @@ export class Daemon extends EventEmitter {
|
|
|
8956
9594
|
* paste that never landed. System pastes carry no envelope and fall back to
|
|
8957
9595
|
* the body signature, which the baseline comparison still ties to this paste.
|
|
8958
9596
|
*/
|
|
8959
|
-
submissionSignature(formatted, trustedId) {
|
|
9597
|
+
submissionSignature(formatted, trustedId, verification = false) {
|
|
8960
9598
|
// The id must come from the message's own metadata, never from scanning the
|
|
8961
9599
|
// rendered text: agents and users discuss message ids in the body all the
|
|
8962
9600
|
// time ("check message_id: abc"), and a body-scanned value would match an
|
|
@@ -8965,7 +9603,7 @@ export class Daemon extends EventEmitter {
|
|
|
8965
9603
|
// the handoff metadata identifies THIS delivery.
|
|
8966
9604
|
const id = trustedId?.trim();
|
|
8967
9605
|
if (id)
|
|
8968
|
-
return { value:
|
|
9606
|
+
return { value: (verification ? "agend-human-id:" : "message_id:") + id, unique: true };
|
|
8969
9607
|
return { value: pastedTextSignature(formatted), unique: false };
|
|
8970
9608
|
}
|
|
8971
9609
|
/**
|
|
@@ -9487,6 +10125,11 @@ export class Daemon extends EventEmitter {
|
|
|
9487
10125
|
respond(null, `reply: ${buttons.error}`);
|
|
9488
10126
|
return;
|
|
9489
10127
|
}
|
|
10128
|
+
// #1823: a poll's results need the fleet's store and outbox; as text it would never close.
|
|
10129
|
+
if (args.poll !== undefined && args.poll !== null) {
|
|
10130
|
+
respond(null, "reply: a poll needs AgEnD's fleet; send the question without poll");
|
|
10131
|
+
return;
|
|
10132
|
+
}
|
|
9490
10133
|
if (buttons) {
|
|
9491
10134
|
routed = { ...args, text: `${String(args.text)}\n\n${replyButtonsFallbackText(buttons.buttons)}` };
|
|
9492
10135
|
delete routed.buttons;
|
|
@@ -10072,7 +10715,7 @@ export class Daemon extends EventEmitter {
|
|
|
10072
10715
|
* the same tmux window, not aborted, writes not stopping — re-checked after every await, and between the paste and
|
|
10073
10716
|
* its Enter. The whole step, every await included, ends at one monotonic deadline: a late capture never confirms
|
|
10074
10717
|
* and a late admission never writes; a paste whose Enter would land past it is taken back. Not confirmed in time:
|
|
10075
|
-
* the old setup stays, a launch warning
|
|
10718
|
+
* 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
|
|
10076
10719
|
* silently, confirming and removing nothing. Never throws into the spawn.
|
|
10077
10720
|
*/
|
|
10078
10721
|
async ensureBackendAgent() {
|
|
@@ -10118,8 +10761,10 @@ export class Daemon extends EventEmitter {
|
|
|
10118
10761
|
if (active === sw.agent && (wroteAt === null || capturedAt > wroteAt)) {
|
|
10119
10762
|
if (sw.alreadyConfirmed && wroteAt === null)
|
|
10120
10763
|
return; // already ours, already on record
|
|
10121
|
-
for (const
|
|
10122
|
-
this.
|
|
10764
|
+
for (const { message, quiet } of sw.confirm()) {
|
|
10765
|
+
this.logger.warn({ agent: sw.agent, warning: message }, "The agent switch was confirmed with a warning");
|
|
10766
|
+
this.emit("backend_launch_warning", { name: this.name, message, quiet: quiet === true });
|
|
10767
|
+
}
|
|
10123
10768
|
this.logger.info({ agent: sw.agent, switched: wroteAt !== null }, "The resumed conversation runs as this instance's agent");
|
|
10124
10769
|
return;
|
|
10125
10770
|
}
|
|
@@ -10168,7 +10813,9 @@ export class Daemon extends EventEmitter {
|
|
|
10168
10813
|
if (!fenced())
|
|
10169
10814
|
return; // a stop, pause or respawn: nothing to say, nothing to do
|
|
10170
10815
|
this.logger.warn({ agent: sw.agent, typed: wroteAt !== null }, "The resumed conversation did not switch to this instance's agent in time");
|
|
10171
|
-
|
|
10816
|
+
// #1794: quiet — the conversation keeps running as its saved agent with the shared entries, and the next launch
|
|
10817
|
+
// tries again; the user loses nothing and has nothing to do. Logged above and kept on the Log tab only.
|
|
10818
|
+
this.emit("backend_launch_warning", { name: this.name, message: t("kiro.switch_timeout", Daemon.AGENT_SWITCH_BUDGET_MS / 1000), quiet: true });
|
|
10172
10819
|
}
|
|
10173
10820
|
catch (err) {
|
|
10174
10821
|
this.startupAdmission?.();
|
|
@@ -10627,7 +11274,7 @@ export class Daemon extends EventEmitter {
|
|
|
10627
11274
|
if (launchFence !== this.launchFenceEpoch)
|
|
10628
11275
|
throw new Error("Launch cancelled: the instance was stopped or paused while the launch was being prepared");
|
|
10629
11276
|
this.backend.writeConfig(backendConfig);
|
|
10630
|
-
this.backend.preTrust?.(this.config.working_directory);
|
|
11277
|
+
this.backend.preTrust?.(this.config.working_directory, backendConfig);
|
|
10631
11278
|
// Resolve working directory (e.g. symlink for hidden paths)
|
|
10632
11279
|
const resolvedCwd = this.backend.resolveWorkingDirectory?.(this.config.working_directory, this.name) ?? this.config.working_directory;
|
|
10633
11280
|
// Generate a fresh per-instance agent token each spawn. agent-cli reads
|
|
@@ -10697,6 +11344,11 @@ export class Daemon extends EventEmitter {
|
|
|
10697
11344
|
this.logger.warn({ warning: launchWarning }, "Backend launch fell back");
|
|
10698
11345
|
this.emit("backend_launch_warning", { name: this.name, message: launchWarning });
|
|
10699
11346
|
}
|
|
11347
|
+
// #1811: what the launch left out and why (an effort this CLI or model does not take) — the Log tab, not the chat.
|
|
11348
|
+
for (const note of this.backend.consumeQuietLaunchNotes?.() ?? []) {
|
|
11349
|
+
this.logger.info({ note }, "Launch note");
|
|
11350
|
+
this.emit("backend_launch_warning", { name: this.name, message: note, quiet: true });
|
|
11351
|
+
}
|
|
10700
11352
|
// Ensure tmux session exists (may have been destroyed if all windows died)
|
|
10701
11353
|
await TmuxManager.ensureSession(this.tmuxSessionName);
|
|
10702
11354
|
if (this.stormWindow?.observeServerAlive(await TmuxManager.getServerPid(this.tmuxSessionName))) {
|
|
@@ -11267,6 +11919,14 @@ export class Daemon extends EventEmitter {
|
|
|
11267
11919
|
}, "Snapshot written");
|
|
11268
11920
|
return snapshot;
|
|
11269
11921
|
}
|
|
11922
|
+
/** #1699: the claude config dir this instance's CLI uses — its credential profile's, else exactly as before. */
|
|
11923
|
+
claudeConfigDir() {
|
|
11924
|
+
return claudeConfigDirFor(claudeProfileConfigDir(this.config.backend_options?.["claude-code"]));
|
|
11925
|
+
}
|
|
11926
|
+
/** Where this instance's CLI keeps the conversation a turn resume is judged on (#1699: claude's follows the profile). */
|
|
11927
|
+
resumeRoots(workingDirectory) {
|
|
11928
|
+
return { instanceDir: this.instanceDir, workingDirectory, claudeConfigDir: this.claudeConfigDir() };
|
|
11929
|
+
}
|
|
11270
11930
|
/**
|
|
11271
11931
|
* The credential profile's store this instance's CLI writes to, if any.
|
|
11272
11932
|
*
|