@adhdev/daemon-core 0.9.82-rc.326 → 0.9.82-rc.328
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 +5 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +529 -337
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +528 -337
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-active-work.d.ts +66 -0
- package/dist/mesh/mesh-work-queue.d.ts +2 -0
- package/dist/repo-mesh-types.d.ts +12 -0
- package/package.json +2 -2
- package/src/commands/router.ts +47 -6
- package/src/config/mesh-config.ts +7 -0
- package/src/index.ts +2 -2
- package/src/mesh/mesh-active-work.ts +154 -0
- package/src/mesh/mesh-reconcile-loop.ts +144 -1
- package/src/mesh/mesh-work-queue.ts +28 -8
- package/src/repo-mesh-types.ts +12 -0
|
@@ -101,6 +101,72 @@ export type StaleDirectPruneClassification = 'prunable_orphan' | 'prunable_termi
|
|
|
101
101
|
export declare function classifyStaleDirectForPrune(record: Pick<MeshActiveWorkRecord, 'staleReason' | 'staleDispatchUnacknowledged' | 'terminal'>, opts?: {
|
|
102
102
|
includeTerminal?: boolean;
|
|
103
103
|
}): StaleDirectPruneClassification;
|
|
104
|
+
/**
|
|
105
|
+
* Outcome of one staleDirect prune pass. Pure data — callers (the MCP tool, the
|
|
106
|
+
* daemon reconcile loop) format/log this however they need. The MCP tool wraps it in
|
|
107
|
+
* its JSON response; the reconcile loop logs prunedCount when > 0.
|
|
108
|
+
*/
|
|
109
|
+
export interface StaleDirectPruneResult {
|
|
110
|
+
mode: 'execute' | 'dry_run';
|
|
111
|
+
includeTerminal: boolean;
|
|
112
|
+
/** Total staleDirect (+terminal when included) candidates surfaced this pass. */
|
|
113
|
+
candidateCount: number;
|
|
114
|
+
/** Records classified prunable AND (when minAgeMs > 0) old enough to auto-prune. */
|
|
115
|
+
prunable: MeshActiveWorkRecord[];
|
|
116
|
+
prunedCount: number;
|
|
117
|
+
/** Prunable by classification but younger than the age gate — only populated when minAgeMs > 0. */
|
|
118
|
+
skippedTooYoung: MeshActiveWorkRecord[];
|
|
119
|
+
preservedUnacknowledged: MeshActiveWorkRecord[];
|
|
120
|
+
/** Prunable orphans/terminals with no store-backed row to delete (ledger-only audit). */
|
|
121
|
+
preservedLedgerOnly: MeshActiveWorkRecord[];
|
|
122
|
+
preservedNotOrphan: MeshActiveWorkRecord[];
|
|
123
|
+
}
|
|
124
|
+
export interface PruneStaleDirectDispatchesOptions {
|
|
125
|
+
meshId: string;
|
|
126
|
+
/** Active direct dispatches from MeshRuntimeStore (getActiveDirectDispatches). */
|
|
127
|
+
directDispatches: DirectDispatchRecord[];
|
|
128
|
+
/** Ledger tail used to attribute remote/terminal dispatches (readLedgerEntries). */
|
|
129
|
+
ledgerEntries?: MeshLedgerEntry[];
|
|
130
|
+
queue?: MeshWorkQueueEntry[];
|
|
131
|
+
/** Live mesh nodes (decorated with live session details) — drives orphan detection. */
|
|
132
|
+
nodes?: any[];
|
|
133
|
+
/** When true, actually delete + append the audit ledger entry. Default false (dry run). */
|
|
134
|
+
execute?: boolean;
|
|
135
|
+
/** Include terminal (idle/failed) direct rows as prune candidates. Default false. */
|
|
136
|
+
includeTerminal?: boolean;
|
|
137
|
+
/**
|
|
138
|
+
* Minimum age (ms, measured from createdAt/dispatchedAt) before a prunable orphan is
|
|
139
|
+
* eligible. 0 (default) prunes immediately regardless of age — the manual prune behavior.
|
|
140
|
+
* The daemon auto-prune passes a conservative threshold so a node/session that is only
|
|
141
|
+
* transiently invisible is never pruned on the spot.
|
|
142
|
+
*/
|
|
143
|
+
minAgeMs?: number;
|
|
144
|
+
/** Audit source string written into the direct_dispatch_pruned ledger payload. */
|
|
145
|
+
source?: string;
|
|
146
|
+
now?: number;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Shared staleDirect prune core. Single source of truth for the prune decision + the
|
|
150
|
+
* mutation (store-row delete + audit-ledger append) used by BOTH the manual MCP tool
|
|
151
|
+
* (mesh_prune_stale_direct, minAgeMs=0) and the daemon reconcile loop's auto-prune
|
|
152
|
+
* PHASE (minAgeMs > 0). Pure decision logic via buildMeshActiveWork + classifyStaleDirectForPrune;
|
|
153
|
+
* the only side effects (on execute) are deleteDirectDispatchesByTaskId and a single
|
|
154
|
+
* direct_dispatch_pruned ledger entry — never touching the append-only audit history of the
|
|
155
|
+
* pruned dispatches themselves.
|
|
156
|
+
*
|
|
157
|
+
* Safety rules (identical for manual + auto):
|
|
158
|
+
* - Only records classified as staleDirectWork against the CURRENT live mesh are eligible.
|
|
159
|
+
* - Of those, only orphans (node/session gone) — and terminals when includeTerminal — are prunable.
|
|
160
|
+
* Fresh unacknowledged dispatch failures (node/session still live) are always preserved.
|
|
161
|
+
* - Only store-backed rows (taskId present in MeshRuntimeStore) are deleted; ledger-only remote
|
|
162
|
+
* entries are preserved.
|
|
163
|
+
* - When minAgeMs > 0, a prunable orphan younger than the gate is held back (skippedTooYoung).
|
|
164
|
+
* This applies ONLY to the auto path; the manual path passes minAgeMs=0 (immediate).
|
|
165
|
+
*
|
|
166
|
+
* Idempotent: a deleted row no longer appears in getActiveDirectDispatches, so a second pass
|
|
167
|
+
* over the same orphan finds nothing to prune.
|
|
168
|
+
*/
|
|
169
|
+
export declare function pruneStaleDirectDispatches(opts: PruneStaleDirectDispatchesOptions): StaleDirectPruneResult;
|
|
104
170
|
export declare function buildCompactStaleDirectWorkSummary(staleDirectWork: MeshActiveWorkRecord[], opts?: {
|
|
105
171
|
sampleLimit?: number;
|
|
106
172
|
detailHint?: string;
|
|
@@ -84,6 +84,8 @@ export declare function buildMeshNodeCapabilityTags(node: {
|
|
|
84
84
|
isLocalWorktree?: unknown;
|
|
85
85
|
worktreeBranch?: unknown;
|
|
86
86
|
userOverrides?: unknown;
|
|
87
|
+
reportedPlatform?: unknown;
|
|
88
|
+
reportedArch?: unknown;
|
|
87
89
|
} | undefined, providerType?: string): string[];
|
|
88
90
|
export declare function nodeSatisfiesRequiredTags(requiredTags: unknown, capabilityTags: unknown): boolean;
|
|
89
91
|
/**
|
|
@@ -418,6 +418,18 @@ export interface LocalMeshNodeEntry {
|
|
|
418
418
|
/** Operator-defined capability tags used by mesh queue matching. */
|
|
419
419
|
capabilities?: string[];
|
|
420
420
|
userOverrides: Partial<RepoMeshNodeCapabilities>;
|
|
421
|
+
/**
|
|
422
|
+
* Live, self-healed platform/arch reported by the daemon that owns this
|
|
423
|
+
* node's workspace (its own process.platform/process.arch), carried on the
|
|
424
|
+
* git_status envelope and persisted by the coordinator on each direct git
|
|
425
|
+
* probe. This is auto-detected truth, kept DISTINCT from `userOverrides`
|
|
426
|
+
* (operator intent) so capability-tag derivation can prefer an explicit
|
|
427
|
+
* operator override while still self-correcting auto-detected nodes — and so
|
|
428
|
+
* a stale value is overwritten by the next report rather than sticking.
|
|
429
|
+
* Absent until the first direct probe succeeds.
|
|
430
|
+
*/
|
|
431
|
+
reportedPlatform?: string;
|
|
432
|
+
reportedArch?: string;
|
|
421
433
|
policy: RepoMeshNodePolicy;
|
|
422
434
|
/**
|
|
423
435
|
* Per-node instruction surfaced in the coordinator prompt so the LLM
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "0.9.82-rc.
|
|
3
|
+
"version": "0.9.82-rc.328",
|
|
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",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"author": "vilmire",
|
|
47
47
|
"license": "AGPL-3.0-or-later",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@adhdev/mesh-shared": "0.9.82-rc.
|
|
49
|
+
"@adhdev/mesh-shared": "0.9.82-rc.328",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
package/src/commands/router.ts
CHANGED
|
@@ -407,8 +407,10 @@ function recordInlineMeshDirectGitTruth(
|
|
|
407
407
|
node: any,
|
|
408
408
|
git: Record<string, unknown>,
|
|
409
409
|
source: 'selected_coordinator_local_git' | 'selected_coordinator_mesh_p2p_git',
|
|
410
|
-
):
|
|
411
|
-
if (!node || typeof node !== 'object' || Array.isArray(node))
|
|
410
|
+
): { reporterPlatform: string | null; reporterArch: string | null } {
|
|
411
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) {
|
|
412
|
+
return { reporterPlatform: null, reporterArch: null };
|
|
413
|
+
}
|
|
412
414
|
const checkedAt = readNumberValue(git.lastCheckedAt) ?? Date.now();
|
|
413
415
|
const updatedAt = new Date(checkedAt).toISOString();
|
|
414
416
|
const nextGit: Record<string, unknown> = {
|
|
@@ -439,6 +441,13 @@ function recordInlineMeshDirectGitTruth(
|
|
|
439
441
|
const reporterPlatform = readStringValue(git.reporterPlatform) ?? (isLocalSource ? process.platform : null);
|
|
440
442
|
const reporterArch = readStringValue(git.reporterArch) ?? (isLocalSource ? process.arch : null);
|
|
441
443
|
stampNodeReporterPlatform(node, reporterPlatform, reporterArch);
|
|
444
|
+
// Mirror onto the in-memory node's dedicated reporter fields too (distinct
|
|
445
|
+
// from userOverrides). For a local_config mesh the caller also persists these
|
|
446
|
+
// to meshes.json via updateNode so the value survives a coordinator restart;
|
|
447
|
+
// for an inline/cache mesh this keeps the runtime object self-consistent.
|
|
448
|
+
if (reporterPlatform) node.reportedPlatform = reporterPlatform;
|
|
449
|
+
if (reporterArch) node.reportedArch = reporterArch;
|
|
450
|
+
return { reporterPlatform, reporterArch };
|
|
442
451
|
}
|
|
443
452
|
|
|
444
453
|
/**
|
|
@@ -460,6 +469,34 @@ function stampNodeReporterPlatform(node: any, platform: string | null, arch: str
|
|
|
460
469
|
if (changed) node.userOverrides = overrides;
|
|
461
470
|
}
|
|
462
471
|
|
|
472
|
+
/**
|
|
473
|
+
* Persist the live self-reported platform/arch onto the local meshes.json node
|
|
474
|
+
* record so capability-tag os=/arch= self-heals across coordinator restarts.
|
|
475
|
+
*
|
|
476
|
+
* The in-memory stamp done by recordInlineMeshDirectGitTruth lives on the
|
|
477
|
+
* mesh_status assembly object and is discarded after the response; only a
|
|
478
|
+
* `local_config` mesh has a backing meshes.json node to write through to. Inline
|
|
479
|
+
* cache/bootstrap meshes have no local node to update, so we no-op for them.
|
|
480
|
+
* Fire-and-forget (same pattern as the worktreeBootstrap writer) — a persistence
|
|
481
|
+
* failure must never block the status response.
|
|
482
|
+
*/
|
|
483
|
+
function persistNodeReporterPlatform(
|
|
484
|
+
meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config',
|
|
485
|
+
mesh: any,
|
|
486
|
+
nodeId: string | undefined,
|
|
487
|
+
reporter: { reporterPlatform: string | null; reporterArch: string | null },
|
|
488
|
+
): void {
|
|
489
|
+
if (meshSource !== 'local_config') return;
|
|
490
|
+
const meshId = readStringValue(mesh?.id);
|
|
491
|
+
if (!meshId || !nodeId) return;
|
|
492
|
+
const reportedPlatform = reporter.reporterPlatform ?? undefined;
|
|
493
|
+
const reportedArch = reporter.reporterArch ?? undefined;
|
|
494
|
+
if (!reportedPlatform && !reportedArch) return;
|
|
495
|
+
void import('../config/mesh-config.js')
|
|
496
|
+
.then(({ updateNode }) => updateNode(meshId, nodeId, { reportedPlatform, reportedArch }))
|
|
497
|
+
.catch(() => { /* best-effort self-heal; never block status assembly */ });
|
|
498
|
+
}
|
|
499
|
+
|
|
463
500
|
function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | undefined {
|
|
464
501
|
const liveGit = buildInlineMeshTransitGitStatus(node);
|
|
465
502
|
if (liveGit) return liveGit;
|
|
@@ -1323,7 +1360,8 @@ async function hydrateInlineMeshDirectTruth(args: {
|
|
|
1323
1360
|
try {
|
|
1324
1361
|
const localGit = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
1325
1362
|
if (localGit?.isGitRepo) {
|
|
1326
|
-
recordInlineMeshDirectGitTruth(node, localGit as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
1363
|
+
const reporter = recordInlineMeshDirectGitTruth(node, localGit as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
1364
|
+
persistNodeReporterPlatform(args.meshSource, args.mesh, nodeId, reporter);
|
|
1327
1365
|
localConfirmedCount += 1;
|
|
1328
1366
|
continue;
|
|
1329
1367
|
}
|
|
@@ -1375,7 +1413,8 @@ async function hydrateInlineMeshDirectTruth(args: {
|
|
|
1375
1413
|
? await args.probeCache.probe(daemonId, workspace, runProbe)
|
|
1376
1414
|
: await runProbe();
|
|
1377
1415
|
if (remoteGit) {
|
|
1378
|
-
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
1416
|
+
const reporter = recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
1417
|
+
persistNodeReporterPlatform(args.meshSource, args.mesh, nodeId, reporter);
|
|
1379
1418
|
peerConfirmedCount += 1;
|
|
1380
1419
|
continue;
|
|
1381
1420
|
}
|
|
@@ -9299,7 +9338,8 @@ export class DaemonCommandRouter {
|
|
|
9299
9338
|
if (!connectionReported || connectionState === 'unknown') {
|
|
9300
9339
|
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
9301
9340
|
}
|
|
9302
|
-
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
9341
|
+
const reporter = recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
9342
|
+
persistNodeReporterPlatform(meshRecord.source, mesh, nodeId, reporter);
|
|
9303
9343
|
remoteProbeApplied = true;
|
|
9304
9344
|
}
|
|
9305
9345
|
}
|
|
@@ -9340,7 +9380,8 @@ export class DaemonCommandRouter {
|
|
|
9340
9380
|
try {
|
|
9341
9381
|
const gitStatus = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
9342
9382
|
status.git = gitStatus;
|
|
9343
|
-
recordInlineMeshDirectGitTruth(node, gitStatus as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
9383
|
+
const reporter = recordInlineMeshDirectGitTruth(node, gitStatus as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
9384
|
+
persistNodeReporterPlatform(meshRecord.source, mesh, nodeId, reporter);
|
|
9344
9385
|
if (gitStatus.isGitRepo) {
|
|
9345
9386
|
status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
|
|
9346
9387
|
} else {
|
|
@@ -604,6 +604,11 @@ export function updateNode(
|
|
|
604
604
|
/** Per-node instruction surfaced in the coordinator prompt. Pass an
|
|
605
605
|
* empty string or undefined to clear it. */
|
|
606
606
|
systemPrompt?: string;
|
|
607
|
+
/** Live self-reported platform/arch from the owning daemon's git_status
|
|
608
|
+
* envelope. Persisted distinctly from userOverrides (auto-detected, not
|
|
609
|
+
* operator intent) so capability-tag os=/arch= self-heals across loads. */
|
|
610
|
+
reportedPlatform?: string;
|
|
611
|
+
reportedArch?: string;
|
|
607
612
|
},
|
|
608
613
|
): LocalMeshNodeEntry | undefined {
|
|
609
614
|
const config = loadMeshConfig();
|
|
@@ -614,6 +619,8 @@ export function updateNode(
|
|
|
614
619
|
if (!node) return undefined;
|
|
615
620
|
|
|
616
621
|
if (opts.userOverrides) node.userOverrides = { ...node.userOverrides, ...opts.userOverrides };
|
|
622
|
+
if (opts.reportedPlatform && opts.reportedPlatform.trim()) node.reportedPlatform = opts.reportedPlatform.trim();
|
|
623
|
+
if (opts.reportedArch && opts.reportedArch.trim()) node.reportedArch = opts.reportedArch.trim();
|
|
617
624
|
if (opts.policy) node.policy = { ...node.policy, ...opts.policy };
|
|
618
625
|
if (opts.worktreeBootstrap) node.worktreeBootstrap = opts.worktreeBootstrap;
|
|
619
626
|
if (Object.prototype.hasOwnProperty.call(opts, 'systemPrompt')) {
|
package/src/index.ts
CHANGED
|
@@ -233,8 +233,8 @@ export type { AnyLedgerSlice, MeshLedgerReconciliationEvidence, MeshLedgerReplic
|
|
|
233
233
|
// ── Mesh Work Queue (GUPP) ──
|
|
234
234
|
export { enqueueTask, recordDirectDispatchTask, getQueue, claimNextTask, updateTaskStatus, updateSessionTaskStatus, cancelTask, requeueTask, getMeshQueueStats, getMeshQueueRevision, normalizeMeshTaskMode, validateMeshTaskModeRequest, buildMeshNodeCapabilityTags, nodeSatisfiesRequiredTags, normalizeMeshCapabilityTags, resolveConvergeRequiredTags, insertDirectDispatch, getActiveDirectDispatches, updateDirectDispatchStatus, cleanupTerminalDirectDispatches, markStaleDirectDispatches, deleteDirectDispatchesByTaskId, recordMeshToolCall, assertNoDependencyCycle, hasPendingDependents, describeTaskDependencyState } from './mesh/mesh-work-queue.js';
|
|
235
235
|
export type { MeshWorkQueueEntry, MeshTaskStatus, MeshTaskMode, MeshWorkQueueStats, MeshQueueMutationOptions, MeshTaskModeValidationResult, DirectDispatchRecord, MeshToolCallRateResult } from './mesh/mesh-work-queue.js';
|
|
236
|
-
export { buildCompactStaleDirectWorkSummary, buildMeshActiveWork, buildMeshActiveWorkSummary, classifyStaleDirectForPrune, PRUNABLE_ORPHAN_STALE_REASONS } from './mesh/mesh-active-work.js';
|
|
237
|
-
export type { StaleDirectPruneClassification } from './mesh/mesh-active-work.js';
|
|
236
|
+
export { buildCompactStaleDirectWorkSummary, buildMeshActiveWork, buildMeshActiveWorkSummary, classifyStaleDirectForPrune, pruneStaleDirectDispatches, PRUNABLE_ORPHAN_STALE_REASONS } from './mesh/mesh-active-work.js';
|
|
237
|
+
export type { StaleDirectPruneClassification, StaleDirectPruneResult, PruneStaleDirectDispatchesOptions } from './mesh/mesh-active-work.js';
|
|
238
238
|
export type { MeshActiveWorkRecord, MeshActiveWorkStatus, MeshActiveWorkSummary, MeshActiveWorkSource, MeshStaleDirectWorkSummary } from './mesh/mesh-active-work.js';
|
|
239
239
|
export { buildMeshAsyncRefineJobs, summarizeMeshAsyncRefineJobs, STALE_TERMINAL_REFINE_WINDOW_MS, RECENT_TERMINAL_REFINE_CAP } from './mesh/mesh-refine-status.js';
|
|
240
240
|
export type { MeshAsyncRefineJobStatus, MeshAsyncRefineJobSummary, MeshAsyncRefineJobsSummary } from './mesh/mesh-refine-status.js';
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { MeshLedgerEntry } from './mesh-ledger.js';
|
|
2
|
+
import { appendLedgerEntry } from './mesh-ledger.js';
|
|
2
3
|
import type { MeshWorkQueueEntry, DirectDispatchRecord } from './mesh-work-queue.js';
|
|
4
|
+
import { deleteDirectDispatchesByTaskId } from './mesh-work-queue.js';
|
|
3
5
|
import { meshNodeIdMatches } from '@adhdev/mesh-shared';
|
|
4
6
|
|
|
5
7
|
export type MeshActiveWorkSource = 'queue' | 'direct';
|
|
@@ -440,6 +442,158 @@ export function classifyStaleDirectForPrune(
|
|
|
440
442
|
return 'preserve_active';
|
|
441
443
|
}
|
|
442
444
|
|
|
445
|
+
/**
|
|
446
|
+
* Outcome of one staleDirect prune pass. Pure data — callers (the MCP tool, the
|
|
447
|
+
* daemon reconcile loop) format/log this however they need. The MCP tool wraps it in
|
|
448
|
+
* its JSON response; the reconcile loop logs prunedCount when > 0.
|
|
449
|
+
*/
|
|
450
|
+
export interface StaleDirectPruneResult {
|
|
451
|
+
mode: 'execute' | 'dry_run';
|
|
452
|
+
includeTerminal: boolean;
|
|
453
|
+
/** Total staleDirect (+terminal when included) candidates surfaced this pass. */
|
|
454
|
+
candidateCount: number;
|
|
455
|
+
/** Records classified prunable AND (when minAgeMs > 0) old enough to auto-prune. */
|
|
456
|
+
prunable: MeshActiveWorkRecord[];
|
|
457
|
+
prunedCount: number;
|
|
458
|
+
/** Prunable by classification but younger than the age gate — only populated when minAgeMs > 0. */
|
|
459
|
+
skippedTooYoung: MeshActiveWorkRecord[];
|
|
460
|
+
preservedUnacknowledged: MeshActiveWorkRecord[];
|
|
461
|
+
/** Prunable orphans/terminals with no store-backed row to delete (ledger-only audit). */
|
|
462
|
+
preservedLedgerOnly: MeshActiveWorkRecord[];
|
|
463
|
+
preservedNotOrphan: MeshActiveWorkRecord[];
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export interface PruneStaleDirectDispatchesOptions {
|
|
467
|
+
meshId: string;
|
|
468
|
+
/** Active direct dispatches from MeshRuntimeStore (getActiveDirectDispatches). */
|
|
469
|
+
directDispatches: DirectDispatchRecord[];
|
|
470
|
+
/** Ledger tail used to attribute remote/terminal dispatches (readLedgerEntries). */
|
|
471
|
+
ledgerEntries?: MeshLedgerEntry[];
|
|
472
|
+
queue?: MeshWorkQueueEntry[];
|
|
473
|
+
/** Live mesh nodes (decorated with live session details) — drives orphan detection. */
|
|
474
|
+
nodes?: any[];
|
|
475
|
+
/** When true, actually delete + append the audit ledger entry. Default false (dry run). */
|
|
476
|
+
execute?: boolean;
|
|
477
|
+
/** Include terminal (idle/failed) direct rows as prune candidates. Default false. */
|
|
478
|
+
includeTerminal?: boolean;
|
|
479
|
+
/**
|
|
480
|
+
* Minimum age (ms, measured from createdAt/dispatchedAt) before a prunable orphan is
|
|
481
|
+
* eligible. 0 (default) prunes immediately regardless of age — the manual prune behavior.
|
|
482
|
+
* The daemon auto-prune passes a conservative threshold so a node/session that is only
|
|
483
|
+
* transiently invisible is never pruned on the spot.
|
|
484
|
+
*/
|
|
485
|
+
minAgeMs?: number;
|
|
486
|
+
/** Audit source string written into the direct_dispatch_pruned ledger payload. */
|
|
487
|
+
source?: string;
|
|
488
|
+
now?: number;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Shared staleDirect prune core. Single source of truth for the prune decision + the
|
|
493
|
+
* mutation (store-row delete + audit-ledger append) used by BOTH the manual MCP tool
|
|
494
|
+
* (mesh_prune_stale_direct, minAgeMs=0) and the daemon reconcile loop's auto-prune
|
|
495
|
+
* PHASE (minAgeMs > 0). Pure decision logic via buildMeshActiveWork + classifyStaleDirectForPrune;
|
|
496
|
+
* the only side effects (on execute) are deleteDirectDispatchesByTaskId and a single
|
|
497
|
+
* direct_dispatch_pruned ledger entry — never touching the append-only audit history of the
|
|
498
|
+
* pruned dispatches themselves.
|
|
499
|
+
*
|
|
500
|
+
* Safety rules (identical for manual + auto):
|
|
501
|
+
* - Only records classified as staleDirectWork against the CURRENT live mesh are eligible.
|
|
502
|
+
* - Of those, only orphans (node/session gone) — and terminals when includeTerminal — are prunable.
|
|
503
|
+
* Fresh unacknowledged dispatch failures (node/session still live) are always preserved.
|
|
504
|
+
* - Only store-backed rows (taskId present in MeshRuntimeStore) are deleted; ledger-only remote
|
|
505
|
+
* entries are preserved.
|
|
506
|
+
* - When minAgeMs > 0, a prunable orphan younger than the gate is held back (skippedTooYoung).
|
|
507
|
+
* This applies ONLY to the auto path; the manual path passes minAgeMs=0 (immediate).
|
|
508
|
+
*
|
|
509
|
+
* Idempotent: a deleted row no longer appears in getActiveDirectDispatches, so a second pass
|
|
510
|
+
* over the same orphan finds nothing to prune.
|
|
511
|
+
*/
|
|
512
|
+
export function pruneStaleDirectDispatches(opts: PruneStaleDirectDispatchesOptions): StaleDirectPruneResult {
|
|
513
|
+
const now = opts.now ?? Date.now();
|
|
514
|
+
const includeTerminal = opts.includeTerminal === true;
|
|
515
|
+
const execute = opts.execute === true;
|
|
516
|
+
const minAgeMs = Math.max(0, opts.minAgeMs ?? 0);
|
|
517
|
+
|
|
518
|
+
const activeWorkEvidence = buildMeshActiveWork({
|
|
519
|
+
meshId: opts.meshId,
|
|
520
|
+
queue: opts.queue,
|
|
521
|
+
ledgerEntries: opts.ledgerEntries,
|
|
522
|
+
directDispatches: opts.directDispatches,
|
|
523
|
+
nodes: opts.nodes,
|
|
524
|
+
now,
|
|
525
|
+
includeTerminalDirect: includeTerminal,
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
const candidates = [
|
|
529
|
+
...activeWorkEvidence.staleDirectWork,
|
|
530
|
+
...(includeTerminal ? activeWorkEvidence.terminalDirectWork : []),
|
|
531
|
+
];
|
|
532
|
+
// Only prune store-backed dispatch rows (taskIds present in MeshRuntimeStore). Ledger-only
|
|
533
|
+
// remote entries have no store row to delete and are pure audit history — leave them alone.
|
|
534
|
+
const storeTaskIds = new Set(opts.directDispatches.map(d => d.taskId));
|
|
535
|
+
|
|
536
|
+
const prunable: MeshActiveWorkRecord[] = [];
|
|
537
|
+
const skippedTooYoung: MeshActiveWorkRecord[] = [];
|
|
538
|
+
const preservedUnacknowledged: MeshActiveWorkRecord[] = [];
|
|
539
|
+
const preservedLedgerOnly: MeshActiveWorkRecord[] = [];
|
|
540
|
+
const preservedNotOrphan: MeshActiveWorkRecord[] = [];
|
|
541
|
+
for (const record of candidates) {
|
|
542
|
+
const classification = classifyStaleDirectForPrune(record, { includeTerminal });
|
|
543
|
+
if (classification === 'preserve_unacknowledged') {
|
|
544
|
+
preservedUnacknowledged.push(record);
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if (classification === 'preserve_active') {
|
|
548
|
+
preservedNotOrphan.push(record);
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
// prunable_orphan | prunable_terminal — only delete store-backed rows; ledger-only remote
|
|
552
|
+
// entries have no store row to delete and are pure audit history.
|
|
553
|
+
if (!storeTaskIds.has(record.taskId)) {
|
|
554
|
+
preservedLedgerOnly.push(record);
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
// Age gate (auto path only): hold back orphans that are too fresh — a node/session that is
|
|
558
|
+
// only transiently invisible must not be pruned the instant it disappears.
|
|
559
|
+
if (minAgeMs > 0) {
|
|
560
|
+
const ageRef = record.dispatchedAt || record.createdAt;
|
|
561
|
+
const ageMs = elapsedSince(ageRef, now);
|
|
562
|
+
if (ageMs < minAgeMs) {
|
|
563
|
+
skippedTooYoung.push(record);
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
prunable.push(record);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
let prunedCount = 0;
|
|
571
|
+
if (execute && prunable.length) {
|
|
572
|
+
prunedCount = deleteDirectDispatchesByTaskId(opts.meshId, prunable.map(r => r.taskId));
|
|
573
|
+
appendLedgerEntry(opts.meshId, {
|
|
574
|
+
kind: 'direct_dispatch_pruned',
|
|
575
|
+
payload: {
|
|
576
|
+
source: opts.source || 'prune_stale_direct',
|
|
577
|
+
prunedCount,
|
|
578
|
+
taskIds: prunable.map(r => r.taskId),
|
|
579
|
+
reasons: Array.from(new Set(prunable.map(r => r.staleReason || (r.terminal ? 'terminal' : 'unknown')))),
|
|
580
|
+
},
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return {
|
|
585
|
+
mode: execute ? 'execute' : 'dry_run',
|
|
586
|
+
includeTerminal,
|
|
587
|
+
candidateCount: candidates.length,
|
|
588
|
+
prunable,
|
|
589
|
+
prunedCount,
|
|
590
|
+
skippedTooYoung,
|
|
591
|
+
preservedUnacknowledged,
|
|
592
|
+
preservedLedgerOnly,
|
|
593
|
+
preservedNotOrphan,
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
443
597
|
export function buildCompactStaleDirectWorkSummary(
|
|
444
598
|
staleDirectWork: MeshActiveWorkRecord[],
|
|
445
599
|
opts: { sampleLimit?: number; detailHint?: string; note?: string } = {},
|
|
@@ -55,7 +55,9 @@ import {
|
|
|
55
55
|
expireStaleUnresolvedDelegateForwards,
|
|
56
56
|
} from './mesh-unresolved-forward-outbox.js';
|
|
57
57
|
import { readNonEmptyString } from './mesh-events-utils.js';
|
|
58
|
-
import { getActiveDirectDispatches } from './mesh-work-queue.js';
|
|
58
|
+
import { getActiveDirectDispatches, getQueue } from './mesh-work-queue.js';
|
|
59
|
+
import { readLedgerEntries } from './mesh-ledger.js';
|
|
60
|
+
import { pruneStaleDirectDispatches } from './mesh-active-work.js';
|
|
59
61
|
import { reconcileDirectDispatchCompletionFromTranscript } from './mesh-events-stale.js';
|
|
60
62
|
import { extractFinalAssistantSummaryEvidence } from '../providers/chat-message-normalization.js';
|
|
61
63
|
import type { ChatMessage } from '../types.js';
|
|
@@ -64,6 +66,25 @@ import type { ChatMessage } from '../types.js';
|
|
|
64
66
|
// coordinator land within at most one interval. Overridable via env for tuning.
|
|
65
67
|
const DEFAULT_RECONCILE_INTERVAL_MS = 4_000;
|
|
66
68
|
|
|
69
|
+
// PHASE 5 (auto-prune) conservative age gate. A direct dispatch whose node/session is
|
|
70
|
+
// orphaned (no longer in the live mesh) is only auto-pruned once it is at least this old,
|
|
71
|
+
// measured from its dispatch time. This protects against a node/session that is only
|
|
72
|
+
// *transiently* invisible (a momentary probe failure, a daemon restart) being pruned the
|
|
73
|
+
// instant it disappears. The MANUAL prune (mesh_prune_stale_direct) has no age gate — an
|
|
74
|
+
// operator pruning explicitly wants the orphan gone now. Overridable via env for tuning.
|
|
75
|
+
const DEFAULT_AUTO_PRUNE_MIN_AGE_MS = 24 * 60 * 60_000; // 24h
|
|
76
|
+
|
|
77
|
+
function resolveAutoPruneMinAgeMs(): number {
|
|
78
|
+
const raw = readNonEmptyString(process.env.MESH_AUTO_PRUNE_MIN_AGE_MS);
|
|
79
|
+
if (raw) {
|
|
80
|
+
const parsed = Number.parseInt(raw, 10);
|
|
81
|
+
// Clamp to [1h, 30d] so a mis-set env can't make the gate pathologically aggressive
|
|
82
|
+
// (prune the moment something blinks) or effectively disable it forever.
|
|
83
|
+
if (Number.isFinite(parsed) && parsed >= 60 * 60_000 && parsed <= 30 * 24 * 60 * 60_000) return parsed;
|
|
84
|
+
}
|
|
85
|
+
return DEFAULT_AUTO_PRUNE_MIN_AGE_MS;
|
|
86
|
+
}
|
|
87
|
+
|
|
67
88
|
function resolveReconcileIntervalMs(): number {
|
|
68
89
|
const raw = readNonEmptyString(process.env.MESH_RECONCILE_INTERVAL_MS);
|
|
69
90
|
if (raw) {
|
|
@@ -310,6 +331,36 @@ export async function runMeshReconcileTick(components: DaemonComponents): Promis
|
|
|
310
331
|
}
|
|
311
332
|
}
|
|
312
333
|
|
|
334
|
+
// ── PHASE 5: auto-prune orphaned direct dispatch records ───────────────────
|
|
335
|
+
// staleDirectWork (orphaned direct-dispatch rows whose node/session is no longer in the
|
|
336
|
+
// live mesh) otherwise accumulates indefinitely: a removed worktree node or a cleanly
|
|
337
|
+
// terminated session leaves its direct-dispatch row behind, stuck in a non-terminal status
|
|
338
|
+
// (e.g. generating) for days. This is NOT a false-idle bug — it is the separate problem of
|
|
339
|
+
// orphaned records that the only existing cleanup path (manual MCP mesh_prune_stale_direct)
|
|
340
|
+
// never reaches unless an operator runs it by hand.
|
|
341
|
+
//
|
|
342
|
+
// This phase runs the SAME prune core the manual tool calls (pruneStaleDirectDispatches),
|
|
343
|
+
// in execute mode, on the daemon timer. The only difference from the manual path is a
|
|
344
|
+
// conservative age gate (DEFAULT_AUTO_PRUNE_MIN_AGE_MS): a freshly-orphaned record is held
|
|
345
|
+
// back until it is provably stale, so a transient probe miss never auto-prunes live work.
|
|
346
|
+
// Every other safety rule is inherited unchanged from the core — active/pending/generating
|
|
347
|
+
// work and fresh unacknowledged dispatch failures are never pruned, ledger-only audit entries
|
|
348
|
+
// are preserved, and the prune itself is recorded with a direct_dispatch_pruned ledger entry.
|
|
349
|
+
// Idempotent: a pruned row is gone from getActiveDirectDispatches, so the next tick finds
|
|
350
|
+
// nothing to re-prune. Isolated in its own try/catch per mesh so it can never kill the tick.
|
|
351
|
+
{
|
|
352
|
+
const minAgeMs = resolveAutoPruneMinAgeMs();
|
|
353
|
+
for (const mesh of listMeshes()) {
|
|
354
|
+
const selfIds = resolveCoordinatorSelfIds(mesh, drainDaemonIds);
|
|
355
|
+
if (!daemonHostsMesh(mesh, selfIds)) continue;
|
|
356
|
+
try {
|
|
357
|
+
await autoPruneStaleDirectDispatches(components, mesh, selfIds, localDaemonId, minAgeMs);
|
|
358
|
+
} catch (e: any) {
|
|
359
|
+
LOG.warn('MeshReconcile', `Auto-prune stale direct failed for mesh ${mesh.id}: ${e?.message || e}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
313
364
|
// ── PHASE 2: inject into live CLI coordinators on this daemon ──────────────
|
|
314
365
|
const coordinators = findLiveCoordinators(components);
|
|
315
366
|
if (coordinators.length === 0) {
|
|
@@ -592,6 +643,98 @@ async function reconcileUnterminatedDirectDispatches(
|
|
|
592
643
|
}
|
|
593
644
|
}
|
|
594
645
|
|
|
646
|
+
// PHASE 5 helper. Build the live-node view (mesh.nodes decorated with each node's live
|
|
647
|
+
// session list) and run the shared prune core in execute mode with the conservative age gate.
|
|
648
|
+
//
|
|
649
|
+
// Orphan detection needs the SAME live-session evidence the manual MCP prune uses: a node still
|
|
650
|
+
// in mesh.nodes whose session list no longer contains the dispatched sessionId is "session not
|
|
651
|
+
// present" (prunable); a node missing from mesh.nodes entirely is "node no longer in live mesh"
|
|
652
|
+
// (prunable). We obtain live sessions per node via get_status_metadata — local nodes through the
|
|
653
|
+
// local commandHandler, remote nodes over P2P (dispatchMeshCommand) — exactly the transports
|
|
654
|
+
// PHASE 4 already uses. A node we cannot probe (offline) keeps an empty session list; combined
|
|
655
|
+
// with the age gate that only matters once the orphan is genuinely old.
|
|
656
|
+
//
|
|
657
|
+
// O(1) fast exit: when there are no active direct dispatches at all there is nothing to prune,
|
|
658
|
+
// so we skip the (per-node) status probes entirely — an idle mesh costs one indexed query.
|
|
659
|
+
async function autoPruneStaleDirectDispatches(
|
|
660
|
+
components: DaemonComponents,
|
|
661
|
+
mesh: LocalMeshEntry,
|
|
662
|
+
selfIds: string[],
|
|
663
|
+
localDaemonId: string | undefined,
|
|
664
|
+
minAgeMs: number,
|
|
665
|
+
): Promise<void> {
|
|
666
|
+
const directDispatches = getActiveDirectDispatches(mesh.id);
|
|
667
|
+
if (directDispatches.length === 0) return; // nothing dispatched → nothing to prune
|
|
668
|
+
|
|
669
|
+
const liveNodes = await collectLiveNodesWithSessions(components, mesh, selfIds, localDaemonId);
|
|
670
|
+
|
|
671
|
+
const result = pruneStaleDirectDispatches({
|
|
672
|
+
meshId: mesh.id,
|
|
673
|
+
queue: getQueue(mesh.id),
|
|
674
|
+
ledgerEntries: readLedgerEntries(mesh.id, { tail: 500 }),
|
|
675
|
+
directDispatches,
|
|
676
|
+
nodes: liveNodes,
|
|
677
|
+
execute: true,
|
|
678
|
+
minAgeMs,
|
|
679
|
+
source: 'daemon_reconcile_auto_prune',
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
// Log only when something was actually pruned — silence on the common no-op tick.
|
|
683
|
+
if (result.prunedCount > 0) {
|
|
684
|
+
LOG.info('MeshReconcile', `Auto-pruned ${result.prunedCount} orphaned direct dispatch record(s) for mesh ${mesh.id}`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Probe each node for its live session list (get_status_metadata) and return mesh.nodes
|
|
689
|
+
// decorated with a `sessions` array — the shape buildMeshActiveWork / sessionStatusFromNodes
|
|
690
|
+
// consume to decide whether a dispatched session is still present. Best-effort: an unreachable
|
|
691
|
+
// node yields an empty session list rather than throwing.
|
|
692
|
+
async function collectLiveNodesWithSessions(
|
|
693
|
+
components: DaemonComponents,
|
|
694
|
+
mesh: LocalMeshEntry,
|
|
695
|
+
selfIds: string[],
|
|
696
|
+
localDaemonId: string | undefined,
|
|
697
|
+
): Promise<any[]> {
|
|
698
|
+
const dispatchMeshCommand = components.dispatchMeshCommand;
|
|
699
|
+
return Promise.all(mesh.nodes.map(async (node) => {
|
|
700
|
+
const nodeDaemonId = readNonEmptyString(node.daemonId);
|
|
701
|
+
const isLocalNode = !nodeDaemonId
|
|
702
|
+
|| selfIds.includes(nodeDaemonId)
|
|
703
|
+
|| (localDaemonId !== undefined && nodeDaemonId === localDaemonId);
|
|
704
|
+
let statusResult: unknown;
|
|
705
|
+
try {
|
|
706
|
+
if (isLocalNode) {
|
|
707
|
+
statusResult = await components.commandHandler.handle('get_status_metadata', {});
|
|
708
|
+
} else if (dispatchMeshCommand) {
|
|
709
|
+
statusResult = await dispatchMeshCommand(nodeDaemonId, 'get_status_metadata', {});
|
|
710
|
+
} else {
|
|
711
|
+
return node; // remote node, no P2P transport — leave undecorated
|
|
712
|
+
}
|
|
713
|
+
} catch {
|
|
714
|
+
return node; // unreachable — leave undecorated (empty session list)
|
|
715
|
+
}
|
|
716
|
+
const sessions = extractStatusMetadataSessions(statusResult);
|
|
717
|
+
return sessions.length > 0 ? { ...node, sessions } : node;
|
|
718
|
+
}));
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// Pull the live session list out of a get_status_metadata result, tolerating the same
|
|
722
|
+
// envelope shapes unwrapReadChatPayload handles (direct CommandResult or { payload }/{ result }).
|
|
723
|
+
function extractStatusMetadataSessions(raw: unknown): any[] {
|
|
724
|
+
let cursor: unknown = raw;
|
|
725
|
+
for (let depth = 0; depth < 4 && cursor && typeof cursor === 'object'; depth++) {
|
|
726
|
+
const record = cursor as Record<string, unknown>;
|
|
727
|
+
const status = record.status && typeof record.status === 'object' ? record.status as Record<string, unknown> : undefined;
|
|
728
|
+
if (status && Array.isArray(status.sessions)) return status.sessions;
|
|
729
|
+
if (Array.isArray(record.sessions)) return record.sessions;
|
|
730
|
+
if (record.payload && typeof record.payload === 'object') { cursor = record.payload; continue; }
|
|
731
|
+
if (record.result && typeof record.result === 'object') { cursor = record.result; continue; }
|
|
732
|
+
if (record.data && typeof record.data === 'object') { cursor = record.data; continue; }
|
|
733
|
+
break;
|
|
734
|
+
}
|
|
735
|
+
return [];
|
|
736
|
+
}
|
|
737
|
+
|
|
595
738
|
function extractPendingEvents(raw: unknown): any[] {
|
|
596
739
|
if (Array.isArray(raw)) return raw;
|
|
597
740
|
if (raw && typeof raw === 'object') {
|
|
@@ -385,8 +385,20 @@ function readNodeOverride(node: { userOverrides?: unknown } | undefined, key: 'p
|
|
|
385
385
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
+
/**
|
|
389
|
+
* Live, self-reported platform/arch the owning daemon stamped onto the node from
|
|
390
|
+
* its own process.platform/process.arch via the git_status envelope. Kept on a
|
|
391
|
+
* field DISTINCT from userOverrides so capability-tag derivation can prefer an
|
|
392
|
+
* explicit operator override while still self-healing auto-detected nodes — and
|
|
393
|
+
* so the value reflects the node's real OS rather than the coordinator's.
|
|
394
|
+
*/
|
|
395
|
+
function readNodeReporter(node: { reportedPlatform?: unknown; reportedArch?: unknown } | undefined, key: 'platform' | 'arch'): string | null {
|
|
396
|
+
const value = key === 'platform' ? node?.reportedPlatform : node?.reportedArch;
|
|
397
|
+
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
398
|
+
}
|
|
399
|
+
|
|
388
400
|
export function buildMeshNodeCapabilityTags(
|
|
389
|
-
node: { capabilities?: unknown; policy?: unknown; isLocalWorktree?: unknown; worktreeBranch?: unknown; userOverrides?: unknown } | undefined,
|
|
401
|
+
node: { capabilities?: unknown; policy?: unknown; isLocalWorktree?: unknown; worktreeBranch?: unknown; userOverrides?: unknown; reportedPlatform?: unknown; reportedArch?: unknown } | undefined,
|
|
390
402
|
providerType?: string,
|
|
391
403
|
): string[] {
|
|
392
404
|
const provider = typeof providerType === 'string' && providerType.trim()
|
|
@@ -395,17 +407,25 @@ export function buildMeshNodeCapabilityTags(
|
|
|
395
407
|
const worktreeBranch = typeof node?.worktreeBranch === 'string' && node.worktreeBranch.trim()
|
|
396
408
|
? node.worktreeBranch.trim()
|
|
397
409
|
: null;
|
|
398
|
-
// Per-node platform/arch
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
410
|
+
// Per-node platform/arch precedence (highest → lowest):
|
|
411
|
+
// 1. userOverrides.platform/arch — an EXPLICIT operator override always wins.
|
|
412
|
+
// 2. reportedPlatform/reportedArch — the live OS the owning daemon
|
|
413
|
+
// self-reported (its own process.platform/process.arch via the git_status
|
|
414
|
+
// envelope), persisted to the node record on each direct probe. This is
|
|
415
|
+
// why a Windows member advertises os=win32 even though the COORDINATOR
|
|
416
|
+
// computing these tags runs on darwin — without it the consumer reads the
|
|
417
|
+
// persistent node (operator userOverrides empty) and would fall straight
|
|
418
|
+
// through to the coordinator's own process.platform, mislabeling every
|
|
419
|
+
// node os=darwin. We prefer this LIVE value over any stale auto-stamp.
|
|
420
|
+
// 3. process.platform/process.arch — last-resort fallback, correct only for
|
|
421
|
+
// the local coordinator node / local worktree nodes that have not yet
|
|
422
|
+
// been probed (their workspace lives on THIS machine anyway).
|
|
403
423
|
// Vocabulary is raw process.platform/process.arch ("darwin"/"win32"/"linux",
|
|
404
424
|
// "arm64"/"x64") on both the advertiser and the required_tags matcher, which
|
|
405
425
|
// compares with plain string equality (nodeSatisfiesRequiredTags) — so this
|
|
406
426
|
// keeps the win32/darwin/linux vocabulary the matcher already expects.
|
|
407
|
-
const os = readNodeOverride(node, 'platform') ?? process.platform;
|
|
408
|
-
const arch = readNodeOverride(node, 'arch') ?? process.arch;
|
|
427
|
+
const os = readNodeOverride(node, 'platform') ?? readNodeReporter(node, 'platform') ?? process.platform;
|
|
428
|
+
const arch = readNodeOverride(node, 'arch') ?? readNodeReporter(node, 'arch') ?? process.arch;
|
|
409
429
|
return normalizeMeshCapabilityTags([
|
|
410
430
|
...(Array.isArray(node?.capabilities) ? node.capabilities : []),
|
|
411
431
|
`os=${os}`,
|