@adhdev/daemon-core 0.9.82-rc.442 → 0.9.82-rc.443

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.
@@ -29,7 +29,7 @@ import { formatAutoApprovalMessage, pickApprovalButton, hasNegativeApprovalOptio
29
29
  import { getCliScriptCommand, parseCliScriptResult } from './cli-script-results.js';
30
30
  import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
31
31
  import { normalizeProviderSessionId } from './provider-session-id.js';
32
- import { buildChatMessage, buildRuntimeSystemChatMessage, isUserFacingChatMessage, normalizeChatMessages, resolveChatMessageKind, extractFinalSummaryFromMessages, extractFinalSummaryFromMessagesAfter } from './chat-message-normalization.js';
32
+ import { buildChatMessage, buildRuntimeSystemChatMessage, isUserFacingChatMessage, normalizeChatMessages, resolveChatMessageKind, extractFinalSummaryFromMessages, extractFinalSummaryFromMessagesAfter, readChatMessageTimestampMs } from './chat-message-normalization.js';
33
33
  import { workingDirBasename } from './working-dir.js';
34
34
  import { ManualAttendanceTracker } from './manual-attendance.js';
35
35
 
@@ -75,6 +75,16 @@ type CompletedDebouncePending = {
75
75
  // debounce that flushes before the producing turn's final assistant bubble lands
76
76
  // in the native transcript never echoes the PRIOR task's last bubble.
77
77
  turnStartedAt?: number;
78
+ // FALSE-IDLE continuity: the busyEpoch value at the instant this pending was armed.
79
+ // The flush guard requires this.busyEpoch to still equal this — proving no busy
80
+ // phase (generating/waiting_approval) opened since arming. A momentary busy→idle
81
+ // blip in an inter-approval valley bumps busyEpoch, so a completion armed before
82
+ // the blip is cancelled at flush instead of emitting a stale mid-turn summary.
83
+ busyEpochAtArm?: number;
84
+ // FALSE-IDLE continuity: the adapter's raw PTY lastOutputAt at arm time. New PTY
85
+ // output after arming means the session was not continuously idle through the
86
+ // settle window (the agent kept printing), so the completion is cancelled.
87
+ lastOutputAtArm?: number;
78
88
  };
79
89
 
80
90
  function isIdleStatus(value: unknown): boolean {
@@ -508,6 +518,15 @@ export class CliProviderInstance implements ProviderInstance {
508
518
  // first sets it; the other becomes a no-op.
509
519
  private agentReadyEmitted = false;
510
520
  private generatingStartedAt: number = 0;
521
+ // FALSE-IDLE continuity epoch: monotonically bumped on EVERY entry into a busy
522
+ // phase (→generating or →waiting_approval). The completedDebouncePending snapshots
523
+ // this value at arm time (busyEpochAtArm); the flush guard requires it UNCHANGED
524
+ // — proving the session did not re-enter a busy phase (a momentary busy→idle blip
525
+ // in an inter-approval valley) between arming the debounce and flushing it. A
526
+ // single point-sample of status at flush time cannot see a generating phase that
527
+ // opened AND closed within the settle window; the epoch can. See
528
+ // flushCompletedDebounceIfFinalized.
529
+ private busyEpoch: number = 0;
511
530
  // GENERATING-BOUNDARY (R4b): the per-turn taskId for which a startup-grace
512
531
  // started+completed pair was already synthesized. Both fast-collapse callers
513
532
  // (starting→idle transition AND the idle-stayed no-status-change poll) route
@@ -1411,7 +1430,7 @@ export class CliProviderInstance implements ProviderInstance {
1411
1430
  this.applyProviderResponse(parsed.payload, { phase: 'immediate' });
1412
1431
  }
1413
1432
 
1414
- private completionHasFinalAssistantMessage(messages: unknown): boolean {
1433
+ private completionHasFinalAssistantMessage(messages: unknown, turnStartedAt?: number): boolean {
1415
1434
  const visibleMessages = (Array.isArray(messages) ? messages : [])
1416
1435
  .filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
1417
1436
  const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
@@ -1421,6 +1440,21 @@ export class CliProviderInstance implements ProviderInstance {
1421
1440
  // Guard: if the last assistant message looks like an active approval/input prompt,
1422
1441
  // it is not a real completion — the session is still awaiting user input.
1423
1442
  if (looksLikeActiveApprovalPromptText(content)) return false;
1443
+ // FALSE-IDLE turn-boundary evidence (Defect 1b): when a producing-turn start is
1444
+ // known, the final assistant bubble must POST-DATE it. A STALE mid-turn assistant
1445
+ // (predating this turn's start — e.g. the last bubble of a prior sub-turn observed
1446
+ // during an inter-approval valley) must NOT satisfy the finalization gate, or a
1447
+ // false-idle blip emits a completion carrying that stale summary. A bubble with no
1448
+ // parseable timestamp cannot be proven stale, so it is kept (fails open — behaviour
1449
+ // identical to before for providers/paths that carry no timestamps).
1450
+ if (typeof turnStartedAt === 'number' && Number.isFinite(turnStartedAt) && turnStartedAt > 0) {
1451
+ // readChatMessageTimestampMs mirrors the summary turn-scoping reader
1452
+ // (extractFinalSummaryFromMessagesAfter) — same seconds-vs-ms heuristic and
1453
+ // field precedence — so the present-check and the summary-scope agree on which
1454
+ // bubbles predate the turn.
1455
+ const ts = readChatMessageTimestampMs(lastVisible);
1456
+ if (typeof ts === 'number' && ts < turnStartedAt) return false;
1457
+ }
1424
1458
  return true;
1425
1459
  }
1426
1460
 
@@ -1485,8 +1519,8 @@ export class CliProviderInstance implements ProviderInstance {
1485
1519
  return restoredHistory.messages;
1486
1520
  }
1487
1521
 
1488
- private completionFinalAssistantEvidence(parsedMessages: unknown): CompletionFinalAssistantEvidence {
1489
- if (this.completionHasFinalAssistantMessage(parsedMessages)) {
1522
+ private completionFinalAssistantEvidence(parsedMessages: unknown, turnStartedAt?: number): CompletionFinalAssistantEvidence {
1523
+ if (this.completionHasFinalAssistantMessage(parsedMessages, turnStartedAt)) {
1490
1524
  return {
1491
1525
  present: true,
1492
1526
  messages: Array.isArray(parsedMessages) ? parsedMessages : [],
@@ -1497,7 +1531,7 @@ export class CliProviderInstance implements ProviderInstance {
1497
1531
  const externalMessages = this.readExternalCompletionMessages();
1498
1532
  if (externalMessages) {
1499
1533
  return {
1500
- present: this.completionHasFinalAssistantMessage(externalMessages),
1534
+ present: this.completionHasFinalAssistantMessage(externalMessages, turnStartedAt),
1501
1535
  messages: externalMessages,
1502
1536
  source: 'external-native',
1503
1537
  };
@@ -1528,10 +1562,17 @@ export class CliProviderInstance implements ProviderInstance {
1528
1562
  // at/after turnStartedAt yields '' in that race instead of the stale tail; the weak/empty
1529
1563
  // summary is later upgraded by the mesh reconcile loop once the real bubble is written.
1530
1564
  const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
1531
- const parsedSummary = extractFinalSummaryFromMessages(
1532
- (this.completionHasFinalAssistantMessage(parsedMessages)
1565
+ // FALSE-IDLE Defect 1b: turn-scope the PARSED screen fallback too. Without this a stale
1566
+ // mid-turn assistant (predating turnStartedAt) that the turn-boundary gate already
1567
+ // rejected as evidence could still leak into the finalSummary via this parsed fallback
1568
+ // when the external transcript's turn-scoped read is empty — freezing the very stale text
1569
+ // the gate rejected. extractFinalSummaryFromMessagesAfter drops bubbles before the turn
1570
+ // start; with no boundary known (turnStartedAt falsy) it is identical to the unscoped read.
1571
+ const parsedSummary = extractFinalSummaryFromMessagesAfter(
1572
+ (this.completionHasFinalAssistantMessage(parsedMessages, turnStartedAt)
1533
1573
  ? (Array.isArray(parsedMessages) ? parsedMessages : [])
1534
1574
  : []) as any,
1575
+ turnStartedAt,
1535
1576
  );
1536
1577
  if (adapterOwnsMessagesElsewhere) {
1537
1578
  const externalMessages = this.readExternalCompletionMessages();
@@ -1669,7 +1710,11 @@ export class CliProviderInstance implements ProviderInstance {
1669
1710
  }
1670
1711
  if (parsed?.activeModal || parsed?.modal) return { reason: 'parsed_modal_active', terminal: true };
1671
1712
  const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
1672
- const finalAssistantEvidence = this.completionFinalAssistantEvidence(parsed?.messages);
1713
+ // FALSE-IDLE turn-boundary evidence (Defect 1b): turn-scope the present-check so a
1714
+ // STALE mid-turn assistant (predating pending.turnStartedAt) cannot satisfy the
1715
+ // finalization gate. Only the confirming final-assistant bubble that POST-DATES this
1716
+ // turn's start counts as evidence the turn genuinely ended.
1717
+ const finalAssistantEvidence = this.completionFinalAssistantEvidence(parsed?.messages, pending.turnStartedAt);
1673
1718
  const allowMissingAssistantTimeout = !!(this.settings.meshNodeFor || this.settings.meshActiveTaskId || this.settings.launchedByCoordinator);
1674
1719
  LOG.debug('CLI', `[${this.type}] finalAssistantEvidence: present=${finalAssistantEvidence.present} source=${finalAssistantEvidence.source} adapterOwnsMessagesElsewhere=${adapterOwnsMessagesElsewhere} parsedStatus=${parsedStatus}`);
1675
1720
  if (!finalAssistantEvidence.present) {
@@ -1844,6 +1889,32 @@ export class CliProviderInstance implements ProviderInstance {
1844
1889
  return;
1845
1890
  }
1846
1891
 
1892
+ // FALSE-IDLE continuity guard (Defect 1a): the point-sample above only proves the
1893
+ // session is idle at THIS instant. A momentary busy→idle blip inside an inter-approval
1894
+ // valley (auto-approved tool turns) opens AND closes a generating phase entirely within
1895
+ // the settle window — so the single sample reads 'idle' even though the turn is still in
1896
+ // flight (it re-enters generating ~0.5s later). Require instead that the session stayed
1897
+ // CONTINUOUSLY idle since the debounce was armed: (1) no entry into a busy phase
1898
+ // (busyEpoch unchanged), and (2) no new raw PTY output (lastOutputAt did not advance).
1899
+ // Either signal ⇒ the idle was not continuous ⇒ cancel; the still-live turn re-arms its
1900
+ // own completion when it genuinely finishes. This only ever cancels (never emits more),
1901
+ // so shared behaviour for claude/codex/antigravity is strictly stricter, never looser.
1902
+ if (typeof pending.busyEpochAtArm === 'number' && this.busyEpoch !== pending.busyEpochAtArm) {
1903
+ LOG.info('CLI', `[${this.type}] cancelled pending completed (busy re-entry during settle: epoch ${pending.busyEpochAtArm}→${this.busyEpoch})`);
1904
+ this.completedDebouncePending = null;
1905
+ this.completedDebounceTimer = null;
1906
+ return;
1907
+ }
1908
+ const latestOutputAt = typeof (latestStatus as any)?.lastOutputAt === 'number' ? (latestStatus as any).lastOutputAt as number : undefined;
1909
+ if (typeof pending.lastOutputAtArm === 'number'
1910
+ && typeof latestOutputAt === 'number'
1911
+ && latestOutputAt > pending.lastOutputAtArm) {
1912
+ LOG.info('CLI', `[${this.type}] cancelled pending completed (new PTY output during settle: ${pending.lastOutputAtArm}→${latestOutputAt})`);
1913
+ this.completedDebouncePending = null;
1914
+ this.completedDebounceTimer = null;
1915
+ return;
1916
+ }
1917
+
1847
1918
  const block = this.getCompletedFinalizationBlock(latestVisibleStatus, pending);
1848
1919
  if (block) {
1849
1920
  const blockReason = block.reason;
@@ -2357,6 +2428,9 @@ export class CliProviderInstance implements ProviderInstance {
2357
2428
  }
2358
2429
 
2359
2430
  if (!this.generatingStartedAt) this.generatingStartedAt = now;
2431
+ // FALSE-IDLE continuity: entering a busy phase invalidates any
2432
+ // completedDebouncePending armed earlier in this settle window.
2433
+ this.busyEpoch++;
2360
2434
  // Defer the generating_started event — if idle comes back within 3s,
2361
2435
  // the whole started→completed pair was a false positive from PTY noise
2362
2436
  if (this.generatingDebounceTimer) clearTimeout(this.generatingDebounceTimer);
@@ -2381,6 +2455,11 @@ export class CliProviderInstance implements ProviderInstance {
2381
2455
  this.completedDebouncePending = null;
2382
2456
 
2383
2457
  if (!this.generatingStartedAt) this.generatingStartedAt = now;
2458
+ // FALSE-IDLE continuity: waiting_approval is a busy phase (the agent
2459
+ // resumes into it), so bump the epoch too — the completedDebouncePending
2460
+ // cancel above covers the currently-armed pending, and the epoch covers
2461
+ // a pending that re-arms and flushes across this same valley.
2462
+ this.busyEpoch++;
2384
2463
  const modal = adapterStatus.activeModal;
2385
2464
  LOG.info('CLI', `[${this.type}] approval modal: "${modal?.message?.slice(0, 80) ?? 'none'}"`);
2386
2465
  // Include the FSM's approval entry seq, mirroring the auto-approve
@@ -2533,6 +2612,14 @@ export class CliProviderInstance implements ProviderInstance {
2533
2612
  const turnStartedAt = engineTurnStart || this.generatingStartedAt || 0;
2534
2613
  return turnStartedAt ? { turnStartedAt } : {};
2535
2614
  })()),
2615
+ // FALSE-IDLE continuity: snapshot the busy epoch + raw PTY output
2616
+ // clock at arm time so the flush guard can prove the session stayed
2617
+ // continuously idle (no busy re-entry, no new PTY output) through the
2618
+ // settle window rather than merely reading 'idle' once at flush.
2619
+ busyEpochAtArm: this.busyEpoch,
2620
+ ...(typeof adapterStatus?.lastOutputAt === 'number' && Number.isFinite(adapterStatus.lastOutputAt)
2621
+ ? { lastOutputAtArm: adapterStatus.lastOutputAt as number }
2622
+ : {}),
2536
2623
  };
2537
2624
  const ownsExternalHistory = !!(this.adapter as any)?.chatMessagesOwnedExternally;
2538
2625
  // (FALSEIDLE-BGCHILD-a) Native-history providers flush immediately (the