@adhdev/daemon-core 0.9.76-rc.3 → 0.9.76-rc.30

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 (41) hide show
  1. package/dist/cli-adapters/provider-cli-adapter.d.ts +2 -1
  2. package/dist/cli-adapters/provider-cli-runtime.d.ts +1 -0
  3. package/dist/commands/cli-manager.d.ts +17 -4
  4. package/dist/commands/mesh-coordinator.d.ts +2 -0
  5. package/dist/commands/router.d.ts +4 -0
  6. package/dist/config/mesh-config.d.ts +3 -0
  7. package/dist/git/git-types.d.ts +1 -1
  8. package/dist/git/git-worktree.d.ts +64 -0
  9. package/dist/git/index.d.ts +2 -0
  10. package/dist/index.js +1033 -375
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +1060 -407
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/mesh/coordinator-prompt.d.ts +1 -0
  15. package/dist/providers/cli-provider-instance.d.ts +3 -0
  16. package/dist/providers/provider-instance-manager.d.ts +1 -0
  17. package/dist/providers/provider-instance.d.ts +2 -0
  18. package/dist/repo-mesh-types.d.ts +6 -0
  19. package/dist/shared-types.d.ts +22 -1
  20. package/package.json +3 -4
  21. package/src/cli-adapters/provider-cli-adapter.ts +6 -3
  22. package/src/cli-adapters/provider-cli-runtime.ts +3 -2
  23. package/src/commands/chat-commands.ts +50 -5
  24. package/src/commands/cli-manager.ts +78 -5
  25. package/src/commands/handler.ts +13 -4
  26. package/src/commands/mesh-coordinator.ts +149 -6
  27. package/src/commands/router.ts +319 -32
  28. package/src/config/mesh-config.ts +6 -0
  29. package/src/git/git-commands.ts +5 -1
  30. package/src/git/git-types.ts +1 -0
  31. package/src/git/git-worktree.ts +214 -0
  32. package/src/git/index.ts +14 -0
  33. package/src/mesh/coordinator-prompt.ts +25 -10
  34. package/src/providers/cli-provider-instance.d.ts +2 -0
  35. package/src/providers/cli-provider-instance.ts +55 -7
  36. package/src/providers/provider-instance-manager.ts +20 -1
  37. package/src/providers/provider-instance.ts +2 -0
  38. package/src/repo-mesh-types.ts +6 -0
  39. package/src/shared-types.ts +24 -1
  40. package/src/status/builders.ts +17 -12
  41. package/src/status/reporter.ts +6 -0
@@ -160,9 +160,12 @@ export function deleteMesh(meshId: string): boolean {
160
160
  export interface AddNodeOptions {
161
161
  workspace: string;
162
162
  repoRoot?: string;
163
+ daemonId?: string;
163
164
  userOverrides?: Partial<RepoMeshNodeCapabilities>;
164
165
  policy?: RepoMeshNodePolicy;
165
166
  isLocalWorktree?: boolean;
167
+ worktreeBranch?: string;
168
+ clonedFromNodeId?: string;
166
169
  }
167
170
 
168
171
  export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntry | undefined {
@@ -183,9 +186,12 @@ export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntr
183
186
  id: `node_${randomUUID().replace(/-/g, '')}`,
184
187
  workspace: opts.workspace.trim(),
185
188
  repoRoot: opts.repoRoot,
189
+ daemonId: opts.daemonId,
186
190
  userOverrides: opts.userOverrides || {},
187
191
  policy: opts.policy || {},
188
192
  isLocalWorktree: opts.isLocalWorktree,
193
+ worktreeBranch: opts.worktreeBranch,
194
+ clonedFromNodeId: opts.clonedFromNodeId,
189
195
  };
190
196
 
191
197
  mesh.nodes.push(node);
@@ -152,6 +152,7 @@ const FAILURE_REASONS = new Set<GitFailureReason>([
152
152
  'dirty_index_required',
153
153
  'conflict',
154
154
  'invalid_args',
155
+ 'nothing_to_commit',
155
156
  'git_command_failed',
156
157
  ]);
157
158
 
@@ -454,7 +455,10 @@ async function gitCheckpoint(
454
455
  } catch (err: any) {
455
456
  const output = (err?.stdout || '') + (err?.stderr || '');
456
457
  if (/nothing to commit/i.test(output)) {
457
- throw new GitCommandError('git_command_failed', 'Nothing to commit');
458
+ throw new GitCommandError('nothing_to_commit', 'Nothing to commit — working tree is clean.', {
459
+ stdout: err?.stdout,
460
+ stderr: err?.stderr,
461
+ });
458
462
  }
459
463
  throw err;
460
464
  }
@@ -14,6 +14,7 @@ export type GitFailureReason =
14
14
  | 'dirty_index_required'
15
15
  | 'conflict'
16
16
  | 'invalid_args'
17
+ | 'nothing_to_commit'
17
18
  | 'git_command_failed';
18
19
 
19
20
  export interface GitRepoIdentity {
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Git Worktree — Create/remove/list worktrees for Repo Mesh node cloning
3
+ *
4
+ * Used by the `clone_mesh_node` daemon command to create isolated
5
+ * worktree-based nodes for parallel branch work within a mesh.
6
+ *
7
+ * Worktrees are placed outside the source repo to avoid .gitignore
8
+ * pollution and submodule conflicts:
9
+ * <repoParent>/.adhdev-worktrees/<meshName>/<branch>/
10
+ */
11
+
12
+ import * as path from 'node:path';
13
+ import { mkdir } from 'node:fs/promises';
14
+ import { existsSync } from 'node:fs';
15
+ import { execFile } from 'node:child_process';
16
+ import { promisify } from 'node:util';
17
+
18
+ const execFileAsync = promisify(execFile);
19
+
20
+ const WORKTREE_DIR_NAME = '.adhdev-worktrees';
21
+ const GIT_TIMEOUT_MS = 30_000;
22
+ const GIT_MAX_BUFFER = 4 * 1024 * 1024;
23
+
24
+ // ─── Types ──────────────────────────────────────
25
+
26
+ export interface WorktreeCreateOptions {
27
+ /** Absolute path to the source repo's git root */
28
+ repoRoot: string;
29
+ /** Branch name for the new worktree */
30
+ branch: string;
31
+ /** Starting point for the branch (default: HEAD) */
32
+ baseBranch?: string;
33
+ /** Mesh name, used for organizing worktree directories */
34
+ meshName: string;
35
+ /** Override the auto-resolved target directory */
36
+ targetDir?: string;
37
+ }
38
+
39
+ export interface WorktreeCreateResult {
40
+ success: true;
41
+ worktreePath: string;
42
+ branch: string;
43
+ }
44
+
45
+ export interface WorktreeEntry {
46
+ path: string;
47
+ head: string;
48
+ branch: string | null;
49
+ bare: boolean;
50
+ }
51
+
52
+ export interface WorktreeRemoveResult {
53
+ success: true;
54
+ removedPath: string;
55
+ }
56
+
57
+ // ─── Path Resolution ────────────────────────────
58
+
59
+ /**
60
+ * Resolve the target directory for a new worktree.
61
+ * Places worktrees at: <repoParent>/.adhdev-worktrees/<meshName>/<branch>/
62
+ */
63
+ export function resolveWorktreePath(repoRoot: string, meshName: string, branch: string): string {
64
+ // Sanitize branch name for filesystem (e.g. feat/auth → feat-auth)
65
+ const safeBranch = branch.replace(/[/\\:*?"<>|]/g, '-').replace(/^\.+|\.+$/g, '');
66
+ const safeMeshName = meshName.replace(/[/\\:*?"<>|]/g, '-').replace(/^\.+|\.+$/g, '');
67
+ const parentDir = path.dirname(repoRoot);
68
+ return path.join(parentDir, WORKTREE_DIR_NAME, safeMeshName, safeBranch);
69
+ }
70
+
71
+ // ─── Create ─────────────────────────────────────
72
+
73
+ /**
74
+ * Create a new git worktree with a fresh branch.
75
+ *
76
+ * Runs: git worktree add <targetDir> -b <branch> [baseBranch]
77
+ */
78
+ export async function createWorktree(opts: WorktreeCreateOptions): Promise<WorktreeCreateResult> {
79
+ const { repoRoot, branch, baseBranch, meshName } = opts;
80
+ const targetDir = opts.targetDir || resolveWorktreePath(repoRoot, meshName, branch);
81
+
82
+ if (existsSync(targetDir)) {
83
+ throw new Error(`Worktree target directory already exists: ${targetDir}`);
84
+ }
85
+
86
+ // Ensure parent directory exists
87
+ await mkdir(path.dirname(targetDir), { recursive: true });
88
+
89
+ const args = ['worktree', 'add', targetDir, '-b', branch];
90
+ if (baseBranch) {
91
+ args.push(baseBranch);
92
+ }
93
+
94
+ try {
95
+ await execFileAsync('git', args, {
96
+ cwd: repoRoot,
97
+ encoding: 'utf8',
98
+ timeout: GIT_TIMEOUT_MS,
99
+ maxBuffer: GIT_MAX_BUFFER,
100
+ windowsHide: true,
101
+ });
102
+ } catch (error: any) {
103
+ const stderr = typeof error.stderr === 'string' ? error.stderr : '';
104
+ // Clean error messages for common failures
105
+ if (/already exists/i.test(stderr)) {
106
+ throw new Error(`Branch '${branch}' already exists or is checked out in another worktree`);
107
+ }
108
+ throw new Error(`git worktree add failed: ${stderr.trim() || error.message}`);
109
+ }
110
+
111
+ return {
112
+ success: true,
113
+ worktreePath: targetDir,
114
+ branch,
115
+ };
116
+ }
117
+
118
+ // ─── Remove ─────────────────────────────────────
119
+
120
+ /**
121
+ * Remove a git worktree and clean up the directory.
122
+ *
123
+ * Runs: git worktree remove <worktreePath> --force
124
+ */
125
+ export async function removeWorktree(repoRoot: string, worktreePath: string): Promise<WorktreeRemoveResult> {
126
+ if (!existsSync(worktreePath)) {
127
+ // Already gone — just prune
128
+ await pruneWorktrees(repoRoot);
129
+ return { success: true, removedPath: worktreePath };
130
+ }
131
+
132
+ try {
133
+ await execFileAsync('git', ['worktree', 'remove', worktreePath, '--force'], {
134
+ cwd: repoRoot,
135
+ encoding: 'utf8',
136
+ timeout: GIT_TIMEOUT_MS,
137
+ maxBuffer: GIT_MAX_BUFFER,
138
+ windowsHide: true,
139
+ });
140
+ } catch (error: any) {
141
+ const stderr = typeof error.stderr === 'string' ? error.stderr : '';
142
+ throw new Error(`git worktree remove failed: ${stderr.trim() || error.message}`);
143
+ }
144
+
145
+ return { success: true, removedPath: worktreePath };
146
+ }
147
+
148
+ // ─── List ───────────────────────────────────────
149
+
150
+ /**
151
+ * List all worktrees for a repository.
152
+ *
153
+ * Runs: git worktree list --porcelain
154
+ */
155
+ export async function listWorktrees(repoRoot: string): Promise<WorktreeEntry[]> {
156
+ const { stdout } = await execFileAsync('git', ['worktree', 'list', '--porcelain'], {
157
+ cwd: repoRoot,
158
+ encoding: 'utf8',
159
+ timeout: GIT_TIMEOUT_MS,
160
+ maxBuffer: GIT_MAX_BUFFER,
161
+ windowsHide: true,
162
+ });
163
+
164
+ return parseWorktreeListOutput(stdout);
165
+ }
166
+
167
+ /**
168
+ * Parse `git worktree list --porcelain` output into structured entries.
169
+ */
170
+ export function parseWorktreeListOutput(output: string): WorktreeEntry[] {
171
+ const entries: WorktreeEntry[] = [];
172
+ const blocks = output.trim().split(/\n\n+/);
173
+
174
+ for (const block of blocks) {
175
+ if (!block.trim()) continue;
176
+ const lines = block.trim().split('\n');
177
+ const entry: WorktreeEntry = { path: '', head: '', branch: null, bare: false };
178
+
179
+ for (const line of lines) {
180
+ if (line.startsWith('worktree ')) {
181
+ entry.path = line.slice('worktree '.length).trim();
182
+ } else if (line.startsWith('HEAD ')) {
183
+ entry.head = line.slice('HEAD '.length).trim();
184
+ } else if (line.startsWith('branch ')) {
185
+ const ref = line.slice('branch '.length).trim();
186
+ // refs/heads/feat/auth → feat/auth
187
+ entry.branch = ref.replace(/^refs\/heads\//, '');
188
+ } else if (line === 'bare') {
189
+ entry.bare = true;
190
+ }
191
+ }
192
+
193
+ if (entry.path) {
194
+ entries.push(entry);
195
+ }
196
+ }
197
+
198
+ return entries;
199
+ }
200
+
201
+ // ─── Prune ──────────────────────────────────────
202
+
203
+ async function pruneWorktrees(repoRoot: string): Promise<void> {
204
+ try {
205
+ await execFileAsync('git', ['worktree', 'prune'], {
206
+ cwd: repoRoot,
207
+ encoding: 'utf8',
208
+ timeout: GIT_TIMEOUT_MS,
209
+ windowsHide: true,
210
+ });
211
+ } catch {
212
+ // Prune is best-effort
213
+ }
214
+ }
package/src/git/index.ts CHANGED
@@ -73,3 +73,17 @@ export type {
73
73
 
74
74
  export { TurnSnapshotTracker } from './turn-snapshot-tracker.js';
75
75
  export type { TurnCompletedCallback } from './turn-snapshot-tracker.js';
76
+
77
+ export {
78
+ createWorktree,
79
+ listWorktrees,
80
+ parseWorktreeListOutput,
81
+ removeWorktree,
82
+ resolveWorktreePath,
83
+ } from './git-worktree.js';
84
+ export type {
85
+ WorktreeCreateOptions,
86
+ WorktreeCreateResult,
87
+ WorktreeEntry,
88
+ WorktreeRemoveResult,
89
+ } from './git-worktree.js';
@@ -16,6 +16,7 @@ import type {
16
16
  RepoMeshStatus,
17
17
  RepoMeshNodeStatus,
18
18
  } from '../repo-mesh-types.js';
19
+ import { DEFAULT_MESH_POLICY } from '../repo-mesh-types.js';
19
20
 
20
21
  // ─── Prompt Builder ─────────────────────────────
21
22
 
@@ -23,10 +24,11 @@ export interface CoordinatorPromptContext {
23
24
  mesh: LocalMeshEntry;
24
25
  status?: RepoMeshStatus;
25
26
  userInstruction?: string;
27
+ coordinatorCliType?: string;
26
28
  }
27
29
 
28
30
  export function buildCoordinatorSystemPrompt(ctx: CoordinatorPromptContext): string {
29
- const { mesh, status, userInstruction } = ctx;
31
+ const { mesh, status, userInstruction, coordinatorCliType } = ctx;
30
32
  const sections: string[] = [];
31
33
 
32
34
  // ── Identity ──
@@ -45,7 +47,7 @@ Repository: \`${mesh.repoIdentity}\`${mesh.defaultBranch ? `\nDefault branch: \`
45
47
  }
46
48
 
47
49
  // ── Policy ──
48
- sections.push(buildPolicySection(mesh.policy));
50
+ sections.push(buildPolicySection({ ...DEFAULT_MESH_POLICY, ...(mesh.policy || {}) }));
49
51
 
50
52
  // ── Tools ──
51
53
  sections.push(TOOLS_SECTION);
@@ -54,14 +56,14 @@ Repository: \`${mesh.repoIdentity}\`${mesh.defaultBranch ? `\nDefault branch: \`
54
56
  sections.push(WORKFLOW_SECTION);
55
57
 
56
58
  // ── Rules ──
57
- sections.push(RULES_SECTION);
59
+ sections.push(buildRulesSection(coordinatorCliType));
58
60
 
59
61
  // ── User instruction ──
60
62
  if (userInstruction) {
61
63
  sections.push(`## Additional Context\n${userInstruction}`);
62
64
  }
63
65
 
64
- if (mesh.coordinator.systemPromptSuffix) {
66
+ if (mesh.coordinator?.systemPromptSuffix) {
65
67
  sections.push(mesh.coordinator.systemPromptSuffix);
66
68
  }
67
69
 
@@ -130,7 +132,9 @@ const TOOLS_SECTION = `## Available Tools
130
132
  | \`mesh_read_chat\` | Read an agent's recent messages to check progress |
131
133
  | \`mesh_git_status\` | Check git status on a specific node |
132
134
  | \`mesh_checkpoint\` | Create a git checkpoint on a node |
133
- | \`mesh_approve\` | Approve/reject a pending agent action |`;
135
+ | \`mesh_approve\` | Approve/reject a pending agent action |
136
+ | \`mesh_clone_node\` | Create a worktree node for isolated parallel branch work |
137
+ | \`mesh_remove_node\` | Remove a node (cleans up worktree if applicable) |`;
134
138
 
135
139
  const WORKFLOW_SECTION = `## Orchestration Workflow
136
140
 
@@ -138,21 +142,32 @@ const WORKFLOW_SECTION = `## Orchestration Workflow
138
142
  2. **Plan** — Decompose the user's request into independent tasks for parallel execution, or sequential tasks when dependencies exist.
139
143
  3. **Delegate** — For each task:
140
144
  a. Pick the best node (consider: health, dirty state, current workload).
141
- b. If no session exists, call \`mesh_launch_session\` to start one.
142
- c. Call \`mesh_send_task\` with a **complete, self-contained** instruction that includes all context the agent needs (file paths, line numbers, what to change, why). Do not send partial instructions expecting future follow-up.
145
+ b. If you need branch isolation for parallel work, call \`mesh_clone_node\` to create a worktree node first.
146
+ c. If no session exists, call \`mesh_launch_session\` to start one.
147
+ d. Call \`mesh_send_task\` with a **complete, self-contained** instruction that includes all context the agent needs (file paths, line numbers, what to change, why). Do not send partial instructions expecting future follow-up.
143
148
  4. **Monitor** — Periodically call \`mesh_read_chat\` to check progress. Handle approvals via \`mesh_approve\`.
144
149
  5. **Verify** — When a task reports completion, call \`mesh_git_status\` to verify changes were made.
145
150
  6. **Checkpoint** — Call \`mesh_checkpoint\` to save the work.
146
- 7. **Report** — Summarize what was done, what changed, and any issues.`;
151
+ 7. **Clean up** — Remove worktree nodes via \`mesh_remove_node\` after their work is merged or no longer needed.
152
+ 8. **Report** — Summarize what was done, what changed, and any issues.`;
147
153
 
148
- const RULES_SECTION = `## Rules
154
+ function buildRulesSection(coordinatorCliType?: string): string {
155
+ const coordinatorNote = coordinatorCliType
156
+ ? `\n- **Coordinator runtime is not a delegation default.** This coordinator is running as \`${coordinatorCliType}\`, but delegated node sessions must follow the user's requested provider, not the coordinator's own runtime.`
157
+ : '';
158
+
159
+ return `## Rules
149
160
 
150
161
  - **Minimize coordinator context.** The coordinator's job is routing, not implementing. Do not read source files, run commands, or analyze code directly — delegate all of that to node agents. Your context should stay lean.
151
162
  - **Delegate analysis too.** If you need to understand a bug or explore the codebase, send that investigation as a task to a node. Do not do it yourself.
163
+ - **Respect explicit provider requests.** If the user names an agent/provider, pass the matching provider type to \`mesh_launch_session\`: Hermes → \`hermes-cli\`, Claude Code/Claude → \`claude-cli\`, Codex → \`codex-cli\`, Gemini → \`gemini-cli\`. Never substitute \`claude-cli\` just because the coordinator itself is Claude Code.
152
164
  - **Front-load the task message.** When calling \`mesh_send_task\`, include everything the agent needs: what files to touch, what the problem is, what the fix should look like. The agent won't ask follow-up questions.
153
165
  - **Don't inspect code.** Trust the agent's output. Verify via \`mesh_git_status\`, not by reading source files.
154
166
  - **Don't over-parallelize.** Start with 1-2 concurrent tasks. Scale up if they succeed.
155
167
  - **Handle failures gracefully.** If a task fails, read the chat to understand why, then retry or reassign.
156
168
  - **Keep the user informed.** Report progress after each delegation round — one or two sentences, not a narration.
157
169
  - **Respect node capabilities.** Don't send build tasks to read-only nodes. Don't push from nodes that aren't allowed to.
158
- - **Never fabricate tool results.** Always call the actual tool; never pretend you did.`;
170
+ - **Never fabricate tool results.** Always call the actual tool; never pretend you did.
171
+ - **Clean up worktree nodes.** After a worktree task completes and its changes are merged or checkpointed, call \`mesh_remove_node\` to free resources.
172
+ - **Name worktree branches meaningfully.** Use descriptive names like \`feat/auth-refactor\` or \`fix/build-123\`.${coordinatorNote}`;
173
+ }
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import type { ProviderModule } from './contracts.js';
8
8
  import type { ProviderInstance, ProviderState, InstanceContext } from './provider-instance.js';
9
+ import type { ChatMessage } from '../types.js';
9
10
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
10
11
  import type { PtyTransportFactory } from '../cli-adapters/pty-transport.js';
11
12
  export declare class CliProviderInstance implements ProviderInstance {
@@ -77,6 +78,7 @@ export declare class CliProviderInstance implements ProviderInstance {
77
78
  private formatMarkerTimestamp;
78
79
  private maybeAppendRuntimeRecoveryMessage;
79
80
  private appendRuntimeSystemMessage;
81
+ mergeRuntimeChatMessages(parsedMessages: ChatMessage[]): ChatMessage[];
80
82
  private mergeConversationMessages;
81
83
  private formatApprovalRequestMessage;
82
84
  private promoteProviderSessionId;
@@ -214,6 +214,7 @@ export class CliProviderInstance implements ProviderInstance {
214
214
  options?: {
215
215
  providerSessionId?: string;
216
216
  launchMode?: 'new' | 'resume' | 'manual';
217
+ extraEnv?: Record<string, string>;
217
218
  onProviderSessionResolved?: (info: {
218
219
  instanceId: string;
219
220
  providerType: string;
@@ -230,7 +231,7 @@ export class CliProviderInstance implements ProviderInstance {
230
231
  this.providerSessionId = options?.providerSessionId;
231
232
  this.launchMode = options?.launchMode || 'new';
232
233
  this.onProviderSessionResolved = options?.onProviderSessionResolved;
233
- this.adapter = new ProviderCliAdapter(provider as CliProviderModule, workingDir, cliArgs, transportFactory);
234
+ this.adapter = new ProviderCliAdapter(provider as CliProviderModule, workingDir, cliArgs, options?.extraEnv || {}, transportFactory);
234
235
  this.monitor = new StatusMonitor();
235
236
  this.historyWriter = new ChatHistoryWriter();
236
237
  }
@@ -737,7 +738,29 @@ export class CliProviderInstance implements ProviderInstance {
737
738
  }
738
739
 
739
740
  private pushEvent(event: ProviderEvent): void {
740
- this.events.push(event);
741
+ const enrichedEvent: ProviderEvent = {
742
+ ...event,
743
+ instanceId: typeof event.instanceId === 'string' && event.instanceId.trim()
744
+ ? event.instanceId
745
+ : this.instanceId,
746
+ targetSessionId: typeof event.targetSessionId === 'string' && event.targetSessionId.trim()
747
+ ? event.targetSessionId
748
+ : this.instanceId,
749
+ providerType: typeof event.providerType === 'string' && event.providerType.trim()
750
+ ? event.providerType
751
+ : this.type,
752
+ workspaceName: typeof event.workspaceName === 'string' && event.workspaceName.trim()
753
+ ? event.workspaceName
754
+ : this.workingDir,
755
+ providerSessionId: typeof event.providerSessionId === 'string' && event.providerSessionId.trim()
756
+ ? event.providerSessionId
757
+ : this.providerSessionId,
758
+ };
759
+ if (this.context?.emitProviderEvent) {
760
+ this.context.emitProviderEvent(enrichedEvent);
761
+ return;
762
+ }
763
+ this.events.push(enrichedEvent);
741
764
  }
742
765
 
743
766
  private flushEvents(): ProviderEvent[] {
@@ -977,15 +1000,40 @@ export class CliProviderInstance implements ProviderInstance {
977
1000
  }
978
1001
  }
979
1002
 
1003
+ mergeRuntimeChatMessages(parsedMessages: ChatMessage[]): ChatMessage[] {
1004
+ return this.mergeConversationMessages(parsedMessages);
1005
+ }
1006
+
980
1007
  private mergeConversationMessages(parsedMessages: any[]): ChatMessage[] {
981
1008
  if (this.runtimeMessages.length === 0) return normalizeChatMessages(parsedMessages);
982
1009
 
983
- return normalizeChatMessages([...parsedMessages, ...this.runtimeMessages.map((entry) => entry.message)]
984
- .map((message, index) => ({ message, index }))
1010
+ type MergeEntry = { message: ChatMessage; index: number; source: 'parsed' | 'runtime' };
1011
+ const parsedEntries: MergeEntry[] = parsedMessages.map((message, index) => ({
1012
+ message,
1013
+ index,
1014
+ source: 'parsed',
1015
+ }));
1016
+ const runtimeEntries: MergeEntry[] = this.runtimeMessages.map((entry, index) => ({
1017
+ message: entry.message,
1018
+ index: parsedMessages.length + index,
1019
+ source: 'runtime',
1020
+ }));
1021
+ const getTime = (message: ChatMessage): number => {
1022
+ const value = typeof message.receivedAt === 'number'
1023
+ ? message.receivedAt
1024
+ : typeof message.timestamp === 'number'
1025
+ ? message.timestamp
1026
+ : 0;
1027
+ return Number.isFinite(value) && value > 0 ? value : 0;
1028
+ };
1029
+
1030
+ return normalizeChatMessages([...parsedEntries, ...runtimeEntries]
985
1031
  .sort((a, b) => {
986
- const aTime = a.message.receivedAt || a.message.timestamp || 0;
987
- const bTime = b.message.receivedAt || b.message.timestamp || 0;
988
- if (aTime !== bTime) return aTime - bTime;
1032
+ const aTime = getTime(a.message);
1033
+ const bTime = getTime(b.message);
1034
+ if (aTime && bTime && aTime !== bTime) return aTime - bTime;
1035
+ if (aTime && !bTime && a.source === 'runtime' && b.source === 'parsed') return -1;
1036
+ if (!aTime && bTime && a.source === 'parsed' && b.source === 'runtime') return 1;
989
1037
  return a.index - b.index;
990
1038
  })
991
1039
  .map((entry) => entry.message));
@@ -47,7 +47,10 @@ export class ProviderInstanceManager {
47
47
  this.instances.get(id)!.dispose();
48
48
  }
49
49
  this.instances.set(id, instance);
50
- await instance.init(context);
50
+ await instance.init({
51
+ ...context,
52
+ emitProviderEvent: (event) => this.emitProviderEvent(instance.type, id, event),
53
+ });
51
54
  }
52
55
 
53
56
  /**
@@ -237,6 +240,22 @@ export class ProviderInstanceManager {
237
240
  this.eventListeners.push(listener);
238
241
  }
239
242
 
243
+ emitProviderEvent(providerType: string, instanceId: string, event: ProviderEvent): void {
244
+ const payload = {
245
+ ...event,
246
+ providerType,
247
+ instanceId: typeof event.instanceId === 'string' && event.instanceId.trim()
248
+ ? event.instanceId
249
+ : instanceId,
250
+ targetSessionId: typeof event.targetSessionId === 'string' && event.targetSessionId.trim()
251
+ ? event.targetSessionId
252
+ : instanceId,
253
+ } as ProviderEvent & { providerType: string };
254
+ for (const listener of this.eventListeners) {
255
+ listener(payload);
256
+ }
257
+ }
258
+
240
259
  private emitPendingEvents(
241
260
  providerType: string,
242
261
  state: ProviderState,
@@ -174,6 +174,8 @@ export interface InstanceContext {
174
174
  onPtyData?: (data: string) => void;
175
175
  /** Provider configvalue (resolved) */
176
176
  settings: Record<string, any>;
177
+ /** Immediate provider-originated status/event emission. Used to avoid waiting for status polling. */
178
+ emitProviderEvent?: (event: ProviderEvent) => void;
177
179
  }
178
180
 
179
181
  export interface ProviderInstance {
@@ -69,6 +69,8 @@ export interface RepoMeshNodePolicy {
69
69
  readOnly?: boolean;
70
70
  canPush?: boolean;
71
71
  maxConcurrentSessions?: number;
72
+ /** Ordered provider preference used when mesh_launch_session omits an explicit type. */
73
+ providerPriority?: string[];
72
74
  }
73
75
 
74
76
  export const DEFAULT_MESH_POLICY: RepoMeshPolicy = {
@@ -189,6 +191,10 @@ export interface LocalMeshNodeEntry {
189
191
  policy: RepoMeshNodePolicy;
190
192
  /** For single-machine mesh: same daemon, different worktree */
191
193
  isLocalWorktree?: boolean;
194
+ /** Branch this worktree tracks (set when created via clone_mesh_node) */
195
+ worktreeBranch?: string;
196
+ /** Node ID this worktree was cloned from */
197
+ clonedFromNodeId?: string;
192
198
  }
193
199
 
194
200
  // ─── Mesh Status (runtime, not persisted) ───────
@@ -432,6 +432,17 @@ export type VersionUpdateReason =
432
432
  | 'patch_mismatch'
433
433
  | 'daemon_ahead';
434
434
 
435
+ export type ReleaseChannel = 'stable' | 'preview';
436
+ export type NpmUpdateTag = 'latest' | 'next';
437
+
438
+ export interface VersionUpdatePolicy {
439
+ channel: ReleaseChannel;
440
+ npmTag: NpmUpdateTag;
441
+ targetVersion: string;
442
+ minVersion?: string;
443
+ updateCommand: string;
444
+ }
445
+
435
446
  /** Available provider information */
436
447
  export interface AvailableProviderInfo {
437
448
  type: string;
@@ -577,6 +588,10 @@ export interface CompactDaemonEntry {
577
588
  versionMismatch?: boolean;
578
589
  versionUpdateRequired?: boolean;
579
590
  versionUpdateReason?: VersionUpdateReason;
591
+ releaseChannel?: ReleaseChannel;
592
+ updateChannel?: ReleaseChannel;
593
+ updatePolicy?: VersionUpdatePolicy;
594
+ updateCommand?: string;
580
595
  terminalBackend?: TerminalBackendStatus;
581
596
  detectedIdes?: DetectedIdeInfo[];
582
597
  availableProviders?: AvailableProviderInfo[];
@@ -599,11 +614,15 @@ export interface CloudDaemonSummaryEntry {
599
614
  versionMismatch?: boolean;
600
615
  versionUpdateRequired?: boolean;
601
616
  versionUpdateReason?: VersionUpdateReason;
617
+ releaseChannel?: ReleaseChannel;
618
+ updateChannel?: ReleaseChannel;
619
+ updatePolicy?: VersionUpdatePolicy;
620
+ updateCommand?: string;
602
621
  terminalBackend?: TerminalBackendStatus;
603
622
  }
604
623
 
605
624
  /** Minimal daemon bootstrap payload used by dashboard WS to initiate P2P. */
606
- export interface DashboardBootstrapDaemonEntry {
625
+ export interface DashboardBootstrapDaemonEntry extends Partial<CloudDaemonSummaryEntry> {
607
626
  id: string;
608
627
  p2p?: StatusReportPayload['p2p'];
609
628
  timestamp?: number;
@@ -622,6 +641,8 @@ export interface DaemonStatusEventPayload {
622
641
  timestamp: number;
623
642
  targetSessionId?: string;
624
643
  providerType?: string;
644
+ providerSessionId?: string;
645
+ workspaceName?: string;
625
646
  duration?: number;
626
647
  elapsedSec?: number;
627
648
  modalMessage?: string;
@@ -643,6 +664,8 @@ export interface DashboardStatusEventPayload {
643
664
  daemonId?: string;
644
665
  providerType?: string;
645
666
  targetSessionId?: string;
667
+ providerSessionId?: string;
668
+ workspaceName?: string;
646
669
  duration?: number;
647
670
  elapsedSec?: number;
648
671
  modalMessage?: string;