Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Binary
/mnemon
/mnemon-harness
/bin/

# Local dogfood / capability test sandboxes (per-test subdirs)
.dogfood/

# Local LLM CLI integration (use mnemon setup --global for user-wide install)
.claude/
Expand All @@ -8,6 +13,8 @@
.kanna/
.supervisor/
.mnemon/
.plan
.insight/
.env
.mnemon-dev/

Expand Down
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ ifeq ($(GOBIN),)
GOBIN := $(shell go env GOPATH)/bin
endif

.PHONY: deps build install uninstall test unit vet harness-validate codex-app-eval codex-app-eval-suite codex-memory-deep-eval codex-skill-deep-eval codex-eval-smoke docker-build docker-run compose-up compose-down compose-dev release-snapshot clean help
.PHONY: deps build install uninstall test unit vet harness-validate harness-docs-check eval-router-check codex-app-eval codex-app-eval-suite codex-memory-deep-eval codex-skill-deep-eval codex-eval-smoke docker-build docker-run compose-up compose-down compose-dev release-snapshot clean help

.DEFAULT_GOAL := help

Expand Down Expand Up @@ -48,6 +48,12 @@ vet: ## Run go vet static analysis
harness-validate: ## Validate harness loop manifests and declared asset paths
bash scripts/validate_harness_loops.sh

harness-docs-check: ## Check bilingual harness doc heading sync
bash scripts/check_bilingual_sync.sh

eval-router-check: ## Check no-model eval failed-finding routing to proposal
bash scripts/check_eval_router_fixture.sh

codex-app-eval: ## Run real Codex app-server harness smoke eval
python3 scripts/codex_app_server_eval.py

Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ LLM agents forget everything between sessions. Context compaction drops critical

Mnemon gives your agent persistent, cross-session memory — a four-graph knowledge store with intent-aware recall, importance decay, and automatic deduplication. Single binary, zero API keys, one setup command.

For the broader harness direction, Mnemon is an event-sourced lifecycle layer
for agents you already use. It does not replace Codex, Claude Code, OpenClaw,
or future hosts; it adds governed memory, skill, eval, proposal, and audit
lifecycles around them.
> **Experimental beta:** this repository also includes `mnemon-harness`, a
> source-built beta for project-local host-agent lifecycle state. It is separate
> from the stable `mnemon` CLI, not production-ready, and may make breaking
> changes at any time. See [harness/README.md](harness/README.md).

> **Claude Max / Pro subscriber?** Mnemon works entirely through your existing subscription — no separate API key required. Your LLM subscription *is* the intelligence layer. Two commands and you're done.

Expand Down Expand Up @@ -275,7 +275,7 @@ See [Development and Deployment](docs/DEPLOYMENT.md) for Docker, Compose, Ollama

## Documentation

- [Modular Self-Evolution Harness](docs/harness/README.md) — formal harness docs for modular agent, memory loop, and skill loop design
- [Mnemon Harness Beta](harness/README.md) — experimental host-agent lifecycle state
- [Memory Loop Harness](harness/loops/memory/README.md) — installable memory loop assets
- [Skill Loop Harness](harness/loops/skill/README.md) — installable skill loop assets
- [Design & Architecture](docs/DESIGN.md) — current engine architecture, algorithms, integration design
Expand Down
59 changes: 59 additions & 0 deletions cmd/event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package cmd

import (
"fmt"

"github.com/mnemon-dev/mnemon/internal/daemonemit"
"github.com/spf13/cobra"
)

var (
eventRoot string
eventPayload string
eventCorrelationID string
eventLoop string
eventHost string
)

var eventCmd = &cobra.Command{
Use: "event",
Short: "Emit Mnemon harness lifecycle events",
}

var eventEmitCmd = &cobra.Command{
Use: "emit <topic>",
Short: "Append one lifecycle event to the harness eventlog",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
payload, err := daemonemit.PayloadFromJSON(eventPayload)
if err != nil {
return err
}
event, path, err := daemonemit.Emit(daemonemit.Options{
Root: eventRoot,
Topic: args[0],
Payload: payload,
CorrelationID: eventCorrelationID,
Loop: eventLoop,
Host: eventHost,
Actor: "mnemon-manual",
Source: "mnemon.event_emit",
})
if err != nil {
return err
}
fmt.Fprintf(cmd.OutOrStdout(), "emitted %s\n", event.ID)
fmt.Fprintf(cmd.OutOrStdout(), "path: %s\n", path)
return nil
},
}

func init() {
eventEmitCmd.Flags().StringVar(&eventRoot, "root", ".", "project root whose .mnemon/events.jsonl should receive the event")
eventEmitCmd.Flags().StringVar(&eventPayload, "payload", "{}", "event payload JSON object")
eventEmitCmd.Flags().StringVar(&eventCorrelationID, "correlation-id", "", "correlation id; generated when unset")
eventEmitCmd.Flags().StringVar(&eventLoop, "loop", "", "loop id associated with the event")
eventEmitCmd.Flags().StringVar(&eventHost, "host", "", "host id associated with the event")
eventCmd.AddCommand(eventEmitCmd)
rootCmd.AddCommand(eventCmd)
}
100 changes: 100 additions & 0 deletions cmd/event_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package cmd

import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"

"github.com/mnemon-dev/mnemon/internal/model"
"github.com/spf13/cobra"
)

func TestEventEmitCommand(t *testing.T) {
root := t.TempDir()
restoreEventFlags(t)
eventRoot = root
eventPayload = `{"k":"v"}`
eventCorrelationID = "corr-test"
eventLoop = "memory"
eventHost = "mnemon"
cmd, output := eventTestCommand()
if err := eventEmitCmd.RunE(cmd, []string{"memory.hot_write_observed"}); err != nil {
t.Fatalf("event emit returned error: %v", err)
}
if !strings.Contains(output.String(), "emitted") {
t.Fatalf("unexpected output: %s", output.String())
}
data, err := os.ReadFile(filepath.Join(root, ".mnemon", "events.jsonl"))
if err != nil {
t.Fatalf("read eventlog: %v", err)
}
if !strings.Contains(string(data), `"correlation_id":"corr-test"`) {
t.Fatalf("eventlog missing correlation: %s", string(data))
}
if !strings.Contains(string(data), `"loop":"memory"`) || !strings.Contains(string(data), `"host":"mnemon"`) {
t.Fatalf("eventlog missing loop/host metadata: %s", string(data))
}
}

func TestRememberEventEmitIsFeatureFlagged(t *testing.T) {
root := t.TempDir()
t.Setenv("MNEMON_HARNESS_EVENTLOG", filepath.Join(root, "events.jsonl"))
t.Setenv("MNEMON_HARNESS_EVENT_EMIT", "1")
restoreRootFlags(t)
storeName = "test_store"
emitRememberEvent(&model.Insight{
ID: "ins-1",
Category: model.CategoryInsight,
Importance: 4,
}, "added")
data, err := os.ReadFile(filepath.Join(root, "events.jsonl"))
if err != nil {
t.Fatalf("read eventlog: %v", err)
}
if !strings.Contains(string(data), `"type":"memory.hot_write_observed"`) || !strings.Contains(string(data), `"store":"test_store"`) {
t.Fatalf("unexpected remember event: %s", string(data))
}
}

func eventTestCommand() (*cobra.Command, *bytes.Buffer) {
output := &bytes.Buffer{}
cmd := &cobra.Command{}
cmd.SetOut(output)
cmd.SetErr(output)
return cmd, output
}

func restoreEventFlags(t *testing.T) {
t.Helper()
oldRoot := eventRoot
oldPayload := eventPayload
oldCorrelationID := eventCorrelationID
oldLoop := eventLoop
oldHost := eventHost
t.Cleanup(func() {
eventRoot = oldRoot
eventPayload = oldPayload
eventCorrelationID = oldCorrelationID
eventLoop = oldLoop
eventHost = oldHost
})
eventRoot = "."
eventPayload = "{}"
eventCorrelationID = ""
eventLoop = ""
eventHost = ""
}

func restoreRootFlags(t *testing.T) {
t.Helper()
oldStoreName := storeName
oldDataDir := dataDir
t.Cleanup(func() {
storeName = oldStoreName
dataDir = oldDataDir
})
storeName = ""
dataDir = t.TempDir()
}
28 changes: 27 additions & 1 deletion cmd/remember.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

"github.com/google/uuid"
"github.com/mnemon-dev/mnemon/internal/daemonemit"
"github.com/mnemon-dev/mnemon/internal/embed"
"github.com/mnemon-dev/mnemon/internal/graph"
"github.com/mnemon-dev/mnemon/internal/model"
Expand Down Expand Up @@ -231,7 +232,9 @@ var rememberCmd = &cobra.Command{

// Update entities extracted by the engine
if len(insight.Entities) > 0 {
_ = db.UpdateEntities(insight.ID, insight.Entities)
if err := db.UpdateEntities(insight.ID, insight.Entities); err != nil {
fmt.Fprintf(os.Stderr, "warning: update entities: %v\n", err)
}
}

// Compute and store effective_importance (after edges are created)
Expand Down Expand Up @@ -292,6 +295,7 @@ var rememberCmd = &cobra.Command{
if replacedID != "" {
output["replaced_id"] = replacedID
}
emitRememberEvent(insight, diffAction)
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
return enc.Encode(output)
Expand All @@ -308,3 +312,25 @@ func init() {
rememberCmd.Flags().BoolVar(&remNoDiff, "no-diff", false, "skip duplicate/conflict detection")
rootCmd.AddCommand(rememberCmd)
}

func emitRememberEvent(insight *model.Insight, action string) {
if os.Getenv("MNEMON_HARNESS_EVENT_EMIT") != "1" {
return
}
_, _, _ = daemonemit.Emit(daemonemit.Options{
Root: ".",
Topic: "memory.hot_write_observed",
CorrelationID: "memory:" + insight.ID,
Loop: "memory",
Host: "mnemon",
Actor: "mnemon-manual",
Source: "mnemon.remember",
Store: resolveStoreName(),
Payload: map[string]any{
"insight_id": insight.ID,
"category": string(insight.Category),
"importance": insight.Importance,
"action": action,
},
})
}
Loading
Loading