@mu-crew/mu 4.1.1 → 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"
@@ -6222,8 +6314,8 @@ function mapMurmurState(token, since) {
6222
6314
  function parseSince(value) {
6223
6315
  return value !== void 0 && /^\d+$/.test(value) ? Number(value) : null;
6224
6316
  }
6225
- async function readMurmur(now2) {
6226
- 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) {
6227
6319
  return murmurCache.stdout;
6228
6320
  }
6229
6321
  let stdout;
@@ -6232,7 +6324,7 @@ async function readMurmur(now2) {
6232
6324
  } catch {
6233
6325
  stdout = null;
6234
6326
  }
6235
- murmurCache = { at: now2, stdout };
6327
+ murmurCache = { at: now3, stdout };
6236
6328
  return stdout;
6237
6329
  }
6238
6330
  function remotePanes(stdout) {
@@ -6443,9 +6535,117 @@ async function readBaseStates(agents, opts, livenessOnly = /* @__PURE__ */ new S
6443
6535
  }
6444
6536
 
6445
6537
  // src/hosts.ts
6446
- import { randomUUID as randomUUID4 } from "crypto";
6538
+ import { randomUUID as randomUUID5 } from "crypto";
6447
6539
  import { readFileSync } from "fs";
6448
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
6449
6649
  var HostProfileError = class extends Error {
6450
6650
  name = "HostProfileError";
6451
6651
  };
@@ -6498,6 +6698,19 @@ var HostSpawnUnsupportedError = class extends Error {
6498
6698
  mux;
6499
6699
  name = "HostSpawnUnsupportedError";
6500
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
+ };
6501
6714
  var HostCheckFailedError = class extends Error {
6502
6715
  constructor(host, failed) {
6503
6716
  super(say("host.check_failed", host, failed));
@@ -6508,6 +6721,8 @@ var HostCheckFailedError = class extends Error {
6508
6721
  failed;
6509
6722
  name = "HostCheckFailedError";
6510
6723
  };
6724
+ var HOST_TRANSPORTS = ["session", "lease"];
6725
+ var JOB_HANDLE_RE = /^[A-Za-z0-9._:@-]{1,128}$/;
6511
6726
  var HOST_NAME_RE = /^[a-z][a-z0-9_-]{0,31}$/;
6512
6727
  function isValidHostName(name) {
6513
6728
  return HOST_NAME_RE.test(name);
@@ -6520,7 +6735,12 @@ function getHost(db, name) {
6520
6735
  function listHosts(db) {
6521
6736
  return db.prepare("SELECT * FROM host_profiles ORDER BY name").all();
6522
6737
  }
6523
- 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
+ };
6524
6744
  if (!isValidHostName(p.name)) {
6525
6745
  throw new HostProfileError(
6526
6746
  `invalid host name ${JSON.stringify(p.name)}: lowercase letters, digits, - and _, starting with a letter (max 32)`
@@ -6532,17 +6752,26 @@ function addHost(db, p) {
6532
6752
  if (p.repo !== null && p.repo.trim() === "") {
6533
6753
  throw new HostProfileError("--repo must not be empty");
6534
6754
  }
6535
- for (const f of ["shell", "exec", "fetch"]) {
6536
- try {
6537
- validateHostTemplate(p[f]);
6538
- } catch (e) {
6539
- if (e instanceof HostTemplateError) throw new HostTemplateError(`--${f}: ${e.message}`);
6540
- throw e;
6541
- }
6755
+ if (!HOST_TRANSPORTS.includes(p.transport)) {
6756
+ throw new HostProfileError(
6757
+ `invalid --transport ${JSON.stringify(p.transport)}: ${HOST_TRANSPORTS.join(" or ")}`
6758
+ );
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
+ );
6542
6769
  }
6770
+ const problem = profileTemplateProblem(p);
6771
+ if (problem !== void 0) throw new HostTemplateError(problem);
6543
6772
  try {
6544
6773
  db.prepare(
6545
- "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)"
6546
6775
  ).run(p);
6547
6776
  } catch (e) {
6548
6777
  if (e.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
@@ -6563,13 +6792,53 @@ function removeHost(db, name) {
6563
6792
  function hostStarter(host) {
6564
6793
  const ssh = "ssh -o ControlMaster=no -o ControlPath=none";
6565
6794
  return {
6566
- // 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.
6567
6799
  shell: `${ssh} -o StreamLocalBindUnlink=yes -o ExitOnForwardFailure=yes -L {local_sock}:{remote_sock} ${host} -t 'cd {cwd:sh} && {env} "$SHELL" -ilc {cmd:sh}'`,
6568
6800
  exec: `${ssh} -o BatchMode=yes ${host} {cmd}`,
6569
6801
  fetch: `git fetch ssh://${host}{cwd} HEAD`
6570
6802
  };
6571
6803
  }
6572
- 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
+ };
6573
6842
  var HOST_ENV_KEYS = [
6574
6843
  "MU_MANAGED_AGENT",
6575
6844
  "MU_AGENT_NAME",
@@ -6594,7 +6863,7 @@ function hostEnvWords(env) {
6594
6863
  return words.join(" ");
6595
6864
  }
6596
6865
  var KNOWN = `${HOST_PLACEHOLDERS.map((p) => `{${p}}`).join(" ")}; {name:sh} quotes for a second shell`;
6597
- function parseTemplate(t) {
6866
+ function parseTemplate(t, scope) {
6598
6867
  const out = [];
6599
6868
  let lit = "";
6600
6869
  let ctx = "none";
@@ -6630,6 +6899,11 @@ function parseTemplate(t) {
6630
6899
  "{env:sh} is refused: {env} is already text for the inner shell"
6631
6900
  );
6632
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
+ }
6633
6907
  if (ctx === "dq" && subst) {
6634
6908
  throw new HostTemplateError(`{${name}} inside $(...) or backticks in double quotes`);
6635
6909
  }
@@ -6668,12 +6942,44 @@ function parseTemplate(t) {
6668
6942
  flush();
6669
6943
  return out;
6670
6944
  }
6671
- function validateHostTemplate(template) {
6672
- 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;
6673
6979
  }
6674
- function expandHostTemplate(template, values) {
6980
+ function expandHostTemplate(template, values, scope) {
6675
6981
  let out = "";
6676
- for (const tok of parseTemplate(template)) {
6982
+ for (const tok of parseTemplate(template, scope)) {
6677
6983
  if ("lit" in tok) {
6678
6984
  out += tok.lit;
6679
6985
  continue;
@@ -6722,8 +7028,8 @@ function pidProvenGone(pid, start) {
6722
7028
  return e.code === "ESRCH";
6723
7029
  }
6724
7030
  if (start === null) return false;
6725
- const now2 = pidStart(pid);
6726
- return now2 !== null && now2 !== start;
7031
+ const now3 = pidStart(pid);
7032
+ return now3 !== null && now3 !== start;
6727
7033
  }
6728
7034
  function groupAlive(pgid, start) {
6729
7035
  try {
@@ -6732,8 +7038,8 @@ function groupAlive(pgid, start) {
6732
7038
  return e.code !== "ESRCH";
6733
7039
  }
6734
7040
  if (start === null) return true;
6735
- const now2 = pidStart(pgid);
6736
- return now2 === null || now2 === start;
7041
+ const now3 = pidStart(pgid);
7042
+ return now3 === null || now3 === start;
6737
7043
  }
6738
7044
  async function probeAttached(db, host, paneAlive3) {
6739
7045
  if (paneAlive3 === void 0) return [];
@@ -6775,7 +7081,7 @@ async function admitHostChannel(db, host, holder, opts = {}) {
6775
7081
  async function admit(db, host, holder, opts) {
6776
7082
  getHost(db, host);
6777
7083
  const dead = await probeAttached(db, host, opts.paneAlive);
6778
- const token = randomUUID4();
7084
+ const token = randomUUID5();
6779
7085
  const profile = db.transaction(() => {
6780
7086
  const profile2 = getHost(db, host);
6781
7087
  reclaim(db, host, dead);
@@ -6830,13 +7136,27 @@ async function withHostChannel(db, host, holder, run2, opts = {}) {
6830
7136
  }
6831
7137
  }
6832
7138
  var HOST_COMMAND_TIMEOUT_MS = 2e4;
7139
+ var HOST_GRACE_MS = 5e3;
6833
7140
  async function runLeasedTemplate(db, lease, profile, field, values, timeoutMs, localCwd) {
6834
- const expanded = expandHostTemplate(profile[field], values);
6835
- 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], {
6836
7146
  timeoutMs,
7147
+ // A carrier interrupted by the timeout needs time to stop what it dispatched.
7148
+ graceMs: HOST_GRACE_MS,
6837
7149
  ...localCwd !== void 0 ? { cwd: localCwd } : {},
6838
7150
  onSpawn: (pgid) => recordGroup(db, lease, pgid)
6839
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;
6840
7160
  }
6841
7161
  async function runHostTemplate(db, host, field, holder, values, opts = {}) {
6842
7162
  const timeoutMs = opts.timeoutMs ?? HOST_COMMAND_TIMEOUT_MS;
@@ -6854,14 +7174,22 @@ async function checkHost(db, host, opts = {}) {
6854
7174
  getHost(db, host);
6855
7175
  const timeoutMs = opts.timeoutMs ?? HOST_COMMAND_TIMEOUT_MS;
6856
7176
  const steps = [];
6857
- const login = (cmd) => `"\${SHELL:-sh}" -ilc ${shellQuote(cmd)}`;
7177
+ let warnings = [];
7178
+ let handedBack = false;
6858
7179
  await withHostChannel(
6859
7180
  db,
6860
7181
  host,
6861
7182
  `check:${process.pid}`,
6862
7183
  async (profile, lease) => {
6863
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));
6864
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
+ }
6865
7193
  let detail;
6866
7194
  let ok = false;
6867
7195
  try {
@@ -6879,6 +7207,12 @@ async function checkHost(db, host, opts = {}) {
6879
7207
  ok = good1 !== void 0;
6880
7208
  detail = good1 ?? (r.exitCode === null ? `timed out after ${timeoutMs}ms` : r.exitCode === 0 ? unmet : `exit ${r.exitCode}${why === "" ? "" : `: ${why}`}`);
6881
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
+ }
6882
7216
  if (!(e instanceof HostTemplateError)) throw e;
6883
7217
  detail = e.message;
6884
7218
  }
@@ -6905,10 +7239,19 @@ async function checkHost(db, host, opts = {}) {
6905
7239
  );
6906
7240
  } else {
6907
7241
  for (const s of ["repo", "pi", "extension"]) {
6908
- 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
+ });
6909
7247
  }
6910
7248
  }
6911
- 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) {
6912
7255
  const { name, workstream } = opts.agent;
6913
7256
  const p = await ctlProbe(
6914
7257
  ctlSocketPath(workstream, name, dirname4(db.name)),
@@ -6930,11 +7273,62 @@ async function checkHost(db, host, opts = {}) {
6930
7273
  },
6931
7274
  opts
6932
7275
  );
6933
- 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
+ ];
6934
7328
  }
6935
7329
 
6936
7330
  // src/op-context.ts
6937
- import { randomUUID as randomUUID5 } from "crypto";
7331
+ import { randomUUID as randomUUID6 } from "crypto";
6938
7332
  var DEFAULT_CTX = { group_id: null, actor: null, intent: null, applying: 0 };
6939
7333
  function currentOpContext(db) {
6940
7334
  const row = readCtx(db) ?? DEFAULT_CTX;
@@ -7023,7 +7417,7 @@ function withOpContext(db, ctx, fn) {
7023
7417
  return withCtxRow(
7024
7418
  db,
7025
7419
  (previous) => {
7026
- 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();
7027
7421
  return {
7028
7422
  group_id: groupId,
7029
7423
  // An explicit undefined inherits; only a provided value overrides.
@@ -7058,15 +7452,15 @@ function reapAgentRow(db, agentId, name, opts = {}) {
7058
7452
  "INSERT INTO task_notes (task_id, author, content, created_at, kind) VALUES (?, 'reaper', ?, ?, 'note')"
7059
7453
  );
7060
7454
  for (const t of stuck) {
7061
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
7062
- reopen.run(now2, t.taskId);
7455
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
7456
+ reopen.run(now3, t.taskId);
7063
7457
  withOpContext(
7064
7458
  db,
7065
7459
  { intent: "task.note", actor: "reaper" },
7066
7460
  () => note.run(
7067
7461
  t.taskId,
7068
7462
  `[reaper] previous owner ${name} gone (agent removed); status reverted IN_PROGRESS \u2192 OPEN, owner cleared`,
7069
- now2
7463
+ now3
7070
7464
  )
7071
7465
  );
7072
7466
  }
@@ -7221,7 +7615,10 @@ function withTaskFence(db, workstream, id, expected, fn, previewCommand) {
7221
7615
  }).immediate();
7222
7616
  }
7223
7617
  function agentRevision(db, workstream, name, recoveryId) {
7224
- 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
+ }
7225
7622
  const agent = db.prepare(
7226
7623
  `SELECT a.id, a.pane_id, a.cli, a.role, a.tab, a.cwd, a.base, a.command, a.command_source,
7227
7624
  a.spawner, a.host, a.collected_head, a.repo, a.gate_token, a.created_at
@@ -7237,19 +7634,36 @@ function agentRevision(db, workstream, name, recoveryId) {
7237
7634
  `SELECT path, root, base, last_collected_head, created_at FROM worktrees
7238
7635
  WHERE workstream = ? AND agent = ? ORDER BY path`
7239
7636
  ).all(workstream, name);
7240
- if (agent === void 0 && worktrees.length === 0) return recoveryRevision(db, workstream, name);
7241
- 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();
7242
7643
  return digest.readUIntBE(0, 6);
7243
7644
  }
7244
- 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) {
7245
7652
  const rows = db.prepare(
7246
7653
  `SELECT * FROM remote_recovery WHERE workstream = ? AND name = ?${recoveryId === void 0 ? "" : " AND agent_id = ?"} ORDER BY agent_id`
7247
7654
  ).all(...recoveryId === void 0 ? [workstream, name] : [workstream, name, recoveryId]);
7248
- 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
+ }
7249
7659
  const live2 = db.prepare(
7250
7660
  "SELECT a.id FROM agents a JOIN workstreams w ON w.id = a.workstream_id WHERE w.name = ? AND a.name = ?"
7251
7661
  ).get(workstream, name);
7252
- 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();
7253
7667
  return digest.readUIntBE(0, 6);
7254
7668
  }
7255
7669
 
@@ -7531,8 +7945,8 @@ function taskIdFor(db, localId, workstream) {
7531
7945
  const row = db.prepare("SELECT id FROM tasks WHERE workstream_id = ? AND local_id = ?").get(wsId, localId);
7532
7946
  return row ? row.id : null;
7533
7947
  }
7534
- function touchTask(db, taskId, now2) {
7535
- const ts = now2 ?? (/* @__PURE__ */ new Date()).toISOString();
7948
+ function touchTask(db, taskId, now3) {
7949
+ const ts = now3 ?? (/* @__PURE__ */ new Date()).toISOString();
7536
7950
  db.prepare("UPDATE tasks SET updated_at = ? WHERE id = ? AND updated_at < ?").run(ts, taskId, ts);
7537
7951
  }
7538
7952
 
@@ -7918,13 +8332,13 @@ function addBlockEdgeImpl(db, workstream, blocked, blocker) {
7918
8332
  if (wouldCreateCycle(db, blockerId, blockedId)) {
7919
8333
  throw cycleErrorFor(db, blocker, blocked, blockerId, blockedId, blockedRow.workstreamName);
7920
8334
  }
7921
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
8335
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
7922
8336
  const added = db.transaction(() => {
7923
8337
  const result = db.prepare(
7924
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 = ?)"
7925
- ).run(blockerId, blockedId, now2, blockerId, blockedId);
8339
+ ).run(blockerId, blockedId, now3, blockerId, blockedId);
7926
8340
  if (result.changes > 0) {
7927
- touchTask(db, blockedId, now2);
8341
+ touchTask(db, blockedId, now3);
7928
8342
  return true;
7929
8343
  }
7930
8344
  return false;
@@ -8010,11 +8424,11 @@ function reparentTaskImpl(db, taskLocalId, blockers, scope) {
8010
8424
  const insertEdge = db.prepare(
8011
8425
  "INSERT INTO task_edges (from_task_id, to_task_id, created_at) VALUES (?, ?, ?)"
8012
8426
  );
8013
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
8427
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
8014
8428
  for (const blockerId of blockerIds) {
8015
- insertEdge.run(blockerId, taskSurrogateId, now2);
8429
+ insertEdge.run(blockerId, taskSurrogateId, now3);
8016
8430
  }
8017
- touchTask(db, taskSurrogateId, now2);
8431
+ touchTask(db, taskSurrogateId, now3);
8018
8432
  return { removedEdges: removed.changes, addedEdges: blockerIds.length };
8019
8433
  }).immediate();
8020
8434
  }
@@ -8926,9 +9340,9 @@ async function teardownWorkstream(db, opts) {
8926
9340
  db,
8927
9341
  { intent: "workstream.teardown", group: "new" },
8928
9342
  () => db.transaction(() => {
8929
- const now2 = workstreamRevision(db, opts.workstream);
8930
- if (now2 !== opts.ifRevision) {
8931
- 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);
8932
9346
  }
8933
9347
  for (const a of dead) {
8934
9348
  if (!sameRow2.get(a.incarnation, a.name, a.paneId, opts.workstream)) continue;
@@ -9137,7 +9551,7 @@ function addTaskImpl(db, opts) {
9137
9551
  if (existing) {
9138
9552
  throw new TaskExistsError(opts.localId);
9139
9553
  }
9140
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
9554
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
9141
9555
  const insertResult = db.prepare(
9142
9556
  `INSERT INTO tasks (workstream_id, local_id, title, status, substate, kind, wave, impact, effort_days, scored, created_at, updated_at)
9143
9557
  VALUES (?, ?, ?, 'OPEN', ?, ?, ?, ?, ?, ?, ?, ?)`
@@ -9151,8 +9565,8 @@ function addTaskImpl(db, opts) {
9151
9565
  opts.impact ?? DEFAULT_IMPACT,
9152
9566
  opts.effortDays ?? DEFAULT_EFFORT_DAYS,
9153
9567
  opts.impact !== void 0 || opts.effortDays !== void 0 ? 1 : 0,
9154
- now2,
9155
- now2
9568
+ now3,
9569
+ now3
9156
9570
  );
9157
9571
  const newTaskId = Number(insertResult.lastInsertRowid);
9158
9572
  if (opts.blockedBy && opts.blockedBy.length > 0) {
@@ -9176,7 +9590,7 @@ function addTaskImpl(db, opts) {
9176
9590
  "INSERT INTO task_edges (from_task_id, to_task_id, created_at) VALUES (?, ?, ?)"
9177
9591
  );
9178
9592
  for (const blocker of canonicalBlockers) {
9179
- insertEdge.run(blocker.id, newTaskId, now2);
9593
+ insertEdge.run(blocker.id, newTaskId, now3);
9180
9594
  }
9181
9595
  }
9182
9596
  const row = getTask(db, opts.localId, opts.workstream);
@@ -9231,7 +9645,7 @@ function addBlocksEdges(db, input, blocks, workstream) {
9231
9645
  }
9232
9646
  umbrellas.set(row.id, local);
9233
9647
  }
9234
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
9648
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
9235
9649
  const insertEdge = db.prepare(
9236
9650
  "INSERT INTO task_edges (from_task_id, to_task_id, created_at) VALUES (?, ?, ?)"
9237
9651
  );
@@ -9242,10 +9656,10 @@ function addBlocksEdges(db, input, blocks, workstream) {
9242
9656
  if (wouldCreateCycle(db, newId, umbrellaId)) {
9243
9657
  throw cycleErrorFor(db, t.id, umbrella, newId, umbrellaId, workstream);
9244
9658
  }
9245
- insertEdge.run(newId, umbrellaId, now2);
9659
+ insertEdge.run(newId, umbrellaId, now3);
9246
9660
  }
9247
9661
  }
9248
- for (const umbrellaId of umbrellas.keys()) touchTask(db, umbrellaId, now2);
9662
+ for (const umbrellaId of umbrellas.keys()) touchTask(db, umbrellaId, now3);
9249
9663
  }
9250
9664
  function lookupEdgeTask(db, wsId, localId, workstream) {
9251
9665
  const row = db.prepare(
@@ -9278,18 +9692,18 @@ function insertNote(db, taskLocalId, content, opts, touch = true) {
9278
9692
  }
9279
9693
  const taskId = taskIdFor(db, task.name, task.workstreamName);
9280
9694
  if (taskId === null) throw new TaskNotFoundError(taskLocalId, task.workstreamName);
9281
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
9695
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
9282
9696
  db.transaction(() => {
9283
9697
  const r = db.prepare(
9284
9698
  "INSERT INTO task_notes (task_id, author, content, created_at, kind) VALUES (?, ?, ?, ?, ?)"
9285
- ).run(taskId, opts.author ?? null, content, now2, opts.kind ?? "note");
9286
- if (touch) touchTask(db, taskId, now2);
9699
+ ).run(taskId, opts.author ?? null, content, now3, opts.kind ?? "note");
9700
+ if (touch) touchTask(db, taskId, now3);
9287
9701
  return r;
9288
9702
  }).immediate();
9289
9703
  return {
9290
9704
  author: opts.author ?? null,
9291
9705
  content,
9292
- createdAt: now2,
9706
+ createdAt: now3,
9293
9707
  kind: opts.kind ?? "note"
9294
9708
  };
9295
9709
  }
@@ -9757,7 +10171,7 @@ async function claimTaskImpl(db, localId, opts) {
9757
10171
  throw new ClaimerNotRegisteredError(agentName, null);
9758
10172
  }
9759
10173
  }
9760
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
10174
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
9761
10175
  let result;
9762
10176
  try {
9763
10177
  result = db.prepare(
@@ -9771,7 +10185,7 @@ async function claimTaskImpl(db, localId, opts) {
9771
10185
  WHERE local_id = ?
9772
10186
  AND workstream_id = (SELECT id FROM workstreams WHERE name = ?)
9773
10187
  AND (owner_id IS NULL OR owner_id = ?)`
9774
- ).run(claimerRow.id, now2, localId, opts.workstream, claimerRow.id);
10188
+ ).run(claimerRow.id, now3, localId, opts.workstream, claimerRow.id);
9775
10189
  } catch (err) {
9776
10190
  if (err instanceof Error && err.message === GUARD_ALREADY_OWNED) {
9777
10191
  throw new TaskAlreadyOwnedError(localId, before.ownerName ?? "<unknown>");
@@ -9827,7 +10241,7 @@ async function claimSelf(db, localId, opts) {
9827
10241
  if (!before) throw new TaskNotFoundError(localId, opts.workstream);
9828
10242
  assertTaskRevision(db, before.workstreamName, localId, opts.ifRevision);
9829
10243
  assertNotParked(before, opts);
9830
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
10244
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
9831
10245
  const result = db.prepare(
9832
10246
  `UPDATE tasks
9833
10247
  SET status = CASE WHEN status = 'OPEN' THEN 'IN_PROGRESS' ELSE status END,
@@ -9836,7 +10250,7 @@ async function claimSelf(db, localId, opts) {
9836
10250
  WHERE local_id = ?
9837
10251
  AND workstream_id = (SELECT id FROM workstreams WHERE name = ?)
9838
10252
  AND owner_id IS NULL`
9839
- ).run(now2, localId, before.workstreamName);
10253
+ ).run(now3, localId, before.workstreamName);
9840
10254
  if (result.changes === 0) {
9841
10255
  throw new TaskAlreadyOwnedError(localId, before.ownerName ?? "<unknown>");
9842
10256
  }
@@ -10301,7 +10715,8 @@ function remoteShellCommand(profile, a) {
10301
10715
  cmd: a.command,
10302
10716
  local_sock: a.localSock,
10303
10717
  remote_sock: remoteSock,
10304
- cwd: a.cwd
10718
+ cwd: a.cwd,
10719
+ ...a.jobFile !== void 0 ? { job_file: a.jobFile } : {}
10305
10720
  });
10306
10721
  const unset = LOCAL_ONLY_ENV.map((k) => `-u ${k}`).join(" ");
10307
10722
  return `exec env ${unset} /bin/sh -c ${shellQuote(filled)}`;
@@ -10699,16 +11114,16 @@ function registerOwnedWait(db, caller, refs) {
10699
11114
  for (const r of add) if (r.kind === "agent") unmark.run(caller, doneKey(r.workstream, r.id));
10700
11115
  const baselines = { ...old?.baselines ?? {} };
10701
11116
  for (const [k, b] of incoming) baselines[k] = mergeBaseline(baselines[k], b);
10702
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
11117
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
10703
11118
  const baselinesJson = JSON.stringify(baselines);
10704
11119
  if (row === void 0) {
10705
11120
  db.prepare(
10706
11121
  "INSERT INTO wait_registrations (caller, refs_json, state, generation, updated_at, baselines_json) VALUES (?, ?, 'armed', 0, ?, ?)"
10707
- ).run(caller, JSON.stringify(merged), now2, baselinesJson);
11122
+ ).run(caller, JSON.stringify(merged), now3, baselinesJson);
10708
11123
  } else if (merged.length !== have.length || baselinesJson !== row.baselines_json) {
10709
11124
  db.prepare(
10710
11125
  "UPDATE wait_registrations SET refs_json = ?, baselines_json = ?, updated_at = ? WHERE caller = ?"
10711
- ).run(JSON.stringify(merged), baselinesJson, now2, caller);
11126
+ ).run(JSON.stringify(merged), baselinesJson, now3, caller);
10712
11127
  }
10713
11128
  const after = select(db, caller);
10714
11129
  if (after === void 0) throw new WaitRegistrationError("wait registration vanished");
@@ -10916,8 +11331,8 @@ async function closeRecovery(db, rec, opts) {
10916
11331
  }
10917
11332
 
10918
11333
  // src/agents/gate.ts
10919
- import { randomUUID as randomUUID6 } from "crypto";
10920
- var newGateToken = () => randomUUID6();
11334
+ import { randomUUID as randomUUID7 } from "crypto";
11335
+ var newGateToken = () => randomUUID7();
10921
11336
  var TOKEN_RE = /^[0-9a-f-]{36}$/;
10922
11337
  var DIR = `"$HOME"/.local/state/mu-next/remote-gates`;
10923
11338
  var sh = (body) => `sh -c ${shellQuote(body)}`;
@@ -10988,6 +11403,7 @@ async function readGate(db, agent) {
10988
11403
  { paneAlive: localPaneAlive }
10989
11404
  );
10990
11405
  } catch (e) {
11406
+ if (e instanceof HostHandbackError) throw e;
10991
11407
  return unknown2(`result read: ${e instanceof Error ? e.message : String(e)}`);
10992
11408
  }
10993
11409
  if (!sameRow(db, agent)) return { kind: "dead", reason: "the agent was replaced" };
@@ -11021,6 +11437,71 @@ async function cleanGateResult(db, agent) {
11021
11437
  }
11022
11438
  }
11023
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
+
11024
11505
  // src/agents/lock.ts
11025
11506
  import { createHash as createHash4 } from "crypto";
11026
11507
  import { dirname as dirname8, join as join7 } from "path";
@@ -11222,7 +11703,312 @@ async function withSpawnAttemptLock(db, taskId, attemptKey, fn, onBusy) {
11222
11703
  );
11223
11704
  }
11224
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
+
11225
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
+ }
11226
12012
  function remoteOf(agent) {
11227
12013
  if (agent?.host == null || agent.cwd === null || agent.base === null) return null;
11228
12014
  return {
@@ -11247,17 +12033,18 @@ function worktreeOf(db, workstream, name, agent) {
11247
12033
  }
11248
12034
  async function previewClose(db, name, opts) {
11249
12035
  const discard = opts.discard === true;
12036
+ const abandonJob = opts.abandonJob === true;
11250
12037
  for (let attempt = 0; attempt < 3; attempt++) {
11251
12038
  const agent = getAgentForCleanup(db, name, opts.workstream);
11252
12039
  const row = worktreeOf(db, opts.workstream, name, agent);
11253
12040
  const state = row === void 0 ? void 0 : await worktreeState(row.path, row.base, row.root);
11254
12041
  const preview = db.transaction(() => {
11255
- const now2 = getAgentForCleanup(db, name, opts.workstream);
11256
- const nowRow = worktreeOf(db, opts.workstream, name, now2);
11257
- 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) {
11258
12045
  return void 0;
11259
12046
  }
11260
- 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;
11261
12048
  if (rec === void 0 && opts.recoveryId !== void 0) {
11262
12049
  return {
11263
12050
  agentName: name,
@@ -11270,11 +12057,14 @@ async function previewClose(db, name, opts) {
11270
12057
  worktree: null,
11271
12058
  remote: null,
11272
12059
  recovery: null,
12060
+ carrierJob: null,
11273
12061
  refusesDirty: false,
11274
12062
  discard,
11275
- keepBranch: opts.keepBranch === true
12063
+ keepBranch: opts.keepBranch === true,
12064
+ abandonJob
11276
12065
  };
11277
12066
  }
12067
+ const carrierJob = carrierJobOf(db, opts.workstream, name);
11278
12068
  if (rec !== void 0) {
11279
12069
  return {
11280
12070
  agentName: name,
@@ -11286,25 +12076,29 @@ async function previewClose(db, name, opts) {
11286
12076
  ownedTasks: [],
11287
12077
  worktree: null,
11288
12078
  ...recoveryPreview(db, rec),
12079
+ carrierJob,
11289
12080
  refusesDirty: false,
11290
12081
  discard,
11291
- keepBranch: opts.keepBranch === true
12082
+ keepBranch: opts.keepBranch === true,
12083
+ abandonJob
11292
12084
  };
11293
12085
  }
11294
12086
  return {
11295
12087
  agentName: name,
11296
12088
  workstream: opts.workstream,
11297
- kind: now2 ? "agent" : nowRow ? "orphan-worktree" : "none",
12089
+ kind: now3 ? "agent" : nowRow ? "orphan-worktree" : carrierJob !== null ? "lease-job" : "none",
11298
12090
  revision: opts.recoveryId === void 0 ? agentRevision(db, opts.workstream, name) : 0,
11299
- incarnation: now2?.incarnation ?? null,
11300
- paneId: now2?.paneId ?? null,
11301
- ownedTasks: now2 ? ownedTasksOf(db, now2) : [],
12091
+ incarnation: now3?.incarnation ?? null,
12092
+ paneId: now3?.paneId ?? null,
12093
+ ownedTasks: now3 ? ownedTasksOf(db, now3) : [],
11302
12094
  worktree: nowRow && state ? { path: nowRow.path, state } : null,
11303
- remote: remoteOf(now2),
12095
+ remote: remoteOf(now3),
11304
12096
  recovery: null,
12097
+ carrierJob,
11305
12098
  refusesDirty: state === "dirty" && !discard,
11306
12099
  discard,
11307
- keepBranch: opts.keepBranch === true
12100
+ keepBranch: opts.keepBranch === true,
12101
+ abandonJob
11308
12102
  };
11309
12103
  })();
11310
12104
  if (preview !== void 0) return preview;
@@ -11325,29 +12119,55 @@ async function closeLocked(db, name, opts) {
11325
12119
  };
11326
12120
  const agent = getAgentForCleanup(db, name, opts.workstream);
11327
12121
  const orphan = agent ? void 0 : getWorktreeFor(db, opts.workstream, name);
11328
- if (opts.recoveryId !== void 0 || agent === void 0 && orphan === void 0) {
11329
- const rec = selectRemoteRecovery(db, opts.workstream, name, opts.recoveryId);
11330
- if (rec === void 0) return { killedPane: false, deletedRow: false, worktree: null };
11331
- fence();
11332
- return closeRecovery(db, rec, {
11333
- keepBranch: opts.keepBranch === true,
11334
- force: opts.force === true
11335
- });
11336
- }
11337
- if (!agent) {
11338
- if (orphan === void 0) return { killedPane: false, deletedRow: false, worktree: null };
11339
- fence();
11340
- const worktree2 = await releaseWorktree(db, orphan, { discard, force: opts.force === true });
11341
- emitEvent(db, opts.workstream, "agent.close", closeLine(name, void 0, worktree2));
11342
- 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) };
11343
12163
  }
11344
12164
  const row = worktreeOf(db, opts.workstream, name, agent);
11345
12165
  if (row !== void 0) await assertReleasable(row, { discard });
11346
12166
  const mux = await activeMux().catch(() => void 0);
11347
12167
  const running = await runInFlight(agent);
11348
12168
  const replaced = () => {
11349
- const now2 = getAgentForCleanup(db, name, opts.workstream);
11350
- return now2 !== void 0 && !sameIncarnation(now2, agent);
12169
+ const now3 = getAgentForCleanup(db, name, opts.workstream);
12170
+ return now3 !== void 0 && !sameIncarnation(now3, agent);
11351
12171
  };
11352
12172
  if (replaced()) throw new AgentReplacedError(name, opts.workstream);
11353
12173
  fence();
@@ -11374,6 +12194,9 @@ async function closeLocked(db, name, opts) {
11374
12194
  throw new AgentPaneStillAliveError(name, agent.workstreamName, agent.paneId, alive, why);
11375
12195
  }
11376
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);
11377
12200
  let worktree = null;
11378
12201
  let remoteHandled = false;
11379
12202
  if (row !== void 0)
@@ -11405,16 +12228,89 @@ async function closeLocked(db, name, opts) {
11405
12228
  { preserve: !remoteHandled }
11406
12229
  );
11407
12230
  if (!deletedRow && replaced()) throw new AgentReplacedError(name, opts.workstream);
12231
+ if (lease !== void 0) releaseLeaseClose(db, lease);
11408
12232
  if (deletedRow && !running && openAtClose === 0)
11409
12233
  setAgentDone(db, agent.workstreamName, name, true);
11410
- emitEvent(db, agent.workstreamName, "agent.close", closeLine(name, agent.paneId, worktree));
11411
- return { killedPane: true, deletedRow, worktree };
11412
- }
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
+ );
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
+ });
11413
12304
  function sameIncarnation(a, b) {
11414
12305
  return a.incarnation === b.incarnation && a.paneId === b.paneId;
11415
12306
  }
11416
- function closeLine(name, paneId, wt) {
12307
+ function closeLine(name, paneId, wt, jobs = []) {
11417
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
+ }
11418
12314
  if (wt !== null) {
11419
12315
  bits.push(`worktree ${wt.action}${wt.branch === void 0 ? "" : ` as ${wt.branch}`}`);
11420
12316
  }
@@ -11434,7 +12330,7 @@ function delegateOutcome(agent, timedOut) {
11434
12330
  }
11435
12331
 
11436
12332
  // src/agents/observe.ts
11437
- import { dirname as dirname9 } from "path";
12333
+ import { dirname as dirname10 } from "path";
11438
12334
  function observationFromReading(agent, reading) {
11439
12335
  return {
11440
12336
  workstream: agent.workstreamName,
@@ -11477,7 +12373,7 @@ function observationFromGate(agent, gate) {
11477
12373
  async function observeAgentStates(db, agents) {
11478
12374
  const gates = agents.filter((a) => a.gateToken !== null);
11479
12375
  const rest = agents.filter((a) => a.gateToken === null);
11480
- const readings = await readAgentStates(rest, { stateDir: dirname9(db.name) });
12376
+ const readings = await readAgentStates(rest, { stateDir: dirname10(db.name) });
11481
12377
  const out = new Map(
11482
12378
  rest.map((a) => [agentKey(a), observationFromReading(a, readings.get(agentKey(a)))])
11483
12379
  );
@@ -11604,7 +12500,7 @@ async function watchAgentRun(agent, sock, signal, opts) {
11604
12500
 
11605
12501
  // src/delegates.ts
11606
12502
  import { createHash as createHash5 } from "crypto";
11607
- import { dirname as dirname10, join as join8 } from "path";
12503
+ import { dirname as dirname11, join as join8 } from "path";
11608
12504
 
11609
12505
  // src/delegate-verdict.ts
11610
12506
  var VERDICT_LABELS = ["CONFIRMED", "REFUTED", "UNVERIFIED"];
@@ -11643,11 +12539,11 @@ ${body}`;
11643
12539
 
11644
12540
  // src/delegates.ts
11645
12541
  function delegateSessionDir(dbPath, workstream, task) {
11646
- return join8(dirname10(dbPath), "delegate-sessions", workstream, task);
12542
+ return join8(dirname11(dbPath), "delegate-sessions", workstream, task);
11647
12543
  }
11648
12544
  function delegateProcessDir(dbPath, callId) {
11649
12545
  return join8(
11650
- dirname10(dbPath),
12546
+ dirname11(dbPath),
11651
12547
  "delegate-process",
11652
12548
  createHash5("sha256").update(callId).digest("hex").slice(0, 16)
11653
12549
  );
@@ -11778,7 +12674,7 @@ function taskIdFor2(callId, label) {
11778
12674
  const h = createHash5("sha256").update(callId).digest("hex").slice(0, 8);
11779
12675
  return `delegate_${stem ? `${stem}_` : ""}${h}`;
11780
12676
  }
11781
- var now = () => (/* @__PURE__ */ new Date()).toISOString();
12677
+ var now2 = () => (/* @__PURE__ */ new Date()).toISOString();
11782
12678
  function registerDelegateCall(db, opts) {
11783
12679
  const state = opts.state ?? "queued";
11784
12680
  return withOpContext(
@@ -11836,7 +12732,7 @@ function registerDelegateCall(db, opts) {
11836
12732
  opts.record?.workstream ?? null,
11837
12733
  opts.record?.task ?? null,
11838
12734
  opts.request.label ?? null,
11839
- now(),
12735
+ now2(),
11840
12736
  canonical2(opts.request)
11841
12737
  );
11842
12738
  return { call: mustGet(db, opts.callId), created: true };
@@ -11862,7 +12758,7 @@ function markDelegate(db, callId, to, extra = {}) {
11862
12758
  SET state = ?, updated_at = ?, agent = COALESCE(?, agent),
11863
12759
  error = CASE WHEN ? = 'refused' THEN COALESCE(?, error) ELSE NULL END
11864
12760
  WHERE call_id = ? AND state = ?`
11865
- ).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);
11866
12762
  if (r.changes !== 1) throw new DelegateStateError(callId, mustGet(db, callId).state, to);
11867
12763
  if (to === "starting")
11868
12764
  setTaskStatus(db, row.task, "OPEN", { workstream: row.workstream, substate: "todo" });
@@ -11900,7 +12796,7 @@ function finishDelegate(db, callId, opts) {
11900
12796
  if (isTerminal(before.state)) return unchanged(before);
11901
12797
  db.prepare(
11902
12798
  "UPDATE delegate_calls SET state = 'recording', updated_at = ? WHERE call_id = ? AND state NOT IN ('done','failed','cancelled')"
11903
- ).run(now(), callId);
12799
+ ).run(now2(), callId);
11904
12800
  const outcome = opts.outcome === "done" && !opts.text?.trim() ? "empty" : opts.outcome;
11905
12801
  const answered = outcome === "done";
11906
12802
  const cancelled = outcome === "cancelled";
@@ -11947,7 +12843,7 @@ function finishDelegate(db, callId, opts) {
11947
12843
  });
11948
12844
  db.prepare(
11949
12845
  "UPDATE delegate_calls SET state = ?, verdict = ?, error = ?, updated_at = ? WHERE call_id = ?"
11950
- ).run(state, ok ? verdict : null, error ?? null, now(), callId);
12846
+ ).run(state, ok ? verdict : null, error ?? null, now2(), callId);
11951
12847
  return {
11952
12848
  changed: true,
11953
12849
  state,
@@ -12041,12 +12937,12 @@ function getReceipt(db, taskId, attemptKey) {
12041
12937
  return row ? fromRaw2(row) : void 0;
12042
12938
  }
12043
12939
  function reserveReceipt(db, r) {
12044
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
12940
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
12045
12941
  db.prepare(
12046
12942
  `INSERT INTO spawn_receipts
12047
12943
  (task_id, attempt_key, name, workstream, request_sha, msg_id, state, created_at, updated_at)
12048
12944
  VALUES (?, ?, ?, ?, ?, ?, 'reserved', ?, ?)`
12049
- ).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);
12050
12946
  const row = getReceipt(db, r.taskId, r.attemptKey);
12051
12947
  if (row === void 0) throw new Error("spawn receipt vanished after reservation");
12052
12948
  return row;
@@ -12328,25 +13224,25 @@ async function drive(db, req, taskId, key, start, resumed, guard, keep = false)
12328
13224
  }
12329
13225
  req.spawn.signal?.throwIfAborted();
12330
13226
  const checked = db.transaction(() => {
12331
- const now2 = getAgentForCleanup(db, target.name, ws);
12332
- 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) {
12333
13229
  throw new AgentReplacedError(target.name, ws, "sent to");
12334
13230
  }
12335
13231
  guard?.("send");
12336
13232
  const t = getTask(db, req.task, ws);
12337
13233
  if (t === void 0) throw new TaskNotFoundError(req.task, ws);
12338
13234
  if (t.status === "CLOSED") throw new SpawnTaskStateError(req.task, "was closed");
12339
- if (t.ownerId !== now2.incarnation) {
13235
+ if (t.ownerId !== now3.incarnation) {
12340
13236
  if (t.ownerId !== null) {
12341
13237
  throw new TaskAlreadyOwnedError(req.task, t.ownerName ?? "<unknown>");
12342
13238
  }
12343
- 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}`);
12344
13240
  }
12345
13241
  const journaled = updateReceipt(db, taskId, key, {
12346
13242
  state: "sending",
12347
- transport: expectsCtl(now2, agentCtlSocket(db, now2)) ? "ctl" : "mux"
13243
+ transport: expectsCtl(now3, agentCtlSocket(db, now3)) ? "ctl" : "mux"
12348
13244
  });
12349
- return { row: now2, rec: journaled };
13245
+ return { row: now3, rec: journaled };
12350
13246
  }).immediate();
12351
13247
  row = checked.row;
12352
13248
  rec = checked.rec;
@@ -12458,13 +13354,13 @@ function idleThresholdMs() {
12458
13354
  if (!Number.isFinite(n) || n < 0) return DEFAULT_IDLE_THRESHOLD_MS;
12459
13355
  return n;
12460
13356
  }
12461
- function computeAgentIdle(db, agent, now2 = Date.now()) {
13357
+ function computeAgentIdle(db, agent, now3 = Date.now()) {
12462
13358
  if (agent.state !== "needs_input" || agent.since === null) return false;
12463
13359
  const threshold = idleThresholdMs();
12464
13360
  if (threshold <= 0) return false;
12465
13361
  const since = Date.parse(agent.since);
12466
13362
  if (!Number.isFinite(since)) return false;
12467
- if (now2 - since < threshold) return false;
13363
+ if (now3 - since < threshold) return false;
12468
13364
  if (isScratchWorkstream(agent.workstreamName)) return true;
12469
13365
  const wsId = tryResolveWorkstreamId(db, agent.workstreamName);
12470
13366
  if (wsId === null) return false;
@@ -12529,15 +13425,19 @@ function agentIdByName(db, name, workstream) {
12529
13425
  function insertAgent(db, input) {
12530
13426
  ensureWorkstream(db, input.workstream);
12531
13427
  const workstreamId = resolveWorkstreamId(db, input.workstream);
12532
- const now2 = (/* @__PURE__ */ new Date()).toISOString();
13428
+ const now3 = (/* @__PURE__ */ new Date()).toISOString();
13429
+ let inserted;
12533
13430
  try {
12534
- db.prepare(
12535
- `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)
12536
- 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',
12537
13434
  COALESCE(@role, 'full-access'), @tab, @cwd, @base, @command, @commandSource,
12538
- @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)`
12539
13438
  ).run({
12540
13439
  name: input.name,
13440
+ workstream: input.workstream,
12541
13441
  workstreamId,
12542
13442
  cli: input.cli ?? null,
12543
13443
  paneId: input.paneId,
@@ -12552,14 +13452,21 @@ function insertAgent(db, input) {
12552
13452
  collectedHead: input.collectedHead ?? null,
12553
13453
  repo: input.repo ?? null,
12554
13454
  gateToken: input.gateToken ?? null,
12555
- now: now2
12556
- });
13455
+ hostTransport: input.hostTransport ?? null,
13456
+ reservation: input.reservation ?? null,
13457
+ now: now3
13458
+ }).changes;
12557
13459
  } catch (err) {
12558
13460
  if (err.code === "SQLITE_CONSTRAINT_UNIQUE") {
12559
13461
  throw new AgentExistsError(input.name);
12560
13462
  }
12561
13463
  throw err;
12562
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
+ }
12563
13470
  const row = getAgent(db, input.name, input.workstream);
12564
13471
  if (!row) throw new Error(`agents.insertAgent: row not found after insert: ${input.name}`);
12565
13472
  return row;
@@ -12673,7 +13580,7 @@ async function readLiveAgents(db, workstream, only, states) {
12673
13580
  const baseAgents = listAgents(db, { workstream }).filter(
12674
13581
  (a) => only === void 0 || a.name === only
12675
13582
  );
12676
- const stateDir = dirname11(db.name);
13583
+ const stateDir = dirname12(db.name);
12677
13584
  let readings;
12678
13585
  if (states === void 0) readings = await readAgentStates(baseAgents, { stateDir });
12679
13586
  else {
@@ -12683,7 +13590,7 @@ async function readLiveAgents(db, workstream, only, states) {
12683
13590
  }
12684
13591
  readings = states.cache;
12685
13592
  }
12686
- const now2 = Date.now();
13593
+ const now3 = Date.now();
12687
13594
  return baseAgents.map((agent) => {
12688
13595
  const reading = readings.get(agentKey(agent)) ?? {
12689
13596
  state: "unknown",
@@ -12701,7 +13608,7 @@ async function readLiveAgents(db, workstream, only, states) {
12701
13608
  ...reading.reason !== void 0 ? { reason: reading.reason } : {},
12702
13609
  ...reading.tool !== void 0 ? { tool: reading.tool } : {}
12703
13610
  };
12704
- return computeAgentIdle(db, live2, now2) ? { ...live2, idle: true } : live2;
13611
+ return computeAgentIdle(db, live2, now3) ? { ...live2, idle: true } : live2;
12705
13612
  });
12706
13613
  }
12707
13614
 
@@ -12721,13 +13628,239 @@ async function withSpawnLock(session, fn, opts) {
12721
13628
  return withFileLock(lockPathForSession(session), session, fn, lockOpts);
12722
13629
  }
12723
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
+
12724
13857
  // src/agents/staging.ts
12725
13858
  var STAGING_DEAD_MS = 6e4;
12726
- function deadStagings(db, workstream, now2 = Date.now()) {
13859
+ function deadStagings(db, workstream, now3 = Date.now()) {
12727
13860
  return listAgents(db, workstream === void 0 ? {} : { workstream }).filter((a) => {
12728
13861
  if (!isPendingPaneId(a.paneId) || a.host !== null) return false;
12729
13862
  const made = a.cwd === null ? void 0 : getWorktreeAt(db, a.cwd)?.createdAt;
12730
- 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;
12731
13864
  });
12732
13865
  }
12733
13866
  async function reclaimDeadStagings(db, workstream) {
@@ -12816,7 +13949,7 @@ async function checkCommandResolvable(command) {
12816
13949
  var NEXT_FREE_ATTEMPTS = 32;
12817
13950
  var NEXT_FREE_LOOKUPS = 1e3;
12818
13951
  function nextFreeAgentName(db, name, workstream) {
12819
- 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));
12820
13953
  if (free(name)) return name;
12821
13954
  const m = /^(.*)-(\d+)$/.exec(name);
12822
13955
  const [stem, first] = m ? [m[1] ?? "", BigInt(m[2] ?? "1")] : [name, 1n];
@@ -12830,9 +13963,6 @@ function nextFreeAgentName(db, name, workstream) {
12830
13963
  }
12831
13964
  throw new AgentNameExhaustedError(name, `the next ${NEXT_FREE_LOOKUPS} names are taken`);
12832
13965
  }
12833
- function probeToSpawnCtl(kind) {
12834
- return kind === "ok" ? "ok" : kind === "missing" ? "missing" : "refused";
12835
- }
12836
13966
  function speaksMuCtl(cli, command) {
12837
13967
  if (cli === "pi") return true;
12838
13968
  const argv0 = basename2(parseFirstToken(command));
@@ -12844,49 +13974,6 @@ function isCommandGate(opts) {
12844
13974
  const argv0 = basename2(parseFirstToken(opts.command));
12845
13975
  return argv0 !== "pi" && argv0 !== "pi-meta";
12846
13976
  }
12847
- function defaultSpawnCtlMs() {
12848
- const raw = process.env.MU_SPAWN_CTL_MS;
12849
- if (raw === void 0) return 3e4;
12850
- const parsed = Number.parseInt(raw, 10);
12851
- if (Number.isNaN(parsed) || parsed < 0) return 3e4;
12852
- return parsed;
12853
- }
12854
- var CTL_POLL_INTERVAL_MS = 250;
12855
- var CTL_STAT_INTERVAL_MS = 50;
12856
- var handshakeClock = { now: () => Date.now(), delay: (ms) => delay(ms) };
12857
- async function awaitCtlHandshake(sock, onMiss, signal) {
12858
- const budgetMs = defaultSpawnCtlMs();
12859
- const deadline = handshakeClock.now() + budgetMs;
12860
- let last = "missing";
12861
- let lastMissAt = Number.NEGATIVE_INFINITY;
12862
- let first = true;
12863
- for (; ; ) {
12864
- signal?.throwIfAborted();
12865
- if (first || existsSync5(sock)) {
12866
- first = false;
12867
- const remaining = deadline - handshakeClock.now();
12868
- const probe = await ctlProbe(
12869
- sock,
12870
- Math.max(1, Math.min(remaining, CTL_POLL_INTERVAL_MS * 4))
12871
- );
12872
- if (probe.kind === "ok") {
12873
- return {
12874
- ctl: "ok",
12875
- ...probe.incarnation !== void 0 ? { incarnation: probe.incarnation } : {}
12876
- };
12877
- }
12878
- last = probeToSpawnCtl(probe.kind);
12879
- }
12880
- const atDeadline = handshakeClock.now() >= deadline;
12881
- if (onMiss && (atDeadline || handshakeClock.now() - lastMissAt >= CTL_POLL_INTERVAL_MS)) {
12882
- lastMissAt = handshakeClock.now();
12883
- await onMiss();
12884
- }
12885
- const left = deadline - handshakeClock.now();
12886
- if (left <= 0) return { ctl: last };
12887
- await handshakeClock.delay(Math.min(CTL_STAT_INTERVAL_MS, left));
12888
- }
12889
- }
12890
13977
  async function spawnAgent3(db, opts) {
12891
13978
  await reclaimDeadStagings(db, opts.workstream);
12892
13979
  if (opts.nextFree === true) {
@@ -12909,6 +13996,7 @@ async function spawnAgent3(db, opts) {
12909
13996
  if (getAgent(db, opts.name, opts.workstream) !== void 0) {
12910
13997
  throw new AgentExistsError(opts.name);
12911
13998
  }
13999
+ refuseReserved(db, opts.workstream, opts.name);
12912
14000
  const session = opts.tmuxSession ?? `mu-${opts.workstream}`;
12913
14001
  const windowName = opts.tab ?? opts.name;
12914
14002
  const cli = opts.cli ?? "pi";
@@ -12920,6 +14008,15 @@ async function spawnAgent3(db, opts) {
12920
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"
12921
14009
  );
12922
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
+ };
12923
14020
  const command = opts.command ?? (profile ? cli : resolved.command);
12924
14021
  const commandSource = opts.command !== void 0 ? "explicit" : !profile && resolved.resolvedFromEnv ? "env" : "cli-key";
12925
14022
  if (opts.command === void 0 && !profile) {
@@ -12934,32 +14031,28 @@ async function spawnAgent3(db, opts) {
12934
14031
  let site;
12935
14032
  try {
12936
14033
  if (profile) {
12937
- ({ lease, profile } = await admitHostChannel(
14034
+ ({ lease, profile, staged, site } = await admitRemoteSpawn(
12938
14035
  db,
12939
- profile.name,
12940
- `spawn:${opts.workstream}/${opts.name}`,
12941
- { paneAlive: localPaneAlive }
14036
+ opts,
14037
+ cli,
14038
+ spawner,
14039
+ profile,
14040
+ leaseCheck,
14041
+ reservation?.token
12942
14042
  ));
12943
- if (opts.worktree) staged = await prestageRemote(db, opts, cli, spawner, lease, profile);
12944
- else
12945
- site = await prepareRemote(db, lease, profile, {
12946
- workstream: opts.workstream,
12947
- name: opts.name,
12948
- worktree: false,
12949
- ...opts.cwd !== void 0 ? { cwd: opts.cwd } : {},
12950
- ...remoteRoot(opts, profile)
12951
- });
12952
14043
  } else if (opts.worktree) {
12953
14044
  staged = await prestageWorktree(db, opts, cli, spawner);
12954
14045
  }
12955
14046
  } catch (err) {
12956
14047
  if (lease !== void 0) releaseHostChannel(db, lease);
14048
+ if (reservation !== void 0) releaseLeaseReservation(db, reservation.token, "none");
12957
14049
  emitSpawnFailed(db, opts, "worktree", err);
12958
14050
  throw err;
12959
14051
  }
12960
14052
  const paneCwd = staged?.path ?? site?.cwd ?? resolve3(opts.cwd ?? process.cwd());
12961
- const ctlSocket = ctlSocketPath(opts.workstream, opts.name, dirname12(db.name));
12962
- 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 });
12963
14056
  const paneEnv = profile ? {} : {
12964
14057
  MU_MANAGED_AGENT: "1",
12965
14058
  MU_AGENT_NAME: opts.name,
@@ -12990,71 +14083,100 @@ async function spawnAgent3(db, opts) {
12990
14083
  let readiness = Promise.resolve();
12991
14084
  try {
12992
14085
  opts.signal?.throwIfAborted();
12993
- const created = await withSpawnLock(session, async () => {
12994
- if (!hasWorktree && getAgent(db, opts.name, opts.workstream) !== void 0) {
12995
- throw new AgentExistsError(opts.name);
12996
- }
12997
- if (staged !== void 0) {
12998
- const cur = getAgent(db, opts.name, opts.workstream);
12999
- if (cur === void 0 || cur.incarnation !== staged.incarnation || cur.paneId !== pendingPaneIdFor(opts.name)) {
13000
- 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);
13001
14093
  }
13002
- }
13003
- rmSync3(ctlSocket, { force: true });
13004
- const mux = await activeMux();
13005
- if (isScratchWorkstream(opts.workstream)) await mux.ensureSession(session);
13006
- let onCreated = () => {
13007
- };
13008
- const createdPane = new Promise((res) => {
13009
- onCreated = res;
13010
- });
13011
- const done = mux.spawnAgent({
13012
- session,
13013
- window: windowName,
13014
- name: opts.name,
13015
- cli,
13016
- 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, {
13017
14106
  workstream: opts.workstream,
13018
14107
  name: opts.name,
13019
14108
  command: gateToken === void 0 ? command : gateWrapper(command, gateToken),
13020
14109
  localSock: ctlSocket,
13021
- cwd: paneCwd
13022
- }) : command,
13023
- commandSource,
13024
- ...profile ? {} : { cwd: paneCwd },
13025
- env: paneEnv,
13026
- onPaneCreated: onCreated,
13027
- // A gate's pane outlives its command: the result is read after it ends.
13028
- ...gateToken !== void 0 ? { retainOnExit: true } : {}
13029
- });
13030
- done.catch(() => {
13031
- });
13032
- const pid = await Promise.race([createdPane, done]);
13033
- paneId = pid;
13034
- readiness = done;
13035
- phase = "finalize";
13036
- const row = finalizeAgentRow(db, {
13037
- opts,
13038
- cli,
13039
- paneId: pid,
13040
- cwd: paneCwd,
13041
- staged,
13042
- command,
13043
- commandSource,
13044
- spawner,
13045
- host: profile?.name,
13046
- gateToken
13047
- });
13048
- ownedIncarnation = row.incarnation;
13049
- if (lease !== void 0 && !bindHostChannel(db, lease, { agentId: row.incarnation, paneId: pid })) {
13050
- throw new Error(say("host.channel_lost", lease.host, lease.holder));
13051
- }
13052
- opts.onIdentity?.(row);
13053
- return { pid, row };
13054
- });
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
+ );
13055
14166
  paneId = created.pid;
13056
14167
  agent = created.row;
13057
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
+ }
13058
14180
  if ((await activeMux()).spawnAgentWaitsReady) {
13059
14181
  await readiness;
13060
14182
  } else if (wantsCtl) {
@@ -13100,7 +14222,16 @@ async function spawnAgent3(db, opts) {
13100
14222
  } catch (err) {
13101
14223
  emitSpawnFailed(db, opts, phase, err);
13102
14224
  try {
13103
- 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
+ );
13104
14235
  } catch (cleanupErr) {
13105
14236
  if (err instanceof Error && err.cause === void 0) {
13106
14237
  try {
@@ -13190,65 +14321,6 @@ async function prestageLocked(db, opts, cli, spawner) {
13190
14321
  throw err;
13191
14322
  }
13192
14323
  }
13193
- function remoteRoot(opts, profile) {
13194
- const root = opts.root ?? profile.repo ?? void 0;
13195
- return root === void 0 ? {} : { root };
13196
- }
13197
- async function requireHostSpawn(db, opts) {
13198
- const host = opts.host ?? "";
13199
- const profile = getHost(db, host);
13200
- if (opts.worktree && opts.root === void 0 && profile.repo === null) {
13201
- throw new HostProfileError(
13202
- `host ${host} has no default repo: pass --root <remote repo path> with --worktree`
13203
- );
13204
- }
13205
- const mux = await activeMux();
13206
- if (!mux.runsShellCommand) throw new HostSpawnUnsupportedError(host, mux.name);
13207
- return profile;
13208
- }
13209
- async function prestageRemote(db, opts, cli, spawner, lease, profile) {
13210
- return withAgentLock(db, opts.workstream, opts.name, async () => {
13211
- const placeholder = insertAgent(db, {
13212
- name: opts.name,
13213
- workstream: opts.workstream,
13214
- cli,
13215
- paneId: pendingPaneIdFor(opts.name),
13216
- role: opts.role,
13217
- tab: opts.tab ?? null,
13218
- cwd: null,
13219
- spawner,
13220
- host: profile.name
13221
- });
13222
- try {
13223
- const made = await prepareRemote(db, lease, profile, {
13224
- workstream: opts.workstream,
13225
- name: opts.name,
13226
- worktree: true,
13227
- ...opts.from !== void 0 ? { from: opts.from } : {},
13228
- ...remoteRoot(opts, profile),
13229
- // An unused leftover of a prepare whose answer was lost is taken over;
13230
- // never while a removed agent's recovery record may name that tree.
13231
- reclaim: !recoveriesFor(db, opts.workstream, opts.name).some(
13232
- (r) => r.host === profile.name
13233
- )
13234
- });
13235
- return {
13236
- path: made.cwd,
13237
- base: made.base ?? "",
13238
- root: made.repo ?? "",
13239
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
13240
- incarnation: placeholder.incarnation,
13241
- host: profile.name
13242
- };
13243
- } catch (err) {
13244
- try {
13245
- deleteAgent(db, opts.name, opts.workstream, { incarnation: placeholder.incarnation });
13246
- } catch {
13247
- }
13248
- throw err;
13249
- }
13250
- });
13251
- }
13252
14324
  function spawnerFromEnv(env = process.env) {
13253
14325
  const { MU_MANAGED_AGENT: managed, MU_WORKSTREAM: ws, MU_AGENT_NAME: name } = env;
13254
14326
  if (managed === "1" && ws && name) return `${ws}/${name}`;
@@ -13257,8 +14329,10 @@ function spawnerFromEnv(env = process.env) {
13257
14329
  }
13258
14330
  function finalizeAgentRow(db, args) {
13259
14331
  const { opts, cli, paneId, cwd, staged, command, commandSource, spawner, host, gateToken } = args;
14332
+ const hostTransport = args.hostTransport ?? null;
13260
14333
  if (staged === void 0) {
13261
14334
  return insertAgent(db, {
14335
+ ...args.reservation !== void 0 ? { reservation: args.reservation } : {},
13262
14336
  name: opts.name,
13263
14337
  workstream: opts.workstream,
13264
14338
  cli,
@@ -13270,11 +14344,12 @@ function finalizeAgentRow(db, args) {
13270
14344
  commandSource,
13271
14345
  spawner,
13272
14346
  host: host ?? null,
14347
+ hostTransport,
13273
14348
  gateToken: gateToken ?? null
13274
14349
  });
13275
14350
  }
13276
14351
  const patched = db.prepare(
13277
- `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 = ?
13278
14353
  WHERE id = ? AND pane_id = ?
13279
14354
  AND name = ?
13280
14355
  AND workstream_id = (SELECT id FROM workstreams WHERE name = ?)`
@@ -13286,6 +14361,7 @@ function finalizeAgentRow(db, args) {
13286
14361
  commandSource,
13287
14362
  spawner,
13288
14363
  host ?? null,
14364
+ hostTransport,
13289
14365
  staged.host !== void 0 ? staged.root : null,
13290
14366
  (/* @__PURE__ */ new Date()).toISOString(),
13291
14367
  staged.incarnation,
@@ -13310,7 +14386,12 @@ function emitSpawnFailed(db, opts, phase, err) {
13310
14386
  } catch {
13311
14387
  }
13312
14388
  }
13313
- 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) {
13314
14395
  let paneGone2 = paneId === void 0;
13315
14396
  if (paneId !== void 0) {
13316
14397
  const mux = await activeMux().catch(() => void 0);
@@ -13341,32 +14422,12 @@ async function rollbackSpawn(db, name, paneId, staged, ownedIncarnation, workstr
13341
14422
  } catch {
13342
14423
  }
13343
14424
  }
14425
+ carrier ??= reservationOf(db, ownedIncarnation);
14426
+ const job = carrier ? await rollbackLeaseJob(db, { workstream, name, token: carrier.token }, paneGone2) : void 0;
13344
14427
  let remoteGone = false;
13345
14428
  if (staged?.host !== void 0) {
13346
- const remote = staged;
13347
- try {
13348
- if (!paneGone2) throw new Error("pane not proven gone");
13349
- await withAgentLock(db, workstream, name, async () => {
13350
- const cur = getAgent(db, name, workstream);
13351
- if (cur !== void 0 && cur.incarnation !== remote.incarnation) return;
13352
- remoteGone = await removeRemoteWorktree(
13353
- db,
13354
- remote.host ?? "",
13355
- `rollback:${workstream}/${name}`,
13356
- remote
13357
- );
13358
- });
13359
- } catch {
13360
- try {
13361
- emitEvent(
13362
- db,
13363
- workstream,
13364
- "agent.spawn_failed",
13365
- `agent spawn rollback ${name}: remote worktree ${staged.path} on ${staged.host} was not removed`
13366
- );
13367
- } catch {
13368
- }
13369
- }
14429
+ if (job !== "kept")
14430
+ remoteGone = await rollbackRemoteWorktree(db, name, workstream, staged, paneGone2);
13370
14431
  } else if (staged !== void 0 && paneGone2) {
13371
14432
  const say2 = (what) => {
13372
14433
  try {
@@ -13410,60 +14471,20 @@ async function rollbackSpawn(db, name, paneId, staged, ownedIncarnation, workstr
13410
14471
  }
13411
14472
  if (ownedIncarnation !== void 0)
13412
14473
  deleteAgent(db, name, workstream, { incarnation: ownedIncarnation }, { preserve: !remoteGone });
13413
- }
13414
- function defaultSpawnLivenessMs() {
13415
- const raw = process.env.MU_SPAWN_LIVENESS_MS;
13416
- if (raw === void 0) return 1500;
13417
- const parsed = Number.parseInt(raw, 10);
13418
- if (Number.isNaN(parsed) || parsed < 0) return 1500;
13419
- return parsed;
13420
- }
13421
- var AUTH_ERROR_PATTERNS = [
13422
- /No API key found for [\w-]+/i,
13423
- /Error: invalid API key/i,
13424
- /Authentication failed/i,
13425
- /401 Unauthorized/i,
13426
- /Could not authenticate/i
13427
- ];
13428
- var EXEC_FAILURE_PATTERN = /^(?:.*: )?(?:command not found|No such file or directory)(?::\s*\S+)?$/i;
13429
- var STARTUP_ERROR_TAIL_LINES = 30;
13430
- function detectSpawnStartupError(scrollback, cliStarted = false) {
13431
- const patterns = cliStarted ? AUTH_ERROR_PATTERNS : [...AUTH_ERROR_PATTERNS, EXEC_FAILURE_PATTERN];
13432
- const lines = scrollback.split(/\r?\n/);
13433
- const tail = lines.slice(Math.max(0, lines.length - STARTUP_ERROR_TAIL_LINES));
13434
- for (const line of tail) {
13435
- for (const pattern of patterns) {
13436
- if (pattern.test(line)) return line;
13437
- }
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);
13438
14478
  }
13439
- return void 0;
13440
14479
  }
13441
- async function awaitSpawnLiveness(paneId, agentName, command, host) {
13442
- const ms = defaultSpawnLivenessMs();
13443
- if (ms === 0) return false;
13444
- await sleep2(ms);
13445
- return checkSpawnHealth(paneId, agentName, command, void 0, false, host);
13446
- }
13447
- var CLAUDE_TRUST_DIALOG = /Yes, I trust this folder/i;
13448
- function detectClaudeTrustDialog(scrollback) {
13449
- const lines = scrollback.split(/\r?\n/);
13450
- const tail = lines.slice(Math.max(0, lines.length - STARTUP_ERROR_TAIL_LINES));
13451
- return tail.some((line) => CLAUDE_TRUST_DIALOG.test(line));
13452
- }
13453
- async function checkSpawnHealth(paneId, agentName, command, when, cliStarted = false, host) {
13454
- const mux = await activeMux();
13455
- const scrollback = await mux.capture(paneId, { lines: 50 }).catch(() => void 0);
13456
- if (!await mux.paneAlive(paneId)) {
13457
- throw new AgentDiedOnSpawnError(agentName, paneId, scrollback, command, when, host);
13458
- }
13459
- if (scrollback !== void 0) {
13460
- const matchedLine = detectSpawnStartupError(scrollback, cliStarted);
13461
- if (matchedLine !== void 0) {
13462
- throw new AgentSpawnStartupError(agentName, paneId, matchedLine, scrollback, when);
13463
- }
13464
- 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 {
13465
14487
  }
13466
- return false;
13467
14488
  }
13468
14489
 
13469
14490
  // src/agents/errors.ts
@@ -13487,6 +14508,59 @@ var AgentExistsError = class extends Error {
13487
14508
  agentName;
13488
14509
  name = "AgentExistsError";
13489
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
+ };
13490
14564
  var AgentNameExhaustedError = class extends Error {
13491
14565
  constructor(agentName, reason) {
13492
14566
  super(say("agent.name_exhausted", agentName, reason));
@@ -13957,6 +15031,19 @@ export {
13957
15031
  setMuxForTests,
13958
15032
  resetMux,
13959
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,
13960
15047
  HostProfileError,
13961
15048
  HostTemplateError,
13962
15049
  HostNotFoundError,
@@ -13964,17 +15051,22 @@ export {
13964
15051
  HostChannelBusyError,
13965
15052
  HostInUseError,
13966
15053
  HostSpawnUnsupportedError,
15054
+ HostHandbackError,
13967
15055
  HostCheckFailedError,
15056
+ HOST_TRANSPORTS,
15057
+ JOB_HANDLE_RE,
13968
15058
  isValidHostName,
13969
15059
  getHost,
13970
15060
  listHosts,
13971
15061
  addHost,
13972
15062
  removeHost,
13973
15063
  hostStarter,
15064
+ hostMuleStarter,
13974
15065
  HOST_PLACEHOLDERS,
13975
15066
  HOST_ENV_KEYS,
13976
15067
  hostEnvWords,
13977
15068
  validateHostTemplate,
15069
+ placeholderUses,
13978
15070
  expandHostTemplate,
13979
15071
  listHostChannels,
13980
15072
  reserveHostChannel,
@@ -14154,10 +15246,16 @@ export {
14154
15246
  agentOwed,
14155
15247
  hasDoneMark,
14156
15248
  setAgentDone,
15249
+ CarrierStopRefusedError,
15250
+ CarrierStopFailedError,
15251
+ runStop,
14157
15252
  locksDir,
14158
15253
  withFileLock,
14159
15254
  readFileLockMeta,
14160
15255
  withAgentLock,
15256
+ LeaseJobTimeoutError,
15257
+ LeaseJobInvalidError,
15258
+ LeaseJobUnreadableError,
14161
15259
  previewClose,
14162
15260
  closeAgent,
14163
15261
  delegateOutcome,
@@ -14200,6 +15298,7 @@ export {
14200
15298
  listLiveAgents,
14201
15299
  readLiveAgents,
14202
15300
  tmux_exports,
15301
+ defaultSpawnLivenessMs,
14203
15302
  deadStagings,
14204
15303
  resolveCliCommand,
14205
15304
  envVarNameForCli2 as envVarNameForCli,
@@ -14211,9 +15310,11 @@ export {
14211
15310
  isCommandGate,
14212
15311
  spawnAgent3 as spawnAgent,
14213
15312
  spawnerFromEnv,
14214
- defaultSpawnLivenessMs,
14215
15313
  AgentSpawnCliNotFoundError,
14216
15314
  AgentExistsError,
15315
+ CarrierJobUnstoppedError,
15316
+ CarrierJobUnresolvedError,
15317
+ CarrierJobKeptError,
14217
15318
  AgentNameExhaustedError,
14218
15319
  AgentNotFoundError,
14219
15320
  AgentNotInWorkstreamError,
@@ -14256,4 +15357,4 @@ export {
14256
15357
  murmurAvailable,
14257
15358
  readAgentStates
14258
15359
  };
14259
- //# sourceMappingURL=chunk-L7I3BLXP.js.map
15360
+ //# sourceMappingURL=chunk-SFV5RPZW.js.map