@adhdev/daemon-core 0.9.82-rc.18 → 0.9.82-rc.181

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 (225) 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 +22 -2
  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 +106 -63
  9. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  10. package/dist/cli-adapters/provider-cli-shared.d.ts +29 -0
  11. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  12. package/dist/commands/handler.d.ts +110 -0
  13. package/dist/commands/mesh-coordinator.d.ts +72 -1
  14. package/dist/commands/router.d.ts +22 -0
  15. package/dist/config/chat-history.d.ts +8 -0
  16. package/dist/config/config.d.ts +5 -0
  17. package/dist/config/mesh-config.d.ts +72 -1
  18. package/dist/git/git-commands.d.ts +5 -1
  19. package/dist/index.d.ts +35 -8
  20. package/dist/index.js +20754 -5387
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +20567 -5270
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/installer.d.ts +1 -4
  25. package/dist/ipc/local-ipc-server.d.ts +91 -0
  26. package/dist/launch.d.ts +1 -1
  27. package/dist/logging/async-batch-writer.d.ts +10 -0
  28. package/dist/mesh/beads-db.d.ts +72 -0
  29. package/dist/mesh/contracts.d.ts +164 -0
  30. package/dist/mesh/coordinator-prompt.d.ts +30 -0
  31. package/dist/mesh/coordinator-registry.d.ts +59 -0
  32. package/dist/mesh/mesh-active-work.d.ts +90 -0
  33. package/dist/mesh/mesh-events.d.ts +97 -5
  34. package/dist/mesh/mesh-fast-forward.d.ts +41 -0
  35. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  36. package/dist/mesh/mesh-ledger.d.ts +58 -1
  37. package/dist/mesh/mesh-refine-status.d.ts +26 -0
  38. package/dist/mesh/mesh-work-queue.d.ts +54 -6
  39. package/dist/mesh/preview-freshness.d.ts +18 -0
  40. package/dist/mesh/refine-config.d.ts +193 -0
  41. package/dist/mesh/worktree-bootstrap-config.d.ts +113 -0
  42. package/dist/providers/approval-utils.d.ts +13 -0
  43. package/dist/providers/cli-provider-instance.d.ts +35 -3
  44. package/dist/providers/contracts.d.ts +98 -5
  45. package/dist/providers/external-sources.d.ts +71 -0
  46. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  47. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  48. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  49. package/dist/providers/native-history/dispatcher.d.ts +25 -0
  50. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  51. package/dist/providers/native-history/index.d.ts +13 -0
  52. package/dist/providers/provider-instance-manager.d.ts +12 -0
  53. package/dist/providers/provider-instance.d.ts +12 -0
  54. package/dist/providers/provider-loader.d.ts +26 -4
  55. package/dist/providers/provider-trust.d.ts +31 -0
  56. package/dist/providers/read-chat-contract.d.ts +29 -0
  57. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  58. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  59. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  60. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  61. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  62. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  63. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  64. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  65. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  66. package/dist/providers/sdk/v1/index.d.ts +30 -0
  67. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  68. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  69. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  70. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  71. package/dist/providers/sdk/v1/types/common/index.d.ts +169 -0
  72. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  73. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  74. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  75. package/dist/providers/spec/adapter.d.ts +56 -0
  76. package/dist/providers/spec/cli-adapter.d.ts +97 -0
  77. package/dist/providers/spec/driver.d.ts +159 -0
  78. package/dist/providers/spec/evaluator.d.ts +47 -0
  79. package/dist/providers/spec/loader.d.ts +14 -0
  80. package/dist/providers/spec/native-history-executor.d.ts +39 -0
  81. package/dist/providers/spec/route.d.ts +4 -0
  82. package/dist/providers/spec/schema.gen.d.ts +511 -0
  83. package/dist/providers/spec/types.d.ts +212 -0
  84. package/dist/providers/transcript-v2.d.ts +176 -0
  85. package/dist/providers/types/interactive-prompt.d.ts +48 -0
  86. package/dist/repo-mesh-types.d.ts +102 -1
  87. package/dist/sessions/registry.d.ts +3 -0
  88. package/dist/shared-types.d.ts +42 -1
  89. package/dist/status/reporter.d.ts +2 -0
  90. package/dist/status/snapshot.d.ts +1 -0
  91. package/dist/types.d.ts +5 -0
  92. package/package.json +7 -2
  93. package/src/agent-stream/poller.ts +2 -3
  94. package/src/boot/daemon-lifecycle.ts +17 -10
  95. package/src/boot/process-hardening.ts +89 -0
  96. package/src/chat/source-machine.ts +534 -0
  97. package/src/chat/source-resolver.ts +0 -0
  98. package/src/chat/subscription-updates.ts +14 -1
  99. package/src/cli-adapter-types.d.ts +1 -0
  100. package/src/cli-adapter-types.ts +21 -2
  101. package/src/cli-adapters/cli-script-runner.ts +421 -0
  102. package/src/cli-adapters/cli-state-engine.ts +1086 -0
  103. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  104. package/src/cli-adapters/provider-cli-adapter.ts +670 -1137
  105. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  106. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  107. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  108. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  109. package/src/cli-adapters/provider-cli-shared.ts +59 -11
  110. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  111. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
  112. package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
  113. package/src/commands/chat-commands.ts +1597 -60
  114. package/src/commands/cli-manager.ts +230 -3
  115. package/src/commands/handler.ts +809 -2
  116. package/src/commands/mesh-coordinator.ts +231 -127
  117. package/src/commands/router.ts +3495 -502
  118. package/src/config/chat-history.ts +91 -24
  119. package/src/config/config.ts +12 -0
  120. package/src/config/mesh-config.ts +280 -2
  121. package/src/config/recent-activity.ts +8 -2
  122. package/src/daemon/dev-cli-debug.ts +10 -1
  123. package/src/detection/ide-detector.ts +26 -16
  124. package/src/git/git-commands.ts +17 -5
  125. package/src/git/git-worktree.ts +8 -1
  126. package/src/index.ts +113 -7
  127. package/src/installer.d.ts +1 -1
  128. package/src/installer.ts +8 -6
  129. package/src/ipc/local-ipc-server.ts +278 -0
  130. package/src/launch.d.ts +1 -1
  131. package/src/launch.ts +37 -28
  132. package/src/logging/async-batch-writer.ts +55 -0
  133. package/src/logging/logger.ts +2 -1
  134. package/src/mesh/beads-db.ts +486 -0
  135. package/src/mesh/contracts.ts +329 -0
  136. package/src/mesh/coordinator-prompt.ts +204 -30
  137. package/src/mesh/coordinator-registry.ts +121 -0
  138. package/src/mesh/mesh-active-work.ts +437 -0
  139. package/src/mesh/mesh-events.ts +963 -67
  140. package/src/mesh/mesh-fast-forward.ts +438 -0
  141. package/src/mesh/mesh-host-ownership.ts +73 -0
  142. package/src/mesh/mesh-ledger.ts +457 -104
  143. package/src/mesh/mesh-refine-status.ts +144 -0
  144. package/src/mesh/mesh-work-queue.ts +263 -159
  145. package/src/mesh/preview-freshness.ts +118 -0
  146. package/src/mesh/refine-config.ts +366 -0
  147. package/src/mesh/worktree-bootstrap-config.ts +247 -0
  148. package/src/providers/approval-utils.d.ts +4 -0
  149. package/src/providers/approval-utils.ts +47 -5
  150. package/src/providers/chat-message-normalization.ts +4 -11
  151. package/src/providers/cli-provider-instance.ts +651 -57
  152. package/src/providers/contracts.ts +105 -5
  153. package/src/providers/external-sources.ts +218 -0
  154. package/src/providers/ide-provider-instance.ts +19 -5
  155. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  156. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  157. package/src/providers/native-history/codex-cli-transcript.ts +419 -0
  158. package/src/providers/native-history/dispatcher.ts +231 -0
  159. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  160. package/src/providers/native-history/index.ts +30 -0
  161. package/src/providers/provider-instance-manager.ts +30 -0
  162. package/src/providers/provider-instance.ts +9 -0
  163. package/src/providers/provider-loader.ts +585 -50
  164. package/src/providers/provider-schema.ts +31 -13
  165. package/src/providers/provider-trust.ts +114 -0
  166. package/src/providers/read-chat-contract.ts +76 -16
  167. package/src/providers/sdk/README.md +49 -0
  168. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  169. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  170. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  171. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  172. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  173. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  174. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  175. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  176. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  177. package/src/providers/sdk/v1/index.ts +152 -0
  178. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  179. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  180. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  181. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +458 -0
  182. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  183. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  184. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  185. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  186. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  187. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  188. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  189. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  190. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  191. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  192. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  193. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  194. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  195. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  196. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  197. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  198. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  199. package/src/providers/sdk/v1/types/common/index.ts +210 -0
  200. package/src/providers/sdk/v1/validators/index.ts +19 -0
  201. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  202. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  203. package/src/providers/spec/adapter.ts +168 -0
  204. package/src/providers/spec/cli-adapter.ts +507 -0
  205. package/src/providers/spec/driver.ts +540 -0
  206. package/src/providers/spec/evaluator.ts +298 -0
  207. package/src/providers/spec/loader.ts +130 -0
  208. package/src/providers/spec/native-history-executor.ts +706 -0
  209. package/src/providers/spec/route.ts +51 -0
  210. package/src/providers/spec/schema.gen.ts +511 -0
  211. package/src/providers/spec/schema.json +211 -0
  212. package/src/providers/spec/types.ts +231 -0
  213. package/src/providers/transcript-v2.ts +567 -0
  214. package/src/providers/types/interactive-prompt.ts +396 -0
  215. package/src/providers/version-archive.ts +38 -20
  216. package/src/repo-mesh-types.ts +112 -1
  217. package/src/sessions/registry.ts +3 -0
  218. package/src/shared-types.ts +45 -1
  219. package/src/status/builders.ts +24 -6
  220. package/src/status/reporter.ts +15 -0
  221. package/src/status/snapshot.ts +84 -25
  222. package/src/system/host-memory.ts +29 -12
  223. package/src/types.ts +5 -0
  224. package/dist/mesh/mesh-sync.d.ts +0 -53
  225. package/src/mesh/mesh-sync.ts +0 -111
@@ -13,15 +13,17 @@ import { createRequire } from 'node:module';
13
13
  import { normalizeInputEnvelope, type ProviderModule, flattenContent, type InputEnvelope, type InputPart } from './contracts.js';
14
14
  import { assertProviderSupportsDeclaredInput, getEffectiveMessageInputSupport } from './provider-input-support.js';
15
15
  import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext, ProviderErrorReason, HotChatSessionState, SessionModalState } from './provider-instance.js';
16
+ import { normalizeInteractivePrompt, normalizeInteractivePromptResponse, type InteractivePrompt } from './types/interactive-prompt.js';
16
17
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
17
18
  import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
19
+ import { createCliAdapter } from './spec/route.js';
18
20
  import type { PtyRuntimeMetadata, PtyTransportFactory } from '../cli-adapters/pty-transport.js';
19
21
  import { StatusMonitor } from './status-monitor.js';
20
22
  import { ChatHistoryWriter, isNativeSourceCanonicalHistory, materializeProviderNativeHistory, readChatHistory, readProviderChatHistory } from '../config/chat-history.js';
21
23
  import { LOG } from '../logging/logger.js';
22
24
  import type { ChatMessage } from '../types.js';
23
25
  import { buildPersistedProviderEffectMessage, normalizeProviderEffects } from './control-effects.js';
24
- import { formatAutoApprovalMessage, pickApprovalButton } from './approval-utils.js';
26
+ import { formatAutoApprovalMessage, pickApprovalButton, pickAutoApprovalButton, looksLikeActiveApprovalPromptText } from './approval-utils.js';
25
27
  import { getCliScriptCommand, parseCliScriptResult } from './cli-script-results.js';
26
28
  import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
27
29
  import { normalizeProviderSessionId } from './provider-session-id.js';
@@ -40,12 +42,59 @@ type CompletedDebouncePending = {
40
42
  duration: number;
41
43
  timestamp: number;
42
44
  firstObservedAt: number;
45
+ previousStatus: string;
43
46
  loggedBlockReason?: string;
47
+ loggedTranscriptProbe?: boolean;
48
+ transcriptProbeHistory?: ExternalTranscriptProbe[];
49
+ };
50
+
51
+ function isIdleStatus(value: unknown): boolean {
52
+ const status = typeof value === 'string' ? value.trim().toLowerCase() : '';
53
+ return !status || status === 'idle' || status === 'ready';
54
+ }
55
+
56
+
57
+ function getMessageTime(message: unknown): number {
58
+ if (!message || typeof message !== 'object') return 0;
59
+ const record = message as { receivedAt?: unknown; timestamp?: unknown };
60
+ const value = Number(record.receivedAt ?? record.timestamp ?? 0);
61
+ return Number.isFinite(value) ? value : 0;
62
+ }
63
+
64
+ type CompletedFinalizationBlock = {
65
+ reason: string;
66
+ terminal?: boolean;
67
+ };
68
+
69
+ type CompletionFinalAssistantEvidence = {
70
+ present: boolean;
71
+ messages: unknown[];
72
+ source: 'parsed' | 'external-native' | 'unavailable';
73
+ };
74
+
75
+ type ExternalTranscriptProbe = {
76
+ readAt: number;
77
+ msgCount: number;
78
+ lastRole: string | null;
79
+ lastKind: string | null;
80
+ contentLen: number;
81
+ sourcePath: string | null;
82
+ sourceMtimeMs: number | null;
83
+ mtimeAgeMs: number | null;
44
84
  };
45
85
 
46
86
  const COMPLETED_FINALIZATION_RETRY_MS = 1000;
47
87
  const COMPLETED_FINALIZATION_MAX_WAIT_MS = 30_000;
48
88
 
89
+ /** Events that signal a dispatched mesh task has reached a terminal state.
90
+ * Detach the mesh assignment after emitting one of these so the worker's
91
+ * next unrelated turn doesn't impersonate another completion. */
92
+ const TERMINAL_MESH_EVENTS = new Set([
93
+ 'agent:generating_completed',
94
+ 'agent:stopped',
95
+ 'agent:ready',
96
+ ]);
97
+
49
98
  const IMAGE_MIME_EXTENSIONS: Record<string, string> = {
50
99
  'image/png': '.png',
51
100
  'image/jpeg': '.jpg',
@@ -309,9 +358,10 @@ export class CliProviderInstance implements ProviderInstance {
309
358
  private monitor: StatusMonitor;
310
359
  private generatingDebounceTimer: NodeJS.Timeout | null = null;
311
360
  private generatingDebouncePending: { chatTitle: string; timestamp: number } | null = null;
312
- private lastApprovalEventAt = 0;
361
+ private lastApprovalEventFingerprint = '';
313
362
  private autoApproveBusy = false;
314
363
  private autoApproveBusyTimer: NodeJS.Timeout | null = null;
364
+ private lastAutoApprovalSignature = '';
315
365
  private controlValues: Record<string, string | number | boolean> = {};
316
366
  private summaryMetadata: unknown = undefined;
317
367
  private appliedEffectKeys = new Set<string>();
@@ -330,6 +380,7 @@ export class CliProviderInstance implements ProviderInstance {
330
380
  private suppressIdleHistoryReplay = false;
331
381
  private errorMessage: string | undefined = undefined;
332
382
  private errorReason: ProviderErrorReason | undefined = undefined;
383
+ private activeInteractivePrompt: InteractivePrompt | null = null;
333
384
 
334
385
  private presentationMode: 'terminal' | 'chat';
335
386
  private providerSessionId?: string;
@@ -370,7 +421,10 @@ export class CliProviderInstance implements ProviderInstance {
370
421
  this.providerSessionId = options?.providerSessionId;
371
422
  this.launchMode = options?.launchMode || 'new';
372
423
  this.onProviderSessionResolved = options?.onProviderSessionResolved;
373
- this.adapter = new ProviderCliAdapter(provider as CliProviderModule, workingDir, cliArgs, options?.extraEnv || {}, transportFactory);
424
+ this.adapter = createCliAdapter(provider as CliProviderModule, workingDir, cliArgs, options?.extraEnv || {}, transportFactory) as ProviderCliAdapter;
425
+ if (this.providerSessionId) {
426
+ this.adapter.updateRuntimeMeta({ providerSessionId: this.providerSessionId });
427
+ }
374
428
  this.monitor = new StatusMonitor();
375
429
  this.historyWriter = new ChatHistoryWriter();
376
430
  }
@@ -412,7 +466,7 @@ export class CliProviderInstance implements ProviderInstance {
412
466
  await this.adapter.spawn();
413
467
  await this.enforceFreshSessionLaunchIfNeeded();
414
468
  this.maybeAppendRuntimeRecoveryMessage(this.adapter.getRuntimeMetadata());
415
- if (this.providerSessionId) {
469
+ if (this.providerSessionId && this.shouldHydrateExistingProviderHistory()) {
416
470
  this.restorePersistedHistoryFromCurrentSession();
417
471
  }
418
472
  if (this.providerSessionId && this.launchMode === 'resume') {
@@ -486,14 +540,38 @@ export class CliProviderInstance implements ProviderInstance {
486
540
  }
487
541
 
488
542
  getState(): ProviderState {
543
+ // TODO(phase5-sandbox): JS override scripts (detectStatus, parseApproval,
544
+ // parseSession) are currently invoked by CliScriptRunner.invoke() via direct
545
+ // function calls — the scripts run in the daemon process with full Node.js
546
+ // access and no resource limits.
547
+ //
548
+ // When Phase 5 lands, CliScriptRunner should route these calls through a
549
+ // SandboxedScriptRunner (see providers/sdk/v1/sandbox/script-runner.ts) so
550
+ // that each call gets a fresh isolated-vm context with a 50 ms CPU limit and
551
+ // a 32 MB memory cap. The execution path to change is:
552
+ // CliScriptRunner.invoke() → SandboxedScriptRunner.run(scriptSource, context)
553
+ //
554
+ // This getState() call-site is NOT where the change goes — the wiring belongs
555
+ // in cli-script-runner.ts (CliScriptRunner.detectStatus / parseApproval /
556
+ // parseSession), with provider-loader.ts updated to store script source strings
557
+ // alongside the loaded function references for extended-legacy providers.
489
558
  const adapterStatus = this.adapter.getStatus();
559
+ if (Object.prototype.hasOwnProperty.call(adapterStatus, 'activeInteractivePrompt')) {
560
+ this.activeInteractivePrompt = adapterStatus.activeInteractivePrompt ?? null;
561
+ }
490
562
  let parsedStatus: any = null;
491
563
  let parseErrorMessage: string | undefined;
492
564
  if (typeof this.adapter.getScriptParsedStatus === 'function') {
493
565
  try {
494
566
  parsedStatus = this.adapter.getScriptParsedStatus() || null;
495
- this.errorMessage = undefined;
496
- this.errorReason = undefined;
567
+ const parsedErrorMessage = typeof parsedStatus?.errorMessage === 'string' && parsedStatus.errorMessage.trim()
568
+ ? parsedStatus.errorMessage.trim()
569
+ : undefined;
570
+ const parsedErrorReason = typeof parsedStatus?.errorReason === 'string' && parsedStatus.errorReason.trim()
571
+ ? parsedStatus.errorReason.trim() as ProviderErrorReason
572
+ : undefined;
573
+ this.errorMessage = parsedErrorMessage;
574
+ this.errorReason = parsedErrorReason;
497
575
  } catch (error: any) {
498
576
  parseErrorMessage = error?.message || String(error);
499
577
  this.errorMessage = parseErrorMessage;
@@ -503,22 +581,39 @@ export class CliProviderInstance implements ProviderInstance {
503
581
  this.errorMessage = undefined;
504
582
  this.errorReason = undefined;
505
583
  }
584
+ const adapterProviderSessionId = normalizeProviderSessionId(
585
+ this.provider,
586
+ typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
587
+ );
506
588
  const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, Date.now());
507
- const visibleStatus = parseErrorMessage
589
+ const visibleStatus = parseErrorMessage || parsedStatus?.status === 'error'
508
590
  ? 'error'
509
591
  : (autoApproveActive ? 'generating' : adapterStatus.status);
592
+ const runtime = this.adapter.getRuntimeMetadata();
593
+ this.maybeAppendRuntimeRecoveryMessage(runtime);
594
+ let parsedMessages = Array.isArray(parsedStatus?.messages)
595
+ ? parsedStatus.messages
596
+ : [];
510
597
  const parsedProviderSessionId = normalizeProviderSessionId(
511
598
  this.provider,
512
599
  typeof parsedStatus?.providerSessionId === 'string' ? parsedStatus.providerSessionId : '',
513
600
  );
514
- if (parsedProviderSessionId) {
601
+ const suppressFreshLaunchStartupReplay = this.shouldSuppressFreshLaunchStartupReplay(
602
+ parsedMessages,
603
+ parsedStatus,
604
+ adapterStatus,
605
+ parsedProviderSessionId,
606
+ );
607
+ if (adapterProviderSessionId && !suppressFreshLaunchStartupReplay) {
608
+ this.promoteProviderSessionId(adapterProviderSessionId);
609
+ }
610
+ if (parsedProviderSessionId && !suppressFreshLaunchStartupReplay) {
515
611
  this.promoteProviderSessionId(parsedProviderSessionId);
516
612
  }
517
- const runtime = this.adapter.getRuntimeMetadata();
518
- this.maybeAppendRuntimeRecoveryMessage(runtime);
519
- let parsedMessages = Array.isArray(parsedStatus?.messages)
520
- ? parsedStatus.messages
521
- : [];
613
+ if (suppressFreshLaunchStartupReplay) {
614
+ parsedMessages = [];
615
+ }
616
+ const activeChatId = this.providerSessionId || runtime?.runtimeId || this.instanceId;
522
617
  const historyMessageCount = Number.isFinite(parsedStatus?.historyMessageCount)
523
618
  ? Math.max(0, Number(parsedStatus.historyMessageCount))
524
619
  : null;
@@ -528,7 +623,18 @@ export class CliProviderInstance implements ProviderInstance {
528
623
  : [];
529
624
  }
530
625
  const mergedMessages = this.mergeConversationMessages(parsedMessages);
531
- const canonicalBackedHistory = this.syncCanonicalSavedHistoryIfNeeded();
626
+ const canonicalBackedHistory = this.shouldHydrateExistingProviderHistory()
627
+ ? this.syncCanonicalSavedHistoryIfNeeded()
628
+ : false;
629
+ const statusMessages = canonicalBackedHistory && this.lastPersistedHistoryMessages.length > 0
630
+ ? this.lastPersistedHistoryMessages.map((message) => ({
631
+ role: message.role,
632
+ content: message.content,
633
+ kind: message.kind,
634
+ senderName: message.senderName,
635
+ receivedAt: message.receivedAt,
636
+ }))
637
+ : mergedMessages;
532
638
 
533
639
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
534
640
  const parsedChatStatus = typeof parsedStatus?.status === 'string' && parsedStatus.status.trim()
@@ -572,7 +678,12 @@ export class CliProviderInstance implements ProviderInstance {
572
678
  }
573
679
  }
574
680
 
575
- this.applyProviderResponse(parsedStatus, { phase: 'immediate' });
681
+ this.applyProviderResponse(
682
+ suppressFreshLaunchStartupReplay && parsedStatus && typeof parsedStatus === 'object'
683
+ ? { ...parsedStatus, providerSessionId: undefined }
684
+ : parsedStatus,
685
+ { phase: 'immediate' },
686
+ );
576
687
  const surface = resolveProviderStateSurface({
577
688
  summaryMetadata: this.summaryMetadata as any,
578
689
  controlValues: this.controlValues,
@@ -592,13 +703,15 @@ export class CliProviderInstance implements ProviderInstance {
592
703
  status: visibleStatus,
593
704
  mode: this.presentationMode,
594
705
  activeChat: {
595
- id: `${this.type}_${this.workingDir}`,
706
+ id: activeChatId,
596
707
  title: parsedStatus?.title || dirName,
597
708
  status: activeChatStatus,
598
- messages: mergedMessages,
709
+ messages: statusMessages,
599
710
  activeModal: autoApproveActive ? null : (parsedStatus?.activeModal ?? adapterStatus.activeModal),
711
+ activeInteractivePrompt: this.activeInteractivePrompt,
600
712
  inputContent: '',
601
713
  },
714
+ activeInteractivePrompt: this.activeInteractivePrompt,
602
715
  workspace: this.workingDir,
603
716
  instanceId: this.instanceId,
604
717
  providerSessionId: this.providerSessionId,
@@ -651,13 +764,17 @@ export class CliProviderInstance implements ProviderInstance {
651
764
  };
652
765
  }
653
766
 
654
- getSessionModalState(): SessionModalState {
655
- const adapterStatus = this.adapter.getStatus({ allowParse: false });
767
+ getSessionModalState(sessionId?: string): SessionModalState {
768
+ const adapterStatus = this.adapter.getStatus({ allowParse: true });
656
769
  const autoApproveActive = adapterStatus.status === 'waiting_approval' && this.shouldAutoApprove();
657
770
  const visibleStatus = autoApproveActive ? 'generating' : adapterStatus.status;
658
771
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
659
772
  return {
660
- id: this.instanceId,
773
+ // Honor the caller-supplied sessionId — InstanceMgr rejects the
774
+ // projection when projected.id !== requested sessionId, and
775
+ // this.instanceId is the manager's internal key, not the public
776
+ // sessionId the dashboard subscribes by.
777
+ id: sessionId ?? this.instanceId,
661
778
  status: visibleStatus,
662
779
  title: dirName,
663
780
  activeModal: autoApproveActive ? null : adapterStatus.activeModal,
@@ -674,6 +791,39 @@ export class CliProviderInstance implements ProviderInstance {
674
791
  });
675
792
  }
676
793
 
794
+ /**
795
+ * Stamp a direct-dispatch mesh assignment on this instance.
796
+ * setupMeshEventForwarding reads settings.meshNodeFor + meshActiveTaskId to
797
+ * route generating_completed back to the originating coordinator. Without
798
+ * this stamp, mesh_send_task --direct targets a plain CLI session whose
799
+ * completion events silently drop because the forwarder has nothing to
800
+ * match against.
801
+ */
802
+ attachMeshAssignment(assignment: { meshId: string; nodeId?: string; taskId?: string }): void {
803
+ if (!assignment?.meshId) return;
804
+ this.settings = {
805
+ ...this.settings,
806
+ meshNodeFor: assignment.meshId,
807
+ ...(assignment.nodeId ? { meshNodeId: assignment.nodeId } : {}),
808
+ ...(assignment.taskId ? { meshActiveTaskId: assignment.taskId } : {}),
809
+ };
810
+ this.adapter.updateRuntimeSettings?.(this.settings);
811
+ }
812
+
813
+ /**
814
+ * Clear a previously-attached mesh assignment after the task reaches a
815
+ * terminal state. Leaving meshNodeFor pinned would route this session's
816
+ * subsequent unrelated turns (e.g. ad-hoc dashboard chats) to the
817
+ * coordinator as if they were task completions.
818
+ */
819
+ detachMeshAssignment(): void {
820
+ if (!this.settings.meshNodeFor && !this.settings.meshActiveTaskId && !this.settings.meshNodeId) return;
821
+ const { meshNodeFor, meshNodeId, meshActiveTaskId, ...rest } = this.settings;
822
+ void meshNodeFor; void meshNodeId; void meshActiveTaskId;
823
+ this.settings = rest;
824
+ this.adapter.updateRuntimeSettings?.(this.settings);
825
+ }
826
+
677
827
  onEvent(event: string, data?: any): void {
678
828
  if (event === 'send_message') {
679
829
  const input = normalizeInputEnvelope(data);
@@ -690,11 +840,65 @@ export class CliProviderInstance implements ProviderInstance {
690
840
  void this.adapter.resolveAction(data).catch((e: any) => {
691
841
  LOG.warn('CLI', `[${this.type}] resolve_action failed: ${e?.message || e}`);
692
842
  });
843
+ } else if (event === 'interactive_prompt' && data) {
844
+ const prompt = normalizeInteractivePrompt(data);
845
+ if (prompt) {
846
+ this.activeInteractivePrompt = prompt;
847
+ this.events.push({
848
+ event: 'interactive_prompt',
849
+ timestamp: Date.now(),
850
+ promptId: prompt.promptId,
851
+ });
852
+ }
853
+ } else if (event === 'interactive_prompt_response' && data) {
854
+ try {
855
+ const response = normalizeInteractivePromptResponse(data);
856
+ if (this.activeInteractivePrompt?.promptId === response.promptId) {
857
+ this.activeInteractivePrompt = null;
858
+ }
859
+ if (typeof this.adapter.setInteractivePromptResponse !== 'function') {
860
+ LOG.warn('CLI', `[${this.type}] interactive_prompt_response ignored: adapter does not support interactive prompts`);
861
+ return;
862
+ }
863
+ void this.adapter.setInteractivePromptResponse(response).catch((e: any) => {
864
+ LOG.warn('CLI', `[${this.type}] interactive_prompt_response failed: ${e?.message || e}`);
865
+ });
866
+ } catch (e: any) {
867
+ LOG.warn('CLI', `[${this.type}] invalid interactive_prompt_response: ${e?.message || e}`);
868
+ }
693
869
  } else if (event === 'provider_state_patch' && data && typeof data === 'object') {
694
870
  this.applyProviderResponse(data, { phase: 'immediate' });
695
871
  }
696
872
  }
697
873
 
874
+ recordAcknowledgedUserInput(input: InputEnvelope | string): void {
875
+ const content = typeof input === 'string'
876
+ ? input.trim()
877
+ : buildCliStructuredInputPrompt(input).trim();
878
+ if (!content) return;
879
+
880
+ const receivedAt = Date.now();
881
+ const dedupKey = `user_input_ack:${crypto
882
+ .createHash('sha256')
883
+ .update(`${this.instanceId}:${content}:${receivedAt}`)
884
+ .digest('hex')
885
+ .slice(0, 24)}`;
886
+ this.appendRuntimeMessage(buildChatMessage({
887
+ role: 'user',
888
+ senderName: 'User',
889
+ kind: 'standard',
890
+ content,
891
+ receivedAt,
892
+ timestamp: receivedAt,
893
+ source: 'runtime_input_ack',
894
+ meta: {
895
+ runtimeInputAck: true,
896
+ provider: this.type,
897
+ workspace: this.workingDir,
898
+ },
899
+ } as ChatMessage), dedupKey);
900
+ }
901
+
698
902
  dispose(): void {
699
903
  this.adapter.shutdown();
700
904
  this.monitor.reset();
@@ -706,6 +910,7 @@ export class CliProviderInstance implements ProviderInstance {
706
910
 
707
911
  private completedDebounceTimer: NodeJS.Timeout | null = null;
708
912
  private completedDebouncePending: CompletedDebouncePending | null = null;
913
+ private lastExternalCompletionProbe: ExternalTranscriptProbe | null = null;
709
914
 
710
915
  private async enforceFreshSessionLaunchIfNeeded(): Promise<void> {
711
916
  const scriptName = getForcedNewSessionScriptName(this.provider, this.launchMode);
@@ -740,7 +945,157 @@ export class CliProviderInstance implements ProviderInstance {
740
945
  const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
741
946
  const role = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : '';
742
947
  const content = lastVisible ? flattenContent(lastVisible.content).trim() : '';
743
- return role === 'assistant' && !!content;
948
+ if (role !== 'assistant' || !content) return false;
949
+ // Guard: if the last assistant message looks like an active approval/input prompt,
950
+ // it is not a real completion — the session is still awaiting user input.
951
+ if (looksLikeActiveApprovalPromptText(content)) return false;
952
+ return true;
953
+ }
954
+
955
+ private buildExternalTranscriptProbe(messages: unknown[], sourcePath?: string, sourceMtimeMs?: number): ExternalTranscriptProbe {
956
+ const visibleMessages = messages.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
957
+ const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
958
+ const readAt = Date.now();
959
+ const mtimeMs = Number(sourceMtimeMs) || 0;
960
+ return {
961
+ readAt,
962
+ msgCount: messages.length,
963
+ lastRole: typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null,
964
+ lastKind: typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null,
965
+ contentLen: lastVisible ? flattenContent(lastVisible.content).trim().length : 0,
966
+ sourcePath: typeof sourcePath === 'string' && sourcePath ? sourcePath : null,
967
+ sourceMtimeMs: mtimeMs || null,
968
+ mtimeAgeMs: mtimeMs ? Math.max(0, readAt - mtimeMs) : null,
969
+ };
970
+ }
971
+
972
+ private recordPendingTranscriptProbe(pending: CompletedDebouncePending): ExternalTranscriptProbe | null {
973
+ const probe = this.lastExternalCompletionProbe;
974
+ if (!probe) return null;
975
+ const history = pending.transcriptProbeHistory || [];
976
+ const last = history[history.length - 1];
977
+ if (!last || last.readAt !== probe.readAt || last.msgCount !== probe.msgCount || last.lastRole !== probe.lastRole || last.contentLen !== probe.contentLen) {
978
+ history.push(probe);
979
+ pending.transcriptProbeHistory = history.slice(-5);
980
+ }
981
+ return probe;
982
+ }
983
+
984
+ private readExternalCompletionMessages(): unknown[] | null {
985
+ const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
986
+ if (!adapterOwnsMessagesElsewhere) return null;
987
+ if (!this.providerSessionId) return null;
988
+ if (!isNativeSourceCanonicalHistory(this.provider.nativeHistory)) return null;
989
+
990
+ if (this.lastExternalCompletionProbe?.sourcePath) {
991
+ try { fs.statSync(this.lastExternalCompletionProbe.sourcePath); } catch { /* best-effort metadata refresh */ }
992
+ }
993
+ const restoredHistory = readProviderChatHistory(this.type, {
994
+ canonicalHistory: this.provider.nativeHistory,
995
+ historySessionId: this.providerSessionId,
996
+ workspace: this.workingDir,
997
+ offset: 0,
998
+ limit: Number.MAX_SAFE_INTEGER,
999
+ historyBehavior: this.provider.historyBehavior,
1000
+ scripts: this.provider.scripts as any,
1001
+ sessionStartedAtMs: this.startedAt,
1002
+ forceRefresh: true,
1003
+ });
1004
+ if (restoredHistory.source !== 'provider-native') {
1005
+ this.lastExternalCompletionProbe = null;
1006
+ return null;
1007
+ }
1008
+ this.lastExternalCompletionProbe = this.buildExternalTranscriptProbe(
1009
+ restoredHistory.messages,
1010
+ restoredHistory.sourcePath,
1011
+ restoredHistory.sourceMtimeMs,
1012
+ );
1013
+ return restoredHistory.messages;
1014
+ }
1015
+
1016
+ private completionFinalAssistantEvidence(parsedMessages: unknown): CompletionFinalAssistantEvidence {
1017
+ if (this.completionHasFinalAssistantMessage(parsedMessages)) {
1018
+ return {
1019
+ present: true,
1020
+ messages: Array.isArray(parsedMessages) ? parsedMessages : [],
1021
+ source: 'parsed',
1022
+ };
1023
+ }
1024
+
1025
+ const externalMessages = this.readExternalCompletionMessages();
1026
+ if (externalMessages) {
1027
+ return {
1028
+ present: this.completionHasFinalAssistantMessage(externalMessages),
1029
+ messages: externalMessages,
1030
+ source: 'external-native',
1031
+ };
1032
+ }
1033
+
1034
+ return {
1035
+ present: false,
1036
+ messages: Array.isArray(parsedMessages) ? parsedMessages : [],
1037
+ source: 'unavailable',
1038
+ };
1039
+ }
1040
+
1041
+ private completionFinalSummary(parsedMessages: unknown): string | undefined {
1042
+ const evidence = this.completionFinalAssistantEvidence(parsedMessages);
1043
+ return extractFinalSummaryFromMessages(evidence.messages as any);
1044
+ }
1045
+
1046
+ private buildCompletedFinalizationDiagnostic(args: {
1047
+ blockReason: string;
1048
+ latestStatus?: any;
1049
+ latestVisibleStatus: string;
1050
+ waitedMs: number;
1051
+ pending: CompletedDebouncePending;
1052
+ emittedAfterFinalizationTimeout: boolean;
1053
+ }): Record<string, unknown> {
1054
+ let parsed: any = null;
1055
+ let parseError: string | undefined;
1056
+ try {
1057
+ parsed = this.adapter.getScriptParsedStatus();
1058
+ } catch (error: any) {
1059
+ parseError = error?.message || String(error);
1060
+ }
1061
+
1062
+ const evidence = this.completionFinalAssistantEvidence(parsed?.messages);
1063
+ if (evidence.source === 'external-native') {
1064
+ this.recordPendingTranscriptProbe(args.pending);
1065
+ }
1066
+ const visibleMessages = (Array.isArray(evidence.messages) ? evidence.messages : [])
1067
+ .filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
1068
+ const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
1069
+ const lastVisibleRole = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null;
1070
+ const lastVisibleKind = typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null;
1071
+ const lastVisibleContentLength = lastVisible ? flattenContent(lastVisible.content).trim().length : 0;
1072
+
1073
+ return {
1074
+ providerType: this.type,
1075
+ sessionId: this.instanceId,
1076
+ providerSessionId: this.providerSessionId || null,
1077
+ workspace: this.workingDir,
1078
+ blockReason: args.blockReason,
1079
+ emittedAfterFinalizationTimeout: args.emittedAfterFinalizationTimeout,
1080
+ waitedMs: args.waitedMs,
1081
+ maxWaitMs: COMPLETED_FINALIZATION_MAX_WAIT_MS,
1082
+ adapterStatus: typeof args.latestStatus?.status === 'string' ? args.latestStatus.status : null,
1083
+ latestVisibleStatus: args.latestVisibleStatus,
1084
+ parsedStatus: typeof parsed?.status === 'string' ? parsed.status : (parseError ? 'parse_error' : 'unknown'),
1085
+ parseError: parseError || undefined,
1086
+ finalAssistantPresent: evidence.present,
1087
+ finalAssistantEvidenceSource: evidence.source,
1088
+ visibleMessageCount: visibleMessages.length,
1089
+ lastVisibleRole,
1090
+ lastVisibleKind,
1091
+ lastVisibleContentLength,
1092
+ pendingStartedAt: this.generatingStartedAt || null,
1093
+ pendingFirstObservedAt: args.pending.firstObservedAt,
1094
+ pendingTimestamp: args.pending.timestamp,
1095
+ pendingDurationSec: args.pending.duration,
1096
+ previousBlockReason: args.pending.loggedBlockReason || null,
1097
+ transcriptProbeHistory: args.pending.transcriptProbeHistory || [],
1098
+ };
744
1099
  }
745
1100
 
746
1101
  private hasAdapterPendingResponse(): boolean {
@@ -765,32 +1120,90 @@ export class CliProviderInstance implements ProviderInstance {
765
1120
  if (!isCliGeneratingLikeStatus(parsedRawStatus)) return false;
766
1121
  if (adapterRawStatus !== 'idle') return false;
767
1122
  if (hasNonEmptyCliModalButtons(parsedStatus?.activeModal ?? parsedStatus?.modal)) return false;
768
- return !this.hasAdapterPendingResponse();
1123
+ if (this.hasAdapterPendingResponse()) return false;
1124
+ // Do not suppress when the adapter's raw response buffer is still non-empty.
1125
+ // This catches the case where isWaitingForResponse has already flipped to false
1126
+ // (so getPartialResponse() returns '') but the provider's native parser still
1127
+ // reports generating because it's parsing buffered content. Suppressing the
1128
+ // finalization block here would emit a false completion event while the provider
1129
+ // session is still actively processing its response stream.
1130
+ const adapterAny = this.adapter as any;
1131
+ if (typeof adapterAny?.responseBuffer === 'string' && adapterAny.responseBuffer.trim()) return false;
1132
+ return true;
769
1133
  }
770
1134
 
771
- private getCompletedFinalizationBlockReason(latestVisibleStatus: string): string | null {
772
- if (latestVisibleStatus !== 'idle') return `status:${latestVisibleStatus}`;
1135
+ private getCompletedFinalizationBlock(latestVisibleStatus: string, pending: CompletedDebouncePending): CompletedFinalizationBlock | null {
1136
+ if (latestVisibleStatus !== 'idle') return { reason: `status:${latestVisibleStatus}`, terminal: true };
773
1137
 
774
1138
  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';
1139
+ const approvalResolvedIdle = pending.previousStatus === 'waiting_approval';
1140
+ if (!approvalResolvedIdle) {
1141
+ if (adapterAny?.isWaitingForResponse === true) return { reason: 'adapter_waiting_for_response', terminal: true };
1142
+ if (adapterAny?.currentTurnScope) return { reason: 'adapter_turn_scope_active', terminal: true };
1143
+ if (this.hasAdapterPendingResponse()) return { reason: 'adapter_pending_response', terminal: true };
1144
+ }
777
1145
 
778
1146
  const partial = typeof this.adapter.getPartialResponse === 'function'
779
1147
  ? this.adapter.getPartialResponse()
780
1148
  : '';
781
- if (typeof partial === 'string' && partial.trim()) return 'partial_response_pending';
1149
+ if (typeof partial === 'string' && partial.trim()) return { reason: 'partial_response_pending', terminal: true };
782
1150
 
783
1151
  let parsed: any;
784
1152
  try {
785
1153
  parsed = this.adapter.getScriptParsedStatus();
786
1154
  } catch (error: any) {
787
- return `parse_error:${error?.message || String(error)}`;
1155
+ return { reason: `parse_error:${error?.message || String(error)}` };
788
1156
  }
789
1157
 
790
1158
  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';
1159
+ if (parsedStatus !== 'idle') {
1160
+ const adapterStatus = this.adapter.getStatus({ allowParse: false });
1161
+ if (this.shouldSuppressStaleParsedBusyStatus(parsed, adapterStatus)) return null;
1162
+ return { reason: `parsed_status:${parsedStatus}`, terminal: isCliGeneratingLikeStatus(parsedStatus) };
1163
+ }
1164
+ if (parsed?.activeModal || parsed?.modal) return { reason: 'parsed_modal_active', terminal: true };
1165
+ const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
1166
+ const finalAssistantEvidence = this.completionFinalAssistantEvidence(parsed?.messages);
1167
+ if (!finalAssistantEvidence.present) {
1168
+ if (adapterOwnsMessagesElsewhere) {
1169
+ if (finalAssistantEvidence.source === 'external-native') {
1170
+ const probe = this.recordPendingTranscriptProbe(pending);
1171
+ if (probe && !pending.loggedTranscriptProbe) {
1172
+ LOG.info('CLI', `[${this.type}] external transcript probe: msgCount=${probe.msgCount} lastRole=${probe.lastRole || 'none'} lastKind=${probe.lastKind || 'none'} contentLen=${probe.contentLen} sourceMtime=${probe.sourceMtimeMs ?? 'unknown'} mtimeAge=${probe.mtimeAgeMs ?? 'unknown'}ms`);
1173
+ pending.loggedTranscriptProbe = true;
1174
+ }
1175
+ return { reason: 'missing_final_assistant', terminal: true };
1176
+ }
1177
+ // SpecCliAdapter never populates parsed.messages — chat history flows
1178
+ // through the daemon's native-history pipeline, not the status hook.
1179
+ // If that pipeline is unavailable, keep the old skip behavior for
1180
+ // providers that have not opted into strict final-assistant evidence.
1181
+ if ((this.provider as any).requiresFinalAssistantBeforeIdle === true) {
1182
+ return { reason: 'missing_final_assistant', terminal: true };
1183
+ }
1184
+ } else {
1185
+ return {
1186
+ reason: 'missing_final_assistant',
1187
+ terminal: (this.provider as any).requiresFinalAssistantBeforeIdle === true,
1188
+ };
1189
+ }
1190
+ }
1191
+
1192
+ // Guard: if the screen still shows an approval/choice prompt as the last visible text,
1193
+ // the turn is not complete even if the parsed status says idle and there is an assistant
1194
+ // message. This catches the case where waiting_approval→idle transitions occur before
1195
+ // the modal has been resolved (e.g. the PTY rendered the prompt but no button press fired).
1196
+ try {
1197
+ const screenText = typeof (this.adapter as any).getScreenText === 'function'
1198
+ ? String((this.adapter as any).getScreenText() || '')
1199
+ : '';
1200
+ if (screenText) {
1201
+ const tailLines = screenText.split(/\r?\n/).slice(-16).join('\n');
1202
+ if (looksLikeActiveApprovalPromptText(tailLines)) {
1203
+ return { reason: 'screen_shows_approval_prompt', terminal: approvalResolvedIdle };
1204
+ }
1205
+ }
1206
+ } catch { /* defensive: screen text read is best-effort */ }
794
1207
 
795
1208
  return null;
796
1209
  }
@@ -817,10 +1230,11 @@ export class CliProviderInstance implements ProviderInstance {
817
1230
  return;
818
1231
  }
819
1232
 
820
- const blockReason = this.getCompletedFinalizationBlockReason(latestVisibleStatus);
821
- if (blockReason) {
1233
+ const block = this.getCompletedFinalizationBlock(latestVisibleStatus, pending);
1234
+ if (block) {
1235
+ const blockReason = block.reason;
822
1236
  const waitedMs = Date.now() - pending.firstObservedAt;
823
- if (waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
1237
+ if (block.terminal || waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
824
1238
  if (pending.loggedBlockReason !== blockReason) {
825
1239
  LOG.info('CLI', `[${this.type}] waiting to emit completed until transcript finalizes (${blockReason})`);
826
1240
  pending.loggedBlockReason = blockReason;
@@ -828,10 +1242,29 @@ export class CliProviderInstance implements ProviderInstance {
828
1242
  this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
829
1243
  return;
830
1244
  }
831
- LOG.warn('CLI', `[${this.type}] suppressed completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
1245
+ const completionDiagnostic = this.buildCompletedFinalizationDiagnostic({
1246
+ blockReason,
1247
+ latestStatus,
1248
+ latestVisibleStatus,
1249
+ waitedMs,
1250
+ pending,
1251
+ emittedAfterFinalizationTimeout: true,
1252
+ });
1253
+ LOG.warn('CLI', `[${this.type}] emitting completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
1254
+ this.pushEvent({
1255
+ event: 'agent:generating_completed',
1256
+ chatTitle: pending.chatTitle,
1257
+ duration: pending.duration,
1258
+ timestamp: pending.timestamp,
1259
+ finalSummary: blockReason.startsWith('parsed_status:')
1260
+ ? ''
1261
+ : this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
1262
+ completionDiagnostic,
1263
+ });
832
1264
  this.completedDebouncePending = null;
833
1265
  this.completedDebounceTimer = null;
834
1266
  this.generatingStartedAt = 0;
1267
+ this.lastApprovalEventFingerprint = '';
835
1268
  return;
836
1269
  }
837
1270
 
@@ -841,11 +1274,12 @@ export class CliProviderInstance implements ProviderInstance {
841
1274
  chatTitle: pending.chatTitle,
842
1275
  duration: pending.duration,
843
1276
  timestamp: pending.timestamp,
844
- finalSummary: extractFinalSummaryFromMessages(this.adapter?.getScriptParsedStatus()?.messages),
1277
+ finalSummary: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
845
1278
  });
846
1279
  this.completedDebouncePending = null;
847
1280
  this.completedDebounceTimer = null;
848
1281
  this.generatingStartedAt = 0;
1282
+ this.lastApprovalEventFingerprint = '';
849
1283
  }
850
1284
 
851
1285
  private maybeAutoApproveStatus(adapterStatus: any, now = Date.now()): boolean {
@@ -853,15 +1287,46 @@ export class CliProviderInstance implements ProviderInstance {
853
1287
  // Guard re-entry: onStatusChange/getState can observe the same modal multiple
854
1288
  // times while the PTY absorbs the approval key. Without this flag, repeated
855
1289
  // snapshots would write stray keys into the input once the modal dismisses.
856
- if (autoApproveActive && !this.autoApproveBusy) {
1290
+ // However, Claude Code can present a second approval immediately after the
1291
+ // first. Resolve a changed modal signature even while the previous write is
1292
+ // still inside the short busy window.
1293
+ if (!autoApproveActive) {
1294
+ this.lastAutoApprovalSignature = '';
1295
+ return autoApproveActive;
1296
+ }
1297
+ const modal = adapterStatus.activeModal;
1298
+ // (fix) Do not auto-approve when no concrete modal/buttons are present.
1299
+ // Claude TUI flaps between paints; without this guard adapterStatus
1300
+ // could report status=waiting_approval with activeModal=null (or with
1301
+ // an empty buttons array briefly) and we'd still call
1302
+ // resolveModal(-1) — which used to type "1" into the prompt
1303
+ // repeatedly. Skip until a real modal is captured.
1304
+ const buttons = Array.isArray(modal?.buttons)
1305
+ ? modal.buttons.map((b: any) => String(b || '').trim()).filter(Boolean)
1306
+ : [];
1307
+ if (!modal || buttons.length === 0) {
1308
+ return autoApproveActive;
1309
+ }
1310
+ const { index: buttonIndex, label: buttonLabel } = pickAutoApprovalButton(buttons);
1311
+ if (buttonIndex < 0) {
1312
+ // No concrete button matched — don't pick a random index, just
1313
+ // surface the modal so the user can decide.
1314
+ return autoApproveActive;
1315
+ }
1316
+ const signature = [
1317
+ typeof modal?.message === 'string' ? modal.message.trim() : '',
1318
+ buttons.join('|'),
1319
+ buttonIndex,
1320
+ ].join('::');
1321
+ if (!this.autoApproveBusy || signature !== this.lastAutoApprovalSignature) {
857
1322
  this.autoApproveBusy = true;
1323
+ this.lastAutoApprovalSignature = signature;
858
1324
  if (this.autoApproveBusyTimer) clearTimeout(this.autoApproveBusyTimer);
859
1325
  this.autoApproveBusyTimer = setTimeout(() => {
860
1326
  this.autoApproveBusy = false;
861
1327
  this.autoApproveBusyTimer = null;
1328
+ this.lastAutoApprovalSignature = '';
862
1329
  }, 2000);
863
- const modal = adapterStatus.activeModal;
864
- const { index: buttonIndex, label: buttonLabel } = pickApprovalButton(modal?.buttons, this.provider);
865
1330
  this.recordAutoApproval(modal?.message, buttonLabel, now);
866
1331
  setTimeout(() => {
867
1332
  this.adapter.resolveModal(buttonIndex);
@@ -876,6 +1341,13 @@ export class CliProviderInstance implements ProviderInstance {
876
1341
  // during long-running CLI sessions. Keep this path on adapter-owned light
877
1342
  // state only; rich provider parsing is reserved for getState/read_chat.
878
1343
  const adapterStatus = this.adapter.getStatus({ allowParse: false });
1344
+ const adapterProviderSessionId = normalizeProviderSessionId(
1345
+ this.provider,
1346
+ typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
1347
+ );
1348
+ if (adapterProviderSessionId) {
1349
+ this.promoteProviderSessionId(adapterProviderSessionId);
1350
+ }
879
1351
  const parsedStatus = null;
880
1352
  const rawStatus = adapterStatus.status;
881
1353
  const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, now);
@@ -926,10 +1398,14 @@ export class CliProviderInstance implements ProviderInstance {
926
1398
  if (!this.generatingStartedAt) this.generatingStartedAt = now;
927
1399
  const modal = adapterStatus.activeModal;
928
1400
  LOG.info('CLI', `[${this.type}] approval modal: "${modal?.message?.slice(0, 80) ?? 'none'}"`);
929
- // Only push event if not already in waiting_approval (prevent flood from rapid cycles)
930
- const approvalCooldown = 5000;
931
- if (this.lastStatus !== 'waiting_approval' && (!this.lastApprovalEventAt || now - this.lastApprovalEventAt > approvalCooldown)) {
932
- this.lastApprovalEventAt = now;
1401
+ const approvalFingerprint = JSON.stringify({
1402
+ message: typeof modal?.message === 'string' ? modal.message.trim() : '',
1403
+ buttons: Array.isArray(modal?.buttons) ? modal.buttons.map((button: unknown) => String(button).trim()) : [],
1404
+ });
1405
+ // PTY redraws can briefly leave waiting_approval and then re-enter it.
1406
+ // Keep one coordinator event per logical modal until the turn completes.
1407
+ if (this.lastStatus !== 'waiting_approval' && approvalFingerprint !== this.lastApprovalEventFingerprint) {
1408
+ this.lastApprovalEventFingerprint = approvalFingerprint;
933
1409
  this.appendRuntimeSystemMessage(
934
1410
  this.formatApprovalRequestMessage(modal?.message, modal?.buttons),
935
1411
  `approval_request:${now}`,
@@ -943,20 +1419,72 @@ export class CliProviderInstance implements ProviderInstance {
943
1419
  }
944
1420
  } else if (newStatus === 'idle' && (this.lastStatus === 'generating' || this.lastStatus === 'waiting_approval')) {
945
1421
  const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : 0;
946
- // If debounce still pending (generating lasted < 1s), cancel both events
1422
+ // If debounce still pending (generating lasted < 1s), cancel both UI events.
1423
+ // Still emit agent:generating_completed so mesh orchestration can record
1424
+ // task_completed for direct dispatches that complete faster than the debounce.
947
1425
  if (this.generatingDebouncePending) {
948
- LOG.info('CLI', `[${this.type}] suppressed short generating (${now - this.generatingStartedAt}ms)`);
1426
+ const shortDurationMs = this.generatingStartedAt ? now - this.generatingStartedAt : 0;
1427
+ LOG.info('CLI', `[${this.type}] suppressed short generating (${shortDurationMs}ms)`);
949
1428
  if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
950
1429
  this.generatingDebouncePending = null;
951
1430
  this.generatingStartedAt = 0;
1431
+ // Emit completion for mesh task association even though the UI generating
1432
+ // started/completed pair is suppressed (too short for visible UI update).
1433
+ let shortFinalSummary: string | undefined;
1434
+ let shortEvidenceSource: CompletionFinalAssistantEvidence['source'] = 'unavailable';
1435
+ try {
1436
+ const parsedMessages = this.adapter?.getScriptParsedStatus()?.messages;
1437
+ const evidence = this.completionFinalAssistantEvidence(parsedMessages);
1438
+ shortEvidenceSource = evidence.source;
1439
+ shortFinalSummary = extractFinalSummaryFromMessages(evidence.messages as any);
1440
+ } catch { /* best-effort */ }
1441
+ if (((this.provider as any).requiresFinalAssistantBeforeIdle === true || shortEvidenceSource === 'external-native') && !shortFinalSummary) {
1442
+ LOG.info('CLI', `[${this.type}] suppressed short completion without final assistant evidence`);
1443
+ } else {
1444
+ this.pushEvent({
1445
+ event: 'agent:generating_completed',
1446
+ chatTitle,
1447
+ duration: 0,
1448
+ timestamp: now,
1449
+ finalSummary: shortFinalSummary,
1450
+ completionDiagnostic: {
1451
+ reason: 'short_generating_suppressed',
1452
+ shortDurationMs,
1453
+ finalAssistantEvidenceSource: shortEvidenceSource,
1454
+ },
1455
+ });
1456
+ }
952
1457
  } else {
953
1458
  // Debounce completed, then require the rich transcript path that read_chat
954
1459
  // uses to show an idle turn whose last user-facing message is assistant.
955
- this.completedDebouncePending = { chatTitle, duration, timestamp: now, firstObservedAt: now };
1460
+ this.completedDebouncePending = {
1461
+ chatTitle,
1462
+ duration,
1463
+ timestamp: now,
1464
+ firstObservedAt: now,
1465
+ previousStatus: this.lastStatus,
1466
+ };
956
1467
  this.scheduleCompletedDebounceFlush(3000);
957
1468
  }
958
1469
  } else if (newStatus === 'idle' && this.lastStatus === 'starting') {
959
1470
  this.pushEvent({ event: 'agent:ready', chatTitle, timestamp: now });
1471
+ } else if (newStatus === 'error') {
1472
+ if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
1473
+ this.generatingDebouncePending = null;
1474
+ if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
1475
+ this.completedDebouncePending = null;
1476
+ this.errorMessage = adapterStatus.errorMessage || this.errorMessage;
1477
+ this.errorReason = (adapterStatus.errorReason as ProviderErrorReason) || this.errorReason;
1478
+ this.pushEvent({
1479
+ event: 'agent:stopped',
1480
+ chatTitle,
1481
+ timestamp: now,
1482
+ finalSummary: adapterStatus.errorMessage || adapterStatus.errorReason || 'Provider reported an error',
1483
+ completionDiagnostic: {
1484
+ reason: adapterStatus.errorReason || 'provider_error',
1485
+ errorMessage: adapterStatus.errorMessage || undefined,
1486
+ },
1487
+ });
960
1488
  } else if (newStatus === 'stopped') {
961
1489
  // Cancel any pending debounce
962
1490
  if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
@@ -977,7 +1505,31 @@ export class CliProviderInstance implements ProviderInstance {
977
1505
  // Monitor check (cooldown based notification, IDE/CLI common)
978
1506
  const agentKey = `${this.type}:cli`;
979
1507
  const monitorEvents = this.monitor.check(agentKey, newStatus, now, progressFingerprint);
1508
+ const monitorParsedStatus: any = parsedStatus;
980
1509
  for (const me of monitorEvents) {
1510
+ if (
1511
+ me.type === 'monitor:long_generating'
1512
+ && this.completionHasFinalAssistantMessage(monitorParsedStatus?.messages)
1513
+ && !this.hasAdapterPendingResponse()
1514
+ && !hasNonEmptyCliModalButtons(monitorParsedStatus?.activeModal ?? monitorParsedStatus?.modal)
1515
+ ) {
1516
+ this.pushEvent({
1517
+ event: 'agent:generating_completed',
1518
+ chatTitle,
1519
+ duration: this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : undefined,
1520
+ timestamp: me.timestamp,
1521
+ finalSummary: extractFinalSummaryFromMessages(monitorParsedStatus?.messages),
1522
+ completionDiagnostic: {
1523
+ providerType: this.type,
1524
+ sessionId: this.instanceId,
1525
+ providerSessionId: this.providerSessionId || null,
1526
+ reconciliationReason: 'long_generating_monitor_final_summary',
1527
+ finalAssistantPresent: true,
1528
+ },
1529
+ });
1530
+ this.generatingStartedAt = 0;
1531
+ continue;
1532
+ }
981
1533
  this.pushEvent({ event: me.type, agentKey: me.agentKey, message: me.message, elapsedSec: me.elapsedSec, timestamp: me.timestamp });
982
1534
  }
983
1535
  }
@@ -1003,9 +1555,18 @@ export class CliProviderInstance implements ProviderInstance {
1003
1555
  };
1004
1556
  if (this.context?.emitProviderEvent) {
1005
1557
  this.context.emitProviderEvent(enrichedEvent);
1006
- return;
1558
+ } else {
1559
+ this.events.push(enrichedEvent);
1560
+ }
1561
+ // Auto-detach a direct-dispatch mesh assignment once the dispatched
1562
+ // task reaches a terminal state. Leaving meshNodeFor pinned would
1563
+ // route this session's next unrelated turn (a dashboard chat) into
1564
+ // the coordinator as if it were the completion of another task.
1565
+ // We schedule after the emit so the originating coordinator still
1566
+ // observes the completion event with its routing marker intact.
1567
+ if (TERMINAL_MESH_EVENTS.has(event.event) && this.settings.meshActiveTaskId) {
1568
+ try { this.detachMeshAssignment(); } catch { /* best-effort */ }
1007
1569
  }
1008
- this.events.push(enrichedEvent);
1009
1570
  }
1010
1571
 
1011
1572
  private flushEvents(): ProviderEvent[] {
@@ -1258,12 +1819,28 @@ export class CliProviderInstance implements ProviderInstance {
1258
1819
  index,
1259
1820
  source: 'parsed',
1260
1821
  }));
1822
+ const getRole = (message: ChatMessage): string => typeof message.role === 'string'
1823
+ ? message.role.trim().toLowerCase()
1824
+ : '';
1261
1825
  const runtimeEntries: MergeEntry[] = this.runtimeMessages.map((entry, index) => ({
1262
1826
  message: entry.message,
1263
1827
  index: parsedMessages.length + index,
1264
- source: 'runtime',
1828
+ source: 'runtime' as const,
1265
1829
  runtimeKey: entry.key,
1266
- }));
1830
+ })).filter((entry) => {
1831
+ const meta = entry.message.meta && typeof entry.message.meta === 'object' && !Array.isArray(entry.message.meta)
1832
+ ? entry.message.meta as Record<string, unknown>
1833
+ : {};
1834
+ if (meta.runtimeInputAck !== true) return true;
1835
+ const runtimeText = flattenContent(entry.message.content).replace(/\s+/g, ' ').trim();
1836
+ if (!runtimeText) return false;
1837
+ return !parsedEntries.some((parsedEntry) => {
1838
+ const parsedRole = getRole(parsedEntry.message);
1839
+ if (parsedRole !== 'user' && parsedRole !== 'human') return false;
1840
+ const parsedText = flattenContent(parsedEntry.message.content).replace(/\s+/g, ' ').trim();
1841
+ return parsedText === runtimeText;
1842
+ });
1843
+ });
1267
1844
  const getTime = (message: ChatMessage): number => {
1268
1845
  const value = typeof message.receivedAt === 'number'
1269
1846
  ? message.receivedAt
@@ -1273,9 +1850,6 @@ export class CliProviderInstance implements ProviderInstance {
1273
1850
  return Number.isFinite(value) && value > 0 ? value : 0;
1274
1851
  };
1275
1852
 
1276
- const getRole = (message: ChatMessage): string => typeof message.role === 'string'
1277
- ? message.role.trim().toLowerCase()
1278
- : '';
1279
1853
  const isRuntimeOverlay = (entry: MergeEntry): boolean => {
1280
1854
  if (entry.source !== 'runtime') return false;
1281
1855
  const key = typeof entry.runtimeKey === 'string' ? entry.runtimeKey.trim().toLowerCase() : '';
@@ -1341,7 +1915,9 @@ export class CliProviderInstance implements ProviderInstance {
1341
1915
  this.providerSessionId = nextSessionId;
1342
1916
  this.historyWriter.promoteHistorySession(this.type, previousHistorySessionId, nextSessionId);
1343
1917
  this.historyWriter.writeSessionStart(this.type, nextSessionId, this.workingDir, this.instanceId);
1344
- this.restorePersistedHistoryFromCurrentSession();
1918
+ if (this.shouldHydrateExistingProviderHistory()) {
1919
+ this.restorePersistedHistoryFromCurrentSession();
1920
+ }
1345
1921
  this.adapter.updateRuntimeMeta({ providerSessionId: nextSessionId });
1346
1922
  this.onProviderSessionResolved?.({
1347
1923
  instanceId: this.instanceId,
@@ -1354,9 +1930,27 @@ export class CliProviderInstance implements ProviderInstance {
1354
1930
  LOG.info('CLI', `[${this.type}] discovered provider session id: ${nextSessionId}`);
1355
1931
  }
1356
1932
 
1933
+ private shouldHydrateExistingProviderHistory(): boolean {
1934
+ return this.launchMode === 'resume' || this.launchMode === 'manual';
1935
+ }
1936
+
1937
+ private shouldSuppressFreshLaunchStartupReplay(parsedMessages: unknown[], parsedStatus: any, adapterStatus: any, parsedProviderSessionId = ''): boolean {
1938
+ if (this.launchMode !== 'new') return false;
1939
+ if (this.providerSessionId) return false;
1940
+ if (!Array.isArray(parsedMessages) || parsedMessages.length === 0) return false;
1941
+ if (!isIdleStatus(adapterStatus?.status) || !isIdleStatus(parsedStatus?.status)) return false;
1942
+ if (parsedProviderSessionId) return true;
1943
+
1944
+ const newestMessageAt = parsedMessages.reduce<number>((newest, message) => Math.max(newest, getMessageTime(message)), 0);
1945
+
1946
+ // Untimestamped idle parser output during a fresh launch is usually the
1947
+ // provider's last workspace transcript before a new turn exists.
1948
+ return newestMessageAt === 0;
1949
+ }
1950
+
1357
1951
  private syncCanonicalSavedHistoryIfNeeded(): boolean {
1358
1952
  if (!this.providerSessionId) return false;
1359
- const canonicalHistory = this.provider.canonicalHistory;
1953
+ const canonicalHistory = this.provider.nativeHistory;
1360
1954
  if (!canonicalHistory) return false;
1361
1955
 
1362
1956
  if (isNativeSourceCanonicalHistory(canonicalHistory)) {
@@ -1418,9 +2012,9 @@ export class CliProviderInstance implements ProviderInstance {
1418
2012
  private restorePersistedHistoryFromCurrentSession(): void {
1419
2013
  if (!this.providerSessionId) return;
1420
2014
  this.syncCanonicalSavedHistoryIfNeeded();
1421
- const restoredHistory = isNativeSourceCanonicalHistory(this.provider.canonicalHistory)
2015
+ const restoredHistory = isNativeSourceCanonicalHistory(this.provider.nativeHistory)
1422
2016
  ? readProviderChatHistory(this.type, {
1423
- canonicalHistory: this.provider.canonicalHistory,
2017
+ canonicalHistory: this.provider.nativeHistory,
1424
2018
  historySessionId: this.providerSessionId,
1425
2019
  workspace: this.workingDir,
1426
2020
  offset: 0,