@adhdev/daemon-core 0.9.82-rc.253 → 0.9.82-rc.255
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/index.js +185 -121
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +185 -121
- package/dist/index.mjs.map +1 -1
- package/dist/providers/cli-provider-instance.d.ts +0 -3
- package/dist/providers/spec/fsm-driver.d.ts +1 -0
- package/package.json +1 -1
- package/src/commands/router.ts +55 -1
- package/src/mesh/mesh-events-coordinator.ts +94 -35
- package/src/mesh/mesh-events-pending.ts +14 -6
- package/src/mesh/mesh-work-queue.ts +60 -2
- package/src/providers/cli-provider-instance.ts +49 -101
- package/src/providers/spec/cli-adapter.ts +12 -8
- package/src/providers/spec/fsm-driver.ts +8 -3
|
@@ -73,12 +73,6 @@ type CompletionFinalAssistantEvidence = {
|
|
|
73
73
|
source: 'parsed' | 'external-native' | 'unavailable';
|
|
74
74
|
};
|
|
75
75
|
|
|
76
|
-
type ExternalNativeFinalReconciliation = {
|
|
77
|
-
fingerprint: string;
|
|
78
|
-
finalSummary: string;
|
|
79
|
-
evidence: CompletionFinalAssistantEvidence;
|
|
80
|
-
};
|
|
81
|
-
|
|
82
76
|
type ExternalTranscriptProbe = {
|
|
83
77
|
readAt: number;
|
|
84
78
|
msgCount: number;
|
|
@@ -376,7 +370,6 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
376
370
|
private runtimeMessages: Array<{ key: string; message: ChatMessage }> = [];
|
|
377
371
|
private lastPersistedHistoryMessages: PersistableCliHistoryMessage[] = [];
|
|
378
372
|
private lastAcknowledgedUserInputAt = 0;
|
|
379
|
-
private externalBusyIdleFingerprint = '';
|
|
380
373
|
private lastNativeSourceCanonicalCheckAt = 0;
|
|
381
374
|
private lastNativeSourceCanonicalCacheKey: string | undefined = undefined;
|
|
382
375
|
private cachedSqliteDb: {
|
|
@@ -599,17 +592,11 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
599
592
|
let visibleStatus = parseErrorMessage || parsedStatus?.status === 'error'
|
|
600
593
|
? 'error'
|
|
601
594
|
: (autoApproveActive || autoApproveHoldIdle ? 'generating' : adapterStatus.status);
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
//
|
|
607
|
-
// has not yet emitted a state_changed(idle) event but the parsed transcript
|
|
608
|
-
// already shows a final assistant turn, treat the session as idle so that
|
|
609
|
-
// getState() agrees with what detectStatusTransition already recorded via
|
|
610
|
-
// lastStatus. Without this guard, getState() returns 'generating' even after
|
|
611
|
-
// the instance's lastStatus has flipped to 'idle', causing the dashboard to
|
|
612
|
-
// show a perpetual generating spinner.
|
|
595
|
+
// getState() must agree with the status the FSM-driven detectStatusTransition()
|
|
596
|
+
// already committed to lastStatus. The adapter's own status is authoritative; we do
|
|
597
|
+
// not second-guess it with native-transcript shape. Only reconcile a generating-like
|
|
598
|
+
// read down to idle when our own lastStatus has already flipped idle (avoids a
|
|
599
|
+
// perpetual dashboard spinner during the brief window before the next getStatus()).
|
|
613
600
|
if (isCliGeneratingLikeStatus(visibleStatus) && this.lastStatus === 'idle') {
|
|
614
601
|
visibleStatus = 'idle';
|
|
615
602
|
}
|
|
@@ -885,7 +872,13 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
885
872
|
assertProviderSupportsDeclaredInput(this.provider, input);
|
|
886
873
|
const promptText = buildCliStructuredInputPrompt(input);
|
|
887
874
|
if (promptText) {
|
|
888
|
-
|
|
875
|
+
// force:true bypasses the busy/generating send guard so terminal mesh
|
|
876
|
+
// events (completion/failure/bootstrap) land in a coordinator session that
|
|
877
|
+
// is itself parked in `generating` while awaiting that very event.
|
|
878
|
+
// Without it the message is queued and only flushed on the coordinator's
|
|
879
|
+
// own idle transition — which never happens until it receives the message.
|
|
880
|
+
const force = data?.force === true;
|
|
881
|
+
void this.adapter.sendMessage(promptText, force ? { force: true } : {}).catch((e: any) => {
|
|
889
882
|
LOG.warn('CLI', `[${this.type}] send_message failed: ${e?.message || e}`);
|
|
890
883
|
});
|
|
891
884
|
}
|
|
@@ -934,7 +927,6 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
934
927
|
|
|
935
928
|
const receivedAt = Date.now();
|
|
936
929
|
this.lastAcknowledgedUserInputAt = receivedAt;
|
|
937
|
-
this.externalBusyIdleFingerprint = '';
|
|
938
930
|
const dedupKey = `user_input_ack:${crypto
|
|
939
931
|
.createHash('sha256')
|
|
940
932
|
.update(`${this.instanceId}:${content}:${receivedAt}`)
|
|
@@ -1100,59 +1092,6 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1100
1092
|
return extractFinalSummaryFromMessages(evidence.messages as any);
|
|
1101
1093
|
}
|
|
1102
1094
|
|
|
1103
|
-
private externalNativeFinalFingerprint(evidence: CompletionFinalAssistantEvidence): string {
|
|
1104
|
-
const messages = Array.isArray(evidence.messages) ? evidence.messages : [];
|
|
1105
|
-
const visibleMessages = messages.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
1106
|
-
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
1107
|
-
const content = lastVisible ? flattenContent(lastVisible.content).trim() : '';
|
|
1108
|
-
const receivedAt = lastVisible ? getMessageTime(lastVisible) : 0;
|
|
1109
|
-
const probe = this.lastExternalCompletionProbe;
|
|
1110
|
-
return crypto
|
|
1111
|
-
.createHash('sha256')
|
|
1112
|
-
.update([
|
|
1113
|
-
this.type,
|
|
1114
|
-
this.providerSessionId || '',
|
|
1115
|
-
probe?.sourcePath || '',
|
|
1116
|
-
String(probe?.sourceMtimeMs || 0),
|
|
1117
|
-
String(receivedAt || 0),
|
|
1118
|
-
content.slice(-500),
|
|
1119
|
-
].join('\0'))
|
|
1120
|
-
.digest('hex')
|
|
1121
|
-
.slice(0, 24);
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
private getExternalNativeFinalReconciliation(parsedMessages: unknown, adapterStatus: any): ExternalNativeFinalReconciliation | null {
|
|
1125
|
-
const rawStatus = typeof adapterStatus?.status === 'string' ? adapterStatus.status.trim() : '';
|
|
1126
|
-
if (!isCliGeneratingLikeStatus(rawStatus)) return null;
|
|
1127
|
-
if (hasNonEmptyCliModalButtons(adapterStatus?.activeModal ?? adapterStatus?.modal)) return null;
|
|
1128
|
-
|
|
1129
|
-
const evidence = this.completionFinalAssistantEvidence(parsedMessages);
|
|
1130
|
-
if (evidence.source !== 'external-native' || !evidence.present) return null;
|
|
1131
|
-
|
|
1132
|
-
const messages = Array.isArray(evidence.messages) ? evidence.messages : [];
|
|
1133
|
-
const visibleMessages = messages.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
1134
|
-
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
1135
|
-
const lastMessageAt = lastVisible ? getMessageTime(lastVisible) : 0;
|
|
1136
|
-
const sourceMtimeMs = Number(this.lastExternalCompletionProbe?.sourceMtimeMs || 0);
|
|
1137
|
-
const minEvidenceAt = Math.max(
|
|
1138
|
-
this.startedAt > 0 ? this.startedAt - 5_000 : 0,
|
|
1139
|
-
this.generatingStartedAt > 0 ? this.generatingStartedAt - 5_000 : 0,
|
|
1140
|
-
this.lastAcknowledgedUserInputAt > 0 ? this.lastAcknowledgedUserInputAt - 1_000 : 0,
|
|
1141
|
-
);
|
|
1142
|
-
if (minEvidenceAt > 0 && lastMessageAt > 0 && lastMessageAt < minEvidenceAt && sourceMtimeMs < minEvidenceAt) {
|
|
1143
|
-
return null;
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
const finalSummary = extractFinalSummaryFromMessages(evidence.messages as any);
|
|
1147
|
-
if (!finalSummary) return null;
|
|
1148
|
-
const fingerprint = this.externalNativeFinalFingerprint(evidence);
|
|
1149
|
-
if (fingerprint === this.externalBusyIdleFingerprint) {
|
|
1150
|
-
return { fingerprint, finalSummary, evidence };
|
|
1151
|
-
}
|
|
1152
|
-
this.externalBusyIdleFingerprint = fingerprint;
|
|
1153
|
-
return { fingerprint, finalSummary, evidence };
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
1095
|
private buildCompletedFinalizationDiagnostic(args: {
|
|
1157
1096
|
blockReason: string;
|
|
1158
1097
|
latestStatus?: any;
|
|
@@ -1242,13 +1181,12 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1242
1181
|
return true;
|
|
1243
1182
|
}
|
|
1244
1183
|
|
|
1245
|
-
private getCompletedFinalizationBlock(latestVisibleStatus: string, pending: CompletedDebouncePending
|
|
1184
|
+
private getCompletedFinalizationBlock(latestVisibleStatus: string, pending: CompletedDebouncePending): CompletedFinalizationBlock | null {
|
|
1246
1185
|
if (latestVisibleStatus !== 'idle') return { reason: `status:${latestVisibleStatus}`, terminal: true };
|
|
1247
1186
|
|
|
1248
1187
|
const adapterAny = this.adapter as any;
|
|
1249
1188
|
const approvalResolvedIdle = pending.previousStatus === 'waiting_approval';
|
|
1250
|
-
|
|
1251
|
-
if (!approvalResolvedIdle && !externalNativeFinal) {
|
|
1189
|
+
if (!approvalResolvedIdle) {
|
|
1252
1190
|
if (adapterAny?.isWaitingForResponse === true) return { reason: 'adapter_waiting_for_response', terminal: true };
|
|
1253
1191
|
if (adapterAny?.currentTurnScope) return { reason: 'adapter_turn_scope_active', terminal: true };
|
|
1254
1192
|
if (this.hasAdapterPendingResponse()) return { reason: 'adapter_pending_response', terminal: true };
|
|
@@ -1257,7 +1195,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1257
1195
|
const partial = typeof this.adapter.getPartialResponse === 'function'
|
|
1258
1196
|
? this.adapter.getPartialResponse()
|
|
1259
1197
|
: '';
|
|
1260
|
-
if (
|
|
1198
|
+
if (typeof partial === 'string' && partial.trim()) return { reason: 'partial_response_pending', terminal: true };
|
|
1261
1199
|
|
|
1262
1200
|
let parsed: any;
|
|
1263
1201
|
try {
|
|
@@ -1269,7 +1207,6 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1269
1207
|
const parsedStatus = typeof parsed?.status === 'string' ? parsed.status : 'unknown';
|
|
1270
1208
|
if (parsedStatus !== 'idle') {
|
|
1271
1209
|
const adapterStatus = this.adapter.getStatus({ allowParse: false });
|
|
1272
|
-
if (externalNativeFinal && isCliGeneratingLikeStatus(parsedStatus)) return null;
|
|
1273
1210
|
if (this.shouldSuppressStaleParsedBusyStatus(parsed, adapterStatus)) return null;
|
|
1274
1211
|
return { reason: `parsed_status:${parsedStatus}`, terminal: isCliGeneratingLikeStatus(parsedStatus) };
|
|
1275
1212
|
}
|
|
@@ -1277,6 +1214,33 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1277
1214
|
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1278
1215
|
const finalAssistantEvidence = this.completionFinalAssistantEvidence(parsed?.messages);
|
|
1279
1216
|
const allowMissingAssistantTimeout = !!(this.settings.meshNodeFor || this.settings.meshActiveTaskId || this.settings.launchedByCoordinator);
|
|
1217
|
+
|
|
1218
|
+
// Transcript settle guard: even when a final assistant message is present, a
|
|
1219
|
+
// native-source transcript the CLI is still appending to (the final assistant turn
|
|
1220
|
+
// lands in chunks) yields a *partial* finalSummary if read mid-flush — e.g. a 77-char
|
|
1221
|
+
// prefix "...base 8e788950, and". Re-read the transcript and compare against the prior
|
|
1222
|
+
// probe: if the last assistant message is still growing (msgCount or contentLen
|
|
1223
|
+
// increased since the previous probe), it is mid-write — block and retry. Once two
|
|
1224
|
+
// consecutive probes agree, the turn is fully flushed and we finalize. This keys on
|
|
1225
|
+
// observed growth, not wall-clock age, so an already-settled transcript finalizes with
|
|
1226
|
+
// no added latency. Matters most for worktree workers (cwd → a different projects/
|
|
1227
|
+
// folder than the primary checkout, where flush timing skews the read).
|
|
1228
|
+
if (adapterOwnsMessagesElsewhere && finalAssistantEvidence.source === 'external-native') {
|
|
1229
|
+
const prevProbe = (pending.transcriptProbeHistory || [])[ (pending.transcriptProbeHistory?.length ?? 0) - 1 ];
|
|
1230
|
+
this.readExternalCompletionMessages();
|
|
1231
|
+
const settleProbe = this.lastExternalCompletionProbe;
|
|
1232
|
+
if (settleProbe && prevProbe) {
|
|
1233
|
+
const stillGrowing = settleProbe.msgCount > prevProbe.msgCount
|
|
1234
|
+
|| (settleProbe.lastRole === 'assistant' && settleProbe.contentLen > prevProbe.contentLen);
|
|
1235
|
+
if (stillGrowing) {
|
|
1236
|
+
this.recordPendingTranscriptProbe(pending);
|
|
1237
|
+
return { reason: `transcript_settling:${prevProbe.contentLen}->${settleProbe.contentLen}`, terminal: false };
|
|
1238
|
+
}
|
|
1239
|
+
} else if (settleProbe) {
|
|
1240
|
+
// First observation: record a baseline so the next flush attempt can detect growth.
|
|
1241
|
+
this.recordPendingTranscriptProbe(pending);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1280
1244
|
LOG.debug('CLI', `[${this.type}] finalAssistantEvidence: present=${finalAssistantEvidence.present} source=${finalAssistantEvidence.source} adapterOwnsMessagesElsewhere=${adapterOwnsMessagesElsewhere} parsedStatus=${parsedStatus}`);
|
|
1281
1245
|
if (!finalAssistantEvidence.present) {
|
|
1282
1246
|
if (adapterOwnsMessagesElsewhere) {
|
|
@@ -1341,11 +1305,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1341
1305
|
|
|
1342
1306
|
const latestStatus = this.adapter.getStatus({ allowParse: false });
|
|
1343
1307
|
const latestAutoApproveActive = latestStatus.status === 'waiting_approval' && this.shouldAutoApprove();
|
|
1344
|
-
const
|
|
1345
|
-
|
|
1346
|
-
? 'idle'
|
|
1347
|
-
: (latestAutoApproveActive || this.autoApproveBusy ? 'generating' : latestStatus.status);
|
|
1348
|
-
LOG.debug('CLI', `[${this.type}] flush attempt: adapterStatus=${latestStatus.status} latestVisible=${latestVisibleStatus} externalNativeFinal=${!!externalNativeFinal} generatingStartedAt=${this.generatingStartedAt} isWaitingForResponse=${!!(this.adapter as any)?.isWaitingForResponse} hasPartial=${!!this.adapter.getPartialResponse?.()}`);
|
|
1308
|
+
const latestVisibleStatus = latestAutoApproveActive || this.autoApproveBusy ? 'generating' : latestStatus.status;
|
|
1309
|
+
LOG.debug('CLI', `[${this.type}] flush attempt: adapterStatus=${latestStatus.status} latestVisible=${latestVisibleStatus} generatingStartedAt=${this.generatingStartedAt} isWaitingForResponse=${!!(this.adapter as any)?.isWaitingForResponse} hasPartial=${!!this.adapter.getPartialResponse?.()}`);
|
|
1349
1310
|
if (latestVisibleStatus !== 'idle') {
|
|
1350
1311
|
LOG.info('CLI', `[${this.type}] cancelled pending completed (resumed ${latestVisibleStatus})`);
|
|
1351
1312
|
this.completedDebouncePending = null;
|
|
@@ -1353,7 +1314,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1353
1314
|
return;
|
|
1354
1315
|
}
|
|
1355
1316
|
|
|
1356
|
-
const block = this.getCompletedFinalizationBlock(latestVisibleStatus, pending
|
|
1317
|
+
const block = this.getCompletedFinalizationBlock(latestVisibleStatus, pending);
|
|
1357
1318
|
if (block) {
|
|
1358
1319
|
const blockReason = block.reason;
|
|
1359
1320
|
const waitedMs = Date.now() - pending.firstObservedAt;
|
|
@@ -1398,18 +1359,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1398
1359
|
chatTitle: pending.chatTitle,
|
|
1399
1360
|
duration: pending.duration,
|
|
1400
1361
|
timestamp: pending.timestamp,
|
|
1401
|
-
finalSummary:
|
|
1402
|
-
...(externalNativeFinal ? {
|
|
1403
|
-
completionDiagnostic: {
|
|
1404
|
-
providerType: this.type,
|
|
1405
|
-
sessionId: this.instanceId,
|
|
1406
|
-
providerSessionId: this.providerSessionId || null,
|
|
1407
|
-
reconciliationReason: 'external_native_final_assistant_while_adapter_busy',
|
|
1408
|
-
finalAssistantPresent: true,
|
|
1409
|
-
finalAssistantEvidenceSource: externalNativeFinal.evidence.source,
|
|
1410
|
-
externalFinalFingerprint: externalNativeFinal.fingerprint,
|
|
1411
|
-
},
|
|
1412
|
-
} : {}),
|
|
1362
|
+
finalSummary: this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
|
|
1413
1363
|
});
|
|
1414
1364
|
this.completedDebouncePending = null;
|
|
1415
1365
|
this.completedDebounceTimer = null;
|
|
@@ -1486,15 +1436,13 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1486
1436
|
const parsedStatus = null;
|
|
1487
1437
|
const rawStatus = adapterStatus.status;
|
|
1488
1438
|
const autoApproveActive = this.maybeAutoApproveStatus(adapterStatus, now);
|
|
1489
|
-
const externalNativeFinal = this.getExternalNativeFinalReconciliation(undefined, adapterStatus);
|
|
1490
1439
|
// During the autoApproveBusy window (2s after firing approval key), the PTY
|
|
1491
1440
|
// can briefly report 'idle' before the next generating phase starts. Treat that
|
|
1492
1441
|
// transient idle as 'generating' to suppress a spurious agent:generating_completed
|
|
1493
|
-
// push notification.
|
|
1442
|
+
// push notification. The adapter's status is otherwise authoritative — native
|
|
1443
|
+
// transcript shape does NOT override the FSM's busy/idle decision.
|
|
1494
1444
|
const autoApproveHoldIdle = this.autoApproveBusy && rawStatus === 'idle';
|
|
1495
|
-
const newStatus =
|
|
1496
|
-
? 'idle'
|
|
1497
|
-
: (autoApproveActive || autoApproveHoldIdle ? 'generating' : rawStatus);
|
|
1445
|
+
const newStatus = autoApproveActive || autoApproveHoldIdle ? 'generating' : rawStatus;
|
|
1498
1446
|
const dirName = this.workingDir.split('/').filter(Boolean).pop() || 'session';
|
|
1499
1447
|
const chatTitle = `${this.provider.name} · ${dirName}`;
|
|
1500
1448
|
const partial = this.adapter.getPartialResponse();
|
|
@@ -68,7 +68,7 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
68
68
|
native_history?: NativeHistoryConfig;
|
|
69
69
|
};
|
|
70
70
|
private lastEvent: DashboardEvent | null = null;
|
|
71
|
-
private latestState: { id: string; label: string; title: string | null } | null = null;
|
|
71
|
+
private latestState: { id: string; label: string; title: string | null; status: 'idle' | 'generating' | 'approval' } | null = null;
|
|
72
72
|
private latestModal: { title: string | null; buttons: { index: number; label: string }[] } | null = null;
|
|
73
73
|
private statusCallback: (() => void) | null = null;
|
|
74
74
|
private ptyDataCallback: ((data: string) => void) | null = null;
|
|
@@ -159,21 +159,25 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
159
159
|
const state = this.latestState;
|
|
160
160
|
if (!state) return { status: 'starting', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, ...sessionFields };
|
|
161
161
|
|
|
162
|
+
// The FSM state is authoritative for status. We do NOT infer status from whether
|
|
163
|
+
// a modal was parsed this frame: a modal/approval state whose buttons briefly fail
|
|
164
|
+
// to parse (PTY repaint) must still report waiting_approval, not collapse to idle —
|
|
165
|
+
// that collapse fired false completions while a session sat at an approval prompt.
|
|
162
166
|
const modal = this.latestModal;
|
|
163
|
-
|
|
164
|
-
if (modal) {
|
|
167
|
+
if (state.status === 'approval') {
|
|
165
168
|
return {
|
|
166
169
|
status: 'waiting_approval',
|
|
167
170
|
messages: [],
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
// Surface buttons when we have them; an approval state with no parsed
|
|
172
|
+
// modal this frame still stays waiting_approval (no activeModal yet).
|
|
173
|
+
activeModal: modal
|
|
174
|
+
? { message: modal.title ?? state.label, buttons: modal.buttons.map(b => b.label) }
|
|
175
|
+
: null,
|
|
172
176
|
activeInteractivePrompt: this.activeInteractivePrompt,
|
|
173
177
|
...sessionFields,
|
|
174
178
|
};
|
|
175
179
|
}
|
|
176
|
-
if (
|
|
180
|
+
if (state.status === 'generating') {
|
|
177
181
|
return { status: 'generating', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, ...sessionFields };
|
|
178
182
|
}
|
|
179
183
|
return { status: 'idle', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, ...sessionFields };
|
|
@@ -41,7 +41,7 @@ import { LOG } from '../../logging/logger.js';
|
|
|
41
41
|
|
|
42
42
|
export type DashboardEvent =
|
|
43
43
|
| { kind: 'pty_data'; chunk: string }
|
|
44
|
-
| { kind: 'state_changed'; state: { id: string; label: string; title: string | null };
|
|
44
|
+
| { kind: 'state_changed'; state: { id: string; label: string; title: string | null; status: 'idle' | 'generating' | 'approval' };
|
|
45
45
|
modal: { title: string | null; buttons: { index: number; label: string }[] } | null;
|
|
46
46
|
controls: { id: string; label: string; action_type: string }[] }
|
|
47
47
|
| { kind: 'notification'; id: string; title: string; body: string }
|
|
@@ -144,7 +144,7 @@ type HistoryEntry = DriverHistoryEntry;
|
|
|
144
144
|
/** Per-state evaluation snapshot (mirrors v3 SpecEvaluation shape for the
|
|
145
145
|
* parts the cli-adapter / panel consume). */
|
|
146
146
|
interface CurrentEval {
|
|
147
|
-
state: { id: string; label: string; title: string | null };
|
|
147
|
+
state: { id: string; label: string; title: string | null; status: 'idle' | 'generating' | 'approval' };
|
|
148
148
|
modal: ModalSnapshot | null;
|
|
149
149
|
controls: VisibleControl[];
|
|
150
150
|
}
|
|
@@ -432,7 +432,12 @@ export class FsmDriver implements ISpecDriver {
|
|
|
432
432
|
const title = modal?.title ?? this.deriveTitle(state, sections, lines.join('\n'));
|
|
433
433
|
|
|
434
434
|
const next: CurrentEval = {
|
|
435
|
-
|
|
435
|
+
// status is derived from the FSM state itself (statusForState), NOT from
|
|
436
|
+
// whether a modal was parsed this frame. A modal state whose buttons briefly
|
|
437
|
+
// fail to parse (PTY repaint → deriveModal returns null) must still report
|
|
438
|
+
// its authoritative status (e.g. 'approval'), so the adapter never collapses
|
|
439
|
+
// an approval/busy state to idle on a transient modal-parse miss.
|
|
440
|
+
state: { id: state.id, label: state.label, title, status: statusForState(state) },
|
|
436
441
|
modal,
|
|
437
442
|
controls,
|
|
438
443
|
};
|