@adhdev/daemon-core 0.9.82-rc.291 → 0.9.82-rc.293
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/commands/router.d.ts +50 -0
- package/dist/index.js +637 -327
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +639 -329
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-missions.d.ts +25 -1
- package/dist/mesh/mesh-runtime-store.d.ts +15 -0
- package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +30 -0
- package/package.json +2 -2
- package/src/commands/router.ts +274 -58
- package/src/git/git-status.ts +46 -11
- package/src/mesh/coordinator-prompt.ts +2 -2
- package/src/mesh/mesh-active-work.ts +2 -1
- package/src/mesh/mesh-events-coordinator.ts +58 -10
- package/src/mesh/mesh-missions.ts +41 -3
- package/src/mesh/mesh-reconcile-loop.ts +55 -0
- package/src/mesh/mesh-runtime-store.ts +30 -0
- package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
- package/src/providers/cli-provider-instance.ts +21 -16
- package/src/providers/extension-provider-instance.ts +5 -1
- package/src/providers/ide-provider-instance.ts +6 -1
package/src/commands/router.ts
CHANGED
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
normalizeGitStatus as sharedNormalizeGitStatus,
|
|
32
32
|
pickBestTransitGitStatus as sharedPickBestTransitGitStatus,
|
|
33
33
|
summarizeGitShape as sharedSummarizeGitShape,
|
|
34
|
+
normalizeMeshNodeId,
|
|
35
|
+
meshNodeIdMatches,
|
|
34
36
|
} from '@adhdev/mesh-shared';
|
|
35
37
|
import { SessionRegistry } from '../sessions/registry.js';
|
|
36
38
|
import { LOG } from '../logging/logger.js';
|
|
@@ -161,7 +163,10 @@ function summarizeRepoMeshStatusDebug(status: any): Record<string, unknown> {
|
|
|
161
163
|
branchConvergenceSummary: status?.branchConvergenceSummary ?? status?.branch_convergence_summary ?? null,
|
|
162
164
|
nodeCount: nodes.length,
|
|
163
165
|
nodes: nodes.map((node: any) => ({
|
|
164
|
-
nodeId
|
|
166
|
+
// Status emits the id under `nodeId` (3-way input absorbed). The
|
|
167
|
+
// inline cache keeps `id` and `nodeId` equal, so this serialized form
|
|
168
|
+
// round-trips back through the cache without flipping shape.
|
|
169
|
+
nodeId: normalizeMeshNodeId(node) ?? null,
|
|
165
170
|
daemonId: readStringValue(node?.daemonId, node?.daemon_id) ?? null,
|
|
166
171
|
workspace: readStringValue(node?.workspace, node?.git?.workspace) ?? null,
|
|
167
172
|
health: readStringValue(node?.health) ?? null,
|
|
@@ -476,7 +481,49 @@ function inlineMeshCarriesTransientNodeTruth(inlineMesh: any): boolean {
|
|
|
476
481
|
}
|
|
477
482
|
|
|
478
483
|
function readInlineMeshNodeId(node: any): string {
|
|
479
|
-
|
|
484
|
+
// 3-way (id / nodeId / node_id) via the shared normalizer. The old 2-way
|
|
485
|
+
// `id ?? nodeId` dropped the SQLite `node_id` form, so an inline-cached node
|
|
486
|
+
// that arrived in that form failed to reconcile against its cached twin.
|
|
487
|
+
return normalizeMeshNodeId(node) ?? '';
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Boundary normalization: reconcile a node's identity so `id` and `nodeId` both
|
|
491
|
+
// carry the same canonical value (any incoming form — id / nodeId / node_id — is
|
|
492
|
+
// absorbed by normalizeMeshNodeId, and the SQLite `node_id` leak is dropped).
|
|
493
|
+
// See foldMeshNodeIdentityToCanonical for why both fields are kept equal rather
|
|
494
|
+
// than collapsing to one. The rewrite is shallow (other runtime fields are
|
|
495
|
+
// preserved); records that already agree are returned unchanged so
|
|
496
|
+
// identity-equality fast paths hold.
|
|
497
|
+
function foldMeshNodeIdentityToCanonical(node: any): any {
|
|
498
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return node;
|
|
499
|
+
const canonical = normalizeMeshNodeId(node);
|
|
500
|
+
if (canonical === undefined) return node;
|
|
501
|
+
// Save-boundary identity folding, applied IN PLACE. We DUAL-WRITE both `id`
|
|
502
|
+
// and `nodeId` to the single canonical value (and drop the SQLite `node_id`
|
|
503
|
+
// leak), rather than collapsing to one field. Two halves of the system read
|
|
504
|
+
// different field names: the mesh_status serializer emits `nodeId`, while the
|
|
505
|
+
// worktree clone path and get_mesh membership consumers read `node.id`.
|
|
506
|
+
// Folding to ONE form would break whichever side reads the other. Keeping
|
|
507
|
+
// both fields equal makes every reader correct AND makes the
|
|
508
|
+
// snapshot→cache→reconcile→snapshot round-trip form-stable (no field ever
|
|
509
|
+
// flips, because both always agree). Mutating in place (not returning a new
|
|
510
|
+
// object) preserves the cached node-object identity that callers warming an
|
|
511
|
+
// inline mesh from an already-shared snapshot rely on.
|
|
512
|
+
if (node.id === canonical && node.nodeId === canonical && node.node_id === undefined) return node;
|
|
513
|
+
node.id = canonical;
|
|
514
|
+
node.nodeId = canonical;
|
|
515
|
+
if ('node_id' in node) delete node.node_id;
|
|
516
|
+
return node;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function normalizeInlineMeshNodeIdentity(inlineMesh: any): any {
|
|
520
|
+
if (!inlineMesh || typeof inlineMesh !== 'object' || Array.isArray(inlineMesh)) return inlineMesh;
|
|
521
|
+
if (!Array.isArray(inlineMesh.nodes) || inlineMesh.nodes.length === 0) return inlineMesh;
|
|
522
|
+
// Fold each node IN PLACE so the mesh object and its nodes array keep their
|
|
523
|
+
// identity — sanitizeInlineMesh and the cache-sharing callers depend on
|
|
524
|
+
// unchanged inputs returning the same reference.
|
|
525
|
+
for (const node of inlineMesh.nodes) foldMeshNodeIdentityToCanonical(node);
|
|
526
|
+
return inlineMesh;
|
|
480
527
|
}
|
|
481
528
|
|
|
482
529
|
function sanitizeInlineMesh(inlineMesh: any): any {
|
|
@@ -603,7 +650,7 @@ function deriveMeshNodeHealthFromGit(git: Record<string, unknown> | null | undef
|
|
|
603
650
|
}
|
|
604
651
|
|
|
605
652
|
function readMeshNodeLabel(status: Record<string, unknown>, node: any): string {
|
|
606
|
-
return readStringValue(status.nodeId, node
|
|
653
|
+
return readStringValue(status.nodeId, normalizeMeshNodeId(node)) ?? 'unknown';
|
|
607
654
|
}
|
|
608
655
|
|
|
609
656
|
function buildInlineMeshBranchConvergence(args: {
|
|
@@ -946,7 +993,7 @@ async function hydrateInlineMeshDirectTruth(args: {
|
|
|
946
993
|
const unavailableNodeIds: string[] = [];
|
|
947
994
|
|
|
948
995
|
for (const [nodeIndex, node] of nodes.entries()) {
|
|
949
|
-
const nodeId =
|
|
996
|
+
const nodeId = normalizeMeshNodeId(node) || `node_${nodeIndex}`;
|
|
950
997
|
const workspace = readStringValue(node?.workspace);
|
|
951
998
|
const daemonId = readStringValue(node?.daemonId);
|
|
952
999
|
const isSelfNode = Boolean(
|
|
@@ -1122,7 +1169,7 @@ function buildHistoricalMeshSessions(args: {
|
|
|
1122
1169
|
const liveWorkspaces = new Set<string>();
|
|
1123
1170
|
const missingLocalWorktreeNodeIds = new Set<string>();
|
|
1124
1171
|
for (const node of args.nodes || []) {
|
|
1125
|
-
const nodeId =
|
|
1172
|
+
const nodeId = normalizeMeshNodeId(node);
|
|
1126
1173
|
const workspace = readStringValue(node?.workspace);
|
|
1127
1174
|
if (nodeId) liveNodeIds.add(nodeId);
|
|
1128
1175
|
if (workspace) liveWorkspaces.add(workspace);
|
|
@@ -1497,9 +1544,24 @@ function resolveRefineryAutoPublishSubmoduleMainCommits(mesh: any, workspace: st
|
|
|
1497
1544
|
return { enabled: false };
|
|
1498
1545
|
}
|
|
1499
1546
|
|
|
1500
|
-
async function computeGitPatchId(
|
|
1547
|
+
async function computeGitPatchId(
|
|
1548
|
+
cwd: string,
|
|
1549
|
+
fromRef: string,
|
|
1550
|
+
toRef: string,
|
|
1551
|
+
excludePaths: string[] = [],
|
|
1552
|
+
): Promise<string> {
|
|
1501
1553
|
const { execFileSync } = await import('node:child_process');
|
|
1502
|
-
|
|
1554
|
+
// When excludePaths is non-empty we drop those paths from the diff via
|
|
1555
|
+
// `:(exclude)` pathspecs. This is used to omit gitlink paths that have
|
|
1556
|
+
// already been proven a safe fast-forward: their patch hunks legitimately
|
|
1557
|
+
// differ between the expected (mergeBase→branch) and actual (base→merged)
|
|
1558
|
+
// diffs because base may have advanced the same gitlink, so comparing them
|
|
1559
|
+
// would spuriously fail patch-equivalence even though the merge is sound.
|
|
1560
|
+
const diffArgs = ['diff', '--patch', '--full-index', fromRef, toRef];
|
|
1561
|
+
if (excludePaths.length > 0) {
|
|
1562
|
+
diffArgs.push('--', '.', ...excludePaths.map(path => `:(exclude)${path}`));
|
|
1563
|
+
}
|
|
1564
|
+
const diff = execFileSync('git', diffArgs, {
|
|
1503
1565
|
cwd,
|
|
1504
1566
|
encoding: 'utf8',
|
|
1505
1567
|
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
@@ -1514,7 +1576,7 @@ async function computeGitPatchId(cwd: string, fromRef: string, toRef: string): P
|
|
|
1514
1576
|
return patchId.split(/\s+/)[0] || '';
|
|
1515
1577
|
}
|
|
1516
1578
|
|
|
1517
|
-
async function runMeshRefinePatchEquivalenceGate(
|
|
1579
|
+
export async function runMeshRefinePatchEquivalenceGate(
|
|
1518
1580
|
repoRoot: string,
|
|
1519
1581
|
baseHead: string,
|
|
1520
1582
|
branchHead: string,
|
|
@@ -1582,8 +1644,25 @@ async function runMeshRefinePatchEquivalenceGate(
|
|
|
1582
1644
|
gitlinkTrivialFastForward,
|
|
1583
1645
|
};
|
|
1584
1646
|
}
|
|
1585
|
-
|
|
1586
|
-
|
|
1647
|
+
// Exclude *proven fast-forward* gitlink paths from BOTH patch-ids. When
|
|
1648
|
+
// base has advanced a submodule pointer (a sibling merged into main ahead
|
|
1649
|
+
// of us) the gitlink hunk's old-value differs between the expected diff
|
|
1650
|
+
// (mergeBase→branch, showing the full base→branch advance) and the actual
|
|
1651
|
+
// diff (base→merged, showing only the shorter advanced-base→branch
|
|
1652
|
+
// advance). That mismatch would spuriously fail equivalence even though
|
|
1653
|
+
// advancing the pointer to the branch side is a provably safe
|
|
1654
|
+
// fast-forward — this is the root cause of the diverged-base
|
|
1655
|
+
// patch_equivalence_failed misjudgment.
|
|
1656
|
+
//
|
|
1657
|
+
// We exclude ONLY gitlinks whose base-side commit is an ancestor of the
|
|
1658
|
+
// branch-side commit (a strict ff, both objects available locally). A
|
|
1659
|
+
// non-ff or ambiguous gitlink (a genuine submodule divergence, or objects
|
|
1660
|
+
// not fetched locally) is deliberately left in the diff so its differing
|
|
1661
|
+
// hunk still drives the comparison — this preserves the original behavior
|
|
1662
|
+
// and prevents a false pass on a real divergence.
|
|
1663
|
+
const ffGitlinkExcludePaths = collectFastForwardGitlinkPaths(repoRoot, baseHead, branchHead);
|
|
1664
|
+
const expectedPatchId = await computeGitPatchId(repoRoot, mergeBase, branchHead, ffGitlinkExcludePaths);
|
|
1665
|
+
const actualPatchId = await computeGitPatchId(repoRoot, baseHead, mergedTree, ffGitlinkExcludePaths);
|
|
1587
1666
|
const equivalent = expectedPatchId === actualPatchId;
|
|
1588
1667
|
return {
|
|
1589
1668
|
status: equivalent ? 'passed' : 'failed',
|
|
@@ -1871,6 +1950,24 @@ function readChangedPathKinds(repoRoot: string, fromRef: string, toRef: string):
|
|
|
1871
1950
|
}
|
|
1872
1951
|
}
|
|
1873
1952
|
|
|
1953
|
+
/**
|
|
1954
|
+
* Return the changed gitlink paths between base and branch whose advance is a
|
|
1955
|
+
* strict fast-forward (the base-side commit is an ancestor of the branch-side
|
|
1956
|
+
* commit inside that submodule's repo). These are the paths whose patch-id hunk
|
|
1957
|
+
* may legitimately differ when base has advanced the same submodule, so they
|
|
1958
|
+
* are safe to exclude from the patch-equivalence comparison. A non-ff (genuinely
|
|
1959
|
+
* diverged) gitlink is deliberately excluded from this set so it still fails the
|
|
1960
|
+
* gate.
|
|
1961
|
+
*/
|
|
1962
|
+
export function collectFastForwardGitlinkPaths(repoRoot: string, baseHead: string, branchHead: string): string[] {
|
|
1963
|
+
return readChangedGitlinkPaths(repoRoot, baseHead, branchHead).filter(path => {
|
|
1964
|
+
const baseCommit = readTreeObject(repoRoot, baseHead, path);
|
|
1965
|
+
const branchCommit = readTreeObject(repoRoot, branchHead, path);
|
|
1966
|
+
if (!baseCommit || !branchCommit) return false;
|
|
1967
|
+
return isSubmoduleFastForward(pathResolve(repoRoot, path), baseCommit, branchCommit);
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1874
1971
|
/**
|
|
1875
1972
|
* Decide whether a merge-tree submodule conflict between base and branch is a
|
|
1876
1973
|
* trivial gitlink fast-forward (and nothing else).
|
|
@@ -1955,12 +2052,64 @@ export function evaluateGitlinkTrivialFastForward(
|
|
|
1955
2052
|
}
|
|
1956
2053
|
|
|
1957
2054
|
/**
|
|
1958
|
-
*
|
|
1959
|
-
*
|
|
1960
|
-
*
|
|
1961
|
-
*
|
|
1962
|
-
*
|
|
1963
|
-
|
|
2055
|
+
* Build a tree identical to `commitish`'s tree except every gitlink in `paths`
|
|
2056
|
+
* is rewritten to `placeholderCommit`. Used to neutralize submodule pointers so
|
|
2057
|
+
* `git merge-tree` stops bailing on the "Recursive merging with submodules"
|
|
2058
|
+
* limitation and can 3-way merge the surrounding regular-file content. Returns
|
|
2059
|
+
* the tree SHA, or undefined on failure.
|
|
2060
|
+
*/
|
|
2061
|
+
function buildTreeWithGitlinksEqualized(
|
|
2062
|
+
repoRoot: string,
|
|
2063
|
+
commitish: string,
|
|
2064
|
+
paths: string[],
|
|
2065
|
+
placeholderCommit: string,
|
|
2066
|
+
): string | undefined {
|
|
2067
|
+
try {
|
|
2068
|
+
const tree = execFileSync('git', ['rev-parse', `${commitish}^{tree}`], {
|
|
2069
|
+
cwd: repoRoot, encoding: 'utf8', maxBuffer: 1024 * 1024,
|
|
2070
|
+
}).trim();
|
|
2071
|
+
if (!tree) return undefined;
|
|
2072
|
+
const updates = paths.map(path => `160000 commit ${placeholderCommit}\t${path}`).join('\n');
|
|
2073
|
+
if (!updates) return tree;
|
|
2074
|
+
const tmpIndex = pathJoin(resolveGitDir(repoRoot), `adhdev-refine-eq-${commitish.slice(0, 12)}.index`);
|
|
2075
|
+
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
2076
|
+
try {
|
|
2077
|
+
execFileSync('git', ['read-tree', tree], { cwd: repoRoot, env, stdio: 'ignore' });
|
|
2078
|
+
execFileSync('git', ['update-index', '--index-info'], {
|
|
2079
|
+
cwd: repoRoot, env, input: `${updates}\n`, encoding: 'utf8',
|
|
2080
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
2081
|
+
});
|
|
2082
|
+
const newTree = execFileSync('git', ['write-tree'], { cwd: repoRoot, env, encoding: 'utf8' }).trim();
|
|
2083
|
+
return newTree || undefined;
|
|
2084
|
+
} finally {
|
|
2085
|
+
try { fs.rmSync(tmpIndex, { force: true }); } catch { /* ignore */ }
|
|
2086
|
+
}
|
|
2087
|
+
} catch {
|
|
2088
|
+
return undefined;
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
/**
|
|
2093
|
+
* Synthesize the merge result for a trivial gitlink fast-forward.
|
|
2094
|
+
*
|
|
2095
|
+
* `git merge-tree` bails whenever a gitlink differs across base/branch even
|
|
2096
|
+
* when the advance is a strict fast-forward, so we synthesize the result it
|
|
2097
|
+
* *would* have produced. Crucially, when the merge-base of base and branch is
|
|
2098
|
+
* NOT `baseHead` (i.e. base has diverged — a sibling was merged into main
|
|
2099
|
+
* ahead of us), `baseHead`'s tree does not contain our branch's own
|
|
2100
|
+
* non-gitlink changes. Simply overlaying gitlinks onto `baseHead`'s tree would
|
|
2101
|
+
* therefore drop those changes and break patch-equivalence.
|
|
2102
|
+
*
|
|
2103
|
+
* To handle the diverged case correctly we run a REAL 3-way merge of the
|
|
2104
|
+
* regular-file content (with the conflicting gitlinks temporarily equalized to
|
|
2105
|
+
* a common placeholder so merge-tree won't bail), then overlay each changed
|
|
2106
|
+
* gitlink's branch-side commit onto the merged result. This preserves both
|
|
2107
|
+
* sides' non-gitlink changes.
|
|
2108
|
+
*
|
|
2109
|
+
* Returns the tree SHA, or undefined on failure / genuine non-gitlink
|
|
2110
|
+
* conflict. Caller must have already proven (via
|
|
2111
|
+
* evaluateGitlinkTrivialFastForward) that every changed gitlink fast-forwards
|
|
2112
|
+
* and no other path conflicts.
|
|
1964
2113
|
*/
|
|
1965
2114
|
function synthesizeTrivialFastForwardMergeTree(
|
|
1966
2115
|
repoRoot: string,
|
|
@@ -1969,21 +2118,67 @@ function synthesizeTrivialFastForwardMergeTree(
|
|
|
1969
2118
|
gitlinks: Array<{ path: string; branchCommit?: string }>,
|
|
1970
2119
|
): string | undefined {
|
|
1971
2120
|
try {
|
|
1972
|
-
const
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
2121
|
+
const branchGitlinks = gitlinks.filter(entry => entry.branchCommit);
|
|
2122
|
+
const gitlinkPaths = branchGitlinks.map(entry => entry.path);
|
|
2123
|
+
|
|
2124
|
+
// Establish the regular-file content of the merge via a real 3-way merge
|
|
2125
|
+
// with the conflicting gitlinks neutralized. The placeholder is the
|
|
2126
|
+
// merge-base's value for a gitlink (or, failing that, any branch-side
|
|
2127
|
+
// commit) — it only needs to be identical across all three trees.
|
|
2128
|
+
const mergeBase = execFileSync('git', ['merge-base', baseHead, branchHead], {
|
|
2129
|
+
cwd: repoRoot, encoding: 'utf8', maxBuffer: 1024 * 1024,
|
|
1976
2130
|
}).trim();
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2131
|
+
|
|
2132
|
+
let mergedContentTree: string | undefined;
|
|
2133
|
+
if (mergeBase && gitlinkPaths.length > 0) {
|
|
2134
|
+
const placeholder = readTreeObject(repoRoot, mergeBase, gitlinkPaths[0])
|
|
2135
|
+
|| branchGitlinks[0].branchCommit!;
|
|
2136
|
+
const baseEqTree = buildTreeWithGitlinksEqualized(repoRoot, mergeBase, gitlinkPaths, placeholder);
|
|
2137
|
+
const oursEqTree = buildTreeWithGitlinksEqualized(repoRoot, baseHead, gitlinkPaths, placeholder);
|
|
2138
|
+
const theirsEqTree = buildTreeWithGitlinksEqualized(repoRoot, branchHead, gitlinkPaths, placeholder);
|
|
2139
|
+
if (baseEqTree && oursEqTree && theirsEqTree) {
|
|
2140
|
+
try {
|
|
2141
|
+
// merge-tree --write-tree needs commits (to derive a merge-base);
|
|
2142
|
+
// synthesize ours/theirs as children of a common base commit.
|
|
2143
|
+
const baseEqCommit = execFileSync('git', ['commit-tree', baseEqTree, '-m', 'refine-ff-base'], {
|
|
2144
|
+
cwd: repoRoot, encoding: 'utf8', maxBuffer: 1024 * 1024,
|
|
2145
|
+
}).trim();
|
|
2146
|
+
const oursEqCommit = execFileSync('git', ['commit-tree', oursEqTree, '-p', baseEqCommit, '-m', 'refine-ff-ours'], {
|
|
2147
|
+
cwd: repoRoot, encoding: 'utf8', maxBuffer: 1024 * 1024,
|
|
2148
|
+
}).trim();
|
|
2149
|
+
const theirsEqCommit = execFileSync('git', ['commit-tree', theirsEqTree, '-p', baseEqCommit, '-m', 'refine-ff-theirs'], {
|
|
2150
|
+
cwd: repoRoot, encoding: 'utf8', maxBuffer: 1024 * 1024,
|
|
2151
|
+
}).trim();
|
|
2152
|
+
const mergeOut = execFileSync('git', ['merge-tree', '--write-tree', oursEqCommit, theirsEqCommit], {
|
|
2153
|
+
cwd: repoRoot, encoding: 'utf8', maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
2154
|
+
}).trim();
|
|
2155
|
+
mergedContentTree = mergeOut.split(/\s+/)[0] || undefined;
|
|
2156
|
+
} catch {
|
|
2157
|
+
// A real conflict in the equalized merge means a genuine
|
|
2158
|
+
// non-gitlink content conflict the evaluator did not foresee
|
|
2159
|
+
// (or unavailable objects). Fall through to the simple synth.
|
|
2160
|
+
mergedContentTree = undefined;
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
// Fallback: when there is no diverged base (merge-base === baseHead) the
|
|
2166
|
+
// regular-file content of the merge is exactly baseHead's tree, so just
|
|
2167
|
+
// overlay the gitlinks. Also used when the real merge could not run.
|
|
2168
|
+
const contentTree = mergedContentTree
|
|
2169
|
+
|| execFileSync('git', ['rev-parse', `${baseHead}^{tree}`], {
|
|
2170
|
+
cwd: repoRoot, encoding: 'utf8', maxBuffer: 1024 * 1024,
|
|
2171
|
+
}).trim();
|
|
2172
|
+
if (!contentTree) return undefined;
|
|
2173
|
+
|
|
2174
|
+
const updates = branchGitlinks
|
|
1980
2175
|
.map(entry => `160000 commit ${entry.branchCommit}\t${entry.path}`)
|
|
1981
2176
|
.join('\n');
|
|
1982
|
-
if (!updates) return
|
|
2177
|
+
if (!updates) return contentTree;
|
|
1983
2178
|
const tmpIndex = pathJoin(resolveGitDir(repoRoot), `adhdev-refine-ff-${baseHead.slice(0, 12)}-${branchHead.slice(0, 12)}.index`);
|
|
1984
2179
|
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
1985
2180
|
try {
|
|
1986
|
-
execFileSync('git', ['read-tree',
|
|
2181
|
+
execFileSync('git', ['read-tree', contentTree], { cwd: repoRoot, env, stdio: 'ignore' });
|
|
1987
2182
|
execFileSync('git', ['update-index', '--index-info'], {
|
|
1988
2183
|
cwd: repoRoot,
|
|
1989
2184
|
env,
|
|
@@ -2787,7 +2982,7 @@ function buildMemberJoinNode(mesh: any, args: any, fallbackDaemonId?: string): R
|
|
|
2787
2982
|
: null;
|
|
2788
2983
|
const configured = Array.isArray(mesh?.nodes)
|
|
2789
2984
|
? (requestedNodeId
|
|
2790
|
-
? mesh.nodes.find((node: any) => node
|
|
2985
|
+
? mesh.nodes.find((node: any) => meshNodeIdMatches(node, requestedNodeId))
|
|
2791
2986
|
: mesh.nodes[0])
|
|
2792
2987
|
: null;
|
|
2793
2988
|
const source = explicit || configured;
|
|
@@ -2859,7 +3054,7 @@ export class DaemonCommandRouter {
|
|
|
2859
3054
|
}
|
|
2860
3055
|
|
|
2861
3056
|
const nodes = snapshot.nodes.map((statusNode: any) => {
|
|
2862
|
-
const nodeId =
|
|
3057
|
+
const nodeId = normalizeMeshNodeId(statusNode);
|
|
2863
3058
|
const inlineNode = nodeId ? inlineNodesById.get(nodeId) : undefined;
|
|
2864
3059
|
if (!inlineNode) return statusNode;
|
|
2865
3060
|
const liveGit = buildInlineMeshTransitGitStatus(inlineNode);
|
|
@@ -2987,7 +3182,10 @@ export class DaemonCommandRouter {
|
|
|
2987
3182
|
|
|
2988
3183
|
private warmInlineMeshCache(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
2989
3184
|
if (!inlineMesh || typeof inlineMesh !== 'object') return undefined;
|
|
2990
|
-
|
|
3185
|
+
// Save-boundary node-id normalization: reconcile each node's identity so
|
|
3186
|
+
// `id` and `nodeId` agree before it enters the cache, so reconcile keys
|
|
3187
|
+
// and the round-trip through the status serializer stay form-stable.
|
|
3188
|
+
const sanitizedInlineMesh = sanitizeInlineMesh(normalizeInlineMeshNodeIdentity(inlineMesh as any));
|
|
2991
3189
|
const cached = this.inlineMeshCache.get(meshId);
|
|
2992
3190
|
if (cached) {
|
|
2993
3191
|
const merged = reconcileInlineMeshCache(cached, sanitizedInlineMesh);
|
|
@@ -3009,7 +3207,7 @@ export class DaemonCommandRouter {
|
|
|
3009
3207
|
if (cached) {
|
|
3010
3208
|
if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
|
|
3011
3209
|
const merged = reconcileInlineMeshCache(cached, inlineMesh as any);
|
|
3012
|
-
this.inlineMeshCache.set(meshId, sanitizeInlineMesh(merged));
|
|
3210
|
+
this.inlineMeshCache.set(meshId, sanitizeInlineMesh(normalizeInlineMeshNodeIdentity(merged)));
|
|
3013
3211
|
return { mesh: merged, inline: true, source: 'inline_cache' };
|
|
3014
3212
|
}
|
|
3015
3213
|
return { mesh: cached, inline: true, source: 'inline_cache' };
|
|
@@ -3048,18 +3246,25 @@ export class DaemonCommandRouter {
|
|
|
3048
3246
|
}
|
|
3049
3247
|
|
|
3050
3248
|
private updateInlineMeshNode(meshId: string, mesh: any, node: any): void {
|
|
3051
|
-
|
|
3052
|
-
|
|
3249
|
+
const incomingId = normalizeMeshNodeId(node);
|
|
3250
|
+
if (!mesh || !Array.isArray(mesh.nodes) || !incomingId) return;
|
|
3251
|
+
const idx = mesh.nodes.findIndex((entry: any) => meshNodeIdMatches(entry, incomingId));
|
|
3053
3252
|
if (idx >= 0) mesh.nodes[idx] = node;
|
|
3054
3253
|
else mesh.nodes.push(node);
|
|
3055
3254
|
mesh.updatedAt = new Date().toISOString();
|
|
3255
|
+
// Canonicalize node identity in place (id and nodeId kept equal): clone
|
|
3256
|
+
// nodes are created with `id` and re-inserted here (bypassing
|
|
3257
|
+
// warmInlineMeshCache), so this is a second save boundary. In-place
|
|
3258
|
+
// folding preserves the caller's `mesh` / nodes-array references, which
|
|
3259
|
+
// persistWorktreeSetupState reuses across subsequent calls.
|
|
3260
|
+
for (const entry of mesh.nodes) foldMeshNodeIdentityToCanonical(entry);
|
|
3056
3261
|
this.inlineMeshCache.set(meshId, mesh);
|
|
3057
3262
|
this.invalidateAggregateMeshStatus(meshId);
|
|
3058
3263
|
}
|
|
3059
3264
|
|
|
3060
3265
|
private removeInlineMeshNode(meshId: string, mesh: any, nodeId: string): boolean {
|
|
3061
3266
|
if (!mesh || !Array.isArray(mesh.nodes)) return false;
|
|
3062
|
-
const idx = mesh.nodes.findIndex((entry: any) => entry
|
|
3267
|
+
const idx = mesh.nodes.findIndex((entry: any) => meshNodeIdMatches(entry, nodeId));
|
|
3063
3268
|
if (idx === -1) return false;
|
|
3064
3269
|
mesh.nodes.splice(idx, 1);
|
|
3065
3270
|
mesh.updatedAt = new Date().toISOString();
|
|
@@ -3105,7 +3310,7 @@ export class DaemonCommandRouter {
|
|
|
3105
3310
|
|
|
3106
3311
|
const worktreeExists = fs.existsSync(workspace);
|
|
3107
3312
|
const sourceNode = args.node?.clonedFromNodeId
|
|
3108
|
-
? args.mesh?.nodes?.find((n: any) => n
|
|
3313
|
+
? args.mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, args.node.clonedFromNodeId))
|
|
3109
3314
|
: args.mesh?.nodes?.find((n: any) => !n.isLocalWorktree);
|
|
3110
3315
|
const repoRoot = typeof sourceNode?.repoRoot === 'string' && sourceNode.repoRoot.trim()
|
|
3111
3316
|
? sourceNode.repoRoot.trim()
|
|
@@ -3865,7 +4070,7 @@ export class DaemonCommandRouter {
|
|
|
3865
4070
|
// preferInline: same as startMeshRefineJob — inline-cache-only clone nodes must resolve.
|
|
3866
4071
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
3867
4072
|
const mesh = meshRecord?.mesh;
|
|
3868
|
-
const node = mesh?.nodes?.find((n: any) => n
|
|
4073
|
+
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
3869
4074
|
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages };
|
|
3870
4075
|
|
|
3871
4076
|
if (!node.isLocalWorktree || !node.workspace) {
|
|
@@ -3873,7 +4078,7 @@ export class DaemonCommandRouter {
|
|
|
3873
4078
|
}
|
|
3874
4079
|
|
|
3875
4080
|
const sourceNode = node.clonedFromNodeId
|
|
3876
|
-
? mesh?.nodes.find((n: any) => n
|
|
4081
|
+
? mesh?.nodes.find((n: any) => meshNodeIdMatches(n, node.clonedFromNodeId))
|
|
3877
4082
|
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
3878
4083
|
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
3879
4084
|
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found', refineStages };
|
|
@@ -4586,7 +4791,7 @@ export class DaemonCommandRouter {
|
|
|
4586
4791
|
const missing: string[] = [];
|
|
4587
4792
|
const nonWorktree: string[] = [];
|
|
4588
4793
|
for (const nodeId of requestedNodeIds) {
|
|
4589
|
-
const node = allNodes.find(n => n
|
|
4794
|
+
const node = allNodes.find(n => meshNodeIdMatches(n, nodeId));
|
|
4590
4795
|
if (!node) { missing.push(nodeId); continue; }
|
|
4591
4796
|
if (!isConvergeable(node)) { nonWorktree.push(nodeId); continue; }
|
|
4592
4797
|
targetNodes.push(node);
|
|
@@ -4615,7 +4820,7 @@ export class DaemonCommandRouter {
|
|
|
4615
4820
|
// Resolve the base repo root and a base ref to analyze change areas against.
|
|
4616
4821
|
const resolveRepoRootFor = (node: any): string | undefined => {
|
|
4617
4822
|
const sourceNode = node.clonedFromNodeId
|
|
4618
|
-
? allNodes.find(n => n
|
|
4823
|
+
? allNodes.find(n => meshNodeIdMatches(n, node.clonedFromNodeId))
|
|
4619
4824
|
: allNodes.find(n => !n.isLocalWorktree);
|
|
4620
4825
|
return sourceNode?.repoRoot || sourceNode?.workspace;
|
|
4621
4826
|
};
|
|
@@ -4701,7 +4906,7 @@ export class DaemonCommandRouter {
|
|
|
4701
4906
|
|
|
4702
4907
|
const ordering = orderMeshRefineBatchNodes(changeAreas);
|
|
4703
4908
|
const orderedNodes = ordering.order
|
|
4704
|
-
.map(nodeId => targetNodes.find(n => n
|
|
4909
|
+
.map(nodeId => targetNodes.find(n => meshNodeIdMatches(n, nodeId)))
|
|
4705
4910
|
.filter((n): n is any => !!n);
|
|
4706
4911
|
|
|
4707
4912
|
const dryRun = args?.dryRun !== false && args?.execute !== true;
|
|
@@ -5039,7 +5244,7 @@ export class DaemonCommandRouter {
|
|
|
5039
5244
|
const mesh = meshRecord?.mesh;
|
|
5040
5245
|
const allNodes: any[] = Array.isArray(mesh?.nodes) ? mesh.nodes : [];
|
|
5041
5246
|
const orderedNodes = nodeIds
|
|
5042
|
-
.map(id => allNodes.find(n => n
|
|
5247
|
+
.map(id => allNodes.find(n => meshNodeIdMatches(n, id)))
|
|
5043
5248
|
.filter((n): n is any => !!n);
|
|
5044
5249
|
if (orderedNodes.length === 0) {
|
|
5045
5250
|
return { success: false, error: 'Batch nodes no longer resolvable in mesh', batch: true };
|
|
@@ -5198,7 +5403,7 @@ export class DaemonCommandRouter {
|
|
|
5198
5403
|
// config-first and misses nodes that only live in the inline cache.
|
|
5199
5404
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
5200
5405
|
const mesh = meshRecord?.mesh;
|
|
5201
|
-
const node = mesh?.nodes?.find((n: any) => n
|
|
5406
|
+
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
5202
5407
|
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
5203
5408
|
if (!node.isLocalWorktree || !node.workspace) return { success: false, error: `Refinery requires a local worktree node` };
|
|
5204
5409
|
|
|
@@ -5282,7 +5487,7 @@ export class DaemonCommandRouter {
|
|
|
5282
5487
|
const { getMesh } = await import('../config/mesh-config.js');
|
|
5283
5488
|
const meshObj = getMesh(meshId) ?? this.getCachedInlineMesh(meshId);
|
|
5284
5489
|
const nodeObj = Array.isArray(meshObj?.nodes)
|
|
5285
|
-
? meshObj.nodes.find((n: any) => n
|
|
5490
|
+
? meshObj.nodes.find((n: any) => meshNodeIdMatches(n, meshNodeId))
|
|
5286
5491
|
: undefined;
|
|
5287
5492
|
const bootstrapStatus = readStringValue(nodeObj?.worktreeBootstrap?.status);
|
|
5288
5493
|
if (bootstrapStatus === 'running') {
|
|
@@ -5339,7 +5544,7 @@ export class DaemonCommandRouter {
|
|
|
5339
5544
|
const { getMesh } = await import('../config/mesh-config.js');
|
|
5340
5545
|
const meshObj = getMesh(dispatchMeshId) ?? this.getCachedInlineMesh(dispatchMeshId);
|
|
5341
5546
|
const nodeObj = Array.isArray(meshObj?.nodes)
|
|
5342
|
-
? meshObj.nodes.find((n: any) => n
|
|
5547
|
+
? meshObj.nodes.find((n: any) => meshNodeIdMatches(n, dispatchNodeId))
|
|
5343
5548
|
: undefined;
|
|
5344
5549
|
const bootstrapStatus = readStringValue(nodeObj?.worktreeBootstrap?.status);
|
|
5345
5550
|
if (bootstrapStatus === 'running') {
|
|
@@ -6778,7 +6983,7 @@ export class DaemonCommandRouter {
|
|
|
6778
6983
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
6779
6984
|
const mesh = meshRecord?.mesh;
|
|
6780
6985
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
6781
|
-
const node = mesh?.nodes?.find((n: any) => n
|
|
6986
|
+
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
6782
6987
|
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
6783
6988
|
const mode = this.normalizeMeshSessionCleanupMode(args?.mode ?? mesh?.policy?.sessionCleanupOnNodeRemove);
|
|
6784
6989
|
const sessionIds = Array.isArray(args?.sessionIds)
|
|
@@ -6844,7 +7049,7 @@ export class DaemonCommandRouter {
|
|
|
6844
7049
|
// preferInline: plan is the dry-run sibling of refine — clone nodes must resolve.
|
|
6845
7050
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
6846
7051
|
const mesh = meshRecord?.mesh;
|
|
6847
|
-
const node = mesh?.nodes?.find((n: any) => n
|
|
7052
|
+
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
6848
7053
|
if (!node?.workspace) return { success: false, error: `Node '${nodeId}' workspace not found` };
|
|
6849
7054
|
return {
|
|
6850
7055
|
success: true,
|
|
@@ -6870,7 +7075,7 @@ export class DaemonCommandRouter {
|
|
|
6870
7075
|
// preferInline so fast-forward can resolve inline-cache-only clone worktree nodes.
|
|
6871
7076
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
6872
7077
|
const mesh = meshRecord?.mesh;
|
|
6873
|
-
const node = mesh?.nodes?.find((n: any) => n
|
|
7078
|
+
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
6874
7079
|
if (!workspace) {
|
|
6875
7080
|
workspace = typeof node?.workspace === 'string' ? node.workspace.trim() : '';
|
|
6876
7081
|
}
|
|
@@ -6923,7 +7128,7 @@ export class DaemonCommandRouter {
|
|
|
6923
7128
|
// preferInline: plan is the dry-run sibling of refine — clone nodes must resolve.
|
|
6924
7129
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
6925
7130
|
const mesh = meshRecord?.mesh;
|
|
6926
|
-
const node = mesh?.nodes?.find((n: any) => n
|
|
7131
|
+
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
6927
7132
|
if (!node?.workspace) return { success: false, error: `Node '${nodeId}' workspace not found` };
|
|
6928
7133
|
return {
|
|
6929
7134
|
success: true,
|
|
@@ -6963,7 +7168,7 @@ export class DaemonCommandRouter {
|
|
|
6963
7168
|
// preferInline so removal can resolve inline-cache-only clone worktree nodes.
|
|
6964
7169
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
6965
7170
|
const mesh = meshRecord?.mesh;
|
|
6966
|
-
const node = mesh?.nodes?.find((n: any) => n
|
|
7171
|
+
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
6967
7172
|
|
|
6968
7173
|
// Guard: refuse to remove the coordinator's OWN local base node
|
|
6969
7174
|
// (same machine, NOT a worktree). Removing it breaks live mesh
|
|
@@ -7115,7 +7320,7 @@ export class DaemonCommandRouter {
|
|
|
7115
7320
|
const mesh = meshRecord?.mesh;
|
|
7116
7321
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
7117
7322
|
|
|
7118
|
-
const sourceNode = mesh.nodes?.find((n: any) => n
|
|
7323
|
+
const sourceNode = mesh.nodes?.find((n: any) => meshNodeIdMatches(n, sourceNodeId));
|
|
7119
7324
|
if (!sourceNode) return { success: false, error: `Source node '${sourceNodeId}' not found in mesh` };
|
|
7120
7325
|
|
|
7121
7326
|
// Forward to the source node's daemon if it's on a different machine.
|
|
@@ -7390,7 +7595,7 @@ export class DaemonCommandRouter {
|
|
|
7390
7595
|
const mesh = meshRecord?.mesh;
|
|
7391
7596
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
7392
7597
|
|
|
7393
|
-
const node = mesh.nodes?.find((n: any) => n
|
|
7598
|
+
const node = mesh.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
7394
7599
|
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
7395
7600
|
if (!node.isLocalWorktree) return { success: false, error: 'Node is not a local worktree node' };
|
|
7396
7601
|
|
|
@@ -7566,14 +7771,14 @@ export class DaemonCommandRouter {
|
|
|
7566
7771
|
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
7567
7772
|
const workspace = readLiveMeshNodeWorkspace({
|
|
7568
7773
|
meshId,
|
|
7569
|
-
nodeId: String(coordinatorNode
|
|
7774
|
+
nodeId: String(normalizeMeshNodeId(coordinatorNode) || preferredCoordinatorNodeId || ''),
|
|
7570
7775
|
liveSessionRecords: liveMeshSessions,
|
|
7571
7776
|
allowCoordinatorSession: true,
|
|
7572
7777
|
}) || (typeof coordinatorNode.workspace === 'string' ? coordinatorNode.workspace.trim() : '');
|
|
7573
7778
|
if (!workspace) return { success: false, error: 'Coordinator node workspace required', meshId, cliType };
|
|
7574
7779
|
if (!cliType) {
|
|
7575
7780
|
const resolved = await resolveProviderTypeFromPriority({
|
|
7576
|
-
nodeId: String(coordinatorNode
|
|
7781
|
+
nodeId: String(normalizeMeshNodeId(coordinatorNode) || preferredCoordinatorNodeId || 'coordinator'),
|
|
7577
7782
|
providerPriority: readProviderPriorityFromPolicy(coordinatorNode.policy),
|
|
7578
7783
|
providerLoader: this.deps.providerLoader,
|
|
7579
7784
|
onStatusChange: this.deps.onStatusChange,
|
|
@@ -8062,6 +8267,13 @@ export class DaemonCommandRouter {
|
|
|
8062
8267
|
const meshHost = resolveMeshHostStatus(mesh);
|
|
8063
8268
|
|
|
8064
8269
|
const refreshRequested = args?.refresh === true || args?.forceRefresh === true;
|
|
8270
|
+
// Compact (default) elides each mission's full goal text from the
|
|
8271
|
+
// payload — coordinators polling node health don't need every
|
|
8272
|
+
// mission's multi-hundred-char goal repeated. verbose=true (or the
|
|
8273
|
+
// explicit compact=false) restores full goals. Verbose bypasses the
|
|
8274
|
+
// shared (compact) aggregate cache so a verbose call never poisons
|
|
8275
|
+
// the compact cache and vice versa.
|
|
8276
|
+
const verboseMissions = args?.verbose === true || args?.compact === false;
|
|
8065
8277
|
// See (B3) below: scope the peek to this daemon when the
|
|
8066
8278
|
// caller doesn't tell us, otherwise scoped events look
|
|
8067
8279
|
// missing and we falsely return a stale cache.
|
|
@@ -8070,7 +8282,7 @@ export class DaemonCommandRouter {
|
|
|
8070
8282
|
: (this.deps.statusInstanceId || undefined);
|
|
8071
8283
|
const pendingCoordinatorEventCount = getPendingMeshCoordinatorEvents(meshId, peekScope).length;
|
|
8072
8284
|
const hadAggregateCache = this.aggregateMeshStatusCache.has(meshId);
|
|
8073
|
-
if (!refreshRequested && pendingCoordinatorEventCount === 0) {
|
|
8285
|
+
if (!refreshRequested && !verboseMissions && pendingCoordinatorEventCount === 0) {
|
|
8074
8286
|
const cachedStatus = this.getCachedAggregateMeshStatus(meshId, mesh, { requireDirectPeerTruth: args?.requireDirectPeerTruth === true });
|
|
8075
8287
|
if (cachedStatus) {
|
|
8076
8288
|
logRepoMeshStatusDebug('return_cached', {
|
|
@@ -8133,7 +8345,7 @@ export class DaemonCommandRouter {
|
|
|
8133
8345
|
const unavailableNodesAreOnlyRemovedWorktrees = unavailableDirectTruthNodeIds.size > 0
|
|
8134
8346
|
&& Array.isArray(mesh.nodes)
|
|
8135
8347
|
&& mesh.nodes
|
|
8136
|
-
.filter((node: any) => unavailableDirectTruthNodeIds.has(
|
|
8348
|
+
.filter((node: any) => unavailableDirectTruthNodeIds.has(normalizeMeshNodeId(node) ?? ''))
|
|
8137
8349
|
.every((node: any) => node?.isLocalWorktree === true);
|
|
8138
8350
|
const directTruthSatisfied = !requireDirectPeerTruth
|
|
8139
8351
|
|| (effectiveDirectTruth.directEvidenceCount > 0 && (effectiveDirectTruth.unavailableNodeIds.length === 0 || unavailableNodesAreOnlyRemovedWorktrees));
|
|
@@ -8174,8 +8386,7 @@ export class DaemonCommandRouter {
|
|
|
8174
8386
|
const coordinatorHostname = osHostname();
|
|
8175
8387
|
const selectedCoordinatorNodeId = readStringValue(
|
|
8176
8388
|
mesh.coordinator?.preferredNodeId,
|
|
8177
|
-
(mesh.nodes?.[0] as any)
|
|
8178
|
-
(mesh.nodes?.[0] as any)?.nodeId,
|
|
8389
|
+
normalizeMeshNodeId(mesh.nodes?.[0] as any),
|
|
8179
8390
|
);
|
|
8180
8391
|
const inlineCoordinatorNodeId = meshRecord?.inline && Array.isArray(mesh.nodes)
|
|
8181
8392
|
? selectedCoordinatorNodeId
|
|
@@ -8183,7 +8394,7 @@ export class DaemonCommandRouter {
|
|
|
8183
8394
|
const refreshedAt = new Date().toISOString();
|
|
8184
8395
|
const nodeStatuses = [];
|
|
8185
8396
|
for (const [nodeIndex, node] of (mesh.nodes || []).entries()) {
|
|
8186
|
-
const nodeId =
|
|
8397
|
+
const nodeId = normalizeMeshNodeId(node) ?? '';
|
|
8187
8398
|
const daemonId = readStringValue(node.daemonId);
|
|
8188
8399
|
const nodeMachineId = readMeshNodeMachineId(node as Record<string, unknown>);
|
|
8189
8400
|
const nodeHostname = readMeshNodeHostname(node as Record<string, unknown>);
|
|
@@ -8445,7 +8656,7 @@ export class DaemonCommandRouter {
|
|
|
8445
8656
|
liveSessionRecords: liveMeshSessions,
|
|
8446
8657
|
});
|
|
8447
8658
|
const { getMeshStatusMissionSummaries } = await import('../mesh/mesh-missions.js');
|
|
8448
|
-
const missions = getMeshStatusMissionSummaries(meshId);
|
|
8659
|
+
const missions = getMeshStatusMissionSummaries(meshId, { verbose: verboseMissions });
|
|
8449
8660
|
const statusResult = {
|
|
8450
8661
|
success: true,
|
|
8451
8662
|
meshId: mesh.id,
|
|
@@ -8505,7 +8716,12 @@ export class DaemonCommandRouter {
|
|
|
8505
8716
|
})),
|
|
8506
8717
|
};
|
|
8507
8718
|
const { pendingCoordinatorEvents: _pendingCoordinatorEvents, unroutableDeliveries: _unroutableDeliveries, ...cacheableStatusResult } = statusResult as any;
|
|
8508
|
-
|
|
8719
|
+
// Verbose carries full mission goals; never store it in the shared
|
|
8720
|
+
// (compact) aggregate cache or a later compact poll would return the
|
|
8721
|
+
// heavy goals from cache. Return it without caching.
|
|
8722
|
+
const rememberedStatus = verboseMissions
|
|
8723
|
+
? cacheableStatusResult
|
|
8724
|
+
: this.rememberAggregateMeshStatus(meshId, cacheableStatusResult, refreshReason);
|
|
8509
8725
|
const returnedStatus = {
|
|
8510
8726
|
...rememberedStatus,
|
|
8511
8727
|
...(pendingCoordinatorEvents.length > 0 ? { pendingCoordinatorEvents } : {}),
|