@adhdev/daemon-core 0.9.76-rc.6 → 0.9.76-rc.60

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 (53) 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.d.ts +2 -2
  11. package/dist/index.js +1525 -446
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.mjs +1550 -477
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/mesh/coordinator-prompt.d.ts +1 -0
  16. package/dist/mesh/mesh-events.d.ts +9 -0
  17. package/dist/providers/chat-message-normalization.d.ts +11 -0
  18. package/dist/providers/cli-provider-instance.d.ts +3 -0
  19. package/dist/providers/provider-instance-manager.d.ts +1 -0
  20. package/dist/providers/provider-instance.d.ts +2 -0
  21. package/dist/repo-mesh-types.d.ts +27 -0
  22. package/dist/session-host/runtime-support.d.ts +2 -1
  23. package/dist/shared-types.d.ts +4 -0
  24. package/dist/types.d.ts +9 -0
  25. package/package.json +4 -5
  26. package/src/cli-adapters/provider-cli-adapter.ts +28 -7
  27. package/src/cli-adapters/provider-cli-runtime.ts +3 -2
  28. package/src/commands/chat-commands.ts +126 -11
  29. package/src/commands/cli-manager.ts +78 -5
  30. package/src/commands/handler.ts +13 -4
  31. package/src/commands/mesh-coordinator.ts +148 -5
  32. package/src/commands/router.d.ts +1 -0
  33. package/src/commands/router.ts +553 -34
  34. package/src/config/mesh-config.ts +23 -2
  35. package/src/git/git-commands.ts +5 -1
  36. package/src/git/git-types.ts +1 -0
  37. package/src/git/git-worktree.ts +214 -0
  38. package/src/git/index.ts +14 -0
  39. package/src/index.ts +3 -0
  40. package/src/mesh/coordinator-prompt.ts +29 -14
  41. package/src/mesh/mesh-events.ts +109 -43
  42. package/src/providers/chat-message-normalization.ts +80 -0
  43. package/src/providers/cli-provider-instance.d.ts +2 -0
  44. package/src/providers/cli-provider-instance.ts +93 -8
  45. package/src/providers/provider-instance-manager.ts +20 -1
  46. package/src/providers/provider-instance.ts +2 -0
  47. package/src/providers/read-chat-contract.ts +8 -0
  48. package/src/repo-mesh-types.ts +30 -0
  49. package/src/session-host/runtime-support.ts +55 -7
  50. package/src/shared-types.ts +4 -0
  51. package/src/status/builders.ts +17 -12
  52. package/src/status/reporter.ts +6 -0
  53. package/src/types.ts +9 -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,31 @@ 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;
58
+ }
59
+ export interface RepoMeshRelatedRepo {
60
+ /** Stable display label for an explicitly configured associated checkout. */
61
+ label: string;
62
+ /** Absolute checkout/workspace path for git freshness probes. */
63
+ workspace: string;
51
64
  }
52
65
  export interface RepoMeshNodePolicy {
53
66
  readOnly?: boolean;
54
67
  canPush?: boolean;
55
68
  maxConcurrentSessions?: number;
69
+ /** Ordered provider preference used when mesh_launch_session omits an explicit type. */
70
+ providerPriority?: string[];
71
+ /**
72
+ * Optional associated/external repos that must be checked alongside this node.
73
+ * These are explicit policy/config entries only; Repo Mesh does not auto-discover
74
+ * sibling paths so freshness checks stay fail-closed and non-surprising.
75
+ */
76
+ relatedRepos?: RepoMeshRelatedRepo[];
56
77
  }
57
78
  export declare const DEFAULT_MESH_POLICY: RepoMeshPolicy;
58
79
  export interface RepoMeshNodeCapabilities {
@@ -149,6 +170,12 @@ export interface LocalMeshNodeEntry {
149
170
  policy: RepoMeshNodePolicy;
150
171
  /** For single-machine mesh: same daemon, different worktree */
151
172
  isLocalWorktree?: boolean;
173
+ /** Branch this worktree tracks (set when created via clone_mesh_node) */
174
+ worktreeBranch?: string;
175
+ /** Node ID this worktree was cloned from */
176
+ clonedFromNodeId?: string;
177
+ /** Optional associated/external repos configured as node metadata. */
178
+ relatedRepos?: RepoMeshRelatedRepo[];
152
179
  }
153
180
  export interface RepoMeshStatus {
154
181
  meshId: string;
@@ -1,8 +1,9 @@
1
- import { type SessionHostEndpoint } from '@adhdev/session-host-core';
1
+ import { type SessionHostEndpoint, type SessionHostRequestType } from '@adhdev/session-host-core';
2
2
  import type { HostedCliRuntimeDescriptor } from '../commands/cli-manager.js';
3
3
  export declare function ensureSessionHostReady(options: {
4
4
  appName?: string;
5
5
  spawnHost: () => void;
6
6
  timeoutMs?: number;
7
+ requiredRequestTypes?: readonly SessionHostRequestType[];
7
8
  }): Promise<SessionHostEndpoint>;
8
9
  export declare function listHostedCliRuntimes(endpoint: SessionHostEndpoint): Promise<HostedCliRuntimeDescriptor[]>;
@@ -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/dist/types.d.ts CHANGED
@@ -42,6 +42,15 @@ export interface ChatMessage {
42
42
  /** Optional: fiber metadata */
43
43
  _type?: string;
44
44
  _sub?: string;
45
+ /** Transcript visibility/audience contract for separating chat-visible content from internal/debug runtime rows. */
46
+ visibility?: 'visible' | 'user' | 'chat' | 'hidden' | 'debug' | 'internal' | (string & {});
47
+ transcriptVisibility?: 'visible' | 'user' | 'chat' | 'hidden' | 'debug' | 'internal' | (string & {});
48
+ audience?: 'chat' | 'debug' | 'trace' | 'internal' | (string & {});
49
+ source?: 'assistant_text' | 'tool_call' | 'terminal_command' | 'runtime_activity' | 'runtime_status' | 'provider_chrome' | 'control' | (string & {});
50
+ userFacing?: boolean;
51
+ internal?: boolean;
52
+ isInternal?: boolean;
53
+ debug?: boolean;
45
54
  /** Meta information for thought/terminal logs etc */
46
55
  meta?: {
47
56
  label?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.76-rc.6",
3
+ "version": "0.9.76-rc.60",
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.
@@ -19,15 +19,21 @@ import { getRecentDebugTrace, recordDebugTrace } from '../logging/debug-trace.js
19
19
  import { buildChatMessageSignature } from '../chat/chat-signatures.js';
20
20
  import type { ChatMessage } from '../types.js';
21
21
  import type { SessionTransport } from '../shared-types.js';
22
+ import { filterUserFacingChatMessages, normalizeChatMessages } from '../providers/chat-message-normalization.js';
22
23
 
23
24
  const RECENT_SEND_WINDOW_MS = 1200;
24
25
  export const READ_CHAT_PROVIDER_EVAL_TIMEOUT_MS = 25_000;
26
+ const HERMES_CLI_STARTING_SEND_SETTLE_MS = 2_000;
25
27
  const recentSendByTarget = new Map<string, number>();
26
28
 
27
29
  interface ApprovalSelectableInstance extends ProviderInstance {
28
30
  recordApprovalSelection?(buttonText: string): void;
29
31
  }
30
32
 
33
+ interface RuntimeChatMessageMerger extends ProviderInstance {
34
+ mergeRuntimeChatMessages?(messages: ChatMessage[]): ChatMessage[];
35
+ }
36
+
31
37
  type LegacyStringScript = (params?: Record<string, unknown> | string) => string;
32
38
 
33
39
  function getCurrentProviderType(h: CommandHelpers, fallback = ''): string {
@@ -97,6 +103,19 @@ function getSendChatInputEnvelope(args: any): InputEnvelope {
97
103
  return normalizeInputEnvelope(args?.input ? { input: args.input } : args);
98
104
  }
99
105
 
106
+ function sleep(ms: number): Promise<void> {
107
+ return new Promise((resolve) => setTimeout(resolve, ms));
108
+ }
109
+
110
+ async function waitOnceForFreshHermesCliStart(adapter: CliAdapter, log: (msg: string) => void): Promise<void> {
111
+ if (adapter.cliType !== 'hermes-cli') return;
112
+ const status = typeof adapter.getStatus === 'function' ? adapter.getStatus()?.status : undefined;
113
+ if (status !== 'starting') return;
114
+
115
+ log(`Hermes CLI is still starting; waiting ${HERMES_CLI_STARTING_SEND_SETTLE_MS}ms before first send`);
116
+ await sleep(HERMES_CLI_STARTING_SEND_SETTLE_MS);
117
+ }
118
+
100
119
  function getHistorySessionId(h: CommandHelpers, args: any): string | undefined {
101
120
  const explicit = typeof args?.historySessionId === 'string' ? args.historySessionId.trim() : '';
102
121
  if (explicit) return explicit;
@@ -177,7 +196,7 @@ function normalizeReadChatTailLimit(args: any): number {
177
196
 
178
197
  function normalizeReadChatMessages(payload: Record<string, any>): ChatMessage[] {
179
198
  const messages = Array.isArray(payload.messages) ? payload.messages as ChatMessage[] : [];
180
- return messages;
199
+ return normalizeChatMessages(messages);
181
200
  }
182
201
 
183
202
 
@@ -250,6 +269,40 @@ function normalizeReadChatCommandStatus(status: unknown, activeModal: unknown):
250
269
  }
251
270
  }
252
271
 
272
+ function isGeneratingLikeStatus(status: unknown): boolean {
273
+ return status === 'generating' || status === 'streaming' || status === 'long_generating' || status === 'starting';
274
+ }
275
+
276
+ function shouldTrustCliAdapterTerminalStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any): boolean {
277
+ if (!isGeneratingLikeStatus(parsedStatus)) return false;
278
+ if (hasNonEmptyModalButtons(activeModal)) return false;
279
+ const adapterRawStatus = typeof adapterStatus?.status === 'string' ? adapterStatus.status.trim() : '';
280
+ if (adapterRawStatus !== 'idle') return false;
281
+ if (typeof adapter.isProcessing === 'function' && adapter.isProcessing()) return false;
282
+ return true;
283
+ }
284
+
285
+ function normalizeCliReadChatStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any): string {
286
+ if (shouldTrustCliAdapterTerminalStatus(parsedStatus, activeModal, adapter, adapterStatus)) return 'idle';
287
+ return typeof parsedStatus === 'string' && parsedStatus.trim() ? parsedStatus : 'idle';
288
+ }
289
+
290
+ function finalizeStreamingMessagesWhenIdle(messages: ChatMessage[], status: string): ChatMessage[] {
291
+ if (status !== 'idle') return messages;
292
+ return messages.map((message) => {
293
+ const meta = message.meta && typeof message.meta === 'object'
294
+ ? message.meta as Record<string, unknown>
295
+ : undefined;
296
+ const hasStreamingMeta = meta?.streaming === true;
297
+ if (message.bubbleState !== 'streaming' && !hasStreamingMeta) return message;
298
+ return {
299
+ ...message,
300
+ ...(message.bubbleState === 'streaming' ? { bubbleState: 'final' as const } : {}),
301
+ ...(hasStreamingMeta ? { meta: { ...meta, streaming: false } } : {}),
302
+ };
303
+ });
304
+ }
305
+
253
306
  function buildReadChatCommandResult(payload: Record<string, any>, args: any): CommandResult {
254
307
  let validatedPayload: Record<string, any>;
255
308
  const debugReadChat = payload?.debugReadChat && typeof payload.debugReadChat === 'object'
@@ -264,13 +317,26 @@ function buildReadChatCommandResult(payload: Record<string, any>, args: any): Co
264
317
  return { success: false, error: error?.message || String(error) };
265
318
  }
266
319
  const messages = normalizeReadChatMessages(validatedPayload);
267
- const sync = buildFullTail(messages, normalizeReadChatTailLimit(args));
320
+ const visibleMessages = filterUserFacingChatMessages(messages);
321
+ const sync = buildFullTail(visibleMessages, normalizeReadChatTailLimit(args));
322
+ const hiddenMsgCount = Math.max(0, messages.length - visibleMessages.length);
323
+ const returnedDebugReadChat = debugReadChat
324
+ ? {
325
+ ...debugReadChat,
326
+ fullMsgCount: typeof debugReadChat.fullMsgCount === 'number'
327
+ ? debugReadChat.fullMsgCount
328
+ : messages.length,
329
+ visibleMsgCount: visibleMessages.length,
330
+ hiddenMsgCount,
331
+ returnedMsgCount: sync.messages.length,
332
+ }
333
+ : undefined;
268
334
  return {
269
335
  success: true,
270
336
  ...validatedPayload,
271
337
  messages: sync.messages,
272
338
  totalMessages: sync.totalMessages,
273
- ...(debugReadChat ? { debugReadChat } : {}),
339
+ ...(returnedDebugReadChat ? { debugReadChat: returnedDebugReadChat } : {}),
274
340
  };
275
341
  }
276
342
 
@@ -464,6 +530,18 @@ function buildChatDebugBundleSummary(bundle: Record<string, unknown>): Record<st
464
530
  const readChat = bundle.readChat && typeof bundle.readChat === 'object' ? bundle.readChat as Record<string, unknown> : {};
465
531
  const cli = bundle.cli && typeof bundle.cli === 'object' ? bundle.cli as Record<string, unknown> : null;
466
532
  const frontend = bundle.frontend && typeof bundle.frontend === 'object' ? bundle.frontend as Record<string, unknown> : null;
533
+ const debugReadChat = readChat.debugReadChat && typeof readChat.debugReadChat === 'object'
534
+ ? readChat.debugReadChat as Record<string, unknown>
535
+ : {};
536
+ const parsedStatus = cli?.parsedStatus && typeof cli.parsedStatus === 'object'
537
+ ? cli.parsedStatus as Record<string, unknown>
538
+ : null;
539
+ const cliParsedMessageCount = Array.isArray(parsedStatus?.messages) ? parsedStatus.messages.length : undefined;
540
+ const readChatReturnedMessages = Array.isArray(readChat.messagesTail) ? readChat.messagesTail.length : undefined;
541
+ const cliPartialResponse = typeof cli?.partialResponse === 'string' ? cli.partialResponse : '';
542
+ const readChatStatus = typeof readChat.status === 'string' ? readChat.status : '';
543
+ const cliStatus = typeof cli?.status === 'string' ? cli.status : '';
544
+ const cliParsedStatus = typeof parsedStatus?.status === 'string' ? parsedStatus.status : '';
467
545
  return {
468
546
  createdAt: bundle.createdAt,
469
547
  targetSessionId: target.targetSessionId,
@@ -472,8 +550,22 @@ function buildChatDebugBundleSummary(bundle: Record<string, unknown>): Record<st
472
550
  readChatSuccess: readChat.success,
473
551
  readChatStatus: readChat.status,
474
552
  readChatTotalMessages: readChat.totalMessages,
553
+ readChatReturnedMessages,
475
554
  cliStatus: cli?.status,
555
+ cliParsedStatus: cliParsedStatus || undefined,
476
556
  cliMessageCount: cli?.messageCount,
557
+ cliParsedMessageCount,
558
+ cliPartialResponseChars: cliPartialResponse.length,
559
+ parserAdapterStatusMismatch: Boolean(cliStatus && cliParsedStatus && cliStatus !== cliParsedStatus),
560
+ parserReadChatStatusMismatch: Boolean(readChatStatus && cliParsedStatus && readChatStatus !== cliParsedStatus),
561
+ readChatDebug: Object.keys(debugReadChat).length ? {
562
+ adapterStatus: debugReadChat.adapterStatus,
563
+ parsedStatus: debugReadChat.parsedStatus,
564
+ returnedStatus: debugReadChat.returnedStatus,
565
+ parsedMsgCount: debugReadChat.parsedMsgCount,
566
+ returnedMsgCount: debugReadChat.returnedMsgCount,
567
+ shouldPreferAdapterMessages: debugReadChat.shouldPreferAdapterMessages,
568
+ } : undefined,
477
569
  hasFrontendSnapshot: !!frontend,
478
570
  };
479
571
  }
@@ -720,7 +812,7 @@ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<C
720
812
  }
721
813
 
722
814
  export async function handleReadChat(h: CommandHelpers, args: any): Promise<CommandResult> {
723
- const provider = h.getProvider(args?.agentType);
815
+ const provider = h.getProvider(args?.agentType || args?.providerType);
724
816
  const transport = getTargetTransport(h, provider);
725
817
  const historySessionId = getHistorySessionId(h, args);
726
818
 
@@ -760,10 +852,17 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
760
852
  ? parsedRecord.coverage
761
853
  : undefined;
762
854
  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}`);
855
+ const returnedStatus = normalizeCliReadChatStatus(parsedRecord.status, activeModal, adapter, adapterStatus);
856
+ const runtimeMessageMerger = getTargetInstance(h, args) as RuntimeChatMessageMerger | null;
857
+ const parsedMessages = finalizeStreamingMessagesWhenIdle(parsedRecord.messages as ChatMessage[], returnedStatus);
858
+ const returnedMessages = runtimeMessageMerger?.category === 'cli'
859
+ && runtimeMessageMerger.type === adapter.cliType
860
+ && typeof runtimeMessageMerger.mergeRuntimeChatMessages === 'function'
861
+ ? runtimeMessageMerger.mergeRuntimeChatMessages(parsedMessages)
862
+ : parsedMessages;
863
+ 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
864
  return buildReadChatCommandResult({
766
- messages: parsedRecord.messages,
865
+ messages: returnedMessages,
767
866
  status: returnedStatus,
768
867
  activeModal,
769
868
  debugReadChat: {
@@ -774,7 +873,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
774
873
  returnedStatus: String(returnedStatus || ''),
775
874
  shouldPreferAdapterMessages: false,
776
875
  parsedMsgCount: parsedRecord.messages.length,
777
- returnedMsgCount: parsedRecord.messages.length,
876
+ returnedMsgCount: returnedMessages.length,
778
877
  },
779
878
  ...(title ? { title } : {}),
780
879
  ...(providerSessionId ? { providerSessionId } : {}),
@@ -1049,6 +1148,7 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
1049
1148
  try {
1050
1149
  assertTextOnlyInput(provider, input);
1051
1150
  if (!text) return { success: false, error: 'text required for PTY send' };
1151
+ await waitOnceForFreshHermesCliStart(adapter, _log);
1052
1152
  await adapter.sendMessage(text);
1053
1153
  return _logSendSuccess(`${transport}-adapter`, adapter.cliType);
1054
1154
  } catch (e: any) {
@@ -1571,11 +1671,26 @@ export async function handleResolveAction(h: CommandHelpers, args: any): Promise
1571
1671
  && status.activeModal.buttons.some((candidate) => typeof candidate === 'string' && candidate.trim())
1572
1672
  ? status.activeModal
1573
1673
  : null;
1574
- const effectiveModal = statusModal || surfacedModal;
1575
- const effectiveStatus = status?.status === 'waiting_approval' || targetState?.activeChat?.status === 'waiting_approval'
1674
+ const parsedStatus = !statusModal && !surfacedModal && typeof adapter.getScriptParsedStatus === 'function'
1675
+ ? (() => {
1676
+ try {
1677
+ return parseMaybeJson(adapter.getScriptParsedStatus());
1678
+ } catch {
1679
+ return null;
1680
+ }
1681
+ })()
1682
+ : null;
1683
+ const parsedModal = parsedStatus?.status === 'waiting_approval'
1684
+ && parsedStatus?.activeModal
1685
+ && Array.isArray(parsedStatus.activeModal.buttons)
1686
+ && parsedStatus.activeModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim())
1687
+ ? parsedStatus.activeModal
1688
+ : null;
1689
+ const effectiveModal = statusModal || surfacedModal || parsedModal;
1690
+ const effectiveStatus = status?.status === 'waiting_approval' || targetState?.activeChat?.status === 'waiting_approval' || parsedStatus?.status === 'waiting_approval'
1576
1691
  ? 'waiting_approval'
1577
1692
  : 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'}`);
1693
+ 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
1694
  if (!effectiveModal) {
1580
1695
  return { success: false, error: 'Not in approval state' };
1581
1696
  }