@ouro.bot/cli 0.1.0-alpha.990 → 0.1.0-alpha.991
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/changelog.json +6 -0
- package/deploy/unraid/Dockerfile +15 -2
- package/deploy/unraid/README.txt +44 -9
- package/deploy/unraid/github_known_hosts +3 -0
- package/deploy/unraid/ouro-sync-ssh +98 -0
- package/deploy/unraid/ouro-sync.sudoers +3 -0
- package/deploy/unraid/sanctuary-butler-upgrade.mjs +26 -2
- package/deploy/unraid/sanctuary-replay-gate.mjs +361 -8
- package/deploy/unraid/sanctuary-sync-cutover.sh +1039 -0
- package/deploy/unraid/sanctuary.ouro/agent.json +2 -1
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.ouro/tool-profiles.json +3 -1
- package/deploy/unraid/sanctuary.xml +2 -1
- package/dist/heart/config.js +2 -1
- package/dist/heart/daemon/agent-discovery.js +5 -1
- package/dist/heart/daemon/agent-home.js +67 -31
- package/dist/heart/daemon/boot-sync-probe.js +36 -10
- package/dist/heart/daemon/cli-exec.js +79 -20
- package/dist/heart/daemon/cli-help.js +12 -0
- package/dist/heart/daemon/cli-parse.js +14 -0
- package/dist/heart/daemon/container-spec-auditor-main.js +22 -6
- package/dist/heart/daemon/container-spec-auditor.js +18 -2
- package/dist/heart/daemon/daemon-entry.js +22 -0
- package/dist/heart/daemon/sanctuary-acceptance-harness.js +1 -1
- package/dist/heart/daemon/sanctuary-bundle-migration.js +97 -1
- package/dist/heart/daemon/sanctuary-package-management.js +10 -0
- package/dist/heart/sync-lock.js +431 -0
- package/dist/heart/sync.js +1252 -399
- package/dist/mind/prompt.js +92 -19
- package/dist/repertoire/guardrails.js +285 -3
- package/dist/repertoire/tools-bundle.js +107 -185
- package/dist/repertoire/tools.js +5 -1
- package/dist/senses/pipeline.js +37 -3
- package/dist/senses/private-runtime.js +1 -6
- package/dist/senses/voice/twilio-phone.js +16 -16
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"version": 2,
|
|
3
3
|
"profiles": {
|
|
4
4
|
"sanctuary-owner": {
|
|
5
|
-
"version":
|
|
5
|
+
"version": 18,
|
|
6
6
|
"contextScopes": [
|
|
7
7
|
"household.status",
|
|
8
8
|
"household.policy",
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
"steward_policy_manage",
|
|
39
39
|
"house_sweep",
|
|
40
40
|
"house_digest_send",
|
|
41
|
+
"bundle_push",
|
|
42
|
+
"bundle_pull_rebase",
|
|
41
43
|
"send_message",
|
|
42
44
|
"ponder",
|
|
43
45
|
"rest",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<?xml version="1.0"?>
|
|
2
2
|
<Container version="2">
|
|
3
3
|
<Name>ouro-butler</Name>
|
|
4
|
-
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.
|
|
4
|
+
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.991</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
|
@@ -26,4 +26,5 @@
|
|
|
26
26
|
<Config Name="Privileged event spool" Target="/run/ouro-events" Default="/boot/config/custom/ouro-events/spool" Mode="ro" Description="Root-owned detector envelopes, read-only inside the Butler" Type="Path" Display="always" Required="true" Mask="false">/boot/config/custom/ouro-events/spool</Config>
|
|
27
27
|
<Config Name="Root authority socket" Target="/run/ouro-authority" Default="/run/ouro-authority" Mode="ro" Description="Root-owned gateway socket and public issuer pins only" Type="Path" Display="always" Required="true" Mask="false">/run/ouro-authority</Config>
|
|
28
28
|
<Config Name="Operator trust (read-only)" Target="/etc/ouro/trust/sanctuary" Default="/mnt/user/appdata/ouro-butler/trust/sanctuary" Mode="ro" Description="Root-owned grants the Butler honours (delegated commands, escalation). Read-only inside the Butler; only the host operator can change it" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/ouro-butler/trust/sanctuary</Config>
|
|
29
|
+
<Config Name="Bundle sync key (read-only)" Target="/run/ouro-sync" Default="/mnt/user/appdata/ouro-butler/sync" Mode="ro" Description="Deploy key the Butler's bundle sync pushes with, owned by 10002:10002 mode 0400 so only the sync wrapper reads it. Read-only inside the Butler" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/ouro-butler/sync</Config>
|
|
29
30
|
</Container>
|
package/dist/heart/config.js
CHANGED
|
@@ -342,11 +342,12 @@ function getSyncConfig() {
|
|
|
342
342
|
return {
|
|
343
343
|
enabled: agentConfig.sync?.enabled ?? false,
|
|
344
344
|
remote: agentConfig.sync?.remote ?? "origin",
|
|
345
|
+
branch: agentConfig.sync?.branch ?? "main",
|
|
345
346
|
};
|
|
346
347
|
}
|
|
347
348
|
catch {
|
|
348
349
|
/* v8 ignore next -- defensive: loadAgentConfig failure in test/bootstrap @preserve */
|
|
349
|
-
return { enabled: false, remote: "origin" };
|
|
350
|
+
return { enabled: false, remote: "origin", branch: "main" };
|
|
350
351
|
}
|
|
351
352
|
}
|
|
352
353
|
function getOpenAIEmbeddingsApiKey() {
|
|
@@ -315,6 +315,7 @@ function listBundleSyncRows(options = {}) {
|
|
|
315
315
|
const configPath = path.join(bundleRoot, "agent.json");
|
|
316
316
|
let enabled = false;
|
|
317
317
|
let remote = "origin";
|
|
318
|
+
let branch = "main";
|
|
318
319
|
try {
|
|
319
320
|
const raw = readFileSync(configPath, "utf-8");
|
|
320
321
|
const parsed = JSON.parse(raw);
|
|
@@ -324,12 +325,15 @@ function listBundleSyncRows(options = {}) {
|
|
|
324
325
|
if (typeof parsed.sync.remote === "string" && parsed.sync.remote.length > 0) {
|
|
325
326
|
remote = parsed.sync.remote;
|
|
326
327
|
}
|
|
328
|
+
if (typeof parsed.sync.branch === "string" && parsed.sync.branch.length > 0) {
|
|
329
|
+
branch = parsed.sync.branch;
|
|
330
|
+
}
|
|
327
331
|
}
|
|
328
332
|
}
|
|
329
333
|
catch {
|
|
330
334
|
// Best-effort: bundle without readable config still gets a row with defaults
|
|
331
335
|
}
|
|
332
|
-
const row = { agent, enabled, remote };
|
|
336
|
+
const row = { agent, enabled, remote, branch };
|
|
333
337
|
if (enabled) {
|
|
334
338
|
// Only meaningful when sync is enabled — we don't care about disabled bundles'
|
|
335
339
|
// git state. Check .git presence before attempting any git invocation.
|
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
* module writes it (`ouro move <agent> here`) and checks a running daemon
|
|
9
9
|
* against it.
|
|
10
10
|
*
|
|
11
|
-
* A move is a compare-and-swap on the bundle's remote. It
|
|
12
|
-
* bundle
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* A move is a compare-and-swap on the bundle's remote. It runs under the
|
|
12
|
+
* bundle sync lock, honours a sync pause, and names `<remote>` and
|
|
13
|
+
* `sync.branch` explicitly (never `@{u}`). It starts only from a bundle that
|
|
14
|
+
* is exactly at `<remote>/<branch>` (nothing unpushed, agent.json clean),
|
|
15
|
+
* commits only agent.json and pushes `HEAD:<branch>`. A fast-forward push
|
|
16
|
+
* succeeds only if nobody pushed in between. When the push is refused, the
|
|
17
|
+
* move is undone; if the remote's lease changed meanwhile, another machine won
|
|
18
|
+
* and the move stops, otherwise it is retried once on the new remote state.
|
|
17
19
|
*/
|
|
18
20
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
19
21
|
if (k2 === undefined) k2 = k;
|
|
@@ -57,13 +59,20 @@ const fs = __importStar(require("fs"));
|
|
|
57
59
|
const path = __importStar(require("path"));
|
|
58
60
|
const child_process_1 = require("child_process");
|
|
59
61
|
const runtime_1 = require("../../nerves/runtime");
|
|
62
|
+
const sync_1 = require("../sync");
|
|
60
63
|
const agent_discovery_1 = require("./agent-discovery");
|
|
64
|
+
const SYNC_NETWORK = {
|
|
65
|
+
fetch: (agentRoot, config) => (0, sync_1.fetchRemote)(agentRoot, config),
|
|
66
|
+
push: (agentRoot, config) => (0, sync_1.pushHeadToBranch)(agentRoot, config),
|
|
67
|
+
};
|
|
61
68
|
const MOVE_ATTEMPTS = 2;
|
|
69
|
+
const MOVE_LOCK_WAIT_MS = 10_000;
|
|
62
70
|
function readSyncSettings(parsed) {
|
|
63
71
|
const sync = parsed.sync;
|
|
64
72
|
return {
|
|
65
73
|
enabled: sync?.enabled === true,
|
|
66
74
|
remote: typeof sync?.remote === "string" && sync.remote.trim() ? sync.remote : "origin",
|
|
75
|
+
branch: typeof sync?.branch === "string" && sync.branch.trim() ? sync.branch : "main",
|
|
67
76
|
};
|
|
68
77
|
}
|
|
69
78
|
function errorText(error) {
|
|
@@ -87,11 +96,12 @@ function recordedPushError(agentRoot) {
|
|
|
87
96
|
}
|
|
88
97
|
}
|
|
89
98
|
/** Record this machine as the agent's home in its synced agent.json. */
|
|
90
|
-
function moveAgentHomeHere(options) {
|
|
99
|
+
async function moveAgentHomeHere(options) {
|
|
91
100
|
const { agent, machineId, machineName } = options;
|
|
92
101
|
const agentRoot = path.join(options.bundlesRoot, `${agent}.ouro`);
|
|
93
102
|
const configPath = path.join(agentRoot, "agent.json");
|
|
94
103
|
const git = options.execFileSync ?? child_process_1.execFileSync;
|
|
104
|
+
const network = options.network ?? SYNC_NETWORK;
|
|
95
105
|
const now = options.now ?? (() => new Date());
|
|
96
106
|
const run = (args) => git("git", args, { cwd: agentRoot, stdio: "pipe", timeout: 30_000 }).toString().trim();
|
|
97
107
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -140,35 +150,47 @@ function moveAgentHomeHere(options) {
|
|
|
140
150
|
const why = sync.enabled ? "the bundle is not a git repo" : "bundle sync is off";
|
|
141
151
|
return finish({ ok: true, changed: true, message: `${movedMessage(current)}; ${why}, so only this machine sees the change` });
|
|
142
152
|
}
|
|
153
|
+
const pausedRefusal = (until) => refuse(`${agent}'s bundle sync is paused until ${until}, so nothing changed; run \`ouro move ${agent} here\` again after that`);
|
|
154
|
+
const pausedUntil = (0, sync_1.syncPausedUntil)(agentRoot);
|
|
155
|
+
if (pausedUntil !== null)
|
|
156
|
+
return pausedRefusal(pausedUntil);
|
|
157
|
+
const lock = await (0, sync_1.holdSyncLock)(agentRoot, options.lockWaitMs ?? MOVE_LOCK_WAIT_MS);
|
|
158
|
+
if ("busy" in lock) {
|
|
159
|
+
return refuse(`another bundle sync holds ${agent}'s lock${lock.holderPid === null ? "" : ` (pid ${lock.holderPid})`}, so nothing changed; try again in a moment`);
|
|
160
|
+
}
|
|
143
161
|
try {
|
|
162
|
+
// A pause can begin while this move waited for the lock (the replay gate
|
|
163
|
+
// pauses, then takes the lock), so look again now that the lock is ours.
|
|
164
|
+
const pausedNow = (0, sync_1.syncPausedUntil)(agentRoot);
|
|
165
|
+
if (pausedNow !== null)
|
|
166
|
+
return pausedRefusal(pausedNow);
|
|
144
167
|
if (run(["status", "--porcelain", "--", "agent.json"]).length > 0) {
|
|
145
168
|
return refuse(`${agent}'s agent.json has uncommitted changes, so nothing changed; commit or discard them first (git -C ${agentRoot} diff agent.json)`);
|
|
146
169
|
}
|
|
147
170
|
const hasRemote = run(["remote"]).split("\n").includes(sync.remote);
|
|
171
|
+
const tracking = (0, sync_1.syncTrackingRef)(sync);
|
|
148
172
|
let seen = null;
|
|
149
173
|
let lastPushError = "";
|
|
150
174
|
for (let attempt = 1; attempt <= MOVE_ATTEMPTS; attempt++) {
|
|
151
175
|
if (hasRemote) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
catch (error) {
|
|
156
|
-
return refuse(`could not reach ${agent}'s bundle remote, so nothing changed: ${errorText(error)}`);
|
|
176
|
+
const fetched = await network.fetch(agentRoot, sync);
|
|
177
|
+
if (!fetched.ok) {
|
|
178
|
+
return refuse(`could not reach ${agent}'s bundle remote, so nothing changed: ${fetched.error}`);
|
|
157
179
|
}
|
|
158
|
-
let unpushed;
|
|
159
180
|
try {
|
|
160
|
-
|
|
181
|
+
run(["rev-parse", "--verify", "-q", tracking]);
|
|
161
182
|
}
|
|
162
|
-
catch
|
|
163
|
-
return refuse(`${agent}'s bundle
|
|
183
|
+
catch {
|
|
184
|
+
return refuse(`${agent}'s bundle remote has no ${sync.branch} branch to publish the move to, so nothing changed`);
|
|
164
185
|
}
|
|
186
|
+
const unpushed = Number(run(["rev-list", "--count", `${tracking}..HEAD`]));
|
|
165
187
|
if (unpushed > 0) {
|
|
166
188
|
const recorded = recordedPushError(agentRoot);
|
|
167
|
-
const why = recorded ? `the last bundle push failed with: ${recorded}` : `run \`
|
|
189
|
+
const why = recorded ? `the last bundle push failed with: ${recorded}` : `run \`ouro sync --agent ${agent}\` to see why they did not publish`;
|
|
168
190
|
return refuse(`${agent}'s bundle has ${unpushed} unpushed commit${unpushed === 1 ? "" : "s"}, so nothing changed; fix bundle sync first, because a move must not publish them as a side effect (${why})`);
|
|
169
191
|
}
|
|
170
192
|
try {
|
|
171
|
-
run(["merge", "--ff-only", "-q",
|
|
193
|
+
run(["merge", "--ff-only", "-q", tracking]);
|
|
172
194
|
}
|
|
173
195
|
catch (error) {
|
|
174
196
|
return refuse(`could not bring ${agent}'s bundle up to date with its remote, so nothing changed: ${errorText(error)}`);
|
|
@@ -188,7 +210,7 @@ function moveAgentHomeHere(options) {
|
|
|
188
210
|
}
|
|
189
211
|
writeHome(config.parsed, current);
|
|
190
212
|
try {
|
|
191
|
-
run(["commit", "-q", "-m", `home: ${agent} moves to ${machineName}`, "--", "agent.json"]);
|
|
213
|
+
run(["-c", `user.name=${agent}`, "-c", `user.email=${agent}@ouro.local`, "commit", "-q", "-m", `home: ${agent} moves to ${machineName}`, "--", "agent.json"]);
|
|
192
214
|
}
|
|
193
215
|
catch (error) {
|
|
194
216
|
fs.writeFileSync(configPath, config.text, "utf-8");
|
|
@@ -197,13 +219,10 @@ function moveAgentHomeHere(options) {
|
|
|
197
219
|
if (!hasRemote) {
|
|
198
220
|
return finish({ ok: true, changed: true, message: `${movedMessage(current)}; the bundle has no remote, so only this machine sees the change` });
|
|
199
221
|
}
|
|
200
|
-
|
|
201
|
-
|
|
222
|
+
const pushed = await network.push(agentRoot, sync);
|
|
223
|
+
if (pushed.ok)
|
|
202
224
|
return finish({ ok: true, changed: true, message: movedMessage(current) });
|
|
203
|
-
|
|
204
|
-
catch (error) {
|
|
205
|
-
lastPushError = errorText(error);
|
|
206
|
-
}
|
|
225
|
+
lastPushError = pushed.error ?? "push failed";
|
|
207
226
|
// Undo exactly our own commit; the working tree goes back to the
|
|
208
227
|
// pre-move agent.json. If even that fails, say what to run by hand.
|
|
209
228
|
try {
|
|
@@ -220,6 +239,9 @@ function moveAgentHomeHere(options) {
|
|
|
220
239
|
catch (error) {
|
|
221
240
|
return refuse(`could not move ${agent}, so nothing changed: ${errorText(error)}`);
|
|
222
241
|
}
|
|
242
|
+
finally {
|
|
243
|
+
lock.release();
|
|
244
|
+
}
|
|
223
245
|
}
|
|
224
246
|
function execGit(cwd, args) {
|
|
225
247
|
return new Promise((resolve, reject) => {
|
|
@@ -232,18 +254,32 @@ function execGit(cwd, args) {
|
|
|
232
254
|
});
|
|
233
255
|
}
|
|
234
256
|
/**
|
|
235
|
-
* Fetch
|
|
236
|
-
* lease in the
|
|
237
|
-
* even while the agent is idle and never pulls.
|
|
257
|
+
* Fetch `<remote>/<sync.branch>` (without touching the working tree) and read
|
|
258
|
+
* the lease in the remote's agent.json, so a machine notices a move away from
|
|
259
|
+
* it even while the agent is idle and never pulls. Fetch only, under the sync
|
|
260
|
+
* lock without waiting: a busy lock or a sync pause skips this check (unknown).
|
|
261
|
+
* It never commits or pushes.
|
|
238
262
|
*/
|
|
239
263
|
async function readUpstreamAgentHome(agentRoot, git = execGit) {
|
|
240
264
|
try {
|
|
241
265
|
const sync = readSyncSettings(JSON.parse(fs.readFileSync(path.join(agentRoot, "agent.json"), "utf-8")));
|
|
242
266
|
if (!sync.enabled || !fs.existsSync(path.join(agentRoot, ".git")))
|
|
243
267
|
return undefined;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
268
|
+
if ((0, sync_1.syncPausedUntil)(agentRoot) !== null)
|
|
269
|
+
return undefined;
|
|
270
|
+
const lock = await (0, sync_1.holdSyncLock)(agentRoot, 0);
|
|
271
|
+
if ("busy" in lock)
|
|
272
|
+
return undefined;
|
|
273
|
+
try {
|
|
274
|
+
const fetched = await (0, sync_1.fetchRemote)(agentRoot, sync);
|
|
275
|
+
if (!fetched.ok)
|
|
276
|
+
return undefined;
|
|
277
|
+
const upstream = JSON.parse(await git(agentRoot, ["show", `${(0, sync_1.syncTrackingRef)(sync)}:agent.json`]));
|
|
278
|
+
return (0, agent_discovery_1.parseAgentHome)(upstream.home);
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
lock.release();
|
|
282
|
+
}
|
|
247
283
|
}
|
|
248
284
|
catch {
|
|
249
285
|
return undefined;
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Boot sync probe — Layer 2 orchestrator for `ouro up`.
|
|
4
4
|
*
|
|
5
|
-
* For each sync-enabled bundle, runs `
|
|
6
|
-
* `runWithTimeouts`, classifies failures
|
|
7
|
-
* returns aggregated findings
|
|
8
|
-
*
|
|
5
|
+
* For each sync-enabled bundle, runs `probeSync` (fetch only, then count
|
|
6
|
+
* commits ahead and behind) wrapped in `runWithTimeouts`, classifies failures
|
|
7
|
+
* via `classifySyncFailure`, and returns aggregated findings plus one status
|
|
8
|
+
* per probed bundle. Both surface in the boot stdout summary written by
|
|
9
|
+
* `cli-exec.ts` after the probe phase.
|
|
9
10
|
*
|
|
10
11
|
* Layer 2 is intentionally a **boot-time preflight surface only** — it
|
|
11
12
|
* does NOT feed into the running daemon's rollup state. `computeDaemonRollup`
|
|
@@ -15,6 +16,8 @@
|
|
|
15
16
|
* dispatches the right diagnostic skill).
|
|
16
17
|
*
|
|
17
18
|
* The probe NEVER:
|
|
19
|
+
* - Merges, commits or pushes: it fetches only.
|
|
20
|
+
* - Waits for the bundle sync lock: a held lock is reported as busy.
|
|
18
21
|
* - Writes to `state/` (verified by Unit 7's grep gate).
|
|
19
22
|
* - Throws — every failure becomes a finding.
|
|
20
23
|
* - Hangs — `AbortSignal` from `runWithTimeouts` cuts hung remotes
|
|
@@ -76,6 +79,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
76
79
|
exports.runBootSyncProbe = runBootSyncProbe;
|
|
77
80
|
const path = __importStar(require("path"));
|
|
78
81
|
const sync_1 = require("../sync");
|
|
82
|
+
const sync_lock_1 = require("../sync-lock");
|
|
79
83
|
const sync_classification_1 = require("../sync-classification");
|
|
80
84
|
const timeouts_1 = require("../timeouts");
|
|
81
85
|
const runtime_1 = require("../../nerves/runtime");
|
|
@@ -110,6 +114,7 @@ async function runBootSyncProbe(bundles, options) {
|
|
|
110
114
|
meta: { bundleCount: bundles.length, softMs, hardMs },
|
|
111
115
|
});
|
|
112
116
|
const findings = [];
|
|
117
|
+
const statuses = [];
|
|
113
118
|
for (const bundle of bundles) {
|
|
114
119
|
if (!bundle.enabled)
|
|
115
120
|
continue;
|
|
@@ -120,17 +125,24 @@ async function runBootSyncProbe(bundles, options) {
|
|
|
120
125
|
findings.push({
|
|
121
126
|
agent: bundle.agent,
|
|
122
127
|
classification: "unknown",
|
|
123
|
-
error:
|
|
128
|
+
error: sync_1.NOT_A_REPO_ERROR,
|
|
124
129
|
conflictFiles: [],
|
|
125
130
|
warnings: [],
|
|
126
131
|
advisory: true,
|
|
127
132
|
});
|
|
128
133
|
continue;
|
|
129
134
|
}
|
|
130
|
-
const syncConfig = { enabled: bundle.enabled, remote: bundle.remote };
|
|
131
|
-
|
|
135
|
+
const syncConfig = { enabled: bundle.enabled, remote: bundle.remote, branch: bundle.branch };
|
|
136
|
+
// A lock this pid left in an earlier life (PID 1 in the container) would read as busy forever.
|
|
137
|
+
try {
|
|
138
|
+
(0, sync_lock_1.clearOwnStaleSyncLock)(agentRoot);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
// An unreadable lock is reported by the probe's own attempt below.
|
|
142
|
+
}
|
|
143
|
+
const outcome = await (0, timeouts_1.runWithTimeouts)((signal) => (0, sync_1.probeSync)(agentRoot, syncConfig, signal), { softMs, hardMs, label: `boot-sync-probe ${bundle.agent}`, envKey: "GIT" });
|
|
132
144
|
// Hard timeout — the probe was aborted. Synthesise a finding from the
|
|
133
|
-
// outcome's classification (the underlying `
|
|
145
|
+
// outcome's classification (the underlying `probeSync` may also
|
|
134
146
|
// have rejected with an AbortError, but the wrapper already swallowed
|
|
135
147
|
// it).
|
|
136
148
|
if (outcome.classification === "timeout-hard") {
|
|
@@ -170,6 +182,9 @@ async function runBootSyncProbe(bundles, options) {
|
|
|
170
182
|
});
|
|
171
183
|
continue;
|
|
172
184
|
}
|
|
185
|
+
const status = describeProbeResult(result, syncConfig);
|
|
186
|
+
if (status)
|
|
187
|
+
statuses.push({ agent: bundle.agent, text: status });
|
|
173
188
|
// Probe succeeded. If the soft warning fired, surface it as an advisory
|
|
174
189
|
// finding so the operator sees the slow-pull warning even on success.
|
|
175
190
|
if (outcome.warnings.length > 0) {
|
|
@@ -186,12 +201,23 @@ async function runBootSyncProbe(bundles, options) {
|
|
|
186
201
|
// Stable order — sort by agent name so renderers and tests see the same
|
|
187
202
|
// sequence.
|
|
188
203
|
findings.sort((a, b) => a.agent.localeCompare(b.agent));
|
|
204
|
+
statuses.sort((a, b) => a.agent.localeCompare(b.agent));
|
|
189
205
|
const durationMs = Date.now() - startedAt;
|
|
190
206
|
(0, runtime_1.emitNervesEvent)({
|
|
191
207
|
component: "daemon",
|
|
192
208
|
event: "daemon.boot_sync_probe_end",
|
|
193
209
|
message: "boot sync probe complete",
|
|
194
|
-
meta: { findingCount: findings.length, durationMs },
|
|
210
|
+
meta: { findingCount: findings.length, statusCount: statuses.length, durationMs },
|
|
195
211
|
});
|
|
196
|
-
return { findings, durationMs };
|
|
212
|
+
return { findings, statuses, durationMs };
|
|
213
|
+
}
|
|
214
|
+
/** The probe's view of one healthy bundle, or null when there is nothing to say (no remote). */
|
|
215
|
+
function describeProbeResult(result, config) {
|
|
216
|
+
if (result.paused)
|
|
217
|
+
return `sync paused until ${result.pausedUntil}`;
|
|
218
|
+
if (result.busy)
|
|
219
|
+
return `sync busy: another sync holds the lock${result.holderPid == null ? "" : ` (pid ${result.holderPid})`}`;
|
|
220
|
+
if (result.ahead === undefined || result.behind === undefined)
|
|
221
|
+
return null;
|
|
222
|
+
return `${result.ahead} ahead, ${result.behind} behind ${config.remote}/${config.branch}`;
|
|
197
223
|
}
|