Capture Suggestions¶
Capture suggestions are draft memories proposed by an agent before anything is written to storage.
They exist to make Nuzo useful without turning memory into hidden state.
Decision¶
Nuzo starts capture as host or agent behavior, not as a new persisted memory state.
In the MVP, a host integration may detect a memory-worthy statement and
prepare a draft, but persistence still routes through explicit confirmation
after user approval. Confirmed capture uses memory.confirm_capture, which in
turn routes writes through the canonical remember and update behavior.
conversation context
-> candidate detection
-> memory.suggest_capture
-> validated memory draft
-> user confirm / edit / reject
-> memory.confirm_capture
There is no silent write path.
Candidate detection is outside Nuzo core. The host or agent may decide that a
message looks durable, but the Nuzo MCP server provides memory.suggest_capture
as the read-only validation boundary before any confirmation prompt is shown.
The tool validates the draft, normalizes fields, runs secret and scope policy,
and reports exact active duplicates without writing storage or audit state. In
the 0.6.0 bounded mode it also returns versioned relationship evidence; the
default remains the exact-only 0.5.0 behavior for compatibility.
Candidate Criteria¶
A statement is memory-worthy when it is:
- stable beyond the current turn;
- useful in future sessions;
- specific enough to act on;
- safe to store;
- not already represented by an equivalent active memory.
Common memory-worthy categories:
| Category | Example |
|---|---|
| Preference | "I prefer concise final answers." |
| Project decision | "Nuzo should keep memory business logic in packages/core." |
| Recurring instruction | "For this repo, run MkDocs strict before pushing docs changes." |
| Durable fact | "The docs domain for this project is nuzo.com.br." |
| Workflow note | "Use GitHub Issues as the execution tracker for Nuzo." |
Not every useful sentence should become memory. One-off task details, temporary errors, command output, and speculative notes should stay in the current conversation unless the user explicitly asks to save them.
Candidate Decision Rules¶
Candidate detection is a host or agent heuristic. It must decide whether to
call memory.suggest_capture; the core service only validates the proposed
draft. Use this decision table before proposing a draft:
| Signal | Action | Example |
|---|---|---|
| Durable preference, project decision, recurring instruction, stable fact, or repeated workflow note | Suggest a draft through memory.suggest_capture |
"For Nuzo, always use GitHub Issues for executable work." |
| The scope is unclear, mixed between user/project/team, or likely host-specific | Ask a clarifying question before calling memory.suggest_capture |
"Remember that for me, or only for this repo?" |
| The statement contains credentials, private file contents, sensitive customer/payment details, raw logs, one-off task state, or speculation | Do not call memory.suggest_capture; ask for a sanitized durable memory only if useful |
"My token is ghp_...." |
Allowed candidates should be short, affirmative memories. Avoid storing long conversation excerpts. Rewrite only to remove filler; do not invent stronger claims than the user stated.
Hosts should suggest a small set of lowercase topical tags from the durable
subjects explicitly present in the statement. For example, a recurring
Cloudflare workflow may use cloudflare, docker, and workflow. Add
autoload only when the memory should be present at every session start in its
scope; ordinary topic-specific memories should be recalled when a prompt
matches their content or tags.
Blocked candidates are not "rejected memories". They should leave no memory, draft record, audit event, or hidden note unless the user explicitly provides a safe replacement and confirms it.
Draft Format¶
A capture suggestion should be shown as a small, editable draft:
{
"content": "The user prefers concise final answers.",
"kind": "preference",
"scope": "user:default",
"tags": ["workflow"],
"source": "codex:capture-suggestion",
"confidence": 0.72,
"reason": "The user stated a recurring response style preference.",
"requires_confirmation": true
}
Draft fields:
| Field | Required | Notes |
|---|---|---|
content |
yes | The exact memory text proposed for persistence. |
kind |
yes | One of preference, project_decision, fact, instruction, or note. |
scope |
yes | Defaults by the scope rules below. |
tags |
yes | Short lowercase tags for filtering. |
source |
yes | Host and path that produced the draft, such as codex:capture-suggestion. |
confidence |
yes | Confidence in the draft, not permission to save it. |
reason |
yes | Short explanation shown to the user. |
requires_confirmation |
yes | Must be true for inferred writes. |
The draft is not a stored memory. It has no memory ID until memory.remember succeeds.
Drafts returned by memory.suggest_capture always include
requires_confirmation: true and memory_writes: false in the tool response.
User Flow¶
Capture suggestions must offer explicit outcomes:
| Outcome | Behavior |
|---|---|
| Create | Call memory.confirm_capture with decision: "create" and confirm: true. |
| Update | Call memory.confirm_capture with decision: "update", target_memory_id, expected_revision, and confirm: true. |
| Keep separate | Call memory.confirm_capture with decision: "keep_separate" and confirm: true. |
| Clarify | Ask a clarifying question and call memory.confirm_capture with decision: "clarify" only if a machine-readable no-write result is needed. |
| Reject | Do not write anything. The rejection itself is not stored unless the user explicitly asks. |
The confirmation prompt should be direct:
Save this Nuzo memory?
Content: The user prefers concise final answers.
Kind: preference
Scope: user:default
Tags: workflow
Reason: The user stated a recurring response style preference.
Create, edit, keep separate, clarify, or reject?
Agents may batch multiple suggestions, but each suggested memory must remain inspectable and individually rejectable.
Duplicate Handling¶
By default, memory.suggest_capture checks active memories in the same scope
before a host asks the user to save a draft.
For the MVP, duplicate detection is exact and conservative:
- trim leading and trailing whitespace;
- collapse repeated whitespace to one space;
- compare content case-insensitively;
- ignore archived memories;
- ignore tags, kind, source, and confidence.
When a duplicate exists, the response status is duplicate, memory_writes is
still false, and the existing memory is returned for display. Hosts should
normally show the existing memory and skip the save prompt unless the user
explicitly asks to create a separate memory.
Relationship Evidence Contract¶
Relationship analysis is opt-in so existing clients cannot silently
misinterpret a new decision as the legacy ready result.
Relationship meanings are normative:
| Relationship | Contract meaning |
|---|---|
exact_duplicate |
Draft content equals an active same-scope memory after the existing case-insensitive, whitespace-collapsed normalization. |
update_candidate |
The draft appears to revise the same durable assertion, preference, decision, or instruction, such that keeping both active would be contradictory or misleading. |
related |
The draft and an active same-scope memory share durable subject matter, but both may remain true and independently useful. |
independent |
Exhaustive bounded retrieval found no active same-scope candidate meeting the documented evidence threshold. |
uncertain |
Evidence is weak, conflicting, ambiguous between relationships, or non-exhaustive in a way that could change the decision. |
These relationships describe evidence, not truth. In particular,
update_candidate does not establish that the draft is newer or correct, and
independent does not authorize creation. Policy rejection is a separate,
terminal outcome and is never represented as a relationship.
The core input adds:
type CaptureRelationshipMode = "exact" | "bounded";
interface SuggestCaptureInput extends RememberMemoryInput {
reason: string;
relationshipMode?: CaptureRelationshipMode;
}
Omitting relationshipMode means exact and preserves the 0.5.0 result
shape. bounded requests the new relationship fields. At the MCP boundary the
same input is named relationship_mode.
The bounded core result adds these contracts:
type CaptureRelationship =
| "exact_duplicate"
| "update_candidate"
| "related"
| "independent"
| "uncertain";
interface CaptureRelationshipCandidate {
memory: MemoryRecord;
matchedTerms: string[];
matchedTags: string[];
reason: string;
}
interface CaptureRelationshipEvidence {
version: 1;
primaryMemoryId: string | null;
candidateLimit: 20;
returnedLimit: 3;
evaluatedCount: number;
searchExhaustive: boolean;
evidenceTruncated: boolean;
reason: string;
candidates: CaptureRelationshipCandidate[];
}
interface BoundedCaptureSuggestionResult {
status: "ready" | "duplicate" | "review";
memoryWrites: false;
requiresConfirmation: true;
draft: CaptureSuggestionDraft;
duplicate: MemoryRecord | null;
relationshipMode: "bounded";
relationship: CaptureRelationship;
relationshipEvidence: CaptureRelationshipEvidence;
}
Exact-mode results retain the existing CaptureSuggestionResult fields only.
Bounded mode uses this status mapping:
| Relationship | Status | Primary memory | Host behavior before any write |
|---|---|---|---|
exact_duplicate |
duplicate |
Required | Show the existing memory; default to no write. |
update_candidate |
review |
Required | Show current and proposed content; offer a confirmed update. |
related |
review |
Required | Explain the relation; ask whether to keep a separate memory. |
independent |
ready |
null |
Offer confirmed creation. |
uncertain |
review |
null |
Ask for clarification; do not offer a default write. |
status remains for compatibility and coarse control flow. Bounded clients
must use relationship for the decision and must never treat review as
creation-ready.
Evidence Rules And Limits¶
- Policy validation and scope authorization run before candidate lookup. A blocked candidate returns the existing structured policy error, not a relationship.
- Candidate lookup uses only active memories in the resolved target scope. It
does not include
user:default, another project, or any other scope unless that scope is itself the explicit authorized target. - Exact normalized duplicate lookup is deterministic and happens before ranked evaluation. It must not become a best-effort result hidden by the ranked candidate cap.
- The built-in SQLite adapter stores the normalized exact key and resolves it through an active same-scope partial index. This lookup remains exhaustive at every supported scope size.
- Relationship retrieval is exhaustive for built-in SQLite scopes with at
most 100 active records. Larger scopes use an indexed FTS prefilter that
returns at most 20 records and sets
searchExhaustive: false, even when the prefilter returns fewer than 20 rows. This prevents a best-effort lookup from claiming independence. - Ranked evaluation considers at most 20 candidates and returns at most 3, ordered from strongest to weakest evidence. Equivalent evidence is ordered by memory ID ascending so repeated evaluation is deterministic.
- Each returned candidate includes the complete authorized memory record, at most 8 matched terms, at most 8 matched tags, and a reason no longer than 1,000 characters.
- The top-level reason is also limited to 1,000 characters.
evaluatedCountis between 0 and 20.candidatescontains between 0 and 3 items.evidenceTruncatedis true when qualifying evidence was omitted from the returned list.searchExhaustivesays whether candidate retrieval proved that no additional candidate could change the decision. Hitting the evaluation limit normally makes it false.primaryMemoryIdequals the first candidate's memory ID forexact_duplicate,update_candidate, andrelated.independentrequiresprimaryMemoryId: null, an empty candidate list,searchExhaustive: true, andevidenceTruncated: false.uncertainrequiresprimaryMemoryId: null. It is mandatory when evidence is ambiguous or a non-exhaustive search cannot safely establish independence.- An exact duplicate short-circuits broader classification. The legacy
duplicatefield and bounded primary candidate refer to the same memory. Its bounded evidence reports one evaluated and returned candidate,searchExhaustive: true, andevidenceTruncated: false.
The evidence version starts at 1. Additive fields may be introduced within
that version, but changing relationship meaning, limits, required fields, or
status mapping requires an explicit contract and versioning decision.
The MemoryStore.findCaptureCandidates port capability is optional for
compatibility with existing library adapters. When absent, core falls back to
the legacy exhaustive same-scope list. Custom adapters can adopt the new
capability independently, but they must return searchExhaustive: false
whenever their prefilter cannot prove that no omitted record could change the
decision. Core filters archived and cross-scope adapter results, caps exhaustive
results at 100 and non-exhaustive results at 20, and degrades completeness to
false when an adapter violates those invariants.
Compatibility And Failure Safety¶
- Existing clients omit relationship mode and continue receiving only
readyorduplicatewith the legacyduplicatefield. - New clients request bounded mode and require
relationshipMode: "bounded"in the core result orrelationship_mode: "bounded"in public JSON. - If a bounded request reaches an older server, an unknown-input error or a
missing response marker is treated as unsupported. The client must clarify
or fall back to explicit exact-only behavior; it must not assume
independent. - Relationship output does not create a memory, append an audit event, change
a revision, update
last_used_at, or persist a draft. - Relationship evidence cannot call or authorize
memory.rememberormemory.update. Those remain separate, explicitly confirmed operations. - No relationship result is retained for analytics, telemetry, or later background processing.
Update Handling¶
Some capture candidates are not exact duplicates, but they clearly change an existing memory. Hosts should treat these as update candidates instead of creating unbounded new memories.
Use this decision table:
| Candidate relationship | Host behavior |
|---|---|
exact_duplicate |
Show the duplicate returned by memory.suggest_capture; do not write a new memory by default. |
update_candidate |
Show the existing memory and proposed replacement; call memory.confirm_capture with decision: "update" only after the user confirms or edits the change. |
related |
Ask whether to save a separate memory, then call memory.confirm_capture with decision: "keep_separate" only after confirmation. |
independent |
Offer to save through memory.confirm_capture with decision: "create", but only after confirmation. |
uncertain |
Ask a clarifying question before saving or updating. |
Update prompts should include the current memory content, proposed content,
kind, scope, tags, and reason. The host should pass expected_revision from the
memory it displayed to the user. If memory.confirm_capture or memory.update returns
MEMORY_REVISION_CONFLICT, the host must re-read the current memory and ask the
user again; it must not retry silently.
Confirmed updates remain subject to the same policy checks as new memories: secret scanning, scope validation, tag normalization, authorization, and audit history.
memory.confirm_capture is a convenience boundary for confirmed capture flows.
It still routes writes through the canonical remember and update behavior.
reject and clarify decisions write nothing.
For host integrations, confirm: true is an attestation by the host that it
displayed the final draft and obtained the represented decision. The current
protocol does not store suggestion drafts, issue confirmation tokens, or
cryptographically bind confirmation to an earlier memory.suggest_capture
response. Core policy, authorization, revision, and duplicate checks therefore
remain mandatory at confirmation time.
The final Capture Confirmation Binding Decision records the bounded threat model, stateless digest and minimal-state HMAC prototypes, replay/mutation/multi-process evidence, and the decision to keep this host-attested boundary instead of implying authenticated human consent.
Scope Rules¶
Choose the narrowest useful scope.
| Situation | Default scope |
|---|---|
| Cross-project user preference | user:default |
| Project architecture decision | project:<path-hash> |
| Project workflow rule | project:<path-hash> |
| Host-specific usage preference | agent:<name> |
| Unclear or mixed scope | Ask before saving. |
Do not promote project-specific information to user:default by default.
Allowed Examples¶
These examples may produce drafts:
| User statement | Draft kind | Default scope |
|---|---|---|
| "For Nuzo, always use GitHub Issues for executable work." | instruction |
project:<path-hash> |
| "I prefer Apache-2.0 for this project." | project_decision |
project:<path-hash> |
| "I like concise status updates while work is running." | preference |
user:default |
"This repo uses /tmp/nuzo-git as the git dir workaround." |
fact |
project:<path-hash> |
"When changing MCP tools, update docs/spec/tools.md first." |
instruction |
project:<path-hash> |
"For Cloudflare work on this host, use /example/workflows/cloudflare." |
instruction |
user:default |
Blocked Examples¶
These examples must not produce a persisted memory unless the user explicitly rewrites them into a safe memory:
| Input | Reason |
|---|---|
"My token is ghp_...." |
Secret. |
"Here is my .env file..." |
Raw private file content. |
| "The build failed once with this stack trace..." | Transient log. |
"Use password hunter2 for local testing." |
Credential. |
| "I might maybe switch to Postgres later." | Speculative and not durable. |
| "The customer said their card failed." | Sensitive personal or payment context. |
When blocked, the agent may explain that Nuzo avoids storing that kind of content and ask for a sanitized durable preference or decision instead.
Host Responsibilities¶
Host plugins may provide prompts, skills, or hooks that create capture drafts.
They must not contain:
- secret detection logic beyond calling
memory.suggest_captureor confirmed core policy checks; - separate memory schemas;
- storage behavior;
- host-specific import/export formats;
- automatic write paths.
Confirmed host flows must call memory.confirm_capture with the explicit user
decision. Confirmed new memories route through the canonical remember behavior,
and confirmed replacements route through the canonical update behavior with the
revision shown to the user. Host wrappers must not write storage directly.
Core Responsibilities¶
Core must validate both capture suggestions and confirmed writes.
memory.suggest_capture is responsible for:
- applying the same content, kind, scope, tag, source, confidence, secret, and
authorization policy as
memory.remember; - returning a normalized draft;
- detecting exact active duplicates in the same scope in every mode;
- returning the versioned, bounded relationship result only when the caller explicitly requests it;
- classifying relationship evidence in core rather than a CLI, MCP handler, or host plugin;
- staying read-only.
Confirmation is not a bypass for:
- secret scanning;
- kind validation;
- scope validation;
- tag normalization;
- duplicate detection;
- disabled-scope checks.
If core rejects a confirmed draft, the host should show the structured error and avoid retrying automatically.
MVP Implementation Order¶
- Document capture suggestion policy.
- Add test fixtures for allowed and blocked examples.
- Add host prompt/skill guidance for suggesting drafts.
- Add optional read-only recall hook.
- Add
memory.suggest_captureas a read-only validation and duplicate check. - Add confirmed capture flow that routes explicit decisions through
memory.confirm_capture, backed by canonical remember and update behavior.
Do not add a capture hook that writes directly to storage.