@adhdev/daemon-core 1.0.28-rc.6 → 1.0.28-rc.7

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.6",
3
+ "version": "1.0.28-rc.7",
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.6",
53
- "@adhdev/session-host-core": "1.0.28-rc.6",
52
+ "@adhdev/mesh-shared": "1.0.28-rc.7",
53
+ "@adhdev/session-host-core": "1.0.28-rc.7",
54
54
  "@agentclientprotocol/sdk": "^0.16.1",
55
55
  "ajv": "^8.20.0",
56
56
  "ajv-formats": "^3.0.1",
@@ -1604,7 +1604,15 @@ export class CliStateEngine {
1604
1604
  // background-tool race this was meant to guard against. If a provider
1605
1605
  // really needs the gate, the manifest can set
1606
1606
  // `requiresFinalAssistantBeforeIdle: true`.
1607
- const requiresFinalAssistant = !!this.provider.requiresFinalAssistantBeforeIdle;
1607
+ //
1608
+ // This is a completion-timing JUDGMENT, so it goes through the shared
1609
+ // transcript-authority profile rather than reading the raw flag: the
1610
+ // 'floor' class is exactly `requiresFinalAssistantBeforeIdle && !hold`
1611
+ // (see resolveTranscriptAuthorityProfile), which is equivalent to the
1612
+ // old `!!requiresFinalAssistantBeforeIdle` for every real provider
1613
+ // because none declares both flags — a hold provider (antigravity)
1614
+ // sets holdCompletionForTranscript only, so it was already false here.
1615
+ const requiresFinalAssistant = resolveTranscriptAuthorityProfile(this.provider).timing === 'floor';
1608
1616
  if (!requiresFinalAssistant) return false;
1609
1617
  if (!this.isWaitingForResponse || !this.currentTurnScope || this.hasActionableApproval()) return false;
1610
1618
  const parsedStatus = typeof parsed?.status === 'string' ? parsed.status.trim() : '';
@@ -232,6 +232,9 @@ export class ProviderCliAdapter implements CliAdapter {
232
232
  private pendingOutboundQueue: PendingOutboundMessage[] = [];
233
233
  private pendingOutboundFlushTimer: NodeJS.Timeout | null = null;
234
234
  private pendingOutboundFlushInFlight = false;
235
+ // Stale-queue watchdog: fires STALE_QUEUE_WARN_MS after the oldest queued
236
+ // message was enqueued if nothing has been flushed yet.
237
+ private pendingOutboundStaleTimer: NodeJS.Timeout | null = null;
235
238
  // Submit retry timer — PTY-level, not state machine
236
239
  private submitRetryTimer: NodeJS.Timeout | null = null;
237
240
 
@@ -1715,6 +1718,10 @@ export class ProviderCliAdapter implements CliAdapter {
1715
1718
  await new Promise<void>(resolve => setTimeout(resolve, FORCE_SUBMIT_SETTLE_MS));
1716
1719
  }
1717
1720
 
1721
+ // Stale-queue threshold: warn if a queued message has not been flushed
1722
+ // within this many milliseconds (instrumentation only, no behavior change).
1723
+ private static readonly STALE_QUEUE_WARN_MS = 15000;
1724
+
1718
1725
  private enqueuePendingOutboundMessage(text: string, reason: string, meshTaskId?: string): void {
1719
1726
  const content = String(text || '');
1720
1727
  const duplicate = this.pendingOutboundQueue.some((message) => message.content === content);
@@ -1722,6 +1729,18 @@ export class ProviderCliAdapter implements CliAdapter {
1722
1729
  return;
1723
1730
  }
1724
1731
  const queuedAt = Date.now();
1732
+ const stableMs = this.lastScreenChangeAt ? (queuedAt - this.lastScreenChangeAt) : -1;
1733
+ // Diagnostic context: which gate caused the silent-queue and the full
1734
+ // session readiness snapshot at the moment of enqueue.
1735
+ const gateContext = {
1736
+ reason,
1737
+ startupParseGate: this.startupParseGate,
1738
+ ready: this.ready,
1739
+ engineStatus: this.engine.currentStatus,
1740
+ isWaitingForResponse: this.engine.isWaitingForResponse,
1741
+ stableMs,
1742
+ sessionId: this.engine.getTraceSessionId(),
1743
+ };
1725
1744
  const message: PendingOutboundMessage = {
1726
1745
  id: `${queuedAt}:${this.pendingOutboundQueue.length}:${Math.random().toString(36).slice(2, 10)}`,
1727
1746
  role: 'user',
@@ -1731,8 +1750,23 @@ export class ProviderCliAdapter implements CliAdapter {
1731
1750
  ...(typeof meshTaskId === 'string' && meshTaskId.trim() ? { meshTaskId } : {}),
1732
1751
  };
1733
1752
  this.pendingOutboundQueue.push(message);
1734
- LOG.info('CLI', `[${this.cliType}] queued outbound message while busy (${reason}); queue=${this.pendingOutboundQueue.length}`);
1753
+ LOG.info('CLI', `[${this.cliType}] queued outbound message; gate=${JSON.stringify(gateContext)}; queue=${this.pendingOutboundQueue.length}`);
1735
1754
  this.onStatusChange?.();
1755
+ // Stale-queue watchdog: emit a warn-level log if the enqueued message
1756
+ // has not been flushed after STALE_QUEUE_WARN_MS. This fires once per
1757
+ // enqueue event (not once per message still in queue) so it does not
1758
+ // spam on a persistently stuck queue; it simply surfaces that the queue
1759
+ // is stuck and records the current gate state at warn time.
1760
+ if (!this.pendingOutboundStaleTimer) {
1761
+ this.pendingOutboundStaleTimer = setTimeout(() => {
1762
+ this.pendingOutboundStaleTimer = null;
1763
+ if (this.pendingOutboundQueue.length === 0) return;
1764
+ const oldest = this.pendingOutboundQueue[0];
1765
+ const staleSec = ((Date.now() - oldest.queuedAt) / 1000).toFixed(1);
1766
+ const nowStableMs = this.lastScreenChangeAt ? (Date.now() - this.lastScreenChangeAt) : -1;
1767
+ LOG.warn('CLI', `[${this.cliType}] STALE QUEUE: ${this.pendingOutboundQueue.length} message(s) undelivered for ${staleSec}s; gate=startupParseGate:${this.startupParseGate} ready:${this.ready} engineStatus:${this.engine.currentStatus} isWaitingForResponse:${this.engine.isWaitingForResponse} stableMs:${nowStableMs} sessionId:${this.engine.getTraceSessionId()} enqueueReason:${oldest.id}`);
1768
+ }, ProviderCliAdapter.STALE_QUEUE_WARN_MS);
1769
+ }
1736
1770
  }
1737
1771
 
1738
1772
  private shouldQueuePendingOutboundMessage(parsedStatusBeforeSend: any | null = null): string | null {
@@ -1785,6 +1819,11 @@ export class ProviderCliAdapter implements CliAdapter {
1785
1819
  try {
1786
1820
  await this.sendMessageNow(next.content, false, next.meshTaskId);
1787
1821
  this.pendingOutboundQueue.shift();
1822
+ // Clear stale watchdog once the queue drains.
1823
+ if (this.pendingOutboundQueue.length === 0 && this.pendingOutboundStaleTimer) {
1824
+ clearTimeout(this.pendingOutboundStaleTimer);
1825
+ this.pendingOutboundStaleTimer = null;
1826
+ }
1788
1827
  this.onStatusChange?.();
1789
1828
  } catch (error: any) {
1790
1829
  LOG.warn('CLI', `[${this.cliType}] queued outbound flush failed: ${error?.message || error}`);
@@ -2174,6 +2213,7 @@ export class ProviderCliAdapter implements CliAdapter {
2174
2213
  this.pendingTerminalQueryTail = '';
2175
2214
  this.ptyOutputChunks = [];
2176
2215
  if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
2216
+ if (this.pendingOutboundStaleTimer) { clearTimeout(this.pendingOutboundStaleTimer); this.pendingOutboundStaleTimer = null; }
2177
2217
  this.pendingOutboundQueue = [];
2178
2218
  this.pendingOutboundFlushInFlight = false;
2179
2219
  if (this.ptyProcess) {
@@ -2197,6 +2237,7 @@ export class ProviderCliAdapter implements CliAdapter {
2197
2237
  this.pendingTerminalQueryTail = '';
2198
2238
  this.ptyOutputChunks = [];
2199
2239
  if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
2240
+ if (this.pendingOutboundStaleTimer) { clearTimeout(this.pendingOutboundStaleTimer); this.pendingOutboundStaleTimer = null; }
2200
2241
  this.pendingOutboundQueue = [];
2201
2242
  this.pendingOutboundFlushInFlight = false;
2202
2243
  if (this.ptyProcess) {
@@ -785,16 +785,21 @@ export class CliProviderInstance implements ProviderInstance {
785
785
  }))
786
786
  : mergedMessages;
787
787
 
788
- // Dashboard-tail repair (native-source providers, e.g. antigravity): the
789
- // assistant answer lives only in native-history, so the PTY-parsed
790
- // statusMessages end on the user prompt / auto-approve system lines and the
791
- // snapshot's preview / lastMessageRole / completionMarker never see the
792
- // answer — the session looks stuck on the user turn. We already cached the
793
- // real final assistant summary at completion time (lastCompletionSummary),
794
- // so append it as the trailing assistant bubble when the current tail has no
795
- // assistant message at/after it. Purely additive to the status view; no
796
- // per-tick native read, no effect on providers whose PTY carries the
797
- // assistant (they surface it themselves and the guard below is a no-op).
788
+ // purpose: 'display-tail' (zero-read) — Dashboard-tail repair (native-source
789
+ // providers, e.g. antigravity): the assistant answer lives only in native-history,
790
+ // so the PTY-parsed statusMessages end on the user prompt / auto-approve system
791
+ // lines and the snapshot's preview / lastMessageRole / completionMarker never see
792
+ // the answer — the session looks stuck on the user turn. We already cached the
793
+ // real final assistant summary at completion time (lastCompletionSummary), so
794
+ // append it as the trailing assistant bubble when the current tail has no
795
+ // assistant message at/after it. Purely additive to the status view; no per-tick
796
+ // native read, no effect on providers whose PTY carries the assistant (they
797
+ // surface it themselves and the guard below is a no-op).
798
+ //
799
+ // authority-ok: this is a DISPLAY-ONLY tail repair, never a completion/stall/
800
+ // redrive verdict — it reads the pre-cached summary (zero native read) and only
801
+ // paints the status view. It keys off the adapter's runtime chatMessagesOwnedExternally
802
+ // capability (not a class predicate); a completion decision is never taken here.
798
803
  const adapterOwnsMessagesElsewhereForTail = (this.adapter as any)?.chatMessagesOwnedExternally === true;
799
804
  if (adapterOwnsMessagesElsewhereForTail && this.lastCompletionSummary) {
800
805
  const summary = this.lastCompletionSummary;
@@ -1580,6 +1585,11 @@ export class CliProviderInstance implements ProviderInstance {
1580
1585
  private readExternalCompletionMessages(): unknown[] | null {
1581
1586
  const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
1582
1587
  if (!adapterOwnsMessagesElsewhere) return null;
1588
+ // authority-ok: native READ resolution, not a completion/stall/redrive verdict.
1589
+ // This gate only decides whether an on-disk native transcript EXISTS to read for
1590
+ // this session; the completion decision is made by callers over the returned
1591
+ // messages (completionFinalAssistantEvidence / completionFinalSummary), which
1592
+ // route class through resolveTranscriptAuthorityProfile.
1583
1593
  if (!isNativeSourceCanonicalHistory(this.provider.nativeHistory)) return null;
1584
1594
 
1585
1595
  // Resolve a CONCRETE native-history handle for this session's OWN
@@ -2613,6 +2623,11 @@ export class CliProviderInstance implements ProviderInstance {
2613
2623
  * (≥180s of PTY stasis), never on the routine 5s tick.
2614
2624
  */
2615
2625
  private sampleNativeTranscriptProgress(): { msgCount: number; sourceMtimeMs: number } | null {
2626
+ // authority-ok: native fingerprint SAMPLING, not a stall verdict. This only
2627
+ // decides whether a native source exists to sample a progress fingerprint from;
2628
+ // the caller (checkMeshWorkerStall) makes the stall/no-progress decision over the
2629
+ // returned fingerprint. Non-native-source classes have nothing to sample here and
2630
+ // fall back to the unchanged lastOutputAt-only judgment.
2616
2631
  if (!isNativeSourceCanonicalHistory(this.provider?.nativeHistory)) return null;
2617
2632
  // readExternalCompletionMessages resolves this session's OWN native-source
2618
2633
  // conversation (providerSessionId / persisted pin / floor claim) and, as a
@@ -3900,7 +3915,31 @@ export class CliProviderInstance implements ProviderInstance {
3900
3915
  fcEvidenceSource = evidence.source;
3901
3916
  fcFinalSummary = extractFinalSummaryFromMessages(evidence.messages as any);
3902
3917
  } catch { /* best-effort */ }
3903
- const missingEvidence = (resolveTranscriptAuthorityProfile(this.provider).timing === 'floor' || fcEvidenceSource === 'external-native') && !fcFinalSummary;
3918
+ // SPEC-DRIVEN completion timing (mission f2f6da1b / AGY-BOOT-PHANTOM): the
3919
+ // startup-grace synth must enumerate the SAME transcript-authority timing
3920
+ // classes the genuine finalization gate does (getCompletedFinalizationBlock,
3921
+ // the external-native branch ~line 2120), not just 'floor' + external-native.
3922
+ // The 'hold' class (antigravity — holdCompletionForTranscript) was missing:
3923
+ // at boot its native history is often not yet written, so fcEvidenceSource is
3924
+ // 'unavailable' (neither 'floor' nor 'external-native'), which left
3925
+ // missingEvidence=false and fired a WEAK phantom completion the moment the FSM
3926
+ // collapsed to idle — a completion for a turn whose authoritative transcript
3927
+ // had not landed.
3928
+ const synthTiming = resolveTranscriptAuthorityProfile(this.provider).timing;
3929
+ const missingEvidence = (synthTiming === 'floor' || synthTiming === 'hold' || fcEvidenceSource === 'external-native') && !fcFinalSummary;
3930
+ // HOLD class (antigravity): idle holds for the native transcript to land. The
3931
+ // finalization gate never emits a completion for a hold provider without the
3932
+ // transcript (it returns holdForTranscript / null at ~line 2120), so the synth
3933
+ // must not either — even WITH mesh context. Suppress the synth and leave the
3934
+ // task UNMARKED so a later idle poll re-runs this path once the transcript's
3935
+ // final assistant lands (then fcFinalSummary is present → missingEvidence=false
3936
+ // → a genuine, summary-bearing emit). This is the HOLD-and-retry equivalent of
3937
+ // the finalization gate's non-terminal missing_final_assistant hold; it is what
3938
+ // stops the boot-time phantom weak completion for a mesh worker.
3939
+ if (missingEvidence && synthTiming === 'hold') {
3940
+ LOG.info('CLI', `[${this.type}] ${reason} held: hold-class transcript not yet landed (source=${fcEvidenceSource})`);
3941
+ return false;
3942
+ }
3904
3943
  // Mirror the short-generating idle path's suppression: a provider that
3905
3944
  // requires a final assistant (or external-native history) with NO confirmed
3906
3945
  // summary and NO mesh context emits nothing — the session is idle with no
@@ -5119,6 +5158,9 @@ export class CliProviderInstance implements ProviderInstance {
5119
5158
  const limit = options.full ? Number.MAX_SAFE_INTEGER : STATUS_HYDRATION_TAIL_LIMIT;
5120
5159
  const windowTag = options.full ? 'full' : `tail:${STATUS_HYDRATION_TAIL_LIMIT}`;
5121
5160
 
5161
+ // authority-ok: history-hydration READ routing, not a completion verdict. Selects
5162
+ // the on-disk native transcript vs the materialized-mirror read path; no
5163
+ // completion/stall/redrive decision is taken here.
5122
5164
  if (isNativeSourceCanonicalHistory(canonicalHistory)) {
5123
5165
  const cacheKey = [this.type, this.providerSessionId, this.workingDir, windowTag].join('\0');
5124
5166
  const now = Date.now();
@@ -5188,6 +5230,8 @@ export class CliProviderInstance implements ProviderInstance {
5188
5230
  // transcript so seedSessionHistory can prime dedup state. Pass full so the
5189
5231
  // hydration read is unbounded here (and only here).
5190
5232
  this.syncCanonicalSavedHistoryIfNeeded({ full: true });
5233
+ // authority-ok: history-restore READ routing, not a completion verdict — picks the
5234
+ // native transcript read vs the legacy chat-history read for seeding dedup state.
5191
5235
  const restoredHistory = isNativeSourceCanonicalHistory(this.provider.nativeHistory)
5192
5236
  ? readProviderChatHistory(this.type, {
5193
5237
  canonicalHistory: this.provider.nativeHistory,