@adhdev/daemon-core 0.9.76-rc.9 → 0.9.76

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 (65) hide show
  1. package/dist/cli-adapters/provider-cli-adapter.d.ts +5 -2
  2. package/dist/cli-adapters/provider-cli-runtime.d.ts +1 -0
  3. package/dist/cli-adapters/provider-cli-shared.d.ts +24 -0
  4. package/dist/commands/chat-commands.d.ts +2 -0
  5. package/dist/commands/cli-manager.d.ts +17 -4
  6. package/dist/commands/mesh-coordinator.d.ts +2 -0
  7. package/dist/commands/router.d.ts +11 -0
  8. package/dist/config/mesh-config.d.ts +3 -0
  9. package/dist/git/git-types.d.ts +1 -1
  10. package/dist/git/git-worktree.d.ts +64 -0
  11. package/dist/git/index.d.ts +2 -0
  12. package/dist/index.d.ts +4 -4
  13. package/dist/index.js +2427 -561
  14. package/dist/index.js.map +1 -1
  15. package/dist/index.mjs +2432 -584
  16. package/dist/index.mjs.map +1 -1
  17. package/dist/mesh/coordinator-prompt.d.ts +1 -0
  18. package/dist/mesh/mesh-events.d.ts +18 -0
  19. package/dist/providers/chat-message-normalization.d.ts +40 -0
  20. package/dist/providers/cli-provider-instance.d.ts +7 -1
  21. package/dist/providers/contracts.d.ts +20 -1
  22. package/dist/providers/io-contracts.d.ts +17 -1
  23. package/dist/providers/provider-input-support.d.ts +18 -2
  24. package/dist/providers/provider-instance-manager.d.ts +1 -0
  25. package/dist/providers/provider-instance.d.ts +4 -0
  26. package/dist/repo-mesh-types.d.ts +34 -0
  27. package/dist/session-host/runtime-support.d.ts +2 -1
  28. package/dist/shared-types.d.ts +8 -0
  29. package/dist/types.d.ts +9 -0
  30. package/package.json +4 -5
  31. package/src/chat/subscription-updates.ts +3 -1
  32. package/src/cli-adapters/provider-cli-adapter.ts +44 -11
  33. package/src/cli-adapters/provider-cli-runtime.ts +3 -2
  34. package/src/cli-adapters/provider-cli-shared.ts +201 -15
  35. package/src/commands/chat-commands.ts +166 -16
  36. package/src/commands/cli-manager.ts +78 -5
  37. package/src/commands/handler.ts +13 -4
  38. package/src/commands/mesh-coordinator.ts +155 -5
  39. package/src/commands/router.d.ts +1 -0
  40. package/src/commands/router.ts +606 -32
  41. package/src/config/mesh-config.ts +27 -2
  42. package/src/git/git-commands.ts +5 -1
  43. package/src/git/git-types.ts +1 -0
  44. package/src/git/git-worktree.ts +214 -0
  45. package/src/git/index.ts +14 -0
  46. package/src/index.ts +20 -1
  47. package/src/mesh/coordinator-prompt.ts +36 -14
  48. package/src/mesh/mesh-events.ts +173 -42
  49. package/src/providers/acp-provider-instance.ts +118 -30
  50. package/src/providers/chat-message-normalization.ts +241 -0
  51. package/src/providers/cli-provider-instance.d.ts +2 -0
  52. package/src/providers/cli-provider-instance.ts +219 -13
  53. package/src/providers/contracts.ts +25 -1
  54. package/src/providers/io-contracts.ts +63 -5
  55. package/src/providers/provider-input-support.ts +125 -1
  56. package/src/providers/provider-instance-manager.ts +20 -1
  57. package/src/providers/provider-instance.ts +4 -0
  58. package/src/providers/provider-schema.ts +38 -8
  59. package/src/providers/read-chat-contract.ts +8 -0
  60. package/src/repo-mesh-types.ts +38 -0
  61. package/src/session-host/runtime-support.ts +55 -7
  62. package/src/shared-types.ts +8 -0
  63. package/src/status/builders.ts +5 -3
  64. package/src/status/reporter.ts +6 -0
  65. package/src/types.ts +9 -0
@@ -173,32 +173,218 @@ export interface CliProviderModule {
173
173
  function stripAnsi(str: string): string {
174
174
  // eslint-disable-next-line no-control-regex
175
175
  return str
176
- .replace(/\x1B\][^\x07]*\x07/g, '')
177
- .replace(/\x1B\][\s\S]*?\x1B\\/g, '')
176
+ .replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, '')
178
177
  .replace(/\x1B[P^_X][\s\S]*?(?:\x07|\x1B\\)/g, '')
179
- .replace(/\x1B\[\d*[A-HJKSTfG]/g, ' ')
180
- .replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '')
181
- .replace(/ +/g, ' ');
178
+ .replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
179
+ }
180
+
181
+ type SavedCursor = { row: number; col: number };
182
+
183
+ function parseCount(params: string, fallback = 1): number {
184
+ const first = Number(String(params || '').split(';')[0] || fallback);
185
+ return Math.max(1, Number.isFinite(first) ? first : fallback);
186
+ }
187
+
188
+ function isCombiningMark(ch: string): boolean {
189
+ return /[\u0300-\u036F\u1AB0-\u1AFF\u1DC0-\u1DFF\u20D0-\u20FF\uFE20-\uFE2F]/.test(ch);
190
+ }
191
+
192
+ function isWideCodePoint(ch: string): boolean {
193
+ const cp = ch.codePointAt(0) || 0;
194
+ return cp >= 0x1100 && (
195
+ cp <= 0x115F || cp === 0x2329 || cp === 0x232A ||
196
+ (cp >= 0x2E80 && cp <= 0xA4CF && cp !== 0x303F) ||
197
+ (cp >= 0xAC00 && cp <= 0xD7A3) ||
198
+ (cp >= 0xF900 && cp <= 0xFAFF) ||
199
+ (cp >= 0xFE10 && cp <= 0xFE19) ||
200
+ (cp >= 0xFE30 && cp <= 0xFE6F) ||
201
+ (cp >= 0xFF00 && cp <= 0xFF60) ||
202
+ (cp >= 0xFFE0 && cp <= 0xFFE6) ||
203
+ (cp >= 0x1F300 && cp <= 0x1FAFF)
204
+ );
205
+ }
206
+
207
+ /**
208
+ * Stateful, transcript-oriented terminal cell accumulator.
209
+ *
210
+ * CLI transcript parsing must not consume raw PTY append text for user-visible
211
+ * readback: CLIs rewrite prompts/status/tool lines with CR, BS, CSI cursor
212
+ * motion and erase-line. This accumulator preserves parser state across chunks
213
+ * and mutates rendered cells before exposing plain transcript text. It is a
214
+ * deliberately small terminal model for readback buffers; live UI rendering still
215
+ * uses TerminalScreen's ghostty/xterm backend.
216
+ */
217
+ export class TerminalTranscriptAccumulator {
218
+ private lines: string[][] = [[]];
219
+ private row = 0;
220
+ private col = 0;
221
+ private savedCursor: SavedCursor | null = null;
222
+ private pendingEscape = '';
223
+
224
+ append(data: string): string {
225
+ const input = this.pendingEscape + String(data || '');
226
+ this.pendingEscape = '';
227
+ for (let i = 0; i < input.length; i += 1) {
228
+ let ch = input[i];
229
+ if (ch === '\x1B') {
230
+ const consumed = this.consumeEscape(input.slice(i));
231
+ if (consumed === 0) {
232
+ this.pendingEscape = input.slice(i);
233
+ break;
234
+ }
235
+ i += consumed - 1;
236
+ continue;
237
+ }
238
+ const cp = input.codePointAt(i);
239
+ if (cp && cp > 0xFFFF) {
240
+ ch = String.fromCodePoint(cp);
241
+ i += 1;
242
+ }
243
+ this.writeControlOrChar(ch);
244
+ }
245
+ return this.getText();
246
+ }
247
+
248
+ reset(): void {
249
+ this.lines = [[]];
250
+ this.row = 0;
251
+ this.col = 0;
252
+ this.savedCursor = null;
253
+ this.pendingEscape = '';
254
+ }
255
+
256
+ getText(): string {
257
+ return this.lines.map(line => line.join('').replace(/[ \t]+$/g, '')).join('\n');
258
+ }
259
+
260
+ private ensureRow(row = this.row): void {
261
+ while (this.lines.length <= row) this.lines.push([]);
262
+ }
263
+
264
+ private writeControlOrChar(ch: string): void {
265
+ if (ch === '\r') {
266
+ this.col = 0;
267
+ return;
268
+ }
269
+ if (ch === '\n') {
270
+ this.row += 1;
271
+ this.col = 0;
272
+ this.ensureRow();
273
+ return;
274
+ }
275
+ if (ch === '\b') {
276
+ this.col = Math.max(0, this.col - 1);
277
+ return;
278
+ }
279
+ if (ch < ' ' || ch === '\x7F') return;
280
+
281
+ this.ensureRow();
282
+ const line = this.lines[this.row];
283
+ if (isCombiningMark(ch) && this.col > 0) {
284
+ line[this.col - 1] = `${line[this.col - 1] || ''}${ch}`;
285
+ return;
286
+ }
287
+ while (line.length < this.col) line.push(' ');
288
+ const wide = isWideCodePoint(ch);
289
+ line[this.col] = ch;
290
+ if (wide) line[this.col + 1] = '';
291
+ this.col += wide ? 2 : 1;
292
+ }
293
+
294
+ private consumeEscape(seq: string): number {
295
+ if (seq.length < 2) return 0;
296
+ const next = seq[1];
297
+ if (next === '7') {
298
+ this.savedCursor = { row: this.row, col: this.col };
299
+ return 2;
300
+ }
301
+ if (next === '8') {
302
+ if (this.savedCursor) {
303
+ this.row = this.savedCursor.row;
304
+ this.col = this.savedCursor.col;
305
+ this.ensureRow();
306
+ }
307
+ return 2;
308
+ }
309
+ if (next === ']') {
310
+ const bel = seq.indexOf('\x07', 2);
311
+ const st = seq.indexOf('\x1B\\', 2);
312
+ const end = bel >= 0 && (st < 0 || bel < st) ? bel + 1 : st >= 0 ? st + 2 : 0;
313
+ return end;
314
+ }
315
+ if (next === '[') {
316
+ const match = seq.match(/^\x1B\[([0-?]*)([ -/]*)([@-~])/);
317
+ if (!match) return seq.length < 32 ? 0 : 1;
318
+ this.applyCsi(match[1] || '', match[3]);
319
+ return match[0].length;
320
+ }
321
+ if (/[P^_X]/.test(next)) {
322
+ const bel = seq.indexOf('\x07', 2);
323
+ const st = seq.indexOf('\x1B\\', 2);
324
+ const end = bel >= 0 && (st < 0 || bel < st) ? bel + 1 : st >= 0 ? st + 2 : 0;
325
+ return end;
326
+ }
327
+ return 2;
328
+ }
329
+
330
+ private applyCsi(params: string, final: string): void {
331
+ const count = parseCount(params);
332
+ this.ensureRow();
333
+ if (final === 'A') this.row = Math.max(0, this.row - count);
334
+ else if (final === 'B') this.row += count;
335
+ else if (final === 'C') this.col += count;
336
+ else if (final === 'D') this.col = Math.max(0, this.col - count);
337
+ else if (final === 'G') this.col = Math.max(0, count - 1);
338
+ else if (final === 'H' || final === 'f') {
339
+ const parts = String(params || '').split(';');
340
+ this.row = Math.max(0, (Number(parts[0] || 1) || 1) - 1);
341
+ this.col = Math.max(0, (Number(parts[1] || 1) || 1) - 1);
342
+ } else if (final === 'J') {
343
+ const mode = Number(params || 0) || 0;
344
+ if (mode === 2 || mode === 3) {
345
+ this.lines = [[]];
346
+ this.row = 0;
347
+ this.col = 0;
348
+ } else if (mode === 0) {
349
+ this.lines[this.row] = this.lines[this.row].slice(0, this.col);
350
+ this.lines.splice(this.row + 1);
351
+ } else if (mode === 1) {
352
+ for (let r = 0; r < this.row; r += 1) this.lines[r] = [];
353
+ const line = this.lines[this.row];
354
+ for (let c = 0; c <= Math.min(this.col, line.length - 1); c += 1) line[c] = ' ';
355
+ }
356
+ } else if (final === 'K') {
357
+ const mode = Number(params || 0) || 0;
358
+ const line = this.lines[this.row];
359
+ if (mode === 2) this.lines[this.row] = [];
360
+ else if (mode === 1) {
361
+ for (let c = 0; c <= Math.min(this.col, line.length - 1); c += 1) line[c] = ' ';
362
+ } else {
363
+ this.lines[this.row] = line.slice(0, this.col);
364
+ }
365
+ } else if (final === 's') {
366
+ this.savedCursor = { row: this.row, col: this.col };
367
+ } else if (final === 'u') {
368
+ if (this.savedCursor) {
369
+ this.row = this.savedCursor.row;
370
+ this.col = this.savedCursor.col;
371
+ }
372
+ }
373
+ this.ensureRow();
374
+ }
182
375
  }
183
376
 
184
377
  function stripTerminalNoise(str: string): string {
185
378
  return String(str || '')
186
379
  .replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F]/g, '')
187
- .replace(/(^|[\s([])(?:\??\d{1,4}(?:;\d{1,4})*[A-Za-z])(?=$|[\s)\]])/g, '$1')
188
- .replace(/(^|[\s([])(?:\[\??\d{1,4}(?:;\d{1,4})*[A-Za-z])(?=$|[\s)\]])/g, '$1')
189
- .replace(/(^|[\s([])(?:\d{1,4};\?)(?=$|[\s)\]])/g, '$1')
190
- .replace(/(^|[\s([])(?:\d+\$r[0-9;\" ]*[A-Za-z]?)(?=$|[\s)\]])/g, '$1')
191
- .replace(/(^|[\s([])(?:>\|[A-Za-z0-9_.:-]+(?:\([^)]*\))?)(?=$|[\s)\]])/g, '$1')
192
- .replace(/(^|[\s([])(?:[A-Z]\d(?:\s+[A-Z]\d)+)(?=$|[\s)\]])/g, '$1')
193
- .replace(/(^|[\s([])(?:\d+;[^\s)\]]+)(?=$|[\s)\]])/g, '$1')
194
380
  .replace(/\r+/g, '\n')
195
381
  .replace(/[ \t]+\n/g, '\n')
196
- .replace(/\n{3,}/g, '\n\n')
197
- .replace(/ {2,}/g, ' ');
382
+ .replace(/\n{4,}/g, '\n\n\n');
198
383
  }
199
384
 
200
385
  export function sanitizeTerminalText(str: string): string {
201
- return stripTerminalNoise(stripAnsi(str));
386
+ const accumulator = new TerminalTranscriptAccumulator();
387
+ return stripTerminalNoise(stripAnsi(accumulator.append(str)));
202
388
  }
203
389
 
204
390
  export function listCliScriptNames(scripts: CliScripts | undefined): string[] {
@@ -16,18 +16,24 @@ import type { ProviderInstance } from '../providers/provider-instance.js';
16
16
  import { readProviderChatHistory } from '../config/chat-history.js';
17
17
  import { LOG, getRecentLogs } from '../logging/logger.js';
18
18
  import { getRecentDebugTrace, recordDebugTrace } from '../logging/debug-trace.js';
19
- import { buildChatMessageSignature } from '../chat/chat-signatures.js';
19
+ import { buildChatMessageSignature, hashSignatureParts } 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 {
@@ -87,16 +93,51 @@ function buildRecentSendKey(h: CommandHelpers, args: any, provider: ProviderModu
87
93
  return `${transport}:${target}:${signature.trim()}`;
88
94
  }
89
95
 
90
- function buildSendInputSignature(input: InputEnvelope): string {
96
+ function summarizeSendInputPart(part: any): string {
97
+ if (!part || typeof part !== 'object') return String(part ?? '');
98
+ if (part.type === 'text') return `text:${String(part.text || '').trim()}`;
99
+ const fields = [
100
+ `type=${String(part.type || '')}`,
101
+ `mime=${String(part.mimeType || '')}`,
102
+ `uri=${String(part.uri || '')}`,
103
+ `name=${String(part.name || '')}`,
104
+ ];
105
+ const data = typeof part.data === 'string'
106
+ ? part.data
107
+ : typeof part.resource?.blob === 'string'
108
+ ? part.resource.blob
109
+ : '';
110
+ if (data) fields.push(`dataLen=${data.length}`, `dataHash=${hashSignatureParts([data]).slice(0, 12)}`);
111
+ const textish = [part.alt, part.transcript, part.description, part.title, part.resource?.uri]
112
+ .filter((value) => typeof value === 'string' && value.trim())
113
+ .join('\u001f');
114
+ if (textish) fields.push(`meta=${hashSignatureParts([textish]).slice(0, 12)}`);
115
+ return fields.join(';');
116
+ }
117
+
118
+ export function buildSendInputSignature(input: InputEnvelope): string {
91
119
  const text = typeof input.textFallback === 'string' ? input.textFallback.trim() : '';
92
- if (text) return text;
93
- return JSON.stringify(input.parts || []);
120
+ const partSummaries = (input.parts || []).map(summarizeSendInputPart);
121
+ return hashSignatureParts([text, ...partSummaries]);
94
122
  }
95
123
 
96
124
  function getSendChatInputEnvelope(args: any): InputEnvelope {
97
125
  return normalizeInputEnvelope(args?.input ? { input: args.input } : args);
98
126
  }
99
127
 
128
+ function sleep(ms: number): Promise<void> {
129
+ return new Promise((resolve) => setTimeout(resolve, ms));
130
+ }
131
+
132
+ async function waitOnceForFreshHermesCliStart(adapter: CliAdapter, log: (msg: string) => void): Promise<void> {
133
+ if (adapter.cliType !== 'hermes-cli') return;
134
+ const status = typeof adapter.getStatus === 'function' ? adapter.getStatus()?.status : undefined;
135
+ if (status !== 'starting') return;
136
+
137
+ log(`Hermes CLI is still starting; waiting ${HERMES_CLI_STARTING_SEND_SETTLE_MS}ms before first send`);
138
+ await sleep(HERMES_CLI_STARTING_SEND_SETTLE_MS);
139
+ }
140
+
100
141
  function getHistorySessionId(h: CommandHelpers, args: any): string | undefined {
101
142
  const explicit = typeof args?.historySessionId === 'string' ? args.historySessionId.trim() : '';
102
143
  if (explicit) return explicit;
@@ -177,7 +218,7 @@ function normalizeReadChatTailLimit(args: any): number {
177
218
 
178
219
  function normalizeReadChatMessages(payload: Record<string, any>): ChatMessage[] {
179
220
  const messages = Array.isArray(payload.messages) ? payload.messages as ChatMessage[] : [];
180
- return messages;
221
+ return normalizeChatMessages(messages);
181
222
  }
182
223
 
183
224
 
@@ -250,6 +291,40 @@ function normalizeReadChatCommandStatus(status: unknown, activeModal: unknown):
250
291
  }
251
292
  }
252
293
 
294
+ function isGeneratingLikeStatus(status: unknown): boolean {
295
+ return status === 'generating' || status === 'streaming' || status === 'long_generating' || status === 'starting';
296
+ }
297
+
298
+ function shouldTrustCliAdapterTerminalStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any): boolean {
299
+ if (!isGeneratingLikeStatus(parsedStatus)) return false;
300
+ if (hasNonEmptyModalButtons(activeModal)) return false;
301
+ const adapterRawStatus = typeof adapterStatus?.status === 'string' ? adapterStatus.status.trim() : '';
302
+ if (adapterRawStatus !== 'idle') return false;
303
+ if (typeof adapter.isProcessing === 'function' && adapter.isProcessing()) return false;
304
+ return true;
305
+ }
306
+
307
+ function normalizeCliReadChatStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any): string {
308
+ if (shouldTrustCliAdapterTerminalStatus(parsedStatus, activeModal, adapter, adapterStatus)) return 'idle';
309
+ return typeof parsedStatus === 'string' && parsedStatus.trim() ? parsedStatus : 'idle';
310
+ }
311
+
312
+ function finalizeStreamingMessagesWhenIdle(messages: ChatMessage[], status: string): ChatMessage[] {
313
+ if (status !== 'idle') return messages;
314
+ return messages.map((message) => {
315
+ const meta = message.meta && typeof message.meta === 'object'
316
+ ? message.meta as Record<string, unknown>
317
+ : undefined;
318
+ const hasStreamingMeta = meta?.streaming === true;
319
+ if (message.bubbleState !== 'streaming' && !hasStreamingMeta) return message;
320
+ return {
321
+ ...message,
322
+ ...(message.bubbleState === 'streaming' ? { bubbleState: 'final' as const } : {}),
323
+ ...(hasStreamingMeta ? { meta: { ...meta, streaming: false } } : {}),
324
+ };
325
+ });
326
+ }
327
+
253
328
  function buildReadChatCommandResult(payload: Record<string, any>, args: any): CommandResult {
254
329
  let validatedPayload: Record<string, any>;
255
330
  const debugReadChat = payload?.debugReadChat && typeof payload.debugReadChat === 'object'
@@ -264,13 +339,26 @@ function buildReadChatCommandResult(payload: Record<string, any>, args: any): Co
264
339
  return { success: false, error: error?.message || String(error) };
265
340
  }
266
341
  const messages = normalizeReadChatMessages(validatedPayload);
267
- const sync = buildFullTail(messages, normalizeReadChatTailLimit(args));
342
+ const visibleMessages = filterUserFacingChatMessages(messages);
343
+ const sync = buildFullTail(visibleMessages, normalizeReadChatTailLimit(args));
344
+ const hiddenMsgCount = Math.max(0, messages.length - visibleMessages.length);
345
+ const returnedDebugReadChat = debugReadChat
346
+ ? {
347
+ ...debugReadChat,
348
+ fullMsgCount: typeof debugReadChat.fullMsgCount === 'number'
349
+ ? debugReadChat.fullMsgCount
350
+ : messages.length,
351
+ visibleMsgCount: visibleMessages.length,
352
+ hiddenMsgCount,
353
+ returnedMsgCount: sync.messages.length,
354
+ }
355
+ : undefined;
268
356
  return {
269
357
  success: true,
270
358
  ...validatedPayload,
271
359
  messages: sync.messages,
272
360
  totalMessages: sync.totalMessages,
273
- ...(debugReadChat ? { debugReadChat } : {}),
361
+ ...(returnedDebugReadChat ? { debugReadChat: returnedDebugReadChat } : {}),
274
362
  };
275
363
  }
276
364
 
@@ -464,6 +552,18 @@ function buildChatDebugBundleSummary(bundle: Record<string, unknown>): Record<st
464
552
  const readChat = bundle.readChat && typeof bundle.readChat === 'object' ? bundle.readChat as Record<string, unknown> : {};
465
553
  const cli = bundle.cli && typeof bundle.cli === 'object' ? bundle.cli as Record<string, unknown> : null;
466
554
  const frontend = bundle.frontend && typeof bundle.frontend === 'object' ? bundle.frontend as Record<string, unknown> : null;
555
+ const debugReadChat = readChat.debugReadChat && typeof readChat.debugReadChat === 'object'
556
+ ? readChat.debugReadChat as Record<string, unknown>
557
+ : {};
558
+ const parsedStatus = cli?.parsedStatus && typeof cli.parsedStatus === 'object'
559
+ ? cli.parsedStatus as Record<string, unknown>
560
+ : null;
561
+ const cliParsedMessageCount = Array.isArray(parsedStatus?.messages) ? parsedStatus.messages.length : undefined;
562
+ const readChatReturnedMessages = Array.isArray(readChat.messagesTail) ? readChat.messagesTail.length : undefined;
563
+ const cliPartialResponse = typeof cli?.partialResponse === 'string' ? cli.partialResponse : '';
564
+ const readChatStatus = typeof readChat.status === 'string' ? readChat.status : '';
565
+ const cliStatus = typeof cli?.status === 'string' ? cli.status : '';
566
+ const cliParsedStatus = typeof parsedStatus?.status === 'string' ? parsedStatus.status : '';
467
567
  return {
468
568
  createdAt: bundle.createdAt,
469
569
  targetSessionId: target.targetSessionId,
@@ -472,8 +572,22 @@ function buildChatDebugBundleSummary(bundle: Record<string, unknown>): Record<st
472
572
  readChatSuccess: readChat.success,
473
573
  readChatStatus: readChat.status,
474
574
  readChatTotalMessages: readChat.totalMessages,
575
+ readChatReturnedMessages,
475
576
  cliStatus: cli?.status,
577
+ cliParsedStatus: cliParsedStatus || undefined,
476
578
  cliMessageCount: cli?.messageCount,
579
+ cliParsedMessageCount,
580
+ cliPartialResponseChars: cliPartialResponse.length,
581
+ parserAdapterStatusMismatch: Boolean(cliStatus && cliParsedStatus && cliStatus !== cliParsedStatus),
582
+ parserReadChatStatusMismatch: Boolean(readChatStatus && cliParsedStatus && readChatStatus !== cliParsedStatus),
583
+ readChatDebug: Object.keys(debugReadChat).length ? {
584
+ adapterStatus: debugReadChat.adapterStatus,
585
+ parsedStatus: debugReadChat.parsedStatus,
586
+ returnedStatus: debugReadChat.returnedStatus,
587
+ parsedMsgCount: debugReadChat.parsedMsgCount,
588
+ returnedMsgCount: debugReadChat.returnedMsgCount,
589
+ shouldPreferAdapterMessages: debugReadChat.shouldPreferAdapterMessages,
590
+ } : undefined,
477
591
  hasFrontendSnapshot: !!frontend,
478
592
  };
479
593
  }
@@ -720,7 +834,7 @@ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<C
720
834
  }
721
835
 
722
836
  export async function handleReadChat(h: CommandHelpers, args: any): Promise<CommandResult> {
723
- const provider = h.getProvider(args?.agentType);
837
+ const provider = h.getProvider(args?.agentType || args?.providerType);
724
838
  const transport = getTargetTransport(h, provider);
725
839
  const historySessionId = getHistorySessionId(h, args);
726
840
 
@@ -760,10 +874,17 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
760
874
  ? parsedRecord.coverage
761
875
  : undefined;
762
876
  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}`);
877
+ const returnedStatus = normalizeCliReadChatStatus(parsedRecord.status, activeModal, adapter, adapterStatus);
878
+ const runtimeMessageMerger = getTargetInstance(h, args) as RuntimeChatMessageMerger | null;
879
+ const parsedMessages = finalizeStreamingMessagesWhenIdle(parsedRecord.messages as ChatMessage[], returnedStatus);
880
+ const returnedMessages = runtimeMessageMerger?.category === 'cli'
881
+ && runtimeMessageMerger.type === adapter.cliType
882
+ && typeof runtimeMessageMerger.mergeRuntimeChatMessages === 'function'
883
+ ? runtimeMessageMerger.mergeRuntimeChatMessages(parsedMessages)
884
+ : parsedMessages;
885
+ 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
886
  return buildReadChatCommandResult({
766
- messages: parsedRecord.messages,
887
+ messages: returnedMessages,
767
888
  status: returnedStatus,
768
889
  activeModal,
769
890
  debugReadChat: {
@@ -774,7 +895,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
774
895
  returnedStatus: String(returnedStatus || ''),
775
896
  shouldPreferAdapterMessages: false,
776
897
  parsedMsgCount: parsedRecord.messages.length,
777
- returnedMsgCount: parsedRecord.messages.length,
898
+ returnedMsgCount: returnedMessages.length,
778
899
  },
779
900
  ...(title ? { title } : {}),
780
901
  ...(providerSessionId ? { providerSessionId } : {}),
@@ -1041,14 +1162,28 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
1041
1162
  }
1042
1163
  }
1043
1164
 
1044
- // PTY transport: text-only send via adapter
1165
+ // PTY transport: route structured input through the provider instance so
1166
+ // provider-specific CLI attachment strategies (for example Hermes file-path
1167
+ // image prompts) are applied instead of collapsing everything to text.
1045
1168
  if (transport === 'pty') {
1046
1169
  const adapter = getTargetedCliAdapter(h, args, provider?.type);
1047
1170
  if (adapter) {
1048
1171
  _log(`${transport} adapter: ${adapter.cliType}`);
1049
1172
  try {
1173
+ const hasStructuredParts = input.parts.some((part) => part.type !== 'text');
1174
+ if (hasStructuredParts) {
1175
+ const target = getTargetInstance(h, args);
1176
+ if (!target || target.category !== 'cli') {
1177
+ return { success: false, error: `CLI instance not found for ${provider?.type || args?.agentType || 'unknown'}` };
1178
+ }
1179
+ assertProviderSupportsDeclaredInput(provider, input);
1180
+ await waitOnceForFreshHermesCliStart(adapter, _log);
1181
+ target.onEvent('send_message', { input });
1182
+ return _logSendSuccess(`${transport}-instance`, target.type);
1183
+ }
1050
1184
  assertTextOnlyInput(provider, input);
1051
1185
  if (!text) return { success: false, error: 'text required for PTY send' };
1186
+ await waitOnceForFreshHermesCliStart(adapter, _log);
1052
1187
  await adapter.sendMessage(text);
1053
1188
  return _logSendSuccess(`${transport}-adapter`, adapter.cliType);
1054
1189
  } catch (e: any) {
@@ -1571,11 +1706,26 @@ export async function handleResolveAction(h: CommandHelpers, args: any): Promise
1571
1706
  && status.activeModal.buttons.some((candidate) => typeof candidate === 'string' && candidate.trim())
1572
1707
  ? status.activeModal
1573
1708
  : null;
1574
- const effectiveModal = statusModal || surfacedModal;
1575
- const effectiveStatus = status?.status === 'waiting_approval' || targetState?.activeChat?.status === 'waiting_approval'
1709
+ const parsedStatus = !statusModal && !surfacedModal && typeof adapter.getScriptParsedStatus === 'function'
1710
+ ? (() => {
1711
+ try {
1712
+ return parseMaybeJson(adapter.getScriptParsedStatus());
1713
+ } catch {
1714
+ return null;
1715
+ }
1716
+ })()
1717
+ : null;
1718
+ const parsedModal = parsedStatus?.status === 'waiting_approval'
1719
+ && parsedStatus?.activeModal
1720
+ && Array.isArray(parsedStatus.activeModal.buttons)
1721
+ && parsedStatus.activeModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim())
1722
+ ? parsedStatus.activeModal
1723
+ : null;
1724
+ const effectiveModal = statusModal || surfacedModal || parsedModal;
1725
+ const effectiveStatus = status?.status === 'waiting_approval' || targetState?.activeChat?.status === 'waiting_approval' || parsedStatus?.status === 'waiting_approval'
1576
1726
  ? 'waiting_approval'
1577
1727
  : 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'}`);
1728
+ 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
1729
  if (!effectiveModal) {
1580
1730
  return { success: false, error: 'Not in approval state' };
1581
1731
  }