@adhdev/daemon-core 0.9.82-rc.35 → 0.9.82-rc.351
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/dist/boot/daemon-lifecycle.d.ts +8 -0
- package/dist/boot/process-hardening.d.ts +50 -0
- package/dist/build-info.d.ts +37 -0
- package/dist/chat/source-machine.d.ts +166 -0
- package/dist/chat/source-resolver.d.ts +104 -0
- package/dist/chat/subscription-updates.d.ts +1 -0
- package/dist/cli-adapter-types.d.ts +46 -2
- package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +198 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +116 -64
- package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
- package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +53 -0
- package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
- package/dist/cli-adapters/resolve-executable.d.ts +14 -0
- package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
- package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
- package/dist/cli-adapters/terminal-screen.d.ts +4 -11
- package/dist/commands/cli-manager.d.ts +2 -1
- package/dist/commands/handler.d.ts +110 -0
- package/dist/commands/mesh-coordinator.d.ts +85 -1
- package/dist/commands/router.d.ts +324 -1
- package/dist/commands/upgrade-helper.d.ts +12 -0
- package/dist/config/chat-history.d.ts +9 -0
- package/dist/config/config.d.ts +5 -0
- package/dist/config/mesh-config.d.ts +77 -1
- package/dist/detection/ide-detector.d.ts +13 -0
- package/dist/detection/win32-ide-version.d.ts +37 -0
- package/dist/git/change-impact-config.d.ts +159 -0
- package/dist/git/git-commands.d.ts +11 -1
- package/dist/git/git-diff.d.ts +6 -0
- package/dist/git/git-executor.d.ts +11 -0
- package/dist/git/git-status.d.ts +23 -0
- package/dist/git/git-types.d.ts +2 -50
- package/dist/git/index.d.ts +2 -0
- package/dist/index.d.ts +53 -13
- package/dist/index.js +39817 -14303
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +41436 -16047
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/ipc/local-ipc-server.d.ts +91 -0
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/logging/log-redactor.d.ts +24 -0
- package/dist/logging/log-tail-reader.d.ts +46 -0
- package/dist/mesh/contracts.d.ts +164 -0
- package/dist/mesh/coordinator-prompt.d.ts +36 -0
- package/dist/mesh/coordinator-registry.d.ts +59 -0
- package/dist/mesh/mesh-active-work.d.ts +174 -0
- package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
- package/dist/mesh/mesh-events-coordinator.d.ts +169 -0
- package/dist/mesh/mesh-events-pending.d.ts +55 -0
- package/dist/mesh/mesh-events-stale.d.ts +40 -0
- package/dist/mesh/mesh-events-utils.d.ts +62 -0
- package/dist/mesh/mesh-events.d.ts +6 -49
- package/dist/mesh/mesh-fast-forward.d.ts +81 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-init.d.ts +86 -0
- package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
- package/dist/mesh/mesh-ledger.d.ts +77 -1
- package/dist/mesh/mesh-missions.d.ts +164 -0
- package/dist/mesh/mesh-reconcile-loop.d.ts +7 -0
- package/dist/mesh/mesh-refine-batch.d.ts +68 -0
- package/dist/mesh/mesh-refine-status.d.ts +62 -0
- package/dist/mesh/mesh-review-inbox.d.ts +90 -0
- package/dist/mesh/mesh-routing.d.ts +70 -0
- package/dist/mesh/mesh-runtime-store.d.ts +420 -0
- package/dist/mesh/mesh-task-stats.d.ts +49 -0
- package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +30 -0
- package/dist/mesh/mesh-work-queue.d.ts +218 -5
- package/dist/mesh/preview-freshness.d.ts +18 -0
- package/dist/mesh/refine-config.d.ts +216 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +135 -0
- package/dist/providers/acp-provider-instance.d.ts +2 -0
- package/dist/providers/approval-utils.d.ts +13 -0
- package/dist/providers/chat-message-normalization.d.ts +12 -0
- package/dist/providers/cli-provider-instance.d.ts +77 -3
- package/dist/providers/contracts.d.ts +139 -6
- package/dist/providers/external-sources.d.ts +71 -0
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
- package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
- package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
- package/dist/providers/native-history/dispatcher.d.ts +27 -0
- package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
- package/dist/providers/native-history/index.d.ts +13 -0
- package/dist/providers/provider-instance-manager.d.ts +14 -0
- package/dist/providers/provider-instance.d.ts +15 -1
- package/dist/providers/provider-loader.d.ts +26 -4
- package/dist/providers/provider-trust.d.ts +31 -0
- package/dist/providers/read-chat-contract.d.ts +29 -0
- package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
- package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
- package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
- package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
- package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
- package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
- package/dist/providers/sdk/v1/index.d.ts +30 -0
- package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
- package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
- package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
- package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
- package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
- package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
- package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
- package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
- package/dist/providers/spec/adapter.d.ts +69 -0
- package/dist/providers/spec/cli-adapter.d.ts +217 -0
- package/dist/providers/spec/evaluator.d.ts +23 -0
- package/dist/providers/spec/fsm-driver.d.ts +322 -0
- package/dist/providers/spec/fsm-evaluator.d.ts +72 -0
- package/dist/providers/spec/fsm-loader.d.ts +14 -0
- package/dist/providers/spec/fsm-types.d.ts +144 -0
- package/dist/providers/spec/native-history-executor.d.ts +41 -0
- package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
- package/dist/providers/spec/route.d.ts +4 -0
- package/dist/providers/spec/types.d.ts +180 -0
- package/dist/providers/status-monitor.d.ts +7 -7
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/providers/types/interactive-prompt.d.ts +84 -0
- package/dist/providers/working-dir.d.ts +17 -0
- package/dist/repo-mesh-types.d.ts +295 -13
- package/dist/sessions/registry.d.ts +6 -0
- package/dist/shared-types-extra.d.ts +2 -4
- package/dist/shared-types.d.ts +59 -2
- package/dist/status/normalize.d.ts +1 -1
- package/dist/status/normalize.js +1 -0
- package/dist/status/normalize.js.map +1 -1
- package/dist/status/normalize.mjs +1 -0
- package/dist/status/normalize.mjs.map +1 -1
- package/dist/status/reporter.d.ts +2 -0
- package/dist/status/snapshot.d.ts +26 -0
- package/dist/system/load-better-sqlite3.d.ts +21 -0
- package/dist/types.d.ts +5 -0
- package/package.json +7 -3
- package/src/agent-stream/poller.ts +2 -3
- package/src/agent-stream/provider-adapter.ts +1 -1
- package/src/boot/daemon-lifecycle.ts +55 -12
- package/src/boot/process-hardening.ts +89 -0
- package/src/build-info.ts +73 -0
- package/src/chat/source-machine.ts +534 -0
- package/src/chat/source-resolver.ts +0 -0
- package/src/chat/subscription-updates.ts +20 -1
- package/src/cli-adapter-types.d.ts +1 -0
- package/src/cli-adapter-types.ts +47 -2
- package/src/cli-adapters/cli-script-runner.ts +421 -0
- package/src/cli-adapters/cli-state-engine.ts +1215 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +774 -1141
- package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/src/cli-adapters/provider-cli-parse.ts +13 -0
- package/src/cli-adapters/provider-cli-runtime.ts +56 -7
- package/src/cli-adapters/provider-cli-shared.d.ts +4 -0
- package/src/cli-adapters/provider-cli-shared.ts +100 -11
- package/src/cli-adapters/pty-transport.ts +2 -1
- package/src/cli-adapters/raw-terminal-io.ts +252 -0
- package/src/cli-adapters/resolve-executable.ts +90 -0
- package/src/cli-adapters/session-host-transport.ts +2 -1
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +53 -29
- package/src/cli-adapters/terminal-backends/types.ts +10 -3
- package/src/cli-adapters/terminal-screen.ts +16 -81
- package/src/commands/chat-commands.ts +1823 -62
- package/src/commands/cli-manager.ts +304 -14
- package/src/commands/handler.ts +809 -2
- package/src/commands/mesh-coordinator.ts +332 -122
- package/src/commands/router.ts +7505 -1311
- package/src/commands/stream-commands.ts +8 -0
- package/src/commands/upgrade-helper.ts +81 -30
- package/src/config/chat-history.ts +483 -24
- package/src/config/config.ts +12 -0
- package/src/config/mesh-config.ts +388 -6
- package/src/config/recent-activity.ts +8 -2
- package/src/daemon/dev-auto-implement.ts +3 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/detection/cli-detector.ts +28 -9
- package/src/detection/ide-detector.ts +55 -16
- package/src/detection/win32-ide-version.ts +106 -0
- package/src/git/change-impact-config.ts +354 -0
- package/src/git/git-commands.ts +55 -14
- package/src/git/git-diff.ts +53 -0
- package/src/git/git-executor.ts +12 -0
- package/src/git/git-status.ts +574 -43
- package/src/git/git-types.ts +14 -62
- package/src/git/git-worktree.ts +8 -1
- package/src/git/index.ts +16 -0
- package/src/index.ts +151 -12
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/ipc/local-ipc-server.ts +278 -0
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/log-redactor.ts +100 -0
- package/src/logging/log-tail-reader.ts +220 -0
- package/src/logging/logger.ts +2 -1
- package/src/mesh/contracts.ts +329 -0
- package/src/mesh/coordinator-prompt.ts +234 -31
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +667 -0
- package/src/mesh/mesh-delivery-policy.ts +315 -0
- package/src/mesh/mesh-events-coordinator.ts +2303 -0
- package/src/mesh/mesh-events-pending.ts +567 -0
- package/src/mesh/mesh-events-stale.ts +307 -0
- package/src/mesh/mesh-events-utils.ts +381 -0
- package/src/mesh/mesh-events.ts +30 -1035
- package/src/mesh/mesh-fast-forward.ts +839 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-init.ts +260 -0
- package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
- package/src/mesh/mesh-ledger.ts +590 -102
- package/src/mesh/mesh-missions.ts +322 -0
- package/src/mesh/mesh-reconcile-loop.ts +989 -0
- package/src/mesh/mesh-refine-batch.ts +197 -0
- package/src/mesh/mesh-refine-status.ts +231 -0
- package/src/mesh/mesh-review-inbox.ts +307 -0
- package/src/mesh/mesh-routing.ts +291 -0
- package/src/mesh/mesh-runtime-store.ts +1664 -0
- package/src/mesh/mesh-task-stats.ts +161 -0
- package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
- package/src/mesh/mesh-work-queue.ts +901 -141
- package/src/mesh/preview-freshness.ts +118 -0
- package/src/mesh/refine-config.ts +423 -0
- package/src/mesh/worktree-bootstrap-config.ts +331 -0
- package/src/providers/acp-provider-instance.ts +25 -9
- package/src/providers/approval-utils.d.ts +4 -0
- package/src/providers/approval-utils.ts +47 -5
- package/src/providers/chat-message-normalization.ts +45 -5
- package/src/providers/cli-provider-instance.ts +1041 -99
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +150 -6
- package/src/providers/extension-provider-instance.ts +12 -7
- package/src/providers/external-sources.ts +218 -0
- package/src/providers/ide-provider-instance.ts +35 -12
- package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
- package/src/providers/native-history/claude-cli-transcript.ts +396 -0
- package/src/providers/native-history/codex-cli-transcript.ts +479 -0
- package/src/providers/native-history/dispatcher.ts +340 -0
- package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
- package/src/providers/native-history/index.ts +30 -0
- package/src/providers/provider-instance-manager.ts +30 -0
- package/src/providers/provider-instance.ts +10 -1
- package/src/providers/provider-loader.ts +670 -50
- package/src/providers/provider-schema.ts +87 -14
- package/src/providers/provider-trust.ts +114 -0
- package/src/providers/read-chat-contract.ts +76 -16
- package/src/providers/sdk/README.md +49 -0
- package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
- package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
- package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
- package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
- package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
- package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
- package/src/providers/sdk/v1/index.ts +152 -0
- package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
- package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
- package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
- package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
- package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
- package/src/providers/sdk/v1/types/cli/index.ts +365 -0
- package/src/providers/sdk/v1/types/common/index.ts +229 -0
- package/src/providers/sdk/v1/validators/index.ts +19 -0
- package/src/providers/sdk/v1/validators/manifest.ts +110 -0
- package/src/providers/sdk/v1/validators/taint.ts +309 -0
- package/src/providers/spec/adapter.ts +168 -0
- package/src/providers/spec/cli-adapter.ts +986 -0
- package/src/providers/spec/evaluator.ts +345 -0
- package/src/providers/spec/fsm-driver.ts +1065 -0
- package/src/providers/spec/fsm-evaluator.ts +255 -0
- package/src/providers/spec/fsm-loader.ts +120 -0
- package/src/providers/spec/fsm-types.ts +238 -0
- package/src/providers/spec/native-history-executor.ts +1136 -0
- package/src/providers/spec/pre-launch-trust.ts +104 -0
- package/src/providers/spec/route.ts +51 -0
- package/src/providers/spec/types.ts +226 -0
- package/src/providers/status-monitor.d.ts +7 -7
- package/src/providers/status-monitor.ts +37 -22
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +536 -0
- package/src/providers/version-archive.ts +64 -24
- package/src/providers/working-dir.ts +23 -0
- package/src/repo-mesh-types.ts +378 -14
- package/src/sessions/registry.ts +6 -0
- package/src/shared-types-extra.ts +2 -4
- package/src/shared-types.d.ts +8 -0
- package/src/shared-types.ts +64 -1
- package/src/status/builders.ts +26 -6
- package/src/status/normalize.ts +2 -0
- package/src/status/reporter.ts +19 -1
- package/src/status/snapshot.ts +95 -26
- package/src/system/host-memory.ts +29 -12
- package/src/system/load-better-sqlite3.ts +68 -0
- package/src/types.ts +5 -0
- package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
- package/dist/mesh/mesh-sync.d.ts +0 -53
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
- package/src/mesh/mesh-sync.ts +0 -111
|
@@ -13,19 +13,22 @@ import { createRequire } from 'node:module';
|
|
|
13
13
|
import { normalizeInputEnvelope, type ProviderModule, flattenContent, type InputEnvelope, type InputPart } from './contracts.js';
|
|
14
14
|
import { assertProviderSupportsDeclaredInput, getEffectiveMessageInputSupport } from './provider-input-support.js';
|
|
15
15
|
import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext, ProviderErrorReason, HotChatSessionState, SessionModalState } from './provider-instance.js';
|
|
16
|
+
import { normalizeInteractivePrompt, normalizeInteractivePromptResponse, type InteractivePrompt } from './types/interactive-prompt.js';
|
|
16
17
|
import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
|
|
17
18
|
import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
|
|
19
|
+
import { createCliAdapter } from './spec/route.js';
|
|
18
20
|
import type { PtyRuntimeMetadata, PtyTransportFactory } from '../cli-adapters/pty-transport.js';
|
|
19
21
|
import { StatusMonitor } from './status-monitor.js';
|
|
20
22
|
import { ChatHistoryWriter, isNativeSourceCanonicalHistory, materializeProviderNativeHistory, readChatHistory, readProviderChatHistory } from '../config/chat-history.js';
|
|
21
23
|
import { LOG } from '../logging/logger.js';
|
|
22
24
|
import type { ChatMessage } from '../types.js';
|
|
23
25
|
import { buildPersistedProviderEffectMessage, normalizeProviderEffects } from './control-effects.js';
|
|
24
|
-
import { formatAutoApprovalMessage, pickApprovalButton } from './approval-utils.js';
|
|
26
|
+
import { formatAutoApprovalMessage, pickApprovalButton, pickAutoApprovalButton, looksLikeActiveApprovalPromptText } from './approval-utils.js';
|
|
25
27
|
import { getCliScriptCommand, parseCliScriptResult } from './cli-script-results.js';
|
|
26
28
|
import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
|
|
27
29
|
import { normalizeProviderSessionId } from './provider-session-id.js';
|
|
28
30
|
import { buildChatMessage, buildRuntimeSystemChatMessage, isUserFacingChatMessage, normalizeChatMessages, resolveChatMessageKind, extractFinalSummaryFromMessages } from './chat-message-normalization.js';
|
|
31
|
+
import { workingDirBasename } from './working-dir.js';
|
|
29
32
|
|
|
30
33
|
type PersistableCliHistoryMessage = {
|
|
31
34
|
role: string;
|
|
@@ -35,17 +38,77 @@ type PersistableCliHistoryMessage = {
|
|
|
35
38
|
receivedAt?: number;
|
|
36
39
|
};
|
|
37
40
|
|
|
41
|
+
// Status snapshots only ever surface the newest messages: the cloud 'live'
|
|
42
|
+
// profile drops chat messages entirely (loaded lazily via read_chat on
|
|
43
|
+
// subscribe) and the 'full' profile caps activeChat.messages to the last 60
|
|
44
|
+
// (see status/normalize.ts). Unread/completion markers walk only the tail.
|
|
45
|
+
// So getState()'s saved-history hydration — which runs once per resume/manual
|
|
46
|
+
// CLI session on every status report — must read only a bounded tail, not the
|
|
47
|
+
// entire transcript. A full MAX_SAFE_INTEGER read here makes the initial
|
|
48
|
+
// status report O(transcript) × N(sessions), which is the real cold first-
|
|
49
|
+
// connection bottleneck on chat-heavy machines. The window comfortably exceeds
|
|
50
|
+
// the 60-message snapshot cap so dedup/collapse at the boundary stays stable.
|
|
51
|
+
const STATUS_HYDRATION_TAIL_LIMIT = 200;
|
|
52
|
+
|
|
38
53
|
type CompletedDebouncePending = {
|
|
39
54
|
chatTitle: string;
|
|
40
55
|
duration: number;
|
|
41
56
|
timestamp: number;
|
|
42
57
|
firstObservedAt: number;
|
|
58
|
+
previousStatus: string;
|
|
43
59
|
loggedBlockReason?: string;
|
|
60
|
+
loggedTranscriptProbe?: boolean;
|
|
61
|
+
transcriptProbeHistory?: ExternalTranscriptProbe[];
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function isIdleStatus(value: unknown): boolean {
|
|
65
|
+
const status = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
66
|
+
return !status || status === 'idle' || status === 'ready';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
function getMessageTime(message: unknown): number {
|
|
71
|
+
if (!message || typeof message !== 'object') return 0;
|
|
72
|
+
const record = message as { receivedAt?: unknown; timestamp?: unknown };
|
|
73
|
+
const value = Number(record.receivedAt ?? record.timestamp ?? 0);
|
|
74
|
+
return Number.isFinite(value) ? value : 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type CompletedFinalizationBlock = {
|
|
78
|
+
reason: string;
|
|
79
|
+
terminal?: boolean;
|
|
80
|
+
allowTimeout?: boolean;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
type CompletionFinalAssistantEvidence = {
|
|
84
|
+
present: boolean;
|
|
85
|
+
messages: unknown[];
|
|
86
|
+
source: 'parsed' | 'external-native' | 'unavailable';
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
type ExternalTranscriptProbe = {
|
|
90
|
+
readAt: number;
|
|
91
|
+
msgCount: number;
|
|
92
|
+
lastRole: string | null;
|
|
93
|
+
lastKind: string | null;
|
|
94
|
+
contentLen: number;
|
|
95
|
+
sourcePath: string | null;
|
|
96
|
+
sourceMtimeMs: number | null;
|
|
97
|
+
mtimeAgeMs: number | null;
|
|
44
98
|
};
|
|
45
99
|
|
|
46
100
|
const COMPLETED_FINALIZATION_RETRY_MS = 1000;
|
|
47
101
|
const COMPLETED_FINALIZATION_MAX_WAIT_MS = 30_000;
|
|
48
102
|
|
|
103
|
+
/** Events that signal a dispatched mesh task has reached a terminal state.
|
|
104
|
+
* Detach the mesh assignment after emitting one of these so the worker's
|
|
105
|
+
* next unrelated turn doesn't impersonate another completion. */
|
|
106
|
+
const TERMINAL_MESH_EVENTS = new Set([
|
|
107
|
+
'agent:generating_completed',
|
|
108
|
+
'agent:stopped',
|
|
109
|
+
'agent:ready',
|
|
110
|
+
]);
|
|
111
|
+
|
|
49
112
|
const IMAGE_MIME_EXTENSIONS: Record<string, string> = {
|
|
50
113
|
'image/png': '.png',
|
|
51
114
|
'image/jpeg': '.jpg',
|
|
@@ -120,7 +183,7 @@ function hasNonEmptyCliModalButtons(activeModal: unknown): boolean {
|
|
|
120
183
|
}
|
|
121
184
|
|
|
122
185
|
function isCliGeneratingLikeStatus(status: unknown): boolean {
|
|
123
|
-
return status === 'generating' || status === 'streaming' || status === 'long_generating' || status === 'starting';
|
|
186
|
+
return status === 'generating' || status === 'streaming' || status === 'no_progress' || status === 'long_generating' || status === 'starting';
|
|
124
187
|
}
|
|
125
188
|
|
|
126
189
|
export function buildCliStructuredInputPrompt(
|
|
@@ -300,6 +363,19 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
300
363
|
readonly type: string;
|
|
301
364
|
readonly category = 'cli' as const;
|
|
302
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Quiet period an approval modal's signature must be stable before
|
|
368
|
+
* auto-approve sends the approve key. Guards against firing on a prompt
|
|
369
|
+
* that is still streaming into the PTY (the "resolves too fast" symptom):
|
|
370
|
+
* while the modal text/buttons are still changing, every frame yields a
|
|
371
|
+
* new signature and the settle clock restarts. Once the prompt finishes
|
|
372
|
+
* rendering the signature holds and the key is sent after this window.
|
|
373
|
+
* Bounded + small so genuine approvals stay timely. The FSM is already
|
|
374
|
+
* authoritative over the `waiting_approval` state; this only delays the
|
|
375
|
+
* keystroke until the modal *content* has settled.
|
|
376
|
+
*/
|
|
377
|
+
private static readonly AUTO_APPROVE_SETTLE_MS = 600;
|
|
378
|
+
|
|
303
379
|
private adapter: ProviderCliAdapter;
|
|
304
380
|
private context: InstanceContext | null = null;
|
|
305
381
|
private events: ProviderEvent[] = [];
|
|
@@ -309,15 +385,25 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
309
385
|
private monitor: StatusMonitor;
|
|
310
386
|
private generatingDebounceTimer: NodeJS.Timeout | null = null;
|
|
311
387
|
private generatingDebouncePending: { chatTitle: string; timestamp: number } | null = null;
|
|
312
|
-
private
|
|
388
|
+
private lastApprovalEventFingerprint = '';
|
|
313
389
|
private autoApproveBusy = false;
|
|
314
390
|
private autoApproveBusyTimer: NodeJS.Timeout | null = null;
|
|
391
|
+
private lastAutoApprovalSignature = '';
|
|
392
|
+
// Settle gate: the approval modal's signature + the wall-clock when this
|
|
393
|
+
// exact signature was first observed. Auto-approve only fires once the
|
|
394
|
+
// SAME signature has been stable for AUTO_APPROVE_SETTLE_MS, so a prompt
|
|
395
|
+
// still streaming into the PTY (its buttons/message changing frame to
|
|
396
|
+
// frame) keeps resetting the timer and is never approved half-rendered.
|
|
397
|
+
private pendingAutoApprovalSignature = '';
|
|
398
|
+
private pendingAutoApprovalSince = 0;
|
|
399
|
+
private autoApproveSettleTimer: NodeJS.Timeout | null = null;
|
|
315
400
|
private controlValues: Record<string, string | number | boolean> = {};
|
|
316
401
|
private summaryMetadata: unknown = undefined;
|
|
317
402
|
private appliedEffectKeys = new Set<string>();
|
|
318
403
|
private historyWriter: ChatHistoryWriter;
|
|
319
404
|
private runtimeMessages: Array<{ key: string; message: ChatMessage }> = [];
|
|
320
405
|
private lastPersistedHistoryMessages: PersistableCliHistoryMessage[] = [];
|
|
406
|
+
private lastAcknowledgedUserInputAt = 0;
|
|
321
407
|
private lastNativeSourceCanonicalCheckAt = 0;
|
|
322
408
|
private lastNativeSourceCanonicalCacheKey: string | undefined = undefined;
|
|
323
409
|
private cachedSqliteDb: {
|
|
@@ -330,6 +416,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
330
416
|
private suppressIdleHistoryReplay = false;
|
|
331
417
|
private errorMessage: string | undefined = undefined;
|
|
332
418
|
private errorReason: ProviderErrorReason | undefined = undefined;
|
|
419
|
+
private activeInteractivePrompt: InteractivePrompt | null = null;
|
|
333
420
|
|
|
334
421
|
private presentationMode: 'terminal' | 'chat';
|
|
335
422
|
private providerSessionId?: string;
|
|
@@ -370,7 +457,10 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
370
457
|
this.providerSessionId = options?.providerSessionId;
|
|
371
458
|
this.launchMode = options?.launchMode || 'new';
|
|
372
459
|
this.onProviderSessionResolved = options?.onProviderSessionResolved;
|
|
373
|
-
this.adapter =
|
|
460
|
+
this.adapter = createCliAdapter(provider as CliProviderModule, workingDir, cliArgs, options?.extraEnv || {}, transportFactory) as ProviderCliAdapter;
|
|
461
|
+
if (this.providerSessionId) {
|
|
462
|
+
this.adapter.updateRuntimeMeta({ providerSessionId: this.providerSessionId });
|
|
463
|
+
}
|
|
374
464
|
this.monitor = new StatusMonitor();
|
|
375
465
|
this.historyWriter = new ChatHistoryWriter();
|
|
376
466
|
}
|
|
@@ -389,8 +479,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
389
479
|
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
390
480
|
this.monitor.updateConfig({
|
|
391
481
|
approvalAlert: this.settings.approvalAlert !== false,
|
|
392
|
-
|
|
393
|
-
|
|
482
|
+
noProgressAlert: (this.settings.noProgressAlert ?? this.settings.longGeneratingAlert) !== false,
|
|
483
|
+
noProgressThresholdSec: this.settings.noProgressThresholdSec ?? this.settings.longGeneratingThresholdSec ?? 180,
|
|
394
484
|
});
|
|
395
485
|
|
|
396
486
|
// Server connection
|
|
@@ -412,7 +502,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
412
502
|
await this.adapter.spawn();
|
|
413
503
|
await this.enforceFreshSessionLaunchIfNeeded();
|
|
414
504
|
this.maybeAppendRuntimeRecoveryMessage(this.adapter.getRuntimeMetadata());
|
|
415
|
-
if (this.providerSessionId) {
|
|
505
|
+
if (this.providerSessionId && this.shouldHydrateExistingProviderHistory()) {
|
|
416
506
|
this.restorePersistedHistoryFromCurrentSession();
|
|
417
507
|
}
|
|
418
508
|
if (this.providerSessionId && this.launchMode === 'resume') {
|
|
@@ -486,14 +576,38 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
486
576
|
}
|
|
487
577
|
|
|
488
578
|
getState(): ProviderState {
|
|
579
|
+
// TODO(phase5-sandbox): JS override scripts (detectStatus, parseApproval,
|
|
580
|
+
// parseSession) are currently invoked by CliScriptRunner.invoke() via direct
|
|
581
|
+
// function calls — the scripts run in the daemon process with full Node.js
|
|
582
|
+
// access and no resource limits.
|
|
583
|
+
//
|
|
584
|
+
// When Phase 5 lands, CliScriptRunner should route these calls through a
|
|
585
|
+
// SandboxedScriptRunner (see providers/sdk/v1/sandbox/script-runner.ts) so
|
|
586
|
+
// that each call gets a fresh isolated-vm context with a 50 ms CPU limit and
|
|
587
|
+
// a 32 MB memory cap. The execution path to change is:
|
|
588
|
+
// CliScriptRunner.invoke() → SandboxedScriptRunner.run(scriptSource, context)
|
|
589
|
+
//
|
|
590
|
+
// This getState() call-site is NOT where the change goes — the wiring belongs
|
|
591
|
+
// in cli-script-runner.ts (CliScriptRunner.detectStatus / parseApproval /
|
|
592
|
+
// parseSession), with provider-loader.ts updated to store script source strings
|
|
593
|
+
// alongside the loaded function references for extended-legacy providers.
|
|
489
594
|
const adapterStatus = this.adapter.getStatus();
|
|
595
|
+
if (Object.prototype.hasOwnProperty.call(adapterStatus, 'activeInteractivePrompt')) {
|
|
596
|
+
this.activeInteractivePrompt = adapterStatus.activeInteractivePrompt ?? null;
|
|
597
|
+
}
|
|
490
598
|
let parsedStatus: any = null;
|
|
491
599
|
let parseErrorMessage: string | undefined;
|
|
492
600
|
if (typeof this.adapter.getScriptParsedStatus === 'function') {
|
|
493
601
|
try {
|
|
494
602
|
parsedStatus = this.adapter.getScriptParsedStatus() || null;
|
|
495
|
-
|
|
496
|
-
|
|
603
|
+
const parsedErrorMessage = typeof parsedStatus?.errorMessage === 'string' && parsedStatus.errorMessage.trim()
|
|
604
|
+
? parsedStatus.errorMessage.trim()
|
|
605
|
+
: undefined;
|
|
606
|
+
const parsedErrorReason = typeof parsedStatus?.errorReason === 'string' && parsedStatus.errorReason.trim()
|
|
607
|
+
? parsedStatus.errorReason.trim() as ProviderErrorReason
|
|
608
|
+
: undefined;
|
|
609
|
+
this.errorMessage = parsedErrorMessage;
|
|
610
|
+
this.errorReason = parsedErrorReason;
|
|
497
611
|
} catch (error: any) {
|
|
498
612
|
parseErrorMessage = error?.message || String(error);
|
|
499
613
|
this.errorMessage = parseErrorMessage;
|
|
@@ -503,22 +617,53 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
503
617
|
this.errorMessage = undefined;
|
|
504
618
|
this.errorReason = undefined;
|
|
505
619
|
}
|
|
506
|
-
const
|
|
507
|
-
const visibleStatus = parseErrorMessage
|
|
508
|
-
? 'error'
|
|
509
|
-
: (autoApproveActive ? 'generating' : adapterStatus.status);
|
|
510
|
-
const parsedProviderSessionId = normalizeProviderSessionId(
|
|
620
|
+
const adapterProviderSessionId = normalizeProviderSessionId(
|
|
511
621
|
this.provider,
|
|
512
|
-
typeof
|
|
622
|
+
typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
|
|
513
623
|
);
|
|
514
|
-
|
|
515
|
-
|
|
624
|
+
const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, Date.now());
|
|
625
|
+
const autoApproveHoldIdle = this.autoApproveBusy && adapterStatus.status === 'idle';
|
|
626
|
+
let visibleStatus = parseErrorMessage || parsedStatus?.status === 'error'
|
|
627
|
+
? 'error'
|
|
628
|
+
: (autoApproveActive || autoApproveHoldIdle ? 'generating' : adapterStatus.status);
|
|
629
|
+
// getState() must agree with the status the FSM-driven detectStatusTransition()
|
|
630
|
+
// already committed to lastStatus. The adapter's own status is authoritative; we do
|
|
631
|
+
// not second-guess it with native-transcript shape. Only reconcile a generating-like
|
|
632
|
+
// read down to idle when our own lastStatus has already flipped idle (avoids a
|
|
633
|
+
// perpetual dashboard spinner during the brief window before the next getStatus()).
|
|
634
|
+
if (isCliGeneratingLikeStatus(visibleStatus) && this.lastStatus === 'idle') {
|
|
635
|
+
visibleStatus = 'idle';
|
|
516
636
|
}
|
|
517
637
|
const runtime = this.adapter.getRuntimeMetadata();
|
|
518
638
|
this.maybeAppendRuntimeRecoveryMessage(runtime);
|
|
519
639
|
let parsedMessages = Array.isArray(parsedStatus?.messages)
|
|
520
640
|
? parsedStatus.messages
|
|
521
641
|
: [];
|
|
642
|
+
const parsedProviderSessionId = normalizeProviderSessionId(
|
|
643
|
+
this.provider,
|
|
644
|
+
typeof parsedStatus?.providerSessionId === 'string' ? parsedStatus.providerSessionId : '',
|
|
645
|
+
);
|
|
646
|
+
const suppressFreshLaunchStartupReplay = this.shouldSuppressFreshLaunchStartupReplay(
|
|
647
|
+
parsedMessages,
|
|
648
|
+
parsedStatus,
|
|
649
|
+
adapterStatus,
|
|
650
|
+
parsedProviderSessionId,
|
|
651
|
+
);
|
|
652
|
+
if (adapterProviderSessionId && !suppressFreshLaunchStartupReplay) {
|
|
653
|
+
this.promoteProviderSessionId(adapterProviderSessionId);
|
|
654
|
+
}
|
|
655
|
+
if (parsedProviderSessionId && !suppressFreshLaunchStartupReplay) {
|
|
656
|
+
this.promoteProviderSessionId(parsedProviderSessionId);
|
|
657
|
+
}
|
|
658
|
+
if (suppressFreshLaunchStartupReplay) {
|
|
659
|
+
parsedMessages = [];
|
|
660
|
+
}
|
|
661
|
+
// Adapter runtime metadata is transport-owned and is not guaranteed to
|
|
662
|
+
// identify this conversation. Spec adapters historically exposed the
|
|
663
|
+
// provider spec id (for example "codex-cli") as runtimeId, which made
|
|
664
|
+
// concurrent sessions share one activeChat identity until their native
|
|
665
|
+
// provider session ids were discovered.
|
|
666
|
+
const activeChatId = this.providerSessionId || this.instanceId;
|
|
522
667
|
const historyMessageCount = Number.isFinite(parsedStatus?.historyMessageCount)
|
|
523
668
|
? Math.max(0, Number(parsedStatus.historyMessageCount))
|
|
524
669
|
: null;
|
|
@@ -528,9 +673,20 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
528
673
|
: [];
|
|
529
674
|
}
|
|
530
675
|
const mergedMessages = this.mergeConversationMessages(parsedMessages);
|
|
531
|
-
const canonicalBackedHistory = this.
|
|
676
|
+
const canonicalBackedHistory = this.shouldHydrateExistingProviderHistory()
|
|
677
|
+
? this.syncCanonicalSavedHistoryIfNeeded()
|
|
678
|
+
: false;
|
|
679
|
+
const statusMessages = canonicalBackedHistory && this.lastPersistedHistoryMessages.length > 0
|
|
680
|
+
? this.lastPersistedHistoryMessages.map((message) => ({
|
|
681
|
+
role: message.role,
|
|
682
|
+
content: message.content,
|
|
683
|
+
kind: message.kind,
|
|
684
|
+
senderName: message.senderName,
|
|
685
|
+
receivedAt: message.receivedAt,
|
|
686
|
+
}))
|
|
687
|
+
: mergedMessages;
|
|
532
688
|
|
|
533
|
-
const dirName = this.workingDir
|
|
689
|
+
const dirName = workingDirBasename(this.workingDir);
|
|
534
690
|
const parsedChatStatus = typeof parsedStatus?.status === 'string' && parsedStatus.status.trim()
|
|
535
691
|
? parsedStatus.status.trim()
|
|
536
692
|
: undefined;
|
|
@@ -542,7 +698,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
542
698
|
&& adapterStatus.status === 'idle'
|
|
543
699
|
&& parsedStatus?.status === 'idle';
|
|
544
700
|
let messagesToSave = parsedMessages;
|
|
545
|
-
if (!suppressStaleParsedBusyStatus && (parsedChatStatus === 'generating' || parsedChatStatus === 'long_generating')) {
|
|
701
|
+
if (!suppressStaleParsedBusyStatus && (parsedChatStatus === 'generating' || parsedChatStatus === 'no_progress' || parsedChatStatus === 'long_generating')) {
|
|
546
702
|
const lastIdx = messagesToSave.length - 1;
|
|
547
703
|
if (lastIdx >= 0 && messagesToSave[lastIdx]?.role === 'assistant') {
|
|
548
704
|
messagesToSave = messagesToSave.slice(0, lastIdx);
|
|
@@ -572,33 +728,48 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
572
728
|
}
|
|
573
729
|
}
|
|
574
730
|
|
|
575
|
-
this.applyProviderResponse(
|
|
731
|
+
this.applyProviderResponse(
|
|
732
|
+
suppressFreshLaunchStartupReplay && parsedStatus && typeof parsedStatus === 'object'
|
|
733
|
+
? { ...parsedStatus, providerSessionId: undefined }
|
|
734
|
+
: parsedStatus,
|
|
735
|
+
{ phase: 'immediate' },
|
|
736
|
+
);
|
|
576
737
|
const surface = resolveProviderStateSurface({
|
|
577
738
|
summaryMetadata: this.summaryMetadata as any,
|
|
578
739
|
controlValues: this.controlValues,
|
|
579
740
|
});
|
|
580
741
|
const activeChatStatus = parseErrorMessage
|
|
581
742
|
? 'error'
|
|
582
|
-
: autoApproveActive && parsedStatus?.status === 'waiting_approval'
|
|
743
|
+
: (autoApproveActive && parsedStatus?.status === 'waiting_approval') || autoApproveHoldIdle
|
|
583
744
|
? 'generating'
|
|
584
745
|
: (adapterStatus.status !== 'idle'
|
|
585
746
|
? visibleStatus
|
|
586
747
|
: (suppressStaleParsedBusyStatus ? visibleStatus : (parsedChatStatus || visibleStatus)));
|
|
587
748
|
|
|
749
|
+
// If an AskUserQuestion prompt is awaiting user input, overlay status as
|
|
750
|
+
// waiting_choice. This is distinct from waiting_approval (tool-use consent)
|
|
751
|
+
// — the engine's isWaitingForResponse state is unchanged, so completion
|
|
752
|
+
// tracking continues normally once the user responds.
|
|
753
|
+
const hasInteractivePrompt = !!this.activeInteractivePrompt;
|
|
754
|
+
const finalStatus = hasInteractivePrompt ? 'waiting_choice' : visibleStatus;
|
|
755
|
+
const finalChatStatus = hasInteractivePrompt ? 'waiting_choice' : activeChatStatus;
|
|
756
|
+
|
|
588
757
|
return {
|
|
589
758
|
type: this.type,
|
|
590
759
|
name: this.provider.name,
|
|
591
760
|
category: 'cli',
|
|
592
|
-
status:
|
|
761
|
+
status: finalStatus,
|
|
593
762
|
mode: this.presentationMode,
|
|
594
763
|
activeChat: {
|
|
595
|
-
id:
|
|
764
|
+
id: activeChatId,
|
|
596
765
|
title: parsedStatus?.title || dirName,
|
|
597
|
-
status:
|
|
598
|
-
messages:
|
|
599
|
-
activeModal: autoApproveActive ? null : (parsedStatus?.activeModal ?? adapterStatus.activeModal),
|
|
766
|
+
status: finalChatStatus,
|
|
767
|
+
messages: statusMessages,
|
|
768
|
+
activeModal: (autoApproveActive || autoApproveHoldIdle) ? null : (parsedStatus?.activeModal ?? adapterStatus.activeModal),
|
|
769
|
+
activeInteractivePrompt: this.activeInteractivePrompt,
|
|
600
770
|
inputContent: '',
|
|
601
771
|
},
|
|
772
|
+
activeInteractivePrompt: this.activeInteractivePrompt,
|
|
602
773
|
workspace: this.workingDir,
|
|
603
774
|
instanceId: this.instanceId,
|
|
604
775
|
providerSessionId: this.providerSessionId,
|
|
@@ -639,7 +810,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
639
810
|
getHotChatSessionState(): HotChatSessionState {
|
|
640
811
|
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
641
812
|
const autoApproveActive = adapterStatus.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
642
|
-
const
|
|
813
|
+
const autoApproveHoldIdle = this.autoApproveBusy && adapterStatus.status === 'idle';
|
|
814
|
+
const visibleStatus = autoApproveActive || autoApproveHoldIdle ? 'generating' : adapterStatus.status;
|
|
643
815
|
const runtime = this.adapter.getRuntimeMetadata();
|
|
644
816
|
return {
|
|
645
817
|
id: this.instanceId,
|
|
@@ -651,36 +823,149 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
651
823
|
};
|
|
652
824
|
}
|
|
653
825
|
|
|
654
|
-
getSessionModalState(): SessionModalState {
|
|
655
|
-
const adapterStatus = this.adapter.getStatus({ allowParse:
|
|
826
|
+
getSessionModalState(sessionId?: string): SessionModalState {
|
|
827
|
+
const adapterStatus = this.adapter.getStatus({ allowParse: true });
|
|
656
828
|
const autoApproveActive = adapterStatus.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
657
|
-
const
|
|
658
|
-
const
|
|
829
|
+
const autoApproveHoldIdle = this.autoApproveBusy && adapterStatus.status === 'idle';
|
|
830
|
+
const visibleStatus = autoApproveActive || autoApproveHoldIdle ? 'generating' : adapterStatus.status;
|
|
831
|
+
const dirName = workingDirBasename(this.workingDir);
|
|
659
832
|
return {
|
|
660
|
-
|
|
833
|
+
// Honor the caller-supplied sessionId — InstanceMgr rejects the
|
|
834
|
+
// projection when projected.id !== requested sessionId, and
|
|
835
|
+
// this.instanceId is the manager's internal key, not the public
|
|
836
|
+
// sessionId the dashboard subscribes by.
|
|
837
|
+
id: sessionId ?? this.instanceId,
|
|
661
838
|
status: visibleStatus,
|
|
662
839
|
title: dirName,
|
|
663
|
-
activeModal: autoApproveActive ? null : adapterStatus.activeModal,
|
|
840
|
+
activeModal: (autoApproveActive || autoApproveHoldIdle) ? null : adapterStatus.activeModal,
|
|
664
841
|
};
|
|
665
842
|
}
|
|
666
843
|
|
|
667
844
|
updateSettings(newSettings: Record<string, any>): void {
|
|
668
|
-
|
|
845
|
+
// Merge semantics: a key omitted from newSettings preserves its existing
|
|
846
|
+
// value, a key present in newSettings (even as false) overrides it.
|
|
847
|
+
//
|
|
848
|
+
// This is required because updateSettings has two callers with opposite
|
|
849
|
+
// intent:
|
|
850
|
+
// 1. Full re-injection — the dashboard toggle path (handleSetProviderSetting
|
|
851
|
+
// → getSettings → updateInstanceSettings) sends the COMPLETE settings
|
|
852
|
+
// object, so an explicit autoApprove:false must win.
|
|
853
|
+
// 2. Partial stamp — the mesh relay-safety stamp (router.ts agent_command,
|
|
854
|
+
// buildMeshWorkerRelayStamp) sends ONLY {meshNodeFor, meshNodeId,
|
|
855
|
+
// meshCoordinatorDaemonId, launchedByCoordinator} on every coordinator
|
|
856
|
+
// re-dispatch. It carries no autoApprove, so a full replacement would
|
|
857
|
+
// wipe the launch-time autoApprove:true the worker was started with,
|
|
858
|
+
// silently dropping every later approval to a manual gate until the
|
|
859
|
+
// machine-page toggle re-injected the full settings.
|
|
860
|
+
//
|
|
861
|
+
// A plain merge satisfies both: undefined keys fall through to the existing
|
|
862
|
+
// value (preserving launch-stamp settings like autoApprove + the mesh routing
|
|
863
|
+
// keys), explicit keys override. This subsumes the previous mesh-key preserve
|
|
864
|
+
// list, which only protected the routing keys and not autoApprove.
|
|
865
|
+
this.settings = { ...this.settings, ...newSettings };
|
|
669
866
|
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
670
867
|
this.monitor.updateConfig({
|
|
671
868
|
approvalAlert: this.settings.approvalAlert !== false,
|
|
672
|
-
|
|
673
|
-
|
|
869
|
+
noProgressAlert: (this.settings.noProgressAlert ?? this.settings.longGeneratingAlert) !== false,
|
|
870
|
+
noProgressThresholdSec: this.settings.noProgressThresholdSec ?? this.settings.longGeneratingThresholdSec ?? 180,
|
|
674
871
|
});
|
|
675
872
|
}
|
|
676
873
|
|
|
874
|
+
/**
|
|
875
|
+
* Stamp a direct-dispatch mesh assignment on this instance.
|
|
876
|
+
* setupMeshEventForwarding reads settings.meshNodeFor + meshActiveTaskId to
|
|
877
|
+
* route generating_completed back to the originating coordinator. Without
|
|
878
|
+
* this stamp, mesh_send_task --direct targets a plain CLI session whose
|
|
879
|
+
* completion events silently drop because the forwarder has nothing to
|
|
880
|
+
* match against.
|
|
881
|
+
*/
|
|
882
|
+
attachMeshAssignment(assignment: { meshId: string; nodeId?: string; taskId?: string; coordinatorDaemonId?: string; coordinatorSessionId?: string }): void {
|
|
883
|
+
if (!assignment?.meshId) return;
|
|
884
|
+
this.settings = {
|
|
885
|
+
...this.settings,
|
|
886
|
+
meshNodeFor: assignment.meshId,
|
|
887
|
+
...(assignment.nodeId ? { meshNodeId: assignment.nodeId } : {}),
|
|
888
|
+
...(assignment.taskId ? { meshActiveTaskId: assignment.taskId } : {}),
|
|
889
|
+
...(assignment.coordinatorDaemonId ? { meshCoordinatorDaemonId: assignment.coordinatorDaemonId } : {}),
|
|
890
|
+
// Session-level routing anchor: the originating coordinator session, so this
|
|
891
|
+
// worker's completion events route back to the exact session that dispatched it.
|
|
892
|
+
...(assignment.coordinatorSessionId ? { meshCoordinatorSessionId: assignment.coordinatorSessionId } : {}),
|
|
893
|
+
};
|
|
894
|
+
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Clear a previously-attached mesh assignment after the task reaches a
|
|
899
|
+
* terminal state. Leaving meshNodeFor pinned would route this session's
|
|
900
|
+
* subsequent unrelated turns (e.g. ad-hoc dashboard chats) to the
|
|
901
|
+
* coordinator as if they were task completions.
|
|
902
|
+
*/
|
|
903
|
+
detachMeshAssignment(): void {
|
|
904
|
+
if (!this.settings.meshNodeFor && !this.settings.meshActiveTaskId && !this.settings.meshNodeId) return;
|
|
905
|
+
const { meshNodeFor, meshNodeId, meshActiveTaskId, ...rest } = this.settings;
|
|
906
|
+
void meshNodeFor; void meshNodeId; void meshActiveTaskId;
|
|
907
|
+
this.settings = rest;
|
|
908
|
+
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* The resolved modal-park status of this session, or null when it is not
|
|
913
|
+
* parked on a modal awaiting a human answer. Mirrors the overlay logic in
|
|
914
|
+
* getState(): an active AskUserQuestion interactive prompt resolves to
|
|
915
|
+
* waiting_choice; otherwise the adapter's waiting_approval (tool consent)
|
|
916
|
+
* counts — UNLESS auto-approve will dismiss it, in which case the session is
|
|
917
|
+
* effectively generating and is NOT modal-parked. This is the single signal
|
|
918
|
+
* the mesh force-inject guard consults, and the same status string the
|
|
919
|
+
* reconcile loop reads off get_status_metadata. Lowercase literals only —
|
|
920
|
+
* the SessionStatus enum is forked across modules and waiting_choice is
|
|
921
|
+
* absent from some of them.
|
|
922
|
+
*/
|
|
923
|
+
resolveModalParkStatus(): 'waiting_choice' | 'waiting_approval' | null {
|
|
924
|
+
if (this.activeInteractivePrompt) return 'waiting_choice';
|
|
925
|
+
let adapterStatus: { status?: string };
|
|
926
|
+
try {
|
|
927
|
+
adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
928
|
+
} catch {
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
if (adapterStatus.status === 'waiting_approval' && !this.shouldAutoApprove()) {
|
|
932
|
+
return 'waiting_approval';
|
|
933
|
+
}
|
|
934
|
+
return null;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/** True when this session is parked on a modal awaiting a human answer. */
|
|
938
|
+
isModalParked(): boolean {
|
|
939
|
+
return this.resolveModalParkStatus() !== null;
|
|
940
|
+
}
|
|
941
|
+
|
|
677
942
|
onEvent(event: string, data?: any): void {
|
|
678
943
|
if (event === 'send_message') {
|
|
679
944
|
const input = normalizeInputEnvelope(data);
|
|
680
945
|
assertProviderSupportsDeclaredInput(this.provider, input);
|
|
681
946
|
const promptText = buildCliStructuredInputPrompt(input);
|
|
682
947
|
if (promptText) {
|
|
683
|
-
|
|
948
|
+
// force:true bypasses the busy/generating send guard so terminal mesh
|
|
949
|
+
// events (completion/failure/bootstrap) land in a coordinator session that
|
|
950
|
+
// is itself parked in `generating` while awaiting that very event.
|
|
951
|
+
// Without it the message is queued and only flushed on the coordinator's
|
|
952
|
+
// own idle transition — which never happens until it receives the message.
|
|
953
|
+
const force = data?.force === true;
|
|
954
|
+
// Modal guard: a force-inject still writes raw keystrokes into the PTY,
|
|
955
|
+
// bypassing the busy send-guard. If the coordinator is parked on a
|
|
956
|
+
// harness modal (claude-cli AskUserQuestion → waiting_choice, or a
|
|
957
|
+
// tool-consent waiting_approval), those keystrokes are consumed by the
|
|
958
|
+
// modal's key handler and silently select a choice the user never made
|
|
959
|
+
// (data corruption). Hold the force-inject in that narrow window —
|
|
960
|
+
// the event stays queued and the reconcile loop redelivers it on the
|
|
961
|
+
// next tick once the modal is resolved. We ONLY hold for the two modal
|
|
962
|
+
// states; generating is still force-injected (that is the deadlock the
|
|
963
|
+
// force path exists to break — see mesh-events-coordinator).
|
|
964
|
+
if (force && this.isModalParked()) {
|
|
965
|
+
LOG.info('CLI', `[${this.type}] force send_message held — coordinator parked on modal (${this.resolveModalParkStatus()})`);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
void this.adapter.sendMessage(promptText, force ? { force: true } : {}).catch((e: any) => {
|
|
684
969
|
LOG.warn('CLI', `[${this.type}] send_message failed: ${e?.message || e}`);
|
|
685
970
|
});
|
|
686
971
|
}
|
|
@@ -690,14 +975,73 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
690
975
|
void this.adapter.resolveAction(data).catch((e: any) => {
|
|
691
976
|
LOG.warn('CLI', `[${this.type}] resolve_action failed: ${e?.message || e}`);
|
|
692
977
|
});
|
|
978
|
+
} else if (event === 'interactive_prompt' && data) {
|
|
979
|
+
const prompt = normalizeInteractivePrompt(data);
|
|
980
|
+
if (prompt) {
|
|
981
|
+
this.activeInteractivePrompt = prompt;
|
|
982
|
+
this.events.push({
|
|
983
|
+
event: 'interactive_prompt',
|
|
984
|
+
timestamp: Date.now(),
|
|
985
|
+
promptId: prompt.promptId,
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
} else if (event === 'interactive_prompt_response' && data) {
|
|
989
|
+
try {
|
|
990
|
+
const response = normalizeInteractivePromptResponse(data);
|
|
991
|
+
if (this.activeInteractivePrompt?.promptId === response.promptId) {
|
|
992
|
+
this.activeInteractivePrompt = null;
|
|
993
|
+
}
|
|
994
|
+
if (typeof this.adapter.setInteractivePromptResponse !== 'function') {
|
|
995
|
+
LOG.warn('CLI', `[${this.type}] interactive_prompt_response ignored: adapter does not support interactive prompts`);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
void this.adapter.setInteractivePromptResponse(response).catch((e: any) => {
|
|
999
|
+
LOG.warn('CLI', `[${this.type}] interactive_prompt_response failed: ${e?.message || e}`);
|
|
1000
|
+
});
|
|
1001
|
+
} catch (e: any) {
|
|
1002
|
+
LOG.warn('CLI', `[${this.type}] invalid interactive_prompt_response: ${e?.message || e}`);
|
|
1003
|
+
}
|
|
693
1004
|
} else if (event === 'provider_state_patch' && data && typeof data === 'object') {
|
|
694
1005
|
this.applyProviderResponse(data, { phase: 'immediate' });
|
|
695
1006
|
}
|
|
696
1007
|
}
|
|
697
1008
|
|
|
1009
|
+
recordAcknowledgedUserInput(input: InputEnvelope | string): void {
|
|
1010
|
+
const content = typeof input === 'string'
|
|
1011
|
+
? input.trim()
|
|
1012
|
+
: buildCliStructuredInputPrompt(input).trim();
|
|
1013
|
+
if (!content) return;
|
|
1014
|
+
|
|
1015
|
+
const receivedAt = Date.now();
|
|
1016
|
+
this.lastAcknowledgedUserInputAt = receivedAt;
|
|
1017
|
+
const dedupKey = `user_input_ack:${crypto
|
|
1018
|
+
.createHash('sha256')
|
|
1019
|
+
.update(`${this.instanceId}:${content}:${receivedAt}`)
|
|
1020
|
+
.digest('hex')
|
|
1021
|
+
.slice(0, 24)}`;
|
|
1022
|
+
this.appendRuntimeMessage(buildChatMessage({
|
|
1023
|
+
role: 'user',
|
|
1024
|
+
senderName: 'User',
|
|
1025
|
+
kind: 'standard',
|
|
1026
|
+
content,
|
|
1027
|
+
receivedAt,
|
|
1028
|
+
timestamp: receivedAt,
|
|
1029
|
+
source: 'runtime_input_ack',
|
|
1030
|
+
meta: {
|
|
1031
|
+
runtimeInputAck: true,
|
|
1032
|
+
provider: this.type,
|
|
1033
|
+
workspace: this.workingDir,
|
|
1034
|
+
},
|
|
1035
|
+
} as ChatMessage), dedupKey);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
698
1038
|
dispose(): void {
|
|
699
1039
|
this.adapter.shutdown();
|
|
700
1040
|
this.monitor.reset();
|
|
1041
|
+
// Cancel any armed auto-approve timers so a pending settle re-check
|
|
1042
|
+
// can't fire resolveModal/detectStatusTransition against a dead adapter.
|
|
1043
|
+
if (this.autoApproveSettleTimer) { clearTimeout(this.autoApproveSettleTimer); this.autoApproveSettleTimer = null; }
|
|
1044
|
+
if (this.autoApproveBusyTimer) { clearTimeout(this.autoApproveBusyTimer); this.autoApproveBusyTimer = null; }
|
|
701
1045
|
this.appliedEffectKeys.clear();
|
|
702
1046
|
try { this.cachedSqliteDb?.close(); } catch { /* noop */ }
|
|
703
1047
|
this.cachedSqliteDb = null;
|
|
@@ -706,6 +1050,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
706
1050
|
|
|
707
1051
|
private completedDebounceTimer: NodeJS.Timeout | null = null;
|
|
708
1052
|
private completedDebouncePending: CompletedDebouncePending | null = null;
|
|
1053
|
+
private lastExternalCompletionProbe: ExternalTranscriptProbe | null = null;
|
|
709
1054
|
|
|
710
1055
|
private async enforceFreshSessionLaunchIfNeeded(): Promise<void> {
|
|
711
1056
|
const scriptName = getForcedNewSessionScriptName(this.provider, this.launchMode);
|
|
@@ -740,7 +1085,180 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
740
1085
|
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
741
1086
|
const role = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : '';
|
|
742
1087
|
const content = lastVisible ? flattenContent(lastVisible.content).trim() : '';
|
|
743
|
-
|
|
1088
|
+
if (role !== 'assistant' || !content) return false;
|
|
1089
|
+
// Guard: if the last assistant message looks like an active approval/input prompt,
|
|
1090
|
+
// it is not a real completion — the session is still awaiting user input.
|
|
1091
|
+
if (looksLikeActiveApprovalPromptText(content)) return false;
|
|
1092
|
+
return true;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
private buildExternalTranscriptProbe(messages: unknown[], sourcePath?: string, sourceMtimeMs?: number): ExternalTranscriptProbe {
|
|
1096
|
+
const visibleMessages = messages.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
1097
|
+
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
1098
|
+
const readAt = Date.now();
|
|
1099
|
+
const mtimeMs = Number(sourceMtimeMs) || 0;
|
|
1100
|
+
return {
|
|
1101
|
+
readAt,
|
|
1102
|
+
msgCount: messages.length,
|
|
1103
|
+
lastRole: typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null,
|
|
1104
|
+
lastKind: typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null,
|
|
1105
|
+
contentLen: lastVisible ? flattenContent(lastVisible.content).trim().length : 0,
|
|
1106
|
+
sourcePath: typeof sourcePath === 'string' && sourcePath ? sourcePath : null,
|
|
1107
|
+
sourceMtimeMs: mtimeMs || null,
|
|
1108
|
+
mtimeAgeMs: mtimeMs ? Math.max(0, readAt - mtimeMs) : null,
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
private recordPendingTranscriptProbe(pending: CompletedDebouncePending): ExternalTranscriptProbe | null {
|
|
1113
|
+
const probe = this.lastExternalCompletionProbe;
|
|
1114
|
+
if (!probe) return null;
|
|
1115
|
+
const history = pending.transcriptProbeHistory || [];
|
|
1116
|
+
const last = history[history.length - 1];
|
|
1117
|
+
if (!last || last.readAt !== probe.readAt || last.msgCount !== probe.msgCount || last.lastRole !== probe.lastRole || last.contentLen !== probe.contentLen) {
|
|
1118
|
+
history.push(probe);
|
|
1119
|
+
pending.transcriptProbeHistory = history.slice(-5);
|
|
1120
|
+
}
|
|
1121
|
+
return probe;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
private readExternalCompletionMessages(): unknown[] | null {
|
|
1125
|
+
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1126
|
+
if (!adapterOwnsMessagesElsewhere) return null;
|
|
1127
|
+
if (!this.providerSessionId) return null;
|
|
1128
|
+
if (!isNativeSourceCanonicalHistory(this.provider.nativeHistory)) return null;
|
|
1129
|
+
|
|
1130
|
+
if (this.lastExternalCompletionProbe?.sourcePath) {
|
|
1131
|
+
try { fs.statSync(this.lastExternalCompletionProbe.sourcePath); } catch { /* best-effort metadata refresh */ }
|
|
1132
|
+
}
|
|
1133
|
+
const restoredHistory = readProviderChatHistory(this.type, {
|
|
1134
|
+
canonicalHistory: this.provider.nativeHistory,
|
|
1135
|
+
historySessionId: this.providerSessionId,
|
|
1136
|
+
workspace: this.workingDir,
|
|
1137
|
+
offset: 0,
|
|
1138
|
+
limit: Number.MAX_SAFE_INTEGER,
|
|
1139
|
+
historyBehavior: this.provider.historyBehavior,
|
|
1140
|
+
scripts: this.provider.scripts as any,
|
|
1141
|
+
sessionStartedAtMs: this.startedAt,
|
|
1142
|
+
forceRefresh: true,
|
|
1143
|
+
});
|
|
1144
|
+
if (restoredHistory.source !== 'provider-native') {
|
|
1145
|
+
this.lastExternalCompletionProbe = null;
|
|
1146
|
+
return null;
|
|
1147
|
+
}
|
|
1148
|
+
this.lastExternalCompletionProbe = this.buildExternalTranscriptProbe(
|
|
1149
|
+
restoredHistory.messages,
|
|
1150
|
+
restoredHistory.sourcePath,
|
|
1151
|
+
restoredHistory.sourceMtimeMs,
|
|
1152
|
+
);
|
|
1153
|
+
return restoredHistory.messages;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
private completionFinalAssistantEvidence(parsedMessages: unknown): CompletionFinalAssistantEvidence {
|
|
1157
|
+
if (this.completionHasFinalAssistantMessage(parsedMessages)) {
|
|
1158
|
+
return {
|
|
1159
|
+
present: true,
|
|
1160
|
+
messages: Array.isArray(parsedMessages) ? parsedMessages : [],
|
|
1161
|
+
source: 'parsed',
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
const externalMessages = this.readExternalCompletionMessages();
|
|
1166
|
+
if (externalMessages) {
|
|
1167
|
+
return {
|
|
1168
|
+
present: this.completionHasFinalAssistantMessage(externalMessages),
|
|
1169
|
+
messages: externalMessages,
|
|
1170
|
+
source: 'external-native',
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
return {
|
|
1175
|
+
present: false,
|
|
1176
|
+
messages: Array.isArray(parsedMessages) ? parsedMessages : [],
|
|
1177
|
+
source: 'unavailable',
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
private completionFinalSummary(parsedMessages: unknown): string | undefined {
|
|
1182
|
+
// For native-source providers (claude-cli: chatMessagesOwnedExternally), the PTY
|
|
1183
|
+
// screen parse is NOT the source of truth for the final summary — the terminal
|
|
1184
|
+
// wraps/scrolls/clips text, so a screen-parsed assistant message is often a partial
|
|
1185
|
+
// prefix (e.g. 76 chars of a 112-char turn). The append-only native transcript holds
|
|
1186
|
+
// the complete turn. Prefer it whenever it yields a longer/complete summary; fall back
|
|
1187
|
+
// to the parsed screen only when the transcript is unavailable. This is the real cause
|
|
1188
|
+
// of the truncated finalSummary — independent of cloud vs standalone (it surfaces on
|
|
1189
|
+
// any short, fast-completing task where screen parse wins the race).
|
|
1190
|
+
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1191
|
+
const parsedSummary = extractFinalSummaryFromMessages(
|
|
1192
|
+
(this.completionHasFinalAssistantMessage(parsedMessages)
|
|
1193
|
+
? (Array.isArray(parsedMessages) ? parsedMessages : [])
|
|
1194
|
+
: []) as any,
|
|
1195
|
+
);
|
|
1196
|
+
if (adapterOwnsMessagesElsewhere) {
|
|
1197
|
+
const externalMessages = this.readExternalCompletionMessages();
|
|
1198
|
+
const externalSummary = externalMessages
|
|
1199
|
+
? extractFinalSummaryFromMessages(externalMessages as any)
|
|
1200
|
+
: '';
|
|
1201
|
+
// The transcript is authoritative for native-source providers. Use it unless it is
|
|
1202
|
+
// empty (not yet written) — only then fall back to whatever the screen parsed.
|
|
1203
|
+
if (externalSummary) return externalSummary;
|
|
1204
|
+
return parsedSummary || undefined;
|
|
1205
|
+
}
|
|
1206
|
+
return parsedSummary || undefined;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
private buildCompletedFinalizationDiagnostic(args: {
|
|
1210
|
+
blockReason: string;
|
|
1211
|
+
latestStatus?: any;
|
|
1212
|
+
latestVisibleStatus: string;
|
|
1213
|
+
waitedMs: number;
|
|
1214
|
+
pending: CompletedDebouncePending;
|
|
1215
|
+
emittedAfterFinalizationTimeout: boolean;
|
|
1216
|
+
}): Record<string, unknown> {
|
|
1217
|
+
let parsed: any = null;
|
|
1218
|
+
let parseError: string | undefined;
|
|
1219
|
+
try {
|
|
1220
|
+
parsed = this.adapter.getScriptParsedStatus();
|
|
1221
|
+
} catch (error: any) {
|
|
1222
|
+
parseError = error?.message || String(error);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
const evidence = this.completionFinalAssistantEvidence(parsed?.messages);
|
|
1226
|
+
if (evidence.source === 'external-native') {
|
|
1227
|
+
this.recordPendingTranscriptProbe(args.pending);
|
|
1228
|
+
}
|
|
1229
|
+
const visibleMessages = (Array.isArray(evidence.messages) ? evidence.messages : [])
|
|
1230
|
+
.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
1231
|
+
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
1232
|
+
const lastVisibleRole = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null;
|
|
1233
|
+
const lastVisibleKind = typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null;
|
|
1234
|
+
const lastVisibleContentLength = lastVisible ? flattenContent(lastVisible.content).trim().length : 0;
|
|
1235
|
+
|
|
1236
|
+
return {
|
|
1237
|
+
providerType: this.type,
|
|
1238
|
+
sessionId: this.instanceId,
|
|
1239
|
+
providerSessionId: this.providerSessionId || null,
|
|
1240
|
+
workspace: this.workingDir,
|
|
1241
|
+
blockReason: args.blockReason,
|
|
1242
|
+
emittedAfterFinalizationTimeout: args.emittedAfterFinalizationTimeout,
|
|
1243
|
+
waitedMs: args.waitedMs,
|
|
1244
|
+
maxWaitMs: COMPLETED_FINALIZATION_MAX_WAIT_MS,
|
|
1245
|
+
adapterStatus: typeof args.latestStatus?.status === 'string' ? args.latestStatus.status : null,
|
|
1246
|
+
latestVisibleStatus: args.latestVisibleStatus,
|
|
1247
|
+
parsedStatus: typeof parsed?.status === 'string' ? parsed.status : (parseError ? 'parse_error' : 'unknown'),
|
|
1248
|
+
parseError: parseError || undefined,
|
|
1249
|
+
finalAssistantPresent: evidence.present,
|
|
1250
|
+
finalAssistantEvidenceSource: evidence.source,
|
|
1251
|
+
visibleMessageCount: visibleMessages.length,
|
|
1252
|
+
lastVisibleRole,
|
|
1253
|
+
lastVisibleKind,
|
|
1254
|
+
lastVisibleContentLength,
|
|
1255
|
+
pendingStartedAt: this.generatingStartedAt || null,
|
|
1256
|
+
pendingFirstObservedAt: args.pending.firstObservedAt,
|
|
1257
|
+
pendingTimestamp: args.pending.timestamp,
|
|
1258
|
+
pendingDurationSec: args.pending.duration,
|
|
1259
|
+
previousBlockReason: args.pending.loggedBlockReason || null,
|
|
1260
|
+
transcriptProbeHistory: args.pending.transcriptProbeHistory || [],
|
|
1261
|
+
};
|
|
744
1262
|
}
|
|
745
1263
|
|
|
746
1264
|
private hasAdapterPendingResponse(): boolean {
|
|
@@ -765,32 +1283,97 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
765
1283
|
if (!isCliGeneratingLikeStatus(parsedRawStatus)) return false;
|
|
766
1284
|
if (adapterRawStatus !== 'idle') return false;
|
|
767
1285
|
if (hasNonEmptyCliModalButtons(parsedStatus?.activeModal ?? parsedStatus?.modal)) return false;
|
|
768
|
-
|
|
1286
|
+
if (this.hasAdapterPendingResponse()) return false;
|
|
1287
|
+
// Do not suppress when the adapter's raw response buffer is still non-empty.
|
|
1288
|
+
// This catches the case where isWaitingForResponse has already flipped to false
|
|
1289
|
+
// (so getPartialResponse() returns '') but the provider's native parser still
|
|
1290
|
+
// reports generating because it's parsing buffered content. Suppressing the
|
|
1291
|
+
// finalization block here would emit a false completion event while the provider
|
|
1292
|
+
// session is still actively processing its response stream.
|
|
1293
|
+
const adapterAny = this.adapter as any;
|
|
1294
|
+
if (typeof adapterAny?.responseBuffer === 'string' && adapterAny.responseBuffer.trim()) return false;
|
|
1295
|
+
return true;
|
|
769
1296
|
}
|
|
770
1297
|
|
|
771
|
-
private
|
|
772
|
-
if (latestVisibleStatus !== 'idle') return `status:${latestVisibleStatus}
|
|
1298
|
+
private getCompletedFinalizationBlock(latestVisibleStatus: string, pending: CompletedDebouncePending): CompletedFinalizationBlock | null {
|
|
1299
|
+
if (latestVisibleStatus !== 'idle') return { reason: `status:${latestVisibleStatus}`, terminal: true };
|
|
773
1300
|
|
|
774
1301
|
const adapterAny = this.adapter as any;
|
|
775
|
-
|
|
776
|
-
if (
|
|
1302
|
+
const approvalResolvedIdle = pending.previousStatus === 'waiting_approval';
|
|
1303
|
+
if (!approvalResolvedIdle) {
|
|
1304
|
+
if (adapterAny?.isWaitingForResponse === true) return { reason: 'adapter_waiting_for_response', terminal: true };
|
|
1305
|
+
if (adapterAny?.currentTurnScope) return { reason: 'adapter_turn_scope_active', terminal: true };
|
|
1306
|
+
if (this.hasAdapterPendingResponse()) return { reason: 'adapter_pending_response', terminal: true };
|
|
1307
|
+
}
|
|
777
1308
|
|
|
778
1309
|
const partial = typeof this.adapter.getPartialResponse === 'function'
|
|
779
1310
|
? this.adapter.getPartialResponse()
|
|
780
1311
|
: '';
|
|
781
|
-
if (typeof partial === 'string' && partial.trim()) return 'partial_response_pending';
|
|
1312
|
+
if (typeof partial === 'string' && partial.trim()) return { reason: 'partial_response_pending', terminal: true };
|
|
782
1313
|
|
|
783
1314
|
let parsed: any;
|
|
784
1315
|
try {
|
|
785
1316
|
parsed = this.adapter.getScriptParsedStatus();
|
|
786
1317
|
} catch (error: any) {
|
|
787
|
-
return `parse_error:${error?.message || String(error)}
|
|
1318
|
+
return { reason: `parse_error:${error?.message || String(error)}` };
|
|
788
1319
|
}
|
|
789
1320
|
|
|
790
1321
|
const parsedStatus = typeof parsed?.status === 'string' ? parsed.status : 'unknown';
|
|
791
|
-
if (parsedStatus !== 'idle')
|
|
792
|
-
|
|
793
|
-
|
|
1322
|
+
if (parsedStatus !== 'idle') {
|
|
1323
|
+
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
1324
|
+
if (this.shouldSuppressStaleParsedBusyStatus(parsed, adapterStatus)) return null;
|
|
1325
|
+
return { reason: `parsed_status:${parsedStatus}`, terminal: isCliGeneratingLikeStatus(parsedStatus) };
|
|
1326
|
+
}
|
|
1327
|
+
if (parsed?.activeModal || parsed?.modal) return { reason: 'parsed_modal_active', terminal: true };
|
|
1328
|
+
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1329
|
+
const finalAssistantEvidence = this.completionFinalAssistantEvidence(parsed?.messages);
|
|
1330
|
+
const allowMissingAssistantTimeout = !!(this.settings.meshNodeFor || this.settings.meshActiveTaskId || this.settings.launchedByCoordinator);
|
|
1331
|
+
LOG.debug('CLI', `[${this.type}] finalAssistantEvidence: present=${finalAssistantEvidence.present} source=${finalAssistantEvidence.source} adapterOwnsMessagesElsewhere=${adapterOwnsMessagesElsewhere} parsedStatus=${parsedStatus}`);
|
|
1332
|
+
if (!finalAssistantEvidence.present) {
|
|
1333
|
+
if (adapterOwnsMessagesElsewhere) {
|
|
1334
|
+
if (finalAssistantEvidence.source === 'external-native') {
|
|
1335
|
+
const probe = this.recordPendingTranscriptProbe(pending);
|
|
1336
|
+
if (probe && !pending.loggedTranscriptProbe) {
|
|
1337
|
+
LOG.info('CLI', `[${this.type}] external transcript probe: msgCount=${probe.msgCount} lastRole=${probe.lastRole || 'none'} lastKind=${probe.lastKind || 'none'} contentLen=${probe.contentLen} sourceMtime=${probe.sourceMtimeMs ?? 'unknown'} mtimeAge=${probe.mtimeAgeMs ?? 'unknown'}ms`);
|
|
1338
|
+
pending.loggedTranscriptProbe = true;
|
|
1339
|
+
}
|
|
1340
|
+
LOG.debug('CLI', `[${this.type}] external-native probe result: lastRole=${probe?.lastRole} contentLen=${probe?.contentLen}`);
|
|
1341
|
+
if (probe?.lastRole === 'assistant' && (probe.contentLen ?? 0) > 0) {
|
|
1342
|
+
return null;
|
|
1343
|
+
}
|
|
1344
|
+
if (this.type === 'antigravity-cli') {
|
|
1345
|
+
return null;
|
|
1346
|
+
}
|
|
1347
|
+
return { reason: 'missing_final_assistant', terminal: true, allowTimeout: allowMissingAssistantTimeout };
|
|
1348
|
+
}
|
|
1349
|
+
if ((this.provider as any).requiresFinalAssistantBeforeIdle === true) {
|
|
1350
|
+
return { reason: 'missing_final_assistant', terminal: true, allowTimeout: allowMissingAssistantTimeout };
|
|
1351
|
+
}
|
|
1352
|
+
} else {
|
|
1353
|
+
LOG.debug('CLI', `[${this.type}] missing_final_assistant (not ownsExternal) requiresFinalAssistant=${!!(this.provider as any).requiresFinalAssistantBeforeIdle}`);
|
|
1354
|
+
return {
|
|
1355
|
+
reason: 'missing_final_assistant',
|
|
1356
|
+
terminal: (this.provider as any).requiresFinalAssistantBeforeIdle === true,
|
|
1357
|
+
allowTimeout: allowMissingAssistantTimeout,
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// Guard: if the screen still shows an approval/choice prompt as the last visible text,
|
|
1363
|
+
// the turn is not complete even if the parsed status says idle and there is an assistant
|
|
1364
|
+
// message. This catches the case where waiting_approval→idle transitions occur before
|
|
1365
|
+
// the modal has been resolved (e.g. the PTY rendered the prompt but no button press fired).
|
|
1366
|
+
try {
|
|
1367
|
+
const screenText = typeof (this.adapter as any).getScreenText === 'function'
|
|
1368
|
+
? String((this.adapter as any).getScreenText() || '')
|
|
1369
|
+
: '';
|
|
1370
|
+
if (screenText) {
|
|
1371
|
+
const tailLines = screenText.split(/\r?\n/).slice(-16).join('\n');
|
|
1372
|
+
if (looksLikeActiveApprovalPromptText(tailLines)) {
|
|
1373
|
+
return { reason: 'screen_shows_approval_prompt', terminal: approvalResolvedIdle };
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
} catch { /* defensive: screen text read is best-effort */ }
|
|
794
1377
|
|
|
795
1378
|
return null;
|
|
796
1379
|
}
|
|
@@ -809,7 +1392,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
809
1392
|
|
|
810
1393
|
const latestStatus = this.adapter.getStatus({ allowParse: false });
|
|
811
1394
|
const latestAutoApproveActive = latestStatus.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
812
|
-
const latestVisibleStatus = latestAutoApproveActive ? 'generating' : latestStatus.status;
|
|
1395
|
+
const latestVisibleStatus = latestAutoApproveActive || this.autoApproveBusy ? 'generating' : latestStatus.status;
|
|
1396
|
+
LOG.debug('CLI', `[${this.type}] flush attempt: adapterStatus=${latestStatus.status} latestVisible=${latestVisibleStatus} generatingStartedAt=${this.generatingStartedAt} isWaitingForResponse=${!!(this.adapter as any)?.isWaitingForResponse} hasPartial=${!!this.adapter.getPartialResponse?.()}`);
|
|
813
1397
|
if (latestVisibleStatus !== 'idle') {
|
|
814
1398
|
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed ${latestVisibleStatus})`);
|
|
815
1399
|
this.completedDebouncePending = null;
|
|
@@ -817,10 +1401,12 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
817
1401
|
return;
|
|
818
1402
|
}
|
|
819
1403
|
|
|
820
|
-
const
|
|
821
|
-
if (
|
|
1404
|
+
const block = this.getCompletedFinalizationBlock(latestVisibleStatus, pending);
|
|
1405
|
+
if (block) {
|
|
1406
|
+
const blockReason = block.reason;
|
|
822
1407
|
const waitedMs = Date.now() - pending.firstObservedAt;
|
|
823
|
-
|
|
1408
|
+
LOG.debug('CLI', `[${this.type}] finalization block: reason=${blockReason} terminal=${block.terminal} waitedMs=${waitedMs} maxWait=${COMPLETED_FINALIZATION_MAX_WAIT_MS}`);
|
|
1409
|
+
if ((block.terminal && !block.allowTimeout) || waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
|
|
824
1410
|
if (pending.loggedBlockReason !== blockReason) {
|
|
825
1411
|
LOG.info('CLI', `[${this.type}] waiting to emit completed until transcript finalizes (${blockReason})`);
|
|
826
1412
|
pending.loggedBlockReason = blockReason;
|
|
@@ -828,10 +1414,36 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
828
1414
|
this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
|
|
829
1415
|
return;
|
|
830
1416
|
}
|
|
831
|
-
|
|
1417
|
+
const completionDiagnostic = this.buildCompletedFinalizationDiagnostic({
|
|
1418
|
+
blockReason,
|
|
1419
|
+
latestStatus,
|
|
1420
|
+
latestVisibleStatus,
|
|
1421
|
+
waitedMs,
|
|
1422
|
+
pending,
|
|
1423
|
+
emittedAfterFinalizationTimeout: true,
|
|
1424
|
+
});
|
|
1425
|
+
LOG.warn('CLI', `[${this.type}] emitting completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
|
|
1426
|
+
this.pushEvent({
|
|
1427
|
+
event: 'agent:generating_completed',
|
|
1428
|
+
chatTitle: pending.chatTitle,
|
|
1429
|
+
duration: pending.duration,
|
|
1430
|
+
timestamp: pending.timestamp,
|
|
1431
|
+
// When finalization is forced past the timeout on a `parsed_status:` block
|
|
1432
|
+
// (the parser never confirmed a final assistant turn) we previously rode an
|
|
1433
|
+
// empty `finalSummary` unconditionally. That empty value propagates to the
|
|
1434
|
+
// mesh coordinator's mirror preview (meshSessionLastMessagePreview), leaving a
|
|
1435
|
+
// delegated session's inbox preview blank — or, for a LOCAL worktree session,
|
|
1436
|
+
// stuck on the dispatched user task. If the parser DID surface assistant text,
|
|
1437
|
+
// prefer it; only fall back to '' when no assistant summary can be derived.
|
|
1438
|
+
finalSummary: blockReason.startsWith('parsed_status:')
|
|
1439
|
+
? (this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages) ?? '')
|
|
1440
|
+
: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
|
|
1441
|
+
completionDiagnostic,
|
|
1442
|
+
});
|
|
832
1443
|
this.completedDebouncePending = null;
|
|
833
1444
|
this.completedDebounceTimer = null;
|
|
834
1445
|
this.generatingStartedAt = 0;
|
|
1446
|
+
this.lastApprovalEventFingerprint = '';
|
|
835
1447
|
return;
|
|
836
1448
|
}
|
|
837
1449
|
|
|
@@ -841,11 +1453,12 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
841
1453
|
chatTitle: pending.chatTitle,
|
|
842
1454
|
duration: pending.duration,
|
|
843
1455
|
timestamp: pending.timestamp,
|
|
844
|
-
finalSummary:
|
|
1456
|
+
finalSummary: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
|
|
845
1457
|
});
|
|
846
1458
|
this.completedDebouncePending = null;
|
|
847
1459
|
this.completedDebounceTimer = null;
|
|
848
1460
|
this.generatingStartedAt = 0;
|
|
1461
|
+
this.lastApprovalEventFingerprint = '';
|
|
849
1462
|
}
|
|
850
1463
|
|
|
851
1464
|
private maybeAutoApproveStatus(adapterStatus: any, now = Date.now()): boolean {
|
|
@@ -853,48 +1466,168 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
853
1466
|
// Guard re-entry: onStatusChange/getState can observe the same modal multiple
|
|
854
1467
|
// times while the PTY absorbs the approval key. Without this flag, repeated
|
|
855
1468
|
// snapshots would write stray keys into the input once the modal dismisses.
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
this.
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
1469
|
+
// However, Claude Code can present a second approval immediately after the
|
|
1470
|
+
// first. Resolve a changed modal signature even while the previous write is
|
|
1471
|
+
// still inside the short busy window.
|
|
1472
|
+
if (!autoApproveActive) {
|
|
1473
|
+
this.lastAutoApprovalSignature = '';
|
|
1474
|
+
// Clear the settle gate so the next approval starts its own quiet
|
|
1475
|
+
// window from scratch (a stale timestamp would let it fire instantly).
|
|
1476
|
+
this.pendingAutoApprovalSignature = '';
|
|
1477
|
+
this.pendingAutoApprovalSince = 0;
|
|
1478
|
+
if (this.autoApproveSettleTimer) { clearTimeout(this.autoApproveSettleTimer); this.autoApproveSettleTimer = null; }
|
|
1479
|
+
return autoApproveActive;
|
|
1480
|
+
}
|
|
1481
|
+
const modal = adapterStatus.activeModal;
|
|
1482
|
+
// (fix) Do not auto-approve when no concrete modal/buttons are present.
|
|
1483
|
+
// Claude TUI flaps between paints; without this guard adapterStatus
|
|
1484
|
+
// could report status=waiting_approval with activeModal=null (or with
|
|
1485
|
+
// an empty buttons array briefly) and we'd still call
|
|
1486
|
+
// resolveModal(-1) — which used to type "1" into the prompt
|
|
1487
|
+
// repeatedly. Skip until a real modal is captured.
|
|
1488
|
+
const buttons = Array.isArray(modal?.buttons)
|
|
1489
|
+
? modal.buttons.map((b: any) => String(b || '').trim()).filter(Boolean)
|
|
1490
|
+
: [];
|
|
1491
|
+
if (!modal || buttons.length === 0) {
|
|
1492
|
+
return autoApproveActive;
|
|
1493
|
+
}
|
|
1494
|
+
const { index: buttonIndex, label: buttonLabel } = pickAutoApprovalButton(buttons);
|
|
1495
|
+
if (buttonIndex < 0) {
|
|
1496
|
+
// No concrete button matched — don't pick a random index, just
|
|
1497
|
+
// surface the modal so the user can decide.
|
|
1498
|
+
return autoApproveActive;
|
|
1499
|
+
}
|
|
1500
|
+
// Include the FSM's approval entry seq: two distinct back-to-back
|
|
1501
|
+
// approvals can carry identical message/buttons (common with
|
|
1502
|
+
// claude-cli). Without the seq their signatures collide and the 5s
|
|
1503
|
+
// busy-window re-entry guard below swallows the second auto-approve,
|
|
1504
|
+
// leaving it stuck. The seq is bumped by the FSM on every fresh
|
|
1505
|
+
// waiting_approval entry, so a new approval always yields a new
|
|
1506
|
+
// signature and fires through.
|
|
1507
|
+
const approvalEntrySeq = typeof adapterStatus?.approvalEntrySeq === 'number'
|
|
1508
|
+
? adapterStatus.approvalEntrySeq
|
|
1509
|
+
: 0;
|
|
1510
|
+
const signature = [
|
|
1511
|
+
approvalEntrySeq,
|
|
1512
|
+
typeof modal?.message === 'string' ? modal.message.trim() : '',
|
|
1513
|
+
buttons.join('|'),
|
|
1514
|
+
buttonIndex,
|
|
1515
|
+
].join('::');
|
|
1516
|
+
// Already fired for this exact modal and still inside the busy window —
|
|
1517
|
+
// nothing to do (re-entry guard for repeated snapshots of one modal).
|
|
1518
|
+
if (this.autoApproveBusy && signature === this.lastAutoApprovalSignature) {
|
|
1519
|
+
return autoApproveActive;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
// Settle gate: only fire once this exact signature has been stable for
|
|
1523
|
+
// AUTO_APPROVE_SETTLE_MS. A still-streaming prompt mutates its
|
|
1524
|
+
// message/buttons each frame → new signature → clock restarts, so we
|
|
1525
|
+
// never approve a half-rendered prompt (the "resolves too fast" bug).
|
|
1526
|
+
if (signature !== this.pendingAutoApprovalSignature) {
|
|
1527
|
+
this.pendingAutoApprovalSignature = signature;
|
|
1528
|
+
this.pendingAutoApprovalSince = now;
|
|
1529
|
+
}
|
|
1530
|
+
const settledForMs = now - this.pendingAutoApprovalSince;
|
|
1531
|
+
if (settledForMs < CliProviderInstance.AUTO_APPROVE_SETTLE_MS) {
|
|
1532
|
+
// Not yet settled. Arm a timer to re-check after the remaining quiet
|
|
1533
|
+
// window — the PTY may go silent once the prompt finishes painting,
|
|
1534
|
+
// so there is no guaranteed status-change frame to re-drive us.
|
|
1535
|
+
if (this.autoApproveSettleTimer) clearTimeout(this.autoApproveSettleTimer);
|
|
1536
|
+
this.autoApproveSettleTimer = setTimeout(() => {
|
|
1537
|
+
this.autoApproveSettleTimer = null;
|
|
1538
|
+
this.recheckAutoApproveSettled();
|
|
1539
|
+
}, CliProviderInstance.AUTO_APPROVE_SETTLE_MS - settledForMs + 20);
|
|
1540
|
+
return autoApproveActive;
|
|
869
1541
|
}
|
|
1542
|
+
|
|
1543
|
+
// Settled — fire the approve key.
|
|
1544
|
+
if (this.autoApproveSettleTimer) { clearTimeout(this.autoApproveSettleTimer); this.autoApproveSettleTimer = null; }
|
|
1545
|
+
this.autoApproveBusy = true;
|
|
1546
|
+
this.lastAutoApprovalSignature = signature;
|
|
1547
|
+
this.pendingAutoApprovalSignature = '';
|
|
1548
|
+
this.pendingAutoApprovalSince = 0;
|
|
1549
|
+
if (this.autoApproveBusyTimer) clearTimeout(this.autoApproveBusyTimer);
|
|
1550
|
+
this.autoApproveBusyTimer = setTimeout(() => {
|
|
1551
|
+
this.autoApproveBusy = false;
|
|
1552
|
+
this.autoApproveBusyTimer = null;
|
|
1553
|
+
this.lastAutoApprovalSignature = '';
|
|
1554
|
+
}, 5000);
|
|
1555
|
+
this.recordAutoApproval(modal?.message, buttonLabel, now);
|
|
1556
|
+
setTimeout(() => {
|
|
1557
|
+
this.adapter.resolveModal(buttonIndex);
|
|
1558
|
+
}, 0);
|
|
870
1559
|
return autoApproveActive;
|
|
871
1560
|
}
|
|
872
1561
|
|
|
1562
|
+
/**
|
|
1563
|
+
* Re-drive the auto-approve check after the settle quiet window elapses.
|
|
1564
|
+
* The PTY may have gone silent once the approval prompt finished painting,
|
|
1565
|
+
* so no status-change frame is guaranteed to re-enter maybeAutoApproveStatus
|
|
1566
|
+
* — this timer-driven re-check picks up the now-settled modal and fires.
|
|
1567
|
+
* Deliberately lighter than detectStatusTransition(): it only re-evaluates
|
|
1568
|
+
* the approval decision; the next real PTY frame refreshes visible status.
|
|
1569
|
+
*/
|
|
1570
|
+
private recheckAutoApproveSettled(): void {
|
|
1571
|
+
try {
|
|
1572
|
+
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
1573
|
+
this.maybeAutoApproveStatus(adapterStatus, Date.now());
|
|
1574
|
+
} catch { /* adapter gone / transient — next frame retries */ }
|
|
1575
|
+
}
|
|
1576
|
+
|
|
873
1577
|
private detectStatusTransition(): void {
|
|
874
1578
|
const now = Date.now();
|
|
875
1579
|
// Status-change handling is a hot path: PTY output can fire it many times
|
|
876
1580
|
// during long-running CLI sessions. Keep this path on adapter-owned light
|
|
877
1581
|
// state only; rich provider parsing is reserved for getState/read_chat.
|
|
878
1582
|
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
1583
|
+
const adapterProviderSessionId = normalizeProviderSessionId(
|
|
1584
|
+
this.provider,
|
|
1585
|
+
typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
|
|
1586
|
+
);
|
|
1587
|
+
if (adapterProviderSessionId) {
|
|
1588
|
+
this.promoteProviderSessionId(adapterProviderSessionId);
|
|
1589
|
+
}
|
|
879
1590
|
const parsedStatus = null;
|
|
880
1591
|
const rawStatus = adapterStatus.status;
|
|
881
1592
|
const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, now);
|
|
882
|
-
|
|
883
|
-
|
|
1593
|
+
// During the autoApproveBusy window (2s after firing approval key), the PTY
|
|
1594
|
+
// can briefly report 'idle' before the next generating phase starts. Treat that
|
|
1595
|
+
// transient idle as 'generating' to suppress a spurious agent:generating_completed
|
|
1596
|
+
// push notification. The adapter's status is otherwise authoritative — native
|
|
1597
|
+
// transcript shape does NOT override the FSM's busy/idle decision.
|
|
1598
|
+
const autoApproveHoldIdle = this.autoApproveBusy && rawStatus === 'idle';
|
|
1599
|
+
const newStatus = autoApproveActive || autoApproveHoldIdle ? 'generating' : rawStatus;
|
|
1600
|
+
const dirName = workingDirBasename(this.workingDir);
|
|
884
1601
|
const chatTitle = `${this.provider.name} · ${dirName}`;
|
|
885
1602
|
const partial = this.adapter.getPartialResponse();
|
|
1603
|
+
// Liveness fingerprint for the no-progress watchdog. The parsed
|
|
1604
|
+
// assistant buffer (`partial`) alone goes static while a tool/build runs
|
|
1605
|
+
// — the assistant emits no tokens even though the PTY is actively
|
|
1606
|
+
// printing tool output — which made the watchdog false-fire a "stuck"
|
|
1607
|
+
// alert mid-turn. Fold in the adapter's raw-activity timestamps so any
|
|
1608
|
+
// visible terminal progress (lastScreenChangeAt) or raw PTY byte
|
|
1609
|
+
// (lastOutputAt) keeps the fingerprint moving. The watchdog then only
|
|
1610
|
+
// survives a genuine stall where nothing at all is happening.
|
|
886
1611
|
const progressFingerprint = newStatus === 'generating'
|
|
887
|
-
? `${partial || ''}`.slice(-2000)
|
|
1612
|
+
? `${`${partial || ''}`.slice(-2000)}::scr=${adapterStatus.lastScreenChangeAt ?? 0}::out=${adapterStatus.lastOutputAt ?? 0}`
|
|
888
1613
|
: undefined;
|
|
889
1614
|
|
|
890
1615
|
const previousStatus = this.lastStatus;
|
|
891
1616
|
if (newStatus !== this.lastStatus) {
|
|
892
1617
|
LOG.info('CLI', `[${this.type}] status: ${this.lastStatus} → ${newStatus}`);
|
|
893
1618
|
if (this.lastStatus === 'idle' && newStatus === 'generating') {
|
|
1619
|
+
// If a completion event is already pending and the turn has ended
|
|
1620
|
+
// (generatingStartedAt===0), the PTY is painting its prompt area
|
|
1621
|
+
// after completing. Ignore this blip — do not cancel the pending
|
|
1622
|
+
// completion and do not advance lastStatus to generating.
|
|
1623
|
+
if (this.completedDebouncePending && this.generatingStartedAt === 0) {
|
|
1624
|
+
LOG.debug('CLI', `[${this.type}] ignoring post-completion PTY generating blip (generatingStartedAt=0)`);
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
894
1627
|
this.suppressIdleHistoryReplay = false;
|
|
895
1628
|
// Cancel any pending completed event (multi-step: idle→generating resume)
|
|
896
1629
|
if (this.completedDebouncePending) {
|
|
897
|
-
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed generating)`);
|
|
1630
|
+
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed generating) generatingStartedAt=${this.generatingStartedAt} isWaitingForResponse=${!!(this.adapter as any)?.isWaitingForResponse}`);
|
|
898
1631
|
if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
|
|
899
1632
|
this.completedDebouncePending = null;
|
|
900
1633
|
}
|
|
@@ -926,10 +1659,29 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
926
1659
|
if (!this.generatingStartedAt) this.generatingStartedAt = now;
|
|
927
1660
|
const modal = adapterStatus.activeModal;
|
|
928
1661
|
LOG.info('CLI', `[${this.type}] approval modal: "${modal?.message?.slice(0, 80) ?? 'none'}"`);
|
|
929
|
-
//
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1662
|
+
// Include the FSM's approval entry seq, mirroring the auto-approve
|
|
1663
|
+
// path (maybeAutoApproveStatus) and resolveModal's sameEntryReResolve
|
|
1664
|
+
// guard. Two distinct back-to-back approvals can carry identical
|
|
1665
|
+
// message/buttons (very common with claude-cli's "Allow Bash
|
|
1666
|
+
// command?"). Without the seq their fingerprints collide and the dedup
|
|
1667
|
+
// below silently drops the second waiting_approval event — it is never
|
|
1668
|
+
// emitted, so it cannot even land in the pending inbox for a later
|
|
1669
|
+
// read_chat reconcile to recover. The seq is bumped by the FSM on every
|
|
1670
|
+
// fresh waiting_approval entry, so a new approval always yields a new
|
|
1671
|
+
// fingerprint and emits.
|
|
1672
|
+
const approvalEntrySeq = typeof adapterStatus?.approvalEntrySeq === 'number'
|
|
1673
|
+
? adapterStatus.approvalEntrySeq
|
|
1674
|
+
: 0;
|
|
1675
|
+
const approvalFingerprint = JSON.stringify({
|
|
1676
|
+
message: typeof modal?.message === 'string' ? modal.message.trim() : '',
|
|
1677
|
+
buttons: Array.isArray(modal?.buttons) ? modal.buttons.map((button: unknown) => String(button).trim()) : [],
|
|
1678
|
+
seq: approvalEntrySeq,
|
|
1679
|
+
});
|
|
1680
|
+
// PTY redraws repeat the same modal content; fingerprint dedup prevents duplicate events.
|
|
1681
|
+
// Do NOT also gate on lastStatus: consecutive approvals can arrive waiting_approval→waiting_approval
|
|
1682
|
+
// (e.g. antigravity-cli resolves one prompt and immediately shows the next) and would be silently dropped.
|
|
1683
|
+
if (approvalFingerprint !== this.lastApprovalEventFingerprint) {
|
|
1684
|
+
this.lastApprovalEventFingerprint = approvalFingerprint;
|
|
933
1685
|
this.appendRuntimeSystemMessage(
|
|
934
1686
|
this.formatApprovalRequestMessage(modal?.message, modal?.buttons),
|
|
935
1687
|
`approval_request:${now}`,
|
|
@@ -941,22 +1693,113 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
941
1693
|
modalButtons: modal?.buttons,
|
|
942
1694
|
});
|
|
943
1695
|
}
|
|
1696
|
+
} else if (newStatus === 'generating' && this.lastStatus === 'waiting_approval') {
|
|
1697
|
+
// Approval resolved and the agent resumed work. Defense-in-depth:
|
|
1698
|
+
// clear the approval emit fingerprint here too (not only on
|
|
1699
|
+
// completion at scheduleCompletedDebounceFlush). A subsequent
|
|
1700
|
+
// waiting_approval with the same modal content as the one just
|
|
1701
|
+
// resolved would otherwise collide with the stale fingerprint and be
|
|
1702
|
+
// dropped. The seq in the fingerprint already separates entries; this
|
|
1703
|
+
// reset is a belt-and-suspenders guard for the re-entry case.
|
|
1704
|
+
this.lastApprovalEventFingerprint = '';
|
|
944
1705
|
} else if (newStatus === 'idle' && (this.lastStatus === 'generating' || this.lastStatus === 'waiting_approval')) {
|
|
945
1706
|
const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : 0;
|
|
946
|
-
//
|
|
1707
|
+
// Guard: if generatingStartedAt===0 and no debounce pending, the generating phase
|
|
1708
|
+
// was entered from 'starting' state (startup PTY noise), not from a real idle→generating
|
|
1709
|
+
// task dispatch. The idle→generating handler is the only code path that sets
|
|
1710
|
+
// generatingStartedAt and generatingDebouncePending, so both being absent means no
|
|
1711
|
+
// task was ever dispatched. Suppress the spurious completion event and fall through
|
|
1712
|
+
// to a simple lastStatus update.
|
|
1713
|
+
if (!this.generatingStartedAt && !this.generatingDebouncePending) {
|
|
1714
|
+
LOG.debug('CLI', `[${this.type}] suppressed startup-phase generating→idle blip (generatingStartedAt=0, no debounce pending)`);
|
|
1715
|
+
} else
|
|
1716
|
+
// If debounce still pending (generating lasted < 1s), cancel both UI events.
|
|
1717
|
+
// Still emit agent:generating_completed so mesh orchestration can record
|
|
1718
|
+
// task_completed for direct dispatches that complete faster than the debounce.
|
|
947
1719
|
if (this.generatingDebouncePending) {
|
|
948
|
-
|
|
1720
|
+
const shortDurationMs = this.generatingStartedAt ? now - this.generatingStartedAt : 0;
|
|
1721
|
+
LOG.info('CLI', `[${this.type}] suppressed short generating (${shortDurationMs}ms)`);
|
|
949
1722
|
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
1723
|
+
// Emit completion for mesh task association even though the UI generating
|
|
1724
|
+
// started/completed pair is suppressed (too short for visible UI update).
|
|
1725
|
+
let shortFinalSummary: string | undefined;
|
|
1726
|
+
let shortEvidenceSource: CompletionFinalAssistantEvidence['source'] = 'unavailable';
|
|
1727
|
+
try {
|
|
1728
|
+
const parsedMessages = this.adapter?.getScriptParsedStatus()?.messages;
|
|
1729
|
+
const evidence = this.completionFinalAssistantEvidence(parsedMessages);
|
|
1730
|
+
shortEvidenceSource = evidence.source;
|
|
1731
|
+
shortFinalSummary = extractFinalSummaryFromMessages(evidence.messages as any);
|
|
1732
|
+
} catch { /* best-effort */ }
|
|
1733
|
+
// If a real response is confirmed, retroactively emit started so the chat
|
|
1734
|
+
// bubble appears even though the debounce suppressed the original event.
|
|
1735
|
+
if (shortFinalSummary) {
|
|
1736
|
+
this.pushEvent({ event: 'agent:generating_started', chatTitle, timestamp: now - shortDurationMs });
|
|
1737
|
+
}
|
|
950
1738
|
this.generatingDebouncePending = null;
|
|
951
1739
|
this.generatingStartedAt = 0;
|
|
1740
|
+
const missingEvidence = ((this.provider as any).requiresFinalAssistantBeforeIdle === true || shortEvidenceSource === 'external-native') && !shortFinalSummary;
|
|
1741
|
+
if (missingEvidence) {
|
|
1742
|
+
LOG.warn('CLI', `[${this.type}] short completion missing final assistant evidence (source=${shortEvidenceSource})`);
|
|
1743
|
+
}
|
|
1744
|
+
// When evidence is missing and there is no active mesh task context, suppress
|
|
1745
|
+
// the completion event. Providers with requiresFinalAssistantBeforeIdle or
|
|
1746
|
+
// external-native history must confirm a final assistant message before the
|
|
1747
|
+
// coordinator records task_completed. Only emit here if a mesh task is active
|
|
1748
|
+
// so the coordinator can apply its own timeout/retry logic.
|
|
1749
|
+
const hasMeshContext = !!(this.settings.meshNodeFor || this.settings.meshActiveTaskId || this.settings.launchedByCoordinator);
|
|
1750
|
+
if (missingEvidence && !hasMeshContext) {
|
|
1751
|
+
LOG.info('CLI', `[${this.type}] short completion suppressed: missing final assistant evidence, no mesh context (source=${shortEvidenceSource})`);
|
|
1752
|
+
// completedDebouncePending intentionally left null — the session is now idle
|
|
1753
|
+
// with no confirmed turn, matching the startup-blip suppression semantics.
|
|
1754
|
+
} else {
|
|
1755
|
+
this.pushEvent({
|
|
1756
|
+
event: 'agent:generating_completed',
|
|
1757
|
+
chatTitle,
|
|
1758
|
+
duration: 0,
|
|
1759
|
+
timestamp: now,
|
|
1760
|
+
finalSummary: shortFinalSummary,
|
|
1761
|
+
completionDiagnostic: {
|
|
1762
|
+
reason: 'short_generating_suppressed',
|
|
1763
|
+
shortDurationMs,
|
|
1764
|
+
finalAssistantEvidenceSource: shortEvidenceSource,
|
|
1765
|
+
...(missingEvidence ? { blockReason: 'missing_final_assistant' } : {}),
|
|
1766
|
+
},
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
952
1769
|
} else {
|
|
953
1770
|
// Debounce completed, then require the rich transcript path that read_chat
|
|
954
1771
|
// uses to show an idle turn whose last user-facing message is assistant.
|
|
955
|
-
this.completedDebouncePending = {
|
|
956
|
-
|
|
1772
|
+
this.completedDebouncePending = {
|
|
1773
|
+
chatTitle,
|
|
1774
|
+
duration,
|
|
1775
|
+
timestamp: now,
|
|
1776
|
+
firstObservedAt: now,
|
|
1777
|
+
previousStatus: this.lastStatus,
|
|
1778
|
+
};
|
|
1779
|
+
const ownsExternalHistory = !!(this.adapter as any)?.chatMessagesOwnedExternally;
|
|
1780
|
+
const flushDelay = ownsExternalHistory ? 0 : 3000;
|
|
1781
|
+
LOG.debug('CLI', `[${this.type}] set completedDebouncePending duration=${duration}s ownsExternalHistory=${ownsExternalHistory} flushDelay=${flushDelay}ms generatingStartedAt=${this.generatingStartedAt}`);
|
|
1782
|
+
this.scheduleCompletedDebounceFlush(flushDelay);
|
|
957
1783
|
}
|
|
958
1784
|
} else if (newStatus === 'idle' && this.lastStatus === 'starting') {
|
|
959
1785
|
this.pushEvent({ event: 'agent:ready', chatTitle, timestamp: now });
|
|
1786
|
+
} else if (newStatus === 'error') {
|
|
1787
|
+
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
1788
|
+
this.generatingDebouncePending = null;
|
|
1789
|
+
if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
|
|
1790
|
+
this.completedDebouncePending = null;
|
|
1791
|
+
this.errorMessage = adapterStatus.errorMessage || this.errorMessage;
|
|
1792
|
+
this.errorReason = (adapterStatus.errorReason as ProviderErrorReason) || this.errorReason;
|
|
1793
|
+
this.pushEvent({
|
|
1794
|
+
event: 'agent:stopped',
|
|
1795
|
+
chatTitle,
|
|
1796
|
+
timestamp: now,
|
|
1797
|
+
finalSummary: adapterStatus.errorMessage || adapterStatus.errorReason || 'Provider reported an error',
|
|
1798
|
+
completionDiagnostic: {
|
|
1799
|
+
reason: adapterStatus.errorReason || 'provider_error',
|
|
1800
|
+
errorMessage: adapterStatus.errorMessage || undefined,
|
|
1801
|
+
},
|
|
1802
|
+
});
|
|
960
1803
|
} else if (newStatus === 'stopped') {
|
|
961
1804
|
// Cancel any pending debounce
|
|
962
1805
|
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
@@ -976,8 +1819,39 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
976
1819
|
|
|
977
1820
|
// Monitor check (cooldown based notification, IDE/CLI common)
|
|
978
1821
|
const agentKey = `${this.type}:cli`;
|
|
979
|
-
|
|
1822
|
+
// Approval pending is detected from the raw adapter status, not `newStatus`:
|
|
1823
|
+
// auto-approve synthesizes `waiting_approval` → 'generating', which would
|
|
1824
|
+
// otherwise let the no-progress watchdog accumulate the approval wait.
|
|
1825
|
+
const approvalPending = rawStatus === 'waiting_approval';
|
|
1826
|
+
const monitorEvents = this.monitor.check(agentKey, newStatus, now, progressFingerprint, approvalPending);
|
|
1827
|
+
const monitorParsedStatus: any = parsedStatus;
|
|
980
1828
|
for (const me of monitorEvents) {
|
|
1829
|
+
if (
|
|
1830
|
+
me.type === 'monitor:no_progress'
|
|
1831
|
+
&& this.completionHasFinalAssistantMessage(monitorParsedStatus?.messages)
|
|
1832
|
+
&& !this.hasAdapterPendingResponse()
|
|
1833
|
+
&& !hasNonEmptyCliModalButtons(monitorParsedStatus?.activeModal ?? monitorParsedStatus?.modal)
|
|
1834
|
+
) {
|
|
1835
|
+
this.pushEvent({
|
|
1836
|
+
event: 'agent:generating_completed',
|
|
1837
|
+
chatTitle,
|
|
1838
|
+
duration: this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : undefined,
|
|
1839
|
+
timestamp: me.timestamp,
|
|
1840
|
+
finalSummary: extractFinalSummaryFromMessages(monitorParsedStatus?.messages),
|
|
1841
|
+
completionDiagnostic: {
|
|
1842
|
+
providerType: this.type,
|
|
1843
|
+
sessionId: this.instanceId,
|
|
1844
|
+
providerSessionId: this.providerSessionId || null,
|
|
1845
|
+
reconciliationReason: 'no_progress_monitor_final_summary',
|
|
1846
|
+
finalAssistantPresent: true,
|
|
1847
|
+
},
|
|
1848
|
+
});
|
|
1849
|
+
this.generatingStartedAt = 0;
|
|
1850
|
+
// Cancel any pending debounce flush — monitor already fired completion.
|
|
1851
|
+
if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
|
|
1852
|
+
this.completedDebouncePending = null;
|
|
1853
|
+
continue;
|
|
1854
|
+
}
|
|
981
1855
|
this.pushEvent({ event: me.type, agentKey: me.agentKey, message: me.message, elapsedSec: me.elapsedSec, timestamp: me.timestamp });
|
|
982
1856
|
}
|
|
983
1857
|
}
|
|
@@ -997,15 +1871,32 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
997
1871
|
workspaceName: typeof event.workspaceName === 'string' && event.workspaceName.trim()
|
|
998
1872
|
? event.workspaceName
|
|
999
1873
|
: this.workingDir,
|
|
1874
|
+
// Carry the workspace under BOTH `workspace` and `workspaceName` so the
|
|
1875
|
+
// downstream mesh forward/merge path — which reads `workspace` — can
|
|
1876
|
+
// propagate it to the coordinator snapshot. Without `workspace` the live
|
|
1877
|
+
// event path delivers an empty workspace and the dashboard falls back to
|
|
1878
|
+
// the generic "Terminal (Mesh Node)" title.
|
|
1879
|
+
workspace: typeof event.workspace === 'string' && event.workspace.trim()
|
|
1880
|
+
? event.workspace
|
|
1881
|
+
: this.workingDir,
|
|
1000
1882
|
providerSessionId: typeof event.providerSessionId === 'string' && event.providerSessionId.trim()
|
|
1001
1883
|
? event.providerSessionId
|
|
1002
1884
|
: this.providerSessionId,
|
|
1003
1885
|
};
|
|
1004
1886
|
if (this.context?.emitProviderEvent) {
|
|
1005
1887
|
this.context.emitProviderEvent(enrichedEvent);
|
|
1006
|
-
|
|
1888
|
+
} else {
|
|
1889
|
+
this.events.push(enrichedEvent);
|
|
1890
|
+
}
|
|
1891
|
+
// Auto-detach a direct-dispatch mesh assignment once the dispatched
|
|
1892
|
+
// task reaches a terminal state. Leaving meshNodeFor pinned would
|
|
1893
|
+
// route this session's next unrelated turn (a dashboard chat) into
|
|
1894
|
+
// the coordinator as if it were the completion of another task.
|
|
1895
|
+
// We schedule after the emit so the originating coordinator still
|
|
1896
|
+
// observes the completion event with its routing marker intact.
|
|
1897
|
+
if (TERMINAL_MESH_EVENTS.has(event.event) && this.settings.meshActiveTaskId) {
|
|
1898
|
+
try { this.detachMeshAssignment(); } catch { /* best-effort */ }
|
|
1007
1899
|
}
|
|
1008
|
-
this.events.push(enrichedEvent);
|
|
1009
1900
|
}
|
|
1010
1901
|
|
|
1011
1902
|
private flushEvents(): ProviderEvent[] {
|
|
@@ -1238,7 +2129,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1238
2129
|
receivedAt: normalizedMessage.receivedAt || normalizedMessage.timestamp,
|
|
1239
2130
|
historyDedupKey: dedupKey,
|
|
1240
2131
|
}],
|
|
1241
|
-
this.adapter.getScriptParsedStatus?.()?.title || this.workingDir
|
|
2132
|
+
this.adapter.getScriptParsedStatus?.()?.title || workingDirBasename(this.workingDir),
|
|
1242
2133
|
this.instanceId,
|
|
1243
2134
|
this.providerSessionId,
|
|
1244
2135
|
);
|
|
@@ -1258,12 +2149,28 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1258
2149
|
index,
|
|
1259
2150
|
source: 'parsed',
|
|
1260
2151
|
}));
|
|
2152
|
+
const getRole = (message: ChatMessage): string => typeof message.role === 'string'
|
|
2153
|
+
? message.role.trim().toLowerCase()
|
|
2154
|
+
: '';
|
|
1261
2155
|
const runtimeEntries: MergeEntry[] = this.runtimeMessages.map((entry, index) => ({
|
|
1262
2156
|
message: entry.message,
|
|
1263
2157
|
index: parsedMessages.length + index,
|
|
1264
|
-
source: 'runtime',
|
|
2158
|
+
source: 'runtime' as const,
|
|
1265
2159
|
runtimeKey: entry.key,
|
|
1266
|
-
}))
|
|
2160
|
+
})).filter((entry) => {
|
|
2161
|
+
const meta = entry.message.meta && typeof entry.message.meta === 'object' && !Array.isArray(entry.message.meta)
|
|
2162
|
+
? entry.message.meta as Record<string, unknown>
|
|
2163
|
+
: {};
|
|
2164
|
+
if (meta.runtimeInputAck !== true) return true;
|
|
2165
|
+
const runtimeText = flattenContent(entry.message.content).replace(/\s+/g, ' ').trim();
|
|
2166
|
+
if (!runtimeText) return false;
|
|
2167
|
+
return !parsedEntries.some((parsedEntry) => {
|
|
2168
|
+
const parsedRole = getRole(parsedEntry.message);
|
|
2169
|
+
if (parsedRole !== 'user' && parsedRole !== 'human') return false;
|
|
2170
|
+
const parsedText = flattenContent(parsedEntry.message.content).replace(/\s+/g, ' ').trim();
|
|
2171
|
+
return parsedText === runtimeText;
|
|
2172
|
+
});
|
|
2173
|
+
});
|
|
1267
2174
|
const getTime = (message: ChatMessage): number => {
|
|
1268
2175
|
const value = typeof message.receivedAt === 'number'
|
|
1269
2176
|
? message.receivedAt
|
|
@@ -1273,9 +2180,6 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1273
2180
|
return Number.isFinite(value) && value > 0 ? value : 0;
|
|
1274
2181
|
};
|
|
1275
2182
|
|
|
1276
|
-
const getRole = (message: ChatMessage): string => typeof message.role === 'string'
|
|
1277
|
-
? message.role.trim().toLowerCase()
|
|
1278
|
-
: '';
|
|
1279
2183
|
const isRuntimeOverlay = (entry: MergeEntry): boolean => {
|
|
1280
2184
|
if (entry.source !== 'runtime') return false;
|
|
1281
2185
|
const key = typeof entry.runtimeKey === 'string' ? entry.runtimeKey.trim().toLowerCase() : '';
|
|
@@ -1341,7 +2245,9 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1341
2245
|
this.providerSessionId = nextSessionId;
|
|
1342
2246
|
this.historyWriter.promoteHistorySession(this.type, previousHistorySessionId, nextSessionId);
|
|
1343
2247
|
this.historyWriter.writeSessionStart(this.type, nextSessionId, this.workingDir, this.instanceId);
|
|
1344
|
-
this.
|
|
2248
|
+
if (this.shouldHydrateExistingProviderHistory()) {
|
|
2249
|
+
this.restorePersistedHistoryFromCurrentSession();
|
|
2250
|
+
}
|
|
1345
2251
|
this.adapter.updateRuntimeMeta({ providerSessionId: nextSessionId });
|
|
1346
2252
|
this.onProviderSessionResolved?.({
|
|
1347
2253
|
instanceId: this.instanceId,
|
|
@@ -1354,13 +2260,39 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1354
2260
|
LOG.info('CLI', `[${this.type}] discovered provider session id: ${nextSessionId}`);
|
|
1355
2261
|
}
|
|
1356
2262
|
|
|
1357
|
-
private
|
|
2263
|
+
private shouldHydrateExistingProviderHistory(): boolean {
|
|
2264
|
+
return this.launchMode === 'resume' || this.launchMode === 'manual';
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
private shouldSuppressFreshLaunchStartupReplay(parsedMessages: unknown[], parsedStatus: any, adapterStatus: any, parsedProviderSessionId = ''): boolean {
|
|
2268
|
+
if (this.launchMode !== 'new') return false;
|
|
2269
|
+
if (this.providerSessionId) return false;
|
|
2270
|
+
if (!Array.isArray(parsedMessages) || parsedMessages.length === 0) return false;
|
|
2271
|
+
if (!isIdleStatus(adapterStatus?.status) || !isIdleStatus(parsedStatus?.status)) return false;
|
|
2272
|
+
if (parsedProviderSessionId) return true;
|
|
2273
|
+
|
|
2274
|
+
const newestMessageAt = parsedMessages.reduce<number>((newest, message) => Math.max(newest, getMessageTime(message)), 0);
|
|
2275
|
+
|
|
2276
|
+
// Untimestamped idle parser output during a fresh launch is usually the
|
|
2277
|
+
// provider's last workspace transcript before a new turn exists.
|
|
2278
|
+
return newestMessageAt === 0;
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
private syncCanonicalSavedHistoryIfNeeded(options: { full?: boolean } = {}): boolean {
|
|
1358
2282
|
if (!this.providerSessionId) return false;
|
|
1359
|
-
const canonicalHistory = this.provider.
|
|
2283
|
+
const canonicalHistory = this.provider.nativeHistory;
|
|
1360
2284
|
if (!canonicalHistory) return false;
|
|
1361
2285
|
|
|
2286
|
+
// Per-status-report hydration reads only a bounded tail (snapshot needs at
|
|
2287
|
+
// most the newest 60). The once-per-resume restore path passes full:true
|
|
2288
|
+
// because seedSessionHistory needs the COMPLETE transcript to seed dedup
|
|
2289
|
+
// state. The read-cache key encodes the window so the bounded and full
|
|
2290
|
+
// reads don't share/clobber each other's 2s cache entry.
|
|
2291
|
+
const limit = options.full ? Number.MAX_SAFE_INTEGER : STATUS_HYDRATION_TAIL_LIMIT;
|
|
2292
|
+
const windowTag = options.full ? 'full' : `tail:${STATUS_HYDRATION_TAIL_LIMIT}`;
|
|
2293
|
+
|
|
1362
2294
|
if (isNativeSourceCanonicalHistory(canonicalHistory)) {
|
|
1363
|
-
const cacheKey = [this.type, this.providerSessionId, this.workingDir].join('\0');
|
|
2295
|
+
const cacheKey = [this.type, this.providerSessionId, this.workingDir, windowTag].join('\0');
|
|
1364
2296
|
const now = Date.now();
|
|
1365
2297
|
if (cacheKey === this.lastNativeSourceCanonicalCacheKey && now - this.lastNativeSourceCanonicalCheckAt < 2_000) {
|
|
1366
2298
|
return true;
|
|
@@ -1373,7 +2305,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1373
2305
|
historySessionId: this.providerSessionId,
|
|
1374
2306
|
workspace: this.workingDir,
|
|
1375
2307
|
offset: 0,
|
|
1376
|
-
limit
|
|
2308
|
+
limit,
|
|
1377
2309
|
historyBehavior: this.provider.historyBehavior,
|
|
1378
2310
|
scripts: this.provider.scripts as any,
|
|
1379
2311
|
});
|
|
@@ -1390,7 +2322,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1390
2322
|
}
|
|
1391
2323
|
|
|
1392
2324
|
try {
|
|
1393
|
-
const cacheKey = [this.type, this.providerSessionId, this.workingDir, canonicalHistory.mode || 'materialized-mirror'].join('\0');
|
|
2325
|
+
const cacheKey = [this.type, this.providerSessionId, this.workingDir, canonicalHistory.mode || 'materialized-mirror', windowTag].join('\0');
|
|
1394
2326
|
const now = Date.now();
|
|
1395
2327
|
if (cacheKey === this.lastNativeSourceCanonicalCacheKey && now - this.lastNativeSourceCanonicalCheckAt < 2_000) {
|
|
1396
2328
|
return true;
|
|
@@ -1401,7 +2333,14 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1401
2333
|
if (!materializeProviderNativeHistory(this.type, canonicalHistory, this.providerSessionId, this.workingDir, this.provider.scripts as any)) {
|
|
1402
2334
|
return false;
|
|
1403
2335
|
}
|
|
1404
|
-
|
|
2336
|
+
// Bounded by default: the per-status-report path only needs the newest
|
|
2337
|
+
// STATUS_HYDRATION_TAIL_LIMIT messages because the snapshot caps
|
|
2338
|
+
// activeChat.messages to the last 60 (status/normalize.ts) and loads
|
|
2339
|
+
// the rest lazily via read_chat on subscribe. The once-per-resume
|
|
2340
|
+
// restore path passes full:true so seedSessionHistory still sees the
|
|
2341
|
+
// COMPLETE transcript for prefix-dedup seeding. readChatHistory serves
|
|
2342
|
+
// a bounded limit as an O(tail) read.
|
|
2343
|
+
const restoredHistory = readChatHistory(this.type, 0, limit, this.providerSessionId, 0, this.provider.historyBehavior);
|
|
1405
2344
|
this.lastPersistedHistoryMessages = restoredHistory.messages.map((message) => ({
|
|
1406
2345
|
role: message.role,
|
|
1407
2346
|
content: message.content,
|
|
@@ -1417,10 +2356,13 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1417
2356
|
|
|
1418
2357
|
private restorePersistedHistoryFromCurrentSession(): void {
|
|
1419
2358
|
if (!this.providerSessionId) return;
|
|
1420
|
-
|
|
1421
|
-
|
|
2359
|
+
// Restore is the once-per-resume seeding path: it needs the COMPLETE
|
|
2360
|
+
// transcript so seedSessionHistory can prime dedup state. Pass full so the
|
|
2361
|
+
// hydration read is unbounded here (and only here).
|
|
2362
|
+
this.syncCanonicalSavedHistoryIfNeeded({ full: true });
|
|
2363
|
+
const restoredHistory = isNativeSourceCanonicalHistory(this.provider.nativeHistory)
|
|
1422
2364
|
? readProviderChatHistory(this.type, {
|
|
1423
|
-
canonicalHistory: this.provider.
|
|
2365
|
+
canonicalHistory: this.provider.nativeHistory,
|
|
1424
2366
|
historySessionId: this.providerSessionId,
|
|
1425
2367
|
workspace: this.workingDir,
|
|
1426
2368
|
offset: 0,
|