Documentation
¶
Overview ¶
Package memsync holds the memory-store rules that both halves of the sync share (docs/plan/36_memory-stores.md decision 17). Two callers, one copy: internal/api validates what a client writes, and the executor (which writes rows, from slice 4) and internal/worker (which goes over the wire, from slice 6) reconcile a sandbox directory against a store — a path or a body the routes would refuse has to be refused locally too, or a run spends a round trip learning it.
Slice 2 put the path and content rules and the mount slug here; slice 4 the rest of the shared half: the marker file's bytes, the baseline file's encoding, the tree-hash command and its parser (tree.go), and the pure decision table Plan(local, baseline, remote) → actions that both engines apply (plan.go).
Nothing here touches a database, a sandbox or the network — it is text rules over strings, so either caller can use it without carrying the other's dependencies.
Index ¶
- Constants
- func HashTreeCommand(mount string) string
- func MarkerBytes(storeID string) []byte
- func ParseHashTree(stdout []byte) (map[string]string, error)
- func RemoveCommands(mount string, paths []string) []string
- func Slug(name, fallback string) string
- func ValidateContent(content string) error
- func ValidatePath(path string) error
- type Action
- type Baseline
- type Head
- type Input
- type Kind
- type Result
Constants ¶
const ( // MaxPathBytes and MaxContentBytes are the documented memory bounds — a // path of at most 1,024 bytes and content of at most 100 kB — counted in // BYTES, not runes, which is what the reference states for both // (checked against anthropic-sdk-go v1.66.0 — betamemorystorememory.go // BetaManagedAgentsMemory.Path and BetaManagedAgentsMemory.Content and // BetaMemoryStoreMemoryNewParams.Content and // BetaMemoryStoreMemoryNewParams.Path). They differ from the store // surface's rune-counted "characters" for that reason and no other. MaxPathBytes = 1024 MaxContentBytes = 102400 // MaxMemoriesPerStore is the documented cap: 2,000 memories per store, // past which "writes to new memories fail … Existing memories remain // readable and editable" (the memory guide). The API's create and the // sync's create push both hold it. MaxMemoriesPerStore = 2000 )
const MarkerName = ".anthropic-memory-store"
MarkerName is the file every mounted store directory carries at its root (decision 10). Its bytes name the store the directory is a copy of; a directory whose marker is missing or altered is no longer trusted with pushes, because nothing says its files are that store's any more.
Variables ¶
This section is empty.
Functions ¶
func HashTreeCommand ¶
HashTreeCommand is the read phase's one exec: every regular file under the mount but the marker, with its SHA-256, NUL-terminated so any file name survives, and sorted under LC_ALL=C so the listing is in byte order like the store's own. An absent mount lists nothing and exits 0 — the empty tree the wipe guard then judges; a mount that is no longer the directory the store was landed in exits 3 (mountPrelude); and a stage that fails fails the command, under `pipefail`, because the pipeline's own status would be xargs's — a `find` that could not enter a directory would pass as a smaller tree than there is, and a `sha256sum` without `-z` (BusyBox's) as an empty one. Only the exit status is judged: a shell that warns on stderr (an image whose locale is not installed) still lists. It wants bash, which both sandbox backends exec under, and GNU coreutils (`sha256sum -z`, `sort -z`), which the images this platform ships and tests have.
func MarkerBytes ¶
MarkerBytes is the marker's content: a version line and the store id, which is what the reference worker writes and later re-hashes to decide whether a directory it finds is one it stamped (checked against anthropic-sdk-go v1.66.0 — memories.go SessionMemoryStores.scanMarker and markerSHA).
func ParseHashTree ¶
ParseHashTree reads HashTreeCommand's stdout into memory paths and digests. A record is `<64 hex> ./<name>` (or ` *` before the name, sha256sum's binary-mode spelling) ending in NUL; a record missing its NUL is refused rather than trusted, since a cut-off capture ends in exactly that.
func RemoveCommands ¶
RemoveCommands is the apply phase's deletions, as one exec or several: the store's removed memories unlinked from the mount, then each one's directory removed as far up as it is empty — never the mount itself, which the relative `rmdir -p` cannot reach — so a memory the store later creates at a removed directory's path (`/a/b` gone, `/a` created, a transition its occupancy rule allows) finds no directory in its way. Paths are the memories' own, `/`-rooted at the mount, which mountPrelude first checks is still the directory it was. An unlink that fails fails its command; the directory pass is best-effort.
func Slug ¶
Slug renders a store's display name as the directory name it mounts under (decision 8): "The directory name is the store's display name sanitized to a filesystem-safe slug (lowercased; non-alphanumeric runs become a single hyphen)". Two readings the reference leaves open are settled here and registered: leading and trailing hyphens are trimmed — the documented rule alone would mount "(Notes)" at /mnt/memory/-notes- — and "alphanumeric" means ASCII, so a name written in another script slugs to its ASCII remainder. A name with nothing left is the caller's fallback.
func ValidateContent ¶
ValidateContent holds the other half of decision 4: at most 100 kB of valid UTF-8 text — and text Postgres can hold, which valid UTF-8 does not settle: U+0000 is a legal code point that a text column refuses (SQLSTATE 22021). Both of those halves are inert on the API lane — a JSON string decodes to valid UTF-8 by construction, and the API refuses a NUL anywhere in a body before any field binds — and load-bearing on the sync lane, where the bytes come from a file in a sandbox: unrefused, a NUL in one file would fail its store's whole settlement on every run until the sandbox died.
func ValidatePath ¶
ValidatePath holds the reference's own documented path rule verbatim (checked against anthropic-sdk-go v1.70.1 — betamemorystorememory.go BetaMemoryStoreMemoryNewParams.Path): "Must start with `/`, contain at least one non-empty segment, and be at most 1,024 bytes. Must not contain empty segments, `.` or `..` segments, control or format characters, or the Unicode line and paragraph separators (U+2028, U+2029), and must be NFC-normalized. Paths are case-sensitive."
docs/plan/36_memory-stores.md decision 4 is the same table as it read at the SDK version pinned then, before the separators clause: an archived plan is the record of a decision, so it keeps the rule it was decided against.
NFC is a rejection, not a normalization: the rule reads as a constraint on what a client may send, and normalizing silently would hand back a path the caller did not write while its own SHA-based preconditions still spoke of the original bytes.
The marker path is this platform's own addition, registered as an inference: a memory there would overwrite the mount's marker file or be overwritten by it, and the marker is what tells a worker the directory is a real store.
Types ¶
type Action ¶
type Action struct {
Kind Kind
Path string
ID string
BaselineSHA string
LocalSHA string
RemoteSHA string
Conflict bool
}
Action is one step of a sync. Conflict marks a Pull that overwrites a local edit or restores a local deletion — the store won.
type Baseline ¶
type Baseline struct {
Synced map[string]string `json:"synced,omitempty"`
Refused map[string]string `json:"refused,omitempty"`
}
Baseline is what the directory and the store last agreed on, kept beside the mounts (decision 11): Synced maps a memory path to the digest both sides held after the last sync, which is how the next one tells an edit from a remote change; Refused maps a path to the digest the store refused (a path or body the routes would reject, a full store), which is skipped rather than retried until the bytes change.
func DecodeBaseline ¶
DecodeBaseline reads Encode's bytes back. Nothing — an absent file, an empty one — is an empty baseline, and either way both maps are usable.
type Head ¶
Head is a store's current memory at a path: its id, and the digest the store holds for it.
type Input ¶
type Input struct {
Local map[string]string
Baseline Baseline
Remote map[string]Head
PullOnly bool
}
Input is what Plan decides from: the directory's files by digest (the marker excluded), the baseline the last sync left, the store's heads, and whether this sync may write to the store at all. PullOnly is a read_only attachment, an archived store, or a directory whose marker is missing or altered (decision 12).
type Kind ¶
type Kind uint8
Kind is what an Action asks its applier to do.
const ( // Pull writes the store's content at Path into the directory (ID names // the memory, RemoteSHA is the entry the baseline takes once written). Pull Kind = iota + 1 // Push writes the directory's bytes at Path to the store: a create when // ID is empty, else an update of ID conditioned on the store still // holding BaselineSHA. LocalSHA is the digest of the bytes being pushed. Push // DeleteRemote deletes memory ID, conditioned on the store still holding // BaselineSHA. DeleteRemote // RemoveLocal removes the directory's file at Path: the store deleted the // memory and the directory never changed it. RemoveLocal )
type Result ¶
Result is the plan. Actions are in path order. Next is the baseline the sync will write, holding only the entries Plan settled without an action — the applier adds what each action lands (a pull's RemoteSHA, a push's LocalSHA), keeps a DeleteRemote's BaselineSHA when the store refused it, and drops the rest. Skipped lists pushes withheld by a standing refusal; their refusals are already carried in Next. Withheld counts the pushes pull-only mode dropped — local edits that stay local, which a sync log should say rather than report a quiet run. Rebuild is the wipe guard.
func Plan ¶
Plan is decision 11's per-path table, the reference worker's syncPath decisions with our two omissions (no corroboration window and no deletion cap, both registered): for each path in the union of the three maps, the baseline tells whether the store changed (its head is not the baseline) and whether the directory changed (its file is not the baseline), and the answer is the one the reference gives — the store wins a conflict, a deletion on one side over an unchanged other side propagates, an edit over a remote deletion re-creates. Pull-only mode drops every write to the store and otherwise decides identically, so a read-only directory still follows the store's changes and deletions. A push whose bytes the store refused before is skipped and stays refused until the bytes change, as is a push that would have to CREATE a memory at a path the rules refuse — a push against a memory the store already holds is judged on its bytes alone.
The wipe guard is the reference's own: an empty directory against a baseline that remembered more than one file is a wiped mount, so everything is pulled and nothing deleted.