diff --git a/.lane/memory/apps/host/src/peers.ts/01M4YX4FKJR9W30M9Y5HVTKBW4-a-peer-s-hello-can-arrive-be.md b/.lane/attic/apps/host/src/peers.ts/01M4YX4FKJR9W30M9Y5HVTKBW4-a-peer-s-hello-can-arrive-be.md similarity index 100% rename from .lane/memory/apps/host/src/peers.ts/01M4YX4FKJR9W30M9Y5HVTKBW4-a-peer-s-hello-can-arrive-be.md rename to .lane/attic/apps/host/src/peers.ts/01M4YX4FKJR9W30M9Y5HVTKBW4-a-peer-s-hello-can-arrive-be.md diff --git a/.lane/memory/apps/host/src/gateway.ts/01M4YVZ6KGNQ7MFS1RP81MQCE0-a-hosted-channel-s-location.md b/.lane/memory/apps/host/src/gateway.ts/01M4YVZ6KGNQ7MFS1RP81MQCE0-a-hosted-channel-s-location.md index c715a588..9c0726ae 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M4YVZ6KGNQ7MFS1RP81MQCE0-a-hosted-channel-s-location.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M4YVZ6KGNQ7MFS1RP81MQCE0-a-hosted-channel-s-location.md @@ -4,9 +4,9 @@ anchor: function handle created: 2026-10-15T04:08:49Z norm: '1' sig: e597b5fbc8e664ce -body_hash: 3d892bdf2a5a0db7 -raw_hash: f681a895851ad4fb -vouched: 2026-10-22T16:18:52Z +body_hash: 41e3e91309de2927 +raw_hash: 79444a43e01e6586 +vouched: 2026-10-22T17:44:28Z lines: 266-519 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M4Z2BCKRNHVMYEMC0KH3Q6BN-a-hosted-channel-must-be-rea.md b/.lane/memory/apps/host/src/gateway.ts/01M4Z2BCKRNHVMYEMC0KH3Q6BN-a-hosted-channel-must-be-rea.md index 60b9a23b..c2e1003d 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M4Z2BCKRNHVMYEMC0KH3Q6BN-a-hosted-channel-must-be-rea.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M4Z2BCKRNHVMYEMC0KH3Q6BN-a-hosted-channel-must-be-rea.md @@ -4,9 +4,9 @@ anchor: function handle created: 2026-10-15T06:00:20Z norm: '1' sig: e597b5fbc8e664ce -body_hash: 3d892bdf2a5a0db7 -raw_hash: f681a895851ad4fb -vouched: 2026-10-22T16:18:52Z +body_hash: 41e3e91309de2927 +raw_hash: 79444a43e01e6586 +vouched: 2026-10-22T17:44:28Z lines: 281-561 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M5AZZV2CRMNFXZKM0B3AWRVX-a-move-to-a-cloud-workspace.md b/.lane/memory/apps/host/src/gateway.ts/01M5AZZV2CRMNFXZKM0B3AWRVX-a-move-to-a-cloud-workspace.md index 1e9bbb96..a122c3f2 100644 --- a/.lane/memory/apps/host/src/gateway.ts/01M5AZZV2CRMNFXZKM0B3AWRVX-a-move-to-a-cloud-workspace.md +++ b/.lane/memory/apps/host/src/gateway.ts/01M5AZZV2CRMNFXZKM0B3AWRVX-a-move-to-a-cloud-workspace.md @@ -4,9 +4,9 @@ anchor: function handle created: 2026-10-19T21:09:57Z norm: '1' sig: e597b5fbc8e664ce -body_hash: 3d892bdf2a5a0db7 -raw_hash: f681a895851ad4fb -vouched: 2026-10-22T16:18:52Z +body_hash: 41e3e91309de2927 +raw_hash: 79444a43e01e6586 +vouched: 2026-10-22T17:44:28Z lines: 384-700 supersedes: 01M5APK9YCQH467YNMKAC8BT5B --- 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 8527a79b..43e5af3e 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: 308268d80c20b68b -raw_hash: 417f8cf07e0e37ad -vouched: 2026-10-22T16:18:52Z +body_hash: 0b20e056c7f04b14 +raw_hash: 2bff560cd8f039e6 +vouched: 2026-10-22T17:44:28Z 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 ba8349f1..8f0f7a25 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: 308268d80c20b68b -raw_hash: 417f8cf07e0e37ad -vouched: 2026-10-22T16:18:52Z +body_hash: 0b20e056c7f04b14 +raw_hash: 2bff560cd8f039e6 +vouched: 2026-10-22T17:44:28Z lines: 1-1309 --- diff --git a/.lane/memory/apps/host/src/gateway.ts/01M5JBDJ3Q1B2CB27WSCJMMCP8-the-directory-s-hosts-github.md b/.lane/memory/apps/host/src/gateway.ts/01M5JBDJ3Q1B2CB27WSCJMMCP8-the-directory-s-hosts-github.md new file mode 100644 index 00000000..30ac6a3d --- /dev/null +++ b/.lane/memory/apps/host/src/gateway.ts/01M5JBDJ3Q1B2CB27WSCJMMCP8-the-directory-s-hosts-github.md @@ -0,0 +1,12 @@ +--- +id: 01M5JBDJ3Q1B2CB27WSCJMMCP8 +anchor: function people +created: 2026-10-22T17:44:22Z +norm: '1' +sig: d4fabfe66743b1ed +body_hash: d893a8d41c8d0aeb +raw_hash: a0226e2068ee94b3 +lines: 315-322 +--- + +the directory's hosts[].github is re-announced by each host every heartbeat, so it must override logins remembered from peers' hellos diff --git a/.lane/memory/apps/host/src/peers.ts/01M5JBDJ536ASQ9VKEE9T4WW7T-a-hello-without-github-leave.md b/.lane/memory/apps/host/src/peers.ts/01M5JBDJ536ASQ9VKEE9T4WW7T-a-hello-without-github-leave.md new file mode 100644 index 00000000..1ae1c2bc --- /dev/null +++ b/.lane/memory/apps/host/src/peers.ts/01M5JBDJ536ASQ9VKEE9T4WW7T-a-hello-without-github-leave.md @@ -0,0 +1,12 @@ +--- +id: 01M5JBDJ536ASQ9VKEE9T4WW7T +anchor: function greet +created: 2026-10-22T17:44:22Z +norm: '1' +sig: 583ffcbd3b791ed2 +body_hash: f003a7d2404a7483 +raw_hash: 18edf8ce741d1310 +lines: 76-86 +--- + +a hello without github leaves the peer in unnamed, and discover re-asks it even with a directory chosen, until a login arrives diff --git a/.lane/memory/apps/host/src/peers.ts/01M5JBDT9ZCB6X9ANFR2K7NJBE-a-peer-s-hello-can-arrive-be.md b/.lane/memory/apps/host/src/peers.ts/01M5JBDT9ZCB6X9ANFR2K7NJBE-a-peer-s-hello-can-arrive-be.md new file mode 100644 index 00000000..cc008feb --- /dev/null +++ b/.lane/memory/apps/host/src/peers.ts/01M5JBDT9ZCB6X9ANFR2K7NJBE-a-peer-s-hello-can-arrive-be.md @@ -0,0 +1,13 @@ +--- +id: 01M5JBDT9ZCB6X9ANFR2K7NJBE +anchor: function discover +created: 2026-10-22T17:44:31Z +norm: '1' +sig: ab69f97386d0db50 +body_hash: 687af47f626b9aa1 +raw_hash: ce225aafa3f68775 +lines: 37-74 +supersedes: 01M4YX4FKJR9W30M9Y5HVTKBW4 +--- + +a peer's hello can arrive before its own gh lookup resolves and carry no github; peers never push a later login, so discover re-asks such peers every pass, even with a directory chosen diff --git a/apps/host/src/gateway.ts b/apps/host/src/gateway.ts index 94876138..5266e7b0 100644 --- a/apps/host/src/gateway.ts +++ b/apps/host/src/gateway.ts @@ -308,11 +308,13 @@ async function owners(own?: Listing[]): Promise { await lobbyProjects(own); } -/** Fresher sources win: the directory, then reachable peers, then this host's own owner. */ +/** + * Fresher sources win: peers' hellos, then the directory, which each host re-announces itself to + * on every heartbeat while a hello is asked only on connect, then this host's own owner. + */ function people(): People { - const known: People = {}; + const known: People = { ...peers.people() }; for (const host of directory.read().hosts) if (host.github) known[host.login] = host.github; - Object.assign(known, peers.people()); Object.assign(known, demo.people()); const own = github.login(loginChanged); if (own) known[catalog.user] = own; @@ -721,23 +723,18 @@ async function handle( case "team-peers": return peers.teams(); case "team-roster": { - const gh = github.login(loginChanged); const roster: Member[] = [ - { - host: name, - login: catalog.user, - ...(gh ? { github: gh } : {}), - online: true, - self: true, - }, + { host: name, login: catalog.user, online: true, self: true }, ...peers.online(), ]; const seen = new Set(roster.map((member) => member.host)); - for (const { name: host, login, github: owner } of directory.read().hosts) { - if (seen.has(host)) continue; - roster.push({ host, login, ...(owner ? { github: owner } : {}), online: false }); + for (const { name: host, login } of directory.read().hosts) { + if (!seen.has(host)) roster.push({ host, login, online: false }); } - return roster; + const known = people(); + return roster.map((member) => + known[member.login] ? { ...member, github: known[member.login] } : member + ); } case "team-ask": { const team = await peers.ask(request.host); diff --git a/apps/host/src/peers.ts b/apps/host/src/peers.ts index be2d0828..5a80fe4d 100644 --- a/apps/host/src/peers.ts +++ b/apps/host/src/peers.ts @@ -27,6 +27,8 @@ const remembered = new Map(); const stalled = new Map(); /** Peer owners' GitHub logins by Tailscale login, from their last hello, so remembered channels keep their owner's name and avatar. */ const named: People = {}; +/** Peers whose last hello carried no GitHub login, asked again until one does. */ +const unnamed = new Set(); function emit() { for (const listener of listeners) listener(); @@ -63,18 +65,21 @@ async function discover(signal: AbortSignal) { }); hosts.set(name, host); } - // A teammate can start a directory after this host first greeted them. - if (directory.isChosen()) return; - for (const { client, machine } of hosts.values()) { - if (client.status === "open") greet(client, machine); + // A teammate can start a directory after this host first greeted them, and a peer that answered + // before `gh` resolved has a login to give. + const chosen = directory.isChosen(); + for (const [name, { client, machine }] of hosts) { + if (client.status === "open" && (!chosen || unnamed.has(name))) greet(client, machine); } } function greet(client: GatewayClient, machine: Machine) { client.request({ op: "hello" }).then((hello) => { if (hello.directory) void directory.adopt(hello.directory); - // A freshly started peer answers before `gh` has told it its owner's login. - if (!hello.github || named[machine.login] === hello.github) return; + // A freshly started peer answers before `gh` has told it its owner's login, which may have changed. + if (!hello.github) return void unnamed.add(machine.name); + unnamed.delete(machine.name); + if (named[machine.login] === hello.github) return; named[machine.login] = hello.github; emit(); }, () => {}); @@ -167,18 +172,11 @@ export async function teams(): Promise { return found.filter((peer) => !!peer); } -/** Hosts running Ace on the tailnet now. */ +/** Hosts running Ace on the tailnet now; the gateway names their owners from every source. */ export function online(): Member[] { - return [...hosts.values()].flatMap(({ machine, client }) => { - if (client.status !== "open") return []; - const github = named[machine.login]; - return [{ - host: machine.name, - login: machine.login, - ...(github ? { github } : {}), - online: true, - }]; - }); + return [...hosts.values()].flatMap(({ machine, client }) => + client.status === "open" ? [{ host: machine.name, login: machine.login, online: true }] : [] + ); } /** Waits for the peer's owner to approve; see asks.ts. */