@runuai/host 0.9.72 → 0.9.74
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/agents/machine-durable.ts +22 -7
- package/lib/machine-exec.ts +14 -6
- package/lib/task-environment/machine-task-up.ts +229 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/main.ts +33 -3
|
@@ -171,14 +171,16 @@ export class MachineDurableProcess implements LineTransport {
|
|
|
171
171
|
(result) => {
|
|
172
172
|
const stderr = Buffer.from(result.stderr).toString("utf8");
|
|
173
173
|
if (stderr) this.#stderrBuf = (this.#stderrBuf + stderr).slice(-8192);
|
|
174
|
-
if (result.exitCode !== 0)
|
|
174
|
+
if (result.exitCode !== 0) {
|
|
175
|
+
this.#finish(null, `launch exited ${result.exitCode}`);
|
|
176
|
+
}
|
|
175
177
|
},
|
|
176
178
|
(error: unknown) => {
|
|
177
179
|
this.#stderrBuf = (
|
|
178
180
|
this.#stderrBuf +
|
|
179
181
|
(error instanceof Error ? error.message : String(error))
|
|
180
182
|
).slice(-8192);
|
|
181
|
-
this.#finish(null);
|
|
183
|
+
this.#finish(null, "launch threw");
|
|
182
184
|
},
|
|
183
185
|
);
|
|
184
186
|
} else {
|
|
@@ -306,7 +308,10 @@ export class MachineDurableProcess implements LineTransport {
|
|
|
306
308
|
this.#stderrBuf = meta.stderrTail.slice(-8192);
|
|
307
309
|
}
|
|
308
310
|
if (this.#debug) this.#log(`exit meta: code ${meta.code ?? null}`);
|
|
309
|
-
this.#finish(
|
|
311
|
+
this.#finish(
|
|
312
|
+
typeof meta.code === "number" ? meta.code : null,
|
|
313
|
+
"runner exit meta",
|
|
314
|
+
);
|
|
310
315
|
}
|
|
311
316
|
}
|
|
312
317
|
|
|
@@ -317,7 +322,9 @@ export class MachineDurableProcess implements LineTransport {
|
|
|
317
322
|
this.#probing = true;
|
|
318
323
|
try {
|
|
319
324
|
if (!this.#sawSpawnMeta) {
|
|
320
|
-
if (Date.now() - this.#startedAt > HEARTBEAT_STALE_MS)
|
|
325
|
+
if (Date.now() - this.#startedAt > HEARTBEAT_STALE_MS) {
|
|
326
|
+
this.#finish(null, "no spawn meta within grace");
|
|
327
|
+
}
|
|
321
328
|
return;
|
|
322
329
|
}
|
|
323
330
|
const fresh = await machineHeartbeatFresh(
|
|
@@ -326,14 +333,19 @@ export class MachineDurableProcess implements LineTransport {
|
|
|
326
333
|
HEARTBEAT_STALE_MS,
|
|
327
334
|
);
|
|
328
335
|
if (this.#closed || this.#detached) return;
|
|
329
|
-
if (!fresh) this.#finish(null);
|
|
336
|
+
if (!fresh) this.#finish(null, "heartbeat stale");
|
|
330
337
|
} finally {
|
|
331
338
|
this.#probing = false;
|
|
332
339
|
}
|
|
333
340
|
}
|
|
334
341
|
|
|
335
|
-
#finish(code: number | null): void {
|
|
342
|
+
#finish(code: number | null, reason = "unspecified"): void {
|
|
336
343
|
if (this.#closed) return;
|
|
344
|
+
// Session deaths are load-bearing and were undiagnosable without this
|
|
345
|
+
// (live 2026-08-27: a 65s recycle loop with no trace). Always log.
|
|
346
|
+
console.warn(
|
|
347
|
+
`[machine-durable] session ${this.#sessionDir} finished (code ${code ?? "null"}): ${reason}`,
|
|
348
|
+
);
|
|
337
349
|
this.#closed = true;
|
|
338
350
|
this.#stopTimers();
|
|
339
351
|
const tail = this.#tail;
|
|
@@ -421,7 +433,10 @@ export class MachineDurableProcess implements LineTransport {
|
|
|
421
433
|
} catch {
|
|
422
434
|
// Inbox unwritable — fall through to the grace timer.
|
|
423
435
|
}
|
|
424
|
-
this.#closeTimer = setTimeout(
|
|
436
|
+
this.#closeTimer = setTimeout(
|
|
437
|
+
() => this.#finish(null, "close grace elapsed"),
|
|
438
|
+
CLOSE_GRACE_MS,
|
|
439
|
+
);
|
|
425
440
|
}
|
|
426
441
|
}
|
|
427
442
|
|
package/lib/machine-exec.ts
CHANGED
|
@@ -84,12 +84,20 @@ export function buildRemoteCommand(request: {
|
|
|
84
84
|
const command = request.argv.map(shellQuote).join(" ");
|
|
85
85
|
if (request.detached) {
|
|
86
86
|
// Detached durable sessions manage their own transcript IO (runner.mjs);
|
|
87
|
-
// the launch just needs the process to survive this ssh connection
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
87
|
+
// the launch just needs the process to survive this ssh connection AND
|
|
88
|
+
// the connection to close immediately. The redirects must cover the
|
|
89
|
+
// WHOLE background job and the job must `exec` into the payload: with
|
|
90
|
+
// redirects on the inner command only, the waiting shell kept the ssh
|
|
91
|
+
// channel's stdout/stderr open until the runner died, so sshd never saw
|
|
92
|
+
// EOF and every launch rode its timeout into a false spawn failure
|
|
93
|
+
// (live 2026-08-27: a 65s session recycle loop on the second machine
|
|
94
|
+
// task; the first machine's stable session was an accident of its host
|
|
95
|
+
// dying seconds before the timeout could mark the row closed).
|
|
96
|
+
const body = [
|
|
97
|
+
...(request.cwd ? [`cd ${shellQuote(request.cwd)} &&`] : []),
|
|
98
|
+
`exec ${envPrefix}setsid ${command}`,
|
|
99
|
+
].join(" ");
|
|
100
|
+
return `{ ${body}; } </dev/null >/dev/null 2>&1 & exit 0`;
|
|
93
101
|
}
|
|
94
102
|
parts.push(`exec ${envPrefix}${command}`);
|
|
95
103
|
return parts.join(" ");
|
|
@@ -15,12 +15,19 @@
|
|
|
15
15
|
* directory that really does hold this task's host-side state — its machine
|
|
16
16
|
* key). Consumers that need the workspace go through the environment handle.
|
|
17
17
|
*/
|
|
18
|
+
import { execFile } from "node:child_process";
|
|
19
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
20
|
+
import { dirname, resolve } from "node:path";
|
|
21
|
+
import { fileURLToPath } from "node:url";
|
|
22
|
+
import { promisify } from "node:util";
|
|
23
|
+
|
|
18
24
|
import type {
|
|
19
25
|
TaskDownResult,
|
|
20
26
|
TaskLaunchInput,
|
|
21
27
|
TaskUpCredentials,
|
|
22
28
|
TaskUpResult,
|
|
23
29
|
} from "../agent";
|
|
30
|
+
import { sharedRoot } from "../shared-files";
|
|
24
31
|
import {
|
|
25
32
|
MACHINE_TASK_ENVIRONMENT_PROVIDER,
|
|
26
33
|
parseMachineTaskEnvironmentLocator,
|
|
@@ -36,6 +43,9 @@ import type {
|
|
|
36
43
|
|
|
37
44
|
/** In-machine clone budget per project. */
|
|
38
45
|
const CLONE_TIMEOUT_MS = 10 * 60_000;
|
|
46
|
+
/** The machine port code-server listens on (dialed directly by the editor
|
|
47
|
+
* tunnel via inspectRoute; never published). */
|
|
48
|
+
export const MACHINE_CODE_SERVER_PORT = 8080;
|
|
39
49
|
const GIT_STEP_TIMEOUT_MS = 60_000;
|
|
40
50
|
const STEP_OUTPUT_BYTES = 256 * 1024;
|
|
41
51
|
|
|
@@ -223,6 +233,191 @@ async function cloneProject(
|
|
|
223
233
|
return null;
|
|
224
234
|
}
|
|
225
235
|
|
|
236
|
+
/** The curated code-server defaults (slim chrome, telemetry off, trust
|
|
237
|
+
* off) — the SAME seed the standard image bakes; resolved from this package
|
|
238
|
+
* so machine and container editors can never diverge. Machines adjust two
|
|
239
|
+
* keys host-side: the task's ADR-091 theme, and bash for the terminal (the
|
|
240
|
+
* machine payload carries no zsh). */
|
|
241
|
+
function editorSettingsSeed(editorTheme: string | undefined): string {
|
|
242
|
+
const seedPath = resolve(
|
|
243
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
244
|
+
"..",
|
|
245
|
+
"..",
|
|
246
|
+
"images",
|
|
247
|
+
"standard",
|
|
248
|
+
"container",
|
|
249
|
+
"code-server-settings.json",
|
|
250
|
+
);
|
|
251
|
+
const seed = JSON.parse(readFileSync(seedPath, "utf8")) as Record<
|
|
252
|
+
string,
|
|
253
|
+
unknown
|
|
254
|
+
>;
|
|
255
|
+
if (editorTheme) seed["workbench.colorTheme"] = editorTheme;
|
|
256
|
+
seed["terminal.integrated.defaultProfile.linux"] = "bash";
|
|
257
|
+
return `${JSON.stringify(seed, null, 2)}\n`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Seed the editor settings exactly once (an existing settings.json is the
|
|
261
|
+
* user's own state and is never touched — same contract as uai-init). */
|
|
262
|
+
async function seedEditorSettings(
|
|
263
|
+
handle: TaskEnvironmentHandle<unknown>,
|
|
264
|
+
editorTheme: string | undefined,
|
|
265
|
+
): Promise<void> {
|
|
266
|
+
const destination =
|
|
267
|
+
"/home/node/.local/share/code-server/User/settings.json";
|
|
268
|
+
await handle.exec({
|
|
269
|
+
argv: [
|
|
270
|
+
"/bin/sh",
|
|
271
|
+
"-c",
|
|
272
|
+
'[ -e "$1" ] && exit 0; mkdir -p "$(dirname "$1")" && cat > "$1"',
|
|
273
|
+
"seed",
|
|
274
|
+
destination,
|
|
275
|
+
],
|
|
276
|
+
stdin: Buffer.from(editorSettingsSeed(editorTheme)),
|
|
277
|
+
timeoutMs: GIT_STEP_TIMEOUT_MS,
|
|
278
|
+
maxOutputBytes: STEP_OUTPUT_BYTES,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const execFileAsync = promisify(execFile);
|
|
283
|
+
/** Shared-files snapshots are reference docs, not repos; a tree past this
|
|
284
|
+
* is skipped with a warning rather than ballooning host memory. */
|
|
285
|
+
const SHARED_FILES_TAR_MAX_BYTES = 256 * 1024 * 1024;
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* ADR-062 shared files on machines: containers get live bind mounts at
|
|
289
|
+
* /workspace/files/{org,me}; a machine shares no filesystem, so it gets a
|
|
290
|
+
* POINT-IN-TIME COPY at provision (tar streamed over the transport). The
|
|
291
|
+
* managed-hosting answer for live semantics is EFS (ADR-121); until then a
|
|
292
|
+
* snapshot with an honest note beats silently absent files.
|
|
293
|
+
* Returns a warning string when rw mode was requested (writes cannot sync
|
|
294
|
+
* back) or a scope was skipped; null when clean.
|
|
295
|
+
*/
|
|
296
|
+
async function copySharedFiles(
|
|
297
|
+
handle: TaskEnvironmentHandle<unknown>,
|
|
298
|
+
workspacePath: string,
|
|
299
|
+
task: { ownerOrgId?: string; ownerUserId: string; sharedFiles?: string },
|
|
300
|
+
): Promise<string | null> {
|
|
301
|
+
const mode = task.sharedFiles ?? "ro";
|
|
302
|
+
if (mode === "off") return null;
|
|
303
|
+
const scopes: Array<{ host: string; destination: string }> = [];
|
|
304
|
+
if (task.ownerOrgId) {
|
|
305
|
+
scopes.push({
|
|
306
|
+
host: sharedRoot("org", task.ownerOrgId, task.ownerUserId),
|
|
307
|
+
destination: `${workspacePath}/files/org`,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
scopes.push({
|
|
311
|
+
host: sharedRoot("me", task.ownerOrgId ?? "", task.ownerUserId),
|
|
312
|
+
destination: `${workspacePath}/files/me`,
|
|
313
|
+
});
|
|
314
|
+
const warnings: string[] = [];
|
|
315
|
+
let copiedAny = false;
|
|
316
|
+
for (const scope of scopes) {
|
|
317
|
+
if (!existsSync(scope.host) || readdirSync(scope.host).length === 0) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
const tarball = await execFileAsync(
|
|
322
|
+
"tar",
|
|
323
|
+
["-C", scope.host, "-cf", "-", "."],
|
|
324
|
+
{ encoding: "buffer", maxBuffer: SHARED_FILES_TAR_MAX_BYTES },
|
|
325
|
+
);
|
|
326
|
+
const result = await handle.exec({
|
|
327
|
+
argv: [
|
|
328
|
+
"/bin/sh",
|
|
329
|
+
"-c",
|
|
330
|
+
'mkdir -p "$1" && tar -C "$1" -xf -',
|
|
331
|
+
"shared",
|
|
332
|
+
scope.destination,
|
|
333
|
+
],
|
|
334
|
+
stdin: tarball.stdout,
|
|
335
|
+
timeoutMs: 5 * 60_000,
|
|
336
|
+
maxOutputBytes: STEP_OUTPUT_BYTES,
|
|
337
|
+
});
|
|
338
|
+
if (result.exitCode !== 0) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
`extract exited ${result.exitCode}: ${Buffer.from(result.stderr).toString("utf8").trim()}`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
copiedAny = true;
|
|
344
|
+
} catch (error) {
|
|
345
|
+
warnings.push(
|
|
346
|
+
`shared files: could not copy ${scope.destination}: ${
|
|
347
|
+
error instanceof Error ? error.message : String(error)
|
|
348
|
+
}`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (copiedAny || warnings.length === 0) {
|
|
353
|
+
// Marker gates the "## Shared files" preamble briefing, mirroring the
|
|
354
|
+
// container path's task-up marker.
|
|
355
|
+
await handle
|
|
356
|
+
.writeWorkspaceFile(`${workspacePath}/.uai/files-mounted`, Buffer.from(""))
|
|
357
|
+
.catch(() => {});
|
|
358
|
+
}
|
|
359
|
+
if (mode === "rw") {
|
|
360
|
+
warnings.push(
|
|
361
|
+
"shared files on this machine task are a point-in-time copy — " +
|
|
362
|
+
"changes under /workspace/files will NOT sync back to the host yet",
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
return warnings.length > 0 ? warnings.join("\n") : null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Launch code-server for the Editor pane, exactly once. The pgrep guard
|
|
369
|
+
* makes re-provision and resume idempotent; the exec chain leaves only
|
|
370
|
+
* code-server holding the detached job. */
|
|
371
|
+
async function ensureCodeServer(
|
|
372
|
+
handle: TaskEnvironmentHandle<unknown>,
|
|
373
|
+
workspacePath: string,
|
|
374
|
+
editorTheme: string | undefined,
|
|
375
|
+
): Promise<void> {
|
|
376
|
+
// Settings before process: code-server reads them at startup. A failed
|
|
377
|
+
// seed still launches — a themed editor is worth less than one that starts.
|
|
378
|
+
await seedEditorSettings(handle, editorTheme).catch((error: unknown) => {
|
|
379
|
+
console.warn(
|
|
380
|
+
`[machine] editor settings seed failed: ${
|
|
381
|
+
error instanceof Error ? error.message : String(error)
|
|
382
|
+
}`,
|
|
383
|
+
);
|
|
384
|
+
});
|
|
385
|
+
// The guard and the launch are SEPARATE execs on purpose: any guard that
|
|
386
|
+
// shares a cmdline with the launch text matches itself — the bracketed
|
|
387
|
+
// pattern alone did not save the first combined script, because the plain
|
|
388
|
+
// launch line later in the same argv still contained code-server +
|
|
389
|
+
// --bind-addr (live 2026-08-27, second silent skip).
|
|
390
|
+
const running = await handle.exec({
|
|
391
|
+
argv: [
|
|
392
|
+
"/bin/sh",
|
|
393
|
+
"-c",
|
|
394
|
+
'pgrep -f "[c]ode-server.*--bind-addr" >/dev/null 2>&1',
|
|
395
|
+
],
|
|
396
|
+
timeoutMs: GIT_STEP_TIMEOUT_MS,
|
|
397
|
+
maxOutputBytes: STEP_OUTPUT_BYTES,
|
|
398
|
+
});
|
|
399
|
+
if (running.exitCode === 0) return;
|
|
400
|
+
const script =
|
|
401
|
+
"mkdir -p /home/node/.local/share/code-server && " +
|
|
402
|
+
"exec /home/node/.local/bin/code-server " +
|
|
403
|
+
"--user-data-dir /home/node/.local/share/code-server " +
|
|
404
|
+
"--disable-workspace-trust --auth none --disable-telemetry " +
|
|
405
|
+
`--disable-update-check --bind-addr 0.0.0.0:${MACHINE_CODE_SERVER_PORT} ` +
|
|
406
|
+
'"$1" >/tmp/code-server.log 2>&1';
|
|
407
|
+
const result = await handle.launchDetachedSession({
|
|
408
|
+
argv: ["/bin/sh", "-c", script, "code-server", workspacePath],
|
|
409
|
+
inheritEnv: [],
|
|
410
|
+
env: {},
|
|
411
|
+
launchTimeoutMs: 30_000,
|
|
412
|
+
maxOutputBytes: 64 * 1024,
|
|
413
|
+
});
|
|
414
|
+
if (result.exitCode !== 0) {
|
|
415
|
+
throw new Error(
|
|
416
|
+
`code-server launch exited ${result.exitCode}: ${Buffer.from(result.stderr).toString("utf8").trim()}`,
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
226
421
|
/** Wrap the void-teardown machine handle so registry consumers get the
|
|
227
422
|
* TaskDownResult contract the compose providers speak. */
|
|
228
423
|
function withTaskDownResult(
|
|
@@ -249,6 +444,9 @@ function withTaskDownResult(
|
|
|
249
444
|
await handle.teardown();
|
|
250
445
|
return { status: "terminated" };
|
|
251
446
|
},
|
|
447
|
+
...(handle.inspectRoute
|
|
448
|
+
? { inspectRoute: () => handle.inspectRoute!() }
|
|
449
|
+
: {}),
|
|
252
450
|
};
|
|
253
451
|
}
|
|
254
452
|
|
|
@@ -311,6 +509,36 @@ export function createMachineHostTaskEnvironmentProvider(
|
|
|
311
509
|
throw error;
|
|
312
510
|
}
|
|
313
511
|
|
|
512
|
+
// ADR-062 shared files: point-in-time copy (see copySharedFiles).
|
|
513
|
+
const sharedWarning = await copySharedFiles(handle, workspacePath, {
|
|
514
|
+
...(input.task.ownerOrgId ? { ownerOrgId: input.task.ownerOrgId } : {}),
|
|
515
|
+
ownerUserId: input.task.ownerUserId,
|
|
516
|
+
...(input.task.sharedFiles ? { sharedFiles: input.task.sharedFiles } : {}),
|
|
517
|
+
}).catch((error: unknown) => {
|
|
518
|
+
console.warn(
|
|
519
|
+
`[machine] task ${request.taskId}: shared files copy failed: ${
|
|
520
|
+
error instanceof Error ? error.message : String(error)
|
|
521
|
+
}`,
|
|
522
|
+
);
|
|
523
|
+
return null;
|
|
524
|
+
});
|
|
525
|
+
if (sharedWarning) warnings.push(sharedWarning);
|
|
526
|
+
|
|
527
|
+
// Editor pane: code-server on the machine (same flags as uai-init's
|
|
528
|
+
// container launch), idempotent across provision re-runs and resume.
|
|
529
|
+
// Best-effort — a missing binary degrades the Editor tab, never the task.
|
|
530
|
+
await ensureCodeServer(
|
|
531
|
+
handle,
|
|
532
|
+
workspacePath,
|
|
533
|
+
input.task.previewEnv?.UAI_EDITOR_THEME,
|
|
534
|
+
).catch((error: unknown) => {
|
|
535
|
+
console.warn(
|
|
536
|
+
`[machine] task ${request.taskId}: code-server launch failed: ${
|
|
537
|
+
error instanceof Error ? error.message : String(error)
|
|
538
|
+
}`,
|
|
539
|
+
);
|
|
540
|
+
});
|
|
541
|
+
|
|
314
542
|
const machineId = parseMachineTaskEnvironmentLocator(
|
|
315
543
|
handle.descriptor.locator,
|
|
316
544
|
).machineId;
|
|
@@ -319,6 +547,7 @@ export function createMachineHostTaskEnvironmentProvider(
|
|
|
319
547
|
result: {
|
|
320
548
|
composeProject: machineId,
|
|
321
549
|
worktreePath: deps.taskControlDir(request.taskId),
|
|
550
|
+
codeServerPort: MACHINE_CODE_SERVER_PORT,
|
|
322
551
|
...(warnings.length > 0
|
|
323
552
|
? { initWarning: warnings.join("\n") }
|
|
324
553
|
: {}),
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
package/src/main.ts
CHANGED
|
@@ -252,6 +252,12 @@ const bridgeUrl =
|
|
|
252
252
|
let ws: WebSocket | null = null;
|
|
253
253
|
let stopping = false;
|
|
254
254
|
let fatal = false;
|
|
255
|
+
/** Auth-rejected (4001) retry cadence. A genuinely revoked credential costs
|
|
256
|
+
* one connection attempt per interval; a transient rejection (the bridge's
|
|
257
|
+
* own DB down during a cloud restart, an auth frame lost on a slow link)
|
|
258
|
+
* heals without an operator visit — the 2026-08-27 outage stranded every
|
|
259
|
+
* connected host overnight because 4001 was treated as terminal. */
|
|
260
|
+
const AUTH_REJECTED_RETRY_MS = 5 * 60_000;
|
|
255
261
|
let reconnectAttempt = 0;
|
|
256
262
|
let connectionRequestGeneration = 0;
|
|
257
263
|
let inFlight = 0;
|
|
@@ -1642,12 +1648,16 @@ async function connect(): Promise<void> {
|
|
|
1642
1648
|
return;
|
|
1643
1649
|
}
|
|
1644
1650
|
if (code === 4001) {
|
|
1645
|
-
fatal = true;
|
|
1646
1651
|
markDisconnected(text || "auth rejected by bridge");
|
|
1647
1652
|
console.error(
|
|
1648
|
-
`[host-agent] auth rejected by bridge${text ? `: ${text}` : ""}`,
|
|
1653
|
+
`[host-agent] auth rejected by bridge${text ? `: ${text}` : ""} — retrying in ${Math.round(AUTH_REJECTED_RETRY_MS / 60_000)} minutes`,
|
|
1649
1654
|
);
|
|
1650
|
-
|
|
1655
|
+
addHostBreadcrumb("bridge", "auth-rejected", {
|
|
1656
|
+
retryInMs: AUTH_REJECTED_RETRY_MS,
|
|
1657
|
+
});
|
|
1658
|
+
setTimeout(() => {
|
|
1659
|
+
void connect();
|
|
1660
|
+
}, AUTH_REJECTED_RETRY_MS);
|
|
1651
1661
|
return;
|
|
1652
1662
|
}
|
|
1653
1663
|
const delay = reconnectDelay();
|
|
@@ -1966,6 +1976,26 @@ async function resolveTunnelTarget(
|
|
|
1966
1976
|
// Persisted ports are meaningful only while the selected runtime is known
|
|
1967
1977
|
// operational. After a daemon restart an unrelated local process could bind
|
|
1968
1978
|
// a stale port before recovery refreshes the task's mappings.
|
|
1979
|
+
const machineTask = getHostTask(frame.taskId);
|
|
1980
|
+
if (machineTask?.environmentProvider === "machine") {
|
|
1981
|
+
// ADR-121: a machine's address is orchestrator-routable (bridge IP on
|
|
1982
|
+
// the Linux guinea pig, VPC-private on EC2) — editor and previews dial
|
|
1983
|
+
// it straight via the handle's ownership-proven route, independent of
|
|
1984
|
+
// the container runtime's health. macOS-local machines need a published
|
|
1985
|
+
// fallback (bridge IPs are not host-routable there) — recorded ADR-121
|
|
1986
|
+
// follow-up, not silently misrouted: the connect simply fails.
|
|
1987
|
+
const environment = await reconstructPersistedTaskEnvironment(machineTask);
|
|
1988
|
+
const route = environment?.inspectRoute
|
|
1989
|
+
? await environment.inspectRoute()
|
|
1990
|
+
: null;
|
|
1991
|
+
if (!route || route.kind !== "running") return null;
|
|
1992
|
+
const port =
|
|
1993
|
+
frame.target === "editor"
|
|
1994
|
+
? (machineTask.codeServerPort ?? null)
|
|
1995
|
+
: (frame.containerPort ?? null);
|
|
1996
|
+
if (!port) return null;
|
|
1997
|
+
return { host: route.ipv4Address, port };
|
|
1998
|
+
}
|
|
1969
1999
|
if (containerRuntimeProblem()) return null;
|
|
1970
2000
|
const task = getHostTask(frame.taskId);
|
|
1971
2001
|
if (!task) return null;
|