@bridge_gpt/mcp-server 0.2.16 → 0.2.19
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/CONDUCTOR.md +75 -0
- package/README.md +2 -2
- package/build/agent-capabilities/probe-context.js +13 -3
- package/build/agent-capabilities/probes.js +262 -11
- package/build/agent-capabilities/reporter.js +1 -0
- package/build/agents.generated.js +3 -3
- package/build/backend-warnings.js +44 -0
- package/build/claude-settings.js +129 -0
- package/build/commands.generated.js +7 -6
- package/build/conductor/bridge-api-client.js +198 -18
- package/build/conductor/claude-hook.js +22 -4
- package/build/conductor/cli.js +76 -25
- package/build/conductor/deny-enforcement-preflight.js +96 -0
- package/build/conductor/doctor.js +183 -2
- package/build/conductor/done-gate.js +5 -0
- package/build/conductor/epic-reconcile.js +71 -14
- package/build/conductor/epic-runtime.js +839 -67
- package/build/conductor/epic-state.js +524 -63
- package/build/conductor/errors.js +156 -3
- package/build/conductor/event-accessors.js +252 -0
- package/build/conductor/file-scope-guard.js +201 -0
- package/build/conductor/github-mergeability.js +85 -0
- package/build/conductor/local-merge.js +47 -1
- package/build/conductor/merge-identity.js +41 -0
- package/build/conductor/merge-ledger.js +19 -72
- package/build/conductor/plan.js +12 -2
- package/build/conductor/pr-ci-producer.js +17 -2
- package/build/conductor/pr-discovery.js +11 -1
- package/build/conductor/producer-ledger.js +1 -1
- package/build/conductor/store.js +161 -18
- package/build/conductor/supervisor-config.js +4 -39
- package/build/conductor/supervisor-escalation.js +10 -26
- package/build/conductor/supervisor-ledger.js +5 -12
- package/build/conductor/supervisor-merge.js +32 -5
- package/build/conductor/supervisor-message-relay.js +2 -5
- package/build/conductor/supervisor-notification.js +1 -1
- package/build/conductor/supervisor-runtime.js +12 -54
- package/build/conductor/supervisor-state.js +4 -18
- package/build/conductor/supervisor-types.js +2 -2
- package/build/conductor/taxonomy.js +12 -0
- package/build/conductor/tools.js +28 -6
- package/build/conductor/worker-ledger-cli.js +244 -0
- package/build/conductor-bin.js +1800 -5166
- package/build/conductor-claude-hook-bin.js +4 -2
- package/build/doctor.js +40 -0
- package/build/executor/cli.js +229 -0
- package/build/executor/credentials.js +65 -0
- package/build/executor/deps.js +117 -0
- package/build/executor/env.js +79 -0
- package/build/executor/heartbeat.js +59 -0
- package/build/executor/http-client.js +131 -0
- package/build/executor/index.js +10 -0
- package/build/executor/job-errors.js +55 -0
- package/build/executor/job-log-registry.js +110 -0
- package/build/executor/job-runner.js +688 -0
- package/build/executor/job-types.js +60 -0
- package/build/executor/merge-job.js +155 -0
- package/build/executor/observation.js +123 -0
- package/build/executor/permissions.js +79 -0
- package/build/executor/preflight.js +144 -0
- package/build/executor/process.js +81 -0
- package/build/executor/prompt-spec.js +235 -0
- package/build/executor/results.js +134 -0
- package/build/executor/resume-pre-spawn.js +179 -0
- package/build/executor/runner.js +98 -0
- package/build/executor/terminal-mutation.js +34 -0
- package/build/executor/test-clock.js +109 -0
- package/build/executor/types.js +18 -0
- package/build/executor/verdict-artifact.js +53 -0
- package/build/executor/viewer-tabs.js +78 -0
- package/build/executor/watch-cli.js +113 -0
- package/build/executor/worker-command.js +106 -0
- package/build/executor/worker-finalization.js +97 -0
- package/build/executor/worker-log.js +92 -0
- package/build/executor/worktree-gc.js +134 -0
- package/build/executor/worktree-inspection.js +86 -0
- package/build/executor/worktree.js +103 -0
- package/build/index.js +13950 -9669
- package/build/install-bridge.js +25 -8
- package/build/install-doctor.js +387 -0
- package/build/mcp-invoke.js +19 -3
- package/build/mcp-provisioning.js +31 -25
- package/build/mcp-registration-doctor.js +27 -7
- package/build/mcp-server-invocation.js +152 -0
- package/build/pipelines.generated.js +31 -6
- package/build/readme.generated.js +1 -1
- package/build/regression-check.js +53 -1
- package/build/review-tickets.js +175 -21
- package/build/sfcc/reads-site-preference.js +52 -19
- package/build/start-tickets-conductor.js +47 -99
- package/build/start-tickets-prereqs.js +185 -4
- package/build/start-tickets.js +218 -180
- package/build/version.generated.js +1 -1
- package/build/visual-diff-worker.js +313 -0
- package/build/visual-diff.js +632 -0
- package/build/worktree-core.js +202 -0
- package/package.json +10 -6
- package/pipelines/review-ticket.json +24 -2
- package/public/css/main.min.css +3311 -1
- package/public/css/main.min.css.map +1 -1
- package/public/js/main.min.js +7924 -1
- package/public/js/main.min.js.map +1 -1
- package/smoke-test/SMOKE-TEST.md +5 -2
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared GitHub PR mergeability normalization + conflict classification (BAPI-494).
|
|
3
|
+
*
|
|
4
|
+
* The conductor gained ZERO mergeability awareness before this ticket: a sibling
|
|
5
|
+
* PR left `CONFLICTING`/`DIRTY` by an overlapping merge would sit at
|
|
6
|
+
* `ready_for_review` forever (catalog A4 / run-report F3). This module is the
|
|
7
|
+
* single, dependency-light source of truth for reading GitHub's
|
|
8
|
+
* `mergeable`/`mergeStateStatus` fields and deciding whether a PR is un-mergeable,
|
|
9
|
+
* so PR discovery, the done-gate binding resolution, and the local merge executor
|
|
10
|
+
* all classify a conflict the SAME way.
|
|
11
|
+
*
|
|
12
|
+
* Every helper is PURE, NON-THROWING, and FAIL-CLOSED: an unknown/missing/malformed
|
|
13
|
+
* mergeability value is treated as non-conflicting/unknown, never as a conflict, and
|
|
14
|
+
* raw `gh` process output is only ever matched against sanitized text — it is never
|
|
15
|
+
* returned, logged, or copied into event data.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Normalize a raw GitHub mergeability field. Returns a trimmed, uppercased string
|
|
19
|
+
* for a non-empty string value, and `null` for every other shape (missing, empty,
|
|
20
|
+
* whitespace-only, non-string). Never throws.
|
|
21
|
+
*/
|
|
22
|
+
export function normalizeGhMergeabilityValue(value) {
|
|
23
|
+
if (typeof value !== "string")
|
|
24
|
+
return null;
|
|
25
|
+
const trimmed = value.trim();
|
|
26
|
+
if (trimmed.length === 0)
|
|
27
|
+
return null;
|
|
28
|
+
return trimmed.toUpperCase();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Read `mergeable` and `mergeStateStatus` from an already-parsed `gh pr view`
|
|
32
|
+
* JSON record and return a normalized {@link GhPrMergeability}. Unknown/malformed
|
|
33
|
+
* fields become `null` (fail-closed) so a valid PR with an unreadable mergeability
|
|
34
|
+
* value is never mistaken for a conflict.
|
|
35
|
+
*/
|
|
36
|
+
export function parseGhPrMergeabilityFields(record) {
|
|
37
|
+
return {
|
|
38
|
+
mergeable: normalizeGhMergeabilityValue(record.mergeable),
|
|
39
|
+
mergeStateStatus: normalizeGhMergeabilityValue(record.mergeStateStatus),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* True only for the two authoritative conflict signals GitHub exposes:
|
|
44
|
+
* `mergeable === "CONFLICTING"` or `mergeStateStatus === "DIRTY"`. Every other
|
|
45
|
+
* combination — mergeable/clean, unknown, or null — is NOT a conflict. Fail-closed.
|
|
46
|
+
*/
|
|
47
|
+
export function isPrMergeConflict(mergeability) {
|
|
48
|
+
return mergeability.mergeable === "CONFLICTING" || mergeability.mergeStateStatus === "DIRTY";
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Sanitized signatures that a `gh pr merge` failure was caused by a merge
|
|
52
|
+
* conflict / non-fast-forward condition (rather than an auth, rate-limit, network,
|
|
53
|
+
* or not-found failure). Matched case-insensitively against the sanitized failure
|
|
54
|
+
* text; the raw output itself is never returned or logged.
|
|
55
|
+
*/
|
|
56
|
+
const MERGE_CONFLICT_OUTPUT_SIGNATURES = [
|
|
57
|
+
"merge conflict",
|
|
58
|
+
"merge conflicts",
|
|
59
|
+
"conflicting",
|
|
60
|
+
"conflict with the base branch",
|
|
61
|
+
"not possible to fast-forward",
|
|
62
|
+
"non-fast-forward",
|
|
63
|
+
"not mergeable",
|
|
64
|
+
"is not mergeable",
|
|
65
|
+
"cannot be cleanly created",
|
|
66
|
+
"would create a merge conflict",
|
|
67
|
+
];
|
|
68
|
+
/**
|
|
69
|
+
* Classify raw `gh pr merge` failure output as a LIKELY merge-conflict /
|
|
70
|
+
* non-fast-forward failure. Reads only `stdout`/`stderr` text for defensive
|
|
71
|
+
* substring matching and returns a boolean — it never returns, logs, or otherwise
|
|
72
|
+
* exposes the raw process output. Returns `false` for unrelated failures
|
|
73
|
+
* (authentication, rate limit, network timeout, repository not found, generic
|
|
74
|
+
* GraphQL errors).
|
|
75
|
+
*/
|
|
76
|
+
export function isLikelyGhMergeConflictOutput(output) {
|
|
77
|
+
if (!output)
|
|
78
|
+
return false;
|
|
79
|
+
const stdout = typeof output.stdout === "string" ? output.stdout : "";
|
|
80
|
+
const stderr = typeof output.stderr === "string" ? output.stderr : "";
|
|
81
|
+
const haystack = `${stdout}\n${stderr}`.toLowerCase();
|
|
82
|
+
if (haystack.trim().length === 0)
|
|
83
|
+
return false;
|
|
84
|
+
return MERGE_CONFLICT_OUTPUT_SIGNATURES.some((sig) => haystack.includes(sig));
|
|
85
|
+
}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { spawnSync } from "child_process";
|
|
20
20
|
import { pollCiChecksForCommit, } from "./bridge-api-client.js";
|
|
21
|
+
import { isLikelyGhMergeConflictOutput, isPrMergeConflict, parseGhPrMergeabilityFields, } from "./github-mergeability.js";
|
|
21
22
|
const MERGE_METHODS = new Set(["squash", "merge", "rebase"]);
|
|
22
23
|
/**
|
|
23
24
|
* Hard wall-clock cap on every `gh` subprocess. The epic-tick runs in a single
|
|
@@ -180,7 +181,52 @@ export function makeLocalMergeExecutor(options = {}, deps = {}) {
|
|
|
180
181
|
// 4. Provider merge.
|
|
181
182
|
const merge = run("gh", ["pr", "merge", String(pr), `--${method}`, "--match-head-commit", expectedSha], ghEnv);
|
|
182
183
|
if (merge.status !== 0) {
|
|
183
|
-
|
|
184
|
+
// A hung `gh` (killed by the wall-clock timeout) is operationally distinct
|
|
185
|
+
// from a permission/conflict failure — keep it mapped to gh_merge_timeout.
|
|
186
|
+
if (merge.timedOut) {
|
|
187
|
+
const reason = "gh_merge_timeout";
|
|
188
|
+
return buildResponse(request, "failed", reason, false, [
|
|
189
|
+
{ type: "merge.attempted", status: "attempted", details: baseDetails },
|
|
190
|
+
{ type: "merge.failed", status: "failed", reason, details: baseDetails },
|
|
191
|
+
]);
|
|
192
|
+
}
|
|
193
|
+
// BAPI-494: classify a conflict / non-fast-forward failure distinctly so it
|
|
194
|
+
// folds to `blocked` and remediates instead of hiding behind gh_merge_failed.
|
|
195
|
+
// First match the sanitized failure output; if inconclusive, do ONE follow-up
|
|
196
|
+
// mergeability re-read (only AFTER a merge failure — never before a successful
|
|
197
|
+
// or guarded merge) and check it. Raw stdout/stderr is used only for defensive
|
|
198
|
+
// classification and is never copied into event data or logs.
|
|
199
|
+
let mergeability = { mergeable: null, mergeStateStatus: null };
|
|
200
|
+
let isConflict = isLikelyGhMergeConflictOutput(merge);
|
|
201
|
+
if (!isConflict) {
|
|
202
|
+
const recheck = run("gh", ["pr", "view", String(pr), "--json", "mergeable,mergeStateStatus"], ghEnv);
|
|
203
|
+
if (recheck.status === 0) {
|
|
204
|
+
try {
|
|
205
|
+
mergeability = parseGhPrMergeabilityFields(JSON.parse(recheck.stdout));
|
|
206
|
+
isConflict = isPrMergeConflict(mergeability);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
/* best-effort classification only */
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (isConflict) {
|
|
214
|
+
const reason = "gh_merge_conflict";
|
|
215
|
+
// Head-scoped to the attempted head SHA so the fold blocks the exact head; a
|
|
216
|
+
// later rebase clears it via the stale-head logic. mergeable/mergeStateStatus
|
|
217
|
+
// are included only when known.
|
|
218
|
+
const conflictDetails = {
|
|
219
|
+
...baseDetails,
|
|
220
|
+
head_sha: expectedSha,
|
|
221
|
+
...(mergeability.mergeable ? { mergeable: mergeability.mergeable } : {}),
|
|
222
|
+
...(mergeability.mergeStateStatus ? { mergeStateStatus: mergeability.mergeStateStatus } : {}),
|
|
223
|
+
};
|
|
224
|
+
return buildResponse(request, "failed", reason, false, [
|
|
225
|
+
{ type: "merge.attempted", status: "attempted", details: baseDetails },
|
|
226
|
+
{ type: "merge.conflict", status: "failed", reason, details: conflictDetails },
|
|
227
|
+
]);
|
|
228
|
+
}
|
|
229
|
+
const mergeFailReason = "gh_merge_failed";
|
|
184
230
|
return buildResponse(request, "failed", mergeFailReason, false, [
|
|
185
231
|
{ type: "merge.attempted", status: "attempted", details: baseDetails },
|
|
186
232
|
{ type: "merge.failed", status: "failed", reason: mergeFailReason, details: baseDetails },
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Low-level merge-identity primitives (BAPI-493).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from `merge-ledger.ts` so the typed event-accessor layer
|
|
5
|
+
* (`event-accessors.ts`) can build a canonical {@link MergeActionIdentity}
|
|
6
|
+
* without importing the merge-ledger store/emit surface (which would create an
|
|
7
|
+
* import cycle: merge-ledger → event-accessors → merge-ledger).
|
|
8
|
+
*
|
|
9
|
+
* This module depends ONLY on the pure boundary validators in `git-ci-types.ts`.
|
|
10
|
+
* It performs no I/O and holds no VCS write credentials. The deterministic action
|
|
11
|
+
* key `merge:{repo}:{pr}:{head_sha}:{gate}` is kept in lock-step with the Python
|
|
12
|
+
* `build_merge_action_key`; the gate-identity segment mirrors the Python
|
|
13
|
+
* `normalize_gate_identity`.
|
|
14
|
+
*/
|
|
15
|
+
import { normalizePrNumber, normalizeRepoName, normalizeSha } from "./git-ci-types.js";
|
|
16
|
+
/**
|
|
17
|
+
* Compose the stable gate-identity segment, mirroring the Python
|
|
18
|
+
* `normalize_gate_identity`: `{name}@{config_hash}` when a hash is present
|
|
19
|
+
* (lower-cased), otherwise just the gate name.
|
|
20
|
+
*/
|
|
21
|
+
export function buildGateIdentity(gateName, configHash) {
|
|
22
|
+
const name = gateName.trim();
|
|
23
|
+
const hash = typeof configHash === "string" ? configHash.trim() : "";
|
|
24
|
+
return hash ? `${name}@${hash.toLowerCase()}` : name;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build the deterministic action key. Normalizes repo / PR / head SHA exactly as
|
|
28
|
+
* the Python side does (lower-cased SHA, trimmed repo, positive integer PR) so the
|
|
29
|
+
* conductor-computed key and the API-recomputed key are byte-identical. Throws on
|
|
30
|
+
* any invalid component. The branch name is never part of the key.
|
|
31
|
+
*/
|
|
32
|
+
export function makeMergeActionKey(repo, prNumber, headSha, gateIdentity) {
|
|
33
|
+
const r = normalizeRepoName(repo);
|
|
34
|
+
const pr = normalizePrNumber(prNumber);
|
|
35
|
+
const sha = normalizeSha(headSha);
|
|
36
|
+
const gate = (gateIdentity ?? "").trim();
|
|
37
|
+
if (r === null || pr === null || sha === null || gate.length === 0) {
|
|
38
|
+
throw new Error("invalid merge action key component");
|
|
39
|
+
}
|
|
40
|
+
return `merge:${r}:${pr}:${sha}:${gate}`;
|
|
41
|
+
}
|
|
@@ -16,82 +16,29 @@
|
|
|
16
16
|
* It performs NO privileged action and never handles VCS write credentials.
|
|
17
17
|
*/
|
|
18
18
|
import { createHash } from "node:crypto";
|
|
19
|
-
import {
|
|
19
|
+
import { getMergeIdentity } from "./event-accessors.js";
|
|
20
|
+
import { buildGateIdentity, makeMergeActionKey, } from "./merge-identity.js";
|
|
20
21
|
import { emitConductorEvent, openReadonlyConductorDatabaseIfExists, } from "./store.js";
|
|
22
|
+
// BAPI-493: the merge-identity primitives now live in the dependency-light
|
|
23
|
+
// `merge-identity.ts` module (so the typed event-accessor layer can build a
|
|
24
|
+
// MergeActionIdentity without an import cycle). They are re-exported here so
|
|
25
|
+
// existing importers (`supervisor-merge.ts`, tests) keep a stable API surface.
|
|
26
|
+
export { buildGateIdentity, makeMergeActionKey };
|
|
21
27
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
*
|
|
33
|
-
* the Python side does (lower-cased SHA, trimmed repo, positive integer PR) so the
|
|
34
|
-
* conductor-computed key and the API-recomputed key are byte-identical. Throws on
|
|
35
|
-
* any invalid component. The branch name is never part of the key.
|
|
36
|
-
*/
|
|
37
|
-
export function makeMergeActionKey(repo, prNumber, headSha, gateIdentity) {
|
|
38
|
-
const r = normalizeRepoName(repo);
|
|
39
|
-
const pr = normalizePrNumber(prNumber);
|
|
40
|
-
const sha = normalizeSha(headSha);
|
|
41
|
-
const gate = (gateIdentity ?? "").trim();
|
|
42
|
-
if (r === null || pr === null || sha === null || gate.length === 0) {
|
|
43
|
-
throw new Error("invalid merge action key component");
|
|
44
|
-
}
|
|
45
|
-
return `merge:${r}:${pr}:${sha}:${gate}`;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Resolve the immutable merge identity from a `gate.met` event. Returns `null`
|
|
49
|
-
* unless the event is a worker-scoped `gate.met` carrying a complete PR binding
|
|
50
|
-
* (repo, pr_number, head_sha, gate_name). Run-level or incomplete events yield
|
|
51
|
-
* `null`. Branch-name fields, if present, are ignored.
|
|
28
|
+
* Resolve the immutable merge identity from a `gate.met` event. Returns a non-null
|
|
29
|
+
* identity for any complete PR-bound `gate.met` (repo, pr_number, head_sha,
|
|
30
|
+
* gate_name), regardless of worker attribution — `worker_id` is attribution
|
|
31
|
+
* metadata only and is not required for merge identity extraction (BAPI-525
|
|
32
|
+
* Change A). Incomplete PR bindings remain fail-closed and yield `null`.
|
|
33
|
+
* Branch-name fields, if present, are ignored.
|
|
34
|
+
*
|
|
35
|
+
* BAPI-493: canonical parsing now lives in the typed accessor layer — this is a
|
|
36
|
+
* thin compatibility wrapper over {@link getMergeIdentity} so `supervisor-merge.ts`
|
|
37
|
+
* and other callers keep an unchanged call site while the hand-rolled
|
|
38
|
+
* `data.details` reader is retired.
|
|
52
39
|
*/
|
|
53
40
|
export function extractMergeActionIdentityFromGateEvent(event) {
|
|
54
|
-
|
|
55
|
-
return null;
|
|
56
|
-
// Worker scope is required — a run-level gate.met never binds a specific worker.
|
|
57
|
-
if (typeof event.worker_id !== "string" || event.worker_id.trim().length === 0) {
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
const data = event.data ?? {};
|
|
61
|
-
const details = data.details;
|
|
62
|
-
if (!details || typeof details !== "object")
|
|
63
|
-
return null;
|
|
64
|
-
const d = details;
|
|
65
|
-
const repo = normalizeRepoName(d.repo);
|
|
66
|
-
const prNumber = normalizePrNumber(d.pr_number);
|
|
67
|
-
const headSha = normalizeSha(d.head_sha);
|
|
68
|
-
const gateName = typeof d.gate_name === "string" ? d.gate_name.trim() : "";
|
|
69
|
-
if (repo === null || prNumber === null || headSha === null || gateName.length === 0) {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
const configHash = typeof d.config_hash === "string" && d.config_hash.trim().length > 0
|
|
73
|
-
? d.config_hash.trim()
|
|
74
|
-
: null;
|
|
75
|
-
const requiredChecks = Array.isArray(d.required_checks)
|
|
76
|
-
? d.required_checks.filter((c) => typeof c === "string" && c.trim().length > 0)
|
|
77
|
-
: [];
|
|
78
|
-
const gateIdentity = buildGateIdentity(gateName, configHash);
|
|
79
|
-
const actionKey = makeMergeActionKey(repo, prNumber, headSha, gateIdentity);
|
|
80
|
-
return {
|
|
81
|
-
repo,
|
|
82
|
-
pr_number: prNumber,
|
|
83
|
-
head_sha: headSha,
|
|
84
|
-
gate_name: gateName,
|
|
85
|
-
config_hash: configHash,
|
|
86
|
-
required_checks: requiredChecks,
|
|
87
|
-
gate_identity: gateIdentity,
|
|
88
|
-
action_key: actionKey,
|
|
89
|
-
gate_event: {
|
|
90
|
-
id: typeof event.id === "string" ? event.id : undefined,
|
|
91
|
-
seq: typeof event.seq === "number" ? event.seq : undefined,
|
|
92
|
-
time: typeof event.time === "string" ? event.time : undefined,
|
|
93
|
-
},
|
|
94
|
-
};
|
|
41
|
+
return getMergeIdentity(event);
|
|
95
42
|
}
|
|
96
43
|
/**
|
|
97
44
|
* Derive a deterministic, UUID-shaped event id from the event type plus action
|
package/build/conductor/plan.js
CHANGED
|
@@ -31,12 +31,16 @@ import { stableJsonHash } from "./git-ci-types.js";
|
|
|
31
31
|
*
|
|
32
32
|
* - Trims outer whitespace from every `ticket_key` (nodes and edges).
|
|
33
33
|
* - Sorts each node's `depends_on` array alphabetically.
|
|
34
|
+
* - Sorts each node's `touched_files` array alphabetically, when present.
|
|
34
35
|
* - Sorts `nodes` lexicographically by normalized `ticket_key`.
|
|
35
36
|
* - Sorts `edges` lexicographically by `from` then `to`.
|
|
37
|
+
* - Sorts each edge's `overlap_files` array alphabetically, when present.
|
|
36
38
|
*
|
|
37
39
|
* The canonical form is fed to {@link hashPlan} / `stableJsonHash`, which
|
|
38
40
|
* additionally sorts object keys recursively, so two independently constructed
|
|
39
|
-
* equal plans always produce a byte-identical SHA-256 digest.
|
|
41
|
+
* equal plans always produce a byte-identical SHA-256 digest. Only fields
|
|
42
|
+
* already present on the input are normalized — canonicalization never
|
|
43
|
+
* derives new data from filesystem, git, network, or LLM state.
|
|
40
44
|
*/
|
|
41
45
|
export function canonicalizePlanDAG(plan) {
|
|
42
46
|
const nodes = plan.nodes
|
|
@@ -44,10 +48,16 @@ export function canonicalizePlanDAG(plan) {
|
|
|
44
48
|
...node,
|
|
45
49
|
ticket_key: node.ticket_key.trim(),
|
|
46
50
|
depends_on: [...node.depends_on].map((k) => k.trim()).sort(),
|
|
51
|
+
...(node.touched_files ? { touched_files: [...node.touched_files].sort() } : {}),
|
|
47
52
|
}))
|
|
48
53
|
.sort((a, b) => a.ticket_key.localeCompare(b.ticket_key));
|
|
49
54
|
const edges = [...plan.edges]
|
|
50
|
-
.map((e) => ({
|
|
55
|
+
.map((e) => ({
|
|
56
|
+
from: e.from.trim(),
|
|
57
|
+
to: e.to.trim(),
|
|
58
|
+
...(e.kind ? { kind: e.kind } : {}),
|
|
59
|
+
...(e.overlap_files ? { overlap_files: [...e.overlap_files].sort() } : {}),
|
|
60
|
+
}))
|
|
51
61
|
.sort((a, b) => {
|
|
52
62
|
const cmp = a.from.localeCompare(b.from);
|
|
53
63
|
return cmp !== 0 ? cmp : a.to.localeCompare(b.to);
|
|
@@ -23,6 +23,11 @@ async function _fetchGateConfigDefault(access) {
|
|
|
23
23
|
}
|
|
24
24
|
import { resolvePrHeadBinding } from "./pr-discovery.js";
|
|
25
25
|
import { emitConductorEventIfNew } from "./producer-ledger.js";
|
|
26
|
+
// In-process ledger writer — the DEFAULT emit sink for conductor-OWNED paths
|
|
27
|
+
// (the CLI/supervisor/epic runtimes that legitimately own the better-sqlite3
|
|
28
|
+
// load). The worker `wait_for_done_gate` MCP path injects a CLI-subprocess emitter
|
|
29
|
+
// instead (BAPI-527) so the worker Node never loads the native binary.
|
|
30
|
+
import { emitConductorEvent as emitConductorEventInProcess } from "./store.js";
|
|
26
31
|
const PRODUCER_OBSERVED_VIA = "pr-ci-producer";
|
|
27
32
|
/** Bounded wait constants, consistent with the existing wait_for_event pattern. */
|
|
28
33
|
export const WAIT_FOR_GATE_TIMEOUT_MAX_MS = 120_000;
|
|
@@ -124,7 +129,15 @@ function defaultSleep(ms) {
|
|
|
124
129
|
* resolved exactly once by the caller.
|
|
125
130
|
*/
|
|
126
131
|
async function observeWithResolved(binding, access, gateConfig, deps) {
|
|
127
|
-
|
|
132
|
+
// The event-WRITE sink defaults to the in-process store; the worker gate path
|
|
133
|
+
// injects a CLI-subprocess emitter (BAPI-527). When a caller supplies its own
|
|
134
|
+
// `emitIfNew` (e.g. unit tests / conductor-owned callers), that fully overrides
|
|
135
|
+
// this and the injected write sink is not consulted.
|
|
136
|
+
const emitConductorEventFn = deps.emitConductorEvent ?? emitConductorEventInProcess;
|
|
137
|
+
const emitIfNew = deps.emitIfNew ??
|
|
138
|
+
((input, dimensions) => emitConductorEventIfNew(input, dimensions, {
|
|
139
|
+
emitEvent: emitConductorEventFn,
|
|
140
|
+
}));
|
|
128
141
|
const pollCi = deps.pollCi ?? pollCiChecksForCommit;
|
|
129
142
|
const now = deps.now ?? (() => new Date().toISOString());
|
|
130
143
|
// Resolve run_id/worker_id for attribution. Env takes precedence; fall back
|
|
@@ -193,7 +206,9 @@ async function observeWithResolved(binding, access, gateConfig, deps) {
|
|
|
193
206
|
let reviewSnapshot = null;
|
|
194
207
|
try {
|
|
195
208
|
const reviewObservation = await observeReviewWithResolved(binding, access, gateConfig, {
|
|
196
|
-
|
|
209
|
+
// Reuse the resolved `emitIfNew` so review-state events emitted alongside CI
|
|
210
|
+
// route through the SAME (possibly CLI-subprocess) write sink (BAPI-527).
|
|
211
|
+
emitIfNew,
|
|
197
212
|
env: deps.env,
|
|
198
213
|
});
|
|
199
214
|
reviewSnapshot = reviewObservation.snapshot;
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { execFileSync } from "node:child_process";
|
|
13
13
|
import { normalizePrNumber, normalizeRepoName, normalizeSha } from "./git-ci-types.js";
|
|
14
|
+
import { parseGhPrMergeabilityFields } from "./github-mergeability.js";
|
|
14
15
|
import { getGitWorktreeContext } from "./git-inspection.js";
|
|
15
16
|
/** Bounded timeout for the one-shot `gh` lookup. */
|
|
16
17
|
export const GH_COMMAND_TIMEOUT_MS = 5_000;
|
|
@@ -30,7 +31,13 @@ export function runGhCommand(args, options = {}) {
|
|
|
30
31
|
return { ok: false, stdout: "" };
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
|
-
const GH_PR_VIEW_ARGS = [
|
|
34
|
+
const GH_PR_VIEW_ARGS = [
|
|
35
|
+
"pr",
|
|
36
|
+
"view",
|
|
37
|
+
"--json",
|
|
38
|
+
// BAPI-494: mergeability fields added to the SAME one-shot call — no new gh process.
|
|
39
|
+
"number,headRefOid,headRefName,url,state,mergeable,mergeStateStatus",
|
|
40
|
+
];
|
|
34
41
|
/**
|
|
35
42
|
* Perform a one-shot `gh pr view` lookup for the current branch's PR. Returns a
|
|
36
43
|
* normalized {@link DiscoveredPr} or `null` when gh is unavailable, there is no
|
|
@@ -55,10 +62,13 @@ export function discoverPrWithGhCli(options = {}, deps = {}) {
|
|
|
55
62
|
const state = typeof record.state === "string" ? record.state : "";
|
|
56
63
|
if (number === null || state.length === 0)
|
|
57
64
|
return null;
|
|
65
|
+
const mergeability = parseGhPrMergeabilityFields(record);
|
|
58
66
|
const discovered = {
|
|
59
67
|
number,
|
|
60
68
|
head_sha: normalizeSha(record.headRefOid),
|
|
61
69
|
state,
|
|
70
|
+
mergeable: mergeability.mergeable,
|
|
71
|
+
mergeStateStatus: mergeability.mergeStateStatus,
|
|
62
72
|
};
|
|
63
73
|
if (typeof record.headRefName === "string" && record.headRefName.trim().length > 0) {
|
|
64
74
|
discovered.head_ref = record.headRefName.trim();
|
|
@@ -37,7 +37,7 @@ export function makeStableProducerEventId(dedupeKey) {
|
|
|
37
37
|
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
|
|
38
38
|
}
|
|
39
39
|
/** Heuristically detect a SQLite duplicate-id / UNIQUE constraint failure. */
|
|
40
|
-
function isDuplicateConstraintError(error) {
|
|
40
|
+
export function isDuplicateConstraintError(error) {
|
|
41
41
|
if (!error || typeof error !== "object")
|
|
42
42
|
return false;
|
|
43
43
|
const code = error.code;
|
package/build/conductor/store.js
CHANGED
|
@@ -38,6 +38,68 @@ export class ConductorPersistenceUnavailableError extends Error {
|
|
|
38
38
|
this.name = "ConductorPersistenceUnavailableError";
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
+
/** The native module whose load status this store owns. */
|
|
42
|
+
const LEDGER_NATIVE_MODULE_NAME = "better-sqlite3";
|
|
43
|
+
/**
|
|
44
|
+
* Thrown when `better-sqlite3` is present but fails to LOAD for this Node runtime
|
|
45
|
+
* (ABI/`NODE_MODULE_VERSION` mismatch or a generic `dlopen` failure) — distinct
|
|
46
|
+
* from "module simply not installed", which stays on the base
|
|
47
|
+
* {@link ConductorPersistenceUnavailableError} (503) path. Subclassing the base
|
|
48
|
+
* keeps existing `instanceof`/name-based degradation working while carrying the
|
|
49
|
+
* structured, secret-free {@link ConductorNativeModuleLoadDetails}. The raw
|
|
50
|
+
* caught error, stack, path, and message are deliberately NOT stored on the
|
|
51
|
+
* public instance.
|
|
52
|
+
*/
|
|
53
|
+
export class ConductorNativeModuleLoadError extends ConductorPersistenceUnavailableError {
|
|
54
|
+
details;
|
|
55
|
+
failureKind;
|
|
56
|
+
constructor(failureKind, details) {
|
|
57
|
+
super("Conductor ledger native module failed to load for this Node runtime.");
|
|
58
|
+
this.name = "ConductorNativeModuleLoadError";
|
|
59
|
+
this.failureKind = failureKind;
|
|
60
|
+
this.details = details;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Build the allowlisted runtime details for the CURRENT process. */
|
|
64
|
+
export function buildNativeModuleLoadDetails() {
|
|
65
|
+
return {
|
|
66
|
+
node_version: process.version,
|
|
67
|
+
node_modules_abi: process.versions.modules ?? "unknown",
|
|
68
|
+
module: LEDGER_NATIVE_MODULE_NAME,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Classify a caught `better-sqlite3` load failure. Inspects ONLY the error `code`
|
|
73
|
+
* and `message` locally; the raw text never leaves this helper.
|
|
74
|
+
*
|
|
75
|
+
* - `MODULE_NOT_FOUND` / `ERR_MODULE_NOT_FOUND` -> base
|
|
76
|
+
* {@link ConductorPersistenceUnavailableError} ("not installed", 503).
|
|
77
|
+
* - `NODE_MODULE_VERSION` / "compiled against a different Node.js version" /
|
|
78
|
+
* "module version mismatch" -> native-load `abi_mismatch`.
|
|
79
|
+
* - `ERR_DLOPEN_FAILED` / generic `dlopen` -> native-load `dlopen_failed`.
|
|
80
|
+
* - Anything else -> base {@link ConductorPersistenceUnavailableError}.
|
|
81
|
+
*/
|
|
82
|
+
export function classifyDatabaseModuleLoadFailure(error) {
|
|
83
|
+
const code = error && typeof error === "object" ? error.code : undefined;
|
|
84
|
+
const rawMessage = error && typeof error === "object" && typeof error.message === "string"
|
|
85
|
+
? (error.message)
|
|
86
|
+
: "";
|
|
87
|
+
const lowered = rawMessage.toLowerCase();
|
|
88
|
+
if (code === "MODULE_NOT_FOUND" || code === "ERR_MODULE_NOT_FOUND") {
|
|
89
|
+
return new ConductorPersistenceUnavailableError();
|
|
90
|
+
}
|
|
91
|
+
const isAbiMismatch = lowered.includes("node_module_version") ||
|
|
92
|
+
lowered.includes("compiled against a different node.js version") ||
|
|
93
|
+
lowered.includes("module version mismatch");
|
|
94
|
+
if (isAbiMismatch) {
|
|
95
|
+
return new ConductorNativeModuleLoadError("abi_mismatch", buildNativeModuleLoadDetails());
|
|
96
|
+
}
|
|
97
|
+
const isDlopen = code === "ERR_DLOPEN_FAILED" || lowered.includes("dlopen");
|
|
98
|
+
if (isDlopen) {
|
|
99
|
+
return new ConductorNativeModuleLoadError("dlopen_failed", buildNativeModuleLoadDetails());
|
|
100
|
+
}
|
|
101
|
+
return new ConductorPersistenceUnavailableError();
|
|
102
|
+
}
|
|
41
103
|
/**
|
|
42
104
|
* Cached, concurrency-safe async loader for the optional `better-sqlite3` module.
|
|
43
105
|
*
|
|
@@ -50,13 +112,21 @@ export class ConductorPersistenceUnavailableError extends Error {
|
|
|
50
112
|
* warning is emitted exactly once and every subsequent call throws
|
|
51
113
|
* {@link ConductorPersistenceUnavailableError} immediately, without re-attempting the
|
|
52
114
|
* failing import on every Conductor operation.
|
|
115
|
+
*
|
|
116
|
+
* BAPI-526: the caught error is CLASSIFIED (not swallowed) into a TYPED failure —
|
|
117
|
+
* ABI/dlopen mismatches become {@link ConductorNativeModuleLoadError} carrying
|
|
118
|
+
* `node_version`/`node_modules_abi`, while "module not installed" stays the base
|
|
119
|
+
* {@link ConductorPersistenceUnavailableError}. That typed failure is cached and
|
|
120
|
+
* re-thrown on every later call so the classification is never lost.
|
|
53
121
|
*/
|
|
54
122
|
let databaseModulePromise = null;
|
|
55
|
-
let
|
|
123
|
+
let dbLoadFailure = null;
|
|
124
|
+
let dbLoadDiagnosticEmitted = false;
|
|
56
125
|
async function getDatabaseModule() {
|
|
57
|
-
// Cached
|
|
58
|
-
|
|
59
|
-
|
|
126
|
+
// Cached typed failure: never re-attempt a known-failing native import, and
|
|
127
|
+
// preserve the ABI/dlopen classification across every subsequent call.
|
|
128
|
+
if (dbLoadFailure) {
|
|
129
|
+
throw dbLoadFailure;
|
|
60
130
|
}
|
|
61
131
|
if (databaseModulePromise)
|
|
62
132
|
return databaseModulePromise;
|
|
@@ -67,13 +137,25 @@ async function getDatabaseModule() {
|
|
|
67
137
|
const ctor = (mod.default ?? mod);
|
|
68
138
|
return ctor;
|
|
69
139
|
}
|
|
70
|
-
catch {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
140
|
+
catch (err) {
|
|
141
|
+
const classified = classifyDatabaseModuleLoadFailure(err);
|
|
142
|
+
dbLoadFailure = classified;
|
|
143
|
+
// Drop the rejected promise so the typed `dbLoadFailure` above is the sole
|
|
144
|
+
// cached state (the rejected promise itself must not be the only record).
|
|
145
|
+
databaseModulePromise = null;
|
|
146
|
+
if (!dbLoadDiagnosticEmitted) {
|
|
147
|
+
dbLoadDiagnosticEmitted = true;
|
|
148
|
+
// Fixed, path-free, secret-free diagnostic: module name, current Node
|
|
149
|
+
// version, current ABI, and coarse failure kind ONLY. Never the raw
|
|
150
|
+
// dlopen path, stack, or caught message.
|
|
151
|
+
const kind = classified instanceof ConductorNativeModuleLoadError
|
|
152
|
+
? classified.failureKind
|
|
153
|
+
: "unavailable";
|
|
154
|
+
console.error(`[conductor] ${LEDGER_NATIVE_MODULE_NAME} native module could not be loaded ` +
|
|
155
|
+
`(node=${process.version} abi=${process.versions.modules ?? "unknown"} kind=${kind}). ` +
|
|
74
156
|
"Conductor coordination features are disabled for this session; core MCP tools are unaffected.");
|
|
75
157
|
}
|
|
76
|
-
throw
|
|
158
|
+
throw classified;
|
|
77
159
|
}
|
|
78
160
|
})();
|
|
79
161
|
return databaseModulePromise;
|
|
@@ -183,11 +265,15 @@ export function resolveConductorStoreConfig(env = process.env) {
|
|
|
183
265
|
* CHECK vocabulary. Bumped to 7 because the durable parse-after-merge fold adds
|
|
184
266
|
* the `parse.triggered` event type to the same `events.type` CHECK vocabulary
|
|
185
267
|
* (replacing the in-memory parse-wait map so stateless epic-tick invocations can
|
|
186
|
-
* fold a merged ticket to `done`).
|
|
187
|
-
*
|
|
188
|
-
* the
|
|
268
|
+
* fold a merged ticket to `done`). Bumped to 8 in BAPI-527 because the durable
|
|
269
|
+
* parse-after-merge TERMINAL fold adds the `parse.succeeded` and `parse.failed`
|
|
270
|
+
* event types to the same `events.type` CHECK vocabulary (so the stateless
|
|
271
|
+
* epic-tick can fold a merged ticket to `done` on terminal parse success and
|
|
272
|
+
* `blocked` on permanent parse failure directly from the ledger). Older ledgers
|
|
273
|
+
* stamped at a lower version are rebuilt by {@link migrateConductorSchemaIfNeeded}
|
|
274
|
+
* so their CHECK clause accepts the current taxonomy.
|
|
189
275
|
*/
|
|
190
|
-
export const CURRENT_CONDUCTOR_SCHEMA_VERSION =
|
|
276
|
+
export const CURRENT_CONDUCTOR_SCHEMA_VERSION = 8;
|
|
191
277
|
/** Render the taxonomy `CHECK (type IN (...))` clause from the single source of truth. */
|
|
192
278
|
function buildTypeCheckClause() {
|
|
193
279
|
const list = SEMANTIC_EVENT_TYPES.map((t) => `'${t}'`).join(", ");
|
|
@@ -327,15 +413,15 @@ export function migrateConductorSchemaIfNeeded(db) {
|
|
|
327
413
|
return;
|
|
328
414
|
}
|
|
329
415
|
const migrate = db.transaction(() => {
|
|
330
|
-
// Scratch table named for the current schema version (
|
|
331
|
-
// migration rebuild never reads as a stale pre-
|
|
332
|
-
db.exec(buildEventsTableSql("
|
|
333
|
-
db.exec(`INSERT INTO
|
|
416
|
+
// Scratch table named for the current schema version (v8, BAPI-527) so the
|
|
417
|
+
// migration rebuild never reads as a stale pre-parse-terminal-taxonomy artifact.
|
|
418
|
+
db.exec(buildEventsTableSql("events_migrated_v8"));
|
|
419
|
+
db.exec(`INSERT INTO events_migrated_v8
|
|
334
420
|
(seq, id, source, type, subject, run_id, worker_id, producer, schema_version, time, data_json, confidence, observed_via, created_at)
|
|
335
421
|
SELECT seq, id, source, type, subject, run_id, worker_id, producer, schema_version, time, data_json, confidence, observed_via, created_at
|
|
336
422
|
FROM events`);
|
|
337
423
|
db.exec("DROP TABLE events");
|
|
338
|
-
db.exec("ALTER TABLE
|
|
424
|
+
db.exec("ALTER TABLE events_migrated_v8 RENAME TO events");
|
|
339
425
|
db.exec(buildEventsIndexesSql());
|
|
340
426
|
// Ensure auxiliary tables and their relay indexes exist on older ledgers too.
|
|
341
427
|
db.exec(buildAuxiliaryTablesSql());
|
|
@@ -818,6 +904,56 @@ export async function upsertSupervisorProjection(input, config = resolveConducto
|
|
|
818
904
|
* and network-home degradation. Never creates the DB, inserts a diagnostic
|
|
819
905
|
* event, or mutates WAL/schema state.
|
|
820
906
|
*/
|
|
907
|
+
/**
|
|
908
|
+
* Read-only probe of whether the ledger native binding actually loads for this
|
|
909
|
+
* Node runtime (BAPI-526). Reuses the lazy loader so it reflects the REAL load
|
|
910
|
+
* outcome, and NEVER opens the SQLite database, runs migrations, creates files,
|
|
911
|
+
* or emits events. On success reports `loads: true`; on a native-load failure
|
|
912
|
+
* reports the coarse `failure_kind` and marks the host degraded. All returned
|
|
913
|
+
* fields are secret-free and path-free.
|
|
914
|
+
*/
|
|
915
|
+
export async function inspectConductorLedgerNativeModuleLoad() {
|
|
916
|
+
const details = buildNativeModuleLoadDetails();
|
|
917
|
+
try {
|
|
918
|
+
await getDatabaseModule();
|
|
919
|
+
return {
|
|
920
|
+
module: details.module,
|
|
921
|
+
node_version: details.node_version,
|
|
922
|
+
node_modules_abi: details.node_modules_abi,
|
|
923
|
+
loads: true,
|
|
924
|
+
failure_kind: "none",
|
|
925
|
+
degraded: false,
|
|
926
|
+
warning: null,
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
catch (err) {
|
|
930
|
+
if (err instanceof ConductorNativeModuleLoadError) {
|
|
931
|
+
return {
|
|
932
|
+
...err.details,
|
|
933
|
+
loads: false,
|
|
934
|
+
failure_kind: err.failureKind,
|
|
935
|
+
degraded: true,
|
|
936
|
+
warning: "better-sqlite3 failed to load for this Node runtime; conductor ledger operations are degraded.",
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
if (err instanceof ConductorPersistenceUnavailableError) {
|
|
940
|
+
return {
|
|
941
|
+
...details,
|
|
942
|
+
loads: false,
|
|
943
|
+
failure_kind: "module_not_found",
|
|
944
|
+
degraded: true,
|
|
945
|
+
warning: "better-sqlite3 is not installed; conductor ledger operations are unavailable.",
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
return {
|
|
949
|
+
...details,
|
|
950
|
+
loads: false,
|
|
951
|
+
failure_kind: "unknown",
|
|
952
|
+
degraded: true,
|
|
953
|
+
warning: "better-sqlite3 could not be loaded; conductor ledger operations are degraded.",
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
}
|
|
821
957
|
export async function doctorConductorLedger(config = resolveConductorStoreConfig()) {
|
|
822
958
|
const health = getConductorPathHealth();
|
|
823
959
|
const network = detectNetworkMountedHome();
|
|
@@ -825,6 +961,12 @@ export async function doctorConductorLedger(config = resolveConductorStoreConfig
|
|
|
825
961
|
if (network.network_mounted && network.warning) {
|
|
826
962
|
warnings.push(network.warning);
|
|
827
963
|
}
|
|
964
|
+
// Read-only native-binding load probe (BAPI-526). Reflects the real lazy-loader
|
|
965
|
+
// outcome; never opens/creates/migrates the ledger.
|
|
966
|
+
const native_load = await inspectConductorLedgerNativeModuleLoad();
|
|
967
|
+
if (native_load.degraded && native_load.warning) {
|
|
968
|
+
warnings.push(native_load.warning);
|
|
969
|
+
}
|
|
828
970
|
let schemaPresent = false;
|
|
829
971
|
let journalMode = null;
|
|
830
972
|
let eventCount = null;
|
|
@@ -873,6 +1015,7 @@ export async function doctorConductorLedger(config = resolveConductorStoreConfig
|
|
|
873
1015
|
message_relay: {
|
|
874
1016
|
message_cooldown_ms: config.message_cooldown_ms,
|
|
875
1017
|
},
|
|
1018
|
+
native_load,
|
|
876
1019
|
degraded: network.network_mounted,
|
|
877
1020
|
warnings,
|
|
878
1021
|
};
|