@brutalsystems/muster 0.8.0 → 0.8.1
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/duration.js +4 -2
- package/dist/format.js +9 -7
- package/dist/guard.js +7 -4
- package/dist/hosts/tmux.js +19 -1
- package/dist/reap.js +4 -1
- package/dist/run.js +18 -6
- package/package.json +1 -1
package/dist/duration.js
CHANGED
|
@@ -11,8 +11,10 @@ export function parseDuration(value, label) {
|
|
|
11
11
|
const raw = value.trim();
|
|
12
12
|
if (raw.toLowerCase() === "off")
|
|
13
13
|
return "off";
|
|
14
|
-
const match = /^(\d+)([smh])
|
|
15
|
-
const seconds = match
|
|
14
|
+
const match = /^(\d+)([smh])$/i.exec(raw);
|
|
15
|
+
const seconds = match
|
|
16
|
+
? Number(match[1]) * UNITS[match[2].toLowerCase()]
|
|
17
|
+
: 0;
|
|
16
18
|
if (!match || seconds <= 0)
|
|
17
19
|
throw new Error(`${label} must be a duration like 90s, 30m, 4h, or off (got ${JSON.stringify(value)})`);
|
|
18
20
|
return seconds;
|
package/dist/format.js
CHANGED
|
@@ -15,6 +15,15 @@ function argument(value) {
|
|
|
15
15
|
export function humanColorEnabled(isTTY, env) {
|
|
16
16
|
return isTTY === true && env.NO_COLOR === undefined && env.TERM !== "dumb";
|
|
17
17
|
}
|
|
18
|
+
/** Rendered from seconds rather than rounded to minutes: parseDuration accepts
|
|
19
|
+
* 45s, and "1m" for a 45s limit misreports it. */
|
|
20
|
+
function duration(seconds) {
|
|
21
|
+
return seconds % 3600 === 0
|
|
22
|
+
? `${seconds / 3600}h`
|
|
23
|
+
: seconds % 60 === 0
|
|
24
|
+
? `${seconds / 60}m`
|
|
25
|
+
: `${seconds}s`;
|
|
26
|
+
}
|
|
18
27
|
function recordHuman(record, color) {
|
|
19
28
|
const paint = (value, code) => color ? `\u001b[${code}m${value}\u001b[0m` : value;
|
|
20
29
|
const field = (label, value) => ` ${paint(label + ":", 90)} ${value}`;
|
|
@@ -54,13 +63,6 @@ function recordHuman(record, color) {
|
|
|
54
63
|
: record.model_source === "config"
|
|
55
64
|
? " (configured default)"
|
|
56
65
|
: ""}`));
|
|
57
|
-
// Rendered from seconds rather than rounded to minutes: parseDuration accepts
|
|
58
|
-
// 45s, and "1m" for a 45s limit misreports it.
|
|
59
|
-
const duration = (seconds) => seconds % 3600 === 0
|
|
60
|
-
? `${seconds / 3600}h`
|
|
61
|
-
: seconds % 60 === 0
|
|
62
|
-
? `${seconds / 60}m`
|
|
63
|
-
: `${seconds}s`;
|
|
64
66
|
if (record.idle_timeout)
|
|
65
67
|
lines.push(field("Idle timeout", duration(record.idle_timeout)));
|
|
66
68
|
if (record.ttl)
|
package/dist/guard.js
CHANGED
|
@@ -265,17 +265,20 @@ export const DEFAULT_IDLE_TIMEOUT = 1800;
|
|
|
265
265
|
* under `host = "pty"` in config and then never arm it.
|
|
266
266
|
*/
|
|
267
267
|
export function resolveLifecycle(req, config, reapable) {
|
|
268
|
-
const one = (flag, configured, label, fallback) => {
|
|
268
|
+
const one = (flag, configured, label, configLabel, fallback) => {
|
|
269
269
|
// `off` asks for nothing, so it is accepted even where a lifecycle cannot
|
|
270
270
|
// be honoured: a wrapper that always passes it defensively must still be
|
|
271
271
|
// able to launch a pty session.
|
|
272
272
|
if (flag !== undefined && !reapable && flag.trim().toLowerCase() !== "off")
|
|
273
273
|
throw new Error(`${label} applies to tmux sessions, which outlive the process that ` +
|
|
274
274
|
`launched them; this launch is ${req.kind === "task" ? "a task" : `hosted on ${req.host ?? "the configured host"}`}`);
|
|
275
|
+
// Each source names ITSELF in the error: a bad config value reported as
|
|
276
|
+
// "--idle-timeout must be ..." sends the reader looking for a flag they
|
|
277
|
+
// never typed.
|
|
275
278
|
const chosen = flag !== undefined
|
|
276
279
|
? parseDuration(flag, label)
|
|
277
280
|
: configured !== undefined
|
|
278
|
-
? parseDuration(configured,
|
|
281
|
+
? parseDuration(configured, configLabel)
|
|
279
282
|
: undefined;
|
|
280
283
|
if (!reapable)
|
|
281
284
|
return null;
|
|
@@ -284,8 +287,8 @@ export function resolveLifecycle(req, config, reapable) {
|
|
|
284
287
|
return chosen === "off" ? null : chosen;
|
|
285
288
|
};
|
|
286
289
|
return {
|
|
287
|
-
idleTimeout: one(req.idleTimeout, config.session.idle_timeout, "--idle-timeout", DEFAULT_IDLE_TIMEOUT),
|
|
288
|
-
ttl: one(req.ttl, config.session.ttl, "--ttl", null),
|
|
290
|
+
idleTimeout: one(req.idleTimeout, config.session.idle_timeout, "--idle-timeout", "[session] idle_timeout", DEFAULT_IDLE_TIMEOUT),
|
|
291
|
+
ttl: one(req.ttl, config.session.ttl, "--ttl", "[session] ttl", null),
|
|
289
292
|
};
|
|
290
293
|
}
|
|
291
294
|
export function runtimeArgs(req) {
|
package/dist/hosts/tmux.js
CHANGED
|
@@ -57,6 +57,19 @@ export function parseWindowList(output) {
|
|
|
57
57
|
label: label.join(FIELD),
|
|
58
58
|
}));
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* One shell command line for `run-shell`.
|
|
62
|
+
*
|
|
63
|
+
* Two escapes, not one. POSIX single-quoting keeps the shell from splitting an
|
|
64
|
+
* argument, and `#` is doubled because tmux expands the command as a FORMAT
|
|
65
|
+
* before any shell sees it: an unescaped `#{pane_pid}` inside a path is
|
|
66
|
+
* silently replaced, and quoting cannot prevent that.
|
|
67
|
+
*/
|
|
68
|
+
export function tmuxCommand(argv) {
|
|
69
|
+
return argv
|
|
70
|
+
.map((a) => `'${a.replaceAll("'", `'\\''`).replaceAll("#", "##")}'`)
|
|
71
|
+
.join(" ");
|
|
72
|
+
}
|
|
60
73
|
export class TmuxHost {
|
|
61
74
|
server;
|
|
62
75
|
id = "tmux";
|
|
@@ -86,6 +99,11 @@ export class TmuxHost {
|
|
|
86
99
|
capabilities() {
|
|
87
100
|
return { watchable: true, attachable: true, survivesParentExit: true };
|
|
88
101
|
}
|
|
102
|
+
/** The tmux server this host talks to, so a caller needing it in an armed
|
|
103
|
+
* command does not reach in with a structural cast. */
|
|
104
|
+
socketName() {
|
|
105
|
+
return this.server;
|
|
106
|
+
}
|
|
89
107
|
/** Null when the window is gone — the caller stops rather than guesses. */
|
|
90
108
|
async sessionState(hostRef) {
|
|
91
109
|
try {
|
|
@@ -113,7 +131,7 @@ export class TmuxHost {
|
|
|
113
131
|
"-b",
|
|
114
132
|
"-d",
|
|
115
133
|
String(Math.max(1, Math.round(seconds))),
|
|
116
|
-
argv
|
|
134
|
+
tmuxCommand(argv),
|
|
117
135
|
]);
|
|
118
136
|
}
|
|
119
137
|
async launch(opts) {
|
package/dist/reap.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { Registry } from "./registry.js";
|
|
2
|
-
|
|
2
|
+
// The floor is low so a short limit is honoured rather than rounded up: a 45s
|
|
3
|
+
// ttl behind a 30s floor fires at up to 75s. A session with a limit that short
|
|
4
|
+
// does not live long enough for the extra polls to matter.
|
|
5
|
+
const MIN_POLL = 5, MAX_POLL = 300;
|
|
3
6
|
/**
|
|
4
7
|
* Derived from the SHORTER active expiry: a 4h idle timeout paired with a 2m
|
|
5
8
|
* ttl must not poll on the 4h scale, or the ttl overshoots by minutes.
|
package/dist/run.js
CHANGED
|
@@ -451,8 +451,8 @@ export class Muster {
|
|
|
451
451
|
// armed before the window is recorded would poll a target the registry
|
|
452
452
|
// cannot name. `schedule` is optional on the host interface, so pty and
|
|
453
453
|
// macos-terminal opt out by not implementing it.
|
|
454
|
-
if (driver?.schedule && (lifecycle.idleTimeout ?? lifecycle.ttl))
|
|
455
|
-
await driver
|
|
454
|
+
if (driver?.schedule && (lifecycle.idleTimeout ?? lifecycle.ttl)) {
|
|
455
|
+
const armed = await driver
|
|
456
456
|
.schedule(pollInterval(lifecycle), [
|
|
457
457
|
process.execPath,
|
|
458
458
|
// ../dist/muster.js, not ./muster.js: this resolves from src/run.ts
|
|
@@ -463,12 +463,24 @@ export class Muster {
|
|
|
463
463
|
"--home",
|
|
464
464
|
this.home,
|
|
465
465
|
"--socket",
|
|
466
|
-
driver.
|
|
466
|
+
driver.socketName?.() ?? "muster",
|
|
467
467
|
entry.launchId,
|
|
468
468
|
])
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
469
|
+
.then(() => true, () => false);
|
|
470
|
+
// A lifecycle that could not be armed must not fail the launch — the
|
|
471
|
+
// session is up and usable, and the orphan risk is what we had before.
|
|
472
|
+
// But it must not be REPORTED either: a record promising an expiry
|
|
473
|
+
// nothing will honour is worse than one promising none, because there
|
|
474
|
+
// is no way to tell from the outside.
|
|
475
|
+
if (!armed) {
|
|
476
|
+
lifecycle.idleTimeout = null;
|
|
477
|
+
lifecycle.ttl = null;
|
|
478
|
+
entry = await this.registry.update(entry.launchId, {
|
|
479
|
+
idleTimeout: null,
|
|
480
|
+
ttl: null,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
472
484
|
let diagnostic = req.runtime === "claude"
|
|
473
485
|
? "no Claude session registry found (check terminal for workspace trust or login prompts)"
|
|
474
486
|
: req.runtime === "opencode"
|