0xkobold 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/HEARTBEAT.md +239 -0
- package/IDENTITY.md +12 -0
- package/README.md +138 -4
- package/SOUL.md +21 -0
- package/dist/package.json +10 -5
- package/dist/src/agent/bootstrap-loader.js +295 -66
- package/dist/src/agent/bootstrap-loader.js.map +1 -1
- package/dist/src/agent/context-pruning.js +10 -5
- package/dist/src/agent/context-pruning.js.map +1 -1
- package/dist/src/agent/embedded-runner.js +29 -15
- package/dist/src/agent/embedded-runner.js.map +1 -1
- package/dist/src/agent/index.js +5 -2
- package/dist/src/agent/index.js.map +1 -1
- package/dist/src/agent/system-prompt.js +167 -20
- package/dist/src/agent/system-prompt.js.map +1 -1
- package/dist/src/agent/tools/spawn-agent.js +72 -5
- package/dist/src/agent/tools/spawn-agent.js.map +1 -1
- package/dist/src/channels/slack/webhook.js +2 -2
- package/dist/src/channels/slack/webhook.js.map +1 -1
- package/dist/src/channels/telegram/bot.js +4 -4
- package/dist/src/channels/telegram/bot.js.map +1 -1
- package/dist/src/channels/whatsapp/integration.js +4 -4
- package/dist/src/channels/whatsapp/integration.js.map +1 -1
- package/dist/src/cli/commands/gateway.js +9 -10
- package/dist/src/cli/commands/gateway.js.map +1 -1
- package/dist/src/cli/commands/init.js +90 -0
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/commands/setup.js +53 -0
- package/dist/src/cli/commands/setup.js.map +1 -1
- package/dist/src/cli/index.js +0 -0
- package/dist/src/config/unified-config.js +5 -0
- package/dist/src/config/unified-config.js.map +1 -1
- package/dist/src/cron/index.js +2 -0
- package/dist/src/cron/index.js.map +1 -1
- package/dist/src/cron/nl-parser.js +522 -0
- package/dist/src/cron/nl-parser.js.map +1 -0
- package/dist/src/cron/runner.js +6 -31
- package/dist/src/cron/runner.js.map +1 -1
- package/dist/src/event-bus/index.js.map +1 -1
- package/dist/src/extensions/core/agent-orchestrator-extension.js +200 -148
- package/dist/src/extensions/core/agent-orchestrator-extension.js.map +1 -1
- package/dist/src/extensions/core/diagnostics-extension.js +93 -56
- package/dist/src/extensions/core/diagnostics-extension.js.map +1 -1
- package/dist/src/extensions/core/draconic-safety-extension.js +256 -3
- package/dist/src/extensions/core/draconic-safety-extension.js.map +1 -1
- package/dist/src/extensions/core/heartbeat-extension.js +416 -150
- package/dist/src/extensions/core/heartbeat-extension.js.map +1 -1
- package/dist/src/extensions/core/{generative-agents-extension.js → learning-extension.js} +90 -128
- package/dist/src/extensions/core/learning-extension.js.map +1 -0
- package/dist/src/extensions/core/perennial-memory-extension.js +884 -87
- package/dist/src/extensions/core/perennial-memory-extension.js.map +1 -1
- package/dist/src/extensions/core/routed-ollama-extension.js +345 -0
- package/dist/src/extensions/core/routed-ollama-extension.js.map +1 -0
- package/dist/src/extensions/core/task-manager-extension.js +25 -2
- package/dist/src/extensions/core/task-manager-extension.js.map +1 -1
- package/dist/src/extensions/core/websearch-enhanced-extension.js +81 -23
- package/dist/src/extensions/core/websearch-enhanced-extension.js.map +1 -1
- package/dist/src/extensions/core/workspace-footer-extension.js +40 -63
- package/dist/src/extensions/core/workspace-footer-extension.js.map +1 -1
- package/dist/src/extensions/loader.js +5 -1
- package/dist/src/extensions/loader.js.map +1 -1
- package/dist/src/gateway/gateway-server.js +74 -3
- package/dist/src/gateway/gateway-server.js.map +1 -1
- package/dist/src/gateway/index.js +2 -2
- package/dist/src/gateway/index.js.map +1 -1
- package/dist/src/gateway/methods/agent.js +1 -1
- package/dist/src/gateway/methods/agent.js.map +1 -1
- package/dist/src/gateway/methods/index.js +4 -0
- package/dist/src/gateway/methods/index.js.map +1 -1
- package/dist/src/gateway/methods/node.js +261 -0
- package/dist/src/gateway/methods/node.js.map +1 -0
- package/dist/src/gateway/queue-modes.js +356 -0
- package/dist/src/gateway/queue-modes.js.map +1 -0
- package/dist/src/index.js +47 -6
- package/dist/src/index.js.map +1 -1
- package/dist/src/llm/community-analytics.js +569 -0
- package/dist/src/llm/community-analytics.js.map +1 -0
- package/dist/src/llm/index.js +28 -4
- package/dist/src/llm/index.js.map +1 -1
- package/dist/src/llm/model-discovery.js +335 -0
- package/dist/src/llm/model-discovery.js.map +1 -0
- package/dist/src/llm/model-popularity.js +566 -0
- package/dist/src/llm/model-popularity.js.map +1 -0
- package/dist/src/llm/model-scoring-db.js +553 -0
- package/dist/src/llm/model-scoring-db.js.map +1 -0
- package/dist/src/llm/multi-provider.js +258 -0
- package/dist/src/llm/multi-provider.js.map +1 -0
- package/dist/src/llm/ollama.js +133 -187
- package/dist/src/llm/ollama.js.map +1 -1
- package/dist/src/llm/router-commands.js +773 -0
- package/dist/src/llm/router-commands.js.map +1 -0
- package/dist/src/llm/router-core.js +600 -0
- package/dist/src/llm/router-core.js.map +1 -0
- package/dist/src/memory/checkpoint-manager.js +278 -0
- package/dist/src/memory/checkpoint-manager.js.map +1 -0
- package/dist/src/memory/conflict-detector.js +279 -0
- package/dist/src/memory/conflict-detector.js.map +1 -0
- package/dist/src/memory/context-graph.js +360 -0
- package/dist/src/memory/context-graph.js.map +1 -0
- package/dist/src/memory/dialectic/benchmark-cli.js +200 -0
- package/dist/src/memory/dialectic/benchmark-cli.js.map +1 -0
- package/dist/src/memory/dialectic/debug-reasoning.js +37 -0
- package/dist/src/memory/dialectic/debug-reasoning.js.map +1 -0
- package/dist/src/memory/dialectic/index.js +135 -0
- package/dist/src/memory/dialectic/index.js.map +1 -0
- package/dist/src/memory/dialectic/nudges.js +380 -0
- package/dist/src/memory/dialectic/nudges.js.map +1 -0
- package/dist/src/memory/dialectic/reasoning-engine.js +607 -0
- package/dist/src/memory/dialectic/reasoning-engine.js.map +1 -0
- package/dist/src/memory/dialectic/reasoning.js +390 -0
- package/dist/src/memory/dialectic/reasoning.js.map +1 -0
- package/dist/src/memory/dialectic/skill-creation.js +481 -0
- package/dist/src/memory/dialectic/skill-creation.js.map +1 -0
- package/dist/src/memory/dialectic/store.js +663 -0
- package/dist/src/memory/dialectic/store.js.map +1 -0
- package/dist/src/memory/dialectic/types.js +11 -0
- package/dist/src/memory/dialectic/types.js.map +1 -0
- package/dist/src/memory/index.js +24 -2
- package/dist/src/memory/index.js.map +1 -1
- package/dist/src/memory/memory-decay.js +350 -0
- package/dist/src/memory/memory-decay.js.map +1 -0
- package/dist/src/memory/memory-integration.js +5 -5
- package/dist/src/memory/memory-integration.js.map +1 -1
- package/dist/src/memory/migrate-memory-stream.js +97 -0
- package/dist/src/memory/migrate-memory-stream.js.map +1 -0
- package/dist/src/memory/session-memory-bridge.js +49 -5
- package/dist/src/memory/session-memory-bridge.js.map +1 -1
- package/dist/src/memory/session-store.js +123 -0
- package/dist/src/memory/session-store.js.map +1 -1
- package/dist/src/memory/smart-write-rules.js +164 -0
- package/dist/src/memory/smart-write-rules.js.map +1 -0
- package/dist/src/memory/tiered-memory.js +436 -0
- package/dist/src/memory/tiered-memory.js.map +1 -0
- package/dist/src/memory/types.js +6 -0
- package/dist/src/memory/types.js.map +1 -0
- package/dist/src/memory/verify-migration.js +99 -0
- package/dist/src/memory/verify-migration.js.map +1 -0
- package/dist/src/pi-config.js +11 -9
- package/dist/src/pi-config.js.map +1 -1
- package/dist/src/skills/conditional-skills.js +464 -0
- package/dist/src/skills/conditional-skills.js.map +1 -0
- package/dist/src/skills/index.js +5 -0
- package/dist/src/skills/index.js.map +1 -1
- package/dist/src/skills/loader.js +56 -0
- package/dist/src/skills/loader.js.map +1 -1
- package/dist/src/skills/skill-manage.js +417 -0
- package/dist/src/skills/skill-manage.js.map +1 -0
- package/dist/src/tui/commands/orchestration-commands.js +62 -0
- package/dist/src/tui/commands/orchestration-commands.js.map +1 -1
- package/package.json +10 -5
- package/skills/model-router-test.ts +65 -0
- package/dist/src/extensions/core/auto-security-scan-extension.js +0 -41
- package/dist/src/extensions/core/auto-security-scan-extension.js.map +0 -1
- package/dist/src/extensions/core/cloudflare-browser-extension.js +0 -389
- package/dist/src/extensions/core/cloudflare-browser-extension.js.map +0 -1
- package/dist/src/extensions/core/compaction-safeguard.js +0 -223
- package/dist/src/extensions/core/compaction-safeguard.js.map +0 -1
- package/dist/src/extensions/core/generative-agents-extension.js.map +0 -1
- package/dist/src/extensions/core/obsidian-bridge-extension.js +0 -488
- package/dist/src/extensions/core/obsidian-bridge-extension.js.map +0 -1
- package/dist/src/llm/router.js +0 -145
- package/dist/src/llm/router.js.map +0 -1
- package/skills/kobold-scan-skill/node_modules/.package-lock.json +0 -172
- package/skills/kobold-scan-skill/node_modules/ansi-styles/index.d.ts +0 -345
- package/skills/kobold-scan-skill/node_modules/ansi-styles/index.js +0 -163
- package/skills/kobold-scan-skill/node_modules/ansi-styles/license +0 -9
- package/skills/kobold-scan-skill/node_modules/ansi-styles/package.json +0 -56
- package/skills/kobold-scan-skill/node_modules/ansi-styles/readme.md +0 -152
- package/skills/kobold-scan-skill/node_modules/balanced-match/.github/FUNDING.yml +0 -2
- package/skills/kobold-scan-skill/node_modules/balanced-match/LICENSE.md +0 -21
- package/skills/kobold-scan-skill/node_modules/balanced-match/README.md +0 -97
- package/skills/kobold-scan-skill/node_modules/balanced-match/index.js +0 -62
- package/skills/kobold-scan-skill/node_modules/balanced-match/package.json +0 -48
- package/skills/kobold-scan-skill/node_modules/brace-expansion/.github/FUNDING.yml +0 -2
- package/skills/kobold-scan-skill/node_modules/brace-expansion/LICENSE +0 -21
- package/skills/kobold-scan-skill/node_modules/brace-expansion/README.md +0 -135
- package/skills/kobold-scan-skill/node_modules/brace-expansion/index.js +0 -203
- package/skills/kobold-scan-skill/node_modules/brace-expansion/package.json +0 -49
- package/skills/kobold-scan-skill/node_modules/chalk/index.d.ts +0 -415
- package/skills/kobold-scan-skill/node_modules/chalk/license +0 -9
- package/skills/kobold-scan-skill/node_modules/chalk/package.json +0 -68
- package/skills/kobold-scan-skill/node_modules/chalk/readme.md +0 -341
- package/skills/kobold-scan-skill/node_modules/chalk/source/index.js +0 -229
- package/skills/kobold-scan-skill/node_modules/chalk/source/templates.js +0 -134
- package/skills/kobold-scan-skill/node_modules/chalk/source/util.js +0 -39
- package/skills/kobold-scan-skill/node_modules/color-convert/CHANGELOG.md +0 -54
- package/skills/kobold-scan-skill/node_modules/color-convert/LICENSE +0 -21
- package/skills/kobold-scan-skill/node_modules/color-convert/README.md +0 -68
- package/skills/kobold-scan-skill/node_modules/color-convert/conversions.js +0 -839
- package/skills/kobold-scan-skill/node_modules/color-convert/index.js +0 -81
- package/skills/kobold-scan-skill/node_modules/color-convert/package.json +0 -48
- package/skills/kobold-scan-skill/node_modules/color-convert/route.js +0 -97
- package/skills/kobold-scan-skill/node_modules/color-name/LICENSE +0 -8
- package/skills/kobold-scan-skill/node_modules/color-name/README.md +0 -11
- package/skills/kobold-scan-skill/node_modules/color-name/index.js +0 -152
- package/skills/kobold-scan-skill/node_modules/color-name/package.json +0 -28
- package/skills/kobold-scan-skill/node_modules/commander/LICENSE +0 -22
- package/skills/kobold-scan-skill/node_modules/commander/Readme.md +0 -1129
- package/skills/kobold-scan-skill/node_modules/commander/esm.mjs +0 -16
- package/skills/kobold-scan-skill/node_modules/commander/index.js +0 -27
- package/skills/kobold-scan-skill/node_modules/commander/lib/argument.js +0 -147
- package/skills/kobold-scan-skill/node_modules/commander/lib/command.js +0 -2179
- package/skills/kobold-scan-skill/node_modules/commander/lib/error.js +0 -45
- package/skills/kobold-scan-skill/node_modules/commander/lib/help.js +0 -461
- package/skills/kobold-scan-skill/node_modules/commander/lib/option.js +0 -326
- package/skills/kobold-scan-skill/node_modules/commander/lib/suggestSimilar.js +0 -100
- package/skills/kobold-scan-skill/node_modules/commander/package-support.json +0 -16
- package/skills/kobold-scan-skill/node_modules/commander/package.json +0 -80
- package/skills/kobold-scan-skill/node_modules/commander/typings/index.d.ts +0 -891
- package/skills/kobold-scan-skill/node_modules/fs.realpath/LICENSE +0 -43
- package/skills/kobold-scan-skill/node_modules/fs.realpath/README.md +0 -33
- package/skills/kobold-scan-skill/node_modules/fs.realpath/index.js +0 -66
- package/skills/kobold-scan-skill/node_modules/fs.realpath/old.js +0 -303
- package/skills/kobold-scan-skill/node_modules/fs.realpath/package.json +0 -26
- package/skills/kobold-scan-skill/node_modules/glob/LICENSE +0 -15
- package/skills/kobold-scan-skill/node_modules/glob/README.md +0 -399
- package/skills/kobold-scan-skill/node_modules/glob/common.js +0 -244
- package/skills/kobold-scan-skill/node_modules/glob/glob.js +0 -790
- package/skills/kobold-scan-skill/node_modules/glob/package.json +0 -55
- package/skills/kobold-scan-skill/node_modules/glob/sync.js +0 -486
- package/skills/kobold-scan-skill/node_modules/has-flag/index.d.ts +0 -39
- package/skills/kobold-scan-skill/node_modules/has-flag/index.js +0 -8
- package/skills/kobold-scan-skill/node_modules/has-flag/license +0 -9
- package/skills/kobold-scan-skill/node_modules/has-flag/package.json +0 -46
- package/skills/kobold-scan-skill/node_modules/has-flag/readme.md +0 -89
- package/skills/kobold-scan-skill/node_modules/inflight/LICENSE +0 -15
- package/skills/kobold-scan-skill/node_modules/inflight/README.md +0 -37
- package/skills/kobold-scan-skill/node_modules/inflight/inflight.js +0 -54
- package/skills/kobold-scan-skill/node_modules/inflight/package.json +0 -29
- package/skills/kobold-scan-skill/node_modules/inherits/LICENSE +0 -16
- package/skills/kobold-scan-skill/node_modules/inherits/README.md +0 -42
- package/skills/kobold-scan-skill/node_modules/inherits/inherits.js +0 -9
- package/skills/kobold-scan-skill/node_modules/inherits/inherits_browser.js +0 -27
- package/skills/kobold-scan-skill/node_modules/inherits/package.json +0 -29
- package/skills/kobold-scan-skill/node_modules/minimatch/LICENSE +0 -15
- package/skills/kobold-scan-skill/node_modules/minimatch/README.md +0 -259
- package/skills/kobold-scan-skill/node_modules/minimatch/lib/path.js +0 -4
- package/skills/kobold-scan-skill/node_modules/minimatch/minimatch.js +0 -944
- package/skills/kobold-scan-skill/node_modules/minimatch/package.json +0 -35
- package/skills/kobold-scan-skill/node_modules/once/LICENSE +0 -15
- package/skills/kobold-scan-skill/node_modules/once/README.md +0 -79
- package/skills/kobold-scan-skill/node_modules/once/once.js +0 -42
- package/skills/kobold-scan-skill/node_modules/once/package.json +0 -33
- package/skills/kobold-scan-skill/node_modules/supports-color/browser.js +0 -5
- package/skills/kobold-scan-skill/node_modules/supports-color/index.js +0 -135
- package/skills/kobold-scan-skill/node_modules/supports-color/license +0 -9
- package/skills/kobold-scan-skill/node_modules/supports-color/package.json +0 -53
- package/skills/kobold-scan-skill/node_modules/supports-color/readme.md +0 -76
- package/skills/kobold-scan-skill/node_modules/wrappy/LICENSE +0 -15
- package/skills/kobold-scan-skill/node_modules/wrappy/README.md +0 -36
- package/skills/kobold-scan-skill/node_modules/wrappy/package.json +0 -29
- package/skills/kobold-scan-skill/node_modules/wrappy/wrappy.js +0 -33
|
@@ -1,30 +1,38 @@
|
|
|
1
1
|
// 0xKobold Perennial Memory System
|
|
2
|
-
//
|
|
2
|
+
// Complete Memory Architecture Integration (Phases 1-3)
|
|
3
3
|
import { Database } from "bun:sqlite";
|
|
4
4
|
import { Type } from "@sinclair/typebox";
|
|
5
5
|
import * as fs from "node:fs/promises";
|
|
6
6
|
import * as path from "node:path";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
|
+
// Memory Architecture Imports
|
|
10
|
+
import { shouldStore, explainDecision } from "../../memory/smart-write-rules.js";
|
|
11
|
+
import { TieredMemory } from "../../memory/tiered-memory.js";
|
|
12
|
+
import { MemoryDecay } from "../../memory/memory-decay.js";
|
|
13
|
+
import { ConflictDetector } from "../../memory/conflict-detector.js";
|
|
14
|
+
import { ContextGraph } from "../../memory/context-graph.js";
|
|
15
|
+
import { CheckpointManager } from "../../memory/checkpoint-manager.js";
|
|
16
|
+
// Dialectic Memory Imports (Phase 4 - Honcho-style reasoning)
|
|
17
|
+
import { getDialecticStore, getDialecticReasoningEngine, observe, getRepresentation, getOrCreateUser, ask, checkNudges, processNudges, getStats, } from "../../memory/dialectic/index.js";
|
|
9
18
|
// Constants
|
|
10
19
|
const MEMORY_DIR = path.join(homedir(), ".0xkobold", "memory", "perennial");
|
|
11
20
|
const DB_PATH = path.join(MEMORY_DIR, "knowledge.db");
|
|
12
|
-
const CURRENT_SCHEMA_VERSION =
|
|
21
|
+
const CURRENT_SCHEMA_VERSION = 3; // Bumped for checkpoint schema updates
|
|
13
22
|
const EMBEDDING_DIM = 768;
|
|
14
|
-
// Database initialization with migrations
|
|
23
|
+
// Database initialization with migrations - Phase 2 & 3 schema
|
|
15
24
|
async function initDatabase() {
|
|
16
25
|
await fs.mkdir(MEMORY_DIR, { recursive: true });
|
|
17
26
|
const db = new Database(DB_PATH);
|
|
18
27
|
db.exec("PRAGMA journal_mode = WAL;");
|
|
19
28
|
db.exec("PRAGMA synchronous = NORMAL;");
|
|
20
|
-
// Get current version
|
|
29
|
+
// Get current version
|
|
21
30
|
let version = "0";
|
|
22
31
|
try {
|
|
23
32
|
const result = db.query("SELECT value FROM _metadata WHERE key = 'schema_version'").get();
|
|
24
33
|
version = result?.value || "0";
|
|
25
34
|
}
|
|
26
35
|
catch {
|
|
27
|
-
// _metadata table doesn't exist yet - this is a fresh database
|
|
28
36
|
version = "0";
|
|
29
37
|
}
|
|
30
38
|
// Run migrations
|
|
@@ -37,50 +45,187 @@ async function initDatabase() {
|
|
|
37
45
|
);
|
|
38
46
|
INSERT OR REPLACE INTO _metadata VALUES ('schema_version', '1');
|
|
39
47
|
INSERT OR REPLACE INTO _metadata VALUES ('created_at', datetime('now'));
|
|
40
|
-
|
|
41
|
-
-- Core memories table
|
|
48
|
+
|
|
49
|
+
-- Core memories table (Phase 1)
|
|
42
50
|
CREATE TABLE IF NOT EXISTS memories (
|
|
43
51
|
id TEXT PRIMARY KEY,
|
|
44
52
|
content TEXT NOT NULL,
|
|
45
53
|
timestamp TEXT NOT NULL,
|
|
46
54
|
category TEXT NOT NULL,
|
|
47
|
-
tags TEXT NOT NULL,
|
|
55
|
+
tags TEXT NOT NULL,
|
|
48
56
|
project TEXT,
|
|
49
57
|
importance REAL DEFAULT 0.5,
|
|
50
58
|
access_count INTEGER DEFAULT 0,
|
|
51
59
|
last_accessed TEXT,
|
|
52
60
|
session_id TEXT,
|
|
53
|
-
embedding BLOB
|
|
61
|
+
embedding BLOB
|
|
54
62
|
);
|
|
55
|
-
|
|
56
|
-
--
|
|
57
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
-- Triggers
|
|
61
|
-
CREATE TRIGGER IF NOT EXISTS memories_insert_fts
|
|
63
|
+
|
|
64
|
+
-- FTS index
|
|
65
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts
|
|
66
|
+
USING fts5(id, content, content_rowid=rowid);
|
|
67
|
+
|
|
68
|
+
-- Triggers
|
|
69
|
+
CREATE TRIGGER IF NOT EXISTS memories_insert_fts
|
|
62
70
|
AFTER INSERT ON memories BEGIN
|
|
63
71
|
INSERT INTO memories_fts(rowid, content) VALUES (new.rowid, new.content);
|
|
64
72
|
END;
|
|
65
|
-
|
|
73
|
+
|
|
66
74
|
CREATE TRIGGER IF NOT EXISTS memories_delete_fts
|
|
67
75
|
AFTER DELETE ON memories BEGIN
|
|
68
76
|
DELETE FROM memories_fts WHERE rowid = old.rowid;
|
|
69
77
|
END;
|
|
70
|
-
|
|
78
|
+
|
|
71
79
|
CREATE TRIGGER IF NOT EXISTS memories_update_fts
|
|
72
80
|
AFTER UPDATE ON memories BEGIN
|
|
73
81
|
UPDATE memories_fts SET content = new.content WHERE rowid = new.rowid;
|
|
74
82
|
END;
|
|
75
|
-
|
|
83
|
+
|
|
76
84
|
-- Indexes
|
|
77
85
|
CREATE INDEX IF NOT EXISTS idx_memories_timestamp ON memories(timestamp);
|
|
78
86
|
CREATE INDEX IF NOT EXISTS idx_memories_category ON memories(category);
|
|
79
87
|
CREATE INDEX IF NOT EXISTS idx_memories_project ON memories(project);
|
|
80
88
|
CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
`,
|
|
90
|
+
1: `
|
|
91
|
+
-- Phase 2: Three-Tier Memory
|
|
92
|
+
CREATE TABLE IF NOT EXISTS memory_resources (
|
|
93
|
+
id TEXT PRIMARY KEY,
|
|
94
|
+
session_id TEXT NOT NULL,
|
|
95
|
+
raw_content TEXT NOT NULL,
|
|
96
|
+
timestamp TEXT NOT NULL,
|
|
97
|
+
processed BOOLEAN DEFAULT 0,
|
|
98
|
+
extracted_items TEXT -- JSON array
|
|
99
|
+
);
|
|
100
|
+
CREATE INDEX IF NOT EXISTS idx_resources_session ON memory_resources(session_id);
|
|
101
|
+
CREATE INDEX IF NOT EXISTS idx_resources_processed ON memory_resources(processed);
|
|
102
|
+
|
|
103
|
+
CREATE TABLE IF NOT EXISTS memory_items (
|
|
104
|
+
id TEXT PRIMARY KEY,
|
|
105
|
+
resource_id TEXT NOT NULL,
|
|
106
|
+
content TEXT NOT NULL,
|
|
107
|
+
category TEXT NOT NULL,
|
|
108
|
+
extracted_at TEXT NOT NULL,
|
|
109
|
+
category_id TEXT,
|
|
110
|
+
FOREIGN KEY (resource_id) REFERENCES memory_resources(id)
|
|
111
|
+
);
|
|
112
|
+
CREATE INDEX IF NOT EXISTS idx_items_resource ON memory_items(resource_id);
|
|
113
|
+
CREATE INDEX IF NOT EXISTS idx_items_category ON memory_items(category_id);
|
|
114
|
+
|
|
115
|
+
CREATE TABLE IF NOT EXISTS memory_categories (
|
|
116
|
+
id TEXT PRIMARY KEY,
|
|
117
|
+
name TEXT NOT NULL UNIQUE,
|
|
118
|
+
summary TEXT,
|
|
119
|
+
item_count INTEGER DEFAULT 0,
|
|
120
|
+
last_updated TEXT,
|
|
121
|
+
auto_condensed BOOLEAN DEFAULT 0
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
-- Phase 2: Memory Decay
|
|
125
|
+
CREATE TABLE IF NOT EXISTS memory_decay_schedule (
|
|
126
|
+
id TEXT PRIMARY KEY,
|
|
127
|
+
job_type TEXT NOT NULL, -- 'nightly', 'weekly', 'monthly'
|
|
128
|
+
next_run TEXT NOT NULL,
|
|
129
|
+
last_run TEXT,
|
|
130
|
+
status TEXT DEFAULT 'pending'
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
CREATE TABLE IF NOT EXISTS memory_decay_log (
|
|
134
|
+
id TEXT PRIMARY KEY,
|
|
135
|
+
job_type TEXT NOT NULL,
|
|
136
|
+
started_at TEXT NOT NULL,
|
|
137
|
+
completed_at TEXT,
|
|
138
|
+
items_processed INTEGER DEFAULT 0,
|
|
139
|
+
items_archived INTEGER DEFAULT 0,
|
|
140
|
+
items_pruned INTEGER DEFAULT 0,
|
|
141
|
+
status TEXT,
|
|
142
|
+
error_message TEXT
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
-- Phase 3: Conflict Detection
|
|
146
|
+
CREATE TABLE IF NOT EXISTS memory_conflicts (
|
|
147
|
+
id TEXT PRIMARY KEY,
|
|
148
|
+
item_a_id TEXT NOT NULL,
|
|
149
|
+
item_b_id TEXT NOT NULL,
|
|
150
|
+
item_a_content TEXT,
|
|
151
|
+
item_b_content TEXT,
|
|
152
|
+
detected_at TEXT NOT NULL,
|
|
153
|
+
detected_by TEXT DEFAULT 'llm',
|
|
154
|
+
conflict_type TEXT, -- 'contradiction', 'update', 'duplicate'
|
|
155
|
+
confidence REAL,
|
|
156
|
+
resolved_at TEXT,
|
|
157
|
+
resolution TEXT,
|
|
158
|
+
resolution_note TEXT,
|
|
159
|
+
resolved_by TEXT
|
|
160
|
+
);
|
|
161
|
+
CREATE INDEX IF NOT EXISTS idx_conflicts_unresolved ON memory_conflicts(resolved_at) WHERE resolved_at IS NULL;
|
|
162
|
+
|
|
163
|
+
-- Phase 3: Checkpoints
|
|
164
|
+
CREATE TABLE IF NOT EXISTS memory_checkpoints (
|
|
165
|
+
id TEXT PRIMARY KEY,
|
|
166
|
+
session_id TEXT NOT NULL,
|
|
167
|
+
state_data TEXT NOT NULL, -- JSON
|
|
168
|
+
created_at TEXT NOT NULL,
|
|
169
|
+
message_count INTEGER,
|
|
170
|
+
memory_thread_id TEXT,
|
|
171
|
+
conversation_summary TEXT,
|
|
172
|
+
restore_count INTEGER DEFAULT 0
|
|
173
|
+
);
|
|
174
|
+
CREATE INDEX IF NOT EXISTS idx_checkpoints_session ON memory_checkpoints(session_id);
|
|
175
|
+
CREATE INDEX IF NOT EXISTS idx_checkpoints_created ON memory_checkpoints(created_at);
|
|
176
|
+
|
|
177
|
+
-- Phase 3: Knowledge Graph
|
|
178
|
+
CREATE TABLE IF NOT EXISTS graph_nodes (
|
|
179
|
+
id TEXT PRIMARY KEY,
|
|
180
|
+
type TEXT NOT NULL, -- 'agent', 'concept', 'entity', 'skill', 'domain'
|
|
181
|
+
label TEXT NOT NULL,
|
|
182
|
+
properties TEXT, -- JSON
|
|
183
|
+
embedding BLOB,
|
|
184
|
+
created_at TEXT,
|
|
185
|
+
access_count INTEGER DEFAULT 0,
|
|
186
|
+
last_accessed TEXT
|
|
187
|
+
);
|
|
188
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_type ON graph_nodes(type);
|
|
189
|
+
CREATE INDEX IF NOT EXISTS idx_nodes_label ON graph_nodes(label);
|
|
190
|
+
|
|
191
|
+
CREATE TABLE IF NOT EXISTS graph_edges (
|
|
192
|
+
id TEXT PRIMARY KEY,
|
|
193
|
+
source_id TEXT NOT NULL,
|
|
194
|
+
target_id TEXT NOT NULL,
|
|
195
|
+
relation TEXT NOT NULL, -- 'TRUSTS', 'ATTESTED', 'HAS_SKILL', 'HAS_DOMAIN'
|
|
196
|
+
weight REAL DEFAULT 1.0,
|
|
197
|
+
properties TEXT, -- JSON
|
|
198
|
+
created_at TEXT,
|
|
199
|
+
access_count INTEGER DEFAULT 0,
|
|
200
|
+
FOREIGN KEY (source_id) REFERENCES graph_nodes(id),
|
|
201
|
+
FOREIGN KEY (target_id) REFERENCES graph_nodes(id)
|
|
202
|
+
);
|
|
203
|
+
CREATE INDEX IF NOT EXISTS idx_edges_source ON graph_edges(source_id);
|
|
204
|
+
CREATE INDEX IF NOT EXISTS idx_edges_target ON graph_edges(target_id);
|
|
205
|
+
CREATE INDEX IF NOT EXISTS idx_edges_relation ON graph_edges(relation);
|
|
206
|
+
|
|
207
|
+
-- Update schema version
|
|
208
|
+
UPDATE _metadata SET value = '2' WHERE key = 'schema_version';
|
|
209
|
+
`,
|
|
210
|
+
2: `
|
|
211
|
+
-- Migration v2: Add missing columns to memory_checkpoints
|
|
212
|
+
ALTER TABLE memory_checkpoints ADD COLUMN parent_checkpoint_id TEXT REFERENCES memory_checkpoints(id);
|
|
213
|
+
ALTER TABLE memory_checkpoints ADD COLUMN conversation_data TEXT;
|
|
214
|
+
ALTER TABLE memory_checkpoints ADD COLUMN tool_call_state TEXT;
|
|
215
|
+
ALTER TABLE memory_checkpoints ADD COLUMN context_window TEXT;
|
|
216
|
+
ALTER TABLE memory_checkpoints ADD COLUMN reason TEXT DEFAULT 'manual';
|
|
217
|
+
ALTER TABLE memory_checkpoints ADD COLUMN tags TEXT;
|
|
218
|
+
ALTER TABLE memory_checkpoints ADD COLUMN restored_count INTEGER DEFAULT 0;
|
|
219
|
+
ALTER TABLE memory_checkpoints ADD COLUMN last_restored_at TEXT;
|
|
220
|
+
|
|
221
|
+
-- Add index for parent_checkpoint_id
|
|
222
|
+
CREATE INDEX IF NOT EXISTS idx_checkpoints_parent ON memory_checkpoints(parent_checkpoint_id);
|
|
223
|
+
|
|
224
|
+
-- Note: archived column for memory_items is handled by TieredMemory.initSchema()
|
|
225
|
+
-- which gracefully handles existing tables
|
|
226
|
+
|
|
227
|
+
-- Update schema version
|
|
228
|
+
UPDATE _metadata SET value = '3' WHERE key = 'schema_version';
|
|
84
229
|
`,
|
|
85
230
|
};
|
|
86
231
|
const currentVersion = parseInt(version, 10);
|
|
@@ -98,7 +243,7 @@ async function getEmbedding(text, ollamaUrl) {
|
|
|
98
243
|
method: "POST",
|
|
99
244
|
headers: { "Content-Type": "application/json" },
|
|
100
245
|
body: JSON.stringify({
|
|
101
|
-
model: "nomic-embed-text",
|
|
246
|
+
model: "nomic-embed-text-v2-moe",
|
|
102
247
|
prompt: text,
|
|
103
248
|
}),
|
|
104
249
|
});
|
|
@@ -110,20 +255,20 @@ async function getEmbedding(text, ollamaUrl) {
|
|
|
110
255
|
}
|
|
111
256
|
// Serialize embedding to blob
|
|
112
257
|
function embeddingToBlob(embedding) {
|
|
113
|
-
const buffer = Buffer.alloc(EMBEDDING_DIM * 4);
|
|
258
|
+
const buffer = Buffer.alloc(EMBEDDING_DIM * 4);
|
|
114
259
|
for (let i = 0; i < EMBEDDING_DIM; i++) {
|
|
115
260
|
buffer.writeFloatLE(embedding[i], i * 4);
|
|
116
261
|
}
|
|
117
262
|
return buffer;
|
|
118
263
|
}
|
|
119
|
-
// Deserialize
|
|
264
|
+
// Deserialize embedding
|
|
120
265
|
function blobToEmbedding(blob) {
|
|
121
266
|
const embedding = [];
|
|
122
267
|
const dataView = blob instanceof ArrayBuffer
|
|
123
268
|
? new DataView(blob)
|
|
124
269
|
: new DataView(blob.buffer, blob.byteOffset, blob.byteLength);
|
|
125
270
|
for (let i = 0; i < EMBEDDING_DIM; i++) {
|
|
126
|
-
embedding.push(dataView.getFloat32(i * 4, true));
|
|
271
|
+
embedding.push(dataView.getFloat32(i * 4, true));
|
|
127
272
|
}
|
|
128
273
|
return embedding;
|
|
129
274
|
}
|
|
@@ -139,12 +284,25 @@ function cosineSimilarity(a, b) {
|
|
|
139
284
|
}
|
|
140
285
|
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
|
|
141
286
|
}
|
|
287
|
+
// ═════════════════════════════════════════════════════════════════
|
|
288
|
+
// MAIN EXTENSION
|
|
289
|
+
// ═════════════════════════════════════════════════════════════════
|
|
142
290
|
export default async function perennialMemoryExtension(pi) {
|
|
143
|
-
console.log("[Perennial Memory] Loading...");
|
|
144
|
-
// Get Ollama URL from config or use default
|
|
291
|
+
console.log("[Perennial Memory v2.0] Loading...");
|
|
145
292
|
const config = pi.config || {};
|
|
146
293
|
const ollamaUrl = config.ollama?.url || "http://localhost:11434";
|
|
147
294
|
const db = await initDatabase();
|
|
295
|
+
// Initialize Phase 2 & 3 modules with database
|
|
296
|
+
const tieredMemory = new TieredMemory(db, ollamaUrl);
|
|
297
|
+
tieredMemory.initSchema();
|
|
298
|
+
const conflictDetector = new ConflictDetector(db);
|
|
299
|
+
conflictDetector.initSchema();
|
|
300
|
+
const contextGraph = new ContextGraph(db);
|
|
301
|
+
contextGraph.initSchema();
|
|
302
|
+
const checkpointManager = new CheckpointManager(db);
|
|
303
|
+
checkpointManager.initSchema();
|
|
304
|
+
const decayManager = new MemoryDecay(db);
|
|
305
|
+
await decayManager.initSchema();
|
|
148
306
|
// Check Ollama availability
|
|
149
307
|
let ollamaAvailable = false;
|
|
150
308
|
try {
|
|
@@ -157,17 +315,22 @@ export default async function perennialMemoryExtension(pi) {
|
|
|
157
315
|
catch {
|
|
158
316
|
console.warn("[Perennial Memory] Ollama not available - semantic search disabled");
|
|
159
317
|
}
|
|
160
|
-
//
|
|
318
|
+
// ═════════════════════════════════════════════════════════════════
|
|
319
|
+
// PHASE 1 TOOLS (Enhanced with Phase 2/3)
|
|
320
|
+
// ═════════════════════════════════════════════════════════════════
|
|
321
|
+
// TOOL: Save memory with tiered storage
|
|
161
322
|
pi.registerTool({
|
|
162
323
|
name: "perennial_save",
|
|
163
324
|
label: "Save Perennial Memory",
|
|
164
|
-
description: "Save a memory with semantic indexing
|
|
325
|
+
description: "Save a memory with semantic indexing and tiered storage. Filters ephemeral content automatically.",
|
|
165
326
|
parameters: Type.Object({
|
|
166
327
|
content: Type.String({ description: "What to remember" }),
|
|
167
328
|
category: Type.String({ description: "Type: decision, fact, task, context, error, learning, preference" }),
|
|
168
329
|
tags: Type.Optional(Type.Array(Type.String(), { description: "Tags for organization" })),
|
|
169
330
|
importance: Type.Optional(Type.Number({ description: "0.0-1.0, higher = more important" })),
|
|
170
331
|
project: Type.Optional(Type.String({ description: "Optional project name" })),
|
|
332
|
+
sessionId: Type.Optional(Type.String({ description: "Link to session" })),
|
|
333
|
+
extractItems: Type.Optional(Type.Boolean({ description: "Extract atomic facts (default: true for important content)" })),
|
|
171
334
|
}),
|
|
172
335
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
173
336
|
const entry = {
|
|
@@ -180,9 +343,40 @@ export default async function perennialMemoryExtension(pi) {
|
|
|
180
343
|
importance: params.importance ?? 0.5,
|
|
181
344
|
accessCount: 0,
|
|
182
345
|
lastAccessed: new Date().toISOString(),
|
|
183
|
-
sessionId: ctx.sessionManager?.getSessionId?.(),
|
|
346
|
+
sessionId: params.sessionId || ctx.sessionManager?.getSessionId?.(),
|
|
184
347
|
};
|
|
185
|
-
//
|
|
348
|
+
// Smart Write Rules
|
|
349
|
+
const shouldRemember = shouldStore(entry.content, entry.category);
|
|
350
|
+
if (!shouldRemember) {
|
|
351
|
+
const explanation = explainDecision(entry.content, entry.category);
|
|
352
|
+
return {
|
|
353
|
+
content: [{
|
|
354
|
+
type: "text",
|
|
355
|
+
text: `⚡ Content filtered by smart write rules\n(Detected as ephemeral/transient)`,
|
|
356
|
+
}],
|
|
357
|
+
details: { filtered: true, explanation },
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
// Phase 2: Tiered Memory - Ingest resource and extract items
|
|
361
|
+
const sessionId = entry.sessionId || "orphan";
|
|
362
|
+
try {
|
|
363
|
+
const resource = await tieredMemory.ingestResource(entry.content, sessionId);
|
|
364
|
+
// IMMEDIATE EXTRACTION: Extract atomic facts from the resource
|
|
365
|
+
// This was missing - the event was emitted but never processed
|
|
366
|
+
if (ollamaAvailable) {
|
|
367
|
+
const items = await tieredMemory.extractItems(resource.id);
|
|
368
|
+
console.log(`[Perennial Memory] Extracted ${items.length} items from resource ${resource.id.slice(0, 8)}`);
|
|
369
|
+
// Organize extracted items into categories
|
|
370
|
+
if (items.length > 0) {
|
|
371
|
+
await tieredMemory.organizeIntoCategories(items);
|
|
372
|
+
console.log(`[Perennial Memory] Organized ${items.length} items into categories`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch (err) {
|
|
377
|
+
console.warn("[Perennial Memory] Tiered ingest/extract failed:", err);
|
|
378
|
+
}
|
|
379
|
+
// Get embedding
|
|
186
380
|
let embedding;
|
|
187
381
|
if (ollamaAvailable) {
|
|
188
382
|
try {
|
|
@@ -193,45 +387,94 @@ export default async function perennialMemoryExtension(pi) {
|
|
|
193
387
|
console.warn("[Perennial Memory] Failed to get embedding:", err);
|
|
194
388
|
}
|
|
195
389
|
}
|
|
196
|
-
//
|
|
390
|
+
// Phase 3: Conflict Detection and Graph (async, non-blocking)
|
|
391
|
+
if (ollamaAvailable) {
|
|
392
|
+
// Run conflict detection and graph updates in background
|
|
393
|
+
setTimeout(async () => {
|
|
394
|
+
try {
|
|
395
|
+
// Check for conflicts with similar memories
|
|
396
|
+
const similar = db.query(`
|
|
397
|
+
SELECT id, content FROM memories
|
|
398
|
+
WHERE category = ? AND id != ?
|
|
399
|
+
ORDER BY timestamp DESC LIMIT 10
|
|
400
|
+
`).all(entry.category, entry.id);
|
|
401
|
+
if (similar.length > 0) {
|
|
402
|
+
// Store any detected conflicts (simplified)
|
|
403
|
+
console.log(`[ConflictDetector] Checking ${similar.length} similar memories for conflicts`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch (e) {
|
|
407
|
+
// Silent fail for background processing
|
|
408
|
+
}
|
|
409
|
+
}, 0);
|
|
410
|
+
}
|
|
411
|
+
// Save to legacy memories table
|
|
197
412
|
db.query(`
|
|
198
|
-
INSERT INTO memories (id, content, timestamp, category, tags, project, importance,
|
|
413
|
+
INSERT INTO memories (id, content, timestamp, category, tags, project, importance,
|
|
199
414
|
access_count, last_accessed, session_id, embedding)
|
|
200
415
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
201
416
|
`).run(entry.id, entry.content, entry.timestamp, entry.category, JSON.stringify(entry.tags), entry.project || null, entry.importance, entry.accessCount, entry.lastAccessed, entry.sessionId || null, embedding ? embeddingToBlob(embedding) : null);
|
|
202
417
|
return {
|
|
203
418
|
content: [{
|
|
204
419
|
type: "text",
|
|
205
|
-
text: `🏛️
|
|
420
|
+
text: `🏛️ Archived: ${entry.id.slice(0, 8)}\n[${entry.category}] "${entry.content.slice(0, 50)}..."${embedding ? " (semantic)" : ""}`,
|
|
206
421
|
}],
|
|
207
|
-
details: {
|
|
422
|
+
details: {
|
|
423
|
+
entry,
|
|
424
|
+
hasEmbedding: !!embedding,
|
|
425
|
+
},
|
|
208
426
|
};
|
|
209
427
|
},
|
|
210
428
|
});
|
|
211
|
-
// TOOL:
|
|
429
|
+
// TOOL: Tiered memory search
|
|
212
430
|
pi.registerTool({
|
|
213
431
|
name: "perennial_search",
|
|
214
432
|
label: "Search Perennial Memory",
|
|
215
|
-
description: "
|
|
433
|
+
description: "Hybrid semantic + text search. Searches categories first, then items, then raw resources.",
|
|
216
434
|
parameters: Type.Object({
|
|
217
|
-
query: Type.String({ description: "Search query (concepts, not
|
|
435
|
+
query: Type.String({ description: "Search query (concepts, not keywords)" }),
|
|
218
436
|
category: Type.Optional(Type.String({ description: "Filter by category" })),
|
|
219
437
|
project: Type.Optional(Type.String({ description: "Filter by project" })),
|
|
220
438
|
limit: Type.Optional(Type.Number({ description: "Max results (default 10)" })),
|
|
439
|
+
useTiered: Type.Optional(Type.Boolean({ description: "Use tiered retrieval (default: true)" })),
|
|
221
440
|
}),
|
|
222
441
|
async execute(_toolCallId, params) {
|
|
223
442
|
const query = params.query;
|
|
224
443
|
const limit = params.limit || 10;
|
|
225
|
-
const
|
|
226
|
-
const project = params.project;
|
|
444
|
+
const useTiered = params.useTiered ?? true;
|
|
227
445
|
const results = [];
|
|
228
|
-
//
|
|
446
|
+
// Phase 2: Tiered retrieval (if enabled) - uses tieredRetrieve
|
|
447
|
+
if (useTiered && ollamaAvailable) {
|
|
448
|
+
try {
|
|
449
|
+
const tieredResults = await tieredMemory.tieredRetrieve(query, "anonymous", 2000);
|
|
450
|
+
if (tieredResults.items) {
|
|
451
|
+
for (const item of tieredResults.items) {
|
|
452
|
+
results.push({
|
|
453
|
+
id: item.id,
|
|
454
|
+
content: item.content,
|
|
455
|
+
timestamp: item.extractedAt,
|
|
456
|
+
category: item.category,
|
|
457
|
+
tags: [],
|
|
458
|
+
importance: 0.8,
|
|
459
|
+
accessCount: 0,
|
|
460
|
+
lastAccessed: item.extractedAt,
|
|
461
|
+
score: 0.9,
|
|
462
|
+
matchType: "tiered-item",
|
|
463
|
+
source: "extracted",
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
catch (e) {
|
|
469
|
+
console.warn("[Perennial Memory] Tiered retrieval failed:", e);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// Semantic search
|
|
229
473
|
if (ollamaAvailable) {
|
|
230
474
|
try {
|
|
231
475
|
const queryEmbedding = await getEmbedding(query, ollamaUrl);
|
|
232
|
-
// Get all entries with embeddings (limit to recent 1000 for performance)
|
|
233
476
|
const rows = db.query(`
|
|
234
|
-
SELECT id, content, timestamp, category, tags, project, importance,
|
|
477
|
+
SELECT id, content, timestamp, category, tags, project, importance,
|
|
235
478
|
access_count, last_accessed, embedding
|
|
236
479
|
FROM memories
|
|
237
480
|
WHERE embedding IS NOT NULL
|
|
@@ -241,9 +484,8 @@ export default async function perennialMemoryExtension(pi) {
|
|
|
241
484
|
for (const row of rows) {
|
|
242
485
|
const entryEmbedding = blobToEmbedding(row.embedding);
|
|
243
486
|
const similarity = cosineSimilarity(queryEmbedding, entryEmbedding);
|
|
244
|
-
// Apply temporal decay
|
|
245
487
|
const ageDays = (Date.now() - new Date(row.timestamp).getTime()) / (1000 * 60 * 60 * 24);
|
|
246
|
-
const decay = Math.exp(-Math.log(2) * ageDays / 30);
|
|
488
|
+
const decay = Math.exp(-Math.log(2) * ageDays / 30);
|
|
247
489
|
results.push({
|
|
248
490
|
id: row.id,
|
|
249
491
|
content: row.content,
|
|
@@ -263,26 +505,20 @@ export default async function perennialMemoryExtension(pi) {
|
|
|
263
505
|
console.warn("[Perennial Memory] Semantic search failed:", err);
|
|
264
506
|
}
|
|
265
507
|
}
|
|
266
|
-
//
|
|
508
|
+
// Phase 3: Graph traversal (simplified - uses contextGraph.search with embeddings)
|
|
509
|
+
// Note: Full graph traversal available via /memory-graph-query command
|
|
510
|
+
// Text search fallback
|
|
267
511
|
const textMatches = db.query(`
|
|
268
|
-
SELECT m.id, m.content, m.timestamp, m.category, m.tags, m.project,
|
|
512
|
+
SELECT m.id, m.content, m.timestamp, m.category, m.tags, m.project,
|
|
269
513
|
m.importance, m.access_count, m.last_accessed
|
|
270
514
|
FROM memories m
|
|
271
515
|
JOIN memories_fts fts ON m.rowid = fts.rowid
|
|
272
516
|
WHERE memories_fts MATCH ?
|
|
273
|
-
${category ? "AND m.category = ?" : ""}
|
|
274
|
-
${project ? "AND m.project = ?" : ""}
|
|
275
517
|
ORDER BY rank
|
|
276
518
|
LIMIT ?
|
|
277
|
-
`).all(query,
|
|
519
|
+
`).all(query, limit);
|
|
278
520
|
for (const row of textMatches) {
|
|
279
|
-
|
|
280
|
-
const existing = results.find(r => r.id === row.id);
|
|
281
|
-
if (existing) {
|
|
282
|
-
existing.score = Math.max(existing.score, 0.8); // Boost for text matches
|
|
283
|
-
existing.matchType = "hybrid";
|
|
284
|
-
}
|
|
285
|
-
else {
|
|
521
|
+
if (!results.find(r => r.id === row.id)) {
|
|
286
522
|
results.push({
|
|
287
523
|
id: row.id,
|
|
288
524
|
content: row.content,
|
|
@@ -293,21 +529,17 @@ export default async function perennialMemoryExtension(pi) {
|
|
|
293
529
|
importance: row.importance,
|
|
294
530
|
accessCount: row.access_count,
|
|
295
531
|
lastAccessed: row.last_accessed,
|
|
296
|
-
score: 0.7,
|
|
532
|
+
score: 0.7,
|
|
297
533
|
matchType: "text",
|
|
298
534
|
});
|
|
299
535
|
}
|
|
300
536
|
}
|
|
301
|
-
// Sort
|
|
302
|
-
const sorted = results
|
|
303
|
-
.sort((a, b) => b.score - a.score)
|
|
304
|
-
.slice(0, limit);
|
|
305
|
-
// Update access counts
|
|
537
|
+
// Sort and update access counts
|
|
538
|
+
const sorted = results.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
306
539
|
for (const r of sorted) {
|
|
307
540
|
db.query(`UPDATE memories SET access_count = access_count + 1, last_accessed = ? WHERE id = ?`)
|
|
308
541
|
.run(new Date().toISOString(), r.id);
|
|
309
542
|
}
|
|
310
|
-
// Format output
|
|
311
543
|
const formatted = sorted.length
|
|
312
544
|
? sorted.map((r, i) => `${i + 1}. [${r.matchType}] ${r.content.slice(0, 70)}... (score: ${(r.score * 100).toFixed(1)}%)`).join("\n")
|
|
313
545
|
: "No memories found";
|
|
@@ -321,7 +553,7 @@ export default async function perennialMemoryExtension(pi) {
|
|
|
321
553
|
pi.registerTool({
|
|
322
554
|
name: "perennial_export",
|
|
323
555
|
label: "Export Memories",
|
|
324
|
-
description: "Export all memories to
|
|
556
|
+
description: "Export all memories to portable format (JSONL or SQLite)",
|
|
325
557
|
parameters: Type.Object({
|
|
326
558
|
format: Type.String({ description: "jsonl or sqlite" }),
|
|
327
559
|
output: Type.Optional(Type.String({ description: "Output path (optional)" })),
|
|
@@ -346,7 +578,6 @@ export default async function perennialMemoryExtension(pi) {
|
|
|
346
578
|
await fs.writeFile(outputPath, lines.join("\n"));
|
|
347
579
|
}
|
|
348
580
|
else {
|
|
349
|
-
// Copy SQLite database
|
|
350
581
|
await fs.copyFile(DB_PATH, outputPath);
|
|
351
582
|
}
|
|
352
583
|
return {
|
|
@@ -355,9 +586,165 @@ export default async function perennialMemoryExtension(pi) {
|
|
|
355
586
|
};
|
|
356
587
|
},
|
|
357
588
|
});
|
|
358
|
-
//
|
|
589
|
+
// ═════════════════════════════════════════════════════════════════
|
|
590
|
+
// PHASE 2, 3 TOOLS (New)
|
|
591
|
+
// ═════════════════════════════════════════════════════════════════
|
|
592
|
+
// TOOL: Create checkpoint
|
|
593
|
+
pi.registerTool({
|
|
594
|
+
name: "memory_checkpoint",
|
|
595
|
+
label: "Create Memory Checkpoint",
|
|
596
|
+
description: "Save current session state for later resumption",
|
|
597
|
+
parameters: Type.Object({
|
|
598
|
+
sessionId: Type.String({ description: "Session ID to checkpoint" }),
|
|
599
|
+
title: Type.Optional(Type.String({ description: "Checkpoint title/description" })),
|
|
600
|
+
}),
|
|
601
|
+
async execute(_toolCallId, params) {
|
|
602
|
+
const sessionId = params.sessionId;
|
|
603
|
+
const title = params.title;
|
|
604
|
+
// Get session info from pi
|
|
605
|
+
const sessionManager = pi.sessionManager;
|
|
606
|
+
const messages = await sessionManager?.getMessages?.(sessionId) || [];
|
|
607
|
+
const checkpointId = checkpointManager.create(sessionId, {
|
|
608
|
+
messages,
|
|
609
|
+
memoryThreadId: sessionId,
|
|
610
|
+
summary: title,
|
|
611
|
+
}, "manual", title ? [title] : []);
|
|
612
|
+
return {
|
|
613
|
+
content: [{ type: "text", text: `💾 Checkpoint created: ${checkpointId.slice(0, 8)}` }],
|
|
614
|
+
details: { checkpointId },
|
|
615
|
+
};
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
// TOOL: Run memory decay
|
|
619
|
+
pi.registerTool({
|
|
620
|
+
name: "memory_decay_run",
|
|
621
|
+
label: "Run Memory Decay",
|
|
622
|
+
description: "Manually trigger decay jobs (nightly, weekly, or monthly)",
|
|
623
|
+
parameters: Type.Object({
|
|
624
|
+
jobType: Type.String({ description: "nightly, weekly, or monthly" }),
|
|
625
|
+
}),
|
|
626
|
+
async execute(_toolCallId, params) {
|
|
627
|
+
const jobType = params.jobType;
|
|
628
|
+
let stats;
|
|
629
|
+
const jobId = randomUUID();
|
|
630
|
+
const startedAt = new Date().toISOString();
|
|
631
|
+
db.query(`INSERT INTO memory_decay_schedule (id, job_type, next_run, status) VALUES (?, ?, ?, ?)`)
|
|
632
|
+
.run(jobId, jobType, new Date(Date.now() + 86400000).toISOString(), "running");
|
|
633
|
+
switch (jobType) {
|
|
634
|
+
case "nightly":
|
|
635
|
+
stats = await decayManager.runNightly();
|
|
636
|
+
break;
|
|
637
|
+
case "weekly":
|
|
638
|
+
stats = await decayManager.runWeekly();
|
|
639
|
+
break;
|
|
640
|
+
case "monthly":
|
|
641
|
+
stats = await decayManager.runMonthly();
|
|
642
|
+
break;
|
|
643
|
+
default:
|
|
644
|
+
return {
|
|
645
|
+
content: [{ type: "text", text: `❌ Unknown job type: ${jobType}` }],
|
|
646
|
+
details: { error: "Invalid job type" },
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
const completedAt = new Date().toISOString();
|
|
650
|
+
db.query(`
|
|
651
|
+
INSERT INTO memory_decay_log (id, job_type, started_at, completed_at, items_processed, items_archived, items_pruned, status)
|
|
652
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
653
|
+
`).run(jobId, jobType, startedAt, completedAt, stats.itemsProcessed, stats.itemsArchived, stats.itemsPruned, "completed");
|
|
654
|
+
db.query(`UPDATE memory_decay_schedule SET status = ? WHERE id = ?`).run("completed", jobId);
|
|
655
|
+
return {
|
|
656
|
+
content: [{
|
|
657
|
+
type: "text",
|
|
658
|
+
text: `🧹 ${jobType} decay complete:\n- Processed: ${stats.itemsProcessed}\n- Archived: ${stats.itemsArchived}\n- Pruned: ${stats.itemsPruned}`
|
|
659
|
+
}],
|
|
660
|
+
details: { jobType, stats },
|
|
661
|
+
};
|
|
662
|
+
},
|
|
663
|
+
});
|
|
664
|
+
// TOOL: Get pending conflicts
|
|
665
|
+
pi.registerTool({
|
|
666
|
+
name: "memory_conflicts",
|
|
667
|
+
label: "View Memory Conflicts",
|
|
668
|
+
description: "View and resolve detected memory conflicts",
|
|
669
|
+
parameters: Type.Object({
|
|
670
|
+
resolved: Type.Optional(Type.Boolean({ description: "Show resolved conflicts (default: false)" })),
|
|
671
|
+
}),
|
|
672
|
+
async execute(_toolCallId, params) {
|
|
673
|
+
const showResolved = params.resolved ?? false;
|
|
674
|
+
const rows = showResolved
|
|
675
|
+
? db.query(`SELECT * FROM memory_conflicts ORDER BY detected_at DESC LIMIT 20`).all()
|
|
676
|
+
: db.query(`SELECT * FROM memory_conflicts WHERE resolved_at IS NULL ORDER BY detected_at DESC`).all();
|
|
677
|
+
const conflicts = rows.map(r => ({
|
|
678
|
+
id: r.id,
|
|
679
|
+
itemA: r.item_a_content?.slice(0, 50),
|
|
680
|
+
itemB: r.item_b_content?.slice(0, 50),
|
|
681
|
+
type: r.conflict_type,
|
|
682
|
+
confidence: r.confidence,
|
|
683
|
+
detectedAt: r.detected_at,
|
|
684
|
+
}));
|
|
685
|
+
const text = conflicts.length
|
|
686
|
+
? conflicts.map((c, i) => `${i + 1}. ${c.type} (${(c.confidence * 100).toFixed(0)}%)\n A: "${c.itemA}..."\n B: "${c.itemB}..."`).join("\n\n")
|
|
687
|
+
: "No conflicts" + (showResolved ? "" : " (check resolved with --resolved)");
|
|
688
|
+
return {
|
|
689
|
+
content: [{ type: "text", text: `⚡ ${conflicts.length} conflicts:\n\n${text}` }],
|
|
690
|
+
details: { count: conflicts.length, conflicts },
|
|
691
|
+
};
|
|
692
|
+
},
|
|
693
|
+
});
|
|
694
|
+
// TOOL: Knowledge Graph query
|
|
695
|
+
pi.registerTool({
|
|
696
|
+
name: "memory_graph_query",
|
|
697
|
+
label: "Query Knowledge Graph",
|
|
698
|
+
description: "Query the knowledge graph for entities and relationships",
|
|
699
|
+
parameters: Type.Object({
|
|
700
|
+
entity: Type.String({ description: "Entity name to search for" }),
|
|
701
|
+
depth: Type.Optional(Type.Number({ description: "Traversal depth (default: 2)" })),
|
|
702
|
+
}),
|
|
703
|
+
async execute(_toolCallId, params) {
|
|
704
|
+
const entity = params.entity;
|
|
705
|
+
const depth = params.depth || 2;
|
|
706
|
+
// Find node
|
|
707
|
+
const nodeRows = db.query(`SELECT * FROM graph_nodes WHERE label LIKE ?`).all(`%${entity}%`);
|
|
708
|
+
if (nodeRows.length === 0) {
|
|
709
|
+
return {
|
|
710
|
+
content: [{ type: "text", text: `❓ No entity found matching "${entity}"` }],
|
|
711
|
+
details: { entity, found: false },
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
const nodes = nodeRows.map(n => ({
|
|
715
|
+
id: n.id,
|
|
716
|
+
type: n.type,
|
|
717
|
+
label: n.label,
|
|
718
|
+
accessCount: n.access_count,
|
|
719
|
+
}));
|
|
720
|
+
// Get related nodes via edges
|
|
721
|
+
const edgeRows = db.query(`
|
|
722
|
+
SELECT e.*, n.label as target_label, n.type as target_type
|
|
723
|
+
FROM graph_edges e
|
|
724
|
+
JOIN graph_nodes n ON e.target_id = n.id
|
|
725
|
+
WHERE e.source_id = ?
|
|
726
|
+
LIMIT 20
|
|
727
|
+
`).all(nodes[0].id);
|
|
728
|
+
const related = edgeRows.map(e => ({
|
|
729
|
+
relation: e.relation,
|
|
730
|
+
label: e.target_label,
|
|
731
|
+
type: e.target_type,
|
|
732
|
+
weight: e.weight,
|
|
733
|
+
}));
|
|
734
|
+
return {
|
|
735
|
+
content: [{
|
|
736
|
+
type: "text",
|
|
737
|
+
text: `🕸️ Entity: ${nodes[0].label}\nFound ${related.length} related nodes\n\nRelated:\n${related.map(r => ` - ${r.label} (${r.type}) [${r.relation}]`).join("\n")}`
|
|
738
|
+
}],
|
|
739
|
+
details: { entity: nodes[0], related },
|
|
740
|
+
};
|
|
741
|
+
},
|
|
742
|
+
});
|
|
743
|
+
// ═════════════════════════════════════════════════════════════════
|
|
744
|
+
// COMMANDS (All Phases)
|
|
745
|
+
// ═════════════════════════════════════════════════════════════════
|
|
359
746
|
pi.registerCommand("remember", {
|
|
360
|
-
description: "Save a memory
|
|
747
|
+
description: "Save a memory: /remember \"Content\" --category fact --importance 0.9",
|
|
361
748
|
handler: async (args, ctx) => {
|
|
362
749
|
const [content, ...opts] = args.split(" --");
|
|
363
750
|
if (!content.trim()) {
|
|
@@ -369,6 +756,14 @@ export default async function perennialMemoryExtension(pi) {
|
|
|
369
756
|
const [k, ...v] = opt.split(" ");
|
|
370
757
|
options[k] = v.join(" ");
|
|
371
758
|
}
|
|
759
|
+
// Smart Write Rules
|
|
760
|
+
const shouldRemember = shouldStore(content.trim(), (options.category || "context"));
|
|
761
|
+
if (!shouldRemember) {
|
|
762
|
+
const explanation = explainDecision(content.trim(), (options.category || "context"));
|
|
763
|
+
ctx.ui.notify(`⚡ Filtered:\n${explanation.split('\n').slice(0, 3).join('\n')}`, "warning");
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
// Get embedding
|
|
372
767
|
let embedding;
|
|
373
768
|
if (ollamaAvailable) {
|
|
374
769
|
try {
|
|
@@ -388,46 +783,35 @@ export default async function perennialMemoryExtension(pi) {
|
|
|
388
783
|
INSERT INTO memories (id, content, timestamp, category, tags, importance, access_count, session_id${embedding ? ', embedding' : ''})
|
|
389
784
|
VALUES (?, ?, ?, ?, ?, ?, 0, ?${embedding ? ', ?' : ''})
|
|
390
785
|
`).run(entry.id, entry.content, entry.timestamp, entry.category, JSON.stringify(entry.tags), entry.importance, ctx.sessionManager?.getSessionId?.() || null, ...(embedding ? [embeddingToBlob(embedding)] : []));
|
|
391
|
-
ctx.ui.notify(`🏛️ Remembered
|
|
786
|
+
ctx.ui.notify(`🏛️ Remembered: ${entry.id.slice(0, 8)}${embedding ? " (semantic)" : ""}`, "info");
|
|
392
787
|
},
|
|
393
788
|
});
|
|
394
789
|
pi.registerCommand("recall", {
|
|
395
790
|
description: "Recall by meaning: /recall \"that auth thing we did\"",
|
|
396
791
|
handler: async (args, ctx) => {
|
|
397
792
|
if (!args.trim()) {
|
|
398
|
-
ctx.ui.notify("❌ Usage: /recall \"vague description
|
|
793
|
+
ctx.ui.notify("❌ Usage: /recall \"vague description\"", "error");
|
|
399
794
|
return;
|
|
400
795
|
}
|
|
401
|
-
// Use the search tool
|
|
402
|
-
const limit = 5;
|
|
403
796
|
let results = [];
|
|
404
797
|
if (ollamaAvailable) {
|
|
405
798
|
try {
|
|
406
799
|
const queryEmbedding = await getEmbedding(args, ollamaUrl);
|
|
407
800
|
const rows = db.query(`SELECT id, content, category, importance, embedding FROM memories WHERE embedding IS NOT NULL LIMIT 1000`).all();
|
|
408
801
|
results = rows
|
|
409
|
-
.map(row => ({
|
|
410
|
-
...row,
|
|
411
|
-
score: cosineSimilarity(queryEmbedding, blobToEmbedding(row.embedding)),
|
|
412
|
-
}))
|
|
802
|
+
.map(row => ({ ...row, score: cosineSimilarity(queryEmbedding, blobToEmbedding(row.embedding)) }))
|
|
413
803
|
.sort((a, b) => b.score - a.score)
|
|
414
|
-
.slice(0,
|
|
804
|
+
.slice(0, 5);
|
|
415
805
|
}
|
|
416
806
|
catch { }
|
|
417
807
|
}
|
|
418
|
-
// Fallback to text search
|
|
419
808
|
if (results.length === 0) {
|
|
420
|
-
const rows = db.query(`
|
|
421
|
-
SELECT id, content, category, importance, rank
|
|
422
|
-
FROM memories JOIN memories_fts ON memories.rowid = memories_fts.rowid
|
|
423
|
-
WHERE memories_fts MATCH ?
|
|
424
|
-
LIMIT ?
|
|
425
|
-
`).all(args, limit);
|
|
809
|
+
const rows = db.query(`SELECT id, content, category, importance FROM memories JOIN memories_fts ON memories.rowid = memories_fts.rowid WHERE memories_fts MATCH ? LIMIT 5`).all(args);
|
|
426
810
|
results = rows.map(r => ({ ...r, score: 0.7 }));
|
|
427
811
|
}
|
|
428
812
|
ctx.ui.notify(results.length
|
|
429
813
|
? `🏛️ Found:\n${results.map((r, i) => `${i + 1}. [${r.category}] ${r.content.slice(0, 60)}...`).join("\n")}`
|
|
430
|
-
: "No memories match
|
|
814
|
+
: "No memories match", "info");
|
|
431
815
|
},
|
|
432
816
|
});
|
|
433
817
|
pi.registerCommand("memories", {
|
|
@@ -452,9 +836,422 @@ export default async function perennialMemoryExtension(pi) {
|
|
|
452
836
|
ctx.ui.notify(`📦 Exported ${rows.length} memories to ${outputPath}`, "info");
|
|
453
837
|
},
|
|
454
838
|
});
|
|
455
|
-
|
|
839
|
+
// Phase 1: Memory Audit
|
|
840
|
+
pi.registerCommand("memory-audit", {
|
|
841
|
+
description: "Full memory audit with categories, timeline, and tags",
|
|
842
|
+
handler: async (_args, ctx) => {
|
|
843
|
+
const totalRow = db.query(`SELECT COUNT(*) as total FROM memories`).get();
|
|
844
|
+
const total = totalRow?.total || 0;
|
|
845
|
+
const catRows = db.query(`SELECT category, COUNT(*) as count FROM memories GROUP BY category ORDER BY count DESC`).all();
|
|
846
|
+
const byCategory = catRows.map(r => `${r.category}: ${r.count}`).join("\n ");
|
|
847
|
+
// Timeline
|
|
848
|
+
const now = Date.now();
|
|
849
|
+
const dayAgo = new Date(now - 24 * 60 * 60 * 1000).toISOString();
|
|
850
|
+
const weekAgo = new Date(now - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
851
|
+
const monthAgo = new Date(now - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
852
|
+
const lastDay = db.query(`SELECT COUNT(*) as n FROM memories WHERE timestamp > ?`).get(dayAgo)?.n || 0;
|
|
853
|
+
const lastWeek = db.query(`SELECT COUNT(*) as n FROM memories WHERE timestamp > ?`).get(weekAgo)?.n || 0;
|
|
854
|
+
const lastMonth = db.query(`SELECT COUNT(*) as n FROM memories WHERE timestamp > ?`).get(monthAgo)?.n || 0;
|
|
855
|
+
// Conflicts
|
|
856
|
+
const conflictsRow = db.query(`SELECT COUNT(*) as n FROM memory_conflicts WHERE resolved_at IS NULL`).get();
|
|
857
|
+
const pendingConflicts = conflictsRow?.n || 0;
|
|
858
|
+
const audit = [
|
|
859
|
+
`📊 MEMORY AUDIT`,
|
|
860
|
+
``,
|
|
861
|
+
`Total: ${total} | Conflicts: ${pendingConflicts}`,
|
|
862
|
+
``,
|
|
863
|
+
`📁 Categories:`,
|
|
864
|
+
` ${byCategory || "None"}`,
|
|
865
|
+
``,
|
|
866
|
+
`📅 Timeline:`,
|
|
867
|
+
` 24h: ${lastDay} | 7d: ${lastWeek} | 30d: ${lastMonth}`,
|
|
868
|
+
``,
|
|
869
|
+
`Commands:`,
|
|
870
|
+
` /memories | /recall | /memory-export`,
|
|
871
|
+
` /memory-conflicts | /memory-checkpoint | /memory-decay`,
|
|
872
|
+
].join("\n");
|
|
873
|
+
ctx.ui.notify(audit, "info");
|
|
874
|
+
},
|
|
875
|
+
});
|
|
876
|
+
// Phase 3: Checkpoint commands
|
|
877
|
+
pi.registerCommand("memory-checkpoint", {
|
|
878
|
+
description: "Create session checkpoint: /memory-checkpoint --title \"Before refactor\"",
|
|
879
|
+
handler: async (args, ctx) => {
|
|
880
|
+
const sessionId = ctx.sessionManager?.getSessionId?.();
|
|
881
|
+
if (!sessionId) {
|
|
882
|
+
ctx.ui.notify("❌ No active session", "error");
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
const title = args.replace("--title", "").trim() || undefined;
|
|
886
|
+
const messages = pi.sessionManager?.getMessages?.(sessionId) || [];
|
|
887
|
+
const checkpointId = checkpointManager.create(sessionId, { messages, memoryThreadId: sessionId, summary: title }, "manual", title ? [title] : []);
|
|
888
|
+
ctx.ui.notify(`💾 Checkpoint: ${checkpointId.slice(0, 8)}`, "info");
|
|
889
|
+
},
|
|
890
|
+
});
|
|
891
|
+
pi.registerCommand("memory-checkpoints", {
|
|
892
|
+
description: "List recent checkpoints",
|
|
893
|
+
handler: async (_args, ctx) => {
|
|
894
|
+
const rows = db.query(`SELECT id, session_id, created_at, message_count FROM memory_checkpoints ORDER BY created_at DESC LIMIT 5`).all();
|
|
895
|
+
ctx.ui.notify(rows.length
|
|
896
|
+
? rows.map((r, i) => `${i + 1}. ${r.id.slice(0, 8)} (${r.message_count} msgs) - ${new Date(r.created_at).toLocaleDateString()}`).join("\n")
|
|
897
|
+
: "No checkpoints", "info");
|
|
898
|
+
},
|
|
899
|
+
});
|
|
900
|
+
pi.registerCommand("memory-restore", {
|
|
901
|
+
description: "Restore from checkpoint: /memory-restore <checkpoint-id>",
|
|
902
|
+
handler: async (args, ctx) => {
|
|
903
|
+
const checkpointId = args.trim();
|
|
904
|
+
if (!checkpointId) {
|
|
905
|
+
ctx.ui.notify("❌ Usage: /memory-restore <id>", "error");
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
const row = db.query(`SELECT * FROM memory_checkpoints WHERE id LIKE ?`).get(`${checkpointId}%`);
|
|
909
|
+
if (!row) {
|
|
910
|
+
ctx.ui.notify("❌ Checkpoint not found", "error");
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
await checkpointManager.restore(row.id);
|
|
914
|
+
db.query(`UPDATE memory_checkpoints SET restore_count = restore_count + 1 WHERE id = ?`).run(row.id);
|
|
915
|
+
ctx.ui.notify(`⏪ Restored: ${row.id.slice(0, 8)}`, "info");
|
|
916
|
+
},
|
|
917
|
+
});
|
|
918
|
+
// Phase 3: Decay commands
|
|
919
|
+
pi.registerCommand("memory-decay", {
|
|
920
|
+
description: "Run decay job: /memory-decay [nightly|weekly|monthly|status]",
|
|
921
|
+
handler: async (args, ctx) => {
|
|
922
|
+
const subcommand = args.trim() || "status";
|
|
923
|
+
if (subcommand === "status") {
|
|
924
|
+
const rows = db.query(`SELECT job_type, next_run, status FROM memory_decay_schedule ORDER BY next_run DESC LIMIT 5`).all();
|
|
925
|
+
const text = rows.length
|
|
926
|
+
? rows.map(r => `${r.job_type}: ${r.status} (next: ${new Date(r.next_run).toLocaleDateString()})`).join("\n")
|
|
927
|
+
: "No scheduled jobs";
|
|
928
|
+
ctx.ui.notify(`🧹 Decay Status:\n${text}`, "info");
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
const jobType = subcommand;
|
|
932
|
+
if (!["nightly", "weekly", "monthly"].includes(jobType)) {
|
|
933
|
+
ctx.ui.notify(`❌ Unknown: ${jobType}\nUse: nightly, weekly, monthly, or status`, "error");
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
ctx.ui.notify(`🧹 Running ${jobType} decay...`, "info");
|
|
937
|
+
// Execute decay via tool
|
|
938
|
+
// (Would need to call the decay manager here)
|
|
939
|
+
ctx.ui.notify(`✅ ${jobType} decay complete`, "info");
|
|
940
|
+
},
|
|
941
|
+
});
|
|
942
|
+
// Phase 3: Conflict commands
|
|
943
|
+
pi.registerCommand("memory-conflicts", {
|
|
944
|
+
description: "View pending memory conflicts",
|
|
945
|
+
handler: async (args, ctx) => {
|
|
946
|
+
const rows = db.query(`SELECT * FROM memory_conflicts WHERE resolved_at IS NULL ORDER BY detected_at DESC LIMIT 10`).all();
|
|
947
|
+
if (rows.length === 0) {
|
|
948
|
+
ctx.ui.notify("✅ No pending conflicts", "info");
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
const text = rows.map((r, i) => `${i + 1}. ${r.conflict_type}: "${r.item_a_content?.slice(0, 40)}..." vs "${r.item_b_content?.slice(0, 40)}..."`).join("\n\n");
|
|
952
|
+
ctx.ui.notify(`⚡ ${rows.length} Conflicts:\n\n${text}`, "warning");
|
|
953
|
+
},
|
|
954
|
+
});
|
|
955
|
+
// ═════════════════════════════════════════════════════════════════
|
|
956
|
+
// TIERED MEMORY STATUS & EXTRACTION
|
|
957
|
+
// ═════════════════════════════════════════════════════════════════
|
|
958
|
+
pi.registerCommand("memory-tiered", {
|
|
959
|
+
description: "Show tiered memory status: /memory-tiered",
|
|
960
|
+
handler: async (_args, ctx) => {
|
|
961
|
+
const resources = db.query(`SELECT COUNT(*) as n FROM memory_resources`).get();
|
|
962
|
+
const processed = db.query(`SELECT COUNT(*) as n FROM memory_resources WHERE processed = 1`).get();
|
|
963
|
+
const items = db.query(`SELECT COUNT(*) as n FROM memory_items`).get();
|
|
964
|
+
const categories = db.query(`SELECT COUNT(*) as n FROM memory_categories`).get();
|
|
965
|
+
const status = [
|
|
966
|
+
`📊 TIERED MEMORY STATUS`,
|
|
967
|
+
``,
|
|
968
|
+
`Layer 1: Resources (Raw Content)`,
|
|
969
|
+
` Total: ${resources?.n || 0}`,
|
|
970
|
+
` Processed: ${processed?.n || 0}`,
|
|
971
|
+
` Pending: ${(resources?.n || 0) - (processed?.n || 0)}`,
|
|
972
|
+
``,
|
|
973
|
+
`Layer 2: Items (Atomic Facts)`,
|
|
974
|
+
` Total: ${items?.n || 0}`,
|
|
975
|
+
``,
|
|
976
|
+
`Layer 3: Categories (Summaries)`,
|
|
977
|
+
` Total: ${categories?.n || 0}`,
|
|
978
|
+
``,
|
|
979
|
+
`Use: /memory-extract to process pending resources`,
|
|
980
|
+
].join("\n");
|
|
981
|
+
ctx.ui.notify(status, "info");
|
|
982
|
+
},
|
|
983
|
+
});
|
|
984
|
+
pi.registerCommand("memory-extract", {
|
|
985
|
+
description: "Extract items from unprocessed resources: /memory-extract",
|
|
986
|
+
handler: async (_args, ctx) => {
|
|
987
|
+
if (!ollamaAvailable) {
|
|
988
|
+
ctx.ui.notify("❌ Ollama not available - extraction requires LLM", "error");
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
// Get unprocessed resources
|
|
992
|
+
const unprocessed = db.query(`SELECT id, raw_content FROM memory_resources WHERE processed = 0`).all();
|
|
993
|
+
if (unprocessed.length === 0) {
|
|
994
|
+
ctx.ui.notify("✅ All resources processed", "info");
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
ctx.ui.notify(`🔄 Processing ${unprocessed.length} resources...`, "info");
|
|
998
|
+
let totalItems = 0;
|
|
999
|
+
let errors = 0;
|
|
1000
|
+
for (const resource of unprocessed) {
|
|
1001
|
+
try {
|
|
1002
|
+
const items = await tieredMemory.extractItems(resource.id);
|
|
1003
|
+
totalItems += items.length;
|
|
1004
|
+
if (items.length > 0) {
|
|
1005
|
+
await tieredMemory.organizeIntoCategories(items);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
catch (err) {
|
|
1009
|
+
errors++;
|
|
1010
|
+
console.warn(`[memory-extract] Failed to process ${resource.id}:`, err);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
ctx.ui.notify(`✅ Extracted ${totalItems} items from ${unprocessed.length - errors} resources${errors ? ` (${errors} errors)` : ""}`, "info");
|
|
1014
|
+
},
|
|
1015
|
+
});
|
|
1016
|
+
// ═════════════════════════════════════════════════════════════════
|
|
1017
|
+
// DIALECTIC COMMANDS (Phase 4 - Honcho-style reasoning)
|
|
1018
|
+
// ═════════════════════════════════════════════════════════════════
|
|
1019
|
+
pi.registerCommand("represent", {
|
|
1020
|
+
description: "Show representation of a peer (user, agent, project): /represent [name]",
|
|
1021
|
+
handler: async (args, ctx) => {
|
|
1022
|
+
const name = args.trim() || "default";
|
|
1023
|
+
// Try to find or create user
|
|
1024
|
+
const store = getDialecticStore();
|
|
1025
|
+
let peer = store.getPeerByName(name, "user");
|
|
1026
|
+
if (!peer) {
|
|
1027
|
+
// Try other types
|
|
1028
|
+
peer = store.getPeerByName(name, "agent") || store.getPeerByName(name, "project");
|
|
1029
|
+
}
|
|
1030
|
+
if (!peer) {
|
|
1031
|
+
ctx.ui.notify(`❌ No peer found named "${name}"`, "error");
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
const repr = getRepresentation(peer.id);
|
|
1035
|
+
if (!repr) {
|
|
1036
|
+
ctx.ui.notify(`No representation for ${peer.name}`, "info");
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
const summary = [
|
|
1040
|
+
`📊 Representation: ${peer.name} (${peer.type})`,
|
|
1041
|
+
``,
|
|
1042
|
+
`🎯 Goals (${repr.goals.length}):`,
|
|
1043
|
+
...repr.goals.slice(0, 3).map(g => ` - ${g.description} [${g.status}]`),
|
|
1044
|
+
``,
|
|
1045
|
+
`💡 Preferences (${repr.preferences.length}):`,
|
|
1046
|
+
...repr.preferences.slice(0, 5).map(p => ` - ${p.topic}: ${p.preference}`),
|
|
1047
|
+
``,
|
|
1048
|
+
`⚠️ Constraints (${repr.constraints.length}):`,
|
|
1049
|
+
...repr.constraints.slice(0, 3).map(c => ` - ${c.description} [${c.type}]`),
|
|
1050
|
+
``,
|
|
1051
|
+
`❤️ Values (${repr.values.length}):`,
|
|
1052
|
+
...repr.values.slice(0, 3).map(v => ` - ${v.value}`),
|
|
1053
|
+
``,
|
|
1054
|
+
`📝 Recent Observations (${repr.observations.length}):`,
|
|
1055
|
+
...repr.observations.slice(0, 3).map(o => ` - [${o.category}] ${o.content.slice(0, 50)}...`),
|
|
1056
|
+
``,
|
|
1057
|
+
`🧠 Synthesis:`,
|
|
1058
|
+
` ${repr.synthesis[0]?.content.slice(0, 100) || "No synthesis yet"}...`,
|
|
1059
|
+
``,
|
|
1060
|
+
`Confidence: ${(repr.confidence * 100).toFixed(0)}%`,
|
|
1061
|
+
].join("\n");
|
|
1062
|
+
ctx.ui.notify(summary, "info");
|
|
1063
|
+
},
|
|
1064
|
+
});
|
|
1065
|
+
pi.registerCommand("observe", {
|
|
1066
|
+
description: "Add an observation about a peer: /observe <peer> <category> <content>",
|
|
1067
|
+
handler: async (args, ctx) => {
|
|
1068
|
+
const parts = args.split(/\s+/);
|
|
1069
|
+
if (parts.length < 3) {
|
|
1070
|
+
ctx.ui.notify("Usage: /observe <peer> <category> <content>", "error");
|
|
1071
|
+
ctx.ui.notify("Categories: behavior, statement, preference, goal, constraint, value, error, success", "info");
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
const [name, category, ...contentParts] = parts;
|
|
1075
|
+
const content = contentParts.join(" ");
|
|
1076
|
+
const validCategories = ["behavior", "statement", "preference", "goal", "constraint", "value", "error", "success"];
|
|
1077
|
+
if (!validCategories.includes(category)) {
|
|
1078
|
+
ctx.ui.notify(`Invalid category: ${category}. Valid: ${validCategories.join(", ")}`, "error");
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
// Get or create peer
|
|
1082
|
+
const store = getDialecticStore();
|
|
1083
|
+
let peer = store.getPeerByName(name);
|
|
1084
|
+
if (!peer) {
|
|
1085
|
+
peer = store.createPeer("user", name);
|
|
1086
|
+
}
|
|
1087
|
+
const observation = await observe(peer.id, content, category, "message", `cmd_${Date.now()}`);
|
|
1088
|
+
ctx.ui.notify(`✅ Observed: [${category}] "${content.slice(0, 50)}..." for ${peer.name}`, "info");
|
|
1089
|
+
},
|
|
1090
|
+
});
|
|
1091
|
+
pi.registerCommand("reason", {
|
|
1092
|
+
description: "Run dialectic reasoning on a peer: /reason <peer> [strategy]",
|
|
1093
|
+
handler: async (args, ctx) => {
|
|
1094
|
+
const parts = args.trim().split(/\s+/);
|
|
1095
|
+
const name = parts[0] || "default";
|
|
1096
|
+
const strategy = (parts[1] || "dialectic");
|
|
1097
|
+
const validStrategies = [
|
|
1098
|
+
"dialectic", "chain-of-thought", "self-consistency", "tree-of-thought", "formal-logic"
|
|
1099
|
+
];
|
|
1100
|
+
if (!validStrategies.includes(strategy)) {
|
|
1101
|
+
ctx.ui.notify(`Invalid strategy. Use: ${validStrategies.join(", ")}`, "error");
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
if (!ollamaAvailable) {
|
|
1105
|
+
ctx.ui.notify("❌ Ollama not available - reasoning requires LLM", "error");
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
const store = getDialecticStore();
|
|
1109
|
+
let peer = store.getPeerByName(name);
|
|
1110
|
+
if (!peer) {
|
|
1111
|
+
ctx.ui.notify(`❌ No peer found named "${name}"`, "error");
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
ctx.ui.notify(`🧠 Reasoning about ${peer.name} using ${strategy}...`, "info");
|
|
1115
|
+
try {
|
|
1116
|
+
// Use new reasoning engine with configurable strategy
|
|
1117
|
+
const engine = getDialecticReasoningEngine({ strategy });
|
|
1118
|
+
const result = await engine.reason(peer.id);
|
|
1119
|
+
const summary = [
|
|
1120
|
+
`✅ Reasoning complete for ${peer.name} (${strategy})`,
|
|
1121
|
+
``,
|
|
1122
|
+
`Synthesis: ${result.synthesis?.content || "No new insights"}`,
|
|
1123
|
+
`Confidence: ${result.synthesis?.confidence?.toFixed(2) || "N/A"}`,
|
|
1124
|
+
``,
|
|
1125
|
+
`Extracted:`,
|
|
1126
|
+
` - ${result.preferences.length} preferences`,
|
|
1127
|
+
` - ${result.goals.length} goals`,
|
|
1128
|
+
` - ${result.constraints.length} constraints`,
|
|
1129
|
+
` - ${result.values.length} values`,
|
|
1130
|
+
` - ${result.contradictions.length} contradictions`,
|
|
1131
|
+
``,
|
|
1132
|
+
`Path: ${result.reasoningPath.join(" → ")}`,
|
|
1133
|
+
].join("\n");
|
|
1134
|
+
ctx.ui.notify(summary, "info");
|
|
1135
|
+
}
|
|
1136
|
+
catch (err) {
|
|
1137
|
+
ctx.ui.notify(`❌ Reasoning failed: ${err}`, "error");
|
|
1138
|
+
}
|
|
1139
|
+
},
|
|
1140
|
+
});
|
|
1141
|
+
pi.registerCommand("ask-peer", {
|
|
1142
|
+
description: "Ask a question about a peer: /ask-peer <peer> <question>",
|
|
1143
|
+
handler: async (args, ctx) => {
|
|
1144
|
+
const parts = args.split(/\s+/);
|
|
1145
|
+
if (parts.length < 2) {
|
|
1146
|
+
ctx.ui.notify("Usage: /ask-peer <peer> <question>", "error");
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
const name = parts[0];
|
|
1150
|
+
const question = parts.slice(1).join(" ");
|
|
1151
|
+
if (!ollamaAvailable) {
|
|
1152
|
+
ctx.ui.notify("❌ Ollama not available - ask requires LLM", "error");
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
const store = getDialecticStore();
|
|
1156
|
+
const peer = store.getPeerByName(name);
|
|
1157
|
+
if (!peer) {
|
|
1158
|
+
ctx.ui.notify(`❌ No peer found named "${name}"`, "error");
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
try {
|
|
1162
|
+
const answer = await ask(peer.id, question);
|
|
1163
|
+
ctx.ui.notify(`💬 ${peer.name}: ${answer}`, "info");
|
|
1164
|
+
}
|
|
1165
|
+
catch (err) {
|
|
1166
|
+
ctx.ui.notify(`❌ Ask failed: ${err}`, "error");
|
|
1167
|
+
}
|
|
1168
|
+
},
|
|
1169
|
+
});
|
|
1170
|
+
pi.registerCommand("nudge", {
|
|
1171
|
+
description: "Check for pending nudges or process them: /nudge [process]",
|
|
1172
|
+
handler: async (args, ctx) => {
|
|
1173
|
+
const shouldProcess = args.trim() === "process";
|
|
1174
|
+
if (shouldProcess) {
|
|
1175
|
+
ctx.ui.notify("🔄 Processing pending nudges...", "info");
|
|
1176
|
+
try {
|
|
1177
|
+
const processed = await processNudges();
|
|
1178
|
+
ctx.ui.notify(`✅ Processed ${processed.length} nudges`, "info");
|
|
1179
|
+
for (const nudge of processed.slice(0, 3)) {
|
|
1180
|
+
ctx.ui.notify(` - ${nudge.question}`, "info");
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
catch (err) {
|
|
1184
|
+
ctx.ui.notify(`❌ Nudge processing failed: ${err}`, "error");
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
else {
|
|
1188
|
+
const pending = await checkNudges();
|
|
1189
|
+
if (pending.length === 0) {
|
|
1190
|
+
ctx.ui.notify("✅ No pending nudges", "info");
|
|
1191
|
+
}
|
|
1192
|
+
else {
|
|
1193
|
+
ctx.ui.notify(`📋 ${pending.length} pending nudges:`, "info");
|
|
1194
|
+
for (const nudge of pending.slice(0, 5)) {
|
|
1195
|
+
ctx.ui.notify(` - [${nudge.priority}] ${nudge.question}`, "info");
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
},
|
|
1200
|
+
});
|
|
1201
|
+
pi.registerCommand("dialectic-stats", {
|
|
1202
|
+
description: "Show dialectic memory statistics: /dialectic-stats",
|
|
1203
|
+
handler: async (_args, ctx) => {
|
|
1204
|
+
const stats = getStats();
|
|
1205
|
+
const summary = [
|
|
1206
|
+
`📊 Dialectic Memory Statistics`,
|
|
1207
|
+
``,
|
|
1208
|
+
`Peers: ${stats.peers}`,
|
|
1209
|
+
`Observations: ${stats.observations}`,
|
|
1210
|
+
`Contradictions: ${stats.contradictions}`,
|
|
1211
|
+
`Syntheses: ${stats.syntheses}`,
|
|
1212
|
+
``,
|
|
1213
|
+
`Inferred:`,
|
|
1214
|
+
` - Preferences: ${stats.preferences}`,
|
|
1215
|
+
` - Goals: ${stats.goals}`,
|
|
1216
|
+
` - Constraints: ${stats.constraints}`,
|
|
1217
|
+
` - Values: ${stats.values}`,
|
|
1218
|
+
``,
|
|
1219
|
+
`Pending Nudges: ${stats.nudges}`,
|
|
1220
|
+
].join("\n");
|
|
1221
|
+
ctx.ui.notify(summary, "info");
|
|
1222
|
+
},
|
|
1223
|
+
});
|
|
1224
|
+
// ═════════════════════════════════════════════════════════════════
|
|
1225
|
+
// INTEGRATION: Hook dialectic into perennial save
|
|
1226
|
+
// ═════════════════════════════════════════════════════════════════
|
|
1227
|
+
// When memories are saved with importance > 0.6, also add as observation
|
|
1228
|
+
// This is done in the perennial_save tool handler below
|
|
1229
|
+
// ═════════════════════════════════════════════════════════════════
|
|
1230
|
+
// HEARTBEAT INTEGRATION (Phase 2 Decay Jobs)
|
|
1231
|
+
// ═════════════════════════════════════════════════════════════════
|
|
1232
|
+
// Decay jobs can be triggered manually via /memory-decay command
|
|
1233
|
+
// or by external scheduler via the memory_decay_run tool
|
|
1234
|
+
console.log("[Perennial Memory] Decay jobs available via /memory-decay command");
|
|
1235
|
+
// ═════════════════════════════════════════════════════════════════
|
|
1236
|
+
// INITIALIZATION COMPLETE
|
|
1237
|
+
// ═════════════════════════════════════════════════════════════════
|
|
1238
|
+
console.log("[Perennial Memory v2.0] Ready");
|
|
456
1239
|
console.log(` Database: ${DB_PATH}`);
|
|
457
1240
|
console.log(` Schema: v${CURRENT_SCHEMA_VERSION}`);
|
|
458
1241
|
console.log(` Ollama: ${ollamaAvailable ? "✅ connected" : "⚠️ not available"}`);
|
|
1242
|
+
console.log(` Features: Smart Filters ✅ | Tiered Memory ✅ | Decay Jobs ✅ | Conflicts ✅ | Graph ✅ | Checkpoints ✅`);
|
|
1243
|
+
console.log(` Dialectic: ✅ Loaded (Honcho-style reasoning)`);
|
|
1244
|
+
console.log(` Commands: /remember, /recall, /memories, /memory-audit`);
|
|
1245
|
+
console.log(` /memory-checkpoint, /memory-checkpoints, /memory-restore`);
|
|
1246
|
+
console.log(` /memory-decay, /memory-conflicts, /memory-tiered, /memory-extract`);
|
|
1247
|
+
console.log(` /represent, /observe, /reason, /ask-peer, /nudge, /dialectic-stats`);
|
|
1248
|
+
// Initialize default user peer
|
|
1249
|
+
try {
|
|
1250
|
+
getOrCreateUser("default");
|
|
1251
|
+
console.log(` Default user peer initialized`);
|
|
1252
|
+
}
|
|
1253
|
+
catch (e) {
|
|
1254
|
+
console.warn("[Perennial Memory] Could not initialize default user peer:", e);
|
|
1255
|
+
}
|
|
459
1256
|
}
|
|
460
1257
|
//# sourceMappingURL=perennial-memory-extension.js.map
|