@adhdev/daemon-core 0.9.82-rc.136 → 0.9.82-rc.138
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.
- package/dist/chat/source-machine.d.ts +166 -0
- package/dist/chat/source-resolver.d.ts +104 -0
- package/dist/cli-adapters/cli-script-runner.d.ts +45 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +169 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +72 -74
- package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +5 -0
- package/dist/config/chat-history.d.ts +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3507 -2288
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3515 -2301
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/beads-db.d.ts +54 -0
- package/dist/mesh/contracts.d.ts +164 -0
- package/dist/mesh/mesh-active-work.d.ts +7 -1
- package/dist/mesh/mesh-events.d.ts +10 -4
- package/dist/mesh/mesh-ledger.d.ts +21 -1
- package/dist/mesh/mesh-refine-status.d.ts +2 -3
- package/dist/mesh/mesh-work-queue.d.ts +17 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +2 -4
- package/dist/providers/contracts.d.ts +19 -0
- package/dist/providers/read-chat-contract.d.ts +29 -0
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/repo-mesh-types.d.ts +5 -0
- package/dist/shared-types.d.ts +7 -0
- package/dist/status/snapshot.d.ts +1 -0
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
- package/src/chat/source-machine.ts +534 -0
- package/src/chat/source-resolver.ts +0 -0
- package/src/chat/subscription-updates.ts +9 -0
- package/src/cli-adapters/cli-script-runner.ts +145 -0
- package/src/cli-adapters/cli-state-engine.ts +1054 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +0 -1
- package/src/cli-adapters/provider-cli-adapter.ts +413 -1399
- package/src/cli-adapters/provider-cli-parse.ts +3 -0
- package/src/cli-adapters/provider-cli-shared.ts +17 -1
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
- package/src/commands/chat-commands.ts +715 -368
- package/src/commands/router.ts +22 -2
- package/src/config/chat-history.ts +43 -16
- package/src/git/git-worktree.ts +8 -1
- package/src/index.ts +3 -2
- package/src/mesh/beads-db.ts +305 -2
- package/src/mesh/contracts.ts +329 -0
- package/src/mesh/coordinator-prompt.ts +12 -17
- package/src/mesh/mesh-active-work.ts +162 -59
- package/src/mesh/mesh-events.ts +198 -53
- package/src/mesh/mesh-ledger.ts +321 -105
- package/src/mesh/mesh-refine-status.ts +2 -3
- package/src/mesh/mesh-work-queue.ts +116 -120
- package/src/mesh/worktree-bootstrap-config.ts +17 -4
- package/src/providers/contracts.ts +19 -0
- package/src/providers/provider-loader.ts +21 -7
- package/src/providers/provider-schema.ts +12 -0
- package/src/providers/read-chat-contract.ts +74 -14
- package/src/providers/transcript-v2.ts +567 -0
- package/src/repo-mesh-types.ts +10 -0
- package/src/shared-types.ts +7 -0
- package/src/status/snapshot.ts +35 -11
- package/src/types.ts +5 -0
|
@@ -0,0 +1,1054 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CliStateEngine — CLI provider status state machine
|
|
3
|
+
*
|
|
4
|
+
* Owns all status-transition logic, timer management, and script-driven
|
|
5
|
+
* evaluation. Reads buffer state from the transport and writes to PTY via
|
|
6
|
+
* the transport interface — adapter stays as pure I/O layer.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { LOG } from '../logging/logger.js';
|
|
10
|
+
import {
|
|
11
|
+
buildCliParseInput,
|
|
12
|
+
normalizeCliParsedMessages,
|
|
13
|
+
type TurnParseScope,
|
|
14
|
+
} from './provider-cli-parse.js';
|
|
15
|
+
import {
|
|
16
|
+
buildCliScreenSnapshot,
|
|
17
|
+
compactPromptText,
|
|
18
|
+
normalizePromptText,
|
|
19
|
+
promptLikelyVisible,
|
|
20
|
+
type CliChatMessage,
|
|
21
|
+
type CliProviderModule,
|
|
22
|
+
type CliSessionStatus,
|
|
23
|
+
type CliTraceEntry,
|
|
24
|
+
type ParsedSession,
|
|
25
|
+
} from './provider-cli-shared.js';
|
|
26
|
+
import type { CliScriptRunner } from './cli-script-runner.js';
|
|
27
|
+
|
|
28
|
+
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
export interface CliBufferSnapshot {
|
|
31
|
+
accumulatedBuffer: string;
|
|
32
|
+
accumulatedRawBuffer: string;
|
|
33
|
+
recentOutputBuffer: string;
|
|
34
|
+
responseBuffer: string;
|
|
35
|
+
screenText: string;
|
|
36
|
+
parseScreenText: string;
|
|
37
|
+
workingDir: string;
|
|
38
|
+
providerSessionId: string | null;
|
|
39
|
+
runtimeSettings: Record<string, any>;
|
|
40
|
+
isWaitingForResponse: boolean;
|
|
41
|
+
currentTurnScope: TurnParseScope | null;
|
|
42
|
+
lastOutputAt: number;
|
|
43
|
+
lastNonEmptyOutputAt: number;
|
|
44
|
+
lastScreenChangeAt: number;
|
|
45
|
+
lastScreenSnapshot: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** What the engine needs from the transport layer */
|
|
49
|
+
export interface CliTransportAccess {
|
|
50
|
+
getSnapshot(): CliBufferSnapshot;
|
|
51
|
+
writeRaw(data: string | Buffer): void;
|
|
52
|
+
getApprovalKeyForIndex(buttonIndex: number): string | undefined;
|
|
53
|
+
flushOutboundQueue(): void;
|
|
54
|
+
isAlive(): boolean;
|
|
55
|
+
/** Optional: override script dispatch (used by tests to mock detection) */
|
|
56
|
+
runDetectStatus?(text: string): string | null;
|
|
57
|
+
/** Optional: override script dispatch (used by tests to mock approval) */
|
|
58
|
+
runParseApproval?(tail: string): { message: string; buttons: string[] } | null;
|
|
59
|
+
/** Optional: override full session parse (used by tests to mock parsing) */
|
|
60
|
+
runParseSession?(): ParsedSession | null;
|
|
61
|
+
/** Optional: provider type override — used when tests patch the adapter's cliType */
|
|
62
|
+
cliType?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface CliStateEngineCallbacks {
|
|
66
|
+
onStatusChange(): void;
|
|
67
|
+
onApplyParsedSession(session: ParsedSession): void;
|
|
68
|
+
onTurnCompleted(): void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface IdleFinishCandidate {
|
|
72
|
+
armedAt: number;
|
|
73
|
+
lastOutputAt: number;
|
|
74
|
+
lastScreenChangeAt: number;
|
|
75
|
+
responseEpoch: number;
|
|
76
|
+
assistantLength: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface SettledEvalContext {
|
|
80
|
+
now: number;
|
|
81
|
+
modal: { message: string; buttons: string[] } | null;
|
|
82
|
+
status: string;
|
|
83
|
+
parsedMessages: CliChatMessage[];
|
|
84
|
+
lastParsedAssistant: CliChatMessage | undefined;
|
|
85
|
+
parsedStatus: string | null;
|
|
86
|
+
prevStatus: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Constants ─────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
const SCRIPT_STATUS_DEBOUNCE_MS = 3000;
|
|
92
|
+
const MAX_FINISH_RETRIES = 2;
|
|
93
|
+
const FINISH_RETRY_DELAY_MS = 300;
|
|
94
|
+
const MAX_TRACE_ENTRIES = 250;
|
|
95
|
+
const APPROVAL_EXIT_TIMEOUT_MS = 60_000;
|
|
96
|
+
const IDLE_CONFIRMATION_GRACE_MS = 2_000;
|
|
97
|
+
|
|
98
|
+
// ─── Engine ────────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
export class CliStateEngine {
|
|
101
|
+
// ── Status ───────────────────────────────────────
|
|
102
|
+
currentStatus: CliSessionStatus['status'] = 'starting';
|
|
103
|
+
isWaitingForResponse = false;
|
|
104
|
+
currentTurnScope: TurnParseScope | null = null;
|
|
105
|
+
activeModal: { message: string; buttons: string[] } | null = null;
|
|
106
|
+
|
|
107
|
+
// ── Approval ─────────────────────────────────────
|
|
108
|
+
lastApprovalResolvedAt = 0;
|
|
109
|
+
lastResolvedModalMessage = '';
|
|
110
|
+
private approvalExitTimeout: NodeJS.Timeout | null = null;
|
|
111
|
+
|
|
112
|
+
// ── Response tracking ────────────────────────────
|
|
113
|
+
responseEpoch = 0;
|
|
114
|
+
submitPendingUntil = 0;
|
|
115
|
+
responseSettleIgnoreUntil = 0;
|
|
116
|
+
submitRetryUsed = false;
|
|
117
|
+
submitRetryPromptSnippet = '';
|
|
118
|
+
finishRetryCount = 0;
|
|
119
|
+
providerErrorMessage: string | null = null;
|
|
120
|
+
providerErrorReason: string | null = null;
|
|
121
|
+
|
|
122
|
+
// ── Timers ───────────────────────────────────────
|
|
123
|
+
private settleTimer: NodeJS.Timeout | null = null;
|
|
124
|
+
private idleTimeout: NodeJS.Timeout | null = null;
|
|
125
|
+
private finishRetryTimer: NodeJS.Timeout | null = null;
|
|
126
|
+
private providerErrorRetryTimer: NodeJS.Timeout | null = null;
|
|
127
|
+
private providerErrorRetryKey = '';
|
|
128
|
+
|
|
129
|
+
// ── Debounce ─────────────────────────────────────
|
|
130
|
+
pendingScriptStatus: 'generating' | 'waiting_approval' | null = null;
|
|
131
|
+
pendingScriptStatusSince = 0;
|
|
132
|
+
private pendingScriptStatusTimer: NodeJS.Timeout | null = null;
|
|
133
|
+
|
|
134
|
+
// ── Idle candidate ───────────────────────────────
|
|
135
|
+
private idleFinishCandidate: IdleFinishCandidate | null = null;
|
|
136
|
+
|
|
137
|
+
// ── Idle confirmation grace ──────────────────────
|
|
138
|
+
/**
|
|
139
|
+
* `finishResponse` produces the `generating → idle` transition that
|
|
140
|
+
* coordinators interpret as "task complete". Some providers (antigravity-
|
|
141
|
+
* cli observed in the wild) briefly paint a screen that looks like an
|
|
142
|
+
* idle prompt between tool result frames while still actively running,
|
|
143
|
+
* which fired `response_finished` and broke completion semantics.
|
|
144
|
+
* We defer the actual idle transition by IDLE_CONFIRMATION_GRACE_MS and
|
|
145
|
+
* cancel it if the scripted detection re-detects generating during that
|
|
146
|
+
* window — a true completion stays idle for many seconds, so a 2-second
|
|
147
|
+
* grace is sufficient to filter the paint blip.
|
|
148
|
+
*/
|
|
149
|
+
private pendingIdleFinishTimer: NodeJS.Timeout | null = null;
|
|
150
|
+
private pendingIdleFinishAt = 0;
|
|
151
|
+
|
|
152
|
+
// ── Status history (debug) ───────────────────────
|
|
153
|
+
private statusHistory: { status: string; at: number; trigger?: string }[] = [];
|
|
154
|
+
private traceEntries: CliTraceEntry[] = [];
|
|
155
|
+
private traceSeq = 0;
|
|
156
|
+
private traceSessionId = '';
|
|
157
|
+
|
|
158
|
+
constructor(
|
|
159
|
+
private readonly provider: CliProviderModule,
|
|
160
|
+
private readonly runner: CliScriptRunner,
|
|
161
|
+
private readonly transport: CliTransportAccess,
|
|
162
|
+
private readonly callbacks: CliStateEngineCallbacks,
|
|
163
|
+
private readonly timeouts: Required<NonNullable<CliProviderModule['timeouts']>>,
|
|
164
|
+
) {}
|
|
165
|
+
|
|
166
|
+
// ─── Public API ────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
setStatus(status: CliSessionStatus['status'], trigger?: string): void {
|
|
169
|
+
const prev = this.currentStatus;
|
|
170
|
+
if (prev === status) return;
|
|
171
|
+
this.currentStatus = status;
|
|
172
|
+
this.statusHistory.push({ status, at: Date.now(), trigger });
|
|
173
|
+
if (this.statusHistory.length > 50) this.statusHistory.shift();
|
|
174
|
+
this.recordTrace('status', { previousStatus: prev, trigger: trigger || null });
|
|
175
|
+
LOG.info('CLI', `[${this.provider.type}] status: ${prev} → ${status}${trigger ? ` (${trigger})` : ''}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
scheduleSettle(): void {
|
|
179
|
+
if (this.settleTimer) clearTimeout(this.settleTimer);
|
|
180
|
+
const epoch = this.responseEpoch;
|
|
181
|
+
const delay = Math.max(
|
|
182
|
+
this.timeouts.outputSettle,
|
|
183
|
+
this.submitPendingUntil > Date.now()
|
|
184
|
+
? (this.submitPendingUntil - Date.now()) + this.timeouts.outputSettle
|
|
185
|
+
: 0,
|
|
186
|
+
);
|
|
187
|
+
this.settleTimer = setTimeout(() => {
|
|
188
|
+
this.settleTimer = null;
|
|
189
|
+
if (epoch !== this.responseEpoch) return;
|
|
190
|
+
this.evaluateSettled(this.transport.getSnapshot());
|
|
191
|
+
}, delay);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Called from sendMessage in transport once a turn scope is established. */
|
|
195
|
+
onTurnStarted(turnScope: TurnParseScope): void {
|
|
196
|
+
this.isWaitingForResponse = true;
|
|
197
|
+
this.finishRetryCount = 0;
|
|
198
|
+
this.clearIdleFinishCandidate('send_message');
|
|
199
|
+
this.currentTurnScope = turnScope;
|
|
200
|
+
this.responseEpoch += 1;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Called when PTY exits */
|
|
204
|
+
onPtyExit(): void {
|
|
205
|
+
this.clearAllTimers();
|
|
206
|
+
this.setStatus('stopped', 'pty_exit');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Called when adapter starts up successfully */
|
|
210
|
+
onSpawnReady(): void {
|
|
211
|
+
this.setStatus('starting', 'pty_ready');
|
|
212
|
+
this.traceEntries = [];
|
|
213
|
+
this.traceSeq = 0;
|
|
214
|
+
this.traceSessionId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
215
|
+
this.recordTrace('session_start', { providerType: this.provider.type });
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
resolveModal(buttonIndex: number): void {
|
|
219
|
+
const snap = this.transport.getSnapshot();
|
|
220
|
+
const parseApproval = typeof this.transport.runParseApproval === 'function'
|
|
221
|
+
? (s: CliBufferSnapshot) => this.transport.runParseApproval!(s.recentOutputBuffer.slice(-500))
|
|
222
|
+
: (s: CliBufferSnapshot) => this.runParseApproval(s);
|
|
223
|
+
let modal = this.activeModal ?? parseApproval(snap);
|
|
224
|
+
|
|
225
|
+
if (!modal && this.runner.hasParseSession()) {
|
|
226
|
+
try {
|
|
227
|
+
const parsed = this.runParseSession(snap) as any;
|
|
228
|
+
const parsedModal = parsed?.activeModal && Array.isArray(parsed.activeModal.buttons)
|
|
229
|
+
&& parsed.activeModal.buttons.some((b: any) => typeof b === 'string' && b.trim())
|
|
230
|
+
? parsed.activeModal : null;
|
|
231
|
+
if (parsed?.status === 'waiting_approval' && parsedModal) {
|
|
232
|
+
modal = parsedModal;
|
|
233
|
+
this.activeModal = parsedModal;
|
|
234
|
+
if (this.currentStatus !== 'waiting_approval') {
|
|
235
|
+
this.setStatus('waiting_approval', 'resolve_modal_parse');
|
|
236
|
+
this.callbacks.onStatusChange();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
} catch { /* ignore parse failures */ }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!this.transport.isAlive() || (this.currentStatus !== 'waiting_approval' && !modal)) return;
|
|
243
|
+
|
|
244
|
+
const currentModalMessage = typeof modal?.message === 'string' ? modal.message.trim() : '';
|
|
245
|
+
const inCooldown = !!this.lastApprovalResolvedAt
|
|
246
|
+
&& (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown;
|
|
247
|
+
if (inCooldown && currentModalMessage === this.lastResolvedModalMessage) return;
|
|
248
|
+
|
|
249
|
+
this.clearIdleFinishCandidate('resolve_modal');
|
|
250
|
+
this.recordTrace('resolve_modal', { buttonIndex, activeModal: modal });
|
|
251
|
+
this.activeModal = null;
|
|
252
|
+
this.lastApprovalResolvedAt = Date.now();
|
|
253
|
+
this.lastResolvedModalMessage = currentModalMessage;
|
|
254
|
+
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
255
|
+
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
256
|
+
this.setStatus('generating', 'approval_resolved');
|
|
257
|
+
this.callbacks.onStatusChange();
|
|
258
|
+
|
|
259
|
+
const approvalKey = this.transport.getApprovalKeyForIndex(buttonIndex);
|
|
260
|
+
if (approvalKey !== undefined) {
|
|
261
|
+
this.transport.writeRaw(approvalKey);
|
|
262
|
+
} else {
|
|
263
|
+
const DOWN = '\x1B[B';
|
|
264
|
+
const buttonCount = Array.isArray(modal?.buttons) ? modal.buttons.length : 0;
|
|
265
|
+
const clamped = buttonCount > 0
|
|
266
|
+
? Math.min(Math.max(0, buttonIndex), buttonCount - 1)
|
|
267
|
+
: Math.max(0, buttonIndex);
|
|
268
|
+
this.transport.writeRaw(DOWN.repeat(clamped) + '\r');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
isApprovalRecentlyResolved(): boolean {
|
|
273
|
+
return !!(this.lastApprovalResolvedAt
|
|
274
|
+
&& (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Called from sendMessage before starting a new turn.
|
|
279
|
+
* Clears stale idle response state when the terminal looks idle and no modal is active.
|
|
280
|
+
*/
|
|
281
|
+
clearStaleIdleResponseGuard(reason: string, snap: CliBufferSnapshot): boolean {
|
|
282
|
+
const blockingModal = this.activeModal
|
|
283
|
+
?? (typeof this.transport.runParseApproval === 'function'
|
|
284
|
+
? this.transport.runParseApproval(snap.recentOutputBuffer.slice(-500))
|
|
285
|
+
: this.runParseApproval(snap));
|
|
286
|
+
const isIdle = (typeof this.transport.runDetectStatus === 'function'
|
|
287
|
+
? this.transport.runDetectStatus(snap.recentOutputBuffer)
|
|
288
|
+
: this.runDetectStatus(snap)) === 'idle';
|
|
289
|
+
if (!this.isWaitingForResponse || this.currentStatus !== 'idle' || !isIdle || !!blockingModal) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
this.clearAllTimers();
|
|
293
|
+
this.clearIdleFinishCandidate(reason);
|
|
294
|
+
this.isWaitingForResponse = false;
|
|
295
|
+
this.responseSettleIgnoreUntil = 0;
|
|
296
|
+
this.submitRetryUsed = false;
|
|
297
|
+
this.submitRetryPromptSnippet = '';
|
|
298
|
+
this.finishRetryCount = 0;
|
|
299
|
+
this.currentTurnScope = null;
|
|
300
|
+
this.activeModal = null;
|
|
301
|
+
this.recordTrace('stale_idle_response_cleared', { reason });
|
|
302
|
+
this.callbacks.onTurnCompleted();
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Called from sendMessage before starting a new turn.
|
|
308
|
+
* Clears stale idle response state when the parsed session confirms idle with a final assistant message.
|
|
309
|
+
*/
|
|
310
|
+
clearParsedIdleResponseGuard(reason: string, parsedStatus: any, snap: CliBufferSnapshot): boolean {
|
|
311
|
+
const parsedRawStatus = typeof parsedStatus?.status === 'string' ? parsedStatus.status.trim() : '';
|
|
312
|
+
const parsedModal = parsedStatus?.activeModal ?? parsedStatus?.modal ?? null;
|
|
313
|
+
const blockingModal = this.activeModal
|
|
314
|
+
?? (typeof this.transport.runParseApproval === 'function'
|
|
315
|
+
? this.transport.runParseApproval(snap.recentOutputBuffer.slice(-500))
|
|
316
|
+
: this.runParseApproval(snap));
|
|
317
|
+
if (
|
|
318
|
+
!this.isWaitingForResponse
|
|
319
|
+
|| parsedRawStatus !== 'idle'
|
|
320
|
+
|| !!parsedModal
|
|
321
|
+
|| !!blockingModal
|
|
322
|
+
|| !this.parsedStatusHasFinalAssistantMessage(parsedStatus)
|
|
323
|
+
) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
this.clearAllTimers();
|
|
327
|
+
this.clearIdleFinishCandidate(reason);
|
|
328
|
+
this.isWaitingForResponse = false;
|
|
329
|
+
this.responseSettleIgnoreUntil = 0;
|
|
330
|
+
this.submitRetryUsed = false;
|
|
331
|
+
this.submitRetryPromptSnippet = '';
|
|
332
|
+
this.finishRetryCount = 0;
|
|
333
|
+
this.currentTurnScope = null;
|
|
334
|
+
this.activeModal = null;
|
|
335
|
+
this.setStatus('idle', reason);
|
|
336
|
+
this.recordTrace('parsed_idle_response_cleared', {
|
|
337
|
+
reason,
|
|
338
|
+
parsedStatus: parsedRawStatus,
|
|
339
|
+
parsedMessageCount: Array.isArray(parsedStatus?.messages) ? parsedStatus.messages.length : 0,
|
|
340
|
+
});
|
|
341
|
+
this.callbacks.onTurnCompleted();
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
clearAllTimers(): void {
|
|
346
|
+
if (this.settleTimer) { clearTimeout(this.settleTimer); this.settleTimer = null; }
|
|
347
|
+
if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
|
|
348
|
+
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
349
|
+
if (this.finishRetryTimer) { clearTimeout(this.finishRetryTimer); this.finishRetryTimer = null; }
|
|
350
|
+
if (this.pendingScriptStatusTimer) { clearTimeout(this.pendingScriptStatusTimer); this.pendingScriptStatusTimer = null; }
|
|
351
|
+
if (this.providerErrorRetryTimer) { clearTimeout(this.providerErrorRetryTimer); this.providerErrorRetryTimer = null; }
|
|
352
|
+
if (this.pendingIdleFinishTimer) { clearTimeout(this.pendingIdleFinishTimer); this.pendingIdleFinishTimer = null; this.pendingIdleFinishAt = 0; }
|
|
353
|
+
this.providerErrorRetryKey = '';
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
resetActiveTurnState(): void {
|
|
357
|
+
this.clearAllTimers();
|
|
358
|
+
this.isWaitingForResponse = false;
|
|
359
|
+
this.responseSettleIgnoreUntil = 0;
|
|
360
|
+
this.submitRetryUsed = false;
|
|
361
|
+
this.submitRetryPromptSnippet = '';
|
|
362
|
+
this.finishRetryCount = 0;
|
|
363
|
+
this.currentTurnScope = null;
|
|
364
|
+
this.activeModal = null;
|
|
365
|
+
this.pendingScriptStatus = null;
|
|
366
|
+
this.pendingScriptStatusSince = 0;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
clearIdleFinishCandidate(reason: string): void {
|
|
370
|
+
if (!this.idleFinishCandidate) return;
|
|
371
|
+
this.recordTrace('idle_candidate_reset', { reason, candidate: this.idleFinishCandidate });
|
|
372
|
+
this.idleFinishCandidate = null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
hasActionableApproval(startupModal?: { message: string; buttons: string[] } | null): boolean {
|
|
376
|
+
return !!(startupModal ?? this.activeModal);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
getTraceEntries(): CliTraceEntry[] { return this.traceEntries; }
|
|
380
|
+
getStatusHistory(): { status: string; at: number; trigger?: string }[] { return this.statusHistory; }
|
|
381
|
+
getTraceSessionId(): string { return this.traceSessionId; }
|
|
382
|
+
|
|
383
|
+
/** Record a trace entry from the transport layer (e.g. output events in debug mode). */
|
|
384
|
+
recordExternalTrace(type: string, payload: Record<string, any> = {}): void {
|
|
385
|
+
this.recordTrace(type, payload);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ─── Script dispatch (builds inputs from snapshot) ──────────────────────
|
|
389
|
+
|
|
390
|
+
runDetectStatus(snap: CliBufferSnapshot): string | null {
|
|
391
|
+
const tail = snap.recentOutputBuffer.slice(-500);
|
|
392
|
+
return this.runner.detectStatus({
|
|
393
|
+
tail,
|
|
394
|
+
screenText: snap.screenText,
|
|
395
|
+
rawBuffer: snap.accumulatedRawBuffer,
|
|
396
|
+
isWaitingForResponse: snap.isWaitingForResponse,
|
|
397
|
+
screen: buildCliScreenSnapshot(snap.screenText),
|
|
398
|
+
tailScreen: buildCliScreenSnapshot(tail),
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
runParseApproval(snap: CliBufferSnapshot): { message: string; buttons: string[] } | null {
|
|
403
|
+
const tail = snap.recentOutputBuffer.slice(-500);
|
|
404
|
+
const buffer = snap.screenText || snap.accumulatedBuffer;
|
|
405
|
+
return this.runner.parseApproval({
|
|
406
|
+
buffer,
|
|
407
|
+
screenText: snap.screenText,
|
|
408
|
+
rawBuffer: snap.accumulatedRawBuffer,
|
|
409
|
+
tail,
|
|
410
|
+
screen: buildCliScreenSnapshot(snap.screenText),
|
|
411
|
+
bufferScreen: buildCliScreenSnapshot(buffer),
|
|
412
|
+
tailScreen: buildCliScreenSnapshot(tail),
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
runParseSession(snap: CliBufferSnapshot): ParsedSession | null {
|
|
417
|
+
// Allow transport to override session parsing (enables test mocking)
|
|
418
|
+
if (typeof this.transport.runParseSession === 'function') {
|
|
419
|
+
const session = this.transport.runParseSession();
|
|
420
|
+
if (session && typeof session === 'object') {
|
|
421
|
+
this.callbacks.onApplyParsedSession(session);
|
|
422
|
+
}
|
|
423
|
+
return session;
|
|
424
|
+
}
|
|
425
|
+
const tail = snap.recentOutputBuffer.slice(-500);
|
|
426
|
+
const input = buildCliParseInput({
|
|
427
|
+
accumulatedBuffer: snap.accumulatedBuffer,
|
|
428
|
+
accumulatedRawBuffer: snap.accumulatedRawBuffer,
|
|
429
|
+
recentOutputBuffer: snap.recentOutputBuffer,
|
|
430
|
+
terminalScreenText: snap.parseScreenText,
|
|
431
|
+
workingDir: snap.workingDir,
|
|
432
|
+
providerSessionId: snap.providerSessionId || undefined,
|
|
433
|
+
historySessionId: snap.providerSessionId || undefined,
|
|
434
|
+
baseMessages: [],
|
|
435
|
+
partialResponse: snap.responseBuffer,
|
|
436
|
+
isWaitingForResponse: snap.isWaitingForResponse,
|
|
437
|
+
scope: snap.currentTurnScope,
|
|
438
|
+
runtimeSettings: snap.runtimeSettings,
|
|
439
|
+
});
|
|
440
|
+
const session = this.runner.parseSession({
|
|
441
|
+
...input,
|
|
442
|
+
tail,
|
|
443
|
+
tailScreen: buildCliScreenSnapshot(tail),
|
|
444
|
+
});
|
|
445
|
+
if (session && typeof session === 'object') {
|
|
446
|
+
this.callbacks.onApplyParsedSession(session);
|
|
447
|
+
}
|
|
448
|
+
return session;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ─── Core evaluation loop ───────────────────────────────────────────────
|
|
452
|
+
|
|
453
|
+
evaluateSettled(snap: CliBufferSnapshot): void {
|
|
454
|
+
const now = Date.now();
|
|
455
|
+
if (this.submitPendingUntil > now || this.responseSettleIgnoreUntil > now) {
|
|
456
|
+
const delayTime = Math.max(this.submitPendingUntil - now, this.responseSettleIgnoreUntil - now) + 50;
|
|
457
|
+
if (this.settleTimer) clearTimeout(this.settleTimer);
|
|
458
|
+
this.settleTimer = setTimeout(() => {
|
|
459
|
+
this.settleTimer = null;
|
|
460
|
+
this.evaluateSettled(this.transport.getSnapshot());
|
|
461
|
+
}, delayTime);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (!this.isWaitingForResponse && !this.currentTurnScope && !this.activeModal && !this.runner.parseErrorMessage) {
|
|
466
|
+
const tail = snap.recentOutputBuffer;
|
|
467
|
+
const modal = this.runParseApproval(snap);
|
|
468
|
+
const lightweightStatus = this.runner.hasDetectStatus() ? this.runDetectStatus(snap) : null;
|
|
469
|
+
if (!modal && lightweightStatus === 'idle' && this.currentStatus === 'idle') return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const session = this.runParseSession(snap);
|
|
473
|
+
if (!session) return;
|
|
474
|
+
|
|
475
|
+
const { status, messages } = session;
|
|
476
|
+
const modal = (session as any).activeModal ?? session.modal ?? null;
|
|
477
|
+
const parsedStatus = (session as any).parsedStatus ?? null;
|
|
478
|
+
const parsedMessages = normalizeCliParsedMessages(messages, {
|
|
479
|
+
scope: null,
|
|
480
|
+
lastOutputAt: snap.lastOutputAt,
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
if (this.maybeCommitVisibleIdleTranscript(session, parsedMessages, snap)) return;
|
|
484
|
+
|
|
485
|
+
const lastParsedAssistant = [...parsedMessages].reverse().find((m) => m.role === 'assistant');
|
|
486
|
+
|
|
487
|
+
if (
|
|
488
|
+
this.currentTurnScope
|
|
489
|
+
&& !lastParsedAssistant
|
|
490
|
+
&& !this.submitRetryUsed
|
|
491
|
+
&& this.transport.isAlive()
|
|
492
|
+
&& !this.hasActionableApproval()
|
|
493
|
+
&& promptLikelyVisible(snap.screenText, normalizePromptText(this.submitRetryPromptSnippet || this.currentTurnScope?.prompt || ''))
|
|
494
|
+
&& !this.hasMeaningfulResponseBuffer(snap, normalizePromptText(this.submitRetryPromptSnippet || this.currentTurnScope?.prompt || ''))
|
|
495
|
+
) {
|
|
496
|
+
this.submitRetryUsed = true;
|
|
497
|
+
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
498
|
+
LOG.info('CLI', `[${this.provider.type}] Retrying submit key from settled parser (no assistant yet)`);
|
|
499
|
+
this.transport.writeRaw('\r');
|
|
500
|
+
if (this.settleTimer) clearTimeout(this.settleTimer);
|
|
501
|
+
this.settleTimer = setTimeout(() => {
|
|
502
|
+
this.settleTimer = null;
|
|
503
|
+
this.evaluateSettled(this.transport.getSnapshot());
|
|
504
|
+
}, this.timeouts.outputSettle + 150);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (!status) return;
|
|
509
|
+
|
|
510
|
+
const prevStatus = this.currentStatus;
|
|
511
|
+
const ctx: SettledEvalContext = { now, modal, status, parsedMessages, lastParsedAssistant, parsedStatus: parsedStatus || null, prevStatus };
|
|
512
|
+
|
|
513
|
+
if (!this.applyPendingScriptStatusDebounce(ctx)) return;
|
|
514
|
+
|
|
515
|
+
const recentInteractiveActivity = this.hasRecentInteractiveActivity(snap, now);
|
|
516
|
+
LOG.debug(
|
|
517
|
+
'CLI',
|
|
518
|
+
`[${this.provider.type}] settled diagnostics prompt=${JSON.stringify(this.currentTurnScope?.prompt || '').slice(0, 140)} status=${String(status || '')} parsedStatus=${String(parsedStatus || '')} parsedMsgCount=${parsedMessages.length} lastParsedAssistant=${JSON.stringify((lastParsedAssistant?.content || '').slice(0, 120)).slice(0, 160)} responseBuffer=${JSON.stringify((snap.responseBuffer || '').slice(0, 160)).slice(0, 220)}`
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
// recent_activity_hold protects an in-flight user turn from a false
|
|
522
|
+
// idle blip. It must NOT fire during startup — when the adapter has
|
|
523
|
+
// no currentTurnScope, there is no user turn to protect; the recent
|
|
524
|
+
// activity is just the CLI painting its welcome screen. Firing here
|
|
525
|
+
// produced the startup status flip the user reported
|
|
526
|
+
// (generating → idle → generating → idle within the first few seconds
|
|
527
|
+
// of claude-cli launch).
|
|
528
|
+
const shouldHoldGenerating = status === 'idle'
|
|
529
|
+
&& this.isWaitingForResponse
|
|
530
|
+
&& !!this.currentTurnScope
|
|
531
|
+
&& !modal
|
|
532
|
+
&& recentInteractiveActivity
|
|
533
|
+
&& !(parsedStatus === 'idle' && !!lastParsedAssistant);
|
|
534
|
+
|
|
535
|
+
if (shouldHoldGenerating) { this.applyHoldGenerating(ctx); return; }
|
|
536
|
+
if (status === 'error') {
|
|
537
|
+
if (this.maybeScheduleProviderErrorRetry(ctx, session, snap)) return;
|
|
538
|
+
this.applyError(ctx, session);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (status === 'waiting_approval') { this.applyWaitingApproval(ctx); return; }
|
|
542
|
+
if (status === 'generating') { this.applyGenerating(ctx); return; }
|
|
543
|
+
if (status === 'idle') { this.applyIdle(ctx, snap, now); }
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ─── State transitions ──────────────────────────────────────────────────
|
|
547
|
+
|
|
548
|
+
private applyPendingScriptStatusDebounce(ctx: SettledEvalContext): boolean {
|
|
549
|
+
const { now, status, prevStatus } = ctx;
|
|
550
|
+
const shouldDebounce = prevStatus === 'idle' && !this.isWaitingForResponse
|
|
551
|
+
&& !this.currentTurnScope && (status === 'generating' || status === 'waiting_approval');
|
|
552
|
+
|
|
553
|
+
if (!shouldDebounce) {
|
|
554
|
+
this.pendingScriptStatus = null;
|
|
555
|
+
this.pendingScriptStatusSince = 0;
|
|
556
|
+
if (this.pendingScriptStatusTimer) { clearTimeout(this.pendingScriptStatusTimer); this.pendingScriptStatusTimer = null; }
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const armPending = (delayMs: number) => {
|
|
561
|
+
if (this.pendingScriptStatusTimer) clearTimeout(this.pendingScriptStatusTimer);
|
|
562
|
+
this.pendingScriptStatusTimer = setTimeout(() => {
|
|
563
|
+
this.pendingScriptStatusTimer = null;
|
|
564
|
+
this.evaluateSettled(this.transport.getSnapshot());
|
|
565
|
+
}, delayMs);
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
if (this.pendingScriptStatus !== status) {
|
|
569
|
+
this.pendingScriptStatus = status as 'generating' | 'waiting_approval';
|
|
570
|
+
this.pendingScriptStatusSince = now;
|
|
571
|
+
armPending(SCRIPT_STATUS_DEBOUNCE_MS);
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
const elapsed = now - this.pendingScriptStatusSince;
|
|
575
|
+
if (elapsed < SCRIPT_STATUS_DEBOUNCE_MS) { armPending(SCRIPT_STATUS_DEBOUNCE_MS - elapsed); return false; }
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
private applyHoldGenerating(ctx: SettledEvalContext): void {
|
|
580
|
+
this.clearIdleFinishCandidate('hold_generating_recent_activity');
|
|
581
|
+
this.setStatus('generating', 'recent_activity_hold');
|
|
582
|
+
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
583
|
+
this.idleTimeout = setTimeout(() => {
|
|
584
|
+
if (this.isWaitingForResponse && !this.hasActionableApproval()) {
|
|
585
|
+
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
586
|
+
this.finishResponse();
|
|
587
|
+
}
|
|
588
|
+
}, this.timeouts.generatingIdle);
|
|
589
|
+
this.callbacks.onStatusChange();
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
private applyWaitingApproval(ctx: SettledEvalContext): void {
|
|
593
|
+
const { modal } = ctx;
|
|
594
|
+
this.clearIdleFinishCandidate('waiting_approval');
|
|
595
|
+
const inCooldown = this.lastApprovalResolvedAt
|
|
596
|
+
&& (Date.now() - this.lastApprovalResolvedAt) < this.timeouts.approvalCooldown;
|
|
597
|
+
if (inCooldown && !modal) {
|
|
598
|
+
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
599
|
+
this.activeModal = null;
|
|
600
|
+
const reason = inCooldown ? 'approval_cooldown_non_actionable' : 'approval_prompt_gone_non_actionable';
|
|
601
|
+
if (this.isWaitingForResponse) {
|
|
602
|
+
this.setStatus('idle', reason);
|
|
603
|
+
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
604
|
+
this.idleTimeout = setTimeout(() => {
|
|
605
|
+
if (this.isWaitingForResponse && !this.hasActionableApproval()) {
|
|
606
|
+
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
607
|
+
this.finishResponse();
|
|
608
|
+
}
|
|
609
|
+
}, this.timeouts.generatingIdle);
|
|
610
|
+
} else {
|
|
611
|
+
this.setStatus('idle', reason);
|
|
612
|
+
}
|
|
613
|
+
this.callbacks.onStatusChange();
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
if (!inCooldown) {
|
|
617
|
+
if (!modal) {
|
|
618
|
+
LOG.warn('CLI', `[${this.provider.type}] detectStatus=waiting_approval but parseApproval returned null; ignoring`);
|
|
619
|
+
// (fix) If we previously surfaced waiting_approval but the
|
|
620
|
+
// modal extraction is now failing, do NOT keep the status
|
|
621
|
+
// pinned to waiting_approval forever — the dashboard would
|
|
622
|
+
// show a "waiting" badge with no buttons (activeModal=null)
|
|
623
|
+
// and the user perceives the agent as stuck. Drop the modal
|
|
624
|
+
// and fall back to generating so the rest of the run can
|
|
625
|
+
// settle normally; a future evaluate with a real modal will
|
|
626
|
+
// re-enter waiting_approval cleanly.
|
|
627
|
+
if (this.currentStatus === 'waiting_approval') {
|
|
628
|
+
this.activeModal = null;
|
|
629
|
+
this.setStatus('generating', 'approval_lost_modal');
|
|
630
|
+
this.callbacks.onStatusChange();
|
|
631
|
+
}
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
this.isWaitingForResponse = true;
|
|
635
|
+
this.setStatus('waiting_approval', 'script_detect');
|
|
636
|
+
// (fix) Don't overwrite an already-captured modal with a fresh
|
|
637
|
+
// re-parse on every evaluate — Claude TUI redraws option labels
|
|
638
|
+
// partially between paints (e.g. "Yes, and don't ask again for ..."
|
|
639
|
+
// is wider than the row and ships with a different trailing
|
|
640
|
+
// string each paint), which made the dashboard flap the modal
|
|
641
|
+
// signature continuously. Keep the first stable modal whose
|
|
642
|
+
// button count matches the latest parse; only swap when the
|
|
643
|
+
// shape clearly changed (different number of buttons → different
|
|
644
|
+
// approval).
|
|
645
|
+
const prev = this.activeModal;
|
|
646
|
+
const prevBtnCount = Array.isArray(prev?.buttons) ? prev!.buttons.length : 0;
|
|
647
|
+
const nextBtnCount = Array.isArray(modal.buttons) ? modal.buttons.length : 0;
|
|
648
|
+
if (!prev || prevBtnCount !== nextBtnCount) {
|
|
649
|
+
this.activeModal = modal;
|
|
650
|
+
this.callbacks.onStatusChange();
|
|
651
|
+
}
|
|
652
|
+
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
653
|
+
this.armApprovalExitTimeout();
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private applyGenerating(ctx: SettledEvalContext): void {
|
|
658
|
+
const { modal, parsedMessages, lastParsedAssistant, parsedStatus, prevStatus } = ctx;
|
|
659
|
+
this.clearIdleFinishCandidate('generating');
|
|
660
|
+
// Cancel any pending grace-window idle transition. We have fresh
|
|
661
|
+
// evidence the provider is still generating; the previous
|
|
662
|
+
// finishResponse() was a paint blip, not a real completion.
|
|
663
|
+
this.cancelPendingIdleFinish('generating_signal_returned');
|
|
664
|
+
const snap = this.transport.getSnapshot();
|
|
665
|
+
const effectiveScreenText = snap.screenText || snap.accumulatedBuffer;
|
|
666
|
+
const noActiveTurn = !this.currentTurnScope;
|
|
667
|
+
const looksIdleChrome = /(^|\n)\s*[❯›>]\s*(?:\n|$)/m.test(effectiveScreenText);
|
|
668
|
+
const parsedShowsLiveProgress = parsedStatus === 'generating' && !!lastParsedAssistant;
|
|
669
|
+
if (prevStatus === 'idle' && !this.isWaitingForResponse && noActiveTurn && !modal && looksIdleChrome && !parsedShowsLiveProgress) return;
|
|
670
|
+
if (prevStatus === 'waiting_approval') {
|
|
671
|
+
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
672
|
+
this.activeModal = null;
|
|
673
|
+
this.lastApprovalResolvedAt = Date.now();
|
|
674
|
+
}
|
|
675
|
+
if (!this.isWaitingForResponse) { this.isWaitingForResponse = true; }
|
|
676
|
+
this.setStatus('generating', 'script_detect');
|
|
677
|
+
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
678
|
+
this.idleTimeout = setTimeout(() => {
|
|
679
|
+
if (this.isWaitingForResponse) {
|
|
680
|
+
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
681
|
+
this.finishResponse();
|
|
682
|
+
}
|
|
683
|
+
}, this.timeouts.generatingIdle);
|
|
684
|
+
this.callbacks.onStatusChange();
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
private applyError(ctx: SettledEvalContext, session: ParsedSession): void {
|
|
688
|
+
this.clearIdleFinishCandidate('provider_error');
|
|
689
|
+
this.clearAllTimers();
|
|
690
|
+
this.isWaitingForResponse = false;
|
|
691
|
+
this.responseSettleIgnoreUntil = 0;
|
|
692
|
+
this.submitRetryUsed = false;
|
|
693
|
+
this.submitRetryPromptSnippet = '';
|
|
694
|
+
this.finishRetryCount = 0;
|
|
695
|
+
this.currentTurnScope = null;
|
|
696
|
+
this.activeModal = null;
|
|
697
|
+
this.providerErrorMessage = typeof session.errorMessage === 'string' && session.errorMessage.trim()
|
|
698
|
+
? session.errorMessage.trim() : 'Provider reported an error';
|
|
699
|
+
this.providerErrorReason = typeof session.errorReason === 'string' && session.errorReason.trim()
|
|
700
|
+
? session.errorReason.trim() : 'provider_error';
|
|
701
|
+
this.setStatus('error', this.providerErrorReason);
|
|
702
|
+
this.callbacks.onStatusChange();
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
private maybeScheduleProviderErrorRetry(ctx: SettledEvalContext, session: ParsedSession, snap: CliBufferSnapshot): boolean {
|
|
706
|
+
const retryPrompt = typeof (session as any).retryPrompt === 'string' ? String((session as any).retryPrompt).trim() : '';
|
|
707
|
+
const retryDelayMs = typeof (session as any).retryDelayMs === 'number' ? Number((session as any).retryDelayMs) : NaN;
|
|
708
|
+
if (!retryPrompt || !Number.isFinite(retryDelayMs) || retryDelayMs < 0 || !this.transport.isAlive()) return false;
|
|
709
|
+
|
|
710
|
+
const retryAttempt = typeof (session as any).retryAttempt === 'number' ? Number((session as any).retryAttempt) : 0;
|
|
711
|
+
const errorReason = typeof session.errorReason === 'string' && session.errorReason.trim() ? session.errorReason.trim() : 'provider_error';
|
|
712
|
+
const retryKey = `${errorReason}:${retryAttempt}:${retryPrompt}`;
|
|
713
|
+
if (this.providerErrorRetryTimer && this.providerErrorRetryKey === retryKey) return true;
|
|
714
|
+
|
|
715
|
+
if (this.providerErrorRetryTimer) clearTimeout(this.providerErrorRetryTimer);
|
|
716
|
+
this.providerErrorRetryKey = retryKey;
|
|
717
|
+
this.clearIdleFinishCandidate('provider_error_retry');
|
|
718
|
+
if (this.idleTimeout) { clearTimeout(this.idleTimeout); this.idleTimeout = null; }
|
|
719
|
+
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
720
|
+
this.providerErrorMessage = typeof session.errorMessage === 'string' && session.errorMessage.trim()
|
|
721
|
+
? session.errorMessage.trim() : 'Provider reported an error';
|
|
722
|
+
this.providerErrorReason = errorReason;
|
|
723
|
+
this.activeModal = null;
|
|
724
|
+
this.responseSettleIgnoreUntil = Date.now() + retryDelayMs + this.timeouts.outputSettle + 400;
|
|
725
|
+
this.setStatus('generating', 'provider_error_retry_scheduled');
|
|
726
|
+
this.callbacks.onStatusChange();
|
|
727
|
+
|
|
728
|
+
this.providerErrorRetryTimer = setTimeout(() => {
|
|
729
|
+
this.providerErrorRetryTimer = null;
|
|
730
|
+
this.providerErrorRetryKey = '';
|
|
731
|
+
if (!this.transport.isAlive()) return;
|
|
732
|
+
this.responseSettleIgnoreUntil = Date.now() + this.timeouts.outputSettle + 400;
|
|
733
|
+
this.submitRetryUsed = false;
|
|
734
|
+
this.transport.writeRaw(`${retryPrompt}\r`);
|
|
735
|
+
if (this.settleTimer) clearTimeout(this.settleTimer);
|
|
736
|
+
this.settleTimer = setTimeout(() => {
|
|
737
|
+
this.settleTimer = null;
|
|
738
|
+
this.evaluateSettled(this.transport.getSnapshot());
|
|
739
|
+
}, this.timeouts.outputSettle + 150);
|
|
740
|
+
}, retryDelayMs);
|
|
741
|
+
return true;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
private applyIdle(ctx: SettledEvalContext, snap: CliBufferSnapshot, now: number): void {
|
|
745
|
+
const { modal, lastParsedAssistant, prevStatus } = ctx;
|
|
746
|
+
if (prevStatus === 'waiting_approval') {
|
|
747
|
+
if (this.approvalExitTimeout) { clearTimeout(this.approvalExitTimeout); this.approvalExitTimeout = null; }
|
|
748
|
+
this.activeModal = null;
|
|
749
|
+
this.lastApprovalResolvedAt = Date.now();
|
|
750
|
+
this.setStatus('idle', 'approval_prompt_gone_script_idle');
|
|
751
|
+
}
|
|
752
|
+
if (!this.isWaitingForResponse) {
|
|
753
|
+
if (prevStatus !== 'idle') {
|
|
754
|
+
this.clearIdleFinishCandidate('idle_without_response');
|
|
755
|
+
this.setStatus('idle', 'script_detect');
|
|
756
|
+
this.callbacks.onStatusChange();
|
|
757
|
+
}
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
const quietForMs = snap.lastNonEmptyOutputAt ? (now - snap.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
|
|
761
|
+
const screenStableMs = snap.lastScreenChangeAt ? (now - snap.lastScreenChangeAt) : 0;
|
|
762
|
+
const hasAssistantTurn = !!lastParsedAssistant;
|
|
763
|
+
const assistantLength = (lastParsedAssistant as any)?.content?.length || 0;
|
|
764
|
+
const idleFinishConfirmMs = this.timeouts.idleFinishConfirm;
|
|
765
|
+
const idleQuietThresholdMs = Math.max(idleFinishConfirmMs, this.timeouts.outputSettle);
|
|
766
|
+
const idleReady = !modal && hasAssistantTurn && quietForMs >= idleQuietThresholdMs && screenStableMs >= idleFinishConfirmMs;
|
|
767
|
+
const candidate = this.idleFinishCandidate;
|
|
768
|
+
const candidateQuiet = !!candidate && candidate.responseEpoch === this.responseEpoch
|
|
769
|
+
&& candidate.lastOutputAt === snap.lastOutputAt
|
|
770
|
+
&& candidate.lastScreenChangeAt === snap.lastScreenChangeAt
|
|
771
|
+
&& assistantLength >= candidate.assistantLength
|
|
772
|
+
&& (now - candidate.armedAt) >= idleFinishConfirmMs;
|
|
773
|
+
|
|
774
|
+
if (idleReady && candidateQuiet) {
|
|
775
|
+
this.clearIdleFinishCandidate('finish_response');
|
|
776
|
+
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
777
|
+
this.finishResponse();
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (idleReady) {
|
|
782
|
+
if (!candidate) { this.armIdleFinishCandidate(snap, assistantLength); return; }
|
|
783
|
+
} else {
|
|
784
|
+
this.clearIdleFinishCandidate('idle_not_ready');
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
788
|
+
this.idleTimeout = setTimeout(() => {
|
|
789
|
+
if (this.isWaitingForResponse && !this.hasActionableApproval()) {
|
|
790
|
+
if (this.shouldDeferIdleTimeoutFinish()) return;
|
|
791
|
+
const parsed = this.runParseSession(this.transport.getSnapshot());
|
|
792
|
+
if (this.shouldDeferFinishForTranscript(parsed)) {
|
|
793
|
+
this.rescheduleTranscriptFinishCheck('transcript_idle_timeout_not_final');
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
this.clearIdleFinishCandidate('idle_timeout_finish');
|
|
797
|
+
this.finishResponse();
|
|
798
|
+
}
|
|
799
|
+
}, this.timeouts.idleFinish);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
finishResponse(): void {
|
|
803
|
+
if (this.submitPendingUntil > Date.now()) return;
|
|
804
|
+
if (this.responseSettleIgnoreUntil > Date.now()) return;
|
|
805
|
+
const snap = this.transport.getSnapshot();
|
|
806
|
+
const parsedBeforeFinish = this.runParseSession(snap);
|
|
807
|
+
if (this.shouldDeferFinishForTranscript(parsedBeforeFinish)) {
|
|
808
|
+
this.rescheduleTranscriptFinishCheck('transcript_finish_not_final');
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
this.clearIdleFinishCandidate('finish_response_enter');
|
|
812
|
+
const commitResult = this.commitCurrentTranscript(snap);
|
|
813
|
+
if (this.shouldRetryFinishResponse(snap, commitResult)) {
|
|
814
|
+
this.finishRetryCount += 1;
|
|
815
|
+
if (this.finishRetryTimer) clearTimeout(this.finishRetryTimer);
|
|
816
|
+
this.finishRetryTimer = setTimeout(() => {
|
|
817
|
+
this.finishRetryTimer = null;
|
|
818
|
+
if (this.isWaitingForResponse && !this.hasActionableApproval()) this.finishResponse();
|
|
819
|
+
}, FINISH_RETRY_DELAY_MS);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
this.resetActiveTurnState();
|
|
823
|
+
this.callbacks.onTurnCompleted();
|
|
824
|
+
// Defer the actual `generating → idle` transition by a short grace
|
|
825
|
+
// window. If applyGenerating fires again before the grace expires —
|
|
826
|
+
// antigravity's tool-result paint blips do this — cancelPendingIdle
|
|
827
|
+
// Finish() drops the pending transition and we stay generating.
|
|
828
|
+
this.scheduleIdleFinish('response_finished');
|
|
829
|
+
this.transport.flushOutboundQueue();
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
private scheduleIdleFinish(reason: string): void {
|
|
833
|
+
// If we are already deferring, replace the schedule so the most
|
|
834
|
+
// recent finishResponse "wins". Reasons accumulate via the trigger.
|
|
835
|
+
if (this.pendingIdleFinishTimer) clearTimeout(this.pendingIdleFinishTimer);
|
|
836
|
+
this.pendingIdleFinishAt = Date.now() + IDLE_CONFIRMATION_GRACE_MS;
|
|
837
|
+
this.pendingIdleFinishTimer = setTimeout(() => {
|
|
838
|
+
this.pendingIdleFinishTimer = null;
|
|
839
|
+
this.pendingIdleFinishAt = 0;
|
|
840
|
+
// If a new user turn started during the grace window, we owe
|
|
841
|
+
// generating semantics to that turn — do not retroactively idle.
|
|
842
|
+
if (this.isWaitingForResponse) return;
|
|
843
|
+
// The timer firing without a cancelPendingIdleFinish call means
|
|
844
|
+
// no fresh applyGenerating happened during the grace window;
|
|
845
|
+
// the previous fake-blip is over. Commit the idle.
|
|
846
|
+
//
|
|
847
|
+
// Note: the previous "currentStatus === 'generating' → return"
|
|
848
|
+
// guard caused stuck-generating sessions — finishResponse leaves
|
|
849
|
+
// currentStatus='generating' on purpose (so the dashboard keeps
|
|
850
|
+
// the spinner during the 2s grace), and then the timer fired
|
|
851
|
+
// without cancellation but refused to commit because the very
|
|
852
|
+
// status we are about to transition out of was still set.
|
|
853
|
+
// Re-checking currentStatus there meant idle was unreachable.
|
|
854
|
+
this.setStatus('idle', reason);
|
|
855
|
+
this.callbacks.onStatusChange();
|
|
856
|
+
}, IDLE_CONFIRMATION_GRACE_MS);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
private cancelPendingIdleFinish(reason: string): void {
|
|
860
|
+
if (!this.pendingIdleFinishTimer) return;
|
|
861
|
+
clearTimeout(this.pendingIdleFinishTimer);
|
|
862
|
+
this.pendingIdleFinishTimer = null;
|
|
863
|
+
this.pendingIdleFinishAt = 0;
|
|
864
|
+
this.recordTrace('idle_finish_cancelled', { trigger: reason });
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// ─── Helpers ────────────────────────────────────────────────────────────
|
|
868
|
+
|
|
869
|
+
private armApprovalExitTimeout(): void {
|
|
870
|
+
if (this.approvalExitTimeout) clearTimeout(this.approvalExitTimeout);
|
|
871
|
+
this.approvalExitTimeout = setTimeout(() => {
|
|
872
|
+
if (!this.hasActionableApproval()) return;
|
|
873
|
+
const snap = this.transport.getSnapshot();
|
|
874
|
+
const modal = typeof this.transport.runParseApproval === 'function'
|
|
875
|
+
? this.transport.runParseApproval(snap.recentOutputBuffer.slice(-500))
|
|
876
|
+
: this.runParseApproval(snap);
|
|
877
|
+
const detectStatus = typeof this.transport.runDetectStatus === 'function'
|
|
878
|
+
? this.transport.runDetectStatus(snap.recentOutputBuffer)
|
|
879
|
+
: this.runDetectStatus(snap);
|
|
880
|
+
const stillWaiting = detectStatus === 'waiting_approval' || !!modal;
|
|
881
|
+
if (stillWaiting) {
|
|
882
|
+
if (!modal) {
|
|
883
|
+
LOG.warn('CLI', `[${this.provider.type}] approval timeout: no actionable modal; keeping fail-closed`);
|
|
884
|
+
this.activeModal = null;
|
|
885
|
+
this.callbacks.onStatusChange();
|
|
886
|
+
this.armApprovalExitTimeout();
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
this.activeModal = modal;
|
|
890
|
+
this.callbacks.onStatusChange();
|
|
891
|
+
this.armApprovalExitTimeout();
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
LOG.warn('CLI', `[${this.provider.type}] Approval timeout — auto-clearing`);
|
|
895
|
+
this.activeModal = null;
|
|
896
|
+
this.lastApprovalResolvedAt = Date.now();
|
|
897
|
+
this.setStatus('idle', 'approval_timeout');
|
|
898
|
+
this.callbacks.onStatusChange();
|
|
899
|
+
}, APPROVAL_EXIT_TIMEOUT_MS);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
private armIdleFinishCandidate(snap: CliBufferSnapshot, assistantLength: number): void {
|
|
903
|
+
const now = Date.now();
|
|
904
|
+
this.idleFinishCandidate = {
|
|
905
|
+
armedAt: now,
|
|
906
|
+
lastOutputAt: snap.lastOutputAt,
|
|
907
|
+
lastScreenChangeAt: snap.lastScreenChangeAt,
|
|
908
|
+
responseEpoch: this.responseEpoch,
|
|
909
|
+
assistantLength,
|
|
910
|
+
};
|
|
911
|
+
if (this.settleTimer) clearTimeout(this.settleTimer);
|
|
912
|
+
this.settleTimer = setTimeout(() => {
|
|
913
|
+
this.settleTimer = null;
|
|
914
|
+
this.evaluateSettled(this.transport.getSnapshot());
|
|
915
|
+
}, this.timeouts.idleFinishConfirm);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
private shouldDeferIdleTimeoutFinish(): boolean {
|
|
919
|
+
if (!this.isWaitingForResponse || this.hasActionableApproval()) return false;
|
|
920
|
+
const snap = this.transport.getSnapshot();
|
|
921
|
+
const detectFn = typeof this.transport.runDetectStatus === 'function'
|
|
922
|
+
? () => this.transport.runDetectStatus!(snap.recentOutputBuffer)
|
|
923
|
+
: () => this.runDetectStatus(snap);
|
|
924
|
+
const latestStatus = detectFn() || this.currentStatus;
|
|
925
|
+
if (latestStatus === 'generating') {
|
|
926
|
+
this.evaluateSettled(snap);
|
|
927
|
+
return true;
|
|
928
|
+
}
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
private hasRecentInteractiveActivity(snap: CliBufferSnapshot, now: number): boolean {
|
|
933
|
+
const quietForMs = snap.lastNonEmptyOutputAt ? (now - snap.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
|
|
934
|
+
const screenStableMs = snap.lastScreenChangeAt ? (now - snap.lastScreenChangeAt) : Number.MAX_SAFE_INTEGER;
|
|
935
|
+
return quietForMs < this.timeouts.statusActivityHold || screenStableMs < this.timeouts.statusActivityHold;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
private hasMeaningfulResponseBuffer(snap: CliBufferSnapshot, normalizedPromptSnippet: string): boolean {
|
|
939
|
+
const raw = String(snap.responseBuffer || '').trim();
|
|
940
|
+
if (!raw) return false;
|
|
941
|
+
const normalizedPrompt = compactPromptText(normalizedPromptSnippet);
|
|
942
|
+
if (!normalizedPrompt) return true;
|
|
943
|
+
const normalizedBuffer = compactPromptText(raw);
|
|
944
|
+
if (!normalizedBuffer) return false;
|
|
945
|
+
if (normalizedBuffer === normalizedPrompt) return false;
|
|
946
|
+
if (normalizedBuffer.startsWith(normalizedPrompt)) {
|
|
947
|
+
const remainder = normalizedBuffer.slice(normalizedPrompt.length)
|
|
948
|
+
.replace(/[─═\-]+/g, '')
|
|
949
|
+
.replace(/⏵⏵accepteditson\([^)]*\)/gi, '')
|
|
950
|
+
.replace(/esctointerrupt/gi, '')
|
|
951
|
+
.replace(/❯/g, '')
|
|
952
|
+
.replace(/^[\s\-–—:;,.!/?]+/, '')
|
|
953
|
+
.trim();
|
|
954
|
+
return remainder.length > 0;
|
|
955
|
+
}
|
|
956
|
+
return true;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
private shouldDeferFinishForTranscript(parsed: any): boolean {
|
|
960
|
+
// Support both explicit flag and legacy codex-cli type check
|
|
961
|
+
// Also check transport.cliType to support tests that patch adapter.cliType directly
|
|
962
|
+
const effectiveType = this.transport.cliType ?? this.provider.type;
|
|
963
|
+
const requiresFinalAssistant = !!this.provider.requiresFinalAssistantBeforeIdle
|
|
964
|
+
|| effectiveType === 'codex-cli';
|
|
965
|
+
if (!requiresFinalAssistant) return false;
|
|
966
|
+
if (!this.isWaitingForResponse || !this.currentTurnScope || this.hasActionableApproval()) return false;
|
|
967
|
+
const parsedStatus = typeof parsed?.status === 'string' ? parsed.status.trim() : '';
|
|
968
|
+
if (parsedStatus !== 'idle') return true;
|
|
969
|
+
if (parsed?.activeModal || parsed?.modal) return true;
|
|
970
|
+
return !this.parsedStatusHasFinalStandardAssistantMessage(parsed);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
private rescheduleTranscriptFinishCheck(reason: string): void {
|
|
974
|
+
this.clearIdleFinishCandidate(reason);
|
|
975
|
+
this.setStatus('generating', reason);
|
|
976
|
+
if (this.idleTimeout) clearTimeout(this.idleTimeout);
|
|
977
|
+
this.idleTimeout = setTimeout(() => {
|
|
978
|
+
if (!this.isWaitingForResponse || this.hasActionableApproval()) return;
|
|
979
|
+
this.evaluateSettled(this.transport.getSnapshot());
|
|
980
|
+
}, this.timeouts.idleFinishConfirm);
|
|
981
|
+
this.recordTrace('transcript_finish_deferred', { reason });
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
private shouldRetryFinishResponse(snap: CliBufferSnapshot, commitResult: { hasAssistant: boolean; assistantContent: string }): boolean {
|
|
985
|
+
if (!this.currentTurnScope) return false;
|
|
986
|
+
if (this.hasActionableApproval()) return false;
|
|
987
|
+
if (this.finishRetryCount >= MAX_FINISH_RETRIES) return false;
|
|
988
|
+
if (commitResult.hasAssistant && commitResult.assistantContent.trim()) return false;
|
|
989
|
+
if (this.runDetectStatus(snap) !== 'idle') return false;
|
|
990
|
+
const now = Date.now();
|
|
991
|
+
const quietForMs = snap.lastNonEmptyOutputAt ? (now - snap.lastNonEmptyOutputAt) : Number.MAX_SAFE_INTEGER;
|
|
992
|
+
const screenStableMs = snap.lastScreenChangeAt ? (now - snap.lastScreenChangeAt) : 0;
|
|
993
|
+
return quietForMs < 1200 || screenStableMs < 1200 || !commitResult.hasAssistant;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
private commitCurrentTranscript(snap: CliBufferSnapshot): { hasAssistant: boolean; assistantContent: string } {
|
|
997
|
+
const parsed = this.runParseSession(snap);
|
|
998
|
+
if (parsed && Array.isArray(parsed.messages)) {
|
|
999
|
+
const parsedMessages = normalizeCliParsedMessages(parsed.messages, { scope: null, lastOutputAt: snap.lastOutputAt });
|
|
1000
|
+
const lastAssistant = [...parsedMessages].reverse().find((m) => m.role === 'assistant');
|
|
1001
|
+
if (lastAssistant) return { hasAssistant: true, assistantContent: lastAssistant.content || '' };
|
|
1002
|
+
}
|
|
1003
|
+
return { hasAssistant: false, assistantContent: '' };
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
private maybeCommitVisibleIdleTranscript(session: ParsedSession, parsedMessages: CliChatMessage[], snap: CliBufferSnapshot): boolean {
|
|
1007
|
+
if (!this.provider.allowInputDuringGeneration) return false;
|
|
1008
|
+
if (!session || session.status !== 'idle' || !this.isWaitingForResponse || !this.currentTurnScope || this.activeModal || session.modal) return false;
|
|
1009
|
+
const visibleAssistant = [...parsedMessages].reverse().find((m) => m.role === 'assistant' && String(m.content || '').trim());
|
|
1010
|
+
if (!visibleAssistant) return false;
|
|
1011
|
+
this.resetActiveTurnState();
|
|
1012
|
+
this.callbacks.onTurnCompleted();
|
|
1013
|
+
this.setStatus('idle', 'script_idle_commit');
|
|
1014
|
+
this.callbacks.onStatusChange();
|
|
1015
|
+
this.transport.flushOutboundQueue();
|
|
1016
|
+
return true;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
private parsedStatusHasFinalAssistantMessage(parsed: any): boolean {
|
|
1020
|
+
const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
|
|
1021
|
+
const last = [...messages].reverse().find((m: any) => {
|
|
1022
|
+
if (!m || m.role !== 'assistant') return false;
|
|
1023
|
+
return typeof m.content === 'string' && m.content.trim().length > 0;
|
|
1024
|
+
});
|
|
1025
|
+
return !!last;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
private parsedStatusHasFinalStandardAssistantMessage(parsed: any): boolean {
|
|
1029
|
+
const messages = Array.isArray(parsed?.messages) ? parsed.messages : [];
|
|
1030
|
+
const last = [...messages].reverse().find((m: any) => {
|
|
1031
|
+
if (!m || m.role !== 'assistant') return false;
|
|
1032
|
+
if (typeof m.content !== 'string' || !m.content.trim()) return false;
|
|
1033
|
+
const kind = typeof m.kind === 'string' && m.kind.trim() ? m.kind.trim() : 'standard';
|
|
1034
|
+
return kind === 'standard' && m.meta?.streaming !== true;
|
|
1035
|
+
});
|
|
1036
|
+
return !!last;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
private recordTrace(type: string, payload: Record<string, any> = {}): void {
|
|
1040
|
+
const entry: CliTraceEntry = {
|
|
1041
|
+
id: ++this.traceSeq,
|
|
1042
|
+
at: Date.now(),
|
|
1043
|
+
type,
|
|
1044
|
+
status: this.currentStatus,
|
|
1045
|
+
isWaitingForResponse: this.isWaitingForResponse,
|
|
1046
|
+
activeModal: this.activeModal ? { message: this.activeModal.message, buttons: [...this.activeModal.buttons] } : null,
|
|
1047
|
+
payload,
|
|
1048
|
+
};
|
|
1049
|
+
this.traceEntries.push(entry);
|
|
1050
|
+
if (this.traceEntries.length > MAX_TRACE_ENTRIES) {
|
|
1051
|
+
this.traceEntries.splice(0, this.traceEntries.length - MAX_TRACE_ENTRIES);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|