Part I

Coalgebraic Memory

Download PDF

1 Introduction

An agent that forgets everything between sessions is a function; an agent that remembers is a state-based system, and state-based systems are the subject matter of coalgebra (15, 5). Banu takes this as the first row of a correspondence between the four externalization pillars of Zhou et al. and the ArchAgents Architecture triple (1, 20, 14). Memory there is a coalgebra (S,φ)(S, \varphi) for a polynomial functor, and the correspondence is validated against one reference implementation. It is stated at the level of the pair (S,φ)(S, \varphi). It does not fix the functor, the laws, or the means of checking either against a system that was not built to fit the account.

This paper fixes all three for one system. ContextFS is a memory layer for coding agents. It stores typed records and exposes them over a command line, a Python facade and a Model Context Protocol server. It also maintains a graph of relation-labelled edges recording how records were derived from one another. Its lineage module offers four derivation operations, evolve, merge, split and supersede, each of which creates new records and leaves the originals in place. The coalgebraic account presupposes a persistent carrier, non-destructive derivation and an explicit edge vocabulary. A system with all three is the informative case to examine: where the account fails here, it fails for reasons other than the absence of the structure it needs.

The test isolates a stable derivation fragment rather than a property of the whole system. Derivations add fresh records and ancestry edges, which preserves the ancestry of existing records and confines effects when every named record belongs to one namespace. Deletion and replacement fall outside that fragment: deletion removes incident edges, while a save under an existing identifier replaces a record. Proposition 5.9 proves that the restricted fragment is closed and reachable. The failed clauses identify conditions that the informal state-and-transitions description leaves unstated.

1.1 Scope

Every result below is proved from the definitions in Section 3 or established by the inspection procedure of Section 1.3. Companion work on the other three pillars is cited where a reader may want the adjacent layer.

The claim being tested is narrow. It is not claimed that the four externalization pillars and the Architecture triple (G,Know,Φ)(G, \mathrm{Know}, \Phi) are the same object. Three things are claimed, under stated assumptions. There is a structure-preserving interpretation of the pillars in the triple. A production system realises that interpretation on an identifiable fragment. The places where the interpretation fails are exhibited rather than passed over.

For memory, the identifiable fragment is the lineage fragment: the record store, the edge graph, and the four derivation operations, together with saves that allocate fresh identifiers. Deletion and saves under an existing identifier are outside it. Every positive result below carries that exclusion in its hypotheses (Proposition 5.2, Proposition 5.9). The fragment not covered is retrieval: similarity search over an external vector index is not an observation of the state space in the sense of Definition 3.14, for the reasons given in Section 7.4. The most-quoted feature of the correspondence is that agent memory is bitemporal in the sense of Jensen and Snodgrass (6). Its absence here is not merely conceded: valid time is proved to be unrecoverable from the recorded state (Theorem 5.27).

1.2 Contributions

  1. A model of a memory store as a pair (m,E)(m, E) of a finite partial map on identifiers and a finite relation-labelled edge set, with a partial commutative monoid structure given by domain-disjoint union (Definition 3.4, Proposition 3.6).

  2. Two coalgebras over that data and an account of what each is for. The store coalgebra (S,α ⁣:S→FS)(S, \alpha \colon S \to F S) for FX=(Out×X)A+F X = (\mathrm{Out}\times X)^{A^{+}} models the API dynamics (Definition 3.14). The lineage coalgebra (Id,βs)(\mathrm{Id}, \beta_{s}) for HX=Rec⊥×Pfin(R×X)H X = \mathrm{Rec}_{\bot} \times \mathcal{P}_{\mathrm{fin}}(\mathcal{R}\times X) models what can be observed about one memory’s derivation (Definition 3.18). The alphabet of the first is the memory API extended by an allocation oracle, without which the structure map is not a function (Remark 3.9).

  3. Seven laws that a lineage realisation should satisfy (Definition 3.21), chosen so that each is decidable by inspection of a candidate implementation rather than by testing.

  4. A proof that the ancestry fragment is homomorphism-stable (Theorem 5.7) and that the full lineage fragment is not (Proposition 5.13). This is the positive result of the paper.

  5. A separation of the two halves of namespace locality: operations that name only records of one namespace have effects confined to it (Proposition 5.17), while merge nevertheless carries payload across the boundary by construction (Proposition 5.18).

  6. Three further negative results with code-level witnesses. The merged record’s type is decided by an enumeration order the interface does not declare (Proposition 5.20). Ancestry is recorded in two representations that need not agree (Proposition 5.14). The recorded state is single-axis (Theorem 5.27).

  7. An algebraic account of merge at the level a coalgebra calls for: it is idempotent up to allocation-blind bisimilarity (Proposition 5.24) and not commutative even up to that equivalence (Proposition 5.25).

  8. A frame property for operations that name their identifiers (Proposition 6.2) and two independent failures of it, one for recall by identifier prefix (Proposition 6.3) and one for the lineage observation on stores with dangling edges (Proposition 6.4). Together these are the obstruction to composing memory stores by disjoint union in a multi-tenant setting.

  9. An appendix mapping every code-backed claim in the paper to a repository, path, identifier, line and commit.

1.3 Evidence and proof method

The results below are of two kinds and it matters that they are not confused, so we separate them by construction rather than by convention.

Results of the first kind are theorems about the model of Section 3. Their hypotheses are properties of a lineage realisation stated in the model’s own vocabulary, chiefly the derivation schema of Definition 3.23, which says which records and edges each operation adds. They are proved from those hypotheses and from the definitions, with no appeal to any repository. Theorem 5.7, Corollary 5.12, Proposition 5.17, Proposition 5.24, Proposition 7.1 and Proposition 6.2 are of this kind, as are all results in Section 3. They stand or fall on the mathematics.

Results of the second kind are instantiation claims: statements that the source of one repository at one commit exhibits a construct the model names. Their evidence is a path, an identifier and a line range, and their epistemic status is that of a careful reading, not that of a proof. We do not give an operational semantics for Python and SQLite, and we do not claim to have verified anything mechanically. Where such a claim appears in a numbered environment it is because it plays a role in an argument and needs a label. The environment is not a claim of formal derivation. Every instantiation claim in the paper appears as a row of Appendix A with its file, identifier and lines, so a reader can check each one directly against the repository.

Companion work on the other three pillars is cited in a third way again, and never as a premise. Such a citation reports a fact about a different repository that a reader may want for context, for instance whether one system declares a dependency on another. No definition, hypothesis or proof step below rests on a companion, and every object used here is defined in Section 2 or Section 3.

The two kinds meet in exactly one place. Lemma 5.6 is an instantiation claim: it asserts that ContextFS at a93035d instantiates the derivation schema. Every positive result about the system in Section 5 is then a theorem of the first kind applied to that lemma. A reader who disputes the reading of the source can reject Lemma 5.6 without any of the mathematics changing, and a reader who accepts it gets the theorems. Negative results are simpler: each exhibits a concrete counterexample whose only code-dependent ingredient is a single named expression.

1.4 Series context

This paper is the first of four on the externalization pillars, and the papers share notation. The results below do not depend on formal claims from the companion papers.

Nothing formal is imported. The proofs need Set\mathbf{Set}, the definition of a coalgebra and its homomorphisms, and the bitemporal vocabulary of the temporal database literature, all recalled in Section 2.

The companion treatment of skills as operations of a coloured operad (10) uses the memory carrier as one colour. The type Mem\mathsf{Mem} of a handle on the carrier is therefore defined here (Definition 3.15), as a single colour rather than a family indexed by record schemas. Port types themselves are defined in the companion treatment of protocols (11), together with the alphabet T\mathsf{T}. The notion is used here in that sense, and the symbol T\mathsf{T} is not reused for anything else.

The companion treatment of the harness (12) develops the Architecture triple (G,Know,Φ)(G, \mathrm{Know}, \Phi) in full, together with the categories Arch\mathbf{Arch} and Sys\mathbf{Sys} and the reading of an agent as a lax monoidal functor A ⁣:Arch→SysA \colon \mathbf{Arch}\to \mathbf{Sys}. None of that is needed here and none of it is used. One boundary is needed, and we fix it ourselves so that nothing below depends on a companion. A certificate cert\mathrm{cert} in the sense of Banu’s Definition 1 is a checkable statement about an architecture, consisting of a claim, an assignment of its parameters, and evidence that can be replayed (1). The Know\mathrm{Know} component of an architecture is a collection of such statements. The state space SS of this paper is a different thing: it is what is remembered, not what is checked. In ContextFS, the schema registry and the structured-data validation are Know\mathrm{Know}-level and lie outside SS. The record store and the edge set are SS.

Theorem 5.27 is proved here rather than assumed anywhere, and is what companion work cites when it describes the memory component of a harness as a single-axis store.

What this layer offers a composite is the carrier together with one stability guarantee. A skill that reads the ancestry of a record at one point in a composite and again later is entitled, by Theorem 5.7, to assume the ancestry has not changed. It is not entitled to assume the same of descendants, and Proposition 6.3 records what identifier discipline is needed for even the first assumption to be meaningful when several tenants share a store.

Throughout, statements about code are claims about the source at the pinned commit. We write “the implementation at a93035d does XX” and mean that a reader of the named file and line will find XX written there. We do not report runtime observations, and we do not describe any property as established by anything stronger than a reading of the source.

2 Background

2.1 The memory pillar

Zhou et al. survey the externalization of agent capability into four components that sit around a language model rather than inside it: memory, skills, protocols, and the harness that composes them (20). Memory in that taxonomy is whatever survives the boundary of a single model call. It is not the context window, which is an input. It is the store the agent consults to build the input and updates after acting.

Two things follow from that definition. Memory is stateful in a strong sense: the same query at two times may legitimately return different answers, and the difference is information rather than error. Memory is also observed rather than inspected. An agent does not read the store. It calls an operation and receives an answer. Both properties are the defining properties of the systems coalgebra was designed for (15, 5), which is why the identification of memory with coalgebraic state is natural rather than decorative.

Banu takes the identification as the first row of a four-row correspondence between Zhou’s pillars and the ArchAgents Architecture triple (G,Know,Φ)(G, \mathrm{Know}, \Phi) of de los Riscos, Corbacho and Arbib (1, 14). In that correspondence memory is modelled as a coalgebra for a polynomial functor, a pair (S,φ)(S, \varphi) with φ ⁣:S→P(S)\varphi \colon S \to \mathcal{P}(S). The reference implementation is said to carry bitemporal records, so that the question “what did the agent believe at time tt” has an answer. The correspondence is stated at that level of detail and validated against a single reference implementation, which the author identifies as a limitation of the work (1, Section 7.2). The identifiers named there, including BiTemporalMemory and RunContext, belong to that reference implementation. They do not occur in ContextFS at a93035d, and this paper does not use them (Remark 7.3).

Cao argues the wider motivation for externalizing memory on complexity grounds (2). The argument contrasts systems whose decision logic is fixed in source with systems that generate it at run time, and treats the growth of interaction paths against roughly constant human capacity as the reason the second kind displaces the first. We take that as context, not as a premise. Nothing below depends on it.

2.2 Coalgebras

We use only the elementary theory, in the form set out by Rutten (15) and Jacobs (5), and record it here to fix notation rather than to introduce it. Fix the category Set\mathbf{Set}. For a functor F ⁣:Set→SetF \colon \mathbf{Set}\to \mathbf{Set}, an FF-coalgebra is a pair (X,c)(X, c) with c ⁣:X→FXc \colon X \to F X, and a homomorphism f ⁣:(X,c)→(Y,d)f \colon (X, c) \to (Y, d) is a function with d∘f=Ff∘cd \circ f = F f \circ c. These form a category Coalg(F)\mathbf{Coalg}(F). A final FF-coalgebra is a terminal object (νF,ι)(\nu F, \iota) of that category. When it exists, each (X,c)(X, c) carries a unique homomorphism [ ⁣[−] ⁣]c ⁣:X→νF[\![-]\!]_{c} \colon X \to \nu F, the behaviour map. An FF-bisimulation between (X,c)(X, c) and (Y,d)(Y, d) is a relation B⊆X×Y\mathcal{B} \subseteq X \times Y carrying an FF-coalgebra structure making both projections homomorphisms.

Two standard facts are used. First, every finitary Set\mathbf{Set}-endofunctor, that is, one preserving filtered colimits, is accessible and therefore has a final coalgebra. Second, when FF preserves weak pullbacks, bisimilarity coincides with behavioural equivalence, so two states have equal behaviour exactly when some bisimulation relates them. Both are in (15, 5).

The homomorphism condition carries the weight below. If cc says what can be observed at a state and where the state can go, then d∘f=Ff∘cd \circ f = F f \circ c says that observing at f(x)f(x) agrees with observing at xx. It also says that the successors of f(x)f(x) are the ff-images of the successors of xx. Several results below are failures of exactly this condition for maps one would like to be homomorphisms, such as “restrict the store to one namespace”.

2.3 Bitemporal data

The temporal database literature distinguishes two independent time dimensions (6, 17). Valid time is when a fact is asserted to hold in the modelled world. Transaction time is when the system recorded the assertion. A relation is bitemporal when each tuple carries timestamps in exactly one valid-time and one transaction-time dimension. The distinction is not decoration. It is what separates two situations that a single-axis log conflates: a fact about the past that we learn late, and a fact about the present that has just changed.

For an agent the distinction has an operational reading. Transaction time answers “what did the system have on file at tt”. Valid time answers “what was actually the case at tt”. Their combination answers “what did the system believe at t2t_{2} about the state of the world at t1t_{1}”, which is the question an audit of an agent’s decision requires. We use the series symbols τv,τt\tau_{v}, \tau_{t} for the two axes.

Definition 2.1 (Bitemporal record). Let T\mathbb{T} be a linear order of time instants and Val\mathrm{Val} a set of values. A bitemporal record over Val\mathrm{Val} is a triple (v,τv,τt)∈Val×T×T(v, \tau_{v}, \tau_{t}) \in \mathrm{Val}\times \mathbb{T}\times \mathbb{T} read as: the value vv is asserted to hold from valid time τv\tau_{v}, and that assertion was recorded at transaction time τt\tau_{t}. A record schema is single-axis when it carries no field whose value is under the caller’s control and is read as a valid time.

Definition 2.2 (Bitemporal history and its snapshot). A bitemporal history is a finite sequence h=((v1,p1,t1),…,(vn,pn,tn))h = \big((v_{1}, p_{1}, t_{1}), \dots, (v_{n}, p_{n}, t_{n})\big) of bitemporal records with t1<⋯<tnt_{1} < \dots < t_{n}. For (τv,τt)∈T×T(\tau_{v}, \tau_{t}) \in \mathbb{T}\times \mathbb{T} put Jh(τv,τt)  =  { j:tj≤τt and pj≤τv },J_{h}(\tau_{v}, \tau_{t}) \;=\; \{\, j : t_{j} \le \tau_{t} \text{ and } p_{j} \le \tau_{v} \,\}, the assertions recorded by τt\tau_{t} whose validity had begun by τv\tau_{v}. The snapshot function σh ⁣:T×T→Val⊥\sigma_{h} \colon \mathbb{T}\times \mathbb{T}\to \mathrm{Val}_{\bot} is ⊥\bot when Jh(τv,τt)J_{h}(\tau_{v}, \tau_{t}) is empty, and otherwise vkv_{k} where P  =  max⁡{ pj:j∈Jh(τv,τt) },k  =  max⁡{ j∈Jh(τv,τt):pj=P }.P \;=\; \max\{\, p_{j} : j \in J_{h}(\tau_{v}, \tau_{t}) \,\}, \qquad k \;=\; \max\{\, j \in J_{h}(\tau_{v}, \tau_{t}) : p_{j} = P \,\}. The two maxima are taken in the stated order and neither may be omitted. The inner one selects on the valid-time axis: among the assertions in force at τv\tau_{v}, the governing one is the one whose validity began latest, since a later assertion about τv\tau_{v} supersedes an earlier one about the same instant. The outer one selects on the transaction-time axis and breaks the remaining tie: among assertions with the same valid-time start, the one recorded last is the current belief. Maximising over jj alone would conflate the axes. It would let a correction recorded late about a distant past instant shadow an assertion about an instant nearer to τv\tau_{v}, which is the confusion the two-axis model exists to prevent.

Remark 2.3. Definition 2.2 uses valid-time instants rather than intervals. This is the weaker setting, and it makes the negative result of Theorem 5.27 stronger. A system that cannot record the instant from which an assertion is claimed to hold certainly cannot record an interval.

2.4 Notation

T\mathbb{T} is a linear order. For a set XX we write X⊥=X+{⊥}X_{\bot} = X + \{\bot\}, X∗X^{\ast} for the set of finite sequences over XX, and Pfin(X)\mathcal{P}_{\mathrm{fin}}(X) for the set of finite subsets of XX. For sets X,YX, Y we write X⇀finYX \rightharpoonup_{\mathrm{fin}} Y for the set of finite partial functions and dom(f)\mathrm{dom}(f) for the domain of ff. Repository names are set as ContextFS; code identifiers and paths as memory_lineage.py. All ContextFS line references are to commit a93035d.

3 The formal model

3.1 Identifiers, records, relations

Definition 3.1 (Basic data). Fix a countably infinite set Id\mathrm{Id} of identifiers, a set Rec\mathrm{Rec} of records, and a finite set R\mathcal{R} of relation labels. Records carry at least the fields id ⁣:Rec→Id,ns ⁣:Rec→N,ty ⁣:Rec→T,ct ⁣:Rec→T,\mathrm{id} \colon \mathrm{Rec}\to \mathrm{Id}, \qquad \mathrm{ns}\colon \mathrm{Rec}\to N, \qquad \mathrm{ty} \colon \mathrm{Rec}\to T, \qquad \mathrm{ct} \colon \mathrm{Rec}\to \mathbb{T}, an identifier, a namespace drawn from a set NN, a type drawn from a finite set TT, and a creation instant. Records carry further fields (content, tags, summary, a metadata map) that the model treats as opaque payload.

Definition 3.2 (Inverse structure on labels). An inverse structure on R\mathcal{R} is a function (−)‾ ⁣:R→R\overline{(-)} \colon \mathcal{R}\to \mathcal{R} with ρ‾‾=ρ\overline{\overline{\rho}} = \rho for all ρ\rho, that is, an involution. Labels with ρ‾=ρ\overline{\rho} = \rho are symmetric.

Definition 3.3 (Ancestry labels). An ancestry selection is a subset R−⊆R\mathcal{R}^{-}\subseteq \mathcal{R} closed under no operation in particular, singled out as the labels whose direction points from a derived record to the records it was derived from. We write R+={ρ‾:ρ∈R−}\mathcal{R}^{+} = \{\overline{\rho} : \rho \in \mathcal{R}^{-}\} for the corresponding descendant labels and assume R−∩R+=∅\mathcal{R}^{-}\cap \mathcal{R}^{+} = \emptyset.

Definition 3.4 (Store). A store is a pair s=(ms,Es)s = (m_{s}, E_{s}) with ms∈Id⇀finRecm_{s} \in \mathrm{Id}\rightharpoonup_{\mathrm{fin}} \mathrm{Rec} satisfying id(ms(i))=i\mathrm{id}(m_{s}(i)) = i for all i∈dom(ms)i \in \mathrm{dom}(m_{s}), and Es∈Pfin(Id×R×Id)E_{s} \in \mathcal{P}_{\mathrm{fin}}(\mathrm{Id}\times \mathcal{R}\times \mathrm{Id}). We write SS for the set of stores. A store is ancestry-closed when for every i∈dom(ms)i \in \mathrm{dom}(m_{s}) and every (i,ρ,j)∈Es(i, \rho, j) \in E_{s} with ρ∈R−\rho \in \mathcal{R}^{-} we have j∈dom(ms)j \in \mathrm{dom}(m_{s}). A store is inverse-closed when (i,ρ,j)∈Es(i, \rho, j) \in E_{s} implies (j,ρ‾,i)∈Es(j, \overline{\rho}, i) \in E_{s}.

Neither closure condition is imposed on SS. Both are properties that a realisation may or may not maintain, and Section 5 reports which of them ContextFS maintains.

Definition 3.5 (Domain-disjoint union). For stores s,s′s, s' with dom(ms)∩dom(ms′)=∅\mathrm{dom}(m_{s}) \cap \mathrm{dom}(m_{s'}) = \emptyset, set s⊎s′=(ms∪ms′, Es∪Es′)s \uplus s' = (m_{s} \cup m_{s'},\, E_{s} \cup E_{s'}). Otherwise s⊎s′s \uplus s' is undefined.

We define ⊎\uplus with this side condition because disjointness of record identifiers is what a reading of ContextFS’s own tenancy boundary suggests, and because seeing precisely where it is too weak is one of the results of Section 6. It is not the operator we end up recommending. Proposition 6.4 shows that ⊎\uplus does not preserve lineage behaviour, and Definition 6.5 refines it to the operator that does. The refinement requires disjointness of the identifiers a store mentions in either component rather than only in its record map. A reader who wants the compositional structure alone, without the diagnosis, may take Definition 6.5 as the definition and Proposition 6.6 as its property.

Proposition 3.6 (Stores form a partial commutative monoid). (S,⊎,∅)(S, \uplus, \emptyset) is a partial commutative monoid: ⊎\uplus is commutative and associative where defined, and the empty store ∅=(∅,∅)\emptyset = (\varnothing, \varnothing) is a two-sided unit.

Proof. Commutativity and the unit law are immediate from the definition, since union of partial functions with disjoint domains and union of finite sets are commutative with the empty map and empty set as units. For associativity, both (s⊎s′)⊎s′′(s \uplus s') \uplus s'' and s⊎(s′⊎s′′)s \uplus(s' \uplus s'') are defined exactly when dom(ms)\mathrm{dom}(m_{s}), dom(ms′)\mathrm{dom}(m_{s'}) and dom(ms′′)\mathrm{dom}(m_{s''}) are pairwise disjoint, and in that case both equal (ms∪ms′∪ms′′, Es∪Es′∪Es′′)(m_{s} \cup m_{s'} \cup m_{s''},\, E_{s} \cup E_{s'} \cup E_{s''}). ◻

⊎\uplus places no condition on edges. Two stores with disjoint record domains may carry edges pointing into each other’s records, and their union will too. This is deliberate: the implementation places no such condition either (Proposition 5.30).

3.2 The operation alphabet

Definition 3.7 (Lineage signature). The lineage signature Σ\Sigma consists of the following operation symbols with the indicated argument sorts, where CC is a set of contents, Θ\Theta a set of merge strategies, Ξ\Xi a set of change reasons and WW a set of edge weights: save(c,θr)c∈C, θr∈Rec-attributes,recall(i)i∈Id,evolve(i,c,ξ)i∈Id, c∈C, ξ∈Ξ,merge(ı⃗,c,θ)ı⃗∈Id∗, c∈C⊥, θ∈Θ,split(i,c⃗)i∈Id, c⃗∈C∗,supersede(i,j,r)i,j∈Id, r∈C⊥,link(i,j,ρ,w,b)i,j∈Id, ρ∈R, w∈W, b∈{0,1}.\begin{array}{ll} \mathsf{save}(c, \theta_{r}) & c \in C,\ \theta_{r} \in \mathrm{Rec}\text{-attributes},\\ \mathsf{recall}(i) & i \in \mathrm{Id},\\ \mathsf{evolve}(i, c, \xi) & i \in \mathrm{Id},\ c \in C,\ \xi \in \Xi,\\ \mathsf{merge}(\vec{\imath}, c, \theta) & \vec{\imath} \in \mathrm{Id}^{\ast},\ c \in C_{\bot},\ \theta \in \Theta,\\ \mathsf{split}(i, \vec{c}) & i \in \mathrm{Id},\ \vec{c} \in C^{\ast},\\ \mathsf{supersede}(i, j, r) & i, j \in \mathrm{Id},\ r \in C_{\bot},\\ \mathsf{link}(i, j, \rho, w, b) & i, j \in \mathrm{Id},\ \rho \in \mathcal{R},\ w \in W,\ b \in \{0,1\}. \end{array} We write AA for the set of fully applied calls of these symbols.

Definition 3.8 (Ambient parameter). An ambient parameter is a triple ω=(k⃗,t,π)\omega = (\vec{k}, t, \pi) consisting of a finite sequence k⃗∈Id∗\vec{k} \in \mathrm{Id}^{\ast} of identifiers to be allocated, an instant t∈Tt \in \mathbb{T} to be used as the clock reading, and a resolution parameter π∈Π\pi \in \Pi. Here Π\Pi is a set of enumeration data: an element of Π\Pi fixes, for every finite set of values the implementation may enumerate, a linear order on that set. We call (k⃗,t)(\vec{k}, t) the allocation part of ω\omega and π\pi its resolution part. Write Ω\Omega for the set of ambient parameters and A+=A×ΩA^{+} = A \times \Omega for the extended alphabet.

Remark 3.9 (Why the ambient parameter is necessary, and where to draw its boundary). Without Ω\Omega there is no function S→(Out×S)AS \to (\mathrm{Out}\times S)^{A} to be had. Each derivation operation constructs records whose identifiers come from a fresh-name generator and whose timestamps come from the system clock, so the successor state is not determined by the store and the call. One can respond by moving to a nominal or named-set semantics, by quotienting states by identifier renaming, or by making the ambient data explicit. We make it explicit because it is the option that keeps the model in Set\mathbf{Set} and keeps the laws below decidable by inspection.

The choice of what Ω\Omega absorbs is not free, and drawing it too narrowly would be a way of smuggling a negative result into a definition. If any source of ambient variation is left outside Ω\Omega then α\alpha is not a function and the coalgebraic reading fails before any law is evaluated. We therefore let Ω\Omega absorb every source we found: allocation, the clock, and the enumeration orders that the implementation consults when it iterates a finite collection whose iteration order the language does not fix. The resolution component π\pi is what makes α\alpha total and single-valued in the presence of the two constructs identified in Proposition 5.20 and Proposition 6.3.

Absorbing a source of variation into the alphabet is not the same as excusing it. The substantive question is which components of ω\omega a well-behaved realisation is allowed to read, and that is what L5 (Definition 3.21) asks. The allocation part is data the interface declares, since records carry identifier and timestamp fields. The resolution part is not data the interface mentions at all. A realisation that reads π\pi has a parameter its own specification does not admit to having.

Remark 3.10 (Three alternative presentations). Three other presentations are available, each with a different cost.

The first is to keep the alphabet at AA and observe that α\alpha is then not a function, so the system is not a coalgebra and there is nothing further to say. That conclusion is correct and uninformative. It applies equally to every system that allocates identifiers. It also cannot distinguish the part of ContextFS that satisfies Theorem 5.7 from the expression in merge that does not satisfy L5, because it refuses both at the same point. A model whose only verdict is a single global refusal is not a model of anything.

The second is to keep the alphabet at AA and move the variation into the codomain, taking a coalgebra S→M(Out×S)AS \to \mathcal{M}(\mathrm{Out}\times S)^{A} for a suitable monad M\mathcal{M}. For the variation at issue this is not an alternative but the same thing written differently. Currying gives a bijection between functions S→(Out×S)A×ΩS \to (\mathrm{Out}\times S)^{A \times \Omega} and functions S→((Out×S)Ω)AS \to \big((\mathrm{Out}\times S)^{\Omega}\big)^{A}, and (−)Ω(-)^{\Omega} is the reader monad on Ω\Omega. The presentation of Definition 3.14 is the uncurried form of a coalgebra valued in that monad. We work uncurried because the laws are then equations between functions of a named parameter, which is what makes “the result depends on this component of ω\omega and not that one” a statable property rather than an informal gloss.

A third presentation combines the two, using the reader monad for the allocation part and the finite powerset for the rest, giving S→Pfin(Out×S)A×Id∗×TS \to \mathcal{P}_{\mathrm{fin}}(\mathrm{Out}\times S)^{A \times \mathrm{Id}^{\ast} \times \mathbb{T}}. This is a perfectly serviceable alternative and L5 remains statable in it, as the requirement that the value be a singleton for every argument. We do not claim our formulation is the only one available. We prefer it for one reason. The powerset records that two outcomes are possible and forgets what selects between them, so a realisation that reads an enumeration order and one that flips a coin receive the same description. But Proposition 5.20 and Proposition 6.3 are about a specific ambient datum being read, and it is convenient to be able to name it.

Definition 3.11 (Outputs). Let Out=Rec+Rec∗+(Id×R×Id)+{ack}+{err}\mathrm{Out}= \mathrm{Rec}+ \mathrm{Rec}^{\ast} + (\mathrm{Id}\times \mathcal{R}\times \mathrm{Id}) + \{\mathsf{ack}\} + \{\mathsf{err}\}, the disjoint union of a record, a finite list of records, an edge, an acknowledgement and a single error value err\mathsf{err}. The two derived-observation cases Rec⊥\mathrm{Rec}_{\bot} used by recall\mathsf{recall} embed in Out\mathrm{Out} via Rec+{err}\mathrm{Rec}+ \{\mathsf{err}\}.

3.3 The store coalgebra

Definition 3.12 (Behaviour functor). Define F ⁣:Set→SetF \colon \mathbf{Set}\to \mathbf{Set} by FX=(Out×X)A+F X = (\mathrm{Out}\times X)^{A^{+}} on objects and, for f ⁣:X→Yf \colon X \to Y, by Ff(g)=(idOut×f)∘gF f (g) = (\mathrm{id}_{\mathrm{Out}} \times f) \circ g.

Lemma 3.13. FF is a functor.

Proof. FF is the composite (−)A+∘(Out×−)(-)^{A^{+}} \circ (\mathrm{Out}\times -) of the product functor with a fixed set and the exponential functor with a fixed exponent, both of which are functors on Set\mathbf{Set}; the stated action on morphisms is the composite action. Explicitly, FidX(g)=(id×id)∘g=gF\mathrm{id}_{X}(g) = (\mathrm{id}\times \mathrm{id})\circ g = g and F(f′∘f)(g)=(id×f′∘f)∘g=(id×f′)∘(id×f)∘g=Ff′(Ff(g))F(f' \circ f)(g) = (\mathrm{id}\times f' \circ f)\circ g = (\mathrm{id}\times f')\circ (\mathrm{id}\times f) \circ g = F f'(F f(g)). ◻

Definition 3.14 (Store coalgebra). A lineage realisation is an FF-coalgebra (S,α)(S, \alpha) on the set of stores. We write α(s)(a,ω)=(out(s,a,ω), nx(s,a,ω))\alpha(s)(a, \omega) = (\mathrm{out}(s, a, \omega),\, \mathrm{nx}(s, a, \omega)) for the two components and call out\mathrm{out} the output map and nx\mathrm{nx} the transition map. The memory coalgebra of a system is its lineage realisation.

Definition 3.15 (Memory port type). Mem\mathsf{Mem} is the type of a handle on the carrier SS of a memory coalgebra: a value of type Mem\mathsf{Mem} determines a store and admits the calls of Definition 3.7. Mem\mathsf{Mem} is exported as a single type, not a family indexed by record schemas.

Definition 3.15 is the only thing later Parts need from this Part in order to speak about memory as a component. It deliberately says nothing about record schemas: schema validation belongs to the Know\mathrm{Know} component in the sense fixed by Part IV, not to the state space.

3.4 The lineage coalgebra

The store coalgebra models what the API does. It does not directly model what a lineage query returns, because a lineage query is about one memory, not about the whole store. We therefore give a second coalgebra, on the identifiers of a fixed store.

Definition 3.16 (Lineage functor). Define H ⁣:Set→SetH \colon \mathbf{Set}\to \mathbf{Set} by HX=Rec⊥×Pfin(R×X)H X = \mathrm{Rec}_{\bot} \times \mathcal{P}_{\mathrm{fin}}(\mathcal{R}\times X), with Hf=idRec⊥×Pfin(idR×f)H f = \mathrm{id}_{\mathrm{Rec}_{\bot}} \times \mathcal{P}_{\mathrm{fin}}(\mathrm{id}_{\mathcal{R}} \times f). For R−⊆R\mathcal{R}^{-}\subseteq \mathcal{R} define the ancestry functor H−X=Rec⊥×Pfin(R−×X)H^{-} X = \mathrm{Rec}_{\bot} \times \mathcal{P}_{\mathrm{fin}}(\mathcal{R}^{-}\times X) likewise.

Lemma 3.17. HH and H−H^{-} are finitary Set\mathbf{Set}-endofunctors preserving weak pullbacks. Consequently each has a final coalgebra, and for each of them bisimilarity coincides with behavioural equivalence.

Proof. Pfin\mathcal{P}_{\mathrm{fin}} is finitary and preserves weak pullbacks; constant functors and finite products are finitary and preserve weak pullbacks; both properties are closed under composition and finite product. Hence HH and H−H^{-} have both properties. A finitary Set\mathbf{Set}-endofunctor is accessible, hence has a final coalgebra, and weak-pullback preservation gives the coincidence of bisimilarity with behavioural equivalence (15, 5). ◻

Definition 3.18 (Lineage coalgebra of a store). For a store ss define βs ⁣:Id→HId\beta_{s} \colon \mathrm{Id}\to H\mathrm{Id} by βs(i)  =  ( ms(i), { (ρ,j):(i,ρ,j)∈Es } ),\beta_{s}(i) \;=\; \big(\, m_{s}(i),\ \{\, (\rho, j) : (i, \rho, j) \in E_{s} \,\} \,\big), where ms(i)=⊥m_{s}(i) = \bot when i∉dom(ms)i \notin \mathrm{dom}(m_{s}). The ancestry coalgebra is βs− ⁣:Id→H−Id\beta^{-}_{s} \colon \mathrm{Id}\to H^{-}\mathrm{Id}, defined the same way but with the second component restricted to labels in R−\mathcal{R}^{-}. We write [ ⁣[i] ⁣]s∈νH[\![i]\!]_{s} \in \nu H and [ ⁣[i] ⁣]s−∈νH−[\![i]\!]^{-}_{s} \in \nu H^{-} for the corresponding behaviours.

Concretely, [ ⁣[i] ⁣]s−[\![i]\!]^{-}_{s} is the ancestry unfolding of ii: the record at ii, together with the labelled ancestry unfoldings of everything ii was derived from, to any depth. This is the object a lineage query approximates, and it is the object Theorem 5.7 says is stable.

Remark 3.19 (The cost of totality). βs\beta_{s} is defined on all of Id\mathrm{Id}, including identifiers with no record, in which case the first component is ⊥\bot. This makes βs\beta_{s} total without requiring stores to be ancestry-closed, which is necessary because ContextFS does not enforce referential integrity on edges (Proposition 5.30).

The choice has a consequence that we use rather than ignore. If two distinct absent identifiers j,j′j, j' have bisimilar outgoing edge structure, which happens in particular when neither occurs as the source of an edge of EsE_{s}, then βs(j)\beta_{s}(j) and βs(j′)\beta_{s}(j') are bisimilar and behaviour identifies them. Absence alone does not force this: Definition 3.4 places no referential constraint on EsE_{s}, so an identifier outside dom(ms)\mathrm{dom}(m_{s}) may perfectly well be the source of edges and then carries a nontrivial behaviour below a ⊥\bot observation. What the choice does force is that the observation at an absent identifier is the same ⊥\bot whichever identifier it is, so behaviour never records which record is missing, only that one is. One could avoid the identification by taking HX=(Rec+Id)×Pfin(R×X)H X = (\mathrm{Rec}+ \mathrm{Id}) \times \mathcal{P}_{\mathrm{fin}}(\mathcal{R}\times X), recording which identifier is missing. The behaviour would then distinguish states that differ only in the name of an absent record. That defeats the purpose of an allocation-blind observation (Definition 5.23) and would make Proposition 5.24 false.

Where the identification could distort a result, we exclude it by hypothesis. Every statement in Section 5.2 assumes an ancestry-closed store, and on such a store no ancestry behaviour contains a ⊥\bot observation at all, so the question does not arise. The one place it does real work is Proposition 6.4. There it is the ⊥\bot observation and not the successor structure that carries the argument: a ⊥\bot first component and a record first component differ whatever the outgoing edges are. The bisimulation exhibited in Proposition 5.24 pairs each absent identifier with itself, so no two distinct absent identifiers are ever identified there.

3.5 Bitemporal store observations

Definition 3.20 (Bitemporal observation). Let (S,α)(S, \alpha) be a lineage realisation and L⊆IdL \subseteq \mathrm{Id} a set of lineage roots. A bitemporal observation for (S,α)(S, \alpha) is a map obs ⁣:S×L×T×T→Rec⊥\mathrm{obs}\colon S \times L \times \mathbb{T}\times \mathbb{T}\to \mathrm{Rec}_{\bot}, read: obs(s,ℓ,τv,τt)\mathrm{obs}(s, \ell, \tau_{v}, \tau_{t}) is the record that, according to ss, was believed at transaction time τt\tau_{t} to describe the state of affairs at valid time τv\tau_{v} for the lineage rooted at ℓ\ell. The observation is degenerate in valid time when obs(s,ℓ,τv,τt)=obs(s,ℓ,τv′,τt)\mathrm{obs}(s, \ell, \tau_{v}, \tau_{t}) = \mathrm{obs}(s, \ell, \tau_{v}', \tau_{t}) for all τv,τv′\tau_{v}, \tau_{v}'. Given a map Ψ\Psi that records histories under lineage roots, the observation is sound for Ψ\Psi when obs(Ψ(h),ℓ,τv,τt)=σh(τv,τt)\mathrm{obs}(\Psi(h), \ell, \tau_v, \tau_t)=\sigma_h(\tau_v,\tau_t) for every recorded history hh, its root ℓ\ell, and both time arguments.

3.6 The laws

We now fix what it means for a lineage realisation to be well behaved. The laws are chosen so that each can be settled for a candidate implementation by reading its source, and so that each failure has an operational consequence rather than only an aesthetic one.

Definition 3.21 (Laws for a lineage realisation). Let (S,α)(S, \alpha) be a lineage realisation with output map out\mathrm{out} and transition map nx\mathrm{nx}. Let D⊆AD \subseteq A be the derivation calls, those of the symbols evolve\mathsf{evolve}, merge\mathsf{merge}, split\mathsf{split} and supersede\mathsf{supersede}. The realisation satisfies:

  1. Record monotonicity. For every ss, every a∈Da \in D and every ω\omega whose allocated identifiers lie outside supp(s)\mathrm{supp}(s), writing t=nx(s,a,ω)t = \mathrm{nx}(s, a, \omega): dom(ms)⊆dom(mt)\mathrm{dom}(m_{s}) \subseteq \mathrm{dom}(m_{t}) and mt(i)=ms(i)m_{t}(i) = m_{s}(i) for every i∈dom(ms)i \in \mathrm{dom}(m_{s}). No derivation call overwrites or deletes a record.

  2. Edge and record agreement. For every reachable ss and every i∈dom(ms)i \in \mathrm{dom}(m_{s}): the ancestry recorded in the payload of ms(i)m_{s}(i) and the ancestry recorded in EsE_{s} agree. That is, jj is named as a derivation source in the payload of ms(i)m_{s}(i) if and only if (i,ρ,j)∈Es(i, \rho, j) \in E_{s} for the corresponding ρ∈R−\rho \in \mathcal{R}^{-}.

  3. Inverse closure. Every store reachable from the empty store by derivation calls whose edge writes complete is inverse-closed in the sense of Definition 3.4.

  4. Namespace locality. Two clauses. (i) Confinement. For every ν∈N\nu \in N, writing Aν(s)⊆AA_{\nu}(s) \subseteq A for the calls all of whose identifier arguments name records of ss in the fibre ns−1(ν)\mathrm{ns}^{-1}(\nu), the square S→ α(−)(a,ω) Out×S↓πν↓id×πνS→ α(−)(a,ω) Out×S\begin{array}{ccc} S & \xrightarrow{\ \alpha(-)(a, \omega)\ } & \mathrm{Out}\times S \\[2pt] \downarrow{\scriptstyle \pi_{\nu}} & & \downarrow{\scriptstyle \mathrm{id}\times \pi_{\nu}} \\[2pt] S & \xrightarrow{\ \alpha(-)(a, \omega)\ } & \mathrm{Out}\times S \end{array} commutes at ss for every a∈D∩Aν(s)a \in D \cap A_{\nu}(s) and every ω\omega that allocates outside supp(s)\mathrm{supp}(s) and resolves identifiers by equality. Thus πν\pi_{\nu} is a homomorphism for the derivation fragment that names only ν\nu-records. (ii) Construction locality. For every call aa and every record rr constructed by α(s)(a,ω)\alpha(s)(a, \omega), the payload of rr is determined by the records of ss lying in the fibre ns−1(ns(r))\mathrm{ns}^{-1}(\mathrm{ns}(r)), by the non-identifier arguments of aa, and by ω\omega.

  5. Resolution independence. For all ss, aa and all ambient parameters ω=(k⃗,t,π)\omega = (\vec{k}, t, \pi) and ω′=(k⃗,t,π′)\omega' = (\vec{k}, t, \pi') with the same allocation part, α(s)(a,ω)=α(s)(a,ω′)\alpha(s)(a, \omega) = \alpha(s)(a, \omega'). A realisation may read the identifiers it is given and the clock; it may read no other ambient datum.

  6. Bitemporality. There is a recording map Ψ\Psi and a bitemporal observation for (S,α)(S, \alpha) that is sound for Ψ\Psi, not degenerate in valid time, and distinguishes a retroactive correction from a belated recording of a subsequent fact.

  7. Lineage referential integrity. Every reachable store is ancestry-closed in the sense of Definition 3.4.

Definition 3.22 (Namespace restriction). For ν∈N\nu \in N define πν(s)=(ms ⁣↾ν, Es ⁣↾ν)\pi_{\nu}(s) = (m_{s}\!\restriction_{\nu},\, E_{s}\!\restriction_{\nu}) where ms ⁣↾νm_{s}\!\restriction_{\nu} is the restriction of msm_{s} to {i∈dom(ms):ns(ms(i))=ν}\{ i \in \mathrm{dom}(m_{s}) : \mathrm{ns}(m_{s}(i)) = \nu \} and Es ⁣↾νE_{s}\!\restriction_{\nu} is the restriction of EsE_{s} to triples both of whose endpoints lie in dom(ms ⁣↾ν)\mathrm{dom}(m_{s}\!\restriction_{\nu}).

L1 and L7 are structural: they say the carrier grows and stays connected. L2 and L3 are redundancy laws: they say that two ways of recording the same fact do not diverge. L4 and L5 are locality laws: they say an operation depends only on what it names and on what its interface admits to reading. L6 is the temporal law.

Two remarks on the statement of L4. Its first clause is restricted to Aν(s)A_{\nu}(s) deliberately. Asking πν\pi_{\nu} to be an endomorphism for the whole alphabet would make the law false for any realisation whatever, since recall(i)\mathsf{recall}(i) for ii outside the fibre returns a record before restriction and an error after it. A law that no realisation can satisfy tells one nothing about the realisation at hand. The interesting question is whether operations that stay inside a namespace have effects that stay inside it, and that is clause (i). Clause (ii) is the separate question of whether an operation can pull payload across the boundary, and it is the clause that fails below. The seven laws are not independent in a logical sense, but no one of them entails another for the implementation we examine, and each is settled separately in Section 5.

3.7 The derivation schema

The positive results of Section 5 are theorems about any realisation whose derivation operations have the following shape. Isolating that shape as a hypothesis lets the mathematics be proved once, independently of any repository, and lets the reading of the source enter at exactly one point (Lemma 5.6).

Definition 3.23 (Derivation schema). A lineage realisation follows the derivation schema when, for every store ss, every derivation call a∈Da \in D and every ambient parameter ω\omega whose allocated identifiers lie outside supp(s)\mathrm{supp}(s), writing t=nx(s,a,ω)t = \mathrm{nx}(s, a, \omega) and I(a)⊆IdI(a) \subseteq \mathrm{Id} for the identifiers named in aa:

  1. Guarded retrieval. No record outside I(a)∩dom(ms)I(a) \cap \mathrm{dom}(m_{s}) is read. If the call constructs a record and some i∈I(a)i \in I(a) is absent from dom(ms)\mathrm{dom}(m_{s}), then either the output is err\mathsf{err} and t=st = s, or the absent identifier is dropped and the call proceeds on the remainder. A call that constructs no record need not retrieve anything.

  2. Fresh construction. mtm_{t} extends msm_{s}, and every identifier in dom(mt)∖dom(ms)\mathrm{dom}(m_{t}) \setminus \mathrm{dom}(m_{s}) is one of the identifiers allocated by ω\omega. Each constructed record is a function of the retrieved records, the non-identifier arguments of aa and ω\omega.

  3. Namespace inheritance. Each constructed record has the namespace of one of the retrieved records.

  4. Edge polarity. Es⊆EtE_{s} \subseteq E_{t}, and every triple in Et∖EsE_{t} \setminus E_{s} is either (κ,ρ,i)(\kappa, \rho, i) with ρ∈R−\rho \in \mathcal{R}^{-}, κ\kappa allocated by ω\omega and i∈I(a)∩dom(ms)i \in I(a) \cap \mathrm{dom}(m_{s}), or (i,ρ‾,κ)(i, \overline{\rho}, \kappa) with the same data, or a pair (i,ρ′,j)(i, \rho', j), (j,ρ′‾,i)(j, \overline{\rho'}, i) with i,j∈I(a)i, j \in I(a) and ρ′∉R−∪R+\rho' \notin \mathcal{R}^{-}\cup \mathcal{R}^{+}. Only the first two forms are required to have an endpoint in dom(ms)\mathrm{dom}(m_{s}); the third may name an absent identifier, since its label is outside the ancestry vocabulary and Theorem 5.7 never inspects it.

S4 is the load-bearing clause. A derivation call may attach an ancestry-labelled edge only to an identifier it has just created. It may attach an edge to a pre-existing identifier only in the descendant direction, or with a label outside the ancestry vocabulary altogether. Everything in Section 5.2 follows from that clause.

4 The system

ContextFS is a memory layer for coding agents, written in Python. At commit a93035d its state lives in a SQLite database, with an optional vector store for similarity search and an optional graph database for edge queries. SQLite is the authoritative store on every write path (storage_router.py, save, line 89). Three surfaces expose the same operations: a Python facade (core.py, class ContextFS, line 34), a Model Context Protocol server (mcp/fastmcp_server.py) and a command line (cli/memory.py). Throughout this paper, ContextFS paths are relative to src/contextfs/ in the repository at /Users/mlong/Documents/Development/contextfs-ai/contextfs, and line numbers are those of commit a93035d.

4.1 Model to code

Table 1 gives the correspondence. Every identifier in the right-hand column was read at a93035d before being listed.

Model objects and the ContextFS identifiers that realise them at commit a93035d.
Model object ContextFS identifier at a93035d
Rec\mathrm{Rec}, the record set class Memory(BaseModel), schemas.py:1362
Id\mathrm{Id}, identifiers Memory.id, default str(uuid.uuid4())[:12], schemas.py:1371
NN, namespaces Memory.namespace_id, schemas.py:1384
TT, record types class MemoryType(str, Enum), schemas.py:23
R\mathcal{R}, relation labels class EdgeRelation(str, Enum), storage_protocol.py:32
(−)‾\overline{(-)}, the involution EdgeRelation.get_inverse, storage_protocol.py:80
R−\mathcal{R}^{-}, ancestry labels EVOLVED_FROM, MERGED_FROM, SPLIT_FROM
EsE_{s}, the edge set memory_edges table, created by migrations/versions/003_add_memory_edges.py:56--69, written by StorageRouter.add_edge, storage_router.py:1327
SS, stores the memories table (core.py:203--220) together with the memory_edges table
α\alpha, the structure map class MemoryLineage, memory_lineage.py:61, plus ContextFS.save/recall
save\mathsf{save} ContextFS.save, core.py:673; StorageRouter.save, storage_router.py:89
recall\mathsf{recall} ContextFS.recall, core.py:1288; StorageRouter.recall, storage_router.py:290
evolve\mathsf{evolve} MemoryLineage.evolve, memory_lineage.py:93
merge\mathsf{merge} MemoryLineage.merge, memory_lineage.py:191
split\mathsf{split} MemoryLineage.split, memory_lineage.py:360
supersede\mathsf{supersede} MemoryLineage.supersede, memory_lineage.py:508
link\mathsf{link} MemoryLineage.link, memory_lineage.py:551
Θ\Theta, merge strategies class MergeStrategy(str, Enum), memory_lineage.py:43
Ξ\Xi, change reasons class ChangeReason(str, Enum), types/versioned.py:44
βs\beta_{s}, lineage observation StorageRouter.get_lineage, storage_router.py:1683
[ ⁣[i] ⁣]s−[\![i]\!]^{-}_{s}, ancestry behaviour approximated by MemoryLineage.get_history, memory_lineage.py:599
obs\mathrm{obs}, timed observation Timeline.at, types/versioned.py:238
abstract carrier signature class StorageBackend(Protocol), storage_protocol.py:148
abstract edge signature class GraphBackend(Protocol), storage_protocol.py:347

4.2 The relation vocabulary

Listing 1 shows the head of the label enumeration. At a93035d it has twenty-two members, of which twenty appear in the inverse table of EdgeRelation.get_inverse and two, DEPENDS_ON and IMPLEMENTS, do not and therefore fall through the table’s default.

Listing 1. src/contextfs/storage_protocol.py, lines 32-49 with the class docstring and blank lines elided, commit a93035d. The first six of the twenty-two labels.

class EdgeRelation(str, Enum):
    # Evolution relationships
    EVOLVED_INTO = "evolved_into"
    EVOLVED_FROM = "evolved_from"
    # Merge relationships
    MERGED_INTO = "merged_into"
    MERGED_FROM = "merged_from"
    # Split relationships
    SPLIT_INTO = "split_into"
    SPLIT_FROM = "split_from"

Lemma 4.1 (The inverse map is an involution). Let (−)‾\overline{(-)} denote EdgeRelation.get_inverse at a93035d: the function that looks a label up in the table at lines 82 to 104 of storage_protocol.py and returns the label itself when it is absent. Then ρ‾‾=ρ\overline{\overline{\rho}} = \rho for every ρ∈R\rho \in \mathcal{R}, so (R,(−)‾)(\mathcal{R}, \overline{(-)}) is an inverse structure in the sense of Definition 3.2, with four fixed points.

Proof. The table lists twenty entries. Eighteen of them form the nine unordered pairs

EVOLVED_INTO/EVOLVED_FROM MERGED_INTO/MERGED_FROM SPLIT_INTO/SPLIT_FROM
REFERENCES/REFERENCED_BY SUPERSEDES/SUPERSEDED_BY PARENT_OF/CHILD_OF
PART_OF/CONTAINS CAUSED_BY/CAUSES RESOLVES/RESOLVED_BY

each of which appears in the table in both orders, so ρ‾‾=ρ\overline{\overline{\rho}} = \rho on those eighteen. The remaining two table entries send RELATED_TO and CONTRADICTS to themselves. The two labels absent from the table, DEPENDS_ON and IMPLEMENTS, are returned unchanged by the default of the lookup. Every label is therefore either paired with a distinct label that is paired back with it, or fixed, and the map is an involution whose fixed-point set is {RELATED_TO\{\texttt{RELATED\_TO}, CONTRADICTS\texttt{CONTRADICTS}, DEPENDS_ON\texttt{DEPENDS\_ON}, IMPLEMENTS}\texttt{IMPLEMENTS}\}. ◻

Remark 4.2. The two implicit fixed points are not the same kind of object as the two explicit ones. RELATED_TO and CONTRADICTS are symmetric relations and are annotated as such in the source. DEPENDS_ON and IMPLEMENTS are directed relations that acquire a fixed point only because the lookup defaults to the identity on a missing key. The involution law holds, but for one of these pairs it holds by accident of the default rather than by design.

4.3 Derivation

Listing 2 is the core of evolve\mathsf{evolve}. Three features matter below. A new Memory is constructed rather than the original mutated. The ancestry is written into the new record’s metadata unconditionally. The two edges are written inside an exception handler that logs and continues.

Listing 2. src/contextfs/memory_lineage.py, lines 147-182 abridged, commit a93035d. Four constructor keyword arguments (summary, source_repo, project, source_tool) are elided.

evolved = Memory(
    content=new_content, type=original.type, tags=tags,
    namespace_id=original.namespace_id,
    metadata={**original.metadata,
              "evolved_from": memory_id,
              "evolution_timestamp": datetime.now(timezone.utc).isoformat(),
              "change_reason": reason.value})
self._storage.save(evolved)
try:
    self._storage.add_edge(from_id=evolved.id, to_id=memory_id,
                           relation=EdgeRelation.EVOLVED_FROM)
    self._storage.add_edge(from_id=memory_id, to_id=evolved.id,
                           relation=EdgeRelation.EVOLVED_INTO)
except Exception as e:
    logger.warning(f"Failed to create evolution edges: {e}")

merge and split follow the same shape: construct new records, write merged_from or split_from into metadata, then attempt the edge pairs inside a handler. supersede writes only edges, with the timestamp in the edge metadata.

Listing 3 shows the attribute selection performed by merge under the default union strategy. The namespace and type of the merged record are selected here and in the constructor call at line 256, and both selections are the subject of results in Section 5.

Listing 3. src/contextfs/memory_lineage.py, two non-adjacent excerpts, commit a93035d: lines 298-307 (the union branch of _apply_merge_strategy, with its comments elided) and line 256 (the namespace argument of the merged record's constructor). The two are separated below by an inserted marker line.

if strategy == MergeStrategy.UNION:
    all_tags = [tag for m in memories for tag in m.tags]
    combined_meta = {}
    for m in memories:
        combined_meta.update(m.metadata)
    types = [m.type for m in memories]
    final_type = max(set(types), key=types.count)
### line 256, in the merged record's constructor:
            namespace_id=memories[0].namespace_id,

4.4 Observation

Two observation paths exist and they are not the same. recall resolves an identifier against SQLite by prefix (Listing 4). Timeline.at answers the timed question over an in-memory version list (Listing 5).

Listing 4. src/contextfs/storage_router.py, lines 310-326 abridged, commit a93035d.

def _recall_from_sqlite(self, memory_id: str) -> Memory | None:
    conn = sqlite3.connect(self._db_path)
    cursor = conn.cursor()
    try:
        cursor.execute(
            "SELECT * FROM memories WHERE id LIKE ? AND deleted_at IS NULL",
            (f"{memory_id}
        )
        row = cursor.fetchone()
        if row:
            return self._row_to_memory(row)
        return None
    finally:
        conn.close()

Listing 5. src/contextfs/types/versioned.py, lines 238-263 abridged, commit a93035d.

def at(self, timestamp: datetime) -> VersionEntry[S] | None:
    ts = timestamp
    if ts.tzinfo is None:
        ts = ts.replace(tzinfo=timezone.utc)
    result = None
    for entry in self.entries:
        entry_ts = entry.timestamp
        if entry_ts.tzinfo is None:
            entry_ts = entry_ts.replace(tzinfo=timezone.utc)
        if entry_ts <= ts:
            result = entry
        else:
            break
    return result

Timeline.at compares against a single timestamp field of VersionEntry (types/versioned.py:117). There is no second time field to compare against, which is the content of Theorem 5.27.

4.5 The agent-facing surface

Listing 6 shows the tail of the facade’s evolve. The reason parameter of MemoryLineage.evolve is not among the arguments forwarded, so the default ChangeReason.OBSERVATION is used. The Model Context Protocol tool (mcp/fastmcp_server.py:623) and the command line (cli/memory.py:247) call this same facade method and likewise do not expose a change reason.

Listing 6. src/contextfs/core.py, lines 1545-1554, commit a93035d.

if preserve_tags is None:
            preserve_tags = self.config.lineage_preserve_tags

        return self._lineage.evolve(
            memory_id=memory_id,
            new_content=new_content,
            summary=summary,
            preserve_tags=preserve_tags,
            additional_tags=additional_tags,
        )

5 Law checking

Throughout this section (S,α)(S, \alpha) is the lineage realisation given by ContextFS at a93035d under the correspondence of Table 1, with R−={EVOLVED_FROM,MERGED_FROM,SPLIT_FROM}\mathcal{R}^{-}= \{\texttt{EVOLVED\_FROM}, \texttt{MERGED\_FROM}, \texttt{SPLIT\_FROM}\}. Every claim about the code is a claim about the source at that commit.

5.1 L1, record monotonicity

Proposition 5.1 (Derivation calls are monotone). Every derivation call satisfies L1: for a∈Da \in D and any ω\omega whose allocated identifiers lie outside supp(s)\mathrm{supp}(s), writing t=nx(s,a,ω)t = \mathrm{nx}(s, a, \omega), we have dom(ms)⊆dom(mt)\mathrm{dom}(m_{s}) \subseteq \mathrm{dom}(m_{t}) and mt ⁣↾dom(ms)=msm_{t}\!\restriction_{\mathrm{dom}(m_{s})} = m_{s}, while ∣dom(mt)∣−∣dom(ms)∣|\mathrm{dom}(m_{t})| - |\mathrm{dom}(m_{s})| is 11 for evolve\mathsf{evolve} and merge\mathsf{merge}, kk for a kk-part split\mathsf{split}, and 00 for supersede\mathsf{supersede}.

Proof. Three of the four methods, evolve, merge and split, obtain their inputs through self._storage.recall, which returns a Memory object read out of SQLite. Each then constructs one or more new Memory instances with the default identifier factory (schemas.py:1371) and persists them with self._storage.save applied to a newly constructed instance (memory_lineage.py:165, 269, 436). The fourth, supersede, retrieves nothing, constructs nothing and issues no save at all (memory_lineage.py, lines 508 to 545). It writes two edges and returns, so it is monotone trivially. No method assigns to a field of a retrieved object, and none issues a write against the identifier of a retrieved object. Since the constructed identifiers are drawn fresh, the pre-existing part of msm_{s} is untouched. The domain grows by exactly the number of constructed records, which is one for evolve, one for merge, len(parts) for split and zero for supersede. ◻

Proposition 5.2 (Two operations outside DD break monotonicity). L1 is stated for derivation calls only, and the restriction is necessary: at a93035d the implementation has two write paths outside DD that are not monotone. A save issued under an existing identifier replaces the stored record, and a deletion removes the record together with every edge incident to it in either direction.

Evidence. For the first path, StorageRouter._save_to_sqlite issues INSERT OR REPLACE INTO memories (storage_router.py:172), and ContextFS.save accepts an explicit id argument (core.py:686). For the second, StorageRouter.delete (storage_router.py:586) resolves the identifier and calls _delete_edges_for_memory, which issues DELETE FROM memory_edges WHERE from_id = ? OR to_id = ? (storage_router.py:626,639), before deleting the record itself. Deletion is therefore hard rather than soft, on the record and on every incident edge. ◻

Proposition 5.3 (Identifier freshness is assumed, not checked). Every statement about derivation calls in this paper carries the hypothesis that the ambient parameter allocates identifiers fresh for the store. At a93035d nothing in the implementation establishes that hypothesis, and the write path converts a violation of it into a silent replacement rather than an error.

Evidence. Memory.id has default factory str(uuid.uuid4())[:12] (schemas.py:1371). The value is drawn at random and is not compared against the store: neither MemoryLineage nor StorageRouter.save (storage_router.py:89) queries for an existing row with that identifier before writing, and _save_to_sqlite issues INSERT OR REPLACE INTO memories (line 172). A drawn identifier that collides with a stored one therefore replaces that row, and the collision is not reported. ◻

Remark 5.4 (The freshness hypothesis and where it is discharged). This is a hypothesis in the model, so it is worth being exact about its status. In Definition 3.8 the allocated identifiers are a component of the ambient parameter, and Theorem 5.7 quantifies over parameters whose identifiers are fresh. That is a legitimate way to state a theorem, and the theorem is true as stated. What Proposition 5.3 adds is that the implementation does not restrict itself to such parameters: it samples them, so the hypothesis holds with high probability rather than by construction. The scale at which the probability stops being negligible is given in Remark 6.8: roughly five million records for a collision between two identifiers. That is a far larger store than the one at which Proposition 6.3 bites. Read together, the two say that identifier discipline in ContextFS degrades in a specific order as a store grows: abbreviated lookups become ambiguous first, by about two orders of magnitude, and outright allocation collisions much later. A realisation that checked freshness on write, or that drew identifiers from a counter, would discharge the hypothesis and lose nothing else.

Remark 5.5. The deletion path has a further consequence. Because _delete_edges_for_memory removes edges in which the deleted memory is either endpoint, deleting an ancestor erases that ancestry relation from every descendant that pointed at it. The resulting store remains ancestry-closed: the incident edges disappear with the record. What fails is cross-state preservation of previously recorded ancestry, including for descendants the caller did not name. Deletion is the one operation in the API that can erase this part of the past.

5.2 Stable ancestry

L1 says records survive. It does not say that what can be observed about a record survives, and in general it does not: evolve adds an EVOLVED_INTO edge out of the original, so the original’s outgoing edge set changes. The content of the following theorem is that the change is confined to the descendant direction.

Lemma 5.6 (ContextFS instantiates the schema). For every ambient parameter whose allocated identifiers are fresh for the store, the lineage realisation given by ContextFS at a93035d follows the derivation schema of Definition 3.23. This is an instantiation claim in the sense of Section 1.3, established by reading the four methods of src/contextfs/memory_lineage.py. The freshness proviso is not vacuous and is discussed in Proposition 5.3.

Evidence. S1: evolve and split call self._storage.recall on their single named identifier and raise ValueError when it returns None (lines 132 to 134, 397 to 399). merge loops over its named identifiers, appends those that resolve and logs a warning for those that do not, then raises when fewer than two resolve (lines 225 to 232). supersede constructs no record and performs no retrieval at all: it calls add_edge directly on its two named identifiers (lines 525 to 540), which is the case S1’s last sentence allows, and which is sound here only because both labels it writes lie outside R−∪R+\mathcal{R}^{-}\cup \mathcal{R}^{+}. No method reads a record it did not name.

S2 and S3: each of evolve, merge and split constructs Memory instances with the default identifier factory (schemas.py:1371) and persists them with self._storage.save (lines 165, 269, 436); supersede constructs none. S2 also requires the constructed identifiers to be the ones the ambient parameter allocates, which is the freshness hypothesis; the implementation samples rather than checks it, and the exact status of that hypothesis is Proposition 5.3 and Remark 5.4. The lemma is therefore stated for ambient parameters whose allocated identifiers are fresh for the store, as are all the results that use it. The namespace of each constructed record is copied from a retrieved record: namespace_id=original.namespace_id in evolve (line 152) and split (line 422), namespace_id=memories[0].namespace_id in merge (line 256).

S4: the added triples are, for evolve, (κ,EVOLVED_FROM,i)(\kappa, \texttt{EVOLVED\_FROM}, i) and (i,EVOLVED_INTO,κ)(i, \texttt{EVOLVED\_INTO}, \kappa) with κ\kappa the constructed identifier (lines 170 to 180); for merge, (κ,MERGED_FROM,ir)(\kappa, \texttt{MERGED\_FROM}, i_{r}) and (ir,MERGED_INTO,κ)(i_{r}, \texttt{MERGED\_INTO}, \kappa) for each retrieved iri_{r} (lines 275 to 285); for split, (κr,SPLIT_FROM,i)(\kappa_{r}, \texttt{SPLIT\_FROM}, i) and (i,SPLIT_INTO,κr)(i, \texttt{SPLIT\_INTO}, \kappa_{r}) for each constructed κr\kappa_{r} (lines 442 to 452); and for supersede, (j,SUPERSEDES,i)(j, \texttt{SUPERSEDES}, i) and (i,SUPERSEDED_BY,j)(i, \texttt{SUPERSEDED\_BY}, j) between two named identifiers (lines 526 to 540), with SUPERSEDES and SUPERSEDED_BY outside R−∪R+\mathcal{R}^{-}\cup \mathcal{R}^{+}. No method removes an edge. The labels used are inverse to one another by Lemma 4.1.

The point on which Theorem 5.7 turns is best made here rather than left to the proof. In each of the first three methods the two added edges share their pair of endpoints and differ only in direction and label. The label pointing from the pre-existing identifier to the constructed one is always the R+\mathcal{R}^{+} member of the pair, never the R−\mathcal{R}^{-} member: EVOLVED_INTO rather than EVOLVED_FROM out of memory_id, MERGED_INTO rather than MERGED_FROM out of each original.id, SPLIT_INTO rather than SPLIT_FROM out of memory_id. Since R−∩R+=∅\mathcal{R}^{-}\cap \mathcal{R}^{+} = \emptyset by Definition 3.3, no edge with a label in R−\mathcal{R}^{-} acquires a pre-existing source, which is clause S4 and what the theorem uses. ◻

Theorem 5.7 (Ancestry immutability). Let (S,α)(S, \alpha) be any lineage realisation following the derivation schema. Let ss be an ancestry-closed store, let a∈Da \in D and let ω\omega be an ambient parameter whose identifiers are fresh for ss. Put t=nx(s,a,ω)t = \mathrm{nx}(s, a, \omega) and D0=dom(ms)D_{0} = \mathrm{dom}(m_{s}). Then tt is ancestry-closed, βs−\beta^{-}_{s} restricts to a coalgebra structure on D0D_{0}, and the inclusion ι ⁣:D0↪Id\iota \colon D_{0} \hookrightarrow \mathrm{Id} is a homomorphism of H−H^{-}-coalgebras ι ⁣:(D0,βs− ⁣↾D0)⟶(Id,βt−).\iota \colon (D_{0}, \beta^{-}_{s}\!\restriction_{D_{0}}) \longrightarrow (\mathrm{Id}, \beta^{-}_{t}).

Proof. Write Δ=Et∖Es\Delta = E_{t} \setminus E_{s}, which is well defined and satisfies Es⊆EtE_{s} \subseteq E_{t} by S4.

Claim: every triple in Δ\Delta whose label lies in R−\mathcal{R}^{-} has its source outside D0D_{0}. By S4 a triple in Δ\Delta is of one of three forms. In the first its label lies in R−\mathcal{R}^{-} and its source is an identifier allocated by ω\omega, hence fresh for ss and so not in D0D_{0}. In the second its label lies in R+\mathcal{R}^{+}, which is disjoint from R−\mathcal{R}^{-} by Definition 3.3. In the third its label lies outside R−∪R+\mathcal{R}^{-}\cup \mathcal{R}^{+}. Only the first form contributes a triple with label in R−\mathcal{R}^{-}, and it has source outside D0D_{0}.

Ancestry closure of tt. Let i∈dom(mt)i \in \mathrm{dom}(m_{t}) and (i,ρ,j)∈Et(i, \rho, j) \in E_{t} with ρ∈R−\rho \in \mathcal{R}^{-}. If the triple lies in EsE_{s} then i∈D0i \in D_{0}, since an identifier in dom(mt)∖D0\mathrm{dom}(m_{t}) \setminus D_{0} is fresh for ss by S2 and so occurs in no triple of EsE_{s}; ancestry closure of ss then gives j∈D0⊆dom(mt)j \in D_{0} \subseteq \mathrm{dom}(m_{t}) by S2. If instead the triple lies in Δ\Delta then by the claim it has the first form of S4, whose target lies in I(a)∩dom(ms)⊆D0⊆dom(mt)I(a) \cap \mathrm{dom}(m_{s}) \subseteq D_{0} \subseteq \mathrm{dom}(m_{t}).

βs−\beta^{-}_{s} restricts. For i∈D0i \in D_{0}, ancestry closure of ss gives that every R−\mathcal{R}^{-}-labelled edge out of ii lands in D0D_{0}, so βs−(i)∈Rec⊥×Pfin(R−×D0)\beta^{-}_{s}(i) \in \mathrm{Rec}_{\bot} \times \mathcal{P}_{\mathrm{fin}}(\mathcal{R}^{-}\times D_{0}) and the restriction is a map D0→H−D0D_{0} \to H^{-}D_{0}.

The homomorphism condition. Let i∈D0i \in D_{0}. We must show βt−(ι(i))=H−ι(βs−(i))\beta^{-}_{t}(\iota(i)) = H^{-}\iota\big(\beta^{-}_{s}(i)\big). The first components agree because mtm_{t} extends msm_{s} by S2. Since H−ιH^{-}\iota acts as the identity on labels and as ι\iota on targets, the second components agree exactly when {(ρ,j):(i,ρ,j)∈Et, ρ∈R−}  =  {(ρ,j):(i,ρ,j)∈Es, ρ∈R−}.\{(\rho, j) : (i, \rho, j) \in E_{t},\ \rho \in \mathcal{R}^{-}\} \;=\; \{(\rho, j) : (i, \rho, j) \in E_{s},\ \rho \in \mathcal{R}^{-}\}. The inclusion from right to left is Es⊆EtE_{s} \subseteq E_{t}. For the other, a triple (i,ρ,j)∈Et∖Es(i, \rho, j) \in E_{t} \setminus E_{s} with ρ∈R−\rho \in \mathcal{R}^{-} lies in Δ\Delta, so by the claim i∉D0i \notin D_{0}, contradicting i∈D0i \in D_{0}. Hence ι\iota is a homomorphism. ◻

Corollary 5.8. The conclusion of Theorem 5.7 holds of ContextFS at a93035d, conditionally on Lemma 5.6.

The hypothesis of ancestry closure needs justifying rather than assuming, because Proposition 5.30 below shows that L7 fails: not every store of SS is ancestry-closed. What rescues the theorem is that ancestry closure is an invariant of the fragment the theorem is about, so the relevant stores are not merely hypothetical.

Proposition 5.9 (Ancestry closure is reachable and is an invariant). For any realisation following the derivation schema, the empty store is ancestry-closed. Every derivation call takes an ancestry-closed store to an ancestry-closed store, and so does every call that adds a record with a fresh identifier and no edges. Consequently every store reachable from the empty store by such calls is ancestry-closed, and Theorem 5.7 applies at every step of every such run.

Proof. The empty store has no edges, so it is vacuously ancestry-closed. Preservation under derivation calls is the first conclusion of Theorem 5.7. A call that adds a record at a fresh identifier and no edges leaves EE unchanged and enlarges dom(m)\mathrm{dom}(m), and ancestry closure is monotone in dom(m)\mathrm{dom}(m) for fixed EE. Induction on the length of the run. ◻

Proposition 5.10 (The unchecked escape route). At a93035d, MemoryLineage.link is a code path outside the fragment of Proposition 5.9 that can produce a store that is not ancestry-closed.

Evidence. MemoryLineage.link (memory_lineage.py:551) accepts an arbitrary relation argument, including an ancestry label, and calls add_edge without retrieving either endpoint. add_edge takes validate: bool = False (storage_router.py:1327) and the call site omits the argument. The facade ContextFS.link (core.py:1798) is not such a path: it recalls both endpoints and returns False when either is absent (lines 1827 to 1831), then passes the resolved full identifiers. ◻

Remark 5.11. StorageRouter.delete does not supply a second route. It resolves the identifier twice, once before deleting incident edges and once before deleting the row. The memory set is unchanged between those queries, so the fixed resolution parameter π\pi of Definition 3.8 selects the same row both times. An ambiguous prefix still leaves the identity of the deleted row dependent on an order the interface does not expose, as in Proposition 6.3, but it does not leave a dangling ancestry edge in this model.

Corollary 5.12 (Ancestry behaviour is invariant). Under the hypotheses of Theorem 5.7, [ ⁣[i] ⁣]t−=[ ⁣[i] ⁣]s−[\![i]\!]^{-}_{t} = [\![i]\!]^{-}_{s} for every i∈dom(ms)i \in \mathrm{dom}(m_{s}): the ancestry unfolding of a memory is unchanged by any later derivation call. Consequently, for any i,i′∈dom(ms)i, i' \in \mathrm{dom}(m_{s}), ii and i′i' are H−H^{-}-bisimilar in ss if and only if they are H−H^{-}-bisimilar in tt.

Proof. Behaviour maps into a final coalgebra compose with homomorphisms: since ι\iota is a homomorphism and [ ⁣[−] ⁣]t−[\![-]\!]^{-}_{t} is the unique homomorphism out of (Id,βt−)(\mathrm{Id}, \beta^{-}_{t}), the composite [ ⁣[−] ⁣]t−∘ι[\![-]\!]^{-}_{t} \circ \iota is a homomorphism out of (D0,βs− ⁣↾D0)(D_{0}, \beta^{-}_{s}\!\restriction_{D_{0}}), as is the restriction of [ ⁣[−] ⁣]s−[\![-]\!]^{-}_{s}. By finality these coincide, which is the displayed equality. The bisimilarity statement follows because H−H^{-} preserves weak pullbacks (Lemma 3.17), so bisimilarity is equality of behaviour. ◻

Proposition 5.13 (Descendant structure is not stable). Theorem 5.7 fails for HH in place of H−H^{-}. Concretely, let ss contain a single record at ii with no edges, let a=evolve(i,c,ξ)a = \mathsf{evolve}(i, c, \xi) and let ω\omega allocate κ\kappa. Then βs(i)\beta_{s}(i) has empty edge component while βt(i)\beta_{t}(i) has edge component {(EVOLVED_INTO,κ)}\{(\texttt{EVOLVED\_INTO}, \kappa)\}, so ι\iota is not an HH-homomorphism and [ ⁣[i] ⁣]t≠[ ⁣[i] ⁣]s[\![i]\!]_{t} \neq [\![i]\!]_{s}.

Proof. Immediate from the enumeration of added edges in the proof of Theorem 5.7: the call adds (i,EVOLVED_INTO,κ)(i, \texttt{EVOLVED\_INTO}, \kappa) with source i∈D0i \in D_{0}. The two behaviours differ because a state with no outgoing transitions is not bisimilar to a state with one. ◻

The asymmetry is the useful content. A consumer of ContextFS may cache the ancestry of a memory and rely on it. It may not cache the descendants. In the terms of the series, this is the structural property the memory layer contributes to any composite that contains it, and Section 6 states what a skill or a harness is entitled to conclude from it.

5.3 L2, edge and record agreement

Proposition 5.14 (Ancestry is recorded twice and the two records can disagree). L2 fails. The implementation at a93035d maintains two independent representations of ancestry, and the write of one is guarded while the write of the other is not.

Proof. In evolve, the key "evolved_from" is written into the constructed record’s metadata dictionary as part of the Memory(...) constructor call (memory_lineage.py:158), and the record is then persisted by an unguarded self._storage.save (line 165). The two EVOLVED_FROM/EVOLVED_INTO edges are written afterwards inside try: ... except Exception as e: logger.warning(...) (lines 168 to 182). Any exception raised by add_edge, including one raised by the first of the two calls before the second is attempted, is caught, logged at warning level, and not re-raised. Control then falls through to return evolved. The same structure appears in merge (metadata at line 262, guarded edges at lines 273 to 287) and in split (metadata at lines 426 to 432, guarded edges at lines 440 to 454). Hence a store is reachable in which ms(κ)m_{s}(\kappa) carries metadata["evolved_from"] = i while (κ,EVOLVED_FROM,i)∉Es(\kappa, \texttt{EVOLVED\_FROM}, i) \notin E_{s}, which is a violation of L2. ◻

Remark 5.15 (The two representations are read by different code paths). The divergence is not merely latent, because both representations are read. StorageRouter.get_lineage (storage_router.py:1683) first delegates to the graph backend if one is configured, then queries the edge table. If the edge query yields neither ancestors nor descendants, it falls back to reading evolved_from, merged_from and split_from out of the record’s metadata (lines 1712 to 1739). MemoryLineage.get_history (memory_lineage.py:599) calls that method and then applies a second, and differently shaped, metadata fallback of its own (lines 639 to 663). The two fallbacks do not agree on the shape of an ancestor entry: the router emits dictionaries keyed "id", "relation", "depth" (line 1274), while get_history emits dictionaries keyed "memory_id" and "relation" with no depth (lines 641 to 646). A consumer of the lineage observation therefore cannot rely on a single field name for the ancestor identifier, which is a well-typedness failure of the observation and not only a redundancy failure.

5.4 L3, inverse closure

Proposition 5.16 (Inverse closure holds on the unexceptional path). Every derivation call at a93035d writes edges in inverse pairs, so along any execution in which no add_edge call raises, every reachable store is inverse-closed. The property is not maintained unconditionally: because the two writes of a pair are separate calls inside one handler, an exception raised by the second leaves the first in place.

Proof. The enumeration in the proof of Theorem 5.7 lists, for each derivation call, the added triples in (−)‾\overline{(-)}-inverse pairs, and Lemma 4.1 shows the labels used are genuinely inverse to one another. link writes the inverse only when its bidirectional argument is true, and then obtains the inverse label through EdgeRelation.get_inverse (memory_lineage.py, lines 580 to 587). So link\mathsf{link} calls with b=0b = 0 do not preserve inverse closure, which is why L3 is stated for derivation calls. For the failure clause, the two add_edge calls of a pair are consecutive statements inside a single try block (for instance memory_lineage.py, lines 170 to 180), so an exception in the second is caught by the same handler that would have caught one in the first, and the effect of the first is not undone. ◻

5.5 L4, namespace locality

The two clauses of L4 come apart, and which one holds is the useful information.

Proposition 5.17 (Confinement holds). L4(i) holds for derivation calls under resolution by equality. Let ν∈N\nu \in N, let ss be a store, let a∈D∩Aν(s)a \in D \cap A_{\nu}(s) and let ω\omega be an ambient parameter allocating identifiers fresh for ss. Then α(πνs)(a,ω)=(id×πν)(α(s)(a,ω))\alpha(\pi_{\nu}s)(a, \omega) = (\mathrm{id}\times \pi_{\nu})\big(\alpha(s)(a, \omega)\big).

Proof. Every identifier named in aa lies in dom(mπνs)\mathrm{dom}(m_{\pi_{\nu}s}) by the definition of Aν(s)A_{\nu}(s), and the record stored at each is the same in ss and πνs\pi_{\nu}s, since πν\pi_{\nu} restricts the domain and does not alter values. Under resolution by equality the three retrieving methods therefore obtain the same records in both stores, so they raise in neither. The records they construct are equal, since each constructed record is a function of the retrieved records, the non-identifier arguments and ω\omega. supersede retrieves nothing, and its output is a function of its two named identifiers alone. Outputs therefore agree.

For the state component we must check that the added data survives restriction to ν\nu. Each constructed record takes its namespace from a named argument: evolve and split set namespace_id=original.namespace_id (memory_lineage.py:152, 422) and merge sets namespace_id=memories[0].namespace_id (line 256). Since every named argument lies in the fibre ns−1(ν)\mathrm{ns}^{-1}(\nu), every constructed record does too, so it is retained by πν\pi_{\nu}. Each added edge is incident to one named identifier and one constructed identifier, both now in the fibre, so it is retained by the edge clause of Definition 3.22. supersede adds no record and two edges between named identifiers, both in the fibre. Hence nx(πνs,a,ω)=πν(nx(s,a,ω))\mathrm{nx}(\pi_{\nu}s, a, \omega) = \pi_{\nu}\big(\mathrm{nx}(s, a, \omega)\big). ◻

Proposition 5.18 (Construction locality fails). L4(ii) fails. There is a store ss, a call aa and an ambient parameter ω\omega such that the record constructed by α(s)(a,ω)\alpha(s)(a, \omega) lies in the fibre ns−1(ν)\mathrm{ns}^{-1}(\nu) while its payload depends on a record of ss lying in a different fibre.

Proof. Let ss contain records at i1,i2i_{1}, i_{2} with ns=ν\mathrm{ns}= \nu and at i3i_{3} with ns=ν′≠ν\mathrm{ns}= \nu' \neq \nu, where the record at i3i_{3} carries a metadata key kk absent from the other two and content distinct from theirs. Let a=merge(⟨i1,i3,i2⟩,⊥,UNION)a = \mathsf{merge}(\langle i_{1}, i_{3}, i_{2}\rangle, \bot, \texttt{UNION}). The retrieval loop (memory_lineage.py, lines 225 to 230) resolves all three identifiers, and the guard at line 232 passes. The constructor at line 256 sets namespace_id=memories[0].namespace_id =ν= \nu, so the constructed record lies in the fibre ns−1(ν)\mathrm{ns}^{-1}(\nu). Its content is produced by _generate_merged_content (lines 349 to 354), which concatenates a truncation of each argument’s content in argument order, so it contains the content of the record at i3i_{3}. Its metadata is produced by the union branch of _apply_merge_strategy, whose loop combined_meta.update(m.metadata) (lines 303 to 304) runs over every retrieved record, so it contains kk. The payload of a record in the fibre ns−1(ν)\mathrm{ns}^{-1}(\nu) therefore depends on a record outside that fibre, which is a violation of L4(ii). No comparison of the arguments’ namespaces occurs anywhere in merge. ◻

Remark 5.19 (What the two clauses say together). The pair of results is more informative than either alone. Confinement (Proposition 5.17) says the boundary is not leaky. An operation that stays inside a namespace has effects that stay inside it, and no restricted view of the store can be surprised by an operation issued against the full store from within its own fibre. Construction locality (Proposition 5.18) says the boundary is nevertheless crossable, and by construction rather than by leakage. The field namespace_id is what delete_by_namespace (storage_router.py:736) and the namespace-scoped search paths select on, and merge files a record under the namespace of its first argument while copying content and metadata from all of them. A caller who merges across the boundary moves payload across it. This is an implementation-level statement about the source at a93035d, not a report of an exploited path, and the repair is a namespace comparison in the retrieval loop.

5.6 L5, resolution independence

Proposition 5.20 (The merged type reads the resolution parameter). L5 fails. The type field of the record constructed by merge is computed as max(set(types), key=types.count) (memory_lineage.py:307, and identically at lines 323 and 345 for the intersection and weighted branches). When two distinct types attain the same maximal multiplicity in types, this expression returns whichever of the maximisers occurs first in the enumeration order of the Python set set(types). In the model that order is a component of the resolution parameter π\pi and not of the allocation part of ω\omega.

Proof. max with a key returns the first element of its iterable attaining the maximal key value, so when the multiset types has two or more modes the result is determined by the enumeration order of set(types). The smallest witness has two arguments of distinct types: with types == [DECISION, ERROR] both members have count one, both are maximisers, and the returned value is the first member enumerated. Two narrowings are worth stating, since neither rescues the law. The expression occurs in the union, intersection and weighted branches only. The latest and oldest branches take the type of a designated argument and are unaffected. And a caller who supplies the optional memory_type argument overrides the computed value at memory_lineage.py, lines 247 to 248, which is evaluated after _apply_merge_strategy returns. So the failure is exhibited by any call that omits memory_type and uses one of the three affected strategies, the default among them. Take ω=(k⃗,t,π)\omega = (\vec{k}, t, \pi) and ω′=(k⃗,t,π′)\omega' = (\vec{k}, t, \pi') where π\pi orders this two-element set one way and π′\pi' the other. Then α(s)(a,ω)\alpha(s)(a, \omega) and α(s)(a,ω′)\alpha(s)(a, \omega') construct records differing in their type field, so they differ, contradicting L5. ◻

Remark 5.21 (The order is not merely unspecified, it varies). The proof above needs only that the enumeration order is not fixed by the arguments. What fixes it in practice matters too, because the two readings have different consequences for a caller. MemoryType is declared class MemoryType(str, Enum) (schemas.py:23), and the enumeration order of a Python set of such members is determined by their hash values. The hash of a string-valued object is salted per process by default, and the salt is controlled by the PYTHONHASHSEED environment variable (13). So π\pi is not merely unspecified in the documentation. It is a parameter of the operating-system environment, and two processes running the same code against the same store may construct records with different types from the same call.

Remark 5.22. This is a small defect with a large formal consequence. L5 asks that a realisation read only the ambient data its own interface declares: fresh identifiers and the clock, both of which appear as record fields. A field decided by a hash-order tie-break is read from a parameter the interface never mentions. The repair is one line: replace the expression by a mode computation with a declared tie-break, for instance the type of the first argument, which would make merge agree with its own treatment of namespace_id. That change repairs this merge-local counterexample, not L5 for the whole action alphabet. Prefix recall also depends on an unordered row choice (Proposition 6.3); full repair requires deterministic identifier resolution, either exact matching or a documented total order.

5.7 Merge up to bisimilarity

L5 concerns one field of the merged record. The wider question is what algebraic laws merge obeys. The obvious answer, that it obeys none since every call allocates a fresh identifier, is an artefact of comparing states on the nose. The right comparison for a coalgebra is behavioural, and under it one of the three candidate laws holds.

Definition 5.23 (Allocation-blind observation). Let q ⁣:Rec→Rec/ ⁣ ⁣≈q \colon \mathrm{Rec}\to \mathrm{Rec}/\!\!\approx be the quotient forgetting the id, created_at and updated_at fields and the metadata keys evolution_timestamp, merge_timestamp and split_timestamp. Let H≈X=(Rec/ ⁣ ⁣≈)⊥×Pfin(R×X)H^{\approx} X = (\mathrm{Rec}/\!\!\approx)_{\bot} \times \mathcal{P}_{\mathrm{fin}}(\mathcal{R}\times X) and let βs≈=(q⊥×id)∘βs\beta^{\approx}_{s} = (q_{\bot} \times \mathrm{id}) \circ \beta_{s}, an H≈H^{\approx}-coalgebra on Id\mathrm{Id}. Write [ ⁣[i] ⁣]s≈[\![i]\!]^{\approx}_{s} for the corresponding behaviour.

Proposition 5.24 (Merge is idempotent up to bisimilarity). Let a=merge(ı⃗,c,θ)a = \mathsf{merge}(\vec{\imath}, c, \theta) be a call whose arguments all resolve in ss, let t1=nx(s,a,ω1)t_{1} = \mathrm{nx}(s, a, \omega_{1}) allocating κ1\kappa_{1}, and let t2=nx(t1,a,ω2)t_{2} = \mathrm{nx}(t_{1}, a, \omega_{2}) allocating κ2≠κ1\kappa_{2} \neq \kappa_{1}, where ω1\omega_{1} and ω2\omega_{2} agree on clock and resolution parts. Then [ ⁣[i] ⁣]t1≈=[ ⁣[i] ⁣]t2≈[\![i]\!]^{\approx}_{t_{1}} = [\![i]\!]^{\approx}_{t_{2}} for every i∈dom(mt1)i \in \mathrm{dom}(m_{t_{1}}).

Proof. Write ı⃗=⟨i1,…,in⟩\vec{\imath} = \langle i_{1}, \dots, i_{n} \rangle and let B={(i,i):i∈Id∖{κ2}}∪{(κ1,κ2)}\mathcal{B} = \{(i, i) : i \in \mathrm{Id}\setminus \{\kappa_{2}\}\} \cup \{(\kappa_{1}, \kappa_{2})\}. We check that B\mathcal{B} is an H≈H^{\approx}-bisimulation between (Id,βt1≈)(\mathrm{Id}, \beta^{\approx}_{t_{1}}) and (Id,βt2≈)(\mathrm{Id}, \beta^{\approx}_{t_{2}}).

First note what the second call changes. By Proposition 5.1 it adds one record, at κ2\kappa_{2}, and by the enumeration in the proof of Theorem 5.7 it adds the edges (κ2,MERGED_FROM,ir)(\kappa_{2}, \texttt{MERGED\_FROM}, i_{r}) and (ir,MERGED_INTO,κ2)(i_{r}, \texttt{MERGED\_INTO}, \kappa_{2}) for 1≤r≤n1 \le r \le n, and nothing else. The second call resolves the same identifiers to the same records, and merge\texttt{merge}’s constructed record is a function of the retrieved records, the non-identifier arguments and ω\omega. So the records at κ1\kappa_{1} and κ2\kappa_{2} agree in every field except id, created_at, updated_at and merge_timestamp, all of which qq forgets. Hence q(mt1(κ1))=q(mt2(κ2))q(m_{t_{1}}(\kappa_{1})) = q(m_{t_{2}}(\kappa_{2})).

Now take the pairs in turn. For (i,i)(i, i) with i∉{i1,…,in}i \notin \{i_{1}, \dots, i_{n}\} we have βt1≈(i)=βt2≈(i)\beta^{\approx}_{t_{1}}(i) = \beta^{\approx}_{t_{2}}(i), since no record or edge at such an ii was touched, and the successors are related by the identity part of B\mathcal{B}. For (ir,ir)(i_{r}, i_{r}) the record is unchanged, and the outgoing edge sets are X∪{(MERGED_INTO,κ1)}X \cup \{(\texttt{MERGED\_INTO}, \kappa_{1})\} on the left and X∪{(MERGED_INTO,κ1),(MERGED_INTO,κ2)}X \cup \{(\texttt{MERGED\_INTO}, \kappa_{1}), (\texttt{MERGED\_INTO}, \kappa_{2})\} on the right, for a common XX. Every left successor is B\mathcal{B}-related to a right successor with the same label, by the identity. Every right successor is B\mathcal{B}-related to a left one, using (κ1,κ2)∈B(\kappa_{1}, \kappa_{2}) \in \mathcal{B} for the new pair. For (κ1,κ2)(\kappa_{1}, \kappa_{2}) the quotiented records agree by the previous paragraph, and the outgoing edge sets are {(MERGED_FROM,ir)}r\{(\texttt{MERGED\_FROM}, i_{r})\}_{r} on both sides, related by the identity. This exhausts the pairs, so B\mathcal{B} is a bisimulation. Since H≈H^{\approx} preserves weak pullbacks by the argument of Lemma 3.17, bisimilarity is equality of behaviour, and every i∈dom(mt1)i \in \mathrm{dom}(m_{t_{1}}) is B\mathcal{B}-related to itself. ◻

Proposition 5.25 (Merge is not commutative, even up to bisimilarity). There are a store ss, identifiers i1,i2i_{1}, i_{2} and an ambient parameter ω\omega such that the record constructed by merge(⟨i1,i2⟩,⊥,UNION)\mathsf{merge}(\langle i_{1}, i_{2} \rangle, \bot, \texttt{UNION}) and the record constructed by merge(⟨i2,i1⟩,⊥,UNION)\mathsf{merge}(\langle i_{2}, i_{1} \rangle, \bot, \texttt{UNION}) have different images under qq.

Proof. Let the records at i1i_{1} and i2i_{2} lie in distinct namespaces ν1≠ν2\nu_{1} \neq \nu_{2}. The constructor at memory_lineage.py:256 takes memories[0].namespace_id, so the two calls produce records with namespaces ν1\nu_{1} and ν2\nu_{2} respectively, and qq does not forget namespace_id. There is a second, independent witness. Even within one namespace, _generate_merged_content (lines 349 to 354) emits the argument contents in argument order under the labels [Source 1], [Source 2] and so on. The two calls therefore produce different content whenever the two arguments have different content, and qq does not forget content. ◻

Remark 5.26. Associativity is not among the candidates, because merge is a variadic operation on a sequence of identifiers rather than a binary operation on records, and there is no identifier-level composite to associate. What the two propositions leave is a merge that is idempotent up to allocation-blind bisimilarity and sensitive to argument order in two independent ways. One of those ways, the namespace, is the same field responsible for the failure of L4(ii).

5.8 L6, bitemporality

Theorem 5.27 (The recorded lineage does not determine a bitemporal state). Let Ψ\Psi record a bitemporal history hh (Definition 2.2) through the ContextFS lineage API at a93035d. Fix ambient parameters ω1,…,ωn\omega_1,\ldots,\omega_n whose clock components are t1,…,tnt_1,\ldots,t_n, whose allocated identifiers i1,…,ini_1,\ldots,i_n are fresh at each step, and whose resolution components agree. The recording issues save(v1)\mathsf{save}(v_{1}) under ω1\omega_1 and evolve(ik−1,vk,ξk)\mathsf{evolve}(i_{k-1}, v_{k}, \xi_{k}) under ωk\omega_k for 2≤k≤n2 \le k \le n. Then:

  1. There are histories ha≠hbh_{a} \neq h_{b}, agreeing in every value vkv_{k}, every transaction time tkt_{k} and every change reason ξk\xi_{k}, and differing only in valid times, with Ψ(ha)=Ψ(hb)\Psi(h_{a}) = \Psi(h_{b}) and σha≠σhb\sigma_{h_{a}} \neq \sigma_{h_{b}}.

  2. Consequently there is no function gg on stores with g(Ψ(h))=σhg(\Psi(h)) = \sigma_{h} for all hh, and no bitemporal observation in the sense of Definition 3.20 is sound for Ψ\Psi. The quantifier over gg ranges over all functions of the entire store, so no reading of any record field, metadata key, edge, edge attribute, or derived timeline recovers valid time.

Proof. (1) Take Val={A,B}\mathrm{Val}= \{A, B\}, t1=1 Februaryt_{1} = \text{1 February}, t2=1 Marcht_{2} = \text{1 March}, and ha=((A,1 Jan,1 Feb), (B,10 Feb,1 Mar)),hb=((A,1 Jan,1 Feb), (B,20 Feb,1 Mar)),\begin{align*} h_{a} &= \big((A, \text{1 Jan}, \text{1 Feb}),\ (B, \text{10 Feb}, \text{1 Mar})\big), \\ h_{b} &= \big((A, \text{1 Jan}, \text{1 Feb}),\ (B, \text{20 Feb}, \text{1 Mar})\big), \end{align*} with the same change reason on both second records. Evaluate Definition 2.2 at (τv,τt)=(15 Feb,15 Mar)(\tau_{v}, \tau_{t}) = (\text{15 Feb}, \text{15 Mar}). For hah_{a} both records satisfy tj≤15 Mart_{j} \le \text{15 Mar} and both satisfy pj≤15 Febp_{j} \le \text{15 Feb}, so Jha={1,2}J_{h_{a}} = \{1, 2\}; the maximal valid-time start in JhaJ_{h_{a}} is P=10 FebP = \text{10 Feb}, attained only at j=2j = 2, so k=2k = 2 and σha(τv,τt)=B\sigma_{h_{a}}(\tau_{v}, \tau_{t}) = B. For hbh_{b} we have p2=20 Feb>15 Febp_{2} = \text{20 Feb} > \text{15 Feb}, so Jhb={1}J_{h_{b}} = \{1\}, P=1 JanP = \text{1 Jan}, k=1k = 1 and σhb(τv,τt)=A\sigma_{h_{b}}(\tau_{v}, \tau_{t}) = A. Hence σha≠σhb\sigma_{h_{a}} \neq \sigma_{h_{b}}. The two histories describe genuinely different situations: in hah_{a} the value became BB on 10 February and we found out on 1 March, while in hbh_{b} it became BB on 20 February and we found out on 1 March. On 15 February the first was already BB while the second was still AA.

Now compare Ψ(ha)\Psi(h_{a}) and Ψ(hb)\Psi(h_{b}). The valid times pkp_{k} occur in no argument position of any operation of Definition 3.7 as realised at a93035d. The signature of MemoryLineage.evolve (memory_lineage.py, lines 93 to 101) is (memory_id,new_content,summary,preserve_tags,additional_tags,reason)(\texttt{memory\_id}, \texttt{new\_content}, \texttt{summary}, \texttt{preserve\_tags}, \texttt{additional\_tags}, \texttt{reason}), and the record type Memory (schemas.py, lines 1362 to 1398) has exactly two time-valued fields, created_at and updated_at, both defaulting to datetime.now(). The metadata key written by evolve is "evolution_timestamp", assigned datetime.now(timezone.utc).isoformat() at line 159, again a reading of the clock at the moment of the call. Therefore the two recordings issue the same calls with the same arguments at the same transaction times under the same allocation parameter, and Ψ(ha)=Ψ(hb)\Psi(h_{a}) = \Psi(h_{b}).

(2) If such a gg existed then σha=g(Ψ(ha))=g(Ψ(hb))=σhb\sigma_{h_{a}} = g(\Psi(h_{a})) = g(\Psi(h_{b})) = \sigma_{h_{b}}, contradicting (1). Since gg was an arbitrary function on stores, no quantity computed from the store, however derived, determines σh\sigma_{h}. The soundness claim is the same argument applied to a bitemporal observation obs\mathrm{obs}: at the common store Ψ(ha)=Ψ(hb)\Psi(h_{a}) = \Psi(h_{b}) and the common arguments (ℓ,15 Feb,15 Mar)(\ell, \text{15 Feb}, \text{15 Mar}) it would have to return BB and AA, which no function does. ◻

The result is stronger than counting timestamp fields. The store contains created_at, updated_at, several timestamp metadata keys, timestamps on VersionEntry, and a temporal ordering implicit in the ancestry graph. The theorem quantifies over every function of the whole store, so none of those representations can recover valid time for the recording map Ψ\Psi.

Corollary 5.28 (Timeline.at is a transaction-time query). Timeline.at (types/versioned.py:238) computes the last version entry whose single timestamp field is at or before the argument. When the timeline is built by VersionedMem.from_memory (types/versioned.py:359), that field is set from memory.created_at (line 387). When it is built by VersionedMem.from_lineage (line 402), it is read out of lineage history entries whose timestamps are likewise clock readings at call time. In the vocabulary of Section 2.3, Timeline.at answers “what did the system have on file at tt” and not “what was the case at tt”.

Remark 5.29 (The weaker distinction is also not exposed). Theorem 5.27 is deliberately independent of the ChangeReason parameter, since the two histories it constructs carry the same reason. The parameter is in any case not reachable from outside the lineage module at a93035d. ChangeReason has four members, OBSERVATION, INFERENCE, CORRECTION and DECAY (types/versioned.py, lines 63 to 71), and MemoryLineage.evolve accepts it with default ChangeReason.OBSERVATION (line 100). None of the three agent-facing surfaces forwards it. ContextFS.evolve (core.py:1515) calls the lineage method with five keyword arguments, none of them reason (Listing 6). Both contextfs_evolve (mcp/fastmcp_server.py:623) and the command line evolve (cli/memory.py:247) call ContextFS.evolve. So on every agent-facing path the recorded reason is OBSERVATION, and a correction is not distinguished from an observation even by the four-valued label, let alone by a time.

5.9 L7, lineage referential integrity

Proposition 5.30 (Edge endpoints are not checked by default). L7 fails as a property of the whole state space. Stores that are not ancestry-closed are reachable through the path of Proposition 5.10, and no constraint in the storage layer prevents them.

Evidence. StorageRouter.add_edge (storage_router.py:1327) takes a parameter validate: bool = False and performs the endpoint existence query only when it is true (lines 1354 to 1370). Its documentation states that the default is false “since most callers (core.link, memory_lineage) already validate via recall()”. Every call site in memory_lineage.py omits the argument. The memory_edges table is created without a foreign key constraint on either endpoint. Reachability of a violating store is Proposition 5.10. ◻

Remark 5.31 (What the failure of L7 does and does not cost). The documented justification is accurate for the three retrieving derivation methods, which do resolve every named identifier through recall before writing edges, and it is accurate for the facade link. It is not a property of the store because the direct MemoryLineage.link path in Proposition 5.10 omits the check.

The consequence for the rest of the paper is bounded, and Proposition 5.9 is what bounds it. Ancestry closure is not an accident of a well-chosen initial state: it holds at the empty store and is preserved by every derivation call and every fresh save, so it holds throughout any run built from those calls and the facade link. The results of Section 5.2 therefore describe the reachable behaviour of the system on its own principal interface, and the hypothesis they carry is discharged for that fragment rather than assumed. What L7’s failure costs is that the fragment is not the whole state space: a caller that reaches past the facade into MemoryLineage.link can leave it. Deletion also lies outside the monotone derivation fragment, but it removes incident edges and does not by itself violate ancestry closure. Outside that fragment Theorem 5.7 has nothing to say.

5.10 Summary

The eight law clauses of Definition 3.21 against ContextFS at commit a93035d. Positive entries are theorems of Section 3 applied through Lemma 5.6; negative entries are counterexamples with a named code witness.
Law Status at a93035d Witness
L1 monotonicity holds on DD S2 of the schema; Proposition 5.1
fails outside DD save with an explicit id; delete (Proposition 5.2)
L2 edge/record agreement fails Proposition 5.14
L3 inverse closure holds on the unexceptional path Proposition 5.16
L4(i) confinement holds on DD S3 and S4 of the schema; Proposition 5.17
L4(ii) construction locality fails Proposition 5.18
L5 resolution independence fails Proposition 5.20
L6 bitemporality fails Theorem 5.27
L7 referential integrity fails Proposition 5.30

Table 2 collects the results. The passing clauses are enforced by the shape of the code, and what fails would require a check the code does not perform. Monotonicity holds because the derivation methods construct new objects and never assign to retrieved ones; inverse closure holds because the two writes are adjacent statements; confinement holds because every constructed record takes its namespace from a named argument. Construction locality, resolution independence, bitemporality and referential integrity would each require an explicit comparison, an explicit tie-break, an extra field, or a constraint, and none is present. Counted by clause, three hold and five fail.

6 Composition with adjacent layers

6.1 Layer contribution

A composite that contains a memory coalgebra gains one structural property that is not available from a stateless component: an observation that survives further activity. The following corollary states the form a consumer can use.

Corollary 6.1 (Cacheable observation). Let s0s_{0} be an ancestry-closed store and let s0,s1,…,sks_{0}, s_{1}, \dots, s_{k} be the stores obtained by a sequence of derivation calls with pairwise fresh allocation parameters. Then for every i∈dom(ms0)i \in \mathrm{dom}(m_{s_{0}}), [ ⁣[i] ⁣]sk−=[ ⁣[i] ⁣]s0−[\![i]\!]^{-}_{s_{k}} = [\![i]\!]^{-}_{s_{0}}.

Proof. Each step preserves ancestry closure and fixes ancestry behaviour on the identifiers present before it, by Theorem 5.7 and Corollary 5.12. Induction on kk, noting that dom(ms0)⊆dom(msj)\mathrm{dom}(m_{s_{0}}) \subseteq \mathrm{dom}(m_{s_{j}}) for every jj by Proposition 5.1. ◻

The hypothesis matters. Corollary 6.1 covers derivation calls only, so a composite that also issues save\mathsf{save} under an existing identifier, or a deletion, is outside it; by Proposition 5.2 both of those can change an ancestry that a consumer has already read.

6.2 Memory composition

Before asking how memory composes with skills or with a harness, we ask how it composes with itself, since a harness that hosts several agents hosts several stores. The candidate operation is ⊎\uplus of Definition 3.5, and the question is whether operations are stable under enlarging the surrounding store.

Proposition 6.2 (Frame property for identifier-naming calls). Let s,s′s, s' be stores with dom(ms)∩dom(ms′)=∅\mathrm{dom}(m_{s}) \cap \mathrm{dom}(m_{s'}) = \emptyset, and let aa be a derivation call all of whose identifier arguments are exact identifiers in dom(ms)\mathrm{dom}(m_{s}), under an allocation parameter fresh for both. Suppose identifier resolution is by equality. Then α(s⊎s′)(a,ω)=(o, t⊎s′)\alpha(s \uplus s')(a, \omega) = \big(o,\ t \uplus s'\big) where α(s)(a,ω)=(o,t)\alpha(s)(a, \omega) = (o, t).

Proof. Under resolution by equality, each derivation method reads at most the records at the identifiers named in aa, and those records are the same in ss and s⊎s′s \uplus s' because the domains are disjoint and ms⊎s′m_{s \uplus s'} extends msm_{s}; supersede reads none. The constructed records are a function of the retrieved records, the remaining arguments and ω\omega, so they are the same in both cases, and the written edges are incident only to named identifiers and to the fresh ones. Hence the added records and edges are identical, the outputs are identical, and the resulting store is t⊎s′t \uplus s'. ◻

Proposition 6.3 (The frame property fails for recall). The hypothesis of resolution by equality is not satisfied by the implementation at a93035d, and without it Proposition 6.2 is false. StorageRouter._recall_from_sqlite resolves an identifier by the SQL predicate id LIKE ? with the pattern f"{memory_id}%" and takes cursor.fetchone() from an unordered result set (Listing 4). Consequently there are stores s,s′s, s' with disjoint record domains and an identifier argument qq such that out(s,recall(q),ω)≠out(s⊎s′,recall(q),ω)\mathrm{out}(s, \mathsf{recall}(q), \omega) \neq \mathrm{out}(s \uplus s', \mathsf{recall}(q), \omega).

Proof. Let ss contain a single record at ii and let qq be a proper prefix of ii. Then in ss the predicate id LIKE ’q%’ selects exactly the row at ii, and the output is that record. Let s′s' contain a single record at i′′≠ii'' \neq i with qq also a prefix of i′′i''; the domains are disjoint. In s⊎s′s \uplus s' the predicate selects two rows. The query carries no ORDER BY, so the row returned by fetchone is determined by the resolution component π\pi of the ambient parameter rather than by the query, and for a suitable π\pi it is the row at i′′i''. The two outputs therefore need not agree. This is a second appearance of the same defect as Proposition 5.20: an operation whose result is decided by an enumeration order the interface does not mention. ◻

The lineage observation fails to compose for a different reason, which is worth separating from the identifier question because it survives resolution by equality.

Proposition 6.4 (Extension can repair a dangling edge). There are stores s,s′s, s' with disjoint record domains and an identifier i∈dom(ms)i \in \mathrm{dom}(m_{s}) with [ ⁣[i] ⁣]s≠[ ⁣[i] ⁣]s⊎s′[\![i]\!]_{s} \neq [\![i]\!]_{s \uplus s'}. Ancestry closure is therefore not preserved by restriction, and ⊎\uplus does not preserve lineage behaviour on stores that are not ancestry-closed.

Proof. By Proposition 5.30 a store with a dangling ancestry edge is reachable, so let ss contain a record at ii together with the edge (i,EVOLVED_FROM,j)(i, \texttt{EVOLVED\_FROM}, j) and no record at jj. Then βs(j)=(⊥,∅)\beta_{s}(j) = (\bot, \varnothing), so the behaviour [ ⁣[i] ⁣]s[\![i]\!]_{s} is the tree whose root carries ms(i)m_{s}(i) and whose single EVOLVED_FROM child carries ⊥\bot and has no further children. Let s′s' consist of a single record at jj; the domains are disjoint. In s⊎s′s \uplus s' we have β(j)=(ms′(j),∅)\beta(j) = (m_{s'}(j), \varnothing), so the same child now carries a record. The two behaviours differ in the first component of that child, hence [ ⁣[i] ⁣]s≠[ ⁣[i] ⁣]s⊎s′[\![i]\!]_{s} \neq [\![i]\!]_{s \uplus s'}. If instead ss is ancestry-closed then no such jj exists, every R−\mathcal{R}^{-}-edge from dom(ms)\mathrm{dom}(m_{s}) lands in dom(ms)\mathrm{dom}(m_{s}), and the argument of Theorem 5.7 applied to the inclusion shows [ ⁣[i] ⁣]−[\![i]\!]^{-} is unchanged. ◻

Proposition 6.4 says that domain-disjoint union is the wrong composition operator for isolation: two tenants with disjoint record identifiers may still interfere, because one’s dangling edge can acquire a target from the other. The repair is to strengthen the side condition, and with the stronger one the composition behaves.

Definition 6.5 (Isolated union). Write supp(s)=dom(ms)∪{ i:(i,ρ,j)∈Es or (j,ρ,i)∈Es }\mathrm{supp}(s) = \mathrm{dom}(m_{s}) \cup \{\, i : (i, \rho, j) \in E_{s} \text{ or } (j, \rho, i) \in E_{s} \,\} for the set of identifiers a store mentions, in either component. For stores s,s′s, s' with supp(s)∩supp(s′)=∅\mathrm{supp}(s) \cap \mathrm{supp}(s') = \emptyset, the isolated union s⊎!s′s \uplus_{!} s' is s⊎s′s \uplus s'.

Proposition 6.6 (Isolated union preserves lineage behaviour). Let s⊎!s′s \uplus_{!} s' be defined. Then βs⊎!s′(i)=βs(i)\beta_{s \uplus_{!} s'}(i) = \beta_{s}(i) for every i∈supp(s)i \in \mathrm{supp}(s), and every successor of such an ii again lies in supp(s)\mathrm{supp}(s). Consequently [ ⁣[i] ⁣]s⊎!s′=[ ⁣[i] ⁣]s[\![i]\!]_{s \uplus_{!} s'} = [\![i]\!]_{s} for every i∈supp(s)i \in \mathrm{supp}(s), and symmetrically for s′s'. The structure (S,⊎!,∅)(S, \uplus_{!}, \emptyset) is again a partial commutative monoid.

Proof. Let i∈supp(s)i \in \mathrm{supp}(s). For the first component, i∉dom(ms′)i \notin \mathrm{dom}(m_{s'}) since the supports are disjoint, so ms⊎s′(i)=ms(i)m_{s \uplus s'}(i) = m_{s}(i). For the second, an edge of Es′E_{s'} with source ii would put ii in supp(s′)\mathrm{supp}(s'), so the outgoing edges of ii in Es∪Es′E_{s} \cup E_{s'} are exactly those in EsE_{s}; and each of their targets lies in supp(s)\mathrm{supp}(s) by the definition of the support. Hence β\beta restricted to supp(s)\mathrm{supp}(s) is unchanged and closed, and behaviour is determined by the restriction of the coalgebra to a closed subset. The monoid claim follows as in Proposition 3.6, since supp\mathrm{supp}-disjointness of three stores pairwise is again symmetric in the three. ◻

Remark 6.7 (Isolation is not what namespaces provide). Proposition 6.6 identifies the side condition a multi-tenant deployment needs: identifier supports must be disjoint, not merely record domains. Namespaces do not supply it at a93035d, for two independent reasons visible in the source. Identifier resolution does not consult the namespace: the predicate in Listing 4 filters on id and deleted_at only. And edge creation does not consult it either: StorageRouter.add_edge (storage_router.py:1327) compares no namespaces, so an edge between records of two namespaces is writable and puts each namespace’s identifiers in the other’s support. Isolation therefore has to be enforced above ContextFS, by partitioning identifiers, and not by the namespace field.

Remark 6.8 (The prefix regime is not narrow). It would be fair to object that prefix collisions are a measure-zero concern. They are not, for two reasons that follow from the identifier scheme rather than from any deployment. First, Memory.id defaults to str(uuid.uuid4())[:12] (schemas.py:1371). The string form of a version 4 UUID is grouped 88-44-44-44-1212, so the first twelve characters are eight hexadecimal digits, a hyphen, and three more hexadecimal digits: eleven hexadecimal digits, that is 4444 bits, all of them random, since the version nibble sits at string position 1414. By the birthday bound, two identifiers coincide with probability about one half once the store holds roughly 1.18⋅222≈4.9×1061.18 \cdot 2^{22} \approx 4.9 \times 10^{6} records. Second, and more to the point, resolution is by prefix, and the documented interface invites abbreviation: the docstring of ContextFS.recall (core.py:1288--1298) says the identifier “can be partial, at least 8 chars”, and no code enforces the minimum. An eight-character query prefix fixes 3232 bits, so two records match the same eight-character prefix with probability about one half once the store holds roughly 1.18⋅216≈7.7×1041.18 \cdot 2^{16} \approx 7.7 \times 10^{4} records. Ambiguity at the interface a caller is told to use therefore arrives some two orders of magnitude before ambiguity of the identifiers themselves.

The conclusion for composition is narrow and usable: memory stores compose by disjoint union for every operation that names full identifiers, and do not compose for the one operation that resolves abbreviations. A harness that shares one store across tenants must either resolve identifiers by equality or partition the identifier space, and namespaces do not partition it, because Listing 4 does not filter on namespace_id.

6.3 Composition with skills

Part II treats skills as operations of a coloured operad and takes the memory carrier as one of the colours a skill may consume and produce. What this Part supplies is Definition 3.15, the type Mem\mathsf{Mem} of a handle on the carrier, and nothing else: in particular it does not supply a schema-indexed family of colours, because record schemas belong to the Know\mathrm{Know} component in the sense fixed by Part IV rather than to the state space.

Two of the results above have direct consequences for a skill composite. A skill that reads the ancestry of a memory early in a composite and uses it late is sound by Corollary 6.1, provided no step of the composite deletes a record or saves under an existing identifier. A skill composite that is scoped to a namespace does not thereby scope its memory effects to that namespace, by Proposition 5.18 and Remark 5.19: a merge issued inside the composite may name arguments in another namespace and will file the result in the namespace of its first argument. Scoping a composite is therefore not the same as scoping the state it touches, which is exactly the kind of gap the operadic reading of Part II cannot see, because operad substitution constrains the composition of operations and not the effects of the operations composed.

Part II does carry one mechanism that bears on this (10). Its operations are graded by an effect class, and as stated there an operation whose input colour is Mem\mathsf{Mem} is not pure, so the grading determines where such an operation may be scheduled relative to its siblings. That grading orders memory-touching operations relative to one another. It does not bound which records they touch, which is what Proposition 5.18 is about, so the two mechanisms are complementary rather than overlapping.

6.4 Composition with the harness

A harness surrounds a model with memory, tools, wiring and policy, and the Architecture triple (G,Know,Φ)(G, \mathrm{Know}, \Phi) is one categorical account of that layer (14, 1). We use only the boundary fixed in Section 1.4: Know\mathrm{Know} is a collection of checkable statements cert\mathrm{cert}, and SS is the coalgebraic state. Applied to ContextFS, the schema registry (types/registry.py, class SchemaRegistry, line 23) and the structured-data validation performed by Memory’s model validator (schemas.py:1411) are Know\mathrm{Know}-level: they are checks on records rather than records. The record store and the edge set are SS.

What Theorem 5.27 contributes to that discussion is a limit on what a Know\mathrm{Know}-level check over memory can be: no check evaluated against a ContextFS store at a93035d can have the form “at transaction time τt\tau_{t} the system believed PP of the world at valid time τv\tau_{v}”, because the store does not distinguish the second parameter.

The boundary has a consequence on both sides. As stated in Part IV (12), the harness carries a projection of session content into its own store, which is state but is neither a certificate nor a memory coalgebra in the sense of Definition 3.14: it has no derivation operations and no lineage edges. The same negative reading applies in the protocol layer, where, as stated in Part III (11), the append-only operation chain of CatDB is a wire-level ordering discipline and not a structure map α ⁣:S→FS\alpha \colon S \to F S. We agree with both readings. What makes a store a memory coalgebra on this account is not persistence and not an ordering on writes; it is the presence of derivation operations whose lineage is observable, which is what Definition 3.7 asks for and what neither of those two stores has.

One cross-cutting dependency runs against the presentation order. A skill may call a memory operation at run time, so the companion treatment of skills depends on this one operationally, while nothing here depends formally on any companion. At the pinned commits this is a composition of models rather than of running code: AgentHero at 1c3ad24 declares no dependency on ContextFS, as reported in companion work on the harness (12).

7 Limits and counterexamples

Four laws fail in Section 5. Further limits concern the cost of repairing the temporal gap, the second unpersisted memory model in ContextFS, and the part of the system the coalgebraic reading does not reach at all.

7.1 Bitemporal repair

Theorem 5.27 is a statement about a record schema, not about an architecture, and the corresponding repair is small. We state it as a proposition about the model so that it is clear the lineage machinery survives unchanged.

Proposition 7.1 (The single-axis coalgebra is a quotient of a bitemporal one). Let Recbt=Rec×T\mathrm{Rec}^{bt} = \mathrm{Rec}\times \mathbb{T}, with the second component read as a valid-time instant, and let AbtA^{bt} extend the alphabet of Definition 3.7 by giving save\mathsf{save}, evolve\mathsf{evolve}, merge\mathsf{merge} and split\mathsf{split} one additional argument in T\mathbb{T}. Let SbtS^{bt} be the corresponding set of stores and αbt\alpha^{bt} the structure map that behaves as α\alpha on the Rec\mathrm{Rec} component and stores the extra argument in the T\mathbb{T} component of each constructed record. Let u ⁣:Sbt→Su \colon S^{bt} \to S delete the second component of every record and let ε ⁣:Abt→A\varepsilon \colon A^{bt} \to A delete the extra argument. Then for all s∈Sbts \in S^{bt}, a∈Abta \in A^{bt} and ω\omega, α(u(s))(ε(a),ω)  =  ( uOut ,u )(αbt(s)(a,ω)),\alpha\big(u(s)\big)\big(\varepsilon(a), \omega\big) \;=\; \big(\, u_{\mathrm{Out}}\,, u \,\big)\Big( \alpha^{bt}(s)(a, \omega) \Big), where uOutu_{\mathrm{Out}} deletes the valid-time component of a record-valued output. The observation obsbt(s,ℓ,τv,τt)\mathrm{obs}^{bt}(s, \ell, \tau_{v}, \tau_{t}), defined by selecting the latest record with clock field at most τt\tau_{t} among those with valid-time field at most τv\tau_{v}, is not degenerate in valid time, while its image under uu is.

Proof. The displayed equation is immediate from the construction: αbt\alpha^{bt} agrees with α\alpha on the Rec\mathrm{Rec} component by definition, and uu deletes exactly the component that the extra argument feeds and that α\alpha never reads. Non-degeneracy of obsbt\mathrm{obs}^{bt} is witnessed by the two histories of Theorem 5.27: recorded through AbtA^{bt} they differ in the valid-time field of the second record, and obsbt\mathrm{obs}^{bt} at (15 Feb,15 Mar)(\text{15 Feb}, \text{15 Mar}) returns the second record for one and the first for the other. Degeneracy of the image under uu is Theorem 5.27(2). ◻

Remark 7.2 (Cost of the repair). Proposition 7.1 touches neither R\mathcal{R} nor the lineage graph nor any of the laws L1 to L5 and L7. In implementation terms the repair is: one nullable time column on the memories table, one field on Memory, one optional argument on the four derivation methods and on the three agent-facing surfaces, and a comparison against that field in Timeline.at. It belongs here because the same theorem is cited by the synthesis of the series, that the gap is a design gap and not an obstruction: nothing about the coalgebraic reading of memory resists two axes, and the reason ContextFS has one is that its operations timestamp themselves rather than being told when their content became true.

7.2 Repository memory models

The analysis above concerns the persisted model: records in SQLite, edges in memory_edges, derivation through MemoryLineage. ContextFS also contains a second, richer memory model in types/versioned.py, and at a93035d the two do not meet.

The second model is the one that would satisfy more of Definition 3.21. VersionEntry (line 87) carries a reason drawn from the four-valued ChangeReason and an author; Timeline (line 175) keeps entries sorted by timestamp and offers at, since and by_reason; VersionedMem (line 306) adds an authoritative-version pointer and a checkable invariant, is_consistent (line 576), which tests that timestamps are monotone, that all entries share a memory identifier, and that no version identifier repeats. That invariant is exactly the kind of internal consistency L2 and L3 ask for, and it is stated and checkable in the source.

It is, however, not reachable from the store. A VersionedMem is constructed on demand by Memory.as_versioned (schemas.py:1995) or Mem.as_versioned (types/memory.py:278), both of which call VersionedMem.from_memory (types/versioned.py:359); that constructor builds a timeline containing at most one entry, derived from the single record it is given, and only when that record has non-empty structured_data (lines 383–396). No code path in storage_router.py or memory_lineage.py at a93035d persists a Timeline or a VersionEntry, and the memories table (core.py:203--220) has no column for one. The alternative constructor VersionedMem.from_lineage (line 402) reconstructs a timeline from lineage history dictionaries, so the richer model is designed to be derived from the poorer one rather than to replace it.

The consequence for this paper is a scope statement. The results of Section 5 are about the persisted model, which is the one an agent reaches through the facade, the Model Context Protocol server and the command line. They are not claims about types/versioned.py, whose is_consistent may well hold of every timeline it constructs. The consequence for the system is a question rather than a defect: the module that reasons about why a belief changed is the module whose output is not written down.

7.3 Missing run-scoped state

Remark 7.3 (There is no RunContext). The correspondence this series tests names RunContext as the concrete component playing the role of coalgebraic state, and BiTemporalMemory as the memory implementation (1). Both are identifiers of that paper’s own reference implementation. Neither occurs in ContextFS at a93035d. The nearest structural analogue of a run-scoped state object is Session (schemas.py:2035), which carries a namespace, an originating tool, a device name, a repository path and branch, a list of SessionMessage entries (schemas.py:2025), start and end instants, a summary and a list of identifiers of memories created during the session; its lifecycle is ContextFS.start_session (core.py:2061), end_session (line 2115) and load_session (line 2185). That is a different object with a different field set under a different name, and this paper claims only that ContextFS independently realises an analogous structure. It does not claim that ContextFS implements the vocabulary of any other reference implementation.

7.4 Coalgebraic scope

The most useful limit to state is the one where the model has nothing to say. ContextFS is not only a lineage store; it is a retrieval system, and retrieval is how an agent actually uses it. ContextFS.search (core.py:968) and StorageRouter.search (storage_router.py:435) route queries to a full-text index and, when configured, to a vector store. Neither is an observation of (ms,Es)(m_{s}, E_{s}) in the sense of Definition 3.14, and the reason is visible in the write path. StorageRouter.save (line 89) commits the record to SQLite first, described in the source as authoritative, and then attempts the vector-store write inside try: ... except Exception as e: logger.warning(...) (lines 103–106), followed by an optional graph synchronisation under a second handler (lines 109–113). The vector index is therefore not determined by the record store along every path in the code, and even where it is, the result of a similarity query depends on an embedding function and on index parameters that are not part of the state.

One could enlarge the state space to include the index. That would be honest but not useful: the enlarged state would have no laws, because nothing in Definition 3.21 constrains an approximate nearest-neighbour structure, and the enlarged coalgebra’s behaviours would distinguish states that no consumer can distinguish. The better description is that ContextFS has two faces. The lineage face is a coalgebra with checkable laws and one proved stability property. The retrieval face is a ranking function over the same records, and the vocabulary that fits it is information retrieval, not universal coalgebra. Reporting the correspondence for the first face and declining it for the second is the scoped claim of Section 1.1 applied honestly to this pillar.

Remark 7.4. This limit is different in kind from the five failed clauses. A failed clause is a defect that a patch could fix. Section 7.4 describes a part of the system for which the formalism is the wrong tool, and no patch to ContextFS would change that. When the series asks at which point the categorical account stops paying for itself, this is Part I’s answer.

8 Related work

8.0.0.1 Coalgebra for systems.

The identification of state-based systems with coalgebras for a Set\mathbf{Set}-endofunctor, and of behavioural equivalence with the kernel of the map into a final coalgebra, is Rutten’s (15); Jacobs’s book is the standard extended treatment (5). Our two functors are unremarkable within that framework: FX=(Out×X)A+F X = (\mathrm{Out}\times X)^{A^{+}} is a deterministic machine with input and output, and HX=Rec⊥×Pfin(R×X)H X = \mathrm{Rec}_{\bot} \times \mathcal{P}_{\mathrm{fin}}(\mathcal{R}\times X) is a finitely branching labelled transition system with state observations. What is specific here is the use to which the framework is put. We do not construct a final semantics for ContextFS; we use finality only in Corollary 5.12, and the substantive work is the identification of the sub-functor H−H^{-} for which the inclusion of existing identifiers is a homomorphism. Restricting a labelled transition system to a sub-alphabet in order to recover a homomorphism that fails on the full alphabet is a standard move; applying it to separate the immutable and mutable directions of a lineage graph appears not to have been done for agent memory.

8.0.0.2 Temporal databases.

Valid time and transaction time as independent dimensions, and the definition of a bitemporal relation as one timestamped in exactly one of each, are due to the temporal database community; Jensen and Snodgrass give the consensus semantics (6) and Snodgrass’s book the practical treatment (17). Theorem 5.27 is a negative result relative to that literature, and it is worth being clear about what is and is not new in it. That an append-only log with one clock cannot answer valid-time queries is not new. What the theorem adds is the precise form of the failure for a system whose designers describe it as history preserving: the recording map identifies histories that differ in valid time, so the deficiency is in the schema and not in the query implementation, and no improvement to Timeline.at could recover the distinction.

8.0.0.3 Immutability and merge.

The design pattern of deriving new records rather than mutating old ones, and of treating the accumulated log as the system of record, is argued for at length by Helland (4). ContextFS’s derivation operations follow it. The merge operation is closer to the conflict-free replicated data type literature (16). The failure identified in Proposition 5.20 has a familiar form: the result depends on something other than the states being merged. The comparison has to be made at the right level, though. Asking for idempotency on the nose is a category error for an operation that allocates a fresh identifier on every call, and the correct question for a coalgebra is whether the resulting states are behaviourally equivalent. Proposition 5.24 settles that question positively: under an observation blind to allocation, merging the same arguments twice yields a store bisimilar to merging them once. Proposition 5.25 settles the other direction negatively, and identifies the two order-sensitive fields responsible. The difference from a replicated data type is therefore not that ContextFS’s merge fails the lattice laws outright, but that it satisfies one of them only after quotienting and fails the other by design, since it records provenance in argument order.

8.0.0.4 Typed agent memory.

A prior paper by the present author describes a typed memory layer for an agent operating system in Elixir, with schema registries, causal versioning and a graph relationship manager (8). That work and this one share a name for their reference implementation and little else: it is a design paper about a different runtime, it argues from the type system rather than from coalgebraic laws, and it makes no bitemporal claim. It is cited here as prior art by the same author, and its numbering is unrelated to this series: its Part III is a memory paper, this series’ Part III is about protocols.

8.0.0.5 The four pillars and the Architecture triple.

Zhou et al. supply the pillar taxonomy (20); de los Riscos, Corbacho and Arbib supply the Architecture triple (14); Banu proposes the correspondence between them and validates it against one reference implementation (1). Meng et al. give the alternative enumerative taxonomy of harness components (9), and Liu’s typed lambda calculus for agent composition is a different formal route to the same territory (7). Cao’s argument for why externalization is happening at all is economic and complexity-theoretic rather than structural (2). This paper contributes one row of the correspondence, checked against a system that was not built with the correspondence in mind, and reports the row as partial; the remaining three rows and the question of where the correspondence stops paying for itself are taken up in companion work (10, 11, 12, 19).

8.0.0.6 Operads and wiring.

Companion treatments of skills and protocols use the operad of wiring diagrams and its typed refinements (10, 11, 18, 3). We mention the line only to place the boundary: the memory coalgebra composes with itself by the partial monoid of Proposition 3.6, and the operadic machinery those Parts need is not required here.

9 Open problems

Each problem has an explicit criterion for resolution.

  1. The second axis. Proposition 7.1 shows a valid-time field is sufficient to make the observation non-degenerate. It does not show what the derivation operations should do with valid time when their arguments disagree. If merge receives three records with three valid-time instants, no choice among “earliest”, “latest” and “the interval spanned” is forced by the model, and the correct answer plausibly depends on the record type. Settled by: a merge rule for valid time whose induced observation satisfies a stated law, together with a proof that the law fails for the other candidate rules.

  2. A merge that reads only what it is given. Proposition 5.20 identifies a tie-break that reads an ambient enumeration order. A one-line change repairs that merge path but not L5 globally, because prefix recall has a second order-dependent choice (Proposition 6.3). Deterministic resolution would be required as well. The merge change does not settle the algebraic question, which Proposition 5.24 and Proposition 5.25 leave in an odd state: idempotent up to allocation-blind bisimilarity, and not commutative, because the merged record records its sources in argument order. Whether a lineage merge should be commutative at all is genuinely unclear, since argument order is provenance. Settled by: either an argument-order-blind merge whose provenance is recovered from the edge set rather than from the content, together with a proof that it is commutative up to Definition 5.23, or a proof that no such merge can distinguish histories the current one distinguishes.

  3. One ancestry representation. Proposition 5.14 and Remark 5.15 describe two representations, two fallbacks and two entry shapes. The question is not which to keep, since keeping the edges and deriving the metadata is the obvious answer, but what to do about stores already written under the current scheme, where the two representations may already disagree and there is no record of which is right. Settled by: a reconciliation procedure whose output is a store satisfying L2, together with a statement of what it does when the two representations conflict.

  4. Identifier resolution and composition. Proposition 6.3 makes the frame property conditional on resolution by equality, and Remark 6.8 gives the scale at which the condition starts to bite. The open question is whether a namespace-scoped resolution rule restores Proposition 6.2 in full, given that Proposition 5.18 shows namespaces are not respected by every operation. Settled by: either a proof that prefix resolution scoped to a namespace satisfies the frame property, or a counterexample of the same shape as Proposition 5.18.

  5. Persisting the version model. Section 7.2 describes a module whose invariant is_consistent is exactly the internal-agreement property L2 and L3 approximate, and which is never written down. Whether the persisted lineage graph and a persisted timeline can be kept in agreement, and at what cost, is open. Settled by: a mapping from lineage graphs to timelines together with a proof that it takes stores satisfying L1 to L3 to timelines satisfying is_consistent.

  6. Deletion. Every result in Section 5.2 carries the hypothesis that the operation is a derivation call, and Proposition 5.2 shows why. A memory system that must support erasure cannot be monotone, so the question is what weakening of Theorem 5.7 survives a deletion operation that is required to remove content while retaining the shape of the ancestry. Settled by: a coalgebra on stores-with-tombstones for which the analogue of Theorem 5.7 holds and under which erased records are not recoverable.

10 Conclusion

For ContextFS at commit a93035d, the coalgebraic model establishes that a record’s ancestry is invariant under later derivation on the ancestry-closed fragment, while its descendants may change. Five law clauses fail with named source witnesses, and the retrieval subsystem lies outside the useful scope of the coalgebraic account.

Three failures share one cause: merge typing, prefix resolution, and deletion consult an enumeration order that no interface declares. L5 exposes that hidden dependency directly. The strongest conclusion is therefore local: the model earns its cost where it converts implementation details into falsifiable stability and independence claims, and should not be extended to retrieval ranking merely to preserve a uniform vocabulary.

11 Code evidence

Every code-backed claim in the body has a row below. Repository ContextFS is at /Users/mlong/Documents/Development/contextfs-ai/contextfs, commit a93035d; agent-os is at /Users/mlong/Documents/Development/agentherowork/agent-os, commit 61cb399. ContextFS paths in the third column are relative to src/contextfs/; the one agent-os path is relative to the repository root. Statements in the last column are implementation-level claims established by reading the file at the stated path and lines at the stated commit, not reports of runtime behaviour.

Claim Repository, commit Path Identifier, lines What the code shows
Claim Repository, commit Path Identifier, lines What the code shows
The relation set R\mathcal{R} is a closed enumeration of twenty-two labels (Section 4.2) ContextFS a93035d storage_protocol.py EdgeRelation, 32–78 The implementation at a93035d declares an Enum with twenty-two members grouped as evolution, merge, split, reference, semantic, hierarchical, causal, resolution and dependency relations.
The inverse map is an involution with four fixed points (Lemma 4.1) ContextFS a93035d storage_protocol.py EdgeRelation.get_inverse, 80–104 The implementation looks the label up in a twenty-entry table and returns the label itself on a missing key; the table pairs eighteen labels in nine mutually inverse pairs and fixes two.
The abstract carrier signature is save, recall, search and delete (Table 1) ContextFS a93035d storage_protocol.py StorageBackend, 148–249 The implementation declares a Protocol with save, save_batch, recall, search, delete and delete_by_namespace.
The abstract edge signature is separate from the record signature (Table 1) ContextFS a93035d storage_protocol.py GraphBackend, 347–547 The implementation declares a second Protocol carrying add_edge, get_edges, get_related, find_path and get_lineage.
The four derivation methods instantiate the derivation schema S1 to S4 (Lemma 5.6) ContextFS a93035d memory_lineage.py evolve 132–134, 152, 165, 170–180; merge 225–232, 256, 269, 275–285; split 397–399, 422, 436, 442–452; supersede 526–540 evolve, merge and split retrieve only their named identifiers and raise or skip when one is absent, construct records with fresh identifiers and a namespace copied from a retrieved record, and write ancestry-labelled edges out of the constructed identifier only. supersede retrieves and constructs nothing and writes two edges whose labels lie outside the ancestry vocabulary. No method removes an edge. S2’s freshness proviso is carried as a hypothesis and is not established by this code; see the next row.
Identifier freshness is sampled rather than checked (Proposition 5.3) ContextFS a93035d schemas.py, storage_router.py Memory.id 1371; StorageRouter.save 89; _save_to_sqlite 172 The identifier default factory is str(uuid.uuid4())[:12]; no code path queries the store for that identifier before writing, and the insert statement is INSERT OR REPLACE, so a collision replaces the existing row without an error.
No derivation call mutates a retrieved record, and the three that construct records draw identifiers from the default factory (Proposition 5.1) ContextFS a93035d memory_lineage.py MemoryLineage, 61–91; evolve 93; merge 191; split 360; supersede 508 evolve, merge and split retrieve inputs through self._storage.recall, construct Memory instances with the default identifier factory, and persist them with self._storage.save at lines 165, 269 and 436; supersede neither retrieves nor constructs nor saves, writing only edges. No method assigns to a field of a retrieved object.
The ancestry edge for evolve originates at the constructed identifier (Theorem 5.7) ContextFS a93035d memory_lineage.py evolve, 170–180 The implementation writes EVOLVED_FROM from evolved.id to memory_id and EVOLVED_INTO in the opposite direction.
The same holds for merge, split and supersede (Theorem 5.7) ContextFS a93035d memory_lineage.py merge 275–285; split 442–452; supersede 526–540 Each pair is written with the derived record as source of the _FROM label and the original as source of the _INTO label; supersede uses SUPERSEDES and SUPERSEDED_BY, neither of which is an ancestry label. Whether the derived record’s identifier is fresh is the separate question of Proposition 5.3.
Ancestry is written twice, once guarded and once not (Proposition 5.14) ContextFS a93035d memory_lineage.py evolve 158, 165, 168–182; merge 262, 273–287; split 426–432, 440–454 The implementation writes evolved_from, merged_from and split_from into the constructed record’s metadata and persists the record unguarded, then attempts the edge writes inside try ... except Exception handlers that log at warning level and do not re-raise.
Two lineage fallbacks disagree on the ancestor entry shape (Remark 5.15) ContextFS a93035d storage_router.py, memory_lineage.py _get_lineage_from_sqlite 1274; get_lineage 1683, 1712–1739; get_history 599, 639–663 The router emits ancestor dictionaries keyed "id", "relation", "depth"; get_history emits dictionaries keyed "memory_id" and "relation".
link writes an inverse only when asked (Proposition 5.16) ContextFS a93035d memory_lineage.py link, 551, 580–587 The implementation writes the inverse edge inside if bidirectional: and obtains the label from EdgeRelation.get_inverse.
merge takes the namespace of its first retrieved argument unconditionally, and under the default union strategy unions metadata across all retrieved arguments (Proposition 5.18, Remark 5.19) ContextFS a93035d memory_lineage.py merge 225–230, 232, 256; _apply_merge_strategy 292, 298–307; _generate_merged_content 349–354 The retrieval loop skips identifiers that fail to resolve with a warning; the guard raises when fewer than two resolve; the constructor at line 256 sets namespace_id=memories[0].namespace_id outside any strategy branch, so it applies to every strategy; the union branch runs combined_meta.update(m.metadata) over every retrieved record, and the intersection, latest and oldest branches select metadata differently.
For a call that omits memory_type and uses the union, intersection or weighted strategy, the merged type is chosen by maximising a count over set(types) (Proposition 5.20) ContextFS a93035d memory_lineage.py, schemas.py _apply_merge_strategy 307, 323, 345; merge 247–248; MemoryType 23 The implementation computes the type by maximising a count over a Python set of enumeration members in three of the five strategy branches; the caller’s optional memory_type argument overrides the result afterwards at lines 247–248; MemoryType is declared class MemoryType(str, Enum).
No operation argument or record field carries a valid time (Theorem 5.27) ContextFS a93035d schemas.py, memory_lineage.py Memory 1362–1398; evolve signature 93–101, metadata 159 Memory declares exactly two time-valued fields, created_at and updated_at, both defaulting to datetime.now(); evolve’s parameters are identifier, content, summary, tag flags and a change reason, and its "evolution_timestamp" is assigned a UTC clock reading in ISO 8601 form.
The change reason is not exposed on any agent-facing surface (Remark 5.29) ContextFS a93035d core.py, mcp/fastmcp_server.py, cli/memory.py ContextFS.evolve 1515, 1545–1554; contextfs_evolve 623; evolve 247 The facade forwards five keyword arguments to MemoryLineage.evolve and reason is not among them; the Model Context Protocol tool and the command line both call the facade method.
Timeline.at compares against a single timestamp field (Corollary 5.28) ContextFS a93035d types/versioned.py VersionEntry 87, 117; Timeline.at 238–263; from_memory 359, 383–396 VersionEntry declares one timestamp field; at returns the last entry whose timestamp is at or before the argument; from_memory sets that field from memory.created_at.
Edge endpoints are unchecked by default (Proposition 5.30) ContextFS a93035d storage_router.py add_edge 1327–1370 The parameter validate: bool = False guards the endpoint existence query; every call site in memory_lineage.py omits the argument.
save can replace and delete removes edges in both directions (Proposition 5.2) ContextFS a93035d storage_router.py, core.py _save_to_sqlite 172; ContextFS.save 673, 686; delete 586; _delete_edges_for_memory 626, 639 The insert statement is INSERT OR REPLACE INTO memories; the facade accepts an explicit id; deletion issues DELETE FROM memory_edges WHERE from_id = ? OR to_id = ? before deleting the record.
Recall resolves by identifier prefix without ordering (Proposition 6.3) ContextFS a93035d storage_router.py, core.py _recall_from_sqlite 310–326; ContextFS.recall 1288–1298 The query is SELECT * FROM memories WHERE id LIKE ? AND deleted_at IS NULL with the pattern f"{memory_id}%" and no ORDER BY, followed by fetchone; the docstring says the identifier can be partial with at least eight characters.
Identifiers carry forty-four random bits (Remark 6.8) ContextFS a93035d schemas.py Memory.id 1371 The default factory is str(uuid.uuid4())[:12], whose first twelve characters of the grouped string form are eleven hexadecimal digits.
The versioned model is constructed on demand and never persisted (Section 7.2) ContextFS a93035d types/versioned.py, schemas.py, types/memory.py, core.py VersionedMem 306; is_consistent 576; from_memory 359; from_lineage 402; Memory.as_versioned 1995; Mem.as_versioned 278; memories DDL 203–220 Both entry points call VersionedMem.from_memory, which builds a timeline of at most one entry from a single record; the memories table declaration has no timeline column.
The schema layer is a checking layer, not part of the state (Section 6.4) ContextFS a93035d types/registry.py, schemas.py SchemaRegistry 23; Memory.validate_structured_data_schema 1411 The registry resolves schema types by name and the model validator rejects structured_data that fails the type’s JSON schema.
The nearest run-scoped object is Session, not RunContext (Remark 7.3) ContextFS a93035d schemas.py, core.py Session 2035; SessionMessage 2025; start_session 2061; end_session 2115; load_session 2185 Session declares identifier, label, namespace, tool, device, repository path and branch, messages, start and end instants, summary and created-memory identifiers.
The identifier RunContext does not occur in ContextFS (Remark 7.3) ContextFS a93035d repository root none A recursive search of the working tree at a93035d for the literal string RunContext, excluding compiled caches, returns zero matches. This row is evidenced by a search over the repository rather than by a single file, and is the only row of this appendix of that kind.
The retrieval face is not determined by the record store (Section 7.4) ContextFS a93035d storage_router.py, core.py StorageRouter.save 89, 103–106, 109–113; StorageRouter.search 435; ContextFS.search 968 The implementation commits to SQLite first, described in the source as authoritative, and then attempts the vector-store and graph writes inside exception handlers that log and continue.
Prior typed memory work by the same author (Section 8) agent-os 61cb399 papers/latex/memory-layer.tex abstract, 169–181 The abstract at commit 61cb399 describes a typed memory layer on Elixir with twenty-four memory types, a schema registry, vector-clock versioning, a graph relationship manager and a multi-backend storage router, and makes no bitemporal claim.

References

[1] B. Banu. Harness engineering as categorical architecture. arXiv:2605.12239, 2026.

[2] Z. Cao. The end of software engineering: how AI agents are fundamentally restructuring the software paradigm. arXiv:2606.05608v1, 2026. Retitled “Agentic software: how AI agents are restructuring the software paradigm” in a later version; citations here are to v1.

[3] B. Fong and D. I. Spivak. Seven sketches in compositionality: an invitation to applied category theory. arXiv:1803.05316, 2018.

[4] P. Helland. Immutability changes everything. Communications of the ACM, 59(1):64–70, 2016. DOI 10.1145/2844112.

[5] B. Jacobs. Introduction to Coalgebra: Towards Mathematics of States and Observation. Cambridge Tracts in Theoretical Computer Science 59. Cambridge University Press, 2016. ISBN 9781107177895.

[6] C. S. Jensen and R. T. Snodgrass. Semantics of time-varying information. Information Systems, 21(4):311–352, 1996. DOI 10.1016/0306-4379(96)00017-8.

[7] Q. Liu. λA\lambda_{A}: a typed lambda calculus for LLM agent composition. arXiv:2604.11767, 2026.

[8] M. Long. The AI operating system, Part III: memory layer, a typed filesystem for persistent agent cognition. YonedaAI Research Collective, 2026.

[9] Q. Meng, Y. Wang, L. Chen, W. Wu, Y. Li, W. Jiang, Q. Wang, C. Lu, Y. Gao, Y. Wu, and Y. Hu. Agent harness for large language model agents: a survey. Preprints, 2026. DOI 10.20944/preprints202604.0428.v2.

[10] M. Long. Operadic skill composition. Unpublished Part II manuscript in the Agentic Engineering repository, The YonedaAI Collaboration, 2026. https://github.com/YonedaAI/agent-engineering.

[11] M. Long. Typed protocol wiring. Unpublished Part III manuscript in the Agentic Engineering repository, The YonedaAI Collaboration, 2026. https://github.com/YonedaAI/agent-engineering.

[12] M. Long. Harness architecture. Unpublished Part IV manuscript in the Agentic Engineering repository, The YonedaAI Collaboration, 2026. https://github.com/YonedaAI/agent-engineering.

[13] Python Software Foundation. The Python language reference: PYTHONHASHSEED and hash randomisation. https://docs.python.org/3/using/cmdline.html#envvar-PYTHONHASHSEED, accessed 2026.

[14] P. de los Riscos, F. J. Corbacho, and M. A. Arbib. Working paper: towards a category-theoretic comparative framework for artificial general intelligence. arXiv:2603.28906, 2026.

[15] J. J. M. M. Rutten. Universal coalgebra: a theory of systems. Theoretical Computer Science, 249(1):3–80, 2000. DOI 10.1016/S0304-3975(00)00056-6.

[16] M. Shapiro, N. Preguiça, C. Baquero, and M. Zawirski. Conflict-free replicated data types. In Stabilization, Safety, and Security of Distributed Systems (SSS 2011), LNCS 6976, pages 386–400. Springer, 2011. DOI 10.1007/978-3-642-24550-3_29.

[17] R. T. Snodgrass. Developing Time-Oriented Database Applications in SQL. Morgan Kaufmann, 2000.

[18] D. I. Spivak. The operad of wiring diagrams: formalizing a graphical language for databases, recursion, and plug-and-play circuits. arXiv:1305.0297, 2013.

[19] M. Long. The End of Software Engineering and the Rise of Agentic Engineering. Unpublished synthesis manuscript in the Agentic Engineering repository, The YonedaAI Collaboration, 2026. https://github.com/YonedaAI/agent-engineering.

[20] C. Zhou, H. Chai, W. Chen, et al. Externalization in LLM agents: a unified review of memory, skills, protocols and harness engineering. arXiv:2604.08224, 2026.