diff --git a/.lane/memory/apps/host/src/catalog.ts/01M52ZT4QFXW6G0AFQFZNG86B1-channel-names-stay-unique-ac.md b/.lane/memory/apps/host/src/catalog.ts/01M52ZT4QFXW6G0AFQFZNG86B1-channel-names-stay-unique-ac.md index 69672aff..c85ab2d7 100644 --- a/.lane/memory/apps/host/src/catalog.ts/01M52ZT4QFXW6G0AFQFZNG86B1-channel-names-stay-unique-ac.md +++ b/.lane/memory/apps/host/src/catalog.ts/01M52ZT4QFXW6G0AFQFZNG86B1-channel-names-stay-unique-ac.md @@ -4,9 +4,9 @@ anchor: function draft created: 2026-10-16T18:32:55Z norm: '1' sig: 9775c8890db42443 -body_hash: 715d3c6a84062b19 -raw_hash: 4ac416bc7ff419f2 -vouched: 2026-10-21T21:54:51Z +body_hash: 6918a46a3994c088 +raw_hash: 8984d6500c60a112 +vouched: 2026-10-22T21:42:37Z lines: 133-155 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M4YT9Z8YJ2ZWZCVB752WDCFF-workers-and-agent-shells-fin.md b/.lane/memory/apps/host/src/gateway.ts/01M4YT9Z8YJ2ZWZCVB752WDCFF-workers-and-agent-shells-fin.md index 1a64f1b2..9c27ae90 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M4YT9Z8YJ2ZWZCVB752WDCFF-workers-and-agent-shells-fin.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M4YT9Z8YJ2ZWZCVB752WDCFF-workers-and-agent-shells-fin.md @@ -4,9 +4,9 @@ anchor: function serve created: 2026-10-15T03:39:45Z norm: '1' sig: 171797818ef4d836 -body_hash: 0f15dc30991bf467 -raw_hash: aa609814e48a6d89 -vouched: 2026-10-22T21:10:47Z +body_hash: f0a0ef2c205726cc +raw_hash: a98db292f7a9e13d +vouched: 2026-10-22T21:42:37Z lines: 668-751 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M4ZH1NMBK54TCB970R3AJ1X1-a-loopback-host-must-not-cal.md b/.lane/memory/apps/host/src/gateway.ts/01M4ZH1NMBK54TCB970R3AJ1X1-a-loopback-host-must-not-cal.md index 13b94120..4d43e9c9 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M4ZH1NMBK54TCB970R3AJ1X1-a-loopback-host-must-not-cal.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M4ZH1NMBK54TCB970R3AJ1X1-a-loopback-host-must-not-cal.md @@ -4,9 +4,9 @@ anchor: function serve created: 2026-10-15T10:17:10Z norm: '1' sig: 171797818ef4d836 -body_hash: 0f15dc30991bf467 -raw_hash: aa609814e48a6d89 -vouched: 2026-10-22T21:10:47Z +body_hash: f0a0ef2c205726cc +raw_hash: a98db292f7a9e13d +vouched: 2026-10-22T21:42:37Z lines: 830-948 supersedes: 01M4Z2R1HEVDKEA8VEJ7XRJQ8F --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M5D4BXMERGZ1EZ3EYAFM3YC4-worker-requests-worker-never.md b/.lane/memory/apps/host/src/gateway.ts/01M5D4BXMERGZ1EZ3EYAFM3YC4-worker-requests-worker-never.md index 87799eda..747a13e8 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M5D4BXMERGZ1EZ3EYAFM3YC4-worker-requests-worker-never.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M5D4BXMERGZ1EZ3EYAFM3YC4-worker-requests-worker-never.md @@ -4,9 +4,9 @@ anchor: '@file' created: 2026-10-20T17:04:56Z norm: '1' sig: 1acdc0f6a0895ac5 -body_hash: 734539b04f3c9fb5 -raw_hash: faceff1158ed3942 -vouched: 2026-10-22T21:30:21Z +body_hash: 94af0091ce8ea05b +raw_hash: 43d4d53a6b1fecd9 +vouched: 2026-10-22T21:42:37Z lines: 1-1153 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M5HYM54QCSF4ESXJSWZK588G-every-project-op-that-change.md b/.lane/memory/apps/host/src/gateway.ts/01M5HYM54QCSF4ESXJSWZK588G-every-project-op-that-change.md index cdf92fe1..d4f13cd9 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M5HYM54QCSF4ESXJSWZK588G-every-project-op-that-change.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M5HYM54QCSF4ESXJSWZK588G-every-project-op-that-change.md @@ -4,9 +4,9 @@ anchor: '@file' created: 2026-10-22T14:00:47Z norm: '1' sig: 1acdc0f6a0895ac5 -body_hash: 734539b04f3c9fb5 -raw_hash: faceff1158ed3942 -vouched: 2026-10-22T21:30:21Z +body_hash: 94af0091ce8ea05b +raw_hash: 43d4d53a6b1fecd9 +vouched: 2026-10-22T21:42:37Z lines: 1-1309 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M5JQ79TQ8E8R693FQD7PF3RP-only-a-command-line-host-pri.md b/.lane/memory/apps/host/src/gateway.ts/01M5JQ79TQ8E8R693FQD7PF3RP-only-a-command-line-host-pri.md index c6250da2..4d238023 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M5JQ79TQ8E8R693FQD7PF3RP-only-a-command-line-host-pri.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M5JQ79TQ8E8R693FQD7PF3RP-only-a-command-line-host-pri.md @@ -4,8 +4,9 @@ anchor: function serve created: 2026-10-22T21:10:40Z norm: '1' sig: 171797818ef4d836 -body_hash: 0f15dc30991bf467 -raw_hash: aa609814e48a6d89 +body_hash: f0a0ef2c205726cc +raw_hash: a98db292f7a9e13d +vouched: 2026-10-22T21:42:37Z lines: 1191-1341 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M5JS1HEWX8RK77BQVSA9XQZG-ace-helper-s-tailnet-self-re.md b/.lane/memory/apps/host/src/gateway.ts/01M5JS1HEWX8RK77BQVSA9XQZG-ace-helper-s-tailnet-self-re.md new file mode 100644 index 00000000..838821cb --- /dev/null +++ b/.lane/memory/apps/host/src/gateway.ts/01M5JS1HEWX8RK77BQVSA9XQZG-ace-helper-s-tailnet-self-re.md @@ -0,0 +1,12 @@ +--- +id: 01M5JS1HEWX8RK77BQVSA9XQZG +anchor: function serve +created: 2026-10-22T21:42:29Z +norm: '1' +sig: 171797818ef4d836 +body_hash: f0a0ef2c205726cc +raw_hash: a98db292f7a9e13d +lines: 1200-1357 +--- + +Ace Helper's tailnet self-restart exits non-zero because launchd's KeepAlive relaunches only a failed exit diff --git a/.lane/memory/apps/host/src/tailnet.ts/01M5JS1HGCGP3SEXKET1EFDJ87-tailscale-stopping-must-not.md b/.lane/memory/apps/host/src/tailnet.ts/01M5JS1HGCGP3SEXKET1EFDJ87-tailscale-stopping-must-not.md new file mode 100644 index 00000000..9fb6eefb --- /dev/null +++ b/.lane/memory/apps/host/src/tailnet.ts/01M5JS1HGCGP3SEXKET1EFDJ87-tailscale-stopping-must-not.md @@ -0,0 +1,12 @@ +--- +id: 01M5JS1HGCGP3SEXKET1EFDJ87 +anchor: function watch +created: 2026-10-22T21:42:29Z +norm: '1' +sig: 6c1e8049ae309bc1 +body_hash: 8530c8fc8dec8e95 +raw_hash: 03c20bc7ed33888a +lines: 113-125 +--- + +Tailscale stopping must not restart the host; only a running tailnet other than the one the host started on does diff --git a/apps/host/src/catalog.ts b/apps/host/src/catalog.ts index c3b32b2f..443a3e51 100644 --- a/apps/host/src/catalog.ts +++ b/apps/host/src/catalog.ts @@ -199,12 +199,13 @@ export function fork(source: Listing, model: ModelRef | undefined, branches: Set /** A new channel's record, not yet saved: a hosted channel saves it once the service has it. */ export function draft(project: string, model?: ModelRef, name?: string, hosted?: string): Listing { if (!tailnet) { - // The tailnet is read once at startup, so Tailscale started later needs a restart. - const host = config.helper ? "Ace Helper" : "ace serve"; + // The tailnet is read once at startup; Ace Helper restarts itself when Tailscale starts later. throw new Error( - identity().tailnet - ? `Restart ${host}: it started before Tailscale was running.` - : "Start Tailscale first: every channel belongs to a tailnet.", + !identity().tailnet + ? "Start Tailscale first: every channel belongs to a tailnet." + : config.helper + ? "Ace Helper is restarting to use Tailscale. Try again in a moment." + : "Restart ace serve: it started before Tailscale was running.", ); } const names = taken(); diff --git a/apps/host/src/gateway.ts b/apps/host/src/gateway.ts index 81dbcba6..3a920493 100644 --- a/apps/host/src/gateway.ts +++ b/apps/host/src/gateway.ts @@ -51,7 +51,7 @@ import { projectId, type WorkerRequest, } from "./protocol"; -import { type Machine, self, whois } from "./tailnet"; +import { type Machine, self, watch as tailscale, whois } from "./tailnet"; import { checkKey, forgetKey, @@ -1231,7 +1231,7 @@ export async function serve(port: number, loopback = false, script?: string): Pr tick = now; }, 1000); cleanup.push(() => clearInterval(stalls)); - async function stop() { + async function stop(code = 0) { if (closing) return; closing = true; log("info", "host.stop", { pending: pending.size }); @@ -1244,10 +1244,17 @@ export async function serve(port: number, loopback = false, script?: string): Pr log("error", "host.shutdown.failed", failure(result.reason)); } } - process.exit(results.some((result) => result.status === "rejected") ? 1 : 0); + process.exit(results.some((result) => result.status === "rejected") ? 1 : code); } process.on("SIGTERM", () => void stop()); process.on("SIGINT", () => void stop()); + // The owner's login, the channels' tailnet, and every tailnet listener are read once at startup. + cleanup.push(tailscale(catalog.tailnet, (tailnet) => { + log("info", "host.tailnet.moved", { from: catalog.tailnet, to: tailnet }); + // launchd starts Ace Helper again after a failed exit, which picks up the new tailnet. + if (config.helper) void stop(75); + else console.log(`Tailscale is on ${tailnet} now. Restart ace serve to use it.`); + })); console.log(`Ace on http://${server.hostname}:${server.port}`); // Ace Helper's console is its log file, which must not hold the token. if (!config.helper) { diff --git a/apps/host/src/tailnet.ts b/apps/host/src/tailnet.ts index 57bf5670..ab80b581 100644 --- a/apps/host/src/tailnet.ts +++ b/apps/host/src/tailnet.ts @@ -106,6 +106,24 @@ export async function diagnostics(started?: string): Promise void): () => void { + let seen: string | undefined; + const timer = setInterval(async () => { + const now = (await status())?.MagicDNSSuffix; + if (!now || now === started) seen = undefined; + else if (now !== seen) seen = now; + else { + clearInterval(timer); + moved(now); + } + }, 10_000); + return () => clearInterval(timer); +} + /** Online machines owned by people; tagged nodes are servers, not participants. */ export async function peers(): Promise { const value = await status();