Help us shape the future of Go! Take the 2026 Go Developer Survey

memsync

package
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 17, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

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

View Source
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
)
View Source
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

func HashTreeCommand(mount string) string

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

func MarkerBytes(storeID string) []byte

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

func ParseHashTree(stdout []byte) (map[string]string, error)

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

func RemoveCommands(mount string, paths []string) []string

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

func Slug(name, fallback string) string

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

func ValidateContent(content string) error

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

func ValidatePath(path string) error

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

func DecodeBaseline(data []byte) (Baseline, error)

DecodeBaseline reads Encode's bytes back. Nothing — an absent file, an empty one — is an empty baseline, and either way both maps are usable.

func (Baseline) Encode

func (b Baseline) Encode() []byte

Encode is the baseline file's bytes: JSON with sorted keys, so an unchanged baseline rewrites to identical bytes.

type Head struct {
	ID  string
	SHA string
}

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

type Result struct {
	Rebuild  bool
	Actions  []Action
	Next     Baseline
	Skipped  []string
	Withheld int
}

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

func Plan(in Input) Result

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL