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.
Files changed (252) hide show
  1. package/HEARTBEAT.md +239 -0
  2. package/IDENTITY.md +12 -0
  3. package/README.md +138 -4
  4. package/SOUL.md +21 -0
  5. package/dist/package.json +10 -5
  6. package/dist/src/agent/bootstrap-loader.js +295 -66
  7. package/dist/src/agent/bootstrap-loader.js.map +1 -1
  8. package/dist/src/agent/context-pruning.js +10 -5
  9. package/dist/src/agent/context-pruning.js.map +1 -1
  10. package/dist/src/agent/embedded-runner.js +29 -15
  11. package/dist/src/agent/embedded-runner.js.map +1 -1
  12. package/dist/src/agent/index.js +5 -2
  13. package/dist/src/agent/index.js.map +1 -1
  14. package/dist/src/agent/system-prompt.js +167 -20
  15. package/dist/src/agent/system-prompt.js.map +1 -1
  16. package/dist/src/agent/tools/spawn-agent.js +72 -5
  17. package/dist/src/agent/tools/spawn-agent.js.map +1 -1
  18. package/dist/src/channels/slack/webhook.js +2 -2
  19. package/dist/src/channels/slack/webhook.js.map +1 -1
  20. package/dist/src/channels/telegram/bot.js +4 -4
  21. package/dist/src/channels/telegram/bot.js.map +1 -1
  22. package/dist/src/channels/whatsapp/integration.js +4 -4
  23. package/dist/src/channels/whatsapp/integration.js.map +1 -1
  24. package/dist/src/cli/commands/gateway.js +9 -10
  25. package/dist/src/cli/commands/gateway.js.map +1 -1
  26. package/dist/src/cli/commands/init.js +90 -0
  27. package/dist/src/cli/commands/init.js.map +1 -1
  28. package/dist/src/cli/commands/setup.js +53 -0
  29. package/dist/src/cli/commands/setup.js.map +1 -1
  30. package/dist/src/cli/index.js +0 -0
  31. package/dist/src/config/unified-config.js +5 -0
  32. package/dist/src/config/unified-config.js.map +1 -1
  33. package/dist/src/cron/index.js +2 -0
  34. package/dist/src/cron/index.js.map +1 -1
  35. package/dist/src/cron/nl-parser.js +522 -0
  36. package/dist/src/cron/nl-parser.js.map +1 -0
  37. package/dist/src/cron/runner.js +6 -31
  38. package/dist/src/cron/runner.js.map +1 -1
  39. package/dist/src/event-bus/index.js.map +1 -1
  40. package/dist/src/extensions/core/agent-orchestrator-extension.js +200 -148
  41. package/dist/src/extensions/core/agent-orchestrator-extension.js.map +1 -1
  42. package/dist/src/extensions/core/diagnostics-extension.js +93 -56
  43. package/dist/src/extensions/core/diagnostics-extension.js.map +1 -1
  44. package/dist/src/extensions/core/draconic-safety-extension.js +256 -3
  45. package/dist/src/extensions/core/draconic-safety-extension.js.map +1 -1
  46. package/dist/src/extensions/core/heartbeat-extension.js +416 -150
  47. package/dist/src/extensions/core/heartbeat-extension.js.map +1 -1
  48. package/dist/src/extensions/core/{generative-agents-extension.js → learning-extension.js} +90 -128
  49. package/dist/src/extensions/core/learning-extension.js.map +1 -0
  50. package/dist/src/extensions/core/perennial-memory-extension.js +884 -87
  51. package/dist/src/extensions/core/perennial-memory-extension.js.map +1 -1
  52. package/dist/src/extensions/core/routed-ollama-extension.js +345 -0
  53. package/dist/src/extensions/core/routed-ollama-extension.js.map +1 -0
  54. package/dist/src/extensions/core/task-manager-extension.js +25 -2
  55. package/dist/src/extensions/core/task-manager-extension.js.map +1 -1
  56. package/dist/src/extensions/core/websearch-enhanced-extension.js +81 -23
  57. package/dist/src/extensions/core/websearch-enhanced-extension.js.map +1 -1
  58. package/dist/src/extensions/core/workspace-footer-extension.js +40 -63
  59. package/dist/src/extensions/core/workspace-footer-extension.js.map +1 -1
  60. package/dist/src/extensions/loader.js +5 -1
  61. package/dist/src/extensions/loader.js.map +1 -1
  62. package/dist/src/gateway/gateway-server.js +74 -3
  63. package/dist/src/gateway/gateway-server.js.map +1 -1
  64. package/dist/src/gateway/index.js +2 -2
  65. package/dist/src/gateway/index.js.map +1 -1
  66. package/dist/src/gateway/methods/agent.js +1 -1
  67. package/dist/src/gateway/methods/agent.js.map +1 -1
  68. package/dist/src/gateway/methods/index.js +4 -0
  69. package/dist/src/gateway/methods/index.js.map +1 -1
  70. package/dist/src/gateway/methods/node.js +261 -0
  71. package/dist/src/gateway/methods/node.js.map +1 -0
  72. package/dist/src/gateway/queue-modes.js +356 -0
  73. package/dist/src/gateway/queue-modes.js.map +1 -0
  74. package/dist/src/index.js +47 -6
  75. package/dist/src/index.js.map +1 -1
  76. package/dist/src/llm/community-analytics.js +569 -0
  77. package/dist/src/llm/community-analytics.js.map +1 -0
  78. package/dist/src/llm/index.js +28 -4
  79. package/dist/src/llm/index.js.map +1 -1
  80. package/dist/src/llm/model-discovery.js +335 -0
  81. package/dist/src/llm/model-discovery.js.map +1 -0
  82. package/dist/src/llm/model-popularity.js +566 -0
  83. package/dist/src/llm/model-popularity.js.map +1 -0
  84. package/dist/src/llm/model-scoring-db.js +553 -0
  85. package/dist/src/llm/model-scoring-db.js.map +1 -0
  86. package/dist/src/llm/multi-provider.js +258 -0
  87. package/dist/src/llm/multi-provider.js.map +1 -0
  88. package/dist/src/llm/ollama.js +133 -187
  89. package/dist/src/llm/ollama.js.map +1 -1
  90. package/dist/src/llm/router-commands.js +773 -0
  91. package/dist/src/llm/router-commands.js.map +1 -0
  92. package/dist/src/llm/router-core.js +600 -0
  93. package/dist/src/llm/router-core.js.map +1 -0
  94. package/dist/src/memory/checkpoint-manager.js +278 -0
  95. package/dist/src/memory/checkpoint-manager.js.map +1 -0
  96. package/dist/src/memory/conflict-detector.js +279 -0
  97. package/dist/src/memory/conflict-detector.js.map +1 -0
  98. package/dist/src/memory/context-graph.js +360 -0
  99. package/dist/src/memory/context-graph.js.map +1 -0
  100. package/dist/src/memory/dialectic/benchmark-cli.js +200 -0
  101. package/dist/src/memory/dialectic/benchmark-cli.js.map +1 -0
  102. package/dist/src/memory/dialectic/debug-reasoning.js +37 -0
  103. package/dist/src/memory/dialectic/debug-reasoning.js.map +1 -0
  104. package/dist/src/memory/dialectic/index.js +135 -0
  105. package/dist/src/memory/dialectic/index.js.map +1 -0
  106. package/dist/src/memory/dialectic/nudges.js +380 -0
  107. package/dist/src/memory/dialectic/nudges.js.map +1 -0
  108. package/dist/src/memory/dialectic/reasoning-engine.js +607 -0
  109. package/dist/src/memory/dialectic/reasoning-engine.js.map +1 -0
  110. package/dist/src/memory/dialectic/reasoning.js +390 -0
  111. package/dist/src/memory/dialectic/reasoning.js.map +1 -0
  112. package/dist/src/memory/dialectic/skill-creation.js +481 -0
  113. package/dist/src/memory/dialectic/skill-creation.js.map +1 -0
  114. package/dist/src/memory/dialectic/store.js +663 -0
  115. package/dist/src/memory/dialectic/store.js.map +1 -0
  116. package/dist/src/memory/dialectic/types.js +11 -0
  117. package/dist/src/memory/dialectic/types.js.map +1 -0
  118. package/dist/src/memory/index.js +24 -2
  119. package/dist/src/memory/index.js.map +1 -1
  120. package/dist/src/memory/memory-decay.js +350 -0
  121. package/dist/src/memory/memory-decay.js.map +1 -0
  122. package/dist/src/memory/memory-integration.js +5 -5
  123. package/dist/src/memory/memory-integration.js.map +1 -1
  124. package/dist/src/memory/migrate-memory-stream.js +97 -0
  125. package/dist/src/memory/migrate-memory-stream.js.map +1 -0
  126. package/dist/src/memory/session-memory-bridge.js +49 -5
  127. package/dist/src/memory/session-memory-bridge.js.map +1 -1
  128. package/dist/src/memory/session-store.js +123 -0
  129. package/dist/src/memory/session-store.js.map +1 -1
  130. package/dist/src/memory/smart-write-rules.js +164 -0
  131. package/dist/src/memory/smart-write-rules.js.map +1 -0
  132. package/dist/src/memory/tiered-memory.js +436 -0
  133. package/dist/src/memory/tiered-memory.js.map +1 -0
  134. package/dist/src/memory/types.js +6 -0
  135. package/dist/src/memory/types.js.map +1 -0
  136. package/dist/src/memory/verify-migration.js +99 -0
  137. package/dist/src/memory/verify-migration.js.map +1 -0
  138. package/dist/src/pi-config.js +11 -9
  139. package/dist/src/pi-config.js.map +1 -1
  140. package/dist/src/skills/conditional-skills.js +464 -0
  141. package/dist/src/skills/conditional-skills.js.map +1 -0
  142. package/dist/src/skills/index.js +5 -0
  143. package/dist/src/skills/index.js.map +1 -1
  144. package/dist/src/skills/loader.js +56 -0
  145. package/dist/src/skills/loader.js.map +1 -1
  146. package/dist/src/skills/skill-manage.js +417 -0
  147. package/dist/src/skills/skill-manage.js.map +1 -0
  148. package/dist/src/tui/commands/orchestration-commands.js +62 -0
  149. package/dist/src/tui/commands/orchestration-commands.js.map +1 -1
  150. package/package.json +10 -5
  151. package/skills/model-router-test.ts +65 -0
  152. package/dist/src/extensions/core/auto-security-scan-extension.js +0 -41
  153. package/dist/src/extensions/core/auto-security-scan-extension.js.map +0 -1
  154. package/dist/src/extensions/core/cloudflare-browser-extension.js +0 -389
  155. package/dist/src/extensions/core/cloudflare-browser-extension.js.map +0 -1
  156. package/dist/src/extensions/core/compaction-safeguard.js +0 -223
  157. package/dist/src/extensions/core/compaction-safeguard.js.map +0 -1
  158. package/dist/src/extensions/core/generative-agents-extension.js.map +0 -1
  159. package/dist/src/extensions/core/obsidian-bridge-extension.js +0 -488
  160. package/dist/src/extensions/core/obsidian-bridge-extension.js.map +0 -1
  161. package/dist/src/llm/router.js +0 -145
  162. package/dist/src/llm/router.js.map +0 -1
  163. package/skills/kobold-scan-skill/node_modules/.package-lock.json +0 -172
  164. package/skills/kobold-scan-skill/node_modules/ansi-styles/index.d.ts +0 -345
  165. package/skills/kobold-scan-skill/node_modules/ansi-styles/index.js +0 -163
  166. package/skills/kobold-scan-skill/node_modules/ansi-styles/license +0 -9
  167. package/skills/kobold-scan-skill/node_modules/ansi-styles/package.json +0 -56
  168. package/skills/kobold-scan-skill/node_modules/ansi-styles/readme.md +0 -152
  169. package/skills/kobold-scan-skill/node_modules/balanced-match/.github/FUNDING.yml +0 -2
  170. package/skills/kobold-scan-skill/node_modules/balanced-match/LICENSE.md +0 -21
  171. package/skills/kobold-scan-skill/node_modules/balanced-match/README.md +0 -97
  172. package/skills/kobold-scan-skill/node_modules/balanced-match/index.js +0 -62
  173. package/skills/kobold-scan-skill/node_modules/balanced-match/package.json +0 -48
  174. package/skills/kobold-scan-skill/node_modules/brace-expansion/.github/FUNDING.yml +0 -2
  175. package/skills/kobold-scan-skill/node_modules/brace-expansion/LICENSE +0 -21
  176. package/skills/kobold-scan-skill/node_modules/brace-expansion/README.md +0 -135
  177. package/skills/kobold-scan-skill/node_modules/brace-expansion/index.js +0 -203
  178. package/skills/kobold-scan-skill/node_modules/brace-expansion/package.json +0 -49
  179. package/skills/kobold-scan-skill/node_modules/chalk/index.d.ts +0 -415
  180. package/skills/kobold-scan-skill/node_modules/chalk/license +0 -9
  181. package/skills/kobold-scan-skill/node_modules/chalk/package.json +0 -68
  182. package/skills/kobold-scan-skill/node_modules/chalk/readme.md +0 -341
  183. package/skills/kobold-scan-skill/node_modules/chalk/source/index.js +0 -229
  184. package/skills/kobold-scan-skill/node_modules/chalk/source/templates.js +0 -134
  185. package/skills/kobold-scan-skill/node_modules/chalk/source/util.js +0 -39
  186. package/skills/kobold-scan-skill/node_modules/color-convert/CHANGELOG.md +0 -54
  187. package/skills/kobold-scan-skill/node_modules/color-convert/LICENSE +0 -21
  188. package/skills/kobold-scan-skill/node_modules/color-convert/README.md +0 -68
  189. package/skills/kobold-scan-skill/node_modules/color-convert/conversions.js +0 -839
  190. package/skills/kobold-scan-skill/node_modules/color-convert/index.js +0 -81
  191. package/skills/kobold-scan-skill/node_modules/color-convert/package.json +0 -48
  192. package/skills/kobold-scan-skill/node_modules/color-convert/route.js +0 -97
  193. package/skills/kobold-scan-skill/node_modules/color-name/LICENSE +0 -8
  194. package/skills/kobold-scan-skill/node_modules/color-name/README.md +0 -11
  195. package/skills/kobold-scan-skill/node_modules/color-name/index.js +0 -152
  196. package/skills/kobold-scan-skill/node_modules/color-name/package.json +0 -28
  197. package/skills/kobold-scan-skill/node_modules/commander/LICENSE +0 -22
  198. package/skills/kobold-scan-skill/node_modules/commander/Readme.md +0 -1129
  199. package/skills/kobold-scan-skill/node_modules/commander/esm.mjs +0 -16
  200. package/skills/kobold-scan-skill/node_modules/commander/index.js +0 -27
  201. package/skills/kobold-scan-skill/node_modules/commander/lib/argument.js +0 -147
  202. package/skills/kobold-scan-skill/node_modules/commander/lib/command.js +0 -2179
  203. package/skills/kobold-scan-skill/node_modules/commander/lib/error.js +0 -45
  204. package/skills/kobold-scan-skill/node_modules/commander/lib/help.js +0 -461
  205. package/skills/kobold-scan-skill/node_modules/commander/lib/option.js +0 -326
  206. package/skills/kobold-scan-skill/node_modules/commander/lib/suggestSimilar.js +0 -100
  207. package/skills/kobold-scan-skill/node_modules/commander/package-support.json +0 -16
  208. package/skills/kobold-scan-skill/node_modules/commander/package.json +0 -80
  209. package/skills/kobold-scan-skill/node_modules/commander/typings/index.d.ts +0 -891
  210. package/skills/kobold-scan-skill/node_modules/fs.realpath/LICENSE +0 -43
  211. package/skills/kobold-scan-skill/node_modules/fs.realpath/README.md +0 -33
  212. package/skills/kobold-scan-skill/node_modules/fs.realpath/index.js +0 -66
  213. package/skills/kobold-scan-skill/node_modules/fs.realpath/old.js +0 -303
  214. package/skills/kobold-scan-skill/node_modules/fs.realpath/package.json +0 -26
  215. package/skills/kobold-scan-skill/node_modules/glob/LICENSE +0 -15
  216. package/skills/kobold-scan-skill/node_modules/glob/README.md +0 -399
  217. package/skills/kobold-scan-skill/node_modules/glob/common.js +0 -244
  218. package/skills/kobold-scan-skill/node_modules/glob/glob.js +0 -790
  219. package/skills/kobold-scan-skill/node_modules/glob/package.json +0 -55
  220. package/skills/kobold-scan-skill/node_modules/glob/sync.js +0 -486
  221. package/skills/kobold-scan-skill/node_modules/has-flag/index.d.ts +0 -39
  222. package/skills/kobold-scan-skill/node_modules/has-flag/index.js +0 -8
  223. package/skills/kobold-scan-skill/node_modules/has-flag/license +0 -9
  224. package/skills/kobold-scan-skill/node_modules/has-flag/package.json +0 -46
  225. package/skills/kobold-scan-skill/node_modules/has-flag/readme.md +0 -89
  226. package/skills/kobold-scan-skill/node_modules/inflight/LICENSE +0 -15
  227. package/skills/kobold-scan-skill/node_modules/inflight/README.md +0 -37
  228. package/skills/kobold-scan-skill/node_modules/inflight/inflight.js +0 -54
  229. package/skills/kobold-scan-skill/node_modules/inflight/package.json +0 -29
  230. package/skills/kobold-scan-skill/node_modules/inherits/LICENSE +0 -16
  231. package/skills/kobold-scan-skill/node_modules/inherits/README.md +0 -42
  232. package/skills/kobold-scan-skill/node_modules/inherits/inherits.js +0 -9
  233. package/skills/kobold-scan-skill/node_modules/inherits/inherits_browser.js +0 -27
  234. package/skills/kobold-scan-skill/node_modules/inherits/package.json +0 -29
  235. package/skills/kobold-scan-skill/node_modules/minimatch/LICENSE +0 -15
  236. package/skills/kobold-scan-skill/node_modules/minimatch/README.md +0 -259
  237. package/skills/kobold-scan-skill/node_modules/minimatch/lib/path.js +0 -4
  238. package/skills/kobold-scan-skill/node_modules/minimatch/minimatch.js +0 -944
  239. package/skills/kobold-scan-skill/node_modules/minimatch/package.json +0 -35
  240. package/skills/kobold-scan-skill/node_modules/once/LICENSE +0 -15
  241. package/skills/kobold-scan-skill/node_modules/once/README.md +0 -79
  242. package/skills/kobold-scan-skill/node_modules/once/once.js +0 -42
  243. package/skills/kobold-scan-skill/node_modules/once/package.json +0 -33
  244. package/skills/kobold-scan-skill/node_modules/supports-color/browser.js +0 -5
  245. package/skills/kobold-scan-skill/node_modules/supports-color/index.js +0 -135
  246. package/skills/kobold-scan-skill/node_modules/supports-color/license +0 -9
  247. package/skills/kobold-scan-skill/node_modules/supports-color/package.json +0 -53
  248. package/skills/kobold-scan-skill/node_modules/supports-color/readme.md +0 -76
  249. package/skills/kobold-scan-skill/node_modules/wrappy/LICENSE +0 -15
  250. package/skills/kobold-scan-skill/node_modules/wrappy/README.md +0 -36
  251. package/skills/kobold-scan-skill/node_modules/wrappy/package.json +0 -29
  252. package/skills/kobold-scan-skill/node_modules/wrappy/wrappy.js +0 -33
@@ -1,30 +1,38 @@
1
1
  // 0xKobold Perennial Memory System
2
- // A digital familiar that remembers forever
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 = 1;
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 (handle first-run case where _metadata doesn't exist)
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, -- JSON array
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 -- 768 floats, stored as bytes
61
+ embedding BLOB
54
62
  );
55
-
56
- -- Full-text search index
57
- CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts
58
- USING fts5(id, content, content_rowid=rowid);
59
-
60
- -- Triggers to keep FTS in sync
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
- -- Schema version
83
- UPDATE _metadata SET value = '1' WHERE key = 'schema_version';
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); // Float32 = 4 bytes
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 - handles both Bun Blob/ArrayBuffer and Node Buffer
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)); // little-endian
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
- // TOOL: Save memory with embedding
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. Will be remembered forever and findable by meaning, not just exact words.",
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
- // Get embedding if Ollama available
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
- // Save to database
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: `🏛️ Memory archived: ${entry.id.slice(0, 8)}\n[${entry.category}] "${entry.content.slice(0, 50)}..."`,
420
+ text: `🏛️ Archived: ${entry.id.slice(0, 8)}\n[${entry.category}] "${entry.content.slice(0, 50)}..."${embedding ? " (semantic)" : ""}`,
206
421
  }],
207
- details: { entry, hasEmbedding: !!embedding },
422
+ details: {
423
+ entry,
424
+ hasEmbedding: !!embedding,
425
+ },
208
426
  };
209
427
  },
210
428
  });
211
- // TOOL: Hybrid search
429
+ // TOOL: Tiered memory search
212
430
  pi.registerTool({
213
431
  name: "perennial_search",
214
432
  label: "Search Perennial Memory",
215
- description: "Semantic search across all memories. Finds related concepts even with different wording.",
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 just keywords)" }),
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 category = params.category;
226
- const project = params.project;
444
+ const useTiered = params.useTiered ?? true;
227
445
  const results = [];
228
- // 1. Semantic search (if Ollama available)
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); // 30-day half-life
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
- // 2. Text search (always works)
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, ...(category ? [category] : []), ...(project ? [project] : []), limit);
519
+ `).all(query, limit);
278
520
  for (const row of textMatches) {
279
- // Check if already in results
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, // Base score for text match
532
+ score: 0.7,
297
533
  matchType: "text",
298
534
  });
299
535
  }
300
536
  }
301
- // Sort by score and take top results
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 a portable format (JSONL or SQLite)",
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
- // COMMANDS
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 forever: /remember \"Content\" --category fact --importance 0.9",
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 forever: ${entry.id.slice(0, 8)}${embedding ? " (semantic)" : ""}`, "info");
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 of what you remember\"", "error");
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, limit);
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 that description", "info");
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
- console.log("[Perennial Memory] Ready");
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