Appearance
Private means private
A badge failed the test
A product added a lock icon to temporary chats. The UI looked private, but the same capture poller extracted facts, full-text recall indexed the messages, failure analytics recorded execution details, and scheduled tools remained available. The badge described an intention that the runtime did not enforce.
A private conversation needs a different data flow. It may read the assistant's existing memory so the interaction remains useful, but its contents do not become memory, appear in later recall, create follow-ups, emit proactive messages, or enter structural analytics.
External delivery, export, and public app publishing are blocked by default. If the product permits one, it must be an explicit, confirmed boundary crossing that creates a separate non-private artifact with its own retention policy.
Privacy is a property of every producer and consumer that touches the conversation.
Concept map
text
normal topic
messages -> capture -> daily notes -> consolidation
-> recall index -> later conversations
-> agent events -> structural reports
-> follow-ups -> push and channel delivery
private topic
messages -> local thread context -> expiry deletion
x capture
x recall source
x memory, goal, and follow-up tools
x agent events
x external delivery, export, or public publishing by default
-> confirmed crossing -> new non-private artifactThe crossed paths are the contract. Tint, badge, and expiry copy only explain it.
Define the boundary first
A useful private mode must answer five questions.
What can it read? In an incognito model, the assistant may read existing persona and user memory. The user still gets the same assistant. A blank-slate mode is a separate product choice and should not be implied.
What can it write? It can write messages and tool operations needed for the live private thread. It cannot write memory documents, goals, reminders, event watches, proactive records, or analytics events.
Where can it be retrieved? The current private thread may use its own recent history and summary. Global recall queries must exclude it at the SQL boundary.
Where can it be delivered? Not outside the private thread by default. No Telegram mirror, email, push generated from private activity, or shared inbox excerpt. Export or public app publishing must either be unavailable or require confirmation that names the destination and explains that the result is a new non-private artifact. That artifact must not silently inherit the private label.
How long does it live? The user may choose an expiry. A sweeper deletes the conversation row, and foreign-key cascades remove its messages, operations, artifacts, runs, and outbox records.
These answers should be testable without opening the UI.
Tool absence beats tool refusal
Prompt instructions are weak access control. Telling a model not to call remember still exposes a callable function that another prompt fragment can request.
Select the tool set before the provider sees it:
ts
function toolsFor(kind: "topic" | "private", mode: "chat" | "isolated") {
const tools = allTools()
.filter(tool => mode !== "isolated" || tool.name !== "run_code");
if (kind === "private") {
return tools.filter(tool =>
!["remember", "recall", "forget",
"list_goals", "set_goal", "update_goal",
"schedule_followup", "watch_event",
"list_followups", "pause_followup"].includes(tool.name)
);
}
return tools;
}The exact list should come from capabilities or tags in a larger system. The important point is timing. The provider never receives unavailable tool definitions.
The private system line should still explain the mode so the assistant does not offer actions it cannot perform. That message improves conversation quality. It is not the enforcement mechanism.
Change each data path
Message insertion should mark assistant replies in private threads as skipped for capture. The capture poller should also join against conversation kind. Two checks protect against a future caller forgetting one.
Recall queries must include conversation.kind <> 'private'. Filtering after retrieval is too late because private snippets may already have reached application memory, logs, or model context.
Agent runs should omit their analytics context for private conversations. The event writer then has no valid route to insert protocol, tool, provider, or terminal events. A private label on an analytics row is not sufficient. The row should not exist.
Delivery creation should reject private conversation ids. The database write that creates channel deliveries belongs in the same transaction as normal assistant messages, but its query must exclude private kinds. Binding a private thread to a channel should also fail at the mutator boundary.
Export and public-publish tools should be absent by default. An allowed crossing should use a separate confirmed operation that writes a non-private artifact rather than relabeling the thread.
Proactive engagement should select only normal topic threads. Private sessions should never become candidates, even if recent and active.
Finally, backup and observability policies need review. Expiry in the live database does not erase a row from an existing backup. In the reference implementation, Postgres backups exclude agent-event and mini-app-revision data, but they do not exclude private conversations or messages.
Local backup retention defaults to 14 days as an operational example, not a privacy standard. The backup script does not prune remote copies, so off-site retention must be configured at the destination.
Restoring a snapshot restores every private row present when that snapshot was taken, including a thread that expired later in the live database. A stronger promise requires filtered or cryptographically erasable backups plus restore-time enforcement. Describe that stronger behavior as a requirement until it exists.
Terms for review
- Data flow is the full path through collection, processing, storage, retrieval, and delivery.
- Tool availability is the callable capability list sent to the model.
- Read memory means existing user context may enter the private turn.
- Write memory means the private turn may affect later turns. This must be false.
- Retrieval exclusion prevents private content from becoming a future recall source.
- Expiry is scheduled deletion of the owning conversation and its dependent records.
- Delivery is any copy sent outside the private thread.
- Boundary crossing is a confirmed export or publication that creates a separately governed non-private artifact.
Use these terms in tests and product copy. "Private" alone is too vague for engineering review.
Decisions and rejected alternatives
The main decision is incognito rather than amnesia. Existing memory remains readable, while new private material does not alter later assistant behavior. Ordinary delivery and publication stay blocked; any permitted export is an explicit transition to a new non-private artifact.
The second decision is defense in depth. Tool selection, writer checks, query predicates, delivery filters, analytics omission, and deletion each enforce part of the promise. No single mode flag can protect every path after the system grows.
The third decision is to let database cascades perform expiry cleanup. Handwritten deletion lists drift when new child tables appear. Foreign keys keep ownership explicit.
Rejected alternatives include a visual badge with ordinary backend behavior, a prompt-only prohibition, and post-processing that deletes private analytics later. Each allows forbidden data to exist, even if briefly. Another rejected design stores private messages encrypted but still feeds them to capture and recall. Encryption at rest does not fix an incorrect processing path.
Failure modes
- Private replies are marked pending for memory capture.
- Recall excludes private daily notes but still searches private messages.
- A goal tool is hidden while scheduling tools remain exposed.
- Structural error events retain private conversation or operation ids.
- A channel delivery row is created before a later private check drops it.
- Expired threads leave orphaned operations or runs.
- A private summary enters a global prompt.
- Search indexes or backups retain content beyond the stated lifetime.
- A restore reintroduces a private thread that had expired after the snapshot.
- A public app is published from private content without a named confirmation boundary.
- The UI says "nothing is remembered" while existing memory is still read.
- A user expects blank slate behavior from an incognito design.
The final two failures are copy failures with technical consequences. Say exactly what the mode does.
Field checklist
Private mode limits unsolicited behavior. The next chapter designs that behavior for ordinary threads with budgets, quiet hours, cooldowns, triage, and a legitimate decision to stay silent: Proactive with a budget. For the records private mode must withhold, see Goals, todos, and follow-through.