@adhdev/daemon-core 0.9.82-rc.17 → 0.9.82-rc.171

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 (221) 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 +7 -0
  16. package/dist/config/config.d.ts +5 -0
  17. package/dist/config/mesh-config.d.ts +71 -1
  18. package/dist/git/git-commands.d.ts +5 -1
  19. package/dist/index.d.ts +35 -6
  20. package/dist/index.js +18634 -3760
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +18538 -3732
  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 +96 -5
  34. package/dist/mesh/mesh-fast-forward.d.ts +39 -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 +44 -5
  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 +9 -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 +24 -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.d.ts +3 -0
  53. package/dist/providers/provider-loader.d.ts +26 -4
  54. package/dist/providers/provider-trust.d.ts +31 -0
  55. package/dist/providers/read-chat-contract.d.ts +29 -0
  56. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  57. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  58. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  59. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  60. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  61. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  62. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  63. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  64. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  65. package/dist/providers/sdk/v1/index.d.ts +30 -0
  66. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  67. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  68. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  69. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  70. package/dist/providers/sdk/v1/types/common/index.d.ts +169 -0
  71. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  72. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  73. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  74. package/dist/providers/spec/adapter.d.ts +56 -0
  75. package/dist/providers/spec/cli-adapter.d.ts +95 -0
  76. package/dist/providers/spec/driver.d.ts +149 -0
  77. package/dist/providers/spec/evaluator.d.ts +47 -0
  78. package/dist/providers/spec/loader.d.ts +14 -0
  79. package/dist/providers/spec/native-history-executor.d.ts +39 -0
  80. package/dist/providers/spec/route.d.ts +4 -0
  81. package/dist/providers/spec/schema.gen.d.ts +511 -0
  82. package/dist/providers/spec/types.d.ts +212 -0
  83. package/dist/providers/transcript-v2.d.ts +176 -0
  84. package/dist/providers/types/interactive-prompt.d.ts +48 -0
  85. package/dist/repo-mesh-types.d.ts +100 -1
  86. package/dist/sessions/registry.d.ts +3 -0
  87. package/dist/shared-types.d.ts +42 -1
  88. package/dist/status/reporter.d.ts +2 -0
  89. package/dist/status/snapshot.d.ts +1 -0
  90. package/dist/types.d.ts +5 -0
  91. package/package.json +7 -2
  92. package/src/agent-stream/poller.ts +2 -3
  93. package/src/boot/daemon-lifecycle.ts +17 -10
  94. package/src/boot/process-hardening.ts +89 -0
  95. package/src/chat/source-machine.ts +534 -0
  96. package/src/chat/source-resolver.ts +0 -0
  97. package/src/chat/subscription-updates.ts +14 -1
  98. package/src/cli-adapter-types.d.ts +1 -0
  99. package/src/cli-adapter-types.ts +21 -2
  100. package/src/cli-adapters/cli-script-runner.ts +421 -0
  101. package/src/cli-adapters/cli-state-engine.ts +1086 -0
  102. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  103. package/src/cli-adapters/provider-cli-adapter.ts +670 -1137
  104. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  105. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  106. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  107. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  108. package/src/cli-adapters/provider-cli-shared.ts +59 -11
  109. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  110. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
  111. package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
  112. package/src/commands/chat-commands.ts +1597 -60
  113. package/src/commands/cli-manager.ts +214 -3
  114. package/src/commands/handler.ts +809 -2
  115. package/src/commands/mesh-coordinator.ts +231 -127
  116. package/src/commands/router.ts +3574 -502
  117. package/src/config/chat-history.ts +87 -24
  118. package/src/config/config.ts +12 -0
  119. package/src/config/mesh-config.ts +264 -2
  120. package/src/config/recent-activity.ts +8 -2
  121. package/src/daemon/dev-cli-debug.ts +10 -1
  122. package/src/detection/ide-detector.ts +26 -16
  123. package/src/git/git-commands.ts +17 -5
  124. package/src/git/git-worktree.ts +8 -1
  125. package/src/index.ts +113 -5
  126. package/src/installer.d.ts +1 -1
  127. package/src/installer.ts +8 -6
  128. package/src/ipc/local-ipc-server.ts +278 -0
  129. package/src/launch.d.ts +1 -1
  130. package/src/launch.ts +37 -28
  131. package/src/logging/async-batch-writer.ts +55 -0
  132. package/src/logging/logger.ts +2 -1
  133. package/src/mesh/beads-db.ts +479 -0
  134. package/src/mesh/contracts.ts +329 -0
  135. package/src/mesh/coordinator-prompt.ts +204 -30
  136. package/src/mesh/coordinator-registry.ts +121 -0
  137. package/src/mesh/mesh-active-work.ts +437 -0
  138. package/src/mesh/mesh-events.ts +879 -61
  139. package/src/mesh/mesh-fast-forward.ts +430 -0
  140. package/src/mesh/mesh-host-ownership.ts +73 -0
  141. package/src/mesh/mesh-ledger.ts +457 -104
  142. package/src/mesh/mesh-refine-status.ts +144 -0
  143. package/src/mesh/mesh-work-queue.ts +216 -158
  144. package/src/mesh/preview-freshness.ts +118 -0
  145. package/src/mesh/refine-config.ts +366 -0
  146. package/src/mesh/worktree-bootstrap-config.ts +247 -0
  147. package/src/providers/approval-utils.d.ts +4 -0
  148. package/src/providers/approval-utils.ts +47 -5
  149. package/src/providers/chat-message-normalization.ts +4 -11
  150. package/src/providers/cli-provider-instance.ts +456 -54
  151. package/src/providers/contracts.ts +105 -5
  152. package/src/providers/external-sources.ts +218 -0
  153. package/src/providers/ide-provider-instance.ts +19 -5
  154. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  155. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  156. package/src/providers/native-history/codex-cli-transcript.ts +419 -0
  157. package/src/providers/native-history/dispatcher.ts +227 -0
  158. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  159. package/src/providers/native-history/index.ts +30 -0
  160. package/src/providers/provider-instance.ts +3 -0
  161. package/src/providers/provider-loader.ts +585 -50
  162. package/src/providers/provider-schema.ts +31 -13
  163. package/src/providers/provider-trust.ts +114 -0
  164. package/src/providers/read-chat-contract.ts +76 -16
  165. package/src/providers/sdk/README.md +49 -0
  166. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  167. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  168. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  169. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  170. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  171. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  172. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  173. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  174. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  175. package/src/providers/sdk/v1/index.ts +152 -0
  176. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  177. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  178. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  179. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +458 -0
  180. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  181. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  182. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  183. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  184. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  185. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  186. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  187. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  188. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  189. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  190. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  191. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  192. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  193. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  194. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  195. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  196. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  197. package/src/providers/sdk/v1/types/common/index.ts +210 -0
  198. package/src/providers/sdk/v1/validators/index.ts +19 -0
  199. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  200. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  201. package/src/providers/spec/adapter.ts +168 -0
  202. package/src/providers/spec/cli-adapter.ts +430 -0
  203. package/src/providers/spec/driver.ts +502 -0
  204. package/src/providers/spec/evaluator.ts +298 -0
  205. package/src/providers/spec/loader.ts +130 -0
  206. package/src/providers/spec/native-history-executor.ts +623 -0
  207. package/src/providers/spec/route.ts +51 -0
  208. package/src/providers/spec/schema.gen.ts +511 -0
  209. package/src/providers/spec/schema.json +211 -0
  210. package/src/providers/spec/types.ts +231 -0
  211. package/src/providers/transcript-v2.ts +567 -0
  212. package/src/providers/types/interactive-prompt.ts +297 -0
  213. package/src/providers/version-archive.ts +38 -20
  214. package/src/repo-mesh-types.ts +110 -1
  215. package/src/sessions/registry.ts +3 -0
  216. package/src/shared-types.ts +45 -1
  217. package/src/status/builders.ts +24 -6
  218. package/src/status/reporter.ts +15 -0
  219. package/src/status/snapshot.ts +84 -25
  220. package/src/system/host-memory.ts +29 -12
  221. package/src/types.ts +5 -0
@@ -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,9 +42,28 @@ type CompletedDebouncePending = {
40
42
  duration: number;
41
43
  timestamp: number;
42
44
  firstObservedAt: number;
45
+ previousStatus: string;
43
46
  loggedBlockReason?: string;
44
47
  };
45
48
 
49
+ function isIdleStatus(value: unknown): boolean {
50
+ const status = typeof value === 'string' ? value.trim().toLowerCase() : '';
51
+ return !status || status === 'idle' || status === 'ready';
52
+ }
53
+
54
+
55
+ function getMessageTime(message: unknown): number {
56
+ if (!message || typeof message !== 'object') return 0;
57
+ const record = message as { receivedAt?: unknown; timestamp?: unknown };
58
+ const value = Number(record.receivedAt ?? record.timestamp ?? 0);
59
+ return Number.isFinite(value) ? value : 0;
60
+ }
61
+
62
+ type CompletedFinalizationBlock = {
63
+ reason: string;
64
+ terminal?: boolean;
65
+ };
66
+
46
67
  const COMPLETED_FINALIZATION_RETRY_MS = 1000;
47
68
  const COMPLETED_FINALIZATION_MAX_WAIT_MS = 30_000;
48
69
 
@@ -309,9 +330,10 @@ export class CliProviderInstance implements ProviderInstance {
309
330
  private monitor: StatusMonitor;
310
331
  private generatingDebounceTimer: NodeJS.Timeout | null = null;
311
332
  private generatingDebouncePending: { chatTitle: string; timestamp: number } | null = null;
312
- private lastApprovalEventAt = 0;
333
+ private lastApprovalEventFingerprint = '';
313
334
  private autoApproveBusy = false;
314
335
  private autoApproveBusyTimer: NodeJS.Timeout | null = null;
336
+ private lastAutoApprovalSignature = '';
315
337
  private controlValues: Record<string, string | number | boolean> = {};
316
338
  private summaryMetadata: unknown = undefined;
317
339
  private appliedEffectKeys = new Set<string>();
@@ -330,6 +352,7 @@ export class CliProviderInstance implements ProviderInstance {
330
352
  private suppressIdleHistoryReplay = false;
331
353
  private errorMessage: string | undefined = undefined;
332
354
  private errorReason: ProviderErrorReason | undefined = undefined;
355
+ private activeInteractivePrompt: InteractivePrompt | null = null;
333
356
 
334
357
  private presentationMode: 'terminal' | 'chat';
335
358
  private providerSessionId?: string;
@@ -370,7 +393,10 @@ export class CliProviderInstance implements ProviderInstance {
370
393
  this.providerSessionId = options?.providerSessionId;
371
394
  this.launchMode = options?.launchMode || 'new';
372
395
  this.onProviderSessionResolved = options?.onProviderSessionResolved;
373
- this.adapter = new ProviderCliAdapter(provider as CliProviderModule, workingDir, cliArgs, options?.extraEnv || {}, transportFactory);
396
+ this.adapter = createCliAdapter(provider as CliProviderModule, workingDir, cliArgs, options?.extraEnv || {}, transportFactory) as ProviderCliAdapter;
397
+ if (this.providerSessionId) {
398
+ this.adapter.updateRuntimeMeta({ providerSessionId: this.providerSessionId });
399
+ }
374
400
  this.monitor = new StatusMonitor();
375
401
  this.historyWriter = new ChatHistoryWriter();
376
402
  }
@@ -412,7 +438,7 @@ export class CliProviderInstance implements ProviderInstance {
412
438
  await this.adapter.spawn();
413
439
  await this.enforceFreshSessionLaunchIfNeeded();
414
440
  this.maybeAppendRuntimeRecoveryMessage(this.adapter.getRuntimeMetadata());
415
- if (this.providerSessionId) {
441
+ if (this.providerSessionId && this.shouldHydrateExistingProviderHistory()) {
416
442
  this.restorePersistedHistoryFromCurrentSession();
417
443
  }
418
444
  if (this.providerSessionId && this.launchMode === 'resume') {
@@ -486,14 +512,38 @@ export class CliProviderInstance implements ProviderInstance {
486
512
  }
487
513
 
488
514
  getState(): ProviderState {
515
+ // TODO(phase5-sandbox): JS override scripts (detectStatus, parseApproval,
516
+ // parseSession) are currently invoked by CliScriptRunner.invoke() via direct
517
+ // function calls — the scripts run in the daemon process with full Node.js
518
+ // access and no resource limits.
519
+ //
520
+ // When Phase 5 lands, CliScriptRunner should route these calls through a
521
+ // SandboxedScriptRunner (see providers/sdk/v1/sandbox/script-runner.ts) so
522
+ // that each call gets a fresh isolated-vm context with a 50 ms CPU limit and
523
+ // a 32 MB memory cap. The execution path to change is:
524
+ // CliScriptRunner.invoke() → SandboxedScriptRunner.run(scriptSource, context)
525
+ //
526
+ // This getState() call-site is NOT where the change goes — the wiring belongs
527
+ // in cli-script-runner.ts (CliScriptRunner.detectStatus / parseApproval /
528
+ // parseSession), with provider-loader.ts updated to store script source strings
529
+ // alongside the loaded function references for extended-legacy providers.
489
530
  const adapterStatus = this.adapter.getStatus();
531
+ if (Object.prototype.hasOwnProperty.call(adapterStatus, 'activeInteractivePrompt')) {
532
+ this.activeInteractivePrompt = adapterStatus.activeInteractivePrompt ?? null;
533
+ }
490
534
  let parsedStatus: any = null;
491
535
  let parseErrorMessage: string | undefined;
492
536
  if (typeof this.adapter.getScriptParsedStatus === 'function') {
493
537
  try {
494
538
  parsedStatus = this.adapter.getScriptParsedStatus() || null;
495
- this.errorMessage = undefined;
496
- this.errorReason = undefined;
539
+ const parsedErrorMessage = typeof parsedStatus?.errorMessage === 'string' && parsedStatus.errorMessage.trim()
540
+ ? parsedStatus.errorMessage.trim()
541
+ : undefined;
542
+ const parsedErrorReason = typeof parsedStatus?.errorReason === 'string' && parsedStatus.errorReason.trim()
543
+ ? parsedStatus.errorReason.trim() as ProviderErrorReason
544
+ : undefined;
545
+ this.errorMessage = parsedErrorMessage;
546
+ this.errorReason = parsedErrorReason;
497
547
  } catch (error: any) {
498
548
  parseErrorMessage = error?.message || String(error);
499
549
  this.errorMessage = parseErrorMessage;
@@ -503,22 +553,39 @@ export class CliProviderInstance implements ProviderInstance {
503
553
  this.errorMessage = undefined;
504
554
  this.errorReason = undefined;
505
555
  }
556
+ const adapterProviderSessionId = normalizeProviderSessionId(
557
+ this.provider,
558
+ typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
559
+ );
506
560
  const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, Date.now());
507
- const visibleStatus = parseErrorMessage
561
+ const visibleStatus = parseErrorMessage || parsedStatus?.status === 'error'
508
562
  ? 'error'
509
563
  : (autoApproveActive ? 'generating' : adapterStatus.status);
564
+ const runtime = this.adapter.getRuntimeMetadata();
565
+ this.maybeAppendRuntimeRecoveryMessage(runtime);
566
+ let parsedMessages = Array.isArray(parsedStatus?.messages)
567
+ ? parsedStatus.messages
568
+ : [];
510
569
  const parsedProviderSessionId = normalizeProviderSessionId(
511
570
  this.provider,
512
571
  typeof parsedStatus?.providerSessionId === 'string' ? parsedStatus.providerSessionId : '',
513
572
  );
514
- if (parsedProviderSessionId) {
573
+ const suppressFreshLaunchStartupReplay = this.shouldSuppressFreshLaunchStartupReplay(
574
+ parsedMessages,
575
+ parsedStatus,
576
+ adapterStatus,
577
+ parsedProviderSessionId,
578
+ );
579
+ if (adapterProviderSessionId && !suppressFreshLaunchStartupReplay) {
580
+ this.promoteProviderSessionId(adapterProviderSessionId);
581
+ }
582
+ if (parsedProviderSessionId && !suppressFreshLaunchStartupReplay) {
515
583
  this.promoteProviderSessionId(parsedProviderSessionId);
516
584
  }
517
- const runtime = this.adapter.getRuntimeMetadata();
518
- this.maybeAppendRuntimeRecoveryMessage(runtime);
519
- let parsedMessages = Array.isArray(parsedStatus?.messages)
520
- ? parsedStatus.messages
521
- : [];
585
+ if (suppressFreshLaunchStartupReplay) {
586
+ parsedMessages = [];
587
+ }
588
+ const activeChatId = this.providerSessionId || runtime?.runtimeId || this.instanceId;
522
589
  const historyMessageCount = Number.isFinite(parsedStatus?.historyMessageCount)
523
590
  ? Math.max(0, Number(parsedStatus.historyMessageCount))
524
591
  : null;
@@ -528,7 +595,18 @@ export class CliProviderInstance implements ProviderInstance {
528
595
  : [];
529
596
  }
530
597
  const mergedMessages = this.mergeConversationMessages(parsedMessages);
531
- const canonicalBackedHistory = this.syncCanonicalSavedHistoryIfNeeded();
598
+ const canonicalBackedHistory = this.shouldHydrateExistingProviderHistory()
599
+ ? this.syncCanonicalSavedHistoryIfNeeded()
600
+ : false;
601
+ const statusMessages = canonicalBackedHistory && this.lastPersistedHistoryMessages.length > 0
602
+ ? this.lastPersistedHistoryMessages.map((message) => ({
603
+ role: message.role,
604
+ content: message.content,
605
+ kind: message.kind,
606
+ senderName: message.senderName,
607
+ receivedAt: message.receivedAt,
608
+ }))
609
+ : mergedMessages;
532
610
 
533
611
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
534
612
  const parsedChatStatus = typeof parsedStatus?.status === 'string' && parsedStatus.status.trim()
@@ -572,7 +650,12 @@ export class CliProviderInstance implements ProviderInstance {
572
650
  }
573
651
  }
574
652
 
575
- this.applyProviderResponse(parsedStatus, { phase: 'immediate' });
653
+ this.applyProviderResponse(
654
+ suppressFreshLaunchStartupReplay && parsedStatus && typeof parsedStatus === 'object'
655
+ ? { ...parsedStatus, providerSessionId: undefined }
656
+ : parsedStatus,
657
+ { phase: 'immediate' },
658
+ );
576
659
  const surface = resolveProviderStateSurface({
577
660
  summaryMetadata: this.summaryMetadata as any,
578
661
  controlValues: this.controlValues,
@@ -592,13 +675,15 @@ export class CliProviderInstance implements ProviderInstance {
592
675
  status: visibleStatus,
593
676
  mode: this.presentationMode,
594
677
  activeChat: {
595
- id: `${this.type}_${this.workingDir}`,
678
+ id: activeChatId,
596
679
  title: parsedStatus?.title || dirName,
597
680
  status: activeChatStatus,
598
- messages: mergedMessages,
681
+ messages: statusMessages,
599
682
  activeModal: autoApproveActive ? null : (parsedStatus?.activeModal ?? adapterStatus.activeModal),
683
+ activeInteractivePrompt: this.activeInteractivePrompt,
600
684
  inputContent: '',
601
685
  },
686
+ activeInteractivePrompt: this.activeInteractivePrompt,
602
687
  workspace: this.workingDir,
603
688
  instanceId: this.instanceId,
604
689
  providerSessionId: this.providerSessionId,
@@ -651,13 +736,17 @@ export class CliProviderInstance implements ProviderInstance {
651
736
  };
652
737
  }
653
738
 
654
- getSessionModalState(): SessionModalState {
655
- const adapterStatus = this.adapter.getStatus({ allowParse: false });
739
+ getSessionModalState(sessionId?: string): SessionModalState {
740
+ const adapterStatus = this.adapter.getStatus({ allowParse: true });
656
741
  const autoApproveActive = adapterStatus.status === 'waiting_approval' && this.shouldAutoApprove();
657
742
  const visibleStatus = autoApproveActive ? 'generating' : adapterStatus.status;
658
743
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
659
744
  return {
660
- id: this.instanceId,
745
+ // Honor the caller-supplied sessionId — InstanceMgr rejects the
746
+ // projection when projected.id !== requested sessionId, and
747
+ // this.instanceId is the manager's internal key, not the public
748
+ // sessionId the dashboard subscribes by.
749
+ id: sessionId ?? this.instanceId,
661
750
  status: visibleStatus,
662
751
  title: dirName,
663
752
  activeModal: autoApproveActive ? null : adapterStatus.activeModal,
@@ -690,11 +779,65 @@ export class CliProviderInstance implements ProviderInstance {
690
779
  void this.adapter.resolveAction(data).catch((e: any) => {
691
780
  LOG.warn('CLI', `[${this.type}] resolve_action failed: ${e?.message || e}`);
692
781
  });
782
+ } else if (event === 'interactive_prompt' && data) {
783
+ const prompt = normalizeInteractivePrompt(data);
784
+ if (prompt) {
785
+ this.activeInteractivePrompt = prompt;
786
+ this.events.push({
787
+ event: 'interactive_prompt',
788
+ timestamp: Date.now(),
789
+ promptId: prompt.promptId,
790
+ });
791
+ }
792
+ } else if (event === 'interactive_prompt_response' && data) {
793
+ try {
794
+ const response = normalizeInteractivePromptResponse(data);
795
+ if (this.activeInteractivePrompt?.promptId === response.promptId) {
796
+ this.activeInteractivePrompt = null;
797
+ }
798
+ if (typeof this.adapter.setInteractivePromptResponse !== 'function') {
799
+ LOG.warn('CLI', `[${this.type}] interactive_prompt_response ignored: adapter does not support interactive prompts`);
800
+ return;
801
+ }
802
+ void this.adapter.setInteractivePromptResponse(response).catch((e: any) => {
803
+ LOG.warn('CLI', `[${this.type}] interactive_prompt_response failed: ${e?.message || e}`);
804
+ });
805
+ } catch (e: any) {
806
+ LOG.warn('CLI', `[${this.type}] invalid interactive_prompt_response: ${e?.message || e}`);
807
+ }
693
808
  } else if (event === 'provider_state_patch' && data && typeof data === 'object') {
694
809
  this.applyProviderResponse(data, { phase: 'immediate' });
695
810
  }
696
811
  }
697
812
 
813
+ recordAcknowledgedUserInput(input: InputEnvelope | string): void {
814
+ const content = typeof input === 'string'
815
+ ? input.trim()
816
+ : buildCliStructuredInputPrompt(input).trim();
817
+ if (!content) return;
818
+
819
+ const receivedAt = Date.now();
820
+ const dedupKey = `user_input_ack:${crypto
821
+ .createHash('sha256')
822
+ .update(`${this.instanceId}:${content}:${receivedAt}`)
823
+ .digest('hex')
824
+ .slice(0, 24)}`;
825
+ this.appendRuntimeMessage(buildChatMessage({
826
+ role: 'user',
827
+ senderName: 'User',
828
+ kind: 'standard',
829
+ content,
830
+ receivedAt,
831
+ timestamp: receivedAt,
832
+ source: 'runtime_input_ack',
833
+ meta: {
834
+ runtimeInputAck: true,
835
+ provider: this.type,
836
+ workspace: this.workingDir,
837
+ },
838
+ } as ChatMessage), dedupKey);
839
+ }
840
+
698
841
  dispose(): void {
699
842
  this.adapter.shutdown();
700
843
  this.monitor.reset();
@@ -740,7 +883,60 @@ export class CliProviderInstance implements ProviderInstance {
740
883
  const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
741
884
  const role = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : '';
742
885
  const content = lastVisible ? flattenContent(lastVisible.content).trim() : '';
743
- return role === 'assistant' && !!content;
886
+ if (role !== 'assistant' || !content) return false;
887
+ // Guard: if the last assistant message looks like an active approval/input prompt,
888
+ // it is not a real completion — the session is still awaiting user input.
889
+ if (looksLikeActiveApprovalPromptText(content)) return false;
890
+ return true;
891
+ }
892
+
893
+ private buildCompletedFinalizationDiagnostic(args: {
894
+ blockReason: string;
895
+ latestStatus?: any;
896
+ latestVisibleStatus: string;
897
+ waitedMs: number;
898
+ pending: CompletedDebouncePending;
899
+ emittedAfterFinalizationTimeout: boolean;
900
+ }): Record<string, unknown> {
901
+ let parsed: any = null;
902
+ let parseError: string | undefined;
903
+ try {
904
+ parsed = this.adapter.getScriptParsedStatus();
905
+ } catch (error: any) {
906
+ parseError = error?.message || String(error);
907
+ }
908
+
909
+ const visibleMessages = (Array.isArray(parsed?.messages) ? parsed.messages : [])
910
+ .filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
911
+ const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
912
+ const lastVisibleRole = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null;
913
+ const lastVisibleKind = typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null;
914
+ const lastVisibleContentLength = lastVisible ? flattenContent(lastVisible.content).trim().length : 0;
915
+
916
+ return {
917
+ providerType: this.type,
918
+ sessionId: this.instanceId,
919
+ providerSessionId: this.providerSessionId || null,
920
+ workspace: this.workingDir,
921
+ blockReason: args.blockReason,
922
+ emittedAfterFinalizationTimeout: args.emittedAfterFinalizationTimeout,
923
+ waitedMs: args.waitedMs,
924
+ maxWaitMs: COMPLETED_FINALIZATION_MAX_WAIT_MS,
925
+ adapterStatus: typeof args.latestStatus?.status === 'string' ? args.latestStatus.status : null,
926
+ latestVisibleStatus: args.latestVisibleStatus,
927
+ parsedStatus: typeof parsed?.status === 'string' ? parsed.status : (parseError ? 'parse_error' : 'unknown'),
928
+ parseError: parseError || undefined,
929
+ finalAssistantPresent: this.completionHasFinalAssistantMessage(parsed?.messages),
930
+ visibleMessageCount: visibleMessages.length,
931
+ lastVisibleRole,
932
+ lastVisibleKind,
933
+ lastVisibleContentLength,
934
+ pendingStartedAt: this.generatingStartedAt || null,
935
+ pendingFirstObservedAt: args.pending.firstObservedAt,
936
+ pendingTimestamp: args.pending.timestamp,
937
+ pendingDurationSec: args.pending.duration,
938
+ previousBlockReason: args.pending.loggedBlockReason || null,
939
+ };
744
940
  }
745
941
 
746
942
  private hasAdapterPendingResponse(): boolean {
@@ -765,32 +961,73 @@ export class CliProviderInstance implements ProviderInstance {
765
961
  if (!isCliGeneratingLikeStatus(parsedRawStatus)) return false;
766
962
  if (adapterRawStatus !== 'idle') return false;
767
963
  if (hasNonEmptyCliModalButtons(parsedStatus?.activeModal ?? parsedStatus?.modal)) return false;
768
- return !this.hasAdapterPendingResponse();
964
+ if (this.hasAdapterPendingResponse()) return false;
965
+ // Do not suppress when the adapter's raw response buffer is still non-empty.
966
+ // This catches the case where isWaitingForResponse has already flipped to false
967
+ // (so getPartialResponse() returns '') but the provider's native parser still
968
+ // reports generating because it's parsing buffered content. Suppressing the
969
+ // finalization block here would emit a false completion event while the provider
970
+ // session is still actively processing its response stream.
971
+ const adapterAny = this.adapter as any;
972
+ if (typeof adapterAny?.responseBuffer === 'string' && adapterAny.responseBuffer.trim()) return false;
973
+ return true;
769
974
  }
770
975
 
771
- private getCompletedFinalizationBlockReason(latestVisibleStatus: string): string | null {
772
- if (latestVisibleStatus !== 'idle') return `status:${latestVisibleStatus}`;
976
+ private getCompletedFinalizationBlock(latestVisibleStatus: string, pending: CompletedDebouncePending): CompletedFinalizationBlock | null {
977
+ if (latestVisibleStatus !== 'idle') return { reason: `status:${latestVisibleStatus}`, terminal: true };
773
978
 
774
979
  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';
980
+ const approvalResolvedIdle = pending.previousStatus === 'waiting_approval';
981
+ if (!approvalResolvedIdle) {
982
+ if (adapterAny?.isWaitingForResponse === true) return { reason: 'adapter_waiting_for_response', terminal: true };
983
+ if (adapterAny?.currentTurnScope) return { reason: 'adapter_turn_scope_active', terminal: true };
984
+ if (this.hasAdapterPendingResponse()) return { reason: 'adapter_pending_response', terminal: true };
985
+ }
777
986
 
778
987
  const partial = typeof this.adapter.getPartialResponse === 'function'
779
988
  ? this.adapter.getPartialResponse()
780
989
  : '';
781
- if (typeof partial === 'string' && partial.trim()) return 'partial_response_pending';
990
+ if (typeof partial === 'string' && partial.trim()) return { reason: 'partial_response_pending', terminal: true };
782
991
 
783
992
  let parsed: any;
784
993
  try {
785
994
  parsed = this.adapter.getScriptParsedStatus();
786
995
  } catch (error: any) {
787
- return `parse_error:${error?.message || String(error)}`;
996
+ return { reason: `parse_error:${error?.message || String(error)}` };
788
997
  }
789
998
 
790
999
  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';
1000
+ if (parsedStatus !== 'idle') {
1001
+ const adapterStatus = this.adapter.getStatus({ allowParse: false });
1002
+ if (this.shouldSuppressStaleParsedBusyStatus(parsed, adapterStatus)) return null;
1003
+ return { reason: `parsed_status:${parsedStatus}`, terminal: isCliGeneratingLikeStatus(parsedStatus) };
1004
+ }
1005
+ if (parsed?.activeModal || parsed?.modal) return { reason: 'parsed_modal_active', terminal: true };
1006
+ // SpecCliAdapter never populates parsed.messages — chat history flows
1007
+ // through the daemon's native-history pipeline, not the status hook.
1008
+ // Skipping the final-assistant gate avoids a 30s stall on every turn
1009
+ // for spec-routed providers (agy / codex / claude / hermes).
1010
+ const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
1011
+ if (!adapterOwnsMessagesElsewhere
1012
+ && !this.completionHasFinalAssistantMessage(parsed?.messages)) {
1013
+ return { reason: 'missing_final_assistant' };
1014
+ }
1015
+
1016
+ // Guard: if the screen still shows an approval/choice prompt as the last visible text,
1017
+ // the turn is not complete even if the parsed status says idle and there is an assistant
1018
+ // message. This catches the case where waiting_approval→idle transitions occur before
1019
+ // the modal has been resolved (e.g. the PTY rendered the prompt but no button press fired).
1020
+ try {
1021
+ const screenText = typeof (this.adapter as any).getScreenText === 'function'
1022
+ ? String((this.adapter as any).getScreenText() || '')
1023
+ : '';
1024
+ if (screenText) {
1025
+ const tailLines = screenText.split(/\r?\n/).slice(-16).join('\n');
1026
+ if (looksLikeActiveApprovalPromptText(tailLines)) {
1027
+ return { reason: 'screen_shows_approval_prompt', terminal: approvalResolvedIdle };
1028
+ }
1029
+ }
1030
+ } catch { /* defensive: screen text read is best-effort */ }
794
1031
 
795
1032
  return null;
796
1033
  }
@@ -817,10 +1054,11 @@ export class CliProviderInstance implements ProviderInstance {
817
1054
  return;
818
1055
  }
819
1056
 
820
- const blockReason = this.getCompletedFinalizationBlockReason(latestVisibleStatus);
821
- if (blockReason) {
1057
+ const block = this.getCompletedFinalizationBlock(latestVisibleStatus, pending);
1058
+ if (block) {
1059
+ const blockReason = block.reason;
822
1060
  const waitedMs = Date.now() - pending.firstObservedAt;
823
- if (waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
1061
+ if (block.terminal || waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
824
1062
  if (pending.loggedBlockReason !== blockReason) {
825
1063
  LOG.info('CLI', `[${this.type}] waiting to emit completed until transcript finalizes (${blockReason})`);
826
1064
  pending.loggedBlockReason = blockReason;
@@ -828,10 +1066,29 @@ export class CliProviderInstance implements ProviderInstance {
828
1066
  this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
829
1067
  return;
830
1068
  }
831
- LOG.warn('CLI', `[${this.type}] suppressed completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
1069
+ const completionDiagnostic = this.buildCompletedFinalizationDiagnostic({
1070
+ blockReason,
1071
+ latestStatus,
1072
+ latestVisibleStatus,
1073
+ waitedMs,
1074
+ pending,
1075
+ emittedAfterFinalizationTimeout: true,
1076
+ });
1077
+ LOG.warn('CLI', `[${this.type}] emitting completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
1078
+ this.pushEvent({
1079
+ event: 'agent:generating_completed',
1080
+ chatTitle: pending.chatTitle,
1081
+ duration: pending.duration,
1082
+ timestamp: pending.timestamp,
1083
+ finalSummary: blockReason.startsWith('parsed_status:')
1084
+ ? ''
1085
+ : extractFinalSummaryFromMessages(this.adapter?.getScriptParsedStatus()?.messages),
1086
+ completionDiagnostic,
1087
+ });
832
1088
  this.completedDebouncePending = null;
833
1089
  this.completedDebounceTimer = null;
834
1090
  this.generatingStartedAt = 0;
1091
+ this.lastApprovalEventFingerprint = '';
835
1092
  return;
836
1093
  }
837
1094
 
@@ -846,6 +1103,7 @@ export class CliProviderInstance implements ProviderInstance {
846
1103
  this.completedDebouncePending = null;
847
1104
  this.completedDebounceTimer = null;
848
1105
  this.generatingStartedAt = 0;
1106
+ this.lastApprovalEventFingerprint = '';
849
1107
  }
850
1108
 
851
1109
  private maybeAutoApproveStatus(adapterStatus: any, now = Date.now()): boolean {
@@ -853,15 +1111,46 @@ export class CliProviderInstance implements ProviderInstance {
853
1111
  // Guard re-entry: onStatusChange/getState can observe the same modal multiple
854
1112
  // times while the PTY absorbs the approval key. Without this flag, repeated
855
1113
  // snapshots would write stray keys into the input once the modal dismisses.
856
- if (autoApproveActive && !this.autoApproveBusy) {
1114
+ // However, Claude Code can present a second approval immediately after the
1115
+ // first. Resolve a changed modal signature even while the previous write is
1116
+ // still inside the short busy window.
1117
+ if (!autoApproveActive) {
1118
+ this.lastAutoApprovalSignature = '';
1119
+ return autoApproveActive;
1120
+ }
1121
+ const modal = adapterStatus.activeModal;
1122
+ // (fix) Do not auto-approve when no concrete modal/buttons are present.
1123
+ // Claude TUI flaps between paints; without this guard adapterStatus
1124
+ // could report status=waiting_approval with activeModal=null (or with
1125
+ // an empty buttons array briefly) and we'd still call
1126
+ // resolveModal(-1) — which used to type "1" into the prompt
1127
+ // repeatedly. Skip until a real modal is captured.
1128
+ const buttons = Array.isArray(modal?.buttons)
1129
+ ? modal.buttons.map((b: any) => String(b || '').trim()).filter(Boolean)
1130
+ : [];
1131
+ if (!modal || buttons.length === 0) {
1132
+ return autoApproveActive;
1133
+ }
1134
+ const { index: buttonIndex, label: buttonLabel } = pickAutoApprovalButton(buttons);
1135
+ if (buttonIndex < 0) {
1136
+ // No concrete button matched — don't pick a random index, just
1137
+ // surface the modal so the user can decide.
1138
+ return autoApproveActive;
1139
+ }
1140
+ const signature = [
1141
+ typeof modal?.message === 'string' ? modal.message.trim() : '',
1142
+ buttons.join('|'),
1143
+ buttonIndex,
1144
+ ].join('::');
1145
+ if (!this.autoApproveBusy || signature !== this.lastAutoApprovalSignature) {
857
1146
  this.autoApproveBusy = true;
1147
+ this.lastAutoApprovalSignature = signature;
858
1148
  if (this.autoApproveBusyTimer) clearTimeout(this.autoApproveBusyTimer);
859
1149
  this.autoApproveBusyTimer = setTimeout(() => {
860
1150
  this.autoApproveBusy = false;
861
1151
  this.autoApproveBusyTimer = null;
1152
+ this.lastAutoApprovalSignature = '';
862
1153
  }, 2000);
863
- const modal = adapterStatus.activeModal;
864
- const { index: buttonIndex, label: buttonLabel } = pickApprovalButton(modal?.buttons, this.provider);
865
1154
  this.recordAutoApproval(modal?.message, buttonLabel, now);
866
1155
  setTimeout(() => {
867
1156
  this.adapter.resolveModal(buttonIndex);
@@ -876,6 +1165,13 @@ export class CliProviderInstance implements ProviderInstance {
876
1165
  // during long-running CLI sessions. Keep this path on adapter-owned light
877
1166
  // state only; rich provider parsing is reserved for getState/read_chat.
878
1167
  const adapterStatus = this.adapter.getStatus({ allowParse: false });
1168
+ const adapterProviderSessionId = normalizeProviderSessionId(
1169
+ this.provider,
1170
+ typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
1171
+ );
1172
+ if (adapterProviderSessionId) {
1173
+ this.promoteProviderSessionId(adapterProviderSessionId);
1174
+ }
879
1175
  const parsedStatus = null;
880
1176
  const rawStatus = adapterStatus.status;
881
1177
  const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, now);
@@ -926,10 +1222,14 @@ export class CliProviderInstance implements ProviderInstance {
926
1222
  if (!this.generatingStartedAt) this.generatingStartedAt = now;
927
1223
  const modal = adapterStatus.activeModal;
928
1224
  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;
1225
+ const approvalFingerprint = JSON.stringify({
1226
+ message: typeof modal?.message === 'string' ? modal.message.trim() : '',
1227
+ buttons: Array.isArray(modal?.buttons) ? modal.buttons.map((button: unknown) => String(button).trim()) : [],
1228
+ });
1229
+ // PTY redraws can briefly leave waiting_approval and then re-enter it.
1230
+ // Keep one coordinator event per logical modal until the turn completes.
1231
+ if (this.lastStatus !== 'waiting_approval' && approvalFingerprint !== this.lastApprovalEventFingerprint) {
1232
+ this.lastApprovalEventFingerprint = approvalFingerprint;
933
1233
  this.appendRuntimeSystemMessage(
934
1234
  this.formatApprovalRequestMessage(modal?.message, modal?.buttons),
935
1235
  `approval_request:${now}`,
@@ -943,20 +1243,65 @@ export class CliProviderInstance implements ProviderInstance {
943
1243
  }
944
1244
  } else if (newStatus === 'idle' && (this.lastStatus === 'generating' || this.lastStatus === 'waiting_approval')) {
945
1245
  const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : 0;
946
- // If debounce still pending (generating lasted < 1s), cancel both events
1246
+ // If debounce still pending (generating lasted < 1s), cancel both UI events.
1247
+ // Still emit agent:generating_completed so mesh orchestration can record
1248
+ // task_completed for direct dispatches that complete faster than the debounce.
947
1249
  if (this.generatingDebouncePending) {
948
- LOG.info('CLI', `[${this.type}] suppressed short generating (${now - this.generatingStartedAt}ms)`);
1250
+ const shortDurationMs = this.generatingStartedAt ? now - this.generatingStartedAt : 0;
1251
+ LOG.info('CLI', `[${this.type}] suppressed short generating (${shortDurationMs}ms)`);
949
1252
  if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
950
1253
  this.generatingDebouncePending = null;
951
1254
  this.generatingStartedAt = 0;
1255
+ // Emit completion for mesh task association even though the UI generating
1256
+ // started/completed pair is suppressed (too short for visible UI update).
1257
+ let shortFinalSummary: string | undefined;
1258
+ try { shortFinalSummary = extractFinalSummaryFromMessages(this.adapter?.getScriptParsedStatus()?.messages); } catch { /* best-effort */ }
1259
+ if ((this.provider as any).requiresFinalAssistantBeforeIdle === true && !shortFinalSummary) {
1260
+ LOG.info('CLI', `[${this.type}] suppressed short completion without final assistant evidence`);
1261
+ } else {
1262
+ this.pushEvent({
1263
+ event: 'agent:generating_completed',
1264
+ chatTitle,
1265
+ duration: 0,
1266
+ timestamp: now,
1267
+ finalSummary: shortFinalSummary,
1268
+ completionDiagnostic: {
1269
+ reason: 'short_generating_suppressed',
1270
+ shortDurationMs,
1271
+ },
1272
+ });
1273
+ }
952
1274
  } else {
953
1275
  // Debounce completed, then require the rich transcript path that read_chat
954
1276
  // uses to show an idle turn whose last user-facing message is assistant.
955
- this.completedDebouncePending = { chatTitle, duration, timestamp: now, firstObservedAt: now };
1277
+ this.completedDebouncePending = {
1278
+ chatTitle,
1279
+ duration,
1280
+ timestamp: now,
1281
+ firstObservedAt: now,
1282
+ previousStatus: this.lastStatus,
1283
+ };
956
1284
  this.scheduleCompletedDebounceFlush(3000);
957
1285
  }
958
1286
  } else if (newStatus === 'idle' && this.lastStatus === 'starting') {
959
1287
  this.pushEvent({ event: 'agent:ready', chatTitle, timestamp: now });
1288
+ } else if (newStatus === 'error') {
1289
+ if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
1290
+ this.generatingDebouncePending = null;
1291
+ if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
1292
+ this.completedDebouncePending = null;
1293
+ this.errorMessage = adapterStatus.errorMessage || this.errorMessage;
1294
+ this.errorReason = (adapterStatus.errorReason as ProviderErrorReason) || this.errorReason;
1295
+ this.pushEvent({
1296
+ event: 'agent:stopped',
1297
+ chatTitle,
1298
+ timestamp: now,
1299
+ finalSummary: adapterStatus.errorMessage || adapterStatus.errorReason || 'Provider reported an error',
1300
+ completionDiagnostic: {
1301
+ reason: adapterStatus.errorReason || 'provider_error',
1302
+ errorMessage: adapterStatus.errorMessage || undefined,
1303
+ },
1304
+ });
960
1305
  } else if (newStatus === 'stopped') {
961
1306
  // Cancel any pending debounce
962
1307
  if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
@@ -977,7 +1322,31 @@ export class CliProviderInstance implements ProviderInstance {
977
1322
  // Monitor check (cooldown based notification, IDE/CLI common)
978
1323
  const agentKey = `${this.type}:cli`;
979
1324
  const monitorEvents = this.monitor.check(agentKey, newStatus, now, progressFingerprint);
1325
+ const monitorParsedStatus: any = parsedStatus;
980
1326
  for (const me of monitorEvents) {
1327
+ if (
1328
+ me.type === 'monitor:long_generating'
1329
+ && this.completionHasFinalAssistantMessage(monitorParsedStatus?.messages)
1330
+ && !this.hasAdapterPendingResponse()
1331
+ && !hasNonEmptyCliModalButtons(monitorParsedStatus?.activeModal ?? monitorParsedStatus?.modal)
1332
+ ) {
1333
+ this.pushEvent({
1334
+ event: 'agent:generating_completed',
1335
+ chatTitle,
1336
+ duration: this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : undefined,
1337
+ timestamp: me.timestamp,
1338
+ finalSummary: extractFinalSummaryFromMessages(monitorParsedStatus?.messages),
1339
+ completionDiagnostic: {
1340
+ providerType: this.type,
1341
+ sessionId: this.instanceId,
1342
+ providerSessionId: this.providerSessionId || null,
1343
+ reconciliationReason: 'long_generating_monitor_final_summary',
1344
+ finalAssistantPresent: true,
1345
+ },
1346
+ });
1347
+ this.generatingStartedAt = 0;
1348
+ continue;
1349
+ }
981
1350
  this.pushEvent({ event: me.type, agentKey: me.agentKey, message: me.message, elapsedSec: me.elapsedSec, timestamp: me.timestamp });
982
1351
  }
983
1352
  }
@@ -1258,12 +1627,28 @@ export class CliProviderInstance implements ProviderInstance {
1258
1627
  index,
1259
1628
  source: 'parsed',
1260
1629
  }));
1630
+ const getRole = (message: ChatMessage): string => typeof message.role === 'string'
1631
+ ? message.role.trim().toLowerCase()
1632
+ : '';
1261
1633
  const runtimeEntries: MergeEntry[] = this.runtimeMessages.map((entry, index) => ({
1262
1634
  message: entry.message,
1263
1635
  index: parsedMessages.length + index,
1264
- source: 'runtime',
1636
+ source: 'runtime' as const,
1265
1637
  runtimeKey: entry.key,
1266
- }));
1638
+ })).filter((entry) => {
1639
+ const meta = entry.message.meta && typeof entry.message.meta === 'object' && !Array.isArray(entry.message.meta)
1640
+ ? entry.message.meta as Record<string, unknown>
1641
+ : {};
1642
+ if (meta.runtimeInputAck !== true) return true;
1643
+ const runtimeText = flattenContent(entry.message.content).replace(/\s+/g, ' ').trim();
1644
+ if (!runtimeText) return false;
1645
+ return !parsedEntries.some((parsedEntry) => {
1646
+ const parsedRole = getRole(parsedEntry.message);
1647
+ if (parsedRole !== 'user' && parsedRole !== 'human') return false;
1648
+ const parsedText = flattenContent(parsedEntry.message.content).replace(/\s+/g, ' ').trim();
1649
+ return parsedText === runtimeText;
1650
+ });
1651
+ });
1267
1652
  const getTime = (message: ChatMessage): number => {
1268
1653
  const value = typeof message.receivedAt === 'number'
1269
1654
  ? message.receivedAt
@@ -1273,9 +1658,6 @@ export class CliProviderInstance implements ProviderInstance {
1273
1658
  return Number.isFinite(value) && value > 0 ? value : 0;
1274
1659
  };
1275
1660
 
1276
- const getRole = (message: ChatMessage): string => typeof message.role === 'string'
1277
- ? message.role.trim().toLowerCase()
1278
- : '';
1279
1661
  const isRuntimeOverlay = (entry: MergeEntry): boolean => {
1280
1662
  if (entry.source !== 'runtime') return false;
1281
1663
  const key = typeof entry.runtimeKey === 'string' ? entry.runtimeKey.trim().toLowerCase() : '';
@@ -1341,7 +1723,9 @@ export class CliProviderInstance implements ProviderInstance {
1341
1723
  this.providerSessionId = nextSessionId;
1342
1724
  this.historyWriter.promoteHistorySession(this.type, previousHistorySessionId, nextSessionId);
1343
1725
  this.historyWriter.writeSessionStart(this.type, nextSessionId, this.workingDir, this.instanceId);
1344
- this.restorePersistedHistoryFromCurrentSession();
1726
+ if (this.shouldHydrateExistingProviderHistory()) {
1727
+ this.restorePersistedHistoryFromCurrentSession();
1728
+ }
1345
1729
  this.adapter.updateRuntimeMeta({ providerSessionId: nextSessionId });
1346
1730
  this.onProviderSessionResolved?.({
1347
1731
  instanceId: this.instanceId,
@@ -1354,9 +1738,27 @@ export class CliProviderInstance implements ProviderInstance {
1354
1738
  LOG.info('CLI', `[${this.type}] discovered provider session id: ${nextSessionId}`);
1355
1739
  }
1356
1740
 
1741
+ private shouldHydrateExistingProviderHistory(): boolean {
1742
+ return this.launchMode === 'resume' || this.launchMode === 'manual';
1743
+ }
1744
+
1745
+ private shouldSuppressFreshLaunchStartupReplay(parsedMessages: unknown[], parsedStatus: any, adapterStatus: any, parsedProviderSessionId = ''): boolean {
1746
+ if (this.launchMode !== 'new') return false;
1747
+ if (this.providerSessionId) return false;
1748
+ if (!Array.isArray(parsedMessages) || parsedMessages.length === 0) return false;
1749
+ if (!isIdleStatus(adapterStatus?.status) || !isIdleStatus(parsedStatus?.status)) return false;
1750
+ if (parsedProviderSessionId) return true;
1751
+
1752
+ const newestMessageAt = parsedMessages.reduce<number>((newest, message) => Math.max(newest, getMessageTime(message)), 0);
1753
+
1754
+ // Untimestamped idle parser output during a fresh launch is usually the
1755
+ // provider's last workspace transcript before a new turn exists.
1756
+ return newestMessageAt === 0;
1757
+ }
1758
+
1357
1759
  private syncCanonicalSavedHistoryIfNeeded(): boolean {
1358
1760
  if (!this.providerSessionId) return false;
1359
- const canonicalHistory = this.provider.canonicalHistory;
1761
+ const canonicalHistory = this.provider.nativeHistory;
1360
1762
  if (!canonicalHistory) return false;
1361
1763
 
1362
1764
  if (isNativeSourceCanonicalHistory(canonicalHistory)) {
@@ -1418,9 +1820,9 @@ export class CliProviderInstance implements ProviderInstance {
1418
1820
  private restorePersistedHistoryFromCurrentSession(): void {
1419
1821
  if (!this.providerSessionId) return;
1420
1822
  this.syncCanonicalSavedHistoryIfNeeded();
1421
- const restoredHistory = isNativeSourceCanonicalHistory(this.provider.canonicalHistory)
1823
+ const restoredHistory = isNativeSourceCanonicalHistory(this.provider.nativeHistory)
1422
1824
  ? readProviderChatHistory(this.type, {
1423
- canonicalHistory: this.provider.canonicalHistory,
1825
+ canonicalHistory: this.provider.nativeHistory,
1424
1826
  historySessionId: this.providerSessionId,
1425
1827
  workspace: this.workingDir,
1426
1828
  offset: 0,