@adhdev/daemon-core 0.9.77-rc.50 → 0.9.77-rc.52
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/index.d.ts +4 -1
- package/dist/index.js +282 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +278 -9
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events.d.ts +4 -0
- package/dist/mesh/mesh-visualization.d.ts +70 -0
- package/dist/mesh/mesh-work-queue.d.ts +2 -0
- package/dist/providers/chat-message-normalization.d.ts +1 -0
- package/package.json +1 -1
- package/src/index.ts +6 -1
- package/src/mesh/mesh-events.ts +10 -0
- package/src/mesh/mesh-visualization.ts +341 -0
- package/src/mesh/mesh-work-queue.ts +20 -7
- 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 +2 -1
- package/src/providers/extension-provider-instance.ts +2 -1
- package/src/providers/ide-provider-instance.ts +2 -2
|
@@ -8,6 +8,10 @@ export interface PendingMeshCoordinatorEvent {
|
|
|
8
8
|
}
|
|
9
9
|
/** Drain and return all pending coordinator events, clearing the queue. */
|
|
10
10
|
export declare function drainPendingMeshCoordinatorEvents(): PendingMeshCoordinatorEvent[];
|
|
11
|
+
/** Peek at pending coordinator events without draining (non-destructive). */
|
|
12
|
+
export declare function getPendingMeshCoordinatorEvents(): readonly PendingMeshCoordinatorEvent[];
|
|
13
|
+
/** Explicitly clear all pending coordinator events. */
|
|
14
|
+
export declare function clearPendingMeshCoordinatorEvents(): void;
|
|
11
15
|
export declare function tryAssignQueueTask(components: DaemonComponents, meshId: string, nodeId: string, sessionId: string, providerType: string): boolean;
|
|
12
16
|
/**
|
|
13
17
|
* Triggers a queue check for all nodes in the mesh.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mesh Visualization — Transform RepoMeshStatus into a graph structure
|
|
3
|
+
* for SVG/Canvas rendering without external dependencies.
|
|
4
|
+
*/
|
|
5
|
+
import type { RepoMeshStatus, RepoMeshNodeStatus, RepoMeshNodeHealth } from '../repo-mesh-types.js';
|
|
6
|
+
export type MeshGraphNodeType = 'defaultBranchNode' | 'worktreeNode' | 'orphanNode';
|
|
7
|
+
export type MeshGraphEdgeType = 'parentBranch' | 'worktreeLink' | 'sessionLink';
|
|
8
|
+
export interface MeshGraphNode {
|
|
9
|
+
id: string;
|
|
10
|
+
type: MeshGraphNodeType;
|
|
11
|
+
label: string;
|
|
12
|
+
workspace: string;
|
|
13
|
+
branch: string | null;
|
|
14
|
+
health: RepoMeshNodeHealth;
|
|
15
|
+
ahead: number;
|
|
16
|
+
behind: number;
|
|
17
|
+
dirty: boolean;
|
|
18
|
+
dirtyFiles: number;
|
|
19
|
+
hasConflicts: boolean;
|
|
20
|
+
activeSessionCount: number;
|
|
21
|
+
activeSessions: string[];
|
|
22
|
+
providers: string[];
|
|
23
|
+
isOrphan: boolean;
|
|
24
|
+
orphanReasons: string[];
|
|
25
|
+
/** Next-step hint from convergence analysis */
|
|
26
|
+
nextStepHint?: string;
|
|
27
|
+
/** Original node status for drill-down */
|
|
28
|
+
source: RepoMeshNodeStatus;
|
|
29
|
+
}
|
|
30
|
+
export interface MeshGraphEdge {
|
|
31
|
+
id: string;
|
|
32
|
+
source: string;
|
|
33
|
+
target: string;
|
|
34
|
+
type: MeshGraphEdgeType;
|
|
35
|
+
label?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface MeshGraph {
|
|
38
|
+
meshId: string;
|
|
39
|
+
meshName: string;
|
|
40
|
+
repoIdentity: string;
|
|
41
|
+
refreshedAt: string;
|
|
42
|
+
nodes: MeshGraphNode[];
|
|
43
|
+
edges: MeshGraphEdge[];
|
|
44
|
+
/** Summary statistics */
|
|
45
|
+
stats: {
|
|
46
|
+
totalNodes: number;
|
|
47
|
+
onlineNodes: number;
|
|
48
|
+
dirtyNodes: number;
|
|
49
|
+
orphanNodes: number;
|
|
50
|
+
errorNodes: number;
|
|
51
|
+
offlineNodes: number;
|
|
52
|
+
totalActiveSessions: number;
|
|
53
|
+
};
|
|
54
|
+
/** Global orphan / stale warnings */
|
|
55
|
+
warnings: string[];
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Build a visualization graph from RepoMeshStatus.
|
|
59
|
+
*
|
|
60
|
+
* Nodes:
|
|
61
|
+
* - defaultBranchNode: the mesh's default branch (aggregated from nodes on that branch)
|
|
62
|
+
* - worktreeNode: a node on a feature / worktree branch
|
|
63
|
+
* - orphanNode: a node with orphanReasons (upstream missing, detached HEAD, etc.)
|
|
64
|
+
*
|
|
65
|
+
* Edges:
|
|
66
|
+
* - parentBranch: default branch → worktree branch (when branch name differs)
|
|
67
|
+
* - worktreeLink: links nodes that share the same branch (clustering hint)
|
|
68
|
+
* - sessionLink: node → session (lightweight, optional; not rendered as primary edge)
|
|
69
|
+
*/
|
|
70
|
+
export declare function buildMeshGraph(status: RepoMeshStatus): MeshGraph;
|
|
@@ -32,6 +32,8 @@ export interface MeshWorkQueueEntry {
|
|
|
32
32
|
sessionId?: string;
|
|
33
33
|
updatedAt: string;
|
|
34
34
|
};
|
|
35
|
+
/** ISO timestamp when the task was dispatched (assigned) to a node/session. Used for precise matching on completion. */
|
|
36
|
+
dispatchTimestamp?: string;
|
|
35
37
|
createdAt: string;
|
|
36
38
|
updatedAt: string;
|
|
37
39
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChatMessage } from '../types.js';
|
|
2
|
+
export declare function extractFinalSummaryFromMessages(messages: ChatMessage[] | null | undefined, maxChars?: number): string;
|
|
2
3
|
export declare const BUILTIN_CHAT_MESSAGE_KINDS: readonly ["standard", "thought", "tool", "terminal", "system"];
|
|
3
4
|
export type BuiltinChatMessageKind = typeof BUILTIN_CHAT_MESSAGE_KINDS[number];
|
|
4
5
|
export type ChatMessageKind = BuiltinChatMessageKind | (string & {});
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -159,8 +159,13 @@ export type { MeshLedgerReconciliationEvidence, MeshLedgerReplicaEvidence, MeshL
|
|
|
159
159
|
export { enqueueTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats } from './mesh/mesh-work-queue.js';
|
|
160
160
|
export type { MeshWorkQueueEntry, MeshTaskStatus, MeshWorkQueueStats } from './mesh/mesh-work-queue.js';
|
|
161
161
|
|
|
162
|
+
// ── Mesh Visualization ──
|
|
163
|
+
export { buildMeshGraph } from './mesh/mesh-visualization.js';
|
|
164
|
+
export type { MeshGraph, MeshGraphNode, MeshGraphEdge, MeshGraphNodeType, MeshGraphEdgeType } from './mesh/mesh-visualization.js';
|
|
165
|
+
|
|
162
166
|
// ── Mesh Events ──
|
|
163
|
-
export { triggerMeshQueue } from './mesh/mesh-events.js';
|
|
167
|
+
export { triggerMeshQueue, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, clearPendingMeshCoordinatorEvents } from './mesh/mesh-events.js';
|
|
168
|
+
export type { PendingMeshCoordinatorEvent } from './mesh/mesh-events.js';
|
|
164
169
|
|
|
165
170
|
// ── Mesh P2P Relay Failure Classification ──
|
|
166
171
|
export {
|
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -44,6 +44,16 @@ export function drainPendingMeshCoordinatorEvents(): PendingMeshCoordinatorEvent
|
|
|
44
44
|
return pendingMeshCoordinatorEvents.splice(0);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/** Peek at pending coordinator events without draining (non-destructive). */
|
|
48
|
+
export function getPendingMeshCoordinatorEvents(): readonly PendingMeshCoordinatorEvent[] {
|
|
49
|
+
return pendingMeshCoordinatorEvents.slice();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Explicitly clear all pending coordinator events. */
|
|
53
|
+
export function clearPendingMeshCoordinatorEvents(): void {
|
|
54
|
+
pendingMeshCoordinatorEvents.splice(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
47
57
|
function readNonEmptyString(value: unknown): string {
|
|
48
58
|
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
49
59
|
}
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mesh Visualization — Transform RepoMeshStatus into a graph structure
|
|
3
|
+
* for SVG/Canvas rendering without external dependencies.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { RepoMeshStatus, RepoMeshNodeStatus, RepoMeshNodeHealth } from '../repo-mesh-types.js';
|
|
7
|
+
import type { GitRepoStatus } from '../git/git-types.js';
|
|
8
|
+
|
|
9
|
+
// ─── Graph Types ─────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export type MeshGraphNodeType = 'defaultBranchNode' | 'worktreeNode' | 'orphanNode';
|
|
12
|
+
export type MeshGraphEdgeType = 'parentBranch' | 'worktreeLink' | 'sessionLink';
|
|
13
|
+
|
|
14
|
+
export interface MeshGraphNode {
|
|
15
|
+
id: string;
|
|
16
|
+
type: MeshGraphNodeType;
|
|
17
|
+
label: string;
|
|
18
|
+
workspace: string;
|
|
19
|
+
branch: string | null;
|
|
20
|
+
health: RepoMeshNodeHealth;
|
|
21
|
+
ahead: number;
|
|
22
|
+
behind: number;
|
|
23
|
+
dirty: boolean;
|
|
24
|
+
dirtyFiles: number;
|
|
25
|
+
hasConflicts: boolean;
|
|
26
|
+
activeSessionCount: number;
|
|
27
|
+
activeSessions: string[];
|
|
28
|
+
providers: string[];
|
|
29
|
+
isOrphan: boolean;
|
|
30
|
+
orphanReasons: string[];
|
|
31
|
+
/** Next-step hint from convergence analysis */
|
|
32
|
+
nextStepHint?: string;
|
|
33
|
+
/** Original node status for drill-down */
|
|
34
|
+
source: RepoMeshNodeStatus;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface MeshGraphEdge {
|
|
38
|
+
id: string;
|
|
39
|
+
source: string;
|
|
40
|
+
target: string;
|
|
41
|
+
type: MeshGraphEdgeType;
|
|
42
|
+
label?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface MeshGraph {
|
|
46
|
+
meshId: string;
|
|
47
|
+
meshName: string;
|
|
48
|
+
repoIdentity: string;
|
|
49
|
+
refreshedAt: string;
|
|
50
|
+
nodes: MeshGraphNode[];
|
|
51
|
+
edges: MeshGraphEdge[];
|
|
52
|
+
/** Summary statistics */
|
|
53
|
+
stats: {
|
|
54
|
+
totalNodes: number;
|
|
55
|
+
onlineNodes: number;
|
|
56
|
+
dirtyNodes: number;
|
|
57
|
+
orphanNodes: number;
|
|
58
|
+
errorNodes: number;
|
|
59
|
+
offlineNodes: number;
|
|
60
|
+
totalActiveSessions: number;
|
|
61
|
+
};
|
|
62
|
+
/** Global orphan / stale warnings */
|
|
63
|
+
warnings: string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─── Helpers ─────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
function isDirty(git?: GitRepoStatus): boolean {
|
|
69
|
+
if (!git) return false;
|
|
70
|
+
return (git.staged + git.modified + git.untracked + git.deleted + git.renamed) > 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function dirtyFileCount(git?: GitRepoStatus): number {
|
|
74
|
+
if (!git) return 0;
|
|
75
|
+
return git.staged + git.modified + git.untracked + git.deleted + git.renamed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function detectOrphanReasons(node: RepoMeshNodeStatus, defaultBranch?: string | null): string[] {
|
|
79
|
+
const reasons: string[] = [];
|
|
80
|
+
const git = node.git;
|
|
81
|
+
|
|
82
|
+
if (!git) {
|
|
83
|
+
reasons.push('No git status available');
|
|
84
|
+
return reasons;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!git.isGitRepo) {
|
|
88
|
+
reasons.push('Not a git repository');
|
|
89
|
+
return reasons;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Detached HEAD
|
|
93
|
+
if (git.branch === null && git.headCommit) {
|
|
94
|
+
reasons.push('Detached HEAD');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// No upstream tracking
|
|
98
|
+
if (git.branch && !git.upstream) {
|
|
99
|
+
// Local-only branch — orphan if not default
|
|
100
|
+
if (defaultBranch && git.branch !== defaultBranch) {
|
|
101
|
+
reasons.push(`No upstream: ${git.branch}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Worktree branch with no upstream
|
|
106
|
+
if (git.branch && git.upstream === null && defaultBranch && git.branch !== defaultBranch) {
|
|
107
|
+
if (!reasons.includes(`No upstream: ${git.branch}`)) {
|
|
108
|
+
reasons.push(`No upstream: ${git.branch}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Stale assigned tasks would be detected at queue level, not per node git status
|
|
113
|
+
// We surface node-level errors as orphan indicators
|
|
114
|
+
if (node.error) {
|
|
115
|
+
reasons.push(`Error: ${node.error}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return reasons;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function nodeHealthPriority(health: RepoMeshNodeHealth): number {
|
|
122
|
+
switch (health) {
|
|
123
|
+
case 'online': return 0;
|
|
124
|
+
case 'dirty': return 1;
|
|
125
|
+
case 'degraded': return 2;
|
|
126
|
+
case 'wrong_branch': return 3;
|
|
127
|
+
case 'offline': return 4;
|
|
128
|
+
case 'unknown': return 5;
|
|
129
|
+
default: return 5;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function pickDominantHealth(healths: RepoMeshNodeHealth[]): RepoMeshNodeHealth {
|
|
134
|
+
if (healths.length === 0) return 'unknown';
|
|
135
|
+
return healths.reduce((best, h) =>
|
|
136
|
+
nodeHealthPriority(h) > nodeHealthPriority(best) ? h : best
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ─── Graph Builder ─────────────────────────────────
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Build a visualization graph from RepoMeshStatus.
|
|
144
|
+
*
|
|
145
|
+
* Nodes:
|
|
146
|
+
* - defaultBranchNode: the mesh's default branch (aggregated from nodes on that branch)
|
|
147
|
+
* - worktreeNode: a node on a feature / worktree branch
|
|
148
|
+
* - orphanNode: a node with orphanReasons (upstream missing, detached HEAD, etc.)
|
|
149
|
+
*
|
|
150
|
+
* Edges:
|
|
151
|
+
* - parentBranch: default branch → worktree branch (when branch name differs)
|
|
152
|
+
* - worktreeLink: links nodes that share the same branch (clustering hint)
|
|
153
|
+
* - sessionLink: node → session (lightweight, optional; not rendered as primary edge)
|
|
154
|
+
*/
|
|
155
|
+
export function buildMeshGraph(status: RepoMeshStatus): MeshGraph {
|
|
156
|
+
const nodes: MeshGraphNode[] = [];
|
|
157
|
+
const edges: MeshGraphEdge[] = [];
|
|
158
|
+
const warnings: string[] = [];
|
|
159
|
+
|
|
160
|
+
// Track branch → nodes for edge creation
|
|
161
|
+
const branchToNodeIds = new Map<string, string[]>();
|
|
162
|
+
|
|
163
|
+
for (const nodeStatus of status.nodes) {
|
|
164
|
+
const git = nodeStatus.git;
|
|
165
|
+
const branch = git?.branch || null;
|
|
166
|
+
const orphanReasons = detectOrphanReasons(nodeStatus, null);
|
|
167
|
+
const dirty = isDirty(git);
|
|
168
|
+
const dfc = dirtyFileCount(git);
|
|
169
|
+
|
|
170
|
+
// Determine node type
|
|
171
|
+
let type: MeshGraphNodeType = 'worktreeNode';
|
|
172
|
+
if (orphanReasons.length > 0) {
|
|
173
|
+
type = 'orphanNode';
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const graphNode: MeshGraphNode = {
|
|
177
|
+
id: nodeStatus.nodeId,
|
|
178
|
+
type,
|
|
179
|
+
label: nodeStatus.machineLabel || nodeStatus.nodeId.slice(0, 8),
|
|
180
|
+
workspace: nodeStatus.workspace,
|
|
181
|
+
branch,
|
|
182
|
+
health: nodeStatus.health,
|
|
183
|
+
ahead: git?.ahead ?? 0,
|
|
184
|
+
behind: git?.behind ?? 0,
|
|
185
|
+
dirty,
|
|
186
|
+
dirtyFiles: dfc,
|
|
187
|
+
hasConflicts: git?.hasConflicts ?? false,
|
|
188
|
+
activeSessionCount: nodeStatus.activeSessions?.length ?? 0,
|
|
189
|
+
activeSessions: nodeStatus.activeSessions ?? [],
|
|
190
|
+
providers: nodeStatus.providers ?? [],
|
|
191
|
+
isOrphan: orphanReasons.length > 0,
|
|
192
|
+
orphanReasons,
|
|
193
|
+
source: nodeStatus,
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
nodes.push(graphNode);
|
|
197
|
+
|
|
198
|
+
if (branch) {
|
|
199
|
+
const list = branchToNodeIds.get(branch) ?? [];
|
|
200
|
+
list.push(graphNode.id);
|
|
201
|
+
branchToNodeIds.set(branch, list);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Create synthetic default branch node if we can infer one
|
|
206
|
+
// Heuristic: most common branch among nodes with upstream
|
|
207
|
+
const branchUpstreamCounts = new Map<string, number>();
|
|
208
|
+
for (const n of status.nodes) {
|
|
209
|
+
const b = n.git?.branch;
|
|
210
|
+
const upstream = n.git?.upstream;
|
|
211
|
+
if (b && upstream) {
|
|
212
|
+
branchUpstreamCounts.set(b, (branchUpstreamCounts.get(b) ?? 0) + 1);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
let inferredDefaultBranch: string | null = null;
|
|
216
|
+
let bestCount = 0;
|
|
217
|
+
for (const [b, count] of branchUpstreamCounts) {
|
|
218
|
+
if (count > bestCount) {
|
|
219
|
+
bestCount = count;
|
|
220
|
+
inferredDefaultBranch = b;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// If we have a clear default branch, create a synthetic node for it
|
|
225
|
+
const defaultBranchNodeId = inferredDefaultBranch
|
|
226
|
+
? `__branch_${inferredDefaultBranch}`
|
|
227
|
+
: null;
|
|
228
|
+
|
|
229
|
+
if (defaultBranchNodeId && inferredDefaultBranch) {
|
|
230
|
+
const branchNodes = branchToNodeIds.get(inferredDefaultBranch) ?? [];
|
|
231
|
+
const branchHealths = branchNodes.map(id =>
|
|
232
|
+
nodes.find(n => n.id === id)!.health
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const defaultNode: MeshGraphNode = {
|
|
236
|
+
id: defaultBranchNodeId,
|
|
237
|
+
type: 'defaultBranchNode',
|
|
238
|
+
label: inferredDefaultBranch,
|
|
239
|
+
workspace: '',
|
|
240
|
+
branch: inferredDefaultBranch,
|
|
241
|
+
health: pickDominantHealth(branchHealths),
|
|
242
|
+
ahead: 0,
|
|
243
|
+
behind: 0,
|
|
244
|
+
dirty: false,
|
|
245
|
+
dirtyFiles: 0,
|
|
246
|
+
hasConflicts: false,
|
|
247
|
+
activeSessionCount: 0,
|
|
248
|
+
activeSessions: [],
|
|
249
|
+
providers: [],
|
|
250
|
+
isOrphan: false,
|
|
251
|
+
orphanReasons: [],
|
|
252
|
+
nextStepHint: branchNodes.length > 0 ? `${branchNodes.length} node(s) on default branch` : undefined,
|
|
253
|
+
source: {
|
|
254
|
+
nodeId: defaultBranchNodeId,
|
|
255
|
+
machineLabel: inferredDefaultBranch,
|
|
256
|
+
workspace: '',
|
|
257
|
+
health: 'online',
|
|
258
|
+
providers: [],
|
|
259
|
+
activeSessions: [],
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
nodes.push(defaultNode);
|
|
263
|
+
|
|
264
|
+
// Edge: default branch → each worktree branch that is NOT the default
|
|
265
|
+
const seenBranches = new Set<string>();
|
|
266
|
+
for (const n of nodes) {
|
|
267
|
+
if (n.type === 'defaultBranchNode') continue;
|
|
268
|
+
if (!n.branch) continue;
|
|
269
|
+
if (n.branch === inferredDefaultBranch) {
|
|
270
|
+
// Link worktree nodes on default branch directly to default branch node
|
|
271
|
+
edges.push({
|
|
272
|
+
id: `${defaultBranchNodeId}--${n.id}`,
|
|
273
|
+
source: defaultBranchNodeId,
|
|
274
|
+
target: n.id,
|
|
275
|
+
type: 'parentBranch',
|
|
276
|
+
label: 'default',
|
|
277
|
+
});
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (seenBranches.has(n.branch)) continue;
|
|
281
|
+
seenBranches.add(n.branch);
|
|
282
|
+
edges.push({
|
|
283
|
+
id: `${defaultBranchNodeId}--branch_${n.branch}`,
|
|
284
|
+
source: defaultBranchNodeId,
|
|
285
|
+
target: n.branch,
|
|
286
|
+
type: 'parentBranch',
|
|
287
|
+
label: n.branch,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// worktreeLink edges: connect nodes sharing the same branch
|
|
293
|
+
for (const [branch, ids] of branchToNodeIds) {
|
|
294
|
+
if (ids.length < 2) continue;
|
|
295
|
+
for (let i = 1; i < ids.length; i++) {
|
|
296
|
+
edges.push({
|
|
297
|
+
id: `wt_${ids[0]}--${ids[i]}`,
|
|
298
|
+
source: ids[0],
|
|
299
|
+
target: ids[i],
|
|
300
|
+
type: 'worktreeLink',
|
|
301
|
+
label: branch,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Collect warnings
|
|
307
|
+
const orphanCount = nodes.filter(n => n.isOrphan).length;
|
|
308
|
+
if (orphanCount > 0) {
|
|
309
|
+
warnings.push(`${orphanCount} orphan node(s) detected`);
|
|
310
|
+
}
|
|
311
|
+
const conflictCount = nodes.filter(n => n.hasConflicts).length;
|
|
312
|
+
if (conflictCount > 0) {
|
|
313
|
+
warnings.push(`${conflictCount} node(s) with merge conflicts`);
|
|
314
|
+
}
|
|
315
|
+
const offlineCount = nodes.filter(n => n.health === 'offline').length;
|
|
316
|
+
if (offlineCount > 0) {
|
|
317
|
+
warnings.push(`${offlineCount} node(s) offline`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Stats
|
|
321
|
+
const stats = {
|
|
322
|
+
totalNodes: status.nodes.length,
|
|
323
|
+
onlineNodes: status.nodes.filter(n => n.health === 'online').length,
|
|
324
|
+
dirtyNodes: nodes.filter(n => n.dirty).length,
|
|
325
|
+
orphanNodes: orphanCount,
|
|
326
|
+
errorNodes: status.nodes.filter(n => !!n.error).length,
|
|
327
|
+
offlineNodes: offlineCount,
|
|
328
|
+
totalActiveSessions: status.nodes.reduce((sum, n) => sum + (n.activeSessions?.length ?? 0), 0),
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
meshId: status.meshId,
|
|
333
|
+
meshName: status.meshName,
|
|
334
|
+
repoIdentity: status.repoIdentity,
|
|
335
|
+
refreshedAt: status.refreshedAt,
|
|
336
|
+
nodes,
|
|
337
|
+
edges,
|
|
338
|
+
stats,
|
|
339
|
+
warnings,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
@@ -39,6 +39,8 @@ export interface MeshWorkQueueEntry {
|
|
|
39
39
|
sessionId?: string;
|
|
40
40
|
updatedAt: string;
|
|
41
41
|
};
|
|
42
|
+
/** ISO timestamp when the task was dispatched (assigned) to a node/session. Used for precise matching on completion. */
|
|
43
|
+
dispatchTimestamp?: string;
|
|
42
44
|
createdAt: string;
|
|
43
45
|
updatedAt: string;
|
|
44
46
|
}
|
|
@@ -132,6 +134,7 @@ export function claimNextTask(meshId: string, nodeId: string, sessionId: string)
|
|
|
132
134
|
entry.status = 'assigned';
|
|
133
135
|
entry.assignedNodeId = nodeId;
|
|
134
136
|
entry.assignedSessionId = sessionId;
|
|
137
|
+
entry.dispatchTimestamp = new Date().toISOString();
|
|
135
138
|
entry.updatedAt = new Date().toISOString();
|
|
136
139
|
|
|
137
140
|
writeQueue(meshId, queue);
|
|
@@ -243,17 +246,27 @@ export function updateSessionTaskStatus(
|
|
|
243
246
|
status: MeshTaskStatus,
|
|
244
247
|
): MeshWorkQueueEntry | null {
|
|
245
248
|
const queue = readQueue(meshId);
|
|
246
|
-
//
|
|
247
|
-
//
|
|
249
|
+
// Collect all assigned tasks for this session, then pick the one with the
|
|
250
|
+
// most recent dispatchTimestamp (or updatedAt fallback for legacy entries).
|
|
251
|
+
// This prevents completing the wrong task when multiple tasks were assigned
|
|
252
|
+
// to the same session in rapid succession.
|
|
253
|
+
let bestIdx = -1;
|
|
254
|
+
let bestTime = 0;
|
|
248
255
|
for (let i = queue.length - 1; i >= 0; i--) {
|
|
249
256
|
if (queue[i].assignedSessionId === sessionId && queue[i].status === 'assigned') {
|
|
250
|
-
queue[i].
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
257
|
+
const time = new Date(queue[i].dispatchTimestamp || queue[i].updatedAt).getTime();
|
|
258
|
+
if (time > bestTime) {
|
|
259
|
+
bestTime = time;
|
|
260
|
+
bestIdx = i;
|
|
261
|
+
}
|
|
254
262
|
}
|
|
255
263
|
}
|
|
256
|
-
return null;
|
|
264
|
+
if (bestIdx === -1) return null;
|
|
265
|
+
|
|
266
|
+
queue[bestIdx].status = status;
|
|
267
|
+
queue[bestIdx].updatedAt = new Date().toISOString();
|
|
268
|
+
writeQueue(meshId, queue);
|
|
269
|
+
return queue[bestIdx];
|
|
257
270
|
}
|
|
258
271
|
|
|
259
272
|
export interface MeshWorkQueueStats {
|
|
@@ -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;
|
|
@@ -841,6 +841,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
841
841
|
chatTitle: pending.chatTitle,
|
|
842
842
|
duration: pending.duration,
|
|
843
843
|
timestamp: pending.timestamp,
|
|
844
|
+
finalSummary: extractFinalSummaryFromMessages(this.adapter?.getScriptParsedStatus()?.messages),
|
|
844
845
|
});
|
|
845
846
|
this.completedDebouncePending = null;
|
|
846
847
|
this.completedDebounceTimer = 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
|
|