@songsid/agend 2.3.2-beta.9 → 2.3.3-beta.1
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 +6 -0
- package/README.zh-TW.md +6 -0
- package/dist/agent-cli-instructions.md +2 -2
- package/dist/agent-cli.js +48 -0
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.js +6 -0
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.d.ts +21 -0
- package/dist/agent-whats-new.js +53 -21
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.js +2 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/claude-code.d.ts +13 -1
- package/dist/backend/claude-code.js +43 -2
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex-session-lookup.d.ts +15 -0
- package/dist/backend/codex-session-lookup.js +58 -0
- package/dist/backend/codex-session-lookup.js.map +1 -1
- package/dist/backend/codex.js +15 -4
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro-classic-archive.d.ts +57 -0
- package/dist/backend/kiro-classic-archive.js +622 -0
- package/dist/backend/kiro-classic-archive.js.map +1 -0
- package/dist/backend/kiro-classic-links.d.ts +24 -0
- package/dist/backend/kiro-classic-links.js +135 -0
- package/dist/backend/kiro-classic-links.js.map +1 -0
- package/dist/backend/kiro-identity.d.ts +100 -1
- package/dist/backend/kiro-identity.js +262 -31
- package/dist/backend/kiro-identity.js.map +1 -1
- package/dist/backend/kiro-migration-verify.d.ts +10 -0
- package/dist/backend/kiro-migration-verify.js +52 -0
- package/dist/backend/kiro-migration-verify.js.map +1 -0
- package/dist/backend/kiro-reconcile-store.d.ts +27 -0
- package/dist/backend/kiro-reconcile-store.js +351 -0
- package/dist/backend/kiro-reconcile-store.js.map +1 -0
- package/dist/backend/kiro-v2-store-worker.js +35 -1
- package/dist/backend/kiro-v2-store-worker.js.map +1 -1
- package/dist/backend/kiro-v2-store.d.ts +31 -1
- package/dist/backend/kiro-v2-store.js +22 -12
- package/dist/backend/kiro-v2-store.js.map +1 -1
- package/dist/backend/kiro.d.ts +124 -6
- package/dist/backend/kiro.js +553 -39
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +109 -4
- package/dist/backend/types.js +9 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/cache-service.d.ts +13 -1
- package/dist/cache-service.js +45 -2
- package/dist/cache-service.js.map +1 -1
- package/dist/channel/ipc-timeouts.js +3 -0
- package/dist/channel/ipc-timeouts.js.map +1 -1
- package/dist/channel/mcp-tools.js +40 -5
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.js +2 -1
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/classic-channel-manager.d.ts +6 -0
- package/dist/classic-channel-manager.js +25 -0
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +52 -6
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.d.ts +7 -1
- package/dist/config-validator.js +20 -9
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/cross-instance-envelope.d.ts +10 -5
- package/dist/cross-instance-envelope.js +20 -17
- package/dist/cross-instance-envelope.js.map +1 -1
- package/dist/daemon.d.ts +125 -63
- package/dist/daemon.js +694 -330
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard-permissions.d.ts +15 -4
- package/dist/dashboard-permissions.js +49 -23
- package/dist/dashboard-permissions.js.map +1 -1
- package/dist/delivery-outbox.d.ts +27 -22
- package/dist/delivery-outbox.js +109 -161
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/delivery-outbox.test.js +10 -25
- package/dist/delivery-outbox.test.js.map +1 -1
- package/dist/doctor.js +2 -1
- package/dist/doctor.js.map +1 -1
- package/dist/fleet-control-audit.d.ts +1 -1
- package/dist/fleet-control-audit.js +3 -0
- package/dist/fleet-control-audit.js.map +1 -1
- package/dist/fleet-manager.d.ts +94 -15
- package/dist/fleet-manager.js +597 -133
- package/dist/fleet-manager.js.map +1 -1
- package/dist/forged-envelope-scan.d.ts +9 -0
- package/dist/forged-envelope-scan.js +30 -5
- package/dist/forged-envelope-scan.js.map +1 -1
- package/dist/general-instance.d.ts +24 -0
- package/dist/general-instance.js +53 -0
- package/dist/general-instance.js.map +1 -1
- package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +1 -1
- package/dist/general-knowledge/skills/docker-install/SKILL.md +25 -0
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -3
- package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -1
- package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +34 -0
- package/dist/general-knowledge/skills/session-management/SKILL.md +2 -2
- package/dist/general-knowledge/skills/tui-effort/SKILL.md +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +32 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +32 -0
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +57 -17
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +57 -17
- package/dist/instance-config-impact.js +3 -2
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +4 -0
- package/dist/instance-lifecycle.js +28 -5
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/instructions.js +6 -7
- package/dist/instructions.js.map +1 -1
- package/dist/kiro-migrate.d.ts +19 -2
- package/dist/kiro-migrate.js +133 -35
- package/dist/kiro-migrate.js.map +1 -1
- package/dist/kiro-reconcile.d.ts +21 -0
- package/dist/kiro-reconcile.js +88 -0
- package/dist/kiro-reconcile.js.map +1 -0
- package/dist/kiro-session-proof.d.ts +41 -0
- package/dist/kiro-session-proof.js +89 -0
- package/dist/kiro-session-proof.js.map +1 -0
- package/dist/kiro-tui-forensic.d.ts +19 -0
- package/dist/kiro-tui-forensic.js +124 -0
- package/dist/kiro-tui-forensic.js.map +1 -0
- package/dist/locale.js +202 -16
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +3 -3
- package/dist/log-service.js.map +1 -1
- package/dist/needs-you.d.ts +3 -1
- package/dist/needs-you.js +4 -2
- package/dist/needs-you.js.map +1 -1
- package/dist/office-levels.d.ts +64 -0
- package/dist/office-levels.js +66 -0
- package/dist/office-levels.js.map +1 -0
- package/dist/office-share-page.d.ts +3 -1
- package/dist/office-share-page.js +8 -3
- package/dist/office-share-page.js.map +1 -1
- package/dist/office-share-service.d.ts +6 -3
- package/dist/office-share-service.js +12 -8
- package/dist/office-share-service.js.map +1 -1
- package/dist/office-share.d.ts +45 -8
- package/dist/office-share.js +75 -15
- package/dist/office-share.js.map +1 -1
- package/dist/outbound-handlers.js +21 -2
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +47 -2
- package/dist/outbound-schemas.js +42 -3
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +1 -1
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/quickstart-api.d.ts +39 -1
- package/dist/quickstart-api.js +115 -17
- package/dist/quickstart-api.js.map +1 -1
- package/dist/remote-card-answer.d.ts +83 -0
- package/dist/remote-card-answer.js +208 -0
- package/dist/remote-card-answer.js.map +1 -0
- package/dist/remote-card-prompt.d.ts +38 -0
- package/dist/remote-card-prompt.js +86 -0
- package/dist/remote-card-prompt.js.map +1 -0
- package/dist/scheduler/task-board.d.ts +44 -17
- package/dist/scheduler/task-board.js +95 -36
- package/dist/scheduler/task-board.js.map +1 -1
- package/dist/sd-notify.d.ts +13 -14
- package/dist/sd-notify.js +62 -28
- package/dist/sd-notify.js.map +1 -1
- package/dist/service-installer.d.ts +13 -0
- package/dist/service-installer.js +15 -0
- package/dist/service-installer.js.map +1 -1
- package/dist/service-owner.d.ts +25 -0
- package/dist/service-owner.js +43 -2
- package/dist/service-owner.js.map +1 -1
- package/dist/settings-api.d.ts +2 -1
- package/dist/settings-api.js +19 -26
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +10 -5
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-effect.js +7 -6
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-instance-patch.js +1 -1
- package/dist/settings-instance-patch.js.map +1 -1
- package/dist/setup-form.d.ts +2 -2
- package/dist/setup-form.js +139 -93
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-i18n.d.ts +25 -0
- package/dist/setup-i18n.js +226 -0
- package/dist/setup-i18n.js.map +1 -0
- package/dist/task-board-waker.d.ts +40 -1
- package/dist/task-board-waker.js +115 -9
- package/dist/task-board-waker.js.map +1 -1
- package/dist/tmux-control.d.ts +1 -1
- package/dist/tmux-control.js +3 -2
- package/dist/tmux-control.js.map +1 -1
- package/dist/tmux-manager.js +18 -16
- package/dist/tmux-manager.js.map +1 -1
- package/dist/tmux-read.d.ts +2 -0
- package/dist/tmux-read.js +7 -1
- package/dist/tmux-read.js.map +1 -1
- package/dist/tool-permissions.js +18 -0
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +2 -0
- package/dist/topic-commands.js +2 -1
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-sources.d.ts +1 -0
- package/dist/transcript-sources.js +20 -3
- package/dist/transcript-sources.js.map +1 -1
- package/dist/types.d.ts +11 -5
- package/dist/ui/backend-signin.js +4 -3
- package/dist/ui/chat-store.js +3 -2
- package/dist/ui/chat-strings.js +2 -2
- package/dist/ui/fleet-office-card.js +114 -0
- package/dist/ui/fleet-office-editor.js +81 -9
- package/dist/ui/fleet-office-layout.js +56 -11
- package/dist/ui/fleet-office-levels.js +105 -0
- package/dist/ui/fleet-office-model.js +9 -1
- package/dist/ui/fleet-office-pack.d.ts +15 -3
- package/dist/ui/fleet-office-pack.js +177 -23
- package/dist/ui/fleet-office-skin.js +39 -3
- package/dist/ui/fleet-office-store.js +74 -3
- package/dist/ui/fleet-office-view.js +8 -0
- package/dist/ui/fleet-office-zones.js +102 -19
- package/dist/ui/fleet-office.js +308 -44
- package/dist/ui/fleet-org.js +2 -0
- package/dist/ui/fleet-task-card.js +2 -2
- package/dist/ui/fleet-task-common.js +30 -7
- package/dist/ui/fleet-task-work.js +2 -2
- package/dist/ui/fleet-tasks.js +10 -11
- package/dist/ui/office-share-dialog.js +31 -7
- package/dist/ui/office-share-page.js +1 -0
- package/dist/ui/panel-chat.js +14 -2
- package/dist/ui/panel-log.js +6 -6
- package/dist/ui/panel-needs.js +1 -1
- package/dist/ui/panel-settings.js +16 -8
- package/dist/ui/settings-appearance.js +3 -35
- package/dist/ui/settings-discord.js +3 -2
- package/dist/ui/settings-guests.js +17 -7
- package/dist/ui/settings-model.js +17 -9
- package/dist/ui/settings-registry.js +1 -1
- package/dist/ui/settings-strings.js +18 -6
- package/dist/ui/settings-token.js +3 -2
- package/dist/ui/settings-wizard.js +25 -9
- package/dist/ui/shared/app-i18n.js +2 -2
- package/dist/ui/shared/app-stream.js +1 -1
- package/dist/ui/shared/app.css +70 -2
- package/dist/ui/shared/setup-host-text.js +107 -0
- package/dist/ui/structured-signin.js +89 -38
- package/dist/update-check.d.ts +13 -4
- package/dist/update-check.js +17 -4
- package/dist/update-check.js.map +1 -1
- package/dist/usage-rollup.d.ts +34 -3
- package/dist/usage-rollup.js +56 -3
- package/dist/usage-rollup.js.map +1 -1
- package/dist/web-api.d.ts +10 -1
- package/dist/web-api.js +207 -17
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.js +1 -1
- package/dist/web-auth.js.map +1 -1
- package/dist/web-experimental.d.ts +6 -2
- package/dist/web-experimental.js +8 -4
- package/dist/web-experimental.js.map +1 -1
- package/dist/web-office-acts.d.ts +5 -2
- package/dist/web-office-acts.js +9 -4
- package/dist/web-office-acts.js.map +1 -1
- package/dist/web-office-pack-read.d.ts +35 -0
- package/dist/web-office-pack-read.js +252 -0
- package/dist/web-office-pack-read.js.map +1 -0
- package/dist/web-office-pack-tools.d.ts +241 -0
- package/dist/web-office-pack-tools.js +785 -0
- package/dist/web-office-pack-tools.js.map +1 -0
- package/dist/web-office-skins.d.ts +68 -4
- package/dist/web-office-skins.js +324 -33
- package/dist/web-office-skins.js.map +1 -1
- package/dist/web-office-store.d.ts +47 -4
- package/dist/web-office-store.js +124 -23
- package/dist/web-office-store.js.map +1 -1
- package/dist/web-settings-patch.d.ts +3 -2
- package/dist/web-settings-patch.js +7 -5
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/workflow-templates/default.md +3 -3
- package/dist/working-recovery.d.ts +0 -2
- package/dist/working-recovery.js +2 -23
- package/dist/working-recovery.js.map +1 -1
- package/package.json +2 -2
- package/templates/windows-shortcuts.ps1 +18 -17
- package/dist/upgrade-notices.d.ts +0 -17
- package/dist/upgrade-notices.js +0 -93
- package/dist/upgrade-notices.js.map +0 -1
package/dist/daemon.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { readActiveKiroModel } from "./backend/kiro.js";
|
|
1
2
|
import { dashboardAccessOf } from "./dashboard-permissions.js";
|
|
2
|
-
import {
|
|
3
|
+
import { sendPackSources } from "./web-office-pack-read.js";
|
|
4
|
+
import { agendVersion, planWhatsNew, readWhatsNewRecord, recordWhatsNew, withWhatsNew } from "./agent-whats-new.js";
|
|
3
5
|
import { join, dirname, resolve } from "node:path";
|
|
4
6
|
import { checkoutTarget, readCheckouts, releaseCandidate, reposRoot, verifyWorktree, writeCheckouts } from "./repo-worktree-guard.js";
|
|
5
7
|
import { mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, unlinkSync, rmSync, appendFileSync, statSync, renameSync } from "node:fs";
|
|
6
8
|
import { COMPACT_WATCH_TIMEOUT_MS, launchIdentity } from "./compact-watch.js";
|
|
7
9
|
import { KIRO_COMPACT_PANE_LINES, KiroCompactPaneFollower } from "./kiro-compact-pane.js";
|
|
10
|
+
import { clearKiroSessionProof, proveKiroSessionId, recordKiroSessionProof } from "./kiro-session-proof.js";
|
|
8
11
|
import { fileURLToPath } from "node:url";
|
|
9
12
|
import { ensureInstanceDir } from "./private-dir.js";
|
|
10
13
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
@@ -21,8 +24,9 @@ import { TranscriptMonitor } from "./transcript-monitor.js";
|
|
|
21
24
|
import { createTranscriptSource } from "./transcript-sources.js";
|
|
22
25
|
import { TranscriptTurnLedger } from "./transcript-turns.js";
|
|
23
26
|
import { credentialProfileStoreHome, resolveCredentialProfile } from "./backend/credential-profile.js";
|
|
24
|
-
import { resolveToolSet } from "./tool-permissions.js";
|
|
25
|
-
import {
|
|
27
|
+
import { resolveToolSet, toolsFor } from "./tool-permissions.js";
|
|
28
|
+
import { TOOLS, toolsWithCanvasWorker } from "./channel/mcp-tools.js";
|
|
29
|
+
import { dashboardAccessProblem, dashboardFeaturesOff, withoutFeatures } from "./dashboard-permissions.js";
|
|
26
30
|
import { getAgendHome } from "./paths.js";
|
|
27
31
|
import { readCanvasWorkerEnabled, scopedFleetConfigEnvironment } from "./web-experimental.js";
|
|
28
32
|
import { readAgentEffect, sendEffectInParts } from "./web-effect-upload.js";
|
|
@@ -36,13 +40,14 @@ import { ContextGuardian } from "./context-guardian.js";
|
|
|
36
40
|
import { IpcServer } from "./channel/ipc-bridge.js";
|
|
37
41
|
import { daemonBudgetMs } from "./channel/ipc-timeouts.js";
|
|
38
42
|
import { MessageBus } from "./channel/message-bus.js";
|
|
39
|
-
import { shellQuote, UnsupportedCliError } from "./backend/types.js";
|
|
43
|
+
import { shellQuote, UnsupportedCliError, ResumeBlockedError } from "./backend/types.js";
|
|
40
44
|
import { getTmuxSession } from "./config.js";
|
|
41
45
|
import { routeToolCall } from "./channel/tool-router.js";
|
|
42
46
|
import { parseReplyButtons, replyButtonsFallbackText } from "./reply-buttons.js";
|
|
43
47
|
import { HangDetector } from "./hang-detector.js";
|
|
44
48
|
import { writeSecretFile } from "./secret-file.js";
|
|
45
49
|
import { PaneWriteLock } from "./pane-write-lock.js";
|
|
50
|
+
import { answerRemoteCard } from "./remote-card-answer.js";
|
|
46
51
|
import { buildFleetInstructions } from "./instructions.js";
|
|
47
52
|
import { formatHumanInboundMessage, prepareDeliveryWire, steerInboundEnvelope, BTW_INBOUND_PREFIX, humanBodyHash, humanOwnerObjectId } from "./human-delivery-receipts.js";
|
|
48
53
|
import { formatCrossInstanceInboundMessage, renderCrossInstanceHandoffMetadata, } from "./cross-instance-envelope.js";
|
|
@@ -63,8 +68,6 @@ import { claudeConfigDirFor, claudeProfileConfigDir, claudeSwitchDialog, claudeS
|
|
|
63
68
|
import { MuseBackend } from "./backend/muse.js";
|
|
64
69
|
import { InteractionObservation, INTERACTION_CONFIRM_MS, sameInteractionOwner } from "./interaction-observation.js";
|
|
65
70
|
import { unrecognisedPromptShape } from "./unrecognised-prompt.js";
|
|
66
|
-
/** #2140: deliveries that may carry an unwritten what's-new notice before it is recorded anyway. */
|
|
67
|
-
const WHATS_NEW_MAX_UNWRITTEN = 3;
|
|
68
71
|
const __filename = fileURLToPath(import.meta.url);
|
|
69
72
|
const __dirname = dirname(__filename);
|
|
70
73
|
// Tool routing sets — module-level to avoid re-creation on every handleToolCall
|
|
@@ -89,6 +92,10 @@ const WEB_THEME_IPC = Object.freeze({
|
|
|
89
92
|
delete_web_effect: "fleet_delete_web_effect",
|
|
90
93
|
// #2578: its own character in the Office view — the args as they are (the fleet checks them).
|
|
91
94
|
office_view: "fleet_office_view", office_set: "fleet_office_set",
|
|
95
|
+
// #2692: the skin packs — check and upload read the pack here (web-office-pack-read.ts) and send its bytes in parts.
|
|
96
|
+
office_pack_check: "fleet_office_pack_check", office_pack_upload: "fleet_office_pack_upload",
|
|
97
|
+
office_pack_delete: "fleet_office_pack_delete", office_pack_restore: "fleet_office_pack_restore",
|
|
98
|
+
office_pack_list: "fleet_office_pack_list", office_set_style: "fleet_office_set_style",
|
|
92
99
|
});
|
|
93
100
|
/** The largest picture any slot may take (a card slot's ceiling; the fleet checks the limit in force): read no more. */
|
|
94
101
|
const WEB_THEME_ASSET_MAX_BYTES = ASSET_UPLOAD_MAX_BYTES;
|
|
@@ -105,7 +112,6 @@ const TURN_OUTBOUND_TOOLS = new Set([
|
|
|
105
112
|
// These actions reach the human-facing channel and satisfy reply-completion:
|
|
106
113
|
// a deliberate reaction or edit is a valid response even without new text.
|
|
107
114
|
const TURN_COMPLETION_TOOLS = new Set(["reply", "react", "edit_message"]);
|
|
108
|
-
const REPLY_DROP_WARNING_COOLDOWN_MS = 5 * 60_000;
|
|
109
115
|
/**
|
|
110
116
|
* #1241: how long an idle edge must persist — with no reply and no further
|
|
111
117
|
* observed work — before the reply guard declares the turn a miss. A claude
|
|
@@ -128,50 +134,12 @@ const TRANSCRIPT_RUNNING_HOLD_MAX_MS = 30 * 60_000;
|
|
|
128
134
|
*/
|
|
129
135
|
export const TRANSCRIPT_OPEN_TURN_QUIET_MS = 15 * 60_000;
|
|
130
136
|
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.";
|
|
131
|
-
/** Point a resumed CLI at its one backend-native instruction source. */
|
|
132
137
|
/** The outbox evidence for a delivery the CLI's transcript shows taken (#758; #1201 absorbed; #1647 a raw paste's text). */
|
|
133
138
|
function transcriptMarkerEvidence(kind, rawPaste = false) {
|
|
134
139
|
if (rawPaste)
|
|
135
140
|
return "transcript-turn-user-entry-raw-paste-text";
|
|
136
141
|
return kind === "user" ? "transcript-marker" : kind === "absorbed" ? "transcript-marker-absorbed" : "transcript-marker-queued";
|
|
137
142
|
}
|
|
138
|
-
export function buildInstructionReloadNotice(binaryName, instanceName, instanceDir, backendSource) {
|
|
139
|
-
const source = backendSource ? backendSource : binaryName === "codex" || binaryName === "grok" || binaryName === "muse"
|
|
140
|
-
? "AGENTS.md"
|
|
141
|
-
: binaryName === "kiro-cli"
|
|
142
|
-
? `.kiro/steering/agend-${instanceName}.md`
|
|
143
|
-
: binaryName === "agy"
|
|
144
|
-
? ".agents/AGENTS.md"
|
|
145
|
-
: join(instanceDir, "fleet-instructions.md");
|
|
146
|
-
return `[system] Your AgEnD instructions have been updated. Reload only ${source}; do not scan other instruction directories. Do not reply to this message.`;
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Whether this backend has to be told in its pane that its instructions moved.
|
|
150
|
-
*
|
|
151
|
-
* A backend that re-reads them on resume already received the new ones from the
|
|
152
|
-
* resume itself, so the notice is pure interruption. Both places that can send
|
|
153
|
-
* it — the warmup paste and the deferred pendingInstructionsNotice — ask through
|
|
154
|
-
* here, so the two cannot drift apart again: the warmup path used to omit this
|
|
155
|
-
* check entirely and told claude-code to reload instructions it had just read.
|
|
156
|
-
*/
|
|
157
|
-
export function backendNeedsPaneReloadNotice(backend) {
|
|
158
|
-
return !backend?.instructionsReloadedOnResume;
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* What the warmup should do about changed instructions.
|
|
162
|
-
*
|
|
163
|
-
* `skip` — nothing to say, or this backend learns of the change by itself.
|
|
164
|
-
* `defer` — worth saying, but nobody is talking to the instance; say it when a
|
|
165
|
-
* real message next arrives rather than provoking an unsolicited reply.
|
|
166
|
-
* `paste` — say it now, into a pane that already has a delivery in flight.
|
|
167
|
-
*/
|
|
168
|
-
export function warmupNoticeAction(opts) {
|
|
169
|
-
if (!opts.warmupNeeded)
|
|
170
|
-
return "skip";
|
|
171
|
-
if (!backendNeedsPaneReloadNotice(opts.backend))
|
|
172
|
-
return "skip";
|
|
173
|
-
return opts.pasteQueueDepth === 0 ? "defer" : "paste";
|
|
174
|
-
}
|
|
175
143
|
export const DEFAULT_STUCK_TIMEOUT_MS = 10 * 60_000;
|
|
176
144
|
export const DEFAULT_STATE_IDLE_DEBOUNCE_MS = 2_000;
|
|
177
145
|
export const DEFAULT_STATE_SAFETY_SWEEP_MS = 60_000;
|
|
@@ -181,6 +149,14 @@ export const PERIODIC_REDRAW_PROBE_MS = 25;
|
|
|
181
149
|
const KIRO_COMPACT_PANE_POLL_MS = 1_000;
|
|
182
150
|
/** Do not turn a streaming Codex turn into one capture-pane per burst. */
|
|
183
151
|
export const STRUCTURED_IDLE_PROBE_MS = 500;
|
|
152
|
+
/** #2770 (ii): how often output may schedule a look for a layout that repaints while idle (CliBackend.periodicRedrawFrom). */
|
|
153
|
+
export const PERIODIC_LAYOUT_LOOK_MS = 5_000;
|
|
154
|
+
/** #2770: how often a delivery waiting for idle asks again whether it can be handed off instead. */
|
|
155
|
+
export const HANDOFF_RECHECK_MS = 1_000;
|
|
156
|
+
/** #2770: how often a wait for idle asks its stop condition (a cancel, a hand-off that became possible). */
|
|
157
|
+
export const WAIT_STOP_POLL_MS = 250;
|
|
158
|
+
/** #2770: human messages a cancel dropped are told in one notice, this long after the last of them. */
|
|
159
|
+
export const CANCELLED_HUMANS_NOTICE_MS = 1_000;
|
|
184
160
|
/**
|
|
185
161
|
* An instance publishing more than this many execution-state edges inside the
|
|
186
162
|
* window is flapping; its edges stop being logged one by one (#1116). One
|
|
@@ -573,6 +549,8 @@ export const OWN_SWITCH_WAIT_MS = 6_000;
|
|
|
573
549
|
const OWN_SWITCH_SETTLE_MS = 3_000;
|
|
574
550
|
/** A blocking dialog still on screen this long after first being seen is reported for a human. */
|
|
575
551
|
const DIALOG_PARKED_NOTIFY_MS = 60_000;
|
|
552
|
+
/** #2675: a card a person can answer from the chat is posted once it has been on screen, unchanged, this long. */
|
|
553
|
+
const REMOTE_CARD_STABLE_MS = 1_500;
|
|
576
554
|
/** #1691: a prompt no table knows must sit unchanged this long (monotonic) before it holds deliveries and is reported. */
|
|
577
555
|
export const UNRECOGNISED_PROMPT_STABLE_MS = 60_000;
|
|
578
556
|
/** #1691: the rows under a prompt a backend without a composer reader shows its ready chrome in (status bar, hint). */
|
|
@@ -701,6 +679,18 @@ const FIRST_DELIVERY_WINDOW_MS = 5_000;
|
|
|
701
679
|
const NATIVE_QUEUE_PASTE_VERIFY_MS = 2_000;
|
|
702
680
|
/** #1405: how long a steer's paste gets to show in a mode-switched composer before its one Enter (captures included). */
|
|
703
681
|
const STEER_PASTE_VISIBLE_MS = 2_000;
|
|
682
|
+
/**
|
|
683
|
+
* #2676: the longest one-line paste kiro's TUI composer still shows as typed text; longer, it collapses to a
|
|
684
|
+
* `N chars ▸` chip (2.34.0 tui.js: more than 10 lines or 500 characters collapse).
|
|
685
|
+
*/
|
|
686
|
+
const KIRO_COLLAPSE_CHARS = 500;
|
|
687
|
+
/**
|
|
688
|
+
* #1405/#2676: whether a mode-switched composer in `mode` may take a busy hand-off: a steer only on "steer", an ordinary
|
|
689
|
+
* message on "steer" or "queue" (kiro takes either into its tray), and "idle" — the turn over — only where `allowIdle`.
|
|
690
|
+
*/
|
|
691
|
+
function composerModeAdmits(mode, allowIdle, steerOnly) {
|
|
692
|
+
return mode === "steer" || (!steerOnly && mode === "queue") || (allowIdle && mode === "idle");
|
|
693
|
+
}
|
|
704
694
|
/** `promise`'s value if it settles within `ms`, else undefined — the late answer is dropped, never awaited (#1405). */
|
|
705
695
|
async function withinMs(promise, ms) {
|
|
706
696
|
if (ms <= 0) {
|
|
@@ -1138,6 +1128,29 @@ export class Daemon extends EventEmitter {
|
|
|
1138
1128
|
logTranscriptPath() { return this.transcriptMonitor?.currentPath() ?? null; }
|
|
1139
1129
|
/** #1218 (kiro): the launch this daemon's backend last built, for the Log's reason only (kiro-log-gate.ts). */
|
|
1140
1130
|
logLaunch() { return this.backend?.logLaunch?.() ?? null; }
|
|
1131
|
+
async kiroMigrationIdle() {
|
|
1132
|
+
const tmux = this.tmux, owner = this.interactionOwner();
|
|
1133
|
+
if (!tmux || this.spawning || this.runtimeMonitorsFrozen || this.processStatus !== "running")
|
|
1134
|
+
return false;
|
|
1135
|
+
const pane = await tmux.capturePane(2000).catch(() => null);
|
|
1136
|
+
return pane !== null && tmux === this.tmux && sameInteractionOwner(owner, this.interactionOwner())
|
|
1137
|
+
&& this.processStatus === "running" && !this.spawning && !this.runtimeMonitorsFrozen && this.paneAuthoritativelyIdle(pane);
|
|
1138
|
+
}
|
|
1139
|
+
/** One read-only migration observation. Never types into a pane or selects a picker. */
|
|
1140
|
+
async kiroMigrationObservation() {
|
|
1141
|
+
const tmux = this.tmux, owner = this.interactionOwner(), backend = this.backend;
|
|
1142
|
+
if (!tmux || backend?.binaryName !== "kiro-cli" || this.processStatus !== "running" || this.spawning || this.runtimeMonitorsFrozen)
|
|
1143
|
+
return null;
|
|
1144
|
+
const pane = await tmux.capturePane(2000).catch(() => null);
|
|
1145
|
+
if (pane === null || tmux !== this.tmux || !sameInteractionOwner(owner, this.interactionOwner())
|
|
1146
|
+
|| this.processStatus !== "running" || this.spawning || this.runtimeMonitorsFrozen || !this.paneAuthoritativelyIdle(pane)
|
|
1147
|
+
|| backend.readSteerComposer?.(pane) !== "idle")
|
|
1148
|
+
return null;
|
|
1149
|
+
const launch = backend.transcriptLaunch?.(), path = this.logTranscriptPath();
|
|
1150
|
+
// Agent switching is separately disabled for saved-agent resumes; it is not an observation API.
|
|
1151
|
+
const model = readActiveKiroModel(pane);
|
|
1152
|
+
return launch?.engine === "v2" && launch.sessionId && model && path ? { sessionId: launch.sessionId, model, transcript: path } : null;
|
|
1153
|
+
}
|
|
1141
1154
|
guardian = null;
|
|
1142
1155
|
adapter = null;
|
|
1143
1156
|
pendingIpcRequests = new Map();
|
|
@@ -1257,6 +1270,12 @@ export class Daemon extends EventEmitter {
|
|
|
1257
1270
|
dialogParkedKey = null;
|
|
1258
1271
|
dialogParkedInteractionEpisode = null;
|
|
1259
1272
|
dialogParkedReported = false;
|
|
1273
|
+
/** #2675: the card on screen that a person can answer from the chat — what it asks, since when, and whether it was posted. */
|
|
1274
|
+
remoteCardShown = null;
|
|
1275
|
+
/** #2675: when (performance.now, taken before its capture) the newest read the card tracker used was started. */
|
|
1276
|
+
remoteCardReadAt = -Infinity;
|
|
1277
|
+
/** #2675: an answer from the chat is being keyed in (one at a time). */
|
|
1278
|
+
remoteCardAnswering = false;
|
|
1260
1279
|
/**
|
|
1261
1280
|
* #1691: the prompt at the bottom of the pane that no backend table knows — its block, since when (performance.now)
|
|
1262
1281
|
* it has been unchanged, and whether that has lasted UNRECOGNISED_PROMPT_STABLE_MS. Updated by the runtime scan.
|
|
@@ -1347,6 +1366,10 @@ export class Daemon extends EventEmitter {
|
|
|
1347
1366
|
/** Consecutive structural idle captures for a noisy, periodically-redrawing
|
|
1348
1367
|
* pane. Two are required before a working -> idle transition. */
|
|
1349
1368
|
instanceStatePeriodicIdleConfirmations = 0;
|
|
1369
|
+
/** #2770 (ii): the latest capture showed a layout that repaints while idle (CliBackend.periodicRedrawFrom). */
|
|
1370
|
+
periodicRedrawLayout = false;
|
|
1371
|
+
/** When output last scheduled a look for that layout (monotonic ms), so a busy pane is not captured on every chunk. */
|
|
1372
|
+
periodicLayoutLookAt = -Infinity;
|
|
1350
1373
|
/** Recent execution-state edge times, for the flap log limiter (#1116). */
|
|
1351
1374
|
stateEdgeTimes = [];
|
|
1352
1375
|
stateEdgesNotLogged = 0;
|
|
@@ -1388,7 +1411,6 @@ export class Daemon extends EventEmitter {
|
|
|
1388
1411
|
/** Prevent a visible stale XML fragment from being recovered on later turns. */
|
|
1389
1412
|
lastMalformedToolCallSignature;
|
|
1390
1413
|
proxyReplySeq = 0;
|
|
1391
|
-
lastReplyDropWarningAt = 0;
|
|
1392
1414
|
/**
|
|
1393
1415
|
* #1241: an idle edge that has not yet proven the turn ended. Armed instead
|
|
1394
1416
|
* of recovering immediately; a steady idle snapshot past confirmAt promotes
|
|
@@ -1474,6 +1496,8 @@ export class Daemon extends EventEmitter {
|
|
|
1474
1496
|
authFailureUnresolved = false;
|
|
1475
1497
|
/** One login-screen auth report per spawn — the screen persists across polls. */
|
|
1476
1498
|
loginScreenReported = false;
|
|
1499
|
+
/** #2795: the auth pattern match the token-free check is still to judge; it arms nothing until confirmAuthFailure(). */
|
|
1500
|
+
unverifiedAuthMatch = null;
|
|
1477
1501
|
/**
|
|
1478
1502
|
* While the lifecycle is still starting this daemon, `pty_error` has no
|
|
1479
1503
|
* listener and `InstanceLifecycle.daemons` has no entry — so an incident the
|
|
@@ -1532,19 +1556,12 @@ export class Daemon extends EventEmitter {
|
|
|
1532
1556
|
deliveryWritesStopping = false;
|
|
1533
1557
|
/** #829: the latest pastes, by spawn generation (see ownedInputDraft). */
|
|
1534
1558
|
recentPastes = [];
|
|
1535
|
-
pendingInstructionsUpdate;
|
|
1536
|
-
pendingInstructionsNotice = false;
|
|
1537
1559
|
/** #1633: the one-time "what's new for you" notice, pasted before the next real delivery; recorded once pasted. */
|
|
1538
1560
|
pendingWhatsNew = null;
|
|
1539
|
-
/** #2140: deliveries tried for the pending what's-new notice that never reached the pane (capped: never before every message). */
|
|
1540
|
-
whatsNewUnwritten = 0;
|
|
1541
|
-
// Whether the warmup steering-reload notice should be injected after spawn.
|
|
1542
|
-
// Set in trySpawn by comparing the freshly-built instructions against the
|
|
1543
|
-
// last value the agent was told about (prev-instructions). Skipped when
|
|
1544
|
-
// unchanged so agents don't waste 10-30s re-reading identical steering.
|
|
1545
|
-
warmupNeeded = false;
|
|
1546
|
-
lastBuiltInstructions = "";
|
|
1547
1561
|
pasteQueueDepth = 0;
|
|
1562
|
+
/** #2770: a person's messages a cancel dropped before they reached the CLI, told together (noteCancelledHuman). */
|
|
1563
|
+
cancelledHumans = [];
|
|
1564
|
+
cancelledHumansTimer = null;
|
|
1548
1565
|
firstDeliveryDelay = new FirstDeliveryDelay();
|
|
1549
1566
|
/** Orders /steer pastes against each other. Deliberately NOT pasteLock:
|
|
1550
1567
|
* a steer must not queue behind the normal deliveries it exists to
|
|
@@ -1787,21 +1804,14 @@ export class Daemon extends EventEmitter {
|
|
|
1787
1804
|
return super.emit(event, ...args);
|
|
1788
1805
|
}
|
|
1789
1806
|
/**
|
|
1790
|
-
* Undo
|
|
1791
|
-
* probe
|
|
1792
|
-
*
|
|
1793
|
-
*
|
|
1794
|
-
*
|
|
1795
|
-
* daemon permanently suspicious of an auth failure that never existed. That
|
|
1796
|
-
* state is not inert: authFailureUnresolved suppresses the stuck/hang
|
|
1797
|
-
* notification and holds MCP auto-restart (a later real MCP death is then read
|
|
1798
|
-
* as already-confirmed auth trouble rather than re-verified), and the recovery
|
|
1799
|
-
* gate suppresses further error detection until a ready pattern shows up.
|
|
1800
|
-
*
|
|
1801
|
-
* Only what an auth match armed is rolled back. If some other pattern has
|
|
1802
|
-
* since armed the recovery gate, that one is still live and stays.
|
|
1807
|
+
* Undo an auth suspicion once the backend's token-free probe has said the credentials are fine. Since #2795 a pane
|
|
1808
|
+
* pattern match arms nothing before that probe (its pending match is just dropped); what is left to undo is a
|
|
1809
|
+
* login-screen report, which arms authFailureUnresolved itself. That state is not inert: it suppresses the
|
|
1810
|
+
* stuck/hang notification and holds MCP auto-restart. If some other pattern has armed the recovery gate, that one
|
|
1811
|
+
* is still live and stays.
|
|
1803
1812
|
*/
|
|
1804
1813
|
clearSuspectedAuthFailure() {
|
|
1814
|
+
this.unverifiedAuthMatch = null;
|
|
1805
1815
|
if (!this.authFailureUnresolved && !this.loginScreenReported)
|
|
1806
1816
|
return false;
|
|
1807
1817
|
this.authFailureUnresolved = false;
|
|
@@ -1813,6 +1823,29 @@ export class Daemon extends EventEmitter {
|
|
|
1813
1823
|
this.logger.info("Auth suspicion withdrawn — the token-free probe reports valid credentials");
|
|
1814
1824
|
return true;
|
|
1815
1825
|
}
|
|
1826
|
+
armErrorRecoveryGate(key, type, now) {
|
|
1827
|
+
this.errorWaitingForRecovery = true;
|
|
1828
|
+
this.errorDetectedAt = now;
|
|
1829
|
+
this.errorRecoveryDeadlineAt = now + Daemon.ERROR_RECOVERY_TIMEOUT_MS;
|
|
1830
|
+
this.activeErrorPatternKey = key;
|
|
1831
|
+
this.lastDetectedErrorType = type;
|
|
1832
|
+
}
|
|
1833
|
+
/**
|
|
1834
|
+
* #2795: the token-free check did not vouch for the credentials behind an auth pattern match. Only now does the
|
|
1835
|
+
* match become an auth failure: the state the detector used to arm at match time. Remembered for as long as it is
|
|
1836
|
+
* unresolved — an auth-broken CLI cannot finish the turn it is holding, so every other detector would otherwise
|
|
1837
|
+
* keep reporting the same dead instance. A login-screen report arms its own state and has no match to confirm.
|
|
1838
|
+
*/
|
|
1839
|
+
confirmAuthFailure() {
|
|
1840
|
+
const match = this.unverifiedAuthMatch;
|
|
1841
|
+
if (!match)
|
|
1842
|
+
return false;
|
|
1843
|
+
this.unverifiedAuthMatch = null;
|
|
1844
|
+
if (match.gate)
|
|
1845
|
+
this.armErrorRecoveryGate(match.key, "auth_error", match.at);
|
|
1846
|
+
this.authFailureUnresolved = true;
|
|
1847
|
+
return true;
|
|
1848
|
+
}
|
|
1816
1849
|
clearErrorRecoveryGate() {
|
|
1817
1850
|
this.errorWaitingForRecovery = false;
|
|
1818
1851
|
this.errorDetectedAt = 0;
|
|
@@ -2026,6 +2059,11 @@ export class Daemon extends EventEmitter {
|
|
|
2026
2059
|
this.finishDurableDelivery(delivery, "delivered", `steer-accepted; composer emptied (${verdict.proof.slice("steer-composer-".length)})`, verdict);
|
|
2027
2060
|
return;
|
|
2028
2061
|
}
|
|
2062
|
+
// #2676: an ordinary message typed into that busy composer, proven the same way: taken into the CLI's tray.
|
|
2063
|
+
if (verdict.proof?.startsWith("handoff-composer-")) {
|
|
2064
|
+
this.finishDurableDelivery(delivery, "delivered", `composer-handoff-accepted; composer emptied (${verdict.proof.slice("handoff-composer-".length)})`, verdict);
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
2029
2067
|
// A backend whose input row cannot be read: all that vouched for this delivery is that the pane printed something
|
|
2030
2068
|
// after Enter, which a redraw that wiped the paste satisfies too (#758). Its transcript can say more.
|
|
2031
2069
|
if (verdict.proof === "output-edge") {
|
|
@@ -2385,16 +2423,11 @@ export class Daemon extends EventEmitter {
|
|
|
2385
2423
|
catch { /* may not exist */ }
|
|
2386
2424
|
}
|
|
2387
2425
|
// #1209: conditional continuation for the turn interrupted by the last
|
|
2388
|
-
// restart, if the gates allow. Detached
|
|
2426
|
+
// restart, if the gates allow. Detached; the gate
|
|
2389
2427
|
// consumes its one-shot marker first and never throws into boot.
|
|
2390
2428
|
const legacyResume = this.maybeResumeInterruptedTurn().catch(() => "unknown");
|
|
2391
|
-
// Warmup: wait for CLI idle, then trigger steering reload — but only when
|
|
2392
|
-
// the instructions actually changed since the agent last saw them.
|
|
2393
|
-
// Skipping the no-op reload saves 10-30s of agent time on every restart
|
|
2394
|
-
// where instructions are unchanged.
|
|
2395
|
-
const warmup = this.runWarmupInstructionNotice();
|
|
2396
2429
|
const recoveryOwner = JSON.stringify(this.interactionOwner());
|
|
2397
|
-
void Promise.allSettled([legacyResume
|
|
2430
|
+
void Promise.allSettled([legacyResume]).then(() => {
|
|
2398
2431
|
if (JSON.stringify(this.interactionOwner()) === recoveryOwner) {
|
|
2399
2432
|
this.recoveryBootWorkPending = false;
|
|
2400
2433
|
this.recoveryBootSettled = true;
|
|
@@ -3432,6 +3465,11 @@ export class Daemon extends EventEmitter {
|
|
|
3432
3465
|
if (!(err instanceof UnsupportedCliError))
|
|
3433
3466
|
return false;
|
|
3434
3467
|
this.healthCheckPaused = true;
|
|
3468
|
+
if (err instanceof ResumeBlockedError) {
|
|
3469
|
+
this.logger.error({ reason: err.message }, "Not respawning: retained session needs store repair");
|
|
3470
|
+
this.emitSupervisionEnded(err.message, err.remedy, undefined, "resume_blocked");
|
|
3471
|
+
return true;
|
|
3472
|
+
}
|
|
3435
3473
|
this.logger.error({ reason: err.message }, "Not respawning: the installed CLI cannot run this instance as configured");
|
|
3436
3474
|
this.emitSupervisionEnded(err.message, "Install a CLI version that supports this instance's configuration, then start it again.", undefined, "cli_unsupported");
|
|
3437
3475
|
return true;
|
|
@@ -3743,6 +3781,7 @@ export class Daemon extends EventEmitter {
|
|
|
3743
3781
|
if (!answersBackedOff)
|
|
3744
3782
|
this.dialogAnswers = null;
|
|
3745
3783
|
this.trackDialogParked(blockingSeen);
|
|
3784
|
+
this.trackRemoteCard(blockingSeen, pane, captureMono);
|
|
3746
3785
|
if (blockingSeen)
|
|
3747
3786
|
return; // held dialog: skip error checks this cycle
|
|
3748
3787
|
this.evaluateErrorPatterns(pane, patterns, readyPattern, Date.now(), busyPattern);
|
|
@@ -3786,6 +3825,14 @@ export class Daemon extends EventEmitter {
|
|
|
3786
3825
|
rebasedSeededBaselines.add(key);
|
|
3787
3826
|
}
|
|
3788
3827
|
}
|
|
3828
|
+
// #2795: an auth match the token-free check has not judged yet holds the other detectors, as the armed gate did
|
|
3829
|
+
// before: a real auth failure's text also matches generic patterns (kiro's "having trouble responding"). It is not
|
|
3830
|
+
// an error state — nobody is warned — and it is bounded like the gate.
|
|
3831
|
+
if (this.unverifiedAuthMatch?.gate) {
|
|
3832
|
+
if (now - this.unverifiedAuthMatch.at < Daemon.ERROR_RECOVERY_TIMEOUT_MS)
|
|
3833
|
+
return;
|
|
3834
|
+
this.unverifiedAuthMatch = null;
|
|
3835
|
+
}
|
|
3789
3836
|
// State: waiting for recovery. A missing/outdated ready pattern must not
|
|
3790
3837
|
// suppress every future error forever, so the gate has a hard deadline.
|
|
3791
3838
|
if (this.errorWaitingForRecovery) {
|
|
@@ -3931,25 +3978,20 @@ export class Daemon extends EventEmitter {
|
|
|
3931
3978
|
}
|
|
3932
3979
|
/** Arm the recovery gates and emit one detected error pattern. */
|
|
3933
3980
|
emitErrorPattern(ep, key, pane, now) {
|
|
3981
|
+
// #2795: auth text on the pane proves nothing about the credentials (22 of 22 such matches on the main host were
|
|
3982
|
+
// conversation text). It arms nothing here; it only asks the lifecycle's token-free check, and a failing check
|
|
3983
|
+
// arms the auth state through confirmAuthFailure().
|
|
3984
|
+
if (ep.type === "auth_error")
|
|
3985
|
+
this.unverifiedAuthMatch = { key, at: now, gate: !ep.skipRecoveryWait };
|
|
3934
3986
|
// skipRecoveryWait: this error self-recovers (e.g. a timeout — Kiro is
|
|
3935
3987
|
// back at its prompt immediately). Keep monitoring so the next occurrence
|
|
3936
3988
|
// can fire without waiting for a possibly startup-only ready pattern.
|
|
3937
|
-
if (!ep.skipRecoveryWait)
|
|
3938
|
-
this.
|
|
3939
|
-
this.errorDetectedAt = now;
|
|
3940
|
-
this.errorRecoveryDeadlineAt = now + Daemon.ERROR_RECOVERY_TIMEOUT_MS;
|
|
3941
|
-
this.activeErrorPatternKey = key;
|
|
3942
|
-
this.lastDetectedErrorType = ep.type;
|
|
3943
|
-
}
|
|
3989
|
+
else if (!ep.skipRecoveryWait)
|
|
3990
|
+
this.armErrorRecoveryGate(key, ep.type, now);
|
|
3944
3991
|
this.lastErrorNotifiedAt.set(key, now);
|
|
3945
3992
|
if (ep.action === "failover")
|
|
3946
3993
|
this.lastFailoverAt = now;
|
|
3947
3994
|
const message = this.resolveErrorMessage(pane, ep);
|
|
3948
|
-
// Remember an auth failure for as long as it is unresolved. An auth-broken
|
|
3949
|
-
// CLI cannot finish the turn it is holding, so every other detector in this
|
|
3950
|
-
// scan would otherwise keep reporting the same dead instance forever.
|
|
3951
|
-
if (ep.type === "auth_error")
|
|
3952
|
-
this.authFailureUnresolved = true;
|
|
3953
3995
|
this.logger.warn({ errorType: ep.type, action: ep.action }, `PTY error detected: ${message}`);
|
|
3954
3996
|
this.lastErrorEpisode = { key, pattern: ep.pattern, pane, spawn: this.spawnGeneration, fence: this.launchFenceEpoch };
|
|
3955
3997
|
this.emit("pty_error", { name: this.name, ...ep, message });
|
|
@@ -5124,6 +5166,7 @@ export class Daemon extends EventEmitter {
|
|
|
5124
5166
|
this.pausePending = false;
|
|
5125
5167
|
this.pauseReconfirmAuth = false;
|
|
5126
5168
|
this.authFailureUnresolved = false;
|
|
5169
|
+
this.unverifiedAuthMatch = null;
|
|
5127
5170
|
this.loginScreenReported = false;
|
|
5128
5171
|
this.fatalStartupReported = false;
|
|
5129
5172
|
this.fatalStartupBlocked = false;
|
|
@@ -5968,20 +6011,7 @@ export class Daemon extends EventEmitter {
|
|
|
5968
6011
|
reason,
|
|
5969
6012
|
message: formatReplyDropDetection(turn, reason),
|
|
5970
6013
|
});
|
|
5971
|
-
|
|
5972
|
-
}
|
|
5973
|
-
/** Channel notice for a reply-drop. `rateLimit` shares the per-instance cooldown (#2544). */
|
|
5974
|
-
deliverReplyDropNotice(key, kind, title, turn, reason, rateLimit) {
|
|
5975
|
-
if (rateLimit) {
|
|
5976
|
-
const now = Date.now();
|
|
5977
|
-
if (this.lastReplyDropWarningAt !== 0
|
|
5978
|
-
&& now - this.lastReplyDropWarningAt < REPLY_DROP_WARNING_COOLDOWN_MS) {
|
|
5979
|
-
this.logger.debug({ reason, kind }, "Reply-drop channel notice rate-limited");
|
|
5980
|
-
return;
|
|
5981
|
-
}
|
|
5982
|
-
this.lastReplyDropWarningAt = now;
|
|
5983
|
-
}
|
|
5984
|
-
void this.deliverDaemonReply(t(key), kind, title, turn.target, true);
|
|
6014
|
+
// #2802: the event stays (Log tab); the person is no longer told in the topic or the web chat.
|
|
5985
6015
|
}
|
|
5986
6016
|
/** Relay the pane's final text to the channel, marked as a daemon proxy reply. */
|
|
5987
6017
|
async sendProxyReply(pane, target) {
|
|
@@ -6014,7 +6044,7 @@ export class Daemon extends EventEmitter {
|
|
|
6014
6044
|
}
|
|
6015
6045
|
}
|
|
6016
6046
|
/** Context-bound reply path shared by dead-MCP and malformed-call recovery. */
|
|
6017
|
-
deliverDaemonReply(body, requestPrefix, logLabel, target
|
|
6047
|
+
deliverDaemonReply(body, requestPrefix, logLabel, target) {
|
|
6018
6048
|
const args = { text: body };
|
|
6019
6049
|
const chatId = target?.chatId ?? this.lastChatId;
|
|
6020
6050
|
const threadId = target?.threadId ?? this.lastThreadId;
|
|
@@ -6091,7 +6121,6 @@ export class Daemon extends EventEmitter {
|
|
|
6091
6121
|
args,
|
|
6092
6122
|
fleetRequestId: fleetReqId,
|
|
6093
6123
|
adapterId,
|
|
6094
|
-
statusOnly,
|
|
6095
6124
|
});
|
|
6096
6125
|
}
|
|
6097
6126
|
catch {
|
|
@@ -6267,7 +6296,9 @@ export class Daemon extends EventEmitter {
|
|
|
6267
6296
|
// changed when tmux reported output (observedChangeAt), but idle/stuck are
|
|
6268
6297
|
// decisions about *now*. The old double-observe expressed that by calling
|
|
6269
6298
|
// observe twice, which silently disabled the "content moved" branch.
|
|
6270
|
-
|
|
6299
|
+
if (this.backend?.periodicRedrawFrom)
|
|
6300
|
+
this.periodicRedrawLayout = this.backend.periodicRedrawFrom(pane);
|
|
6301
|
+
const structuredPeriodicIdle = this.periodicIdleProof();
|
|
6271
6302
|
// #1641: the transcript outranks the pane: while it shows the turn running, no reading of this pane is idle (codex
|
|
6272
6303
|
// hides its status row while streaming, leaving the idle composer and footer in view mid-turn).
|
|
6273
6304
|
const transcriptTurn = this.transcriptTurnState();
|
|
@@ -6459,7 +6490,7 @@ export class Daemon extends EventEmitter {
|
|
|
6459
6490
|
return;
|
|
6460
6491
|
this.instanceStateLastOutputAt = event.at;
|
|
6461
6492
|
this.interactionOutputRevision++;
|
|
6462
|
-
const canProvePeriodicIdle = !!this.
|
|
6493
|
+
const canProvePeriodicIdle = !!this.periodicIdleProof();
|
|
6463
6494
|
if (canProvePeriodicIdle || (this.backend?.hasPeriodicPaneRedraw?.() === true && this.instanceState === "idle")) {
|
|
6464
6495
|
// Do not emit idle→working solely because agy repainted an identical
|
|
6465
6496
|
// footer. Codex additionally uses a structural probe while working: two
|
|
@@ -6477,6 +6508,25 @@ export class Daemon extends EventEmitter {
|
|
|
6477
6508
|
this.applyInstanceStateSnapshot(snapshot);
|
|
6478
6509
|
this.scheduleInstanceStateIdleCapture();
|
|
6479
6510
|
this.scheduleInstanceStateStuckDeadline(event.at);
|
|
6511
|
+
// #2770 (ii): a layout that repaints while idle (Claude's background-agent list) never leaves the quiet window the
|
|
6512
|
+
// idle capture waits for, so nothing would look at the pane again before the 60 s sweep. Look now and then.
|
|
6513
|
+
if (this.backend?.periodicRedrawFrom && performance.now() - this.periodicLayoutLookAt >= PERIODIC_LAYOUT_LOOK_MS) {
|
|
6514
|
+
this.periodicLayoutLookAt = performance.now();
|
|
6515
|
+
this.scheduleInstanceStateOutputProbe(STRUCTURED_IDLE_PROBE_MS);
|
|
6516
|
+
}
|
|
6517
|
+
}
|
|
6518
|
+
/**
|
|
6519
|
+
* The backend's structural idle proof for periodically repainting panes, when it applies to the current layout: always
|
|
6520
|
+
* for a backend that repaints in every layout (Codex, Muse), only while the latest capture showed such a layout for one
|
|
6521
|
+
* that names it (CliBackend.periodicRedrawFrom — Claude's background-agent list, #2770 (ii)).
|
|
6522
|
+
*/
|
|
6523
|
+
periodicIdleProof() {
|
|
6524
|
+
const proof = this.backend?.isPeriodicRedrawIdlePane;
|
|
6525
|
+
if (!proof)
|
|
6526
|
+
return undefined;
|
|
6527
|
+
if (this.backend?.periodicRedrawFrom && !this.periodicRedrawLayout)
|
|
6528
|
+
return undefined;
|
|
6529
|
+
return proof;
|
|
6480
6530
|
}
|
|
6481
6531
|
bindInstanceStateOutputListener(windowId) {
|
|
6482
6532
|
if (!this.controlClient || !this.instanceStateMonitorActive)
|
|
@@ -6493,6 +6543,7 @@ export class Daemon extends EventEmitter {
|
|
|
6493
6543
|
startInstanceStateMonitor() {
|
|
6494
6544
|
if (this.runtimeMonitorsFrozen || !this.tmux || !this.backend || this.instanceStateMonitorActive)
|
|
6495
6545
|
return;
|
|
6546
|
+
this.periodicRedrawLayout = false; // a new launch's layout is not the old one's
|
|
6496
6547
|
const rawConfig = this.config.hang_detector;
|
|
6497
6548
|
const timeoutMinutes = rawConfig?.timeout_minutes;
|
|
6498
6549
|
this.instanceStateStuckTimeoutMs = typeof timeoutMinutes === "number" && timeoutMinutes > 0
|
|
@@ -6739,6 +6790,10 @@ export class Daemon extends EventEmitter {
|
|
|
6739
6790
|
this.config.office_access = update.office_access;
|
|
6740
6791
|
else if (update.office_access === null)
|
|
6741
6792
|
delete this.config.office_access;
|
|
6793
|
+
if (typeof update.office_pack_access === "boolean")
|
|
6794
|
+
this.config.office_pack_access = update.office_pack_access;
|
|
6795
|
+
else if (update.office_pack_access === null)
|
|
6796
|
+
delete this.config.office_pack_access;
|
|
6742
6797
|
if (update.dashboard && typeof update.dashboard === "object" && !dashboardAccessProblem(update.dashboard))
|
|
6743
6798
|
this.config.dashboard = update.dashboard;
|
|
6744
6799
|
else if (update.dashboard === null)
|
|
@@ -6905,6 +6960,26 @@ export class Daemon extends EventEmitter {
|
|
|
6905
6960
|
return { receiptId: id, verificationId: meta.verification_id, expectedWireHash: meta.human_wire_sha256,
|
|
6906
6961
|
ownerKey: meta.human_owner_key, current: () => this.captureHumanReceiptOwner() === daemonOwner };
|
|
6907
6962
|
}
|
|
6963
|
+
/**
|
|
6964
|
+
* #2770: a person's message a cancel dropped before it reached the CLI is told, not left as a silent "uncertain":
|
|
6965
|
+
* the messages dropped together go out as one `human_messages_cancelled` (the fleet marks each ❌ and says so once).
|
|
6966
|
+
* Agents' messages are not a person's, and a durable row (delivery_id) is retried by the outbox: neither is ❌'d. A
|
|
6967
|
+
* message with no chat to mark has nowhere to say it.
|
|
6968
|
+
*/
|
|
6969
|
+
noteCancelledHuman(meta) {
|
|
6970
|
+
if (meta.from_instance || meta.delivery_id || !meta.chat_id || !meta.message_id)
|
|
6971
|
+
return;
|
|
6972
|
+
this.cancelledHumans.push({ chatId: meta.chat_id, messageId: meta.message_id, ...(meta.thread_id ? { threadId: meta.thread_id } : {}) });
|
|
6973
|
+
if (this.cancelledHumansTimer)
|
|
6974
|
+
clearTimeout(this.cancelledHumansTimer);
|
|
6975
|
+
this.cancelledHumansTimer = setTimeout(() => {
|
|
6976
|
+
this.cancelledHumansTimer = null;
|
|
6977
|
+
const messages = this.cancelledHumans.splice(0);
|
|
6978
|
+
if (messages.length)
|
|
6979
|
+
this.emit("human_messages_cancelled", { messages });
|
|
6980
|
+
}, CANCELLED_HUMANS_NOTICE_MS);
|
|
6981
|
+
this.cancelledHumansTimer.unref?.();
|
|
6982
|
+
}
|
|
6908
6983
|
finishHumanReceipt(meta, verdict) {
|
|
6909
6984
|
if (!meta.human_receipt_id || !meta.human_owner_key)
|
|
6910
6985
|
return;
|
|
@@ -6979,6 +7054,7 @@ export class Daemon extends EventEmitter {
|
|
|
6979
7054
|
steerMessage(content, meta, deliveryEpoch = this.deliveryEpoch) {
|
|
6980
7055
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
6981
7056
|
this.deferDurableDelivery(meta, "delivery epoch changed before steer began");
|
|
7057
|
+
this.noteCancelledHuman(meta);
|
|
6982
7058
|
return;
|
|
6983
7059
|
}
|
|
6984
7060
|
// The fleet facade prevents a steer from overtaking an item still in its
|
|
@@ -7002,14 +7078,17 @@ export class Daemon extends EventEmitter {
|
|
|
7002
7078
|
this.steerLock = this.steerLock.then(async () => {
|
|
7003
7079
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
7004
7080
|
this.deferDurableDelivery(meta, "delivery epoch changed before steer began");
|
|
7081
|
+
this.noteCancelledHuman(meta);
|
|
7005
7082
|
return;
|
|
7006
7083
|
}
|
|
7007
7084
|
await this.wake();
|
|
7008
7085
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
7009
7086
|
this.deferDurableDelivery(meta, "delivery epoch changed while waking for steer");
|
|
7087
|
+
this.noteCancelledHuman(meta);
|
|
7010
7088
|
return;
|
|
7011
7089
|
}
|
|
7012
7090
|
const verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
|
|
7091
|
+
let delivered = false;
|
|
7013
7092
|
try {
|
|
7014
7093
|
if (await this.deliverMessage(formatted, status, {
|
|
7015
7094
|
steer: true,
|
|
@@ -7019,6 +7098,7 @@ export class Daemon extends EventEmitter {
|
|
|
7019
7098
|
verdict,
|
|
7020
7099
|
durableAttempt: durableAttempt || undefined,
|
|
7021
7100
|
})) {
|
|
7101
|
+
delivered = true;
|
|
7022
7102
|
this.finishDurableSubmission(durableAttempt, verdict);
|
|
7023
7103
|
this.markTurnStarted(meta, formatted, deliveryEpoch);
|
|
7024
7104
|
}
|
|
@@ -7048,6 +7128,8 @@ export class Daemon extends EventEmitter {
|
|
|
7048
7128
|
}
|
|
7049
7129
|
finally {
|
|
7050
7130
|
this.finishHumanReceipt(meta, verdict);
|
|
7131
|
+
if (!delivered && !verdict.paneWriteStarted && !this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
7132
|
+
this.noteCancelledHuman(meta);
|
|
7051
7133
|
}
|
|
7052
7134
|
}).catch(err => {
|
|
7053
7135
|
this.deferDurableDelivery(meta, `target wake/steer failed before pane submission: ${err.message}`);
|
|
@@ -7066,6 +7148,7 @@ export class Daemon extends EventEmitter {
|
|
|
7066
7148
|
btwMessage(content, meta, deliveryEpoch = this.deliveryEpoch) {
|
|
7067
7149
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
7068
7150
|
this.finishHumanReceipt(meta, { reached: false });
|
|
7151
|
+
this.noteCancelledHuman(meta);
|
|
7069
7152
|
return;
|
|
7070
7153
|
}
|
|
7071
7154
|
this.updateLastChat(meta.chat_id, meta.thread_id, meta.adapter_id);
|
|
@@ -7076,22 +7159,28 @@ export class Daemon extends EventEmitter {
|
|
|
7076
7159
|
this.steerLock = this.steerLock.then(async () => {
|
|
7077
7160
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
7078
7161
|
this.finishHumanReceipt(meta, { reached: false });
|
|
7162
|
+
this.noteCancelledHuman(meta);
|
|
7079
7163
|
return;
|
|
7080
7164
|
}
|
|
7081
7165
|
await this.wake();
|
|
7082
7166
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
7083
7167
|
this.finishHumanReceipt(meta, { reached: false });
|
|
7168
|
+
this.noteCancelledHuman(meta);
|
|
7084
7169
|
return;
|
|
7085
7170
|
}
|
|
7086
7171
|
const verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
|
|
7172
|
+
let delivered = false;
|
|
7087
7173
|
try {
|
|
7088
7174
|
if (await this.deliverMessage(formatted, status, { steer: true, deliveryEpoch, submissionId: meta.message_id,
|
|
7089
7175
|
...(meta.verification_id ? { humanVerificationId: meta.verification_id } : {}), verdict })) {
|
|
7176
|
+
delivered = true;
|
|
7090
7177
|
this.markTurnStarted(meta, formatted, deliveryEpoch);
|
|
7091
7178
|
}
|
|
7092
7179
|
}
|
|
7093
7180
|
finally {
|
|
7094
7181
|
this.finishHumanReceipt(meta, verdict);
|
|
7182
|
+
if (!delivered && !verdict.paneWriteStarted && !this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
7183
|
+
this.noteCancelledHuman(meta);
|
|
7095
7184
|
}
|
|
7096
7185
|
}).catch(err => {
|
|
7097
7186
|
this.finishHumanReceipt(meta, { reached: false });
|
|
@@ -7107,6 +7196,7 @@ export class Daemon extends EventEmitter {
|
|
|
7107
7196
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
7108
7197
|
this.logger.info("Pending channel delivery dropped by user cancel");
|
|
7109
7198
|
this.deferDurableDelivery(meta, "delivery epoch changed before queue admission");
|
|
7199
|
+
this.noteCancelledHuman(meta);
|
|
7110
7200
|
return;
|
|
7111
7201
|
}
|
|
7112
7202
|
if (!this.tmux) {
|
|
@@ -7128,10 +7218,6 @@ export class Daemon extends EventEmitter {
|
|
|
7128
7218
|
this.logger.warn({ err: err.message }, "Failed to persist last inbound timestamp");
|
|
7129
7219
|
}
|
|
7130
7220
|
}
|
|
7131
|
-
if (this.pendingInstructionsUpdate) {
|
|
7132
|
-
writeFileSync(join(this.instanceDir, "prev-instructions"), this.pendingInstructionsUpdate);
|
|
7133
|
-
this.pendingInstructionsUpdate = undefined;
|
|
7134
|
-
}
|
|
7135
7221
|
this.pendingWork.recordInbound();
|
|
7136
7222
|
// v3: record user messages for rotation snapshot
|
|
7137
7223
|
this.recordRecentUserMessage(content, meta);
|
|
@@ -7170,6 +7256,7 @@ export class Daemon extends EventEmitter {
|
|
|
7170
7256
|
this.pasteLock = this.pasteLock.then(async () => {
|
|
7171
7257
|
let durable = null;
|
|
7172
7258
|
let verdict = { reached: false };
|
|
7259
|
+
let delivered = false;
|
|
7173
7260
|
try {
|
|
7174
7261
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
|
|
7175
7262
|
this.logger.info("Pending channel delivery dropped by user cancel");
|
|
@@ -7181,44 +7268,11 @@ export class Daemon extends EventEmitter {
|
|
|
7181
7268
|
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
7182
7269
|
return;
|
|
7183
7270
|
}
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
}
|
|
7190
|
-
const whatsNew = this.pendingWhatsNew ? this.currentWhatsNew(this.pendingWhatsNew) : null;
|
|
7191
|
-
if (whatsNew) {
|
|
7192
|
-
// #1633: here, inside the delivery lock and ahead of a real delivery — never into a busy turn, never to an
|
|
7193
|
-
// instance nobody writes to. #2140: told at most once. Once its paste reached the pane it is recorded, proven
|
|
7194
|
-
// submitted or not. A system paste has no unique id: its only pane proof is its opening line on the screen, and
|
|
7195
|
-
// a long notice (every entry since an old session began) scrolls that line above the captured rows once Claude
|
|
7196
|
-
// paints it — unproven, though the agent read it. One that never reached the pane (cancelled, never idle) is
|
|
7197
|
-
// tried again, at most WHATS_NEW_MAX_UNWRITTEN times, then recorded with a warning.
|
|
7198
|
-
const attempt = { reached: false };
|
|
7199
|
-
const pasted = await this.deliverMessage(whatsNew.notice, undefined, { deliveryEpoch, verdict: attempt });
|
|
7200
|
-
if (this.pendingWhatsNew === whatsNew) {
|
|
7201
|
-
const written = pasted || attempt.paneWriteStarted === true;
|
|
7202
|
-
if (!written)
|
|
7203
|
-
this.whatsNewUnwritten++;
|
|
7204
|
-
if (written || this.whatsNewUnwritten >= WHATS_NEW_MAX_UNWRITTEN) {
|
|
7205
|
-
if (!pasted) {
|
|
7206
|
-
this.logger.warn({ entries: whatsNew.ids, written, attempts: this.whatsNewUnwritten, phase: attempt.phase, proof: attempt.proof }, written ? "what's-new notice pasted but its submission could not be proven — recorded so it is not sent again"
|
|
7207
|
-
: "what's-new notice did not reach the pane after several deliveries — recorded so it is not sent again");
|
|
7208
|
-
}
|
|
7209
|
-
this.pendingWhatsNew = null;
|
|
7210
|
-
this.whatsNewUnwritten = 0;
|
|
7211
|
-
try {
|
|
7212
|
-
recordWhatsNew(this.instanceDir, whatsNew.ids, whatsNew.version);
|
|
7213
|
-
}
|
|
7214
|
-
catch (err) {
|
|
7215
|
-
this.logger.warn({ err }, "what's-new record not written — the notice may repeat once");
|
|
7216
|
-
}
|
|
7217
|
-
}
|
|
7218
|
-
}
|
|
7219
|
-
if (!this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
7220
|
-
return;
|
|
7221
|
-
}
|
|
7271
|
+
// #2800: with this real message, in the same paste — never a paste or turn of its own: after its delivery marker
|
|
7272
|
+
// (transcript proof anchors on it), before the message. A person's message with a verified receipt is pasted
|
|
7273
|
+
// byte for byte (its proof is the wire's hash), so the notice waits for the next delivery without one.
|
|
7274
|
+
const whatsNew = this.pendingWhatsNew && !meta.human_receipt_id ? this.currentWhatsNew(this.pendingWhatsNew) : null;
|
|
7275
|
+
const pasted = whatsNew ? withWhatsNew(formatted, whatsNew.notice) : formatted;
|
|
7222
7276
|
const status = channelStatus(meta);
|
|
7223
7277
|
// A fresh delivery begins a fresh turn — its bubble must not inherit
|
|
7224
7278
|
// the previous turn's tool list.
|
|
@@ -7230,13 +7284,24 @@ export class Daemon extends EventEmitter {
|
|
|
7230
7284
|
return;
|
|
7231
7285
|
}
|
|
7232
7286
|
durable = durableAttempt;
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7287
|
+
let sent = false;
|
|
7288
|
+
try {
|
|
7289
|
+
sent = await this.deliverMessage(pasted, status, {
|
|
7290
|
+
deliveryEpoch,
|
|
7291
|
+
submissionId: meta.message_id,
|
|
7292
|
+
...(meta.verification_id ? { humanVerificationId: meta.verification_id } : {}),
|
|
7293
|
+
verdict,
|
|
7294
|
+
durableAttempt: durableAttempt || undefined,
|
|
7295
|
+
});
|
|
7296
|
+
}
|
|
7297
|
+
finally {
|
|
7298
|
+
// #2800: told once — recorded as soon as it went out with this message, proven submitted or not, also when
|
|
7299
|
+
// a later step of the write threw; a paste that never reached the pane keeps it for the next message.
|
|
7300
|
+
if (whatsNew && (sent || verdict.paneWriteStarted))
|
|
7301
|
+
this.recordWhatsNewSent(whatsNew);
|
|
7302
|
+
}
|
|
7303
|
+
if (sent) {
|
|
7304
|
+
delivered = true;
|
|
7240
7305
|
this.finishDurableSubmission(durable, verdict);
|
|
7241
7306
|
this.markTurnStarted(meta, formatted, deliveryEpoch);
|
|
7242
7307
|
}
|
|
@@ -7282,6 +7347,8 @@ export class Daemon extends EventEmitter {
|
|
|
7282
7347
|
}
|
|
7283
7348
|
finally {
|
|
7284
7349
|
this.finishHumanReceipt(meta, verdict);
|
|
7350
|
+
if (!delivered && !verdict.paneWriteStarted && !this.isDeliveryEpochCurrent(deliveryEpoch))
|
|
7351
|
+
this.noteCancelledHuman(meta);
|
|
7285
7352
|
this.pasteQueueDepth--;
|
|
7286
7353
|
}
|
|
7287
7354
|
}).catch(err => {
|
|
@@ -7346,12 +7413,29 @@ export class Daemon extends EventEmitter {
|
|
|
7346
7413
|
return false;
|
|
7347
7414
|
let windowId = this.getWindowId();
|
|
7348
7415
|
const supportsQueuedInput = this.backend?.supportsQueuedInput?.() === true;
|
|
7416
|
+
const composerQueue = !opts?.steer && this.composerTakesBusyInput() && this.composerCanShowPaste(formatted);
|
|
7349
7417
|
let handingOffToNativeQueue = false;
|
|
7350
7418
|
// If the CLI is busy, either hand the complete submission to its native input
|
|
7351
7419
|
// queue or wait for idle. Native queue support is an explicit backend capability:
|
|
7352
7420
|
// normal Enter input has steering/interrupt semantics in several other CLIs.
|
|
7353
7421
|
let readiness = "ready";
|
|
7354
7422
|
const gateWindowId = windowId;
|
|
7423
|
+
const handOffEligible = !!(supportsQueuedInput || opts?.steer || composerQueue);
|
|
7424
|
+
const handOffAdmissible = async () => (await this.probeBlockingDialog()).state === "clear"
|
|
7425
|
+
&& await this.hasPositiveDeliveryInput(true)
|
|
7426
|
+
&& ((!opts?.steer && !composerQueue) || (this.launchTakesSteer() && await this.steerComposerAdmits(false, opts?.steer === true)));
|
|
7427
|
+
// #2770: a delivery waiting for idle holds the paste queue, and every message behind it. It stops waiting at once
|
|
7428
|
+
// on a cancel, and as soon as the busy pane can take it the way it would have on arrival (a box that reads again, a
|
|
7429
|
+
// dialog gone) — rather than for the end of whatever keeps the pane moving (2026-10-27: 11 minutes).
|
|
7430
|
+
let handOffCheckedAt = -Infinity;
|
|
7431
|
+
const waitStop = async () => {
|
|
7432
|
+
if (cancelled())
|
|
7433
|
+
return true;
|
|
7434
|
+
if (!handOffEligible || !gateWindowId || performance.now() - handOffCheckedAt < HANDOFF_RECHECK_MS)
|
|
7435
|
+
return false;
|
|
7436
|
+
handOffCheckedAt = performance.now();
|
|
7437
|
+
return (await this.paneReadinessForDelivery(gateWindowId)) === "busy" && await handOffAdmissible() && !cancelled();
|
|
7438
|
+
};
|
|
7355
7439
|
if (gateWindowId && this.controlClient)
|
|
7356
7440
|
readiness = await this.paneReadinessForDelivery(gateWindowId);
|
|
7357
7441
|
if (readiness !== "ready") {
|
|
@@ -7362,11 +7446,9 @@ export class Daemon extends EventEmitter {
|
|
|
7362
7446
|
// press Enter at once. A dialog painted a moment ago is still "busy" to
|
|
7363
7447
|
// the silence gate, so the probe is mandatory here; "dialog"/"unknown"
|
|
7364
7448
|
// fall through to the wait.
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
&& await this.hasPositiveDeliveryInput(true)
|
|
7369
|
-
&& (!opts?.steer || (this.launchTakesSteer() && await this.steerComposerAdmits(false)));
|
|
7449
|
+
// #2676: an ordinary message goes into a mode-switched composer (kiro TUI) mid-turn too, on a launch whose busy
|
|
7450
|
+
// composer AgEnD drives, by the steer path's own gates and proof — when its box can show it is this paste.
|
|
7451
|
+
const canHandOff = handOffEligible && readiness === "busy" && await handOffAdmissible();
|
|
7370
7452
|
if (canHandOff) {
|
|
7371
7453
|
// Native queue (codex), or an explicit /steer: hand the complete
|
|
7372
7454
|
// paste+Enter transaction to the busy CLI now. For steer this is the
|
|
@@ -7374,14 +7456,20 @@ export class Daemon extends EventEmitter {
|
|
|
7374
7456
|
// visibility check + idle-gated fallback catch TUIs that swallow
|
|
7375
7457
|
// busy input (see steerMessage).
|
|
7376
7458
|
handingOffToNativeQueue = true;
|
|
7377
|
-
this.logger.debug(opts?.steer ? "CLI busy — steering into the running turn"
|
|
7459
|
+
this.logger.debug(opts?.steer ? "CLI busy — steering into the running turn"
|
|
7460
|
+
: composerQueue ? "CLI busy — typing message into the busy composer"
|
|
7461
|
+
: "CLI busy — handing message to backend-native input queue");
|
|
7378
7462
|
}
|
|
7379
7463
|
else {
|
|
7380
7464
|
this.logger.debug("CLI busy — queuing message until idle");
|
|
7381
|
-
const becameIdle = await this.waitForPaneReadyForDelivery(gateWindowId);
|
|
7465
|
+
const becameIdle = await this.waitForPaneReadyForDelivery(gateWindowId, undefined, waitStop);
|
|
7382
7466
|
if (cancelled())
|
|
7383
7467
|
return false;
|
|
7384
|
-
if (
|
|
7468
|
+
if (becameIdle === "stopped") {
|
|
7469
|
+
handingOffToNativeQueue = true;
|
|
7470
|
+
this.logger.info("The busy CLI can take this delivery now — handing it off instead of waiting for idle (#2770)");
|
|
7471
|
+
}
|
|
7472
|
+
else if (!becameIdle) {
|
|
7385
7473
|
// The pane never freed up. Report the failure instead of pasting into a
|
|
7386
7474
|
// wedged CLI (where the text would sit unsubmitted and the next message
|
|
7387
7475
|
// would land on top of it) — and instead of holding the queue silently.
|
|
@@ -7468,9 +7556,9 @@ export class Daemon extends EventEmitter {
|
|
|
7468
7556
|
// #1405: a launch that no longer takes a steer is not the one the gate saw — redo from the top (bounded), where
|
|
7469
7557
|
// the gate sends it down the idle path. The composer can also have changed mode since (the user's Ctrl+S), or
|
|
7470
7558
|
// the turn ended.
|
|
7471
|
-
if (handingOffToNativeQueue && opts?.steer && !this.launchTakesSteer())
|
|
7559
|
+
if (handingOffToNativeQueue && (opts?.steer || composerQueue) && !this.launchTakesSteer())
|
|
7472
7560
|
return "spawn-started";
|
|
7473
|
-
if (handingOffToNativeQueue && opts?.steer && !(await this.steerComposerAdmits(true)))
|
|
7561
|
+
if (handingOffToNativeQueue && (opts?.steer || composerQueue) && !(await this.steerComposerAdmits(true, opts?.steer === true)))
|
|
7474
7562
|
return "dialog";
|
|
7475
7563
|
// #829: a CLI that restores a cancelled prompt into its input box would
|
|
7476
7564
|
// have this message pasted onto it and both submitted as one. Clear it
|
|
@@ -7526,9 +7614,13 @@ export class Daemon extends EventEmitter {
|
|
|
7526
7614
|
return this.failDelivery(verdict, status, "pre-write", "dialog-or-input-not-ready");
|
|
7527
7615
|
}
|
|
7528
7616
|
this.logger.info("Dialog or input transition appeared before the pane write — waiting for readiness");
|
|
7529
|
-
const clear = gateWindowId ? await this.waitForPaneReadyForDelivery(gateWindowId) : false;
|
|
7617
|
+
const clear = gateWindowId ? await this.waitForPaneReadyForDelivery(gateWindowId, undefined, waitStop) : false;
|
|
7530
7618
|
if (cancelled())
|
|
7531
7619
|
return false;
|
|
7620
|
+
if (clear === "stopped") {
|
|
7621
|
+
handingOffToNativeQueue = true;
|
|
7622
|
+
continue;
|
|
7623
|
+
}
|
|
7532
7624
|
if (!clear) {
|
|
7533
7625
|
return this.failDelivery(verdict, status, "pre-write", "readiness-timeout");
|
|
7534
7626
|
}
|
|
@@ -7968,9 +8060,11 @@ export class Daemon extends EventEmitter {
|
|
|
7968
8060
|
&& dialogs.some(other => other !== dialog && Daemon.dialogMatches(other, pane)))
|
|
7969
8061
|
continue;
|
|
7970
8062
|
this.trackDialogParked(dialog);
|
|
8063
|
+
this.trackRemoteCard(dialog, pane, probeMono);
|
|
7971
8064
|
return { state: "dialog", dialog };
|
|
7972
8065
|
}
|
|
7973
8066
|
this.trackDialogParked(null);
|
|
8067
|
+
this.trackRemoteCard(null, pane, probeMono);
|
|
7974
8068
|
return { state: "clear" };
|
|
7975
8069
|
}
|
|
7976
8070
|
/**
|
|
@@ -8211,6 +8305,77 @@ export class Daemon extends EventEmitter {
|
|
|
8211
8305
|
this.logger.warn({ dialog: dialog.description, attempts: episode.ignored, holdsDeliveries }, "Runtime dialog is ignoring AgEnD's answer — reporting for a human");
|
|
8212
8306
|
this.emit("dialog_answer_ignored", { name: this.name, description: dialog.description, attempts: episode.ignored, holdsDeliveries });
|
|
8213
8307
|
}
|
|
8308
|
+
/**
|
|
8309
|
+
* #2675: a held card a person can answer from the chat (the dialog's remoteCard reader) is posted once it has stayed
|
|
8310
|
+
* the same card for REMOTE_CARD_STABLE_MS — `remote_card` with what it asks; the fleet posts it with its buttons. When
|
|
8311
|
+
* it closes or another card replaces it, `remote_card_gone` retires that post. Moving through the card (ours or the
|
|
8312
|
+
* person's in the pane) is the same card: its identity has no cursor. `readAt` is when the pane's capture was started
|
|
8313
|
+
* (performance.now): a read that started before the newest one used here finishes late and says nothing about now —
|
|
8314
|
+
* it may neither retire a newer card nor bring an older one back (#2677 review).
|
|
8315
|
+
*/
|
|
8316
|
+
trackRemoteCard(dialog, pane, readAt) {
|
|
8317
|
+
if (readAt < this.remoteCardReadAt)
|
|
8318
|
+
return;
|
|
8319
|
+
this.remoteCardReadAt = readAt;
|
|
8320
|
+
const card = dialog?.remoteCard?.(pane) ?? null;
|
|
8321
|
+
const shown = this.remoteCardShown;
|
|
8322
|
+
if (shown && shown.identity !== card?.identity) {
|
|
8323
|
+
this.remoteCardShown = null;
|
|
8324
|
+
if (shown.posted)
|
|
8325
|
+
this.emit("remote_card_gone", { name: this.name, identity: shown.identity });
|
|
8326
|
+
}
|
|
8327
|
+
if (!card)
|
|
8328
|
+
return;
|
|
8329
|
+
if (!this.remoteCardShown) {
|
|
8330
|
+
this.remoteCardShown = { identity: card.identity, since: performance.now(), posted: false };
|
|
8331
|
+
return;
|
|
8332
|
+
}
|
|
8333
|
+
if (this.remoteCardShown.posted || performance.now() - this.remoteCardShown.since < REMOTE_CARD_STABLE_MS)
|
|
8334
|
+
return;
|
|
8335
|
+
this.remoteCardShown.posted = true;
|
|
8336
|
+
this.logger.info({ kind: card.kind }, "A card a person can answer from the chat is on screen — posting it");
|
|
8337
|
+
this.emit("remote_card", { name: this.name, card });
|
|
8338
|
+
}
|
|
8339
|
+
/** #2675: the card's post was used up without answering it (refused, stopped part-way): post it again once it holds still. */
|
|
8340
|
+
repostRemoteCard() {
|
|
8341
|
+
this.remoteCardShown = null;
|
|
8342
|
+
}
|
|
8343
|
+
/**
|
|
8344
|
+
* #2675: answer the card on screen from the chat, as the person chose: only while it is the card they saw
|
|
8345
|
+
* (`identity`), on a verified cursor, under the pane write lock (remote-card-answer.ts). Never logs the answer.
|
|
8346
|
+
*/
|
|
8347
|
+
async answerRemoteCard(identity, action) {
|
|
8348
|
+
const tmux = this.tmux;
|
|
8349
|
+
const readers = this.runtimeDialogs().filter(dialog => dialog.remoteCard);
|
|
8350
|
+
if (!tmux || readers.length === 0)
|
|
8351
|
+
return { outcome: "stale" };
|
|
8352
|
+
if (this.remoteCardAnswering)
|
|
8353
|
+
return { outcome: "refused", reason: "another answer to this card is being keyed in" };
|
|
8354
|
+
this.remoteCardAnswering = true;
|
|
8355
|
+
try {
|
|
8356
|
+
const outcome = await answerRemoteCard({
|
|
8357
|
+
locked: fn => this.paneWriteLock.run(fn),
|
|
8358
|
+
// An unreadable screen (a failed capture, another spawn's pane) is unknown — never "the card closed".
|
|
8359
|
+
read: () => this.tmux === tmux ? tmux.capturePane().catch(() => null) : Promise.resolve(null),
|
|
8360
|
+
key: key => this.tmux !== tmux ? Promise.resolve(false) : key === "Space" ? tmux.sendKeys(" ") : tmux.sendSpecialKey(key),
|
|
8361
|
+
paste: text => this.tmux !== tmux ? Promise.resolve(false) : tmux.pasteBuffer(text),
|
|
8362
|
+
sleep: ms => new Promise(resolve => setTimeout(resolve, ms)),
|
|
8363
|
+
now: () => performance.now(),
|
|
8364
|
+
}, pane => {
|
|
8365
|
+
for (const dialog of readers) {
|
|
8366
|
+
const card = dialog.remoteCard(pane);
|
|
8367
|
+
if (card)
|
|
8368
|
+
return card;
|
|
8369
|
+
}
|
|
8370
|
+
return null;
|
|
8371
|
+
}, identity, action);
|
|
8372
|
+
this.logger.info({ action: action.kind, ...outcome }, "Answered a card from the chat");
|
|
8373
|
+
return outcome;
|
|
8374
|
+
}
|
|
8375
|
+
finally {
|
|
8376
|
+
this.remoteCardAnswering = false;
|
|
8377
|
+
}
|
|
8378
|
+
}
|
|
8214
8379
|
/**
|
|
8215
8380
|
* Remember how long a blocking dialog has been on screen. Once it has
|
|
8216
8381
|
* outlived DIALOG_PARKED_NOTIFY_MS — the auto-dismiss did not take, or the
|
|
@@ -8314,21 +8479,48 @@ export class Daemon extends EventEmitter {
|
|
|
8314
8479
|
* while a fresh capture reads its composer as "steer" — or "idle" once the turn has ended (`allowIdle`, the pre-write
|
|
8315
8480
|
* re-check). "queue" (the user's choice), text in the box, or a screen it cannot read: not now — the caller waits for
|
|
8316
8481
|
* the idle prompt as for any message. AgEnD never switches the mode. Backends without the reader are not asked.
|
|
8482
|
+
* #2676: an ordinary message (`steerOnly` false) goes in on "queue" too — kiro takes it into the same tray either way,
|
|
8483
|
+
* held for the turn's end instead of steered into it.
|
|
8317
8484
|
*/
|
|
8318
|
-
async steerComposerAdmits(allowIdle) {
|
|
8485
|
+
async steerComposerAdmits(allowIdle, steerOnly = true) {
|
|
8319
8486
|
const read = this.backend?.readSteerComposer;
|
|
8320
8487
|
if (!read)
|
|
8321
8488
|
return true;
|
|
8322
8489
|
if (!this.tmux)
|
|
8323
8490
|
return false;
|
|
8324
|
-
let
|
|
8491
|
+
let pane;
|
|
8325
8492
|
try {
|
|
8326
|
-
|
|
8493
|
+
pane = await this.tmux.capturePane();
|
|
8327
8494
|
}
|
|
8328
8495
|
catch {
|
|
8329
8496
|
return false;
|
|
8330
8497
|
}
|
|
8331
|
-
return
|
|
8498
|
+
return composerModeAdmits(read.call(this.backend, pane), allowIdle, steerOnly) && this.busyInputReachesTurn(pane);
|
|
8499
|
+
}
|
|
8500
|
+
/** #2682: a busy composer's typed input reaches the CLI's own turn (busyTurnTakesInput); an idle box, or no reader: yes. */
|
|
8501
|
+
busyInputReachesTurn(pane) {
|
|
8502
|
+
const mode = this.backend?.readSteerComposer?.(pane);
|
|
8503
|
+
if (mode !== "steer" && mode !== "queue")
|
|
8504
|
+
return true;
|
|
8505
|
+
return this.backend?.busyTurnTakesInput?.(pane) !== false;
|
|
8506
|
+
}
|
|
8507
|
+
/**
|
|
8508
|
+
* #2676: whether an ordinary message may be typed into this CLI's busy composer — a mode-switched one (kiro TUI) on a
|
|
8509
|
+
* launch whose busy composer AgEnD drives (the same evidence as a steer: KIRO_STEER_VERIFIED). The legacy UI, an
|
|
8510
|
+
* unlisted version and every backend without the reader answer false; claude and codex hand off by
|
|
8511
|
+
* supportsQueuedInput instead.
|
|
8512
|
+
*/
|
|
8513
|
+
composerTakesBusyInput() {
|
|
8514
|
+
return !!this.backend?.readSteerComposer && this.backend.supportsSteer?.() === true;
|
|
8515
|
+
}
|
|
8516
|
+
/**
|
|
8517
|
+
* #2676: whether the box can show this payload as this delivery's paste before the Enter (steerPasteIsOurs): its
|
|
8518
|
+
* first line, or a collapsed paste's line count. A one-line payload kiro would collapse (over 500 characters) shows only
|
|
8519
|
+
* a character count, which kiro's own normalisation (tabs widened) makes no proof: it waits for the idle prompt.
|
|
8520
|
+
* kiro measures after widening each tab to its 2-column stop, so a tab is counted as 2 here (at most what it becomes).
|
|
8521
|
+
*/
|
|
8522
|
+
composerCanShowPaste(formatted) {
|
|
8523
|
+
return /[\r\n]/.test(formatted) || formatted.replace(/\t/g, " ").length <= KIRO_COLLAPSE_CHARS;
|
|
8332
8524
|
}
|
|
8333
8525
|
/**
|
|
8334
8526
|
* #1405: one look at a mode-switched composer for a steer's own proof: on the tmux of this write, inside `deadline`
|
|
@@ -8362,10 +8554,18 @@ export class Daemon extends EventEmitter {
|
|
|
8362
8554
|
if (first === "")
|
|
8363
8555
|
return false;
|
|
8364
8556
|
const row = norm(shown);
|
|
8557
|
+
// #2676: a long paste is collapsed to a chip naming its line count (kiro TUI: more than 10 lines or 500 characters);
|
|
8558
|
+
// it is this paste when that count is exactly this payload's as kiro counts it. tmux pastes every LF as CR (no -r),
|
|
8559
|
+
// so a CRLF arrives as CR CR, and kiro turns each CR into a line break: every CR and every LF is one.
|
|
8560
|
+
const collapsed = this.backend?.readSteerComposerPasteLines?.(shown);
|
|
8561
|
+
if (collapsed != null)
|
|
8562
|
+
return collapsed > 1 && collapsed === formatted.split(/\r|\n/).length;
|
|
8365
8563
|
// The marker line holds nothing but the marker: the row is exactly it.
|
|
8366
8564
|
if (/^\[agend-(?:delivery|human)-id:[^\]\s]+\]$/.test(first))
|
|
8367
8565
|
return row === first;
|
|
8368
|
-
|
|
8566
|
+
// #2676: a first line wider than the box is word-wrapped onto indented rows (live, 2.34.0): the row is its start, cut
|
|
8567
|
+
// at a word boundary.
|
|
8568
|
+
return row === first || row.startsWith(`${first} `) || first.startsWith(`${row} `);
|
|
8369
8569
|
}
|
|
8370
8570
|
/**
|
|
8371
8571
|
* Codex can paint a prompt before the TTY enters raw mode. Both are required.
|
|
@@ -8512,6 +8712,18 @@ export class Daemon extends EventEmitter {
|
|
|
8512
8712
|
if (this.needsStartupInputProof()) {
|
|
8513
8713
|
return await this.hasPositiveDeliveryInput() ? "ready" : "transient";
|
|
8514
8714
|
}
|
|
8715
|
+
// #2682: a backend that can read its own idle input box (kiro's TUI) must show it before an idle paste. Anything
|
|
8716
|
+
// else — another program owning the pane, a frame it cannot read, a capture that fails — is not one to type into.
|
|
8717
|
+
if (this.backend?.idleComposerReady && this.tmux) {
|
|
8718
|
+
try {
|
|
8719
|
+
if (!this.backend.idleComposerReady(await this.tmux.capturePane()))
|
|
8720
|
+
return "busy";
|
|
8721
|
+
}
|
|
8722
|
+
catch (err) {
|
|
8723
|
+
this.logger.debug({ err }, "capture-pane failed during the composer check — treating the pane as not ready");
|
|
8724
|
+
return "busy";
|
|
8725
|
+
}
|
|
8726
|
+
}
|
|
8515
8727
|
if (this.backend?.dropsEnterWhileBusy?.() !== true)
|
|
8516
8728
|
return "ready";
|
|
8517
8729
|
const prompt = this.backend.getBottomReadyPattern?.();
|
|
@@ -8545,13 +8757,7 @@ export class Daemon extends EventEmitter {
|
|
|
8545
8757
|
return "busy";
|
|
8546
8758
|
}
|
|
8547
8759
|
}
|
|
8548
|
-
|
|
8549
|
-
* Like waitForPaneIdleForDelivery, plus the bottom-anchored prompt check for
|
|
8550
|
-
* Enter-dropping TUIs. Polls at a coarse cadence (each poll is a capture-pane)
|
|
8551
|
-
* only after the pane is already quiet, so the common fast-idle delivery still
|
|
8552
|
-
* costs a single capture.
|
|
8553
|
-
*/
|
|
8554
|
-
async waitForPaneReadyForDelivery(windowId, timeoutMs = 30 * 60_000) {
|
|
8760
|
+
async waitForPaneReadyForDelivery(windowId, timeoutMs = 30 * 60_000, stop) {
|
|
8555
8761
|
// Every backend polls: readiness now also means "no blocking dialog on
|
|
8556
8762
|
// screen", and a dialog disappears without any output edge the silence
|
|
8557
8763
|
// gate could see.
|
|
@@ -8564,9 +8770,10 @@ export class Daemon extends EventEmitter {
|
|
|
8564
8770
|
const remaining = deadline - Date.now();
|
|
8565
8771
|
if (remaining <= 0)
|
|
8566
8772
|
return false;
|
|
8567
|
-
const idle = await this.waitForPaneIdleForDelivery(windowId, remaining)
|
|
8568
|
-
|
|
8569
|
-
|
|
8773
|
+
const idle = stop ? await this.waitForPaneIdleForDelivery(windowId, remaining, stop)
|
|
8774
|
+
: await this.waitForPaneIdleForDelivery(windowId, remaining);
|
|
8775
|
+
if (idle !== true)
|
|
8776
|
+
return idle;
|
|
8570
8777
|
// The idle wait just resolved, so re-reading the silence gate would only
|
|
8571
8778
|
// add a stale-cache race; ask the pane the remaining questions directly.
|
|
8572
8779
|
const transient = await this.probeInputTransient();
|
|
@@ -8586,6 +8793,8 @@ export class Daemon extends EventEmitter {
|
|
|
8586
8793
|
this.retireInputTransients("transient stalled");
|
|
8587
8794
|
return false;
|
|
8588
8795
|
}
|
|
8796
|
+
if (stop && await stop())
|
|
8797
|
+
return "stopped";
|
|
8589
8798
|
await new Promise(r => setTimeout(r, BOTTOM_READY_POLL_MS));
|
|
8590
8799
|
continue;
|
|
8591
8800
|
}
|
|
@@ -8595,6 +8804,8 @@ export class Daemon extends EventEmitter {
|
|
|
8595
8804
|
this.logger.error({ probes: unknownStreak }, "Pane stayed unreadable during the input-transient probe — refusing to deliver blind");
|
|
8596
8805
|
return false;
|
|
8597
8806
|
}
|
|
8807
|
+
if (stop && await stop())
|
|
8808
|
+
return "stopped";
|
|
8598
8809
|
await new Promise(r => setTimeout(r, BOTTOM_READY_POLL_MS));
|
|
8599
8810
|
continue;
|
|
8600
8811
|
}
|
|
@@ -8616,6 +8827,8 @@ export class Daemon extends EventEmitter {
|
|
|
8616
8827
|
unknownStreak = 0;
|
|
8617
8828
|
}
|
|
8618
8829
|
}
|
|
8830
|
+
if (stop && await stop())
|
|
8831
|
+
return "stopped";
|
|
8619
8832
|
await new Promise(r => setTimeout(r, BOTTOM_READY_POLL_MS));
|
|
8620
8833
|
}
|
|
8621
8834
|
}
|
|
@@ -9023,9 +9236,11 @@ export class Daemon extends EventEmitter {
|
|
|
9023
9236
|
return observedAt >= lastOutputAt
|
|
9024
9237
|
&& observedAt >= this.controlClient.getObservationResetAt();
|
|
9025
9238
|
}
|
|
9026
|
-
waitForPaneIdleForDelivery(windowId, timeoutMs = 30 * 60_000) {
|
|
9239
|
+
waitForPaneIdleForDelivery(windowId, timeoutMs = 30 * 60_000, stop) {
|
|
9027
9240
|
if (!this.controlClient)
|
|
9028
9241
|
return Promise.resolve(true);
|
|
9242
|
+
if (stop)
|
|
9243
|
+
return this.waitForPaneIdleOrStop(windowId, timeoutMs, stop);
|
|
9029
9244
|
if (!this.canTrustRedrawIdleState()) {
|
|
9030
9245
|
return this.controlClient.waitUntilIdle(windowId, timeoutMs);
|
|
9031
9246
|
}
|
|
@@ -9045,6 +9260,44 @@ export class Daemon extends EventEmitter {
|
|
|
9045
9260
|
}, timeoutMs);
|
|
9046
9261
|
});
|
|
9047
9262
|
}
|
|
9263
|
+
/**
|
|
9264
|
+
* #2770: waitForPaneIdleForDelivery that also ends as soon as `stop` says so ("stopped"). The same idle readings
|
|
9265
|
+
* (tmux silence, or the state machine where its idle is trusted), polled here so nothing outlives a stopped wait.
|
|
9266
|
+
*/
|
|
9267
|
+
waitForPaneIdleOrStop(windowId, timeoutMs, stop) {
|
|
9268
|
+
const control = this.controlClient;
|
|
9269
|
+
const trusted = this.canTrustRedrawIdleState();
|
|
9270
|
+
if (trusted && this.isPaneIdleForDelivery(windowId))
|
|
9271
|
+
return Promise.resolve(true);
|
|
9272
|
+
return new Promise(resolve => {
|
|
9273
|
+
let done = false, asking = false, askedAt = -Infinity;
|
|
9274
|
+
const finish = (outcome) => {
|
|
9275
|
+
if (done)
|
|
9276
|
+
return;
|
|
9277
|
+
done = true;
|
|
9278
|
+
clearInterval(check);
|
|
9279
|
+
clearTimeout(timer);
|
|
9280
|
+
resolve(outcome);
|
|
9281
|
+
};
|
|
9282
|
+
// The same idle readings as the plain wait: tmux silence (abandoned once this wait ends), or the state machine.
|
|
9283
|
+
if (!trusted) {
|
|
9284
|
+
control.waitUntilIdle(windowId, timeoutMs, () => done).then(idle => finish(idle), () => finish(false));
|
|
9285
|
+
}
|
|
9286
|
+
const check = setInterval(() => {
|
|
9287
|
+
if (trusted && this.isPaneIdleForDelivery(windowId)) {
|
|
9288
|
+
finish(true);
|
|
9289
|
+
return;
|
|
9290
|
+
}
|
|
9291
|
+
if (asking || performance.now() - askedAt < WAIT_STOP_POLL_MS)
|
|
9292
|
+
return;
|
|
9293
|
+
asking = true;
|
|
9294
|
+
askedAt = performance.now();
|
|
9295
|
+
stop().then(stopNow => { asking = false; if (stopNow)
|
|
9296
|
+
finish("stopped"); }, err => { asking = false; this.logger.debug({ err }, "Delivery wait stop check failed — still waiting"); });
|
|
9297
|
+
}, 50);
|
|
9298
|
+
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
9299
|
+
});
|
|
9300
|
+
}
|
|
9048
9301
|
/**
|
|
9049
9302
|
* Antigravity has always opted into pane-state delivery readiness because its
|
|
9050
9303
|
* footer redraws while idle. Codex's structural idle proof is useful only
|
|
@@ -9057,7 +9310,7 @@ export class Daemon extends EventEmitter {
|
|
|
9057
9310
|
canTrustRedrawIdleState() {
|
|
9058
9311
|
if (this.backend?.hasPeriodicPaneRedraw?.() === true)
|
|
9059
9312
|
return true;
|
|
9060
|
-
return this.instanceStateMonitorActive && !!this.
|
|
9313
|
+
return this.instanceStateMonitorActive && !!this.periodicIdleProof();
|
|
9061
9314
|
}
|
|
9062
9315
|
/**
|
|
9063
9316
|
* The write half of a delivery: paste → Enter → confirm, with retries.
|
|
@@ -9155,15 +9408,16 @@ export class Daemon extends EventEmitter {
|
|
|
9155
9408
|
}
|
|
9156
9409
|
// #1405: on that same last capture a mode-switched composer (kiro TUI) must still read "steer" — or "idle", the
|
|
9157
9410
|
// turn over. "queue", text in the box or an unreadable row: not written; handled as the unread box above.
|
|
9158
|
-
|
|
9159
|
-
|
|
9411
|
+
// #2676: an ordinary message typed into it (every other hand-off there) may also find "queue".
|
|
9412
|
+
if (handingOffToNativeQueue && this.backend?.readSteerComposer
|
|
9413
|
+
&& (!composerModeAdmits(pasteBaseline?.steerComposer ?? null, true, steer) || pasteBaseline?.busyInputReachesTurn === false)) {
|
|
9160
9414
|
if (!verdict.durableBeginCommitted)
|
|
9161
9415
|
return "handoff-box-unread";
|
|
9162
|
-
return this.failDelivery(verdict, status, "paste", "steer-composer-not-steering");
|
|
9416
|
+
return this.failDelivery(verdict, status, "paste", steer ? "steer-composer-not-steering" : "composer-not-empty");
|
|
9163
9417
|
}
|
|
9164
9418
|
// …and the launch must still take a steer (see launchTakesSteer). Not written: before the durable begin the row
|
|
9165
|
-
// is retried, as a steer the gate will then send down the idle path.
|
|
9166
|
-
if (handingOffToNativeQueue && steer && !this.launchTakesSteer()) {
|
|
9419
|
+
// is retried, as a steer the gate will then send down the idle path (#2676: so is a message typed into the box).
|
|
9420
|
+
if (handingOffToNativeQueue && (steer || this.backend?.readSteerComposer) && !this.launchTakesSteer()) {
|
|
9167
9421
|
verdict.fenced = !verdict.durableBeginCommitted;
|
|
9168
9422
|
return false;
|
|
9169
9423
|
}
|
|
@@ -9293,7 +9547,8 @@ export class Daemon extends EventEmitter {
|
|
|
9293
9547
|
// #1405: a steer into a mode-switched composer (kiro TUI) is proven by the box alone — before the Enter it must
|
|
9294
9548
|
// positively hold typed text, and after it be the empty placeholder again. The empty placeholder here means the
|
|
9295
9549
|
// paste never reached the box; an unreadable box (a failed or late capture, a dialog) proves nothing: no Enter.
|
|
9296
|
-
|
|
9550
|
+
// #2676: an ordinary message typed into the busy box is proven the same way.
|
|
9551
|
+
const steerComposerProof = handingOffToNativeQueue && !!this.backend?.readSteerComposer && tmuxAtWrite !== null;
|
|
9297
9552
|
let humanComposerOwned = false;
|
|
9298
9553
|
let steerEnterDeadline = Infinity;
|
|
9299
9554
|
if (steerComposerProof) {
|
|
@@ -9394,8 +9649,8 @@ export class Daemon extends EventEmitter {
|
|
|
9394
9649
|
if (humanComposerOwned)
|
|
9395
9650
|
signature.humanPositive?.(after === "steer" ? "composer-empty-steer"
|
|
9396
9651
|
: after === "queue" ? "composer-empty-queue" : "composer-empty-idle");
|
|
9397
|
-
verdict.phase = "steer-accepted";
|
|
9398
|
-
verdict.proof =
|
|
9652
|
+
verdict.phase = steer ? "steer-accepted" : "composer-accepted";
|
|
9653
|
+
verdict.proof = `${steer ? "steer" : "handoff"}-composer-${after}`;
|
|
9399
9654
|
if (status)
|
|
9400
9655
|
this.emit("message_confirmed", status); // ✅ taken from the box
|
|
9401
9656
|
return true;
|
|
@@ -10086,6 +10341,7 @@ export class Daemon extends EventEmitter {
|
|
|
10086
10341
|
collapsedPastes: input?.collapsedPastes ?? 0,
|
|
10087
10342
|
inputReadable: input != null,
|
|
10088
10343
|
steerComposer: this.backend?.readSteerComposer?.(pane) ?? null,
|
|
10344
|
+
busyInputReachesTurn: this.busyInputReachesTurn(pane),
|
|
10089
10345
|
};
|
|
10090
10346
|
}
|
|
10091
10347
|
/**
|
|
@@ -10581,6 +10837,12 @@ export class Daemon extends EventEmitter {
|
|
|
10581
10837
|
reply(failed ? null : answer, failed);
|
|
10582
10838
|
return;
|
|
10583
10839
|
}
|
|
10840
|
+
if (tool === "office_pack_check" || tool === "office_pack_upload") {
|
|
10841
|
+
// #2692: the folder (or the manifest's files) is read here, on the agent's side; the fleet gets bytes in parts.
|
|
10842
|
+
const answer = await sendPackSources(args, { workspace: this.config.working_directory, configuredWorkspace: this.config.working_directory, stateDir: getAgendHome(), instance: this.name }, async (payload) => { const r = await ask(payload); return r.error ? { error: "fleet", message: r.error } : (r.result && typeof r.result === "object" ? r.result : {}); });
|
|
10843
|
+
reply(answer, undefined);
|
|
10844
|
+
return;
|
|
10845
|
+
}
|
|
10584
10846
|
if (tool === "upload_web_effect") {
|
|
10585
10847
|
const owner = { fence: this.launchFenceEpoch, spawn: this.spawnGeneration, boot: this.bootId, ipc: this.ipcServer };
|
|
10586
10848
|
const current = () => !!owner.ipc && owner.ipc === this.ipcServer && owner.fence === this.launchFenceEpoch && owner.spawn === this.spawnGeneration && owner.boot === this.bootId && !this.deliveryWritesStopping && !this.startupAborted;
|
|
@@ -11029,10 +11291,6 @@ export class Daemon extends EventEmitter {
|
|
|
11029
11291
|
const snapshot = this.buildSnapshotPrompt();
|
|
11030
11292
|
if (!snapshot || !this.tmux)
|
|
11031
11293
|
return;
|
|
11032
|
-
if (this.pendingInstructionsUpdate) {
|
|
11033
|
-
writeFileSync(join(this.instanceDir, "prev-instructions"), this.pendingInstructionsUpdate);
|
|
11034
|
-
this.pendingInstructionsUpdate = undefined;
|
|
11035
|
-
}
|
|
11036
11294
|
// Small delay to let the CLI fully render its ready prompt
|
|
11037
11295
|
await new Promise(r => setTimeout(r, 1_000));
|
|
11038
11296
|
admission?.();
|
|
@@ -11351,6 +11609,23 @@ export class Daemon extends EventEmitter {
|
|
|
11351
11609
|
paneText = await this.tmux?.capturePaneWithHistory(50);
|
|
11352
11610
|
}
|
|
11353
11611
|
catch { /* pane may be gone */ }
|
|
11612
|
+
if (paneText && this.backend.resumeBlockedPattern?.().test(paneText)) {
|
|
11613
|
+
// #2598: a duplicate id proves the conversation exists. The generic retry escape hatch must not
|
|
11614
|
+
// abandon that pin or turn a store-selection error into a fresh conversation, even after many retries.
|
|
11615
|
+
await this.killProcessTree("SIGTERM", "spawn: retained session is blocked");
|
|
11616
|
+
this.startupAdmission?.();
|
|
11617
|
+
await this.tmux.killWindow();
|
|
11618
|
+
this.startupAdmission?.();
|
|
11619
|
+
throw new ResumeBlockedError(t("inst.kiro_resume_ambiguous"), t("inst.kiro_resume_ambiguous_remedy"));
|
|
11620
|
+
}
|
|
11621
|
+
const retained = this.backend.retainedResumeBlock?.();
|
|
11622
|
+
if (retained) {
|
|
11623
|
+
await this.killProcessTree("SIGTERM", "spawn: carried session must be preserved");
|
|
11624
|
+
this.startupAdmission?.();
|
|
11625
|
+
await this.tmux.killWindow();
|
|
11626
|
+
this.startupAdmission?.();
|
|
11627
|
+
throw new ResumeBlockedError(retained.reason, retained.remedy);
|
|
11628
|
+
}
|
|
11354
11629
|
const proven = this.paneSaysNoConversation(paneText);
|
|
11355
11630
|
if (!proven) {
|
|
11356
11631
|
const failures = this.bumpUnprovenResumeFailures();
|
|
@@ -11543,6 +11818,188 @@ export class Daemon extends EventEmitter {
|
|
|
11543
11818
|
this.logger.warn({ err }, "The agent switch after resume failed — the shared entries stay; the next launch tries again");
|
|
11544
11819
|
}
|
|
11545
11820
|
}
|
|
11821
|
+
/**
|
|
11822
|
+
* M-2025: after a tui/V3 launch is ready, type `/session-id` and `/title` once
|
|
11823
|
+
* each, under the pane-write lock, with the same fences as {@link ensureBackendAgent}.
|
|
11824
|
+
* Does not touch the delivery epoch. A mismatch with the #2617 pin is surfaced;
|
|
11825
|
+
* the pin is never replaced by a file-store guess. Legacy backends return null
|
|
11826
|
+
* from sessionProof().
|
|
11827
|
+
*
|
|
11828
|
+
* #2683: these are AgEnD's own housekeeping, so unlike a delivery (which may join
|
|
11829
|
+
* a person's draft) each one goes only into a composer the final capture shows
|
|
11830
|
+
* positively EMPTY; Enter only once a capture shows exactly the command in the box;
|
|
11831
|
+
* and it counts as run only once the box is empty again — a command left in the
|
|
11832
|
+
* box stops here (it is taken back when the box shows exactly it), and nothing
|
|
11833
|
+
* else is typed after it. The id is taken only from kiro's `/session-id` alert
|
|
11834
|
+
* (readSessionIdAlert) on a capture after the command ran, and never when that
|
|
11835
|
+
* alert was already on the final capture before it was typed: an announcement
|
|
11836
|
+
* that was there before is not an answer to this request.
|
|
11837
|
+
*/
|
|
11838
|
+
async ensureKiroSessionProof() {
|
|
11839
|
+
let proof = null;
|
|
11840
|
+
try {
|
|
11841
|
+
proof = this.backend?.sessionProof?.() ?? null;
|
|
11842
|
+
}
|
|
11843
|
+
catch {
|
|
11844
|
+
proof = null;
|
|
11845
|
+
}
|
|
11846
|
+
const tmux = this.tmux;
|
|
11847
|
+
if (!proof || !tmux || !this.backend)
|
|
11848
|
+
return;
|
|
11849
|
+
const windowId = tmux.getWindowId();
|
|
11850
|
+
const spawn = this.spawnGeneration, fence = this.launchFenceEpoch, phase = this.pauseWakeState;
|
|
11851
|
+
const fenced = () => !this.startupAborted && !this.deliveryWritesStopping && !this.fatalStartupBlocked
|
|
11852
|
+
&& (phase === "active" || phase === "waking") && this.pauseWakeState === phase
|
|
11853
|
+
&& spawn === this.spawnGeneration && fence === this.launchFenceEpoch && this.tmux === tmux;
|
|
11854
|
+
const deadline = performance.now() + Daemon.AGENT_SWITCH_BUDGET_MS;
|
|
11855
|
+
const live = () => { this.startupAdmission?.(); return fenced() && performance.now() < deadline; };
|
|
11856
|
+
if (!live())
|
|
11857
|
+
return;
|
|
11858
|
+
// A proof belongs to the launch that read it: an earlier launch's never stands for this one.
|
|
11859
|
+
try {
|
|
11860
|
+
clearKiroSessionProof(this.instanceDir);
|
|
11861
|
+
}
|
|
11862
|
+
catch { /* recorded again below only on a fresh match */ }
|
|
11863
|
+
const TIMEOUT = Symbol("deadline");
|
|
11864
|
+
const within = (work) => {
|
|
11865
|
+
const left = deadline - performance.now();
|
|
11866
|
+
if (left <= 0) {
|
|
11867
|
+
work.catch(() => { });
|
|
11868
|
+
return Promise.resolve(TIMEOUT);
|
|
11869
|
+
}
|
|
11870
|
+
let timer;
|
|
11871
|
+
return Promise.race([work, new Promise(r => { timer = setTimeout(() => r(TIMEOUT), left); })])
|
|
11872
|
+
.finally(() => clearTimeout(timer));
|
|
11873
|
+
};
|
|
11874
|
+
const capture = async () => {
|
|
11875
|
+
const pane = await within(tmux.capturePane().catch(() => null));
|
|
11876
|
+
return pane === TIMEOUT ? null : pane;
|
|
11877
|
+
};
|
|
11878
|
+
const pause = (ms) => within(new Promise(r => setTimeout(r, ms)));
|
|
11879
|
+
const op = Daemon.AGENT_SWITCH_TMUX_OP_MS;
|
|
11880
|
+
const box = async () => {
|
|
11881
|
+
const pane = await capture();
|
|
11882
|
+
return pane === null ? null : proof.readComposer(pane);
|
|
11883
|
+
};
|
|
11884
|
+
/**
|
|
11885
|
+
* Our command, and only it, is in the box: take it back — also past the deadline, on its own bounded capture.
|
|
11886
|
+
* Anything else there is left as it is.
|
|
11887
|
+
*/
|
|
11888
|
+
const takeBack = async (command) => {
|
|
11889
|
+
let timer;
|
|
11890
|
+
const pane = await Promise.race([tmux.capturePane().catch(() => null), new Promise(r => { timer = setTimeout(() => r(null), op); })])
|
|
11891
|
+
.finally(() => clearTimeout(timer));
|
|
11892
|
+
if (pane !== null && fenced() && proof.readComposer(pane) === command)
|
|
11893
|
+
await tmux.deleteBackward(command.length, op);
|
|
11894
|
+
};
|
|
11895
|
+
/**
|
|
11896
|
+
* "ran": typed, Enter, and the box is empty again; `after` then read the pane with the capture from before the
|
|
11897
|
+
* paste, still under the lock. "stuck": typed but not run — nothing more may be typed. null: nothing typed.
|
|
11898
|
+
*/
|
|
11899
|
+
const typeOnce = async (command, after) => {
|
|
11900
|
+
const wrote = await within(this.paneWriteLock.run(async () => {
|
|
11901
|
+
if (!live())
|
|
11902
|
+
return null;
|
|
11903
|
+
const readiness = await within(this.paneReadinessForDelivery(windowId));
|
|
11904
|
+
if (readiness !== "ready" || !live())
|
|
11905
|
+
return null;
|
|
11906
|
+
const before = await capture();
|
|
11907
|
+
if (before === null || !live())
|
|
11908
|
+
return null;
|
|
11909
|
+
if (this.inputTransientInPane(before) || !this.paneAuthoritativelyIdle(before))
|
|
11910
|
+
return null;
|
|
11911
|
+
if (proof.readComposer(before) !== "")
|
|
11912
|
+
return null; // a draft, or a box that cannot be read
|
|
11913
|
+
if (deadline - performance.now() < Daemon.AGENT_SWITCH_WRITE_RESERVE_MS)
|
|
11914
|
+
return null;
|
|
11915
|
+
if (!await tmux.pasteBuffer(command, { guard: live, timeoutMs: op }))
|
|
11916
|
+
return "stuck";
|
|
11917
|
+
let shown = null;
|
|
11918
|
+
for (let i = 0; i < 4 && live() && shown !== command; i++) {
|
|
11919
|
+
await pause(i === 0 ? Daemon.AGENT_SWITCH_PASTE_SETTLE_MS : Daemon.AGENT_SWITCH_POLL_MS);
|
|
11920
|
+
shown = await box();
|
|
11921
|
+
}
|
|
11922
|
+
if (shown !== command || !live()) {
|
|
11923
|
+
await takeBack(command);
|
|
11924
|
+
return "stuck";
|
|
11925
|
+
}
|
|
11926
|
+
if (!await tmux.sendSpecialKey("Enter", op)) {
|
|
11927
|
+
await takeBack(command);
|
|
11928
|
+
return "stuck";
|
|
11929
|
+
}
|
|
11930
|
+
let left = command;
|
|
11931
|
+
for (let i = 0; i < 6 && left !== ""; i++) {
|
|
11932
|
+
await pause(Daemon.AGENT_SWITCH_POLL_MS);
|
|
11933
|
+
if (!fenced())
|
|
11934
|
+
return "stuck";
|
|
11935
|
+
left = await box();
|
|
11936
|
+
}
|
|
11937
|
+
if (left !== "") {
|
|
11938
|
+
await takeBack(command);
|
|
11939
|
+
return "stuck";
|
|
11940
|
+
}
|
|
11941
|
+
return { ran: true, value: await after(before) };
|
|
11942
|
+
}));
|
|
11943
|
+
return wrote === TIMEOUT ? "stuck" : wrote;
|
|
11944
|
+
};
|
|
11945
|
+
try {
|
|
11946
|
+
const sid = await typeOnce(proof.sessionIdCommand, async (before) => {
|
|
11947
|
+
if (proof.readSessionIdAlert(before) !== null)
|
|
11948
|
+
return null; // already there: ours cannot be told apart
|
|
11949
|
+
for (let i = 0; i < 6 && live(); i++) {
|
|
11950
|
+
const pane = await capture();
|
|
11951
|
+
if (pane === null)
|
|
11952
|
+
break;
|
|
11953
|
+
const id = proof.readSessionIdAlert(pane);
|
|
11954
|
+
if (id !== null)
|
|
11955
|
+
return id;
|
|
11956
|
+
await pause(Daemon.AGENT_SWITCH_POLL_MS);
|
|
11957
|
+
}
|
|
11958
|
+
return null;
|
|
11959
|
+
});
|
|
11960
|
+
if (!live())
|
|
11961
|
+
return;
|
|
11962
|
+
if (sid === "stuck") {
|
|
11963
|
+
this.logger.warn("kiro /session-id did not run from an empty composer; /title is not typed after it");
|
|
11964
|
+
return;
|
|
11965
|
+
}
|
|
11966
|
+
// Unread/mismatch are about kiro's answer after we ran /session-id.
|
|
11967
|
+
// A composer that never read empty is not unread — we never asked.
|
|
11968
|
+
if (sid) {
|
|
11969
|
+
const verdict = proveKiroSessionId(proof.pinned, sid.value);
|
|
11970
|
+
if (verdict.kind === "mismatch") {
|
|
11971
|
+
const message = t("kiro.session_id_mismatch", verdict.pinned, verdict.observed);
|
|
11972
|
+
this.logger.warn({ pinned: verdict.pinned, observed: verdict.observed }, "kiro /session-id does not match the pinned conversation");
|
|
11973
|
+
this.emit("backend_launch_warning", { name: this.name, message });
|
|
11974
|
+
}
|
|
11975
|
+
else if (verdict.kind === "unread") {
|
|
11976
|
+
const message = t("kiro.session_id_unread");
|
|
11977
|
+
this.logger.warn("kiro /session-id did not answer on the pane; not guessing from files");
|
|
11978
|
+
this.emit("backend_launch_warning", { name: this.name, message });
|
|
11979
|
+
}
|
|
11980
|
+
else if (verdict.kind === "match" || verdict.kind === "fresh") {
|
|
11981
|
+
this.logger.info({ id: verdict.id, kind: verdict.kind }, "kiro /session-id on the pane");
|
|
11982
|
+
try {
|
|
11983
|
+
recordKiroSessionProof(this.instanceDir, { sessionId: verdict.id, kind: verdict.kind });
|
|
11984
|
+
}
|
|
11985
|
+
catch (err) {
|
|
11986
|
+
this.logger.warn({ err, id: verdict.id }, "kiro /session-id was proven on the pane but could not be recorded");
|
|
11987
|
+
}
|
|
11988
|
+
}
|
|
11989
|
+
}
|
|
11990
|
+
if (!live() || !proof.titleCommand)
|
|
11991
|
+
return;
|
|
11992
|
+
const titled = await typeOnce(proof.titleCommand, async () => true);
|
|
11993
|
+
if (titled && titled !== "stuck")
|
|
11994
|
+
this.logger.info({ title: proof.titleCommand }, "kiro /title sent for this instance");
|
|
11995
|
+
else if (titled === "stuck")
|
|
11996
|
+
this.logger.warn({ title: proof.titleCommand }, "kiro /title did not run from an empty composer");
|
|
11997
|
+
}
|
|
11998
|
+
catch (err) {
|
|
11999
|
+
this.startupAdmission?.();
|
|
12000
|
+
this.logger.warn({ err }, "kiro /session-id or /title after launch failed — the pin was not guessed from files");
|
|
12001
|
+
}
|
|
12002
|
+
}
|
|
11546
12003
|
/**
|
|
11547
12004
|
* Startup budget for this launch: the backend's override (resume-aware), never
|
|
11548
12005
|
* below a user-configured startup_timeout_ms; undefined = trySpawn's default.
|
|
@@ -11665,10 +12122,13 @@ export class Daemon extends EventEmitter {
|
|
|
11665
12122
|
}
|
|
11666
12123
|
/** Kill the entire process tree of the current tmux pane (CLI + MCP server). */
|
|
11667
12124
|
async killProcessTree(signal = "SIGTERM", reason = "unspecified") {
|
|
11668
|
-
|
|
12125
|
+
const windowId = this.tmux?.getWindowId();
|
|
12126
|
+
// Before createWindow succeeds there is no pane owned by this daemon.
|
|
12127
|
+
// tmux interprets an empty selector as the session's active window.
|
|
12128
|
+
if (!windowId?.trim())
|
|
11669
12129
|
return;
|
|
11670
12130
|
try {
|
|
11671
|
-
const pid = await TmuxManager.getPanePid(this.tmuxSessionName,
|
|
12131
|
+
const pid = await TmuxManager.getPanePid(this.tmuxSessionName, windowId);
|
|
11672
12132
|
if (pid) {
|
|
11673
12133
|
this.noteStopRequest(signal, reason);
|
|
11674
12134
|
process.kill(-pid, signal);
|
|
@@ -11705,6 +12165,9 @@ export class Daemon extends EventEmitter {
|
|
|
11705
12165
|
if (ready)
|
|
11706
12166
|
await this.ensureBackendAgent();
|
|
11707
12167
|
this.startupAdmission?.();
|
|
12168
|
+
if (ready)
|
|
12169
|
+
await this.ensureKiroSessionProof();
|
|
12170
|
+
this.startupAdmission?.();
|
|
11708
12171
|
return ready;
|
|
11709
12172
|
}
|
|
11710
12173
|
/**
|
|
@@ -11837,113 +12300,40 @@ export class Daemon extends EventEmitter {
|
|
|
11837
12300
|
return this.museRelayFallbackInFlight;
|
|
11838
12301
|
}
|
|
11839
12302
|
/**
|
|
11840
|
-
*
|
|
11841
|
-
*
|
|
11842
|
-
*
|
|
11843
|
-
* testable: warmupNoticeAction's own tests pass even if this call site stops
|
|
11844
|
-
* consulting it, which is exactly the bug this path had — the decision was
|
|
11845
|
-
* made inline and never asked whether the backend needed telling at all.
|
|
11846
|
-
*
|
|
11847
|
-
* Never throws: a missed reload notice must not fail a spawn.
|
|
11848
|
-
*/
|
|
11849
|
-
async runWarmupInstructionNotice() {
|
|
11850
|
-
try {
|
|
11851
|
-
const action = warmupNoticeAction({
|
|
11852
|
-
warmupNeeded: this.warmupNeeded,
|
|
11853
|
-
backend: this.backend ?? undefined,
|
|
11854
|
-
pasteQueueDepth: this.pasteQueueDepth,
|
|
11855
|
-
});
|
|
11856
|
-
if (action === "skip") {
|
|
11857
|
-
this.logger.debug(this.warmupNeeded
|
|
11858
|
-
? "Warmup skipped — this backend reloads instructions on resume"
|
|
11859
|
-
: "Warmup skipped — instructions unchanged");
|
|
11860
|
-
// When the backend learns of the change on its own, record what it now
|
|
11861
|
-
// holds: leaving prev-instructions behind would keep every later
|
|
11862
|
-
// restart comparing against a value the agent has long since replaced.
|
|
11863
|
-
if (this.warmupNeeded) {
|
|
11864
|
-
try {
|
|
11865
|
-
writeFileSync(join(this.instanceDir, "prev-instructions"), this.lastBuiltInstructions);
|
|
11866
|
-
}
|
|
11867
|
-
catch { /* best effort */ }
|
|
11868
|
-
}
|
|
11869
|
-
return;
|
|
11870
|
-
}
|
|
11871
|
-
if (action === "defer") {
|
|
11872
|
-
this.logger.debug("Warmup deferred — no pending inbound messages");
|
|
11873
|
-
// Convert to pendingInstructionsNotice so it fires on next real message.
|
|
11874
|
-
this.pendingInstructionsNotice = true;
|
|
11875
|
-
try {
|
|
11876
|
-
writeFileSync(join(this.instanceDir, "prev-instructions"), this.lastBuiltInstructions);
|
|
11877
|
-
}
|
|
11878
|
-
catch { }
|
|
11879
|
-
return;
|
|
11880
|
-
}
|
|
11881
|
-
const wid = existsSync(join(this.instanceDir, "window-id"))
|
|
11882
|
-
? readFileSync(join(this.instanceDir, "window-id"), "utf-8").trim() : "";
|
|
11883
|
-
if (wid && this.controlClient) {
|
|
11884
|
-
await this.controlClient.waitForIdle(wid, 120_000);
|
|
11885
|
-
}
|
|
11886
|
-
else {
|
|
11887
|
-
await new Promise(r => setTimeout(r, 5000));
|
|
11888
|
-
}
|
|
11889
|
-
// This path only runs when pasteQueueDepth > 0 — i.e. exactly when a real
|
|
11890
|
-
// delivery is already in flight or queued. Without the lock the notice and
|
|
11891
|
-
// that delivery race into the same pane.
|
|
11892
|
-
const told = await this.paneWriteLock.run(() => this.submitSystemPaste(buildInstructionReloadNotice(this.backend?.binaryName ?? "unknown", this.name, this.instanceDir, this.backend?.instructionsSource?.()), "instruction-reload-notice"));
|
|
11893
|
-
if (!told) {
|
|
11894
|
-
// Recording the new instructions here would mark the agent as told about
|
|
11895
|
-
// a notice it never received, and every later restart would then skip
|
|
11896
|
-
// the reload. Leave the snapshot behind and hand it to the deferred
|
|
11897
|
-
// notice, which fires on the next real message.
|
|
11898
|
-
this.logger.warn("Instruction reload notice not confirmed — deferring it to the next message");
|
|
11899
|
-
this.pendingInstructionsNotice = true;
|
|
11900
|
-
return;
|
|
11901
|
-
}
|
|
11902
|
-
// Record the value the agent has now been told about so the next
|
|
11903
|
-
// unchanged restart skips the reload.
|
|
11904
|
-
try {
|
|
11905
|
-
writeFileSync(join(this.instanceDir, "prev-instructions"), this.lastBuiltInstructions);
|
|
11906
|
-
}
|
|
11907
|
-
catch { /* best effort */ }
|
|
11908
|
-
this.logger.debug("Warmup sent after idle");
|
|
11909
|
-
}
|
|
11910
|
-
catch { /* non-fatal */ }
|
|
11911
|
-
}
|
|
11912
|
-
/**
|
|
11913
|
-
* #1633 (#1639 review): the pending notice as the record stands NOW, just before the paste — it was decided at launch,
|
|
11914
|
-
* and the record may have changed while the delivery waited. Unreadable → nothing is sent (and nothing pending);
|
|
11915
|
-
* entries recorded meanwhile are dropped from it; none left → nothing.
|
|
12303
|
+
* #1633 (#1639 review): the pending notice as things stand NOW, just before the paste — it was decided at launch, and
|
|
12304
|
+
* the record, a switch or the instance's tools may have changed while the delivery waited. Planned again from the
|
|
12305
|
+
* record (#2800: the running release only, tools it has now); nothing left to tell → recorded, nothing pending.
|
|
11916
12306
|
*/
|
|
11917
12307
|
currentWhatsNew(pending) {
|
|
11918
|
-
const
|
|
11919
|
-
|
|
12308
|
+
const plan = planWhatsNew({ record: readWhatsNewRecord(this.instanceDir), fresh: false, version: pending.version,
|
|
12309
|
+
roles: this.whatsNewRoles(), tools: this.whatsNewTools(), canvasWorker: readCanvasWorkerEnabled(join(this.instanceDir, "..", "..")) });
|
|
12310
|
+
if (!plan || "silent" in plan) {
|
|
11920
12311
|
this.pendingWhatsNew = null;
|
|
12312
|
+
if (plan)
|
|
12313
|
+
this.recordWhatsNewSent({ ids: plan.silent, version: plan.version });
|
|
11921
12314
|
return null;
|
|
11922
12315
|
}
|
|
11923
|
-
|
|
11924
|
-
|
|
11925
|
-
|
|
12316
|
+
this.pendingWhatsNew = plan;
|
|
12317
|
+
return plan;
|
|
12318
|
+
}
|
|
12319
|
+
recordWhatsNewSent(told) {
|
|
12320
|
+
if (this.pendingWhatsNew && told.ids.every(id => this.pendingWhatsNew.ids.includes(id)))
|
|
11926
12321
|
this.pendingWhatsNew = null;
|
|
11927
|
-
|
|
12322
|
+
try {
|
|
12323
|
+
recordWhatsNew(this.instanceDir, told.ids, told.version);
|
|
11928
12324
|
}
|
|
11929
|
-
|
|
11930
|
-
|
|
11931
|
-
// #2163: rebuilt as planned — only the entries this instance's roles are told; the rest are recorded with them.
|
|
11932
|
-
const roles = this.whatsNewRoles();
|
|
11933
|
-
const shown = agentWhatsNew(readCanvasWorkerEnabled(join(this.instanceDir, "..", ".."))).filter(e => ids.includes(e.id) && (!e.audience || e.audience.some(r => roles.includes(r))));
|
|
11934
|
-
if (!shown.length) {
|
|
11935
|
-
this.pendingWhatsNew = null;
|
|
11936
|
-
try {
|
|
11937
|
-
recordWhatsNew(this.instanceDir, ids, pending.version);
|
|
11938
|
-
}
|
|
11939
|
-
catch (err) {
|
|
11940
|
-
this.logger.warn({ err }, "what's-new record not written — the notice may repeat once");
|
|
11941
|
-
}
|
|
11942
|
-
return null;
|
|
12325
|
+
catch (err) {
|
|
12326
|
+
this.logger.warn({ err }, "what's-new record not written — the notice may repeat once");
|
|
11943
12327
|
}
|
|
11944
|
-
|
|
11945
|
-
|
|
11946
|
-
|
|
12328
|
+
}
|
|
12329
|
+
/**
|
|
12330
|
+
* #2800: the tools this instance's agend MCP server lists now — the same filters (tool set, dashboard switches, the
|
|
12331
|
+
* Canvas Worker switch) — so a notice names only changes to tools it has.
|
|
12332
|
+
*/
|
|
12333
|
+
whatsNewTools() {
|
|
12334
|
+
const allowed = toolsFor(resolveToolSet(this.config, this.name));
|
|
12335
|
+
const listed = withoutFeatures(TOOLS.filter(t => allowed.has(t.name)), dashboardFeaturesOff(dashboardAccessOf(this.config)));
|
|
12336
|
+
return new Set(toolsWithCanvasWorker(readCanvasWorkerEnabled(join(this.instanceDir, "..", "..")), listed).map(t => t.name));
|
|
11947
12337
|
}
|
|
11948
12338
|
/**
|
|
11949
12339
|
* #2163: who this instance is to the what's-new notices — a ClassicBot room's agent, General, or a fleet worker (with
|
|
@@ -11960,7 +12350,8 @@ export class Daemon extends EventEmitter {
|
|
|
11960
12350
|
/** #1633: what this instance has not been told about its AgEnD version yet (see agent-whats-new.ts). */
|
|
11961
12351
|
prepareWhatsNew(fresh, version = agendVersion()) {
|
|
11962
12352
|
try {
|
|
11963
|
-
const plan = planWhatsNew({ record: readWhatsNewRecord(this.instanceDir), fresh, version, roles: this.whatsNewRoles(),
|
|
12353
|
+
const plan = planWhatsNew({ record: readWhatsNewRecord(this.instanceDir), fresh, version, roles: this.whatsNewRoles(),
|
|
12354
|
+
tools: this.whatsNewTools(), canvasWorker: readCanvasWorkerEnabled(join(this.instanceDir, "..", "..")) });
|
|
11964
12355
|
if (!plan) {
|
|
11965
12356
|
this.pendingWhatsNew = null;
|
|
11966
12357
|
return;
|
|
@@ -11971,8 +12362,7 @@ export class Daemon extends EventEmitter {
|
|
|
11971
12362
|
return;
|
|
11972
12363
|
}
|
|
11973
12364
|
this.pendingWhatsNew = plan;
|
|
11974
|
-
this.
|
|
11975
|
-
this.logger.info({ entries: plan.ids }, "what's-new notice pending for the next delivery");
|
|
12365
|
+
this.logger.debug({ entries: plan.ids }, "what's-new notice pending for the next delivery");
|
|
11976
12366
|
}
|
|
11977
12367
|
catch (err) {
|
|
11978
12368
|
this.logger.debug({ err }, "what's-new not prepared");
|
|
@@ -11983,41 +12373,15 @@ export class Daemon extends EventEmitter {
|
|
|
11983
12373
|
this.startupAdmission?.();
|
|
11984
12374
|
const launchFence = this.launchFenceEpoch;
|
|
11985
12375
|
const backendConfig = this.buildBackendConfig();
|
|
11986
|
-
// Compare freshly-built instructions against the last value the agent was
|
|
11987
|
-
// told about. Computed for ALL backends (not gated by
|
|
11988
|
-
// instructionsReloadedOnResume) so the warmup steering-reload can be
|
|
11989
|
-
// skipped when nothing changed.
|
|
11990
|
-
this.lastBuiltInstructions = backendConfig.instructions ?? "";
|
|
11991
12376
|
// #1633: decided before prev-instructions is first written — its absence is what says "never launched before".
|
|
11992
12377
|
this.prepareWhatsNew(!existsSync(join(this.instanceDir, "prev-instructions")));
|
|
11993
|
-
|
|
11994
|
-
|
|
11995
|
-
|
|
12378
|
+
// Record what this launch writes. #2787: a change is never announced in the pane; the CLI takes the new
|
|
12379
|
+
// instructions whenever it next reads its own file.
|
|
12380
|
+
if (backendConfig.instructions) {
|
|
11996
12381
|
try {
|
|
11997
|
-
|
|
11998
|
-
}
|
|
11999
|
-
catch { }
|
|
12000
|
-
if (!prev && backendConfig.instructions) {
|
|
12001
|
-
// First time (no prev-instructions file): write current hash, skip warmup.
|
|
12002
|
-
try {
|
|
12003
|
-
writeFileSync(prevFile, backendConfig.instructions);
|
|
12004
|
-
}
|
|
12005
|
-
catch { }
|
|
12006
|
-
this.warmupNeeded = false;
|
|
12007
|
-
}
|
|
12008
|
-
else {
|
|
12009
|
-
this.warmupNeeded = !!backendConfig.instructions && prev !== backendConfig.instructions;
|
|
12010
|
-
}
|
|
12011
|
-
// For backends that don't re-read instructions on resume (kiro/codex),
|
|
12012
|
-
// also notify the agent on next message instead of forcing a new
|
|
12013
|
-
// session. Resume is preserved so context isn't lost.
|
|
12014
|
-
if (!backendConfig.skipResume && backendNeedsPaneReloadNotice(this.backend) && this.warmupNeeded) {
|
|
12015
|
-
if (prev) {
|
|
12016
|
-
this.logger.info("Instructions changed — will notify agent on next message");
|
|
12017
|
-
this.pendingInstructionsNotice = true;
|
|
12018
|
-
}
|
|
12019
|
-
this.pendingInstructionsUpdate = backendConfig.instructions;
|
|
12382
|
+
writeFileSync(join(this.instanceDir, "prev-instructions"), backendConfig.instructions);
|
|
12020
12383
|
}
|
|
12384
|
+
catch { /* best effort */ }
|
|
12021
12385
|
}
|
|
12022
12386
|
// A backend that has to ask its CLI something before the launch command exists (OpenCode: does
|
|
12023
12387
|
// this binary take --auto?) does it here, off the event loop, bounded, and never fails the launch.
|