zudo-text

検索したい単語を入力

いつでも検索バーを開ける

Workspace Memory

Workspace Memory gives the workspace Assistant a small, lasting, human-editable memory: exactly one plain-Markdown document per workspace, edited in a standalone Memory frame and supplied in full to eligible Assistant turns. This page is the contract for that document, its policy, how the Assistant reads it, how an automatic update is authorized, and what Clear and Undo do and do not promise (epic #7564; #7565 foundation, #7566 frame, #7567 runtime).

The document

FactContract
Path.zudotext/memory/MEMORY.md, defined once as MEMORY_DOCUMENT_PATH in @takazudo/workspace-memory.
StorageAn ordinary encrypted cloud workspace document: same E2EE blobs, files/file_versions rows, history and sync as any note. It is a workspace namespace, not a local filesystem mirror.
FormatFree Markdown. Any Japanese or English headings; no required sections, frontmatter schema, index, or second file.
LifecycleA never-created document reads as empty and is created by its first authorized write (base version 0). A deleted document (tombstone) is reported as deleted; only an explicit human Save against the tombstone version brings it back, and the automatic route never revives it.

The memory text exists only in that encrypted document. Every operational table below stores policy fields, ids, versions and ciphertext content hashes — never a plaintext copy and never before/after diffs. Changes in the frame reconstructs history from the document's own encrypted versions.

Modes are workspace policy

workspace_memory_policy (D1 migration 0049) holds one row per workspace: mode, policy_revision, clear_generation. A missing row means Read only, revision 0, generation 0.

ModeRecallWrites by the Assistant
OffNone. Generic tools cannot reach the document either.None.
Read only (default)The complete current document, when AI access permits.remember_in_memory returns a version-bound proposal; only a human Apply writes, through the human save path.
Auto rememberThe complete current document.One bounded, fenced update per admitted user turn.

The mode is changed with PUT /api/v1/workspaces/:id/memory/policy ({ mode, expectedRevision }, compare-and-set; a stale revision is 409). Every change bumps policy_revision, which retires every admission taken under the previous one. Changing the mode needs only normal authenticated workspace access: it never enables workspace AI, opens a key session, or trusts workspace skills, and human editing works in every mode.

Human writes are explicit online saves

BackendAPI.memory (packages/backend-bridge/src/workspace-memory-api.ts) does not use the background outbox. Save, Clear and Undo are each one direct upload of the encrypted document with X-Base-Version:

  • the acknowledged version is exact, so the renderer knows precisely which revision the server holds;

  • a failure (offline, auth, conflict) changes nothing and keeps the draft;

  • nothing remains queued, so no delayed write can refill memory after a Clear.

Undo restores an older revision's content as a new revision against the current version, so it cannot overwrite newer text and never rewinds the Clear generation or the policy revision.

Acknowledgement and freshness

memory-save-state.ts tracks this renderer's own Saves per workspace. Each Assistant send waits (up to 5 s) for an in-flight Save, then sends X-Memory-State:

ValueMeaningRecall
saved:<v>This renderer last saved or loaded revision v.Server revision ≥ v is used; an older one is withheld as stale.
pendingA Save is still in flight.Withheld.
conflictThe user's latest Save lost to a newer revision.Withheld.
noneNothing known.Server revision is used.

An unsaved editor draft is never reported: it is not saved memory.

Recall: attached at the model binding

The pinned Flue 2.0.3 composes a submission's first system prompt before useAgentStart runs, so memory loaded there and supplied through useInstruction reaches the model one call late, and every change appends a System instructions updated. signal plus an extra model turn. workers/agent-server/test/memory-context-probe.test.ts records both, and proves the adopted alternative.

Memory is attached by the authorized model binding (authorized-ai-provider.ts → memory-recall.ts) immediately before every model call: it reads the conversation's latest admission and its delivery credential, fetches GET /api/v1/automation/workspaces/:id/memory?purpose=recall, and appends a block to the outgoing system message. Consequences:

  • a saved edit, a Clear, or Off reaches the very next model call — no fresh conversation context is needed;

  • memory text never enters Flue's durable records, so a cleared memory is not replayed from the transcript;

  • initialData stays { userId, workspaceId }; no mutable memory is frozen there.

The block states that memory is background context, not instructions: it cannot grant permissions, enable skills, change tools or name write targets, and the current message and freshly read notes take precedence. A forged closing marker inside the text is neutralized. When memory is withheld (pending, conflict, stale, oversized, deleted, unavailable, no workspace access, or a newer turn's credential) the model receives an explicit "not provided because …" note — never an older copy. Off and empty documents add nothing.

Each recall records the inspected status and revision on the admission row. GET /api/v1/agents/assistant/:conversationId/memory-receipts returns those receipts (ids, versions, statuses — no text) so the Assistant panel can show which revision an answer used and open that exact revision in Changes.

Size bound

MEMORY_RECALL_MAX_BYTES = 8192 (UTF-8). Measured with the Llama 3 tokenizer against the real request: the complete assistant system prompt plus tool schemas is about 2,600 tokens; mixed Japanese/English Markdown memory costs about 300 tokens per KiB (pure Japanese prose about 225), so 8 KiB is about 2,400 tokens — roughly a tenth of the model's 24,000-token window, leaving room for conversation and tool results before Flue's compaction threshold.

Human text over the bound is kept intact. It is excluded from recall (status oversized, explained to the user and the model), and automatic capture is refused until the user shortens it. Nothing is truncated, split, or compacted automatically.

Automatic capture: admission and the commit fence

Admission

For every send, agent-server's ingress writes a workspace_memory_admissions row before the turn reaches Flue (memory-admission.ts). It freezes the authority the turn was admitted under: mode, policy revision, Clear generation, AI consent revision, key session id, the client's memory state, the original 15-minute expiry, and a fresh credential_nonce that is written into the conversation's agent_delivery_credentials slot at the same time. A resend with the same idempotency key reuses the original admission and nonce.

The admission id reaches the agent as a server-set signal attribute. ZudoAssistant reads it from useDelivery() — the model never supplies it, a path, or a credential. A recovered older submission keeps its own (by then retired) admission.

The tool

remember_in_memory (agents/memory-tools.ts) takes at most five exact edits — add (optionally under a heading), replace, remove — each one line of at most 500 bytes, applied by applyMemoryChanges. replace/remove must match exactly once, so a stale or ambiguous target fails instead of rewriting unrelated prose; an add already present is a no-op. In Auto it commits with Idempotency-Key = memory:<admissionId>; in Read only it returns a proposal; when Off it does nothing. A version conflict leaves the newer human text untouched and returns the same change as a reviewable proposal against it — never an automatic rebase. The UI says “Remembered” only for the committed receipt.

The commit fence

PUT /api/v1/automation/workspaces/:id/memory is the only automatic writer of the namespace. After the usual scope, idempotency and key-session checks, it writes the encrypted document with expectedVersion (or create-only for a never-created document) and requireLive (no tombstone revival), and appends to the same D1 batch a fence row in workspace_memory_commit_fence whose CHECK (ok = 1) column is computed from one predicate over the admission:

  • owner and workspace match; the admission is unconsumed and within its original expiry; it was admitted in Auto;

  • the current policy is still Auto with the same policy revision and Clear generation;

  • workspace AI is still enabled with the same consent revision;

  • the conversation's delivery credential still carries the admission's nonce (a later turn overwrote it otherwise) and is unexpired;

  • the presented key session is the admission's, and it has no row in key_session_revocations.

Any false predicate violates the CHECK and rolls the write back with it. The same batch marks the admission committed, so a second logical capture in the turn is refused and a transport retry with the same body replays one receipt with no extra revision.

Linearization point. The D1 batch. A mode change, Clear, consent change, newer delivery credential, or explicit key-session revocation that commits before it retires the capture; one that commits after it sees the capture as already done. Workspace keys live only in the KeySessions Durable Object's RAM, which no D1 predicate can read, so the route revalidates the key session against the Durable Object immediately before the batch, and DELETE /api/v1/automation/key-sessions/:id records a durable key_session_revocations row before discarding the keys, which the predicate does read. Idle eviction is not a revocation: a capture whose session was evicted fails earlier at key-session resolution.

Clear

A successful Clear writes an empty revision with X-Base-Version and X-Memory-Clear: 1. The human file route appends one statement to that write's batch that advances clear_generation only if the batch's own file_versions row exists — the empty revision and the generation advance are one transaction. The mode is preserved. Every admission taken under the old generation is retired across all conversations, and Read only proposals carry the generation they were made under, so applying one after a Clear is refused.

If another write wins first, the base version no longer matches: nothing is written and the generation does not move. The frame keeps the newer text and asks for an explicit retry. Before clearing, the frame resolves an unsaved draft through the usual Save / Discard / Cancel decision and waits for an in-flight Save; a retained draft in another frame cannot refill memory later because its Save carries the old base version and conflicts.

Genuinely new user evidence after a Clear is eligible for capture while Auto remains selected, because the next admission records the new generation.

Clear has an honest boundary. It clears the canonical saved memory and fences older automatic work. It does not delete conversation history, and a model can still see what was said earlier in an open conversation.

The memory namespace is fenced

  • Generic automation PUT/DELETE /documents, POST /documents/move and POST /documents/restore reject every spelling of .zudotext/memory/ (case, ./, empty segments, compatibility forms) with MEMORY_NAMESPACE_RESERVED — for the Assistant and for external MCP clients alike. External clients get no memory API in v1.

  • The Assistant's list_documents and search_documents hide the namespace, and read_document, create_note and propose_edit refuse it without making a request, so Off cannot be bypassed by an ordinary read.

  • Human document management in the app is unchanged.

  • A turn sent through the MCP server's ask_zudo_agent is an ordinary Assistant turn: it is admitted by the same ingress and follows the same policy. It sends no X-Memory-State, so it uses the server's current revision.

Skills and authority

Memory is context text. It does not load, enable, or replace skills, and the workspace-skills trust setting is independent of the memory mode. Instructions found inside memory or ordinary notes cannot widen write targets: the only automatic write target is fixed by the server route, and the authority is the server-minted admission.

Tests

BoundaryFile
Policy CAS, lazy create, receipts, retries, human-vs-capture races, Clear CAS and generation, fence rollback, revocation, tombstone, size, namespace fenceworkers/sync-server/test/workspace-memory.test.ts
Flue probe (rejected and adopted seams) through the real runtime and ZudoAssistantworkers/agent-server/test/memory-context-probe.test.ts
Tool modes, conflict-to-proposal, once per turn, admission freeze/resend, withheld recall, generic tool refusalsworkers/agent-server/test/workspace-memory.test.ts
Explicit online saves, conflict/offline state, Clear marker, arm changes, mock semanticspackages/backend-bridge/src/workspace-memory-api.test.ts
Bounded edit semantics, namespace matchingpackages/workspace-memory/src/index.test.ts