@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
@@ -16,6 +16,7 @@
16
16
 
17
17
  import * as os from 'os';
18
18
  import type { CliAdapter } from '../cli-adapter-types.js';
19
+ import type { InteractivePromptResponse } from '../providers/types/interactive-prompt.js';
19
20
  import { LOG } from '../logging/logger.js';
20
21
  import { getDebugRuntimeConfig } from '../logging/debug-config.js';
21
22
  import { TerminalScreen } from './terminal-screen.js';
@@ -44,6 +45,13 @@ import {
44
45
  type CliTraceEntry,
45
46
  type ParsedSession,
46
47
  } from './provider-cli-shared.js';
48
+ import { CliScriptRunner } from './cli-script-runner.js';
49
+ import {
50
+ CliStateEngine,
51
+ type CliBufferSnapshot,
52
+ type CliTransportAccess,
53
+ type CliStateEngineCallbacks,
54
+ } from './cli-state-engine.js';
47
55
  import {
48
56
  buildCliParseInput,
49
57
  buildCliTraceParseSnapshot,
@@ -78,24 +86,6 @@ export {
78
86
  } from './provider-cli-shared.js';
79
87
 
80
88
 
81
- interface IdleFinishCandidate {
82
- armedAt: number;
83
- lastOutputAt: number;
84
- lastScreenChangeAt: number;
85
- responseEpoch: number;
86
- assistantLength: number;
87
- }
88
-
89
- interface SettledEvalContext {
90
- now: number;
91
- modal: any;
92
- status: string;
93
- parsedMessages: CliChatMessage[];
94
- lastParsedAssistant: CliChatMessage | undefined;
95
- parsedStatus: string | null;
96
- prevStatus: string;
97
- }
98
-
99
89
  interface SendMessageState {
100
90
  text: string;
101
91
  normalizedPromptSnippet: string;
@@ -110,6 +100,14 @@ interface SendMessageCompletion {
110
100
  rejectOnce: (error: unknown) => void;
111
101
  }
112
102
 
103
+ interface PendingOutboundMessage {
104
+ id: string;
105
+ role: 'user';
106
+ content: string;
107
+ queuedAt: number;
108
+ source: 'sendMessage';
109
+ }
110
+
113
111
  export function appendBoundedText(current: string, chunk: string, maxChars: number): string {
114
112
  if (!chunk) return current.length <= maxChars ? current : current.slice(-maxChars);
115
113
  if (maxChars <= 0) return '';
@@ -122,23 +120,23 @@ export function appendBoundedText(current: string, chunk: string, maxChars: numb
122
120
  // ─── Adapter ────────────────────────────────────────
123
121
 
124
122
  export class ProviderCliAdapter implements CliAdapter {
125
- readonly cliType: string;
123
+ cliType: string;
126
124
  readonly cliName: string;
127
125
  public workingDir: string;
128
126
 
129
127
  private provider: CliProviderModule;
130
128
  private ptyProcess: PtyRuntimeTransport | null = null;
131
129
  private transportFactory: PtyTransportFactory;
132
- private currentStatus: CliSessionStatus['status'] = 'starting';
133
130
  private onStatusChange: (() => void) | null = null;
134
131
 
132
+ // ─── State machine engine ─────────────────────────
133
+ readonly engine: CliStateEngine;
134
+
135
135
  private responseBuffer = '';
136
136
  private recentOutputBuffer = '';
137
- private isWaitingForResponse = false;
138
- private activeModal: { message: string; buttons: string[] } | null = null;
139
- private parseErrorMessage: string | null = null;
137
+ private get parseErrorMessage(): string | null { return this.runner.parseErrorMessage; }
138
+ private providerSessionId: string | null = null;
140
139
  private responseTimeout: NodeJS.Timeout | null = null;
141
- private idleTimeout: NodeJS.Timeout | null = null;
142
140
  private ready = false;
143
141
  private startupBuffer = '';
144
142
  private startupParseGate = false;
@@ -164,39 +162,48 @@ export class ProviderCliAdapter implements CliAdapter {
164
162
  private serverConn: any = null;
165
163
  private logBuffer: { message: string; level: string }[] = [];
166
164
 
167
- // Approval cooldown
168
- private lastApprovalResolvedAt: number = 0;
169
-
170
- // Approval state machine
171
- private approvalTransitionBuffer: string = '';
172
- private approvalExitTimeout: NodeJS.Timeout | null = null;
173
- private pendingScriptStatus: 'generating' | 'waiting_approval' | null = null;
174
- private pendingScriptStatusSince = 0;
175
- private pendingScriptStatusTimer: NodeJS.Timeout | null = null;
176
-
177
- // Output settle debounce — fires after PTY output goes quiet
178
- private settleTimer: NodeJS.Timeout | null = null;
179
- private settledBuffer: string = '';
180
- private submitPendingUntil = 0;
181
- private responseSettleIgnoreUntil = 0;
182
- private responseEpoch = 0;
165
+ private pendingOutboundQueue: PendingOutboundMessage[] = [];
166
+ private pendingOutboundFlushTimer: NodeJS.Timeout | null = null;
167
+ private pendingOutboundFlushInFlight = false;
168
+ // Submit retry timer — PTY-level, not state machine
183
169
  private submitRetryTimer: NodeJS.Timeout | null = null;
184
- private submitRetryUsed = false;
185
- private submitRetryPromptSnippet = '';
186
- private idleFinishCandidate: IdleFinishCandidate | null = null;
187
- private finishRetryTimer: NodeJS.Timeout | null = null;
188
- private finishRetryCount = 0;
189
170
 
190
171
  // Resize redraw suppression
191
172
  private resizeSuppressUntil: number = 0;
192
173
 
193
- // Debug: status transition history
194
- private statusHistory: { status: string; at: number; trigger?: string }[] = [];
174
+ // (A2.2) Native transcript anchor moved to CHAT_SOURCE_REGISTRY.
175
+ // ChatSourceMachine holds the lock by state, not by a mutable field on
176
+ // the adapter. Removed entirely; no callers remain after the readChat
177
+ // ladder was replaced.
178
+
179
+ // ─── Script runner (parsing isolated here, adapter stays as transport) ───
180
+ private readonly runner: CliScriptRunner;
181
+ /** @deprecated use runner.cliScripts for direct script access */
182
+ get cliScripts(): CliScripts { return this.runner.cliScripts; }
195
183
 
196
- // ─── CLI Scripts (script-based parsing) ───
197
- private cliScripts: CliScripts;
198
- /** Per-session opaque state object created by cliScripts.createState(), reset on stop. */
199
- private scriptState: unknown = null;
184
+ /**
185
+ * Recent script invocations (oldest → newest). Exposed via the
186
+ * `get_chat_debug_bundle` daemon command so anyone debugging a
187
+ * stuck-status regression can read what each detectStatus /
188
+ * parseSession call actually saw and returned, instead of having
189
+ * to instrument the daemon.
190
+ */
191
+ getScriptInvocationTrace() {
192
+ return this.runner.getInvocationTrace();
193
+ }
194
+
195
+ /**
196
+ * Returns the full raw PTY byte stream captured since adapter start.
197
+ * Used by the `record_provider_pty` IPC command to produce fixtures.
198
+ * Bounded by MAX_ACCUMULATED_BUFFER; older bytes may have been dropped.
199
+ */
200
+ getAccumulatedRawBuffer(): { text: string; droppedChars: number } {
201
+ return {
202
+ text: this.accumulatedRawBuffer,
203
+ droppedChars: this.accumulatedRawBufferDroppedChars,
204
+ };
205
+ }
206
+ set cliScripts(scripts: CliScripts) { this.setCliScripts(scripts); }
200
207
  private runtimeSettings: Record<string, any> = {};
201
208
  /** Full accumulated rendered PTY transcript for parser/readback use */
202
209
  private accumulatedBuffer: string = '';
@@ -216,10 +223,6 @@ export class ProviderCliAdapter implements CliAdapter {
216
223
  * Hermes turn (tool calls + reasoning + final bubble) without the
217
224
  * rolling window pushing the turn's ╭─ opening line out of view. */
218
225
  private static readonly MAX_ACCUMULATED_BUFFER = 262144;
219
- private currentTurnScope: TurnParseScope | null = null;
220
- private traceEntries: CliTraceEntry[] = [];
221
- private traceSeq = 0;
222
- private traceSessionId = '';
223
226
  private parsedStatusCache: {
224
227
  responseBuffer: string;
225
228
  currentTurnScope: TurnParseScope | null;
@@ -233,11 +236,8 @@ export class ProviderCliAdapter implements CliAdapter {
233
236
  result: any;
234
237
  } | null = null;
235
238
  private static readonly SCREEN_SNAPSHOT_MIN_INTERVAL_MS = 250;
236
- private static readonly MAX_TRACE_ENTRIES = 250;
237
239
 
238
240
  private readonly providerResolutionMeta: ProviderResolutionMeta;
239
- private static readonly FINISH_RETRY_DELAY_MS = 300;
240
- private static readonly MAX_FINISH_RETRIES = 2;
241
241
 
242
242
  private getBufferState(): NonNullable<CliSessionStatus['bufferState']> | undefined {
243
243
  const build = (droppedChars: number, maxChars: number) => droppedChars > 0
@@ -313,13 +313,13 @@ export class ProviderCliAdapter implements CliAdapter {
313
313
  if (
314
314
  cached
315
315
  && cached.responseBuffer === this.responseBuffer
316
- && cached.currentTurnScope === this.currentTurnScope
316
+ && cached.currentTurnScope === this.engine.currentTurnScope
317
317
  && cached.recentOutputBuffer === this.recentOutputBuffer
318
318
  && cached.accumulatedBuffer === this.accumulatedBuffer
319
319
  && cached.accumulatedRawBufferKey === accumulatedRawBufferKey
320
320
  && cached.screenText === this.lastScreenText
321
- && cached.currentStatus === this.currentStatus
322
- && cached.activeModal === this.activeModal
321
+ && cached.currentStatus === this.engine.currentStatus
322
+ && cached.activeModal === this.engine.activeModal
323
323
  && cached.cliName === this.cliName
324
324
  ) {
325
325
  return cached.result;
@@ -343,86 +343,6 @@ export class ProviderCliAdapter implements CliAdapter {
343
343
  return this.timeouts.statusActivityHold;
344
344
  }
345
345
 
346
- private setStatus(status: CliSessionStatus['status'], trigger?: string): void {
347
- const prev = this.currentStatus;
348
- if (prev === status) return;
349
- this.currentStatus = status;
350
- this.statusHistory.push({ status, at: Date.now(), trigger });
351
- if (this.statusHistory.length > 50) this.statusHistory.shift();
352
- this.recordTrace('status', {
353
- previousStatus: prev,
354
- trigger: trigger || null,
355
- });
356
- LOG.info('CLI', `[${this.cliType}] status: ${prev} → ${status}${trigger ? ` (${trigger})` : ''}`);
357
- }
358
-
359
- private clearIdleFinishCandidate(reason: string): void {
360
- if (!this.idleFinishCandidate) return;
361
- this.recordTrace('idle_candidate_reset', {
362
- reason,
363
- candidate: this.idleFinishCandidate,
364
- });
365
- this.idleFinishCandidate = null;
366
- }
367
-
368
- private armIdleFinishCandidate(assistantLength: number): void {
369
- const now = Date.now();
370
- const idleFinishConfirmMs = this.getIdleFinishConfirmMs();
371
- this.idleFinishCandidate = {
372
- armedAt: now,
373
- lastOutputAt: this.lastOutputAt,
374
- lastScreenChangeAt: this.lastScreenChangeAt,
375
- responseEpoch: this.responseEpoch,
376
- assistantLength,
377
- };
378
- this.recordTrace('idle_candidate_armed', {
379
- confirmMs: idleFinishConfirmMs,
380
- candidate: this.idleFinishCandidate,
381
- ...buildCliTraceParseSnapshot({
382
- accumulatedBuffer: this.accumulatedBuffer,
383
- accumulatedRawBuffer: this.accumulatedRawBuffer,
384
- responseBuffer: this.responseBuffer,
385
- partialResponse: this.responseBuffer,
386
- scope: this.currentTurnScope,
387
- }),
388
- });
389
- if (this.settleTimer) clearTimeout(this.settleTimer);
390
- this.settleTimer = setTimeout(() => {
391
- this.settleTimer = null;
392
- this.settledBuffer = this.recentOutputBuffer;
393
- this.evaluateSettled();
394
- }, idleFinishConfirmMs);
395
- }
396
-
397
-
398
- private recordTrace(type: string, payload: Record<string, any> = {}): void {
399
- const entry: CliTraceEntry = {
400
- id: ++this.traceSeq,
401
- at: Date.now(),
402
- type,
403
- status: this.currentStatus,
404
- isWaitingForResponse: this.isWaitingForResponse,
405
- activeModal: this.activeModal
406
- ? { message: this.activeModal.message, buttons: [...this.activeModal.buttons] }
407
- : null,
408
- payload,
409
- };
410
- this.traceEntries.push(entry);
411
- if (this.traceEntries.length > ProviderCliAdapter.MAX_TRACE_ENTRIES) {
412
- this.traceEntries.splice(0, this.traceEntries.length - ProviderCliAdapter.MAX_TRACE_ENTRIES);
413
- }
414
- }
415
-
416
- private resetTraceSession(): void {
417
- this.traceEntries = [];
418
- this.traceSeq = 0;
419
- this.traceSessionId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
420
- this.recordTrace('session_start', {
421
- providerType: this.cliType,
422
- workingDir: this.workingDir,
423
- });
424
- }
425
-
426
346
  // Resolved timeouts
427
347
  private readonly timeouts: Required<NonNullable<CliProviderModule['timeouts']>>;
428
348
 
@@ -432,7 +352,6 @@ export class ProviderCliAdapter implements CliAdapter {
432
352
  private readonly sendKey: string;
433
353
  private readonly submitStrategy: 'wait_for_echo' | 'immediate';
434
354
  private readonly requirePromptEchoBeforeSubmit: boolean;
435
- private static readonly SCRIPT_STATUS_DEBOUNCE_MS = 3000;
436
355
 
437
356
  constructor(
438
357
  provider: CliProviderModule,
@@ -441,6 +360,7 @@ export class ProviderCliAdapter implements CliAdapter {
441
360
  private extraEnv: Record<string, string> = {},
442
361
  transportFactory: PtyTransportFactory = new NodePtyTransportFactory(),
443
362
  ) {
363
+ this.runner = new CliScriptRunner(provider.type);
444
364
  this.provider = provider;
445
365
  this.transportFactory = transportFactory;
446
366
  this.cliType = provider.type;
@@ -458,10 +378,33 @@ export class ProviderCliAdapter implements CliAdapter {
458
378
  this.requirePromptEchoBeforeSubmit = resolvedConfig.requirePromptEchoBeforeSubmit;
459
379
  this.providerResolutionMeta = resolvedConfig.providerResolutionMeta;
460
380
 
461
- // Scripts are requiredloaded by ProviderLoader via compatibility array
462
- this.cliScripts = provider.scripts || {};
463
- this.scriptState = typeof this.cliScripts.createState === 'function' ? (this.cliScripts.createState() ?? null) : null;
464
- const scriptNames = listCliScriptNames(this.cliScripts);
381
+ // State machine engineowns all status transitions
382
+ this.engine = new CliStateEngine(
383
+ provider,
384
+ this.runner,
385
+ this as unknown as CliTransportAccess,
386
+ {
387
+ onStatusChange: () => { this.onStatusChange?.(); },
388
+ onApplyParsedSession: (session) => { this.applyParsedSessionMetadata(session); },
389
+ onTurnCompleted: () => { this.responseBuffer = ''; },
390
+ } satisfies CliStateEngineCallbacks,
391
+ resolvedConfig.timeouts,
392
+ );
393
+
394
+ // Scripts delegated to CliScriptRunner — adapter stays as transport.
395
+ // Pass the manifest tui block so the runner can build the
396
+ // declarativeDetectStatus / declarativeParseApproval SDK functions
397
+ // that v1 extended-tier overrides depend on (without these, scripts
398
+ // like codex-cli's detect_status v1 fail-closed with return 'idle'
399
+ // and the session sticks in `generating` forever).
400
+ this.runner.setScripts(provider.scripts || {}, provider.tui);
401
+ // Per-invocation wall-clock budget for provider scripts. Manifests may
402
+ // raise this for genuinely slow parsers, but the default (50ms) is the
403
+ // settle-loop safety net — any script that exceeds it gets flagged in
404
+ // the invocation trace and surfaced via the debug bundle so we can
405
+ // identify the offender instead of guessing why the settle loop hangs.
406
+ this.runner.setScriptCallBudget(provider.scriptCallBudgetMs ?? 50);
407
+ const scriptNames = this.runner.getScriptNames();
465
408
  if (scriptNames.length > 0) {
466
409
  LOG.info('CLI', `[${this.cliType}] CLI scripts: [${scriptNames.join(', ')}]`);
467
410
  LOG.info(
@@ -487,14 +430,9 @@ export class ProviderCliAdapter implements CliAdapter {
487
430
 
488
431
  /** Inject CLI scripts after construction (e.g. when resolved by ProviderLoader) */
489
432
  setCliScripts(scripts: CliScripts): void {
490
- this.cliScripts = scripts;
433
+ this.runner.setScripts(scripts);
491
434
  this.parsedStatusCache = null;
492
- this.parseErrorMessage = null;
493
- // Initialize per-session state: createState() is called once here and on script reload.
494
- // The returned object lives until the PTY exits (scriptState = null on exit).
495
- this.scriptState = typeof scripts.createState === 'function' ? (scripts.createState() ?? null) : null;
496
- const scriptNames = listCliScriptNames(scripts);
497
- LOG.info('CLI', `[${this.cliType}] CLI scripts injected: [${scriptNames.join(', ')}]`);
435
+ LOG.info('CLI', `[${this.cliType}] CLI scripts injected: [${this.runner.getScriptNames().join(', ')}]`);
498
436
  }
499
437
 
500
438
  /** Refresh provider scripts/config used by this adapter without restarting the PTY runtime. */
@@ -548,15 +486,6 @@ export class ProviderCliAdapter implements CliAdapter {
548
486
  });
549
487
 
550
488
  LOG.info('CLI', `[${this.cliType}] Spawning in ${this.workingDir}`);
551
- this.resetTraceSession();
552
- this.recordTrace('spawn', {
553
- shellCommand: spawnPlan.shellCmd,
554
- shellArgs: spawnPlan.shellArgs,
555
- cwd: spawnPlan.ptyOptions.cwd,
556
- cols: spawnPlan.ptyOptions.cols,
557
- rows: spawnPlan.ptyOptions.rows,
558
- providerResolution: this.providerResolutionMeta,
559
- });
560
489
 
561
490
  try {
562
491
  this.ptyProcess = this.transportFactory.spawn(
@@ -620,13 +549,12 @@ export class ProviderCliAdapter implements CliAdapter {
620
549
  this.ptyProcess.onExit(({ exitCode }: { exitCode: number }) => {
621
550
  LOG.info('CLI', `[${this.cliType}] Exit code ${exitCode}`);
622
551
  this.flushPendingOutputParse();
623
- this.recordTrace('exit', { exitCode });
624
552
  this.ptyProcess = null;
625
- this.setStatus('stopped', 'pty_exit');
553
+ this.engine.onPtyExit();
626
554
  this.ready = false;
627
555
  this.startupParseGate = false;
628
556
  this.spawnAt = 0;
629
- this.scriptState = null;
557
+ this.runner.resetSessionState();
630
558
  this.onStatusChange?.();
631
559
  });
632
560
 
@@ -637,15 +565,9 @@ export class ProviderCliAdapter implements CliAdapter {
637
565
  if (this.startupSettleTimer) { clearTimeout(this.startupSettleTimer); this.startupSettleTimer = null; }
638
566
  this.resetTerminalScreen(24, 80);
639
567
  this.pendingTerminalQueryTail = '';
640
- this.currentTurnScope = null;
641
- this.finishRetryCount = 0;
642
- if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
643
568
  this.ready = false;
644
569
  await this.ptyProcess.ready;
645
- this.recordTrace('ready', {
646
- runtimeMeta: this.getRuntimeMetadata(),
647
- });
648
- this.setStatus('starting', 'pty_ready');
570
+ this.engine.onSpawnReady();
649
571
  this.scheduleStartupSettleCheck();
650
572
  this.onStatusChange?.();
651
573
  }
@@ -671,11 +593,11 @@ export class ProviderCliAdapter implements CliAdapter {
671
593
  if (this.startupParseGate && !this.startupFirstOutputAt && (cleanData.trim() || normalizedScreenSnapshot.trim())) {
672
594
  this.startupFirstOutputAt = now;
673
595
  }
674
- if (this.idleFinishCandidate && (rawData.length > 0 || cleanData.length > 0)) {
675
- this.clearIdleFinishCandidate('new_output');
596
+ if (rawData.length > 0 || cleanData.length > 0) {
597
+ this.engine.clearIdleFinishCandidate('new_output');
676
598
  }
677
599
  if (getDebugRuntimeConfig().collectDebugTrace) {
678
- this.recordTrace('output', {
600
+ this.engine.recordExternalTrace('output', {
679
601
  rawLength: rawData.length,
680
602
  cleanLength: cleanData.length,
681
603
  rawPreview: summarizeCliTraceText(rawData, 300),
@@ -687,7 +609,7 @@ export class ProviderCliAdapter implements CliAdapter {
687
609
  this.scheduleStartupSettleCheck();
688
610
  }
689
611
 
690
- if (this.isWaitingForResponse && cleanData) {
612
+ if (this.engine.isWaitingForResponse && cleanData) {
691
613
  const previousResponseLen = this.responseBuffer.length;
692
614
  this.responseBuffer = appendBoundedText(this.responseBuffer, cleanData, ProviderCliAdapter.MAX_RESPONSE_BUFFER);
693
615
  this.responseBufferDroppedChars += this.recordBoundedAppendDrop(previousResponseLen, cleanData.length, this.responseBuffer.length);
@@ -726,19 +648,19 @@ export class ProviderCliAdapter implements CliAdapter {
726
648
  // Keep turn-scope offsets aligned with the truncated buffer so scoped
727
649
  // parses don't lose the beginning of a long turn (e.g. the Hermes
728
650
  // ╭─ opening line) when the rolling window sheds bytes.
729
- if (this.currentTurnScope) {
651
+ if (this.engine.currentTurnScope) {
730
652
  if (droppedClean > 0) {
731
- this.currentTurnScope.bufferStart = Math.max(0, this.currentTurnScope.bufferStart - droppedClean);
653
+ this.engine.currentTurnScope.bufferStart = Math.max(0, this.engine.currentTurnScope.bufferStart - droppedClean);
732
654
  }
733
655
  if (droppedRaw > 0) {
734
- this.currentTurnScope.rawBufferStart = Math.max(0, this.currentTurnScope.rawBufferStart - droppedRaw);
656
+ this.engine.currentTurnScope.rawBufferStart = Math.max(0, this.engine.currentTurnScope.rawBufferStart - droppedRaw);
735
657
  }
736
658
  }
737
659
 
738
660
  this.resolveStartupState('output', screenText, normalizedScreenSnapshot, now);
739
661
 
740
662
  // ─── Script-based status detection
741
- this.scheduleSettle();
663
+ this.engine.scheduleSettle();
742
664
  }
743
665
 
744
666
  private resolveStartupState(
@@ -761,6 +683,11 @@ export class ProviderCliAdapter implements CliAdapter {
761
683
  if (stableMs < 2000) return;
762
684
 
763
685
  const startupModal = this.runParseApproval(this.recentOutputBuffer);
686
+ const startupStatus = this.runDetectStatus(screenText || this.recentOutputBuffer);
687
+ if (!startupModal && startupStatus !== 'idle') {
688
+ this.scheduleStartupSettleCheck();
689
+ return;
690
+ }
764
691
  this.startupParseGate = false;
765
692
  if (this.startupSettleTimer) {
766
693
  clearTimeout(this.startupSettleTimer);
@@ -768,14 +695,22 @@ export class ProviderCliAdapter implements CliAdapter {
768
695
  }
769
696
  this.ready = true;
770
697
  if (startupModal) {
771
- this.activeModal = startupModal;
772
- this.setStatus('waiting_approval', `startup_ready:${trigger}`);
698
+ this.engine.activeModal = startupModal;
699
+ this.engine.setStatus('waiting_approval', `startup_ready:${trigger}`);
773
700
  } else {
774
- if (this.currentStatus === 'waiting_approval' || this.activeModal) {
775
- this.lastApprovalResolvedAt = Date.now();
701
+ if (this.engine.currentStatus === 'waiting_approval' || this.engine.activeModal) {
702
+ this.engine.lastApprovalResolvedAt = Date.now();
776
703
  }
777
- this.activeModal = null;
778
- this.setStatus('idle', `startup_ready:${trigger}`);
704
+ this.engine.activeModal = null;
705
+ // Clear the in-flight turn flag at the same time we declare
706
+ // startup-idle. Otherwise the next settled evaluation sees
707
+ // isWaitingForResponse=true + recent CLI welcome-screen paints
708
+ // and flips us right back to generating via the hold path
709
+ // (the "startup → generating → idle → generating → idle"
710
+ // flicker the user observed on claude-cli launch).
711
+ this.engine.isWaitingForResponse = false;
712
+ this.engine.currentTurnScope = null;
713
+ this.engine.setStatus('idle', `startup_ready:${trigger}`);
779
714
  }
780
715
  LOG.info(
781
716
  'CLI',
@@ -801,88 +736,6 @@ export class ProviderCliAdapter implements CliAdapter {
801
736
  }, delayMs);
802
737
  }
803
738
 
804
- private scheduleSettle(): void {
805
- if (this.settleTimer) clearTimeout(this.settleTimer);
806
- const settleEpoch = this.responseEpoch;
807
- const delay = Math.max(
808
- this.timeouts.outputSettle,
809
- this.submitPendingUntil > Date.now()
810
- ? (this.submitPendingUntil - Date.now()) + this.timeouts.outputSettle
811
- : 0,
812
- );
813
- this.settleTimer = setTimeout(() => {
814
- this.settleTimer = null;
815
- if (settleEpoch !== this.responseEpoch) return;
816
- this.settledBuffer = this.recentOutputBuffer;
817
- this.evaluateSettled();
818
- }, delay);
819
- }
820
-
821
- private armApprovalExitTimeout(): void {
822
- if (this.approvalExitTimeout) clearTimeout(this.approvalExitTimeout);
823
- this.approvalExitTimeout = setTimeout(() => {
824
- if (!this.hasActionableApproval()) return;
825
- const tail = this.recentOutputBuffer;
826
- const screenText = this.terminalScreen.getText() || '';
827
- const modal = this.runParseApproval(tail);
828
- const stillWaiting = this.runDetectStatus(tail) === 'waiting_approval' || !!modal;
829
- if (stillWaiting) {
830
- if (!modal) {
831
- LOG.warn('CLI', `[${this.cliType}] approval timeout check found no actionable modal; keeping approval state fail-closed`);
832
- this.activeModal = null;
833
- this.onStatusChange?.();
834
- this.armApprovalExitTimeout();
835
- return;
836
- }
837
- this.activeModal = modal;
838
- this.onStatusChange?.();
839
- this.armApprovalExitTimeout();
840
- return;
841
- }
842
- LOG.warn('CLI', `[${this.cliType}] Approval timeout — auto-clearing`);
843
- this.activeModal = null;
844
- this.lastApprovalResolvedAt = Date.now();
845
- this.setStatus('idle', 'approval_timeout');
846
- this.onStatusChange?.();
847
- }, 60000);
848
- }
849
-
850
- private shouldRetryFinishResponse(commitResult: { hasAssistant: boolean; assistantContent: string }): boolean {
851
- if (!this.currentTurnScope) return false;
852
- if (this.hasActionableApproval()) return false;
853
- if (this.finishRetryCount >= ProviderCliAdapter.MAX_FINISH_RETRIES) return false;
854
- if (commitResult.hasAssistant && commitResult.assistantContent.trim()) return false;
855
-
856
- if (this.runDetectStatus(this.recentOutputBuffer) !== 'idle') return false;
857
-
858
- const now = Date.now();
859
- const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
860
- const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
861
- return quietForMs < 1200 || screenStableMs < 1200 || !commitResult.hasAssistant;
862
- }
863
-
864
- private hasRecentInteractiveActivity(now: number): boolean {
865
- const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
866
- const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : Number.MAX_SAFE_INTEGER;
867
- const holdMs = this.getStatusActivityHoldMs();
868
- return quietForMs < holdMs
869
- || screenStableMs < holdMs;
870
- }
871
-
872
- private shouldDeferIdleTimeoutFinish(): boolean {
873
- if (!this.isWaitingForResponse || this.hasActionableApproval()) {
874
- return false;
875
- }
876
- const latestStatus = this.runDetectStatus(this.recentOutputBuffer) || this.currentStatus;
877
- if (latestStatus === 'generating') {
878
- this.settledBuffer = this.recentOutputBuffer;
879
- this.evaluateSettled();
880
- return true;
881
- }
882
- return false;
883
- }
884
-
885
-
886
739
  private async waitForInteractivePrompt(maxWaitMs = 5000): Promise<void> {
887
740
  const startedAt = Date.now();
888
741
  let loggedWait = false;
@@ -892,7 +745,7 @@ export class ProviderCliAdapter implements CliAdapter {
892
745
  const screenText = this.terminalScreen.getText() || '';
893
746
  const stableMs = this.lastScreenChangeAt ? (Date.now() - this.lastScreenChangeAt) : 0;
894
747
  const recentlyOutput = this.lastNonEmptyOutputAt ? (Date.now() - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
895
- const status = this.runDetectStatus(this.recentOutputBuffer) || this.currentStatus;
748
+ const status = this.runDetectStatus(this.recentOutputBuffer) || this.engine.currentStatus;
896
749
  const interactiveReady = status === 'idle'
897
750
  && stableMs >= 700
898
751
  && recentlyOutput >= 350;
@@ -926,637 +779,84 @@ export class ProviderCliAdapter implements CliAdapter {
926
779
 
927
780
  private clearAllTimers(): void {
928
781
  if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
929
- if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
930
- if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
931
782
  if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
932
- if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
933
- if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
934
- if (this.pendingScriptStatusTimer) { clearTimeout(this.pendingScriptStatusTimer); this.pendingScriptStatusTimer = null; }
935
783
  if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
936
784
  if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
785
+ this.engine.clearAllTimers();
937
786
  }
938
787
 
939
- private clearStaleIdleResponseGuard(reason: string): boolean {
940
- const blockingModal = this.activeModal || this.runParseApproval(this.recentOutputBuffer);
941
- const isIdle = this.runDetectStatus(this.recentOutputBuffer) === 'idle';
942
- if (!this.isWaitingForResponse || this.currentStatus !== 'idle' || !isIdle || !!blockingModal) {
943
- return false;
944
- }
945
- this.clearAllTimers();
946
- this.clearIdleFinishCandidate(reason);
947
- this.responseBuffer = '';
948
- this.isWaitingForResponse = false;
949
- this.responseSettleIgnoreUntil = 0;
950
- this.submitRetryUsed = false;
951
- this.submitRetryPromptSnippet = '';
952
- this.finishRetryCount = 0;
953
- this.currentTurnScope = null;
954
- this.activeModal = null;
955
- this.recordTrace('stale_idle_response_cleared', { reason });
956
- return true;
957
- }
958
-
959
- private hasMeaningfulResponseBuffer(promptSnippet: string): boolean {
960
- const raw = String(this.responseBuffer || '').trim();
961
- if (!raw) return false;
962
- const normalizedPrompt = compactPromptText(promptSnippet);
963
- if (!normalizedPrompt) return true;
964
- const normalizedBuffer = compactPromptText(raw);
965
- if (!normalizedBuffer) return false;
966
- if (normalizedBuffer === normalizedPrompt) return false;
967
- if (normalizedBuffer.startsWith(normalizedPrompt)) {
968
- const remainder = normalizedBuffer
969
- .slice(normalizedPrompt.length)
970
- .replace(/[─═\-]+/g, '')
971
- .replace(/⏵⏵accepteditson\([^)]*\)/gi, '')
972
- .replace(/accepteditson\([^)]*\)/gi, '')
973
- .replace(/(?:◐|◑|◒|◓|◔|◕|◉|●|·)?(?:x?high|medium|low|max)·?\/effort/gi, '')
974
- .replace(/updateavailable!run:[a-z0-9:._\-/]+/gi, '')
975
- .replace(/esctointerrupt/gi, '')
976
- .replace(/❯/g, '')
977
- .replace(/^[\s\-–—:;,.!/?]+/, '')
978
- .trim();
979
- return remainder.length > 0;
980
- }
981
- return true;
982
- }
983
-
984
- private evaluateSettled(): void {
985
- const now = Date.now();
986
- if (this.submitPendingUntil > now || this.responseSettleIgnoreUntil > now) {
987
- const delayTime = Math.max(this.submitPendingUntil - now, this.responseSettleIgnoreUntil - now) + 50;
988
- if (this.settleTimer) clearTimeout(this.settleTimer);
989
- this.settleTimer = setTimeout(() => {
990
- this.settleTimer = null;
991
- this.settledBuffer = this.recentOutputBuffer;
992
- this.evaluateSettled();
993
- }, delayTime);
994
- return;
995
- }
996
-
997
- this.resolveStartupState('settled');
998
- if (this.startupParseGate) return;
999
-
1000
- if (!this.isWaitingForResponse && !this.currentTurnScope && !this.activeModal && !this.parseErrorMessage) {
1001
- const tail = this.settledBuffer || this.recentOutputBuffer;
1002
- const modal = this.runParseApproval(tail);
1003
- const lightweightStatus = this.cliScripts?.detectStatus
1004
- ? this.runDetectStatus(tail)
1005
- : null;
1006
- if (!modal && lightweightStatus === 'idle' && this.currentStatus === 'idle') {
1007
- return;
1008
- }
1009
- }
1010
-
1011
- const session = this.runParseSession();
1012
- if (!session) return;
1013
-
1014
- const { status, messages, parsedStatus } = session;
1015
- const modal = (session as any).activeModal ?? session.modal ?? null;
1016
- const parsedMessages = normalizeCliParsedMessages(messages, {
1017
- scope: null,
1018
- lastOutputAt: this.lastOutputAt,
1019
- });
1020
-
1021
- if (this.maybeCommitVisibleIdleTranscript(session, parsedMessages)) return;
1022
-
1023
- const lastParsedAssistant = [...parsedMessages].reverse().find((m) => m.role === 'assistant');
1024
- const normalizedPromptSnippet = normalizePromptText(this.submitRetryPromptSnippet || this.currentTurnScope?.prompt || '');
1025
- const screenText = this.terminalScreen.getText() || '';
788
+ // ─── Script dispatch — builds inputs for CliScriptRunner ──────────────────
1026
789
 
1027
- this.recordTrace('settled', {
1028
- tail: summarizeCliTraceText(this.settledBuffer, 500),
1029
- screenText: summarizeCliTraceText(screenText, 1200),
1030
- detectStatus: status,
1031
- parsedStatus: parsedStatus || null,
1032
- parsedMessageCount: parsedMessages.length,
1033
- parsedLastAssistant: lastParsedAssistant ? summarizeCliTraceText(lastParsedAssistant.content, 280) : '',
1034
- parsedActiveModal: modal,
1035
- approval: modal,
1036
- ...buildCliTraceParseSnapshot({
1037
- accumulatedBuffer: this.accumulatedBuffer,
1038
- accumulatedRawBuffer: this.accumulatedRawBuffer,
1039
- responseBuffer: this.responseBuffer,
1040
- partialResponse: this.responseBuffer,
1041
- scope: this.currentTurnScope,
1042
- }),
790
+ runParseSession(): ParsedSession | null {
791
+ const screenText = this.terminalScreen.getText();
792
+ const parseScreenText = this.getParseScreenText(screenText);
793
+ const tail = this.recentOutputBuffer.slice(-500);
794
+ const input = buildCliParseInput({
795
+ accumulatedBuffer: this.accumulatedBuffer,
796
+ accumulatedRawBuffer: this.accumulatedRawBuffer,
797
+ recentOutputBuffer: this.recentOutputBuffer,
798
+ terminalScreenText: parseScreenText,
799
+ workingDir: this.workingDir,
800
+ providerSessionId: this.providerSessionId || undefined,
801
+ historySessionId: this.providerSessionId || undefined,
802
+ baseMessages: [],
803
+ partialResponse: this.responseBuffer,
804
+ isWaitingForResponse: this.engine.isWaitingForResponse,
805
+ scope: this.engine.currentTurnScope,
806
+ runtimeSettings: this.runtimeSettings,
807
+ spawnAt: this.spawnAt,
1043
808
  });
1044
-
1045
- if (
1046
- this.currentTurnScope
1047
- && !lastParsedAssistant
1048
- && !this.submitRetryUsed
1049
- && this.ptyProcess
1050
- && !this.hasActionableApproval()
1051
- && promptLikelyVisible(screenText, normalizedPromptSnippet)
1052
- && !this.hasMeaningfulResponseBuffer(normalizedPromptSnippet)
1053
- ) {
1054
- this.submitRetryUsed = true;
1055
- this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
1056
- LOG.info('CLI', `[${this.cliType}] Retrying submit key from settled parser (no assistant yet)`);
1057
- this.recordTrace('submit_write', {
1058
- mode: 'settled_retry',
1059
- sendKey: this.sendKey,
1060
- screenText: summarizeCliTraceText(screenText, 500),
1061
- });
1062
- this.ptyProcess.write(this.sendKey);
1063
- if (this.settleTimer) clearTimeout(this.settleTimer);
1064
- this.settleTimer = setTimeout(() => {
1065
- this.settleTimer = null;
1066
- this.settledBuffer = this.recentOutputBuffer;
1067
- this.evaluateSettled();
1068
- }, this.timeouts.outputSettle + 150);
1069
- return;
1070
- }
1071
-
1072
- if (this.currentTurnScope && !lastParsedAssistant) {
1073
- LOG.debug(
1074
- 'CLI',
1075
- `[${this.cliType}] Settled without assistant: prompt=${JSON.stringify(this.currentTurnScope.prompt).slice(0, 140)} responseBuffer=${JSON.stringify(summarizeCliTraceText(this.responseBuffer, 220)).slice(0, 260)} screen=${JSON.stringify(summarizeCliTraceText(screenText, 220)).slice(0, 260)} providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'}`
1076
- );
1077
- }
1078
-
1079
- if (!status) return;
1080
-
1081
- const prevStatus = this.currentStatus;
1082
- const ctx: SettledEvalContext = { now, modal, status, parsedMessages, lastParsedAssistant, parsedStatus: parsedStatus || null, prevStatus };
1083
-
1084
- if (!this.applyPendingScriptStatusDebounce(ctx)) return;
1085
-
1086
- const recentInteractiveActivity = this.hasRecentInteractiveActivity(now);
1087
- LOG.debug(
1088
- 'CLI',
1089
- `[${this.cliType}] settled diagnostics prompt=${JSON.stringify(this.currentTurnScope?.prompt || '').slice(0, 140)} status=${String(status || '')} parsedStatus=${String(parsedStatus || '')} parsedMsgCount=${parsedMessages.length} lastParsedAssistant=${JSON.stringify(summarizeCliTraceText(lastParsedAssistant?.content || '', 120)).slice(0, 160)} responseBuffer=${JSON.stringify(summarizeCliTraceText(this.responseBuffer, 160)).slice(0, 220)} screen=${JSON.stringify(summarizeCliTraceText(screenText, 160)).slice(0, 220)}`
1090
- );
1091
-
1092
- const shouldHoldGenerating =
1093
- status === 'idle'
1094
- && this.isWaitingForResponse
1095
- && !modal
1096
- && recentInteractiveActivity
1097
- && !(parsedStatus === 'idle' && !!lastParsedAssistant);
1098
-
1099
- if (shouldHoldGenerating) { this.applyHoldGenerating(ctx, recentInteractiveActivity); return; }
1100
- if (status === 'waiting_approval') { this.applyWaitingApproval(ctx); return; }
1101
- if (status === 'generating') { this.applyGenerating(ctx); return; }
1102
- if (status === 'idle') { this.applyIdle(ctx, now); }
1103
- }
1104
-
1105
- // Returns false if the caller should bail out (debounce pending).
1106
- private applyPendingScriptStatusDebounce(ctx: SettledEvalContext): boolean {
1107
- const { now, status, prevStatus } = ctx;
1108
- const shouldDebounce =
1109
- prevStatus === 'idle'
1110
- && !this.isWaitingForResponse
1111
- && !this.currentTurnScope
1112
- && (status === 'generating' || status === 'waiting_approval');
1113
-
1114
- if (!shouldDebounce) {
1115
- this.pendingScriptStatus = null;
1116
- this.pendingScriptStatusSince = 0;
1117
- if (this.pendingScriptStatusTimer) { clearTimeout(this.pendingScriptStatusTimer); this.pendingScriptStatusTimer = null; }
1118
- return true;
1119
- }
1120
-
1121
- const armPending = (delayMs: number) => {
1122
- if (this.pendingScriptStatusTimer) clearTimeout(this.pendingScriptStatusTimer);
1123
- this.pendingScriptStatusTimer = setTimeout(() => {
1124
- this.pendingScriptStatusTimer = null;
1125
- this.settledBuffer = this.recentOutputBuffer;
1126
- this.evaluateSettled();
1127
- }, delayMs);
1128
- };
1129
-
1130
- if (this.pendingScriptStatus !== status) {
1131
- this.pendingScriptStatus = status as 'generating' | 'waiting_approval';
1132
- this.pendingScriptStatusSince = now;
1133
- armPending(ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS);
1134
- return false;
1135
- }
1136
- const elapsed = now - this.pendingScriptStatusSince;
1137
- if (elapsed < ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS) {
1138
- armPending(ProviderCliAdapter.SCRIPT_STATUS_DEBOUNCE_MS - elapsed);
1139
- return false;
1140
- }
1141
- return true;
1142
- }
1143
-
1144
- private applyHoldGenerating(ctx: SettledEvalContext, recentInteractiveActivity: boolean): void {
1145
- const { status } = ctx;
1146
- this.clearIdleFinishCandidate('hold_generating_recent_activity');
1147
- this.setStatus('generating', 'recent_activity_hold');
1148
- if (this.idleTimeout) clearTimeout(this.idleTimeout);
1149
- this.idleTimeout = setTimeout(() => {
1150
- if (this.isWaitingForResponse && !this.hasActionableApproval()) {
1151
- if (this.shouldDeferIdleTimeoutFinish()) return;
1152
- this.finishResponse();
1153
- }
1154
- }, this.timeouts.generatingIdle);
1155
- this.recordTrace('hold_generating_recent_activity', {
1156
- scriptStatus: status,
1157
- recentInteractiveActivity,
1158
- lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
1159
- lastScreenChangeAt: this.lastScreenChangeAt,
1160
- holdMs: this.getStatusActivityHoldMs(),
1161
- ...buildCliTraceParseSnapshot({
1162
- accumulatedBuffer: this.accumulatedBuffer,
1163
- accumulatedRawBuffer: this.accumulatedRawBuffer,
1164
- responseBuffer: this.responseBuffer,
1165
- partialResponse: this.responseBuffer,
1166
- scope: this.currentTurnScope,
1167
- }),
809
+ const session = this.runner.parseSession({
810
+ ...input,
811
+ tail,
812
+ tailScreen: buildCliScreenSnapshot(tail),
1168
813
  });
1169
- this.onStatusChange?.();
1170
- }
1171
-
1172
- private applyWaitingApproval(ctx: SettledEvalContext): void {
1173
- const { modal } = ctx;
1174
- this.clearIdleFinishCandidate('waiting_approval');
1175
- const inCooldown = this.lastApprovalResolvedAt && (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown;
1176
- if (inCooldown && !modal) {
1177
- if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1178
- this.activeModal = null;
1179
- if (this.isWaitingForResponse) {
1180
- this.setStatus('idle', inCooldown ? 'approval_cooldown_non_actionable' : 'approval_prompt_gone_non_actionable');
1181
- if (this.idleTimeout) clearTimeout(this.idleTimeout);
1182
- this.idleTimeout = setTimeout(() => {
1183
- if (this.isWaitingForResponse && !this.hasActionableApproval()) {
1184
- if (this.shouldDeferIdleTimeoutFinish()) return;
1185
- this.finishResponse();
1186
- }
1187
- }, this.timeouts.generatingIdle);
1188
- } else {
1189
- this.setStatus('idle', inCooldown ? 'approval_cooldown_non_actionable' : 'approval_prompt_gone_non_actionable');
1190
- }
1191
- this.onStatusChange?.();
1192
- return;
1193
- }
1194
- if (!inCooldown) {
1195
- if (!modal) {
1196
- LOG.warn('CLI', `[${this.cliType}] detectStatus reported waiting_approval without parseApproval modal; ignoring non-actionable approval state`);
1197
- return;
1198
- }
1199
- this.isWaitingForResponse = true;
1200
- this.setStatus('waiting_approval', 'script_detect');
1201
- this.activeModal = modal;
1202
- if (this.idleTimeout) clearTimeout(this.idleTimeout);
1203
- this.armApprovalExitTimeout();
1204
- this.onStatusChange?.();
1205
- }
1206
- }
1207
-
1208
- private applyGenerating(ctx: SettledEvalContext): void {
1209
- const { modal, parsedMessages, lastParsedAssistant, parsedStatus, prevStatus } = ctx;
1210
- this.clearIdleFinishCandidate('generating');
1211
- const screenText = this.terminalScreen.getText() || '';
1212
- const effectiveScreenText = screenText || this.accumulatedBuffer;
1213
- const noActiveTurn = !this.currentTurnScope;
1214
- const looksIdleChrome = /(^|\n)\s*[❯›>]\s*(?:\n|$)/m.test(effectiveScreenText);
1215
- const parsedShowsLiveAssistantProgress = parsedStatus === 'generating'
1216
- && !!lastParsedAssistant
1217
- ;
1218
- if (prevStatus === 'idle' && !this.isWaitingForResponse && noActiveTurn && !modal && looksIdleChrome && !parsedShowsLiveAssistantProgress) {
1219
- return;
1220
- }
1221
- if (prevStatus === 'waiting_approval') {
1222
- // Transitioned out of approval → generating
1223
- if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1224
- this.activeModal = null;
1225
- this.lastApprovalResolvedAt = Date.now();
1226
- }
1227
- if (!this.isWaitingForResponse) {
1228
- this.isWaitingForResponse = true;
1229
- this.responseBuffer = '';
1230
- }
1231
- this.setStatus('generating', 'script_detect');
1232
- // Reset idle timeout
1233
- if (this.idleTimeout) clearTimeout(this.idleTimeout);
1234
- this.idleTimeout = setTimeout(() => {
1235
- if (this.isWaitingForResponse) {
1236
- if (this.shouldDeferIdleTimeoutFinish()) return;
1237
- this.finishResponse();
1238
- }
1239
- }, this.timeouts.generatingIdle);
1240
- this.onStatusChange?.();
814
+ if (session && typeof session === 'object') this.applyParsedSessionMetadata(session);
815
+ return session;
1241
816
  }
1242
817
 
1243
- private applyIdle(ctx: SettledEvalContext, now: number): void {
1244
- const { modal, lastParsedAssistant, prevStatus } = ctx;
1245
- if (prevStatus === 'waiting_approval') {
1246
- if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
1247
- this.activeModal = null;
1248
- this.lastApprovalResolvedAt = Date.now();
1249
- this.setStatus('idle', 'approval_prompt_gone_script_idle');
1250
- }
1251
- if (!this.isWaitingForResponse) {
1252
- if (prevStatus !== 'idle') {
1253
- this.clearIdleFinishCandidate('idle_without_response');
1254
- this.setStatus('idle', 'script_detect');
1255
- this.onStatusChange?.();
1256
- }
1257
- return;
1258
- }
1259
- const quietForMs = this.lastNonEmptyOutputAt ? (now - this.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
1260
- const screenStableMs = this.lastScreenChangeAt ? (now - this.lastScreenChangeAt) : 0;
1261
- const hasAssistantTurn = !!lastParsedAssistant;
1262
- const assistantLength = lastParsedAssistant?.content?.length || 0;
1263
- const idleFinishConfirmMs = this.getIdleFinishConfirmMs();
1264
- const idleQuietThresholdMs = Math.max(idleFinishConfirmMs, this.timeouts.outputSettle);
1265
- const idleReady = !modal
1266
- && hasAssistantTurn
1267
- && quietForMs >= idleQuietThresholdMs
1268
- && screenStableMs >= idleFinishConfirmMs;
1269
- const candidate = this.idleFinishCandidate;
1270
- const candidateQuiet = !!candidate
1271
- && candidate.responseEpoch === this.responseEpoch
1272
- && candidate.lastOutputAt === this.lastOutputAt
1273
- && candidate.lastScreenChangeAt === this.lastScreenChangeAt
1274
- && assistantLength >= candidate.assistantLength
1275
- && (now - candidate.armedAt) >= idleFinishConfirmMs;
1276
-
1277
- this.recordTrace('idle_decision', {
1278
- quietForMs,
1279
- screenStableMs,
1280
- hasAssistantTurn,
1281
- assistantLength,
1282
- hasModal: !!modal,
1283
- idleQuietThresholdMs,
1284
- idleStableThresholdMs: idleFinishConfirmMs,
1285
- idleReady,
1286
- idleFinishConfirmMs,
1287
- idleFinishCandidate: candidate,
1288
- candidateQuiet,
1289
- canFinishImmediately: idleReady && candidateQuiet,
1290
- submitPendingUntil: this.submitPendingUntil,
1291
- responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
1292
- ...buildCliTraceParseSnapshot({
1293
- accumulatedBuffer: this.accumulatedBuffer,
1294
- accumulatedRawBuffer: this.accumulatedRawBuffer,
1295
- responseBuffer: this.responseBuffer,
1296
- partialResponse: this.responseBuffer,
1297
- scope: this.currentTurnScope,
1298
- }),
1299
- });
1300
-
1301
- if (idleReady && candidateQuiet) {
1302
- this.clearIdleFinishCandidate('finish_response');
1303
- if (this.idleTimeout) clearTimeout(this.idleTimeout);
1304
- this.finishResponse();
1305
- return;
1306
- }
1307
-
1308
- if (idleReady) {
1309
- if (!candidate) {
1310
- this.armIdleFinishCandidate(assistantLength);
1311
- return;
1312
- }
1313
- } else {
1314
- this.clearIdleFinishCandidate('idle_not_ready');
1315
- }
1316
-
1317
- if (this.idleTimeout) clearTimeout(this.idleTimeout);
1318
- this.idleTimeout = setTimeout(() => {
1319
- if (this.isWaitingForResponse && !this.hasActionableApproval()) {
1320
- if (this.shouldDeferIdleTimeoutFinish()) return;
1321
- this.clearIdleFinishCandidate('idle_timeout_finish');
1322
- this.finishResponse();
1323
- }
1324
- }, this.timeouts.idleFinish);
1325
- }
1326
-
1327
- private finishResponse(): void {
1328
- if (this.submitPendingUntil > Date.now()) return;
1329
- if (this.responseSettleIgnoreUntil > Date.now()) return;
1330
- this.clearIdleFinishCandidate('finish_response_enter');
1331
- this.recordTrace('finish_response', {
1332
- ...buildCliTraceParseSnapshot({
1333
- accumulatedBuffer: this.accumulatedBuffer,
1334
- accumulatedRawBuffer: this.accumulatedRawBuffer,
1335
- responseBuffer: this.responseBuffer,
1336
- partialResponse: this.responseBuffer,
1337
- scope: this.currentTurnScope,
1338
- }),
818
+ runDetectStatus(text: string): string | null {
819
+ const screenText = this.terminalScreen.getText();
820
+ const tail = text.slice(-500);
821
+ return this.runner.detectStatus({
822
+ tail,
823
+ screenText,
824
+ rawBuffer: this.accumulatedRawBuffer,
825
+ isWaitingForResponse: this.engine.isWaitingForResponse,
826
+ screen: buildCliScreenSnapshot(screenText),
827
+ tailScreen: buildCliScreenSnapshot(tail),
1339
828
  });
1340
- const commitResult = this.commitCurrentTranscript();
1341
- if (this.shouldRetryFinishResponse(commitResult)) {
1342
- this.finishRetryCount += 1;
1343
- this.recordTrace('finish_response_retry', {
1344
- retryCount: this.finishRetryCount,
1345
- retryDelayMs: ProviderCliAdapter.FINISH_RETRY_DELAY_MS,
1346
- assistantContent: summarizeCliTraceText(commitResult.assistantContent, 220),
1347
- ...buildCliTraceParseSnapshot({
1348
- accumulatedBuffer: this.accumulatedBuffer,
1349
- accumulatedRawBuffer: this.accumulatedRawBuffer,
1350
- responseBuffer: this.responseBuffer,
1351
- partialResponse: this.responseBuffer,
1352
- scope: this.currentTurnScope,
1353
- }),
1354
- });
1355
- if (this.finishRetryTimer) clearTimeout(this.finishRetryTimer);
1356
- this.finishRetryTimer = setTimeout(() => {
1357
- this.finishRetryTimer = null;
1358
- if (this.isWaitingForResponse && !this.hasActionableApproval()) {
1359
- this.finishResponse();
1360
- }
1361
- }, ProviderCliAdapter.FINISH_RETRY_DELAY_MS);
1362
- return;
1363
- }
1364
- this.clearAllTimers();
1365
- this.responseBuffer = '';
1366
- this.isWaitingForResponse = false;
1367
- this.responseSettleIgnoreUntil = 0;
1368
- this.submitRetryUsed = false;
1369
- this.submitRetryPromptSnippet = '';
1370
- this.finishRetryCount = 0;
1371
- this.currentTurnScope = null;
1372
- this.activeModal = null;
1373
- this.setStatus('idle', 'response_finished');
1374
- this.onStatusChange?.();
1375
829
  }
1376
830
 
1377
- private maybeCommitVisibleIdleTranscript(session: ParsedSession, parsedMessages: CliChatMessage[]): boolean {
1378
- const allowImmediateScriptIdleCommit = this.provider.allowInputDuringGeneration === true;
1379
- if (!allowImmediateScriptIdleCommit) return false;
1380
- if (
1381
- !session
1382
- || session.status !== 'idle'
1383
- || !this.isWaitingForResponse
1384
- || !this.currentTurnScope
1385
- || this.activeModal
1386
- || session.modal
1387
- ) {
1388
- return false;
1389
- }
1390
-
1391
- const visibleAssistant = [...parsedMessages].reverse().find((m) => m.role === 'assistant' && m.content.trim());
1392
- if (!visibleAssistant) return false;
1393
-
1394
- this.clearAllTimers();
1395
- this.responseBuffer = '';
1396
- this.isWaitingForResponse = false;
1397
- this.responseSettleIgnoreUntil = 0;
1398
- this.submitRetryUsed = false;
1399
- this.submitRetryPromptSnippet = '';
1400
- this.finishRetryCount = 0;
1401
- this.currentTurnScope = null;
1402
- this.activeModal = null;
1403
- this.setStatus('idle', 'script_idle_commit');
1404
- this.onStatusChange?.();
1405
- this.recordTrace('script_idle_commit', {
1406
- messageCount: parsedMessages.length,
1407
- lastAssistant: summarizeCliTraceText(visibleAssistant.content, 320),
831
+ runParseApproval(tail: string): { message: string; buttons: string[] } | null {
832
+ const screenText = this.terminalScreen.getText();
833
+ const buffer = screenText || this.accumulatedBuffer;
834
+ return this.runner.parseApproval({
835
+ buffer,
836
+ screenText,
837
+ rawBuffer: this.accumulatedRawBuffer,
838
+ tail,
839
+ screen: buildCliScreenSnapshot(screenText),
840
+ bufferScreen: buildCliScreenSnapshot(buffer),
841
+ tailScreen: buildCliScreenSnapshot(tail),
1408
842
  });
1409
- return true;
1410
- }
1411
-
1412
- private commitCurrentTranscript(): { hasAssistant: boolean; assistantContent: string } {
1413
- const parsed = this.parseCurrentTranscript(
1414
- [],
1415
- this.responseBuffer,
1416
- this.currentTurnScope,
1417
- );
1418
- if (parsed && Array.isArray(parsed.messages)) {
1419
- const parsedMessages = normalizeCliParsedMessages(parsed.messages, {
1420
- scope: null,
1421
- lastOutputAt: this.lastOutputAt,
1422
- });
1423
- const lastAssistant = [...parsedMessages].reverse().find((message) => message.role === 'assistant');
1424
- if (this.currentTurnScope) {
1425
- LOG.info(
1426
- 'CLI',
1427
- `[${this.cliType}] commitCurrentTranscript parserMessages=${parsedMessages.length} finalLastAssistant=${JSON.stringify(summarizeCliTraceText(lastAssistant?.content || '', 220)).slice(0, 260)}`
1428
- );
1429
- }
1430
- this.recordTrace('commit_transcript', {
1431
- parsedStatus: parsed.status || null,
1432
- messageCount: parsedMessages.length,
1433
- lastAssistant: lastAssistant ? summarizeCliTraceText(lastAssistant.content, 320) : '',
1434
- messages: summarizeCliTraceMessages(parsedMessages),
1435
- ...buildCliTraceParseSnapshot({
1436
- accumulatedBuffer: this.accumulatedBuffer,
1437
- accumulatedRawBuffer: this.accumulatedRawBuffer,
1438
- responseBuffer: this.responseBuffer,
1439
- partialResponse: this.responseBuffer,
1440
- scope: this.currentTurnScope,
1441
- }),
1442
- });
1443
- if (!lastAssistant && this.currentTurnScope) {
1444
- LOG.warn(
1445
- 'CLI',
1446
- `[${this.cliType}] Commit without assistant turn: prompt=${JSON.stringify(this.currentTurnScope.prompt).slice(0, 140)} responseBuffer=${JSON.stringify(summarizeCliTraceText(this.responseBuffer, 220)).slice(0, 260)} providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'} scriptsPath=${this.providerResolutionMeta.scriptsPath || '-'}`
1447
- );
1448
- }
1449
- const hasAssistant = !!lastAssistant;
1450
- return {
1451
- hasAssistant,
1452
- assistantContent: lastAssistant?.content || '',
1453
- };
1454
- }
1455
- if (this.currentTurnScope) {
1456
- LOG.info(
1457
- 'CLI',
1458
- `[${this.cliType}] commitCurrentTranscript parsed.messages=none responseBufferLen=${this.responseBuffer.length} accumulatedBufferLen=${this.accumulatedBuffer.length} parsedStatus=${parsed?.status || '-'} providerDir=${this.providerResolutionMeta.providerDir || '-'} scriptDir=${this.providerResolutionMeta.scriptDir || '-'}`
1459
- );
1460
- }
1461
- return {
1462
- hasAssistant: false,
1463
- assistantContent: '',
1464
- };
1465
- }
1466
-
1467
-
1468
- // ─── Script Execution ──────────────────────────
1469
-
1470
- private invokeCliScript<T>(script: Function, input: any): T {
1471
- const hasStateFactory = typeof this.cliScripts?.createState === 'function';
1472
- const expectsStateArgument = hasStateFactory || this.scriptState !== null || script.length >= 2;
1473
- return expectsStateArgument
1474
- ? script(this.scriptState, input)
1475
- : script(input);
1476
- }
1477
-
1478
- private runParseSession(): ParsedSession | null {
1479
- if (typeof this.cliScripts?.parseSession !== 'function') {
1480
- this.parseErrorMessage = `${this.cliType} parseSession unavailable`;
1481
- return null;
1482
- }
1483
- try {
1484
- const screenText = this.terminalScreen.getText();
1485
- const parseScreenText = this.getParseScreenText(screenText);
1486
- const tail = this.recentOutputBuffer.slice(-500);
1487
- const input = buildCliParseInput({
1488
- accumulatedBuffer: this.accumulatedBuffer,
1489
- accumulatedRawBuffer: this.accumulatedRawBuffer,
1490
- recentOutputBuffer: this.recentOutputBuffer,
1491
- terminalScreenText: parseScreenText,
1492
- baseMessages: [],
1493
- partialResponse: this.responseBuffer,
1494
- isWaitingForResponse: this.isWaitingForResponse,
1495
- scope: this.currentTurnScope,
1496
- runtimeSettings: this.runtimeSettings,
1497
- });
1498
- const session = this.invokeCliScript<ParsedSession | null>(
1499
- this.cliScripts.parseSession,
1500
- { ...input, tail, tailScreen: buildCliScreenSnapshot(tail) },
1501
- );
1502
- this.parseErrorMessage = null;
1503
- return session && typeof session === 'object' ? session : null;
1504
- } catch (e: any) {
1505
- const message = e?.message || String(e);
1506
- this.parseErrorMessage = message;
1507
- LOG.warn('CLI', `[${this.cliType}] parseSession error: ${message}`);
1508
- return null;
1509
- }
1510
- }
1511
-
1512
- private runDetectStatus(text: string): string | null {
1513
- if (!this.cliScripts?.detectStatus) return null;
1514
- try {
1515
- const screenText = this.terminalScreen.getText();
1516
- const status = this.invokeCliScript<string | null>(this.cliScripts.detectStatus, {
1517
- tail: text.slice(-500),
1518
- screenText,
1519
- rawBuffer: this.accumulatedRawBuffer,
1520
- isWaitingForResponse: this.isWaitingForResponse,
1521
- screen: buildCliScreenSnapshot(screenText),
1522
- tailScreen: buildCliScreenSnapshot(text.slice(-500)),
1523
- });
1524
- return status;
1525
- } catch (e: any) {
1526
- LOG.warn('CLI', `[${this.cliType}] detectStatus error: ${e.message}`);
1527
- return null;
1528
- }
1529
843
  }
1530
844
 
1531
- private runParseApproval(tail: string): { message: string; buttons: string[] } | null {
1532
- if (!this.cliScripts?.parseApproval) return null;
1533
- try {
1534
- const screenText = this.terminalScreen.getText();
1535
- const buffer = screenText || this.accumulatedBuffer;
1536
- return this.invokeCliScript<{ message: string; buttons: string[] } | null>(this.cliScripts.parseApproval, {
1537
- buffer,
1538
- screenText,
1539
- rawBuffer: this.accumulatedRawBuffer,
1540
- tail,
1541
- screen: buildCliScreenSnapshot(screenText),
1542
- bufferScreen: buildCliScreenSnapshot(buffer),
1543
- tailScreen: buildCliScreenSnapshot(tail),
1544
- });
1545
- } catch (e: any) {
1546
- LOG.warn('CLI', `[${this.cliType}] parseApproval error: ${e.message}`);
1547
- return null;
845
+ private applyParsedSessionMetadata(parsed: any): void {
846
+ const providerSessionId = typeof parsed?.providerSessionId === 'string' && parsed.providerSessionId.trim()
847
+ ? parsed.providerSessionId.trim()
848
+ : '';
849
+ if (providerSessionId && providerSessionId !== this.providerSessionId) {
850
+ this.providerSessionId = providerSessionId;
851
+ this.updateRuntimeMeta({ providerSessionId });
1548
852
  }
1549
853
  }
1550
854
 
1551
- private hasActionableApproval(startupModal: { message: string; buttons: string[] } | null = null): boolean {
1552
- return !!(startupModal || this.activeModal);
1553
- }
1554
-
1555
855
  private projectEffectiveStatus(startupModal: { message: string; buttons: string[] } | null = null): CliSessionStatus['status'] {
1556
856
  if (this.parseErrorMessage) return 'error';
1557
- if (this.hasActionableApproval(startupModal)) return 'waiting_approval';
1558
- if (this.isWaitingForResponse && this.currentTurnScope && this.currentStatus !== 'stopped') return 'generating';
1559
- return this.currentStatus;
857
+ if (!!(startupModal || this.engine.activeModal)) return 'waiting_approval';
858
+ if (this.engine.isWaitingForResponse && this.engine.currentTurnScope && this.engine.currentStatus !== 'stopped') return 'generating';
859
+ return this.engine.currentStatus;
1560
860
  }
1561
861
 
1562
862
  // ─── Public API (CliAdapter) ───────────────────
@@ -1564,17 +864,73 @@ export class ProviderCliAdapter implements CliAdapter {
1564
864
  getStatus(options: { allowParse?: boolean } = {}): CliSessionStatus {
1565
865
  const allowParse = options.allowParse !== false;
1566
866
  const startupModal = allowParse && this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
867
+ const startupDetectedStatus = allowParse && this.startupParseGate && !startupModal
868
+ ? this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText())
869
+ : null;
1567
870
  let effectiveStatus = this.projectEffectiveStatus(startupModal);
1568
- let effectiveModal = startupModal || this.activeModal;
871
+ let effectiveModal = startupModal || this.engine.activeModal;
872
+ // (fix) When we have no captured modal yet, take one more live attempt
873
+ // with the current screen text — the engine's settle pass can miss
874
+ // the modal when it happens to fire exactly between writes, and
875
+ // without a modal here the dashboard could never show the buttons.
876
+ // We deliberately do NOT overwrite an existing engine.activeModal so
877
+ // a stable matched modal wins. This runs even outside the startup
878
+ // gate because Claude's approval frames can appear long after launch.
879
+ if (allowParse && !effectiveModal && this.engine.isWaitingForResponse) {
880
+ const liveDetect = this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText());
881
+ if (liveDetect === 'waiting_approval') {
882
+ const liveModal = this.runParseApproval(this.terminalScreen.getText())
883
+ || this.runParseApproval(this.recentOutputBuffer);
884
+ // (fix) Only surface modals that have at least one non-empty
885
+ // button. Half-rendered approval frames produced
886
+ // { message, buttons: [] } briefly — auto-approve would then
887
+ // pick buttonIndex=-1 and resolveModal would write the
888
+ // provider's index-0 key into the prompt. Skip those frames
889
+ // and let a later evaluate find the complete modal.
890
+ const buttonsOk = liveModal && Array.isArray(liveModal.buttons)
891
+ && liveModal.buttons.some((b: any) => typeof b === 'string' && b.trim());
892
+ if (liveModal && buttonsOk) {
893
+ effectiveModal = liveModal;
894
+ if (!this.engine.activeModal) this.engine.activeModal = liveModal;
895
+ }
896
+ }
897
+ }
898
+ // Only surface waiting_approval when we ALSO have a concrete modal
899
+ // (message + buttons). detectStatus alone can fire while parseApproval
900
+ // is still null — the engine logs "detectStatus=waiting_approval but
901
+ // parseApproval returned null; ignoring". Without this guard getStatus
902
+ // was shipping a bare waiting_approval with activeModal=null and the
903
+ // user perceived the flow as broken.
904
+ if (startupDetectedStatus === 'waiting_approval' && effectiveModal) {
905
+ effectiveStatus = 'waiting_approval';
906
+ } else if (startupDetectedStatus === 'idle' && !startupModal && !effectiveModal) {
907
+ effectiveStatus = 'idle';
908
+ }
1569
909
  if (allowParse && !startupModal && !effectiveModal) {
1570
910
  const parsed = this.getFreshParsedStatusCache();
1571
911
  const parsedModal = parsed?.activeModal && Array.isArray(parsed.activeModal.buttons)
1572
912
  && parsed.activeModal.buttons.some((button: any) => typeof button === 'string' && button.trim())
1573
913
  ? parsed.activeModal
1574
914
  : null;
915
+ const hasFinalAssistant = (p: any) => {
916
+ const msgs = Array.isArray(p?.messages) ? p.messages : [];
917
+ return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
918
+ };
1575
919
  if (parsed?.status === 'waiting_approval' && parsedModal) {
1576
920
  effectiveStatus = 'waiting_approval';
1577
921
  effectiveModal = parsedModal;
922
+ } else if (
923
+ effectiveStatus === 'idle'
924
+ && parsed?.status === 'generating'
925
+ && !hasFinalAssistant(parsed)
926
+ ) {
927
+ effectiveStatus = 'generating';
928
+ } else if (
929
+ effectiveStatus === 'generating'
930
+ && parsed?.status === 'idle'
931
+ && hasFinalAssistant(parsed)
932
+ ) {
933
+ effectiveStatus = 'idle';
1578
934
  }
1579
935
  }
1580
936
  const bufferState = this.getBufferState();
@@ -1583,8 +939,17 @@ export class ProviderCliAdapter implements CliAdapter {
1583
939
  messages: [],
1584
940
  workingDir: this.workingDir,
1585
941
  activeModal: effectiveModal,
1586
- errorMessage: this.parseErrorMessage || undefined,
1587
- errorReason: this.parseErrorMessage ? 'parse_error' : undefined,
942
+ pendingOutboundCount: this.pendingOutboundQueue.length,
943
+ pendingOutboundMessages: this.pendingOutboundQueue.map((message) => ({
944
+ id: message.id,
945
+ role: message.role,
946
+ content: message.content,
947
+ queuedAt: message.queuedAt,
948
+ source: message.source,
949
+ })),
950
+ errorMessage: this.parseErrorMessage || this.engine.providerErrorMessage || undefined,
951
+ errorReason: this.parseErrorMessage ? 'parse_error' : (this.engine.providerErrorReason || undefined),
952
+ providerSessionId: this.providerSessionId || undefined,
1588
953
  ...(bufferState ? { bufferState } : {}),
1589
954
  };
1590
955
  }
@@ -1600,15 +965,16 @@ export class ProviderCliAdapter implements CliAdapter {
1600
965
  const cached = this.parsedStatusCache;
1601
966
  const accumulatedRawBufferKey = this.getAccumulatedRawBufferCacheKey();
1602
967
  if (
1603
- cached
968
+ !this.providerOwnsTranscript()
969
+ && cached
1604
970
  && cached.responseBuffer === this.responseBuffer
1605
- && cached.currentTurnScope === this.currentTurnScope
971
+ && cached.currentTurnScope === this.engine.currentTurnScope
1606
972
  && cached.recentOutputBuffer === this.recentOutputBuffer
1607
973
  && cached.accumulatedBuffer === this.accumulatedBuffer
1608
974
  && cached.accumulatedRawBufferKey === accumulatedRawBufferKey
1609
975
  && cached.screenText === parseScreenText
1610
- && cached.currentStatus === this.currentStatus
1611
- && cached.activeModal === this.activeModal
976
+ && cached.currentStatus === this.engine.currentStatus
977
+ && cached.activeModal === this.engine.activeModal
1612
978
  && cached.cliName === this.cliName
1613
979
  ) {
1614
980
  return cached.result;
@@ -1623,14 +989,20 @@ export class ProviderCliAdapter implements CliAdapter {
1623
989
  const bufferState = this.getBufferState();
1624
990
  const result = {
1625
991
  id: (parsed as any).id || 'cli_session',
1626
- status: parsed.status || this.currentStatus,
992
+ status: parsed.status || this.engine.currentStatus,
1627
993
  title: (parsed as any).title || this.cliName,
1628
994
  messages: normalizeCliParsedMessages(parsed.messages, {
1629
995
  scope: null,
1630
996
  lastOutputAt: this.lastOutputAt,
1631
997
  }),
1632
998
  activeModal,
1633
- providerSessionId: typeof (parsed as any).providerSessionId === 'string' ? (parsed as any).providerSessionId : undefined,
999
+ providerSessionId: this.providerSessionId || (typeof (parsed as any).providerSessionId === 'string' ? (parsed as any).providerSessionId : undefined),
1000
+ errorMessage: typeof (parsed as any).errorMessage === 'string' && (parsed as any).errorMessage.trim()
1001
+ ? (parsed as any).errorMessage.trim()
1002
+ : undefined,
1003
+ errorReason: typeof (parsed as any).errorReason === 'string' && (parsed as any).errorReason.trim()
1004
+ ? (parsed as any).errorReason.trim()
1005
+ : undefined,
1634
1006
  ...(bufferState ? { bufferState } : {}),
1635
1007
  ...((parsed as any).transcriptAuthority === 'provider' || (parsed as any).transcriptAuthority === 'daemon'
1636
1008
  ? { transcriptAuthority: (parsed as any).transcriptAuthority }
@@ -1642,13 +1014,13 @@ export class ProviderCliAdapter implements CliAdapter {
1642
1014
 
1643
1015
  this.parsedStatusCache = {
1644
1016
  responseBuffer: this.responseBuffer,
1645
- currentTurnScope: this.currentTurnScope,
1017
+ currentTurnScope: this.engine.currentTurnScope,
1646
1018
  recentOutputBuffer: this.recentOutputBuffer,
1647
1019
  accumulatedBuffer: this.accumulatedBuffer,
1648
1020
  accumulatedRawBufferKey,
1649
1021
  screenText: parseScreenText,
1650
- currentStatus: this.currentStatus,
1651
- activeModal: this.activeModal,
1022
+ currentStatus: this.engine.currentStatus,
1023
+ activeModal: this.engine.activeModal,
1652
1024
  cliName: this.cliName,
1653
1025
  result,
1654
1026
  };
@@ -1656,22 +1028,22 @@ export class ProviderCliAdapter implements CliAdapter {
1656
1028
  }
1657
1029
 
1658
1030
  async invokeScript(scriptName: string, args?: Record<string, any>): Promise<any> {
1659
- const fn = this.cliScripts?.[scriptName];
1660
- if (typeof fn !== 'function') {
1661
- throw new Error(`CLI script '${scriptName}' not available`);
1662
- }
1663
1031
  const input = buildCliParseInput({
1664
1032
  accumulatedBuffer: this.accumulatedBuffer,
1665
1033
  accumulatedRawBuffer: this.accumulatedRawBuffer,
1666
1034
  recentOutputBuffer: this.recentOutputBuffer,
1667
1035
  terminalScreenText: this.getParseScreenText(this.terminalScreen.getText()),
1036
+ workingDir: this.workingDir,
1037
+ providerSessionId: this.providerSessionId || undefined,
1038
+ historySessionId: this.providerSessionId || undefined,
1668
1039
  baseMessages: [],
1669
1040
  partialResponse: this.responseBuffer,
1670
- isWaitingForResponse: this.isWaitingForResponse,
1671
- scope: this.currentTurnScope,
1041
+ isWaitingForResponse: this.engine.isWaitingForResponse,
1042
+ scope: this.engine.currentTurnScope,
1672
1043
  runtimeSettings: this.runtimeSettings,
1044
+ spawnAt: this.spawnAt,
1673
1045
  });
1674
- return await Promise.resolve(this.invokeCliScript(fn, {
1046
+ return await Promise.resolve(this.runner.invokeByName(scriptName, {
1675
1047
  ...input,
1676
1048
  args: args && typeof args === 'object' ? { ...args } : {},
1677
1049
  }));
@@ -1683,7 +1055,7 @@ export class ProviderCliAdapter implements CliAdapter {
1683
1055
 
1684
1056
  /** Whether this adapter has CLI scripts loaded */
1685
1057
  hasCliScripts(): boolean {
1686
- return typeof this.cliScripts?.detectStatus === 'function';
1058
+ return this.runner.hasDetectStatus();
1687
1059
  }
1688
1060
 
1689
1061
  /**
@@ -1692,24 +1064,29 @@ export class ProviderCliAdapter implements CliAdapter {
1692
1064
  */
1693
1065
  async resolveAction(data: any): Promise<void> {
1694
1066
  let promptText = '';
1695
- if (this.cliScripts && typeof this.cliScripts.resolveAction === 'function') {
1696
- try {
1697
- promptText = this.cliScripts.resolveAction(data);
1698
- } catch (e: any) {
1699
- LOG.warn('CLI', `[${this.cliType}] resolveAction error: ${e.message}`);
1700
- }
1067
+ try {
1068
+ promptText = this.runner.invokeByName('resolveAction', data);
1069
+ } catch {
1070
+ LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script not available`);
1071
+ return;
1701
1072
  }
1702
1073
  if (!promptText) {
1703
- LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script did not supply a prompt`);
1074
+ LOG.warn('CLI', `[${this.cliType}] resolveAction skipped: provider script did not return a prompt`);
1704
1075
  return;
1705
1076
  }
1706
1077
  await this.sendMessage(promptText);
1707
1078
  }
1708
1079
 
1080
+ async setInteractivePromptResponse(_response: InteractivePromptResponse): Promise<void> {
1081
+ // Legacy TUI providers do not currently expose an interactive prompt
1082
+ // protocol. Spec-backed claude-cli implements the first real wiring.
1083
+ }
1084
+
1709
1085
  private isSubmitStuck(normalizedPromptSnippet: string): boolean {
1710
- if (!this.ptyProcess || !this.isWaitingForResponse || this.submitRetryUsed) return false;
1711
- if (this.hasActionableApproval()) return false;
1712
- if (this.hasMeaningfulResponseBuffer(normalizedPromptSnippet)) return false;
1086
+ if (!this.ptyProcess || !this.engine.isWaitingForResponse || this.engine.submitRetryUsed) return false;
1087
+ if (this.engine.hasActionableApproval()) return false;
1088
+ // If there's already meaningful response content beyond the echoed prompt, not stuck
1089
+ if (this.hasMeaningfulResponseBufferLocal(normalizedPromptSnippet)) return false;
1713
1090
  const screenText = this.terminalScreen.getText();
1714
1091
  if (!promptLikelyVisible(screenText, normalizedPromptSnippet)) return false;
1715
1092
  const liveApproval = this.runParseApproval(screenText) || this.runParseApproval(this.recentOutputBuffer);
@@ -1718,20 +1095,41 @@ export class ProviderCliAdapter implements CliAdapter {
1718
1095
  return liveStatus !== 'generating' && liveStatus !== 'waiting_approval';
1719
1096
  }
1720
1097
 
1098
+ private hasMeaningfulResponseBufferLocal(promptSnippet: string): boolean {
1099
+ const raw = String(this.responseBuffer || '').trim();
1100
+ if (!raw) return false;
1101
+ const normalizedPrompt = compactPromptText(promptSnippet);
1102
+ if (!normalizedPrompt) return true;
1103
+ const normalizedBuffer = compactPromptText(raw);
1104
+ if (!normalizedBuffer) return false;
1105
+ if (normalizedBuffer === normalizedPrompt) return false;
1106
+ if (normalizedBuffer.startsWith(normalizedPrompt)) {
1107
+ const remainder = normalizedBuffer
1108
+ .slice(normalizedPrompt.length)
1109
+ .replace(/[─═\-]+/g, '')
1110
+ .replace(/⏵⏵accepteditson\([^)]*\)/gi, '')
1111
+ .replace(/accepteditson\([^)]*\)/gi, '')
1112
+ .replace(/(?:◐|◑|◒|◓|◔|◕|◉|●|·)?(?:x?high|medium|low|max)·?\/effort/gi, '')
1113
+ .replace(/updateavailable!run:[a-z0-9:._\-/]+/gi, '')
1114
+ .replace(/esctointerrupt/gi, '')
1115
+ .replace(/❯/g, '')
1116
+ .replace(/^[\s\-–—:;,.!/?]+/, '')
1117
+ .trim();
1118
+ return remainder.length > 0;
1119
+ }
1120
+ return true;
1121
+ }
1122
+
1721
1123
  private async writeToPty(data: string): Promise<void> {
1722
1124
  if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
1723
1125
  await this.ptyProcess.write(data);
1724
1126
  }
1725
1127
 
1726
1128
  private resetPendingSendState(reason: string): void {
1727
- this.isWaitingForResponse = false;
1728
1129
  this.responseBuffer = '';
1729
- this.currentTurnScope = null;
1730
- this.submitPendingUntil = 0;
1731
- this.clearIdleFinishCandidate(reason);
1732
1130
  if (this.responseTimeout) { clearTimeout(this.responseTimeout); this.responseTimeout = null; }
1733
- if (this.submitRetryTimer) { clearTimeout(this.submitRetryTimer); this.submitRetryTimer = null; }
1734
- if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
1131
+ this.engine.resetActiveTurnState();
1132
+ this.engine.clearIdleFinishCandidate(reason);
1735
1133
  }
1736
1134
 
1737
1135
  private commitSendUserTurn(state: SendMessageState): void {
@@ -1748,42 +1146,14 @@ export class ProviderCliAdapter implements CliAdapter {
1748
1146
  }
1749
1147
  this.responseTimeout = setTimeout(() => {
1750
1148
  this.responseTimeout = null;
1751
- if (!this.isWaitingForResponse) return;
1752
-
1753
- const detectedStatusBeforeEval = this.runDetectStatus(this.recentOutputBuffer);
1754
- this.recordTrace('response_timeout_check', {
1755
- timeoutMs,
1756
- detectedStatus: detectedStatusBeforeEval,
1757
- currentStatus: this.currentStatus,
1758
- isWaitingForResponse: this.isWaitingForResponse,
1759
- hasActionableApproval: this.hasActionableApproval(),
1760
- ...buildCliTraceParseSnapshot({
1761
- accumulatedBuffer: this.accumulatedBuffer,
1762
- accumulatedRawBuffer: this.accumulatedRawBuffer,
1763
- responseBuffer: this.responseBuffer,
1764
- partialResponse: this.responseBuffer,
1765
- scope: this.currentTurnScope,
1766
- }),
1767
- });
1149
+ if (!this.engine.isWaitingForResponse) return;
1768
1150
 
1769
- // maxResponse is a watchdog/checkpoint, not a completion signal. The old
1770
- // behavior called finishResponse() unconditionally at the default 300s,
1771
- // which fabricated idle transitions and downstream generating_completed
1772
- // notifications while long-running CLIs were still generating. Re-run the
1151
+ // maxResponse is a watchdog/checkpoint, not a completion signal. Re-run the
1773
1152
  // normal settled parser instead and keep the turn open unless the provider
1774
1153
  // actually reports an idle, commit-ready state.
1775
- this.settledBuffer = this.recentOutputBuffer;
1776
- this.evaluateSettled();
1777
-
1778
- if (this.isWaitingForResponse && !this.hasActionableApproval()) {
1779
- const detectedStatusAfterEval = this.runDetectStatus(this.recentOutputBuffer);
1780
- this.recordTrace('response_timeout_kept_open', {
1781
- timeoutMs,
1782
- detectedStatusBeforeEval,
1783
- detectedStatusAfterEval,
1784
- currentStatus: this.currentStatus,
1785
- isWaitingForResponse: this.isWaitingForResponse,
1786
- });
1154
+ this.engine.evaluateSettled(this.getSnapshot());
1155
+
1156
+ if (this.engine.isWaitingForResponse && !this.engine.hasActionableApproval()) {
1787
1157
  this.armResponseTimeout();
1788
1158
  }
1789
1159
  }, timeoutMs);
@@ -1798,34 +1168,20 @@ export class ProviderCliAdapter implements CliAdapter {
1798
1168
  private retrySubmitIfStuck(state: SendMessageState, attempt: number): void {
1799
1169
  this.submitRetryTimer = null;
1800
1170
  if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
1801
- const screenText = this.terminalScreen.getText();
1802
- this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
1171
+ this.engine.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
1803
1172
  LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt ${attempt})`);
1804
- this.recordTrace('submit_write', {
1805
- mode: 'submit_retry',
1806
- attempt,
1807
- sendKey: this.sendKey,
1808
- screenText: summarizeCliTraceText(screenText, 500),
1809
- });
1810
1173
  this.writeSubmitKeyForRetry('submit_retry');
1811
- if (attempt >= 3) { this.submitRetryUsed = true; return; }
1174
+ if (attempt >= 3) { this.engine.submitRetryUsed = true; return; }
1812
1175
  this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, attempt + 1), state.retryDelayMs);
1813
1176
  }
1814
1177
 
1815
1178
  private retryImmediateSubmitIfStuck(state: SendMessageState): void {
1816
1179
  this.submitRetryTimer = null;
1817
1180
  if (!this.isSubmitStuck(state.normalizedPromptSnippet)) return;
1818
- const screenText = this.terminalScreen.getText();
1819
- this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
1181
+ this.engine.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
1820
1182
  LOG.info('CLI', `[${this.cliType}] Retrying submit key for stuck prompt (attempt 1)`);
1821
- this.recordTrace('submit_write', {
1822
- mode: 'immediate_retry',
1823
- attempt: 1,
1824
- sendKey: this.sendKey,
1825
- screenText: summarizeCliTraceText(screenText, 500),
1826
- });
1827
1183
  this.writeSubmitKeyForRetry('immediate_retry');
1828
- this.submitRetryUsed = true;
1184
+ this.engine.submitRetryUsed = true;
1829
1185
  }
1830
1186
 
1831
1187
  private submitSendKey(state: SendMessageState, completion: SendMessageCompletion): void {
@@ -1833,13 +1189,7 @@ export class ProviderCliAdapter implements CliAdapter {
1833
1189
  completion.resolveOnce();
1834
1190
  return;
1835
1191
  }
1836
- this.submitPendingUntil = 0;
1837
- const screenText = this.terminalScreen.getText();
1838
- this.recordTrace('submit_write', {
1839
- mode: 'submit_key',
1840
- sendKey: this.sendKey,
1841
- screenText: summarizeCliTraceText(screenText, 500),
1842
- });
1192
+ this.engine.submitPendingUntil = 0;
1843
1193
  void this.writeToPty(this.sendKey).then(() => {
1844
1194
  this.commitSendUserTurn(state);
1845
1195
  this.submitRetryTimer = setTimeout(() => this.retrySubmitIfStuck(state, 1), state.retryDelayMs);
@@ -1849,13 +1199,7 @@ export class ProviderCliAdapter implements CliAdapter {
1849
1199
  }
1850
1200
 
1851
1201
  private submitImmediatePrompt(state: SendMessageState, completion: SendMessageCompletion): void {
1852
- this.submitPendingUntil = 0;
1853
- this.recordTrace('submit_write', {
1854
- mode: 'immediate',
1855
- text: summarizeCliTraceText(state.text, 500),
1856
- sendKey: this.sendKey,
1857
- screenText: summarizeCliTraceText(this.terminalScreen.getText(), 500),
1858
- });
1202
+ this.engine.submitPendingUntil = 0;
1859
1203
  void this.writeToPty(state.text + this.sendKey).then(() => {
1860
1204
  this.commitSendUserTurn(state);
1861
1205
  this.submitRetryTimer = setTimeout(() => this.retryImmediateSubmitIfStuck(state), state.retryDelayMs);
@@ -1899,7 +1243,8 @@ export class ProviderCliAdapter implements CliAdapter {
1899
1243
  requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
1900
1244
  screenText: summarizeCliTraceText(screenText, 1000),
1901
1245
  };
1902
- this.recordTrace('submit_echo_missing', diagnostic);
1246
+ LOG.warn('CLI', `[${this.cliType}] submit_echo_missing: ${JSON.stringify(diagnostic)}`);
1247
+
1903
1248
  if (this.requirePromptEchoBeforeSubmit) {
1904
1249
  // At this point the prompt text write already completed. Rejecting without
1905
1250
  // a submit key can leave the delegated CLI with an unsent prompt sitting at
@@ -1924,12 +1269,110 @@ export class ProviderCliAdapter implements CliAdapter {
1924
1269
  ), 50);
1925
1270
  }
1926
1271
 
1927
- async sendMessage(text: string): Promise<void> {
1272
+ async sendMessage(text: string, options: { force?: boolean } = {}): Promise<void> {
1273
+ if (options.force === true) {
1274
+ await this.forceSendMessage(text);
1275
+ return;
1276
+ }
1277
+ await this.sendMessageNow(text, true);
1278
+ }
1279
+
1280
+ async forceSendMessage(text: string): Promise<void> {
1281
+ if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
1282
+ const content = String(text || '');
1283
+ if (!content.trim()) return;
1284
+ LOG.info('CLI', `[${this.cliType}] force-sending prompt while status=${this.engine.currentStatus}`);
1285
+ await this.writeToPty(content + this.sendKey);
1286
+ this.onStatusChange?.();
1287
+ }
1288
+
1289
+ private enqueuePendingOutboundMessage(text: string, reason: string): void {
1290
+ const content = String(text || '');
1291
+ const duplicate = this.pendingOutboundQueue.some((message) => message.content === content);
1292
+ if (duplicate) {
1293
+ return;
1294
+ }
1295
+ const queuedAt = Date.now();
1296
+ const message: PendingOutboundMessage = {
1297
+ id: `${queuedAt}:${this.pendingOutboundQueue.length}:${Math.random().toString(36).slice(2, 10)}`,
1298
+ role: 'user',
1299
+ content,
1300
+ queuedAt,
1301
+ source: 'sendMessage',
1302
+ };
1303
+ this.pendingOutboundQueue.push(message);
1304
+ LOG.info('CLI', `[${this.cliType}] queued outbound message while busy (${reason}); queue=${this.pendingOutboundQueue.length}`);
1305
+ this.onStatusChange?.();
1306
+ }
1307
+
1308
+ private shouldQueuePendingOutboundMessage(parsedStatusBeforeSend: any | null = null): string | null {
1309
+ if (this.provider.allowInputDuringGeneration === true) return null;
1310
+ if (this.engine.hasActionableApproval()) return null;
1311
+ const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
1312
+ ? String(parsedStatusBeforeSend.status)
1313
+ : '';
1314
+ const hasFinalAssistant = (p: any) => {
1315
+ const msgs = Array.isArray(p?.messages) ? p.messages : [];
1316
+ return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
1317
+ };
1318
+ if (parsedSessionStatus === 'idle' && hasFinalAssistant(parsedStatusBeforeSend)) return null;
1319
+ if (this.engine.currentStatus === 'generating') return 'current_status_generating';
1320
+ if (parsedSessionStatus === 'generating' || parsedSessionStatus === 'long_generating') {
1321
+ const parsedModal = parsedStatusBeforeSend?.activeModal ?? parsedStatusBeforeSend?.modal ?? null;
1322
+ const parsedHasActionableModal = Boolean(
1323
+ parsedModal
1324
+ && Array.isArray(parsedModal.buttons)
1325
+ && parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
1326
+ );
1327
+ const terminalLooksIdle = this.engine.currentStatus === 'idle'
1328
+ && this.runDetectStatus(this.recentOutputBuffer) === 'idle'
1329
+ && !this.engine.isWaitingForResponse
1330
+ && !this.engine.currentTurnScope
1331
+ && !this.engine.hasActionableApproval()
1332
+ && !parsedHasActionableModal;
1333
+ return terminalLooksIdle ? null : `parsed_status_${parsedSessionStatus}`;
1334
+ }
1335
+ if (this.engine.isWaitingForResponse && this.engine.currentTurnScope) return 'active_turn_in_progress';
1336
+ return null;
1337
+ }
1338
+
1339
+ private schedulePendingOutboundFlush(delayMs = 0): void {
1340
+ if (this.pendingOutboundFlushTimer) clearTimeout(this.pendingOutboundFlushTimer);
1341
+ this.pendingOutboundFlushTimer = setTimeout(() => {
1342
+ this.pendingOutboundFlushTimer = null;
1343
+ void this.flushPendingOutboundQueue();
1344
+ }, Math.max(0, delayMs));
1345
+ }
1346
+
1347
+ private async flushPendingOutboundQueue(): Promise<void> {
1348
+ if (this.pendingOutboundFlushInFlight || this.pendingOutboundQueue.length === 0) return;
1349
+ if (this.engine.currentStatus !== 'idle' || this.engine.isWaitingForResponse || this.engine.hasActionableApproval()) return;
1350
+ this.pendingOutboundFlushInFlight = true;
1351
+ try {
1352
+ while (this.pendingOutboundQueue.length > 0) {
1353
+ if (this.engine.currentStatus !== 'idle' || this.engine.isWaitingForResponse || this.engine.hasActionableApproval()) break;
1354
+ const next = this.pendingOutboundQueue[0];
1355
+ try {
1356
+ await this.sendMessageNow(next.content, false);
1357
+ this.pendingOutboundQueue.shift();
1358
+ this.onStatusChange?.();
1359
+ } catch (error: any) {
1360
+ LOG.warn('CLI', `[${this.cliType}] queued outbound flush failed: ${error?.message || error}`);
1361
+ this.schedulePendingOutboundFlush(1000);
1362
+ break;
1363
+ }
1364
+ }
1365
+ } finally {
1366
+ this.pendingOutboundFlushInFlight = false;
1367
+ }
1368
+ }
1369
+
1370
+ private async sendMessageNow(text: string, allowQueue: boolean): Promise<void> {
1928
1371
  if (!this.ptyProcess) throw new Error(`${this.cliName} is not running`);
1929
1372
  const allowInputDuringGeneration = this.provider.allowInputDuringGeneration === true;
1930
1373
  const allowInterventionPrompt = allowInputDuringGeneration
1931
- && this.isWaitingForResponse
1932
- && !this.hasActionableApproval();
1374
+ && this.engine.isWaitingForResponse
1375
+ && !this.engine.hasActionableApproval();
1933
1376
  if (this.startupParseGate) {
1934
1377
  const deadline = Date.now() + 10000;
1935
1378
  while (this.startupParseGate && Date.now() < deadline) {
@@ -1937,27 +1380,33 @@ export class ProviderCliAdapter implements CliAdapter {
1937
1380
  await new Promise(resolve => setTimeout(resolve, 50));
1938
1381
  }
1939
1382
  }
1383
+ const parsedStatusBeforeSend = !allowInputDuringGeneration
1384
+ ? (() => {
1385
+ try {
1386
+ return this.getScriptParsedStatus?.() || null;
1387
+ } catch {
1388
+ return null;
1389
+ }
1390
+ })()
1391
+ : null;
1392
+ const queueReason = this.shouldQueuePendingOutboundMessage(parsedStatusBeforeSend);
1393
+ if (allowQueue && queueReason) {
1394
+ this.enqueuePendingOutboundMessage(text, queueReason);
1395
+ return;
1396
+ }
1940
1397
  if (!allowInterventionPrompt) {
1941
1398
  await this.waitForInteractivePrompt();
1942
1399
  }
1943
1400
  if (!this.ready) {
1944
1401
  this.resolveStartupState('send_precheck');
1945
- if (this.runDetectStatus(this.recentOutputBuffer) === 'idle' && this.currentStatus === 'idle') {
1402
+ if (this.runDetectStatus(this.recentOutputBuffer) === 'idle') {
1946
1403
  this.ready = true;
1947
1404
  this.startupParseGate = false;
1405
+ this.engine.setStatus('idle', 'send_message_idle_prompt_recovery');
1948
1406
  LOG.info('CLI', `[${this.cliType}] sendMessage recovered idle prompt readiness`);
1949
1407
  }
1950
1408
  }
1951
- if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.currentStatus})`);
1952
- const parsedStatusBeforeSend = !allowInputDuringGeneration
1953
- ? (() => {
1954
- try {
1955
- return this.getScriptParsedStatus?.() || null;
1956
- } catch {
1957
- return null;
1958
- }
1959
- })()
1960
- : null;
1409
+ if (!this.ready) throw new Error(`${this.cliName} not ready (status: ${this.engine.currentStatus})`);
1961
1410
  const parsedSessionStatus = typeof parsedStatusBeforeSend?.status === 'string'
1962
1411
  ? String(parsedStatusBeforeSend.status)
1963
1412
  : '';
@@ -1968,45 +1417,48 @@ export class ProviderCliAdapter implements CliAdapter {
1968
1417
  && Array.isArray(parsedModal.buttons)
1969
1418
  && parsedModal.buttons.some((candidate: unknown) => typeof candidate === 'string' && candidate.trim()),
1970
1419
  );
1971
- const terminalLooksIdle = this.currentStatus === 'idle'
1420
+ const terminalLooksIdle = this.engine.currentStatus === 'idle'
1972
1421
  && this.runDetectStatus(this.recentOutputBuffer) === 'idle'
1973
- && !this.isWaitingForResponse
1974
- && !this.currentTurnScope
1975
- && !this.hasActionableApproval()
1422
+ && !this.engine.isWaitingForResponse
1423
+ && !this.engine.currentTurnScope
1424
+ && !this.engine.hasActionableApproval()
1976
1425
  && !parsedHasActionableModal;
1977
1426
  if (!terminalLooksIdle) {
1427
+ if (allowQueue) {
1428
+ this.enqueuePendingOutboundMessage(text, `parsed_status_${parsedSessionStatus}`);
1429
+ return;
1430
+ }
1978
1431
  throw new Error(`${this.cliName} is still processing the previous prompt`);
1979
1432
  }
1980
1433
  }
1981
- if (this.isWaitingForResponse && !allowInputDuringGeneration) {
1982
- if (!this.clearStaleIdleResponseGuard('send_message_guard')) {
1434
+ if (this.engine.isWaitingForResponse && !allowInputDuringGeneration) {
1435
+ const snap = this.getSnapshot();
1436
+ if (
1437
+ !this.engine.clearStaleIdleResponseGuard('send_message_guard', snap)
1438
+ && !this.engine.clearParsedIdleResponseGuard('send_message_parsed_idle_guard', parsedStatusBeforeSend, snap)
1439
+ ) {
1440
+ if (allowQueue) {
1441
+ this.enqueuePendingOutboundMessage(text, 'waiting_for_response');
1442
+ return;
1443
+ }
1983
1444
  throw new Error(`${this.cliName} is still processing the previous prompt`);
1984
1445
  }
1985
1446
  }
1986
- this.isWaitingForResponse = true;
1987
1447
  this.responseBuffer = '';
1988
- this.finishRetryCount = 0;
1989
- if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
1990
- this.clearIdleFinishCandidate('send_message');
1991
- this.currentTurnScope = {
1448
+ const turnScope: TurnParseScope = {
1992
1449
  prompt: text,
1993
1450
  startedAt: Date.now(),
1994
1451
  bufferStart: this.accumulatedBuffer.length,
1995
1452
  rawBufferStart: this.accumulatedRawBuffer.length,
1996
1453
  };
1997
- this.recordTrace('send_message', {
1998
- text: summarizeCliTraceText(text, 500),
1999
- estimatedLines: estimatePromptDisplayLines(text),
2000
- turnScope: this.currentTurnScope,
2001
- });
2002
- LOG.info('CLI', `[${this.cliType}] sendMessage turn scope buffer=${this.currentTurnScope.bufferStart} raw=${this.currentTurnScope.rawBufferStart} prompt=${JSON.stringify(text).slice(0, 120)}`);
2003
- this.submitRetryUsed = false;
2004
- this.submitRetryPromptSnippet = extractPromptRetrySnippet(text);
2005
- const normalizedPromptSnippet = normalizePromptText(this.submitRetryPromptSnippet);
1454
+ LOG.info('CLI', `[${this.cliType}] sendMessage turn scope buffer=${turnScope.bufferStart} raw=${turnScope.rawBufferStart} prompt=${JSON.stringify(text).slice(0, 120)}`);
2006
1455
  if (this.submitRetryTimer) {
2007
1456
  clearTimeout(this.submitRetryTimer);
2008
1457
  this.submitRetryTimer = null;
2009
1458
  }
1459
+ this.engine.onTurnStarted(turnScope);
1460
+ this.engine.submitRetryPromptSnippet = extractPromptRetrySnippet(text);
1461
+ const normalizedPromptSnippet = normalizePromptText(this.engine.submitRetryPromptSnippet);
2010
1462
  const estimatedLines = estimatePromptDisplayLines(text);
2011
1463
  const submitDelayMs = this.sendDelayMs + Math.min(2000, Math.max(0, estimatedLines - 1) * 350);
2012
1464
  const maxEchoWaitMs = submitDelayMs + Math.max(1500, Math.min(5000, estimatedLines * 500));
@@ -2019,12 +1471,7 @@ export class ProviderCliAdapter implements CliAdapter {
2019
1471
  retryDelayMs,
2020
1472
  didCommitUserTurn: false,
2021
1473
  };
2022
- if (this.settleTimer) {
2023
- clearTimeout(this.settleTimer);
2024
- this.settleTimer = null;
2025
- }
2026
- this.responseEpoch += 1;
2027
- this.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
1474
+ this.engine.responseSettleIgnoreUntil = Date.now() + submitDelayMs + this.timeouts.outputSettle + 250;
2028
1475
  await new Promise<void>((resolve, reject) => {
2029
1476
  let resolved = false;
2030
1477
  const completion: SendMessageCompletion = {
@@ -2047,24 +1494,20 @@ export class ProviderCliAdapter implements CliAdapter {
2047
1494
  }
2048
1495
 
2049
1496
  if (submitDelayMs > 0) {
2050
- this.submitPendingUntil = Date.now() + submitDelayMs;
1497
+ this.engine.submitPendingUntil = Date.now() + submitDelayMs;
2051
1498
  }
2052
- this.recordTrace('submit_write', {
2053
- mode: 'type_then_submit',
2054
- text: summarizeCliTraceText(text, 500),
2055
- sendKey: this.sendKey,
2056
- screenText: summarizeCliTraceText(this.terminalScreen.getText(), 500),
2057
- });
2058
1499
  const submitStartedAt = Date.now();
2059
1500
  void this.writeToPty(text).then(
2060
1501
  () => this.waitForEchoAndSubmit(sendState, completion, submitStartedAt),
2061
1502
  completion.rejectOnce,
2062
1503
  );
2063
1504
  });
1505
+ // Schedule settle after successful send
1506
+ this.engine.scheduleSettle();
2064
1507
  }
2065
1508
 
2066
1509
  getPartialResponse(): string {
2067
- if (!this.isWaitingForResponse) return '';
1510
+ if (!this.engine.isWaitingForResponse) return '';
2068
1511
  return this.responseBuffer;
2069
1512
  }
2070
1513
 
@@ -2077,10 +1520,10 @@ export class ProviderCliAdapter implements CliAdapter {
2077
1520
  cliType: this.cliType,
2078
1521
  cliName: this.cliName,
2079
1522
  workingDir: this.workingDir,
2080
- currentStatus: this.currentStatus,
1523
+ currentStatus: this.engine.currentStatus,
2081
1524
  ready: this.ready,
2082
- isWaitingForResponse: this.isWaitingForResponse,
2083
- activeModal: this.activeModal,
1525
+ isWaitingForResponse: this.engine.isWaitingForResponse,
1526
+ activeModal: this.engine.activeModal,
2084
1527
  parseErrorMessage: this.parseErrorMessage,
2085
1528
  messageCounts: {
2086
1529
  parsedCache: Array.isArray(parsedResult?.messages) ? parsedResult.messages.length : undefined,
@@ -2106,10 +1549,10 @@ export class ProviderCliAdapter implements CliAdapter {
2106
1549
  lastScreenSnapshotReadAt: this.lastScreenSnapshotReadAt,
2107
1550
  },
2108
1551
  parser: {
2109
- scriptNames: listCliScriptNames(this.cliScripts),
2110
- traceSessionId: this.traceSessionId,
2111
- traceSeq: this.traceSeq,
2112
- currentTurnScope: this.currentTurnScope,
1552
+ scriptNames: this.runner.getScriptNames(),
1553
+ traceSessionId: this.engine.getTraceSessionId(),
1554
+ traceSeq: this.engine.getTraceEntries().length,
1555
+ currentTurnScope: this.engine.currentTurnScope,
2113
1556
  parsedStatusCache: parsedResult
2114
1557
  ? {
2115
1558
  id: parsedResult.id,
@@ -2122,26 +1565,25 @@ export class ProviderCliAdapter implements CliAdapter {
2122
1565
  activeModal: parsedResult.activeModal,
2123
1566
  }
2124
1567
  : null,
2125
- pendingScriptStatus: this.pendingScriptStatus,
2126
- pendingScriptStatusSince: this.pendingScriptStatusSince,
1568
+ pendingScriptStatus: this.engine.pendingScriptStatus,
1569
+ pendingScriptStatusSince: this.engine.pendingScriptStatusSince,
2127
1570
  },
2128
1571
  runtimeMetadata: this.getRuntimeMetadata(),
2129
- statusHistory: this.statusHistory.slice(-80),
2130
- traceEntries: this.traceEntries.slice(-120),
1572
+ statusHistory: this.engine.getStatusHistory().slice(-80),
1573
+ traceEntries: this.engine.getTraceEntries().slice(-120),
2131
1574
  timing: {
2132
1575
  spawnAt: this.spawnAt,
2133
1576
  startupFirstOutputAt: this.startupFirstOutputAt,
2134
- submitPendingUntil: this.submitPendingUntil,
2135
- responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
2136
- responseEpoch: this.responseEpoch,
1577
+ submitPendingUntil: this.engine.submitPendingUntil,
1578
+ responseSettleIgnoreUntil: this.engine.responseSettleIgnoreUntil,
1579
+ responseEpoch: this.engine.responseEpoch,
2137
1580
  resizeSuppressUntil: this.resizeSuppressUntil,
2138
- lastApprovalResolvedAt: this.lastApprovalResolvedAt,
1581
+ lastApprovalResolvedAt: this.engine.lastApprovalResolvedAt,
2139
1582
  },
2140
1583
  finish: {
2141
- idleFinishCandidate: this.idleFinishCandidate,
2142
- finishRetryCount: this.finishRetryCount,
2143
- submitRetryUsed: this.submitRetryUsed,
2144
- submitRetryPromptSnippet: this.submitRetryPromptSnippet,
1584
+ finishRetryCount: this.engine.finishRetryCount,
1585
+ submitRetryUsed: this.engine.submitRetryUsed,
1586
+ submitRetryPromptSnippet: this.engine.submitRetryPromptSnippet,
2145
1587
  },
2146
1588
  };
2147
1589
  }
@@ -2152,6 +1594,12 @@ export class ProviderCliAdapter implements CliAdapter {
2152
1594
  }
2153
1595
 
2154
1596
  updateRuntimeMeta(meta: Record<string, unknown>, replace = false): void {
1597
+ const nextProviderSessionId = typeof meta?.providerSessionId === 'string'
1598
+ ? meta.providerSessionId.trim()
1599
+ : '';
1600
+ if (nextProviderSessionId) {
1601
+ this.providerSessionId = nextProviderSessionId;
1602
+ }
2155
1603
  if (!this.ptyProcess || typeof this.ptyProcess.updateMeta !== 'function') return;
2156
1604
  this.ptyProcess.updateMeta(meta, replace);
2157
1605
  }
@@ -2172,7 +1620,7 @@ export class ProviderCliAdapter implements CliAdapter {
2172
1620
  this.timeouts.shutdownGrace,
2173
1621
  typeof resume.shutdownGraceMs === 'number' ? resume.shutdownGraceMs : 3000,
2174
1622
  );
2175
- const wasProcessing = this.currentStatus === 'generating' || this.currentStatus === 'waiting_approval';
1623
+ const wasProcessing = this.engine.currentStatus === 'generating' || this.engine.currentStatus === 'waiting_approval';
2176
1624
 
2177
1625
  try {
2178
1626
  if (wasProcessing) {
@@ -2210,7 +1658,7 @@ export class ProviderCliAdapter implements CliAdapter {
2210
1658
  return new Promise((resolve) => {
2211
1659
  const startedAt = Date.now();
2212
1660
  const timer = setInterval(() => {
2213
- if (!this.ptyProcess || this.currentStatus === 'stopped') {
1661
+ if (!this.ptyProcess || this.engine.currentStatus === 'stopped') {
2214
1662
  clearInterval(timer);
2215
1663
  resolve(true);
2216
1664
  return;
@@ -2224,18 +1672,20 @@ export class ProviderCliAdapter implements CliAdapter {
2224
1672
  }
2225
1673
 
2226
1674
  shutdown(): void {
2227
- this.clearIdleFinishCandidate('shutdown');
1675
+ this.engine.clearIdleFinishCandidate('shutdown');
2228
1676
  this.clearAllTimers();
2229
1677
  this.pendingOutputParseChunks = [];
2230
1678
  this.pendingTerminalQueryTail = '';
2231
1679
  this.ptyOutputChunks = [];
2232
- this.finishRetryCount = 0;
1680
+ if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
1681
+ this.pendingOutboundQueue = [];
1682
+ this.pendingOutboundFlushInFlight = false;
2233
1683
  if (this.ptyProcess) {
2234
1684
  this.ptyProcess.write('\x03');
2235
1685
  setTimeout(() => {
2236
1686
  try { this.ptyProcess?.kill(); } catch { }
2237
1687
  this.ptyProcess = null;
2238
- this.setStatus('stopped', 'stop_cmd');
1688
+ this.engine.setStatus('stopped', 'stop_cmd');
2239
1689
  this.ready = false;
2240
1690
  this.startupParseGate = false;
2241
1691
  this.spawnAt = 0;
@@ -2245,12 +1695,14 @@ export class ProviderCliAdapter implements CliAdapter {
2245
1695
  }
2246
1696
 
2247
1697
  detach(): void {
2248
- this.clearIdleFinishCandidate('detach');
1698
+ this.engine.clearIdleFinishCandidate('detach');
2249
1699
  this.clearAllTimers();
2250
1700
  this.pendingOutputParseChunks = [];
2251
1701
  this.pendingTerminalQueryTail = '';
2252
1702
  this.ptyOutputChunks = [];
2253
- this.finishRetryCount = 0;
1703
+ if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
1704
+ this.pendingOutboundQueue = [];
1705
+ this.pendingOutboundFlushInFlight = false;
2254
1706
  if (this.ptyProcess) {
2255
1707
  try {
2256
1708
  if (typeof this.ptyProcess.detach === 'function') {
@@ -2268,82 +1720,128 @@ export class ProviderCliAdapter implements CliAdapter {
2268
1720
  }
2269
1721
 
2270
1722
  clearHistory(): void {
2271
- this.clearIdleFinishCandidate('clear_history');
1723
+ this.engine.clearIdleFinishCandidate('clear_history');
2272
1724
  this.accumulatedBuffer = '';
2273
1725
  this.accumulatedRawBuffer = '';
2274
- this.currentTurnScope = null;
2275
- this.submitRetryUsed = false;
2276
- this.submitRetryPromptSnippet = '';
1726
+ this.engine.currentTurnScope = null;
1727
+ this.engine.submitRetryUsed = false;
1728
+ this.engine.submitRetryPromptSnippet = '';
2277
1729
  if (this.pendingOutputParseTimer) { clearTimeout(this.pendingOutputParseTimer); this.pendingOutputParseTimer = null; }
2278
1730
  this.pendingOutputParseChunks = [];
2279
1731
  this.pendingTerminalQueryTail = '';
2280
1732
  if (this.ptyOutputFlushTimer) { clearTimeout(this.ptyOutputFlushTimer); this.ptyOutputFlushTimer = null; }
2281
1733
  this.ptyOutputChunks = [];
2282
- if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
2283
- this.finishRetryCount = 0;
1734
+ if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
1735
+ this.pendingOutboundQueue = [];
1736
+ this.pendingOutboundFlushInFlight = false;
2284
1737
  this.resetTerminalScreen();
2285
1738
  this.ptyProcess?.clearBuffer?.();
2286
1739
  this.onStatusChange?.();
2287
1740
  }
2288
1741
 
2289
- isProcessing(): boolean { return this.isWaitingForResponse; }
1742
+ isProcessing(): boolean { return this.engine.isWaitingForResponse; }
2290
1743
  isReady(): boolean { return this.ready; }
2291
1744
 
2292
- async writeRaw(data: string): Promise<void> {
2293
- this.recordTrace('write_raw', {
2294
- keys: JSON.stringify(data),
2295
- length: data.length,
2296
- });
2297
- await this.writeToPty(data);
1745
+ // ─── State machine property accessors (delegate to engine) ──────────────
1746
+ // These expose engine state for external callers (tests, debug tools, etc.)
1747
+
1748
+ get currentStatus(): CliSessionStatus['status'] { return this.engine.currentStatus; }
1749
+ set currentStatus(v: CliSessionStatus['status']) { this.engine.setStatus(v); }
1750
+
1751
+ get isWaitingForResponse(): boolean { return this.engine.isWaitingForResponse; }
1752
+ set isWaitingForResponse(v: boolean) { this.engine.isWaitingForResponse = v; }
1753
+
1754
+ get activeModal(): { message: string; buttons: string[] } | null { return this.engine.activeModal; }
1755
+ set activeModal(v: { message: string; buttons: string[] } | null) { this.engine.activeModal = v; }
1756
+
1757
+ get currentTurnScope(): TurnParseScope | null { return this.engine.currentTurnScope; }
1758
+ set currentTurnScope(v: TurnParseScope | null) { this.engine.currentTurnScope = v; }
1759
+
1760
+ get responseEpoch(): number { return this.engine.responseEpoch; }
1761
+ set responseEpoch(v: number) { this.engine.responseEpoch = v; }
1762
+
1763
+ get submitRetryUsed(): boolean { return this.engine.submitRetryUsed; }
1764
+ set submitRetryUsed(v: boolean) { this.engine.submitRetryUsed = v; }
1765
+
1766
+ get submitRetryPromptSnippet(): string { return this.engine.submitRetryPromptSnippet; }
1767
+ set submitRetryPromptSnippet(v: string) { this.engine.submitRetryPromptSnippet = v; }
1768
+
1769
+ get responseSettleIgnoreUntil(): number { return this.engine.responseSettleIgnoreUntil; }
1770
+ set responseSettleIgnoreUntil(v: number) { this.engine.responseSettleIgnoreUntil = v; }
1771
+
1772
+ get submitPendingUntil(): number { return this.engine.submitPendingUntil; }
1773
+ set submitPendingUntil(v: number) { this.engine.submitPendingUntil = v; }
1774
+
1775
+ get lastApprovalResolvedAt(): number { return this.engine.lastApprovalResolvedAt; }
1776
+ set lastApprovalResolvedAt(v: number) { this.engine.lastApprovalResolvedAt = v; }
1777
+
1778
+ get providerErrorMessage(): string | null { return this.engine.providerErrorMessage; }
1779
+ get providerErrorReason(): string | null { return this.engine.providerErrorReason; }
1780
+
1781
+ get pendingScriptStatus(): 'generating' | 'waiting_approval' | null { return this.engine.pendingScriptStatus; }
1782
+ get pendingScriptStatusSince(): number { return this.engine.pendingScriptStatusSince; }
1783
+
1784
+ get finishRetryCount(): number { return this.engine.finishRetryCount; }
1785
+ set finishRetryCount(v: number) { this.engine.finishRetryCount = v; }
1786
+
1787
+ get traceSessionId(): string { return this.engine.getTraceSessionId(); }
1788
+ get traceEntries(): CliTraceEntry[] { return this.engine.getTraceEntries(); }
1789
+ get statusHistory(): { status: string; at: number; trigger?: string }[] { return this.engine.getStatusHistory(); }
1790
+ get traceSeq(): number { return this.engine.getTraceEntries().length; }
1791
+
1792
+ /** Expose engine's evaluateSettled for test access */
1793
+ evaluateSettled(): void {
1794
+ LOG.debug(
1795
+ 'CLI',
1796
+ `[${this.cliType}] settled diagnostics delegated to state engine`);
1797
+ this.engine.evaluateSettled(this.getSnapshot());
1798
+ }
1799
+ /** Expose engine's scheduleSettle for test access */
1800
+ scheduleSettle(): void { this.engine.scheduleSettle(); }
1801
+ /** Expose engine's clearIdleFinishCandidate for test access */
1802
+ clearIdleFinishCandidate(reason: string): void { this.engine.clearIdleFinishCandidate(reason); }
1803
+ /** Expose engine's finishResponse for test access */
1804
+ finishResponse(): void { this.engine.finishResponse(); }
1805
+ /** Returns a point-in-time snapshot of all buffer/screen state for external consumers (e.g. CliStateEngine). */
1806
+ getSnapshot(): CliBufferSnapshot {
1807
+ const screenText = this.terminalScreen.getText() || '';
1808
+ return {
1809
+ accumulatedBuffer: this.accumulatedBuffer,
1810
+ accumulatedRawBuffer: this.accumulatedRawBuffer,
1811
+ recentOutputBuffer: this.recentOutputBuffer,
1812
+ responseBuffer: this.responseBuffer,
1813
+ screenText,
1814
+ parseScreenText: this.getParseScreenText(screenText),
1815
+ workingDir: this.workingDir,
1816
+ providerSessionId: this.providerSessionId,
1817
+ runtimeSettings: this.runtimeSettings,
1818
+ isWaitingForResponse: this.engine.isWaitingForResponse,
1819
+ currentTurnScope: this.engine.currentTurnScope,
1820
+ lastOutputAt: this.lastOutputAt,
1821
+ lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
1822
+ lastScreenChangeAt: this.lastScreenChangeAt,
1823
+ lastScreenSnapshot: this.lastScreenSnapshot,
1824
+ };
1825
+ }
1826
+ isAlive(): boolean { return this.ptyProcess !== null; }
1827
+ flushOutboundQueue(): void { this.schedulePendingOutboundFlush(); }
1828
+
1829
+ async writeRaw(data: string | Buffer): Promise<void> {
1830
+ const str = Buffer.isBuffer(data) ? data.toString('utf8') : data;
1831
+ await this.writeToPty(str);
2298
1832
  }
2299
1833
 
2300
1834
  resolveModal(buttonIndex: number): void {
2301
- let modal = this.activeModal || this.runParseApproval(this.recentOutputBuffer);
2302
- if (!modal && typeof this.cliScripts?.parseSession === 'function') {
2303
- try {
2304
- const parsed = this.getScriptParsedStatus();
2305
- const parsedModal = parsed?.activeModal && Array.isArray(parsed.activeModal.buttons)
2306
- && parsed.activeModal.buttons.some((button: any) => typeof button === 'string' && button.trim())
2307
- ? parsed.activeModal
2308
- : null;
2309
- if (parsed?.status === 'waiting_approval' && parsedModal) {
2310
- modal = parsedModal;
2311
- this.activeModal = parsedModal;
2312
- if (this.currentStatus !== 'waiting_approval') {
2313
- this.setStatus('waiting_approval', 'resolve_modal_parse');
2314
- this.onStatusChange?.();
2315
- }
2316
- }
2317
- } catch {
2318
- // Ignore parse failures here; resolveModal falls back to current state.
2319
- }
2320
- }
2321
- if (!this.ptyProcess || ((this.currentStatus !== 'waiting_approval') && !modal)) return;
2322
- this.clearIdleFinishCandidate('resolve_modal');
2323
- this.recordTrace('resolve_modal', {
2324
- buttonIndex,
2325
- activeModal: modal,
2326
- });
2327
- this.activeModal = null;
2328
- this.lastApprovalResolvedAt = Date.now();
2329
- this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
2330
- if (this.approvalExitTimeout) {
2331
- clearTimeout(this.approvalExitTimeout);
2332
- this.approvalExitTimeout = null;
2333
- }
2334
- this.setStatus('generating', 'approval_resolved');
2335
- this.onStatusChange?.();
2336
- if (buttonIndex in this.approvalKeys) {
2337
- this.ptyProcess.write(this.approvalKeys[buttonIndex]);
2338
- } else {
2339
- const buttonCount = Array.isArray(modal?.buttons) ? modal.buttons.length : 0;
2340
- const clampedIndex = buttonCount > 0
2341
- ? Math.min(Math.max(0, buttonIndex), buttonCount - 1)
2342
- : Math.max(0, buttonIndex);
2343
- const DOWN = '\x1B[B';
2344
- const keys = DOWN.repeat(clampedIndex) + '\r';
2345
- this.ptyProcess.write(keys);
2346
- }
1835
+ this.engine.resolveModal(buttonIndex);
1836
+ }
1837
+
1838
+ getApprovalKeyForIndex(buttonIndex: number): string | undefined {
1839
+ return buttonIndex in this.approvalKeys ? this.approvalKeys[buttonIndex] : undefined;
1840
+ }
1841
+
1842
+ /** Returns true if an approval was resolved within the adapter's cooldown window. */
1843
+ isApprovalRecentlyResolved(): boolean {
1844
+ return this.engine.isApprovalRecentlyResolved();
2347
1845
  }
2348
1846
 
2349
1847
  resize(cols: number, rows: number): void {
@@ -2357,7 +1855,7 @@ export class ProviderCliAdapter implements CliAdapter {
2357
1855
  }
2358
1856
 
2359
1857
  private getParsedDebugState(): Record<string, any> | null {
2360
- if (this.startupParseGate || typeof this.cliScripts?.parseSession !== 'function') return null;
1858
+ if (this.startupParseGate || !this.runner.hasParseSession()) return null;
2361
1859
  try {
2362
1860
  const parsed = this.getScriptParsedStatus();
2363
1861
  return parsed && typeof parsed === 'object' ? parsed as Record<string, any> : null;
@@ -2369,18 +1867,43 @@ export class ProviderCliAdapter implements CliAdapter {
2369
1867
  getDebugState(): Record<string, any> {
2370
1868
  const screenText = sanitizeTerminalText(this.terminalScreen.getText());
2371
1869
  const startupModal = this.startupParseGate ? this.runParseApproval(this.recentOutputBuffer) : null;
2372
- const effectiveStatus = this.projectEffectiveStatus(startupModal);
2373
- const effectiveReady = this.ready || !!startupModal;
1870
+ const startupDetectedStatus = this.startupParseGate && !startupModal
1871
+ ? this.runDetectStatus(this.recentOutputBuffer || screenText)
1872
+ : null;
1873
+ const effectiveReady = this.ready || !!startupModal || startupDetectedStatus === 'waiting_approval';
2374
1874
  const parsedDebugState = this.getParsedDebugState();
2375
1875
  const parsedMessages = Array.isArray(parsedDebugState?.messages) ? parsedDebugState.messages : [];
1876
+ const hasFinalAssistant = (p: any) => {
1877
+ const msgs = Array.isArray(p?.messages) ? p.messages : [];
1878
+ return msgs.some((m: any) => m?.role === 'assistant' && typeof m.content === 'string' && m.content.trim());
1879
+ };
1880
+ let effectiveStatus = this.projectEffectiveStatus(startupModal);
1881
+ if (parsedDebugState?.status === 'error') {
1882
+ effectiveStatus = 'error';
1883
+ }
1884
+ // (fix) Mirror the getStatus contract: never surface waiting_approval
1885
+ // without a concrete modal. detectStatus alone can fire while
1886
+ // parseApproval is still null, and a bare debugState waiting_approval
1887
+ // confused dashboards reading the debug payload.
1888
+ const debugEffectiveModal = startupModal || this.engine.activeModal;
1889
+ if (startupDetectedStatus === 'waiting_approval' && debugEffectiveModal) {
1890
+ effectiveStatus = 'waiting_approval';
1891
+ }
1892
+ if (
1893
+ effectiveStatus === 'idle'
1894
+ && parsedDebugState?.status === 'generating'
1895
+ && !hasFinalAssistant(parsedDebugState)
1896
+ ) {
1897
+ effectiveStatus = 'generating';
1898
+ }
2376
1899
  return {
2377
1900
  type: this.cliType,
2378
1901
  name: this.cliName,
2379
1902
  providerResolution: this.providerResolutionMeta,
2380
1903
  status: effectiveStatus,
2381
1904
  projectedStatus: effectiveStatus,
2382
- rawStatus: this.currentStatus,
2383
- lifecycleStatus: this.isWaitingForResponse ? 'awaiting_response' : 'idle',
1905
+ rawStatus: this.engine.currentStatus,
1906
+ lifecycleStatus: this.engine.isWaitingForResponse ? 'awaiting_response' : 'idle',
2384
1907
  ready: effectiveReady,
2385
1908
  startupParseGate: this.startupParseGate,
2386
1909
  spawnAt: this.spawnAt,
@@ -2394,38 +1917,47 @@ export class ProviderCliAdapter implements CliAdapter {
2394
1917
  providerSessionId: parsedDebugState.providerSessionId,
2395
1918
  transcriptAuthority: parsedDebugState.transcriptAuthority,
2396
1919
  coverage: parsedDebugState.coverage,
1920
+ errorMessage: parsedDebugState.errorMessage,
1921
+ errorReason: parsedDebugState.errorReason,
2397
1922
  activeModal: parsedDebugState.activeModal,
2398
1923
  messageCount: parsedMessages.length,
2399
1924
  } : null,
2400
1925
  screenText: screenText.slice(-4000),
2401
- currentTurnScope: this.currentTurnScope,
1926
+ currentTurnScope: this.engine.currentTurnScope,
2402
1927
  startupBuffer: this.startupBuffer.slice(-4000),
2403
1928
  recentOutputBuffer: this.recentOutputBuffer.slice(-500),
2404
- settledBuffer: this.settledBuffer.slice(-500),
2405
1929
  accumulatedBufferLength: this.accumulatedBuffer.length,
2406
1930
  accumulatedRawBufferLength: this.accumulatedRawBuffer.length,
2407
1931
  rawBufferPreview: this.accumulatedRawBuffer.slice(-1000),
2408
1932
  sanitizedRawPreview: sanitizeTerminalText(this.accumulatedRawBuffer).slice(-1000),
2409
1933
  responseBuffer: this.responseBuffer.slice(-1000),
1934
+ pendingOutboundQueue: this.pendingOutboundQueue.map((message) => ({
1935
+ id: message.id,
1936
+ role: message.role,
1937
+ content: message.content,
1938
+ queuedAt: message.queuedAt,
1939
+ source: message.source,
1940
+ })),
1941
+ pendingOutboundCount: this.pendingOutboundQueue.length,
2410
1942
  lastOutputAt: this.lastOutputAt,
2411
1943
  lastNonEmptyOutputAt: this.lastNonEmptyOutputAt,
2412
1944
  lastScreenChangeAt: this.lastScreenChangeAt,
2413
1945
  lastScreenSnapshot: this.lastScreenSnapshot.slice(-500),
2414
- isWaitingForResponse: this.isWaitingForResponse,
2415
- activeModal: startupModal || this.activeModal,
2416
- lastApprovalResolvedAt: this.lastApprovalResolvedAt,
1946
+ isWaitingForResponse: this.engine.isWaitingForResponse,
1947
+ activeModal: startupModal || this.engine.activeModal,
1948
+ lastApprovalResolvedAt: this.engine.lastApprovalResolvedAt,
2417
1949
  sendDelayMs: this.sendDelayMs,
2418
1950
  sendKey: this.sendKey,
2419
1951
  submitStrategy: this.submitStrategy,
2420
1952
  requirePromptEchoBeforeSubmit: this.requirePromptEchoBeforeSubmit,
2421
- submitPendingUntil: this.submitPendingUntil,
2422
- responseSettleIgnoreUntil: this.responseSettleIgnoreUntil,
1953
+ submitPendingUntil: this.engine.submitPendingUntil,
1954
+ responseSettleIgnoreUntil: this.engine.responseSettleIgnoreUntil,
2423
1955
  resizeSuppressUntil: this.resizeSuppressUntil,
2424
1956
  hasCliScripts: this.hasCliScripts(),
2425
- scriptNames: listCliScriptNames(this.cliScripts),
2426
- traceSessionId: this.traceSessionId,
2427
- traceEntryCount: this.traceEntries.length,
2428
- statusHistory: this.statusHistory.slice(-30),
1957
+ scriptNames: this.runner.getScriptNames(),
1958
+ traceSessionId: this.engine.getTraceSessionId(),
1959
+ traceEntryCount: this.engine.getTraceEntries().length,
1960
+ statusHistory: this.engine.getStatusHistory().slice(-30),
2429
1961
  timeouts: this.timeouts,
2430
1962
  pendingOutputParseBufferLength: this.pendingOutputParseChunks.reduce((total, chunk) => total + chunk.length, 0),
2431
1963
  pendingOutputParseScheduled: !!this.pendingOutputParseTimer,
@@ -2435,20 +1967,21 @@ export class ProviderCliAdapter implements CliAdapter {
2435
1967
 
2436
1968
  getTraceState(limit = 120): Record<string, any> {
2437
1969
  const cappedLimit = Math.max(1, Math.min(500, Number.isFinite(limit) ? Math.floor(limit) : 120));
1970
+ const traceEntries = this.engine.getTraceEntries();
2438
1971
  return {
2439
- sessionId: this.traceSessionId,
1972
+ sessionId: this.engine.getTraceSessionId(),
2440
1973
  providerResolution: this.providerResolutionMeta,
2441
- entryCount: this.traceEntries.length,
2442
- entries: this.traceEntries.slice(-cappedLimit),
1974
+ entryCount: traceEntries.length,
1975
+ entries: traceEntries.slice(-cappedLimit),
2443
1976
  screenText: summarizeCliTraceText(this.terminalScreen.getText(), 4000),
2444
1977
  recentOutputBuffer: summarizeCliTraceText(this.recentOutputBuffer, 1000),
2445
1978
  responseBuffer: summarizeCliTraceText(this.responseBuffer, 1200),
2446
1979
  status: this.projectEffectiveStatus(),
2447
1980
  projectedStatus: this.projectEffectiveStatus(),
2448
- rawStatus: this.currentStatus,
2449
- lifecycleStatus: this.isWaitingForResponse ? 'awaiting_response' : 'idle',
2450
- activeModal: this.activeModal,
2451
- currentTurnScope: this.currentTurnScope,
1981
+ rawStatus: this.engine.currentStatus,
1982
+ lifecycleStatus: this.engine.isWaitingForResponse ? 'awaiting_response' : 'idle',
1983
+ activeModal: this.engine.activeModal,
1984
+ currentTurnScope: this.engine.currentTurnScope,
2452
1985
  messages: [],
2453
1986
  };
2454
1987
  }