@brutalsystems/muster 3.3.1 → 3.3.3
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/help.js +1 -1
- package/dist/muster.js +2 -1
- package/dist/reap.js +3 -1
- package/dist/registry.js +46 -13
- package/dist/run.js +17 -3
- package/package.json +1 -1
package/dist/help.js
CHANGED
|
@@ -138,7 +138,7 @@ export const COMMANDS = [
|
|
|
138
138
|
["--format FORMAT", "json (default) or human"],
|
|
139
139
|
],
|
|
140
140
|
notes: [
|
|
141
|
-
"Prints the final record, with state, exit_code and signal (null for a session, whose terminal keeps no exit status). Exits 0 when it has ended, 2 for an id that names nothing,
|
|
141
|
+
"Prints the final record, with state, exit_code and signal (null for a session, whose terminal keeps no exit status). Exits 0 when it has ended, 2 for an id that names nothing, 124 on timeout, printing the record as it stands, and 75 when the registry is locked (as every command does): retry.",
|
|
142
142
|
],
|
|
143
143
|
examples: [
|
|
144
144
|
"muster wait 6fae755b-8f5d-4425-8ed7-69e84bfac8c5",
|
package/dist/muster.js
CHANGED
|
@@ -6,6 +6,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
6
6
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { Muster, UnknownRun, durableId, failedLaunchId } from "./run.js";
|
|
9
|
+
import { RegistryLocked } from "./registry.js";
|
|
9
10
|
import { waitSeconds } from "./duration.js";
|
|
10
11
|
import { delay } from "./identity/processes.js";
|
|
11
12
|
import { formatHuman, humanColorEnabled, statusText } from "./format.js";
|
|
@@ -486,5 +487,5 @@ main().catch((e) => {
|
|
|
486
487
|
? `\n[muster] see \`muster ${command} --help\` for its options`
|
|
487
488
|
: "";
|
|
488
489
|
process.stderr.write(`[muster] ${e.message}${hint}\n`);
|
|
489
|
-
process.exitCode = 1;
|
|
490
|
+
process.exitCode = e instanceof RegistryLocked ? RegistryLocked.exitCode : 1;
|
|
490
491
|
});
|
package/dist/reap.js
CHANGED
|
@@ -125,8 +125,10 @@ async function stopSession(home, socket, launchId) {
|
|
|
125
125
|
});
|
|
126
126
|
try {
|
|
127
127
|
const entry = (await new Registry(home).all()).find((e) => e.launchId === launchId);
|
|
128
|
+
// By launch id: since --resume, the durable id may name a newer launch of
|
|
129
|
+
// the same session, which this check was not armed for.
|
|
128
130
|
if (entry)
|
|
129
|
-
await muster.stop(entry.
|
|
131
|
+
await muster.stop(entry.launchId);
|
|
130
132
|
}
|
|
131
133
|
finally {
|
|
132
134
|
await muster.close();
|
package/dist/registry.js
CHANGED
|
@@ -38,13 +38,41 @@ export class IdempotencyKeyReuse extends Error {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
|
-
* A lock directory with no owner file is presumed live for this long.
|
|
42
|
-
*
|
|
43
|
-
* file immediately after taking
|
|
44
|
-
* version that wrote none (
|
|
41
|
+
* A lock directory with no owner file is presumed live for this long. A
|
|
42
|
+
* transaction holds the lock for milliseconds and the holder writes its owner
|
|
43
|
+
* file immediately after taking it, so a lock still unowned this long was left
|
|
44
|
+
* by a holder killed in between, or by a version that wrote none (#64). Three
|
|
45
|
+
* default lock waits: one waiter may still time out on it, the next breaks it.
|
|
45
46
|
*/
|
|
46
|
-
const UNOWNED_LOCK_MS =
|
|
47
|
+
const UNOWNED_LOCK_MS = 15_000;
|
|
48
|
+
/**
|
|
49
|
+
* The registry is busy, not broken and not empty: the CLI exits 75
|
|
50
|
+
* (EX_TEMPFAIL, "try again later") for it, so a script polling `list` or
|
|
51
|
+
* `wait` can tell it from a run that is gone (#64) and retry.
|
|
52
|
+
*/
|
|
53
|
+
export class RegistryLocked extends Error {
|
|
54
|
+
static exitCode = 75;
|
|
55
|
+
constructor(message) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = "RegistryLocked";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
47
60
|
let self;
|
|
61
|
+
/** Statuses whose process is over: such an entry no longer holds its id. */
|
|
62
|
+
const FINISHED = ["exited", "failed", "stopped"];
|
|
63
|
+
/**
|
|
64
|
+
* The entry a durable id names. One id can have several since `--resume`: the
|
|
65
|
+
* launches that ran a session before, now finished, and the one running it
|
|
66
|
+
* now. The live one is meant, else the newest; two live ones cannot arise,
|
|
67
|
+
* because `update` refuses a second live holder of an id.
|
|
68
|
+
*/
|
|
69
|
+
export function currentEntry(entries) {
|
|
70
|
+
const live = entries.filter((e) => !FINISHED.includes(e.status));
|
|
71
|
+
if (live.length > 1)
|
|
72
|
+
throw new Error("Ambiguous durable id; update by launchId");
|
|
73
|
+
return (live[0] ??
|
|
74
|
+
[...entries].sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0]);
|
|
75
|
+
}
|
|
48
76
|
export class Registry {
|
|
49
77
|
home;
|
|
50
78
|
lockWait;
|
|
@@ -69,9 +97,14 @@ export class Registry {
|
|
|
69
97
|
continue;
|
|
70
98
|
if (Date.now() > deadline) {
|
|
71
99
|
const owner = await lockOwner(lock);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
100
|
+
const age = await ageMs(lock);
|
|
101
|
+
const held = [
|
|
102
|
+
owner ? `held by pid ${owner.pid}` : "with no owner recorded",
|
|
103
|
+
age === undefined ? undefined : `for ${Math.round(age / 1000)}s`,
|
|
104
|
+
]
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
.join(" ");
|
|
107
|
+
throw new RegistryLocked(`Registry locked, ${held}; retry shortly. If it persists, verify no Muster operation is running before removing ${lock}`);
|
|
75
108
|
}
|
|
76
109
|
await delay(20);
|
|
77
110
|
}
|
|
@@ -300,16 +333,16 @@ export class Registry {
|
|
|
300
333
|
async update(id, patch) {
|
|
301
334
|
return this.transaction(async (entries) => {
|
|
302
335
|
const byLaunch = entries.find((e) => e.launchId === id);
|
|
303
|
-
const
|
|
304
|
-
if (!byLaunch && durable.length > 1)
|
|
305
|
-
throw new Error("Ambiguous durable id; update by launchId");
|
|
306
|
-
const entry = byLaunch ?? durable[0];
|
|
336
|
+
const entry = byLaunch ?? currentEntry(entries.filter((e) => e.id === id));
|
|
307
337
|
if (!entry)
|
|
308
338
|
throw new Error(`Unknown launch ${id}`);
|
|
309
339
|
if ("requester" in patch)
|
|
310
340
|
throw new Error("requester is recorded once and cannot be updated");
|
|
311
341
|
if (patch.id &&
|
|
312
|
-
entries.some((e) => e !== entry &&
|
|
342
|
+
entries.some((e) => e !== entry &&
|
|
343
|
+
e.runtime === entry.runtime &&
|
|
344
|
+
e.id === patch.id &&
|
|
345
|
+
!FINISHED.includes(e.status)))
|
|
313
346
|
throw new Error("Durable identity already registered");
|
|
314
347
|
Object.assign(entry, patch);
|
|
315
348
|
return structuredClone(entry);
|
package/dist/run.js
CHANGED
|
@@ -11,7 +11,7 @@ import { assertEnforcementMeetsMinimum, assertLevelWithinCeiling, assertMayAct,
|
|
|
11
11
|
import { IdentityError, readIdentity, } from "./identity-store.js";
|
|
12
12
|
import { resolveCredential } from "./identity-token.js";
|
|
13
13
|
import { copyIdentity, identityEnv } from "./identity-copy.js";
|
|
14
|
-
import { Registry } from "./registry.js";
|
|
14
|
+
import { currentEntry, Registry, RegistryLocked, } from "./registry.js";
|
|
15
15
|
import { LaunchLog } from "./log.js";
|
|
16
16
|
import { hosts, selectHost } from "./hosts/index.js";
|
|
17
17
|
import { AGENTS } from "./agents.js";
|
|
@@ -1185,11 +1185,21 @@ export class Muster {
|
|
|
1185
1185
|
}
|
|
1186
1186
|
async find(id) {
|
|
1187
1187
|
const all = await this.registry.all();
|
|
1188
|
+
// A launch id names exactly one launch, so a caller that holds one (the
|
|
1189
|
+
// reaper) can never reach a later launch of the same resumed session.
|
|
1190
|
+
const launch = all.find((e) => e.launchId === id);
|
|
1191
|
+
if (launch)
|
|
1192
|
+
return launch;
|
|
1188
1193
|
const exact = all.filter((e) => e.id === id);
|
|
1189
1194
|
if (exact.length === 1)
|
|
1190
1195
|
return exact[0];
|
|
1191
|
-
|
|
1192
|
-
|
|
1196
|
+
// The same session under two runtimes is ambiguous. Under one, it was
|
|
1197
|
+
// resumed: the launch running it now, else the latest, is the one meant.
|
|
1198
|
+
if (exact.length > 1) {
|
|
1199
|
+
if (new Set(exact.map((e) => e.runtime)).size > 1)
|
|
1200
|
+
throw new Error(`Ambiguous durable id: ${exact.map((e) => e.runtime + ":" + e.id).join(", ")}`);
|
|
1201
|
+
return currentEntry(exact);
|
|
1202
|
+
}
|
|
1193
1203
|
const peers = all.filter((e) => e.kind === "session" && e.status === "running" && e.peer);
|
|
1194
1204
|
const named = peers.map((e) => ({
|
|
1195
1205
|
runtime: runtimeName(e.runtime),
|
|
@@ -1460,6 +1470,10 @@ export class Muster {
|
|
|
1460
1470
|
return await this.find(id);
|
|
1461
1471
|
}
|
|
1462
1472
|
catch (e) {
|
|
1473
|
+
// Busy is not gone: `wait` exits 2 for an UnknownRun, and a poller that
|
|
1474
|
+
// read a lock as "no such run" is how #64 ended a wait loop early.
|
|
1475
|
+
if (e instanceof RegistryLocked)
|
|
1476
|
+
throw e;
|
|
1463
1477
|
const message = e.message;
|
|
1464
1478
|
// The resolver's "unknown: <every live name>" reads as a list of
|
|
1465
1479
|
// matches; the ambiguous case does want its candidates, so it keeps them.
|