From 17139004aede6f1fd4ce4bc8d4aad7687bf33abd Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Fri, 23 Oct 2026 11:02:06 -0400 Subject: [PATCH 1/3] fix(host): leave the directory on its server, so the serving Mac sees a teammate leave and rejoin Why: apps/host/src/directory.ts#function configure | leaving or changing directories DELETEs this host from the old one, after any publish under way to it, or the serving Mac keeps the stale row and a rejoin never counts as a join Why: apps/host/src/directory-server.ts#function handle | a host leaves only as its whois machine name, and leaving calls members so the serving host re-reads its own directory now --- apps/app/src/directory-settings.tsx | 22 ++++++++++++++++++---- apps/host/src/directory-server.ts | 20 +++++++++++++++++--- apps/host/src/directory.ts | 28 +++++++++++++++++++++++++--- apps/host/src/gateway.ts | 8 ++++---- services/directory/src/index.ts | 12 ++++++++++++ 5 files changed, 76 insertions(+), 14 deletions(-) diff --git a/apps/app/src/directory-settings.tsx b/apps/app/src/directory-settings.tsx index 88412612..846c4d7a 100644 --- a/apps/app/src/directory-settings.tsx +++ b/apps/app/src/directory-settings.tsx @@ -199,8 +199,17 @@ function shared(shares: Share[]): string { return [...projects].map(([name, people]) => `${name} with ${people.join(", ")}`).join("; "); } -/** `row` keeps each result beside the control that caused it. */ -type Action = { row?: "directory" | "secret"; busy?: string; error?: string; message?: string }; +/** + * `row` keeps each result beside the control that caused it. `joined` is whether `message` says + * this host is in a directory, so a join or leave made elsewhere, as with `ace directory`, hides it. + */ +type Action = { + row?: "directory" | "secret"; + busy?: string; + error?: string; + message?: string; + joined?: boolean; +}; const link = "rounded-sm underline outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/30"; @@ -222,6 +231,7 @@ function TeamDirectory( busy: string, message: string, request: () => Promise, + joined?: boolean, ) { setAction({ row, busy }); try { @@ -229,7 +239,7 @@ function TeamDirectory( setEditing(false); setSecret(""); setConfirm(undefined); - setAction({ row, message }); + setAction({ row, message, joined }); } catch (error) { setAction({ row, error: (error as Error).message }); } @@ -242,7 +252,9 @@ function TeamDirectory( <> {action.busy && {action.busy}} {action.error && {action.error}} - {action.message && {action.message}} + {action.message && (action.joined === undefined || action.joined === !!directory.url) && ( + {action.message} + )} ); } @@ -291,6 +303,7 @@ function TeamDirectory( url, ...(secret.trim() ? { secret } : {}), }), + true, ); }} onKeyDown={(event) => { @@ -371,6 +384,7 @@ function TeamDirectory( "Leaving the directory…", "Left the directory.", () => host.request({ op: "directory-set", url: null }), + false, )} onCancel={() => setConfirm(undefined)} > diff --git a/apps/host/src/directory-server.ts b/apps/host/src/directory-server.ts index 275fb368..1c3df5b2 100644 --- a/apps/host/src/directory-server.ts +++ b/apps/host/src/directory-server.ts @@ -102,12 +102,15 @@ export function shares(): Share[] { return open().query('SELECT project, person, shared_by AS "by", at FROM shares').all(); } -/** Requests under `/directory`, with `path` the rest; `joined` runs when a host publishes for the first time. */ +/** + * Requests under `/directory`, with `path` the rest; `members` runs when a host publishes for the + * first time or leaves, so this host's own views follow at once. + */ export async function handle( path: string, request: Request, caller: Caller, - joined: () => void, + members: () => void, ): Promise { if (!isServing()) return new Response("This host serves no directory", { status: 404 }); const sql = open(); @@ -121,7 +124,18 @@ export async function handle( } const known = sql.query("SELECT 1 FROM hosts WHERE name = ?").get(host); publish(sql, host, publication); - if (!known) joined(); + if (!known) members(); + return new Response(null, { status: 204 }); + } + if (host && request.method === "DELETE") { + if (host !== caller.host) { + return new Response("Hosts leave only as themselves", { status: 403 }); + } + sql.transaction(() => { + sql.run("DELETE FROM hosts WHERE name = ?", [host]); + sql.run("DELETE FROM channels WHERE host = ?", [host]); + })(); + members(); return new Response(null, { status: 204 }); } if (path === "/shares" && request.method === "POST") { diff --git a/apps/host/src/directory.ts b/apps/host/src/directory.ts index cd498c61..71149a2c 100644 --- a/apps/host/src/directory.ts +++ b/apps/host/src/directory.ts @@ -61,7 +61,13 @@ let error: string | undefined; let failed: number | undefined; /** The running host's watch, so a change made through it takes effect without waiting for a heartbeat. */ let active: - | { sync(): Promise; again(): Promise; changed(): void; chose(): void } + | { + sync(): Promise; + again(): Promise; + changed(): void; + chose(): void; + leave(base: string): Promise; + } | undefined; /** A directory base URL without a trailing slash. */ @@ -83,7 +89,11 @@ export function canonical(value: string): string { * an empty file, so a host that left never joins a peer's directory on its own again. */ export function configure(value: string | undefined): Promise | undefined { - writeFileSync(config, JSON.stringify(value ? { url: canonical(value) } : {})); + const before = url(); + const after = value && canonical(value); + writeFileSync(config, JSON.stringify(after ? { url: after } : {})); + // Teammates' views, including the serving Mac's, drop this host only when it says it left. + if (before && before !== after) void active?.leave(before); known = { hosts: [], channels: [], shares: [] }; synced = undefined; error = undefined; @@ -339,7 +349,19 @@ export function watch({ self, channels, changed, chose, published, covered }: Op if (!running || running.base !== url()) return join(); return running.again ??= running.done.then(join); }; - active = { sync: join, again: sync, changed, chose }; + const leave = async (base: string) => { + // A publish under way would list this host again after it left. + if (running?.base === base) await running.done; + try { + await call(base, `/hosts/${encodeURIComponent(self().name)}`, controller.signal, { + method: "DELETE", + }); + log("info", "directory.left", { url: base }); + } catch (cause) { + log("warn", "directory.leave.failed", { url: base, ...failure(cause) }); + } + }; + active = { sync: join, again: sync, changed, chose, leave }; void join(); const interval = setInterval(() => { // A failed publish keeps retrying every heartbeat; lobbies only carry what was published. diff --git a/apps/host/src/gateway.ts b/apps/host/src/gateway.ts index 91cdebbd..d7dcfbd0 100644 --- a/apps/host/src/gateway.ts +++ b/apps/host/src/gateway.ts @@ -411,10 +411,10 @@ function asksChanged(): void { } /** - * A host joined the directory this one serves. This host reads it only on its heartbeat, so it - * reads it now for its apps' People and In the directory. + * A host joined or left the directory this one serves. This host reads it only on its heartbeat, + * so it reads it now for its apps' People and In the directory. */ -function directoryJoined(): void { +function directoryMembers(): void { void directory.refresh().then(settingsChanged); } @@ -1153,7 +1153,7 @@ async function directoryRequest( return new Response("Forbidden", { status: 403 }); } const project = served.lobbyOf(path); - if (project === undefined) return served.handle(path, request, caller, directoryJoined); + if (project === undefined) return served.handle(path, request, caller, directoryMembers); if (!served.isServing()) return new Response("This host serves no directory", { status: 404 }); const data = { ...client(caller.login, true, false, caller.host), diff --git a/services/directory/src/index.ts b/services/directory/src/index.ts index b3ad4379..81137b08 100644 --- a/services/directory/src/index.ts +++ b/services/directory/src/index.ts @@ -98,6 +98,14 @@ export class Directory extends DurableObject { }); } + leave(name: string): void { + const sql = this.ctx.storage.sql; + this.ctx.storage.transactionSync(() => { + sql.exec("DELETE FROM hosts WHERE name = ?", name); + sql.exec("DELETE FROM channels WHERE host = ?", name); + }); + } + read(): { hosts: Host[]; channels: unknown[]; shares: Share[] } { const sql = this.ctx.storage.sql; return { @@ -173,6 +181,10 @@ export default { await directory.publish(host[1], publication); return new Response(null, { status: 204 }); } + if (host && request.method === "DELETE") { + await directory.leave(host[1]); + return new Response(null, { status: 204 }); + } if (pathname === "/shares" && request.method === "POST") { const { shared, ...share } = await request.json(); if ( From 81477225b6cc0c5f3d0a78d06eb066a7c13b6ce2 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Fri, 23 Oct 2026 11:02:31 -0400 Subject: [PATCH 2/3] chore: update lane notes for leaving the directory --- ...H04X7KJRTPMZRD3355B0H-a-host-s-first-publish-to-th.md | 0 ...MH0DD8WZ8X1APBFA87YYPN-a-host-publishes-only-under.md | 0 ...MH86TBN5X4NFTC9KCNEF9-a-host-leaves-only-as-its-wh.md | 8 ++++++++ ...MMHKVTATGPKXKCYAQ78HPE-a-host-publishes-and-leaves.md | 9 +++++++++ ...VZAF65FJNKV1XBYTBZAFX-a-directory-change-resets-th.md | 6 +++--- ...WKAGD3FTMWYWE1KVXT9Q1-the-poll-may-slow-only-while.md | 6 +++--- ...BG92EP0H76ECNZM2E28MS-every-join-or-leave-tells-th.md | 5 +++-- ...MH7A97XGVD8D76KQ8T8PR-leaving-or-changing-director.md | 8 ++++++++ ...4BXMERGZ1EZ3EYAFM3YC4-worker-requests-worker-never.md | 6 +++--- ...YM54QCSF4ESXJSWZK588G-every-project-op-that-change.md | 6 +++--- ...3ZHB0N4SMGC71CRBVSFPS-hosts-publish-every-heartbea.md | 6 +++--- 11 files changed, 43 insertions(+), 17 deletions(-) rename .lane/{memory => attic}/apps/host/src/directory-server.ts/01M5MH04X7KJRTPMZRD3355B0H-a-host-s-first-publish-to-th.md (100%) rename .lane/{memory => attic}/apps/host/src/directory-server.ts/01M5MH0DD8WZ8X1APBFA87YYPN-a-host-publishes-only-under.md (100%) create mode 100644 .lane/memory/apps/host/src/directory-server.ts/01M5MMH86TBN5X4NFTC9KCNEF9-a-host-leaves-only-as-its-wh.md create mode 100644 .lane/memory/apps/host/src/directory-server.ts/01M5MMHKVTATGPKXKCYAQ78HPE-a-host-publishes-and-leaves.md create mode 100644 .lane/memory/apps/host/src/directory.ts/01M5MMH7A97XGVD8D76KQ8T8PR-leaving-or-changing-director.md diff --git a/.lane/memory/apps/host/src/directory-server.ts/01M5MH04X7KJRTPMZRD3355B0H-a-host-s-first-publish-to-th.md b/.lane/attic/apps/host/src/directory-server.ts/01M5MH04X7KJRTPMZRD3355B0H-a-host-s-first-publish-to-th.md similarity index 100% rename from .lane/memory/apps/host/src/directory-server.ts/01M5MH04X7KJRTPMZRD3355B0H-a-host-s-first-publish-to-th.md rename to .lane/attic/apps/host/src/directory-server.ts/01M5MH04X7KJRTPMZRD3355B0H-a-host-s-first-publish-to-th.md diff --git a/.lane/memory/apps/host/src/directory-server.ts/01M5MH0DD8WZ8X1APBFA87YYPN-a-host-publishes-only-under.md b/.lane/attic/apps/host/src/directory-server.ts/01M5MH0DD8WZ8X1APBFA87YYPN-a-host-publishes-only-under.md similarity index 100% rename from .lane/memory/apps/host/src/directory-server.ts/01M5MH0DD8WZ8X1APBFA87YYPN-a-host-publishes-only-under.md rename to .lane/attic/apps/host/src/directory-server.ts/01M5MH0DD8WZ8X1APBFA87YYPN-a-host-publishes-only-under.md diff --git a/.lane/memory/apps/host/src/directory-server.ts/01M5MMH86TBN5X4NFTC9KCNEF9-a-host-leaves-only-as-its-wh.md b/.lane/memory/apps/host/src/directory-server.ts/01M5MMH86TBN5X4NFTC9KCNEF9-a-host-leaves-only-as-its-wh.md new file mode 100644 index 00000000..9dd88cad --- /dev/null +++ b/.lane/memory/apps/host/src/directory-server.ts/01M5MMH86TBN5X4NFTC9KCNEF9-a-host-leaves-only-as-its-wh.md @@ -0,0 +1,8 @@ +--- +id: 01M5MMH86TBN5X4NFTC9KCNEF9 +anchor: function handle +created: 2026-10-23T15:02:08Z +norm: '1' +--- + +a host leaves only as its whois machine name, and leaving calls members so the serving host re-reads its own directory now diff --git a/.lane/memory/apps/host/src/directory-server.ts/01M5MMHKVTATGPKXKCYAQ78HPE-a-host-publishes-and-leaves.md b/.lane/memory/apps/host/src/directory-server.ts/01M5MMHKVTATGPKXKCYAQ78HPE-a-host-publishes-and-leaves.md new file mode 100644 index 00000000..bd6b732a --- /dev/null +++ b/.lane/memory/apps/host/src/directory-server.ts/01M5MMHKVTATGPKXKCYAQ78HPE-a-host-publishes-and-leaves.md @@ -0,0 +1,9 @@ +--- +id: 01M5MMHKVTATGPKXKCYAQ78HPE +anchor: function handle +created: 2026-10-23T15:02:21Z +norm: '1' +supersedes: 01M5MH0DD8WZ8X1APBFA87YYPN +--- + +a host publishes and leaves only under its whois machine name (publishes also under its login); shares only as the caller's login; a host's first publish and its leave call members so the serving host re-reads its directory now diff --git a/.lane/memory/apps/host/src/directory.ts/01M4YVZAF65FJNKV1XBYTBZAFX-a-directory-change-resets-th.md b/.lane/memory/apps/host/src/directory.ts/01M4YVZAF65FJNKV1XBYTBZAFX-a-directory-change-resets-th.md index 702e25d3..c1a8aa79 100644 --- a/.lane/memory/apps/host/src/directory.ts/01M4YVZAF65FJNKV1XBYTBZAFX-a-directory-change-resets-th.md +++ b/.lane/memory/apps/host/src/directory.ts/01M4YVZAF65FJNKV1XBYTBZAFX-a-directory-change-resets-th.md @@ -4,9 +4,9 @@ anchor: function configure created: 2026-10-15T04:08:53Z norm: '1' sig: f3e1bf286b59976d -body_hash: 8ac9e99449a990ef -raw_hash: 5b6fd56a9863703b -vouched: 2026-10-23T12:24:44Z +body_hash: 929475baa9c1eeb0 +raw_hash: 9a3e5ddfb28d0a2a +vouched: 2026-10-23T15:02:22Z lines: 46-54 --- diff --git a/.lane/memory/apps/host/src/directory.ts/01M4YWKAGD3FTMWYWE1KVXT9Q1-the-poll-may-slow-only-while.md b/.lane/memory/apps/host/src/directory.ts/01M4YWKAGD3FTMWYWE1KVXT9Q1-the-poll-may-slow-only-while.md index c611e3aa..b0c7ef9f 100644 --- a/.lane/memory/apps/host/src/directory.ts/01M4YWKAGD3FTMWYWE1KVXT9Q1-the-poll-may-slow-only-while.md +++ b/.lane/memory/apps/host/src/directory.ts/01M4YWKAGD3FTMWYWE1KVXT9Q1-the-poll-may-slow-only-while.md @@ -4,9 +4,9 @@ anchor: function watch created: 2026-10-15T04:19:48Z norm: '1' sig: 576e815f30f4d9cc -body_hash: c1335e94c837e607 -raw_hash: a182e4b2f0856f99 -vouched: 2026-10-23T12:24:51Z +body_hash: cae48791b9d477fd +raw_hash: fb3f73b6c2c2e94b +vouched: 2026-10-23T15:02:22Z lines: 89-149 --- diff --git a/.lane/memory/apps/host/src/directory.ts/01M5MBG92EP0H76ECNZM2E28MS-every-join-or-leave-tells-th.md b/.lane/memory/apps/host/src/directory.ts/01M5MBG92EP0H76ECNZM2E28MS-every-join-or-leave-tells-th.md index 7bf4ae3e..9550e097 100644 --- a/.lane/memory/apps/host/src/directory.ts/01M5MBG92EP0H76ECNZM2E28MS-every-join-or-leave-tells-th.md +++ b/.lane/memory/apps/host/src/directory.ts/01M5MBG92EP0H76ECNZM2E28MS-every-join-or-leave-tells-th.md @@ -4,8 +4,9 @@ anchor: function configure created: 2026-10-23T12:24:20Z norm: '1' sig: f3e1bf286b59976d -body_hash: 8ac9e99449a990ef -raw_hash: 5b6fd56a9863703b +body_hash: 929475baa9c1eeb0 +raw_hash: 9a3e5ddfb28d0a2a +vouched: 2026-10-23T15:02:23Z lines: 85-94 --- diff --git a/.lane/memory/apps/host/src/directory.ts/01M5MMH7A97XGVD8D76KQ8T8PR-leaving-or-changing-director.md b/.lane/memory/apps/host/src/directory.ts/01M5MMH7A97XGVD8D76KQ8T8PR-leaving-or-changing-director.md new file mode 100644 index 00000000..78c5bedc --- /dev/null +++ b/.lane/memory/apps/host/src/directory.ts/01M5MMH7A97XGVD8D76KQ8T8PR-leaving-or-changing-director.md @@ -0,0 +1,8 @@ +--- +id: 01M5MMH7A97XGVD8D76KQ8T8PR +anchor: function configure +created: 2026-10-23T15:02:06Z +norm: '1' +--- + +leaving or changing directories DELETEs this host from the old one, after any publish under way to it, or the serving Mac keeps the stale row and a rejoin never counts as a join 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 cd6e018d..680c1126 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: c240b7c2f39b2623 -raw_hash: 3915b3b7c7b5bb0f -vouched: 2026-10-23T14:00:32Z +body_hash: fe20eb5454fb7a15 +raw_hash: f105febd5bb519a1 +vouched: 2026-10-23T15:02:23Z 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 9b49d88f..9f901bc4 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: c240b7c2f39b2623 -raw_hash: 3915b3b7c7b5bb0f -vouched: 2026-10-23T14:00:32Z +body_hash: fe20eb5454fb7a15 +raw_hash: f105febd5bb519a1 +vouched: 2026-10-23T15:02:24Z lines: 1-1309 --- diff --git a/.lane/memory/services/directory/src/index.ts/01M4Z3ZHB0N4SMGC71CRBVSFPS-hosts-publish-every-heartbea.md b/.lane/memory/services/directory/src/index.ts/01M4Z3ZHB0N4SMGC71CRBVSFPS-hosts-publish-every-heartbea.md index 12b4975f..55b330d4 100644 --- a/.lane/memory/services/directory/src/index.ts/01M4Z3ZHB0N4SMGC71CRBVSFPS-hosts-publish-every-heartbea.md +++ b/.lane/memory/services/directory/src/index.ts/01M4Z3ZHB0N4SMGC71CRBVSFPS-hosts-publish-every-heartbea.md @@ -4,9 +4,9 @@ anchor: class Directory created: 2026-10-15T06:28:49Z norm: '1' sig: 6dd0ab7d7a4fcaa0 -body_hash: 35f5333e78ef4ab2 -raw_hash: 48cbc89e6e566ec8 -vouched: 2026-10-19T20:58:11Z +body_hash: 6322e74c70344729 +raw_hash: 6c5edbcdbf70a074 +vouched: 2026-10-23T15:02:24Z lines: 27-77 --- From 17eb98d592a33945c32056cd24e0e6924fa97efc Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Fri, 23 Oct 2026 11:02:32 -0400 Subject: [PATCH 3/3] lane: sync fix-rejoin memory --- ...01M5MMH86TBN5X4NFTC9KCNEF9-a-host-leaves-only-as-its-wh.md | 4 ++++ .../01M5MMHKVTATGPKXKCYAQ78HPE-a-host-publishes-and-leaves.md | 4 ++++ ...01M5MMH7A97XGVD8D76KQ8T8PR-leaving-or-changing-director.md | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/.lane/memory/apps/host/src/directory-server.ts/01M5MMH86TBN5X4NFTC9KCNEF9-a-host-leaves-only-as-its-wh.md b/.lane/memory/apps/host/src/directory-server.ts/01M5MMH86TBN5X4NFTC9KCNEF9-a-host-leaves-only-as-its-wh.md index 9dd88cad..d0b9e521 100644 --- a/.lane/memory/apps/host/src/directory-server.ts/01M5MMH86TBN5X4NFTC9KCNEF9-a-host-leaves-only-as-its-wh.md +++ b/.lane/memory/apps/host/src/directory-server.ts/01M5MMH86TBN5X4NFTC9KCNEF9-a-host-leaves-only-as-its-wh.md @@ -3,6 +3,10 @@ id: 01M5MMH86TBN5X4NFTC9KCNEF9 anchor: function handle created: 2026-10-23T15:02:08Z norm: '1' +sig: 4bdf740420c97daf +body_hash: 6de71005918c450b +raw_hash: f80ca60da9be0283 +lines: 109-159 --- a host leaves only as its whois machine name, and leaving calls members so the serving host re-reads its own directory now diff --git a/.lane/memory/apps/host/src/directory-server.ts/01M5MMHKVTATGPKXKCYAQ78HPE-a-host-publishes-and-leaves.md b/.lane/memory/apps/host/src/directory-server.ts/01M5MMHKVTATGPKXKCYAQ78HPE-a-host-publishes-and-leaves.md index bd6b732a..ec1c50ac 100644 --- a/.lane/memory/apps/host/src/directory-server.ts/01M5MMHKVTATGPKXKCYAQ78HPE-a-host-publishes-and-leaves.md +++ b/.lane/memory/apps/host/src/directory-server.ts/01M5MMHKVTATGPKXKCYAQ78HPE-a-host-publishes-and-leaves.md @@ -3,6 +3,10 @@ id: 01M5MMHKVTATGPKXKCYAQ78HPE anchor: function handle created: 2026-10-23T15:02:21Z norm: '1' +sig: 4bdf740420c97daf +body_hash: 6de71005918c450b +raw_hash: f80ca60da9be0283 +lines: 109-159 supersedes: 01M5MH0DD8WZ8X1APBFA87YYPN --- diff --git a/.lane/memory/apps/host/src/directory.ts/01M5MMH7A97XGVD8D76KQ8T8PR-leaving-or-changing-director.md b/.lane/memory/apps/host/src/directory.ts/01M5MMH7A97XGVD8D76KQ8T8PR-leaving-or-changing-director.md index 78c5bedc..75ef02f3 100644 --- a/.lane/memory/apps/host/src/directory.ts/01M5MMH7A97XGVD8D76KQ8T8PR-leaving-or-changing-director.md +++ b/.lane/memory/apps/host/src/directory.ts/01M5MMH7A97XGVD8D76KQ8T8PR-leaving-or-changing-director.md @@ -3,6 +3,10 @@ id: 01M5MMH7A97XGVD8D76KQ8T8PR anchor: function configure created: 2026-10-23T15:02:06Z norm: '1' +sig: f3e1bf286b59976d +body_hash: 929475baa9c1eeb0 +raw_hash: 9a3e5ddfb28d0a2a +lines: 91-104 --- leaving or changing directories DELETEs this host from the old one, after any publish under way to it, or the serving Mac keeps the stale row and a rejoin never counts as a join