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

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 +1398 -436
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +1425 -467
  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 +109 -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 +58 -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
@@ -14,5 +14,6 @@ export interface CoordinatorPromptContext {
14
14
  mesh: LocalMeshEntry;
15
15
  status?: RepoMeshStatus;
16
16
  userInstruction?: string;
17
+ coordinatorCliType?: string;
17
18
  }
18
19
  export declare function buildCoordinatorSystemPrompt(ctx: CoordinatorPromptContext): string;
@@ -1,2 +1,11 @@
1
1
  import type { DaemonComponents } from '../boot/daemon-lifecycle.js';
2
+ export declare function handleMeshForwardEvent(components: DaemonComponents, payload: Record<string, unknown>): {
3
+ success: boolean;
4
+ forwarded: number;
5
+ error?: undefined;
6
+ } | {
7
+ success: boolean;
8
+ error: string;
9
+ forwarded?: undefined;
10
+ };
2
11
  export declare function setupMeshEventForwarding(components: DaemonComponents): void;
@@ -63,3 +63,14 @@ export declare function buildUserChatMessage<T extends Omit<ChatMessage, 'role'
63
63
  });
64
64
  export declare function normalizeChatMessage<T extends ChatMessage>(message: T): T;
65
65
  export declare function normalizeChatMessages<T extends ChatMessage>(messages: T[] | null | undefined): T[];
66
+ /**
67
+ * Product chat transcript visibility contract.
68
+ *
69
+ * read_chat/debug paths may preserve every normalized message, including tool,
70
+ * terminal, thought, status, and control rows. The default user-facing chat UX
71
+ * should only render meaningful conversation turns unless a producer explicitly
72
+ * marks a non-standard row as user-facing. This keeps internal tool/status/control
73
+ * plumbing out of the ordinary transcript without matching provider-specific text.
74
+ */
75
+ export declare function isUserFacingChatMessage(message: ChatMessage | null | undefined): boolean;
76
+ export declare function filterUserFacingChatMessages<T extends ChatMessage>(messages: T[] | null | undefined): T[];
@@ -8,6 +8,7 @@ import { type ProviderModule } from './contracts.js';
8
8
  import type { ProviderInstance, ProviderState, InstanceContext, HotChatSessionState, SessionModalState } from './provider-instance.js';
9
9
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
10
10
  import type { PtyTransportFactory } from '../cli-adapters/pty-transport.js';
11
+ import type { ChatMessage } from '../types.js';
11
12
  type PersistableCliHistoryMessage = {
12
13
  role: string;
13
14
  content: string;
@@ -67,6 +68,7 @@ export declare class CliProviderInstance implements ProviderInstance {
67
68
  constructor(provider: ProviderModule, workingDir: string, cliArgs?: string[], instanceId?: string, transportFactory?: PtyTransportFactory, options?: {
68
69
  providerSessionId?: string;
69
70
  launchMode?: 'new' | 'resume' | 'manual';
71
+ extraEnv?: Record<string, string>;
70
72
  onProviderSessionResolved?: (info: {
71
73
  instanceId: string;
72
74
  providerType: string;
@@ -112,6 +114,7 @@ export declare class CliProviderInstance implements ProviderInstance {
112
114
  private maybeAppendRuntimeRecoveryMessage;
113
115
  private appendRuntimeSystemMessage;
114
116
  private appendRuntimeMessage;
117
+ mergeRuntimeChatMessages(parsedMessages: ChatMessage[]): ChatMessage[];
115
118
  private mergeConversationMessages;
116
119
  private formatApprovalRequestMessage;
117
120
  private promoteProviderSessionId;
@@ -67,6 +67,7 @@ export declare class ProviderInstanceManager {
67
67
  onEvent(listener: (event: ProviderEvent & {
68
68
  providerType: string;
69
69
  }) => void): void;
70
+ emitProviderEvent(providerType: string, instanceId: string, event: ProviderEvent): void;
70
71
  private emitPendingEvents;
71
72
  /**
72
73
  * Forward event to specific Instance
@@ -147,6 +147,8 @@ export interface InstanceContext {
147
147
  onPtyData?: (data: string) => void;
148
148
  /** Provider configvalue (resolved) */
149
149
  settings: Record<string, any>;
150
+ /** Immediate provider-originated status/event emission. Used to avoid waiting for status polling. */
151
+ emitProviderEvent?: (event: ProviderEvent) => void;
150
152
  }
151
153
  export interface ProviderInstance {
152
154
  /** Provider type */
@@ -40,6 +40,7 @@ export interface RepoMeshNode {
40
40
  status: 'enabled' | 'disabled' | 'removed';
41
41
  }
42
42
  export type RepoMeshNodeHealth = 'online' | 'offline' | 'degraded' | 'dirty' | 'wrong_branch' | 'unknown';
43
+ export type RepoMeshSessionCleanupMode = 'preserve' | 'stop' | 'delete_stopped' | 'stop_and_delete';
43
44
  export interface RepoMeshPolicy {
44
45
  requirePreTaskCheckpoint: boolean;
45
46
  requirePostTaskCheckpoint: boolean;
@@ -48,11 +49,19 @@ export interface RepoMeshPolicy {
48
49
  dirtyWorkspaceBehavior: 'block' | 'warn' | 'checkpoint_then_continue';
49
50
  maxParallelTasks: number;
50
51
  allowedProviders?: string[];
52
+ /**
53
+ * What to do with delegated session-host records for a node when it is removed.
54
+ * Defaults to 'preserve' so completed work can be reviewed later and live
55
+ * runtimes are never stopped/deleted unless the mesh owner opts in.
56
+ */
57
+ sessionCleanupOnNodeRemove?: RepoMeshSessionCleanupMode;
51
58
  }
52
59
  export interface RepoMeshNodePolicy {
53
60
  readOnly?: boolean;
54
61
  canPush?: boolean;
55
62
  maxConcurrentSessions?: number;
63
+ /** Ordered provider preference used when mesh_launch_session omits an explicit type. */
64
+ providerPriority?: string[];
56
65
  }
57
66
  export declare const DEFAULT_MESH_POLICY: RepoMeshPolicy;
58
67
  export interface RepoMeshNodeCapabilities {
@@ -149,6 +158,10 @@ export interface LocalMeshNodeEntry {
149
158
  policy: RepoMeshNodePolicy;
150
159
  /** For single-machine mesh: same daemon, different worktree */
151
160
  isLocalWorktree?: boolean;
161
+ /** Branch this worktree tracks (set when created via clone_mesh_node) */
162
+ worktreeBranch?: string;
163
+ /** Node ID this worktree was cloned from */
164
+ clonedFromNodeId?: string;
152
165
  }
153
166
  export interface RepoMeshStatus {
154
167
  meshId: string;
@@ -522,6 +522,8 @@ export interface DaemonStatusEventPayload {
522
522
  timestamp: number;
523
523
  targetSessionId?: string;
524
524
  providerType?: string;
525
+ providerSessionId?: string;
526
+ workspaceName?: string;
525
527
  duration?: number;
526
528
  elapsedSec?: number;
527
529
  modalMessage?: string;
@@ -535,6 +537,8 @@ export interface DashboardStatusEventPayload {
535
537
  daemonId?: string;
536
538
  providerType?: string;
537
539
  targetSessionId?: string;
540
+ providerSessionId?: string;
541
+ workspaceName?: string;
538
542
  duration?: number;
539
543
  elapsedSec?: number;
540
544
  modalMessage?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.76-rc.5",
3
+ "version": "0.9.76-rc.51",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -50,18 +50,17 @@
50
50
  "@agentclientprotocol/sdk": "^0.16.1",
51
51
  "@xterm/xterm": "^6.0.0",
52
52
  "chalk": "^5.3.0",
53
- "chokidar": "^5.0.0",
53
+ "chokidar": "^4.0.3",
54
54
  "conf": "^13.0.0",
55
+ "js-yaml": "^4.1.1",
55
56
  "node-pty": "^1.2.0-beta.12",
56
57
  "ws": "^8.19.0"
57
58
  },
58
- "bundleDependencies": [
59
- "@adhdev/session-host-core"
60
- ],
61
59
  "optionalDependencies": {
62
60
  "@adhdev/ghostty-vt-node": "*"
63
61
  },
64
62
  "devDependencies": {
63
+ "@types/js-yaml": "^4.0.9",
65
64
  "@types/node": "^22.0.0",
66
65
  "@types/ws": "^8.18.1",
67
66
  "tsup": "^8.2.0",
@@ -266,9 +266,10 @@ export class ProviderCliAdapter implements CliAdapter {
266
266
  const currentSnapshot = normalizeScreenSnapshot(screenText);
267
267
  const lastSnapshot = this.lastScreenSnapshot;
268
268
  if (!lastSnapshot || lastSnapshot === currentSnapshot) return screenText;
269
- const staleSnapshotLooksActive = /\besc to (?:interrupt|stop)\b|Enter to interrupt, Ctrl\+C to cancel/i.test(lastSnapshot);
270
- const currentScreenLooksIdle = /(?:^|\n|\r)\s*[❯›>]\s*(?:\n|\r|$)/.test(screenText)
271
- && !/\besc to (?:interrupt|stop)\b|Enter to interrupt, Ctrl\+C to cancel/i.test(screenText);
269
+ const activeScreenPattern = /\besc to (?:interrupt|stop)\b|Enter to interrupt, Ctrl\+C to cancel|Enter to confirm\s*[·•-]\s*Esc to cancel|\b(?:MCP servers?|tool calls?)\b[^\n\r]{0,160}\brequire approval\b/i;
270
+ const staleSnapshotLooksActive = activeScreenPattern.test(lastSnapshot);
271
+ const currentScreenLooksIdle = /(?:^|\n|\r)\s*[❯›>]\s*(?:Try\s+["“][^\n\r"”]+["”])?\s*(?:\n|\r|$)/.test(screenText)
272
+ && !activeScreenPattern.test(screenText);
272
273
  if (staleSnapshotLooksActive && currentScreenLooksIdle) return screenText;
273
274
  if (currentSnapshot.length >= lastSnapshot.length) return screenText;
274
275
  // Terminal screen reads can miss a just-rendered completed Hermes box while
@@ -421,6 +422,7 @@ export class ProviderCliAdapter implements CliAdapter {
421
422
  provider: CliProviderModule,
422
423
  workingDir: string,
423
424
  private extraArgs: string[] = [],
425
+ private extraEnv: Record<string, string> = {},
424
426
  transportFactory: PtyTransportFactory = new NodePtyTransportFactory(),
425
427
  ) {
426
428
  this.provider = provider;
@@ -522,6 +524,7 @@ export class ProviderCliAdapter implements CliAdapter {
522
524
  runtimeSettings: this.runtimeSettings,
523
525
  workingDir: this.workingDir,
524
526
  extraArgs: this.extraArgs,
527
+ extraEnv: this.extraEnv,
525
528
  });
526
529
 
527
530
  LOG.info('CLI', `[${this.cliType}] Spawning in ${this.workingDir}`);
@@ -1854,9 +1857,13 @@ export class ProviderCliAdapter implements CliAdapter {
1854
1857
  };
1855
1858
  this.recordTrace('submit_echo_missing', diagnostic);
1856
1859
  if (this.requirePromptEchoBeforeSubmit) {
1857
- const message = `${this.cliName} prompt echo was not observed on the PTY screen before submit`;
1858
- LOG.warn('CLI', `[${this.cliType}] ${message} elapsed=${elapsed}ms maxEchoWaitMs=${state.maxEchoWaitMs} screen=${JSON.stringify(diagnostic.screenText).slice(0, 240)}`);
1859
- completion.rejectOnce(new Error(message));
1860
+ // At this point the prompt text write already completed. Rejecting without
1861
+ // a submit key can leave the delegated CLI with an unsent prompt sitting at
1862
+ // the input line, which makes later coordinator sends appear stuck. Prefer a
1863
+ // guarded submit after the full echo wait; the existing stuck-submit retry
1864
+ // will send a delayed follow-up Enter if the prompt remains visible.
1865
+ LOG.warn('CLI', `[${this.cliType}] prompt echo was not observed before submit; sending guarded submit key anyway elapsed=${elapsed}ms maxEchoWaitMs=${state.maxEchoWaitMs} screen=${JSON.stringify(diagnostic.screenText).slice(0, 240)}`);
1866
+ this.submitSendKey(state, completion);
1860
1867
  return;
1861
1868
  }
1862
1869
  LOG.warn('CLI', `[${this.cliType}] prompt echo was not observed before submit; sending submit key anyway elapsed=${elapsed}ms maxEchoWaitMs=${state.maxEchoWaitMs}`);
@@ -1911,7 +1918,21 @@ export class ProviderCliAdapter implements CliAdapter {
1911
1918
  ? String(parsedStatusBeforeSend.status)
1912
1919
  : '';
1913
1920
  if (!allowInputDuringGeneration && (parsedSessionStatus === 'generating' || parsedSessionStatus === 'long_generating')) {
1914
- throw new Error(`${this.cliName} is still processing the previous prompt`);
1921
+ const parsedModal = parsedStatusBeforeSend?.activeModal ?? parsedStatusBeforeSend?.modal ?? null;
1922
+ const parsedHasActionableModal = Boolean(
1923
+ parsedModal
1924
+ && Array.isArray(parsedModal.buttons)
1925
+ && parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
1926
+ );
1927
+ const terminalLooksIdle = this.currentStatus === 'idle'
1928
+ && this.runDetectStatus(this.recentOutputBuffer) === 'idle'
1929
+ && !this.isWaitingForResponse
1930
+ && !this.currentTurnScope
1931
+ && !this.hasActionableApproval()
1932
+ && !parsedHasActionableModal;
1933
+ if (!terminalLooksIdle) {
1934
+ throw new Error(`${this.cliName} is still processing the previous prompt`);
1935
+ }
1915
1936
  }
1916
1937
  if (this.isWaitingForResponse && !allowInputDuringGeneration) {
1917
1938
  if (!this.clearStaleIdleResponseGuard('send_message_guard')) {
@@ -27,8 +27,9 @@ export function resolveCliSpawnPlan(options: {
27
27
  runtimeSettings: Record<string, any>;
28
28
  workingDir: string;
29
29
  extraArgs: string[];
30
+ extraEnv?: Record<string, string>;
30
31
  }): CliSpawnPlan {
31
- const { provider, runtimeSettings, workingDir, extraArgs } = options;
32
+ const { provider, runtimeSettings, workingDir, extraArgs, extraEnv } = options;
32
33
  const { spawn: spawnConfig } = provider;
33
34
  const configuredCommand = typeof runtimeSettings.executablePath === 'string' && runtimeSettings.executablePath.trim()
34
35
  ? runtimeSettings.executablePath.trim()
@@ -65,7 +66,7 @@ export function resolveCliSpawnPlan(options: {
65
66
  shellArgs = allArgs;
66
67
  }
67
68
 
68
- const env = buildCliSpawnEnv(process.env, spawnConfig.env);
69
+ const env = buildCliSpawnEnv(process.env, { ...(spawnConfig.env || {}), ...(extraEnv || {}) });
69
70
  // Some CLI agents, notably Hermes, route their tools through TERMINAL_CWD
70
71
  // rather than process.cwd(). Keep the generic ADHDev launch workspace as
71
72
  // the single source of truth so PTY cwd and tool cwd cannot diverge.
@@ -22,12 +22,17 @@ import type { SessionTransport } from '../shared-types.js';
22
22
 
23
23
  const RECENT_SEND_WINDOW_MS = 1200;
24
24
  export const READ_CHAT_PROVIDER_EVAL_TIMEOUT_MS = 25_000;
25
+ const HERMES_CLI_STARTING_SEND_SETTLE_MS = 2_000;
25
26
  const recentSendByTarget = new Map<string, number>();
26
27
 
27
28
  interface ApprovalSelectableInstance extends ProviderInstance {
28
29
  recordApprovalSelection?(buttonText: string): void;
29
30
  }
30
31
 
32
+ interface RuntimeChatMessageMerger extends ProviderInstance {
33
+ mergeRuntimeChatMessages?(messages: ChatMessage[]): ChatMessage[];
34
+ }
35
+
31
36
  type LegacyStringScript = (params?: Record<string, unknown> | string) => string;
32
37
 
33
38
  function getCurrentProviderType(h: CommandHelpers, fallback = ''): string {
@@ -97,6 +102,19 @@ function getSendChatInputEnvelope(args: any): InputEnvelope {
97
102
  return normalizeInputEnvelope(args?.input ? { input: args.input } : args);
98
103
  }
99
104
 
105
+ function sleep(ms: number): Promise<void> {
106
+ return new Promise((resolve) => setTimeout(resolve, ms));
107
+ }
108
+
109
+ async function waitOnceForFreshHermesCliStart(adapter: CliAdapter, log: (msg: string) => void): Promise<void> {
110
+ if (adapter.cliType !== 'hermes-cli') return;
111
+ const status = typeof adapter.getStatus === 'function' ? adapter.getStatus()?.status : undefined;
112
+ if (status !== 'starting') return;
113
+
114
+ log(`Hermes CLI is still starting; waiting ${HERMES_CLI_STARTING_SEND_SETTLE_MS}ms before first send`);
115
+ await sleep(HERMES_CLI_STARTING_SEND_SETTLE_MS);
116
+ }
117
+
100
118
  function getHistorySessionId(h: CommandHelpers, args: any): string | undefined {
101
119
  const explicit = typeof args?.historySessionId === 'string' ? args.historySessionId.trim() : '';
102
120
  if (explicit) return explicit;
@@ -250,6 +268,40 @@ function normalizeReadChatCommandStatus(status: unknown, activeModal: unknown):
250
268
  }
251
269
  }
252
270
 
271
+ function isGeneratingLikeStatus(status: unknown): boolean {
272
+ return status === 'generating' || status === 'streaming' || status === 'long_generating' || status === 'starting';
273
+ }
274
+
275
+ function shouldTrustCliAdapterTerminalStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any): boolean {
276
+ if (!isGeneratingLikeStatus(parsedStatus)) return false;
277
+ if (hasNonEmptyModalButtons(activeModal)) return false;
278
+ const adapterRawStatus = typeof adapterStatus?.status === 'string' ? adapterStatus.status.trim() : '';
279
+ if (adapterRawStatus !== 'idle') return false;
280
+ if (typeof adapter.isProcessing === 'function' && adapter.isProcessing()) return false;
281
+ return true;
282
+ }
283
+
284
+ function normalizeCliReadChatStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any): string {
285
+ if (shouldTrustCliAdapterTerminalStatus(parsedStatus, activeModal, adapter, adapterStatus)) return 'idle';
286
+ return typeof parsedStatus === 'string' && parsedStatus.trim() ? parsedStatus : 'idle';
287
+ }
288
+
289
+ function finalizeStreamingMessagesWhenIdle(messages: ChatMessage[], status: string): ChatMessage[] {
290
+ if (status !== 'idle') return messages;
291
+ return messages.map((message) => {
292
+ const meta = message.meta && typeof message.meta === 'object'
293
+ ? message.meta as Record<string, unknown>
294
+ : undefined;
295
+ const hasStreamingMeta = meta?.streaming === true;
296
+ if (message.bubbleState !== 'streaming' && !hasStreamingMeta) return message;
297
+ return {
298
+ ...message,
299
+ ...(message.bubbleState === 'streaming' ? { bubbleState: 'final' as const } : {}),
300
+ ...(hasStreamingMeta ? { meta: { ...meta, streaming: false } } : {}),
301
+ };
302
+ });
303
+ }
304
+
253
305
  function buildReadChatCommandResult(payload: Record<string, any>, args: any): CommandResult {
254
306
  let validatedPayload: Record<string, any>;
255
307
  const debugReadChat = payload?.debugReadChat && typeof payload.debugReadChat === 'object'
@@ -464,6 +516,18 @@ function buildChatDebugBundleSummary(bundle: Record<string, unknown>): Record<st
464
516
  const readChat = bundle.readChat && typeof bundle.readChat === 'object' ? bundle.readChat as Record<string, unknown> : {};
465
517
  const cli = bundle.cli && typeof bundle.cli === 'object' ? bundle.cli as Record<string, unknown> : null;
466
518
  const frontend = bundle.frontend && typeof bundle.frontend === 'object' ? bundle.frontend as Record<string, unknown> : null;
519
+ const debugReadChat = readChat.debugReadChat && typeof readChat.debugReadChat === 'object'
520
+ ? readChat.debugReadChat as Record<string, unknown>
521
+ : {};
522
+ const parsedStatus = cli?.parsedStatus && typeof cli.parsedStatus === 'object'
523
+ ? cli.parsedStatus as Record<string, unknown>
524
+ : null;
525
+ const cliParsedMessageCount = Array.isArray(parsedStatus?.messages) ? parsedStatus.messages.length : undefined;
526
+ const readChatReturnedMessages = Array.isArray(readChat.messagesTail) ? readChat.messagesTail.length : undefined;
527
+ const cliPartialResponse = typeof cli?.partialResponse === 'string' ? cli.partialResponse : '';
528
+ const readChatStatus = typeof readChat.status === 'string' ? readChat.status : '';
529
+ const cliStatus = typeof cli?.status === 'string' ? cli.status : '';
530
+ const cliParsedStatus = typeof parsedStatus?.status === 'string' ? parsedStatus.status : '';
467
531
  return {
468
532
  createdAt: bundle.createdAt,
469
533
  targetSessionId: target.targetSessionId,
@@ -472,8 +536,22 @@ function buildChatDebugBundleSummary(bundle: Record<string, unknown>): Record<st
472
536
  readChatSuccess: readChat.success,
473
537
  readChatStatus: readChat.status,
474
538
  readChatTotalMessages: readChat.totalMessages,
539
+ readChatReturnedMessages,
475
540
  cliStatus: cli?.status,
541
+ cliParsedStatus: cliParsedStatus || undefined,
476
542
  cliMessageCount: cli?.messageCount,
543
+ cliParsedMessageCount,
544
+ cliPartialResponseChars: cliPartialResponse.length,
545
+ parserAdapterStatusMismatch: Boolean(cliStatus && cliParsedStatus && cliStatus !== cliParsedStatus),
546
+ parserReadChatStatusMismatch: Boolean(readChatStatus && cliParsedStatus && readChatStatus !== cliParsedStatus),
547
+ readChatDebug: Object.keys(debugReadChat).length ? {
548
+ adapterStatus: debugReadChat.adapterStatus,
549
+ parsedStatus: debugReadChat.parsedStatus,
550
+ returnedStatus: debugReadChat.returnedStatus,
551
+ parsedMsgCount: debugReadChat.parsedMsgCount,
552
+ returnedMsgCount: debugReadChat.returnedMsgCount,
553
+ shouldPreferAdapterMessages: debugReadChat.shouldPreferAdapterMessages,
554
+ } : undefined,
477
555
  hasFrontendSnapshot: !!frontend,
478
556
  };
479
557
  }
@@ -720,7 +798,7 @@ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<C
720
798
  }
721
799
 
722
800
  export async function handleReadChat(h: CommandHelpers, args: any): Promise<CommandResult> {
723
- const provider = h.getProvider(args?.agentType);
801
+ const provider = h.getProvider(args?.agentType || args?.providerType);
724
802
  const transport = getTargetTransport(h, provider);
725
803
  const historySessionId = getHistorySessionId(h, args);
726
804
 
@@ -760,10 +838,17 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
760
838
  ? parsedRecord.coverage
761
839
  : undefined;
762
840
  const activeModal = parsedRecord.activeModal ?? parsedRecord.modal ?? null;
763
- const returnedStatus = parsedRecord.status || 'idle';
764
- LOG.debug('Command', `[read_chat] cli-like parsed provider=${adapter.cliType} target=${String(args?.targetSessionId || '')} adapterStatus=${String(adapterStatus.status || '')} parsedStatus=${String(parsedRecord.status || '')} parsedMsgCount=${parsedRecord.messages.length}`);
841
+ const returnedStatus = normalizeCliReadChatStatus(parsedRecord.status, activeModal, adapter, adapterStatus);
842
+ const runtimeMessageMerger = getTargetInstance(h, args) as RuntimeChatMessageMerger | null;
843
+ const parsedMessages = finalizeStreamingMessagesWhenIdle(parsedRecord.messages as ChatMessage[], returnedStatus);
844
+ const returnedMessages = runtimeMessageMerger?.category === 'cli'
845
+ && runtimeMessageMerger.type === adapter.cliType
846
+ && typeof runtimeMessageMerger.mergeRuntimeChatMessages === 'function'
847
+ ? runtimeMessageMerger.mergeRuntimeChatMessages(parsedMessages)
848
+ : parsedMessages;
849
+ LOG.debug('Command', `[read_chat] cli-like parsed provider=${adapter.cliType} target=${String(args?.targetSessionId || '')} adapterStatus=${String(adapterStatus.status || '')} parsedStatus=${String(parsedRecord.status || '')} parsedMsgCount=${parsedRecord.messages.length} returnedMsgCount=${returnedMessages.length}`);
765
850
  return buildReadChatCommandResult({
766
- messages: parsedRecord.messages,
851
+ messages: returnedMessages,
767
852
  status: returnedStatus,
768
853
  activeModal,
769
854
  debugReadChat: {
@@ -774,7 +859,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
774
859
  returnedStatus: String(returnedStatus || ''),
775
860
  shouldPreferAdapterMessages: false,
776
861
  parsedMsgCount: parsedRecord.messages.length,
777
- returnedMsgCount: parsedRecord.messages.length,
862
+ returnedMsgCount: returnedMessages.length,
778
863
  },
779
864
  ...(title ? { title } : {}),
780
865
  ...(providerSessionId ? { providerSessionId } : {}),
@@ -1049,6 +1134,7 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
1049
1134
  try {
1050
1135
  assertTextOnlyInput(provider, input);
1051
1136
  if (!text) return { success: false, error: 'text required for PTY send' };
1137
+ await waitOnceForFreshHermesCliStart(adapter, _log);
1052
1138
  await adapter.sendMessage(text);
1053
1139
  return _logSendSuccess(`${transport}-adapter`, adapter.cliType);
1054
1140
  } catch (e: any) {
@@ -1571,11 +1657,26 @@ export async function handleResolveAction(h: CommandHelpers, args: any): Promise
1571
1657
  && status.activeModal.buttons.some((candidate) => typeof candidate === 'string' && candidate.trim())
1572
1658
  ? status.activeModal
1573
1659
  : null;
1574
- const effectiveModal = statusModal || surfacedModal;
1575
- const effectiveStatus = status?.status === 'waiting_approval' || targetState?.activeChat?.status === 'waiting_approval'
1660
+ const parsedStatus = !statusModal && !surfacedModal && typeof adapter.getScriptParsedStatus === 'function'
1661
+ ? (() => {
1662
+ try {
1663
+ return parseMaybeJson(adapter.getScriptParsedStatus());
1664
+ } catch {
1665
+ return null;
1666
+ }
1667
+ })()
1668
+ : null;
1669
+ const parsedModal = parsedStatus?.status === 'waiting_approval'
1670
+ && parsedStatus?.activeModal
1671
+ && Array.isArray(parsedStatus.activeModal.buttons)
1672
+ && parsedStatus.activeModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim())
1673
+ ? parsedStatus.activeModal
1674
+ : null;
1675
+ const effectiveModal = statusModal || surfacedModal || parsedModal;
1676
+ const effectiveStatus = status?.status === 'waiting_approval' || targetState?.activeChat?.status === 'waiting_approval' || parsedStatus?.status === 'waiting_approval'
1576
1677
  ? 'waiting_approval'
1577
1678
  : status?.status;
1578
- LOG.info('Command', `[resolveAction] CLI PTY gate target=${String(args?.targetSessionId || '')} rawStatus=${String(status?.status || '')} effectiveStatus=${String(effectiveStatus || '')} statusModal=${statusModal ? 'yes' : 'no'} surfacedModal=${surfacedModal ? 'yes' : 'no'} instance=${targetInstance ? 'yes' : 'no'}`);
1679
+ LOG.info('Command', `[resolveAction] CLI PTY gate target=${String(args?.targetSessionId || '')} rawStatus=${String(status?.status || '')} effectiveStatus=${String(effectiveStatus || '')} statusModal=${statusModal ? 'yes' : 'no'} surfacedModal=${surfacedModal ? 'yes' : 'no'} parsedModal=${parsedModal ? 'yes' : 'no'} instance=${targetInstance ? 'yes' : 'no'}`);
1579
1680
  if (!effectiveModal) {
1580
1681
  return { success: false, error: 'Not in approval state' };
1581
1682
  }
@@ -8,7 +8,7 @@
8
8
  import * as os from 'os';
9
9
  import * as path from 'path';
10
10
  import * as crypto from 'crypto';
11
- import { existsSync } from 'fs';
11
+ import { existsSync, mkdirSync, writeFileSync } from 'fs';
12
12
  import { execFileSync } from 'child_process';
13
13
  import chalk from 'chalk';
14
14
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
@@ -132,6 +132,62 @@ type CliAdapterWithExtraArgs = CliAdapter & {
132
132
  extraArgs?: string[];
133
133
  };
134
134
 
135
+ type CliStartOptions = {
136
+ resumeSessionId?: string;
137
+ settingsOverride?: Record<string, any>;
138
+ extraEnv?: Record<string, string>;
139
+ };
140
+
141
+ const COORDINATOR_DELEGATED_ENV_UNSETS: Record<string, string> = {
142
+ ADHDEV_INLINE_MESH: '',
143
+ ADHDEV_MCP_TRANSPORT: '',
144
+ ADHDEV_MESH_ID: '',
145
+ HERMES_EPHEMERAL_SYSTEM_PROMPT: '',
146
+ };
147
+
148
+ export interface CoordinatorDelegatedCliLaunchOptionsInput {
149
+ cliType: string;
150
+ workspace: string;
151
+ cliArgs?: string[];
152
+ env?: Record<string, string>;
153
+ }
154
+
155
+ export interface CoordinatorDelegatedCliLaunchOptions {
156
+ cliArgs: string[];
157
+ env: Record<string, string>;
158
+ }
159
+
160
+ function hasCliArg(args: string[], flag: string): boolean {
161
+ return args.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
162
+ }
163
+
164
+ function ensureEmptyDelegatedMcpConfig(workspace: string): string {
165
+ const baseDir = path.join(os.tmpdir(), 'adhdev-delegated-agent-empty-mcp');
166
+ mkdirSync(baseDir, { recursive: true });
167
+ const workspaceHash = crypto.createHash('sha256').update(path.resolve(workspace || os.tmpdir())).digest('hex').slice(0, 16);
168
+ const filePath = path.join(baseDir, `${workspaceHash}.json`);
169
+ writeFileSync(filePath, JSON.stringify({ mcpServers: {} }, null, 2), 'utf-8');
170
+ return filePath;
171
+ }
172
+
173
+ export function buildCoordinatorDelegatedCliLaunchOptions(
174
+ input: CoordinatorDelegatedCliLaunchOptionsInput,
175
+ ): CoordinatorDelegatedCliLaunchOptions {
176
+ const cliType = String(input.cliType || '').trim();
177
+ const cliArgs = Array.isArray(input.cliArgs) ? [...input.cliArgs] : [];
178
+ const env: Record<string, string> = { ...(input.env || {}), ...COORDINATOR_DELEGATED_ENV_UNSETS };
179
+
180
+ if (cliType === 'hermes-cli' && !hasCliArg(cliArgs, '--ignore-user-config')) {
181
+ cliArgs.unshift('--ignore-user-config');
182
+ }
183
+
184
+ if (cliType === 'claude-cli' && !hasCliArg(cliArgs, '--mcp-config')) {
185
+ cliArgs.unshift('--mcp-config', ensureEmptyDelegatedMcpConfig(input.workspace));
186
+ }
187
+
188
+ return { cliArgs, env };
189
+ }
190
+
135
191
  function isUuid(value: string): boolean {
136
192
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
137
193
  }
@@ -365,6 +421,7 @@ export class DaemonCliManager {
365
421
  runtimeId: string,
366
422
  providerSessionId?: string,
367
423
  attachExisting = false,
424
+ extraEnv?: Record<string, string>,
368
425
  ): CliAdapter {
369
426
  // cliType normalize (Resolve alias)
370
427
  const normalizedType = this.providerLoader.resolveAlias(cliType);
@@ -382,7 +439,7 @@ export class DaemonCliManager {
382
439
  providerSessionId,
383
440
  attachExisting,
384
441
  );
385
- return new ProviderCliAdapter(resolvedProvider as CliProviderModule, workingDir, cliArgs, transportFactory);
442
+ return new ProviderCliAdapter(resolvedProvider as CliProviderModule, workingDir, cliArgs, extraEnv || {}, transportFactory);
386
443
  }
387
444
 
388
445
  throw new Error(`No CLI provider found for '${cliType}'. Create a provider.js in providers/cli/${cliType}/`);
@@ -425,6 +482,7 @@ export class DaemonCliManager {
425
482
  options?: {
426
483
  providerSessionId?: string;
427
484
  launchMode?: CliLaunchMode;
485
+ extraEnv?: Record<string, string>;
428
486
  onProviderSessionResolved?: (info: {
429
487
  instanceId: string;
430
488
  providerType: string;
@@ -480,7 +538,7 @@ export class DaemonCliManager {
480
538
  workingDir: string,
481
539
  cliArgs?: string[],
482
540
  initialModel?: string,
483
- options?: { resumeSessionId?: string, settingsOverride?: Record<string, any> },
541
+ options?: CliStartOptions,
484
542
  ): Promise<{ runtimeSessionId: string; providerSessionId?: string }> {
485
543
  const trimmed = (workingDir || '').trim();
486
544
  if (!trimmed) throw new Error('working directory required');
@@ -629,6 +687,7 @@ export class DaemonCliManager {
629
687
  {
630
688
  providerSessionId: sessionBinding.providerSessionId,
631
689
  launchMode: sessionBinding.launchMode,
690
+ extraEnv: options?.extraEnv,
632
691
  onProviderSessionResolved: ({ providerSessionId, providerName, providerType, workspace }) => {
633
692
  this.persistRecentActivity({
634
693
  kind: 'cli',
@@ -651,6 +710,7 @@ export class DaemonCliManager {
651
710
  key,
652
711
  sessionBinding.providerSessionId,
653
712
  false,
713
+ options?.extraEnv,
654
714
  );
655
715
  try {
656
716
  await adapter.spawn();
@@ -899,12 +959,25 @@ export class DaemonCliManager {
899
959
  const launchSource = resolved.source;
900
960
  if (!cliType) throw new Error('cliType required');
901
961
 
962
+ const settingsOverride = args?.settings && typeof args.settings === 'object' ? args.settings : undefined;
963
+ const delegatedLaunch = settingsOverride?.launchedByCoordinator === true
964
+ ? buildCoordinatorDelegatedCliLaunchOptions({
965
+ cliType,
966
+ workspace: dir,
967
+ cliArgs: args?.cliArgs,
968
+ env: args?.env,
969
+ })
970
+ : null;
902
971
  const started = await this.startSession(
903
972
  cliType,
904
973
  dir,
905
- args?.cliArgs,
974
+ delegatedLaunch ? delegatedLaunch.cliArgs : args?.cliArgs,
906
975
  args?.initialModel,
907
- { resumeSessionId: args?.resumeSessionId, settingsOverride: args?.settings },
976
+ {
977
+ resumeSessionId: args?.resumeSessionId,
978
+ settingsOverride,
979
+ extraEnv: delegatedLaunch ? delegatedLaunch.env : args?.env,
980
+ },
908
981
  );
909
982
 
910
983
  return {
@@ -303,14 +303,15 @@ export class DaemonCommandHandler implements CommandHelpers {
303
303
  const sessionLookupFailed = !!targetSessionId && !session;
304
304
 
305
305
  const managerKey = this.extractIdeType(args, sessionLookupFailed);
306
- let providerType: string | undefined;
306
+ let providerType: string | undefined = args?.agentType || args?.providerType;
307
307
 
308
308
  if (!sessionLookupFailed) {
309
309
  providerType =
310
310
  session?.providerType
311
- || args?.agentType
312
- || args?.providerType
311
+ || providerType
313
312
  || this.inferProviderType(managerKey);
313
+ } else if (!providerType) {
314
+ providerType = this.inferProviderType(managerKey);
314
315
  }
315
316
 
316
317
  return { session, managerKey, providerType, sessionLookupFailed };
@@ -407,7 +408,15 @@ export class DaemonCommandHandler implements CommandHelpers {
407
408
  'invoke_provider_script',
408
409
  ]);
409
410
 
410
- if (this._currentRoute.sessionLookupFailed && sessionScopedCommands.has(cmd)) {
411
+ const allowsInactiveReadChatFallback =
412
+ cmd === 'read_chat'
413
+ && !!this._currentRoute.providerType
414
+ && (
415
+ (typeof args?.providerSessionId === 'string' && args.providerSessionId.trim().length > 0)
416
+ || (typeof args?.historySessionId === 'string' && args.historySessionId.trim().length > 0)
417
+ );
418
+
419
+ if (this._currentRoute.sessionLookupFailed && sessionScopedCommands.has(cmd) && !allowsInactiveReadChatFallback) {
411
420
  const result = {
412
421
  success: false,
413
422
  error: `Live session not found for targetSessionId: ${String(args?.targetSessionId || '').trim() || 'unknown'}`,