@songsid/agend 2.3.2-beta.9 → 2.3.2
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.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.js +3 -0
- 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.js +23 -1
- 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 +344 -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 -1
- package/dist/backend/kiro.js +553 -29
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +102 -0
- 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 +50 -4
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.d.ts +7 -1
- package/dist/config-validator.js +17 -8
- package/dist/config-validator.js.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +66 -0
- package/dist/daemon.js +421 -19
- 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/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 +90 -3
- package/dist/fleet-manager.js +569 -41
- 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/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 +1 -1
- 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 +7 -0
- package/dist/instance-lifecycle.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 +198 -0
- package/dist/locale.js.map +1 -1
- package/dist/log-service.js +2 -2
- 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 +20 -1
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +47 -2
- package/dist/outbound-schemas.js +41 -2
- 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 +28 -6
- package/dist/scheduler/task-board.js +66 -11
- package/dist/scheduler/task-board.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 -0
- package/dist/settings-api.js +18 -12
- 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 -4
- 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.js +7 -2
- package/dist/task-board-waker.js.map +1 -1
- package/dist/tool-permissions.js +18 -0
- package/dist/tool-permissions.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 +10 -0
- 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 +4 -4
- package/dist/ui/panel-needs.js +1 -1
- package/dist/ui/panel-settings.js +16 -8
- 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 -0
- package/dist/ui/settings-strings.js +16 -4
- 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/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-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 -1
- package/dist/web-settings-patch.js +4 -1
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/workflow-templates/default.md +1 -1
- package/package.json +2 -2
- package/templates/windows-shortcuts.ps1 +18 -17
package/dist/daemon.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { readActiveKiroModel } from "./backend/kiro.js";
|
|
1
2
|
import { dashboardAccessOf } from "./dashboard-permissions.js";
|
|
3
|
+
import { sendPackSources } from "./web-office-pack-read.js";
|
|
2
4
|
import { agentWhatsNew, agendVersion, planWhatsNew, readWhatsNewRecord, recordWhatsNew, whatsNewNotice } 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";
|
|
@@ -36,13 +39,14 @@ import { ContextGuardian } from "./context-guardian.js";
|
|
|
36
39
|
import { IpcServer } from "./channel/ipc-bridge.js";
|
|
37
40
|
import { daemonBudgetMs } from "./channel/ipc-timeouts.js";
|
|
38
41
|
import { MessageBus } from "./channel/message-bus.js";
|
|
39
|
-
import { shellQuote, UnsupportedCliError } from "./backend/types.js";
|
|
42
|
+
import { shellQuote, UnsupportedCliError, ResumeBlockedError } from "./backend/types.js";
|
|
40
43
|
import { getTmuxSession } from "./config.js";
|
|
41
44
|
import { routeToolCall } from "./channel/tool-router.js";
|
|
42
45
|
import { parseReplyButtons, replyButtonsFallbackText } from "./reply-buttons.js";
|
|
43
46
|
import { HangDetector } from "./hang-detector.js";
|
|
44
47
|
import { writeSecretFile } from "./secret-file.js";
|
|
45
48
|
import { PaneWriteLock } from "./pane-write-lock.js";
|
|
49
|
+
import { answerRemoteCard } from "./remote-card-answer.js";
|
|
46
50
|
import { buildFleetInstructions } from "./instructions.js";
|
|
47
51
|
import { formatHumanInboundMessage, prepareDeliveryWire, steerInboundEnvelope, BTW_INBOUND_PREFIX, humanBodyHash, humanOwnerObjectId } from "./human-delivery-receipts.js";
|
|
48
52
|
import { formatCrossInstanceInboundMessage, renderCrossInstanceHandoffMetadata, } from "./cross-instance-envelope.js";
|
|
@@ -89,6 +93,10 @@ const WEB_THEME_IPC = Object.freeze({
|
|
|
89
93
|
delete_web_effect: "fleet_delete_web_effect",
|
|
90
94
|
// #2578: its own character in the Office view — the args as they are (the fleet checks them).
|
|
91
95
|
office_view: "fleet_office_view", office_set: "fleet_office_set",
|
|
96
|
+
// #2692: the skin packs — check and upload read the pack here (web-office-pack-read.ts) and send its bytes in parts.
|
|
97
|
+
office_pack_check: "fleet_office_pack_check", office_pack_upload: "fleet_office_pack_upload",
|
|
98
|
+
office_pack_delete: "fleet_office_pack_delete", office_pack_restore: "fleet_office_pack_restore",
|
|
99
|
+
office_pack_list: "fleet_office_pack_list", office_set_style: "fleet_office_set_style",
|
|
92
100
|
});
|
|
93
101
|
/** The largest picture any slot may take (a card slot's ceiling; the fleet checks the limit in force): read no more. */
|
|
94
102
|
const WEB_THEME_ASSET_MAX_BYTES = ASSET_UPLOAD_MAX_BYTES;
|
|
@@ -573,6 +581,8 @@ export const OWN_SWITCH_WAIT_MS = 6_000;
|
|
|
573
581
|
const OWN_SWITCH_SETTLE_MS = 3_000;
|
|
574
582
|
/** A blocking dialog still on screen this long after first being seen is reported for a human. */
|
|
575
583
|
const DIALOG_PARKED_NOTIFY_MS = 60_000;
|
|
584
|
+
/** #2675: a card a person can answer from the chat is posted once it has been on screen, unchanged, this long. */
|
|
585
|
+
const REMOTE_CARD_STABLE_MS = 1_500;
|
|
576
586
|
/** #1691: a prompt no table knows must sit unchanged this long (monotonic) before it holds deliveries and is reported. */
|
|
577
587
|
export const UNRECOGNISED_PROMPT_STABLE_MS = 60_000;
|
|
578
588
|
/** #1691: the rows under a prompt a backend without a composer reader shows its ready chrome in (status bar, hint). */
|
|
@@ -701,6 +711,18 @@ const FIRST_DELIVERY_WINDOW_MS = 5_000;
|
|
|
701
711
|
const NATIVE_QUEUE_PASTE_VERIFY_MS = 2_000;
|
|
702
712
|
/** #1405: how long a steer's paste gets to show in a mode-switched composer before its one Enter (captures included). */
|
|
703
713
|
const STEER_PASTE_VISIBLE_MS = 2_000;
|
|
714
|
+
/**
|
|
715
|
+
* #2676: the longest one-line paste kiro's TUI composer still shows as typed text; longer, it collapses to a
|
|
716
|
+
* `N chars ▸` chip (2.34.0 tui.js: more than 10 lines or 500 characters collapse).
|
|
717
|
+
*/
|
|
718
|
+
const KIRO_COLLAPSE_CHARS = 500;
|
|
719
|
+
/**
|
|
720
|
+
* #1405/#2676: whether a mode-switched composer in `mode` may take a busy hand-off: a steer only on "steer", an ordinary
|
|
721
|
+
* message on "steer" or "queue" (kiro takes either into its tray), and "idle" — the turn over — only where `allowIdle`.
|
|
722
|
+
*/
|
|
723
|
+
function composerModeAdmits(mode, allowIdle, steerOnly) {
|
|
724
|
+
return mode === "steer" || (!steerOnly && mode === "queue") || (allowIdle && mode === "idle");
|
|
725
|
+
}
|
|
704
726
|
/** `promise`'s value if it settles within `ms`, else undefined — the late answer is dropped, never awaited (#1405). */
|
|
705
727
|
async function withinMs(promise, ms) {
|
|
706
728
|
if (ms <= 0) {
|
|
@@ -1138,6 +1160,29 @@ export class Daemon extends EventEmitter {
|
|
|
1138
1160
|
logTranscriptPath() { return this.transcriptMonitor?.currentPath() ?? null; }
|
|
1139
1161
|
/** #1218 (kiro): the launch this daemon's backend last built, for the Log's reason only (kiro-log-gate.ts). */
|
|
1140
1162
|
logLaunch() { return this.backend?.logLaunch?.() ?? null; }
|
|
1163
|
+
async kiroMigrationIdle() {
|
|
1164
|
+
const tmux = this.tmux, owner = this.interactionOwner();
|
|
1165
|
+
if (!tmux || this.spawning || this.runtimeMonitorsFrozen || this.processStatus !== "running")
|
|
1166
|
+
return false;
|
|
1167
|
+
const pane = await tmux.capturePane(2000).catch(() => null);
|
|
1168
|
+
return pane !== null && tmux === this.tmux && sameInteractionOwner(owner, this.interactionOwner())
|
|
1169
|
+
&& this.processStatus === "running" && !this.spawning && !this.runtimeMonitorsFrozen && this.paneAuthoritativelyIdle(pane);
|
|
1170
|
+
}
|
|
1171
|
+
/** One read-only migration observation. Never types into a pane or selects a picker. */
|
|
1172
|
+
async kiroMigrationObservation() {
|
|
1173
|
+
const tmux = this.tmux, owner = this.interactionOwner(), backend = this.backend;
|
|
1174
|
+
if (!tmux || backend?.binaryName !== "kiro-cli" || this.processStatus !== "running" || this.spawning || this.runtimeMonitorsFrozen)
|
|
1175
|
+
return null;
|
|
1176
|
+
const pane = await tmux.capturePane(2000).catch(() => null);
|
|
1177
|
+
if (pane === null || tmux !== this.tmux || !sameInteractionOwner(owner, this.interactionOwner())
|
|
1178
|
+
|| this.processStatus !== "running" || this.spawning || this.runtimeMonitorsFrozen || !this.paneAuthoritativelyIdle(pane)
|
|
1179
|
+
|| backend.readSteerComposer?.(pane) !== "idle")
|
|
1180
|
+
return null;
|
|
1181
|
+
const launch = backend.transcriptLaunch?.(), path = this.logTranscriptPath();
|
|
1182
|
+
// Agent switching is separately disabled for saved-agent resumes; it is not an observation API.
|
|
1183
|
+
const model = readActiveKiroModel(pane);
|
|
1184
|
+
return launch?.engine === "v2" && launch.sessionId && model && path ? { sessionId: launch.sessionId, model, transcript: path } : null;
|
|
1185
|
+
}
|
|
1141
1186
|
guardian = null;
|
|
1142
1187
|
adapter = null;
|
|
1143
1188
|
pendingIpcRequests = new Map();
|
|
@@ -1257,6 +1302,12 @@ export class Daemon extends EventEmitter {
|
|
|
1257
1302
|
dialogParkedKey = null;
|
|
1258
1303
|
dialogParkedInteractionEpisode = null;
|
|
1259
1304
|
dialogParkedReported = false;
|
|
1305
|
+
/** #2675: the card on screen that a person can answer from the chat — what it asks, since when, and whether it was posted. */
|
|
1306
|
+
remoteCardShown = null;
|
|
1307
|
+
/** #2675: when (performance.now, taken before its capture) the newest read the card tracker used was started. */
|
|
1308
|
+
remoteCardReadAt = -Infinity;
|
|
1309
|
+
/** #2675: an answer from the chat is being keyed in (one at a time). */
|
|
1310
|
+
remoteCardAnswering = false;
|
|
1260
1311
|
/**
|
|
1261
1312
|
* #1691: the prompt at the bottom of the pane that no backend table knows — its block, since when (performance.now)
|
|
1262
1313
|
* it has been unchanged, and whether that has lasted UNRECOGNISED_PROMPT_STABLE_MS. Updated by the runtime scan.
|
|
@@ -2026,6 +2077,11 @@ export class Daemon extends EventEmitter {
|
|
|
2026
2077
|
this.finishDurableDelivery(delivery, "delivered", `steer-accepted; composer emptied (${verdict.proof.slice("steer-composer-".length)})`, verdict);
|
|
2027
2078
|
return;
|
|
2028
2079
|
}
|
|
2080
|
+
// #2676: an ordinary message typed into that busy composer, proven the same way: taken into the CLI's tray.
|
|
2081
|
+
if (verdict.proof?.startsWith("handoff-composer-")) {
|
|
2082
|
+
this.finishDurableDelivery(delivery, "delivered", `composer-handoff-accepted; composer emptied (${verdict.proof.slice("handoff-composer-".length)})`, verdict);
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2029
2085
|
// A backend whose input row cannot be read: all that vouched for this delivery is that the pane printed something
|
|
2030
2086
|
// after Enter, which a redraw that wiped the paste satisfies too (#758). Its transcript can say more.
|
|
2031
2087
|
if (verdict.proof === "output-edge") {
|
|
@@ -3432,6 +3488,11 @@ export class Daemon extends EventEmitter {
|
|
|
3432
3488
|
if (!(err instanceof UnsupportedCliError))
|
|
3433
3489
|
return false;
|
|
3434
3490
|
this.healthCheckPaused = true;
|
|
3491
|
+
if (err instanceof ResumeBlockedError) {
|
|
3492
|
+
this.logger.error({ reason: err.message }, "Not respawning: retained session needs store repair");
|
|
3493
|
+
this.emitSupervisionEnded(err.message, err.remedy, undefined, "resume_blocked");
|
|
3494
|
+
return true;
|
|
3495
|
+
}
|
|
3435
3496
|
this.logger.error({ reason: err.message }, "Not respawning: the installed CLI cannot run this instance as configured");
|
|
3436
3497
|
this.emitSupervisionEnded(err.message, "Install a CLI version that supports this instance's configuration, then start it again.", undefined, "cli_unsupported");
|
|
3437
3498
|
return true;
|
|
@@ -3743,6 +3804,7 @@ export class Daemon extends EventEmitter {
|
|
|
3743
3804
|
if (!answersBackedOff)
|
|
3744
3805
|
this.dialogAnswers = null;
|
|
3745
3806
|
this.trackDialogParked(blockingSeen);
|
|
3807
|
+
this.trackRemoteCard(blockingSeen, pane, captureMono);
|
|
3746
3808
|
if (blockingSeen)
|
|
3747
3809
|
return; // held dialog: skip error checks this cycle
|
|
3748
3810
|
this.evaluateErrorPatterns(pane, patterns, readyPattern, Date.now(), busyPattern);
|
|
@@ -6739,6 +6801,10 @@ export class Daemon extends EventEmitter {
|
|
|
6739
6801
|
this.config.office_access = update.office_access;
|
|
6740
6802
|
else if (update.office_access === null)
|
|
6741
6803
|
delete this.config.office_access;
|
|
6804
|
+
if (typeof update.office_pack_access === "boolean")
|
|
6805
|
+
this.config.office_pack_access = update.office_pack_access;
|
|
6806
|
+
else if (update.office_pack_access === null)
|
|
6807
|
+
delete this.config.office_pack_access;
|
|
6742
6808
|
if (update.dashboard && typeof update.dashboard === "object" && !dashboardAccessProblem(update.dashboard))
|
|
6743
6809
|
this.config.dashboard = update.dashboard;
|
|
6744
6810
|
else if (update.dashboard === null)
|
|
@@ -7346,6 +7412,7 @@ export class Daemon extends EventEmitter {
|
|
|
7346
7412
|
return false;
|
|
7347
7413
|
let windowId = this.getWindowId();
|
|
7348
7414
|
const supportsQueuedInput = this.backend?.supportsQueuedInput?.() === true;
|
|
7415
|
+
const composerQueue = !opts?.steer && this.composerTakesBusyInput() && this.composerCanShowPaste(formatted);
|
|
7349
7416
|
let handingOffToNativeQueue = false;
|
|
7350
7417
|
// If the CLI is busy, either hand the complete submission to its native input
|
|
7351
7418
|
// queue or wait for idle. Native queue support is an explicit backend capability:
|
|
@@ -7362,11 +7429,13 @@ export class Daemon extends EventEmitter {
|
|
|
7362
7429
|
// press Enter at once. A dialog painted a moment ago is still "busy" to
|
|
7363
7430
|
// the silence gate, so the probe is mandatory here; "dialog"/"unknown"
|
|
7364
7431
|
// fall through to the wait.
|
|
7365
|
-
|
|
7432
|
+
// #2676: an ordinary message goes into a mode-switched composer (kiro TUI) mid-turn too, on a launch whose busy
|
|
7433
|
+
// composer AgEnD drives, by the steer path's own gates and proof — when its box can show it is this paste.
|
|
7434
|
+
const canHandOff = (supportsQueuedInput || opts?.steer || composerQueue)
|
|
7366
7435
|
&& readiness === "busy"
|
|
7367
7436
|
&& (await this.probeBlockingDialog()).state === "clear"
|
|
7368
7437
|
&& await this.hasPositiveDeliveryInput(true)
|
|
7369
|
-
&& (!opts?.steer || (this.launchTakesSteer() && await this.steerComposerAdmits(false)));
|
|
7438
|
+
&& ((!opts?.steer && !composerQueue) || (this.launchTakesSteer() && await this.steerComposerAdmits(false, opts?.steer === true)));
|
|
7370
7439
|
if (canHandOff) {
|
|
7371
7440
|
// Native queue (codex), or an explicit /steer: hand the complete
|
|
7372
7441
|
// paste+Enter transaction to the busy CLI now. For steer this is the
|
|
@@ -7374,7 +7443,9 @@ export class Daemon extends EventEmitter {
|
|
|
7374
7443
|
// visibility check + idle-gated fallback catch TUIs that swallow
|
|
7375
7444
|
// busy input (see steerMessage).
|
|
7376
7445
|
handingOffToNativeQueue = true;
|
|
7377
|
-
this.logger.debug(opts?.steer ? "CLI busy — steering into the running turn"
|
|
7446
|
+
this.logger.debug(opts?.steer ? "CLI busy — steering into the running turn"
|
|
7447
|
+
: composerQueue ? "CLI busy — typing message into the busy composer"
|
|
7448
|
+
: "CLI busy — handing message to backend-native input queue");
|
|
7378
7449
|
}
|
|
7379
7450
|
else {
|
|
7380
7451
|
this.logger.debug("CLI busy — queuing message until idle");
|
|
@@ -7468,9 +7539,9 @@ export class Daemon extends EventEmitter {
|
|
|
7468
7539
|
// #1405: a launch that no longer takes a steer is not the one the gate saw — redo from the top (bounded), where
|
|
7469
7540
|
// the gate sends it down the idle path. The composer can also have changed mode since (the user's Ctrl+S), or
|
|
7470
7541
|
// the turn ended.
|
|
7471
|
-
if (handingOffToNativeQueue && opts?.steer && !this.launchTakesSteer())
|
|
7542
|
+
if (handingOffToNativeQueue && (opts?.steer || composerQueue) && !this.launchTakesSteer())
|
|
7472
7543
|
return "spawn-started";
|
|
7473
|
-
if (handingOffToNativeQueue && opts?.steer && !(await this.steerComposerAdmits(true)))
|
|
7544
|
+
if (handingOffToNativeQueue && (opts?.steer || composerQueue) && !(await this.steerComposerAdmits(true, opts?.steer === true)))
|
|
7474
7545
|
return "dialog";
|
|
7475
7546
|
// #829: a CLI that restores a cancelled prompt into its input box would
|
|
7476
7547
|
// have this message pasted onto it and both submitted as one. Clear it
|
|
@@ -7968,9 +8039,11 @@ export class Daemon extends EventEmitter {
|
|
|
7968
8039
|
&& dialogs.some(other => other !== dialog && Daemon.dialogMatches(other, pane)))
|
|
7969
8040
|
continue;
|
|
7970
8041
|
this.trackDialogParked(dialog);
|
|
8042
|
+
this.trackRemoteCard(dialog, pane, probeMono);
|
|
7971
8043
|
return { state: "dialog", dialog };
|
|
7972
8044
|
}
|
|
7973
8045
|
this.trackDialogParked(null);
|
|
8046
|
+
this.trackRemoteCard(null, pane, probeMono);
|
|
7974
8047
|
return { state: "clear" };
|
|
7975
8048
|
}
|
|
7976
8049
|
/**
|
|
@@ -8211,6 +8284,77 @@ export class Daemon extends EventEmitter {
|
|
|
8211
8284
|
this.logger.warn({ dialog: dialog.description, attempts: episode.ignored, holdsDeliveries }, "Runtime dialog is ignoring AgEnD's answer — reporting for a human");
|
|
8212
8285
|
this.emit("dialog_answer_ignored", { name: this.name, description: dialog.description, attempts: episode.ignored, holdsDeliveries });
|
|
8213
8286
|
}
|
|
8287
|
+
/**
|
|
8288
|
+
* #2675: a held card a person can answer from the chat (the dialog's remoteCard reader) is posted once it has stayed
|
|
8289
|
+
* the same card for REMOTE_CARD_STABLE_MS — `remote_card` with what it asks; the fleet posts it with its buttons. When
|
|
8290
|
+
* it closes or another card replaces it, `remote_card_gone` retires that post. Moving through the card (ours or the
|
|
8291
|
+
* person's in the pane) is the same card: its identity has no cursor. `readAt` is when the pane's capture was started
|
|
8292
|
+
* (performance.now): a read that started before the newest one used here finishes late and says nothing about now —
|
|
8293
|
+
* it may neither retire a newer card nor bring an older one back (#2677 review).
|
|
8294
|
+
*/
|
|
8295
|
+
trackRemoteCard(dialog, pane, readAt) {
|
|
8296
|
+
if (readAt < this.remoteCardReadAt)
|
|
8297
|
+
return;
|
|
8298
|
+
this.remoteCardReadAt = readAt;
|
|
8299
|
+
const card = dialog?.remoteCard?.(pane) ?? null;
|
|
8300
|
+
const shown = this.remoteCardShown;
|
|
8301
|
+
if (shown && shown.identity !== card?.identity) {
|
|
8302
|
+
this.remoteCardShown = null;
|
|
8303
|
+
if (shown.posted)
|
|
8304
|
+
this.emit("remote_card_gone", { name: this.name, identity: shown.identity });
|
|
8305
|
+
}
|
|
8306
|
+
if (!card)
|
|
8307
|
+
return;
|
|
8308
|
+
if (!this.remoteCardShown) {
|
|
8309
|
+
this.remoteCardShown = { identity: card.identity, since: performance.now(), posted: false };
|
|
8310
|
+
return;
|
|
8311
|
+
}
|
|
8312
|
+
if (this.remoteCardShown.posted || performance.now() - this.remoteCardShown.since < REMOTE_CARD_STABLE_MS)
|
|
8313
|
+
return;
|
|
8314
|
+
this.remoteCardShown.posted = true;
|
|
8315
|
+
this.logger.info({ kind: card.kind }, "A card a person can answer from the chat is on screen — posting it");
|
|
8316
|
+
this.emit("remote_card", { name: this.name, card });
|
|
8317
|
+
}
|
|
8318
|
+
/** #2675: the card's post was used up without answering it (refused, stopped part-way): post it again once it holds still. */
|
|
8319
|
+
repostRemoteCard() {
|
|
8320
|
+
this.remoteCardShown = null;
|
|
8321
|
+
}
|
|
8322
|
+
/**
|
|
8323
|
+
* #2675: answer the card on screen from the chat, as the person chose: only while it is the card they saw
|
|
8324
|
+
* (`identity`), on a verified cursor, under the pane write lock (remote-card-answer.ts). Never logs the answer.
|
|
8325
|
+
*/
|
|
8326
|
+
async answerRemoteCard(identity, action) {
|
|
8327
|
+
const tmux = this.tmux;
|
|
8328
|
+
const readers = this.runtimeDialogs().filter(dialog => dialog.remoteCard);
|
|
8329
|
+
if (!tmux || readers.length === 0)
|
|
8330
|
+
return { outcome: "stale" };
|
|
8331
|
+
if (this.remoteCardAnswering)
|
|
8332
|
+
return { outcome: "refused", reason: "another answer to this card is being keyed in" };
|
|
8333
|
+
this.remoteCardAnswering = true;
|
|
8334
|
+
try {
|
|
8335
|
+
const outcome = await answerRemoteCard({
|
|
8336
|
+
locked: fn => this.paneWriteLock.run(fn),
|
|
8337
|
+
// An unreadable screen (a failed capture, another spawn's pane) is unknown — never "the card closed".
|
|
8338
|
+
read: () => this.tmux === tmux ? tmux.capturePane().catch(() => null) : Promise.resolve(null),
|
|
8339
|
+
key: key => this.tmux !== tmux ? Promise.resolve(false) : key === "Space" ? tmux.sendKeys(" ") : tmux.sendSpecialKey(key),
|
|
8340
|
+
paste: text => this.tmux !== tmux ? Promise.resolve(false) : tmux.pasteBuffer(text),
|
|
8341
|
+
sleep: ms => new Promise(resolve => setTimeout(resolve, ms)),
|
|
8342
|
+
now: () => performance.now(),
|
|
8343
|
+
}, pane => {
|
|
8344
|
+
for (const dialog of readers) {
|
|
8345
|
+
const card = dialog.remoteCard(pane);
|
|
8346
|
+
if (card)
|
|
8347
|
+
return card;
|
|
8348
|
+
}
|
|
8349
|
+
return null;
|
|
8350
|
+
}, identity, action);
|
|
8351
|
+
this.logger.info({ action: action.kind, ...outcome }, "Answered a card from the chat");
|
|
8352
|
+
return outcome;
|
|
8353
|
+
}
|
|
8354
|
+
finally {
|
|
8355
|
+
this.remoteCardAnswering = false;
|
|
8356
|
+
}
|
|
8357
|
+
}
|
|
8214
8358
|
/**
|
|
8215
8359
|
* Remember how long a blocking dialog has been on screen. Once it has
|
|
8216
8360
|
* outlived DIALOG_PARKED_NOTIFY_MS — the auto-dismiss did not take, or the
|
|
@@ -8314,21 +8458,48 @@ export class Daemon extends EventEmitter {
|
|
|
8314
8458
|
* while a fresh capture reads its composer as "steer" — or "idle" once the turn has ended (`allowIdle`, the pre-write
|
|
8315
8459
|
* re-check). "queue" (the user's choice), text in the box, or a screen it cannot read: not now — the caller waits for
|
|
8316
8460
|
* the idle prompt as for any message. AgEnD never switches the mode. Backends without the reader are not asked.
|
|
8461
|
+
* #2676: an ordinary message (`steerOnly` false) goes in on "queue" too — kiro takes it into the same tray either way,
|
|
8462
|
+
* held for the turn's end instead of steered into it.
|
|
8317
8463
|
*/
|
|
8318
|
-
async steerComposerAdmits(allowIdle) {
|
|
8464
|
+
async steerComposerAdmits(allowIdle, steerOnly = true) {
|
|
8319
8465
|
const read = this.backend?.readSteerComposer;
|
|
8320
8466
|
if (!read)
|
|
8321
8467
|
return true;
|
|
8322
8468
|
if (!this.tmux)
|
|
8323
8469
|
return false;
|
|
8324
|
-
let
|
|
8470
|
+
let pane;
|
|
8325
8471
|
try {
|
|
8326
|
-
|
|
8472
|
+
pane = await this.tmux.capturePane();
|
|
8327
8473
|
}
|
|
8328
8474
|
catch {
|
|
8329
8475
|
return false;
|
|
8330
8476
|
}
|
|
8331
|
-
return
|
|
8477
|
+
return composerModeAdmits(read.call(this.backend, pane), allowIdle, steerOnly) && this.busyInputReachesTurn(pane);
|
|
8478
|
+
}
|
|
8479
|
+
/** #2682: a busy composer's typed input reaches the CLI's own turn (busyTurnTakesInput); an idle box, or no reader: yes. */
|
|
8480
|
+
busyInputReachesTurn(pane) {
|
|
8481
|
+
const mode = this.backend?.readSteerComposer?.(pane);
|
|
8482
|
+
if (mode !== "steer" && mode !== "queue")
|
|
8483
|
+
return true;
|
|
8484
|
+
return this.backend?.busyTurnTakesInput?.(pane) !== false;
|
|
8485
|
+
}
|
|
8486
|
+
/**
|
|
8487
|
+
* #2676: whether an ordinary message may be typed into this CLI's busy composer — a mode-switched one (kiro TUI) on a
|
|
8488
|
+
* launch whose busy composer AgEnD drives (the same evidence as a steer: KIRO_STEER_VERIFIED). The legacy UI, an
|
|
8489
|
+
* unlisted version and every backend without the reader answer false; claude and codex hand off by
|
|
8490
|
+
* supportsQueuedInput instead.
|
|
8491
|
+
*/
|
|
8492
|
+
composerTakesBusyInput() {
|
|
8493
|
+
return !!this.backend?.readSteerComposer && this.backend.supportsSteer?.() === true;
|
|
8494
|
+
}
|
|
8495
|
+
/**
|
|
8496
|
+
* #2676: whether the box can show this payload as this delivery's paste before the Enter (steerPasteIsOurs): its
|
|
8497
|
+
* first line, or a collapsed paste's line count. A one-line payload kiro would collapse (over 500 characters) shows only
|
|
8498
|
+
* a character count, which kiro's own normalisation (tabs widened) makes no proof: it waits for the idle prompt.
|
|
8499
|
+
* kiro measures after widening each tab to its 2-column stop, so a tab is counted as 2 here (at most what it becomes).
|
|
8500
|
+
*/
|
|
8501
|
+
composerCanShowPaste(formatted) {
|
|
8502
|
+
return /[\r\n]/.test(formatted) || formatted.replace(/\t/g, " ").length <= KIRO_COLLAPSE_CHARS;
|
|
8332
8503
|
}
|
|
8333
8504
|
/**
|
|
8334
8505
|
* #1405: one look at a mode-switched composer for a steer's own proof: on the tmux of this write, inside `deadline`
|
|
@@ -8362,10 +8533,18 @@ export class Daemon extends EventEmitter {
|
|
|
8362
8533
|
if (first === "")
|
|
8363
8534
|
return false;
|
|
8364
8535
|
const row = norm(shown);
|
|
8536
|
+
// #2676: a long paste is collapsed to a chip naming its line count (kiro TUI: more than 10 lines or 500 characters);
|
|
8537
|
+
// it is this paste when that count is exactly this payload's as kiro counts it. tmux pastes every LF as CR (no -r),
|
|
8538
|
+
// so a CRLF arrives as CR CR, and kiro turns each CR into a line break: every CR and every LF is one.
|
|
8539
|
+
const collapsed = this.backend?.readSteerComposerPasteLines?.(shown);
|
|
8540
|
+
if (collapsed != null)
|
|
8541
|
+
return collapsed > 1 && collapsed === formatted.split(/\r|\n/).length;
|
|
8365
8542
|
// The marker line holds nothing but the marker: the row is exactly it.
|
|
8366
8543
|
if (/^\[agend-(?:delivery|human)-id:[^\]\s]+\]$/.test(first))
|
|
8367
8544
|
return row === first;
|
|
8368
|
-
|
|
8545
|
+
// #2676: a first line wider than the box is word-wrapped onto indented rows (live, 2.34.0): the row is its start, cut
|
|
8546
|
+
// at a word boundary.
|
|
8547
|
+
return row === first || row.startsWith(`${first} `) || first.startsWith(`${row} `);
|
|
8369
8548
|
}
|
|
8370
8549
|
/**
|
|
8371
8550
|
* Codex can paint a prompt before the TTY enters raw mode. Both are required.
|
|
@@ -8512,6 +8691,18 @@ export class Daemon extends EventEmitter {
|
|
|
8512
8691
|
if (this.needsStartupInputProof()) {
|
|
8513
8692
|
return await this.hasPositiveDeliveryInput() ? "ready" : "transient";
|
|
8514
8693
|
}
|
|
8694
|
+
// #2682: a backend that can read its own idle input box (kiro's TUI) must show it before an idle paste. Anything
|
|
8695
|
+
// else — another program owning the pane, a frame it cannot read, a capture that fails — is not one to type into.
|
|
8696
|
+
if (this.backend?.idleComposerReady && this.tmux) {
|
|
8697
|
+
try {
|
|
8698
|
+
if (!this.backend.idleComposerReady(await this.tmux.capturePane()))
|
|
8699
|
+
return "busy";
|
|
8700
|
+
}
|
|
8701
|
+
catch (err) {
|
|
8702
|
+
this.logger.debug({ err }, "capture-pane failed during the composer check — treating the pane as not ready");
|
|
8703
|
+
return "busy";
|
|
8704
|
+
}
|
|
8705
|
+
}
|
|
8515
8706
|
if (this.backend?.dropsEnterWhileBusy?.() !== true)
|
|
8516
8707
|
return "ready";
|
|
8517
8708
|
const prompt = this.backend.getBottomReadyPattern?.();
|
|
@@ -9155,15 +9346,16 @@ export class Daemon extends EventEmitter {
|
|
|
9155
9346
|
}
|
|
9156
9347
|
// #1405: on that same last capture a mode-switched composer (kiro TUI) must still read "steer" — or "idle", the
|
|
9157
9348
|
// turn over. "queue", text in the box or an unreadable row: not written; handled as the unread box above.
|
|
9158
|
-
|
|
9159
|
-
|
|
9349
|
+
// #2676: an ordinary message typed into it (every other hand-off there) may also find "queue".
|
|
9350
|
+
if (handingOffToNativeQueue && this.backend?.readSteerComposer
|
|
9351
|
+
&& (!composerModeAdmits(pasteBaseline?.steerComposer ?? null, true, steer) || pasteBaseline?.busyInputReachesTurn === false)) {
|
|
9160
9352
|
if (!verdict.durableBeginCommitted)
|
|
9161
9353
|
return "handoff-box-unread";
|
|
9162
|
-
return this.failDelivery(verdict, status, "paste", "steer-composer-not-steering");
|
|
9354
|
+
return this.failDelivery(verdict, status, "paste", steer ? "steer-composer-not-steering" : "composer-not-empty");
|
|
9163
9355
|
}
|
|
9164
9356
|
// …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()) {
|
|
9357
|
+
// is retried, as a steer the gate will then send down the idle path (#2676: so is a message typed into the box).
|
|
9358
|
+
if (handingOffToNativeQueue && (steer || this.backend?.readSteerComposer) && !this.launchTakesSteer()) {
|
|
9167
9359
|
verdict.fenced = !verdict.durableBeginCommitted;
|
|
9168
9360
|
return false;
|
|
9169
9361
|
}
|
|
@@ -9293,7 +9485,8 @@ export class Daemon extends EventEmitter {
|
|
|
9293
9485
|
// #1405: a steer into a mode-switched composer (kiro TUI) is proven by the box alone — before the Enter it must
|
|
9294
9486
|
// positively hold typed text, and after it be the empty placeholder again. The empty placeholder here means the
|
|
9295
9487
|
// paste never reached the box; an unreadable box (a failed or late capture, a dialog) proves nothing: no Enter.
|
|
9296
|
-
|
|
9488
|
+
// #2676: an ordinary message typed into the busy box is proven the same way.
|
|
9489
|
+
const steerComposerProof = handingOffToNativeQueue && !!this.backend?.readSteerComposer && tmuxAtWrite !== null;
|
|
9297
9490
|
let humanComposerOwned = false;
|
|
9298
9491
|
let steerEnterDeadline = Infinity;
|
|
9299
9492
|
if (steerComposerProof) {
|
|
@@ -9394,8 +9587,8 @@ export class Daemon extends EventEmitter {
|
|
|
9394
9587
|
if (humanComposerOwned)
|
|
9395
9588
|
signature.humanPositive?.(after === "steer" ? "composer-empty-steer"
|
|
9396
9589
|
: after === "queue" ? "composer-empty-queue" : "composer-empty-idle");
|
|
9397
|
-
verdict.phase = "steer-accepted";
|
|
9398
|
-
verdict.proof =
|
|
9590
|
+
verdict.phase = steer ? "steer-accepted" : "composer-accepted";
|
|
9591
|
+
verdict.proof = `${steer ? "steer" : "handoff"}-composer-${after}`;
|
|
9399
9592
|
if (status)
|
|
9400
9593
|
this.emit("message_confirmed", status); // ✅ taken from the box
|
|
9401
9594
|
return true;
|
|
@@ -10086,6 +10279,7 @@ export class Daemon extends EventEmitter {
|
|
|
10086
10279
|
collapsedPastes: input?.collapsedPastes ?? 0,
|
|
10087
10280
|
inputReadable: input != null,
|
|
10088
10281
|
steerComposer: this.backend?.readSteerComposer?.(pane) ?? null,
|
|
10282
|
+
busyInputReachesTurn: this.busyInputReachesTurn(pane),
|
|
10089
10283
|
};
|
|
10090
10284
|
}
|
|
10091
10285
|
/**
|
|
@@ -10581,6 +10775,12 @@ export class Daemon extends EventEmitter {
|
|
|
10581
10775
|
reply(failed ? null : answer, failed);
|
|
10582
10776
|
return;
|
|
10583
10777
|
}
|
|
10778
|
+
if (tool === "office_pack_check" || tool === "office_pack_upload") {
|
|
10779
|
+
// #2692: the folder (or the manifest's files) is read here, on the agent's side; the fleet gets bytes in parts.
|
|
10780
|
+
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 : {}); });
|
|
10781
|
+
reply(answer, undefined);
|
|
10782
|
+
return;
|
|
10783
|
+
}
|
|
10584
10784
|
if (tool === "upload_web_effect") {
|
|
10585
10785
|
const owner = { fence: this.launchFenceEpoch, spawn: this.spawnGeneration, boot: this.bootId, ipc: this.ipcServer };
|
|
10586
10786
|
const current = () => !!owner.ipc && owner.ipc === this.ipcServer && owner.fence === this.launchFenceEpoch && owner.spawn === this.spawnGeneration && owner.boot === this.bootId && !this.deliveryWritesStopping && !this.startupAborted;
|
|
@@ -11351,6 +11551,23 @@ export class Daemon extends EventEmitter {
|
|
|
11351
11551
|
paneText = await this.tmux?.capturePaneWithHistory(50);
|
|
11352
11552
|
}
|
|
11353
11553
|
catch { /* pane may be gone */ }
|
|
11554
|
+
if (paneText && this.backend.resumeBlockedPattern?.().test(paneText)) {
|
|
11555
|
+
// #2598: a duplicate id proves the conversation exists. The generic retry escape hatch must not
|
|
11556
|
+
// abandon that pin or turn a store-selection error into a fresh conversation, even after many retries.
|
|
11557
|
+
await this.killProcessTree("SIGTERM", "spawn: retained session is blocked");
|
|
11558
|
+
this.startupAdmission?.();
|
|
11559
|
+
await this.tmux.killWindow();
|
|
11560
|
+
this.startupAdmission?.();
|
|
11561
|
+
throw new ResumeBlockedError(t("inst.kiro_resume_ambiguous"), t("inst.kiro_resume_ambiguous_remedy"));
|
|
11562
|
+
}
|
|
11563
|
+
const retained = this.backend.retainedResumeBlock?.();
|
|
11564
|
+
if (retained) {
|
|
11565
|
+
await this.killProcessTree("SIGTERM", "spawn: carried session must be preserved");
|
|
11566
|
+
this.startupAdmission?.();
|
|
11567
|
+
await this.tmux.killWindow();
|
|
11568
|
+
this.startupAdmission?.();
|
|
11569
|
+
throw new ResumeBlockedError(retained.reason, retained.remedy);
|
|
11570
|
+
}
|
|
11354
11571
|
const proven = this.paneSaysNoConversation(paneText);
|
|
11355
11572
|
if (!proven) {
|
|
11356
11573
|
const failures = this.bumpUnprovenResumeFailures();
|
|
@@ -11543,6 +11760,188 @@ export class Daemon extends EventEmitter {
|
|
|
11543
11760
|
this.logger.warn({ err }, "The agent switch after resume failed — the shared entries stay; the next launch tries again");
|
|
11544
11761
|
}
|
|
11545
11762
|
}
|
|
11763
|
+
/**
|
|
11764
|
+
* M-2025: after a tui/V3 launch is ready, type `/session-id` and `/title` once
|
|
11765
|
+
* each, under the pane-write lock, with the same fences as {@link ensureBackendAgent}.
|
|
11766
|
+
* Does not touch the delivery epoch. A mismatch with the #2617 pin is surfaced;
|
|
11767
|
+
* the pin is never replaced by a file-store guess. Legacy backends return null
|
|
11768
|
+
* from sessionProof().
|
|
11769
|
+
*
|
|
11770
|
+
* #2683: these are AgEnD's own housekeeping, so unlike a delivery (which may join
|
|
11771
|
+
* a person's draft) each one goes only into a composer the final capture shows
|
|
11772
|
+
* positively EMPTY; Enter only once a capture shows exactly the command in the box;
|
|
11773
|
+
* and it counts as run only once the box is empty again — a command left in the
|
|
11774
|
+
* box stops here (it is taken back when the box shows exactly it), and nothing
|
|
11775
|
+
* else is typed after it. The id is taken only from kiro's `/session-id` alert
|
|
11776
|
+
* (readSessionIdAlert) on a capture after the command ran, and never when that
|
|
11777
|
+
* alert was already on the final capture before it was typed: an announcement
|
|
11778
|
+
* that was there before is not an answer to this request.
|
|
11779
|
+
*/
|
|
11780
|
+
async ensureKiroSessionProof() {
|
|
11781
|
+
let proof = null;
|
|
11782
|
+
try {
|
|
11783
|
+
proof = this.backend?.sessionProof?.() ?? null;
|
|
11784
|
+
}
|
|
11785
|
+
catch {
|
|
11786
|
+
proof = null;
|
|
11787
|
+
}
|
|
11788
|
+
const tmux = this.tmux;
|
|
11789
|
+
if (!proof || !tmux || !this.backend)
|
|
11790
|
+
return;
|
|
11791
|
+
const windowId = tmux.getWindowId();
|
|
11792
|
+
const spawn = this.spawnGeneration, fence = this.launchFenceEpoch, phase = this.pauseWakeState;
|
|
11793
|
+
const fenced = () => !this.startupAborted && !this.deliveryWritesStopping && !this.fatalStartupBlocked
|
|
11794
|
+
&& (phase === "active" || phase === "waking") && this.pauseWakeState === phase
|
|
11795
|
+
&& spawn === this.spawnGeneration && fence === this.launchFenceEpoch && this.tmux === tmux;
|
|
11796
|
+
const deadline = performance.now() + Daemon.AGENT_SWITCH_BUDGET_MS;
|
|
11797
|
+
const live = () => { this.startupAdmission?.(); return fenced() && performance.now() < deadline; };
|
|
11798
|
+
if (!live())
|
|
11799
|
+
return;
|
|
11800
|
+
// A proof belongs to the launch that read it: an earlier launch's never stands for this one.
|
|
11801
|
+
try {
|
|
11802
|
+
clearKiroSessionProof(this.instanceDir);
|
|
11803
|
+
}
|
|
11804
|
+
catch { /* recorded again below only on a fresh match */ }
|
|
11805
|
+
const TIMEOUT = Symbol("deadline");
|
|
11806
|
+
const within = (work) => {
|
|
11807
|
+
const left = deadline - performance.now();
|
|
11808
|
+
if (left <= 0) {
|
|
11809
|
+
work.catch(() => { });
|
|
11810
|
+
return Promise.resolve(TIMEOUT);
|
|
11811
|
+
}
|
|
11812
|
+
let timer;
|
|
11813
|
+
return Promise.race([work, new Promise(r => { timer = setTimeout(() => r(TIMEOUT), left); })])
|
|
11814
|
+
.finally(() => clearTimeout(timer));
|
|
11815
|
+
};
|
|
11816
|
+
const capture = async () => {
|
|
11817
|
+
const pane = await within(tmux.capturePane().catch(() => null));
|
|
11818
|
+
return pane === TIMEOUT ? null : pane;
|
|
11819
|
+
};
|
|
11820
|
+
const pause = (ms) => within(new Promise(r => setTimeout(r, ms)));
|
|
11821
|
+
const op = Daemon.AGENT_SWITCH_TMUX_OP_MS;
|
|
11822
|
+
const box = async () => {
|
|
11823
|
+
const pane = await capture();
|
|
11824
|
+
return pane === null ? null : proof.readComposer(pane);
|
|
11825
|
+
};
|
|
11826
|
+
/**
|
|
11827
|
+
* Our command, and only it, is in the box: take it back — also past the deadline, on its own bounded capture.
|
|
11828
|
+
* Anything else there is left as it is.
|
|
11829
|
+
*/
|
|
11830
|
+
const takeBack = async (command) => {
|
|
11831
|
+
let timer;
|
|
11832
|
+
const pane = await Promise.race([tmux.capturePane().catch(() => null), new Promise(r => { timer = setTimeout(() => r(null), op); })])
|
|
11833
|
+
.finally(() => clearTimeout(timer));
|
|
11834
|
+
if (pane !== null && fenced() && proof.readComposer(pane) === command)
|
|
11835
|
+
await tmux.deleteBackward(command.length, op);
|
|
11836
|
+
};
|
|
11837
|
+
/**
|
|
11838
|
+
* "ran": typed, Enter, and the box is empty again; `after` then read the pane with the capture from before the
|
|
11839
|
+
* paste, still under the lock. "stuck": typed but not run — nothing more may be typed. null: nothing typed.
|
|
11840
|
+
*/
|
|
11841
|
+
const typeOnce = async (command, after) => {
|
|
11842
|
+
const wrote = await within(this.paneWriteLock.run(async () => {
|
|
11843
|
+
if (!live())
|
|
11844
|
+
return null;
|
|
11845
|
+
const readiness = await within(this.paneReadinessForDelivery(windowId));
|
|
11846
|
+
if (readiness !== "ready" || !live())
|
|
11847
|
+
return null;
|
|
11848
|
+
const before = await capture();
|
|
11849
|
+
if (before === null || !live())
|
|
11850
|
+
return null;
|
|
11851
|
+
if (this.inputTransientInPane(before) || !this.paneAuthoritativelyIdle(before))
|
|
11852
|
+
return null;
|
|
11853
|
+
if (proof.readComposer(before) !== "")
|
|
11854
|
+
return null; // a draft, or a box that cannot be read
|
|
11855
|
+
if (deadline - performance.now() < Daemon.AGENT_SWITCH_WRITE_RESERVE_MS)
|
|
11856
|
+
return null;
|
|
11857
|
+
if (!await tmux.pasteBuffer(command, { guard: live, timeoutMs: op }))
|
|
11858
|
+
return "stuck";
|
|
11859
|
+
let shown = null;
|
|
11860
|
+
for (let i = 0; i < 4 && live() && shown !== command; i++) {
|
|
11861
|
+
await pause(i === 0 ? Daemon.AGENT_SWITCH_PASTE_SETTLE_MS : Daemon.AGENT_SWITCH_POLL_MS);
|
|
11862
|
+
shown = await box();
|
|
11863
|
+
}
|
|
11864
|
+
if (shown !== command || !live()) {
|
|
11865
|
+
await takeBack(command);
|
|
11866
|
+
return "stuck";
|
|
11867
|
+
}
|
|
11868
|
+
if (!await tmux.sendSpecialKey("Enter", op)) {
|
|
11869
|
+
await takeBack(command);
|
|
11870
|
+
return "stuck";
|
|
11871
|
+
}
|
|
11872
|
+
let left = command;
|
|
11873
|
+
for (let i = 0; i < 6 && left !== ""; i++) {
|
|
11874
|
+
await pause(Daemon.AGENT_SWITCH_POLL_MS);
|
|
11875
|
+
if (!fenced())
|
|
11876
|
+
return "stuck";
|
|
11877
|
+
left = await box();
|
|
11878
|
+
}
|
|
11879
|
+
if (left !== "") {
|
|
11880
|
+
await takeBack(command);
|
|
11881
|
+
return "stuck";
|
|
11882
|
+
}
|
|
11883
|
+
return { ran: true, value: await after(before) };
|
|
11884
|
+
}));
|
|
11885
|
+
return wrote === TIMEOUT ? "stuck" : wrote;
|
|
11886
|
+
};
|
|
11887
|
+
try {
|
|
11888
|
+
const sid = await typeOnce(proof.sessionIdCommand, async (before) => {
|
|
11889
|
+
if (proof.readSessionIdAlert(before) !== null)
|
|
11890
|
+
return null; // already there: ours cannot be told apart
|
|
11891
|
+
for (let i = 0; i < 6 && live(); i++) {
|
|
11892
|
+
const pane = await capture();
|
|
11893
|
+
if (pane === null)
|
|
11894
|
+
break;
|
|
11895
|
+
const id = proof.readSessionIdAlert(pane);
|
|
11896
|
+
if (id !== null)
|
|
11897
|
+
return id;
|
|
11898
|
+
await pause(Daemon.AGENT_SWITCH_POLL_MS);
|
|
11899
|
+
}
|
|
11900
|
+
return null;
|
|
11901
|
+
});
|
|
11902
|
+
if (!live())
|
|
11903
|
+
return;
|
|
11904
|
+
if (sid === "stuck") {
|
|
11905
|
+
this.logger.warn("kiro /session-id did not run from an empty composer; /title is not typed after it");
|
|
11906
|
+
return;
|
|
11907
|
+
}
|
|
11908
|
+
// Unread/mismatch are about kiro's answer after we ran /session-id.
|
|
11909
|
+
// A composer that never read empty is not unread — we never asked.
|
|
11910
|
+
if (sid) {
|
|
11911
|
+
const verdict = proveKiroSessionId(proof.pinned, sid.value);
|
|
11912
|
+
if (verdict.kind === "mismatch") {
|
|
11913
|
+
const message = t("kiro.session_id_mismatch", verdict.pinned, verdict.observed);
|
|
11914
|
+
this.logger.warn({ pinned: verdict.pinned, observed: verdict.observed }, "kiro /session-id does not match the pinned conversation");
|
|
11915
|
+
this.emit("backend_launch_warning", { name: this.name, message });
|
|
11916
|
+
}
|
|
11917
|
+
else if (verdict.kind === "unread") {
|
|
11918
|
+
const message = t("kiro.session_id_unread");
|
|
11919
|
+
this.logger.warn("kiro /session-id did not answer on the pane; not guessing from files");
|
|
11920
|
+
this.emit("backend_launch_warning", { name: this.name, message });
|
|
11921
|
+
}
|
|
11922
|
+
else if (verdict.kind === "match" || verdict.kind === "fresh") {
|
|
11923
|
+
this.logger.info({ id: verdict.id, kind: verdict.kind }, "kiro /session-id on the pane");
|
|
11924
|
+
try {
|
|
11925
|
+
recordKiroSessionProof(this.instanceDir, { sessionId: verdict.id, kind: verdict.kind });
|
|
11926
|
+
}
|
|
11927
|
+
catch (err) {
|
|
11928
|
+
this.logger.warn({ err, id: verdict.id }, "kiro /session-id was proven on the pane but could not be recorded");
|
|
11929
|
+
}
|
|
11930
|
+
}
|
|
11931
|
+
}
|
|
11932
|
+
if (!live() || !proof.titleCommand)
|
|
11933
|
+
return;
|
|
11934
|
+
const titled = await typeOnce(proof.titleCommand, async () => true);
|
|
11935
|
+
if (titled && titled !== "stuck")
|
|
11936
|
+
this.logger.info({ title: proof.titleCommand }, "kiro /title sent for this instance");
|
|
11937
|
+
else if (titled === "stuck")
|
|
11938
|
+
this.logger.warn({ title: proof.titleCommand }, "kiro /title did not run from an empty composer");
|
|
11939
|
+
}
|
|
11940
|
+
catch (err) {
|
|
11941
|
+
this.startupAdmission?.();
|
|
11942
|
+
this.logger.warn({ err }, "kiro /session-id or /title after launch failed — the pin was not guessed from files");
|
|
11943
|
+
}
|
|
11944
|
+
}
|
|
11546
11945
|
/**
|
|
11547
11946
|
* Startup budget for this launch: the backend's override (resume-aware), never
|
|
11548
11947
|
* below a user-configured startup_timeout_ms; undefined = trySpawn's default.
|
|
@@ -11705,6 +12104,9 @@ export class Daemon extends EventEmitter {
|
|
|
11705
12104
|
if (ready)
|
|
11706
12105
|
await this.ensureBackendAgent();
|
|
11707
12106
|
this.startupAdmission?.();
|
|
12107
|
+
if (ready)
|
|
12108
|
+
await this.ensureKiroSessionProof();
|
|
12109
|
+
this.startupAdmission?.();
|
|
11708
12110
|
return ready;
|
|
11709
12111
|
}
|
|
11710
12112
|
/**
|