@staff0rd/assist 0.759.2 → 0.761.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/README.md +172 -181
- package/dist/commands/sessions/web/bundle.js +437 -437
- package/dist/index.js +868 -663
- 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.0",
|
|
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);
|
|
@@ -40652,7 +41002,8 @@ function toSessionInfo(session) {
|
|
|
40652
41002
|
pendingPrPreview,
|
|
40653
41003
|
undurable,
|
|
40654
41004
|
lastUserMessage,
|
|
40655
|
-
activeViewer
|
|
41005
|
+
activeViewer,
|
|
41006
|
+
activeViewerNode
|
|
40656
41007
|
} = session;
|
|
40657
41008
|
return {
|
|
40658
41009
|
...toSessionRunInfo(session),
|
|
@@ -40679,6 +41030,7 @@ function toSessionInfo(session) {
|
|
|
40679
41030
|
undurable,
|
|
40680
41031
|
lastUserMessage,
|
|
40681
41032
|
activeViewer,
|
|
41033
|
+
activeViewerNode,
|
|
40682
41034
|
joinable: joinRefusal(session) === void 0
|
|
40683
41035
|
};
|
|
40684
41036
|
}
|
|
@@ -40697,11 +41049,29 @@ function broadcastSessions(sessions, clients, linkedSessions = [], active) {
|
|
|
40697
41049
|
broadcast(peers, { type: "sessions", sessions: local, active: {} });
|
|
40698
41050
|
}
|
|
40699
41051
|
|
|
41052
|
+
// src/commands/sessions/daemon/releaseViewer.ts
|
|
41053
|
+
function releaseViewer(sessions, viewerId) {
|
|
41054
|
+
let released = false;
|
|
41055
|
+
for (const s of sessions.values()) {
|
|
41056
|
+
if (s.activeViewer !== viewerId) continue;
|
|
41057
|
+
daemonLog(
|
|
41058
|
+
`session ${s.id} active viewer ${viewerId} on ${s.activeViewerNode ?? "unknown node"} left`
|
|
41059
|
+
);
|
|
41060
|
+
s.activeViewer = void 0;
|
|
41061
|
+
s.activeViewerNode = void 0;
|
|
41062
|
+
released = true;
|
|
41063
|
+
}
|
|
41064
|
+
return released;
|
|
41065
|
+
}
|
|
41066
|
+
|
|
40700
41067
|
// src/commands/sessions/daemon/claimViewer.ts
|
|
40701
|
-
function claimViewer(session, { viewerId, onClaim }, via) {
|
|
41068
|
+
function claimViewer(session, { viewerId, viewerNode, onClaim }, via) {
|
|
40702
41069
|
if (!viewerId || session.activeViewer === viewerId) return;
|
|
40703
41070
|
session.activeViewer = viewerId;
|
|
40704
|
-
|
|
41071
|
+
session.activeViewerNode = viewerNode;
|
|
41072
|
+
daemonLog(
|
|
41073
|
+
`session ${session.id} active viewer ${viewerId} on ${viewerNode ?? "unknown node"} (${via})`
|
|
41074
|
+
);
|
|
40705
41075
|
onClaim?.();
|
|
40706
41076
|
}
|
|
40707
41077
|
|
|
@@ -40743,6 +41113,29 @@ function resizePty(s, cols, rows) {
|
|
|
40743
41113
|
}
|
|
40744
41114
|
}
|
|
40745
41115
|
|
|
41116
|
+
// src/commands/sessions/daemon/ViewerConnections.ts
|
|
41117
|
+
var ViewerConnections = class {
|
|
41118
|
+
byClient = /* @__PURE__ */ new Map();
|
|
41119
|
+
track(client, viewerId) {
|
|
41120
|
+
const ids = this.byClient.get(client) ?? /* @__PURE__ */ new Set();
|
|
41121
|
+
ids.add(viewerId);
|
|
41122
|
+
this.byClient.set(client, ids);
|
|
41123
|
+
}
|
|
41124
|
+
releaseUnheld(client, viewerId) {
|
|
41125
|
+
const ids = this.byClient.get(client);
|
|
41126
|
+
if (!ids) return [];
|
|
41127
|
+
const dropped = viewerId === void 0 ? [...ids] : [viewerId];
|
|
41128
|
+
for (const id of dropped) ids.delete(id);
|
|
41129
|
+
if (ids.size === 0) this.byClient.delete(client);
|
|
41130
|
+
return dropped.filter((id) => !this.isHeld(id));
|
|
41131
|
+
}
|
|
41132
|
+
isHeld(viewerId) {
|
|
41133
|
+
for (const ids of this.byClient.values())
|
|
41134
|
+
if (ids.has(viewerId)) return true;
|
|
41135
|
+
return false;
|
|
41136
|
+
}
|
|
41137
|
+
};
|
|
41138
|
+
|
|
40746
41139
|
// src/commands/sessions/daemon/watchPromptSubmit.ts
|
|
40747
41140
|
var CARRIAGE_RETURN = "\r";
|
|
40748
41141
|
function isPromptSubmit(data) {
|
|
@@ -40793,14 +41186,29 @@ function setAutoAdvance(sessions, id, enabled) {
|
|
|
40793
41186
|
|
|
40794
41187
|
// src/commands/sessions/daemon/SessionIo.ts
|
|
40795
41188
|
var SessionIo = class {
|
|
40796
|
-
constructor(sessions, onStatusChange, notify2) {
|
|
41189
|
+
constructor(sessions, onStatusChange, notify2, links2) {
|
|
40797
41190
|
this.sessions = sessions;
|
|
40798
41191
|
this.onStatusChange = onStatusChange;
|
|
40799
41192
|
this.notify = notify2;
|
|
41193
|
+
this.links = links2;
|
|
41194
|
+
}
|
|
41195
|
+
viewers = new ViewerConnections();
|
|
41196
|
+
trackViewer(client, d) {
|
|
41197
|
+
if (typeof d.viewerId !== "string") return;
|
|
41198
|
+
this.viewers.track(client, d.viewerId);
|
|
41199
|
+
d.viewerNode ??= this.links.localNode();
|
|
40800
41200
|
}
|
|
40801
|
-
|
|
41201
|
+
viewerLeft(client, viewerId) {
|
|
41202
|
+
let released = false;
|
|
41203
|
+
for (const id of this.viewers.releaseUnheld(client, viewerId)) {
|
|
41204
|
+
released = releaseViewer(this.sessions, id) || released;
|
|
41205
|
+
this.links.viewerLeft(client, id);
|
|
41206
|
+
}
|
|
41207
|
+
if (released) this.notify();
|
|
41208
|
+
}
|
|
41209
|
+
write(id, data, viewer) {
|
|
40802
41210
|
writeToSession(this.sessions, id, data, this.onStatusChange(), {
|
|
40803
|
-
|
|
41211
|
+
...viewer,
|
|
40804
41212
|
onClaim: this.notify
|
|
40805
41213
|
});
|
|
40806
41214
|
}
|
|
@@ -43246,6 +43654,7 @@ function shouldAutoDismiss(session, exitCode) {
|
|
|
43246
43654
|
}
|
|
43247
43655
|
|
|
43248
43656
|
// src/commands/sessions/daemon/finishStatusChange.ts
|
|
43657
|
+
var DIVERGED_EXIT_CODE = 3;
|
|
43249
43658
|
function finishStatusChange(session, status2, exitCode, deps2) {
|
|
43250
43659
|
daemonLog(`session ${session.id} status: ${session.status} -> ${status2}`);
|
|
43251
43660
|
void flushPhaseActiveMs(session);
|
|
@@ -43253,6 +43662,8 @@ function finishStatusChange(session, status2, exitCode, deps2) {
|
|
|
43253
43662
|
if (status2 === "waiting")
|
|
43254
43663
|
startTranscriptTitleGeneration(session, deps2.notify);
|
|
43255
43664
|
if (status2 !== "stopped") session.undurable = void 0;
|
|
43665
|
+
if (session.watcher === true && exitCode === DIVERGED_EXIT_CODE)
|
|
43666
|
+
deps2.escalateDivergence?.(session);
|
|
43256
43667
|
const autoRun = shouldAutoRun(session);
|
|
43257
43668
|
if (autoRun.run) {
|
|
43258
43669
|
deps2.reuseForRun(session, autoRun.itemId);
|
|
@@ -43269,19 +43680,18 @@ function finishStatusChange(session, status2, exitCode, deps2) {
|
|
|
43269
43680
|
}
|
|
43270
43681
|
|
|
43271
43682
|
// src/commands/sessions/daemon/applyStatusChange.ts
|
|
43272
|
-
function applyStatusChange(session, status2, exitCode,
|
|
43683
|
+
function applyStatusChange(session, status2, exitCode, deps2, sharedWithOtherAgents = () => false) {
|
|
43273
43684
|
if (session.status === status2) return;
|
|
43274
|
-
const deps2 = { dismiss, notify: notify2, reuseForRun };
|
|
43275
43685
|
if (endsStreamLife(session, status2, sharedWithOtherAgents)) {
|
|
43276
43686
|
session.closing = true;
|
|
43277
43687
|
daemonLog(
|
|
43278
43688
|
`session ${session.id} closing: checking durability of ${session.worktree?.path} before reap`
|
|
43279
43689
|
);
|
|
43280
|
-
|
|
43690
|
+
deps2.notify();
|
|
43281
43691
|
void resolveCloseDurability(
|
|
43282
43692
|
session,
|
|
43283
43693
|
() => finishStatusChange(session, "done", exitCode, deps2),
|
|
43284
|
-
|
|
43694
|
+
deps2.notify
|
|
43285
43695
|
);
|
|
43286
43696
|
return;
|
|
43287
43697
|
}
|
|
@@ -43289,18 +43699,378 @@ function applyStatusChange(session, status2, exitCode, dismiss, notify2, reuseFo
|
|
|
43289
43699
|
}
|
|
43290
43700
|
|
|
43291
43701
|
// src/commands/sessions/daemon/makeStatusChangeHandler.ts
|
|
43292
|
-
function makeStatusChangeHandler(sessions,
|
|
43702
|
+
function makeStatusChangeHandler(sessions, deps2) {
|
|
43293
43703
|
return (s, status2, exitCode) => applyStatusChange(
|
|
43294
43704
|
s,
|
|
43295
43705
|
status2,
|
|
43296
43706
|
exitCode,
|
|
43297
|
-
|
|
43298
|
-
notify2,
|
|
43299
|
-
reuseForRun,
|
|
43707
|
+
deps2,
|
|
43300
43708
|
(session) => otherTreeHolders(sessions, session).length > 0
|
|
43301
43709
|
);
|
|
43302
43710
|
}
|
|
43303
43711
|
|
|
43712
|
+
// src/commands/sessions/daemon/worktree/detectExistingWorktree.ts
|
|
43713
|
+
function detectExistingWorktree(cwd) {
|
|
43714
|
+
const repoRoot2 = findRepoRoot(cwd) ?? cwd;
|
|
43715
|
+
if (!worktreeConfigFor(repoRoot2).enabled) return void 0;
|
|
43716
|
+
const clone = mainWorktree(repoRoot2);
|
|
43717
|
+
if (clone && clone !== repoRoot2) return { path: repoRoot2, clone };
|
|
43718
|
+
return void 0;
|
|
43719
|
+
}
|
|
43720
|
+
|
|
43721
|
+
// src/commands/sessions/daemon/worktree/bindNewWorktree.ts
|
|
43722
|
+
function bindNewWorktree(session, alloc, notify2, startHeld = () => {
|
|
43723
|
+
}) {
|
|
43724
|
+
if (alloc.kind !== "worktree" || !alloc.cwd || !alloc.clone || !session)
|
|
43725
|
+
return;
|
|
43726
|
+
session.worktree = { path: alloc.cwd, clone: alloc.clone };
|
|
43727
|
+
if (alloc.created)
|
|
43728
|
+
seedWorktree(alloc.cwd, alloc.clone, () => startHeld(session));
|
|
43729
|
+
notify2();
|
|
43730
|
+
}
|
|
43731
|
+
function bindRestoredWorktrees(sessions) {
|
|
43732
|
+
for (const session of sessions.values()) {
|
|
43733
|
+
if (session.worktree || !session.cwd) continue;
|
|
43734
|
+
const worktree = detectExistingWorktree(session.cwd);
|
|
43735
|
+
if (worktree) session.worktree = worktree;
|
|
43736
|
+
}
|
|
43737
|
+
}
|
|
43738
|
+
function bindResumedWorktree(session, cwd, notify2) {
|
|
43739
|
+
if (!session) return;
|
|
43740
|
+
const worktree = detectExistingWorktree(cwd);
|
|
43741
|
+
if (!worktree) return;
|
|
43742
|
+
session.worktree = worktree;
|
|
43743
|
+
notify2();
|
|
43744
|
+
}
|
|
43745
|
+
|
|
43746
|
+
// src/commands/sessions/daemon/attachReusedRun.ts
|
|
43747
|
+
function attachReusedRun(session, alloc, clients, onStatusChange, tree) {
|
|
43748
|
+
broadcast(clients, { type: "clear", sessionId: session.id });
|
|
43749
|
+
if (alloc)
|
|
43750
|
+
bindNewWorktree(
|
|
43751
|
+
session,
|
|
43752
|
+
alloc,
|
|
43753
|
+
tree?.notify ?? (() => {
|
|
43754
|
+
}),
|
|
43755
|
+
tree?.startHeld
|
|
43756
|
+
);
|
|
43757
|
+
else wirePtyEvents(session, clients, onStatusChange);
|
|
43758
|
+
}
|
|
43759
|
+
|
|
43760
|
+
// src/commands/sessions/daemon/resetCardForRun.ts
|
|
43761
|
+
function resetCardForRun(session, assistArgs) {
|
|
43762
|
+
session.pty?.kill();
|
|
43763
|
+
session.gitWatcher?.close();
|
|
43764
|
+
session.gitWatcher = void 0;
|
|
43765
|
+
session.undurable = void 0;
|
|
43766
|
+
session.assistArgs = assistArgs;
|
|
43767
|
+
session.name = `assist ${assistArgs.join(" ")}`;
|
|
43768
|
+
session.commandType = "assist";
|
|
43769
|
+
session.activity = void 0;
|
|
43770
|
+
removeActivity(session.id);
|
|
43771
|
+
session.scrollback = "";
|
|
43772
|
+
session.startedAt = Date.now();
|
|
43773
|
+
session.runningMs = 0;
|
|
43774
|
+
session.runningSince = null;
|
|
43775
|
+
session.usageSeeded = false;
|
|
43776
|
+
setStatus2(session, "running");
|
|
43777
|
+
session.restored = void 0;
|
|
43778
|
+
session.launchedFrom = void 0;
|
|
43779
|
+
}
|
|
43780
|
+
|
|
43781
|
+
// src/commands/sessions/daemon/refuseSpawn.ts
|
|
43782
|
+
function refuseSpawn(session, error, clients, onStatusChange, stage = "respawned") {
|
|
43783
|
+
const reason4 = error instanceof Error ? error.message : String(error);
|
|
43784
|
+
session.pty = null;
|
|
43785
|
+
session.pendingStart = void 0;
|
|
43786
|
+
session.error = reason4;
|
|
43787
|
+
daemonLog(
|
|
43788
|
+
`session ${session.id} ("${session.name}") not ${stage}: ${reason4}`
|
|
43789
|
+
);
|
|
43790
|
+
broadcast(clients, { type: "clear", sessionId: session.id });
|
|
43791
|
+
emitSessionOutput(
|
|
43792
|
+
session,
|
|
43793
|
+
clients,
|
|
43794
|
+
`\r
|
|
43795
|
+
\x1B[31mCannot start this session: ${reason4}\x1B[0m\r
|
|
43796
|
+
`
|
|
43797
|
+
);
|
|
43798
|
+
onStatusChange(session, "error");
|
|
43799
|
+
}
|
|
43800
|
+
|
|
43801
|
+
// src/commands/sessions/daemon/startReusedRunPty.ts
|
|
43802
|
+
function startReusedRunPty(session, assistArgs, itemId2, hold, clients, onStatusChange) {
|
|
43803
|
+
try {
|
|
43804
|
+
Object.assign(
|
|
43805
|
+
session,
|
|
43806
|
+
startOrHoldPty(
|
|
43807
|
+
() => spawnPty(["assist", ...assistArgs], session.cwd, session.id),
|
|
43808
|
+
hold
|
|
43809
|
+
)
|
|
43810
|
+
);
|
|
43811
|
+
} catch (error) {
|
|
43812
|
+
refuseSpawn(
|
|
43813
|
+
session,
|
|
43814
|
+
error,
|
|
43815
|
+
clients,
|
|
43816
|
+
onStatusChange,
|
|
43817
|
+
`reused for backlog run ${itemId2}`
|
|
43818
|
+
);
|
|
43819
|
+
return false;
|
|
43820
|
+
}
|
|
43821
|
+
return true;
|
|
43822
|
+
}
|
|
43823
|
+
|
|
43824
|
+
// src/commands/sessions/daemon/createAssistSession.ts
|
|
43825
|
+
function createAssistSession(id, assistArgs, cwd, meta, holdPty) {
|
|
43826
|
+
const startedAt = Date.now();
|
|
43827
|
+
return {
|
|
43828
|
+
id,
|
|
43829
|
+
name: `assist ${assistArgs.join(" ")}`,
|
|
43830
|
+
title: meta?.title,
|
|
43831
|
+
subtitle: meta?.subtitle,
|
|
43832
|
+
commandType: "assist",
|
|
43833
|
+
status: "running",
|
|
43834
|
+
startedAt,
|
|
43835
|
+
runningMs: 0,
|
|
43836
|
+
runningSince: startedAt,
|
|
43837
|
+
waitingSince: null,
|
|
43838
|
+
...startOrHoldPty(
|
|
43839
|
+
() => spawnPty(["assist", ...assistArgs], cwd, id),
|
|
43840
|
+
holdPty
|
|
43841
|
+
),
|
|
43842
|
+
scrollback: "",
|
|
43843
|
+
assistArgs,
|
|
43844
|
+
cwd
|
|
43845
|
+
};
|
|
43846
|
+
}
|
|
43847
|
+
|
|
43848
|
+
// src/commands/sessions/daemon/createWatcherSession.ts
|
|
43849
|
+
var WATCHER_ARGS = ["watch", "loop"];
|
|
43850
|
+
function createWatcherSession(id, cwd) {
|
|
43851
|
+
return {
|
|
43852
|
+
...createAssistSession(id, WATCHER_ARGS, cwd),
|
|
43853
|
+
watcher: true
|
|
43854
|
+
};
|
|
43855
|
+
}
|
|
43856
|
+
|
|
43857
|
+
// src/commands/sessions/daemon/worktree/boundTreeRoots.ts
|
|
43858
|
+
function boundTreeRoots(sessions, exclude) {
|
|
43859
|
+
const roots = /* @__PURE__ */ new Set();
|
|
43860
|
+
for (const session of sessions.values()) {
|
|
43861
|
+
if (session === exclude) continue;
|
|
43862
|
+
const root2 = treeRootOf(session);
|
|
43863
|
+
if (root2 && !roots.has(root2) && holdsTree(session, root2)) roots.add(root2);
|
|
43864
|
+
}
|
|
43865
|
+
return roots;
|
|
43866
|
+
}
|
|
43867
|
+
function treeRootOf(session) {
|
|
43868
|
+
if (!session.cwd) return void 0;
|
|
43869
|
+
return canonicalTreePath(findRepoRoot(session.cwd) ?? session.cwd);
|
|
43870
|
+
}
|
|
43871
|
+
function holdsTree(session, root2) {
|
|
43872
|
+
if (session.closing) return true;
|
|
43873
|
+
if (session.status !== "done" && session.status !== "error") return true;
|
|
43874
|
+
if (!worktreeConfigFor(root2).enabled) return true;
|
|
43875
|
+
const durability = checkDurabilitySync(root2);
|
|
43876
|
+
daemonLog(`tree ${root2} ${releaseReason(durability, session)}`);
|
|
43877
|
+
return !durability.durable;
|
|
43878
|
+
}
|
|
43879
|
+
function releaseReason(durability, session) {
|
|
43880
|
+
if (!durability.durable)
|
|
43881
|
+
return `still held by finished session ${session.id}: ${durability.reason}`;
|
|
43882
|
+
if (durability.gone)
|
|
43883
|
+
return `free for allocation: session ${session.id}'s directory is gone from disk \u2014 nothing to land, not landed work`;
|
|
43884
|
+
return `free for allocation: session ${session.id} finished and its work is landed`;
|
|
43885
|
+
}
|
|
43886
|
+
|
|
43887
|
+
// src/commands/sessions/daemon/worktree/allocateAndBind.ts
|
|
43888
|
+
function allocateAndBind(ctx, cwd, create, options2 = {}, context) {
|
|
43889
|
+
const alloc = allocateTree(cwd, boundTreeRoots(ctx.sessions), options2);
|
|
43890
|
+
const needsSeeding = alloc.kind === "worktree" && alloc.created === true;
|
|
43891
|
+
const id = ctx.spawnWith(
|
|
43892
|
+
(sid) => create(sid, alloc.cwd, needsSeeding),
|
|
43893
|
+
context
|
|
43894
|
+
);
|
|
43895
|
+
bindNewWorktree(ctx.sessions.get(id), alloc, ctx.notify, ctx.startHeld);
|
|
43896
|
+
return id;
|
|
43897
|
+
}
|
|
43898
|
+
|
|
43899
|
+
// src/commands/sessions/daemon/worktree/ensureWatcher.ts
|
|
43900
|
+
function ensureWatcher(ctx, cwd) {
|
|
43901
|
+
if (!cwd) return void 0;
|
|
43902
|
+
const repoRoot2 = findRepoRoot(cwd) ?? cwd;
|
|
43903
|
+
const cfg = worktreeConfigFor(repoRoot2);
|
|
43904
|
+
if (!cfg.enabled || cfg.watcher !== true) return void 0;
|
|
43905
|
+
const clone = resolveClone(repoRoot2);
|
|
43906
|
+
const live = liveWatcherFor(ctx.sessions, clone);
|
|
43907
|
+
if (live) {
|
|
43908
|
+
daemonLog(
|
|
43909
|
+
`no watcher spawned for the clone ${clone}: session ${live.id} is already watching it (${live.status})`
|
|
43910
|
+
);
|
|
43911
|
+
return void 0;
|
|
43912
|
+
}
|
|
43913
|
+
const id = allocateAndBind(
|
|
43914
|
+
ctx,
|
|
43915
|
+
clone,
|
|
43916
|
+
(sid, resolvedCwd) => createWatcherSession(sid, resolvedCwd ?? clone),
|
|
43917
|
+
{ inPlace: true }
|
|
43918
|
+
);
|
|
43919
|
+
daemonLog(
|
|
43920
|
+
`spawned watcher session ${id} running assist ${WATCHER_ARGS.join(" ")} in the clone ${clone}`
|
|
43921
|
+
);
|
|
43922
|
+
return id;
|
|
43923
|
+
}
|
|
43924
|
+
|
|
43925
|
+
// src/commands/sessions/daemon/worktree/isBacklogRunArgs.ts
|
|
43926
|
+
var ITEM_ID = /^a?\d+$/;
|
|
43927
|
+
function isBacklogRunArgs(args) {
|
|
43928
|
+
const [command, subcommand, ...rest] = args;
|
|
43929
|
+
if (command !== "backlog" || subcommand !== "run") return false;
|
|
43930
|
+
return rest.some((arg) => ITEM_ID.test(arg));
|
|
43931
|
+
}
|
|
43932
|
+
|
|
43933
|
+
// src/commands/sessions/daemon/worktree/isPrCheckoutArgs.ts
|
|
43934
|
+
var CHECKOUT_COMMANDS = /* @__PURE__ */ new Set([
|
|
43935
|
+
"review",
|
|
43936
|
+
"review-pr-comments",
|
|
43937
|
+
"fix-conflict"
|
|
43938
|
+
]);
|
|
43939
|
+
function isPrCheckoutArgs(args) {
|
|
43940
|
+
const [command, ...rest] = args;
|
|
43941
|
+
if (!command || !CHECKOUT_COMMANDS.has(command)) return false;
|
|
43942
|
+
return rest.some((arg) => /^[1-9]\d*$/.test(arg));
|
|
43943
|
+
}
|
|
43944
|
+
|
|
43945
|
+
// src/commands/sessions/daemon/worktree/isCommittingArgs.ts
|
|
43946
|
+
function isCommittingArgs(args) {
|
|
43947
|
+
return isPrCheckoutArgs(args) || isBacklogRunArgs(args);
|
|
43948
|
+
}
|
|
43949
|
+
|
|
43950
|
+
// src/commands/sessions/daemon/worktree/planReuseTree.ts
|
|
43951
|
+
function planReuseTree(session, ctx, options2 = {}) {
|
|
43952
|
+
if (!ctx || !session.cwd || session.worktree) return void 0;
|
|
43953
|
+
const alloc = allocateTree(
|
|
43954
|
+
session.cwd,
|
|
43955
|
+
boundTreeRoots(ctx.sessions, session),
|
|
43956
|
+
options2
|
|
43957
|
+
);
|
|
43958
|
+
if (alloc.kind !== "worktree" || !alloc.cwd) return void 0;
|
|
43959
|
+
daemonLog(
|
|
43960
|
+
`session ${session.id} moved from ${session.cwd} to ${alloc.cwd} for its chained run`
|
|
43961
|
+
);
|
|
43962
|
+
return alloc;
|
|
43963
|
+
}
|
|
43964
|
+
|
|
43965
|
+
// src/commands/sessions/daemon/failChainedRun.ts
|
|
43966
|
+
function failChainedRun(session, itemId2, error, clients, tree) {
|
|
43967
|
+
const reason4 = error instanceof Error ? error.message : String(error);
|
|
43968
|
+
session.pty = null;
|
|
43969
|
+
session.error = `backlog run ${itemId2} could not start: ${reason4}`;
|
|
43970
|
+
setStatus2(session, "error");
|
|
43971
|
+
daemonLog(
|
|
43972
|
+
`session ${session.id} not reused for backlog run ${itemId2}: workspace allocation failed: ${reason4}`
|
|
43973
|
+
);
|
|
43974
|
+
broadcast(clients, { type: "clear", sessionId: session.id });
|
|
43975
|
+
emitSessionOutput(
|
|
43976
|
+
session,
|
|
43977
|
+
clients,
|
|
43978
|
+
`\r
|
|
43979
|
+
\x1B[31m${session.error}\x1B[0m\r
|
|
43980
|
+
`
|
|
43981
|
+
);
|
|
43982
|
+
tree?.notify();
|
|
43983
|
+
}
|
|
43984
|
+
|
|
43985
|
+
// src/commands/sessions/daemon/reuseSessionForRun.ts
|
|
43986
|
+
function reuseSessionForRun(session, itemId2, clients, onStatusChange, tree) {
|
|
43987
|
+
const assistArgs = ["backlog", "run", String(itemId2)];
|
|
43988
|
+
const originCwd = session.cwd;
|
|
43989
|
+
resetCardForRun(session, assistArgs);
|
|
43990
|
+
let alloc;
|
|
43991
|
+
try {
|
|
43992
|
+
alloc = planReuseTree(session, tree, {
|
|
43993
|
+
commits: isCommittingArgs(assistArgs),
|
|
43994
|
+
backlogRun: true
|
|
43995
|
+
});
|
|
43996
|
+
} catch (error) {
|
|
43997
|
+
failChainedRun(session, itemId2, error, clients, tree);
|
|
43998
|
+
return;
|
|
43999
|
+
}
|
|
44000
|
+
if (alloc) session.cwd = alloc.cwd;
|
|
44001
|
+
const started = startReusedRunPty(
|
|
44002
|
+
session,
|
|
44003
|
+
assistArgs,
|
|
44004
|
+
itemId2,
|
|
44005
|
+
alloc !== void 0,
|
|
44006
|
+
clients,
|
|
44007
|
+
onStatusChange
|
|
44008
|
+
);
|
|
44009
|
+
if (!started) return;
|
|
44010
|
+
if (tree) ensureWatcher(tree, originCwd);
|
|
44011
|
+
attachReusedRun(session, alloc, clients, onStatusChange, tree);
|
|
44012
|
+
daemonLog(
|
|
44013
|
+
`session ${session.id} reused for backlog run ${itemId2}: ${session.name}`
|
|
44014
|
+
);
|
|
44015
|
+
}
|
|
44016
|
+
|
|
44017
|
+
// src/commands/sessions/daemon/worktree/divergencePrompt.ts
|
|
44018
|
+
var MAX_REASON_LINES = 20;
|
|
44019
|
+
var MAX_REASON_CHARS = 2e3;
|
|
44020
|
+
function divergencePrompt(clone, scrollback) {
|
|
44021
|
+
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);
|
|
44022
|
+
return [
|
|
44023
|
+
`The watcher in ${clone} stopped because \`assist watch wait --pull --build\` could not fast-forward the branch (exit 3). The tail of its output:`,
|
|
44024
|
+
"",
|
|
44025
|
+
"```",
|
|
44026
|
+
reason4 || "(no output captured)",
|
|
44027
|
+
"```",
|
|
44028
|
+
"",
|
|
44029
|
+
"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."
|
|
44030
|
+
].join("\n");
|
|
44031
|
+
}
|
|
44032
|
+
|
|
44033
|
+
// src/commands/sessions/daemon/worktree/escalateDivergence.ts
|
|
44034
|
+
var ENDED = ["done", "error", "stopped"];
|
|
44035
|
+
function escalateDivergence(ctx, watcher) {
|
|
44036
|
+
if (!watcher.cwd) return void 0;
|
|
44037
|
+
const clone = canonicalTreePath(watcher.cwd);
|
|
44038
|
+
const live = [...ctx.sessions.values()].find(
|
|
44039
|
+
(s) => s.divergenceEscalation === true && s.cwd !== void 0 && canonicalTreePath(s.cwd) === clone && !ENDED.includes(s.status)
|
|
44040
|
+
);
|
|
44041
|
+
if (live) {
|
|
44042
|
+
daemonLog(
|
|
44043
|
+
`watcher session ${watcher.id} exited 3 (divergence) in the clone ${clone}: no escalation spawned, session ${live.id} is already diagnosing it (${live.status})`
|
|
44044
|
+
);
|
|
44045
|
+
return void 0;
|
|
44046
|
+
}
|
|
44047
|
+
const prompt = divergencePrompt(clone, watcher.scrollback);
|
|
44048
|
+
const id = allocateAndBind(
|
|
44049
|
+
ctx,
|
|
44050
|
+
clone,
|
|
44051
|
+
(sid, resolvedCwd) => ({
|
|
44052
|
+
...createSession(sid, { prompt, cwd: resolvedCwd ?? clone }),
|
|
44053
|
+
divergenceEscalation: true
|
|
44054
|
+
}),
|
|
44055
|
+
{ inPlace: true }
|
|
44056
|
+
);
|
|
44057
|
+
daemonLog(
|
|
44058
|
+
`watcher session ${watcher.id} exited 3 (divergence) in the clone ${clone}: spawned escalation session ${id} to diagnose it`
|
|
44059
|
+
);
|
|
44060
|
+
return id;
|
|
44061
|
+
}
|
|
44062
|
+
|
|
44063
|
+
// src/commands/sessions/daemon/managerStatusChangeHandler.ts
|
|
44064
|
+
function managerStatusChangeHandler(sessions, clients, dismiss, notify2, treeCtx) {
|
|
44065
|
+
const handler = makeStatusChangeHandler(sessions, {
|
|
44066
|
+
dismiss,
|
|
44067
|
+
notify: notify2,
|
|
44068
|
+
reuseForRun: (session, itemId2) => reuseSessionForRun(session, itemId2, clients, handler, treeCtx()),
|
|
44069
|
+
escalateDivergence: (watcher) => escalateDivergence(treeCtx(), watcher)
|
|
44070
|
+
});
|
|
44071
|
+
return handler;
|
|
44072
|
+
}
|
|
44073
|
+
|
|
43304
44074
|
// src/commands/sessions/daemon/applySetStatus.ts
|
|
43305
44075
|
function applySetStatus(sessions, id, status2, source, onStatusChange, rebind) {
|
|
43306
44076
|
const session = sessions.get(id);
|
|
@@ -43350,26 +44120,6 @@ function setStatusFromHook(sessions, report2, notify2, onStatusChange) {
|
|
|
43350
44120
|
);
|
|
43351
44121
|
}
|
|
43352
44122
|
|
|
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
44123
|
// src/commands/sessions/daemon/respawnSession.ts
|
|
43374
44124
|
function respawnSession(session, respawn, status2, clients, onStatusChange) {
|
|
43375
44125
|
session.gitWatcher?.close();
|
|
@@ -43566,6 +44316,13 @@ function restartManagedSession(sessions, id, clients, onStatusChange) {
|
|
|
43566
44316
|
return { ok: true };
|
|
43567
44317
|
}
|
|
43568
44318
|
|
|
44319
|
+
// src/commands/sessions/daemon/restartAndNotify.ts
|
|
44320
|
+
function restartAndNotify(sessions, id, clients, onStatusChange, notify2) {
|
|
44321
|
+
const result = restartManagedSession(sessions, id, clients, onStatusChange);
|
|
44322
|
+
if (result.ok) notify2();
|
|
44323
|
+
return result;
|
|
44324
|
+
}
|
|
44325
|
+
|
|
43569
44326
|
// src/commands/sessions/daemon/releaseClient.ts
|
|
43570
44327
|
function releaseClient(client, clients, prPreview, verify) {
|
|
43571
44328
|
clients.delete(client);
|
|
@@ -43602,6 +44359,7 @@ function restoreBase(id, persisted) {
|
|
|
43602
44359
|
initialPrompt: persisted.initialPrompt,
|
|
43603
44360
|
starred: persisted.watcher ? false : persisted.starred,
|
|
43604
44361
|
watcher: persisted.watcher,
|
|
44362
|
+
divergenceEscalation: persisted.divergenceEscalation,
|
|
43605
44363
|
design: persisted.design,
|
|
43606
44364
|
auto: persisted.auto,
|
|
43607
44365
|
autoRun: persisted.autoRun,
|
|
@@ -43990,6 +44748,25 @@ function isUpdate(persisted) {
|
|
|
43990
44748
|
return persisted.commandType === "assist" && persisted.assistArgs?.[0] === "update";
|
|
43991
44749
|
}
|
|
43992
44750
|
|
|
44751
|
+
// src/commands/sessions/daemon/migrateClaudeWatcher.ts
|
|
44752
|
+
function migrateClaudeWatcher(id, persisted) {
|
|
44753
|
+
if (!persisted.watcher || persisted.commandType !== "claude")
|
|
44754
|
+
return persisted;
|
|
44755
|
+
daemonLog(
|
|
44756
|
+
`migrating claude /watch watcher session ${id} to assist ${WATCHER_ARGS.join(" ")}`
|
|
44757
|
+
);
|
|
44758
|
+
return {
|
|
44759
|
+
...persisted,
|
|
44760
|
+
name: `assist ${WATCHER_ARGS.join(" ")}`,
|
|
44761
|
+
commandType: "assist",
|
|
44762
|
+
assistArgs: WATCHER_ARGS,
|
|
44763
|
+
initialPrompt: void 0,
|
|
44764
|
+
claudeSessionId: void 0,
|
|
44765
|
+
harness: void 0,
|
|
44766
|
+
harnessSessionId: void 0
|
|
44767
|
+
};
|
|
44768
|
+
}
|
|
44769
|
+
|
|
43993
44770
|
// src/commands/sessions/daemon/needsWrapperRelaunch.ts
|
|
43994
44771
|
var LAUNCH_COMMANDS = ["draft", "feat", "bug", "refine"];
|
|
43995
44772
|
function needsWrapperRelaunch(persisted) {
|
|
@@ -44015,7 +44792,8 @@ function relaunchConsoleWatcher(id, persisted, base) {
|
|
|
44015
44792
|
}
|
|
44016
44793
|
|
|
44017
44794
|
// src/commands/sessions/daemon/restoreSession.ts
|
|
44018
|
-
function restoreSession(id,
|
|
44795
|
+
function restoreSession(id, stored) {
|
|
44796
|
+
const persisted = migrateClaudeWatcher(id, stored);
|
|
44019
44797
|
const base = restoreBase(id, persisted);
|
|
44020
44798
|
if (persisted.status === "stopped") return stoppedSession(base, persisted);
|
|
44021
44799
|
if (isUpdate(persisted)) return updatedSession(id, persisted);
|
|
@@ -44136,40 +44914,6 @@ function accountedTrees(sessions) {
|
|
|
44136
44914
|
return accounted;
|
|
44137
44915
|
}
|
|
44138
44916
|
|
|
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
44917
|
// src/commands/sessions/daemon/worktree/reclaimVanishedWorktrees.ts
|
|
44174
44918
|
import { existsSync as existsSync91 } from "fs";
|
|
44175
44919
|
async function reclaimVanishedWorktrees(clone, paths) {
|
|
@@ -44453,257 +45197,6 @@ function runRetry(sessions, id, replace, deps2) {
|
|
|
44453
45197
|
return null;
|
|
44454
45198
|
}
|
|
44455
45199
|
|
|
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
45200
|
// src/commands/sessions/daemon/setSessionTitle.ts
|
|
44708
45201
|
function setSessionTitle(sessions, id, title) {
|
|
44709
45202
|
const normalised = singleLineTitle(title);
|
|
@@ -44888,43 +45381,6 @@ async function collectHistory(linked) {
|
|
|
44888
45381
|
return withRepoGroups(local).concat(remote);
|
|
44889
45382
|
}
|
|
44890
45383
|
|
|
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
45384
|
// src/commands/sessions/daemon/links/guardLinkCallback.ts
|
|
44929
45385
|
function guardLinkCallback(label2, callback) {
|
|
44930
45386
|
return (...args) => {
|
|
@@ -45304,11 +45760,6 @@ var LinkHealer = class {
|
|
|
45304
45760
|
}
|
|
45305
45761
|
};
|
|
45306
45762
|
|
|
45307
|
-
// src/commands/sessions/daemon/links/linkVersionCheck.ts
|
|
45308
|
-
function linkVersionCheck() {
|
|
45309
|
-
return loadConfig().sessions?.linkVersionCheck ?? "block";
|
|
45310
|
-
}
|
|
45311
|
-
|
|
45312
45763
|
// src/commands/sessions/daemon/links/acceptPeerHello.ts
|
|
45313
45764
|
function acceptPeerHello(linkName, msg) {
|
|
45314
45765
|
if (!isHello(msg)) return { kind: "reject", reason: "malformed hello" };
|
|
@@ -45535,6 +45986,11 @@ var NodeLink = class {
|
|
|
45535
45986
|
route(client, data) {
|
|
45536
45987
|
forwardToLink(this.ctx, client, data);
|
|
45537
45988
|
}
|
|
45989
|
+
viewerLeft(viewerId) {
|
|
45990
|
+
if (!this.ctx.greeted) return;
|
|
45991
|
+
if (sendToLink(this.ctx, { type: "viewer-left", viewerId }))
|
|
45992
|
+
daemonLog(`link ${this.spec.name} ws: viewer ${viewerId} left`);
|
|
45993
|
+
}
|
|
45538
45994
|
replayScrollback(client) {
|
|
45539
45995
|
replayLinkScrollback(this.ctx.relay, client);
|
|
45540
45996
|
}
|
|
@@ -45627,6 +46083,10 @@ var NodeLinks = class {
|
|
|
45627
46083
|
if (this.clients.isPeer(client)) return false;
|
|
45628
46084
|
return routeToLink(this.links, this.localNode(), client, data);
|
|
45629
46085
|
}
|
|
46086
|
+
viewerLeft(client, viewerId) {
|
|
46087
|
+
if (this.clients.isPeer(client)) return;
|
|
46088
|
+
for (const link3 of this.all()) link3.viewerLeft(viewerId);
|
|
46089
|
+
}
|
|
45630
46090
|
dispose() {
|
|
45631
46091
|
for (const link3 of this.all()) link3.dispose();
|
|
45632
46092
|
this.links.clear();
|
|
@@ -45852,6 +46312,7 @@ var SessionManager = class {
|
|
|
45852
46312
|
}
|
|
45853
46313
|
replayLocal = (client) => replayScrollback(this.sessions, client);
|
|
45854
46314
|
removeClient(client) {
|
|
46315
|
+
this.io.viewerLeft(client);
|
|
45855
46316
|
releaseClient(client, this.clients, this.prPreview, this.verify);
|
|
45856
46317
|
this.onIdleChange?.(this.isIdle());
|
|
45857
46318
|
}
|
|
@@ -45904,22 +46365,18 @@ var SessionManager = class {
|
|
|
45904
46365
|
resume(sessionId, cwd, name, harness) {
|
|
45905
46366
|
return resumeInTree(this.treeCtx(), sessionId, cwd, name, harness);
|
|
45906
46367
|
}
|
|
45907
|
-
onStatusChange =
|
|
46368
|
+
onStatusChange = managerStatusChangeHandler(
|
|
45908
46369
|
this.sessions,
|
|
46370
|
+
this.clients,
|
|
45909
46371
|
(id) => this.dismissSession(id),
|
|
45910
46372
|
() => this.notify(),
|
|
45911
|
-
(
|
|
45912
|
-
session,
|
|
45913
|
-
itemId2,
|
|
45914
|
-
this.clients,
|
|
45915
|
-
this.onStatusChange,
|
|
45916
|
-
this.treeCtx()
|
|
45917
|
-
)
|
|
46373
|
+
() => this.treeCtx()
|
|
45918
46374
|
);
|
|
45919
46375
|
io = new SessionIo(
|
|
45920
46376
|
this.sessions,
|
|
45921
46377
|
() => this.onStatusChange,
|
|
45922
|
-
() => this.notify()
|
|
46378
|
+
() => this.notify(),
|
|
46379
|
+
this.links
|
|
45923
46380
|
);
|
|
45924
46381
|
retrySession(id, replace = false) {
|
|
45925
46382
|
return runRetry(this.sessions, id, replace, {
|
|
@@ -45930,14 +46387,13 @@ var SessionManager = class {
|
|
|
45930
46387
|
});
|
|
45931
46388
|
}
|
|
45932
46389
|
restart(id) {
|
|
45933
|
-
|
|
46390
|
+
return restartAndNotify(
|
|
45934
46391
|
this.sessions,
|
|
45935
46392
|
id,
|
|
45936
46393
|
this.clients,
|
|
45937
|
-
this.onStatusChange
|
|
46394
|
+
this.onStatusChange,
|
|
46395
|
+
this.notify
|
|
45938
46396
|
);
|
|
45939
|
-
if (result.ok) this.notify();
|
|
45940
|
-
return result;
|
|
45941
46397
|
}
|
|
45942
46398
|
dismissSession = (id) => {
|
|
45943
46399
|
dismissSessionGated(this.sessions, id, this.notify);
|
|
@@ -46416,6 +46872,38 @@ var lifecycleHandlers = {
|
|
|
46416
46872
|
shutdown: handleShutdown
|
|
46417
46873
|
};
|
|
46418
46874
|
|
|
46875
|
+
// src/commands/sessions/daemon/viewed.ts
|
|
46876
|
+
function viewed(handler) {
|
|
46877
|
+
return (client, m, d) => {
|
|
46878
|
+
m.io.trackViewer(client, d);
|
|
46879
|
+
handler(client, m, d);
|
|
46880
|
+
};
|
|
46881
|
+
}
|
|
46882
|
+
|
|
46883
|
+
// src/commands/sessions/daemon/viewerHandlers.ts
|
|
46884
|
+
var viewerOf = (d) => ({
|
|
46885
|
+
viewerId: d.viewerId,
|
|
46886
|
+
viewerNode: d.viewerNode
|
|
46887
|
+
});
|
|
46888
|
+
var viewerHandlers = {
|
|
46889
|
+
input: viewed(
|
|
46890
|
+
routed(
|
|
46891
|
+
(_client, m, d) => m.io.write(d.sessionId, d.data, viewerOf(d))
|
|
46892
|
+
)
|
|
46893
|
+
),
|
|
46894
|
+
resize: viewed(
|
|
46895
|
+
routed(
|
|
46896
|
+
(_client, m, d) => m.io.resize(d.sessionId, d.cols, d.rows, {
|
|
46897
|
+
...viewerOf(d),
|
|
46898
|
+
claim: d.claim === true
|
|
46899
|
+
})
|
|
46900
|
+
)
|
|
46901
|
+
),
|
|
46902
|
+
"viewer-left": (client, m, d) => {
|
|
46903
|
+
if (typeof d.viewerId === "string") m.io.viewerLeft(client, d.viewerId);
|
|
46904
|
+
}
|
|
46905
|
+
};
|
|
46906
|
+
|
|
46419
46907
|
// src/commands/sessions/daemon/setSessionCwd.ts
|
|
46420
46908
|
function setSessionCwd(sessions, id, cwd) {
|
|
46421
46909
|
const s = sessions.get(id);
|
|
@@ -46537,19 +47025,7 @@ var messageHandlers = {
|
|
|
46537
47025
|
d.usedPct
|
|
46538
47026
|
),
|
|
46539
47027
|
"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
|
-
),
|
|
47028
|
+
...viewerHandlers,
|
|
46553
47029
|
retry: routed((client, m, d) => {
|
|
46554
47030
|
const conflict = m.retrySession(d.sessionId, d.replace === true);
|
|
46555
47031
|
if (conflict)
|
|
@@ -46788,236 +47264,6 @@ async function closeSession() {
|
|
|
46788
47264
|
}
|
|
46789
47265
|
}
|
|
46790
47266
|
|
|
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
47267
|
// src/commands/sessions/nodes/findUnlinkedWindowsNode.ts
|
|
47022
47268
|
var WINDOWS_NODE_URL = "http://127.0.0.1:3101";
|
|
47023
47269
|
async function findUnlinkedWindowsNode(health2, platform = detectPlatform()) {
|
|
@@ -47035,36 +47281,6 @@ async function findUnlinkedWindowsNode(health2, platform = detectPlatform()) {
|
|
|
47035
47281
|
}
|
|
47036
47282
|
}
|
|
47037
47283
|
|
|
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
47284
|
// src/commands/sessions/nodes/printReport.ts
|
|
47069
47285
|
import chalk254 from "chalk";
|
|
47070
47286
|
function printReport(report2) {
|
|
@@ -47089,22 +47305,11 @@ function printDiagnosis(diagnosis) {
|
|
|
47089
47305
|
}
|
|
47090
47306
|
|
|
47091
47307
|
// 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
47308
|
async function doctorNodes(name, options2) {
|
|
47104
47309
|
const specs = loadLinkSpecs().filter((spec) => !name || spec.name === name);
|
|
47105
47310
|
if (name && specs.length === 0)
|
|
47106
47311
|
throw new Error(`No link named ${name}; see assist sessions nodes`);
|
|
47107
|
-
const probes =
|
|
47312
|
+
const probes = buildDoctorProbes(await queryNodes());
|
|
47108
47313
|
const report2 = { local: resolveNodeName(), links: [] };
|
|
47109
47314
|
for (const spec of specs) report2.links.push(await diagnoseLink(spec, probes));
|
|
47110
47315
|
if (specs.length === 0)
|