@adhdev/daemon-core 0.9.82-rc.14 → 0.9.82-rc.140

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 (115) 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/chat/subscription-updates.d.ts +1 -0
  4. package/dist/cli-adapter-types.d.ts +5 -1
  5. package/dist/cli-adapters/cli-script-runner.d.ts +45 -0
  6. package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
  7. package/dist/cli-adapters/provider-cli-adapter.d.ts +87 -63
  8. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  9. package/dist/cli-adapters/provider-cli-shared.d.ts +21 -0
  10. package/dist/commands/router.d.ts +22 -0
  11. package/dist/config/chat-history.d.ts +5 -0
  12. package/dist/config/config.d.ts +5 -0
  13. package/dist/config/mesh-config.d.ts +68 -1
  14. package/dist/git/git-commands.d.ts +5 -1
  15. package/dist/index.d.ts +18 -6
  16. package/dist/index.js +10431 -2827
  17. package/dist/index.js.map +1 -1
  18. package/dist/index.mjs +10376 -2811
  19. package/dist/index.mjs.map +1 -1
  20. package/dist/installer.d.ts +1 -4
  21. package/dist/launch.d.ts +1 -1
  22. package/dist/logging/async-batch-writer.d.ts +10 -0
  23. package/dist/mesh/beads-db.d.ts +72 -0
  24. package/dist/mesh/contracts.d.ts +164 -0
  25. package/dist/mesh/coordinator-registry.d.ts +25 -0
  26. package/dist/mesh/mesh-active-work.d.ts +90 -0
  27. package/dist/mesh/mesh-events.d.ts +77 -5
  28. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  29. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  30. package/dist/mesh/mesh-ledger.d.ts +58 -1
  31. package/dist/mesh/mesh-refine-status.d.ts +26 -0
  32. package/dist/mesh/mesh-work-queue.d.ts +44 -5
  33. package/dist/mesh/preview-freshness.d.ts +18 -0
  34. package/dist/mesh/refine-config.d.ts +193 -0
  35. package/dist/mesh/worktree-bootstrap-config.d.ts +113 -0
  36. package/dist/providers/approval-utils.d.ts +9 -0
  37. package/dist/providers/chat-message-normalization.d.ts +1 -0
  38. package/dist/providers/cli-provider-instance.d.ts +6 -1
  39. package/dist/providers/contracts.d.ts +19 -0
  40. package/dist/providers/read-chat-contract.d.ts +29 -0
  41. package/dist/providers/transcript-v2.d.ts +176 -0
  42. package/dist/repo-mesh-types.d.ts +67 -0
  43. package/dist/shared-types.d.ts +12 -0
  44. package/dist/status/reporter.d.ts +2 -0
  45. package/dist/status/snapshot.d.ts +1 -0
  46. package/dist/types.d.ts +5 -0
  47. package/package.json +3 -1
  48. package/src/boot/daemon-lifecycle.ts +3 -0
  49. package/src/chat/source-machine.ts +534 -0
  50. package/src/chat/source-resolver.ts +0 -0
  51. package/src/chat/subscription-updates.ts +14 -1
  52. package/src/cli-adapter-types.d.ts +1 -0
  53. package/src/cli-adapter-types.ts +3 -1
  54. package/src/cli-adapters/cli-script-runner.ts +145 -0
  55. package/src/cli-adapters/cli-state-engine.ts +1083 -0
  56. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  57. package/src/cli-adapters/provider-cli-adapter.ts +630 -1137
  58. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  59. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  60. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  61. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  62. package/src/cli-adapters/provider-cli-shared.ts +51 -11
  63. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
  64. package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
  65. package/src/commands/chat-commands.ts +1428 -50
  66. package/src/commands/cli-manager.ts +145 -3
  67. package/src/commands/handler.ts +8 -1
  68. package/src/commands/mesh-coordinator.ts +13 -143
  69. package/src/commands/router.ts +3271 -427
  70. package/src/config/chat-history.ts +79 -24
  71. package/src/config/config.ts +12 -0
  72. package/src/config/mesh-config.ts +249 -2
  73. package/src/config/recent-activity.ts +8 -2
  74. package/src/daemon/dev-cli-debug.ts +10 -1
  75. package/src/detection/ide-detector.ts +26 -16
  76. package/src/git/git-commands.ts +17 -5
  77. package/src/git/git-worktree.ts +8 -1
  78. package/src/index.ts +45 -5
  79. package/src/installer.d.ts +1 -1
  80. package/src/installer.ts +8 -6
  81. package/src/launch.d.ts +1 -1
  82. package/src/launch.ts +37 -28
  83. package/src/logging/async-batch-writer.ts +55 -0
  84. package/src/logging/logger.ts +2 -1
  85. package/src/mesh/beads-db.ts +479 -0
  86. package/src/mesh/contracts.ts +329 -0
  87. package/src/mesh/coordinator-prompt.ts +40 -22
  88. package/src/mesh/coordinator-registry.ts +75 -0
  89. package/src/mesh/mesh-active-work.ts +437 -0
  90. package/src/mesh/mesh-events.ts +799 -56
  91. package/src/mesh/mesh-fast-forward.ts +430 -0
  92. package/src/mesh/mesh-host-ownership.ts +73 -0
  93. package/src/mesh/mesh-ledger.ts +457 -104
  94. package/src/mesh/mesh-refine-status.ts +144 -0
  95. package/src/mesh/mesh-work-queue.ts +216 -158
  96. package/src/mesh/preview-freshness.ts +118 -0
  97. package/src/mesh/refine-config.ts +366 -0
  98. package/src/mesh/worktree-bootstrap-config.ts +247 -0
  99. package/src/providers/approval-utils.ts +39 -5
  100. package/src/providers/chat-message-normalization.ts +7 -12
  101. package/src/providers/cli-provider-instance.ts +362 -41
  102. package/src/providers/contracts.ts +19 -0
  103. package/src/providers/ide-provider-instance.ts +17 -3
  104. package/src/providers/provider-loader.ts +31 -11
  105. package/src/providers/provider-schema.ts +12 -0
  106. package/src/providers/read-chat-contract.ts +76 -16
  107. package/src/providers/transcript-v2.ts +567 -0
  108. package/src/providers/version-archive.ts +38 -20
  109. package/src/repo-mesh-types.ts +77 -0
  110. package/src/shared-types.ts +9 -0
  111. package/src/status/builders.ts +23 -6
  112. package/src/status/reporter.ts +15 -0
  113. package/src/status/snapshot.ts +35 -11
  114. package/src/system/host-memory.ts +29 -12
  115. package/src/types.ts +5 -0
@@ -21,7 +21,7 @@ import { ChatHistoryWriter, isNativeSourceCanonicalHistory, materializeProviderN
21
21
  import { LOG } from '../logging/logger.js';
22
22
  import type { ChatMessage } from '../types.js';
23
23
  import { buildPersistedProviderEffectMessage, normalizeProviderEffects } from './control-effects.js';
24
- import { formatAutoApprovalMessage, pickApprovalButton } from './approval-utils.js';
24
+ import { formatAutoApprovalMessage, pickApprovalButton, looksLikeActiveApprovalPromptText } from './approval-utils.js';
25
25
  import { getCliScriptCommand, parseCliScriptResult } from './cli-script-results.js';
26
26
  import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
27
27
  import { normalizeProviderSessionId } from './provider-session-id.js';
@@ -43,6 +43,24 @@ type CompletedDebouncePending = {
43
43
  loggedBlockReason?: string;
44
44
  };
45
45
 
46
+ function isIdleStatus(value: unknown): boolean {
47
+ const status = typeof value === 'string' ? value.trim().toLowerCase() : '';
48
+ return !status || status === 'idle' || status === 'ready';
49
+ }
50
+
51
+
52
+ function getMessageTime(message: unknown): number {
53
+ if (!message || typeof message !== 'object') return 0;
54
+ const record = message as { receivedAt?: unknown; timestamp?: unknown };
55
+ const value = Number(record.receivedAt ?? record.timestamp ?? 0);
56
+ return Number.isFinite(value) ? value : 0;
57
+ }
58
+
59
+ type CompletedFinalizationBlock = {
60
+ reason: string;
61
+ terminal?: boolean;
62
+ };
63
+
46
64
  const COMPLETED_FINALIZATION_RETRY_MS = 1000;
47
65
  const COMPLETED_FINALIZATION_MAX_WAIT_MS = 30_000;
48
66
 
@@ -312,6 +330,7 @@ export class CliProviderInstance implements ProviderInstance {
312
330
  private lastApprovalEventAt = 0;
313
331
  private autoApproveBusy = false;
314
332
  private autoApproveBusyTimer: NodeJS.Timeout | null = null;
333
+ private lastAutoApprovalSignature = '';
315
334
  private controlValues: Record<string, string | number | boolean> = {};
316
335
  private summaryMetadata: unknown = undefined;
317
336
  private appliedEffectKeys = new Set<string>();
@@ -371,6 +390,9 @@ export class CliProviderInstance implements ProviderInstance {
371
390
  this.launchMode = options?.launchMode || 'new';
372
391
  this.onProviderSessionResolved = options?.onProviderSessionResolved;
373
392
  this.adapter = new ProviderCliAdapter(provider as CliProviderModule, workingDir, cliArgs, options?.extraEnv || {}, transportFactory);
393
+ if (this.providerSessionId) {
394
+ this.adapter.updateRuntimeMeta({ providerSessionId: this.providerSessionId });
395
+ }
374
396
  this.monitor = new StatusMonitor();
375
397
  this.historyWriter = new ChatHistoryWriter();
376
398
  }
@@ -412,7 +434,7 @@ export class CliProviderInstance implements ProviderInstance {
412
434
  await this.adapter.spawn();
413
435
  await this.enforceFreshSessionLaunchIfNeeded();
414
436
  this.maybeAppendRuntimeRecoveryMessage(this.adapter.getRuntimeMetadata());
415
- if (this.providerSessionId) {
437
+ if (this.providerSessionId && this.shouldHydrateExistingProviderHistory()) {
416
438
  this.restorePersistedHistoryFromCurrentSession();
417
439
  }
418
440
  if (this.providerSessionId && this.launchMode === 'resume') {
@@ -492,8 +514,14 @@ export class CliProviderInstance implements ProviderInstance {
492
514
  if (typeof this.adapter.getScriptParsedStatus === 'function') {
493
515
  try {
494
516
  parsedStatus = this.adapter.getScriptParsedStatus() || null;
495
- this.errorMessage = undefined;
496
- this.errorReason = undefined;
517
+ const parsedErrorMessage = typeof parsedStatus?.errorMessage === 'string' && parsedStatus.errorMessage.trim()
518
+ ? parsedStatus.errorMessage.trim()
519
+ : undefined;
520
+ const parsedErrorReason = typeof parsedStatus?.errorReason === 'string' && parsedStatus.errorReason.trim()
521
+ ? parsedStatus.errorReason.trim() as ProviderErrorReason
522
+ : undefined;
523
+ this.errorMessage = parsedErrorMessage;
524
+ this.errorReason = parsedErrorReason;
497
525
  } catch (error: any) {
498
526
  parseErrorMessage = error?.message || String(error);
499
527
  this.errorMessage = parseErrorMessage;
@@ -503,22 +531,39 @@ export class CliProviderInstance implements ProviderInstance {
503
531
  this.errorMessage = undefined;
504
532
  this.errorReason = undefined;
505
533
  }
534
+ const adapterProviderSessionId = normalizeProviderSessionId(
535
+ this.provider,
536
+ typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
537
+ );
506
538
  const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, Date.now());
507
- const visibleStatus = parseErrorMessage
539
+ const visibleStatus = parseErrorMessage || parsedStatus?.status === 'error'
508
540
  ? 'error'
509
541
  : (autoApproveActive ? 'generating' : adapterStatus.status);
542
+ const runtime = this.adapter.getRuntimeMetadata();
543
+ this.maybeAppendRuntimeRecoveryMessage(runtime);
544
+ let parsedMessages = Array.isArray(parsedStatus?.messages)
545
+ ? parsedStatus.messages
546
+ : [];
510
547
  const parsedProviderSessionId = normalizeProviderSessionId(
511
548
  this.provider,
512
549
  typeof parsedStatus?.providerSessionId === 'string' ? parsedStatus.providerSessionId : '',
513
550
  );
514
- if (parsedProviderSessionId) {
551
+ const suppressFreshLaunchStartupReplay = this.shouldSuppressFreshLaunchStartupReplay(
552
+ parsedMessages,
553
+ parsedStatus,
554
+ adapterStatus,
555
+ parsedProviderSessionId,
556
+ );
557
+ if (adapterProviderSessionId && !suppressFreshLaunchStartupReplay) {
558
+ this.promoteProviderSessionId(adapterProviderSessionId);
559
+ }
560
+ if (parsedProviderSessionId && !suppressFreshLaunchStartupReplay) {
515
561
  this.promoteProviderSessionId(parsedProviderSessionId);
516
562
  }
517
- const runtime = this.adapter.getRuntimeMetadata();
518
- this.maybeAppendRuntimeRecoveryMessage(runtime);
519
- let parsedMessages = Array.isArray(parsedStatus?.messages)
520
- ? parsedStatus.messages
521
- : [];
563
+ if (suppressFreshLaunchStartupReplay) {
564
+ parsedMessages = [];
565
+ }
566
+ const activeChatId = this.providerSessionId || runtime?.runtimeId || this.instanceId;
522
567
  const historyMessageCount = Number.isFinite(parsedStatus?.historyMessageCount)
523
568
  ? Math.max(0, Number(parsedStatus.historyMessageCount))
524
569
  : null;
@@ -528,7 +573,18 @@ export class CliProviderInstance implements ProviderInstance {
528
573
  : [];
529
574
  }
530
575
  const mergedMessages = this.mergeConversationMessages(parsedMessages);
531
- const canonicalBackedHistory = this.syncCanonicalSavedHistoryIfNeeded();
576
+ const canonicalBackedHistory = this.shouldHydrateExistingProviderHistory()
577
+ ? this.syncCanonicalSavedHistoryIfNeeded()
578
+ : false;
579
+ const statusMessages = canonicalBackedHistory && this.lastPersistedHistoryMessages.length > 0
580
+ ? this.lastPersistedHistoryMessages.map((message) => ({
581
+ role: message.role,
582
+ content: message.content,
583
+ kind: message.kind,
584
+ senderName: message.senderName,
585
+ receivedAt: message.receivedAt,
586
+ }))
587
+ : mergedMessages;
532
588
 
533
589
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
534
590
  const parsedChatStatus = typeof parsedStatus?.status === 'string' && parsedStatus.status.trim()
@@ -572,7 +628,12 @@ export class CliProviderInstance implements ProviderInstance {
572
628
  }
573
629
  }
574
630
 
575
- this.applyProviderResponse(parsedStatus, { phase: 'immediate' });
631
+ this.applyProviderResponse(
632
+ suppressFreshLaunchStartupReplay && parsedStatus && typeof parsedStatus === 'object'
633
+ ? { ...parsedStatus, providerSessionId: undefined }
634
+ : parsedStatus,
635
+ { phase: 'immediate' },
636
+ );
576
637
  const surface = resolveProviderStateSurface({
577
638
  summaryMetadata: this.summaryMetadata as any,
578
639
  controlValues: this.controlValues,
@@ -592,10 +653,10 @@ export class CliProviderInstance implements ProviderInstance {
592
653
  status: visibleStatus,
593
654
  mode: this.presentationMode,
594
655
  activeChat: {
595
- id: `${this.type}_${this.workingDir}`,
656
+ id: activeChatId,
596
657
  title: parsedStatus?.title || dirName,
597
658
  status: activeChatStatus,
598
- messages: mergedMessages,
659
+ messages: statusMessages,
599
660
  activeModal: autoApproveActive ? null : (parsedStatus?.activeModal ?? adapterStatus.activeModal),
600
661
  inputContent: '',
601
662
  },
@@ -695,6 +756,34 @@ export class CliProviderInstance implements ProviderInstance {
695
756
  }
696
757
  }
697
758
 
759
+ recordAcknowledgedUserInput(input: InputEnvelope | string): void {
760
+ const content = typeof input === 'string'
761
+ ? input.trim()
762
+ : buildCliStructuredInputPrompt(input).trim();
763
+ if (!content) return;
764
+
765
+ const receivedAt = Date.now();
766
+ const dedupKey = `user_input_ack:${crypto
767
+ .createHash('sha256')
768
+ .update(`${this.instanceId}:${content}:${receivedAt}`)
769
+ .digest('hex')
770
+ .slice(0, 24)}`;
771
+ this.appendRuntimeMessage(buildChatMessage({
772
+ role: 'user',
773
+ senderName: 'User',
774
+ kind: 'standard',
775
+ content,
776
+ receivedAt,
777
+ timestamp: receivedAt,
778
+ source: 'runtime_input_ack',
779
+ meta: {
780
+ runtimeInputAck: true,
781
+ provider: this.type,
782
+ workspace: this.workingDir,
783
+ },
784
+ } as ChatMessage), dedupKey);
785
+ }
786
+
698
787
  dispose(): void {
699
788
  this.adapter.shutdown();
700
789
  this.monitor.reset();
@@ -740,7 +829,60 @@ export class CliProviderInstance implements ProviderInstance {
740
829
  const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
741
830
  const role = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : '';
742
831
  const content = lastVisible ? flattenContent(lastVisible.content).trim() : '';
743
- return role === 'assistant' && !!content;
832
+ if (role !== 'assistant' || !content) return false;
833
+ // Guard: if the last assistant message looks like an active approval/input prompt,
834
+ // it is not a real completion — the session is still awaiting user input.
835
+ if (looksLikeActiveApprovalPromptText(content)) return false;
836
+ return true;
837
+ }
838
+
839
+ private buildCompletedFinalizationDiagnostic(args: {
840
+ blockReason: string;
841
+ latestStatus?: any;
842
+ latestVisibleStatus: string;
843
+ waitedMs: number;
844
+ pending: CompletedDebouncePending;
845
+ emittedAfterFinalizationTimeout: boolean;
846
+ }): Record<string, unknown> {
847
+ let parsed: any = null;
848
+ let parseError: string | undefined;
849
+ try {
850
+ parsed = this.adapter.getScriptParsedStatus();
851
+ } catch (error: any) {
852
+ parseError = error?.message || String(error);
853
+ }
854
+
855
+ const visibleMessages = (Array.isArray(parsed?.messages) ? parsed.messages : [])
856
+ .filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
857
+ const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
858
+ const lastVisibleRole = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null;
859
+ const lastVisibleKind = typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null;
860
+ const lastVisibleContentLength = lastVisible ? flattenContent(lastVisible.content).trim().length : 0;
861
+
862
+ return {
863
+ providerType: this.type,
864
+ sessionId: this.instanceId,
865
+ providerSessionId: this.providerSessionId || null,
866
+ workspace: this.workingDir,
867
+ blockReason: args.blockReason,
868
+ emittedAfterFinalizationTimeout: args.emittedAfterFinalizationTimeout,
869
+ waitedMs: args.waitedMs,
870
+ maxWaitMs: COMPLETED_FINALIZATION_MAX_WAIT_MS,
871
+ adapterStatus: typeof args.latestStatus?.status === 'string' ? args.latestStatus.status : null,
872
+ latestVisibleStatus: args.latestVisibleStatus,
873
+ parsedStatus: typeof parsed?.status === 'string' ? parsed.status : (parseError ? 'parse_error' : 'unknown'),
874
+ parseError: parseError || undefined,
875
+ finalAssistantPresent: this.completionHasFinalAssistantMessage(parsed?.messages),
876
+ visibleMessageCount: visibleMessages.length,
877
+ lastVisibleRole,
878
+ lastVisibleKind,
879
+ lastVisibleContentLength,
880
+ pendingStartedAt: this.generatingStartedAt || null,
881
+ pendingFirstObservedAt: args.pending.firstObservedAt,
882
+ pendingTimestamp: args.pending.timestamp,
883
+ pendingDurationSec: args.pending.duration,
884
+ previousBlockReason: args.pending.loggedBlockReason || null,
885
+ };
744
886
  }
745
887
 
746
888
  private hasAdapterPendingResponse(): boolean {
@@ -765,32 +907,62 @@ export class CliProviderInstance implements ProviderInstance {
765
907
  if (!isCliGeneratingLikeStatus(parsedRawStatus)) return false;
766
908
  if (adapterRawStatus !== 'idle') return false;
767
909
  if (hasNonEmptyCliModalButtons(parsedStatus?.activeModal ?? parsedStatus?.modal)) return false;
768
- return !this.hasAdapterPendingResponse();
910
+ if (this.hasAdapterPendingResponse()) return false;
911
+ // Do not suppress when the adapter's raw response buffer is still non-empty.
912
+ // This catches the case where isWaitingForResponse has already flipped to false
913
+ // (so getPartialResponse() returns '') but the provider's native parser still
914
+ // reports generating because it's parsing buffered content. Suppressing the
915
+ // finalization block here would emit a false completion event while the provider
916
+ // session is still actively processing its response stream.
917
+ const adapterAny = this.adapter as any;
918
+ if (typeof adapterAny?.responseBuffer === 'string' && adapterAny.responseBuffer.trim()) return false;
919
+ return true;
769
920
  }
770
921
 
771
- private getCompletedFinalizationBlockReason(latestVisibleStatus: string): string | null {
772
- if (latestVisibleStatus !== 'idle') return `status:${latestVisibleStatus}`;
922
+ private getCompletedFinalizationBlock(latestVisibleStatus: string): CompletedFinalizationBlock | null {
923
+ if (latestVisibleStatus !== 'idle') return { reason: `status:${latestVisibleStatus}`, terminal: true };
773
924
 
774
925
  const adapterAny = this.adapter as any;
775
- if (adapterAny?.isWaitingForResponse === true) return 'adapter_waiting_for_response';
776
- if (adapterAny?.currentTurnScope) return 'adapter_turn_scope_active';
926
+ if (adapterAny?.isWaitingForResponse === true) return { reason: 'adapter_waiting_for_response', terminal: true };
927
+ if (adapterAny?.currentTurnScope) return { reason: 'adapter_turn_scope_active', terminal: true };
928
+ if (this.hasAdapterPendingResponse()) return { reason: 'adapter_pending_response', terminal: true };
777
929
 
778
930
  const partial = typeof this.adapter.getPartialResponse === 'function'
779
931
  ? this.adapter.getPartialResponse()
780
932
  : '';
781
- if (typeof partial === 'string' && partial.trim()) return 'partial_response_pending';
933
+ if (typeof partial === 'string' && partial.trim()) return { reason: 'partial_response_pending', terminal: true };
782
934
 
783
935
  let parsed: any;
784
936
  try {
785
937
  parsed = this.adapter.getScriptParsedStatus();
786
938
  } catch (error: any) {
787
- return `parse_error:${error?.message || String(error)}`;
939
+ return { reason: `parse_error:${error?.message || String(error)}` };
788
940
  }
789
941
 
790
942
  const parsedStatus = typeof parsed?.status === 'string' ? parsed.status : 'unknown';
791
- if (parsedStatus !== 'idle') return `parsed_status:${parsedStatus}`;
792
- if (parsed?.activeModal || parsed?.modal) return 'parsed_modal_active';
793
- if (!this.completionHasFinalAssistantMessage(parsed?.messages)) return 'missing_final_assistant';
943
+ if (parsedStatus !== 'idle') {
944
+ const adapterStatus = this.adapter.getStatus({ allowParse: false });
945
+ if (this.shouldSuppressStaleParsedBusyStatus(parsed, adapterStatus)) return null;
946
+ return { reason: `parsed_status:${parsedStatus}`, terminal: isCliGeneratingLikeStatus(parsedStatus) };
947
+ }
948
+ if (parsed?.activeModal || parsed?.modal) return { reason: 'parsed_modal_active', terminal: true };
949
+ if (!this.completionHasFinalAssistantMessage(parsed?.messages)) return { reason: 'missing_final_assistant' };
950
+
951
+ // Guard: if the screen still shows an approval/choice prompt as the last visible text,
952
+ // the turn is not complete even if the parsed status says idle and there is an assistant
953
+ // message. This catches the case where waiting_approval→idle transitions occur before
954
+ // the modal has been resolved (e.g. the PTY rendered the prompt but no button press fired).
955
+ try {
956
+ const screenText = typeof (this.adapter as any).getScreenText === 'function'
957
+ ? String((this.adapter as any).getScreenText() || '')
958
+ : '';
959
+ if (screenText) {
960
+ const tailLines = screenText.split(/\r?\n/).slice(-16).join('\n');
961
+ if (looksLikeActiveApprovalPromptText(tailLines)) {
962
+ return { reason: 'screen_shows_approval_prompt', terminal: false };
963
+ }
964
+ }
965
+ } catch { /* defensive: screen text read is best-effort */ }
794
966
 
795
967
  return null;
796
968
  }
@@ -817,10 +989,11 @@ export class CliProviderInstance implements ProviderInstance {
817
989
  return;
818
990
  }
819
991
 
820
- const blockReason = this.getCompletedFinalizationBlockReason(latestVisibleStatus);
821
- if (blockReason) {
992
+ const block = this.getCompletedFinalizationBlock(latestVisibleStatus);
993
+ if (block) {
994
+ const blockReason = block.reason;
822
995
  const waitedMs = Date.now() - pending.firstObservedAt;
823
- if (waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
996
+ if (block.terminal || waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
824
997
  if (pending.loggedBlockReason !== blockReason) {
825
998
  LOG.info('CLI', `[${this.type}] waiting to emit completed until transcript finalizes (${blockReason})`);
826
999
  pending.loggedBlockReason = blockReason;
@@ -828,7 +1001,25 @@ export class CliProviderInstance implements ProviderInstance {
828
1001
  this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
829
1002
  return;
830
1003
  }
831
- LOG.warn('CLI', `[${this.type}] suppressed completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
1004
+ const completionDiagnostic = this.buildCompletedFinalizationDiagnostic({
1005
+ blockReason,
1006
+ latestStatus,
1007
+ latestVisibleStatus,
1008
+ waitedMs,
1009
+ pending,
1010
+ emittedAfterFinalizationTimeout: true,
1011
+ });
1012
+ LOG.warn('CLI', `[${this.type}] emitting completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
1013
+ this.pushEvent({
1014
+ event: 'agent:generating_completed',
1015
+ chatTitle: pending.chatTitle,
1016
+ duration: pending.duration,
1017
+ timestamp: pending.timestamp,
1018
+ finalSummary: blockReason.startsWith('parsed_status:')
1019
+ ? ''
1020
+ : extractFinalSummaryFromMessages(this.adapter?.getScriptParsedStatus()?.messages),
1021
+ completionDiagnostic,
1022
+ });
832
1023
  this.completedDebouncePending = null;
833
1024
  this.completedDebounceTimer = null;
834
1025
  this.generatingStartedAt = 0;
@@ -853,15 +1044,46 @@ export class CliProviderInstance implements ProviderInstance {
853
1044
  // Guard re-entry: onStatusChange/getState can observe the same modal multiple
854
1045
  // times while the PTY absorbs the approval key. Without this flag, repeated
855
1046
  // snapshots would write stray keys into the input once the modal dismisses.
856
- if (autoApproveActive && !this.autoApproveBusy) {
1047
+ // However, Claude Code can present a second approval immediately after the
1048
+ // first. Resolve a changed modal signature even while the previous write is
1049
+ // still inside the short busy window.
1050
+ if (!autoApproveActive) {
1051
+ this.lastAutoApprovalSignature = '';
1052
+ return autoApproveActive;
1053
+ }
1054
+ const modal = adapterStatus.activeModal;
1055
+ // (fix) Do not auto-approve when no concrete modal/buttons are present.
1056
+ // Claude TUI flaps between paints; without this guard adapterStatus
1057
+ // could report status=waiting_approval with activeModal=null (or with
1058
+ // an empty buttons array briefly) and we'd still call
1059
+ // resolveModal(-1) — which used to type "1" into the prompt
1060
+ // repeatedly. Skip until a real modal is captured.
1061
+ const buttons = Array.isArray(modal?.buttons)
1062
+ ? modal.buttons.map((b: any) => String(b || '').trim()).filter(Boolean)
1063
+ : [];
1064
+ if (!modal || buttons.length === 0) {
1065
+ return autoApproveActive;
1066
+ }
1067
+ const { index: buttonIndex, label: buttonLabel } = pickApprovalButton(buttons, this.provider);
1068
+ if (buttonIndex < 0) {
1069
+ // No positive button matched — don't pick a random index, just
1070
+ // surface the modal so the user can decide.
1071
+ return autoApproveActive;
1072
+ }
1073
+ const signature = [
1074
+ typeof modal?.message === 'string' ? modal.message.trim() : '',
1075
+ buttons.join('|'),
1076
+ buttonIndex,
1077
+ ].join('::');
1078
+ if (!this.autoApproveBusy || signature !== this.lastAutoApprovalSignature) {
857
1079
  this.autoApproveBusy = true;
1080
+ this.lastAutoApprovalSignature = signature;
858
1081
  if (this.autoApproveBusyTimer) clearTimeout(this.autoApproveBusyTimer);
859
1082
  this.autoApproveBusyTimer = setTimeout(() => {
860
1083
  this.autoApproveBusy = false;
861
1084
  this.autoApproveBusyTimer = null;
1085
+ this.lastAutoApprovalSignature = '';
862
1086
  }, 2000);
863
- const modal = adapterStatus.activeModal;
864
- const { index: buttonIndex, label: buttonLabel } = pickApprovalButton(modal?.buttons, this.provider);
865
1087
  this.recordAutoApproval(modal?.message, buttonLabel, now);
866
1088
  setTimeout(() => {
867
1089
  this.adapter.resolveModal(buttonIndex);
@@ -876,6 +1098,13 @@ export class CliProviderInstance implements ProviderInstance {
876
1098
  // during long-running CLI sessions. Keep this path on adapter-owned light
877
1099
  // state only; rich provider parsing is reserved for getState/read_chat.
878
1100
  const adapterStatus = this.adapter.getStatus({ allowParse: false });
1101
+ const adapterProviderSessionId = normalizeProviderSessionId(
1102
+ this.provider,
1103
+ typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
1104
+ );
1105
+ if (adapterProviderSessionId) {
1106
+ this.promoteProviderSessionId(adapterProviderSessionId);
1107
+ }
879
1108
  const parsedStatus = null;
880
1109
  const rawStatus = adapterStatus.status;
881
1110
  const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, now);
@@ -943,12 +1172,30 @@ export class CliProviderInstance implements ProviderInstance {
943
1172
  }
944
1173
  } else if (newStatus === 'idle' && (this.lastStatus === 'generating' || this.lastStatus === 'waiting_approval')) {
945
1174
  const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : 0;
946
- // If debounce still pending (generating lasted < 1s), cancel both events
1175
+ // If debounce still pending (generating lasted < 1s), cancel both UI events.
1176
+ // Still emit agent:generating_completed so mesh orchestration can record
1177
+ // task_completed for direct dispatches that complete faster than the debounce.
947
1178
  if (this.generatingDebouncePending) {
948
- LOG.info('CLI', `[${this.type}] suppressed short generating (${now - this.generatingStartedAt}ms)`);
1179
+ const shortDurationMs = this.generatingStartedAt ? now - this.generatingStartedAt : 0;
1180
+ LOG.info('CLI', `[${this.type}] suppressed short generating (${shortDurationMs}ms)`);
949
1181
  if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
950
1182
  this.generatingDebouncePending = null;
951
1183
  this.generatingStartedAt = 0;
1184
+ // Emit completion for mesh task association even though the UI generating
1185
+ // started/completed pair is suppressed (too short for visible UI update).
1186
+ let shortFinalSummary: string | undefined;
1187
+ try { shortFinalSummary = extractFinalSummaryFromMessages(this.adapter?.getScriptParsedStatus()?.messages); } catch { /* best-effort */ }
1188
+ this.pushEvent({
1189
+ event: 'agent:generating_completed',
1190
+ chatTitle,
1191
+ duration: 0,
1192
+ timestamp: now,
1193
+ finalSummary: shortFinalSummary,
1194
+ completionDiagnostic: {
1195
+ reason: 'short_generating_suppressed',
1196
+ shortDurationMs,
1197
+ },
1198
+ });
952
1199
  } else {
953
1200
  // Debounce completed, then require the rich transcript path that read_chat
954
1201
  // uses to show an idle turn whose last user-facing message is assistant.
@@ -957,6 +1204,23 @@ export class CliProviderInstance implements ProviderInstance {
957
1204
  }
958
1205
  } else if (newStatus === 'idle' && this.lastStatus === 'starting') {
959
1206
  this.pushEvent({ event: 'agent:ready', chatTitle, timestamp: now });
1207
+ } else if (newStatus === 'error') {
1208
+ if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
1209
+ this.generatingDebouncePending = null;
1210
+ if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
1211
+ this.completedDebouncePending = null;
1212
+ this.errorMessage = adapterStatus.errorMessage || this.errorMessage;
1213
+ this.errorReason = (adapterStatus.errorReason as ProviderErrorReason) || this.errorReason;
1214
+ this.pushEvent({
1215
+ event: 'agent:stopped',
1216
+ chatTitle,
1217
+ timestamp: now,
1218
+ finalSummary: adapterStatus.errorMessage || adapterStatus.errorReason || 'Provider reported an error',
1219
+ completionDiagnostic: {
1220
+ reason: adapterStatus.errorReason || 'provider_error',
1221
+ errorMessage: adapterStatus.errorMessage || undefined,
1222
+ },
1223
+ });
960
1224
  } else if (newStatus === 'stopped') {
961
1225
  // Cancel any pending debounce
962
1226
  if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
@@ -977,7 +1241,31 @@ export class CliProviderInstance implements ProviderInstance {
977
1241
  // Monitor check (cooldown based notification, IDE/CLI common)
978
1242
  const agentKey = `${this.type}:cli`;
979
1243
  const monitorEvents = this.monitor.check(agentKey, newStatus, now, progressFingerprint);
1244
+ const monitorParsedStatus: any = parsedStatus;
980
1245
  for (const me of monitorEvents) {
1246
+ if (
1247
+ me.type === 'monitor:long_generating'
1248
+ && this.completionHasFinalAssistantMessage(monitorParsedStatus?.messages)
1249
+ && !this.hasAdapterPendingResponse()
1250
+ && !hasNonEmptyCliModalButtons(monitorParsedStatus?.activeModal ?? monitorParsedStatus?.modal)
1251
+ ) {
1252
+ this.pushEvent({
1253
+ event: 'agent:generating_completed',
1254
+ chatTitle,
1255
+ duration: this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : undefined,
1256
+ timestamp: me.timestamp,
1257
+ finalSummary: extractFinalSummaryFromMessages(monitorParsedStatus?.messages),
1258
+ completionDiagnostic: {
1259
+ providerType: this.type,
1260
+ sessionId: this.instanceId,
1261
+ providerSessionId: this.providerSessionId || null,
1262
+ reconciliationReason: 'long_generating_monitor_final_summary',
1263
+ finalAssistantPresent: true,
1264
+ },
1265
+ });
1266
+ this.generatingStartedAt = 0;
1267
+ continue;
1268
+ }
981
1269
  this.pushEvent({ event: me.type, agentKey: me.agentKey, message: me.message, elapsedSec: me.elapsedSec, timestamp: me.timestamp });
982
1270
  }
983
1271
  }
@@ -1258,12 +1546,28 @@ export class CliProviderInstance implements ProviderInstance {
1258
1546
  index,
1259
1547
  source: 'parsed',
1260
1548
  }));
1549
+ const getRole = (message: ChatMessage): string => typeof message.role === 'string'
1550
+ ? message.role.trim().toLowerCase()
1551
+ : '';
1261
1552
  const runtimeEntries: MergeEntry[] = this.runtimeMessages.map((entry, index) => ({
1262
1553
  message: entry.message,
1263
1554
  index: parsedMessages.length + index,
1264
- source: 'runtime',
1555
+ source: 'runtime' as const,
1265
1556
  runtimeKey: entry.key,
1266
- }));
1557
+ })).filter((entry) => {
1558
+ const meta = entry.message.meta && typeof entry.message.meta === 'object' && !Array.isArray(entry.message.meta)
1559
+ ? entry.message.meta as Record<string, unknown>
1560
+ : {};
1561
+ if (meta.runtimeInputAck !== true) return true;
1562
+ const runtimeText = flattenContent(entry.message.content).replace(/\s+/g, ' ').trim();
1563
+ if (!runtimeText) return false;
1564
+ return !parsedEntries.some((parsedEntry) => {
1565
+ const parsedRole = getRole(parsedEntry.message);
1566
+ if (parsedRole !== 'user' && parsedRole !== 'human') return false;
1567
+ const parsedText = flattenContent(parsedEntry.message.content).replace(/\s+/g, ' ').trim();
1568
+ return parsedText === runtimeText;
1569
+ });
1570
+ });
1267
1571
  const getTime = (message: ChatMessage): number => {
1268
1572
  const value = typeof message.receivedAt === 'number'
1269
1573
  ? message.receivedAt
@@ -1273,9 +1577,6 @@ export class CliProviderInstance implements ProviderInstance {
1273
1577
  return Number.isFinite(value) && value > 0 ? value : 0;
1274
1578
  };
1275
1579
 
1276
- const getRole = (message: ChatMessage): string => typeof message.role === 'string'
1277
- ? message.role.trim().toLowerCase()
1278
- : '';
1279
1580
  const isRuntimeOverlay = (entry: MergeEntry): boolean => {
1280
1581
  if (entry.source !== 'runtime') return false;
1281
1582
  const key = typeof entry.runtimeKey === 'string' ? entry.runtimeKey.trim().toLowerCase() : '';
@@ -1341,7 +1642,9 @@ export class CliProviderInstance implements ProviderInstance {
1341
1642
  this.providerSessionId = nextSessionId;
1342
1643
  this.historyWriter.promoteHistorySession(this.type, previousHistorySessionId, nextSessionId);
1343
1644
  this.historyWriter.writeSessionStart(this.type, nextSessionId, this.workingDir, this.instanceId);
1344
- this.restorePersistedHistoryFromCurrentSession();
1645
+ if (this.shouldHydrateExistingProviderHistory()) {
1646
+ this.restorePersistedHistoryFromCurrentSession();
1647
+ }
1345
1648
  this.adapter.updateRuntimeMeta({ providerSessionId: nextSessionId });
1346
1649
  this.onProviderSessionResolved?.({
1347
1650
  instanceId: this.instanceId,
@@ -1354,6 +1657,24 @@ export class CliProviderInstance implements ProviderInstance {
1354
1657
  LOG.info('CLI', `[${this.type}] discovered provider session id: ${nextSessionId}`);
1355
1658
  }
1356
1659
 
1660
+ private shouldHydrateExistingProviderHistory(): boolean {
1661
+ return this.launchMode === 'resume' || this.launchMode === 'manual';
1662
+ }
1663
+
1664
+ private shouldSuppressFreshLaunchStartupReplay(parsedMessages: unknown[], parsedStatus: any, adapterStatus: any, parsedProviderSessionId = ''): boolean {
1665
+ if (this.launchMode !== 'new') return false;
1666
+ if (this.providerSessionId) return false;
1667
+ if (!Array.isArray(parsedMessages) || parsedMessages.length === 0) return false;
1668
+ if (!isIdleStatus(adapterStatus?.status) || !isIdleStatus(parsedStatus?.status)) return false;
1669
+ if (parsedProviderSessionId) return true;
1670
+
1671
+ const newestMessageAt = parsedMessages.reduce<number>((newest, message) => Math.max(newest, getMessageTime(message)), 0);
1672
+
1673
+ // Untimestamped idle parser output during a fresh launch is usually the
1674
+ // provider's last workspace transcript before a new turn exists.
1675
+ return newestMessageAt === 0;
1676
+ }
1677
+
1357
1678
  private syncCanonicalSavedHistoryIfNeeded(): boolean {
1358
1679
  if (!this.providerSessionId) return false;
1359
1680
  const canonicalHistory = this.provider.canonicalHistory;
@@ -15,6 +15,14 @@ import type { ChatMessageKind } from './chat-message-normalization.js';
15
15
  export type ReadChatTurnStatus = 'open' | 'waiting_approval' | 'complete' | 'error';
16
16
 
17
17
  export interface ReadChatResult {
18
+ /**
19
+ * Declared chat contract version. Absent or `'1.0'` → legacy v1 payload
20
+ * (current shape). `'2.0'` → v2 payload conforming to transcript-v2.ts
21
+ * (ReadChatResultV2). Validators in read-chat-contract.ts route on this
22
+ * field. A1 only adds the field; A2 will make v2 the daemon-internal
23
+ * canonical form and reject unrecognised versions at provider load time.
24
+ */
25
+ contractVersion?: import('./transcript-v2.js').ChatContractVersion;
18
26
  messages: ChatMessage[];
19
27
  status: AgentStatus;
20
28
  activeModal?: ModalInfo | null;
@@ -702,6 +710,17 @@ export interface ProviderCanonicalHistoryConfig {
702
710
  * Omitted mode defaults to 'native-source'.
703
711
  */
704
712
  mode?: 'native-source' | 'materialized-mirror' | 'disabled';
713
+ /**
714
+ * Chat transcript contract version this provider's read_chat output
715
+ * conforms to. See transcript-v2.ts for the v2 invariants. Absent or `'1.0'`
716
+ * → legacy v1 payload (current behaviour). `'2.0'` → strict v2 payload
717
+ * (stable providerUnitKey/bubbleId/sequence, strict enums, honest coverage).
718
+ *
719
+ * A1 only surfaces this field; validators in read-chat-contract.ts route
720
+ * on it. A2 makes v2 the daemon-internal canonical form and rejects
721
+ * unrecognised values at provider load time.
722
+ */
723
+ contractVersion?: import('./transcript-v2.js').ChatContractVersion;
705
724
  }
706
725
 
707
726
  /**