From 2e1f8e6ff94090a0189f640acf0200c87b6036de Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Tue, 20 Oct 2026 13:04:56 -0400 Subject: [PATCH 1/3] feat(host): export a channel as a seed and import seeds as new channels `ace export` writes a local channel's chats as editable JSON: people's messages by author and time, and agents' turns as the pi messages their runs committed. `ace import` appends a seed through pi into a new channel, optionally moving its times to now, so demos and fixtures get real channels whose agents read the imported history as context. The gateway now refuses worker-only requests (worker, freeze, export), which it previously forwarded to channel workers for any client. Why: apps/host/src/gateway.ts | Worker requests (WORKER) never pass through the gateway; any new WorkerRequest op must be added there Why: packages/channel/src/seed.ts#function importSeed | Imports only into an empty root chat, appending through pi so ids, heads and docs stay pi's --- apps/host/src/cli.ts | 21 +++ apps/host/src/gateway.ts | 5 + apps/host/src/protocol.ts | 3 +- apps/host/src/seed.ts | 82 ++++++++++++ apps/host/src/worker.ts | 1 + docs/data.md | 15 +++ packages/channel/src/channel.ts | 11 ++ packages/channel/src/index.ts | 1 + packages/channel/src/seed.ts | 220 ++++++++++++++++++++++++++++++++ 9 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 apps/host/src/seed.ts create mode 100644 packages/channel/src/seed.ts diff --git a/apps/host/src/cli.ts b/apps/host/src/cli.ts index 5b70c88a..8c5fded2 100755 --- a/apps/host/src/cli.ts +++ b/apps/host/src/cli.ts @@ -7,6 +7,7 @@ import type { ChannelInfo, ChatId, Event, Request } from "@ace/channel/protocol" import { appUrl, token } from "./auth"; import { backup, restore } from "./backup"; +import { exportSeed, importSeed } from "./seed"; import * as catalog from "./catalog"; import { Connection, request } from "./client"; import { config } from "./config"; @@ -58,6 +59,10 @@ const HELP = `ace — channels on this host ace tab rename rename one tab; an empty name restores its default ace backup snapshot a local or hosted channel's data into a new directory ace restore [--project ] restore a backup as a new local channel + ace export save a channel's chats as an editable seed file + ace import [--project ] [--name ] [--now] + add a seed as a new local channel; --now moves its + newest message to now ace say [--chat ] post without invoking the agent ace ask [--chat ] [--model ] [--detach] ace watch [--chat ] @@ -107,6 +112,7 @@ const { values: flags, positionals } = parseArgs({ all: { type: "boolean" }, team: { type: "boolean" }, detach: { type: "boolean" }, + now: { type: "boolean" }, channel: { type: "string" }, trace: { type: "string" }, level: { type: "string" }, @@ -243,6 +249,7 @@ function printAccess(access: ProjectAccess) { const owned: Record = { backup: "back it up", + export: "export it", rename: "rename it", fork: "fork it", kill: "kill it", @@ -408,6 +415,17 @@ async function main() { const record = await restore(ref!, flags.project); return console.log(`${record.name}\t${record.id}`); } + case "import": { + if (positionals.length !== 2) { + throw new Error("ace import [--project ] [--name ] [--now]"); + } + const record = await importSeed(ref!, { + dir: flags.project || process.cwd(), + name: flags.name, + now: flags.now, + }); + return console.log(`${record.name}\t${record.id}`); + } case "models": { for (const m of await models()) console.log(`${m.provider}/${m.modelId}`); return; @@ -554,6 +572,9 @@ async function main() { console.log(await backup(record, rest[0]!)); console.log("Channel data saved. Back up project files and lane worktrees separately."); return; + case "export": + if (rest.length !== 1) throw new Error("ace export "); + return console.log(await exportSeed(record, rest[0]!)); case "rename": if (rest.length !== 1) throw new Error("ace rename "); await request(record.id, { op: "rename", author: catalog.user, name: rest[0]! }); diff --git a/apps/host/src/gateway.ts b/apps/host/src/gateway.ts index 5e04ec0a..f5c8066c 100644 --- a/apps/host/src/gateway.ts +++ b/apps/host/src/gateway.ts @@ -44,6 +44,7 @@ import { type People, type Presence, projectId, + type WorkerRequest, } from "./protocol"; import { type Machine, self, whois } from "./tailnet"; import { @@ -464,6 +465,9 @@ const localOps = new Set([ type ChannelRequest = Extract["request"]; +/** Worker requests come from this host's own processes, never through the gateway. */ +const WORKER = new Set(["worker", "freeze", "export"] satisfies WorkerRequest["op"][]); + /** The verified person replaces any claimed author. Older clients send stop and kill without one. */ function authored(request: ChannelRequest, user: string): ChannelRequest { if (request.op === "stop" || request.op === "kill" || "author" in request) { @@ -739,6 +743,7 @@ async function handle( client.terminals.delete(request.terminal); return terminals.close(request.terminal); case "channel": { + if (WORKER.has(request.request.op)) throw new Error(`Unknown request ${request.request.op}`); // Only a watch streams; other requests must not leave watchers behind in relays. const watch: Watch | undefined = request.request.op === "watch" ? { diff --git a/apps/host/src/protocol.ts b/apps/host/src/protocol.ts index 919cc141..c19dab6b 100644 --- a/apps/host/src/protocol.ts +++ b/apps/host/src/protocol.ts @@ -35,7 +35,8 @@ export type MoveTarget = { target: string; label: string; place: "local" | "host export type WorkerInfo = { id: string; pid: number }; /** `freeze` drains admitted requests and exits the worker for a move, or fails if work remains. */ -export type WorkerRequest = { op: "worker" } | { op: "freeze" }; +/** Only this host's own processes send these; the gateway refuses them from clients. */ +export type WorkerRequest = { op: "worker" } | { op: "freeze" } | { op: "export" }; export type Diagnostics = { tools: { name: string; path?: string; version?: string; error?: string }[]; diff --git a/apps/host/src/seed.ts b/apps/host/src/seed.ts new file mode 100644 index 00000000..ae21ea4f --- /dev/null +++ b/apps/host/src/seed.ts @@ -0,0 +1,82 @@ +import { readFileSync, rmSync, writeFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context"; +import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node"; +import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node"; + +import { Channel, type Seed, shiftTo, validateSeed } from "@ace/channel"; + +import * as catalog from "./catalog"; +import { Connection } from "./client"; +import { models } from "./keys"; +import { log } from "./log"; +import { project } from "./manage"; + +/** A local channel's chats as a seed file, read through its worker so it can keep running. */ +export async function exportSeed(record: catalog.Listing, file: string): Promise { + if (record.hosted) throw new Error("Export a hosted channel after moving it here with ace move"); + const connection = await Connection.open(record.id); + try { + const seed = await connection.request({ op: "export" }); + const path = resolve(file); + writeFileSync(path, `${JSON.stringify(seed, null, "\t")}\n`, { mode: 0o600, flag: "wx" }); + return path; + } finally { + connection.close(); + } +} + +/** + * A seed as a new local channel in `dir`, or the current directory. `now` moves the seed's times + * so its newest message is now. The channel is listed only once its store holds the whole seed. + */ +export async function importSeed( + file: string, + options: { dir: string; name?: string; now?: boolean }, +): Promise { + const seed = JSON.parse(readFileSync(resolve(file), "utf8")) as Seed; + validateSeed(seed); + const record = { + ...catalog.draft(await project(options.dir), seed.chats[0]!.model, options.name || seed.name), + ...(seed.summary ? { summary: seed.summary } : {}), + }; + const { base, storage } = catalog.paths(record.id); + const envs: NodeExecutionEnv[] = []; + try { + const channel = await Channel.open({ + id: record.id, + name: record.name, + prefix: record.prefix, + named: true, + owner: record.owner, + project: record.project, + model: record.model, + storage: await openNodeSqliteStorage(storage), + models: models(), + env(cwd) { + const env = new NodeExecutionEnv({ cwd }); + envs.push(env); + return env; + }, + directory: { + self: { id: record.id, name: record.prefix }, + list: async () => [], + deliver: async () => { + throw new Error("An importing channel delivers nothing"); + }, + }, + log, + }); + try { + await channel.import(seed, options.now ? shiftTo(seed, Date.now()) : 0); + } finally { + await channel.close(); + for (const env of envs) await env.cleanup(BACKGROUND_CONTEXT); + } + return catalog.save({ ...record, named: true }); + } catch (error) { + rmSync(base, { recursive: true, force: true }); + throw error; + } +} diff --git a/apps/host/src/worker.ts b/apps/host/src/worker.ts index b0093064..90904d70 100644 --- a/apps/host/src/worker.ts +++ b/apps/host/src/worker.ts @@ -158,6 +158,7 @@ function serve(socket: Socket) { send({ id: frame, ok: true, value: null }); return exit(false, "move"); } + if (body.op === "export") return send({ id: frame, ok: true, value: await channel.export() }); if (catalog.read(id).archived && ARCHIVED.has(body.op)) { throw new Error("The channel is archived"); } diff --git a/docs/data.md b/docs/data.md index cc14fe28..5802b0e2 100644 --- a/docs/data.md +++ b/docs/data.md @@ -88,6 +88,21 @@ The restored chats keep the original working-directory and lane paths. If the ba during a run, the copy resumes that run when it first opens, and pi reports interrupted tools that cannot safely be rerun. Stop the original channel's work first if both would use the same lanes. +## Seeds + +A seed is a channel's chats as editable JSON, for demos and fixtures rather than recovery. +`ace export ` writes a local channel's ownerless chats: people's messages by author +and ISO time, as they read now, and agents' replies and tool results as the pi messages their runs +committed, with reactions, captions, recaps, and each chat's model. Subagent chats, deleted +messages, run bookkeeping, and lanes stay behind. + +`ace import [--project ] [--name ] [--now]` adds a seed as a new local channel in +the current directory's project or `--project`. It appends every item through pi, so the channel is +ordinary: its agents read the imported history as context, and later runs continue it. Authors are +any valid participant names, and `--now` moves every time so the newest message lands at import. +The first chat is the root chat. Agent items must be `pi.assistant` or `pi.tool-result` entries, +and tool calls and results must still pair up. A failed import removes the new channel's directory. + ## Recover in place on the same machine To return a local channel to a snapshot under its original ID: diff --git a/packages/channel/src/channel.ts b/packages/channel/src/channel.ts index fe6aa1b9..2c4c1202 100644 --- a/packages/channel/src/channel.ts +++ b/packages/channel/src/channel.ts @@ -83,6 +83,7 @@ import type { } from "./protocol"; import { queue, withdrawn } from "./queue"; import * as room from "./room"; +import { exportSeed, importSeed, type Seed } from "./seed"; import { type Tabs, tabs } from "./tabs"; type Settings = { shared: boolean; desktop: boolean }; @@ -553,6 +554,16 @@ export class Channel { } /** Durably abort every chat's work, including background subagents, so reopening resumes none of it. */ + /** The channel's chats as a seed another channel can import. */ + export(): Promise { + return exportSeed(this.#harness); + } + + /** Fills a new channel from a seed, `shift` milliseconds later than its own times. */ + import(value: Seed, shift?: number): Promise { + return importSeed(this.#harness, value, shift); + } + async kill(): Promise { const { chats } = await this.info(); this.#log("warn", "channel.kill", { diff --git a/packages/channel/src/index.ts b/packages/channel/src/index.ts index 488322b2..c0c65e89 100644 --- a/packages/channel/src/index.ts +++ b/packages/channel/src/index.ts @@ -3,3 +3,4 @@ export { Channel, type Options } from "./channel"; export { failure, type Fields, type Level, type Log, scoped } from "./log"; export { validateName } from "./metadata"; export * from "./protocol"; +export { type Seed, type SeedChat, type SeedItem, shiftTo, validateSeed } from "./seed"; diff --git a/packages/channel/src/seed.ts b/packages/channel/src/seed.ts new file mode 100644 index 00000000..96bca22d --- /dev/null +++ b/packages/channel/src/seed.ts @@ -0,0 +1,220 @@ +import { BACKGROUND_CONTEXT as context } from "@earendil-works/chord/context"; +import type { Message } from "@earendil-works/pi-ai"; +import { + AgentDoc, + AssistantEntry, + type ConversationId, + type Cursor, + type EntryDraft, + type EntryId, + type EntryRecord, + type Harness, + ROOT_CONVERSATION_ID, + ToolResultEntry, + type Tx, + UserEntry, +} from "@earendil-works/pi-durable"; + +import { CaptionsDoc } from "./captions"; +import { EditEntry, MessagesDoc } from "./messages"; +import { MetadataDoc } from "./metadata"; +import type { Effort, Image, ModelRef, Reaction, TextFile } from "./protocol"; +import * as room from "./room"; + +/** + * A channel's conversations as editable JSON: people's messages by author and time, and agents' + * turns as the pi messages a real run committed. Importing appends them through pi, so the result + * is an ordinary channel whose agents read the imported history as their context. + */ +export type Seed = { + format: "ace-seed"; + version: 1; + name: string; + summary?: string; + /** The first chat is the channel's root chat. */ + chats: SeedChat[]; +}; + +export type SeedChat = { + model?: ModelRef; + effort?: Effort; + items: SeedItem[]; + /** Tool-call captions and recaps, keyed by call ID, so imports need no captioning model. */ + captions?: { groups: Record; recaps: Record }; +}; + +export type SeedItem = + & { reactions?: Reaction[] } + & ( + | { + author: string; + text: string; + /** ISO 8601. */ + at: string; + /** Whether the message asked the chat's agent to run. */ + invoke?: boolean; + images?: Image[]; + files?: TextFile[]; + } + | { agent: { kind: string; model: Message[]; data?: EntryRecord["data"] } } + ); + +const AGENT = new Set([AssistantEntry.kind, ToolResultEntry.kind]); + +async function history(harness: Harness, chat: ConversationId): Promise { + const entries: EntryRecord[] = []; + let cursor: Cursor | undefined; + do { + const page = await harness.commit( + (tx) => tx.scanEntries({ conversationId: chat }, 256, cursor), + context, + ); + entries.push(...page.items); + cursor = page.next; + } while (cursor); + return entries.reverse(); +} + +/** + * People's messages as they read now, edits applied and deletions dropped, and agents' replies and + * tool results. Subagent chats, run bookkeeping, and prompt markers stay behind. + */ +export async function exportSeed(harness: Harness): Promise { + const { name, summary } = (await harness.snapshot(MetadataDoc, context))!; + const records = []; + let cursor: Cursor | undefined; + do { + const page = await harness.commit((tx) => tx.scanConversations({}, 256, cursor), context); + records.push(...page.items); + cursor = page.next; + } while (cursor); + const chats = await Promise.all( + records.filter((record) => !record.owner).map(async ({ id }): Promise => { + const [agent, actions, captions, entries] = await Promise.all([ + harness.snapshot(AgentDoc, id, context), + harness.snapshot(MessagesDoc, id, context), + harness.snapshot(CaptionsDoc, id, context), + history(harness, id), + ]); + const edited = new Map(); + for (const entry of entries) { + if (EditEntry.is(entry)) edited.set(entry.data.target, entry.data.text); + } + const items = entries.flatMap((entry): SeedItem[] => { + const reactions = actions?.reactions[entry.id]; + const extra = reactions?.length ? { reactions } : {}; + const human = room.human(entry); + if (human) { + if (actions?.deleted[entry.id]) return []; + const { author, text, at, invoked, images, files } = human; + return [{ + author, + text: edited.get(entry.id) ?? text, + at: new Date(at).toISOString(), + ...(invoked ? { invoke: true } : {}), + ...(images ? { images } : {}), + ...(files ? { files } : {}), + ...extra, + }]; + } + if (!AGENT.has(entry.kind) || !entry.model) return []; + const data = entry.data === undefined ? {} : { data: entry.data }; + return [{ agent: { kind: entry.kind, model: [...entry.model], ...data }, ...extra }]; + }); + return { + ...(agent?.model ? { model: agent.model } : {}), + ...(agent?.thinkingLevel ? { effort: agent.thinkingLevel } : {}), + items, + ...(captions ? { captions } : {}), + }; + }), + ); + return { format: "ace-seed", version: 1, name, ...(summary ? { summary } : {}), chats }; +} + +export function validateSeed(seed: Seed): void { + if (seed?.format !== "ace-seed" || seed.version !== 1) { + throw new Error("Not a version 1 Ace seed"); + } + if (!seed.chats?.length) throw new Error("A seed needs at least one chat"); + for (const chat of seed.chats) { + for (const item of chat.items) { + if ("agent" in item) { + const { kind, model } = item.agent; + if (!AGENT.has(kind) || model?.length !== 1) { + throw new Error(`Seeds carry only agent replies and tool results, not ${kind}`); + } + continue; + } + if (!room.isAuthor(item.author)) throw new Error(`Invalid author ${item.author}`); + if (Number.isNaN(Date.parse(item.at))) throw new Error(`Invalid time ${item.at}`); + room.checkAttachments(item); + } + } +} + +/** Every time in the seed, moved by `shift` milliseconds. */ +function moved(message: Message, shift: number): Message { + return { ...message, timestamp: message.timestamp + shift } as Message; +} + +async function append(tx: Tx, chat: ConversationId, seed: SeedChat, shift: number) { + const reactions = await tx.doc(MessagesDoc, chat); + for (const item of seed.items) { + let draft: EntryDraft; + if ("agent" in item) { + const { kind, model, data } = item.agent; + draft = { kind, model: model.map((message) => moved(message, shift)), data }; + } else { + const at = Date.parse(item.at) + shift; + const message = room.draft(item.author, item.text, at, item); + draft = item.invoke ? { kind: UserEntry.kind, model: message.model } : message; + } + const { id } = await tx.appendEntry(chat, draft); + if (item.reactions?.length) reactions.reactions[id as EntryId] = item.reactions; + } + if (seed.captions) Object.assign(await tx.doc(CaptionsDoc, chat), seed.captions); +} + +/** + * Appends a seed to a new channel whose root chat is still empty, `shift` milliseconds later than + * the seed's own times. The channel's name is claimed by its runtime, so only the summary is set. + */ +export async function importSeed(harness: Harness, seed: Seed, shift = 0): Promise { + validateSeed(seed); + const [root, ...rest] = seed.chats as [SeedChat, ...SeedChat[]]; + await harness.commit(async (tx) => { + const page = await tx.scanEntries({ conversationId: ROOT_CONVERSATION_ID }, 1); + if (page.items.length) throw new Error("Seeds import only into a new channel"); + if (seed.summary) { + const metadata = await tx.doc(MetadataDoc); + metadata.summary = seed.summary; + metadata.revision++; + } + await append(tx, ROOT_CONVERSATION_ID, root, shift); + }, context); + const conversation = await harness.conversation(ROOT_CONVERSATION_ID, context); + if (root.model || root.effort) { + await conversation!.configure({ model: root.model, thinkingLevel: root.effort }, context); + } + const { cwd } = (await harness.snapshot(AgentDoc, ROOT_CONVERSATION_ID, context))!; + for (const chat of rest) { + await harness.createConversation({ + ownership: { kind: "ownerless" }, + agent: { model: chat.model || root.model, thinkingLevel: chat.effort, cwd }, + init: (tx, id) => append(tx, id, chat, shift), + }, context); + } +} + +/** The shift that moves a seed's newest time to `now`. */ +export function shiftTo(seed: Seed, now: number): number { + let newest = 0; + for (const chat of seed.chats) { + for (const item of chat.items) { + const at = "agent" in item ? item.agent.model[0]!.timestamp : Date.parse(item.at); + newest = Math.max(newest, at); + } + } + return newest ? now - newest : 0; +} From 54ba6b18b8ccd5e9f73b92124f21fdd10cee096d Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Tue, 20 Oct 2026 13:05:17 -0400 Subject: [PATCH 2/3] chore: confirm lane notes --- ...YY5SXJFQK8VXT78X1E0KAE-every-command-but-serve-must.md | 6 +++--- ...4YVZ6KGNQ7MFS1RP81MQCE0-a-hosted-channel-s-location.md | 6 +++--- ...Z2BCKRNHVMYEMC0KH3Q6BN-a-hosted-channel-must-be-rea.md | 6 +++--- ...5AZZV2CRMNFXZKM0B3AWRVX-a-move-to-a-cloud-workspace.md | 6 +++--- ...D4BXMERGZ1EZ3EYAFM3YC4-worker-requests-worker-never.md | 8 ++++++++ ...YQXGS3RQSHX8N6GH99R7D4-the-directory-s-self-name-st.md | 6 +++--- ...XQKND0900E94CG3BXBEJP7-a-run-s-end-is-read-from-the.md | 6 +++--- ...4YFFTK7TWGKENJAD1W10HJJ-the-desktop-default-changes.md | 6 +++--- ...4YPJQB6KKEW1YTRD08YCYJY-only-gateways-admit-clients.md | 6 +++--- ...ZH31DXZK0Y57C97DP84ZKX-handoffs-learns-queued-hando.md | 6 +++--- ...3CAGW2RVRKCYF5BCJRTZX2-a-fork-s-starting-lane-is-re.md | 6 +++--- ...D4BXMW2NZW40Y17T5QG77G-imports-only-into-an-empty-r.md | 8 ++++++++ 12 files changed, 46 insertions(+), 30 deletions(-) create mode 100644 .lane/memory/apps/host/src/gateway.ts/01M5D4BXMERGZ1EZ3EYAFM3YC4-worker-requests-worker-never.md create mode 100644 .lane/memory/packages/channel/src/seed.ts/01M5D4BXMW2NZW40Y17T5QG77G-imports-only-into-an-empty-r.md diff --git a/.lane/memory/apps/host/src/cli.ts/01M4YY5SXJFQK8VXT78X1E0KAE-every-command-but-serve-must.md b/.lane/memory/apps/host/src/cli.ts/01M4YY5SXJFQK8VXT78X1E0KAE-every-command-but-serve-must.md index 69a40979..e71d8b0e 100644 --- a/.lane/memory/apps/host/src/cli.ts/01M4YY5SXJFQK8VXT78X1E0KAE-every-command-but-serve-must.md +++ b/.lane/memory/apps/host/src/cli.ts/01M4YY5SXJFQK8VXT78X1E0KAE-every-command-but-serve-must.md @@ -4,9 +4,9 @@ anchor: function main created: 2026-10-15T04:47:23Z norm: '1' sig: 7d5d3f63a3bddde4 -body_hash: 1a9df6a60adae3f7 -raw_hash: 7454a2968598e917 -vouched: 2026-10-19T21:10:15Z +body_hash: 2df61fec47c0ebd8 +raw_hash: 0ee7581f4d71df17 +vouched: 2026-10-20T17:05:11Z lines: 253-482 --- 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 060edca9..c4901f86 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: 28e153c79ae61537 -raw_hash: 640ff85ff07439ad -vouched: 2026-10-19T21:15:06Z +body_hash: 1b1e7b788ab2e1af +raw_hash: 581d53ea3cc3de56 +vouched: 2026-10-20T17:05:11Z 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 deafa3d7..c1997c10 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: 28e153c79ae61537 -raw_hash: 640ff85ff07439ad -vouched: 2026-10-19T21:15:07Z +body_hash: 1b1e7b788ab2e1af +raw_hash: 581d53ea3cc3de56 +vouched: 2026-10-20T17:05:11Z 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 1426f5a3..bbceedef 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: 28e153c79ae61537 -raw_hash: 640ff85ff07439ad -vouched: 2026-10-19T21:15:07Z +body_hash: 1b1e7b788ab2e1af +raw_hash: 581d53ea3cc3de56 +vouched: 2026-10-20T17:05:11Z 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 new file mode 100644 index 00000000..f18a4b4e --- /dev/null +++ b/.lane/memory/apps/host/src/gateway.ts/01M5D4BXMERGZ1EZ3EYAFM3YC4-worker-requests-worker-never.md @@ -0,0 +1,8 @@ +--- +id: 01M5D4BXMERGZ1EZ3EYAFM3YC4 +anchor: '@file' +created: 2026-10-20T17:04:56Z +norm: '1' +--- + +Worker requests (WORKER) never pass through the gateway; any new WorkerRequest op must be added there diff --git a/.lane/memory/apps/host/src/worker.ts/01M4YQXGS3RQSHX8N6GH99R7D4-the-directory-s-self-name-st.md b/.lane/memory/apps/host/src/worker.ts/01M4YQXGS3RQSHX8N6GH99R7D4-the-directory-s-self-name-st.md index 9da3e28d..eb56fef5 100644 --- a/.lane/memory/apps/host/src/worker.ts/01M4YQXGS3RQSHX8N6GH99R7D4-the-directory-s-self-name-st.md +++ b/.lane/memory/apps/host/src/worker.ts/01M4YQXGS3RQSHX8N6GH99R7D4-the-directory-s-self-name-st.md @@ -4,9 +4,9 @@ anchor: '@file' created: 2026-10-15T02:58:00Z norm: '1' sig: 1a43097a4e1ad24c -body_hash: 764891fdee20e693 -raw_hash: bd2ba9a358d5c3a8 -vouched: 2026-10-19T20:53:00Z +body_hash: b6bff4ee7aa001b5 +raw_hash: da64faa9eaa88a3e +vouched: 2026-10-20T17:05:11Z lines: 1-194 --- diff --git a/.lane/memory/packages/channel/src/channel.ts/01M4XQKND0900E94CG3BXBEJP7-a-run-s-end-is-read-from-the.md b/.lane/memory/packages/channel/src/channel.ts/01M4XQKND0900E94CG3BXBEJP7-a-run-s-end-is-read-from-the.md index 10c6d4dd..8bbbf81d 100644 --- a/.lane/memory/packages/channel/src/channel.ts/01M4XQKND0900E94CG3BXBEJP7-a-run-s-end-is-read-from-the.md +++ b/.lane/memory/packages/channel/src/channel.ts/01M4XQKND0900E94CG3BXBEJP7-a-run-s-end-is-read-from-the.md @@ -4,9 +4,9 @@ anchor: class Channel created: 2026-10-14T17:33:22Z norm: '1' sig: c905677fa4be33c1 -body_hash: b94131a282fede50 -raw_hash: 0632800a47dcc747 -vouched: 2026-10-19T20:57:54Z +body_hash: ab25308bd79989e5 +raw_hash: 8691059ceba82558 +vouched: 2026-10-20T17:05:11Z lines: 114-792 --- diff --git a/.lane/memory/packages/channel/src/channel.ts/01M4YFFTK7TWGKENJAD1W10HJJ-the-desktop-default-changes.md b/.lane/memory/packages/channel/src/channel.ts/01M4YFFTK7TWGKENJAD1W10HJJ-the-desktop-default-changes.md index 51d5213e..f0228724 100644 --- a/.lane/memory/packages/channel/src/channel.ts/01M4YFFTK7TWGKENJAD1W10HJJ-the-desktop-default-changes.md +++ b/.lane/memory/packages/channel/src/channel.ts/01M4YFFTK7TWGKENJAD1W10HJJ-the-desktop-default-changes.md @@ -4,9 +4,9 @@ anchor: class Channel created: 2026-10-15T00:30:42Z norm: '1' sig: c905677fa4be33c1 -body_hash: b94131a282fede50 -raw_hash: 0632800a47dcc747 -vouched: 2026-10-19T20:57:54Z +body_hash: ab25308bd79989e5 +raw_hash: 8691059ceba82558 +vouched: 2026-10-20T17:05:11Z lines: 114-792 --- diff --git a/.lane/memory/packages/channel/src/channel.ts/01M4YPJQB6KKEW1YTRD08YCYJY-only-gateways-admit-clients.md b/.lane/memory/packages/channel/src/channel.ts/01M4YPJQB6KKEW1YTRD08YCYJY-only-gateways-admit-clients.md index f6cd0b81..352e6f9e 100644 --- a/.lane/memory/packages/channel/src/channel.ts/01M4YPJQB6KKEW1YTRD08YCYJY-only-gateways-admit-clients.md +++ b/.lane/memory/packages/channel/src/channel.ts/01M4YPJQB6KKEW1YTRD08YCYJY-only-gateways-admit-clients.md @@ -4,9 +4,9 @@ anchor: class Channel created: 2026-10-15T02:34:37Z norm: '1' sig: c905677fa4be33c1 -body_hash: b94131a282fede50 -raw_hash: 0632800a47dcc747 -vouched: 2026-10-19T20:57:54Z +body_hash: ab25308bd79989e5 +raw_hash: 8691059ceba82558 +vouched: 2026-10-20T17:05:11Z lines: 114-794 --- diff --git a/.lane/memory/packages/channel/src/channel.ts/01M4ZH31DXZK0Y57C97DP84ZKX-handoffs-learns-queued-hando.md b/.lane/memory/packages/channel/src/channel.ts/01M4ZH31DXZK0Y57C97DP84ZKX-handoffs-learns-queued-hando.md index e5fe9e5a..48d28b8c 100644 --- a/.lane/memory/packages/channel/src/channel.ts/01M4ZH31DXZK0Y57C97DP84ZKX-handoffs-learns-queued-hando.md +++ b/.lane/memory/packages/channel/src/channel.ts/01M4ZH31DXZK0Y57C97DP84ZKX-handoffs-learns-queued-hando.md @@ -4,9 +4,9 @@ anchor: class Channel created: 2026-10-15T10:17:55Z norm: '1' sig: c905677fa4be33c1 -body_hash: b94131a282fede50 -raw_hash: 0632800a47dcc747 -vouched: 2026-10-19T20:57:54Z +body_hash: ab25308bd79989e5 +raw_hash: 8691059ceba82558 +vouched: 2026-10-20T17:05:11Z lines: 121-817 --- diff --git a/.lane/memory/packages/channel/src/channel.ts/01M53CAGW2RVRKCYF5BCJRTZX2-a-fork-s-starting-lane-is-re.md b/.lane/memory/packages/channel/src/channel.ts/01M53CAGW2RVRKCYF5BCJRTZX2-a-fork-s-starting-lane-is-re.md index e3aea1df..5987caeb 100644 --- a/.lane/memory/packages/channel/src/channel.ts/01M53CAGW2RVRKCYF5BCJRTZX2-a-fork-s-starting-lane-is-re.md +++ b/.lane/memory/packages/channel/src/channel.ts/01M53CAGW2RVRKCYF5BCJRTZX2-a-fork-s-starting-lane-is-re.md @@ -4,9 +4,9 @@ anchor: class Channel created: 2026-10-16T22:11:35Z norm: '1' sig: c905677fa4be33c1 -body_hash: b94131a282fede50 -raw_hash: 0632800a47dcc747 -vouched: 2026-10-19T20:57:54Z +body_hash: ab25308bd79989e5 +raw_hash: 8691059ceba82558 +vouched: 2026-10-20T17:05:11Z lines: 127-873 --- diff --git a/.lane/memory/packages/channel/src/seed.ts/01M5D4BXMW2NZW40Y17T5QG77G-imports-only-into-an-empty-r.md b/.lane/memory/packages/channel/src/seed.ts/01M5D4BXMW2NZW40Y17T5QG77G-imports-only-into-an-empty-r.md new file mode 100644 index 00000000..3409043d --- /dev/null +++ b/.lane/memory/packages/channel/src/seed.ts/01M5D4BXMW2NZW40Y17T5QG77G-imports-only-into-an-empty-r.md @@ -0,0 +1,8 @@ +--- +id: 01M5D4BXMW2NZW40Y17T5QG77G +anchor: function importSeed +created: 2026-10-20T17:04:56Z +norm: '1' +--- + +Imports only into an empty root chat, appending through pi so ids, heads and docs stay pi's From 5d6e4dffd3069b09bbb9e19e7fb7e96cdd8db274 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Tue, 20 Oct 2026 13:05:17 -0400 Subject: [PATCH 3/3] lane: sync seed-import memory --- ...01M5D4BXMERGZ1EZ3EYAFM3YC4-worker-requests-worker-never.md | 4 ++++ ...01M5D4BXMW2NZW40Y17T5QG77G-imports-only-into-an-empty-r.md | 4 ++++ 2 files changed, 8 insertions(+) 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 f18a4b4e..4ae0fc1d 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 @@ -3,6 +3,10 @@ id: 01M5D4BXMERGZ1EZ3EYAFM3YC4 anchor: '@file' created: 2026-10-20T17:04:56Z norm: '1' +sig: 1acdc0f6a0895ac5 +body_hash: 2cc87782e4d39750 +raw_hash: 2c0a05f043c689ac +lines: 1-1153 --- Worker requests (WORKER) never pass through the gateway; any new WorkerRequest op must be added there diff --git a/.lane/memory/packages/channel/src/seed.ts/01M5D4BXMW2NZW40Y17T5QG77G-imports-only-into-an-empty-r.md b/.lane/memory/packages/channel/src/seed.ts/01M5D4BXMW2NZW40Y17T5QG77G-imports-only-into-an-empty-r.md index 3409043d..bdf674c9 100644 --- a/.lane/memory/packages/channel/src/seed.ts/01M5D4BXMW2NZW40Y17T5QG77G-imports-only-into-an-empty-r.md +++ b/.lane/memory/packages/channel/src/seed.ts/01M5D4BXMW2NZW40Y17T5QG77G-imports-only-into-an-empty-r.md @@ -3,6 +3,10 @@ id: 01M5D4BXMW2NZW40Y17T5QG77G anchor: function importSeed created: 2026-10-20T17:04:56Z norm: '1' +sig: 2b5122279f6dcc31 +body_hash: 6d99539836bcb9df +raw_hash: fa97fa789d71e663 +lines: 183-208 --- Imports only into an empty root chat, appending through pi so ids, heads and docs stay pi's