@brutalsystems/muster 3.3.3 → 3.3.4
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/hosts/tmux.js +7 -2
- package/dist/muster.js +30 -19
- package/dist/reap.js +30 -4
- package/dist/run.js +2 -2
- package/package.json +1 -1
package/dist/hosts/tmux.js
CHANGED
|
@@ -210,14 +210,19 @@ export class TmuxHost {
|
|
|
210
210
|
* Run `argv` in the background after `seconds`, scheduled by the tmux server
|
|
211
211
|
* itself. This is what lets a lifecycle outlive every muster process: nothing
|
|
212
212
|
* of ours is running between the call and the job.
|
|
213
|
+
*
|
|
214
|
+
* The job's output goes to `log`, and it always exits 0. tmux shows a
|
|
215
|
+
* `run-shell` job's non-zero exit as "'<cmd>' returned 1" in whichever pane
|
|
216
|
+
* is active on the server, which is someone else's session, and it never
|
|
217
|
+
* shows the job's stderr, so the error itself was lost (#65).
|
|
213
218
|
*/
|
|
214
|
-
async schedule(seconds, argv) {
|
|
219
|
+
async schedule(seconds, argv, log) {
|
|
215
220
|
await this.call([
|
|
216
221
|
"run-shell",
|
|
217
222
|
"-b",
|
|
218
223
|
"-d",
|
|
219
224
|
String(Math.max(1, Math.round(seconds))),
|
|
220
|
-
tmuxCommand(argv)
|
|
225
|
+
`${tmuxCommand(argv)} >>${tmuxCommand([log])} 2>&1 || true`,
|
|
221
226
|
]);
|
|
222
227
|
}
|
|
223
228
|
async launch(opts) {
|
package/dist/muster.js
CHANGED
|
@@ -112,6 +112,30 @@ async function main() {
|
|
|
112
112
|
const allowLaunch = argv[0] === "mcp" && argv[1] === ALLOW_LAUNCH;
|
|
113
113
|
if (argv[0] === "mcp" && argv.length > (allowLaunch ? 2 : 1))
|
|
114
114
|
throw new Error(`mcp takes ${ALLOW_LAUNCH}, or one of ${MCP_SUBCOMMANDS.join(", ")}`);
|
|
115
|
+
// Before Muster.create(), which reads the real home's config: a broken
|
|
116
|
+
// config.toml failed every poll outside `reapCheck`, whose own catch is what
|
|
117
|
+
// re-arms, so the session's idle timeout was silently dropped (#65).
|
|
118
|
+
if (argv[0] === "reap-check") {
|
|
119
|
+
const { values, positionals } = parseArgs({
|
|
120
|
+
args: argv.slice(1),
|
|
121
|
+
allowPositionals: true,
|
|
122
|
+
strict: true,
|
|
123
|
+
options: CLI_OPTIONS,
|
|
124
|
+
});
|
|
125
|
+
// Hidden: armed by muster itself as a tmux job, never typed by a person.
|
|
126
|
+
if (positionals.length !== 1 || !values.socket)
|
|
127
|
+
throw new Error("reap-check requires --socket and one launch id");
|
|
128
|
+
const { reapCheck } = await import("./reap.js");
|
|
129
|
+
await reapCheck({
|
|
130
|
+
home: values.home ?? musterHome(),
|
|
131
|
+
socket: values.socket,
|
|
132
|
+
...(values["stop-now"] ? { stopNow: true } : {}),
|
|
133
|
+
launchId: positionals[0],
|
|
134
|
+
// The exact command that was armed, so a re-arm reproduces itself.
|
|
135
|
+
argv: [...process.argv],
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
115
139
|
const muster = await Muster.create();
|
|
116
140
|
let shutdown;
|
|
117
141
|
const close = () => (shutdown ??= (async () => {
|
|
@@ -453,24 +477,6 @@ async function main() {
|
|
|
453
477
|
await watcher.close();
|
|
454
478
|
}
|
|
455
479
|
}
|
|
456
|
-
else if (command === "reap-check") {
|
|
457
|
-
// Hidden: armed by muster itself as a tmux job, never typed by a person.
|
|
458
|
-
// NOTE: `Muster.create()` above already ran against the REAL home before
|
|
459
|
-
// this dispatch. Harmless (nothing is owned, so `close()` stops nothing)
|
|
460
|
-
// but it means a broken ~/.muster/config.toml would fail every poll.
|
|
461
|
-
// Left as-is: moving that create changes every command's startup.
|
|
462
|
-
if (positionals.length !== 1 || !socket)
|
|
463
|
-
throw new Error("reap-check requires --socket and one launch id");
|
|
464
|
-
const { reapCheck } = await import("./reap.js");
|
|
465
|
-
await reapCheck({
|
|
466
|
-
home: homeFlag ?? musterHome(),
|
|
467
|
-
socket,
|
|
468
|
-
...(stopNow ? { stopNow: true } : {}),
|
|
469
|
-
launchId: positionals[0],
|
|
470
|
-
// The exact command that was armed, so a re-arm reproduces itself.
|
|
471
|
-
argv: [...process.argv],
|
|
472
|
-
});
|
|
473
|
-
}
|
|
474
480
|
else
|
|
475
481
|
throw new Error(topLevelHelp());
|
|
476
482
|
}
|
|
@@ -486,6 +492,11 @@ main().catch((e) => {
|
|
|
486
492
|
commandHelp(command)
|
|
487
493
|
? `\n[muster] see \`muster ${command} --help\` for its options`
|
|
488
494
|
: "";
|
|
489
|
-
|
|
495
|
+
// reap-check's stderr is the reap log, read later by someone asking which
|
|
496
|
+
// poll failed and when.
|
|
497
|
+
const context = command === "reap-check"
|
|
498
|
+
? `${new Date().toISOString()} reap-check ${process.argv.slice(3).find((a) => /^[0-9a-f-]{36}$/i.test(a)) ?? "?"}: `
|
|
499
|
+
: "";
|
|
500
|
+
process.stderr.write(`[muster] ${context}${e.message}${hint}\n`);
|
|
490
501
|
process.exitCode = e instanceof RegistryLocked ? RegistryLocked.exitCode : 1;
|
|
491
502
|
});
|
package/dist/reap.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
1
2
|
import { Registry } from "./registry.js";
|
|
3
|
+
/**
|
|
4
|
+
* Where the reaper's tmux jobs write their output, under muster's home. Not
|
|
5
|
+
* launches.jsonl: that is one JSON event per line, and raw shell output would
|
|
6
|
+
* break every reader of it.
|
|
7
|
+
*/
|
|
8
|
+
export function reapLog(home) {
|
|
9
|
+
return join(home, "reap.log");
|
|
10
|
+
}
|
|
2
11
|
// The floor is low so a short limit is honoured rather than rounded up: a 45s
|
|
3
12
|
// ttl behind a 30s floor fires at up to 75s. A session with a limit that short
|
|
4
13
|
// does not live long enough for the extra polls to matter.
|
|
@@ -57,11 +66,11 @@ export function decide(entry, state) {
|
|
|
57
66
|
export async function reapCheck(opts) {
|
|
58
67
|
const rearm = async (seconds) => {
|
|
59
68
|
const schedule = opts.schedule ??
|
|
60
|
-
(async (s, argv) => {
|
|
69
|
+
(async (s, argv, log) => {
|
|
61
70
|
const { TmuxHost } = await import("./hosts/tmux.js");
|
|
62
|
-
await new TmuxHost(opts.socket).schedule(s, argv);
|
|
71
|
+
await new TmuxHost(opts.socket).schedule(s, argv, log);
|
|
63
72
|
});
|
|
64
|
-
await schedule(seconds, opts.argv);
|
|
73
|
+
await schedule(seconds, opts.argv, reapLog(opts.home));
|
|
65
74
|
return { action: "rearm", seconds };
|
|
66
75
|
};
|
|
67
76
|
try {
|
|
@@ -92,7 +101,24 @@ export async function reapCheck(opts) {
|
|
|
92
101
|
((e, args) => {
|
|
93
102
|
// Detached and unref'd so it survives the tmux server exiting as the
|
|
94
103
|
// session it is stopping goes away.
|
|
95
|
-
|
|
104
|
+
// Its output goes to the reap log too: with stdio ignored, a stop
|
|
105
|
+
// that failed left no trace anywhere.
|
|
106
|
+
void Promise.all([
|
|
107
|
+
import("node:child_process"),
|
|
108
|
+
import("node:fs"),
|
|
109
|
+
]).then(([{ spawn }, { openSync, closeSync }]) => {
|
|
110
|
+
let out = "ignore";
|
|
111
|
+
try {
|
|
112
|
+
out = openSync(reapLog(opts.home), "a", 0o600);
|
|
113
|
+
}
|
|
114
|
+
catch { }
|
|
115
|
+
spawn(e, args, {
|
|
116
|
+
detached: true,
|
|
117
|
+
stdio: ["ignore", out, out],
|
|
118
|
+
}).unref();
|
|
119
|
+
if (out !== "ignore")
|
|
120
|
+
closeSync(out);
|
|
121
|
+
});
|
|
96
122
|
});
|
|
97
123
|
spawnFn(exec, [...opts.argv.slice(1), "--stop-now"]);
|
|
98
124
|
}
|
package/dist/run.js
CHANGED
|
@@ -31,7 +31,7 @@ import { allocateLoopbackPort, OpenCodeHttp } from "./opencode-http.js";
|
|
|
31
31
|
import { assertOpenCodeVersion, openCodeMcpNames, openCodeSessionLaunch, openCodeTaskLaunch, } from "./opencode-policy.js";
|
|
32
32
|
import { assignNames, resolvePeer, runtimeName, suffixOf, } from "./naming.js";
|
|
33
33
|
import { selectMcp, prepareMcp, mcpEnvironment, } from "./mcp.js";
|
|
34
|
-
import { pollInterval } from "./reap.js";
|
|
34
|
+
import { pollInterval, reapLog } from "./reap.js";
|
|
35
35
|
import { selectPlugins } from "./plugins.js";
|
|
36
36
|
import { prepareOnExit, runOnExitCommand } from "./on-exit.js";
|
|
37
37
|
function taskHandle(entry, pid) {
|
|
@@ -714,7 +714,7 @@ export class Muster {
|
|
|
714
714
|
"--socket",
|
|
715
715
|
driver.socketName?.() ?? "muster",
|
|
716
716
|
entry.launchId,
|
|
717
|
-
])
|
|
717
|
+
], reapLog(this.home))
|
|
718
718
|
.then(() => true, () => false);
|
|
719
719
|
// A lifecycle that could not be armed must not fail the launch — the
|
|
720
720
|
// session is up and usable, and the orphan risk is what we had before.
|