@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.
@@ -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,6 +413,17 @@ export declare class CliProviderInstance implements ProviderInstance {
413
413
  private completedDebounceTimer;
414
414
  private completedDebouncePending;
415
415
  private lastExternalCompletionProbe;
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). */
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;
416
427
  /**
417
428
  * The final assistant summary of the last completed turn, cached at
418
429
  * completion-emit time. For a native-source provider (antigravity) whose
@@ -448,6 +459,22 @@ export declare class CliProviderInstance implements ProviderInstance {
448
459
  */
449
460
  private spawnedEnvOverrides;
450
461
  private readExternalCompletionMessages;
462
+ /**
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.
476
+ */
477
+ private publishTranscriptSignalObservation;
451
478
  /**
452
479
  * The content of the LAST visible assistant bubble in a message list, or ''
453
480
  * when the tail is not an assistant reply. Skips trailing system/tool/activity
@@ -576,23 +603,35 @@ export declare class CliProviderInstance implements ProviderInstance {
576
603
  * starts with a clean slate (a restart is not throttled by a prior stall).
577
604
  */
578
605
  private resetMeshStallEpisode;
579
- /**
580
- * KIMI-MESH-COMPLETION-EMIT (axis 1). For a native-source provider
581
- * (transcriptAuthority=provider its authoritative history is an on-disk
582
- * transcript file, e.g. kimi's wire.jsonl), sample the transcript's current
583
- * progress fingerprint (record count + source-file mtime) WITHOUT parsing the
584
- * PTY. Used by the stall watchdog to distinguish "PTY render is quiet but the
585
- * worker is still doing long tool work (transcript growing)" from a genuine
586
- * wedge. Returns null for a pure-PTY provider (no native source) or when the
587
- * source cannot be resolved this tick the caller then falls back to the
588
- * 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".
589
625
  *
590
- * Generalized on the provider's native-source flag, NOT a hardcoded provider
591
- * type, so every current and future pure-PTY long-tool native-source provider
592
- * benefits. Cheap enough for the stall path: it runs only at the stall threshold
593
- * (≥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.
594
633
  */
595
- private sampleNativeTranscriptProgress;
634
+ private probeNativeTranscriptSignals;
596
635
  /**
597
636
  * KIMI-MESH-COMPLETION-EMIT (axis 2). Last-chance completion emit for a mesh
598
637
  * DELEGATED worker whose PTY has already exited (e.g. killed by a false stall)
@@ -645,6 +684,20 @@ export declare class CliProviderInstance implements ProviderInstance {
645
684
  * false for a daemon-owned provider and for a genuinely mid-turn / wedged
646
685
  * worker with no in-turn final assistant, so a real stall still fires
647
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.
648
701
  */
649
702
  private tryReconcileTranscriptCompletionForStall;
650
703
  /**
@@ -223,6 +223,13 @@ export declare class SpecCliAdapter implements CliAdapter {
223
223
  getRuntimeMetadata(): unknown;
224
224
  updateRuntimeMeta(meta?: Record<string, unknown>): void;
225
225
  refreshProviderDefinition(): void;
226
+ /**
227
+ * TX-FSM Stage 0 (shadow): forward the daemon's normalized signal
228
+ * observation into the FSM driver. Observation-only — failures here must
229
+ * never break the adapter, and the driver treats the envelope as a pure
230
+ * injected value (no reads cross the engine boundary).
231
+ */
232
+ setSignalObservation(snapshot: import('./signal-envelope.js').SignalSnapshot | null): void;
226
233
  private handleEvent;
227
234
  private detectInteractivePromptFromPtyChunk;
228
235
  private readCurrentScreenSections;
@@ -2,6 +2,7 @@ import { type SpecPtyEvent } from './adapter.js';
2
2
  import type { PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
3
3
  import { type TraceEntry } from './evaluator.js';
4
4
  import { type TransitionEval } from './fsm-evaluator.js';
5
+ import type { SignalSnapshot } from './signal-envelope.js';
5
6
  import { chunkPreservingSurrogates as chunkPreservingSurrogatesShared } from '../../cli-adapters/pty-write-chunking.js';
6
7
  export type DashboardEvent = {
7
8
  kind: 'pty_data';
@@ -159,6 +160,14 @@ export interface ISpecDriver {
159
160
  getFsmDebug?(): unknown;
160
161
  getFsmSnapshotHistory?(): ReadonlyArray<FsmSnapshotEntry>;
161
162
  getEventTimeline?(limit?: number): ReadonlyArray<SpecPtyEvent>;
163
+ /**
164
+ * TX-FSM Stage 0 (shadow): inject the daemon-normalized signal observation.
165
+ * Observation ONLY — the driver receives the envelope, never a reader; all
166
+ * file discovery / session pinning / parsing stays daemon-side so the FSM
167
+ * engine remains a generic PTY engine. The snapshot feeds the shadow
168
+ * verdict of `signal` conditions exclusively; it cannot gate a transition.
169
+ */
170
+ setSignalObservation?(snapshot: SignalSnapshot | null): void;
162
171
  }
163
172
  export interface SpecDriverOpts {
164
173
  specPath: string;
@@ -244,6 +253,15 @@ export declare class FsmDriver implements ISpecDriver {
244
253
  * transition — the rich pre-transition table that lastFsmEval only keeps
245
254
  * for the single most recent evaluation. Separate from stateHistory. */
246
255
  private fsmSnapshotHistory;
256
+ /** TX-FSM Stage 0 (shadow): the latest daemon-injected signal observation.
257
+ * Read by evalFsmNow for the shadow verdict of `signal` conditions ONLY —
258
+ * the Stage-0 pass-through in the evaluator means it can never alter
259
+ * which transition fires. */
260
+ private signalObservation;
261
+ /** Per-transition last-logged shadow divergence (`${from}→${to}` → whether
262
+ * the shadow verdict currently disagrees with the real one), so the
263
+ * shadow log emits on FLIP only, not every frame. */
264
+ private shadowDivergenceLast;
247
265
  constructor(opts: SpecDriverOpts);
248
266
  subscribe(listener: (ev: DashboardEvent) => void): () => void;
249
267
  start(): void;
@@ -259,6 +277,14 @@ export declare class FsmDriver implements ISpecDriver {
259
277
  * the session registry. Not a DashboardCommand — this is a control-plane
260
278
  * update, not user input. */
261
279
  updateMeta(meta: Record<string, unknown>, replace?: boolean): void;
280
+ /**
281
+ * TX-FSM Stage 0 (shadow): receive the daemon-normalized signal
282
+ * observation. The driver stores it verbatim — it does NOT poll, parse,
283
+ * or read anything itself (generic-PTY-engine boundary). The observation
284
+ * only feeds the shadow verdict of `signal` conditions; the evaluator's
285
+ * Stage-0 pass-through guarantees it cannot change a transition.
286
+ */
287
+ setSignalObservation(snapshot: SignalSnapshot | null): void;
262
288
  snapshot(): string;
263
289
  getCursorPosition(): {
264
290
  row: number;
@@ -319,6 +345,15 @@ export declare class FsmDriver implements ISpecDriver {
319
345
  private emitInitialState;
320
346
  private buildClock;
321
347
  private evalFsmNow;
348
+ /**
349
+ * TX-FSM Stage 0 (shadow): compare each signal-guarded transition's
350
+ * counterfactual verdict against the real (PTY-only) one and log on FLIP.
351
+ * This is the "만약 적용했다면" half of the shadow log — the Stage 1-3
352
+ * evidence for whether signal gating would have changed any verdict, and
353
+ * in which direction. Read-only: runs after the evaluation is complete
354
+ * and never feeds back into it.
355
+ */
356
+ private logShadowDivergence;
322
357
  private reevaluate;
323
358
  private commitTransition;
324
359
  /** Snapshot the full FSM evaluation that produced a transition into the
@@ -1,6 +1,7 @@
1
1
  import type { SectionDef } from './types.js';
2
2
  import { type ResolvedSection, type TraceEntry } from './evaluator.js';
3
3
  import { type CliSpecV4, type FsmCondition } from './fsm-types.js';
4
+ import { type SignalSnapshot } from './signal-envelope.js';
4
5
  export interface FsmClock {
5
6
  /** Wall-clock now (ms). Passed in so the evaluator stays pure. */
6
7
  now: number;
@@ -14,7 +15,7 @@ export interface FsmClock {
14
15
  }
15
16
  /** Per-condition evaluation detail — the debugging payload. */
16
17
  export interface CondResult {
17
- kind: 'regex' | 'changed' | 'elapsed' | 'stable' | 'all' | 'any' | 'not';
18
+ kind: 'regex' | 'changed' | 'elapsed' | 'stable' | 'signal' | 'all' | 'any' | 'not';
18
19
  result: boolean;
19
20
  detail: string;
20
21
  /** Remaining ms until a time-based condition would flip to true. 0 if
@@ -24,6 +25,17 @@ export interface CondResult {
24
25
  * snapshot shows WHAT text the rule fired on — not just which regex. Never
25
26
  * read by the FSM; purely for the Spec Debug Snapshot. */
26
27
  matchedText?: string;
28
+ /** TX-FSM Stage 0 (shadow): present only on `signal` leaves. `result` above
29
+ * is ALWAYS true for a signal leaf (Stage-0 pass-through — the leaf never
30
+ * gates a transition); this payload records what the leaf WOULD have
31
+ * decided against the injected snapshot. shadowResult null = the signal
32
+ * was unavailable/unknown (fail-open). */
33
+ signal?: {
34
+ name: string;
35
+ available: boolean;
36
+ value: boolean | null;
37
+ shadowResult: boolean | null;
38
+ };
27
39
  children?: CondResult[];
28
40
  }
29
41
  /** One evaluated transition with its full reasoning. */
@@ -42,6 +54,20 @@ export interface TransitionEval {
42
54
  /** Overall: would this transition fire? */
43
55
  fires: boolean;
44
56
  priority: number;
57
+ /**
58
+ * TX-FSM Stage 0 (shadow): present only when the guard contains at least
59
+ * one `signal` leaf. Records the verdict the transition WOULD have
60
+ * produced if signal leaves gated (condResult/fires), so the shadow log
61
+ * can compare it against the real PTY-only outcome. `unknown` = at least
62
+ * one signal leaf had no observation (fail-open); the shadow verdict is
63
+ * then informational, not a counterfactual proof. NEVER consumed by the
64
+ * driver for any transition decision — log/debug surface only.
65
+ */
66
+ shadow?: {
67
+ condResult: boolean;
68
+ fires: boolean;
69
+ unknown: boolean;
70
+ };
45
71
  }
46
72
  export interface FsmEvaluation {
47
73
  /** Sections resolved from the screen (shared with v3 shape). */
@@ -79,7 +105,7 @@ export declare function stableRegionKey(cond: {
79
105
  export declare function evaluateFsm(spec: CliSpecV4, currentStateId: string, screenText: string, cursor: {
80
106
  row: number;
81
107
  col: number;
82
- } | undefined, prevLines: string[] | undefined, clock: FsmClock): FsmEvaluation;
108
+ } | undefined, prevLines: string[] | undefined, clock: FsmClock, signalSnapshot?: SignalSnapshot | null): FsmEvaluation;
83
109
  /**
84
110
  * Evaluate a single condition against a screen + section map — for the spec
85
111
  * editor's live preview ("does this regex match the current screen right
@@ -49,7 +49,25 @@ export interface FsmAnyCondition {
49
49
  export interface FsmNotCondition {
50
50
  not: FsmCondition;
51
51
  }
52
- export type FsmCondition = RegexCondition | ChangedCondition | ElapsedCondition | StableCondition | FsmAllCondition | FsmAnyCondition | FsmNotCondition;
52
+ /**
53
+ * TX-FSM Stage 0 (shadow) — a daemon-signal leaf. References a NORMALIZED
54
+ * signal name from the SignalSnapshot envelope (never a provider name), so a
55
+ * spec can combine transcript evidence with PTY conditions via all/any/not:
56
+ *
57
+ * { all: [ { matches: "…" }, { signal: "final_assistant_present" } ] }
58
+ *
59
+ * STAGE-0 SEMANTICS: evaluated for the shadow log only. The leaf NEVER gates
60
+ * a transition — the evaluator treats it as pass-through for the real verdict
61
+ * and records what it WOULD have decided (TransitionEval.shadow). A missing
62
+ * or unavailable signal fails open. Promotion to a real gate is Stage 1+.
63
+ */
64
+ export interface FsmSignalCondition {
65
+ /** Normalized signal name (see SIGNAL_NAMES in signal-envelope.ts). */
66
+ signal: string;
67
+ /** Expected value; default true. */
68
+ equals?: boolean;
69
+ }
70
+ export type FsmCondition = RegexCondition | ChangedCondition | ElapsedCondition | StableCondition | FsmSignalCondition | FsmAllCondition | FsmAnyCondition | FsmNotCondition;
53
71
  export interface FsmState {
54
72
  id: string;
55
73
  label: string;
@@ -0,0 +1,59 @@
1
+ import type { TranscriptClass, CompletionTiming } from '../transcript-evidence.js';
2
+ export declare const SIGNAL_SNAPSHOT_KIND: "adhdev:fsm/signal-snapshot@0";
3
+ /** The normalized signal vocabulary. Stage 0 keeps this deliberately small —
4
+ * every name must be derivable from the FLATTENED native-transcript pipeline
5
+ * (which drops tool_use/tool_result correlation ids — that raw-JSONL read is
6
+ * Stage 4 territory and must NOT be added here). */
7
+ export type SignalName = 'final_assistant_present' | 'in_turn_progress' | 'transcript_growing';
8
+ export declare const SIGNAL_NAMES: readonly SignalName[];
9
+ export type SignalUnavailableReason = 'no_native_source' | 'unresolved' | 'error';
10
+ /** The normalized, provider-agnostic observation the daemon hands the FSM. */
11
+ export interface SignalSnapshot {
12
+ kind: typeof SIGNAL_SNAPSHOT_KIND;
13
+ /** Wall-clock time the underlying transcript read happened (ms). */
14
+ sampledAt: number;
15
+ /** False ⇒ every signal is null and conditions fail open. */
16
+ available: boolean;
17
+ unavailableReason?: SignalUnavailableReason;
18
+ /** The authority profile class/timing this snapshot was produced under
19
+ * (via resolveTranscriptAuthorityProfile — never raw predicates). */
20
+ profile?: {
21
+ class: TranscriptClass;
22
+ timing: CompletionTiming;
23
+ };
24
+ /** Normalized signals; null = unknown (fail-open). */
25
+ signals: Record<SignalName, boolean | null>;
26
+ /** Raw sampling context for the shadow log / Stage 1-3 analysis. */
27
+ detail: {
28
+ msgCount: number;
29
+ sourceMtimeMs: number;
30
+ /** now - sourceMtimeMs; null when the mtime is unknown (0). */
31
+ ageMs: number | null;
32
+ };
33
+ }
34
+ /** The fail-open snapshot: no usable observation, every signal null. */
35
+ export declare function unavailableSignalSnapshot(now: number, reason: SignalUnavailableReason, profile?: {
36
+ class: TranscriptClass;
37
+ timing: CompletionTiming;
38
+ }): SignalSnapshot;
39
+ /** The spec's `signal` leaf: `{ signal: 'transcript_growing', equals?: boolean }`. */
40
+ export interface SignalLeaf {
41
+ signal: string;
42
+ equals?: boolean;
43
+ }
44
+ export interface SignalLeafEval {
45
+ /** The normalized signal value read from the snapshot; null = unknown. */
46
+ value: boolean | null;
47
+ /** What the leaf WOULD evaluate to if signals gated transitions; null =
48
+ * unknown (no snapshot / signal missing → fail-open). */
49
+ shadowResult: boolean | null;
50
+ /** Human-readable shadow-log fragment. */
51
+ detail: string;
52
+ }
53
+ /**
54
+ * Evaluate one `signal` leaf against the injected snapshot. This computes the
55
+ * SHADOW verdict only — the caller (fsm-evaluator) owns the Stage-0
56
+ * pass-through rule that keeps the real result true regardless of what this
57
+ * returns.
58
+ */
59
+ export declare function evaluateSignalLeaf(cond: SignalLeaf, snapshot: SignalSnapshot | null | undefined): SignalLeafEval;
@@ -0,0 +1,59 @@
1
+ import type { TranscriptAuthorityProfile } from './transcript-evidence.js';
2
+ import { type SignalSnapshot } from './spec/signal-envelope.js';
3
+ /** One daemon transcript read, normalized for the source. */
4
+ export interface TranscriptSignalSample {
5
+ /** Parsed native-transcript messages, or null when the read failed to
6
+ * resolve a source this time. */
7
+ messages: unknown[] | null;
8
+ /** The probe metadata the read refreshed (sourcePath/mtime/msgCount), or
9
+ * null when no source is bound. A null sourceMtimeMs is treated as "no
10
+ * freshness evidence" (same contract as the growth-hold path). */
11
+ probe: {
12
+ msgCount: number;
13
+ sourceMtimeMs: number | null;
14
+ sourcePath?: string | null;
15
+ } | null;
16
+ /** True when the read threw — logged distinctly from a clean miss. */
17
+ error?: boolean;
18
+ }
19
+ export interface TranscriptSignalSourceOpts {
20
+ /** Log tag (provider type / session label) — never used for branching. */
21
+ label: string;
22
+ /** The P0 choke-point profile for this provider. */
23
+ profile: TranscriptAuthorityProfile;
24
+ /** Turn-boundary clock (adapter.currentTurnStartedAt) for turn-scoped
25
+ * final-assistant detection; may yield undefined pre-turn. */
26
+ turnStartedAt?: () => number | undefined;
27
+ /** In-turn final-assistant predicate over the flattened messages — the
28
+ * instance passes its existing completionHasFinalAssistantMessage so the
29
+ * signal reuses the exact completion machinery instead of duplicating it. */
30
+ finalAssistantPresent: (messages: unknown[], turnStartedAt?: number) => boolean;
31
+ /** Freshness window (ms) for transcript_growing. The caller passes the
32
+ * completion pipeline's MISSING_ASSISTANT_TRANSCRIPT_GROWTH_QUIET_MS so
33
+ * the shadow signal is comparable to the existing growth-hold judgment. */
34
+ growthQuietMs: number;
35
+ }
36
+ export declare class TranscriptSignalSource {
37
+ private readonly opts;
38
+ /** msgCount seen at the previous update — drives in_turn_progress. */
39
+ private prevMsgCount;
40
+ /** Fingerprint of the last LOGGED snapshot, so the shadow log fires on
41
+ * change only (a quiet session must not spam one line per read). */
42
+ private lastLoggedFingerprint;
43
+ constructor(opts: TranscriptSignalSourceOpts);
44
+ /**
45
+ * Normalize one daemon read into a SignalSnapshot and emit the Stage-0
46
+ * shadow log when the observation changed. Pure w.r.t. the outside world:
47
+ * the only side effect is the (change-gated) log line. Returns the
48
+ * snapshot the caller should inject into the FSM driver.
49
+ */
50
+ update(sample: TranscriptSignalSample, now?: number): SignalSnapshot;
51
+ /** Pure normalization — separated from update() so tests can assert the
52
+ * envelope without touching the log path. */
53
+ buildSnapshot(sample: TranscriptSignalSample, now: number): SignalSnapshot;
54
+ /** Stage-0 shadow log: emit one line when the normalized observation
55
+ * CHANGES. This log is the Stage 1-3 judgment input ("which signals were
56
+ * actually observable, and when"), so it carries the full signal set —
57
+ * but only on change, so a steady-state session stays quiet. */
58
+ private logOnChange;
59
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "1.0.28-rc.10",
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.10",
53
- "@adhdev/session-host-core": "1.0.28-rc.10",
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",
@@ -173,6 +173,13 @@ export interface CliAdapter {
173
173
  // back to resolveModal + the resolution-cooldown check).
174
174
  resolveModalMatched?(buttonIndex: number): boolean;
175
175
  isApprovalRecentlyResolved?(): boolean;
176
+ /**
177
+ * TX-FSM Stage 0 (shadow): receive the daemon-normalized transcript signal
178
+ * observation (SignalSnapshot envelope, providers/spec/signal-envelope.ts).
179
+ * Optional — only the spec-driven FSM adapter implements it; callers must
180
+ * typeof-guard. Shadow-only: the observation can never gate a transition.
181
+ */
182
+ setSignalObservation?(snapshot: unknown): void;
176
183
  // Raw PTY I/O (for terminal view)
177
184
  setOnPtyData?(callback: (data: string) => void): void;
178
185
  writeRaw?(data: string): void;
@@ -112,6 +112,47 @@ export const daemonLifecycleHandlers: Record<string, LowFamilyHandler> = {
112
112
  }
113
113
  },
114
114
 
115
+ // Restart-only path (mesh restart_daemon_node mode="restart"): re-spawn the
116
+ // daemon WITHOUT the npm reinstall daemon_upgrade performs. Used to reset
117
+ // daemon state (memory leaks, zombie sessions, wedged internals) when the
118
+ // version is already correct — downtime drops to the detached re-spawn.
119
+ // killSessionHost is an explicit opt-in hard refresh (see upgrade-helper).
120
+ daemon_restart: async (ctx: LowFamilyContext, args: any) => {
121
+ LOG.info('Restart', 'Restart-only requested (no package reinstall)');
122
+ try {
123
+ const isStandalone = ctx.deps.packageName === '@adhdev/daemon-standalone'
124
+ || process.argv[1]?.includes('daemon-standalone');
125
+ const pkgName = isStandalone ? '@adhdev/daemon-standalone' : 'adhdev';
126
+ const killSessionHost = args?.killSessionHost === true;
127
+ if (killSessionHost) {
128
+ LOG.warn('Restart', 'killSessionHost requested — session-host will be stopped and ALL hosted sessions destroyed');
129
+ }
130
+
131
+ spawnDetachedDaemonUpgradeHelper({
132
+ packageName: pkgName,
133
+ targetVersion: '',
134
+ parentPid: process.pid,
135
+ restartArgv: process.argv.slice(1),
136
+ cwd: process.cwd(),
137
+ sessionHostAppName: process.env.ADHDEV_SESSION_HOST_NAME || 'adhdev',
138
+ skipInstall: true,
139
+ killSessionHost,
140
+ });
141
+ LOG.info('Restart', 'Scheduled detached restart-only helper');
142
+
143
+ // Exit after the command response has been sent so the helper can re-spawn cleanly.
144
+ setTimeout(() => {
145
+ LOG.info('Restart', 'Exiting daemon so detached helper can re-spawn it...');
146
+ process.exit(0);
147
+ }, 3000);
148
+
149
+ return { success: true, restarted: true, restarting: true, mode: 'restart', killSessionHost };
150
+ } catch (e: any) {
151
+ LOG.error('Restart', `Failed: ${e.message}`);
152
+ return { success: false, error: e.message };
153
+ }
154
+ },
155
+
115
156
  set_machine_nickname: async (_ctx: LowFamilyContext, args: any) => {
116
157
  const nickname = args?.nickname;
117
158
  updateConfig({ machineNickname: nickname || null });