@adhdev/daemon-core 0.9.82-rc.28 → 0.9.82-rc.280
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 +23 -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 +106 -63
- package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +38 -0
- package/dist/cli-adapters/raw-terminal-io.d.ts +37 -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 +160 -1
- package/dist/config/chat-history.d.ts +9 -0
- package/dist/config/config.d.ts +5 -0
- package/dist/config/mesh-config.d.ts +72 -1
- package/dist/git/git-commands.d.ts +9 -1
- package/dist/git/git-diff.d.ts +6 -0
- package/dist/git/git-status.d.ts +7 -0
- package/dist/git/git-types.d.ts +2 -50
- package/dist/index.d.ts +51 -12
- package/dist/index.js +34388 -12966
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +38052 -16731
- 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/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 +108 -0
- package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
- package/dist/mesh/mesh-events-coordinator.d.ts +133 -0
- package/dist/mesh/mesh-events-pending.d.ts +45 -0
- package/dist/mesh/mesh-events-stale.d.ts +40 -0
- package/dist/mesh/mesh-events-utils.d.ts +47 -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-ledger-reconciliation.d.ts +23 -3
- package/dist/mesh/mesh-ledger.d.ts +77 -1
- package/dist/mesh/mesh-missions.d.ts +68 -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 +345 -0
- package/dist/mesh/mesh-task-stats.d.ts +49 -0
- package/dist/mesh/mesh-work-queue.d.ts +166 -5
- package/dist/mesh/preview-freshness.d.ts +18 -0
- package/dist/mesh/refine-config.d.ts +215 -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/cli-provider-instance.d.ts +61 -3
- package/dist/providers/contracts.d.ts +130 -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 +13 -0
- package/dist/providers/provider-instance.d.ts +13 -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 +59 -0
- package/dist/providers/spec/cli-adapter.d.ts +140 -0
- package/dist/providers/spec/evaluator.d.ts +23 -0
- package/dist/providers/spec/fsm-driver.d.ts +278 -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 +106 -0
- package/dist/providers/spec/native-history-executor.d.ts +41 -0
- package/dist/providers/spec/route.d.ts +4 -0
- package/dist/providers/spec/types.d.ts +144 -0
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/providers/types/interactive-prompt.d.ts +48 -0
- package/dist/repo-mesh-types.d.ts +154 -13
- package/dist/sessions/registry.d.ts +6 -0
- package/dist/shared-types-extra.d.ts +2 -4
- package/dist/shared-types.d.ts +42 -1
- 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 +1 -0
- package/dist/types.d.ts +5 -0
- package/package.json +7 -3
- package/src/agent-stream/poller.ts +2 -3
- 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 +22 -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 +674 -1139
- 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 +3 -1
- package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/src/cli-adapters/provider-cli-shared.ts +68 -11
- package/src/cli-adapters/raw-terminal-io.ts +252 -0
- 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 +1813 -60
- package/src/commands/cli-manager.ts +286 -14
- package/src/commands/handler.ts +809 -2
- package/src/commands/mesh-coordinator.ts +332 -122
- package/src/commands/router.ts +5739 -566
- package/src/config/chat-history.ts +229 -24
- package/src/config/config.ts +12 -0
- package/src/config/mesh-config.ts +303 -3
- 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/ide-detector.ts +26 -16
- package/src/git/git-commands.ts +42 -12
- package/src/git/git-diff.ts +53 -0
- package/src/git/git-status.ts +198 -7
- package/src/git/git-types.ts +14 -62
- package/src/git/git-worktree.ts +8 -1
- package/src/index.ts +131 -11
- 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/logger.ts +2 -1
- package/src/mesh/contracts.ts +329 -0
- package/src/mesh/coordinator-prompt.ts +219 -31
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +468 -0
- package/src/mesh/mesh-delivery-policy.ts +315 -0
- package/src/mesh/mesh-events-coordinator.ts +1592 -0
- package/src/mesh/mesh-events-pending.ts +508 -0
- package/src/mesh/mesh-events-stale.ts +283 -0
- package/src/mesh/mesh-events-utils.ts +254 -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-ledger-reconciliation.ts +12 -5
- package/src/mesh/mesh-ledger.ts +589 -102
- package/src/mesh/mesh-missions.ts +172 -0
- package/src/mesh/mesh-reconcile-loop.ts +298 -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 +272 -0
- package/src/mesh/mesh-runtime-store.ts +1399 -0
- package/src/mesh/mesh-task-stats.ts +154 -0
- package/src/mesh/mesh-work-queue.ts +586 -145
- package/src/mesh/preview-freshness.ts +118 -0
- package/src/mesh/refine-config.ts +403 -0
- package/src/mesh/worktree-bootstrap-config.ts +326 -0
- package/src/providers/acp-provider-instance.ts +15 -1
- package/src/providers/approval-utils.d.ts +4 -0
- package/src/providers/approval-utils.ts +47 -5
- package/src/providers/chat-message-normalization.ts +4 -11
- package/src/providers/cli-provider-instance.ts +920 -81
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +141 -6
- package/src/providers/external-sources.ts +218 -0
- package/src/providers/ide-provider-instance.ts +19 -5
- 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 +584 -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 +155 -0
- package/src/providers/spec/cli-adapter.ts +730 -0
- package/src/providers/spec/evaluator.ts +345 -0
- package/src/providers/spec/fsm-driver.ts +868 -0
- package/src/providers/spec/fsm-evaluator.ts +255 -0
- package/src/providers/spec/fsm-loader.ts +102 -0
- package/src/providers/spec/fsm-types.ts +199 -0
- package/src/providers/spec/native-history-executor.ts +943 -0
- package/src/providers/spec/route.ts +51 -0
- package/src/providers/spec/types.ts +189 -0
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +446 -0
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +183 -13
- package/src/sessions/registry.ts +6 -0
- package/src/shared-types-extra.ts +2 -4
- package/src/shared-types.ts +45 -1
- package/src/status/builders.ts +26 -6
- package/src/status/normalize.ts +2 -0
- package/src/status/reporter.ts +15 -0
- package/src/status/snapshot.ts +84 -25
- package/src/system/host-memory.ts +29 -12
- 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,15 +13,17 @@ 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';
|
|
@@ -40,12 +42,60 @@ type CompletedDebouncePending = {
|
|
|
40
42
|
duration: number;
|
|
41
43
|
timestamp: number;
|
|
42
44
|
firstObservedAt: number;
|
|
45
|
+
previousStatus: string;
|
|
43
46
|
loggedBlockReason?: string;
|
|
47
|
+
loggedTranscriptProbe?: boolean;
|
|
48
|
+
transcriptProbeHistory?: ExternalTranscriptProbe[];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function isIdleStatus(value: unknown): boolean {
|
|
52
|
+
const status = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
53
|
+
return !status || status === 'idle' || status === 'ready';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
function getMessageTime(message: unknown): number {
|
|
58
|
+
if (!message || typeof message !== 'object') return 0;
|
|
59
|
+
const record = message as { receivedAt?: unknown; timestamp?: unknown };
|
|
60
|
+
const value = Number(record.receivedAt ?? record.timestamp ?? 0);
|
|
61
|
+
return Number.isFinite(value) ? value : 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type CompletedFinalizationBlock = {
|
|
65
|
+
reason: string;
|
|
66
|
+
terminal?: boolean;
|
|
67
|
+
allowTimeout?: boolean;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
type CompletionFinalAssistantEvidence = {
|
|
71
|
+
present: boolean;
|
|
72
|
+
messages: unknown[];
|
|
73
|
+
source: 'parsed' | 'external-native' | 'unavailable';
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
type ExternalTranscriptProbe = {
|
|
77
|
+
readAt: number;
|
|
78
|
+
msgCount: number;
|
|
79
|
+
lastRole: string | null;
|
|
80
|
+
lastKind: string | null;
|
|
81
|
+
contentLen: number;
|
|
82
|
+
sourcePath: string | null;
|
|
83
|
+
sourceMtimeMs: number | null;
|
|
84
|
+
mtimeAgeMs: number | null;
|
|
44
85
|
};
|
|
45
86
|
|
|
46
87
|
const COMPLETED_FINALIZATION_RETRY_MS = 1000;
|
|
47
88
|
const COMPLETED_FINALIZATION_MAX_WAIT_MS = 30_000;
|
|
48
89
|
|
|
90
|
+
/** Events that signal a dispatched mesh task has reached a terminal state.
|
|
91
|
+
* Detach the mesh assignment after emitting one of these so the worker's
|
|
92
|
+
* next unrelated turn doesn't impersonate another completion. */
|
|
93
|
+
const TERMINAL_MESH_EVENTS = new Set([
|
|
94
|
+
'agent:generating_completed',
|
|
95
|
+
'agent:stopped',
|
|
96
|
+
'agent:ready',
|
|
97
|
+
]);
|
|
98
|
+
|
|
49
99
|
const IMAGE_MIME_EXTENSIONS: Record<string, string> = {
|
|
50
100
|
'image/png': '.png',
|
|
51
101
|
'image/jpeg': '.jpg',
|
|
@@ -300,6 +350,19 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
300
350
|
readonly type: string;
|
|
301
351
|
readonly category = 'cli' as const;
|
|
302
352
|
|
|
353
|
+
/**
|
|
354
|
+
* Quiet period an approval modal's signature must be stable before
|
|
355
|
+
* auto-approve sends the approve key. Guards against firing on a prompt
|
|
356
|
+
* that is still streaming into the PTY (the "resolves too fast" symptom):
|
|
357
|
+
* while the modal text/buttons are still changing, every frame yields a
|
|
358
|
+
* new signature and the settle clock restarts. Once the prompt finishes
|
|
359
|
+
* rendering the signature holds and the key is sent after this window.
|
|
360
|
+
* Bounded + small so genuine approvals stay timely. The FSM is already
|
|
361
|
+
* authoritative over the `waiting_approval` state; this only delays the
|
|
362
|
+
* keystroke until the modal *content* has settled.
|
|
363
|
+
*/
|
|
364
|
+
private static readonly AUTO_APPROVE_SETTLE_MS = 600;
|
|
365
|
+
|
|
303
366
|
private adapter: ProviderCliAdapter;
|
|
304
367
|
private context: InstanceContext | null = null;
|
|
305
368
|
private events: ProviderEvent[] = [];
|
|
@@ -309,15 +372,25 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
309
372
|
private monitor: StatusMonitor;
|
|
310
373
|
private generatingDebounceTimer: NodeJS.Timeout | null = null;
|
|
311
374
|
private generatingDebouncePending: { chatTitle: string; timestamp: number } | null = null;
|
|
312
|
-
private
|
|
375
|
+
private lastApprovalEventFingerprint = '';
|
|
313
376
|
private autoApproveBusy = false;
|
|
314
377
|
private autoApproveBusyTimer: NodeJS.Timeout | null = null;
|
|
378
|
+
private lastAutoApprovalSignature = '';
|
|
379
|
+
// Settle gate: the approval modal's signature + the wall-clock when this
|
|
380
|
+
// exact signature was first observed. Auto-approve only fires once the
|
|
381
|
+
// SAME signature has been stable for AUTO_APPROVE_SETTLE_MS, so a prompt
|
|
382
|
+
// still streaming into the PTY (its buttons/message changing frame to
|
|
383
|
+
// frame) keeps resetting the timer and is never approved half-rendered.
|
|
384
|
+
private pendingAutoApprovalSignature = '';
|
|
385
|
+
private pendingAutoApprovalSince = 0;
|
|
386
|
+
private autoApproveSettleTimer: NodeJS.Timeout | null = null;
|
|
315
387
|
private controlValues: Record<string, string | number | boolean> = {};
|
|
316
388
|
private summaryMetadata: unknown = undefined;
|
|
317
389
|
private appliedEffectKeys = new Set<string>();
|
|
318
390
|
private historyWriter: ChatHistoryWriter;
|
|
319
391
|
private runtimeMessages: Array<{ key: string; message: ChatMessage }> = [];
|
|
320
392
|
private lastPersistedHistoryMessages: PersistableCliHistoryMessage[] = [];
|
|
393
|
+
private lastAcknowledgedUserInputAt = 0;
|
|
321
394
|
private lastNativeSourceCanonicalCheckAt = 0;
|
|
322
395
|
private lastNativeSourceCanonicalCacheKey: string | undefined = undefined;
|
|
323
396
|
private cachedSqliteDb: {
|
|
@@ -330,6 +403,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
330
403
|
private suppressIdleHistoryReplay = false;
|
|
331
404
|
private errorMessage: string | undefined = undefined;
|
|
332
405
|
private errorReason: ProviderErrorReason | undefined = undefined;
|
|
406
|
+
private activeInteractivePrompt: InteractivePrompt | null = null;
|
|
333
407
|
|
|
334
408
|
private presentationMode: 'terminal' | 'chat';
|
|
335
409
|
private providerSessionId?: string;
|
|
@@ -370,7 +444,10 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
370
444
|
this.providerSessionId = options?.providerSessionId;
|
|
371
445
|
this.launchMode = options?.launchMode || 'new';
|
|
372
446
|
this.onProviderSessionResolved = options?.onProviderSessionResolved;
|
|
373
|
-
this.adapter =
|
|
447
|
+
this.adapter = createCliAdapter(provider as CliProviderModule, workingDir, cliArgs, options?.extraEnv || {}, transportFactory) as ProviderCliAdapter;
|
|
448
|
+
if (this.providerSessionId) {
|
|
449
|
+
this.adapter.updateRuntimeMeta({ providerSessionId: this.providerSessionId });
|
|
450
|
+
}
|
|
374
451
|
this.monitor = new StatusMonitor();
|
|
375
452
|
this.historyWriter = new ChatHistoryWriter();
|
|
376
453
|
}
|
|
@@ -412,7 +489,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
412
489
|
await this.adapter.spawn();
|
|
413
490
|
await this.enforceFreshSessionLaunchIfNeeded();
|
|
414
491
|
this.maybeAppendRuntimeRecoveryMessage(this.adapter.getRuntimeMetadata());
|
|
415
|
-
if (this.providerSessionId) {
|
|
492
|
+
if (this.providerSessionId && this.shouldHydrateExistingProviderHistory()) {
|
|
416
493
|
this.restorePersistedHistoryFromCurrentSession();
|
|
417
494
|
}
|
|
418
495
|
if (this.providerSessionId && this.launchMode === 'resume') {
|
|
@@ -486,14 +563,38 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
486
563
|
}
|
|
487
564
|
|
|
488
565
|
getState(): ProviderState {
|
|
566
|
+
// TODO(phase5-sandbox): JS override scripts (detectStatus, parseApproval,
|
|
567
|
+
// parseSession) are currently invoked by CliScriptRunner.invoke() via direct
|
|
568
|
+
// function calls — the scripts run in the daemon process with full Node.js
|
|
569
|
+
// access and no resource limits.
|
|
570
|
+
//
|
|
571
|
+
// When Phase 5 lands, CliScriptRunner should route these calls through a
|
|
572
|
+
// SandboxedScriptRunner (see providers/sdk/v1/sandbox/script-runner.ts) so
|
|
573
|
+
// that each call gets a fresh isolated-vm context with a 50 ms CPU limit and
|
|
574
|
+
// a 32 MB memory cap. The execution path to change is:
|
|
575
|
+
// CliScriptRunner.invoke() → SandboxedScriptRunner.run(scriptSource, context)
|
|
576
|
+
//
|
|
577
|
+
// This getState() call-site is NOT where the change goes — the wiring belongs
|
|
578
|
+
// in cli-script-runner.ts (CliScriptRunner.detectStatus / parseApproval /
|
|
579
|
+
// parseSession), with provider-loader.ts updated to store script source strings
|
|
580
|
+
// alongside the loaded function references for extended-legacy providers.
|
|
489
581
|
const adapterStatus = this.adapter.getStatus();
|
|
582
|
+
if (Object.prototype.hasOwnProperty.call(adapterStatus, 'activeInteractivePrompt')) {
|
|
583
|
+
this.activeInteractivePrompt = adapterStatus.activeInteractivePrompt ?? null;
|
|
584
|
+
}
|
|
490
585
|
let parsedStatus: any = null;
|
|
491
586
|
let parseErrorMessage: string | undefined;
|
|
492
587
|
if (typeof this.adapter.getScriptParsedStatus === 'function') {
|
|
493
588
|
try {
|
|
494
589
|
parsedStatus = this.adapter.getScriptParsedStatus() || null;
|
|
495
|
-
|
|
496
|
-
|
|
590
|
+
const parsedErrorMessage = typeof parsedStatus?.errorMessage === 'string' && parsedStatus.errorMessage.trim()
|
|
591
|
+
? parsedStatus.errorMessage.trim()
|
|
592
|
+
: undefined;
|
|
593
|
+
const parsedErrorReason = typeof parsedStatus?.errorReason === 'string' && parsedStatus.errorReason.trim()
|
|
594
|
+
? parsedStatus.errorReason.trim() as ProviderErrorReason
|
|
595
|
+
: undefined;
|
|
596
|
+
this.errorMessage = parsedErrorMessage;
|
|
597
|
+
this.errorReason = parsedErrorReason;
|
|
497
598
|
} catch (error: any) {
|
|
498
599
|
parseErrorMessage = error?.message || String(error);
|
|
499
600
|
this.errorMessage = parseErrorMessage;
|
|
@@ -503,22 +604,53 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
503
604
|
this.errorMessage = undefined;
|
|
504
605
|
this.errorReason = undefined;
|
|
505
606
|
}
|
|
506
|
-
const
|
|
507
|
-
const visibleStatus = parseErrorMessage
|
|
508
|
-
? 'error'
|
|
509
|
-
: (autoApproveActive ? 'generating' : adapterStatus.status);
|
|
510
|
-
const parsedProviderSessionId = normalizeProviderSessionId(
|
|
607
|
+
const adapterProviderSessionId = normalizeProviderSessionId(
|
|
511
608
|
this.provider,
|
|
512
|
-
typeof
|
|
609
|
+
typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
|
|
513
610
|
);
|
|
514
|
-
|
|
515
|
-
|
|
611
|
+
const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, Date.now());
|
|
612
|
+
const autoApproveHoldIdle = this.autoApproveBusy && adapterStatus.status === 'idle';
|
|
613
|
+
let visibleStatus = parseErrorMessage || parsedStatus?.status === 'error'
|
|
614
|
+
? 'error'
|
|
615
|
+
: (autoApproveActive || autoApproveHoldIdle ? 'generating' : adapterStatus.status);
|
|
616
|
+
// getState() must agree with the status the FSM-driven detectStatusTransition()
|
|
617
|
+
// already committed to lastStatus. The adapter's own status is authoritative; we do
|
|
618
|
+
// not second-guess it with native-transcript shape. Only reconcile a generating-like
|
|
619
|
+
// read down to idle when our own lastStatus has already flipped idle (avoids a
|
|
620
|
+
// perpetual dashboard spinner during the brief window before the next getStatus()).
|
|
621
|
+
if (isCliGeneratingLikeStatus(visibleStatus) && this.lastStatus === 'idle') {
|
|
622
|
+
visibleStatus = 'idle';
|
|
516
623
|
}
|
|
517
624
|
const runtime = this.adapter.getRuntimeMetadata();
|
|
518
625
|
this.maybeAppendRuntimeRecoveryMessage(runtime);
|
|
519
626
|
let parsedMessages = Array.isArray(parsedStatus?.messages)
|
|
520
627
|
? parsedStatus.messages
|
|
521
628
|
: [];
|
|
629
|
+
const parsedProviderSessionId = normalizeProviderSessionId(
|
|
630
|
+
this.provider,
|
|
631
|
+
typeof parsedStatus?.providerSessionId === 'string' ? parsedStatus.providerSessionId : '',
|
|
632
|
+
);
|
|
633
|
+
const suppressFreshLaunchStartupReplay = this.shouldSuppressFreshLaunchStartupReplay(
|
|
634
|
+
parsedMessages,
|
|
635
|
+
parsedStatus,
|
|
636
|
+
adapterStatus,
|
|
637
|
+
parsedProviderSessionId,
|
|
638
|
+
);
|
|
639
|
+
if (adapterProviderSessionId && !suppressFreshLaunchStartupReplay) {
|
|
640
|
+
this.promoteProviderSessionId(adapterProviderSessionId);
|
|
641
|
+
}
|
|
642
|
+
if (parsedProviderSessionId && !suppressFreshLaunchStartupReplay) {
|
|
643
|
+
this.promoteProviderSessionId(parsedProviderSessionId);
|
|
644
|
+
}
|
|
645
|
+
if (suppressFreshLaunchStartupReplay) {
|
|
646
|
+
parsedMessages = [];
|
|
647
|
+
}
|
|
648
|
+
// Adapter runtime metadata is transport-owned and is not guaranteed to
|
|
649
|
+
// identify this conversation. Spec adapters historically exposed the
|
|
650
|
+
// provider spec id (for example "codex-cli") as runtimeId, which made
|
|
651
|
+
// concurrent sessions share one activeChat identity until their native
|
|
652
|
+
// provider session ids were discovered.
|
|
653
|
+
const activeChatId = this.providerSessionId || this.instanceId;
|
|
522
654
|
const historyMessageCount = Number.isFinite(parsedStatus?.historyMessageCount)
|
|
523
655
|
? Math.max(0, Number(parsedStatus.historyMessageCount))
|
|
524
656
|
: null;
|
|
@@ -528,7 +660,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
528
660
|
: [];
|
|
529
661
|
}
|
|
530
662
|
const mergedMessages = this.mergeConversationMessages(parsedMessages);
|
|
531
|
-
const canonicalBackedHistory = this.
|
|
663
|
+
const canonicalBackedHistory = this.shouldHydrateExistingProviderHistory()
|
|
664
|
+
? this.syncCanonicalSavedHistoryIfNeeded()
|
|
665
|
+
: false;
|
|
666
|
+
const statusMessages = canonicalBackedHistory && this.lastPersistedHistoryMessages.length > 0
|
|
667
|
+
? this.lastPersistedHistoryMessages.map((message) => ({
|
|
668
|
+
role: message.role,
|
|
669
|
+
content: message.content,
|
|
670
|
+
kind: message.kind,
|
|
671
|
+
senderName: message.senderName,
|
|
672
|
+
receivedAt: message.receivedAt,
|
|
673
|
+
}))
|
|
674
|
+
: mergedMessages;
|
|
532
675
|
|
|
533
676
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
534
677
|
const parsedChatStatus = typeof parsedStatus?.status === 'string' && parsedStatus.status.trim()
|
|
@@ -572,33 +715,48 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
572
715
|
}
|
|
573
716
|
}
|
|
574
717
|
|
|
575
|
-
this.applyProviderResponse(
|
|
718
|
+
this.applyProviderResponse(
|
|
719
|
+
suppressFreshLaunchStartupReplay && parsedStatus && typeof parsedStatus === 'object'
|
|
720
|
+
? { ...parsedStatus, providerSessionId: undefined }
|
|
721
|
+
: parsedStatus,
|
|
722
|
+
{ phase: 'immediate' },
|
|
723
|
+
);
|
|
576
724
|
const surface = resolveProviderStateSurface({
|
|
577
725
|
summaryMetadata: this.summaryMetadata as any,
|
|
578
726
|
controlValues: this.controlValues,
|
|
579
727
|
});
|
|
580
728
|
const activeChatStatus = parseErrorMessage
|
|
581
729
|
? 'error'
|
|
582
|
-
: autoApproveActive && parsedStatus?.status === 'waiting_approval'
|
|
730
|
+
: (autoApproveActive && parsedStatus?.status === 'waiting_approval') || autoApproveHoldIdle
|
|
583
731
|
? 'generating'
|
|
584
732
|
: (adapterStatus.status !== 'idle'
|
|
585
733
|
? visibleStatus
|
|
586
734
|
: (suppressStaleParsedBusyStatus ? visibleStatus : (parsedChatStatus || visibleStatus)));
|
|
587
735
|
|
|
736
|
+
// If an AskUserQuestion prompt is awaiting user input, overlay status as
|
|
737
|
+
// waiting_choice. This is distinct from waiting_approval (tool-use consent)
|
|
738
|
+
// — the engine's isWaitingForResponse state is unchanged, so completion
|
|
739
|
+
// tracking continues normally once the user responds.
|
|
740
|
+
const hasInteractivePrompt = !!this.activeInteractivePrompt;
|
|
741
|
+
const finalStatus = hasInteractivePrompt ? 'waiting_choice' : visibleStatus;
|
|
742
|
+
const finalChatStatus = hasInteractivePrompt ? 'waiting_choice' : activeChatStatus;
|
|
743
|
+
|
|
588
744
|
return {
|
|
589
745
|
type: this.type,
|
|
590
746
|
name: this.provider.name,
|
|
591
747
|
category: 'cli',
|
|
592
|
-
status:
|
|
748
|
+
status: finalStatus,
|
|
593
749
|
mode: this.presentationMode,
|
|
594
750
|
activeChat: {
|
|
595
|
-
id:
|
|
751
|
+
id: activeChatId,
|
|
596
752
|
title: parsedStatus?.title || dirName,
|
|
597
|
-
status:
|
|
598
|
-
messages:
|
|
599
|
-
activeModal: autoApproveActive ? null : (parsedStatus?.activeModal ?? adapterStatus.activeModal),
|
|
753
|
+
status: finalChatStatus,
|
|
754
|
+
messages: statusMessages,
|
|
755
|
+
activeModal: (autoApproveActive || autoApproveHoldIdle) ? null : (parsedStatus?.activeModal ?? adapterStatus.activeModal),
|
|
756
|
+
activeInteractivePrompt: this.activeInteractivePrompt,
|
|
600
757
|
inputContent: '',
|
|
601
758
|
},
|
|
759
|
+
activeInteractivePrompt: this.activeInteractivePrompt,
|
|
602
760
|
workspace: this.workingDir,
|
|
603
761
|
instanceId: this.instanceId,
|
|
604
762
|
providerSessionId: this.providerSessionId,
|
|
@@ -639,7 +797,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
639
797
|
getHotChatSessionState(): HotChatSessionState {
|
|
640
798
|
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
641
799
|
const autoApproveActive = adapterStatus.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
642
|
-
const
|
|
800
|
+
const autoApproveHoldIdle = this.autoApproveBusy && adapterStatus.status === 'idle';
|
|
801
|
+
const visibleStatus = autoApproveActive || autoApproveHoldIdle ? 'generating' : adapterStatus.status;
|
|
643
802
|
const runtime = this.adapter.getRuntimeMetadata();
|
|
644
803
|
return {
|
|
645
804
|
id: this.instanceId,
|
|
@@ -651,21 +810,41 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
651
810
|
};
|
|
652
811
|
}
|
|
653
812
|
|
|
654
|
-
getSessionModalState(): SessionModalState {
|
|
655
|
-
const adapterStatus = this.adapter.getStatus({ allowParse:
|
|
813
|
+
getSessionModalState(sessionId?: string): SessionModalState {
|
|
814
|
+
const adapterStatus = this.adapter.getStatus({ allowParse: true });
|
|
656
815
|
const autoApproveActive = adapterStatus.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
657
|
-
const
|
|
816
|
+
const autoApproveHoldIdle = this.autoApproveBusy && adapterStatus.status === 'idle';
|
|
817
|
+
const visibleStatus = autoApproveActive || autoApproveHoldIdle ? 'generating' : adapterStatus.status;
|
|
658
818
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
659
819
|
return {
|
|
660
|
-
|
|
820
|
+
// Honor the caller-supplied sessionId — InstanceMgr rejects the
|
|
821
|
+
// projection when projected.id !== requested sessionId, and
|
|
822
|
+
// this.instanceId is the manager's internal key, not the public
|
|
823
|
+
// sessionId the dashboard subscribes by.
|
|
824
|
+
id: sessionId ?? this.instanceId,
|
|
661
825
|
status: visibleStatus,
|
|
662
826
|
title: dirName,
|
|
663
|
-
activeModal: autoApproveActive ? null : adapterStatus.activeModal,
|
|
827
|
+
activeModal: (autoApproveActive || autoApproveHoldIdle) ? null : adapterStatus.activeModal,
|
|
664
828
|
};
|
|
665
829
|
}
|
|
666
830
|
|
|
667
831
|
updateSettings(newSettings: Record<string, any>): void {
|
|
668
|
-
|
|
832
|
+
const runtimeMeshSettings: Record<string, any> = {};
|
|
833
|
+
for (const key of [
|
|
834
|
+
'meshNodeFor',
|
|
835
|
+
'meshNodeId',
|
|
836
|
+
'meshActiveTaskId',
|
|
837
|
+
'meshCoordinatorFor',
|
|
838
|
+
'meshCoordinatorDaemonId',
|
|
839
|
+
'meshCoordinatorNodeId',
|
|
840
|
+
'spawnedSessionVisibility',
|
|
841
|
+
'launchedByCoordinator',
|
|
842
|
+
]) {
|
|
843
|
+
if (this.settings[key] !== undefined && newSettings[key] === undefined) {
|
|
844
|
+
runtimeMeshSettings[key] = this.settings[key];
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
this.settings = { ...newSettings, ...runtimeMeshSettings };
|
|
669
848
|
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
670
849
|
this.monitor.updateConfig({
|
|
671
850
|
approvalAlert: this.settings.approvalAlert !== false,
|
|
@@ -674,13 +853,53 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
674
853
|
});
|
|
675
854
|
}
|
|
676
855
|
|
|
856
|
+
/**
|
|
857
|
+
* Stamp a direct-dispatch mesh assignment on this instance.
|
|
858
|
+
* setupMeshEventForwarding reads settings.meshNodeFor + meshActiveTaskId to
|
|
859
|
+
* route generating_completed back to the originating coordinator. Without
|
|
860
|
+
* this stamp, mesh_send_task --direct targets a plain CLI session whose
|
|
861
|
+
* completion events silently drop because the forwarder has nothing to
|
|
862
|
+
* match against.
|
|
863
|
+
*/
|
|
864
|
+
attachMeshAssignment(assignment: { meshId: string; nodeId?: string; taskId?: string; coordinatorDaemonId?: string }): void {
|
|
865
|
+
if (!assignment?.meshId) return;
|
|
866
|
+
this.settings = {
|
|
867
|
+
...this.settings,
|
|
868
|
+
meshNodeFor: assignment.meshId,
|
|
869
|
+
...(assignment.nodeId ? { meshNodeId: assignment.nodeId } : {}),
|
|
870
|
+
...(assignment.taskId ? { meshActiveTaskId: assignment.taskId } : {}),
|
|
871
|
+
...(assignment.coordinatorDaemonId ? { meshCoordinatorDaemonId: assignment.coordinatorDaemonId } : {}),
|
|
872
|
+
};
|
|
873
|
+
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Clear a previously-attached mesh assignment after the task reaches a
|
|
878
|
+
* terminal state. Leaving meshNodeFor pinned would route this session's
|
|
879
|
+
* subsequent unrelated turns (e.g. ad-hoc dashboard chats) to the
|
|
880
|
+
* coordinator as if they were task completions.
|
|
881
|
+
*/
|
|
882
|
+
detachMeshAssignment(): void {
|
|
883
|
+
if (!this.settings.meshNodeFor && !this.settings.meshActiveTaskId && !this.settings.meshNodeId) return;
|
|
884
|
+
const { meshNodeFor, meshNodeId, meshActiveTaskId, ...rest } = this.settings;
|
|
885
|
+
void meshNodeFor; void meshNodeId; void meshActiveTaskId;
|
|
886
|
+
this.settings = rest;
|
|
887
|
+
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
888
|
+
}
|
|
889
|
+
|
|
677
890
|
onEvent(event: string, data?: any): void {
|
|
678
891
|
if (event === 'send_message') {
|
|
679
892
|
const input = normalizeInputEnvelope(data);
|
|
680
893
|
assertProviderSupportsDeclaredInput(this.provider, input);
|
|
681
894
|
const promptText = buildCliStructuredInputPrompt(input);
|
|
682
895
|
if (promptText) {
|
|
683
|
-
|
|
896
|
+
// force:true bypasses the busy/generating send guard so terminal mesh
|
|
897
|
+
// events (completion/failure/bootstrap) land in a coordinator session that
|
|
898
|
+
// is itself parked in `generating` while awaiting that very event.
|
|
899
|
+
// Without it the message is queued and only flushed on the coordinator's
|
|
900
|
+
// own idle transition — which never happens until it receives the message.
|
|
901
|
+
const force = data?.force === true;
|
|
902
|
+
void this.adapter.sendMessage(promptText, force ? { force: true } : {}).catch((e: any) => {
|
|
684
903
|
LOG.warn('CLI', `[${this.type}] send_message failed: ${e?.message || e}`);
|
|
685
904
|
});
|
|
686
905
|
}
|
|
@@ -690,14 +909,73 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
690
909
|
void this.adapter.resolveAction(data).catch((e: any) => {
|
|
691
910
|
LOG.warn('CLI', `[${this.type}] resolve_action failed: ${e?.message || e}`);
|
|
692
911
|
});
|
|
912
|
+
} else if (event === 'interactive_prompt' && data) {
|
|
913
|
+
const prompt = normalizeInteractivePrompt(data);
|
|
914
|
+
if (prompt) {
|
|
915
|
+
this.activeInteractivePrompt = prompt;
|
|
916
|
+
this.events.push({
|
|
917
|
+
event: 'interactive_prompt',
|
|
918
|
+
timestamp: Date.now(),
|
|
919
|
+
promptId: prompt.promptId,
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
} else if (event === 'interactive_prompt_response' && data) {
|
|
923
|
+
try {
|
|
924
|
+
const response = normalizeInteractivePromptResponse(data);
|
|
925
|
+
if (this.activeInteractivePrompt?.promptId === response.promptId) {
|
|
926
|
+
this.activeInteractivePrompt = null;
|
|
927
|
+
}
|
|
928
|
+
if (typeof this.adapter.setInteractivePromptResponse !== 'function') {
|
|
929
|
+
LOG.warn('CLI', `[${this.type}] interactive_prompt_response ignored: adapter does not support interactive prompts`);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
void this.adapter.setInteractivePromptResponse(response).catch((e: any) => {
|
|
933
|
+
LOG.warn('CLI', `[${this.type}] interactive_prompt_response failed: ${e?.message || e}`);
|
|
934
|
+
});
|
|
935
|
+
} catch (e: any) {
|
|
936
|
+
LOG.warn('CLI', `[${this.type}] invalid interactive_prompt_response: ${e?.message || e}`);
|
|
937
|
+
}
|
|
693
938
|
} else if (event === 'provider_state_patch' && data && typeof data === 'object') {
|
|
694
939
|
this.applyProviderResponse(data, { phase: 'immediate' });
|
|
695
940
|
}
|
|
696
941
|
}
|
|
697
942
|
|
|
943
|
+
recordAcknowledgedUserInput(input: InputEnvelope | string): void {
|
|
944
|
+
const content = typeof input === 'string'
|
|
945
|
+
? input.trim()
|
|
946
|
+
: buildCliStructuredInputPrompt(input).trim();
|
|
947
|
+
if (!content) return;
|
|
948
|
+
|
|
949
|
+
const receivedAt = Date.now();
|
|
950
|
+
this.lastAcknowledgedUserInputAt = receivedAt;
|
|
951
|
+
const dedupKey = `user_input_ack:${crypto
|
|
952
|
+
.createHash('sha256')
|
|
953
|
+
.update(`${this.instanceId}:${content}:${receivedAt}`)
|
|
954
|
+
.digest('hex')
|
|
955
|
+
.slice(0, 24)}`;
|
|
956
|
+
this.appendRuntimeMessage(buildChatMessage({
|
|
957
|
+
role: 'user',
|
|
958
|
+
senderName: 'User',
|
|
959
|
+
kind: 'standard',
|
|
960
|
+
content,
|
|
961
|
+
receivedAt,
|
|
962
|
+
timestamp: receivedAt,
|
|
963
|
+
source: 'runtime_input_ack',
|
|
964
|
+
meta: {
|
|
965
|
+
runtimeInputAck: true,
|
|
966
|
+
provider: this.type,
|
|
967
|
+
workspace: this.workingDir,
|
|
968
|
+
},
|
|
969
|
+
} as ChatMessage), dedupKey);
|
|
970
|
+
}
|
|
971
|
+
|
|
698
972
|
dispose(): void {
|
|
699
973
|
this.adapter.shutdown();
|
|
700
974
|
this.monitor.reset();
|
|
975
|
+
// Cancel any armed auto-approve timers so a pending settle re-check
|
|
976
|
+
// can't fire resolveModal/detectStatusTransition against a dead adapter.
|
|
977
|
+
if (this.autoApproveSettleTimer) { clearTimeout(this.autoApproveSettleTimer); this.autoApproveSettleTimer = null; }
|
|
978
|
+
if (this.autoApproveBusyTimer) { clearTimeout(this.autoApproveBusyTimer); this.autoApproveBusyTimer = null; }
|
|
701
979
|
this.appliedEffectKeys.clear();
|
|
702
980
|
try { this.cachedSqliteDb?.close(); } catch { /* noop */ }
|
|
703
981
|
this.cachedSqliteDb = null;
|
|
@@ -706,6 +984,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
706
984
|
|
|
707
985
|
private completedDebounceTimer: NodeJS.Timeout | null = null;
|
|
708
986
|
private completedDebouncePending: CompletedDebouncePending | null = null;
|
|
987
|
+
private lastExternalCompletionProbe: ExternalTranscriptProbe | null = null;
|
|
709
988
|
|
|
710
989
|
private async enforceFreshSessionLaunchIfNeeded(): Promise<void> {
|
|
711
990
|
const scriptName = getForcedNewSessionScriptName(this.provider, this.launchMode);
|
|
@@ -740,7 +1019,180 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
740
1019
|
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
741
1020
|
const role = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : '';
|
|
742
1021
|
const content = lastVisible ? flattenContent(lastVisible.content).trim() : '';
|
|
743
|
-
|
|
1022
|
+
if (role !== 'assistant' || !content) return false;
|
|
1023
|
+
// Guard: if the last assistant message looks like an active approval/input prompt,
|
|
1024
|
+
// it is not a real completion — the session is still awaiting user input.
|
|
1025
|
+
if (looksLikeActiveApprovalPromptText(content)) return false;
|
|
1026
|
+
return true;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
private buildExternalTranscriptProbe(messages: unknown[], sourcePath?: string, sourceMtimeMs?: number): ExternalTranscriptProbe {
|
|
1030
|
+
const visibleMessages = messages.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
1031
|
+
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
1032
|
+
const readAt = Date.now();
|
|
1033
|
+
const mtimeMs = Number(sourceMtimeMs) || 0;
|
|
1034
|
+
return {
|
|
1035
|
+
readAt,
|
|
1036
|
+
msgCount: messages.length,
|
|
1037
|
+
lastRole: typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null,
|
|
1038
|
+
lastKind: typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null,
|
|
1039
|
+
contentLen: lastVisible ? flattenContent(lastVisible.content).trim().length : 0,
|
|
1040
|
+
sourcePath: typeof sourcePath === 'string' && sourcePath ? sourcePath : null,
|
|
1041
|
+
sourceMtimeMs: mtimeMs || null,
|
|
1042
|
+
mtimeAgeMs: mtimeMs ? Math.max(0, readAt - mtimeMs) : null,
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
private recordPendingTranscriptProbe(pending: CompletedDebouncePending): ExternalTranscriptProbe | null {
|
|
1047
|
+
const probe = this.lastExternalCompletionProbe;
|
|
1048
|
+
if (!probe) return null;
|
|
1049
|
+
const history = pending.transcriptProbeHistory || [];
|
|
1050
|
+
const last = history[history.length - 1];
|
|
1051
|
+
if (!last || last.readAt !== probe.readAt || last.msgCount !== probe.msgCount || last.lastRole !== probe.lastRole || last.contentLen !== probe.contentLen) {
|
|
1052
|
+
history.push(probe);
|
|
1053
|
+
pending.transcriptProbeHistory = history.slice(-5);
|
|
1054
|
+
}
|
|
1055
|
+
return probe;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
private readExternalCompletionMessages(): unknown[] | null {
|
|
1059
|
+
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1060
|
+
if (!adapterOwnsMessagesElsewhere) return null;
|
|
1061
|
+
if (!this.providerSessionId) return null;
|
|
1062
|
+
if (!isNativeSourceCanonicalHistory(this.provider.nativeHistory)) return null;
|
|
1063
|
+
|
|
1064
|
+
if (this.lastExternalCompletionProbe?.sourcePath) {
|
|
1065
|
+
try { fs.statSync(this.lastExternalCompletionProbe.sourcePath); } catch { /* best-effort metadata refresh */ }
|
|
1066
|
+
}
|
|
1067
|
+
const restoredHistory = readProviderChatHistory(this.type, {
|
|
1068
|
+
canonicalHistory: this.provider.nativeHistory,
|
|
1069
|
+
historySessionId: this.providerSessionId,
|
|
1070
|
+
workspace: this.workingDir,
|
|
1071
|
+
offset: 0,
|
|
1072
|
+
limit: Number.MAX_SAFE_INTEGER,
|
|
1073
|
+
historyBehavior: this.provider.historyBehavior,
|
|
1074
|
+
scripts: this.provider.scripts as any,
|
|
1075
|
+
sessionStartedAtMs: this.startedAt,
|
|
1076
|
+
forceRefresh: true,
|
|
1077
|
+
});
|
|
1078
|
+
if (restoredHistory.source !== 'provider-native') {
|
|
1079
|
+
this.lastExternalCompletionProbe = null;
|
|
1080
|
+
return null;
|
|
1081
|
+
}
|
|
1082
|
+
this.lastExternalCompletionProbe = this.buildExternalTranscriptProbe(
|
|
1083
|
+
restoredHistory.messages,
|
|
1084
|
+
restoredHistory.sourcePath,
|
|
1085
|
+
restoredHistory.sourceMtimeMs,
|
|
1086
|
+
);
|
|
1087
|
+
return restoredHistory.messages;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
private completionFinalAssistantEvidence(parsedMessages: unknown): CompletionFinalAssistantEvidence {
|
|
1091
|
+
if (this.completionHasFinalAssistantMessage(parsedMessages)) {
|
|
1092
|
+
return {
|
|
1093
|
+
present: true,
|
|
1094
|
+
messages: Array.isArray(parsedMessages) ? parsedMessages : [],
|
|
1095
|
+
source: 'parsed',
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
const externalMessages = this.readExternalCompletionMessages();
|
|
1100
|
+
if (externalMessages) {
|
|
1101
|
+
return {
|
|
1102
|
+
present: this.completionHasFinalAssistantMessage(externalMessages),
|
|
1103
|
+
messages: externalMessages,
|
|
1104
|
+
source: 'external-native',
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
return {
|
|
1109
|
+
present: false,
|
|
1110
|
+
messages: Array.isArray(parsedMessages) ? parsedMessages : [],
|
|
1111
|
+
source: 'unavailable',
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
private completionFinalSummary(parsedMessages: unknown): string | undefined {
|
|
1116
|
+
// For native-source providers (claude-cli: chatMessagesOwnedExternally), the PTY
|
|
1117
|
+
// screen parse is NOT the source of truth for the final summary — the terminal
|
|
1118
|
+
// wraps/scrolls/clips text, so a screen-parsed assistant message is often a partial
|
|
1119
|
+
// prefix (e.g. 76 chars of a 112-char turn). The append-only native transcript holds
|
|
1120
|
+
// the complete turn. Prefer it whenever it yields a longer/complete summary; fall back
|
|
1121
|
+
// to the parsed screen only when the transcript is unavailable. This is the real cause
|
|
1122
|
+
// of the truncated finalSummary — independent of cloud vs standalone (it surfaces on
|
|
1123
|
+
// any short, fast-completing task where screen parse wins the race).
|
|
1124
|
+
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1125
|
+
const parsedSummary = extractFinalSummaryFromMessages(
|
|
1126
|
+
(this.completionHasFinalAssistantMessage(parsedMessages)
|
|
1127
|
+
? (Array.isArray(parsedMessages) ? parsedMessages : [])
|
|
1128
|
+
: []) as any,
|
|
1129
|
+
);
|
|
1130
|
+
if (adapterOwnsMessagesElsewhere) {
|
|
1131
|
+
const externalMessages = this.readExternalCompletionMessages();
|
|
1132
|
+
const externalSummary = externalMessages
|
|
1133
|
+
? extractFinalSummaryFromMessages(externalMessages as any)
|
|
1134
|
+
: '';
|
|
1135
|
+
// The transcript is authoritative for native-source providers. Use it unless it is
|
|
1136
|
+
// empty (not yet written) — only then fall back to whatever the screen parsed.
|
|
1137
|
+
if (externalSummary) return externalSummary;
|
|
1138
|
+
return parsedSummary || undefined;
|
|
1139
|
+
}
|
|
1140
|
+
return parsedSummary || undefined;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
private buildCompletedFinalizationDiagnostic(args: {
|
|
1144
|
+
blockReason: string;
|
|
1145
|
+
latestStatus?: any;
|
|
1146
|
+
latestVisibleStatus: string;
|
|
1147
|
+
waitedMs: number;
|
|
1148
|
+
pending: CompletedDebouncePending;
|
|
1149
|
+
emittedAfterFinalizationTimeout: boolean;
|
|
1150
|
+
}): Record<string, unknown> {
|
|
1151
|
+
let parsed: any = null;
|
|
1152
|
+
let parseError: string | undefined;
|
|
1153
|
+
try {
|
|
1154
|
+
parsed = this.adapter.getScriptParsedStatus();
|
|
1155
|
+
} catch (error: any) {
|
|
1156
|
+
parseError = error?.message || String(error);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
const evidence = this.completionFinalAssistantEvidence(parsed?.messages);
|
|
1160
|
+
if (evidence.source === 'external-native') {
|
|
1161
|
+
this.recordPendingTranscriptProbe(args.pending);
|
|
1162
|
+
}
|
|
1163
|
+
const visibleMessages = (Array.isArray(evidence.messages) ? evidence.messages : [])
|
|
1164
|
+
.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
1165
|
+
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
1166
|
+
const lastVisibleRole = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null;
|
|
1167
|
+
const lastVisibleKind = typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null;
|
|
1168
|
+
const lastVisibleContentLength = lastVisible ? flattenContent(lastVisible.content).trim().length : 0;
|
|
1169
|
+
|
|
1170
|
+
return {
|
|
1171
|
+
providerType: this.type,
|
|
1172
|
+
sessionId: this.instanceId,
|
|
1173
|
+
providerSessionId: this.providerSessionId || null,
|
|
1174
|
+
workspace: this.workingDir,
|
|
1175
|
+
blockReason: args.blockReason,
|
|
1176
|
+
emittedAfterFinalizationTimeout: args.emittedAfterFinalizationTimeout,
|
|
1177
|
+
waitedMs: args.waitedMs,
|
|
1178
|
+
maxWaitMs: COMPLETED_FINALIZATION_MAX_WAIT_MS,
|
|
1179
|
+
adapterStatus: typeof args.latestStatus?.status === 'string' ? args.latestStatus.status : null,
|
|
1180
|
+
latestVisibleStatus: args.latestVisibleStatus,
|
|
1181
|
+
parsedStatus: typeof parsed?.status === 'string' ? parsed.status : (parseError ? 'parse_error' : 'unknown'),
|
|
1182
|
+
parseError: parseError || undefined,
|
|
1183
|
+
finalAssistantPresent: evidence.present,
|
|
1184
|
+
finalAssistantEvidenceSource: evidence.source,
|
|
1185
|
+
visibleMessageCount: visibleMessages.length,
|
|
1186
|
+
lastVisibleRole,
|
|
1187
|
+
lastVisibleKind,
|
|
1188
|
+
lastVisibleContentLength,
|
|
1189
|
+
pendingStartedAt: this.generatingStartedAt || null,
|
|
1190
|
+
pendingFirstObservedAt: args.pending.firstObservedAt,
|
|
1191
|
+
pendingTimestamp: args.pending.timestamp,
|
|
1192
|
+
pendingDurationSec: args.pending.duration,
|
|
1193
|
+
previousBlockReason: args.pending.loggedBlockReason || null,
|
|
1194
|
+
transcriptProbeHistory: args.pending.transcriptProbeHistory || [],
|
|
1195
|
+
};
|
|
744
1196
|
}
|
|
745
1197
|
|
|
746
1198
|
private hasAdapterPendingResponse(): boolean {
|
|
@@ -765,32 +1217,97 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
765
1217
|
if (!isCliGeneratingLikeStatus(parsedRawStatus)) return false;
|
|
766
1218
|
if (adapterRawStatus !== 'idle') return false;
|
|
767
1219
|
if (hasNonEmptyCliModalButtons(parsedStatus?.activeModal ?? parsedStatus?.modal)) return false;
|
|
768
|
-
|
|
1220
|
+
if (this.hasAdapterPendingResponse()) return false;
|
|
1221
|
+
// Do not suppress when the adapter's raw response buffer is still non-empty.
|
|
1222
|
+
// This catches the case where isWaitingForResponse has already flipped to false
|
|
1223
|
+
// (so getPartialResponse() returns '') but the provider's native parser still
|
|
1224
|
+
// reports generating because it's parsing buffered content. Suppressing the
|
|
1225
|
+
// finalization block here would emit a false completion event while the provider
|
|
1226
|
+
// session is still actively processing its response stream.
|
|
1227
|
+
const adapterAny = this.adapter as any;
|
|
1228
|
+
if (typeof adapterAny?.responseBuffer === 'string' && adapterAny.responseBuffer.trim()) return false;
|
|
1229
|
+
return true;
|
|
769
1230
|
}
|
|
770
1231
|
|
|
771
|
-
private
|
|
772
|
-
if (latestVisibleStatus !== 'idle') return `status:${latestVisibleStatus}
|
|
1232
|
+
private getCompletedFinalizationBlock(latestVisibleStatus: string, pending: CompletedDebouncePending): CompletedFinalizationBlock | null {
|
|
1233
|
+
if (latestVisibleStatus !== 'idle') return { reason: `status:${latestVisibleStatus}`, terminal: true };
|
|
773
1234
|
|
|
774
1235
|
const adapterAny = this.adapter as any;
|
|
775
|
-
|
|
776
|
-
if (
|
|
1236
|
+
const approvalResolvedIdle = pending.previousStatus === 'waiting_approval';
|
|
1237
|
+
if (!approvalResolvedIdle) {
|
|
1238
|
+
if (adapterAny?.isWaitingForResponse === true) return { reason: 'adapter_waiting_for_response', terminal: true };
|
|
1239
|
+
if (adapterAny?.currentTurnScope) return { reason: 'adapter_turn_scope_active', terminal: true };
|
|
1240
|
+
if (this.hasAdapterPendingResponse()) return { reason: 'adapter_pending_response', terminal: true };
|
|
1241
|
+
}
|
|
777
1242
|
|
|
778
1243
|
const partial = typeof this.adapter.getPartialResponse === 'function'
|
|
779
1244
|
? this.adapter.getPartialResponse()
|
|
780
1245
|
: '';
|
|
781
|
-
if (typeof partial === 'string' && partial.trim()) return 'partial_response_pending';
|
|
1246
|
+
if (typeof partial === 'string' && partial.trim()) return { reason: 'partial_response_pending', terminal: true };
|
|
782
1247
|
|
|
783
1248
|
let parsed: any;
|
|
784
1249
|
try {
|
|
785
1250
|
parsed = this.adapter.getScriptParsedStatus();
|
|
786
1251
|
} catch (error: any) {
|
|
787
|
-
return `parse_error:${error?.message || String(error)}
|
|
1252
|
+
return { reason: `parse_error:${error?.message || String(error)}` };
|
|
788
1253
|
}
|
|
789
1254
|
|
|
790
1255
|
const parsedStatus = typeof parsed?.status === 'string' ? parsed.status : 'unknown';
|
|
791
|
-
if (parsedStatus !== 'idle')
|
|
792
|
-
|
|
793
|
-
|
|
1256
|
+
if (parsedStatus !== 'idle') {
|
|
1257
|
+
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
1258
|
+
if (this.shouldSuppressStaleParsedBusyStatus(parsed, adapterStatus)) return null;
|
|
1259
|
+
return { reason: `parsed_status:${parsedStatus}`, terminal: isCliGeneratingLikeStatus(parsedStatus) };
|
|
1260
|
+
}
|
|
1261
|
+
if (parsed?.activeModal || parsed?.modal) return { reason: 'parsed_modal_active', terminal: true };
|
|
1262
|
+
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1263
|
+
const finalAssistantEvidence = this.completionFinalAssistantEvidence(parsed?.messages);
|
|
1264
|
+
const allowMissingAssistantTimeout = !!(this.settings.meshNodeFor || this.settings.meshActiveTaskId || this.settings.launchedByCoordinator);
|
|
1265
|
+
LOG.debug('CLI', `[${this.type}] finalAssistantEvidence: present=${finalAssistantEvidence.present} source=${finalAssistantEvidence.source} adapterOwnsMessagesElsewhere=${adapterOwnsMessagesElsewhere} parsedStatus=${parsedStatus}`);
|
|
1266
|
+
if (!finalAssistantEvidence.present) {
|
|
1267
|
+
if (adapterOwnsMessagesElsewhere) {
|
|
1268
|
+
if (finalAssistantEvidence.source === 'external-native') {
|
|
1269
|
+
const probe = this.recordPendingTranscriptProbe(pending);
|
|
1270
|
+
if (probe && !pending.loggedTranscriptProbe) {
|
|
1271
|
+
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`);
|
|
1272
|
+
pending.loggedTranscriptProbe = true;
|
|
1273
|
+
}
|
|
1274
|
+
LOG.debug('CLI', `[${this.type}] external-native probe result: lastRole=${probe?.lastRole} contentLen=${probe?.contentLen}`);
|
|
1275
|
+
if (probe?.lastRole === 'assistant' && (probe.contentLen ?? 0) > 0) {
|
|
1276
|
+
return null;
|
|
1277
|
+
}
|
|
1278
|
+
if (this.type === 'antigravity-cli') {
|
|
1279
|
+
return null;
|
|
1280
|
+
}
|
|
1281
|
+
return { reason: 'missing_final_assistant', terminal: true, allowTimeout: allowMissingAssistantTimeout };
|
|
1282
|
+
}
|
|
1283
|
+
if ((this.provider as any).requiresFinalAssistantBeforeIdle === true) {
|
|
1284
|
+
return { reason: 'missing_final_assistant', terminal: true, allowTimeout: allowMissingAssistantTimeout };
|
|
1285
|
+
}
|
|
1286
|
+
} else {
|
|
1287
|
+
LOG.debug('CLI', `[${this.type}] missing_final_assistant (not ownsExternal) requiresFinalAssistant=${!!(this.provider as any).requiresFinalAssistantBeforeIdle}`);
|
|
1288
|
+
return {
|
|
1289
|
+
reason: 'missing_final_assistant',
|
|
1290
|
+
terminal: (this.provider as any).requiresFinalAssistantBeforeIdle === true,
|
|
1291
|
+
allowTimeout: allowMissingAssistantTimeout,
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
// Guard: if the screen still shows an approval/choice prompt as the last visible text,
|
|
1297
|
+
// the turn is not complete even if the parsed status says idle and there is an assistant
|
|
1298
|
+
// message. This catches the case where waiting_approval→idle transitions occur before
|
|
1299
|
+
// the modal has been resolved (e.g. the PTY rendered the prompt but no button press fired).
|
|
1300
|
+
try {
|
|
1301
|
+
const screenText = typeof (this.adapter as any).getScreenText === 'function'
|
|
1302
|
+
? String((this.adapter as any).getScreenText() || '')
|
|
1303
|
+
: '';
|
|
1304
|
+
if (screenText) {
|
|
1305
|
+
const tailLines = screenText.split(/\r?\n/).slice(-16).join('\n');
|
|
1306
|
+
if (looksLikeActiveApprovalPromptText(tailLines)) {
|
|
1307
|
+
return { reason: 'screen_shows_approval_prompt', terminal: approvalResolvedIdle };
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
} catch { /* defensive: screen text read is best-effort */ }
|
|
794
1311
|
|
|
795
1312
|
return null;
|
|
796
1313
|
}
|
|
@@ -809,7 +1326,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
809
1326
|
|
|
810
1327
|
const latestStatus = this.adapter.getStatus({ allowParse: false });
|
|
811
1328
|
const latestAutoApproveActive = latestStatus.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
812
|
-
const latestVisibleStatus = latestAutoApproveActive ? 'generating' : latestStatus.status;
|
|
1329
|
+
const latestVisibleStatus = latestAutoApproveActive || this.autoApproveBusy ? 'generating' : latestStatus.status;
|
|
1330
|
+
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
1331
|
if (latestVisibleStatus !== 'idle') {
|
|
814
1332
|
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed ${latestVisibleStatus})`);
|
|
815
1333
|
this.completedDebouncePending = null;
|
|
@@ -817,10 +1335,12 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
817
1335
|
return;
|
|
818
1336
|
}
|
|
819
1337
|
|
|
820
|
-
const
|
|
821
|
-
if (
|
|
1338
|
+
const block = this.getCompletedFinalizationBlock(latestVisibleStatus, pending);
|
|
1339
|
+
if (block) {
|
|
1340
|
+
const blockReason = block.reason;
|
|
822
1341
|
const waitedMs = Date.now() - pending.firstObservedAt;
|
|
823
|
-
|
|
1342
|
+
LOG.debug('CLI', `[${this.type}] finalization block: reason=${blockReason} terminal=${block.terminal} waitedMs=${waitedMs} maxWait=${COMPLETED_FINALIZATION_MAX_WAIT_MS}`);
|
|
1343
|
+
if ((block.terminal && !block.allowTimeout) || waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
|
|
824
1344
|
if (pending.loggedBlockReason !== blockReason) {
|
|
825
1345
|
LOG.info('CLI', `[${this.type}] waiting to emit completed until transcript finalizes (${blockReason})`);
|
|
826
1346
|
pending.loggedBlockReason = blockReason;
|
|
@@ -828,10 +1348,29 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
828
1348
|
this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
|
|
829
1349
|
return;
|
|
830
1350
|
}
|
|
831
|
-
|
|
1351
|
+
const completionDiagnostic = this.buildCompletedFinalizationDiagnostic({
|
|
1352
|
+
blockReason,
|
|
1353
|
+
latestStatus,
|
|
1354
|
+
latestVisibleStatus,
|
|
1355
|
+
waitedMs,
|
|
1356
|
+
pending,
|
|
1357
|
+
emittedAfterFinalizationTimeout: true,
|
|
1358
|
+
});
|
|
1359
|
+
LOG.warn('CLI', `[${this.type}] emitting completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
|
|
1360
|
+
this.pushEvent({
|
|
1361
|
+
event: 'agent:generating_completed',
|
|
1362
|
+
chatTitle: pending.chatTitle,
|
|
1363
|
+
duration: pending.duration,
|
|
1364
|
+
timestamp: pending.timestamp,
|
|
1365
|
+
finalSummary: blockReason.startsWith('parsed_status:')
|
|
1366
|
+
? ''
|
|
1367
|
+
: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
|
|
1368
|
+
completionDiagnostic,
|
|
1369
|
+
});
|
|
832
1370
|
this.completedDebouncePending = null;
|
|
833
1371
|
this.completedDebounceTimer = null;
|
|
834
1372
|
this.generatingStartedAt = 0;
|
|
1373
|
+
this.lastApprovalEventFingerprint = '';
|
|
835
1374
|
return;
|
|
836
1375
|
}
|
|
837
1376
|
|
|
@@ -841,11 +1380,12 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
841
1380
|
chatTitle: pending.chatTitle,
|
|
842
1381
|
duration: pending.duration,
|
|
843
1382
|
timestamp: pending.timestamp,
|
|
844
|
-
finalSummary:
|
|
1383
|
+
finalSummary: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
|
|
845
1384
|
});
|
|
846
1385
|
this.completedDebouncePending = null;
|
|
847
1386
|
this.completedDebounceTimer = null;
|
|
848
1387
|
this.generatingStartedAt = 0;
|
|
1388
|
+
this.lastApprovalEventFingerprint = '';
|
|
849
1389
|
}
|
|
850
1390
|
|
|
851
1391
|
private maybeAutoApproveStatus(adapterStatus: any, now = Date.now()): boolean {
|
|
@@ -853,33 +1393,137 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
853
1393
|
// Guard re-entry: onStatusChange/getState can observe the same modal multiple
|
|
854
1394
|
// times while the PTY absorbs the approval key. Without this flag, repeated
|
|
855
1395
|
// 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
|
-
|
|
1396
|
+
// However, Claude Code can present a second approval immediately after the
|
|
1397
|
+
// first. Resolve a changed modal signature even while the previous write is
|
|
1398
|
+
// still inside the short busy window.
|
|
1399
|
+
if (!autoApproveActive) {
|
|
1400
|
+
this.lastAutoApprovalSignature = '';
|
|
1401
|
+
// Clear the settle gate so the next approval starts its own quiet
|
|
1402
|
+
// window from scratch (a stale timestamp would let it fire instantly).
|
|
1403
|
+
this.pendingAutoApprovalSignature = '';
|
|
1404
|
+
this.pendingAutoApprovalSince = 0;
|
|
1405
|
+
if (this.autoApproveSettleTimer) { clearTimeout(this.autoApproveSettleTimer); this.autoApproveSettleTimer = null; }
|
|
1406
|
+
return autoApproveActive;
|
|
1407
|
+
}
|
|
1408
|
+
const modal = adapterStatus.activeModal;
|
|
1409
|
+
// (fix) Do not auto-approve when no concrete modal/buttons are present.
|
|
1410
|
+
// Claude TUI flaps between paints; without this guard adapterStatus
|
|
1411
|
+
// could report status=waiting_approval with activeModal=null (or with
|
|
1412
|
+
// an empty buttons array briefly) and we'd still call
|
|
1413
|
+
// resolveModal(-1) — which used to type "1" into the prompt
|
|
1414
|
+
// repeatedly. Skip until a real modal is captured.
|
|
1415
|
+
const buttons = Array.isArray(modal?.buttons)
|
|
1416
|
+
? modal.buttons.map((b: any) => String(b || '').trim()).filter(Boolean)
|
|
1417
|
+
: [];
|
|
1418
|
+
if (!modal || buttons.length === 0) {
|
|
1419
|
+
return autoApproveActive;
|
|
1420
|
+
}
|
|
1421
|
+
const { index: buttonIndex, label: buttonLabel } = pickAutoApprovalButton(buttons);
|
|
1422
|
+
if (buttonIndex < 0) {
|
|
1423
|
+
// No concrete button matched — don't pick a random index, just
|
|
1424
|
+
// surface the modal so the user can decide.
|
|
1425
|
+
return autoApproveActive;
|
|
1426
|
+
}
|
|
1427
|
+
// Include the FSM's approval entry seq: two distinct back-to-back
|
|
1428
|
+
// approvals can carry identical message/buttons (common with
|
|
1429
|
+
// claude-cli). Without the seq their signatures collide and the 5s
|
|
1430
|
+
// busy-window re-entry guard below swallows the second auto-approve,
|
|
1431
|
+
// leaving it stuck. The seq is bumped by the FSM on every fresh
|
|
1432
|
+
// waiting_approval entry, so a new approval always yields a new
|
|
1433
|
+
// signature and fires through.
|
|
1434
|
+
const approvalEntrySeq = typeof adapterStatus?.approvalEntrySeq === 'number'
|
|
1435
|
+
? adapterStatus.approvalEntrySeq
|
|
1436
|
+
: 0;
|
|
1437
|
+
const signature = [
|
|
1438
|
+
approvalEntrySeq,
|
|
1439
|
+
typeof modal?.message === 'string' ? modal.message.trim() : '',
|
|
1440
|
+
buttons.join('|'),
|
|
1441
|
+
buttonIndex,
|
|
1442
|
+
].join('::');
|
|
1443
|
+
// Already fired for this exact modal and still inside the busy window —
|
|
1444
|
+
// nothing to do (re-entry guard for repeated snapshots of one modal).
|
|
1445
|
+
if (this.autoApproveBusy && signature === this.lastAutoApprovalSignature) {
|
|
1446
|
+
return autoApproveActive;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// Settle gate: only fire once this exact signature has been stable for
|
|
1450
|
+
// AUTO_APPROVE_SETTLE_MS. A still-streaming prompt mutates its
|
|
1451
|
+
// message/buttons each frame → new signature → clock restarts, so we
|
|
1452
|
+
// never approve a half-rendered prompt (the "resolves too fast" bug).
|
|
1453
|
+
if (signature !== this.pendingAutoApprovalSignature) {
|
|
1454
|
+
this.pendingAutoApprovalSignature = signature;
|
|
1455
|
+
this.pendingAutoApprovalSince = now;
|
|
869
1456
|
}
|
|
1457
|
+
const settledForMs = now - this.pendingAutoApprovalSince;
|
|
1458
|
+
if (settledForMs < CliProviderInstance.AUTO_APPROVE_SETTLE_MS) {
|
|
1459
|
+
// Not yet settled. Arm a timer to re-check after the remaining quiet
|
|
1460
|
+
// window — the PTY may go silent once the prompt finishes painting,
|
|
1461
|
+
// so there is no guaranteed status-change frame to re-drive us.
|
|
1462
|
+
if (this.autoApproveSettleTimer) clearTimeout(this.autoApproveSettleTimer);
|
|
1463
|
+
this.autoApproveSettleTimer = setTimeout(() => {
|
|
1464
|
+
this.autoApproveSettleTimer = null;
|
|
1465
|
+
this.recheckAutoApproveSettled();
|
|
1466
|
+
}, CliProviderInstance.AUTO_APPROVE_SETTLE_MS - settledForMs + 20);
|
|
1467
|
+
return autoApproveActive;
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
// Settled — fire the approve key.
|
|
1471
|
+
if (this.autoApproveSettleTimer) { clearTimeout(this.autoApproveSettleTimer); this.autoApproveSettleTimer = null; }
|
|
1472
|
+
this.autoApproveBusy = true;
|
|
1473
|
+
this.lastAutoApprovalSignature = signature;
|
|
1474
|
+
this.pendingAutoApprovalSignature = '';
|
|
1475
|
+
this.pendingAutoApprovalSince = 0;
|
|
1476
|
+
if (this.autoApproveBusyTimer) clearTimeout(this.autoApproveBusyTimer);
|
|
1477
|
+
this.autoApproveBusyTimer = setTimeout(() => {
|
|
1478
|
+
this.autoApproveBusy = false;
|
|
1479
|
+
this.autoApproveBusyTimer = null;
|
|
1480
|
+
this.lastAutoApprovalSignature = '';
|
|
1481
|
+
}, 5000);
|
|
1482
|
+
this.recordAutoApproval(modal?.message, buttonLabel, now);
|
|
1483
|
+
setTimeout(() => {
|
|
1484
|
+
this.adapter.resolveModal(buttonIndex);
|
|
1485
|
+
}, 0);
|
|
870
1486
|
return autoApproveActive;
|
|
871
1487
|
}
|
|
872
1488
|
|
|
1489
|
+
/**
|
|
1490
|
+
* Re-drive the auto-approve check after the settle quiet window elapses.
|
|
1491
|
+
* The PTY may have gone silent once the approval prompt finished painting,
|
|
1492
|
+
* so no status-change frame is guaranteed to re-enter maybeAutoApproveStatus
|
|
1493
|
+
* — this timer-driven re-check picks up the now-settled modal and fires.
|
|
1494
|
+
* Deliberately lighter than detectStatusTransition(): it only re-evaluates
|
|
1495
|
+
* the approval decision; the next real PTY frame refreshes visible status.
|
|
1496
|
+
*/
|
|
1497
|
+
private recheckAutoApproveSettled(): void {
|
|
1498
|
+
try {
|
|
1499
|
+
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
1500
|
+
this.maybeAutoApproveStatus(adapterStatus, Date.now());
|
|
1501
|
+
} catch { /* adapter gone / transient — next frame retries */ }
|
|
1502
|
+
}
|
|
1503
|
+
|
|
873
1504
|
private detectStatusTransition(): void {
|
|
874
1505
|
const now = Date.now();
|
|
875
1506
|
// Status-change handling is a hot path: PTY output can fire it many times
|
|
876
1507
|
// during long-running CLI sessions. Keep this path on adapter-owned light
|
|
877
1508
|
// state only; rich provider parsing is reserved for getState/read_chat.
|
|
878
1509
|
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
1510
|
+
const adapterProviderSessionId = normalizeProviderSessionId(
|
|
1511
|
+
this.provider,
|
|
1512
|
+
typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
|
|
1513
|
+
);
|
|
1514
|
+
if (adapterProviderSessionId) {
|
|
1515
|
+
this.promoteProviderSessionId(adapterProviderSessionId);
|
|
1516
|
+
}
|
|
879
1517
|
const parsedStatus = null;
|
|
880
1518
|
const rawStatus = adapterStatus.status;
|
|
881
1519
|
const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, now);
|
|
882
|
-
|
|
1520
|
+
// During the autoApproveBusy window (2s after firing approval key), the PTY
|
|
1521
|
+
// can briefly report 'idle' before the next generating phase starts. Treat that
|
|
1522
|
+
// transient idle as 'generating' to suppress a spurious agent:generating_completed
|
|
1523
|
+
// push notification. The adapter's status is otherwise authoritative — native
|
|
1524
|
+
// transcript shape does NOT override the FSM's busy/idle decision.
|
|
1525
|
+
const autoApproveHoldIdle = this.autoApproveBusy && rawStatus === 'idle';
|
|
1526
|
+
const newStatus = autoApproveActive || autoApproveHoldIdle ? 'generating' : rawStatus;
|
|
883
1527
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
884
1528
|
const chatTitle = `${this.provider.name} · ${dirName}`;
|
|
885
1529
|
const partial = this.adapter.getPartialResponse();
|
|
@@ -891,10 +1535,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
891
1535
|
if (newStatus !== this.lastStatus) {
|
|
892
1536
|
LOG.info('CLI', `[${this.type}] status: ${this.lastStatus} → ${newStatus}`);
|
|
893
1537
|
if (this.lastStatus === 'idle' && newStatus === 'generating') {
|
|
1538
|
+
// If a completion event is already pending and the turn has ended
|
|
1539
|
+
// (generatingStartedAt===0), the PTY is painting its prompt area
|
|
1540
|
+
// after completing. Ignore this blip — do not cancel the pending
|
|
1541
|
+
// completion and do not advance lastStatus to generating.
|
|
1542
|
+
if (this.completedDebouncePending && this.generatingStartedAt === 0) {
|
|
1543
|
+
LOG.debug('CLI', `[${this.type}] ignoring post-completion PTY generating blip (generatingStartedAt=0)`);
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
894
1546
|
this.suppressIdleHistoryReplay = false;
|
|
895
1547
|
// Cancel any pending completed event (multi-step: idle→generating resume)
|
|
896
1548
|
if (this.completedDebouncePending) {
|
|
897
|
-
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed generating)`);
|
|
1549
|
+
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed generating) generatingStartedAt=${this.generatingStartedAt} isWaitingForResponse=${!!(this.adapter as any)?.isWaitingForResponse}`);
|
|
898
1550
|
if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
|
|
899
1551
|
this.completedDebouncePending = null;
|
|
900
1552
|
}
|
|
@@ -926,10 +1578,29 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
926
1578
|
if (!this.generatingStartedAt) this.generatingStartedAt = now;
|
|
927
1579
|
const modal = adapterStatus.activeModal;
|
|
928
1580
|
LOG.info('CLI', `[${this.type}] approval modal: "${modal?.message?.slice(0, 80) ?? 'none'}"`);
|
|
929
|
-
//
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1581
|
+
// Include the FSM's approval entry seq, mirroring the auto-approve
|
|
1582
|
+
// path (maybeAutoApproveStatus) and resolveModal's sameEntryReResolve
|
|
1583
|
+
// guard. Two distinct back-to-back approvals can carry identical
|
|
1584
|
+
// message/buttons (very common with claude-cli's "Allow Bash
|
|
1585
|
+
// command?"). Without the seq their fingerprints collide and the dedup
|
|
1586
|
+
// below silently drops the second waiting_approval event — it is never
|
|
1587
|
+
// emitted, so it cannot even land in the pending inbox for a later
|
|
1588
|
+
// read_chat reconcile to recover. The seq is bumped by the FSM on every
|
|
1589
|
+
// fresh waiting_approval entry, so a new approval always yields a new
|
|
1590
|
+
// fingerprint and emits.
|
|
1591
|
+
const approvalEntrySeq = typeof adapterStatus?.approvalEntrySeq === 'number'
|
|
1592
|
+
? adapterStatus.approvalEntrySeq
|
|
1593
|
+
: 0;
|
|
1594
|
+
const approvalFingerprint = JSON.stringify({
|
|
1595
|
+
message: typeof modal?.message === 'string' ? modal.message.trim() : '',
|
|
1596
|
+
buttons: Array.isArray(modal?.buttons) ? modal.buttons.map((button: unknown) => String(button).trim()) : [],
|
|
1597
|
+
seq: approvalEntrySeq,
|
|
1598
|
+
});
|
|
1599
|
+
// PTY redraws repeat the same modal content; fingerprint dedup prevents duplicate events.
|
|
1600
|
+
// Do NOT also gate on lastStatus: consecutive approvals can arrive waiting_approval→waiting_approval
|
|
1601
|
+
// (e.g. antigravity-cli resolves one prompt and immediately shows the next) and would be silently dropped.
|
|
1602
|
+
if (approvalFingerprint !== this.lastApprovalEventFingerprint) {
|
|
1603
|
+
this.lastApprovalEventFingerprint = approvalFingerprint;
|
|
933
1604
|
this.appendRuntimeSystemMessage(
|
|
934
1605
|
this.formatApprovalRequestMessage(modal?.message, modal?.buttons),
|
|
935
1606
|
`approval_request:${now}`,
|
|
@@ -941,22 +1612,113 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
941
1612
|
modalButtons: modal?.buttons,
|
|
942
1613
|
});
|
|
943
1614
|
}
|
|
1615
|
+
} else if (newStatus === 'generating' && this.lastStatus === 'waiting_approval') {
|
|
1616
|
+
// Approval resolved and the agent resumed work. Defense-in-depth:
|
|
1617
|
+
// clear the approval emit fingerprint here too (not only on
|
|
1618
|
+
// completion at scheduleCompletedDebounceFlush). A subsequent
|
|
1619
|
+
// waiting_approval with the same modal content as the one just
|
|
1620
|
+
// resolved would otherwise collide with the stale fingerprint and be
|
|
1621
|
+
// dropped. The seq in the fingerprint already separates entries; this
|
|
1622
|
+
// reset is a belt-and-suspenders guard for the re-entry case.
|
|
1623
|
+
this.lastApprovalEventFingerprint = '';
|
|
944
1624
|
} else if (newStatus === 'idle' && (this.lastStatus === 'generating' || this.lastStatus === 'waiting_approval')) {
|
|
945
1625
|
const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : 0;
|
|
946
|
-
//
|
|
1626
|
+
// Guard: if generatingStartedAt===0 and no debounce pending, the generating phase
|
|
1627
|
+
// was entered from 'starting' state (startup PTY noise), not from a real idle→generating
|
|
1628
|
+
// task dispatch. The idle→generating handler is the only code path that sets
|
|
1629
|
+
// generatingStartedAt and generatingDebouncePending, so both being absent means no
|
|
1630
|
+
// task was ever dispatched. Suppress the spurious completion event and fall through
|
|
1631
|
+
// to a simple lastStatus update.
|
|
1632
|
+
if (!this.generatingStartedAt && !this.generatingDebouncePending) {
|
|
1633
|
+
LOG.debug('CLI', `[${this.type}] suppressed startup-phase generating→idle blip (generatingStartedAt=0, no debounce pending)`);
|
|
1634
|
+
} else
|
|
1635
|
+
// If debounce still pending (generating lasted < 1s), cancel both UI events.
|
|
1636
|
+
// Still emit agent:generating_completed so mesh orchestration can record
|
|
1637
|
+
// task_completed for direct dispatches that complete faster than the debounce.
|
|
947
1638
|
if (this.generatingDebouncePending) {
|
|
948
|
-
|
|
1639
|
+
const shortDurationMs = this.generatingStartedAt ? now - this.generatingStartedAt : 0;
|
|
1640
|
+
LOG.info('CLI', `[${this.type}] suppressed short generating (${shortDurationMs}ms)`);
|
|
949
1641
|
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
1642
|
+
// Emit completion for mesh task association even though the UI generating
|
|
1643
|
+
// started/completed pair is suppressed (too short for visible UI update).
|
|
1644
|
+
let shortFinalSummary: string | undefined;
|
|
1645
|
+
let shortEvidenceSource: CompletionFinalAssistantEvidence['source'] = 'unavailable';
|
|
1646
|
+
try {
|
|
1647
|
+
const parsedMessages = this.adapter?.getScriptParsedStatus()?.messages;
|
|
1648
|
+
const evidence = this.completionFinalAssistantEvidence(parsedMessages);
|
|
1649
|
+
shortEvidenceSource = evidence.source;
|
|
1650
|
+
shortFinalSummary = extractFinalSummaryFromMessages(evidence.messages as any);
|
|
1651
|
+
} catch { /* best-effort */ }
|
|
1652
|
+
// If a real response is confirmed, retroactively emit started so the chat
|
|
1653
|
+
// bubble appears even though the debounce suppressed the original event.
|
|
1654
|
+
if (shortFinalSummary) {
|
|
1655
|
+
this.pushEvent({ event: 'agent:generating_started', chatTitle, timestamp: now - shortDurationMs });
|
|
1656
|
+
}
|
|
950
1657
|
this.generatingDebouncePending = null;
|
|
951
1658
|
this.generatingStartedAt = 0;
|
|
1659
|
+
const missingEvidence = ((this.provider as any).requiresFinalAssistantBeforeIdle === true || shortEvidenceSource === 'external-native') && !shortFinalSummary;
|
|
1660
|
+
if (missingEvidence) {
|
|
1661
|
+
LOG.warn('CLI', `[${this.type}] short completion missing final assistant evidence (source=${shortEvidenceSource})`);
|
|
1662
|
+
}
|
|
1663
|
+
// When evidence is missing and there is no active mesh task context, suppress
|
|
1664
|
+
// the completion event. Providers with requiresFinalAssistantBeforeIdle or
|
|
1665
|
+
// external-native history must confirm a final assistant message before the
|
|
1666
|
+
// coordinator records task_completed. Only emit here if a mesh task is active
|
|
1667
|
+
// so the coordinator can apply its own timeout/retry logic.
|
|
1668
|
+
const hasMeshContext = !!(this.settings.meshNodeFor || this.settings.meshActiveTaskId || this.settings.launchedByCoordinator);
|
|
1669
|
+
if (missingEvidence && !hasMeshContext) {
|
|
1670
|
+
LOG.info('CLI', `[${this.type}] short completion suppressed: missing final assistant evidence, no mesh context (source=${shortEvidenceSource})`);
|
|
1671
|
+
// completedDebouncePending intentionally left null — the session is now idle
|
|
1672
|
+
// with no confirmed turn, matching the startup-blip suppression semantics.
|
|
1673
|
+
} else {
|
|
1674
|
+
this.pushEvent({
|
|
1675
|
+
event: 'agent:generating_completed',
|
|
1676
|
+
chatTitle,
|
|
1677
|
+
duration: 0,
|
|
1678
|
+
timestamp: now,
|
|
1679
|
+
finalSummary: shortFinalSummary,
|
|
1680
|
+
completionDiagnostic: {
|
|
1681
|
+
reason: 'short_generating_suppressed',
|
|
1682
|
+
shortDurationMs,
|
|
1683
|
+
finalAssistantEvidenceSource: shortEvidenceSource,
|
|
1684
|
+
...(missingEvidence ? { blockReason: 'missing_final_assistant' } : {}),
|
|
1685
|
+
},
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
952
1688
|
} else {
|
|
953
1689
|
// Debounce completed, then require the rich transcript path that read_chat
|
|
954
1690
|
// uses to show an idle turn whose last user-facing message is assistant.
|
|
955
|
-
this.completedDebouncePending = {
|
|
956
|
-
|
|
1691
|
+
this.completedDebouncePending = {
|
|
1692
|
+
chatTitle,
|
|
1693
|
+
duration,
|
|
1694
|
+
timestamp: now,
|
|
1695
|
+
firstObservedAt: now,
|
|
1696
|
+
previousStatus: this.lastStatus,
|
|
1697
|
+
};
|
|
1698
|
+
const ownsExternalHistory = !!(this.adapter as any)?.chatMessagesOwnedExternally;
|
|
1699
|
+
const flushDelay = ownsExternalHistory ? 0 : 3000;
|
|
1700
|
+
LOG.debug('CLI', `[${this.type}] set completedDebouncePending duration=${duration}s ownsExternalHistory=${ownsExternalHistory} flushDelay=${flushDelay}ms generatingStartedAt=${this.generatingStartedAt}`);
|
|
1701
|
+
this.scheduleCompletedDebounceFlush(flushDelay);
|
|
957
1702
|
}
|
|
958
1703
|
} else if (newStatus === 'idle' && this.lastStatus === 'starting') {
|
|
959
1704
|
this.pushEvent({ event: 'agent:ready', chatTitle, timestamp: now });
|
|
1705
|
+
} else if (newStatus === 'error') {
|
|
1706
|
+
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
1707
|
+
this.generatingDebouncePending = null;
|
|
1708
|
+
if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
|
|
1709
|
+
this.completedDebouncePending = null;
|
|
1710
|
+
this.errorMessage = adapterStatus.errorMessage || this.errorMessage;
|
|
1711
|
+
this.errorReason = (adapterStatus.errorReason as ProviderErrorReason) || this.errorReason;
|
|
1712
|
+
this.pushEvent({
|
|
1713
|
+
event: 'agent:stopped',
|
|
1714
|
+
chatTitle,
|
|
1715
|
+
timestamp: now,
|
|
1716
|
+
finalSummary: adapterStatus.errorMessage || adapterStatus.errorReason || 'Provider reported an error',
|
|
1717
|
+
completionDiagnostic: {
|
|
1718
|
+
reason: adapterStatus.errorReason || 'provider_error',
|
|
1719
|
+
errorMessage: adapterStatus.errorMessage || undefined,
|
|
1720
|
+
},
|
|
1721
|
+
});
|
|
960
1722
|
} else if (newStatus === 'stopped') {
|
|
961
1723
|
// Cancel any pending debounce
|
|
962
1724
|
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
@@ -977,7 +1739,34 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
977
1739
|
// Monitor check (cooldown based notification, IDE/CLI common)
|
|
978
1740
|
const agentKey = `${this.type}:cli`;
|
|
979
1741
|
const monitorEvents = this.monitor.check(agentKey, newStatus, now, progressFingerprint);
|
|
1742
|
+
const monitorParsedStatus: any = parsedStatus;
|
|
980
1743
|
for (const me of monitorEvents) {
|
|
1744
|
+
if (
|
|
1745
|
+
me.type === 'monitor:long_generating'
|
|
1746
|
+
&& this.completionHasFinalAssistantMessage(monitorParsedStatus?.messages)
|
|
1747
|
+
&& !this.hasAdapterPendingResponse()
|
|
1748
|
+
&& !hasNonEmptyCliModalButtons(monitorParsedStatus?.activeModal ?? monitorParsedStatus?.modal)
|
|
1749
|
+
) {
|
|
1750
|
+
this.pushEvent({
|
|
1751
|
+
event: 'agent:generating_completed',
|
|
1752
|
+
chatTitle,
|
|
1753
|
+
duration: this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : undefined,
|
|
1754
|
+
timestamp: me.timestamp,
|
|
1755
|
+
finalSummary: extractFinalSummaryFromMessages(monitorParsedStatus?.messages),
|
|
1756
|
+
completionDiagnostic: {
|
|
1757
|
+
providerType: this.type,
|
|
1758
|
+
sessionId: this.instanceId,
|
|
1759
|
+
providerSessionId: this.providerSessionId || null,
|
|
1760
|
+
reconciliationReason: 'long_generating_monitor_final_summary',
|
|
1761
|
+
finalAssistantPresent: true,
|
|
1762
|
+
},
|
|
1763
|
+
});
|
|
1764
|
+
this.generatingStartedAt = 0;
|
|
1765
|
+
// Cancel any pending debounce flush — monitor already fired completion.
|
|
1766
|
+
if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
|
|
1767
|
+
this.completedDebouncePending = null;
|
|
1768
|
+
continue;
|
|
1769
|
+
}
|
|
981
1770
|
this.pushEvent({ event: me.type, agentKey: me.agentKey, message: me.message, elapsedSec: me.elapsedSec, timestamp: me.timestamp });
|
|
982
1771
|
}
|
|
983
1772
|
}
|
|
@@ -1003,9 +1792,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1003
1792
|
};
|
|
1004
1793
|
if (this.context?.emitProviderEvent) {
|
|
1005
1794
|
this.context.emitProviderEvent(enrichedEvent);
|
|
1006
|
-
|
|
1795
|
+
} else {
|
|
1796
|
+
this.events.push(enrichedEvent);
|
|
1797
|
+
}
|
|
1798
|
+
// Auto-detach a direct-dispatch mesh assignment once the dispatched
|
|
1799
|
+
// task reaches a terminal state. Leaving meshNodeFor pinned would
|
|
1800
|
+
// route this session's next unrelated turn (a dashboard chat) into
|
|
1801
|
+
// the coordinator as if it were the completion of another task.
|
|
1802
|
+
// We schedule after the emit so the originating coordinator still
|
|
1803
|
+
// observes the completion event with its routing marker intact.
|
|
1804
|
+
if (TERMINAL_MESH_EVENTS.has(event.event) && this.settings.meshActiveTaskId) {
|
|
1805
|
+
try { this.detachMeshAssignment(); } catch { /* best-effort */ }
|
|
1007
1806
|
}
|
|
1008
|
-
this.events.push(enrichedEvent);
|
|
1009
1807
|
}
|
|
1010
1808
|
|
|
1011
1809
|
private flushEvents(): ProviderEvent[] {
|
|
@@ -1258,12 +2056,28 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1258
2056
|
index,
|
|
1259
2057
|
source: 'parsed',
|
|
1260
2058
|
}));
|
|
2059
|
+
const getRole = (message: ChatMessage): string => typeof message.role === 'string'
|
|
2060
|
+
? message.role.trim().toLowerCase()
|
|
2061
|
+
: '';
|
|
1261
2062
|
const runtimeEntries: MergeEntry[] = this.runtimeMessages.map((entry, index) => ({
|
|
1262
2063
|
message: entry.message,
|
|
1263
2064
|
index: parsedMessages.length + index,
|
|
1264
|
-
source: 'runtime',
|
|
2065
|
+
source: 'runtime' as const,
|
|
1265
2066
|
runtimeKey: entry.key,
|
|
1266
|
-
}))
|
|
2067
|
+
})).filter((entry) => {
|
|
2068
|
+
const meta = entry.message.meta && typeof entry.message.meta === 'object' && !Array.isArray(entry.message.meta)
|
|
2069
|
+
? entry.message.meta as Record<string, unknown>
|
|
2070
|
+
: {};
|
|
2071
|
+
if (meta.runtimeInputAck !== true) return true;
|
|
2072
|
+
const runtimeText = flattenContent(entry.message.content).replace(/\s+/g, ' ').trim();
|
|
2073
|
+
if (!runtimeText) return false;
|
|
2074
|
+
return !parsedEntries.some((parsedEntry) => {
|
|
2075
|
+
const parsedRole = getRole(parsedEntry.message);
|
|
2076
|
+
if (parsedRole !== 'user' && parsedRole !== 'human') return false;
|
|
2077
|
+
const parsedText = flattenContent(parsedEntry.message.content).replace(/\s+/g, ' ').trim();
|
|
2078
|
+
return parsedText === runtimeText;
|
|
2079
|
+
});
|
|
2080
|
+
});
|
|
1267
2081
|
const getTime = (message: ChatMessage): number => {
|
|
1268
2082
|
const value = typeof message.receivedAt === 'number'
|
|
1269
2083
|
? message.receivedAt
|
|
@@ -1273,9 +2087,6 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1273
2087
|
return Number.isFinite(value) && value > 0 ? value : 0;
|
|
1274
2088
|
};
|
|
1275
2089
|
|
|
1276
|
-
const getRole = (message: ChatMessage): string => typeof message.role === 'string'
|
|
1277
|
-
? message.role.trim().toLowerCase()
|
|
1278
|
-
: '';
|
|
1279
2090
|
const isRuntimeOverlay = (entry: MergeEntry): boolean => {
|
|
1280
2091
|
if (entry.source !== 'runtime') return false;
|
|
1281
2092
|
const key = typeof entry.runtimeKey === 'string' ? entry.runtimeKey.trim().toLowerCase() : '';
|
|
@@ -1341,7 +2152,9 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1341
2152
|
this.providerSessionId = nextSessionId;
|
|
1342
2153
|
this.historyWriter.promoteHistorySession(this.type, previousHistorySessionId, nextSessionId);
|
|
1343
2154
|
this.historyWriter.writeSessionStart(this.type, nextSessionId, this.workingDir, this.instanceId);
|
|
1344
|
-
this.
|
|
2155
|
+
if (this.shouldHydrateExistingProviderHistory()) {
|
|
2156
|
+
this.restorePersistedHistoryFromCurrentSession();
|
|
2157
|
+
}
|
|
1345
2158
|
this.adapter.updateRuntimeMeta({ providerSessionId: nextSessionId });
|
|
1346
2159
|
this.onProviderSessionResolved?.({
|
|
1347
2160
|
instanceId: this.instanceId,
|
|
@@ -1354,9 +2167,27 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1354
2167
|
LOG.info('CLI', `[${this.type}] discovered provider session id: ${nextSessionId}`);
|
|
1355
2168
|
}
|
|
1356
2169
|
|
|
2170
|
+
private shouldHydrateExistingProviderHistory(): boolean {
|
|
2171
|
+
return this.launchMode === 'resume' || this.launchMode === 'manual';
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
private shouldSuppressFreshLaunchStartupReplay(parsedMessages: unknown[], parsedStatus: any, adapterStatus: any, parsedProviderSessionId = ''): boolean {
|
|
2175
|
+
if (this.launchMode !== 'new') return false;
|
|
2176
|
+
if (this.providerSessionId) return false;
|
|
2177
|
+
if (!Array.isArray(parsedMessages) || parsedMessages.length === 0) return false;
|
|
2178
|
+
if (!isIdleStatus(adapterStatus?.status) || !isIdleStatus(parsedStatus?.status)) return false;
|
|
2179
|
+
if (parsedProviderSessionId) return true;
|
|
2180
|
+
|
|
2181
|
+
const newestMessageAt = parsedMessages.reduce<number>((newest, message) => Math.max(newest, getMessageTime(message)), 0);
|
|
2182
|
+
|
|
2183
|
+
// Untimestamped idle parser output during a fresh launch is usually the
|
|
2184
|
+
// provider's last workspace transcript before a new turn exists.
|
|
2185
|
+
return newestMessageAt === 0;
|
|
2186
|
+
}
|
|
2187
|
+
|
|
1357
2188
|
private syncCanonicalSavedHistoryIfNeeded(): boolean {
|
|
1358
2189
|
if (!this.providerSessionId) return false;
|
|
1359
|
-
const canonicalHistory = this.provider.
|
|
2190
|
+
const canonicalHistory = this.provider.nativeHistory;
|
|
1360
2191
|
if (!canonicalHistory) return false;
|
|
1361
2192
|
|
|
1362
2193
|
if (isNativeSourceCanonicalHistory(canonicalHistory)) {
|
|
@@ -1401,6 +2232,14 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1401
2232
|
if (!materializeProviderNativeHistory(this.type, canonicalHistory, this.providerSessionId, this.workingDir, this.provider.scripts as any)) {
|
|
1402
2233
|
return false;
|
|
1403
2234
|
}
|
|
2235
|
+
// Full read is intentional: lastPersistedHistoryMessages is the COMPLETE
|
|
2236
|
+
// session transcript — emitted as statusMessages and used as the
|
|
2237
|
+
// prefix-comparison base for incremental appends — so a bounded tail
|
|
2238
|
+
// would both truncate output and break prefix dedup. This is gated to
|
|
2239
|
+
// once-per-2s (cache key above) for resume/manual launches only, so it
|
|
2240
|
+
// does not run on the per-subscribe/per-poll dashboard tail path (that
|
|
2241
|
+
// path goes through handleReadChat → readChatHistory with a bounded
|
|
2242
|
+
// tailLimit, which is now O(tail)).
|
|
1404
2243
|
const restoredHistory = readChatHistory(this.type, 0, Number.MAX_SAFE_INTEGER, this.providerSessionId, 0, this.provider.historyBehavior);
|
|
1405
2244
|
this.lastPersistedHistoryMessages = restoredHistory.messages.map((message) => ({
|
|
1406
2245
|
role: message.role,
|
|
@@ -1418,9 +2257,9 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1418
2257
|
private restorePersistedHistoryFromCurrentSession(): void {
|
|
1419
2258
|
if (!this.providerSessionId) return;
|
|
1420
2259
|
this.syncCanonicalSavedHistoryIfNeeded();
|
|
1421
|
-
const restoredHistory = isNativeSourceCanonicalHistory(this.provider.
|
|
2260
|
+
const restoredHistory = isNativeSourceCanonicalHistory(this.provider.nativeHistory)
|
|
1422
2261
|
? readProviderChatHistory(this.type, {
|
|
1423
|
-
canonicalHistory: this.provider.
|
|
2262
|
+
canonicalHistory: this.provider.nativeHistory,
|
|
1424
2263
|
historySessionId: this.providerSessionId,
|
|
1425
2264
|
workspace: this.workingDir,
|
|
1426
2265
|
offset: 0,
|