The Conversation office module. An office is one conversation's persistent working area and data boundary (CONTEXT.md); this module owns its identity, its host-side directory layout, its durable record, and the legacy migration.
- Identity:
OfficeAddressisplatformplus the platform's rawconversationId; raw ids stay at platform I/O boundaries. Storage paths use the versionedOfficeKey(v1-<platform>-<readable>-<16 hex>), whose digest is SHA-256 over both values (ADR 0005) — the readable middle is a hint, the digest is the authority.createConversationEvent/createConversationMessagenormalize any intake (platform adapter or scheduler) onto one authoritativeOfficeAddress. The raw conversation id grammars also live here: the Telegram, Discord, and Slack recognizers used by legacy migration, and the GitHubGH_<owner>_<repo>_<number>encode/parse (buildGithubConversationId,parseGithubConversationId) that the GitHub adapter imports.officeStateDir(stateDir, address)is the one path helper exported for stateDir-only surfaces that hold no Office value, such as migration and settings helpers.officeDiris module-internal, because callers outsidesrc/office/useOffice.dir. - Layout:
createWorkspace({root, stateDir})builds the per-processWorkspace(workspace-global paths, reserved-name set, office factory);workspace.office(address)returns the memoized, frozenOffice(key, dir, subpaths, host state dir,ensure()).Office.ensure()is the single materialization seam: it records the office in the registry before creating the directory, so the registry stays the durable raw-id ↔ office mapping (office keys are not reversible). The registry instance and recorded-office cache live on the Workspace value — there is no process-wide registry state. - Registry:
OfficeRegistry, the host-only record (office-registry.jsonin the state dir) of every office's address, written under a domain lease. Plus the cold-pathlistRegisteredOfficeslookup used by Admin enumeration andmikan office list. Moving 0.5.3 raw-id directories into this layout belongs tosrc/migrations/. - Projection (
projection.ts): The office-owned data-view policy seam.resolveOfficeVisibility(office)derives public/private from the platform (only Slack public channels are public; other platforms are private by decision), the recorded channel kind, and the operator override;resolveWorkspaceProjection(office)turns that into the uniform runtime mounts (own office rw, every other public office ro, shared knowledge rw/ro), authorizes prompt sources, and lists the conversations whose offices those mounts expose. The system prompt names only those conversations, so a private office's existence and name stay out of every other office's prompt.
Runtime consumers are switched: session state, vaults, per-conversation host state, and Admin scope are all office-keyed (ADR 0005). Platform adapters keep raw ids at their external I/O boundaries, and sandbox resource names stay raw-derived until the resource-naming migration.
The Office value is the argument, not a bag of derived strings: option
objects that used to carry a workspace root plus a conversation id plus a
state dir now carry one office field, and callers read office.dir,
office.key, office.stateDir from it. The main crossings:
| Consumer | Entry point |
|---|---|
| conversation settings | conversationSettingsPath(office), resolveConversationSettings(office) (src/settings/index.ts) |
| workspace projection | resolveWorkspaceProjection(office) (src/office/projection.ts) |
| conversation vault | vault key = officeKey(address) (src/vault/) |
| chat log | appendOfficeLog, appendBotResponseLog, readOfficeLog (log.ts); the record shape is ConversationLogMessage |
| attachments | saveIncomingAttachments(office, …) (src/adapters/shared.ts) |
officeStateDir(stateDir, address) covers migration and settings surfaces
that genuinely hold no Office.