claude-code: the memory watch only notes a change, so it starts on a node with hundreds of files (issue 367)
mesh/merge-gate head e46eeb829853 merged into trunk 4d15a155e634 — pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes …
mesh/repo-check head e46eeb829853 merged into trunk 4d15a155e634 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
mesh/merge-gate head e46eeb829853 merged into trunk 4d15a155e634 — pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes …
mesh/repo-check head e46eeb829853 merged into trunk 4d15a155e634 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
The runtime hands a watch its current values one at a time and gives them ten seconds. The handler read the state, waited for a running look and wrote files for each value, so with about 450 memory values the watch never started on g14 or shanks, and a file written on g14 never reached shanks. The handler now notes each key's newest revision and returns; a worker applies what was noted at the state's current version.
This commit is contained in:
@@ -118,7 +118,11 @@ type MemoryCarrier struct {
|
||||
Now func() time.Time
|
||||
Say func(format string, args ...any)
|
||||
|
||||
mu sync.Mutex
|
||||
mu sync.Mutex
|
||||
// queue is what the watch handed over and Drain has not applied yet: each key's newest revision.
|
||||
queueMu sync.Mutex
|
||||
queue map[string]uint64
|
||||
wake chan struct{}
|
||||
rec memoryRecord
|
||||
loaded bool
|
||||
notCarried map[string]string
|
||||
@@ -143,7 +147,7 @@ func HomeKind(p Paths) (string, string) {
|
||||
func NewMemoryCarrier(p Paths, store MemoryStore, notify func(map[string]any) error) *MemoryCarrier {
|
||||
kind, why := HomeKind(p)
|
||||
return &MemoryCarrier{Home: p.Home, Node: p.Node, State: p.State, Kind: kind, Why: why, Store: store,
|
||||
Notify: notify, Now: time.Now, Say: say}
|
||||
Notify: notify, Now: time.Now, Say: say, wake: make(chan struct{}, 1)}
|
||||
}
|
||||
|
||||
func (m *MemoryCarrier) projects() string { return filepath.Join(m.Home, ".claude", "projects") }
|
||||
@@ -894,10 +898,53 @@ func (m *MemoryCarrier) Take(key, op string, value json.RawMessage, revision uin
|
||||
if err := badName(v.Project, v.Path); err != nil {
|
||||
return fmt.Errorf("the state's value for %s is not taken: %w", key, err)
|
||||
}
|
||||
// Only noted here, never applied: the runtime hands the watch's values over one at a time and gives the whole
|
||||
// current set ten seconds, so a watch whose handler reads the state, waits for a look or writes a file never
|
||||
// starts on a node with a few hundred memory files (issue 367, seen on shanks on 2026-10-13). Drain applies
|
||||
// what was noted, the newest revision of each key.
|
||||
m.queueMu.Lock()
|
||||
if m.queue == nil {
|
||||
m.queue = map[string]uint64{}
|
||||
}
|
||||
if m.queue[key] < revision {
|
||||
m.queue[key] = revision
|
||||
}
|
||||
m.queueMu.Unlock()
|
||||
select {
|
||||
case m.wake <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Drain applies every change Take noted, each key once, at the state's current version (ADR 0343 §6).
|
||||
func (m *MemoryCarrier) Drain() error {
|
||||
m.queueMu.Lock()
|
||||
queued := m.queue
|
||||
m.queue = nil
|
||||
m.queueMu.Unlock()
|
||||
if len(queued) == 0 {
|
||||
return nil
|
||||
}
|
||||
keys := make([]string, 0, len(queued))
|
||||
for k := range queued {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.load()
|
||||
// A change handed over late is never written over a newer one: what is applied is the state's current version.
|
||||
var problems []error
|
||||
for _, key := range keys {
|
||||
if err := m.takeNow(key, queued[key]); err != nil {
|
||||
problems = append(problems, err)
|
||||
}
|
||||
}
|
||||
return errors.Join(append(problems, m.save())...)
|
||||
}
|
||||
|
||||
// takeNow applies one key at the state's current version: a change noted late is never written over a newer one.
|
||||
func (m *MemoryCarrier) takeNow(key string, revision uint64) error {
|
||||
cur, rev, err := m.current(key)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -908,8 +955,7 @@ func (m *MemoryCarrier) Take(key, op string, value json.RawMessage, revision uin
|
||||
if cur.Kind != m.Kind || MemoryKey(m.Kind, cur.Project, cur.Path) != key {
|
||||
return fmt.Errorf("the state's value for %s names another file or kind, and is not taken", key)
|
||||
}
|
||||
err = m.apply(key, *cur, rev)
|
||||
return errors.Join(err, m.save())
|
||||
return m.apply(key, *cur, rev)
|
||||
}
|
||||
|
||||
// apply writes one incoming version, read at revision, into this node's folder (ADR 0343 §6, §7, §9).
|
||||
@@ -1075,6 +1121,13 @@ func (m *MemoryCarrier) Run(watch func(handle func(key, op string, value json.Ra
|
||||
}
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
for range m.wake {
|
||||
if err := m.Drain(); err != nil && m.Say != nil {
|
||||
m.Say("memory: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
go persist("watching the agent's memory", func() error {
|
||||
return watch(func(key, op string, value json.RawMessage, revision uint64) error {
|
||||
if err := m.Take(key, op, value, revision); err != nil && m.Say != nil {
|
||||
|
||||
Reference in New Issue
Block a user