@adhdev/daemon-core 0.9.76-rc.5 → 0.9.76-rc.50

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 (46) 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 +11 -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 +1382 -430
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +1409 -461
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/mesh/coordinator-prompt.d.ts +1 -0
  15. package/dist/mesh/mesh-events.d.ts +9 -0
  16. package/dist/providers/chat-message-normalization.d.ts +11 -0
  17. package/dist/providers/cli-provider-instance.d.ts +3 -0
  18. package/dist/providers/provider-instance-manager.d.ts +1 -0
  19. package/dist/providers/provider-instance.d.ts +2 -0
  20. package/dist/repo-mesh-types.d.ts +13 -0
  21. package/dist/shared-types.d.ts +4 -0
  22. package/package.json +4 -5
  23. package/src/cli-adapters/provider-cli-adapter.ts +28 -7
  24. package/src/cli-adapters/provider-cli-runtime.ts +3 -2
  25. package/src/commands/chat-commands.ts +94 -8
  26. package/src/commands/cli-manager.ts +78 -5
  27. package/src/commands/handler.ts +13 -4
  28. package/src/commands/mesh-coordinator.ts +149 -6
  29. package/src/commands/router.d.ts +1 -0
  30. package/src/commands/router.ts +554 -34
  31. package/src/config/mesh-config.ts +23 -2
  32. package/src/git/git-commands.ts +5 -1
  33. package/src/git/git-types.ts +1 -0
  34. package/src/git/git-worktree.ts +214 -0
  35. package/src/git/index.ts +14 -0
  36. package/src/mesh/coordinator-prompt.ts +25 -10
  37. package/src/mesh/mesh-events.ts +109 -43
  38. package/src/providers/chat-message-normalization.ts +54 -0
  39. package/src/providers/cli-provider-instance.d.ts +2 -0
  40. package/src/providers/cli-provider-instance.ts +55 -7
  41. package/src/providers/provider-instance-manager.ts +20 -1
  42. package/src/providers/provider-instance.ts +2 -0
  43. package/src/repo-mesh-types.ts +15 -0
  44. package/src/shared-types.ts +4 -0
  45. package/src/status/builders.ts +17 -12
  46. package/src/status/reporter.ts +6 -0
@@ -74,6 +74,21 @@ export function normalizeRepoIdentity(remoteUrl: string): string {
74
74
 
75
75
  // ─── CRUD Operations ────────────────────────────
76
76
 
77
+ const SESSION_CLEANUP_MODES = new Set(['preserve', 'stop', 'delete_stopped', 'stop_and_delete']);
78
+
79
+ function mergeMeshPolicy(base: RepoMeshPolicy | undefined, patch: Partial<RepoMeshPolicy> | undefined): RepoMeshPolicy {
80
+ const policy: RepoMeshPolicy = { ...DEFAULT_MESH_POLICY, ...(base || {}), ...(patch || {}) };
81
+ if (!['block', 'warn', 'checkpoint_then_continue'].includes(policy.dirtyWorkspaceBehavior)) {
82
+ policy.dirtyWorkspaceBehavior = 'warn';
83
+ }
84
+ const maxParallelTasks = Number(policy.maxParallelTasks);
85
+ policy.maxParallelTasks = Number.isFinite(maxParallelTasks) ? Math.max(1, Math.min(8, Math.floor(maxParallelTasks))) : 2;
86
+ if (!SESSION_CLEANUP_MODES.has(String(policy.sessionCleanupOnNodeRemove))) {
87
+ policy.sessionCleanupOnNodeRemove = 'preserve';
88
+ }
89
+ return policy;
90
+ }
91
+
77
92
  export function listMeshes(): LocalMeshEntry[] {
78
93
  return loadMeshConfig().meshes;
79
94
  }
@@ -112,7 +127,7 @@ export function createMesh(opts: CreateMeshOptions): LocalMeshEntry {
112
127
  repoIdentity,
113
128
  repoRemoteUrl: opts.repoRemoteUrl,
114
129
  defaultBranch: opts.defaultBranch,
115
- policy: { ...DEFAULT_MESH_POLICY, ...opts.policy },
130
+ policy: mergeMeshPolicy(undefined, opts.policy),
116
131
  coordinator: opts.coordinator || {},
117
132
  nodes: [],
118
133
  createdAt: now,
@@ -138,7 +153,7 @@ export function updateMesh(meshId: string, opts: UpdateMeshOptions): LocalMeshEn
138
153
 
139
154
  if (opts.name !== undefined) mesh.name = opts.name.trim().slice(0, 100);
140
155
  if (opts.defaultBranch !== undefined) mesh.defaultBranch = opts.defaultBranch;
141
- if (opts.policy) mesh.policy = { ...mesh.policy, ...opts.policy };
156
+ if (opts.policy) mesh.policy = mergeMeshPolicy(mesh.policy, opts.policy);
142
157
  if (opts.coordinator) mesh.coordinator = opts.coordinator;
143
158
  mesh.updatedAt = new Date().toISOString();
144
159
 
@@ -160,9 +175,12 @@ export function deleteMesh(meshId: string): boolean {
160
175
  export interface AddNodeOptions {
161
176
  workspace: string;
162
177
  repoRoot?: string;
178
+ daemonId?: string;
163
179
  userOverrides?: Partial<RepoMeshNodeCapabilities>;
164
180
  policy?: RepoMeshNodePolicy;
165
181
  isLocalWorktree?: boolean;
182
+ worktreeBranch?: string;
183
+ clonedFromNodeId?: string;
166
184
  }
167
185
 
168
186
  export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntry | undefined {
@@ -183,9 +201,12 @@ export function addNode(meshId: string, opts: AddNodeOptions): LocalMeshNodeEntr
183
201
  id: `node_${randomUUID().replace(/-/g, '')}`,
184
202
  workspace: opts.workspace.trim(),
185
203
  repoRoot: opts.repoRoot,
204
+ daemonId: opts.daemonId,
186
205
  userOverrides: opts.userOverrides || {},
187
206
  policy: opts.policy || {},
188
207
  isLocalWorktree: opts.isLocalWorktree,
208
+ worktreeBranch: opts.worktreeBranch,
209
+ clonedFromNodeId: opts.clonedFromNodeId,
189
210
  };
190
211
 
191
212
  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
+ }
@@ -1,61 +1,127 @@
1
1
  import type { DaemonComponents } from '../boot/daemon-lifecycle.js';
2
- import { getMeshByRepo } from '../config/mesh-config.js';
2
+ import { getMesh, getMeshByRepo } from '../config/mesh-config.js';
3
3
  import { LOG } from '../logging/logger.js';
4
4
 
5
+ function readNonEmptyString(value: unknown): string {
6
+ return typeof value === 'string' && value.trim() ? value.trim() : '';
7
+ }
8
+
9
+ function formatCompletionMetadata(event: Record<string, unknown>): string {
10
+ const parts = [
11
+ readNonEmptyString(event.targetSessionId) ? `session_id=${readNonEmptyString(event.targetSessionId)}` : '',
12
+ readNonEmptyString(event.providerType) ? `provider=${readNonEmptyString(event.providerType)}` : '',
13
+ readNonEmptyString(event.providerSessionId) ? `provider_session_id=${readNonEmptyString(event.providerSessionId)}` : '',
14
+ ].filter(Boolean);
15
+ return parts.length > 0 ? ` (${parts.join('; ')})` : '';
16
+ }
17
+
18
+ function buildMeshSystemMessage(args: {
19
+ event: string;
20
+ nodeLabel: string;
21
+ metadataEvent: Record<string, unknown>;
22
+ }): string {
23
+ const metadata = formatCompletionMetadata(args.metadataEvent);
24
+ if (args.event === 'agent:generating_completed') {
25
+ return `[System] ${args.nodeLabel} has completed its task and is now idle${metadata}. This completion came from the agent status event path; use mesh_read_chat once to review its final progress, but do not poll repeatedly.`;
26
+ }
27
+ if (args.event === 'agent:waiting_approval') {
28
+ return `[System] ${args.nodeLabel} is waiting for approval to proceed${metadata}. You may use mesh_read_chat and mesh_approve to handle it.`;
29
+ }
30
+ return '';
31
+ }
32
+
33
+ function injectMeshSystemMessage(components: DaemonComponents, args: {
34
+ meshId: string;
35
+ sourceInstanceId?: string;
36
+ nodeLabel: string;
37
+ event: string;
38
+ metadataEvent: Record<string, unknown>;
39
+ }) {
40
+ const coordinatorInstances = components.instanceManager.getByCategory('cli').filter((inst) => {
41
+ const instState = inst.getState();
42
+ if (instState.settings?.meshCoordinatorFor !== args.meshId) return false;
43
+ if (args.sourceInstanceId && instState.instanceId === args.sourceInstanceId) return false;
44
+ return true;
45
+ });
46
+
47
+ if (coordinatorInstances.length === 0) return { success: true, forwarded: 0 };
48
+
49
+ const messageText = buildMeshSystemMessage({
50
+ event: args.event,
51
+ nodeLabel: args.nodeLabel,
52
+ metadataEvent: args.metadataEvent,
53
+ });
54
+ if (!messageText) return { success: false, error: 'unsupported mesh event' };
55
+
56
+ for (const coord of coordinatorInstances) {
57
+ const coordState = coord.getState();
58
+ LOG.info('MeshEvents', `Forwarding mesh event to coordinator ${coordState.instanceId}`);
59
+ coord.onEvent('send_message', { input: { text: messageText, textFallback: messageText } });
60
+ }
61
+ return { success: true, forwarded: coordinatorInstances.length };
62
+ }
63
+
64
+ export function handleMeshForwardEvent(components: DaemonComponents, payload: Record<string, unknown>) {
65
+ const eventName = readNonEmptyString(payload.event);
66
+ if (eventName !== 'agent:generating_completed' && eventName !== 'agent:waiting_approval') {
67
+ return { success: false, error: 'unsupported mesh event' };
68
+ }
69
+ const meshId = readNonEmptyString(payload.meshId);
70
+ if (!meshId) return { success: false, error: 'meshId required' };
71
+
72
+ const nodeId = readNonEmptyString(payload.nodeId);
73
+ const workspace = readNonEmptyString(payload.workspace);
74
+ const nodeLabel = nodeId ? `Node '${nodeId}'` : workspace ? `Agent at ${workspace}` : 'Remote agent';
75
+ return injectMeshSystemMessage(components, {
76
+ meshId,
77
+ nodeLabel,
78
+ event: eventName,
79
+ metadataEvent: {
80
+ targetSessionId: readNonEmptyString(payload.targetSessionId) || readNonEmptyString(payload.sessionId),
81
+ providerType: readNonEmptyString(payload.providerType),
82
+ providerSessionId: readNonEmptyString(payload.providerSessionId),
83
+ },
84
+ });
85
+ }
86
+
5
87
  export function setupMeshEventForwarding(components: DaemonComponents) {
6
88
  components.instanceManager.onEvent((event) => {
7
89
  // We only care about agent sub-session completion or waiting approval
8
90
  if (event.event !== 'agent:generating_completed' && event.event !== 'agent:waiting_approval') return;
9
-
10
- const instanceId = event.instanceId as string;
91
+
92
+ const instanceId = readNonEmptyString(event.instanceId);
11
93
  if (!instanceId) return;
12
94
 
13
- // Try to find the workspace of the sub-agent
95
+ // Try to find the workspace and mesh metadata of the sub-agent.
14
96
  const sourceInstance = components.instanceManager.getInstance(instanceId);
15
97
  if (!sourceInstance || sourceInstance.category !== 'cli') return;
16
98
  const state = sourceInstance.getState();
17
- const workspace = state.workspace;
99
+ const workspace = readNonEmptyString(state.workspace);
18
100
  if (!workspace) return;
101
+ const settings = state.settings && typeof state.settings === 'object' ? state.settings as Record<string, unknown> : {};
102
+ const meshIdFromRuntime = readNonEmptyString(settings.meshNodeFor);
19
103
 
20
- // Find the mesh that this workspace belongs to
21
- const mesh = getMeshByRepo(workspace);
22
- if (!mesh) return;
23
-
24
- // Find the coordinator session(s)
25
- const allInstances = components.instanceManager.getByCategory('cli');
26
- const coordinatorInstances = allInstances.filter((inst) => {
27
- const instState = inst.getState();
28
-
29
- // The coordinator session was launched with meshCoordinatorFor setting
30
- if (instState.settings?.meshCoordinatorFor !== mesh.id) return false;
31
-
32
- // Exclude the source instance itself (just in case)
33
- if (instState.instanceId === instanceId) return false;
34
-
35
- return true;
36
- });
37
-
38
- if (coordinatorInstances.length === 0) return;
39
-
40
- // Determine node label
41
- const targetNode = mesh.nodes.find((n) => n.workspace === workspace);
42
- const nodeLabel = targetNode ? `Node '${targetNode.id}'` : `Agent at ${workspace}`;
104
+ // Prefer runtime mesh metadata: delegated mesh-node agents can come from inline/cloud meshes
105
+ // that are not present in local meshes.json. Fall back to persisted mesh lookup for legacy sessions.
106
+ const mesh = meshIdFromRuntime ? getMesh(meshIdFromRuntime) : getMeshByRepo(workspace);
107
+ const meshId = meshIdFromRuntime || readNonEmptyString(mesh?.id);
108
+ if (!meshId) return;
43
109
 
44
- // Construct a system message in English
45
- let messageText = '';
46
- if (event.event === 'agent:generating_completed') {
47
- messageText = `[System] ${nodeLabel} has completed its task and is now idle. You may use mesh_read_chat to review its progress.`;
48
- } else if (event.event === 'agent:waiting_approval') {
49
- messageText = `[System] ${nodeLabel} is waiting for approval to proceed. You may use mesh_read_chat and mesh_approve to handle it.`;
50
- }
110
+ // Determine node label. Inline/cloud meshes may be unavailable here, so preserve runtime node id.
111
+ const targetNode = mesh?.nodes?.find((n: any) => n.workspace === workspace);
112
+ const runtimeNodeId = readNonEmptyString(settings.meshNodeId);
113
+ const nodeLabel = targetNode
114
+ ? `Node '${targetNode.id}'`
115
+ : runtimeNodeId
116
+ ? `Node '${runtimeNodeId}'`
117
+ : `Agent at ${workspace}`;
51
118
 
52
- if (!messageText) return;
53
-
54
- // Inject the message into the coordinator sessions
55
- for (const coord of coordinatorInstances) {
56
- const coordState = coord.getState();
57
- LOG.info('MeshEvents', `Forwarding event from ${workspace} to coordinator ${coordState.instanceId}`);
58
- coord.onEvent('send_message', { input: { text: messageText, textFallback: messageText } });
59
- }
119
+ injectMeshSystemMessage(components, {
120
+ meshId,
121
+ sourceInstanceId: instanceId,
122
+ nodeLabel,
123
+ event: event.event,
124
+ metadataEvent: event,
125
+ });
60
126
  });
61
127
  }
@@ -171,3 +171,57 @@ export function normalizeChatMessage<T extends ChatMessage>(message: T): T {
171
171
  export function normalizeChatMessages<T extends ChatMessage>(messages: T[] | null | undefined): T[] {
172
172
  return (Array.isArray(messages) ? messages : []).map((message) => normalizeChatMessage(message));
173
173
  }
174
+
175
+ function readMessageMeta(message: ChatMessage): Record<string, unknown> | null {
176
+ const meta = message?.meta;
177
+ return meta && typeof meta === 'object' && !Array.isArray(meta)
178
+ ? meta as Record<string, unknown>
179
+ : null;
180
+ }
181
+
182
+ function isExplicitlyHiddenFromTranscript(meta: Record<string, unknown> | null): boolean {
183
+ if (!meta) return false;
184
+ const visibility = typeof meta.transcriptVisibility === 'string'
185
+ ? meta.transcriptVisibility.trim().toLowerCase()
186
+ : '';
187
+ return visibility === 'hidden'
188
+ || visibility === 'debug'
189
+ || meta.internal === true
190
+ || meta.debug === true
191
+ || meta.statusOnly === true
192
+ || meta.controlOnly === true;
193
+ }
194
+
195
+ function isExplicitlyVisibleInTranscript(meta: Record<string, unknown> | null): boolean {
196
+ if (!meta) return false;
197
+ const visibility = typeof meta.transcriptVisibility === 'string'
198
+ ? meta.transcriptVisibility.trim().toLowerCase()
199
+ : '';
200
+ return visibility === 'visible' || meta.userFacing === true;
201
+ }
202
+
203
+ /**
204
+ * Product chat transcript visibility contract.
205
+ *
206
+ * read_chat/debug paths may preserve every normalized message, including tool,
207
+ * terminal, thought, status, and control rows. The default user-facing chat UX
208
+ * should only render meaningful conversation turns unless a producer explicitly
209
+ * marks a non-standard row as user-facing. This keeps internal tool/status/control
210
+ * plumbing out of the ordinary transcript without matching provider-specific text.
211
+ */
212
+ export function isUserFacingChatMessage(message: ChatMessage | null | undefined): boolean {
213
+ if (!message) return false;
214
+ const meta = readMessageMeta(message);
215
+ if (isExplicitlyHiddenFromTranscript(meta)) return false;
216
+ if (isExplicitlyVisibleInTranscript(meta)) return true;
217
+
218
+ const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
219
+ const kind = resolveChatMessageKind(message);
220
+ if (role === 'user' || role === 'human') return kind === 'standard' || kind === '';
221
+ if (role === 'assistant') return kind === 'standard' || kind === '';
222
+ return false;
223
+ }
224
+
225
+ export function filterUserFacingChatMessages<T extends ChatMessage>(messages: T[] | null | undefined): T[] {
226
+ return (Array.isArray(messages) ? messages : []).filter((message) => isUserFacingChatMessage(message));
227
+ }
@@ -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;