@songsid/agend 2.2.0-alpha.1 → 2.2.0-alpha.3
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 -4
- package/README.zh-TW.md +6 -4
- package/dist/agent-cli.js +6 -1
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +4 -1
- package/dist/agent-endpoint.js +82 -14
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-token-header.d.ts +13 -0
- package/dist/agent-token-header.js +16 -0
- package/dist/agent-token-header.js.map +1 -0
- package/dist/atomic-write.d.ts +21 -0
- package/dist/atomic-write.js +65 -0
- package/dist/atomic-write.js.map +1 -0
- package/dist/auth-api.js +2 -0
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/antigravity.js +4 -3
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/codex.d.ts +7 -0
- package/dist/backend/codex.js +7 -0
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/grok.js +1 -1
- package/dist/backend/grok.js.map +1 -1
- package/dist/backend/kiro.d.ts +7 -0
- package/dist/backend/kiro.js +82 -33
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/muse.d.ts +7 -0
- package/dist/backend/muse.js +8 -1
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +12 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/binary-probe.d.ts +80 -0
- package/dist/binary-probe.js +182 -0
- package/dist/binary-probe.js.map +1 -0
- package/dist/cache-analysis.d.ts +92 -0
- package/dist/cache-analysis.js +147 -0
- package/dist/cache-analysis.js.map +1 -0
- package/dist/cache-ledger.d.ts +108 -0
- package/dist/cache-ledger.js +230 -0
- package/dist/cache-ledger.js.map +1 -0
- package/dist/cache-prices.d.ts +32 -0
- package/dist/cache-prices.js +73 -0
- package/dist/cache-prices.js.map +1 -0
- package/dist/cache-scan.d.ts +39 -0
- package/dist/cache-scan.js +115 -0
- package/dist/cache-scan.js.map +1 -0
- package/dist/cache-service.d.ts +131 -0
- package/dist/cache-service.js +452 -0
- package/dist/cache-service.js.map +1 -0
- package/dist/channel/adapters/discord.d.ts +5 -1
- package/dist/channel/adapters/discord.js +56 -11
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +38 -1
- package/dist/channel/adapters/telegram.js +82 -0
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +1 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/tool-router.d.ts +8 -1
- package/dist/channel/tool-router.js +4 -2
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +21 -0
- package/dist/cli-entry.d.ts +19 -0
- package/dist/cli-entry.js +36 -0
- package/dist/cli-entry.js.map +1 -0
- package/dist/cli.js +376 -54
- package/dist/cli.js.map +1 -1
- package/dist/connection-secrets.d.ts +5 -0
- package/dist/connection-secrets.js.map +1 -1
- package/dist/cpu-profile.js +50 -0
- package/dist/cpu-profile.js.map +1 -1
- package/dist/daemon.d.ts +18 -0
- package/dist/daemon.js +256 -63
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +1 -0
- package/dist/delivery-outbox.js +12 -0
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/discord-permissions.d.ts +33 -0
- package/dist/discord-permissions.js +42 -0
- package/dist/discord-permissions.js.map +1 -0
- package/dist/env-file.d.ts +11 -0
- package/dist/env-file.js +27 -0
- package/dist/env-file.js.map +1 -0
- package/dist/event-log.d.ts +4 -1
- package/dist/event-log.js +6 -3
- package/dist/event-log.js.map +1 -1
- package/dist/fleet-context.d.ts +5 -0
- package/dist/fleet-context.js.map +1 -1
- package/dist/fleet-control-audit.d.ts +5 -0
- package/dist/fleet-control-audit.js +7 -0
- package/dist/fleet-control-audit.js.map +1 -1
- package/dist/fleet-level-config.d.ts +8 -1
- package/dist/fleet-level-config.js +53 -0
- package/dist/fleet-level-config.js.map +1 -1
- package/dist/fleet-manager.d.ts +122 -7
- package/dist/fleet-manager.js +871 -152
- package/dist/fleet-manager.js.map +1 -1
- package/dist/fleet-stop-budget.d.ts +22 -0
- package/dist/fleet-stop-budget.js +95 -0
- package/dist/fleet-stop-budget.js.map +1 -0
- package/dist/full-restart.js +2 -3
- package/dist/full-restart.js.map +1 -1
- package/dist/general-knowledge/skills/persona-emoji/SKILL.md +3 -1
- package/dist/install-lock.d.ts +49 -0
- package/dist/install-lock.js +200 -0
- package/dist/install-lock.js.map +1 -0
- package/dist/instance-config-impact.js +3 -2
- package/dist/instance-config-impact.js.map +1 -1
- package/dist/kiro-transcript-lane.d.ts +8 -4
- package/dist/kiro-transcript-lane.js +66 -44
- package/dist/kiro-transcript-lane.js.map +1 -1
- package/dist/locale.js +96 -0
- package/dist/locale.js.map +1 -1
- package/dist/next-steps.d.ts +5 -0
- package/dist/next-steps.js +26 -0
- package/dist/next-steps.js.map +1 -0
- package/dist/outbound-handlers.js +8 -0
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +4 -0
- package/dist/outbound-schemas.js +7 -2
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/outbox-open-error.d.ts +7 -0
- package/dist/outbox-open-error.js +15 -0
- package/dist/outbox-open-error.js.map +1 -0
- package/dist/package-preimage.d.ts +26 -0
- package/dist/package-preimage.js +96 -0
- package/dist/package-preimage.js.map +1 -0
- package/dist/pane-context-cache.d.ts +25 -0
- package/dist/pane-context-cache.js +73 -0
- package/dist/pane-context-cache.js.map +1 -0
- package/dist/paths.d.ts +0 -7
- package/dist/paths.js +8 -2
- package/dist/paths.js.map +1 -1
- package/dist/provider-probe.d.ts +15 -0
- package/dist/provider-probe.js +20 -0
- package/dist/provider-probe.js.map +1 -1
- package/dist/public-link-progress.d.ts +73 -0
- package/dist/public-link-progress.js +190 -0
- package/dist/public-link-progress.js.map +1 -0
- package/dist/public-web-gateway.js +2 -2
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +6 -1
- package/dist/public-web-link.js +87 -15
- package/dist/public-web-link.js.map +1 -1
- package/dist/quickstart-api.d.ts +93 -20
- package/dist/quickstart-api.js +194 -62
- package/dist/quickstart-api.js.map +1 -1
- package/dist/quickstart.d.ts +15 -0
- package/dist/quickstart.js +110 -33
- package/dist/quickstart.js.map +1 -1
- package/dist/reply-buttons-controller.d.ts +98 -0
- package/dist/reply-buttons-controller.js +174 -0
- package/dist/reply-buttons-controller.js.map +1 -0
- package/dist/reply-buttons.d.ts +113 -0
- package/dist/reply-buttons.js +208 -0
- package/dist/reply-buttons.js.map +1 -0
- package/dist/reply-dedup.js +3 -1
- package/dist/reply-dedup.js.map +1 -1
- package/dist/restart-guard.d.ts +53 -0
- package/dist/restart-guard.js +114 -0
- package/dist/restart-guard.js.map +1 -0
- package/dist/scheduler/db.js +6 -2
- package/dist/scheduler/db.js.map +1 -1
- package/dist/service-activation.d.ts +62 -7
- package/dist/service-activation.js +237 -55
- package/dist/service-activation.js.map +1 -1
- package/dist/service-installer.d.ts +56 -1
- package/dist/service-installer.js +152 -12
- package/dist/service-installer.js.map +1 -1
- package/dist/service-plan.d.ts +65 -0
- package/dist/service-plan.js +108 -0
- package/dist/service-plan.js.map +1 -0
- package/dist/settings-api.js +92 -10
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-authority.d.ts +11 -0
- package/dist/settings-authority.js +128 -0
- package/dist/settings-authority.js.map +1 -0
- package/dist/settings-change.d.ts +5 -0
- package/dist/settings-change.js +37 -8
- package/dist/settings-change.js.map +1 -1
- package/dist/settings-confirmation.d.ts +3 -0
- package/dist/settings-confirmation.js +7 -1
- package/dist/settings-confirmation.js.map +1 -1
- package/dist/settings-effect.js +19 -7
- package/dist/settings-effect.js.map +1 -1
- package/dist/settings-http-confirmation.js +1 -1
- package/dist/settings-http-confirmation.js.map +1 -1
- package/dist/settings-redaction.d.ts +18 -0
- package/dist/settings-redaction.js +58 -0
- package/dist/settings-redaction.js.map +1 -0
- package/dist/setup-form.d.ts +9 -1
- package/dist/setup-form.js +25 -1
- package/dist/setup-form.js.map +1 -1
- package/dist/setup-host.js +15 -6
- package/dist/setup-host.js.map +1 -1
- package/dist/setup-wizard.js +27 -19
- package/dist/setup-wizard.js.map +1 -1
- package/dist/sqlite-open-errors.d.ts +12 -0
- package/dist/sqlite-open-errors.js +13 -0
- package/dist/sqlite-open-errors.js.map +1 -0
- package/dist/systemd-runtime.d.ts +15 -0
- package/dist/systemd-runtime.js +41 -0
- package/dist/systemd-runtime.js.map +1 -0
- package/dist/tmux-control.d.ts +44 -0
- package/dist/tmux-control.js +170 -21
- package/dist/tmux-control.js.map +1 -1
- package/dist/token-env-name.d.ts +37 -0
- package/dist/token-env-name.js +93 -0
- package/dist/token-env-name.js.map +1 -0
- package/dist/tool-permissions.js +5 -3
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +7 -16
- package/dist/topic-commands.js +56 -85
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.js +3 -0
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/transcript-sources.d.ts +68 -0
- package/dist/transcript-sources.js +336 -29
- package/dist/transcript-sources.js.map +1 -1
- package/dist/transcript-turns.d.ts +39 -0
- package/dist/transcript-turns.js +58 -0
- package/dist/transcript-turns.js.map +1 -0
- package/dist/tunnel/cloudflared-install.d.ts +18 -0
- package/dist/tunnel/cloudflared-install.js +15 -1
- package/dist/tunnel/cloudflared-install.js.map +1 -1
- package/dist/turn-reply-guard.d.ts +3 -0
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/ui/chat-commands.js +43 -0
- package/dist/ui/chat-render.js +34 -8
- package/dist/ui/chat-store.js +51 -2
- package/dist/ui/chat-strings.js +30 -2
- package/dist/ui/chat-thread.js +141 -47
- package/dist/ui/first-run.js +64 -0
- package/dist/ui/fleet-cache.js +168 -0
- package/dist/ui/html-attachment.js +26 -0
- package/dist/ui/panel-chat.js +117 -23
- package/dist/ui/panel-fleet.js +31 -17
- package/dist/ui/panel-settings.js +29 -15
- package/dist/ui/preview-panel.js +237 -0
- package/dist/ui/preview.js +7 -3
- package/dist/ui/settings-apply.js +3 -1
- package/dist/ui/settings-dialogs.js +95 -25
- package/dist/ui/settings-discord.js +130 -0
- package/dist/ui/settings-model.js +21 -0
- package/dist/ui/settings-strings.js +50 -8
- package/dist/ui/settings-token.js +54 -0
- package/dist/ui/settings-wizard.js +63 -29
- package/dist/ui/shared/app-route.js +1 -1
- package/dist/ui/shared/app-shell.js +25 -37
- package/dist/ui/shared/app-store.js +5 -3
- package/dist/ui/shared/app-stream.js +2 -1
- package/dist/ui/shared/app.css +181 -5
- package/dist/ui/shared/app.js +2 -1
- package/dist/ui/shared/instance-nav.js +310 -0
- package/dist/ui/shared/panel-view.js +12 -190
- package/dist/ui/shared/signin.js +13 -1
- package/dist/ui/shared/ui-icons.js +2 -0
- package/dist/ui/shared/view-roster-store.js +5 -0
- package/dist/ui/shared/view-strings.js +12 -0
- package/dist/ui/signin.html +2 -1
- package/dist/update-dispatch.d.ts +28 -0
- package/dist/update-dispatch.js +73 -0
- package/dist/update-dispatch.js.map +1 -0
- package/dist/update-install.d.ts +78 -8
- package/dist/update-install.js +158 -30
- package/dist/update-install.js.map +1 -1
- package/dist/update-launch.d.ts +22 -0
- package/dist/update-launch.js +106 -0
- package/dist/update-launch.js.map +1 -0
- package/dist/update-marker.js +3 -8
- package/dist/update-marker.js.map +1 -1
- package/dist/usage/format-rich.js +7 -1
- package/dist/usage/format-rich.js.map +1 -1
- package/dist/usage/i18n-keys.d.ts +1 -1
- package/dist/usage/i18n-keys.js +1 -1
- package/dist/usage/i18n-keys.js.map +1 -1
- package/dist/usage/providers.d.ts +4 -0
- package/dist/usage/providers.js +2 -1
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/usage-api.js +48 -2
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/view-api.d.ts +40 -0
- package/dist/view-api.js +28 -10
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +22 -0
- package/dist/web-api.js +115 -16
- package/dist/web-api.js.map +1 -1
- package/dist/web-auth.d.ts +8 -0
- package/dist/web-auth.js +34 -8
- package/dist/web-auth.js.map +1 -1
- package/dist/web-chat-history.d.ts +21 -0
- package/dist/web-chat-history.js +44 -0
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +77 -0
- package/dist/web-commands.js +81 -0
- package/dist/web-commands.js.map +1 -0
- package/dist/web-host-guard.d.ts +18 -2
- package/dist/web-host-guard.js +30 -5
- package/dist/web-host-guard.js.map +1 -1
- package/dist/web-session.d.ts +7 -1
- package/dist/web-session.js +23 -2
- package/dist/web-session.js.map +1 -1
- package/dist/web-shell-routes.d.ts +1 -1
- package/dist/web-shell-routes.js +1 -1
- package/dist/web-shell-routes.js.map +1 -1
- package/launcher/agend +110 -0
- package/launcher/agend-agent +110 -0
- package/launcher/agend-agent.cjs +4 -0
- package/launcher/agend.cjs +4 -0
- package/launcher/install-admission.cjs +105 -0
- package/launcher/launch.cjs +64 -0
- package/launcher/postinstall.cjs +118 -0
- package/launcher/run-in-process.cjs +13 -0
- package/launcher/runtime-platform.cjs +86 -0
- package/launcher/runtime-select.cjs +217 -0
- package/package.json +12 -4
- package/scripts/preinstall-guard.cjs +40 -21
- package/templates/launchd.plist.ejs +2 -2
- package/templates/systemd.service.ejs +4 -4
package/dist/daemon.js
CHANGED
|
@@ -11,6 +11,7 @@ import { clearPausedMarker, writePausedMarker } from "./pause-marker.js";
|
|
|
11
11
|
import { TmuxManager, resolveTmuxLogicalSize } from "./tmux-manager.js";
|
|
12
12
|
import { TranscriptMonitor } from "./transcript-monitor.js";
|
|
13
13
|
import { createTranscriptSource } from "./transcript-sources.js";
|
|
14
|
+
import { TranscriptTurnLedger } from "./transcript-turns.js";
|
|
14
15
|
import { credentialProfileStoreHome, resolveCredentialProfile } from "./backend/credential-profile.js";
|
|
15
16
|
import { resolveToolSet } from "./tool-permissions.js";
|
|
16
17
|
import { getAgendHome } from "./paths.js";
|
|
@@ -25,6 +26,7 @@ import { MessageBus } from "./channel/message-bus.js";
|
|
|
25
26
|
import { shellQuote, UnsupportedCliError } from "./backend/types.js";
|
|
26
27
|
import { getTmuxSession } from "./config.js";
|
|
27
28
|
import { routeToolCall } from "./channel/tool-router.js";
|
|
29
|
+
import { parseReplyButtons, replyButtonsFallbackText } from "./reply-buttons.js";
|
|
28
30
|
import { HangDetector } from "./hang-detector.js";
|
|
29
31
|
import { writeSecretFile } from "./secret-file.js";
|
|
30
32
|
import { PaneWriteLock } from "./pane-write-lock.js";
|
|
@@ -69,6 +71,12 @@ const REPLY_DROP_WARNING_COOLDOWN_MS = 5 * 60_000;
|
|
|
69
71
|
* backstop is delayed by at most this window, never disabled.
|
|
70
72
|
*/
|
|
71
73
|
const REPLY_GUARD_IDLE_CONFIRM_MS = 60_000;
|
|
74
|
+
/**
|
|
75
|
+
* #1510: how long an idle pane may be held because the transcript still shows the turn running. A turn whose end is
|
|
76
|
+
* never written (the CLI killed, the rollout replaced) would otherwise be re-checked for ever; past this it stands
|
|
77
|
+
* down like a turn the transcript cannot place — never a recovery.
|
|
78
|
+
*/
|
|
79
|
+
const TRANSCRIPT_RUNNING_HOLD_MAX_MS = 30 * 60_000;
|
|
72
80
|
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.";
|
|
73
81
|
/** Point a resumed CLI at its one backend-native instruction source. */
|
|
74
82
|
/** The outbox evidence for a delivery the CLI's transcript shows taken (#758; #1201 absorbed). */
|
|
@@ -644,6 +652,8 @@ const CONFIRM_BUSY_POLL_MS = 200;
|
|
|
644
652
|
* hold the pane write lock along with it.
|
|
645
653
|
*/
|
|
646
654
|
const CONFIRM_BUSY_MAX_WAIT_MS = 10_000;
|
|
655
|
+
/** How much longer a confirmation waits while the control client is blind (a drained read; its limit is 10 s). */
|
|
656
|
+
const CONFIRM_BUSY_BLIND_EXTRA_MS = 12_000;
|
|
647
657
|
/**
|
|
648
658
|
* How long a delivery waits for an in-flight spawn. Comfortably past the default
|
|
649
659
|
* 25s startup timeout plus dialog dismissal; past it we fall back to the old
|
|
@@ -1168,6 +1178,8 @@ export class Daemon extends EventEmitter {
|
|
|
1168
1178
|
/** Generation/signature fence for safety dialogs while their screen is still visible. */
|
|
1169
1179
|
autoResolvedDialogKey = null;
|
|
1170
1180
|
autoResolvedDialogGeneration = 0;
|
|
1181
|
+
/** Per-launch safety phases survive startup handoff, redraws and failed/uncertain key ACKs. */
|
|
1182
|
+
launchDialogClaims = null;
|
|
1171
1183
|
/**
|
|
1172
1184
|
* (#1103) Proof that THIS spawn's Codex went onto Luna Reserve: AgEnD saw (and
|
|
1173
1185
|
* answered) the usage-limit menu that codex shows when it switches itself to
|
|
@@ -1223,6 +1235,10 @@ export class Daemon extends EventEmitter {
|
|
|
1223
1235
|
// It also supplies the older dead-MCP/malformed-call recovery paths, so those
|
|
1224
1236
|
// paths cannot disagree about whether the turn already spoke to the channel.
|
|
1225
1237
|
turnReplyGuard = new TurnReplyGuard();
|
|
1238
|
+
/** #1510: turn boundaries from the CLI's transcript, for a backend whose guard reads its turn end there. */
|
|
1239
|
+
transcriptTurns = new TranscriptTurnLedger();
|
|
1240
|
+
/** #1510: since when (monotonic) this generation has been held as running in the transcript. */
|
|
1241
|
+
transcriptRunningSince = null;
|
|
1226
1242
|
/** Prevent a visible stale XML fragment from being recovered on later turns. */
|
|
1227
1243
|
lastMalformedToolCallSignature;
|
|
1228
1244
|
proxyReplySeq = 0;
|
|
@@ -2176,7 +2192,7 @@ export class Daemon extends EventEmitter {
|
|
|
2176
2192
|
// (statusline transcript); codex/kiro/opencode read their CLI's own
|
|
2177
2193
|
// conversation store via a pluggable source. Backends with no known
|
|
2178
2194
|
// source stay inert exactly as before.
|
|
2179
|
-
this.transcriptMonitor =
|
|
2195
|
+
this.transcriptMonitor = this.buildTranscriptMonitor(createTranscriptSource(this.config.backend ?? "claude-code", this.config.working_directory, this.credentialProfileStore()));
|
|
2180
2196
|
const transcriptMonitor = this.transcriptMonitor;
|
|
2181
2197
|
const transcriptFence = this.launchFenceEpoch;
|
|
2182
2198
|
// Baseline in the isolate before this daemon accepts new work. A
|
|
@@ -3166,8 +3182,9 @@ export class Daemon extends EventEmitter {
|
|
|
3166
3182
|
const readyPattern = this.backend.getReadyPattern();
|
|
3167
3183
|
const busyPattern = this.backend.getBusyPattern?.() ?? null;
|
|
3168
3184
|
this.errorMonitorTimer = setInterval(async () => {
|
|
3169
|
-
if (!this.tmux || this.spawning)
|
|
3185
|
+
if (!this.tmux || this.spawning || this.deliveryWritesStopping)
|
|
3170
3186
|
return;
|
|
3187
|
+
const pollTmux = this.tmux;
|
|
3171
3188
|
// This poll belongs to the spawn and the monitors it started under. Every await below can outlive them (a stop,
|
|
3172
3189
|
// a pause, a respawn); a stale poll must not touch state a newer one owns, so it checks before committing.
|
|
3173
3190
|
const pollSpawn = this.spawnGeneration;
|
|
@@ -3176,18 +3193,19 @@ export class Daemon extends EventEmitter {
|
|
|
3176
3193
|
const captureOrder = ++this.interactionCaptureSerial;
|
|
3177
3194
|
const captureMono = performance.now();
|
|
3178
3195
|
const stale = () => pollSpawn !== this.spawnGeneration || pollFence !== this.launchFenceEpoch
|
|
3179
|
-
|| !sameInteractionOwner(pollOwner, this.interactionOwner()) || this.runtimeMonitorsFrozen || this.spawning
|
|
3196
|
+
|| !sameInteractionOwner(pollOwner, this.interactionOwner()) || this.runtimeMonitorsFrozen || this.spawning
|
|
3197
|
+
|| this.deliveryWritesStopping || this.tmux !== pollTmux;
|
|
3180
3198
|
let scanStartedAt = null;
|
|
3181
3199
|
const endScan = () => { if (scanStartedAt !== null)
|
|
3182
3200
|
noteSyncWork("daemon.errorMonitorScan", scanStartedAt); scanStartedAt = null; };
|
|
3183
3201
|
try {
|
|
3184
|
-
const alive = await
|
|
3202
|
+
const alive = await pollTmux.isWindowAlive();
|
|
3185
3203
|
if (stale())
|
|
3186
3204
|
return;
|
|
3187
3205
|
if (!alive)
|
|
3188
3206
|
return;
|
|
3189
3207
|
const captureAt = Date.now();
|
|
3190
|
-
const pane = await
|
|
3208
|
+
const pane = await pollTmux.capturePane();
|
|
3191
3209
|
// The scan of this pane is synchronous until it answers a dialog (an await) or ends: attributed (#1235).
|
|
3192
3210
|
scanStartedAt = performance.now();
|
|
3193
3211
|
if (stale())
|
|
@@ -3264,9 +3282,7 @@ export class Daemon extends EventEmitter {
|
|
|
3264
3282
|
continue;
|
|
3265
3283
|
}
|
|
3266
3284
|
const autoKey = dialog.autoResolutionKey;
|
|
3267
|
-
if (dialog.verifyAfterKeys && autoKey
|
|
3268
|
-
&& this.autoResolvedDialogGeneration === this.spawnGeneration
|
|
3269
|
-
&& this.autoResolvedDialogKey === autoKey) {
|
|
3285
|
+
if (dialog.verifyAfterKeys && autoKey && this.hasAutoDialogClaim(dialog)) {
|
|
3270
3286
|
// A previous poll sent the safety choice but the CLI has not
|
|
3271
3287
|
// repainted yet. Never send another Enter into the same screen.
|
|
3272
3288
|
continue;
|
|
@@ -3285,7 +3301,7 @@ export class Daemon extends EventEmitter {
|
|
|
3285
3301
|
const currentAt = Date.now();
|
|
3286
3302
|
const currentMono = performance.now();
|
|
3287
3303
|
const currentOrder = ++this.interactionCaptureSerial;
|
|
3288
|
-
const currentPane = await
|
|
3304
|
+
const currentPane = await pollTmux.capturePane();
|
|
3289
3305
|
// Every await below can outlive the spawn / monitors this poll started under: after each one, before
|
|
3290
3306
|
// any state is touched or any further key is sent, `stale()` (fixed at the poll's start) is asked again.
|
|
3291
3307
|
if (stale())
|
|
@@ -3309,28 +3325,26 @@ export class Daemon extends EventEmitter {
|
|
|
3309
3325
|
// not evidence that the old request was answered, and the one-minute fallback must keep running.
|
|
3310
3326
|
if (this.dialogAnswers?.key === Daemon.answerKey(dialog) && this.dialogAnswers.screen !== before)
|
|
3311
3327
|
this.endDialogEpisode(dialog.requestIdentity !== undefined);
|
|
3312
|
-
if (dialog.verifyAfterKeys && autoKey
|
|
3313
|
-
&& this.autoResolvedDialogGeneration === this.spawnGeneration
|
|
3314
|
-
&& this.autoResolvedDialogKey === autoKey)
|
|
3328
|
+
if (dialog.verifyAfterKeys && autoKey && this.hasAutoDialogClaim(dialog))
|
|
3315
3329
|
return;
|
|
3316
|
-
if (dialog.verifyAfterKeys && autoKey)
|
|
3317
|
-
this.
|
|
3318
|
-
this.autoResolvedDialogKey = autoKey;
|
|
3319
|
-
}
|
|
3330
|
+
if (dialog.verifyAfterKeys && autoKey)
|
|
3331
|
+
this.claimAutoDialog(dialog);
|
|
3320
3332
|
this.logger.info(`Auto-dismissing runtime dialog: ${dialog.description}`);
|
|
3321
3333
|
const SPECIAL_KEYS = new Set(["Up", "Down", "Enter", "Escape", "Right", "Left"]);
|
|
3322
3334
|
for (const key of dialog.keys) {
|
|
3335
|
+
if (stale())
|
|
3336
|
+
return;
|
|
3323
3337
|
let sent = false;
|
|
3324
3338
|
if (SPECIAL_KEYS.has(key)) {
|
|
3325
|
-
sent = await
|
|
3339
|
+
sent = await pollTmux.sendSpecialKey(key);
|
|
3326
3340
|
}
|
|
3327
3341
|
else {
|
|
3328
|
-
sent = await
|
|
3342
|
+
sent = await pollTmux.pasteText(key, this.systemPasteOptions());
|
|
3329
3343
|
}
|
|
3330
3344
|
if (stale())
|
|
3331
3345
|
return;
|
|
3332
3346
|
if (!sent) {
|
|
3333
|
-
if (dialog.verifyAfterKeys && autoKey) {
|
|
3347
|
+
if (dialog.verifyAfterKeys && autoKey && !dialog.oncePerLaunch) {
|
|
3334
3348
|
this.autoResolvedDialogGeneration = 0;
|
|
3335
3349
|
this.autoResolvedDialogKey = null;
|
|
3336
3350
|
}
|
|
@@ -3345,7 +3359,7 @@ export class Daemon extends EventEmitter {
|
|
|
3345
3359
|
const afterKeysAt = Date.now();
|
|
3346
3360
|
const afterKeysMono = performance.now();
|
|
3347
3361
|
const afterKeysOrder = ++this.interactionCaptureSerial;
|
|
3348
|
-
const afterKeysPane = await
|
|
3362
|
+
const afterKeysPane = await pollTmux.capturePane();
|
|
3349
3363
|
if (stale())
|
|
3350
3364
|
return;
|
|
3351
3365
|
const dialogStillActive = dialog.inputBlocked
|
|
@@ -3375,7 +3389,7 @@ export class Daemon extends EventEmitter {
|
|
|
3375
3389
|
// backend-named identity any difference on the screen (a ticking timer) proves nothing either way: nothing
|
|
3376
3390
|
// is reported and the parked clock is left alone. Two requests that look exactly alike cannot be told apart.
|
|
3377
3391
|
await new Promise(r => setTimeout(r, DIALOG_ANSWER_SETTLE_MS));
|
|
3378
|
-
const afterPane = await
|
|
3392
|
+
const afterPane = await pollTmux.capturePane();
|
|
3379
3393
|
if (stale())
|
|
3380
3394
|
return; // (nothing was touched since the last check: only a read)
|
|
3381
3395
|
const stillThere = Daemon.dialogMatches(dialog, afterPane);
|
|
@@ -4113,6 +4127,23 @@ export class Daemon extends EventEmitter {
|
|
|
4113
4127
|
this.logger.debug({ err: e }, "Failed to remove PID file");
|
|
4114
4128
|
}
|
|
4115
4129
|
}
|
|
4130
|
+
/** Context refresh uses the already-bound manager; no name-based child or backend probe. */
|
|
4131
|
+
getPaneContextSource() {
|
|
4132
|
+
const tmux = this.tmux;
|
|
4133
|
+
const control = this.controlClient;
|
|
4134
|
+
if (!tmux || !control || this.deliveryWritesStopping)
|
|
4135
|
+
return null;
|
|
4136
|
+
const owner = this.interactionOwner();
|
|
4137
|
+
const epoch = this.deliveryEpoch;
|
|
4138
|
+
const isCurrent = () => {
|
|
4139
|
+
const now = this.interactionOwner();
|
|
4140
|
+
return !this.deliveryWritesStopping && this.controlClient === control && this.tmux === tmux && this.deliveryEpoch === epoch
|
|
4141
|
+
&& now.bootId === owner.bootId && now.spawnGeneration === owner.spawnGeneration
|
|
4142
|
+
&& now.launchAttempt === owner.launchAttempt && now.launchFenceEpoch === owner.launchFenceEpoch;
|
|
4143
|
+
};
|
|
4144
|
+
return { owner: tmux, generation: JSON.stringify([owner.bootId, owner.spawnGeneration, owner.launchAttempt, owner.launchFenceEpoch, epoch]),
|
|
4145
|
+
isCurrent, capture: () => isCurrent() ? tmux.capturePaneWithHistory(60, 2_000) : Promise.reject(new Error("Pane context owner retired")) };
|
|
4146
|
+
}
|
|
4116
4147
|
getHangDetector() {
|
|
4117
4148
|
return this.hangDetector;
|
|
4118
4149
|
}
|
|
@@ -4651,11 +4682,14 @@ export class Daemon extends EventEmitter {
|
|
|
4651
4682
|
// chat_id) does not update lastChatId, so its proxy reply would land in
|
|
4652
4683
|
// whatever USER topic spoke to this instance last — the wrong audience for
|
|
4653
4684
|
// a task result, and a stale one (sol's review of #515).
|
|
4654
|
-
|
|
4685
|
+
// A web message on a fleet with no chat platform has no chat_id (its reply goes to the web chat through the fleet's
|
|
4686
|
+
// web-only sink), but it is a person waiting for an answer all the same: the guard arms for it (alpha.2).
|
|
4687
|
+
if (meta.from_instance || (!meta.chat_id && meta.source !== "web"))
|
|
4655
4688
|
return;
|
|
4656
4689
|
// The last non-empty line of what we pasted: everything on screen after it
|
|
4657
4690
|
// is the agent's own output.
|
|
4658
4691
|
const inboundMarker = deliveredText.split(/\r?\n/).map(l => l.trim()).filter(Boolean).pop();
|
|
4692
|
+
const deliveredAt = Date.now();
|
|
4659
4693
|
// #1241 P2-1: carry pre-arm work evidence over the arm, bounded by this
|
|
4660
4694
|
// delivery's ingress — output the paste produced before it confirmed.
|
|
4661
4695
|
this.turnReplyGuard.arm({
|
|
@@ -4665,6 +4699,8 @@ export class Daemon extends EventEmitter {
|
|
|
4665
4699
|
messageId: meta.message_id || undefined,
|
|
4666
4700
|
correlationId: meta.correlation_id || undefined,
|
|
4667
4701
|
inboundMarker,
|
|
4702
|
+
deliveredText,
|
|
4703
|
+
deliveredAt,
|
|
4668
4704
|
}, this.pendingWork.lastInboundTimestamp());
|
|
4669
4705
|
// #1209: persist the in-flight turn with a seam checkpoint. A marker
|
|
4670
4706
|
// that survives a restart is an interrupted turn by construction —
|
|
@@ -4953,6 +4989,34 @@ export class Daemon extends EventEmitter {
|
|
|
4953
4989
|
this.clearReplyGuardConfirm();
|
|
4954
4990
|
return;
|
|
4955
4991
|
}
|
|
4992
|
+
// #1510: a backend whose turn end is read from its own transcript (codex) must show it there too: the turn that
|
|
4993
|
+
// took this delivery ended and nothing started since. Anything less never starts a recovery.
|
|
4994
|
+
if (this.backend?.turnEndFromTranscript === true) {
|
|
4995
|
+
let verdict = this.transcriptTurns.verdict(turn.target.deliveredText, turn.target.deliveredAt ?? 0);
|
|
4996
|
+
if (verdict === "running") {
|
|
4997
|
+
if (this.transcriptRunningSince?.generation !== turn.generation) {
|
|
4998
|
+
this.transcriptRunningSince = { generation: turn.generation, at: performance.now() };
|
|
4999
|
+
}
|
|
5000
|
+
if (performance.now() - this.transcriptRunningSince.at >= TRANSCRIPT_RUNNING_HOLD_MAX_MS)
|
|
5001
|
+
verdict = "unknown";
|
|
5002
|
+
}
|
|
5003
|
+
if (verdict !== "running")
|
|
5004
|
+
this.transcriptRunningSince = null;
|
|
5005
|
+
if (verdict === "running") {
|
|
5006
|
+
// Idle on screen, still working in the transcript: look again after another window.
|
|
5007
|
+
this.armReplyGuardConfirm(turn.generation);
|
|
5008
|
+
this.logger.info({ correlationId: turn.target.correlationId, generation: turn.generation, reason: "transcript_turn_running" }, "Reply guard holding — the transcript shows the turn still running");
|
|
5009
|
+
return;
|
|
5010
|
+
}
|
|
5011
|
+
if (verdict !== "ended") {
|
|
5012
|
+
this.clearReplyGuardConfirm();
|
|
5013
|
+
this.turnReplyGuard.complete(turn.generation);
|
|
5014
|
+
this.logger.info({ correlationId: turn.target.correlationId, generation: turn.generation, reason: `transcript_${verdict}` }, verdict === "unknown" ? "Reply guard stood down — this delivery's turn is not in the transcript followed (fail-safe)"
|
|
5015
|
+
: verdict === "aborted" ? "Reply guard stood down — the turn was interrupted"
|
|
5016
|
+
: "Reply guard stood down — the turn ended on a provider error (its own notice covers it)");
|
|
5017
|
+
return;
|
|
5018
|
+
}
|
|
5019
|
+
}
|
|
4956
5020
|
this.clearReplyGuardConfirm();
|
|
4957
5021
|
if (turn.replyAttempted) {
|
|
4958
5022
|
// A reply was attempted but its deliver result is unknown (in-flight
|
|
@@ -4970,6 +5034,16 @@ export class Daemon extends EventEmitter {
|
|
|
4970
5034
|
|| this.config.backend === "claude-code"
|
|
4971
5035
|
|| this.backend?.binaryName === "claude";
|
|
4972
5036
|
}
|
|
5037
|
+
/**
|
|
5038
|
+
* This launch's transcript monitor. #1510: its turn boundaries feed the reply guard's ledger, which starts empty
|
|
5039
|
+
* with it — a new monitor re-baselines at the transcript's end, so nothing the old one read can be attributed.
|
|
5040
|
+
*/
|
|
5041
|
+
buildTranscriptMonitor(source) {
|
|
5042
|
+
const monitor = new TranscriptMonitor(this.instanceDir, this.logger, source);
|
|
5043
|
+
this.transcriptTurns.reset();
|
|
5044
|
+
monitor.on("turns", (turns) => this.transcriptTurns.observe(turns));
|
|
5045
|
+
return monitor;
|
|
5046
|
+
}
|
|
4973
5047
|
replyCompletionGuardEnabled() {
|
|
4974
5048
|
return this.config.reply_completion_guard !== false
|
|
4975
5049
|
&& this.backend?.replyCompletionGuard === true;
|
|
@@ -6727,6 +6801,32 @@ export class Daemon extends EventEmitter {
|
|
|
6727
6801
|
}
|
|
6728
6802
|
return active;
|
|
6729
6803
|
}
|
|
6804
|
+
hasAutoDialogClaim(dialog) {
|
|
6805
|
+
const key = dialog.autoResolutionKey;
|
|
6806
|
+
if (!key)
|
|
6807
|
+
return false;
|
|
6808
|
+
if (dialog.oncePerLaunch) {
|
|
6809
|
+
const claims = this.launchDialogClaims;
|
|
6810
|
+
return claims !== null && claims.spawn === this.spawnGeneration && claims.attempt === this.launchAttempt
|
|
6811
|
+
&& claims.keys.has(key);
|
|
6812
|
+
}
|
|
6813
|
+
return this.autoResolvedDialogGeneration === this.spawnGeneration && this.autoResolvedDialogKey === key;
|
|
6814
|
+
}
|
|
6815
|
+
claimAutoDialog(dialog) {
|
|
6816
|
+
const key = dialog.autoResolutionKey;
|
|
6817
|
+
if (!key)
|
|
6818
|
+
return;
|
|
6819
|
+
if (dialog.oncePerLaunch) {
|
|
6820
|
+
if (!this.launchDialogClaims || this.launchDialogClaims.spawn !== this.spawnGeneration
|
|
6821
|
+
|| this.launchDialogClaims.attempt !== this.launchAttempt) {
|
|
6822
|
+
this.launchDialogClaims = { spawn: this.spawnGeneration, attempt: this.launchAttempt, keys: new Set() };
|
|
6823
|
+
}
|
|
6824
|
+
this.launchDialogClaims.keys.add(key);
|
|
6825
|
+
return;
|
|
6826
|
+
}
|
|
6827
|
+
this.autoResolvedDialogGeneration = this.spawnGeneration;
|
|
6828
|
+
this.autoResolvedDialogKey = key;
|
|
6829
|
+
}
|
|
6730
6830
|
/** Whether a runtime prompt currently owns the pane's stdin. */
|
|
6731
6831
|
isInputBlocked() {
|
|
6732
6832
|
return this.inputBlockedDialogKey !== null;
|
|
@@ -6788,8 +6888,7 @@ export class Daemon extends EventEmitter {
|
|
|
6788
6888
|
// this spawn is not what the screen is waiting on any more — the entry
|
|
6789
6889
|
// after it (typically a hold) describes it.
|
|
6790
6890
|
if (dialog.verifyAfterKeys && dialog.autoResolutionKey
|
|
6791
|
-
&& this.
|
|
6792
|
-
&& this.autoResolvedDialogKey === dialog.autoResolutionKey
|
|
6891
|
+
&& this.hasAutoDialogClaim(dialog)
|
|
6793
6892
|
&& dialogs.some(other => other !== dialog && Daemon.dialogMatches(other, pane)))
|
|
6794
6893
|
continue;
|
|
6795
6894
|
this.trackDialogParked(dialog);
|
|
@@ -7766,6 +7865,10 @@ export class Daemon extends EventEmitter {
|
|
|
7766
7865
|
async sendDeliveryEnter(phase, stillCurrent, durableAttempt) {
|
|
7767
7866
|
if (!(await this.waitForInputTransientToClear(phase)))
|
|
7768
7867
|
return false;
|
|
7868
|
+
return this.sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt);
|
|
7869
|
+
}
|
|
7870
|
+
/** Write half only: callers must finish the availability wait before any final recovery ownership proof. */
|
|
7871
|
+
async sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt) {
|
|
7769
7872
|
// A recovery Enter may have waited for a transient while cancel or spawn
|
|
7770
7873
|
// replaced the delivery. Check at the last point before the tmux write.
|
|
7771
7874
|
if (this.deliveryWritesStopping || (stillCurrent && !stillCurrent()))
|
|
@@ -8246,6 +8349,10 @@ export class Daemon extends EventEmitter {
|
|
|
8246
8349
|
// is disqualifying and no amount of output may override it. Where it
|
|
8247
8350
|
// cannot (a backend with no prompt pattern), the output signal remains
|
|
8248
8351
|
// exactly as before — this must not regress backends we cannot read.
|
|
8352
|
+
// The attempt's owner (#1514 review): a confirmation fenced because the window was replaced must not be revived
|
|
8353
|
+
// by the late proof below on the replacement's pane.
|
|
8354
|
+
const ownerTmux = this.tmux, ownerGeneration = this.spawnGeneration;
|
|
8355
|
+
const owned = () => this.tmux === ownerTmux && this.spawnGeneration === ownerGeneration;
|
|
8249
8356
|
const becameBusy = await this.confirmAfterEnter(windowId, enterAt, signature, pasteBaseline, "idle-to-busy-retry", !rawPaste);
|
|
8250
8357
|
if (becameBusy) {
|
|
8251
8358
|
if (status)
|
|
@@ -8257,15 +8364,29 @@ export class Daemon extends EventEmitter {
|
|
|
8257
8364
|
if (!rawPaste && !steer && !this.canProveSubmission())
|
|
8258
8365
|
verdict.proof = "output-edge";
|
|
8259
8366
|
}
|
|
8367
|
+
else if (!owned()) {
|
|
8368
|
+
// The window this delivery was pasted into was replaced while it was being confirmed: whether its CLI took
|
|
8369
|
+
// the message is unknown, and the new window's pane says nothing about it. Uncertain, never ✅ or ❌.
|
|
8370
|
+
this.logger.warn({ phase: "post-submit-proof" }, "Window replaced during submission proof — delivery outcome uncertain");
|
|
8371
|
+
verdict.phase = "post-submit-proof";
|
|
8372
|
+
verdict.proof = "window-replaced";
|
|
8373
|
+
return false;
|
|
8374
|
+
}
|
|
8260
8375
|
else {
|
|
8261
8376
|
const proof = this.structuredInputEvidence()
|
|
8262
|
-
? await this.lateCodexSubmissionProof(signature, pasteBaseline)
|
|
8263
|
-
: await this.confirmSubmitted(signature, pasteBaseline);
|
|
8264
|
-
if (proof === "submitted") {
|
|
8377
|
+
? await this.lateCodexSubmissionProof(signature, pasteBaseline, false, owned)
|
|
8378
|
+
: await this.confirmSubmitted(signature, pasteBaseline, owned);
|
|
8379
|
+
if (proof === "submitted" && owned()) {
|
|
8265
8380
|
if (status)
|
|
8266
8381
|
this.emit("message_confirmed", status);
|
|
8267
8382
|
return true;
|
|
8268
8383
|
}
|
|
8384
|
+
if (!owned()) {
|
|
8385
|
+
this.logger.warn({ phase: "post-submit-proof" }, "Window replaced during submission proof — delivery outcome uncertain");
|
|
8386
|
+
verdict.phase = "post-submit-proof";
|
|
8387
|
+
verdict.proof = "window-replaced";
|
|
8388
|
+
return false;
|
|
8389
|
+
}
|
|
8269
8390
|
if (this.structuredInputEvidence() && proof !== "stranded") {
|
|
8270
8391
|
// This is the observed #910 race: the CLI processed the message
|
|
8271
8392
|
// although its echo had not appeared within the proof window. A
|
|
@@ -8368,16 +8489,25 @@ export class Daemon extends EventEmitter {
|
|
|
8368
8489
|
* still confirmed on output alone).
|
|
8369
8490
|
*/
|
|
8370
8491
|
async confirmAfterEnter(windowId, enterAt, signature, baseline, retryPhase, allowRecoveryEnter = true) {
|
|
8492
|
+
// The window this confirmation is about (#1490). The waits below can span a recoverWindow, which replaces
|
|
8493
|
+
// this.tmux with a new TmuxManager, so its identity (with the spawn generation) is the fence. A recovery Enter or
|
|
8494
|
+
// a proof taken after that would be about another window: the fence goes down to the write
|
|
8495
|
+
// (sendDeliveryEnterAfterAvailability asks it last, right before the key) and into every proof, which asks it
|
|
8496
|
+
// before its capture and again before judging what it read (#1514 review) — so a replacement during a capture
|
|
8497
|
+
// can neither confirm this delivery nor run a proof's side effects.
|
|
8498
|
+
const generation = this.spawnGeneration, tmux = this.tmux;
|
|
8499
|
+
const current = () => this.spawnGeneration === generation && this.tmux === tmux;
|
|
8500
|
+
const proofNow = () => this.confirmSubmitted(signature, baseline, current);
|
|
8371
8501
|
if (!this.canProveSubmission()) {
|
|
8372
|
-
let busy = await this.confirmBusyAfterEnter(windowId, enterAt);
|
|
8502
|
+
let busy = (await this.confirmBusyAfterEnter(windowId, enterAt)) && current();
|
|
8373
8503
|
if (!busy) {
|
|
8374
|
-
if (!allowRecoveryEnter)
|
|
8504
|
+
if (!allowRecoveryEnter || !current())
|
|
8375
8505
|
return false;
|
|
8376
8506
|
this.logger.warn("No idle→busy transition after Enter — re-sending Enter once");
|
|
8377
8507
|
const retryAt = Date.now();
|
|
8378
|
-
if (!(await this.sendDeliveryEnter(retryPhase)))
|
|
8508
|
+
if (!(await this.sendDeliveryEnter(retryPhase, current)))
|
|
8379
8509
|
return false;
|
|
8380
|
-
busy = await this.confirmBusyAfterEnter(windowId, retryAt);
|
|
8510
|
+
busy = (await this.confirmBusyAfterEnter(windowId, retryAt)) && current();
|
|
8381
8511
|
}
|
|
8382
8512
|
return busy;
|
|
8383
8513
|
}
|
|
@@ -8393,7 +8523,9 @@ export class Daemon extends EventEmitter {
|
|
|
8393
8523
|
// proves a swallowed Enter only once it outlasts the window; an early one is polled through. Codex keeps its rule.
|
|
8394
8524
|
const strandMustPersist = !!this.backend?.readInputRow;
|
|
8395
8525
|
for (;;) {
|
|
8396
|
-
proof = await
|
|
8526
|
+
proof = await proofNow();
|
|
8527
|
+
if (!current())
|
|
8528
|
+
return false;
|
|
8397
8529
|
if (proof === "submitted")
|
|
8398
8530
|
return true;
|
|
8399
8531
|
if ((proof === "stranded" && !strandMustPersist) || Date.now() >= firstDeadline)
|
|
@@ -8402,18 +8534,22 @@ export class Daemon extends EventEmitter {
|
|
|
8402
8534
|
}
|
|
8403
8535
|
if (proof !== "stranded" || !allowRecoveryEnter)
|
|
8404
8536
|
return false;
|
|
8405
|
-
if (!(await this.waitForPaneReadyForDelivery(windowId, STRANDED_RETRY_READY_WAIT_MS)))
|
|
8537
|
+
if (!(await this.waitForPaneReadyForDelivery(windowId, STRANDED_RETRY_READY_WAIT_MS)) || !current())
|
|
8538
|
+
return false;
|
|
8539
|
+
proof = await proofNow();
|
|
8540
|
+
if (!current())
|
|
8406
8541
|
return false;
|
|
8407
|
-
proof = await this.confirmSubmitted(signature, baseline);
|
|
8408
8542
|
if (proof === "submitted")
|
|
8409
8543
|
return true;
|
|
8410
8544
|
if (proof !== "stranded")
|
|
8411
8545
|
return false;
|
|
8412
|
-
if (!(await this.sendDeliveryEnter(retryPhase)))
|
|
8546
|
+
if (!(await this.sendDeliveryEnter(retryPhase, current)))
|
|
8413
8547
|
return false;
|
|
8414
8548
|
const retryDeadline = Date.now() + POST_ENTER_PROOF_WINDOW_MS;
|
|
8415
8549
|
for (;;) {
|
|
8416
|
-
proof = await
|
|
8550
|
+
proof = await proofNow();
|
|
8551
|
+
if (!current())
|
|
8552
|
+
return false;
|
|
8417
8553
|
if (proof === "submitted")
|
|
8418
8554
|
return true;
|
|
8419
8555
|
if (Date.now() >= retryDeadline)
|
|
@@ -8421,8 +8557,10 @@ export class Daemon extends EventEmitter {
|
|
|
8421
8557
|
await new Promise(r => setTimeout(r, POST_ENTER_PROOF_POLL_MS));
|
|
8422
8558
|
}
|
|
8423
8559
|
}
|
|
8424
|
-
if (await
|
|
8560
|
+
if (await proofNow() === "submitted" && current())
|
|
8425
8561
|
return true;
|
|
8562
|
+
if (!current())
|
|
8563
|
+
return false;
|
|
8426
8564
|
// Retry once the prompt is back — an Enter sent into a mid-redraw TUI is
|
|
8427
8565
|
// how we got here. Then poll briefly rather than judging on one capture:
|
|
8428
8566
|
// the key takes a moment to be digested, and a single early look would
|
|
@@ -8432,25 +8570,32 @@ export class Daemon extends EventEmitter {
|
|
|
8432
8570
|
this.logger.warn("Enter did not submit — waiting for the prompt, then re-sending once");
|
|
8433
8571
|
if (!(await this.waitForPaneReadyForDelivery(windowId, STRANDED_RETRY_READY_WAIT_MS)))
|
|
8434
8572
|
return false;
|
|
8435
|
-
if (!(await this.sendDeliveryEnter(retryPhase)))
|
|
8573
|
+
if (!(await this.sendDeliveryEnter(retryPhase, current)))
|
|
8436
8574
|
return false;
|
|
8437
8575
|
const deadline = Date.now() + POST_ENTER_PROOF_WINDOW_MS;
|
|
8438
8576
|
for (;;) {
|
|
8439
|
-
|
|
8577
|
+
const proof = await proofNow();
|
|
8578
|
+
if (!current())
|
|
8579
|
+
return false;
|
|
8580
|
+
if (proof === "submitted")
|
|
8440
8581
|
return true;
|
|
8441
8582
|
if (Date.now() >= deadline)
|
|
8442
8583
|
return false;
|
|
8443
8584
|
await new Promise(r => setTimeout(r, POST_ENTER_PROOF_POLL_MS));
|
|
8444
8585
|
}
|
|
8445
8586
|
}
|
|
8446
|
-
async confirmSubmitted(signature, baseline) {
|
|
8447
|
-
|
|
8587
|
+
async confirmSubmitted(signature, baseline, owned) {
|
|
8588
|
+
// `owned`: the attempt's window fence (#1514 review), asked before the capture and again before judging it —
|
|
8589
|
+
// judging can retire the spawn's input guard, so a capture of a replaced window must not even be judged.
|
|
8590
|
+
if (!this.tmux || (owned && !owned()))
|
|
8448
8591
|
return "unproven";
|
|
8449
8592
|
let pane = null;
|
|
8450
8593
|
try {
|
|
8451
8594
|
pane = await this.tmux.capturePane();
|
|
8452
8595
|
}
|
|
8453
8596
|
catch { /* the transcript may still say */ }
|
|
8597
|
+
if (owned && !owned())
|
|
8598
|
+
return "unproven";
|
|
8454
8599
|
const onPane = pane === null ? "unproven" : this.judgeSubmission(pane, signature, baseline);
|
|
8455
8600
|
if (onPane === "submitted")
|
|
8456
8601
|
return onPane;
|
|
@@ -8458,7 +8603,7 @@ export class Daemon extends EventEmitter {
|
|
|
8458
8603
|
const found = await this.transcriptLook(signature);
|
|
8459
8604
|
// One synchronous stretch from here: the fence is asked after the last await, and only then is the hit kept and the
|
|
8460
8605
|
// spawn's guard retired — a stop or respawn that lands between the look and this line leaves both untouched.
|
|
8461
|
-
if (!found || !t?.current())
|
|
8606
|
+
if (!found || !t?.current() || (owned && !owned()))
|
|
8462
8607
|
return onPane;
|
|
8463
8608
|
if (!t.provenBy) {
|
|
8464
8609
|
t.provenBy = found;
|
|
@@ -8547,12 +8692,12 @@ export class Daemon extends EventEmitter {
|
|
|
8547
8692
|
// read as ours, we would press Enter to "recover" it, and the turn IT
|
|
8548
8693
|
// starts would confirm a message that never reached the pane.
|
|
8549
8694
|
//
|
|
8550
|
-
//
|
|
8551
|
-
// unreadable
|
|
8552
|
-
//
|
|
8553
|
-
//
|
|
8695
|
+
// Structured evidence (Claude's box and Codex's current input/footer pair) requires both snapshots to be READ.
|
|
8696
|
+
// An unreadable baseline says nothing about what the box held; treating it as empty would claim an older draft
|
|
8697
|
+
// as ours and press Enter on it (#1353/#1359). Codex's native warning viewer can ignore the paste and first Enter,
|
|
8698
|
+
// then reveal an older draft when the user dismisses it. Row-only backends retain their existing attribution.
|
|
8554
8699
|
const attributable = baseline != null
|
|
8555
|
-
&& (!this.
|
|
8700
|
+
&& (!this.structuredInputEvidence() || (baseline.inputReadable && after.inputReadable));
|
|
8556
8701
|
if (after.strandedInput && (signature.unique || (attributable && baseline.strandedInput === false)))
|
|
8557
8702
|
return "stranded";
|
|
8558
8703
|
// A paste the CLI shows collapsed carries no signature at all. One the box did not hold before we pasted is
|
|
@@ -8593,11 +8738,13 @@ export class Daemon extends EventEmitter {
|
|
|
8593
8738
|
return "unverifiable";
|
|
8594
8739
|
}
|
|
8595
8740
|
/** Only positive echo/queue evidence may turn an ambiguous Codex write into ✅. */
|
|
8596
|
-
async lateCodexSubmissionProof(signature, baseline, waitThroughStranded = false) {
|
|
8741
|
+
async lateCodexSubmissionProof(signature, baseline, waitThroughStranded = false, owned) {
|
|
8597
8742
|
const deadline = Date.now() + CODEX_LATE_PROOF_MS;
|
|
8598
8743
|
let proof = "unproven";
|
|
8599
8744
|
for (;;) {
|
|
8600
|
-
|
|
8745
|
+
if (owned && !owned())
|
|
8746
|
+
return "unproven";
|
|
8747
|
+
proof = await this.confirmSubmitted(signature, baseline, owned);
|
|
8601
8748
|
// Ordinary idle-path proof may return a strand immediately. After a
|
|
8602
8749
|
// native-queue recovery Enter, though, it can be the previous frame
|
|
8603
8750
|
// still on screen; only a strand that survives the bounded repaint
|
|
@@ -8609,6 +8756,8 @@ export class Daemon extends EventEmitter {
|
|
|
8609
8756
|
// Scrollback can hold the echo the viewport lost — but it is judged exactly like the viewport: a box that
|
|
8610
8757
|
// cannot be read, or that still holds our text, is never upgraded by an echo further up.
|
|
8611
8758
|
const history = await this.tmux.capturePaneWithHistory(300);
|
|
8759
|
+
if (owned && !owned())
|
|
8760
|
+
return "unproven";
|
|
8612
8761
|
if (this.judgeSubmission(history, signature, baseline) === "submitted")
|
|
8613
8762
|
return this.submittedProof();
|
|
8614
8763
|
}
|
|
@@ -8802,12 +8951,36 @@ export class Daemon extends EventEmitter {
|
|
|
8802
8951
|
return true; // best effort, exactly as before — nothing here is verified
|
|
8803
8952
|
}
|
|
8804
8953
|
if (proof !== "submitted") {
|
|
8805
|
-
// A bare Enter is a no-op at an empty prompt, so this is safe even if the
|
|
8806
|
-
// first one did land; when the text is still in the input row it is the
|
|
8807
|
-
// submit it never got. Re-pasting would append the text to itself.
|
|
8808
8954
|
await new Promise(r => setTimeout(r, NORMAL_ENTER_SETTLE_MS));
|
|
8809
|
-
if (
|
|
8955
|
+
if (current && !current())
|
|
8810
8956
|
return false;
|
|
8957
|
+
const retryPhase = `${label}-retry`;
|
|
8958
|
+
if (this.structuredInputEvidence()) {
|
|
8959
|
+
// Both the retry delay and the Enter availability wait may outlive the
|
|
8960
|
+
// paste. Complete those waits BEFORE the last ownership/positive proof.
|
|
8961
|
+
if (!(await this.waitForInputTransientToClear(retryPhase)))
|
|
8962
|
+
return false;
|
|
8963
|
+
if (this.deliveryWritesStopping || (current && !current()))
|
|
8964
|
+
return false;
|
|
8965
|
+
proof = await this.confirmSubmitted(signature, baseline);
|
|
8966
|
+
if (this.deliveryWritesStopping || (current && !current()))
|
|
8967
|
+
return false;
|
|
8968
|
+
if (proof === "submitted")
|
|
8969
|
+
return true;
|
|
8970
|
+
if (proof !== "stranded") {
|
|
8971
|
+
this.logger.warn({ label, proof }, "System paste recovery input could not be attributed");
|
|
8972
|
+
return false;
|
|
8973
|
+
}
|
|
8974
|
+
// No further availability wait may invalidate this proof. The write
|
|
8975
|
+
// half retains the same stopping/admission fence immediately before IPC.
|
|
8976
|
+
if (!(await this.sendDeliveryEnterAfterAvailability(retryPhase, current)))
|
|
8977
|
+
return false;
|
|
8978
|
+
}
|
|
8979
|
+
else {
|
|
8980
|
+
// Row-only backends retain their ordinary availability wait and retry.
|
|
8981
|
+
if (!(await this.sendDeliveryEnter(retryPhase, current)))
|
|
8982
|
+
return false;
|
|
8983
|
+
}
|
|
8811
8984
|
proof = await this.confirmSubmitted(signature, baseline);
|
|
8812
8985
|
if (current && !current())
|
|
8813
8986
|
return false;
|
|
@@ -8871,12 +9044,18 @@ export class Daemon extends EventEmitter {
|
|
|
8871
9044
|
async confirmBusyAfterEnter(windowId, since) {
|
|
8872
9045
|
const client = this.controlClient;
|
|
8873
9046
|
const hardDeadline = Date.now() + CONFIRM_BUSY_MAX_WAIT_MS;
|
|
9047
|
+
// A drained control read hides notifications (#1517 review): while the client is blind a quiet pane is not
|
|
9048
|
+
// evidence of a swallowed Enter, so blind polls do not count toward "no reaction". Bounded: past the drain limit
|
|
9049
|
+
// the client retires and resets, which restarts this check from when it could see again.
|
|
9050
|
+
const blindDeadline = hardDeadline + CONFIRM_BUSY_BLIND_EXTRA_MS;
|
|
8874
9051
|
let observedFrom = since;
|
|
8875
9052
|
let polls = 0;
|
|
8876
9053
|
while (polls < CONFIRM_BUSY_POLLS) {
|
|
8877
9054
|
await new Promise(r => setTimeout(r, CONFIRM_BUSY_POLL_MS));
|
|
8878
9055
|
if (client.hasOutputSince(windowId, observedFrom))
|
|
8879
9056
|
return true;
|
|
9057
|
+
if (client.isObservationBlind?.() && Date.now() < blindDeadline)
|
|
9058
|
+
continue;
|
|
8880
9059
|
const resetAt = client.getObservationResetAt();
|
|
8881
9060
|
if (resetAt > observedFrom && Date.now() < hardDeadline) {
|
|
8882
9061
|
this.logger.debug({ windowId, resetAt }, "Control mode reconnected mid-confirmation — restarting the Enter check from when we could see again");
|
|
@@ -9194,7 +9373,20 @@ export class Daemon extends EventEmitter {
|
|
|
9194
9373
|
respond(null, "No channel world bound to this chat context — awaiting an inbound message to establish it");
|
|
9195
9374
|
return;
|
|
9196
9375
|
}
|
|
9197
|
-
|
|
9376
|
+
// #1266: without the fleet there is no button store: buttons are checked, then offered as text.
|
|
9377
|
+
let routed = args;
|
|
9378
|
+
if (tool === "reply") {
|
|
9379
|
+
const buttons = parseReplyButtons(args.buttons, args);
|
|
9380
|
+
if (buttons && "error" in buttons) {
|
|
9381
|
+
respond(null, `reply: ${buttons.error}`);
|
|
9382
|
+
return;
|
|
9383
|
+
}
|
|
9384
|
+
if (buttons) {
|
|
9385
|
+
routed = { ...args, text: `${String(args.text)}\n\n${replyButtonsFallbackText(buttons.buttons)}` };
|
|
9386
|
+
delete routed.buttons;
|
|
9387
|
+
}
|
|
9388
|
+
}
|
|
9389
|
+
if (!routeToolCall(adapter, tool, routed, this.lastThreadId, respond)) {
|
|
9198
9390
|
respond(null, `Unknown tool: ${tool}`);
|
|
9199
9391
|
}
|
|
9200
9392
|
}
|
|
@@ -9353,7 +9545,8 @@ export class Daemon extends EventEmitter {
|
|
|
9353
9545
|
instanceName: this.name,
|
|
9354
9546
|
mcpServers: isCliMode ? {} : {
|
|
9355
9547
|
"agend": {
|
|
9356
|
-
|
|
9548
|
+
// The Node this daemon runs on (#1450 C5): the CLIs start the MCP server with it, not a `node` from PATH.
|
|
9549
|
+
command: process.execPath,
|
|
9357
9550
|
args: [serverJs],
|
|
9358
9551
|
env: mcpEnv,
|
|
9359
9552
|
},
|
|
@@ -10290,15 +10483,16 @@ export class Daemon extends EventEmitter {
|
|
|
10290
10483
|
// this file from <instanceDir>/agent.token (mode 0o600) and sends its
|
|
10291
10484
|
// value in the X-Agend-Instance-Token header; the daemon-side /agent
|
|
10292
10485
|
// endpoint verifies it matches the on-disk value for the claimed
|
|
10293
|
-
// instance.
|
|
10294
|
-
//
|
|
10486
|
+
// instance. A global web token cannot substitute for this credential, but
|
|
10487
|
+
// same-uid shell processes can read sibling tokens. Mode 0600 separates
|
|
10488
|
+
// OS users; it does not isolate mutually untrusted agents on this account.
|
|
10295
10489
|
const agentTokenPath = join(this.instanceDir, "agent.token");
|
|
10296
10490
|
const agentToken = randomBytes(32).toString("hex");
|
|
10297
10491
|
const tokenWrite = writeSecretFile(agentTokenPath, agentToken);
|
|
10298
10492
|
if (!tokenWrite.ok) {
|
|
10299
10493
|
// Do not fail the spawn over it — an instance that cannot start is worse
|
|
10300
10494
|
// than one whose token is readable. But say so, loudly: the whole point of
|
|
10301
|
-
// this
|
|
10495
|
+
// this permission check separates other OS users, not same-uid processes.
|
|
10302
10496
|
this.logger.error({ path: agentTokenPath, mode: tokenWrite.mode?.toString(8), reason: tokenWrite.reason }, "Agent token file is not owner-only — other local users can impersonate this instance");
|
|
10303
10497
|
}
|
|
10304
10498
|
// AGEND_HOME points the child's agent-cli at the same data dir the daemon
|
|
@@ -10499,7 +10693,8 @@ export class Daemon extends EventEmitter {
|
|
|
10499
10693
|
const ownerTmux = this.tmux;
|
|
10500
10694
|
const ownerSpawn = this.spawnGeneration;
|
|
10501
10695
|
const ownerFence = this.launchFenceEpoch;
|
|
10502
|
-
const owned = () => this.
|
|
10696
|
+
const owned = () => !this.deliveryWritesStopping && this.tmux === ownerTmux
|
|
10697
|
+
&& this.spawnGeneration === ownerSpawn && this.launchFenceEpoch === ownerFence;
|
|
10503
10698
|
do {
|
|
10504
10699
|
this.startupAdmission?.();
|
|
10505
10700
|
attempts++;
|
|
@@ -10545,8 +10740,7 @@ export class Daemon extends EventEmitter {
|
|
|
10545
10740
|
if (Daemon.dialogMatches(dialog, pane)) {
|
|
10546
10741
|
if (dialog.autoResolutionKey
|
|
10547
10742
|
&& (attemptedSafetyChoices.has(dialog.autoResolutionKey)
|
|
10548
|
-
||
|
|
10549
|
-
&& this.autoResolvedDialogKey === dialog.autoResolutionKey)))
|
|
10743
|
+
|| this.hasAutoDialogClaim(dialog)))
|
|
10550
10744
|
continue;
|
|
10551
10745
|
lastDialog = dialog;
|
|
10552
10746
|
this.noteCodexReserveDialog(dialog, pane);
|
|
@@ -10610,8 +10804,7 @@ export class Daemon extends EventEmitter {
|
|
|
10610
10804
|
}
|
|
10611
10805
|
if (dialog.autoResolutionKey) {
|
|
10612
10806
|
attemptedSafetyChoices.add(dialog.autoResolutionKey);
|
|
10613
|
-
this.
|
|
10614
|
-
this.autoResolvedDialogKey = dialog.autoResolutionKey;
|
|
10807
|
+
this.claimAutoDialog(dialog);
|
|
10615
10808
|
}
|
|
10616
10809
|
for (const key of dialog.keys) {
|
|
10617
10810
|
this.startupAdmission?.();
|