@adhdev/daemon-core 1.0.28-rc.10 → 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.
- package/dist/cli-adapter-types.d.ts +7 -0
- package/dist/commands/upgrade-helper.d.ts +14 -0
- package/dist/index.js +623 -84
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +623 -84
- package/dist/index.mjs.map +1 -1
- package/dist/providers/cli-provider-instance.d.ts +69 -16
- package/dist/providers/spec/cli-adapter.d.ts +7 -0
- package/dist/providers/spec/fsm-driver.d.ts +35 -0
- package/dist/providers/spec/fsm-evaluator.d.ts +28 -2
- package/dist/providers/spec/fsm-types.d.ts +19 -1
- package/dist/providers/spec/signal-envelope.d.ts +59 -0
- package/dist/providers/transcript-signal-source.d.ts +59 -0
- package/package.json +3 -3
- package/src/cli-adapter-types.ts +7 -0
- package/src/commands/low-family/daemon-lifecycle.ts +41 -0
- package/src/commands/med-family/mesh-restart.ts +233 -30
- package/src/commands/upgrade-helper.ts +34 -3
- package/src/providers/cli-provider-instance.ts +204 -87
- package/src/providers/spec/cli-adapter.ts +10 -0
- package/src/providers/spec/fsm-driver.ts +62 -1
- package/src/providers/spec/fsm-evaluator.ts +110 -5
- package/src/providers/spec/fsm-loader.ts +7 -0
- package/src/providers/spec/fsm-types.ts +20 -0
- package/src/providers/spec/signal-envelope.ts +136 -0
- package/src/providers/transcript-signal-source.ts +179 -0
|
@@ -18,6 +18,8 @@ import { shortHash } from '../system/hash.js';
|
|
|
18
18
|
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
|
+
import { TranscriptSignalSource } from './transcript-signal-source.js';
|
|
22
|
+
import type { SignalSnapshot } from './spec/signal-envelope.js';
|
|
21
23
|
import { createCliAdapter } from './spec/route.js';
|
|
22
24
|
import type { PtyRuntimeMetadata, PtyTransportFactory } from '../cli-adapters/pty-transport.js';
|
|
23
25
|
import { StatusMonitor } from './status-monitor.js';
|
|
@@ -336,18 +338,20 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
336
338
|
// MESH_WORKER_STALL_REFIRE_COOLDOWN_MS between successive emissions across
|
|
337
339
|
// anchor re-arms (the per-anchor guard only covers a single continuous stall).
|
|
338
340
|
private meshStallLastFiredAt = -1;
|
|
339
|
-
// KIMI-MESH-COMPLETION-EMIT (axis 1)
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
// handling can lead to the worker being stopped mid-work → completion
|
|
349
|
-
// emitted).
|
|
350
|
-
|
|
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;
|
|
351
355
|
// FALSE-IDLE continuity epoch: monotonically bumped on EVERY entry into a busy
|
|
352
356
|
// phase (→generating or →waiting_approval). The completedDebouncePending snapshots
|
|
353
357
|
// this value at arm time (busyEpochAtArm); the flush guard requires it UNCHANGED
|
|
@@ -1426,6 +1430,17 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1426
1430
|
private completedDebounceTimer: NodeJS.Timeout | null = null;
|
|
1427
1431
|
private completedDebouncePending: CompletedDebouncePending | null = null;
|
|
1428
1432
|
private lastExternalCompletionProbe: ExternalTranscriptProbe | null = null;
|
|
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). */
|
|
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;
|
|
1429
1444
|
/**
|
|
1430
1445
|
* The final assistant summary of the last completed turn, cached at
|
|
1431
1446
|
* completion-emit time. For a native-source provider (antigravity) whose
|
|
@@ -1649,6 +1664,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1649
1664
|
});
|
|
1650
1665
|
if (restoredHistory.source !== 'provider-native') {
|
|
1651
1666
|
this.lastExternalCompletionProbe = null;
|
|
1667
|
+
this.publishTranscriptSignalObservation(null);
|
|
1652
1668
|
return null;
|
|
1653
1669
|
}
|
|
1654
1670
|
this.lastExternalCompletionProbe = buildExternalTranscriptProbe(
|
|
@@ -1656,9 +1672,61 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1656
1672
|
restoredHistory.sourcePath,
|
|
1657
1673
|
restoredHistory.sourceMtimeMs,
|
|
1658
1674
|
);
|
|
1675
|
+
this.publishTranscriptSignalObservation(restoredHistory.messages);
|
|
1659
1676
|
return restoredHistory.messages;
|
|
1660
1677
|
}
|
|
1661
1678
|
|
|
1679
|
+
/**
|
|
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.
|
|
1693
|
+
*/
|
|
1694
|
+
private publishTranscriptSignalObservation(messages: unknown[] | null, error = false): void {
|
|
1695
|
+
try {
|
|
1696
|
+
if (!this.transcriptSignalSource) {
|
|
1697
|
+
this.transcriptSignalSource = new TranscriptSignalSource({
|
|
1698
|
+
label: this.type,
|
|
1699
|
+
// Choke point: class/timing come from the P0 profile
|
|
1700
|
+
// resolver, never from raw predicates or provider names.
|
|
1701
|
+
profile: resolveTranscriptAuthorityProfile(this.provider),
|
|
1702
|
+
turnStartedAt: () => {
|
|
1703
|
+
const t = (this.adapter as any)?.currentTurnStartedAt;
|
|
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;
|
|
1711
|
+
},
|
|
1712
|
+
// Reuse the exact completion machinery (I1) for the
|
|
1713
|
+
// final_assistant_present signal rather than duplicating
|
|
1714
|
+
// the message scan.
|
|
1715
|
+
finalAssistantPresent: (msgs, ts) => this.completionHasFinalAssistantMessage(msgs, ts),
|
|
1716
|
+
growthQuietMs: MISSING_ASSISTANT_TRANSCRIPT_GROWTH_QUIET_MS,
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
const snapshot = this.transcriptSignalSource.update(
|
|
1720
|
+
{ messages, probe: this.lastExternalCompletionProbe, error },
|
|
1721
|
+
);
|
|
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 */ }
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1662
1730
|
/**
|
|
1663
1731
|
* The content of the LAST visible assistant bubble in a message list, or ''
|
|
1664
1732
|
* when the tail is not an assistant reply. Skips trailing system/tool/activity
|
|
@@ -2506,31 +2574,38 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2506
2574
|
const stalledMs = now - this.meshStallAnchorAt;
|
|
2507
2575
|
if (stalledMs < threshold) return;
|
|
2508
2576
|
|
|
2509
|
-
// KIMI-MESH-COMPLETION-EMIT (axis 1)
|
|
2510
|
-
//
|
|
2511
|
-
//
|
|
2512
|
-
//
|
|
2513
|
-
//
|
|
2514
|
-
//
|
|
2515
|
-
//
|
|
2516
|
-
//
|
|
2517
|
-
//
|
|
2518
|
-
//
|
|
2519
|
-
//
|
|
2520
|
-
//
|
|
2521
|
-
//
|
|
2522
|
-
//
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
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) {
|
|
2531
2606
|
if (this.isMeshWorkerSession()) {
|
|
2532
2607
|
traceMeshEventDrop('mesh_worker_stall_transcript_advancing', this.meshTraceCtx('monitor:no_progress'),
|
|
2533
|
-
`msgCount=${
|
|
2608
|
+
`msgCount=${signalDetail.msgCount} sourceMtime=${signalDetail.sourceMtimeMs} (PTY quiet ${Math.round(stalledMs / 1000)}s but transcript advancing)`);
|
|
2534
2609
|
}
|
|
2535
2610
|
this.meshStallAnchorAt = now;
|
|
2536
2611
|
this.meshStallEmittedForAnchor = false;
|
|
@@ -2545,11 +2620,12 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2545
2620
|
// collapse) sits at a STATIC idle prompt; the status-agnostic watchdog
|
|
2546
2621
|
// would misread that finished-but-quiet state as a wedge and false-fire
|
|
2547
2622
|
// monitor:no_progress. If the class-appropriate transcript (PTY parse or
|
|
2548
|
-
// authoritative native history
|
|
2549
|
-
//
|
|
2550
|
-
//
|
|
2551
|
-
//
|
|
2552
|
-
|
|
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)) {
|
|
2553
2629
|
this.meshStallEmittedForAnchor = true;
|
|
2554
2630
|
return;
|
|
2555
2631
|
}
|
|
@@ -2604,49 +2680,51 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2604
2680
|
this.meshStallEmittedForAnchor = false;
|
|
2605
2681
|
this.meshStallTurnActiveLast = undefined;
|
|
2606
2682
|
this.meshStallLastFiredAt = -1;
|
|
2607
|
-
this.
|
|
2683
|
+
this.meshStallTranscriptSignalSampled = false;
|
|
2608
2684
|
}
|
|
2609
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). */
|
|
2610
2691
|
/**
|
|
2611
|
-
*
|
|
2612
|
-
*
|
|
2613
|
-
*
|
|
2614
|
-
*
|
|
2615
|
-
*
|
|
2616
|
-
*
|
|
2617
|
-
*
|
|
2618
|
-
*
|
|
2619
|
-
*
|
|
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".
|
|
2620
2705
|
*
|
|
2621
|
-
*
|
|
2622
|
-
*
|
|
2623
|
-
*
|
|
2624
|
-
* (
|
|
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.
|
|
2625
2713
|
*/
|
|
2626
|
-
private
|
|
2627
|
-
|
|
2628
|
-
// decides whether a native source exists to sample a progress fingerprint from;
|
|
2629
|
-
// the caller (checkMeshWorkerStall) makes the stall/no-progress decision over the
|
|
2630
|
-
// returned fingerprint. Non-native-source classes have nothing to sample here and
|
|
2631
|
-
// fall back to the unchanged lastOutputAt-only judgment.
|
|
2632
|
-
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;
|
|
2633
2716
|
// readExternalCompletionMessages resolves this session's OWN native-source
|
|
2634
2717
|
// conversation (providerSessionId / persisted pin / floor claim) and, as a
|
|
2635
|
-
// side effect,
|
|
2636
|
-
//
|
|
2637
|
-
// 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.
|
|
2638
2721
|
let messages: unknown[] | null = null;
|
|
2639
2722
|
try {
|
|
2640
2723
|
messages = this.readExternalCompletionMessages();
|
|
2641
2724
|
} catch {
|
|
2642
|
-
return null; // best-effort:
|
|
2725
|
+
return { snapshot: null, messages: null }; // best-effort: fail-open
|
|
2643
2726
|
}
|
|
2644
|
-
|
|
2645
|
-
if (!probe) return null;
|
|
2646
|
-
return {
|
|
2647
|
-
msgCount: typeof probe.msgCount === 'number' ? probe.msgCount : (Array.isArray(messages) ? messages.length : 0),
|
|
2648
|
-
sourceMtimeMs: typeof probe.sourceMtimeMs === 'number' ? probe.sourceMtimeMs : 0,
|
|
2649
|
-
};
|
|
2727
|
+
return { snapshot: this.lastTranscriptSignalSnapshot, messages };
|
|
2650
2728
|
}
|
|
2651
2729
|
|
|
2652
2730
|
/**
|
|
@@ -2749,8 +2827,25 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2749
2827
|
* false for a daemon-owned provider and for a genuinely mid-turn / wedged
|
|
2750
2828
|
* worker with no in-turn final assistant, so a real stall still fires
|
|
2751
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.
|
|
2752
2844
|
*/
|
|
2753
|
-
private tryReconcileTranscriptCompletionForStall(
|
|
2845
|
+
private tryReconcileTranscriptCompletionForStall(
|
|
2846
|
+
observedStatus: string,
|
|
2847
|
+
transcriptSignals?: { snapshot: SignalSnapshot | null; messages: unknown[] | null } | null,
|
|
2848
|
+
): boolean {
|
|
2754
2849
|
// daemon-owned transcripts get real PTY turn events — an idle-quiet
|
|
2755
2850
|
// daemon-owned worker with no completion emit is a genuine anomaly the
|
|
2756
2851
|
// stall should surface, exactly as before this unification.
|
|
@@ -2779,14 +2874,29 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2779
2874
|
try {
|
|
2780
2875
|
parsedMessages = this.adapter?.getScriptParsedStatus?.()?.messages;
|
|
2781
2876
|
} catch { parsedMessages = undefined; }
|
|
2782
|
-
//
|
|
2783
|
-
//
|
|
2784
|
-
//
|
|
2785
|
-
//
|
|
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.
|
|
2786
2885
|
let finalSummary: string | undefined;
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
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
|
+
}
|
|
2790
2900
|
// No in-turn final assistant evidence → not a proven turn-end. Let the real
|
|
2791
2901
|
// stall fire so a genuinely-wedged worker is still surfaced.
|
|
2792
2902
|
if (!finalSummary) return false;
|
|
@@ -3168,20 +3278,27 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
3168
3278
|
// untouched; claude-cli's write-lag CANON-C immediate emit is likewise
|
|
3169
3279
|
// untouched (its block deliberately omits noExternalTranscriptSource).
|
|
3170
3280
|
if (blockReason === 'missing_final_assistant' && block.noExternalTranscriptSource === true) {
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
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
|
|
3175
3292
|
if (pending.loggedBlockReason !== 'native_transcript_advancing') {
|
|
3176
|
-
LOG.info('CLI', `[${this.type}] holding pending completed (native_transcript_advancing: msgCount=${
|
|
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`);
|
|
3177
3294
|
if (this.isMeshWorkerSession()) {
|
|
3178
|
-
traceMeshEventDrop('completion_gate_hold', this.meshTraceCtx(), `native_transcript_advancing msgCount=${
|
|
3295
|
+
traceMeshEventDrop('completion_gate_hold', this.meshTraceCtx(), `native_transcript_advancing msgCount=${growthDetail.msgCount} mtimeAge=${mtimeAgeMs}ms`);
|
|
3179
3296
|
}
|
|
3180
3297
|
if (this.completionTraceOn()) this.recordCompletionGateTrace('hold', {
|
|
3181
3298
|
blockReason: 'native_transcript_advancing',
|
|
3182
3299
|
latestVisibleStatus,
|
|
3183
|
-
msgCount:
|
|
3184
|
-
sourceMtimeAgeMs:
|
|
3300
|
+
msgCount: growthDetail.msgCount,
|
|
3301
|
+
sourceMtimeAgeMs: mtimeAgeMs,
|
|
3185
3302
|
growthQuietMs: MISSING_ASSISTANT_TRANSCRIPT_GROWTH_QUIET_MS,
|
|
3186
3303
|
waitedMs,
|
|
3187
3304
|
});
|
|
@@ -864,6 +864,16 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
864
864
|
}
|
|
865
865
|
refreshProviderDefinition(): void { /* hot reload handled by SpecDriver fs.watch */ }
|
|
866
866
|
|
|
867
|
+
/**
|
|
868
|
+
* TX-FSM Stage 0 (shadow): forward the daemon's normalized signal
|
|
869
|
+
* observation into the FSM driver. Observation-only — failures here must
|
|
870
|
+
* never break the adapter, and the driver treats the envelope as a pure
|
|
871
|
+
* injected value (no reads cross the engine boundary).
|
|
872
|
+
*/
|
|
873
|
+
setSignalObservation(snapshot: import('./signal-envelope.js').SignalSnapshot | null): void {
|
|
874
|
+
try { this.driver.setSignalObservation?.(snapshot); } catch { /* shadow-only: never break the adapter */ }
|
|
875
|
+
}
|
|
876
|
+
|
|
867
877
|
private handleEvent(ev: DashboardEvent): void {
|
|
868
878
|
this.lastEvent = ev;
|
|
869
879
|
switch (ev.kind) {
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
type ResolvedSection, type TraceEntry,
|
|
31
31
|
} from './evaluator.js';
|
|
32
32
|
import { evaluateFsm, stableRegionKey, type FsmClock, type TransitionEval, type FsmEvaluation } from './fsm-evaluator.js';
|
|
33
|
+
import type { SignalSnapshot } from './signal-envelope.js';
|
|
33
34
|
import {
|
|
34
35
|
type CliSpecV4, type FsmState, type FsmTransition,
|
|
35
36
|
initialState, stateById, statusForState, modalKindForState, outgoingTransitions,
|
|
@@ -144,6 +145,14 @@ export interface ISpecDriver {
|
|
|
144
145
|
getFsmDebug?(): unknown;
|
|
145
146
|
getFsmSnapshotHistory?(): ReadonlyArray<FsmSnapshotEntry>;
|
|
146
147
|
getEventTimeline?(limit?: number): ReadonlyArray<SpecPtyEvent>;
|
|
148
|
+
/**
|
|
149
|
+
* TX-FSM Stage 0 (shadow): inject the daemon-normalized signal observation.
|
|
150
|
+
* Observation ONLY — the driver receives the envelope, never a reader; all
|
|
151
|
+
* file discovery / session pinning / parsing stays daemon-side so the FSM
|
|
152
|
+
* engine remains a generic PTY engine. The snapshot feeds the shadow
|
|
153
|
+
* verdict of `signal` conditions exclusively; it cannot gate a transition.
|
|
154
|
+
*/
|
|
155
|
+
setSignalObservation?(snapshot: SignalSnapshot | null): void;
|
|
147
156
|
}
|
|
148
157
|
|
|
149
158
|
export interface SpecDriverOpts {
|
|
@@ -367,6 +376,15 @@ export class FsmDriver implements ISpecDriver {
|
|
|
367
376
|
* transition — the rich pre-transition table that lastFsmEval only keeps
|
|
368
377
|
* for the single most recent evaluation. Separate from stateHistory. */
|
|
369
378
|
private fsmSnapshotHistory: FsmSnapshotEntry[] = [];
|
|
379
|
+
/** TX-FSM Stage 0 (shadow): the latest daemon-injected signal observation.
|
|
380
|
+
* Read by evalFsmNow for the shadow verdict of `signal` conditions ONLY —
|
|
381
|
+
* the Stage-0 pass-through in the evaluator means it can never alter
|
|
382
|
+
* which transition fires. */
|
|
383
|
+
private signalObservation: SignalSnapshot | null = null;
|
|
384
|
+
/** Per-transition last-logged shadow divergence (`${from}→${to}` → whether
|
|
385
|
+
* the shadow verdict currently disagrees with the real one), so the
|
|
386
|
+
* shadow log emits on FLIP only, not every frame. */
|
|
387
|
+
private shadowDivergenceLast = new Map<string, boolean>();
|
|
370
388
|
|
|
371
389
|
constructor(private readonly opts: SpecDriverOpts) {
|
|
372
390
|
this.loadSpecOrThrow();
|
|
@@ -451,6 +469,17 @@ export class FsmDriver implements ISpecDriver {
|
|
|
451
469
|
this.adapter.updateMeta(meta, replace);
|
|
452
470
|
}
|
|
453
471
|
|
|
472
|
+
/**
|
|
473
|
+
* TX-FSM Stage 0 (shadow): receive the daemon-normalized signal
|
|
474
|
+
* observation. The driver stores it verbatim — it does NOT poll, parse,
|
|
475
|
+
* or read anything itself (generic-PTY-engine boundary). The observation
|
|
476
|
+
* only feeds the shadow verdict of `signal` conditions; the evaluator's
|
|
477
|
+
* Stage-0 pass-through guarantees it cannot change a transition.
|
|
478
|
+
*/
|
|
479
|
+
setSignalObservation(snapshot: SignalSnapshot | null): void {
|
|
480
|
+
this.signalObservation = snapshot ?? null;
|
|
481
|
+
}
|
|
482
|
+
|
|
454
483
|
snapshot(): string { return this.adapter.snapshot(); }
|
|
455
484
|
getCursorPosition(): { row: number; col: number } { return this.adapter.getCursorPosition(); }
|
|
456
485
|
getScreen(): string { return this.adapter.snapshot(); }
|
|
@@ -643,7 +672,38 @@ export class FsmDriver implements ISpecDriver {
|
|
|
643
672
|
|
|
644
673
|
private evalFsmNow(screen: string, cursor: { row: number; col: number }, now: number) {
|
|
645
674
|
const prev = this.prevScreenLines.length > 0 ? this.prevScreenLines : undefined;
|
|
646
|
-
return evaluateFsm(this.spec, this.currentStateId, screen, cursor, prev, this.buildClock(now));
|
|
675
|
+
return evaluateFsm(this.spec, this.currentStateId, screen, cursor, prev, this.buildClock(now), this.signalObservation);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* TX-FSM Stage 0 (shadow): compare each signal-guarded transition's
|
|
680
|
+
* counterfactual verdict against the real (PTY-only) one and log on FLIP.
|
|
681
|
+
* This is the "만약 적용했다면" half of the shadow log — the Stage 1-3
|
|
682
|
+
* evidence for whether signal gating would have changed any verdict, and
|
|
683
|
+
* in which direction. Read-only: runs after the evaluation is complete
|
|
684
|
+
* and never feeds back into it.
|
|
685
|
+
*/
|
|
686
|
+
private logShadowDivergence(ev: FsmEvaluation): void {
|
|
687
|
+
for (const t of ev.transitions) {
|
|
688
|
+
if (!t.shadow) continue;
|
|
689
|
+
const key = `${this.currentStateId}→${t.to}`;
|
|
690
|
+
const diverges = t.shadow.fires !== t.fires;
|
|
691
|
+
if ((this.shadowDivergenceLast.get(key) ?? false) === diverges) continue;
|
|
692
|
+
this.shadowDivergenceLast.set(key, diverges);
|
|
693
|
+
if (diverges) {
|
|
694
|
+
LOG.info(
|
|
695
|
+
'FsmDriver',
|
|
696
|
+
`[${this.specTag()}] [shadow] ${key} (${t.label}): real fires=${t.fires}`
|
|
697
|
+
+ ` but signal-gated verdict would be fires=${t.shadow.fires}`
|
|
698
|
+
+ ` (shadowCond=${t.shadow.condResult}${t.shadow.unknown ? ', signal unknown/fail-open' : ''})`,
|
|
699
|
+
);
|
|
700
|
+
} else {
|
|
701
|
+
LOG.info(
|
|
702
|
+
'FsmDriver',
|
|
703
|
+
`[${this.specTag()}] [shadow] ${key} (${t.label}): shadow verdict realigned with real fires=${t.fires}`,
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
647
707
|
}
|
|
648
708
|
|
|
649
709
|
private reevaluate(forceEmit = false): void {
|
|
@@ -659,6 +719,7 @@ export class FsmDriver implements ISpecDriver {
|
|
|
659
719
|
const ev = this.evalFsmNow(screen, cursor, now);
|
|
660
720
|
this.lastFsmEval = ev;
|
|
661
721
|
this.prevScreenLines = currentLines;
|
|
722
|
+
this.logShadowDivergence(ev);
|
|
662
723
|
|
|
663
724
|
if (ev.fired) {
|
|
664
725
|
this.commitTransition(ev.fired, now, ev);
|