@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.
@@ -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
- const externalNativeFinal = this.getExternalNativeFinalReconciliation(parsedStatus?.messages, adapterStatus);
603
- if (externalNativeFinal && isCliGeneratingLikeStatus(visibleStatus)) {
604
- visibleStatus = 'idle';
605
- }
606
- // Adapter raw status can lag behind parsed/native evidence: if the spec driver
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
- void this.adapter.sendMessage(promptText).catch((e: any) => {
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, opts?: { externalNativeFinal?: ExternalNativeFinalReconciliation | null }): CompletedFinalizationBlock | null {
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
- const externalNativeFinal = opts?.externalNativeFinal || null;
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 (!externalNativeFinal && typeof partial === 'string' && partial.trim()) return { reason: 'partial_response_pending', terminal: true };
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 externalNativeFinal = this.getExternalNativeFinalReconciliation(undefined, latestStatus);
1345
- const latestVisibleStatus = externalNativeFinal && isCliGeneratingLikeStatus(latestStatus.status)
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, { externalNativeFinal });
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: externalNativeFinal?.finalSummary || this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages),
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. externalNativeFinal still wins to allow hard-stop overrides.
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 = externalNativeFinal && isCliGeneratingLikeStatus(rawStatus)
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
- const lc = state.id.toLowerCase();
164
- if (modal) {
167
+ if (state.status === 'approval') {
165
168
  return {
166
169
  status: 'waiting_approval',
167
170
  messages: [],
168
- activeModal: {
169
- message: modal.title ?? state.label,
170
- buttons: modal.buttons.map(b => b.label),
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 (lc === 'busy' || lc === 'generating') {
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
- state: { id: state.id, label: state.label, title },
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
  };