@mu-crew/mu 4.1.0 → 4.2.0

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.
@@ -2052,7 +2052,7 @@ Matched line: ${line}${scroll}`,
2052
2052
  "mux.none_found": (tried) => `no supported multiplexer found (tried: ${tried.join(", ")}); install tmux \u2265 3.0 (task, DAG and sql verbs work without one)`,
2053
2053
  "mux.tmux_failed": (args, exit, detail) => `tmux ${args.join(" ")} failed (exit ${exit}): ${detail}`,
2054
2054
  "mux.herdr_failed": (args, exit, detail) => `herdr ${args.join(" ")} failed (exit ${exit}): ${detail}`,
2055
- "mux.herdr_syntax": (args, firstLine3) => `herdr rejected the command line (exit 2) \u2014 this is a bug in mu, not a herdr outage. Ran: herdr ${args.join(" ")}. herdr said: ${firstLine3}`,
2055
+ "mux.herdr_syntax": (args, firstLine4) => `herdr rejected the command line (exit 2) \u2014 this is a bug in mu, not a herdr outage. Ran: herdr ${args.join(" ")}. herdr said: ${firstLine4}`,
2056
2056
  "mux.herdr_unsupported_cli": (cli, detail) => `herdr does not recognise --cli ${cli} as an interactive agent kind, so it cannot classify that pane's status. herdr said: ${detail}`,
2057
2057
  "mux.herdr_command_override": (cli, what) => `${what} cannot be honoured on the herdr backend: 'herdr agent start --kind ${cli}' resolves the executable itself and has no override flag. mu refuses rather than starting a different binary than you asked for.`,
2058
2058
  "mux.herdr_group_close": (workspaceId, session, detail) => `herdr refused to close workspace ${workspaceId} (${session}): it has linked worktree workspaces, which mu did not create and will not close on your behalf. herdr said: ${detail}`,
@@ -2067,6 +2067,23 @@ Matched line: ${line}${scroll}`,
2067
2067
  "host.in_use": (host, holders) => `host ${host} still holds channels (${holders.join(", ")}): close those agents first`,
2068
2068
  "host.spawn_unsupported": (host, mux) => `--host ${host} needs a multiplexer that runs a shell command in its pane; ${mux} starts agents itself and cannot run the profile's shell template (use tmux)`,
2069
2069
  "host.check_failed": (host, failed) => `host ${host} check failed: ${failed.join(", ")}`,
2070
+ "host.handback": (host, field, detail) => `host ${host}: its ${field} template exited 3, a hand-back (a human must act, e.g. a hardware key)${detail === "" ? "" : `: ${detail}`}. Do not retry or ssh yourself: stop and ask`,
2071
+ "carrier.stop_refused": (host, handle, why) => why === "stop-changed" ? `host ${host}: the stop template changed since job ${handle} was recorded; stop it by hand with the old template, or mu agent close <name> --abandon-job` : why === "no-host" ? `host ${host} has no profile; job ${handle} was not stopped: stop it by hand, or mu agent close <name> --abandon-job` : `job handle ${JSON.stringify(handle)} on host ${host} is not a valid handle (A-Z a-z 0-9 . _ : @ -, 1 to 128 bytes); nothing was run`,
2072
+ "carrier.stop_failed": (host, handle, detail) => `host ${host}: stopping job ${handle} failed${detail === "" ? "" : `: ${detail}`}; its handle is kept`,
2073
+ "carrier.job_unstopped": (name, handle, host, reason = null) => reason === null ? `${name} has an unstopped job ${handle} on ${host}: mu agent close ${name}` : `${name} has job ${handle} on ${host} kept with its reservation (${reason}): mu agent close ${name}`,
2074
+ "carrier.job_reserved": (name, host, launch, pane, reason = null) => launch === "none" ? reason !== null ? `${name} is reserved by a lease spawn on ${host} that failed before launching its carrier and kept the reservation as the only pointer to what it left: ${reason}. Remove that on the host if it is still there, then drop the reservation: mu agent close ${name} --abandon-job` : `${name} is reserved by a lease spawn on ${host} that has not launched its carrier (in progress, or its spawn was killed): mu agent close ${name}` : `${name} is reserved by a lease spawn on ${host} whose carrier may have been launched (pane ${pane ?? "unknown"}) and whose job handle is unknown${reason === null ? "" : ` (${reason})`}: mu agent close ${name}, or mu agent close ${name} --abandon-job once that job is dealt with`,
2075
+ "carrier.job_unresolved": (name, ws, host, facts, keptForTree) => keptForTree ? `${name}: its lease spawn on ${host} failed before launching a carrier and kept the reservation for what it left (${facts}); nothing was closed. Remove that on the host if it is still there, then: mu agent close ${name} -w ${ws} --abandon-job` : `${name}: spawn in progress, or its lease carrier on ${host} may still publish a job handle (${facts}); nothing was closed. Retry once the spawn settles; once that job is dealt with: mu agent close ${name} -w ${ws} --abandon-job`,
2076
+ "carrier.job_kept": (name, ws, host, why) => `${name}: ${why}; its reservation on ${host} is kept: check that job, then mu agent close ${name} -w ${ws} again, or --abandon-job`,
2077
+ "carrier.job_unreadable": (name, file, detail) => `agent ${name}: the job file ${file} exists but cannot be read (${detail}); rolled back, its reservation and any remote worktree kept`,
2078
+ "carrier.rollback_reserved": (name, ws, host, why) => `the lease spawn's carrier on ${host} may have dispatched a job (${why}): the reservation and any remote worktree are kept; mu agent close ${name} -w ${ws}, or --abandon-job once that job is dealt with`,
2079
+ "carrier.reservation_abandoned": (name, host, file, facts) => `agent close ${name} --abandon-job: reservation on ${host} dropped without a stop (${facts}); a late job handle would be in ${file}, left in place`,
2080
+ "carrier.lease_gate": (host) => `host ${host} is a lease profile, which runs no command gates (a carrier's exit is not a gate result): use a session profile, or run the command through the carrier directly`,
2081
+ "carrier.job_timeout": (name, host, ms, file) => `agent ${name} on ${host}: its carrier wrote no job handle to ${file} within ${ms}ms (MU_SPAWN_JOB_MS); rolled back. Does the profile's shell pass {job_file} to the carrier?`,
2082
+ "carrier.job_invalid": (name, file, line) => `agent ${name}: the job file ${file} holds ${JSON.stringify(line.slice(0, 80))}, not a handle (A-Z a-z 0-9 . _ : @ -, 1 to 128 bytes); rolled back, its reservation and any remote worktree kept`,
2083
+ "carrier.job_unrecorded": (name, handle) => `agent ${name}: job ${handle} could not be recorded (the spawn's reservation was abandoned or holds another handle); rolled back, nothing stopped`,
2084
+ "carrier.transport_changed": (host) => `host ${host}: its transport changed while the spawn was admitted; nothing was launched, try again`,
2085
+ "carrier.rollback_kept": (name, ws, host, handle, why) => `job ${handle} on ${host} was not stopped (${why}): its handle and any remote worktree are kept; once the host answers: mu agent close ${name} -w ${ws}`,
2086
+ "carrier.dead_worker": (name, host, handle) => handle === null ? `remote worker ${name} on ${host} has no pane, and its carrier may have dispatched a job whose handle is unknown: mu agent close ${name} (mu agent collect first keeps its commits)` : `remote worker ${name} on ${host} has no pane, but its job ${handle} may still run there: mu agent close ${name} stops it (mu agent collect first keeps its commits)`,
2070
2087
  "host.channel_lost": (host, holder) => `host ${host}: channel ${holder} was lost`,
2071
2088
  "remote.prepare": (host, what, detail) => `host ${host}: ${what}${detail === "" ? "" : `: ${detail}`}`,
2072
2089
  "remote.worktree_kept": (agent, detail) => `agent ${agent}: remote worktree kept: ${detail}`,
@@ -2096,13 +2113,12 @@ function say(key, ...facts) {
2096
2113
 
2097
2114
  // src/agents/spawn.ts
2098
2115
  import { execFile as execFile4 } from "child_process";
2099
- import { existsSync as existsSync5, mkdirSync as mkdirSync3, rmSync as rmSync3, statSync as statSync2 } from "fs";
2100
- import { basename as basename2, dirname as dirname12, resolve as resolve3 } from "path";
2101
- import { setTimeout as delay } from "timers/promises";
2116
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, rmSync as rmSync4, statSync as statSync2 } from "fs";
2117
+ import { basename as basename2, dirname as dirname13, resolve as resolve3 } from "path";
2102
2118
  import { promisify as promisify3 } from "util";
2103
2119
 
2104
2120
  // src/agents.ts
2105
- import { dirname as dirname11 } from "path";
2121
+ import { dirname as dirname12 } from "path";
2106
2122
 
2107
2123
  // src/agent-state.ts
2108
2124
  import { accessSync, constants, existsSync as existsSync2 } from "fs";
@@ -2783,6 +2799,53 @@ function captureDoctorCheck(db, sqliteVersion) {
2783
2799
  return { status: "ok", detail: `capture ${stamp}, triggers ${short}, sqlite ${version}` };
2784
2800
  }
2785
2801
 
2802
+ // src/db-carrier.ts
2803
+ var CARRIER_JOBS_DDL = `
2804
+ -- carrier_jobs: the RESERVATION of a lease agent's remote job (machine-local).
2805
+ -- Inserted before the carrier's pane opens, name-exclusive per (workstream, name)
2806
+ -- (ordinal 0; a rebuilt pre-reservation table's older rows of one name get 1..n),
2807
+ -- every update guarded by the immutable spawn token. 'launch' turns 'intent' just
2808
+ -- before the mux is asked for the pane; agent_id binds at finalize, job at the
2809
+ -- handshake. No FK, no trigger, no op: agent deletion never touches it. Released
2810
+ -- (deleted) only after a successful stop and the job file's and tree's cleanup, by
2811
+ -- its creator when nothing was launched, or by an explicit abandon. 'host_stop' is
2812
+ -- the admitted stop template. 'tree_path'/'tree_agent_id': the remote worktree a
2813
+ -- --worktree spawn prepared for it (path on 'host', and the placeholder incarnation a
2814
+ -- recovery record would carry); a close with no agent row releases the reservation
2815
+ -- only once THAT tree's close succeeded.
2816
+ CREATE TABLE IF NOT EXISTS carrier_jobs (
2817
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2818
+ token TEXT NOT NULL UNIQUE,
2819
+ workstream TEXT NOT NULL,
2820
+ name TEXT NOT NULL,
2821
+ ordinal INTEGER NOT NULL DEFAULT 0,
2822
+ host TEXT NOT NULL,
2823
+ host_stop TEXT NOT NULL,
2824
+ launch TEXT NOT NULL DEFAULT 'none' CHECK (launch IN ('none', 'intent')),
2825
+ creator_pid INTEGER,
2826
+ creator_start TEXT,
2827
+ agent_id INTEGER,
2828
+ pane_id TEXT,
2829
+ job TEXT,
2830
+ reason TEXT,
2831
+ created_at TEXT NOT NULL,
2832
+ recorded_at TEXT,
2833
+ tree_path TEXT,
2834
+ tree_agent_id INTEGER
2835
+ );
2836
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_carrier_jobs_name ON carrier_jobs (workstream, name, ordinal);
2837
+ `;
2838
+ function carrierJobsPreReservation(db) {
2839
+ return db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'carrier_jobs'").get() !== void 0 && db.prepare("SELECT 1 FROM pragma_table_info('carrier_jobs') WHERE name = 'token'").get() === void 0;
2840
+ }
2841
+ var CARRIER_JOBS_REBUILD = `INSERT INTO carrier_jobs
2842
+ (token, workstream, name, ordinal, host, host_stop, launch, agent_id, job, created_at, recorded_at)
2843
+ SELECT lower(hex(randomblob(16))), COALESCE(workstream, ''), COALESCE(name, ''),
2844
+ ROW_NUMBER() OVER (PARTITION BY workstream, name ORDER BY agent_id DESC) - 1,
2845
+ COALESCE(host, ''), host_stop, 'intent', agent_id, job,
2846
+ COALESCE(recorded_at, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), recorded_at
2847
+ FROM carrier_jobs_pre_reservation ORDER BY agent_id`;
2848
+
2786
2849
  // src/db-recovery.ts
2787
2850
  var REASON = `CASE COALESCE((SELECT intent FROM op_ctx WHERE id = 1), '')
2788
2851
  WHEN 'workstream.teardown' THEN 'teardown'
@@ -3104,7 +3167,13 @@ function hostProfilesRepoRequired(db) {
3104
3167
  function applySchemaDdl(db) {
3105
3168
  if (hostProfilesRepoRequired(db)) db.exec("DROP TABLE host_profiles");
3106
3169
  for (const t of LEGACY_RECOVERY_TRIGGERS) db.exec(`DROP TRIGGER IF EXISTS ${t}`);
3170
+ const oldCarrierJobs = carrierJobsPreReservation(db);
3171
+ if (oldCarrierJobs) db.exec("ALTER TABLE carrier_jobs RENAME TO carrier_jobs_pre_reservation");
3107
3172
  db.exec(CURRENT_SCHEMA);
3173
+ if (oldCarrierJobs) {
3174
+ db.exec(CARRIER_JOBS_REBUILD);
3175
+ db.exec("DROP TABLE carrier_jobs_pre_reservation");
3176
+ }
3108
3177
  for (const [table, column, ddl] of ADDED_COLUMNS) {
3109
3178
  if (!hasColumn(db, table, column)) db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddl}`);
3110
3179
  }
@@ -3182,7 +3251,17 @@ var ADDED_COLUMNS = [
3182
3251
  ...["request", "verdict", "agent"].map((c) => ["delegate_calls", c, "TEXT"]),
3183
3252
  // The template's process group, recorded before it runs (hosts.ts).
3184
3253
  ["host_channels", "pgid", "INTEGER"],
3185
- ["host_channels", "pgid_start", "TEXT"]
3254
+ ["host_channels", "pgid_start", "TEXT"],
3255
+ // A host profile's transport ('session' | 'lease', validated in addHost: SQLite adds no CHECK
3256
+ // in place) and a lease profile's stop template (hosts.ts).
3257
+ ["host_profiles", "transport", "TEXT NOT NULL DEFAULT 'session'"],
3258
+ ["host_profiles", "stop", "TEXT"],
3259
+ // The transport of the profile a remote agent was spawned on ('session' | 'lease'): a close
3260
+ // decides lease-ness from the row, never from the profile as it is now.
3261
+ ["agents", "host_transport", "TEXT"],
3262
+ // A lease reservation's remote worktree (src/carrier-jobs.ts): which tree it belongs to.
3263
+ ["carrier_jobs", "tree_path", "TEXT"],
3264
+ ["carrier_jobs", "tree_agent_id", "INTEGER"]
3186
3265
  ];
3187
3266
  function hasColumn(db, table, column) {
3188
3267
  return db.prepare("SELECT 1 AS x FROM pragma_table_info(?) WHERE name = ?").get(table, column) !== void 0;
@@ -3196,6 +3275,7 @@ function schemaIsCurrent(db) {
3196
3275
  if (ADDED_COLUMNS.some(([t, c]) => !hasColumn(db, t, c))) return false;
3197
3276
  if (LEGACY_RECOVERY_TRIGGERS.some((t) => have.has(`trigger:${t}`))) return false;
3198
3277
  if (hostProfilesRepoRequired(db)) return false;
3278
+ if (carrierJobsPreReservation(db)) return false;
3199
3279
  if (expected.indexes.some((i) => !have.has(`index:${i}`))) return false;
3200
3280
  for (const [name, sql] of expected.views) {
3201
3281
  if (have.get(`view:${name}`) !== sql) return false;
@@ -3213,6 +3293,7 @@ var DEFAULT_EFFORT_DAYS = 1;
3213
3293
  var EXPECTED_TABLES = [
3214
3294
  "agent_settles",
3215
3295
  "agents",
3296
+ "carrier_jobs",
3216
3297
  "delegate_calls",
3217
3298
  "host_channels",
3218
3299
  "host_profiles",
@@ -3244,6 +3325,7 @@ var PORTABLE_TABLES = ["task_edges", "task_notes", "tasks", "workstreams"];
3244
3325
  var MACHINE_LOCAL_TABLES = [
3245
3326
  "agent_settles",
3246
3327
  "agents",
3328
+ "carrier_jobs",
3247
3329
  "delegate_calls",
3248
3330
  "host_channels",
3249
3331
  "host_profiles",
@@ -3416,6 +3498,7 @@ CREATE TABLE IF NOT EXISTS agents (
3416
3498
  collected_head TEXT, -- HEAD last collected from a remote worktree (machine-local)
3417
3499
  repo TEXT, -- a remote worktree's repo on its host, made absolute at spawn (machine-local); NULL: none
3418
3500
  gate_token TEXT, -- remote command gate: spawn-unique token, also the key of its remote result record (machine-local); NULL: not a gate
3501
+ host_transport TEXT, -- the transport of the profile it was spawned on, 'session' | 'lease' (machine-local); NULL: local, or a row older than the column
3419
3502
  created_at TEXT NOT NULL,
3420
3503
  updated_at TEXT NOT NULL,
3421
3504
  UNIQUE (workstream_id, name),
@@ -3661,9 +3744,12 @@ CREATE TABLE IF NOT EXISTS host_profiles (
3661
3744
  exec TEXT NOT NULL,
3662
3745
  fetch TEXT NOT NULL,
3663
3746
  repo TEXT, -- default remote repo for spawns without --root; NULL: none
3664
- max_sessions INTEGER CHECK (max_sessions > 0)
3747
+ max_sessions INTEGER CHECK (max_sessions > 0),
3748
+ transport TEXT NOT NULL DEFAULT 'session', -- 'session' | 'lease' (validated in addHost, no CHECK: ADDED_COLUMNS cannot add one)
3749
+ stop TEXT -- lease only: the template that ends a job given its handle ({job}); NULL for session
3665
3750
  );
3666
3751
 
3752
+ ${CARRIER_JOBS_DDL}
3667
3753
  -- host_channels: the connection slots of a host (machine-local). One row
3668
3754
  -- per holder. 'transient' rides an invocation (exec, fetch, check) and is
3669
3755
  -- reclaimed only when its pid is proven gone; 'attached' rides an agent's
@@ -3801,17 +3887,23 @@ ${GOALS_VIEW_SQL}
3801
3887
  // src/ctl/path.ts
3802
3888
  var CTL_SOCK_ENV = "MU_CTL_SOCK";
3803
3889
  var MAX_SOCK_PATH = 103;
3804
- function sockPathUnder(dir, workstream, agent) {
3805
- const readable = join2(dir, workstream, `${agent}.sock`);
3890
+ var HASHED_DIR = "_h";
3891
+ function sockPathUnder(dir, workstream, agent, ext = "sock") {
3892
+ const readable = join2(dir, workstream, `${agent}.${ext}`);
3806
3893
  if (Buffer.byteLength(readable) <= MAX_SOCK_PATH) return readable;
3807
3894
  const h = createHash2("sha1").update(`${workstream}/${agent}`).digest("hex").slice(0, 16);
3808
- return join2(dir, "h", `${h}.sock`);
3895
+ return join2(dir, HASHED_DIR, `${h}.${ext}`);
3809
3896
  }
3810
3897
  function ctlSocketPath(workstream, agent, stateDir) {
3811
3898
  const dbPath = process.env.MU_DB_PATH;
3812
3899
  const base = stateDir ?? (dbPath ? dirname2(dbPath) : defaultStateDir());
3813
3900
  return sockPathUnder(join2(base, "sock"), workstream, agent);
3814
3901
  }
3902
+ function carrierJobFilePath(workstream, agent, stateDir) {
3903
+ const dbPath = process.env.MU_DB_PATH;
3904
+ const base = stateDir ?? (dbPath ? dirname2(dbPath) : defaultStateDir());
3905
+ return sockPathUnder(join2(base, "carrier"), workstream, agent, "job");
3906
+ }
3815
3907
  function unlinkCtlSocket(db, name, workstream) {
3816
3908
  if (db.memory) return;
3817
3909
  try {
@@ -3888,20 +3980,20 @@ function compareHlc(a, b) {
3888
3980
  if (a > b) return 1;
3889
3981
  return 0;
3890
3982
  }
3891
- function nextHlc(db, now2 = Date.now()) {
3983
+ function nextHlc(db, now3 = Date.now()) {
3892
3984
  const row = db.prepare(
3893
3985
  `UPDATE machine_identity
3894
3986
  SET last_wall = MAX(last_wall, @now),
3895
3987
  last_counter = CASE WHEN @now > last_wall THEN 0 ELSE last_counter + 1 END
3896
3988
  WHERE id = 1
3897
3989
  RETURNING machine_id, last_wall, last_counter`
3898
- ).get({ now: Math.floor(now2) });
3990
+ ).get({ now: Math.floor(now3) });
3899
3991
  if (!row) throw new MachineIdentityMissingError();
3900
3992
  return formatHlc({ wallMs: row.last_wall, counter: row.last_counter, machineId: row.machine_id });
3901
3993
  }
3902
- function receiveHlc(db, remoteHlc, now2 = Date.now()) {
3994
+ function receiveHlc(db, remoteHlc, now3 = Date.now()) {
3903
3995
  const remote = parseHlc(remoteHlc);
3904
- const wallNow = Math.floor(now2);
3996
+ const wallNow = Math.floor(now3);
3905
3997
  const read = prepareCached(
3906
3998
  db,
3907
3999
  "SELECT machine_id, last_wall, last_counter FROM machine_identity WHERE id = 1"
@@ -4778,7 +4870,8 @@ function muTable(opts) {
4778
4870
  }
4779
4871
  function isJsonMode() {
4780
4872
  const argv = process.argv;
4781
- return argv.some((a) => a === "--json" || a.startsWith("--json=")) || isSqlVerbArgv(argv) && !argv.includes("--table") || isStateVerbArgv(argv) || isWaitVerbArgv(argv);
4873
+ return argv.some((a) => a === "--json" || a.startsWith("--json=")) || (isSqlVerbArgv(argv) || isAgentListArgv(argv)) && !argv.includes("--table") || isStateVerbArgv(argv) || isWaitVerbArgv(argv) || // `mu next` prints ONE JSON result for every outcome; its failures too.
4874
+ verbWords(argv)[0] === "next";
4782
4875
  }
4783
4876
  function verbWords(argv) {
4784
4877
  const words = [];
@@ -4793,6 +4886,10 @@ function isSqlVerbArgv(argv) {
4793
4886
  const words = verbWords(argv);
4794
4887
  return words[0] === "sql" || words[0] === "db" && words[1] === "query";
4795
4888
  }
4889
+ function isAgentListArgv(argv) {
4890
+ const [first, second] = verbWords(argv);
4891
+ return first === "agent" && second === "list";
4892
+ }
4796
4893
  function isStateVerbArgv(argv) {
4797
4894
  const [first, second] = verbWords(argv);
4798
4895
  return first === "state" || first === "me" && second !== "tasks" && second !== "next";
@@ -6217,8 +6314,8 @@ function mapMurmurState(token, since) {
6217
6314
  function parseSince(value) {
6218
6315
  return value !== void 0 && /^\d+$/.test(value) ? Number(value) : null;
6219
6316
  }
6220
- async function readMurmur(now2) {
6221
- if (murmurCache !== void 0 && now2 - murmurCache.at < MURMUR_CACHE_MS) {
6317
+ async function readMurmur(now3) {
6318
+ if (murmurCache !== void 0 && now3 - murmurCache.at < MURMUR_CACHE_MS) {
6222
6319
  return murmurCache.stdout;
6223
6320
  }
6224
6321
  let stdout;
@@ -6227,7 +6324,7 @@ async function readMurmur(now2) {
6227
6324
  } catch {
6228
6325
  stdout = null;
6229
6326
  }
6230
- murmurCache = { at: now2, stdout };
6327
+ murmurCache = { at: now3, stdout };
6231
6328
  return stdout;
6232
6329
  }
6233
6330
  function remotePanes(stdout) {
@@ -6438,9 +6535,117 @@ async function readBaseStates(agents, opts, livenessOnly = /* @__PURE__ */ new S
6438
6535
  }
6439
6536
 
6440
6537
  // src/hosts.ts
6441
- import { randomUUID as randomUUID4 } from "crypto";
6538
+ import { randomUUID as randomUUID5 } from "crypto";
6442
6539
  import { readFileSync } from "fs";
6443
6540
  import { dirname as dirname4 } from "path";
6541
+
6542
+ // src/carrier-jobs.ts
6543
+ import { randomUUID as randomUUID4 } from "crypto";
6544
+ var toJob = (r) => ({
6545
+ token: r.token,
6546
+ workstream: r.workstream,
6547
+ name: r.name,
6548
+ host: r.host,
6549
+ hostStop: r.host_stop,
6550
+ launch: r.launch,
6551
+ creatorPid: r.creator_pid,
6552
+ creatorStart: r.creator_start,
6553
+ agentId: r.agent_id,
6554
+ paneId: r.pane_id,
6555
+ job: r.job,
6556
+ reason: r.reason,
6557
+ treePath: r.tree_path,
6558
+ treeAgentId: r.tree_agent_id,
6559
+ createdAt: r.created_at,
6560
+ recordedAt: r.recorded_at
6561
+ });
6562
+ var now = () => (/* @__PURE__ */ new Date()).toISOString();
6563
+ function reserveCarrierJob(db, a) {
6564
+ const token = randomUUID4();
6565
+ try {
6566
+ const r = db.prepare(
6567
+ `INSERT INTO carrier_jobs (token, workstream, name, host, host_stop, launch, creator_pid, creator_start, created_at)
6568
+ SELECT ?, ?, ?, ?, ?, 'none', ?, ?, ?
6569
+ WHERE NOT EXISTS (SELECT 1 FROM carrier_jobs WHERE workstream = ? AND name = ?)`
6570
+ ).run(
6571
+ token,
6572
+ a.workstream,
6573
+ a.name,
6574
+ a.host,
6575
+ a.hostStop,
6576
+ a.creatorPid ?? null,
6577
+ a.creatorStart ?? null,
6578
+ now(),
6579
+ a.workstream,
6580
+ a.name
6581
+ );
6582
+ if (r.changes !== 1) return void 0;
6583
+ } catch (err) {
6584
+ if (err.code === "SQLITE_CONSTRAINT_UNIQUE") return void 0;
6585
+ throw err;
6586
+ }
6587
+ return getCarrierJob(db, token);
6588
+ }
6589
+ function getCarrierJob(db, token) {
6590
+ const r = db.prepare("SELECT * FROM carrier_jobs WHERE token = ?").get(token);
6591
+ return r === void 0 ? void 0 : toJob(r);
6592
+ }
6593
+ function getCarrierJobByAgent(db, agentId) {
6594
+ const r = db.prepare("SELECT * FROM carrier_jobs WHERE agent_id = ?").get(agentId);
6595
+ return r === void 0 ? void 0 : toJob(r);
6596
+ }
6597
+ function getCarrierJobByName(db, workstream, name) {
6598
+ return listCarrierJobsByName(db, workstream, name)[0];
6599
+ }
6600
+ function listCarrierJobsByName(db, workstream, name) {
6601
+ return db.prepare(
6602
+ "SELECT * FROM carrier_jobs WHERE workstream = ? AND name = ? ORDER BY ordinal, id DESC"
6603
+ ).all(workstream, name).map(toJob);
6604
+ }
6605
+ function listCarrierJobs(db) {
6606
+ return db.prepare("SELECT * FROM carrier_jobs ORDER BY id").all().map(toJob);
6607
+ }
6608
+ function setCarrierJobStop(db, token, hostStop) {
6609
+ return db.prepare(
6610
+ "UPDATE carrier_jobs SET host_stop = ? WHERE token = ? AND launch = 'none' AND job IS NULL"
6611
+ ).run(hostStop, token).changes === 1;
6612
+ }
6613
+ function markCarrierLaunch(db, token) {
6614
+ return db.prepare("UPDATE carrier_jobs SET launch = 'intent' WHERE token = ?").run(token).changes === 1;
6615
+ }
6616
+ function recordCarrierPane(db, token, paneId) {
6617
+ return db.prepare(
6618
+ "UPDATE carrier_jobs SET pane_id = ? WHERE token = ? AND (pane_id IS NULL OR pane_id = ?)"
6619
+ ).run(paneId, token, paneId).changes === 1;
6620
+ }
6621
+ function bindCarrierJobAgent(db, token, agentId, paneId) {
6622
+ return db.prepare(
6623
+ `UPDATE carrier_jobs SET agent_id = ?, pane_id = ?
6624
+ WHERE token = ? AND (agent_id IS NULL OR agent_id = ?) AND (pane_id IS NULL OR pane_id = ?)`
6625
+ ).run(agentId, paneId, token, agentId, paneId).changes === 1;
6626
+ }
6627
+ function fillCarrierJob(db, token, job) {
6628
+ db.prepare(
6629
+ "UPDATE carrier_jobs SET job = ?, reason = NULL, recorded_at = ? WHERE token = ? AND job IS NULL"
6630
+ ).run(job, now(), token);
6631
+ return getCarrierJob(db, token)?.job === job;
6632
+ }
6633
+ function noteCarrierJob(db, token, reason) {
6634
+ db.prepare("UPDATE carrier_jobs SET reason = ? WHERE token = ?").run(reason, token);
6635
+ }
6636
+ function keepCarrierJobForTree(db, token, tree) {
6637
+ db.prepare(
6638
+ "UPDATE carrier_jobs SET reason = ?, tree_path = ?, tree_agent_id = ? WHERE token = ?"
6639
+ ).run(tree.reason, tree.path, tree.agentId, token);
6640
+ }
6641
+ function releaseCarrierJob(db, job) {
6642
+ return db.prepare("DELETE FROM carrier_jobs WHERE token = ? AND job IS ?").run(job.token, job.job).changes === 1;
6643
+ }
6644
+ function abandonCarrierJob(db, job) {
6645
+ return releaseCarrierJob(db, job);
6646
+ }
6647
+
6648
+ // src/hosts.ts
6444
6649
  var HostProfileError = class extends Error {
6445
6650
  name = "HostProfileError";
6446
6651
  };
@@ -6493,6 +6698,19 @@ var HostSpawnUnsupportedError = class extends Error {
6493
6698
  mux;
6494
6699
  name = "HostSpawnUnsupportedError";
6495
6700
  };
6701
+ var HostHandbackError = class extends Error {
6702
+ constructor(host, field, detail) {
6703
+ super(say("host.handback", host, field, detail));
6704
+ this.host = host;
6705
+ this.field = field;
6706
+ this.detail = detail;
6707
+ }
6708
+ host;
6709
+ field;
6710
+ detail;
6711
+ name = "HostHandbackError";
6712
+ handback = true;
6713
+ };
6496
6714
  var HostCheckFailedError = class extends Error {
6497
6715
  constructor(host, failed) {
6498
6716
  super(say("host.check_failed", host, failed));
@@ -6503,6 +6721,8 @@ var HostCheckFailedError = class extends Error {
6503
6721
  failed;
6504
6722
  name = "HostCheckFailedError";
6505
6723
  };
6724
+ var HOST_TRANSPORTS = ["session", "lease"];
6725
+ var JOB_HANDLE_RE = /^[A-Za-z0-9._:@-]{1,128}$/;
6506
6726
  var HOST_NAME_RE = /^[a-z][a-z0-9_-]{0,31}$/;
6507
6727
  function isValidHostName(name) {
6508
6728
  return HOST_NAME_RE.test(name);
@@ -6515,7 +6735,12 @@ function getHost(db, name) {
6515
6735
  function listHosts(db) {
6516
6736
  return db.prepare("SELECT * FROM host_profiles ORDER BY name").all();
6517
6737
  }
6518
- function addHost(db, p) {
6738
+ function addHost(db, input) {
6739
+ const p = {
6740
+ ...input,
6741
+ transport: input.transport ?? "session",
6742
+ stop: input.stop ?? null
6743
+ };
6519
6744
  if (!isValidHostName(p.name)) {
6520
6745
  throw new HostProfileError(
6521
6746
  `invalid host name ${JSON.stringify(p.name)}: lowercase letters, digits, - and _, starting with a letter (max 32)`
@@ -6527,17 +6752,26 @@ function addHost(db, p) {
6527
6752
  if (p.repo !== null && p.repo.trim() === "") {
6528
6753
  throw new HostProfileError("--repo must not be empty");
6529
6754
  }
6530
- for (const f of ["shell", "exec", "fetch"]) {
6531
- try {
6532
- validateHostTemplate(p[f]);
6533
- } catch (e) {
6534
- if (e instanceof HostTemplateError) throw new HostTemplateError(`--${f}: ${e.message}`);
6535
- throw e;
6536
- }
6755
+ if (!HOST_TRANSPORTS.includes(p.transport)) {
6756
+ throw new HostProfileError(
6757
+ `invalid --transport ${JSON.stringify(p.transport)}: ${HOST_TRANSPORTS.join(" or ")}`
6758
+ );
6537
6759
  }
6760
+ if (p.transport === "lease" && p.stop === null) {
6761
+ throw new HostProfileError(
6762
+ "--stop is required for --transport lease: the template that ends a job given its handle ({job})"
6763
+ );
6764
+ }
6765
+ if (p.transport === "session" && p.stop !== null) {
6766
+ throw new HostProfileError(
6767
+ "--stop is only for --transport lease: a session has no job to stop"
6768
+ );
6769
+ }
6770
+ const problem = profileTemplateProblem(p);
6771
+ if (problem !== void 0) throw new HostTemplateError(problem);
6538
6772
  try {
6539
6773
  db.prepare(
6540
- "INSERT INTO host_profiles (name, shell, exec, fetch, repo, max_sessions) VALUES (@name, @shell, @exec, @fetch, @repo, @max_sessions)"
6774
+ "INSERT INTO host_profiles (name, shell, exec, fetch, repo, max_sessions, transport, stop) VALUES (@name, @shell, @exec, @fetch, @repo, @max_sessions, @transport, @stop)"
6541
6775
  ).run(p);
6542
6776
  } catch (e) {
6543
6777
  if (e.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
@@ -6558,13 +6792,53 @@ function removeHost(db, name) {
6558
6792
  function hostStarter(host) {
6559
6793
  const ssh = "ssh -o ControlMaster=no -o ControlPath=none";
6560
6794
  return {
6561
- // ControlMaster=no: a multiplexed ssh never binds the local end of the forward.
6795
+ // ControlMaster=no ControlPath=none: a `-L` through a shared control master is bound by the
6796
+ // master, not by this client. The listener outlives the pane, and a later ssh asking for
6797
+ // the same forward exits 0 with no socket file (the master never re-binds it). Each pane
6798
+ // keeps its own connection, so its forward dies with it.
6562
6799
  shell: `${ssh} -o StreamLocalBindUnlink=yes -o ExitOnForwardFailure=yes -L {local_sock}:{remote_sock} ${host} -t 'cd {cwd:sh} && {env} "$SHELL" -ilc {cmd:sh}'`,
6563
6800
  exec: `${ssh} -o BatchMode=yes ${host} {cmd}`,
6564
6801
  fetch: `git fetch ssh://${host}{cwd} HEAD`
6565
6802
  };
6566
6803
  }
6567
- var HOST_PLACEHOLDERS = ["env", "cmd", "local_sock", "remote_sock", "cwd"];
6804
+ function hostMuleStarter(host) {
6805
+ return {
6806
+ shell: `mule run --host ${host} --lease --human --cwd {cwd} --job-file {job_file} --forward-unix {local_sock}:{remote_sock} -- env {env} sh -c {cmd}`,
6807
+ exec: `mule exec --host ${host} -- sh -c {cmd}`,
6808
+ fetch: `git fetch ssh://${host}{cwd} HEAD`,
6809
+ stop: `mule rm --host ${host} {job}`
6810
+ };
6811
+ }
6812
+ var HOST_PLACEHOLDERS = [
6813
+ "env",
6814
+ "cmd",
6815
+ "local_sock",
6816
+ "remote_sock",
6817
+ "cwd",
6818
+ "job_file",
6819
+ "job"
6820
+ ];
6821
+ var LEGACY_PLACEHOLDERS = [
6822
+ "env",
6823
+ "cmd",
6824
+ "local_sock",
6825
+ "remote_sock",
6826
+ "cwd"
6827
+ ];
6828
+ var TEMPLATE_SCOPES = {
6829
+ shell: LEGACY_PLACEHOLDERS,
6830
+ "lease-shell": [...LEGACY_PLACEHOLDERS, "job_file"],
6831
+ exec: LEGACY_PLACEHOLDERS,
6832
+ fetch: LEGACY_PLACEHOLDERS,
6833
+ stop: ["job", "cwd"]
6834
+ };
6835
+ var SCOPE_LABEL = {
6836
+ shell: "session shell",
6837
+ "lease-shell": "lease shell",
6838
+ exec: "exec",
6839
+ fetch: "fetch",
6840
+ stop: "stop"
6841
+ };
6568
6842
  var HOST_ENV_KEYS = [
6569
6843
  "MU_MANAGED_AGENT",
6570
6844
  "MU_AGENT_NAME",
@@ -6589,7 +6863,7 @@ function hostEnvWords(env) {
6589
6863
  return words.join(" ");
6590
6864
  }
6591
6865
  var KNOWN = `${HOST_PLACEHOLDERS.map((p) => `{${p}}`).join(" ")}; {name:sh} quotes for a second shell`;
6592
- function parseTemplate(t) {
6866
+ function parseTemplate(t, scope) {
6593
6867
  const out = [];
6594
6868
  let lit = "";
6595
6869
  let ctx = "none";
@@ -6625,6 +6899,11 @@ function parseTemplate(t) {
6625
6899
  "{env:sh} is refused: {env} is already text for the inner shell"
6626
6900
  );
6627
6901
  }
6902
+ if (scope !== void 0 && !TEMPLATE_SCOPES[scope].includes(name)) {
6903
+ throw new HostTemplateError(
6904
+ `{${name}} is not valid in the ${SCOPE_LABEL[scope]} template (it takes ${TEMPLATE_SCOPES[scope].map((p) => `{${p}}`).join(" ")})`
6905
+ );
6906
+ }
6628
6907
  if (ctx === "dq" && subst) {
6629
6908
  throw new HostTemplateError(`{${name}} inside $(...) or backticks in double quotes`);
6630
6909
  }
@@ -6663,12 +6942,44 @@ function parseTemplate(t) {
6663
6942
  flush();
6664
6943
  return out;
6665
6944
  }
6666
- function validateHostTemplate(template) {
6667
- parseTemplate(template);
6945
+ function validateHostTemplate(template, scope) {
6946
+ parseTemplate(template, scope);
6947
+ }
6948
+ function placeholderUses(template, scope) {
6949
+ const n = Object.fromEntries(HOST_PLACEHOLDERS.map((p) => [p, 0]));
6950
+ for (const tok of parseTemplate(template, scope)) if ("ph" in tok) n[tok.ph]++;
6951
+ return n;
6952
+ }
6953
+ function leaseShellProblems(shell) {
6954
+ const n = placeholderUses(shell, "lease-shell");
6955
+ return [
6956
+ ...n.job_file === 1 ? [] : [`a lease shell template must contain {job_file} exactly once (found ${n.job_file})`],
6957
+ ...n.local_sock > 0 ? [] : ["a lease shell template must contain {local_sock}"],
6958
+ ...n.remote_sock > 0 ? [] : ["a lease shell template must contain {remote_sock}"]
6959
+ ];
6960
+ }
6961
+ function profileTemplateProblem(p) {
6962
+ const fields = [
6963
+ ["shell", p.shell, p.transport === "lease" ? "lease-shell" : "shell"],
6964
+ ["exec", p.exec, "exec"],
6965
+ ["fetch", p.fetch, "fetch"],
6966
+ ...p.stop === null ? [] : [["stop", p.stop, "stop"]]
6967
+ ];
6968
+ for (const [f, t, scope] of fields) {
6969
+ try {
6970
+ validateHostTemplate(t, scope);
6971
+ const extra = scope === "lease-shell" ? leaseShellProblems(t) : scope === "stop" && placeholderUses(t, scope).job === 0 ? ["a stop template must contain {job}"] : [];
6972
+ if (extra.length > 0) return `--${f}: ${extra.join("; ")}`;
6973
+ } catch (e) {
6974
+ if (e instanceof HostTemplateError) return `--${f}: ${e.message}`;
6975
+ throw e;
6976
+ }
6977
+ }
6978
+ return void 0;
6668
6979
  }
6669
- function expandHostTemplate(template, values) {
6980
+ function expandHostTemplate(template, values, scope) {
6670
6981
  let out = "";
6671
- for (const tok of parseTemplate(template)) {
6982
+ for (const tok of parseTemplate(template, scope)) {
6672
6983
  if ("lit" in tok) {
6673
6984
  out += tok.lit;
6674
6985
  continue;
@@ -6717,8 +7028,8 @@ function pidProvenGone(pid, start) {
6717
7028
  return e.code === "ESRCH";
6718
7029
  }
6719
7030
  if (start === null) return false;
6720
- const now2 = pidStart(pid);
6721
- return now2 !== null && now2 !== start;
7031
+ const now3 = pidStart(pid);
7032
+ return now3 !== null && now3 !== start;
6722
7033
  }
6723
7034
  function groupAlive(pgid, start) {
6724
7035
  try {
@@ -6727,8 +7038,8 @@ function groupAlive(pgid, start) {
6727
7038
  return e.code !== "ESRCH";
6728
7039
  }
6729
7040
  if (start === null) return true;
6730
- const now2 = pidStart(pgid);
6731
- return now2 === null || now2 === start;
7041
+ const now3 = pidStart(pgid);
7042
+ return now3 === null || now3 === start;
6732
7043
  }
6733
7044
  async function probeAttached(db, host, paneAlive3) {
6734
7045
  if (paneAlive3 === void 0) return [];
@@ -6770,7 +7081,7 @@ async function admitHostChannel(db, host, holder, opts = {}) {
6770
7081
  async function admit(db, host, holder, opts) {
6771
7082
  getHost(db, host);
6772
7083
  const dead = await probeAttached(db, host, opts.paneAlive);
6773
- const token = randomUUID4();
7084
+ const token = randomUUID5();
6774
7085
  const profile = db.transaction(() => {
6775
7086
  const profile2 = getHost(db, host);
6776
7087
  reclaim(db, host, dead);
@@ -6825,13 +7136,27 @@ async function withHostChannel(db, host, holder, run2, opts = {}) {
6825
7136
  }
6826
7137
  }
6827
7138
  var HOST_COMMAND_TIMEOUT_MS = 2e4;
7139
+ var HOST_GRACE_MS = 5e3;
6828
7140
  async function runLeasedTemplate(db, lease, profile, field, values, timeoutMs, localCwd) {
6829
- const expanded = expandHostTemplate(profile[field], values);
6830
- return execGroupCapture("/bin/sh", ["-c", expanded], {
7141
+ const template = profile[field];
7142
+ if (template === null)
7143
+ throw new HostTemplateError(`host ${profile.name} has no ${field} template`);
7144
+ const expanded = expandHostTemplate(template, values, field);
7145
+ const run2 = await execGroupCapture("/bin/sh", ["-c", expanded], {
6831
7146
  timeoutMs,
7147
+ // A carrier interrupted by the timeout needs time to stop what it dispatched.
7148
+ graceMs: HOST_GRACE_MS,
6832
7149
  ...localCwd !== void 0 ? { cwd: localCwd } : {},
6833
7150
  onSpawn: (pgid) => recordGroup(db, lease, pgid)
6834
7151
  });
7152
+ if (run2.exitCode === 3) {
7153
+ throw new HostHandbackError(
7154
+ profile.name,
7155
+ field,
7156
+ firstLine(run2.stderr) || firstLine(run2.stdout)
7157
+ );
7158
+ }
7159
+ return run2;
6835
7160
  }
6836
7161
  async function runHostTemplate(db, host, field, holder, values, opts = {}) {
6837
7162
  const timeoutMs = opts.timeoutMs ?? HOST_COMMAND_TIMEOUT_MS;
@@ -6849,14 +7174,22 @@ async function checkHost(db, host, opts = {}) {
6849
7174
  getHost(db, host);
6850
7175
  const timeoutMs = opts.timeoutMs ?? HOST_COMMAND_TIMEOUT_MS;
6851
7176
  const steps = [];
6852
- const login = (cmd) => `"\${SHELL:-sh}" -ilc ${shellQuote(cmd)}`;
7177
+ let warnings = [];
7178
+ let handedBack = false;
6853
7179
  await withHostChannel(
6854
7180
  db,
6855
7181
  host,
6856
7182
  `check:${process.pid}`,
6857
7183
  async (profile, lease) => {
6858
7184
  const root = opts.root ?? profile.repo ?? void 0;
7185
+ warnings = sessionShellWarnings(profile);
7186
+ const login = (cmd) => profile.transport === "lease" ? cmd : `"\${SHELL:-sh}" -ilc ${shellQuote(cmd)}`;
7187
+ if (profile.transport === "lease") steps.push(...leaseTemplateSteps(profile));
6859
7188
  const step = async (name, cmd, good, unmet) => {
7189
+ if (handedBack) {
7190
+ steps.push({ step: name, ok: null, detail: "skipped: the host handed back" });
7191
+ return false;
7192
+ }
6860
7193
  let detail;
6861
7194
  let ok = false;
6862
7195
  try {
@@ -6874,6 +7207,12 @@ async function checkHost(db, host, opts = {}) {
6874
7207
  ok = good1 !== void 0;
6875
7208
  detail = good1 ?? (r.exitCode === null ? `timed out after ${timeoutMs}ms` : r.exitCode === 0 ? unmet : `exit ${r.exitCode}${why === "" ? "" : `: ${why}`}`);
6876
7209
  } catch (e) {
7210
+ if (e instanceof HostHandbackError) {
7211
+ handedBack = true;
7212
+ steps.push({ step: name, ok: null, detail: "skipped: the host handed back" });
7213
+ steps.push({ step: "handback", ok: false, detail: `${name}: ${e.message}` });
7214
+ return false;
7215
+ }
6877
7216
  if (!(e instanceof HostTemplateError)) throw e;
6878
7217
  detail = e.message;
6879
7218
  }
@@ -6900,10 +7239,19 @@ async function checkHost(db, host, opts = {}) {
6900
7239
  );
6901
7240
  } else {
6902
7241
  for (const s of ["repo", "pi", "extension"]) {
6903
- steps.push({ step: s, ok: null, detail: "skipped: host not reachable" });
7242
+ steps.push({
7243
+ step: s,
7244
+ ok: null,
7245
+ detail: handedBack ? "skipped: the host handed back" : "skipped: host not reachable"
7246
+ });
6904
7247
  }
6905
7248
  }
6906
- if (opts.agent !== void 0) {
7249
+ if (opts.agent !== void 0 && profile.transport === "lease") {
7250
+ steps.push(leaseJobStep(db, profile, opts.agent));
7251
+ }
7252
+ if (opts.agent !== void 0 && handedBack) {
7253
+ steps.push({ step: "ctl", ok: null, detail: "skipped: the host handed back" });
7254
+ } else if (opts.agent !== void 0) {
6907
7255
  const { name, workstream } = opts.agent;
6908
7256
  const p = await ctlProbe(
6909
7257
  ctlSocketPath(workstream, name, dirname4(db.name)),
@@ -6925,11 +7273,62 @@ async function checkHost(db, host, opts = {}) {
6925
7273
  },
6926
7274
  opts
6927
7275
  );
6928
- return { host, ok: steps.every((s) => s.ok !== false), steps };
7276
+ return { host, ok: steps.every((s) => s.ok !== false), steps, warnings };
7277
+ }
7278
+ var DUMMY_JOB = "check-job";
7279
+ function leaseTemplateSteps(profile) {
7280
+ const problems = (() => {
7281
+ try {
7282
+ return leaseShellProblems(profile.shell);
7283
+ } catch (e) {
7284
+ if (e instanceof HostTemplateError) return [e.message];
7285
+ throw e;
7286
+ }
7287
+ })();
7288
+ const shell = problems.length === 0 ? {
7289
+ step: "shell",
7290
+ ok: true,
7291
+ detail: "lease shell has {job_file}, {local_sock} and {remote_sock}"
7292
+ } : { step: "shell", ok: false, detail: problems.join("; ") };
7293
+ let stop;
7294
+ try {
7295
+ if (profile.stop === null) throw new HostTemplateError("a lease profile has no stop template");
7296
+ if (placeholderUses(profile.stop, "stop").job === 0) {
7297
+ throw new HostTemplateError("a stop template must contain {job}");
7298
+ }
7299
+ const line = expandHostTemplate(profile.stop, { job: DUMMY_JOB, cwd: "." }, "stop");
7300
+ stop = { step: "stop", ok: true, detail: `expands (not run) to: ${line}` };
7301
+ } catch (e) {
7302
+ if (!(e instanceof HostTemplateError)) throw e;
7303
+ stop = { step: "stop", ok: false, detail: e.message };
7304
+ }
7305
+ return [shell, stop];
7306
+ }
7307
+ function leaseJobStep(db, profile, agent) {
7308
+ const who = `${agent.workstream}/${agent.name}`;
7309
+ const job = getCarrierJobByName(db, agent.workstream, agent.name);
7310
+ if (job === void 0 || job.host !== profile.name || job.job === null) {
7311
+ return { step: "job", ok: false, detail: `${who}: no job handle recorded on ${profile.name}` };
7312
+ }
7313
+ const drift = job.hostStop === profile.stop ? "" : "; the stop template changed since it was recorded: a close will refuse and name the handle";
7314
+ return { step: "job", ok: true, detail: `${who}: job ${job.job}${drift}` };
7315
+ }
7316
+ function sessionShellWarnings(profile) {
7317
+ if (profile.transport !== "session") return [];
7318
+ const noMaster = /ControlMaster[= ]+no\b/i.test(profile.shell);
7319
+ const noPath = /ControlPath[= ]+none\b/i.test(profile.shell);
7320
+ if (noMaster && noPath) return [];
7321
+ const lacks = [
7322
+ ...noMaster ? [] : ["ControlMaster=no"],
7323
+ ...noPath ? [] : ["ControlPath=none"]
7324
+ ];
7325
+ return [
7326
+ `shell template lacks ${lacks.join(" and ")}: a forward that rides a shared ssh master is bound by the master and outlives the pane (heuristic; see mu host init --ssh)`
7327
+ ];
6929
7328
  }
6930
7329
 
6931
7330
  // src/op-context.ts
6932
- import { randomUUID as randomUUID5 } from "crypto";
7331
+ import { randomUUID as randomUUID6 } from "crypto";
6933
7332
  var DEFAULT_CTX = { group_id: null, actor: null, intent: null, applying: 0 };
6934
7333
  function currentOpContext(db) {
6935
7334
  const row = readCtx(db) ?? DEFAULT_CTX;
@@ -7018,7 +7417,7 @@ function withOpContext(db, ctx, fn) {
7018
7417
  return withCtxRow(
7019
7418
  db,
7020
7419
  (previous) => {
7021
- const groupId = ctx.group === "new" ? randomUUID5() : ctx.group !== void 0 ? ctx.group : previous.group_id ?? randomUUID5();
7420
+ const groupId = ctx.group === "new" ? randomUUID6() : ctx.group !== void 0 ? ctx.group : previous.group_id ?? randomUUID6();
7022
7421
  return {
7023
7422
  group_id: groupId,
7024
7423
  // An explicit undefined inherits; only a provided value overrides.
@@ -7053,15 +7452,15 @@ function reapAgentRow(db, agentId, name, opts = {}) {
7053
7452
  "INSERT INTO task_notes (task_id, author, content, created_at, kind) VALUES (?, 'reaper', ?, ?, 'note')"
7054
7453
  );
7055
7454
  for (const t of stuck) {
7056
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
7057
- reopen.run(now2, t.taskId);
7455
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
7456
+ reopen.run(now3, t.taskId);
7058
7457
  withOpContext(
7059
7458
  db,
7060
7459
  { intent: "task.note", actor: "reaper" },
7061
7460
  () => note.run(
7062
7461
  t.taskId,
7063
7462
  `[reaper] previous owner ${name} gone (agent removed); status reverted IN_PROGRESS \u2192 OPEN, owner cleared`,
7064
- now2
7463
+ now3
7065
7464
  )
7066
7465
  );
7067
7466
  }
@@ -7216,7 +7615,10 @@ function withTaskFence(db, workstream, id, expected, fn, previewCommand) {
7216
7615
  }).immediate();
7217
7616
  }
7218
7617
  function agentRevision(db, workstream, name, recoveryId) {
7219
- if (recoveryId !== void 0) return recoveryRevision(db, workstream, name, recoveryId);
7618
+ const carrier = carrierJobsOf(db, workstream, name);
7619
+ if (recoveryId !== void 0) {
7620
+ return recoveryRevision(db, workstream, name, carrier, recoveryId);
7621
+ }
7220
7622
  const agent = db.prepare(
7221
7623
  `SELECT a.id, a.pane_id, a.cli, a.role, a.tab, a.cwd, a.base, a.command, a.command_source,
7222
7624
  a.spawner, a.host, a.collected_head, a.repo, a.gate_token, a.created_at
@@ -7232,19 +7634,36 @@ function agentRevision(db, workstream, name, recoveryId) {
7232
7634
  `SELECT path, root, base, last_collected_head, created_at FROM worktrees
7233
7635
  WHERE workstream = ? AND agent = ? ORDER BY path`
7234
7636
  ).all(workstream, name);
7235
- if (agent === void 0 && worktrees.length === 0) return recoveryRevision(db, workstream, name);
7236
- const digest = createHash3("sha256").update(JSON.stringify([agent ?? null, owned, worktrees])).digest();
7637
+ if (agent === void 0 && worktrees.length === 0) {
7638
+ return recoveryRevision(db, workstream, name, carrier);
7639
+ }
7640
+ const state = [agent ?? null, owned, worktrees];
7641
+ if (carrier.length > 0) state.push(carrier);
7642
+ const digest = createHash3("sha256").update(JSON.stringify(state)).digest();
7237
7643
  return digest.readUIntBE(0, 6);
7238
7644
  }
7239
- function recoveryRevision(db, workstream, name, recoveryId) {
7645
+ function carrierJobsOf(db, workstream, name) {
7646
+ return db.prepare(
7647
+ `SELECT token, host, host_stop, launch, agent_id, pane_id, job, reason FROM carrier_jobs
7648
+ WHERE workstream = ? AND name = ? ORDER BY id`
7649
+ ).all(workstream, name);
7650
+ }
7651
+ function recoveryRevision(db, workstream, name, carrier, recoveryId) {
7240
7652
  const rows = db.prepare(
7241
7653
  `SELECT * FROM remote_recovery WHERE workstream = ? AND name = ?${recoveryId === void 0 ? "" : " AND agent_id = ?"} ORDER BY agent_id`
7242
7654
  ).all(...recoveryId === void 0 ? [workstream, name] : [workstream, name, recoveryId]);
7243
- if (rows.length === 0) return 0;
7655
+ if (rows.length === 0) {
7656
+ if (carrier.length === 0 || recoveryId !== void 0) return 0;
7657
+ return createHash3("sha256").update(JSON.stringify(["lease-job", carrier])).digest().readUIntBE(0, 6);
7658
+ }
7244
7659
  const live2 = db.prepare(
7245
7660
  "SELECT a.id FROM agents a JOIN workstreams w ON w.id = a.workstream_id WHERE w.name = ? AND a.name = ?"
7246
7661
  ).get(workstream, name);
7247
- const digest = createHash3("sha256").update(JSON.stringify(["recovery", rows, live2?.id ?? null])).digest();
7662
+ const digest = createHash3("sha256").update(
7663
+ JSON.stringify(
7664
+ carrier.length > 0 ? ["recovery", rows, live2?.id ?? null, carrier] : ["recovery", rows, live2?.id ?? null]
7665
+ )
7666
+ ).digest();
7248
7667
  return digest.readUIntBE(0, 6);
7249
7668
  }
7250
7669
 
@@ -7526,8 +7945,8 @@ function taskIdFor(db, localId, workstream) {
7526
7945
  const row = db.prepare("SELECT id FROM tasks WHERE workstream_id = ? AND local_id = ?").get(wsId, localId);
7527
7946
  return row ? row.id : null;
7528
7947
  }
7529
- function touchTask(db, taskId, now2) {
7530
- const ts = now2 ?? (/* @__PURE__ */ new Date()).toISOString();
7948
+ function touchTask(db, taskId, now3) {
7949
+ const ts = now3 ?? (/* @__PURE__ */ new Date()).toISOString();
7531
7950
  db.prepare("UPDATE tasks SET updated_at = ? WHERE id = ? AND updated_at < ?").run(ts, taskId, ts);
7532
7951
  }
7533
7952
 
@@ -7913,13 +8332,13 @@ function addBlockEdgeImpl(db, workstream, blocked, blocker) {
7913
8332
  if (wouldCreateCycle(db, blockerId, blockedId)) {
7914
8333
  throw cycleErrorFor(db, blocker, blocked, blockerId, blockedId, blockedRow.workstreamName);
7915
8334
  }
7916
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
8335
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
7917
8336
  const added = db.transaction(() => {
7918
8337
  const result = db.prepare(
7919
8338
  "INSERT INTO task_edges (from_task_id, to_task_id, created_at) SELECT ?, ?, ? WHERE NOT EXISTS (SELECT 1 FROM task_edges WHERE from_task_id = ? AND to_task_id = ?)"
7920
- ).run(blockerId, blockedId, now2, blockerId, blockedId);
8339
+ ).run(blockerId, blockedId, now3, blockerId, blockedId);
7921
8340
  if (result.changes > 0) {
7922
- touchTask(db, blockedId, now2);
8341
+ touchTask(db, blockedId, now3);
7923
8342
  return true;
7924
8343
  }
7925
8344
  return false;
@@ -8005,11 +8424,11 @@ function reparentTaskImpl(db, taskLocalId, blockers, scope) {
8005
8424
  const insertEdge = db.prepare(
8006
8425
  "INSERT INTO task_edges (from_task_id, to_task_id, created_at) VALUES (?, ?, ?)"
8007
8426
  );
8008
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
8427
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
8009
8428
  for (const blockerId of blockerIds) {
8010
- insertEdge.run(blockerId, taskSurrogateId, now2);
8429
+ insertEdge.run(blockerId, taskSurrogateId, now3);
8011
8430
  }
8012
- touchTask(db, taskSurrogateId, now2);
8431
+ touchTask(db, taskSurrogateId, now3);
8013
8432
  return { removedEdges: removed.changes, addedEdges: blockerIds.length };
8014
8433
  }).immediate();
8015
8434
  }
@@ -8921,9 +9340,9 @@ async function teardownWorkstream(db, opts) {
8921
9340
  db,
8922
9341
  { intent: "workstream.teardown", group: "new" },
8923
9342
  () => db.transaction(() => {
8924
- const now2 = workstreamRevision(db, opts.workstream);
8925
- if (now2 !== opts.ifRevision) {
8926
- throw new WorkstreamRevisionMismatchError(opts.workstream, opts.ifRevision, now2);
9343
+ const now3 = workstreamRevision(db, opts.workstream);
9344
+ if (now3 !== opts.ifRevision) {
9345
+ throw new WorkstreamRevisionMismatchError(opts.workstream, opts.ifRevision, now3);
8927
9346
  }
8928
9347
  for (const a of dead) {
8929
9348
  if (!sameRow2.get(a.incarnation, a.name, a.paneId, opts.workstream)) continue;
@@ -9132,7 +9551,7 @@ function addTaskImpl(db, opts) {
9132
9551
  if (existing) {
9133
9552
  throw new TaskExistsError(opts.localId);
9134
9553
  }
9135
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
9554
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
9136
9555
  const insertResult = db.prepare(
9137
9556
  `INSERT INTO tasks (workstream_id, local_id, title, status, substate, kind, wave, impact, effort_days, scored, created_at, updated_at)
9138
9557
  VALUES (?, ?, ?, 'OPEN', ?, ?, ?, ?, ?, ?, ?, ?)`
@@ -9146,8 +9565,8 @@ function addTaskImpl(db, opts) {
9146
9565
  opts.impact ?? DEFAULT_IMPACT,
9147
9566
  opts.effortDays ?? DEFAULT_EFFORT_DAYS,
9148
9567
  opts.impact !== void 0 || opts.effortDays !== void 0 ? 1 : 0,
9149
- now2,
9150
- now2
9568
+ now3,
9569
+ now3
9151
9570
  );
9152
9571
  const newTaskId = Number(insertResult.lastInsertRowid);
9153
9572
  if (opts.blockedBy && opts.blockedBy.length > 0) {
@@ -9171,7 +9590,7 @@ function addTaskImpl(db, opts) {
9171
9590
  "INSERT INTO task_edges (from_task_id, to_task_id, created_at) VALUES (?, ?, ?)"
9172
9591
  );
9173
9592
  for (const blocker of canonicalBlockers) {
9174
- insertEdge.run(blocker.id, newTaskId, now2);
9593
+ insertEdge.run(blocker.id, newTaskId, now3);
9175
9594
  }
9176
9595
  }
9177
9596
  const row = getTask(db, opts.localId, opts.workstream);
@@ -9226,7 +9645,7 @@ function addBlocksEdges(db, input, blocks, workstream) {
9226
9645
  }
9227
9646
  umbrellas.set(row.id, local);
9228
9647
  }
9229
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
9648
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
9230
9649
  const insertEdge = db.prepare(
9231
9650
  "INSERT INTO task_edges (from_task_id, to_task_id, created_at) VALUES (?, ?, ?)"
9232
9651
  );
@@ -9237,10 +9656,10 @@ function addBlocksEdges(db, input, blocks, workstream) {
9237
9656
  if (wouldCreateCycle(db, newId, umbrellaId)) {
9238
9657
  throw cycleErrorFor(db, t.id, umbrella, newId, umbrellaId, workstream);
9239
9658
  }
9240
- insertEdge.run(newId, umbrellaId, now2);
9659
+ insertEdge.run(newId, umbrellaId, now3);
9241
9660
  }
9242
9661
  }
9243
- for (const umbrellaId of umbrellas.keys()) touchTask(db, umbrellaId, now2);
9662
+ for (const umbrellaId of umbrellas.keys()) touchTask(db, umbrellaId, now3);
9244
9663
  }
9245
9664
  function lookupEdgeTask(db, wsId, localId, workstream) {
9246
9665
  const row = db.prepare(
@@ -9273,18 +9692,18 @@ function insertNote(db, taskLocalId, content, opts, touch = true) {
9273
9692
  }
9274
9693
  const taskId = taskIdFor(db, task.name, task.workstreamName);
9275
9694
  if (taskId === null) throw new TaskNotFoundError(taskLocalId, task.workstreamName);
9276
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
9695
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
9277
9696
  db.transaction(() => {
9278
9697
  const r = db.prepare(
9279
9698
  "INSERT INTO task_notes (task_id, author, content, created_at, kind) VALUES (?, ?, ?, ?, ?)"
9280
- ).run(taskId, opts.author ?? null, content, now2, opts.kind ?? "note");
9281
- if (touch) touchTask(db, taskId, now2);
9699
+ ).run(taskId, opts.author ?? null, content, now3, opts.kind ?? "note");
9700
+ if (touch) touchTask(db, taskId, now3);
9282
9701
  return r;
9283
9702
  }).immediate();
9284
9703
  return {
9285
9704
  author: opts.author ?? null,
9286
9705
  content,
9287
- createdAt: now2,
9706
+ createdAt: now3,
9288
9707
  kind: opts.kind ?? "note"
9289
9708
  };
9290
9709
  }
@@ -9752,7 +10171,7 @@ async function claimTaskImpl(db, localId, opts) {
9752
10171
  throw new ClaimerNotRegisteredError(agentName, null);
9753
10172
  }
9754
10173
  }
9755
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
10174
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
9756
10175
  let result;
9757
10176
  try {
9758
10177
  result = db.prepare(
@@ -9766,7 +10185,7 @@ async function claimTaskImpl(db, localId, opts) {
9766
10185
  WHERE local_id = ?
9767
10186
  AND workstream_id = (SELECT id FROM workstreams WHERE name = ?)
9768
10187
  AND (owner_id IS NULL OR owner_id = ?)`
9769
- ).run(claimerRow.id, now2, localId, opts.workstream, claimerRow.id);
10188
+ ).run(claimerRow.id, now3, localId, opts.workstream, claimerRow.id);
9770
10189
  } catch (err) {
9771
10190
  if (err instanceof Error && err.message === GUARD_ALREADY_OWNED) {
9772
10191
  throw new TaskAlreadyOwnedError(localId, before.ownerName ?? "<unknown>");
@@ -9822,7 +10241,7 @@ async function claimSelf(db, localId, opts) {
9822
10241
  if (!before) throw new TaskNotFoundError(localId, opts.workstream);
9823
10242
  assertTaskRevision(db, before.workstreamName, localId, opts.ifRevision);
9824
10243
  assertNotParked(before, opts);
9825
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
10244
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
9826
10245
  const result = db.prepare(
9827
10246
  `UPDATE tasks
9828
10247
  SET status = CASE WHEN status = 'OPEN' THEN 'IN_PROGRESS' ELSE status END,
@@ -9831,7 +10250,7 @@ async function claimSelf(db, localId, opts) {
9831
10250
  WHERE local_id = ?
9832
10251
  AND workstream_id = (SELECT id FROM workstreams WHERE name = ?)
9833
10252
  AND owner_id IS NULL`
9834
- ).run(now2, localId, before.workstreamName);
10253
+ ).run(now3, localId, before.workstreamName);
9835
10254
  if (result.changes === 0) {
9836
10255
  throw new TaskAlreadyOwnedError(localId, before.ownerName ?? "<unknown>");
9837
10256
  }
@@ -10296,7 +10715,8 @@ function remoteShellCommand(profile, a) {
10296
10715
  cmd: a.command,
10297
10716
  local_sock: a.localSock,
10298
10717
  remote_sock: remoteSock,
10299
- cwd: a.cwd
10718
+ cwd: a.cwd,
10719
+ ...a.jobFile !== void 0 ? { job_file: a.jobFile } : {}
10300
10720
  });
10301
10721
  const unset = LOCAL_ONLY_ENV.map((k) => `-u ${k}`).join(" ");
10302
10722
  return `exec env ${unset} /bin/sh -c ${shellQuote(filled)}`;
@@ -10694,16 +11114,16 @@ function registerOwnedWait(db, caller, refs) {
10694
11114
  for (const r of add) if (r.kind === "agent") unmark.run(caller, doneKey(r.workstream, r.id));
10695
11115
  const baselines = { ...old?.baselines ?? {} };
10696
11116
  for (const [k, b] of incoming) baselines[k] = mergeBaseline(baselines[k], b);
10697
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
11117
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
10698
11118
  const baselinesJson = JSON.stringify(baselines);
10699
11119
  if (row === void 0) {
10700
11120
  db.prepare(
10701
11121
  "INSERT INTO wait_registrations (caller, refs_json, state, generation, updated_at, baselines_json) VALUES (?, ?, 'armed', 0, ?, ?)"
10702
- ).run(caller, JSON.stringify(merged), now2, baselinesJson);
11122
+ ).run(caller, JSON.stringify(merged), now3, baselinesJson);
10703
11123
  } else if (merged.length !== have.length || baselinesJson !== row.baselines_json) {
10704
11124
  db.prepare(
10705
11125
  "UPDATE wait_registrations SET refs_json = ?, baselines_json = ?, updated_at = ? WHERE caller = ?"
10706
- ).run(JSON.stringify(merged), baselinesJson, now2, caller);
11126
+ ).run(JSON.stringify(merged), baselinesJson, now3, caller);
10707
11127
  }
10708
11128
  const after = select(db, caller);
10709
11129
  if (after === void 0) throw new WaitRegistrationError("wait registration vanished");
@@ -10911,8 +11331,8 @@ async function closeRecovery(db, rec, opts) {
10911
11331
  }
10912
11332
 
10913
11333
  // src/agents/gate.ts
10914
- import { randomUUID as randomUUID6 } from "crypto";
10915
- var newGateToken = () => randomUUID6();
11334
+ import { randomUUID as randomUUID7 } from "crypto";
11335
+ var newGateToken = () => randomUUID7();
10916
11336
  var TOKEN_RE = /^[0-9a-f-]{36}$/;
10917
11337
  var DIR = `"$HOME"/.local/state/mu-next/remote-gates`;
10918
11338
  var sh = (body) => `sh -c ${shellQuote(body)}`;
@@ -10983,6 +11403,7 @@ async function readGate(db, agent) {
10983
11403
  { paneAlive: localPaneAlive }
10984
11404
  );
10985
11405
  } catch (e) {
11406
+ if (e instanceof HostHandbackError) throw e;
10986
11407
  return unknown2(`result read: ${e instanceof Error ? e.message : String(e)}`);
10987
11408
  }
10988
11409
  if (!sameRow(db, agent)) return { kind: "dead", reason: "the agent was replaced" };
@@ -11016,6 +11437,71 @@ async function cleanGateResult(db, agent) {
11016
11437
  }
11017
11438
  }
11018
11439
 
11440
+ // src/agents/lease-carrier.ts
11441
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync2, rmSync as rmSync3 } from "fs";
11442
+ import { dirname as dirname9 } from "path";
11443
+ import { setTimeout as delay } from "timers/promises";
11444
+
11445
+ // src/carrier-stop.ts
11446
+ var CarrierStopRefusedError = class extends Error {
11447
+ constructor(host, handle, why) {
11448
+ super(say("carrier.stop_refused", host, handle, why));
11449
+ this.host = host;
11450
+ this.handle = handle;
11451
+ }
11452
+ host;
11453
+ handle;
11454
+ name = "CarrierStopRefusedError";
11455
+ };
11456
+ var CarrierStopFailedError = class extends Error {
11457
+ constructor(host, handle, detail) {
11458
+ super(say("carrier.stop_failed", host, handle, detail));
11459
+ this.host = host;
11460
+ this.handle = handle;
11461
+ this.detail = detail;
11462
+ }
11463
+ host;
11464
+ handle;
11465
+ detail;
11466
+ name = "CarrierStopFailedError";
11467
+ };
11468
+ var firstLine3 = (s) => s.split("\n").find((l) => l.trim() !== "")?.trim() ?? "";
11469
+ async function runStop(db, host, handle, hostStop, opts = {}) {
11470
+ if (!JOB_HANDLE_RE.test(handle)) throw new CarrierStopRefusedError(host, handle, "bad-handle");
11471
+ let current;
11472
+ try {
11473
+ current = getHost(db, host).stop;
11474
+ } catch (e) {
11475
+ if (e instanceof HostNotFoundError) throw new CarrierStopRefusedError(host, handle, "no-host");
11476
+ throw e;
11477
+ }
11478
+ if (current !== hostStop) throw new CarrierStopRefusedError(host, handle, "stop-changed");
11479
+ const run2 = await withHostChannel(
11480
+ db,
11481
+ host,
11482
+ `stop:${handle}`,
11483
+ (profile, lease) => {
11484
+ if (profile.stop !== hostStop)
11485
+ throw new CarrierStopRefusedError(host, handle, "stop-changed");
11486
+ return runLeasedTemplate(
11487
+ db,
11488
+ lease,
11489
+ profile,
11490
+ "stop",
11491
+ { job: handle, cwd: "." },
11492
+ opts.timeoutMs ?? HOST_COMMAND_TIMEOUT_MS
11493
+ );
11494
+ },
11495
+ opts
11496
+ );
11497
+ if (run2.exitCode === 0) return;
11498
+ throw new CarrierStopFailedError(
11499
+ host,
11500
+ handle,
11501
+ run2.exitCode === null ? "timed out" : `exit ${run2.exitCode}${firstLine3(run2.stderr) || firstLine3(run2.stdout) ? `: ${firstLine3(run2.stderr) || firstLine3(run2.stdout)}` : ""}`
11502
+ );
11503
+ }
11504
+
11019
11505
  // src/agents/lock.ts
11020
11506
  import { createHash as createHash4 } from "crypto";
11021
11507
  import { dirname as dirname8, join as join7 } from "path";
@@ -11217,7 +11703,312 @@ async function withSpawnAttemptLock(db, taskId, attemptKey, fn, onBusy) {
11217
11703
  );
11218
11704
  }
11219
11705
 
11706
+ // src/agents/lease-carrier.ts
11707
+ var LeaseJobTimeoutError = class extends Error {
11708
+ constructor(agentName, host, budgetMs, jobFile) {
11709
+ super(say("carrier.job_timeout", agentName, host, budgetMs, jobFile));
11710
+ this.agentName = agentName;
11711
+ this.host = host;
11712
+ this.budgetMs = budgetMs;
11713
+ this.jobFile = jobFile;
11714
+ }
11715
+ agentName;
11716
+ host;
11717
+ budgetMs;
11718
+ jobFile;
11719
+ name = "LeaseJobTimeoutError";
11720
+ };
11721
+ var LeaseJobInvalidError = class extends Error {
11722
+ constructor(agentName, jobFile, line) {
11723
+ super(say("carrier.job_invalid", agentName, jobFile, line));
11724
+ this.agentName = agentName;
11725
+ this.jobFile = jobFile;
11726
+ this.line = line;
11727
+ }
11728
+ agentName;
11729
+ jobFile;
11730
+ line;
11731
+ name = "LeaseJobInvalidError";
11732
+ };
11733
+ var LeaseJobUnreadableError = class extends Error {
11734
+ constructor(agentName, jobFile, detail) {
11735
+ super(say("carrier.job_unreadable", agentName, jobFile, detail));
11736
+ this.agentName = agentName;
11737
+ this.jobFile = jobFile;
11738
+ this.detail = detail;
11739
+ }
11740
+ agentName;
11741
+ jobFile;
11742
+ detail;
11743
+ name = "LeaseJobUnreadableError";
11744
+ };
11745
+ var DEFAULT_SPAWN_JOB_MS = 3e4;
11746
+ var realClock = { now: () => Date.now(), delay: (ms) => delay(ms) };
11747
+ var clock = realClock;
11748
+ var JOB_STAT_INTERVAL_MS = 50;
11749
+ var JOB_PANE_INTERVAL_MS = 250;
11750
+ function defaultSpawnJobMs() {
11751
+ const parsed = Number.parseInt(process.env.MU_SPAWN_JOB_MS ?? "", 10);
11752
+ return Number.isNaN(parsed) || parsed <= 0 ? DEFAULT_SPAWN_JOB_MS : parsed;
11753
+ }
11754
+ function leaseJobFile(db, workstream, name) {
11755
+ return carrierJobFilePath(workstream, name, dirname9(db.name));
11756
+ }
11757
+ function readJobFile(path) {
11758
+ let text;
11759
+ try {
11760
+ text = readFileSync3(path, "utf8");
11761
+ } catch (e) {
11762
+ const code = e.code;
11763
+ if (code === "ENOENT") return { kind: "none" };
11764
+ return { kind: "unreadable", detail: code ?? (e instanceof Error ? e.message : String(e)) };
11765
+ }
11766
+ const line = text.split("\n")[0]?.trim() ?? "";
11767
+ return JOB_HANDLE_RE.test(line) ? { kind: "handle", job: line } : { kind: "invalid", line };
11768
+ }
11769
+ var describeRead = (r) => r.kind === "handle" ? `handle ${r.job}` : r.kind === "invalid" ? `${JSON.stringify(r.line.slice(0, 80))}, not a handle` : r.kind === "unreadable" ? `unreadable: ${r.detail}` : "absent";
11770
+ function refuseReserved(db, workstream, name, own) {
11771
+ const r = getCarrierJobByName(db, workstream, name);
11772
+ if (r !== void 0 && r.token !== own) throw new CarrierJobUnstoppedError(name, r);
11773
+ }
11774
+ function assertLeaseSpawnable(profile, isGate) {
11775
+ if (profile.transport === "lease" && isGate) {
11776
+ throw new HostProfileError(say("carrier.lease_gate", profile.name));
11777
+ }
11778
+ }
11779
+ async function reserveLeaseJob(db, workstream, name, profile) {
11780
+ return withAgentLock(db, workstream, name, async () => {
11781
+ if (getAgent(db, name, workstream) !== void 0) throw new AgentExistsError(name);
11782
+ refuseReserved(db, workstream, name);
11783
+ const r = reserveCarrierJob(db, {
11784
+ workstream,
11785
+ name,
11786
+ host: profile.name,
11787
+ hostStop: profile.stop ?? "",
11788
+ creatorPid: process.pid,
11789
+ creatorStart: pidStart(process.pid)
11790
+ });
11791
+ if (r !== void 0) return r;
11792
+ refuseReserved(db, workstream, name);
11793
+ throw new Error(`carrier_jobs: reservation of ${workstream}/${name} not inserted`);
11794
+ });
11795
+ }
11796
+ function claimLeaseJobFile(db, workstream, name) {
11797
+ const path = leaseJobFile(db, workstream, name);
11798
+ mkdirSync3(dirname9(path), { recursive: true, mode: 448 });
11799
+ const read = readJobFile(path);
11800
+ if (read.kind === "none") return path;
11801
+ const aside = `${path}.stale-${Date.now()}`;
11802
+ renameSync2(path, aside);
11803
+ try {
11804
+ emitEvent(
11805
+ db,
11806
+ workstream,
11807
+ "agent.spawn",
11808
+ `agent spawn ${name}: a job file of an earlier reservation (${describeRead(read)}) moved to ${aside}`
11809
+ );
11810
+ } catch {
11811
+ }
11812
+ return path;
11813
+ }
11814
+ async function awaitLeaseJob(db, a) {
11815
+ const budget = defaultSpawnJobMs();
11816
+ const deadline = clock.now() + budget;
11817
+ let lastPaneCheck = Number.NEGATIVE_INFINITY;
11818
+ let scrollback;
11819
+ for (; ; ) {
11820
+ a.signal?.throwIfAborted();
11821
+ const read = readJobFile(a.jobFile);
11822
+ if (read.kind === "invalid") throw new LeaseJobInvalidError(a.name, a.jobFile, read.line);
11823
+ if (read.kind === "unreadable") {
11824
+ throw new LeaseJobUnreadableError(a.name, a.jobFile, read.detail);
11825
+ }
11826
+ if (read.kind === "handle") {
11827
+ if (!fillCarrierJob(db, a.token, read.job)) {
11828
+ throw new Error(say("carrier.job_unrecorded", a.name, read.job));
11829
+ }
11830
+ return read.job;
11831
+ }
11832
+ const atDeadline = clock.now() >= deadline;
11833
+ if (atDeadline || clock.now() - lastPaneCheck >= JOB_PANE_INTERVAL_MS) {
11834
+ lastPaneCheck = clock.now();
11835
+ const mux = await activeMux();
11836
+ scrollback = await mux.capture(a.paneId, { lines: 50 }).catch(() => void 0) ?? scrollback;
11837
+ if (!await mux.paneAlive(a.paneId) && readJobFile(a.jobFile).kind === "none") {
11838
+ throw new AgentDiedOnSpawnError(
11839
+ a.name,
11840
+ a.paneId,
11841
+ scrollback,
11842
+ a.command,
11843
+ `before writing its job handle (up to ${budget}ms after spawn)`,
11844
+ a.profile.name
11845
+ );
11846
+ }
11847
+ }
11848
+ if (atDeadline) throw new LeaseJobTimeoutError(a.name, a.profile.name, budget, a.jobFile);
11849
+ await clock.delay(Math.min(JOB_STAT_INTERVAL_MS, Math.max(1, deadline - clock.now())));
11850
+ }
11851
+ }
11852
+ async function rollbackLeaseJob(db, a, paneGone2) {
11853
+ const jobFile = leaseJobFile(db, a.workstream, a.name);
11854
+ const kept = (host, handle, why) => {
11855
+ const line = handle === null ? say("carrier.rollback_reserved", a.name, a.workstream, host, why) : say("carrier.rollback_kept", a.name, a.workstream, host, handle, why);
11856
+ try {
11857
+ emitEvent(db, a.workstream, "agent.spawn_failed", `agent spawn rollback ${a.name}: ${line}`);
11858
+ } catch {
11859
+ }
11860
+ if (!isJsonMode()) process.stderr.write(`${line}
11861
+ `);
11862
+ return "kept";
11863
+ };
11864
+ try {
11865
+ let job = getCarrierJob(db, a.token);
11866
+ if (job === void 0) return kept("?", null, "its reservation is gone");
11867
+ if (job.job === null) {
11868
+ if (job.launch === "none") return "none";
11869
+ const read = readJobFile(jobFile);
11870
+ if (read.kind === "none") {
11871
+ return kept(job.host, null, `no job handle in ${jobFile}; the carrier may still write one`);
11872
+ }
11873
+ if (read.kind !== "handle") {
11874
+ const reason = `the job file ${jobFile} is ${describeRead(read)}`;
11875
+ noteCarrierJob(db, job.token, reason);
11876
+ return kept(job.host, null, reason);
11877
+ }
11878
+ if (!fillCarrierJob(db, job.token, read.job)) {
11879
+ return kept(job.host, read.job, "the reservation holds another handle");
11880
+ }
11881
+ job = getCarrierJob(db, a.token);
11882
+ if (job?.job == null) return kept("?", read.job, "its reservation is gone");
11883
+ }
11884
+ const handle = job.job;
11885
+ if (!paneGone2 || job.paneId === null) {
11886
+ return kept(job.host, handle, "its pane is not proven gone");
11887
+ }
11888
+ try {
11889
+ await runStop(db, job.host, handle, job.hostStop);
11890
+ } catch (e) {
11891
+ return kept(job.host, handle, `stop failed: ${e instanceof Error ? e.message : String(e)}`);
11892
+ }
11893
+ const now3 = readJobFile(jobFile);
11894
+ if (now3.kind === "handle" && now3.job === handle) rmSync3(jobFile, { force: true });
11895
+ else if (now3.kind !== "none") {
11896
+ return kept(job.host, handle, `stopped, but the job file ${jobFile} is ${describeRead(now3)}`);
11897
+ }
11898
+ return "stopped";
11899
+ } catch (e) {
11900
+ return kept("?", null, e instanceof Error ? e.message : String(e));
11901
+ }
11902
+ }
11903
+ function releaseLeaseReservation(db, token, outcome) {
11904
+ try {
11905
+ const r = getCarrierJob(db, token);
11906
+ if (r === void 0) return;
11907
+ const ok = outcome === "stopped" ? r.job !== null : outcome === "none" && r.launch === "none" && r.job === null;
11908
+ if (ok) releaseCarrierJob(db, r);
11909
+ } catch {
11910
+ }
11911
+ }
11912
+ async function describeEmptyReservation(db, r) {
11913
+ const jobFile = leaseJobFile(db, r.workstream, r.name);
11914
+ const creator = r.creatorPid === null ? "unknown" : pidProvenGone(r.creatorPid, r.creatorStart) ? "gone" : "not proven gone";
11915
+ let pane = "none";
11916
+ if (r.paneId !== null) {
11917
+ const alive = await activeMux().then((m) => m.paneAlive(r.paneId ?? "")).catch(() => void 0);
11918
+ pane = `${r.paneId} (${alive === void 0 ? "unknown" : alive ? "alive" : "dead"})`;
11919
+ }
11920
+ const facts = `launch ${r.launch}, pane ${pane}, creator pid ${r.creatorPid ?? "?"} ${creator}, job file ${describeRead(readJobFile(jobFile))}${r.reason === null ? "" : `, ${r.reason}`}`;
11921
+ const unlaunched = r.launch === "none" && r.paneId === null && r.reason === null && creator === "gone";
11922
+ return { facts, unlaunched };
11923
+ }
11924
+ function emitAbandoned(db, r, facts) {
11925
+ emitEvent(
11926
+ db,
11927
+ r.workstream,
11928
+ "agent.close",
11929
+ say(
11930
+ "carrier.reservation_abandoned",
11931
+ r.name,
11932
+ r.host,
11933
+ leaseJobFile(db, r.workstream, r.name),
11934
+ facts
11935
+ )
11936
+ );
11937
+ }
11938
+ async function stopLeaseReservation(db, reservation, opts = {}) {
11939
+ let r = getCarrierJob(db, reservation.token);
11940
+ if (r === void 0) return void 0;
11941
+ const ws = r.workstream;
11942
+ if (opts.abandon === true) {
11943
+ const facts = r.job !== null ? `job ${r.job}${r.reason === null ? "" : `, ${r.reason}`}` : (await describeEmptyReservation(db, r)).facts;
11944
+ if (!abandonCarrierJob(db, r)) {
11945
+ throw new CarrierJobKeptError(r.name, ws, r.host, "the reservation changed during the close");
11946
+ }
11947
+ emitAbandoned(db, r, facts);
11948
+ return { reservation: r, action: "abandon", facts };
11949
+ }
11950
+ if (r.job === null) {
11951
+ const jobFile = leaseJobFile(db, ws, r.name);
11952
+ const read = r.launch === "intent" ? readJobFile(jobFile) : { kind: "none" };
11953
+ if (read.kind === "handle") {
11954
+ if (!fillCarrierJob(db, r.token, read.job)) {
11955
+ throw new CarrierJobUnresolvedError(r.name, ws, r.host, `the reservation changed`);
11956
+ }
11957
+ r = getCarrierJob(db, r.token);
11958
+ if (r?.job == null) return void 0;
11959
+ } else if (read.kind !== "none") {
11960
+ const why = `the job file ${jobFile} is ${describeRead(read)}`;
11961
+ noteCarrierJob(db, r.token, why);
11962
+ throw new CarrierJobUnresolvedError(r.name, ws, r.host, why);
11963
+ } else {
11964
+ const { facts, unlaunched } = await describeEmptyReservation(db, r);
11965
+ if (!unlaunched) {
11966
+ const forTree = r.launch === "none" && r.paneId === null && r.reason !== null;
11967
+ throw new CarrierJobUnresolvedError(r.name, ws, r.host, facts, forTree);
11968
+ }
11969
+ return { reservation: r, action: "unlaunched", facts };
11970
+ }
11971
+ }
11972
+ const handle = r.job;
11973
+ await runStop(db, r.host, handle, r.hostStop);
11974
+ return { reservation: r, action: "stopped", facts: `job ${handle}` };
11975
+ }
11976
+ function cleanLeaseJobFile(db, close) {
11977
+ if (close.action !== "stopped") return;
11978
+ const r = close.reservation;
11979
+ const jobFile = leaseJobFile(db, r.workstream, r.name);
11980
+ const now3 = readJobFile(jobFile);
11981
+ if (now3.kind === "none") return;
11982
+ if (now3.kind === "handle" && now3.job === r.job) {
11983
+ rmSync3(jobFile, { force: true });
11984
+ return;
11985
+ }
11986
+ throw new CarrierJobKeptError(
11987
+ r.name,
11988
+ r.workstream,
11989
+ r.host,
11990
+ `job ${r.job} is stopped, but the job file ${jobFile} is ${describeRead(now3)}`
11991
+ );
11992
+ }
11993
+ function releaseLeaseClose(db, close) {
11994
+ if (close.action === "abandon") return;
11995
+ const r = close.reservation;
11996
+ if (!releaseCarrierJob(db, r)) {
11997
+ if (getCarrierJob(db, r.token) === void 0) return;
11998
+ throw new CarrierJobKeptError(
11999
+ r.name,
12000
+ r.workstream,
12001
+ r.host,
12002
+ "the reservation changed during the close (a job handle landed)"
12003
+ );
12004
+ }
12005
+ }
12006
+
11220
12007
  // src/agents/close.ts
12008
+ function carrierJobOf(db, workstream, name) {
12009
+ const r = getCarrierJobByName(db, workstream, name);
12010
+ return r === void 0 ? null : { host: r.host, job: r.job, launch: r.launch, paneId: r.paneId, reason: r.reason };
12011
+ }
11221
12012
  function remoteOf(agent) {
11222
12013
  if (agent?.host == null || agent.cwd === null || agent.base === null) return null;
11223
12014
  return {
@@ -11242,17 +12033,18 @@ function worktreeOf(db, workstream, name, agent) {
11242
12033
  }
11243
12034
  async function previewClose(db, name, opts) {
11244
12035
  const discard = opts.discard === true;
12036
+ const abandonJob = opts.abandonJob === true;
11245
12037
  for (let attempt = 0; attempt < 3; attempt++) {
11246
12038
  const agent = getAgentForCleanup(db, name, opts.workstream);
11247
12039
  const row = worktreeOf(db, opts.workstream, name, agent);
11248
12040
  const state = row === void 0 ? void 0 : await worktreeState(row.path, row.base, row.root);
11249
12041
  const preview = db.transaction(() => {
11250
- const now2 = getAgentForCleanup(db, name, opts.workstream);
11251
- const nowRow = worktreeOf(db, opts.workstream, name, now2);
11252
- if (now2?.incarnation !== agent?.incarnation || now2?.paneId !== agent?.paneId || nowRow?.path !== row?.path) {
12042
+ const now3 = getAgentForCleanup(db, name, opts.workstream);
12043
+ const nowRow = worktreeOf(db, opts.workstream, name, now3);
12044
+ if (now3?.incarnation !== agent?.incarnation || now3?.paneId !== agent?.paneId || nowRow?.path !== row?.path) {
11253
12045
  return void 0;
11254
12046
  }
11255
- const rec = opts.recoveryId !== void 0 || now2 === void 0 && nowRow === void 0 ? selectRemoteRecovery(db, opts.workstream, name, opts.recoveryId) : void 0;
12047
+ const rec = opts.recoveryId !== void 0 || now3 === void 0 && nowRow === void 0 ? selectRemoteRecovery(db, opts.workstream, name, opts.recoveryId) : void 0;
11256
12048
  if (rec === void 0 && opts.recoveryId !== void 0) {
11257
12049
  return {
11258
12050
  agentName: name,
@@ -11265,11 +12057,14 @@ async function previewClose(db, name, opts) {
11265
12057
  worktree: null,
11266
12058
  remote: null,
11267
12059
  recovery: null,
12060
+ carrierJob: null,
11268
12061
  refusesDirty: false,
11269
12062
  discard,
11270
- keepBranch: opts.keepBranch === true
12063
+ keepBranch: opts.keepBranch === true,
12064
+ abandonJob
11271
12065
  };
11272
12066
  }
12067
+ const carrierJob = carrierJobOf(db, opts.workstream, name);
11273
12068
  if (rec !== void 0) {
11274
12069
  return {
11275
12070
  agentName: name,
@@ -11281,25 +12076,29 @@ async function previewClose(db, name, opts) {
11281
12076
  ownedTasks: [],
11282
12077
  worktree: null,
11283
12078
  ...recoveryPreview(db, rec),
12079
+ carrierJob,
11284
12080
  refusesDirty: false,
11285
12081
  discard,
11286
- keepBranch: opts.keepBranch === true
12082
+ keepBranch: opts.keepBranch === true,
12083
+ abandonJob
11287
12084
  };
11288
12085
  }
11289
12086
  return {
11290
12087
  agentName: name,
11291
12088
  workstream: opts.workstream,
11292
- kind: now2 ? "agent" : nowRow ? "orphan-worktree" : "none",
12089
+ kind: now3 ? "agent" : nowRow ? "orphan-worktree" : carrierJob !== null ? "lease-job" : "none",
11293
12090
  revision: opts.recoveryId === void 0 ? agentRevision(db, opts.workstream, name) : 0,
11294
- incarnation: now2?.incarnation ?? null,
11295
- paneId: now2?.paneId ?? null,
11296
- ownedTasks: now2 ? ownedTasksOf(db, now2) : [],
12091
+ incarnation: now3?.incarnation ?? null,
12092
+ paneId: now3?.paneId ?? null,
12093
+ ownedTasks: now3 ? ownedTasksOf(db, now3) : [],
11297
12094
  worktree: nowRow && state ? { path: nowRow.path, state } : null,
11298
- remote: remoteOf(now2),
12095
+ remote: remoteOf(now3),
11299
12096
  recovery: null,
12097
+ carrierJob,
11300
12098
  refusesDirty: state === "dirty" && !discard,
11301
12099
  discard,
11302
- keepBranch: opts.keepBranch === true
12100
+ keepBranch: opts.keepBranch === true,
12101
+ abandonJob
11303
12102
  };
11304
12103
  })();
11305
12104
  if (preview !== void 0) return preview;
@@ -11320,29 +12119,55 @@ async function closeLocked(db, name, opts) {
11320
12119
  };
11321
12120
  const agent = getAgentForCleanup(db, name, opts.workstream);
11322
12121
  const orphan = agent ? void 0 : getWorktreeFor(db, opts.workstream, name);
11323
- if (opts.recoveryId !== void 0 || agent === void 0 && orphan === void 0) {
11324
- const rec = selectRemoteRecovery(db, opts.workstream, name, opts.recoveryId);
11325
- if (rec === void 0) return { killedPane: false, deletedRow: false, worktree: null };
11326
- fence();
11327
- return closeRecovery(db, rec, {
11328
- keepBranch: opts.keepBranch === true,
11329
- force: opts.force === true
11330
- });
11331
- }
11332
- if (!agent) {
11333
- if (orphan === void 0) return { killedPane: false, deletedRow: false, worktree: null };
11334
- fence();
11335
- const worktree2 = await releaseWorktree(db, orphan, { discard, force: opts.force === true });
11336
- emitEvent(db, opts.workstream, "agent.close", closeLine(name, void 0, worktree2));
11337
- return { killedPane: false, deletedRow: false, worktree: worktree2 };
12122
+ const nothing = { killedPane: false, deletedRow: false, worktree: null };
12123
+ if (agent === void 0 || opts.recoveryId !== void 0) {
12124
+ if (opts.recoveryId !== void 0 && selectRemoteRecovery(db, opts.workstream, name, opts.recoveryId) === void 0) {
12125
+ return nothing;
12126
+ }
12127
+ const jobs = agent === void 0 ? listCarrierJobsByName(db, opts.workstream, name) : [];
12128
+ if (jobs.length > 0) {
12129
+ fence();
12130
+ if (orphan === void 0) selectRemoteRecovery(db, opts.workstream, name, opts.recoveryId);
12131
+ }
12132
+ const closes2 = [];
12133
+ for (const j of jobs) {
12134
+ const c = await stopLeaseReservation(db, j, { abandon: opts.abandonJob === true });
12135
+ if (c === void 0) continue;
12136
+ cleanLeaseJobFile(db, c);
12137
+ closes2.push(c);
12138
+ }
12139
+ const { result, tree } = await closeTreeWithoutRow(
12140
+ db,
12141
+ name,
12142
+ opts,
12143
+ orphan,
12144
+ fence,
12145
+ jobs.length > 0
12146
+ );
12147
+ for (const c of closes2) {
12148
+ const r = c.reservation;
12149
+ if (c.action === "stopped" && r.reason !== null && !closedKeptTree(r, tree)) {
12150
+ throw new CarrierJobKeptError(
12151
+ name,
12152
+ opts.workstream,
12153
+ r.host,
12154
+ `job ${r.job} is stopped, but ${r.reason}${tree === void 0 ? "" : ` (the close handled ${tree.path} on ${tree.host}, another tree)`}`
12155
+ );
12156
+ }
12157
+ }
12158
+ if (closes2.length > 0) unlinkCtlSocket(db, name, opts.workstream);
12159
+ for (const c of closes2) releaseLeaseClose(db, c);
12160
+ if (closes2.length === 0) return result;
12161
+ emitEvent(db, opts.workstream, "agent.close", closeLine(name, void 0, null, closes2));
12162
+ return { ...result, carrierJobs: closes2.map(closedJob) };
11338
12163
  }
11339
12164
  const row = worktreeOf(db, opts.workstream, name, agent);
11340
12165
  if (row !== void 0) await assertReleasable(row, { discard });
11341
12166
  const mux = await activeMux().catch(() => void 0);
11342
12167
  const running = await runInFlight(agent);
11343
12168
  const replaced = () => {
11344
- const now2 = getAgentForCleanup(db, name, opts.workstream);
11345
- return now2 !== void 0 && !sameIncarnation(now2, agent);
12169
+ const now3 = getAgentForCleanup(db, name, opts.workstream);
12170
+ return now3 !== void 0 && !sameIncarnation(now3, agent);
11346
12171
  };
11347
12172
  if (replaced()) throw new AgentReplacedError(name, opts.workstream);
11348
12173
  fence();
@@ -11369,6 +12194,9 @@ async function closeLocked(db, name, opts) {
11369
12194
  throw new AgentPaneStillAliveError(name, agent.workstreamName, agent.paneId, alive, why);
11370
12195
  }
11371
12196
  if (replaced()) throw new AgentReplacedError(name, opts.workstream);
12197
+ const lease = await stopAgentLease(db, agent, opts.abandonJob === true);
12198
+ if (replaced()) throw new AgentReplacedError(name, opts.workstream);
12199
+ if (lease !== void 0) cleanLeaseJobFile(db, lease);
11372
12200
  let worktree = null;
11373
12201
  let remoteHandled = false;
11374
12202
  if (row !== void 0)
@@ -11400,16 +12228,89 @@ async function closeLocked(db, name, opts) {
11400
12228
  { preserve: !remoteHandled }
11401
12229
  );
11402
12230
  if (!deletedRow && replaced()) throw new AgentReplacedError(name, opts.workstream);
12231
+ if (lease !== void 0) releaseLeaseClose(db, lease);
11403
12232
  if (deletedRow && !running && openAtClose === 0)
11404
12233
  setAgentDone(db, agent.workstreamName, name, true);
11405
- emitEvent(db, agent.workstreamName, "agent.close", closeLine(name, agent.paneId, worktree));
11406
- return { killedPane: true, deletedRow, worktree };
12234
+ const closes = lease === void 0 ? [] : [lease];
12235
+ emitEvent(
12236
+ db,
12237
+ agent.workstreamName,
12238
+ "agent.close",
12239
+ closeLine(name, agent.paneId, worktree, closes)
12240
+ );
12241
+ return {
12242
+ killedPane: true,
12243
+ deletedRow,
12244
+ worktree,
12245
+ ...lease === void 0 ? {} : { carrierJobs: [closedJob(lease)] }
12246
+ };
12247
+ }
12248
+ function closedKeptTree(r, tree) {
12249
+ return tree !== void 0 && r.treePath !== null && tree.host === r.host && tree.path === r.treePath && tree.agentId === r.treeAgentId;
12250
+ }
12251
+ async function closeTreeWithoutRow(db, name, opts, orphan, fence, fenced) {
12252
+ const nothing = { killedPane: false, deletedRow: false, worktree: null };
12253
+ if (opts.recoveryId !== void 0 || orphan === void 0) {
12254
+ const rec = selectRemoteRecovery(db, opts.workstream, name, opts.recoveryId);
12255
+ if (rec === void 0) return { result: nothing };
12256
+ if (!fenced) fence();
12257
+ const result = await closeRecovery(db, rec, {
12258
+ keepBranch: opts.keepBranch === true,
12259
+ force: opts.force === true
12260
+ });
12261
+ return { result, tree: { host: rec.host, path: rec.cwd, agentId: rec.agentId } };
12262
+ }
12263
+ if (!fenced) fence();
12264
+ const worktree = await releaseWorktree(db, orphan, {
12265
+ discard: opts.discard === true,
12266
+ force: opts.force === true
12267
+ });
12268
+ emitEvent(db, opts.workstream, "agent.close", closeLine(name, void 0, worktree));
12269
+ return { result: { ...nothing, worktree } };
12270
+ }
12271
+ async function stopAgentLease(db, agent, abandon) {
12272
+ const jobs = listCarrierJobsByName(db, agent.workstreamName, agent.name);
12273
+ const mine = jobs.find((j) => j.agentId === agent.incarnation) ?? jobs.find((j) => j.agentId === null);
12274
+ if (mine !== void 0) return stopLeaseReservation(db, mine, { abandon });
12275
+ if (agent.host === null || abandon) return void 0;
12276
+ const file = leaseJobFile(db, agent.workstreamName, agent.name);
12277
+ const read = readJobFile(file);
12278
+ const transport = hostTransportOf(db, agent.incarnation);
12279
+ const lease = transport === "lease" || transport === null && (read.kind !== "none" || isLeaseHost(db, agent.host));
12280
+ if (!lease) return void 0;
12281
+ throw new CarrierJobUnresolvedError(
12282
+ agent.name,
12283
+ agent.workstreamName,
12284
+ agent.host,
12285
+ read.kind === "none" ? `no carrier_jobs reservation records its job and no job file is at ${file}` : `no carrier_jobs reservation records its job, but its job file ${file} ${read.kind === "handle" ? `names ${read.job}: stop it by hand` : `is ${read.kind}`}`
12286
+ );
11407
12287
  }
12288
+ function hostTransportOf(db, incarnation) {
12289
+ const r = db.prepare("SELECT host_transport AS t FROM agents WHERE id = ?").get(incarnation);
12290
+ return r?.t ?? null;
12291
+ }
12292
+ function isLeaseHost(db, host) {
12293
+ try {
12294
+ return getHost(db, host).transport === "lease";
12295
+ } catch {
12296
+ return false;
12297
+ }
12298
+ }
12299
+ var closedJob = (c) => ({
12300
+ host: c.reservation.host,
12301
+ job: c.reservation.job,
12302
+ action: c.action
12303
+ });
11408
12304
  function sameIncarnation(a, b) {
11409
12305
  return a.incarnation === b.incarnation && a.paneId === b.paneId;
11410
12306
  }
11411
- function closeLine(name, paneId, wt) {
12307
+ function closeLine(name, paneId, wt, jobs = []) {
11412
12308
  const bits = [paneId === void 0 ? void 0 : `pane=${paneId}`];
12309
+ for (const j of jobs) {
12310
+ const what = j.reservation.job === null ? "lease reservation" : `job ${j.reservation.job}`;
12311
+ const did = j.action === "stopped" ? "stopped" : j.action === "abandon" ? "abandoned" : "released";
12312
+ bits.push(`${what} on ${j.reservation.host} ${did}`);
12313
+ }
11413
12314
  if (wt !== null) {
11414
12315
  bits.push(`worktree ${wt.action}${wt.branch === void 0 ? "" : ` as ${wt.branch}`}`);
11415
12316
  }
@@ -11429,7 +12330,7 @@ function delegateOutcome(agent, timedOut) {
11429
12330
  }
11430
12331
 
11431
12332
  // src/agents/observe.ts
11432
- import { dirname as dirname9 } from "path";
12333
+ import { dirname as dirname10 } from "path";
11433
12334
  function observationFromReading(agent, reading) {
11434
12335
  return {
11435
12336
  workstream: agent.workstreamName,
@@ -11472,7 +12373,7 @@ function observationFromGate(agent, gate) {
11472
12373
  async function observeAgentStates(db, agents) {
11473
12374
  const gates = agents.filter((a) => a.gateToken !== null);
11474
12375
  const rest = agents.filter((a) => a.gateToken === null);
11475
- const readings = await readAgentStates(rest, { stateDir: dirname9(db.name) });
12376
+ const readings = await readAgentStates(rest, { stateDir: dirname10(db.name) });
11476
12377
  const out = new Map(
11477
12378
  rest.map((a) => [agentKey(a), observationFromReading(a, readings.get(agentKey(a)))])
11478
12379
  );
@@ -11599,7 +12500,7 @@ async function watchAgentRun(agent, sock, signal, opts) {
11599
12500
 
11600
12501
  // src/delegates.ts
11601
12502
  import { createHash as createHash5 } from "crypto";
11602
- import { dirname as dirname10, join as join8 } from "path";
12503
+ import { dirname as dirname11, join as join8 } from "path";
11603
12504
 
11604
12505
  // src/delegate-verdict.ts
11605
12506
  var VERDICT_LABELS = ["CONFIRMED", "REFUTED", "UNVERIFIED"];
@@ -11638,11 +12539,11 @@ ${body}`;
11638
12539
 
11639
12540
  // src/delegates.ts
11640
12541
  function delegateSessionDir(dbPath, workstream, task) {
11641
- return join8(dirname10(dbPath), "delegate-sessions", workstream, task);
12542
+ return join8(dirname11(dbPath), "delegate-sessions", workstream, task);
11642
12543
  }
11643
12544
  function delegateProcessDir(dbPath, callId) {
11644
12545
  return join8(
11645
- dirname10(dbPath),
12546
+ dirname11(dbPath),
11646
12547
  "delegate-process",
11647
12548
  createHash5("sha256").update(callId).digest("hex").slice(0, 16)
11648
12549
  );
@@ -11773,7 +12674,7 @@ function taskIdFor2(callId, label) {
11773
12674
  const h = createHash5("sha256").update(callId).digest("hex").slice(0, 8);
11774
12675
  return `delegate_${stem ? `${stem}_` : ""}${h}`;
11775
12676
  }
11776
- var now = () => (/* @__PURE__ */ new Date()).toISOString();
12677
+ var now2 = () => (/* @__PURE__ */ new Date()).toISOString();
11777
12678
  function registerDelegateCall(db, opts) {
11778
12679
  const state = opts.state ?? "queued";
11779
12680
  return withOpContext(
@@ -11831,7 +12732,7 @@ function registerDelegateCall(db, opts) {
11831
12732
  opts.record?.workstream ?? null,
11832
12733
  opts.record?.task ?? null,
11833
12734
  opts.request.label ?? null,
11834
- now(),
12735
+ now2(),
11835
12736
  canonical2(opts.request)
11836
12737
  );
11837
12738
  return { call: mustGet(db, opts.callId), created: true };
@@ -11857,7 +12758,7 @@ function markDelegate(db, callId, to, extra = {}) {
11857
12758
  SET state = ?, updated_at = ?, agent = COALESCE(?, agent),
11858
12759
  error = CASE WHEN ? = 'refused' THEN COALESCE(?, error) ELSE NULL END
11859
12760
  WHERE call_id = ? AND state = ?`
11860
- ).run(to, now(), extra.agent ?? null, to, extra.error ?? null, callId, row.state);
12761
+ ).run(to, now2(), extra.agent ?? null, to, extra.error ?? null, callId, row.state);
11861
12762
  if (r.changes !== 1) throw new DelegateStateError(callId, mustGet(db, callId).state, to);
11862
12763
  if (to === "starting")
11863
12764
  setTaskStatus(db, row.task, "OPEN", { workstream: row.workstream, substate: "todo" });
@@ -11895,7 +12796,7 @@ function finishDelegate(db, callId, opts) {
11895
12796
  if (isTerminal(before.state)) return unchanged(before);
11896
12797
  db.prepare(
11897
12798
  "UPDATE delegate_calls SET state = 'recording', updated_at = ? WHERE call_id = ? AND state NOT IN ('done','failed','cancelled')"
11898
- ).run(now(), callId);
12799
+ ).run(now2(), callId);
11899
12800
  const outcome = opts.outcome === "done" && !opts.text?.trim() ? "empty" : opts.outcome;
11900
12801
  const answered = outcome === "done";
11901
12802
  const cancelled = outcome === "cancelled";
@@ -11942,7 +12843,7 @@ function finishDelegate(db, callId, opts) {
11942
12843
  });
11943
12844
  db.prepare(
11944
12845
  "UPDATE delegate_calls SET state = ?, verdict = ?, error = ?, updated_at = ? WHERE call_id = ?"
11945
- ).run(state, ok ? verdict : null, error ?? null, now(), callId);
12846
+ ).run(state, ok ? verdict : null, error ?? null, now2(), callId);
11946
12847
  return {
11947
12848
  changed: true,
11948
12849
  state,
@@ -12036,12 +12937,12 @@ function getReceipt(db, taskId, attemptKey) {
12036
12937
  return row ? fromRaw2(row) : void 0;
12037
12938
  }
12038
12939
  function reserveReceipt(db, r) {
12039
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
12940
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
12040
12941
  db.prepare(
12041
12942
  `INSERT INTO spawn_receipts
12042
12943
  (task_id, attempt_key, name, workstream, request_sha, msg_id, state, created_at, updated_at)
12043
12944
  VALUES (?, ?, ?, ?, ?, ?, 'reserved', ?, ?)`
12044
- ).run(r.taskId, r.attemptKey, r.name, r.workstream, r.requestSha, r.msgId, now2, now2);
12945
+ ).run(r.taskId, r.attemptKey, r.name, r.workstream, r.requestSha, r.msgId, now3, now3);
12045
12946
  const row = getReceipt(db, r.taskId, r.attemptKey);
12046
12947
  if (row === void 0) throw new Error("spawn receipt vanished after reservation");
12047
12948
  return row;
@@ -12323,25 +13224,25 @@ async function drive(db, req, taskId, key, start, resumed, guard, keep = false)
12323
13224
  }
12324
13225
  req.spawn.signal?.throwIfAborted();
12325
13226
  const checked = db.transaction(() => {
12326
- const now2 = getAgentForCleanup(db, target.name, ws);
12327
- if (now2 === void 0 || now2.incarnation !== target.incarnation || now2.paneId !== target.paneId) {
13227
+ const now3 = getAgentForCleanup(db, target.name, ws);
13228
+ if (now3 === void 0 || now3.incarnation !== target.incarnation || now3.paneId !== target.paneId) {
12328
13229
  throw new AgentReplacedError(target.name, ws, "sent to");
12329
13230
  }
12330
13231
  guard?.("send");
12331
13232
  const t = getTask(db, req.task, ws);
12332
13233
  if (t === void 0) throw new TaskNotFoundError(req.task, ws);
12333
13234
  if (t.status === "CLOSED") throw new SpawnTaskStateError(req.task, "was closed");
12334
- if (t.ownerId !== now2.incarnation) {
13235
+ if (t.ownerId !== now3.incarnation) {
12335
13236
  if (t.ownerId !== null) {
12336
13237
  throw new TaskAlreadyOwnedError(req.task, t.ownerName ?? "<unknown>");
12337
13238
  }
12338
- throw new SpawnTaskStateError(req.task, `is no longer claimed by ${now2.name}`);
13239
+ throw new SpawnTaskStateError(req.task, `is no longer claimed by ${now3.name}`);
12339
13240
  }
12340
13241
  const journaled = updateReceipt(db, taskId, key, {
12341
13242
  state: "sending",
12342
- transport: expectsCtl(now2, agentCtlSocket(db, now2)) ? "ctl" : "mux"
13243
+ transport: expectsCtl(now3, agentCtlSocket(db, now3)) ? "ctl" : "mux"
12343
13244
  });
12344
- return { row: now2, rec: journaled };
13245
+ return { row: now3, rec: journaled };
12345
13246
  }).immediate();
12346
13247
  row = checked.row;
12347
13248
  rec = checked.rec;
@@ -12453,13 +13354,13 @@ function idleThresholdMs() {
12453
13354
  if (!Number.isFinite(n) || n < 0) return DEFAULT_IDLE_THRESHOLD_MS;
12454
13355
  return n;
12455
13356
  }
12456
- function computeAgentIdle(db, agent, now2 = Date.now()) {
13357
+ function computeAgentIdle(db, agent, now3 = Date.now()) {
12457
13358
  if (agent.state !== "needs_input" || agent.since === null) return false;
12458
13359
  const threshold = idleThresholdMs();
12459
13360
  if (threshold <= 0) return false;
12460
13361
  const since = Date.parse(agent.since);
12461
13362
  if (!Number.isFinite(since)) return false;
12462
- if (now2 - since < threshold) return false;
13363
+ if (now3 - since < threshold) return false;
12463
13364
  if (isScratchWorkstream(agent.workstreamName)) return true;
12464
13365
  const wsId = tryResolveWorkstreamId(db, agent.workstreamName);
12465
13366
  if (wsId === null) return false;
@@ -12524,15 +13425,19 @@ function agentIdByName(db, name, workstream) {
12524
13425
  function insertAgent(db, input) {
12525
13426
  ensureWorkstream(db, input.workstream);
12526
13427
  const workstreamId = resolveWorkstreamId(db, input.workstream);
12527
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
13428
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
13429
+ let inserted;
12528
13430
  try {
12529
- db.prepare(
12530
- `INSERT INTO agents (name, workstream_id, cli, pane_id, status, role, tab, cwd, base, command, command_source, spawner, host, collected_head, repo, gate_token, created_at, updated_at)
12531
- VALUES (@name, @workstreamId, COALESCE(@cli, 'pi'), @paneId, 'spawning',
13431
+ inserted = db.prepare(
13432
+ `INSERT INTO agents (name, workstream_id, cli, pane_id, status, role, tab, cwd, base, command, command_source, spawner, host, collected_head, repo, gate_token, host_transport, created_at, updated_at)
13433
+ SELECT @name, @workstreamId, COALESCE(@cli, 'pi'), @paneId, 'spawning',
12532
13434
  COALESCE(@role, 'full-access'), @tab, @cwd, @base, @command, @commandSource,
12533
- @spawner, @host, @collectedHead, @repo, @gateToken, @now, @now)`
13435
+ @spawner, @host, @collectedHead, @repo, @gateToken, @hostTransport, @now, @now
13436
+ WHERE NOT EXISTS (SELECT 1 FROM carrier_jobs c
13437
+ WHERE c.workstream = @workstream AND c.name = @name AND c.token IS NOT @reservation)`
12534
13438
  ).run({
12535
13439
  name: input.name,
13440
+ workstream: input.workstream,
12536
13441
  workstreamId,
12537
13442
  cli: input.cli ?? null,
12538
13443
  paneId: input.paneId,
@@ -12547,14 +13452,21 @@ function insertAgent(db, input) {
12547
13452
  collectedHead: input.collectedHead ?? null,
12548
13453
  repo: input.repo ?? null,
12549
13454
  gateToken: input.gateToken ?? null,
12550
- now: now2
12551
- });
13455
+ hostTransport: input.hostTransport ?? null,
13456
+ reservation: input.reservation ?? null,
13457
+ now: now3
13458
+ }).changes;
12552
13459
  } catch (err) {
12553
13460
  if (err.code === "SQLITE_CONSTRAINT_UNIQUE") {
12554
13461
  throw new AgentExistsError(input.name);
12555
13462
  }
12556
13463
  throw err;
12557
13464
  }
13465
+ if (inserted === 0) {
13466
+ const r = getCarrierJobByName(db, input.workstream, input.name);
13467
+ if (r !== void 0) throw new CarrierJobUnstoppedError(input.name, r);
13468
+ throw new Error(`agents.insertAgent: row not inserted: ${input.name}`);
13469
+ }
12558
13470
  const row = getAgent(db, input.name, input.workstream);
12559
13471
  if (!row) throw new Error(`agents.insertAgent: row not found after insert: ${input.name}`);
12560
13472
  return row;
@@ -12668,7 +13580,7 @@ async function readLiveAgents(db, workstream, only, states) {
12668
13580
  const baseAgents = listAgents(db, { workstream }).filter(
12669
13581
  (a) => only === void 0 || a.name === only
12670
13582
  );
12671
- const stateDir = dirname11(db.name);
13583
+ const stateDir = dirname12(db.name);
12672
13584
  let readings;
12673
13585
  if (states === void 0) readings = await readAgentStates(baseAgents, { stateDir });
12674
13586
  else {
@@ -12678,7 +13590,7 @@ async function readLiveAgents(db, workstream, only, states) {
12678
13590
  }
12679
13591
  readings = states.cache;
12680
13592
  }
12681
- const now2 = Date.now();
13593
+ const now3 = Date.now();
12682
13594
  return baseAgents.map((agent) => {
12683
13595
  const reading = readings.get(agentKey(agent)) ?? {
12684
13596
  state: "unknown",
@@ -12696,7 +13608,7 @@ async function readLiveAgents(db, workstream, only, states) {
12696
13608
  ...reading.reason !== void 0 ? { reason: reading.reason } : {},
12697
13609
  ...reading.tool !== void 0 ? { tool: reading.tool } : {}
12698
13610
  };
12699
- return computeAgentIdle(db, live2, now2) ? { ...live2, idle: true } : live2;
13611
+ return computeAgentIdle(db, live2, now3) ? { ...live2, idle: true } : live2;
12700
13612
  });
12701
13613
  }
12702
13614
 
@@ -12716,13 +13628,239 @@ async function withSpawnLock(session, fn, opts) {
12716
13628
  return withFileLock(lockPathForSession(session), session, fn, lockOpts);
12717
13629
  }
12718
13630
 
13631
+ // src/agents/spawn-ready.ts
13632
+ import { existsSync as existsSync5 } from "fs";
13633
+ import { setTimeout as delay2 } from "timers/promises";
13634
+ function probeToSpawnCtl(kind) {
13635
+ return kind === "ok" ? "ok" : kind === "missing" ? "missing" : "refused";
13636
+ }
13637
+ function defaultSpawnCtlMs() {
13638
+ const raw = process.env.MU_SPAWN_CTL_MS;
13639
+ if (raw === void 0) return 3e4;
13640
+ const parsed = Number.parseInt(raw, 10);
13641
+ if (Number.isNaN(parsed) || parsed < 0) return 3e4;
13642
+ return parsed;
13643
+ }
13644
+ var CTL_POLL_INTERVAL_MS = 250;
13645
+ var CTL_STAT_INTERVAL_MS = 50;
13646
+ var handshakeClock = { now: () => Date.now(), delay: (ms) => delay2(ms) };
13647
+ async function awaitCtlHandshake(sock, onMiss, signal) {
13648
+ const budgetMs = defaultSpawnCtlMs();
13649
+ const deadline = handshakeClock.now() + budgetMs;
13650
+ let last = "missing";
13651
+ let lastMissAt = Number.NEGATIVE_INFINITY;
13652
+ let first = true;
13653
+ for (; ; ) {
13654
+ signal?.throwIfAborted();
13655
+ if (first || existsSync5(sock)) {
13656
+ first = false;
13657
+ const remaining = deadline - handshakeClock.now();
13658
+ const probe = await ctlProbe(
13659
+ sock,
13660
+ Math.max(1, Math.min(remaining, CTL_POLL_INTERVAL_MS * 4))
13661
+ );
13662
+ if (probe.kind === "ok") {
13663
+ return {
13664
+ ctl: "ok",
13665
+ ...probe.incarnation !== void 0 ? { incarnation: probe.incarnation } : {}
13666
+ };
13667
+ }
13668
+ last = probeToSpawnCtl(probe.kind);
13669
+ }
13670
+ const atDeadline = handshakeClock.now() >= deadline;
13671
+ if (onMiss && (atDeadline || handshakeClock.now() - lastMissAt >= CTL_POLL_INTERVAL_MS)) {
13672
+ lastMissAt = handshakeClock.now();
13673
+ await onMiss();
13674
+ }
13675
+ const left = deadline - handshakeClock.now();
13676
+ if (left <= 0) return { ctl: last };
13677
+ await handshakeClock.delay(Math.min(CTL_STAT_INTERVAL_MS, left));
13678
+ }
13679
+ }
13680
+ function defaultSpawnLivenessMs() {
13681
+ const raw = process.env.MU_SPAWN_LIVENESS_MS;
13682
+ if (raw === void 0) return 1500;
13683
+ const parsed = Number.parseInt(raw, 10);
13684
+ if (Number.isNaN(parsed) || parsed < 0) return 1500;
13685
+ return parsed;
13686
+ }
13687
+ var AUTH_ERROR_PATTERNS = [
13688
+ /No API key found for [\w-]+/i,
13689
+ /Error: invalid API key/i,
13690
+ /Authentication failed/i,
13691
+ /401 Unauthorized/i,
13692
+ /Could not authenticate/i
13693
+ ];
13694
+ var EXEC_FAILURE_PATTERN = /^(?:.*: )?(?:command not found|No such file or directory)(?::\s*\S+)?$/i;
13695
+ var STARTUP_ERROR_TAIL_LINES = 30;
13696
+ function detectSpawnStartupError(scrollback, cliStarted = false) {
13697
+ const patterns = cliStarted ? AUTH_ERROR_PATTERNS : [...AUTH_ERROR_PATTERNS, EXEC_FAILURE_PATTERN];
13698
+ const lines = scrollback.split(/\r?\n/);
13699
+ const tail = lines.slice(Math.max(0, lines.length - STARTUP_ERROR_TAIL_LINES));
13700
+ for (const line of tail) {
13701
+ for (const pattern of patterns) {
13702
+ if (pattern.test(line)) return line;
13703
+ }
13704
+ }
13705
+ return void 0;
13706
+ }
13707
+ async function awaitSpawnLiveness(paneId, agentName, command, host) {
13708
+ const ms = defaultSpawnLivenessMs();
13709
+ if (ms === 0) return false;
13710
+ await sleep2(ms);
13711
+ return checkSpawnHealth(paneId, agentName, command, void 0, false, host);
13712
+ }
13713
+ var CLAUDE_TRUST_DIALOG = /Yes, I trust this folder/i;
13714
+ function detectClaudeTrustDialog(scrollback) {
13715
+ const lines = scrollback.split(/\r?\n/);
13716
+ const tail = lines.slice(Math.max(0, lines.length - STARTUP_ERROR_TAIL_LINES));
13717
+ return tail.some((line) => CLAUDE_TRUST_DIALOG.test(line));
13718
+ }
13719
+ async function checkSpawnHealth(paneId, agentName, command, when, cliStarted = false, host) {
13720
+ const mux = await activeMux();
13721
+ const scrollback = await mux.capture(paneId, { lines: 50 }).catch(() => void 0);
13722
+ if (!await mux.paneAlive(paneId)) {
13723
+ throw new AgentDiedOnSpawnError(agentName, paneId, scrollback, command, when, host);
13724
+ }
13725
+ if (scrollback !== void 0) {
13726
+ const matchedLine = detectSpawnStartupError(scrollback, cliStarted);
13727
+ if (matchedLine !== void 0) {
13728
+ throw new AgentSpawnStartupError(agentName, paneId, matchedLine, scrollback, when);
13729
+ }
13730
+ return detectClaudeTrustDialog(scrollback);
13731
+ }
13732
+ return false;
13733
+ }
13734
+
13735
+ // src/agents/spawn-remote.ts
13736
+ function remoteRoot(opts, profile) {
13737
+ const root = opts.root ?? profile.repo ?? void 0;
13738
+ return root === void 0 ? {} : { root };
13739
+ }
13740
+ async function requireHostSpawn(db, opts) {
13741
+ const host = opts.host ?? "";
13742
+ const profile = getHost(db, host);
13743
+ if (opts.worktree && opts.root === void 0 && profile.repo === null) {
13744
+ throw new HostProfileError(
13745
+ `host ${host} has no default repo: pass --root <remote repo path> with --worktree`
13746
+ );
13747
+ }
13748
+ const mux = await activeMux();
13749
+ if (!mux.runsShellCommand) throw new HostSpawnUnsupportedError(host, mux.name);
13750
+ return profile;
13751
+ }
13752
+ async function prestageRemote(db, opts, cli, spawner, lease, profile, reservation) {
13753
+ return withAgentLock(db, opts.workstream, opts.name, async () => {
13754
+ const placeholder = insertAgent(db, {
13755
+ ...reservation !== void 0 ? { reservation } : {},
13756
+ name: opts.name,
13757
+ workstream: opts.workstream,
13758
+ cli,
13759
+ paneId: pendingPaneIdFor(opts.name),
13760
+ role: opts.role,
13761
+ tab: opts.tab ?? null,
13762
+ cwd: null,
13763
+ spawner,
13764
+ host: profile.name,
13765
+ hostTransport: profile.transport
13766
+ });
13767
+ try {
13768
+ const made = await prepareRemote(db, lease, profile, {
13769
+ workstream: opts.workstream,
13770
+ name: opts.name,
13771
+ worktree: true,
13772
+ ...opts.from !== void 0 ? { from: opts.from } : {},
13773
+ ...remoteRoot(opts, profile),
13774
+ // An unused leftover of a prepare whose answer was lost is taken over;
13775
+ // never while a removed agent's recovery record may name that tree.
13776
+ reclaim: !recoveriesFor(db, opts.workstream, opts.name).some(
13777
+ (r) => r.host === profile.name
13778
+ )
13779
+ });
13780
+ return {
13781
+ path: made.cwd,
13782
+ base: made.base ?? "",
13783
+ root: made.repo ?? "",
13784
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
13785
+ incarnation: placeholder.incarnation,
13786
+ host: profile.name
13787
+ };
13788
+ } catch (err) {
13789
+ try {
13790
+ deleteAgent(db, opts.name, opts.workstream, { incarnation: placeholder.incarnation });
13791
+ } catch {
13792
+ }
13793
+ throw err;
13794
+ }
13795
+ });
13796
+ }
13797
+ async function admitRemoteSpawn(db, opts, cli, spawner, profile, admit2, reservation) {
13798
+ const admitted = await admitHostChannel(
13799
+ db,
13800
+ profile.name,
13801
+ `spawn:${opts.workstream}/${opts.name}`,
13802
+ { paneAlive: localPaneAlive }
13803
+ );
13804
+ const { lease } = admitted;
13805
+ const admittedProfile = admitted.profile;
13806
+ const transient = admittedProfile.transport === "lease";
13807
+ try {
13808
+ admit2?.(admittedProfile);
13809
+ let staged;
13810
+ let site;
13811
+ if (opts.worktree) {
13812
+ staged = await prestageRemote(db, opts, cli, spawner, lease, admittedProfile, reservation);
13813
+ } else {
13814
+ site = await prepareRemote(db, lease, admittedProfile, {
13815
+ workstream: opts.workstream,
13816
+ name: opts.name,
13817
+ worktree: false,
13818
+ ...opts.cwd !== void 0 ? { cwd: opts.cwd } : {},
13819
+ ...remoteRoot(opts, admittedProfile)
13820
+ });
13821
+ }
13822
+ if (transient) releaseHostChannel(db, lease);
13823
+ return { lease: transient ? void 0 : lease, profile: admittedProfile, staged, site };
13824
+ } catch (err) {
13825
+ releaseHostChannel(db, lease);
13826
+ throw err;
13827
+ }
13828
+ }
13829
+ async function rollbackRemoteWorktree(db, name, workstream, staged, paneGone2) {
13830
+ let remoteGone = false;
13831
+ try {
13832
+ if (!paneGone2) throw new Error("pane not proven gone");
13833
+ await withAgentLock(db, workstream, name, async () => {
13834
+ const cur = getAgent(db, name, workstream);
13835
+ if (cur !== void 0 && cur.incarnation !== staged.incarnation) return;
13836
+ remoteGone = await removeRemoteWorktree(
13837
+ db,
13838
+ staged.host ?? "",
13839
+ `rollback:${workstream}/${name}`,
13840
+ staged
13841
+ );
13842
+ });
13843
+ } catch {
13844
+ try {
13845
+ emitEvent(
13846
+ db,
13847
+ workstream,
13848
+ "agent.spawn_failed",
13849
+ `agent spawn rollback ${name}: remote worktree ${staged.path} on ${staged.host} was not removed`
13850
+ );
13851
+ } catch {
13852
+ }
13853
+ }
13854
+ return remoteGone;
13855
+ }
13856
+
12719
13857
  // src/agents/staging.ts
12720
13858
  var STAGING_DEAD_MS = 6e4;
12721
- function deadStagings(db, workstream, now2 = Date.now()) {
13859
+ function deadStagings(db, workstream, now3 = Date.now()) {
12722
13860
  return listAgents(db, workstream === void 0 ? {} : { workstream }).filter((a) => {
12723
13861
  if (!isPendingPaneId(a.paneId) || a.host !== null) return false;
12724
13862
  const made = a.cwd === null ? void 0 : getWorktreeAt(db, a.cwd)?.createdAt;
12725
- return now2 - Math.max(Date.parse(a.updatedAt), made ? Date.parse(made) : 0) > STAGING_DEAD_MS;
13863
+ return now3 - Math.max(Date.parse(a.updatedAt), made ? Date.parse(made) : 0) > STAGING_DEAD_MS;
12726
13864
  });
12727
13865
  }
12728
13866
  async function reclaimDeadStagings(db, workstream) {
@@ -12811,7 +13949,7 @@ async function checkCommandResolvable(command) {
12811
13949
  var NEXT_FREE_ATTEMPTS = 32;
12812
13950
  var NEXT_FREE_LOOKUPS = 1e3;
12813
13951
  function nextFreeAgentName(db, name, workstream) {
12814
- const free = (n) => getAgent(db, n, workstream) === void 0 && !existsSync5(worktreePath(workstream, n));
13952
+ const free = (n) => getAgent(db, n, workstream) === void 0 && getCarrierJobByName(db, workstream, n) === void 0 && !existsSync6(worktreePath(workstream, n));
12815
13953
  if (free(name)) return name;
12816
13954
  const m = /^(.*)-(\d+)$/.exec(name);
12817
13955
  const [stem, first] = m ? [m[1] ?? "", BigInt(m[2] ?? "1")] : [name, 1n];
@@ -12825,9 +13963,6 @@ function nextFreeAgentName(db, name, workstream) {
12825
13963
  }
12826
13964
  throw new AgentNameExhaustedError(name, `the next ${NEXT_FREE_LOOKUPS} names are taken`);
12827
13965
  }
12828
- function probeToSpawnCtl(kind) {
12829
- return kind === "ok" ? "ok" : kind === "missing" ? "missing" : "refused";
12830
- }
12831
13966
  function speaksMuCtl(cli, command) {
12832
13967
  if (cli === "pi") return true;
12833
13968
  const argv0 = basename2(parseFirstToken(command));
@@ -12839,49 +13974,6 @@ function isCommandGate(opts) {
12839
13974
  const argv0 = basename2(parseFirstToken(opts.command));
12840
13975
  return argv0 !== "pi" && argv0 !== "pi-meta";
12841
13976
  }
12842
- function defaultSpawnCtlMs() {
12843
- const raw = process.env.MU_SPAWN_CTL_MS;
12844
- if (raw === void 0) return 3e4;
12845
- const parsed = Number.parseInt(raw, 10);
12846
- if (Number.isNaN(parsed) || parsed < 0) return 3e4;
12847
- return parsed;
12848
- }
12849
- var CTL_POLL_INTERVAL_MS = 250;
12850
- var CTL_STAT_INTERVAL_MS = 50;
12851
- var handshakeClock = { now: () => Date.now(), delay: (ms) => delay(ms) };
12852
- async function awaitCtlHandshake(sock, onMiss, signal) {
12853
- const budgetMs = defaultSpawnCtlMs();
12854
- const deadline = handshakeClock.now() + budgetMs;
12855
- let last = "missing";
12856
- let lastMissAt = Number.NEGATIVE_INFINITY;
12857
- let first = true;
12858
- for (; ; ) {
12859
- signal?.throwIfAborted();
12860
- if (first || existsSync5(sock)) {
12861
- first = false;
12862
- const remaining = deadline - handshakeClock.now();
12863
- const probe = await ctlProbe(
12864
- sock,
12865
- Math.max(1, Math.min(remaining, CTL_POLL_INTERVAL_MS * 4))
12866
- );
12867
- if (probe.kind === "ok") {
12868
- return {
12869
- ctl: "ok",
12870
- ...probe.incarnation !== void 0 ? { incarnation: probe.incarnation } : {}
12871
- };
12872
- }
12873
- last = probeToSpawnCtl(probe.kind);
12874
- }
12875
- const atDeadline = handshakeClock.now() >= deadline;
12876
- if (onMiss && (atDeadline || handshakeClock.now() - lastMissAt >= CTL_POLL_INTERVAL_MS)) {
12877
- lastMissAt = handshakeClock.now();
12878
- await onMiss();
12879
- }
12880
- const left = deadline - handshakeClock.now();
12881
- if (left <= 0) return { ctl: last };
12882
- await handshakeClock.delay(Math.min(CTL_STAT_INTERVAL_MS, left));
12883
- }
12884
- }
12885
13977
  async function spawnAgent3(db, opts) {
12886
13978
  await reclaimDeadStagings(db, opts.workstream);
12887
13979
  if (opts.nextFree === true) {
@@ -12904,6 +13996,7 @@ async function spawnAgent3(db, opts) {
12904
13996
  if (getAgent(db, opts.name, opts.workstream) !== void 0) {
12905
13997
  throw new AgentExistsError(opts.name);
12906
13998
  }
13999
+ refuseReserved(db, opts.workstream, opts.name);
12907
14000
  const session = opts.tmuxSession ?? `mu-${opts.workstream}`;
12908
14001
  const windowName = opts.tab ?? opts.name;
12909
14002
  const cli = opts.cli ?? "pi";
@@ -12915,6 +14008,15 @@ async function spawnAgent3(db, opts) {
12915
14008
  "--worktree cannot be used with a remote command gate (--host with a non-pi --command): a gate runs in --cwd, else the profile's default repo"
12916
14009
  );
12917
14010
  }
14011
+ if (profile) assertLeaseSpawnable(profile, gateToken !== void 0);
14012
+ const reservation = profile?.transport === "lease" ? await reserveLeaseJob(db, opts.workstream, opts.name, profile) : void 0;
14013
+ const leaseCheck = (p) => {
14014
+ assertLeaseSpawnable(p, gateToken !== void 0);
14015
+ if (p.transport === "lease" !== (reservation !== void 0)) {
14016
+ throw new HostProfileError(say("carrier.transport_changed", p.name));
14017
+ }
14018
+ if (reservation !== void 0) setCarrierJobStop(db, reservation.token, p.stop ?? "");
14019
+ };
12918
14020
  const command = opts.command ?? (profile ? cli : resolved.command);
12919
14021
  const commandSource = opts.command !== void 0 ? "explicit" : !profile && resolved.resolvedFromEnv ? "env" : "cli-key";
12920
14022
  if (opts.command === void 0 && !profile) {
@@ -12929,32 +14031,28 @@ async function spawnAgent3(db, opts) {
12929
14031
  let site;
12930
14032
  try {
12931
14033
  if (profile) {
12932
- ({ lease, profile } = await admitHostChannel(
14034
+ ({ lease, profile, staged, site } = await admitRemoteSpawn(
12933
14035
  db,
12934
- profile.name,
12935
- `spawn:${opts.workstream}/${opts.name}`,
12936
- { paneAlive: localPaneAlive }
14036
+ opts,
14037
+ cli,
14038
+ spawner,
14039
+ profile,
14040
+ leaseCheck,
14041
+ reservation?.token
12937
14042
  ));
12938
- if (opts.worktree) staged = await prestageRemote(db, opts, cli, spawner, lease, profile);
12939
- else
12940
- site = await prepareRemote(db, lease, profile, {
12941
- workstream: opts.workstream,
12942
- name: opts.name,
12943
- worktree: false,
12944
- ...opts.cwd !== void 0 ? { cwd: opts.cwd } : {},
12945
- ...remoteRoot(opts, profile)
12946
- });
12947
14043
  } else if (opts.worktree) {
12948
14044
  staged = await prestageWorktree(db, opts, cli, spawner);
12949
14045
  }
12950
14046
  } catch (err) {
12951
14047
  if (lease !== void 0) releaseHostChannel(db, lease);
14048
+ if (reservation !== void 0) releaseLeaseReservation(db, reservation.token, "none");
12952
14049
  emitSpawnFailed(db, opts, "worktree", err);
12953
14050
  throw err;
12954
14051
  }
12955
14052
  const paneCwd = staged?.path ?? site?.cwd ?? resolve3(opts.cwd ?? process.cwd());
12956
- const ctlSocket = ctlSocketPath(opts.workstream, opts.name, dirname12(db.name));
12957
- mkdirSync3(dirname12(ctlSocket), { recursive: true, mode: 448 });
14053
+ const carrier = reservation !== void 0 ? { token: reservation.token } : void 0;
14054
+ const ctlSocket = ctlSocketPath(opts.workstream, opts.name, dirname13(db.name));
14055
+ mkdirSync4(dirname13(ctlSocket), { recursive: true, mode: 448 });
12958
14056
  const paneEnv = profile ? {} : {
12959
14057
  MU_MANAGED_AGENT: "1",
12960
14058
  MU_AGENT_NAME: opts.name,
@@ -12985,71 +14083,100 @@ async function spawnAgent3(db, opts) {
12985
14083
  let readiness = Promise.resolve();
12986
14084
  try {
12987
14085
  opts.signal?.throwIfAborted();
12988
- const created = await withSpawnLock(session, async () => {
12989
- if (!hasWorktree && getAgent(db, opts.name, opts.workstream) !== void 0) {
12990
- throw new AgentExistsError(opts.name);
12991
- }
12992
- if (staged !== void 0) {
12993
- const cur = getAgent(db, opts.name, opts.workstream);
12994
- if (cur === void 0 || cur.incarnation !== staged.incarnation || cur.paneId !== pendingPaneIdFor(opts.name)) {
12995
- throw new AgentReplacedError(opts.name, opts.workstream);
14086
+ const created = await withAgentLock(
14087
+ db,
14088
+ opts.workstream,
14089
+ opts.name,
14090
+ () => withSpawnLock(session, async () => {
14091
+ if (!hasWorktree && getAgent(db, opts.name, opts.workstream) !== void 0) {
14092
+ throw new AgentExistsError(opts.name);
12996
14093
  }
12997
- }
12998
- rmSync3(ctlSocket, { force: true });
12999
- const mux = await activeMux();
13000
- if (isScratchWorkstream(opts.workstream)) await mux.ensureSession(session);
13001
- let onCreated = () => {
13002
- };
13003
- const createdPane = new Promise((res) => {
13004
- onCreated = res;
13005
- });
13006
- const done = mux.spawnAgent({
13007
- session,
13008
- window: windowName,
13009
- name: opts.name,
13010
- cli,
13011
- command: profile ? remoteShellCommand(profile, {
14094
+ if (staged !== void 0) {
14095
+ const cur = getAgent(db, opts.name, opts.workstream);
14096
+ if (cur === void 0 || cur.incarnation !== staged.incarnation || cur.paneId !== pendingPaneIdFor(opts.name)) {
14097
+ throw new AgentReplacedError(opts.name, opts.workstream);
14098
+ }
14099
+ }
14100
+ refuseReserved(db, opts.workstream, opts.name, carrier?.token);
14101
+ rmSync4(ctlSocket, { force: true });
14102
+ const jobFile = carrier ? claimLeaseJobFile(db, opts.workstream, opts.name) : void 0;
14103
+ const mux = await activeMux();
14104
+ if (isScratchWorkstream(opts.workstream)) await mux.ensureSession(session);
14105
+ const paneCommand = profile ? remoteShellCommand(profile, {
13012
14106
  workstream: opts.workstream,
13013
14107
  name: opts.name,
13014
14108
  command: gateToken === void 0 ? command : gateWrapper(command, gateToken),
13015
14109
  localSock: ctlSocket,
13016
- cwd: paneCwd
13017
- }) : command,
13018
- commandSource,
13019
- ...profile ? {} : { cwd: paneCwd },
13020
- env: paneEnv,
13021
- onPaneCreated: onCreated,
13022
- // A gate's pane outlives its command: the result is read after it ends.
13023
- ...gateToken !== void 0 ? { retainOnExit: true } : {}
13024
- });
13025
- done.catch(() => {
13026
- });
13027
- const pid = await Promise.race([createdPane, done]);
13028
- paneId = pid;
13029
- readiness = done;
13030
- phase = "finalize";
13031
- const row = finalizeAgentRow(db, {
13032
- opts,
13033
- cli,
13034
- paneId: pid,
13035
- cwd: paneCwd,
13036
- staged,
13037
- command,
13038
- commandSource,
13039
- spawner,
13040
- host: profile?.name,
13041
- gateToken
13042
- });
13043
- ownedIncarnation = row.incarnation;
13044
- if (lease !== void 0 && !bindHostChannel(db, lease, { agentId: row.incarnation, paneId: pid })) {
13045
- throw new Error(say("host.channel_lost", lease.host, lease.holder));
13046
- }
13047
- opts.onIdentity?.(row);
13048
- return { pid, row };
13049
- });
14110
+ cwd: paneCwd,
14111
+ ...jobFile !== void 0 ? { jobFile } : {}
14112
+ }) : command;
14113
+ let onCreated = () => {
14114
+ };
14115
+ const createdPane = new Promise((res) => {
14116
+ onCreated = res;
14117
+ });
14118
+ if (carrier && !markCarrierLaunch(db, carrier.token)) {
14119
+ throw new Error(`carrier_jobs: reservation of ${opts.workstream}/${opts.name} is gone`);
14120
+ }
14121
+ const done = mux.spawnAgent({
14122
+ session,
14123
+ window: windowName,
14124
+ name: opts.name,
14125
+ cli,
14126
+ command: paneCommand,
14127
+ commandSource,
14128
+ ...profile ? {} : { cwd: paneCwd },
14129
+ env: paneEnv,
14130
+ onPaneCreated: onCreated,
14131
+ // A gate's pane outlives its command: the result is read after it ends.
14132
+ ...gateToken !== void 0 ? { retainOnExit: true } : {}
14133
+ });
14134
+ done.catch(() => {
14135
+ });
14136
+ const pid = await Promise.race([createdPane, done]);
14137
+ paneId = pid;
14138
+ if (carrier) recordCarrierPane(db, carrier.token, pid);
14139
+ readiness = done;
14140
+ phase = "finalize";
14141
+ const row = finalizeAgentRow(db, {
14142
+ opts,
14143
+ cli,
14144
+ paneId: pid,
14145
+ cwd: paneCwd,
14146
+ staged,
14147
+ command,
14148
+ commandSource,
14149
+ spawner,
14150
+ host: profile?.name,
14151
+ hostTransport: profile?.transport,
14152
+ gateToken,
14153
+ reservation: carrier?.token
14154
+ });
14155
+ ownedIncarnation = row.incarnation;
14156
+ if (carrier && !bindCarrierJobAgent(db, carrier.token, row.incarnation, pid)) {
14157
+ throw new Error(`carrier_jobs: reservation of ${opts.workstream}/${opts.name} is gone`);
14158
+ }
14159
+ if (lease !== void 0 && !bindHostChannel(db, lease, { agentId: row.incarnation, paneId: pid })) {
14160
+ throw new Error(say("host.channel_lost", lease.host, lease.holder));
14161
+ }
14162
+ opts.onIdentity?.(row);
14163
+ return { pid, row, jobFile };
14164
+ })
14165
+ );
13050
14166
  paneId = created.pid;
13051
14167
  agent = created.row;
13052
14168
  phase = "liveness";
14169
+ if (carrier && profile && created.jobFile !== void 0) {
14170
+ await awaitLeaseJob(db, {
14171
+ name: opts.name,
14172
+ token: carrier.token,
14173
+ paneId: created.pid,
14174
+ profile,
14175
+ jobFile: created.jobFile,
14176
+ command,
14177
+ ...opts.signal !== void 0 ? { signal: opts.signal } : {}
14178
+ });
14179
+ }
13053
14180
  if ((await activeMux()).spawnAgentWaitsReady) {
13054
14181
  await readiness;
13055
14182
  } else if (wantsCtl) {
@@ -13095,7 +14222,16 @@ async function spawnAgent3(db, opts) {
13095
14222
  } catch (err) {
13096
14223
  emitSpawnFailed(db, opts, phase, err);
13097
14224
  try {
13098
- await rollbackSpawn(db, opts.name, paneId, staged, ownedIncarnation, opts.workstream, lease);
14225
+ await rollbackSpawn(
14226
+ db,
14227
+ opts.name,
14228
+ paneId,
14229
+ staged,
14230
+ ownedIncarnation,
14231
+ opts.workstream,
14232
+ lease,
14233
+ carrier
14234
+ );
13099
14235
  } catch (cleanupErr) {
13100
14236
  if (err instanceof Error && err.cause === void 0) {
13101
14237
  try {
@@ -13185,65 +14321,6 @@ async function prestageLocked(db, opts, cli, spawner) {
13185
14321
  throw err;
13186
14322
  }
13187
14323
  }
13188
- function remoteRoot(opts, profile) {
13189
- const root = opts.root ?? profile.repo ?? void 0;
13190
- return root === void 0 ? {} : { root };
13191
- }
13192
- async function requireHostSpawn(db, opts) {
13193
- const host = opts.host ?? "";
13194
- const profile = getHost(db, host);
13195
- if (opts.worktree && opts.root === void 0 && profile.repo === null) {
13196
- throw new HostProfileError(
13197
- `host ${host} has no default repo: pass --root <remote repo path> with --worktree`
13198
- );
13199
- }
13200
- const mux = await activeMux();
13201
- if (!mux.runsShellCommand) throw new HostSpawnUnsupportedError(host, mux.name);
13202
- return profile;
13203
- }
13204
- async function prestageRemote(db, opts, cli, spawner, lease, profile) {
13205
- return withAgentLock(db, opts.workstream, opts.name, async () => {
13206
- const placeholder = insertAgent(db, {
13207
- name: opts.name,
13208
- workstream: opts.workstream,
13209
- cli,
13210
- paneId: pendingPaneIdFor(opts.name),
13211
- role: opts.role,
13212
- tab: opts.tab ?? null,
13213
- cwd: null,
13214
- spawner,
13215
- host: profile.name
13216
- });
13217
- try {
13218
- const made = await prepareRemote(db, lease, profile, {
13219
- workstream: opts.workstream,
13220
- name: opts.name,
13221
- worktree: true,
13222
- ...opts.from !== void 0 ? { from: opts.from } : {},
13223
- ...remoteRoot(opts, profile),
13224
- // An unused leftover of a prepare whose answer was lost is taken over;
13225
- // never while a removed agent's recovery record may name that tree.
13226
- reclaim: !recoveriesFor(db, opts.workstream, opts.name).some(
13227
- (r) => r.host === profile.name
13228
- )
13229
- });
13230
- return {
13231
- path: made.cwd,
13232
- base: made.base ?? "",
13233
- root: made.repo ?? "",
13234
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
13235
- incarnation: placeholder.incarnation,
13236
- host: profile.name
13237
- };
13238
- } catch (err) {
13239
- try {
13240
- deleteAgent(db, opts.name, opts.workstream, { incarnation: placeholder.incarnation });
13241
- } catch {
13242
- }
13243
- throw err;
13244
- }
13245
- });
13246
- }
13247
14324
  function spawnerFromEnv(env = process.env) {
13248
14325
  const { MU_MANAGED_AGENT: managed, MU_WORKSTREAM: ws, MU_AGENT_NAME: name } = env;
13249
14326
  if (managed === "1" && ws && name) return `${ws}/${name}`;
@@ -13252,8 +14329,10 @@ function spawnerFromEnv(env = process.env) {
13252
14329
  }
13253
14330
  function finalizeAgentRow(db, args) {
13254
14331
  const { opts, cli, paneId, cwd, staged, command, commandSource, spawner, host, gateToken } = args;
14332
+ const hostTransport = args.hostTransport ?? null;
13255
14333
  if (staged === void 0) {
13256
14334
  return insertAgent(db, {
14335
+ ...args.reservation !== void 0 ? { reservation: args.reservation } : {},
13257
14336
  name: opts.name,
13258
14337
  workstream: opts.workstream,
13259
14338
  cli,
@@ -13265,11 +14344,12 @@ function finalizeAgentRow(db, args) {
13265
14344
  commandSource,
13266
14345
  spawner,
13267
14346
  host: host ?? null,
14347
+ hostTransport,
13268
14348
  gateToken: gateToken ?? null
13269
14349
  });
13270
14350
  }
13271
14351
  const patched = db.prepare(
13272
- `UPDATE agents SET pane_id = ?, cwd = ?, base = ?, command = ?, command_source = ?, spawner = ?, host = ?, repo = ?, updated_at = ?
14352
+ `UPDATE agents SET pane_id = ?, cwd = ?, base = ?, command = ?, command_source = ?, spawner = ?, host = ?, host_transport = ?, repo = ?, updated_at = ?
13273
14353
  WHERE id = ? AND pane_id = ?
13274
14354
  AND name = ?
13275
14355
  AND workstream_id = (SELECT id FROM workstreams WHERE name = ?)`
@@ -13281,6 +14361,7 @@ function finalizeAgentRow(db, args) {
13281
14361
  commandSource,
13282
14362
  spawner,
13283
14363
  host ?? null,
14364
+ hostTransport,
13284
14365
  staged.host !== void 0 ? staged.root : null,
13285
14366
  (/* @__PURE__ */ new Date()).toISOString(),
13286
14367
  staged.incarnation,
@@ -13305,7 +14386,12 @@ function emitSpawnFailed(db, opts, phase, err) {
13305
14386
  } catch {
13306
14387
  }
13307
14388
  }
13308
- async function rollbackSpawn(db, name, paneId, staged, ownedIncarnation, workstream, lease) {
14389
+ function reservationOf(db, incarnation) {
14390
+ if (incarnation === void 0) return void 0;
14391
+ const r = getCarrierJobByAgent(db, incarnation);
14392
+ return r === void 0 ? void 0 : { token: r.token };
14393
+ }
14394
+ async function rollbackSpawn(db, name, paneId, staged, ownedIncarnation, workstream, lease, carrier) {
13309
14395
  let paneGone2 = paneId === void 0;
13310
14396
  if (paneId !== void 0) {
13311
14397
  const mux = await activeMux().catch(() => void 0);
@@ -13336,32 +14422,12 @@ async function rollbackSpawn(db, name, paneId, staged, ownedIncarnation, workstr
13336
14422
  } catch {
13337
14423
  }
13338
14424
  }
14425
+ carrier ??= reservationOf(db, ownedIncarnation);
14426
+ const job = carrier ? await rollbackLeaseJob(db, { workstream, name, token: carrier.token }, paneGone2) : void 0;
13339
14427
  let remoteGone = false;
13340
14428
  if (staged?.host !== void 0) {
13341
- const remote = staged;
13342
- try {
13343
- if (!paneGone2) throw new Error("pane not proven gone");
13344
- await withAgentLock(db, workstream, name, async () => {
13345
- const cur = getAgent(db, name, workstream);
13346
- if (cur !== void 0 && cur.incarnation !== remote.incarnation) return;
13347
- remoteGone = await removeRemoteWorktree(
13348
- db,
13349
- remote.host ?? "",
13350
- `rollback:${workstream}/${name}`,
13351
- remote
13352
- );
13353
- });
13354
- } catch {
13355
- try {
13356
- emitEvent(
13357
- db,
13358
- workstream,
13359
- "agent.spawn_failed",
13360
- `agent spawn rollback ${name}: remote worktree ${staged.path} on ${staged.host} was not removed`
13361
- );
13362
- } catch {
13363
- }
13364
- }
14429
+ if (job !== "kept")
14430
+ remoteGone = await rollbackRemoteWorktree(db, name, workstream, staged, paneGone2);
13365
14431
  } else if (staged !== void 0 && paneGone2) {
13366
14432
  const say2 = (what) => {
13367
14433
  try {
@@ -13405,60 +14471,20 @@ async function rollbackSpawn(db, name, paneId, staged, ownedIncarnation, workstr
13405
14471
  }
13406
14472
  if (ownedIncarnation !== void 0)
13407
14473
  deleteAgent(db, name, workstream, { incarnation: ownedIncarnation }, { preserve: !remoteGone });
13408
- }
13409
- function defaultSpawnLivenessMs() {
13410
- const raw = process.env.MU_SPAWN_LIVENESS_MS;
13411
- if (raw === void 0) return 1500;
13412
- const parsed = Number.parseInt(raw, 10);
13413
- if (Number.isNaN(parsed) || parsed < 0) return 1500;
13414
- return parsed;
13415
- }
13416
- var AUTH_ERROR_PATTERNS = [
13417
- /No API key found for [\w-]+/i,
13418
- /Error: invalid API key/i,
13419
- /Authentication failed/i,
13420
- /401 Unauthorized/i,
13421
- /Could not authenticate/i
13422
- ];
13423
- var EXEC_FAILURE_PATTERN = /^(?:.*: )?(?:command not found|No such file or directory)(?::\s*\S+)?$/i;
13424
- var STARTUP_ERROR_TAIL_LINES = 30;
13425
- function detectSpawnStartupError(scrollback, cliStarted = false) {
13426
- const patterns = cliStarted ? AUTH_ERROR_PATTERNS : [...AUTH_ERROR_PATTERNS, EXEC_FAILURE_PATTERN];
13427
- const lines = scrollback.split(/\r?\n/);
13428
- const tail = lines.slice(Math.max(0, lines.length - STARTUP_ERROR_TAIL_LINES));
13429
- for (const line of tail) {
13430
- for (const pattern of patterns) {
13431
- if (pattern.test(line)) return line;
13432
- }
14474
+ const treeDone = staged?.host === void 0 || remoteGone;
14475
+ if (carrier && (job === "none" || job === "stopped")) {
14476
+ if (treeDone) releaseLeaseReservation(db, carrier.token, job);
14477
+ else keepForTree(db, carrier.token, staged);
13433
14478
  }
13434
- return void 0;
13435
14479
  }
13436
- async function awaitSpawnLiveness(paneId, agentName, command, host) {
13437
- const ms = defaultSpawnLivenessMs();
13438
- if (ms === 0) return false;
13439
- await sleep2(ms);
13440
- return checkSpawnHealth(paneId, agentName, command, void 0, false, host);
13441
- }
13442
- var CLAUDE_TRUST_DIALOG = /Yes, I trust this folder/i;
13443
- function detectClaudeTrustDialog(scrollback) {
13444
- const lines = scrollback.split(/\r?\n/);
13445
- const tail = lines.slice(Math.max(0, lines.length - STARTUP_ERROR_TAIL_LINES));
13446
- return tail.some((line) => CLAUDE_TRUST_DIALOG.test(line));
13447
- }
13448
- async function checkSpawnHealth(paneId, agentName, command, when, cliStarted = false, host) {
13449
- const mux = await activeMux();
13450
- const scrollback = await mux.capture(paneId, { lines: 50 }).catch(() => void 0);
13451
- if (!await mux.paneAlive(paneId)) {
13452
- throw new AgentDiedOnSpawnError(agentName, paneId, scrollback, command, when, host);
13453
- }
13454
- if (scrollback !== void 0) {
13455
- const matchedLine = detectSpawnStartupError(scrollback, cliStarted);
13456
- if (matchedLine !== void 0) {
13457
- throw new AgentSpawnStartupError(agentName, paneId, matchedLine, scrollback, when);
13458
- }
13459
- return detectClaudeTrustDialog(scrollback);
14480
+ function keepForTree(db, token, staged) {
14481
+ try {
14482
+ const reason = `remote worktree ${staged?.path ?? "?"} on ${staged?.host ?? "?"} was not removed`;
14483
+ if (staged === void 0) noteCarrierJob(db, token, reason);
14484
+ else
14485
+ keepCarrierJobForTree(db, token, { path: staged.path, agentId: staged.incarnation, reason });
14486
+ } catch {
13460
14487
  }
13461
- return false;
13462
14488
  }
13463
14489
 
13464
14490
  // src/agents/errors.ts
@@ -13482,6 +14508,59 @@ var AgentExistsError = class extends Error {
13482
14508
  agentName;
13483
14509
  name = "AgentExistsError";
13484
14510
  };
14511
+ var CarrierJobUnstoppedError = class extends Error {
14512
+ constructor(agentName, reservation) {
14513
+ const reason = reservation.reason ?? null;
14514
+ super(
14515
+ reservation.job !== null ? say("carrier.job_unstopped", agentName, reservation.job, reservation.host, reason) : say(
14516
+ "carrier.job_reserved",
14517
+ agentName,
14518
+ reservation.host,
14519
+ reservation.launch,
14520
+ reservation.paneId,
14521
+ reason
14522
+ )
14523
+ );
14524
+ this.agentName = agentName;
14525
+ this.handle = reservation.job;
14526
+ this.host = reservation.host;
14527
+ this.reason = reason;
14528
+ }
14529
+ agentName;
14530
+ name = "CarrierJobUnstoppedError";
14531
+ handle;
14532
+ host;
14533
+ /** Why the reservation was kept without a handle (e.g. a remote tree its rollback did not remove). */
14534
+ reason;
14535
+ };
14536
+ var CarrierJobUnresolvedError = class extends Error {
14537
+ constructor(agentName, workstream, host, facts, keptForTree = false) {
14538
+ super(say("carrier.job_unresolved", agentName, workstream, host, facts, keptForTree));
14539
+ this.agentName = agentName;
14540
+ this.workstream = workstream;
14541
+ this.host = host;
14542
+ this.facts = facts;
14543
+ }
14544
+ agentName;
14545
+ workstream;
14546
+ host;
14547
+ facts;
14548
+ name = "CarrierJobUnresolvedError";
14549
+ };
14550
+ var CarrierJobKeptError = class extends Error {
14551
+ constructor(agentName, workstream, host, why) {
14552
+ super(say("carrier.job_kept", agentName, workstream, host, why));
14553
+ this.agentName = agentName;
14554
+ this.workstream = workstream;
14555
+ this.host = host;
14556
+ this.why = why;
14557
+ }
14558
+ agentName;
14559
+ workstream;
14560
+ host;
14561
+ why;
14562
+ name = "CarrierJobKeptError";
14563
+ };
13485
14564
  var AgentNameExhaustedError = class extends Error {
13486
14565
  constructor(agentName, reason) {
13487
14566
  super(say("agent.name_exhausted", agentName, reason));
@@ -13917,6 +14996,7 @@ export {
13917
14996
  pc,
13918
14997
  muTable,
13919
14998
  isJsonMode,
14999
+ isAgentListArgv,
13920
15000
  isStateVerbArgv,
13921
15001
  usageLines,
13922
15002
  printUsageHuman,
@@ -13951,6 +15031,19 @@ export {
13951
15031
  setMuxForTests,
13952
15032
  resetMux,
13953
15033
  mux_exports,
15034
+ reserveCarrierJob,
15035
+ getCarrierJob,
15036
+ getCarrierJobByAgent,
15037
+ getCarrierJobByName,
15038
+ listCarrierJobsByName,
15039
+ listCarrierJobs,
15040
+ markCarrierLaunch,
15041
+ recordCarrierPane,
15042
+ bindCarrierJobAgent,
15043
+ fillCarrierJob,
15044
+ noteCarrierJob,
15045
+ releaseCarrierJob,
15046
+ abandonCarrierJob,
13954
15047
  HostProfileError,
13955
15048
  HostTemplateError,
13956
15049
  HostNotFoundError,
@@ -13958,17 +15051,22 @@ export {
13958
15051
  HostChannelBusyError,
13959
15052
  HostInUseError,
13960
15053
  HostSpawnUnsupportedError,
15054
+ HostHandbackError,
13961
15055
  HostCheckFailedError,
15056
+ HOST_TRANSPORTS,
15057
+ JOB_HANDLE_RE,
13962
15058
  isValidHostName,
13963
15059
  getHost,
13964
15060
  listHosts,
13965
15061
  addHost,
13966
15062
  removeHost,
13967
15063
  hostStarter,
15064
+ hostMuleStarter,
13968
15065
  HOST_PLACEHOLDERS,
13969
15066
  HOST_ENV_KEYS,
13970
15067
  hostEnvWords,
13971
15068
  validateHostTemplate,
15069
+ placeholderUses,
13972
15070
  expandHostTemplate,
13973
15071
  listHostChannels,
13974
15072
  reserveHostChannel,
@@ -14148,10 +15246,16 @@ export {
14148
15246
  agentOwed,
14149
15247
  hasDoneMark,
14150
15248
  setAgentDone,
15249
+ CarrierStopRefusedError,
15250
+ CarrierStopFailedError,
15251
+ runStop,
14151
15252
  locksDir,
14152
15253
  withFileLock,
14153
15254
  readFileLockMeta,
14154
15255
  withAgentLock,
15256
+ LeaseJobTimeoutError,
15257
+ LeaseJobInvalidError,
15258
+ LeaseJobUnreadableError,
14155
15259
  previewClose,
14156
15260
  closeAgent,
14157
15261
  delegateOutcome,
@@ -14194,6 +15298,7 @@ export {
14194
15298
  listLiveAgents,
14195
15299
  readLiveAgents,
14196
15300
  tmux_exports,
15301
+ defaultSpawnLivenessMs,
14197
15302
  deadStagings,
14198
15303
  resolveCliCommand,
14199
15304
  envVarNameForCli2 as envVarNameForCli,
@@ -14205,9 +15310,11 @@ export {
14205
15310
  isCommandGate,
14206
15311
  spawnAgent3 as spawnAgent,
14207
15312
  spawnerFromEnv,
14208
- defaultSpawnLivenessMs,
14209
15313
  AgentSpawnCliNotFoundError,
14210
15314
  AgentExistsError,
15315
+ CarrierJobUnstoppedError,
15316
+ CarrierJobUnresolvedError,
15317
+ CarrierJobKeptError,
14211
15318
  AgentNameExhaustedError,
14212
15319
  AgentNotFoundError,
14213
15320
  AgentNotInWorkstreamError,
@@ -14250,4 +15357,4 @@ export {
14250
15357
  murmurAvailable,
14251
15358
  readAgentStates
14252
15359
  };
14253
- //# sourceMappingURL=chunk-UCZPJX7K.js.map
15360
+ //# sourceMappingURL=chunk-SFV5RPZW.js.map