← all documents · raw markdown · 9 KB

TRANSCRIPT STORAGE — GOAL DOC

Status: SCOPED · Phase 1 SHIPPED v9.10.442 · Phases 2-4 NOT BUILT

Owner: Ryan (nastermodx)

> Owner ask, verbatim: *"when I say don't need transcripts is when user voice talks with AIs

> we don't need to see the voice script, but it can be in the server placed on the user's

> account, their cloud storage. and then when calling an AI, the AI retrieves the transcript

> with that user on their cloud storage account in the area it finds it and then it

> continues... so transcripts still happen for memory yes, but not needing to see a

> transcript under the transcript channels, as that is Discord stuff that we are slowly

> getting off from."*

Transcripts are not being deleted. They are the agent's memory of a person. What changes

is where they live and whether they are surfaced.

---

1. What transcripts.db actually is — read this before planning a move

It is not a transcript store. It is a shared multi-subsystem SQLite file at

tools/oracle-discord/transcripts.db. Five modules open it read-write:

| Module | Tables |
|---|---|
| shared/transcript-memory.mjs | transcript_fts, user_profile_memories, message_meta, entity_facts |
| shared/epistemic-vault.mjs | epistemic_cells |
| shared/lattice-mempalace.mjs | user_profiles, channel_profiles |
| shared/user-warehouse.mjs | user_facts, user_fingerprints |
| shared/kai-dream.mjs | (opens the same path) |

Anyone who says "move transcripts.db" is moving four other subsystems with it — the

epistemic vault, the mempalace user/channel profiles, and the user-warehouse fact store.

This is the single biggest risk in the whole task.

The dashboard server only ever opens it READ-ONLY (command-center-server.mjs:472,

{ readonly:true, fileMustExist:true }), and every reader is guarded — if the file vanishes

the dashboard degrades, it does not crash. That read/write split is the clean seam.

Confirmed dependencies

getRecentContext, getEntityProfile. Same in native-bot.mjs, openjarvis.mjs,

gemini-live-bridge.mjs, kai-dream.mjs, social-scoring.mjs.

half only when isOwnerCaller (the v9.10.365 isolation fix). Guests already get the

JSON half only, and the whole merge is inside a best-effort try/catch.

offloadToEngine() POSTs to /api/bulk-ingest. Its only caller is runRetention()

(:880) — transcript text reaches the lattice *only as rows are about to be pruned*, not

on ingest. Fails soft; retention defers deletion when the engine is unreachable.

Denied — do NOT plan around these

Phase 3, unbuilt (:150). It touches user_facts only, indirectly.

transcript or .db.

Voice transcripts are already per-user scoped

oracle-live-voice.mjs:399 — ` const _thread = oracle-os-voice:${_speakerId} `, and the

v9.10.365 comment records that this line *used to* write every caller's speech into the

owner's record. The per-user requirement is already met for voice. Unidentified callers

land in a neutral bucket, never the owner's.

---

2. "Their cloud storage" — what that concretely is today

Investigated rather than assumed. The honest answer is **the thing the owner is describing

does not exist yet**, and the nearest two candidates each have a disqualifying property.

| Candidate | Real? | Per-user? | Location | Verdict |
|---|---|---|---|---|
| /api/storage/*shared/drive-storage.mjs | Yes, wired (8 call sites) | Folder-path isolation KAI-Server/users/<userId>/ | Google Drive | Blocked — see below |
| shared/workspace-vault.mjs + /api/vault/* | Yes, wired, tests pass | Yes, genuinely | Local disk state/workspace_vault_blobs/ | Blocked — see below |
| state/<subsystem>/<userId>.json | Yes | Yes | Local disk | Works, but it is not "cloud" |
| Per-user Google Drive credentials | NO — do not exist | — | — | Scaffolding gap |

Two findings that change the shape of this task:

1. There is no per-user Google Drive credential anywhere in this codebase.

/api/integrations/google/* is real per-user OAuth with an on-disk token store — but its

scopes are openid email profile only, no Drive scope. And drive-storage.mjs never

imports it; it reads GDRIVE_* env vars for one shared service account. The two

Google systems are disconnected. "The user's own cloud storage" would require adding

Drive scope to the per-user OAuth and re-pointing drive-storage at per-user tokens.

That is a real piece of work, not a config change.

2. Google Drive storage is OFF by default and has no offline path. isDriveEnabled()

requires three env vars; without them every /api/storage/* call returns 503 today.

There is no local cache and no fallback — downloadFile is a live drive.files.get.

The blocker that matters most: an AI cannot read a zero-knowledge vault

workspace-vault.mjs is per-user, local, and properly designed — **the client holds the

keys and the server stores ciphertext only.** That is exactly why it cannot be the memory

store: an agent running server-side has no key and cannot decrypt it. Using the vault for

transcripts would mean either giving the server the plaintext key (destroying the vault's

entire security property) or the AI never being able to read the memory. Neither is

acceptable. This must not be papered over.

Honest risk statement, as requested

> **Moving agent memory to Google Drive makes every conversation depend on an external

> service being reachable.** Today, everything an agent uses for memory mid-conversation

> (transcript_fts, entity_facts, user_facts) is local SQLite and survives a network

> outage. Drive has no local mirror. **An AI that cannot reach its memory mid-call is a worse

> failure than one with local memory** — it would either stall or, worse, continue while

> having silently forgotten the person.

>

> Therefore any move MUST be local-first: local SQLite stays the read path an agent uses

> live, and cloud is a *replica* for durability and portability, not the primary. If the

> owner wants the file genuinely off the server, the fallback has to be an explicit, visible

> degraded state ("I can't reach my memory of you right now"), never silent amnesia.

---

3. Phased plan

Phase 1 — hide the Discord-era surface. ✅ SHIPPED v9.10.442.

Removed 'transcript' from the channel rail order array (oracle.html). The rail section

is gone; channels, writes and memory are all untouched. Guests never saw it anyway.

Deliberately did NOT touch #transcript (the shared message pane for channels AND DMs) or

view-transcripts (the Messages page) — three different things share that word and only the

rail group is safe.

Phase 2 — stop voice transcripts polluting the channel list. NOT BUILT.

Because the per-caller thread string is used as channel_id, every voice caller creates a

discovered "channel" in dynamicChannels() (command-center-server.mjs:559). Filter

oracle-os-voice:* out of channel discovery. Small, self-contained, no data migration.

Phase 3 — per-user export/replica. NOT BUILT.

Add a per-user transcript export keyed by currentContentUser().id, written to that user's

storage area, on a schedule or on call-end. Local SQLite remains the live read path. This

is the first phase that touches data, so it inherits the history-migration discipline:

backups first, counts reconciled, verified through the real read path, idempotent, reversible.

Needs an EXACT GUEST_API_EXACT entry if it adds a route.

Phase 4 — retrieval-on-call. NOT BUILT.

The agent pulls that user's transcript from their storage at call start and continues from

it. Only worth building after Phase 3 proves the replica is trustworthy, and only with the

local-first fallback above.

Prerequisite for Phases 3-4, owner decision required: which backend? Enabling the shared

Drive account (3 env vars) is quick but is *the operator's* Drive, not the user's. Genuine

per-user Drive means adding Drive scope to the existing per-user OAuth — more work, and it

matches what the owner actually described.

4. Constraints

readable by another.