@adhdev/daemon-core 0.9.82-rc.136 → 0.9.82-rc.138

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 (63) hide show
  1. package/dist/chat/source-machine.d.ts +166 -0
  2. package/dist/chat/source-resolver.d.ts +104 -0
  3. package/dist/cli-adapters/cli-script-runner.d.ts +45 -0
  4. package/dist/cli-adapters/cli-state-engine.d.ts +169 -0
  5. package/dist/cli-adapters/provider-cli-adapter.d.ts +72 -74
  6. package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
  7. package/dist/cli-adapters/provider-cli-shared.d.ts +5 -0
  8. package/dist/config/chat-history.d.ts +1 -0
  9. package/dist/index.d.ts +3 -3
  10. package/dist/index.js +3507 -2288
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +3515 -2301
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/mesh/beads-db.d.ts +54 -0
  15. package/dist/mesh/contracts.d.ts +164 -0
  16. package/dist/mesh/mesh-active-work.d.ts +7 -1
  17. package/dist/mesh/mesh-events.d.ts +10 -4
  18. package/dist/mesh/mesh-ledger.d.ts +21 -1
  19. package/dist/mesh/mesh-refine-status.d.ts +2 -3
  20. package/dist/mesh/mesh-work-queue.d.ts +17 -0
  21. package/dist/mesh/worktree-bootstrap-config.d.ts +2 -4
  22. package/dist/providers/contracts.d.ts +19 -0
  23. package/dist/providers/read-chat-contract.d.ts +29 -0
  24. package/dist/providers/transcript-v2.d.ts +176 -0
  25. package/dist/repo-mesh-types.d.ts +5 -0
  26. package/dist/shared-types.d.ts +7 -0
  27. package/dist/status/snapshot.d.ts +1 -0
  28. package/dist/types.d.ts +5 -0
  29. package/package.json +1 -1
  30. package/src/chat/source-machine.ts +534 -0
  31. package/src/chat/source-resolver.ts +0 -0
  32. package/src/chat/subscription-updates.ts +9 -0
  33. package/src/cli-adapters/cli-script-runner.ts +145 -0
  34. package/src/cli-adapters/cli-state-engine.ts +1054 -0
  35. package/src/cli-adapters/provider-cli-adapter.d.ts +0 -1
  36. package/src/cli-adapters/provider-cli-adapter.ts +413 -1399
  37. package/src/cli-adapters/provider-cli-parse.ts +3 -0
  38. package/src/cli-adapters/provider-cli-shared.ts +17 -1
  39. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
  40. package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
  41. package/src/commands/chat-commands.ts +715 -368
  42. package/src/commands/router.ts +22 -2
  43. package/src/config/chat-history.ts +43 -16
  44. package/src/git/git-worktree.ts +8 -1
  45. package/src/index.ts +3 -2
  46. package/src/mesh/beads-db.ts +305 -2
  47. package/src/mesh/contracts.ts +329 -0
  48. package/src/mesh/coordinator-prompt.ts +12 -17
  49. package/src/mesh/mesh-active-work.ts +162 -59
  50. package/src/mesh/mesh-events.ts +198 -53
  51. package/src/mesh/mesh-ledger.ts +321 -105
  52. package/src/mesh/mesh-refine-status.ts +2 -3
  53. package/src/mesh/mesh-work-queue.ts +116 -120
  54. package/src/mesh/worktree-bootstrap-config.ts +17 -4
  55. package/src/providers/contracts.ts +19 -0
  56. package/src/providers/provider-loader.ts +21 -7
  57. package/src/providers/provider-schema.ts +12 -0
  58. package/src/providers/read-chat-contract.ts +74 -14
  59. package/src/providers/transcript-v2.ts +567 -0
  60. package/src/repo-mesh-types.ts +10 -0
  61. package/src/shared-types.ts +7 -0
  62. package/src/status/snapshot.ts +35 -11
  63. package/src/types.ts +5 -0
@@ -18,6 +18,14 @@ import { isNativeSourceCanonicalHistory, readChatHistory, readProviderChatHistor
18
18
  import { LOG, getRecentLogs } from '../logging/logger.js';
19
19
  import { getRecentDebugTrace, recordDebugTrace } from '../logging/debug-trace.js';
20
20
  import { buildChatMessageSignature, hashSignatureParts } from '../chat/chat-signatures.js';
21
+ import {
22
+ CHAT_SOURCE_REGISTRY,
23
+ buildV1NativePresentObservation,
24
+ chatSourceSessionKey,
25
+ type ChatSourceDecision,
26
+ type ChatSourceObservation,
27
+ type ChatSourceTransitionCause,
28
+ } from '../chat/source-resolver.js';
21
29
  import type { ChatMessage } from '../types.js';
22
30
  import type { SessionTransport } from '../shared-types.js';
23
31
  import { filterUserFacingChatMessages, normalizeChatMessages } from '../providers/chat-message-normalization.js';
@@ -25,13 +33,25 @@ import { filterUserFacingChatMessages, normalizeChatMessages } from '../provider
25
33
  const RECENT_SEND_WINDOW_MS = 1200;
26
34
  export const READ_CHAT_PROVIDER_EVAL_TIMEOUT_MS = 25_000;
27
35
  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.
36
+ // (A2.2) CLI_NATIVE_HISTORY_FRESH_MS removed with isNativeHistoryFreshEnough.
37
+ // Hardcoded native-transcript provider allow-list. Deprecated. Kept only as a
38
+ // last-resort fallback when ProviderModule is not yet loaded; on every hit we
39
+ // warn so the dependency on this set is visible. A2 deletes the set entirely
40
+ // and routes solely through canonicalHistory.contractVersion +
41
+ // isNativeSourceCanonicalHistory().
34
42
  const CLI_NATIVE_TRANSCRIPT_PROVIDERS = new Set(['codex-cli', 'claude-cli', 'hermes-cli', 'antigravity-cli']);
43
+ const warnedLegacyNativeAllowlistHits = new Set<string>();
44
+ function warnLegacyNativeAllowlistHit(providerType: string): void {
45
+ if (warnedLegacyNativeAllowlistHits.has(providerType)) return;
46
+ warnedLegacyNativeAllowlistHits.add(providerType);
47
+ // eslint-disable-next-line no-console
48
+ console.warn(
49
+ `[chat-commands] supportsCliNativeTranscript fell back to the hardcoded `
50
+ + `CLI_NATIVE_TRANSCRIPT_PROVIDERS set for "${providerType}". `
51
+ + `The provider module was unavailable or did not declare canonicalHistory. `
52
+ + `Set canonicalHistory.contractVersion in the provider.json to remove this dependency.`,
53
+ );
54
+ }
35
55
  const recentSendByTarget = new Map<string, number>();
36
56
 
37
57
  interface ApprovalSelectableInstance extends ProviderInstance {
@@ -288,7 +308,15 @@ function readHistorySessionIdFromMessages(messages: ChatMessage[]): string | und
288
308
  function shouldPreserveNativeIdentity(providerType: string, sessionId: string, message: ChatMessage): boolean {
289
309
  const providerUnitKey = typeof message.providerUnitKey === 'string' ? message.providerUnitKey.trim() : '';
290
310
  const turnKey = typeof message._turnKey === 'string' ? message._turnKey.trim() : '';
291
- if (!providerUnitKey || !turnKey) return false;
311
+ if (!providerUnitKey) return false;
312
+ // (A2.3) v2 stamped identity is producer-owned and globally stable; trust it
313
+ // unconditionally. Producers may omit _turnKey (the daemon recomputes it
314
+ // from the current ordering), so do not require turnKey for v2 messages.
315
+ if (providerUnitKey.startsWith('v2:') || providerUnitKey.startsWith('v2-pty:')) {
316
+ return true;
317
+ }
318
+ // v1 identity always required both keys to be present.
319
+ if (!turnKey) return false;
292
320
  if (providerType === 'hermes-cli' && sessionId) {
293
321
  return providerUnitKey.startsWith(`${providerType}:native:${sessionId}:`)
294
322
  && turnKey.startsWith(`${providerType}:native-turn:${sessionId}:`);
@@ -323,6 +351,18 @@ function normalizeNativeHistoryMessages(providerType: string, messages: ChatMess
323
351
  const meta = message.meta && typeof message.meta === 'object' ? message.meta as Record<string, unknown> : undefined;
324
352
  const isSystemSessionStart = role === 'system' || kind === 'system' || kind === 'session_start';
325
353
  const isActivity = role === 'assistant' && (kind === 'tool' || kind === 'terminal' || kind === 'thought');
354
+ // (A2.3) sequence emit. Producer-supplied wins (v2-stamped messages
355
+ // bring their own monotonic sequence); otherwise derive from
356
+ // receivedAt/timestamp; otherwise positional. Always present on the
357
+ // output so consumers (ChatSourceMachine) have a stable ordering key.
358
+ const existingSequence = typeof (message as any).sequence === 'number'
359
+ && Number.isFinite((message as any).sequence)
360
+ ? (message as any).sequence
361
+ : null;
362
+ const tsCandidate = Number(message.receivedAt || message.timestamp || 0);
363
+ const sequence = existingSequence !== null
364
+ ? existingSequence
365
+ : (tsCandidate > 0 ? tsCandidate : index);
326
366
  return {
327
367
  ...message,
328
368
  role: role === 'human' ? 'user' : (role || 'assistant'),
@@ -332,6 +372,7 @@ function normalizeNativeHistoryMessages(providerType: string, messages: ChatMess
332
372
  && preserveNativeIdentity
333
373
  ? message.bubbleId.trim()
334
374
  : `bubble:${providerUnitKey}`,
375
+ sequence,
335
376
  _turnKey: preserveNativeIdentity
336
377
  ? existingTurnKey
337
378
  : `${providerType}:native-turn:${nativeIdentitySessionId || 'workspace'}:${turnIndex}`,
@@ -420,28 +461,336 @@ function buildCliMessageSourceProvenance(args: {
420
461
  };
421
462
  }
422
463
 
423
- function buildNativeHistoryFallbackReason(args: {
464
+ /**
465
+ * Map a ChatSourceMachine transition cause back to the v1 messageSource
466
+ * `fallbackReason` vocabulary so legacy consumers (web-cloud, tests, mesh
467
+ * debug bundles) keep parsing strings they already know. A3 replaces the
468
+ * caller surface with stateTransition/lockState, after which this map can be
469
+ * deleted.
470
+ *
471
+ * Returns undefined when the cause does not correspond to a fallback (i.e.
472
+ * the source is native-history and there is nothing to explain).
473
+ */
474
+ function causeToLegacyFallbackReason(
475
+ cause: ChatSourceTransitionCause,
476
+ selected: 'native-history' | 'pty-parser',
477
+ extraDetail?: { unavailableReason?: string; nativeSource?: string },
478
+ ): string | undefined {
479
+ if (selected === 'native-history') return undefined;
480
+ switch (cause) {
481
+ case 'initial':
482
+ return 'native_history_not_checked';
483
+ case 'native_progressed':
484
+ // Selected pty-parser despite a progressed observation — that
485
+ // means we held PtyOnly stickily (peak unmet or non-superset).
486
+ return 'native_history_not_selected';
487
+ case 'native_regressed_shrunk':
488
+ return 'native_history_empty';
489
+ case 'native_regressed_unsafe_mapping':
490
+ return 'native_history_not_safely_mapped';
491
+ case 'native_regressed_coverage_partial':
492
+ return 'native_history_partial';
493
+ case 'native_regressed_coverage_unavailable':
494
+ return 'native_history_unavailable';
495
+ case 'native_unavailable_read_error':
496
+ return extraDetail?.unavailableReason
497
+ ? `native_history_unavailable:${extraDetail.unavailableReason}`
498
+ : 'native_history_unavailable';
499
+ case 'native_unavailable_provider_unsupported':
500
+ return 'provider_native_transcript_not_supported';
501
+ case 'native_unavailable_empty':
502
+ return 'native_history_empty';
503
+ case 'native_unavailable_not_native_source':
504
+ return extraDetail?.nativeSource
505
+ ? `native_history_source_${extraDetail.nativeSource}`
506
+ : 'native_history_unavailable';
507
+ }
508
+ }
509
+
510
+ /**
511
+ * Translate a native-history fetch result + provider/adapter context into a
512
+ * ChatSourceObservation and drive ChatSourceRegistry. Returns the decision
513
+ * together with the legacy messageSource payload so call sites can produce
514
+ * a v1-compatible response without duplicating the registry plumbing.
515
+ *
516
+ * This is the replacement for the 300-line if-ladder that previously lived
517
+ * inline in handleReadChat. It is intentionally split out for two reasons:
518
+ * (1) we will call it from two places (CLI adapter branch + history-only
519
+ * branch) instead of duplicating the ladder, (2) tests can drive it with
520
+ * synthetic native-history results to verify the cause→fallbackReason
521
+ * mapping without booting the whole readChat pipeline.
522
+ */
523
+ function decideCliReadChatSource(args: {
424
524
  providerType: string;
425
525
  provider?: ProviderModule;
426
- nativeSource?: string;
427
- nativeHistoryCoverage?: string;
428
- unavailableReason?: string;
429
- nativeMessageCount: number;
526
+ sessionId: string;
527
+ nativeHistoryResult: any | null;
528
+ nativeHistoryError?: unknown;
430
529
  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';
530
+ sessionWorkspace?: string;
531
+ intendedWorkspace?: string;
532
+ ptyMessages: ChatMessage[];
533
+ ptyStatusApprovalOnly: boolean;
534
+ }): {
535
+ decision: ChatSourceDecision;
536
+ messageSource: Record<string, unknown>;
537
+ nativeMessages: ChatMessage[];
538
+ nativeSelected: boolean;
539
+ } {
540
+ const supportsNative = supportsCliNativeTranscript(args.providerType, args.provider);
541
+ const observation = buildObservationForCli(args, supportsNative);
542
+ const sessionKey = chatSourceSessionKey(args.providerType, args.sessionId);
543
+ const decision = CHAT_SOURCE_REGISTRY.observe(sessionKey, observation);
544
+
545
+ const nativeMessages: ChatMessage[] = observation.kind === 'native_present'
546
+ ? extractNativeMessagesFromResult(args.providerType, args.nativeHistoryResult)
547
+ : [];
548
+
549
+ const nativeSource = typeof args.nativeHistoryResult?.source === 'string'
550
+ ? args.nativeHistoryResult.source
551
+ : undefined;
552
+ const sourcePath = typeof args.nativeHistoryResult?.sourcePath === 'string'
553
+ ? args.nativeHistoryResult.sourcePath
554
+ : undefined;
555
+ const sourceMtimeMs = typeof args.nativeHistoryResult?.sourceMtimeMs === 'number'
556
+ ? args.nativeHistoryResult.sourceMtimeMs
557
+ : undefined;
558
+ const coverageHint = typeof args.nativeHistoryResult?.nativeHistoryCoverage === 'string'
559
+ ? args.nativeHistoryResult.nativeHistoryCoverage
560
+ : undefined;
561
+ const partialReason = typeof args.nativeHistoryResult?.partialReason === 'string'
562
+ ? args.nativeHistoryResult.partialReason
563
+ : undefined;
564
+ const unavailableReason = typeof args.nativeHistoryResult?.unavailableReason === 'string'
565
+ ? args.nativeHistoryResult.unavailableReason
566
+ : args.nativeHistoryError
567
+ ? `error:${(args.nativeHistoryError as any)?.message || String(args.nativeHistoryError)}`
568
+ : undefined;
569
+ const nativeHandle = typeof args.nativeHistoryResult?.providerSessionId === 'string'
570
+ ? args.nativeHistoryResult.providerSessionId
571
+ : undefined;
572
+ const transcriptWorkspace = typeof args.nativeHistoryResult?.workspace === 'string'
573
+ ? args.nativeHistoryResult.workspace
574
+ : nativeMessages.map((m: any) => typeof m?.workspace === 'string' ? m.workspace.trim() : '').find(Boolean);
575
+
576
+ const fallbackReason = causeToLegacyFallbackReason(decision.transition.cause, decision.selected, {
577
+ unavailableReason,
578
+ nativeSource: nativeSource && nativeSource !== 'provider-native' ? nativeSource : undefined,
579
+ });
580
+
581
+ // ptyStatusApprovalOnly: when the machine selected native-history we
582
+ // suppress PTY content so the dashboard does not double-show messages
583
+ // already in the native transcript. When the machine selected
584
+ // pty-parser, PTY is the authoritative source — do NOT suppress it.
585
+ // Callers used to hard-code this to `nativeSelected first = true` which
586
+ // suppressed PTY content even when native was empty/unavailable, leaving
587
+ // the dashboard with zero visible messages (the codex generating/waiting
588
+ // approval stuck state). Trust the machine here, not the caller hint.
589
+ const ptyStatusApprovalOnly = decision.selected === 'native-history'
590
+ ? true
591
+ : args.ptyStatusApprovalOnly;
592
+
593
+ const messageSource = buildCliMessageSourceProvenance({
594
+ selected: decision.selected,
595
+ provider: args.providerType,
596
+ nativeHandle,
597
+ sessionWorkspace: args.sessionWorkspace,
598
+ intendedWorkspace: args.intendedWorkspace,
599
+ transcriptWorkspace,
600
+ fallbackReason,
601
+ nativeSource,
602
+ sourcePath,
603
+ sourceMtimeMs,
604
+ nativeHistoryCoverage: coverageHint,
605
+ partialReason,
606
+ unavailableReason,
607
+ nativeMessages,
608
+ ptyMessages: args.ptyMessages,
609
+ returnedMessages: decision.selected === 'native-history' ? nativeMessages : args.ptyMessages,
610
+ safeMapping: args.safeMapping,
611
+ // freshEnough is a v1 concept the machine does not model directly.
612
+ // We surface lockState.locked here so v1 consumers reading
613
+ // staleness.freshEnough still get a meaningful boolean.
614
+ freshEnough: decision.lockState.locked,
615
+ ptyStatusApprovalOnly,
616
+ });
617
+
618
+ return {
619
+ decision,
620
+ messageSource,
621
+ nativeMessages,
622
+ nativeSelected: decision.selected === 'native-history',
623
+ };
624
+ }
625
+
626
+ function buildObservationForCli(
627
+ args: {
628
+ providerType: string;
629
+ sessionId: string;
630
+ nativeHistoryResult: any | null;
631
+ nativeHistoryError?: unknown;
632
+ safeMapping: boolean;
633
+ },
634
+ supportsNative: boolean,
635
+ ): ChatSourceObservation {
636
+ if (!supportsNative) {
637
+ return { kind: 'native_unavailable', reason: 'provider_not_supported' };
638
+ }
639
+ if (args.nativeHistoryError) {
640
+ return { kind: 'native_unavailable', reason: 'read_error' };
641
+ }
642
+ const result = args.nativeHistoryResult;
643
+ if (!result || typeof result !== 'object') {
644
+ return { kind: 'native_unavailable', reason: 'read_error' };
645
+ }
646
+ const source = typeof result.source === 'string' ? result.source : '';
647
+ if (source && source !== 'provider-native') {
648
+ // 'native-unavailable' or other producer-side declined source.
649
+ return { kind: 'native_unavailable', reason: source === 'native-unavailable' ? 'empty' : 'not_native_source' };
650
+ }
651
+ const messages = Array.isArray(result.messages) ? result.messages : [];
652
+ if (messages.length === 0) {
653
+ return { kind: 'native_unavailable', reason: 'empty' };
654
+ }
655
+ const coverage = typeof result.nativeHistoryCoverage === 'string'
656
+ ? result.nativeHistoryCoverage
657
+ : 'tail';
658
+ if (coverage === 'unavailable') {
659
+ return { kind: 'native_unavailable', reason: 'coverage_unavailable' };
660
+ }
661
+ return buildV1NativePresentObservation({
662
+ providerType: args.providerType,
663
+ sessionId: args.sessionId,
664
+ messages,
665
+ coverage: coverage === 'full' || coverage === 'tail' || coverage === 'current-turn' || coverage === 'partial'
666
+ ? coverage
667
+ : 'tail',
668
+ safeMapping: args.safeMapping,
669
+ });
443
670
  }
444
671
 
672
+ function extractNativeMessagesFromResult(providerType: string, result: any): ChatMessage[] {
673
+ if (!result || !Array.isArray(result.messages)) return [];
674
+ return normalizeNativeHistoryMessages(
675
+ providerType,
676
+ result.messages as ChatMessage[],
677
+ typeof result.providerSessionId === 'string' ? result.providerSessionId : undefined,
678
+ );
679
+ }
680
+
681
+ /**
682
+ * ptyStatusApprovalOnly is true when the daemon should treat PTY content as
683
+ * status/approval signal only (not as chat messages). v1 set this to `true`
684
+ * whenever native-history was selected as the source, and `false` otherwise.
685
+ * The machine equivalent: when native is the source we want PTY suppressed.
686
+ */
687
+ function primaryPtyApprovalOnlyFor(_cliType: string, nativeSelected: boolean): boolean {
688
+ return nativeSelected;
689
+ }
690
+
691
+ /**
692
+ * Codex-only unsafe-native fallback: when the primary native fetch produced
693
+ * unsafe-mapping data, v1 attempted to recover by reading exact runtime
694
+ * mirror messages, runtime input ACK messages, or by trusting the current-
695
+ * runtime PTY when safely attributed. None of this is the machine's
696
+ * responsibility — the machine already decided pty-parser. This helper
697
+ * preserves the daemon-side message selection and annotates messageSource.
698
+ */
699
+ function applyUnsafeNativeDaemonFallback(args: {
700
+ providerType: string;
701
+ adapter: CliAdapter;
702
+ helpers: CommandHelpers;
703
+ readChatArgs: any;
704
+ sessionWorkspace?: string;
705
+ intendedWorkspace?: string;
706
+ ptyMessages: ChatMessage[];
707
+ nativeHistoryLimit: number;
708
+ provider?: ProviderModule;
709
+ messageSourceRef: { set(value: Record<string, unknown>): void; get(): Record<string, unknown> };
710
+ apply(selection: {
711
+ messages: ChatMessage[];
712
+ transcriptAuthority?: 'provider' | 'daemon';
713
+ coverage?: 'full' | 'tail' | 'current-turn';
714
+ status?: string;
715
+ }): void;
716
+ activeModal: unknown;
717
+ returnedStatus: string;
718
+ coverage?: 'full' | 'tail' | 'current-turn';
719
+ }): void {
720
+ if (args.adapter.cliType !== 'codex-cli') {
721
+ // Only codex-cli had v1 daemon mirror recovery. Other providers skip.
722
+ return;
723
+ }
724
+ const ms = args.messageSourceRef.get();
725
+ const fallbackReason = typeof ms.fallbackReason === 'string' ? ms.fallbackReason : '';
726
+ if (!isUnsafeNativeTranscriptFallback(fallbackReason)) {
727
+ return;
728
+ }
729
+ const safeCurrentRuntimePtyMessages = isCurrentRuntimePtySafelyAttributed({
730
+ adapter: args.adapter,
731
+ helpers: args.helpers,
732
+ readChatArgs: args.readChatArgs,
733
+ sessionWorkspace: args.sessionWorkspace,
734
+ intendedWorkspace: args.intendedWorkspace,
735
+ ptyMessages: args.ptyMessages,
736
+ });
737
+ if (safeCurrentRuntimePtyMessages) {
738
+ args.apply({
739
+ messages: args.ptyMessages,
740
+ transcriptAuthority: 'daemon',
741
+ coverage: args.coverage || 'current-turn',
742
+ status: args.returnedStatus,
743
+ });
744
+ const next = { ...ms, selectedDaemonSource: 'current-runtime-pty', transcriptAuthority: 'daemon', runtimeMappingSafe: true };
745
+ args.messageSourceRef.set(next);
746
+ return;
747
+ }
748
+ const safeRuntimeAckMessages = selectRuntimeInputAckMessages(args.ptyMessages);
749
+ if (safeRuntimeAckMessages.length > 0) {
750
+ args.apply({
751
+ messages: safeRuntimeAckMessages,
752
+ transcriptAuthority: 'daemon',
753
+ coverage: 'tail',
754
+ status: coerceUnsafeNativeFallbackStatus(args.returnedStatus, args.activeModal),
755
+ });
756
+ const next = { ...ms, ptyStatusApprovalOnly: true };
757
+ args.messageSourceRef.set(next);
758
+ return;
759
+ }
760
+ const exactRuntimeMirrorMessages = readExactRuntimeMirrorMessages({
761
+ providerType: args.providerType,
762
+ targetSessionId: typeof args.readChatArgs?.targetSessionId === 'string' ? args.readChatArgs.targetSessionId : undefined,
763
+ currentSessionId: typeof (args.helpers.currentSession as any)?.sessionId === 'string' ? (args.helpers.currentSession as any).sessionId : undefined,
764
+ tailLimit: args.nativeHistoryLimit,
765
+ historyBehavior: args.provider?.historyBehavior,
766
+ });
767
+ if (exactRuntimeMirrorMessages.length > 0) {
768
+ args.apply({
769
+ messages: exactRuntimeMirrorMessages,
770
+ transcriptAuthority: 'daemon',
771
+ coverage: 'tail',
772
+ status: coerceUnsafeNativeFallbackStatus(args.returnedStatus, args.activeModal),
773
+ });
774
+ const next = { ...ms, selectedDaemonSource: 'exact-runtime-mirror', transcriptAuthority: 'daemon', ptyStatusApprovalOnly: true };
775
+ args.messageSourceRef.set(next);
776
+ return;
777
+ }
778
+ // No daemon mirror available — keep PTY messages as-is (still pty-parser
779
+ // selection); just coerce status for waiting_approval consistency.
780
+ args.apply({
781
+ messages: args.ptyMessages,
782
+ coverage: args.coverage,
783
+ status: coerceUnsafeNativeFallbackStatus(args.returnedStatus, args.activeModal),
784
+ });
785
+ const next = { ...ms, ptyStatusApprovalOnly: true };
786
+ args.messageSourceRef.set(next);
787
+ }
788
+
789
+ // (A2.2) buildNativeHistoryFallbackReason removed. ChatSourceMachine emits a
790
+ // ChatSourceTransitionCause; causeToLegacyFallbackReason maps it back to the
791
+ // v1 vocabulary for response compatibility. A3 deletes the v1 vocabulary
792
+ // entirely and surfaces stateTransition/lockState directly.
793
+
445
794
  function isUnsafeNativeTranscriptFallback(reason?: string): boolean {
446
795
  const value = String(reason || '').trim();
447
796
  return value.startsWith('native_history_unavailable')
@@ -551,8 +900,21 @@ function isCurrentRuntimePtySafelyAttributed(args: {
551
900
  }
552
901
 
553
902
  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);
903
+ // Preferred path: the provider module declares canonicalHistory in its
904
+ // provider.json. We trust that declaration regardless of the legacy
905
+ // allow-list. A2 will additionally require canonicalHistory.contractVersion
906
+ // to be a supported value (transcript-v2.ts).
907
+ if (provider?.category === 'cli' && isNativeSourceCanonicalHistory(provider?.canonicalHistory)) {
908
+ return true;
909
+ }
910
+ // Last-resort fallback for early call sites where the provider module is
911
+ // not yet loaded. Warn once per provider type so this dependency is visible
912
+ // and can be removed in A2.
913
+ if (CLI_NATIVE_TRANSCRIPT_PROVIDERS.has(providerType)) {
914
+ warnLegacyNativeAllowlistHit(providerType);
915
+ return true;
916
+ }
917
+ return false;
556
918
  }
557
919
 
558
920
  function getComparableVisibleText(message: ChatMessage | undefined): string {
@@ -650,6 +1012,7 @@ function readCliProviderNativeHistory(agentStr: string, args: {
650
1012
  excludeRecentCount: number;
651
1013
  historyBehavior?: ProviderModule['historyBehavior'];
652
1014
  scripts?: ProviderScripts;
1015
+ excludeInProgressTurn?: boolean;
653
1016
  }): ReturnType<typeof readProviderChatHistory> & { lookup: 'session' | 'workspace' } {
654
1017
  if (!args.historySessionId) {
655
1018
  return {
@@ -669,6 +1032,7 @@ function readCliProviderNativeHistory(agentStr: string, args: {
669
1032
  excludeRecentCount: args.excludeRecentCount,
670
1033
  historyBehavior: args.historyBehavior,
671
1034
  scripts: args.scripts as any,
1035
+ excludeInProgressTurn: args.excludeInProgressTurn,
672
1036
  });
673
1037
  // Native transcripts are keyed by provider/runtime session identity. Falling
674
1038
  // back to workspace makes concurrent local Codex/Hermes sessions alias each
@@ -700,18 +1064,12 @@ function readLiveCodexWorkspaceNativeHistory(agentStr: string, args: {
700
1064
  return { ...(history as any), lookup: 'workspace' };
701
1065
  }
702
1066
 
703
- function isNativeHistoryFreshEnough(args: {
704
- sourceMtimeMs?: number;
705
- nativeMessages: ChatMessage[];
706
- ptyMessages: ChatMessage[];
707
- }): boolean {
708
- const nativeNewest = getMessageNewestReceivedAt(args.nativeMessages);
709
- const ptyNewest = getMessageNewestReceivedAt(args.ptyMessages);
710
- if (nativeNewest > 0 && nativeNewest >= ptyNewest) return true;
711
- const sourceMtimeMs = Number(args.sourceMtimeMs || 0);
712
- if (sourceMtimeMs > 0 && Date.now() - sourceMtimeMs <= CLI_NATIVE_HISTORY_FRESH_MS) return true;
713
- return ptyNewest === 0 && nativeNewest > 0;
714
- }
1067
+ // (A2.2) isNativeHistoryFreshEnough removed. The v1 freshness comparison
1068
+ // (native_newest vs pty_newest with a 5-minute mtime grace window) was the
1069
+ // direct cause of the plipping behaviour: PTY arrived every turn so native
1070
+ // looked stale by default. ChatSourceMachine never compares native vs PTY
1071
+ // freshness — the lock holds across arbitrary PTY arrival. See
1072
+ // chat/source-machine.ts for the new semantics.
715
1073
 
716
1074
  function shouldPreserveReadChatPayloadField(key: string): boolean {
717
1075
  return key === 'messageSource' || key === 'transcriptProvenance';
@@ -797,6 +1155,15 @@ function normalizeReadChatCommandStatus(status: unknown, activeModal: unknown):
797
1155
  case 'disconnected':
798
1156
  case 'not_monitored':
799
1157
  return 'error';
1158
+ case 'waiting_approval':
1159
+ // The contract validator requires activeModal+buttons whenever
1160
+ // status is waiting_approval. If a producer/coercer set this
1161
+ // status without staging the modal yet (a race we hit with
1162
+ // codex-cli during tool approval setup), downgrade to a
1163
+ // generating-like status so readChat still returns successfully.
1164
+ // The next poll will pick up the modal once the provider has
1165
+ // emitted it.
1166
+ return hasNonEmptyModalButtons(activeModal) ? 'waiting_approval' : 'generating';
800
1167
  default:
801
1168
  return raw;
802
1169
  }
@@ -865,6 +1232,64 @@ function finalizeStreamingMessagesWhenIdle(messages: ChatMessage[], status: stri
865
1232
  });
866
1233
  }
867
1234
 
1235
+ /**
1236
+ * Collapse adjacent PTY messages whose canonical (whitespace-stripped)
1237
+ * content is identical, OR whose turn key + role/kind match.
1238
+ *
1239
+ * The PTY parser of some providers (hermes-cli observed in the wild)
1240
+ * emits the same logical assistant turn twice when the terminal re-wraps
1241
+ * the text at a different column. The two emissions differ in newline
1242
+ * position — and sometimes in a single inserted space next to punctuation
1243
+ * (e.g. `(수정 2개), upstream` vs `(수정 2개 ), upstream`), so a simple
1244
+ * `\s+ -> ' '` normalize cannot collapse them.
1245
+ *
1246
+ * Strategy:
1247
+ * 1. If both messages carry the same _turnKey + role + kind, they are
1248
+ * the same logical turn by construction. Collapse.
1249
+ * 2. Otherwise compare with all whitespace stripped — wrap variants
1250
+ * collapse to identical strings.
1251
+ *
1252
+ * Native-history paths run through pageHistoryRecords and already
1253
+ * collapse on a normalized signature; this helper is the PTY equivalent
1254
+ * the readChat sync path was missing.
1255
+ */
1256
+ function collapseAdjacentDuplicateChatMessages(messages: ChatMessage[]): ChatMessage[] {
1257
+ if (!Array.isArray(messages) || messages.length <= 1) return messages;
1258
+ const result: ChatMessage[] = [];
1259
+ let prevRoleKind = '';
1260
+ let prevStripped = '';
1261
+ for (const message of messages) {
1262
+ const role = typeof message.role === 'string' ? message.role : '';
1263
+ const kind = typeof message.kind === 'string' ? message.kind : 'standard';
1264
+ const content = typeof message.content === 'string'
1265
+ ? message.content
1266
+ : (Array.isArray(message.content) ? message.content.map((p: any) => typeof p?.text === 'string' ? p.text : '').join('') : '');
1267
+ const strippedContent = content.replace(/\s+/g, '');
1268
+ // Empty content or system messages are passed through untouched.
1269
+ if (!strippedContent || role === 'system') {
1270
+ result.push(message);
1271
+ prevRoleKind = '';
1272
+ prevStripped = '';
1273
+ continue;
1274
+ }
1275
+ const roleKind = `${role}:${kind}`;
1276
+ const sameStripped = strippedContent === prevStripped && roleKind === prevRoleKind;
1277
+ if (result.length > 0 && sameStripped) {
1278
+ // Adjacent duplicate after stripping all whitespace. Keep the
1279
+ // *later* copy because PTY's last emission usually has the most
1280
+ // complete formatting.
1281
+ result[result.length - 1] = message;
1282
+ prevRoleKind = roleKind;
1283
+ prevStripped = strippedContent;
1284
+ continue;
1285
+ }
1286
+ result.push(message);
1287
+ prevRoleKind = roleKind;
1288
+ prevStripped = strippedContent;
1289
+ }
1290
+ return result;
1291
+ }
1292
+
868
1293
  function buildReadChatCommandResult(payload: Record<string, any>, args: any): CommandResult {
869
1294
  let validatedPayload: Record<string, any>;
870
1295
  const debugReadChat = payload?.debugReadChat && typeof payload.debugReadChat === 'object'
@@ -1475,7 +1900,9 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
1475
1900
  const activeModal = parsedRecord.activeModal ?? parsedRecord.modal ?? null;
1476
1901
  const returnedStatus = normalizeCliReadChatStatus(parsedRecord.status, activeModal, adapter, adapterStatus, parsedRecord.messages);
1477
1902
  const runtimeMessageMerger = getTargetInstance(h, args) as RuntimeChatMessageMerger | null;
1478
- const parsedMessages = finalizeStreamingMessagesWhenIdle(parsedRecord.messages as ChatMessage[], returnedStatus);
1903
+ const parsedMessages = collapseAdjacentDuplicateChatMessages(
1904
+ finalizeStreamingMessagesWhenIdle(parsedRecord.messages as ChatMessage[], returnedStatus),
1905
+ );
1479
1906
  const returnedMessages = runtimeMessageMerger?.category === 'cli'
1480
1907
  && runtimeMessageMerger.type === adapter.cliType
1481
1908
  && typeof runtimeMessageMerger.mergeRuntimeChatMessages === 'function'
@@ -1494,35 +1921,57 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
1494
1921
  ? adapter.workingDir
1495
1922
  : undefined;
1496
1923
  const intendedWorkspace = typeof args?.workspace === 'string' ? args.workspace : undefined;
1497
- let messageSource = buildCliMessageSourceProvenance({
1498
- selected: 'pty-parser',
1499
- provider: adapter.cliType,
1500
- fallbackReason: supportsCliNativeTranscript(providerType, provider) ? 'native_history_not_checked' : 'provider_native_transcript_not_supported',
1501
- sessionWorkspace,
1502
- intendedWorkspace,
1503
- ptyMessages: returnedMessages,
1504
- returnedMessages,
1505
- ptyStatusApprovalOnly: false,
1506
- });
1924
+ // ───────────────────────────────────────────────────────────
1925
+ // Chat source decision via ChatSourceMachine (A2 big-bang).
1926
+ // Replaces the ~300-line if-ladder that mixed source decision
1927
+ // with native fetch, anchor mutation, and runtime mirror
1928
+ // selection. The machine decides only between native-history
1929
+ // and pty-parser; downstream selection of which message array
1930
+ // to surface stays here.
1931
+ //
1932
+ // Behavioural changes vs v1:
1933
+ // - No more nativeHistoryAnchoredAt mutation on the adapter.
1934
+ // Lock state lives in CHAT_SOURCE_REGISTRY keyed by
1935
+ // (providerType, sessionId).
1936
+ // - No PTY-vs-native freshness comparison. The lock holds
1937
+ // across arbitrary PTY arrival; only native regression /
1938
+ // unavailability unlocks. This is the plipping fix.
1939
+ // - 6 trigger strings (native_history_partial / _stale /
1940
+ // _not_safely_mapped / _empty / _error / _unavailable)
1941
+ // collapse to 3 events with diagnostic causes preserved
1942
+ // and mapped back to legacy fallbackReason strings for
1943
+ // response compatibility.
1944
+ // - Codex live-workspace native probe and unsafe-native
1945
+ // daemon mirror fallbacks are preserved as additional
1946
+ // input rounds to the machine; they were never the source
1947
+ // decision itself, they were retries.
1948
+ // ───────────────────────────────────────────────────────────
1949
+
1950
+ const supportsNative = supportsCliNativeTranscript(providerType, provider)
1951
+ && isNativeSourceCanonicalHistory(provider?.canonicalHistory);
1952
+ const agentStr = provider?.type || args?.agentType || getCurrentProviderType(h, adapter.cliType);
1953
+ const workspace = sessionWorkspace;
1954
+ const nativeHistoryLimit = Math.max(
1955
+ normalizeReadChatTailLimit(args) || 0,
1956
+ returnedMessages.length,
1957
+ 200,
1958
+ );
1959
+ const nativeHistorySessionId = supportsNative
1960
+ ? resolveCliNativeHistorySessionId(args, historySessionId, providerSessionId)
1961
+ : undefined;
1962
+ const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
1963
+ const exactNativeHistoryScope = Boolean(
1964
+ (typeof args?.historySessionId === 'string' && args.historySessionId.trim())
1965
+ || (typeof args?.providerSessionId === 'string' && args.providerSessionId.trim())
1966
+ || providerSessionId
1967
+ || (nativeHistorySessionId && nativeHistorySessionId !== targetSessionId)
1968
+ || ((h.currentSession as any)?.sessionId === args?.targetSessionId && typeof (h.currentSession as any)?.providerSessionId === 'string' && (h.currentSession as any).providerSessionId.trim())
1969
+ );
1507
1970
 
1508
- if (supportsCliNativeTranscript(providerType, provider) && isNativeSourceCanonicalHistory(provider?.canonicalHistory)) {
1509
- const agentStr = provider?.type || args?.agentType || getCurrentProviderType(h, adapter.cliType);
1510
- const workspace = sessionWorkspace;
1511
- const nativeHistoryLimit = Math.max(
1512
- normalizeReadChatTailLimit(args) || 0,
1513
- returnedMessages.length,
1514
- 200,
1515
- );
1516
- const nativeHistorySessionId = resolveCliNativeHistorySessionId(args, historySessionId, providerSessionId);
1517
- const targetSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
1518
- const exactNativeHistoryScope = Boolean(
1519
- (typeof args?.historySessionId === 'string' && args.historySessionId.trim())
1520
- || (typeof args?.providerSessionId === 'string' && args.providerSessionId.trim())
1521
- || providerSessionId
1522
- || (nativeHistorySessionId && nativeHistorySessionId !== targetSessionId)
1523
- || ((h.currentSession as any)?.sessionId === args?.targetSessionId && typeof (h.currentSession as any)?.providerSessionId === 'string' && (h.currentSession as any).providerSessionId.trim())
1524
- );
1525
- let nativeHistory: (ReturnType<typeof readProviderChatHistory> & { lookup?: 'session' | 'workspace' }) | null = null;
1971
+ // 1. Fetch native history (or skip if provider does not support it).
1972
+ let nativeHistory: any | null = null;
1973
+ let nativeHistoryError: unknown | undefined;
1974
+ if (supportsNative) {
1526
1975
  try {
1527
1976
  nativeHistory = readCliProviderNativeHistory(agentStr, {
1528
1977
  canonicalHistory: provider?.canonicalHistory,
@@ -1533,257 +1982,185 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
1533
1982
  excludeRecentCount: 0,
1534
1983
  historyBehavior: provider?.historyBehavior,
1535
1984
  scripts: provider?.scripts as any,
1985
+ excludeInProgressTurn: returnedStatus === 'waiting_approval',
1536
1986
  });
1537
1987
  } catch (error: any) {
1538
- const fallbackReason = `native_history_error:${error?.message || String(error)}`;
1539
- messageSource = buildCliMessageSourceProvenance({
1540
- selected: 'pty-parser',
1541
- provider: adapter.cliType,
1542
- fallbackReason,
1543
- sessionWorkspace,
1544
- intendedWorkspace,
1545
- ptyMessages: returnedMessages,
1546
- returnedMessages,
1547
- ptyStatusApprovalOnly: false,
1548
- });
1988
+ nativeHistoryError = error;
1549
1989
  nativeHistory = null;
1550
1990
  }
1991
+ }
1992
+
1993
+ // 2. Compute safeMapping with the same rules the v1 code used so the
1994
+ // machine sees the same observation it always would have.
1995
+ const nativeMessages: ChatMessage[] = nativeHistory && Array.isArray(nativeHistory.messages)
1996
+ ? normalizeNativeHistoryMessages(agentStr, nativeHistory.messages as ChatMessage[], nativeHistory.providerSessionId)
1997
+ : [];
1998
+ const historyProviderSessionId = typeof nativeHistory?.providerSessionId === 'string'
1999
+ ? nativeHistory.providerSessionId
2000
+ : readHistorySessionIdFromMessages(nativeMessages) || nativeHistorySessionId || historySessionId;
2001
+ const lookup = nativeHistory?.lookup === 'workspace' ? 'workspace' : 'session';
2002
+ const nativeHistorySessionForMapping = adapter.cliType === 'antigravity-cli'
2003
+ && historyProviderSessionId
2004
+ && nativeHistorySessionId
2005
+ && historyProviderSessionId !== nativeHistorySessionId
2006
+ ? undefined
2007
+ : nativeHistorySessionId;
2008
+ const safeMapping = supportsNative && nativeHistory
2009
+ ? hasSafeNativeHistoryMapping({
2010
+ historySessionId: lookup === 'workspace' ? undefined : nativeHistorySessionForMapping,
2011
+ providerSessionId: lookup === 'workspace' ? undefined : historyProviderSessionId || providerSessionId,
2012
+ workspace,
2013
+ nativeMessages,
2014
+ ptyMessages: returnedMessages,
2015
+ requireWorkspaceContentOverlap: lookup === 'workspace' && !exactNativeHistoryScope,
2016
+ })
2017
+ : false;
1551
2018
 
1552
- if (nativeHistory) {
1553
- const nativeMessages = Array.isArray((nativeHistory as any).messages)
1554
- ? normalizeNativeHistoryMessages(agentStr, (nativeHistory as any).messages as ChatMessage[], (nativeHistory as any)?.providerSessionId)
1555
- : [];
1556
- const historyProviderSessionId = typeof (nativeHistory as any)?.providerSessionId === 'string'
1557
- ? (nativeHistory as any).providerSessionId
1558
- : readHistorySessionIdFromMessages(nativeMessages) || nativeHistorySessionId || historySessionId;
1559
- const nativeHistoryCoverage = typeof (nativeHistory as any)?.nativeHistoryCoverage === 'string'
1560
- ? (nativeHistory as any).nativeHistoryCoverage
1561
- : undefined;
1562
- const partialReason = typeof (nativeHistory as any)?.partialReason === 'string'
1563
- ? (nativeHistory as any).partialReason
1564
- : undefined;
1565
- const unavailableReason = typeof (nativeHistory as any)?.unavailableReason === 'string'
1566
- ? (nativeHistory as any).unavailableReason
1567
- : undefined;
1568
- const lookup = (nativeHistory as any).lookup === 'workspace' ? 'workspace' : 'session';
1569
- const transcriptWorkspace = typeof (nativeHistory as any)?.workspace === 'string'
1570
- ? (nativeHistory as any).workspace
1571
- : nativeMessages.map((message: any) => typeof message?.workspace === 'string' ? message.workspace.trim() : '').find(Boolean);
1572
- const nativeHistorySessionForMapping = adapter.cliType === 'antigravity-cli'
1573
- && historyProviderSessionId
1574
- && nativeHistorySessionId
1575
- && historyProviderSessionId !== nativeHistorySessionId
1576
- ? undefined
1577
- : nativeHistorySessionId;
1578
- const safeMapping = hasSafeNativeHistoryMapping({
1579
- historySessionId: lookup === 'workspace' ? undefined : nativeHistorySessionForMapping,
1580
- providerSessionId: lookup === 'workspace' ? undefined : historyProviderSessionId || providerSessionId,
2019
+ // 3. Drive ChatSourceMachine — one observation per readChat call,
2020
+ // keyed by (providerType, sessionKey-for-this-call). targetSessionId
2021
+ // is the most specific session anchor we have; fall back to
2022
+ // historySessionId so we never leak state across distinct sessions.
2023
+ const machineSessionKey = String(
2024
+ args?.targetSessionId
2025
+ || providerSessionId
2026
+ || historySessionId
2027
+ || (h.currentSession as any)?.sessionId
2028
+ || ''
2029
+ );
2030
+ const primary = decideCliReadChatSource({
2031
+ providerType,
2032
+ provider,
2033
+ sessionId: machineSessionKey,
2034
+ nativeHistoryResult: nativeHistory,
2035
+ nativeHistoryError,
2036
+ safeMapping,
2037
+ sessionWorkspace,
2038
+ intendedWorkspace,
2039
+ ptyMessages: returnedMessages,
2040
+ // Start with PTY visible; decideCliReadChatSource flips this
2041
+ // to true when the machine actually selects native-history.
2042
+ ptyStatusApprovalOnly: false,
2043
+ });
2044
+ let messageSource: Record<string, unknown> = primary.messageSource;
2045
+
2046
+ if (primary.nativeSelected) {
2047
+ selectedMessages = finalizeStreamingMessagesWhenIdle(primary.nativeMessages, returnedStatus);
2048
+ selectedProviderSessionId = historyProviderSessionId || providerSessionId;
2049
+ selectedTranscriptAuthority = 'provider';
2050
+ selectedCoverage = nativeHistory?.hasMore ? 'tail' : 'full';
2051
+ } else if (supportsNative) {
2052
+ // Native not selected. Two preserved v1 fallbacks before settling
2053
+ // on PTY: (a) Codex-only live workspace native probe; (b) unsafe-
2054
+ // native daemon mirror selection. The machine sees each retry as
2055
+ // an additional observation.
2056
+ const liveCurrentRuntimePtySafe = isCurrentRuntimePtySafelyAttributed({
2057
+ adapter,
2058
+ helpers: h,
2059
+ readChatArgs: args,
2060
+ sessionWorkspace,
2061
+ intendedWorkspace,
2062
+ ptyMessages: returnedMessages,
2063
+ });
2064
+ const mayProbeLiveCodexWorkspaceNative = adapter.cliType === 'codex-cli'
2065
+ && liveCurrentRuntimePtySafe
2066
+ && !(typeof args?.providerSessionId === 'string' && args.providerSessionId.trim())
2067
+ && !(providerSessionId && providerSessionId.trim())
2068
+ && (!historyProviderSessionId || historyProviderSessionId === nativeHistorySessionId || historyProviderSessionId === historySessionId);
2069
+ const liveWorkspaceNativeHistory = mayProbeLiveCodexWorkspaceNative
2070
+ ? readLiveCodexWorkspaceNativeHistory(agentStr, {
2071
+ canonicalHistory: provider?.canonicalHistory,
1581
2072
  workspace,
1582
- nativeMessages,
2073
+ offset: 0,
2074
+ limit: nativeHistoryLimit,
2075
+ excludeRecentCount: 0,
2076
+ historyBehavior: provider?.historyBehavior,
2077
+ scripts: provider?.scripts as any,
2078
+ })
2079
+ : null;
2080
+ const liveWorkspaceNativeMessages = Array.isArray((liveWorkspaceNativeHistory as any)?.messages)
2081
+ ? normalizeNativeHistoryMessages(agentStr, (liveWorkspaceNativeHistory as any).messages as ChatMessage[], (liveWorkspaceNativeHistory as any)?.providerSessionId)
2082
+ : [];
2083
+ const liveWorkspaceNativeProviderSessionId = typeof (liveWorkspaceNativeHistory as any)?.providerSessionId === 'string'
2084
+ ? (liveWorkspaceNativeHistory as any).providerSessionId
2085
+ : readHistorySessionIdFromMessages(liveWorkspaceNativeMessages);
2086
+ const liveWorkspaceNativeSafeMapping = liveWorkspaceNativeMessages.length > 0
2087
+ && hasSafeNativeHistoryMapping({
2088
+ workspace,
2089
+ nativeMessages: liveWorkspaceNativeMessages,
1583
2090
  ptyMessages: returnedMessages,
1584
- requireWorkspaceContentOverlap: lookup === 'workspace' && !exactNativeHistoryScope,
2091
+ requireWorkspaceContentOverlap: true,
1585
2092
  });
1586
- const freshEnough = isNativeHistoryFreshEnough({
1587
- sourceMtimeMs: (nativeHistory as any).sourceMtimeMs,
1588
- nativeMessages,
2093
+ if (liveWorkspaceNativeHistory) {
2094
+ const liveDecision = decideCliReadChatSource({
2095
+ providerType,
2096
+ provider,
2097
+ // Distinct session key so a transient codex live-probe does not
2098
+ // clobber the primary session's lock. The machine treats this
2099
+ // as its own session; the primary session's state is untouched.
2100
+ sessionId: `${machineSessionKey}::live-workspace`,
2101
+ nativeHistoryResult: liveWorkspaceNativeHistory,
2102
+ safeMapping: liveWorkspaceNativeSafeMapping,
2103
+ sessionWorkspace,
2104
+ intendedWorkspace,
1589
2105
  ptyMessages: returnedMessages,
2106
+ ptyStatusApprovalOnly: true,
1590
2107
  });
1591
- const nativeUsableForChatMessages = (nativeHistory as any).source === 'provider-native'
1592
- && nativeMessages.length > 0
1593
- && nativeHistoryCoverage !== 'partial'
1594
- && nativeHistoryCoverage !== 'unavailable'
1595
- && safeMapping;
1596
- const allowStaleNativeChatMessages = adapter.cliType === 'antigravity-cli' && nativeUsableForChatMessages;
1597
- if (nativeUsableForChatMessages && (freshEnough || allowStaleNativeChatMessages)) {
1598
- selectedMessages = finalizeStreamingMessagesWhenIdle(nativeMessages, returnedStatus);
1599
- selectedProviderSessionId = historyProviderSessionId || providerSessionId;
2108
+ if (liveDecision.nativeSelected) {
2109
+ selectedMessages = finalizeStreamingMessagesWhenIdle(liveDecision.nativeMessages, returnedStatus);
2110
+ selectedProviderSessionId = liveWorkspaceNativeProviderSessionId || providerSessionId;
1600
2111
  selectedTranscriptAuthority = 'provider';
1601
- selectedCoverage = (nativeHistory as any).hasMore ? 'tail' : 'full';
1602
- messageSource = buildCliMessageSourceProvenance({
1603
- selected: 'native-history',
1604
- provider: adapter.cliType,
1605
- nativeHandle: selectedProviderSessionId || nativeHistorySessionId || historySessionId,
1606
- sessionWorkspace,
1607
- intendedWorkspace,
1608
- transcriptWorkspace,
1609
- nativeSource: (nativeHistory as any).source,
1610
- sourcePath: (nativeHistory as any).sourcePath,
1611
- sourceMtimeMs: (nativeHistory as any).sourceMtimeMs,
1612
- nativeHistoryCoverage,
1613
- partialReason,
1614
- unavailableReason,
1615
- nativeMessages,
1616
- ptyMessages: returnedMessages,
1617
- returnedMessages: selectedMessages,
1618
- safeMapping,
1619
- freshEnough,
1620
- ptyStatusApprovalOnly: true,
1621
- });
2112
+ selectedCoverage = (liveWorkspaceNativeHistory as any).hasMore ? 'tail' : 'full';
2113
+ messageSource = liveDecision.messageSource;
2114
+ (messageSource as any).selectedDaemonSource = 'live-workspace-native-history';
2115
+ (messageSource as any).runtimeMappingSafe = true;
1622
2116
  } else {
1623
- const liveCurrentRuntimePtySafe = isCurrentRuntimePtySafelyAttributed({
2117
+ // Live probe also rejected: apply unsafe-native daemon mirror
2118
+ // selection (codex-only) using the primary decision's
2119
+ // fallbackReason.
2120
+ applyUnsafeNativeDaemonFallback({
2121
+ providerType,
1624
2122
  adapter,
1625
2123
  helpers: h,
1626
2124
  readChatArgs: args,
1627
2125
  sessionWorkspace,
1628
2126
  intendedWorkspace,
1629
2127
  ptyMessages: returnedMessages,
2128
+ nativeHistoryLimit,
2129
+ provider,
2130
+ messageSourceRef: { set(value) { messageSource = value; }, get() { return messageSource; } },
2131
+ apply(selection) {
2132
+ selectedMessages = selection.messages;
2133
+ selectedTranscriptAuthority = selection.transcriptAuthority;
2134
+ selectedCoverage = selection.coverage ?? coverage;
2135
+ selectedStatus = selection.status ?? returnedStatus;
2136
+ },
2137
+ activeModal,
2138
+ returnedStatus,
2139
+ coverage,
1630
2140
  });
1631
- const mayProbeLiveCodexWorkspaceNative = adapter.cliType === 'codex-cli'
1632
- && liveCurrentRuntimePtySafe
1633
- && !(typeof args?.providerSessionId === 'string' && args.providerSessionId.trim())
1634
- && !(providerSessionId && providerSessionId.trim())
1635
- && (!historyProviderSessionId || historyProviderSessionId === nativeHistorySessionId || historyProviderSessionId === historySessionId);
1636
- const liveWorkspaceNativeHistory = mayProbeLiveCodexWorkspaceNative
1637
- ? readLiveCodexWorkspaceNativeHistory(agentStr, {
1638
- canonicalHistory: provider?.canonicalHistory,
1639
- workspace,
1640
- offset: 0,
1641
- limit: nativeHistoryLimit,
1642
- excludeRecentCount: 0,
1643
- historyBehavior: provider?.historyBehavior,
1644
- scripts: provider?.scripts as any,
1645
- })
1646
- : null;
1647
- const liveWorkspaceNativeMessages = Array.isArray((liveWorkspaceNativeHistory as any)?.messages)
1648
- ? normalizeNativeHistoryMessages(agentStr, (liveWorkspaceNativeHistory as any).messages as ChatMessage[], (liveWorkspaceNativeHistory as any)?.providerSessionId)
1649
- : [];
1650
- const liveWorkspaceNativeProviderSessionId = typeof (liveWorkspaceNativeHistory as any)?.providerSessionId === 'string'
1651
- ? (liveWorkspaceNativeHistory as any).providerSessionId
1652
- : readHistorySessionIdFromMessages(liveWorkspaceNativeMessages);
1653
- const liveWorkspaceTranscriptWorkspace = typeof (liveWorkspaceNativeHistory as any)?.workspace === 'string'
1654
- ? (liveWorkspaceNativeHistory as any).workspace
1655
- : liveWorkspaceNativeMessages.map((message: any) => typeof message?.workspace === 'string' ? message.workspace.trim() : '').find(Boolean);
1656
- const liveWorkspaceNativeSafeMapping = liveWorkspaceNativeMessages.length > 0
1657
- && hasSafeNativeHistoryMapping({
1658
- workspace,
1659
- nativeMessages: liveWorkspaceNativeMessages,
1660
- ptyMessages: returnedMessages,
1661
- requireWorkspaceContentOverlap: true,
1662
- });
1663
- const liveWorkspaceNativeFreshEnough = liveWorkspaceNativeMessages.length > 0
1664
- && isNativeHistoryFreshEnough({
1665
- sourceMtimeMs: (liveWorkspaceNativeHistory as any)?.sourceMtimeMs,
1666
- nativeMessages: liveWorkspaceNativeMessages,
1667
- ptyMessages: returnedMessages,
1668
- });
1669
- const liveWorkspaceNativeUsable = (liveWorkspaceNativeHistory as any)?.source === 'provider-native'
1670
- && liveWorkspaceNativeMessages.length > 0
1671
- && (liveWorkspaceNativeHistory as any)?.nativeHistoryCoverage !== 'partial'
1672
- && (liveWorkspaceNativeHistory as any)?.nativeHistoryCoverage !== 'unavailable'
1673
- && liveWorkspaceNativeSafeMapping
1674
- && liveWorkspaceNativeFreshEnough;
1675
- if (liveWorkspaceNativeUsable) {
1676
- selectedMessages = finalizeStreamingMessagesWhenIdle(liveWorkspaceNativeMessages, returnedStatus);
1677
- selectedProviderSessionId = liveWorkspaceNativeProviderSessionId || providerSessionId;
1678
- selectedTranscriptAuthority = 'provider';
1679
- selectedCoverage = (liveWorkspaceNativeHistory as any).hasMore ? 'tail' : 'full';
1680
- messageSource = buildCliMessageSourceProvenance({
1681
- selected: 'native-history',
1682
- provider: adapter.cliType,
1683
- nativeHandle: selectedProviderSessionId || nativeHistorySessionId || historySessionId,
1684
- sessionWorkspace,
1685
- intendedWorkspace,
1686
- transcriptWorkspace: liveWorkspaceTranscriptWorkspace,
1687
- nativeSource: (liveWorkspaceNativeHistory as any).source,
1688
- sourcePath: (liveWorkspaceNativeHistory as any).sourcePath,
1689
- sourceMtimeMs: (liveWorkspaceNativeHistory as any).sourceMtimeMs,
1690
- nativeHistoryCoverage: (liveWorkspaceNativeHistory as any).nativeHistoryCoverage,
1691
- partialReason: (liveWorkspaceNativeHistory as any).partialReason,
1692
- unavailableReason: (liveWorkspaceNativeHistory as any).unavailableReason,
1693
- nativeMessages: liveWorkspaceNativeMessages,
1694
- ptyMessages: returnedMessages,
1695
- returnedMessages: selectedMessages,
1696
- safeMapping: true,
1697
- freshEnough: true,
1698
- ptyStatusApprovalOnly: true,
1699
- });
1700
- (messageSource as any).selectedDaemonSource = 'live-workspace-native-history';
1701
- (messageSource as any).runtimeMappingSafe = true;
1702
- } else {
1703
- const fallbackReason = buildNativeHistoryFallbackReason({
1704
- providerType,
1705
- provider,
1706
- nativeSource: (nativeHistory as any).source,
1707
- nativeHistoryCoverage,
1708
- unavailableReason,
1709
- nativeMessageCount: nativeMessages.length,
1710
- safeMapping,
1711
- freshEnough,
1712
- });
1713
- const unsafeNativeFallback = adapter.cliType === 'codex-cli'
1714
- && isUnsafeNativeTranscriptFallback(fallbackReason);
1715
- const safeCurrentRuntimePtyMessages = unsafeNativeFallback
1716
- && isCurrentRuntimePtySafelyAttributed({
1717
- adapter,
1718
- helpers: h,
1719
- readChatArgs: args,
1720
- sessionWorkspace,
1721
- intendedWorkspace,
1722
- ptyMessages: returnedMessages,
1723
- });
1724
- const safeRuntimeAckMessages = unsafeNativeFallback
1725
- && !safeCurrentRuntimePtyMessages
1726
- ? selectRuntimeInputAckMessages(returnedMessages)
1727
- : [];
1728
- const exactRuntimeMirrorMessages = unsafeNativeFallback
1729
- && !safeCurrentRuntimePtyMessages
1730
- && safeRuntimeAckMessages.length === 0
1731
- ? readExactRuntimeMirrorMessages({
1732
- providerType,
1733
- targetSessionId: typeof args?.targetSessionId === 'string' ? args.targetSessionId : undefined,
1734
- currentSessionId: typeof (h.currentSession as any)?.sessionId === 'string' ? (h.currentSession as any).sessionId : undefined,
1735
- tailLimit: nativeHistoryLimit,
1736
- historyBehavior: provider?.historyBehavior,
1737
- })
1738
- : [];
1739
- const safeDaemonMessages = safeRuntimeAckMessages.length > 0
1740
- ? safeRuntimeAckMessages
1741
- : exactRuntimeMirrorMessages;
1742
- if (unsafeNativeFallback) {
1743
- if (safeCurrentRuntimePtyMessages) {
1744
- selectedMessages = returnedMessages;
1745
- selectedTranscriptAuthority = 'daemon';
1746
- selectedCoverage = coverage || 'current-turn';
1747
- selectedStatus = returnedStatus;
1748
- } else {
1749
- selectedMessages = safeDaemonMessages;
1750
- selectedTranscriptAuthority = safeDaemonMessages.length > 0 ? 'daemon' : undefined;
1751
- selectedCoverage = safeDaemonMessages.length > 0 ? 'tail' : undefined;
1752
- selectedStatus = coerceUnsafeNativeFallbackStatus(returnedStatus, activeModal);
1753
- }
1754
- }
1755
- messageSource = buildCliMessageSourceProvenance({
1756
- selected: 'pty-parser',
1757
- provider: adapter.cliType,
1758
- nativeHandle: historyProviderSessionId || nativeHistorySessionId || historySessionId,
1759
- sessionWorkspace,
1760
- intendedWorkspace,
1761
- transcriptWorkspace,
1762
- fallbackReason,
1763
- nativeSource: (nativeHistory as any).source,
1764
- sourcePath: (nativeHistory as any).sourcePath,
1765
- sourceMtimeMs: (nativeHistory as any).sourceMtimeMs,
1766
- nativeHistoryCoverage,
1767
- partialReason,
1768
- unavailableReason,
1769
- nativeMessages,
1770
- ptyMessages: returnedMessages,
1771
- returnedMessages: unsafeNativeFallback && !safeCurrentRuntimePtyMessages ? safeDaemonMessages : returnedMessages,
1772
- safeMapping,
1773
- freshEnough,
1774
- ptyStatusApprovalOnly: unsafeNativeFallback && !safeCurrentRuntimePtyMessages,
1775
- });
1776
- if (safeCurrentRuntimePtyMessages) {
1777
- (messageSource as any).selectedDaemonSource = 'current-runtime-pty';
1778
- (messageSource as any).transcriptAuthority = 'daemon';
1779
- (messageSource as any).runtimeMappingSafe = true;
1780
- }
1781
- if (unsafeNativeFallback && exactRuntimeMirrorMessages.length > 0) {
1782
- (messageSource as any).selectedDaemonSource = 'exact-runtime-mirror';
1783
- (messageSource as any).transcriptAuthority = 'daemon';
1784
- }
1785
- }
1786
2141
  }
2142
+ } else {
2143
+ applyUnsafeNativeDaemonFallback({
2144
+ providerType,
2145
+ adapter,
2146
+ helpers: h,
2147
+ readChatArgs: args,
2148
+ sessionWorkspace,
2149
+ intendedWorkspace,
2150
+ ptyMessages: returnedMessages,
2151
+ nativeHistoryLimit,
2152
+ provider,
2153
+ messageSourceRef: { set(value) { messageSource = value; }, get() { return messageSource; } },
2154
+ apply(selection) {
2155
+ selectedMessages = selection.messages;
2156
+ selectedTranscriptAuthority = selection.transcriptAuthority;
2157
+ selectedCoverage = selection.coverage ?? coverage;
2158
+ selectedStatus = selection.status ?? returnedStatus;
2159
+ },
2160
+ activeModal,
2161
+ returnedStatus,
2162
+ coverage,
2163
+ });
1787
2164
  }
1788
2165
  }
1789
2166
  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}`);
@@ -1818,6 +2195,11 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
1818
2195
  ...(selectedCoverage ? { coverage: selectedCoverage } : {}),
1819
2196
  }, args);
1820
2197
  }
2198
+ // History-only path (no adapter). Same source-decision contract as
2199
+ // the adapter path above, but with no PTY messages — the machine
2200
+ // simply decides whether native is usable; if not we return the
2201
+ // history we have plus a `native_history_not_safely_available`
2202
+ // error response when the provider requires native source.
1821
2203
  const historyLimit = normalizeReadChatTailLimit(args);
1822
2204
  try {
1823
2205
  const agentStr = provider?.type || args?.agentType || getCurrentProviderType(h);
@@ -1825,13 +2207,9 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
1825
2207
  ? (h.currentSession as any).workspace
1826
2208
  : undefined;
1827
2209
  const intendedWorkspace = typeof args?.workspace === 'string' ? args.workspace : undefined;
1828
- const exactNativeHistoryScope = Boolean(
1829
- (typeof args?.targetSessionId === 'string' && args.targetSessionId.trim())
1830
- || (typeof args?.historySessionId === 'string' && args.historySessionId.trim())
1831
- || (typeof args?.providerSessionId === 'string' && args.providerSessionId.trim())
1832
- || ((h.currentSession as any)?.sessionId === args?.targetSessionId && typeof (h.currentSession as any)?.providerSessionId === 'string' && (h.currentSession as any).providerSessionId.trim())
1833
- );
1834
- const history = supportsCliNativeTranscript(agentStr, provider) && isNativeSourceCanonicalHistory(provider?.canonicalHistory)
2210
+ const supportsNative = supportsCliNativeTranscript(agentStr, provider)
2211
+ && isNativeSourceCanonicalHistory(provider?.canonicalHistory);
2212
+ const history = supportsNative
1835
2213
  ? readCliProviderNativeHistory(agentStr, {
1836
2214
  canonicalHistory: provider?.canonicalHistory,
1837
2215
  historySessionId,
@@ -1843,35 +2221,23 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
1843
2221
  scripts: provider?.scripts as any,
1844
2222
  })
1845
2223
  : readProviderChatHistory(agentStr, {
1846
- canonicalHistory: provider?.canonicalHistory,
1847
- historySessionId,
1848
- workspace,
1849
- offset: 0,
1850
- limit: historyLimit,
1851
- excludeRecentCount: 0,
1852
- historyBehavior: provider?.historyBehavior,
1853
- scripts: provider?.scripts as any,
1854
- });
1855
- const lookup = (history as any).lookup === 'workspace' ? 'workspace' : 'session';
2224
+ canonicalHistory: provider?.canonicalHistory,
2225
+ historySessionId,
2226
+ workspace,
2227
+ offset: 0,
2228
+ limit: historyLimit,
2229
+ excludeRecentCount: 0,
2230
+ historyBehavior: provider?.historyBehavior,
2231
+ scripts: provider?.scripts as any,
2232
+ });
2233
+ const lookup = (history as any)?.lookup === 'workspace' ? 'workspace' : 'session';
1856
2234
  const historyMessages = Array.isArray((history as any)?.messages)
1857
2235
  ? normalizeNativeHistoryMessages(agentStr, (history as any).messages as ChatMessage[], (history as any)?.providerSessionId)
1858
2236
  : [];
1859
2237
  const historyProviderSessionId = typeof (history as any)?.providerSessionId === 'string'
1860
2238
  ? (history as any).providerSessionId
1861
2239
  : readHistorySessionIdFromMessages(historyMessages) || historySessionId;
1862
- const nativeHistoryCoverage = typeof (history as any)?.nativeHistoryCoverage === 'string'
1863
- ? (history as any).nativeHistoryCoverage
1864
- : undefined;
1865
- const partialReason = typeof (history as any)?.partialReason === 'string'
1866
- ? (history as any).partialReason
1867
- : undefined;
1868
- const unavailableReason = typeof (history as any)?.unavailableReason === 'string'
1869
- ? (history as any).unavailableReason
1870
- : undefined;
1871
- const transcriptWorkspace = typeof (history as any)?.workspace === 'string'
1872
- ? (history as any).workspace
1873
- : historyMessages.map((message: any) => typeof message?.workspace === 'string' ? message.workspace.trim() : '').find(Boolean);
1874
- const safeMapping = supportsCliNativeTranscript(agentStr, provider)
2240
+ const safeMapping = supportsNative
1875
2241
  ? hasSafeNativeHistoryMapping({
1876
2242
  historySessionId: lookup === 'workspace' ? undefined : historySessionId,
1877
2243
  providerSessionId: lookup === 'workspace' ? undefined : historyProviderSessionId,
@@ -1879,60 +2245,41 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
1879
2245
  nativeMessages: historyMessages,
1880
2246
  })
1881
2247
  : false;
1882
- const nativeSelected = supportsCliNativeTranscript(agentStr, provider)
1883
- && (history as any).source === 'provider-native'
1884
- && historyMessages.length > 0
1885
- && nativeHistoryCoverage !== 'partial'
1886
- && nativeHistoryCoverage !== 'unavailable'
1887
- && safeMapping;
1888
- const messageSource = buildCliMessageSourceProvenance({
1889
- selected: nativeSelected ? 'native-history' : 'pty-parser',
1890
- provider: agentStr,
1891
- nativeHandle: historyProviderSessionId || historySessionId,
2248
+
2249
+ const machineSessionKey = String(
2250
+ args?.targetSessionId
2251
+ || historyProviderSessionId
2252
+ || historySessionId
2253
+ || (h.currentSession as any)?.sessionId
2254
+ || ''
2255
+ );
2256
+ const decision = decideCliReadChatSource({
2257
+ providerType: agentStr,
2258
+ provider,
2259
+ sessionId: machineSessionKey,
2260
+ nativeHistoryResult: history,
2261
+ safeMapping,
1892
2262
  sessionWorkspace: workspace,
1893
2263
  intendedWorkspace,
1894
- transcriptWorkspace,
1895
- fallbackReason: nativeSelected
1896
- ? undefined
1897
- : buildNativeHistoryFallbackReason({
1898
- providerType: agentStr,
1899
- provider,
1900
- nativeSource: (history as any).source,
1901
- nativeHistoryCoverage,
1902
- unavailableReason,
1903
- nativeMessageCount: historyMessages.length,
1904
- safeMapping,
1905
- freshEnough: true,
1906
- }),
1907
- nativeSource: (history as any).source,
1908
- sourcePath: (history as any).sourcePath,
1909
- sourceMtimeMs: (history as any).sourceMtimeMs,
1910
- nativeHistoryCoverage,
1911
- partialReason,
1912
- unavailableReason,
1913
- nativeMessages: historyMessages,
1914
- returnedMessages: historyMessages,
1915
- safeMapping,
1916
- freshEnough: true,
2264
+ ptyMessages: [],
1917
2265
  ptyStatusApprovalOnly: false,
1918
2266
  });
1919
- const requiresNativeSource = supportsCliNativeTranscript(agentStr, provider)
1920
- && isNativeSourceCanonicalHistory(provider?.canonicalHistory);
1921
- if (requiresNativeSource && !nativeSelected) {
2267
+
2268
+ if (supportsNative && !decision.nativeSelected) {
1922
2269
  return {
1923
2270
  success: false,
1924
2271
  code: 'native_history_not_safely_available',
1925
2272
  error: 'Provider-native history was not safely available for the requested CLI session.',
1926
2273
  providerSessionId: historyProviderSessionId,
1927
- messageSource,
1928
- transcriptProvenance: messageSource,
2274
+ messageSource: decision.messageSource,
2275
+ transcriptProvenance: decision.messageSource,
1929
2276
  };
1930
2277
  }
1931
2278
  return buildReadChatCommandResult({
1932
2279
  messages: historyMessages,
1933
2280
  status: 'idle',
1934
- messageSource,
1935
- transcriptProvenance: messageSource,
2281
+ messageSource: decision.messageSource,
2282
+ transcriptProvenance: decision.messageSource,
1936
2283
  ...(typeof (history as any)?.title === 'string' ? { title: (history as any).title } : {}),
1937
2284
  ...(historyProviderSessionId ? { providerSessionId: historyProviderSessionId } : {}),
1938
2285
  ...(((provider?.historyBehavior as any)?.transcriptAuthority === 'provider' || (provider?.historyBehavior as any)?.transcriptAuthority === 'daemon')