@adhdev/daemon-core 0.9.82-rc.16 → 0.9.82-rc.161

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