@adhdev/daemon-core 1.0.28-rc.11 → 1.0.28-rc.12

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.
@@ -192,7 +192,7 @@ export declare class CliProviderInstance implements ProviderInstance {
192
192
  private meshStallEmittedForAnchor;
193
193
  private meshStallTurnActiveLast;
194
194
  private meshStallLastFiredAt;
195
- private meshStallNativeTranscriptSample;
195
+ private meshStallTranscriptSignalSampled;
196
196
  private busyEpoch;
197
197
  private fastCollapseSynthesizedTaskId;
198
198
  private startupGraceCollapseAt;
@@ -413,10 +413,17 @@ export declare class CliProviderInstance implements ProviderInstance {
413
413
  private completedDebounceTimer;
414
414
  private completedDebouncePending;
415
415
  private lastExternalCompletionProbe;
416
- /** TX-FSM Stage 0 (shadow): lazily-created transcript signal normalizer.
417
- * Fed ONLY by transcript reads this instance already performs — it adds
418
- * zero I/O and its output never feeds back into any verdict. */
416
+ /** TX-FSM: lazily-created transcript signal normalizer. Fed ONLY by
417
+ * transcript reads this instance already performs — it adds zero I/O.
418
+ * Stage 0: its output was a pure shadow observation for the FSM driver.
419
+ * Stage 1: the instance's own stall/growth-hold judgments consume the
420
+ * normalized snapshot too (single source of truth). */
419
421
  private transcriptSignalSource;
422
+ /** TX-FSM Stage 1: the latest snapshot the source produced (set inside
423
+ * publishTranscriptSignalObservation). Consumed the same tick by the
424
+ * stall-path / growth-hold judgments via probeNativeTranscriptSignals —
425
+ * never treated as fresh across ticks. */
426
+ private lastTranscriptSignalSnapshot;
420
427
  /**
421
428
  * The final assistant summary of the last completed turn, cached at
422
429
  * completion-emit time. For a native-source provider (antigravity) whose
@@ -453,15 +460,19 @@ export declare class CliProviderInstance implements ProviderInstance {
453
460
  private spawnedEnvOverrides;
454
461
  private readExternalCompletionMessages;
455
462
  /**
456
- * TX-FSM Stage 0 (shadow): normalize the transcript read that JUST
457
- * happened into a SignalSnapshot and inject it into the FSM driver as a
458
- * pure observation (daemon → SpecCliAdapter → FsmDriver). Fed ONLY by
459
- * reads this method's caller already performs — it adds zero I/O, so the
460
- * getState() zero-native-read invariant and the stall-path read cadence
461
- * are untouched. The snapshot is shadow data: nothing on this path feeds
462
- * back into completion/stall/redrive verdicts. Fail-open end to end a
463
- * missing adapter hook (non-spec providers), an unresolved transcript, or
464
- * any throw degrades to "no observation", never to a wedge.
463
+ * TX-FSM: normalize the transcript read that JUST happened into a
464
+ * SignalSnapshot. Stage 0 injected it into the FSM driver as a pure
465
+ * shadow observation (daemon → SpecCliAdapter → FsmDriver); Stage 1
466
+ * additionally caches it (lastTranscriptSignalSnapshot) so the instance's
467
+ * OWN stall/growth-hold judgments consume the SAME normalized snapshot
468
+ * instead of re-running private transcript scans. Fed ONLY by reads this
469
+ * method's caller already performs it adds zero I/O, so the getState()
470
+ * zero-native-read invariant and the stall-path read cadence are
471
+ * untouched. The source update runs regardless of the adapter hook so a
472
+ * non-spec provider still produces the instance-side snapshot (the FSM
473
+ * injection is simply skipped there). Fail-open end to end — an
474
+ * unresolved transcript or any throw degrades to "no observation", never
475
+ * to a wedge.
465
476
  */
466
477
  private publishTranscriptSignalObservation;
467
478
  /**
@@ -592,23 +603,35 @@ export declare class CliProviderInstance implements ProviderInstance {
592
603
  * starts with a clean slate (a restart is not throttled by a prior stall).
593
604
  */
594
605
  private resetMeshStallEpisode;
595
- /**
596
- * KIMI-MESH-COMPLETION-EMIT (axis 1). For a native-source provider
597
- * (transcriptAuthority=provider its authoritative history is an on-disk
598
- * transcript file, e.g. kimi's wire.jsonl), sample the transcript's current
599
- * progress fingerprint (record count + source-file mtime) WITHOUT parsing the
600
- * PTY. Used by the stall watchdog to distinguish "PTY render is quiet but the
601
- * worker is still doing long tool work (transcript growing)" from a genuine
602
- * wedge. Returns null for a pure-PTY provider (no native source) or when the
603
- * source cannot be resolved this tick the caller then falls back to the
604
- * unchanged lastOutputAt-only judgment.
606
+ /** The result of one native-transcript signal probe: the normalized
607
+ * snapshot the shared TranscriptSignalSource produced from the read, and
608
+ * the very messages it was normalized from (so a judgment site can pull
609
+ * a payload e.g. the final summary — from the SAME read with zero
610
+ * added I/O). */
611
+ /**
612
+ * TX-FSM Stage 1 the single native-transcript signal probe (replaces
613
+ * the Stage-0 sampleNativeTranscriptProgress fingerprint sampler). For a
614
+ * native-source provider (its authoritative history is an on-disk
615
+ * transcript file, e.g. kimi's wire.jsonl), perform the read this
616
+ * judgment point already owns — SAME cadence as before, one
617
+ * readExternalCompletionMessages() per call, never more — and return the
618
+ * NORMALIZED SignalSnapshot the shared TranscriptSignalSource produced
619
+ * from it (publishTranscriptSignalObservation runs inside the read), plus
620
+ * the messages that read returned. Judgment sites (the stall watchdog's
621
+ * transcript-advancing axis, the completion growth-hold, the stall-path
622
+ * completion rescue) consume the snapshot's SIGNALS instead of running
623
+ * their own fingerprint/freshness/final-assistant scans — one source of
624
+ * truth for "what does the transcript say right now".
605
625
  *
606
- * Generalized on the provider's native-source flag, NOT a hardcoded provider
607
- * type, so every current and future pure-PTY long-tool native-source provider
608
- * benefits. Cheap enough for the stall path: it runs only at the stall threshold
609
- * (≥180s of PTY stasis), never on the routine 5s tick.
626
+ * Class gating goes through resolveTranscriptAuthorityProfile ONLY.
627
+ * Returns null for a non-native-source class (nothing to signal from) and
628
+ * a null snapshot when the read threw callers keep their fail-open
629
+ * fallbacks ("couldn't tell" never blocks an idle verdict and never
630
+ * fabricates a completion). Cheap enough for the stall path: it runs
631
+ * only at the stall threshold (≥180s of PTY stasis) or during an armed
632
+ * completion-debounce retry, never on the routine 5s tick.
610
633
  */
611
- private sampleNativeTranscriptProgress;
634
+ private probeNativeTranscriptSignals;
612
635
  /**
613
636
  * KIMI-MESH-COMPLETION-EMIT (axis 2). Last-chance completion emit for a mesh
614
637
  * DELEGATED worker whose PTY has already exited (e.g. killed by a false stall)
@@ -661,6 +684,20 @@ export declare class CliProviderInstance implements ProviderInstance {
661
684
  * false for a daemon-owned provider and for a genuinely mid-turn / wedged
662
685
  * worker with no in-turn final assistant, so a real stall still fires
663
686
  * unchanged. Evidence bar is identical to flushMeshCompletionBeforeCleanup.
687
+ *
688
+ * TX-FSM Stage 1 (FIX 3 probe delegation): for a native-source class the
689
+ * completion VERDICT is the shared TranscriptSignalSource's
690
+ * final_assistant_present signal, normalized from the ONE transcript read
691
+ * the stall watchdog already performed this tick (passed in as
692
+ * `transcriptSignals`) — not a second private read + scan. The emit
693
+ * payload (finalSummary) is extracted from the SAME messages the signal
694
+ * was normalized from, with the SAME turn boundary the legacy path used,
695
+ * so the extraction re-proves the turn scope the signal checked. Fail-open
696
+ * both ways: no usable snapshot (read failed / unresolved / a pure-PTY
697
+ * class, which has no native signal by construction) → the legacy
698
+ * class-appropriate evidence path (completionFinalSummary), unchanged; and
699
+ * a present-but-unextractable signal yields false rather than a
700
+ * payload-less emit.
664
701
  */
665
702
  private tryReconcileTranscriptCompletionForStall;
666
703
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "1.0.28-rc.11",
3
+ "version": "1.0.28-rc.12",
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.11",
53
- "@adhdev/session-host-core": "1.0.28-rc.11",
52
+ "@adhdev/mesh-shared": "1.0.28-rc.12",
53
+ "@adhdev/session-host-core": "1.0.28-rc.12",
54
54
  "@agentclientprotocol/sdk": "^0.16.1",
55
55
  "ajv": "^8.20.0",
56
56
  "ajv-formats": "^3.0.1",
@@ -19,6 +19,7 @@ import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js'
19
19
  import type { MeshSendKeyItem, MeshSendKeyName } from '../cli-adapters/provider-cli-shared.js';
20
20
  import { resolveTranscriptAuthorityProfile } from './transcript-evidence.js';
21
21
  import { TranscriptSignalSource } from './transcript-signal-source.js';
22
+ import type { SignalSnapshot } from './spec/signal-envelope.js';
22
23
  import { createCliAdapter } from './spec/route.js';
23
24
  import type { PtyRuntimeMetadata, PtyTransportFactory } from '../cli-adapters/pty-transport.js';
24
25
  import { StatusMonitor } from './status-monitor.js';
@@ -337,18 +338,20 @@ export class CliProviderInstance implements ProviderInstance {
337
338
  // MESH_WORKER_STALL_REFIRE_COOLDOWN_MS between successive emissions across
338
339
  // anchor re-arms (the per-anchor guard only covers a single continuous stall).
339
340
  private meshStallLastFiredAt = -1;
340
- // KIMI-MESH-COMPLETION-EMIT (axis 1): the native-source transcript progress
341
- // fingerprint (record count + source mtime) observed the LAST time the stall
342
- // watchdog checked a native-source provider (transcriptAuthority=provider e.g.
343
- // kimi wire.jsonl). A pure-PTY native-source worker running a long, screen-quiet
344
- // tool (no viewport bytes for minutes) looks stalled by the lastOutputAt clock
345
- // even though its transcript file is still growing. Before firing the stall, we
346
- // compare the current transcript fingerprint against this snapshot; if the
347
- // transcript advanced, the "stall" is a false positive of PTY-render stasis, so
348
- // we re-arm the anchor instead of paging the coordinator (whose no_progress
349
- // handling can lead to the worker being stopped mid-work → completion never
350
- // emitted). null = not yet sampled for this session/anchor.
351
- private meshStallNativeTranscriptSample: { msgCount: number; sourceMtimeMs: number } | null = null;
341
+ // KIMI-MESH-COMPLETION-EMIT (axis 1) TX-FSM Stage 1: whether the stall
342
+ // watchdog has consumed a usable native-transcript SIGNAL SNAPSHOT for the
343
+ // current stall episode. A pure-PTY native-source worker running a long,
344
+ // screen-quiet tool (no viewport bytes for minutes) looks stalled by the
345
+ // lastOutputAt clock even though its transcript file is still growing.
346
+ // Before firing the stall, the watchdog consults the shared
347
+ // TranscriptSignalSource's in_turn_progress signal; if the transcript is
348
+ // advancing, the "stall" is a false positive of PTY-render stasis, so we
349
+ // re-arm the anchor instead of paging the coordinator (whose no_progress
350
+ // handling can lead to the worker being stopped mid-work → completion
351
+ // never emitted). The FIRST usable sample of an episode still re-arms
352
+ // unconditionally (no episode-scoped baseline exists yet to prove stasis
353
+ // against) — the historical `!prev → advanced` semantics, preserved.
354
+ private meshStallTranscriptSignalSampled = false;
352
355
  // FALSE-IDLE continuity epoch: monotonically bumped on EVERY entry into a busy
353
356
  // phase (→generating or →waiting_approval). The completedDebouncePending snapshots
354
357
  // this value at arm time (busyEpochAtArm); the flush guard requires it UNCHANGED
@@ -1427,10 +1430,17 @@ export class CliProviderInstance implements ProviderInstance {
1427
1430
  private completedDebounceTimer: NodeJS.Timeout | null = null;
1428
1431
  private completedDebouncePending: CompletedDebouncePending | null = null;
1429
1432
  private lastExternalCompletionProbe: ExternalTranscriptProbe | null = null;
1430
- /** TX-FSM Stage 0 (shadow): lazily-created transcript signal normalizer.
1431
- * Fed ONLY by transcript reads this instance already performs — it adds
1432
- * zero I/O and its output never feeds back into any verdict. */
1433
+ /** TX-FSM: lazily-created transcript signal normalizer. Fed ONLY by
1434
+ * transcript reads this instance already performs — it adds zero I/O.
1435
+ * Stage 0: its output was a pure shadow observation for the FSM driver.
1436
+ * Stage 1: the instance's own stall/growth-hold judgments consume the
1437
+ * normalized snapshot too (single source of truth). */
1433
1438
  private transcriptSignalSource: TranscriptSignalSource | null = null;
1439
+ /** TX-FSM Stage 1: the latest snapshot the source produced (set inside
1440
+ * publishTranscriptSignalObservation). Consumed the same tick by the
1441
+ * stall-path / growth-hold judgments via probeNativeTranscriptSignals —
1442
+ * never treated as fresh across ticks. */
1443
+ private lastTranscriptSignalSnapshot: SignalSnapshot | null = null;
1434
1444
  /**
1435
1445
  * The final assistant summary of the last completed turn, cached at
1436
1446
  * completion-emit time. For a native-source provider (antigravity) whose
@@ -1667,19 +1677,21 @@ export class CliProviderInstance implements ProviderInstance {
1667
1677
  }
1668
1678
 
1669
1679
  /**
1670
- * TX-FSM Stage 0 (shadow): normalize the transcript read that JUST
1671
- * happened into a SignalSnapshot and inject it into the FSM driver as a
1672
- * pure observation (daemon → SpecCliAdapter → FsmDriver). Fed ONLY by
1673
- * reads this method's caller already performs — it adds zero I/O, so the
1674
- * getState() zero-native-read invariant and the stall-path read cadence
1675
- * are untouched. The snapshot is shadow data: nothing on this path feeds
1676
- * back into completion/stall/redrive verdicts. Fail-open end to end a
1677
- * missing adapter hook (non-spec providers), an unresolved transcript, or
1678
- * any throw degrades to "no observation", never to a wedge.
1680
+ * TX-FSM: normalize the transcript read that JUST happened into a
1681
+ * SignalSnapshot. Stage 0 injected it into the FSM driver as a pure
1682
+ * shadow observation (daemon → SpecCliAdapter → FsmDriver); Stage 1
1683
+ * additionally caches it (lastTranscriptSignalSnapshot) so the instance's
1684
+ * OWN stall/growth-hold judgments consume the SAME normalized snapshot
1685
+ * instead of re-running private transcript scans. Fed ONLY by reads this
1686
+ * method's caller already performs it adds zero I/O, so the getState()
1687
+ * zero-native-read invariant and the stall-path read cadence are
1688
+ * untouched. The source update runs regardless of the adapter hook so a
1689
+ * non-spec provider still produces the instance-side snapshot (the FSM
1690
+ * injection is simply skipped there). Fail-open end to end — an
1691
+ * unresolved transcript or any throw degrades to "no observation", never
1692
+ * to a wedge.
1679
1693
  */
1680
1694
  private publishTranscriptSignalObservation(messages: unknown[] | null, error = false): void {
1681
- const adapter = this.adapter as { setSignalObservation?: (snapshot: unknown) => void } | null | undefined;
1682
- if (typeof adapter?.setSignalObservation !== 'function') return; // non-spec path: no FSM consumer
1683
1695
  try {
1684
1696
  if (!this.transcriptSignalSource) {
1685
1697
  this.transcriptSignalSource = new TranscriptSignalSource({
@@ -1689,7 +1701,13 @@ export class CliProviderInstance implements ProviderInstance {
1689
1701
  profile: resolveTranscriptAuthorityProfile(this.provider),
1690
1702
  turnStartedAt: () => {
1691
1703
  const t = (this.adapter as any)?.currentTurnStartedAt;
1692
- return typeof t === 'number' && Number.isFinite(t) ? t : undefined;
1704
+ if (typeof t === 'number' && Number.isFinite(t)) return t;
1705
+ // Mesh fallback: for an emitsPtyTurnEvents=false worker
1706
+ // (idle→idle collapse) currentTurnStartedAt may never
1707
+ // bind; scope to the task injection instead — the SAME
1708
+ // boundary the stall-path rescue uses, so the signal and
1709
+ // the rescue's payload extraction agree on the turn.
1710
+ return this.meshTaskInjectedAt > 0 ? this.meshTaskInjectedAt : undefined;
1693
1711
  },
1694
1712
  // Reuse the exact completion machinery (I1) for the
1695
1713
  // final_assistant_present signal rather than duplicating
@@ -1701,8 +1719,12 @@ export class CliProviderInstance implements ProviderInstance {
1701
1719
  const snapshot = this.transcriptSignalSource.update(
1702
1720
  { messages, probe: this.lastExternalCompletionProbe, error },
1703
1721
  );
1704
- adapter.setSignalObservation(snapshot);
1705
- } catch { /* shadow-only: signal collection must never break the read path */ }
1722
+ this.lastTranscriptSignalSnapshot = snapshot;
1723
+ const adapter = this.adapter as { setSignalObservation?: (snapshot: unknown) => void } | null | undefined;
1724
+ if (typeof adapter?.setSignalObservation === 'function') {
1725
+ adapter.setSignalObservation(snapshot);
1726
+ }
1727
+ } catch { /* signal collection must never break the read path */ }
1706
1728
  }
1707
1729
 
1708
1730
  /**
@@ -2552,31 +2574,38 @@ export class CliProviderInstance implements ProviderInstance {
2552
2574
  const stalledMs = now - this.meshStallAnchorAt;
2553
2575
  if (stalledMs < threshold) return;
2554
2576
 
2555
- // KIMI-MESH-COMPLETION-EMIT (axis 1): transcript-aware stall for native-source
2556
- // providers. A pure-PTY native-source worker (transcriptAuthority=provider,
2557
- // e.g. kimi) running a long, screen-quiet tool emits no viewport bytes for
2558
- // minutes, so the lastOutputAt clock above reads it as stalled — but its
2559
- // authoritative transcript (wire.jsonl) is still being appended. Firing
2560
- // monitor:no_progress here surfaces a false stall to the coordinator whose
2561
- // downstream handling can stop the worker mid-work, killing the emit window
2562
- // before the real completion propagates. So before firing, sample the native
2563
- // transcript's progress fingerprint: if it advanced (more records OR a fresher
2564
- // source mtime) since the last sample, the worker is demonstrably alive — treat
2565
- // it as progress. Re-arm the anchor to `now` and reset the episode so the clock
2566
- // restarts from this proven-live moment; only a transcript that is ALSO static
2567
- // falls through to the genuine-stall fire below. No-op for pure-PTY providers
2568
- // (sample is null unchanged behavior).
2569
- const nativeSample = this.sampleNativeTranscriptProgress();
2570
- if (nativeSample) {
2571
- const prev = this.meshStallNativeTranscriptSample;
2572
- const advanced = !prev
2573
- || nativeSample.msgCount > prev.msgCount
2574
- || nativeSample.sourceMtimeMs > prev.sourceMtimeMs;
2575
- this.meshStallNativeTranscriptSample = nativeSample;
2576
- if (advanced) {
2577
+ // KIMI-MESH-COMPLETION-EMIT (axis 1) — TX-FSM Stage 1: transcript-aware
2578
+ // stall for native-source providers, now delegated to the SHARED
2579
+ // TranscriptSignalSource. A pure-PTY native-source worker
2580
+ // (transcriptAuthority=provider, e.g. kimi) running a long, screen-quiet
2581
+ // tool emits no viewport bytes for minutes, so the lastOutputAt clock
2582
+ // above reads it as stalled but its authoritative transcript
2583
+ // (wire.jsonl) is still being appended. Firing monitor:no_progress here
2584
+ // surfaces a false stall to the coordinator whose downstream handling
2585
+ // can stop the worker mid-work, killing the emit window before the real
2586
+ // completion propagates. So before firing, consume the signal source's
2587
+ // in_turn_progress (transcript advanced since the previous sample OR
2588
+ // source mtime inside the growth-quiet window the same liveness test
2589
+ // the completion growth-hold uses, normalized from the ONE read this
2590
+ // tick): if true, the worker is demonstrably alive treat it as
2591
+ // progress. Re-arm the anchor to `now` and reset the episode so the
2592
+ // clock restarts from this proven-live moment; only a transcript that
2593
+ // is ALSO static falls through to the genuine-stall fire below. The
2594
+ // FIRST usable sample of an episode re-arms unconditionally — no
2595
+ // episode-scoped baseline exists yet to prove stasis against (the
2596
+ // historical `!prev → advanced` semantics). Fail-open: a non-native-
2597
+ // source class, an unresolved transcript, or a read error yields no
2598
+ // usable snapshot → fall through to the unchanged lastOutputAt-only
2599
+ // judgment.
2600
+ const transcriptSignals = this.probeNativeTranscriptSignals();
2601
+ if (transcriptSignals?.snapshot?.available === true) {
2602
+ const firstSampleThisEpisode = !this.meshStallTranscriptSignalSampled;
2603
+ this.meshStallTranscriptSignalSampled = true;
2604
+ const signalDetail = transcriptSignals.snapshot.detail;
2605
+ if (firstSampleThisEpisode || transcriptSignals.snapshot.signals.in_turn_progress === true) {
2577
2606
  if (this.isMeshWorkerSession()) {
2578
2607
  traceMeshEventDrop('mesh_worker_stall_transcript_advancing', this.meshTraceCtx('monitor:no_progress'),
2579
- `msgCount=${nativeSample.msgCount} sourceMtime=${nativeSample.sourceMtimeMs} (PTY quiet ${Math.round(stalledMs / 1000)}s but transcript advancing)`);
2608
+ `msgCount=${signalDetail.msgCount} sourceMtime=${signalDetail.sourceMtimeMs} (PTY quiet ${Math.round(stalledMs / 1000)}s but transcript advancing)`);
2580
2609
  }
2581
2610
  this.meshStallAnchorAt = now;
2582
2611
  this.meshStallEmittedForAnchor = false;
@@ -2591,11 +2620,12 @@ export class CliProviderInstance implements ProviderInstance {
2591
2620
  // collapse) sits at a STATIC idle prompt; the status-agnostic watchdog
2592
2621
  // would misread that finished-but-quiet state as a wedge and false-fire
2593
2622
  // monitor:no_progress. If the class-appropriate transcript (PTY parse or
2594
- // authoritative native history completionFinalSummary picks) proves a
2595
- // finished turn, emit the missing completion and SUPPRESS the stall. A
2596
- // genuinely mid-turn / wedged worker with no in-turn final assistant falls
2597
- // through and the real stall fires below unchanged.
2598
- if (this.tryReconcileTranscriptCompletionForStall(observedStatus)) {
2623
+ // authoritative native history) proves a finished turn, emit the missing
2624
+ // completion and SUPPRESS the stall. A genuinely mid-turn / wedged worker
2625
+ // with no in-turn final assistant falls through and the real stall fires
2626
+ // below unchanged. TX-FSM Stage 1: the native-source verdict consumes the
2627
+ // SAME snapshot axis 1 just produced — no second read.
2628
+ if (this.tryReconcileTranscriptCompletionForStall(observedStatus, transcriptSignals)) {
2599
2629
  this.meshStallEmittedForAnchor = true;
2600
2630
  return;
2601
2631
  }
@@ -2650,49 +2680,51 @@ export class CliProviderInstance implements ProviderInstance {
2650
2680
  this.meshStallEmittedForAnchor = false;
2651
2681
  this.meshStallTurnActiveLast = undefined;
2652
2682
  this.meshStallLastFiredAt = -1;
2653
- this.meshStallNativeTranscriptSample = null;
2683
+ this.meshStallTranscriptSignalSampled = false;
2654
2684
  }
2655
2685
 
2686
+ /** The result of one native-transcript signal probe: the normalized
2687
+ * snapshot the shared TranscriptSignalSource produced from the read, and
2688
+ * the very messages it was normalized from (so a judgment site can pull
2689
+ * a payload — e.g. the final summary — from the SAME read with zero
2690
+ * added I/O). */
2656
2691
  /**
2657
- * KIMI-MESH-COMPLETION-EMIT (axis 1). For a native-source provider
2658
- * (transcriptAuthority=provider its authoritative history is an on-disk
2659
- * transcript file, e.g. kimi's wire.jsonl), sample the transcript's current
2660
- * progress fingerprint (record count + source-file mtime) WITHOUT parsing the
2661
- * PTY. Used by the stall watchdog to distinguish "PTY render is quiet but the
2662
- * worker is still doing long tool work (transcript growing)" from a genuine
2663
- * wedge. Returns null for a pure-PTY provider (no native source) or when the
2664
- * source cannot be resolved this tick — the caller then falls back to the
2665
- * unchanged lastOutputAt-only judgment.
2692
+ * TX-FSM Stage 1 the single native-transcript signal probe (replaces
2693
+ * the Stage-0 sampleNativeTranscriptProgress fingerprint sampler). For a
2694
+ * native-source provider (its authoritative history is an on-disk
2695
+ * transcript file, e.g. kimi's wire.jsonl), perform the read this
2696
+ * judgment point already owns SAME cadence as before, one
2697
+ * readExternalCompletionMessages() per call, never more and return the
2698
+ * NORMALIZED SignalSnapshot the shared TranscriptSignalSource produced
2699
+ * from it (publishTranscriptSignalObservation runs inside the read), plus
2700
+ * the messages that read returned. Judgment sites (the stall watchdog's
2701
+ * transcript-advancing axis, the completion growth-hold, the stall-path
2702
+ * completion rescue) consume the snapshot's SIGNALS instead of running
2703
+ * their own fingerprint/freshness/final-assistant scans — one source of
2704
+ * truth for "what does the transcript say right now".
2666
2705
  *
2667
- * Generalized on the provider's native-source flag, NOT a hardcoded provider
2668
- * type, so every current and future pure-PTY long-tool native-source provider
2669
- * benefits. Cheap enough for the stall path: it runs only at the stall threshold
2670
- * (≥180s of PTY stasis), never on the routine 5s tick.
2706
+ * Class gating goes through resolveTranscriptAuthorityProfile ONLY.
2707
+ * Returns null for a non-native-source class (nothing to signal from) and
2708
+ * a null snapshot when the read threw callers keep their fail-open
2709
+ * fallbacks ("couldn't tell" never blocks an idle verdict and never
2710
+ * fabricates a completion). Cheap enough for the stall path: it runs
2711
+ * only at the stall threshold (≥180s of PTY stasis) or during an armed
2712
+ * completion-debounce retry, never on the routine 5s tick.
2671
2713
  */
2672
- private sampleNativeTranscriptProgress(): { msgCount: number; sourceMtimeMs: number } | null {
2673
- // authority-ok: native fingerprint SAMPLING, not a stall verdict. This only
2674
- // decides whether a native source exists to sample a progress fingerprint from;
2675
- // the caller (checkMeshWorkerStall) makes the stall/no-progress decision over the
2676
- // returned fingerprint. Non-native-source classes have nothing to sample here and
2677
- // fall back to the unchanged lastOutputAt-only judgment.
2678
- if (!isNativeSourceCanonicalHistory(this.provider?.nativeHistory)) return null;
2714
+ private probeNativeTranscriptSignals(): { snapshot: SignalSnapshot | null; messages: unknown[] | null } | null {
2715
+ if (resolveTranscriptAuthorityProfile(this.provider).class !== 'native-source') return null;
2679
2716
  // readExternalCompletionMessages resolves this session's OWN native-source
2680
2717
  // conversation (providerSessionId / persisted pin / floor claim) and, as a
2681
- // side effect, refreshes this.lastExternalCompletionProbe with the source
2682
- // path, mtime, and message count. Reusing it keeps the resolution logic in
2683
- // one place and immune to the antigravity-style session-id quirks.
2718
+ // side effect, feeds the shared TranscriptSignalSource (which refreshes
2719
+ // this.lastTranscriptSignalSnapshot). Reusing it keeps the resolution
2720
+ // logic in one place and immune to the antigravity-style session-id quirks.
2684
2721
  let messages: unknown[] | null = null;
2685
2722
  try {
2686
2723
  messages = this.readExternalCompletionMessages();
2687
2724
  } catch {
2688
- return null; // best-effort: an unresolved transcript → fall back to PTY clock
2725
+ return { snapshot: null, messages: null }; // best-effort: fail-open
2689
2726
  }
2690
- const probe = this.lastExternalCompletionProbe;
2691
- if (!probe) return null;
2692
- return {
2693
- msgCount: typeof probe.msgCount === 'number' ? probe.msgCount : (Array.isArray(messages) ? messages.length : 0),
2694
- sourceMtimeMs: typeof probe.sourceMtimeMs === 'number' ? probe.sourceMtimeMs : 0,
2695
- };
2727
+ return { snapshot: this.lastTranscriptSignalSnapshot, messages };
2696
2728
  }
2697
2729
 
2698
2730
  /**
@@ -2795,8 +2827,25 @@ export class CliProviderInstance implements ProviderInstance {
2795
2827
  * false for a daemon-owned provider and for a genuinely mid-turn / wedged
2796
2828
  * worker with no in-turn final assistant, so a real stall still fires
2797
2829
  * unchanged. Evidence bar is identical to flushMeshCompletionBeforeCleanup.
2830
+ *
2831
+ * TX-FSM Stage 1 (FIX 3 probe delegation): for a native-source class the
2832
+ * completion VERDICT is the shared TranscriptSignalSource's
2833
+ * final_assistant_present signal, normalized from the ONE transcript read
2834
+ * the stall watchdog already performed this tick (passed in as
2835
+ * `transcriptSignals`) — not a second private read + scan. The emit
2836
+ * payload (finalSummary) is extracted from the SAME messages the signal
2837
+ * was normalized from, with the SAME turn boundary the legacy path used,
2838
+ * so the extraction re-proves the turn scope the signal checked. Fail-open
2839
+ * both ways: no usable snapshot (read failed / unresolved / a pure-PTY
2840
+ * class, which has no native signal by construction) → the legacy
2841
+ * class-appropriate evidence path (completionFinalSummary), unchanged; and
2842
+ * a present-but-unextractable signal yields false rather than a
2843
+ * payload-less emit.
2798
2844
  */
2799
- private tryReconcileTranscriptCompletionForStall(observedStatus: string): boolean {
2845
+ private tryReconcileTranscriptCompletionForStall(
2846
+ observedStatus: string,
2847
+ transcriptSignals?: { snapshot: SignalSnapshot | null; messages: unknown[] | null } | null,
2848
+ ): boolean {
2800
2849
  // daemon-owned transcripts get real PTY turn events — an idle-quiet
2801
2850
  // daemon-owned worker with no completion emit is a genuine anomaly the
2802
2851
  // stall should surface, exactly as before this unification.
@@ -2825,14 +2874,29 @@ export class CliProviderInstance implements ProviderInstance {
2825
2874
  try {
2826
2875
  parsedMessages = this.adapter?.getScriptParsedStatus?.()?.messages;
2827
2876
  } catch { parsedMessages = undefined; }
2828
- // completionFinalSummary turn-scopes the class-appropriate transcript (native
2829
- // history for a native-source provider, the PTY parse otherwise) — a stale
2830
- // prior-turn tail or a mid-turn worker with no in-turn final assistant yields
2831
- // '' return false below.
2877
+ // TX-FSM Stage 1: a native-source class with a usable signal snapshot
2878
+ // takes its verdict from the SHARED signal source (final_assistant_present),
2879
+ // and the payload from the SAME messages zero added I/O. Every other
2880
+ // case falls back to completionFinalSummary, which turn-scopes the
2881
+ // class-appropriate transcript (native history for a native-source
2882
+ // provider, the PTY parse otherwise) — a stale prior-turn tail or a
2883
+ // mid-turn worker with no in-turn final assistant yields '' → return
2884
+ // false below.
2832
2885
  let finalSummary: string | undefined;
2833
- try {
2834
- finalSummary = this.completionFinalSummary(parsedMessages, turnStartedAt);
2835
- } catch { finalSummary = undefined; }
2886
+ const signalSnapshot = transcriptSignals?.snapshot;
2887
+ if (profile.class === 'native-source' && signalSnapshot?.available === true) {
2888
+ // The signal is the verdict; the turn-scoped extraction re-proves
2889
+ // the scope and supplies the emit payload from the same read.
2890
+ if (signalSnapshot.signals.final_assistant_present !== true) return false;
2891
+ finalSummary = extractFinalSummaryFromMessagesAfter(
2892
+ (Array.isArray(transcriptSignals?.messages) ? transcriptSignals.messages : []) as any,
2893
+ turnStartedAt,
2894
+ ) || undefined;
2895
+ } else {
2896
+ try {
2897
+ finalSummary = this.completionFinalSummary(parsedMessages, turnStartedAt);
2898
+ } catch { finalSummary = undefined; }
2899
+ }
2836
2900
  // No in-turn final assistant evidence → not a proven turn-end. Let the real
2837
2901
  // stall fire so a genuinely-wedged worker is still surfaced.
2838
2902
  if (!finalSummary) return false;
@@ -3214,20 +3278,27 @@ export class CliProviderInstance implements ProviderInstance {
3214
3278
  // untouched; claude-cli's write-lag CANON-C immediate emit is likewise
3215
3279
  // untouched (its block deliberately omits noExternalTranscriptSource).
3216
3280
  if (blockReason === 'missing_final_assistant' && block.noExternalTranscriptSource === true) {
3217
- let nativeSample: { msgCount: number; sourceMtimeMs: number } | null = null;
3218
- try { nativeSample = this.sampleNativeTranscriptProgress(); } catch { nativeSample = null; }
3219
- const sourceMtimeMs = nativeSample?.sourceMtimeMs ?? 0;
3220
- if (nativeSample && sourceMtimeMs > 0 && Date.now() - sourceMtimeMs < MISSING_ASSISTANT_TRANSCRIPT_GROWTH_QUIET_MS) {
3281
+ // TX-FSM Stage 1: the freshness judgment is the shared signal
3282
+ // source's transcript_growing signal (same growth-quiet window,
3283
+ // normalized from the ONE re-probe this retry performs) instead
3284
+ // of a private mtime scan. Fail-open identically: no signal
3285
+ // (non-native-source / unresolved / read error / unknown mtime)
3286
+ // → falls through to the unchanged floor/cap logic.
3287
+ let growthSnapshot: SignalSnapshot | null = null;
3288
+ try { growthSnapshot = this.probeNativeTranscriptSignals()?.snapshot ?? null; } catch { growthSnapshot = null; }
3289
+ if (growthSnapshot?.available === true && growthSnapshot.signals.transcript_growing === true) {
3290
+ const growthDetail = growthSnapshot.detail;
3291
+ const mtimeAgeMs = growthDetail.ageMs ?? 0; // growing ⇒ ageMs non-null
3221
3292
  if (pending.loggedBlockReason !== 'native_transcript_advancing') {
3222
- LOG.info('CLI', `[${this.type}] holding pending completed (native_transcript_advancing: msgCount=${nativeSample.msgCount} mtimeAge=${Date.now() - sourceMtimeMs}ms < ${MISSING_ASSISTANT_TRANSCRIPT_GROWTH_QUIET_MS}ms) — transcript still growing, screen-idle verdict not trusted`);
3293
+ LOG.info('CLI', `[${this.type}] holding pending completed (native_transcript_advancing: msgCount=${growthDetail.msgCount} mtimeAge=${mtimeAgeMs}ms < ${MISSING_ASSISTANT_TRANSCRIPT_GROWTH_QUIET_MS}ms) — transcript still growing, screen-idle verdict not trusted`);
3223
3294
  if (this.isMeshWorkerSession()) {
3224
- traceMeshEventDrop('completion_gate_hold', this.meshTraceCtx(), `native_transcript_advancing msgCount=${nativeSample.msgCount} mtimeAge=${Date.now() - sourceMtimeMs}ms`);
3295
+ traceMeshEventDrop('completion_gate_hold', this.meshTraceCtx(), `native_transcript_advancing msgCount=${growthDetail.msgCount} mtimeAge=${mtimeAgeMs}ms`);
3225
3296
  }
3226
3297
  if (this.completionTraceOn()) this.recordCompletionGateTrace('hold', {
3227
3298
  blockReason: 'native_transcript_advancing',
3228
3299
  latestVisibleStatus,
3229
- msgCount: nativeSample.msgCount,
3230
- sourceMtimeAgeMs: Date.now() - sourceMtimeMs,
3300
+ msgCount: growthDetail.msgCount,
3301
+ sourceMtimeAgeMs: mtimeAgeMs,
3231
3302
  growthQuietMs: MISSING_ASSISTANT_TRANSCRIPT_GROWTH_QUIET_MS,
3232
3303
  waitedMs,
3233
3304
  });
@@ -1,12 +1,21 @@
1
1
  /**
2
- * TranscriptSignalSource — TX-FSM Stage 0 (shadow), daemon side.
2
+ * TranscriptSignalSource — TX-FSM transcript signal normalizer, daemon side.
3
3
  *
4
4
  * Owns the collection/normalization half of the dual-source redesign: given
5
5
  * the daemon's EXISTING native-transcript reads (file discovery + session pin
6
6
  * + parsing all stay in CliProviderInstance.readExternalCompletionMessages,
7
7
  * which resolves providerSessionId / persisted pins / floor-claims), this
8
8
  * source normalizes each read into the provider-agnostic SignalSnapshot
9
- * envelope (spec/signal-envelope.ts) that gets injected into the FsmDriver.
9
+ * envelope (spec/signal-envelope.ts).
10
+ *
11
+ * Consumers:
12
+ * - Stage 0 (shadow): the snapshot is injected into the FsmDriver as a pure
13
+ * observation; `signal` conditions stay pass-through there (fsm-evaluator).
14
+ * - Stage 1 (delegation): the instance's OWN stall/growth-hold judgments
15
+ * (checkMeshWorkerStall's transcript-advancing axis, the TRANSCRIPT-
16
+ * GROWTH-HOLD, and tryReconcileTranscriptCompletionForStall — the FIX 3
17
+ * probe) consume the SAME normalized snapshot instead of running private
18
+ * transcript scans. The FSM-side consumption remains shadow/pass-through.
10
19
  *
11
20
  * Hard contracts:
12
21
  * - ZERO added I/O. update() is fed by transcript reads the daemon already
@@ -16,10 +25,9 @@
16
25
  * P0 choke point) — never raw isNativeSourceCanonicalHistory /
17
26
  * isPurePtyTranscriptProvider calls, and never a provider-name branch.
18
27
  * - Fail-open: a non-native-source class, an unresolved transcript, or a
19
- * read error yields an unavailable snapshot — the FSM side treats every
20
- * signal as null and every signal condition passes through.
21
- * - SHADOW ONLY: the emitted shadow log line is the Stage-0 deliverable (the
22
- * Stage 1-3 judgment data). Nothing here feeds back into any verdict.
28
+ * read error yields an unavailable snapshot — consumers treat every signal
29
+ * as null and keep their pre-delegation fallbacks; a missing signal must
30
+ * never wedge a session or fabricate a completion.
23
31
  * - No approval signal (Stage 4 scope — see signal-envelope.ts).
24
32
  */
25
33
  'use strict';