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

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.
@@ -616,6 +616,20 @@ export interface MeshWorkQueueEntry {
616
616
  * by counting active assignments grouped by node + provider.
617
617
  */
618
618
  assignedProviderType?: string;
619
+ /**
620
+ * Transcript-authority class of the claiming session's provider, stamped at
621
+ * claim time (P1 of the transcript-authority unification — root repo
622
+ * docs/design/2026-07-25-transcript-authority-unification.md). Lets the
623
+ * COORDINATOR side classify a remote worker (early-arm / redrive gates)
624
+ * without resolving the provider module locally — the structural fix for
625
+ * the "remote class unknowable → reprobe-only" blind spot. Absent on rows
626
+ * claimed by older daemons; consumers must fall back to local resolution.
627
+ */
628
+ assignedTranscriptProfile?: {
629
+ class: 'native-source' | 'pure-pty' | 'daemon-owned';
630
+ timing: 'hold' | 'floor' | 'immediate';
631
+ emitsPtyTurnEvents: boolean;
632
+ };
619
633
  /** Human/operator reason for terminal cancellation. */
620
634
  cancelReason?: string;
621
635
  cancelledAt?: string;
@@ -1165,7 +1179,12 @@ export function claimNextTask(
1165
1179
  nodeId: string,
1166
1180
  sessionId: string,
1167
1181
  capabilityTags?: string[],
1168
- opts?: { providerType?: string; providerMaxParallel?: number; nodeIsWorktree?: boolean },
1182
+ opts?: {
1183
+ providerType?: string;
1184
+ providerMaxParallel?: number;
1185
+ nodeIsWorktree?: boolean;
1186
+ assignedTranscriptProfile?: MeshWorkQueueEntry['assignedTranscriptProfile'];
1187
+ },
1169
1188
  ): MeshWorkQueueEntry | null {
1170
1189
  return MeshRuntimeStore.getInstance().claimNextQueueTask(meshId, nodeId, sessionId, capabilityTags, opts);
1171
1190
  }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * buildLocalNodeFacts — THE single producer of this daemon's MeshNodeFacts
3
+ * bundle (design: root repo docs/design/2026-07-25-deploy-lag-visibility.md).
4
+ *
5
+ * De-mirroring core: the reporter envelope (git-commands handleGitCommand)
6
+ * and the self/worktree node stamp (mesh-node-identity, whose local probe
7
+ * bypasses the envelope) BOTH call this one function, so a fact added here
8
+ * reaches self and remote nodes identically — the field-by-field dual-path
9
+ * plumbing that produced the mirror-defect class (version chip self-miss,
10
+ * slot-cap chip remote-miss, dead stale-build badge) cannot recur for bundle
11
+ * fields.
12
+ */
13
+
14
+ import type { MeshNodeFacts } from '@adhdev/mesh-shared';
15
+ import { getDaemonBuildInfo } from '../build-info.js';
16
+
17
+ export function buildLocalNodeFacts(deps?: {
18
+ providerVersions?: Record<string, string> | null;
19
+ machineNickname?: string | null;
20
+ }): MeshNodeFacts {
21
+ const build = (() => {
22
+ try {
23
+ const info = getDaemonBuildInfo();
24
+ const commit = typeof info.commit === 'string' && info.commit && info.commit !== 'unknown' ? info.commit : undefined;
25
+ const commitShort = typeof info.commitShort === 'string' && info.commitShort && info.commitShort !== 'unknown' ? info.commitShort : undefined;
26
+ const version = typeof info.version === 'string' && info.version && info.version !== 'unknown' ? info.version : undefined;
27
+ const builtAt = typeof info.builtAt === 'string' && info.builtAt ? info.builtAt : undefined;
28
+ if (!commit && !version) return undefined;
29
+ return {
30
+ ...(commit ? { commit } : {}),
31
+ ...(commitShort ? { commitShort } : {}),
32
+ ...(version ? { version } : {}),
33
+ ...(builtAt ? { builtAt } : {}),
34
+ };
35
+ } catch {
36
+ return undefined;
37
+ }
38
+ })();
39
+ const providerVersions = deps?.providerVersions && Object.keys(deps.providerVersions).length > 0
40
+ ? deps.providerVersions
41
+ : undefined;
42
+ const machineNickname = typeof deps?.machineNickname === 'string' && deps.machineNickname.trim()
43
+ ? deps.machineNickname.trim()
44
+ : undefined;
45
+ return {
46
+ schemaVersion: 1,
47
+ reportedAt: Date.now(),
48
+ ...(build ? { daemonBuild: build } : {}),
49
+ ...(providerVersions ? { providerVersions } : {}),
50
+ platform: process.platform,
51
+ arch: process.arch,
52
+ ...(machineNickname ? { machineNickname } : {}),
53
+ };
54
+ }
@@ -17,7 +17,7 @@ import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
17
17
  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
- import { isPurePtyTranscriptProvider } from '../cli-adapters/provider-cli-shared.js';
20
+ import { resolveTranscriptAuthorityProfile } from './transcript-evidence.js';
21
21
  import { createCliAdapter } from './spec/route.js';
22
22
  import type { PtyRuntimeMetadata, PtyTransportFactory } from '../cli-adapters/pty-transport.js';
23
23
  import { StatusMonitor } from './status-monitor.js';
@@ -2101,12 +2101,13 @@ export class CliProviderInstance implements ProviderInstance {
2101
2101
  // autonomous mesh sessions (allowMissingAssistantTimeout) so an interactive
2102
2102
  // antigravity session, which has no coordinator to misfire at, is untouched.
2103
2103
  //
2104
- // SPEC-DRIVEN completion timing (mission f2f6da1b root 2): the HOLD class is the
2105
- // manifest flag `holdCompletionForTranscript` (antigravity-cli declares it), NOT a
2104
+ // SPEC-DRIVEN completion timing (mission f2f6da1b root 2, P4 profile
2105
+ // substitution): the HOLD class is the authority profile's timing==='hold'
2106
+ // (manifest holdCompletionForTranscript — antigravity-cli declares it), NOT a
2106
2107
  // hardcoded provider name — a native-history provider whose PTY-derived idle can
2107
- // precede the authoritative transcript write. write-lag (claude) and floor
2108
- // (codex/kimi/…) providers leave it false and fall through to the decision below.
2109
- if ((this.provider as any)?.holdCompletionForTranscript === true) {
2108
+ // precede the authoritative transcript write. write-lag (claude, 'immediate') and
2109
+ // floor (codex/kimi/…, 'floor') classes fall through to the decision below.
2110
+ if (resolveTranscriptAuthorityProfile(this.provider).timing === 'hold') {
2110
2111
  if (allowMissingAssistantTimeout) {
2111
2112
  return { reason: 'missing_final_assistant', terminal: false, holdForTranscript: true };
2112
2113
  }
@@ -2168,7 +2169,7 @@ export class CliProviderInstance implements ProviderInstance {
2168
2169
  // This is the SAME flag the sibling branch below (source !== 'external-native')
2169
2170
  // uses for the identical noExternalTranscriptSource decision, so the two branches
2170
2171
  // agree; and it covers third-party providers instead of a hardcoded provider name.
2171
- const isWriteLagNativeSource = (this.provider as any).requiresFinalAssistantBeforeIdle !== true;
2172
+ const isWriteLagNativeSource = resolveTranscriptAuthorityProfile(this.provider).timing !== 'floor';
2172
2173
  return {
2173
2174
  reason: 'missing_final_assistant',
2174
2175
  terminal: true,
@@ -2176,14 +2177,15 @@ export class CliProviderInstance implements ProviderInstance {
2176
2177
  ...(isWriteLagNativeSource ? {} : { noExternalTranscriptSource: true }),
2177
2178
  };
2178
2179
  }
2179
- if ((this.provider as any).requiresFinalAssistantBeforeIdle === true) {
2180
+ if (resolveTranscriptAuthorityProfile(this.provider).timing === 'floor') {
2180
2181
  return { reason: 'missing_final_assistant', terminal: true, allowTimeout: allowMissingAssistantTimeout, noExternalTranscriptSource: true };
2181
2182
  }
2182
2183
  } else {
2183
- LOG.debug('CLI', `[${this.type}] missing_final_assistant (not ownsExternal) requiresFinalAssistant=${!!(this.provider as any).requiresFinalAssistantBeforeIdle}`);
2184
+ const notOwnsExternalTiming = resolveTranscriptAuthorityProfile(this.provider).timing;
2185
+ LOG.debug('CLI', `[${this.type}] missing_final_assistant (not ownsExternal) requiresFinalAssistant=${notOwnsExternalTiming === 'floor'}`);
2184
2186
  return {
2185
2187
  reason: 'missing_final_assistant',
2186
- terminal: (this.provider as any).requiresFinalAssistantBeforeIdle === true,
2188
+ terminal: notOwnsExternalTiming === 'floor',
2187
2189
  allowTimeout: allowMissingAssistantTimeout,
2188
2190
  // PTY-parsed provider (codex-cli): no external transcript trails the idle
2189
2191
  // transition, so the CANON-C decoupled emit must observe the min-elapsed floor
@@ -2525,30 +2527,18 @@ export class CliProviderInstance implements ProviderInstance {
2525
2527
  }
2526
2528
  }
2527
2529
 
2528
- // KIMI-PURE-PTY-COMPLETION-EMIT (Fix 3): before firing a false stall, check the
2529
- // pure-PTY class (no native transcript → nativeSample was null above). A finished
2530
- // pure-PTY worker sits at a static idle prompt with an in-turn final assistant
2531
- // message but never emitted generating_completed the status-agnostic watchdog
2532
- // would misread that quiet as a wedge. If the PTY transcript proves a finished
2533
- // turn, emit the missing completion and SUPPRESS the stall (mark the anchor
2534
- // emitted so this static idle is not re-checked every tick). No-op for
2535
- // native-source providers and for a genuinely mid-turn / wedged worker with no
2536
- // final assistant the real stall fires below unchanged.
2537
- if (this.tryReconcilePurePtyCompletionForStall(observedStatus)) {
2538
- this.meshStallEmittedForAnchor = true;
2539
- return;
2540
- }
2541
-
2542
- // KIMI-NATIVE-SOURCE-COMPLETION-EMIT (Fix 3): the pure-PTY check above is a no-op
2543
- // for a native-source provider (kimi and kin — transcriptAuthority=provider +
2544
- // nativeHistory). For that class, a finished-but-quiet idle whose transcript has
2545
- // gone static (sampleNativeTranscriptProgress stopped re-arming) is otherwise
2546
- // misread as a wedge. Resolve the completion from the authoritative native
2547
- // transcript; if it proves a finished turn, emit the missing completion and
2548
- // SUPPRESS the stall. No-op for pure-PTY providers and for a genuinely mid-turn /
2549
- // wedged native-source worker with no in-turn final assistant → the real stall
2550
- // fires below unchanged. Mutually exclusive with the pure-PTY path.
2551
- if (this.tryReconcileNativeSourceCompletionForStall(observedStatus)) {
2530
+ // TRANSCRIPT-COMPLETION-STALL-RESCUE (P2 of the transcript-authority
2531
+ // unification historically KIMI-PURE-PTY-COMPLETION-EMIT + KIMI-NATIVE-
2532
+ // SOURCE-COMPLETION-EMIT, two class-enumerated copies of the same rescue).
2533
+ // A finished worker whose generating_completed never emitted (idle→idle
2534
+ // collapse) sits at a STATIC idle prompt; the status-agnostic watchdog
2535
+ // would misread that finished-but-quiet state as a wedge and false-fire
2536
+ // monitor:no_progress. If the class-appropriate transcript (PTY parse or
2537
+ // authoritative native history completionFinalSummary picks) proves a
2538
+ // finished turn, emit the missing completion and SUPPRESS the stall. A
2539
+ // genuinely mid-turn / wedged worker with no in-turn final assistant falls
2540
+ // through and the real stall fires below unchanged.
2541
+ if (this.tryReconcileTranscriptCompletionForStall(observedStatus)) {
2552
2542
  this.meshStallEmittedForAnchor = true;
2553
2543
  return;
2554
2544
  }
@@ -2718,35 +2708,38 @@ export class CliProviderInstance implements ProviderInstance {
2718
2708
  }
2719
2709
 
2720
2710
  /**
2721
- * KIMI-PURE-PTY-COMPLETION-EMIT (Fix 3): stall-path completion reconcile for the
2722
- * PURE-PTY transcript class (kimi and kin no native transcript, no provider
2723
- * authority; see isPurePtyTranscriptProvider). Such a worker whose
2724
- * generating_completed never emitted (the onTurnStarted idle→idle collapse Fix 1
2725
- * fixes at the source) sits at a STATIC idle prompt: its PTY output stops the
2726
- * instant the answer is rendered, so the status-agnostic no-progress watchdog
2727
- * (checkMeshWorkerStall) reads the finished-but-quiet session as a stall and would
2728
- * false-fire monitor:no_progress. The native-transcript reconcile
2729
- * (sampleNativeTranscriptProgress) does NOT cover this class (no native source →
2730
- * null sample), so the stall path needs its own guard.
2711
+ * TRANSCRIPT-COMPLETION-STALL-RESCUE (P2 of the transcript-authority
2712
+ * unificationroot repo docs/design/2026-07-25-transcript-authority-
2713
+ * unification.md): stall-path completion reconcile for every class whose
2714
+ * turns can finish without a generating_completed emit (idle→idle collapse)
2715
+ * and then sit PTY-quiet at a static idle prompt.
2731
2716
  *
2732
- * Called from checkMeshWorkerStall just before the fire. Returns true when the
2733
- * session is a finished pure-PTY turn idle, no pending response, and a PTY-parsed
2734
- * in-turn final assistant summary — in which case it emits the missing
2735
- * generating_completed (idempotent: a real/late emit writes the terminal ledger and
2736
- * the coordinator's reconcile makes any duplicate a no-op) and the caller SUPPRESSES
2737
- * the stall. Returns false for every other class/state (native-source provider, a
2738
- * genuinely mid-turn or wedged worker with no final assistant), so a real stall still
2739
- * fires unchanged.
2717
+ * Historically this was TWO class-enumerated copies KIMI-PURE-PTY-
2718
+ * COMPLETION-EMIT (Fix 3) gated on isPurePtyTranscriptProvider, and
2719
+ * KIMI-NATIVE-SOURCE-COMPLETION-EMIT gated on isNativeSourceCanonicalHistory
2720
+ * whose bodies were identical because completionFinalSummary already picks
2721
+ * the class-appropriate evidence source (authoritative native transcript for
2722
+ * a native-source provider, the PTY parse otherwise). The enumeration itself
2723
+ * was the recurring defect: each new class had to be remembered at this site.
2724
+ * The profile collapses it: any class except daemon-owned is eligible, and
2725
+ * the evidence bar — not the class — decides.
2740
2726
  *
2741
- * Evidence bar is identical to flushMeshCompletionBeforeCleanup: injected task's turn
2742
- * genuinely started + an in-turn final assistant summary. Conservative by
2743
- * construction no summary no proof of completion return false and let the real
2744
- * stall fire.
2727
+ * Called from checkMeshWorkerStall just before the fire. Returns true when
2728
+ * the session is a finished turn idle, nothing pending, the injected
2729
+ * task's turn genuinely started, and an in-turn final assistant summary
2730
+ * in which case it emits the missing generating_completed (idempotent: a
2731
+ * real/late emit writes the terminal ledger and the coordinator's reconcile
2732
+ * makes any duplicate a no-op) and the caller SUPPRESSES the stall. Returns
2733
+ * false for a daemon-owned provider and for a genuinely mid-turn / wedged
2734
+ * worker with no in-turn final assistant, so a real stall still fires
2735
+ * unchanged. Evidence bar is identical to flushMeshCompletionBeforeCleanup.
2745
2736
  */
2746
- private tryReconcilePurePtyCompletionForStall(observedStatus: string): boolean {
2747
- // Only the pure-PTY class native-source providers are handled by
2748
- // sampleNativeTranscriptProgress above.
2749
- if (!isPurePtyTranscriptProvider(this.provider)) return false;
2737
+ private tryReconcileTranscriptCompletionForStall(observedStatus: string): boolean {
2738
+ // daemon-owned transcripts get real PTY turn events an idle-quiet
2739
+ // daemon-owned worker with no completion emit is a genuine anomaly the
2740
+ // stall should surface, exactly as before this unification.
2741
+ const profile = resolveTranscriptAuthorityProfile(this.provider);
2742
+ if (profile.class === 'daemon-owned') return false;
2750
2743
  // A finished turn is idle with nothing pending. A generating/waiting session is
2751
2744
  // mid-turn (the real stall path should evaluate it), so never complete it here.
2752
2745
  if (observedStatus !== 'idle') return false;
@@ -2770,6 +2763,10 @@ export class CliProviderInstance implements ProviderInstance {
2770
2763
  try {
2771
2764
  parsedMessages = this.adapter?.getScriptParsedStatus?.()?.messages;
2772
2765
  } catch { parsedMessages = undefined; }
2766
+ // completionFinalSummary turn-scopes the class-appropriate transcript (native
2767
+ // history for a native-source provider, the PTY parse otherwise) — a stale
2768
+ // prior-turn tail or a mid-turn worker with no in-turn final assistant yields
2769
+ // '' → return false below.
2773
2770
  let finalSummary: string | undefined;
2774
2771
  try {
2775
2772
  finalSummary = this.completionFinalSummary(parsedMessages, turnStartedAt);
@@ -2778,93 +2775,16 @@ export class CliProviderInstance implements ProviderInstance {
2778
2775
  // stall fire so a genuinely-wedged worker is still surfaced.
2779
2776
  if (!finalSummary) return false;
2780
2777
 
2781
- LOG.warn('CLI', `[${this.type}] reconciling pure-PTY mesh completion from the stall path for session ${this.instanceId} `
2778
+ // Telemetry keeps the historical per-class source strings so traces and
2779
+ // dashboards stay comparable across the unification.
2780
+ const diagnosticSource = profile.class === 'pure-pty'
2781
+ ? 'stall_pure_pty_transcript_completion'
2782
+ : 'stall_native_source_transcript_completion';
2783
+ LOG.warn('CLI', `[${this.type}] reconciling ${profile.class} mesh completion from the stall path for session ${this.instanceId} `
2782
2784
  + `task=${taskId ?? '(none)'} — PTY is idle-quiet with an in-turn final assistant message but the completion `
2783
- + `event never fired (pure-PTY provider); emitting it instead of a false monitor:no_progress.`);
2784
- if (this.isMeshWorkerSession()) {
2785
- traceMeshEventStage('fired', this.meshTraceCtx(), 'stall_pure_pty_transcript_completion');
2786
- }
2787
- this.emitGeneratingCompleted({
2788
- chatTitle: '',
2789
- duration: undefined,
2790
- timestamp: Date.now(),
2791
- taskId,
2792
- finalSummary,
2793
- evidenceLevel: 'transcript',
2794
- completionDiagnostic: { source: 'stall_pure_pty_transcript_completion' },
2795
- });
2796
- return true;
2797
- }
2798
-
2799
- /**
2800
- * KIMI-NATIVE-SOURCE-COMPLETION-EMIT (Fix 3): stall-path completion reconcile for a
2801
- * NATIVE-SOURCE provider (transcriptAuthority=provider + nativeHistory — e.g. kimi,
2802
- * whose authoritative transcript is ~/.kimi-code/.../wire.jsonl). Like the pure-PTY
2803
- * class, a native-source worker that submits a prompt while already idle can collapse
2804
- * idle→idle so agent:generating_completed never emits; its PTY then goes screen-quiet
2805
- * the instant the answer renders, and the status-agnostic no-progress watchdog
2806
- * (checkMeshWorkerStall) misreads that finished-but-static idle as a wedge and would
2807
- * false-fire monitor:no_progress. sampleNativeTranscriptProgress only re-arms while the
2808
- * transcript is STILL ADVANCING; once the turn is done the transcript is static and the
2809
- * sample stops rescuing — so the stall path needs this decisive completion check for the
2810
- * finished-and-quiet case. tryReconcilePurePtyCompletionForStall does NOT cover this
2811
- * class (isPurePtyTranscriptProvider is false for a native-source provider), so it runs
2812
- * as a fallback here.
2813
- *
2814
- * Mutually exclusive with the pure-PTY path by construction: a provider is either
2815
- * native-source (this method) or pure-PTY (that method), never both. Called from
2816
- * checkMeshWorkerStall only after tryReconcilePurePtyCompletionForStall returned false.
2817
- *
2818
- * Evidence bar is identical to the pure-PTY / pre-cleanup paths: idle with nothing
2819
- * pending, the injected task's turn genuinely started, and an in-turn final assistant
2820
- * summary — here resolved from the native transcript by completionFinalSummary (its
2821
- * native-source branch reads readExternalCompletionMessages). No summary ⇒ no proof of
2822
- * completion ⇒ return false and let the real stall fire so a genuinely-wedged worker is
2823
- * still surfaced. Idempotent: a real/late emit writes the terminal ledger and the
2824
- * coordinator's reconcile makes any duplicate a no-op.
2825
- */
2826
- private tryReconcileNativeSourceCompletionForStall(observedStatus: string): boolean {
2827
- // Only the native-source class — the pure-PTY class is handled above.
2828
- if (!isNativeSourceCanonicalHistory(this.provider?.nativeHistory)) return false;
2829
- // A finished turn is idle with nothing pending. A generating/waiting session is
2830
- // mid-turn (the real stall path should evaluate it), so never complete it here.
2831
- if (observedStatus !== 'idle') return false;
2832
- if (this.hasAdapterPendingResponse()) return false;
2833
-
2834
- const taskId = this.completingTurnTaskId();
2835
- // DOUBLE-EMIT guard (COMPLETION-WEAK-REARM fix1): suppress a re-emit only when this
2836
- // turn's completion already fired with GENUINE evidence. A prior WEAK emit is
2837
- // re-armable once a real generating→idle transition intervened (one-shot).
2838
- if (this.shouldSuppressCompletionReEmit(taskId)) {
2839
- return false;
2840
- }
2841
- // The injected task's own turn must have genuinely started (guards against a
2842
- // reused session's stale tail before this task ran).
2843
- if (!this.injectedTaskHasStartedGenerating()) return false;
2844
-
2845
- const turnStartedAt = typeof (this.adapter as any)?.currentTurnStartedAt === 'number'
2846
- ? (this.adapter as any).currentTurnStartedAt as number
2847
- : this.meshTaskInjectedAt || undefined;
2848
- let parsedMessages: unknown;
2849
- try {
2850
- parsedMessages = this.adapter?.getScriptParsedStatus?.()?.messages;
2851
- } catch { parsedMessages = undefined; }
2852
- // completionFinalSummary reads the AUTHORITATIVE native transcript (wire.jsonl) for
2853
- // this native-source provider and turn-scopes it — so a stale prior-turn tail or a
2854
- // mid-turn worker with no in-turn final assistant yields '' → return false below.
2855
- let finalSummary: string | undefined;
2856
- try {
2857
- finalSummary = this.completionFinalSummary(parsedMessages, turnStartedAt);
2858
- } catch { finalSummary = undefined; }
2859
- // No in-turn final assistant evidence → not a proven turn-end. Let the real stall
2860
- // fire so a genuinely-wedged worker is still surfaced.
2861
- if (!finalSummary) return false;
2862
-
2863
- LOG.warn('CLI', `[${this.type}] reconciling native-source mesh completion from the stall path for session ${this.instanceId} `
2864
- + `task=${taskId ?? '(none)'} — PTY is idle-quiet with an in-turn final assistant message in the native `
2865
- + `transcript but the completion event never fired; emitting it instead of a false monitor:no_progress.`);
2785
+ + `event never fired; emitting it instead of a false monitor:no_progress.`);
2866
2786
  if (this.isMeshWorkerSession()) {
2867
- traceMeshEventStage('fired', this.meshTraceCtx(), 'stall_native_source_transcript_completion');
2787
+ traceMeshEventStage('fired', this.meshTraceCtx(), diagnosticSource);
2868
2788
  }
2869
2789
  this.emitGeneratingCompleted({
2870
2790
  chatTitle: '',
@@ -2873,7 +2793,7 @@ export class CliProviderInstance implements ProviderInstance {
2873
2793
  taskId,
2874
2794
  finalSummary,
2875
2795
  evidenceLevel: 'transcript',
2876
- completionDiagnostic: { source: 'stall_native_source_transcript_completion' },
2796
+ completionDiagnostic: { source: diagnosticSource },
2877
2797
  });
2878
2798
  return true;
2879
2799
  }
@@ -3282,7 +3202,7 @@ export class CliProviderInstance implements ProviderInstance {
3282
3202
  // HOLD completion-timing class (`holdCompletionForTranscript`, e.g. antigravity-cli) via
3283
3203
  // its holdForTranscript block — NOT a hardcoded provider name — so claude-cli/codex-cli
3284
3204
  // (immediate/floor classes, which never produce a holdForTranscript block) are unchanged.
3285
- if ((this.provider as any)?.holdCompletionForTranscript === true
3205
+ if (resolveTranscriptAuthorityProfile(this.provider).timing === 'hold'
3286
3206
  && block.holdForTranscript === true
3287
3207
  && waitedMs < ANTIGRAVITY_HOLD_HARD_CAP_MS
3288
3208
  && this.antigravityHoldPtyStillActive()) {
@@ -3466,9 +3386,9 @@ export class CliProviderInstance implements ProviderInstance {
3466
3386
  }
3467
3387
 
3468
3388
  /**
3469
- * COMPLETION-WEAK-REARM (fix1): the double-emit guard shared by the three transcript
3470
- * re-emit paths (flushMeshCompletionBeforeCleanup, tryReconcilePurePtyCompletionForStall,
3471
- * tryReconcileNativeSourceCompletionForStall). Returns true when a re-emit for `taskId`
3389
+ * COMPLETION-WEAK-REARM (fix1): the double-emit guard shared by the transcript
3390
+ * re-emit paths (flushMeshCompletionBeforeCleanup,
3391
+ * tryReconcileTranscriptCompletionForStall). Returns true when a re-emit for `taskId`
3472
3392
  * must be SUPPRESSED because this turn's completion already fired with strong evidence.
3473
3393
  *
3474
3394
  * The defect this replaces: the old guard short-circuited on ANY prior emit for the
@@ -3980,7 +3900,7 @@ export class CliProviderInstance implements ProviderInstance {
3980
3900
  fcEvidenceSource = evidence.source;
3981
3901
  fcFinalSummary = extractFinalSummaryFromMessages(evidence.messages as any);
3982
3902
  } catch { /* best-effort */ }
3983
- const missingEvidence = ((this.provider as any).requiresFinalAssistantBeforeIdle === true || fcEvidenceSource === 'external-native') && !fcFinalSummary;
3903
+ const missingEvidence = (resolveTranscriptAuthorityProfile(this.provider).timing === 'floor' || fcEvidenceSource === 'external-native') && !fcFinalSummary;
3984
3904
  // Mirror the short-generating idle path's suppression: a provider that
3985
3905
  // requires a final assistant (or external-native history) with NO confirmed
3986
3906
  // summary and NO mesh context emits nothing — the session is idle with no
@@ -4285,7 +4205,7 @@ export class CliProviderInstance implements ProviderInstance {
4285
4205
  // (the mid-turn point-sample that triggered this whole false-idle bug) is treated
4286
4206
  // as weak/held, not fired as a genuine completion. A real shortFinalSummary being
4287
4207
  // present still clears the gate (the !shortFinalSummary guard is unchanged).
4288
- const missingEvidence = ((this.provider as any).requiresFinalAssistantBeforeIdle === true
4208
+ const missingEvidence = (resolveTranscriptAuthorityProfile(this.provider).timing === 'floor'
4289
4209
  || shortEvidenceSource === 'external-native'
4290
4210
  || shortEvidenceSource === 'unavailable') && !shortFinalSummary;
4291
4211
  if (missingEvidence) {
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Transcript authority profile — the single place that classifies a provider
3
+ * for completion/stall/redrive decisions.
4
+ *
5
+ * Historically five predicates coexisted (transcriptAuthority, nativeHistory
6
+ * presence, adapter chatMessagesOwnedExternally, isPurePtyTranscriptProvider,
7
+ * and the two completion-timing flags) and every judgment site combined a
8
+ * different subset — each recurring "pure-PTY gate excludes native-source"
9
+ * defect (kimi reconcile, antigravity tail/guard, codex floor, STARTED
10
+ * redrive) was an instance of a site enumerating only some classes. This
11
+ * module makes the enumeration itself the shared artifact: sites consume a
12
+ * profile, never the raw predicates.
13
+ *
14
+ * Design: docs/design/2026-07-25-transcript-authority-unification.md (root
15
+ * repo). This file is Phase P0 (layer A — pure, synchronous classification).
16
+ * Layer B (the evidence-query choke point) lands in later phases.
17
+ *
18
+ * Rule for NEW code: call resolveTranscriptAuthorityProfile() — do not call
19
+ * isPurePtyTranscriptProvider / isNativeSourceCanonicalHistory or read the
20
+ * timing flags directly (annotate `// authority-ok: <why>` where a raw read
21
+ * is genuinely about something other than completion classification).
22
+ */
23
+
24
+ import type { ProviderCanonicalHistoryConfig } from './contracts.js';
25
+ import { isPurePtyTranscriptProvider } from '../cli-adapters/provider-cli-shared.js';
26
+ import { isNativeSourceCanonicalHistory } from '../config/chat-history.js';
27
+
28
+ /** Where the authoritative transcript for completion evidence lives. */
29
+ export type TranscriptClass = 'native-source' | 'pure-pty' | 'daemon-owned';
30
+
31
+ /**
32
+ * When a completion may be emitted relative to the PTY idle verdict:
33
+ * - 'hold' — idle holds for the native transcript to land (antigravity).
34
+ * - 'floor' — idle without a final assistant holds under the CANON-C
35
+ * min-elapsed floor (codex / kimi / cursor / opencode).
36
+ * - 'immediate' — emit immediately; a write-lag native source upgrades the
37
+ * weak emit on reconcile (claude), daemon-owned emits plainly.
38
+ */
39
+ export type CompletionTiming = 'hold' | 'floor' | 'immediate';
40
+
41
+ export interface TranscriptAuthorityProfile {
42
+ class: TranscriptClass;
43
+ timing: CompletionTiming;
44
+ /** transcriptAuthority === 'provider' — provider parser output is canonical. */
45
+ providerOwnsTranscript: boolean;
46
+ /**
47
+ * False ⇒ this class may run whole turns without PTY generating_started /
48
+ * generating_completed events, so the ABSENCE of a turn-start event must
49
+ * never be read as "dispatch not consumed" (the STARTED-redrive rule,
50
+ * generalized). True only where PTY turn events are reliable.
51
+ */
52
+ emitsPtyTurnEvents: boolean;
53
+ /** The provider's native history config when the class is native-source. */
54
+ nativeHistory?: ProviderCanonicalHistoryConfig;
55
+ }
56
+
57
+ /**
58
+ * Structural input — matches how judgment sites actually hold providers
59
+ * (CliProviderModule / ProviderModule / nullable this.provider). Null or
60
+ * undefined resolves to the conservative daemon-owned/immediate default,
61
+ * matching the established null-provider ⇒ not-pure-PTY contract.
62
+ */
63
+ export interface TranscriptAuthorityInput {
64
+ transcriptAuthority?: 'provider' | 'daemon';
65
+ nativeHistory?: ProviderCanonicalHistoryConfig;
66
+ tui?: Record<string, unknown>;
67
+ requiresFinalAssistantBeforeIdle?: boolean;
68
+ holdCompletionForTranscript?: boolean;
69
+ }
70
+
71
+ export function resolveTranscriptAuthorityProfile(
72
+ provider: TranscriptAuthorityInput | null | undefined,
73
+ ): TranscriptAuthorityProfile {
74
+ if (!provider) {
75
+ return { class: 'daemon-owned', timing: 'immediate', providerOwnsTranscript: false, emitsPtyTurnEvents: true };
76
+ }
77
+
78
+ const nativeSource = isNativeSourceCanonicalHistory(provider.nativeHistory);
79
+ const transcriptClass: TranscriptClass = nativeSource
80
+ ? 'native-source'
81
+ : isPurePtyTranscriptProvider(provider)
82
+ ? 'pure-pty'
83
+ : 'daemon-owned';
84
+
85
+ const timing: CompletionTiming = provider.holdCompletionForTranscript === true
86
+ ? 'hold'
87
+ : provider.requiresFinalAssistantBeforeIdle === true
88
+ ? 'floor'
89
+ : 'immediate';
90
+
91
+ // PTY turn events are unreliable for (a) pure-PTY (turns collapse
92
+ // idle→idle; the parser, not the PTY event stream, notices the turn) and
93
+ // (b) native-source classes that hold/floor precisely because their turn
94
+ // activity lives in the native transcript rather than the PTY.
95
+ const emitsPtyTurnEvents = transcriptClass === 'daemon-owned'
96
+ || (transcriptClass === 'native-source' && timing === 'immediate');
97
+
98
+ return {
99
+ class: transcriptClass,
100
+ timing,
101
+ providerOwnsTranscript: provider.transcriptAuthority === 'provider',
102
+ emitsPtyTurnEvents,
103
+ ...(nativeSource && provider.nativeHistory ? { nativeHistory: provider.nativeHistory } : {}),
104
+ };
105
+ }
@@ -1183,6 +1183,14 @@ export interface LocalMeshNodeEntry {
1183
1183
  * first envelope carrying it is ingested.
1184
1184
  */
1185
1185
  reportedMemberState?: MeshReportedMemberState;
1186
+ /**
1187
+ * Versioned per-machine runtime facts bundle (MeshNodeFacts — deploy-lag
1188
+ * visibility design §a). Remote nodes: ingested wholesale from the
1189
+ * git_status envelope's reporterNodeFacts. Self/worktree nodes: built by
1190
+ * the SAME producer (buildLocalNodeFacts). Relayed opaquely — surfaces
1191
+ * must pass the object through, never rebuild it field-by-field.
1192
+ */
1193
+ nodeFacts?: import('@adhdev/mesh-shared').MeshNodeFacts;
1186
1194
  /**
1187
1195
  * The operator-set machine nickname (config.machineNickname) of the daemon
1188
1196
  * that owns this node's workspace. The local coordinator stamps its own
@@ -1302,6 +1310,14 @@ export interface RepoMeshStatus {
1302
1310
  * an optional `stats` operational rollup (durations / retries).
1303
1311
  */
1304
1312
  missions?: (MeshMissionSummary | MeshMissionSlimSummary)[];
1313
+ /**
1314
+ * Preview-deploy freshness rollup (deploy-lag visibility): stamped by the
1315
+ * dashboard mesh_status when the repo has a preview pipeline. Shape is
1316
+ * daemon-defined (mesh/preview-freshness.ts) — `currentMainCommit` is the
1317
+ * global deploy-lag anchor the dashboard compares nodeFacts.daemonBuild
1318
+ * against. Omitted for repos without the pipeline and by older daemons.
1319
+ */
1320
+ previewFreshness?: Record<string, unknown>;
1305
1321
  /**
1306
1322
  * MAGI cross-verification activity, reconstructed from the mesh ledger
1307
1323
  * (magi_dispatched / magi_synthesis entries) and folded in so the dashboard's
@@ -1476,6 +1492,13 @@ export interface RepoMeshNodeStatus {
1476
1492
  * (scope/isDaemonAffecting flags). Omitted when the build is current.
1477
1493
  */
1478
1494
  staleDaemonBuild?: Record<string, unknown>;
1495
+ /**
1496
+ * Versioned per-machine runtime facts bundle (MeshNodeFacts). Opaque
1497
+ * pass-through from the node record — see the node-level field doc.
1498
+ * Carries the daemon build COMMIT (not just the version string), which is
1499
+ * the deploy-lag anchor the dashboard renders.
1500
+ */
1501
+ nodeFacts?: import('@adhdev/mesh-shared').MeshNodeFacts;
1479
1502
  error?: string;
1480
1503
  }
1481
1504