@adhdev/daemon-core 0.9.77-rc.9 → 0.9.77
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 +3 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +2 -0
- package/dist/commands/mesh-coordinator.d.ts +10 -0
- package/dist/commands/router.d.ts +4 -1
- package/dist/config/mesh-config.d.ts +1 -0
- package/dist/git/git-worktree.d.ts +15 -2
- package/dist/index.d.ts +10 -6
- package/dist/index.js +2116 -299
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2101 -299
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events.d.ts +14 -7
- package/dist/mesh/mesh-ledger-reconciliation.d.ts +55 -0
- package/dist/mesh/mesh-ledger.d.ts +84 -4
- package/dist/mesh/mesh-sync.d.ts +4 -12
- package/dist/mesh/mesh-visualization.d.ts +70 -0
- package/dist/mesh/mesh-work-queue.d.ts +58 -1
- package/dist/mesh/p2p-relay-failure.d.ts +35 -0
- package/dist/providers/chat-message-normalization.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +6 -0
- package/dist/repo-mesh-types.d.ts +2 -0
- package/dist/shared-types.d.ts +38 -0
- package/package.json +1 -1
- package/src/boot/daemon-lifecycle.ts +5 -0
- package/src/cli-adapters/provider-cli-adapter.ts +30 -5
- package/src/commands/cli-manager.ts +0 -4
- package/src/commands/mesh-coordinator.ts +55 -7
- package/src/commands/router.ts +964 -26
- package/src/commands/stream-commands.ts +8 -1
- package/src/config/config.ts +2 -1
- package/src/config/mesh-config.ts +2 -0
- package/src/config/workspaces.ts +1 -1
- package/src/git/git-worktree.ts +56 -4
- package/src/index.d.ts +3 -0
- package/src/index.ts +29 -6
- package/src/mesh/coordinator-prompt.ts +21 -10
- package/src/mesh/mesh-events.ts +532 -22
- package/src/mesh/mesh-ledger-reconciliation.ts +115 -0
- package/src/mesh/mesh-ledger.ts +209 -8
- package/src/mesh/mesh-sync.ts +4 -34
- package/src/mesh/mesh-visualization.ts +341 -0
- package/src/mesh/mesh-work-queue.ts +183 -17
- package/src/mesh/p2p-relay-failure.ts +152 -0
- package/src/providers/acp-provider-instance.ts +2 -1
- package/src/providers/chat-message-normalization.ts +32 -0
- package/src/providers/cli-provider-instance.ts +155 -31
- package/src/providers/extension-provider-instance.ts +2 -1
- package/src/providers/ide-provider-instance.ts +2 -2
- package/src/repo-mesh-types.ts +2 -0
- package/src/shared-types.ts +38 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
export type P2pRelayFailureCode =
|
|
2
|
+
| 'p2p_unavailable'
|
|
3
|
+
| 'p2p_timeout'
|
|
4
|
+
| 'p2p_not_connected'
|
|
5
|
+
| 'p2p_datachannel_closed'
|
|
6
|
+
| 'p2p_no_route'
|
|
7
|
+
| 'p2p_daemon_offline'
|
|
8
|
+
| 'mesh_logic_or_provider_failure';
|
|
9
|
+
|
|
10
|
+
export interface P2pRelayFailureContext {
|
|
11
|
+
command?: string;
|
|
12
|
+
targetDaemonId?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface P2pRelayFailureClassification {
|
|
16
|
+
code: P2pRelayFailureCode;
|
|
17
|
+
reason: string;
|
|
18
|
+
transport: 'p2p' | 'unknown';
|
|
19
|
+
recoverable: boolean;
|
|
20
|
+
retryRecommended: boolean;
|
|
21
|
+
nextAction: string;
|
|
22
|
+
noFallbackReason: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface P2pRelayFailurePayload extends P2pRelayFailureClassification {
|
|
26
|
+
success: false;
|
|
27
|
+
error: string;
|
|
28
|
+
command?: string;
|
|
29
|
+
targetDaemonId?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const NO_FALLBACK_REASON = 'Repo Mesh command/data-plane is P2P-only; WS/REST command fallback is intentionally disabled to preserve the transport boundary.';
|
|
33
|
+
const P2P_NEXT_ACTION = 'Check daemon/P2P health, wait briefly for connection establishment, then do one bounded retry or requeue the mesh task after clearing stale target session metadata.';
|
|
34
|
+
const NON_P2P_NEXT_ACTION = 'Inspect the provider/command error and fix the underlying logic or configuration before retrying.';
|
|
35
|
+
|
|
36
|
+
function messageFromError(error: unknown): string {
|
|
37
|
+
if (error instanceof Error) return error.message;
|
|
38
|
+
if (typeof error === 'string') return error;
|
|
39
|
+
if (error && typeof error === 'object') {
|
|
40
|
+
const candidate = (error as any).error ?? (error as any).message ?? (error as any).reason;
|
|
41
|
+
if (typeof candidate === 'string') return candidate;
|
|
42
|
+
}
|
|
43
|
+
return String(error || 'mesh relay command failed');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function classifyP2pRelayFailure(error: unknown, _context: P2pRelayFailureContext = {}): P2pRelayFailureClassification {
|
|
47
|
+
const message = messageFromError(error);
|
|
48
|
+
const lower = message.toLowerCase();
|
|
49
|
+
|
|
50
|
+
const hasP2pSignal = /p2p|datachannel|node-datachannel|webrtc|ice|mesh_relay_command|daemon_mesh_p2p_transport/i.test(message);
|
|
51
|
+
const hasFailureSignal = /unavailable|missing|failed|failure|timeout|timed out|not connected|closed|disconnected|offline|no route|route unavailable|cannot send|cannot establish/i.test(message);
|
|
52
|
+
|
|
53
|
+
// Validation errors that merely mention mesh_relay_command are not transport failures.
|
|
54
|
+
if (/requires targetdaemonid and command|providerpriority|no inference provider|permission denied|read-only|not a member/i.test(message)) {
|
|
55
|
+
return {
|
|
56
|
+
code: 'mesh_logic_or_provider_failure',
|
|
57
|
+
reason: 'mesh_logic_or_provider_failure',
|
|
58
|
+
transport: 'unknown',
|
|
59
|
+
recoverable: false,
|
|
60
|
+
retryRecommended: false,
|
|
61
|
+
nextAction: NON_P2P_NEXT_ACTION,
|
|
62
|
+
noFallbackReason: NO_FALLBACK_REASON,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let code: P2pRelayFailureCode | null = null;
|
|
67
|
+
let reason = '';
|
|
68
|
+
|
|
69
|
+
if (/timeout|timed out/i.test(message) && (hasP2pSignal || /mesh transport/i.test(message))) {
|
|
70
|
+
code = 'p2p_timeout';
|
|
71
|
+
reason = 'daemon_mesh_p2p_timeout';
|
|
72
|
+
} else if (/no route|route unavailable/i.test(message)) {
|
|
73
|
+
code = 'p2p_no_route';
|
|
74
|
+
reason = 'daemon_mesh_p2p_no_route';
|
|
75
|
+
} else if (/offline|not owned|not found|not connected to server/i.test(message) && /daemon|peer|target/i.test(message)) {
|
|
76
|
+
code = 'p2p_daemon_offline';
|
|
77
|
+
reason = 'daemon_mesh_target_offline';
|
|
78
|
+
} else if (/closed|disconnected/i.test(message) && (hasP2pSignal || /state changed/i.test(message))) {
|
|
79
|
+
code = 'p2p_datachannel_closed';
|
|
80
|
+
reason = 'daemon_mesh_p2p_datachannel_closed';
|
|
81
|
+
} else if (/not connected|cannot send|cannot establish/i.test(message) && hasP2pSignal) {
|
|
82
|
+
code = 'p2p_not_connected';
|
|
83
|
+
reason = 'daemon_mesh_p2p_not_connected';
|
|
84
|
+
} else if (hasP2pSignal && hasFailureSignal) {
|
|
85
|
+
code = 'p2p_unavailable';
|
|
86
|
+
reason = 'daemon_mesh_p2p_transport_unavailable';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!code) {
|
|
90
|
+
return {
|
|
91
|
+
code: 'mesh_logic_or_provider_failure',
|
|
92
|
+
reason: 'mesh_logic_or_provider_failure',
|
|
93
|
+
transport: 'unknown',
|
|
94
|
+
recoverable: false,
|
|
95
|
+
retryRecommended: false,
|
|
96
|
+
nextAction: NON_P2P_NEXT_ACTION,
|
|
97
|
+
noFallbackReason: NO_FALLBACK_REASON,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
code,
|
|
103
|
+
reason,
|
|
104
|
+
transport: 'p2p',
|
|
105
|
+
recoverable: true,
|
|
106
|
+
retryRecommended: true,
|
|
107
|
+
nextAction: P2P_NEXT_ACTION,
|
|
108
|
+
noFallbackReason: NO_FALLBACK_REASON,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function isP2pRelayTransportFailure(error: unknown): boolean {
|
|
113
|
+
return classifyP2pRelayFailure(error).recoverable === true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function buildP2pRelayFailurePayload(error: unknown, context: P2pRelayFailureContext = {}): P2pRelayFailurePayload {
|
|
117
|
+
const classification = classifyP2pRelayFailure(error, context);
|
|
118
|
+
return {
|
|
119
|
+
success: false,
|
|
120
|
+
...classification,
|
|
121
|
+
error: messageFromError(error),
|
|
122
|
+
...(context.command ? { command: context.command } : {}),
|
|
123
|
+
...(context.targetDaemonId ? { targetDaemonId: context.targetDaemonId } : {}),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export class P2pRelayFailureError extends Error {
|
|
128
|
+
code: P2pRelayFailureCode;
|
|
129
|
+
reason: string;
|
|
130
|
+
transport: 'p2p' | 'unknown';
|
|
131
|
+
recoverable: boolean;
|
|
132
|
+
retryRecommended: boolean;
|
|
133
|
+
nextAction: string;
|
|
134
|
+
noFallbackReason: string;
|
|
135
|
+
command?: string;
|
|
136
|
+
targetDaemonId?: string;
|
|
137
|
+
|
|
138
|
+
constructor(message: string, context: P2pRelayFailureContext = {}) {
|
|
139
|
+
super(message);
|
|
140
|
+
this.name = 'P2pRelayFailureError';
|
|
141
|
+
const payload = buildP2pRelayFailurePayload(message, context);
|
|
142
|
+
this.code = payload.code;
|
|
143
|
+
this.reason = payload.reason;
|
|
144
|
+
this.transport = payload.transport;
|
|
145
|
+
this.recoverable = payload.recoverable;
|
|
146
|
+
this.retryRecommended = payload.retryRecommended;
|
|
147
|
+
this.nextAction = payload.nextAction;
|
|
148
|
+
this.noFallbackReason = payload.noFallbackReason;
|
|
149
|
+
this.command = context.command;
|
|
150
|
+
this.targetDaemonId = context.targetDaemonId;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -61,6 +61,7 @@ import {
|
|
|
61
61
|
buildToolChatMessage,
|
|
62
62
|
buildUserChatMessage,
|
|
63
63
|
normalizeChatMessages,
|
|
64
|
+
extractFinalSummaryFromMessages,
|
|
64
65
|
} from './chat-message-normalization.js';
|
|
65
66
|
import { LOG } from '../logging/logger.js';
|
|
66
67
|
import type { ChatMessage } from '../types.js';
|
|
@@ -1507,7 +1508,7 @@ export class AcpProviderInstance implements ProviderInstance {
|
|
|
1507
1508
|
});
|
|
1508
1509
|
} else if (newStatus === 'idle' && (this.lastStatus === 'generating' || this.lastStatus === 'waiting_approval')) {
|
|
1509
1510
|
const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : 0;
|
|
1510
|
-
this.pushEvent({ event: 'agent:generating_completed', chatTitle, duration, timestamp: now });
|
|
1511
|
+
this.pushEvent({ event: 'agent:generating_completed', chatTitle, duration, timestamp: now, finalSummary: extractFinalSummaryFromMessages(this.messages) });
|
|
1511
1512
|
this.generatingStartedAt = 0;
|
|
1512
1513
|
} else if (newStatus === 'stopped') {
|
|
1513
1514
|
this.pushEvent({ event: 'agent:stopped', chatTitle, timestamp: now });
|
|
@@ -1,4 +1,36 @@
|
|
|
1
1
|
import type { ChatMessage } from '../types.js';
|
|
2
|
+
import { flattenContent } from './contracts.js';
|
|
3
|
+
|
|
4
|
+
export function extractFinalSummaryFromMessages(
|
|
5
|
+
messages: ChatMessage[] | null | undefined,
|
|
6
|
+
maxChars: number = 500,
|
|
7
|
+
): string {
|
|
8
|
+
if (!Array.isArray(messages) || messages.length === 0) return '';
|
|
9
|
+
|
|
10
|
+
// Find last user-facing assistant message
|
|
11
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
12
|
+
const msg = messages[i];
|
|
13
|
+
if (!msg) continue;
|
|
14
|
+
const classification = classifyChatMessageVisibility(msg);
|
|
15
|
+
if (classification.isUserFacing && (msg.role === 'assistant' || msg.role === 'model')) {
|
|
16
|
+
const text = flattenContent(msg.content).trim();
|
|
17
|
+
if (text) return text.slice(0, maxChars);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Fallback: last user-facing message of any role
|
|
22
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
23
|
+
const msg = messages[i];
|
|
24
|
+
if (!msg) continue;
|
|
25
|
+
const classification = classifyChatMessageVisibility(msg);
|
|
26
|
+
if (classification.isUserFacing) {
|
|
27
|
+
const text = flattenContent(msg.content).trim();
|
|
28
|
+
if (text) return text.slice(0, maxChars);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
2
34
|
|
|
3
35
|
export const BUILTIN_CHAT_MESSAGE_KINDS = ['standard', 'thought', 'tool', 'terminal', 'system'] as const;
|
|
4
36
|
|
|
@@ -25,7 +25,7 @@ import { formatAutoApprovalMessage, pickApprovalButton } from './approval-utils.
|
|
|
25
25
|
import { getCliScriptCommand, parseCliScriptResult } from './cli-script-results.js';
|
|
26
26
|
import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
|
|
27
27
|
import { normalizeProviderSessionId } from './provider-session-id.js';
|
|
28
|
-
import { buildChatMessage, buildRuntimeSystemChatMessage, isUserFacingChatMessage, normalizeChatMessages, resolveChatMessageKind } from './chat-message-normalization.js';
|
|
28
|
+
import { buildChatMessage, buildRuntimeSystemChatMessage, isUserFacingChatMessage, normalizeChatMessages, resolveChatMessageKind, extractFinalSummaryFromMessages } from './chat-message-normalization.js';
|
|
29
29
|
|
|
30
30
|
type PersistableCliHistoryMessage = {
|
|
31
31
|
role: string;
|
|
@@ -35,6 +35,17 @@ type PersistableCliHistoryMessage = {
|
|
|
35
35
|
receivedAt?: number;
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
type CompletedDebouncePending = {
|
|
39
|
+
chatTitle: string;
|
|
40
|
+
duration: number;
|
|
41
|
+
timestamp: number;
|
|
42
|
+
firstObservedAt: number;
|
|
43
|
+
loggedBlockReason?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const COMPLETED_FINALIZATION_RETRY_MS = 1000;
|
|
47
|
+
const COMPLETED_FINALIZATION_MAX_WAIT_MS = 30_000;
|
|
48
|
+
|
|
38
49
|
const IMAGE_MIME_EXTENSIONS: Record<string, string> = {
|
|
39
50
|
'image/png': '.png',
|
|
40
51
|
'image/jpeg': '.jpg',
|
|
@@ -103,6 +114,15 @@ function cleanupStaleMaterializedImages(dir: string): void {
|
|
|
103
114
|
} catch { /* dir may not exist or be inaccessible */ }
|
|
104
115
|
}
|
|
105
116
|
|
|
117
|
+
function hasNonEmptyCliModalButtons(activeModal: unknown): boolean {
|
|
118
|
+
const buttons = (activeModal as any)?.buttons;
|
|
119
|
+
return Array.isArray(buttons) && buttons.some((button) => String(button || '').trim().length > 0);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isCliGeneratingLikeStatus(status: unknown): boolean {
|
|
123
|
+
return status === 'generating' || status === 'streaming' || status === 'long_generating' || status === 'starting';
|
|
124
|
+
}
|
|
125
|
+
|
|
106
126
|
export function buildCliStructuredInputPrompt(
|
|
107
127
|
input: InputEnvelope,
|
|
108
128
|
options: { materializeDir?: string } = {},
|
|
@@ -511,6 +531,10 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
511
531
|
const canonicalBackedHistory = this.syncCanonicalSavedHistoryIfNeeded();
|
|
512
532
|
|
|
513
533
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
534
|
+
const parsedChatStatus = typeof parsedStatus?.status === 'string' && parsedStatus.status.trim()
|
|
535
|
+
? parsedStatus.status.trim()
|
|
536
|
+
: undefined;
|
|
537
|
+
const suppressStaleParsedBusyStatus = this.shouldSuppressStaleParsedBusyStatus(parsedStatus, adapterStatus);
|
|
514
538
|
|
|
515
539
|
if (parsedMessages.length > 0) {
|
|
516
540
|
const shouldSkipReplayPersist =
|
|
@@ -518,7 +542,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
518
542
|
&& adapterStatus.status === 'idle'
|
|
519
543
|
&& parsedStatus?.status === 'idle';
|
|
520
544
|
let messagesToSave = parsedMessages;
|
|
521
|
-
if ((
|
|
545
|
+
if (!suppressStaleParsedBusyStatus && (parsedChatStatus === 'generating' || parsedChatStatus === 'long_generating')) {
|
|
522
546
|
const lastIdx = messagesToSave.length - 1;
|
|
523
547
|
if (lastIdx >= 0 && messagesToSave[lastIdx]?.role === 'assistant') {
|
|
524
548
|
messagesToSave = messagesToSave.slice(0, lastIdx);
|
|
@@ -553,6 +577,13 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
553
577
|
summaryMetadata: this.summaryMetadata as any,
|
|
554
578
|
controlValues: this.controlValues,
|
|
555
579
|
});
|
|
580
|
+
const activeChatStatus = parseErrorMessage
|
|
581
|
+
? 'error'
|
|
582
|
+
: autoApproveActive && parsedStatus?.status === 'waiting_approval'
|
|
583
|
+
? 'generating'
|
|
584
|
+
: (adapterStatus.status !== 'idle'
|
|
585
|
+
? visibleStatus
|
|
586
|
+
: (suppressStaleParsedBusyStatus ? visibleStatus : (parsedChatStatus || visibleStatus)));
|
|
556
587
|
|
|
557
588
|
return {
|
|
558
589
|
type: this.type,
|
|
@@ -563,13 +594,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
563
594
|
activeChat: {
|
|
564
595
|
id: `${this.type}_${this.workingDir}`,
|
|
565
596
|
title: parsedStatus?.title || dirName,
|
|
566
|
-
status:
|
|
567
|
-
? 'error'
|
|
568
|
-
: autoApproveActive && parsedStatus?.status === 'waiting_approval'
|
|
569
|
-
? 'generating'
|
|
570
|
-
: (adapterStatus.status !== 'idle'
|
|
571
|
-
? visibleStatus
|
|
572
|
-
: (parsedStatus?.status || visibleStatus)),
|
|
597
|
+
status: activeChatStatus,
|
|
573
598
|
messages: mergedMessages,
|
|
574
599
|
activeModal: autoApproveActive ? null : (parsedStatus?.activeModal ?? adapterStatus.activeModal),
|
|
575
600
|
inputContent: '',
|
|
@@ -680,7 +705,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
680
705
|
}
|
|
681
706
|
|
|
682
707
|
private completedDebounceTimer: NodeJS.Timeout | null = null;
|
|
683
|
-
private completedDebouncePending:
|
|
708
|
+
private completedDebouncePending: CompletedDebouncePending | null = null;
|
|
684
709
|
|
|
685
710
|
private async enforceFreshSessionLaunchIfNeeded(): Promise<void> {
|
|
686
711
|
const scriptName = getForcedNewSessionScriptName(this.provider, this.launchMode);
|
|
@@ -709,6 +734,120 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
709
734
|
this.applyProviderResponse(parsed.payload, { phase: 'immediate' });
|
|
710
735
|
}
|
|
711
736
|
|
|
737
|
+
private completionHasFinalAssistantMessage(messages: unknown): boolean {
|
|
738
|
+
const visibleMessages = (Array.isArray(messages) ? messages : [])
|
|
739
|
+
.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
740
|
+
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
741
|
+
const role = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : '';
|
|
742
|
+
const content = lastVisible ? flattenContent(lastVisible.content).trim() : '';
|
|
743
|
+
return role === 'assistant' && !!content;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
private hasAdapterPendingResponse(): boolean {
|
|
747
|
+
const adapterAny = this.adapter as any;
|
|
748
|
+
if (adapterAny?.isWaitingForResponse === true) return true;
|
|
749
|
+
if (adapterAny?.currentTurnScope) return true;
|
|
750
|
+
try {
|
|
751
|
+
if (typeof this.adapter.isProcessing === 'function' && this.adapter.isProcessing()) return true;
|
|
752
|
+
} catch { /* defensive: status rendering must not fail because of adapter diagnostics */ }
|
|
753
|
+
try {
|
|
754
|
+
const partial = typeof this.adapter.getPartialResponse === 'function'
|
|
755
|
+
? this.adapter.getPartialResponse()
|
|
756
|
+
: '';
|
|
757
|
+
if (typeof partial === 'string' && partial.trim()) return true;
|
|
758
|
+
} catch { /* defensive: missing partial means no pending response evidence */ }
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
private shouldSuppressStaleParsedBusyStatus(parsedStatus: any, adapterStatus: any): boolean {
|
|
763
|
+
const parsedRawStatus = typeof parsedStatus?.status === 'string' ? parsedStatus.status.trim() : '';
|
|
764
|
+
const adapterRawStatus = typeof adapterStatus?.status === 'string' ? adapterStatus.status.trim() : '';
|
|
765
|
+
if (!isCliGeneratingLikeStatus(parsedRawStatus)) return false;
|
|
766
|
+
if (adapterRawStatus !== 'idle') return false;
|
|
767
|
+
if (hasNonEmptyCliModalButtons(parsedStatus?.activeModal ?? parsedStatus?.modal)) return false;
|
|
768
|
+
return !this.hasAdapterPendingResponse();
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
private getCompletedFinalizationBlockReason(latestVisibleStatus: string): string | null {
|
|
772
|
+
if (latestVisibleStatus !== 'idle') return `status:${latestVisibleStatus}`;
|
|
773
|
+
|
|
774
|
+
const adapterAny = this.adapter as any;
|
|
775
|
+
if (adapterAny?.isWaitingForResponse === true) return 'adapter_waiting_for_response';
|
|
776
|
+
if (adapterAny?.currentTurnScope) return 'adapter_turn_scope_active';
|
|
777
|
+
|
|
778
|
+
const partial = typeof this.adapter.getPartialResponse === 'function'
|
|
779
|
+
? this.adapter.getPartialResponse()
|
|
780
|
+
: '';
|
|
781
|
+
if (typeof partial === 'string' && partial.trim()) return 'partial_response_pending';
|
|
782
|
+
|
|
783
|
+
let parsed: any;
|
|
784
|
+
try {
|
|
785
|
+
parsed = this.adapter.getScriptParsedStatus();
|
|
786
|
+
} catch (error: any) {
|
|
787
|
+
return `parse_error:${error?.message || String(error)}`;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
const parsedStatus = typeof parsed?.status === 'string' ? parsed.status : 'unknown';
|
|
791
|
+
if (parsedStatus !== 'idle') return `parsed_status:${parsedStatus}`;
|
|
792
|
+
if (parsed?.activeModal || parsed?.modal) return 'parsed_modal_active';
|
|
793
|
+
if (!this.completionHasFinalAssistantMessage(parsed?.messages)) return 'missing_final_assistant';
|
|
794
|
+
|
|
795
|
+
return null;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
private scheduleCompletedDebounceFlush(delayMs: number): void {
|
|
799
|
+
if (this.completedDebounceTimer) clearTimeout(this.completedDebounceTimer);
|
|
800
|
+
this.completedDebounceTimer = setTimeout(() => this.flushCompletedDebounceIfFinalized(), delayMs);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
private flushCompletedDebounceIfFinalized(): void {
|
|
804
|
+
const pending = this.completedDebouncePending;
|
|
805
|
+
if (!pending) {
|
|
806
|
+
this.completedDebounceTimer = null;
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const latestStatus = this.adapter.getStatus({ allowParse: false });
|
|
811
|
+
const latestAutoApproveActive = latestStatus.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
812
|
+
const latestVisibleStatus = latestAutoApproveActive ? 'generating' : latestStatus.status;
|
|
813
|
+
if (latestVisibleStatus !== 'idle') {
|
|
814
|
+
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed ${latestVisibleStatus})`);
|
|
815
|
+
this.completedDebouncePending = null;
|
|
816
|
+
this.completedDebounceTimer = null;
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const blockReason = this.getCompletedFinalizationBlockReason(latestVisibleStatus);
|
|
821
|
+
if (blockReason) {
|
|
822
|
+
const waitedMs = Date.now() - pending.firstObservedAt;
|
|
823
|
+
if (waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
|
|
824
|
+
if (pending.loggedBlockReason !== blockReason) {
|
|
825
|
+
LOG.info('CLI', `[${this.type}] waiting to emit completed until transcript finalizes (${blockReason})`);
|
|
826
|
+
pending.loggedBlockReason = blockReason;
|
|
827
|
+
}
|
|
828
|
+
this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
LOG.warn('CLI', `[${this.type}] suppressed completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
|
|
832
|
+
this.completedDebouncePending = null;
|
|
833
|
+
this.completedDebounceTimer = null;
|
|
834
|
+
this.generatingStartedAt = 0;
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
LOG.info('CLI', `[${this.type}] completed in ${pending.duration}s`);
|
|
839
|
+
this.pushEvent({
|
|
840
|
+
event: 'agent:generating_completed',
|
|
841
|
+
chatTitle: pending.chatTitle,
|
|
842
|
+
duration: pending.duration,
|
|
843
|
+
timestamp: pending.timestamp,
|
|
844
|
+
finalSummary: extractFinalSummaryFromMessages(this.adapter?.getScriptParsedStatus()?.messages),
|
|
845
|
+
});
|
|
846
|
+
this.completedDebouncePending = null;
|
|
847
|
+
this.completedDebounceTimer = null;
|
|
848
|
+
this.generatingStartedAt = 0;
|
|
849
|
+
}
|
|
850
|
+
|
|
712
851
|
private maybeAutoApproveStatus(adapterStatus: any, now = Date.now()): boolean {
|
|
713
852
|
const autoApproveActive = adapterStatus?.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
714
853
|
// Guard re-entry: onStatusChange/getState can observe the same modal multiple
|
|
@@ -811,28 +950,13 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
811
950
|
this.generatingDebouncePending = null;
|
|
812
951
|
this.generatingStartedAt = 0;
|
|
813
952
|
} else {
|
|
814
|
-
// Debounce completed
|
|
815
|
-
|
|
816
|
-
this.completedDebouncePending = { chatTitle, duration, timestamp: now };
|
|
817
|
-
this.
|
|
818
|
-
if (this.completedDebouncePending) {
|
|
819
|
-
const latestStatus = this.adapter.getStatus({ allowParse: false });
|
|
820
|
-
const latestAutoApproveActive = latestStatus.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
821
|
-
const latestVisibleStatus = latestAutoApproveActive ? 'generating' : latestStatus.status;
|
|
822
|
-
if (latestVisibleStatus !== 'idle') {
|
|
823
|
-
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed ${latestVisibleStatus})`);
|
|
824
|
-
this.completedDebouncePending = null;
|
|
825
|
-
this.completedDebounceTimer = null;
|
|
826
|
-
return;
|
|
827
|
-
}
|
|
828
|
-
LOG.info('CLI', `[${this.type}] completed in ${this.completedDebouncePending.duration}s`);
|
|
829
|
-
this.pushEvent({ event: 'agent:generating_completed', ...this.completedDebouncePending });
|
|
830
|
-
this.completedDebouncePending = null;
|
|
831
|
-
this.generatingStartedAt = 0;
|
|
832
|
-
}
|
|
833
|
-
this.completedDebounceTimer = null;
|
|
834
|
-
}, 3000);
|
|
953
|
+
// Debounce completed, then require the rich transcript path that read_chat
|
|
954
|
+
// uses to show an idle turn whose last user-facing message is assistant.
|
|
955
|
+
this.completedDebouncePending = { chatTitle, duration, timestamp: now, firstObservedAt: now };
|
|
956
|
+
this.scheduleCompletedDebounceFlush(3000);
|
|
835
957
|
}
|
|
958
|
+
} else if (newStatus === 'idle' && this.lastStatus === 'starting') {
|
|
959
|
+
this.pushEvent({ event: 'agent:ready', chatTitle, timestamp: now });
|
|
836
960
|
} else if (newStatus === 'stopped') {
|
|
837
961
|
// Cancel any pending debounce
|
|
838
962
|
if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
|
|
@@ -12,7 +12,7 @@ import { buildPersistedProviderEffectMessage, normalizeProviderEffects } from '.
|
|
|
12
12
|
import { ChatHistoryWriter } from '../config/chat-history.js';
|
|
13
13
|
import type { ChatMessage } from '../types.js';
|
|
14
14
|
import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
|
|
15
|
-
import { buildChatMessage, buildRuntimeSystemChatMessage, normalizeChatMessages } from './chat-message-normalization.js';
|
|
15
|
+
import { buildChatMessage, buildRuntimeSystemChatMessage, normalizeChatMessages, extractFinalSummaryFromMessages } from './chat-message-normalization.js';
|
|
16
16
|
import { getProviderSessionCapabilities, EXTENSION_PROVIDER_SESSION_CAPABILITIES_BASE } from './open-panel-support.js';
|
|
17
17
|
|
|
18
18
|
export class ExtensionProviderInstance implements ProviderInstance {
|
|
@@ -234,6 +234,7 @@ export class ExtensionProviderInstance implements ProviderInstance {
|
|
|
234
234
|
agentType: this.type,
|
|
235
235
|
agentName: this.agentName || this.provider.name,
|
|
236
236
|
extensionId: this.extensionId || this.type,
|
|
237
|
+
finalSummary: extractFinalSummaryFromMessages(data?.messages),
|
|
237
238
|
});
|
|
238
239
|
this.generatingStartedAt = 0;
|
|
239
240
|
}
|
|
@@ -22,7 +22,7 @@ import { validateReadChatResultPayload } from './read-chat-contract.js';
|
|
|
22
22
|
import type { ChatMessage } from '../types.js';
|
|
23
23
|
import { formatAutoApprovalMessage, pickApprovalButton } from './approval-utils.js';
|
|
24
24
|
import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
|
|
25
|
-
import { buildChatMessage, buildRuntimeSystemChatMessage, normalizeChatMessages } from './chat-message-normalization.js';
|
|
25
|
+
import { buildChatMessage, buildRuntimeSystemChatMessage, normalizeChatMessages, extractFinalSummaryFromMessages } from './chat-message-normalization.js';
|
|
26
26
|
import { getProviderSessionCapabilities, IDE_PROVIDER_SESSION_CAPABILITIES_BASE } from './open-panel-support.js';
|
|
27
27
|
|
|
28
28
|
type ReadChatModal = {
|
|
@@ -470,7 +470,7 @@ export class IdeProviderInstance implements ProviderInstance {
|
|
|
470
470
|
} else if (agentStatus === 'idle' && (lastStatus === 'generating' || lastStatus === 'waiting_approval')) {
|
|
471
471
|
const startedAt = this.generatingStartedAt.get(agentKey);
|
|
472
472
|
const duration = startedAt ? Math.round((now - startedAt) / 1000) : 0;
|
|
473
|
-
this.pushEvent({ event: 'agent:generating_completed', chatTitle, duration, timestamp: now, ideType: this.type });
|
|
473
|
+
this.pushEvent({ event: 'agent:generating_completed', chatTitle, duration, timestamp: now, ideType: this.type, finalSummary: extractFinalSummaryFromMessages(chatData?.messages) });
|
|
474
474
|
this.generatingStartedAt.delete(agentKey);
|
|
475
475
|
}
|
|
476
476
|
|
package/src/repo-mesh-types.ts
CHANGED
|
@@ -224,6 +224,8 @@ export interface LocalMeshNodeEntry {
|
|
|
224
224
|
workspace: string;
|
|
225
225
|
repoRoot?: string;
|
|
226
226
|
daemonId?: string;
|
|
227
|
+
/** Machine registry ID that owns this workspace, when known. */
|
|
228
|
+
machineId?: string;
|
|
227
229
|
userOverrides: Partial<RepoMeshNodeCapabilities>;
|
|
228
230
|
policy: RepoMeshNodePolicy;
|
|
229
231
|
/** For single-machine mesh: same daemon, different worktree */
|
package/src/shared-types.ts
CHANGED
|
@@ -390,10 +390,29 @@ export interface SessionEntry {
|
|
|
390
390
|
surfaceHidden?: boolean;
|
|
391
391
|
settings?: Record<string, any>;
|
|
392
392
|
meshQueueStats?: {
|
|
393
|
+
total?: number;
|
|
394
|
+
active?: number;
|
|
395
|
+
historical?: number;
|
|
393
396
|
pending: number;
|
|
394
397
|
assigned: number;
|
|
395
398
|
completed: number;
|
|
396
399
|
failed: number;
|
|
400
|
+
cancelled?: number;
|
|
401
|
+
activeCounts?: {
|
|
402
|
+
pending: number;
|
|
403
|
+
assigned: number;
|
|
404
|
+
};
|
|
405
|
+
historicalCounts?: {
|
|
406
|
+
completed: number;
|
|
407
|
+
failed: number;
|
|
408
|
+
cancelled: number;
|
|
409
|
+
};
|
|
410
|
+
activeAssignments?: Array<{
|
|
411
|
+
id: string;
|
|
412
|
+
nodeId?: string;
|
|
413
|
+
sessionId?: string;
|
|
414
|
+
message: string;
|
|
415
|
+
}>;
|
|
397
416
|
};
|
|
398
417
|
}
|
|
399
418
|
|
|
@@ -435,10 +454,29 @@ export interface CompactSessionEntry {
|
|
|
435
454
|
summaryMetadata?: ProviderSummaryMetadata;
|
|
436
455
|
settings?: Record<string, any>;
|
|
437
456
|
meshQueueStats?: {
|
|
457
|
+
total?: number;
|
|
458
|
+
active?: number;
|
|
459
|
+
historical?: number;
|
|
438
460
|
pending: number;
|
|
439
461
|
assigned: number;
|
|
440
462
|
completed: number;
|
|
441
463
|
failed: number;
|
|
464
|
+
cancelled?: number;
|
|
465
|
+
activeCounts?: {
|
|
466
|
+
pending: number;
|
|
467
|
+
assigned: number;
|
|
468
|
+
};
|
|
469
|
+
historicalCounts?: {
|
|
470
|
+
completed: number;
|
|
471
|
+
failed: number;
|
|
472
|
+
cancelled: number;
|
|
473
|
+
};
|
|
474
|
+
activeAssignments?: Array<{
|
|
475
|
+
id: string;
|
|
476
|
+
nodeId?: string;
|
|
477
|
+
sessionId?: string;
|
|
478
|
+
message: string;
|
|
479
|
+
}>;
|
|
442
480
|
};
|
|
443
481
|
}
|
|
444
482
|
|