@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 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])$/.exec(raw);
15
- const seconds = match ? Number(match[1]) * UNITS[match[2]] : 0;
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, label)
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) {
@@ -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.map((a) => `'${a.replaceAll("'", `'\\''`)}'`).join(" "),
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
- const MIN_POLL = 30, MAX_POLL = 300;
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.server ?? "muster",
466
+ driver.socketName?.() ?? "muster",
467
467
  entry.launchId,
468
468
  ])
469
- // A lifecycle that cannot be armed must not fail the launch: the
470
- // session is up and usable, and the orphan risk is what we had before.
471
- .catch(() => { });
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"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brutalsystems/muster",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Launch instructed, reachable local agent sessions",
5
5
  "type": "module",
6
6
  "engines": {