@adhdev/daemon-core 1.0.28-rc.17 → 1.0.28-rc.18

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "1.0.28-rc.17",
3
+ "version": "1.0.28-rc.18",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -49,8 +49,8 @@
49
49
  "author": "vilmire",
50
50
  "license": "AGPL-3.0-or-later",
51
51
  "dependencies": {
52
- "@adhdev/mesh-shared": "1.0.28-rc.17",
53
- "@adhdev/session-host-core": "1.0.28-rc.17",
52
+ "@adhdev/mesh-shared": "1.0.28-rc.18",
53
+ "@adhdev/session-host-core": "1.0.28-rc.18",
54
54
  "@agentclientprotocol/sdk": "^0.16.1",
55
55
  "ajv": "^8.20.0",
56
56
  "ajv-formats": "^3.0.1",
@@ -729,7 +729,25 @@ export class CliStateEngine {
729
729
  if (!session) return;
730
730
 
731
731
  const { status, messages } = session;
732
- const modal = (session as any).activeModal ?? session.modal ?? null;
732
+ // RC17-APPROVAL-NO-MODAL: a provider's own detectStatus/parseSession script can
733
+ // report status='waiting_approval' with a modal object whose buttons array is
734
+ // empty (a half-rendered approval frame, or — as observed live on a Claude
735
+ // session with no actionable modal anywhere in the raw PTY — a bare status flip
736
+ // with no real prompt at all). Previously this raw modal was latched straight
737
+ // into this.activeModal and dispatched through applyWaitingApproval's
738
+ // ALREADY-actionable branch (line ~1042 below), which sets status +
739
+ // fires agent:waiting_approval unconditionally — the "!modal" hysteresis/warn
740
+ // path (the one place button-validity was actually enforced) only runs when the
741
+ // modal is null, not when it merely has no buttons. Treat a buttons-empty modal
742
+ // as no modal here so it flows into that same proven hysteresis: it warns, waits
743
+ // out approvalCooldown, and never fires an event unless a LATER settle pass
744
+ // observes a genuinely actionable modal. Mirrors hasNonEmptyCliModalButtons used
745
+ // elsewhere (resolveModal, stabilizeFlappingApprovalStatus) — same predicate,
746
+ // applied at the point the modal first enters the FSM.
747
+ const rawModal = (session as any).activeModal ?? session.modal ?? null;
748
+ const modal = rawModal && Array.isArray(rawModal.buttons) && rawModal.buttons.some(
749
+ (button: unknown) => typeof button === 'string' && button.trim().length > 0,
750
+ ) ? rawModal : null;
733
751
  const parsedStatus = (session as any).parsedStatus ?? null;
734
752
  const parsedMessages = normalizeCliParsedMessages(messages, {
735
753
  scope: null,
@@ -24,7 +24,7 @@ import {
24
24
  type ChatSourceTransitionCause,
25
25
  } from '../chat/source-resolver.js';
26
26
  import type { ChatMessage } from '../types.js';
27
- import { filterUserFacingChatMessages, isActivityChatMessage, isUserFacingChatMessage, normalizeChatMessages } from '../providers/chat-message-normalization.js';
27
+ import { filterUserFacingChatMessages, isActivityChatMessage, isUserFacingChatMessage, normalizeChatMessages, hasTrailingToolActivityAfterFinalAssistant } from '../providers/chat-message-normalization.js';
28
28
  import {
29
29
  READ_CHAT_PROVIDER_EVAL_TIMEOUT_MS,
30
30
  type RuntimeChatMessageMerger,
@@ -2480,11 +2480,43 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
2480
2480
  });
2481
2481
  }
2482
2482
  }
2483
+ // RC17-NATIVE-FINAL-ASSISTANT-MIDTURN: hasFinalVisibleAssistantMessage only
2484
+ // checks that the LAST visible native-transcript message is a non-empty
2485
+ // assistant/model bubble — it has no notion of whether that bubble is
2486
+ // actually the end of the turn. A live Claude repro showed this reconciling
2487
+ // generating→idle on an INTERIM narration bubble ("Starting the collector
2488
+ // now, before the two-turn protocol.") emitted mid-turn, well before the
2489
+ // real two-turn protocol/tool work ran — exactly the false-completion class
2490
+ // rc.16 (9452bd03/6677a565) closed for the mesh-event ingress via
2491
+ // hasTrailingToolActivityAfterFinalAssistant + hasLiveTurnPendingEvidence.
2492
+ // This read_chat status ingress is a SEPARATE consumer of the same
2493
+ // native-final-assistant signal (it feeds the `status` field returned to
2494
+ // read_chat/dashboard, not the mesh agent:generating_completed event) and
2495
+ // never got the equivalent veto. This reconciliation exists precisely for a
2496
+ // PTY/adapter status detector that never transitions to idle on its own (see
2497
+ // read-chat-completed-session-fallback.test.ts's antigravity stuck-busy
2498
+ // case: isProcessing() stays true forever, empty PTY messages, and native
2499
+ // history is the only signal that ever resolves it) — so gating on the
2500
+ // adapter's own pending-response bit here would neuter this reconciliation
2501
+ // for that exact intended case. Apply only STRUCTURAL, evidence-based vetoes
2502
+ // instead, mirroring rc.16's hasLiveTurnPendingEvidence: trailing tool/
2503
+ // terminal activity after the final-looking assistant bubble, scanned in (a)
2504
+ // the native messages being judged — the transcript's own admission that its
2505
+ // "final" bubble kept going — and (b) the live PTY-parsed tail
2506
+ // (returnedMessages), which carries no native-transcript write-lag and is
2507
+ // exactly how a live Claude repro was caught: the native JSONL's last row was
2508
+ // still the interim narration bubble (no trailing tool row landed there yet),
2509
+ // but the PTY had already rendered the very next "Auto-approved: Yes\nBash
2510
+ // command" tool activity. The antigravity stuck-busy case has an EMPTY PTY
2511
+ // message list, so (b) is a no-op there (the function short-circuits false on
2512
+ // an empty/absent array) and the existing regression is unaffected.
2483
2513
  if (
2484
2514
  isGeneratingLikeStatus(selectedStatus)
2485
2515
  && selectedTranscriptAuthority === 'provider'
2486
2516
  && !hasNonEmptyModalButtons(activeModal)
2487
2517
  && hasFinalVisibleAssistantMessage(selectedMessages)
2518
+ && !hasTrailingToolActivityAfterFinalAssistant(selectedMessages as any)
2519
+ && !hasTrailingToolActivityAfterFinalAssistant(returnedMessages as any)
2488
2520
  ) {
2489
2521
  selectedStatus = 'idle';
2490
2522
  selectedMessages = finalizeStreamingMessagesWhenIdle(selectedMessages, selectedStatus);