@adhdev/daemon-core 0.9.82-rc.460 → 0.9.82-rc.462
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/config/mesh-config.d.ts +7 -0
- package/dist/detection/cli-detector.d.ts +17 -0
- package/dist/git/git-commands.d.ts +14 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +567 -28
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +565 -27
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-pending.d.ts +75 -1
- package/dist/mesh/mesh-events.d.ts +2 -2
- package/dist/mesh/mesh-node-identity.d.ts +4 -0
- package/dist/mesh/mesh-reconcile-loop.d.ts +12 -0
- package/dist/mesh/mesh-runtime-store.d.ts +17 -0
- package/dist/providers/approval-utils.d.ts +1 -0
- package/dist/repo-mesh-types.d.ts +111 -0
- package/package.json +3 -3
- package/src/boot/daemon-lifecycle.ts +15 -2
- package/src/commands/high-family/mesh-events.ts +14 -1
- package/src/commands/high-family/mesh-status.ts +29 -2
- package/src/config/mesh-config.ts +13 -0
- package/src/detection/cli-detector.ts +66 -0
- package/src/git/git-commands.ts +35 -2
- package/src/index.ts +1 -1
- package/src/mesh/coordinator-prompt.ts +19 -1
- package/src/mesh/mesh-event-forwarding.ts +19 -1
- package/src/mesh/mesh-events-pending.ts +465 -5
- package/src/mesh/mesh-events.ts +5 -0
- package/src/mesh/mesh-node-identity.ts +77 -6
- package/src/mesh/mesh-reconcile-loop.ts +74 -1
- package/src/mesh/mesh-runtime-store.ts +30 -0
- package/src/providers/approval-utils.ts +1 -1
- package/src/providers/cli-provider-instance.ts +24 -12
- package/src/repo-mesh-types.ts +111 -0
|
@@ -45,7 +45,7 @@ import type { LocalMeshEntry } from '../repo-mesh-types.js';
|
|
|
45
45
|
import { loadConfig } from '../config/config.js';
|
|
46
46
|
import { listMeshes } from '../config/mesh-config.js';
|
|
47
47
|
import { LOG, getLogLevel } from '../logging/logger.js';
|
|
48
|
-
import { drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, buildPendingEventFingerprint, queuePendingMeshCoordinatorEvent } from './mesh-events-pending.js';
|
|
48
|
+
import { drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, buildPendingEventFingerprint, queuePendingMeshCoordinatorEvent, serializeV2EnvelopeToWire } from './mesh-events-pending.js';
|
|
49
49
|
import type { PendingMeshCoordinatorEvent } from './mesh-events-pending.js';
|
|
50
50
|
import { appendLedgerEntry } from './mesh-ledger.js';
|
|
51
51
|
import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
@@ -258,6 +258,59 @@ const inFlightAckedHoldState = new Map<string, AckedHoldState>();
|
|
|
258
258
|
// A restart resets this set, so the first touch of each mesh reloads from disk.
|
|
259
259
|
const rehydratedHoldMeshes = new Set<string>();
|
|
260
260
|
|
|
261
|
+
// ─── T6 (B3c): PHASE-4 synthesis + acked-hold fast-track demoted to last-resort ──
|
|
262
|
+
//
|
|
263
|
+
// Under mesh-protocol-v2 enforce, the completion contract is explicit: a worker's
|
|
264
|
+
// terminal emit is a v2 unicast event drained straight to the coordinator. The
|
|
265
|
+
// PHASE-4 transcript-synthesis backstop and the acked-hold fast-track exist to
|
|
266
|
+
// paper over a LOST emit — they should NEVER fire once v2 delivery is healthy. So
|
|
267
|
+
// their firing is now a demoted last-resort signal: every fire bumps a counter, and
|
|
268
|
+
// under enforce a fire additionally emits a WARN naming it a v2-contract violation
|
|
269
|
+
// (a real emit was expected but never arrived). Target = 0 fires in steady state.
|
|
270
|
+
//
|
|
271
|
+
// The code is NOT removed — it stays as the correctness net for a genuinely lost
|
|
272
|
+
// emit (rollout plan §B3c: "코드 삭제는 하지 않고 관측 후 다음 사이클에 판단"). Process-
|
|
273
|
+
// lifetime totals; read by tests + surfaced in mesh_status.
|
|
274
|
+
const meshV2BackstopCounters = {
|
|
275
|
+
/** PHASE-4 transcript synthesis actually reconciled a missing completion. */
|
|
276
|
+
phase4SynthesisFired: 0,
|
|
277
|
+
/** Acked-hold transcript fast-track promoted a synth ahead of the death deadline. */
|
|
278
|
+
ackedHoldFastTrackFired: 0,
|
|
279
|
+
/** Acked-hold death-deadline backstop released a held synth (the 8-min net). */
|
|
280
|
+
ackedHoldDeathDeadlineFired: 0,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/** Test/observability accessor for the v2 last-resort backstop counters (snapshot). */
|
|
284
|
+
export function getMeshV2BackstopCounters(): Readonly<typeof meshV2BackstopCounters> {
|
|
285
|
+
return { ...meshV2BackstopCounters };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Test helper: zero the backstop counters so a case starts from a clean slate. */
|
|
289
|
+
export function __resetMeshV2BackstopCountersForTests(): void {
|
|
290
|
+
for (const k of Object.keys(meshV2BackstopCounters) as Array<keyof typeof meshV2BackstopCounters>) {
|
|
291
|
+
meshV2BackstopCounters[k] = 0;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Enforce switch mirror (see isMeshProtocolV2EnforceEnabled in mesh-events-pending);
|
|
296
|
+
* re-read here (not imported) to keep the reconcile loop free of a cross-file coupling
|
|
297
|
+
* and to read env at fire time. Same truthy vocabulary. */
|
|
298
|
+
function meshProtocolV2EnforceOn(): boolean {
|
|
299
|
+
const raw = process.env.MESH_PROTOCOL_V2_ENFORCE;
|
|
300
|
+
if (typeof raw !== 'string') return false;
|
|
301
|
+
const v = raw.trim().toLowerCase();
|
|
302
|
+
return v === '1' || v === 'true' || v === 'on' || v === 'yes';
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Bump a backstop counter and, under enforce, WARN that a last-resort net fired —
|
|
306
|
+
* which under a healthy v2 contract should not happen (the real emit was lost). */
|
|
307
|
+
function recordBackstopFire(kind: keyof typeof meshV2BackstopCounters, detail: string): void {
|
|
308
|
+
meshV2BackstopCounters[kind]++;
|
|
309
|
+
if (meshProtocolV2EnforceOn()) {
|
|
310
|
+
LOG.warn('MeshReconcileV2', `v2 ENFORCE last-resort backstop fired (${kind}): ${detail}. Under a healthy v2 completion contract this should be 0 — a worker's real terminal emit was lost/late.`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
261
314
|
function inFlightSynthKey(meshId: string, taskId: string): string {
|
|
262
315
|
return `${meshId}::${taskId}`;
|
|
263
316
|
}
|
|
@@ -2011,6 +2064,12 @@ async function reconcileUnterminatedDirectDispatches(
|
|
|
2011
2064
|
|
|
2012
2065
|
const synthKey = inFlightSynthKey(mesh.id, taskId);
|
|
2013
2066
|
const isAcked = dispatch.status === 'acked';
|
|
2067
|
+
// T6: which last-resort backstop (if any) drove this synth. Set when the
|
|
2068
|
+
// acked-hold fast-track / death-deadline promotes the synth; the counter is
|
|
2069
|
+
// bumped only if the synth actually COMMITS (result.reconciled), so a
|
|
2070
|
+
// deferred/re-probed-away synth is not miscounted. A never-acked dispatch
|
|
2071
|
+
// that reaches the commit is a plain PHASE-4 transcript synthesis.
|
|
2072
|
+
let backstopKind: keyof ReturnType<typeof getMeshV2BackstopCounters> | undefined;
|
|
2014
2073
|
|
|
2015
2074
|
// R4f: read the worker session. A FAILED read (transport error / success:false / no payload)
|
|
2016
2075
|
// is no longer silently swallowed for an acked task — it is the liveness side of the
|
|
@@ -2130,6 +2189,7 @@ async function reconcileUnterminatedDirectDispatches(
|
|
|
2130
2189
|
const idleHeldMs = nowMs - idleSinceMs;
|
|
2131
2190
|
if (idleHeldMs >= fastTrackGraceMs) {
|
|
2132
2191
|
fastTrackReady = true;
|
|
2192
|
+
backstopKind = 'ackedHoldFastTrackFired';
|
|
2133
2193
|
LOG.info('MeshReconcile', `Acked-hold transcript fast-track: task ${taskId} on node ${nodeId} (mesh ${mesh.id}) read idle WITH a final assistant message for ${Math.round(idleHeldMs / 1000)}s continuous (grace ${Math.round(fastTrackGraceMs / 1000)}s) — promoting the synth ahead of the ${Math.round(deathDeadlineMs / 1000)}s death backstop; the worker's real emit was lost/late and a later one no-ops idempotently.`);
|
|
2134
2194
|
}
|
|
2135
2195
|
} else if (holdState?.transcriptIdleSinceMs !== undefined) {
|
|
@@ -2144,6 +2204,7 @@ async function reconcileUnterminatedDirectDispatches(
|
|
|
2144
2204
|
continue;
|
|
2145
2205
|
}
|
|
2146
2206
|
if (!fastTrackReady) {
|
|
2207
|
+
backstopKind = 'ackedHoldDeathDeadlineFired';
|
|
2147
2208
|
LOG.warn('MeshReconcile', `Acked-hold death deadline reached: task ${taskId} on node ${nodeId} (mesh ${mesh.id}) still idle ${Math.round(sinceAckMs / 1000)}s after the ack (deadline ${Math.round(deathDeadlineMs / 1000)}s) — synthesizing the missing completion as a notification-loss net (a real emit, if it ever lands, no-ops idempotently).`);
|
|
2148
2209
|
}
|
|
2149
2210
|
}
|
|
@@ -2221,6 +2282,11 @@ async function reconcileUnterminatedDirectDispatches(
|
|
|
2221
2282
|
source: 'daemon_reconcile_transcript_completion',
|
|
2222
2283
|
});
|
|
2223
2284
|
if (result.reconciled) {
|
|
2285
|
+
// T6: this synth actually committed → count the last-resort backstop fire.
|
|
2286
|
+
// An acked hold routes to the fast-track / death-deadline kind captured
|
|
2287
|
+
// above; a never-acked dispatch is a plain PHASE-4 transcript synthesis.
|
|
2288
|
+
// Under enforce, recordBackstopFire additionally WARNs (target = 0 fires).
|
|
2289
|
+
recordBackstopFire(backstopKind ?? 'phase4SynthesisFired', `task ${taskId} on node ${nodeId} (mesh ${mesh.id}), kind=${result.kind}`);
|
|
2224
2290
|
LOG.info('MeshReconcile', `Synthesized missing completion (${result.kind}) for task ${taskId} on node ${nodeId} (mesh ${mesh.id})`);
|
|
2225
2291
|
}
|
|
2226
2292
|
} catch (e: any) {
|
|
@@ -2360,6 +2426,13 @@ function buildForwardPayloadFromPending(event: any): Record<string, unknown> {
|
|
|
2360
2426
|
const tid = readNonEmptyString(metadata.taskId) || readNonEmptyString(metadata.meshActiveTaskId);
|
|
2361
2427
|
return tid ? { taskId: tid } : {};
|
|
2362
2428
|
})(),
|
|
2429
|
+
// T4 (B3b): carry the v2 envelope (protocolVersion/eventId/scope/dispatchedBy/
|
|
2430
|
+
// intendedFor) across the P2P relay boundary at the TOP LEVEL. These live on the
|
|
2431
|
+
// pending event itself, not inside metadataEvent, so without this the remote pull
|
|
2432
|
+
// re-queue would re-stamp a fresh eventId — breaking cross-machine idempotency and
|
|
2433
|
+
// downgrading the relayed completion to v1 broadcast routing. Spread LAST so the
|
|
2434
|
+
// authoritative envelope always wins over any stale key the metadata spread carried.
|
|
2435
|
+
...serializeV2EnvelopeToWire(event as PendingMeshCoordinatorEvent),
|
|
2363
2436
|
};
|
|
2364
2437
|
}
|
|
2365
2438
|
|
|
@@ -1917,6 +1917,36 @@ export class MeshRuntimeStore {
|
|
|
1917
1917
|
return row !== undefined;
|
|
1918
1918
|
}
|
|
1919
1919
|
|
|
1920
|
+
/**
|
|
1921
|
+
* B3a — v2 eventId idempotency. Returns true when a row with this event_id has
|
|
1922
|
+
* ALREADY been drained (drained = 1) for the mesh. Drained rows are retained
|
|
1923
|
+
* (soft-marked, not deleted until mesh deletion), so this is a durable, restart-
|
|
1924
|
+
* surviving dedup: a v2 event whose eventId was already consumed is skipped on
|
|
1925
|
+
* re-delivery even when its content fingerprint differs. Scoped by mesh_id +
|
|
1926
|
+
* the partial event_id index (idx_mesh_pending_events_event_id).
|
|
1927
|
+
*/
|
|
1928
|
+
hasDrainedEventId(meshId: string, eventId: string): boolean {
|
|
1929
|
+
if (!eventId) return false;
|
|
1930
|
+
const row = this.db.prepare(
|
|
1931
|
+
'SELECT 1 FROM mesh_pending_events WHERE mesh_id = ? AND event_id = ? AND drained = 1 LIMIT 1'
|
|
1932
|
+
).get(meshId, eventId);
|
|
1933
|
+
return row !== undefined;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
/**
|
|
1937
|
+
* B3a — snapshot of the v2 event_ids ALREADY drained (drained = 1) for the mesh.
|
|
1938
|
+
* Taken BEFORE a drain call marks the current batch drained=1, so the resulting
|
|
1939
|
+
* set names only PRIOR drains — the re-delivery dedup baseline. (Reading it after
|
|
1940
|
+
* the drain would self-match the batch's own freshly-drained rows.) Non-v2 rows
|
|
1941
|
+
* have a NULL event_id and are excluded by the index/WHERE.
|
|
1942
|
+
*/
|
|
1943
|
+
drainedEventIdsForMesh(meshId: string): Set<string> {
|
|
1944
|
+
const rows = this.db.prepare(
|
|
1945
|
+
'SELECT DISTINCT event_id FROM mesh_pending_events WHERE mesh_id = ? AND drained = 1 AND event_id IS NOT NULL'
|
|
1946
|
+
).all(meshId) as Array<{ event_id: string }>;
|
|
1947
|
+
return new Set(rows.map(r => r.event_id));
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1920
1950
|
// ── M3: Mission Records ─────────────────────────────────────────────────
|
|
1921
1951
|
|
|
1922
1952
|
upsertMission(mission: {
|
|
@@ -16,7 +16,7 @@ const DEFAULT_APPROVAL_POSITIVE_HINTS = [
|
|
|
16
16
|
'always allow',
|
|
17
17
|
];
|
|
18
18
|
|
|
19
|
-
function normalizeApprovalLabel(value: string): string {
|
|
19
|
+
export function normalizeApprovalLabel(value: string): string {
|
|
20
20
|
return String(value || '')
|
|
21
21
|
.toLowerCase()
|
|
22
22
|
.replace(/^[\s\[(<{]*\d+(?:\s*[.)\]}>:-]|\s)+/, '')
|
|
@@ -27,7 +27,7 @@ import { shouldCollectTraceCategory } from '../logging/debug-config.js';
|
|
|
27
27
|
import { traceMeshEventStage, traceMeshEventDrop } from '../mesh/mesh-event-trace.js';
|
|
28
28
|
import type { ChatMessage } from '../types.js';
|
|
29
29
|
import { buildPersistedProviderEffectMessage, normalizeProviderEffects } from './control-effects.js';
|
|
30
|
-
import { formatAutoApprovalMessage, pickApprovalButton, hasNegativeApprovalOption, hasReliableApprovalAffirmative, looksLikeActiveApprovalPromptText } from './approval-utils.js';
|
|
30
|
+
import { formatAutoApprovalMessage, pickApprovalButton, hasNegativeApprovalOption, hasReliableApprovalAffirmative, looksLikeActiveApprovalPromptText, normalizeApprovalLabel } from './approval-utils.js';
|
|
31
31
|
import { getCliScriptCommand, parseCliScriptResult } from './cli-script-results.js';
|
|
32
32
|
import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
|
|
33
33
|
import { normalizeProviderSessionId } from './provider-session-id.js';
|
|
@@ -2317,19 +2317,31 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2317
2317
|
// kind gate). Surface the modal so the user decides; never pick blindly.
|
|
2318
2318
|
return autoApproveActive;
|
|
2319
2319
|
}
|
|
2320
|
-
// Modal *identity* signature — the question
|
|
2321
|
-
//
|
|
2322
|
-
// approvalEntrySeq on every fresh
|
|
2323
|
-
// modal→generating→modal flap (the question
|
|
2324
|
-
// captured frame while the button block stays)
|
|
2325
|
-
// again. Folding that seq into the settle signature
|
|
2326
|
-
// settle clock restart on every flap, so the modal never
|
|
2327
|
-
// long enough to fire — the gate was never satisfied.
|
|
2328
|
-
// the seq so
|
|
2320
|
+
// Modal *identity* signature — the question plus the STABLE affirmative
|
|
2321
|
+
// anchor only, NO volatile counters and NO raw button set. This is what
|
|
2322
|
+
// the settle gate tracks: the FSM bumps approvalEntrySeq on every fresh
|
|
2323
|
+
// waiting_approval entry, and a modal→generating→modal flap (the question
|
|
2324
|
+
// line scrolled out of the captured frame while the button block stays)
|
|
2325
|
+
// re-enters and bumps it again. Folding that seq into the settle signature
|
|
2326
|
+
// made the 600ms settle clock restart on every flap, so the modal never
|
|
2327
|
+
// stayed stable long enough to fire — the gate was never satisfied.
|
|
2328
|
+
// Identity excludes the seq so seq flap of the SAME modal keeps one clock.
|
|
2329
|
+
//
|
|
2330
|
+
// The raw button set is also excluded: on a TALL Write/Edit diff claude's
|
|
2331
|
+
// TUI repaints the button block 3↔5↔none between frames (buttons scroll in
|
|
2332
|
+
// and out of the captured region), which flipped both buttons.join('|') and
|
|
2333
|
+
// the positional buttonIndex every frame → signature flap → the settle
|
|
2334
|
+
// clock reset 4–9s and only mask-stalled episodes leaked to the coordinator
|
|
2335
|
+
// (AUTOAPPROVE-SETTLE-FLAP). The affirmative the auto-approve will actually
|
|
2336
|
+
// press is the invariant across those repaints, so we anchor on its
|
|
2337
|
+
// NORMALIZED label (numbers/bullets/punctuation stripped, so "1. Yes" and
|
|
2338
|
+
// "3. Yes" collapse to "yes"). Message + affirmative label uniquely
|
|
2339
|
+
// identifies the consent question without tracking the volatile button
|
|
2340
|
+
// positions.
|
|
2341
|
+
const affirmativeAnchor = normalizeApprovalLabel(buttonLabel);
|
|
2329
2342
|
const modalSignature = [
|
|
2330
2343
|
typeof modal?.message === 'string' ? modal.message.trim() : '',
|
|
2331
|
-
|
|
2332
|
-
buttonIndex,
|
|
2344
|
+
affirmativeAnchor,
|
|
2333
2345
|
].join('::');
|
|
2334
2346
|
// Busy-window re-entry guard still needs the seq: two DISTINCT
|
|
2335
2347
|
// back-to-back approvals can carry identical message/buttons (common
|
package/src/repo-mesh-types.ts
CHANGED
|
@@ -703,6 +703,23 @@ export interface RepoMeshNodeCapabilities {
|
|
|
703
703
|
canPush?: boolean;
|
|
704
704
|
readOnly?: boolean;
|
|
705
705
|
userLabels?: string[];
|
|
706
|
+
/**
|
|
707
|
+
* Detected provider CLI/ACP versions on this node, keyed by provider id
|
|
708
|
+
* (e.g. `{ 'claude-cli': '1.2.3', 'codex-cli': '0.9.0' }`). Populated from the
|
|
709
|
+
* same CLI detection pass that feeds providerPriority (see buildProviderVersions
|
|
710
|
+
* over detectCLIs' CLIInfo[]). Absent/undefined when detection has not run or a
|
|
711
|
+
* daemon predates the exposure — never a hard signal, purely observability so a
|
|
712
|
+
* coordinator can spot a provider-version skew across nodes before dispatch.
|
|
713
|
+
* Additive: existing status consumers ignore it.
|
|
714
|
+
*/
|
|
715
|
+
providerVersions?: Record<string, string>;
|
|
716
|
+
/**
|
|
717
|
+
* The daemon build version (package.json version baked into the running bundle,
|
|
718
|
+
* see getDaemonBuildInfo().version) that detected the above providerVersions.
|
|
719
|
+
* Complements the commit-level daemonBuild stamp with a human-readable version
|
|
720
|
+
* for node-card rendering. Absent when the build define was not injected.
|
|
721
|
+
*/
|
|
722
|
+
daemonBuildVersion?: string;
|
|
706
723
|
}
|
|
707
724
|
|
|
708
725
|
export interface DetectedCommand {
|
|
@@ -849,6 +866,19 @@ export interface LocalMeshNodeEntry {
|
|
|
849
866
|
*/
|
|
850
867
|
reportedPlatform?: string;
|
|
851
868
|
reportedArch?: string;
|
|
869
|
+
/**
|
|
870
|
+
* Live, self-healed provider CLI/ACP versions reported by the daemon that owns
|
|
871
|
+
* this node's workspace, carried on the git_status envelope (reporterProviderVersions)
|
|
872
|
+
* and persisted by the coordinator on each direct git probe — mirrors the
|
|
873
|
+
* reportedPlatform/reportedArch self-heal pattern. Auto-detected truth, overwritten
|
|
874
|
+
* by the next report so a stale value never sticks. Absent until the first probe
|
|
875
|
+
* carrying versions succeeds. Surfaced as RepoMeshNodeStatus.providerVersions.
|
|
876
|
+
*/
|
|
877
|
+
reportedProviderVersions?: Record<string, string>;
|
|
878
|
+
/** Live, self-healed daemon build version (getDaemonBuildInfo().version) of the
|
|
879
|
+
* owning daemon, carried on the git_status envelope (reporterDaemonBuildVersion)
|
|
880
|
+
* alongside the provider versions. Absent until first reported. */
|
|
881
|
+
reportedDaemonBuildVersion?: string;
|
|
852
882
|
/**
|
|
853
883
|
* The operator-set machine nickname (config.machineNickname) of the daemon
|
|
854
884
|
* that owns this node's workspace. The local coordinator stamps its own
|
|
@@ -974,6 +1004,76 @@ export interface RepoMeshStatus {
|
|
|
974
1004
|
* optional. Mirrors the MCP `mesh_status` tool's `magiActivity` field.
|
|
975
1005
|
*/
|
|
976
1006
|
magiActivity?: MeshMagiActivitySummary[];
|
|
1007
|
+
/**
|
|
1008
|
+
* T7 (visibility 7-2b): provider CLI/ACP version skew across nodes. Each entry
|
|
1009
|
+
* names a provider running ≥2 distinct versions across the nodes that reported
|
|
1010
|
+
* it, with the node ids per version. Observational only — never a dispatch
|
|
1011
|
+
* blocker. Omitted when every reported provider is uniform (or none reported).
|
|
1012
|
+
* Mirrors the MCP `mesh_status` tool's `providerVersionSkew` field.
|
|
1013
|
+
*/
|
|
1014
|
+
providerVersionSkew?: MeshProviderVersionSkew[];
|
|
1015
|
+
/** Human-readable companion warning to providerVersionSkew. Omitted when no skew. */
|
|
1016
|
+
providerVersionSkewWarning?: string;
|
|
1017
|
+
/**
|
|
1018
|
+
* T7 (B4): mesh-protocol-v2 adoption metrics for the batch of pending events
|
|
1019
|
+
* surfaced in the drain backing this status. Snapshot, not a durable counter.
|
|
1020
|
+
* Omitted when nothing was drained. Mirrors the MCP tool's meshProtocolMetrics.
|
|
1021
|
+
*/
|
|
1022
|
+
meshProtocolMetrics?: MeshProtocolMetrics;
|
|
1023
|
+
/**
|
|
1024
|
+
* T6 (B3c): live process-lifetime mesh-protocol-v2 enforce counters from THIS
|
|
1025
|
+
* daemon — the enforce flag state, drain-routing tallies (deliver / route-away /
|
|
1026
|
+
* dedup / quarantine), and the last-resort backstop fire counts (PHASE-4 synth,
|
|
1027
|
+
* acked-hold fast-track / death-deadline). Diagnostic-only and never cached (a
|
|
1028
|
+
* live snapshot). Under enforce, non-zero quarantine or backstop counts are the
|
|
1029
|
+
* rollout-health signal (target 0). Omitted when unavailable.
|
|
1030
|
+
*/
|
|
1031
|
+
meshProtocolV2Counters?: MeshProtocolV2Counters;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/** T6 (B3c) live v2 enforce/observability counters (see RepoMeshStatus.meshProtocolV2Counters). */
|
|
1035
|
+
export interface MeshProtocolV2Counters {
|
|
1036
|
+
/** True when MESH_PROTOCOL_V2_ENFORCE is active on this daemon. */
|
|
1037
|
+
enforce: boolean;
|
|
1038
|
+
/** Drain-path routing tallies (accept + enforce). Process-lifetime totals. */
|
|
1039
|
+
drain: {
|
|
1040
|
+
v2Delivered: number;
|
|
1041
|
+
v2RoutedAway: number;
|
|
1042
|
+
v2DedupSkipped: number;
|
|
1043
|
+
v2ValidationFailedAccepted: number;
|
|
1044
|
+
v2ReattributedToDrainer: number;
|
|
1045
|
+
v1BroadcastAccepted: number;
|
|
1046
|
+
v2ValidationFailedQuarantined: number;
|
|
1047
|
+
v1UnversionedQuarantined: number;
|
|
1048
|
+
};
|
|
1049
|
+
/** Last-resort backstop fire counts. Target 0 under a healthy v2 contract. */
|
|
1050
|
+
backstop: {
|
|
1051
|
+
phase4SynthesisFired: number;
|
|
1052
|
+
ackedHoldFastTrackFired: number;
|
|
1053
|
+
ackedHoldDeathDeadlineFired: number;
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
/** One provider's version skew across mesh nodes (see RepoMeshStatus.providerVersionSkew). */
|
|
1058
|
+
export interface MeshProviderVersionSkew {
|
|
1059
|
+
/** Provider id (e.g. 'claude-cli'). */
|
|
1060
|
+
provider: string;
|
|
1061
|
+
/** Each distinct detected version and the node ids running it. */
|
|
1062
|
+
versions: Array<{ version: string; nodeIds: string[] }>;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/** Mesh-protocol-v2 adoption snapshot over one drain (see RepoMeshStatus.meshProtocolMetrics). */
|
|
1066
|
+
export interface MeshProtocolMetrics {
|
|
1067
|
+
/** Total pending events surfaced in the drain. */
|
|
1068
|
+
total: number;
|
|
1069
|
+
/** Count carrying a v2 envelope (protocolVersion '2.0'). */
|
|
1070
|
+
v2: number;
|
|
1071
|
+
/** Count still on v1 (unstamped). */
|
|
1072
|
+
v1: number;
|
|
1073
|
+
/** v2/total, rounded to 2 decimals (0 when total is 0). */
|
|
1074
|
+
v2Ratio: number;
|
|
1075
|
+
/** Scope breakdown of the v2 events (unicast/broadcast/system/unspecified → count). */
|
|
1076
|
+
scopes: Record<string, number>;
|
|
977
1077
|
}
|
|
978
1078
|
|
|
979
1079
|
// RepoMeshSessionStatus shape now lives in @adhdev/mesh-shared (shared with
|
|
@@ -1026,6 +1126,17 @@ export interface RepoMeshNodeStatus {
|
|
|
1026
1126
|
*/
|
|
1027
1127
|
gitProbePending?: boolean;
|
|
1028
1128
|
providers: string[];
|
|
1129
|
+
/**
|
|
1130
|
+
* Detected provider CLI/ACP versions on this node, keyed by provider id. Mirrors
|
|
1131
|
+
* RepoMeshNodeCapabilities.providerVersions onto the status snapshot so the mesh
|
|
1132
|
+
* UI / coordinator prompt can render per-provider versions and flag a version
|
|
1133
|
+
* skew across nodes. Optional — omitted by daemons predating the exposure or when
|
|
1134
|
+
* detection has not run. Additive; existing consumers ignore it. */
|
|
1135
|
+
providerVersions?: Record<string, string>;
|
|
1136
|
+
/** Human-readable daemon build version (getDaemonBuildInfo().version) of the
|
|
1137
|
+
* daemon that owns this node. Complements the per-daemon commit stamp
|
|
1138
|
+
* (daemonBuilds) for node-card display. Omitted when unknown. */
|
|
1139
|
+
daemonBuildVersion?: string;
|
|
1029
1140
|
activeSessions: string[];
|
|
1030
1141
|
activeSessionDetails?: RepoMeshSessionStatus[];
|
|
1031
1142
|
providerPriority?: string[];
|