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