@songsid/agend 2.3.3 → 2.3.4-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/README.zh-TW.md +3 -3
- package/THIRD_PARTY_NOTICES.md +37 -0
- package/dist/agend-home.d.ts +13 -0
- package/dist/agend-home.js +42 -0
- package/dist/agend-home.js.map +1 -1
- package/dist/agent-cli-instructions.md +2 -2
- package/dist/agent-credentials.d.ts +24 -0
- package/dist/agent-credentials.js +66 -0
- package/dist/agent-credentials.js.map +1 -0
- package/dist/agent-endpoint.d.ts +3 -1
- package/dist/agent-endpoint.js +13 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/auth-api.d.ts +16 -3
- package/dist/auth-api.js +32 -8
- package/dist/auth-api.js.map +1 -1
- package/dist/authority/session-authorizations.d.ts +28 -4
- package/dist/authority/session-authorizations.js +63 -17
- package/dist/authority/session-authorizations.js.map +1 -1
- package/dist/authority/web-authority.d.ts +15 -1
- package/dist/authority/web-authority.js +27 -10
- package/dist/authority/web-authority.js.map +1 -1
- package/dist/backend/antigravity.js +3 -1
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/claude-code.d.ts +3 -0
- package/dist/backend/claude-code.js +100 -5
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-footer-hints.d.ts +4 -0
- package/dist/backend/codex-footer-hints.js +27 -0
- package/dist/backend/codex-footer-hints.js.map +1 -0
- package/dist/backend/codex-session-lookup.d.ts +35 -0
- package/dist/backend/codex-session-lookup.js +93 -0
- package/dist/backend/codex-session-lookup.js.map +1 -1
- package/dist/backend/codex.d.ts +27 -0
- package/dist/backend/codex.js +136 -23
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro-identity.d.ts +2 -0
- package/dist/backend/kiro-identity.js +13 -6
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro.d.ts +3 -0
- package/dist/backend/kiro.js +68 -5
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.js +5 -3
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +13 -1
- package/dist/backend/types.js +3 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/backend-availability.d.ts +6 -0
- package/dist/backend-availability.js +13 -5
- package/dist/backend-availability.js.map +1 -1
- package/dist/cache-ledger.d.ts +85 -0
- package/dist/cache-ledger.js +236 -1
- package/dist/cache-ledger.js.map +1 -1
- package/dist/cache-service.d.ts +5 -0
- package/dist/cache-service.js +16 -4
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/adapters/discord.js +27 -4
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +51 -0
- package/dist/channel/adapters/telegram.js +238 -18
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/inbox-temp-name.d.ts +9 -0
- package/dist/channel/inbox-temp-name.js +13 -0
- package/dist/channel/inbox-temp-name.js.map +1 -0
- package/dist/channel/mcp-tools.js +3 -3
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/types.d.ts +2 -0
- package/dist/chat-log-ids.d.ts +48 -0
- package/dist/chat-log-ids.js +182 -0
- package/dist/chat-log-ids.js.map +1 -0
- package/dist/classic-channel-manager.d.ts +8 -1
- package/dist/classic-channel-manager.js +24 -2
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +4 -6
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +18 -1
- package/dist/config-validator.js.map +1 -1
- package/dist/cross-instance-notice.d.ts +10 -0
- package/dist/cross-instance-notice.js +67 -5
- package/dist/cross-instance-notice.js.map +1 -1
- package/dist/daemon.d.ts +52 -0
- package/dist/daemon.js +538 -35
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-consumed-watch.d.ts +23 -1
- package/dist/delivery-consumed-watch.js +30 -3
- package/dist/delivery-consumed-watch.js.map +1 -1
- package/dist/delivery-reconciliation.d.ts +7 -0
- package/dist/delivery-reconciliation.js +9 -1
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/dialog-screen-text.d.ts +16 -0
- package/dist/dialog-screen-text.js +51 -0
- package/dist/dialog-screen-text.js.map +1 -0
- package/dist/env-file.d.ts +2 -0
- package/dist/env-file.js +13 -8
- package/dist/env-file.js.map +1 -1
- package/dist/export-import.d.ts +7 -0
- package/dist/export-import.js +44 -10
- package/dist/export-import.js.map +1 -1
- package/dist/fleet-context.d.ts +2 -0
- package/dist/fleet-manager.d.ts +172 -12
- package/dist/fleet-manager.js +897 -139
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-yaml-patch.d.ts +3 -0
- package/dist/fleet-yaml-patch.js +103 -0
- package/dist/fleet-yaml-patch.js.map +1 -0
- package/dist/general-knowledge/skills/concepts/SKILL.md +393 -0
- package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +2 -1
- package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -1
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +41 -4
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +2 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +14 -4
- package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
- package/dist/install-stream.d.ts +53 -0
- package/dist/install-stream.js +134 -0
- package/dist/install-stream.js.map +1 -0
- package/dist/instance-config-impact.js +1 -0
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +22 -2
- package/dist/instance-lifecycle.js +103 -19
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instance-topic-delete.d.ts +77 -0
- package/dist/instance-topic-delete.js +123 -0
- package/dist/instance-topic-delete.js.map +1 -0
- package/dist/instructions.d.ts +2 -2
- package/dist/instructions.js +2 -2
- package/dist/instructions.js.map +1 -1
- package/dist/interaction-observation.d.ts +1 -0
- package/dist/interaction-observation.js +1 -1
- package/dist/interaction-observation.js.map +1 -1
- package/dist/kiro-engine-status.d.ts +11 -0
- package/dist/kiro-engine-status.js +6 -0
- package/dist/kiro-engine-status.js.map +1 -1
- package/dist/kiro-resume-summary.d.ts +37 -0
- package/dist/kiro-resume-summary.js +85 -0
- package/dist/kiro-resume-summary.js.map +1 -0
- package/dist/locale.js +130 -6
- package/dist/locale.js.map +1 -1
- package/dist/log-service.d.ts +30 -0
- package/dist/log-service.js +36 -3
- package/dist/log-service.js.map +1 -1
- package/dist/log-steps.d.ts +4 -3
- package/dist/log-steps.js +32 -7
- package/dist/log-steps.js.map +1 -1
- package/dist/logger.d.ts +12 -0
- package/dist/logger.js +43 -1
- package/dist/logger.js.map +1 -1
- package/dist/login-controller.js +9 -1
- package/dist/login-controller.js.map +1 -1
- package/dist/login-flows.d.ts +2 -0
- package/dist/login-flows.js +5 -2
- package/dist/login-flows.js.map +1 -1
- package/dist/login-manager.d.ts +6 -0
- package/dist/login-manager.js +10 -1
- package/dist/login-manager.js.map +1 -1
- package/dist/missed-offline.d.ts +25 -0
- package/dist/missed-offline.js +79 -0
- package/dist/missed-offline.js.map +1 -0
- package/dist/needs-you-hub.d.ts +6 -1
- package/dist/needs-you-hub.js +31 -8
- package/dist/needs-you-hub.js.map +1 -1
- package/dist/needs-you.d.ts +22 -2
- package/dist/needs-you.js +17 -3
- package/dist/needs-you.js.map +1 -1
- package/dist/next-steps.d.ts +12 -2
- package/dist/next-steps.js +19 -3
- package/dist/next-steps.js.map +1 -1
- package/dist/office-share-page.d.ts +1 -1
- package/dist/office-share-page.js +6 -2
- package/dist/office-share-page.js.map +1 -1
- package/dist/office-share-service.d.ts +57 -0
- package/dist/office-share-service.js +76 -11
- package/dist/office-share-service.js.map +1 -1
- package/dist/office-share.d.ts +8 -0
- package/dist/office-share.js +34 -0
- package/dist/office-share.js.map +1 -1
- package/dist/outbound-handlers.js +59 -3
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +1 -0
- package/dist/outbound-schemas.js +10 -4
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/package-preimage.js +27 -2
- package/dist/package-preimage.js.map +1 -1
- package/dist/page-share.d.ts +122 -0
- package/dist/page-share.js +199 -0
- package/dist/page-share.js.map +1 -0
- package/dist/public-preview.d.ts +4 -0
- package/dist/public-preview.js +5 -4
- package/dist/public-preview.js.map +1 -1
- package/dist/public-web-gateway.js +5 -0
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/queued-handoffs.d.ts +33 -0
- package/dist/queued-handoffs.js +75 -0
- package/dist/queued-handoffs.js.map +1 -0
- package/dist/quickstart-api.d.ts +5 -0
- package/dist/quickstart-api.js +1 -1
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +40 -0
- package/dist/quickstart.js +174 -28
- package/dist/quickstart.js.map +1 -1
- package/dist/remote-card-answer.js +2 -0
- package/dist/remote-card-answer.js.map +1 -1
- package/dist/remote-card-prompt.js +4 -2
- package/dist/remote-card-prompt.js.map +1 -1
- package/dist/reply-buttons-controller.js +10 -0
- package/dist/reply-buttons-controller.js.map +1 -1
- package/dist/scheduler/db.d.ts +2 -1
- package/dist/scheduler/db.js +23 -9
- package/dist/scheduler/db.js.map +1 -1
- package/dist/selection-card.d.ts +20 -0
- package/dist/selection-card.js +202 -0
- package/dist/selection-card.js.map +1 -0
- package/dist/settings-api.d.ts +13 -0
- package/dist/settings-api.js +16 -1
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +5 -0
- 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/setup-host.js +2 -2
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +2 -2
- package/dist/setup-wizard.js.map +1 -1
- package/dist/stall-reporter.d.ts +21 -0
- package/dist/stall-reporter.js +82 -0
- package/dist/stall-reporter.js.map +1 -0
- package/dist/tmux-control.d.ts +2 -2
- package/dist/tmux-control.js +4 -2
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-exec-helper.d.ts +6 -3
- package/dist/tmux-exec-helper.js +115 -53
- package/dist/tmux-exec-helper.js.map +1 -1
- package/dist/tmux-manager.d.ts +6 -0
- package/dist/tmux-manager.js +17 -0
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tool-permissions.d.ts +5 -2
- package/dist/tool-permissions.js +6 -5
- package/dist/tool-permissions.js.map +1 -1
- package/dist/tool-progress.d.ts +2 -0
- package/dist/tool-progress.js +19 -3
- package/dist/tool-progress.js.map +1 -1
- package/dist/topic-commands.js +4 -0
- package/dist/topic-commands.js.map +1 -1
- package/dist/tunnel/cloudflared-install.d.ts +4 -0
- package/dist/tunnel/cloudflared-install.js +20 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/tunnel/cloudflared.d.ts +8 -1
- package/dist/tunnel/cloudflared.js +34 -6
- package/dist/tunnel/cloudflared.js.map +1 -1
- package/dist/tunnel/lease.d.ts +3 -3
- package/dist/tunnel/lease.js +23 -19
- package/dist/tunnel/lease.js.map +1 -1
- package/dist/tunnel/manager.js +6 -7
- package/dist/tunnel/manager.js.map +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/ui/chat-store.js +28 -4
- package/dist/ui/chat-strings.js +40 -4
- package/dist/ui/chat-thread.js +52 -10
- package/dist/ui/fleet-canvas.js +4 -3
- package/dist/ui/fleet-office-model.js +55 -22
- package/dist/ui/fleet-office-overview.js +6 -38
- package/dist/ui/fleet-office-pack.js +4 -3
- package/dist/ui/fleet-office-skin.js +114 -21
- package/dist/ui/fleet-office-store.js +5 -0
- package/dist/ui/fleet-office-zones.js +2 -2
- package/dist/ui/fleet-office.js +41 -9
- package/dist/ui/fleet-usage.js +90 -27
- package/dist/ui/page-share-dialog.js +191 -0
- package/dist/ui/panel-chat.js +16 -0
- package/dist/ui/panel-log.js +7 -1
- package/dist/ui/panel-needs.js +17 -8
- package/dist/ui/panel-notices.js +98 -0
- package/dist/ui/panel-settings.js +9 -2
- package/dist/ui/preview-pages.js +41 -0
- package/dist/ui/preview-panel-actions.js +161 -0
- package/dist/ui/preview-panel.js +209 -28
- package/dist/ui/preview-versions.js +155 -0
- package/dist/ui/preview.js +88 -3
- package/dist/ui/settings-registry.js +1 -0
- package/dist/ui/settings-strings.js +6 -2
- package/dist/ui/shared/app-i18n.js +6 -6
- package/dist/ui/shared/app-needs.js +1 -1
- package/dist/ui/shared/app-route.js +4 -0
- package/dist/ui/shared/app-shell.js +9 -5
- package/dist/ui/shared/app-store.js +71 -1
- package/dist/ui/shared/app-stream.js +27 -1
- package/dist/ui/shared/app.css +94 -4
- package/dist/ui/shared/app.js +22 -1
- package/dist/ui/shared/office/guild.png +0 -0
- package/dist/ui/shared/office/school.png +0 -0
- package/dist/ui/shared/panel-view.js +339 -17
- package/dist/ui/shared/signin.js +12 -1
- package/dist/ui/shared/ui-icons.js +3 -0
- package/dist/ui/shared/usage-dialog.js +18 -1
- package/dist/ui/shared/view-history.css +17 -0
- package/dist/ui/shared/view-history.js +438 -0
- package/dist/ui/shared/view-strings.js +42 -0
- package/dist/ui/shared/web-authorize.js +3 -1
- package/dist/unattended-install.d.ts +6 -0
- package/dist/unattended-install.js +79 -0
- package/dist/unattended-install.js.map +1 -0
- package/dist/usage/capacity.d.ts +69 -0
- package/dist/usage/capacity.js +119 -0
- package/dist/usage/capacity.js.map +1 -0
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +3 -0
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/profile-timeline.d.ts +16 -0
- package/dist/usage/profile-timeline.js +58 -0
- package/dist/usage/profile-timeline.js.map +1 -0
- package/dist/usage/quota-history.d.ts +43 -0
- package/dist/usage/quota-history.js +168 -0
- package/dist/usage/quota-history.js.map +1 -0
- package/dist/usage/usage-api.d.ts +31 -0
- package/dist/usage/usage-api.js +80 -1
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/usage-report.d.ts +25 -1
- package/dist/usage-report.js +55 -4
- package/dist/usage-report.js.map +1 -1
- package/dist/usage-rollup.d.ts +4 -1
- package/dist/usage-rollup.js +12 -3
- package/dist/usage-rollup.js.map +1 -1
- package/dist/view-api.d.ts +31 -0
- package/dist/view-api.js +186 -41
- package/dist/view-api.js.map +1 -1
- package/dist/view-history.d.ts +84 -0
- package/dist/view-history.js +116 -0
- package/dist/view-history.js.map +1 -0
- package/dist/web-api.d.ts +32 -0
- package/dist/web-api.js +195 -5
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +7 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-chat-history.d.ts +20 -0
- package/dist/web-chat-history.js +21 -0
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-guest-access.js +5 -1
- package/dist/web-guest-access.js.map +1 -1
- package/dist/web-login.d.ts +6 -3
- package/dist/web-login.js +26 -15
- package/dist/web-login.js.map +1 -1
- package/dist/web-notices.d.ts +51 -0
- package/dist/web-notices.js +118 -0
- package/dist/web-notices.js.map +1 -0
- package/dist/web-office-pack-tools.d.ts +1 -1
- package/dist/web-office-pack-tools.js +1 -1
- package/dist/web-office-pack-tools.js.map +1 -1
- package/dist/web-only-fleet.d.ts +22 -0
- package/dist/web-only-fleet.js +13 -0
- package/dist/web-only-fleet.js.map +1 -0
- package/dist/web-preview.d.ts +32 -3
- package/dist/web-preview.js +52 -19
- package/dist/web-preview.js.map +1 -1
- package/dist/web-session-authorization.d.ts +13 -2
- package/dist/web-session-authorization.js +10 -3
- package/dist/web-session-authorization.js.map +1 -1
- package/dist/web-session.d.ts +12 -0
- package/dist/web-session.js +22 -8
- 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 +2 -0
- package/dist/web-shell-routes.js +3 -0
- package/dist/web-shell-routes.js.map +1 -1
- package/launcher/agend-install.cjs +16 -2
- package/launcher/private-home.cjs +1 -1
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dialogScreenText } from "./dialog-screen-text.js";
|
|
1
2
|
import { readActiveKiroModel } from "./backend/kiro.js";
|
|
2
3
|
import { dashboardAccessOf } from "./dashboard-permissions.js";
|
|
3
4
|
import { sendPackSources } from "./web-office-pack-read.js";
|
|
@@ -32,14 +33,14 @@ import { scopedFleetConfigEnvironment } from "./web-experimental.js";
|
|
|
32
33
|
import { readAgentFile } from "./agent-file-read.js";
|
|
33
34
|
import { ASSET_UPLOAD_MAX_BYTES, receivePreview, sendAssetInParts, writePreview } from "./web-asset-parts.js";
|
|
34
35
|
import { LOGIN_FLOWS } from "./login-flows.js";
|
|
35
|
-
import { ProgressAccumulator, summarizeProgress } from "./tool-progress.js";
|
|
36
|
+
import { ProgressAccumulator, isSearchProgram, summarizeProgress } from "./tool-progress.js";
|
|
36
37
|
import { assembleSystemPrompt, resolveWorkflowText } from "./prompt-file-ref.js";
|
|
37
38
|
import { isCrossInstanceVisibility } from "./cross-instance-notice.js";
|
|
38
39
|
import { ContextGuardian } from "./context-guardian.js";
|
|
39
40
|
import { IpcServer } from "./channel/ipc-bridge.js";
|
|
40
41
|
import { daemonBudgetMs } from "./channel/ipc-timeouts.js";
|
|
41
42
|
import { MessageBus } from "./channel/message-bus.js";
|
|
42
|
-
import { shellQuote, UnsupportedCliError, ResumeBlockedError } from "./backend/types.js";
|
|
43
|
+
import { shellQuote, KiroCompatibilityError, UnsupportedCliError, ResumeBlockedError } from "./backend/types.js";
|
|
43
44
|
import { getTmuxSession } from "./config.js";
|
|
44
45
|
import { routeToolCall } from "./channel/tool-router.js";
|
|
45
46
|
import { parseReplyButtons, replyButtonsFallbackText } from "./reply-buttons.js";
|
|
@@ -59,7 +60,8 @@ import { MuseUsageRelay, clearMuseUsageSnapshot } from "./muse-usage-relay.js";
|
|
|
59
60
|
import { deliveryContentDigest } from "./delivery-outbox.js";
|
|
60
61
|
import { consumedWatches } from "./delivery-consumed-watch.js";
|
|
61
62
|
import { queueResumePolicyForAttempt } from "./delivery-queue-evidence.js";
|
|
62
|
-
import { scanTranscriptForDeliveryMarker } from "./delivery-reconciliation.js";
|
|
63
|
+
import { markerConsumed, scanTranscriptForDeliveryMarker } from "./delivery-reconciliation.js";
|
|
64
|
+
import { QUEUED_HANDOFF_MAX_REDELIVERIES, QueuedHandoffStore } from "./queued-handoffs.js";
|
|
63
65
|
import { measureSyncWork, noteSyncWork } from "./sync-work-attribution.js";
|
|
64
66
|
import { TranscriptDeltaReader } from "./transcript-delta-reader.js";
|
|
65
67
|
import { buildResumeContinuation, clearInFlightTurnMarker, consumeInFlightTurnMarker, decideTurnResume, normalizeResumeBackend, readResumeFingerprint, RESUME_BLOCKING_DELIVERY_STATES, writeInFlightTurnMarker, } from "./turn-resume.js";
|
|
@@ -67,8 +69,9 @@ import { codexLiveRowMatches } from "./backend/codex.js";
|
|
|
67
69
|
import { codexPackageProblemMessage } from "./backend/codex-package.js";
|
|
68
70
|
import { claudeConfigDirFor, claudeProfileConfigDir, claudeSwitchDialog, claudeSwitchTargetMatches } from "./backend/claude-code.js";
|
|
69
71
|
import { MuseBackend } from "./backend/muse.js";
|
|
70
|
-
import { InteractionObservation, INTERACTION_CONFIRM_MS, sameInteractionOwner } from "./interaction-observation.js";
|
|
72
|
+
import { BACKEND_LABELS, InteractionObservation, INTERACTION_CONFIRM_MS, sameInteractionOwner } from "./interaction-observation.js";
|
|
71
73
|
import { unrecognisedPromptShape } from "./unrecognised-prompt.js";
|
|
74
|
+
import { selectionCard } from "./selection-card.js";
|
|
72
75
|
const __filename = fileURLToPath(import.meta.url);
|
|
73
76
|
const __dirname = dirname(__filename);
|
|
74
77
|
// Tool routing sets — module-level to avoid re-creation on every handleToolCall
|
|
@@ -149,6 +152,8 @@ export const HANDOFF_RECHECK_MS = 1_000;
|
|
|
149
152
|
export const WAIT_STOP_POLL_MS = 250;
|
|
150
153
|
/** #2770: human messages a cancel dropped are told in one notice, this long after the last of them. */
|
|
151
154
|
export const CANCELLED_HUMANS_NOTICE_MS = 1_000;
|
|
155
|
+
/** #3030: a queued hand-off's watch outlives a long turn (the record outlives the watch in any case). */
|
|
156
|
+
export const QUEUED_HANDOFF_WATCH_LIFETIME_MS = 12 * 60 * 60_000;
|
|
152
157
|
/**
|
|
153
158
|
* An instance publishing more than this many execution-state edges inside the
|
|
154
159
|
* window is flapping; its edges stop being logged one by one (#1116). One
|
|
@@ -341,7 +346,8 @@ export function shellCommandLabel(command) {
|
|
|
341
346
|
if (!program)
|
|
342
347
|
return "";
|
|
343
348
|
const isBareWord = (t) => !!t && /^[A-Za-z][A-Za-z0-9_-]*$/.test(t);
|
|
344
|
-
|
|
349
|
+
// A search program's next word is what it searches for, not a subcommand: `grep <token>` keeps only `grep`.
|
|
350
|
+
const label = isBareWord(tokens[1]) && !isSearchProgram(program) ? `${program} ${tokens[1]}` : program;
|
|
345
351
|
return label.length > 40 ? `${label.slice(0, 39)}…` : label;
|
|
346
352
|
}
|
|
347
353
|
export class PaneStateMachine {
|
|
@@ -530,6 +536,9 @@ function countOccurrences(haystack, needle) {
|
|
|
530
536
|
}
|
|
531
537
|
/** Bottom-ready re-poll cadence for Enter-dropping TUIs once the pane is quiet. */
|
|
532
538
|
const BOTTOM_READY_POLL_MS = 250;
|
|
539
|
+
/** M-2267: how often a pane left in a tmux mode is looked at again, and how long the delivery Enter waits for it to end. */
|
|
540
|
+
const PANE_MODE_POLL_MS = 500;
|
|
541
|
+
const PANE_MODE_ENTER_WAIT_MS = 10 * 60_000;
|
|
533
542
|
/** Consecutive unreadable pane probes tolerated by the delivery gate before the delivery is failed (≈10s at the poll cadence). */
|
|
534
543
|
const DIALOG_PROBE_UNKNOWN_MAX = 40;
|
|
535
544
|
/** Startup dialog scan: poll cadence and wall-clock budget (keys and waits included). */
|
|
@@ -1305,6 +1314,12 @@ export class Daemon extends EventEmitter {
|
|
|
1305
1314
|
holdOnly: true,
|
|
1306
1315
|
isActive: (pane) => this.unrecognisedPromptActive(pane),
|
|
1307
1316
|
requestIdentity: () => this.unrecognisedPrompt?.block ?? null,
|
|
1317
|
+
// #2910 D4: a numbered prompt no table knows (Claude's Edit / Write / MCP tool permission menus among them) is put to
|
|
1318
|
+
// a web-only fleet's dashboard as its own options; still never answered by the scanner.
|
|
1319
|
+
remoteCard: (pane) => {
|
|
1320
|
+
const backend = this.config.backend ?? "claude-code";
|
|
1321
|
+
return selectionCard(pane, Object.hasOwn(BACKEND_LABELS, backend) ? BACKEND_LABELS[backend] : "CLI");
|
|
1322
|
+
},
|
|
1308
1323
|
};
|
|
1309
1324
|
/**
|
|
1310
1325
|
* #1690: AgEnD's own runtime `/model` in flight — from the moment its paste went in until its "Switch model?" dialog
|
|
@@ -1569,6 +1584,12 @@ export class Daemon extends EventEmitter {
|
|
|
1569
1584
|
pendingWhatsNew = null;
|
|
1570
1585
|
pasteQueueDepth = 0;
|
|
1571
1586
|
/** #2770: a person's messages a cancel dropped before they reached the CLI, told together (noteCancelledHuman). */
|
|
1587
|
+
/** #3030: people's messages a CLI never took (or that a restart left unverifiable), told once per batch. */
|
|
1588
|
+
unsettledHandoffs = { "not-taken": [], unverifiable: [] };
|
|
1589
|
+
unsettledHandoffsTimer = null;
|
|
1590
|
+
/** #3030: since when (monotonic) the CLI has been idle without a break; null while it is not. */
|
|
1591
|
+
instanceIdleSinceMono = null;
|
|
1592
|
+
queuedHandoffStore = null;
|
|
1572
1593
|
cancelledHumans = [];
|
|
1573
1594
|
cancelledHumansTimer = null;
|
|
1574
1595
|
firstDeliveryDelay = new FirstDeliveryDelay();
|
|
@@ -1980,6 +2001,9 @@ export class Daemon extends EventEmitter {
|
|
|
1980
2001
|
this.publishWorkingState("turn_closed");
|
|
1981
2002
|
};
|
|
1982
2003
|
}
|
|
2004
|
+
/** Register each issued HTTP credential with the same-process lifecycle owner. */
|
|
2005
|
+
agentCredentialIssuer;
|
|
2006
|
+
setAgentCredentialIssuer(issue) { this.agentCredentialIssuer = issue; }
|
|
1983
2007
|
/** FleetManager and this daemon share a process, so durable begin/ACK is a direct synchronous store call. */
|
|
1984
2008
|
setDeliveryOutboxPort(port) {
|
|
1985
2009
|
this.deliveryOutbox = port;
|
|
@@ -2116,6 +2140,243 @@ export class Daemon extends EventEmitter {
|
|
|
2116
2140
|
logger: this.logger,
|
|
2117
2141
|
});
|
|
2118
2142
|
}
|
|
2143
|
+
handoffStore() {
|
|
2144
|
+
return this.queuedHandoffStore ??= new QueuedHandoffStore(this.instanceDir);
|
|
2145
|
+
}
|
|
2146
|
+
/** #3030: the CLI that holds a queued hand-off; a record is settled only by a later one. */
|
|
2147
|
+
handoffHolder() { return `${this.bootId}:${this.spawnGeneration}`; }
|
|
2148
|
+
/**
|
|
2149
|
+
* #3030: before the paste. A hand-off into a busy Claude Code / Codex queue gets its record — the transcript
|
|
2150
|
+
* checkpoint before the paste, the unique leading marker and what a redelivery needs — or, for a redelivery written
|
|
2151
|
+
* as an ordinary idle submission, its record moves its checkpoint here, so a crash right after this paste is still
|
|
2152
|
+
* judged from the transcript that will show it. A write that did not keep a record runs as before.
|
|
2153
|
+
*/
|
|
2154
|
+
async recordQueuedHandoff(formatted, handingOff, verdict, durableAttempt, humanVerificationId) {
|
|
2155
|
+
const track = verdict.trackHandoff;
|
|
2156
|
+
if (!track)
|
|
2157
|
+
return;
|
|
2158
|
+
if (!handingOff && !track.id) {
|
|
2159
|
+
// An earlier attempt of this delivery was a hand-off; this one is an idle submission.
|
|
2160
|
+
if (verdict.handoff)
|
|
2161
|
+
this.dropQueuedHandoff(verdict);
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
const backend = this.config.backend ?? this.backend?.binaryName ?? "";
|
|
2165
|
+
if (!["claude-code", "codex"].includes(backend))
|
|
2166
|
+
return;
|
|
2167
|
+
const marker = durableAttempt ? `[agend-delivery-id:${durableAttempt.deliveryId}]`
|
|
2168
|
+
: humanVerificationId ? `[agend-human-id:${humanVerificationId}]` : null;
|
|
2169
|
+
if (!marker || !formatted.startsWith(marker))
|
|
2170
|
+
return;
|
|
2171
|
+
let checkpoint = verdict.transcriptCheckpoint ?? null;
|
|
2172
|
+
if (!checkpoint) {
|
|
2173
|
+
try {
|
|
2174
|
+
checkpoint = await this.transcriptMonitor?.reconciliationCheckpoint() ?? null;
|
|
2175
|
+
}
|
|
2176
|
+
catch (err) {
|
|
2177
|
+
this.logger.debug({ err }, "Could not checkpoint the transcript before a queued hand-off");
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
if (!checkpoint)
|
|
2181
|
+
return; // nothing could judge it later: it runs as before (✅ at the hand-off)
|
|
2182
|
+
const entry = {
|
|
2183
|
+
id: track.id ?? randomUUID(), marker, backend, path: checkpoint.path, offset: checkpoint.offset,
|
|
2184
|
+
content: track.content, meta: track.meta, holder: this.handoffHolder(), createdAt: Date.now(),
|
|
2185
|
+
redeliveries: track.redeliveries ?? 0,
|
|
2186
|
+
};
|
|
2187
|
+
try {
|
|
2188
|
+
const dropped = this.handoffStore().put(entry);
|
|
2189
|
+
if (handingOff)
|
|
2190
|
+
verdict.handoff = entry;
|
|
2191
|
+
for (const old of dropped)
|
|
2192
|
+
this.logger.warn({ handoff: old.id, createdAt: old.createdAt }, "Queued hand-off record dropped: too many on record");
|
|
2193
|
+
}
|
|
2194
|
+
catch (err) {
|
|
2195
|
+
this.logger.warn({ err }, "Could not record a queued hand-off; it runs as before (✅ at the hand-off)");
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
/**
|
|
2199
|
+
* #3030: after pushChannelMessage's write. A hand-off on record whose paste went out is watched (✅ when a turn
|
|
2200
|
+
* takes it); one that was never written goes. A redelivery that ended as an ordinary submission is done; one that a
|
|
2201
|
+
* cancel dropped goes too (a cancel is the person's); one stopped before its write stays for the next CLI.
|
|
2202
|
+
*/
|
|
2203
|
+
afterTrackedWrite(meta, verdict, sent, deliveryEpoch) {
|
|
2204
|
+
try {
|
|
2205
|
+
// A stale epoch with writes still allowed is a cancel (the person's); a stop fences writes too, and keeps records.
|
|
2206
|
+
const cancelled = !this.isDeliveryEpochCurrent(deliveryEpoch) && !this.deliveryWritesStopping;
|
|
2207
|
+
if (verdict.handoff) {
|
|
2208
|
+
if (sent || verdict.paneWriteStarted)
|
|
2209
|
+
this.watchQueuedHandoff(verdict.handoff);
|
|
2210
|
+
else if (!verdict.trackHandoff?.id || cancelled)
|
|
2211
|
+
this.dropQueuedHandoff(verdict);
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
const id = meta.handoff_redelivery;
|
|
2215
|
+
if (!id)
|
|
2216
|
+
return;
|
|
2217
|
+
if (sent || verdict.paneWriteStarted || cancelled) {
|
|
2218
|
+
this.handoffStore().remove(id);
|
|
2219
|
+
this.logger.info({ handoff: id, sent, proof: verdict.proof ?? null }, "Queued hand-off redelivered as an ordinary submission");
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
catch (err) {
|
|
2223
|
+
this.logger.warn({ err }, "Could not update the queued hand-off record after its write");
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
redeliveryStillOurs(meta) {
|
|
2227
|
+
const holder = this.handoffHolder();
|
|
2228
|
+
if (meta.handoff_holder !== holder)
|
|
2229
|
+
return false;
|
|
2230
|
+
try {
|
|
2231
|
+
return this.handoffStore().get(meta.handoff_redelivery)?.holder === holder;
|
|
2232
|
+
}
|
|
2233
|
+
catch {
|
|
2234
|
+
return false;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
dropQueuedHandoff(verdict) {
|
|
2238
|
+
const entry = verdict.handoff;
|
|
2239
|
+
verdict.handoff = undefined;
|
|
2240
|
+
if (!entry)
|
|
2241
|
+
return;
|
|
2242
|
+
try {
|
|
2243
|
+
this.handoffStore().remove(entry.id);
|
|
2244
|
+
}
|
|
2245
|
+
catch (err) {
|
|
2246
|
+
this.logger.warn({ err, handoff: entry.id }, "Could not remove a queued hand-off record");
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* #3030: the hand-off's CLI is the current one: watch its transcript for the turn that takes it (✅ then), and for an
|
|
2251
|
+
* idle CLI that never did (reported). A stop ends the watch; the record stays for the next CLI to settle.
|
|
2252
|
+
*/
|
|
2253
|
+
watchQueuedHandoff(entry) {
|
|
2254
|
+
const spawn = this.spawnGeneration, fence = this.launchFenceEpoch, epoch = this.deliveryEpoch;
|
|
2255
|
+
const started = consumedWatches.start({
|
|
2256
|
+
deliveryId: `handoff:${entry.id}`,
|
|
2257
|
+
attemptNo: entry.redeliveries,
|
|
2258
|
+
backend: entry.backend,
|
|
2259
|
+
path: entry.path,
|
|
2260
|
+
offset: entry.offset,
|
|
2261
|
+
marker: entry.marker,
|
|
2262
|
+
// A cancel or stop moves the epoch: the watch ends quietly (a cancel cleared the record, a stop leaves it).
|
|
2263
|
+
current: () => !this.deliveryWritesStopping && spawn === this.spawnGeneration && fence === this.launchFenceEpoch
|
|
2264
|
+
&& epoch === this.deliveryEpoch,
|
|
2265
|
+
consumed: (via) => this.settleQueuedHandoff(entry, "taken", `transcript:${via}`),
|
|
2266
|
+
idleForMs: () => this.instanceIdleSinceMono === null ? null : performance.now() - this.instanceIdleSinceMono,
|
|
2267
|
+
notTaken: () => this.settleQueuedHandoff(entry, "not-taken", "transcript:absent-while-idle"),
|
|
2268
|
+
// A queued message waits for the running turn, however long that is.
|
|
2269
|
+
lifetimeMs: QUEUED_HANDOFF_WATCH_LIFETIME_MS,
|
|
2270
|
+
logger: this.logger,
|
|
2271
|
+
});
|
|
2272
|
+
if (!started) {
|
|
2273
|
+
// No watch (the fleet's cap): ✅ as before; the record still lets a restart judge it.
|
|
2274
|
+
const status = channelStatus(entry.meta);
|
|
2275
|
+
if (status)
|
|
2276
|
+
this.emit("message_confirmed", status);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
/** #3030: a record's end — taken (✅), or never taken / not judgeable (❌, told once per batch). */
|
|
2280
|
+
settleQueuedHandoff(entry, outcome, proof) {
|
|
2281
|
+
try {
|
|
2282
|
+
this.handoffStore().remove(entry.id);
|
|
2283
|
+
}
|
|
2284
|
+
catch (err) {
|
|
2285
|
+
this.logger.warn({ err, handoff: entry.id }, "Could not remove a settled queued hand-off record");
|
|
2286
|
+
}
|
|
2287
|
+
const meta = entry.meta;
|
|
2288
|
+
const status = channelStatus(meta);
|
|
2289
|
+
if (outcome === "taken") {
|
|
2290
|
+
this.logger.info({ handoff: entry.id, proof, redeliveries: entry.redeliveries }, "Queued hand-off taken by the CLI");
|
|
2291
|
+
if (status)
|
|
2292
|
+
this.emit("message_confirmed", status);
|
|
2293
|
+
return;
|
|
2294
|
+
}
|
|
2295
|
+
this.logger.warn({ handoff: entry.id, outcome, proof, redeliveries: entry.redeliveries }, "Queued hand-off was not taken by the CLI");
|
|
2296
|
+
if (meta.from_instance) {
|
|
2297
|
+
this.reportCrossInstanceDeliveryFailure(meta, { reached: true, phase: "handoff-not-consumed", proof: outcome === "not-taken" ? "not-taken" : "unverifiable" });
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
if (!status)
|
|
2301
|
+
return;
|
|
2302
|
+
this.unsettledHandoffs[outcome].push({ chatId: status.chatId, messageId: status.messageId, ...(status.threadId ? { threadId: status.threadId } : {}) });
|
|
2303
|
+
if (this.unsettledHandoffsTimer)
|
|
2304
|
+
clearTimeout(this.unsettledHandoffsTimer);
|
|
2305
|
+
this.unsettledHandoffsTimer = setTimeout(() => {
|
|
2306
|
+
this.unsettledHandoffsTimer = null;
|
|
2307
|
+
for (const reason of ["not-taken", "unverifiable"]) {
|
|
2308
|
+
const messages = this.unsettledHandoffs[reason].splice(0);
|
|
2309
|
+
if (messages.length)
|
|
2310
|
+
this.emit("human_handoffs_unsettled", { reason, messages });
|
|
2311
|
+
}
|
|
2312
|
+
}, CANCELLED_HUMANS_NOTICE_MS);
|
|
2313
|
+
this.unsettledHandoffsTimer.unref?.();
|
|
2314
|
+
}
|
|
2315
|
+
/**
|
|
2316
|
+
* #3030: right after a CLI came up — the records left by CLIs that are gone. Their queue went with them, so each is
|
|
2317
|
+
* judged by its own transcript from its checkpoint: taken there (✅, nothing pasted); proven not taken (the whole
|
|
2318
|
+
* delta read, the marker absent or only enqueued) → redelivered once, with the same marker, through the ordinary
|
|
2319
|
+
* queue; anything that cannot be judged → never pasted, reported. Ownership is evidence: never pasted twice.
|
|
2320
|
+
*/
|
|
2321
|
+
async reconcileQueuedHandoffs() {
|
|
2322
|
+
let entries;
|
|
2323
|
+
try {
|
|
2324
|
+
entries = this.handoffStore().list().filter(e => e.holder !== this.handoffHolder());
|
|
2325
|
+
}
|
|
2326
|
+
catch (err) {
|
|
2327
|
+
this.logger.warn({ err }, "Could not read the queued hand-off records");
|
|
2328
|
+
return;
|
|
2329
|
+
}
|
|
2330
|
+
if (!entries.length)
|
|
2331
|
+
return;
|
|
2332
|
+
// Captured before any await: a reconciliation that outlives its CLI can never claim the next CLI's holder.
|
|
2333
|
+
const holder = this.handoffHolder();
|
|
2334
|
+
const spawn = this.spawnGeneration, fence = this.launchFenceEpoch, epoch = this.deliveryEpoch;
|
|
2335
|
+
const fresh = () => spawn === this.spawnGeneration && fence === this.launchFenceEpoch && epoch === this.deliveryEpoch
|
|
2336
|
+
&& !this.deliveryWritesStopping;
|
|
2337
|
+
let redelivered = 0, taken = 0, unverifiable = 0;
|
|
2338
|
+
for (const entry of entries) {
|
|
2339
|
+
const kind = await scanTranscriptForDeliveryMarker(entry.path, entry.offset, entry.backend, { marker: entry.marker });
|
|
2340
|
+
// This CLI is gone already, or a cancel came: the next CLI settles them (a cancel cleared them).
|
|
2341
|
+
if (!fresh())
|
|
2342
|
+
return;
|
|
2343
|
+
// …and the record is still the one that was read (another reconciliation or a cancel did not change it meanwhile).
|
|
2344
|
+
let current;
|
|
2345
|
+
try {
|
|
2346
|
+
current = this.handoffStore().get(entry.id);
|
|
2347
|
+
}
|
|
2348
|
+
catch {
|
|
2349
|
+
current = undefined;
|
|
2350
|
+
}
|
|
2351
|
+
if (!current || current.holder !== entry.holder || current.redeliveries !== entry.redeliveries)
|
|
2352
|
+
continue;
|
|
2353
|
+
if (markerConsumed(kind)) {
|
|
2354
|
+
taken++;
|
|
2355
|
+
this.settleQueuedHandoff(entry, "taken", `restart-transcript:${kind}`);
|
|
2356
|
+
continue;
|
|
2357
|
+
}
|
|
2358
|
+
if (kind === "unavailable" || entry.redeliveries >= QUEUED_HANDOFF_MAX_REDELIVERIES) {
|
|
2359
|
+
unverifiable++;
|
|
2360
|
+
this.settleQueuedHandoff(entry, "unverifiable", kind === "unavailable" ? "restart-transcript:unavailable" : "redelivery-limit");
|
|
2361
|
+
continue;
|
|
2362
|
+
}
|
|
2363
|
+
// Proven not taken: the CLI that queued it is gone and its transcript, read to the end, never took it.
|
|
2364
|
+
const next = { ...entry, holder, redeliveries: entry.redeliveries + 1 };
|
|
2365
|
+
try {
|
|
2366
|
+
this.handoffStore().put(next);
|
|
2367
|
+
}
|
|
2368
|
+
catch (err) {
|
|
2369
|
+
this.logger.warn({ err, handoff: entry.id }, "Could not update a queued hand-off record; not redelivered now");
|
|
2370
|
+
continue;
|
|
2371
|
+
}
|
|
2372
|
+
redelivered++;
|
|
2373
|
+
// The person's receipt was settled by the first write: the redelivery carries its marker, not its receipt binding.
|
|
2374
|
+
const { human_receipt_id: _r, human_owner_key: _o, human_daemon_owner: _d, human_wire_sha256: _w, ...meta } = entry.meta;
|
|
2375
|
+
this.pushChannelMessage(entry.content, { ...meta, handoff_redelivery: entry.id,
|
|
2376
|
+
handoff_redeliveries: String(next.redeliveries), handoff_holder: next.holder }, undefined, epoch);
|
|
2377
|
+
}
|
|
2378
|
+
this.logger.info({ instance: this.name, redelivered, taken, unverifiable, proof: "transcript" }, "Queued hand-offs left by the previous CLI settled");
|
|
2379
|
+
}
|
|
2119
2380
|
finishDurableSubmission(delivery, verdict) {
|
|
2120
2381
|
// Legacy backends may accept a pane write without enough pane evidence to
|
|
2121
2382
|
// claim submission. Keep that row inspectable instead of recording a false
|
|
@@ -2624,6 +2885,8 @@ export class Daemon extends EventEmitter {
|
|
|
2624
2885
|
this.startErrorMonitor();
|
|
2625
2886
|
}
|
|
2626
2887
|
this.startInstanceStateMonitor();
|
|
2888
|
+
// #3030: messages a gone CLI still had queued — judged by its transcript, redelivered once or reported.
|
|
2889
|
+
void this.reconcileQueuedHandoffs().catch(err => this.logger.warn({ err }, "Queued hand-off reconciliation failed"));
|
|
2627
2890
|
this.logger.info(`${this.name} ready`);
|
|
2628
2891
|
}
|
|
2629
2892
|
/**
|
|
@@ -3098,8 +3361,6 @@ export class Daemon extends EventEmitter {
|
|
|
3098
3361
|
scheduleNext();
|
|
3099
3362
|
return;
|
|
3100
3363
|
}
|
|
3101
|
-
// The CLI owns the MCP server process, so the daemon can only observe it.
|
|
3102
|
-
measureSyncWork("daemon.healthCheck", () => this.checkMcpServerAlive());
|
|
3103
3364
|
// Human-readable backend label for logs (e.g. "claude", "kiro-cli")
|
|
3104
3365
|
const cliLabel = this.backend?.binaryName ?? "CLI";
|
|
3105
3366
|
const healthTmux = this.tmux;
|
|
@@ -3120,6 +3381,9 @@ export class Daemon extends EventEmitter {
|
|
|
3120
3381
|
if (discardStaleHealth())
|
|
3121
3382
|
return;
|
|
3122
3383
|
if (paneStatus?.alive) {
|
|
3384
|
+
// A CLI shutdown also closes its MCP children. Revive tools only for
|
|
3385
|
+
// a positively live pane, never while its exit is being classified.
|
|
3386
|
+
measureSyncWork("daemon.healthCheck", () => this.checkMcpServerAlive());
|
|
3123
3387
|
// Capture only a known primary while it lives. No extra tmux call: PID comes
|
|
3124
3388
|
// from this same status read. Fast/unobserved deaths remain ordinary crashes.
|
|
3125
3389
|
const key = this.oomSessionKey();
|
|
@@ -3166,6 +3430,29 @@ export class Daemon extends EventEmitter {
|
|
|
3166
3430
|
return;
|
|
3167
3431
|
}
|
|
3168
3432
|
}
|
|
3433
|
+
// tmux marks the PTY dead when its fd closes, before SIGCHLD can fill
|
|
3434
|
+
// pane_dead_status. Claude /exit (and any other CLI) can hit this gap.
|
|
3435
|
+
// Give the status four bounded re-probes; missing is never exit zero.
|
|
3436
|
+
if (paneStatus && !paneStatus.alive && paneStatus.exitCode === undefined) {
|
|
3437
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
3438
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
3439
|
+
if (discardStaleHealth())
|
|
3440
|
+
return;
|
|
3441
|
+
const observed = await healthTmux.getPaneStatus();
|
|
3442
|
+
if (discardStaleHealth())
|
|
3443
|
+
return;
|
|
3444
|
+
if (observed)
|
|
3445
|
+
paneStatus = observed;
|
|
3446
|
+
if (paneStatus.alive || paneStatus.exitCode !== undefined)
|
|
3447
|
+
break;
|
|
3448
|
+
}
|
|
3449
|
+
if (paneStatus.alive) {
|
|
3450
|
+
this.windowQueryFailureTicks = 0;
|
|
3451
|
+
this.stormWindow?.noteWindowAlive(this.name);
|
|
3452
|
+
scheduleNext();
|
|
3453
|
+
return;
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3169
3456
|
// paneStatus === null → window gone entirely (e.g. tmux server crash)
|
|
3170
3457
|
// paneStatus.alive === false → pane dead, exit code available
|
|
3171
3458
|
const exitCode = paneStatus?.exitCode;
|
|
@@ -3349,6 +3636,8 @@ export class Daemon extends EventEmitter {
|
|
|
3349
3636
|
try {
|
|
3350
3637
|
await this.spawnClaudeWindow();
|
|
3351
3638
|
this.setProcessStatus("running");
|
|
3639
|
+
// #3030: messages a gone CLI still had queued — judged by its transcript, redelivered once or reported.
|
|
3640
|
+
void this.reconcileQueuedHandoffs().catch(err => this.logger.warn({ err }, "Queued hand-off reconciliation failed"));
|
|
3352
3641
|
this.logger.info("Recovered from background session conflict");
|
|
3353
3642
|
this.emit("crash_respawn", this.name);
|
|
3354
3643
|
}
|
|
@@ -3508,6 +3797,8 @@ export class Daemon extends EventEmitter {
|
|
|
3508
3797
|
catch { /* may not exist */ }
|
|
3509
3798
|
}
|
|
3510
3799
|
this.setProcessStatus("running");
|
|
3800
|
+
// #3030: messages a gone CLI still had queued — judged by its transcript, redelivered once or reported.
|
|
3801
|
+
void this.reconcileQueuedHandoffs().catch(err => this.logger.warn({ err }, "Queued hand-off reconciliation failed"));
|
|
3511
3802
|
this.logger.info({ resumed }, `Respawned ${cliLabel} window after crash`);
|
|
3512
3803
|
this.emit("crash_respawn", this.name);
|
|
3513
3804
|
// Emit the per-instance audit event while the storm is still active
|
|
@@ -3699,7 +3990,9 @@ export class Daemon extends EventEmitter {
|
|
|
3699
3990
|
const interactivePrompt = this.interactivePromptDetector.observe(pane, Date.now(), this.instanceStateLastOutputAt);
|
|
3700
3991
|
if (interactivePrompt) {
|
|
3701
3992
|
this.logger.warn(interactivePrompt, "Interactive terminal prompt is waiting for human input");
|
|
3702
|
-
|
|
3993
|
+
// M-2269: the prompt as it was on screen, from this capture, for the Log.
|
|
3994
|
+
const screen = dialogScreenText(pane);
|
|
3995
|
+
this.emit("interactive_prompt", { name: this.name, ...interactivePrompt, ...(screen ? { screen } : {}) });
|
|
3703
3996
|
// A prompt is not an error and must not enter the PTY recovery gate.
|
|
3704
3997
|
// Continue scanning real errors in this same snapshot.
|
|
3705
3998
|
}
|
|
@@ -3894,7 +4187,7 @@ export class Daemon extends EventEmitter {
|
|
|
3894
4187
|
else if (dialog.verifyAfterKeys && !resolved) {
|
|
3895
4188
|
this.logger.warn({ dialog: dialog.description }, "Safety dialog choice was sent but follow-up verification or notice submission failed");
|
|
3896
4189
|
}
|
|
3897
|
-
this.trackDialogParked(blockingSeen);
|
|
4190
|
+
this.trackDialogParked(blockingSeen, pane);
|
|
3898
4191
|
return; // Dialog handled (or deliberately deferred): skip error checks this cycle
|
|
3899
4192
|
}
|
|
3900
4193
|
// Reached only when no dialog was answered this poll (none on screen, a fenced one, or one waiting out its
|
|
@@ -3904,7 +4197,7 @@ export class Daemon extends EventEmitter {
|
|
|
3904
4197
|
this.dialogAnswers = null;
|
|
3905
4198
|
this.retireParkedNotice("answer");
|
|
3906
4199
|
}
|
|
3907
|
-
this.trackDialogParked(blockingSeen);
|
|
4200
|
+
this.trackDialogParked(blockingSeen, pane);
|
|
3908
4201
|
this.trackRemoteCard(blockingSeen, pane, captureMono);
|
|
3909
4202
|
if (blockingSeen)
|
|
3910
4203
|
return; // held dialog: skip error checks this cycle
|
|
@@ -4202,6 +4495,14 @@ export class Daemon extends EventEmitter {
|
|
|
4202
4495
|
// (Completion paths clear via the guard's onComplete; a cancel with no
|
|
4203
4496
|
// later idle edge would otherwise leave the marker behind.)
|
|
4204
4497
|
clearInFlightTurnMarker(this.instanceDir);
|
|
4498
|
+
// #3030: what the person cancelled is not kept for a redelivery (nor reported as not taken).
|
|
4499
|
+
try {
|
|
4500
|
+
for (const entry of this.handoffStore().list())
|
|
4501
|
+
this.handoffStore().remove(entry.id);
|
|
4502
|
+
}
|
|
4503
|
+
catch (err) {
|
|
4504
|
+
this.logger.warn({ err }, "Could not clear queued hand-off records on cancel");
|
|
4505
|
+
}
|
|
4205
4506
|
this.deliveryEpoch = fleetEpoch === undefined
|
|
4206
4507
|
? this.deliveryEpoch + 1
|
|
4207
4508
|
: Math.max(this.deliveryEpoch, fleetEpoch);
|
|
@@ -5498,6 +5799,10 @@ export class Daemon extends EventEmitter {
|
|
|
5498
5799
|
if (snapshot.state === "working" && this.recoveryBootSettled
|
|
5499
5800
|
&& this.workingRecoveryInputOwner !== JSON.stringify(this.interactionOwner()))
|
|
5500
5801
|
this.recoveryNativeWork = true;
|
|
5802
|
+
if (snapshot.state !== "idle")
|
|
5803
|
+
this.instanceIdleSinceMono = null;
|
|
5804
|
+
else if (this.instanceState !== "idle" || this.instanceIdleSinceMono === null)
|
|
5805
|
+
this.instanceIdleSinceMono = performance.now();
|
|
5501
5806
|
this.instanceState = snapshot.state;
|
|
5502
5807
|
this.acceptedInstanceSnapshot = { ...snapshot };
|
|
5503
5808
|
// Reuse an already-captured pane and the backend's pure parser, never capture for the journal.
|
|
@@ -5609,7 +5914,7 @@ export class Daemon extends EventEmitter {
|
|
|
5609
5914
|
* message can arrive mid-turn and sit queued, and marking at arrival would let
|
|
5610
5915
|
* the PREVIOUS turn's idle edge consume (and reset) the new turn's state.
|
|
5611
5916
|
*/
|
|
5612
|
-
markTurnStarted(meta, deliveredText, deliveryEpoch = this.deliveryEpoch) {
|
|
5917
|
+
markTurnStarted(meta, deliveredText, deliveryEpoch = this.deliveryEpoch, queuedHandoff = false) {
|
|
5613
5918
|
// Cancel may have interrupted an in-flight paste before its promise settled.
|
|
5614
5919
|
// Its late success must not re-arm the turn that the user just cancelled.
|
|
5615
5920
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
@@ -5657,7 +5962,9 @@ export class Daemon extends EventEmitter {
|
|
|
5657
5962
|
// during the continuation boots clean instead of chaining resumes.
|
|
5658
5963
|
// #2544: a reply that settled before this arm already answered the person —
|
|
5659
5964
|
// writing a marker would re-inject a continuation for a finished turn.
|
|
5660
|
-
|
|
5965
|
+
// #3030: a message only queued in the busy CLI has not started a turn: the interrupted turn is still the running one,
|
|
5966
|
+
// and a queued message lost to a restart is the hand-off record's to redeliver, not a continuation's.
|
|
5967
|
+
if (!meta.resumedContinuationOf && !armed.completionDelivered && !queuedHandoff) {
|
|
5661
5968
|
const resumeBackend = this.config.backend ?? this.backend?.binaryName ?? this.runtimeIdentity?.backend;
|
|
5662
5969
|
const resumeCwd = this.config.working_directory ?? "";
|
|
5663
5970
|
writeInFlightTurnMarker(this.instanceDir, {
|
|
@@ -7062,10 +7369,12 @@ export class Daemon extends EventEmitter {
|
|
|
7062
7369
|
// Command name only — never the arguments. See shellCommandLabel.
|
|
7063
7370
|
if (name === "Bash")
|
|
7064
7371
|
return `$ ${shellCommandLabel(String(inp.command ?? ""))}`;
|
|
7372
|
+
// The tool name only — never the search pattern: it can be a literal secret someone is looking for, and this label
|
|
7373
|
+
// reaches the channel's progress line.
|
|
7065
7374
|
if (name === "Glob")
|
|
7066
|
-
return
|
|
7375
|
+
return "Glob";
|
|
7067
7376
|
if (name === "Grep")
|
|
7068
|
-
return
|
|
7377
|
+
return "Grep";
|
|
7069
7378
|
if (name === "Agent")
|
|
7070
7379
|
return "Agent (subagent)";
|
|
7071
7380
|
if (name.startsWith("mcp__agend__"))
|
|
@@ -7436,6 +7745,13 @@ export class Daemon extends EventEmitter {
|
|
|
7436
7745
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
7437
7746
|
this.logger.info("Pending channel delivery dropped by user cancel");
|
|
7438
7747
|
this.deferDurableDelivery(meta, "delivery epoch changed while queued");
|
|
7748
|
+
// #3030: a cancel is the person's — a queued redelivery it dropped is not redelivered later.
|
|
7749
|
+
if (meta.handoff_redelivery && !this.deliveryWritesStopping) {
|
|
7750
|
+
try {
|
|
7751
|
+
this.handoffStore().remove(meta.handoff_redelivery);
|
|
7752
|
+
}
|
|
7753
|
+
catch { /* the next CLI judges it */ }
|
|
7754
|
+
}
|
|
7439
7755
|
return;
|
|
7440
7756
|
}
|
|
7441
7757
|
if (this.config.pre_task_command) {
|
|
@@ -7446,13 +7762,26 @@ export class Daemon extends EventEmitter {
|
|
|
7446
7762
|
// #2800: with this real message, in the same paste — never a paste or turn of its own: after its delivery marker
|
|
7447
7763
|
// (transcript proof anchors on it), before the message. A person's message with a verified receipt is pasted
|
|
7448
7764
|
// byte for byte (its proof is the wire's hash), so the notice waits for the next delivery without one.
|
|
7449
|
-
const whatsNew = this.pendingWhatsNew && !meta.human_receipt_id
|
|
7765
|
+
const whatsNew = this.pendingWhatsNew && !meta.human_receipt_id && !meta.handoff_redelivery
|
|
7766
|
+
? this.currentWhatsNew(this.pendingWhatsNew) : null;
|
|
7450
7767
|
const pasted = whatsNew ? withWhatsNew(formatted, whatsNew.notice) : formatted;
|
|
7451
7768
|
const status = channelStatus(meta);
|
|
7452
7769
|
// A fresh delivery begins a fresh turn — its bubble must not inherit
|
|
7453
7770
|
// the previous turn's tool list.
|
|
7454
7771
|
this.resetToolProgress();
|
|
7455
7772
|
verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
|
|
7773
|
+
// #3030: a hand-off into the CLI's own queue stays on record until a turn takes it (redeliveries included).
|
|
7774
|
+
if (!content.startsWith("/raw ") || meta.from_instance) {
|
|
7775
|
+
const { handoff_redelivery: redeliveryOf, handoff_redeliveries: redeliveries, handoff_holder: _holder, ...kept } = meta;
|
|
7776
|
+
verdict.trackHandoff = { content, meta: kept,
|
|
7777
|
+
...(redeliveryOf ? { id: redeliveryOf, redeliveries: Number(redeliveries) || 0 } : {}) };
|
|
7778
|
+
}
|
|
7779
|
+
// #3030: a redelivery is written only by the CLI it was issued for, and only while its record still says so —
|
|
7780
|
+
// one queued for a CLI that was replaced meanwhile is that next CLI's reconciliation's to judge again.
|
|
7781
|
+
if (meta.handoff_redelivery && !this.redeliveryStillOurs(meta)) {
|
|
7782
|
+
this.logger.info({ handoff: meta.handoff_redelivery }, "Queued hand-off redelivery dropped: its CLI was replaced before the write");
|
|
7783
|
+
return;
|
|
7784
|
+
}
|
|
7456
7785
|
const durableAttempt = this.durableDeliveryAttempt(meta, content);
|
|
7457
7786
|
if (durableAttempt === false) {
|
|
7458
7787
|
this.deferDurableDelivery(meta, "invalid durable delivery attempt metadata");
|
|
@@ -7475,10 +7804,11 @@ export class Daemon extends EventEmitter {
|
|
|
7475
7804
|
if (whatsNew && (sent || verdict.paneWriteStarted))
|
|
7476
7805
|
this.recordWhatsNewSent(whatsNew);
|
|
7477
7806
|
}
|
|
7807
|
+
this.afterTrackedWrite(meta, verdict, !!sent, deliveryEpoch);
|
|
7478
7808
|
if (sent) {
|
|
7479
7809
|
delivered = true;
|
|
7480
7810
|
this.finishDurableSubmission(durable, verdict);
|
|
7481
|
-
this.markTurnStarted(meta, formatted, deliveryEpoch);
|
|
7811
|
+
this.markTurnStarted(meta, formatted, deliveryEpoch, !!verdict.handoff);
|
|
7482
7812
|
}
|
|
7483
7813
|
else if (durableAttempt && verdict.paneWriteStarted) {
|
|
7484
7814
|
this.finishDurableDelivery(durable, "uncertain", `${verdict.phase ?? "post-submit"}:${verdict.proof ?? "unproven"}`, verdict);
|
|
@@ -7761,6 +8091,9 @@ export class Daemon extends EventEmitter {
|
|
|
7761
8091
|
// The hand-off's last capture had no box: nothing was begun or written — wait for readiness, then try again.
|
|
7762
8092
|
if (written === "handoff-box-unread")
|
|
7763
8093
|
return "dialog";
|
|
8094
|
+
// M-2267: the pane is in a tmux mode: nothing written — wait for readiness (it holds while the mode lasts), retry.
|
|
8095
|
+
if (written === "pane-in-mode")
|
|
8096
|
+
return "dialog";
|
|
7764
8097
|
// Fenced before its first write: not attempted — redo after a spawn, else drop without a ❌.
|
|
7765
8098
|
return written === false && verdict.fenced ? stale() : written;
|
|
7766
8099
|
});
|
|
@@ -8234,7 +8567,7 @@ export class Daemon extends EventEmitter {
|
|
|
8234
8567
|
&& this.hasAutoDialogClaim(dialog)
|
|
8235
8568
|
&& dialogs.some(other => other !== dialog && Daemon.dialogMatches(other, pane)))
|
|
8236
8569
|
continue;
|
|
8237
|
-
this.trackDialogParked(dialog);
|
|
8570
|
+
this.trackDialogParked(dialog, pane);
|
|
8238
8571
|
this.trackRemoteCard(dialog, pane, probeMono);
|
|
8239
8572
|
return { state: "dialog", dialog };
|
|
8240
8573
|
}
|
|
@@ -8613,7 +8946,8 @@ export class Daemon extends EventEmitter {
|
|
|
8613
8946
|
this.parkedNoticeOut = null;
|
|
8614
8947
|
this.emit("dialog_unparked", { name: this.name });
|
|
8615
8948
|
}
|
|
8616
|
-
|
|
8949
|
+
/** `pane`: the capture the caller already holds — M-2269 keeps the dialog's text from it for the Log (no new capture). */
|
|
8950
|
+
trackDialogParked(dialog, pane = "") {
|
|
8617
8951
|
if (!dialog) {
|
|
8618
8952
|
if (this.dialogParkedSince !== 0)
|
|
8619
8953
|
this.logger.info({ dialog: this.dialogParkedKey }, "Blocking dialog is gone from the pane");
|
|
@@ -8651,7 +8985,9 @@ export class Daemon extends EventEmitter {
|
|
|
8651
8985
|
this.dialogParkedReported = true;
|
|
8652
8986
|
this.parkedNoticeOut = "parked";
|
|
8653
8987
|
this.logger.warn({ dialog: dialog.description, parkedForMs: Date.now() - this.dialogParkedSince }, "CLI dialog is still on screen — not auto-answering, reporting for a human");
|
|
8988
|
+
const screen = dialogScreenText(pane);
|
|
8654
8989
|
this.emit("dialog_parked", { name: this.name, description: dialog.description, holdOnly: dialog.holdOnly === true,
|
|
8990
|
+
...(screen ? { screen } : {}),
|
|
8655
8991
|
...(dialog.parkedAdvice ? { advice: dialog.parkedAdvice } : {}),
|
|
8656
8992
|
...(episode !== null && interaction.kind ? { kind: interaction.kind, episode, backend: this.config.backend ?? "claude-code" } : {}),
|
|
8657
8993
|
...(unrecognised ? { unrecognised: true, question: this.unrecognisedPrompt?.shape.question ?? null } : {}) });
|
|
@@ -9011,6 +9347,14 @@ export class Daemon extends EventEmitter {
|
|
|
9011
9347
|
: await this.waitForPaneIdleForDelivery(windowId, remaining);
|
|
9012
9348
|
if (idle !== true)
|
|
9013
9349
|
return idle;
|
|
9350
|
+
// M-2267: a person left the pane in a tmux mode (copy-mode): its keys would go to the mode. Hold — never take the
|
|
9351
|
+
// pane out of it — and look again; the delivery goes once the mode ends (or this wait's own deadline).
|
|
9352
|
+
if (await this.paneInTmuxMode("readiness")) {
|
|
9353
|
+
if (stop && await stop())
|
|
9354
|
+
return "stopped";
|
|
9355
|
+
await new Promise(r => setTimeout(r, BOTTOM_READY_POLL_MS));
|
|
9356
|
+
continue;
|
|
9357
|
+
}
|
|
9014
9358
|
// The idle wait just resolved, so re-reading the silence gate would only
|
|
9015
9359
|
// add a stale-cache race; ask the pane the remaining questions directly.
|
|
9016
9360
|
const transient = await this.probeInputTransient();
|
|
@@ -9556,17 +9900,119 @@ export class Daemon extends EventEmitter {
|
|
|
9556
9900
|
* precisely so the lock's scope is visible at the call site rather than being
|
|
9557
9901
|
* an invariant maintained by comments.
|
|
9558
9902
|
*/
|
|
9559
|
-
async sendDeliveryEnter(phase, stillCurrent, durableAttempt) {
|
|
9903
|
+
async sendDeliveryEnter(phase, stillCurrent, durableAttempt, onInputHeld) {
|
|
9560
9904
|
if (!(await this.waitForInputTransientToClear(phase)))
|
|
9561
9905
|
return false;
|
|
9562
|
-
return this.sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt);
|
|
9906
|
+
return this.sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt, onInputHeld);
|
|
9907
|
+
}
|
|
9908
|
+
/**
|
|
9909
|
+
* M-2267 (hmsg-b8a170c0): is the pane in a tmux mode (a person left it in copy-mode)? Logged once per stretch. Unknown
|
|
9910
|
+
* (unreadable) is not a mode: the delivery's own reads decide then, as before.
|
|
9911
|
+
*/
|
|
9912
|
+
paneModeSince = 0;
|
|
9913
|
+
async paneInTmuxMode(phase) {
|
|
9914
|
+
const inMode = await this.tmux?.paneInMode?.();
|
|
9915
|
+
if (inMode !== true) {
|
|
9916
|
+
this.paneModeSince = 0;
|
|
9917
|
+
return false;
|
|
9918
|
+
}
|
|
9919
|
+
if (!this.paneModeSince) {
|
|
9920
|
+
this.paneModeSince = performance.now();
|
|
9921
|
+
this.logger.warn({ phase }, "The pane is in a tmux mode (copy-mode) — delivery waits for it to end; no key is sent to it");
|
|
9922
|
+
}
|
|
9923
|
+
return true;
|
|
9924
|
+
}
|
|
9925
|
+
/** M-2267: wait while the pane is in a tmux mode, until `deadline` (PANE_MODE_ENTER_WAIT_MS) — "clear", "stale" (fenced), "held". */
|
|
9926
|
+
async waitWhilePaneInMode(phase, current, deadline = performance.now() + PANE_MODE_ENTER_WAIT_MS) {
|
|
9927
|
+
for (;;) {
|
|
9928
|
+
const inMode = await this.paneInTmuxMode(phase);
|
|
9929
|
+
if (!current())
|
|
9930
|
+
return "stale";
|
|
9931
|
+
if (!inMode)
|
|
9932
|
+
return "clear";
|
|
9933
|
+
if (performance.now() >= deadline)
|
|
9934
|
+
return "held";
|
|
9935
|
+
await new Promise(r => setTimeout(r, PANE_MODE_POLL_MS));
|
|
9936
|
+
if (!current())
|
|
9937
|
+
return "stale";
|
|
9938
|
+
}
|
|
9563
9939
|
}
|
|
9564
9940
|
/** Write half only: callers must finish the availability wait before any final recovery ownership proof. */
|
|
9565
|
-
async sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt) {
|
|
9566
|
-
// A recovery Enter may have waited for a transient while cancel or spawn
|
|
9567
|
-
//
|
|
9568
|
-
|
|
9941
|
+
async sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt, onInputHeld) {
|
|
9942
|
+
// A recovery Enter may have waited for a transient while cancel or spawn replaced the delivery: check here, and
|
|
9943
|
+
// again after every wait below, at the last point before the tmux write.
|
|
9944
|
+
// M-2290 (#2985 review, Fable): the mode wait can last up to 10 minutes, and a respawn or relaunch reuses the same
|
|
9945
|
+
// tmux object and window — so the spawn generation and launch fence are part of the fence too: an Enter meant for
|
|
9946
|
+
// the CLI this paste went to is never sent to its successor.
|
|
9947
|
+
const tmux = this.tmux;
|
|
9948
|
+
const generation = this.spawnGeneration;
|
|
9949
|
+
const fence = this.launchFenceEpoch;
|
|
9950
|
+
const current = () => this.tmux === tmux && this.spawnGeneration === generation && this.launchFenceEpoch === fence
|
|
9951
|
+
&& !this.deliveryWritesStopping && (!stillCurrent || stillCurrent());
|
|
9952
|
+
if (!current())
|
|
9569
9953
|
return false;
|
|
9954
|
+
// The pasted text may still be in the box: the caller keeps it uncertain (no false failure, no second paste).
|
|
9955
|
+
const hold = () => {
|
|
9956
|
+
onInputHeld?.("pane-in-mode");
|
|
9957
|
+
this.logger.warn({ phase }, "The pane stayed in a tmux mode; Enter withheld");
|
|
9958
|
+
return false;
|
|
9959
|
+
};
|
|
9960
|
+
// M-2267 (#2971): a pane in a tmux mode (a person left it in copy-mode) would take this Enter. Wait for the mode to
|
|
9961
|
+
// end, then look once more within the same deadline — a pane flickering back into a mode is waited out again, never
|
|
9962
|
+
// past the bound. (beta.5: #2958's composer proof that used to sit between the two looks is reverted, decision
|
|
9963
|
+
// f57474aa: it rejected Codex's footer after a resume and while busy. #2957 is open again.)
|
|
9964
|
+
const modeDeadline = performance.now() + PANE_MODE_ENTER_WAIT_MS;
|
|
9965
|
+
for (;;) {
|
|
9966
|
+
const mode = await this.waitWhilePaneInMode(phase, current, modeDeadline);
|
|
9967
|
+
if (mode === "stale")
|
|
9968
|
+
return false;
|
|
9969
|
+
if (mode === "held")
|
|
9970
|
+
return hold();
|
|
9971
|
+
if (await this.paneInTmuxMode(phase)) {
|
|
9972
|
+
if (!current())
|
|
9973
|
+
return false;
|
|
9974
|
+
if (performance.now() >= modeDeadline)
|
|
9975
|
+
return hold();
|
|
9976
|
+
await new Promise(r => setTimeout(r, PANE_MODE_POLL_MS));
|
|
9977
|
+
if (!current())
|
|
9978
|
+
return false;
|
|
9979
|
+
continue;
|
|
9980
|
+
}
|
|
9981
|
+
if (!current())
|
|
9982
|
+
return false;
|
|
9983
|
+
break;
|
|
9984
|
+
}
|
|
9985
|
+
// M-2290 (the simple half of the #2957 redesign, leader xmsg-1793712561468): a dialog the backend RECOGNISES
|
|
9986
|
+
// (runtimeDialogs: a permission card, the dangerous-command card, …) that painted between the paste and this Enter
|
|
9987
|
+
// would take the key as its answer. Read the pane once more: such a dialog withholds the Enter, the caller keeps the
|
|
9988
|
+
// delivery uncertain (Needs-you), and the dialog scanner parks the dialog for a person. Nothing else is judged here
|
|
9989
|
+
// — an unknown or unreadable screen sends, exactly as before: #2958's composer proof (decision f57474aa) is NOT back.
|
|
9990
|
+
// Only a caller that can hold (onInputHeld: the delivery's initial submit, settled uncertain → Needs-you) asks: one
|
|
9991
|
+
// that cannot would read `false` as a failure and paste again. Only the backend's OWN dialogs count — not AgEnD's
|
|
9992
|
+
// unrecognised-prompt guess — so an unknown screen still sends.
|
|
9993
|
+
if (onInputHeld) {
|
|
9994
|
+
let finalPane = null;
|
|
9995
|
+
try {
|
|
9996
|
+
finalPane = await tmux.capturePane();
|
|
9997
|
+
}
|
|
9998
|
+
catch {
|
|
9999
|
+
finalPane = null;
|
|
10000
|
+
}
|
|
10001
|
+
if (!current())
|
|
10002
|
+
return false;
|
|
10003
|
+
let dialog;
|
|
10004
|
+
try {
|
|
10005
|
+
dialog = finalPane === null ? undefined : (this.backend?.getRuntimeDialogs?.() ?? []).find(d => Daemon.dialogMatches(d, finalPane))?.description;
|
|
10006
|
+
}
|
|
10007
|
+
catch {
|
|
10008
|
+
dialog = undefined;
|
|
10009
|
+
}
|
|
10010
|
+
if (dialog !== undefined) {
|
|
10011
|
+
onInputHeld?.("dialog");
|
|
10012
|
+
this.logger.warn({ phase, dialog }, "A dialog is on the pane; the delivery's Enter is withheld (it would answer it)");
|
|
10013
|
+
return false;
|
|
10014
|
+
}
|
|
10015
|
+
}
|
|
9570
10016
|
// W1: persist the "Enter may have happened" boundary before tmux receives
|
|
9571
10017
|
// the key. A crash after this commit is never auto-replayed without a
|
|
9572
10018
|
// positive transcript marker, even if the stale pane still shows composer.
|
|
@@ -9574,7 +10020,7 @@ export class Daemon extends EventEmitter {
|
|
|
9574
10020
|
this.logger.error({ phase, deliveryId: durableAttempt.deliveryId }, "Could not commit durable Enter write-ahead fence");
|
|
9575
10021
|
return false;
|
|
9576
10022
|
}
|
|
9577
|
-
const sent = await
|
|
10023
|
+
const sent = await tmux.sendSpecialKey("Enter");
|
|
9578
10024
|
if (!sent) {
|
|
9579
10025
|
this.logger.error({
|
|
9580
10026
|
phase,
|
|
@@ -9634,6 +10080,10 @@ export class Daemon extends EventEmitter {
|
|
|
9634
10080
|
verdict.fenced = !verdict.durableBeginCommitted;
|
|
9635
10081
|
return false;
|
|
9636
10082
|
}
|
|
10083
|
+
// M-2267: the pane went into a tmux mode after the readiness wait. Nothing begun or written: the caller waits for
|
|
10084
|
+
// readiness (which holds while it lasts) and tries again — as for a dialog that appeared before the write.
|
|
10085
|
+
if (!verdict.durableBeginCommitted && await this.paneInTmuxMode("paste"))
|
|
10086
|
+
return "pane-in-mode";
|
|
9637
10087
|
// A hand-off pastes and presses Enter into a busy pane: the box must still be readable on this, the last capture
|
|
9638
10088
|
// before the write (#1169 review) — a modal that appeared after the under-lock check would take that Enter. Before
|
|
9639
10089
|
// the durable begin the caller waits for readiness outside the lock and tries again; after it (a paste retry),
|
|
@@ -9697,6 +10147,17 @@ export class Daemon extends EventEmitter {
|
|
|
9697
10147
|
}
|
|
9698
10148
|
}
|
|
9699
10149
|
}
|
|
10150
|
+
// #3030: a hand-off into a busy CLI's own queue is on record before its paste (write-ahead): that queue lives only
|
|
10151
|
+
// in the CLI process, and a stop before a turn takes it would lose it without a trace.
|
|
10152
|
+
if (verdict.trackHandoff && !rawPaste) {
|
|
10153
|
+
await this.recordQueuedHandoff(formatted, handingOffToNativeQueue, verdict, durableAttempt, humanVerificationId);
|
|
10154
|
+
if (stillCurrent && !stillCurrent()) {
|
|
10155
|
+
if (!verdict.trackHandoff.id)
|
|
10156
|
+
this.dropQueuedHandoff(verdict);
|
|
10157
|
+
verdict.fenced = !verdict.durableBeginCommitted;
|
|
10158
|
+
return false;
|
|
10159
|
+
}
|
|
10160
|
+
}
|
|
9700
10161
|
if (lookBeforePaste) {
|
|
9701
10162
|
await lookBeforePaste();
|
|
9702
10163
|
// The look awaited: this attempt is still this write's — the same fence, tmux and launch — or nothing is written.
|
|
@@ -9730,6 +10191,22 @@ export class Daemon extends EventEmitter {
|
|
|
9730
10191
|
// failed/throwing write as uncertain rather than retrying bytes into the
|
|
9731
10192
|
// same pane; the write-ahead Enter marker later separates safe pre-Enter
|
|
9732
10193
|
// recovery from a possibly executed command.
|
|
10194
|
+
// M-2267 (#2971 review, Prism P2): EVERY paste attempt — a retry after a failed paste, a first paste whose durable
|
|
10195
|
+
// begin already went — asks too. Before the begin: nothing begun, the caller waits for readiness and tries again.
|
|
10196
|
+
// After it: this attempt waits here, keeping its durable attempt; a mode outlasting the wait fails it unwritten.
|
|
10197
|
+
if (!verdict.durableBeginCommitted) {
|
|
10198
|
+
if (await this.paneInTmuxMode("paste"))
|
|
10199
|
+
return "pane-in-mode";
|
|
10200
|
+
}
|
|
10201
|
+
else {
|
|
10202
|
+
const mode = await this.waitWhilePaneInMode("paste", () => (!stillCurrent || stillCurrent()) && !this.deliveryWritesStopping);
|
|
10203
|
+
if (mode === "stale") {
|
|
10204
|
+
verdict.fenced = false;
|
|
10205
|
+
return false;
|
|
10206
|
+
}
|
|
10207
|
+
if (mode === "held")
|
|
10208
|
+
return this.failDelivery(verdict, status, "paste", "pane-in-tmux-mode");
|
|
10209
|
+
}
|
|
9733
10210
|
const pasteGeneration = this.spawnGeneration;
|
|
9734
10211
|
beforePaste?.();
|
|
9735
10212
|
const pasted = await this.tmux.pasteBuffer(formatted);
|
|
@@ -9818,7 +10295,15 @@ export class Daemon extends EventEmitter {
|
|
|
9818
10295
|
let enterAt = Date.now();
|
|
9819
10296
|
const enterMono = performance.now();
|
|
9820
10297
|
const steerEnterCurrent = () => steerCurrent() && performance.now() <= steerEnterDeadline;
|
|
9821
|
-
|
|
10298
|
+
let inputHeld = null;
|
|
10299
|
+
if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof ? steerEnterCurrent : undefined, durableAttempt, (why) => { inputHeld = why; }))) {
|
|
10300
|
+
if (inputHeld) {
|
|
10301
|
+
// #2971: held in a tmux mode past the bound; M-2290: a recognised dialog on the pane. The paste may still be in
|
|
10302
|
+
// the CLI: keep it uncertain (Needs-you), never re-paste, never answer the dialog.
|
|
10303
|
+
verdict.phase = "submit-enter";
|
|
10304
|
+
verdict.proof = inputHeld === "dialog" ? "dialog-on-pane" : "pane-in-mode-held";
|
|
10305
|
+
return false;
|
|
10306
|
+
}
|
|
9822
10307
|
if (steerComposerProof && !steerCurrent()) {
|
|
9823
10308
|
verdict.phase = "submit-enter";
|
|
9824
10309
|
verdict.proof = "fenced-before-enter";
|
|
@@ -9909,8 +10394,9 @@ export class Daemon extends EventEmitter {
|
|
|
9909
10394
|
}
|
|
9910
10395
|
const proof = await this.confirmSubmitted(signature, pasteBaseline);
|
|
9911
10396
|
if (proof === "submitted") {
|
|
9912
|
-
|
|
9913
|
-
|
|
10397
|
+
// ✅ native queue accepted — unless it is on record (#3030): then ✅ waits for the turn that takes it.
|
|
10398
|
+
if (status && !verdict.handoff)
|
|
10399
|
+
this.emit("message_confirmed", status);
|
|
9914
10400
|
return true;
|
|
9915
10401
|
}
|
|
9916
10402
|
if (proof === "unverifiable") {
|
|
@@ -9942,8 +10428,8 @@ export class Daemon extends EventEmitter {
|
|
|
9942
10428
|
this.logger.warn("Paste reached the pane but could not be verified as submitted — accepting without proof");
|
|
9943
10429
|
verdict.phase = "best-effort-submission";
|
|
9944
10430
|
verdict.proof = "unverified";
|
|
9945
|
-
if (status)
|
|
9946
|
-
this.emit("message_confirmed", status); // ✅ (best-effort)
|
|
10431
|
+
if (status && !verdict.handoff)
|
|
10432
|
+
this.emit("message_confirmed", status); // ✅ (best-effort; on record: when taken, #3030)
|
|
9947
10433
|
return true;
|
|
9948
10434
|
}
|
|
9949
10435
|
// raw_paste has no unique marker and is not allowed to re-paste or
|
|
@@ -9958,8 +10444,14 @@ export class Daemon extends EventEmitter {
|
|
|
9958
10444
|
// confirmation ladder. "stranded" is the case that used to be a silent
|
|
9959
10445
|
// ✅ — the text was visible, so the old check called it delivered.
|
|
9960
10446
|
this.logger.warn({ proof }, "Native-queue paste not confirmed as submitted — retrying via idle-gated delivery");
|
|
9961
|
-
|
|
9962
|
-
|
|
10447
|
+
// #3030: what follows is an idle submission (or a failure the caller reports): no longer a queued hand-off.
|
|
10448
|
+
if (verdict.handoff && !verdict.trackHandoff?.id)
|
|
10449
|
+
this.dropQueuedHandoff(verdict);
|
|
10450
|
+
verdict.handoff = undefined;
|
|
10451
|
+
if (windowId && this.controlClient && !(await this.controlClient.waitUntilIdle(windowId))) {
|
|
10452
|
+
verdict.phase = "native-queue-idle-wait";
|
|
10453
|
+
verdict.proof = "idle-wait-failed";
|
|
10454
|
+
return false;
|
|
9963
10455
|
}
|
|
9964
10456
|
// Re-read before doing anything: the CLI had a whole turn to flush its
|
|
9965
10457
|
// queue while we waited, and the right recovery differs per outcome.
|
|
@@ -12354,7 +12846,12 @@ export class Daemon extends EventEmitter {
|
|
|
12354
12846
|
|| this.startupAborted || this.deliveryWritesStopping || this.runtimeMonitorsFrozen || this.healthCheckPaused)) {
|
|
12355
12847
|
throw new Error("OOM recovery cancelled: the instance was stopped, paused or replaced while memory was unavailable");
|
|
12356
12848
|
}
|
|
12357
|
-
return this.trySpawnInsideGate(reuseWindow, startupTimeoutMs)
|
|
12849
|
+
return this.trySpawnInsideGate(reuseWindow, startupTimeoutMs).catch(err => {
|
|
12850
|
+
// Includes an in-place binary replacement during asynchronous launch preparation.
|
|
12851
|
+
if (err instanceof KiroCompatibilityError)
|
|
12852
|
+
this.emit("kiro_compatibility_hold", err.message);
|
|
12853
|
+
throw err;
|
|
12854
|
+
});
|
|
12358
12855
|
};
|
|
12359
12856
|
const ready = !this.spawnGate ? await enter() : await this.spawnGate.run({
|
|
12360
12857
|
instanceName: this.name,
|
|
@@ -12577,6 +13074,11 @@ export class Daemon extends EventEmitter {
|
|
|
12577
13074
|
this.startupAdmission?.();
|
|
12578
13075
|
const launchFence = this.launchFenceEpoch;
|
|
12579
13076
|
const backendConfig = this.buildBackendConfig();
|
|
13077
|
+
// A compatibility refusal is not a failed resume: no configuration, pin, history or pane may change.
|
|
13078
|
+
if (this.backend?.assertLaunchSupported) {
|
|
13079
|
+
this.backend.assertLaunchSupported(backendConfig);
|
|
13080
|
+
this.emit("kiro_compatibility_hold", null);
|
|
13081
|
+
}
|
|
12580
13082
|
// #1633: decided before prev-instructions is first written — its absence is what says "never launched before".
|
|
12581
13083
|
this.prepareWhatsNew(!existsSync(join(this.instanceDir, "prev-instructions")));
|
|
12582
13084
|
// Record what this launch writes. #2787: a change is never announced in the pane; the CLI takes the new
|
|
@@ -12613,6 +13115,7 @@ export class Daemon extends EventEmitter {
|
|
|
12613
13115
|
const agentTokenPath = join(this.instanceDir, "agent.token");
|
|
12614
13116
|
const agentToken = randomBytes(32).toString("hex");
|
|
12615
13117
|
const tokenWrite = writeSecretFile(agentTokenPath, agentToken);
|
|
13118
|
+
this.agentCredentialIssuer?.(agentToken);
|
|
12616
13119
|
if (!tokenWrite.ok) {
|
|
12617
13120
|
// Do not fail the spawn over it — an instance that cannot start is worse
|
|
12618
13121
|
// than one whose token is readable. But say so, loudly: the whole point of
|
|
@@ -12883,7 +13386,7 @@ export class Daemon extends EventEmitter {
|
|
|
12883
13386
|
// ones included: if the auto-dismiss keeps failing here, the human
|
|
12884
13387
|
// report should count from first sight, not from the runtime scan.
|
|
12885
13388
|
if (!dialog.fatal && this.deliveryBlockingDialogs().some(d => Daemon.dialogKey(d) === Daemon.dialogKey(dialog))) {
|
|
12886
|
-
this.trackDialogParked(dialog);
|
|
13389
|
+
this.trackDialogParked(dialog, pane);
|
|
12887
13390
|
}
|
|
12888
13391
|
// A fatal startup screen cannot be dismissed by keypresses (e.g. a
|
|
12889
13392
|
// corrupt claude.json modal offering only "exit" / "reset config").
|
|
@@ -12913,7 +13416,7 @@ export class Daemon extends EventEmitter {
|
|
|
12913
13416
|
this.heldReasonLoggedKey = Daemon.dialogKey(dialog);
|
|
12914
13417
|
this.logger.warn({ description: dialog.description, ...reason }, "Startup dialog held — not auto-answering, deliveries stay blocked");
|
|
12915
13418
|
}
|
|
12916
|
-
this.trackDialogParked(dialog);
|
|
13419
|
+
this.trackDialogParked(dialog, pane);
|
|
12917
13420
|
matched = true;
|
|
12918
13421
|
break;
|
|
12919
13422
|
}
|