Background ConsolidatorWorker dispatches consolidation work to a Claude Code agent through harness.Harness.Execute (NOT via system DMs — per feedback_system_dm_no_trigger.md) with a one-time 15m dispatch token. The dispatched agent uses six new MCP tools, all token-gated and recording every action to memory_consolidation_jobs.actions JSON. Stores: - memory_links.go (+ test): typed edges with actor-prefix reserved-type guard; AddConsolidationLink bypass for memory_mark_duplicate / memory_supersede (their contractual writers). - memory_pins.go (+ test): owner pin overlay, bypasses score floor. - memory_status.go: queries the memory_status view. - consolidation_jobs.go: Create / Dispatch / Lease / AppendAction / Complete with ErrJobAlreadyInFlight via partial unique index. - auto_links.go: MessageListener generating mention / reply_to / channel_cooccurrence links automatically on send. Worker: - consolidator.go (+ test): ticker pattern modeled on StalemateWorker. Watermark trigger for link_gen / dedup_contradiction; daily 03:00 UTC for sleep-time core rewrite. Wallclock budget via harness Budget. Global semaphore gates concurrent owners. Mocked-harness test asserts no system DM is ever sent. - consolidator_prompts.go: four job-type prompts passed via env to the dispatched agent. MCP tools (internal/mcp/memory_tools.go + test): - memory_list_unprocessed, memory_write_reflection, memory_rewrite_core, memory_mark_duplicate, memory_supersede, memory_add_link. - Full error-code matrix tested per contracts/mcp-memory-tools.md. - Registered only when SYNAPBUS_DREAM_ENABLED=1. Injection extensions: - search/injection.go: pin overlay applied after retrieval; status filter drops soft_deleted / superseded unless pinned. New PinProvider, StatusProvider, MessageLookup hooks on InjectionOpts. Wiring: - cmd/synapbus/main.go: stores constructed, AutoLinkListener attached to MessagingService, mcpSrv.SetDream wired, ConsolidatorWorker start/stop, admin DreamRun closure. - cmd/synapbus/admin.go: synapbus memory dream-run --owner --job socket-RPC command (forces a single job bypassing trigger). Cycle workarounds (documented in code): - messaging.DreamAgent / HarnessDispatcher are local interfaces (the agents and harness packages import messaging, not the reverse). main.go wraps the real types via adapter structs. Stubbed: - Cron expression parsing (DreamDeepCron). Hardcoded daily 03:00 UTC. Adding robfig/cron deferred to keep no-new-deps. Pre-existing reactor test failures (5) are unchanged; confirmed pre-020 via stash check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
82 lines
1.9 KiB
Go
82 lines
1.9 KiB
Go
package messaging
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
)
|
|
|
|
func TestPinStore_PinUnpin(t *testing.T) {
|
|
db := newTestDB(t)
|
|
s := NewPinStore(db)
|
|
ctx := context.Background()
|
|
|
|
if err := s.Pin(ctx, "1", 42, "human:algis", "always relevant"); err != nil {
|
|
t.Fatalf("Pin: %v", err)
|
|
}
|
|
|
|
ids, err := s.ListForOwner(ctx, "1")
|
|
if err != nil {
|
|
t.Fatalf("ListForOwner: %v", err)
|
|
}
|
|
if len(ids) != 1 || ids[0] != 42 {
|
|
t.Errorf("ListForOwner: got %v want [42]", ids)
|
|
}
|
|
|
|
// Re-pin updates note in place; no duplicate row.
|
|
if err := s.Pin(ctx, "1", 42, "human:algis", "updated note"); err != nil {
|
|
t.Fatalf("re-Pin: %v", err)
|
|
}
|
|
pins, err := s.ListPinsForOwner(ctx, "1")
|
|
if err != nil {
|
|
t.Fatalf("ListPinsForOwner: %v", err)
|
|
}
|
|
if len(pins) != 1 || pins[0].Note != "updated note" {
|
|
t.Errorf("Re-pin: unexpected pins %v", pins)
|
|
}
|
|
|
|
if err := s.Unpin(ctx, "1", 42); err != nil {
|
|
t.Fatalf("Unpin: %v", err)
|
|
}
|
|
ids, _ = s.ListForOwner(ctx, "1")
|
|
if len(ids) != 0 {
|
|
t.Errorf("after Unpin: want empty, got %v", ids)
|
|
}
|
|
|
|
// Unpin on missing row is a no-op.
|
|
if err := s.Unpin(ctx, "1", 42); err != nil {
|
|
t.Errorf("Unpin on missing: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestPinStore_OwnerScoping(t *testing.T) {
|
|
db := newTestDB(t)
|
|
s := NewPinStore(db)
|
|
ctx := context.Background()
|
|
|
|
if err := s.Pin(ctx, "1", 10, "human:1", ""); err != nil {
|
|
t.Fatalf("Pin 1: %v", err)
|
|
}
|
|
if err := s.Pin(ctx, "1", 20, "human:1", ""); err != nil {
|
|
t.Fatalf("Pin 1: %v", err)
|
|
}
|
|
if err := s.Pin(ctx, "2", 30, "human:2", ""); err != nil {
|
|
t.Fatalf("Pin 2: %v", err)
|
|
}
|
|
|
|
one, err := s.ListForOwner(ctx, "1")
|
|
if err != nil {
|
|
t.Fatalf("ListForOwner 1: %v", err)
|
|
}
|
|
if len(one) != 2 {
|
|
t.Errorf("owner 1: want 2 pins, got %d", len(one))
|
|
}
|
|
|
|
two, err := s.ListForOwner(ctx, "2")
|
|
if err != nil {
|
|
t.Fatalf("ListForOwner 2: %v", err)
|
|
}
|
|
if len(two) != 1 || two[0] != 30 {
|
|
t.Errorf("owner 2: got %v want [30]", two)
|
|
}
|
|
}
|