@adhdev/daemon-core 0.9.82-rc.19 → 0.9.82-rc.191
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 +22 -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/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 +26 -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 +7 -1
- package/dist/git/git-types.d.ts +2 -0
- package/dist/index.d.ts +37 -10
- package/dist/index.js +32864 -16401
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +32584 -16191
- 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/beads-db.d.ts +83 -0
- package/dist/mesh/contracts.d.ts +164 -0
- package/dist/mesh/coordinator-prompt.d.ts +30 -0
- package/dist/mesh/coordinator-registry.d.ts +59 -0
- package/dist/mesh/mesh-active-work.d.ts +90 -0
- package/dist/mesh/mesh-events.d.ts +143 -6
- 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.d.ts +58 -1
- package/dist/mesh/mesh-refine-status.d.ts +26 -0
- package/dist/mesh/mesh-work-queue.d.ts +55 -6
- package/dist/mesh/preview-freshness.d.ts +18 -0
- package/dist/mesh/refine-config.d.ts +193 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +113 -0
- package/dist/providers/approval-utils.d.ts +13 -0
- package/dist/providers/cli-provider-instance.d.ts +39 -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 +12 -0
- package/dist/providers/provider-instance.d.ts +12 -0
- 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 +60 -0
- package/dist/providers/spec/cli-adapter.d.ts +98 -0
- package/dist/providers/spec/driver.d.ts +168 -0
- package/dist/providers/spec/evaluator.d.ts +55 -0
- package/dist/providers/spec/loader.d.ts +14 -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/schema.gen.d.ts +549 -0
- package/dist/providers/spec/types.d.ts +237 -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 +108 -1
- package/dist/sessions/registry.d.ts +3 -0
- package/dist/shared-types.d.ts +42 -1
- package/dist/status/reporter.d.ts +2 -0
- package/dist/status/snapshot.d.ts +1 -0
- package/dist/types.d.ts +5 -0
- package/package.json +7 -2
- package/src/agent-stream/poller.ts +2 -3
- package/src/boot/daemon-lifecycle.ts +19 -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 +21 -2
- package/src/cli-adapters/cli-script-runner.ts +421 -0
- package/src/cli-adapters/cli-state-engine.ts +1086 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +670 -1137
- 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 +17 -1
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
- package/src/commands/chat-commands.ts +1787 -60
- package/src/commands/cli-manager.ts +283 -14
- package/src/commands/handler.ts +809 -2
- package/src/commands/mesh-coordinator.ts +331 -122
- package/src/commands/router.ts +3600 -510
- package/src/config/chat-history.ts +95 -24
- package/src/config/config.ts +12 -0
- package/src/config/mesh-config.ts +280 -2
- package/src/config/recent-activity.ts +8 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/detection/ide-detector.ts +26 -16
- package/src/git/git-commands.ts +37 -7
- 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 +115 -9
- 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/beads-db.ts +527 -0
- package/src/mesh/contracts.ts +329 -0
- package/src/mesh/coordinator-prompt.ts +204 -30
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +437 -0
- package/src/mesh/mesh-events.ts +1275 -108
- package/src/mesh/mesh-fast-forward.ts +438 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger.ts +457 -104
- package/src/mesh/mesh-refine-status.ts +144 -0
- package/src/mesh/mesh-work-queue.ts +267 -159
- package/src/mesh/preview-freshness.ts +118 -0
- package/src/mesh/refine-config.ts +366 -0
- package/src/mesh/worktree-bootstrap-config.ts +247 -0
- 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 +763 -60
- 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 +419 -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 +9 -0
- package/src/providers/provider-loader.ts +582 -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 +176 -0
- package/src/providers/spec/cli-adapter.ts +534 -0
- package/src/providers/spec/driver.ts +612 -0
- package/src/providers/spec/evaluator.ts +344 -0
- package/src/providers/spec/loader.ts +130 -0
- package/src/providers/spec/native-history-executor.ts +939 -0
- package/src/providers/spec/route.ts +51 -0
- package/src/providers/spec/schema.gen.ts +538 -0
- package/src/providers/spec/schema.json +235 -0
- package/src/providers/spec/types.ts +256 -0
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +408 -0
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +118 -1
- package/src/sessions/registry.ts +3 -0
- package/src/shared-types.ts +45 -1
- package/src/status/builders.ts +24 -6
- 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/mesh/mesh-sync.d.ts +0 -53
- 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,48 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
503
590
|
this.errorMessage = undefined;
|
|
504
591
|
this.errorReason = undefined;
|
|
505
592
|
}
|
|
593
|
+
const adapterProviderSessionId = normalizeProviderSessionId(
|
|
594
|
+
this.provider,
|
|
595
|
+
typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
|
|
596
|
+
);
|
|
506
597
|
const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, Date.now());
|
|
507
|
-
|
|
598
|
+
let visibleStatus = parseErrorMessage || parsedStatus?.status === 'error'
|
|
508
599
|
? 'error'
|
|
509
600
|
: (autoApproveActive ? 'generating' : adapterStatus.status);
|
|
510
|
-
const
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
);
|
|
514
|
-
if (parsedProviderSessionId) {
|
|
515
|
-
this.promoteProviderSessionId(parsedProviderSessionId);
|
|
601
|
+
const externalNativeFinal = this.getExternalNativeFinalReconciliation(parsedStatus?.messages, adapterStatus);
|
|
602
|
+
if (externalNativeFinal && isCliGeneratingLikeStatus(visibleStatus)) {
|
|
603
|
+
visibleStatus = 'idle';
|
|
516
604
|
}
|
|
517
605
|
const runtime = this.adapter.getRuntimeMetadata();
|
|
518
606
|
this.maybeAppendRuntimeRecoveryMessage(runtime);
|
|
519
607
|
let parsedMessages = Array.isArray(parsedStatus?.messages)
|
|
520
608
|
? parsedStatus.messages
|
|
521
609
|
: [];
|
|
610
|
+
const parsedProviderSessionId = normalizeProviderSessionId(
|
|
611
|
+
this.provider,
|
|
612
|
+
typeof parsedStatus?.providerSessionId === 'string' ? parsedStatus.providerSessionId : '',
|
|
613
|
+
);
|
|
614
|
+
const suppressFreshLaunchStartupReplay = this.shouldSuppressFreshLaunchStartupReplay(
|
|
615
|
+
parsedMessages,
|
|
616
|
+
parsedStatus,
|
|
617
|
+
adapterStatus,
|
|
618
|
+
parsedProviderSessionId,
|
|
619
|
+
);
|
|
620
|
+
if (adapterProviderSessionId && !suppressFreshLaunchStartupReplay) {
|
|
621
|
+
this.promoteProviderSessionId(adapterProviderSessionId);
|
|
622
|
+
}
|
|
623
|
+
if (parsedProviderSessionId && !suppressFreshLaunchStartupReplay) {
|
|
624
|
+
this.promoteProviderSessionId(parsedProviderSessionId);
|
|
625
|
+
}
|
|
626
|
+
if (suppressFreshLaunchStartupReplay) {
|
|
627
|
+
parsedMessages = [];
|
|
628
|
+
}
|
|
629
|
+
// Adapter runtime metadata is transport-owned and is not guaranteed to
|
|
630
|
+
// identify this conversation. Spec adapters historically exposed the
|
|
631
|
+
// provider spec id (for example "codex-cli") as runtimeId, which made
|
|
632
|
+
// concurrent sessions share one activeChat identity until their native
|
|
633
|
+
// provider session ids were discovered.
|
|
634
|
+
const activeChatId = this.providerSessionId || this.instanceId;
|
|
522
635
|
const historyMessageCount = Number.isFinite(parsedStatus?.historyMessageCount)
|
|
523
636
|
? Math.max(0, Number(parsedStatus.historyMessageCount))
|
|
524
637
|
: null;
|
|
@@ -528,7 +641,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
528
641
|
: [];
|
|
529
642
|
}
|
|
530
643
|
const mergedMessages = this.mergeConversationMessages(parsedMessages);
|
|
531
|
-
const canonicalBackedHistory = this.
|
|
644
|
+
const canonicalBackedHistory = this.shouldHydrateExistingProviderHistory()
|
|
645
|
+
? this.syncCanonicalSavedHistoryIfNeeded()
|
|
646
|
+
: false;
|
|
647
|
+
const statusMessages = canonicalBackedHistory && this.lastPersistedHistoryMessages.length > 0
|
|
648
|
+
? this.lastPersistedHistoryMessages.map((message) => ({
|
|
649
|
+
role: message.role,
|
|
650
|
+
content: message.content,
|
|
651
|
+
kind: message.kind,
|
|
652
|
+
senderName: message.senderName,
|
|
653
|
+
receivedAt: message.receivedAt,
|
|
654
|
+
}))
|
|
655
|
+
: mergedMessages;
|
|
532
656
|
|
|
533
657
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
534
658
|
const parsedChatStatus = typeof parsedStatus?.status === 'string' && parsedStatus.status.trim()
|
|
@@ -572,7 +696,12 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
572
696
|
}
|
|
573
697
|
}
|
|
574
698
|
|
|
575
|
-
this.applyProviderResponse(
|
|
699
|
+
this.applyProviderResponse(
|
|
700
|
+
suppressFreshLaunchStartupReplay && parsedStatus && typeof parsedStatus === 'object'
|
|
701
|
+
? { ...parsedStatus, providerSessionId: undefined }
|
|
702
|
+
: parsedStatus,
|
|
703
|
+
{ phase: 'immediate' },
|
|
704
|
+
);
|
|
576
705
|
const surface = resolveProviderStateSurface({
|
|
577
706
|
summaryMetadata: this.summaryMetadata as any,
|
|
578
707
|
controlValues: this.controlValues,
|
|
@@ -592,13 +721,15 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
592
721
|
status: visibleStatus,
|
|
593
722
|
mode: this.presentationMode,
|
|
594
723
|
activeChat: {
|
|
595
|
-
id:
|
|
724
|
+
id: activeChatId,
|
|
596
725
|
title: parsedStatus?.title || dirName,
|
|
597
726
|
status: activeChatStatus,
|
|
598
|
-
messages:
|
|
727
|
+
messages: statusMessages,
|
|
599
728
|
activeModal: autoApproveActive ? null : (parsedStatus?.activeModal ?? adapterStatus.activeModal),
|
|
729
|
+
activeInteractivePrompt: this.activeInteractivePrompt,
|
|
600
730
|
inputContent: '',
|
|
601
731
|
},
|
|
732
|
+
activeInteractivePrompt: this.activeInteractivePrompt,
|
|
602
733
|
workspace: this.workingDir,
|
|
603
734
|
instanceId: this.instanceId,
|
|
604
735
|
providerSessionId: this.providerSessionId,
|
|
@@ -651,13 +782,17 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
651
782
|
};
|
|
652
783
|
}
|
|
653
784
|
|
|
654
|
-
getSessionModalState(): SessionModalState {
|
|
655
|
-
const adapterStatus = this.adapter.getStatus({ allowParse:
|
|
785
|
+
getSessionModalState(sessionId?: string): SessionModalState {
|
|
786
|
+
const adapterStatus = this.adapter.getStatus({ allowParse: true });
|
|
656
787
|
const autoApproveActive = adapterStatus.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
657
788
|
const visibleStatus = autoApproveActive ? 'generating' : adapterStatus.status;
|
|
658
789
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
659
790
|
return {
|
|
660
|
-
|
|
791
|
+
// Honor the caller-supplied sessionId — InstanceMgr rejects the
|
|
792
|
+
// projection when projected.id !== requested sessionId, and
|
|
793
|
+
// this.instanceId is the manager's internal key, not the public
|
|
794
|
+
// sessionId the dashboard subscribes by.
|
|
795
|
+
id: sessionId ?? this.instanceId,
|
|
661
796
|
status: visibleStatus,
|
|
662
797
|
title: dirName,
|
|
663
798
|
activeModal: autoApproveActive ? null : adapterStatus.activeModal,
|
|
@@ -665,7 +800,22 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
665
800
|
}
|
|
666
801
|
|
|
667
802
|
updateSettings(newSettings: Record<string, any>): void {
|
|
668
|
-
|
|
803
|
+
const runtimeMeshSettings: Record<string, any> = {};
|
|
804
|
+
for (const key of [
|
|
805
|
+
'meshNodeFor',
|
|
806
|
+
'meshNodeId',
|
|
807
|
+
'meshActiveTaskId',
|
|
808
|
+
'meshCoordinatorFor',
|
|
809
|
+
'meshCoordinatorDaemonId',
|
|
810
|
+
'meshCoordinatorNodeId',
|
|
811
|
+
'spawnedSessionVisibility',
|
|
812
|
+
'launchedByCoordinator',
|
|
813
|
+
]) {
|
|
814
|
+
if (this.settings[key] !== undefined && newSettings[key] === undefined) {
|
|
815
|
+
runtimeMeshSettings[key] = this.settings[key];
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
this.settings = { ...newSettings, ...runtimeMeshSettings };
|
|
669
819
|
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
670
820
|
this.monitor.updateConfig({
|
|
671
821
|
approvalAlert: this.settings.approvalAlert !== false,
|
|
@@ -674,6 +824,39 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
674
824
|
});
|
|
675
825
|
}
|
|
676
826
|
|
|
827
|
+
/**
|
|
828
|
+
* Stamp a direct-dispatch mesh assignment on this instance.
|
|
829
|
+
* setupMeshEventForwarding reads settings.meshNodeFor + meshActiveTaskId to
|
|
830
|
+
* route generating_completed back to the originating coordinator. Without
|
|
831
|
+
* this stamp, mesh_send_task --direct targets a plain CLI session whose
|
|
832
|
+
* completion events silently drop because the forwarder has nothing to
|
|
833
|
+
* match against.
|
|
834
|
+
*/
|
|
835
|
+
attachMeshAssignment(assignment: { meshId: string; nodeId?: string; taskId?: string }): void {
|
|
836
|
+
if (!assignment?.meshId) return;
|
|
837
|
+
this.settings = {
|
|
838
|
+
...this.settings,
|
|
839
|
+
meshNodeFor: assignment.meshId,
|
|
840
|
+
...(assignment.nodeId ? { meshNodeId: assignment.nodeId } : {}),
|
|
841
|
+
...(assignment.taskId ? { meshActiveTaskId: assignment.taskId } : {}),
|
|
842
|
+
};
|
|
843
|
+
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Clear a previously-attached mesh assignment after the task reaches a
|
|
848
|
+
* terminal state. Leaving meshNodeFor pinned would route this session's
|
|
849
|
+
* subsequent unrelated turns (e.g. ad-hoc dashboard chats) to the
|
|
850
|
+
* coordinator as if they were task completions.
|
|
851
|
+
*/
|
|
852
|
+
detachMeshAssignment(): void {
|
|
853
|
+
if (!this.settings.meshNodeFor && !this.settings.meshActiveTaskId && !this.settings.meshNodeId) return;
|
|
854
|
+
const { meshNodeFor, meshNodeId, meshActiveTaskId, ...rest } = this.settings;
|
|
855
|
+
void meshNodeFor; void meshNodeId; void meshActiveTaskId;
|
|
856
|
+
this.settings = rest;
|
|
857
|
+
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
858
|
+
}
|
|
859
|
+
|
|
677
860
|
onEvent(event: string, data?: any): void {
|
|
678
861
|
if (event === 'send_message') {
|
|
679
862
|
const input = normalizeInputEnvelope(data);
|
|
@@ -690,11 +873,67 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
690
873
|
void this.adapter.resolveAction(data).catch((e: any) => {
|
|
691
874
|
LOG.warn('CLI', `[${this.type}] resolve_action failed: ${e?.message || e}`);
|
|
692
875
|
});
|
|
876
|
+
} else if (event === 'interactive_prompt' && data) {
|
|
877
|
+
const prompt = normalizeInteractivePrompt(data);
|
|
878
|
+
if (prompt) {
|
|
879
|
+
this.activeInteractivePrompt = prompt;
|
|
880
|
+
this.events.push({
|
|
881
|
+
event: 'interactive_prompt',
|
|
882
|
+
timestamp: Date.now(),
|
|
883
|
+
promptId: prompt.promptId,
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
} else if (event === 'interactive_prompt_response' && data) {
|
|
887
|
+
try {
|
|
888
|
+
const response = normalizeInteractivePromptResponse(data);
|
|
889
|
+
if (this.activeInteractivePrompt?.promptId === response.promptId) {
|
|
890
|
+
this.activeInteractivePrompt = null;
|
|
891
|
+
}
|
|
892
|
+
if (typeof this.adapter.setInteractivePromptResponse !== 'function') {
|
|
893
|
+
LOG.warn('CLI', `[${this.type}] interactive_prompt_response ignored: adapter does not support interactive prompts`);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
void this.adapter.setInteractivePromptResponse(response).catch((e: any) => {
|
|
897
|
+
LOG.warn('CLI', `[${this.type}] interactive_prompt_response failed: ${e?.message || e}`);
|
|
898
|
+
});
|
|
899
|
+
} catch (e: any) {
|
|
900
|
+
LOG.warn('CLI', `[${this.type}] invalid interactive_prompt_response: ${e?.message || e}`);
|
|
901
|
+
}
|
|
693
902
|
} else if (event === 'provider_state_patch' && data && typeof data === 'object') {
|
|
694
903
|
this.applyProviderResponse(data, { phase: 'immediate' });
|
|
695
904
|
}
|
|
696
905
|
}
|
|
697
906
|
|
|
907
|
+
recordAcknowledgedUserInput(input: InputEnvelope | string): void {
|
|
908
|
+
const content = typeof input === 'string'
|
|
909
|
+
? input.trim()
|
|
910
|
+
: buildCliStructuredInputPrompt(input).trim();
|
|
911
|
+
if (!content) return;
|
|
912
|
+
|
|
913
|
+
const receivedAt = Date.now();
|
|
914
|
+
this.lastAcknowledgedUserInputAt = receivedAt;
|
|
915
|
+
this.externalBusyIdleFingerprint = '';
|
|
916
|
+
const dedupKey = `user_input_ack:${crypto
|
|
917
|
+
.createHash('sha256')
|
|
918
|
+
.update(`${this.instanceId}:${content}:${receivedAt}`)
|
|
919
|
+
.digest('hex')
|
|
920
|
+
.slice(0, 24)}`;
|
|
921
|
+
this.appendRuntimeMessage(buildChatMessage({
|
|
922
|
+
role: 'user',
|
|
923
|
+
senderName: 'User',
|
|
924
|
+
kind: 'standard',
|
|
925
|
+
content,
|
|
926
|
+
receivedAt,
|
|
927
|
+
timestamp: receivedAt,
|
|
928
|
+
source: 'runtime_input_ack',
|
|
929
|
+
meta: {
|
|
930
|
+
runtimeInputAck: true,
|
|
931
|
+
provider: this.type,
|
|
932
|
+
workspace: this.workingDir,
|
|
933
|
+
},
|
|
934
|
+
} as ChatMessage), dedupKey);
|
|
935
|
+
}
|
|
936
|
+
|
|
698
937
|
dispose(): void {
|
|
699
938
|
this.adapter.shutdown();
|
|
700
939
|
this.monitor.reset();
|
|
@@ -706,6 +945,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
706
945
|
|
|
707
946
|
private completedDebounceTimer: NodeJS.Timeout | null = null;
|
|
708
947
|
private completedDebouncePending: CompletedDebouncePending | null = null;
|
|
948
|
+
private lastExternalCompletionProbe: ExternalTranscriptProbe | null = null;
|
|
709
949
|
|
|
710
950
|
private async enforceFreshSessionLaunchIfNeeded(): Promise<void> {
|
|
711
951
|
const scriptName = getForcedNewSessionScriptName(this.provider, this.launchMode);
|
|
@@ -740,7 +980,210 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
740
980
|
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
741
981
|
const role = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : '';
|
|
742
982
|
const content = lastVisible ? flattenContent(lastVisible.content).trim() : '';
|
|
743
|
-
|
|
983
|
+
if (role !== 'assistant' || !content) return false;
|
|
984
|
+
// Guard: if the last assistant message looks like an active approval/input prompt,
|
|
985
|
+
// it is not a real completion — the session is still awaiting user input.
|
|
986
|
+
if (looksLikeActiveApprovalPromptText(content)) return false;
|
|
987
|
+
return true;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
private buildExternalTranscriptProbe(messages: unknown[], sourcePath?: string, sourceMtimeMs?: number): ExternalTranscriptProbe {
|
|
991
|
+
const visibleMessages = messages.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
992
|
+
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
993
|
+
const readAt = Date.now();
|
|
994
|
+
const mtimeMs = Number(sourceMtimeMs) || 0;
|
|
995
|
+
return {
|
|
996
|
+
readAt,
|
|
997
|
+
msgCount: messages.length,
|
|
998
|
+
lastRole: typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null,
|
|
999
|
+
lastKind: typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null,
|
|
1000
|
+
contentLen: lastVisible ? flattenContent(lastVisible.content).trim().length : 0,
|
|
1001
|
+
sourcePath: typeof sourcePath === 'string' && sourcePath ? sourcePath : null,
|
|
1002
|
+
sourceMtimeMs: mtimeMs || null,
|
|
1003
|
+
mtimeAgeMs: mtimeMs ? Math.max(0, readAt - mtimeMs) : null,
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
private recordPendingTranscriptProbe(pending: CompletedDebouncePending): ExternalTranscriptProbe | null {
|
|
1008
|
+
const probe = this.lastExternalCompletionProbe;
|
|
1009
|
+
if (!probe) return null;
|
|
1010
|
+
const history = pending.transcriptProbeHistory || [];
|
|
1011
|
+
const last = history[history.length - 1];
|
|
1012
|
+
if (!last || last.readAt !== probe.readAt || last.msgCount !== probe.msgCount || last.lastRole !== probe.lastRole || last.contentLen !== probe.contentLen) {
|
|
1013
|
+
history.push(probe);
|
|
1014
|
+
pending.transcriptProbeHistory = history.slice(-5);
|
|
1015
|
+
}
|
|
1016
|
+
return probe;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
private readExternalCompletionMessages(): unknown[] | null {
|
|
1020
|
+
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1021
|
+
if (!adapterOwnsMessagesElsewhere) return null;
|
|
1022
|
+
if (!this.providerSessionId) return null;
|
|
1023
|
+
if (!isNativeSourceCanonicalHistory(this.provider.nativeHistory)) return null;
|
|
1024
|
+
|
|
1025
|
+
if (this.lastExternalCompletionProbe?.sourcePath) {
|
|
1026
|
+
try { fs.statSync(this.lastExternalCompletionProbe.sourcePath); } catch { /* best-effort metadata refresh */ }
|
|
1027
|
+
}
|
|
1028
|
+
const restoredHistory = readProviderChatHistory(this.type, {
|
|
1029
|
+
canonicalHistory: this.provider.nativeHistory,
|
|
1030
|
+
historySessionId: this.providerSessionId,
|
|
1031
|
+
workspace: this.workingDir,
|
|
1032
|
+
offset: 0,
|
|
1033
|
+
limit: Number.MAX_SAFE_INTEGER,
|
|
1034
|
+
historyBehavior: this.provider.historyBehavior,
|
|
1035
|
+
scripts: this.provider.scripts as any,
|
|
1036
|
+
sessionStartedAtMs: this.startedAt,
|
|
1037
|
+
forceRefresh: true,
|
|
1038
|
+
});
|
|
1039
|
+
if (restoredHistory.source !== 'provider-native') {
|
|
1040
|
+
this.lastExternalCompletionProbe = null;
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
1043
|
+
this.lastExternalCompletionProbe = this.buildExternalTranscriptProbe(
|
|
1044
|
+
restoredHistory.messages,
|
|
1045
|
+
restoredHistory.sourcePath,
|
|
1046
|
+
restoredHistory.sourceMtimeMs,
|
|
1047
|
+
);
|
|
1048
|
+
return restoredHistory.messages;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
private completionFinalAssistantEvidence(parsedMessages: unknown): CompletionFinalAssistantEvidence {
|
|
1052
|
+
if (this.completionHasFinalAssistantMessage(parsedMessages)) {
|
|
1053
|
+
return {
|
|
1054
|
+
present: true,
|
|
1055
|
+
messages: Array.isArray(parsedMessages) ? parsedMessages : [],
|
|
1056
|
+
source: 'parsed',
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
const externalMessages = this.readExternalCompletionMessages();
|
|
1061
|
+
if (externalMessages) {
|
|
1062
|
+
return {
|
|
1063
|
+
present: this.completionHasFinalAssistantMessage(externalMessages),
|
|
1064
|
+
messages: externalMessages,
|
|
1065
|
+
source: 'external-native',
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
return {
|
|
1070
|
+
present: false,
|
|
1071
|
+
messages: Array.isArray(parsedMessages) ? parsedMessages : [],
|
|
1072
|
+
source: 'unavailable',
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
private completionFinalSummary(parsedMessages: unknown): string | undefined {
|
|
1077
|
+
const evidence = this.completionFinalAssistantEvidence(parsedMessages);
|
|
1078
|
+
return extractFinalSummaryFromMessages(evidence.messages as any);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
private externalNativeFinalFingerprint(evidence: CompletionFinalAssistantEvidence): string {
|
|
1082
|
+
const messages = Array.isArray(evidence.messages) ? evidence.messages : [];
|
|
1083
|
+
const visibleMessages = messages.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
1084
|
+
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
1085
|
+
const content = lastVisible ? flattenContent(lastVisible.content).trim() : '';
|
|
1086
|
+
const receivedAt = lastVisible ? getMessageTime(lastVisible) : 0;
|
|
1087
|
+
const probe = this.lastExternalCompletionProbe;
|
|
1088
|
+
return crypto
|
|
1089
|
+
.createHash('sha256')
|
|
1090
|
+
.update([
|
|
1091
|
+
this.type,
|
|
1092
|
+
this.providerSessionId || '',
|
|
1093
|
+
probe?.sourcePath || '',
|
|
1094
|
+
String(probe?.sourceMtimeMs || 0),
|
|
1095
|
+
String(receivedAt || 0),
|
|
1096
|
+
content.slice(-500),
|
|
1097
|
+
].join('\0'))
|
|
1098
|
+
.digest('hex')
|
|
1099
|
+
.slice(0, 24);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
private getExternalNativeFinalReconciliation(parsedMessages: unknown, adapterStatus: any): ExternalNativeFinalReconciliation | null {
|
|
1103
|
+
const rawStatus = typeof adapterStatus?.status === 'string' ? adapterStatus.status.trim() : '';
|
|
1104
|
+
if (!isCliGeneratingLikeStatus(rawStatus)) return null;
|
|
1105
|
+
if (hasNonEmptyCliModalButtons(adapterStatus?.activeModal ?? adapterStatus?.modal)) return null;
|
|
1106
|
+
|
|
1107
|
+
const evidence = this.completionFinalAssistantEvidence(parsedMessages);
|
|
1108
|
+
if (evidence.source !== 'external-native' || !evidence.present) return null;
|
|
1109
|
+
|
|
1110
|
+
const messages = Array.isArray(evidence.messages) ? evidence.messages : [];
|
|
1111
|
+
const visibleMessages = messages.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
1112
|
+
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
1113
|
+
const lastMessageAt = lastVisible ? getMessageTime(lastVisible) : 0;
|
|
1114
|
+
const sourceMtimeMs = Number(this.lastExternalCompletionProbe?.sourceMtimeMs || 0);
|
|
1115
|
+
const minEvidenceAt = Math.max(
|
|
1116
|
+
this.startedAt > 0 ? this.startedAt - 5_000 : 0,
|
|
1117
|
+
this.generatingStartedAt > 0 ? this.generatingStartedAt - 5_000 : 0,
|
|
1118
|
+
this.lastAcknowledgedUserInputAt > 0 ? this.lastAcknowledgedUserInputAt - 1_000 : 0,
|
|
1119
|
+
);
|
|
1120
|
+
if (minEvidenceAt > 0 && lastMessageAt > 0 && lastMessageAt < minEvidenceAt && sourceMtimeMs < minEvidenceAt) {
|
|
1121
|
+
return null;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const finalSummary = extractFinalSummaryFromMessages(evidence.messages as any);
|
|
1125
|
+
if (!finalSummary) return null;
|
|
1126
|
+
const fingerprint = this.externalNativeFinalFingerprint(evidence);
|
|
1127
|
+
if (fingerprint === this.externalBusyIdleFingerprint) {
|
|
1128
|
+
return { fingerprint, finalSummary, evidence };
|
|
1129
|
+
}
|
|
1130
|
+
this.externalBusyIdleFingerprint = fingerprint;
|
|
1131
|
+
return { fingerprint, finalSummary, evidence };
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
private buildCompletedFinalizationDiagnostic(args: {
|
|
1135
|
+
blockReason: string;
|
|
1136
|
+
latestStatus?: any;
|
|
1137
|
+
latestVisibleStatus: string;
|
|
1138
|
+
waitedMs: number;
|
|
1139
|
+
pending: CompletedDebouncePending;
|
|
1140
|
+
emittedAfterFinalizationTimeout: boolean;
|
|
1141
|
+
}): Record<string, unknown> {
|
|
1142
|
+
let parsed: any = null;
|
|
1143
|
+
let parseError: string | undefined;
|
|
1144
|
+
try {
|
|
1145
|
+
parsed = this.adapter.getScriptParsedStatus();
|
|
1146
|
+
} catch (error: any) {
|
|
1147
|
+
parseError = error?.message || String(error);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
const evidence = this.completionFinalAssistantEvidence(parsed?.messages);
|
|
1151
|
+
if (evidence.source === 'external-native') {
|
|
1152
|
+
this.recordPendingTranscriptProbe(args.pending);
|
|
1153
|
+
}
|
|
1154
|
+
const visibleMessages = (Array.isArray(evidence.messages) ? evidence.messages : [])
|
|
1155
|
+
.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
1156
|
+
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
1157
|
+
const lastVisibleRole = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null;
|
|
1158
|
+
const lastVisibleKind = typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null;
|
|
1159
|
+
const lastVisibleContentLength = lastVisible ? flattenContent(lastVisible.content).trim().length : 0;
|
|
1160
|
+
|
|
1161
|
+
return {
|
|
1162
|
+
providerType: this.type,
|
|
1163
|
+
sessionId: this.instanceId,
|
|
1164
|
+
providerSessionId: this.providerSessionId || null,
|
|
1165
|
+
workspace: this.workingDir,
|
|
1166
|
+
blockReason: args.blockReason,
|
|
1167
|
+
emittedAfterFinalizationTimeout: args.emittedAfterFinalizationTimeout,
|
|
1168
|
+
waitedMs: args.waitedMs,
|
|
1169
|
+
maxWaitMs: COMPLETED_FINALIZATION_MAX_WAIT_MS,
|
|
1170
|
+
adapterStatus: typeof args.latestStatus?.status === 'string' ? args.latestStatus.status : null,
|
|
1171
|
+
latestVisibleStatus: args.latestVisibleStatus,
|
|
1172
|
+
parsedStatus: typeof parsed?.status === 'string' ? parsed.status : (parseError ? 'parse_error' : 'unknown'),
|
|
1173
|
+
parseError: parseError || undefined,
|
|
1174
|
+
finalAssistantPresent: evidence.present,
|
|
1175
|
+
finalAssistantEvidenceSource: evidence.source,
|
|
1176
|
+
visibleMessageCount: visibleMessages.length,
|
|
1177
|
+
lastVisibleRole,
|
|
1178
|
+
lastVisibleKind,
|
|
1179
|
+
lastVisibleContentLength,
|
|
1180
|
+
pendingStartedAt: this.generatingStartedAt || null,
|
|
1181
|
+
pendingFirstObservedAt: args.pending.firstObservedAt,
|
|
1182
|
+
pendingTimestamp: args.pending.timestamp,
|
|
1183
|
+
pendingDurationSec: args.pending.duration,
|
|
1184
|
+
previousBlockReason: args.pending.loggedBlockReason || null,
|
|
1185
|
+
transcriptProbeHistory: args.pending.transcriptProbeHistory || [],
|
|
1186
|
+
};
|
|
744
1187
|
}
|
|
745
1188
|
|
|
746
1189
|
private hasAdapterPendingResponse(): boolean {
|
|
@@ -765,32 +1208,94 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
765
1208
|
if (!isCliGeneratingLikeStatus(parsedRawStatus)) return false;
|
|
766
1209
|
if (adapterRawStatus !== 'idle') return false;
|
|
767
1210
|
if (hasNonEmptyCliModalButtons(parsedStatus?.activeModal ?? parsedStatus?.modal)) return false;
|
|
768
|
-
|
|
1211
|
+
if (this.hasAdapterPendingResponse()) return false;
|
|
1212
|
+
// Do not suppress when the adapter's raw response buffer is still non-empty.
|
|
1213
|
+
// This catches the case where isWaitingForResponse has already flipped to false
|
|
1214
|
+
// (so getPartialResponse() returns '') but the provider's native parser still
|
|
1215
|
+
// reports generating because it's parsing buffered content. Suppressing the
|
|
1216
|
+
// finalization block here would emit a false completion event while the provider
|
|
1217
|
+
// session is still actively processing its response stream.
|
|
1218
|
+
const adapterAny = this.adapter as any;
|
|
1219
|
+
if (typeof adapterAny?.responseBuffer === 'string' && adapterAny.responseBuffer.trim()) return false;
|
|
1220
|
+
return true;
|
|
769
1221
|
}
|
|
770
1222
|
|
|
771
|
-
private
|
|
772
|
-
if (latestVisibleStatus !== 'idle') return `status:${latestVisibleStatus}
|
|
1223
|
+
private getCompletedFinalizationBlock(latestVisibleStatus: string, pending: CompletedDebouncePending, opts?: { externalNativeFinal?: ExternalNativeFinalReconciliation | null }): CompletedFinalizationBlock | null {
|
|
1224
|
+
if (latestVisibleStatus !== 'idle') return { reason: `status:${latestVisibleStatus}`, terminal: true };
|
|
773
1225
|
|
|
774
1226
|
const adapterAny = this.adapter as any;
|
|
775
|
-
|
|
776
|
-
|
|
1227
|
+
const approvalResolvedIdle = pending.previousStatus === 'waiting_approval';
|
|
1228
|
+
const externalNativeFinal = opts?.externalNativeFinal || null;
|
|
1229
|
+
if (!approvalResolvedIdle && !externalNativeFinal) {
|
|
1230
|
+
if (adapterAny?.isWaitingForResponse === true) return { reason: 'adapter_waiting_for_response', terminal: true };
|
|
1231
|
+
if (adapterAny?.currentTurnScope) return { reason: 'adapter_turn_scope_active', terminal: true };
|
|
1232
|
+
if (this.hasAdapterPendingResponse()) return { reason: 'adapter_pending_response', terminal: true };
|
|
1233
|
+
}
|
|
777
1234
|
|
|
778
1235
|
const partial = typeof this.adapter.getPartialResponse === 'function'
|
|
779
1236
|
? this.adapter.getPartialResponse()
|
|
780
1237
|
: '';
|
|
781
|
-
if (typeof partial === 'string' && partial.trim()) return 'partial_response_pending';
|
|
1238
|
+
if (!externalNativeFinal && typeof partial === 'string' && partial.trim()) return { reason: 'partial_response_pending', terminal: true };
|
|
782
1239
|
|
|
783
1240
|
let parsed: any;
|
|
784
1241
|
try {
|
|
785
1242
|
parsed = this.adapter.getScriptParsedStatus();
|
|
786
1243
|
} catch (error: any) {
|
|
787
|
-
return `parse_error:${error?.message || String(error)}
|
|
1244
|
+
return { reason: `parse_error:${error?.message || String(error)}` };
|
|
788
1245
|
}
|
|
789
1246
|
|
|
790
1247
|
const parsedStatus = typeof parsed?.status === 'string' ? parsed.status : 'unknown';
|
|
791
|
-
if (parsedStatus !== 'idle')
|
|
792
|
-
|
|
793
|
-
|
|
1248
|
+
if (parsedStatus !== 'idle') {
|
|
1249
|
+
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
1250
|
+
if (externalNativeFinal && isCliGeneratingLikeStatus(parsedStatus)) return null;
|
|
1251
|
+
if (this.shouldSuppressStaleParsedBusyStatus(parsed, adapterStatus)) return null;
|
|
1252
|
+
return { reason: `parsed_status:${parsedStatus}`, terminal: isCliGeneratingLikeStatus(parsedStatus) };
|
|
1253
|
+
}
|
|
1254
|
+
if (parsed?.activeModal || parsed?.modal) return { reason: 'parsed_modal_active', terminal: true };
|
|
1255
|
+
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1256
|
+
const finalAssistantEvidence = this.completionFinalAssistantEvidence(parsed?.messages);
|
|
1257
|
+
const allowMissingAssistantTimeout = !!(this.settings.meshNodeFor || this.settings.meshActiveTaskId || this.settings.launchedByCoordinator);
|
|
1258
|
+
if (!finalAssistantEvidence.present) {
|
|
1259
|
+
if (adapterOwnsMessagesElsewhere) {
|
|
1260
|
+
if (finalAssistantEvidence.source === 'external-native') {
|
|
1261
|
+
const probe = this.recordPendingTranscriptProbe(pending);
|
|
1262
|
+
if (probe && !pending.loggedTranscriptProbe) {
|
|
1263
|
+
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`);
|
|
1264
|
+
pending.loggedTranscriptProbe = true;
|
|
1265
|
+
}
|
|
1266
|
+
return { reason: 'missing_final_assistant', terminal: true, allowTimeout: allowMissingAssistantTimeout };
|
|
1267
|
+
}
|
|
1268
|
+
// SpecCliAdapter never populates parsed.messages — chat history flows
|
|
1269
|
+
// through the daemon's native-history pipeline, not the status hook.
|
|
1270
|
+
// If that pipeline is unavailable, keep the old skip behavior for
|
|
1271
|
+
// providers that have not opted into strict final-assistant evidence.
|
|
1272
|
+
if ((this.provider as any).requiresFinalAssistantBeforeIdle === true) {
|
|
1273
|
+
return { reason: 'missing_final_assistant', terminal: true, allowTimeout: allowMissingAssistantTimeout };
|
|
1274
|
+
}
|
|
1275
|
+
} else {
|
|
1276
|
+
return {
|
|
1277
|
+
reason: 'missing_final_assistant',
|
|
1278
|
+
terminal: (this.provider as any).requiresFinalAssistantBeforeIdle === true,
|
|
1279
|
+
allowTimeout: allowMissingAssistantTimeout,
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
// Guard: if the screen still shows an approval/choice prompt as the last visible text,
|
|
1285
|
+
// the turn is not complete even if the parsed status says idle and there is an assistant
|
|
1286
|
+
// message. This catches the case where waiting_approval→idle transitions occur before
|
|
1287
|
+
// the modal has been resolved (e.g. the PTY rendered the prompt but no button press fired).
|
|
1288
|
+
try {
|
|
1289
|
+
const screenText = typeof (this.adapter as any).getScreenText === 'function'
|
|
1290
|
+
? String((this.adapter as any).getScreenText() || '')
|
|
1291
|
+
: '';
|
|
1292
|
+
if (screenText) {
|
|
1293
|
+
const tailLines = screenText.split(/\r?\n/).slice(-16).join('\n');
|
|
1294
|
+
if (looksLikeActiveApprovalPromptText(tailLines)) {
|
|
1295
|
+
return { reason: 'screen_shows_approval_prompt', terminal: approvalResolvedIdle };
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
} catch { /* defensive: screen text read is best-effort */ }
|
|
794
1299
|
|
|
795
1300
|
return null;
|
|
796
1301
|
}
|
|
@@ -809,7 +1314,10 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
809
1314
|
|
|
810
1315
|
const latestStatus = this.adapter.getStatus({ allowParse: false });
|
|
811
1316
|
const latestAutoApproveActive = latestStatus.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
812
|
-
const
|
|
1317
|
+
const externalNativeFinal = this.getExternalNativeFinalReconciliation(undefined, latestStatus);
|
|
1318
|
+
const latestVisibleStatus = externalNativeFinal && isCliGeneratingLikeStatus(latestStatus.status)
|
|
1319
|
+
? 'idle'
|
|
1320
|
+
: (latestAutoApproveActive ? 'generating' : latestStatus.status);
|
|
813
1321
|
if (latestVisibleStatus !== 'idle') {
|
|
814
1322
|
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed ${latestVisibleStatus})`);
|
|
815
1323
|
this.completedDebouncePending = null;
|
|
@@ -817,10 +1325,11 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
817
1325
|
return;
|
|
818
1326
|
}
|
|
819
1327
|
|
|
820
|
-
const
|
|
821
|
-
if (
|
|
1328
|
+
const block = this.getCompletedFinalizationBlock(latestVisibleStatus, pending, { externalNativeFinal });
|
|
1329
|
+
if (block) {
|
|
1330
|
+
const blockReason = block.reason;
|
|
822
1331
|
const waitedMs = Date.now() - pending.firstObservedAt;
|
|
823
|
-
if (waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
|
|
1332
|
+
if ((block.terminal && !block.allowTimeout) || waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
|
|
824
1333
|
if (pending.loggedBlockReason !== blockReason) {
|
|
825
1334
|
LOG.info('CLI', `[${this.type}] waiting to emit completed until transcript finalizes (${blockReason})`);
|
|
826
1335
|
pending.loggedBlockReason = blockReason;
|
|
@@ -828,10 +1337,29 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
828
1337
|
this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
|
|
829
1338
|
return;
|
|
830
1339
|
}
|
|
831
|
-
|
|
1340
|
+
const completionDiagnostic = this.buildCompletedFinalizationDiagnostic({
|
|
1341
|
+
blockReason,
|
|
1342
|
+
latestStatus,
|
|
1343
|
+
latestVisibleStatus,
|
|
1344
|
+
waitedMs,
|
|
1345
|
+
pending,
|
|
1346
|
+
emittedAfterFinalizationTimeout: true,
|
|
1347
|
+
});
|
|
1348
|
+
LOG.warn('CLI', `[${this.type}] emitting completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
|
|
1349
|
+
this.pushEvent({
|
|
1350
|
+
event: 'agent:generating_completed',
|
|
1351
|
+
chatTitle: pending.chatTitle,
|
|
1352
|
+
duration: pending.duration,
|
|
1353
|
+
timestamp: pending.timestamp,
|
|
1354
|
+
finalSummary: blockReason.startsWith('parsed_status:')
|
|
1355
|
+
? ''
|
|
1356
|
+
: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
|
|
1357
|
+
completionDiagnostic,
|
|
1358
|
+
});
|
|
832
1359
|
this.completedDebouncePending = null;
|
|
833
1360
|
this.completedDebounceTimer = null;
|
|
834
1361
|
this.generatingStartedAt = 0;
|
|
1362
|
+
this.lastApprovalEventFingerprint = '';
|
|
835
1363
|
return;
|
|
836
1364
|
}
|
|
837
1365
|
|
|
@@ -841,11 +1369,23 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
841
1369
|
chatTitle: pending.chatTitle,
|
|
842
1370
|
duration: pending.duration,
|
|
843
1371
|
timestamp: pending.timestamp,
|
|
844
|
-
finalSummary:
|
|
1372
|
+
finalSummary: externalNativeFinal?.finalSummary || this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
|
|
1373
|
+
...(externalNativeFinal ? {
|
|
1374
|
+
completionDiagnostic: {
|
|
1375
|
+
providerType: this.type,
|
|
1376
|
+
sessionId: this.instanceId,
|
|
1377
|
+
providerSessionId: this.providerSessionId || null,
|
|
1378
|
+
reconciliationReason: 'external_native_final_assistant_while_adapter_busy',
|
|
1379
|
+
finalAssistantPresent: true,
|
|
1380
|
+
finalAssistantEvidenceSource: externalNativeFinal.evidence.source,
|
|
1381
|
+
externalFinalFingerprint: externalNativeFinal.fingerprint,
|
|
1382
|
+
},
|
|
1383
|
+
} : {}),
|
|
845
1384
|
});
|
|
846
1385
|
this.completedDebouncePending = null;
|
|
847
1386
|
this.completedDebounceTimer = null;
|
|
848
1387
|
this.generatingStartedAt = 0;
|
|
1388
|
+
this.lastApprovalEventFingerprint = '';
|
|
849
1389
|
}
|
|
850
1390
|
|
|
851
1391
|
private maybeAutoApproveStatus(adapterStatus: any, now = Date.now()): boolean {
|
|
@@ -853,15 +1393,46 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
853
1393
|
// Guard re-entry: onStatusChange/getState can observe the same modal multiple
|
|
854
1394
|
// times while the PTY absorbs the approval key. Without this flag, repeated
|
|
855
1395
|
// snapshots would write stray keys into the input once the modal dismisses.
|
|
856
|
-
|
|
1396
|
+
// However, Claude Code can present a second approval immediately after the
|
|
1397
|
+
// first. Resolve a changed modal signature even while the previous write is
|
|
1398
|
+
// still inside the short busy window.
|
|
1399
|
+
if (!autoApproveActive) {
|
|
1400
|
+
this.lastAutoApprovalSignature = '';
|
|
1401
|
+
return autoApproveActive;
|
|
1402
|
+
}
|
|
1403
|
+
const modal = adapterStatus.activeModal;
|
|
1404
|
+
// (fix) Do not auto-approve when no concrete modal/buttons are present.
|
|
1405
|
+
// Claude TUI flaps between paints; without this guard adapterStatus
|
|
1406
|
+
// could report status=waiting_approval with activeModal=null (or with
|
|
1407
|
+
// an empty buttons array briefly) and we'd still call
|
|
1408
|
+
// resolveModal(-1) — which used to type "1" into the prompt
|
|
1409
|
+
// repeatedly. Skip until a real modal is captured.
|
|
1410
|
+
const buttons = Array.isArray(modal?.buttons)
|
|
1411
|
+
? modal.buttons.map((b: any) => String(b || '').trim()).filter(Boolean)
|
|
1412
|
+
: [];
|
|
1413
|
+
if (!modal || buttons.length === 0) {
|
|
1414
|
+
return autoApproveActive;
|
|
1415
|
+
}
|
|
1416
|
+
const { index: buttonIndex, label: buttonLabel } = pickAutoApprovalButton(buttons);
|
|
1417
|
+
if (buttonIndex < 0) {
|
|
1418
|
+
// No concrete button matched — don't pick a random index, just
|
|
1419
|
+
// surface the modal so the user can decide.
|
|
1420
|
+
return autoApproveActive;
|
|
1421
|
+
}
|
|
1422
|
+
const signature = [
|
|
1423
|
+
typeof modal?.message === 'string' ? modal.message.trim() : '',
|
|
1424
|
+
buttons.join('|'),
|
|
1425
|
+
buttonIndex,
|
|
1426
|
+
].join('::');
|
|
1427
|
+
if (!this.autoApproveBusy || signature !== this.lastAutoApprovalSignature) {
|
|
857
1428
|
this.autoApproveBusy = true;
|
|
1429
|
+
this.lastAutoApprovalSignature = signature;
|
|
858
1430
|
if (this.autoApproveBusyTimer) clearTimeout(this.autoApproveBusyTimer);
|
|
859
1431
|
this.autoApproveBusyTimer = setTimeout(() => {
|
|
860
1432
|
this.autoApproveBusy = false;
|
|
861
1433
|
this.autoApproveBusyTimer = null;
|
|
1434
|
+
this.lastAutoApprovalSignature = '';
|
|
862
1435
|
}, 2000);
|
|
863
|
-
const modal = adapterStatus.activeModal;
|
|
864
|
-
const { index: buttonIndex, label: buttonLabel } = pickApprovalButton(modal?.buttons, this.provider);
|
|
865
1436
|
this.recordAutoApproval(modal?.message, buttonLabel, now);
|
|
866
1437
|
setTimeout(() => {
|
|
867
1438
|
this.adapter.resolveModal(buttonIndex);
|
|
@@ -876,10 +1447,20 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
876
1447
|
// during long-running CLI sessions. Keep this path on adapter-owned light
|
|
877
1448
|
// state only; rich provider parsing is reserved for getState/read_chat.
|
|
878
1449
|
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
1450
|
+
const adapterProviderSessionId = normalizeProviderSessionId(
|
|
1451
|
+
this.provider,
|
|
1452
|
+
typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
|
|
1453
|
+
);
|
|
1454
|
+
if (adapterProviderSessionId) {
|
|
1455
|
+
this.promoteProviderSessionId(adapterProviderSessionId);
|
|
1456
|
+
}
|
|
879
1457
|
const parsedStatus = null;
|
|
880
1458
|
const rawStatus = adapterStatus.status;
|
|
881
1459
|
const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, now);
|
|
882
|
-
const
|
|
1460
|
+
const externalNativeFinal = this.getExternalNativeFinalReconciliation(undefined, adapterStatus);
|
|
1461
|
+
const newStatus = externalNativeFinal && isCliGeneratingLikeStatus(rawStatus)
|
|
1462
|
+
? 'idle'
|
|
1463
|
+
: (autoApproveActive ? 'generating' : rawStatus);
|
|
883
1464
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
884
1465
|
const chatTitle = `${this.provider.name} · ${dirName}`;
|
|
885
1466
|
const partial = this.adapter.getPartialResponse();
|
|
@@ -926,10 +1507,14 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
926
1507
|
if (!this.generatingStartedAt) this.generatingStartedAt = now;
|
|
927
1508
|
const modal = adapterStatus.activeModal;
|
|
928
1509
|
LOG.info('CLI', `[${this.type}] approval modal: "${modal?.message?.slice(0, 80) ?? 'none'}"`);
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1510
|
+
const approvalFingerprint = JSON.stringify({
|
|
1511
|
+
message: typeof modal?.message === 'string' ? modal.message.trim() : '',
|
|
1512
|
+
buttons: Array.isArray(modal?.buttons) ? modal.buttons.map((button: unknown) => String(button).trim()) : [],
|
|
1513
|
+
});
|
|
1514
|
+
// PTY redraws can briefly leave waiting_approval and then re-enter it.
|
|
1515
|
+
// Keep one coordinator event per logical modal until the turn completes.
|
|
1516
|
+
if (this.lastStatus !== 'waiting_approval' && approvalFingerprint !== this.lastApprovalEventFingerprint) {
|
|
1517
|
+
this.lastApprovalEventFingerprint = approvalFingerprint;
|
|
933
1518
|
this.appendRuntimeSystemMessage(
|
|
934
1519
|
this.formatApprovalRequestMessage(modal?.message, modal?.buttons),
|
|
935
1520
|
`approval_request:${now}`,
|
|
@@ -943,20 +1528,72 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
943
1528
|
}
|
|
944
1529
|
} else if (newStatus === 'idle' && (this.lastStatus === 'generating' || this.lastStatus === 'waiting_approval')) {
|
|
945
1530
|
const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : 0;
|
|
946
|
-
// If debounce still pending (generating lasted < 1s), cancel both events
|
|
1531
|
+
// If debounce still pending (generating lasted < 1s), cancel both UI events.
|
|
1532
|
+
// Still emit agent:generating_completed so mesh orchestration can record
|
|
1533
|
+
// task_completed for direct dispatches that complete faster than the debounce.
|
|
947
1534
|
if (this.generatingDebouncePending) {
|
|
948
|
-
|
|
1535
|
+
const shortDurationMs = this.generatingStartedAt ? now - this.generatingStartedAt : 0;
|
|
1536
|
+
LOG.info('CLI', `[${this.type}] suppressed short generating (${shortDurationMs}ms)`);
|
|
949
1537
|
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
950
1538
|
this.generatingDebouncePending = null;
|
|
951
1539
|
this.generatingStartedAt = 0;
|
|
1540
|
+
// Emit completion for mesh task association even though the UI generating
|
|
1541
|
+
// started/completed pair is suppressed (too short for visible UI update).
|
|
1542
|
+
let shortFinalSummary: string | undefined;
|
|
1543
|
+
let shortEvidenceSource: CompletionFinalAssistantEvidence['source'] = 'unavailable';
|
|
1544
|
+
try {
|
|
1545
|
+
const parsedMessages = this.adapter?.getScriptParsedStatus()?.messages;
|
|
1546
|
+
const evidence = this.completionFinalAssistantEvidence(parsedMessages);
|
|
1547
|
+
shortEvidenceSource = evidence.source;
|
|
1548
|
+
shortFinalSummary = extractFinalSummaryFromMessages(evidence.messages as any);
|
|
1549
|
+
} catch { /* best-effort */ }
|
|
1550
|
+
if (((this.provider as any).requiresFinalAssistantBeforeIdle === true || shortEvidenceSource === 'external-native') && !shortFinalSummary) {
|
|
1551
|
+
LOG.info('CLI', `[${this.type}] suppressed short completion without final assistant evidence`);
|
|
1552
|
+
} else {
|
|
1553
|
+
this.pushEvent({
|
|
1554
|
+
event: 'agent:generating_completed',
|
|
1555
|
+
chatTitle,
|
|
1556
|
+
duration: 0,
|
|
1557
|
+
timestamp: now,
|
|
1558
|
+
finalSummary: shortFinalSummary,
|
|
1559
|
+
completionDiagnostic: {
|
|
1560
|
+
reason: 'short_generating_suppressed',
|
|
1561
|
+
shortDurationMs,
|
|
1562
|
+
finalAssistantEvidenceSource: shortEvidenceSource,
|
|
1563
|
+
},
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
952
1566
|
} else {
|
|
953
1567
|
// Debounce completed, then require the rich transcript path that read_chat
|
|
954
1568
|
// uses to show an idle turn whose last user-facing message is assistant.
|
|
955
|
-
this.completedDebouncePending = {
|
|
1569
|
+
this.completedDebouncePending = {
|
|
1570
|
+
chatTitle,
|
|
1571
|
+
duration,
|
|
1572
|
+
timestamp: now,
|
|
1573
|
+
firstObservedAt: now,
|
|
1574
|
+
previousStatus: this.lastStatus,
|
|
1575
|
+
};
|
|
956
1576
|
this.scheduleCompletedDebounceFlush(3000);
|
|
957
1577
|
}
|
|
958
1578
|
} else if (newStatus === 'idle' && this.lastStatus === 'starting') {
|
|
959
1579
|
this.pushEvent({ event: 'agent:ready', chatTitle, timestamp: now });
|
|
1580
|
+
} else if (newStatus === 'error') {
|
|
1581
|
+
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
1582
|
+
this.generatingDebouncePending = null;
|
|
1583
|
+
if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
|
|
1584
|
+
this.completedDebouncePending = null;
|
|
1585
|
+
this.errorMessage = adapterStatus.errorMessage || this.errorMessage;
|
|
1586
|
+
this.errorReason = (adapterStatus.errorReason as ProviderErrorReason) || this.errorReason;
|
|
1587
|
+
this.pushEvent({
|
|
1588
|
+
event: 'agent:stopped',
|
|
1589
|
+
chatTitle,
|
|
1590
|
+
timestamp: now,
|
|
1591
|
+
finalSummary: adapterStatus.errorMessage || adapterStatus.errorReason || 'Provider reported an error',
|
|
1592
|
+
completionDiagnostic: {
|
|
1593
|
+
reason: adapterStatus.errorReason || 'provider_error',
|
|
1594
|
+
errorMessage: adapterStatus.errorMessage || undefined,
|
|
1595
|
+
},
|
|
1596
|
+
});
|
|
960
1597
|
} else if (newStatus === 'stopped') {
|
|
961
1598
|
// Cancel any pending debounce
|
|
962
1599
|
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
@@ -977,7 +1614,31 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
977
1614
|
// Monitor check (cooldown based notification, IDE/CLI common)
|
|
978
1615
|
const agentKey = `${this.type}:cli`;
|
|
979
1616
|
const monitorEvents = this.monitor.check(agentKey, newStatus, now, progressFingerprint);
|
|
1617
|
+
const monitorParsedStatus: any = parsedStatus;
|
|
980
1618
|
for (const me of monitorEvents) {
|
|
1619
|
+
if (
|
|
1620
|
+
me.type === 'monitor:long_generating'
|
|
1621
|
+
&& this.completionHasFinalAssistantMessage(monitorParsedStatus?.messages)
|
|
1622
|
+
&& !this.hasAdapterPendingResponse()
|
|
1623
|
+
&& !hasNonEmptyCliModalButtons(monitorParsedStatus?.activeModal ?? monitorParsedStatus?.modal)
|
|
1624
|
+
) {
|
|
1625
|
+
this.pushEvent({
|
|
1626
|
+
event: 'agent:generating_completed',
|
|
1627
|
+
chatTitle,
|
|
1628
|
+
duration: this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : undefined,
|
|
1629
|
+
timestamp: me.timestamp,
|
|
1630
|
+
finalSummary: extractFinalSummaryFromMessages(monitorParsedStatus?.messages),
|
|
1631
|
+
completionDiagnostic: {
|
|
1632
|
+
providerType: this.type,
|
|
1633
|
+
sessionId: this.instanceId,
|
|
1634
|
+
providerSessionId: this.providerSessionId || null,
|
|
1635
|
+
reconciliationReason: 'long_generating_monitor_final_summary',
|
|
1636
|
+
finalAssistantPresent: true,
|
|
1637
|
+
},
|
|
1638
|
+
});
|
|
1639
|
+
this.generatingStartedAt = 0;
|
|
1640
|
+
continue;
|
|
1641
|
+
}
|
|
981
1642
|
this.pushEvent({ event: me.type, agentKey: me.agentKey, message: me.message, elapsedSec: me.elapsedSec, timestamp: me.timestamp });
|
|
982
1643
|
}
|
|
983
1644
|
}
|
|
@@ -1003,9 +1664,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1003
1664
|
};
|
|
1004
1665
|
if (this.context?.emitProviderEvent) {
|
|
1005
1666
|
this.context.emitProviderEvent(enrichedEvent);
|
|
1006
|
-
|
|
1667
|
+
} else {
|
|
1668
|
+
this.events.push(enrichedEvent);
|
|
1669
|
+
}
|
|
1670
|
+
// Auto-detach a direct-dispatch mesh assignment once the dispatched
|
|
1671
|
+
// task reaches a terminal state. Leaving meshNodeFor pinned would
|
|
1672
|
+
// route this session's next unrelated turn (a dashboard chat) into
|
|
1673
|
+
// the coordinator as if it were the completion of another task.
|
|
1674
|
+
// We schedule after the emit so the originating coordinator still
|
|
1675
|
+
// observes the completion event with its routing marker intact.
|
|
1676
|
+
if (TERMINAL_MESH_EVENTS.has(event.event) && this.settings.meshActiveTaskId) {
|
|
1677
|
+
try { this.detachMeshAssignment(); } catch { /* best-effort */ }
|
|
1007
1678
|
}
|
|
1008
|
-
this.events.push(enrichedEvent);
|
|
1009
1679
|
}
|
|
1010
1680
|
|
|
1011
1681
|
private flushEvents(): ProviderEvent[] {
|
|
@@ -1258,12 +1928,28 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1258
1928
|
index,
|
|
1259
1929
|
source: 'parsed',
|
|
1260
1930
|
}));
|
|
1931
|
+
const getRole = (message: ChatMessage): string => typeof message.role === 'string'
|
|
1932
|
+
? message.role.trim().toLowerCase()
|
|
1933
|
+
: '';
|
|
1261
1934
|
const runtimeEntries: MergeEntry[] = this.runtimeMessages.map((entry, index) => ({
|
|
1262
1935
|
message: entry.message,
|
|
1263
1936
|
index: parsedMessages.length + index,
|
|
1264
|
-
source: 'runtime',
|
|
1937
|
+
source: 'runtime' as const,
|
|
1265
1938
|
runtimeKey: entry.key,
|
|
1266
|
-
}))
|
|
1939
|
+
})).filter((entry) => {
|
|
1940
|
+
const meta = entry.message.meta && typeof entry.message.meta === 'object' && !Array.isArray(entry.message.meta)
|
|
1941
|
+
? entry.message.meta as Record<string, unknown>
|
|
1942
|
+
: {};
|
|
1943
|
+
if (meta.runtimeInputAck !== true) return true;
|
|
1944
|
+
const runtimeText = flattenContent(entry.message.content).replace(/\s+/g, ' ').trim();
|
|
1945
|
+
if (!runtimeText) return false;
|
|
1946
|
+
return !parsedEntries.some((parsedEntry) => {
|
|
1947
|
+
const parsedRole = getRole(parsedEntry.message);
|
|
1948
|
+
if (parsedRole !== 'user' && parsedRole !== 'human') return false;
|
|
1949
|
+
const parsedText = flattenContent(parsedEntry.message.content).replace(/\s+/g, ' ').trim();
|
|
1950
|
+
return parsedText === runtimeText;
|
|
1951
|
+
});
|
|
1952
|
+
});
|
|
1267
1953
|
const getTime = (message: ChatMessage): number => {
|
|
1268
1954
|
const value = typeof message.receivedAt === 'number'
|
|
1269
1955
|
? message.receivedAt
|
|
@@ -1273,9 +1959,6 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1273
1959
|
return Number.isFinite(value) && value > 0 ? value : 0;
|
|
1274
1960
|
};
|
|
1275
1961
|
|
|
1276
|
-
const getRole = (message: ChatMessage): string => typeof message.role === 'string'
|
|
1277
|
-
? message.role.trim().toLowerCase()
|
|
1278
|
-
: '';
|
|
1279
1962
|
const isRuntimeOverlay = (entry: MergeEntry): boolean => {
|
|
1280
1963
|
if (entry.source !== 'runtime') return false;
|
|
1281
1964
|
const key = typeof entry.runtimeKey === 'string' ? entry.runtimeKey.trim().toLowerCase() : '';
|
|
@@ -1341,7 +2024,9 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1341
2024
|
this.providerSessionId = nextSessionId;
|
|
1342
2025
|
this.historyWriter.promoteHistorySession(this.type, previousHistorySessionId, nextSessionId);
|
|
1343
2026
|
this.historyWriter.writeSessionStart(this.type, nextSessionId, this.workingDir, this.instanceId);
|
|
1344
|
-
this.
|
|
2027
|
+
if (this.shouldHydrateExistingProviderHistory()) {
|
|
2028
|
+
this.restorePersistedHistoryFromCurrentSession();
|
|
2029
|
+
}
|
|
1345
2030
|
this.adapter.updateRuntimeMeta({ providerSessionId: nextSessionId });
|
|
1346
2031
|
this.onProviderSessionResolved?.({
|
|
1347
2032
|
instanceId: this.instanceId,
|
|
@@ -1354,9 +2039,27 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1354
2039
|
LOG.info('CLI', `[${this.type}] discovered provider session id: ${nextSessionId}`);
|
|
1355
2040
|
}
|
|
1356
2041
|
|
|
2042
|
+
private shouldHydrateExistingProviderHistory(): boolean {
|
|
2043
|
+
return this.launchMode === 'resume' || this.launchMode === 'manual';
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
private shouldSuppressFreshLaunchStartupReplay(parsedMessages: unknown[], parsedStatus: any, adapterStatus: any, parsedProviderSessionId = ''): boolean {
|
|
2047
|
+
if (this.launchMode !== 'new') return false;
|
|
2048
|
+
if (this.providerSessionId) return false;
|
|
2049
|
+
if (!Array.isArray(parsedMessages) || parsedMessages.length === 0) return false;
|
|
2050
|
+
if (!isIdleStatus(adapterStatus?.status) || !isIdleStatus(parsedStatus?.status)) return false;
|
|
2051
|
+
if (parsedProviderSessionId) return true;
|
|
2052
|
+
|
|
2053
|
+
const newestMessageAt = parsedMessages.reduce<number>((newest, message) => Math.max(newest, getMessageTime(message)), 0);
|
|
2054
|
+
|
|
2055
|
+
// Untimestamped idle parser output during a fresh launch is usually the
|
|
2056
|
+
// provider's last workspace transcript before a new turn exists.
|
|
2057
|
+
return newestMessageAt === 0;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
1357
2060
|
private syncCanonicalSavedHistoryIfNeeded(): boolean {
|
|
1358
2061
|
if (!this.providerSessionId) return false;
|
|
1359
|
-
const canonicalHistory = this.provider.
|
|
2062
|
+
const canonicalHistory = this.provider.nativeHistory;
|
|
1360
2063
|
if (!canonicalHistory) return false;
|
|
1361
2064
|
|
|
1362
2065
|
if (isNativeSourceCanonicalHistory(canonicalHistory)) {
|
|
@@ -1418,9 +2121,9 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1418
2121
|
private restorePersistedHistoryFromCurrentSession(): void {
|
|
1419
2122
|
if (!this.providerSessionId) return;
|
|
1420
2123
|
this.syncCanonicalSavedHistoryIfNeeded();
|
|
1421
|
-
const restoredHistory = isNativeSourceCanonicalHistory(this.provider.
|
|
2124
|
+
const restoredHistory = isNativeSourceCanonicalHistory(this.provider.nativeHistory)
|
|
1422
2125
|
? readProviderChatHistory(this.type, {
|
|
1423
|
-
canonicalHistory: this.provider.
|
|
2126
|
+
canonicalHistory: this.provider.nativeHistory,
|
|
1424
2127
|
historySessionId: this.providerSessionId,
|
|
1425
2128
|
workspace: this.workingDir,
|
|
1426
2129
|
offset: 0,
|