@adhdev/daemon-core 0.9.82-rc.23 → 0.9.82-rc.231
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 +1 -0
- package/dist/boot/process-hardening.d.ts +50 -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 +178 -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 +29 -0
- package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
- package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +3 -3
- package/dist/cli-adapters/terminal-backends/types.d.ts +1 -2
- package/dist/cli-adapters/terminal-screen.d.ts +2 -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 +32 -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-types.d.ts +2 -0
- package/dist/index.d.ts +47 -11
- package/dist/index.js +30490 -11310
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +30346 -11258
- 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 +90 -0
- package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
- package/dist/mesh/mesh-events-coordinator.d.ts +151 -0
- package/dist/mesh/mesh-events-pending.d.ts +33 -0
- package/dist/mesh/mesh-events-stale.d.ts +40 -0
- package/dist/mesh/mesh-events-utils.d.ts +14 -0
- package/dist/mesh/mesh-events.d.ts +5 -40
- package/dist/mesh/mesh-fast-forward.d.ts +41 -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 +58 -0
- package/dist/mesh/mesh-refine-status.d.ts +26 -0
- package/dist/mesh/mesh-review-inbox.d.ts +90 -0
- package/dist/mesh/mesh-runtime-store.d.ts +324 -0
- package/dist/mesh/mesh-task-stats.d.ts +49 -0
- package/dist/mesh/mesh-work-queue.d.ts +137 -6
- 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 +40 -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 +52 -0
- package/dist/providers/spec/cli-adapter.d.ts +102 -0
- package/dist/providers/spec/evaluator.d.ts +23 -0
- package/dist/providers/spec/fsm-driver.d.ts +231 -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 +91 -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 +129 -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 +126 -1
- 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 +6 -3
- package/src/agent-stream/poller.ts +2 -3
- package/src/boot/daemon-lifecycle.ts +22 -10
- package/src/boot/process-hardening.ts +89 -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 +1162 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +673 -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 +59 -11
- package/src/cli-adapters/raw-terminal-io.ts +252 -0
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +35 -29
- package/src/cli-adapters/terminal-backends/types.ts +1 -3
- package/src/cli-adapters/terminal-screen.ts +11 -81
- package/src/commands/chat-commands.ts +1806 -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 +4624 -538
- package/src/config/chat-history.ts +95 -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 +35 -6
- package/src/git/git-types.ts +2 -0
- package/src/git/git-worktree.ts +8 -1
- package/src/index.ts +127 -10
- 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 +437 -0
- package/src/mesh/mesh-delivery-policy.ts +315 -0
- package/src/mesh/mesh-events-coordinator.ts +1484 -0
- package/src/mesh/mesh-events-pending.ts +371 -0
- package/src/mesh/mesh-events-stale.ts +283 -0
- package/src/mesh/mesh-events-utils.ts +175 -0
- package/src/mesh/mesh-events.ts +24 -908
- package/src/mesh/mesh-fast-forward.ts +438 -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 +587 -102
- package/src/mesh/mesh-missions.ts +151 -0
- package/src/mesh/mesh-refine-status.ts +144 -0
- package/src/mesh/mesh-review-inbox.ts +307 -0
- package/src/mesh/mesh-runtime-store.ts +1305 -0
- package/src/mesh/mesh-task-stats.ts +154 -0
- package/src/mesh/mesh-work-queue.ts +490 -166
- 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 +850 -71
- 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 +138 -0
- package/src/providers/spec/cli-adapter.ts +646 -0
- package/src/providers/spec/evaluator.ts +321 -0
- package/src/providers/spec/fsm-driver.ts +749 -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 +184 -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 +173 -0
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +425 -0
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +138 -1
- 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,66 @@ 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 ExternalNativeFinalReconciliation = {
|
|
77
|
+
fingerprint: string;
|
|
78
|
+
finalSummary: string;
|
|
79
|
+
evidence: CompletionFinalAssistantEvidence;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
type ExternalTranscriptProbe = {
|
|
83
|
+
readAt: number;
|
|
84
|
+
msgCount: number;
|
|
85
|
+
lastRole: string | null;
|
|
86
|
+
lastKind: string | null;
|
|
87
|
+
contentLen: number;
|
|
88
|
+
sourcePath: string | null;
|
|
89
|
+
sourceMtimeMs: number | null;
|
|
90
|
+
mtimeAgeMs: number | null;
|
|
44
91
|
};
|
|
45
92
|
|
|
46
93
|
const COMPLETED_FINALIZATION_RETRY_MS = 1000;
|
|
47
94
|
const COMPLETED_FINALIZATION_MAX_WAIT_MS = 30_000;
|
|
48
95
|
|
|
96
|
+
/** Events that signal a dispatched mesh task has reached a terminal state.
|
|
97
|
+
* Detach the mesh assignment after emitting one of these so the worker's
|
|
98
|
+
* next unrelated turn doesn't impersonate another completion. */
|
|
99
|
+
const TERMINAL_MESH_EVENTS = new Set([
|
|
100
|
+
'agent:generating_completed',
|
|
101
|
+
'agent:stopped',
|
|
102
|
+
'agent:ready',
|
|
103
|
+
]);
|
|
104
|
+
|
|
49
105
|
const IMAGE_MIME_EXTENSIONS: Record<string, string> = {
|
|
50
106
|
'image/png': '.png',
|
|
51
107
|
'image/jpeg': '.jpg',
|
|
@@ -309,15 +365,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
309
365
|
private monitor: StatusMonitor;
|
|
310
366
|
private generatingDebounceTimer: NodeJS.Timeout | null = null;
|
|
311
367
|
private generatingDebouncePending: { chatTitle: string; timestamp: number } | null = null;
|
|
312
|
-
private
|
|
368
|
+
private lastApprovalEventFingerprint = '';
|
|
313
369
|
private autoApproveBusy = false;
|
|
314
370
|
private autoApproveBusyTimer: NodeJS.Timeout | null = null;
|
|
371
|
+
private lastAutoApprovalSignature = '';
|
|
315
372
|
private controlValues: Record<string, string | number | boolean> = {};
|
|
316
373
|
private summaryMetadata: unknown = undefined;
|
|
317
374
|
private appliedEffectKeys = new Set<string>();
|
|
318
375
|
private historyWriter: ChatHistoryWriter;
|
|
319
376
|
private runtimeMessages: Array<{ key: string; message: ChatMessage }> = [];
|
|
320
377
|
private lastPersistedHistoryMessages: PersistableCliHistoryMessage[] = [];
|
|
378
|
+
private lastAcknowledgedUserInputAt = 0;
|
|
379
|
+
private externalBusyIdleFingerprint = '';
|
|
321
380
|
private lastNativeSourceCanonicalCheckAt = 0;
|
|
322
381
|
private lastNativeSourceCanonicalCacheKey: string | undefined = undefined;
|
|
323
382
|
private cachedSqliteDb: {
|
|
@@ -330,6 +389,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
330
389
|
private suppressIdleHistoryReplay = false;
|
|
331
390
|
private errorMessage: string | undefined = undefined;
|
|
332
391
|
private errorReason: ProviderErrorReason | undefined = undefined;
|
|
392
|
+
private activeInteractivePrompt: InteractivePrompt | null = null;
|
|
333
393
|
|
|
334
394
|
private presentationMode: 'terminal' | 'chat';
|
|
335
395
|
private providerSessionId?: string;
|
|
@@ -370,7 +430,10 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
370
430
|
this.providerSessionId = options?.providerSessionId;
|
|
371
431
|
this.launchMode = options?.launchMode || 'new';
|
|
372
432
|
this.onProviderSessionResolved = options?.onProviderSessionResolved;
|
|
373
|
-
this.adapter =
|
|
433
|
+
this.adapter = createCliAdapter(provider as CliProviderModule, workingDir, cliArgs, options?.extraEnv || {}, transportFactory) as ProviderCliAdapter;
|
|
434
|
+
if (this.providerSessionId) {
|
|
435
|
+
this.adapter.updateRuntimeMeta({ providerSessionId: this.providerSessionId });
|
|
436
|
+
}
|
|
374
437
|
this.monitor = new StatusMonitor();
|
|
375
438
|
this.historyWriter = new ChatHistoryWriter();
|
|
376
439
|
}
|
|
@@ -412,7 +475,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
412
475
|
await this.adapter.spawn();
|
|
413
476
|
await this.enforceFreshSessionLaunchIfNeeded();
|
|
414
477
|
this.maybeAppendRuntimeRecoveryMessage(this.adapter.getRuntimeMetadata());
|
|
415
|
-
if (this.providerSessionId) {
|
|
478
|
+
if (this.providerSessionId && this.shouldHydrateExistingProviderHistory()) {
|
|
416
479
|
this.restorePersistedHistoryFromCurrentSession();
|
|
417
480
|
}
|
|
418
481
|
if (this.providerSessionId && this.launchMode === 'resume') {
|
|
@@ -486,14 +549,38 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
486
549
|
}
|
|
487
550
|
|
|
488
551
|
getState(): ProviderState {
|
|
552
|
+
// TODO(phase5-sandbox): JS override scripts (detectStatus, parseApproval,
|
|
553
|
+
// parseSession) are currently invoked by CliScriptRunner.invoke() via direct
|
|
554
|
+
// function calls — the scripts run in the daemon process with full Node.js
|
|
555
|
+
// access and no resource limits.
|
|
556
|
+
//
|
|
557
|
+
// When Phase 5 lands, CliScriptRunner should route these calls through a
|
|
558
|
+
// SandboxedScriptRunner (see providers/sdk/v1/sandbox/script-runner.ts) so
|
|
559
|
+
// that each call gets a fresh isolated-vm context with a 50 ms CPU limit and
|
|
560
|
+
// a 32 MB memory cap. The execution path to change is:
|
|
561
|
+
// CliScriptRunner.invoke() → SandboxedScriptRunner.run(scriptSource, context)
|
|
562
|
+
//
|
|
563
|
+
// This getState() call-site is NOT where the change goes — the wiring belongs
|
|
564
|
+
// in cli-script-runner.ts (CliScriptRunner.detectStatus / parseApproval /
|
|
565
|
+
// parseSession), with provider-loader.ts updated to store script source strings
|
|
566
|
+
// alongside the loaded function references for extended-legacy providers.
|
|
489
567
|
const adapterStatus = this.adapter.getStatus();
|
|
568
|
+
if (Object.prototype.hasOwnProperty.call(adapterStatus, 'activeInteractivePrompt')) {
|
|
569
|
+
this.activeInteractivePrompt = adapterStatus.activeInteractivePrompt ?? null;
|
|
570
|
+
}
|
|
490
571
|
let parsedStatus: any = null;
|
|
491
572
|
let parseErrorMessage: string | undefined;
|
|
492
573
|
if (typeof this.adapter.getScriptParsedStatus === 'function') {
|
|
493
574
|
try {
|
|
494
575
|
parsedStatus = this.adapter.getScriptParsedStatus() || null;
|
|
495
|
-
|
|
496
|
-
|
|
576
|
+
const parsedErrorMessage = typeof parsedStatus?.errorMessage === 'string' && parsedStatus.errorMessage.trim()
|
|
577
|
+
? parsedStatus.errorMessage.trim()
|
|
578
|
+
: undefined;
|
|
579
|
+
const parsedErrorReason = typeof parsedStatus?.errorReason === 'string' && parsedStatus.errorReason.trim()
|
|
580
|
+
? parsedStatus.errorReason.trim() as ProviderErrorReason
|
|
581
|
+
: undefined;
|
|
582
|
+
this.errorMessage = parsedErrorMessage;
|
|
583
|
+
this.errorReason = parsedErrorReason;
|
|
497
584
|
} catch (error: any) {
|
|
498
585
|
parseErrorMessage = error?.message || String(error);
|
|
499
586
|
this.errorMessage = parseErrorMessage;
|
|
@@ -503,22 +590,59 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
503
590
|
this.errorMessage = undefined;
|
|
504
591
|
this.errorReason = undefined;
|
|
505
592
|
}
|
|
506
|
-
const
|
|
507
|
-
const visibleStatus = parseErrorMessage
|
|
508
|
-
? 'error'
|
|
509
|
-
: (autoApproveActive ? 'generating' : adapterStatus.status);
|
|
510
|
-
const parsedProviderSessionId = normalizeProviderSessionId(
|
|
593
|
+
const adapterProviderSessionId = normalizeProviderSessionId(
|
|
511
594
|
this.provider,
|
|
512
|
-
typeof
|
|
595
|
+
typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
|
|
513
596
|
);
|
|
514
|
-
|
|
515
|
-
|
|
597
|
+
const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, Date.now());
|
|
598
|
+
const autoApproveHoldIdle = this.autoApproveBusy && adapterStatus.status === 'idle';
|
|
599
|
+
let visibleStatus = parseErrorMessage || parsedStatus?.status === 'error'
|
|
600
|
+
? 'error'
|
|
601
|
+
: (autoApproveActive || autoApproveHoldIdle ? 'generating' : adapterStatus.status);
|
|
602
|
+
const externalNativeFinal = this.getExternalNativeFinalReconciliation(parsedStatus?.messages, adapterStatus);
|
|
603
|
+
if (externalNativeFinal && isCliGeneratingLikeStatus(visibleStatus)) {
|
|
604
|
+
visibleStatus = 'idle';
|
|
605
|
+
}
|
|
606
|
+
// Adapter raw status can lag behind parsed/native evidence: if the spec driver
|
|
607
|
+
// has not yet emitted a state_changed(idle) event but the parsed transcript
|
|
608
|
+
// already shows a final assistant turn, treat the session as idle so that
|
|
609
|
+
// getState() agrees with what detectStatusTransition already recorded via
|
|
610
|
+
// lastStatus. Without this guard, getState() returns 'generating' even after
|
|
611
|
+
// the instance's lastStatus has flipped to 'idle', causing the dashboard to
|
|
612
|
+
// show a perpetual generating spinner.
|
|
613
|
+
if (isCliGeneratingLikeStatus(visibleStatus) && this.lastStatus === 'idle') {
|
|
614
|
+
visibleStatus = 'idle';
|
|
516
615
|
}
|
|
517
616
|
const runtime = this.adapter.getRuntimeMetadata();
|
|
518
617
|
this.maybeAppendRuntimeRecoveryMessage(runtime);
|
|
519
618
|
let parsedMessages = Array.isArray(parsedStatus?.messages)
|
|
520
619
|
? parsedStatus.messages
|
|
521
620
|
: [];
|
|
621
|
+
const parsedProviderSessionId = normalizeProviderSessionId(
|
|
622
|
+
this.provider,
|
|
623
|
+
typeof parsedStatus?.providerSessionId === 'string' ? parsedStatus.providerSessionId : '',
|
|
624
|
+
);
|
|
625
|
+
const suppressFreshLaunchStartupReplay = this.shouldSuppressFreshLaunchStartupReplay(
|
|
626
|
+
parsedMessages,
|
|
627
|
+
parsedStatus,
|
|
628
|
+
adapterStatus,
|
|
629
|
+
parsedProviderSessionId,
|
|
630
|
+
);
|
|
631
|
+
if (adapterProviderSessionId && !suppressFreshLaunchStartupReplay) {
|
|
632
|
+
this.promoteProviderSessionId(adapterProviderSessionId);
|
|
633
|
+
}
|
|
634
|
+
if (parsedProviderSessionId && !suppressFreshLaunchStartupReplay) {
|
|
635
|
+
this.promoteProviderSessionId(parsedProviderSessionId);
|
|
636
|
+
}
|
|
637
|
+
if (suppressFreshLaunchStartupReplay) {
|
|
638
|
+
parsedMessages = [];
|
|
639
|
+
}
|
|
640
|
+
// Adapter runtime metadata is transport-owned and is not guaranteed to
|
|
641
|
+
// identify this conversation. Spec adapters historically exposed the
|
|
642
|
+
// provider spec id (for example "codex-cli") as runtimeId, which made
|
|
643
|
+
// concurrent sessions share one activeChat identity until their native
|
|
644
|
+
// provider session ids were discovered.
|
|
645
|
+
const activeChatId = this.providerSessionId || this.instanceId;
|
|
522
646
|
const historyMessageCount = Number.isFinite(parsedStatus?.historyMessageCount)
|
|
523
647
|
? Math.max(0, Number(parsedStatus.historyMessageCount))
|
|
524
648
|
: null;
|
|
@@ -528,7 +652,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
528
652
|
: [];
|
|
529
653
|
}
|
|
530
654
|
const mergedMessages = this.mergeConversationMessages(parsedMessages);
|
|
531
|
-
const canonicalBackedHistory = this.
|
|
655
|
+
const canonicalBackedHistory = this.shouldHydrateExistingProviderHistory()
|
|
656
|
+
? this.syncCanonicalSavedHistoryIfNeeded()
|
|
657
|
+
: false;
|
|
658
|
+
const statusMessages = canonicalBackedHistory && this.lastPersistedHistoryMessages.length > 0
|
|
659
|
+
? this.lastPersistedHistoryMessages.map((message) => ({
|
|
660
|
+
role: message.role,
|
|
661
|
+
content: message.content,
|
|
662
|
+
kind: message.kind,
|
|
663
|
+
senderName: message.senderName,
|
|
664
|
+
receivedAt: message.receivedAt,
|
|
665
|
+
}))
|
|
666
|
+
: mergedMessages;
|
|
532
667
|
|
|
533
668
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
534
669
|
const parsedChatStatus = typeof parsedStatus?.status === 'string' && parsedStatus.status.trim()
|
|
@@ -572,33 +707,48 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
572
707
|
}
|
|
573
708
|
}
|
|
574
709
|
|
|
575
|
-
this.applyProviderResponse(
|
|
710
|
+
this.applyProviderResponse(
|
|
711
|
+
suppressFreshLaunchStartupReplay && parsedStatus && typeof parsedStatus === 'object'
|
|
712
|
+
? { ...parsedStatus, providerSessionId: undefined }
|
|
713
|
+
: parsedStatus,
|
|
714
|
+
{ phase: 'immediate' },
|
|
715
|
+
);
|
|
576
716
|
const surface = resolveProviderStateSurface({
|
|
577
717
|
summaryMetadata: this.summaryMetadata as any,
|
|
578
718
|
controlValues: this.controlValues,
|
|
579
719
|
});
|
|
580
720
|
const activeChatStatus = parseErrorMessage
|
|
581
721
|
? 'error'
|
|
582
|
-
: autoApproveActive && parsedStatus?.status === 'waiting_approval'
|
|
722
|
+
: (autoApproveActive && parsedStatus?.status === 'waiting_approval') || autoApproveHoldIdle
|
|
583
723
|
? 'generating'
|
|
584
724
|
: (adapterStatus.status !== 'idle'
|
|
585
725
|
? visibleStatus
|
|
586
726
|
: (suppressStaleParsedBusyStatus ? visibleStatus : (parsedChatStatus || visibleStatus)));
|
|
587
727
|
|
|
728
|
+
// If an AskUserQuestion prompt is awaiting user input, overlay status as
|
|
729
|
+
// waiting_choice. This is distinct from waiting_approval (tool-use consent)
|
|
730
|
+
// — the engine's isWaitingForResponse state is unchanged, so completion
|
|
731
|
+
// tracking continues normally once the user responds.
|
|
732
|
+
const hasInteractivePrompt = !!this.activeInteractivePrompt;
|
|
733
|
+
const finalStatus = hasInteractivePrompt ? 'waiting_choice' : visibleStatus;
|
|
734
|
+
const finalChatStatus = hasInteractivePrompt ? 'waiting_choice' : activeChatStatus;
|
|
735
|
+
|
|
588
736
|
return {
|
|
589
737
|
type: this.type,
|
|
590
738
|
name: this.provider.name,
|
|
591
739
|
category: 'cli',
|
|
592
|
-
status:
|
|
740
|
+
status: finalStatus,
|
|
593
741
|
mode: this.presentationMode,
|
|
594
742
|
activeChat: {
|
|
595
|
-
id:
|
|
743
|
+
id: activeChatId,
|
|
596
744
|
title: parsedStatus?.title || dirName,
|
|
597
|
-
status:
|
|
598
|
-
messages:
|
|
599
|
-
activeModal: autoApproveActive ? null : (parsedStatus?.activeModal ?? adapterStatus.activeModal),
|
|
745
|
+
status: finalChatStatus,
|
|
746
|
+
messages: statusMessages,
|
|
747
|
+
activeModal: (autoApproveActive || autoApproveHoldIdle) ? null : (parsedStatus?.activeModal ?? adapterStatus.activeModal),
|
|
748
|
+
activeInteractivePrompt: this.activeInteractivePrompt,
|
|
600
749
|
inputContent: '',
|
|
601
750
|
},
|
|
751
|
+
activeInteractivePrompt: this.activeInteractivePrompt,
|
|
602
752
|
workspace: this.workingDir,
|
|
603
753
|
instanceId: this.instanceId,
|
|
604
754
|
providerSessionId: this.providerSessionId,
|
|
@@ -639,7 +789,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
639
789
|
getHotChatSessionState(): HotChatSessionState {
|
|
640
790
|
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
641
791
|
const autoApproveActive = adapterStatus.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
642
|
-
const
|
|
792
|
+
const autoApproveHoldIdle = this.autoApproveBusy && adapterStatus.status === 'idle';
|
|
793
|
+
const visibleStatus = autoApproveActive || autoApproveHoldIdle ? 'generating' : adapterStatus.status;
|
|
643
794
|
const runtime = this.adapter.getRuntimeMetadata();
|
|
644
795
|
return {
|
|
645
796
|
id: this.instanceId,
|
|
@@ -651,21 +802,41 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
651
802
|
};
|
|
652
803
|
}
|
|
653
804
|
|
|
654
|
-
getSessionModalState(): SessionModalState {
|
|
655
|
-
const adapterStatus = this.adapter.getStatus({ allowParse:
|
|
805
|
+
getSessionModalState(sessionId?: string): SessionModalState {
|
|
806
|
+
const adapterStatus = this.adapter.getStatus({ allowParse: true });
|
|
656
807
|
const autoApproveActive = adapterStatus.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
657
|
-
const
|
|
808
|
+
const autoApproveHoldIdle = this.autoApproveBusy && adapterStatus.status === 'idle';
|
|
809
|
+
const visibleStatus = autoApproveActive || autoApproveHoldIdle ? 'generating' : adapterStatus.status;
|
|
658
810
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
659
811
|
return {
|
|
660
|
-
|
|
812
|
+
// Honor the caller-supplied sessionId — InstanceMgr rejects the
|
|
813
|
+
// projection when projected.id !== requested sessionId, and
|
|
814
|
+
// this.instanceId is the manager's internal key, not the public
|
|
815
|
+
// sessionId the dashboard subscribes by.
|
|
816
|
+
id: sessionId ?? this.instanceId,
|
|
661
817
|
status: visibleStatus,
|
|
662
818
|
title: dirName,
|
|
663
|
-
activeModal: autoApproveActive ? null : adapterStatus.activeModal,
|
|
819
|
+
activeModal: (autoApproveActive || autoApproveHoldIdle) ? null : adapterStatus.activeModal,
|
|
664
820
|
};
|
|
665
821
|
}
|
|
666
822
|
|
|
667
823
|
updateSettings(newSettings: Record<string, any>): void {
|
|
668
|
-
|
|
824
|
+
const runtimeMeshSettings: Record<string, any> = {};
|
|
825
|
+
for (const key of [
|
|
826
|
+
'meshNodeFor',
|
|
827
|
+
'meshNodeId',
|
|
828
|
+
'meshActiveTaskId',
|
|
829
|
+
'meshCoordinatorFor',
|
|
830
|
+
'meshCoordinatorDaemonId',
|
|
831
|
+
'meshCoordinatorNodeId',
|
|
832
|
+
'spawnedSessionVisibility',
|
|
833
|
+
'launchedByCoordinator',
|
|
834
|
+
]) {
|
|
835
|
+
if (this.settings[key] !== undefined && newSettings[key] === undefined) {
|
|
836
|
+
runtimeMeshSettings[key] = this.settings[key];
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
this.settings = { ...newSettings, ...runtimeMeshSettings };
|
|
669
840
|
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
670
841
|
this.monitor.updateConfig({
|
|
671
842
|
approvalAlert: this.settings.approvalAlert !== false,
|
|
@@ -674,6 +845,40 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
674
845
|
});
|
|
675
846
|
}
|
|
676
847
|
|
|
848
|
+
/**
|
|
849
|
+
* Stamp a direct-dispatch mesh assignment on this instance.
|
|
850
|
+
* setupMeshEventForwarding reads settings.meshNodeFor + meshActiveTaskId to
|
|
851
|
+
* route generating_completed back to the originating coordinator. Without
|
|
852
|
+
* this stamp, mesh_send_task --direct targets a plain CLI session whose
|
|
853
|
+
* completion events silently drop because the forwarder has nothing to
|
|
854
|
+
* match against.
|
|
855
|
+
*/
|
|
856
|
+
attachMeshAssignment(assignment: { meshId: string; nodeId?: string; taskId?: string; coordinatorDaemonId?: string }): void {
|
|
857
|
+
if (!assignment?.meshId) return;
|
|
858
|
+
this.settings = {
|
|
859
|
+
...this.settings,
|
|
860
|
+
meshNodeFor: assignment.meshId,
|
|
861
|
+
...(assignment.nodeId ? { meshNodeId: assignment.nodeId } : {}),
|
|
862
|
+
...(assignment.taskId ? { meshActiveTaskId: assignment.taskId } : {}),
|
|
863
|
+
...(assignment.coordinatorDaemonId ? { meshCoordinatorDaemonId: assignment.coordinatorDaemonId } : {}),
|
|
864
|
+
};
|
|
865
|
+
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Clear a previously-attached mesh assignment after the task reaches a
|
|
870
|
+
* terminal state. Leaving meshNodeFor pinned would route this session's
|
|
871
|
+
* subsequent unrelated turns (e.g. ad-hoc dashboard chats) to the
|
|
872
|
+
* coordinator as if they were task completions.
|
|
873
|
+
*/
|
|
874
|
+
detachMeshAssignment(): void {
|
|
875
|
+
if (!this.settings.meshNodeFor && !this.settings.meshActiveTaskId && !this.settings.meshNodeId) return;
|
|
876
|
+
const { meshNodeFor, meshNodeId, meshActiveTaskId, ...rest } = this.settings;
|
|
877
|
+
void meshNodeFor; void meshNodeId; void meshActiveTaskId;
|
|
878
|
+
this.settings = rest;
|
|
879
|
+
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
880
|
+
}
|
|
881
|
+
|
|
677
882
|
onEvent(event: string, data?: any): void {
|
|
678
883
|
if (event === 'send_message') {
|
|
679
884
|
const input = normalizeInputEnvelope(data);
|
|
@@ -690,11 +895,67 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
690
895
|
void this.adapter.resolveAction(data).catch((e: any) => {
|
|
691
896
|
LOG.warn('CLI', `[${this.type}] resolve_action failed: ${e?.message || e}`);
|
|
692
897
|
});
|
|
898
|
+
} else if (event === 'interactive_prompt' && data) {
|
|
899
|
+
const prompt = normalizeInteractivePrompt(data);
|
|
900
|
+
if (prompt) {
|
|
901
|
+
this.activeInteractivePrompt = prompt;
|
|
902
|
+
this.events.push({
|
|
903
|
+
event: 'interactive_prompt',
|
|
904
|
+
timestamp: Date.now(),
|
|
905
|
+
promptId: prompt.promptId,
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
} else if (event === 'interactive_prompt_response' && data) {
|
|
909
|
+
try {
|
|
910
|
+
const response = normalizeInteractivePromptResponse(data);
|
|
911
|
+
if (this.activeInteractivePrompt?.promptId === response.promptId) {
|
|
912
|
+
this.activeInteractivePrompt = null;
|
|
913
|
+
}
|
|
914
|
+
if (typeof this.adapter.setInteractivePromptResponse !== 'function') {
|
|
915
|
+
LOG.warn('CLI', `[${this.type}] interactive_prompt_response ignored: adapter does not support interactive prompts`);
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
void this.adapter.setInteractivePromptResponse(response).catch((e: any) => {
|
|
919
|
+
LOG.warn('CLI', `[${this.type}] interactive_prompt_response failed: ${e?.message || e}`);
|
|
920
|
+
});
|
|
921
|
+
} catch (e: any) {
|
|
922
|
+
LOG.warn('CLI', `[${this.type}] invalid interactive_prompt_response: ${e?.message || e}`);
|
|
923
|
+
}
|
|
693
924
|
} else if (event === 'provider_state_patch' && data && typeof data === 'object') {
|
|
694
925
|
this.applyProviderResponse(data, { phase: 'immediate' });
|
|
695
926
|
}
|
|
696
927
|
}
|
|
697
928
|
|
|
929
|
+
recordAcknowledgedUserInput(input: InputEnvelope | string): void {
|
|
930
|
+
const content = typeof input === 'string'
|
|
931
|
+
? input.trim()
|
|
932
|
+
: buildCliStructuredInputPrompt(input).trim();
|
|
933
|
+
if (!content) return;
|
|
934
|
+
|
|
935
|
+
const receivedAt = Date.now();
|
|
936
|
+
this.lastAcknowledgedUserInputAt = receivedAt;
|
|
937
|
+
this.externalBusyIdleFingerprint = '';
|
|
938
|
+
const dedupKey = `user_input_ack:${crypto
|
|
939
|
+
.createHash('sha256')
|
|
940
|
+
.update(`${this.instanceId}:${content}:${receivedAt}`)
|
|
941
|
+
.digest('hex')
|
|
942
|
+
.slice(0, 24)}`;
|
|
943
|
+
this.appendRuntimeMessage(buildChatMessage({
|
|
944
|
+
role: 'user',
|
|
945
|
+
senderName: 'User',
|
|
946
|
+
kind: 'standard',
|
|
947
|
+
content,
|
|
948
|
+
receivedAt,
|
|
949
|
+
timestamp: receivedAt,
|
|
950
|
+
source: 'runtime_input_ack',
|
|
951
|
+
meta: {
|
|
952
|
+
runtimeInputAck: true,
|
|
953
|
+
provider: this.type,
|
|
954
|
+
workspace: this.workingDir,
|
|
955
|
+
},
|
|
956
|
+
} as ChatMessage), dedupKey);
|
|
957
|
+
}
|
|
958
|
+
|
|
698
959
|
dispose(): void {
|
|
699
960
|
this.adapter.shutdown();
|
|
700
961
|
this.monitor.reset();
|
|
@@ -706,6 +967,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
706
967
|
|
|
707
968
|
private completedDebounceTimer: NodeJS.Timeout | null = null;
|
|
708
969
|
private completedDebouncePending: CompletedDebouncePending | null = null;
|
|
970
|
+
private lastExternalCompletionProbe: ExternalTranscriptProbe | null = null;
|
|
709
971
|
|
|
710
972
|
private async enforceFreshSessionLaunchIfNeeded(): Promise<void> {
|
|
711
973
|
const scriptName = getForcedNewSessionScriptName(this.provider, this.launchMode);
|
|
@@ -740,7 +1002,210 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
740
1002
|
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
741
1003
|
const role = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : '';
|
|
742
1004
|
const content = lastVisible ? flattenContent(lastVisible.content).trim() : '';
|
|
743
|
-
|
|
1005
|
+
if (role !== 'assistant' || !content) return false;
|
|
1006
|
+
// Guard: if the last assistant message looks like an active approval/input prompt,
|
|
1007
|
+
// it is not a real completion — the session is still awaiting user input.
|
|
1008
|
+
if (looksLikeActiveApprovalPromptText(content)) return false;
|
|
1009
|
+
return true;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
private buildExternalTranscriptProbe(messages: unknown[], sourcePath?: string, sourceMtimeMs?: number): ExternalTranscriptProbe {
|
|
1013
|
+
const visibleMessages = messages.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
1014
|
+
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
1015
|
+
const readAt = Date.now();
|
|
1016
|
+
const mtimeMs = Number(sourceMtimeMs) || 0;
|
|
1017
|
+
return {
|
|
1018
|
+
readAt,
|
|
1019
|
+
msgCount: messages.length,
|
|
1020
|
+
lastRole: typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null,
|
|
1021
|
+
lastKind: typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null,
|
|
1022
|
+
contentLen: lastVisible ? flattenContent(lastVisible.content).trim().length : 0,
|
|
1023
|
+
sourcePath: typeof sourcePath === 'string' && sourcePath ? sourcePath : null,
|
|
1024
|
+
sourceMtimeMs: mtimeMs || null,
|
|
1025
|
+
mtimeAgeMs: mtimeMs ? Math.max(0, readAt - mtimeMs) : null,
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
private recordPendingTranscriptProbe(pending: CompletedDebouncePending): ExternalTranscriptProbe | null {
|
|
1030
|
+
const probe = this.lastExternalCompletionProbe;
|
|
1031
|
+
if (!probe) return null;
|
|
1032
|
+
const history = pending.transcriptProbeHistory || [];
|
|
1033
|
+
const last = history[history.length - 1];
|
|
1034
|
+
if (!last || last.readAt !== probe.readAt || last.msgCount !== probe.msgCount || last.lastRole !== probe.lastRole || last.contentLen !== probe.contentLen) {
|
|
1035
|
+
history.push(probe);
|
|
1036
|
+
pending.transcriptProbeHistory = history.slice(-5);
|
|
1037
|
+
}
|
|
1038
|
+
return probe;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
private readExternalCompletionMessages(): unknown[] | null {
|
|
1042
|
+
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1043
|
+
if (!adapterOwnsMessagesElsewhere) return null;
|
|
1044
|
+
if (!this.providerSessionId) return null;
|
|
1045
|
+
if (!isNativeSourceCanonicalHistory(this.provider.nativeHistory)) return null;
|
|
1046
|
+
|
|
1047
|
+
if (this.lastExternalCompletionProbe?.sourcePath) {
|
|
1048
|
+
try { fs.statSync(this.lastExternalCompletionProbe.sourcePath); } catch { /* best-effort metadata refresh */ }
|
|
1049
|
+
}
|
|
1050
|
+
const restoredHistory = readProviderChatHistory(this.type, {
|
|
1051
|
+
canonicalHistory: this.provider.nativeHistory,
|
|
1052
|
+
historySessionId: this.providerSessionId,
|
|
1053
|
+
workspace: this.workingDir,
|
|
1054
|
+
offset: 0,
|
|
1055
|
+
limit: Number.MAX_SAFE_INTEGER,
|
|
1056
|
+
historyBehavior: this.provider.historyBehavior,
|
|
1057
|
+
scripts: this.provider.scripts as any,
|
|
1058
|
+
sessionStartedAtMs: this.startedAt,
|
|
1059
|
+
forceRefresh: true,
|
|
1060
|
+
});
|
|
1061
|
+
if (restoredHistory.source !== 'provider-native') {
|
|
1062
|
+
this.lastExternalCompletionProbe = null;
|
|
1063
|
+
return null;
|
|
1064
|
+
}
|
|
1065
|
+
this.lastExternalCompletionProbe = this.buildExternalTranscriptProbe(
|
|
1066
|
+
restoredHistory.messages,
|
|
1067
|
+
restoredHistory.sourcePath,
|
|
1068
|
+
restoredHistory.sourceMtimeMs,
|
|
1069
|
+
);
|
|
1070
|
+
return restoredHistory.messages;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
private completionFinalAssistantEvidence(parsedMessages: unknown): CompletionFinalAssistantEvidence {
|
|
1074
|
+
if (this.completionHasFinalAssistantMessage(parsedMessages)) {
|
|
1075
|
+
return {
|
|
1076
|
+
present: true,
|
|
1077
|
+
messages: Array.isArray(parsedMessages) ? parsedMessages : [],
|
|
1078
|
+
source: 'parsed',
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const externalMessages = this.readExternalCompletionMessages();
|
|
1083
|
+
if (externalMessages) {
|
|
1084
|
+
return {
|
|
1085
|
+
present: this.completionHasFinalAssistantMessage(externalMessages),
|
|
1086
|
+
messages: externalMessages,
|
|
1087
|
+
source: 'external-native',
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
return {
|
|
1092
|
+
present: false,
|
|
1093
|
+
messages: Array.isArray(parsedMessages) ? parsedMessages : [],
|
|
1094
|
+
source: 'unavailable',
|
|
1095
|
+
};
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
private completionFinalSummary(parsedMessages: unknown): string | undefined {
|
|
1099
|
+
const evidence = this.completionFinalAssistantEvidence(parsedMessages);
|
|
1100
|
+
return extractFinalSummaryFromMessages(evidence.messages as any);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
private externalNativeFinalFingerprint(evidence: CompletionFinalAssistantEvidence): string {
|
|
1104
|
+
const messages = Array.isArray(evidence.messages) ? evidence.messages : [];
|
|
1105
|
+
const visibleMessages = messages.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
1106
|
+
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
1107
|
+
const content = lastVisible ? flattenContent(lastVisible.content).trim() : '';
|
|
1108
|
+
const receivedAt = lastVisible ? getMessageTime(lastVisible) : 0;
|
|
1109
|
+
const probe = this.lastExternalCompletionProbe;
|
|
1110
|
+
return crypto
|
|
1111
|
+
.createHash('sha256')
|
|
1112
|
+
.update([
|
|
1113
|
+
this.type,
|
|
1114
|
+
this.providerSessionId || '',
|
|
1115
|
+
probe?.sourcePath || '',
|
|
1116
|
+
String(probe?.sourceMtimeMs || 0),
|
|
1117
|
+
String(receivedAt || 0),
|
|
1118
|
+
content.slice(-500),
|
|
1119
|
+
].join('\0'))
|
|
1120
|
+
.digest('hex')
|
|
1121
|
+
.slice(0, 24);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
private getExternalNativeFinalReconciliation(parsedMessages: unknown, adapterStatus: any): ExternalNativeFinalReconciliation | null {
|
|
1125
|
+
const rawStatus = typeof adapterStatus?.status === 'string' ? adapterStatus.status.trim() : '';
|
|
1126
|
+
if (!isCliGeneratingLikeStatus(rawStatus)) return null;
|
|
1127
|
+
if (hasNonEmptyCliModalButtons(adapterStatus?.activeModal ?? adapterStatus?.modal)) return null;
|
|
1128
|
+
|
|
1129
|
+
const evidence = this.completionFinalAssistantEvidence(parsedMessages);
|
|
1130
|
+
if (evidence.source !== 'external-native' || !evidence.present) return null;
|
|
1131
|
+
|
|
1132
|
+
const messages = Array.isArray(evidence.messages) ? evidence.messages : [];
|
|
1133
|
+
const visibleMessages = messages.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
1134
|
+
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
1135
|
+
const lastMessageAt = lastVisible ? getMessageTime(lastVisible) : 0;
|
|
1136
|
+
const sourceMtimeMs = Number(this.lastExternalCompletionProbe?.sourceMtimeMs || 0);
|
|
1137
|
+
const minEvidenceAt = Math.max(
|
|
1138
|
+
this.startedAt > 0 ? this.startedAt - 5_000 : 0,
|
|
1139
|
+
this.generatingStartedAt > 0 ? this.generatingStartedAt - 5_000 : 0,
|
|
1140
|
+
this.lastAcknowledgedUserInputAt > 0 ? this.lastAcknowledgedUserInputAt - 1_000 : 0,
|
|
1141
|
+
);
|
|
1142
|
+
if (minEvidenceAt > 0 && lastMessageAt > 0 && lastMessageAt < minEvidenceAt && sourceMtimeMs < minEvidenceAt) {
|
|
1143
|
+
return null;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
const finalSummary = extractFinalSummaryFromMessages(evidence.messages as any);
|
|
1147
|
+
if (!finalSummary) return null;
|
|
1148
|
+
const fingerprint = this.externalNativeFinalFingerprint(evidence);
|
|
1149
|
+
if (fingerprint === this.externalBusyIdleFingerprint) {
|
|
1150
|
+
return { fingerprint, finalSummary, evidence };
|
|
1151
|
+
}
|
|
1152
|
+
this.externalBusyIdleFingerprint = fingerprint;
|
|
1153
|
+
return { fingerprint, finalSummary, evidence };
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
private buildCompletedFinalizationDiagnostic(args: {
|
|
1157
|
+
blockReason: string;
|
|
1158
|
+
latestStatus?: any;
|
|
1159
|
+
latestVisibleStatus: string;
|
|
1160
|
+
waitedMs: number;
|
|
1161
|
+
pending: CompletedDebouncePending;
|
|
1162
|
+
emittedAfterFinalizationTimeout: boolean;
|
|
1163
|
+
}): Record<string, unknown> {
|
|
1164
|
+
let parsed: any = null;
|
|
1165
|
+
let parseError: string | undefined;
|
|
1166
|
+
try {
|
|
1167
|
+
parsed = this.adapter.getScriptParsedStatus();
|
|
1168
|
+
} catch (error: any) {
|
|
1169
|
+
parseError = error?.message || String(error);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
const evidence = this.completionFinalAssistantEvidence(parsed?.messages);
|
|
1173
|
+
if (evidence.source === 'external-native') {
|
|
1174
|
+
this.recordPendingTranscriptProbe(args.pending);
|
|
1175
|
+
}
|
|
1176
|
+
const visibleMessages = (Array.isArray(evidence.messages) ? evidence.messages : [])
|
|
1177
|
+
.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
1178
|
+
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
1179
|
+
const lastVisibleRole = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null;
|
|
1180
|
+
const lastVisibleKind = typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null;
|
|
1181
|
+
const lastVisibleContentLength = lastVisible ? flattenContent(lastVisible.content).trim().length : 0;
|
|
1182
|
+
|
|
1183
|
+
return {
|
|
1184
|
+
providerType: this.type,
|
|
1185
|
+
sessionId: this.instanceId,
|
|
1186
|
+
providerSessionId: this.providerSessionId || null,
|
|
1187
|
+
workspace: this.workingDir,
|
|
1188
|
+
blockReason: args.blockReason,
|
|
1189
|
+
emittedAfterFinalizationTimeout: args.emittedAfterFinalizationTimeout,
|
|
1190
|
+
waitedMs: args.waitedMs,
|
|
1191
|
+
maxWaitMs: COMPLETED_FINALIZATION_MAX_WAIT_MS,
|
|
1192
|
+
adapterStatus: typeof args.latestStatus?.status === 'string' ? args.latestStatus.status : null,
|
|
1193
|
+
latestVisibleStatus: args.latestVisibleStatus,
|
|
1194
|
+
parsedStatus: typeof parsed?.status === 'string' ? parsed.status : (parseError ? 'parse_error' : 'unknown'),
|
|
1195
|
+
parseError: parseError || undefined,
|
|
1196
|
+
finalAssistantPresent: evidence.present,
|
|
1197
|
+
finalAssistantEvidenceSource: evidence.source,
|
|
1198
|
+
visibleMessageCount: visibleMessages.length,
|
|
1199
|
+
lastVisibleRole,
|
|
1200
|
+
lastVisibleKind,
|
|
1201
|
+
lastVisibleContentLength,
|
|
1202
|
+
pendingStartedAt: this.generatingStartedAt || null,
|
|
1203
|
+
pendingFirstObservedAt: args.pending.firstObservedAt,
|
|
1204
|
+
pendingTimestamp: args.pending.timestamp,
|
|
1205
|
+
pendingDurationSec: args.pending.duration,
|
|
1206
|
+
previousBlockReason: args.pending.loggedBlockReason || null,
|
|
1207
|
+
transcriptProbeHistory: args.pending.transcriptProbeHistory || [],
|
|
1208
|
+
};
|
|
744
1209
|
}
|
|
745
1210
|
|
|
746
1211
|
private hasAdapterPendingResponse(): boolean {
|
|
@@ -765,32 +1230,99 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
765
1230
|
if (!isCliGeneratingLikeStatus(parsedRawStatus)) return false;
|
|
766
1231
|
if (adapterRawStatus !== 'idle') return false;
|
|
767
1232
|
if (hasNonEmptyCliModalButtons(parsedStatus?.activeModal ?? parsedStatus?.modal)) return false;
|
|
768
|
-
|
|
1233
|
+
if (this.hasAdapterPendingResponse()) return false;
|
|
1234
|
+
// Do not suppress when the adapter's raw response buffer is still non-empty.
|
|
1235
|
+
// This catches the case where isWaitingForResponse has already flipped to false
|
|
1236
|
+
// (so getPartialResponse() returns '') but the provider's native parser still
|
|
1237
|
+
// reports generating because it's parsing buffered content. Suppressing the
|
|
1238
|
+
// finalization block here would emit a false completion event while the provider
|
|
1239
|
+
// session is still actively processing its response stream.
|
|
1240
|
+
const adapterAny = this.adapter as any;
|
|
1241
|
+
if (typeof adapterAny?.responseBuffer === 'string' && adapterAny.responseBuffer.trim()) return false;
|
|
1242
|
+
return true;
|
|
769
1243
|
}
|
|
770
1244
|
|
|
771
|
-
private
|
|
772
|
-
if (latestVisibleStatus !== 'idle') return `status:${latestVisibleStatus}
|
|
1245
|
+
private getCompletedFinalizationBlock(latestVisibleStatus: string, pending: CompletedDebouncePending, opts?: { externalNativeFinal?: ExternalNativeFinalReconciliation | null }): CompletedFinalizationBlock | null {
|
|
1246
|
+
if (latestVisibleStatus !== 'idle') return { reason: `status:${latestVisibleStatus}`, terminal: true };
|
|
773
1247
|
|
|
774
1248
|
const adapterAny = this.adapter as any;
|
|
775
|
-
|
|
776
|
-
|
|
1249
|
+
const approvalResolvedIdle = pending.previousStatus === 'waiting_approval';
|
|
1250
|
+
const externalNativeFinal = opts?.externalNativeFinal || null;
|
|
1251
|
+
if (!approvalResolvedIdle && !externalNativeFinal) {
|
|
1252
|
+
if (adapterAny?.isWaitingForResponse === true) return { reason: 'adapter_waiting_for_response', terminal: true };
|
|
1253
|
+
if (adapterAny?.currentTurnScope) return { reason: 'adapter_turn_scope_active', terminal: true };
|
|
1254
|
+
if (this.hasAdapterPendingResponse()) return { reason: 'adapter_pending_response', terminal: true };
|
|
1255
|
+
}
|
|
777
1256
|
|
|
778
1257
|
const partial = typeof this.adapter.getPartialResponse === 'function'
|
|
779
1258
|
? this.adapter.getPartialResponse()
|
|
780
1259
|
: '';
|
|
781
|
-
if (typeof partial === 'string' && partial.trim()) return 'partial_response_pending';
|
|
1260
|
+
if (!externalNativeFinal && typeof partial === 'string' && partial.trim()) return { reason: 'partial_response_pending', terminal: true };
|
|
782
1261
|
|
|
783
1262
|
let parsed: any;
|
|
784
1263
|
try {
|
|
785
1264
|
parsed = this.adapter.getScriptParsedStatus();
|
|
786
1265
|
} catch (error: any) {
|
|
787
|
-
return `parse_error:${error?.message || String(error)}
|
|
1266
|
+
return { reason: `parse_error:${error?.message || String(error)}` };
|
|
788
1267
|
}
|
|
789
1268
|
|
|
790
1269
|
const parsedStatus = typeof parsed?.status === 'string' ? parsed.status : 'unknown';
|
|
791
|
-
if (parsedStatus !== 'idle')
|
|
792
|
-
|
|
793
|
-
|
|
1270
|
+
if (parsedStatus !== 'idle') {
|
|
1271
|
+
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
1272
|
+
if (externalNativeFinal && isCliGeneratingLikeStatus(parsedStatus)) return null;
|
|
1273
|
+
if (this.shouldSuppressStaleParsedBusyStatus(parsed, adapterStatus)) return null;
|
|
1274
|
+
return { reason: `parsed_status:${parsedStatus}`, terminal: isCliGeneratingLikeStatus(parsedStatus) };
|
|
1275
|
+
}
|
|
1276
|
+
if (parsed?.activeModal || parsed?.modal) return { reason: 'parsed_modal_active', terminal: true };
|
|
1277
|
+
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1278
|
+
const finalAssistantEvidence = this.completionFinalAssistantEvidence(parsed?.messages);
|
|
1279
|
+
const allowMissingAssistantTimeout = !!(this.settings.meshNodeFor || this.settings.meshActiveTaskId || this.settings.launchedByCoordinator);
|
|
1280
|
+
LOG.debug('CLI', `[${this.type}] finalAssistantEvidence: present=${finalAssistantEvidence.present} source=${finalAssistantEvidence.source} adapterOwnsMessagesElsewhere=${adapterOwnsMessagesElsewhere} parsedStatus=${parsedStatus}`);
|
|
1281
|
+
if (!finalAssistantEvidence.present) {
|
|
1282
|
+
if (adapterOwnsMessagesElsewhere) {
|
|
1283
|
+
if (finalAssistantEvidence.source === 'external-native') {
|
|
1284
|
+
const probe = this.recordPendingTranscriptProbe(pending);
|
|
1285
|
+
if (probe && !pending.loggedTranscriptProbe) {
|
|
1286
|
+
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`);
|
|
1287
|
+
pending.loggedTranscriptProbe = true;
|
|
1288
|
+
}
|
|
1289
|
+
LOG.debug('CLI', `[${this.type}] external-native probe result: lastRole=${probe?.lastRole} contentLen=${probe?.contentLen}`);
|
|
1290
|
+
if (probe?.lastRole === 'assistant' && (probe.contentLen ?? 0) > 0) {
|
|
1291
|
+
return null;
|
|
1292
|
+
}
|
|
1293
|
+
if (this.type === 'antigravity-cli') {
|
|
1294
|
+
return null;
|
|
1295
|
+
}
|
|
1296
|
+
return { reason: 'missing_final_assistant', terminal: true, allowTimeout: allowMissingAssistantTimeout };
|
|
1297
|
+
}
|
|
1298
|
+
if ((this.provider as any).requiresFinalAssistantBeforeIdle === true) {
|
|
1299
|
+
return { reason: 'missing_final_assistant', terminal: true, allowTimeout: allowMissingAssistantTimeout };
|
|
1300
|
+
}
|
|
1301
|
+
} else {
|
|
1302
|
+
LOG.debug('CLI', `[${this.type}] missing_final_assistant (not ownsExternal) requiresFinalAssistant=${!!(this.provider as any).requiresFinalAssistantBeforeIdle}`);
|
|
1303
|
+
return {
|
|
1304
|
+
reason: 'missing_final_assistant',
|
|
1305
|
+
terminal: (this.provider as any).requiresFinalAssistantBeforeIdle === true,
|
|
1306
|
+
allowTimeout: allowMissingAssistantTimeout,
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// Guard: if the screen still shows an approval/choice prompt as the last visible text,
|
|
1312
|
+
// the turn is not complete even if the parsed status says idle and there is an assistant
|
|
1313
|
+
// message. This catches the case where waiting_approval→idle transitions occur before
|
|
1314
|
+
// the modal has been resolved (e.g. the PTY rendered the prompt but no button press fired).
|
|
1315
|
+
try {
|
|
1316
|
+
const screenText = typeof (this.adapter as any).getScreenText === 'function'
|
|
1317
|
+
? String((this.adapter as any).getScreenText() || '')
|
|
1318
|
+
: '';
|
|
1319
|
+
if (screenText) {
|
|
1320
|
+
const tailLines = screenText.split(/\r?\n/).slice(-16).join('\n');
|
|
1321
|
+
if (looksLikeActiveApprovalPromptText(tailLines)) {
|
|
1322
|
+
return { reason: 'screen_shows_approval_prompt', terminal: approvalResolvedIdle };
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
} catch { /* defensive: screen text read is best-effort */ }
|
|
794
1326
|
|
|
795
1327
|
return null;
|
|
796
1328
|
}
|
|
@@ -809,7 +1341,11 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
809
1341
|
|
|
810
1342
|
const latestStatus = this.adapter.getStatus({ allowParse: false });
|
|
811
1343
|
const latestAutoApproveActive = latestStatus.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
812
|
-
const
|
|
1344
|
+
const externalNativeFinal = this.getExternalNativeFinalReconciliation(undefined, latestStatus);
|
|
1345
|
+
const latestVisibleStatus = externalNativeFinal && isCliGeneratingLikeStatus(latestStatus.status)
|
|
1346
|
+
? 'idle'
|
|
1347
|
+
: (latestAutoApproveActive || this.autoApproveBusy ? 'generating' : latestStatus.status);
|
|
1348
|
+
LOG.debug('CLI', `[${this.type}] flush attempt: adapterStatus=${latestStatus.status} latestVisible=${latestVisibleStatus} externalNativeFinal=${!!externalNativeFinal} generatingStartedAt=${this.generatingStartedAt} isWaitingForResponse=${!!(this.adapter as any)?.isWaitingForResponse} hasPartial=${!!this.adapter.getPartialResponse?.()}`);
|
|
813
1349
|
if (latestVisibleStatus !== 'idle') {
|
|
814
1350
|
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed ${latestVisibleStatus})`);
|
|
815
1351
|
this.completedDebouncePending = null;
|
|
@@ -817,10 +1353,12 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
817
1353
|
return;
|
|
818
1354
|
}
|
|
819
1355
|
|
|
820
|
-
const
|
|
821
|
-
if (
|
|
1356
|
+
const block = this.getCompletedFinalizationBlock(latestVisibleStatus, pending, { externalNativeFinal });
|
|
1357
|
+
if (block) {
|
|
1358
|
+
const blockReason = block.reason;
|
|
822
1359
|
const waitedMs = Date.now() - pending.firstObservedAt;
|
|
823
|
-
|
|
1360
|
+
LOG.debug('CLI', `[${this.type}] finalization block: reason=${blockReason} terminal=${block.terminal} waitedMs=${waitedMs} maxWait=${COMPLETED_FINALIZATION_MAX_WAIT_MS}`);
|
|
1361
|
+
if ((block.terminal && !block.allowTimeout) || waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
|
|
824
1362
|
if (pending.loggedBlockReason !== blockReason) {
|
|
825
1363
|
LOG.info('CLI', `[${this.type}] waiting to emit completed until transcript finalizes (${blockReason})`);
|
|
826
1364
|
pending.loggedBlockReason = blockReason;
|
|
@@ -828,10 +1366,29 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
828
1366
|
this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
|
|
829
1367
|
return;
|
|
830
1368
|
}
|
|
831
|
-
|
|
1369
|
+
const completionDiagnostic = this.buildCompletedFinalizationDiagnostic({
|
|
1370
|
+
blockReason,
|
|
1371
|
+
latestStatus,
|
|
1372
|
+
latestVisibleStatus,
|
|
1373
|
+
waitedMs,
|
|
1374
|
+
pending,
|
|
1375
|
+
emittedAfterFinalizationTimeout: true,
|
|
1376
|
+
});
|
|
1377
|
+
LOG.warn('CLI', `[${this.type}] emitting completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
|
|
1378
|
+
this.pushEvent({
|
|
1379
|
+
event: 'agent:generating_completed',
|
|
1380
|
+
chatTitle: pending.chatTitle,
|
|
1381
|
+
duration: pending.duration,
|
|
1382
|
+
timestamp: pending.timestamp,
|
|
1383
|
+
finalSummary: blockReason.startsWith('parsed_status:')
|
|
1384
|
+
? ''
|
|
1385
|
+
: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
|
|
1386
|
+
completionDiagnostic,
|
|
1387
|
+
});
|
|
832
1388
|
this.completedDebouncePending = null;
|
|
833
1389
|
this.completedDebounceTimer = null;
|
|
834
1390
|
this.generatingStartedAt = 0;
|
|
1391
|
+
this.lastApprovalEventFingerprint = '';
|
|
835
1392
|
return;
|
|
836
1393
|
}
|
|
837
1394
|
|
|
@@ -841,11 +1398,23 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
841
1398
|
chatTitle: pending.chatTitle,
|
|
842
1399
|
duration: pending.duration,
|
|
843
1400
|
timestamp: pending.timestamp,
|
|
844
|
-
finalSummary:
|
|
1401
|
+
finalSummary: externalNativeFinal?.finalSummary || this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
|
|
1402
|
+
...(externalNativeFinal ? {
|
|
1403
|
+
completionDiagnostic: {
|
|
1404
|
+
providerType: this.type,
|
|
1405
|
+
sessionId: this.instanceId,
|
|
1406
|
+
providerSessionId: this.providerSessionId || null,
|
|
1407
|
+
reconciliationReason: 'external_native_final_assistant_while_adapter_busy',
|
|
1408
|
+
finalAssistantPresent: true,
|
|
1409
|
+
finalAssistantEvidenceSource: externalNativeFinal.evidence.source,
|
|
1410
|
+
externalFinalFingerprint: externalNativeFinal.fingerprint,
|
|
1411
|
+
},
|
|
1412
|
+
} : {}),
|
|
845
1413
|
});
|
|
846
1414
|
this.completedDebouncePending = null;
|
|
847
1415
|
this.completedDebounceTimer = null;
|
|
848
1416
|
this.generatingStartedAt = 0;
|
|
1417
|
+
this.lastApprovalEventFingerprint = '';
|
|
849
1418
|
}
|
|
850
1419
|
|
|
851
1420
|
private maybeAutoApproveStatus(adapterStatus: any, now = Date.now()): boolean {
|
|
@@ -853,15 +1422,46 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
853
1422
|
// Guard re-entry: onStatusChange/getState can observe the same modal multiple
|
|
854
1423
|
// times while the PTY absorbs the approval key. Without this flag, repeated
|
|
855
1424
|
// snapshots would write stray keys into the input once the modal dismisses.
|
|
856
|
-
|
|
1425
|
+
// However, Claude Code can present a second approval immediately after the
|
|
1426
|
+
// first. Resolve a changed modal signature even while the previous write is
|
|
1427
|
+
// still inside the short busy window.
|
|
1428
|
+
if (!autoApproveActive) {
|
|
1429
|
+
this.lastAutoApprovalSignature = '';
|
|
1430
|
+
return autoApproveActive;
|
|
1431
|
+
}
|
|
1432
|
+
const modal = adapterStatus.activeModal;
|
|
1433
|
+
// (fix) Do not auto-approve when no concrete modal/buttons are present.
|
|
1434
|
+
// Claude TUI flaps between paints; without this guard adapterStatus
|
|
1435
|
+
// could report status=waiting_approval with activeModal=null (or with
|
|
1436
|
+
// an empty buttons array briefly) and we'd still call
|
|
1437
|
+
// resolveModal(-1) — which used to type "1" into the prompt
|
|
1438
|
+
// repeatedly. Skip until a real modal is captured.
|
|
1439
|
+
const buttons = Array.isArray(modal?.buttons)
|
|
1440
|
+
? modal.buttons.map((b: any) => String(b || '').trim()).filter(Boolean)
|
|
1441
|
+
: [];
|
|
1442
|
+
if (!modal || buttons.length === 0) {
|
|
1443
|
+
return autoApproveActive;
|
|
1444
|
+
}
|
|
1445
|
+
const { index: buttonIndex, label: buttonLabel } = pickAutoApprovalButton(buttons);
|
|
1446
|
+
if (buttonIndex < 0) {
|
|
1447
|
+
// No concrete button matched — don't pick a random index, just
|
|
1448
|
+
// surface the modal so the user can decide.
|
|
1449
|
+
return autoApproveActive;
|
|
1450
|
+
}
|
|
1451
|
+
const signature = [
|
|
1452
|
+
typeof modal?.message === 'string' ? modal.message.trim() : '',
|
|
1453
|
+
buttons.join('|'),
|
|
1454
|
+
buttonIndex,
|
|
1455
|
+
].join('::');
|
|
1456
|
+
if (!this.autoApproveBusy || signature !== this.lastAutoApprovalSignature) {
|
|
857
1457
|
this.autoApproveBusy = true;
|
|
1458
|
+
this.lastAutoApprovalSignature = signature;
|
|
858
1459
|
if (this.autoApproveBusyTimer) clearTimeout(this.autoApproveBusyTimer);
|
|
859
1460
|
this.autoApproveBusyTimer = setTimeout(() => {
|
|
860
1461
|
this.autoApproveBusy = false;
|
|
861
1462
|
this.autoApproveBusyTimer = null;
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
const { index: buttonIndex, label: buttonLabel } = pickApprovalButton(modal?.buttons, this.provider);
|
|
1463
|
+
this.lastAutoApprovalSignature = '';
|
|
1464
|
+
}, 5000);
|
|
865
1465
|
this.recordAutoApproval(modal?.message, buttonLabel, now);
|
|
866
1466
|
setTimeout(() => {
|
|
867
1467
|
this.adapter.resolveModal(buttonIndex);
|
|
@@ -876,10 +1476,25 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
876
1476
|
// during long-running CLI sessions. Keep this path on adapter-owned light
|
|
877
1477
|
// state only; rich provider parsing is reserved for getState/read_chat.
|
|
878
1478
|
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
1479
|
+
const adapterProviderSessionId = normalizeProviderSessionId(
|
|
1480
|
+
this.provider,
|
|
1481
|
+
typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
|
|
1482
|
+
);
|
|
1483
|
+
if (adapterProviderSessionId) {
|
|
1484
|
+
this.promoteProviderSessionId(adapterProviderSessionId);
|
|
1485
|
+
}
|
|
879
1486
|
const parsedStatus = null;
|
|
880
1487
|
const rawStatus = adapterStatus.status;
|
|
881
1488
|
const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, now);
|
|
882
|
-
const
|
|
1489
|
+
const externalNativeFinal = this.getExternalNativeFinalReconciliation(undefined, adapterStatus);
|
|
1490
|
+
// During the autoApproveBusy window (2s after firing approval key), the PTY
|
|
1491
|
+
// can briefly report 'idle' before the next generating phase starts. Treat that
|
|
1492
|
+
// transient idle as 'generating' to suppress a spurious agent:generating_completed
|
|
1493
|
+
// push notification. externalNativeFinal still wins to allow hard-stop overrides.
|
|
1494
|
+
const autoApproveHoldIdle = this.autoApproveBusy && rawStatus === 'idle';
|
|
1495
|
+
const newStatus = externalNativeFinal && isCliGeneratingLikeStatus(rawStatus)
|
|
1496
|
+
? 'idle'
|
|
1497
|
+
: (autoApproveActive || autoApproveHoldIdle ? 'generating' : rawStatus);
|
|
883
1498
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
884
1499
|
const chatTitle = `${this.provider.name} · ${dirName}`;
|
|
885
1500
|
const partial = this.adapter.getPartialResponse();
|
|
@@ -891,10 +1506,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
891
1506
|
if (newStatus !== this.lastStatus) {
|
|
892
1507
|
LOG.info('CLI', `[${this.type}] status: ${this.lastStatus} → ${newStatus}`);
|
|
893
1508
|
if (this.lastStatus === 'idle' && newStatus === 'generating') {
|
|
1509
|
+
// If a completion event is already pending and the turn has ended
|
|
1510
|
+
// (generatingStartedAt===0), the PTY is painting its prompt area
|
|
1511
|
+
// after completing. Ignore this blip — do not cancel the pending
|
|
1512
|
+
// completion and do not advance lastStatus to generating.
|
|
1513
|
+
if (this.completedDebouncePending && this.generatingStartedAt === 0) {
|
|
1514
|
+
LOG.debug('CLI', `[${this.type}] ignoring post-completion PTY generating blip (generatingStartedAt=0)`);
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
894
1517
|
this.suppressIdleHistoryReplay = false;
|
|
895
1518
|
// Cancel any pending completed event (multi-step: idle→generating resume)
|
|
896
1519
|
if (this.completedDebouncePending) {
|
|
897
|
-
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed generating)`);
|
|
1520
|
+
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed generating) generatingStartedAt=${this.generatingStartedAt} isWaitingForResponse=${!!(this.adapter as any)?.isWaitingForResponse}`);
|
|
898
1521
|
if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
|
|
899
1522
|
this.completedDebouncePending = null;
|
|
900
1523
|
}
|
|
@@ -926,10 +1549,15 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
926
1549
|
if (!this.generatingStartedAt) this.generatingStartedAt = now;
|
|
927
1550
|
const modal = adapterStatus.activeModal;
|
|
928
1551
|
LOG.info('CLI', `[${this.type}] approval modal: "${modal?.message?.slice(0, 80) ?? 'none'}"`);
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1552
|
+
const approvalFingerprint = JSON.stringify({
|
|
1553
|
+
message: typeof modal?.message === 'string' ? modal.message.trim() : '',
|
|
1554
|
+
buttons: Array.isArray(modal?.buttons) ? modal.buttons.map((button: unknown) => String(button).trim()) : [],
|
|
1555
|
+
});
|
|
1556
|
+
// PTY redraws repeat the same modal content; fingerprint dedup prevents duplicate events.
|
|
1557
|
+
// Do NOT also gate on lastStatus: consecutive approvals can arrive waiting_approval→waiting_approval
|
|
1558
|
+
// (e.g. antigravity-cli resolves one prompt and immediately shows the next) and would be silently dropped.
|
|
1559
|
+
if (approvalFingerprint !== this.lastApprovalEventFingerprint) {
|
|
1560
|
+
this.lastApprovalEventFingerprint = approvalFingerprint;
|
|
933
1561
|
this.appendRuntimeSystemMessage(
|
|
934
1562
|
this.formatApprovalRequestMessage(modal?.message, modal?.buttons),
|
|
935
1563
|
`approval_request:${now}`,
|
|
@@ -943,20 +1571,102 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
943
1571
|
}
|
|
944
1572
|
} else if (newStatus === 'idle' && (this.lastStatus === 'generating' || this.lastStatus === 'waiting_approval')) {
|
|
945
1573
|
const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : 0;
|
|
946
|
-
//
|
|
1574
|
+
// Guard: if generatingStartedAt===0 and no debounce pending, the generating phase
|
|
1575
|
+
// was entered from 'starting' state (startup PTY noise), not from a real idle→generating
|
|
1576
|
+
// task dispatch. The idle→generating handler is the only code path that sets
|
|
1577
|
+
// generatingStartedAt and generatingDebouncePending, so both being absent means no
|
|
1578
|
+
// task was ever dispatched. Suppress the spurious completion event and fall through
|
|
1579
|
+
// to a simple lastStatus update.
|
|
1580
|
+
if (!this.generatingStartedAt && !this.generatingDebouncePending) {
|
|
1581
|
+
LOG.debug('CLI', `[${this.type}] suppressed startup-phase generating→idle blip (generatingStartedAt=0, no debounce pending)`);
|
|
1582
|
+
} else
|
|
1583
|
+
// If debounce still pending (generating lasted < 1s), cancel both UI events.
|
|
1584
|
+
// Still emit agent:generating_completed so mesh orchestration can record
|
|
1585
|
+
// task_completed for direct dispatches that complete faster than the debounce.
|
|
947
1586
|
if (this.generatingDebouncePending) {
|
|
948
|
-
|
|
1587
|
+
const shortDurationMs = this.generatingStartedAt ? now - this.generatingStartedAt : 0;
|
|
1588
|
+
LOG.info('CLI', `[${this.type}] suppressed short generating (${shortDurationMs}ms)`);
|
|
949
1589
|
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
1590
|
+
// Emit completion for mesh task association even though the UI generating
|
|
1591
|
+
// started/completed pair is suppressed (too short for visible UI update).
|
|
1592
|
+
let shortFinalSummary: string | undefined;
|
|
1593
|
+
let shortEvidenceSource: CompletionFinalAssistantEvidence['source'] = 'unavailable';
|
|
1594
|
+
try {
|
|
1595
|
+
const parsedMessages = this.adapter?.getScriptParsedStatus()?.messages;
|
|
1596
|
+
const evidence = this.completionFinalAssistantEvidence(parsedMessages);
|
|
1597
|
+
shortEvidenceSource = evidence.source;
|
|
1598
|
+
shortFinalSummary = extractFinalSummaryFromMessages(evidence.messages as any);
|
|
1599
|
+
} catch { /* best-effort */ }
|
|
1600
|
+
// If a real response is confirmed, retroactively emit started so the chat
|
|
1601
|
+
// bubble appears even though the debounce suppressed the original event.
|
|
1602
|
+
if (shortFinalSummary) {
|
|
1603
|
+
this.pushEvent({ event: 'agent:generating_started', chatTitle, timestamp: now - shortDurationMs });
|
|
1604
|
+
}
|
|
950
1605
|
this.generatingDebouncePending = null;
|
|
951
1606
|
this.generatingStartedAt = 0;
|
|
1607
|
+
const missingEvidence = ((this.provider as any).requiresFinalAssistantBeforeIdle === true || shortEvidenceSource === 'external-native') && !shortFinalSummary;
|
|
1608
|
+
if (missingEvidence) {
|
|
1609
|
+
LOG.warn('CLI', `[${this.type}] short completion missing final assistant evidence (source=${shortEvidenceSource})`);
|
|
1610
|
+
}
|
|
1611
|
+
// When evidence is missing and there is no active mesh task context, suppress
|
|
1612
|
+
// the completion event. Providers with requiresFinalAssistantBeforeIdle or
|
|
1613
|
+
// external-native history must confirm a final assistant message before the
|
|
1614
|
+
// coordinator records task_completed. Only emit here if a mesh task is active
|
|
1615
|
+
// so the coordinator can apply its own timeout/retry logic.
|
|
1616
|
+
const hasMeshContext = !!(this.settings.meshNodeFor || this.settings.meshActiveTaskId || this.settings.launchedByCoordinator);
|
|
1617
|
+
if (missingEvidence && !hasMeshContext) {
|
|
1618
|
+
LOG.info('CLI', `[${this.type}] short completion suppressed: missing final assistant evidence, no mesh context (source=${shortEvidenceSource})`);
|
|
1619
|
+
// completedDebouncePending intentionally left null — the session is now idle
|
|
1620
|
+
// with no confirmed turn, matching the startup-blip suppression semantics.
|
|
1621
|
+
} else {
|
|
1622
|
+
this.pushEvent({
|
|
1623
|
+
event: 'agent:generating_completed',
|
|
1624
|
+
chatTitle,
|
|
1625
|
+
duration: 0,
|
|
1626
|
+
timestamp: now,
|
|
1627
|
+
finalSummary: shortFinalSummary,
|
|
1628
|
+
completionDiagnostic: {
|
|
1629
|
+
reason: 'short_generating_suppressed',
|
|
1630
|
+
shortDurationMs,
|
|
1631
|
+
finalAssistantEvidenceSource: shortEvidenceSource,
|
|
1632
|
+
...(missingEvidence ? { blockReason: 'missing_final_assistant' } : {}),
|
|
1633
|
+
},
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
952
1636
|
} else {
|
|
953
1637
|
// Debounce completed, then require the rich transcript path that read_chat
|
|
954
1638
|
// uses to show an idle turn whose last user-facing message is assistant.
|
|
955
|
-
this.completedDebouncePending = {
|
|
956
|
-
|
|
1639
|
+
this.completedDebouncePending = {
|
|
1640
|
+
chatTitle,
|
|
1641
|
+
duration,
|
|
1642
|
+
timestamp: now,
|
|
1643
|
+
firstObservedAt: now,
|
|
1644
|
+
previousStatus: this.lastStatus,
|
|
1645
|
+
};
|
|
1646
|
+
const ownsExternalHistory = !!(this.adapter as any)?.chatMessagesOwnedExternally;
|
|
1647
|
+
const flushDelay = ownsExternalHistory ? 0 : 3000;
|
|
1648
|
+
LOG.debug('CLI', `[${this.type}] set completedDebouncePending duration=${duration}s ownsExternalHistory=${ownsExternalHistory} flushDelay=${flushDelay}ms generatingStartedAt=${this.generatingStartedAt}`);
|
|
1649
|
+
this.scheduleCompletedDebounceFlush(flushDelay);
|
|
957
1650
|
}
|
|
958
1651
|
} else if (newStatus === 'idle' && this.lastStatus === 'starting') {
|
|
959
1652
|
this.pushEvent({ event: 'agent:ready', chatTitle, timestamp: now });
|
|
1653
|
+
} else if (newStatus === 'error') {
|
|
1654
|
+
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
1655
|
+
this.generatingDebouncePending = null;
|
|
1656
|
+
if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
|
|
1657
|
+
this.completedDebouncePending = null;
|
|
1658
|
+
this.errorMessage = adapterStatus.errorMessage || this.errorMessage;
|
|
1659
|
+
this.errorReason = (adapterStatus.errorReason as ProviderErrorReason) || this.errorReason;
|
|
1660
|
+
this.pushEvent({
|
|
1661
|
+
event: 'agent:stopped',
|
|
1662
|
+
chatTitle,
|
|
1663
|
+
timestamp: now,
|
|
1664
|
+
finalSummary: adapterStatus.errorMessage || adapterStatus.errorReason || 'Provider reported an error',
|
|
1665
|
+
completionDiagnostic: {
|
|
1666
|
+
reason: adapterStatus.errorReason || 'provider_error',
|
|
1667
|
+
errorMessage: adapterStatus.errorMessage || undefined,
|
|
1668
|
+
},
|
|
1669
|
+
});
|
|
960
1670
|
} else if (newStatus === 'stopped') {
|
|
961
1671
|
// Cancel any pending debounce
|
|
962
1672
|
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
@@ -977,7 +1687,34 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
977
1687
|
// Monitor check (cooldown based notification, IDE/CLI common)
|
|
978
1688
|
const agentKey = `${this.type}:cli`;
|
|
979
1689
|
const monitorEvents = this.monitor.check(agentKey, newStatus, now, progressFingerprint);
|
|
1690
|
+
const monitorParsedStatus: any = parsedStatus;
|
|
980
1691
|
for (const me of monitorEvents) {
|
|
1692
|
+
if (
|
|
1693
|
+
me.type === 'monitor:long_generating'
|
|
1694
|
+
&& this.completionHasFinalAssistantMessage(monitorParsedStatus?.messages)
|
|
1695
|
+
&& !this.hasAdapterPendingResponse()
|
|
1696
|
+
&& !hasNonEmptyCliModalButtons(monitorParsedStatus?.activeModal ?? monitorParsedStatus?.modal)
|
|
1697
|
+
) {
|
|
1698
|
+
this.pushEvent({
|
|
1699
|
+
event: 'agent:generating_completed',
|
|
1700
|
+
chatTitle,
|
|
1701
|
+
duration: this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : undefined,
|
|
1702
|
+
timestamp: me.timestamp,
|
|
1703
|
+
finalSummary: extractFinalSummaryFromMessages(monitorParsedStatus?.messages),
|
|
1704
|
+
completionDiagnostic: {
|
|
1705
|
+
providerType: this.type,
|
|
1706
|
+
sessionId: this.instanceId,
|
|
1707
|
+
providerSessionId: this.providerSessionId || null,
|
|
1708
|
+
reconciliationReason: 'long_generating_monitor_final_summary',
|
|
1709
|
+
finalAssistantPresent: true,
|
|
1710
|
+
},
|
|
1711
|
+
});
|
|
1712
|
+
this.generatingStartedAt = 0;
|
|
1713
|
+
// Cancel any pending debounce flush — monitor already fired completion.
|
|
1714
|
+
if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
|
|
1715
|
+
this.completedDebouncePending = null;
|
|
1716
|
+
continue;
|
|
1717
|
+
}
|
|
981
1718
|
this.pushEvent({ event: me.type, agentKey: me.agentKey, message: me.message, elapsedSec: me.elapsedSec, timestamp: me.timestamp });
|
|
982
1719
|
}
|
|
983
1720
|
}
|
|
@@ -1003,9 +1740,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1003
1740
|
};
|
|
1004
1741
|
if (this.context?.emitProviderEvent) {
|
|
1005
1742
|
this.context.emitProviderEvent(enrichedEvent);
|
|
1006
|
-
|
|
1743
|
+
} else {
|
|
1744
|
+
this.events.push(enrichedEvent);
|
|
1745
|
+
}
|
|
1746
|
+
// Auto-detach a direct-dispatch mesh assignment once the dispatched
|
|
1747
|
+
// task reaches a terminal state. Leaving meshNodeFor pinned would
|
|
1748
|
+
// route this session's next unrelated turn (a dashboard chat) into
|
|
1749
|
+
// the coordinator as if it were the completion of another task.
|
|
1750
|
+
// We schedule after the emit so the originating coordinator still
|
|
1751
|
+
// observes the completion event with its routing marker intact.
|
|
1752
|
+
if (TERMINAL_MESH_EVENTS.has(event.event) && this.settings.meshActiveTaskId) {
|
|
1753
|
+
try { this.detachMeshAssignment(); } catch { /* best-effort */ }
|
|
1007
1754
|
}
|
|
1008
|
-
this.events.push(enrichedEvent);
|
|
1009
1755
|
}
|
|
1010
1756
|
|
|
1011
1757
|
private flushEvents(): ProviderEvent[] {
|
|
@@ -1258,12 +2004,28 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1258
2004
|
index,
|
|
1259
2005
|
source: 'parsed',
|
|
1260
2006
|
}));
|
|
2007
|
+
const getRole = (message: ChatMessage): string => typeof message.role === 'string'
|
|
2008
|
+
? message.role.trim().toLowerCase()
|
|
2009
|
+
: '';
|
|
1261
2010
|
const runtimeEntries: MergeEntry[] = this.runtimeMessages.map((entry, index) => ({
|
|
1262
2011
|
message: entry.message,
|
|
1263
2012
|
index: parsedMessages.length + index,
|
|
1264
|
-
source: 'runtime',
|
|
2013
|
+
source: 'runtime' as const,
|
|
1265
2014
|
runtimeKey: entry.key,
|
|
1266
|
-
}))
|
|
2015
|
+
})).filter((entry) => {
|
|
2016
|
+
const meta = entry.message.meta && typeof entry.message.meta === 'object' && !Array.isArray(entry.message.meta)
|
|
2017
|
+
? entry.message.meta as Record<string, unknown>
|
|
2018
|
+
: {};
|
|
2019
|
+
if (meta.runtimeInputAck !== true) return true;
|
|
2020
|
+
const runtimeText = flattenContent(entry.message.content).replace(/\s+/g, ' ').trim();
|
|
2021
|
+
if (!runtimeText) return false;
|
|
2022
|
+
return !parsedEntries.some((parsedEntry) => {
|
|
2023
|
+
const parsedRole = getRole(parsedEntry.message);
|
|
2024
|
+
if (parsedRole !== 'user' && parsedRole !== 'human') return false;
|
|
2025
|
+
const parsedText = flattenContent(parsedEntry.message.content).replace(/\s+/g, ' ').trim();
|
|
2026
|
+
return parsedText === runtimeText;
|
|
2027
|
+
});
|
|
2028
|
+
});
|
|
1267
2029
|
const getTime = (message: ChatMessage): number => {
|
|
1268
2030
|
const value = typeof message.receivedAt === 'number'
|
|
1269
2031
|
? message.receivedAt
|
|
@@ -1273,9 +2035,6 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1273
2035
|
return Number.isFinite(value) && value > 0 ? value : 0;
|
|
1274
2036
|
};
|
|
1275
2037
|
|
|
1276
|
-
const getRole = (message: ChatMessage): string => typeof message.role === 'string'
|
|
1277
|
-
? message.role.trim().toLowerCase()
|
|
1278
|
-
: '';
|
|
1279
2038
|
const isRuntimeOverlay = (entry: MergeEntry): boolean => {
|
|
1280
2039
|
if (entry.source !== 'runtime') return false;
|
|
1281
2040
|
const key = typeof entry.runtimeKey === 'string' ? entry.runtimeKey.trim().toLowerCase() : '';
|
|
@@ -1341,7 +2100,9 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1341
2100
|
this.providerSessionId = nextSessionId;
|
|
1342
2101
|
this.historyWriter.promoteHistorySession(this.type, previousHistorySessionId, nextSessionId);
|
|
1343
2102
|
this.historyWriter.writeSessionStart(this.type, nextSessionId, this.workingDir, this.instanceId);
|
|
1344
|
-
this.
|
|
2103
|
+
if (this.shouldHydrateExistingProviderHistory()) {
|
|
2104
|
+
this.restorePersistedHistoryFromCurrentSession();
|
|
2105
|
+
}
|
|
1345
2106
|
this.adapter.updateRuntimeMeta({ providerSessionId: nextSessionId });
|
|
1346
2107
|
this.onProviderSessionResolved?.({
|
|
1347
2108
|
instanceId: this.instanceId,
|
|
@@ -1354,9 +2115,27 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1354
2115
|
LOG.info('CLI', `[${this.type}] discovered provider session id: ${nextSessionId}`);
|
|
1355
2116
|
}
|
|
1356
2117
|
|
|
2118
|
+
private shouldHydrateExistingProviderHistory(): boolean {
|
|
2119
|
+
return this.launchMode === 'resume' || this.launchMode === 'manual';
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
private shouldSuppressFreshLaunchStartupReplay(parsedMessages: unknown[], parsedStatus: any, adapterStatus: any, parsedProviderSessionId = ''): boolean {
|
|
2123
|
+
if (this.launchMode !== 'new') return false;
|
|
2124
|
+
if (this.providerSessionId) return false;
|
|
2125
|
+
if (!Array.isArray(parsedMessages) || parsedMessages.length === 0) return false;
|
|
2126
|
+
if (!isIdleStatus(adapterStatus?.status) || !isIdleStatus(parsedStatus?.status)) return false;
|
|
2127
|
+
if (parsedProviderSessionId) return true;
|
|
2128
|
+
|
|
2129
|
+
const newestMessageAt = parsedMessages.reduce<number>((newest, message) => Math.max(newest, getMessageTime(message)), 0);
|
|
2130
|
+
|
|
2131
|
+
// Untimestamped idle parser output during a fresh launch is usually the
|
|
2132
|
+
// provider's last workspace transcript before a new turn exists.
|
|
2133
|
+
return newestMessageAt === 0;
|
|
2134
|
+
}
|
|
2135
|
+
|
|
1357
2136
|
private syncCanonicalSavedHistoryIfNeeded(): boolean {
|
|
1358
2137
|
if (!this.providerSessionId) return false;
|
|
1359
|
-
const canonicalHistory = this.provider.
|
|
2138
|
+
const canonicalHistory = this.provider.nativeHistory;
|
|
1360
2139
|
if (!canonicalHistory) return false;
|
|
1361
2140
|
|
|
1362
2141
|
if (isNativeSourceCanonicalHistory(canonicalHistory)) {
|
|
@@ -1418,9 +2197,9 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1418
2197
|
private restorePersistedHistoryFromCurrentSession(): void {
|
|
1419
2198
|
if (!this.providerSessionId) return;
|
|
1420
2199
|
this.syncCanonicalSavedHistoryIfNeeded();
|
|
1421
|
-
const restoredHistory = isNativeSourceCanonicalHistory(this.provider.
|
|
2200
|
+
const restoredHistory = isNativeSourceCanonicalHistory(this.provider.nativeHistory)
|
|
1422
2201
|
? readProviderChatHistory(this.type, {
|
|
1423
|
-
canonicalHistory: this.provider.
|
|
2202
|
+
canonicalHistory: this.provider.nativeHistory,
|
|
1424
2203
|
historySessionId: this.providerSessionId,
|
|
1425
2204
|
workspace: this.workingDir,
|
|
1426
2205
|
offset: 0,
|