@adhdev/daemon-core 0.9.82-rc.15 → 0.9.82-rc.152

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 (182) hide show
  1. package/dist/boot/process-hardening.d.ts +50 -0
  2. package/dist/chat/source-machine.d.ts +166 -0
  3. package/dist/chat/source-resolver.d.ts +104 -0
  4. package/dist/chat/subscription-updates.d.ts +1 -0
  5. package/dist/cli-adapter-types.d.ts +5 -1
  6. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  7. package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
  8. package/dist/cli-adapters/provider-cli-adapter.d.ts +104 -63
  9. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  10. package/dist/cli-adapters/provider-cli-shared.d.ts +27 -0
  11. package/dist/commands/handler.d.ts +66 -0
  12. package/dist/commands/router.d.ts +22 -0
  13. package/dist/config/chat-history.d.ts +5 -0
  14. package/dist/config/config.d.ts +5 -0
  15. package/dist/config/mesh-config.d.ts +68 -1
  16. package/dist/git/git-commands.d.ts +5 -1
  17. package/dist/index.d.ts +20 -6
  18. package/dist/index.js +13153 -3031
  19. package/dist/index.js.map +1 -1
  20. package/dist/index.mjs +13099 -3023
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/installer.d.ts +1 -4
  23. package/dist/ipc/local-ipc-server.d.ts +91 -0
  24. package/dist/launch.d.ts +1 -1
  25. package/dist/logging/async-batch-writer.d.ts +10 -0
  26. package/dist/mesh/beads-db.d.ts +72 -0
  27. package/dist/mesh/contracts.d.ts +164 -0
  28. package/dist/mesh/coordinator-registry.d.ts +25 -0
  29. package/dist/mesh/mesh-active-work.d.ts +90 -0
  30. package/dist/mesh/mesh-events.d.ts +77 -5
  31. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  32. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  33. package/dist/mesh/mesh-ledger.d.ts +58 -1
  34. package/dist/mesh/mesh-refine-status.d.ts +26 -0
  35. package/dist/mesh/mesh-work-queue.d.ts +44 -5
  36. package/dist/mesh/preview-freshness.d.ts +18 -0
  37. package/dist/mesh/refine-config.d.ts +193 -0
  38. package/dist/mesh/worktree-bootstrap-config.d.ts +113 -0
  39. package/dist/providers/approval-utils.d.ts +9 -0
  40. package/dist/providers/cli-provider-instance.d.ts +6 -1
  41. package/dist/providers/contracts.d.ts +50 -5
  42. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  43. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  44. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  45. package/dist/providers/native-history/index.d.ts +11 -0
  46. package/dist/providers/provider-loader.d.ts +19 -1
  47. package/dist/providers/read-chat-contract.d.ts +29 -0
  48. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  49. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  50. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  51. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +64 -0
  52. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  53. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  54. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  55. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  56. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  57. package/dist/providers/sdk/v1/index.d.ts +30 -0
  58. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  59. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  60. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  61. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  62. package/dist/providers/sdk/v1/types/common/index.d.ts +169 -0
  63. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  64. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  65. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  66. package/dist/providers/transcript-v2.d.ts +176 -0
  67. package/dist/repo-mesh-types.d.ts +67 -0
  68. package/dist/shared-types.d.ts +12 -0
  69. package/dist/status/reporter.d.ts +2 -0
  70. package/dist/status/snapshot.d.ts +1 -0
  71. package/dist/types.d.ts +5 -0
  72. package/package.json +6 -2
  73. package/src/boot/daemon-lifecycle.ts +17 -10
  74. package/src/boot/process-hardening.ts +89 -0
  75. package/src/chat/source-machine.ts +534 -0
  76. package/src/chat/source-resolver.ts +0 -0
  77. package/src/chat/subscription-updates.ts +14 -1
  78. package/src/cli-adapter-types.d.ts +1 -0
  79. package/src/cli-adapter-types.ts +3 -1
  80. package/src/cli-adapters/cli-script-runner.ts +421 -0
  81. package/src/cli-adapters/cli-state-engine.ts +1086 -0
  82. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  83. package/src/cli-adapters/provider-cli-adapter.ts +664 -1137
  84. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  85. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  86. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  87. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  88. package/src/cli-adapters/provider-cli-shared.ts +57 -11
  89. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
  90. package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
  91. package/src/commands/chat-commands.ts +1451 -52
  92. package/src/commands/cli-manager.ts +184 -3
  93. package/src/commands/handler.ts +547 -2
  94. package/src/commands/mesh-coordinator.ts +13 -143
  95. package/src/commands/router.ts +3273 -428
  96. package/src/config/chat-history.ts +79 -24
  97. package/src/config/config.ts +12 -0
  98. package/src/config/mesh-config.ts +249 -2
  99. package/src/config/recent-activity.ts +8 -2
  100. package/src/daemon/dev-cli-debug.ts +10 -1
  101. package/src/detection/ide-detector.ts +26 -16
  102. package/src/git/git-commands.ts +17 -5
  103. package/src/git/git-worktree.ts +8 -1
  104. package/src/index.ts +72 -5
  105. package/src/installer.d.ts +1 -1
  106. package/src/installer.ts +8 -6
  107. package/src/ipc/local-ipc-server.ts +278 -0
  108. package/src/launch.d.ts +1 -1
  109. package/src/launch.ts +37 -28
  110. package/src/logging/async-batch-writer.ts +55 -0
  111. package/src/logging/logger.ts +2 -1
  112. package/src/mesh/beads-db.ts +479 -0
  113. package/src/mesh/contracts.ts +329 -0
  114. package/src/mesh/coordinator-prompt.ts +40 -22
  115. package/src/mesh/coordinator-registry.ts +75 -0
  116. package/src/mesh/mesh-active-work.ts +437 -0
  117. package/src/mesh/mesh-events.ts +820 -61
  118. package/src/mesh/mesh-fast-forward.ts +430 -0
  119. package/src/mesh/mesh-host-ownership.ts +73 -0
  120. package/src/mesh/mesh-ledger.ts +457 -104
  121. package/src/mesh/mesh-refine-status.ts +144 -0
  122. package/src/mesh/mesh-work-queue.ts +216 -158
  123. package/src/mesh/preview-freshness.ts +118 -0
  124. package/src/mesh/refine-config.ts +366 -0
  125. package/src/mesh/worktree-bootstrap-config.ts +247 -0
  126. package/src/providers/approval-utils.ts +39 -5
  127. package/src/providers/chat-message-normalization.ts +4 -11
  128. package/src/providers/cli-provider-instance.ts +380 -44
  129. package/src/providers/contracts.ts +52 -5
  130. package/src/providers/ide-provider-instance.ts +17 -3
  131. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  132. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  133. package/src/providers/native-history/codex-cli-transcript.ts +419 -0
  134. package/src/providers/native-history/index.ts +23 -0
  135. package/src/providers/provider-loader.ts +294 -28
  136. package/src/providers/provider-schema.ts +31 -13
  137. package/src/providers/read-chat-contract.ts +76 -16
  138. package/src/providers/sdk/README.md +49 -0
  139. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  140. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  141. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  142. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +245 -0
  143. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  144. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  145. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  146. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  147. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  148. package/src/providers/sdk/v1/index.ts +152 -0
  149. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  150. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  151. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  152. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +444 -0
  153. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  154. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  155. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  156. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  157. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  158. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  159. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  160. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  161. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +119 -0
  162. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  163. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  164. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  165. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  166. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  167. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  168. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  169. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  170. package/src/providers/sdk/v1/types/common/index.ts +210 -0
  171. package/src/providers/sdk/v1/validators/index.ts +19 -0
  172. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  173. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  174. package/src/providers/transcript-v2.ts +567 -0
  175. package/src/providers/version-archive.ts +38 -20
  176. package/src/repo-mesh-types.ts +77 -0
  177. package/src/shared-types.ts +9 -0
  178. package/src/status/builders.ts +23 -6
  179. package/src/status/reporter.ts +15 -0
  180. package/src/status/snapshot.ts +35 -11
  181. package/src/system/host-memory.ts +29 -12
  182. 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') {
@@ -486,14 +508,35 @@ export class CliProviderInstance implements ProviderInstance {
486
508
  }
487
509
 
488
510
  getState(): ProviderState {
511
+ // TODO(phase5-sandbox): JS override scripts (detectStatus, parseApproval,
512
+ // parseSession) are currently invoked by CliScriptRunner.invoke() via direct
513
+ // function calls — the scripts run in the daemon process with full Node.js
514
+ // access and no resource limits.
515
+ //
516
+ // When Phase 5 lands, CliScriptRunner should route these calls through a
517
+ // SandboxedScriptRunner (see providers/sdk/v1/sandbox/script-runner.ts) so
518
+ // that each call gets a fresh isolated-vm context with a 50 ms CPU limit and
519
+ // a 32 MB memory cap. The execution path to change is:
520
+ // CliScriptRunner.invoke() → SandboxedScriptRunner.run(scriptSource, context)
521
+ //
522
+ // This getState() call-site is NOT where the change goes — the wiring belongs
523
+ // in cli-script-runner.ts (CliScriptRunner.detectStatus / parseApproval /
524
+ // parseSession), with provider-loader.ts updated to store script source strings
525
+ // alongside the loaded function references for extended-legacy providers.
489
526
  const adapterStatus = this.adapter.getStatus();
490
527
  let parsedStatus: any = null;
491
528
  let parseErrorMessage: string | undefined;
492
529
  if (typeof this.adapter.getScriptParsedStatus === 'function') {
493
530
  try {
494
531
  parsedStatus = this.adapter.getScriptParsedStatus() || null;
495
- this.errorMessage = undefined;
496
- this.errorReason = undefined;
532
+ const parsedErrorMessage = typeof parsedStatus?.errorMessage === 'string' && parsedStatus.errorMessage.trim()
533
+ ? parsedStatus.errorMessage.trim()
534
+ : undefined;
535
+ const parsedErrorReason = typeof parsedStatus?.errorReason === 'string' && parsedStatus.errorReason.trim()
536
+ ? parsedStatus.errorReason.trim() as ProviderErrorReason
537
+ : undefined;
538
+ this.errorMessage = parsedErrorMessage;
539
+ this.errorReason = parsedErrorReason;
497
540
  } catch (error: any) {
498
541
  parseErrorMessage = error?.message || String(error);
499
542
  this.errorMessage = parseErrorMessage;
@@ -503,22 +546,39 @@ export class CliProviderInstance implements ProviderInstance {
503
546
  this.errorMessage = undefined;
504
547
  this.errorReason = undefined;
505
548
  }
549
+ const adapterProviderSessionId = normalizeProviderSessionId(
550
+ this.provider,
551
+ typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
552
+ );
506
553
  const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, Date.now());
507
- const visibleStatus = parseErrorMessage
554
+ const visibleStatus = parseErrorMessage || parsedStatus?.status === 'error'
508
555
  ? 'error'
509
556
  : (autoApproveActive ? 'generating' : adapterStatus.status);
557
+ const runtime = this.adapter.getRuntimeMetadata();
558
+ this.maybeAppendRuntimeRecoveryMessage(runtime);
559
+ let parsedMessages = Array.isArray(parsedStatus?.messages)
560
+ ? parsedStatus.messages
561
+ : [];
510
562
  const parsedProviderSessionId = normalizeProviderSessionId(
511
563
  this.provider,
512
564
  typeof parsedStatus?.providerSessionId === 'string' ? parsedStatus.providerSessionId : '',
513
565
  );
514
- if (parsedProviderSessionId) {
566
+ const suppressFreshLaunchStartupReplay = this.shouldSuppressFreshLaunchStartupReplay(
567
+ parsedMessages,
568
+ parsedStatus,
569
+ adapterStatus,
570
+ parsedProviderSessionId,
571
+ );
572
+ if (adapterProviderSessionId && !suppressFreshLaunchStartupReplay) {
573
+ this.promoteProviderSessionId(adapterProviderSessionId);
574
+ }
575
+ if (parsedProviderSessionId && !suppressFreshLaunchStartupReplay) {
515
576
  this.promoteProviderSessionId(parsedProviderSessionId);
516
577
  }
517
- const runtime = this.adapter.getRuntimeMetadata();
518
- this.maybeAppendRuntimeRecoveryMessage(runtime);
519
- let parsedMessages = Array.isArray(parsedStatus?.messages)
520
- ? parsedStatus.messages
521
- : [];
578
+ if (suppressFreshLaunchStartupReplay) {
579
+ parsedMessages = [];
580
+ }
581
+ const activeChatId = this.providerSessionId || runtime?.runtimeId || this.instanceId;
522
582
  const historyMessageCount = Number.isFinite(parsedStatus?.historyMessageCount)
523
583
  ? Math.max(0, Number(parsedStatus.historyMessageCount))
524
584
  : null;
@@ -528,7 +588,18 @@ export class CliProviderInstance implements ProviderInstance {
528
588
  : [];
529
589
  }
530
590
  const mergedMessages = this.mergeConversationMessages(parsedMessages);
531
- const canonicalBackedHistory = this.syncCanonicalSavedHistoryIfNeeded();
591
+ const canonicalBackedHistory = this.shouldHydrateExistingProviderHistory()
592
+ ? this.syncCanonicalSavedHistoryIfNeeded()
593
+ : false;
594
+ const statusMessages = canonicalBackedHistory && this.lastPersistedHistoryMessages.length > 0
595
+ ? this.lastPersistedHistoryMessages.map((message) => ({
596
+ role: message.role,
597
+ content: message.content,
598
+ kind: message.kind,
599
+ senderName: message.senderName,
600
+ receivedAt: message.receivedAt,
601
+ }))
602
+ : mergedMessages;
532
603
 
533
604
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
534
605
  const parsedChatStatus = typeof parsedStatus?.status === 'string' && parsedStatus.status.trim()
@@ -572,7 +643,12 @@ export class CliProviderInstance implements ProviderInstance {
572
643
  }
573
644
  }
574
645
 
575
- this.applyProviderResponse(parsedStatus, { phase: 'immediate' });
646
+ this.applyProviderResponse(
647
+ suppressFreshLaunchStartupReplay && parsedStatus && typeof parsedStatus === 'object'
648
+ ? { ...parsedStatus, providerSessionId: undefined }
649
+ : parsedStatus,
650
+ { phase: 'immediate' },
651
+ );
576
652
  const surface = resolveProviderStateSurface({
577
653
  summaryMetadata: this.summaryMetadata as any,
578
654
  controlValues: this.controlValues,
@@ -592,10 +668,10 @@ export class CliProviderInstance implements ProviderInstance {
592
668
  status: visibleStatus,
593
669
  mode: this.presentationMode,
594
670
  activeChat: {
595
- id: `${this.type}_${this.workingDir}`,
671
+ id: activeChatId,
596
672
  title: parsedStatus?.title || dirName,
597
673
  status: activeChatStatus,
598
- messages: mergedMessages,
674
+ messages: statusMessages,
599
675
  activeModal: autoApproveActive ? null : (parsedStatus?.activeModal ?? adapterStatus.activeModal),
600
676
  inputContent: '',
601
677
  },
@@ -695,6 +771,34 @@ export class CliProviderInstance implements ProviderInstance {
695
771
  }
696
772
  }
697
773
 
774
+ recordAcknowledgedUserInput(input: InputEnvelope | string): void {
775
+ const content = typeof input === 'string'
776
+ ? input.trim()
777
+ : buildCliStructuredInputPrompt(input).trim();
778
+ if (!content) return;
779
+
780
+ const receivedAt = Date.now();
781
+ const dedupKey = `user_input_ack:${crypto
782
+ .createHash('sha256')
783
+ .update(`${this.instanceId}:${content}:${receivedAt}`)
784
+ .digest('hex')
785
+ .slice(0, 24)}`;
786
+ this.appendRuntimeMessage(buildChatMessage({
787
+ role: 'user',
788
+ senderName: 'User',
789
+ kind: 'standard',
790
+ content,
791
+ receivedAt,
792
+ timestamp: receivedAt,
793
+ source: 'runtime_input_ack',
794
+ meta: {
795
+ runtimeInputAck: true,
796
+ provider: this.type,
797
+ workspace: this.workingDir,
798
+ },
799
+ } as ChatMessage), dedupKey);
800
+ }
801
+
698
802
  dispose(): void {
699
803
  this.adapter.shutdown();
700
804
  this.monitor.reset();
@@ -740,7 +844,60 @@ export class CliProviderInstance implements ProviderInstance {
740
844
  const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
741
845
  const role = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : '';
742
846
  const content = lastVisible ? flattenContent(lastVisible.content).trim() : '';
743
- return role === 'assistant' && !!content;
847
+ if (role !== 'assistant' || !content) return false;
848
+ // Guard: if the last assistant message looks like an active approval/input prompt,
849
+ // it is not a real completion — the session is still awaiting user input.
850
+ if (looksLikeActiveApprovalPromptText(content)) return false;
851
+ return true;
852
+ }
853
+
854
+ private buildCompletedFinalizationDiagnostic(args: {
855
+ blockReason: string;
856
+ latestStatus?: any;
857
+ latestVisibleStatus: string;
858
+ waitedMs: number;
859
+ pending: CompletedDebouncePending;
860
+ emittedAfterFinalizationTimeout: boolean;
861
+ }): Record<string, unknown> {
862
+ let parsed: any = null;
863
+ let parseError: string | undefined;
864
+ try {
865
+ parsed = this.adapter.getScriptParsedStatus();
866
+ } catch (error: any) {
867
+ parseError = error?.message || String(error);
868
+ }
869
+
870
+ const visibleMessages = (Array.isArray(parsed?.messages) ? parsed.messages : [])
871
+ .filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
872
+ const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
873
+ const lastVisibleRole = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null;
874
+ const lastVisibleKind = typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null;
875
+ const lastVisibleContentLength = lastVisible ? flattenContent(lastVisible.content).trim().length : 0;
876
+
877
+ return {
878
+ providerType: this.type,
879
+ sessionId: this.instanceId,
880
+ providerSessionId: this.providerSessionId || null,
881
+ workspace: this.workingDir,
882
+ blockReason: args.blockReason,
883
+ emittedAfterFinalizationTimeout: args.emittedAfterFinalizationTimeout,
884
+ waitedMs: args.waitedMs,
885
+ maxWaitMs: COMPLETED_FINALIZATION_MAX_WAIT_MS,
886
+ adapterStatus: typeof args.latestStatus?.status === 'string' ? args.latestStatus.status : null,
887
+ latestVisibleStatus: args.latestVisibleStatus,
888
+ parsedStatus: typeof parsed?.status === 'string' ? parsed.status : (parseError ? 'parse_error' : 'unknown'),
889
+ parseError: parseError || undefined,
890
+ finalAssistantPresent: this.completionHasFinalAssistantMessage(parsed?.messages),
891
+ visibleMessageCount: visibleMessages.length,
892
+ lastVisibleRole,
893
+ lastVisibleKind,
894
+ lastVisibleContentLength,
895
+ pendingStartedAt: this.generatingStartedAt || null,
896
+ pendingFirstObservedAt: args.pending.firstObservedAt,
897
+ pendingTimestamp: args.pending.timestamp,
898
+ pendingDurationSec: args.pending.duration,
899
+ previousBlockReason: args.pending.loggedBlockReason || null,
900
+ };
744
901
  }
745
902
 
746
903
  private hasAdapterPendingResponse(): boolean {
@@ -765,32 +922,62 @@ export class CliProviderInstance implements ProviderInstance {
765
922
  if (!isCliGeneratingLikeStatus(parsedRawStatus)) return false;
766
923
  if (adapterRawStatus !== 'idle') return false;
767
924
  if (hasNonEmptyCliModalButtons(parsedStatus?.activeModal ?? parsedStatus?.modal)) return false;
768
- return !this.hasAdapterPendingResponse();
925
+ if (this.hasAdapterPendingResponse()) return false;
926
+ // Do not suppress when the adapter's raw response buffer is still non-empty.
927
+ // This catches the case where isWaitingForResponse has already flipped to false
928
+ // (so getPartialResponse() returns '') but the provider's native parser still
929
+ // reports generating because it's parsing buffered content. Suppressing the
930
+ // finalization block here would emit a false completion event while the provider
931
+ // session is still actively processing its response stream.
932
+ const adapterAny = this.adapter as any;
933
+ if (typeof adapterAny?.responseBuffer === 'string' && adapterAny.responseBuffer.trim()) return false;
934
+ return true;
769
935
  }
770
936
 
771
- private getCompletedFinalizationBlockReason(latestVisibleStatus: string): string | null {
772
- if (latestVisibleStatus !== 'idle') return `status:${latestVisibleStatus}`;
937
+ private getCompletedFinalizationBlock(latestVisibleStatus: string): CompletedFinalizationBlock | null {
938
+ if (latestVisibleStatus !== 'idle') return { reason: `status:${latestVisibleStatus}`, terminal: true };
773
939
 
774
940
  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';
941
+ if (adapterAny?.isWaitingForResponse === true) return { reason: 'adapter_waiting_for_response', terminal: true };
942
+ if (adapterAny?.currentTurnScope) return { reason: 'adapter_turn_scope_active', terminal: true };
943
+ if (this.hasAdapterPendingResponse()) return { reason: 'adapter_pending_response', terminal: true };
777
944
 
778
945
  const partial = typeof this.adapter.getPartialResponse === 'function'
779
946
  ? this.adapter.getPartialResponse()
780
947
  : '';
781
- if (typeof partial === 'string' && partial.trim()) return 'partial_response_pending';
948
+ if (typeof partial === 'string' && partial.trim()) return { reason: 'partial_response_pending', terminal: true };
782
949
 
783
950
  let parsed: any;
784
951
  try {
785
952
  parsed = this.adapter.getScriptParsedStatus();
786
953
  } catch (error: any) {
787
- return `parse_error:${error?.message || String(error)}`;
954
+ return { reason: `parse_error:${error?.message || String(error)}` };
788
955
  }
789
956
 
790
957
  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';
958
+ if (parsedStatus !== 'idle') {
959
+ const adapterStatus = this.adapter.getStatus({ allowParse: false });
960
+ if (this.shouldSuppressStaleParsedBusyStatus(parsed, adapterStatus)) return null;
961
+ return { reason: `parsed_status:${parsedStatus}`, terminal: isCliGeneratingLikeStatus(parsedStatus) };
962
+ }
963
+ if (parsed?.activeModal || parsed?.modal) return { reason: 'parsed_modal_active', terminal: true };
964
+ if (!this.completionHasFinalAssistantMessage(parsed?.messages)) return { reason: 'missing_final_assistant' };
965
+
966
+ // Guard: if the screen still shows an approval/choice prompt as the last visible text,
967
+ // the turn is not complete even if the parsed status says idle and there is an assistant
968
+ // message. This catches the case where waiting_approval→idle transitions occur before
969
+ // the modal has been resolved (e.g. the PTY rendered the prompt but no button press fired).
970
+ try {
971
+ const screenText = typeof (this.adapter as any).getScreenText === 'function'
972
+ ? String((this.adapter as any).getScreenText() || '')
973
+ : '';
974
+ if (screenText) {
975
+ const tailLines = screenText.split(/\r?\n/).slice(-16).join('\n');
976
+ if (looksLikeActiveApprovalPromptText(tailLines)) {
977
+ return { reason: 'screen_shows_approval_prompt', terminal: false };
978
+ }
979
+ }
980
+ } catch { /* defensive: screen text read is best-effort */ }
794
981
 
795
982
  return null;
796
983
  }
@@ -817,10 +1004,11 @@ export class CliProviderInstance implements ProviderInstance {
817
1004
  return;
818
1005
  }
819
1006
 
820
- const blockReason = this.getCompletedFinalizationBlockReason(latestVisibleStatus);
821
- if (blockReason) {
1007
+ const block = this.getCompletedFinalizationBlock(latestVisibleStatus);
1008
+ if (block) {
1009
+ const blockReason = block.reason;
822
1010
  const waitedMs = Date.now() - pending.firstObservedAt;
823
- if (waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
1011
+ if (block.terminal || waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
824
1012
  if (pending.loggedBlockReason !== blockReason) {
825
1013
  LOG.info('CLI', `[${this.type}] waiting to emit completed until transcript finalizes (${blockReason})`);
826
1014
  pending.loggedBlockReason = blockReason;
@@ -828,7 +1016,25 @@ export class CliProviderInstance implements ProviderInstance {
828
1016
  this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
829
1017
  return;
830
1018
  }
831
- LOG.warn('CLI', `[${this.type}] suppressed completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
1019
+ const completionDiagnostic = this.buildCompletedFinalizationDiagnostic({
1020
+ blockReason,
1021
+ latestStatus,
1022
+ latestVisibleStatus,
1023
+ waitedMs,
1024
+ pending,
1025
+ emittedAfterFinalizationTimeout: true,
1026
+ });
1027
+ LOG.warn('CLI', `[${this.type}] emitting completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
1028
+ this.pushEvent({
1029
+ event: 'agent:generating_completed',
1030
+ chatTitle: pending.chatTitle,
1031
+ duration: pending.duration,
1032
+ timestamp: pending.timestamp,
1033
+ finalSummary: blockReason.startsWith('parsed_status:')
1034
+ ? ''
1035
+ : extractFinalSummaryFromMessages(this.adapter?.getScriptParsedStatus()?.messages),
1036
+ completionDiagnostic,
1037
+ });
832
1038
  this.completedDebouncePending = null;
833
1039
  this.completedDebounceTimer = null;
834
1040
  this.generatingStartedAt = 0;
@@ -853,15 +1059,46 @@ export class CliProviderInstance implements ProviderInstance {
853
1059
  // Guard re-entry: onStatusChange/getState can observe the same modal multiple
854
1060
  // times while the PTY absorbs the approval key. Without this flag, repeated
855
1061
  // snapshots would write stray keys into the input once the modal dismisses.
856
- if (autoApproveActive && !this.autoApproveBusy) {
1062
+ // However, Claude Code can present a second approval immediately after the
1063
+ // first. Resolve a changed modal signature even while the previous write is
1064
+ // still inside the short busy window.
1065
+ if (!autoApproveActive) {
1066
+ this.lastAutoApprovalSignature = '';
1067
+ return autoApproveActive;
1068
+ }
1069
+ const modal = adapterStatus.activeModal;
1070
+ // (fix) Do not auto-approve when no concrete modal/buttons are present.
1071
+ // Claude TUI flaps between paints; without this guard adapterStatus
1072
+ // could report status=waiting_approval with activeModal=null (or with
1073
+ // an empty buttons array briefly) and we'd still call
1074
+ // resolveModal(-1) — which used to type "1" into the prompt
1075
+ // repeatedly. Skip until a real modal is captured.
1076
+ const buttons = Array.isArray(modal?.buttons)
1077
+ ? modal.buttons.map((b: any) => String(b || '').trim()).filter(Boolean)
1078
+ : [];
1079
+ if (!modal || buttons.length === 0) {
1080
+ return autoApproveActive;
1081
+ }
1082
+ const { index: buttonIndex, label: buttonLabel } = pickApprovalButton(buttons, this.provider);
1083
+ if (buttonIndex < 0) {
1084
+ // No positive button matched — don't pick a random index, just
1085
+ // surface the modal so the user can decide.
1086
+ return autoApproveActive;
1087
+ }
1088
+ const signature = [
1089
+ typeof modal?.message === 'string' ? modal.message.trim() : '',
1090
+ buttons.join('|'),
1091
+ buttonIndex,
1092
+ ].join('::');
1093
+ if (!this.autoApproveBusy || signature !== this.lastAutoApprovalSignature) {
857
1094
  this.autoApproveBusy = true;
1095
+ this.lastAutoApprovalSignature = signature;
858
1096
  if (this.autoApproveBusyTimer) clearTimeout(this.autoApproveBusyTimer);
859
1097
  this.autoApproveBusyTimer = setTimeout(() => {
860
1098
  this.autoApproveBusy = false;
861
1099
  this.autoApproveBusyTimer = null;
1100
+ this.lastAutoApprovalSignature = '';
862
1101
  }, 2000);
863
- const modal = adapterStatus.activeModal;
864
- const { index: buttonIndex, label: buttonLabel } = pickApprovalButton(modal?.buttons, this.provider);
865
1102
  this.recordAutoApproval(modal?.message, buttonLabel, now);
866
1103
  setTimeout(() => {
867
1104
  this.adapter.resolveModal(buttonIndex);
@@ -876,6 +1113,13 @@ export class CliProviderInstance implements ProviderInstance {
876
1113
  // during long-running CLI sessions. Keep this path on adapter-owned light
877
1114
  // state only; rich provider parsing is reserved for getState/read_chat.
878
1115
  const adapterStatus = this.adapter.getStatus({ allowParse: false });
1116
+ const adapterProviderSessionId = normalizeProviderSessionId(
1117
+ this.provider,
1118
+ typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
1119
+ );
1120
+ if (adapterProviderSessionId) {
1121
+ this.promoteProviderSessionId(adapterProviderSessionId);
1122
+ }
879
1123
  const parsedStatus = null;
880
1124
  const rawStatus = adapterStatus.status;
881
1125
  const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, now);
@@ -943,12 +1187,30 @@ export class CliProviderInstance implements ProviderInstance {
943
1187
  }
944
1188
  } else if (newStatus === 'idle' && (this.lastStatus === 'generating' || this.lastStatus === 'waiting_approval')) {
945
1189
  const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : 0;
946
- // If debounce still pending (generating lasted < 1s), cancel both events
1190
+ // If debounce still pending (generating lasted < 1s), cancel both UI events.
1191
+ // Still emit agent:generating_completed so mesh orchestration can record
1192
+ // task_completed for direct dispatches that complete faster than the debounce.
947
1193
  if (this.generatingDebouncePending) {
948
- LOG.info('CLI', `[${this.type}] suppressed short generating (${now - this.generatingStartedAt}ms)`);
1194
+ const shortDurationMs = this.generatingStartedAt ? now - this.generatingStartedAt : 0;
1195
+ LOG.info('CLI', `[${this.type}] suppressed short generating (${shortDurationMs}ms)`);
949
1196
  if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
950
1197
  this.generatingDebouncePending = null;
951
1198
  this.generatingStartedAt = 0;
1199
+ // Emit completion for mesh task association even though the UI generating
1200
+ // started/completed pair is suppressed (too short for visible UI update).
1201
+ let shortFinalSummary: string | undefined;
1202
+ try { shortFinalSummary = extractFinalSummaryFromMessages(this.adapter?.getScriptParsedStatus()?.messages); } catch { /* best-effort */ }
1203
+ this.pushEvent({
1204
+ event: 'agent:generating_completed',
1205
+ chatTitle,
1206
+ duration: 0,
1207
+ timestamp: now,
1208
+ finalSummary: shortFinalSummary,
1209
+ completionDiagnostic: {
1210
+ reason: 'short_generating_suppressed',
1211
+ shortDurationMs,
1212
+ },
1213
+ });
952
1214
  } else {
953
1215
  // Debounce completed, then require the rich transcript path that read_chat
954
1216
  // uses to show an idle turn whose last user-facing message is assistant.
@@ -957,6 +1219,23 @@ export class CliProviderInstance implements ProviderInstance {
957
1219
  }
958
1220
  } else if (newStatus === 'idle' && this.lastStatus === 'starting') {
959
1221
  this.pushEvent({ event: 'agent:ready', chatTitle, timestamp: now });
1222
+ } else if (newStatus === 'error') {
1223
+ if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
1224
+ this.generatingDebouncePending = null;
1225
+ if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
1226
+ this.completedDebouncePending = null;
1227
+ this.errorMessage = adapterStatus.errorMessage || this.errorMessage;
1228
+ this.errorReason = (adapterStatus.errorReason as ProviderErrorReason) || this.errorReason;
1229
+ this.pushEvent({
1230
+ event: 'agent:stopped',
1231
+ chatTitle,
1232
+ timestamp: now,
1233
+ finalSummary: adapterStatus.errorMessage || adapterStatus.errorReason || 'Provider reported an error',
1234
+ completionDiagnostic: {
1235
+ reason: adapterStatus.errorReason || 'provider_error',
1236
+ errorMessage: adapterStatus.errorMessage || undefined,
1237
+ },
1238
+ });
960
1239
  } else if (newStatus === 'stopped') {
961
1240
  // Cancel any pending debounce
962
1241
  if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
@@ -977,7 +1256,31 @@ export class CliProviderInstance implements ProviderInstance {
977
1256
  // Monitor check (cooldown based notification, IDE/CLI common)
978
1257
  const agentKey = `${this.type}:cli`;
979
1258
  const monitorEvents = this.monitor.check(agentKey, newStatus, now, progressFingerprint);
1259
+ const monitorParsedStatus: any = parsedStatus;
980
1260
  for (const me of monitorEvents) {
1261
+ if (
1262
+ me.type === 'monitor:long_generating'
1263
+ && this.completionHasFinalAssistantMessage(monitorParsedStatus?.messages)
1264
+ && !this.hasAdapterPendingResponse()
1265
+ && !hasNonEmptyCliModalButtons(monitorParsedStatus?.activeModal ?? monitorParsedStatus?.modal)
1266
+ ) {
1267
+ this.pushEvent({
1268
+ event: 'agent:generating_completed',
1269
+ chatTitle,
1270
+ duration: this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : undefined,
1271
+ timestamp: me.timestamp,
1272
+ finalSummary: extractFinalSummaryFromMessages(monitorParsedStatus?.messages),
1273
+ completionDiagnostic: {
1274
+ providerType: this.type,
1275
+ sessionId: this.instanceId,
1276
+ providerSessionId: this.providerSessionId || null,
1277
+ reconciliationReason: 'long_generating_monitor_final_summary',
1278
+ finalAssistantPresent: true,
1279
+ },
1280
+ });
1281
+ this.generatingStartedAt = 0;
1282
+ continue;
1283
+ }
981
1284
  this.pushEvent({ event: me.type, agentKey: me.agentKey, message: me.message, elapsedSec: me.elapsedSec, timestamp: me.timestamp });
982
1285
  }
983
1286
  }
@@ -1258,12 +1561,28 @@ export class CliProviderInstance implements ProviderInstance {
1258
1561
  index,
1259
1562
  source: 'parsed',
1260
1563
  }));
1564
+ const getRole = (message: ChatMessage): string => typeof message.role === 'string'
1565
+ ? message.role.trim().toLowerCase()
1566
+ : '';
1261
1567
  const runtimeEntries: MergeEntry[] = this.runtimeMessages.map((entry, index) => ({
1262
1568
  message: entry.message,
1263
1569
  index: parsedMessages.length + index,
1264
- source: 'runtime',
1570
+ source: 'runtime' as const,
1265
1571
  runtimeKey: entry.key,
1266
- }));
1572
+ })).filter((entry) => {
1573
+ const meta = entry.message.meta && typeof entry.message.meta === 'object' && !Array.isArray(entry.message.meta)
1574
+ ? entry.message.meta as Record<string, unknown>
1575
+ : {};
1576
+ if (meta.runtimeInputAck !== true) return true;
1577
+ const runtimeText = flattenContent(entry.message.content).replace(/\s+/g, ' ').trim();
1578
+ if (!runtimeText) return false;
1579
+ return !parsedEntries.some((parsedEntry) => {
1580
+ const parsedRole = getRole(parsedEntry.message);
1581
+ if (parsedRole !== 'user' && parsedRole !== 'human') return false;
1582
+ const parsedText = flattenContent(parsedEntry.message.content).replace(/\s+/g, ' ').trim();
1583
+ return parsedText === runtimeText;
1584
+ });
1585
+ });
1267
1586
  const getTime = (message: ChatMessage): number => {
1268
1587
  const value = typeof message.receivedAt === 'number'
1269
1588
  ? message.receivedAt
@@ -1273,9 +1592,6 @@ export class CliProviderInstance implements ProviderInstance {
1273
1592
  return Number.isFinite(value) && value > 0 ? value : 0;
1274
1593
  };
1275
1594
 
1276
- const getRole = (message: ChatMessage): string => typeof message.role === 'string'
1277
- ? message.role.trim().toLowerCase()
1278
- : '';
1279
1595
  const isRuntimeOverlay = (entry: MergeEntry): boolean => {
1280
1596
  if (entry.source !== 'runtime') return false;
1281
1597
  const key = typeof entry.runtimeKey === 'string' ? entry.runtimeKey.trim().toLowerCase() : '';
@@ -1341,7 +1657,9 @@ export class CliProviderInstance implements ProviderInstance {
1341
1657
  this.providerSessionId = nextSessionId;
1342
1658
  this.historyWriter.promoteHistorySession(this.type, previousHistorySessionId, nextSessionId);
1343
1659
  this.historyWriter.writeSessionStart(this.type, nextSessionId, this.workingDir, this.instanceId);
1344
- this.restorePersistedHistoryFromCurrentSession();
1660
+ if (this.shouldHydrateExistingProviderHistory()) {
1661
+ this.restorePersistedHistoryFromCurrentSession();
1662
+ }
1345
1663
  this.adapter.updateRuntimeMeta({ providerSessionId: nextSessionId });
1346
1664
  this.onProviderSessionResolved?.({
1347
1665
  instanceId: this.instanceId,
@@ -1354,9 +1672,27 @@ export class CliProviderInstance implements ProviderInstance {
1354
1672
  LOG.info('CLI', `[${this.type}] discovered provider session id: ${nextSessionId}`);
1355
1673
  }
1356
1674
 
1675
+ private shouldHydrateExistingProviderHistory(): boolean {
1676
+ return this.launchMode === 'resume' || this.launchMode === 'manual';
1677
+ }
1678
+
1679
+ private shouldSuppressFreshLaunchStartupReplay(parsedMessages: unknown[], parsedStatus: any, adapterStatus: any, parsedProviderSessionId = ''): boolean {
1680
+ if (this.launchMode !== 'new') return false;
1681
+ if (this.providerSessionId) return false;
1682
+ if (!Array.isArray(parsedMessages) || parsedMessages.length === 0) return false;
1683
+ if (!isIdleStatus(adapterStatus?.status) || !isIdleStatus(parsedStatus?.status)) return false;
1684
+ if (parsedProviderSessionId) return true;
1685
+
1686
+ const newestMessageAt = parsedMessages.reduce<number>((newest, message) => Math.max(newest, getMessageTime(message)), 0);
1687
+
1688
+ // Untimestamped idle parser output during a fresh launch is usually the
1689
+ // provider's last workspace transcript before a new turn exists.
1690
+ return newestMessageAt === 0;
1691
+ }
1692
+
1357
1693
  private syncCanonicalSavedHistoryIfNeeded(): boolean {
1358
1694
  if (!this.providerSessionId) return false;
1359
- const canonicalHistory = this.provider.canonicalHistory;
1695
+ const canonicalHistory = this.provider.nativeHistory;
1360
1696
  if (!canonicalHistory) return false;
1361
1697
 
1362
1698
  if (isNativeSourceCanonicalHistory(canonicalHistory)) {
@@ -1418,9 +1754,9 @@ export class CliProviderInstance implements ProviderInstance {
1418
1754
  private restorePersistedHistoryFromCurrentSession(): void {
1419
1755
  if (!this.providerSessionId) return;
1420
1756
  this.syncCanonicalSavedHistoryIfNeeded();
1421
- const restoredHistory = isNativeSourceCanonicalHistory(this.provider.canonicalHistory)
1757
+ const restoredHistory = isNativeSourceCanonicalHistory(this.provider.nativeHistory)
1422
1758
  ? readProviderChatHistory(this.type, {
1423
- canonicalHistory: this.provider.canonicalHistory,
1759
+ canonicalHistory: this.provider.nativeHistory,
1424
1760
  historySessionId: this.providerSessionId,
1425
1761
  workspace: this.workingDir,
1426
1762
  offset: 0,