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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/chat/subscription-updates.d.ts +1 -0
  2. package/dist/cli-adapter-types.d.ts +4 -1
  3. package/dist/cli-adapters/provider-cli-adapter.d.ts +25 -1
  4. package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
  5. package/dist/cli-adapters/provider-cli-shared.d.ts +14 -0
  6. package/dist/commands/router.d.ts +22 -0
  7. package/dist/config/chat-history.d.ts +4 -0
  8. package/dist/config/mesh-config.d.ts +68 -1
  9. package/dist/git/git-commands.d.ts +5 -1
  10. package/dist/index.d.ts +15 -5
  11. package/dist/index.js +7461 -1380
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.mjs +7417 -1364
  14. package/dist/index.mjs.map +1 -1
  15. package/dist/installer.d.ts +1 -4
  16. package/dist/launch.d.ts +1 -1
  17. package/dist/logging/async-batch-writer.d.ts +10 -0
  18. package/dist/mesh/beads-db.d.ts +18 -0
  19. package/dist/mesh/mesh-active-work.d.ts +73 -0
  20. package/dist/mesh/mesh-events.d.ts +54 -5
  21. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  22. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  23. package/dist/mesh/mesh-ledger.d.ts +38 -1
  24. package/dist/mesh/mesh-refine-status.d.ts +27 -0
  25. package/dist/mesh/mesh-work-queue.d.ts +27 -5
  26. package/dist/mesh/preview-freshness.d.ts +18 -0
  27. package/dist/mesh/refine-config.d.ts +193 -0
  28. package/dist/mesh/worktree-bootstrap-config.d.ts +115 -0
  29. package/dist/providers/chat-message-normalization.d.ts +1 -0
  30. package/dist/providers/cli-provider-instance.d.ts +6 -1
  31. package/dist/repo-mesh-types.d.ts +62 -0
  32. package/dist/status/reporter.d.ts +2 -0
  33. package/package.json +3 -1
  34. package/src/boot/daemon-lifecycle.ts +1 -0
  35. package/src/chat/subscription-updates.ts +5 -1
  36. package/src/cli-adapter-types.ts +2 -1
  37. package/src/cli-adapters/provider-cli-adapter.ts +473 -18
  38. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  39. package/src/cli-adapters/provider-cli-parse.ts +4 -0
  40. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  41. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  42. package/src/cli-adapters/provider-cli-shared.ts +32 -10
  43. package/src/commands/chat-commands.ts +960 -40
  44. package/src/commands/cli-manager.ts +138 -2
  45. package/src/commands/handler.ts +8 -1
  46. package/src/commands/mesh-coordinator.ts +13 -143
  47. package/src/commands/router.ts +3238 -423
  48. package/src/config/chat-history.ts +37 -9
  49. package/src/config/mesh-config.ts +249 -2
  50. package/src/config/recent-activity.ts +8 -2
  51. package/src/daemon/dev-cli-debug.ts +10 -1
  52. package/src/detection/ide-detector.ts +26 -16
  53. package/src/git/git-commands.ts +17 -5
  54. package/src/index.ts +41 -4
  55. package/src/installer.d.ts +1 -1
  56. package/src/installer.ts +8 -6
  57. package/src/launch.d.ts +1 -1
  58. package/src/launch.ts +37 -28
  59. package/src/logging/async-batch-writer.ts +55 -0
  60. package/src/logging/logger.ts +2 -1
  61. package/src/mesh/beads-db.ts +176 -0
  62. package/src/mesh/coordinator-prompt.ts +31 -8
  63. package/src/mesh/mesh-active-work.ts +295 -0
  64. package/src/mesh/mesh-events.ts +595 -48
  65. package/src/mesh/mesh-fast-forward.ts +430 -0
  66. package/src/mesh/mesh-host-ownership.ts +73 -0
  67. package/src/mesh/mesh-ledger.ts +138 -1
  68. package/src/mesh/mesh-refine-status.ts +145 -0
  69. package/src/mesh/mesh-work-queue.ts +199 -137
  70. package/src/mesh/preview-freshness.ts +118 -0
  71. package/src/mesh/refine-config.ts +366 -0
  72. package/src/mesh/worktree-bootstrap-config.ts +234 -0
  73. package/src/providers/approval-utils.ts +12 -5
  74. package/src/providers/chat-message-normalization.ts +7 -12
  75. package/src/providers/cli-provider-instance.ts +289 -36
  76. package/src/providers/ide-provider-instance.ts +17 -3
  77. package/src/providers/provider-loader.ts +10 -4
  78. package/src/providers/read-chat-contract.ts +1 -1
  79. package/src/providers/version-archive.ts +38 -20
  80. package/src/repo-mesh-types.ts +67 -0
  81. package/src/status/reporter.ts +15 -0
  82. package/src/system/host-memory.ts +29 -12
@@ -43,6 +43,23 @@ 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
+ function getMessageTime(message: unknown): number {
52
+ if (!message || typeof message !== 'object') return 0;
53
+ const record = message as { receivedAt?: unknown; timestamp?: unknown };
54
+ const value = Number(record.receivedAt ?? record.timestamp ?? 0);
55
+ return Number.isFinite(value) ? value : 0;
56
+ }
57
+
58
+ type CompletedFinalizationBlock = {
59
+ reason: string;
60
+ terminal?: boolean;
61
+ };
62
+
46
63
  const COMPLETED_FINALIZATION_RETRY_MS = 1000;
47
64
  const COMPLETED_FINALIZATION_MAX_WAIT_MS = 30_000;
48
65
 
@@ -312,6 +329,7 @@ export class CliProviderInstance implements ProviderInstance {
312
329
  private lastApprovalEventAt = 0;
313
330
  private autoApproveBusy = false;
314
331
  private autoApproveBusyTimer: NodeJS.Timeout | null = null;
332
+ private lastAutoApprovalSignature = '';
315
333
  private controlValues: Record<string, string | number | boolean> = {};
316
334
  private summaryMetadata: unknown = undefined;
317
335
  private appliedEffectKeys = new Set<string>();
@@ -412,7 +430,7 @@ export class CliProviderInstance implements ProviderInstance {
412
430
  await this.adapter.spawn();
413
431
  await this.enforceFreshSessionLaunchIfNeeded();
414
432
  this.maybeAppendRuntimeRecoveryMessage(this.adapter.getRuntimeMetadata());
415
- if (this.providerSessionId) {
433
+ if (this.providerSessionId && this.shouldHydrateExistingProviderHistory()) {
416
434
  this.restorePersistedHistoryFromCurrentSession();
417
435
  }
418
436
  if (this.providerSessionId && this.launchMode === 'resume') {
@@ -492,8 +510,14 @@ export class CliProviderInstance implements ProviderInstance {
492
510
  if (typeof this.adapter.getScriptParsedStatus === 'function') {
493
511
  try {
494
512
  parsedStatus = this.adapter.getScriptParsedStatus() || null;
495
- this.errorMessage = undefined;
496
- this.errorReason = undefined;
513
+ const parsedErrorMessage = typeof parsedStatus?.errorMessage === 'string' && parsedStatus.errorMessage.trim()
514
+ ? parsedStatus.errorMessage.trim()
515
+ : undefined;
516
+ const parsedErrorReason = typeof parsedStatus?.errorReason === 'string' && parsedStatus.errorReason.trim()
517
+ ? parsedStatus.errorReason.trim() as ProviderErrorReason
518
+ : undefined;
519
+ this.errorMessage = parsedErrorMessage;
520
+ this.errorReason = parsedErrorReason;
497
521
  } catch (error: any) {
498
522
  parseErrorMessage = error?.message || String(error);
499
523
  this.errorMessage = parseErrorMessage;
@@ -503,22 +527,39 @@ export class CliProviderInstance implements ProviderInstance {
503
527
  this.errorMessage = undefined;
504
528
  this.errorReason = undefined;
505
529
  }
530
+ const adapterProviderSessionId = normalizeProviderSessionId(
531
+ this.provider,
532
+ typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
533
+ );
506
534
  const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, Date.now());
507
- const visibleStatus = parseErrorMessage
535
+ const visibleStatus = parseErrorMessage || parsedStatus?.status === 'error'
508
536
  ? 'error'
509
537
  : (autoApproveActive ? 'generating' : adapterStatus.status);
538
+ const runtime = this.adapter.getRuntimeMetadata();
539
+ this.maybeAppendRuntimeRecoveryMessage(runtime);
540
+ let parsedMessages = Array.isArray(parsedStatus?.messages)
541
+ ? parsedStatus.messages
542
+ : [];
510
543
  const parsedProviderSessionId = normalizeProviderSessionId(
511
544
  this.provider,
512
545
  typeof parsedStatus?.providerSessionId === 'string' ? parsedStatus.providerSessionId : '',
513
546
  );
514
- if (parsedProviderSessionId) {
547
+ const suppressFreshLaunchStartupReplay = this.shouldSuppressFreshLaunchStartupReplay(
548
+ parsedMessages,
549
+ parsedStatus,
550
+ adapterStatus,
551
+ parsedProviderSessionId,
552
+ );
553
+ if (adapterProviderSessionId && !suppressFreshLaunchStartupReplay) {
554
+ this.promoteProviderSessionId(adapterProviderSessionId);
555
+ }
556
+ if (parsedProviderSessionId && !suppressFreshLaunchStartupReplay) {
515
557
  this.promoteProviderSessionId(parsedProviderSessionId);
516
558
  }
517
- const runtime = this.adapter.getRuntimeMetadata();
518
- this.maybeAppendRuntimeRecoveryMessage(runtime);
519
- let parsedMessages = Array.isArray(parsedStatus?.messages)
520
- ? parsedStatus.messages
521
- : [];
559
+ if (suppressFreshLaunchStartupReplay) {
560
+ parsedMessages = [];
561
+ }
562
+ const activeChatId = this.providerSessionId || runtime?.runtimeId || this.instanceId;
522
563
  const historyMessageCount = Number.isFinite(parsedStatus?.historyMessageCount)
523
564
  ? Math.max(0, Number(parsedStatus.historyMessageCount))
524
565
  : null;
@@ -528,7 +569,18 @@ export class CliProviderInstance implements ProviderInstance {
528
569
  : [];
529
570
  }
530
571
  const mergedMessages = this.mergeConversationMessages(parsedMessages);
531
- const canonicalBackedHistory = this.syncCanonicalSavedHistoryIfNeeded();
572
+ const canonicalBackedHistory = this.shouldHydrateExistingProviderHistory()
573
+ ? this.syncCanonicalSavedHistoryIfNeeded()
574
+ : false;
575
+ const statusMessages = canonicalBackedHistory && this.lastPersistedHistoryMessages.length > 0
576
+ ? this.lastPersistedHistoryMessages.map((message) => ({
577
+ role: message.role,
578
+ content: message.content,
579
+ kind: message.kind,
580
+ senderName: message.senderName,
581
+ receivedAt: message.receivedAt,
582
+ }))
583
+ : mergedMessages;
532
584
 
533
585
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
534
586
  const parsedChatStatus = typeof parsedStatus?.status === 'string' && parsedStatus.status.trim()
@@ -572,7 +624,12 @@ export class CliProviderInstance implements ProviderInstance {
572
624
  }
573
625
  }
574
626
 
575
- this.applyProviderResponse(parsedStatus, { phase: 'immediate' });
627
+ this.applyProviderResponse(
628
+ suppressFreshLaunchStartupReplay && parsedStatus && typeof parsedStatus === 'object'
629
+ ? { ...parsedStatus, providerSessionId: undefined }
630
+ : parsedStatus,
631
+ { phase: 'immediate' },
632
+ );
576
633
  const surface = resolveProviderStateSurface({
577
634
  summaryMetadata: this.summaryMetadata as any,
578
635
  controlValues: this.controlValues,
@@ -592,10 +649,10 @@ export class CliProviderInstance implements ProviderInstance {
592
649
  status: visibleStatus,
593
650
  mode: this.presentationMode,
594
651
  activeChat: {
595
- id: `${this.type}_${this.workingDir}`,
652
+ id: activeChatId,
596
653
  title: parsedStatus?.title || dirName,
597
654
  status: activeChatStatus,
598
- messages: mergedMessages,
655
+ messages: statusMessages,
599
656
  activeModal: autoApproveActive ? null : (parsedStatus?.activeModal ?? adapterStatus.activeModal),
600
657
  inputContent: '',
601
658
  },
@@ -695,6 +752,34 @@ export class CliProviderInstance implements ProviderInstance {
695
752
  }
696
753
  }
697
754
 
755
+ recordAcknowledgedUserInput(input: InputEnvelope | string): void {
756
+ const content = typeof input === 'string'
757
+ ? input.trim()
758
+ : buildCliStructuredInputPrompt(input).trim();
759
+ if (!content) return;
760
+
761
+ const receivedAt = Date.now();
762
+ const dedupKey = `user_input_ack:${crypto
763
+ .createHash('sha256')
764
+ .update(`${this.instanceId}:${content}:${receivedAt}`)
765
+ .digest('hex')
766
+ .slice(0, 24)}`;
767
+ this.appendRuntimeMessage(buildChatMessage({
768
+ role: 'user',
769
+ senderName: 'User',
770
+ kind: 'standard',
771
+ content,
772
+ receivedAt,
773
+ timestamp: receivedAt,
774
+ source: 'runtime_input_ack',
775
+ meta: {
776
+ runtimeInputAck: true,
777
+ provider: this.type,
778
+ workspace: this.workingDir,
779
+ },
780
+ } as ChatMessage), dedupKey);
781
+ }
782
+
698
783
  dispose(): void {
699
784
  this.adapter.shutdown();
700
785
  this.monitor.reset();
@@ -743,6 +828,55 @@ export class CliProviderInstance implements ProviderInstance {
743
828
  return role === 'assistant' && !!content;
744
829
  }
745
830
 
831
+ private buildCompletedFinalizationDiagnostic(args: {
832
+ blockReason: string;
833
+ latestStatus?: any;
834
+ latestVisibleStatus: string;
835
+ waitedMs: number;
836
+ pending: CompletedDebouncePending;
837
+ emittedAfterFinalizationTimeout: boolean;
838
+ }): Record<string, unknown> {
839
+ let parsed: any = null;
840
+ let parseError: string | undefined;
841
+ try {
842
+ parsed = this.adapter.getScriptParsedStatus();
843
+ } catch (error: any) {
844
+ parseError = error?.message || String(error);
845
+ }
846
+
847
+ const visibleMessages = (Array.isArray(parsed?.messages) ? parsed.messages : [])
848
+ .filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
849
+ const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
850
+ const lastVisibleRole = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null;
851
+ const lastVisibleKind = typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null;
852
+ const lastVisibleContentLength = lastVisible ? flattenContent(lastVisible.content).trim().length : 0;
853
+
854
+ return {
855
+ providerType: this.type,
856
+ sessionId: this.instanceId,
857
+ providerSessionId: this.providerSessionId || null,
858
+ workspace: this.workingDir,
859
+ blockReason: args.blockReason,
860
+ emittedAfterFinalizationTimeout: args.emittedAfterFinalizationTimeout,
861
+ waitedMs: args.waitedMs,
862
+ maxWaitMs: COMPLETED_FINALIZATION_MAX_WAIT_MS,
863
+ adapterStatus: typeof args.latestStatus?.status === 'string' ? args.latestStatus.status : null,
864
+ latestVisibleStatus: args.latestVisibleStatus,
865
+ parsedStatus: typeof parsed?.status === 'string' ? parsed.status : (parseError ? 'parse_error' : 'unknown'),
866
+ parseError: parseError || undefined,
867
+ finalAssistantPresent: this.completionHasFinalAssistantMessage(parsed?.messages),
868
+ visibleMessageCount: visibleMessages.length,
869
+ lastVisibleRole,
870
+ lastVisibleKind,
871
+ lastVisibleContentLength,
872
+ pendingStartedAt: this.generatingStartedAt || null,
873
+ pendingFirstObservedAt: args.pending.firstObservedAt,
874
+ pendingTimestamp: args.pending.timestamp,
875
+ pendingDurationSec: args.pending.duration,
876
+ previousBlockReason: args.pending.loggedBlockReason || null,
877
+ };
878
+ }
879
+
746
880
  private hasAdapterPendingResponse(): boolean {
747
881
  const adapterAny = this.adapter as any;
748
882
  if (adapterAny?.isWaitingForResponse === true) return true;
@@ -768,29 +902,34 @@ export class CliProviderInstance implements ProviderInstance {
768
902
  return !this.hasAdapterPendingResponse();
769
903
  }
770
904
 
771
- private getCompletedFinalizationBlockReason(latestVisibleStatus: string): string | null {
772
- if (latestVisibleStatus !== 'idle') return `status:${latestVisibleStatus}`;
905
+ private getCompletedFinalizationBlock(latestVisibleStatus: string): CompletedFinalizationBlock | null {
906
+ if (latestVisibleStatus !== 'idle') return { reason: `status:${latestVisibleStatus}`, terminal: true };
773
907
 
774
908
  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';
909
+ if (adapterAny?.isWaitingForResponse === true) return { reason: 'adapter_waiting_for_response', terminal: true };
910
+ if (adapterAny?.currentTurnScope) return { reason: 'adapter_turn_scope_active', terminal: true };
911
+ if (this.hasAdapterPendingResponse()) return { reason: 'adapter_pending_response', terminal: true };
777
912
 
778
913
  const partial = typeof this.adapter.getPartialResponse === 'function'
779
914
  ? this.adapter.getPartialResponse()
780
915
  : '';
781
- if (typeof partial === 'string' && partial.trim()) return 'partial_response_pending';
916
+ if (typeof partial === 'string' && partial.trim()) return { reason: 'partial_response_pending', terminal: true };
782
917
 
783
918
  let parsed: any;
784
919
  try {
785
920
  parsed = this.adapter.getScriptParsedStatus();
786
921
  } catch (error: any) {
787
- return `parse_error:${error?.message || String(error)}`;
922
+ return { reason: `parse_error:${error?.message || String(error)}` };
788
923
  }
789
924
 
790
925
  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';
926
+ if (parsedStatus !== 'idle') {
927
+ const adapterStatus = this.adapter.getStatus({ allowParse: false });
928
+ if (this.shouldSuppressStaleParsedBusyStatus(parsed, adapterStatus)) return null;
929
+ return { reason: `parsed_status:${parsedStatus}`, terminal: isCliGeneratingLikeStatus(parsedStatus) };
930
+ }
931
+ if (parsed?.activeModal || parsed?.modal) return { reason: 'parsed_modal_active', terminal: true };
932
+ if (!this.completionHasFinalAssistantMessage(parsed?.messages)) return { reason: 'missing_final_assistant' };
794
933
 
795
934
  return null;
796
935
  }
@@ -817,10 +956,11 @@ export class CliProviderInstance implements ProviderInstance {
817
956
  return;
818
957
  }
819
958
 
820
- const blockReason = this.getCompletedFinalizationBlockReason(latestVisibleStatus);
821
- if (blockReason) {
959
+ const block = this.getCompletedFinalizationBlock(latestVisibleStatus);
960
+ if (block) {
961
+ const blockReason = block.reason;
822
962
  const waitedMs = Date.now() - pending.firstObservedAt;
823
- if (waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
963
+ if (block.terminal || waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
824
964
  if (pending.loggedBlockReason !== blockReason) {
825
965
  LOG.info('CLI', `[${this.type}] waiting to emit completed until transcript finalizes (${blockReason})`);
826
966
  pending.loggedBlockReason = blockReason;
@@ -828,7 +968,25 @@ export class CliProviderInstance implements ProviderInstance {
828
968
  this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
829
969
  return;
830
970
  }
831
- LOG.warn('CLI', `[${this.type}] suppressed completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
971
+ const completionDiagnostic = this.buildCompletedFinalizationDiagnostic({
972
+ blockReason,
973
+ latestStatus,
974
+ latestVisibleStatus,
975
+ waitedMs,
976
+ pending,
977
+ emittedAfterFinalizationTimeout: true,
978
+ });
979
+ LOG.warn('CLI', `[${this.type}] emitting completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
980
+ this.pushEvent({
981
+ event: 'agent:generating_completed',
982
+ chatTitle: pending.chatTitle,
983
+ duration: pending.duration,
984
+ timestamp: pending.timestamp,
985
+ finalSummary: blockReason.startsWith('parsed_status:')
986
+ ? ''
987
+ : extractFinalSummaryFromMessages(this.adapter?.getScriptParsedStatus()?.messages),
988
+ completionDiagnostic,
989
+ });
832
990
  this.completedDebouncePending = null;
833
991
  this.completedDebounceTimer = null;
834
992
  this.generatingStartedAt = 0;
@@ -853,15 +1011,29 @@ export class CliProviderInstance implements ProviderInstance {
853
1011
  // Guard re-entry: onStatusChange/getState can observe the same modal multiple
854
1012
  // times while the PTY absorbs the approval key. Without this flag, repeated
855
1013
  // snapshots would write stray keys into the input once the modal dismisses.
856
- if (autoApproveActive && !this.autoApproveBusy) {
1014
+ // However, Claude Code can present a second approval immediately after the
1015
+ // first. Resolve a changed modal signature even while the previous write is
1016
+ // still inside the short busy window.
1017
+ if (!autoApproveActive) {
1018
+ this.lastAutoApprovalSignature = '';
1019
+ return autoApproveActive;
1020
+ }
1021
+ const modal = adapterStatus.activeModal;
1022
+ const { index: buttonIndex, label: buttonLabel } = pickApprovalButton(modal?.buttons, this.provider);
1023
+ const signature = [
1024
+ typeof modal?.message === 'string' ? modal.message.trim() : '',
1025
+ Array.isArray(modal?.buttons) ? modal.buttons.join('|') : '',
1026
+ buttonIndex,
1027
+ ].join('::');
1028
+ if (!this.autoApproveBusy || signature !== this.lastAutoApprovalSignature) {
857
1029
  this.autoApproveBusy = true;
1030
+ this.lastAutoApprovalSignature = signature;
858
1031
  if (this.autoApproveBusyTimer) clearTimeout(this.autoApproveBusyTimer);
859
1032
  this.autoApproveBusyTimer = setTimeout(() => {
860
1033
  this.autoApproveBusy = false;
861
1034
  this.autoApproveBusyTimer = null;
1035
+ this.lastAutoApprovalSignature = '';
862
1036
  }, 2000);
863
- const modal = adapterStatus.activeModal;
864
- const { index: buttonIndex, label: buttonLabel } = pickApprovalButton(modal?.buttons, this.provider);
865
1037
  this.recordAutoApproval(modal?.message, buttonLabel, now);
866
1038
  setTimeout(() => {
867
1039
  this.adapter.resolveModal(buttonIndex);
@@ -876,6 +1048,13 @@ export class CliProviderInstance implements ProviderInstance {
876
1048
  // during long-running CLI sessions. Keep this path on adapter-owned light
877
1049
  // state only; rich provider parsing is reserved for getState/read_chat.
878
1050
  const adapterStatus = this.adapter.getStatus({ allowParse: false });
1051
+ const adapterProviderSessionId = normalizeProviderSessionId(
1052
+ this.provider,
1053
+ typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
1054
+ );
1055
+ if (adapterProviderSessionId) {
1056
+ this.promoteProviderSessionId(adapterProviderSessionId);
1057
+ }
879
1058
  const parsedStatus = null;
880
1059
  const rawStatus = adapterStatus.status;
881
1060
  const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, now);
@@ -957,6 +1136,23 @@ export class CliProviderInstance implements ProviderInstance {
957
1136
  }
958
1137
  } else if (newStatus === 'idle' && this.lastStatus === 'starting') {
959
1138
  this.pushEvent({ event: 'agent:ready', chatTitle, timestamp: now });
1139
+ } else if (newStatus === 'error') {
1140
+ if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
1141
+ this.generatingDebouncePending = null;
1142
+ if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
1143
+ this.completedDebouncePending = null;
1144
+ this.errorMessage = adapterStatus.errorMessage || this.errorMessage;
1145
+ this.errorReason = (adapterStatus.errorReason as ProviderErrorReason) || this.errorReason;
1146
+ this.pushEvent({
1147
+ event: 'agent:stopped',
1148
+ chatTitle,
1149
+ timestamp: now,
1150
+ finalSummary: adapterStatus.errorMessage || adapterStatus.errorReason || 'Provider reported an error',
1151
+ completionDiagnostic: {
1152
+ reason: adapterStatus.errorReason || 'provider_error',
1153
+ errorMessage: adapterStatus.errorMessage || undefined,
1154
+ },
1155
+ });
960
1156
  } else if (newStatus === 'stopped') {
961
1157
  // Cancel any pending debounce
962
1158
  if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
@@ -977,7 +1173,31 @@ export class CliProviderInstance implements ProviderInstance {
977
1173
  // Monitor check (cooldown based notification, IDE/CLI common)
978
1174
  const agentKey = `${this.type}:cli`;
979
1175
  const monitorEvents = this.monitor.check(agentKey, newStatus, now, progressFingerprint);
1176
+ const monitorParsedStatus: any = parsedStatus;
980
1177
  for (const me of monitorEvents) {
1178
+ if (
1179
+ me.type === 'monitor:long_generating'
1180
+ && this.completionHasFinalAssistantMessage(monitorParsedStatus?.messages)
1181
+ && !this.hasAdapterPendingResponse()
1182
+ && !hasNonEmptyCliModalButtons(monitorParsedStatus?.activeModal ?? monitorParsedStatus?.modal)
1183
+ ) {
1184
+ this.pushEvent({
1185
+ event: 'agent:generating_completed',
1186
+ chatTitle,
1187
+ duration: this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : undefined,
1188
+ timestamp: me.timestamp,
1189
+ finalSummary: extractFinalSummaryFromMessages(monitorParsedStatus?.messages),
1190
+ completionDiagnostic: {
1191
+ providerType: this.type,
1192
+ sessionId: this.instanceId,
1193
+ providerSessionId: this.providerSessionId || null,
1194
+ reconciliationReason: 'long_generating_monitor_final_summary',
1195
+ finalAssistantPresent: true,
1196
+ },
1197
+ });
1198
+ this.generatingStartedAt = 0;
1199
+ continue;
1200
+ }
981
1201
  this.pushEvent({ event: me.type, agentKey: me.agentKey, message: me.message, elapsedSec: me.elapsedSec, timestamp: me.timestamp });
982
1202
  }
983
1203
  }
@@ -1258,12 +1478,28 @@ export class CliProviderInstance implements ProviderInstance {
1258
1478
  index,
1259
1479
  source: 'parsed',
1260
1480
  }));
1481
+ const getRole = (message: ChatMessage): string => typeof message.role === 'string'
1482
+ ? message.role.trim().toLowerCase()
1483
+ : '';
1261
1484
  const runtimeEntries: MergeEntry[] = this.runtimeMessages.map((entry, index) => ({
1262
1485
  message: entry.message,
1263
1486
  index: parsedMessages.length + index,
1264
- source: 'runtime',
1487
+ source: 'runtime' as const,
1265
1488
  runtimeKey: entry.key,
1266
- }));
1489
+ })).filter((entry) => {
1490
+ const meta = entry.message.meta && typeof entry.message.meta === 'object' && !Array.isArray(entry.message.meta)
1491
+ ? entry.message.meta as Record<string, unknown>
1492
+ : {};
1493
+ if (meta.runtimeInputAck !== true) return true;
1494
+ const runtimeText = flattenContent(entry.message.content).replace(/\s+/g, ' ').trim();
1495
+ if (!runtimeText) return false;
1496
+ return !parsedEntries.some((parsedEntry) => {
1497
+ const parsedRole = getRole(parsedEntry.message);
1498
+ if (parsedRole !== 'user' && parsedRole !== 'human') return false;
1499
+ const parsedText = flattenContent(parsedEntry.message.content).replace(/\s+/g, ' ').trim();
1500
+ return parsedText === runtimeText;
1501
+ });
1502
+ });
1267
1503
  const getTime = (message: ChatMessage): number => {
1268
1504
  const value = typeof message.receivedAt === 'number'
1269
1505
  ? message.receivedAt
@@ -1273,9 +1509,6 @@ export class CliProviderInstance implements ProviderInstance {
1273
1509
  return Number.isFinite(value) && value > 0 ? value : 0;
1274
1510
  };
1275
1511
 
1276
- const getRole = (message: ChatMessage): string => typeof message.role === 'string'
1277
- ? message.role.trim().toLowerCase()
1278
- : '';
1279
1512
  const isRuntimeOverlay = (entry: MergeEntry): boolean => {
1280
1513
  if (entry.source !== 'runtime') return false;
1281
1514
  const key = typeof entry.runtimeKey === 'string' ? entry.runtimeKey.trim().toLowerCase() : '';
@@ -1341,7 +1574,9 @@ export class CliProviderInstance implements ProviderInstance {
1341
1574
  this.providerSessionId = nextSessionId;
1342
1575
  this.historyWriter.promoteHistorySession(this.type, previousHistorySessionId, nextSessionId);
1343
1576
  this.historyWriter.writeSessionStart(this.type, nextSessionId, this.workingDir, this.instanceId);
1344
- this.restorePersistedHistoryFromCurrentSession();
1577
+ if (this.shouldHydrateExistingProviderHistory()) {
1578
+ this.restorePersistedHistoryFromCurrentSession();
1579
+ }
1345
1580
  this.adapter.updateRuntimeMeta({ providerSessionId: nextSessionId });
1346
1581
  this.onProviderSessionResolved?.({
1347
1582
  instanceId: this.instanceId,
@@ -1354,6 +1589,24 @@ export class CliProviderInstance implements ProviderInstance {
1354
1589
  LOG.info('CLI', `[${this.type}] discovered provider session id: ${nextSessionId}`);
1355
1590
  }
1356
1591
 
1592
+ private shouldHydrateExistingProviderHistory(): boolean {
1593
+ return this.launchMode === 'resume' || this.launchMode === 'manual';
1594
+ }
1595
+
1596
+ private shouldSuppressFreshLaunchStartupReplay(parsedMessages: unknown[], parsedStatus: any, adapterStatus: any, parsedProviderSessionId = ''): boolean {
1597
+ if (this.launchMode !== 'new') return false;
1598
+ if (this.providerSessionId) return false;
1599
+ if (!Array.isArray(parsedMessages) || parsedMessages.length === 0) return false;
1600
+ if (!isIdleStatus(adapterStatus?.status) || !isIdleStatus(parsedStatus?.status)) return false;
1601
+ if (parsedProviderSessionId) return true;
1602
+
1603
+ const newestMessageAt = parsedMessages.reduce<number>((newest, message) => Math.max(newest, getMessageTime(message)), 0);
1604
+
1605
+ // Untimestamped idle parser output during a fresh launch is usually the
1606
+ // provider's last workspace transcript before a new turn exists.
1607
+ return newestMessageAt === 0;
1608
+ }
1609
+
1357
1610
  private syncCanonicalSavedHistoryIfNeeded(): boolean {
1358
1611
  if (!this.providerSessionId) return false;
1359
1612
  const canonicalHistory = this.provider.canonicalHistory;
@@ -43,6 +43,20 @@ type ReadChatPayload = {
43
43
  [key: string]: unknown;
44
44
  };
45
45
 
46
+ async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
47
+ let timer: ReturnType<typeof setTimeout> | null = null;
48
+ try {
49
+ return await Promise.race([
50
+ promise,
51
+ new Promise<never>((_, reject) => {
52
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
53
+ }),
54
+ ]);
55
+ } finally {
56
+ if (timer) clearTimeout(timer);
57
+ }
58
+ }
59
+
46
60
  export class IdeProviderInstance implements ProviderInstance {
47
61
  readonly type: string;
48
62
  readonly category = 'ide' as const;
@@ -320,7 +334,7 @@ export class IdeProviderInstance implements ProviderInstance {
320
334
  if (webviewScript) {
321
335
  const matchText = this.provider.webviewMatchText;
322
336
  const matchFn = matchText ? (body: string) => body.includes(matchText) : undefined;
323
- const webviewRaw = await cdp.evaluateInWebviewFrame(webviewScript, matchFn);
337
+ const webviewRaw = await withTimeout(cdp.evaluateInWebviewFrame(webviewScript, matchFn), 30000, 'evaluateInWebviewFrame');
324
338
  if (webviewRaw) {
325
339
  raw = typeof webviewRaw === 'string' ? (() => { try { return JSON.parse(webviewRaw); } catch { return null; } })() : webviewRaw;
326
340
  }
@@ -331,7 +345,7 @@ export class IdeProviderInstance implements ProviderInstance {
331
345
  if (!raw) {
332
346
  const readChatScript = this.getReadChatScript();
333
347
  if (!readChatScript) return;
334
- raw = await cdp.evaluate(readChatScript, 30000);
348
+ raw = await withTimeout(cdp.evaluate(readChatScript, 30000), 30000, 'evaluate.readChatScript');
335
349
  if (typeof raw === 'string') {
336
350
  try { raw = JSON.parse(raw); } catch { return; }
337
351
  }
@@ -706,7 +720,7 @@ export class IdeProviderInstance implements ProviderInstance {
706
720
  );
707
721
 
708
722
  LOG.info('IdeInstance', `[IdeInstance:${this.type}] autoApprove: executing resolveAction for "${targetButton}"`);
709
- let rawResult = await cdp.evaluate(script, 10000);
723
+ let rawResult = await withTimeout(cdp.evaluate(script, 10000), 10000, 'evaluate.autoApprove');
710
724
  if (typeof rawResult === 'string') {
711
725
  try { rawResult = JSON.parse(rawResult); } catch { }
712
726
  }
@@ -1067,13 +1067,17 @@ export class ProviderLoader {
1067
1067
  awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
1068
1068
  });
1069
1069
 
1070
+ let reloadTimer: ReturnType<typeof setTimeout> | null = null;
1070
1071
  const handleChange = (filePath: string) => {
1071
1072
  if (/[\/\\]fixtures[\/\\]/.test(filePath)) {
1072
1073
  return;
1073
1074
  }
1074
1075
  if (filePath.endsWith('.js') || filePath.endsWith('.json')) {
1075
- this.log(`File changed: ${path.basename(filePath)}, reloading...`);
1076
- this.reload();
1076
+ if (reloadTimer) clearTimeout(reloadTimer);
1077
+ reloadTimer = setTimeout(() => {
1078
+ this.log(`File changed: ${path.basename(filePath)}, reloading...`);
1079
+ this.reload();
1080
+ }, 300);
1077
1081
  }
1078
1082
  };
1079
1083
 
@@ -1130,7 +1134,9 @@ export class ProviderLoader {
1130
1134
  return { updated: false };
1131
1135
  }
1132
1136
  const https = require('https') as typeof import('https');
1133
- const { execSync } = require('child_process') as typeof import('child_process');
1137
+ const { exec } = require('child_process') as typeof import('child_process');
1138
+ const { promisify } = require('util');
1139
+ const execAsync = promisify(exec);
1134
1140
 
1135
1141
  const metaPath = path.join(this.upstreamDir, ProviderLoader.META_FILE);
1136
1142
  let prevEtag = '';
@@ -1207,7 +1213,7 @@ export class ProviderLoader {
1207
1213
 
1208
1214
  // Extract
1209
1215
  fs.mkdirSync(tmpExtract, { recursive: true });
1210
- execSync(`tar -xzf "${tmpTar}" -C "${tmpExtract}"`, { timeout: 30000 });
1216
+ await execAsync(`tar -xzf "${tmpTar}" -C "${tmpExtract}"`, { timeout: 30000 });
1211
1217
 
1212
1218
  // Tarball internal structure: adhdev-providers-main/ide/... → strip 1 level
1213
1219
  const extracted = fs.readdirSync(tmpExtract);
@@ -2,7 +2,7 @@ import type { MessagePart, ModalInfo, ReadChatResult } from './contracts.js'
2
2
  import { normalizeMessageParts } from './contracts.js'
3
3
  import type { ChatBubbleState, ChatMessage } from '../types.js'
4
4
 
5
- const VALID_STATUSES = ['idle', 'generating', 'waiting_approval', 'error', 'panel_hidden', 'streaming', 'long_generating'] as const
5
+ const VALID_STATUSES = ['idle', 'generating', 'waiting_approval', 'error', 'panel_hidden', 'starting', 'streaming', 'long_generating'] as const
6
6
  const VALID_ROLES = ['user', 'assistant', 'system', 'human'] as const
7
7
  const VALID_BUBBLE_STATES = ['draft', 'streaming', 'final', 'removed'] as const
8
8
  const VALID_TURN_STATUSES = ['open', 'waiting_approval', 'complete', 'error'] as const