@adhdev/daemon-core 0.9.82-rc.18 → 0.9.82-rc.181
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/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/handler.d.ts +110 -0
- package/dist/commands/mesh-coordinator.d.ts +72 -1
- package/dist/commands/router.d.ts +22 -0
- package/dist/config/chat-history.d.ts +8 -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 +5 -1
- package/dist/index.d.ts +35 -8
- package/dist/index.js +20754 -5387
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +20567 -5270
- 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 +72 -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 +97 -5
- 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 +54 -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 +35 -3
- package/dist/providers/contracts.d.ts +98 -5
- 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 +25 -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 +169 -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 +56 -0
- package/dist/providers/spec/cli-adapter.d.ts +97 -0
- package/dist/providers/spec/driver.d.ts +159 -0
- package/dist/providers/spec/evaluator.d.ts +47 -0
- package/dist/providers/spec/loader.d.ts +14 -0
- package/dist/providers/spec/native-history-executor.d.ts +39 -0
- package/dist/providers/spec/route.d.ts +4 -0
- package/dist/providers/spec/schema.gen.d.ts +511 -0
- package/dist/providers/spec/types.d.ts +212 -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 +102 -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 +17 -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 +14 -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 +1597 -60
- package/src/commands/cli-manager.ts +230 -3
- package/src/commands/handler.ts +809 -2
- package/src/commands/mesh-coordinator.ts +231 -127
- package/src/commands/router.ts +3495 -502
- package/src/config/chat-history.ts +91 -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 +17 -5
- package/src/git/git-worktree.ts +8 -1
- package/src/index.ts +113 -7
- 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 +486 -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 +963 -67
- 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 +263 -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 +651 -57
- package/src/providers/contracts.ts +105 -5
- 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 +231 -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 +585 -50
- package/src/providers/provider-schema.ts +31 -13
- 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 +458 -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 +210 -0
- package/src/providers/sdk/v1/validators/index.ts +19 -0
- package/src/providers/sdk/v1/validators/manifest.ts +110 -0
- package/src/providers/sdk/v1/validators/taint.ts +309 -0
- package/src/providers/spec/adapter.ts +168 -0
- package/src/providers/spec/cli-adapter.ts +507 -0
- package/src/providers/spec/driver.ts +540 -0
- package/src/providers/spec/evaluator.ts +298 -0
- package/src/providers/spec/loader.ts +130 -0
- package/src/providers/spec/native-history-executor.ts +706 -0
- package/src/providers/spec/route.ts +51 -0
- package/src/providers/spec/schema.gen.ts +511 -0
- package/src/providers/spec/schema.json +211 -0
- package/src/providers/spec/types.ts +231 -0
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +396 -0
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +112 -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,59 @@ 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
|
+
};
|
|
68
|
+
|
|
69
|
+
type CompletionFinalAssistantEvidence = {
|
|
70
|
+
present: boolean;
|
|
71
|
+
messages: unknown[];
|
|
72
|
+
source: 'parsed' | 'external-native' | 'unavailable';
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
type ExternalTranscriptProbe = {
|
|
76
|
+
readAt: number;
|
|
77
|
+
msgCount: number;
|
|
78
|
+
lastRole: string | null;
|
|
79
|
+
lastKind: string | null;
|
|
80
|
+
contentLen: number;
|
|
81
|
+
sourcePath: string | null;
|
|
82
|
+
sourceMtimeMs: number | null;
|
|
83
|
+
mtimeAgeMs: number | null;
|
|
44
84
|
};
|
|
45
85
|
|
|
46
86
|
const COMPLETED_FINALIZATION_RETRY_MS = 1000;
|
|
47
87
|
const COMPLETED_FINALIZATION_MAX_WAIT_MS = 30_000;
|
|
48
88
|
|
|
89
|
+
/** Events that signal a dispatched mesh task has reached a terminal state.
|
|
90
|
+
* Detach the mesh assignment after emitting one of these so the worker's
|
|
91
|
+
* next unrelated turn doesn't impersonate another completion. */
|
|
92
|
+
const TERMINAL_MESH_EVENTS = new Set([
|
|
93
|
+
'agent:generating_completed',
|
|
94
|
+
'agent:stopped',
|
|
95
|
+
'agent:ready',
|
|
96
|
+
]);
|
|
97
|
+
|
|
49
98
|
const IMAGE_MIME_EXTENSIONS: Record<string, string> = {
|
|
50
99
|
'image/png': '.png',
|
|
51
100
|
'image/jpeg': '.jpg',
|
|
@@ -309,9 +358,10 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
309
358
|
private monitor: StatusMonitor;
|
|
310
359
|
private generatingDebounceTimer: NodeJS.Timeout | null = null;
|
|
311
360
|
private generatingDebouncePending: { chatTitle: string; timestamp: number } | null = null;
|
|
312
|
-
private
|
|
361
|
+
private lastApprovalEventFingerprint = '';
|
|
313
362
|
private autoApproveBusy = false;
|
|
314
363
|
private autoApproveBusyTimer: NodeJS.Timeout | null = null;
|
|
364
|
+
private lastAutoApprovalSignature = '';
|
|
315
365
|
private controlValues: Record<string, string | number | boolean> = {};
|
|
316
366
|
private summaryMetadata: unknown = undefined;
|
|
317
367
|
private appliedEffectKeys = new Set<string>();
|
|
@@ -330,6 +380,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
330
380
|
private suppressIdleHistoryReplay = false;
|
|
331
381
|
private errorMessage: string | undefined = undefined;
|
|
332
382
|
private errorReason: ProviderErrorReason | undefined = undefined;
|
|
383
|
+
private activeInteractivePrompt: InteractivePrompt | null = null;
|
|
333
384
|
|
|
334
385
|
private presentationMode: 'terminal' | 'chat';
|
|
335
386
|
private providerSessionId?: string;
|
|
@@ -370,7 +421,10 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
370
421
|
this.providerSessionId = options?.providerSessionId;
|
|
371
422
|
this.launchMode = options?.launchMode || 'new';
|
|
372
423
|
this.onProviderSessionResolved = options?.onProviderSessionResolved;
|
|
373
|
-
this.adapter =
|
|
424
|
+
this.adapter = createCliAdapter(provider as CliProviderModule, workingDir, cliArgs, options?.extraEnv || {}, transportFactory) as ProviderCliAdapter;
|
|
425
|
+
if (this.providerSessionId) {
|
|
426
|
+
this.adapter.updateRuntimeMeta({ providerSessionId: this.providerSessionId });
|
|
427
|
+
}
|
|
374
428
|
this.monitor = new StatusMonitor();
|
|
375
429
|
this.historyWriter = new ChatHistoryWriter();
|
|
376
430
|
}
|
|
@@ -412,7 +466,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
412
466
|
await this.adapter.spawn();
|
|
413
467
|
await this.enforceFreshSessionLaunchIfNeeded();
|
|
414
468
|
this.maybeAppendRuntimeRecoveryMessage(this.adapter.getRuntimeMetadata());
|
|
415
|
-
if (this.providerSessionId) {
|
|
469
|
+
if (this.providerSessionId && this.shouldHydrateExistingProviderHistory()) {
|
|
416
470
|
this.restorePersistedHistoryFromCurrentSession();
|
|
417
471
|
}
|
|
418
472
|
if (this.providerSessionId && this.launchMode === 'resume') {
|
|
@@ -486,14 +540,38 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
486
540
|
}
|
|
487
541
|
|
|
488
542
|
getState(): ProviderState {
|
|
543
|
+
// TODO(phase5-sandbox): JS override scripts (detectStatus, parseApproval,
|
|
544
|
+
// parseSession) are currently invoked by CliScriptRunner.invoke() via direct
|
|
545
|
+
// function calls — the scripts run in the daemon process with full Node.js
|
|
546
|
+
// access and no resource limits.
|
|
547
|
+
//
|
|
548
|
+
// When Phase 5 lands, CliScriptRunner should route these calls through a
|
|
549
|
+
// SandboxedScriptRunner (see providers/sdk/v1/sandbox/script-runner.ts) so
|
|
550
|
+
// that each call gets a fresh isolated-vm context with a 50 ms CPU limit and
|
|
551
|
+
// a 32 MB memory cap. The execution path to change is:
|
|
552
|
+
// CliScriptRunner.invoke() → SandboxedScriptRunner.run(scriptSource, context)
|
|
553
|
+
//
|
|
554
|
+
// This getState() call-site is NOT where the change goes — the wiring belongs
|
|
555
|
+
// in cli-script-runner.ts (CliScriptRunner.detectStatus / parseApproval /
|
|
556
|
+
// parseSession), with provider-loader.ts updated to store script source strings
|
|
557
|
+
// alongside the loaded function references for extended-legacy providers.
|
|
489
558
|
const adapterStatus = this.adapter.getStatus();
|
|
559
|
+
if (Object.prototype.hasOwnProperty.call(adapterStatus, 'activeInteractivePrompt')) {
|
|
560
|
+
this.activeInteractivePrompt = adapterStatus.activeInteractivePrompt ?? null;
|
|
561
|
+
}
|
|
490
562
|
let parsedStatus: any = null;
|
|
491
563
|
let parseErrorMessage: string | undefined;
|
|
492
564
|
if (typeof this.adapter.getScriptParsedStatus === 'function') {
|
|
493
565
|
try {
|
|
494
566
|
parsedStatus = this.adapter.getScriptParsedStatus() || null;
|
|
495
|
-
|
|
496
|
-
|
|
567
|
+
const parsedErrorMessage = typeof parsedStatus?.errorMessage === 'string' && parsedStatus.errorMessage.trim()
|
|
568
|
+
? parsedStatus.errorMessage.trim()
|
|
569
|
+
: undefined;
|
|
570
|
+
const parsedErrorReason = typeof parsedStatus?.errorReason === 'string' && parsedStatus.errorReason.trim()
|
|
571
|
+
? parsedStatus.errorReason.trim() as ProviderErrorReason
|
|
572
|
+
: undefined;
|
|
573
|
+
this.errorMessage = parsedErrorMessage;
|
|
574
|
+
this.errorReason = parsedErrorReason;
|
|
497
575
|
} catch (error: any) {
|
|
498
576
|
parseErrorMessage = error?.message || String(error);
|
|
499
577
|
this.errorMessage = parseErrorMessage;
|
|
@@ -503,22 +581,39 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
503
581
|
this.errorMessage = undefined;
|
|
504
582
|
this.errorReason = undefined;
|
|
505
583
|
}
|
|
584
|
+
const adapterProviderSessionId = normalizeProviderSessionId(
|
|
585
|
+
this.provider,
|
|
586
|
+
typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
|
|
587
|
+
);
|
|
506
588
|
const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, Date.now());
|
|
507
|
-
const visibleStatus = parseErrorMessage
|
|
589
|
+
const visibleStatus = parseErrorMessage || parsedStatus?.status === 'error'
|
|
508
590
|
? 'error'
|
|
509
591
|
: (autoApproveActive ? 'generating' : adapterStatus.status);
|
|
592
|
+
const runtime = this.adapter.getRuntimeMetadata();
|
|
593
|
+
this.maybeAppendRuntimeRecoveryMessage(runtime);
|
|
594
|
+
let parsedMessages = Array.isArray(parsedStatus?.messages)
|
|
595
|
+
? parsedStatus.messages
|
|
596
|
+
: [];
|
|
510
597
|
const parsedProviderSessionId = normalizeProviderSessionId(
|
|
511
598
|
this.provider,
|
|
512
599
|
typeof parsedStatus?.providerSessionId === 'string' ? parsedStatus.providerSessionId : '',
|
|
513
600
|
);
|
|
514
|
-
|
|
601
|
+
const suppressFreshLaunchStartupReplay = this.shouldSuppressFreshLaunchStartupReplay(
|
|
602
|
+
parsedMessages,
|
|
603
|
+
parsedStatus,
|
|
604
|
+
adapterStatus,
|
|
605
|
+
parsedProviderSessionId,
|
|
606
|
+
);
|
|
607
|
+
if (adapterProviderSessionId && !suppressFreshLaunchStartupReplay) {
|
|
608
|
+
this.promoteProviderSessionId(adapterProviderSessionId);
|
|
609
|
+
}
|
|
610
|
+
if (parsedProviderSessionId && !suppressFreshLaunchStartupReplay) {
|
|
515
611
|
this.promoteProviderSessionId(parsedProviderSessionId);
|
|
516
612
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
: [];
|
|
613
|
+
if (suppressFreshLaunchStartupReplay) {
|
|
614
|
+
parsedMessages = [];
|
|
615
|
+
}
|
|
616
|
+
const activeChatId = this.providerSessionId || runtime?.runtimeId || this.instanceId;
|
|
522
617
|
const historyMessageCount = Number.isFinite(parsedStatus?.historyMessageCount)
|
|
523
618
|
? Math.max(0, Number(parsedStatus.historyMessageCount))
|
|
524
619
|
: null;
|
|
@@ -528,7 +623,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
528
623
|
: [];
|
|
529
624
|
}
|
|
530
625
|
const mergedMessages = this.mergeConversationMessages(parsedMessages);
|
|
531
|
-
const canonicalBackedHistory = this.
|
|
626
|
+
const canonicalBackedHistory = this.shouldHydrateExistingProviderHistory()
|
|
627
|
+
? this.syncCanonicalSavedHistoryIfNeeded()
|
|
628
|
+
: false;
|
|
629
|
+
const statusMessages = canonicalBackedHistory && this.lastPersistedHistoryMessages.length > 0
|
|
630
|
+
? this.lastPersistedHistoryMessages.map((message) => ({
|
|
631
|
+
role: message.role,
|
|
632
|
+
content: message.content,
|
|
633
|
+
kind: message.kind,
|
|
634
|
+
senderName: message.senderName,
|
|
635
|
+
receivedAt: message.receivedAt,
|
|
636
|
+
}))
|
|
637
|
+
: mergedMessages;
|
|
532
638
|
|
|
533
639
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
534
640
|
const parsedChatStatus = typeof parsedStatus?.status === 'string' && parsedStatus.status.trim()
|
|
@@ -572,7 +678,12 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
572
678
|
}
|
|
573
679
|
}
|
|
574
680
|
|
|
575
|
-
this.applyProviderResponse(
|
|
681
|
+
this.applyProviderResponse(
|
|
682
|
+
suppressFreshLaunchStartupReplay && parsedStatus && typeof parsedStatus === 'object'
|
|
683
|
+
? { ...parsedStatus, providerSessionId: undefined }
|
|
684
|
+
: parsedStatus,
|
|
685
|
+
{ phase: 'immediate' },
|
|
686
|
+
);
|
|
576
687
|
const surface = resolveProviderStateSurface({
|
|
577
688
|
summaryMetadata: this.summaryMetadata as any,
|
|
578
689
|
controlValues: this.controlValues,
|
|
@@ -592,13 +703,15 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
592
703
|
status: visibleStatus,
|
|
593
704
|
mode: this.presentationMode,
|
|
594
705
|
activeChat: {
|
|
595
|
-
id:
|
|
706
|
+
id: activeChatId,
|
|
596
707
|
title: parsedStatus?.title || dirName,
|
|
597
708
|
status: activeChatStatus,
|
|
598
|
-
messages:
|
|
709
|
+
messages: statusMessages,
|
|
599
710
|
activeModal: autoApproveActive ? null : (parsedStatus?.activeModal ?? adapterStatus.activeModal),
|
|
711
|
+
activeInteractivePrompt: this.activeInteractivePrompt,
|
|
600
712
|
inputContent: '',
|
|
601
713
|
},
|
|
714
|
+
activeInteractivePrompt: this.activeInteractivePrompt,
|
|
602
715
|
workspace: this.workingDir,
|
|
603
716
|
instanceId: this.instanceId,
|
|
604
717
|
providerSessionId: this.providerSessionId,
|
|
@@ -651,13 +764,17 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
651
764
|
};
|
|
652
765
|
}
|
|
653
766
|
|
|
654
|
-
getSessionModalState(): SessionModalState {
|
|
655
|
-
const adapterStatus = this.adapter.getStatus({ allowParse:
|
|
767
|
+
getSessionModalState(sessionId?: string): SessionModalState {
|
|
768
|
+
const adapterStatus = this.adapter.getStatus({ allowParse: true });
|
|
656
769
|
const autoApproveActive = adapterStatus.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
657
770
|
const visibleStatus = autoApproveActive ? 'generating' : adapterStatus.status;
|
|
658
771
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
659
772
|
return {
|
|
660
|
-
|
|
773
|
+
// Honor the caller-supplied sessionId — InstanceMgr rejects the
|
|
774
|
+
// projection when projected.id !== requested sessionId, and
|
|
775
|
+
// this.instanceId is the manager's internal key, not the public
|
|
776
|
+
// sessionId the dashboard subscribes by.
|
|
777
|
+
id: sessionId ?? this.instanceId,
|
|
661
778
|
status: visibleStatus,
|
|
662
779
|
title: dirName,
|
|
663
780
|
activeModal: autoApproveActive ? null : adapterStatus.activeModal,
|
|
@@ -674,6 +791,39 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
674
791
|
});
|
|
675
792
|
}
|
|
676
793
|
|
|
794
|
+
/**
|
|
795
|
+
* Stamp a direct-dispatch mesh assignment on this instance.
|
|
796
|
+
* setupMeshEventForwarding reads settings.meshNodeFor + meshActiveTaskId to
|
|
797
|
+
* route generating_completed back to the originating coordinator. Without
|
|
798
|
+
* this stamp, mesh_send_task --direct targets a plain CLI session whose
|
|
799
|
+
* completion events silently drop because the forwarder has nothing to
|
|
800
|
+
* match against.
|
|
801
|
+
*/
|
|
802
|
+
attachMeshAssignment(assignment: { meshId: string; nodeId?: string; taskId?: string }): void {
|
|
803
|
+
if (!assignment?.meshId) return;
|
|
804
|
+
this.settings = {
|
|
805
|
+
...this.settings,
|
|
806
|
+
meshNodeFor: assignment.meshId,
|
|
807
|
+
...(assignment.nodeId ? { meshNodeId: assignment.nodeId } : {}),
|
|
808
|
+
...(assignment.taskId ? { meshActiveTaskId: assignment.taskId } : {}),
|
|
809
|
+
};
|
|
810
|
+
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* Clear a previously-attached mesh assignment after the task reaches a
|
|
815
|
+
* terminal state. Leaving meshNodeFor pinned would route this session's
|
|
816
|
+
* subsequent unrelated turns (e.g. ad-hoc dashboard chats) to the
|
|
817
|
+
* coordinator as if they were task completions.
|
|
818
|
+
*/
|
|
819
|
+
detachMeshAssignment(): void {
|
|
820
|
+
if (!this.settings.meshNodeFor && !this.settings.meshActiveTaskId && !this.settings.meshNodeId) return;
|
|
821
|
+
const { meshNodeFor, meshNodeId, meshActiveTaskId, ...rest } = this.settings;
|
|
822
|
+
void meshNodeFor; void meshNodeId; void meshActiveTaskId;
|
|
823
|
+
this.settings = rest;
|
|
824
|
+
this.adapter.updateRuntimeSettings?.(this.settings);
|
|
825
|
+
}
|
|
826
|
+
|
|
677
827
|
onEvent(event: string, data?: any): void {
|
|
678
828
|
if (event === 'send_message') {
|
|
679
829
|
const input = normalizeInputEnvelope(data);
|
|
@@ -690,11 +840,65 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
690
840
|
void this.adapter.resolveAction(data).catch((e: any) => {
|
|
691
841
|
LOG.warn('CLI', `[${this.type}] resolve_action failed: ${e?.message || e}`);
|
|
692
842
|
});
|
|
843
|
+
} else if (event === 'interactive_prompt' && data) {
|
|
844
|
+
const prompt = normalizeInteractivePrompt(data);
|
|
845
|
+
if (prompt) {
|
|
846
|
+
this.activeInteractivePrompt = prompt;
|
|
847
|
+
this.events.push({
|
|
848
|
+
event: 'interactive_prompt',
|
|
849
|
+
timestamp: Date.now(),
|
|
850
|
+
promptId: prompt.promptId,
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
} else if (event === 'interactive_prompt_response' && data) {
|
|
854
|
+
try {
|
|
855
|
+
const response = normalizeInteractivePromptResponse(data);
|
|
856
|
+
if (this.activeInteractivePrompt?.promptId === response.promptId) {
|
|
857
|
+
this.activeInteractivePrompt = null;
|
|
858
|
+
}
|
|
859
|
+
if (typeof this.adapter.setInteractivePromptResponse !== 'function') {
|
|
860
|
+
LOG.warn('CLI', `[${this.type}] interactive_prompt_response ignored: adapter does not support interactive prompts`);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
void this.adapter.setInteractivePromptResponse(response).catch((e: any) => {
|
|
864
|
+
LOG.warn('CLI', `[${this.type}] interactive_prompt_response failed: ${e?.message || e}`);
|
|
865
|
+
});
|
|
866
|
+
} catch (e: any) {
|
|
867
|
+
LOG.warn('CLI', `[${this.type}] invalid interactive_prompt_response: ${e?.message || e}`);
|
|
868
|
+
}
|
|
693
869
|
} else if (event === 'provider_state_patch' && data && typeof data === 'object') {
|
|
694
870
|
this.applyProviderResponse(data, { phase: 'immediate' });
|
|
695
871
|
}
|
|
696
872
|
}
|
|
697
873
|
|
|
874
|
+
recordAcknowledgedUserInput(input: InputEnvelope | string): void {
|
|
875
|
+
const content = typeof input === 'string'
|
|
876
|
+
? input.trim()
|
|
877
|
+
: buildCliStructuredInputPrompt(input).trim();
|
|
878
|
+
if (!content) return;
|
|
879
|
+
|
|
880
|
+
const receivedAt = Date.now();
|
|
881
|
+
const dedupKey = `user_input_ack:${crypto
|
|
882
|
+
.createHash('sha256')
|
|
883
|
+
.update(`${this.instanceId}:${content}:${receivedAt}`)
|
|
884
|
+
.digest('hex')
|
|
885
|
+
.slice(0, 24)}`;
|
|
886
|
+
this.appendRuntimeMessage(buildChatMessage({
|
|
887
|
+
role: 'user',
|
|
888
|
+
senderName: 'User',
|
|
889
|
+
kind: 'standard',
|
|
890
|
+
content,
|
|
891
|
+
receivedAt,
|
|
892
|
+
timestamp: receivedAt,
|
|
893
|
+
source: 'runtime_input_ack',
|
|
894
|
+
meta: {
|
|
895
|
+
runtimeInputAck: true,
|
|
896
|
+
provider: this.type,
|
|
897
|
+
workspace: this.workingDir,
|
|
898
|
+
},
|
|
899
|
+
} as ChatMessage), dedupKey);
|
|
900
|
+
}
|
|
901
|
+
|
|
698
902
|
dispose(): void {
|
|
699
903
|
this.adapter.shutdown();
|
|
700
904
|
this.monitor.reset();
|
|
@@ -706,6 +910,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
706
910
|
|
|
707
911
|
private completedDebounceTimer: NodeJS.Timeout | null = null;
|
|
708
912
|
private completedDebouncePending: CompletedDebouncePending | null = null;
|
|
913
|
+
private lastExternalCompletionProbe: ExternalTranscriptProbe | null = null;
|
|
709
914
|
|
|
710
915
|
private async enforceFreshSessionLaunchIfNeeded(): Promise<void> {
|
|
711
916
|
const scriptName = getForcedNewSessionScriptName(this.provider, this.launchMode);
|
|
@@ -740,7 +945,157 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
740
945
|
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
741
946
|
const role = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : '';
|
|
742
947
|
const content = lastVisible ? flattenContent(lastVisible.content).trim() : '';
|
|
743
|
-
|
|
948
|
+
if (role !== 'assistant' || !content) return false;
|
|
949
|
+
// Guard: if the last assistant message looks like an active approval/input prompt,
|
|
950
|
+
// it is not a real completion — the session is still awaiting user input.
|
|
951
|
+
if (looksLikeActiveApprovalPromptText(content)) return false;
|
|
952
|
+
return true;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
private buildExternalTranscriptProbe(messages: unknown[], sourcePath?: string, sourceMtimeMs?: number): ExternalTranscriptProbe {
|
|
956
|
+
const visibleMessages = messages.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
957
|
+
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
958
|
+
const readAt = Date.now();
|
|
959
|
+
const mtimeMs = Number(sourceMtimeMs) || 0;
|
|
960
|
+
return {
|
|
961
|
+
readAt,
|
|
962
|
+
msgCount: messages.length,
|
|
963
|
+
lastRole: typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null,
|
|
964
|
+
lastKind: typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null,
|
|
965
|
+
contentLen: lastVisible ? flattenContent(lastVisible.content).trim().length : 0,
|
|
966
|
+
sourcePath: typeof sourcePath === 'string' && sourcePath ? sourcePath : null,
|
|
967
|
+
sourceMtimeMs: mtimeMs || null,
|
|
968
|
+
mtimeAgeMs: mtimeMs ? Math.max(0, readAt - mtimeMs) : null,
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
private recordPendingTranscriptProbe(pending: CompletedDebouncePending): ExternalTranscriptProbe | null {
|
|
973
|
+
const probe = this.lastExternalCompletionProbe;
|
|
974
|
+
if (!probe) return null;
|
|
975
|
+
const history = pending.transcriptProbeHistory || [];
|
|
976
|
+
const last = history[history.length - 1];
|
|
977
|
+
if (!last || last.readAt !== probe.readAt || last.msgCount !== probe.msgCount || last.lastRole !== probe.lastRole || last.contentLen !== probe.contentLen) {
|
|
978
|
+
history.push(probe);
|
|
979
|
+
pending.transcriptProbeHistory = history.slice(-5);
|
|
980
|
+
}
|
|
981
|
+
return probe;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
private readExternalCompletionMessages(): unknown[] | null {
|
|
985
|
+
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
986
|
+
if (!adapterOwnsMessagesElsewhere) return null;
|
|
987
|
+
if (!this.providerSessionId) return null;
|
|
988
|
+
if (!isNativeSourceCanonicalHistory(this.provider.nativeHistory)) return null;
|
|
989
|
+
|
|
990
|
+
if (this.lastExternalCompletionProbe?.sourcePath) {
|
|
991
|
+
try { fs.statSync(this.lastExternalCompletionProbe.sourcePath); } catch { /* best-effort metadata refresh */ }
|
|
992
|
+
}
|
|
993
|
+
const restoredHistory = readProviderChatHistory(this.type, {
|
|
994
|
+
canonicalHistory: this.provider.nativeHistory,
|
|
995
|
+
historySessionId: this.providerSessionId,
|
|
996
|
+
workspace: this.workingDir,
|
|
997
|
+
offset: 0,
|
|
998
|
+
limit: Number.MAX_SAFE_INTEGER,
|
|
999
|
+
historyBehavior: this.provider.historyBehavior,
|
|
1000
|
+
scripts: this.provider.scripts as any,
|
|
1001
|
+
sessionStartedAtMs: this.startedAt,
|
|
1002
|
+
forceRefresh: true,
|
|
1003
|
+
});
|
|
1004
|
+
if (restoredHistory.source !== 'provider-native') {
|
|
1005
|
+
this.lastExternalCompletionProbe = null;
|
|
1006
|
+
return null;
|
|
1007
|
+
}
|
|
1008
|
+
this.lastExternalCompletionProbe = this.buildExternalTranscriptProbe(
|
|
1009
|
+
restoredHistory.messages,
|
|
1010
|
+
restoredHistory.sourcePath,
|
|
1011
|
+
restoredHistory.sourceMtimeMs,
|
|
1012
|
+
);
|
|
1013
|
+
return restoredHistory.messages;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
private completionFinalAssistantEvidence(parsedMessages: unknown): CompletionFinalAssistantEvidence {
|
|
1017
|
+
if (this.completionHasFinalAssistantMessage(parsedMessages)) {
|
|
1018
|
+
return {
|
|
1019
|
+
present: true,
|
|
1020
|
+
messages: Array.isArray(parsedMessages) ? parsedMessages : [],
|
|
1021
|
+
source: 'parsed',
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const externalMessages = this.readExternalCompletionMessages();
|
|
1026
|
+
if (externalMessages) {
|
|
1027
|
+
return {
|
|
1028
|
+
present: this.completionHasFinalAssistantMessage(externalMessages),
|
|
1029
|
+
messages: externalMessages,
|
|
1030
|
+
source: 'external-native',
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
return {
|
|
1035
|
+
present: false,
|
|
1036
|
+
messages: Array.isArray(parsedMessages) ? parsedMessages : [],
|
|
1037
|
+
source: 'unavailable',
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
private completionFinalSummary(parsedMessages: unknown): string | undefined {
|
|
1042
|
+
const evidence = this.completionFinalAssistantEvidence(parsedMessages);
|
|
1043
|
+
return extractFinalSummaryFromMessages(evidence.messages as any);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
private buildCompletedFinalizationDiagnostic(args: {
|
|
1047
|
+
blockReason: string;
|
|
1048
|
+
latestStatus?: any;
|
|
1049
|
+
latestVisibleStatus: string;
|
|
1050
|
+
waitedMs: number;
|
|
1051
|
+
pending: CompletedDebouncePending;
|
|
1052
|
+
emittedAfterFinalizationTimeout: boolean;
|
|
1053
|
+
}): Record<string, unknown> {
|
|
1054
|
+
let parsed: any = null;
|
|
1055
|
+
let parseError: string | undefined;
|
|
1056
|
+
try {
|
|
1057
|
+
parsed = this.adapter.getScriptParsedStatus();
|
|
1058
|
+
} catch (error: any) {
|
|
1059
|
+
parseError = error?.message || String(error);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
const evidence = this.completionFinalAssistantEvidence(parsed?.messages);
|
|
1063
|
+
if (evidence.source === 'external-native') {
|
|
1064
|
+
this.recordPendingTranscriptProbe(args.pending);
|
|
1065
|
+
}
|
|
1066
|
+
const visibleMessages = (Array.isArray(evidence.messages) ? evidence.messages : [])
|
|
1067
|
+
.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
1068
|
+
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
1069
|
+
const lastVisibleRole = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null;
|
|
1070
|
+
const lastVisibleKind = typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null;
|
|
1071
|
+
const lastVisibleContentLength = lastVisible ? flattenContent(lastVisible.content).trim().length : 0;
|
|
1072
|
+
|
|
1073
|
+
return {
|
|
1074
|
+
providerType: this.type,
|
|
1075
|
+
sessionId: this.instanceId,
|
|
1076
|
+
providerSessionId: this.providerSessionId || null,
|
|
1077
|
+
workspace: this.workingDir,
|
|
1078
|
+
blockReason: args.blockReason,
|
|
1079
|
+
emittedAfterFinalizationTimeout: args.emittedAfterFinalizationTimeout,
|
|
1080
|
+
waitedMs: args.waitedMs,
|
|
1081
|
+
maxWaitMs: COMPLETED_FINALIZATION_MAX_WAIT_MS,
|
|
1082
|
+
adapterStatus: typeof args.latestStatus?.status === 'string' ? args.latestStatus.status : null,
|
|
1083
|
+
latestVisibleStatus: args.latestVisibleStatus,
|
|
1084
|
+
parsedStatus: typeof parsed?.status === 'string' ? parsed.status : (parseError ? 'parse_error' : 'unknown'),
|
|
1085
|
+
parseError: parseError || undefined,
|
|
1086
|
+
finalAssistantPresent: evidence.present,
|
|
1087
|
+
finalAssistantEvidenceSource: evidence.source,
|
|
1088
|
+
visibleMessageCount: visibleMessages.length,
|
|
1089
|
+
lastVisibleRole,
|
|
1090
|
+
lastVisibleKind,
|
|
1091
|
+
lastVisibleContentLength,
|
|
1092
|
+
pendingStartedAt: this.generatingStartedAt || null,
|
|
1093
|
+
pendingFirstObservedAt: args.pending.firstObservedAt,
|
|
1094
|
+
pendingTimestamp: args.pending.timestamp,
|
|
1095
|
+
pendingDurationSec: args.pending.duration,
|
|
1096
|
+
previousBlockReason: args.pending.loggedBlockReason || null,
|
|
1097
|
+
transcriptProbeHistory: args.pending.transcriptProbeHistory || [],
|
|
1098
|
+
};
|
|
744
1099
|
}
|
|
745
1100
|
|
|
746
1101
|
private hasAdapterPendingResponse(): boolean {
|
|
@@ -765,32 +1120,90 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
765
1120
|
if (!isCliGeneratingLikeStatus(parsedRawStatus)) return false;
|
|
766
1121
|
if (adapterRawStatus !== 'idle') return false;
|
|
767
1122
|
if (hasNonEmptyCliModalButtons(parsedStatus?.activeModal ?? parsedStatus?.modal)) return false;
|
|
768
|
-
|
|
1123
|
+
if (this.hasAdapterPendingResponse()) return false;
|
|
1124
|
+
// Do not suppress when the adapter's raw response buffer is still non-empty.
|
|
1125
|
+
// This catches the case where isWaitingForResponse has already flipped to false
|
|
1126
|
+
// (so getPartialResponse() returns '') but the provider's native parser still
|
|
1127
|
+
// reports generating because it's parsing buffered content. Suppressing the
|
|
1128
|
+
// finalization block here would emit a false completion event while the provider
|
|
1129
|
+
// session is still actively processing its response stream.
|
|
1130
|
+
const adapterAny = this.adapter as any;
|
|
1131
|
+
if (typeof adapterAny?.responseBuffer === 'string' && adapterAny.responseBuffer.trim()) return false;
|
|
1132
|
+
return true;
|
|
769
1133
|
}
|
|
770
1134
|
|
|
771
|
-
private
|
|
772
|
-
if (latestVisibleStatus !== 'idle') return `status:${latestVisibleStatus}
|
|
1135
|
+
private getCompletedFinalizationBlock(latestVisibleStatus: string, pending: CompletedDebouncePending): CompletedFinalizationBlock | null {
|
|
1136
|
+
if (latestVisibleStatus !== 'idle') return { reason: `status:${latestVisibleStatus}`, terminal: true };
|
|
773
1137
|
|
|
774
1138
|
const adapterAny = this.adapter as any;
|
|
775
|
-
|
|
776
|
-
if (
|
|
1139
|
+
const approvalResolvedIdle = pending.previousStatus === 'waiting_approval';
|
|
1140
|
+
if (!approvalResolvedIdle) {
|
|
1141
|
+
if (adapterAny?.isWaitingForResponse === true) return { reason: 'adapter_waiting_for_response', terminal: true };
|
|
1142
|
+
if (adapterAny?.currentTurnScope) return { reason: 'adapter_turn_scope_active', terminal: true };
|
|
1143
|
+
if (this.hasAdapterPendingResponse()) return { reason: 'adapter_pending_response', terminal: true };
|
|
1144
|
+
}
|
|
777
1145
|
|
|
778
1146
|
const partial = typeof this.adapter.getPartialResponse === 'function'
|
|
779
1147
|
? this.adapter.getPartialResponse()
|
|
780
1148
|
: '';
|
|
781
|
-
if (typeof partial === 'string' && partial.trim()) return 'partial_response_pending';
|
|
1149
|
+
if (typeof partial === 'string' && partial.trim()) return { reason: 'partial_response_pending', terminal: true };
|
|
782
1150
|
|
|
783
1151
|
let parsed: any;
|
|
784
1152
|
try {
|
|
785
1153
|
parsed = this.adapter.getScriptParsedStatus();
|
|
786
1154
|
} catch (error: any) {
|
|
787
|
-
return `parse_error:${error?.message || String(error)}
|
|
1155
|
+
return { reason: `parse_error:${error?.message || String(error)}` };
|
|
788
1156
|
}
|
|
789
1157
|
|
|
790
1158
|
const parsedStatus = typeof parsed?.status === 'string' ? parsed.status : 'unknown';
|
|
791
|
-
if (parsedStatus !== 'idle')
|
|
792
|
-
|
|
793
|
-
|
|
1159
|
+
if (parsedStatus !== 'idle') {
|
|
1160
|
+
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
1161
|
+
if (this.shouldSuppressStaleParsedBusyStatus(parsed, adapterStatus)) return null;
|
|
1162
|
+
return { reason: `parsed_status:${parsedStatus}`, terminal: isCliGeneratingLikeStatus(parsedStatus) };
|
|
1163
|
+
}
|
|
1164
|
+
if (parsed?.activeModal || parsed?.modal) return { reason: 'parsed_modal_active', terminal: true };
|
|
1165
|
+
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1166
|
+
const finalAssistantEvidence = this.completionFinalAssistantEvidence(parsed?.messages);
|
|
1167
|
+
if (!finalAssistantEvidence.present) {
|
|
1168
|
+
if (adapterOwnsMessagesElsewhere) {
|
|
1169
|
+
if (finalAssistantEvidence.source === 'external-native') {
|
|
1170
|
+
const probe = this.recordPendingTranscriptProbe(pending);
|
|
1171
|
+
if (probe && !pending.loggedTranscriptProbe) {
|
|
1172
|
+
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`);
|
|
1173
|
+
pending.loggedTranscriptProbe = true;
|
|
1174
|
+
}
|
|
1175
|
+
return { reason: 'missing_final_assistant', terminal: true };
|
|
1176
|
+
}
|
|
1177
|
+
// SpecCliAdapter never populates parsed.messages — chat history flows
|
|
1178
|
+
// through the daemon's native-history pipeline, not the status hook.
|
|
1179
|
+
// If that pipeline is unavailable, keep the old skip behavior for
|
|
1180
|
+
// providers that have not opted into strict final-assistant evidence.
|
|
1181
|
+
if ((this.provider as any).requiresFinalAssistantBeforeIdle === true) {
|
|
1182
|
+
return { reason: 'missing_final_assistant', terminal: true };
|
|
1183
|
+
}
|
|
1184
|
+
} else {
|
|
1185
|
+
return {
|
|
1186
|
+
reason: 'missing_final_assistant',
|
|
1187
|
+
terminal: (this.provider as any).requiresFinalAssistantBeforeIdle === true,
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Guard: if the screen still shows an approval/choice prompt as the last visible text,
|
|
1193
|
+
// the turn is not complete even if the parsed status says idle and there is an assistant
|
|
1194
|
+
// message. This catches the case where waiting_approval→idle transitions occur before
|
|
1195
|
+
// the modal has been resolved (e.g. the PTY rendered the prompt but no button press fired).
|
|
1196
|
+
try {
|
|
1197
|
+
const screenText = typeof (this.adapter as any).getScreenText === 'function'
|
|
1198
|
+
? String((this.adapter as any).getScreenText() || '')
|
|
1199
|
+
: '';
|
|
1200
|
+
if (screenText) {
|
|
1201
|
+
const tailLines = screenText.split(/\r?\n/).slice(-16).join('\n');
|
|
1202
|
+
if (looksLikeActiveApprovalPromptText(tailLines)) {
|
|
1203
|
+
return { reason: 'screen_shows_approval_prompt', terminal: approvalResolvedIdle };
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
} catch { /* defensive: screen text read is best-effort */ }
|
|
794
1207
|
|
|
795
1208
|
return null;
|
|
796
1209
|
}
|
|
@@ -817,10 +1230,11 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
817
1230
|
return;
|
|
818
1231
|
}
|
|
819
1232
|
|
|
820
|
-
const
|
|
821
|
-
if (
|
|
1233
|
+
const block = this.getCompletedFinalizationBlock(latestVisibleStatus, pending);
|
|
1234
|
+
if (block) {
|
|
1235
|
+
const blockReason = block.reason;
|
|
822
1236
|
const waitedMs = Date.now() - pending.firstObservedAt;
|
|
823
|
-
if (waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
|
|
1237
|
+
if (block.terminal || waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
|
|
824
1238
|
if (pending.loggedBlockReason !== blockReason) {
|
|
825
1239
|
LOG.info('CLI', `[${this.type}] waiting to emit completed until transcript finalizes (${blockReason})`);
|
|
826
1240
|
pending.loggedBlockReason = blockReason;
|
|
@@ -828,10 +1242,29 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
828
1242
|
this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
|
|
829
1243
|
return;
|
|
830
1244
|
}
|
|
831
|
-
|
|
1245
|
+
const completionDiagnostic = this.buildCompletedFinalizationDiagnostic({
|
|
1246
|
+
blockReason,
|
|
1247
|
+
latestStatus,
|
|
1248
|
+
latestVisibleStatus,
|
|
1249
|
+
waitedMs,
|
|
1250
|
+
pending,
|
|
1251
|
+
emittedAfterFinalizationTimeout: true,
|
|
1252
|
+
});
|
|
1253
|
+
LOG.warn('CLI', `[${this.type}] emitting completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
|
|
1254
|
+
this.pushEvent({
|
|
1255
|
+
event: 'agent:generating_completed',
|
|
1256
|
+
chatTitle: pending.chatTitle,
|
|
1257
|
+
duration: pending.duration,
|
|
1258
|
+
timestamp: pending.timestamp,
|
|
1259
|
+
finalSummary: blockReason.startsWith('parsed_status:')
|
|
1260
|
+
? ''
|
|
1261
|
+
: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
|
|
1262
|
+
completionDiagnostic,
|
|
1263
|
+
});
|
|
832
1264
|
this.completedDebouncePending = null;
|
|
833
1265
|
this.completedDebounceTimer = null;
|
|
834
1266
|
this.generatingStartedAt = 0;
|
|
1267
|
+
this.lastApprovalEventFingerprint = '';
|
|
835
1268
|
return;
|
|
836
1269
|
}
|
|
837
1270
|
|
|
@@ -841,11 +1274,12 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
841
1274
|
chatTitle: pending.chatTitle,
|
|
842
1275
|
duration: pending.duration,
|
|
843
1276
|
timestamp: pending.timestamp,
|
|
844
|
-
finalSummary:
|
|
1277
|
+
finalSummary: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
|
|
845
1278
|
});
|
|
846
1279
|
this.completedDebouncePending = null;
|
|
847
1280
|
this.completedDebounceTimer = null;
|
|
848
1281
|
this.generatingStartedAt = 0;
|
|
1282
|
+
this.lastApprovalEventFingerprint = '';
|
|
849
1283
|
}
|
|
850
1284
|
|
|
851
1285
|
private maybeAutoApproveStatus(adapterStatus: any, now = Date.now()): boolean {
|
|
@@ -853,15 +1287,46 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
853
1287
|
// Guard re-entry: onStatusChange/getState can observe the same modal multiple
|
|
854
1288
|
// times while the PTY absorbs the approval key. Without this flag, repeated
|
|
855
1289
|
// snapshots would write stray keys into the input once the modal dismisses.
|
|
856
|
-
|
|
1290
|
+
// However, Claude Code can present a second approval immediately after the
|
|
1291
|
+
// first. Resolve a changed modal signature even while the previous write is
|
|
1292
|
+
// still inside the short busy window.
|
|
1293
|
+
if (!autoApproveActive) {
|
|
1294
|
+
this.lastAutoApprovalSignature = '';
|
|
1295
|
+
return autoApproveActive;
|
|
1296
|
+
}
|
|
1297
|
+
const modal = adapterStatus.activeModal;
|
|
1298
|
+
// (fix) Do not auto-approve when no concrete modal/buttons are present.
|
|
1299
|
+
// Claude TUI flaps between paints; without this guard adapterStatus
|
|
1300
|
+
// could report status=waiting_approval with activeModal=null (or with
|
|
1301
|
+
// an empty buttons array briefly) and we'd still call
|
|
1302
|
+
// resolveModal(-1) — which used to type "1" into the prompt
|
|
1303
|
+
// repeatedly. Skip until a real modal is captured.
|
|
1304
|
+
const buttons = Array.isArray(modal?.buttons)
|
|
1305
|
+
? modal.buttons.map((b: any) => String(b || '').trim()).filter(Boolean)
|
|
1306
|
+
: [];
|
|
1307
|
+
if (!modal || buttons.length === 0) {
|
|
1308
|
+
return autoApproveActive;
|
|
1309
|
+
}
|
|
1310
|
+
const { index: buttonIndex, label: buttonLabel } = pickAutoApprovalButton(buttons);
|
|
1311
|
+
if (buttonIndex < 0) {
|
|
1312
|
+
// No concrete button matched — don't pick a random index, just
|
|
1313
|
+
// surface the modal so the user can decide.
|
|
1314
|
+
return autoApproveActive;
|
|
1315
|
+
}
|
|
1316
|
+
const signature = [
|
|
1317
|
+
typeof modal?.message === 'string' ? modal.message.trim() : '',
|
|
1318
|
+
buttons.join('|'),
|
|
1319
|
+
buttonIndex,
|
|
1320
|
+
].join('::');
|
|
1321
|
+
if (!this.autoApproveBusy || signature !== this.lastAutoApprovalSignature) {
|
|
857
1322
|
this.autoApproveBusy = true;
|
|
1323
|
+
this.lastAutoApprovalSignature = signature;
|
|
858
1324
|
if (this.autoApproveBusyTimer) clearTimeout(this.autoApproveBusyTimer);
|
|
859
1325
|
this.autoApproveBusyTimer = setTimeout(() => {
|
|
860
1326
|
this.autoApproveBusy = false;
|
|
861
1327
|
this.autoApproveBusyTimer = null;
|
|
1328
|
+
this.lastAutoApprovalSignature = '';
|
|
862
1329
|
}, 2000);
|
|
863
|
-
const modal = adapterStatus.activeModal;
|
|
864
|
-
const { index: buttonIndex, label: buttonLabel } = pickApprovalButton(modal?.buttons, this.provider);
|
|
865
1330
|
this.recordAutoApproval(modal?.message, buttonLabel, now);
|
|
866
1331
|
setTimeout(() => {
|
|
867
1332
|
this.adapter.resolveModal(buttonIndex);
|
|
@@ -876,6 +1341,13 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
876
1341
|
// during long-running CLI sessions. Keep this path on adapter-owned light
|
|
877
1342
|
// state only; rich provider parsing is reserved for getState/read_chat.
|
|
878
1343
|
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
1344
|
+
const adapterProviderSessionId = normalizeProviderSessionId(
|
|
1345
|
+
this.provider,
|
|
1346
|
+
typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
|
|
1347
|
+
);
|
|
1348
|
+
if (adapterProviderSessionId) {
|
|
1349
|
+
this.promoteProviderSessionId(adapterProviderSessionId);
|
|
1350
|
+
}
|
|
879
1351
|
const parsedStatus = null;
|
|
880
1352
|
const rawStatus = adapterStatus.status;
|
|
881
1353
|
const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, now);
|
|
@@ -926,10 +1398,14 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
926
1398
|
if (!this.generatingStartedAt) this.generatingStartedAt = now;
|
|
927
1399
|
const modal = adapterStatus.activeModal;
|
|
928
1400
|
LOG.info('CLI', `[${this.type}] approval modal: "${modal?.message?.slice(0, 80) ?? 'none'}"`);
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1401
|
+
const approvalFingerprint = JSON.stringify({
|
|
1402
|
+
message: typeof modal?.message === 'string' ? modal.message.trim() : '',
|
|
1403
|
+
buttons: Array.isArray(modal?.buttons) ? modal.buttons.map((button: unknown) => String(button).trim()) : [],
|
|
1404
|
+
});
|
|
1405
|
+
// PTY redraws can briefly leave waiting_approval and then re-enter it.
|
|
1406
|
+
// Keep one coordinator event per logical modal until the turn completes.
|
|
1407
|
+
if (this.lastStatus !== 'waiting_approval' && approvalFingerprint !== this.lastApprovalEventFingerprint) {
|
|
1408
|
+
this.lastApprovalEventFingerprint = approvalFingerprint;
|
|
933
1409
|
this.appendRuntimeSystemMessage(
|
|
934
1410
|
this.formatApprovalRequestMessage(modal?.message, modal?.buttons),
|
|
935
1411
|
`approval_request:${now}`,
|
|
@@ -943,20 +1419,72 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
943
1419
|
}
|
|
944
1420
|
} else if (newStatus === 'idle' && (this.lastStatus === 'generating' || this.lastStatus === 'waiting_approval')) {
|
|
945
1421
|
const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : 0;
|
|
946
|
-
// If debounce still pending (generating lasted < 1s), cancel both events
|
|
1422
|
+
// If debounce still pending (generating lasted < 1s), cancel both UI events.
|
|
1423
|
+
// Still emit agent:generating_completed so mesh orchestration can record
|
|
1424
|
+
// task_completed for direct dispatches that complete faster than the debounce.
|
|
947
1425
|
if (this.generatingDebouncePending) {
|
|
948
|
-
|
|
1426
|
+
const shortDurationMs = this.generatingStartedAt ? now - this.generatingStartedAt : 0;
|
|
1427
|
+
LOG.info('CLI', `[${this.type}] suppressed short generating (${shortDurationMs}ms)`);
|
|
949
1428
|
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
950
1429
|
this.generatingDebouncePending = null;
|
|
951
1430
|
this.generatingStartedAt = 0;
|
|
1431
|
+
// Emit completion for mesh task association even though the UI generating
|
|
1432
|
+
// started/completed pair is suppressed (too short for visible UI update).
|
|
1433
|
+
let shortFinalSummary: string | undefined;
|
|
1434
|
+
let shortEvidenceSource: CompletionFinalAssistantEvidence['source'] = 'unavailable';
|
|
1435
|
+
try {
|
|
1436
|
+
const parsedMessages = this.adapter?.getScriptParsedStatus()?.messages;
|
|
1437
|
+
const evidence = this.completionFinalAssistantEvidence(parsedMessages);
|
|
1438
|
+
shortEvidenceSource = evidence.source;
|
|
1439
|
+
shortFinalSummary = extractFinalSummaryFromMessages(evidence.messages as any);
|
|
1440
|
+
} catch { /* best-effort */ }
|
|
1441
|
+
if (((this.provider as any).requiresFinalAssistantBeforeIdle === true || shortEvidenceSource === 'external-native') && !shortFinalSummary) {
|
|
1442
|
+
LOG.info('CLI', `[${this.type}] suppressed short completion without final assistant evidence`);
|
|
1443
|
+
} else {
|
|
1444
|
+
this.pushEvent({
|
|
1445
|
+
event: 'agent:generating_completed',
|
|
1446
|
+
chatTitle,
|
|
1447
|
+
duration: 0,
|
|
1448
|
+
timestamp: now,
|
|
1449
|
+
finalSummary: shortFinalSummary,
|
|
1450
|
+
completionDiagnostic: {
|
|
1451
|
+
reason: 'short_generating_suppressed',
|
|
1452
|
+
shortDurationMs,
|
|
1453
|
+
finalAssistantEvidenceSource: shortEvidenceSource,
|
|
1454
|
+
},
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
952
1457
|
} else {
|
|
953
1458
|
// Debounce completed, then require the rich transcript path that read_chat
|
|
954
1459
|
// uses to show an idle turn whose last user-facing message is assistant.
|
|
955
|
-
this.completedDebouncePending = {
|
|
1460
|
+
this.completedDebouncePending = {
|
|
1461
|
+
chatTitle,
|
|
1462
|
+
duration,
|
|
1463
|
+
timestamp: now,
|
|
1464
|
+
firstObservedAt: now,
|
|
1465
|
+
previousStatus: this.lastStatus,
|
|
1466
|
+
};
|
|
956
1467
|
this.scheduleCompletedDebounceFlush(3000);
|
|
957
1468
|
}
|
|
958
1469
|
} else if (newStatus === 'idle' && this.lastStatus === 'starting') {
|
|
959
1470
|
this.pushEvent({ event: 'agent:ready', chatTitle, timestamp: now });
|
|
1471
|
+
} else if (newStatus === 'error') {
|
|
1472
|
+
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
1473
|
+
this.generatingDebouncePending = null;
|
|
1474
|
+
if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
|
|
1475
|
+
this.completedDebouncePending = null;
|
|
1476
|
+
this.errorMessage = adapterStatus.errorMessage || this.errorMessage;
|
|
1477
|
+
this.errorReason = (adapterStatus.errorReason as ProviderErrorReason) || this.errorReason;
|
|
1478
|
+
this.pushEvent({
|
|
1479
|
+
event: 'agent:stopped',
|
|
1480
|
+
chatTitle,
|
|
1481
|
+
timestamp: now,
|
|
1482
|
+
finalSummary: adapterStatus.errorMessage || adapterStatus.errorReason || 'Provider reported an error',
|
|
1483
|
+
completionDiagnostic: {
|
|
1484
|
+
reason: adapterStatus.errorReason || 'provider_error',
|
|
1485
|
+
errorMessage: adapterStatus.errorMessage || undefined,
|
|
1486
|
+
},
|
|
1487
|
+
});
|
|
960
1488
|
} else if (newStatus === 'stopped') {
|
|
961
1489
|
// Cancel any pending debounce
|
|
962
1490
|
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
@@ -977,7 +1505,31 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
977
1505
|
// Monitor check (cooldown based notification, IDE/CLI common)
|
|
978
1506
|
const agentKey = `${this.type}:cli`;
|
|
979
1507
|
const monitorEvents = this.monitor.check(agentKey, newStatus, now, progressFingerprint);
|
|
1508
|
+
const monitorParsedStatus: any = parsedStatus;
|
|
980
1509
|
for (const me of monitorEvents) {
|
|
1510
|
+
if (
|
|
1511
|
+
me.type === 'monitor:long_generating'
|
|
1512
|
+
&& this.completionHasFinalAssistantMessage(monitorParsedStatus?.messages)
|
|
1513
|
+
&& !this.hasAdapterPendingResponse()
|
|
1514
|
+
&& !hasNonEmptyCliModalButtons(monitorParsedStatus?.activeModal ?? monitorParsedStatus?.modal)
|
|
1515
|
+
) {
|
|
1516
|
+
this.pushEvent({
|
|
1517
|
+
event: 'agent:generating_completed',
|
|
1518
|
+
chatTitle,
|
|
1519
|
+
duration: this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : undefined,
|
|
1520
|
+
timestamp: me.timestamp,
|
|
1521
|
+
finalSummary: extractFinalSummaryFromMessages(monitorParsedStatus?.messages),
|
|
1522
|
+
completionDiagnostic: {
|
|
1523
|
+
providerType: this.type,
|
|
1524
|
+
sessionId: this.instanceId,
|
|
1525
|
+
providerSessionId: this.providerSessionId || null,
|
|
1526
|
+
reconciliationReason: 'long_generating_monitor_final_summary',
|
|
1527
|
+
finalAssistantPresent: true,
|
|
1528
|
+
},
|
|
1529
|
+
});
|
|
1530
|
+
this.generatingStartedAt = 0;
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
981
1533
|
this.pushEvent({ event: me.type, agentKey: me.agentKey, message: me.message, elapsedSec: me.elapsedSec, timestamp: me.timestamp });
|
|
982
1534
|
}
|
|
983
1535
|
}
|
|
@@ -1003,9 +1555,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1003
1555
|
};
|
|
1004
1556
|
if (this.context?.emitProviderEvent) {
|
|
1005
1557
|
this.context.emitProviderEvent(enrichedEvent);
|
|
1006
|
-
|
|
1558
|
+
} else {
|
|
1559
|
+
this.events.push(enrichedEvent);
|
|
1560
|
+
}
|
|
1561
|
+
// Auto-detach a direct-dispatch mesh assignment once the dispatched
|
|
1562
|
+
// task reaches a terminal state. Leaving meshNodeFor pinned would
|
|
1563
|
+
// route this session's next unrelated turn (a dashboard chat) into
|
|
1564
|
+
// the coordinator as if it were the completion of another task.
|
|
1565
|
+
// We schedule after the emit so the originating coordinator still
|
|
1566
|
+
// observes the completion event with its routing marker intact.
|
|
1567
|
+
if (TERMINAL_MESH_EVENTS.has(event.event) && this.settings.meshActiveTaskId) {
|
|
1568
|
+
try { this.detachMeshAssignment(); } catch { /* best-effort */ }
|
|
1007
1569
|
}
|
|
1008
|
-
this.events.push(enrichedEvent);
|
|
1009
1570
|
}
|
|
1010
1571
|
|
|
1011
1572
|
private flushEvents(): ProviderEvent[] {
|
|
@@ -1258,12 +1819,28 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1258
1819
|
index,
|
|
1259
1820
|
source: 'parsed',
|
|
1260
1821
|
}));
|
|
1822
|
+
const getRole = (message: ChatMessage): string => typeof message.role === 'string'
|
|
1823
|
+
? message.role.trim().toLowerCase()
|
|
1824
|
+
: '';
|
|
1261
1825
|
const runtimeEntries: MergeEntry[] = this.runtimeMessages.map((entry, index) => ({
|
|
1262
1826
|
message: entry.message,
|
|
1263
1827
|
index: parsedMessages.length + index,
|
|
1264
|
-
source: 'runtime',
|
|
1828
|
+
source: 'runtime' as const,
|
|
1265
1829
|
runtimeKey: entry.key,
|
|
1266
|
-
}))
|
|
1830
|
+
})).filter((entry) => {
|
|
1831
|
+
const meta = entry.message.meta && typeof entry.message.meta === 'object' && !Array.isArray(entry.message.meta)
|
|
1832
|
+
? entry.message.meta as Record<string, unknown>
|
|
1833
|
+
: {};
|
|
1834
|
+
if (meta.runtimeInputAck !== true) return true;
|
|
1835
|
+
const runtimeText = flattenContent(entry.message.content).replace(/\s+/g, ' ').trim();
|
|
1836
|
+
if (!runtimeText) return false;
|
|
1837
|
+
return !parsedEntries.some((parsedEntry) => {
|
|
1838
|
+
const parsedRole = getRole(parsedEntry.message);
|
|
1839
|
+
if (parsedRole !== 'user' && parsedRole !== 'human') return false;
|
|
1840
|
+
const parsedText = flattenContent(parsedEntry.message.content).replace(/\s+/g, ' ').trim();
|
|
1841
|
+
return parsedText === runtimeText;
|
|
1842
|
+
});
|
|
1843
|
+
});
|
|
1267
1844
|
const getTime = (message: ChatMessage): number => {
|
|
1268
1845
|
const value = typeof message.receivedAt === 'number'
|
|
1269
1846
|
? message.receivedAt
|
|
@@ -1273,9 +1850,6 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1273
1850
|
return Number.isFinite(value) && value > 0 ? value : 0;
|
|
1274
1851
|
};
|
|
1275
1852
|
|
|
1276
|
-
const getRole = (message: ChatMessage): string => typeof message.role === 'string'
|
|
1277
|
-
? message.role.trim().toLowerCase()
|
|
1278
|
-
: '';
|
|
1279
1853
|
const isRuntimeOverlay = (entry: MergeEntry): boolean => {
|
|
1280
1854
|
if (entry.source !== 'runtime') return false;
|
|
1281
1855
|
const key = typeof entry.runtimeKey === 'string' ? entry.runtimeKey.trim().toLowerCase() : '';
|
|
@@ -1341,7 +1915,9 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1341
1915
|
this.providerSessionId = nextSessionId;
|
|
1342
1916
|
this.historyWriter.promoteHistorySession(this.type, previousHistorySessionId, nextSessionId);
|
|
1343
1917
|
this.historyWriter.writeSessionStart(this.type, nextSessionId, this.workingDir, this.instanceId);
|
|
1344
|
-
this.
|
|
1918
|
+
if (this.shouldHydrateExistingProviderHistory()) {
|
|
1919
|
+
this.restorePersistedHistoryFromCurrentSession();
|
|
1920
|
+
}
|
|
1345
1921
|
this.adapter.updateRuntimeMeta({ providerSessionId: nextSessionId });
|
|
1346
1922
|
this.onProviderSessionResolved?.({
|
|
1347
1923
|
instanceId: this.instanceId,
|
|
@@ -1354,9 +1930,27 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1354
1930
|
LOG.info('CLI', `[${this.type}] discovered provider session id: ${nextSessionId}`);
|
|
1355
1931
|
}
|
|
1356
1932
|
|
|
1933
|
+
private shouldHydrateExistingProviderHistory(): boolean {
|
|
1934
|
+
return this.launchMode === 'resume' || this.launchMode === 'manual';
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
private shouldSuppressFreshLaunchStartupReplay(parsedMessages: unknown[], parsedStatus: any, adapterStatus: any, parsedProviderSessionId = ''): boolean {
|
|
1938
|
+
if (this.launchMode !== 'new') return false;
|
|
1939
|
+
if (this.providerSessionId) return false;
|
|
1940
|
+
if (!Array.isArray(parsedMessages) || parsedMessages.length === 0) return false;
|
|
1941
|
+
if (!isIdleStatus(adapterStatus?.status) || !isIdleStatus(parsedStatus?.status)) return false;
|
|
1942
|
+
if (parsedProviderSessionId) return true;
|
|
1943
|
+
|
|
1944
|
+
const newestMessageAt = parsedMessages.reduce<number>((newest, message) => Math.max(newest, getMessageTime(message)), 0);
|
|
1945
|
+
|
|
1946
|
+
// Untimestamped idle parser output during a fresh launch is usually the
|
|
1947
|
+
// provider's last workspace transcript before a new turn exists.
|
|
1948
|
+
return newestMessageAt === 0;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1357
1951
|
private syncCanonicalSavedHistoryIfNeeded(): boolean {
|
|
1358
1952
|
if (!this.providerSessionId) return false;
|
|
1359
|
-
const canonicalHistory = this.provider.
|
|
1953
|
+
const canonicalHistory = this.provider.nativeHistory;
|
|
1360
1954
|
if (!canonicalHistory) return false;
|
|
1361
1955
|
|
|
1362
1956
|
if (isNativeSourceCanonicalHistory(canonicalHistory)) {
|
|
@@ -1418,9 +2012,9 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1418
2012
|
private restorePersistedHistoryFromCurrentSession(): void {
|
|
1419
2013
|
if (!this.providerSessionId) return;
|
|
1420
2014
|
this.syncCanonicalSavedHistoryIfNeeded();
|
|
1421
|
-
const restoredHistory = isNativeSourceCanonicalHistory(this.provider.
|
|
2015
|
+
const restoredHistory = isNativeSourceCanonicalHistory(this.provider.nativeHistory)
|
|
1422
2016
|
? readProviderChatHistory(this.type, {
|
|
1423
|
-
canonicalHistory: this.provider.
|
|
2017
|
+
canonicalHistory: this.provider.nativeHistory,
|
|
1424
2018
|
historySessionId: this.providerSessionId,
|
|
1425
2019
|
workspace: this.workingDir,
|
|
1426
2020
|
offset: 0,
|