@mu-crew/mu 4.1.1 → 4.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-L7I3BLXP.js → chunk-4ZWBLTZK.js} +1512 -411
- package/dist/chunk-4ZWBLTZK.js.map +1 -0
- package/dist/{chunk-DEYBQDTO.js → chunk-FVRIKUBR.js} +2 -2
- package/dist/{chunk-B74N2EAS.js → chunk-RJQNDFBU.js} +8 -4
- package/dist/chunk-RJQNDFBU.js.map +1 -0
- package/dist/{chunk-LBBZAG5C.js → chunk-U2QLTREB.js} +26 -23
- package/dist/chunk-U2QLTREB.js.map +1 -0
- package/dist/{cli-H2PWJAJN.js → cli-H3VVCP7M.js} +146 -31
- package/dist/cli-H3VVCP7M.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/{errors-G5IVXSMY.js → errors-MDQZHB42.js} +8 -2
- package/dist/extension/mu-pi.js +20 -3
- package/dist/extension/mu-pi.js.map +1 -1
- package/dist/index.d.ts +2574 -2288
- package/dist/index.js +58 -4
- package/dist/index.js.map +1 -1
- package/dist/{tui-EN2CDP6G.js → tui-5ZARE7RS.js} +14 -15
- package/dist/tui-5ZARE7RS.js.map +1 -0
- package/docs/ARCHITECTURE.md +15 -8
- package/docs/VOCABULARY.md +14 -6
- package/docs/guide/remote.md +113 -4
- package/docs/guide/sql.md +1 -1
- package/docs/reference/env.md +1 -0
- package/package.json +1 -1
- package/skills/mu/recipes/remote-workers.md +7 -6
- package/dist/chunk-B74N2EAS.js.map +0 -1
- package/dist/chunk-L7I3BLXP.js.map +0 -1
- package/dist/chunk-LBBZAG5C.js.map +0 -1
- package/dist/cli-H2PWJAJN.js.map +0 -1
- package/dist/tui-EN2CDP6G.js.map +0 -1
- /package/dist/{chunk-DEYBQDTO.js.map → chunk-FVRIKUBR.js.map} +0 -0
- /package/dist/{errors-G5IVXSMY.js.map → errors-MDQZHB42.js.map} +0 -0
|
@@ -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,
|
|
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
|
|
2100
|
-
import { basename as basename2, dirname as
|
|
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
|
|
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
|
-
|
|
3805
|
-
|
|
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,
|
|
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,
|
|
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(
|
|
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,
|
|
3994
|
+
function receiveHlc(db, remoteHlc, now3 = Date.now()) {
|
|
3903
3995
|
const remote = parseHlc(remoteHlc);
|
|
3904
|
-
const wallNow = Math.floor(
|
|
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(
|
|
6226
|
-
if (murmurCache !== void 0 &&
|
|
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:
|
|
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
|
|
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,
|
|
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
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
6726
|
-
return
|
|
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
|
|
6736
|
-
return
|
|
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 =
|
|
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
|
|
6835
|
-
|
|
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
|
-
|
|
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({
|
|
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
|
|
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" ?
|
|
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
|
|
7062
|
-
reopen.run(
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
7241
|
-
|
|
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
|
|
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)
|
|
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(
|
|
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,
|
|
7535
|
-
const ts =
|
|
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
|
|
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,
|
|
8339
|
+
).run(blockerId, blockedId, now3, blockerId, blockedId);
|
|
7926
8340
|
if (result.changes > 0) {
|
|
7927
|
-
touchTask(db, blockedId,
|
|
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
|
|
8427
|
+
const now3 = (/* @__PURE__ */ new Date()).toISOString();
|
|
8014
8428
|
for (const blockerId of blockerIds) {
|
|
8015
|
-
insertEdge.run(blockerId, taskSurrogateId,
|
|
8429
|
+
insertEdge.run(blockerId, taskSurrogateId, now3);
|
|
8016
8430
|
}
|
|
8017
|
-
touchTask(db, taskSurrogateId,
|
|
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
|
|
8930
|
-
if (
|
|
8931
|
-
throw new WorkstreamRevisionMismatchError(opts.workstream, opts.ifRevision,
|
|
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
|
|
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
|
-
|
|
9155
|
-
|
|
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,
|
|
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
|
|
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,
|
|
9659
|
+
insertEdge.run(newId, umbrellaId, now3);
|
|
9246
9660
|
}
|
|
9247
9661
|
}
|
|
9248
|
-
for (const umbrellaId of umbrellas.keys()) touchTask(db, umbrellaId,
|
|
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
|
|
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,
|
|
9286
|
-
if (touch) touchTask(db, taskId,
|
|
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:
|
|
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
|
|
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,
|
|
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
|
|
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(
|
|
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
|
|
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),
|
|
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,
|
|
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
|
|
10920
|
-
var newGateToken = () =>
|
|
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
|
|
11256
|
-
const nowRow = worktreeOf(db, opts.workstream, name,
|
|
11257
|
-
if (
|
|
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 ||
|
|
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:
|
|
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:
|
|
11300
|
-
paneId:
|
|
11301
|
-
ownedTasks:
|
|
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(
|
|
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
|
-
|
|
11329
|
-
|
|
11330
|
-
if (
|
|
11331
|
-
|
|
11332
|
-
|
|
11333
|
-
|
|
11334
|
-
|
|
11335
|
-
|
|
11336
|
-
|
|
11337
|
-
|
|
11338
|
-
|
|
11339
|
-
|
|
11340
|
-
|
|
11341
|
-
|
|
11342
|
-
|
|
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
|
|
11350
|
-
return
|
|
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
|
-
|
|
11411
|
-
|
|
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
|
|
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:
|
|
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
|
|
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(
|
|
12542
|
+
return join8(dirname11(dbPath), "delegate-sessions", workstream, task);
|
|
11647
12543
|
}
|
|
11648
12544
|
function delegateProcessDir(dbPath, callId) {
|
|
11649
12545
|
return join8(
|
|
11650
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
12332
|
-
if (
|
|
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 !==
|
|
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 ${
|
|
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(
|
|
13243
|
+
transport: expectsCtl(now3, agentCtlSocket(db, now3)) ? "ctl" : "mux"
|
|
12348
13244
|
});
|
|
12349
|
-
return { row:
|
|
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,
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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 && !
|
|
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
|
|
14034
|
+
({ lease, profile, staged, site } = await admitRemoteSpawn(
|
|
12938
14035
|
db,
|
|
12939
|
-
|
|
12940
|
-
|
|
12941
|
-
|
|
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
|
|
12962
|
-
|
|
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
|
|
12994
|
-
|
|
12995
|
-
|
|
12996
|
-
|
|
12997
|
-
|
|
12998
|
-
|
|
12999
|
-
|
|
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
|
-
|
|
13004
|
-
|
|
13005
|
-
|
|
13006
|
-
|
|
13007
|
-
|
|
13008
|
-
|
|
13009
|
-
|
|
13010
|
-
|
|
13011
|
-
|
|
13012
|
-
session
|
|
13013
|
-
|
|
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
|
-
|
|
13023
|
-
|
|
13024
|
-
|
|
13025
|
-
|
|
13026
|
-
|
|
13027
|
-
|
|
13028
|
-
|
|
13029
|
-
|
|
13030
|
-
|
|
13031
|
-
|
|
13032
|
-
|
|
13033
|
-
|
|
13034
|
-
|
|
13035
|
-
|
|
13036
|
-
|
|
13037
|
-
|
|
13038
|
-
|
|
13039
|
-
|
|
13040
|
-
|
|
13041
|
-
|
|
13042
|
-
|
|
13043
|
-
|
|
13044
|
-
|
|
13045
|
-
|
|
13046
|
-
|
|
13047
|
-
|
|
13048
|
-
|
|
13049
|
-
|
|
13050
|
-
|
|
13051
|
-
|
|
13052
|
-
|
|
13053
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
13347
|
-
|
|
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
|
-
|
|
13415
|
-
|
|
13416
|
-
|
|
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
|
-
|
|
13442
|
-
|
|
13443
|
-
|
|
13444
|
-
|
|
13445
|
-
|
|
13446
|
-
}
|
|
13447
|
-
|
|
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-
|
|
15360
|
+
//# sourceMappingURL=chunk-4ZWBLTZK.js.map
|