@adhdev/daemon-core 0.9.82-rc.19 → 0.9.82-rc.190
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/boot/daemon-lifecycle.d.ts +1 -0
- package/dist/boot/process-hardening.d.ts +50 -0
- package/dist/chat/source-machine.d.ts +166 -0
- package/dist/chat/source-resolver.d.ts +104 -0
- package/dist/chat/subscription-updates.d.ts +1 -0
- package/dist/cli-adapter-types.d.ts +22 -2
- package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +106 -63
- package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +29 -0
- package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
- package/dist/commands/cli-manager.d.ts +2 -1
- package/dist/commands/handler.d.ts +110 -0
- package/dist/commands/mesh-coordinator.d.ts +85 -1
- package/dist/commands/router.d.ts +26 -1
- package/dist/config/chat-history.d.ts +9 -0
- package/dist/config/config.d.ts +5 -0
- package/dist/config/mesh-config.d.ts +72 -1
- package/dist/git/git-commands.d.ts +7 -1
- package/dist/git/git-types.d.ts +2 -0
- package/dist/index.d.ts +37 -10
- package/dist/index.js +32814 -16395
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +32529 -16180
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/ipc/local-ipc-server.d.ts +91 -0
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/mesh/beads-db.d.ts +73 -0
- package/dist/mesh/contracts.d.ts +164 -0
- package/dist/mesh/coordinator-prompt.d.ts +30 -0
- package/dist/mesh/coordinator-registry.d.ts +59 -0
- package/dist/mesh/mesh-active-work.d.ts +90 -0
- package/dist/mesh/mesh-events.d.ts +143 -6
- package/dist/mesh/mesh-fast-forward.d.ts +41 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-ledger.d.ts +58 -1
- package/dist/mesh/mesh-refine-status.d.ts +26 -0
- package/dist/mesh/mesh-work-queue.d.ts +55 -6
- package/dist/mesh/preview-freshness.d.ts +18 -0
- package/dist/mesh/refine-config.d.ts +193 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +113 -0
- package/dist/providers/approval-utils.d.ts +13 -0
- package/dist/providers/cli-provider-instance.d.ts +39 -3
- package/dist/providers/contracts.d.ts +130 -6
- package/dist/providers/external-sources.d.ts +71 -0
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
- package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
- package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
- package/dist/providers/native-history/dispatcher.d.ts +27 -0
- package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
- package/dist/providers/native-history/index.d.ts +13 -0
- package/dist/providers/provider-instance-manager.d.ts +12 -0
- package/dist/providers/provider-instance.d.ts +12 -0
- package/dist/providers/provider-loader.d.ts +26 -4
- package/dist/providers/provider-trust.d.ts +31 -0
- package/dist/providers/read-chat-contract.d.ts +29 -0
- package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
- package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
- package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
- package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
- package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
- package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
- package/dist/providers/sdk/v1/index.d.ts +30 -0
- package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
- package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
- package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
- package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
- package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
- package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
- package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
- package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
- package/dist/providers/spec/adapter.d.ts +60 -0
- package/dist/providers/spec/cli-adapter.d.ts +98 -0
- package/dist/providers/spec/driver.d.ts +168 -0
- package/dist/providers/spec/evaluator.d.ts +55 -0
- package/dist/providers/spec/loader.d.ts +14 -0
- package/dist/providers/spec/native-history-executor.d.ts +41 -0
- package/dist/providers/spec/route.d.ts +4 -0
- package/dist/providers/spec/schema.gen.d.ts +549 -0
- package/dist/providers/spec/types.d.ts +237 -0
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/providers/types/interactive-prompt.d.ts +48 -0
- package/dist/repo-mesh-types.d.ts +108 -1
- package/dist/sessions/registry.d.ts +3 -0
- package/dist/shared-types.d.ts +42 -1
- package/dist/status/reporter.d.ts +2 -0
- package/dist/status/snapshot.d.ts +1 -0
- package/dist/types.d.ts +5 -0
- package/package.json +7 -2
- package/src/agent-stream/poller.ts +2 -3
- package/src/boot/daemon-lifecycle.ts +19 -10
- package/src/boot/process-hardening.ts +89 -0
- package/src/chat/source-machine.ts +534 -0
- package/src/chat/source-resolver.ts +0 -0
- package/src/chat/subscription-updates.ts +20 -1
- package/src/cli-adapter-types.d.ts +1 -0
- package/src/cli-adapter-types.ts +21 -2
- package/src/cli-adapters/cli-script-runner.ts +421 -0
- package/src/cli-adapters/cli-state-engine.ts +1086 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +670 -1137
- package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/src/cli-adapters/provider-cli-parse.ts +13 -0
- package/src/cli-adapters/provider-cli-runtime.ts +3 -1
- package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/src/cli-adapters/provider-cli-shared.ts +59 -11
- package/src/cli-adapters/raw-terminal-io.ts +252 -0
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
- package/src/commands/chat-commands.ts +1787 -60
- package/src/commands/cli-manager.ts +283 -14
- package/src/commands/handler.ts +809 -2
- package/src/commands/mesh-coordinator.ts +331 -122
- package/src/commands/router.ts +3600 -510
- package/src/config/chat-history.ts +95 -24
- package/src/config/config.ts +12 -0
- package/src/config/mesh-config.ts +280 -2
- package/src/config/recent-activity.ts +8 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/detection/ide-detector.ts +26 -16
- package/src/git/git-commands.ts +37 -7
- package/src/git/git-status.ts +35 -6
- package/src/git/git-types.ts +2 -0
- package/src/git/git-worktree.ts +8 -1
- package/src/index.ts +115 -9
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/ipc/local-ipc-server.ts +278 -0
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/logger.ts +2 -1
- package/src/mesh/beads-db.ts +490 -0
- package/src/mesh/contracts.ts +329 -0
- package/src/mesh/coordinator-prompt.ts +204 -30
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +437 -0
- package/src/mesh/mesh-events.ts +1234 -71
- package/src/mesh/mesh-fast-forward.ts +438 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger.ts +457 -104
- package/src/mesh/mesh-refine-status.ts +144 -0
- package/src/mesh/mesh-work-queue.ts +267 -159
- package/src/mesh/preview-freshness.ts +118 -0
- package/src/mesh/refine-config.ts +366 -0
- package/src/mesh/worktree-bootstrap-config.ts +247 -0
- package/src/providers/approval-utils.d.ts +4 -0
- package/src/providers/approval-utils.ts +47 -5
- package/src/providers/chat-message-normalization.ts +4 -11
- package/src/providers/cli-provider-instance.ts +763 -60
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +141 -6
- package/src/providers/external-sources.ts +218 -0
- package/src/providers/ide-provider-instance.ts +19 -5
- package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
- package/src/providers/native-history/claude-cli-transcript.ts +396 -0
- package/src/providers/native-history/codex-cli-transcript.ts +419 -0
- package/src/providers/native-history/dispatcher.ts +340 -0
- package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
- package/src/providers/native-history/index.ts +30 -0
- package/src/providers/provider-instance-manager.ts +30 -0
- package/src/providers/provider-instance.ts +9 -0
- package/src/providers/provider-loader.ts +582 -50
- package/src/providers/provider-schema.ts +87 -14
- package/src/providers/provider-trust.ts +114 -0
- package/src/providers/read-chat-contract.ts +76 -16
- package/src/providers/sdk/README.md +49 -0
- package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
- package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
- package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
- package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
- package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
- package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
- package/src/providers/sdk/v1/index.ts +152 -0
- package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
- package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
- package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
- package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
- package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
- package/src/providers/sdk/v1/types/cli/index.ts +365 -0
- package/src/providers/sdk/v1/types/common/index.ts +229 -0
- package/src/providers/sdk/v1/validators/index.ts +19 -0
- package/src/providers/sdk/v1/validators/manifest.ts +110 -0
- package/src/providers/sdk/v1/validators/taint.ts +309 -0
- package/src/providers/spec/adapter.ts +176 -0
- package/src/providers/spec/cli-adapter.ts +534 -0
- package/src/providers/spec/driver.ts +612 -0
- package/src/providers/spec/evaluator.ts +344 -0
- package/src/providers/spec/loader.ts +130 -0
- package/src/providers/spec/native-history-executor.ts +939 -0
- package/src/providers/spec/route.ts +51 -0
- package/src/providers/spec/schema.gen.ts +538 -0
- package/src/providers/spec/schema.json +235 -0
- package/src/providers/spec/types.ts +256 -0
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +408 -0
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +118 -1
- package/src/sessions/registry.ts +3 -0
- package/src/shared-types.ts +45 -1
- package/src/status/builders.ts +24 -6
- package/src/status/reporter.ts +15 -0
- package/src/status/snapshot.ts +84 -25
- package/src/system/host-memory.ts +29 -12
- package/src/types.ts +5 -0
- package/dist/mesh/mesh-sync.d.ts +0 -53
- package/src/mesh/mesh-sync.ts +0 -111
package/src/commands/router.ts
CHANGED
|
@@ -38,14 +38,38 @@ import { createInteractionId, getRecentDebugTrace, recordDebugTrace } from '../l
|
|
|
38
38
|
import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../session-host/runtime-surface.js';
|
|
39
39
|
import { createHermesManualMeshCoordinatorSetup, resolveMeshCoordinatorSetup } from './mesh-coordinator.js';
|
|
40
40
|
import { buildSessionEntries } from '../status/builders.js';
|
|
41
|
-
import {
|
|
41
|
+
import { registerMeshCoordinator, getCoordinatorForSession } from '../mesh/coordinator-registry.js';
|
|
42
|
+
import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
|
|
43
|
+
import { buildMeshHostRequiredFailure, normalizeMeshDaemonRole, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
|
|
44
|
+
import { fastForwardMeshNode } from '../mesh/mesh-fast-forward.js';
|
|
45
|
+
import { buildPreviewFreshness } from '../mesh/preview-freshness.js';
|
|
46
|
+
import { buildMeshAsyncRefineJobs } from '../mesh/mesh-refine-status.js';
|
|
47
|
+
import {
|
|
48
|
+
MESH_REFINE_CONFIG_LOCATIONS,
|
|
49
|
+
MESH_REFINE_CONFIG_SCHEMA,
|
|
50
|
+
loadMeshRefineConfig,
|
|
51
|
+
resolveMeshRefineValidationPlan,
|
|
52
|
+
suggestMeshRefineConfig,
|
|
53
|
+
validateMeshRefineConfig,
|
|
54
|
+
type MeshRefineValidationCommandPlan,
|
|
55
|
+
} from '../mesh/refine-config.js';
|
|
56
|
+
import {
|
|
57
|
+
MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS,
|
|
58
|
+
MESH_WORKTREE_BOOTSTRAP_CONFIG_SCHEMA,
|
|
59
|
+
loadMeshWorktreeBootstrapConfig,
|
|
60
|
+
runMeshWorktreeBootstrap,
|
|
61
|
+
type WorktreeBootstrapState,
|
|
62
|
+
} from '../mesh/worktree-bootstrap-config.js';
|
|
42
63
|
import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
|
|
43
64
|
import { getSessionCompletionMarker } from '../status/snapshot.js';
|
|
44
65
|
import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
|
|
66
|
+
import { getMeshQueueRevision } from '../mesh/mesh-work-queue.js';
|
|
45
67
|
import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
|
|
46
|
-
import { homedir } from 'os';
|
|
47
|
-
import { join as pathJoin, resolve as pathResolve } from 'path';
|
|
68
|
+
import { homedir, hostname as osHostname } from 'os';
|
|
69
|
+
import { basename as pathBasename, join as pathJoin, resolve as pathResolve } from 'path';
|
|
48
70
|
import * as fs from 'fs';
|
|
71
|
+
import { execFileSync } from 'node:child_process';
|
|
72
|
+
import { normalizeInteractivePromptResponse } from '../providers/types/interactive-prompt.js';
|
|
49
73
|
|
|
50
74
|
type ReleaseChannel = 'stable' | 'preview';
|
|
51
75
|
const CHANNEL_NPM_TAG: Record<ReleaseChannel, 'latest' | 'next'> = { stable: 'latest', preview: 'next' };
|
|
@@ -114,14 +138,95 @@ function readBooleanValue(...values: unknown[]): boolean | undefined {
|
|
|
114
138
|
return undefined;
|
|
115
139
|
}
|
|
116
140
|
|
|
117
|
-
function
|
|
141
|
+
function summarizeRepoMeshDebugGit(git: unknown): Record<string, unknown> | null {
|
|
142
|
+
const record = readObjectRecord(git);
|
|
143
|
+
if (!Object.keys(record).length) return null;
|
|
144
|
+
const submodules = Array.isArray(record.submodules)
|
|
145
|
+
? record.submodules.map((entry: any) => ({
|
|
146
|
+
path: readStringValue(entry?.path) ?? null,
|
|
147
|
+
commit: readStringValue(entry?.commit)?.slice(0, 12) ?? null,
|
|
148
|
+
dirty: readBooleanValue(entry?.dirty) ?? false,
|
|
149
|
+
outOfSync: readBooleanValue(entry?.outOfSync, entry?.out_of_sync) ?? false,
|
|
150
|
+
}))
|
|
151
|
+
: [];
|
|
152
|
+
return {
|
|
153
|
+
isGitRepo: readBooleanValue(record.isGitRepo),
|
|
154
|
+
workspace: readStringValue(record.workspace) ?? null,
|
|
155
|
+
repoRoot: readStringValue(record.repoRoot, record.repo_root) ?? null,
|
|
156
|
+
branch: readStringValue(record.branch) ?? null,
|
|
157
|
+
upstream: readStringValue(record.upstream) ?? null,
|
|
158
|
+
upstreamStatus: readStringValue(record.upstreamStatus, record.upstream_status) ?? null,
|
|
159
|
+
headCommit: readStringValue(record.headCommit, record.head_commit)?.slice(0, 12) ?? null,
|
|
160
|
+
ahead: readNumberValue(record.ahead) ?? null,
|
|
161
|
+
behind: readNumberValue(record.behind) ?? null,
|
|
162
|
+
dirtyCounts: {
|
|
163
|
+
staged: readNumberValue(record.staged) ?? 0,
|
|
164
|
+
modified: readNumberValue(record.modified) ?? 0,
|
|
165
|
+
untracked: readNumberValue(record.untracked) ?? 0,
|
|
166
|
+
deleted: readNumberValue(record.deleted) ?? 0,
|
|
167
|
+
renamed: readNumberValue(record.renamed) ?? 0,
|
|
168
|
+
},
|
|
169
|
+
lastCheckedAt: readNumberValue(record.lastCheckedAt, record.last_checked_at) ?? null,
|
|
170
|
+
submoduleCount: submodules.length,
|
|
171
|
+
submodules,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function summarizeRepoMeshStatusDebug(status: any): Record<string, unknown> {
|
|
176
|
+
const nodes = Array.isArray(status?.nodes) ? status.nodes : [];
|
|
177
|
+
return {
|
|
178
|
+
success: status?.success,
|
|
179
|
+
meshId: readStringValue(status?.meshId, status?.mesh_id) ?? null,
|
|
180
|
+
refreshedAt: readStringValue(status?.refreshedAt, status?.refreshed_at) ?? null,
|
|
181
|
+
sourceOfTruth: status?.sourceOfTruth ?? null,
|
|
182
|
+
branchConvergenceSummary: status?.branchConvergenceSummary ?? status?.branch_convergence_summary ?? null,
|
|
183
|
+
nodeCount: nodes.length,
|
|
184
|
+
nodes: nodes.map((node: any) => ({
|
|
185
|
+
nodeId: readStringValue(node?.nodeId, node?.id) ?? null,
|
|
186
|
+
daemonId: readStringValue(node?.daemonId, node?.daemon_id) ?? null,
|
|
187
|
+
workspace: readStringValue(node?.workspace, node?.git?.workspace) ?? null,
|
|
188
|
+
health: readStringValue(node?.health) ?? null,
|
|
189
|
+
machineStatus: readStringValue(node?.machineStatus, node?.machine_status) ?? null,
|
|
190
|
+
connection: node?.connection && typeof node.connection === 'object' ? {
|
|
191
|
+
state: readStringValue(node.connection.state) ?? null,
|
|
192
|
+
transport: readStringValue(node.connection.transport) ?? null,
|
|
193
|
+
source: readStringValue(node.connection.source) ?? null,
|
|
194
|
+
reported: readBooleanValue(node.connection.reported) ?? null,
|
|
195
|
+
} : null,
|
|
196
|
+
gitProbePending: node?.gitProbePending === true,
|
|
197
|
+
launchReady: node?.launchReady === true,
|
|
198
|
+
git: summarizeRepoMeshDebugGit(node?.git),
|
|
199
|
+
branchConvergence: node?.branchConvergence ?? node?.branch_convergence ?? null,
|
|
200
|
+
})),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function logRepoMeshStatusDebug(event: string, fields: Record<string, unknown>): void {
|
|
205
|
+
try {
|
|
206
|
+
LOG.info('MeshStatusDebug', `[RepoMeshStatusDebug] ${JSON.stringify({ event, ...fields })}`);
|
|
207
|
+
} catch {
|
|
208
|
+
LOG.info('MeshStatusDebug', `[RepoMeshStatusDebug] ${event}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function joinRepoPath(root: string | undefined, relativePath: string | undefined): string | undefined {
|
|
213
|
+
const normalizedRoot = typeof root === 'string' ? root.trim().replace(/[\\/]+$/, '') : '';
|
|
214
|
+
const normalizedPath = typeof relativePath === 'string' ? relativePath.trim() : '';
|
|
215
|
+
if (!normalizedPath) return undefined;
|
|
216
|
+
if (/^(?:[A-Za-z]:[\\/]|\/)/.test(normalizedPath)) return normalizedPath;
|
|
217
|
+
if (!normalizedRoot) return undefined;
|
|
218
|
+
return `${normalizedRoot}/${normalizedPath.replace(/^[\\/]+/, '')}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function readGitSubmodules(value: unknown, parentRepoRoot?: string): GitSubmoduleStatus[] | undefined {
|
|
118
222
|
if (!Array.isArray(value)) return undefined;
|
|
119
223
|
const submodules = value
|
|
120
224
|
.map(entry => {
|
|
121
225
|
const submodule = readObjectRecord(entry);
|
|
122
226
|
const path = readStringValue(submodule.path);
|
|
123
227
|
const commit = readStringValue(submodule.commit);
|
|
124
|
-
const repoPath = readStringValue(submodule.repoPath, submodule.repo_root)
|
|
228
|
+
const repoPath = readStringValue(submodule.repoPath, submodule.repo_root)
|
|
229
|
+
?? joinRepoPath(parentRepoRoot, path);
|
|
125
230
|
if (!path || !commit || !repoPath) return null;
|
|
126
231
|
return {
|
|
127
232
|
path,
|
|
@@ -137,60 +242,146 @@ function readGitSubmodules(value: unknown): GitSubmoduleStatus[] | undefined {
|
|
|
137
242
|
return submodules.length > 0 ? submodules : undefined;
|
|
138
243
|
}
|
|
139
244
|
|
|
140
|
-
function
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
245
|
+
function buildMeshNodeDisplayLabel(node: Record<string, unknown>, nodeId: string, providerPriority: string[]): string {
|
|
246
|
+
const explicit = readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias);
|
|
247
|
+
if (explicit) return explicit;
|
|
248
|
+
const workspace = readStringValue(node.workspace, node.repoRoot, node.repo_root);
|
|
249
|
+
const workspaceName = workspace ? pathBasename(workspace) : undefined;
|
|
250
|
+
const host = readStringValue(node.machineName, node.machine_name, node.hostname, node.host, node.daemonId, node.daemon_id, node.machineId, node.machine_id);
|
|
251
|
+
const provider = providerPriority[0] || (Array.isArray(node.providers) ? readStringValue(...node.providers) : undefined);
|
|
252
|
+
const parts = [workspaceName, host, provider].filter(Boolean);
|
|
253
|
+
if (parts.length > 0) return parts.join(' · ');
|
|
254
|
+
return nodeId || 'unidentified mesh node';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function normalizeMeshHostname(value: unknown): string | undefined {
|
|
258
|
+
const hostname = readStringValue(value);
|
|
259
|
+
if (!hostname) return undefined;
|
|
260
|
+
return hostname.toLowerCase().replace(/\.$/, '');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function readMeshNodeMachineId(node: Record<string, unknown>): string | undefined {
|
|
264
|
+
return readStringValue(
|
|
265
|
+
node.machineId,
|
|
266
|
+
node.machine_id,
|
|
267
|
+
readObjectRecord(node.machine)?.id,
|
|
268
|
+
readObjectRecord(node.machine)?.machineId,
|
|
269
|
+
readObjectRecord(node.lastProbe)?.machineId,
|
|
270
|
+
readObjectRecord(node.last_probe)?.machine_id,
|
|
271
|
+
readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.id,
|
|
272
|
+
readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.machineId,
|
|
273
|
+
readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.id,
|
|
274
|
+
readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.machine_id,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function readMeshNodeDaemonId(node: Record<string, unknown>): string | undefined {
|
|
279
|
+
return readStringValue(
|
|
280
|
+
node.daemonId,
|
|
281
|
+
node.daemon_id,
|
|
282
|
+
readObjectRecord(node.machine)?.daemonId,
|
|
283
|
+
readObjectRecord(node.machine)?.daemon_id,
|
|
284
|
+
readObjectRecord(node.lastProbe)?.daemonId,
|
|
285
|
+
readObjectRecord(node.last_probe)?.daemon_id,
|
|
286
|
+
readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.daemonId,
|
|
287
|
+
readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.daemon_id,
|
|
288
|
+
readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.daemonId,
|
|
289
|
+
readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.daemon_id,
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function readMeshNodeHostname(node: Record<string, unknown>): string | undefined {
|
|
294
|
+
return readStringValue(
|
|
295
|
+
node.hostname,
|
|
296
|
+
node.host,
|
|
297
|
+
node.machineHostname,
|
|
298
|
+
node.machine_hostname,
|
|
299
|
+
readObjectRecord(node.machine)?.hostname,
|
|
300
|
+
readObjectRecord(node.machine)?.host,
|
|
301
|
+
readObjectRecord(node.lastProbe)?.hostname,
|
|
302
|
+
readObjectRecord(node.last_probe)?.hostname,
|
|
303
|
+
readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.hostname,
|
|
304
|
+
readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.hostname,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function readMeshNodeDisplayMachineName(node: Record<string, unknown>): string | undefined {
|
|
309
|
+
return readStringValue(
|
|
310
|
+
node.machineName,
|
|
311
|
+
node.machine_name,
|
|
312
|
+
node.machineLabel,
|
|
313
|
+
node.machine_label,
|
|
314
|
+
node.machineNickname,
|
|
315
|
+
node.machine_nickname,
|
|
316
|
+
node.alias,
|
|
317
|
+
readObjectRecord(node.machine)?.name,
|
|
318
|
+
readObjectRecord(node.machine)?.displayName,
|
|
319
|
+
readObjectRecord(node.machine)?.display_name,
|
|
320
|
+
readObjectRecord(node.lastProbe)?.machineName,
|
|
321
|
+
readObjectRecord(node.last_probe)?.machine_name,
|
|
322
|
+
readObjectRecord(readObjectRecord(node.lastProbe)?.machine)?.name,
|
|
323
|
+
readObjectRecord(readObjectRecord(node.last_probe)?.machine)?.name,
|
|
324
|
+
readMeshNodeHostname(node),
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function compactMeshIdentityEvidence(value: string | undefined): string | undefined {
|
|
329
|
+
if (!value) return undefined;
|
|
330
|
+
return value.length > 24 ? `${value.slice(0, 12)}…${value.slice(-8)}` : value;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function buildMeshNodeMachineIdentity(node: Record<string, unknown>, opts: {
|
|
334
|
+
localMachineId?: string;
|
|
335
|
+
localDaemonId?: string;
|
|
336
|
+
coordinatorHostname?: string;
|
|
337
|
+
isSelfNode?: boolean;
|
|
338
|
+
}): Record<string, unknown> {
|
|
339
|
+
const machineId = readMeshNodeMachineId(node);
|
|
340
|
+
const daemonId = readMeshNodeDaemonId(node);
|
|
341
|
+
const hostname = readMeshNodeHostname(node);
|
|
342
|
+
const machineName = readMeshNodeDisplayMachineName(node);
|
|
343
|
+
const coordinatorHostname = readStringValue(opts.coordinatorHostname);
|
|
344
|
+
const machineIdMatches = Boolean(opts.localMachineId && machineId && opts.localMachineId === machineId);
|
|
345
|
+
const daemonIdMatches = Boolean(opts.localDaemonId && daemonId && opts.localDaemonId === daemonId);
|
|
346
|
+
const hostnameMatches = Boolean(
|
|
347
|
+
normalizeMeshHostname(hostname)
|
|
348
|
+
&& normalizeMeshHostname(coordinatorHostname)
|
|
349
|
+
&& normalizeMeshHostname(hostname) === normalizeMeshHostname(coordinatorHostname),
|
|
350
|
+
);
|
|
351
|
+
const sameMachine = opts.isSelfNode === true || machineIdMatches || daemonIdMatches || hostnameMatches;
|
|
352
|
+
const evidence: string[] = [];
|
|
353
|
+
for (const [label, value] of [['machineName', machineName], ['hostname', hostname], ['machineId', machineId], ['daemonId', daemonId]] as const) {
|
|
354
|
+
const compact = compactMeshIdentityEvidence(value);
|
|
355
|
+
if (compact) evidence.push(`${label}:${compact}`);
|
|
174
356
|
}
|
|
357
|
+
const locality = sameMachine ? 'same_machine' : (evidence.length > 0 ? 'remote_known' : 'remote_or_unknown');
|
|
358
|
+
const localityReason = sameMachine
|
|
359
|
+
? (machineIdMatches ? 'matched coordinator machine id'
|
|
360
|
+
: daemonIdMatches ? 'matched coordinator daemon id'
|
|
361
|
+
: hostnameMatches ? 'matched coordinator hostname'
|
|
362
|
+
: 'selected coordinator node')
|
|
363
|
+
: evidence.length > 0
|
|
364
|
+
? `known remote/other machine identity; no local coordinator match (${evidence.join(', ')})`
|
|
365
|
+
: 'no useful machine identity evidence available';
|
|
366
|
+
return {
|
|
367
|
+
daemonId,
|
|
368
|
+
machineId,
|
|
369
|
+
hostname,
|
|
370
|
+
machineName,
|
|
371
|
+
displayName: machineName || hostname || daemonId || machineId,
|
|
372
|
+
coordinatorHostname,
|
|
373
|
+
sameMachine,
|
|
374
|
+
locality,
|
|
375
|
+
localityReason,
|
|
376
|
+
identityEvidence: evidence,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
175
379
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const probeGit = readObjectRecord(rawProbe.git);
|
|
182
|
-
const probeGitResult = readObjectRecord(probeGit.result);
|
|
183
|
-
const probeDirectStatus = readObjectRecord(probeGit.status);
|
|
184
|
-
const probeNestedStatus = readObjectRecord(probeGitResult.status);
|
|
185
|
-
const status = Object.keys(directStatus).length
|
|
186
|
-
? directStatus
|
|
187
|
-
: Object.keys(nestedStatus).length
|
|
188
|
-
? nestedStatus
|
|
189
|
-
: Object.keys(probeDirectStatus).length
|
|
190
|
-
? probeDirectStatus
|
|
191
|
-
: Object.keys(probeNestedStatus).length
|
|
192
|
-
? probeNestedStatus
|
|
193
|
-
: {};
|
|
380
|
+
function normalizeInlineMeshGitStatus(
|
|
381
|
+
status: Record<string, unknown>,
|
|
382
|
+
node: any,
|
|
383
|
+
options?: { lastCheckedAt?: number },
|
|
384
|
+
): Record<string, unknown> | undefined {
|
|
194
385
|
const isGitRepo = readBooleanValue(status.isGitRepo);
|
|
195
386
|
if (!Object.keys(status).length || isGitRepo === undefined) return undefined;
|
|
196
387
|
const conflictFiles = Array.isArray(status.conflictFiles)
|
|
@@ -198,15 +389,20 @@ function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | un
|
|
|
198
389
|
: [];
|
|
199
390
|
const conflictCount = readNumberValue(status.conflicts) ?? conflictFiles.length;
|
|
200
391
|
const hasConflicts = readBooleanValue(status.hasConflicts) ?? conflictCount > 0;
|
|
201
|
-
const
|
|
392
|
+
const repoRoot = readStringValue(status.repoRoot, status.repo_root, node?.repoRoot, node?.repo_root, status.workspace, node?.workspace) || undefined;
|
|
393
|
+
const submodules = readGitSubmodules(status.submodules, repoRoot);
|
|
202
394
|
return {
|
|
203
395
|
workspace: readStringValue(status.workspace, node?.workspace) || '',
|
|
204
|
-
repoRoot:
|
|
396
|
+
repoRoot: repoRoot ?? null,
|
|
205
397
|
isGitRepo,
|
|
206
398
|
branch: readStringValue(status.branch) ?? null,
|
|
207
399
|
headCommit: readStringValue(status.headCommit) ?? null,
|
|
208
400
|
headMessage: readStringValue(status.headMessage) ?? null,
|
|
209
401
|
upstream: readStringValue(status.upstream) ?? null,
|
|
402
|
+
upstreamStatus: readStringValue(status.upstreamStatus, status.upstream_status)
|
|
403
|
+
?? (readStringValue(status.upstream) ? 'unchecked' : 'no_upstream'),
|
|
404
|
+
upstreamFetchedAt: readNumberValue(status.upstreamFetchedAt, status.upstream_fetched_at),
|
|
405
|
+
upstreamFetchError: readStringValue(status.upstreamFetchError, status.upstream_fetch_error),
|
|
210
406
|
ahead: readNumberValue(status.ahead) ?? 0,
|
|
211
407
|
behind: readNumberValue(status.behind) ?? 0,
|
|
212
408
|
staged: readNumberValue(status.staged) ?? 0,
|
|
@@ -217,14 +413,247 @@ function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | un
|
|
|
217
413
|
hasConflicts,
|
|
218
414
|
conflictFiles,
|
|
219
415
|
stashCount: readNumberValue(status.stashCount) ?? 0,
|
|
220
|
-
lastCheckedAt: Date.now(),
|
|
416
|
+
lastCheckedAt: options?.lastCheckedAt ?? readNumberValue(status.lastCheckedAt) ?? Date.now(),
|
|
221
417
|
...(submodules ? { submodules } : {}),
|
|
222
418
|
};
|
|
223
419
|
}
|
|
224
420
|
|
|
421
|
+
function scoreInlineMeshGitStatus(git: Record<string, unknown> | undefined): number {
|
|
422
|
+
if (!git) return Number.NEGATIVE_INFINITY;
|
|
423
|
+
let score = 0;
|
|
424
|
+
if (readBooleanValue(git.isGitRepo) === true) score += 50;
|
|
425
|
+
if (readBooleanValue(git.isGitRepo) === false) score -= 10;
|
|
426
|
+
if (readStringValue(git.branch)) score += 20;
|
|
427
|
+
if (readStringValue(git.headCommit)) score += 20;
|
|
428
|
+
if (readStringValue(git.upstream)) score += 10;
|
|
429
|
+
if (readStringValue(git.upstreamStatus)) score += 5;
|
|
430
|
+
if (readNumberValue(git.ahead) !== undefined) score += 2;
|
|
431
|
+
if (readNumberValue(git.behind) !== undefined) score += 2;
|
|
432
|
+
if (Array.isArray(git.submodules) && git.submodules.length > 0) score += 4 + git.submodules.length;
|
|
433
|
+
if (readStringValue(git.error)) score -= 20;
|
|
434
|
+
return score;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function buildInlineMeshTransitGitStatus(node: any): Record<string, unknown> | undefined {
|
|
438
|
+
const rawGit = readObjectRecord(node?.lastGit ?? node?.last_git);
|
|
439
|
+
const gitResult = readObjectRecord(rawGit.result);
|
|
440
|
+
const directStatus = readObjectRecord(rawGit.status);
|
|
441
|
+
const nestedStatus = readObjectRecord(gitResult.status);
|
|
442
|
+
const rawProbe = readObjectRecord(node?.lastProbe ?? node?.last_probe);
|
|
443
|
+
const probeGit = readObjectRecord(rawProbe.git);
|
|
444
|
+
const probeGitResult = readObjectRecord(probeGit.result);
|
|
445
|
+
const probeDirectStatus = readObjectRecord(probeGit.status);
|
|
446
|
+
const probeNestedStatus = readObjectRecord(probeGitResult.status);
|
|
447
|
+
const candidates = [directStatus, nestedStatus, probeDirectStatus, probeNestedStatus];
|
|
448
|
+
let best: { git: Record<string, unknown>; score: number } | null = null;
|
|
449
|
+
for (const status of candidates) {
|
|
450
|
+
const normalized = normalizeInlineMeshGitStatus(status, node, { lastCheckedAt: Date.now() });
|
|
451
|
+
if (!normalized) continue;
|
|
452
|
+
const score = scoreInlineMeshGitStatus(normalized);
|
|
453
|
+
if (!best || score > best.score) best = { git: normalized, score };
|
|
454
|
+
}
|
|
455
|
+
return best?.git;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function shouldRefreshStalePendingAggregate(snapshot: any, options?: { requireDirectPeerTruth?: boolean }): boolean {
|
|
459
|
+
if (options?.requireDirectPeerTruth !== true || !Array.isArray(snapshot?.nodes)) return false;
|
|
460
|
+
return snapshot.nodes.some((node: any) => {
|
|
461
|
+
if (node?.gitProbePending !== true) return false;
|
|
462
|
+
const git = readObjectRecord(node?.git);
|
|
463
|
+
return !readBooleanValue(git.isGitRepo) && !readStringValue(git.branch, git.headCommit, git.upstream);
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function buildLivePeerGitConnection(connection: Record<string, unknown>, timestamp = new Date().toISOString()): Record<string, unknown> {
|
|
468
|
+
const source = readStringValue(connection.source);
|
|
469
|
+
const transport = readStringValue(connection.transport);
|
|
470
|
+
return {
|
|
471
|
+
...connection,
|
|
472
|
+
perspective: readStringValue(connection.perspective) ?? 'selected_coordinator',
|
|
473
|
+
source: source && source !== 'not_reported' ? source : 'mesh_peer_status',
|
|
474
|
+
state: 'connected',
|
|
475
|
+
transport: transport && transport !== 'unknown' ? transport : 'direct',
|
|
476
|
+
reported: true,
|
|
477
|
+
reason: 'Live peer git snapshot reported by the selected coordinator.',
|
|
478
|
+
lastStateChangeAt: readStringValue(connection.lastStateChangeAt) ?? timestamp,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function recordInlineMeshDirectGitTruth(
|
|
483
|
+
node: any,
|
|
484
|
+
git: Record<string, unknown>,
|
|
485
|
+
source: 'selected_coordinator_local_git' | 'selected_coordinator_mesh_p2p_git',
|
|
486
|
+
): void {
|
|
487
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return;
|
|
488
|
+
const checkedAt = readNumberValue(git.lastCheckedAt) ?? Date.now();
|
|
489
|
+
const updatedAt = new Date(checkedAt).toISOString();
|
|
490
|
+
const nextGit: Record<string, unknown> = {
|
|
491
|
+
...git,
|
|
492
|
+
lastCheckedAt: checkedAt,
|
|
493
|
+
};
|
|
494
|
+
node.lastGit = {
|
|
495
|
+
source,
|
|
496
|
+
checkedAt,
|
|
497
|
+
status: nextGit,
|
|
498
|
+
};
|
|
499
|
+
node.last_git = node.lastGit;
|
|
500
|
+
node.machineStatus = 'online';
|
|
501
|
+
node.updatedAt = updatedAt;
|
|
502
|
+
node.lastSeenAt = updatedAt;
|
|
503
|
+
const repoRoot = readStringValue(nextGit.repoRoot);
|
|
504
|
+
if (repoRoot && !readStringValue(node.repoRoot)) node.repoRoot = repoRoot;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | undefined {
|
|
508
|
+
const liveGit = buildInlineMeshTransitGitStatus(node);
|
|
509
|
+
if (liveGit) return liveGit;
|
|
510
|
+
|
|
511
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
512
|
+
const cachedGit = readObjectRecord(cachedStatus.git);
|
|
513
|
+
if (!Object.keys(cachedGit).length) return undefined;
|
|
514
|
+
return normalizeInlineMeshGitStatus(cachedGit, node);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function shouldDiscardCachedInlineMeshStatus(node: any): boolean {
|
|
518
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
519
|
+
if (!Object.keys(cachedStatus).length) return false;
|
|
520
|
+
const cachedGit = readObjectRecord(cachedStatus.git);
|
|
521
|
+
const workspaceError = readStringValue(cachedStatus.error, node?.error);
|
|
522
|
+
if (workspaceError && /workspace must be an existing directory/i.test(workspaceError)) return true;
|
|
523
|
+
const isGitRepo = readBooleanValue(cachedGit.isGitRepo);
|
|
524
|
+
const branch = readStringValue(cachedGit.branch);
|
|
525
|
+
const headCommit = readStringValue(cachedGit.headCommit);
|
|
526
|
+
return isGitRepo === false && !branch && !headCommit;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function stripInlineMeshTransientNodeState(node: any): any {
|
|
530
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return node;
|
|
531
|
+
const {
|
|
532
|
+
cachedStatus,
|
|
533
|
+
lastGit: _lastGit,
|
|
534
|
+
last_git: _lastGitLegacy,
|
|
535
|
+
lastProbe: _lastProbe,
|
|
536
|
+
last_probe: _lastProbeLegacy,
|
|
537
|
+
error: _error,
|
|
538
|
+
health: _health,
|
|
539
|
+
machineStatus: _machineStatus,
|
|
540
|
+
lastSeenAt: _lastSeenAt,
|
|
541
|
+
last_seen_at: _lastSeenAtLegacy,
|
|
542
|
+
updatedAt: _updatedAt,
|
|
543
|
+
updated_at: _updatedAtLegacy,
|
|
544
|
+
activeSession: _activeSession,
|
|
545
|
+
active_session: _activeSessionLegacy,
|
|
546
|
+
activeSessionId: _activeSessionId,
|
|
547
|
+
active_session_id: _activeSessionIdLegacy,
|
|
548
|
+
sessionId: _sessionId,
|
|
549
|
+
session_id: _sessionIdLegacy,
|
|
550
|
+
providerType: _providerType,
|
|
551
|
+
provider_type: _providerTypeLegacy,
|
|
552
|
+
...rest
|
|
553
|
+
} = node as Record<string, unknown>;
|
|
554
|
+
if (cachedStatus && !shouldDiscardCachedInlineMeshStatus(node)) {
|
|
555
|
+
return { ...rest, cachedStatus };
|
|
556
|
+
}
|
|
557
|
+
return rest;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function hasInlineMeshTransientNodeState(node: any): boolean {
|
|
561
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return false;
|
|
562
|
+
return 'cachedStatus' in node
|
|
563
|
+
|| 'lastGit' in node
|
|
564
|
+
|| 'last_git' in node
|
|
565
|
+
|| 'lastProbe' in node
|
|
566
|
+
|| 'last_probe' in node
|
|
567
|
+
|| 'error' in node
|
|
568
|
+
|| 'health' in node
|
|
569
|
+
|| 'machineStatus' in node
|
|
570
|
+
|| 'lastSeenAt' in node
|
|
571
|
+
|| 'last_seen_at' in node
|
|
572
|
+
|| 'updatedAt' in node
|
|
573
|
+
|| 'updated_at' in node
|
|
574
|
+
|| 'activeSession' in node
|
|
575
|
+
|| 'active_session' in node
|
|
576
|
+
|| 'activeSessionId' in node
|
|
577
|
+
|| 'active_session_id' in node
|
|
578
|
+
|| 'sessionId' in node
|
|
579
|
+
|| 'session_id' in node
|
|
580
|
+
|| 'providerType' in node
|
|
581
|
+
|| 'provider_type' in node;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function inlineMeshCarriesTransientNodeTruth(inlineMesh: any): boolean {
|
|
585
|
+
if (!inlineMesh || typeof inlineMesh !== 'object' || Array.isArray(inlineMesh)) return false;
|
|
586
|
+
if (!Array.isArray(inlineMesh.nodes) || inlineMesh.nodes.length === 0) return false;
|
|
587
|
+
return inlineMesh.nodes.some((node: any) => hasInlineMeshTransientNodeState(node));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function readInlineMeshNodeId(node: any): string {
|
|
591
|
+
return readStringValue(node?.id, node?.nodeId) || '';
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function sanitizeInlineMesh(inlineMesh: any): any {
|
|
595
|
+
if (!inlineMesh || typeof inlineMesh !== 'object' || Array.isArray(inlineMesh)) return inlineMesh;
|
|
596
|
+
if (!Array.isArray(inlineMesh.nodes)) return inlineMesh;
|
|
597
|
+
let changed = false;
|
|
598
|
+
const nodes = inlineMesh.nodes.map((node: any) => {
|
|
599
|
+
if (!hasInlineMeshTransientNodeState(node)) return node;
|
|
600
|
+
changed = true;
|
|
601
|
+
return stripInlineMeshTransientNodeState(node);
|
|
602
|
+
});
|
|
603
|
+
if (!changed) return inlineMesh;
|
|
604
|
+
return {
|
|
605
|
+
...inlineMesh,
|
|
606
|
+
nodes,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function reconcileInlineMeshCache(cached: any, incoming: any): any {
|
|
611
|
+
if (!cached || typeof cached !== 'object' || Array.isArray(cached)) return incoming;
|
|
612
|
+
if (!incoming || typeof incoming !== 'object' || Array.isArray(incoming)) return cached;
|
|
613
|
+
const cachedNodes = Array.isArray(cached.nodes) ? cached.nodes : [];
|
|
614
|
+
const incomingNodes = Array.isArray(incoming.nodes) ? incoming.nodes : [];
|
|
615
|
+
if (!cachedNodes.length || !incomingNodes.length) return { ...cached, ...incoming };
|
|
616
|
+
|
|
617
|
+
const cachedUpdatedAt = Date.parse(readStringValue(cached.updatedAt, cached.updated_at) || '');
|
|
618
|
+
const incomingUpdatedAt = Date.parse(readStringValue(incoming.updatedAt, incoming.updated_at) || '');
|
|
619
|
+
const preserveCachedMembership = Number.isFinite(cachedUpdatedAt)
|
|
620
|
+
&& (!Number.isFinite(incomingUpdatedAt) || cachedUpdatedAt > incomingUpdatedAt);
|
|
621
|
+
|
|
622
|
+
const cachedById = new Map<string, any>();
|
|
623
|
+
for (const node of cachedNodes) {
|
|
624
|
+
const nodeId = readInlineMeshNodeId(node);
|
|
625
|
+
if (nodeId) cachedById.set(nodeId, node);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const nodes = incomingNodes.map((incomingNode: any) => {
|
|
629
|
+
const nodeId = readInlineMeshNodeId(incomingNode);
|
|
630
|
+
const cachedNode = nodeId ? cachedById.get(nodeId) : undefined;
|
|
631
|
+
if (!cachedNode && preserveCachedMembership) return null;
|
|
632
|
+
if (!cachedNode) return incomingNode;
|
|
633
|
+
if (hasInlineMeshTransientNodeState(incomingNode)) {
|
|
634
|
+
return { ...cachedNode, ...incomingNode };
|
|
635
|
+
}
|
|
636
|
+
return { ...stripInlineMeshTransientNodeState(cachedNode), ...incomingNode };
|
|
637
|
+
}).filter(Boolean);
|
|
638
|
+
|
|
639
|
+
return {
|
|
640
|
+
...cached,
|
|
641
|
+
...incoming,
|
|
642
|
+
nodes,
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
225
646
|
function hasGitWorktreeChanges(git: Record<string, unknown> | null | undefined): boolean {
|
|
226
|
-
|
|
227
|
-
|
|
647
|
+
return countGitWorktreeChanges(git) > 0;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function countGitWorktreeChanges(git: Record<string, unknown> | null | undefined): number {
|
|
651
|
+
if (!git) return 0;
|
|
652
|
+
return Number(git.staged || 0)
|
|
653
|
+
+ Number(git.modified || 0)
|
|
654
|
+
+ Number(git.untracked || 0)
|
|
655
|
+
+ Number(git.deleted || 0)
|
|
656
|
+
+ Number(git.renamed || 0);
|
|
228
657
|
}
|
|
229
658
|
|
|
230
659
|
function getGitSubmoduleDriftState(git: Record<string, unknown> | null | undefined): { dirty: boolean; outOfSync: boolean } {
|
|
@@ -249,6 +678,167 @@ function deriveMeshNodeHealthFromGit(git: Record<string, unknown> | null | undef
|
|
|
249
678
|
return 'online';
|
|
250
679
|
}
|
|
251
680
|
|
|
681
|
+
function readMeshNodeLabel(status: Record<string, unknown>, node: any): string {
|
|
682
|
+
return readStringValue(status.nodeId, node?.id, node?.nodeId) ?? 'unknown';
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function buildInlineMeshBranchConvergence(args: {
|
|
686
|
+
mesh: any;
|
|
687
|
+
node: any;
|
|
688
|
+
status: Record<string, unknown>;
|
|
689
|
+
}): Record<string, unknown> {
|
|
690
|
+
const git = readObjectRecord(args.status.git);
|
|
691
|
+
const nodeLabel = readMeshNodeLabel(args.status, args.node);
|
|
692
|
+
const defaultBranch = readStringValue(args.mesh?.defaultBranch) ?? 'main';
|
|
693
|
+
const branch = readStringValue(git.branch, args.node?.worktreeBranch) ?? null;
|
|
694
|
+
const upstream = readStringValue(git.upstream) ?? null;
|
|
695
|
+
const upstreamStatus = readStringValue(git.upstreamStatus, git.upstream_status)
|
|
696
|
+
?? (upstream ? 'unchecked' : 'no_upstream');
|
|
697
|
+
const ahead = readNumberValue(git.ahead) ?? 0;
|
|
698
|
+
const behind = readNumberValue(git.behind) ?? 0;
|
|
699
|
+
const uncommittedChanges = countGitWorktreeChanges(git);
|
|
700
|
+
const hasConflicts = readBooleanValue(git.hasConflicts)
|
|
701
|
+
?? (Array.isArray(git.conflictFiles) && git.conflictFiles.length > 0);
|
|
702
|
+
const base = {
|
|
703
|
+
defaultBranch,
|
|
704
|
+
branch,
|
|
705
|
+
upstream,
|
|
706
|
+
upstreamStatus,
|
|
707
|
+
ahead,
|
|
708
|
+
behind,
|
|
709
|
+
isWorktree: args.node?.isLocalWorktree === true || args.status.isLocalWorktree === true,
|
|
710
|
+
isDefaultBranch: branch === defaultBranch,
|
|
711
|
+
};
|
|
712
|
+
|
|
713
|
+
if (readBooleanValue(git.isGitRepo) !== true) {
|
|
714
|
+
return {
|
|
715
|
+
...base,
|
|
716
|
+
status: 'blocked_review',
|
|
717
|
+
needsConvergence: true,
|
|
718
|
+
reason: 'git_status_unavailable',
|
|
719
|
+
nextStep: `Resolve git status for node '${nodeLabel}' before marking the task complete.`,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (!branch) {
|
|
724
|
+
return {
|
|
725
|
+
...base,
|
|
726
|
+
status: 'blocked_review',
|
|
727
|
+
needsConvergence: true,
|
|
728
|
+
reason: 'branch_unknown',
|
|
729
|
+
nextStep: `Inspect node '${nodeLabel}' git branch before deciding whether it is merged to ${defaultBranch}.`,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (hasConflicts || uncommittedChanges > 0) {
|
|
734
|
+
return {
|
|
735
|
+
...base,
|
|
736
|
+
status: 'not_mergeable',
|
|
737
|
+
needsConvergence: true,
|
|
738
|
+
reason: hasConflicts ? 'conflicts_present' : 'dirty_workspace',
|
|
739
|
+
nextStep: `Commit, checkpoint, or resolve node '${nodeLabel}' before any main convergence step.`,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (branch === defaultBranch) {
|
|
744
|
+
if (upstream && upstreamStatus !== 'fresh') {
|
|
745
|
+
return {
|
|
746
|
+
...base,
|
|
747
|
+
status: 'blocked_review',
|
|
748
|
+
needsConvergence: true,
|
|
749
|
+
reason: 'default_branch_upstream_unverified',
|
|
750
|
+
nextStep: `Refresh ${defaultBranch}'s upstream refs or resolve the fetch failure before declaring convergence complete for node '${nodeLabel}'.`,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
if (ahead > 0 || behind > 0) {
|
|
754
|
+
return {
|
|
755
|
+
...base,
|
|
756
|
+
status: 'blocked_review',
|
|
757
|
+
needsConvergence: true,
|
|
758
|
+
reason: 'default_branch_not_even_with_upstream',
|
|
759
|
+
nextStep: `Bring ${defaultBranch} even with its upstream before declaring convergence complete.`,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
return {
|
|
763
|
+
...base,
|
|
764
|
+
status: 'merged_to_main',
|
|
765
|
+
needsConvergence: false,
|
|
766
|
+
reason: 'clean_default_branch',
|
|
767
|
+
nextStep: null,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
if (args.node?.isLocalWorktree === true || args.status.isLocalWorktree === true) {
|
|
772
|
+
return {
|
|
773
|
+
...base,
|
|
774
|
+
status: 'cleanup_candidate',
|
|
775
|
+
needsConvergence: true,
|
|
776
|
+
reason: 'clean_non_default_worktree_branch',
|
|
777
|
+
nextStep: `Run mesh_refine_node(node_id: "${nodeLabel}") or explicitly classify this worktree as blocked_review/not_mergeable before ending the task.`,
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if (upstream && upstreamStatus !== 'fresh') {
|
|
782
|
+
return {
|
|
783
|
+
...base,
|
|
784
|
+
status: 'blocked_review',
|
|
785
|
+
needsConvergence: true,
|
|
786
|
+
reason: 'feature_branch_upstream_unverified',
|
|
787
|
+
nextStep: `Refresh branch '${branch}' upstream refs or resolve the fetch failure before deciding whether it is ready to merge into ${defaultBranch}.`,
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (!upstream || ahead > 0 || behind > 0) {
|
|
792
|
+
return {
|
|
793
|
+
...base,
|
|
794
|
+
status: 'blocked_review',
|
|
795
|
+
needsConvergence: true,
|
|
796
|
+
reason: !upstream ? 'feature_branch_missing_upstream' : 'feature_branch_not_even_with_upstream',
|
|
797
|
+
nextStep: `Push or reconcile branch '${branch}', then merge it into ${defaultBranch} or mark it not_mergeable with a reason.`,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return {
|
|
802
|
+
...base,
|
|
803
|
+
status: 'pushed_feature_branch_needs_merge',
|
|
804
|
+
needsConvergence: true,
|
|
805
|
+
reason: 'clean_non_default_branch',
|
|
806
|
+
nextStep: `Review and merge branch '${branch}' into ${defaultBranch}; do not report the task as fully complete while it remains off main.`,
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function applyInlineMeshBranchConvergence(mesh: any, node: any, status: Record<string, unknown>): void {
|
|
811
|
+
const git = readObjectRecord(status.git);
|
|
812
|
+
if (Object.keys(git).length === 0 && !status.gitProbePending) return;
|
|
813
|
+
const uncommittedChanges = countGitWorktreeChanges(git);
|
|
814
|
+
status.isDirty = uncommittedChanges > 0;
|
|
815
|
+
status.uncommittedChanges = uncommittedChanges;
|
|
816
|
+
status.branchConvergence = buildInlineMeshBranchConvergence({ mesh, node, status });
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function summarizeInlineMeshBranchConvergence(nodes: Array<Record<string, unknown>>): Record<string, unknown> {
|
|
820
|
+
const followUps = nodes
|
|
821
|
+
.filter(node => readObjectRecord(node.branchConvergence).needsConvergence === true)
|
|
822
|
+
.map(node => {
|
|
823
|
+
const convergence = readObjectRecord(node.branchConvergence);
|
|
824
|
+
return {
|
|
825
|
+
nodeId: node.nodeId,
|
|
826
|
+
workspace: node.workspace,
|
|
827
|
+
branch: convergence.branch,
|
|
828
|
+
status: convergence.status,
|
|
829
|
+
reason: convergence.reason,
|
|
830
|
+
nextStep: convergence.nextStep,
|
|
831
|
+
};
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
return {
|
|
835
|
+
needsFollowUp: followUps.length > 0,
|
|
836
|
+
unresolvedCount: followUps.length,
|
|
837
|
+
requiredFinalStates: ['merged_to_main', 'pushed_feature_branch_needs_merge', 'blocked_review', 'cleanup_candidate', 'not_mergeable'],
|
|
838
|
+
followUps,
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
|
|
252
842
|
function readCachedInlineMeshActiveSessions(node: any): string[] {
|
|
253
843
|
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
254
844
|
const activeSession = readObjectRecord(cachedStatus.activeSession);
|
|
@@ -259,7 +849,7 @@ function readCachedInlineMeshActiveSessions(node: any): string[] {
|
|
|
259
849
|
return sessionId ? [sessionId] : [];
|
|
260
850
|
}
|
|
261
851
|
|
|
262
|
-
function readCachedInlineMeshActiveSessionDetails(node: any): Array<Record<string, unknown>> {
|
|
852
|
+
export function readCachedInlineMeshActiveSessionDetails(node: any): Array<Record<string, unknown>> {
|
|
263
853
|
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
264
854
|
const activeSession = readObjectRecord(cachedStatus.activeSession);
|
|
265
855
|
const fallbackSession = Object.keys(activeSession).length
|
|
@@ -287,9 +877,14 @@ function readCachedInlineMeshActiveSessionDetails(node: any): Array<Record<strin
|
|
|
287
877
|
node?.provider_type,
|
|
288
878
|
),
|
|
289
879
|
state: readStringValue(fallbackSession.status, fallbackSession.state, fallbackSession.lifecycle),
|
|
880
|
+
chatStatus: readStringValue(fallbackSession.chatStatus, fallbackSession.chat_status),
|
|
290
881
|
lifecycle: readStringValue(fallbackSession.lifecycle),
|
|
291
882
|
title: readStringValue(fallbackSession.title, fallbackSession.displayName, fallbackSession.display_name) ?? null,
|
|
292
883
|
workspace: readStringValue(fallbackSession.workspace, node?.workspace) ?? null,
|
|
884
|
+
role: readStringValue(fallbackSession.role) ?? null,
|
|
885
|
+
isSelfCoordinator: fallbackSession.isSelfCoordinator === true || fallbackSession.is_self_coordinator === true,
|
|
886
|
+
createdAt: readStringValue(fallbackSession.createdAt, fallbackSession.created_at) ?? null,
|
|
887
|
+
startedAt: readStringValue(fallbackSession.startedAt, fallbackSession.started_at) ?? null,
|
|
293
888
|
lastActivityAt: readStringValue(fallbackSession.lastActivityAt, fallbackSession.last_activity_at) ?? null,
|
|
294
889
|
recoveryState: readStringValue(fallbackSession.recoveryState, fallbackSession.recovery_state) ?? null,
|
|
295
890
|
isCached: true,
|
|
@@ -313,21 +908,217 @@ function toIsoTimestamp(value: unknown): string | null {
|
|
|
313
908
|
return stringValue || null;
|
|
314
909
|
}
|
|
315
910
|
|
|
911
|
+
function synthesizeMeshNodeFreshnessFromConnection(status: Record<string, unknown>): void {
|
|
912
|
+
const connection = readObjectRecord(status.connection);
|
|
913
|
+
const connectionFreshAt = toIsoTimestamp(connection.lastCommandAt ?? connection.lastConnectedAt ?? connection.lastStateChangeAt);
|
|
914
|
+
const git = readObjectRecord(status.git);
|
|
915
|
+
const gitCheckedAt = toIsoTimestamp(git.lastCheckedAt);
|
|
916
|
+
if (!status.lastSeenAt && connectionFreshAt) status.lastSeenAt = connectionFreshAt;
|
|
917
|
+
if (!status.updatedAt && (gitCheckedAt || connectionFreshAt)) {
|
|
918
|
+
status.updatedAt = gitCheckedAt ?? connectionFreshAt;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function finalizeMeshNodeStatus(args: {
|
|
923
|
+
status: Record<string, unknown>;
|
|
924
|
+
node: any;
|
|
925
|
+
daemonId?: string;
|
|
926
|
+
isSelfNode: boolean;
|
|
927
|
+
}): void {
|
|
928
|
+
const { status, node, daemonId, isSelfNode } = args;
|
|
929
|
+
if (!readStringValue(status.machineStatus)) {
|
|
930
|
+
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
931
|
+
const machineStatus = readStringValue(cachedStatus.machineStatus, cachedStatus.machine_status, node?.machineStatus);
|
|
932
|
+
if (machineStatus) status.machineStatus = machineStatus;
|
|
933
|
+
}
|
|
934
|
+
synthesizeMeshNodeFreshnessFromConnection(status);
|
|
935
|
+
const bootstrap = readObjectRecord(node?.worktreeBootstrap);
|
|
936
|
+
if (node?.isLocalWorktree && readStringValue(bootstrap.status)) {
|
|
937
|
+
status.worktreeBootstrap = bootstrap;
|
|
938
|
+
if (bootstrap.status === 'failed' && bootstrap.required !== false) {
|
|
939
|
+
status.launchReady = false;
|
|
940
|
+
status.launchBlockedReason = 'worktree_bootstrap_failed';
|
|
941
|
+
status.launchBlockedMessage = readStringValue(bootstrap.error)
|
|
942
|
+
|| 'Required worktree bootstrap failed; resolve it before launching an agent into this node.';
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
if (bootstrap.status === 'running' && bootstrap.required !== false) {
|
|
946
|
+
status.launchReady = false;
|
|
947
|
+
status.launchBlockedReason = 'worktree_bootstrap_running';
|
|
948
|
+
status.launchBlockedMessage = 'Required worktree bootstrap is still running; wait for it to finish before launching an agent into this node.';
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
const connectionState = readStringValue(readObjectRecord(status.connection).state);
|
|
953
|
+
status.launchReady = !!daemonId && (
|
|
954
|
+
readStringValue(status.machineStatus) === 'online'
|
|
955
|
+
|| connectionState === 'connected'
|
|
956
|
+
|| isSelfNode
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
async function probeRemoteMeshGitStatus(args: {
|
|
961
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
962
|
+
daemonId: string;
|
|
963
|
+
workspace: string;
|
|
964
|
+
timeoutMs: number;
|
|
965
|
+
}): Promise<Record<string, unknown> | null> {
|
|
966
|
+
if (!args.dispatchMeshCommand) return null;
|
|
967
|
+
const remoteResult = await Promise.race([
|
|
968
|
+
args.dispatchMeshCommand(args.daemonId, 'git_status', { workspace: args.workspace, refreshUpstream: true }),
|
|
969
|
+
new Promise<never>((_, reject) => setTimeout(() => reject(new Error('timeout')), args.timeoutMs)),
|
|
970
|
+
]) as any;
|
|
971
|
+
const remoteGit = remoteResult?.status ?? remoteResult?.git ?? remoteResult;
|
|
972
|
+
return remoteGit && typeof remoteGit === 'object' && typeof remoteGit.isGitRepo === 'boolean'
|
|
973
|
+
? remoteGit as Record<string, unknown>
|
|
974
|
+
: null;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
async function hydrateInlineMeshDirectTruth(args: {
|
|
978
|
+
mesh: any;
|
|
979
|
+
meshSource: 'inline_cache' | 'inline_bootstrap' | 'local_config';
|
|
980
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
981
|
+
statusInstanceId?: string;
|
|
982
|
+
localMachineId?: string;
|
|
983
|
+
}): Promise<{
|
|
984
|
+
directEvidenceCount: number;
|
|
985
|
+
localConfirmedCount: number;
|
|
986
|
+
peerAttemptedCount: number;
|
|
987
|
+
peerConfirmedCount: number;
|
|
988
|
+
unavailableNodeIds: string[];
|
|
989
|
+
}> {
|
|
990
|
+
const nodes = Array.isArray(args.mesh?.nodes) ? args.mesh.nodes : [];
|
|
991
|
+
if (!nodes.length) {
|
|
992
|
+
return {
|
|
993
|
+
directEvidenceCount: 0,
|
|
994
|
+
localConfirmedCount: 0,
|
|
995
|
+
peerAttemptedCount: 0,
|
|
996
|
+
peerConfirmedCount: 0,
|
|
997
|
+
unavailableNodeIds: [],
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
const selectedCoordinatorNodeId = readStringValue(
|
|
1002
|
+
args.mesh?.coordinator?.preferredNodeId,
|
|
1003
|
+
nodes[0]?.id,
|
|
1004
|
+
nodes[0]?.nodeId,
|
|
1005
|
+
);
|
|
1006
|
+
|
|
1007
|
+
let localConfirmedCount = 0;
|
|
1008
|
+
let peerAttemptedCount = 0;
|
|
1009
|
+
let peerConfirmedCount = 0;
|
|
1010
|
+
const unavailableNodeIds: string[] = [];
|
|
1011
|
+
|
|
1012
|
+
for (const [nodeIndex, node] of nodes.entries()) {
|
|
1013
|
+
const nodeId = readStringValue(node?.id, node?.nodeId) || `node_${nodeIndex}`;
|
|
1014
|
+
const workspace = readStringValue(node?.workspace);
|
|
1015
|
+
const daemonId = readStringValue(node?.daemonId);
|
|
1016
|
+
const isSelfNode = Boolean(
|
|
1017
|
+
nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId,
|
|
1018
|
+
) || Boolean(
|
|
1019
|
+
daemonId && (daemonId === args.localMachineId || daemonId === args.statusInstanceId),
|
|
1020
|
+
) || Boolean(args.meshSource !== 'local_config' && nodeIndex === 0);
|
|
1021
|
+
|
|
1022
|
+
if (!workspace) {
|
|
1023
|
+
if (!isSelfNode && daemonId) unavailableNodeIds.push(nodeId);
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
if (fs.existsSync(workspace)) {
|
|
1028
|
+
try {
|
|
1029
|
+
const localGit = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
1030
|
+
if (localGit?.isGitRepo) {
|
|
1031
|
+
recordInlineMeshDirectGitTruth(node, localGit as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
1032
|
+
localConfirmedCount += 1;
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
} catch {
|
|
1036
|
+
// Fall through to remote classification.
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
if (!daemonId || !args.dispatchMeshCommand) {
|
|
1041
|
+
if (!isSelfNode) unavailableNodeIds.push(nodeId);
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
peerAttemptedCount += 1;
|
|
1046
|
+
try {
|
|
1047
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
1048
|
+
dispatchMeshCommand: args.dispatchMeshCommand,
|
|
1049
|
+
daemonId,
|
|
1050
|
+
workspace,
|
|
1051
|
+
timeoutMs: 8_000,
|
|
1052
|
+
});
|
|
1053
|
+
if (remoteGit) {
|
|
1054
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
1055
|
+
peerConfirmedCount += 1;
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
} catch {
|
|
1059
|
+
// Strict direct-only path: do not fall back to persisted cloud truth here.
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
unavailableNodeIds.push(nodeId);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
return {
|
|
1066
|
+
directEvidenceCount: localConfirmedCount + peerConfirmedCount,
|
|
1067
|
+
localConfirmedCount,
|
|
1068
|
+
peerAttemptedCount,
|
|
1069
|
+
peerConfirmedCount,
|
|
1070
|
+
unavailableNodeIds,
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
|
|
316
1074
|
function summarizeMeshSessionRecord(record: any): Record<string, unknown> {
|
|
1075
|
+
const meta = readObjectRecord(record?.meta);
|
|
1076
|
+
const isSelfCoordinator = Boolean(readStringValue(meta.meshCoordinatorFor));
|
|
1077
|
+
const chatStatus = readStringValue(record?.chatStatus, record?.activeChat?.status, meta.chatStatus, meta.sessionStatus);
|
|
1078
|
+
const state = readLiveMeshSessionState(record);
|
|
1079
|
+
const statusNote = isSelfCoordinator && (!chatStatus || chatStatus === 'idle' || state === 'idle')
|
|
1080
|
+
? 'Coordinator self status is sampled from the session host and may read idle while the coordinator is generating this response.'
|
|
1081
|
+
: null;
|
|
317
1082
|
return {
|
|
318
1083
|
sessionId: readStringValue(record?.sessionId) || 'unknown',
|
|
319
1084
|
providerType: readStringValue(record?.providerType),
|
|
320
|
-
state
|
|
1085
|
+
state,
|
|
1086
|
+
chatStatus,
|
|
321
1087
|
lifecycle: readStringValue(record?.lifecycle),
|
|
322
1088
|
surfaceKind: getSessionHostSurfaceKind(record as any),
|
|
323
|
-
recoveryState: readStringValue(
|
|
1089
|
+
recoveryState: readStringValue(meta.runtimeRecoveryState) ?? null,
|
|
324
1090
|
workspace: readStringValue(record?.workspace) ?? null,
|
|
325
1091
|
title: readStringValue(record?.displayName, record?.workspaceLabel) ?? null,
|
|
1092
|
+
role: isSelfCoordinator ? 'coordinator' : readStringValue(meta.meshRole, meta.role) ?? null,
|
|
1093
|
+
isSelfCoordinator,
|
|
1094
|
+
statusNote,
|
|
1095
|
+
createdAt: toIsoTimestamp(record?.createdAt ?? record?.created_at),
|
|
1096
|
+
startedAt: toIsoTimestamp(record?.startedAt ?? record?.started_at ?? record?.spawnedAtMs ?? record?.spawned_at_ms),
|
|
326
1097
|
lastActivityAt: toIsoTimestamp(record?.updatedAt ?? record?.lastActivityAt ?? record?.last_activity_at),
|
|
327
1098
|
isCached: false,
|
|
328
1099
|
};
|
|
329
1100
|
}
|
|
330
1101
|
|
|
1102
|
+
function liveSessionRecordMatchesMeshNode(record: any, meshId: string, nodeId: string, nodeWorkspace = '', nodeIsMissingLocalWorktree = false): boolean {
|
|
1103
|
+
const recordNodeId = readStringValue(record?.meta?.meshNodeId);
|
|
1104
|
+
if (!recordNodeId || recordNodeId !== nodeId) return false;
|
|
1105
|
+
if (nodeIsMissingLocalWorktree) return false;
|
|
1106
|
+
const recordWorkspace = readStringValue(record?.workspace);
|
|
1107
|
+
if (nodeWorkspace && recordWorkspace && recordWorkspace !== nodeWorkspace) return false;
|
|
1108
|
+
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
1109
|
+
return !recordMeshId || recordMeshId === meshId;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function liveSessionRecordMatchesMeshWorkspace(record: any, meshId: string, workspace: string): boolean {
|
|
1113
|
+
const recordWorkspace = readStringValue(record?.workspace);
|
|
1114
|
+
if (!recordWorkspace || !workspace || recordWorkspace !== workspace) return false;
|
|
1115
|
+
|
|
1116
|
+
const recordMeshId = readStringValue(record?.meta?.meshNodeFor);
|
|
1117
|
+
if (recordMeshId) return recordMeshId === meshId;
|
|
1118
|
+
|
|
1119
|
+
return record?.meta?.launchedByCoordinator === true || !!readStringValue(record?.meta?.meshNodeId);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
331
1122
|
function readLiveMeshNodeWorkspace(args: {
|
|
332
1123
|
meshId: string;
|
|
333
1124
|
nodeId: string;
|
|
@@ -335,7 +1126,7 @@ function readLiveMeshNodeWorkspace(args: {
|
|
|
335
1126
|
allowCoordinatorSession?: boolean;
|
|
336
1127
|
}): string {
|
|
337
1128
|
const directNodeWorkspace = args.liveSessionRecords.find((record) => (
|
|
338
|
-
|
|
1129
|
+
liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId)
|
|
339
1130
|
&& readStringValue(record?.workspace)
|
|
340
1131
|
));
|
|
341
1132
|
if (directNodeWorkspace) {
|
|
@@ -362,11 +1153,16 @@ function collectLiveMeshSessionRecords(args: {
|
|
|
362
1153
|
liveSessionRecords: any[];
|
|
363
1154
|
allowCoordinatorSession?: boolean;
|
|
364
1155
|
}): any[] {
|
|
1156
|
+
const nodeWorkspace = readStringValue(args.node?.workspace);
|
|
1157
|
+
const nodeIsMissingLocalWorktree = args.node?.isLocalWorktree === true
|
|
1158
|
+
&& !!nodeWorkspace
|
|
1159
|
+
&& !fs.existsSync(nodeWorkspace);
|
|
365
1160
|
const matches = args.liveSessionRecords.filter((record) => {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
1161
|
+
const recordNodeId = readStringValue(record?.meta?.meshNodeId);
|
|
1162
|
+
if (recordNodeId && recordNodeId !== args.nodeId) return false;
|
|
1163
|
+
if (liveSessionRecordMatchesMeshNode(record, args.meshId, args.nodeId, nodeWorkspace || '', nodeIsMissingLocalWorktree)) return true;
|
|
1164
|
+
if (nodeIsMissingLocalWorktree) return false;
|
|
1165
|
+
return !!nodeWorkspace && liveSessionRecordMatchesMeshWorkspace(record, args.meshId, nodeWorkspace);
|
|
370
1166
|
});
|
|
371
1167
|
|
|
372
1168
|
if (args.allowCoordinatorSession) {
|
|
@@ -381,11 +1177,62 @@ function collectLiveMeshSessionRecords(args: {
|
|
|
381
1177
|
return matches;
|
|
382
1178
|
}
|
|
383
1179
|
|
|
384
|
-
function
|
|
1180
|
+
function buildHistoricalMeshSessions(args: {
|
|
1181
|
+
meshId: string;
|
|
1182
|
+
nodes: any[];
|
|
1183
|
+
liveSessionRecords: any[];
|
|
1184
|
+
}): { count: number; sessions: Record<string, unknown>[]; instruction: string } | undefined {
|
|
1185
|
+
const liveNodeIds = new Set<string>();
|
|
1186
|
+
const liveWorkspaces = new Set<string>();
|
|
1187
|
+
const missingLocalWorktreeNodeIds = new Set<string>();
|
|
1188
|
+
for (const node of args.nodes || []) {
|
|
1189
|
+
const nodeId = readStringValue(node?.id, node?.nodeId);
|
|
1190
|
+
const workspace = readStringValue(node?.workspace);
|
|
1191
|
+
if (nodeId) liveNodeIds.add(nodeId);
|
|
1192
|
+
if (workspace) liveWorkspaces.add(workspace);
|
|
1193
|
+
if (nodeId && node?.isLocalWorktree === true && workspace && !fs.existsSync(workspace)) {
|
|
1194
|
+
missingLocalWorktreeNodeIds.add(nodeId);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
const sessions: Record<string, unknown>[] = [];
|
|
1199
|
+
for (const record of args.liveSessionRecords || []) {
|
|
1200
|
+
const meta = readObjectRecord(record?.meta);
|
|
1201
|
+
const recordMeshId = readStringValue(meta.meshNodeFor, meta.meshCoordinatorFor);
|
|
1202
|
+
if (recordMeshId !== args.meshId) continue;
|
|
1203
|
+
const recordNodeId = readStringValue(meta.meshNodeId);
|
|
1204
|
+
const workspace = readStringValue(record?.workspace);
|
|
1205
|
+
const removedNode = !!recordNodeId && (!liveNodeIds.has(recordNodeId) || missingLocalWorktreeNodeIds.has(recordNodeId));
|
|
1206
|
+
const orphanedWorkspace = !!workspace && !liveWorkspaces.has(workspace) && meta.meshCoordinatorFor !== args.meshId;
|
|
1207
|
+
if (!removedNode && !orphanedWorkspace) continue;
|
|
1208
|
+
sessions.push({
|
|
1209
|
+
...summarizeMeshSessionRecord(record),
|
|
1210
|
+
classification: removedNode ? 'removedNode' : 'orphanedSession',
|
|
1211
|
+
historical: true,
|
|
1212
|
+
meshNodeId: recordNodeId || null,
|
|
1213
|
+
reason: removedNode
|
|
1214
|
+
? 'Session is tagged to a mesh node that is no longer in live membership.'
|
|
1215
|
+
: 'Session workspace is no longer attached to a live mesh node.',
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
if (sessions.length === 0) return undefined;
|
|
1219
|
+
return {
|
|
1220
|
+
count: sessions.length,
|
|
1221
|
+
sessions: sessions.slice(0, 5),
|
|
1222
|
+
instruction: 'These sessions are separated from normal node activeSessions because their mesh node/workspace is no longer live. Use mesh_cleanup_sessions only if cleanup is intended.',
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function applyCachedInlineMeshNodeStatus(
|
|
1227
|
+
status: Record<string, unknown>,
|
|
1228
|
+
node: any,
|
|
1229
|
+
options?: { skipGit?: boolean; skipError?: boolean; skipHealth?: boolean },
|
|
1230
|
+
): boolean {
|
|
385
1231
|
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
386
|
-
const
|
|
387
|
-
const
|
|
388
|
-
const
|
|
1232
|
+
const liveGit = buildInlineMeshTransitGitStatus(node);
|
|
1233
|
+
const git = options?.skipGit ? undefined : (liveGit ?? buildCachedInlineMeshGitStatus(node));
|
|
1234
|
+
const error = options?.skipError ? undefined : (liveGit ? undefined : readStringValue(cachedStatus.error, node?.error));
|
|
1235
|
+
const health = options?.skipHealth ? undefined : (liveGit ? undefined : readStringValue(cachedStatus.health, node?.health));
|
|
389
1236
|
const machineStatus = readStringValue(cachedStatus.machineStatus, node?.machineStatus);
|
|
390
1237
|
const lastSeenAt = toIsoTimestamp(cachedStatus.lastSeenAt ?? cachedStatus.last_seen_at ?? node?.lastSeenAt ?? node?.last_seen_at);
|
|
391
1238
|
const updatedAt = toIsoTimestamp(cachedStatus.updatedAt ?? cachedStatus.updated_at ?? node?.updatedAt ?? node?.updated_at);
|
|
@@ -442,29 +1289,140 @@ async function resolveProviderTypeFromPriority(args: {
|
|
|
442
1289
|
}
|
|
443
1290
|
type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
|
|
444
1291
|
type MeshRefineValidationStatus = 'passed' | 'failed' | 'skipped';
|
|
445
|
-
type MeshRefineValidationCommand =
|
|
446
|
-
command: string;
|
|
447
|
-
args: string[];
|
|
448
|
-
displayCommand: string;
|
|
449
|
-
category: string;
|
|
450
|
-
source: string;
|
|
451
|
-
};
|
|
1292
|
+
type MeshRefineValidationCommand = MeshRefineValidationCommandPlan;
|
|
452
1293
|
|
|
453
1294
|
type MeshRefineValidationSummary = {
|
|
454
1295
|
status: MeshRefineValidationStatus;
|
|
455
1296
|
required: true;
|
|
456
1297
|
commandsRun: Array<Record<string, unknown>>;
|
|
1298
|
+
bootstrapCommandsRun: Array<Record<string, unknown>>;
|
|
457
1299
|
rejectedCommands: Array<Record<string, unknown>>;
|
|
458
1300
|
skippedReason?: string;
|
|
1301
|
+
failureKind?: string;
|
|
1302
|
+
failureCode?: string;
|
|
459
1303
|
timeoutMs: number;
|
|
460
1304
|
outputLimitBytes: number;
|
|
1305
|
+
configSource?: string;
|
|
1306
|
+
configSourceType?: string;
|
|
1307
|
+
suggestions?: unknown[];
|
|
1308
|
+
suggestedConfig?: unknown;
|
|
1309
|
+
};
|
|
1310
|
+
|
|
1311
|
+
type MeshRefineStageStatus = 'passed' | 'failed' | 'skipped';
|
|
1312
|
+
|
|
1313
|
+
type MeshRefinePatchEquivalenceSummary = {
|
|
1314
|
+
status: MeshRefineStageStatus;
|
|
1315
|
+
equivalent: boolean;
|
|
1316
|
+
baseHead: string;
|
|
1317
|
+
branchHead: string;
|
|
1318
|
+
mergeBase?: string;
|
|
1319
|
+
mergedTree?: string;
|
|
1320
|
+
expectedPatchId?: string;
|
|
1321
|
+
actualPatchId?: string;
|
|
1322
|
+
durationMs: number;
|
|
1323
|
+
error?: string;
|
|
1324
|
+
stdout?: string;
|
|
1325
|
+
stderr?: string;
|
|
1326
|
+
actionableHint?: MeshRefineSubmoduleConflictHint;
|
|
1327
|
+
};
|
|
1328
|
+
|
|
1329
|
+
type MeshRefineSubmoduleConflictHint = {
|
|
1330
|
+
kind: 'submodule_conflict';
|
|
1331
|
+
message: string;
|
|
1332
|
+
conflicts: Array<{
|
|
1333
|
+
path: string;
|
|
1334
|
+
baseCommit?: string;
|
|
1335
|
+
branchCommit?: string;
|
|
1336
|
+
}>;
|
|
1337
|
+
nextSteps: string[];
|
|
1338
|
+
};
|
|
1339
|
+
|
|
1340
|
+
type MeshRefineSubmoduleAlignmentSummary = {
|
|
1341
|
+
status: 'passed' | 'failed' | 'skipped';
|
|
1342
|
+
changedGitlinkPaths: string[];
|
|
1343
|
+
outOfSyncPaths: string[];
|
|
1344
|
+
updatedPaths: string[];
|
|
1345
|
+
verifiedPaths: string[];
|
|
1346
|
+
durationMs: number;
|
|
1347
|
+
reason?: string;
|
|
1348
|
+
command?: string;
|
|
1349
|
+
error?: string;
|
|
1350
|
+
stdout?: string;
|
|
1351
|
+
stderr?: string;
|
|
1352
|
+
};
|
|
1353
|
+
|
|
1354
|
+
type MeshRefineSubmoduleReachabilityEntry = {
|
|
1355
|
+
path: string;
|
|
1356
|
+
commit: string;
|
|
1357
|
+
reachable: boolean;
|
|
1358
|
+
publishRequired?: boolean;
|
|
1359
|
+
autoPublishAllowed?: boolean;
|
|
1360
|
+
autoPublishAttempted?: boolean;
|
|
1361
|
+
autoPublishSucceeded?: boolean;
|
|
1362
|
+
autoPublishVerified?: boolean;
|
|
1363
|
+
autoPublishRefspec?: string;
|
|
1364
|
+
autoPublishSkippedReason?: string;
|
|
1365
|
+
importedFromWorktree?: boolean;
|
|
1366
|
+
checkedLocal?: boolean;
|
|
1367
|
+
localReachable?: boolean;
|
|
1368
|
+
remote?: string;
|
|
1369
|
+
remoteUrl?: string;
|
|
1370
|
+
remoteReachable?: boolean;
|
|
1371
|
+
remoteMainBranch?: string;
|
|
1372
|
+
remoteMainReachable?: boolean;
|
|
1373
|
+
fetchedFromOrigin?: boolean;
|
|
1374
|
+
error?: string;
|
|
1375
|
+
publishStdout?: string;
|
|
1376
|
+
publishStderr?: string;
|
|
461
1377
|
};
|
|
462
1378
|
|
|
1379
|
+
type MeshRefineSubmoduleReachabilitySummary = {
|
|
1380
|
+
status: MeshRefineStageStatus;
|
|
1381
|
+
checked: number;
|
|
1382
|
+
unreachable: MeshRefineSubmoduleReachabilityEntry[];
|
|
1383
|
+
entries: MeshRefineSubmoduleReachabilityEntry[];
|
|
1384
|
+
durationMs: number;
|
|
1385
|
+
autoPublishAllowed?: boolean;
|
|
1386
|
+
autoPublishPolicySource?: string;
|
|
1387
|
+
error?: string;
|
|
1388
|
+
};
|
|
1389
|
+
|
|
1390
|
+
type MeshRefineAsyncJobStatus = 'accepted' | 'completed' | 'failed';
|
|
1391
|
+
|
|
1392
|
+
type MeshRefineJobHandle = {
|
|
1393
|
+
success: true;
|
|
1394
|
+
async: true;
|
|
1395
|
+
status: MeshRefineAsyncJobStatus;
|
|
1396
|
+
jobId: string;
|
|
1397
|
+
interactionId: string;
|
|
1398
|
+
meshId: string;
|
|
1399
|
+
nodeId: string;
|
|
1400
|
+
targetNodeId: string;
|
|
1401
|
+
targetDaemonId?: string;
|
|
1402
|
+
workspace?: string;
|
|
1403
|
+
startedAt: string;
|
|
1404
|
+
completedAt?: string;
|
|
1405
|
+
duplicate?: boolean;
|
|
1406
|
+
retryOfJobId?: string;
|
|
1407
|
+
eventDelivery: {
|
|
1408
|
+
pendingEvents: true;
|
|
1409
|
+
ledger: true;
|
|
1410
|
+
};
|
|
1411
|
+
evidence: {
|
|
1412
|
+
pendingEventsCommand: 'get_pending_mesh_events';
|
|
1413
|
+
ledgerCommand: 'get_mesh_ledger_slice';
|
|
1414
|
+
taskHistoryKind: 'task_dispatched' | 'task_completed' | 'task_failed';
|
|
1415
|
+
};
|
|
1416
|
+
};
|
|
1417
|
+
|
|
1418
|
+
type MeshRefineTerminalJob = MeshRefineJobHandle & { result?: Record<string, unknown> };
|
|
1419
|
+
|
|
463
1420
|
const REFINE_VALIDATION_CATEGORIES = ['typecheck', 'test', 'lint', 'build'] as const;
|
|
464
1421
|
const REFINE_VALIDATION_TIMEOUT_MS = 120_000;
|
|
465
1422
|
const REFINE_VALIDATION_OUTPUT_LIMIT_BYTES = 128 * 1024;
|
|
466
1423
|
const REFINE_VALIDATION_SUMMARY_CHARS = 2_000;
|
|
467
1424
|
const REFINE_VALIDATION_MAX_COMMANDS = 4;
|
|
1425
|
+
const REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
468
1426
|
|
|
469
1427
|
function truncateValidationOutput(value: unknown): string {
|
|
470
1428
|
const text = typeof value === 'string' ? value : value == null ? '' : String(value);
|
|
@@ -472,171 +1430,492 @@ function truncateValidationOutput(value: unknown): string {
|
|
|
472
1430
|
return `${text.slice(0, REFINE_VALIDATION_SUMMARY_CHARS)}\n[truncated ${text.length - REFINE_VALIDATION_SUMMARY_CHARS} chars]`;
|
|
473
1431
|
}
|
|
474
1432
|
|
|
475
|
-
function
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
const trimmed = command.trim();
|
|
489
|
-
if (!trimmed) return null;
|
|
490
|
-
// Fail closed: the gate never hands shell syntax to a shell. Package-manager
|
|
491
|
-
// scripts are invoked via execFile(binary, args), and metacharacters/quotes are
|
|
492
|
-
// rejected before tokenization so `npm run test && rm -rf` cannot be smuggled in.
|
|
493
|
-
if (/[;&|<>`$\\\n\r'\"]/.test(trimmed)) return null;
|
|
494
|
-
const tokens = trimmed.split(/\s+/).filter(Boolean);
|
|
495
|
-
if (!tokens.length) return null;
|
|
496
|
-
if (tokens.some(token => !/^[A-Za-z0-9_@./:=+-]+$/.test(token))) return null;
|
|
497
|
-
return tokens;
|
|
1433
|
+
function recordMeshRefineStage(
|
|
1434
|
+
stages: Array<Record<string, unknown>>,
|
|
1435
|
+
stage: string,
|
|
1436
|
+
status: MeshRefineStageStatus,
|
|
1437
|
+
startedAt: number,
|
|
1438
|
+
details?: Record<string, unknown>,
|
|
1439
|
+
): void {
|
|
1440
|
+
stages.push({
|
|
1441
|
+
stage,
|
|
1442
|
+
status,
|
|
1443
|
+
durationMs: Date.now() - startedAt,
|
|
1444
|
+
...(details || {}),
|
|
1445
|
+
});
|
|
498
1446
|
}
|
|
499
1447
|
|
|
500
|
-
function
|
|
501
|
-
|
|
1448
|
+
function buildSubmodulePublishRequiredNextStep(entries: MeshRefineSubmoduleReachabilityEntry[]): string {
|
|
1449
|
+
const refs = entries
|
|
1450
|
+
.map(entry => `${entry.path}@${entry.commit}`)
|
|
1451
|
+
.join(', ');
|
|
1452
|
+
return `Ask the user for explicit approval to push/publish the unreachable submodule commit(s) (${refs}) to the configured submodule remote main branch, then rerun mesh_refine_node. Do not merge the root branch until every submodule gitlink commit is reachable from submodule origin/main.`;
|
|
502
1453
|
}
|
|
503
1454
|
|
|
504
|
-
function
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
)
|
|
510
|
-
|
|
511
|
-
if (!tokens) {
|
|
512
|
-
return { rejected: { command: rawCommand, category, source, reason: 'unsafe command string is not allowlisted' } };
|
|
1455
|
+
function resolveRefineryAutoPublishSubmoduleMainCommits(mesh: any, workspace: string): { enabled: boolean; source?: string } {
|
|
1456
|
+
if (mesh?.policy?.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1457
|
+
process.stderr.write(
|
|
1458
|
+
`[adhdev-mesh] WARNING: allowAutoPublishSubmoduleMainCommits is ENABLED via mesh.policy. `
|
|
1459
|
+
+ `Refinery may push unreachable submodule commits to submodule origin/main without additional user approval.\n`,
|
|
1460
|
+
);
|
|
1461
|
+
return { enabled: true, source: 'mesh.policy.allowAutoPublishSubmoduleMainCommits' };
|
|
513
1462
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
scriptName = third;
|
|
522
|
-
args = ['run', scriptName, ...rest];
|
|
523
|
-
} else if (binary === 'npm' && second === 'test' && !third) {
|
|
524
|
-
scriptName = 'test';
|
|
525
|
-
args = ['test'];
|
|
526
|
-
} else if (binary === 'yarn' && second === 'run' && third) {
|
|
527
|
-
scriptName = third;
|
|
528
|
-
args = ['run', scriptName, ...rest];
|
|
529
|
-
} else if (binary === 'yarn' && second && !third) {
|
|
530
|
-
scriptName = second;
|
|
531
|
-
args = [scriptName];
|
|
532
|
-
} else {
|
|
533
|
-
return { rejected: { command: rawCommand, category, source, reason: 'command is not a supported package-manager script invocation' } };
|
|
1463
|
+
const loaded = loadMeshRefineConfig(mesh, workspace);
|
|
1464
|
+
if (loaded.config?.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1465
|
+
process.stderr.write(
|
|
1466
|
+
`[adhdev-mesh] WARNING: allowAutoPublishSubmoduleMainCommits is ENABLED via ${loaded.path || loaded.source}. `
|
|
1467
|
+
+ `Refinery may push unreachable submodule commits to submodule origin/main without additional user approval.\n`,
|
|
1468
|
+
);
|
|
1469
|
+
return { enabled: true, source: loaded.path || loaded.source };
|
|
534
1470
|
}
|
|
1471
|
+
return { enabled: false };
|
|
1472
|
+
}
|
|
535
1473
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
1474
|
+
async function computeGitPatchId(cwd: string, fromRef: string, toRef: string): Promise<string> {
|
|
1475
|
+
const { execFileSync } = await import('node:child_process');
|
|
1476
|
+
const diff = execFileSync('git', ['diff', '--patch', '--full-index', fromRef, toRef], {
|
|
1477
|
+
cwd,
|
|
1478
|
+
encoding: 'utf8',
|
|
1479
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1480
|
+
});
|
|
1481
|
+
if (!diff.trim()) return '';
|
|
1482
|
+
const patchId = execFileSync('git', ['patch-id', '--stable'], {
|
|
1483
|
+
cwd,
|
|
1484
|
+
input: diff,
|
|
1485
|
+
encoding: 'utf8',
|
|
1486
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1487
|
+
}).trim();
|
|
1488
|
+
return patchId.split(/\s+/)[0] || '';
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
async function runMeshRefinePatchEquivalenceGate(
|
|
1492
|
+
repoRoot: string,
|
|
1493
|
+
baseHead: string,
|
|
1494
|
+
branchHead: string,
|
|
1495
|
+
): Promise<MeshRefinePatchEquivalenceSummary> {
|
|
1496
|
+
const startedAt = Date.now();
|
|
1497
|
+
try {
|
|
1498
|
+
const { execFileSync } = await import('node:child_process');
|
|
1499
|
+
const git = (args: string[]) => execFileSync('git', args, {
|
|
1500
|
+
cwd: repoRoot,
|
|
1501
|
+
encoding: 'utf8',
|
|
1502
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1503
|
+
});
|
|
1504
|
+
const mergeBase = git(['merge-base', baseHead, branchHead]).trim();
|
|
1505
|
+
const mergeTreeStdout = git(['merge-tree', '--write-tree', baseHead, branchHead]);
|
|
1506
|
+
const mergedTree = mergeTreeStdout.trim().split(/\s+/)[0] || '';
|
|
1507
|
+
if (!mergeBase || !mergedTree) {
|
|
1508
|
+
return {
|
|
1509
|
+
status: 'failed',
|
|
1510
|
+
equivalent: false,
|
|
1511
|
+
baseHead,
|
|
1512
|
+
branchHead,
|
|
1513
|
+
mergeBase: mergeBase || undefined,
|
|
1514
|
+
mergedTree: mergedTree || undefined,
|
|
1515
|
+
durationMs: Date.now() - startedAt,
|
|
1516
|
+
error: 'patch equivalence preflight could not resolve merge-base or synthetic merge tree',
|
|
1517
|
+
stdout: truncateValidationOutput(mergeTreeStdout),
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
const expectedPatchId = await computeGitPatchId(repoRoot, mergeBase, branchHead);
|
|
1521
|
+
const actualPatchId = await computeGitPatchId(repoRoot, baseHead, mergedTree);
|
|
1522
|
+
const equivalent = expectedPatchId === actualPatchId;
|
|
1523
|
+
return {
|
|
1524
|
+
status: equivalent ? 'passed' : 'failed',
|
|
1525
|
+
equivalent,
|
|
1526
|
+
baseHead,
|
|
1527
|
+
branchHead,
|
|
1528
|
+
mergeBase,
|
|
1529
|
+
mergedTree,
|
|
1530
|
+
expectedPatchId,
|
|
1531
|
+
actualPatchId,
|
|
1532
|
+
durationMs: Date.now() - startedAt,
|
|
1533
|
+
};
|
|
1534
|
+
} catch (e: any) {
|
|
1535
|
+
return {
|
|
1536
|
+
status: 'failed',
|
|
1537
|
+
equivalent: false,
|
|
1538
|
+
baseHead,
|
|
1539
|
+
branchHead,
|
|
1540
|
+
durationMs: Date.now() - startedAt,
|
|
1541
|
+
error: e?.message || String(e),
|
|
1542
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
1543
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
1544
|
+
actionableHint: buildPatchEquivalenceSubmoduleConflictHint(
|
|
1545
|
+
repoRoot,
|
|
1546
|
+
baseHead,
|
|
1547
|
+
branchHead,
|
|
1548
|
+
`${e?.message || ''}\n${e?.stdout || ''}\n${e?.stderr || ''}`,
|
|
1549
|
+
),
|
|
1550
|
+
};
|
|
541
1551
|
}
|
|
1552
|
+
}
|
|
542
1553
|
|
|
1554
|
+
function buildPatchEquivalenceSubmoduleConflictHint(
|
|
1555
|
+
repoRoot: string,
|
|
1556
|
+
baseHead: string,
|
|
1557
|
+
branchHead: string,
|
|
1558
|
+
output: string,
|
|
1559
|
+
): MeshRefineSubmoduleConflictHint | undefined {
|
|
1560
|
+
if (!/(submodule|160000)/i.test(output) || !/(conflict|failed to merge)/i.test(output)) return undefined;
|
|
1561
|
+
const conflicts = readChangedGitlinkPaths(repoRoot, baseHead, branchHead)
|
|
1562
|
+
.map(path => ({
|
|
1563
|
+
path,
|
|
1564
|
+
baseCommit: readTreeObject(repoRoot, baseHead, path),
|
|
1565
|
+
branchCommit: readTreeObject(repoRoot, branchHead, path),
|
|
1566
|
+
}));
|
|
1567
|
+
if (conflicts.length === 0) return undefined;
|
|
543
1568
|
return {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
1569
|
+
kind: 'submodule_conflict',
|
|
1570
|
+
message: 'Refinery could not synthesize a safe merge tree because the branch and base point the same submodule path at different commits.',
|
|
1571
|
+
conflicts,
|
|
1572
|
+
nextSteps: [
|
|
1573
|
+
'Inspect the listed submodule path in both base and branch: baseCommit is the commit currently recorded by the base workspace, branchCommit is the commit recorded by the worktree branch.',
|
|
1574
|
+
'Resolve the submodule first by checking out or creating the intended submodule commit, then commit the chosen gitlink in the root branch.',
|
|
1575
|
+
'Ensure the chosen submodule commit is reachable from the configured submodule remote main branch, then rerun mesh_refine_node.',
|
|
1576
|
+
],
|
|
551
1577
|
};
|
|
552
1578
|
}
|
|
553
1579
|
|
|
554
|
-
function
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
1580
|
+
function readChangedGitlinkPaths(repoRoot: string, fromRef: string, toRef: string): string[] {
|
|
1581
|
+
try {
|
|
1582
|
+
const output = execFileSync('git', ['diff', '--raw', '--no-abbrev', fromRef, toRef], {
|
|
1583
|
+
cwd: repoRoot,
|
|
1584
|
+
encoding: 'utf8',
|
|
1585
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1586
|
+
});
|
|
1587
|
+
const paths = new Set<string>();
|
|
1588
|
+
for (const line of output.split('\n')) {
|
|
1589
|
+
if (!line.trim()) continue;
|
|
1590
|
+
const metaAndPath = line.split('\t');
|
|
1591
|
+
const meta = metaAndPath[0] || '';
|
|
1592
|
+
const path = metaAndPath[metaAndPath.length - 1]?.trim();
|
|
1593
|
+
if (!path) continue;
|
|
1594
|
+
const parts = meta.split(/\s+/);
|
|
1595
|
+
if (parts[0]?.includes('160000') || parts[1]?.includes('160000')) {
|
|
1596
|
+
paths.add(path);
|
|
1597
|
+
}
|
|
568
1598
|
}
|
|
1599
|
+
return [...paths].sort();
|
|
1600
|
+
} catch {
|
|
1601
|
+
return [];
|
|
569
1602
|
}
|
|
570
|
-
return candidates.sort((a, b) => {
|
|
571
|
-
const rank = (value?: string) => value === 'high' ? 0 : value === 'medium' ? 1 : 2;
|
|
572
|
-
return rank(a.confidence) - rank(b.confidence);
|
|
573
|
-
});
|
|
574
1603
|
}
|
|
575
1604
|
|
|
576
|
-
function
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
const category = REFINE_VALIDATION_CATEGORIES.find(cat => commandText.includes(` ${cat}`)) ?? '';
|
|
589
|
-
return { command: commandText, category, source: 'mesh.policy.validationCommands' };
|
|
590
|
-
})
|
|
591
|
-
.filter((entry: any) => !!entry.category);
|
|
1605
|
+
function readTreeObject(repoRoot: string, ref: string, path: string): string | undefined {
|
|
1606
|
+
try {
|
|
1607
|
+
const output = execFileSync('git', ['ls-tree', ref, '--', path], {
|
|
1608
|
+
cwd: repoRoot,
|
|
1609
|
+
encoding: 'utf8',
|
|
1610
|
+
maxBuffer: 1024 * 1024,
|
|
1611
|
+
}).trim();
|
|
1612
|
+
const match = output.match(/\bcommit\s+([0-9a-f]{40})\b/i);
|
|
1613
|
+
return match?.[1];
|
|
1614
|
+
} catch {
|
|
1615
|
+
return undefined;
|
|
1616
|
+
}
|
|
592
1617
|
}
|
|
593
1618
|
|
|
594
|
-
function
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
1619
|
+
async function alignRefinerySubmodulesAfterMerge(
|
|
1620
|
+
repoRoot: string,
|
|
1621
|
+
previousBaseHead: string,
|
|
1622
|
+
currentHead: string,
|
|
1623
|
+
options: { submoduleIgnorePaths?: string[] } = {},
|
|
1624
|
+
): Promise<MeshRefineSubmoduleAlignmentSummary> {
|
|
1625
|
+
const startedAt = Date.now();
|
|
1626
|
+
const changedGitlinkPaths = readChangedGitlinkPaths(repoRoot, previousBaseHead, currentHead)
|
|
1627
|
+
.filter(path => !(options.submoduleIgnorePaths || []).includes(path));
|
|
1628
|
+
const preStatus = await getGitRepoStatus(repoRoot, {
|
|
1629
|
+
includeSubmodules: true,
|
|
1630
|
+
submoduleIgnorePaths: options.submoduleIgnorePaths,
|
|
1631
|
+
timeoutMs: 15_000,
|
|
1632
|
+
});
|
|
1633
|
+
const outOfSyncPaths = (preStatus.submodules || [])
|
|
1634
|
+
.filter(submodule => submodule.dirty || submodule.outOfSync || !!submodule.error)
|
|
1635
|
+
.map(submodule => submodule.path);
|
|
1636
|
+
const updatePaths = [...new Set([...changedGitlinkPaths, ...outOfSyncPaths])].sort();
|
|
1637
|
+
|
|
1638
|
+
if (updatePaths.length === 0) {
|
|
1639
|
+
return {
|
|
1640
|
+
status: 'skipped',
|
|
1641
|
+
changedGitlinkPaths,
|
|
1642
|
+
outOfSyncPaths,
|
|
1643
|
+
updatedPaths: [],
|
|
1644
|
+
verifiedPaths: [],
|
|
1645
|
+
durationMs: Date.now() - startedAt,
|
|
1646
|
+
reason: 'no_changed_or_out_of_sync_submodules',
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
const commandArgs = ['submodule', 'update', '--init', '--recursive', '--', ...updatePaths];
|
|
1651
|
+
try {
|
|
1652
|
+
const { execFile } = await import('node:child_process');
|
|
1653
|
+
const { promisify } = await import('node:util');
|
|
1654
|
+
const execFileAsync = promisify(execFile);
|
|
1655
|
+
const result = await execFileAsync('git', commandArgs, {
|
|
1656
|
+
cwd: repoRoot,
|
|
1657
|
+
encoding: 'utf8',
|
|
1658
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1659
|
+
timeout: 60_000,
|
|
1660
|
+
});
|
|
1661
|
+
const postStatus = await getGitRepoStatus(repoRoot, {
|
|
1662
|
+
includeSubmodules: true,
|
|
1663
|
+
submoduleIgnorePaths: options.submoduleIgnorePaths,
|
|
1664
|
+
timeoutMs: 15_000,
|
|
1665
|
+
});
|
|
1666
|
+
const remaining = (postStatus.submodules || [])
|
|
1667
|
+
.filter(submodule => updatePaths.includes(submodule.path) && (submodule.dirty || submodule.outOfSync || !!submodule.error));
|
|
1668
|
+
return {
|
|
1669
|
+
status: remaining.length === 0 ? 'passed' : 'failed',
|
|
1670
|
+
changedGitlinkPaths,
|
|
1671
|
+
outOfSyncPaths,
|
|
1672
|
+
updatedPaths: updatePaths,
|
|
1673
|
+
verifiedPaths: updatePaths.filter(path => !remaining.some(submodule => submodule.path === path)),
|
|
1674
|
+
durationMs: Date.now() - startedAt,
|
|
1675
|
+
command: `git ${commandArgs.join(' ')}`,
|
|
1676
|
+
stdout: truncateValidationOutput(result.stdout),
|
|
1677
|
+
stderr: truncateValidationOutput(result.stderr),
|
|
1678
|
+
...(remaining.length > 0 ? { error: `Submodule checkout remained out of sync after update: ${remaining.map(entry => entry.path).join(', ')}` } : {}),
|
|
1679
|
+
};
|
|
1680
|
+
} catch (e: any) {
|
|
1681
|
+
return {
|
|
1682
|
+
status: 'failed',
|
|
1683
|
+
changedGitlinkPaths,
|
|
1684
|
+
outOfSyncPaths,
|
|
1685
|
+
updatedPaths: updatePaths,
|
|
1686
|
+
verifiedPaths: [],
|
|
1687
|
+
durationMs: Date.now() - startedAt,
|
|
1688
|
+
command: `git ${commandArgs.join(' ')}`,
|
|
1689
|
+
error: e?.message || String(e),
|
|
1690
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
1691
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
async function runMeshRefineSubmoduleReachabilityGate(
|
|
1697
|
+
repoRoot: string,
|
|
1698
|
+
mergedTree: string,
|
|
1699
|
+
options: { allowAutoPublishSubmoduleMainCommits?: boolean; autoPublishPolicySource?: string; worktreeRoot?: string } = {},
|
|
1700
|
+
): Promise<MeshRefineSubmoduleReachabilitySummary> {
|
|
1701
|
+
const startedAt = Date.now();
|
|
1702
|
+
const entries: MeshRefineSubmoduleReachabilityEntry[] = [];
|
|
1703
|
+
try {
|
|
1704
|
+
const { execFile } = await import('node:child_process');
|
|
1705
|
+
const { promisify } = await import('node:util');
|
|
1706
|
+
const execFileAsync = promisify(execFile);
|
|
1707
|
+
const runGit = async (cwd: string, args: string[]): Promise<string> => {
|
|
1708
|
+
const { stdout } = await execFileAsync('git', args, {
|
|
1709
|
+
cwd,
|
|
1710
|
+
encoding: 'utf8',
|
|
1711
|
+
timeout: 30_000,
|
|
1712
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1713
|
+
windowsHide: true,
|
|
1714
|
+
});
|
|
1715
|
+
return String(stdout || '');
|
|
1716
|
+
};
|
|
1717
|
+
const verifyRemoteMainContainsCommit = async (submodulePath: string, commit: string, branch = 'main'): Promise<void> => {
|
|
1718
|
+
await runGit(submodulePath, ['-c', 'protocol.file.allow=always', 'fetch', 'origin', `refs/heads/${branch}:refs/remotes/origin/${branch}`]);
|
|
1719
|
+
await runGit(submodulePath, ['merge-base', '--is-ancestor', commit, `refs/remotes/origin/${branch}`]);
|
|
1720
|
+
};
|
|
1721
|
+
const publishCommitToRemoteMain = async (submodulePath: string, commit: string, branch = 'main'): Promise<{ stdout: string; stderr: string; refspec: string }> => {
|
|
1722
|
+
const refspec = `${commit}:refs/heads/${branch}`;
|
|
1723
|
+
const { stdout, stderr } = await execFileAsync('git', ['push', 'origin', refspec], {
|
|
1724
|
+
cwd: submodulePath,
|
|
1725
|
+
encoding: 'utf8',
|
|
1726
|
+
timeout: 30_000,
|
|
1727
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1728
|
+
windowsHide: true,
|
|
1729
|
+
});
|
|
1730
|
+
return { stdout: String(stdout || ''), stderr: String(stderr || ''), refspec };
|
|
1731
|
+
};
|
|
1732
|
+
const importCommitFromWorktreeSubmodule = async (submodulePath: string, worktreeSubmodulePath: string, commit: string): Promise<boolean> => {
|
|
1733
|
+
if (!fs.existsSync(worktreeSubmodulePath)) return false;
|
|
1734
|
+
try {
|
|
1735
|
+
await runGit(worktreeSubmodulePath, ['cat-file', '-e', `${commit}^{commit}`]);
|
|
1736
|
+
} catch {
|
|
1737
|
+
return false;
|
|
1738
|
+
}
|
|
1739
|
+
await runGit(submodulePath, ['-c', 'protocol.file.allow=always', 'fetch', worktreeSubmodulePath, commit]);
|
|
1740
|
+
await runGit(submodulePath, ['cat-file', '-e', `${commit}^{commit}`]);
|
|
1741
|
+
return true;
|
|
1742
|
+
};
|
|
1743
|
+
|
|
1744
|
+
const treeOutput = await runGit(repoRoot, ['ls-tree', '-r', '-z', mergedTree]);
|
|
1745
|
+
const gitlinks = treeOutput
|
|
1746
|
+
.split('\0')
|
|
1747
|
+
.filter(Boolean)
|
|
1748
|
+
.map(record => {
|
|
1749
|
+
const match = /^160000\s+commit\s+([0-9a-f]{40})\t(.+)$/.exec(record);
|
|
1750
|
+
return match ? { commit: match[1], path: match[2] } : null;
|
|
1751
|
+
})
|
|
1752
|
+
.filter((entry): entry is { commit: string; path: string } => !!entry);
|
|
1753
|
+
|
|
1754
|
+
for (const gitlink of gitlinks) {
|
|
1755
|
+
const submodulePath = pathResolve(repoRoot, gitlink.path);
|
|
1756
|
+
const entry: MeshRefineSubmoduleReachabilityEntry = {
|
|
1757
|
+
path: gitlink.path,
|
|
1758
|
+
commit: gitlink.commit,
|
|
1759
|
+
reachable: false,
|
|
1760
|
+
};
|
|
1761
|
+
try {
|
|
1762
|
+
if (!fs.existsSync(submodulePath)) {
|
|
1763
|
+
entry.error = `Submodule checkout missing at ${gitlink.path}`;
|
|
1764
|
+
entry.publishRequired = true;
|
|
1765
|
+
if (options.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1766
|
+
entry.autoPublishAllowed = true;
|
|
1767
|
+
entry.autoPublishAttempted = false;
|
|
1768
|
+
entry.autoPublishSkippedReason = `submodule checkout missing at ${gitlink.path}; cannot perform non-force push to origin/main`;
|
|
1769
|
+
}
|
|
1770
|
+
entries.push(entry);
|
|
1771
|
+
continue;
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
entry.checkedLocal = true;
|
|
1775
|
+
try {
|
|
1776
|
+
await runGit(submodulePath, ['cat-file', '-e', `${gitlink.commit}^{commit}`]);
|
|
1777
|
+
entry.localReachable = true;
|
|
1778
|
+
} catch {
|
|
1779
|
+
entry.localReachable = false;
|
|
1780
|
+
if (options.allowAutoPublishSubmoduleMainCommits === true && options.worktreeRoot) {
|
|
1781
|
+
try {
|
|
1782
|
+
const imported = await importCommitFromWorktreeSubmodule(
|
|
1783
|
+
submodulePath,
|
|
1784
|
+
pathResolve(options.worktreeRoot, gitlink.path),
|
|
1785
|
+
gitlink.commit,
|
|
1786
|
+
);
|
|
1787
|
+
if (imported) {
|
|
1788
|
+
entry.localReachable = true;
|
|
1789
|
+
entry.importedFromWorktree = true;
|
|
1790
|
+
}
|
|
1791
|
+
} catch (importError: any) {
|
|
1792
|
+
entry.autoPublishSkippedReason = `candidate commit was not present in the source checkout and could not be imported from worktree submodule: ${truncateValidationOutput(importError?.stderr || importError?.message || String(importError))}`;
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
// Probe the submodule remote before allowing cleanup/completion.
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
try {
|
|
1799
|
+
entry.remote = 'origin';
|
|
1800
|
+
let remoteUrl = '';
|
|
1801
|
+
try {
|
|
1802
|
+
remoteUrl = (await runGit(submodulePath, ['remote', 'get-url', 'origin'])).trim();
|
|
1803
|
+
if (!remoteUrl) throw new Error('origin remote has no URL');
|
|
1804
|
+
entry.remoteUrl = remoteUrl;
|
|
1805
|
+
} catch {
|
|
1806
|
+
entry.error = 'Submodule remote reachability check failed: no configured origin remote';
|
|
1807
|
+
entry.publishRequired = true;
|
|
1808
|
+
if (options.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1809
|
+
entry.autoPublishAllowed = true;
|
|
1810
|
+
entry.autoPublishAttempted = false;
|
|
1811
|
+
entry.autoPublishSkippedReason = 'submodule origin remote is not configured; cannot perform non-force push to origin/main';
|
|
1812
|
+
}
|
|
1813
|
+
entries.push(entry);
|
|
1814
|
+
continue;
|
|
1815
|
+
}
|
|
1816
|
+
entry.remoteMainBranch = 'main';
|
|
1817
|
+
try {
|
|
1818
|
+
await verifyRemoteMainContainsCommit(submodulePath, gitlink.commit, 'main');
|
|
1819
|
+
entry.fetchedFromOrigin = true;
|
|
1820
|
+
entry.remoteReachable = true;
|
|
1821
|
+
entry.remoteMainReachable = true;
|
|
1822
|
+
entry.reachable = true;
|
|
1823
|
+
} catch (e: any) {
|
|
1824
|
+
entry.remoteReachable = false;
|
|
1825
|
+
entry.remoteMainReachable = false;
|
|
1826
|
+
entry.publishRequired = true;
|
|
1827
|
+
const details = truncateValidationOutput(e?.stderr || e?.message || String(e));
|
|
1828
|
+
entry.error = `Submodule remote main reachability check failed for origin/main: ${details}`;
|
|
1829
|
+
if (options.allowAutoPublishSubmoduleMainCommits === true && entry.localReachable === true) {
|
|
1830
|
+
entry.autoPublishAllowed = true;
|
|
1831
|
+
entry.autoPublishAttempted = true;
|
|
1832
|
+
try {
|
|
1833
|
+
const publish = await publishCommitToRemoteMain(submodulePath, gitlink.commit, 'main');
|
|
1834
|
+
entry.autoPublishRefspec = publish.refspec;
|
|
1835
|
+
entry.publishStdout = truncateValidationOutput(publish.stdout);
|
|
1836
|
+
entry.publishStderr = truncateValidationOutput(publish.stderr);
|
|
1837
|
+
entry.autoPublishSucceeded = true;
|
|
1838
|
+
await verifyRemoteMainContainsCommit(submodulePath, gitlink.commit, 'main');
|
|
1839
|
+
entry.fetchedFromOrigin = true;
|
|
1840
|
+
entry.remoteReachable = true;
|
|
1841
|
+
entry.remoteMainReachable = true;
|
|
1842
|
+
entry.autoPublishVerified = true;
|
|
1843
|
+
entry.publishRequired = false;
|
|
1844
|
+
entry.reachable = true;
|
|
1845
|
+
entry.error = undefined;
|
|
1846
|
+
} catch (publishError: any) {
|
|
1847
|
+
entry.autoPublishSucceeded = false;
|
|
1848
|
+
entry.autoPublishVerified = false;
|
|
1849
|
+
const publishDetails = truncateValidationOutput(publishError?.stderr || publishError?.message || String(publishError));
|
|
1850
|
+
entry.error = `Submodule auto-publish to origin/main failed or could not be verified: ${publishDetails}`;
|
|
1851
|
+
}
|
|
1852
|
+
} else if (options.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1853
|
+
entry.autoPublishAllowed = true;
|
|
1854
|
+
entry.autoPublishAttempted = false;
|
|
1855
|
+
entry.autoPublishSkippedReason = entry.autoPublishSkippedReason
|
|
1856
|
+
|| 'candidate commit is not reachable in the source checkout or worktree submodule, so Refinery cannot push it to origin/main';
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
} catch (e: any) {
|
|
1860
|
+
entry.remoteReachable = false;
|
|
1861
|
+
entry.remoteMainReachable = false;
|
|
1862
|
+
entry.publishRequired = true;
|
|
1863
|
+
const details = truncateValidationOutput(e?.stderr || e?.message || String(e));
|
|
1864
|
+
entry.error = `Submodule remote main reachability check failed for origin/main: ${details}`;
|
|
1865
|
+
}
|
|
1866
|
+
} catch (e: any) {
|
|
1867
|
+
entry.error = truncateValidationOutput(e?.message || String(e));
|
|
1868
|
+
entry.publishRequired = true;
|
|
1869
|
+
}
|
|
1870
|
+
entries.push(entry);
|
|
627
1871
|
}
|
|
1872
|
+
|
|
1873
|
+
const unreachable = entries.filter(entry => !entry.reachable);
|
|
1874
|
+
return {
|
|
1875
|
+
status: unreachable.length ? 'failed' : 'passed',
|
|
1876
|
+
checked: entries.length,
|
|
1877
|
+
unreachable: unreachable.map(entry => ({ ...entry, publishRequired: entry.publishRequired !== false })),
|
|
1878
|
+
entries: entries.map(entry => entry.reachable ? entry : { ...entry, publishRequired: entry.publishRequired !== false }),
|
|
1879
|
+
durationMs: Date.now() - startedAt,
|
|
1880
|
+
autoPublishAllowed: options.allowAutoPublishSubmoduleMainCommits === true,
|
|
1881
|
+
autoPublishPolicySource: options.autoPublishPolicySource,
|
|
1882
|
+
};
|
|
1883
|
+
} catch (e: any) {
|
|
1884
|
+
const unreachable = entries.filter(entry => !entry.reachable).map(entry => ({ ...entry, publishRequired: true }));
|
|
1885
|
+
return {
|
|
1886
|
+
status: 'failed',
|
|
1887
|
+
checked: entries.length,
|
|
1888
|
+
unreachable,
|
|
1889
|
+
entries: entries.map(entry => entry.reachable ? entry : { ...entry, publishRequired: true }),
|
|
1890
|
+
durationMs: Date.now() - startedAt,
|
|
1891
|
+
autoPublishAllowed: options.allowAutoPublishSubmoduleMainCommits === true,
|
|
1892
|
+
autoPublishPolicySource: options.autoPublishPolicySource,
|
|
1893
|
+
error: truncateValidationOutput(e?.message || String(e)),
|
|
1894
|
+
};
|
|
628
1895
|
}
|
|
1896
|
+
}
|
|
629
1897
|
|
|
1898
|
+
function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
|
|
1899
|
+
const plan = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
1900
|
+
const mapCommand = (command: MeshRefineValidationCommandPlan) => ({
|
|
1901
|
+
displayCommand: command.displayCommand,
|
|
1902
|
+
category: command.category,
|
|
1903
|
+
source: command.source,
|
|
1904
|
+
cwd: command.cwd,
|
|
1905
|
+
timeoutMs: command.timeoutMs,
|
|
1906
|
+
});
|
|
630
1907
|
return {
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
1908
|
+
source: plan.source,
|
|
1909
|
+
sourceType: plan.sourceType,
|
|
1910
|
+
bootstrapCommands: plan.bootstrapCommands.map(mapCommand),
|
|
1911
|
+
commands: plan.commands.map(mapCommand),
|
|
1912
|
+
unavailableReason: plan.unavailableReason,
|
|
1913
|
+
rejectedCommands: plan.rejectedCommands,
|
|
1914
|
+
suggestions: plan.suggestions,
|
|
1915
|
+
suggestedConfig: plan.suggestedConfig,
|
|
1916
|
+
note: plan.sourceType === 'unavailable'
|
|
1917
|
+
? 'No validation command will be executed until a repo mesh/refine config is provided. Heuristics are suggestions only.'
|
|
1918
|
+
: 'Validation commands are resolved from repo mesh/refine config; heuristics are suggestions only.',
|
|
640
1919
|
};
|
|
641
1920
|
}
|
|
642
1921
|
|
|
@@ -644,60 +1923,119 @@ async function runMeshRefineValidationGate(mesh: any, workspace: string): Promis
|
|
|
644
1923
|
const { execFile } = await import('node:child_process');
|
|
645
1924
|
const { promisify } = await import('node:util');
|
|
646
1925
|
const execFileAsync = promisify(execFile);
|
|
647
|
-
const selection =
|
|
1926
|
+
const selection = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
648
1927
|
const summary: MeshRefineValidationSummary = {
|
|
649
1928
|
status: 'skipped',
|
|
650
1929
|
required: true,
|
|
651
1930
|
commandsRun: [],
|
|
1931
|
+
bootstrapCommandsRun: [],
|
|
652
1932
|
rejectedCommands: selection.rejectedCommands,
|
|
653
1933
|
skippedReason: undefined,
|
|
654
1934
|
timeoutMs: REFINE_VALIDATION_TIMEOUT_MS,
|
|
655
1935
|
outputLimitBytes: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1936
|
+
configSource: selection.source,
|
|
1937
|
+
configSourceType: selection.sourceType,
|
|
1938
|
+
suggestions: selection.suggestions,
|
|
1939
|
+
suggestedConfig: selection.suggestedConfig,
|
|
656
1940
|
};
|
|
657
1941
|
|
|
658
1942
|
if (!selection.commands.length) {
|
|
659
|
-
summary.skippedReason = 'validation_unavailable:
|
|
1943
|
+
summary.skippedReason = selection.unavailableReason || 'validation_unavailable: repo mesh/refine config did not provide executable validation.commands';
|
|
660
1944
|
return summary;
|
|
661
1945
|
}
|
|
662
1946
|
|
|
1947
|
+
const commandRecord = (candidate: MeshRefineValidationCommand, cwd: string, startedAt: number, result: any, passed: boolean, extras: Record<string, unknown> = {}) => ({
|
|
1948
|
+
command: candidate.command,
|
|
1949
|
+
args: candidate.args,
|
|
1950
|
+
displayCommand: candidate.displayCommand,
|
|
1951
|
+
category: candidate.category,
|
|
1952
|
+
source: candidate.source,
|
|
1953
|
+
cwd,
|
|
1954
|
+
passed,
|
|
1955
|
+
durationMs: Date.now() - startedAt,
|
|
1956
|
+
stdout: truncateValidationOutput(result?.stdout),
|
|
1957
|
+
stderr: truncateValidationOutput(result?.stderr || result?.message),
|
|
1958
|
+
...extras,
|
|
1959
|
+
});
|
|
1960
|
+
const isPackageManagerValidation = (candidate: MeshRefineValidationCommand): boolean => {
|
|
1961
|
+
const command = pathBasename(candidate.command).replace(/\.(?:cmd|exe)$/i, '');
|
|
1962
|
+
return ['npm', 'pnpm', 'yarn', 'bun'].includes(command)
|
|
1963
|
+
&& candidate.args.some(arg => arg === 'run' || arg === 'test' || arg === 'exec');
|
|
1964
|
+
};
|
|
1965
|
+
const dependenciesLikelyMissing = (cwd: string): boolean => {
|
|
1966
|
+
if (!fs.existsSync(pathJoin(cwd, 'package.json'))) return false;
|
|
1967
|
+
if (fs.existsSync(pathJoin(cwd, 'node_modules'))) return false;
|
|
1968
|
+
return ['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb', 'bun.lock']
|
|
1969
|
+
.some(lock => fs.existsSync(pathJoin(cwd, lock)));
|
|
1970
|
+
};
|
|
1971
|
+
|
|
1972
|
+
for (const candidate of selection.bootstrapCommands) {
|
|
1973
|
+
const startedAt = Date.now();
|
|
1974
|
+
const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
|
|
1975
|
+
const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
|
|
1976
|
+
try {
|
|
1977
|
+
const result = await execFileAsync(candidate.command, candidate.args, {
|
|
1978
|
+
cwd,
|
|
1979
|
+
encoding: 'utf8',
|
|
1980
|
+
timeout,
|
|
1981
|
+
maxBuffer: candidate.outputLimitBytes || REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1982
|
+
env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
|
|
1983
|
+
});
|
|
1984
|
+
summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
|
|
1985
|
+
} catch (error: any) {
|
|
1986
|
+
summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
|
|
1987
|
+
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
1988
|
+
signal: typeof error?.signal === 'string' ? error.signal : null,
|
|
1989
|
+
timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
|
|
1990
|
+
failureKind: 'dependency_bootstrap_failed',
|
|
1991
|
+
}));
|
|
1992
|
+
summary.status = 'failed';
|
|
1993
|
+
summary.failureKind = 'dependency_bootstrap_failed';
|
|
1994
|
+
summary.failureCode = 'dependency_bootstrap_failed';
|
|
1995
|
+
return summary;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
|
|
663
1999
|
for (const candidate of selection.commands) {
|
|
664
2000
|
const startedAt = Date.now();
|
|
2001
|
+
const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
|
|
2002
|
+
const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
|
|
2003
|
+
if (selection.bootstrapCommands.length === 0 && isPackageManagerValidation(candidate) && dependenciesLikelyMissing(cwd)) {
|
|
2004
|
+
summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, {
|
|
2005
|
+
stderr: 'Dependencies appear to be missing: package.json and a lockfile are present, but node_modules is absent. Configure validation.bootstrapCommands in repo mesh/refine config if Refinery should install/bootstrap before validation.',
|
|
2006
|
+
}, false, {
|
|
2007
|
+
exitCode: null,
|
|
2008
|
+
skipped: true,
|
|
2009
|
+
failureKind: 'missing_dependencies',
|
|
2010
|
+
}));
|
|
2011
|
+
summary.status = 'failed';
|
|
2012
|
+
summary.failureKind = 'missing_dependencies';
|
|
2013
|
+
summary.failureCode = 'missing_dependencies';
|
|
2014
|
+
return summary;
|
|
2015
|
+
}
|
|
665
2016
|
try {
|
|
666
2017
|
const result = await execFileAsync(candidate.command, candidate.args, {
|
|
667
|
-
cwd
|
|
2018
|
+
cwd,
|
|
668
2019
|
encoding: 'utf8',
|
|
669
|
-
timeout
|
|
670
|
-
maxBuffer: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
671
|
-
env: { ...process.env, CI: process.env.CI || '1' },
|
|
672
|
-
});
|
|
673
|
-
summary.commandsRun.push({
|
|
674
|
-
command: candidate.command,
|
|
675
|
-
args: candidate.args,
|
|
676
|
-
displayCommand: candidate.displayCommand,
|
|
677
|
-
category: candidate.category,
|
|
678
|
-
source: candidate.source,
|
|
679
|
-
passed: true,
|
|
680
|
-
exitCode: 0,
|
|
681
|
-
durationMs: Date.now() - startedAt,
|
|
682
|
-
stdout: truncateValidationOutput(result.stdout),
|
|
683
|
-
stderr: truncateValidationOutput(result.stderr),
|
|
2020
|
+
timeout,
|
|
2021
|
+
maxBuffer: candidate.outputLimitBytes || REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
2022
|
+
env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
|
|
684
2023
|
});
|
|
2024
|
+
summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
|
|
685
2025
|
} catch (error: any) {
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
displayCommand: candidate.displayCommand,
|
|
690
|
-
category: candidate.category,
|
|
691
|
-
source: candidate.source,
|
|
692
|
-
passed: false,
|
|
2026
|
+
const stderr = truncateValidationOutput(error?.stderr || error?.message);
|
|
2027
|
+
const missingDependencyFailure = /Cannot find module|MODULE_NOT_FOUND|node_modules|command not found|not found/i.test(stderr);
|
|
2028
|
+
summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
|
|
693
2029
|
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
694
2030
|
signal: typeof error?.signal === 'string' ? error.signal : null,
|
|
695
2031
|
timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
stderr: truncateValidationOutput(error?.stderr || error?.message),
|
|
699
|
-
});
|
|
2032
|
+
...(missingDependencyFailure ? { failureKind: 'missing_dependencies' } : {}),
|
|
2033
|
+
}));
|
|
700
2034
|
summary.status = 'failed';
|
|
2035
|
+
if (missingDependencyFailure) {
|
|
2036
|
+
summary.failureKind = 'missing_dependencies';
|
|
2037
|
+
summary.failureCode = 'missing_dependencies';
|
|
2038
|
+
}
|
|
701
2039
|
return summary;
|
|
702
2040
|
}
|
|
703
2041
|
}
|
|
@@ -810,12 +2148,14 @@ export interface CommandRouterDeps {
|
|
|
810
2148
|
/** Reference to detected IDEs array (mutable — router updates it) */
|
|
811
2149
|
detectedIdes: { value: any[] };
|
|
812
2150
|
sessionRegistry: SessionRegistry;
|
|
813
|
-
/** Callback
|
|
2151
|
+
/** Callback after CDP manager created (transport-specific extras) */
|
|
814
2152
|
onCdpManagerCreated?: (ideType: string, manager: DaemonCdpManager) => void;
|
|
815
2153
|
/** Callback after IDE connected (e.g., startAgentStreamPolling) */
|
|
816
2154
|
onIdeConnected?: () => void;
|
|
817
2155
|
/** Callback after status change (stop_ide, restart) */
|
|
818
2156
|
onStatusChange?: () => void;
|
|
2157
|
+
/** Callback when a mesh state is invalidated */
|
|
2158
|
+
onMeshStateChange?: (meshId: string) => void;
|
|
819
2159
|
/** Callback after chat-related commands */
|
|
820
2160
|
onPostChatCommand?: () => void;
|
|
821
2161
|
/** Get a connected CDP manager (for agent stream reset check) */
|
|
@@ -829,6 +2169,8 @@ export interface CommandRouterDeps {
|
|
|
829
2169
|
sessionHostControl?: SessionHostControlPlane | null;
|
|
830
2170
|
/** Selected-coordinator mesh peer telemetry surface for target daemons, when supported by the runtime. */
|
|
831
2171
|
getMeshPeerConnectionStatus?: (daemonId: string) => Record<string, unknown> | null;
|
|
2172
|
+
/** Dispatch a command to a remote mesh node via P2P/relay. Injected by cloud runtime; absent in standalone. */
|
|
2173
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
832
2174
|
}
|
|
833
2175
|
|
|
834
2176
|
export interface CommandRouterResult {
|
|
@@ -940,17 +2282,210 @@ function summarizeSessionHostPruneResult(result: unknown): Record<string, unknow
|
|
|
940
2282
|
};
|
|
941
2283
|
}
|
|
942
2284
|
|
|
2285
|
+
function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
|
|
2286
|
+
const raw = hostAddress.trim();
|
|
2287
|
+
if (!raw) throw new Error('hostAddress required');
|
|
2288
|
+
const url = new URL(raw.replace(/^ws:/, 'http:').replace(/^wss:/, 'https:'));
|
|
2289
|
+
url.pathname = '/api/v1/command';
|
|
2290
|
+
url.search = '';
|
|
2291
|
+
url.hash = '';
|
|
2292
|
+
return url.toString();
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
function buildMemberJoinNode(mesh: any, args: any, fallbackDaemonId?: string): Record<string, unknown> | null {
|
|
2296
|
+
const requestedNodeId = typeof args?.memberNodeId === 'string' ? args.memberNodeId.trim() : '';
|
|
2297
|
+
const explicit = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
2298
|
+
? args.memberNode as Record<string, any>
|
|
2299
|
+
: null;
|
|
2300
|
+
const configured = Array.isArray(mesh?.nodes)
|
|
2301
|
+
? (requestedNodeId
|
|
2302
|
+
? mesh.nodes.find((node: any) => node?.id === requestedNodeId || node?.nodeId === requestedNodeId)
|
|
2303
|
+
: mesh.nodes[0])
|
|
2304
|
+
: null;
|
|
2305
|
+
const source = explicit || configured;
|
|
2306
|
+
const workspace = typeof source?.workspace === 'string' && source.workspace.trim()
|
|
2307
|
+
? source.workspace.trim()
|
|
2308
|
+
: typeof args?.workspace === 'string' && args.workspace.trim()
|
|
2309
|
+
? args.workspace.trim()
|
|
2310
|
+
: process.cwd();
|
|
2311
|
+
if (!workspace) return null;
|
|
2312
|
+
const nodeId = typeof source?.id === 'string' && source.id.trim()
|
|
2313
|
+
? source.id.trim()
|
|
2314
|
+
: typeof source?.nodeId === 'string' && source.nodeId.trim()
|
|
2315
|
+
? source.nodeId.trim()
|
|
2316
|
+
: undefined;
|
|
2317
|
+
return {
|
|
2318
|
+
...(nodeId ? { id: nodeId } : {}),
|
|
2319
|
+
workspace,
|
|
2320
|
+
...(typeof source?.repoRoot === 'string' && source.repoRoot.trim() ? { repoRoot: source.repoRoot.trim() } : {}),
|
|
2321
|
+
...(typeof source?.daemonId === 'string' && source.daemonId.trim() ? { daemonId: source.daemonId.trim() } : fallbackDaemonId ? { daemonId: fallbackDaemonId } : {}),
|
|
2322
|
+
...(typeof source?.machineId === 'string' && source.machineId.trim() ? { machineId: source.machineId.trim() } : {}),
|
|
2323
|
+
userOverrides: source?.userOverrides && typeof source.userOverrides === 'object' && !Array.isArray(source.userOverrides) ? source.userOverrides : {},
|
|
2324
|
+
policy: source?.policy && typeof source.policy === 'object' && !Array.isArray(source.policy) ? source.policy : {},
|
|
2325
|
+
role: 'member',
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
|
|
943
2329
|
export class DaemonCommandRouter {
|
|
944
2330
|
private deps: CommandRouterDeps;
|
|
945
2331
|
/** In-memory cache for cloud-originating meshes passed via inlineMesh.
|
|
946
2332
|
* Allows the MCP server to query mesh data via get_mesh even when
|
|
947
2333
|
* the mesh doesn't exist in the local meshes.json file. */
|
|
948
2334
|
private inlineMeshCache = new Map<string, any>();
|
|
2335
|
+
/** Coordinator-owned whole-mesh aggregate status snapshots. Browser callers read this by default. */
|
|
2336
|
+
private aggregateMeshStatusCache = new Map<string, { builtAt: number; snapshot: any; queueRevision: string }>();
|
|
2337
|
+
/** In-memory async Refinery jobs keyed by meshId:nodeId to reject/return duplicate in-flight requests. */
|
|
2338
|
+
private runningRefineJobs = new Map<string, MeshRefineJobHandle>();
|
|
2339
|
+
/** Terminal async Refinery jobs preserve a clear answer after the worktree node has been removed. */
|
|
2340
|
+
private terminalRefineJobs = new Map<string, MeshRefineTerminalJob>();
|
|
949
2341
|
|
|
950
2342
|
constructor(deps: CommandRouterDeps) {
|
|
951
2343
|
this.deps = deps;
|
|
952
2344
|
}
|
|
953
2345
|
|
|
2346
|
+
private cloneJsonValue<T>(value: T): T {
|
|
2347
|
+
if (typeof structuredClone === 'function') return structuredClone(value);
|
|
2348
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
private hydrateCachedAggregateMeshStatusFromInline(snapshot: any, mesh: any, options?: { requireDirectPeerTruth?: boolean }): any {
|
|
2352
|
+
if (!mesh || typeof mesh !== 'object' || !Array.isArray(mesh.nodes) || !Array.isArray(snapshot?.nodes)) return snapshot;
|
|
2353
|
+
const inlineNodesById = new Map<string, any>();
|
|
2354
|
+
for (const node of mesh.nodes) {
|
|
2355
|
+
const nodeId = readInlineMeshNodeId(node);
|
|
2356
|
+
if (nodeId) inlineNodesById.set(nodeId, node);
|
|
2357
|
+
}
|
|
2358
|
+
if (!inlineNodesById.size) return snapshot;
|
|
2359
|
+
|
|
2360
|
+
let changed = false;
|
|
2361
|
+
const unavailableNodeIds = new Set<string>();
|
|
2362
|
+
const sourceOfTruth = readObjectRecord(snapshot.sourceOfTruth);
|
|
2363
|
+
const directPeerTruth = readObjectRecord(sourceOfTruth.directPeerTruth);
|
|
2364
|
+
for (const entry of Array.isArray(directPeerTruth.unavailableNodeIds) ? directPeerTruth.unavailableNodeIds : []) {
|
|
2365
|
+
const nodeId = readStringValue(entry);
|
|
2366
|
+
if (nodeId) unavailableNodeIds.add(nodeId);
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
const nodes = snapshot.nodes.map((statusNode: any) => {
|
|
2370
|
+
const nodeId = readStringValue(statusNode?.nodeId, statusNode?.id);
|
|
2371
|
+
const inlineNode = nodeId ? inlineNodesById.get(nodeId) : undefined;
|
|
2372
|
+
if (!inlineNode) return statusNode;
|
|
2373
|
+
const liveGit = buildInlineMeshTransitGitStatus(inlineNode);
|
|
2374
|
+
if (!liveGit) return statusNode;
|
|
2375
|
+
const nextStatus = { ...statusNode };
|
|
2376
|
+
nextStatus.git = liveGit;
|
|
2377
|
+
nextStatus.health = deriveMeshNodeHealthFromGit(liveGit);
|
|
2378
|
+
applyInlineMeshBranchConvergence(mesh, inlineNode, nextStatus);
|
|
2379
|
+
nextStatus.launchReady = readBooleanValue(nextStatus.launchReady) ?? true;
|
|
2380
|
+
const connection = readObjectRecord(nextStatus.connection);
|
|
2381
|
+
const connectionState = readStringValue(connection.state);
|
|
2382
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
2383
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
2384
|
+
nextStatus.connection = buildLivePeerGitConnection(connection);
|
|
2385
|
+
}
|
|
2386
|
+
delete nextStatus.gitProbePending;
|
|
2387
|
+
const error = readStringValue(nextStatus.error);
|
|
2388
|
+
if (error && /pending_git|git probe|live peer git snapshot|no peer git snapshot/i.test(error)) delete nextStatus.error;
|
|
2389
|
+
if (!readStringValue(nextStatus.machineStatus)) nextStatus.machineStatus = 'online';
|
|
2390
|
+
if (nodeId) unavailableNodeIds.delete(nodeId);
|
|
2391
|
+
changed = true;
|
|
2392
|
+
return nextStatus;
|
|
2393
|
+
});
|
|
2394
|
+
|
|
2395
|
+
const aggregateDirectTruthSatisfied = sourceOfTruth.coordinatorOwnsLiveTruth === true
|
|
2396
|
+
|| directPeerTruth.satisfied === true;
|
|
2397
|
+
if (!changed && !(options?.requireDirectPeerTruth && unavailableNodeIds.size > 0 && !aggregateDirectTruthSatisfied)) return snapshot;
|
|
2398
|
+
const nextSourceOfTruth = {
|
|
2399
|
+
...sourceOfTruth,
|
|
2400
|
+
...(Object.keys(directPeerTruth).length ? {
|
|
2401
|
+
directPeerTruth: {
|
|
2402
|
+
...directPeerTruth,
|
|
2403
|
+
satisfied: options?.requireDirectPeerTruth === true
|
|
2404
|
+
? aggregateDirectTruthSatisfied || unavailableNodeIds.size === 0
|
|
2405
|
+
: directPeerTruth.satisfied,
|
|
2406
|
+
unavailableNodeIds: [...unavailableNodeIds],
|
|
2407
|
+
},
|
|
2408
|
+
...(options?.requireDirectPeerTruth === true ? {
|
|
2409
|
+
coordinatorOwnsLiveTruth: aggregateDirectTruthSatisfied || unavailableNodeIds.size === 0,
|
|
2410
|
+
currentStatus: aggregateDirectTruthSatisfied || unavailableNodeIds.size === 0 ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
|
|
2411
|
+
} : {}),
|
|
2412
|
+
} : {}),
|
|
2413
|
+
};
|
|
2414
|
+
return {
|
|
2415
|
+
...snapshot,
|
|
2416
|
+
...(options?.requireDirectPeerTruth === true && unavailableNodeIds.size > 0 && !aggregateDirectTruthSatisfied ? {
|
|
2417
|
+
success: false,
|
|
2418
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
2419
|
+
error: 'Selected coordinator could not confirm direct mesh truth for every remote node yet.',
|
|
2420
|
+
} : {}),
|
|
2421
|
+
sourceOfTruth: nextSourceOfTruth,
|
|
2422
|
+
branchConvergenceSummary: summarizeInlineMeshBranchConvergence(nodes),
|
|
2423
|
+
nodes,
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
private getCachedAggregateMeshStatus(meshId: string, mesh?: any, options?: { requireDirectPeerTruth?: boolean }): any | null {
|
|
2428
|
+
const cached = this.aggregateMeshStatusCache.get(meshId);
|
|
2429
|
+
if (!cached?.snapshot || cached.snapshot.success !== true || !Array.isArray(cached.snapshot.nodes)) return null;
|
|
2430
|
+
if (cached.queueRevision !== getMeshQueueRevision(meshId)) return null;
|
|
2431
|
+
let snapshot = this.cloneJsonValue(cached.snapshot);
|
|
2432
|
+
snapshot = this.hydrateCachedAggregateMeshStatusFromInline(snapshot, mesh, options);
|
|
2433
|
+
if (shouldRefreshStalePendingAggregate(snapshot, options)) return null;
|
|
2434
|
+
const ageMs = Math.max(0, Date.now() - cached.builtAt);
|
|
2435
|
+
const sourceOfTruth = snapshot.sourceOfTruth && typeof snapshot.sourceOfTruth === 'object'
|
|
2436
|
+
? snapshot.sourceOfTruth
|
|
2437
|
+
: {};
|
|
2438
|
+
snapshot.sourceOfTruth = {
|
|
2439
|
+
...sourceOfTruth,
|
|
2440
|
+
aggregateSnapshot: {
|
|
2441
|
+
...(sourceOfTruth.aggregateSnapshot && typeof sourceOfTruth.aggregateSnapshot === 'object'
|
|
2442
|
+
? sourceOfTruth.aggregateSnapshot
|
|
2443
|
+
: {}),
|
|
2444
|
+
owner: 'coordinator_daemon_memory',
|
|
2445
|
+
cached: true,
|
|
2446
|
+
source: 'memory',
|
|
2447
|
+
refreshReason: 'memory_cache_hit',
|
|
2448
|
+
ageMs,
|
|
2449
|
+
cachedAt: new Date(cached.builtAt).toISOString(),
|
|
2450
|
+
returnedAt: new Date().toISOString(),
|
|
2451
|
+
},
|
|
2452
|
+
};
|
|
2453
|
+
return snapshot;
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
private rememberAggregateMeshStatus(meshId: string, snapshot: any, refreshReason: string): any {
|
|
2457
|
+
if (!snapshot || typeof snapshot !== 'object' || snapshot.success !== true || !Array.isArray(snapshot.nodes)) return snapshot;
|
|
2458
|
+
const builtAt = Date.now();
|
|
2459
|
+
const next = this.cloneJsonValue(snapshot);
|
|
2460
|
+
const sourceOfTruth = next.sourceOfTruth && typeof next.sourceOfTruth === 'object'
|
|
2461
|
+
? next.sourceOfTruth
|
|
2462
|
+
: {};
|
|
2463
|
+
next.sourceOfTruth = {
|
|
2464
|
+
...sourceOfTruth,
|
|
2465
|
+
aggregateSnapshot: {
|
|
2466
|
+
owner: 'coordinator_daemon_memory',
|
|
2467
|
+
cached: false,
|
|
2468
|
+
source: 'live_refresh',
|
|
2469
|
+
refreshReason,
|
|
2470
|
+
ageMs: 0,
|
|
2471
|
+
cachedAt: new Date(builtAt).toISOString(),
|
|
2472
|
+
returnedAt: new Date(builtAt).toISOString(),
|
|
2473
|
+
},
|
|
2474
|
+
};
|
|
2475
|
+
this.aggregateMeshStatusCache.set(meshId, { builtAt, snapshot: this.cloneJsonValue(next), queueRevision: getMeshQueueRevision(meshId) });
|
|
2476
|
+
return next;
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
public getCachedInlineMeshNodes(): any[] {
|
|
2480
|
+
const nodes: any[] = [];
|
|
2481
|
+
for (const mesh of this.inlineMeshCache.values()) {
|
|
2482
|
+
if (Array.isArray(mesh?.nodes)) {
|
|
2483
|
+
nodes.push(...mesh.nodes);
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
return nodes;
|
|
2487
|
+
}
|
|
2488
|
+
|
|
954
2489
|
public getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
955
2490
|
if (inlineMesh && typeof inlineMesh === 'object') {
|
|
956
2491
|
return this.warmInlineMeshCache(meshId, inlineMesh);
|
|
@@ -960,10 +2495,15 @@ export class DaemonCommandRouter {
|
|
|
960
2495
|
|
|
961
2496
|
private warmInlineMeshCache(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
962
2497
|
if (!inlineMesh || typeof inlineMesh !== 'object') return undefined;
|
|
2498
|
+
const sanitizedInlineMesh = sanitizeInlineMesh(inlineMesh as any);
|
|
963
2499
|
const cached = this.inlineMeshCache.get(meshId);
|
|
964
|
-
if (cached)
|
|
965
|
-
|
|
966
|
-
|
|
2500
|
+
if (cached) {
|
|
2501
|
+
const merged = reconcileInlineMeshCache(cached, sanitizedInlineMesh);
|
|
2502
|
+
this.inlineMeshCache.set(meshId, merged);
|
|
2503
|
+
return merged;
|
|
2504
|
+
}
|
|
2505
|
+
this.inlineMeshCache.set(meshId, sanitizedInlineMesh as any);
|
|
2506
|
+
return sanitizedInlineMesh as any;
|
|
967
2507
|
}
|
|
968
2508
|
|
|
969
2509
|
private async getMeshForCommand(
|
|
@@ -974,9 +2514,18 @@ export class DaemonCommandRouter {
|
|
|
974
2514
|
const preferInline = options?.preferInline === true;
|
|
975
2515
|
if (preferInline) {
|
|
976
2516
|
const cached = this.getCachedInlineMesh(meshId);
|
|
977
|
-
if (cached)
|
|
978
|
-
|
|
979
|
-
|
|
2517
|
+
if (cached) {
|
|
2518
|
+
if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
|
|
2519
|
+
const merged = reconcileInlineMeshCache(cached, inlineMesh as any);
|
|
2520
|
+
this.inlineMeshCache.set(meshId, sanitizeInlineMesh(merged));
|
|
2521
|
+
return { mesh: merged, inline: true, source: 'inline_cache' };
|
|
2522
|
+
}
|
|
2523
|
+
return { mesh: cached, inline: true, source: 'inline_cache' };
|
|
2524
|
+
}
|
|
2525
|
+
if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
|
|
2526
|
+
this.warmInlineMeshCache(meshId, inlineMesh);
|
|
2527
|
+
return { mesh: inlineMesh, inline: true, source: 'inline_bootstrap' };
|
|
2528
|
+
}
|
|
980
2529
|
}
|
|
981
2530
|
try {
|
|
982
2531
|
const { getMesh } = await import('../config/mesh-config.js');
|
|
@@ -989,6 +2538,23 @@ export class DaemonCommandRouter {
|
|
|
989
2538
|
return warmedInline ? { mesh: warmedInline, inline: true, source: 'inline_bootstrap' } : null;
|
|
990
2539
|
}
|
|
991
2540
|
|
|
2541
|
+
private invalidateAggregateMeshStatus(meshId: string): void {
|
|
2542
|
+
this.aggregateMeshStatusCache.delete(meshId);
|
|
2543
|
+
this.deps.onMeshStateChange?.(meshId);
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
|
|
2547
|
+
private async requireMeshHostMutationOwner(meshId: string, inlineMesh: unknown, operation: string): Promise<CommandRouterResult | null> {
|
|
2548
|
+
const meshRecord = await this.getMeshForCommand(meshId, inlineMesh, { preferInline: true });
|
|
2549
|
+
const mesh = meshRecord?.mesh;
|
|
2550
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
2551
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
2552
|
+
if (!meshHost.canOwnCoordinator || !meshHost.canOwnQueue) {
|
|
2553
|
+
return { ...buildMeshHostRequiredFailure(mesh, operation), success: false, meshId };
|
|
2554
|
+
}
|
|
2555
|
+
return null;
|
|
2556
|
+
}
|
|
2557
|
+
|
|
992
2558
|
private updateInlineMeshNode(meshId: string, mesh: any, node: any): void {
|
|
993
2559
|
if (!mesh || !Array.isArray(mesh.nodes) || !node?.id) return;
|
|
994
2560
|
const idx = mesh.nodes.findIndex((entry: any) => entry?.id === node.id || entry?.nodeId === node.id);
|
|
@@ -996,6 +2562,7 @@ export class DaemonCommandRouter {
|
|
|
996
2562
|
else mesh.nodes.push(node);
|
|
997
2563
|
mesh.updatedAt = new Date().toISOString();
|
|
998
2564
|
this.inlineMeshCache.set(meshId, mesh);
|
|
2565
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
999
2566
|
}
|
|
1000
2567
|
|
|
1001
2568
|
private removeInlineMeshNode(meshId: string, mesh: any, nodeId: string): boolean {
|
|
@@ -1005,6 +2572,7 @@ export class DaemonCommandRouter {
|
|
|
1005
2572
|
mesh.nodes.splice(idx, 1);
|
|
1006
2573
|
mesh.updatedAt = new Date().toISOString();
|
|
1007
2574
|
this.inlineMeshCache.set(meshId, mesh);
|
|
2575
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
1008
2576
|
return true;
|
|
1009
2577
|
}
|
|
1010
2578
|
|
|
@@ -1534,6 +3102,567 @@ export class DaemonCommandRouter {
|
|
|
1534
3102
|
}
|
|
1535
3103
|
}
|
|
1536
3104
|
|
|
3105
|
+
|
|
3106
|
+
private buildRefineJobKey(meshId: string, nodeId: string): string {
|
|
3107
|
+
return `${meshId}:${nodeId}`;
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
private buildRefineJobHandle(args: {
|
|
3111
|
+
meshId: string;
|
|
3112
|
+
nodeId: string;
|
|
3113
|
+
node?: any;
|
|
3114
|
+
status?: MeshRefineAsyncJobStatus;
|
|
3115
|
+
startedAt?: string;
|
|
3116
|
+
completedAt?: string;
|
|
3117
|
+
jobId?: string;
|
|
3118
|
+
interactionId?: string;
|
|
3119
|
+
retryOfJobId?: string;
|
|
3120
|
+
}): MeshRefineJobHandle {
|
|
3121
|
+
return {
|
|
3122
|
+
success: true,
|
|
3123
|
+
async: true,
|
|
3124
|
+
status: args.status || 'accepted',
|
|
3125
|
+
jobId: args.jobId || `refine_${createInteractionId()}`,
|
|
3126
|
+
interactionId: args.interactionId || createInteractionId(),
|
|
3127
|
+
meshId: args.meshId,
|
|
3128
|
+
nodeId: args.nodeId,
|
|
3129
|
+
targetNodeId: args.nodeId,
|
|
3130
|
+
targetDaemonId: readStringValue(args.node?.daemonId),
|
|
3131
|
+
workspace: readStringValue(args.node?.workspace),
|
|
3132
|
+
startedAt: args.startedAt || new Date().toISOString(),
|
|
3133
|
+
...(args.completedAt ? { completedAt: args.completedAt } : {}),
|
|
3134
|
+
...(args.retryOfJobId ? { retryOfJobId: args.retryOfJobId } : {}),
|
|
3135
|
+
eventDelivery: { pendingEvents: true, ledger: true },
|
|
3136
|
+
evidence: {
|
|
3137
|
+
pendingEventsCommand: 'get_pending_mesh_events',
|
|
3138
|
+
ledgerCommand: 'get_mesh_ledger_slice',
|
|
3139
|
+
taskHistoryKind: args.status === 'completed' ? 'task_completed' : args.status === 'failed' ? 'task_failed' : 'task_dispatched',
|
|
3140
|
+
},
|
|
3141
|
+
};
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
private queueRefineJobEvent(event: 'refine:accepted' | 'refine:completed' | 'refine:failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): void {
|
|
3145
|
+
const metadataEvent = {
|
|
3146
|
+
source: 'refine_mesh_node_async_job',
|
|
3147
|
+
jobId: handle.jobId,
|
|
3148
|
+
interactionId: handle.interactionId,
|
|
3149
|
+
meshId: handle.meshId,
|
|
3150
|
+
nodeId: handle.targetNodeId,
|
|
3151
|
+
targetDaemonId: handle.targetDaemonId,
|
|
3152
|
+
workspace: handle.workspace,
|
|
3153
|
+
status: handle.status,
|
|
3154
|
+
startedAt: handle.startedAt,
|
|
3155
|
+
completedAt: handle.completedAt,
|
|
3156
|
+
retryOfJobId: handle.retryOfJobId,
|
|
3157
|
+
...(result ? { result } : {}),
|
|
3158
|
+
};
|
|
3159
|
+
const eventPayload = {
|
|
3160
|
+
event,
|
|
3161
|
+
meshId: handle.meshId,
|
|
3162
|
+
nodeLabel: handle.targetNodeId,
|
|
3163
|
+
nodeId: handle.targetNodeId,
|
|
3164
|
+
workspace: handle.workspace,
|
|
3165
|
+
metadataEvent,
|
|
3166
|
+
queuedAt: Date.now(),
|
|
3167
|
+
};
|
|
3168
|
+
if (typeof this.deps.instanceManager?.getByCategory === 'function') {
|
|
3169
|
+
const forwarded = handleMeshForwardEvent(
|
|
3170
|
+
{ instanceManager: this.deps.instanceManager } as any,
|
|
3171
|
+
{
|
|
3172
|
+
event,
|
|
3173
|
+
meshId: handle.meshId,
|
|
3174
|
+
nodeId: handle.targetNodeId,
|
|
3175
|
+
workspace: handle.workspace,
|
|
3176
|
+
jobId: handle.jobId,
|
|
3177
|
+
interactionId: handle.interactionId,
|
|
3178
|
+
status: handle.status,
|
|
3179
|
+
targetDaemonId: handle.targetDaemonId,
|
|
3180
|
+
startedAt: handle.startedAt,
|
|
3181
|
+
completedAt: handle.completedAt,
|
|
3182
|
+
retryOfJobId: handle.retryOfJobId,
|
|
3183
|
+
...(result ? { result } : {}),
|
|
3184
|
+
},
|
|
3185
|
+
);
|
|
3186
|
+
if (forwarded?.success === true) return;
|
|
3187
|
+
LOG.warn('Mesh', `[Refinery] Failed to forward async refine event ${event}: ${forwarded?.error || 'unknown error'}`);
|
|
3188
|
+
}
|
|
3189
|
+
queuePendingMeshCoordinatorEvent(eventPayload);
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
private async appendRefineJobLedger(kind: 'task_dispatched' | 'task_completed' | 'task_failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): Promise<void> {
|
|
3193
|
+
try {
|
|
3194
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
3195
|
+
appendLedgerEntry(handle.meshId, {
|
|
3196
|
+
kind,
|
|
3197
|
+
nodeId: handle.targetNodeId,
|
|
3198
|
+
payload: {
|
|
3199
|
+
source: 'refine_mesh_node_async_job',
|
|
3200
|
+
refineJob: {
|
|
3201
|
+
jobId: handle.jobId,
|
|
3202
|
+
interactionId: handle.interactionId,
|
|
3203
|
+
status: handle.status,
|
|
3204
|
+
meshId: handle.meshId,
|
|
3205
|
+
nodeId: handle.targetNodeId,
|
|
3206
|
+
targetDaemonId: handle.targetDaemonId,
|
|
3207
|
+
workspace: handle.workspace,
|
|
3208
|
+
startedAt: handle.startedAt,
|
|
3209
|
+
completedAt: handle.completedAt,
|
|
3210
|
+
retryOfJobId: handle.retryOfJobId,
|
|
3211
|
+
},
|
|
3212
|
+
async: true,
|
|
3213
|
+
retryOfJobId: handle.retryOfJobId,
|
|
3214
|
+
...(result ? {
|
|
3215
|
+
success: result.success === true,
|
|
3216
|
+
result,
|
|
3217
|
+
finalBranchConvergenceState: result.finalBranchConvergenceState,
|
|
3218
|
+
} : {}),
|
|
3219
|
+
},
|
|
3220
|
+
});
|
|
3221
|
+
} catch (e: any) {
|
|
3222
|
+
LOG.warn('Mesh', `[Refinery] Failed to append async refine ledger entry: ${e?.message || e}`);
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
|
|
3226
|
+
private async executeMeshRefineNodeSynchronously(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
3227
|
+
const refineStages: Array<Record<string, unknown>> = [];
|
|
3228
|
+
try {
|
|
3229
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
3230
|
+
const mesh = meshRecord?.mesh;
|
|
3231
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
3232
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages };
|
|
3233
|
+
|
|
3234
|
+
if (!node.isLocalWorktree || !node.workspace) {
|
|
3235
|
+
return { success: false, error: `Refinery requires a local worktree node`, refineStages };
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
const sourceNode = node.clonedFromNodeId
|
|
3239
|
+
? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
|
|
3240
|
+
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
3241
|
+
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
3242
|
+
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found', refineStages };
|
|
3243
|
+
|
|
3244
|
+
const { execFile } = await import('node:child_process');
|
|
3245
|
+
const { promisify } = await import('node:util');
|
|
3246
|
+
const execFileAsync = promisify(execFile);
|
|
3247
|
+
|
|
3248
|
+
const resolveStarted = Date.now();
|
|
3249
|
+
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
3250
|
+
const branch = branchStdout.trim();
|
|
3251
|
+
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node', refineStages };
|
|
3252
|
+
|
|
3253
|
+
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
3254
|
+
const baseBranch = baseBranchStdout.trim();
|
|
3255
|
+
const { stdout: baseHeadStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });
|
|
3256
|
+
const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
|
|
3257
|
+
const baseHead = baseHeadStdout.trim();
|
|
3258
|
+
const branchHead = branchHeadStdout.trim();
|
|
3259
|
+
recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, { branch, baseBranch, baseHead, branchHead });
|
|
3260
|
+
|
|
3261
|
+
const validationStarted = Date.now();
|
|
3262
|
+
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
|
|
3263
|
+
recordMeshRefineStage(
|
|
3264
|
+
refineStages,
|
|
3265
|
+
'validation',
|
|
3266
|
+
validationSummary.status === 'passed' ? 'passed' : validationSummary.status === 'failed' ? 'failed' : 'skipped',
|
|
3267
|
+
validationStarted,
|
|
3268
|
+
{ validationStatus: validationSummary.status, commandsRun: validationSummary.commandsRun.length },
|
|
3269
|
+
);
|
|
3270
|
+
if (validationSummary.status === 'failed') {
|
|
3271
|
+
return {
|
|
3272
|
+
success: false,
|
|
3273
|
+
code: validationSummary.failureCode || 'validation_failed',
|
|
3274
|
+
convergenceStatus: 'blocked_review',
|
|
3275
|
+
error: validationSummary.failureCode === 'missing_dependencies'
|
|
3276
|
+
? 'Refinery validation dependencies are missing; merge/refine was not attempted. Configure validation.bootstrapCommands if Refinery should bootstrap dependencies before validation.'
|
|
3277
|
+
: validationSummary.failureCode === 'dependency_bootstrap_failed'
|
|
3278
|
+
? 'Refinery dependency/bootstrap command failed; merge/refine was not attempted.'
|
|
3279
|
+
: 'Refinery validation gate failed; merge/refine was not attempted.',
|
|
3280
|
+
branch,
|
|
3281
|
+
into: baseBranch,
|
|
3282
|
+
validationSummary,
|
|
3283
|
+
refineStages,
|
|
3284
|
+
finalBranchConvergenceState: {
|
|
3285
|
+
branch,
|
|
3286
|
+
baseBranch,
|
|
3287
|
+
merged: false,
|
|
3288
|
+
removed: false,
|
|
3289
|
+
validation: 'failed',
|
|
3290
|
+
status: 'blocked_review',
|
|
3291
|
+
},
|
|
3292
|
+
};
|
|
3293
|
+
}
|
|
3294
|
+
if (validationSummary.status === 'skipped') {
|
|
3295
|
+
return {
|
|
3296
|
+
success: false,
|
|
3297
|
+
code: 'validation_unavailable',
|
|
3298
|
+
convergenceStatus: 'blocked_review',
|
|
3299
|
+
error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
|
|
3300
|
+
branch,
|
|
3301
|
+
into: baseBranch,
|
|
3302
|
+
validationSummary,
|
|
3303
|
+
refineStages,
|
|
3304
|
+
finalBranchConvergenceState: {
|
|
3305
|
+
branch,
|
|
3306
|
+
baseBranch,
|
|
3307
|
+
merged: false,
|
|
3308
|
+
removed: false,
|
|
3309
|
+
validation: 'unavailable',
|
|
3310
|
+
status: 'blocked_review',
|
|
3311
|
+
},
|
|
3312
|
+
};
|
|
3313
|
+
}
|
|
3314
|
+
|
|
3315
|
+
const patchEquivalenceStarted = Date.now();
|
|
3316
|
+
const patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
|
|
3317
|
+
recordMeshRefineStage(refineStages, 'patch_equivalence', patchEquivalence.status, patchEquivalenceStarted, {
|
|
3318
|
+
equivalent: patchEquivalence.equivalent,
|
|
3319
|
+
expectedPatchId: patchEquivalence.expectedPatchId,
|
|
3320
|
+
actualPatchId: patchEquivalence.actualPatchId,
|
|
3321
|
+
error: patchEquivalence.error,
|
|
3322
|
+
actionableHint: patchEquivalence.actionableHint,
|
|
3323
|
+
});
|
|
3324
|
+
if (!patchEquivalence.equivalent) {
|
|
3325
|
+
return {
|
|
3326
|
+
success: false,
|
|
3327
|
+
code: 'patch_equivalence_failed',
|
|
3328
|
+
convergenceStatus: 'blocked_review',
|
|
3329
|
+
error: 'Refinery patch-equivalence preflight failed; merge/refine was not attempted.',
|
|
3330
|
+
branch,
|
|
3331
|
+
into: baseBranch,
|
|
3332
|
+
validationSummary,
|
|
3333
|
+
patchEquivalence,
|
|
3334
|
+
refineStages,
|
|
3335
|
+
finalBranchConvergenceState: {
|
|
3336
|
+
branch,
|
|
3337
|
+
baseBranch,
|
|
3338
|
+
merged: false,
|
|
3339
|
+
removed: false,
|
|
3340
|
+
validation: 'passed',
|
|
3341
|
+
patchEquivalence: 'failed',
|
|
3342
|
+
status: 'blocked_review',
|
|
3343
|
+
},
|
|
3344
|
+
};
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
const submoduleReachabilityStarted = Date.now();
|
|
3348
|
+
const autoPublishSubmoduleMainCommits = resolveRefineryAutoPublishSubmoduleMainCommits(mesh, node.workspace);
|
|
3349
|
+
const submoduleReachability = await runMeshRefineSubmoduleReachabilityGate(repoRoot, patchEquivalence.mergedTree || branchHead, {
|
|
3350
|
+
allowAutoPublishSubmoduleMainCommits: autoPublishSubmoduleMainCommits.enabled,
|
|
3351
|
+
autoPublishPolicySource: autoPublishSubmoduleMainCommits.source,
|
|
3352
|
+
worktreeRoot: node.workspace,
|
|
3353
|
+
});
|
|
3354
|
+
recordMeshRefineStage(refineStages, 'submodule_reachability', submoduleReachability.status, submoduleReachabilityStarted, {
|
|
3355
|
+
checked: submoduleReachability.checked,
|
|
3356
|
+
autoPublishAllowed: submoduleReachability.autoPublishAllowed,
|
|
3357
|
+
autoPublishPolicySource: submoduleReachability.autoPublishPolicySource,
|
|
3358
|
+
autoPublished: submoduleReachability.entries
|
|
3359
|
+
.filter(entry => entry.autoPublishAttempted)
|
|
3360
|
+
.map(entry => ({
|
|
3361
|
+
path: entry.path,
|
|
3362
|
+
commit: entry.commit,
|
|
3363
|
+
remote: entry.remote,
|
|
3364
|
+
remoteUrl: entry.remoteUrl,
|
|
3365
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
3366
|
+
refspec: entry.autoPublishRefspec,
|
|
3367
|
+
succeeded: entry.autoPublishSucceeded,
|
|
3368
|
+
verified: entry.autoPublishVerified,
|
|
3369
|
+
remoteMainReachable: entry.remoteMainReachable,
|
|
3370
|
+
error: entry.error,
|
|
3371
|
+
})),
|
|
3372
|
+
autoPublishSkipped: submoduleReachability.entries
|
|
3373
|
+
.filter(entry => entry.autoPublishAllowed === true && entry.autoPublishAttempted !== true)
|
|
3374
|
+
.map(entry => ({
|
|
3375
|
+
path: entry.path,
|
|
3376
|
+
commit: entry.commit,
|
|
3377
|
+
remote: entry.remote,
|
|
3378
|
+
remoteUrl: entry.remoteUrl,
|
|
3379
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
3380
|
+
reason: entry.autoPublishSkippedReason || entry.error || 'auto-publish was allowed but no publish attempt was possible',
|
|
3381
|
+
})),
|
|
3382
|
+
unreachable: submoduleReachability.unreachable.map(entry => ({
|
|
3383
|
+
path: entry.path,
|
|
3384
|
+
commit: entry.commit,
|
|
3385
|
+
publishRequired: entry.publishRequired === true,
|
|
3386
|
+
autoPublishAllowed: entry.autoPublishAllowed,
|
|
3387
|
+
autoPublishAttempted: entry.autoPublishAttempted,
|
|
3388
|
+
autoPublishSucceeded: entry.autoPublishSucceeded,
|
|
3389
|
+
autoPublishVerified: entry.autoPublishVerified,
|
|
3390
|
+
autoPublishRefspec: entry.autoPublishRefspec,
|
|
3391
|
+
autoPublishSkippedReason: entry.autoPublishSkippedReason,
|
|
3392
|
+
remote: entry.remote,
|
|
3393
|
+
remoteUrl: entry.remoteUrl,
|
|
3394
|
+
remoteReachable: entry.remoteReachable,
|
|
3395
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
3396
|
+
remoteMainReachable: entry.remoteMainReachable,
|
|
3397
|
+
error: entry.error,
|
|
3398
|
+
})),
|
|
3399
|
+
error: submoduleReachability.error,
|
|
3400
|
+
});
|
|
3401
|
+
if (submoduleReachability.status === 'failed') {
|
|
3402
|
+
const nextStep = buildSubmodulePublishRequiredNextStep(submoduleReachability.unreachable);
|
|
3403
|
+
return {
|
|
3404
|
+
success: false,
|
|
3405
|
+
code: 'submodule_reachability_failed',
|
|
3406
|
+
convergenceStatus: 'blocked_review',
|
|
3407
|
+
publishRequired: true,
|
|
3408
|
+
blockedReason: 'submodule_publish_required',
|
|
3409
|
+
error: 'Refinery submodule reachability preflight failed because one or more submodule gitlink commits are not reachable from their configured remote main branch; merge/refine cleanup was not attempted.',
|
|
3410
|
+
nextStep,
|
|
3411
|
+
nextSteps: [
|
|
3412
|
+
'Ask the user for explicit approval before pushing or publishing any submodule commit.',
|
|
3413
|
+
'Push/publish each unreachable submodule commit to the configured submodule remote main branch shown in the evidence.',
|
|
3414
|
+
'Rerun mesh_refine_node after remote reachability is confirmed.',
|
|
3415
|
+
'Do not merge the root branch until every submodule gitlink commit is reachable from submodule origin/main.',
|
|
3416
|
+
],
|
|
3417
|
+
unreachableSubmoduleCommits: submoduleReachability.unreachable.map(entry => ({
|
|
3418
|
+
path: entry.path,
|
|
3419
|
+
commit: entry.commit,
|
|
3420
|
+
remote: entry.remote,
|
|
3421
|
+
remoteUrl: entry.remoteUrl,
|
|
3422
|
+
remoteReachable: entry.remoteReachable,
|
|
3423
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
3424
|
+
remoteMainReachable: entry.remoteMainReachable,
|
|
3425
|
+
autoPublishAllowed: entry.autoPublishAllowed,
|
|
3426
|
+
autoPublishAttempted: entry.autoPublishAttempted,
|
|
3427
|
+
autoPublishSucceeded: entry.autoPublishSucceeded,
|
|
3428
|
+
autoPublishVerified: entry.autoPublishVerified,
|
|
3429
|
+
autoPublishRefspec: entry.autoPublishRefspec,
|
|
3430
|
+
autoPublishSkippedReason: entry.autoPublishSkippedReason,
|
|
3431
|
+
error: entry.error,
|
|
3432
|
+
})),
|
|
3433
|
+
branch,
|
|
3434
|
+
into: baseBranch,
|
|
3435
|
+
validationSummary,
|
|
3436
|
+
patchEquivalence,
|
|
3437
|
+
submoduleReachability,
|
|
3438
|
+
refineStages,
|
|
3439
|
+
finalBranchConvergenceState: {
|
|
3440
|
+
branch,
|
|
3441
|
+
baseBranch,
|
|
3442
|
+
merged: false,
|
|
3443
|
+
removed: false,
|
|
3444
|
+
validation: 'passed',
|
|
3445
|
+
patchEquivalence: 'passed',
|
|
3446
|
+
submoduleReachability: 'failed',
|
|
3447
|
+
status: 'blocked_review',
|
|
3448
|
+
reason: 'submodule_publish_required',
|
|
3449
|
+
nextStep,
|
|
3450
|
+
},
|
|
3451
|
+
};
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
let mergeResult: Record<string, unknown> | undefined;
|
|
3455
|
+
const mergeStarted = Date.now();
|
|
3456
|
+
try {
|
|
3457
|
+
const result = await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
3458
|
+
mergeResult = {
|
|
3459
|
+
stdout: truncateValidationOutput(result.stdout),
|
|
3460
|
+
stderr: truncateValidationOutput(result.stderr),
|
|
3461
|
+
durationMs: Date.now() - mergeStarted,
|
|
3462
|
+
};
|
|
3463
|
+
recordMeshRefineStage(refineStages, 'merge', 'passed', mergeStarted, mergeResult);
|
|
3464
|
+
} catch (e: any) {
|
|
3465
|
+
recordMeshRefineStage(refineStages, 'merge', 'failed', mergeStarted, {
|
|
3466
|
+
error: e?.message || String(e),
|
|
3467
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
3468
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
3469
|
+
});
|
|
3470
|
+
return {
|
|
3471
|
+
success: false,
|
|
3472
|
+
error: `Merge failed (conflicts?): ${e.message}`,
|
|
3473
|
+
validationSummary,
|
|
3474
|
+
patchEquivalence,
|
|
3475
|
+
refineStages,
|
|
3476
|
+
finalBranchConvergenceState: {
|
|
3477
|
+
branch,
|
|
3478
|
+
baseBranch,
|
|
3479
|
+
merged: false,
|
|
3480
|
+
removed: false,
|
|
3481
|
+
validation: 'passed',
|
|
3482
|
+
patchEquivalence: 'passed',
|
|
3483
|
+
status: 'not_mergeable',
|
|
3484
|
+
},
|
|
3485
|
+
};
|
|
3486
|
+
}
|
|
3487
|
+
|
|
3488
|
+
const submoduleAlignmentStarted = Date.now();
|
|
3489
|
+
const submoduleAlignment = await alignRefinerySubmodulesAfterMerge(repoRoot, baseHead, 'HEAD', {
|
|
3490
|
+
submoduleIgnorePaths: Array.isArray(sourceNode?.policy?.submoduleIgnorePaths)
|
|
3491
|
+
? sourceNode.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
|
|
3492
|
+
: undefined,
|
|
3493
|
+
});
|
|
3494
|
+
if (submoduleAlignment.status !== 'skipped') {
|
|
3495
|
+
recordMeshRefineStage(refineStages, 'submodule_alignment', submoduleAlignment.status, submoduleAlignmentStarted, {
|
|
3496
|
+
changedGitlinkPaths: submoduleAlignment.changedGitlinkPaths,
|
|
3497
|
+
outOfSyncPaths: submoduleAlignment.outOfSyncPaths,
|
|
3498
|
+
updatedPaths: submoduleAlignment.updatedPaths,
|
|
3499
|
+
verifiedPaths: submoduleAlignment.verifiedPaths,
|
|
3500
|
+
command: submoduleAlignment.command,
|
|
3501
|
+
error: submoduleAlignment.error,
|
|
3502
|
+
});
|
|
3503
|
+
}
|
|
3504
|
+
if (submoduleAlignment.status === 'failed') {
|
|
3505
|
+
return {
|
|
3506
|
+
success: false,
|
|
3507
|
+
code: 'post_merge_submodule_alignment_failed',
|
|
3508
|
+
error: 'Refinery merge completed but post-merge submodule checkout alignment failed; run the reported git submodule update command and re-check base workspace status.',
|
|
3509
|
+
merged: true,
|
|
3510
|
+
branch,
|
|
3511
|
+
into: baseBranch,
|
|
3512
|
+
validationSummary,
|
|
3513
|
+
patchEquivalence,
|
|
3514
|
+
submoduleReachability,
|
|
3515
|
+
submoduleAlignment,
|
|
3516
|
+
mergeResult,
|
|
3517
|
+
refineStages,
|
|
3518
|
+
finalBranchConvergenceState: {
|
|
3519
|
+
branch: baseBranch,
|
|
3520
|
+
mergedBranch: branch,
|
|
3521
|
+
baseBranch,
|
|
3522
|
+
merged: true,
|
|
3523
|
+
removed: false,
|
|
3524
|
+
validation: 'passed',
|
|
3525
|
+
patchEquivalence: 'passed',
|
|
3526
|
+
submoduleReachability: 'passed',
|
|
3527
|
+
submoduleAlignment: 'failed',
|
|
3528
|
+
status: 'post_merge_alignment_failed',
|
|
3529
|
+
nextStep: submoduleAlignment.command || 'Run git submodule update --init --recursive for the reported path(s), then re-check base workspace status.',
|
|
3530
|
+
},
|
|
3531
|
+
};
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
const cleanupStarted = Date.now();
|
|
3535
|
+
const removeResult = await this.execute('remove_mesh_node', {
|
|
3536
|
+
meshId,
|
|
3537
|
+
nodeId,
|
|
3538
|
+
sessionCleanupMode: 'preserve',
|
|
3539
|
+
inlineMesh: args?.inlineMesh,
|
|
3540
|
+
});
|
|
3541
|
+
recordMeshRefineStage(refineStages, 'cleanup', removeResult?.success === false ? 'failed' : 'passed', cleanupStarted, {
|
|
3542
|
+
removed: removeResult?.removed,
|
|
3543
|
+
code: removeResult?.code,
|
|
3544
|
+
error: removeResult?.error,
|
|
3545
|
+
});
|
|
3546
|
+
|
|
3547
|
+
let ledgerError: string | undefined;
|
|
3548
|
+
const ledgerStarted = Date.now();
|
|
3549
|
+
try {
|
|
3550
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
3551
|
+
appendLedgerEntry(meshId, {
|
|
3552
|
+
kind: 'node_removed',
|
|
3553
|
+
nodeId,
|
|
3554
|
+
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary, patchEquivalence, submoduleReachability, submoduleAlignment },
|
|
3555
|
+
});
|
|
3556
|
+
recordMeshRefineStage(refineStages, 'ledger', 'passed', ledgerStarted);
|
|
3557
|
+
} catch (e: any) {
|
|
3558
|
+
ledgerError = e?.message || String(e);
|
|
3559
|
+
recordMeshRefineStage(refineStages, 'ledger', 'failed', ledgerStarted, { error: ledgerError });
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
const finalBranchConvergenceState = {
|
|
3563
|
+
branch: baseBranch,
|
|
3564
|
+
mergedBranch: branch,
|
|
3565
|
+
baseBranch,
|
|
3566
|
+
merged: true,
|
|
3567
|
+
removed: removeResult?.success !== false,
|
|
3568
|
+
validation: 'passed',
|
|
3569
|
+
patchEquivalence: 'passed',
|
|
3570
|
+
submoduleAlignment: submoduleAlignment.status,
|
|
3571
|
+
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
3572
|
+
};
|
|
3573
|
+
|
|
3574
|
+
if (removeResult?.success === false) {
|
|
3575
|
+
return {
|
|
3576
|
+
success: false,
|
|
3577
|
+
code: 'cleanup_failed',
|
|
3578
|
+
error: 'Refinery merge completed but worktree cleanup failed; manual cleanup/retry is required.',
|
|
3579
|
+
merged: true,
|
|
3580
|
+
branch,
|
|
3581
|
+
into: baseBranch,
|
|
3582
|
+
removeResult,
|
|
3583
|
+
validationSummary,
|
|
3584
|
+
patchEquivalence,
|
|
3585
|
+
submoduleReachability,
|
|
3586
|
+
submoduleAlignment,
|
|
3587
|
+
mergeResult,
|
|
3588
|
+
refineStages,
|
|
3589
|
+
...(ledgerError ? { ledgerError } : {}),
|
|
3590
|
+
finalBranchConvergenceState,
|
|
3591
|
+
};
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
return {
|
|
3595
|
+
success: true,
|
|
3596
|
+
merged: true,
|
|
3597
|
+
branch,
|
|
3598
|
+
into: baseBranch,
|
|
3599
|
+
removeResult,
|
|
3600
|
+
validationSummary,
|
|
3601
|
+
patchEquivalence,
|
|
3602
|
+
submoduleReachability,
|
|
3603
|
+
submoduleAlignment,
|
|
3604
|
+
mergeResult,
|
|
3605
|
+
refineStages,
|
|
3606
|
+
...(ledgerError ? { ledgerError } : {}),
|
|
3607
|
+
finalBranchConvergenceState,
|
|
3608
|
+
};
|
|
3609
|
+
} catch (e: any) {
|
|
3610
|
+
return { success: false, error: e.message, refineStages };
|
|
3611
|
+
}
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
private async finishMeshRefineJob(handle: MeshRefineJobHandle, args: any): Promise<void> {
|
|
3615
|
+
const key = this.buildRefineJobKey(handle.meshId, handle.targetNodeId);
|
|
3616
|
+
let result: Record<string, unknown>;
|
|
3617
|
+
try {
|
|
3618
|
+
result = await this.executeMeshRefineNodeSynchronously(handle.meshId, handle.targetNodeId, args) as Record<string, unknown>;
|
|
3619
|
+
} catch (e: any) {
|
|
3620
|
+
result = { success: false, error: e?.message || String(e) };
|
|
3621
|
+
}
|
|
3622
|
+
const completedAt = new Date().toISOString();
|
|
3623
|
+
const terminalHandle = this.buildRefineJobHandle({
|
|
3624
|
+
meshId: handle.meshId,
|
|
3625
|
+
nodeId: handle.targetNodeId,
|
|
3626
|
+
status: result.success === true ? 'completed' : 'failed',
|
|
3627
|
+
startedAt: handle.startedAt,
|
|
3628
|
+
completedAt,
|
|
3629
|
+
jobId: handle.jobId,
|
|
3630
|
+
interactionId: handle.interactionId,
|
|
3631
|
+
retryOfJobId: handle.retryOfJobId,
|
|
3632
|
+
node: { daemonId: handle.targetDaemonId, workspace: handle.workspace },
|
|
3633
|
+
});
|
|
3634
|
+
const terminal: MeshRefineTerminalJob = { ...terminalHandle, result };
|
|
3635
|
+
this.terminalRefineJobs.set(key, terminal);
|
|
3636
|
+
this.runningRefineJobs.delete(key);
|
|
3637
|
+
this.invalidateAggregateMeshStatus(handle.meshId);
|
|
3638
|
+
await this.appendRefineJobLedger(result.success === true ? 'task_completed' : 'task_failed', terminalHandle, result);
|
|
3639
|
+
this.queueRefineJobEvent(result.success === true ? 'refine:completed' : 'refine:failed', terminalHandle, result);
|
|
3640
|
+
}
|
|
3641
|
+
|
|
3642
|
+
private async startMeshRefineJob(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
3643
|
+
const key = this.buildRefineJobKey(meshId, nodeId);
|
|
3644
|
+
const running = this.runningRefineJobs.get(key);
|
|
3645
|
+
if (running) return { ...running, duplicate: true };
|
|
3646
|
+
const terminal = this.terminalRefineJobs.get(key);
|
|
3647
|
+
|
|
3648
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
3649
|
+
const mesh = meshRecord?.mesh;
|
|
3650
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
3651
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
3652
|
+
if (!node.isLocalWorktree || !node.workspace) return { success: false, error: `Refinery requires a local worktree node` };
|
|
3653
|
+
|
|
3654
|
+
const handle = this.buildRefineJobHandle({ meshId, nodeId, node, retryOfJobId: terminal?.jobId });
|
|
3655
|
+
this.runningRefineJobs.set(key, handle);
|
|
3656
|
+
await this.appendRefineJobLedger('task_dispatched', handle);
|
|
3657
|
+
this.queueRefineJobEvent('refine:accepted', handle);
|
|
3658
|
+
|
|
3659
|
+
setImmediate(() => {
|
|
3660
|
+
void this.finishMeshRefineJob(handle, args);
|
|
3661
|
+
});
|
|
3662
|
+
|
|
3663
|
+
return handle;
|
|
3664
|
+
}
|
|
3665
|
+
|
|
1537
3666
|
// ─── Daemon-level command core ───────────────────
|
|
1538
3667
|
|
|
1539
3668
|
/**
|
|
@@ -1548,13 +3677,35 @@ export class DaemonCommandRouter {
|
|
|
1548
3677
|
}
|
|
1549
3678
|
|
|
1550
3679
|
case 'get_pending_mesh_events': {
|
|
1551
|
-
const
|
|
3680
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
3681
|
+
// (B3) Respect coordinatorDaemonId when the caller declares it
|
|
3682
|
+
// so unicast events route to the right coordinator instead of
|
|
3683
|
+
// being silently consumed by the first drainer.
|
|
3684
|
+
const coordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
|
|
3685
|
+
? args.coordinatorDaemonId.trim()
|
|
3686
|
+
: undefined;
|
|
3687
|
+
const events = drainPendingMeshCoordinatorEvents(meshId || undefined, coordinatorDaemonId);
|
|
1552
3688
|
return { success: true, events };
|
|
1553
3689
|
}
|
|
1554
3690
|
|
|
3691
|
+
case 'interactive_prompt_response': {
|
|
3692
|
+
const sessionId = typeof args?.targetSessionId === 'string' && args.targetSessionId.trim()
|
|
3693
|
+
? args.targetSessionId.trim()
|
|
3694
|
+
: typeof args?.sessionId === 'string' && args.sessionId.trim()
|
|
3695
|
+
? args.sessionId.trim()
|
|
3696
|
+
: '';
|
|
3697
|
+
if (!sessionId) return { success: false, error: 'targetSessionId required' };
|
|
3698
|
+
const response = normalizeInteractivePromptResponse(args?.response ?? args);
|
|
3699
|
+
const instance = this.deps.instanceManager.getInstance(sessionId);
|
|
3700
|
+
if (!instance) return { success: false, error: `No running instance for session ${sessionId}` };
|
|
3701
|
+
this.deps.instanceManager.sendEvent(sessionId, 'interactive_prompt_response', response);
|
|
3702
|
+
return { success: true };
|
|
3703
|
+
}
|
|
3704
|
+
|
|
1555
3705
|
case 'launch_cli':
|
|
1556
3706
|
case 'stop_cli':
|
|
1557
3707
|
case 'set_cli_view_mode':
|
|
3708
|
+
case 'record_provider_pty':
|
|
1558
3709
|
case 'agent_command': {
|
|
1559
3710
|
return this.deps.cliManager.handleCliCommand(cmd, args);
|
|
1560
3711
|
}
|
|
@@ -1750,7 +3901,7 @@ export class DaemonCommandRouter {
|
|
|
1750
3901
|
: '';
|
|
1751
3902
|
const providerMeta = this.deps.providerLoader.resolve?.(providerType) || this.deps.providerLoader.getMeta(providerType);
|
|
1752
3903
|
const { sessions: historySessions, hasMore, source } = listProviderHistorySessions(providerType, {
|
|
1753
|
-
canonicalHistory: providerMeta?.
|
|
3904
|
+
canonicalHistory: providerMeta?.nativeHistory,
|
|
1754
3905
|
offset,
|
|
1755
3906
|
limit,
|
|
1756
3907
|
historyBehavior: providerMeta?.historyBehavior,
|
|
@@ -1972,6 +4123,128 @@ export class DaemonCommandRouter {
|
|
|
1972
4123
|
};
|
|
1973
4124
|
}
|
|
1974
4125
|
|
|
4126
|
+
// Session-info popup data. Aggregates whatever the daemon knows
|
|
4127
|
+
// about a single live session into one envelope so the dashboard
|
|
4128
|
+
// doesn't need to stitch together status + coordinator registry +
|
|
4129
|
+
// session registry on the client. Includes the actual system
|
|
4130
|
+
// prompt that was injected at launch when the session is a mesh
|
|
4131
|
+
// coordinator — that's the "what prompt did the agent see?"
|
|
4132
|
+
// question the info-icon dialog is meant to answer.
|
|
4133
|
+
case 'get_session_info': {
|
|
4134
|
+
const sessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim()
|
|
4135
|
+
: typeof args?.sessionId === 'string' ? args.sessionId.trim() : '';
|
|
4136
|
+
if (!sessionId) return { success: false, error: 'targetSessionId required' };
|
|
4137
|
+
// Fetch both lookups up front. We used to bail with "Session not
|
|
4138
|
+
// found" when sessionRegistry forgot the SID (auto-cleanup,
|
|
4139
|
+
// daemon restart with the session not yet restored, etc), which
|
|
4140
|
+
// hid the coordinator-side metadata even though the
|
|
4141
|
+
// coordinator-registry still has it. Now we return whichever
|
|
4142
|
+
// side we have. The dashboard renders "no coordinator-specific
|
|
4143
|
+
// prompt" only when *neither* side knows the session.
|
|
4144
|
+
const target = this.deps.sessionRegistry.get(sessionId);
|
|
4145
|
+
const coord = getCoordinatorForSession(sessionId);
|
|
4146
|
+
if (!target && !coord) return { success: false, error: 'Session not found', sessionId };
|
|
4147
|
+
const adapter = target
|
|
4148
|
+
? this.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter
|
|
4149
|
+
: undefined;
|
|
4150
|
+
const runtimeMeta = (adapter && typeof (adapter as any).getRuntimeMetadata === 'function')
|
|
4151
|
+
? (adapter as any).getRuntimeMetadata()
|
|
4152
|
+
: undefined;
|
|
4153
|
+
const providerType = target?.providerType || coord?.cliType || '';
|
|
4154
|
+
const providerMetaForSession = providerType
|
|
4155
|
+
? this.deps.providerLoader.resolve?.(providerType) || this.deps.providerLoader.getMeta(providerType)
|
|
4156
|
+
: undefined;
|
|
4157
|
+
return {
|
|
4158
|
+
success: true,
|
|
4159
|
+
session: {
|
|
4160
|
+
sessionId,
|
|
4161
|
+
providerType,
|
|
4162
|
+
providerName: providerMetaForSession?.name,
|
|
4163
|
+
transport: target?.transport,
|
|
4164
|
+
workspace: (target as any)?.workspace || coord?.workspace,
|
|
4165
|
+
spawnedAtMs: (target as any)?.spawnedAtMs || coord?.startedAt,
|
|
4166
|
+
providerSessionId: (target as any)?.providerSessionId,
|
|
4167
|
+
runtimeMetadata: runtimeMeta,
|
|
4168
|
+
},
|
|
4169
|
+
coordinator: coord ? {
|
|
4170
|
+
meshId: coord.meshId,
|
|
4171
|
+
startedAt: coord.startedAt,
|
|
4172
|
+
cliType: coord.cliType,
|
|
4173
|
+
systemPrompt: coord.systemPrompt,
|
|
4174
|
+
extraSystemPrompt: coord.extraSystemPrompt,
|
|
4175
|
+
injection: coord.injection,
|
|
4176
|
+
mcpConfigPath: coord.mcpConfigPath,
|
|
4177
|
+
} : null,
|
|
4178
|
+
};
|
|
4179
|
+
}
|
|
4180
|
+
|
|
4181
|
+
// ── User-level coordinator-prompt files (~/.adhdev/coordinator-prompts/).
|
|
4182
|
+
// These live on this daemon's filesystem and never sync to the
|
|
4183
|
+
// cloud / other daemons — they're per-machine config. The
|
|
4184
|
+
// Settings page in the dashboard reads/writes via these two
|
|
4185
|
+
// commands instead of going through fs from the browser.
|
|
4186
|
+
case 'list_coordinator_prompts': {
|
|
4187
|
+
const fs = await import('node:fs');
|
|
4188
|
+
const path = await import('node:path');
|
|
4189
|
+
const os = await import('node:os');
|
|
4190
|
+
const dir = path.join(os.homedir(), '.adhdev', 'coordinator-prompts');
|
|
4191
|
+
const entries: Record<string, { override: string; append: string }> = {};
|
|
4192
|
+
try {
|
|
4193
|
+
if (fs.existsSync(dir)) {
|
|
4194
|
+
for (const name of fs.readdirSync(dir)) {
|
|
4195
|
+
// Bucket files into <key>.{md|append.md}; ignore others
|
|
4196
|
+
// so a stray README or .DS_Store doesn't show up.
|
|
4197
|
+
const matchOverride = name.match(/^([a-zA-Z0-9_.-]+)\.md$/);
|
|
4198
|
+
const matchAppend = name.match(/^([a-zA-Z0-9_.-]+)\.append\.md$/);
|
|
4199
|
+
// append-pattern wins when both match (file is `.append.md`).
|
|
4200
|
+
const m = matchAppend || matchOverride;
|
|
4201
|
+
if (!m) continue;
|
|
4202
|
+
const isAppend = !!matchAppend;
|
|
4203
|
+
const key = m[1];
|
|
4204
|
+
const full = path.join(dir, name);
|
|
4205
|
+
let content = '';
|
|
4206
|
+
try { content = fs.readFileSync(full, 'utf8'); } catch { /* skip */ }
|
|
4207
|
+
if (!entries[key]) entries[key] = { override: '', append: '' };
|
|
4208
|
+
if (isAppend) entries[key].append = content;
|
|
4209
|
+
else entries[key].override = content;
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
} catch (error: any) {
|
|
4213
|
+
return { success: false, error: error?.message || String(error) };
|
|
4214
|
+
}
|
|
4215
|
+
return { success: true, dir, entries };
|
|
4216
|
+
}
|
|
4217
|
+
|
|
4218
|
+
case 'write_coordinator_prompt': {
|
|
4219
|
+
const fs = await import('node:fs');
|
|
4220
|
+
const path = await import('node:path');
|
|
4221
|
+
const os = await import('node:os');
|
|
4222
|
+
const key = typeof args?.key === 'string' ? args.key.trim() : '';
|
|
4223
|
+
const kind = args?.kind === 'append' ? 'append' : 'override';
|
|
4224
|
+
const content = typeof args?.content === 'string' ? args.content : '';
|
|
4225
|
+
// Whitelist key chars so a malicious caller can't write
|
|
4226
|
+
// ../../etc/passwd. Same charset readUserPromptFile accepts.
|
|
4227
|
+
if (!key || !/^[a-zA-Z0-9_.-]+$/.test(key)) {
|
|
4228
|
+
return { success: false, error: 'key must match [a-zA-Z0-9_.-]+' };
|
|
4229
|
+
}
|
|
4230
|
+
const dir = path.join(os.homedir(), '.adhdev', 'coordinator-prompts');
|
|
4231
|
+
const filename = kind === 'append' ? `${key}.append.md` : `${key}.md`;
|
|
4232
|
+
const full = path.join(dir, filename);
|
|
4233
|
+
try {
|
|
4234
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
4235
|
+
if (content.trim()) {
|
|
4236
|
+
fs.writeFileSync(full, content, { encoding: 'utf8', mode: 0o600 });
|
|
4237
|
+
} else if (fs.existsSync(full)) {
|
|
4238
|
+
// Empty content = "reset to default" — delete the file
|
|
4239
|
+
// so the daemon's readUserPromptFile path falls through.
|
|
4240
|
+
fs.unlinkSync(full);
|
|
4241
|
+
}
|
|
4242
|
+
return { success: true, path: full, kind, key };
|
|
4243
|
+
} catch (error: any) {
|
|
4244
|
+
return { success: false, error: error?.message || String(error) };
|
|
4245
|
+
}
|
|
4246
|
+
}
|
|
4247
|
+
|
|
1975
4248
|
case 'mark_session_seen': {
|
|
1976
4249
|
const sessionId = args?.sessionId;
|
|
1977
4250
|
if (!sessionId || typeof sessionId !== 'string') {
|
|
@@ -2150,46 +4423,305 @@ export class DaemonCommandRouter {
|
|
|
2150
4423
|
}
|
|
2151
4424
|
}
|
|
2152
4425
|
|
|
2153
|
-
case 'get_mesh': {
|
|
4426
|
+
case 'get_mesh': {
|
|
4427
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4428
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4429
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
4430
|
+
if (!meshRecord?.mesh) return { success: false, error: 'Mesh not found' };
|
|
4431
|
+
|
|
4432
|
+
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
4433
|
+
const directTruth = await hydrateInlineMeshDirectTruth({
|
|
4434
|
+
mesh: meshRecord.mesh,
|
|
4435
|
+
meshSource: meshRecord.source,
|
|
4436
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
4437
|
+
statusInstanceId: this.deps.statusInstanceId,
|
|
4438
|
+
localMachineId: loadConfig().machineId || '',
|
|
4439
|
+
});
|
|
4440
|
+
const directTruthSatisfied = meshRecord.source !== 'inline_bootstrap' || directTruth.directEvidenceCount > 0;
|
|
4441
|
+
const sourceOfTruth = {
|
|
4442
|
+
membership: meshRecord.source === 'inline_cache'
|
|
4443
|
+
? 'coordinator_inline_mesh_cache'
|
|
4444
|
+
: meshRecord.source === 'local_config'
|
|
4445
|
+
? 'local_mesh_config'
|
|
4446
|
+
: 'inline_bootstrap_snapshot',
|
|
4447
|
+
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
4448
|
+
directPeerTruth: {
|
|
4449
|
+
required: requireDirectPeerTruth,
|
|
4450
|
+
satisfied: directTruthSatisfied,
|
|
4451
|
+
directEvidenceCount: directTruth.directEvidenceCount,
|
|
4452
|
+
localConfirmedCount: directTruth.localConfirmedCount,
|
|
4453
|
+
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
4454
|
+
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
4455
|
+
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
4456
|
+
},
|
|
4457
|
+
};
|
|
4458
|
+
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
4459
|
+
return {
|
|
4460
|
+
success: false,
|
|
4461
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
4462
|
+
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct get_mesh probes succeed.',
|
|
4463
|
+
sourceOfTruth,
|
|
4464
|
+
};
|
|
4465
|
+
}
|
|
4466
|
+
return { success: true, mesh: meshRecord.mesh, sourceOfTruth };
|
|
4467
|
+
}
|
|
4468
|
+
|
|
4469
|
+
case 'create_mesh': {
|
|
4470
|
+
const name = typeof args?.name === 'string' ? args.name.trim() : '';
|
|
4471
|
+
const repoIdentity = typeof args?.repoIdentity === 'string' ? args.repoIdentity.trim() : '';
|
|
4472
|
+
const repoRemoteUrl = typeof args?.repoRemoteUrl === 'string' ? args.repoRemoteUrl.trim() : undefined;
|
|
4473
|
+
const defaultBranch = typeof args?.defaultBranch === 'string' ? args.defaultBranch.trim() : undefined;
|
|
4474
|
+
if (!name) return { success: false, error: 'name required' };
|
|
4475
|
+
try {
|
|
4476
|
+
const { createMesh } = await import('../config/mesh-config.js');
|
|
4477
|
+
const meshHost = args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)
|
|
4478
|
+
? args.meshHost
|
|
4479
|
+
: undefined;
|
|
4480
|
+
const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy, meshHost });
|
|
4481
|
+
return { success: true, mesh };
|
|
4482
|
+
} catch (e: any) {
|
|
4483
|
+
return { success: false, error: e.message };
|
|
4484
|
+
}
|
|
4485
|
+
}
|
|
4486
|
+
|
|
4487
|
+
case 'update_mesh': {
|
|
4488
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4489
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4490
|
+
try {
|
|
4491
|
+
const { updateMesh } = await import('../config/mesh-config.js');
|
|
4492
|
+
const patch: Record<string, unknown> = {};
|
|
4493
|
+
if (typeof args?.name === 'string') patch.name = args.name;
|
|
4494
|
+
if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
|
|
4495
|
+
if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
|
|
4496
|
+
if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
|
|
4497
|
+
if (args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)) patch.meshHost = args.meshHost;
|
|
4498
|
+
if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
|
|
4499
|
+
const mesh = updateMesh(meshId, patch as any);
|
|
4500
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
4501
|
+
this.inlineMeshCache.set(meshId, mesh);
|
|
4502
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
4503
|
+
return { success: true, mesh };
|
|
4504
|
+
} catch (e: any) {
|
|
4505
|
+
return { success: false, error: e.message };
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4508
|
+
|
|
4509
|
+
case 'get_mesh_host_pairing': {
|
|
4510
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4511
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4512
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
4513
|
+
const mesh = meshRecord?.mesh;
|
|
4514
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
4515
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
4516
|
+
const pairingStatus = meshHost.pairing?.status || 'not_configured';
|
|
4517
|
+
return {
|
|
4518
|
+
success: true,
|
|
4519
|
+
code: pairingStatus === 'not_configured' ? 'mesh_host_pairing_not_configured' : 'mesh_host_pairing_pending',
|
|
4520
|
+
meshId,
|
|
4521
|
+
hostAddress: meshHost.hostAddress,
|
|
4522
|
+
meshHost,
|
|
4523
|
+
manualPairing: {
|
|
4524
|
+
status: pairingStatus,
|
|
4525
|
+
joinImplemented: true,
|
|
4526
|
+
protocol: 'standalone_command_direct_v1',
|
|
4527
|
+
description: 'Standalone manual pairing can save address/token metadata, apply a host join over direct standalone command HTTP or injected mesh command dispatch, and check persisted status. P2P signaling remains outside this slice.',
|
|
4528
|
+
},
|
|
4529
|
+
};
|
|
4530
|
+
}
|
|
4531
|
+
|
|
4532
|
+
case 'configure_mesh_host_pairing': {
|
|
4533
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4534
|
+
const hostAddress = typeof args?.hostAddress === 'string' ? args.hostAddress.trim() : '';
|
|
4535
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
4536
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4537
|
+
if (!hostAddress || !token) return { success: false, error: 'hostAddress and token required' };
|
|
4538
|
+
try {
|
|
4539
|
+
const { configureMeshHostPairing } = await import('../config/mesh-config.js');
|
|
4540
|
+
const configured = configureMeshHostPairing(meshId, { hostAddress, token });
|
|
4541
|
+
if (!configured) return { success: false, error: 'Mesh not found' };
|
|
4542
|
+
this.inlineMeshCache.set(meshId, configured.mesh);
|
|
4543
|
+
const meshHost = resolveMeshHostStatus(configured.mesh);
|
|
4544
|
+
return {
|
|
4545
|
+
success: true,
|
|
4546
|
+
code: 'mesh_host_pairing_pending',
|
|
4547
|
+
meshId,
|
|
4548
|
+
hostAddress: configured.hostAddress,
|
|
4549
|
+
meshHost,
|
|
4550
|
+
manualPairing: {
|
|
4551
|
+
status: meshHost.pairing?.status || 'pairing',
|
|
4552
|
+
joinImplemented: true,
|
|
4553
|
+
protocol: 'standalone_command_direct_v1',
|
|
4554
|
+
description: 'Manual Mesh Host pairing config was saved locally. Use join_mesh_host_pairing to apply it to the host. Raw token was not persisted.',
|
|
4555
|
+
},
|
|
4556
|
+
};
|
|
4557
|
+
} catch (e: any) {
|
|
4558
|
+
return { success: false, code: 'mesh_host_pairing_invalid', meshId, hostAddress, error: e.message };
|
|
4559
|
+
}
|
|
4560
|
+
}
|
|
4561
|
+
|
|
4562
|
+
case 'create_mesh_host_pairing_token': {
|
|
4563
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4564
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4565
|
+
try {
|
|
4566
|
+
const { createMeshHostPairingToken } = await import('../config/mesh-config.js');
|
|
4567
|
+
const created = createMeshHostPairingToken(meshId, {
|
|
4568
|
+
token: typeof args?.token === 'string' ? args.token : undefined,
|
|
4569
|
+
expiresAt: typeof args?.expiresAt === 'string' ? args.expiresAt : undefined,
|
|
4570
|
+
});
|
|
4571
|
+
if (!created) return { success: false, error: 'Mesh not found' };
|
|
4572
|
+
this.inlineMeshCache.set(meshId, created.mesh);
|
|
4573
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
4574
|
+
return {
|
|
4575
|
+
success: true,
|
|
4576
|
+
code: 'mesh_host_pairing_token_created',
|
|
4577
|
+
meshId,
|
|
4578
|
+
token: created.token,
|
|
4579
|
+
tokenId: created.tokenId,
|
|
4580
|
+
expiresAt: created.expiresAt,
|
|
4581
|
+
meshHost: resolveMeshHostStatus(created.mesh),
|
|
4582
|
+
warning: 'Raw token is returned once and is not persisted; share it with member daemons over a trusted channel.',
|
|
4583
|
+
};
|
|
4584
|
+
} catch (e: any) {
|
|
4585
|
+
return { success: false, code: 'mesh_host_pairing_token_invalid', meshId, error: e.message };
|
|
4586
|
+
}
|
|
4587
|
+
}
|
|
4588
|
+
|
|
4589
|
+
case 'apply_mesh_host_join': {
|
|
2154
4590
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4591
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
4592
|
+
const memberNode = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
4593
|
+
? args.memberNode
|
|
4594
|
+
: null;
|
|
2155
4595
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2156
|
-
|
|
2157
|
-
if (meshRecord?.mesh) return { success: true, mesh: meshRecord.mesh };
|
|
2158
|
-
return { success: false, error: 'Mesh not found' };
|
|
2159
|
-
}
|
|
2160
|
-
|
|
2161
|
-
case 'create_mesh': {
|
|
2162
|
-
const name = typeof args?.name === 'string' ? args.name.trim() : '';
|
|
2163
|
-
const repoIdentity = typeof args?.repoIdentity === 'string' ? args.repoIdentity.trim() : '';
|
|
2164
|
-
const repoRemoteUrl = typeof args?.repoRemoteUrl === 'string' ? args.repoRemoteUrl.trim() : undefined;
|
|
2165
|
-
const defaultBranch = typeof args?.defaultBranch === 'string' ? args.defaultBranch.trim() : undefined;
|
|
2166
|
-
if (!name) return { success: false, error: 'name required' };
|
|
4596
|
+
if (!token || !memberNode) return { success: false, error: 'token and memberNode required' };
|
|
2167
4597
|
try {
|
|
2168
|
-
const {
|
|
2169
|
-
const
|
|
2170
|
-
|
|
4598
|
+
const { applyMeshHostJoinRequest } = await import('../config/mesh-config.js');
|
|
4599
|
+
const applied = applyMeshHostJoinRequest(meshId, {
|
|
4600
|
+
token,
|
|
4601
|
+
memberNode: memberNode as any,
|
|
4602
|
+
memberMeshId: typeof args?.memberMeshId === 'string' ? args.memberMeshId : undefined,
|
|
4603
|
+
});
|
|
4604
|
+
if (!applied) return { success: false, error: 'Mesh not found' };
|
|
4605
|
+
if (!applied.accepted) {
|
|
4606
|
+
return {
|
|
4607
|
+
success: false,
|
|
4608
|
+
code: 'mesh_host_join_rejected',
|
|
4609
|
+
meshId,
|
|
4610
|
+
tokenId: applied.tokenId,
|
|
4611
|
+
meshHost: applied.meshHost ? resolveMeshHostStatus({ meshHost: applied.meshHost }) : undefined,
|
|
4612
|
+
error: applied.reason,
|
|
4613
|
+
};
|
|
4614
|
+
}
|
|
4615
|
+
this.inlineMeshCache.set(meshId, applied.mesh);
|
|
4616
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
4617
|
+
try {
|
|
4618
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
4619
|
+
appendLedgerEntry(meshId, {
|
|
4620
|
+
kind: 'node_joined',
|
|
4621
|
+
nodeId: applied.node.id,
|
|
4622
|
+
payload: { role: 'member', tokenId: applied.tokenId, workspace: applied.node.workspace },
|
|
4623
|
+
});
|
|
4624
|
+
} catch { /* ledger append is best-effort */ }
|
|
4625
|
+
return {
|
|
4626
|
+
success: true,
|
|
4627
|
+
code: 'mesh_host_join_accepted',
|
|
4628
|
+
meshId,
|
|
4629
|
+
node: applied.node,
|
|
4630
|
+
tokenId: applied.tokenId,
|
|
4631
|
+
meshHost: resolveMeshHostStatus(applied.mesh),
|
|
4632
|
+
};
|
|
2171
4633
|
} catch (e: any) {
|
|
2172
|
-
return { success: false, error: e.message };
|
|
4634
|
+
return { success: false, code: 'mesh_host_join_failed', meshId, error: e.message };
|
|
2173
4635
|
}
|
|
2174
4636
|
}
|
|
2175
4637
|
|
|
2176
|
-
case '
|
|
4638
|
+
case 'join_mesh_host_pairing': {
|
|
2177
4639
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
4640
|
+
const token = typeof args?.token === 'string' ? args.token.trim() : '';
|
|
2178
4641
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
4642
|
+
if (!token) return { success: false, error: 'token required because raw pairing tokens are not persisted' };
|
|
4643
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
4644
|
+
const mesh = meshRecord?.mesh;
|
|
4645
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
4646
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
4647
|
+
if (meshHost.role !== 'member') {
|
|
4648
|
+
return { success: false, code: 'mesh_host_join_not_member', meshId, meshHost, error: 'join_mesh_host_pairing must run from a member daemon configured with a Mesh Host address/token.' };
|
|
4649
|
+
}
|
|
2179
4650
|
try {
|
|
2180
|
-
const {
|
|
2181
|
-
const
|
|
2182
|
-
if (
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
if (!
|
|
2187
|
-
const
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
4651
|
+
const { tokenIdForManualPairing, markMeshHostPairingJoined } = await import('../config/mesh-config.js');
|
|
4652
|
+
const tokenId = tokenIdForManualPairing(token);
|
|
4653
|
+
if (meshHost.pairing?.tokenId && meshHost.pairing.tokenId !== tokenId) {
|
|
4654
|
+
return { success: false, code: 'mesh_host_join_rejected', meshId, tokenId, meshHost, error: 'invalid pairing token' };
|
|
4655
|
+
}
|
|
4656
|
+
const memberNode = buildMemberJoinNode(mesh, args, this.deps.statusInstanceId);
|
|
4657
|
+
if (!memberNode) return { success: false, error: 'member node metadata unavailable' };
|
|
4658
|
+
const hostMeshId = typeof args?.hostMeshId === 'string' && args.hostMeshId.trim() ? args.hostMeshId.trim() : meshId;
|
|
4659
|
+
const hostDaemonId = typeof args?.hostDaemonId === 'string' && args.hostDaemonId.trim()
|
|
4660
|
+
? args.hostDaemonId.trim()
|
|
4661
|
+
: meshHost.hostDaemonId;
|
|
4662
|
+
let hostResult: any;
|
|
4663
|
+
let transport: string;
|
|
4664
|
+
if (hostDaemonId && this.deps.dispatchMeshCommand) {
|
|
4665
|
+
transport = 'mesh_command_dispatch';
|
|
4666
|
+
hostResult = await this.deps.dispatchMeshCommand(hostDaemonId, 'apply_mesh_host_join', {
|
|
4667
|
+
meshId: hostMeshId,
|
|
4668
|
+
token,
|
|
4669
|
+
memberMeshId: meshId,
|
|
4670
|
+
memberNode,
|
|
4671
|
+
});
|
|
4672
|
+
} else if (meshHost.hostAddress) {
|
|
4673
|
+
transport = 'standalone_http_command';
|
|
4674
|
+
const commandUrl = normalizeStandaloneHostCommandUrl(meshHost.hostAddress);
|
|
4675
|
+
const response = await fetch(commandUrl, {
|
|
4676
|
+
method: 'POST',
|
|
4677
|
+
headers: { 'Content-Type': 'application/json' },
|
|
4678
|
+
body: JSON.stringify({ type: 'apply_mesh_host_join', payload: { meshId: hostMeshId, token, memberMeshId: meshId, memberNode } }),
|
|
4679
|
+
});
|
|
4680
|
+
hostResult = await response.json().catch(() => ({ success: false, error: `Host returned HTTP ${response.status}` }));
|
|
4681
|
+
if (!response.ok && hostResult?.success !== false) hostResult = { success: false, error: `Host returned HTTP ${response.status}` };
|
|
4682
|
+
} else {
|
|
4683
|
+
return {
|
|
4684
|
+
success: false,
|
|
4685
|
+
code: 'mesh_host_join_transport_unavailable',
|
|
4686
|
+
meshId,
|
|
4687
|
+
meshHost,
|
|
4688
|
+
error: 'No hostDaemonId dispatch path or hostAddress HTTP command path is available. P2P signaling join is not implemented in this slice.',
|
|
4689
|
+
};
|
|
4690
|
+
}
|
|
4691
|
+
if (!hostResult?.success) {
|
|
4692
|
+
return { success: false, code: hostResult?.code || 'mesh_host_join_rejected', meshId, meshHost, transport, error: hostResult?.error || 'Mesh Host rejected join request', hostResult };
|
|
4693
|
+
}
|
|
4694
|
+
const joined = meshRecord.inline
|
|
4695
|
+
? null
|
|
4696
|
+
: markMeshHostPairingJoined(meshId, {
|
|
4697
|
+
tokenId: hostResult.tokenId || tokenId,
|
|
4698
|
+
hostDaemonId: hostResult.meshHost?.hostDaemonId || hostDaemonId,
|
|
4699
|
+
hostNodeId: hostResult.meshHost?.hostNodeId,
|
|
4700
|
+
joinedAt: hostResult.meshHost?.pairing?.joinedAt,
|
|
4701
|
+
});
|
|
4702
|
+
if (joined) {
|
|
4703
|
+
this.inlineMeshCache.set(meshId, joined.mesh);
|
|
4704
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
4705
|
+
}
|
|
4706
|
+
return {
|
|
4707
|
+
success: true,
|
|
4708
|
+
code: 'mesh_host_join_applied',
|
|
4709
|
+
meshId,
|
|
4710
|
+
hostMeshId,
|
|
4711
|
+
transport,
|
|
4712
|
+
node: hostResult.node,
|
|
4713
|
+
tokenId: hostResult.tokenId || tokenId,
|
|
4714
|
+
meshHost: joined ? resolveMeshHostStatus(joined.mesh) : { ...meshHost, pairing: { ...(meshHost.pairing || {}), status: 'paired', tokenId: hostResult.tokenId || tokenId } },
|
|
4715
|
+
hostResult,
|
|
4716
|
+
manualPairing: {
|
|
4717
|
+
status: 'paired',
|
|
4718
|
+
joinImplemented: true,
|
|
4719
|
+
protocol: 'standalone_command_direct_v1',
|
|
4720
|
+
description: 'Mesh Host accepted the join and local member pairing status was marked paired. P2P runtime signaling remains outside this slice.',
|
|
4721
|
+
},
|
|
4722
|
+
};
|
|
2191
4723
|
} catch (e: any) {
|
|
2192
|
-
return { success: false, error: e.message };
|
|
4724
|
+
return { success: false, code: 'mesh_host_join_failed', meshId, meshHost, error: e.message };
|
|
2193
4725
|
}
|
|
2194
4726
|
}
|
|
2195
4727
|
|
|
@@ -2286,6 +4818,8 @@ export class DaemonCommandRouter {
|
|
|
2286
4818
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2287
4819
|
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
2288
4820
|
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
4821
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue cancellation');
|
|
4822
|
+
if (ownerFailure) return ownerFailure;
|
|
2289
4823
|
try {
|
|
2290
4824
|
const { cancelTask } = await import('../mesh/mesh-work-queue.js');
|
|
2291
4825
|
const reason = typeof args?.reason === 'string' ? args.reason : undefined;
|
|
@@ -2301,6 +4835,8 @@ export class DaemonCommandRouter {
|
|
|
2301
4835
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2302
4836
|
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
2303
4837
|
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
4838
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue requeue');
|
|
4839
|
+
if (ownerFailure) return ownerFailure;
|
|
2304
4840
|
try {
|
|
2305
4841
|
const { requeueTask } = await import('../mesh/mesh-work-queue.js');
|
|
2306
4842
|
const task = requeueTask(meshId, taskId, {
|
|
@@ -2322,6 +4858,8 @@ export class DaemonCommandRouter {
|
|
|
2322
4858
|
const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
2323
4859
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2324
4860
|
if (!workspace) return { success: false, error: 'workspace required' };
|
|
4861
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node addition');
|
|
4862
|
+
if (ownerFailure) return ownerFailure;
|
|
2325
4863
|
try {
|
|
2326
4864
|
const { addNode } = await import('../config/mesh-config.js');
|
|
2327
4865
|
const providerPriority = Array.isArray(args?.providerPriority)
|
|
@@ -2332,8 +4870,26 @@ export class DaemonCommandRouter {
|
|
|
2332
4870
|
...(readOnly ? { readOnly: true } : {}),
|
|
2333
4871
|
...(providerPriority.length ? { providerPriority } : {}),
|
|
2334
4872
|
};
|
|
2335
|
-
const
|
|
4873
|
+
const role = normalizeMeshDaemonRole(args?.role);
|
|
4874
|
+
const daemonId = typeof args?.daemonId === 'string' && args.daemonId.trim() ? args.daemonId.trim() : undefined;
|
|
4875
|
+
const machineId = typeof args?.machineId === 'string' && args.machineId.trim() ? args.machineId.trim() : undefined;
|
|
4876
|
+
const repoRoot = typeof args?.repoRoot === 'string' && args.repoRoot.trim() ? args.repoRoot.trim() : undefined;
|
|
4877
|
+
const node = addNode(meshId, {
|
|
4878
|
+
workspace,
|
|
4879
|
+
...(repoRoot ? { repoRoot } : {}),
|
|
4880
|
+
...(daemonId ? { daemonId } : {}),
|
|
4881
|
+
...(machineId ? { machineId } : {}),
|
|
4882
|
+
...(policy ? { policy } : {}),
|
|
4883
|
+
...(role ? { role } : {}),
|
|
4884
|
+
});
|
|
2336
4885
|
if (!node) return { success: false, error: 'Mesh not found' };
|
|
4886
|
+
// mesh_status hands back a coordinator-memory aggregate
|
|
4887
|
+
// snapshot keyed on (meshId, queueRevision). Adding a
|
|
4888
|
+
// node touches neither, so without an explicit cache
|
|
4889
|
+
// bust the dashboard graph keeps rendering the pre-add
|
|
4890
|
+
// node list (empty for a fresh mesh) even after the
|
|
4891
|
+
// user clicks Refresh.
|
|
4892
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
2337
4893
|
return { success: true, node };
|
|
2338
4894
|
} catch (e: any) {
|
|
2339
4895
|
return { success: false, error: e.message };
|
|
@@ -2344,6 +4900,8 @@ export class DaemonCommandRouter {
|
|
|
2344
4900
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2345
4901
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2346
4902
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
4903
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node update');
|
|
4904
|
+
if (ownerFailure) return ownerFailure;
|
|
2347
4905
|
try {
|
|
2348
4906
|
const { updateNode } = await import('../config/mesh-config.js');
|
|
2349
4907
|
const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
|
|
@@ -2360,8 +4918,20 @@ export class DaemonCommandRouter {
|
|
|
2360
4918
|
delete (policy as any).providerPriority;
|
|
2361
4919
|
}
|
|
2362
4920
|
}
|
|
2363
|
-
const
|
|
4921
|
+
const patch: Record<string, unknown> = { policy: policy as any };
|
|
4922
|
+
if (typeof args?.systemPrompt === 'string') {
|
|
4923
|
+
const trimmed = (args.systemPrompt as string).trim();
|
|
4924
|
+
patch.systemPrompt = trimmed || undefined;
|
|
4925
|
+
} else if (args?.systemPrompt === null) {
|
|
4926
|
+
patch.systemPrompt = undefined;
|
|
4927
|
+
}
|
|
4928
|
+
const node = updateNode(meshId, nodeId, patch as any);
|
|
2364
4929
|
if (!node) return { success: false, error: 'Mesh node not found' };
|
|
4930
|
+
// Provider priority / systemPrompt changes don't touch
|
|
4931
|
+
// the queue revision, so without a manual bust the
|
|
4932
|
+
// cached aggregate keeps surfacing pre-update values
|
|
4933
|
+
// (priority chip, coordinator prompt preview, etc.).
|
|
4934
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
2365
4935
|
return { success: true, node };
|
|
2366
4936
|
} catch (e: any) {
|
|
2367
4937
|
return { success: false, error: e.message };
|
|
@@ -2372,6 +4942,8 @@ export class DaemonCommandRouter {
|
|
|
2372
4942
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2373
4943
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2374
4944
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
4945
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node removal');
|
|
4946
|
+
if (ownerFailure) return ownerFailure;
|
|
2375
4947
|
try {
|
|
2376
4948
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2377
4949
|
const mesh = meshRecord?.mesh;
|
|
@@ -2397,131 +4969,97 @@ export class DaemonCommandRouter {
|
|
|
2397
4969
|
}
|
|
2398
4970
|
}
|
|
2399
4971
|
|
|
2400
|
-
case '
|
|
4972
|
+
case 'get_mesh_refine_config_schema': {
|
|
4973
|
+
return {
|
|
4974
|
+
success: true,
|
|
4975
|
+
schema: MESH_REFINE_CONFIG_SCHEMA,
|
|
4976
|
+
locations: MESH_REFINE_CONFIG_LOCATIONS,
|
|
4977
|
+
worktreeBootstrap: {
|
|
4978
|
+
schema: MESH_WORKTREE_BOOTSTRAP_CONFIG_SCHEMA,
|
|
4979
|
+
locations: MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS,
|
|
4980
|
+
sourceOfTruth: 'repo worktree bootstrap config',
|
|
4981
|
+
runBehavior: 'When present and enabled, clone_mesh_node runs commands after submodule initialization and records status on the worktree node.',
|
|
4982
|
+
},
|
|
4983
|
+
sourceOfTruth: 'repo mesh/refine config',
|
|
4984
|
+
heuristicRole: 'suggestions_only_not_execution_path',
|
|
4985
|
+
};
|
|
4986
|
+
}
|
|
4987
|
+
|
|
4988
|
+
case 'validate_mesh_refine_config': {
|
|
4989
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
|
|
4990
|
+
const mesh = args?.inlineMesh || {};
|
|
4991
|
+
const loaded = args?.config !== undefined
|
|
4992
|
+
? { config: args.config, source: 'inline', sourceType: 'mesh_policy' as const }
|
|
4993
|
+
: loadMeshRefineConfig(mesh, workspace);
|
|
4994
|
+
const validation = loaded.config
|
|
4995
|
+
? validateMeshRefineConfig(loaded.config, loaded.source)
|
|
4996
|
+
: { valid: false, errors: [((loaded as { error?: string }).error) || 'repo mesh/refine config unavailable'], commands: [], rejectedCommands: [] };
|
|
4997
|
+
return { success: validation.valid, ...loaded, ...validation };
|
|
4998
|
+
}
|
|
4999
|
+
|
|
5000
|
+
case 'suggest_mesh_refine_config': {
|
|
5001
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
|
|
5002
|
+
const mesh = args?.inlineMesh || {};
|
|
5003
|
+
return {
|
|
5004
|
+
success: true,
|
|
5005
|
+
...suggestMeshRefineConfig(mesh, workspace),
|
|
5006
|
+
note: 'Suggestions are heuristic scaffold only; Refinery will not execute them until saved into repo mesh/refine config.',
|
|
5007
|
+
};
|
|
5008
|
+
}
|
|
5009
|
+
|
|
5010
|
+
case 'plan_mesh_refine_node': {
|
|
2401
5011
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2402
5012
|
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2403
5013
|
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
2404
|
-
|
|
5014
|
+
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
5015
|
+
const mesh = meshRecord?.mesh;
|
|
5016
|
+
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
5017
|
+
if (!node?.workspace) return { success: false, error: `Node '${nodeId}' workspace not found` };
|
|
5018
|
+
return {
|
|
5019
|
+
success: true,
|
|
5020
|
+
dryRun: true,
|
|
5021
|
+
nodeId,
|
|
5022
|
+
workspace: node.workspace,
|
|
5023
|
+
validationPlan: buildMeshRefineValidationPlan(mesh, node.workspace),
|
|
5024
|
+
mergeWillRun: false,
|
|
5025
|
+
cleanupWillRun: false,
|
|
5026
|
+
};
|
|
5027
|
+
}
|
|
5028
|
+
|
|
5029
|
+
case 'fast_forward_mesh_node': {
|
|
5030
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
5031
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
5032
|
+
let workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
5033
|
+
let submoduleIgnorePaths = Array.isArray(args?.submoduleIgnorePaths)
|
|
5034
|
+
? args.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
|
|
5035
|
+
: undefined;
|
|
5036
|
+
if (!workspace && meshId && nodeId) {
|
|
2405
5037
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2406
5038
|
const mesh = meshRecord?.mesh;
|
|
2407
5039
|
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
return { success: false, error: `Refinery requires a local worktree node` };
|
|
2412
|
-
}
|
|
2413
|
-
|
|
2414
|
-
const sourceNode = node.clonedFromNodeId
|
|
2415
|
-
? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
|
|
2416
|
-
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
2417
|
-
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
2418
|
-
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found' };
|
|
2419
|
-
|
|
2420
|
-
const { execFile } = await import('node:child_process');
|
|
2421
|
-
const { promisify } = await import('node:util');
|
|
2422
|
-
const execFileAsync = promisify(execFile);
|
|
2423
|
-
|
|
2424
|
-
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
2425
|
-
const branch = branchStdout.trim();
|
|
2426
|
-
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node' };
|
|
2427
|
-
|
|
2428
|
-
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
2429
|
-
const baseBranch = baseBranchStdout.trim();
|
|
2430
|
-
|
|
2431
|
-
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
|
|
2432
|
-
if (validationSummary.status === 'failed') {
|
|
2433
|
-
return {
|
|
2434
|
-
success: false,
|
|
2435
|
-
code: 'validation_failed',
|
|
2436
|
-
convergenceStatus: 'blocked_review',
|
|
2437
|
-
error: 'Refinery validation gate failed; merge/refine was not attempted.',
|
|
2438
|
-
branch,
|
|
2439
|
-
into: baseBranch,
|
|
2440
|
-
validationSummary,
|
|
2441
|
-
finalBranchConvergenceState: {
|
|
2442
|
-
branch,
|
|
2443
|
-
baseBranch,
|
|
2444
|
-
merged: false,
|
|
2445
|
-
removed: false,
|
|
2446
|
-
validation: 'failed',
|
|
2447
|
-
status: 'blocked_review',
|
|
2448
|
-
},
|
|
2449
|
-
};
|
|
2450
|
-
}
|
|
2451
|
-
if (validationSummary.status === 'skipped') {
|
|
2452
|
-
return {
|
|
2453
|
-
success: false,
|
|
2454
|
-
code: 'validation_unavailable',
|
|
2455
|
-
convergenceStatus: 'blocked_review',
|
|
2456
|
-
error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
|
|
2457
|
-
branch,
|
|
2458
|
-
into: baseBranch,
|
|
2459
|
-
validationSummary,
|
|
2460
|
-
finalBranchConvergenceState: {
|
|
2461
|
-
branch,
|
|
2462
|
-
baseBranch,
|
|
2463
|
-
merged: false,
|
|
2464
|
-
removed: false,
|
|
2465
|
-
validation: 'unavailable',
|
|
2466
|
-
status: 'blocked_review',
|
|
2467
|
-
},
|
|
2468
|
-
};
|
|
2469
|
-
}
|
|
2470
|
-
|
|
2471
|
-
try {
|
|
2472
|
-
await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
2473
|
-
} catch (e: any) {
|
|
2474
|
-
return {
|
|
2475
|
-
success: false,
|
|
2476
|
-
error: `Merge failed (conflicts?): ${e.message}`,
|
|
2477
|
-
validationSummary,
|
|
2478
|
-
finalBranchConvergenceState: {
|
|
2479
|
-
branch,
|
|
2480
|
-
baseBranch,
|
|
2481
|
-
merged: false,
|
|
2482
|
-
removed: false,
|
|
2483
|
-
validation: 'passed',
|
|
2484
|
-
status: 'not_mergeable',
|
|
2485
|
-
},
|
|
2486
|
-
};
|
|
5040
|
+
workspace = typeof node?.workspace === 'string' ? node.workspace.trim() : '';
|
|
5041
|
+
if (!submoduleIgnorePaths && Array.isArray(node?.policy?.submoduleIgnorePaths)) {
|
|
5042
|
+
submoduleIgnorePaths = node.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string');
|
|
2487
5043
|
}
|
|
2488
|
-
|
|
2489
|
-
const removeResult = await this.execute('remove_mesh_node', {
|
|
2490
|
-
meshId,
|
|
2491
|
-
nodeId,
|
|
2492
|
-
sessionCleanupMode: 'kill',
|
|
2493
|
-
inlineMesh: args?.inlineMesh,
|
|
2494
|
-
});
|
|
2495
|
-
|
|
2496
|
-
try {
|
|
2497
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2498
|
-
appendLedgerEntry(meshId, {
|
|
2499
|
-
kind: 'node_removed',
|
|
2500
|
-
nodeId,
|
|
2501
|
-
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary },
|
|
2502
|
-
});
|
|
2503
|
-
} catch {}
|
|
2504
|
-
|
|
2505
|
-
return {
|
|
2506
|
-
success: true,
|
|
2507
|
-
merged: true,
|
|
2508
|
-
branch,
|
|
2509
|
-
into: baseBranch,
|
|
2510
|
-
removeResult,
|
|
2511
|
-
validationSummary,
|
|
2512
|
-
finalBranchConvergenceState: {
|
|
2513
|
-
branch: baseBranch,
|
|
2514
|
-
mergedBranch: branch,
|
|
2515
|
-
baseBranch,
|
|
2516
|
-
merged: true,
|
|
2517
|
-
removed: removeResult?.success !== false,
|
|
2518
|
-
validation: 'passed',
|
|
2519
|
-
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
2520
|
-
},
|
|
2521
|
-
};
|
|
2522
|
-
} catch (e: any) {
|
|
2523
|
-
return { success: false, error: e.message };
|
|
2524
5044
|
}
|
|
5045
|
+
const result = await (fastForwardMeshNode({
|
|
5046
|
+
meshId: meshId || undefined,
|
|
5047
|
+
nodeId: nodeId || undefined,
|
|
5048
|
+
workspace,
|
|
5049
|
+
branch: typeof args?.branch === 'string' ? args.branch : undefined,
|
|
5050
|
+
execute: args?.execute === true,
|
|
5051
|
+
dryRun: args?.dryRun === true,
|
|
5052
|
+
updateSubmodules: args?.updateSubmodules === true,
|
|
5053
|
+
submoduleIgnorePaths,
|
|
5054
|
+
}) as Promise<unknown>);
|
|
5055
|
+
return result as CommandRouterResult;
|
|
5056
|
+
}
|
|
5057
|
+
|
|
5058
|
+
case 'refine_mesh_node': {
|
|
5059
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
5060
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
5061
|
+
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
5062
|
+
return this.startMeshRefineJob(meshId, nodeId, args);
|
|
2525
5063
|
}
|
|
2526
5064
|
|
|
2527
5065
|
case 'remove_mesh_node': {
|
|
@@ -2562,9 +5100,15 @@ export class DaemonCommandRouter {
|
|
|
2562
5100
|
let removed = false;
|
|
2563
5101
|
if (meshRecord?.inline) {
|
|
2564
5102
|
removed = this.removeInlineMeshNode(meshId, mesh, nodeId);
|
|
5103
|
+
// Inline meshes share the same aggregate snapshot cache as
|
|
5104
|
+
// local-config meshes; without this bust the removed node
|
|
5105
|
+
// keeps showing up in the dashboard graph until the cache
|
|
5106
|
+
// ages out on its own.
|
|
5107
|
+
if (removed) this.invalidateAggregateMeshStatus(meshId);
|
|
2565
5108
|
} else {
|
|
2566
5109
|
const { removeNode } = await import('../config/mesh-config.js');
|
|
2567
5110
|
removed = removeNode(meshId, nodeId);
|
|
5111
|
+
if (removed) this.invalidateAggregateMeshStatus(meshId);
|
|
2568
5112
|
}
|
|
2569
5113
|
|
|
2570
5114
|
// Record in task ledger
|
|
@@ -2602,6 +5146,8 @@ export class DaemonCommandRouter {
|
|
|
2602
5146
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2603
5147
|
if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
|
|
2604
5148
|
if (!branch) return { success: false, error: 'branch required' };
|
|
5149
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'worktree clone');
|
|
5150
|
+
if (ownerFailure) return ownerFailure;
|
|
2605
5151
|
|
|
2606
5152
|
try {
|
|
2607
5153
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
@@ -2650,39 +5196,124 @@ export class DaemonCommandRouter {
|
|
|
2650
5196
|
policy: { ...(sourceNode.policy || {}) },
|
|
2651
5197
|
});
|
|
2652
5198
|
if (!node) return { success: false, error: 'Failed to register worktree node' };
|
|
5199
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
2653
5200
|
}
|
|
2654
5201
|
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
5202
|
+
const persistWorktreeSetupState = async (bootstrapState: WorktreeBootstrapState): Promise<void> => {
|
|
5203
|
+
node.worktreeBootstrap = bootstrapState;
|
|
5204
|
+
if (meshRecord.inline) {
|
|
5205
|
+
this.updateInlineMeshNode(meshId, mesh, node);
|
|
5206
|
+
return;
|
|
5207
|
+
}
|
|
2658
5208
|
try {
|
|
2659
|
-
const {
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
5209
|
+
const { updateNode } = await import('../config/mesh-config.js');
|
|
5210
|
+
updateNode(meshId, node.id, { worktreeBootstrap: bootstrapState });
|
|
5211
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
5212
|
+
} catch { /* bootstrap status persistence is best-effort */ }
|
|
5213
|
+
};
|
|
5214
|
+
|
|
5215
|
+
const appendCloneLedger = async (initSubmodules: boolean, bootstrapState: WorktreeBootstrapState): Promise<void> => {
|
|
5216
|
+
try {
|
|
5217
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
5218
|
+
appendLedgerEntry(meshId, {
|
|
5219
|
+
kind: 'node_cloned',
|
|
5220
|
+
nodeId: node.id,
|
|
5221
|
+
payload: {
|
|
5222
|
+
sourceNodeId,
|
|
5223
|
+
branch: result.branch,
|
|
5224
|
+
worktreePath: result.worktreePath,
|
|
5225
|
+
submodulesInitialized: initSubmodules,
|
|
5226
|
+
worktreeBootstrap: {
|
|
5227
|
+
status: bootstrapState.status,
|
|
5228
|
+
required: bootstrapState.required,
|
|
5229
|
+
configSource: bootstrapState.configSource,
|
|
5230
|
+
configSourceType: bootstrapState.configSourceType,
|
|
5231
|
+
lastCommand: bootstrapState.lastCommand,
|
|
5232
|
+
exitCode: bootstrapState.exitCode,
|
|
5233
|
+
},
|
|
5234
|
+
},
|
|
5235
|
+
});
|
|
5236
|
+
} catch { /* ledger append is best-effort */ }
|
|
5237
|
+
};
|
|
5238
|
+
|
|
5239
|
+
const initSubmodules = (sourceNode.policy as any)?.initSubmodulesOnClone !== false;
|
|
5240
|
+
const loadedBootstrap = loadMeshWorktreeBootstrapConfig(mesh, result.worktreePath);
|
|
5241
|
+
const runningBootstrapState: WorktreeBootstrapState = {
|
|
5242
|
+
status: 'running',
|
|
5243
|
+
required: loadedBootstrap.config?.required !== false,
|
|
5244
|
+
configSource: loadedBootstrap.path || loadedBootstrap.source,
|
|
5245
|
+
configSourceType: loadedBootstrap.sourceType,
|
|
5246
|
+
startedAt: new Date().toISOString(),
|
|
5247
|
+
};
|
|
5248
|
+
await persistWorktreeSetupState(runningBootstrapState);
|
|
5249
|
+
|
|
5250
|
+
const finishWorktreeSetup = async (): Promise<{ submodulesInitialized: boolean; bootstrapState: WorktreeBootstrapState }> => {
|
|
5251
|
+
let submodulesInitialized = false;
|
|
5252
|
+
if (initSubmodules) {
|
|
5253
|
+
try {
|
|
5254
|
+
const { runGit } = await import('../git/git-executor.js');
|
|
5255
|
+
await runGit(
|
|
5256
|
+
{ workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true },
|
|
5257
|
+
['submodule', 'update', '--init', '--recursive'],
|
|
5258
|
+
{ timeoutMs: 120000 },
|
|
5259
|
+
);
|
|
5260
|
+
submodulesInitialized = true;
|
|
5261
|
+
} catch (subErr: any) {
|
|
5262
|
+
// Submodule init is best-effort; don't fail the clone
|
|
5263
|
+
console.warn('[mesh] Submodule init failed for worktree:', subErr.message);
|
|
5264
|
+
}
|
|
2668
5265
|
}
|
|
2669
|
-
|
|
5266
|
+
const bootstrapState: WorktreeBootstrapState = await runMeshWorktreeBootstrap(mesh, result.worktreePath);
|
|
5267
|
+
await persistWorktreeSetupState(bootstrapState);
|
|
5268
|
+
await appendCloneLedger(submodulesInitialized, bootstrapState);
|
|
5269
|
+
return { submodulesInitialized, bootstrapState };
|
|
5270
|
+
};
|
|
2670
5271
|
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
5272
|
+
const requestedSetupWaitMs = Number(args?.setupWaitMs ?? args?.bootstrapWaitMs ?? 8000);
|
|
5273
|
+
const setupWaitMs = Number.isFinite(requestedSetupWaitMs)
|
|
5274
|
+
? Math.min(Math.max(requestedSetupWaitMs, 0), 14000)
|
|
5275
|
+
: 8000;
|
|
5276
|
+
const setupPromise = finishWorktreeSetup();
|
|
5277
|
+
const setupResult = await Promise.race([
|
|
5278
|
+
setupPromise.then((value) => ({ completed: true as const, value })),
|
|
5279
|
+
new Promise<{ completed: false }>((resolve) => setTimeout(() => resolve({ completed: false }), setupWaitMs)),
|
|
5280
|
+
]);
|
|
5281
|
+
|
|
5282
|
+
if (!setupResult.completed) {
|
|
5283
|
+
setupPromise.catch((error: any) => {
|
|
5284
|
+
const failedState: WorktreeBootstrapState = {
|
|
5285
|
+
...runningBootstrapState,
|
|
5286
|
+
status: 'failed',
|
|
5287
|
+
completedAt: new Date().toISOString(),
|
|
5288
|
+
error: error?.message || String(error),
|
|
5289
|
+
};
|
|
5290
|
+
void persistWorktreeSetupState(failedState);
|
|
5291
|
+
void appendCloneLedger(false, failedState);
|
|
2678
5292
|
});
|
|
2679
|
-
|
|
5293
|
+
return {
|
|
5294
|
+
success: true,
|
|
5295
|
+
async: true,
|
|
5296
|
+
status: 'accepted',
|
|
5297
|
+
node,
|
|
5298
|
+
worktreePath: result.worktreePath,
|
|
5299
|
+
branch: result.branch,
|
|
5300
|
+
worktreeBootstrap: runningBootstrapState,
|
|
5301
|
+
worktreeSetup: {
|
|
5302
|
+
status: 'running',
|
|
5303
|
+
setupWaitMs,
|
|
5304
|
+
message: 'Worktree node is registered; submodule/bootstrap setup is continuing in the background.',
|
|
5305
|
+
},
|
|
5306
|
+
};
|
|
5307
|
+
}
|
|
2680
5308
|
|
|
5309
|
+
const { submodulesInitialized, bootstrapState } = setupResult.value;
|
|
2681
5310
|
return {
|
|
2682
5311
|
success: true,
|
|
2683
5312
|
node,
|
|
2684
5313
|
worktreePath: result.worktreePath,
|
|
2685
5314
|
branch: result.branch,
|
|
5315
|
+
submodulesInitialized,
|
|
5316
|
+
worktreeBootstrap: bootstrapState,
|
|
2686
5317
|
};
|
|
2687
5318
|
} catch (e: any) {
|
|
2688
5319
|
return { success: false, error: e.message };
|
|
@@ -2691,12 +5322,12 @@ export class DaemonCommandRouter {
|
|
|
2691
5322
|
case 'trigger_mesh_queue': {
|
|
2692
5323
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2693
5324
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
5325
|
+
const ownerFailure = await this.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue trigger');
|
|
5326
|
+
if (ownerFailure) return ownerFailure;
|
|
2694
5327
|
try {
|
|
2695
5328
|
const { triggerMeshQueue } = await import('../mesh/mesh-events.js');
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
}
|
|
2699
|
-
return { success: true };
|
|
5329
|
+
const trigger = await triggerMeshQueue(this.deps as any, meshId);
|
|
5330
|
+
return { success: true, trigger };
|
|
2700
5331
|
} catch (e: any) {
|
|
2701
5332
|
return { success: false, error: e.message };
|
|
2702
5333
|
}
|
|
@@ -2706,6 +5337,19 @@ export class DaemonCommandRouter {
|
|
|
2706
5337
|
case 'launch_mesh_coordinator': {
|
|
2707
5338
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2708
5339
|
let cliType = typeof args?.cliType === 'string' ? args.cliType.trim() : '';
|
|
5340
|
+
// Optional per-launch system-prompt addition. Dashboard or API
|
|
5341
|
+
// callers (e.g. when spawning a mesh-node-specific coordinator)
|
|
5342
|
+
// can pass extra context that gets appended to the rendered
|
|
5343
|
+
// default prompt under the "## Additional Context" section.
|
|
5344
|
+
// Going through buildCoordinatorSystemPrompt's userInstruction
|
|
5345
|
+
// means user-level override files (~/.adhdev/coordinator-prompts)
|
|
5346
|
+
// and this per-launch addition compose cleanly: an override
|
|
5347
|
+
// wins outright, but if there's no override, the default
|
|
5348
|
+
// prompt + the optional append.md file + this extra context
|
|
5349
|
+
// all stack in declared order.
|
|
5350
|
+
const extraSystemPrompt = typeof args?.extraSystemPrompt === 'string'
|
|
5351
|
+
? args.extraSystemPrompt.trim()
|
|
5352
|
+
: '';
|
|
2709
5353
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2710
5354
|
|
|
2711
5355
|
try {
|
|
@@ -2722,6 +5366,15 @@ export class DaemonCommandRouter {
|
|
|
2722
5366
|
mesh = getMesh(meshId);
|
|
2723
5367
|
}
|
|
2724
5368
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
5369
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
5370
|
+
if (!meshHost.canOwnCoordinator) {
|
|
5371
|
+
return {
|
|
5372
|
+
success: false,
|
|
5373
|
+
...buildMeshHostRequiredFailure(mesh, 'coordinator launch'),
|
|
5374
|
+
meshId,
|
|
5375
|
+
cliType,
|
|
5376
|
+
};
|
|
5377
|
+
}
|
|
2725
5378
|
if (!Array.isArray(mesh.nodes) || mesh.nodes.length === 0) return { success: false, error: 'No nodes in mesh' };
|
|
2726
5379
|
|
|
2727
5380
|
const requestedCoordinatorNodeId = typeof args?.coordinatorNodeId === 'string'
|
|
@@ -2807,7 +5460,7 @@ export class DaemonCommandRouter {
|
|
|
2807
5460
|
// Build coordinator prompt first — fail closed on errors.
|
|
2808
5461
|
let cliCmdSystemPrompt = '';
|
|
2809
5462
|
try {
|
|
2810
|
-
cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
|
|
5463
|
+
cliCmdSystemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined });
|
|
2811
5464
|
} catch (error: any) {
|
|
2812
5465
|
const message = error?.message || String(error);
|
|
2813
5466
|
LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
|
|
@@ -2819,55 +5472,126 @@ export class DaemonCommandRouter {
|
|
|
2819
5472
|
};
|
|
2820
5473
|
}
|
|
2821
5474
|
|
|
2822
|
-
// Run the provider's MCP registration command
|
|
5475
|
+
// Run the provider's MCP registration command under a
|
|
5476
|
+
// PTY. Some providers (agy, future bubbletea CLIs)
|
|
5477
|
+
// refuse to run without /dev/tty, so pipe-only
|
|
5478
|
+
// execFileSync silently no-ops the registration and
|
|
5479
|
+
// the coordinator ends up without any mcp tools. With
|
|
5480
|
+
// a real PTY the registration goes through and the
|
|
5481
|
+
// exit code tells us whether it actually persisted.
|
|
5482
|
+
let mcpRegistrationOk = false;
|
|
5483
|
+
let mcpRegistrationFailure: {
|
|
5484
|
+
command: string;
|
|
5485
|
+
output: string;
|
|
5486
|
+
exitCode: number | null;
|
|
5487
|
+
signal: number | null;
|
|
5488
|
+
timedOut: boolean;
|
|
5489
|
+
} | null = null;
|
|
2823
5490
|
try {
|
|
2824
|
-
const {
|
|
2825
|
-
const
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
5491
|
+
const { buildMeshCoordinatorRegistrationPlan, execUnderPty } = await import('./mesh-coordinator.js');
|
|
5492
|
+
const registrationPlan = buildMeshCoordinatorRegistrationPlan(
|
|
5493
|
+
cliType,
|
|
5494
|
+
coordinatorSetup.serverName,
|
|
5495
|
+
coordinatorSetup.command,
|
|
5496
|
+
);
|
|
5497
|
+
for (const step of registrationPlan) {
|
|
5498
|
+
const renderedCommand = [step.command, ...step.args].join(' ');
|
|
5499
|
+
LOG.info('MeshCoordinator', `Running MCP ${step.label} (pty): ${renderedCommand}`);
|
|
5500
|
+
const ptyResult = await execUnderPty(step.command, step.args, { cwd: workspace, timeoutMs: 20_000 });
|
|
5501
|
+
if (ptyResult.exitCode === 0 && !ptyResult.timedOut) {
|
|
5502
|
+
if (step.required) mcpRegistrationOk = true;
|
|
5503
|
+
continue;
|
|
5504
|
+
}
|
|
5505
|
+
LOG.warn('MeshCoordinator', `MCP ${step.label} failed exit=${ptyResult.exitCode} signal=${ptyResult.signal} timedOut=${ptyResult.timedOut} — output:\n${ptyResult.output.slice(-2000)}`);
|
|
5506
|
+
if (step.required) {
|
|
5507
|
+
mcpRegistrationFailure = {
|
|
5508
|
+
command: renderedCommand,
|
|
5509
|
+
output: ptyResult.output.slice(-2000),
|
|
5510
|
+
exitCode: ptyResult.exitCode,
|
|
5511
|
+
signal: ptyResult.signal,
|
|
5512
|
+
timedOut: ptyResult.timedOut,
|
|
5513
|
+
};
|
|
5514
|
+
break;
|
|
5515
|
+
}
|
|
5516
|
+
}
|
|
2829
5517
|
} catch (error: any) {
|
|
2830
|
-
|
|
2831
|
-
|
|
5518
|
+
LOG.warn('MeshCoordinator', `MCP registration command failed: ${error?.message || error}`);
|
|
5519
|
+
mcpRegistrationFailure = {
|
|
5520
|
+
command: coordinatorSetup.command,
|
|
5521
|
+
output: error?.message || String(error),
|
|
5522
|
+
exitCode: null,
|
|
5523
|
+
signal: null,
|
|
5524
|
+
timedOut: false,
|
|
5525
|
+
};
|
|
2832
5526
|
}
|
|
2833
5527
|
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
5528
|
+
if (!mcpRegistrationOk) {
|
|
5529
|
+
return {
|
|
5530
|
+
success: false,
|
|
5531
|
+
code: 'mesh_coordinator_mcp_registration_failed',
|
|
5532
|
+
error: `Could not register ${coordinatorSetup.serverName}; coordinator session was not launched`,
|
|
5533
|
+
meshId,
|
|
5534
|
+
cliType,
|
|
5535
|
+
workspace,
|
|
5536
|
+
registration: mcpRegistrationFailure,
|
|
5537
|
+
};
|
|
5538
|
+
}
|
|
5539
|
+
|
|
5540
|
+
// Codex gives repo-local .mcp.json precedence over its
|
|
5541
|
+
// global `codex mcp add` registration. Refresh an
|
|
5542
|
+
// existing ADHDev entry so a stale workspace command
|
|
5543
|
+
// cannot shadow the registration we just verified.
|
|
5544
|
+
if (cliType === 'codex-cli') {
|
|
5545
|
+
const repoMcpConfigPath = pathJoin(workspace, '.mcp.json');
|
|
5546
|
+
if (fs.existsSync(repoMcpConfigPath)) {
|
|
2847
5547
|
try {
|
|
2848
|
-
const
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
const
|
|
2853
|
-
if (
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
5548
|
+
const repoMcpConfig = parseMeshCoordinatorMcpConfig(
|
|
5549
|
+
fs.readFileSync(repoMcpConfigPath, 'utf-8'),
|
|
5550
|
+
'claude_mcp_json',
|
|
5551
|
+
);
|
|
5552
|
+
const existingServers = repoMcpConfig.mcpServers;
|
|
5553
|
+
if (
|
|
5554
|
+
existingServers
|
|
5555
|
+
&& typeof existingServers === 'object'
|
|
5556
|
+
&& !Array.isArray(existingServers)
|
|
5557
|
+
&& existingServers[coordinatorSetup.serverName]
|
|
5558
|
+
) {
|
|
5559
|
+
fs.writeFileSync(repoMcpConfigPath, serializeMeshCoordinatorMcpConfig({
|
|
5560
|
+
...repoMcpConfig,
|
|
5561
|
+
mcpServers: {
|
|
5562
|
+
...existingServers,
|
|
5563
|
+
[coordinatorSetup.serverName]: coordinatorSetup.mcpServer,
|
|
5564
|
+
},
|
|
5565
|
+
}, 'claude_mcp_json'), 'utf-8');
|
|
5566
|
+
LOG.info('MeshCoordinator', `Refreshed repo-local ${repoMcpConfigPath} entry for ${coordinatorSetup.serverName}`);
|
|
2863
5567
|
}
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
5568
|
+
} catch (error: any) {
|
|
5569
|
+
return {
|
|
5570
|
+
success: false,
|
|
5571
|
+
code: 'mesh_coordinator_config_write_failed',
|
|
5572
|
+
error: `Could not refresh repo-local MCP config: ${error?.message || error}`,
|
|
5573
|
+
meshId,
|
|
5574
|
+
cliType,
|
|
5575
|
+
workspace,
|
|
5576
|
+
};
|
|
2867
5577
|
}
|
|
2868
5578
|
}
|
|
2869
5579
|
}
|
|
2870
5580
|
|
|
5581
|
+
// Inject system prompt declaratively from provider.v1.json.
|
|
5582
|
+
const cliCmdArgs: string[] = [];
|
|
5583
|
+
const cliCmdEnv: Record<string, string> = {};
|
|
5584
|
+
let cliCmdContextFilePath: string | undefined;
|
|
5585
|
+
if (cliCmdSystemPrompt) {
|
|
5586
|
+
const { applyMeshCoordinatorSystemPromptInjection } = await import('./mesh-coordinator.js');
|
|
5587
|
+
const effect = applyMeshCoordinatorSystemPromptInjection(
|
|
5588
|
+
cliCmdSystemPrompt,
|
|
5589
|
+
providerMeta?.meshCoordinator?.systemPromptInjection,
|
|
5590
|
+
{ cliArgs: cliCmdArgs, launchEnv: cliCmdEnv, workspace, cliType },
|
|
5591
|
+
);
|
|
5592
|
+
cliCmdContextFilePath = effect.contextFilePath;
|
|
5593
|
+
}
|
|
5594
|
+
|
|
2871
5595
|
const cliCmdLaunch: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
|
|
2872
5596
|
cliType,
|
|
2873
5597
|
dir: workspace,
|
|
@@ -2876,16 +5600,52 @@ export class DaemonCommandRouter {
|
|
|
2876
5600
|
settings: { meshCoordinatorFor: meshId },
|
|
2877
5601
|
});
|
|
2878
5602
|
|
|
5603
|
+
// R48 inject-then-remove. Spawn was just kicked off above; agy and
|
|
5604
|
+
// gemini-cli read AGENTS.md / GEMINI.md exactly once at startup and
|
|
5605
|
+
// cache it for the rest of the session, so we can safely strip
|
|
5606
|
+
// the wrapper from disk shortly after launch. That keeps any
|
|
5607
|
+
// worker session launched into the same workspace later from
|
|
5608
|
+
// picking up our wrapper block.
|
|
5609
|
+
if (cliCmdLaunch?.success && cliCmdContextFilePath) {
|
|
5610
|
+
const stripPath = cliCmdContextFilePath;
|
|
5611
|
+
setTimeout(() => {
|
|
5612
|
+
void import('./mesh-coordinator.js').then(({ stripCoordinatorWrapperFile }) => {
|
|
5613
|
+
stripCoordinatorWrapperFile(stripPath);
|
|
5614
|
+
LOG.info('MeshCoordinator', `Stripped wrapper from ${stripPath} after launch settle (cli_command)`);
|
|
5615
|
+
}).catch(() => { /* best-effort */ });
|
|
5616
|
+
}, 5000);
|
|
5617
|
+
}
|
|
5618
|
+
|
|
2879
5619
|
if (!cliCmdLaunch?.success) {
|
|
2880
5620
|
return { success: false, error: cliCmdLaunch?.error || 'Failed to launch CLI session' };
|
|
2881
5621
|
}
|
|
2882
5622
|
|
|
2883
5623
|
LOG.info('MeshCoordinator', `Launched ${cliType} coordinator (cli_command) for mesh ${meshId}`);
|
|
5624
|
+
const cliCmdSessionId = cliCmdLaunch.sessionId || cliCmdLaunch.id;
|
|
5625
|
+
if (cliCmdSessionId) {
|
|
5626
|
+
const cliCmdInjectionDecl = providerMeta?.meshCoordinator?.systemPromptInjection;
|
|
5627
|
+
registerMeshCoordinator({
|
|
5628
|
+
meshId,
|
|
5629
|
+
sessionId: cliCmdSessionId,
|
|
5630
|
+
workspace,
|
|
5631
|
+
startedAt: Date.now(),
|
|
5632
|
+
cliType,
|
|
5633
|
+
systemPrompt: cliCmdSystemPrompt || undefined,
|
|
5634
|
+
extraSystemPrompt: extraSystemPrompt || undefined,
|
|
5635
|
+
injection: cliCmdInjectionDecl ? {
|
|
5636
|
+
mode: cliCmdInjectionDecl.mode,
|
|
5637
|
+
target: 'flag' in cliCmdInjectionDecl ? cliCmdInjectionDecl.flag
|
|
5638
|
+
: 'name' in cliCmdInjectionDecl ? cliCmdInjectionDecl.name
|
|
5639
|
+
: 'path' in cliCmdInjectionDecl ? cliCmdInjectionDecl.path
|
|
5640
|
+
: undefined,
|
|
5641
|
+
} : undefined,
|
|
5642
|
+
});
|
|
5643
|
+
}
|
|
2884
5644
|
try {
|
|
2885
5645
|
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2886
5646
|
appendLedgerEntry(meshId, {
|
|
2887
5647
|
kind: 'coordinator_started',
|
|
2888
|
-
sessionId:
|
|
5648
|
+
sessionId: cliCmdSessionId,
|
|
2889
5649
|
providerType: cliType,
|
|
2890
5650
|
payload: { workspace },
|
|
2891
5651
|
});
|
|
@@ -2896,8 +5656,8 @@ export class DaemonCommandRouter {
|
|
|
2896
5656
|
meshId,
|
|
2897
5657
|
cliType,
|
|
2898
5658
|
workspace,
|
|
2899
|
-
sessionId:
|
|
2900
|
-
mcpRegistered:
|
|
5659
|
+
sessionId: cliCmdSessionId,
|
|
5660
|
+
mcpRegistered: mcpRegistrationOk,
|
|
2901
5661
|
};
|
|
2902
5662
|
}
|
|
2903
5663
|
|
|
@@ -2918,7 +5678,7 @@ export class DaemonCommandRouter {
|
|
|
2918
5678
|
// broken mesh state is visible instead of silently launching with weaker rules.
|
|
2919
5679
|
let systemPrompt = '';
|
|
2920
5680
|
try {
|
|
2921
|
-
systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
|
|
5681
|
+
systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType, userInstruction: extraSystemPrompt || undefined });
|
|
2922
5682
|
} catch (error: any) {
|
|
2923
5683
|
const message = error?.message || String(error);
|
|
2924
5684
|
LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
|
|
@@ -3033,12 +5793,15 @@ export class DaemonCommandRouter {
|
|
|
3033
5793
|
launchEnv.HERMES_HOME = dirname(mcpConfigPath);
|
|
3034
5794
|
launchEnv.HERMES_IGNORE_USER_CONFIG = '';
|
|
3035
5795
|
}
|
|
5796
|
+
let autoImportContextFilePath: string | undefined;
|
|
3036
5797
|
if (systemPrompt) {
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
5798
|
+
const { applyMeshCoordinatorSystemPromptInjection } = await import('./mesh-coordinator.js');
|
|
5799
|
+
const effect = applyMeshCoordinatorSystemPromptInjection(
|
|
5800
|
+
systemPrompt,
|
|
5801
|
+
providerMeta?.meshCoordinator?.systemPromptInjection,
|
|
5802
|
+
{ cliArgs, launchEnv, workspace, cliType },
|
|
5803
|
+
);
|
|
5804
|
+
autoImportContextFilePath = effect.contextFilePath;
|
|
3042
5805
|
}
|
|
3043
5806
|
if (cliType === 'claude-cli') {
|
|
3044
5807
|
cliArgs.push('--mcp-config', coordinatorSetup.configPath);
|
|
@@ -3057,18 +5820,53 @@ export class DaemonCommandRouter {
|
|
|
3057
5820
|
}
|
|
3058
5821
|
});
|
|
3059
5822
|
|
|
5823
|
+
// R48 inject-then-remove. See the cli_command branch for context;
|
|
5824
|
+
// same idea: strip the wrapper from disk ~5s after launch so the
|
|
5825
|
+
// user's AGENTS.md / GEMINI.md is untouched the moment any
|
|
5826
|
+
// worker session opens up in the same workspace.
|
|
5827
|
+
if (launchResult?.success && autoImportContextFilePath) {
|
|
5828
|
+
const stripPath = autoImportContextFilePath;
|
|
5829
|
+
setTimeout(() => {
|
|
5830
|
+
void import('./mesh-coordinator.js').then(({ stripCoordinatorWrapperFile }) => {
|
|
5831
|
+
stripCoordinatorWrapperFile(stripPath);
|
|
5832
|
+
LOG.info('MeshCoordinator', `Stripped wrapper from ${stripPath} after launch settle (auto_import)`);
|
|
5833
|
+
}).catch(() => { /* best-effort */ });
|
|
5834
|
+
}, 5000);
|
|
5835
|
+
}
|
|
5836
|
+
|
|
3060
5837
|
if (!launchResult?.success) {
|
|
3061
5838
|
return { success: false, error: launchResult?.error || 'Failed to launch CLI session' };
|
|
3062
5839
|
}
|
|
3063
5840
|
|
|
3064
5841
|
LOG.info('MeshCoordinator', `Launched ${cliType} coordinator for mesh ${meshId} in ${workspace}`);
|
|
5842
|
+
const launchSessionId = launchResult.sessionId || launchResult.id;
|
|
5843
|
+
if (launchSessionId) {
|
|
5844
|
+
const autoImportInjectionDecl = providerMeta?.meshCoordinator?.systemPromptInjection;
|
|
5845
|
+
registerMeshCoordinator({
|
|
5846
|
+
meshId,
|
|
5847
|
+
sessionId: launchSessionId,
|
|
5848
|
+
workspace,
|
|
5849
|
+
startedAt: Date.now(),
|
|
5850
|
+
cliType,
|
|
5851
|
+
systemPrompt: systemPrompt || undefined,
|
|
5852
|
+
extraSystemPrompt: extraSystemPrompt || undefined,
|
|
5853
|
+
mcpConfigPath,
|
|
5854
|
+
injection: autoImportInjectionDecl ? {
|
|
5855
|
+
mode: autoImportInjectionDecl.mode,
|
|
5856
|
+
target: 'flag' in autoImportInjectionDecl ? autoImportInjectionDecl.flag
|
|
5857
|
+
: 'name' in autoImportInjectionDecl ? autoImportInjectionDecl.name
|
|
5858
|
+
: 'path' in autoImportInjectionDecl ? autoImportInjectionDecl.path
|
|
5859
|
+
: undefined,
|
|
5860
|
+
} : undefined,
|
|
5861
|
+
});
|
|
5862
|
+
}
|
|
3065
5863
|
|
|
3066
5864
|
// Record coordinator launch in task ledger
|
|
3067
5865
|
try {
|
|
3068
5866
|
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
3069
5867
|
appendLedgerEntry(meshId, {
|
|
3070
5868
|
kind: 'coordinator_started',
|
|
3071
|
-
sessionId:
|
|
5869
|
+
sessionId: launchSessionId,
|
|
3072
5870
|
providerType: cliType,
|
|
3073
5871
|
payload: { workspace },
|
|
3074
5872
|
});
|
|
@@ -3079,7 +5877,7 @@ export class DaemonCommandRouter {
|
|
|
3079
5877
|
meshId,
|
|
3080
5878
|
cliType,
|
|
3081
5879
|
workspace,
|
|
3082
|
-
sessionId:
|
|
5880
|
+
sessionId: launchSessionId,
|
|
3083
5881
|
mcpConfigWritten: true,
|
|
3084
5882
|
};
|
|
3085
5883
|
} catch (e: any) {
|
|
@@ -3095,6 +5893,36 @@ export class DaemonCommandRouter {
|
|
|
3095
5893
|
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
3096
5894
|
const mesh = meshRecord?.mesh;
|
|
3097
5895
|
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
5896
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
5897
|
+
|
|
5898
|
+
const refreshRequested = args?.refresh === true || args?.forceRefresh === true;
|
|
5899
|
+
// See (B3) below: scope the peek to this daemon when the
|
|
5900
|
+
// caller doesn't tell us, otherwise scoped events look
|
|
5901
|
+
// missing and we falsely return a stale cache.
|
|
5902
|
+
const peekScope = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
|
|
5903
|
+
? args.coordinatorDaemonId.trim()
|
|
5904
|
+
: (this.deps.statusInstanceId || undefined);
|
|
5905
|
+
const pendingCoordinatorEventCount = getPendingMeshCoordinatorEvents(meshId, peekScope).length;
|
|
5906
|
+
const hadAggregateCache = this.aggregateMeshStatusCache.has(meshId);
|
|
5907
|
+
if (!refreshRequested && pendingCoordinatorEventCount === 0) {
|
|
5908
|
+
const cachedStatus = this.getCachedAggregateMeshStatus(meshId, mesh, { requireDirectPeerTruth: args?.requireDirectPeerTruth === true });
|
|
5909
|
+
if (cachedStatus) {
|
|
5910
|
+
logRepoMeshStatusDebug('return_cached', {
|
|
5911
|
+
meshId,
|
|
5912
|
+
command: 'mesh_status',
|
|
5913
|
+
refreshRequested,
|
|
5914
|
+
summary: summarizeRepoMeshStatusDebug(cachedStatus),
|
|
5915
|
+
});
|
|
5916
|
+
return cachedStatus;
|
|
5917
|
+
}
|
|
5918
|
+
}
|
|
5919
|
+
const refreshReason = refreshRequested
|
|
5920
|
+
? 'explicit_refresh'
|
|
5921
|
+
: pendingCoordinatorEventCount > 0
|
|
5922
|
+
? 'pending_coordinator_events'
|
|
5923
|
+
: hadAggregateCache
|
|
5924
|
+
? 'stale_pending_cache_refresh'
|
|
5925
|
+
: 'cold_cache_miss';
|
|
3098
5926
|
|
|
3099
5927
|
const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
|
|
3100
5928
|
const queue = getQueue(meshId);
|
|
@@ -3102,6 +5930,7 @@ export class DaemonCommandRouter {
|
|
|
3102
5930
|
|
|
3103
5931
|
const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
|
|
3104
5932
|
const ledgerEntries = readLedgerEntries(meshId, { tail: 20 });
|
|
5933
|
+
const asyncRefineLedgerEntries = readLedgerEntries(meshId, { tail: 100 });
|
|
3105
5934
|
const ledgerSummary = getLedgerSummary(meshId);
|
|
3106
5935
|
const sessionHostRecords = this.deps.sessionHostControl?.listSessions
|
|
3107
5936
|
? await this.deps.sessionHostControl.listSessions().catch(() => [])
|
|
@@ -3109,6 +5938,74 @@ export class DaemonCommandRouter {
|
|
|
3109
5938
|
const liveMeshSessions = partitionSessionHostRecords(Array.isArray(sessionHostRecords) ? sessionHostRecords : []).liveRuntimes;
|
|
3110
5939
|
|
|
3111
5940
|
const localMachineId = loadConfig().machineId || '';
|
|
5941
|
+
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
5942
|
+
const directTruth = requireDirectPeerTruth
|
|
5943
|
+
? await hydrateInlineMeshDirectTruth({
|
|
5944
|
+
mesh,
|
|
5945
|
+
meshSource: meshRecord.source,
|
|
5946
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
5947
|
+
statusInstanceId: this.deps.statusInstanceId,
|
|
5948
|
+
localMachineId,
|
|
5949
|
+
})
|
|
5950
|
+
: {
|
|
5951
|
+
directEvidenceCount: 0,
|
|
5952
|
+
localConfirmedCount: 0,
|
|
5953
|
+
peerAttemptedCount: 0,
|
|
5954
|
+
peerConfirmedCount: 0,
|
|
5955
|
+
unavailableNodeIds: [] as string[],
|
|
5956
|
+
};
|
|
5957
|
+
// Default/cached loads may not attempt a remote peer probe yet; do not surface that as
|
|
5958
|
+
// a direct mesh truth failure until an explicit probe attempt actually fails.
|
|
5959
|
+
const passivePeerTruthNotAttempted = requireDirectPeerTruth
|
|
5960
|
+
&& !refreshRequested
|
|
5961
|
+
&& directTruth.directEvidenceCount > 0
|
|
5962
|
+
&& directTruth.peerAttemptedCount === 0;
|
|
5963
|
+
const effectiveDirectTruth = passivePeerTruthNotAttempted
|
|
5964
|
+
? { ...directTruth, unavailableNodeIds: [] as string[] }
|
|
5965
|
+
: directTruth;
|
|
5966
|
+
const unavailableDirectTruthNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
|
|
5967
|
+
const unavailableNodesAreOnlyRemovedWorktrees = unavailableDirectTruthNodeIds.size > 0
|
|
5968
|
+
&& Array.isArray(mesh.nodes)
|
|
5969
|
+
&& mesh.nodes
|
|
5970
|
+
.filter((node: any) => unavailableDirectTruthNodeIds.has(String(node.id || node.nodeId || '')))
|
|
5971
|
+
.every((node: any) => node?.isLocalWorktree === true);
|
|
5972
|
+
const directTruthSatisfied = !requireDirectPeerTruth
|
|
5973
|
+
|| (effectiveDirectTruth.directEvidenceCount > 0 && (effectiveDirectTruth.unavailableNodeIds.length === 0 || unavailableNodesAreOnlyRemovedWorktrees));
|
|
5974
|
+
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
5975
|
+
const failureResult = {
|
|
5976
|
+
success: false,
|
|
5977
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
5978
|
+
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct mesh_status probes succeed.',
|
|
5979
|
+
sourceOfTruth: {
|
|
5980
|
+
membership: meshRecord.source === 'inline_cache'
|
|
5981
|
+
? 'coordinator_inline_mesh_cache'
|
|
5982
|
+
: meshRecord.source === 'local_config'
|
|
5983
|
+
? 'local_mesh_config'
|
|
5984
|
+
: 'inline_bootstrap_snapshot',
|
|
5985
|
+
coordinatorOwnsLiveTruth: false,
|
|
5986
|
+
currentStatus: 'direct_peer_truth_unavailable',
|
|
5987
|
+
directPeerTruth: {
|
|
5988
|
+
required: true,
|
|
5989
|
+
satisfied: false,
|
|
5990
|
+
directEvidenceCount: directTruth.directEvidenceCount,
|
|
5991
|
+
localConfirmedCount: directTruth.localConfirmedCount,
|
|
5992
|
+
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
5993
|
+
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
5994
|
+
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
5995
|
+
},
|
|
5996
|
+
},
|
|
5997
|
+
};
|
|
5998
|
+
logRepoMeshStatusDebug('direct_truth_unavailable', {
|
|
5999
|
+
meshId,
|
|
6000
|
+
command: 'mesh_status',
|
|
6001
|
+
refreshRequested,
|
|
6002
|
+
meshSource: meshRecord.source,
|
|
6003
|
+
directTruth,
|
|
6004
|
+
});
|
|
6005
|
+
return failureResult;
|
|
6006
|
+
}
|
|
6007
|
+
const directTruthUnavailableNodeIds = new Set(effectiveDirectTruth.unavailableNodeIds);
|
|
6008
|
+
const coordinatorHostname = osHostname();
|
|
3112
6009
|
const selectedCoordinatorNodeId = readStringValue(
|
|
3113
6010
|
mesh.coordinator?.preferredNodeId,
|
|
3114
6011
|
(mesh.nodes?.[0] as any)?.id,
|
|
@@ -3122,21 +6019,42 @@ export class DaemonCommandRouter {
|
|
|
3122
6019
|
for (const [nodeIndex, node] of (mesh.nodes || []).entries()) {
|
|
3123
6020
|
const nodeId = String(node.id || node.nodeId || '');
|
|
3124
6021
|
const daemonId = readStringValue(node.daemonId);
|
|
6022
|
+
const nodeMachineId = readMeshNodeMachineId(node as Record<string, unknown>);
|
|
6023
|
+
const nodeHostname = readMeshNodeHostname(node as Record<string, unknown>);
|
|
3125
6024
|
const providerPriority = readProviderPriorityFromPolicy(node.policy);
|
|
6025
|
+
const configuredCoordinatorNode = Boolean(
|
|
6026
|
+
nodeId && selectedCoordinatorNodeId && nodeId === selectedCoordinatorNodeId,
|
|
6027
|
+
);
|
|
6028
|
+
const sparseConfiguredCoordinatorNode = configuredCoordinatorNode
|
|
6029
|
+
&& !daemonId
|
|
6030
|
+
&& !nodeMachineId
|
|
6031
|
+
&& !nodeHostname;
|
|
3126
6032
|
const isSelfNode = Boolean(
|
|
3127
6033
|
nodeId && inlineCoordinatorNodeId && nodeId === inlineCoordinatorNodeId,
|
|
3128
6034
|
) || Boolean(
|
|
3129
6035
|
daemonId && (daemonId === localMachineId || daemonId === this.deps.statusInstanceId),
|
|
3130
|
-
) || Boolean(meshRecord?.inline && nodeIndex === 0)
|
|
6036
|
+
) || Boolean(meshRecord?.inline && nodeIndex === 0)
|
|
6037
|
+
|| sparseConfiguredCoordinatorNode;
|
|
6038
|
+
const machineIdentity = buildMeshNodeMachineIdentity(node as Record<string, unknown>, {
|
|
6039
|
+
localMachineId,
|
|
6040
|
+
localDaemonId: this.deps.statusInstanceId,
|
|
6041
|
+
coordinatorHostname,
|
|
6042
|
+
isSelfNode,
|
|
6043
|
+
});
|
|
3131
6044
|
const status: Record<string, unknown> = {
|
|
3132
6045
|
nodeId,
|
|
3133
|
-
machineLabel: node
|
|
6046
|
+
machineLabel: buildMeshNodeDisplayLabel(node as Record<string, unknown>, nodeId, providerPriority),
|
|
6047
|
+
labelSource: readStringValue(node.machineLabel, node.machine_label, node.machineNickname, node.machine_nickname, node.alias)
|
|
6048
|
+
? 'explicit_metadata'
|
|
6049
|
+
: 'workspace_host_provider_context',
|
|
3134
6050
|
workspace: node.workspace,
|
|
3135
6051
|
repoRoot: node.repoRoot,
|
|
3136
6052
|
isLocalWorktree: node.isLocalWorktree,
|
|
3137
6053
|
worktreeBranch: node.worktreeBranch,
|
|
6054
|
+
role: normalizeMeshDaemonRole(node.role) || (meshHost.hostNodeId && nodeId === meshHost.hostNodeId ? 'host' : undefined),
|
|
3138
6055
|
daemonId,
|
|
3139
|
-
machineId: node.machineId,
|
|
6056
|
+
machineId: nodeMachineId || node.machineId,
|
|
6057
|
+
machine: machineIdentity,
|
|
3140
6058
|
machineStatus: node.machineStatus,
|
|
3141
6059
|
health: 'unknown',
|
|
3142
6060
|
providers: node.providers || [],
|
|
@@ -3203,52 +6121,224 @@ export class DaemonCommandRouter {
|
|
|
3203
6121
|
}
|
|
3204
6122
|
}
|
|
3205
6123
|
if (workspace) {
|
|
3206
|
-
if (!fs.existsSync(workspace)
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
if (
|
|
6124
|
+
if (!fs.existsSync(workspace)) {
|
|
6125
|
+
// Workspace not local — prefer direct live inline truth, then attempt a P2P git probe.
|
|
6126
|
+
const inlineTransitGit = buildInlineMeshTransitGitStatus(node);
|
|
6127
|
+
let remoteProbeApplied = false;
|
|
6128
|
+
if (inlineTransitGit) {
|
|
6129
|
+
status.git = inlineTransitGit;
|
|
6130
|
+
status.health = inlineTransitGit.isGitRepo
|
|
6131
|
+
? deriveMeshNodeHealthFromGit(inlineTransitGit as unknown as Record<string, unknown>)
|
|
6132
|
+
: 'degraded';
|
|
6133
|
+
const connection = readObjectRecord(status.connection);
|
|
6134
|
+
const connectionState = readStringValue(connection.state);
|
|
6135
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
6136
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
6137
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
6138
|
+
}
|
|
6139
|
+
remoteProbeApplied = true;
|
|
6140
|
+
} else if (!isSelfNode && daemonId && this.deps.dispatchMeshCommand && !directTruthUnavailableNodeIds.has(nodeId)) {
|
|
6141
|
+
try {
|
|
6142
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
6143
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
6144
|
+
daemonId,
|
|
6145
|
+
workspace,
|
|
6146
|
+
timeoutMs: 8000,
|
|
6147
|
+
});
|
|
6148
|
+
if (remoteGit) {
|
|
6149
|
+
status.git = remoteGit;
|
|
6150
|
+
status.health = remoteGit.isGitRepo
|
|
6151
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
6152
|
+
: 'degraded';
|
|
6153
|
+
const connection = readObjectRecord(status.connection);
|
|
6154
|
+
const connectionState = readStringValue(connection.state);
|
|
6155
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
6156
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
6157
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
6158
|
+
}
|
|
6159
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
6160
|
+
remoteProbeApplied = true;
|
|
6161
|
+
}
|
|
6162
|
+
} catch {
|
|
6163
|
+
const refreshedConnection = this.deps.getMeshPeerConnectionStatus?.(daemonId);
|
|
6164
|
+
const refreshedConnectionState = readStringValue(refreshedConnection?.state);
|
|
6165
|
+
if (refreshedConnection && refreshedConnectionState === 'connected') {
|
|
6166
|
+
status.connection = refreshedConnection;
|
|
6167
|
+
try {
|
|
6168
|
+
const remoteGit = await probeRemoteMeshGitStatus({
|
|
6169
|
+
dispatchMeshCommand: this.deps.dispatchMeshCommand,
|
|
6170
|
+
daemonId,
|
|
6171
|
+
workspace,
|
|
6172
|
+
timeoutMs: 12000,
|
|
6173
|
+
});
|
|
6174
|
+
if (remoteGit) {
|
|
6175
|
+
status.git = remoteGit;
|
|
6176
|
+
status.health = remoteGit.isGitRepo
|
|
6177
|
+
? deriveMeshNodeHealthFromGit(remoteGit as unknown as Record<string, unknown>)
|
|
6178
|
+
: 'degraded';
|
|
6179
|
+
const connection = readObjectRecord(status.connection);
|
|
6180
|
+
const connectionState = readStringValue(connection.state);
|
|
6181
|
+
const connectionReported = readBooleanValue(connection.reported) ?? false;
|
|
6182
|
+
if (!connectionReported || connectionState === 'unknown') {
|
|
6183
|
+
status.connection = buildLivePeerGitConnection(connection, refreshedAt);
|
|
6184
|
+
}
|
|
6185
|
+
recordInlineMeshDirectGitTruth(node, remoteGit, 'selected_coordinator_mesh_p2p_git');
|
|
6186
|
+
remoteProbeApplied = true;
|
|
6187
|
+
}
|
|
6188
|
+
} catch {
|
|
6189
|
+
// Probe timed out again or P2P unavailable — fall back to cached status
|
|
6190
|
+
}
|
|
6191
|
+
}
|
|
6192
|
+
}
|
|
6193
|
+
}
|
|
6194
|
+
if (!remoteProbeApplied) {
|
|
6195
|
+
const connectionState = readStringValue((status.connection as any)?.state);
|
|
6196
|
+
const pendingPeerGitProbe = !inlineTransitGit
|
|
6197
|
+
&& !isSelfNode
|
|
6198
|
+
&& !!daemonId
|
|
6199
|
+
&& (
|
|
6200
|
+
readStringValue(status.machineStatus) === 'online'
|
|
6201
|
+
|| readStringValue(status.health) === 'online'
|
|
6202
|
+
|| connectionState === 'connecting'
|
|
6203
|
+
|| connectionState === 'connected'
|
|
6204
|
+
|| connectionState === 'unknown'
|
|
6205
|
+
);
|
|
6206
|
+
if (pendingPeerGitProbe) {
|
|
6207
|
+
status.gitProbePending = true;
|
|
6208
|
+
status.health = 'unknown';
|
|
6209
|
+
}
|
|
6210
|
+
if (applyCachedInlineMeshNodeStatus(
|
|
6211
|
+
status,
|
|
6212
|
+
node,
|
|
6213
|
+
pendingPeerGitProbe ? { skipGit: true, skipError: true, skipHealth: true } : undefined,
|
|
6214
|
+
)) {
|
|
6215
|
+
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
6216
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
6217
|
+
nodeStatuses.push(status);
|
|
6218
|
+
continue;
|
|
6219
|
+
}
|
|
6220
|
+
if (meshRecord?.source === 'inline_cache' && !isSelfNode) {
|
|
6221
|
+
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
6222
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
6223
|
+
nodeStatuses.push(status);
|
|
6224
|
+
continue;
|
|
6225
|
+
}
|
|
3219
6226
|
}
|
|
3220
|
-
}
|
|
3221
|
-
|
|
3222
|
-
|
|
6227
|
+
} else {
|
|
6228
|
+
try {
|
|
6229
|
+
const gitStatus = await getGitRepoStatus(workspace, { timeoutMs: 10_000, refreshUpstream: true });
|
|
6230
|
+
status.git = gitStatus;
|
|
6231
|
+
recordInlineMeshDirectGitTruth(node, gitStatus as unknown as Record<string, unknown>, 'selected_coordinator_local_git');
|
|
6232
|
+
if (gitStatus.isGitRepo) {
|
|
6233
|
+
status.health = deriveMeshNodeHealthFromGit(gitStatus as unknown as Record<string, unknown>);
|
|
6234
|
+
} else {
|
|
6235
|
+
status.health = 'degraded';
|
|
6236
|
+
if (gitStatus.error && !status.error) status.error = gitStatus.error;
|
|
6237
|
+
}
|
|
6238
|
+
} catch {
|
|
6239
|
+
if (!applyCachedInlineMeshNodeStatus(status, node)) {
|
|
6240
|
+
status.health = 'degraded';
|
|
6241
|
+
}
|
|
3223
6242
|
}
|
|
3224
6243
|
}
|
|
3225
6244
|
} else {
|
|
3226
6245
|
applyCachedInlineMeshNodeStatus(status, node);
|
|
3227
6246
|
}
|
|
3228
|
-
|
|
6247
|
+
applyInlineMeshBranchConvergence(mesh, node, status);
|
|
6248
|
+
finalizeMeshNodeStatus({ status, node, daemonId, isSelfNode });
|
|
3229
6249
|
nodeStatuses.push(status);
|
|
3230
6250
|
}
|
|
3231
6251
|
|
|
3232
|
-
|
|
6252
|
+
// (B3) Resolve the coordinator daemon scope for the drain.
|
|
6253
|
+
// Emit-time always tags events with the worker's
|
|
6254
|
+
// targetCoordinatorDaemonId and writes them to the
|
|
6255
|
+
// scoped pending-events file. If the caller (MCP tool,
|
|
6256
|
+
// dashboard, etc.) doesn't tell us which coordinator
|
|
6257
|
+
// they're running under, fall back to *this daemon's*
|
|
6258
|
+
// ID — the caller reached us over our IPC/WS, so they
|
|
6259
|
+
// are by definition under this daemon. Falling back to
|
|
6260
|
+
// undefined would silently miss every scoped event.
|
|
6261
|
+
const callerCoordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
|
|
6262
|
+
? args.coordinatorDaemonId.trim()
|
|
6263
|
+
: (this.deps.statusInstanceId || undefined);
|
|
6264
|
+
const pendingCoordinatorEvents = drainPendingMeshCoordinatorEvents(meshId, callerCoordinatorDaemonId);
|
|
6265
|
+
const previewFreshness = (() => {
|
|
6266
|
+
const localRepoRoot = nodeStatuses
|
|
6267
|
+
.map((node: any) => readStringValue(node?.git?.repoRoot, node?.repoRoot, node?.workspace))
|
|
6268
|
+
.find((candidate: string | undefined) => !!candidate && fs.existsSync(candidate));
|
|
6269
|
+
return localRepoRoot ? buildPreviewFreshness(localRepoRoot) : undefined;
|
|
6270
|
+
})();
|
|
6271
|
+
const asyncRefineJobs = buildMeshAsyncRefineJobs({
|
|
6272
|
+
meshId,
|
|
6273
|
+
ledgerEntries: asyncRefineLedgerEntries,
|
|
6274
|
+
pendingEvents: pendingCoordinatorEvents,
|
|
6275
|
+
});
|
|
6276
|
+
const historicalSessions = buildHistoricalMeshSessions({
|
|
6277
|
+
meshId,
|
|
6278
|
+
nodes: mesh.nodes || [],
|
|
6279
|
+
liveSessionRecords: liveMeshSessions,
|
|
6280
|
+
});
|
|
6281
|
+
const statusResult = {
|
|
3233
6282
|
success: true,
|
|
3234
6283
|
meshId: mesh.id,
|
|
3235
6284
|
meshName: mesh.name,
|
|
3236
6285
|
repoIdentity: mesh.repoIdentity,
|
|
3237
6286
|
defaultBranch: mesh.defaultBranch,
|
|
3238
|
-
refreshedAt
|
|
6287
|
+
refreshedAt,
|
|
6288
|
+
meshHost,
|
|
3239
6289
|
sourceOfTruth: {
|
|
3240
6290
|
membership: meshRecord?.source === 'inline_cache'
|
|
3241
6291
|
? 'coordinator_inline_mesh_cache'
|
|
3242
6292
|
: meshRecord?.source === 'local_config'
|
|
3243
6293
|
? 'local_mesh_config'
|
|
3244
6294
|
: 'inline_bootstrap_snapshot',
|
|
3245
|
-
coordinatorOwnsLiveTruth:
|
|
3246
|
-
|
|
6295
|
+
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
6296
|
+
meshHost: {
|
|
6297
|
+
owner: 'mesh_host_daemon',
|
|
6298
|
+
localRole: meshHost.role,
|
|
6299
|
+
hostDaemonId: meshHost.hostDaemonId,
|
|
6300
|
+
hostNodeId: meshHost.hostNodeId,
|
|
6301
|
+
hostAddress: meshHost.hostAddress,
|
|
6302
|
+
},
|
|
6303
|
+
...(requireDirectPeerTruth ? {
|
|
6304
|
+
currentStatus: directTruthSatisfied ? 'live_git_and_session_probes' : 'direct_peer_truth_unavailable',
|
|
6305
|
+
directPeerTruth: {
|
|
6306
|
+
required: true,
|
|
6307
|
+
satisfied: directTruthSatisfied,
|
|
6308
|
+
directEvidenceCount: effectiveDirectTruth.directEvidenceCount,
|
|
6309
|
+
localConfirmedCount: effectiveDirectTruth.localConfirmedCount,
|
|
6310
|
+
peerAttemptedCount: effectiveDirectTruth.peerAttemptedCount,
|
|
6311
|
+
peerConfirmedCount: effectiveDirectTruth.peerConfirmedCount,
|
|
6312
|
+
unavailableNodeIds: effectiveDirectTruth.unavailableNodeIds,
|
|
6313
|
+
partialNodeFailures: effectiveDirectTruth.unavailableNodeIds,
|
|
6314
|
+
},
|
|
6315
|
+
} : {}),
|
|
6316
|
+
historicalEvidenceOnly: ['recoveryHints', 'ledger.summary', 'queue.summary', 'historicalSessions'],
|
|
3247
6317
|
},
|
|
6318
|
+
branchConvergenceSummary: summarizeInlineMeshBranchConvergence(nodeStatuses),
|
|
6319
|
+
...(previewFreshness ? { previewFreshness, deployFreshness: previewFreshness } : {}),
|
|
3248
6320
|
nodes: nodeStatuses,
|
|
3249
6321
|
queue: { tasks: queue, summary: queueSummary },
|
|
3250
6322
|
ledger: { entries: ledgerEntries, summary: ledgerSummary },
|
|
6323
|
+
...(asyncRefineJobs.length > 0 ? { asyncRefineJobs } : {}),
|
|
6324
|
+
...(historicalSessions ? { historicalSessions } : {}),
|
|
6325
|
+
...(pendingCoordinatorEvents.length > 0 ? { pendingCoordinatorEvents } : {}),
|
|
3251
6326
|
};
|
|
6327
|
+
const { pendingCoordinatorEvents: _pendingCoordinatorEvents, ...cacheableStatusResult } = statusResult as any;
|
|
6328
|
+
const rememberedStatus = this.rememberAggregateMeshStatus(meshId, cacheableStatusResult, refreshReason);
|
|
6329
|
+
const returnedStatus = pendingCoordinatorEvents.length > 0
|
|
6330
|
+
? { ...rememberedStatus, pendingCoordinatorEvents }
|
|
6331
|
+
: rememberedStatus;
|
|
6332
|
+
logRepoMeshStatusDebug('return_live', {
|
|
6333
|
+
meshId,
|
|
6334
|
+
command: 'mesh_status',
|
|
6335
|
+
refreshRequested,
|
|
6336
|
+
refreshReason,
|
|
6337
|
+
meshSource: meshRecord.source,
|
|
6338
|
+
directTruth,
|
|
6339
|
+
summary: summarizeRepoMeshStatusDebug(returnedStatus),
|
|
6340
|
+
});
|
|
6341
|
+
return returnedStatus;
|
|
3252
6342
|
} catch (e: any) {
|
|
3253
6343
|
return { success: false, error: e.message };
|
|
3254
6344
|
}
|
|
@@ -3306,7 +6396,7 @@ export class DaemonCommandRouter {
|
|
|
3306
6396
|
|
|
3307
6397
|
// 3. Kill OS process if requested
|
|
3308
6398
|
if (killProcess) {
|
|
3309
|
-
const running = isIdeRunning(ideType);
|
|
6399
|
+
const running = await isIdeRunning(ideType);
|
|
3310
6400
|
if (running) {
|
|
3311
6401
|
LOG.info('StopIDE', `Killing IDE process: ${ideType}`);
|
|
3312
6402
|
const killed = await killIdeProcess(ideType);
|