OpenAI Launched Space. Let's Build a Tiny Shared Workspace in TypeScript.
Last week I published OpenAI Launched Dots and built a tiny always-on agent. That post was about what wakes an agent up. This one is about where the work lands. OpenAI just shipped a home for that. Space. On or around October 2, 2026, OpenAI's Help Center updated ChatGPT Space: sharing, data, and co

Last week I published OpenAI Launched Dots and built a tiny always-on agent. That post was about what wakes an agent up. This one is about where the work lands. OpenAI just shipped a home for that. Space. On or around October 2, 2026, OpenAI's Help Center updated ChatGPT Space: sharing, data, and controls. Quote: "Space is the home for your files and pages in ChatGPT. It replaces Library, while your Projects stay as they are." Same article: "Pages are editable documents that you can organize inside other pages and work on with other people." And the line that matters for agents: "When you collaborate, each person can use their own ChatGPT agent to work with the shared content." Sharing a page does not share private chats or Memory. Each person works with their own ChatGPT. But Memory can write personal context onto a page. Once it's on the page, viewers can read it. OpenAI's own warning: review the page before you share it. Files uploaded into a page follow that page's permissions. Training: personal accounts follow the agent user's training settings. Business and Enterprise are not trained on by default. There's a second product moving the same way. Anthropic's Help Center, updated this week, says that on October 6, 2026, new Cowork tasks on Pro and Max plans run in the cloud. The "Only on your computer" option in Settings > General will be removed. Sessions and files follow the account across desktop, web and mobile. Different products. Same direction. The chat is still there. But the interesting surface is no longer the chat. It is the shared page the agents write into. So let's build a tiny shared workspace. By the end, you'll run one command: npx tsx space.ts And watch two agents edit a page, hit a conflict, deny a stranger, and leak a private Memory note onto a shared section. No API key. No real model. Just TypeScript. One honesty note: this is not how OpenAI built Space. It's my small model of the ideas in their Help Center. Code: github.com/bobbyhalljr/tiny-shared-workspace What We Are Building Project Setup Step 1: Model the Page Step 2: Actors and ACLs Step 3: applyEdit With a Revision Check Step 4: Show the Memory Leak Step 5: Print an Activity Timeline Step 6: Run the Collaboration Demo Where It Breaks Down The Bigger Idea A page holds sections. An ACL says who can view, edit or own. An actor is a human or an agent. Agents inherit their human's grants. Every edit goes through the ACL, bumps a revision, and lands in an append-only log. It's also a small version of the architecture behind Roster: shared work, separate agents, clear lanes. Ana, Ben, Casey and the salary number are made up. The salary figure is an example input, not a real number. You will need Node.js 18 or newer. mkdir tiny-space cd tiny-space npm init -y npm install --save-dev typescript tsx @types/node Save the following blocks, in order, as space.ts. type Role = "view" | "edit" | "owner"; type Actor = { id: string; kind: "human" | "agent"; // Agents act on behalf of a human. ACL checks use ownerId for agents. ownerId?: string; }; type Section = { id: string; heading: string; body: string; revision: number; }; type AclEntry = { actorId: string; role: Role; }; type Page = { id: string; title: string; sections: Section[]; acl: AclEntry[]; }; type EventKind = | "page.created" | "section.edited" | "edit.denied" | "conflict" | "memory.leaked" | "share.changed"; type SpaceEvent = { at: string; kind: EventKind; actorId: string; pageId: string; detail: string; }; const rank: Record<Role, number> = { view: 1, edit: 2, owner: 3 }; The page is the shared object. Private chats stay off it on purpose. That's the OpenAI split in one data structure. revision is how we catch two agents writing the same section without a re-read. class Space { private pages = new Map<string, Page>(); private log: SpaceEvent[] = []; private clock = 0; private tick(): string { const mins = this.clock++; const h = 10 + Math.floor(mins / 60); const m = mins % 60; return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`; } private record(kind: EventKind, actorId: string, pageId: string, detail: string) { this.log.push({ at: this.tick(), kind, actorId, pageId, detail }); } createPage( owner: Actor, id: string, title: string, sections: { id: string; heading: string; body: string }[], ): Page { const page: Page = { id, title, sections: sections.map((s) => ({ ...s, revision: 1 })), acl: [{ actorId: owner.id, role: "owner" }], }; this.pages.set(id, page); this.record("page.created", owner.id, id, `title="${title}"`); return page; } share(owner: Actor, pageId: string, actorId: string, role: Role) { const page = this.requirePage(pageId); this.requireRole(owner, page, "owner"); const existing = page.acl.find((e) => e.actorId === actorId); if (existing) existing.role = role; else page.acl.push({ actorId, role }); this.record("share.changed", owner.id, pageId, `${actorId} -> ${role}`); } /** Effective actor id for ACL: agents inherit their human owner's grants. */ private effectiveId(actor: Actor): string { return actor.kind === "agent" ? (actor.ownerId ?? actor.id) : actor.id; } private roleOf(actor: Actor, page: Page): Role | null { const id = this.effectiveId(actor); return page.acl.find((e) => e.actorId === id)?.role ?? null; } private requirePage(pageId: string): Page { const page = this.pages.get(pageId); if (!page) throw new Error(`unknown page: ${pageId}`); return page; } private requireRole(actor: Actor, page: Page, need: Role) { const have = this.roleOf(actor, page); if (!have || rank[have] < rank[need]) { const who = this.effectiveId(actor); this.record( "edit.denied", actor.id, page.id, `${who} needs ${need}, has ${have ?? "none"}`, ); throw new Error( `denied: ${actor.id} needs ${need} on ${page.id} (has ${have ?? "none"})`, ); } } Ana owns the page. She shares edit with Ben. ana-agent does not get its own ACL row. It inherits Ana's. That mirrors OpenAI's rule: each person uses their own agent against the shared content. The page is shared. The agent is personal. The grant is on the human. The agent rides along. readSection(actor: Actor, pageId: string, sectionId: string): Section { const page = this.requirePage(pageId); this.requireRole(actor, page, "view"); const section = page.sections.find((s) => s.id === sectionId); if (!section) throw new Error(`unknown section: ${sectionId}`); // Return a snapshot so callers hold a revision number. return { ...section }; } applyEdit( actor: Actor, pageId: string, sectionId: string, newBody: string, expectedRevision: number, ): Section { const page = this.requirePage(pageId); this.requireRole(actor, page, "edit"); const section = page.sections.find((s) => s.id === sectionId); if (!section) throw new Error(`unknown section: ${sectionId}`); if (section.revision !== expectedRevision) { this.record( "conflict", actor.id, pageId, `${sectionId}: expected rev ${expectedRevision}, actual ${section.revision}`, ); throw new Error( `conflict on ${sectionId}: you read rev ${expectedRevision}, page is at ${section.revision}. Re-read, then edit.`, ); } section.body = newBody; section.revision += 1; this.record( "section.edited", actor.id, pageId, `${sectionId} -> rev ${section.revision}`, ); return { ...section }; } readSection returns a snapshot. You pass that snapshot's revision back into applyEdit. If someone else wrote in between, your write fails. Clear error. Re-read. Try again. That is optimistic concurrency in about fifteen lines. Two agents can share a page. They cannot silently overwrite each other. /** * Memory can write personal context onto a page. * Once on the page, viewers can read it. Mirrors OpenAI's Space warning. */ draftFromMemory( actor: Actor, pageId: string, sectionId: string, privateNote: string, expectedRevision: number, ): Section { const framed = `[from memory] ${privateNote}`; const section = this.applyEdit(actor, pageId, sectionId, framed, expectedRevision); this.record( "memory.leaked", actor.id, pageId, `${sectionId}: private note is now on the shared page`, ); return section; } OpenAI's Help Center is blunt about this. If Memory helps you write a page, that content becomes visible to people who can view the page. Your private chat is not shared. The sentence that landed on the page is. So draftFromMemory is not a feature. It's a demo of the bug wearing a helpful face. Review before share is not etiquette. It's the boundary. printTimeline() { console.log("Activity Timeline"); for (const e of this.log) { const cols = [ e.at, e.actorId.padEnd(12), e.kind.padEnd(16), e.pageId.padEnd(8), ]; console.log(`${cols.join(" ")}${e.detail}`); } } printPage(pageId: string) { const page = this.requirePage(pageId); console.log(`\nPage: ${page.title} (${page.id})`); console.log( "ACL:", page.acl.map((e) => `${e.actorId}:${e.role}`).join(", "), ); for (const s of page.sections) { console.log(` [${s.id} rev=${s.revision}] ${s.heading}`); console.log(` ${s.body}`); } } } Creates, shares, edits, denies, conflicts and memory leaks all become rows. A shared workspace without a timeline is just a Google Doc with better marketing. const ana: Actor = { id: "ana", kind: "human" }; const ben: Actor = { id: "ben", kind: "human" }; const anaAgent: Actor = { id: "ana-agent", kind: "agent", ownerId: "ana" }; const benAgent: Actor = { id: "ben-agent", kind: "agent", ownerId: "ben" }; const stranger: Actor = { id: "casey", kind: "human" }; const space = new Space(); space.createPage(ana, "brief-1", "Q4 launch brief", [ { id: "goals", heading: "Goals", body: "Ship shared pages for humans and agents.", }, { id: "risks", heading: "Risks", body: "TBD", }, ]); space.share(ana, "brief-1", "ben", "edit"); // Each person uses their own agent against the shared page. const goalsForAna = space.readSection(anaAgent, "brief-1", "goals"); space.applyEdit( anaAgent, "brief-1", "goals", "Ship shared pages. Keep private chats private.", goalsForAna.revision, ); const risksForBen = space.readSection(benAgent, "brief-1", "risks"); space.applyEdit( benAgent, "brief-1", "risks", "Two agents editing the same section without a re-read.", risksForBen.revision, ); // Conflict: both agents read goals at the same revision, then both write. const staleA = space.readSection(anaAgent, "brief-1", "goals"); const staleB = space.readSection(benAgent, "brief-1", "goals"); space.applyEdit( anaAgent, "brief-1", "goals", "Ship shared pages. Review before share.", staleA.revision, ); try { space.applyEdit( benAgent, "brief-1", "goals", "Ship shared pages. Add a timeline.", staleB.revision, // stale: ana-agent already bumped revision ); } catch (err) { console.log("Expected conflict:"); console.log(` ${(err as Error).message}`); } // Denied: stranger has no ACL entry. try { space.applyEdit(stranger, "brief-1", "risks", "Should not work", 1); } catch (err) { console.log("\nExpected deny:"); console.log(` ${(err as Error).message}`); } // Memory leak: Ana's agent writes a private note onto the shared page. const risksNow = space.readSection(anaAgent, "brief-1", "risks"); space.draftFromMemory( anaAgent, "brief-1", "risks", "Ana's salary band is $180k (example input). Do not share.", risksNow.revision, ); console.log("\nAfter memory draft, Ben can read the page:"); const leaked = space.readSection(ben, "brief-1", "risks"); console.log(` ${leaked.body}`); space.printPage("brief-1"); console.log(""); space.printTimeline(); Run it: npx tsx space.ts You should see: Expected conflict: conflict on goals: you read rev 2, page is at 3. Re-read, then edit. Expected deny: denied: casey needs edit on brief-1 (has none) After memory draft, Ben can read the page: [from memory] Ana's salary band is $180k (example input). Do not share. Page: Q4 launch brief (brief-1) ACL: ana:owner, ben:edit [goals rev=3] Goals Ship shared pages. Review before share. [risks rev=3] Risks [from memory] Ana's salary band is $180k (example input). Do not share. Activity Timeline 10:00 ana page.created brief-1 title="Q4 launch brief" 10:01 ana share.changed brief-1 ben -> edit 10:02 ana-agent section.edited brief-1 goals -> rev 2 10:03 ben-agent section.edited brief-1 risks -> rev 2 10:04 ana-agent section.edited brief-1 goals -> rev 3 10:05 ben-agent conflict brief-1 goals: expected rev 2, actual 3 10:06 casey edit.denied brief-1 casey needs edit, has none 10:07 ana-agent section.edited brief-1 risks -> rev 3 10:08 ana-agent memory.leaked brief-1 risks: private note is now on the shared page Same page. Two agents. One conflict. One deny. One leak Ben can now read. This is a teaching workspace. Here is what a real one would need. OpenAI lets you organize pages inside other pages. This model is flat. We bump an in-memory revision. Real systems need vector clocks, CRDTs or a server that serializes writes. OpenAI's article says personal training settings follow the agent user, not the page owner. We didn't model that at all. draftFromMemory is intentionally unsafe. A production Memory layer would need a review step before any private note can land on a shared page. Dots was about wake-ups and rules. Space is about the object those agents share. Cowork moving sessions into the cloud on October 6 is the same story from another vendor: the work follows the account, not the laptop. ┌────────────────────────────────────────┐ │ Shared workspace │ │ │ │ Human A ──→ Agent A ──┐ │ │ ├──→ Page + ACL │ │ Human B ──→ Agent B ──┘ ↓ │ │ Event log │ │ Revisions │ └────────────────────────────────────────┘ The page provides the shared surface. The ACL provides the boundary. The revision provides the conflict gate. The event log provides evidence. Each agent provides a private loop. Memory provides continuity, and risk, once it writes onto the page. The chat is a conversation. The page is the job. I'm building Roster around this idea: AI employees with real responsibilities, tools, memory, schedules and computer access. They work inside a lane, share only what you put on the page, and ask before doing anything you'd want to see first. If the same follow-ups, handoffs, and waiting loops keep eating your week, give them to an AI employee. Try Roster →
Key Takeaways
- •Last week I published OpenAI Launched Dots and built a tiny always-on agent. That post was about what wakes an agent up. This one is about where the work lands. OpenAI just shipped a home for that. Space. On or around October 2, 2026, OpenAI's Help Center updated ChatGPT Space: sharing, data, and co
- •This story was reported by Dev.to, covering developments in the dev space.
- •AI advancements continue to reshape industries — read the full article on Dev.to for complete coverage.
📖 Continue reading the full article:
Read Full Article on Dev.to →

