@runuai/host 0.9.96 → 0.9.98
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/lib/browser-testing.ts +37 -5
- package/package.json +1 -1
- package/src/machine-tunnel-route.ts +136 -0
- package/src/main.ts +62 -23
package/lib/browser-testing.ts
CHANGED
|
@@ -933,7 +933,7 @@ console.log(
|
|
|
933
933
|
* under tiny restart loops — x11vnc EXITS whenever the X server it watches
|
|
934
934
|
* isn't up yet (the race that killed the first live run), and the loops
|
|
935
935
|
* also reattach after the display owner changes. Safe to run repeatedly:
|
|
936
|
-
* every piece is
|
|
936
|
+
* every piece is guarded by the X lockfile or a pidfile, loops included.
|
|
937
937
|
*/
|
|
938
938
|
/**
|
|
939
939
|
* The viewer daemons, one docker-exec each (the single-line nohup shape is
|
|
@@ -950,6 +950,22 @@ console.log(
|
|
|
950
950
|
* proxy only SOMETIMES and reconnects the rest (live 2026-09-02). So the
|
|
951
951
|
* start branch — reached only when the tracked loop is already dead — first
|
|
952
952
|
* reaps anything still holding the port, guaranteeing exactly one listener.
|
|
953
|
+
*
|
|
954
|
+
* That reap MUST be `pkill -A` (--ignore-ancestors). The step runs as
|
|
955
|
+
* `sh -c "<step>"`, so the launching shell's own cmdline carries the launch
|
|
956
|
+
* line and matches the reap pattern: plain `pkill -f` SIGTERMed that shell
|
|
957
|
+
* before it reached the nohup, and neither daemon ever started — host
|
|
958
|
+
* 0.9.94–0.9.96, every Linux runtime (the pgrep self-match above, wearing a
|
|
959
|
+
* kill; reproduced in the uai-linux VM: step exit 143, no pidfile). Both
|
|
960
|
+
* shipped images run procps-ng 4.0.2 (Debian bookworm), which has -A; an
|
|
961
|
+
* older procps rejects the flag with a usage error and reaps nothing, which
|
|
962
|
+
* merely restores the pre-reap behaviour and never the self-kill.
|
|
963
|
+
* `-A` protects the launching chain only: the apt worker below inlines
|
|
964
|
+
* these steps into `su -c` / `nohup sh -c` wrappers whose argv also
|
|
965
|
+
* matches, and a per-session reap that lands inside that worker's ~1 s
|
|
966
|
+
* viewer window can kill its node-uid shell. That only costs the worker
|
|
967
|
+
* its done-marker (set -e), so it re-runs on the next ensure and finds the
|
|
968
|
+
* pidfiles alive — self-healing, and no worse than the pre-reap world.
|
|
953
969
|
*/
|
|
954
970
|
const VIEWER_STEPS: string[] = [
|
|
955
971
|
// Xvfb — one-shot; the X lockfile is the truth (also set when an agent
|
|
@@ -958,9 +974,9 @@ const VIEWER_STEPS: string[] = [
|
|
|
958
974
|
`[ -x /usr/bin/Xvfb ] || exit 0; [ -e ${X_LOCK} ] || /usr/bin/nohup /usr/bin/${XVFB_CMD} >>/tmp/uai-xvfb.log 2>&1 &`,
|
|
959
975
|
// x11vnc under a restart loop (it exits whenever the X server isn't up
|
|
960
976
|
// yet). Pidfile-guarded.
|
|
961
|
-
`[ -x /usr/bin/x11vnc ] || exit 0; [ -f /tmp/uai-x11vnc.pid ] && /usr/bin/kill -0 "$(/usr/bin/cat /tmp/uai-x11vnc.pid)" 2>/dev/null && exit 0; /usr/bin/pkill -f 'x11vnc -display' 2>/dev/null; /usr/bin/nohup /bin/sh -c 'echo $$ > /tmp/uai-x11vnc.pid; while true; do /usr/bin/x11vnc -display ${SERVER_DISPLAY} -forever -shared -nopw -quiet >>/tmp/uai-x11vnc.log 2>&1; /bin/sleep 2; done' >/dev/null 2>&1 &`,
|
|
977
|
+
`[ -x /usr/bin/x11vnc ] || exit 0; [ -f /tmp/uai-x11vnc.pid ] && /usr/bin/kill -0 "$(/usr/bin/cat /tmp/uai-x11vnc.pid)" 2>/dev/null && exit 0; /usr/bin/pkill -A -f 'x11vnc -display' 2>/dev/null; /usr/bin/nohup /bin/sh -c 'echo $$ > /tmp/uai-x11vnc.pid; while true; do /usr/bin/x11vnc -display ${SERVER_DISPLAY} -forever -shared -nopw -quiet >>/tmp/uai-x11vnc.log 2>&1; /bin/sleep 2; done' >/dev/null 2>&1 &`,
|
|
962
978
|
// websockify/noVNC under the same pattern.
|
|
963
|
-
`[ -x /usr/bin/websockify ] || exit 0; [ -f /tmp/uai-novnc.pid ] && /usr/bin/kill -0 "$(/usr/bin/cat /tmp/uai-novnc.pid)" 2>/dev/null && exit 0; /usr/bin/pkill -f 'websockify.*6080' 2>/dev/null; /usr/bin/nohup /bin/sh -c 'echo $$ > /tmp/uai-novnc.pid; while true; do /usr/bin/websockify --web=/usr/share/novnc 0.0.0.0:6080 localhost:5900 >>/tmp/uai-websockify.log 2>&1; /bin/sleep 2; done' >/dev/null 2>&1 &`,
|
|
979
|
+
`[ -x /usr/bin/websockify ] || exit 0; [ -f /tmp/uai-novnc.pid ] && /usr/bin/kill -0 "$(/usr/bin/cat /tmp/uai-novnc.pid)" 2>/dev/null && exit 0; /usr/bin/pkill -A -f 'websockify.*6080' 2>/dev/null; /usr/bin/nohup /bin/sh -c 'echo $$ > /tmp/uai-novnc.pid; while true; do /usr/bin/websockify --web=/usr/share/novnc 0.0.0.0:6080 localhost:5900 >>/tmp/uai-websockify.log 2>&1; /bin/sleep 2; done' >/dev/null 2>&1 &`,
|
|
964
980
|
];
|
|
965
981
|
|
|
966
982
|
const DEFAULT_NODE_BIN =
|
|
@@ -1085,13 +1101,20 @@ export async function setupBrowserTesting(
|
|
|
1085
1101
|
// costs ~0.5s once the build is present.
|
|
1086
1102
|
const installed = environment
|
|
1087
1103
|
? await (async () => {
|
|
1088
|
-
await environment.exec({
|
|
1104
|
+
const chown = await environment.exec({
|
|
1089
1105
|
argv: ["/usr/bin/chown", "node:node", "/opt/pw-browsers"],
|
|
1090
1106
|
user: "root",
|
|
1091
1107
|
env: authorityEnv,
|
|
1092
1108
|
timeoutMs: 5_000,
|
|
1093
1109
|
maxOutputBytes: 64 * 1024,
|
|
1094
1110
|
});
|
|
1111
|
+
if (chown.exitCode !== 0) {
|
|
1112
|
+
// Same non-fatal stance as the CLI path below.
|
|
1113
|
+
console.warn(
|
|
1114
|
+
`[browser] task ${taskId}: chown /opt/pw-browsers failed (status ${chown.exitCode}); ` +
|
|
1115
|
+
`continuing with the prewarm. ${Buffer.from(chown.stderr).toString("utf8").slice(0, 200)}`,
|
|
1116
|
+
);
|
|
1117
|
+
}
|
|
1095
1118
|
return environment.exec({
|
|
1096
1119
|
argv: ["sh", "-lc", PREWARM_CMD],
|
|
1097
1120
|
cwd: "/workspace",
|
|
@@ -1113,7 +1136,16 @@ export async function setupBrowserTesting(
|
|
|
1113
1136
|
],
|
|
1114
1137
|
{ timeoutMs: 5_000 },
|
|
1115
1138
|
).then(async (chown) => {
|
|
1116
|
-
if (chown.status !== 0)
|
|
1139
|
+
if (chown.status !== 0) {
|
|
1140
|
+
// Not fatal. On apple-container /opt/pw-browsers is a host-wide
|
|
1141
|
+
// virtiofs volume whose ownership the guest cannot change (EPERM)
|
|
1142
|
+
// although it is already node-owned inside the container; the
|
|
1143
|
+
// prewarm below is the real test of writability, so let it decide.
|
|
1144
|
+
console.warn(
|
|
1145
|
+
`[browser] task ${taskId}: chown /opt/pw-browsers failed (status ${chown.status}); ` +
|
|
1146
|
+
`continuing with the prewarm. ${chown.stderr.slice(0, 200)}`,
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1117
1149
|
return taskContainerCli(
|
|
1118
1150
|
[
|
|
1119
1151
|
"exec",
|
package/package.json
CHANGED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-121 machine tunnel routing, extracted from main.ts so its one expensive
|
|
3
|
+
* step is bounded and testable. Resolving a machine's address is a cloud API
|
|
4
|
+
* call — `aws ec2 describe-instances` through a spawned CLI, 0.5–1.5 s on the
|
|
5
|
+
* default t4g.medium host — and the cloud opens one tunnel per HTTP request,
|
|
6
|
+
* so a browser pane (noVNC ships dozens of unbundled modules) or the editor
|
|
7
|
+
* (code-server, more) paid that call dozens of times per load. Live
|
|
8
|
+
* 2026-09-06: "connecting, but very slow" on every AWS machine task, while
|
|
9
|
+
* Docker and apple-container hosts, whose routes are a local inspect, felt
|
|
10
|
+
* instant.
|
|
11
|
+
*
|
|
12
|
+
* The memo is per task, short (MACHINE_ROUTE_TTL_MS), and holds only a proven
|
|
13
|
+
* `running` route: a stopped or unknown machine is asked again on every open
|
|
14
|
+
* so a wake is seen promptly; a dial failure drops the entry so a machine that
|
|
15
|
+
* moved (a fresh public IP after stop/start) is re-described on the next open;
|
|
16
|
+
* a stop/down command drops it too — and bumps a per-task generation, so a
|
|
17
|
+
* description that was already in flight when the stop landed cannot
|
|
18
|
+
* re-memoise the pre-stop address. Concurrent cold opens share one resolution
|
|
19
|
+
* (the pane's first burst is half a dozen parallel requests), and a machine
|
|
20
|
+
* seen not running is not asked again for a few seconds (noVNC retries every
|
|
21
|
+
* couple of seconds against a stopped machine; without that, each retry was
|
|
22
|
+
* a CLI spawn). A refused dial invalidates at once; an address that
|
|
23
|
+
* black-holes (an instance stopped while memoised) hangs on the OS connect
|
|
24
|
+
* timeout instead — machine dials carry no connect timeout of their own,
|
|
25
|
+
* which predates this module — so the TTL is the real bound there.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export type MachineTunnelRoute =
|
|
29
|
+
| { kind: "running"; ipv4Address: string }
|
|
30
|
+
| { kind: "unavailable" }
|
|
31
|
+
| { kind: "unknown"; detail: string };
|
|
32
|
+
|
|
33
|
+
export interface MachineTunnelRouteDeps<Task> {
|
|
34
|
+
reconstruct(task: Task): Promise<{
|
|
35
|
+
inspectRoute?: () => Promise<MachineTunnelRoute>;
|
|
36
|
+
} | null>;
|
|
37
|
+
now(): number;
|
|
38
|
+
ttlMs?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface MachineTunnelRouteRequest {
|
|
42
|
+
taskId: string;
|
|
43
|
+
target: string;
|
|
44
|
+
containerPort?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Long enough to cover a pane or editor load (dozens of opens inside a few
|
|
48
|
+
* seconds), short enough that a machine's address is never trusted past the
|
|
49
|
+
* next stop/start cycle by much; a failed dial drops it sooner. */
|
|
50
|
+
export const MACHINE_ROUTE_TTL_MS = 45_000;
|
|
51
|
+
/** How long a not-running answer is trusted before the machine is asked
|
|
52
|
+
* again — long enough to absorb a reconnecting pane, short enough that a wake
|
|
53
|
+
* is seen within seconds. */
|
|
54
|
+
export const MACHINE_ROUTE_NEGATIVE_TTL_MS = 5_000;
|
|
55
|
+
|
|
56
|
+
const memo = new Map<string, { ipv4Address: string; expiresAt: number }>();
|
|
57
|
+
const negative = new Map<string, number>();
|
|
58
|
+
const inFlight = new Map<string, Promise<string | null>>();
|
|
59
|
+
/** Bumped by every invalidation; a resolution only writes the memo if the
|
|
60
|
+
* generation it started under is still current. */
|
|
61
|
+
const generation = new Map<string, number>();
|
|
62
|
+
|
|
63
|
+
export async function resolveMachineTunnelRoute<
|
|
64
|
+
Task extends { codeServerPort?: number | null },
|
|
65
|
+
>(
|
|
66
|
+
request: MachineTunnelRouteRequest,
|
|
67
|
+
task: Task,
|
|
68
|
+
deps: MachineTunnelRouteDeps<Task>,
|
|
69
|
+
): Promise<{ host: string; port: number } | null> {
|
|
70
|
+
// The cheap refusal first: an unroutable request never costs an API call.
|
|
71
|
+
const port =
|
|
72
|
+
request.target === "editor"
|
|
73
|
+
? (task.codeServerPort ?? null)
|
|
74
|
+
: (request.containerPort ?? null);
|
|
75
|
+
if (!port) return null;
|
|
76
|
+
const host = await machineAddress(request.taskId, task, deps);
|
|
77
|
+
return host === null ? null : { host, port };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function machineAddress<Task>(
|
|
81
|
+
taskId: string,
|
|
82
|
+
task: Task,
|
|
83
|
+
deps: MachineTunnelRouteDeps<Task>,
|
|
84
|
+
): Promise<string | null> {
|
|
85
|
+
const now = deps.now();
|
|
86
|
+
const hit = memo.get(taskId);
|
|
87
|
+
if (hit && hit.expiresAt > now) return hit.ipv4Address;
|
|
88
|
+
memo.delete(taskId);
|
|
89
|
+
const notRunningUntil = negative.get(taskId);
|
|
90
|
+
if (notRunningUntil !== undefined && notRunningUntil > now) return null;
|
|
91
|
+
negative.delete(taskId);
|
|
92
|
+
const pending = inFlight.get(taskId);
|
|
93
|
+
if (pending) return pending;
|
|
94
|
+
const startedUnder = generation.get(taskId) ?? 0;
|
|
95
|
+
const resolution = (async (): Promise<string | null> => {
|
|
96
|
+
const environment = await deps.reconstruct(task);
|
|
97
|
+
const route = environment?.inspectRoute
|
|
98
|
+
? await environment.inspectRoute()
|
|
99
|
+
: null;
|
|
100
|
+
const settledAt = deps.now();
|
|
101
|
+
const stillCurrent = (generation.get(taskId) ?? 0) === startedUnder;
|
|
102
|
+
if (!route || route.kind !== "running") {
|
|
103
|
+
if (stillCurrent) {
|
|
104
|
+
negative.set(taskId, settledAt + MACHINE_ROUTE_NEGATIVE_TTL_MS);
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
if (stillCurrent) {
|
|
109
|
+
memo.set(taskId, {
|
|
110
|
+
ipv4Address: route.ipv4Address,
|
|
111
|
+
expiresAt: settledAt + (deps.ttlMs ?? MACHINE_ROUTE_TTL_MS),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return route.ipv4Address;
|
|
115
|
+
})().finally(() => {
|
|
116
|
+
inFlight.delete(taskId);
|
|
117
|
+
});
|
|
118
|
+
inFlight.set(taskId, resolution);
|
|
119
|
+
return resolution;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** A dial to the memoised address failed, or the task was stopped, started or
|
|
123
|
+
* torn down: the next open must describe the machine again, and a description
|
|
124
|
+
* already in flight must not write what it learned before this point. */
|
|
125
|
+
export function invalidateMachineTunnelRoute(taskId: string): void {
|
|
126
|
+
memo.delete(taskId);
|
|
127
|
+
negative.delete(taskId);
|
|
128
|
+
generation.set(taskId, (generation.get(taskId) ?? 0) + 1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function resetMachineTunnelRoutesForTesting(): void {
|
|
132
|
+
memo.clear();
|
|
133
|
+
negative.clear();
|
|
134
|
+
inFlight.clear();
|
|
135
|
+
generation.clear();
|
|
136
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -112,6 +112,10 @@ import {
|
|
|
112
112
|
import { reconstructPersistedTaskEnvironment } from "../lib/task-environment";
|
|
113
113
|
import { logVnodePressure } from "../lib/vnode-pressure";
|
|
114
114
|
import { resolveAppleTunnelRoute } from "./apple-tunnel-route";
|
|
115
|
+
import {
|
|
116
|
+
invalidateMachineTunnelRoute,
|
|
117
|
+
resolveMachineTunnelRoute,
|
|
118
|
+
} from "./machine-tunnel-route";
|
|
115
119
|
import {
|
|
116
120
|
configureBundledContainerRuntime,
|
|
117
121
|
selectedRuntimeMatchesPersistedTaskEnvironments,
|
|
@@ -554,9 +558,16 @@ async function startLocalUi(): Promise<void> {
|
|
|
554
558
|
stopTask: async (taskId) => {
|
|
555
559
|
const runtimeProblem = containerRuntimeProblem();
|
|
556
560
|
if (runtimeProblem) return { ok: false, error: runtimeProblem };
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
561
|
+
// ADR-121: a stopped machine loses its memoised address (same
|
|
562
|
+
// before-and-after discipline as the cloud's taskStop command).
|
|
563
|
+
invalidateMachineTunnelRoute(taskId);
|
|
564
|
+
try {
|
|
565
|
+
const result = await getOrchestrator().stopTask(taskId);
|
|
566
|
+
if (result.ok) queueStoppedTaskStatus(taskId);
|
|
567
|
+
return result;
|
|
568
|
+
} finally {
|
|
569
|
+
invalidateMachineTunnelRoute(taskId);
|
|
570
|
+
}
|
|
560
571
|
},
|
|
561
572
|
});
|
|
562
573
|
console.log(`[host-agent] local UI on http://127.0.0.1:${handle.port}`);
|
|
@@ -1861,6 +1872,8 @@ function handleHttpTunnelOpen(
|
|
|
1861
1872
|
// ADR-043: if this was an ad-hoc preview sidecar, drop its cached port so
|
|
1862
1873
|
// the next request recreates it (no-op for published / container-IP targets).
|
|
1863
1874
|
invalidatePreviewSidecar(target.port);
|
|
1875
|
+
// ADR-121: a memoised machine address that no longer answers is stale.
|
|
1876
|
+
invalidateMachineTunnelRoute(frame.taskId);
|
|
1864
1877
|
if (!acked) {
|
|
1865
1878
|
send(wsSocket, {
|
|
1866
1879
|
kind: "tunnel.ack",
|
|
@@ -1927,6 +1940,8 @@ function handleRawTunnelOpen(
|
|
|
1927
1940
|
// ADR-043: if this was an ad-hoc preview sidecar, drop its cached port so
|
|
1928
1941
|
// the next request recreates it (no-op for published / container-IP targets).
|
|
1929
1942
|
invalidatePreviewSidecar(target.port);
|
|
1943
|
+
// ADR-121: a memoised machine address that no longer answers is stale.
|
|
1944
|
+
invalidateMachineTunnelRoute(frame.taskId);
|
|
1930
1945
|
if (!acked) {
|
|
1931
1946
|
send(wsSocket, {
|
|
1932
1947
|
kind: "tunnel.ack",
|
|
@@ -2004,17 +2019,23 @@ async function resolveTunnelTarget(
|
|
|
2004
2019
|
// the container runtime's health. macOS-local machines need a published
|
|
2005
2020
|
// fallback (bridge IPs are not host-routable there) — recorded ADR-121
|
|
2006
2021
|
// follow-up, not silently misrouted: the connect simply fails.
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2022
|
+
// The address resolution is a cloud API call and the cloud opens one
|
|
2023
|
+
// tunnel per request, so it is memoised per task for a short TTL and
|
|
2024
|
+
// dropped on a failed dial or a stop — see machine-tunnel-route.ts.
|
|
2025
|
+
return resolveMachineTunnelRoute(
|
|
2026
|
+
{
|
|
2027
|
+
taskId: frame.taskId,
|
|
2028
|
+
target: frame.target,
|
|
2029
|
+
...(frame.containerPort !== undefined
|
|
2030
|
+
? { containerPort: frame.containerPort }
|
|
2031
|
+
: {}),
|
|
2032
|
+
},
|
|
2033
|
+
machineTask,
|
|
2034
|
+
{
|
|
2035
|
+
reconstruct: (task) => reconstructPersistedTaskEnvironment(task),
|
|
2036
|
+
now: Date.now,
|
|
2037
|
+
},
|
|
2038
|
+
);
|
|
2018
2039
|
}
|
|
2019
2040
|
if (containerRuntimeProblem()) return null;
|
|
2020
2041
|
const task = getHostTask(frame.taskId);
|
|
@@ -2275,18 +2296,36 @@ async function dispatchCommand(
|
|
|
2275
2296
|
args: unknown[],
|
|
2276
2297
|
): Promise<HostCommandResult<unknown>> {
|
|
2277
2298
|
switch (command) {
|
|
2278
|
-
case "taskUp":
|
|
2279
|
-
|
|
2299
|
+
case "taskUp": {
|
|
2300
|
+
const launchInput = expectTaskLaunchInput(args, 0);
|
|
2301
|
+
// A (re)started machine may come back on a new address, and one just
|
|
2302
|
+
// seen as not-running is about to run: forget both, before and after.
|
|
2303
|
+
invalidateMachineTunnelRoute(launchInput.task.id);
|
|
2304
|
+
return hostCommands
|
|
2305
|
+
.taskUp(ctx, launchInput)
|
|
2306
|
+
.finally(() => invalidateMachineTunnelRoute(launchInput.task.id));
|
|
2307
|
+
}
|
|
2280
2308
|
case "taskDown": {
|
|
2281
2309
|
const downInput = expectTaskDownInput(args, 0);
|
|
2282
|
-
|
|
2310
|
+
// A torn-down machine loses its address: drop the memo before (opens in
|
|
2311
|
+
// flight must not re-memoise) and after (opens that raced the command).
|
|
2312
|
+
invalidateMachineTunnelRoute(downInput.taskId);
|
|
2313
|
+
return hostCommands
|
|
2314
|
+
.taskDown(ctx, downInput)
|
|
2315
|
+
.finally(() => invalidateMachineTunnelRoute(downInput.taskId));
|
|
2316
|
+
}
|
|
2317
|
+
case "taskStop": {
|
|
2318
|
+
const stopTaskId = typeof args[0] === "string" ? args[0] : "";
|
|
2319
|
+
// A stopped machine loses its address; same before-and-after discipline.
|
|
2320
|
+
invalidateMachineTunnelRoute(stopTaskId);
|
|
2321
|
+
return hostCommands
|
|
2322
|
+
.taskStop(
|
|
2323
|
+
ctx,
|
|
2324
|
+
stopTaskId,
|
|
2325
|
+
(args[1] ?? { reason: "manual" }) as { reason: "idle" | "manual" },
|
|
2326
|
+
)
|
|
2327
|
+
.finally(() => invalidateMachineTunnelRoute(stopTaskId));
|
|
2283
2328
|
}
|
|
2284
|
-
case "taskStop":
|
|
2285
|
-
return hostCommands.taskStop(
|
|
2286
|
-
ctx,
|
|
2287
|
-
typeof args[0] === "string" ? args[0] : "",
|
|
2288
|
-
(args[1] ?? { reason: "manual" }) as { reason: "idle" | "manual" },
|
|
2289
|
-
);
|
|
2290
2329
|
case "taskStatus":
|
|
2291
2330
|
return hostCommands.taskStatus(ctx, expectString(args, 0));
|
|
2292
2331
|
case "channelEnsure":
|