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.
---
transcripts.db actually is — read this before planning a moveIt 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:
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.
bots/leo.mjs:2363 imports recallProfileMemories, getRecentContext, getEntityProfile. Same in native-bot.mjs, openjarvis.mjs,
gemini-live-bridge.mjs, kai-dream.mjs, social-scoring.mjs.
mergedThread() — REAL but owner-only. command-center-server.mjs:4162 merges the DB 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.
transcript-memory.mjs:688 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.
/api/transcripts and the Discord-429 fallback.person-recognition.mjs does NOT read transcripts. recentMemories: [] — the join is Phase 3, unbuilt (:150). It touches user_facts only, indirectly.
distill_from_bitnet.py has zero matches for transcript or .db.
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.
---
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.
/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" |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.
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.
> **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.
---
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.
transcripts.db is shared by five subsystems; a move is a five-subsystem migration.readable by another.