claude-code: keep two memory versions too large for one file in two, and say a text that cannot fit one value (issue 367)
mesh/merge-gate head 910bc58fe01d merged into trunk a84d0094a0a3 — pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes …
mesh/repo-check head 910bc58fe01d merged into trunk a84d0094a0a3 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate head 910bc58fe01d merged into trunk a84d0094a0a3 — pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes …
mesh/repo-check head 910bc58fe01d merged into trunk a84d0094a0a3 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery superseded: a newer head of the same pull request
A merged file could exceed the state's 256 KiB value; the newer now stays in the file and the older is written beside it, each carried. A node that was away takes the newest version when its own file holds no uncarried change (tested).
This commit is contained in:
@@ -45,6 +45,9 @@ const (
|
||||
// MemoryMostBytes is the largest file carried: the state's value holds the text, and the largest memory file
|
||||
// measured on the laptop on 2026-10-13 was 70 KiB.
|
||||
MemoryMostBytes = 128 * 1024
|
||||
// MemoryValueBytes is the most a file's encoded text may take of one value of the state (256 KiB, ADR 0201),
|
||||
// leaving room for the rest of the value.
|
||||
MemoryValueBytes = 250 * 1024
|
||||
// MemoryKeep is how long a replaced or removed version is kept in the copies, and a removal marker in the state.
|
||||
MemoryKeep = 30 * 24 * time.Hour
|
||||
// LookEvery is how often the memory folders are looked at.
|
||||
@@ -172,6 +175,10 @@ func memoryProblem(text []byte) string {
|
||||
if len(text) > MemoryMostBytes {
|
||||
return fmt.Sprintf("it is %d KiB, over the %d KiB a memory file may be to be carried", len(text)/1024, MemoryMostBytes/1024)
|
||||
}
|
||||
// One value of the state holds at most 256 KiB (ADR 0201): the text as it is encoded, with room for the rest.
|
||||
if encoded, _ := json.Marshal(string(text)); len(encoded) > MemoryValueBytes {
|
||||
return "its text, as the state encodes it, would not fit in one value of the state"
|
||||
}
|
||||
for _, re := range memorySecret {
|
||||
if re.Match(text) {
|
||||
return "it holds something shaped like a secret, which kept state never carries"
|
||||
@@ -429,7 +436,7 @@ func noticeKey(key string) string {
|
||||
}
|
||||
|
||||
// warn tells the operator of a conflict, once per file while it is open.
|
||||
func (m *MemoryCarrier) warn(key, project, path string, nodes []string) {
|
||||
func (m *MemoryCarrier) warn(key, project, path string, nodes []string, beside bool) {
|
||||
nodes = append([]string(nil), nodes...)
|
||||
sort.Strings(nodes)
|
||||
m.rec.Conflicts[key] = conflict{Project: project, Path: path, Nodes: nodes, At: m.Now().UTC()}
|
||||
@@ -441,17 +448,23 @@ func (m *MemoryCarrier) warn(key, project, path string, nodes []string) {
|
||||
if len([]rune(headline)) > 60 {
|
||||
headline = "Claude memory kept twice"
|
||||
}
|
||||
explanation := fmt.Sprintf("Needs you: keep what is right in the memory file of project %s. It was changed on %s "+
|
||||
"before either change reached the other, so both versions are now in it on every machine, the newer first.",
|
||||
label, strings.Join(nodes, " and "))
|
||||
if beside {
|
||||
explanation = fmt.Sprintf("Needs you: keep what is right in the memory file of project %s. It was changed on %s "+
|
||||
"before either change reached the other, and both were too large for one file, so the newer is in it and the "+
|
||||
"older in a second file beside it, on every machine.", label, strings.Join(nodes, " and "))
|
||||
}
|
||||
// The plain rule (ADR 0253 §3): no path, file name, time, "agent" or "session"; the verdict opens the explanation.
|
||||
err := m.Notify(map[string]any{
|
||||
"key": noticeKey(key),
|
||||
"severity": "warning",
|
||||
"subject": "claude-code",
|
||||
"summary": fmt.Sprintf("memory of project %s changed on %s and kept twice", label, strings.Join(nodes, " and ")),
|
||||
"headline": headline,
|
||||
"explanation": fmt.Sprintf("Needs you: keep what is right in the memory file of project %s. It was changed on %s "+
|
||||
"before either change reached the other, so both versions are now in it on every machine, the newer first.",
|
||||
label, strings.Join(nodes, " and ")),
|
||||
"resolved": fmt.Sprintf("the memory file of project %s holds one version again", label),
|
||||
"key": noticeKey(key),
|
||||
"severity": "warning",
|
||||
"subject": "claude-code",
|
||||
"summary": fmt.Sprintf("memory of project %s changed on %s and kept twice", label, strings.Join(nodes, " and ")),
|
||||
"headline": headline,
|
||||
"explanation": explanation,
|
||||
"resolved": fmt.Sprintf("the memory file of project %s holds one version again", label),
|
||||
})
|
||||
if err != nil && m.Say != nil {
|
||||
m.Say("memory: the operator could not be warned of the conflict in %s/%s: %v", project, path, err)
|
||||
@@ -607,7 +620,7 @@ func (m *MemoryCarrier) lookAt(key string, f localFile) error {
|
||||
func (m *MemoryCarrier) conflict(key, project, path string, local []byte, localAt time.Time, cur MemoryVersion, revision uint64) error {
|
||||
merged := MergeVersions(local, m.Node, localAt, []byte(cur.Text), cur.Node, cur.Changed)
|
||||
if memoryProblem(merged) != "" {
|
||||
return fmt.Errorf("%s/%s changed on two nodes, and both together are more than is carried; both stay as they are", project, path)
|
||||
return m.conflictBeside(key, project, path, local, localAt, cur, revision)
|
||||
}
|
||||
v := MemoryVersion{Kind: m.Kind, Project: project, Path: path, Text: string(merged), Digest: digestOf(merged),
|
||||
Base: cur.Digest, Node: m.Node, Changed: m.Now().UTC()}
|
||||
@@ -623,7 +636,48 @@ func (m *MemoryCarrier) conflict(key, project, path string, local []byte, localA
|
||||
if m.Say != nil {
|
||||
m.Say("memory: %s/%s was changed on %s and on %s; both versions are kept in it, the newer first", project, path, m.Node, cur.Node)
|
||||
}
|
||||
m.warn(key, project, path, []string{m.Node, cur.Node})
|
||||
m.warn(key, project, path, []string{m.Node, cur.Node}, false)
|
||||
return nil
|
||||
}
|
||||
|
||||
// OlderBeside is where the older of two versions too large to share one file is kept: beside the file, named for
|
||||
// the node it was changed on ("MEMORY.md" from shanks: "MEMORY.older-from-shanks.md").
|
||||
func OlderBeside(path, node string) string {
|
||||
ext := filepath.Ext(path)
|
||||
return strings.TrimSuffix(path, ext) + ".older-from-" + node + ext
|
||||
}
|
||||
|
||||
// conflictBeside keeps two versions too large to share one file in two (ADR 0343 §7): the newer in the file, the
|
||||
// older beside it, each carried.
|
||||
func (m *MemoryCarrier) conflictBeside(key, project, path string, local []byte, localAt time.Time, cur MemoryVersion, revision uint64) error {
|
||||
newer, newerNode, newerAt := local, m.Node, localAt
|
||||
older, olderNode := []byte(cur.Text), cur.Node
|
||||
if cur.Changed.After(localAt) || (cur.Changed.Equal(localAt) && cur.Node < m.Node) {
|
||||
newer, newerNode, newerAt = []byte(cur.Text), cur.Node, cur.Changed
|
||||
older, olderNode = local, m.Node
|
||||
}
|
||||
beside := OlderBeside(path, olderNode)
|
||||
v := MemoryVersion{Kind: m.Kind, Project: project, Path: path, Text: string(newer), Digest: digestOf(newer),
|
||||
Base: cur.Digest, Node: newerNode, Changed: newerAt}
|
||||
if newerNode == m.Node {
|
||||
v.Node, v.Changed = m.Node, m.Now().UTC()
|
||||
}
|
||||
if err := m.Store.Update(key, v, revision); err != nil {
|
||||
return fmt.Errorf("%s/%s changed on two nodes, and the state changed again meanwhile; it is judged again on "+
|
||||
"the next change: %w", project, path, err)
|
||||
}
|
||||
if err := m.writeLocal(project, beside, older, "before-merge"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.writeLocal(project, path, newer, "before-merge"); err != nil {
|
||||
return err
|
||||
}
|
||||
m.record(key, v)
|
||||
if m.Say != nil {
|
||||
m.Say("memory: %s/%s was changed on %s and on %s, too large to hold both: the newer is in it, the older beside it as %s",
|
||||
project, path, m.Node, cur.Node, beside)
|
||||
}
|
||||
m.warn(key, project, path, []string{m.Node, cur.Node}, true)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user