@adhdev/daemon-core 0.9.77-rc.8 → 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.
Files changed (50) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +3 -0
  2. package/dist/cli-adapters/provider-cli-adapter.d.ts +2 -0
  3. package/dist/commands/mesh-coordinator.d.ts +10 -0
  4. package/dist/commands/router.d.ts +4 -1
  5. package/dist/config/mesh-config.d.ts +1 -0
  6. package/dist/git/git-worktree.d.ts +15 -2
  7. package/dist/index.d.ts +10 -6
  8. package/dist/index.js +2116 -299
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +2101 -299
  11. package/dist/index.mjs.map +1 -1
  12. package/dist/mesh/mesh-events.d.ts +14 -7
  13. package/dist/mesh/mesh-ledger-reconciliation.d.ts +55 -0
  14. package/dist/mesh/mesh-ledger.d.ts +84 -4
  15. package/dist/mesh/mesh-sync.d.ts +4 -12
  16. package/dist/mesh/mesh-visualization.d.ts +70 -0
  17. package/dist/mesh/mesh-work-queue.d.ts +58 -1
  18. package/dist/mesh/p2p-relay-failure.d.ts +35 -0
  19. package/dist/providers/chat-message-normalization.d.ts +1 -0
  20. package/dist/providers/cli-provider-instance.d.ts +6 -0
  21. package/dist/repo-mesh-types.d.ts +2 -0
  22. package/dist/shared-types.d.ts +38 -0
  23. package/package.json +1 -1
  24. package/src/boot/daemon-lifecycle.ts +5 -0
  25. package/src/cli-adapters/provider-cli-adapter.ts +30 -5
  26. package/src/commands/cli-manager.ts +0 -4
  27. package/src/commands/mesh-coordinator.ts +55 -7
  28. package/src/commands/router.ts +964 -26
  29. package/src/commands/stream-commands.ts +8 -1
  30. package/src/config/config.ts +2 -1
  31. package/src/config/mesh-config.ts +2 -0
  32. package/src/config/workspaces.ts +1 -1
  33. package/src/git/git-worktree.ts +56 -4
  34. package/src/index.d.ts +3 -0
  35. package/src/index.ts +29 -6
  36. package/src/mesh/coordinator-prompt.ts +21 -10
  37. package/src/mesh/mesh-events.ts +532 -22
  38. package/src/mesh/mesh-ledger-reconciliation.ts +115 -0
  39. package/src/mesh/mesh-ledger.ts +209 -8
  40. package/src/mesh/mesh-sync.ts +4 -34
  41. package/src/mesh/mesh-visualization.ts +341 -0
  42. package/src/mesh/mesh-work-queue.ts +183 -17
  43. package/src/mesh/p2p-relay-failure.ts +152 -0
  44. package/src/providers/acp-provider-instance.ts +2 -1
  45. package/src/providers/chat-message-normalization.ts +32 -0
  46. package/src/providers/cli-provider-instance.ts +155 -31
  47. package/src/providers/extension-provider-instance.ts +2 -1
  48. package/src/providers/ide-provider-instance.ts +2 -2
  49. package/src/repo-mesh-types.ts +2 -0
  50. 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 ((parsedStatus?.status === 'generating' || parsedStatus?.status === 'long_generating')) {
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: parseErrorMessage
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: { chatTitle: string; duration: number; timestamp: number } | null = null;
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 wait 3s, if still idle then emit
815
- if (this.completedDebounceTimer) clearTimeout(this.completedDebounceTimer);
816
- this.completedDebouncePending = { chatTitle, duration, timestamp: now };
817
- this.completedDebounceTimer = setTimeout(() => {
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
 
@@ -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 */
@@ -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