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

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 (223) hide show
  1. package/dist/boot/process-hardening.d.ts +50 -0
  2. package/dist/chat/source-machine.d.ts +166 -0
  3. package/dist/chat/source-resolver.d.ts +104 -0
  4. package/dist/chat/subscription-updates.d.ts +1 -0
  5. package/dist/cli-adapter-types.d.ts +22 -2
  6. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  7. package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
  8. package/dist/cli-adapters/provider-cli-adapter.d.ts +106 -63
  9. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  10. package/dist/cli-adapters/provider-cli-shared.d.ts +29 -0
  11. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  12. package/dist/commands/handler.d.ts +110 -0
  13. package/dist/commands/mesh-coordinator.d.ts +72 -1
  14. package/dist/commands/router.d.ts +22 -0
  15. package/dist/config/chat-history.d.ts +8 -0
  16. package/dist/config/config.d.ts +5 -0
  17. package/dist/config/mesh-config.d.ts +72 -1
  18. package/dist/git/git-commands.d.ts +5 -1
  19. package/dist/index.d.ts +35 -8
  20. package/dist/index.js +19937 -4800
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +20357 -5290
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/installer.d.ts +1 -4
  25. package/dist/ipc/local-ipc-server.d.ts +91 -0
  26. package/dist/launch.d.ts +1 -1
  27. package/dist/logging/async-batch-writer.d.ts +10 -0
  28. package/dist/mesh/beads-db.d.ts +72 -0
  29. package/dist/mesh/contracts.d.ts +164 -0
  30. package/dist/mesh/coordinator-prompt.d.ts +30 -0
  31. package/dist/mesh/coordinator-registry.d.ts +59 -0
  32. package/dist/mesh/mesh-active-work.d.ts +90 -0
  33. package/dist/mesh/mesh-events.d.ts +97 -5
  34. package/dist/mesh/mesh-fast-forward.d.ts +41 -0
  35. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  36. package/dist/mesh/mesh-ledger.d.ts +58 -1
  37. package/dist/mesh/mesh-refine-status.d.ts +26 -0
  38. package/dist/mesh/mesh-work-queue.d.ts +54 -6
  39. package/dist/mesh/preview-freshness.d.ts +18 -0
  40. package/dist/mesh/refine-config.d.ts +193 -0
  41. package/dist/mesh/worktree-bootstrap-config.d.ts +113 -0
  42. package/dist/providers/approval-utils.d.ts +13 -0
  43. package/dist/providers/cli-provider-instance.d.ts +15 -3
  44. package/dist/providers/contracts.d.ts +98 -5
  45. package/dist/providers/external-sources.d.ts +71 -0
  46. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  47. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  48. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  49. package/dist/providers/native-history/dispatcher.d.ts +25 -0
  50. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  51. package/dist/providers/native-history/index.d.ts +13 -0
  52. package/dist/providers/provider-instance.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 +159 -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 +102 -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 +3495 -502
  117. package/src/config/chat-history.ts +91 -24
  118. package/src/config/config.ts +12 -0
  119. package/src/config/mesh-config.ts +280 -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 -7
  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 +486 -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 +982 -67
  139. package/src/mesh/mesh-fast-forward.ts +438 -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 +263 -159
  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 +598 -55
  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 +231 -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 +453 -0
  203. package/src/providers/spec/driver.ts +540 -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 +706 -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 +112 -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
  222. package/dist/mesh/mesh-sync.d.ts +0 -53
  223. package/src/mesh/mesh-sync.ts +0 -111
@@ -13,15 +13,17 @@ import { createRequire } from 'node:module';
13
13
  import { normalizeInputEnvelope, type ProviderModule, flattenContent, type InputEnvelope, type InputPart } from './contracts.js';
14
14
  import { assertProviderSupportsDeclaredInput, getEffectiveMessageInputSupport } from './provider-input-support.js';
15
15
  import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext, ProviderErrorReason, HotChatSessionState, SessionModalState } from './provider-instance.js';
16
+ import { normalizeInteractivePrompt, normalizeInteractivePromptResponse, type InteractivePrompt } from './types/interactive-prompt.js';
16
17
  import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
17
18
  import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
19
+ import { createCliAdapter } from './spec/route.js';
18
20
  import type { PtyRuntimeMetadata, PtyTransportFactory } from '../cli-adapters/pty-transport.js';
19
21
  import { StatusMonitor } from './status-monitor.js';
20
22
  import { ChatHistoryWriter, isNativeSourceCanonicalHistory, materializeProviderNativeHistory, readChatHistory, readProviderChatHistory } from '../config/chat-history.js';
21
23
  import { LOG } from '../logging/logger.js';
22
24
  import type { ChatMessage } from '../types.js';
23
25
  import { buildPersistedProviderEffectMessage, normalizeProviderEffects } from './control-effects.js';
24
- import { formatAutoApprovalMessage, pickApprovalButton } from './approval-utils.js';
26
+ import { formatAutoApprovalMessage, pickApprovalButton, pickAutoApprovalButton, looksLikeActiveApprovalPromptText } from './approval-utils.js';
25
27
  import { getCliScriptCommand, parseCliScriptResult } from './cli-script-results.js';
26
28
  import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
27
29
  import { normalizeProviderSessionId } from './provider-session-id.js';
@@ -40,7 +42,45 @@ type CompletedDebouncePending = {
40
42
  duration: number;
41
43
  timestamp: number;
42
44
  firstObservedAt: number;
45
+ previousStatus: string;
43
46
  loggedBlockReason?: string;
47
+ loggedTranscriptProbe?: boolean;
48
+ transcriptProbeHistory?: ExternalTranscriptProbe[];
49
+ };
50
+
51
+ function isIdleStatus(value: unknown): boolean {
52
+ const status = typeof value === 'string' ? value.trim().toLowerCase() : '';
53
+ return !status || status === 'idle' || status === 'ready';
54
+ }
55
+
56
+
57
+ function getMessageTime(message: unknown): number {
58
+ if (!message || typeof message !== 'object') return 0;
59
+ const record = message as { receivedAt?: unknown; timestamp?: unknown };
60
+ const value = Number(record.receivedAt ?? record.timestamp ?? 0);
61
+ return Number.isFinite(value) ? value : 0;
62
+ }
63
+
64
+ type CompletedFinalizationBlock = {
65
+ reason: string;
66
+ terminal?: boolean;
67
+ };
68
+
69
+ type CompletionFinalAssistantEvidence = {
70
+ present: boolean;
71
+ messages: unknown[];
72
+ source: 'parsed' | 'external-native' | 'unavailable';
73
+ };
74
+
75
+ type ExternalTranscriptProbe = {
76
+ readAt: number;
77
+ msgCount: number;
78
+ lastRole: string | null;
79
+ lastKind: string | null;
80
+ contentLen: number;
81
+ sourcePath: string | null;
82
+ sourceMtimeMs: number | null;
83
+ mtimeAgeMs: number | null;
44
84
  };
45
85
 
46
86
  const COMPLETED_FINALIZATION_RETRY_MS = 1000;
@@ -309,9 +349,10 @@ export class CliProviderInstance implements ProviderInstance {
309
349
  private monitor: StatusMonitor;
310
350
  private generatingDebounceTimer: NodeJS.Timeout | null = null;
311
351
  private generatingDebouncePending: { chatTitle: string; timestamp: number } | null = null;
312
- private lastApprovalEventAt = 0;
352
+ private lastApprovalEventFingerprint = '';
313
353
  private autoApproveBusy = false;
314
354
  private autoApproveBusyTimer: NodeJS.Timeout | null = null;
355
+ private lastAutoApprovalSignature = '';
315
356
  private controlValues: Record<string, string | number | boolean> = {};
316
357
  private summaryMetadata: unknown = undefined;
317
358
  private appliedEffectKeys = new Set<string>();
@@ -330,6 +371,7 @@ export class CliProviderInstance implements ProviderInstance {
330
371
  private suppressIdleHistoryReplay = false;
331
372
  private errorMessage: string | undefined = undefined;
332
373
  private errorReason: ProviderErrorReason | undefined = undefined;
374
+ private activeInteractivePrompt: InteractivePrompt | null = null;
333
375
 
334
376
  private presentationMode: 'terminal' | 'chat';
335
377
  private providerSessionId?: string;
@@ -370,7 +412,10 @@ export class CliProviderInstance implements ProviderInstance {
370
412
  this.providerSessionId = options?.providerSessionId;
371
413
  this.launchMode = options?.launchMode || 'new';
372
414
  this.onProviderSessionResolved = options?.onProviderSessionResolved;
373
- this.adapter = new ProviderCliAdapter(provider as CliProviderModule, workingDir, cliArgs, options?.extraEnv || {}, transportFactory);
415
+ this.adapter = createCliAdapter(provider as CliProviderModule, workingDir, cliArgs, options?.extraEnv || {}, transportFactory) as ProviderCliAdapter;
416
+ if (this.providerSessionId) {
417
+ this.adapter.updateRuntimeMeta({ providerSessionId: this.providerSessionId });
418
+ }
374
419
  this.monitor = new StatusMonitor();
375
420
  this.historyWriter = new ChatHistoryWriter();
376
421
  }
@@ -412,7 +457,7 @@ export class CliProviderInstance implements ProviderInstance {
412
457
  await this.adapter.spawn();
413
458
  await this.enforceFreshSessionLaunchIfNeeded();
414
459
  this.maybeAppendRuntimeRecoveryMessage(this.adapter.getRuntimeMetadata());
415
- if (this.providerSessionId) {
460
+ if (this.providerSessionId && this.shouldHydrateExistingProviderHistory()) {
416
461
  this.restorePersistedHistoryFromCurrentSession();
417
462
  }
418
463
  if (this.providerSessionId && this.launchMode === 'resume') {
@@ -486,14 +531,38 @@ export class CliProviderInstance implements ProviderInstance {
486
531
  }
487
532
 
488
533
  getState(): ProviderState {
534
+ // TODO(phase5-sandbox): JS override scripts (detectStatus, parseApproval,
535
+ // parseSession) are currently invoked by CliScriptRunner.invoke() via direct
536
+ // function calls — the scripts run in the daemon process with full Node.js
537
+ // access and no resource limits.
538
+ //
539
+ // When Phase 5 lands, CliScriptRunner should route these calls through a
540
+ // SandboxedScriptRunner (see providers/sdk/v1/sandbox/script-runner.ts) so
541
+ // that each call gets a fresh isolated-vm context with a 50 ms CPU limit and
542
+ // a 32 MB memory cap. The execution path to change is:
543
+ // CliScriptRunner.invoke() → SandboxedScriptRunner.run(scriptSource, context)
544
+ //
545
+ // This getState() call-site is NOT where the change goes — the wiring belongs
546
+ // in cli-script-runner.ts (CliScriptRunner.detectStatus / parseApproval /
547
+ // parseSession), with provider-loader.ts updated to store script source strings
548
+ // alongside the loaded function references for extended-legacy providers.
489
549
  const adapterStatus = this.adapter.getStatus();
550
+ if (Object.prototype.hasOwnProperty.call(adapterStatus, 'activeInteractivePrompt')) {
551
+ this.activeInteractivePrompt = adapterStatus.activeInteractivePrompt ?? null;
552
+ }
490
553
  let parsedStatus: any = null;
491
554
  let parseErrorMessage: string | undefined;
492
555
  if (typeof this.adapter.getScriptParsedStatus === 'function') {
493
556
  try {
494
557
  parsedStatus = this.adapter.getScriptParsedStatus() || null;
495
- this.errorMessage = undefined;
496
- this.errorReason = undefined;
558
+ const parsedErrorMessage = typeof parsedStatus?.errorMessage === 'string' && parsedStatus.errorMessage.trim()
559
+ ? parsedStatus.errorMessage.trim()
560
+ : undefined;
561
+ const parsedErrorReason = typeof parsedStatus?.errorReason === 'string' && parsedStatus.errorReason.trim()
562
+ ? parsedStatus.errorReason.trim() as ProviderErrorReason
563
+ : undefined;
564
+ this.errorMessage = parsedErrorMessage;
565
+ this.errorReason = parsedErrorReason;
497
566
  } catch (error: any) {
498
567
  parseErrorMessage = error?.message || String(error);
499
568
  this.errorMessage = parseErrorMessage;
@@ -503,22 +572,39 @@ export class CliProviderInstance implements ProviderInstance {
503
572
  this.errorMessage = undefined;
504
573
  this.errorReason = undefined;
505
574
  }
575
+ const adapterProviderSessionId = normalizeProviderSessionId(
576
+ this.provider,
577
+ typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
578
+ );
506
579
  const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, Date.now());
507
- const visibleStatus = parseErrorMessage
580
+ const visibleStatus = parseErrorMessage || parsedStatus?.status === 'error'
508
581
  ? 'error'
509
582
  : (autoApproveActive ? 'generating' : adapterStatus.status);
583
+ const runtime = this.adapter.getRuntimeMetadata();
584
+ this.maybeAppendRuntimeRecoveryMessage(runtime);
585
+ let parsedMessages = Array.isArray(parsedStatus?.messages)
586
+ ? parsedStatus.messages
587
+ : [];
510
588
  const parsedProviderSessionId = normalizeProviderSessionId(
511
589
  this.provider,
512
590
  typeof parsedStatus?.providerSessionId === 'string' ? parsedStatus.providerSessionId : '',
513
591
  );
514
- if (parsedProviderSessionId) {
592
+ const suppressFreshLaunchStartupReplay = this.shouldSuppressFreshLaunchStartupReplay(
593
+ parsedMessages,
594
+ parsedStatus,
595
+ adapterStatus,
596
+ parsedProviderSessionId,
597
+ );
598
+ if (adapterProviderSessionId && !suppressFreshLaunchStartupReplay) {
599
+ this.promoteProviderSessionId(adapterProviderSessionId);
600
+ }
601
+ if (parsedProviderSessionId && !suppressFreshLaunchStartupReplay) {
515
602
  this.promoteProviderSessionId(parsedProviderSessionId);
516
603
  }
517
- const runtime = this.adapter.getRuntimeMetadata();
518
- this.maybeAppendRuntimeRecoveryMessage(runtime);
519
- let parsedMessages = Array.isArray(parsedStatus?.messages)
520
- ? parsedStatus.messages
521
- : [];
604
+ if (suppressFreshLaunchStartupReplay) {
605
+ parsedMessages = [];
606
+ }
607
+ const activeChatId = this.providerSessionId || runtime?.runtimeId || this.instanceId;
522
608
  const historyMessageCount = Number.isFinite(parsedStatus?.historyMessageCount)
523
609
  ? Math.max(0, Number(parsedStatus.historyMessageCount))
524
610
  : null;
@@ -528,7 +614,18 @@ export class CliProviderInstance implements ProviderInstance {
528
614
  : [];
529
615
  }
530
616
  const mergedMessages = this.mergeConversationMessages(parsedMessages);
531
- const canonicalBackedHistory = this.syncCanonicalSavedHistoryIfNeeded();
617
+ const canonicalBackedHistory = this.shouldHydrateExistingProviderHistory()
618
+ ? this.syncCanonicalSavedHistoryIfNeeded()
619
+ : false;
620
+ const statusMessages = canonicalBackedHistory && this.lastPersistedHistoryMessages.length > 0
621
+ ? this.lastPersistedHistoryMessages.map((message) => ({
622
+ role: message.role,
623
+ content: message.content,
624
+ kind: message.kind,
625
+ senderName: message.senderName,
626
+ receivedAt: message.receivedAt,
627
+ }))
628
+ : mergedMessages;
532
629
 
533
630
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
534
631
  const parsedChatStatus = typeof parsedStatus?.status === 'string' && parsedStatus.status.trim()
@@ -572,7 +669,12 @@ export class CliProviderInstance implements ProviderInstance {
572
669
  }
573
670
  }
574
671
 
575
- this.applyProviderResponse(parsedStatus, { phase: 'immediate' });
672
+ this.applyProviderResponse(
673
+ suppressFreshLaunchStartupReplay && parsedStatus && typeof parsedStatus === 'object'
674
+ ? { ...parsedStatus, providerSessionId: undefined }
675
+ : parsedStatus,
676
+ { phase: 'immediate' },
677
+ );
576
678
  const surface = resolveProviderStateSurface({
577
679
  summaryMetadata: this.summaryMetadata as any,
578
680
  controlValues: this.controlValues,
@@ -592,13 +694,15 @@ export class CliProviderInstance implements ProviderInstance {
592
694
  status: visibleStatus,
593
695
  mode: this.presentationMode,
594
696
  activeChat: {
595
- id: `${this.type}_${this.workingDir}`,
697
+ id: activeChatId,
596
698
  title: parsedStatus?.title || dirName,
597
699
  status: activeChatStatus,
598
- messages: mergedMessages,
700
+ messages: statusMessages,
599
701
  activeModal: autoApproveActive ? null : (parsedStatus?.activeModal ?? adapterStatus.activeModal),
702
+ activeInteractivePrompt: this.activeInteractivePrompt,
600
703
  inputContent: '',
601
704
  },
705
+ activeInteractivePrompt: this.activeInteractivePrompt,
602
706
  workspace: this.workingDir,
603
707
  instanceId: this.instanceId,
604
708
  providerSessionId: this.providerSessionId,
@@ -651,13 +755,17 @@ export class CliProviderInstance implements ProviderInstance {
651
755
  };
652
756
  }
653
757
 
654
- getSessionModalState(): SessionModalState {
655
- const adapterStatus = this.adapter.getStatus({ allowParse: false });
758
+ getSessionModalState(sessionId?: string): SessionModalState {
759
+ const adapterStatus = this.adapter.getStatus({ allowParse: true });
656
760
  const autoApproveActive = adapterStatus.status === 'waiting_approval' && this.shouldAutoApprove();
657
761
  const visibleStatus = autoApproveActive ? 'generating' : adapterStatus.status;
658
762
  const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
659
763
  return {
660
- id: this.instanceId,
764
+ // Honor the caller-supplied sessionId — InstanceMgr rejects the
765
+ // projection when projected.id !== requested sessionId, and
766
+ // this.instanceId is the manager's internal key, not the public
767
+ // sessionId the dashboard subscribes by.
768
+ id: sessionId ?? this.instanceId,
661
769
  status: visibleStatus,
662
770
  title: dirName,
663
771
  activeModal: autoApproveActive ? null : adapterStatus.activeModal,
@@ -690,11 +798,65 @@ export class CliProviderInstance implements ProviderInstance {
690
798
  void this.adapter.resolveAction(data).catch((e: any) => {
691
799
  LOG.warn('CLI', `[${this.type}] resolve_action failed: ${e?.message || e}`);
692
800
  });
801
+ } else if (event === 'interactive_prompt' && data) {
802
+ const prompt = normalizeInteractivePrompt(data);
803
+ if (prompt) {
804
+ this.activeInteractivePrompt = prompt;
805
+ this.events.push({
806
+ event: 'interactive_prompt',
807
+ timestamp: Date.now(),
808
+ promptId: prompt.promptId,
809
+ });
810
+ }
811
+ } else if (event === 'interactive_prompt_response' && data) {
812
+ try {
813
+ const response = normalizeInteractivePromptResponse(data);
814
+ if (this.activeInteractivePrompt?.promptId === response.promptId) {
815
+ this.activeInteractivePrompt = null;
816
+ }
817
+ if (typeof this.adapter.setInteractivePromptResponse !== 'function') {
818
+ LOG.warn('CLI', `[${this.type}] interactive_prompt_response ignored: adapter does not support interactive prompts`);
819
+ return;
820
+ }
821
+ void this.adapter.setInteractivePromptResponse(response).catch((e: any) => {
822
+ LOG.warn('CLI', `[${this.type}] interactive_prompt_response failed: ${e?.message || e}`);
823
+ });
824
+ } catch (e: any) {
825
+ LOG.warn('CLI', `[${this.type}] invalid interactive_prompt_response: ${e?.message || e}`);
826
+ }
693
827
  } else if (event === 'provider_state_patch' && data && typeof data === 'object') {
694
828
  this.applyProviderResponse(data, { phase: 'immediate' });
695
829
  }
696
830
  }
697
831
 
832
+ recordAcknowledgedUserInput(input: InputEnvelope | string): void {
833
+ const content = typeof input === 'string'
834
+ ? input.trim()
835
+ : buildCliStructuredInputPrompt(input).trim();
836
+ if (!content) return;
837
+
838
+ const receivedAt = Date.now();
839
+ const dedupKey = `user_input_ack:${crypto
840
+ .createHash('sha256')
841
+ .update(`${this.instanceId}:${content}:${receivedAt}`)
842
+ .digest('hex')
843
+ .slice(0, 24)}`;
844
+ this.appendRuntimeMessage(buildChatMessage({
845
+ role: 'user',
846
+ senderName: 'User',
847
+ kind: 'standard',
848
+ content,
849
+ receivedAt,
850
+ timestamp: receivedAt,
851
+ source: 'runtime_input_ack',
852
+ meta: {
853
+ runtimeInputAck: true,
854
+ provider: this.type,
855
+ workspace: this.workingDir,
856
+ },
857
+ } as ChatMessage), dedupKey);
858
+ }
859
+
698
860
  dispose(): void {
699
861
  this.adapter.shutdown();
700
862
  this.monitor.reset();
@@ -706,6 +868,7 @@ export class CliProviderInstance implements ProviderInstance {
706
868
 
707
869
  private completedDebounceTimer: NodeJS.Timeout | null = null;
708
870
  private completedDebouncePending: CompletedDebouncePending | null = null;
871
+ private lastExternalCompletionProbe: ExternalTranscriptProbe | null = null;
709
872
 
710
873
  private async enforceFreshSessionLaunchIfNeeded(): Promise<void> {
711
874
  const scriptName = getForcedNewSessionScriptName(this.provider, this.launchMode);
@@ -740,7 +903,157 @@ export class CliProviderInstance implements ProviderInstance {
740
903
  const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
741
904
  const role = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : '';
742
905
  const content = lastVisible ? flattenContent(lastVisible.content).trim() : '';
743
- return role === 'assistant' && !!content;
906
+ if (role !== 'assistant' || !content) return false;
907
+ // Guard: if the last assistant message looks like an active approval/input prompt,
908
+ // it is not a real completion — the session is still awaiting user input.
909
+ if (looksLikeActiveApprovalPromptText(content)) return false;
910
+ return true;
911
+ }
912
+
913
+ private buildExternalTranscriptProbe(messages: unknown[], sourcePath?: string, sourceMtimeMs?: number): ExternalTranscriptProbe {
914
+ const visibleMessages = messages.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
915
+ const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
916
+ const readAt = Date.now();
917
+ const mtimeMs = Number(sourceMtimeMs) || 0;
918
+ return {
919
+ readAt,
920
+ msgCount: messages.length,
921
+ lastRole: typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null,
922
+ lastKind: typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null,
923
+ contentLen: lastVisible ? flattenContent(lastVisible.content).trim().length : 0,
924
+ sourcePath: typeof sourcePath === 'string' && sourcePath ? sourcePath : null,
925
+ sourceMtimeMs: mtimeMs || null,
926
+ mtimeAgeMs: mtimeMs ? Math.max(0, readAt - mtimeMs) : null,
927
+ };
928
+ }
929
+
930
+ private recordPendingTranscriptProbe(pending: CompletedDebouncePending): ExternalTranscriptProbe | null {
931
+ const probe = this.lastExternalCompletionProbe;
932
+ if (!probe) return null;
933
+ const history = pending.transcriptProbeHistory || [];
934
+ const last = history[history.length - 1];
935
+ if (!last || last.readAt !== probe.readAt || last.msgCount !== probe.msgCount || last.lastRole !== probe.lastRole || last.contentLen !== probe.contentLen) {
936
+ history.push(probe);
937
+ pending.transcriptProbeHistory = history.slice(-5);
938
+ }
939
+ return probe;
940
+ }
941
+
942
+ private readExternalCompletionMessages(): unknown[] | null {
943
+ const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
944
+ if (!adapterOwnsMessagesElsewhere) return null;
945
+ if (!this.providerSessionId) return null;
946
+ if (!isNativeSourceCanonicalHistory(this.provider.nativeHistory)) return null;
947
+
948
+ if (this.lastExternalCompletionProbe?.sourcePath) {
949
+ try { fs.statSync(this.lastExternalCompletionProbe.sourcePath); } catch { /* best-effort metadata refresh */ }
950
+ }
951
+ const restoredHistory = readProviderChatHistory(this.type, {
952
+ canonicalHistory: this.provider.nativeHistory,
953
+ historySessionId: this.providerSessionId,
954
+ workspace: this.workingDir,
955
+ offset: 0,
956
+ limit: Number.MAX_SAFE_INTEGER,
957
+ historyBehavior: this.provider.historyBehavior,
958
+ scripts: this.provider.scripts as any,
959
+ sessionStartedAtMs: this.startedAt,
960
+ forceRefresh: true,
961
+ });
962
+ if (restoredHistory.source !== 'provider-native') {
963
+ this.lastExternalCompletionProbe = null;
964
+ return null;
965
+ }
966
+ this.lastExternalCompletionProbe = this.buildExternalTranscriptProbe(
967
+ restoredHistory.messages,
968
+ restoredHistory.sourcePath,
969
+ restoredHistory.sourceMtimeMs,
970
+ );
971
+ return restoredHistory.messages;
972
+ }
973
+
974
+ private completionFinalAssistantEvidence(parsedMessages: unknown): CompletionFinalAssistantEvidence {
975
+ if (this.completionHasFinalAssistantMessage(parsedMessages)) {
976
+ return {
977
+ present: true,
978
+ messages: Array.isArray(parsedMessages) ? parsedMessages : [],
979
+ source: 'parsed',
980
+ };
981
+ }
982
+
983
+ const externalMessages = this.readExternalCompletionMessages();
984
+ if (externalMessages) {
985
+ return {
986
+ present: this.completionHasFinalAssistantMessage(externalMessages),
987
+ messages: externalMessages,
988
+ source: 'external-native',
989
+ };
990
+ }
991
+
992
+ return {
993
+ present: false,
994
+ messages: Array.isArray(parsedMessages) ? parsedMessages : [],
995
+ source: 'unavailable',
996
+ };
997
+ }
998
+
999
+ private completionFinalSummary(parsedMessages: unknown): string | undefined {
1000
+ const evidence = this.completionFinalAssistantEvidence(parsedMessages);
1001
+ return extractFinalSummaryFromMessages(evidence.messages as any);
1002
+ }
1003
+
1004
+ private buildCompletedFinalizationDiagnostic(args: {
1005
+ blockReason: string;
1006
+ latestStatus?: any;
1007
+ latestVisibleStatus: string;
1008
+ waitedMs: number;
1009
+ pending: CompletedDebouncePending;
1010
+ emittedAfterFinalizationTimeout: boolean;
1011
+ }): Record<string, unknown> {
1012
+ let parsed: any = null;
1013
+ let parseError: string | undefined;
1014
+ try {
1015
+ parsed = this.adapter.getScriptParsedStatus();
1016
+ } catch (error: any) {
1017
+ parseError = error?.message || String(error);
1018
+ }
1019
+
1020
+ const evidence = this.completionFinalAssistantEvidence(parsed?.messages);
1021
+ if (evidence.source === 'external-native') {
1022
+ this.recordPendingTranscriptProbe(args.pending);
1023
+ }
1024
+ const visibleMessages = (Array.isArray(evidence.messages) ? evidence.messages : [])
1025
+ .filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
1026
+ const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
1027
+ const lastVisibleRole = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null;
1028
+ const lastVisibleKind = typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null;
1029
+ const lastVisibleContentLength = lastVisible ? flattenContent(lastVisible.content).trim().length : 0;
1030
+
1031
+ return {
1032
+ providerType: this.type,
1033
+ sessionId: this.instanceId,
1034
+ providerSessionId: this.providerSessionId || null,
1035
+ workspace: this.workingDir,
1036
+ blockReason: args.blockReason,
1037
+ emittedAfterFinalizationTimeout: args.emittedAfterFinalizationTimeout,
1038
+ waitedMs: args.waitedMs,
1039
+ maxWaitMs: COMPLETED_FINALIZATION_MAX_WAIT_MS,
1040
+ adapterStatus: typeof args.latestStatus?.status === 'string' ? args.latestStatus.status : null,
1041
+ latestVisibleStatus: args.latestVisibleStatus,
1042
+ parsedStatus: typeof parsed?.status === 'string' ? parsed.status : (parseError ? 'parse_error' : 'unknown'),
1043
+ parseError: parseError || undefined,
1044
+ finalAssistantPresent: evidence.present,
1045
+ finalAssistantEvidenceSource: evidence.source,
1046
+ visibleMessageCount: visibleMessages.length,
1047
+ lastVisibleRole,
1048
+ lastVisibleKind,
1049
+ lastVisibleContentLength,
1050
+ pendingStartedAt: this.generatingStartedAt || null,
1051
+ pendingFirstObservedAt: args.pending.firstObservedAt,
1052
+ pendingTimestamp: args.pending.timestamp,
1053
+ pendingDurationSec: args.pending.duration,
1054
+ previousBlockReason: args.pending.loggedBlockReason || null,
1055
+ transcriptProbeHistory: args.pending.transcriptProbeHistory || [],
1056
+ };
744
1057
  }
745
1058
 
746
1059
  private hasAdapterPendingResponse(): boolean {
@@ -765,32 +1078,90 @@ export class CliProviderInstance implements ProviderInstance {
765
1078
  if (!isCliGeneratingLikeStatus(parsedRawStatus)) return false;
766
1079
  if (adapterRawStatus !== 'idle') return false;
767
1080
  if (hasNonEmptyCliModalButtons(parsedStatus?.activeModal ?? parsedStatus?.modal)) return false;
768
- return !this.hasAdapterPendingResponse();
1081
+ if (this.hasAdapterPendingResponse()) return false;
1082
+ // Do not suppress when the adapter's raw response buffer is still non-empty.
1083
+ // This catches the case where isWaitingForResponse has already flipped to false
1084
+ // (so getPartialResponse() returns '') but the provider's native parser still
1085
+ // reports generating because it's parsing buffered content. Suppressing the
1086
+ // finalization block here would emit a false completion event while the provider
1087
+ // session is still actively processing its response stream.
1088
+ const adapterAny = this.adapter as any;
1089
+ if (typeof adapterAny?.responseBuffer === 'string' && adapterAny.responseBuffer.trim()) return false;
1090
+ return true;
769
1091
  }
770
1092
 
771
- private getCompletedFinalizationBlockReason(latestVisibleStatus: string): string | null {
772
- if (latestVisibleStatus !== 'idle') return `status:${latestVisibleStatus}`;
1093
+ private getCompletedFinalizationBlock(latestVisibleStatus: string, pending: CompletedDebouncePending): CompletedFinalizationBlock | null {
1094
+ if (latestVisibleStatus !== 'idle') return { reason: `status:${latestVisibleStatus}`, terminal: true };
773
1095
 
774
1096
  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';
1097
+ const approvalResolvedIdle = pending.previousStatus === 'waiting_approval';
1098
+ if (!approvalResolvedIdle) {
1099
+ if (adapterAny?.isWaitingForResponse === true) return { reason: 'adapter_waiting_for_response', terminal: true };
1100
+ if (adapterAny?.currentTurnScope) return { reason: 'adapter_turn_scope_active', terminal: true };
1101
+ if (this.hasAdapterPendingResponse()) return { reason: 'adapter_pending_response', terminal: true };
1102
+ }
777
1103
 
778
1104
  const partial = typeof this.adapter.getPartialResponse === 'function'
779
1105
  ? this.adapter.getPartialResponse()
780
1106
  : '';
781
- if (typeof partial === 'string' && partial.trim()) return 'partial_response_pending';
1107
+ if (typeof partial === 'string' && partial.trim()) return { reason: 'partial_response_pending', terminal: true };
782
1108
 
783
1109
  let parsed: any;
784
1110
  try {
785
1111
  parsed = this.adapter.getScriptParsedStatus();
786
1112
  } catch (error: any) {
787
- return `parse_error:${error?.message || String(error)}`;
1113
+ return { reason: `parse_error:${error?.message || String(error)}` };
788
1114
  }
789
1115
 
790
1116
  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';
1117
+ if (parsedStatus !== 'idle') {
1118
+ const adapterStatus = this.adapter.getStatus({ allowParse: false });
1119
+ if (this.shouldSuppressStaleParsedBusyStatus(parsed, adapterStatus)) return null;
1120
+ return { reason: `parsed_status:${parsedStatus}`, terminal: isCliGeneratingLikeStatus(parsedStatus) };
1121
+ }
1122
+ if (parsed?.activeModal || parsed?.modal) return { reason: 'parsed_modal_active', terminal: true };
1123
+ const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
1124
+ const finalAssistantEvidence = this.completionFinalAssistantEvidence(parsed?.messages);
1125
+ if (!finalAssistantEvidence.present) {
1126
+ if (adapterOwnsMessagesElsewhere) {
1127
+ if (finalAssistantEvidence.source === 'external-native') {
1128
+ const probe = this.recordPendingTranscriptProbe(pending);
1129
+ if (probe && !pending.loggedTranscriptProbe) {
1130
+ LOG.info('CLI', `[${this.type}] external transcript probe: msgCount=${probe.msgCount} lastRole=${probe.lastRole || 'none'} lastKind=${probe.lastKind || 'none'} contentLen=${probe.contentLen} sourceMtime=${probe.sourceMtimeMs ?? 'unknown'} mtimeAge=${probe.mtimeAgeMs ?? 'unknown'}ms`);
1131
+ pending.loggedTranscriptProbe = true;
1132
+ }
1133
+ return { reason: 'missing_final_assistant', terminal: true };
1134
+ }
1135
+ // SpecCliAdapter never populates parsed.messages — chat history flows
1136
+ // through the daemon's native-history pipeline, not the status hook.
1137
+ // If that pipeline is unavailable, keep the old skip behavior for
1138
+ // providers that have not opted into strict final-assistant evidence.
1139
+ if ((this.provider as any).requiresFinalAssistantBeforeIdle === true) {
1140
+ return { reason: 'missing_final_assistant', terminal: true };
1141
+ }
1142
+ } else {
1143
+ return {
1144
+ reason: 'missing_final_assistant',
1145
+ terminal: (this.provider as any).requiresFinalAssistantBeforeIdle === true,
1146
+ };
1147
+ }
1148
+ }
1149
+
1150
+ // Guard: if the screen still shows an approval/choice prompt as the last visible text,
1151
+ // the turn is not complete even if the parsed status says idle and there is an assistant
1152
+ // message. This catches the case where waiting_approval→idle transitions occur before
1153
+ // the modal has been resolved (e.g. the PTY rendered the prompt but no button press fired).
1154
+ try {
1155
+ const screenText = typeof (this.adapter as any).getScreenText === 'function'
1156
+ ? String((this.adapter as any).getScreenText() || '')
1157
+ : '';
1158
+ if (screenText) {
1159
+ const tailLines = screenText.split(/\r?\n/).slice(-16).join('\n');
1160
+ if (looksLikeActiveApprovalPromptText(tailLines)) {
1161
+ return { reason: 'screen_shows_approval_prompt', terminal: approvalResolvedIdle };
1162
+ }
1163
+ }
1164
+ } catch { /* defensive: screen text read is best-effort */ }
794
1165
 
795
1166
  return null;
796
1167
  }
@@ -817,10 +1188,11 @@ export class CliProviderInstance implements ProviderInstance {
817
1188
  return;
818
1189
  }
819
1190
 
820
- const blockReason = this.getCompletedFinalizationBlockReason(latestVisibleStatus);
821
- if (blockReason) {
1191
+ const block = this.getCompletedFinalizationBlock(latestVisibleStatus, pending);
1192
+ if (block) {
1193
+ const blockReason = block.reason;
822
1194
  const waitedMs = Date.now() - pending.firstObservedAt;
823
- if (waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
1195
+ if (block.terminal || waitedMs < COMPLETED_FINALIZATION_MAX_WAIT_MS) {
824
1196
  if (pending.loggedBlockReason !== blockReason) {
825
1197
  LOG.info('CLI', `[${this.type}] waiting to emit completed until transcript finalizes (${blockReason})`);
826
1198
  pending.loggedBlockReason = blockReason;
@@ -828,10 +1200,29 @@ export class CliProviderInstance implements ProviderInstance {
828
1200
  this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
829
1201
  return;
830
1202
  }
831
- LOG.warn('CLI', `[${this.type}] suppressed completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
1203
+ const completionDiagnostic = this.buildCompletedFinalizationDiagnostic({
1204
+ blockReason,
1205
+ latestStatus,
1206
+ latestVisibleStatus,
1207
+ waitedMs,
1208
+ pending,
1209
+ emittedAfterFinalizationTimeout: true,
1210
+ });
1211
+ LOG.warn('CLI', `[${this.type}] emitting completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
1212
+ this.pushEvent({
1213
+ event: 'agent:generating_completed',
1214
+ chatTitle: pending.chatTitle,
1215
+ duration: pending.duration,
1216
+ timestamp: pending.timestamp,
1217
+ finalSummary: blockReason.startsWith('parsed_status:')
1218
+ ? ''
1219
+ : this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
1220
+ completionDiagnostic,
1221
+ });
832
1222
  this.completedDebouncePending = null;
833
1223
  this.completedDebounceTimer = null;
834
1224
  this.generatingStartedAt = 0;
1225
+ this.lastApprovalEventFingerprint = '';
835
1226
  return;
836
1227
  }
837
1228
 
@@ -841,11 +1232,12 @@ export class CliProviderInstance implements ProviderInstance {
841
1232
  chatTitle: pending.chatTitle,
842
1233
  duration: pending.duration,
843
1234
  timestamp: pending.timestamp,
844
- finalSummary: extractFinalSummaryFromMessages(this.adapter?.getScriptParsedStatus()?.messages),
1235
+ finalSummary: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
845
1236
  });
846
1237
  this.completedDebouncePending = null;
847
1238
  this.completedDebounceTimer = null;
848
1239
  this.generatingStartedAt = 0;
1240
+ this.lastApprovalEventFingerprint = '';
849
1241
  }
850
1242
 
851
1243
  private maybeAutoApproveStatus(adapterStatus: any, now = Date.now()): boolean {
@@ -853,15 +1245,46 @@ export class CliProviderInstance implements ProviderInstance {
853
1245
  // Guard re-entry: onStatusChange/getState can observe the same modal multiple
854
1246
  // times while the PTY absorbs the approval key. Without this flag, repeated
855
1247
  // snapshots would write stray keys into the input once the modal dismisses.
856
- if (autoApproveActive && !this.autoApproveBusy) {
1248
+ // However, Claude Code can present a second approval immediately after the
1249
+ // first. Resolve a changed modal signature even while the previous write is
1250
+ // still inside the short busy window.
1251
+ if (!autoApproveActive) {
1252
+ this.lastAutoApprovalSignature = '';
1253
+ return autoApproveActive;
1254
+ }
1255
+ const modal = adapterStatus.activeModal;
1256
+ // (fix) Do not auto-approve when no concrete modal/buttons are present.
1257
+ // Claude TUI flaps between paints; without this guard adapterStatus
1258
+ // could report status=waiting_approval with activeModal=null (or with
1259
+ // an empty buttons array briefly) and we'd still call
1260
+ // resolveModal(-1) — which used to type "1" into the prompt
1261
+ // repeatedly. Skip until a real modal is captured.
1262
+ const buttons = Array.isArray(modal?.buttons)
1263
+ ? modal.buttons.map((b: any) => String(b || '').trim()).filter(Boolean)
1264
+ : [];
1265
+ if (!modal || buttons.length === 0) {
1266
+ return autoApproveActive;
1267
+ }
1268
+ const { index: buttonIndex, label: buttonLabel } = pickAutoApprovalButton(buttons);
1269
+ if (buttonIndex < 0) {
1270
+ // No concrete button matched — don't pick a random index, just
1271
+ // surface the modal so the user can decide.
1272
+ return autoApproveActive;
1273
+ }
1274
+ const signature = [
1275
+ typeof modal?.message === 'string' ? modal.message.trim() : '',
1276
+ buttons.join('|'),
1277
+ buttonIndex,
1278
+ ].join('::');
1279
+ if (!this.autoApproveBusy || signature !== this.lastAutoApprovalSignature) {
857
1280
  this.autoApproveBusy = true;
1281
+ this.lastAutoApprovalSignature = signature;
858
1282
  if (this.autoApproveBusyTimer) clearTimeout(this.autoApproveBusyTimer);
859
1283
  this.autoApproveBusyTimer = setTimeout(() => {
860
1284
  this.autoApproveBusy = false;
861
1285
  this.autoApproveBusyTimer = null;
1286
+ this.lastAutoApprovalSignature = '';
862
1287
  }, 2000);
863
- const modal = adapterStatus.activeModal;
864
- const { index: buttonIndex, label: buttonLabel } = pickApprovalButton(modal?.buttons, this.provider);
865
1288
  this.recordAutoApproval(modal?.message, buttonLabel, now);
866
1289
  setTimeout(() => {
867
1290
  this.adapter.resolveModal(buttonIndex);
@@ -876,6 +1299,13 @@ export class CliProviderInstance implements ProviderInstance {
876
1299
  // during long-running CLI sessions. Keep this path on adapter-owned light
877
1300
  // state only; rich provider parsing is reserved for getState/read_chat.
878
1301
  const adapterStatus = this.adapter.getStatus({ allowParse: false });
1302
+ const adapterProviderSessionId = normalizeProviderSessionId(
1303
+ this.provider,
1304
+ typeof adapterStatus?.providerSessionId === 'string' ? adapterStatus.providerSessionId : '',
1305
+ );
1306
+ if (adapterProviderSessionId) {
1307
+ this.promoteProviderSessionId(adapterProviderSessionId);
1308
+ }
879
1309
  const parsedStatus = null;
880
1310
  const rawStatus = adapterStatus.status;
881
1311
  const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, now);
@@ -926,10 +1356,14 @@ export class CliProviderInstance implements ProviderInstance {
926
1356
  if (!this.generatingStartedAt) this.generatingStartedAt = now;
927
1357
  const modal = adapterStatus.activeModal;
928
1358
  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;
1359
+ const approvalFingerprint = JSON.stringify({
1360
+ message: typeof modal?.message === 'string' ? modal.message.trim() : '',
1361
+ buttons: Array.isArray(modal?.buttons) ? modal.buttons.map((button: unknown) => String(button).trim()) : [],
1362
+ });
1363
+ // PTY redraws can briefly leave waiting_approval and then re-enter it.
1364
+ // Keep one coordinator event per logical modal until the turn completes.
1365
+ if (this.lastStatus !== 'waiting_approval' && approvalFingerprint !== this.lastApprovalEventFingerprint) {
1366
+ this.lastApprovalEventFingerprint = approvalFingerprint;
933
1367
  this.appendRuntimeSystemMessage(
934
1368
  this.formatApprovalRequestMessage(modal?.message, modal?.buttons),
935
1369
  `approval_request:${now}`,
@@ -943,20 +1377,72 @@ export class CliProviderInstance implements ProviderInstance {
943
1377
  }
944
1378
  } else if (newStatus === 'idle' && (this.lastStatus === 'generating' || this.lastStatus === 'waiting_approval')) {
945
1379
  const duration = this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : 0;
946
- // If debounce still pending (generating lasted < 1s), cancel both events
1380
+ // If debounce still pending (generating lasted < 1s), cancel both UI events.
1381
+ // Still emit agent:generating_completed so mesh orchestration can record
1382
+ // task_completed for direct dispatches that complete faster than the debounce.
947
1383
  if (this.generatingDebouncePending) {
948
- LOG.info('CLI', `[${this.type}] suppressed short generating (${now - this.generatingStartedAt}ms)`);
1384
+ const shortDurationMs = this.generatingStartedAt ? now - this.generatingStartedAt : 0;
1385
+ LOG.info('CLI', `[${this.type}] suppressed short generating (${shortDurationMs}ms)`);
949
1386
  if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
950
1387
  this.generatingDebouncePending = null;
951
1388
  this.generatingStartedAt = 0;
1389
+ // Emit completion for mesh task association even though the UI generating
1390
+ // started/completed pair is suppressed (too short for visible UI update).
1391
+ let shortFinalSummary: string | undefined;
1392
+ let shortEvidenceSource: CompletionFinalAssistantEvidence['source'] = 'unavailable';
1393
+ try {
1394
+ const parsedMessages = this.adapter?.getScriptParsedStatus()?.messages;
1395
+ const evidence = this.completionFinalAssistantEvidence(parsedMessages);
1396
+ shortEvidenceSource = evidence.source;
1397
+ shortFinalSummary = extractFinalSummaryFromMessages(evidence.messages as any);
1398
+ } catch { /* best-effort */ }
1399
+ if (((this.provider as any).requiresFinalAssistantBeforeIdle === true || shortEvidenceSource === 'external-native') && !shortFinalSummary) {
1400
+ LOG.info('CLI', `[${this.type}] suppressed short completion without final assistant evidence`);
1401
+ } else {
1402
+ this.pushEvent({
1403
+ event: 'agent:generating_completed',
1404
+ chatTitle,
1405
+ duration: 0,
1406
+ timestamp: now,
1407
+ finalSummary: shortFinalSummary,
1408
+ completionDiagnostic: {
1409
+ reason: 'short_generating_suppressed',
1410
+ shortDurationMs,
1411
+ finalAssistantEvidenceSource: shortEvidenceSource,
1412
+ },
1413
+ });
1414
+ }
952
1415
  } else {
953
1416
  // Debounce completed, then require the rich transcript path that read_chat
954
1417
  // uses to show an idle turn whose last user-facing message is assistant.
955
- this.completedDebouncePending = { chatTitle, duration, timestamp: now, firstObservedAt: now };
1418
+ this.completedDebouncePending = {
1419
+ chatTitle,
1420
+ duration,
1421
+ timestamp: now,
1422
+ firstObservedAt: now,
1423
+ previousStatus: this.lastStatus,
1424
+ };
956
1425
  this.scheduleCompletedDebounceFlush(3000);
957
1426
  }
958
1427
  } else if (newStatus === 'idle' && this.lastStatus === 'starting') {
959
1428
  this.pushEvent({ event: 'agent:ready', chatTitle, timestamp: now });
1429
+ } else if (newStatus === 'error') {
1430
+ if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
1431
+ this.generatingDebouncePending = null;
1432
+ if (this.completedDebounceTimer) { clearTimeout(this.completedDebounceTimer); this.completedDebounceTimer = null; }
1433
+ this.completedDebouncePending = null;
1434
+ this.errorMessage = adapterStatus.errorMessage || this.errorMessage;
1435
+ this.errorReason = (adapterStatus.errorReason as ProviderErrorReason) || this.errorReason;
1436
+ this.pushEvent({
1437
+ event: 'agent:stopped',
1438
+ chatTitle,
1439
+ timestamp: now,
1440
+ finalSummary: adapterStatus.errorMessage || adapterStatus.errorReason || 'Provider reported an error',
1441
+ completionDiagnostic: {
1442
+ reason: adapterStatus.errorReason || 'provider_error',
1443
+ errorMessage: adapterStatus.errorMessage || undefined,
1444
+ },
1445
+ });
960
1446
  } else if (newStatus === 'stopped') {
961
1447
  // Cancel any pending debounce
962
1448
  if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
@@ -977,7 +1463,31 @@ export class CliProviderInstance implements ProviderInstance {
977
1463
  // Monitor check (cooldown based notification, IDE/CLI common)
978
1464
  const agentKey = `${this.type}:cli`;
979
1465
  const monitorEvents = this.monitor.check(agentKey, newStatus, now, progressFingerprint);
1466
+ const monitorParsedStatus: any = parsedStatus;
980
1467
  for (const me of monitorEvents) {
1468
+ if (
1469
+ me.type === 'monitor:long_generating'
1470
+ && this.completionHasFinalAssistantMessage(monitorParsedStatus?.messages)
1471
+ && !this.hasAdapterPendingResponse()
1472
+ && !hasNonEmptyCliModalButtons(monitorParsedStatus?.activeModal ?? monitorParsedStatus?.modal)
1473
+ ) {
1474
+ this.pushEvent({
1475
+ event: 'agent:generating_completed',
1476
+ chatTitle,
1477
+ duration: this.generatingStartedAt ? Math.round((now - this.generatingStartedAt) / 1000) : undefined,
1478
+ timestamp: me.timestamp,
1479
+ finalSummary: extractFinalSummaryFromMessages(monitorParsedStatus?.messages),
1480
+ completionDiagnostic: {
1481
+ providerType: this.type,
1482
+ sessionId: this.instanceId,
1483
+ providerSessionId: this.providerSessionId || null,
1484
+ reconciliationReason: 'long_generating_monitor_final_summary',
1485
+ finalAssistantPresent: true,
1486
+ },
1487
+ });
1488
+ this.generatingStartedAt = 0;
1489
+ continue;
1490
+ }
981
1491
  this.pushEvent({ event: me.type, agentKey: me.agentKey, message: me.message, elapsedSec: me.elapsedSec, timestamp: me.timestamp });
982
1492
  }
983
1493
  }
@@ -1258,12 +1768,28 @@ export class CliProviderInstance implements ProviderInstance {
1258
1768
  index,
1259
1769
  source: 'parsed',
1260
1770
  }));
1771
+ const getRole = (message: ChatMessage): string => typeof message.role === 'string'
1772
+ ? message.role.trim().toLowerCase()
1773
+ : '';
1261
1774
  const runtimeEntries: MergeEntry[] = this.runtimeMessages.map((entry, index) => ({
1262
1775
  message: entry.message,
1263
1776
  index: parsedMessages.length + index,
1264
- source: 'runtime',
1777
+ source: 'runtime' as const,
1265
1778
  runtimeKey: entry.key,
1266
- }));
1779
+ })).filter((entry) => {
1780
+ const meta = entry.message.meta && typeof entry.message.meta === 'object' && !Array.isArray(entry.message.meta)
1781
+ ? entry.message.meta as Record<string, unknown>
1782
+ : {};
1783
+ if (meta.runtimeInputAck !== true) return true;
1784
+ const runtimeText = flattenContent(entry.message.content).replace(/\s+/g, ' ').trim();
1785
+ if (!runtimeText) return false;
1786
+ return !parsedEntries.some((parsedEntry) => {
1787
+ const parsedRole = getRole(parsedEntry.message);
1788
+ if (parsedRole !== 'user' && parsedRole !== 'human') return false;
1789
+ const parsedText = flattenContent(parsedEntry.message.content).replace(/\s+/g, ' ').trim();
1790
+ return parsedText === runtimeText;
1791
+ });
1792
+ });
1267
1793
  const getTime = (message: ChatMessage): number => {
1268
1794
  const value = typeof message.receivedAt === 'number'
1269
1795
  ? message.receivedAt
@@ -1273,9 +1799,6 @@ export class CliProviderInstance implements ProviderInstance {
1273
1799
  return Number.isFinite(value) && value > 0 ? value : 0;
1274
1800
  };
1275
1801
 
1276
- const getRole = (message: ChatMessage): string => typeof message.role === 'string'
1277
- ? message.role.trim().toLowerCase()
1278
- : '';
1279
1802
  const isRuntimeOverlay = (entry: MergeEntry): boolean => {
1280
1803
  if (entry.source !== 'runtime') return false;
1281
1804
  const key = typeof entry.runtimeKey === 'string' ? entry.runtimeKey.trim().toLowerCase() : '';
@@ -1341,7 +1864,9 @@ export class CliProviderInstance implements ProviderInstance {
1341
1864
  this.providerSessionId = nextSessionId;
1342
1865
  this.historyWriter.promoteHistorySession(this.type, previousHistorySessionId, nextSessionId);
1343
1866
  this.historyWriter.writeSessionStart(this.type, nextSessionId, this.workingDir, this.instanceId);
1344
- this.restorePersistedHistoryFromCurrentSession();
1867
+ if (this.shouldHydrateExistingProviderHistory()) {
1868
+ this.restorePersistedHistoryFromCurrentSession();
1869
+ }
1345
1870
  this.adapter.updateRuntimeMeta({ providerSessionId: nextSessionId });
1346
1871
  this.onProviderSessionResolved?.({
1347
1872
  instanceId: this.instanceId,
@@ -1354,9 +1879,27 @@ export class CliProviderInstance implements ProviderInstance {
1354
1879
  LOG.info('CLI', `[${this.type}] discovered provider session id: ${nextSessionId}`);
1355
1880
  }
1356
1881
 
1882
+ private shouldHydrateExistingProviderHistory(): boolean {
1883
+ return this.launchMode === 'resume' || this.launchMode === 'manual';
1884
+ }
1885
+
1886
+ private shouldSuppressFreshLaunchStartupReplay(parsedMessages: unknown[], parsedStatus: any, adapterStatus: any, parsedProviderSessionId = ''): boolean {
1887
+ if (this.launchMode !== 'new') return false;
1888
+ if (this.providerSessionId) return false;
1889
+ if (!Array.isArray(parsedMessages) || parsedMessages.length === 0) return false;
1890
+ if (!isIdleStatus(adapterStatus?.status) || !isIdleStatus(parsedStatus?.status)) return false;
1891
+ if (parsedProviderSessionId) return true;
1892
+
1893
+ const newestMessageAt = parsedMessages.reduce<number>((newest, message) => Math.max(newest, getMessageTime(message)), 0);
1894
+
1895
+ // Untimestamped idle parser output during a fresh launch is usually the
1896
+ // provider's last workspace transcript before a new turn exists.
1897
+ return newestMessageAt === 0;
1898
+ }
1899
+
1357
1900
  private syncCanonicalSavedHistoryIfNeeded(): boolean {
1358
1901
  if (!this.providerSessionId) return false;
1359
- const canonicalHistory = this.provider.canonicalHistory;
1902
+ const canonicalHistory = this.provider.nativeHistory;
1360
1903
  if (!canonicalHistory) return false;
1361
1904
 
1362
1905
  if (isNativeSourceCanonicalHistory(canonicalHistory)) {
@@ -1418,9 +1961,9 @@ export class CliProviderInstance implements ProviderInstance {
1418
1961
  private restorePersistedHistoryFromCurrentSession(): void {
1419
1962
  if (!this.providerSessionId) return;
1420
1963
  this.syncCanonicalSavedHistoryIfNeeded();
1421
- const restoredHistory = isNativeSourceCanonicalHistory(this.provider.canonicalHistory)
1964
+ const restoredHistory = isNativeSourceCanonicalHistory(this.provider.nativeHistory)
1422
1965
  ? readProviderChatHistory(this.type, {
1423
- canonicalHistory: this.provider.canonicalHistory,
1966
+ canonicalHistory: this.provider.nativeHistory,
1424
1967
  historySessionId: this.providerSessionId,
1425
1968
  workspace: this.workingDir,
1426
1969
  offset: 0,