@adhdev/daemon-core 0.9.82-rc.13 → 0.9.82-rc.130

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 (82) hide show
  1. package/dist/chat/subscription-updates.d.ts +1 -0
  2. package/dist/cli-adapter-types.d.ts +4 -1
  3. package/dist/cli-adapters/provider-cli-adapter.d.ts +25 -1
  4. package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
  5. package/dist/cli-adapters/provider-cli-shared.d.ts +14 -0
  6. package/dist/commands/router.d.ts +22 -0
  7. package/dist/config/chat-history.d.ts +4 -0
  8. package/dist/config/mesh-config.d.ts +68 -1
  9. package/dist/git/git-commands.d.ts +5 -1
  10. package/dist/index.d.ts +15 -5
  11. package/dist/index.js +7461 -1380
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.mjs +7417 -1364
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/installer.d.ts +1 -4
  16. package/dist/launch.d.ts +1 -1
  17. package/dist/logging/async-batch-writer.d.ts +10 -0
  18. package/dist/mesh/beads-db.d.ts +18 -0
  19. package/dist/mesh/mesh-active-work.d.ts +73 -0
  20. package/dist/mesh/mesh-events.d.ts +54 -5
  21. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  22. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  23. package/dist/mesh/mesh-ledger.d.ts +38 -1
  24. package/dist/mesh/mesh-refine-status.d.ts +27 -0
  25. package/dist/mesh/mesh-work-queue.d.ts +27 -5
  26. package/dist/mesh/preview-freshness.d.ts +18 -0
  27. package/dist/mesh/refine-config.d.ts +193 -0
  28. package/dist/mesh/worktree-bootstrap-config.d.ts +115 -0
  29. package/dist/providers/chat-message-normalization.d.ts +1 -0
  30. package/dist/providers/cli-provider-instance.d.ts +6 -1
  31. package/dist/repo-mesh-types.d.ts +62 -0
  32. package/dist/status/reporter.d.ts +2 -0
  33. package/package.json +3 -1
  34. package/src/boot/daemon-lifecycle.ts +1 -0
  35. package/src/chat/subscription-updates.ts +5 -1
  36. package/src/cli-adapter-types.ts +2 -1
  37. package/src/cli-adapters/provider-cli-adapter.ts +473 -18
  38. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  39. package/src/cli-adapters/provider-cli-parse.ts +4 -0
  40. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  41. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  42. package/src/cli-adapters/provider-cli-shared.ts +32 -10
  43. package/src/commands/chat-commands.ts +960 -40
  44. package/src/commands/cli-manager.ts +138 -2
  45. package/src/commands/handler.ts +8 -1
  46. package/src/commands/mesh-coordinator.ts +13 -143
  47. package/src/commands/router.ts +3238 -423
  48. package/src/config/chat-history.ts +37 -9
  49. package/src/config/mesh-config.ts +249 -2
  50. package/src/config/recent-activity.ts +8 -2
  51. package/src/daemon/dev-cli-debug.ts +10 -1
  52. package/src/detection/ide-detector.ts +26 -16
  53. package/src/git/git-commands.ts +17 -5
  54. package/src/index.ts +41 -4
  55. package/src/installer.d.ts +1 -1
  56. package/src/installer.ts +8 -6
  57. package/src/launch.d.ts +1 -1
  58. package/src/launch.ts +37 -28
  59. package/src/logging/async-batch-writer.ts +55 -0
  60. package/src/logging/logger.ts +2 -1
  61. package/src/mesh/beads-db.ts +176 -0
  62. package/src/mesh/coordinator-prompt.ts +31 -8
  63. package/src/mesh/mesh-active-work.ts +295 -0
  64. package/src/mesh/mesh-events.ts +595 -48
  65. package/src/mesh/mesh-fast-forward.ts +430 -0
  66. package/src/mesh/mesh-host-ownership.ts +73 -0
  67. package/src/mesh/mesh-ledger.ts +138 -1
  68. package/src/mesh/mesh-refine-status.ts +145 -0
  69. package/src/mesh/mesh-work-queue.ts +199 -137
  70. package/src/mesh/preview-freshness.ts +118 -0
  71. package/src/mesh/refine-config.ts +366 -0
  72. package/src/mesh/worktree-bootstrap-config.ts +234 -0
  73. package/src/providers/approval-utils.ts +12 -5
  74. package/src/providers/chat-message-normalization.ts +7 -12
  75. package/src/providers/cli-provider-instance.ts +289 -36
  76. package/src/providers/ide-provider-instance.ts +17 -3
  77. package/src/providers/provider-loader.ts +10 -4
  78. package/src/providers/read-chat-contract.ts +1 -1
  79. package/src/providers/version-archive.ts +38 -20
  80. package/src/repo-mesh-types.ts +67 -0
  81. package/src/status/reporter.ts +15 -0
  82. package/src/system/host-memory.ts +29 -12
@@ -12,8 +12,9 @@ import type { CliAdapter } from '../cli-adapter-types.js';
12
12
  import { flattenContent, normalizeInputEnvelope, type InputEnvelope, type ProviderModule, type ProviderScripts } from '../providers/contracts.js';
13
13
  import { assertProviderSupportsDeclaredInput, assertTextOnlyInput } from '../providers/provider-input-support.js';
14
14
  import { validateReadChatResultPayload } from '../providers/read-chat-contract.js';
15
+ import { pickApprovalButton } from '../providers/approval-utils.js';
15
16
  import type { ProviderInstance } from '../providers/provider-instance.js';
16
- import { readProviderChatHistory } from '../config/chat-history.js';
17
+ import { isNativeSourceCanonicalHistory, readChatHistory, readProviderChatHistory } from '../config/chat-history.js';
17
18
  import { LOG, getRecentLogs } from '../logging/logger.js';
18
19
  import { getRecentDebugTrace, recordDebugTrace } from '../logging/debug-trace.js';
19
20
  import { buildChatMessageSignature, hashSignatureParts } from '../chat/chat-signatures.js';
@@ -24,6 +25,13 @@ import { filterUserFacingChatMessages, normalizeChatMessages } from '../provider
24
25
  const RECENT_SEND_WINDOW_MS = 1200;
25
26
  export const READ_CHAT_PROVIDER_EVAL_TIMEOUT_MS = 25_000;
26
27
  const HERMES_CLI_STARTING_SEND_SETTLE_MS = 2_000;
28
+ const CLI_NATIVE_HISTORY_FRESH_MS = 5 * 60_000;
29
+ // Fallback list for supportsCliNativeTranscript() when the ProviderModule is
30
+ // unavailable (e.g. provider config not yet loaded). The authoritative check is
31
+ // provider.canonicalHistory via isNativeSourceCanonicalHistory(). New providers
32
+ // with native transcripts should set canonicalHistory in their provider.json
33
+ // rather than adding entries here.
34
+ const CLI_NATIVE_TRANSCRIPT_PROVIDERS = new Set(['codex-cli', 'claude-cli', 'hermes-cli', 'antigravity-cli']);
27
35
  const recentSendByTarget = new Map<string, number>();
28
36
 
29
37
  interface ApprovalSelectableInstance extends ProviderInstance {
@@ -32,6 +40,7 @@ interface ApprovalSelectableInstance extends ProviderInstance {
32
40
 
33
41
  interface RuntimeChatMessageMerger extends ProviderInstance {
34
42
  mergeRuntimeChatMessages?(messages: ChatMessage[]): ChatMessage[];
43
+ recordAcknowledgedUserInput?(input: InputEnvelope | string): void;
35
44
  }
36
45
 
37
46
  type LegacyStringScript = (params?: Record<string, unknown> | string) => string;
@@ -48,6 +57,16 @@ function getTargetedCliAdapter(h: CommandHelpers, args: any, providerType?: stri
48
57
  return h.getCliAdapter(args?.targetSessionId || providerType || h.currentSession?.providerType || h.currentManagerKey);
49
58
  }
50
59
 
60
+ function getExplicitHistorySessionId(args: any): string | undefined {
61
+ const explicit = typeof args?.historySessionId === 'string' ? args.historySessionId.trim() : '';
62
+ if (explicit) return explicit;
63
+
64
+ const explicitProviderSessionId = typeof args?.providerSessionId === 'string' ? args.providerSessionId.trim() : '';
65
+ if (explicitProviderSessionId) return explicitProviderSessionId;
66
+
67
+ return undefined;
68
+ }
69
+
51
70
  function getTargetInstance(h: CommandHelpers, args: any): ApprovalSelectableInstance | null {
52
71
  const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
53
72
  const sessionId = targetSessionId || h.currentSession?.sessionId || '';
@@ -139,19 +158,47 @@ async function waitOnceForFreshHermesCliStart(adapter: CliAdapter, log: (msg: st
139
158
  }
140
159
 
141
160
  function getHistorySessionId(h: CommandHelpers, args: any): string | undefined {
142
- const explicit = typeof args?.historySessionId === 'string' ? args.historySessionId.trim() : '';
161
+ const explicit = getExplicitHistorySessionId(args);
143
162
  if (explicit) return explicit;
144
163
 
145
- const explicitProviderSessionId = typeof args?.providerSessionId === 'string' ? args.providerSessionId.trim() : '';
146
- if (explicitProviderSessionId) return explicitProviderSessionId;
147
-
148
164
  const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
149
165
  if (!targetSessionId) return undefined;
150
166
 
151
- const instance = h.ctx.instanceManager?.getInstance(targetSessionId);
167
+ const session = h.ctx.sessionRegistry?.get(targetSessionId) as any;
168
+ const registeredProviderSessionId = typeof session?.providerSessionId === 'string' ? session.providerSessionId.trim() : '';
169
+ if (registeredProviderSessionId) return registeredProviderSessionId;
170
+
171
+ const instance = getTargetInstance(h, args);
152
172
  const state = instance?.getState?.();
153
173
  const providerSessionId = typeof state?.providerSessionId === 'string' ? state.providerSessionId.trim() : '';
154
- return providerSessionId || targetSessionId;
174
+ if (providerSessionId) return providerSessionId;
175
+
176
+ const currentSession = h.currentSession as any;
177
+ if (currentSession?.sessionId === targetSessionId) {
178
+ const currentProviderSessionId = typeof currentSession.providerSessionId === 'string'
179
+ ? currentSession.providerSessionId.trim()
180
+ : '';
181
+ if (currentProviderSessionId) return currentProviderSessionId;
182
+ }
183
+
184
+ return targetSessionId;
185
+ }
186
+
187
+ function resolveCliNativeHistorySessionId(args: any, currentHistorySessionId: string | undefined, parsedProviderSessionId: string | undefined): string | undefined {
188
+ const explicit = getExplicitHistorySessionId(args);
189
+ if (explicit) return explicit;
190
+
191
+ const parsed = typeof parsedProviderSessionId === 'string' ? parsedProviderSessionId.trim() : '';
192
+ const current = typeof currentHistorySessionId === 'string' ? currentHistorySessionId.trim() : '';
193
+ const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
194
+
195
+ // getHistorySessionId falls back to the runtime session id when no native
196
+ // handle has been registered yet. For live CLI adapters the parser may
197
+ // already know the provider-native handle; prefer it over the runtime id so
198
+ // exact native reads do not miss the worker transcript and fall back to PTY
199
+ // or same-workspace history.
200
+ if (parsed && (!current || current === targetSessionId)) return parsed;
201
+ return current || parsed || undefined;
155
202
  }
156
203
 
157
204
  function getInteractionId(args: any): string | undefined {
@@ -221,6 +268,446 @@ function normalizeReadChatMessages(payload: Record<string, any>): ChatMessage[]
221
268
  return normalizeChatMessages(messages);
222
269
  }
223
270
 
271
+ function getMessageNewestReceivedAt(messages: Array<{ receivedAt?: unknown; timestamp?: unknown }>): number {
272
+ let newest = 0;
273
+ for (const message of messages) {
274
+ const receivedAt = Number(message?.receivedAt ?? message?.timestamp ?? 0);
275
+ if (Number.isFinite(receivedAt) && receivedAt > newest) newest = receivedAt;
276
+ }
277
+ return newest;
278
+ }
279
+
280
+ function readHistorySessionIdFromMessages(messages: ChatMessage[]): string | undefined {
281
+ for (const message of messages as Array<ChatMessage & { historySessionId?: unknown }>) {
282
+ const historySessionId = typeof message?.historySessionId === 'string' ? message.historySessionId.trim() : '';
283
+ if (historySessionId) return historySessionId;
284
+ }
285
+ return undefined;
286
+ }
287
+
288
+ function shouldPreserveNativeIdentity(providerType: string, sessionId: string, message: ChatMessage): boolean {
289
+ const providerUnitKey = typeof message.providerUnitKey === 'string' ? message.providerUnitKey.trim() : '';
290
+ const turnKey = typeof message._turnKey === 'string' ? message._turnKey.trim() : '';
291
+ if (!providerUnitKey || !turnKey) return false;
292
+ if (providerType === 'hermes-cli' && sessionId) {
293
+ return providerUnitKey.startsWith(`${providerType}:native:${sessionId}:`)
294
+ && turnKey.startsWith(`${providerType}:native-turn:${sessionId}:`);
295
+ }
296
+ return true;
297
+ }
298
+
299
+ function normalizeNativeHistoryMessages(providerType: string, messages: ChatMessage[], nativeSessionId?: string): ChatMessage[] {
300
+ let turnIndex = 0;
301
+ return normalizeChatMessages(messages).map((message, index) => {
302
+ const role = typeof message.role === 'string' ? message.role.trim().toLowerCase() : '';
303
+ const kind = typeof message.kind === 'string' && message.kind.trim() ? message.kind.trim() : (role === 'system' ? 'system' : 'standard');
304
+ if ((role === 'user' || role === 'human') && index > 0) turnIndex += 1;
305
+ const historySessionId = typeof (message as any).historySessionId === 'string'
306
+ ? (message as any).historySessionId.trim()
307
+ : '';
308
+ const contentHash = hashSignatureParts([
309
+ providerType,
310
+ historySessionId,
311
+ String(message.receivedAt || message.timestamp || index),
312
+ role,
313
+ kind,
314
+ flattenContent(message.content),
315
+ ]).slice(0, 12);
316
+ const nativeIdentitySessionId = historySessionId || (typeof nativeSessionId === 'string' ? nativeSessionId.trim() : '');
317
+ const preserveNativeIdentity = shouldPreserveNativeIdentity(providerType, nativeIdentitySessionId, message);
318
+ const existingProviderUnitKey = typeof message.providerUnitKey === 'string' ? message.providerUnitKey.trim() : '';
319
+ const existingTurnKey = typeof message._turnKey === 'string' ? message._turnKey.trim() : '';
320
+ const providerUnitKey = preserveNativeIdentity
321
+ ? existingProviderUnitKey
322
+ : `${providerType}:native:${nativeIdentitySessionId || 'workspace'}:${index}:${role || 'message'}:${kind}:${contentHash}`;
323
+ const meta = message.meta && typeof message.meta === 'object' ? message.meta as Record<string, unknown> : undefined;
324
+ const isSystemSessionStart = role === 'system' || kind === 'system' || kind === 'session_start';
325
+ const isActivity = role === 'assistant' && (kind === 'tool' || kind === 'terminal' || kind === 'thought');
326
+ return {
327
+ ...message,
328
+ role: role === 'human' ? 'user' : (role || 'assistant'),
329
+ kind: isSystemSessionStart ? 'system' : kind,
330
+ providerUnitKey,
331
+ bubbleId: typeof message.bubbleId === 'string' && message.bubbleId.trim()
332
+ && preserveNativeIdentity
333
+ ? message.bubbleId.trim()
334
+ : `bubble:${providerUnitKey}`,
335
+ _turnKey: preserveNativeIdentity
336
+ ? existingTurnKey
337
+ : `${providerType}:native-turn:${nativeIdentitySessionId || 'workspace'}:${turnIndex}`,
338
+ bubbleState: message.bubbleState || 'final',
339
+ ...(isSystemSessionStart ? {
340
+ visibility: message.visibility || 'hidden',
341
+ transcriptVisibility: message.transcriptVisibility || 'hidden',
342
+ audience: message.audience || 'internal',
343
+ source: message.source || 'runtime_status',
344
+ } : isActivity ? {
345
+ source: message.source || (kind === 'terminal' ? 'terminal_command' : 'tool_call'),
346
+ meta: { ...meta, label: message.senderName || meta?.label || (kind === 'terminal' ? 'Terminal' : 'Tool') },
347
+ } : {
348
+ source: message.source || (role === 'assistant' ? 'assistant_text' : undefined),
349
+ }),
350
+ } as ChatMessage;
351
+ });
352
+ }
353
+
354
+ function buildCliMessageSourceProvenance(args: {
355
+ selected: 'native-history' | 'pty-parser';
356
+ provider: string;
357
+ nativeHandle?: string;
358
+ sessionWorkspace?: string;
359
+ intendedWorkspace?: string;
360
+ transcriptWorkspace?: string;
361
+ fallbackReason?: string;
362
+ nativeSource?: string;
363
+ sourcePath?: string;
364
+ sourceMtimeMs?: number;
365
+ nativeHistoryCoverage?: string;
366
+ partialReason?: string;
367
+ unavailableReason?: string;
368
+ nativeMessages?: ChatMessage[];
369
+ ptyMessages?: ChatMessage[];
370
+ returnedMessages?: ChatMessage[];
371
+ safeMapping?: boolean;
372
+ freshEnough?: boolean;
373
+ ptyStatusApprovalOnly?: boolean;
374
+ }): Record<string, unknown> {
375
+ const sourceMtimeMs = Number(args.sourceMtimeMs || 0);
376
+ const sourceMtimeAgeMs = sourceMtimeMs > 0 ? Math.max(0, Date.now() - sourceMtimeMs) : undefined;
377
+ const nativeMessages = args.nativeMessages || [];
378
+ const ptyMessages = args.ptyMessages || [];
379
+ const returnedMessages = args.returnedMessages || [];
380
+ const identityStatus = args.selected === 'native-history'
381
+ ? 'safe'
382
+ : args.fallbackReason === 'native_history_not_safely_mapped'
383
+ ? 'ambiguous_session_identity'
384
+ : args.fallbackReason?.startsWith('native_history_unavailable')
385
+ ? 'transcript_unmapped'
386
+ : undefined;
387
+ return {
388
+ selected: args.selected,
389
+ provider: args.provider,
390
+ providerType: args.provider,
391
+ ...(identityStatus ? { identityStatus } : {}),
392
+ ...(args.nativeHandle ? { nativeHandle: args.nativeHandle } : {}),
393
+ ...(args.nativeHandle ? { nativeSessionId: args.nativeHandle } : {}),
394
+ ...(args.sessionWorkspace ? { sessionWorkspace: args.sessionWorkspace } : {}),
395
+ ...(args.intendedWorkspace ? { intendedWorkspace: args.intendedWorkspace } : {}),
396
+ ...(args.transcriptWorkspace ? { transcriptWorkspace: args.transcriptWorkspace } : {}),
397
+ ...(args.fallbackReason ? { fallbackReason: args.fallbackReason } : {}),
398
+ ...(args.nativeSource ? { nativeSource: args.nativeSource } : {}),
399
+ ...(args.sourcePath ? { sourcePath: args.sourcePath } : {}),
400
+ ...(args.nativeHistoryCoverage ? { nativeHistoryCoverage: args.nativeHistoryCoverage } : {}),
401
+ ...(args.partialReason ? { partialReason: args.partialReason } : {}),
402
+ ...(args.unavailableReason ? { unavailableReason: args.unavailableReason } : {}),
403
+ ptyStatusApprovalOnly: args.ptyStatusApprovalOnly === true,
404
+ staleness: {
405
+ sourceMtimeMs: sourceMtimeMs || undefined,
406
+ sourceMtimeAgeMs,
407
+ nativeNewestMessageAt: getMessageNewestReceivedAt(nativeMessages),
408
+ ptyNewestMessageAt: getMessageNewestReceivedAt(ptyMessages),
409
+ freshEnough: args.freshEnough === true,
410
+ },
411
+ coverage: {
412
+ nativeMessageCount: nativeMessages.length,
413
+ ptyMessageCount: ptyMessages.length,
414
+ returnedMessageCount: returnedMessages.length,
415
+ safeMapping: args.safeMapping === true,
416
+ // true when PTY message bodies are suppressed and must not be treated as
417
+ // chat content. PTY may still contribute status/approval/screen evidence.
418
+ ptyMessagesSuppressed: args.selected === 'native-history' || args.ptyStatusApprovalOnly === true,
419
+ },
420
+ };
421
+ }
422
+
423
+ function buildNativeHistoryFallbackReason(args: {
424
+ providerType: string;
425
+ provider?: ProviderModule;
426
+ nativeSource?: string;
427
+ nativeHistoryCoverage?: string;
428
+ unavailableReason?: string;
429
+ nativeMessageCount: number;
430
+ safeMapping: boolean;
431
+ freshEnough: boolean;
432
+ }): string {
433
+ if (!supportsCliNativeTranscript(args.providerType, args.provider)) return 'provider_native_transcript_not_supported';
434
+ if (args.unavailableReason) return `native_history_unavailable:${args.unavailableReason}`;
435
+ if (args.nativeSource === 'native-unavailable') return 'native_history_unavailable';
436
+ if (args.nativeHistoryCoverage === 'partial') return 'native_history_partial';
437
+ if (args.nativeHistoryCoverage === 'unavailable') return 'native_history_unavailable';
438
+ if (args.nativeSource && args.nativeSource !== 'provider-native') return `native_history_source_${args.nativeSource}`;
439
+ if (args.nativeMessageCount <= 0) return 'native_history_empty';
440
+ if (!args.safeMapping) return 'native_history_not_safely_mapped';
441
+ if (!args.freshEnough) return 'native_history_stale';
442
+ return 'native_history_not_selected';
443
+ }
444
+
445
+ function isUnsafeNativeTranscriptFallback(reason?: string): boolean {
446
+ const value = String(reason || '').trim();
447
+ return value.startsWith('native_history_unavailable')
448
+ || value === 'native_history_not_safely_mapped'
449
+ || value === 'native_history_stale'
450
+ || value === 'native_history_partial';
451
+ }
452
+
453
+ function coerceUnsafeNativeFallbackStatus(status: string, activeModal: unknown): string {
454
+ if (status === 'waiting_approval' && activeModal) return status;
455
+ return 'idle';
456
+ }
457
+
458
+ function isRuntimeInputAckMessage(message: ChatMessage | undefined): boolean {
459
+ if (!message || typeof message !== 'object') return false;
460
+ const role = String((message as any).role || '').trim().toLowerCase();
461
+ if (role !== 'user' && role !== 'human') return false;
462
+ const meta = (message as any).meta;
463
+ return !!meta && typeof meta === 'object' && !Array.isArray(meta) && meta.runtimeInputAck === true;
464
+ }
465
+
466
+ function selectRuntimeInputAckMessages(messages: ChatMessage[]): ChatMessage[] {
467
+ return messages.filter((message) => isRuntimeInputAckMessage(message));
468
+ }
469
+
470
+ function readExactRuntimeMirrorMessages(args: {
471
+ providerType: string;
472
+ targetSessionId?: string;
473
+ currentSessionId?: string;
474
+ tailLimit: number;
475
+ historyBehavior?: ProviderModule['historyBehavior'];
476
+ }): ChatMessage[] {
477
+ const targetSessionId = String(args.targetSessionId || '').trim();
478
+ const currentSessionId = String(args.currentSessionId || '').trim();
479
+ if (!targetSessionId || targetSessionId !== currentSessionId) return [];
480
+
481
+ const history = readChatHistory(
482
+ args.providerType,
483
+ 0,
484
+ Math.max(args.tailLimit || 0, 200),
485
+ targetSessionId,
486
+ 0,
487
+ args.historyBehavior,
488
+ );
489
+ return normalizeChatMessages((history.messages || []) as ChatMessage[])
490
+ .filter((message) => {
491
+ const historySessionId = String((message as any).historySessionId || '').trim();
492
+ const instanceId = String((message as any).instanceId || '').trim();
493
+ return historySessionId === targetSessionId || instanceId === targetSessionId;
494
+ });
495
+ }
496
+
497
+ function normalizeComparableWorkspace(value: unknown): string {
498
+ const text = typeof value === 'string' ? value.trim() : '';
499
+ if (!text) return '';
500
+ return path.resolve(text);
501
+ }
502
+
503
+ function isCurrentRuntimePtySafelyAttributed(args: {
504
+ adapter: CliAdapter;
505
+ helpers: CommandHelpers;
506
+ readChatArgs: any;
507
+ sessionWorkspace?: string;
508
+ intendedWorkspace?: string;
509
+ ptyMessages: ChatMessage[];
510
+ }): boolean {
511
+ if (args.adapter.cliType !== 'codex-cli') return false;
512
+ if (!Array.isArray(args.ptyMessages) || args.ptyMessages.length === 0) return false;
513
+ const targetSessionId = typeof args.readChatArgs?.targetSessionId === 'string'
514
+ ? args.readChatArgs.targetSessionId.trim()
515
+ : '';
516
+ const currentSession = args.helpers.currentSession as any;
517
+ const currentSessionId = typeof currentSession?.sessionId === 'string'
518
+ ? currentSession.sessionId.trim()
519
+ : '';
520
+ if (!targetSessionId || !currentSessionId || targetSessionId !== currentSessionId) return false;
521
+
522
+ const runtimeMeta = typeof (args.adapter as any).getRuntimeMetadata === 'function'
523
+ ? (args.adapter as any).getRuntimeMetadata()
524
+ : null;
525
+ const runtimeId = typeof runtimeMeta?.runtimeId === 'string' ? runtimeMeta.runtimeId.trim() : '';
526
+ if (!runtimeId || runtimeId !== targetSessionId) return false;
527
+ const surfaceKind = typeof runtimeMeta?.surfaceKind === 'string' ? runtimeMeta.surfaceKind : '';
528
+ if (surfaceKind === 'inactive_record' || surfaceKind === 'recovery_snapshot') return false;
529
+
530
+ const sessionWorkspace = normalizeComparableWorkspace(args.sessionWorkspace);
531
+ const adapterWorkspace = normalizeComparableWorkspace(args.adapter.workingDir);
532
+ if (!sessionWorkspace || !adapterWorkspace || sessionWorkspace !== adapterWorkspace) return false;
533
+ const intendedWorkspace = normalizeComparableWorkspace(args.intendedWorkspace);
534
+ if (intendedWorkspace && intendedWorkspace !== sessionWorkspace) return false;
535
+
536
+ const registryEntry = args.helpers.ctx?.sessionRegistry?.get?.(targetSessionId) as any;
537
+ const registryInstanceKey = typeof registryEntry?.adapterKey === 'string' && registryEntry.adapterKey.trim()
538
+ ? registryEntry.adapterKey.trim()
539
+ : typeof registryEntry?.instanceKey === 'string' && registryEntry.instanceKey.trim()
540
+ ? registryEntry.instanceKey.trim()
541
+ : '';
542
+ if (registryInstanceKey) {
543
+ const targetInstance = args.helpers.ctx?.instanceManager?.getInstance?.(registryInstanceKey);
544
+ if (targetInstance) {
545
+ const instanceType = typeof (targetInstance as any).type === 'string' ? (targetInstance as any).type : '';
546
+ if (instanceType && instanceType !== args.adapter.cliType) return false;
547
+ }
548
+ }
549
+
550
+ return true;
551
+ }
552
+
553
+ function supportsCliNativeTranscript(providerType: string, provider?: ProviderModule): boolean {
554
+ if (CLI_NATIVE_TRANSCRIPT_PROVIDERS.has(providerType)) return true;
555
+ return provider?.category === 'cli' && isNativeSourceCanonicalHistory(provider?.canonicalHistory);
556
+ }
557
+
558
+ function getComparableVisibleText(message: ChatMessage | undefined): string {
559
+ if (!message) return '';
560
+ const role = String((message as any).role || '').trim().toLowerCase();
561
+ if (role !== 'user' && role !== 'assistant') return '';
562
+ const kind = String((message as any).kind || 'standard').trim().toLowerCase();
563
+ if (kind && kind !== 'standard') return '';
564
+ const content = flattenContent((message as any).content).replace(/\s+/g, ' ').trim();
565
+ return content;
566
+ }
567
+
568
+ function hasOverlappingVisibleConversationText(nativeMessages: ChatMessage[], ptyMessages: ChatMessage[]): boolean {
569
+ const nativeTexts = nativeMessages.map(getComparableVisibleText).filter(Boolean);
570
+ const ptyTexts = ptyMessages.map(getComparableVisibleText).filter(Boolean);
571
+ if (nativeTexts.length === 0 || ptyTexts.length === 0) return false;
572
+ for (const nativeText of nativeTexts) {
573
+ for (const ptyText of ptyTexts) {
574
+ if (nativeText === ptyText) return true;
575
+ const shorter = nativeText.length <= ptyText.length ? nativeText : ptyText;
576
+ const longer = nativeText.length <= ptyText.length ? ptyText : nativeText;
577
+ if (shorter.length >= 32 && longer.includes(shorter)) return true;
578
+ }
579
+ }
580
+ return false;
581
+ }
582
+
583
+ function hasSafeNativeHistoryMapping(args: {
584
+ historySessionId?: string;
585
+ providerSessionId?: string;
586
+ workspace?: string;
587
+ nativeMessages: ChatMessage[];
588
+ ptyMessages?: ChatMessage[];
589
+ requireWorkspaceContentOverlap?: boolean;
590
+ }): boolean {
591
+ const isCoordinatorTranscript = args.nativeMessages.some((m: any) => {
592
+ const text = typeof m?.content === 'string' ? m.content : JSON.stringify(m?.content || '');
593
+ return text.includes('mesh_send_task') || text.includes('mesh_status') || text.includes('mesh_read_chat') || text.includes('mesh_launch_session');
594
+ });
595
+
596
+ const explicitSessionId = String(args.historySessionId || args.providerSessionId || '').trim();
597
+ if (explicitSessionId) {
598
+ const messageSessionIds = args.nativeMessages
599
+ .map((message: any) => typeof message?.historySessionId === 'string' ? message.historySessionId.trim() : '')
600
+ .filter(Boolean);
601
+ if (messageSessionIds.length > 0) {
602
+ return messageSessionIds.some((id) => id === explicitSessionId);
603
+ }
604
+
605
+ // Messages carry no historySessionId — cannot confirm they belong to the requested session.
606
+ // Only allow a coordinator transcript that is also confirmed by the PTY side; otherwise
607
+ // fail closed so a same-workspace session's history is never silently accepted.
608
+ if (isCoordinatorTranscript && args.ptyMessages && args.ptyMessages.length > 0) {
609
+ const ptyHasCoordinator = args.ptyMessages.some((m: any) => {
610
+ const text = typeof m?.content === 'string' ? m.content : JSON.stringify(m?.content || '');
611
+ return text.includes('mesh_send_task') || text.includes('mesh_status') || text.includes('mesh_read_chat');
612
+ });
613
+ return ptyHasCoordinator;
614
+ }
615
+
616
+ // No historySessionId in messages and no coordinator cross-check: fail closed.
617
+ // Workspace-only matching must not override an explicit session identity.
618
+ return false;
619
+ }
620
+ const workspace = String(args.workspace || '').trim();
621
+ if (!workspace) return false;
622
+ const workspaceMatches = args.nativeMessages.some((message: any) => String(message?.workspace || '').trim() === workspace);
623
+ if (!workspaceMatches) return false;
624
+
625
+ if (isCoordinatorTranscript && args.ptyMessages && args.ptyMessages.length > 0) {
626
+ const ptyHasCoordinator = args.ptyMessages.some((m: any) => {
627
+ const text = typeof m?.content === 'string' ? m.content : JSON.stringify(m?.content || '');
628
+ return text.includes('mesh_send_task') || text.includes('mesh_status') || text.includes('mesh_read_chat');
629
+ });
630
+ if (!ptyHasCoordinator) {
631
+ return false;
632
+ }
633
+ }
634
+
635
+ if (!args.requireWorkspaceContentOverlap) return true;
636
+ return hasOverlappingVisibleConversationText(args.nativeMessages, args.ptyMessages || []);
637
+ }
638
+
639
+ // Provenance boundary: workspace-only native history lookup is never safe
640
+ // because multiple concurrent sessions sharing the same cwd would alias each
641
+ // other. historySessionId (the provider-native session key) is required to
642
+ // establish ownership. hasSafeNativeHistoryMapping() enforces the same
643
+ // invariant after the read; both guards must hold for native history to be used.
644
+ function readCliProviderNativeHistory(agentStr: string, args: {
645
+ canonicalHistory?: ProviderModule['canonicalHistory'];
646
+ historySessionId?: string;
647
+ workspace?: string;
648
+ offset: number;
649
+ limit: number;
650
+ excludeRecentCount: number;
651
+ historyBehavior?: ProviderModule['historyBehavior'];
652
+ scripts?: ProviderScripts;
653
+ }): ReturnType<typeof readProviderChatHistory> & { lookup: 'session' | 'workspace' } {
654
+ if (!args.historySessionId) {
655
+ return {
656
+ messages: [],
657
+ hasMore: false,
658
+ source: 'native-unavailable',
659
+ unavailableReason: 'native_history_workspace_only_lookup_unsafe',
660
+ lookup: 'session',
661
+ } as ReturnType<typeof readProviderChatHistory> & { lookup: 'session' | 'workspace' };
662
+ }
663
+ const sessionHistory = readProviderChatHistory(agentStr, {
664
+ canonicalHistory: args.canonicalHistory,
665
+ historySessionId: args.historySessionId,
666
+ workspace: args.workspace,
667
+ offset: args.offset,
668
+ limit: args.limit,
669
+ excludeRecentCount: args.excludeRecentCount,
670
+ historyBehavior: args.historyBehavior,
671
+ scripts: args.scripts as any,
672
+ });
673
+ // Native transcripts are keyed by provider/runtime session identity. Falling
674
+ // back to workspace makes concurrent local Codex/Hermes sessions alias each
675
+ // other when they share the same cwd.
676
+ return { ...(sessionHistory as any), lookup: 'session' };
677
+ }
678
+
679
+ function isNativeHistoryFreshEnough(args: {
680
+ sourceMtimeMs?: number;
681
+ nativeMessages: ChatMessage[];
682
+ ptyMessages: ChatMessage[];
683
+ }): boolean {
684
+ const nativeNewest = getMessageNewestReceivedAt(args.nativeMessages);
685
+ const ptyNewest = getMessageNewestReceivedAt(args.ptyMessages);
686
+ if (nativeNewest > 0 && nativeNewest >= ptyNewest) return true;
687
+ const sourceMtimeMs = Number(args.sourceMtimeMs || 0);
688
+ if (sourceMtimeMs > 0 && Date.now() - sourceMtimeMs <= CLI_NATIVE_HISTORY_FRESH_MS) return true;
689
+ return ptyNewest === 0 && nativeNewest > 0;
690
+ }
691
+
692
+ function shouldPreserveReadChatPayloadField(key: string): boolean {
693
+ return key === 'messageSource' || key === 'transcriptProvenance';
694
+ }
695
+
696
+ function updateMessageSourceReturnedCount(value: unknown, returnedMessageCount: number): unknown {
697
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return value;
698
+ const record = value as Record<string, unknown>;
699
+ const coverage = record.coverage && typeof record.coverage === 'object' && !Array.isArray(record.coverage)
700
+ ? record.coverage as Record<string, unknown>
701
+ : undefined;
702
+ if (!coverage) return value;
703
+ return {
704
+ ...record,
705
+ coverage: {
706
+ ...coverage,
707
+ returnedMessageCount,
708
+ },
709
+ };
710
+ }
224
711
 
225
712
  function deriveHistoryDedupKey(message: ChatMessage & { _unitKey?: string; _turnKey?: string }): string | undefined {
226
713
  const unitKey = typeof message._unitKey === 'string' ? message._unitKey.trim() : '';
@@ -281,7 +768,7 @@ function normalizeReadChatCommandStatus(status: unknown, activeModal: unknown):
281
768
  }
282
769
  switch (raw) {
283
770
  case 'starting':
284
- return hasNonEmptyModalButtons(activeModal) ? 'waiting_approval' : 'generating';
771
+ return hasNonEmptyModalButtons(activeModal) ? 'waiting_approval' : 'starting';
285
772
  case 'stopped':
286
773
  case 'disconnected':
287
774
  case 'not_monitored':
@@ -295,6 +782,16 @@ function isGeneratingLikeStatus(status: unknown): boolean {
295
782
  return status === 'generating' || status === 'streaming' || status === 'long_generating' || status === 'starting';
296
783
  }
297
784
 
785
+ function hasVisibleAssistantMessage(messages: unknown[] | undefined): boolean {
786
+ if (!Array.isArray(messages)) return false;
787
+ return messages.some((message: any) => {
788
+ if (!message || message.role !== 'assistant') return false;
789
+ const kind = typeof message.kind === 'string' ? message.kind : 'standard';
790
+ if (kind !== 'standard') return false;
791
+ return String(message.content || '').trim().length > 0;
792
+ });
793
+ }
794
+
298
795
  function shouldTrustCliAdapterTerminalStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any): boolean {
299
796
  if (!isGeneratingLikeStatus(parsedStatus)) return false;
300
797
  if (hasNonEmptyModalButtons(activeModal)) return false;
@@ -304,7 +801,26 @@ function shouldTrustCliAdapterTerminalStatus(parsedStatus: unknown, activeModal:
304
801
  return true;
305
802
  }
306
803
 
307
- function normalizeCliReadChatStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any): string {
804
+ function normalizeCliReadChatStatus(parsedStatus: unknown, activeModal: unknown, adapter: CliAdapter, adapterStatus: any, parsedMessages?: unknown[]): string {
805
+ const adapterRawStatus = typeof adapterStatus?.status === 'string' ? adapterStatus.status.trim() : '';
806
+ if (adapterRawStatus === 'starting'
807
+ && isGeneratingLikeStatus(parsedStatus)
808
+ && !hasNonEmptyModalButtons(activeModal)
809
+ && Array.isArray(parsedMessages)
810
+ && parsedMessages.length === 0
811
+ && Array.isArray(adapterStatus?.messages)
812
+ && adapterStatus.messages.length === 0
813
+ && !(typeof adapter.isProcessing === 'function' && adapter.isProcessing())) {
814
+ return 'starting';
815
+ }
816
+ if (
817
+ isGeneratingLikeStatus(adapterRawStatus)
818
+ && parsedStatus === 'idle'
819
+ && !hasNonEmptyModalButtons(activeModal)
820
+ && !hasVisibleAssistantMessage(parsedMessages)
821
+ ) {
822
+ return adapterRawStatus;
823
+ }
308
824
  if (shouldTrustCliAdapterTerminalStatus(parsedStatus, activeModal, adapter, adapterStatus)) return 'idle';
309
825
  return typeof parsedStatus === 'string' && parsedStatus.trim() ? parsedStatus : 'idle';
310
826
  }
@@ -342,6 +858,13 @@ function buildReadChatCommandResult(payload: Record<string, any>, args: any): Co
342
858
  const visibleMessages = filterUserFacingChatMessages(messages);
343
859
  const sync = buildFullTail(visibleMessages, normalizeReadChatTailLimit(args));
344
860
  const hiddenMsgCount = Math.max(0, messages.length - visibleMessages.length);
861
+ const preservedPayloadFields = Object.fromEntries(Object.entries(payload).filter(([key]) => shouldPreserveReadChatPayloadField(key)));
862
+ if (preservedPayloadFields.messageSource) {
863
+ preservedPayloadFields.messageSource = updateMessageSourceReturnedCount(preservedPayloadFields.messageSource, sync.messages.length);
864
+ }
865
+ if (preservedPayloadFields.transcriptProvenance) {
866
+ preservedPayloadFields.transcriptProvenance = updateMessageSourceReturnedCount(preservedPayloadFields.transcriptProvenance, sync.messages.length);
867
+ }
345
868
  const returnedDebugReadChat = debugReadChat
346
869
  ? {
347
870
  ...debugReadChat,
@@ -356,6 +879,7 @@ function buildReadChatCommandResult(payload: Record<string, any>, args: any): Co
356
879
  return {
357
880
  success: true,
358
881
  ...validatedPayload,
882
+ ...preservedPayloadFields,
359
883
  messages: sync.messages,
360
884
  totalMessages: sync.totalMessages,
361
885
  ...(returnedDebugReadChat ? { debugReadChat: returnedDebugReadChat } : {}),
@@ -584,6 +1108,8 @@ function buildChatDebugBundleSummary(bundle: Record<string, unknown>): Record<st
584
1108
  adapterStatus: debugReadChat.adapterStatus,
585
1109
  parsedStatus: debugReadChat.parsedStatus,
586
1110
  returnedStatus: debugReadChat.returnedStatus,
1111
+ selectedMessageSource: debugReadChat.selectedMessageSource,
1112
+ messageSource: debugReadChat.messageSource,
587
1113
  parsedMsgCount: debugReadChat.parsedMsgCount,
588
1114
  returnedMsgCount: debugReadChat.returnedMsgCount,
589
1115
  shouldPreferAdapterMessages: debugReadChat.shouldPreferAdapterMessages,
@@ -646,11 +1172,20 @@ export async function handleGetChatDebugBundle(h: CommandHelpers, args: any): Pr
646
1172
  providerSessionId: readResult.providerSessionId,
647
1173
  transcriptAuthority: readResult.transcriptAuthority,
648
1174
  coverage: readResult.coverage,
1175
+ messageSource: readResult.messageSource,
1176
+ transcriptProvenance: readResult.transcriptProvenance,
649
1177
  activeModal: readResult.activeModal,
650
1178
  messagesTail: Array.isArray(readResult.messages) ? readResult.messages.slice(-20) : [],
651
1179
  debugReadChat: readResult.debugReadChat,
652
1180
  }
653
- : { success: false, error: readResult.error };
1181
+ : {
1182
+ success: false,
1183
+ error: readResult.error,
1184
+ code: readResult.code,
1185
+ messageSource: readResult.messageSource,
1186
+ transcriptProvenance: readResult.transcriptProvenance,
1187
+ debugReadChat: readResult.debugReadChat,
1188
+ };
654
1189
  } catch (error: any) {
655
1190
  readChat = { success: false, error: error?.message || String(error) };
656
1191
  }
@@ -817,16 +1352,56 @@ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<C
817
1352
  : typeof (h.currentSession as any)?.workspace === 'string'
818
1353
  ? (h.currentSession as any).workspace
819
1354
  : undefined;
820
- const result = readProviderChatHistory(agentStr, {
821
- canonicalHistory: provider?.canonicalHistory,
822
- historySessionId,
823
- workspace,
824
- offset: offset || 0,
825
- limit: limit || 30,
826
- excludeRecentCount,
827
- historyBehavior: provider?.historyBehavior,
828
- scripts: provider?.scripts as any,
829
- });
1355
+ const exactNativeHistoryScope = Boolean(
1356
+ (typeof args?.targetSessionId === 'string' && args.targetSessionId.trim())
1357
+ || (typeof args?.historySessionId === 'string' && args.historySessionId.trim())
1358
+ || (typeof args?.providerSessionId === 'string' && args.providerSessionId.trim())
1359
+ );
1360
+ const result = supportsCliNativeTranscript(agentStr, provider) && isNativeSourceCanonicalHistory(provider?.canonicalHistory)
1361
+ ? readCliProviderNativeHistory(agentStr, {
1362
+ canonicalHistory: provider?.canonicalHistory,
1363
+ historySessionId,
1364
+ workspace,
1365
+ offset: offset || 0,
1366
+ limit: limit || 30,
1367
+ excludeRecentCount,
1368
+ historyBehavior: provider?.historyBehavior,
1369
+ scripts: provider?.scripts as any,
1370
+ })
1371
+ : readProviderChatHistory(agentStr, {
1372
+ canonicalHistory: provider?.canonicalHistory,
1373
+ historySessionId,
1374
+ workspace,
1375
+ offset: offset || 0,
1376
+ limit: limit || 30,
1377
+ excludeRecentCount,
1378
+ historyBehavior: provider?.historyBehavior,
1379
+ scripts: provider?.scripts as any,
1380
+ });
1381
+ if (supportsCliNativeTranscript(agentStr, provider) && isNativeSourceCanonicalHistory(provider?.canonicalHistory)) {
1382
+ const lookup = (result as any).lookup === 'workspace' ? 'workspace' : 'session';
1383
+ const messages = Array.isArray((result as any).messages)
1384
+ ? normalizeNativeHistoryMessages(agentStr, (result as any).messages as ChatMessage[], (result as any)?.providerSessionId)
1385
+ : [];
1386
+ const historyProviderSessionId = typeof (result as any)?.providerSessionId === 'string'
1387
+ ? (result as any).providerSessionId
1388
+ : readHistorySessionIdFromMessages(messages) || historySessionId;
1389
+ const safeMapping = hasSafeNativeHistoryMapping({
1390
+ historySessionId: lookup === 'workspace' ? undefined : historySessionId,
1391
+ providerSessionId: lookup === 'workspace' ? undefined : historyProviderSessionId,
1392
+ workspace,
1393
+ nativeMessages: messages,
1394
+ });
1395
+ if ((result as any).source === 'provider-native' && messages.length > 0 && !safeMapping) {
1396
+ return {
1397
+ success: true,
1398
+ messages: [],
1399
+ hasMore: false,
1400
+ source: 'native-unavailable',
1401
+ agent: agentStr,
1402
+ };
1403
+ }
1404
+ }
830
1405
  return { success: true, ...result, agent: agentStr };
831
1406
  } catch (e: any) {
832
1407
  return { success: false, error: e.message };
@@ -874,7 +1449,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
874
1449
  ? parsedRecord.coverage
875
1450
  : undefined;
876
1451
  const activeModal = parsedRecord.activeModal ?? parsedRecord.modal ?? null;
877
- const returnedStatus = normalizeCliReadChatStatus(parsedRecord.status, activeModal, adapter, adapterStatus);
1452
+ const returnedStatus = normalizeCliReadChatStatus(parsedRecord.status, activeModal, adapter, adapterStatus, parsedRecord.messages);
878
1453
  const runtimeMessageMerger = getTargetInstance(h, args) as RuntimeChatMessageMerger | null;
879
1454
  const parsedMessages = finalizeStreamingMessagesWhenIdle(parsedRecord.messages as ChatMessage[], returnedStatus);
880
1455
  const returnedMessages = runtimeMessageMerger?.category === 'cli'
@@ -882,36 +1457,287 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
882
1457
  && typeof runtimeMessageMerger.mergeRuntimeChatMessages === 'function'
883
1458
  ? runtimeMessageMerger.mergeRuntimeChatMessages(parsedMessages)
884
1459
  : parsedMessages;
1460
+ const providerType = provider?.type || adapter.cliType;
1461
+ let selectedMessages = returnedMessages;
1462
+ let selectedTitle = title;
1463
+ let selectedProviderSessionId = providerSessionId;
1464
+ let selectedTranscriptAuthority = transcriptAuthority;
1465
+ let selectedCoverage = coverage;
1466
+ let selectedStatus = returnedStatus;
1467
+ const sessionWorkspace = typeof (h.currentSession as any)?.workspace === 'string'
1468
+ ? (h.currentSession as any).workspace
1469
+ : typeof adapter.workingDir === 'string'
1470
+ ? adapter.workingDir
1471
+ : undefined;
1472
+ const intendedWorkspace = typeof args?.workspace === 'string' ? args.workspace : undefined;
1473
+ let messageSource = buildCliMessageSourceProvenance({
1474
+ selected: 'pty-parser',
1475
+ provider: adapter.cliType,
1476
+ fallbackReason: supportsCliNativeTranscript(providerType, provider) ? 'native_history_not_checked' : 'provider_native_transcript_not_supported',
1477
+ sessionWorkspace,
1478
+ intendedWorkspace,
1479
+ ptyMessages: returnedMessages,
1480
+ returnedMessages,
1481
+ ptyStatusApprovalOnly: false,
1482
+ });
1483
+
1484
+ if (supportsCliNativeTranscript(providerType, provider) && isNativeSourceCanonicalHistory(provider?.canonicalHistory)) {
1485
+ const agentStr = provider?.type || args?.agentType || getCurrentProviderType(h, adapter.cliType);
1486
+ const workspace = sessionWorkspace;
1487
+ const nativeHistoryLimit = Math.max(
1488
+ normalizeReadChatTailLimit(args) || 0,
1489
+ returnedMessages.length,
1490
+ 200,
1491
+ );
1492
+ const nativeHistorySessionId = resolveCliNativeHistorySessionId(args, historySessionId, providerSessionId);
1493
+ const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
1494
+ const exactNativeHistoryScope = Boolean(
1495
+ (typeof args?.historySessionId === 'string' && args.historySessionId.trim())
1496
+ || (typeof args?.providerSessionId === 'string' && args.providerSessionId.trim())
1497
+ || providerSessionId
1498
+ || (nativeHistorySessionId && nativeHistorySessionId !== targetSessionId)
1499
+ || ((h.currentSession as any)?.sessionId === args?.targetSessionId && typeof (h.currentSession as any)?.providerSessionId === 'string' && (h.currentSession as any).providerSessionId.trim())
1500
+ );
1501
+ let nativeHistory: (ReturnType<typeof readProviderChatHistory> & { lookup?: 'session' | 'workspace' }) | null = null;
1502
+ try {
1503
+ nativeHistory = readCliProviderNativeHistory(agentStr, {
1504
+ canonicalHistory: provider?.canonicalHistory,
1505
+ historySessionId: nativeHistorySessionId,
1506
+ workspace,
1507
+ offset: 0,
1508
+ limit: nativeHistoryLimit,
1509
+ excludeRecentCount: 0,
1510
+ historyBehavior: provider?.historyBehavior,
1511
+ scripts: provider?.scripts as any,
1512
+ });
1513
+ } catch (error: any) {
1514
+ const fallbackReason = `native_history_error:${error?.message || String(error)}`;
1515
+ messageSource = buildCliMessageSourceProvenance({
1516
+ selected: 'pty-parser',
1517
+ provider: adapter.cliType,
1518
+ fallbackReason,
1519
+ sessionWorkspace,
1520
+ intendedWorkspace,
1521
+ ptyMessages: returnedMessages,
1522
+ returnedMessages,
1523
+ ptyStatusApprovalOnly: false,
1524
+ });
1525
+ nativeHistory = null;
1526
+ }
1527
+
1528
+ if (nativeHistory) {
1529
+ const nativeMessages = Array.isArray((nativeHistory as any).messages)
1530
+ ? normalizeNativeHistoryMessages(agentStr, (nativeHistory as any).messages as ChatMessage[], (nativeHistory as any)?.providerSessionId)
1531
+ : [];
1532
+ const historyProviderSessionId = typeof (nativeHistory as any)?.providerSessionId === 'string'
1533
+ ? (nativeHistory as any).providerSessionId
1534
+ : readHistorySessionIdFromMessages(nativeMessages) || nativeHistorySessionId || historySessionId;
1535
+ const nativeHistoryCoverage = typeof (nativeHistory as any)?.nativeHistoryCoverage === 'string'
1536
+ ? (nativeHistory as any).nativeHistoryCoverage
1537
+ : undefined;
1538
+ const partialReason = typeof (nativeHistory as any)?.partialReason === 'string'
1539
+ ? (nativeHistory as any).partialReason
1540
+ : undefined;
1541
+ const unavailableReason = typeof (nativeHistory as any)?.unavailableReason === 'string'
1542
+ ? (nativeHistory as any).unavailableReason
1543
+ : undefined;
1544
+ const lookup = (nativeHistory as any).lookup === 'workspace' ? 'workspace' : 'session';
1545
+ const transcriptWorkspace = typeof (nativeHistory as any)?.workspace === 'string'
1546
+ ? (nativeHistory as any).workspace
1547
+ : nativeMessages.map((message: any) => typeof message?.workspace === 'string' ? message.workspace.trim() : '').find(Boolean);
1548
+ const nativeHistorySessionForMapping = adapter.cliType === 'antigravity-cli'
1549
+ && historyProviderSessionId
1550
+ && nativeHistorySessionId
1551
+ && historyProviderSessionId !== nativeHistorySessionId
1552
+ ? undefined
1553
+ : nativeHistorySessionId;
1554
+ const safeMapping = hasSafeNativeHistoryMapping({
1555
+ historySessionId: lookup === 'workspace' ? undefined : nativeHistorySessionForMapping,
1556
+ providerSessionId: lookup === 'workspace' ? undefined : historyProviderSessionId || providerSessionId,
1557
+ workspace,
1558
+ nativeMessages,
1559
+ ptyMessages: returnedMessages,
1560
+ requireWorkspaceContentOverlap: lookup === 'workspace' && !exactNativeHistoryScope,
1561
+ });
1562
+ const freshEnough = isNativeHistoryFreshEnough({
1563
+ sourceMtimeMs: (nativeHistory as any).sourceMtimeMs,
1564
+ nativeMessages,
1565
+ ptyMessages: returnedMessages,
1566
+ });
1567
+ const nativeUsableForChatMessages = (nativeHistory as any).source === 'provider-native'
1568
+ && nativeMessages.length > 0
1569
+ && nativeHistoryCoverage !== 'partial'
1570
+ && nativeHistoryCoverage !== 'unavailable'
1571
+ && safeMapping;
1572
+ const allowStaleNativeChatMessages = adapter.cliType === 'antigravity-cli' && nativeUsableForChatMessages;
1573
+ if (nativeUsableForChatMessages && (freshEnough || allowStaleNativeChatMessages)) {
1574
+ selectedMessages = finalizeStreamingMessagesWhenIdle(nativeMessages, returnedStatus);
1575
+ selectedProviderSessionId = historyProviderSessionId || providerSessionId;
1576
+ selectedTranscriptAuthority = 'provider';
1577
+ selectedCoverage = (nativeHistory as any).hasMore ? 'tail' : 'full';
1578
+ messageSource = buildCliMessageSourceProvenance({
1579
+ selected: 'native-history',
1580
+ provider: adapter.cliType,
1581
+ nativeHandle: selectedProviderSessionId || nativeHistorySessionId || historySessionId,
1582
+ sessionWorkspace,
1583
+ intendedWorkspace,
1584
+ transcriptWorkspace,
1585
+ nativeSource: (nativeHistory as any).source,
1586
+ sourcePath: (nativeHistory as any).sourcePath,
1587
+ sourceMtimeMs: (nativeHistory as any).sourceMtimeMs,
1588
+ nativeHistoryCoverage,
1589
+ partialReason,
1590
+ unavailableReason,
1591
+ nativeMessages,
1592
+ ptyMessages: returnedMessages,
1593
+ returnedMessages: selectedMessages,
1594
+ safeMapping,
1595
+ freshEnough,
1596
+ ptyStatusApprovalOnly: true,
1597
+ });
1598
+ } else {
1599
+ const fallbackReason = buildNativeHistoryFallbackReason({
1600
+ providerType,
1601
+ provider,
1602
+ nativeSource: (nativeHistory as any).source,
1603
+ nativeHistoryCoverage,
1604
+ unavailableReason,
1605
+ nativeMessageCount: nativeMessages.length,
1606
+ safeMapping,
1607
+ freshEnough,
1608
+ });
1609
+ const unsafeNativeFallback = adapter.cliType === 'codex-cli'
1610
+ && isUnsafeNativeTranscriptFallback(fallbackReason);
1611
+ const safeCurrentRuntimePtyMessages = unsafeNativeFallback
1612
+ && isCurrentRuntimePtySafelyAttributed({
1613
+ adapter,
1614
+ helpers: h,
1615
+ readChatArgs: args,
1616
+ sessionWorkspace,
1617
+ intendedWorkspace,
1618
+ ptyMessages: returnedMessages,
1619
+ });
1620
+ const safeRuntimeAckMessages = unsafeNativeFallback
1621
+ && !safeCurrentRuntimePtyMessages
1622
+ ? selectRuntimeInputAckMessages(returnedMessages)
1623
+ : [];
1624
+ const exactRuntimeMirrorMessages = unsafeNativeFallback
1625
+ && !safeCurrentRuntimePtyMessages
1626
+ && safeRuntimeAckMessages.length === 0
1627
+ ? readExactRuntimeMirrorMessages({
1628
+ providerType,
1629
+ targetSessionId: typeof args?.targetSessionId === 'string' ? args.targetSessionId : undefined,
1630
+ currentSessionId: typeof (h.currentSession as any)?.sessionId === 'string' ? (h.currentSession as any).sessionId : undefined,
1631
+ tailLimit: nativeHistoryLimit,
1632
+ historyBehavior: provider?.historyBehavior,
1633
+ })
1634
+ : [];
1635
+ const safeDaemonMessages = safeRuntimeAckMessages.length > 0
1636
+ ? safeRuntimeAckMessages
1637
+ : exactRuntimeMirrorMessages;
1638
+ if (unsafeNativeFallback) {
1639
+ if (safeCurrentRuntimePtyMessages) {
1640
+ selectedMessages = returnedMessages;
1641
+ selectedTranscriptAuthority = 'daemon';
1642
+ selectedCoverage = coverage || 'current-turn';
1643
+ selectedStatus = returnedStatus;
1644
+ } else {
1645
+ selectedMessages = safeDaemonMessages;
1646
+ selectedTranscriptAuthority = safeDaemonMessages.length > 0 ? 'daemon' : undefined;
1647
+ selectedCoverage = safeDaemonMessages.length > 0 ? 'tail' : undefined;
1648
+ selectedStatus = coerceUnsafeNativeFallbackStatus(returnedStatus, activeModal);
1649
+ }
1650
+ }
1651
+ messageSource = buildCliMessageSourceProvenance({
1652
+ selected: 'pty-parser',
1653
+ provider: adapter.cliType,
1654
+ nativeHandle: historyProviderSessionId || nativeHistorySessionId || historySessionId,
1655
+ sessionWorkspace,
1656
+ intendedWorkspace,
1657
+ transcriptWorkspace,
1658
+ fallbackReason,
1659
+ nativeSource: (nativeHistory as any).source,
1660
+ sourcePath: (nativeHistory as any).sourcePath,
1661
+ sourceMtimeMs: (nativeHistory as any).sourceMtimeMs,
1662
+ nativeHistoryCoverage,
1663
+ partialReason,
1664
+ unavailableReason,
1665
+ nativeMessages,
1666
+ ptyMessages: returnedMessages,
1667
+ returnedMessages: unsafeNativeFallback && !safeCurrentRuntimePtyMessages ? safeDaemonMessages : returnedMessages,
1668
+ safeMapping,
1669
+ freshEnough,
1670
+ ptyStatusApprovalOnly: unsafeNativeFallback && !safeCurrentRuntimePtyMessages,
1671
+ });
1672
+ if (safeCurrentRuntimePtyMessages) {
1673
+ (messageSource as any).selectedDaemonSource = 'current-runtime-pty';
1674
+ (messageSource as any).transcriptAuthority = 'daemon';
1675
+ (messageSource as any).runtimeMappingSafe = true;
1676
+ }
1677
+ if (unsafeNativeFallback && exactRuntimeMirrorMessages.length > 0) {
1678
+ (messageSource as any).selectedDaemonSource = 'exact-runtime-mirror';
1679
+ (messageSource as any).transcriptAuthority = 'daemon';
1680
+ }
1681
+ }
1682
+ }
1683
+ }
885
1684
  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}`);
886
1685
  return buildReadChatCommandResult({
887
- messages: returnedMessages,
888
- status: returnedStatus,
1686
+ messages: selectedMessages,
1687
+ status: selectedStatus,
889
1688
  activeModal,
1689
+ messageSource,
1690
+ transcriptProvenance: messageSource,
890
1691
  debugReadChat: {
891
1692
  provider: adapter.cliType,
892
1693
  targetSessionId: String(args?.targetSessionId || ''),
893
1694
  adapterStatus: String(adapterStatus.status || ''),
894
1695
  parsedStatus: String(parsedRecord.status || ''),
895
- returnedStatus: String(returnedStatus || ''),
896
- shouldPreferAdapterMessages: false,
1696
+ returnedStatus: String(selectedStatus || ''),
1697
+ selectedMessageSource: (messageSource as any).selected,
1698
+ messageSource,
1699
+ shouldPreferAdapterMessages: supportsCliNativeTranscript(providerType, provider)
1700
+ && isNativeSourceCanonicalHistory(provider?.canonicalHistory)
1701
+ && (messageSource as any).selected !== 'native-history'
1702
+ && typeof (messageSource as any).fallbackReason === 'string'
1703
+ && (messageSource as any).fallbackReason.startsWith('native_history_')
1704
+ && (messageSource as any).fallbackReason !== 'native_history_not_checked'
1705
+ && !isUnsafeNativeTranscriptFallback((messageSource as any).fallbackReason)
1706
+ && !(selectedTranscriptAuthority === 'provider' && selectedCoverage === 'full'),
897
1707
  parsedMsgCount: parsedRecord.messages.length,
898
- returnedMsgCount: returnedMessages.length,
1708
+ returnedMsgCount: selectedMessages.length,
899
1709
  },
900
- ...(title ? { title } : {}),
901
- ...(providerSessionId ? { providerSessionId } : {}),
902
- ...(transcriptAuthority ? { transcriptAuthority } : {}),
903
- ...(coverage ? { coverage } : {}),
1710
+ ...(selectedTitle ? { title: selectedTitle } : {}),
1711
+ ...(selectedProviderSessionId ? { providerSessionId: selectedProviderSessionId } : {}),
1712
+ ...(selectedTranscriptAuthority ? { transcriptAuthority: selectedTranscriptAuthority } : {}),
1713
+ ...(selectedCoverage ? { coverage: selectedCoverage } : {}),
904
1714
  }, args);
905
1715
  }
906
1716
  const historyLimit = normalizeReadChatTailLimit(args);
907
1717
  try {
908
1718
  const agentStr = provider?.type || args?.agentType || getCurrentProviderType(h);
909
- const workspace = typeof args?.workspace === 'string'
910
- ? args.workspace
911
- : typeof (h.currentSession as any)?.workspace === 'string'
912
- ? (h.currentSession as any).workspace
913
- : undefined;
914
- const history = readProviderChatHistory(agentStr, {
1719
+ const workspace = typeof (h.currentSession as any)?.workspace === 'string'
1720
+ ? (h.currentSession as any).workspace
1721
+ : undefined;
1722
+ const intendedWorkspace = typeof args?.workspace === 'string' ? args.workspace : undefined;
1723
+ const exactNativeHistoryScope = Boolean(
1724
+ (typeof args?.targetSessionId === 'string' && args.targetSessionId.trim())
1725
+ || (typeof args?.historySessionId === 'string' && args.historySessionId.trim())
1726
+ || (typeof args?.providerSessionId === 'string' && args.providerSessionId.trim())
1727
+ || ((h.currentSession as any)?.sessionId === args?.targetSessionId && typeof (h.currentSession as any)?.providerSessionId === 'string' && (h.currentSession as any).providerSessionId.trim())
1728
+ );
1729
+ const history = supportsCliNativeTranscript(agentStr, provider) && isNativeSourceCanonicalHistory(provider?.canonicalHistory)
1730
+ ? readCliProviderNativeHistory(agentStr, {
1731
+ canonicalHistory: provider?.canonicalHistory,
1732
+ historySessionId,
1733
+ workspace,
1734
+ offset: 0,
1735
+ limit: historyLimit,
1736
+ excludeRecentCount: 0,
1737
+ historyBehavior: provider?.historyBehavior,
1738
+ scripts: provider?.scripts as any,
1739
+ })
1740
+ : readProviderChatHistory(agentStr, {
915
1741
  canonicalHistory: provider?.canonicalHistory,
916
1742
  historySessionId,
917
1743
  workspace,
@@ -921,12 +1747,87 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
921
1747
  historyBehavior: provider?.historyBehavior,
922
1748
  scripts: provider?.scripts as any,
923
1749
  });
1750
+ const lookup = (history as any).lookup === 'workspace' ? 'workspace' : 'session';
1751
+ const historyMessages = Array.isArray((history as any)?.messages)
1752
+ ? normalizeNativeHistoryMessages(agentStr, (history as any).messages as ChatMessage[], (history as any)?.providerSessionId)
1753
+ : [];
924
1754
  const historyProviderSessionId = typeof (history as any)?.providerSessionId === 'string'
925
1755
  ? (history as any).providerSessionId
926
- : historySessionId;
1756
+ : readHistorySessionIdFromMessages(historyMessages) || historySessionId;
1757
+ const nativeHistoryCoverage = typeof (history as any)?.nativeHistoryCoverage === 'string'
1758
+ ? (history as any).nativeHistoryCoverage
1759
+ : undefined;
1760
+ const partialReason = typeof (history as any)?.partialReason === 'string'
1761
+ ? (history as any).partialReason
1762
+ : undefined;
1763
+ const unavailableReason = typeof (history as any)?.unavailableReason === 'string'
1764
+ ? (history as any).unavailableReason
1765
+ : undefined;
1766
+ const transcriptWorkspace = typeof (history as any)?.workspace === 'string'
1767
+ ? (history as any).workspace
1768
+ : historyMessages.map((message: any) => typeof message?.workspace === 'string' ? message.workspace.trim() : '').find(Boolean);
1769
+ const safeMapping = supportsCliNativeTranscript(agentStr, provider)
1770
+ ? hasSafeNativeHistoryMapping({
1771
+ historySessionId: lookup === 'workspace' ? undefined : historySessionId,
1772
+ providerSessionId: lookup === 'workspace' ? undefined : historyProviderSessionId,
1773
+ workspace,
1774
+ nativeMessages: historyMessages,
1775
+ })
1776
+ : false;
1777
+ const nativeSelected = supportsCliNativeTranscript(agentStr, provider)
1778
+ && (history as any).source === 'provider-native'
1779
+ && historyMessages.length > 0
1780
+ && nativeHistoryCoverage !== 'partial'
1781
+ && nativeHistoryCoverage !== 'unavailable'
1782
+ && safeMapping;
1783
+ const messageSource = buildCliMessageSourceProvenance({
1784
+ selected: nativeSelected ? 'native-history' : 'pty-parser',
1785
+ provider: agentStr,
1786
+ nativeHandle: historyProviderSessionId || historySessionId,
1787
+ sessionWorkspace: workspace,
1788
+ intendedWorkspace,
1789
+ transcriptWorkspace,
1790
+ fallbackReason: nativeSelected
1791
+ ? undefined
1792
+ : buildNativeHistoryFallbackReason({
1793
+ providerType: agentStr,
1794
+ provider,
1795
+ nativeSource: (history as any).source,
1796
+ nativeHistoryCoverage,
1797
+ unavailableReason,
1798
+ nativeMessageCount: historyMessages.length,
1799
+ safeMapping,
1800
+ freshEnough: true,
1801
+ }),
1802
+ nativeSource: (history as any).source,
1803
+ sourcePath: (history as any).sourcePath,
1804
+ sourceMtimeMs: (history as any).sourceMtimeMs,
1805
+ nativeHistoryCoverage,
1806
+ partialReason,
1807
+ unavailableReason,
1808
+ nativeMessages: historyMessages,
1809
+ returnedMessages: historyMessages,
1810
+ safeMapping,
1811
+ freshEnough: true,
1812
+ ptyStatusApprovalOnly: false,
1813
+ });
1814
+ const requiresNativeSource = supportsCliNativeTranscript(agentStr, provider)
1815
+ && isNativeSourceCanonicalHistory(provider?.canonicalHistory);
1816
+ if (requiresNativeSource && !nativeSelected) {
1817
+ return {
1818
+ success: false,
1819
+ code: 'native_history_not_safely_available',
1820
+ error: 'Provider-native history was not safely available for the requested CLI session.',
1821
+ providerSessionId: historyProviderSessionId,
1822
+ messageSource,
1823
+ transcriptProvenance: messageSource,
1824
+ };
1825
+ }
927
1826
  return buildReadChatCommandResult({
928
- messages: Array.isArray((history as any)?.messages) ? (history as any).messages : [],
1827
+ messages: historyMessages,
929
1828
  status: 'idle',
1829
+ messageSource,
1830
+ transcriptProvenance: messageSource,
930
1831
  ...(typeof (history as any)?.title === 'string' ? { title: (history as any).title } : {}),
931
1832
  ...(historyProviderSessionId ? { providerSessionId: historyProviderSessionId } : {}),
932
1833
  ...(((provider?.historyBehavior as any)?.transcriptAuthority === 'provider' || (provider?.historyBehavior as any)?.transcriptAuthority === 'daemon')
@@ -1184,8 +2085,24 @@ export async function handleSendChat(h: CommandHelpers, args: any): Promise<Comm
1184
2085
  assertTextOnlyInput(provider, input);
1185
2086
  if (!text) return { success: false, error: 'text required for PTY send' };
1186
2087
  await waitOnceForFreshHermesCliStart(adapter, _log);
1187
- await adapter.sendMessage(text);
1188
- return _logSendSuccess(`${transport}-adapter`, adapter.cliType);
2088
+ const forceSend = args?.force === true || args?.forceSend === true;
2089
+ if (forceSend && typeof adapter.forceSendMessage === 'function') {
2090
+ await adapter.forceSendMessage(text);
2091
+ } else if (forceSend) {
2092
+ await adapter.sendMessage(text, { force: true });
2093
+ } else {
2094
+ await adapter.sendMessage(text);
2095
+ }
2096
+ const target = getTargetInstance(h, args) as RuntimeChatMessageMerger | null;
2097
+ if (target?.category === 'cli'
2098
+ && target.type === adapter.cliType
2099
+ && typeof target.recordAcknowledgedUserInput === 'function') {
2100
+ target.recordAcknowledgedUserInput(input);
2101
+ }
2102
+ return {
2103
+ ..._logSendSuccess(`${transport}-adapter`, adapter.cliType),
2104
+ ...(forceSend ? { forceSent: true } : {}),
2105
+ };
1189
2106
  } catch (e: any) {
1190
2107
  return { success: false, error: `${transport} send failed: ${e.message}` };
1191
2108
  }
@@ -1742,6 +2659,9 @@ export async function handleResolveAction(h: CommandHelpers, args: any): Promise
1742
2659
  if (buttonIndex < 0 && (action === 'always' || /always/i.test(button))) {
1743
2660
  buttonIndex = buttons.findIndex(b => /always/i.test(b));
1744
2661
  }
2662
+ if (buttonIndex < 0 && (action === 'approve' || action === 'accept')) {
2663
+ buttonIndex = pickApprovalButton(buttons, provider).index;
2664
+ }
1745
2665
  if (buttonIndex < 0) {
1746
2666
  return { success: false, error: 'Approval action did not match any visible button' };
1747
2667
  }