This file explains how memory works in this project, how it is stored, how it is injected into model requests, how it influences different features, and how to reproduce the same subsystem in another project.
This is not written as a vague product note.
It is written as a portable engineering spec so another coder can rebuild the same memory behavior with the same rules, the same guardrails, and the same integration points.
Primary implementation sources:
script.jsupdate.mdguide.mdverba.md
If this memory system is implemented in another project, it should behave like a reusable context subsystem rather than an ad hoc textarea that gets pasted into prompts.
The target result is:
- the host project can store long-term user context
- the host project can switch between memory packs
- the host project can inject the correct memory into different AI tasks automatically
- the host project can keep memory helpful without letting it override current truth
- the host project can restore memory as part of a saved workspace or session
- the host project can explain, in code and in prompts, why the model answered the way it did
Important principle:
- memory is background context, not ground truth
In this project, the transcript and current runtime state are treated as the source of truth for anything current or directly observed.
The memory system in this project is a long-term context layer.
It is used to hold durable user context such as:
- preferences
- ongoing projects
- terminology
- stable background facts
- working style
- prior exported memory from another assistant
It is not a vector database in this repo.
It is not an embedding search pipeline in this repo.
It is not a hidden autonomous planner in this repo.
It is a structured, persistent, manually imported memory layer that is automatically reused by:
- AI Output tasks
- Ask Transcript
- Verba Assistant
- workspace save and restore flows
To reproduce this system faithfully, do not describe it incorrectly.
This project does not currently implement:
- semantic retrieval over chunked memories
- embedding similarity search
- ranked retrieval from a memory corpus
- automatic extraction of memory from every conversation
- a separate machine-learning intent classifier dedicated to memory routing
Its intent behavior is simpler and more controlled:
- the app knows what task the user triggered
- the app applies task-specific memory rules
- the app injects memory into prompts only as allowed by those rules
That distinction matters.
If another project wants the same behavior, preserve these guarantees:
- one active memory pack at a time
- at least one memory pack must always exist
- the default pack must be recoverable even when no packs were previously saved
- imported memory must be normalized before storage
- memory must persist across reloads
- memory must restore as part of workspace state
- AI tasks must use the active pack automatically
- different task types must use memory with different strength
- transcript and runtime state must override memory on conflict
- memory must be truncated before prompt injection when needed
- the user must be able to clear memory without breaking the pack system
- the user must be able to copy a built-in export prompt to fetch memory from another AI system
Treat the memory subsystem as these modules:
memory-store- persistent storage and restore
memory-pack-manager- create, delete, normalize, select active pack
memory-normalizer- sanitize imported memory text
memory-context-builder- build prompt-ready memory context with truncation and guardrails
memory-policy-layer- decide how strongly a task may use memory
memory-prompt-injector- attach memory to model requests
memory-workspace-adapter- save and restore memory inside workspace payloads
memory-ui-adapter- connect textareas, selects, buttons, status chips, and helper text
Another project can rename these modules, but it should preserve these responsibilities.
The current project effectively uses this shape:
type MemoryPack = {
id: string;
name: string;
raw: string;
importedAt: string;
};
type MemoryState = {
memoryRaw: string;
memoryImportedAt: string;
memoryPacks: MemoryPack[];
activeMemoryPackId: string;
outputStyle: string;
};Important behavior:
memoryRawandmemoryImportedAtmirror the currently active packmemoryPacksis the canonical listactiveMemoryPackIddecides which pack is in forceoutputStyleis not memory itself, but it is tightly coupled to how memory-aware output is produced
This project stores memory in browser localStorage using these keys:
vt_memory_raw
vt_memory_imported_at
vt_memory_packs
vt_memory_pack_active
vt_output_styleIf you port this to another project, you may rename the keys, but keep the same separation of concerns:
- full pack list
- active pack id
- active pack mirror values
- style selection that affects downstream output
A faithful port should persist:
- all packs
- active pack id
- active pack text
- active pack import time
- output style
If any of these are omitted, the behavior becomes only partially compatible with this project.
The memory system is pack-based.
The user can maintain separate context sets for different workstreams, for example:
- Client A
- Startup
- Personal
- Research
- Hiring
But only one pack is active at a time.
That is deliberate.
The app does not merge multiple packs into a single prompt.
This keeps the prompt smaller, easier to reason about, and less likely to contaminate one context with another.
When the app normalizes packs and none exist, it creates:
{
id: "memory_primary",
name: "Primary",
raw: normalizedExistingMemoryRaw,
importedAt: existingImportedAt
}This means the system never truly operates without a pack container.
Even when memory is empty, there is still a valid primary pack.
When loading packs, the project normalizes:
id- forced to string
name- forced to string, trimmed, and given a fallback name
raw- normalized through memory text cleanup
importedAt- forced to string
If you port this, preserve normalization on load, not only on write.
That protects the app from malformed saved state.
The app mirrors the active pack into top-level state.
Conceptually:
function syncActiveMemoryPackState() {
const active = getActiveMemoryPack();
if (!active) {
state.memoryRaw = "";
state.memoryImportedAt = "";
return;
}
state.activeMemoryPackId = active.id;
state.memoryRaw = normalizeImportedMemory(active.raw || "");
state.memoryImportedAt = active.importedAt || "";
}This matters because downstream consumers can read state.memoryRaw without repeatedly resolving the active pack object.
Creating a pack:
- requires a non-empty name
- generates a unique id
- adds a new empty pack
- makes it active immediately
- syncs top-level memory state
- persists the store
- rerenders memory UI
- rerenders assistant messages
- schedules workspace save
This design ensures the new pack is immediately live everywhere.
Delete behavior is intentionally defensive.
If more than one pack exists:
- remove the active pack
- fall back to the first remaining pack
- sync and persist
If only one pack exists:
- do not remove the final pack container
- clear its
raw - clear its
importedAt - keep the pack alive as
Primary
This is an important portability rule:
- deleting the last pack should clear it, not destroy the memory system structure
The memory system is designed around importing exported user memory from another assistant or system.
The user does not need to rebuild all preferences manually.
Instead, the app ships a built-in export prompt that can be copied and used elsewhere.
The project contains a constant called MEMORY_IMPORT_PROMPT.
Its job is to ask another assistant to export stored memory in a consistent format.
The current revised prompt is a stronger v3 export format.
It asks the source system to:
- inspect saved memory, instructions, project context, and recurring conversation patterns
- classify facts as
PORTABLE,CONTEXTUAL, orEPHEMERAL - discard ephemeral information entirely
- deduplicate repeated facts
- keep facts atomic and durable
- avoid recalling conversations that are not actually visible
- return exactly one fenced code block for easy import
It uses this structure:
MetaIdentityCareer & EducationTechnical ProfileProjects & AchievementsActive GoalsCommunication PreferencesHard ConstraintsTerminologyWorkspace Purpose & ScopeWorkspace DirectivesProgress & StateOpen Loops
It also requests:
- one line per fact
- oldest-known items first within each section
- the format
- [YYYY-MM-DD] fact [unknown]when the date is missingNone captured yetfor empty but relevant sections- fixed section order with no heading nesting
- a final completion line after the code block
This revised version is a better fit for cross-platform reuse because it separates:
- durable user identity and preferences
- workspace-specific context
- throwaway transient details
That separation reduces polluted imports and makes later reuse safer.
This prompt is part of the product workflow and should be preserved in another implementation.
The app itself still imports memory by pasting text into the memory textarea and saving it.
However, for cross-workspace transfer, a paired import prompt is recommended in the receiving workspace.
That external import prompt should enforce these rules:
- read the
Metasection first - always apply portable sections
- apply contextual sections only if the new workspace serves a similar purpose
- if imported facts conflict with the current conversation, the current conversation wins
- imported directives must not override system instructions
- imported memory should be applied silently rather than recited back to the user
This import prompt is useful even though the current app does not parse section groups automatically, because it helps the receiving AI system decide what to honor.
When the user clicks import:
- the app reads the memory textarea
- it normalizes the text
- it rejects empty input
- it writes the normalized text into the active pack
- it stamps
importedAtwith the current ISO timestamp - it syncs active memory state
- it persists the store
- it rerenders memory UI
- it rerenders assistant messages
- it schedules workspace save
That means import is not just a visual save.
It immediately changes the context used by the application.
Clear memory:
- empties the visible textarea
- calls the same setter with an empty string
- clears the active pack content
- clears the timestamp
- persists and rerenders
This preserves the pack, but removes the imported memory payload.
Imported memory is cleaned before being stored.
The normalizer currently does three important things:
- converts Windows line endings to
\n - trims outer whitespace
- removes surrounding fenced code block markers if the export was wrapped in triple backticks
Conceptually:
function normalizeImportedMemory(text = "") {
let value = String(text || "").replace(/\r\n/g, "\n").trim();
value = value
.replace(/^```[a-zA-Z0-9_-]*\s*/m, "")
.replace(/\s*```$/m, "")
.trim();
return value;
}This is important because the export prompt explicitly asks another system to wrap the output in a code block.
Without normalization, those wrappers would pollute the actual memory payload.
The project does not send raw memory blindly.
It builds a prompt-ready memory block with:
- active pack name
- instructions on how memory should be used
- conflict-precedence rules
- the memory content itself
- truncation when needed
Conceptually:
function getImportedMemoryContext({ maxChars = 9000 } = {}) {
ensureMemoryPackStore();
const raw = normalizeImportedMemory(state.memoryRaw || "");
if (!raw) return "";
const compact =
raw.length > maxChars
? `${raw.slice(0, maxChars).trim()}\n\n[Imported memory truncated for token control]`
: raw;
const activePack = getActiveMemoryPack();
return [
`Imported user memory from pack: ${activePack?.name || "Primary"}`,
"Use this for preferences, ongoing projects, terminology, and stable background context.",
"If transcript or runtime state conflicts with memory, trust the transcript/runtime state first.",
"",
compact
].join("\n");
}This small wrapper is one of the most important parts of the system.
It prevents memory from acting like unrestricted truth.
Memory is capped before prompt injection.
Current behavior:
AI OutputandAsk Transcriptuse up to9000charactersVerba Assistantuses up to7000characters
If memory exceeds the limit:
- only the front portion is sent
- a truncation note is appended
This means the system optimizes for stable context retention while guarding total prompt size.
A faithful port should preserve:
- deterministic truncation
- explicit truncation notice
- different limits for different surfaces when needed
The user asked how the app is able to detect and understand intent accurately.
In this project, intent is not primarily inferred through a separate memory intelligence engine.
Instead, intent comes from the combination of:
- the UI action the user triggered
- the named task that action maps to
- the transcript or message content
- the selected output style
- task-specific memory guidance
- runtime state
So the logic is closer to controlled task routing than to free-form hidden inference.
These are the main signals:
task.name- for example
ai-clean,summary,action-items, orprompt-pack*
- for example
- transcript content
- assistant thread content
- active memory pack
- output style
- runtime app settings
The project contains a dedicated function for memory usage policy:
function getTaskMemoryGuidance(taskName = "") {
if (taskName === "ai-clean") {
return "Use memory lightly for terminology, proper nouns, and writing preferences. Do not inject unrelated facts.";
}
if (taskName === "summary") {
return "Use memory strongly to prioritize what matters most to this user, their projects, and their preferred output style.";
}
if (taskName === "action-items") {
return "Use memory to interpret project context and likely priorities, but only derive action items from what the transcript supports.";
}
if (String(taskName || "").startsWith("prompt-pack")) {
return "Use memory strongly to tailor the prompt structure, requirements, context, and examples to the user's actual goals.";
}
return "Use memory when it improves relevance, but do not let it override transcript facts or invent missing information.";
}This is the clearest answer to how intent and memory are combined:
- the app already knows the current task
- each task has an explicit policy for how memory may influence the answer
That is why the system stays more accurate than a loose "always stuff memory into the prompt" approach.
Memory improves accuracy here by helping the model:
- recognize the user’s recurring terminology
- understand which projects are relevant
- format answers in the user’s preferred style
- prioritize likely important details
- avoid misreading domain-specific names
But accuracy is protected by limiting memory’s authority:
- transcript facts win
- runtime state wins
- action items must still be transcript-supported
- cleanup should not invent facts
The active memory pack is automatically reused across multiple surfaces.
AI Output tasks call the chat model with:
- the task system prompt
- the current output style instruction
- task-specific memory guidance
- imported memory context when available
- the transcript
The key structure is:
messages: [
{ role: "system", content: task.system },
{ role: "system", content: `Current output style: ${styleInstruction}` },
...(importedMemory ? [{
role: "system",
content: `${getTaskMemoryGuidance(task?.name)}\n\n${importedMemory}`
}] : []),
{ role: "user", content: `${task.user}\n\nTranscript:\n${text}` }
]This means memory is never the only system instruction.
It is layered under:
- the task definition
- the output style definition
That ordering is intentional.
The transcript-question flow is even stricter.
Its system instruction says, in effect:
- answer questions about a transcript
- use the transcript as current truth
- use imported memory only as background context
Then it adds the active memory pack as a separate system message only if memory exists.
This is one of the most important trust rules in the project.
A faithful port should preserve the phrasing intent even if wording changes:
- transcript first
- memory second
The embedded assistant also consumes memory automatically.
Its prompt context includes:
- assistant knowledge base
- runtime summary
- preferred output style
- imported memory, if present
The memory injection is explicit:
- use imported memory as long-term context
- use it for preferences, projects, terminology, and stable background facts
- do not let it override transcript text or current runtime state when they conflict
This makes the assistant feel personalized without letting it drift away from the actual current workspace state.
The assistant runtime summary also exposes memory state:
- whether memory is loaded
- which memory pack is active
- what output style is selected
This gives the assistant two useful forms of grounding:
- raw imported memory content
- current app memory status
Both are important.
The first tells it what the user tends to care about.
The second tells it what is active right now.
Memory is part of the workspace payload.
This is critical for portability.
If another project ports memory but does not include it in saved workspaces, the behavior will feel incomplete.
The workspace payload includes:
memoryRawmemoryImportedAtmemoryPacksactiveMemoryPackIdoutputStyle
During restore, the app:
- reads saved workspace data
- restores pack list
- restores active pack id
- restores output style
- normalizes packs
- syncs the active pack into top-level state
- rerenders memory UI
That makes the memory system portable across sessions, not just persistent inside one tab.
Output style is not identical to memory, but it is part of the same context-control layer.
Current style options are:
defaultconciseexecutivetechnicalfounderclient-readymeeting-notes
The app converts the selected style into a system instruction such as:
- balanced and practical
- compact and low-fluff
- executive-oriented
- precise and implementation-aware
- founder/operator brief
- polished client-facing output
- meeting-note structure
Memory and output style work together:
- memory tells the model what matters to the user
- style tells the model how to express the answer
That separation is one reason the system stays understandable.
The memory UI is not only decorative.
It communicates actual system state.
The memory area includes:
- memory pack selector
- memory textarea
- import button
- clear button
- new pack name input
- create pack button
- delete pack button
- export prompt textarea
- copy prompt button
- output style select
- helper/status text
UI status includes:
- whether memory is loaded
- active pack name
- relative import time
- imported character count
- reminder that AI Output and Verba Assistant use the active pack automatically
These signals matter because they make the memory system auditable by the user.
Current event wiring is functionally:
- pack selector change
- activate selected pack
- create pack click
- create and activate new pack
- delete pack click
- delete or clear according to pack-count rules
- copy prompt click
- copy built-in export prompt
- import click
- normalize and persist imported memory
- clear click
- clear active pack memory
- output style change
- persist style and refresh assistant/memory UI
Another project does not need the same HTML, but it should preserve these state transitions.
This is the single most important correctness policy in the subsystem.
Priority order is:
- transcript or directly observed current content
- runtime state and current app settings
- active memory pack
- generic assistant knowledge base and model prior knowledge
Memory should never be allowed to override:
- the actual transcript text
- the current selected mode
- current provider/model settings
- current uploaded files or attachments
- current conversation messages
This rule is stated in multiple prompt layers in the current project and should remain explicit in a port.
The user asked how the system "tries to combine the logic to make sense."
The answer is that it combines three different kinds of context:
- immediate evidence
- transcript, attachments, current chat message, current runtime state
- durable user background
- imported memory pack
- formatting and decision framing
- output style plus task instructions
The system does not treat all context equally.
Instead it uses a controlled hierarchy:
- first understand the immediate task
- then ground on current evidence
- then use memory to interpret terminology, priorities, and preferences
- then shape the answer in the selected style
This layered approach is what makes the behavior coherent.
If this subsystem is being moved into another project, implement it as a memory service with a narrow API.
Example interface:
type MemoryContextRequest = {
surface: "assistant" | "ai-output" | "ask-transcript";
taskName?: string;
maxChars?: number;
};
type MemoryService = {
ensureStore(): void;
listPacks(): MemoryPack[];
getActivePack(): MemoryPack | null;
setActivePack(id: string): void;
createPack(name: string): MemoryPack;
deleteActivePack(): void;
setImportedMemory(rawText: string): void;
clearImportedMemory(): void;
getPromptContext(input: MemoryContextRequest): string;
getTaskPolicy(taskName?: string): string;
serialize(): MemoryState;
restore(payload: Partial<MemoryState>): void;
};This is a better long-term design than scattering memory logic through UI handlers.
For the feature to bind cleanly into another project, the host app should provide:
- a durable storage adapter
- a task router or named task registry
- a prompt builder for each AI surface
- a workspace/session serializer
- a UI or settings layer for pack management
- a way to rerender dependent surfaces when memory changes
Without those bindings, the subsystem will exist but will not behave like it does here.
To match this project closely, memory should be injected into:
- transcript cleanup and transformation tasks
- summarization
- action item extraction
- prompt generation flows
- assistant conversation prompts
- saved workspace state
A portable version should define expected failures explicitly.
If there is no imported memory:
- do not inject a memory system message
- do not fail the task
- continue with transcript, runtime state, and task instructions only
If saved packs are malformed:
- normalize them on load
- restore at least one valid default pack
- recover active pack selection automatically
If memory is too large:
- truncate deterministically
- append a truncation note
- avoid silent overflow
If the stored active pack id is missing or invalid:
- fall back to the first normalized pack
- sync mirrors
- persist the corrected state
This repo stores memory locally in the browser.
That means:
- imported memory may contain highly sensitive user context
- anyone with access to the same browser profile may be able to inspect it
- prompts sent to providers may include memory content
A production port should consider:
- encrypted local storage
- server-side secrets handling if moved off client-only architecture
- redaction options
- memory retention controls
- provider-specific privacy policies
Those improvements are not fully implemented in this repo, but they matter for a serious deployment.
Another project should be considered memory-compatible with this one only if all of these are true:
- it supports multiple named memory packs
- exactly one pack is active at a time
- it always preserves at least one valid pack
- it normalizes imported memory text
- it stores import timestamps
- it persists memory and active pack selection
- it injects memory into AI Output automatically
- it injects memory into transcript QA automatically
- it injects memory into the assistant automatically
- it keeps transcript/runtime state above memory in precedence
- it supports task-specific memory policies
- it includes output-style interaction
- it restores memory through workspace/session restore
- it exposes enough UI or API state for the user to know what memory is active
Before calling another implementation complete, test at least these cases:
- import valid memory into an empty default pack
- create multiple packs and switch between them
- delete a non-primary active pack
- delete when only one pack exists
- restore from saved workspace with multiple packs
- use summary task with memory present
- use action-items task with memory present and verify transcript facts still dominate
- use assistant replies with memory present
- use assistant replies with memory absent
- import memory wrapped in a code block and confirm wrappers are removed
- import oversized memory and confirm truncation note appears in prompt context
- save output style, reload, and verify it is still applied
- restore invalid active pack id and verify fallback recovery
If another coder needs one sentence that captures this subsystem, it is this:
The memory system is a persistent, pack-based long-term context layer that personalizes AI behavior across the app, while remaining explicitly subordinate to current transcript evidence and runtime state.