@staff0rd/assist 0.760.0 → 0.761.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/dist/commands/sessions/web/bundle.js +424 -424
- package/dist/index.js +887 -668
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { Command } from "commander";
|
|
|
6
6
|
// package.json
|
|
7
7
|
var package_default = {
|
|
8
8
|
name: "@staff0rd/assist",
|
|
9
|
-
version: "0.
|
|
9
|
+
version: "0.761.1",
|
|
10
10
|
type: "module",
|
|
11
11
|
main: "dist/index.js",
|
|
12
12
|
bin: {
|
|
@@ -15735,6 +15735,9 @@ async function restartWeb(req, res, deps2 = {}) {
|
|
|
15735
15735
|
respondJson(res, 400, { error: "Invalid target" });
|
|
15736
15736
|
return;
|
|
15737
15737
|
}
|
|
15738
|
+
console.log(
|
|
15739
|
+
`restart ${target} from=${req.headers["x-assist-linked-from"] ?? "local"} trace=${req.headers[TRACE_HEADER] ?? "none"}`
|
|
15740
|
+
);
|
|
15738
15741
|
await new Promise((resolve26) => {
|
|
15739
15742
|
res.once("finish", resolve26);
|
|
15740
15743
|
respondJson(res, 200, { ok: true });
|
|
@@ -15937,6 +15940,350 @@ function newSessionDefaults(req, res) {
|
|
|
15937
15940
|
respondJson(res, 200, { mode });
|
|
15938
15941
|
}
|
|
15939
15942
|
|
|
15943
|
+
// src/commands/sessions/nodes/fetchPeerJson.ts
|
|
15944
|
+
var PEER_TIMEOUT_MS = 3e3;
|
|
15945
|
+
function peerErrorCode(error) {
|
|
15946
|
+
if (!(error instanceof Error)) return void 0;
|
|
15947
|
+
const { code, cause } = error;
|
|
15948
|
+
if (code) return code;
|
|
15949
|
+
if (error.name === "TimeoutError") return "ETIMEDOUT";
|
|
15950
|
+
return cause instanceof Error ? peerErrorCode(cause) : void 0;
|
|
15951
|
+
}
|
|
15952
|
+
function describePeerError(error) {
|
|
15953
|
+
if (!(error instanceof Error)) return String(error);
|
|
15954
|
+
const cause = error.cause;
|
|
15955
|
+
return cause instanceof Error ? `${error.message}: ${describePeerError(cause)}` : error.message;
|
|
15956
|
+
}
|
|
15957
|
+
async function fetchPeerJson(baseUrl, path106, timeoutMs = PEER_TIMEOUT_MS) {
|
|
15958
|
+
const res = await fetch(new URL(path106, baseUrl), {
|
|
15959
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
15960
|
+
});
|
|
15961
|
+
if (!res.ok)
|
|
15962
|
+
throw Object.assign(
|
|
15963
|
+
new Error(`GET ${path106} returned ${res.status} ${res.statusText}`),
|
|
15964
|
+
{ status: res.status }
|
|
15965
|
+
);
|
|
15966
|
+
return await res.json();
|
|
15967
|
+
}
|
|
15968
|
+
|
|
15969
|
+
// src/commands/sessions/daemon/links/connectLinkWebSocket.ts
|
|
15970
|
+
import WebSocket from "ws";
|
|
15971
|
+
var HANDSHAKE_TIMEOUT_MS = 5e3;
|
|
15972
|
+
function linkWebSocketUrl(url) {
|
|
15973
|
+
const target = new URL("/ws", url);
|
|
15974
|
+
target.protocol = target.protocol === "https:" ? "wss:" : "ws:";
|
|
15975
|
+
return target.toString();
|
|
15976
|
+
}
|
|
15977
|
+
function connectLinkWebSocket(url, handlers) {
|
|
15978
|
+
return new Promise((resolve26, reject) => {
|
|
15979
|
+
const ws = new WebSocket(linkWebSocketUrl(url), {
|
|
15980
|
+
handshakeTimeout: HANDSHAKE_TIMEOUT_MS
|
|
15981
|
+
});
|
|
15982
|
+
let open = false;
|
|
15983
|
+
ws.on("open", () => {
|
|
15984
|
+
open = true;
|
|
15985
|
+
resolve26({
|
|
15986
|
+
send: (line) => {
|
|
15987
|
+
if (ws.readyState !== WebSocket.OPEN) return false;
|
|
15988
|
+
ws.send(line);
|
|
15989
|
+
return true;
|
|
15990
|
+
},
|
|
15991
|
+
close: () => ws.close()
|
|
15992
|
+
});
|
|
15993
|
+
});
|
|
15994
|
+
ws.on("message", (data) => handlers.onLine(data.toString()));
|
|
15995
|
+
ws.on("error", (error) => {
|
|
15996
|
+
if (!open) reject(error);
|
|
15997
|
+
});
|
|
15998
|
+
ws.on("close", (code, reason4) => {
|
|
15999
|
+
const detail = `code ${code}${reason4.length ? ` ${reason4.toString()}` : ""}`;
|
|
16000
|
+
if (open) handlers.onClose(detail);
|
|
16001
|
+
else reject(new Error(`closed before open (${detail})`));
|
|
16002
|
+
});
|
|
16003
|
+
});
|
|
16004
|
+
}
|
|
16005
|
+
|
|
16006
|
+
// src/commands/sessions/nodes/probePeerHello.ts
|
|
16007
|
+
var HELLO_TIMEOUT_MS = 5e3;
|
|
16008
|
+
async function probePeerHello(url) {
|
|
16009
|
+
let settle = () => {
|
|
16010
|
+
};
|
|
16011
|
+
const reply2 = new Promise((resolve26) => {
|
|
16012
|
+
settle = resolve26;
|
|
16013
|
+
});
|
|
16014
|
+
const socket = await connectLinkWebSocket(url, {
|
|
16015
|
+
onLine: (line) => {
|
|
16016
|
+
try {
|
|
16017
|
+
const msg = JSON.parse(line);
|
|
16018
|
+
if (msg.type === "hello") settle(msg);
|
|
16019
|
+
} catch {
|
|
16020
|
+
}
|
|
16021
|
+
},
|
|
16022
|
+
onClose: (reason4) => settle(new Error(`closed before hello reply (${reason4})`))
|
|
16023
|
+
});
|
|
16024
|
+
const timer = setTimeout(
|
|
16025
|
+
() => settle(new Error(`no hello reply within ${HELLO_TIMEOUT_MS / 1e3}s`)),
|
|
16026
|
+
HELLO_TIMEOUT_MS
|
|
16027
|
+
);
|
|
16028
|
+
socket.send(JSON.stringify(buildHello()));
|
|
16029
|
+
const result = await reply2;
|
|
16030
|
+
clearTimeout(timer);
|
|
16031
|
+
socket.close();
|
|
16032
|
+
if (result instanceof Error) throw result;
|
|
16033
|
+
return result;
|
|
16034
|
+
}
|
|
16035
|
+
|
|
16036
|
+
// src/commands/sessions/nodes/buildDoctorProbes.ts
|
|
16037
|
+
function buildDoctorProbes(live) {
|
|
16038
|
+
return {
|
|
16039
|
+
health: (url) => fetchPeerJson(url, "/api/health"),
|
|
16040
|
+
hello: probePeerHello,
|
|
16041
|
+
tailscale: tailscaleStatus,
|
|
16042
|
+
linkState: (name) => {
|
|
16043
|
+
if (!live) return "no-daemon";
|
|
16044
|
+
return live.links.find((l) => l.name === name) ?? "unknown-link";
|
|
16045
|
+
}
|
|
16046
|
+
};
|
|
16047
|
+
}
|
|
16048
|
+
|
|
16049
|
+
// src/commands/sessions/daemon/links/linkVersionCheck.ts
|
|
16050
|
+
function linkVersionCheck() {
|
|
16051
|
+
return loadConfig().sessions?.linkVersionCheck ?? "block";
|
|
16052
|
+
}
|
|
16053
|
+
|
|
16054
|
+
// src/commands/sessions/nodes/DoctorProbes.ts
|
|
16055
|
+
function passed(hop, detail) {
|
|
16056
|
+
return { hop, ok: true, detail };
|
|
16057
|
+
}
|
|
16058
|
+
function failed(hop, error, remediation) {
|
|
16059
|
+
return { hop, ok: false, error, remediation };
|
|
16060
|
+
}
|
|
16061
|
+
|
|
16062
|
+
// src/commands/sessions/nodes/helloHop.ts
|
|
16063
|
+
async function helloHop(spec, probes) {
|
|
16064
|
+
let msg;
|
|
16065
|
+
try {
|
|
16066
|
+
msg = await probes.hello(spec.url);
|
|
16067
|
+
} catch (error) {
|
|
16068
|
+
return failed(
|
|
16069
|
+
"ws",
|
|
16070
|
+
describePeerError(error),
|
|
16071
|
+
`${spec.name}'s /api/health answers but its /ws does not \u2014 restart the web server on ${spec.name}, then check \`assist sessions nodes logs ${spec.name}\``
|
|
16072
|
+
);
|
|
16073
|
+
}
|
|
16074
|
+
if (!isHello(msg))
|
|
16075
|
+
return failed(
|
|
16076
|
+
"ws",
|
|
16077
|
+
`malformed hello: ${JSON.stringify(msg)}`,
|
|
16078
|
+
`run \`assist update\` on ${spec.name}`
|
|
16079
|
+
);
|
|
16080
|
+
const detail = `hello ${msg.version} (protocol ${msg.protocol ?? "legacy"})`;
|
|
16081
|
+
if (helloCompatible(msg)) return passed("ws", detail);
|
|
16082
|
+
const mismatch = `${helloMismatchKind(msg)} mismatch: peer ${msg.version} (protocol ${msg.protocol ?? "legacy"}), this node ${ASSIST_VERSION} (protocol ${PROTOCOL_VERSION})`;
|
|
16083
|
+
const mode = linkVersionCheck();
|
|
16084
|
+
if (mode !== "block")
|
|
16085
|
+
return passed(
|
|
16086
|
+
"ws",
|
|
16087
|
+
`${mismatch}; allowed by sessions.linkVersionCheck=${mode}`
|
|
16088
|
+
);
|
|
16089
|
+
return failed(
|
|
16090
|
+
"ws",
|
|
16091
|
+
mismatch,
|
|
16092
|
+
`this node heals an older peer via its /api/self-update on connect; if the heal latched, run \`assist update\` on the older node, then \`assist daemon restart\` here`
|
|
16093
|
+
);
|
|
16094
|
+
}
|
|
16095
|
+
|
|
16096
|
+
// src/commands/sessions/nodes/linkStateHop.ts
|
|
16097
|
+
function linkStateHop(spec, probes) {
|
|
16098
|
+
const status2 = probes.linkState(spec.name);
|
|
16099
|
+
if (status2 === "no-daemon")
|
|
16100
|
+
return failed(
|
|
16101
|
+
"link",
|
|
16102
|
+
"this node's daemon is not running",
|
|
16103
|
+
"links run in the daemon \u2014 start this node's web server or run `assist daemon restart`"
|
|
16104
|
+
);
|
|
16105
|
+
if (status2 === "unknown-link")
|
|
16106
|
+
return failed(
|
|
16107
|
+
"link",
|
|
16108
|
+
`this node's daemon has no link named ${spec.name}`,
|
|
16109
|
+
"the daemon has not loaded sessions.links \u2014 run `assist daemon restart`"
|
|
16110
|
+
);
|
|
16111
|
+
if (status2.state === "connected") return passed("link", "connected");
|
|
16112
|
+
const error = status2.error ?? status2.state;
|
|
16113
|
+
if (status2.state === "version-blocked")
|
|
16114
|
+
return failed("link", error, `version mismatch \u2014 heal latched. ${error}`);
|
|
16115
|
+
return failed(
|
|
16116
|
+
"link",
|
|
16117
|
+
error,
|
|
16118
|
+
`every probe passed but the daemon's link is ${status2.state}; it retries behind the circuit breaker \u2014 check this node's daemon.log for \`link ${spec.name} ws:\` lines`
|
|
16119
|
+
);
|
|
16120
|
+
}
|
|
16121
|
+
|
|
16122
|
+
// src/commands/sessions/nodes/findTailscalePeer.ts
|
|
16123
|
+
function findTailscalePeer(status2, hostname2) {
|
|
16124
|
+
const nodes = [
|
|
16125
|
+
...status2.Self ? [{ ...status2.Self, Online: true }] : [],
|
|
16126
|
+
...Object.values(status2.Peer ?? {})
|
|
16127
|
+
];
|
|
16128
|
+
return nodes.find(
|
|
16129
|
+
(node) => node.DNSName?.replace(/\.$/, "").toLowerCase() === hostname2
|
|
16130
|
+
);
|
|
16131
|
+
}
|
|
16132
|
+
|
|
16133
|
+
// src/commands/sessions/nodes/tailscaleHop.ts
|
|
16134
|
+
async function tailscaleHop(spec, probes) {
|
|
16135
|
+
const cli = tailscaleCli();
|
|
16136
|
+
let status2;
|
|
16137
|
+
try {
|
|
16138
|
+
status2 = await probes.tailscale();
|
|
16139
|
+
} catch (error) {
|
|
16140
|
+
return failed(
|
|
16141
|
+
"tailscale",
|
|
16142
|
+
describePeerError(error),
|
|
16143
|
+
`\`${cli} status --json\` failed \u2014 install Tailscale on this node and sign in with \`${cli} up\``
|
|
16144
|
+
);
|
|
16145
|
+
}
|
|
16146
|
+
if (status2.BackendState !== "Running")
|
|
16147
|
+
return failed(
|
|
16148
|
+
"tailscale",
|
|
16149
|
+
`Tailscale on this node is ${status2.BackendState ?? "not running"}`,
|
|
16150
|
+
`run \`${cli} up\` on this node`
|
|
16151
|
+
);
|
|
16152
|
+
const { hostname: hostname2 } = new URL(spec.url);
|
|
16153
|
+
const peer = findTailscalePeer(status2, hostname2);
|
|
16154
|
+
if (!peer)
|
|
16155
|
+
return failed(
|
|
16156
|
+
"tailscale",
|
|
16157
|
+
`${hostname2} is not in this node's tailnet`,
|
|
16158
|
+
`check the host in \`${cli} status\`, then relink with \`assist sessions nodes link ${spec.name} --tailscale <host> --port <port>\``
|
|
16159
|
+
);
|
|
16160
|
+
if (!peer.Online)
|
|
16161
|
+
return failed(
|
|
16162
|
+
"tailscale",
|
|
16163
|
+
`${hostname2} is offline in the tailnet`,
|
|
16164
|
+
`is ${spec.name}'s machine awake with Tailscale running and signed in?`
|
|
16165
|
+
);
|
|
16166
|
+
return passed(
|
|
16167
|
+
"tailscale",
|
|
16168
|
+
`${hostname2} online${peer.TailscaleIPs?.[0] ? ` (${peer.TailscaleIPs[0]})` : ""}`
|
|
16169
|
+
);
|
|
16170
|
+
}
|
|
16171
|
+
|
|
16172
|
+
// src/commands/sessions/nodes/tailscaleServeRemediation.ts
|
|
16173
|
+
function tailscaleServeRemediation(spec, error) {
|
|
16174
|
+
const status2 = error.status;
|
|
16175
|
+
if (status2 !== void 0 && status2 !== 502) return void 0;
|
|
16176
|
+
const { hostname: hostname2, port } = new URL(spec.url);
|
|
16177
|
+
const servePort = port || "443";
|
|
16178
|
+
return status2 === 502 ? `tailscale serve on ${hostname2} answered but nothing listens on 127.0.0.1:${servePort} behind it \u2014 is ${spec.name}'s web server running (project-switch webservers)?` : `nothing serves https on ${servePort} on ${hostname2} \u2014 ${spec.name}'s web server runs tailscale serve when it starts: check the \`tailscale serve:\` line in its log (project-switch "View logs") and that sessions.tailscaleServe is not false there`;
|
|
16179
|
+
}
|
|
16180
|
+
|
|
16181
|
+
// src/commands/sessions/nodes/isTailscaleLink.ts
|
|
16182
|
+
function isTailscaleLink(spec) {
|
|
16183
|
+
return new URL(spec.url).hostname.endsWith(".ts.net");
|
|
16184
|
+
}
|
|
16185
|
+
|
|
16186
|
+
// src/commands/sessions/nodes/webHop.ts
|
|
16187
|
+
function webRemediation(spec, error) {
|
|
16188
|
+
const serve = isTailscaleLink(spec) ? tailscaleServeRemediation(spec, error) : void 0;
|
|
16189
|
+
if (serve) return serve;
|
|
16190
|
+
const { hostname: hostname2, port } = new URL(spec.url);
|
|
16191
|
+
switch (peerErrorCode(error)) {
|
|
16192
|
+
case "ECONNREFUSED":
|
|
16193
|
+
return `nothing listening on ${port} on ${hostname2} \u2014 is ${spec.name}'s web server running (project-switch webservers)?`;
|
|
16194
|
+
case "ETIMEDOUT":
|
|
16195
|
+
return `${hostname2}:${port} did not answer \u2014 is ${spec.name} up and the port reachable from here?`;
|
|
16196
|
+
case "ENOTFOUND":
|
|
16197
|
+
return `cannot resolve ${hostname2} \u2014 fix the url with \`assist sessions nodes link ${spec.name} <url>\``;
|
|
16198
|
+
}
|
|
16199
|
+
if (error.status === 404)
|
|
16200
|
+
return `${spec.name}'s web server has no /api/health \u2014 run \`assist update\` on ${spec.name}`;
|
|
16201
|
+
return `check the link url ${spec.url} with \`assist sessions nodes\``;
|
|
16202
|
+
}
|
|
16203
|
+
async function webHop(spec, probes) {
|
|
16204
|
+
let health2;
|
|
16205
|
+
try {
|
|
16206
|
+
health2 = await probes.health(spec.url);
|
|
16207
|
+
} catch (error) {
|
|
16208
|
+
return {
|
|
16209
|
+
hop: failed("web", describePeerError(error), webRemediation(spec, error))
|
|
16210
|
+
};
|
|
16211
|
+
}
|
|
16212
|
+
if (health2.nodeName !== spec.name)
|
|
16213
|
+
return {
|
|
16214
|
+
hop: failed(
|
|
16215
|
+
"web",
|
|
16216
|
+
`peer reports nodeName ${health2.nodeName}, link expects ${spec.name}`,
|
|
16217
|
+
`relink with \`assist sessions nodes unlink ${spec.name}\` then \`assist sessions nodes link ${health2.nodeName} ${spec.url}\`, or set sessions.nodeName on the peer`
|
|
16218
|
+
)
|
|
16219
|
+
};
|
|
16220
|
+
return {
|
|
16221
|
+
hop: passed(
|
|
16222
|
+
"web",
|
|
16223
|
+
`${health2.nodeName} ${health2.version} (protocol ${health2.protocol})`
|
|
16224
|
+
),
|
|
16225
|
+
health: health2
|
|
16226
|
+
};
|
|
16227
|
+
}
|
|
16228
|
+
|
|
16229
|
+
// src/commands/sessions/nodes/peerDaemonHop.ts
|
|
16230
|
+
function peerDaemonHop(spec, health2) {
|
|
16231
|
+
if (health2.daemon?.reachable) return passed("daemon", "reachable");
|
|
16232
|
+
return failed(
|
|
16233
|
+
"daemon",
|
|
16234
|
+
`${spec.name}'s web server reports its daemon unreachable`,
|
|
16235
|
+
`run \`assist daemon status\` on ${spec.name}; restarting its web server starts the daemon`
|
|
16236
|
+
);
|
|
16237
|
+
}
|
|
16238
|
+
|
|
16239
|
+
// src/commands/sessions/nodes/diagnoseLink.ts
|
|
16240
|
+
async function diagnoseLink(spec, probes) {
|
|
16241
|
+
const hops = [];
|
|
16242
|
+
const done2 = () => ({ ...spec, ok: hops.every((h) => h.ok), hops });
|
|
16243
|
+
if (isTailscaleLink(spec)) {
|
|
16244
|
+
hops.push(await tailscaleHop(spec, probes));
|
|
16245
|
+
if (!hops.at(-1)?.ok) return done2();
|
|
16246
|
+
}
|
|
16247
|
+
const web3 = await webHop(spec, probes);
|
|
16248
|
+
hops.push(web3.hop);
|
|
16249
|
+
if (!web3.health) return done2();
|
|
16250
|
+
hops.push(peerDaemonHop(spec, web3.health));
|
|
16251
|
+
if (!hops.at(-1)?.ok) return done2();
|
|
16252
|
+
hops.push(await helloHop(spec, probes));
|
|
16253
|
+
if (!hops.at(-1)?.ok) return done2();
|
|
16254
|
+
hops.push(linkStateHop(spec, probes));
|
|
16255
|
+
return done2();
|
|
16256
|
+
}
|
|
16257
|
+
|
|
16258
|
+
// src/commands/sessions/nodes/firstBrokenHop.ts
|
|
16259
|
+
async function firstBrokenHop(name) {
|
|
16260
|
+
const spec = findLinkSpec(name);
|
|
16261
|
+
if (!spec)
|
|
16262
|
+
return {
|
|
16263
|
+
hop: "link",
|
|
16264
|
+
ok: false,
|
|
16265
|
+
error: `no link named ${name}`,
|
|
16266
|
+
remediation: "see `assist sessions nodes`"
|
|
16267
|
+
};
|
|
16268
|
+
const diagnosis = await diagnoseLink(
|
|
16269
|
+
spec,
|
|
16270
|
+
buildDoctorProbes(await queryNodes())
|
|
16271
|
+
);
|
|
16272
|
+
return diagnosis.hops.find((hop) => !hop.ok);
|
|
16273
|
+
}
|
|
16274
|
+
|
|
16275
|
+
// src/commands/sessions/web/nodeDoctor.ts
|
|
16276
|
+
async function nodeDoctor(req, res) {
|
|
16277
|
+
const link3 = new URL(req.url ?? "/", "http://localhost").searchParams.get(
|
|
16278
|
+
"link"
|
|
16279
|
+
);
|
|
16280
|
+
if (!link3) {
|
|
16281
|
+
respondJson(res, 400, { error: "Missing link" });
|
|
16282
|
+
return;
|
|
16283
|
+
}
|
|
16284
|
+
respondJson(res, 200, { hop: await firstBrokenHop(link3) ?? null });
|
|
16285
|
+
}
|
|
16286
|
+
|
|
15940
16287
|
// src/commands/sessions/shared/sessionViewDefaults.ts
|
|
15941
16288
|
var sessionViewDefaults = {
|
|
15942
16289
|
floatWaiting: true,
|
|
@@ -16840,6 +17187,7 @@ var routes2 = {
|
|
|
16840
17187
|
"POST /api/pr-preview/upload-image": uploadPrImage,
|
|
16841
17188
|
"GET /api/pr-preview/image": previewImage,
|
|
16842
17189
|
"POST /api/restart": restartWeb,
|
|
17190
|
+
"GET /api/node-doctor": nodeDoctor,
|
|
16843
17191
|
"POST /api/self-update": selfUpdate,
|
|
16844
17192
|
"GET /api/github-url": githubUrl,
|
|
16845
17193
|
"GET /api/git-status": gitStatus,
|
|
@@ -25305,6 +25653,7 @@ var persistedSessionSchema = z9.object({
|
|
|
25305
25653
|
activity: activitySchema.optional(),
|
|
25306
25654
|
starred: z9.boolean().optional(),
|
|
25307
25655
|
watcher: z9.boolean().optional(),
|
|
25656
|
+
divergenceEscalation: z9.boolean().optional(),
|
|
25308
25657
|
design: z9.boolean().optional(),
|
|
25309
25658
|
auto: z9.boolean().optional(),
|
|
25310
25659
|
autoRun: z9.boolean().optional(),
|
|
@@ -25338,6 +25687,7 @@ function toPersistedSession(session) {
|
|
|
25338
25687
|
activity: session.activity,
|
|
25339
25688
|
starred: session.starred,
|
|
25340
25689
|
watcher: session.watcher,
|
|
25690
|
+
divergenceEscalation: session.divergenceEscalation,
|
|
25341
25691
|
design: session.design,
|
|
25342
25692
|
auto: session.auto,
|
|
25343
25693
|
autoRun: session.autoRun,
|
|
@@ -35647,7 +35997,7 @@ function skipReason(results, failures, strict) {
|
|
|
35647
35997
|
}
|
|
35648
35998
|
|
|
35649
35999
|
// src/commands/review/runAndSynthesise.ts
|
|
35650
|
-
function
|
|
36000
|
+
function failed2(results) {
|
|
35651
36001
|
return results.filter((r) => r.exitCode !== 0);
|
|
35652
36002
|
}
|
|
35653
36003
|
async function runAndSynthesise(args) {
|
|
@@ -35664,7 +36014,7 @@ async function runAndSynthesise(args) {
|
|
|
35664
36014
|
models: args.models
|
|
35665
36015
|
}
|
|
35666
36016
|
);
|
|
35667
|
-
const failures =
|
|
36017
|
+
const failures = failed2(results);
|
|
35668
36018
|
const reason4 = skipReason(results, failures, args.strict);
|
|
35669
36019
|
if (reason4) {
|
|
35670
36020
|
console.error(reason4);
|
|
@@ -39548,11 +39898,19 @@ function resolveRunCwd(config, baseDir = runConfigBaseDir()) {
|
|
|
39548
39898
|
|
|
39549
39899
|
// src/commands/run/runPreCommands.ts
|
|
39550
39900
|
import { execSync as execSync66 } from "child_process";
|
|
39551
|
-
function
|
|
39901
|
+
function writeCapturedOutput(error) {
|
|
39902
|
+
for (const stream of ["stdout", "stderr"]) {
|
|
39903
|
+
const output = error[stream];
|
|
39904
|
+
if (output) process.stdout.write(output);
|
|
39905
|
+
}
|
|
39906
|
+
}
|
|
39907
|
+
function runPreCommands(pre, cwd, quiet) {
|
|
39552
39908
|
for (const cmd of pre) {
|
|
39553
39909
|
try {
|
|
39554
|
-
execSync66(cmd, { stdio: "inherit", cwd });
|
|
39910
|
+
execSync66(cmd, { stdio: quiet ? "pipe" : "inherit", cwd });
|
|
39555
39911
|
} catch (error) {
|
|
39912
|
+
if (quiet && error && typeof error === "object")
|
|
39913
|
+
writeCapturedOutput(error);
|
|
39556
39914
|
const code = error && typeof error === "object" && "status" in error ? error.status : 1;
|
|
39557
39915
|
process.exit(code);
|
|
39558
39916
|
}
|
|
@@ -39578,14 +39936,14 @@ async function runWatchBuild(entry) {
|
|
|
39578
39936
|
if (!(error instanceof MissingRunCwdError)) throw error;
|
|
39579
39937
|
return { kind: "failed", exitCode: 1, output: error.message };
|
|
39580
39938
|
}
|
|
39581
|
-
if (config.pre) runPreCommands(config.pre, cwd);
|
|
39939
|
+
if (config.pre) runPreCommands(config.pre, cwd, true);
|
|
39582
39940
|
return toBuildOutcome(
|
|
39583
39941
|
await runCommandToCompletion(
|
|
39584
39942
|
config.command,
|
|
39585
39943
|
[...config.args ?? [], ...resolveParams(config.params, [])],
|
|
39586
39944
|
config.env,
|
|
39587
39945
|
cwd,
|
|
39588
|
-
|
|
39946
|
+
true
|
|
39589
39947
|
)
|
|
39590
39948
|
);
|
|
39591
39949
|
}
|
|
@@ -39605,7 +39963,13 @@ async function reportBuildOrExit(entry) {
|
|
|
39605
39963
|
// src/commands/watch/runPostBuildSync.ts
|
|
39606
39964
|
async function runPostBuildSync() {
|
|
39607
39965
|
return toBuildOutcome(
|
|
39608
|
-
await runCommandToCompletion(
|
|
39966
|
+
await runCommandToCompletion(
|
|
39967
|
+
"assist",
|
|
39968
|
+
["sync", "--yes"],
|
|
39969
|
+
void 0,
|
|
39970
|
+
void 0,
|
|
39971
|
+
true
|
|
39972
|
+
)
|
|
39609
39973
|
);
|
|
39610
39974
|
}
|
|
39611
39975
|
|
|
@@ -40652,7 +41016,8 @@ function toSessionInfo(session) {
|
|
|
40652
41016
|
pendingPrPreview,
|
|
40653
41017
|
undurable,
|
|
40654
41018
|
lastUserMessage,
|
|
40655
|
-
activeViewer
|
|
41019
|
+
activeViewer,
|
|
41020
|
+
activeViewerNode
|
|
40656
41021
|
} = session;
|
|
40657
41022
|
return {
|
|
40658
41023
|
...toSessionRunInfo(session),
|
|
@@ -40679,6 +41044,7 @@ function toSessionInfo(session) {
|
|
|
40679
41044
|
undurable,
|
|
40680
41045
|
lastUserMessage,
|
|
40681
41046
|
activeViewer,
|
|
41047
|
+
activeViewerNode,
|
|
40682
41048
|
joinable: joinRefusal(session) === void 0
|
|
40683
41049
|
};
|
|
40684
41050
|
}
|
|
@@ -40697,11 +41063,29 @@ function broadcastSessions(sessions, clients, linkedSessions = [], active) {
|
|
|
40697
41063
|
broadcast(peers, { type: "sessions", sessions: local, active: {} });
|
|
40698
41064
|
}
|
|
40699
41065
|
|
|
41066
|
+
// src/commands/sessions/daemon/releaseViewer.ts
|
|
41067
|
+
function releaseViewer(sessions, viewerId) {
|
|
41068
|
+
let released = false;
|
|
41069
|
+
for (const s of sessions.values()) {
|
|
41070
|
+
if (s.activeViewer !== viewerId) continue;
|
|
41071
|
+
daemonLog(
|
|
41072
|
+
`session ${s.id} active viewer ${viewerId} on ${s.activeViewerNode ?? "unknown node"} left`
|
|
41073
|
+
);
|
|
41074
|
+
s.activeViewer = void 0;
|
|
41075
|
+
s.activeViewerNode = void 0;
|
|
41076
|
+
released = true;
|
|
41077
|
+
}
|
|
41078
|
+
return released;
|
|
41079
|
+
}
|
|
41080
|
+
|
|
40700
41081
|
// src/commands/sessions/daemon/claimViewer.ts
|
|
40701
|
-
function claimViewer(session, { viewerId, onClaim }, via) {
|
|
41082
|
+
function claimViewer(session, { viewerId, viewerNode, onClaim }, via) {
|
|
40702
41083
|
if (!viewerId || session.activeViewer === viewerId) return;
|
|
40703
41084
|
session.activeViewer = viewerId;
|
|
40704
|
-
|
|
41085
|
+
session.activeViewerNode = viewerNode;
|
|
41086
|
+
daemonLog(
|
|
41087
|
+
`session ${session.id} active viewer ${viewerId} on ${viewerNode ?? "unknown node"} (${via})`
|
|
41088
|
+
);
|
|
40705
41089
|
onClaim?.();
|
|
40706
41090
|
}
|
|
40707
41091
|
|
|
@@ -40743,6 +41127,29 @@ function resizePty(s, cols, rows) {
|
|
|
40743
41127
|
}
|
|
40744
41128
|
}
|
|
40745
41129
|
|
|
41130
|
+
// src/commands/sessions/daemon/ViewerConnections.ts
|
|
41131
|
+
var ViewerConnections = class {
|
|
41132
|
+
byClient = /* @__PURE__ */ new Map();
|
|
41133
|
+
track(client, viewerId) {
|
|
41134
|
+
const ids = this.byClient.get(client) ?? /* @__PURE__ */ new Set();
|
|
41135
|
+
ids.add(viewerId);
|
|
41136
|
+
this.byClient.set(client, ids);
|
|
41137
|
+
}
|
|
41138
|
+
releaseUnheld(client, viewerId) {
|
|
41139
|
+
const ids = this.byClient.get(client);
|
|
41140
|
+
if (!ids) return [];
|
|
41141
|
+
const dropped = viewerId === void 0 ? [...ids] : [viewerId];
|
|
41142
|
+
for (const id of dropped) ids.delete(id);
|
|
41143
|
+
if (ids.size === 0) this.byClient.delete(client);
|
|
41144
|
+
return dropped.filter((id) => !this.isHeld(id));
|
|
41145
|
+
}
|
|
41146
|
+
isHeld(viewerId) {
|
|
41147
|
+
for (const ids of this.byClient.values())
|
|
41148
|
+
if (ids.has(viewerId)) return true;
|
|
41149
|
+
return false;
|
|
41150
|
+
}
|
|
41151
|
+
};
|
|
41152
|
+
|
|
40746
41153
|
// src/commands/sessions/daemon/watchPromptSubmit.ts
|
|
40747
41154
|
var CARRIAGE_RETURN = "\r";
|
|
40748
41155
|
function isPromptSubmit(data) {
|
|
@@ -40793,14 +41200,29 @@ function setAutoAdvance(sessions, id, enabled) {
|
|
|
40793
41200
|
|
|
40794
41201
|
// src/commands/sessions/daemon/SessionIo.ts
|
|
40795
41202
|
var SessionIo = class {
|
|
40796
|
-
constructor(sessions, onStatusChange, notify2) {
|
|
41203
|
+
constructor(sessions, onStatusChange, notify2, links2) {
|
|
40797
41204
|
this.sessions = sessions;
|
|
40798
41205
|
this.onStatusChange = onStatusChange;
|
|
40799
41206
|
this.notify = notify2;
|
|
41207
|
+
this.links = links2;
|
|
41208
|
+
}
|
|
41209
|
+
viewers = new ViewerConnections();
|
|
41210
|
+
trackViewer(client, d) {
|
|
41211
|
+
if (typeof d.viewerId !== "string") return;
|
|
41212
|
+
this.viewers.track(client, d.viewerId);
|
|
41213
|
+
d.viewerNode ??= this.links.localNode();
|
|
40800
41214
|
}
|
|
40801
|
-
|
|
41215
|
+
viewerLeft(client, viewerId) {
|
|
41216
|
+
let released = false;
|
|
41217
|
+
for (const id of this.viewers.releaseUnheld(client, viewerId)) {
|
|
41218
|
+
released = releaseViewer(this.sessions, id) || released;
|
|
41219
|
+
this.links.viewerLeft(client, id);
|
|
41220
|
+
}
|
|
41221
|
+
if (released) this.notify();
|
|
41222
|
+
}
|
|
41223
|
+
write(id, data, viewer) {
|
|
40802
41224
|
writeToSession(this.sessions, id, data, this.onStatusChange(), {
|
|
40803
|
-
|
|
41225
|
+
...viewer,
|
|
40804
41226
|
onClaim: this.notify
|
|
40805
41227
|
});
|
|
40806
41228
|
}
|
|
@@ -43246,6 +43668,7 @@ function shouldAutoDismiss(session, exitCode) {
|
|
|
43246
43668
|
}
|
|
43247
43669
|
|
|
43248
43670
|
// src/commands/sessions/daemon/finishStatusChange.ts
|
|
43671
|
+
var DIVERGED_EXIT_CODE = 3;
|
|
43249
43672
|
function finishStatusChange(session, status2, exitCode, deps2) {
|
|
43250
43673
|
daemonLog(`session ${session.id} status: ${session.status} -> ${status2}`);
|
|
43251
43674
|
void flushPhaseActiveMs(session);
|
|
@@ -43253,6 +43676,8 @@ function finishStatusChange(session, status2, exitCode, deps2) {
|
|
|
43253
43676
|
if (status2 === "waiting")
|
|
43254
43677
|
startTranscriptTitleGeneration(session, deps2.notify);
|
|
43255
43678
|
if (status2 !== "stopped") session.undurable = void 0;
|
|
43679
|
+
if (session.watcher === true && exitCode === DIVERGED_EXIT_CODE)
|
|
43680
|
+
deps2.escalateDivergence?.(session);
|
|
43256
43681
|
const autoRun = shouldAutoRun(session);
|
|
43257
43682
|
if (autoRun.run) {
|
|
43258
43683
|
deps2.reuseForRun(session, autoRun.itemId);
|
|
@@ -43269,19 +43694,18 @@ function finishStatusChange(session, status2, exitCode, deps2) {
|
|
|
43269
43694
|
}
|
|
43270
43695
|
|
|
43271
43696
|
// src/commands/sessions/daemon/applyStatusChange.ts
|
|
43272
|
-
function applyStatusChange(session, status2, exitCode,
|
|
43697
|
+
function applyStatusChange(session, status2, exitCode, deps2, sharedWithOtherAgents = () => false) {
|
|
43273
43698
|
if (session.status === status2) return;
|
|
43274
|
-
const deps2 = { dismiss, notify: notify2, reuseForRun };
|
|
43275
43699
|
if (endsStreamLife(session, status2, sharedWithOtherAgents)) {
|
|
43276
43700
|
session.closing = true;
|
|
43277
43701
|
daemonLog(
|
|
43278
43702
|
`session ${session.id} closing: checking durability of ${session.worktree?.path} before reap`
|
|
43279
43703
|
);
|
|
43280
|
-
|
|
43704
|
+
deps2.notify();
|
|
43281
43705
|
void resolveCloseDurability(
|
|
43282
43706
|
session,
|
|
43283
43707
|
() => finishStatusChange(session, "done", exitCode, deps2),
|
|
43284
|
-
|
|
43708
|
+
deps2.notify
|
|
43285
43709
|
);
|
|
43286
43710
|
return;
|
|
43287
43711
|
}
|
|
@@ -43289,18 +43713,378 @@ function applyStatusChange(session, status2, exitCode, dismiss, notify2, reuseFo
|
|
|
43289
43713
|
}
|
|
43290
43714
|
|
|
43291
43715
|
// src/commands/sessions/daemon/makeStatusChangeHandler.ts
|
|
43292
|
-
function makeStatusChangeHandler(sessions,
|
|
43716
|
+
function makeStatusChangeHandler(sessions, deps2) {
|
|
43293
43717
|
return (s, status2, exitCode) => applyStatusChange(
|
|
43294
43718
|
s,
|
|
43295
43719
|
status2,
|
|
43296
43720
|
exitCode,
|
|
43297
|
-
|
|
43298
|
-
notify2,
|
|
43299
|
-
reuseForRun,
|
|
43721
|
+
deps2,
|
|
43300
43722
|
(session) => otherTreeHolders(sessions, session).length > 0
|
|
43301
43723
|
);
|
|
43302
43724
|
}
|
|
43303
43725
|
|
|
43726
|
+
// src/commands/sessions/daemon/worktree/detectExistingWorktree.ts
|
|
43727
|
+
function detectExistingWorktree(cwd) {
|
|
43728
|
+
const repoRoot2 = findRepoRoot(cwd) ?? cwd;
|
|
43729
|
+
if (!worktreeConfigFor(repoRoot2).enabled) return void 0;
|
|
43730
|
+
const clone = mainWorktree(repoRoot2);
|
|
43731
|
+
if (clone && clone !== repoRoot2) return { path: repoRoot2, clone };
|
|
43732
|
+
return void 0;
|
|
43733
|
+
}
|
|
43734
|
+
|
|
43735
|
+
// src/commands/sessions/daemon/worktree/bindNewWorktree.ts
|
|
43736
|
+
function bindNewWorktree(session, alloc, notify2, startHeld = () => {
|
|
43737
|
+
}) {
|
|
43738
|
+
if (alloc.kind !== "worktree" || !alloc.cwd || !alloc.clone || !session)
|
|
43739
|
+
return;
|
|
43740
|
+
session.worktree = { path: alloc.cwd, clone: alloc.clone };
|
|
43741
|
+
if (alloc.created)
|
|
43742
|
+
seedWorktree(alloc.cwd, alloc.clone, () => startHeld(session));
|
|
43743
|
+
notify2();
|
|
43744
|
+
}
|
|
43745
|
+
function bindRestoredWorktrees(sessions) {
|
|
43746
|
+
for (const session of sessions.values()) {
|
|
43747
|
+
if (session.worktree || !session.cwd) continue;
|
|
43748
|
+
const worktree = detectExistingWorktree(session.cwd);
|
|
43749
|
+
if (worktree) session.worktree = worktree;
|
|
43750
|
+
}
|
|
43751
|
+
}
|
|
43752
|
+
function bindResumedWorktree(session, cwd, notify2) {
|
|
43753
|
+
if (!session) return;
|
|
43754
|
+
const worktree = detectExistingWorktree(cwd);
|
|
43755
|
+
if (!worktree) return;
|
|
43756
|
+
session.worktree = worktree;
|
|
43757
|
+
notify2();
|
|
43758
|
+
}
|
|
43759
|
+
|
|
43760
|
+
// src/commands/sessions/daemon/attachReusedRun.ts
|
|
43761
|
+
function attachReusedRun(session, alloc, clients, onStatusChange, tree) {
|
|
43762
|
+
broadcast(clients, { type: "clear", sessionId: session.id });
|
|
43763
|
+
if (alloc)
|
|
43764
|
+
bindNewWorktree(
|
|
43765
|
+
session,
|
|
43766
|
+
alloc,
|
|
43767
|
+
tree?.notify ?? (() => {
|
|
43768
|
+
}),
|
|
43769
|
+
tree?.startHeld
|
|
43770
|
+
);
|
|
43771
|
+
else wirePtyEvents(session, clients, onStatusChange);
|
|
43772
|
+
}
|
|
43773
|
+
|
|
43774
|
+
// src/commands/sessions/daemon/resetCardForRun.ts
|
|
43775
|
+
function resetCardForRun(session, assistArgs) {
|
|
43776
|
+
session.pty?.kill();
|
|
43777
|
+
session.gitWatcher?.close();
|
|
43778
|
+
session.gitWatcher = void 0;
|
|
43779
|
+
session.undurable = void 0;
|
|
43780
|
+
session.assistArgs = assistArgs;
|
|
43781
|
+
session.name = `assist ${assistArgs.join(" ")}`;
|
|
43782
|
+
session.commandType = "assist";
|
|
43783
|
+
session.activity = void 0;
|
|
43784
|
+
removeActivity(session.id);
|
|
43785
|
+
session.scrollback = "";
|
|
43786
|
+
session.startedAt = Date.now();
|
|
43787
|
+
session.runningMs = 0;
|
|
43788
|
+
session.runningSince = null;
|
|
43789
|
+
session.usageSeeded = false;
|
|
43790
|
+
setStatus2(session, "running");
|
|
43791
|
+
session.restored = void 0;
|
|
43792
|
+
session.launchedFrom = void 0;
|
|
43793
|
+
}
|
|
43794
|
+
|
|
43795
|
+
// src/commands/sessions/daemon/refuseSpawn.ts
|
|
43796
|
+
function refuseSpawn(session, error, clients, onStatusChange, stage = "respawned") {
|
|
43797
|
+
const reason4 = error instanceof Error ? error.message : String(error);
|
|
43798
|
+
session.pty = null;
|
|
43799
|
+
session.pendingStart = void 0;
|
|
43800
|
+
session.error = reason4;
|
|
43801
|
+
daemonLog(
|
|
43802
|
+
`session ${session.id} ("${session.name}") not ${stage}: ${reason4}`
|
|
43803
|
+
);
|
|
43804
|
+
broadcast(clients, { type: "clear", sessionId: session.id });
|
|
43805
|
+
emitSessionOutput(
|
|
43806
|
+
session,
|
|
43807
|
+
clients,
|
|
43808
|
+
`\r
|
|
43809
|
+
\x1B[31mCannot start this session: ${reason4}\x1B[0m\r
|
|
43810
|
+
`
|
|
43811
|
+
);
|
|
43812
|
+
onStatusChange(session, "error");
|
|
43813
|
+
}
|
|
43814
|
+
|
|
43815
|
+
// src/commands/sessions/daemon/startReusedRunPty.ts
|
|
43816
|
+
function startReusedRunPty(session, assistArgs, itemId2, hold, clients, onStatusChange) {
|
|
43817
|
+
try {
|
|
43818
|
+
Object.assign(
|
|
43819
|
+
session,
|
|
43820
|
+
startOrHoldPty(
|
|
43821
|
+
() => spawnPty(["assist", ...assistArgs], session.cwd, session.id),
|
|
43822
|
+
hold
|
|
43823
|
+
)
|
|
43824
|
+
);
|
|
43825
|
+
} catch (error) {
|
|
43826
|
+
refuseSpawn(
|
|
43827
|
+
session,
|
|
43828
|
+
error,
|
|
43829
|
+
clients,
|
|
43830
|
+
onStatusChange,
|
|
43831
|
+
`reused for backlog run ${itemId2}`
|
|
43832
|
+
);
|
|
43833
|
+
return false;
|
|
43834
|
+
}
|
|
43835
|
+
return true;
|
|
43836
|
+
}
|
|
43837
|
+
|
|
43838
|
+
// src/commands/sessions/daemon/createAssistSession.ts
|
|
43839
|
+
function createAssistSession(id, assistArgs, cwd, meta, holdPty) {
|
|
43840
|
+
const startedAt = Date.now();
|
|
43841
|
+
return {
|
|
43842
|
+
id,
|
|
43843
|
+
name: `assist ${assistArgs.join(" ")}`,
|
|
43844
|
+
title: meta?.title,
|
|
43845
|
+
subtitle: meta?.subtitle,
|
|
43846
|
+
commandType: "assist",
|
|
43847
|
+
status: "running",
|
|
43848
|
+
startedAt,
|
|
43849
|
+
runningMs: 0,
|
|
43850
|
+
runningSince: startedAt,
|
|
43851
|
+
waitingSince: null,
|
|
43852
|
+
...startOrHoldPty(
|
|
43853
|
+
() => spawnPty(["assist", ...assistArgs], cwd, id),
|
|
43854
|
+
holdPty
|
|
43855
|
+
),
|
|
43856
|
+
scrollback: "",
|
|
43857
|
+
assistArgs,
|
|
43858
|
+
cwd
|
|
43859
|
+
};
|
|
43860
|
+
}
|
|
43861
|
+
|
|
43862
|
+
// src/commands/sessions/daemon/createWatcherSession.ts
|
|
43863
|
+
var WATCHER_ARGS = ["watch", "loop"];
|
|
43864
|
+
function createWatcherSession(id, cwd) {
|
|
43865
|
+
return {
|
|
43866
|
+
...createAssistSession(id, WATCHER_ARGS, cwd),
|
|
43867
|
+
watcher: true
|
|
43868
|
+
};
|
|
43869
|
+
}
|
|
43870
|
+
|
|
43871
|
+
// src/commands/sessions/daemon/worktree/boundTreeRoots.ts
|
|
43872
|
+
function boundTreeRoots(sessions, exclude) {
|
|
43873
|
+
const roots = /* @__PURE__ */ new Set();
|
|
43874
|
+
for (const session of sessions.values()) {
|
|
43875
|
+
if (session === exclude) continue;
|
|
43876
|
+
const root2 = treeRootOf(session);
|
|
43877
|
+
if (root2 && !roots.has(root2) && holdsTree(session, root2)) roots.add(root2);
|
|
43878
|
+
}
|
|
43879
|
+
return roots;
|
|
43880
|
+
}
|
|
43881
|
+
function treeRootOf(session) {
|
|
43882
|
+
if (!session.cwd) return void 0;
|
|
43883
|
+
return canonicalTreePath(findRepoRoot(session.cwd) ?? session.cwd);
|
|
43884
|
+
}
|
|
43885
|
+
function holdsTree(session, root2) {
|
|
43886
|
+
if (session.closing) return true;
|
|
43887
|
+
if (session.status !== "done" && session.status !== "error") return true;
|
|
43888
|
+
if (!worktreeConfigFor(root2).enabled) return true;
|
|
43889
|
+
const durability = checkDurabilitySync(root2);
|
|
43890
|
+
daemonLog(`tree ${root2} ${releaseReason(durability, session)}`);
|
|
43891
|
+
return !durability.durable;
|
|
43892
|
+
}
|
|
43893
|
+
function releaseReason(durability, session) {
|
|
43894
|
+
if (!durability.durable)
|
|
43895
|
+
return `still held by finished session ${session.id}: ${durability.reason}`;
|
|
43896
|
+
if (durability.gone)
|
|
43897
|
+
return `free for allocation: session ${session.id}'s directory is gone from disk \u2014 nothing to land, not landed work`;
|
|
43898
|
+
return `free for allocation: session ${session.id} finished and its work is landed`;
|
|
43899
|
+
}
|
|
43900
|
+
|
|
43901
|
+
// src/commands/sessions/daemon/worktree/allocateAndBind.ts
|
|
43902
|
+
function allocateAndBind(ctx, cwd, create, options2 = {}, context) {
|
|
43903
|
+
const alloc = allocateTree(cwd, boundTreeRoots(ctx.sessions), options2);
|
|
43904
|
+
const needsSeeding = alloc.kind === "worktree" && alloc.created === true;
|
|
43905
|
+
const id = ctx.spawnWith(
|
|
43906
|
+
(sid) => create(sid, alloc.cwd, needsSeeding),
|
|
43907
|
+
context
|
|
43908
|
+
);
|
|
43909
|
+
bindNewWorktree(ctx.sessions.get(id), alloc, ctx.notify, ctx.startHeld);
|
|
43910
|
+
return id;
|
|
43911
|
+
}
|
|
43912
|
+
|
|
43913
|
+
// src/commands/sessions/daemon/worktree/ensureWatcher.ts
|
|
43914
|
+
function ensureWatcher(ctx, cwd) {
|
|
43915
|
+
if (!cwd) return void 0;
|
|
43916
|
+
const repoRoot2 = findRepoRoot(cwd) ?? cwd;
|
|
43917
|
+
const cfg = worktreeConfigFor(repoRoot2);
|
|
43918
|
+
if (!cfg.enabled || cfg.watcher !== true) return void 0;
|
|
43919
|
+
const clone = resolveClone(repoRoot2);
|
|
43920
|
+
const live = liveWatcherFor(ctx.sessions, clone);
|
|
43921
|
+
if (live) {
|
|
43922
|
+
daemonLog(
|
|
43923
|
+
`no watcher spawned for the clone ${clone}: session ${live.id} is already watching it (${live.status})`
|
|
43924
|
+
);
|
|
43925
|
+
return void 0;
|
|
43926
|
+
}
|
|
43927
|
+
const id = allocateAndBind(
|
|
43928
|
+
ctx,
|
|
43929
|
+
clone,
|
|
43930
|
+
(sid, resolvedCwd) => createWatcherSession(sid, resolvedCwd ?? clone),
|
|
43931
|
+
{ inPlace: true }
|
|
43932
|
+
);
|
|
43933
|
+
daemonLog(
|
|
43934
|
+
`spawned watcher session ${id} running assist ${WATCHER_ARGS.join(" ")} in the clone ${clone}`
|
|
43935
|
+
);
|
|
43936
|
+
return id;
|
|
43937
|
+
}
|
|
43938
|
+
|
|
43939
|
+
// src/commands/sessions/daemon/worktree/isBacklogRunArgs.ts
|
|
43940
|
+
var ITEM_ID = /^a?\d+$/;
|
|
43941
|
+
function isBacklogRunArgs(args) {
|
|
43942
|
+
const [command, subcommand, ...rest] = args;
|
|
43943
|
+
if (command !== "backlog" || subcommand !== "run") return false;
|
|
43944
|
+
return rest.some((arg) => ITEM_ID.test(arg));
|
|
43945
|
+
}
|
|
43946
|
+
|
|
43947
|
+
// src/commands/sessions/daemon/worktree/isPrCheckoutArgs.ts
|
|
43948
|
+
var CHECKOUT_COMMANDS = /* @__PURE__ */ new Set([
|
|
43949
|
+
"review",
|
|
43950
|
+
"review-pr-comments",
|
|
43951
|
+
"fix-conflict"
|
|
43952
|
+
]);
|
|
43953
|
+
function isPrCheckoutArgs(args) {
|
|
43954
|
+
const [command, ...rest] = args;
|
|
43955
|
+
if (!command || !CHECKOUT_COMMANDS.has(command)) return false;
|
|
43956
|
+
return rest.some((arg) => /^[1-9]\d*$/.test(arg));
|
|
43957
|
+
}
|
|
43958
|
+
|
|
43959
|
+
// src/commands/sessions/daemon/worktree/isCommittingArgs.ts
|
|
43960
|
+
function isCommittingArgs(args) {
|
|
43961
|
+
return isPrCheckoutArgs(args) || isBacklogRunArgs(args);
|
|
43962
|
+
}
|
|
43963
|
+
|
|
43964
|
+
// src/commands/sessions/daemon/worktree/planReuseTree.ts
|
|
43965
|
+
function planReuseTree(session, ctx, options2 = {}) {
|
|
43966
|
+
if (!ctx || !session.cwd || session.worktree) return void 0;
|
|
43967
|
+
const alloc = allocateTree(
|
|
43968
|
+
session.cwd,
|
|
43969
|
+
boundTreeRoots(ctx.sessions, session),
|
|
43970
|
+
options2
|
|
43971
|
+
);
|
|
43972
|
+
if (alloc.kind !== "worktree" || !alloc.cwd) return void 0;
|
|
43973
|
+
daemonLog(
|
|
43974
|
+
`session ${session.id} moved from ${session.cwd} to ${alloc.cwd} for its chained run`
|
|
43975
|
+
);
|
|
43976
|
+
return alloc;
|
|
43977
|
+
}
|
|
43978
|
+
|
|
43979
|
+
// src/commands/sessions/daemon/failChainedRun.ts
|
|
43980
|
+
function failChainedRun(session, itemId2, error, clients, tree) {
|
|
43981
|
+
const reason4 = error instanceof Error ? error.message : String(error);
|
|
43982
|
+
session.pty = null;
|
|
43983
|
+
session.error = `backlog run ${itemId2} could not start: ${reason4}`;
|
|
43984
|
+
setStatus2(session, "error");
|
|
43985
|
+
daemonLog(
|
|
43986
|
+
`session ${session.id} not reused for backlog run ${itemId2}: workspace allocation failed: ${reason4}`
|
|
43987
|
+
);
|
|
43988
|
+
broadcast(clients, { type: "clear", sessionId: session.id });
|
|
43989
|
+
emitSessionOutput(
|
|
43990
|
+
session,
|
|
43991
|
+
clients,
|
|
43992
|
+
`\r
|
|
43993
|
+
\x1B[31m${session.error}\x1B[0m\r
|
|
43994
|
+
`
|
|
43995
|
+
);
|
|
43996
|
+
tree?.notify();
|
|
43997
|
+
}
|
|
43998
|
+
|
|
43999
|
+
// src/commands/sessions/daemon/reuseSessionForRun.ts
|
|
44000
|
+
function reuseSessionForRun(session, itemId2, clients, onStatusChange, tree) {
|
|
44001
|
+
const assistArgs = ["backlog", "run", String(itemId2)];
|
|
44002
|
+
const originCwd = session.cwd;
|
|
44003
|
+
resetCardForRun(session, assistArgs);
|
|
44004
|
+
let alloc;
|
|
44005
|
+
try {
|
|
44006
|
+
alloc = planReuseTree(session, tree, {
|
|
44007
|
+
commits: isCommittingArgs(assistArgs),
|
|
44008
|
+
backlogRun: true
|
|
44009
|
+
});
|
|
44010
|
+
} catch (error) {
|
|
44011
|
+
failChainedRun(session, itemId2, error, clients, tree);
|
|
44012
|
+
return;
|
|
44013
|
+
}
|
|
44014
|
+
if (alloc) session.cwd = alloc.cwd;
|
|
44015
|
+
const started = startReusedRunPty(
|
|
44016
|
+
session,
|
|
44017
|
+
assistArgs,
|
|
44018
|
+
itemId2,
|
|
44019
|
+
alloc !== void 0,
|
|
44020
|
+
clients,
|
|
44021
|
+
onStatusChange
|
|
44022
|
+
);
|
|
44023
|
+
if (!started) return;
|
|
44024
|
+
if (tree) ensureWatcher(tree, originCwd);
|
|
44025
|
+
attachReusedRun(session, alloc, clients, onStatusChange, tree);
|
|
44026
|
+
daemonLog(
|
|
44027
|
+
`session ${session.id} reused for backlog run ${itemId2}: ${session.name}`
|
|
44028
|
+
);
|
|
44029
|
+
}
|
|
44030
|
+
|
|
44031
|
+
// src/commands/sessions/daemon/worktree/divergencePrompt.ts
|
|
44032
|
+
var MAX_REASON_LINES = 20;
|
|
44033
|
+
var MAX_REASON_CHARS = 2e3;
|
|
44034
|
+
function divergencePrompt(clone, scrollback) {
|
|
44035
|
+
const reason4 = stripAnsi(scrollback).split(/\r?\n|\r/).map((line) => line.trimEnd()).filter((line) => line.trim().length > 0).slice(-MAX_REASON_LINES).join("\n").slice(-MAX_REASON_CHARS);
|
|
44036
|
+
return [
|
|
44037
|
+
`The watcher in ${clone} stopped because \`assist watch wait --pull --build\` could not fast-forward the branch (exit 3). The tail of its output:`,
|
|
44038
|
+
"",
|
|
44039
|
+
"```",
|
|
44040
|
+
reason4 || "(no output captured)",
|
|
44041
|
+
"```",
|
|
44042
|
+
"",
|
|
44043
|
+
"Diagnose why the local branch has diverged from its upstream and report what you find and the options for reconciling it. Do not force-push, `git reset`, or otherwise discard commits on either side \u2014 leave the decision to the user."
|
|
44044
|
+
].join("\n");
|
|
44045
|
+
}
|
|
44046
|
+
|
|
44047
|
+
// src/commands/sessions/daemon/worktree/escalateDivergence.ts
|
|
44048
|
+
var ENDED = ["done", "error", "stopped"];
|
|
44049
|
+
function escalateDivergence(ctx, watcher) {
|
|
44050
|
+
if (!watcher.cwd) return void 0;
|
|
44051
|
+
const clone = canonicalTreePath(watcher.cwd);
|
|
44052
|
+
const live = [...ctx.sessions.values()].find(
|
|
44053
|
+
(s) => s.divergenceEscalation === true && s.cwd !== void 0 && canonicalTreePath(s.cwd) === clone && !ENDED.includes(s.status)
|
|
44054
|
+
);
|
|
44055
|
+
if (live) {
|
|
44056
|
+
daemonLog(
|
|
44057
|
+
`watcher session ${watcher.id} exited 3 (divergence) in the clone ${clone}: no escalation spawned, session ${live.id} is already diagnosing it (${live.status})`
|
|
44058
|
+
);
|
|
44059
|
+
return void 0;
|
|
44060
|
+
}
|
|
44061
|
+
const prompt = divergencePrompt(clone, watcher.scrollback);
|
|
44062
|
+
const id = allocateAndBind(
|
|
44063
|
+
ctx,
|
|
44064
|
+
clone,
|
|
44065
|
+
(sid, resolvedCwd) => ({
|
|
44066
|
+
...createSession(sid, { prompt, cwd: resolvedCwd ?? clone }),
|
|
44067
|
+
divergenceEscalation: true
|
|
44068
|
+
}),
|
|
44069
|
+
{ inPlace: true }
|
|
44070
|
+
);
|
|
44071
|
+
daemonLog(
|
|
44072
|
+
`watcher session ${watcher.id} exited 3 (divergence) in the clone ${clone}: spawned escalation session ${id} to diagnose it`
|
|
44073
|
+
);
|
|
44074
|
+
return id;
|
|
44075
|
+
}
|
|
44076
|
+
|
|
44077
|
+
// src/commands/sessions/daemon/managerStatusChangeHandler.ts
|
|
44078
|
+
function managerStatusChangeHandler(sessions, clients, dismiss, notify2, treeCtx) {
|
|
44079
|
+
const handler = makeStatusChangeHandler(sessions, {
|
|
44080
|
+
dismiss,
|
|
44081
|
+
notify: notify2,
|
|
44082
|
+
reuseForRun: (session, itemId2) => reuseSessionForRun(session, itemId2, clients, handler, treeCtx()),
|
|
44083
|
+
escalateDivergence: (watcher) => escalateDivergence(treeCtx(), watcher)
|
|
44084
|
+
});
|
|
44085
|
+
return handler;
|
|
44086
|
+
}
|
|
44087
|
+
|
|
43304
44088
|
// src/commands/sessions/daemon/applySetStatus.ts
|
|
43305
44089
|
function applySetStatus(sessions, id, status2, source, onStatusChange, rebind) {
|
|
43306
44090
|
const session = sessions.get(id);
|
|
@@ -43350,26 +44134,6 @@ function setStatusFromHook(sessions, report2, notify2, onStatusChange) {
|
|
|
43350
44134
|
);
|
|
43351
44135
|
}
|
|
43352
44136
|
|
|
43353
|
-
// src/commands/sessions/daemon/refuseSpawn.ts
|
|
43354
|
-
function refuseSpawn(session, error, clients, onStatusChange, stage = "respawned") {
|
|
43355
|
-
const reason4 = error instanceof Error ? error.message : String(error);
|
|
43356
|
-
session.pty = null;
|
|
43357
|
-
session.pendingStart = void 0;
|
|
43358
|
-
session.error = reason4;
|
|
43359
|
-
daemonLog(
|
|
43360
|
-
`session ${session.id} ("${session.name}") not ${stage}: ${reason4}`
|
|
43361
|
-
);
|
|
43362
|
-
broadcast(clients, { type: "clear", sessionId: session.id });
|
|
43363
|
-
emitSessionOutput(
|
|
43364
|
-
session,
|
|
43365
|
-
clients,
|
|
43366
|
-
`\r
|
|
43367
|
-
\x1B[31mCannot start this session: ${reason4}\x1B[0m\r
|
|
43368
|
-
`
|
|
43369
|
-
);
|
|
43370
|
-
onStatusChange(session, "error");
|
|
43371
|
-
}
|
|
43372
|
-
|
|
43373
44137
|
// src/commands/sessions/daemon/respawnSession.ts
|
|
43374
44138
|
function respawnSession(session, respawn, status2, clients, onStatusChange) {
|
|
43375
44139
|
session.gitWatcher?.close();
|
|
@@ -43566,6 +44330,13 @@ function restartManagedSession(sessions, id, clients, onStatusChange) {
|
|
|
43566
44330
|
return { ok: true };
|
|
43567
44331
|
}
|
|
43568
44332
|
|
|
44333
|
+
// src/commands/sessions/daemon/restartAndNotify.ts
|
|
44334
|
+
function restartAndNotify(sessions, id, clients, onStatusChange, notify2) {
|
|
44335
|
+
const result = restartManagedSession(sessions, id, clients, onStatusChange);
|
|
44336
|
+
if (result.ok) notify2();
|
|
44337
|
+
return result;
|
|
44338
|
+
}
|
|
44339
|
+
|
|
43569
44340
|
// src/commands/sessions/daemon/releaseClient.ts
|
|
43570
44341
|
function releaseClient(client, clients, prPreview, verify) {
|
|
43571
44342
|
clients.delete(client);
|
|
@@ -43602,6 +44373,7 @@ function restoreBase(id, persisted) {
|
|
|
43602
44373
|
initialPrompt: persisted.initialPrompt,
|
|
43603
44374
|
starred: persisted.watcher ? false : persisted.starred,
|
|
43604
44375
|
watcher: persisted.watcher,
|
|
44376
|
+
divergenceEscalation: persisted.divergenceEscalation,
|
|
43605
44377
|
design: persisted.design,
|
|
43606
44378
|
auto: persisted.auto,
|
|
43607
44379
|
autoRun: persisted.autoRun,
|
|
@@ -43990,6 +44762,25 @@ function isUpdate(persisted) {
|
|
|
43990
44762
|
return persisted.commandType === "assist" && persisted.assistArgs?.[0] === "update";
|
|
43991
44763
|
}
|
|
43992
44764
|
|
|
44765
|
+
// src/commands/sessions/daemon/migrateClaudeWatcher.ts
|
|
44766
|
+
function migrateClaudeWatcher(id, persisted) {
|
|
44767
|
+
if (!persisted.watcher || persisted.commandType !== "claude")
|
|
44768
|
+
return persisted;
|
|
44769
|
+
daemonLog(
|
|
44770
|
+
`migrating claude /watch watcher session ${id} to assist ${WATCHER_ARGS.join(" ")}`
|
|
44771
|
+
);
|
|
44772
|
+
return {
|
|
44773
|
+
...persisted,
|
|
44774
|
+
name: `assist ${WATCHER_ARGS.join(" ")}`,
|
|
44775
|
+
commandType: "assist",
|
|
44776
|
+
assistArgs: WATCHER_ARGS,
|
|
44777
|
+
initialPrompt: void 0,
|
|
44778
|
+
claudeSessionId: void 0,
|
|
44779
|
+
harness: void 0,
|
|
44780
|
+
harnessSessionId: void 0
|
|
44781
|
+
};
|
|
44782
|
+
}
|
|
44783
|
+
|
|
43993
44784
|
// src/commands/sessions/daemon/needsWrapperRelaunch.ts
|
|
43994
44785
|
var LAUNCH_COMMANDS = ["draft", "feat", "bug", "refine"];
|
|
43995
44786
|
function needsWrapperRelaunch(persisted) {
|
|
@@ -44015,7 +44806,8 @@ function relaunchConsoleWatcher(id, persisted, base) {
|
|
|
44015
44806
|
}
|
|
44016
44807
|
|
|
44017
44808
|
// src/commands/sessions/daemon/restoreSession.ts
|
|
44018
|
-
function restoreSession(id,
|
|
44809
|
+
function restoreSession(id, stored) {
|
|
44810
|
+
const persisted = migrateClaudeWatcher(id, stored);
|
|
44019
44811
|
const base = restoreBase(id, persisted);
|
|
44020
44812
|
if (persisted.status === "stopped") return stoppedSession(base, persisted);
|
|
44021
44813
|
if (isUpdate(persisted)) return updatedSession(id, persisted);
|
|
@@ -44136,40 +44928,6 @@ function accountedTrees(sessions) {
|
|
|
44136
44928
|
return accounted;
|
|
44137
44929
|
}
|
|
44138
44930
|
|
|
44139
|
-
// src/commands/sessions/daemon/worktree/detectExistingWorktree.ts
|
|
44140
|
-
function detectExistingWorktree(cwd) {
|
|
44141
|
-
const repoRoot2 = findRepoRoot(cwd) ?? cwd;
|
|
44142
|
-
if (!worktreeConfigFor(repoRoot2).enabled) return void 0;
|
|
44143
|
-
const clone = mainWorktree(repoRoot2);
|
|
44144
|
-
if (clone && clone !== repoRoot2) return { path: repoRoot2, clone };
|
|
44145
|
-
return void 0;
|
|
44146
|
-
}
|
|
44147
|
-
|
|
44148
|
-
// src/commands/sessions/daemon/worktree/bindNewWorktree.ts
|
|
44149
|
-
function bindNewWorktree(session, alloc, notify2, startHeld = () => {
|
|
44150
|
-
}) {
|
|
44151
|
-
if (alloc.kind !== "worktree" || !alloc.cwd || !alloc.clone || !session)
|
|
44152
|
-
return;
|
|
44153
|
-
session.worktree = { path: alloc.cwd, clone: alloc.clone };
|
|
44154
|
-
if (alloc.created)
|
|
44155
|
-
seedWorktree(alloc.cwd, alloc.clone, () => startHeld(session));
|
|
44156
|
-
notify2();
|
|
44157
|
-
}
|
|
44158
|
-
function bindRestoredWorktrees(sessions) {
|
|
44159
|
-
for (const session of sessions.values()) {
|
|
44160
|
-
if (session.worktree || !session.cwd) continue;
|
|
44161
|
-
const worktree = detectExistingWorktree(session.cwd);
|
|
44162
|
-
if (worktree) session.worktree = worktree;
|
|
44163
|
-
}
|
|
44164
|
-
}
|
|
44165
|
-
function bindResumedWorktree(session, cwd, notify2) {
|
|
44166
|
-
if (!session) return;
|
|
44167
|
-
const worktree = detectExistingWorktree(cwd);
|
|
44168
|
-
if (!worktree) return;
|
|
44169
|
-
session.worktree = worktree;
|
|
44170
|
-
notify2();
|
|
44171
|
-
}
|
|
44172
|
-
|
|
44173
44931
|
// src/commands/sessions/daemon/worktree/reclaimVanishedWorktrees.ts
|
|
44174
44932
|
import { existsSync as existsSync91 } from "fs";
|
|
44175
44933
|
async function reclaimVanishedWorktrees(clone, paths) {
|
|
@@ -44453,257 +45211,6 @@ function runRetry(sessions, id, replace, deps2) {
|
|
|
44453
45211
|
return null;
|
|
44454
45212
|
}
|
|
44455
45213
|
|
|
44456
|
-
// src/commands/sessions/daemon/attachReusedRun.ts
|
|
44457
|
-
function attachReusedRun(session, alloc, clients, onStatusChange, tree) {
|
|
44458
|
-
broadcast(clients, { type: "clear", sessionId: session.id });
|
|
44459
|
-
if (alloc)
|
|
44460
|
-
bindNewWorktree(
|
|
44461
|
-
session,
|
|
44462
|
-
alloc,
|
|
44463
|
-
tree?.notify ?? (() => {
|
|
44464
|
-
}),
|
|
44465
|
-
tree?.startHeld
|
|
44466
|
-
);
|
|
44467
|
-
else wirePtyEvents(session, clients, onStatusChange);
|
|
44468
|
-
}
|
|
44469
|
-
|
|
44470
|
-
// src/commands/sessions/daemon/resetCardForRun.ts
|
|
44471
|
-
function resetCardForRun(session, assistArgs) {
|
|
44472
|
-
session.pty?.kill();
|
|
44473
|
-
session.gitWatcher?.close();
|
|
44474
|
-
session.gitWatcher = void 0;
|
|
44475
|
-
session.undurable = void 0;
|
|
44476
|
-
session.assistArgs = assistArgs;
|
|
44477
|
-
session.name = `assist ${assistArgs.join(" ")}`;
|
|
44478
|
-
session.commandType = "assist";
|
|
44479
|
-
session.activity = void 0;
|
|
44480
|
-
removeActivity(session.id);
|
|
44481
|
-
session.scrollback = "";
|
|
44482
|
-
session.startedAt = Date.now();
|
|
44483
|
-
session.runningMs = 0;
|
|
44484
|
-
session.runningSince = null;
|
|
44485
|
-
session.usageSeeded = false;
|
|
44486
|
-
setStatus2(session, "running");
|
|
44487
|
-
session.restored = void 0;
|
|
44488
|
-
session.launchedFrom = void 0;
|
|
44489
|
-
}
|
|
44490
|
-
|
|
44491
|
-
// src/commands/sessions/daemon/startReusedRunPty.ts
|
|
44492
|
-
function startReusedRunPty(session, assistArgs, itemId2, hold, clients, onStatusChange) {
|
|
44493
|
-
try {
|
|
44494
|
-
Object.assign(
|
|
44495
|
-
session,
|
|
44496
|
-
startOrHoldPty(
|
|
44497
|
-
() => spawnPty(["assist", ...assistArgs], session.cwd, session.id),
|
|
44498
|
-
hold
|
|
44499
|
-
)
|
|
44500
|
-
);
|
|
44501
|
-
} catch (error) {
|
|
44502
|
-
refuseSpawn(
|
|
44503
|
-
session,
|
|
44504
|
-
error,
|
|
44505
|
-
clients,
|
|
44506
|
-
onStatusChange,
|
|
44507
|
-
`reused for backlog run ${itemId2}`
|
|
44508
|
-
);
|
|
44509
|
-
return false;
|
|
44510
|
-
}
|
|
44511
|
-
return true;
|
|
44512
|
-
}
|
|
44513
|
-
|
|
44514
|
-
// src/commands/sessions/daemon/createAssistSession.ts
|
|
44515
|
-
function createAssistSession(id, assistArgs, cwd, meta, holdPty) {
|
|
44516
|
-
const startedAt = Date.now();
|
|
44517
|
-
return {
|
|
44518
|
-
id,
|
|
44519
|
-
name: `assist ${assistArgs.join(" ")}`,
|
|
44520
|
-
title: meta?.title,
|
|
44521
|
-
subtitle: meta?.subtitle,
|
|
44522
|
-
commandType: "assist",
|
|
44523
|
-
status: "running",
|
|
44524
|
-
startedAt,
|
|
44525
|
-
runningMs: 0,
|
|
44526
|
-
runningSince: startedAt,
|
|
44527
|
-
waitingSince: null,
|
|
44528
|
-
...startOrHoldPty(
|
|
44529
|
-
() => spawnPty(["assist", ...assistArgs], cwd, id),
|
|
44530
|
-
holdPty
|
|
44531
|
-
),
|
|
44532
|
-
scrollback: "",
|
|
44533
|
-
assistArgs,
|
|
44534
|
-
cwd
|
|
44535
|
-
};
|
|
44536
|
-
}
|
|
44537
|
-
|
|
44538
|
-
// src/commands/sessions/daemon/createWatcherSession.ts
|
|
44539
|
-
var WATCHER_ARGS = ["watch", "loop"];
|
|
44540
|
-
function createWatcherSession(id, cwd) {
|
|
44541
|
-
return {
|
|
44542
|
-
...createAssistSession(id, WATCHER_ARGS, cwd),
|
|
44543
|
-
watcher: true
|
|
44544
|
-
};
|
|
44545
|
-
}
|
|
44546
|
-
|
|
44547
|
-
// src/commands/sessions/daemon/worktree/boundTreeRoots.ts
|
|
44548
|
-
function boundTreeRoots(sessions, exclude) {
|
|
44549
|
-
const roots = /* @__PURE__ */ new Set();
|
|
44550
|
-
for (const session of sessions.values()) {
|
|
44551
|
-
if (session === exclude) continue;
|
|
44552
|
-
const root2 = treeRootOf(session);
|
|
44553
|
-
if (root2 && !roots.has(root2) && holdsTree(session, root2)) roots.add(root2);
|
|
44554
|
-
}
|
|
44555
|
-
return roots;
|
|
44556
|
-
}
|
|
44557
|
-
function treeRootOf(session) {
|
|
44558
|
-
if (!session.cwd) return void 0;
|
|
44559
|
-
return canonicalTreePath(findRepoRoot(session.cwd) ?? session.cwd);
|
|
44560
|
-
}
|
|
44561
|
-
function holdsTree(session, root2) {
|
|
44562
|
-
if (session.closing) return true;
|
|
44563
|
-
if (session.status !== "done" && session.status !== "error") return true;
|
|
44564
|
-
if (!worktreeConfigFor(root2).enabled) return true;
|
|
44565
|
-
const durability = checkDurabilitySync(root2);
|
|
44566
|
-
daemonLog(`tree ${root2} ${releaseReason(durability, session)}`);
|
|
44567
|
-
return !durability.durable;
|
|
44568
|
-
}
|
|
44569
|
-
function releaseReason(durability, session) {
|
|
44570
|
-
if (!durability.durable)
|
|
44571
|
-
return `still held by finished session ${session.id}: ${durability.reason}`;
|
|
44572
|
-
if (durability.gone)
|
|
44573
|
-
return `free for allocation: session ${session.id}'s directory is gone from disk \u2014 nothing to land, not landed work`;
|
|
44574
|
-
return `free for allocation: session ${session.id} finished and its work is landed`;
|
|
44575
|
-
}
|
|
44576
|
-
|
|
44577
|
-
// src/commands/sessions/daemon/worktree/allocateAndBind.ts
|
|
44578
|
-
function allocateAndBind(ctx, cwd, create, options2 = {}, context) {
|
|
44579
|
-
const alloc = allocateTree(cwd, boundTreeRoots(ctx.sessions), options2);
|
|
44580
|
-
const needsSeeding = alloc.kind === "worktree" && alloc.created === true;
|
|
44581
|
-
const id = ctx.spawnWith(
|
|
44582
|
-
(sid) => create(sid, alloc.cwd, needsSeeding),
|
|
44583
|
-
context
|
|
44584
|
-
);
|
|
44585
|
-
bindNewWorktree(ctx.sessions.get(id), alloc, ctx.notify, ctx.startHeld);
|
|
44586
|
-
return id;
|
|
44587
|
-
}
|
|
44588
|
-
|
|
44589
|
-
// src/commands/sessions/daemon/worktree/ensureWatcher.ts
|
|
44590
|
-
function ensureWatcher(ctx, cwd) {
|
|
44591
|
-
if (!cwd) return void 0;
|
|
44592
|
-
const repoRoot2 = findRepoRoot(cwd) ?? cwd;
|
|
44593
|
-
const cfg = worktreeConfigFor(repoRoot2);
|
|
44594
|
-
if (!cfg.enabled || cfg.watcher !== true) return void 0;
|
|
44595
|
-
const clone = resolveClone(repoRoot2);
|
|
44596
|
-
const live = liveWatcherFor(ctx.sessions, clone);
|
|
44597
|
-
if (live) {
|
|
44598
|
-
daemonLog(
|
|
44599
|
-
`no watcher spawned for the clone ${clone}: session ${live.id} is already watching it (${live.status})`
|
|
44600
|
-
);
|
|
44601
|
-
return void 0;
|
|
44602
|
-
}
|
|
44603
|
-
const id = allocateAndBind(
|
|
44604
|
-
ctx,
|
|
44605
|
-
clone,
|
|
44606
|
-
(sid, resolvedCwd) => createWatcherSession(sid, resolvedCwd ?? clone),
|
|
44607
|
-
{ inPlace: true }
|
|
44608
|
-
);
|
|
44609
|
-
daemonLog(
|
|
44610
|
-
`spawned watcher session ${id} running assist ${WATCHER_ARGS.join(" ")} in the clone ${clone}`
|
|
44611
|
-
);
|
|
44612
|
-
return id;
|
|
44613
|
-
}
|
|
44614
|
-
|
|
44615
|
-
// src/commands/sessions/daemon/worktree/isBacklogRunArgs.ts
|
|
44616
|
-
var ITEM_ID = /^a?\d+$/;
|
|
44617
|
-
function isBacklogRunArgs(args) {
|
|
44618
|
-
const [command, subcommand, ...rest] = args;
|
|
44619
|
-
if (command !== "backlog" || subcommand !== "run") return false;
|
|
44620
|
-
return rest.some((arg) => ITEM_ID.test(arg));
|
|
44621
|
-
}
|
|
44622
|
-
|
|
44623
|
-
// src/commands/sessions/daemon/worktree/isPrCheckoutArgs.ts
|
|
44624
|
-
var CHECKOUT_COMMANDS = /* @__PURE__ */ new Set([
|
|
44625
|
-
"review",
|
|
44626
|
-
"review-pr-comments",
|
|
44627
|
-
"fix-conflict"
|
|
44628
|
-
]);
|
|
44629
|
-
function isPrCheckoutArgs(args) {
|
|
44630
|
-
const [command, ...rest] = args;
|
|
44631
|
-
if (!command || !CHECKOUT_COMMANDS.has(command)) return false;
|
|
44632
|
-
return rest.some((arg) => /^[1-9]\d*$/.test(arg));
|
|
44633
|
-
}
|
|
44634
|
-
|
|
44635
|
-
// src/commands/sessions/daemon/worktree/isCommittingArgs.ts
|
|
44636
|
-
function isCommittingArgs(args) {
|
|
44637
|
-
return isPrCheckoutArgs(args) || isBacklogRunArgs(args);
|
|
44638
|
-
}
|
|
44639
|
-
|
|
44640
|
-
// src/commands/sessions/daemon/worktree/planReuseTree.ts
|
|
44641
|
-
function planReuseTree(session, ctx, options2 = {}) {
|
|
44642
|
-
if (!ctx || !session.cwd || session.worktree) return void 0;
|
|
44643
|
-
const alloc = allocateTree(
|
|
44644
|
-
session.cwd,
|
|
44645
|
-
boundTreeRoots(ctx.sessions, session),
|
|
44646
|
-
options2
|
|
44647
|
-
);
|
|
44648
|
-
if (alloc.kind !== "worktree" || !alloc.cwd) return void 0;
|
|
44649
|
-
daemonLog(
|
|
44650
|
-
`session ${session.id} moved from ${session.cwd} to ${alloc.cwd} for its chained run`
|
|
44651
|
-
);
|
|
44652
|
-
return alloc;
|
|
44653
|
-
}
|
|
44654
|
-
|
|
44655
|
-
// src/commands/sessions/daemon/failChainedRun.ts
|
|
44656
|
-
function failChainedRun(session, itemId2, error, clients, tree) {
|
|
44657
|
-
const reason4 = error instanceof Error ? error.message : String(error);
|
|
44658
|
-
session.pty = null;
|
|
44659
|
-
session.error = `backlog run ${itemId2} could not start: ${reason4}`;
|
|
44660
|
-
setStatus2(session, "error");
|
|
44661
|
-
daemonLog(
|
|
44662
|
-
`session ${session.id} not reused for backlog run ${itemId2}: workspace allocation failed: ${reason4}`
|
|
44663
|
-
);
|
|
44664
|
-
broadcast(clients, { type: "clear", sessionId: session.id });
|
|
44665
|
-
emitSessionOutput(
|
|
44666
|
-
session,
|
|
44667
|
-
clients,
|
|
44668
|
-
`\r
|
|
44669
|
-
\x1B[31m${session.error}\x1B[0m\r
|
|
44670
|
-
`
|
|
44671
|
-
);
|
|
44672
|
-
tree?.notify();
|
|
44673
|
-
}
|
|
44674
|
-
|
|
44675
|
-
// src/commands/sessions/daemon/reuseSessionForRun.ts
|
|
44676
|
-
function reuseSessionForRun(session, itemId2, clients, onStatusChange, tree) {
|
|
44677
|
-
const assistArgs = ["backlog", "run", String(itemId2)];
|
|
44678
|
-
const originCwd = session.cwd;
|
|
44679
|
-
resetCardForRun(session, assistArgs);
|
|
44680
|
-
let alloc;
|
|
44681
|
-
try {
|
|
44682
|
-
alloc = planReuseTree(session, tree, {
|
|
44683
|
-
commits: isCommittingArgs(assistArgs),
|
|
44684
|
-
backlogRun: true
|
|
44685
|
-
});
|
|
44686
|
-
} catch (error) {
|
|
44687
|
-
failChainedRun(session, itemId2, error, clients, tree);
|
|
44688
|
-
return;
|
|
44689
|
-
}
|
|
44690
|
-
if (alloc) session.cwd = alloc.cwd;
|
|
44691
|
-
const started = startReusedRunPty(
|
|
44692
|
-
session,
|
|
44693
|
-
assistArgs,
|
|
44694
|
-
itemId2,
|
|
44695
|
-
alloc !== void 0,
|
|
44696
|
-
clients,
|
|
44697
|
-
onStatusChange
|
|
44698
|
-
);
|
|
44699
|
-
if (!started) return;
|
|
44700
|
-
if (tree) ensureWatcher(tree, originCwd);
|
|
44701
|
-
attachReusedRun(session, alloc, clients, onStatusChange, tree);
|
|
44702
|
-
daemonLog(
|
|
44703
|
-
`session ${session.id} reused for backlog run ${itemId2}: ${session.name}`
|
|
44704
|
-
);
|
|
44705
|
-
}
|
|
44706
|
-
|
|
44707
45214
|
// src/commands/sessions/daemon/setSessionTitle.ts
|
|
44708
45215
|
function setSessionTitle(sessions, id, title) {
|
|
44709
45216
|
const normalised = singleLineTitle(title);
|
|
@@ -44888,43 +45395,6 @@ async function collectHistory(linked) {
|
|
|
44888
45395
|
return withRepoGroups(local).concat(remote);
|
|
44889
45396
|
}
|
|
44890
45397
|
|
|
44891
|
-
// src/commands/sessions/daemon/links/connectLinkWebSocket.ts
|
|
44892
|
-
import WebSocket from "ws";
|
|
44893
|
-
var HANDSHAKE_TIMEOUT_MS = 5e3;
|
|
44894
|
-
function linkWebSocketUrl(url) {
|
|
44895
|
-
const target = new URL("/ws", url);
|
|
44896
|
-
target.protocol = target.protocol === "https:" ? "wss:" : "ws:";
|
|
44897
|
-
return target.toString();
|
|
44898
|
-
}
|
|
44899
|
-
function connectLinkWebSocket(url, handlers) {
|
|
44900
|
-
return new Promise((resolve26, reject) => {
|
|
44901
|
-
const ws = new WebSocket(linkWebSocketUrl(url), {
|
|
44902
|
-
handshakeTimeout: HANDSHAKE_TIMEOUT_MS
|
|
44903
|
-
});
|
|
44904
|
-
let open = false;
|
|
44905
|
-
ws.on("open", () => {
|
|
44906
|
-
open = true;
|
|
44907
|
-
resolve26({
|
|
44908
|
-
send: (line) => {
|
|
44909
|
-
if (ws.readyState !== WebSocket.OPEN) return false;
|
|
44910
|
-
ws.send(line);
|
|
44911
|
-
return true;
|
|
44912
|
-
},
|
|
44913
|
-
close: () => ws.close()
|
|
44914
|
-
});
|
|
44915
|
-
});
|
|
44916
|
-
ws.on("message", (data) => handlers.onLine(data.toString()));
|
|
44917
|
-
ws.on("error", (error) => {
|
|
44918
|
-
if (!open) reject(error);
|
|
44919
|
-
});
|
|
44920
|
-
ws.on("close", (code, reason4) => {
|
|
44921
|
-
const detail = `code ${code}${reason4.length ? ` ${reason4.toString()}` : ""}`;
|
|
44922
|
-
if (open) handlers.onClose(detail);
|
|
44923
|
-
else reject(new Error(`closed before open (${detail})`));
|
|
44924
|
-
});
|
|
44925
|
-
});
|
|
44926
|
-
}
|
|
44927
|
-
|
|
44928
45398
|
// src/commands/sessions/daemon/links/guardLinkCallback.ts
|
|
44929
45399
|
function guardLinkCallback(label2, callback) {
|
|
44930
45400
|
return (...args) => {
|
|
@@ -45304,11 +45774,6 @@ var LinkHealer = class {
|
|
|
45304
45774
|
}
|
|
45305
45775
|
};
|
|
45306
45776
|
|
|
45307
|
-
// src/commands/sessions/daemon/links/linkVersionCheck.ts
|
|
45308
|
-
function linkVersionCheck() {
|
|
45309
|
-
return loadConfig().sessions?.linkVersionCheck ?? "block";
|
|
45310
|
-
}
|
|
45311
|
-
|
|
45312
45777
|
// src/commands/sessions/daemon/links/acceptPeerHello.ts
|
|
45313
45778
|
function acceptPeerHello(linkName, msg) {
|
|
45314
45779
|
if (!isHello(msg)) return { kind: "reject", reason: "malformed hello" };
|
|
@@ -45535,6 +46000,11 @@ var NodeLink = class {
|
|
|
45535
46000
|
route(client, data) {
|
|
45536
46001
|
forwardToLink(this.ctx, client, data);
|
|
45537
46002
|
}
|
|
46003
|
+
viewerLeft(viewerId) {
|
|
46004
|
+
if (!this.ctx.greeted) return;
|
|
46005
|
+
if (sendToLink(this.ctx, { type: "viewer-left", viewerId }))
|
|
46006
|
+
daemonLog(`link ${this.spec.name} ws: viewer ${viewerId} left`);
|
|
46007
|
+
}
|
|
45538
46008
|
replayScrollback(client) {
|
|
45539
46009
|
replayLinkScrollback(this.ctx.relay, client);
|
|
45540
46010
|
}
|
|
@@ -45627,6 +46097,10 @@ var NodeLinks = class {
|
|
|
45627
46097
|
if (this.clients.isPeer(client)) return false;
|
|
45628
46098
|
return routeToLink(this.links, this.localNode(), client, data);
|
|
45629
46099
|
}
|
|
46100
|
+
viewerLeft(client, viewerId) {
|
|
46101
|
+
if (this.clients.isPeer(client)) return;
|
|
46102
|
+
for (const link3 of this.all()) link3.viewerLeft(viewerId);
|
|
46103
|
+
}
|
|
45630
46104
|
dispose() {
|
|
45631
46105
|
for (const link3 of this.all()) link3.dispose();
|
|
45632
46106
|
this.links.clear();
|
|
@@ -45852,6 +46326,7 @@ var SessionManager = class {
|
|
|
45852
46326
|
}
|
|
45853
46327
|
replayLocal = (client) => replayScrollback(this.sessions, client);
|
|
45854
46328
|
removeClient(client) {
|
|
46329
|
+
this.io.viewerLeft(client);
|
|
45855
46330
|
releaseClient(client, this.clients, this.prPreview, this.verify);
|
|
45856
46331
|
this.onIdleChange?.(this.isIdle());
|
|
45857
46332
|
}
|
|
@@ -45904,22 +46379,18 @@ var SessionManager = class {
|
|
|
45904
46379
|
resume(sessionId, cwd, name, harness) {
|
|
45905
46380
|
return resumeInTree(this.treeCtx(), sessionId, cwd, name, harness);
|
|
45906
46381
|
}
|
|
45907
|
-
onStatusChange =
|
|
46382
|
+
onStatusChange = managerStatusChangeHandler(
|
|
45908
46383
|
this.sessions,
|
|
46384
|
+
this.clients,
|
|
45909
46385
|
(id) => this.dismissSession(id),
|
|
45910
46386
|
() => this.notify(),
|
|
45911
|
-
(
|
|
45912
|
-
session,
|
|
45913
|
-
itemId2,
|
|
45914
|
-
this.clients,
|
|
45915
|
-
this.onStatusChange,
|
|
45916
|
-
this.treeCtx()
|
|
45917
|
-
)
|
|
46387
|
+
() => this.treeCtx()
|
|
45918
46388
|
);
|
|
45919
46389
|
io = new SessionIo(
|
|
45920
46390
|
this.sessions,
|
|
45921
46391
|
() => this.onStatusChange,
|
|
45922
|
-
() => this.notify()
|
|
46392
|
+
() => this.notify(),
|
|
46393
|
+
this.links
|
|
45923
46394
|
);
|
|
45924
46395
|
retrySession(id, replace = false) {
|
|
45925
46396
|
return runRetry(this.sessions, id, replace, {
|
|
@@ -45930,14 +46401,13 @@ var SessionManager = class {
|
|
|
45930
46401
|
});
|
|
45931
46402
|
}
|
|
45932
46403
|
restart(id) {
|
|
45933
|
-
|
|
46404
|
+
return restartAndNotify(
|
|
45934
46405
|
this.sessions,
|
|
45935
46406
|
id,
|
|
45936
46407
|
this.clients,
|
|
45937
|
-
this.onStatusChange
|
|
46408
|
+
this.onStatusChange,
|
|
46409
|
+
this.notify
|
|
45938
46410
|
);
|
|
45939
|
-
if (result.ok) this.notify();
|
|
45940
|
-
return result;
|
|
45941
46411
|
}
|
|
45942
46412
|
dismissSession = (id) => {
|
|
45943
46413
|
dismissSessionGated(this.sessions, id, this.notify);
|
|
@@ -46416,6 +46886,38 @@ var lifecycleHandlers = {
|
|
|
46416
46886
|
shutdown: handleShutdown
|
|
46417
46887
|
};
|
|
46418
46888
|
|
|
46889
|
+
// src/commands/sessions/daemon/viewed.ts
|
|
46890
|
+
function viewed(handler) {
|
|
46891
|
+
return (client, m, d) => {
|
|
46892
|
+
m.io.trackViewer(client, d);
|
|
46893
|
+
handler(client, m, d);
|
|
46894
|
+
};
|
|
46895
|
+
}
|
|
46896
|
+
|
|
46897
|
+
// src/commands/sessions/daemon/viewerHandlers.ts
|
|
46898
|
+
var viewerOf = (d) => ({
|
|
46899
|
+
viewerId: d.viewerId,
|
|
46900
|
+
viewerNode: d.viewerNode
|
|
46901
|
+
});
|
|
46902
|
+
var viewerHandlers = {
|
|
46903
|
+
input: viewed(
|
|
46904
|
+
routed(
|
|
46905
|
+
(_client, m, d) => m.io.write(d.sessionId, d.data, viewerOf(d))
|
|
46906
|
+
)
|
|
46907
|
+
),
|
|
46908
|
+
resize: viewed(
|
|
46909
|
+
routed(
|
|
46910
|
+
(_client, m, d) => m.io.resize(d.sessionId, d.cols, d.rows, {
|
|
46911
|
+
...viewerOf(d),
|
|
46912
|
+
claim: d.claim === true
|
|
46913
|
+
})
|
|
46914
|
+
)
|
|
46915
|
+
),
|
|
46916
|
+
"viewer-left": (client, m, d) => {
|
|
46917
|
+
if (typeof d.viewerId === "string") m.io.viewerLeft(client, d.viewerId);
|
|
46918
|
+
}
|
|
46919
|
+
};
|
|
46920
|
+
|
|
46419
46921
|
// src/commands/sessions/daemon/setSessionCwd.ts
|
|
46420
46922
|
function setSessionCwd(sessions, id, cwd) {
|
|
46421
46923
|
const s = sessions.get(id);
|
|
@@ -46537,19 +47039,7 @@ var messageHandlers = {
|
|
|
46537
47039
|
d.usedPct
|
|
46538
47040
|
),
|
|
46539
47041
|
"codex-usage": handleCodexUsage,
|
|
46540
|
-
|
|
46541
|
-
(_client, m, d) => m.io.write(
|
|
46542
|
-
d.sessionId,
|
|
46543
|
-
d.data,
|
|
46544
|
-
d.viewerId
|
|
46545
|
-
)
|
|
46546
|
-
),
|
|
46547
|
-
resize: routed(
|
|
46548
|
-
(_client, m, d) => m.io.resize(d.sessionId, d.cols, d.rows, {
|
|
46549
|
-
viewerId: d.viewerId,
|
|
46550
|
-
claim: d.claim === true
|
|
46551
|
-
})
|
|
46552
|
-
),
|
|
47042
|
+
...viewerHandlers,
|
|
46553
47043
|
retry: routed((client, m, d) => {
|
|
46554
47044
|
const conflict = m.retrySession(d.sessionId, d.replace === true);
|
|
46555
47045
|
if (conflict)
|
|
@@ -46788,236 +47278,6 @@ async function closeSession() {
|
|
|
46788
47278
|
}
|
|
46789
47279
|
}
|
|
46790
47280
|
|
|
46791
|
-
// src/commands/sessions/nodes/DoctorProbes.ts
|
|
46792
|
-
function passed(hop, detail) {
|
|
46793
|
-
return { hop, ok: true, detail };
|
|
46794
|
-
}
|
|
46795
|
-
function failed2(hop, error, remediation) {
|
|
46796
|
-
return { hop, ok: false, error, remediation };
|
|
46797
|
-
}
|
|
46798
|
-
|
|
46799
|
-
// src/commands/sessions/nodes/fetchPeerJson.ts
|
|
46800
|
-
var PEER_TIMEOUT_MS = 3e3;
|
|
46801
|
-
function peerErrorCode(error) {
|
|
46802
|
-
if (!(error instanceof Error)) return void 0;
|
|
46803
|
-
const { code, cause } = error;
|
|
46804
|
-
if (code) return code;
|
|
46805
|
-
if (error.name === "TimeoutError") return "ETIMEDOUT";
|
|
46806
|
-
return cause instanceof Error ? peerErrorCode(cause) : void 0;
|
|
46807
|
-
}
|
|
46808
|
-
function describePeerError(error) {
|
|
46809
|
-
if (!(error instanceof Error)) return String(error);
|
|
46810
|
-
const cause = error.cause;
|
|
46811
|
-
return cause instanceof Error ? `${error.message}: ${describePeerError(cause)}` : error.message;
|
|
46812
|
-
}
|
|
46813
|
-
async function fetchPeerJson(baseUrl, path106, timeoutMs = PEER_TIMEOUT_MS) {
|
|
46814
|
-
const res = await fetch(new URL(path106, baseUrl), {
|
|
46815
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
46816
|
-
});
|
|
46817
|
-
if (!res.ok)
|
|
46818
|
-
throw Object.assign(
|
|
46819
|
-
new Error(`GET ${path106} returned ${res.status} ${res.statusText}`),
|
|
46820
|
-
{ status: res.status }
|
|
46821
|
-
);
|
|
46822
|
-
return await res.json();
|
|
46823
|
-
}
|
|
46824
|
-
|
|
46825
|
-
// src/commands/sessions/nodes/helloHop.ts
|
|
46826
|
-
async function helloHop(spec, probes) {
|
|
46827
|
-
let msg;
|
|
46828
|
-
try {
|
|
46829
|
-
msg = await probes.hello(spec.url);
|
|
46830
|
-
} catch (error) {
|
|
46831
|
-
return failed2(
|
|
46832
|
-
"ws",
|
|
46833
|
-
describePeerError(error),
|
|
46834
|
-
`${spec.name}'s /api/health answers but its /ws does not \u2014 restart the web server on ${spec.name}, then check \`assist sessions nodes logs ${spec.name}\``
|
|
46835
|
-
);
|
|
46836
|
-
}
|
|
46837
|
-
if (!isHello(msg))
|
|
46838
|
-
return failed2(
|
|
46839
|
-
"ws",
|
|
46840
|
-
`malformed hello: ${JSON.stringify(msg)}`,
|
|
46841
|
-
`run \`assist update\` on ${spec.name}`
|
|
46842
|
-
);
|
|
46843
|
-
const detail = `hello ${msg.version} (protocol ${msg.protocol ?? "legacy"})`;
|
|
46844
|
-
if (helloCompatible(msg)) return passed("ws", detail);
|
|
46845
|
-
const mismatch = `${helloMismatchKind(msg)} mismatch: peer ${msg.version} (protocol ${msg.protocol ?? "legacy"}), this node ${ASSIST_VERSION} (protocol ${PROTOCOL_VERSION})`;
|
|
46846
|
-
const mode = linkVersionCheck();
|
|
46847
|
-
if (mode !== "block")
|
|
46848
|
-
return passed(
|
|
46849
|
-
"ws",
|
|
46850
|
-
`${mismatch}; allowed by sessions.linkVersionCheck=${mode}`
|
|
46851
|
-
);
|
|
46852
|
-
return failed2(
|
|
46853
|
-
"ws",
|
|
46854
|
-
mismatch,
|
|
46855
|
-
`this node heals an older peer via its /api/self-update on connect; if the heal latched, run \`assist update\` on the older node, then \`assist daemon restart\` here`
|
|
46856
|
-
);
|
|
46857
|
-
}
|
|
46858
|
-
|
|
46859
|
-
// src/commands/sessions/nodes/linkStateHop.ts
|
|
46860
|
-
function linkStateHop(spec, probes) {
|
|
46861
|
-
const status2 = probes.linkState(spec.name);
|
|
46862
|
-
if (status2 === "no-daemon")
|
|
46863
|
-
return failed2(
|
|
46864
|
-
"link",
|
|
46865
|
-
"this node's daemon is not running",
|
|
46866
|
-
"links run in the daemon \u2014 start this node's web server or run `assist daemon restart`"
|
|
46867
|
-
);
|
|
46868
|
-
if (status2 === "unknown-link")
|
|
46869
|
-
return failed2(
|
|
46870
|
-
"link",
|
|
46871
|
-
`this node's daemon has no link named ${spec.name}`,
|
|
46872
|
-
"the daemon has not loaded sessions.links \u2014 run `assist daemon restart`"
|
|
46873
|
-
);
|
|
46874
|
-
if (status2.state === "connected") return passed("link", "connected");
|
|
46875
|
-
const error = status2.error ?? status2.state;
|
|
46876
|
-
if (status2.state === "version-blocked")
|
|
46877
|
-
return failed2("link", error, `version mismatch \u2014 heal latched. ${error}`);
|
|
46878
|
-
return failed2(
|
|
46879
|
-
"link",
|
|
46880
|
-
error,
|
|
46881
|
-
`every probe passed but the daemon's link is ${status2.state}; it retries behind the circuit breaker \u2014 check this node's daemon.log for \`link ${spec.name} ws:\` lines`
|
|
46882
|
-
);
|
|
46883
|
-
}
|
|
46884
|
-
|
|
46885
|
-
// src/commands/sessions/nodes/findTailscalePeer.ts
|
|
46886
|
-
function findTailscalePeer(status2, hostname2) {
|
|
46887
|
-
const nodes = [
|
|
46888
|
-
...status2.Self ? [{ ...status2.Self, Online: true }] : [],
|
|
46889
|
-
...Object.values(status2.Peer ?? {})
|
|
46890
|
-
];
|
|
46891
|
-
return nodes.find(
|
|
46892
|
-
(node) => node.DNSName?.replace(/\.$/, "").toLowerCase() === hostname2
|
|
46893
|
-
);
|
|
46894
|
-
}
|
|
46895
|
-
|
|
46896
|
-
// src/commands/sessions/nodes/tailscaleHop.ts
|
|
46897
|
-
async function tailscaleHop(spec, probes) {
|
|
46898
|
-
const cli = tailscaleCli();
|
|
46899
|
-
let status2;
|
|
46900
|
-
try {
|
|
46901
|
-
status2 = await probes.tailscale();
|
|
46902
|
-
} catch (error) {
|
|
46903
|
-
return failed2(
|
|
46904
|
-
"tailscale",
|
|
46905
|
-
describePeerError(error),
|
|
46906
|
-
`\`${cli} status --json\` failed \u2014 install Tailscale on this node and sign in with \`${cli} up\``
|
|
46907
|
-
);
|
|
46908
|
-
}
|
|
46909
|
-
if (status2.BackendState !== "Running")
|
|
46910
|
-
return failed2(
|
|
46911
|
-
"tailscale",
|
|
46912
|
-
`Tailscale on this node is ${status2.BackendState ?? "not running"}`,
|
|
46913
|
-
`run \`${cli} up\` on this node`
|
|
46914
|
-
);
|
|
46915
|
-
const { hostname: hostname2 } = new URL(spec.url);
|
|
46916
|
-
const peer = findTailscalePeer(status2, hostname2);
|
|
46917
|
-
if (!peer)
|
|
46918
|
-
return failed2(
|
|
46919
|
-
"tailscale",
|
|
46920
|
-
`${hostname2} is not in this node's tailnet`,
|
|
46921
|
-
`check the host in \`${cli} status\`, then relink with \`assist sessions nodes link ${spec.name} --tailscale <host> --port <port>\``
|
|
46922
|
-
);
|
|
46923
|
-
if (!peer.Online)
|
|
46924
|
-
return failed2(
|
|
46925
|
-
"tailscale",
|
|
46926
|
-
`${hostname2} is offline in the tailnet`,
|
|
46927
|
-
`is ${spec.name}'s machine awake with Tailscale running and signed in?`
|
|
46928
|
-
);
|
|
46929
|
-
return passed(
|
|
46930
|
-
"tailscale",
|
|
46931
|
-
`${hostname2} online${peer.TailscaleIPs?.[0] ? ` (${peer.TailscaleIPs[0]})` : ""}`
|
|
46932
|
-
);
|
|
46933
|
-
}
|
|
46934
|
-
|
|
46935
|
-
// src/commands/sessions/nodes/tailscaleServeRemediation.ts
|
|
46936
|
-
function tailscaleServeRemediation(spec, error) {
|
|
46937
|
-
const status2 = error.status;
|
|
46938
|
-
if (status2 !== void 0 && status2 !== 502) return void 0;
|
|
46939
|
-
const { hostname: hostname2, port } = new URL(spec.url);
|
|
46940
|
-
const servePort = port || "443";
|
|
46941
|
-
return status2 === 502 ? `tailscale serve on ${hostname2} answered but nothing listens on 127.0.0.1:${servePort} behind it \u2014 is ${spec.name}'s web server running (project-switch webservers)?` : `nothing serves https on ${servePort} on ${hostname2} \u2014 ${spec.name}'s web server runs tailscale serve when it starts: check the \`tailscale serve:\` line in its log (project-switch "View logs") and that sessions.tailscaleServe is not false there`;
|
|
46942
|
-
}
|
|
46943
|
-
|
|
46944
|
-
// src/commands/sessions/nodes/isTailscaleLink.ts
|
|
46945
|
-
function isTailscaleLink(spec) {
|
|
46946
|
-
return new URL(spec.url).hostname.endsWith(".ts.net");
|
|
46947
|
-
}
|
|
46948
|
-
|
|
46949
|
-
// src/commands/sessions/nodes/webHop.ts
|
|
46950
|
-
function webRemediation(spec, error) {
|
|
46951
|
-
const serve = isTailscaleLink(spec) ? tailscaleServeRemediation(spec, error) : void 0;
|
|
46952
|
-
if (serve) return serve;
|
|
46953
|
-
const { hostname: hostname2, port } = new URL(spec.url);
|
|
46954
|
-
switch (peerErrorCode(error)) {
|
|
46955
|
-
case "ECONNREFUSED":
|
|
46956
|
-
return `nothing listening on ${port} on ${hostname2} \u2014 is ${spec.name}'s web server running (project-switch webservers)?`;
|
|
46957
|
-
case "ETIMEDOUT":
|
|
46958
|
-
return `${hostname2}:${port} did not answer \u2014 is ${spec.name} up and the port reachable from here?`;
|
|
46959
|
-
case "ENOTFOUND":
|
|
46960
|
-
return `cannot resolve ${hostname2} \u2014 fix the url with \`assist sessions nodes link ${spec.name} <url>\``;
|
|
46961
|
-
}
|
|
46962
|
-
if (error.status === 404)
|
|
46963
|
-
return `${spec.name}'s web server has no /api/health \u2014 run \`assist update\` on ${spec.name}`;
|
|
46964
|
-
return `check the link url ${spec.url} with \`assist sessions nodes\``;
|
|
46965
|
-
}
|
|
46966
|
-
async function webHop(spec, probes) {
|
|
46967
|
-
let health2;
|
|
46968
|
-
try {
|
|
46969
|
-
health2 = await probes.health(spec.url);
|
|
46970
|
-
} catch (error) {
|
|
46971
|
-
return {
|
|
46972
|
-
hop: failed2("web", describePeerError(error), webRemediation(spec, error))
|
|
46973
|
-
};
|
|
46974
|
-
}
|
|
46975
|
-
if (health2.nodeName !== spec.name)
|
|
46976
|
-
return {
|
|
46977
|
-
hop: failed2(
|
|
46978
|
-
"web",
|
|
46979
|
-
`peer reports nodeName ${health2.nodeName}, link expects ${spec.name}`,
|
|
46980
|
-
`relink with \`assist sessions nodes unlink ${spec.name}\` then \`assist sessions nodes link ${health2.nodeName} ${spec.url}\`, or set sessions.nodeName on the peer`
|
|
46981
|
-
)
|
|
46982
|
-
};
|
|
46983
|
-
return {
|
|
46984
|
-
hop: passed(
|
|
46985
|
-
"web",
|
|
46986
|
-
`${health2.nodeName} ${health2.version} (protocol ${health2.protocol})`
|
|
46987
|
-
),
|
|
46988
|
-
health: health2
|
|
46989
|
-
};
|
|
46990
|
-
}
|
|
46991
|
-
|
|
46992
|
-
// src/commands/sessions/nodes/peerDaemonHop.ts
|
|
46993
|
-
function peerDaemonHop(spec, health2) {
|
|
46994
|
-
if (health2.daemon?.reachable) return passed("daemon", "reachable");
|
|
46995
|
-
return failed2(
|
|
46996
|
-
"daemon",
|
|
46997
|
-
`${spec.name}'s web server reports its daemon unreachable`,
|
|
46998
|
-
`run \`assist daemon status\` on ${spec.name}; restarting its web server starts the daemon`
|
|
46999
|
-
);
|
|
47000
|
-
}
|
|
47001
|
-
|
|
47002
|
-
// src/commands/sessions/nodes/diagnoseLink.ts
|
|
47003
|
-
async function diagnoseLink(spec, probes) {
|
|
47004
|
-
const hops = [];
|
|
47005
|
-
const done2 = () => ({ ...spec, ok: hops.every((h) => h.ok), hops });
|
|
47006
|
-
if (isTailscaleLink(spec)) {
|
|
47007
|
-
hops.push(await tailscaleHop(spec, probes));
|
|
47008
|
-
if (!hops.at(-1)?.ok) return done2();
|
|
47009
|
-
}
|
|
47010
|
-
const web3 = await webHop(spec, probes);
|
|
47011
|
-
hops.push(web3.hop);
|
|
47012
|
-
if (!web3.health) return done2();
|
|
47013
|
-
hops.push(peerDaemonHop(spec, web3.health));
|
|
47014
|
-
if (!hops.at(-1)?.ok) return done2();
|
|
47015
|
-
hops.push(await helloHop(spec, probes));
|
|
47016
|
-
if (!hops.at(-1)?.ok) return done2();
|
|
47017
|
-
hops.push(linkStateHop(spec, probes));
|
|
47018
|
-
return done2();
|
|
47019
|
-
}
|
|
47020
|
-
|
|
47021
47281
|
// src/commands/sessions/nodes/findUnlinkedWindowsNode.ts
|
|
47022
47282
|
var WINDOWS_NODE_URL = "http://127.0.0.1:3101";
|
|
47023
47283
|
async function findUnlinkedWindowsNode(health2, platform = detectPlatform()) {
|
|
@@ -47035,36 +47295,6 @@ async function findUnlinkedWindowsNode(health2, platform = detectPlatform()) {
|
|
|
47035
47295
|
}
|
|
47036
47296
|
}
|
|
47037
47297
|
|
|
47038
|
-
// src/commands/sessions/nodes/probePeerHello.ts
|
|
47039
|
-
var HELLO_TIMEOUT_MS = 5e3;
|
|
47040
|
-
async function probePeerHello(url) {
|
|
47041
|
-
let settle = () => {
|
|
47042
|
-
};
|
|
47043
|
-
const reply2 = new Promise((resolve26) => {
|
|
47044
|
-
settle = resolve26;
|
|
47045
|
-
});
|
|
47046
|
-
const socket = await connectLinkWebSocket(url, {
|
|
47047
|
-
onLine: (line) => {
|
|
47048
|
-
try {
|
|
47049
|
-
const msg = JSON.parse(line);
|
|
47050
|
-
if (msg.type === "hello") settle(msg);
|
|
47051
|
-
} catch {
|
|
47052
|
-
}
|
|
47053
|
-
},
|
|
47054
|
-
onClose: (reason4) => settle(new Error(`closed before hello reply (${reason4})`))
|
|
47055
|
-
});
|
|
47056
|
-
const timer = setTimeout(
|
|
47057
|
-
() => settle(new Error(`no hello reply within ${HELLO_TIMEOUT_MS / 1e3}s`)),
|
|
47058
|
-
HELLO_TIMEOUT_MS
|
|
47059
|
-
);
|
|
47060
|
-
socket.send(JSON.stringify(buildHello()));
|
|
47061
|
-
const result = await reply2;
|
|
47062
|
-
clearTimeout(timer);
|
|
47063
|
-
socket.close();
|
|
47064
|
-
if (result instanceof Error) throw result;
|
|
47065
|
-
return result;
|
|
47066
|
-
}
|
|
47067
|
-
|
|
47068
47298
|
// src/commands/sessions/nodes/printReport.ts
|
|
47069
47299
|
import chalk254 from "chalk";
|
|
47070
47300
|
function printReport(report2) {
|
|
@@ -47089,22 +47319,11 @@ function printDiagnosis(diagnosis) {
|
|
|
47089
47319
|
}
|
|
47090
47320
|
|
|
47091
47321
|
// src/commands/sessions/nodes/doctorNodes.ts
|
|
47092
|
-
function doctorProbes(live) {
|
|
47093
|
-
return {
|
|
47094
|
-
health: (url) => fetchPeerJson(url, "/api/health"),
|
|
47095
|
-
hello: probePeerHello,
|
|
47096
|
-
tailscale: tailscaleStatus,
|
|
47097
|
-
linkState: (name) => {
|
|
47098
|
-
if (!live) return "no-daemon";
|
|
47099
|
-
return live.links.find((l) => l.name === name) ?? "unknown-link";
|
|
47100
|
-
}
|
|
47101
|
-
};
|
|
47102
|
-
}
|
|
47103
47322
|
async function doctorNodes(name, options2) {
|
|
47104
47323
|
const specs = loadLinkSpecs().filter((spec) => !name || spec.name === name);
|
|
47105
47324
|
if (name && specs.length === 0)
|
|
47106
47325
|
throw new Error(`No link named ${name}; see assist sessions nodes`);
|
|
47107
|
-
const probes =
|
|
47326
|
+
const probes = buildDoctorProbes(await queryNodes());
|
|
47108
47327
|
const report2 = { local: resolveNodeName(), links: [] };
|
|
47109
47328
|
for (const spec of specs) report2.links.push(await diagnoseLink(spec, probes));
|
|
47110
47329
|
if (specs.length === 0)
|