@adhdev/daemon-core 0.9.82-rc.26 → 0.9.82-rc.261
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 +4 -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 +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 +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 +44 -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 +31114 -11255
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +31615 -11852
- 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 +152 -0
- package/dist/mesh/mesh-events-pending.d.ts +40 -0
- package/dist/mesh/mesh-events-stale.d.ts +40 -0
- package/dist/mesh/mesh-events-utils.d.ts +16 -0
- package/dist/mesh/mesh-events.d.ts +5 -49
- 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-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 +327 -0
- package/dist/mesh/mesh-task-stats.d.ts +49 -0
- package/dist/mesh/mesh-work-queue.d.ts +160 -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 +37 -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 +140 -0
- package/dist/providers/spec/evaluator.d.ts +23 -0
- package/dist/providers/spec/fsm-driver.d.ts +273 -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 +30 -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 +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 +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 +4890 -489
- 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 +1625 -0
- package/src/mesh/mesh-events-pending.ts +439 -0
- package/src/mesh/mesh-events-stale.ts +283 -0
- package/src/mesh/mesh-events-utils.ts +195 -0
- package/src/mesh/mesh-events.ts +26 -1035
- 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 +588 -102
- package/src/mesh/mesh-missions.ts +151 -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 +1342 -0
- package/src/mesh/mesh-task-stats.ts +154 -0
- package/src/mesh/mesh-work-queue.ts +573 -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 +806 -72
- 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 +145 -0
- package/src/providers/spec/cli-adapter.ts +730 -0
- package/src/providers/spec/evaluator.ts +321 -0
- package/src/providers/spec/fsm-driver.ts +811 -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,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',
|
|
@@ -309,15 +359,17 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
309
359
|
private monitor: StatusMonitor;
|
|
310
360
|
private generatingDebounceTimer: NodeJS.Timeout | null = null;
|
|
311
361
|
private generatingDebouncePending: { chatTitle: string; timestamp: number } | null = null;
|
|
312
|
-
private
|
|
362
|
+
private lastApprovalEventFingerprint = '';
|
|
313
363
|
private autoApproveBusy = false;
|
|
314
364
|
private autoApproveBusyTimer: NodeJS.Timeout | null = null;
|
|
365
|
+
private lastAutoApprovalSignature = '';
|
|
315
366
|
private controlValues: Record<string, string | number | boolean> = {};
|
|
316
367
|
private summaryMetadata: unknown = undefined;
|
|
317
368
|
private appliedEffectKeys = new Set<string>();
|
|
318
369
|
private historyWriter: ChatHistoryWriter;
|
|
319
370
|
private runtimeMessages: Array<{ key: string; message: ChatMessage }> = [];
|
|
320
371
|
private lastPersistedHistoryMessages: PersistableCliHistoryMessage[] = [];
|
|
372
|
+
private lastAcknowledgedUserInputAt = 0;
|
|
321
373
|
private lastNativeSourceCanonicalCheckAt = 0;
|
|
322
374
|
private lastNativeSourceCanonicalCacheKey: string | undefined = undefined;
|
|
323
375
|
private cachedSqliteDb: {
|
|
@@ -330,6 +382,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
330
382
|
private suppressIdleHistoryReplay = false;
|
|
331
383
|
private errorMessage: string | undefined = undefined;
|
|
332
384
|
private errorReason: ProviderErrorReason | undefined = undefined;
|
|
385
|
+
private activeInteractivePrompt: InteractivePrompt | null = null;
|
|
333
386
|
|
|
334
387
|
private presentationMode: 'terminal' | 'chat';
|
|
335
388
|
private providerSessionId?: string;
|
|
@@ -370,7 +423,10 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
370
423
|
this.providerSessionId = options?.providerSessionId;
|
|
371
424
|
this.launchMode = options?.launchMode || 'new';
|
|
372
425
|
this.onProviderSessionResolved = options?.onProviderSessionResolved;
|
|
373
|
-
this.adapter =
|
|
426
|
+
this.adapter = createCliAdapter(provider as CliProviderModule, workingDir, cliArgs, options?.extraEnv || {}, transportFactory) as ProviderCliAdapter;
|
|
427
|
+
if (this.providerSessionId) {
|
|
428
|
+
this.adapter.updateRuntimeMeta({ providerSessionId: this.providerSessionId });
|
|
429
|
+
}
|
|
374
430
|
this.monitor = new StatusMonitor();
|
|
375
431
|
this.historyWriter = new ChatHistoryWriter();
|
|
376
432
|
}
|
|
@@ -412,7 +468,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
412
468
|
await this.adapter.spawn();
|
|
413
469
|
await this.enforceFreshSessionLaunchIfNeeded();
|
|
414
470
|
this.maybeAppendRuntimeRecoveryMessage(this.adapter.getRuntimeMetadata());
|
|
415
|
-
if (this.providerSessionId) {
|
|
471
|
+
if (this.providerSessionId && this.shouldHydrateExistingProviderHistory()) {
|
|
416
472
|
this.restorePersistedHistoryFromCurrentSession();
|
|
417
473
|
}
|
|
418
474
|
if (this.providerSessionId && this.launchMode === 'resume') {
|
|
@@ -486,14 +542,38 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
486
542
|
}
|
|
487
543
|
|
|
488
544
|
getState(): ProviderState {
|
|
545
|
+
// TODO(phase5-sandbox): JS override scripts (detectStatus, parseApproval,
|
|
546
|
+
// parseSession) are currently invoked by CliScriptRunner.invoke() via direct
|
|
547
|
+
// function calls — the scripts run in the daemon process with full Node.js
|
|
548
|
+
// access and no resource limits.
|
|
549
|
+
//
|
|
550
|
+
// When Phase 5 lands, CliScriptRunner should route these calls through a
|
|
551
|
+
// SandboxedScriptRunner (see providers/sdk/v1/sandbox/script-runner.ts) so
|
|
552
|
+
// that each call gets a fresh isolated-vm context with a 50 ms CPU limit and
|
|
553
|
+
// a 32 MB memory cap. The execution path to change is:
|
|
554
|
+
// CliScriptRunner.invoke() → SandboxedScriptRunner.run(scriptSource, context)
|
|
555
|
+
//
|
|
556
|
+
// This getState() call-site is NOT where the change goes — the wiring belongs
|
|
557
|
+
// in cli-script-runner.ts (CliScriptRunner.detectStatus / parseApproval /
|
|
558
|
+
// parseSession), with provider-loader.ts updated to store script source strings
|
|
559
|
+
// alongside the loaded function references for extended-legacy providers.
|
|
489
560
|
const adapterStatus = this.adapter.getStatus();
|
|
561
|
+
if (Object.prototype.hasOwnProperty.call(adapterStatus, 'activeInteractivePrompt')) {
|
|
562
|
+
this.activeInteractivePrompt = adapterStatus.activeInteractivePrompt ?? null;
|
|
563
|
+
}
|
|
490
564
|
let parsedStatus: any = null;
|
|
491
565
|
let parseErrorMessage: string | undefined;
|
|
492
566
|
if (typeof this.adapter.getScriptParsedStatus === 'function') {
|
|
493
567
|
try {
|
|
494
568
|
parsedStatus = this.adapter.getScriptParsedStatus() || null;
|
|
495
|
-
|
|
496
|
-
|
|
569
|
+
const parsedErrorMessage = typeof parsedStatus?.errorMessage === 'string' && parsedStatus.errorMessage.trim()
|
|
570
|
+
? parsedStatus.errorMessage.trim()
|
|
571
|
+
: undefined;
|
|
572
|
+
const parsedErrorReason = typeof parsedStatus?.errorReason === 'string' && parsedStatus.errorReason.trim()
|
|
573
|
+
? parsedStatus.errorReason.trim() as ProviderErrorReason
|
|
574
|
+
: undefined;
|
|
575
|
+
this.errorMessage = parsedErrorMessage;
|
|
576
|
+
this.errorReason = parsedErrorReason;
|
|
497
577
|
} catch (error: any) {
|
|
498
578
|
parseErrorMessage = error?.message || String(error);
|
|
499
579
|
this.errorMessage = parseErrorMessage;
|
|
@@ -503,22 +583,53 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
503
583
|
this.errorMessage = undefined;
|
|
504
584
|
this.errorReason = undefined;
|
|
505
585
|
}
|
|
506
|
-
const
|
|
507
|
-
const visibleStatus = parseErrorMessage
|
|
508
|
-
? 'error'
|
|
509
|
-
: (autoApproveActive ? 'generating' : adapterStatus.status);
|
|
510
|
-
const parsedProviderSessionId = normalizeProviderSessionId(
|
|
586
|
+
const adapterProviderSessionId = normalizeProviderSessionId(
|
|
511
587
|
this.provider,
|
|
512
|
-
typeof
|
|
588
|
+
typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
|
|
513
589
|
);
|
|
514
|
-
|
|
515
|
-
|
|
590
|
+
const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, Date.now());
|
|
591
|
+
const autoApproveHoldIdle = this.autoApproveBusy && adapterStatus.status === 'idle';
|
|
592
|
+
let visibleStatus = parseErrorMessage || parsedStatus?.status === 'error'
|
|
593
|
+
? 'error'
|
|
594
|
+
: (autoApproveActive || autoApproveHoldIdle ? 'generating' : adapterStatus.status);
|
|
595
|
+
// getState() must agree with the status the FSM-driven detectStatusTransition()
|
|
596
|
+
// already committed to lastStatus. The adapter's own status is authoritative; we do
|
|
597
|
+
// not second-guess it with native-transcript shape. Only reconcile a generating-like
|
|
598
|
+
// read down to idle when our own lastStatus has already flipped idle (avoids a
|
|
599
|
+
// perpetual dashboard spinner during the brief window before the next getStatus()).
|
|
600
|
+
if (isCliGeneratingLikeStatus(visibleStatus) && this.lastStatus === 'idle') {
|
|
601
|
+
visibleStatus = 'idle';
|
|
516
602
|
}
|
|
517
603
|
const runtime = this.adapter.getRuntimeMetadata();
|
|
518
604
|
this.maybeAppendRuntimeRecoveryMessage(runtime);
|
|
519
605
|
let parsedMessages = Array.isArray(parsedStatus?.messages)
|
|
520
606
|
? parsedStatus.messages
|
|
521
607
|
: [];
|
|
608
|
+
const parsedProviderSessionId = normalizeProviderSessionId(
|
|
609
|
+
this.provider,
|
|
610
|
+
typeof parsedStatus?.providerSessionId === 'string' ? parsedStatus.providerSessionId : '',
|
|
611
|
+
);
|
|
612
|
+
const suppressFreshLaunchStartupReplay = this.shouldSuppressFreshLaunchStartupReplay(
|
|
613
|
+
parsedMessages,
|
|
614
|
+
parsedStatus,
|
|
615
|
+
adapterStatus,
|
|
616
|
+
parsedProviderSessionId,
|
|
617
|
+
);
|
|
618
|
+
if (adapterProviderSessionId && !suppressFreshLaunchStartupReplay) {
|
|
619
|
+
this.promoteProviderSessionId(adapterProviderSessionId);
|
|
620
|
+
}
|
|
621
|
+
if (parsedProviderSessionId && !suppressFreshLaunchStartupReplay) {
|
|
622
|
+
this.promoteProviderSessionId(parsedProviderSessionId);
|
|
623
|
+
}
|
|
624
|
+
if (suppressFreshLaunchStartupReplay) {
|
|
625
|
+
parsedMessages = [];
|
|
626
|
+
}
|
|
627
|
+
// Adapter runtime metadata is transport-owned and is not guaranteed to
|
|
628
|
+
// identify this conversation. Spec adapters historically exposed the
|
|
629
|
+
// provider spec id (for example "codex-cli") as runtimeId, which made
|
|
630
|
+
// concurrent sessions share one activeChat identity until their native
|
|
631
|
+
// provider session ids were discovered.
|
|
632
|
+
const activeChatId = this.providerSessionId || this.instanceId;
|
|
522
633
|
const historyMessageCount = Number.isFinite(parsedStatus?.historyMessageCount)
|
|
523
634
|
? Math.max(0, Number(parsedStatus.historyMessageCount))
|
|
524
635
|
: null;
|
|
@@ -528,7 +639,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
528
639
|
: [];
|
|
529
640
|
}
|
|
530
641
|
const mergedMessages = this.mergeConversationMessages(parsedMessages);
|
|
531
|
-
const canonicalBackedHistory = this.
|
|
642
|
+
const canonicalBackedHistory = this.shouldHydrateExistingProviderHistory()
|
|
643
|
+
? this.syncCanonicalSavedHistoryIfNeeded()
|
|
644
|
+
: false;
|
|
645
|
+
const statusMessages = canonicalBackedHistory && this.lastPersistedHistoryMessages.length > 0
|
|
646
|
+
? this.lastPersistedHistoryMessages.map((message) => ({
|
|
647
|
+
role: message.role,
|
|
648
|
+
content: message.content,
|
|
649
|
+
kind: message.kind,
|
|
650
|
+
senderName: message.senderName,
|
|
651
|
+
receivedAt: message.receivedAt,
|
|
652
|
+
}))
|
|
653
|
+
: mergedMessages;
|
|
532
654
|
|
|
533
655
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
534
656
|
const parsedChatStatus = typeof parsedStatus?.status === 'string' && parsedStatus.status.trim()
|
|
@@ -572,33 +694,48 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
572
694
|
}
|
|
573
695
|
}
|
|
574
696
|
|
|
575
|
-
this.applyProviderResponse(
|
|
697
|
+
this.applyProviderResponse(
|
|
698
|
+
suppressFreshLaunchStartupReplay && parsedStatus && typeof parsedStatus === 'object'
|
|
699
|
+
? { ...parsedStatus, providerSessionId: undefined }
|
|
700
|
+
: parsedStatus,
|
|
701
|
+
{ phase: 'immediate' },
|
|
702
|
+
);
|
|
576
703
|
const surface = resolveProviderStateSurface({
|
|
577
704
|
summaryMetadata: this.summaryMetadata as any,
|
|
578
705
|
controlValues: this.controlValues,
|
|
579
706
|
});
|
|
580
707
|
const activeChatStatus = parseErrorMessage
|
|
581
708
|
? 'error'
|
|
582
|
-
: autoApproveActive && parsedStatus?.status === 'waiting_approval'
|
|
709
|
+
: (autoApproveActive && parsedStatus?.status === 'waiting_approval') || autoApproveHoldIdle
|
|
583
710
|
? 'generating'
|
|
584
711
|
: (adapterStatus.status !== 'idle'
|
|
585
712
|
? visibleStatus
|
|
586
713
|
: (suppressStaleParsedBusyStatus ? visibleStatus : (parsedChatStatus || visibleStatus)));
|
|
587
714
|
|
|
715
|
+
// If an AskUserQuestion prompt is awaiting user input, overlay status as
|
|
716
|
+
// waiting_choice. This is distinct from waiting_approval (tool-use consent)
|
|
717
|
+
// — the engine's isWaitingForResponse state is unchanged, so completion
|
|
718
|
+
// tracking continues normally once the user responds.
|
|
719
|
+
const hasInteractivePrompt = !!this.activeInteractivePrompt;
|
|
720
|
+
const finalStatus = hasInteractivePrompt ? 'waiting_choice' : visibleStatus;
|
|
721
|
+
const finalChatStatus = hasInteractivePrompt ? 'waiting_choice' : activeChatStatus;
|
|
722
|
+
|
|
588
723
|
return {
|
|
589
724
|
type: this.type,
|
|
590
725
|
name: this.provider.name,
|
|
591
726
|
category: 'cli',
|
|
592
|
-
status:
|
|
727
|
+
status: finalStatus,
|
|
593
728
|
mode: this.presentationMode,
|
|
594
729
|
activeChat: {
|
|
595
|
-
id:
|
|
730
|
+
id: activeChatId,
|
|
596
731
|
title: parsedStatus?.title || dirName,
|
|
597
|
-
status:
|
|
598
|
-
messages:
|
|
599
|
-
activeModal: autoApproveActive ? null : (parsedStatus?.activeModal ?? adapterStatus.activeModal),
|
|
732
|
+
status: finalChatStatus,
|
|
733
|
+
messages: statusMessages,
|
|
734
|
+
activeModal: (autoApproveActive || autoApproveHoldIdle) ? null : (parsedStatus?.activeModal ?? adapterStatus.activeModal),
|
|
735
|
+
activeInteractivePrompt: this.activeInteractivePrompt,
|
|
600
736
|
inputContent: '',
|
|
601
737
|
},
|
|
738
|
+
activeInteractivePrompt: this.activeInteractivePrompt,
|
|
602
739
|
workspace: this.workingDir,
|
|
603
740
|
instanceId: this.instanceId,
|
|
604
741
|
providerSessionId: this.providerSessionId,
|
|
@@ -639,7 +776,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
639
776
|
getHotChatSessionState(): HotChatSessionState {
|
|
640
777
|
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
641
778
|
const autoApproveActive = adapterStatus.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
642
|
-
const
|
|
779
|
+
const autoApproveHoldIdle = this.autoApproveBusy && adapterStatus.status === 'idle';
|
|
780
|
+
const visibleStatus = autoApproveActive || autoApproveHoldIdle ? 'generating' : adapterStatus.status;
|
|
643
781
|
const runtime = this.adapter.getRuntimeMetadata();
|
|
644
782
|
return {
|
|
645
783
|
id: this.instanceId,
|
|
@@ -651,21 +789,41 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
651
789
|
};
|
|
652
790
|
}
|
|
653
791
|
|
|
654
|
-
getSessionModalState(): SessionModalState {
|
|
655
|
-
const adapterStatus = this.adapter.getStatus({ allowParse:
|
|
792
|
+
getSessionModalState(sessionId?: string): SessionModalState {
|
|
793
|
+
const adapterStatus = this.adapter.getStatus({ allowParse: true });
|
|
656
794
|
const autoApproveActive = adapterStatus.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
657
|
-
const
|
|
795
|
+
const autoApproveHoldIdle = this.autoApproveBusy && adapterStatus.status === 'idle';
|
|
796
|
+
const visibleStatus = autoApproveActive || autoApproveHoldIdle ? 'generating' : adapterStatus.status;
|
|
658
797
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
659
798
|
return {
|
|
660
|
-
|
|
799
|
+
// Honor the caller-supplied sessionId — InstanceMgr rejects the
|
|
800
|
+
// projection when projected.id !== requested sessionId, and
|
|
801
|
+
// this.instanceId is the manager's internal key, not the public
|
|
802
|
+
// sessionId the dashboard subscribes by.
|
|
803
|
+
id: sessionId ?? this.instanceId,
|
|
661
804
|
status: visibleStatus,
|
|
662
805
|
title: dirName,
|
|
663
|
-
activeModal: autoApproveActive ? null : adapterStatus.activeModal,
|
|
806
|
+
activeModal: (autoApproveActive || autoApproveHoldIdle) ? null : adapterStatus.activeModal,
|
|
664
807
|
};
|
|
665
808
|
}
|
|
666
809
|
|
|
667
810
|
updateSettings(newSettings: Record<string, any>): void {
|
|
668
|
-
|
|
811
|
+
const runtimeMeshSettings: Record<string, any> = {};
|
|
812
|
+
for (const key of [
|
|
813
|
+
'meshNodeFor',
|
|
814
|
+
'meshNodeId',
|
|
815
|
+
'meshActiveTaskId',
|
|
816
|
+
'meshCoordinatorFor',
|
|
817
|
+
'meshCoordinatorDaemonId',
|
|
818
|
+
'meshCoordinatorNodeId',
|
|
819
|
+
'spawnedSessionVisibility',
|
|
820
|
+
'launchedByCoordinator',
|
|
821
|
+
]) {
|
|
822
|
+
if (this.settings[key] !== undefined && newSettings[key] === undefined) {
|
|
823
|
+
runtimeMeshSettings[key] = this.settings[key];
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
this.settings = { ...newSettings, ...runtimeMeshSettings };
|
|
669
827
|
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
670
828
|
this.monitor.updateConfig({
|
|
671
829
|
approvalAlert: this.settings.approvalAlert !== false,
|
|
@@ -674,13 +832,53 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
674
832
|
});
|
|
675
833
|
}
|
|
676
834
|
|
|
835
|
+
/**
|
|
836
|
+
* Stamp a direct-dispatch mesh assignment on this instance.
|
|
837
|
+
* setupMeshEventForwarding reads settings.meshNodeFor + meshActiveTaskId to
|
|
838
|
+
* route generating_completed back to the originating coordinator. Without
|
|
839
|
+
* this stamp, mesh_send_task --direct targets a plain CLI session whose
|
|
840
|
+
* completion events silently drop because the forwarder has nothing to
|
|
841
|
+
* match against.
|
|
842
|
+
*/
|
|
843
|
+
attachMeshAssignment(assignment: { meshId: string; nodeId?: string; taskId?: string; coordinatorDaemonId?: string }): void {
|
|
844
|
+
if (!assignment?.meshId) return;
|
|
845
|
+
this.settings = {
|
|
846
|
+
...this.settings,
|
|
847
|
+
meshNodeFor: assignment.meshId,
|
|
848
|
+
...(assignment.nodeId ? { meshNodeId: assignment.nodeId } : {}),
|
|
849
|
+
...(assignment.taskId ? { meshActiveTaskId: assignment.taskId } : {}),
|
|
850
|
+
...(assignment.coordinatorDaemonId ? { meshCoordinatorDaemonId: assignment.coordinatorDaemonId } : {}),
|
|
851
|
+
};
|
|
852
|
+
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Clear a previously-attached mesh assignment after the task reaches a
|
|
857
|
+
* terminal state. Leaving meshNodeFor pinned would route this session's
|
|
858
|
+
* subsequent unrelated turns (e.g. ad-hoc dashboard chats) to the
|
|
859
|
+
* coordinator as if they were task completions.
|
|
860
|
+
*/
|
|
861
|
+
detachMeshAssignment(): void {
|
|
862
|
+
if (!this.settings.meshNodeFor && !this.settings.meshActiveTaskId && !this.settings.meshNodeId) return;
|
|
863
|
+
const { meshNodeFor, meshNodeId, meshActiveTaskId, ...rest } = this.settings;
|
|
864
|
+
void meshNodeFor; void meshNodeId; void meshActiveTaskId;
|
|
865
|
+
this.settings = rest;
|
|
866
|
+
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
867
|
+
}
|
|
868
|
+
|
|
677
869
|
onEvent(event: string, data?: any): void {
|
|
678
870
|
if (event === 'send_message') {
|
|
679
871
|
const input = normalizeInputEnvelope(data);
|
|
680
872
|
assertProviderSupportsDeclaredInput(this.provider, input);
|
|
681
873
|
const promptText = buildCliStructuredInputPrompt(input);
|
|
682
874
|
if (promptText) {
|
|
683
|
-
|
|
875
|
+
// force:true bypasses the busy/generating send guard so terminal mesh
|
|
876
|
+
// events (completion/failure/bootstrap) land in a coordinator session that
|
|
877
|
+
// is itself parked in `generating` while awaiting that very event.
|
|
878
|
+
// Without it the message is queued and only flushed on the coordinator's
|
|
879
|
+
// own idle transition — which never happens until it receives the message.
|
|
880
|
+
const force = data?.force === true;
|
|
881
|
+
void this.adapter.sendMessage(promptText, force ? { force: true } : {}).catch((e: any) => {
|
|
684
882
|
LOG.warn('CLI', `[${this.type}] send_message failed: ${e?.message || e}`);
|
|
685
883
|
});
|
|
686
884
|
}
|
|
@@ -690,11 +888,66 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
690
888
|
void this.adapter.resolveAction(data).catch((e: any) => {
|
|
691
889
|
LOG.warn('CLI', `[${this.type}] resolve_action failed: ${e?.message || e}`);
|
|
692
890
|
});
|
|
891
|
+
} else if (event === 'interactive_prompt' && data) {
|
|
892
|
+
const prompt = normalizeInteractivePrompt(data);
|
|
893
|
+
if (prompt) {
|
|
894
|
+
this.activeInteractivePrompt = prompt;
|
|
895
|
+
this.events.push({
|
|
896
|
+
event: 'interactive_prompt',
|
|
897
|
+
timestamp: Date.now(),
|
|
898
|
+
promptId: prompt.promptId,
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
} else if (event === 'interactive_prompt_response' && data) {
|
|
902
|
+
try {
|
|
903
|
+
const response = normalizeInteractivePromptResponse(data);
|
|
904
|
+
if (this.activeInteractivePrompt?.promptId === response.promptId) {
|
|
905
|
+
this.activeInteractivePrompt = null;
|
|
906
|
+
}
|
|
907
|
+
if (typeof this.adapter.setInteractivePromptResponse !== 'function') {
|
|
908
|
+
LOG.warn('CLI', `[${this.type}] interactive_prompt_response ignored: adapter does not support interactive prompts`);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
void this.adapter.setInteractivePromptResponse(response).catch((e: any) => {
|
|
912
|
+
LOG.warn('CLI', `[${this.type}] interactive_prompt_response failed: ${e?.message || e}`);
|
|
913
|
+
});
|
|
914
|
+
} catch (e: any) {
|
|
915
|
+
LOG.warn('CLI', `[${this.type}] invalid interactive_prompt_response: ${e?.message || e}`);
|
|
916
|
+
}
|
|
693
917
|
} else if (event === 'provider_state_patch' && data && typeof data === 'object') {
|
|
694
918
|
this.applyProviderResponse(data, { phase: 'immediate' });
|
|
695
919
|
}
|
|
696
920
|
}
|
|
697
921
|
|
|
922
|
+
recordAcknowledgedUserInput(input: InputEnvelope | string): void {
|
|
923
|
+
const content = typeof input === 'string'
|
|
924
|
+
? input.trim()
|
|
925
|
+
: buildCliStructuredInputPrompt(input).trim();
|
|
926
|
+
if (!content) return;
|
|
927
|
+
|
|
928
|
+
const receivedAt = Date.now();
|
|
929
|
+
this.lastAcknowledgedUserInputAt = receivedAt;
|
|
930
|
+
const dedupKey = `user_input_ack:${crypto
|
|
931
|
+
.createHash('sha256')
|
|
932
|
+
.update(`${this.instanceId}:${content}:${receivedAt}`)
|
|
933
|
+
.digest('hex')
|
|
934
|
+
.slice(0, 24)}`;
|
|
935
|
+
this.appendRuntimeMessage(buildChatMessage({
|
|
936
|
+
role: 'user',
|
|
937
|
+
senderName: 'User',
|
|
938
|
+
kind: 'standard',
|
|
939
|
+
content,
|
|
940
|
+
receivedAt,
|
|
941
|
+
timestamp: receivedAt,
|
|
942
|
+
source: 'runtime_input_ack',
|
|
943
|
+
meta: {
|
|
944
|
+
runtimeInputAck: true,
|
|
945
|
+
provider: this.type,
|
|
946
|
+
workspace: this.workingDir,
|
|
947
|
+
},
|
|
948
|
+
} as ChatMessage), dedupKey);
|
|
949
|
+
}
|
|
950
|
+
|
|
698
951
|
dispose(): void {
|
|
699
952
|
this.adapter.shutdown();
|
|
700
953
|
this.monitor.reset();
|
|
@@ -706,6 +959,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
706
959
|
|
|
707
960
|
private completedDebounceTimer: NodeJS.Timeout | null = null;
|
|
708
961
|
private completedDebouncePending: CompletedDebouncePending | null = null;
|
|
962
|
+
private lastExternalCompletionProbe: ExternalTranscriptProbe | null = null;
|
|
709
963
|
|
|
710
964
|
private async enforceFreshSessionLaunchIfNeeded(): Promise<void> {
|
|
711
965
|
const scriptName = getForcedNewSessionScriptName(this.provider, this.launchMode);
|
|
@@ -740,7 +994,180 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
740
994
|
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
741
995
|
const role = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : '';
|
|
742
996
|
const content = lastVisible ? flattenContent(lastVisible.content).trim() : '';
|
|
743
|
-
|
|
997
|
+
if (role !== 'assistant' || !content) return false;
|
|
998
|
+
// Guard: if the last assistant message looks like an active approval/input prompt,
|
|
999
|
+
// it is not a real completion — the session is still awaiting user input.
|
|
1000
|
+
if (looksLikeActiveApprovalPromptText(content)) return false;
|
|
1001
|
+
return true;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
private buildExternalTranscriptProbe(messages: unknown[], sourcePath?: string, sourceMtimeMs?: number): ExternalTranscriptProbe {
|
|
1005
|
+
const visibleMessages = messages.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
1006
|
+
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
1007
|
+
const readAt = Date.now();
|
|
1008
|
+
const mtimeMs = Number(sourceMtimeMs) || 0;
|
|
1009
|
+
return {
|
|
1010
|
+
readAt,
|
|
1011
|
+
msgCount: messages.length,
|
|
1012
|
+
lastRole: typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null,
|
|
1013
|
+
lastKind: typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null,
|
|
1014
|
+
contentLen: lastVisible ? flattenContent(lastVisible.content).trim().length : 0,
|
|
1015
|
+
sourcePath: typeof sourcePath === 'string' && sourcePath ? sourcePath : null,
|
|
1016
|
+
sourceMtimeMs: mtimeMs || null,
|
|
1017
|
+
mtimeAgeMs: mtimeMs ? Math.max(0, readAt - mtimeMs) : null,
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
private recordPendingTranscriptProbe(pending: CompletedDebouncePending): ExternalTranscriptProbe | null {
|
|
1022
|
+
const probe = this.lastExternalCompletionProbe;
|
|
1023
|
+
if (!probe) return null;
|
|
1024
|
+
const history = pending.transcriptProbeHistory || [];
|
|
1025
|
+
const last = history[history.length - 1];
|
|
1026
|
+
if (!last || last.readAt !== probe.readAt || last.msgCount !== probe.msgCount || last.lastRole !== probe.lastRole || last.contentLen !== probe.contentLen) {
|
|
1027
|
+
history.push(probe);
|
|
1028
|
+
pending.transcriptProbeHistory = history.slice(-5);
|
|
1029
|
+
}
|
|
1030
|
+
return probe;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
private readExternalCompletionMessages(): unknown[] | null {
|
|
1034
|
+
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1035
|
+
if (!adapterOwnsMessagesElsewhere) return null;
|
|
1036
|
+
if (!this.providerSessionId) return null;
|
|
1037
|
+
if (!isNativeSourceCanonicalHistory(this.provider.nativeHistory)) return null;
|
|
1038
|
+
|
|
1039
|
+
if (this.lastExternalCompletionProbe?.sourcePath) {
|
|
1040
|
+
try { fs.statSync(this.lastExternalCompletionProbe.sourcePath); } catch { /* best-effort metadata refresh */ }
|
|
1041
|
+
}
|
|
1042
|
+
const restoredHistory = readProviderChatHistory(this.type, {
|
|
1043
|
+
canonicalHistory: this.provider.nativeHistory,
|
|
1044
|
+
historySessionId: this.providerSessionId,
|
|
1045
|
+
workspace: this.workingDir,
|
|
1046
|
+
offset: 0,
|
|
1047
|
+
limit: Number.MAX_SAFE_INTEGER,
|
|
1048
|
+
historyBehavior: this.provider.historyBehavior,
|
|
1049
|
+
scripts: this.provider.scripts as any,
|
|
1050
|
+
sessionStartedAtMs: this.startedAt,
|
|
1051
|
+
forceRefresh: true,
|
|
1052
|
+
});
|
|
1053
|
+
if (restoredHistory.source !== 'provider-native') {
|
|
1054
|
+
this.lastExternalCompletionProbe = null;
|
|
1055
|
+
return null;
|
|
1056
|
+
}
|
|
1057
|
+
this.lastExternalCompletionProbe = this.buildExternalTranscriptProbe(
|
|
1058
|
+
restoredHistory.messages,
|
|
1059
|
+
restoredHistory.sourcePath,
|
|
1060
|
+
restoredHistory.sourceMtimeMs,
|
|
1061
|
+
);
|
|
1062
|
+
return restoredHistory.messages;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
private completionFinalAssistantEvidence(parsedMessages: unknown): CompletionFinalAssistantEvidence {
|
|
1066
|
+
if (this.completionHasFinalAssistantMessage(parsedMessages)) {
|
|
1067
|
+
return {
|
|
1068
|
+
present: true,
|
|
1069
|
+
messages: Array.isArray(parsedMessages) ? parsedMessages : [],
|
|
1070
|
+
source: 'parsed',
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
const externalMessages = this.readExternalCompletionMessages();
|
|
1075
|
+
if (externalMessages) {
|
|
1076
|
+
return {
|
|
1077
|
+
present: this.completionHasFinalAssistantMessage(externalMessages),
|
|
1078
|
+
messages: externalMessages,
|
|
1079
|
+
source: 'external-native',
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
return {
|
|
1084
|
+
present: false,
|
|
1085
|
+
messages: Array.isArray(parsedMessages) ? parsedMessages : [],
|
|
1086
|
+
source: 'unavailable',
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
private completionFinalSummary(parsedMessages: unknown): string | undefined {
|
|
1091
|
+
// For native-source providers (claude-cli: chatMessagesOwnedExternally), the PTY
|
|
1092
|
+
// screen parse is NOT the source of truth for the final summary — the terminal
|
|
1093
|
+
// wraps/scrolls/clips text, so a screen-parsed assistant message is often a partial
|
|
1094
|
+
// prefix (e.g. 76 chars of a 112-char turn). The append-only native transcript holds
|
|
1095
|
+
// the complete turn. Prefer it whenever it yields a longer/complete summary; fall back
|
|
1096
|
+
// to the parsed screen only when the transcript is unavailable. This is the real cause
|
|
1097
|
+
// of the truncated finalSummary — independent of cloud vs standalone (it surfaces on
|
|
1098
|
+
// any short, fast-completing task where screen parse wins the race).
|
|
1099
|
+
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1100
|
+
const parsedSummary = extractFinalSummaryFromMessages(
|
|
1101
|
+
(this.completionHasFinalAssistantMessage(parsedMessages)
|
|
1102
|
+
? (Array.isArray(parsedMessages) ? parsedMessages : [])
|
|
1103
|
+
: []) as any,
|
|
1104
|
+
);
|
|
1105
|
+
if (adapterOwnsMessagesElsewhere) {
|
|
1106
|
+
const externalMessages = this.readExternalCompletionMessages();
|
|
1107
|
+
const externalSummary = externalMessages
|
|
1108
|
+
? extractFinalSummaryFromMessages(externalMessages as any)
|
|
1109
|
+
: '';
|
|
1110
|
+
// The transcript is authoritative for native-source providers. Use it unless it is
|
|
1111
|
+
// empty (not yet written) — only then fall back to whatever the screen parsed.
|
|
1112
|
+
if (externalSummary) return externalSummary;
|
|
1113
|
+
return parsedSummary || undefined;
|
|
1114
|
+
}
|
|
1115
|
+
return parsedSummary || undefined;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
private buildCompletedFinalizationDiagnostic(args: {
|
|
1119
|
+
blockReason: string;
|
|
1120
|
+
latestStatus?: any;
|
|
1121
|
+
latestVisibleStatus: string;
|
|
1122
|
+
waitedMs: number;
|
|
1123
|
+
pending: CompletedDebouncePending;
|
|
1124
|
+
emittedAfterFinalizationTimeout: boolean;
|
|
1125
|
+
}): Record<string, unknown> {
|
|
1126
|
+
let parsed: any = null;
|
|
1127
|
+
let parseError: string | undefined;
|
|
1128
|
+
try {
|
|
1129
|
+
parsed = this.adapter.getScriptParsedStatus();
|
|
1130
|
+
} catch (error: any) {
|
|
1131
|
+
parseError = error?.message || String(error);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
const evidence = this.completionFinalAssistantEvidence(parsed?.messages);
|
|
1135
|
+
if (evidence.source === 'external-native') {
|
|
1136
|
+
this.recordPendingTranscriptProbe(args.pending);
|
|
1137
|
+
}
|
|
1138
|
+
const visibleMessages = (Array.isArray(evidence.messages) ? evidence.messages : [])
|
|
1139
|
+
.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
1140
|
+
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
1141
|
+
const lastVisibleRole = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null;
|
|
1142
|
+
const lastVisibleKind = typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null;
|
|
1143
|
+
const lastVisibleContentLength = lastVisible ? flattenContent(lastVisible.content).trim().length : 0;
|
|
1144
|
+
|
|
1145
|
+
return {
|
|
1146
|
+
providerType: this.type,
|
|
1147
|
+
sessionId: this.instanceId,
|
|
1148
|
+
providerSessionId: this.providerSessionId || null,
|
|
1149
|
+
workspace: this.workingDir,
|
|
1150
|
+
blockReason: args.blockReason,
|
|
1151
|
+
emittedAfterFinalizationTimeout: args.emittedAfterFinalizationTimeout,
|
|
1152
|
+
waitedMs: args.waitedMs,
|
|
1153
|
+
maxWaitMs: COMPLETED_FINALIZATION_MAX_WAIT_MS,
|
|
1154
|
+
adapterStatus: typeof args.latestStatus?.status === 'string' ? args.latestStatus.status : null,
|
|
1155
|
+
latestVisibleStatus: args.latestVisibleStatus,
|
|
1156
|
+
parsedStatus: typeof parsed?.status === 'string' ? parsed.status : (parseError ? 'parse_error' : 'unknown'),
|
|
1157
|
+
parseError: parseError || undefined,
|
|
1158
|
+
finalAssistantPresent: evidence.present,
|
|
1159
|
+
finalAssistantEvidenceSource: evidence.source,
|
|
1160
|
+
visibleMessageCount: visibleMessages.length,
|
|
1161
|
+
lastVisibleRole,
|
|
1162
|
+
lastVisibleKind,
|
|
1163
|
+
lastVisibleContentLength,
|
|
1164
|
+
pendingStartedAt: this.generatingStartedAt || null,
|
|
1165
|
+
pendingFirstObservedAt: args.pending.firstObservedAt,
|
|
1166
|
+
pendingTimestamp: args.pending.timestamp,
|
|
1167
|
+
pendingDurationSec: args.pending.duration,
|
|
1168
|
+
previousBlockReason: args.pending.loggedBlockReason || null,
|
|
1169
|
+
transcriptProbeHistory: args.pending.transcriptProbeHistory || [],
|
|
1170
|
+
};
|
|
744
1171
|
}
|
|
745
1172
|
|
|
746
1173
|
private hasAdapterPendingResponse(): boolean {
|
|
@@ -765,32 +1192,97 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
765
1192
|
if (!isCliGeneratingLikeStatus(parsedRawStatus)) return false;
|
|
766
1193
|
if (adapterRawStatus !== 'idle') return false;
|
|
767
1194
|
if (hasNonEmptyCliModalButtons(parsedStatus?.activeModal ?? parsedStatus?.modal)) return false;
|
|
768
|
-
|
|
1195
|
+
if (this.hasAdapterPendingResponse()) return false;
|
|
1196
|
+
// Do not suppress when the adapter's raw response buffer is still non-empty.
|
|
1197
|
+
// This catches the case where isWaitingForResponse has already flipped to false
|
|
1198
|
+
// (so getPartialResponse() returns '') but the provider's native parser still
|
|
1199
|
+
// reports generating because it's parsing buffered content. Suppressing the
|
|
1200
|
+
// finalization block here would emit a false completion event while the provider
|
|
1201
|
+
// session is still actively processing its response stream.
|
|
1202
|
+
const adapterAny = this.adapter as any;
|
|
1203
|
+
if (typeof adapterAny?.responseBuffer === 'string' && adapterAny.responseBuffer.trim()) return false;
|
|
1204
|
+
return true;
|
|
769
1205
|
}
|
|
770
1206
|
|
|
771
|
-
private
|
|
772
|
-
if (latestVisibleStatus !== 'idle') return `status:${latestVisibleStatus}
|
|
1207
|
+
private getCompletedFinalizationBlock(latestVisibleStatus: string, pending: CompletedDebouncePending): CompletedFinalizationBlock | null {
|
|
1208
|
+
if (latestVisibleStatus !== 'idle') return { reason: `status:${latestVisibleStatus}`, terminal: true };
|
|
773
1209
|
|
|
774
1210
|
const adapterAny = this.adapter as any;
|
|
775
|
-
|
|
776
|
-
if (
|
|
1211
|
+
const approvalResolvedIdle = pending.previousStatus === 'waiting_approval';
|
|
1212
|
+
if (!approvalResolvedIdle) {
|
|
1213
|
+
if (adapterAny?.isWaitingForResponse === true) return { reason: 'adapter_waiting_for_response', terminal: true };
|
|
1214
|
+
if (adapterAny?.currentTurnScope) return { reason: 'adapter_turn_scope_active', terminal: true };
|
|
1215
|
+
if (this.hasAdapterPendingResponse()) return { reason: 'adapter_pending_response', terminal: true };
|
|
1216
|
+
}
|
|
777
1217
|
|
|
778
1218
|
const partial = typeof this.adapter.getPartialResponse === 'function'
|
|
779
1219
|
? this.adapter.getPartialResponse()
|
|
780
1220
|
: '';
|
|
781
|
-
if (typeof partial === 'string' && partial.trim()) return 'partial_response_pending';
|
|
1221
|
+
if (typeof partial === 'string' && partial.trim()) return { reason: 'partial_response_pending', terminal: true };
|
|
782
1222
|
|
|
783
1223
|
let parsed: any;
|
|
784
1224
|
try {
|
|
785
1225
|
parsed = this.adapter.getScriptParsedStatus();
|
|
786
1226
|
} catch (error: any) {
|
|
787
|
-
return `parse_error:${error?.message || String(error)}
|
|
1227
|
+
return { reason: `parse_error:${error?.message || String(error)}` };
|
|
788
1228
|
}
|
|
789
1229
|
|
|
790
1230
|
const parsedStatus = typeof parsed?.status === 'string' ? parsed.status : 'unknown';
|
|
791
|
-
if (parsedStatus !== 'idle')
|
|
792
|
-
|
|
793
|
-
|
|
1231
|
+
if (parsedStatus !== 'idle') {
|
|
1232
|
+
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
1233
|
+
if (this.shouldSuppressStaleParsedBusyStatus(parsed, adapterStatus)) return null;
|
|
1234
|
+
return { reason: `parsed_status:${parsedStatus}`, terminal: isCliGeneratingLikeStatus(parsedStatus) };
|
|
1235
|
+
}
|
|
1236
|
+
if (parsed?.activeModal || parsed?.modal) return { reason: 'parsed_modal_active', terminal: true };
|
|
1237
|
+
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1238
|
+
const finalAssistantEvidence = this.completionFinalAssistantEvidence(parsed?.messages);
|
|
1239
|
+
const allowMissingAssistantTimeout = !!(this.settings.meshNodeFor || this.settings.meshActiveTaskId || this.settings.launchedByCoordinator);
|
|
1240
|
+
LOG.debug('CLI', `[${this.type}] finalAssistantEvidence: present=${finalAssistantEvidence.present} source=${finalAssistantEvidence.source} adapterOwnsMessagesElsewhere=${adapterOwnsMessagesElsewhere} parsedStatus=${parsedStatus}`);
|
|
1241
|
+
if (!finalAssistantEvidence.present) {
|
|
1242
|
+
if (adapterOwnsMessagesElsewhere) {
|
|
1243
|
+
if (finalAssistantEvidence.source === 'external-native') {
|
|
1244
|
+
const probe = this.recordPendingTranscriptProbe(pending);
|
|
1245
|
+
if (probe && !pending.loggedTranscriptProbe) {
|
|
1246
|
+
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`);
|
|
1247
|
+
pending.loggedTranscriptProbe = true;
|
|
1248
|
+
}
|
|
1249
|
+
LOG.debug('CLI', `[${this.type}] external-native probe result: lastRole=${probe?.lastRole} contentLen=${probe?.contentLen}`);
|
|
1250
|
+
if (probe?.lastRole === 'assistant' && (probe.contentLen ?? 0) > 0) {
|
|
1251
|
+
return null;
|
|
1252
|
+
}
|
|
1253
|
+
if (this.type === 'antigravity-cli') {
|
|
1254
|
+
return null;
|
|
1255
|
+
}
|
|
1256
|
+
return { reason: 'missing_final_assistant', terminal: true, allowTimeout: allowMissingAssistantTimeout };
|
|
1257
|
+
}
|
|
1258
|
+
if ((this.provider as any).requiresFinalAssistantBeforeIdle === true) {
|
|
1259
|
+
return { reason: 'missing_final_assistant', terminal: true, allowTimeout: allowMissingAssistantTimeout };
|
|
1260
|
+
}
|
|
1261
|
+
} else {
|
|
1262
|
+
LOG.debug('CLI', `[${this.type}] missing_final_assistant (not ownsExternal) requiresFinalAssistant=${!!(this.provider as any).requiresFinalAssistantBeforeIdle}`);
|
|
1263
|
+
return {
|
|
1264
|
+
reason: 'missing_final_assistant',
|
|
1265
|
+
terminal: (this.provider as any).requiresFinalAssistantBeforeIdle === true,
|
|
1266
|
+
allowTimeout: allowMissingAssistantTimeout,
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// Guard: if the screen still shows an approval/choice prompt as the last visible text,
|
|
1272
|
+
// the turn is not complete even if the parsed status says idle and there is an assistant
|
|
1273
|
+
// message. This catches the case where waiting_approval→idle transitions occur before
|
|
1274
|
+
// the modal has been resolved (e.g. the PTY rendered the prompt but no button press fired).
|
|
1275
|
+
try {
|
|
1276
|
+
const screenText = typeof (this.adapter as any).getScreenText === 'function'
|
|
1277
|
+
? String((this.adapter as any).getScreenText() || '')
|
|
1278
|
+
: '';
|
|
1279
|
+
if (screenText) {
|
|
1280
|
+
const tailLines = screenText.split(/\r?\n/).slice(-16).join('\n');
|
|
1281
|
+
if (looksLikeActiveApprovalPromptText(tailLines)) {
|
|
1282
|
+
return { reason: 'screen_shows_approval_prompt', terminal: approvalResolvedIdle };
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
} catch { /* defensive: screen text read is best-effort */ }
|
|
794
1286
|
|
|
795
1287
|
return null;
|
|
796
1288
|
}
|
|
@@ -809,7 +1301,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
809
1301
|
|
|
810
1302
|
const latestStatus = this.adapter.getStatus({ allowParse: false });
|
|
811
1303
|
const latestAutoApproveActive = latestStatus.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
812
|
-
const latestVisibleStatus = latestAutoApproveActive ? 'generating' : latestStatus.status;
|
|
1304
|
+
const latestVisibleStatus = latestAutoApproveActive || this.autoApproveBusy ? 'generating' : latestStatus.status;
|
|
1305
|
+
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
1306
|
if (latestVisibleStatus !== 'idle') {
|
|
814
1307
|
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed ${latestVisibleStatus})`);
|
|
815
1308
|
this.completedDebouncePending = null;
|
|
@@ -817,10 +1310,12 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
817
1310
|
return;
|
|
818
1311
|
}
|
|
819
1312
|
|
|
820
|
-
const
|
|
821
|
-
if (
|
|
1313
|
+
const block = this.getCompletedFinalizationBlock(latestVisibleStatus, pending);
|
|
1314
|
+
if (block) {
|
|
1315
|
+
const blockReason = block.reason;
|
|
822
1316
|
const waitedMs = Date.now() - pending.firstObservedAt;
|
|
823
|
-
|
|
1317
|
+
LOG.debug('CLI', `[${this.type}] finalization block: reason=${blockReason} terminal=${block.terminal} waitedMs=${waitedMs} maxWait=${COMPLETED_FINALIZATION_MAX_WAIT_MS}`);
|
|
1318
|
+
if ((block.terminal && !block.allowTimeout) || waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
|
|
824
1319
|
if (pending.loggedBlockReason !== blockReason) {
|
|
825
1320
|
LOG.info('CLI', `[${this.type}] waiting to emit completed until transcript finalizes (${blockReason})`);
|
|
826
1321
|
pending.loggedBlockReason = blockReason;
|
|
@@ -828,10 +1323,29 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
828
1323
|
this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
|
|
829
1324
|
return;
|
|
830
1325
|
}
|
|
831
|
-
|
|
1326
|
+
const completionDiagnostic = this.buildCompletedFinalizationDiagnostic({
|
|
1327
|
+
blockReason,
|
|
1328
|
+
latestStatus,
|
|
1329
|
+
latestVisibleStatus,
|
|
1330
|
+
waitedMs,
|
|
1331
|
+
pending,
|
|
1332
|
+
emittedAfterFinalizationTimeout: true,
|
|
1333
|
+
});
|
|
1334
|
+
LOG.warn('CLI', `[${this.type}] emitting completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
|
|
1335
|
+
this.pushEvent({
|
|
1336
|
+
event: 'agent:generating_completed',
|
|
1337
|
+
chatTitle: pending.chatTitle,
|
|
1338
|
+
duration: pending.duration,
|
|
1339
|
+
timestamp: pending.timestamp,
|
|
1340
|
+
finalSummary: blockReason.startsWith('parsed_status:')
|
|
1341
|
+
? ''
|
|
1342
|
+
: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
|
|
1343
|
+
completionDiagnostic,
|
|
1344
|
+
});
|
|
832
1345
|
this.completedDebouncePending = null;
|
|
833
1346
|
this.completedDebounceTimer = null;
|
|
834
1347
|
this.generatingStartedAt = 0;
|
|
1348
|
+
this.lastApprovalEventFingerprint = '';
|
|
835
1349
|
return;
|
|
836
1350
|
}
|
|
837
1351
|
|
|
@@ -841,11 +1355,12 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
841
1355
|
chatTitle: pending.chatTitle,
|
|
842
1356
|
duration: pending.duration,
|
|
843
1357
|
timestamp: pending.timestamp,
|
|
844
|
-
finalSummary:
|
|
1358
|
+
finalSummary: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
|
|
845
1359
|
});
|
|
846
1360
|
this.completedDebouncePending = null;
|
|
847
1361
|
this.completedDebounceTimer = null;
|
|
848
1362
|
this.generatingStartedAt = 0;
|
|
1363
|
+
this.lastApprovalEventFingerprint = '';
|
|
849
1364
|
}
|
|
850
1365
|
|
|
851
1366
|
private maybeAutoApproveStatus(adapterStatus: any, now = Date.now()): boolean {
|
|
@@ -853,15 +1368,57 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
853
1368
|
// Guard re-entry: onStatusChange/getState can observe the same modal multiple
|
|
854
1369
|
// times while the PTY absorbs the approval key. Without this flag, repeated
|
|
855
1370
|
// snapshots would write stray keys into the input once the modal dismisses.
|
|
856
|
-
|
|
1371
|
+
// However, Claude Code can present a second approval immediately after the
|
|
1372
|
+
// first. Resolve a changed modal signature even while the previous write is
|
|
1373
|
+
// still inside the short busy window.
|
|
1374
|
+
if (!autoApproveActive) {
|
|
1375
|
+
this.lastAutoApprovalSignature = '';
|
|
1376
|
+
return autoApproveActive;
|
|
1377
|
+
}
|
|
1378
|
+
const modal = adapterStatus.activeModal;
|
|
1379
|
+
// (fix) Do not auto-approve when no concrete modal/buttons are present.
|
|
1380
|
+
// Claude TUI flaps between paints; without this guard adapterStatus
|
|
1381
|
+
// could report status=waiting_approval with activeModal=null (or with
|
|
1382
|
+
// an empty buttons array briefly) and we'd still call
|
|
1383
|
+
// resolveModal(-1) — which used to type "1" into the prompt
|
|
1384
|
+
// repeatedly. Skip until a real modal is captured.
|
|
1385
|
+
const buttons = Array.isArray(modal?.buttons)
|
|
1386
|
+
? modal.buttons.map((b: any) => String(b || '').trim()).filter(Boolean)
|
|
1387
|
+
: [];
|
|
1388
|
+
if (!modal || buttons.length === 0) {
|
|
1389
|
+
return autoApproveActive;
|
|
1390
|
+
}
|
|
1391
|
+
const { index: buttonIndex, label: buttonLabel } = pickAutoApprovalButton(buttons);
|
|
1392
|
+
if (buttonIndex < 0) {
|
|
1393
|
+
// No concrete button matched — don't pick a random index, just
|
|
1394
|
+
// surface the modal so the user can decide.
|
|
1395
|
+
return autoApproveActive;
|
|
1396
|
+
}
|
|
1397
|
+
// Include the FSM's approval entry seq: two distinct back-to-back
|
|
1398
|
+
// approvals can carry identical message/buttons (common with
|
|
1399
|
+
// claude-cli). Without the seq their signatures collide and the 5s
|
|
1400
|
+
// busy-window re-entry guard below swallows the second auto-approve,
|
|
1401
|
+
// leaving it stuck. The seq is bumped by the FSM on every fresh
|
|
1402
|
+
// waiting_approval entry, so a new approval always yields a new
|
|
1403
|
+
// signature and fires through.
|
|
1404
|
+
const approvalEntrySeq = typeof adapterStatus?.approvalEntrySeq === 'number'
|
|
1405
|
+
? adapterStatus.approvalEntrySeq
|
|
1406
|
+
: 0;
|
|
1407
|
+
const signature = [
|
|
1408
|
+
approvalEntrySeq,
|
|
1409
|
+
typeof modal?.message === 'string' ? modal.message.trim() : '',
|
|
1410
|
+
buttons.join('|'),
|
|
1411
|
+
buttonIndex,
|
|
1412
|
+
].join('::');
|
|
1413
|
+
if (!this.autoApproveBusy || signature !== this.lastAutoApprovalSignature) {
|
|
857
1414
|
this.autoApproveBusy = true;
|
|
1415
|
+
this.lastAutoApprovalSignature = signature;
|
|
858
1416
|
if (this.autoApproveBusyTimer) clearTimeout(this.autoApproveBusyTimer);
|
|
859
1417
|
this.autoApproveBusyTimer = setTimeout(() => {
|
|
860
1418
|
this.autoApproveBusy = false;
|
|
861
1419
|
this.autoApproveBusyTimer = null;
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
const { index: buttonIndex, label: buttonLabel } = pickApprovalButton(modal?.buttons, this.provider);
|
|
1420
|
+
this.lastAutoApprovalSignature = '';
|
|
1421
|
+
}, 5000);
|
|
865
1422
|
this.recordAutoApproval(modal?.message, buttonLabel, now);
|
|
866
1423
|
setTimeout(() => {
|
|
867
1424
|
this.adapter.resolveModal(buttonIndex);
|
|
@@ -876,10 +1433,23 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
876
1433
|
// during long-running CLI sessions. Keep this path on adapter-owned light
|
|
877
1434
|
// state only; rich provider parsing is reserved for getState/read_chat.
|
|
878
1435
|
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
1436
|
+
const adapterProviderSessionId = normalizeProviderSessionId(
|
|
1437
|
+
this.provider,
|
|
1438
|
+
typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
|
|
1439
|
+
);
|
|
1440
|
+
if (adapterProviderSessionId) {
|
|
1441
|
+
this.promoteProviderSessionId(adapterProviderSessionId);
|
|
1442
|
+
}
|
|
879
1443
|
const parsedStatus = null;
|
|
880
1444
|
const rawStatus = adapterStatus.status;
|
|
881
1445
|
const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, now);
|
|
882
|
-
|
|
1446
|
+
// During the autoApproveBusy window (2s after firing approval key), the PTY
|
|
1447
|
+
// can briefly report 'idle' before the next generating phase starts. Treat that
|
|
1448
|
+
// transient idle as 'generating' to suppress a spurious agent:generating_completed
|
|
1449
|
+
// push notification. The adapter's status is otherwise authoritative — native
|
|
1450
|
+
// transcript shape does NOT override the FSM's busy/idle decision.
|
|
1451
|
+
const autoApproveHoldIdle = this.autoApproveBusy && rawStatus === 'idle';
|
|
1452
|
+
const newStatus = autoApproveActive || autoApproveHoldIdle ? 'generating' : rawStatus;
|
|
883
1453
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
884
1454
|
const chatTitle = `${this.provider.name} · ${dirName}`;
|
|
885
1455
|
const partial = this.adapter.getPartialResponse();
|
|
@@ -891,10 +1461,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
891
1461
|
if (newStatus !== this.lastStatus) {
|
|
892
1462
|
LOG.info('CLI', `[${this.type}] status: ${this.lastStatus} → ${newStatus}`);
|
|
893
1463
|
if (this.lastStatus === 'idle' && newStatus === 'generating') {
|
|
1464
|
+
// If a completion event is already pending and the turn has ended
|
|
1465
|
+
// (generatingStartedAt===0), the PTY is painting its prompt area
|
|
1466
|
+
// after completing. Ignore this blip — do not cancel the pending
|
|
1467
|
+
// completion and do not advance lastStatus to generating.
|
|
1468
|
+
if (this.completedDebouncePending && this.generatingStartedAt === 0) {
|
|
1469
|
+
LOG.debug('CLI', `[${this.type}] ignoring post-completion PTY generating blip (generatingStartedAt=0)`);
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
894
1472
|
this.suppressIdleHistoryReplay = false;
|
|
895
1473
|
// Cancel any pending completed event (multi-step: idle→generating resume)
|
|
896
1474
|
if (this.completedDebouncePending) {
|
|
897
|
-
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed generating)`);
|
|
1475
|
+
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed generating) generatingStartedAt=${this.generatingStartedAt} isWaitingForResponse=${!!(this.adapter as any)?.isWaitingForResponse}`);
|
|
898
1476
|
if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
|
|
899
1477
|
this.completedDebouncePending = null;
|
|
900
1478
|
}
|
|
@@ -926,10 +1504,15 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
926
1504
|
if (!this.generatingStartedAt) this.generatingStartedAt = now;
|
|
927
1505
|
const modal = adapterStatus.activeModal;
|
|
928
1506
|
LOG.info('CLI', `[${this.type}] approval modal: "${modal?.message?.slice(0, 80) ?? 'none'}"`);
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1507
|
+
const approvalFingerprint = JSON.stringify({
|
|
1508
|
+
message: typeof modal?.message === 'string' ? modal.message.trim() : '',
|
|
1509
|
+
buttons: Array.isArray(modal?.buttons) ? modal.buttons.map((button: unknown) => String(button).trim()) : [],
|
|
1510
|
+
});
|
|
1511
|
+
// PTY redraws repeat the same modal content; fingerprint dedup prevents duplicate events.
|
|
1512
|
+
// Do NOT also gate on lastStatus: consecutive approvals can arrive waiting_approval→waiting_approval
|
|
1513
|
+
// (e.g. antigravity-cli resolves one prompt and immediately shows the next) and would be silently dropped.
|
|
1514
|
+
if (approvalFingerprint !== this.lastApprovalEventFingerprint) {
|
|
1515
|
+
this.lastApprovalEventFingerprint = approvalFingerprint;
|
|
933
1516
|
this.appendRuntimeSystemMessage(
|
|
934
1517
|
this.formatApprovalRequestMessage(modal?.message, modal?.buttons),
|
|
935
1518
|
`approval_request:${now}`,
|
|
@@ -943,20 +1526,102 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
943
1526
|
}
|
|
944
1527
|
} else if (newStatus === 'idle' && (this.lastStatus === 'generating' || this.lastStatus === 'waiting_approval')) {
|
|
945
1528
|
const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : 0;
|
|
946
|
-
//
|
|
1529
|
+
// Guard: if generatingStartedAt===0 and no debounce pending, the generating phase
|
|
1530
|
+
// was entered from 'starting' state (startup PTY noise), not from a real idle→generating
|
|
1531
|
+
// task dispatch. The idle→generating handler is the only code path that sets
|
|
1532
|
+
// generatingStartedAt and generatingDebouncePending, so both being absent means no
|
|
1533
|
+
// task was ever dispatched. Suppress the spurious completion event and fall through
|
|
1534
|
+
// to a simple lastStatus update.
|
|
1535
|
+
if (!this.generatingStartedAt && !this.generatingDebouncePending) {
|
|
1536
|
+
LOG.debug('CLI', `[${this.type}] suppressed startup-phase generating→idle blip (generatingStartedAt=0, no debounce pending)`);
|
|
1537
|
+
} else
|
|
1538
|
+
// If debounce still pending (generating lasted < 1s), cancel both UI events.
|
|
1539
|
+
// Still emit agent:generating_completed so mesh orchestration can record
|
|
1540
|
+
// task_completed for direct dispatches that complete faster than the debounce.
|
|
947
1541
|
if (this.generatingDebouncePending) {
|
|
948
|
-
|
|
1542
|
+
const shortDurationMs = this.generatingStartedAt ? now - this.generatingStartedAt : 0;
|
|
1543
|
+
LOG.info('CLI', `[${this.type}] suppressed short generating (${shortDurationMs}ms)`);
|
|
949
1544
|
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
1545
|
+
// Emit completion for mesh task association even though the UI generating
|
|
1546
|
+
// started/completed pair is suppressed (too short for visible UI update).
|
|
1547
|
+
let shortFinalSummary: string | undefined;
|
|
1548
|
+
let shortEvidenceSource: CompletionFinalAssistantEvidence['source'] = 'unavailable';
|
|
1549
|
+
try {
|
|
1550
|
+
const parsedMessages = this.adapter?.getScriptParsedStatus()?.messages;
|
|
1551
|
+
const evidence = this.completionFinalAssistantEvidence(parsedMessages);
|
|
1552
|
+
shortEvidenceSource = evidence.source;
|
|
1553
|
+
shortFinalSummary = extractFinalSummaryFromMessages(evidence.messages as any);
|
|
1554
|
+
} catch { /* best-effort */ }
|
|
1555
|
+
// If a real response is confirmed, retroactively emit started so the chat
|
|
1556
|
+
// bubble appears even though the debounce suppressed the original event.
|
|
1557
|
+
if (shortFinalSummary) {
|
|
1558
|
+
this.pushEvent({ event: 'agent:generating_started', chatTitle, timestamp: now - shortDurationMs });
|
|
1559
|
+
}
|
|
950
1560
|
this.generatingDebouncePending = null;
|
|
951
1561
|
this.generatingStartedAt = 0;
|
|
1562
|
+
const missingEvidence = ((this.provider as any).requiresFinalAssistantBeforeIdle === true || shortEvidenceSource === 'external-native') && !shortFinalSummary;
|
|
1563
|
+
if (missingEvidence) {
|
|
1564
|
+
LOG.warn('CLI', `[${this.type}] short completion missing final assistant evidence (source=${shortEvidenceSource})`);
|
|
1565
|
+
}
|
|
1566
|
+
// When evidence is missing and there is no active mesh task context, suppress
|
|
1567
|
+
// the completion event. Providers with requiresFinalAssistantBeforeIdle or
|
|
1568
|
+
// external-native history must confirm a final assistant message before the
|
|
1569
|
+
// coordinator records task_completed. Only emit here if a mesh task is active
|
|
1570
|
+
// so the coordinator can apply its own timeout/retry logic.
|
|
1571
|
+
const hasMeshContext = !!(this.settings.meshNodeFor || this.settings.meshActiveTaskId || this.settings.launchedByCoordinator);
|
|
1572
|
+
if (missingEvidence && !hasMeshContext) {
|
|
1573
|
+
LOG.info('CLI', `[${this.type}] short completion suppressed: missing final assistant evidence, no mesh context (source=${shortEvidenceSource})`);
|
|
1574
|
+
// completedDebouncePending intentionally left null — the session is now idle
|
|
1575
|
+
// with no confirmed turn, matching the startup-blip suppression semantics.
|
|
1576
|
+
} else {
|
|
1577
|
+
this.pushEvent({
|
|
1578
|
+
event: 'agent:generating_completed',
|
|
1579
|
+
chatTitle,
|
|
1580
|
+
duration: 0,
|
|
1581
|
+
timestamp: now,
|
|
1582
|
+
finalSummary: shortFinalSummary,
|
|
1583
|
+
completionDiagnostic: {
|
|
1584
|
+
reason: 'short_generating_suppressed',
|
|
1585
|
+
shortDurationMs,
|
|
1586
|
+
finalAssistantEvidenceSource: shortEvidenceSource,
|
|
1587
|
+
...(missingEvidence ? { blockReason: 'missing_final_assistant' } : {}),
|
|
1588
|
+
},
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
952
1591
|
} else {
|
|
953
1592
|
// Debounce completed, then require the rich transcript path that read_chat
|
|
954
1593
|
// uses to show an idle turn whose last user-facing message is assistant.
|
|
955
|
-
this.completedDebouncePending = {
|
|
956
|
-
|
|
1594
|
+
this.completedDebouncePending = {
|
|
1595
|
+
chatTitle,
|
|
1596
|
+
duration,
|
|
1597
|
+
timestamp: now,
|
|
1598
|
+
firstObservedAt: now,
|
|
1599
|
+
previousStatus: this.lastStatus,
|
|
1600
|
+
};
|
|
1601
|
+
const ownsExternalHistory = !!(this.adapter as any)?.chatMessagesOwnedExternally;
|
|
1602
|
+
const flushDelay = ownsExternalHistory ? 0 : 3000;
|
|
1603
|
+
LOG.debug('CLI', `[${this.type}] set completedDebouncePending duration=${duration}s ownsExternalHistory=${ownsExternalHistory} flushDelay=${flushDelay}ms generatingStartedAt=${this.generatingStartedAt}`);
|
|
1604
|
+
this.scheduleCompletedDebounceFlush(flushDelay);
|
|
957
1605
|
}
|
|
958
1606
|
} else if (newStatus === 'idle' && this.lastStatus === 'starting') {
|
|
959
1607
|
this.pushEvent({ event: 'agent:ready', chatTitle, timestamp: now });
|
|
1608
|
+
} else if (newStatus === 'error') {
|
|
1609
|
+
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
1610
|
+
this.generatingDebouncePending = null;
|
|
1611
|
+
if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
|
|
1612
|
+
this.completedDebouncePending = null;
|
|
1613
|
+
this.errorMessage = adapterStatus.errorMessage || this.errorMessage;
|
|
1614
|
+
this.errorReason = (adapterStatus.errorReason as ProviderErrorReason) || this.errorReason;
|
|
1615
|
+
this.pushEvent({
|
|
1616
|
+
event: 'agent:stopped',
|
|
1617
|
+
chatTitle,
|
|
1618
|
+
timestamp: now,
|
|
1619
|
+
finalSummary: adapterStatus.errorMessage || adapterStatus.errorReason || 'Provider reported an error',
|
|
1620
|
+
completionDiagnostic: {
|
|
1621
|
+
reason: adapterStatus.errorReason || 'provider_error',
|
|
1622
|
+
errorMessage: adapterStatus.errorMessage || undefined,
|
|
1623
|
+
},
|
|
1624
|
+
});
|
|
960
1625
|
} else if (newStatus === 'stopped') {
|
|
961
1626
|
// Cancel any pending debounce
|
|
962
1627
|
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
@@ -977,7 +1642,34 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
977
1642
|
// Monitor check (cooldown based notification, IDE/CLI common)
|
|
978
1643
|
const agentKey = `${this.type}:cli`;
|
|
979
1644
|
const monitorEvents = this.monitor.check(agentKey, newStatus, now, progressFingerprint);
|
|
1645
|
+
const monitorParsedStatus: any = parsedStatus;
|
|
980
1646
|
for (const me of monitorEvents) {
|
|
1647
|
+
if (
|
|
1648
|
+
me.type === 'monitor:long_generating'
|
|
1649
|
+
&& this.completionHasFinalAssistantMessage(monitorParsedStatus?.messages)
|
|
1650
|
+
&& !this.hasAdapterPendingResponse()
|
|
1651
|
+
&& !hasNonEmptyCliModalButtons(monitorParsedStatus?.activeModal ?? monitorParsedStatus?.modal)
|
|
1652
|
+
) {
|
|
1653
|
+
this.pushEvent({
|
|
1654
|
+
event: 'agent:generating_completed',
|
|
1655
|
+
chatTitle,
|
|
1656
|
+
duration: this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : undefined,
|
|
1657
|
+
timestamp: me.timestamp,
|
|
1658
|
+
finalSummary: extractFinalSummaryFromMessages(monitorParsedStatus?.messages),
|
|
1659
|
+
completionDiagnostic: {
|
|
1660
|
+
providerType: this.type,
|
|
1661
|
+
sessionId: this.instanceId,
|
|
1662
|
+
providerSessionId: this.providerSessionId || null,
|
|
1663
|
+
reconciliationReason: 'long_generating_monitor_final_summary',
|
|
1664
|
+
finalAssistantPresent: true,
|
|
1665
|
+
},
|
|
1666
|
+
});
|
|
1667
|
+
this.generatingStartedAt = 0;
|
|
1668
|
+
// Cancel any pending debounce flush — monitor already fired completion.
|
|
1669
|
+
if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
|
|
1670
|
+
this.completedDebouncePending = null;
|
|
1671
|
+
continue;
|
|
1672
|
+
}
|
|
981
1673
|
this.pushEvent({ event: me.type, agentKey: me.agentKey, message: me.message, elapsedSec: me.elapsedSec, timestamp: me.timestamp });
|
|
982
1674
|
}
|
|
983
1675
|
}
|
|
@@ -1003,9 +1695,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1003
1695
|
};
|
|
1004
1696
|
if (this.context?.emitProviderEvent) {
|
|
1005
1697
|
this.context.emitProviderEvent(enrichedEvent);
|
|
1006
|
-
|
|
1698
|
+
} else {
|
|
1699
|
+
this.events.push(enrichedEvent);
|
|
1700
|
+
}
|
|
1701
|
+
// Auto-detach a direct-dispatch mesh assignment once the dispatched
|
|
1702
|
+
// task reaches a terminal state. Leaving meshNodeFor pinned would
|
|
1703
|
+
// route this session's next unrelated turn (a dashboard chat) into
|
|
1704
|
+
// the coordinator as if it were the completion of another task.
|
|
1705
|
+
// We schedule after the emit so the originating coordinator still
|
|
1706
|
+
// observes the completion event with its routing marker intact.
|
|
1707
|
+
if (TERMINAL_MESH_EVENTS.has(event.event) && this.settings.meshActiveTaskId) {
|
|
1708
|
+
try { this.detachMeshAssignment(); } catch { /* best-effort */ }
|
|
1007
1709
|
}
|
|
1008
|
-
this.events.push(enrichedEvent);
|
|
1009
1710
|
}
|
|
1010
1711
|
|
|
1011
1712
|
private flushEvents(): ProviderEvent[] {
|
|
@@ -1258,12 +1959,28 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1258
1959
|
index,
|
|
1259
1960
|
source: 'parsed',
|
|
1260
1961
|
}));
|
|
1962
|
+
const getRole = (message: ChatMessage): string => typeof message.role === 'string'
|
|
1963
|
+
? message.role.trim().toLowerCase()
|
|
1964
|
+
: '';
|
|
1261
1965
|
const runtimeEntries: MergeEntry[] = this.runtimeMessages.map((entry, index) => ({
|
|
1262
1966
|
message: entry.message,
|
|
1263
1967
|
index: parsedMessages.length + index,
|
|
1264
|
-
source: 'runtime',
|
|
1968
|
+
source: 'runtime' as const,
|
|
1265
1969
|
runtimeKey: entry.key,
|
|
1266
|
-
}))
|
|
1970
|
+
})).filter((entry) => {
|
|
1971
|
+
const meta = entry.message.meta && typeof entry.message.meta === 'object' && !Array.isArray(entry.message.meta)
|
|
1972
|
+
? entry.message.meta as Record<string, unknown>
|
|
1973
|
+
: {};
|
|
1974
|
+
if (meta.runtimeInputAck !== true) return true;
|
|
1975
|
+
const runtimeText = flattenContent(entry.message.content).replace(/\s+/g, ' ').trim();
|
|
1976
|
+
if (!runtimeText) return false;
|
|
1977
|
+
return !parsedEntries.some((parsedEntry) => {
|
|
1978
|
+
const parsedRole = getRole(parsedEntry.message);
|
|
1979
|
+
if (parsedRole !== 'user' && parsedRole !== 'human') return false;
|
|
1980
|
+
const parsedText = flattenContent(parsedEntry.message.content).replace(/\s+/g, ' ').trim();
|
|
1981
|
+
return parsedText === runtimeText;
|
|
1982
|
+
});
|
|
1983
|
+
});
|
|
1267
1984
|
const getTime = (message: ChatMessage): number => {
|
|
1268
1985
|
const value = typeof message.receivedAt === 'number'
|
|
1269
1986
|
? message.receivedAt
|
|
@@ -1273,9 +1990,6 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1273
1990
|
return Number.isFinite(value) && value > 0 ? value : 0;
|
|
1274
1991
|
};
|
|
1275
1992
|
|
|
1276
|
-
const getRole = (message: ChatMessage): string => typeof message.role === 'string'
|
|
1277
|
-
? message.role.trim().toLowerCase()
|
|
1278
|
-
: '';
|
|
1279
1993
|
const isRuntimeOverlay = (entry: MergeEntry): boolean => {
|
|
1280
1994
|
if (entry.source !== 'runtime') return false;
|
|
1281
1995
|
const key = typeof entry.runtimeKey === 'string' ? entry.runtimeKey.trim().toLowerCase() : '';
|
|
@@ -1341,7 +2055,9 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1341
2055
|
this.providerSessionId = nextSessionId;
|
|
1342
2056
|
this.historyWriter.promoteHistorySession(this.type, previousHistorySessionId, nextSessionId);
|
|
1343
2057
|
this.historyWriter.writeSessionStart(this.type, nextSessionId, this.workingDir, this.instanceId);
|
|
1344
|
-
this.
|
|
2058
|
+
if (this.shouldHydrateExistingProviderHistory()) {
|
|
2059
|
+
this.restorePersistedHistoryFromCurrentSession();
|
|
2060
|
+
}
|
|
1345
2061
|
this.adapter.updateRuntimeMeta({ providerSessionId: nextSessionId });
|
|
1346
2062
|
this.onProviderSessionResolved?.({
|
|
1347
2063
|
instanceId: this.instanceId,
|
|
@@ -1354,9 +2070,27 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1354
2070
|
LOG.info('CLI', `[${this.type}] discovered provider session id: ${nextSessionId}`);
|
|
1355
2071
|
}
|
|
1356
2072
|
|
|
2073
|
+
private shouldHydrateExistingProviderHistory(): boolean {
|
|
2074
|
+
return this.launchMode === 'resume' || this.launchMode === 'manual';
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
private shouldSuppressFreshLaunchStartupReplay(parsedMessages: unknown[], parsedStatus: any, adapterStatus: any, parsedProviderSessionId = ''): boolean {
|
|
2078
|
+
if (this.launchMode !== 'new') return false;
|
|
2079
|
+
if (this.providerSessionId) return false;
|
|
2080
|
+
if (!Array.isArray(parsedMessages) || parsedMessages.length === 0) return false;
|
|
2081
|
+
if (!isIdleStatus(adapterStatus?.status) || !isIdleStatus(parsedStatus?.status)) return false;
|
|
2082
|
+
if (parsedProviderSessionId) return true;
|
|
2083
|
+
|
|
2084
|
+
const newestMessageAt = parsedMessages.reduce<number>((newest, message) => Math.max(newest, getMessageTime(message)), 0);
|
|
2085
|
+
|
|
2086
|
+
// Untimestamped idle parser output during a fresh launch is usually the
|
|
2087
|
+
// provider's last workspace transcript before a new turn exists.
|
|
2088
|
+
return newestMessageAt === 0;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
1357
2091
|
private syncCanonicalSavedHistoryIfNeeded(): boolean {
|
|
1358
2092
|
if (!this.providerSessionId) return false;
|
|
1359
|
-
const canonicalHistory = this.provider.
|
|
2093
|
+
const canonicalHistory = this.provider.nativeHistory;
|
|
1360
2094
|
if (!canonicalHistory) return false;
|
|
1361
2095
|
|
|
1362
2096
|
if (isNativeSourceCanonicalHistory(canonicalHistory)) {
|
|
@@ -1418,9 +2152,9 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1418
2152
|
private restorePersistedHistoryFromCurrentSession(): void {
|
|
1419
2153
|
if (!this.providerSessionId) return;
|
|
1420
2154
|
this.syncCanonicalSavedHistoryIfNeeded();
|
|
1421
|
-
const restoredHistory = isNativeSourceCanonicalHistory(this.provider.
|
|
2155
|
+
const restoredHistory = isNativeSourceCanonicalHistory(this.provider.nativeHistory)
|
|
1422
2156
|
? readProviderChatHistory(this.type, {
|
|
1423
|
-
canonicalHistory: this.provider.
|
|
2157
|
+
canonicalHistory: this.provider.nativeHistory,
|
|
1424
2158
|
historySessionId: this.providerSessionId,
|
|
1425
2159
|
workspace: this.workingDir,
|
|
1426
2160
|
offset: 0,
|