DEV Community

Cover image for OpenAI Launched Space. Let's Build a Tiny Shared Workspace in TypeScript.
Bobby Hall Jr
Bobby Hall Jr

Posted on

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 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
Enter fullscreen mode Exit fullscreen mode

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

Table of Contents

  1. What We Are Building
  2. Project Setup
  3. Step 1: Model the Page
  4. Step 2: Actors and ACLs
  5. Step 3: applyEdit With a Revision Check
  6. Step 4: Show the Memory Leak
  7. Step 5: Print an Activity Timeline
  8. Step 6: Run the Collaboration Demo
  9. Where It Breaks Down
  10. The Bigger Idea

What We Are Building

Page, ACL, two agents

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.

Project Setup

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
Enter fullscreen mode Exit fullscreen mode

Save the following blocks, in order, as space.ts.

Step 1: Model the Page

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 };
Enter fullscreen mode Exit fullscreen mode

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.

Step 2: Actors and ACLs

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"})`,
      );
    }
  }
Enter fullscreen mode Exit fullscreen mode

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.

Step 3: applyEdit With a Revision Check

  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 };
  }
Enter fullscreen mode Exit fullscreen mode

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, one section, conflict gate

Two agents can share a page.

They cannot silently overwrite each other.

Step 4: Show the Memory Leak

  /**
   * 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;
  }
Enter fullscreen mode Exit fullscreen mode

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.

Memory note lands on a shared page

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.

Step 5: Print an Activity Timeline

  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}`);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

Step 6: Run the Collaboration Demo

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();
Enter fullscreen mode Exit fullscreen mode

Run it:

npx tsx space.ts
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Same page.

Two agents.

One conflict.

One deny.

One leak Ben can now read.

Where It Breaks Down

This is a teaching workspace. Here is what a real one would need.

Nested Pages

OpenAI lets you organize pages inside other pages. This model is flat.

Real Sync

We bump an in-memory revision. Real systems need vector clocks, CRDTs or a server that serializes writes.

Training Settings

OpenAI's article says personal training settings follow the agent user, not the page owner. We didn't model that at all.

The Leak Is the Point

draftFromMemory is intentionally unsafe. A production Memory layer would need a review step before any private note can land on a shared page.

The Bigger Idea

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     │
└────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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.


Try Roster

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 →

Top comments (0)