@cogmem/engram 0.2.0 → 0.3.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/README.md +16 -1
- package/SKILL.md +113 -0
- package/package.json +4 -2
- package/src/cli/commands/install.ts +113 -0
- package/src/cli/index.ts +2 -0
- package/src/cli/providers/claude.ts +89 -0
- package/src/cli/providers/index.ts +12 -0
- package/src/cli/providers/types.ts +16 -0
- package/src/core/associations.ts +25 -19
- package/src/core/consolidation.ts +2 -2
- package/src/core/recall.ts +6 -11
- package/src/storage/sqlite.ts +14 -0
package/README.md
CHANGED
|
@@ -19,6 +19,21 @@ bun install -g @cogmem/engram
|
|
|
19
19
|
engram --help
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
### Quick Setup for AI Editors
|
|
23
|
+
|
|
24
|
+
The `install` command sets up the skill file and MCP server config for your editor:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
engram install # interactive — prompts for provider + scope
|
|
28
|
+
engram install --provider claude --global # no prompts, installs to ~/.claude/
|
|
29
|
+
engram install --provider claude --project # installs to ./.claude/ in current dir
|
|
30
|
+
engram install --provider claude --global --dry-run # preview without writing files
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This installs two things:
|
|
34
|
+
1. **SKILL.md** — a cognitive protocol that teaches agents how to use engram effectively
|
|
35
|
+
2. **MCP config** — adds the engram server to your editor's MCP settings
|
|
36
|
+
|
|
22
37
|
## The Science
|
|
23
38
|
|
|
24
39
|
engram is built on memory research. Every design decision traces back to how the brain operates.
|
|
@@ -158,7 +173,7 @@ engram exposes its cognitive model as an MCP (Model Context Protocol) server, so
|
|
|
158
173
|
|
|
159
174
|
### Setup
|
|
160
175
|
|
|
161
|
-
|
|
176
|
+
The easiest way is `engram install` (see above). To configure manually, add to your MCP client configuration:
|
|
162
177
|
|
|
163
178
|
```json
|
|
164
179
|
{
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: engram
|
|
3
|
+
description: Cognitive memory for AI agents — use when encoding, recalling, or managing persistent memories across sessions
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Engram — Cognitive Memory Protocol
|
|
7
|
+
|
|
8
|
+
You have a biologically-inspired memory system. Use it like a brain, not a database.
|
|
9
|
+
|
|
10
|
+
## Session Lifecycle
|
|
11
|
+
|
|
12
|
+
**Start:** Recall what you know about the current context.
|
|
13
|
+
```
|
|
14
|
+
memory_recall → { action: "recall", cue: "<project or topic>" }
|
|
15
|
+
memory_manage → { action: "focus_get" }
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**During:** Encode insights as they emerge. Don't batch everything at the end.
|
|
19
|
+
```
|
|
20
|
+
memory_store → { action: "encode", content: "...", type: "...", emotion: "..." }
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**End:** Consolidate to strengthen and link memories.
|
|
24
|
+
```
|
|
25
|
+
memory_manage → { action: "consolidate" }
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Memory Types
|
|
29
|
+
|
|
30
|
+
| Type | Use when | Examples |
|
|
31
|
+
|------|----------|---------|
|
|
32
|
+
| `episodic` | Something *happened* — events, interactions, debugging sessions | "User reported login failing on Safari", "Deployed v2.3 with new caching" |
|
|
33
|
+
| `semantic` | A *fact* or *concept* — knowledge, definitions, relationships | "Auth uses JWT with 24h expiry", "The payments module depends on Stripe SDK" |
|
|
34
|
+
| `procedural` | A *skill* or *process* — how to do things, patterns, workflows | "To deploy: run tests → build → push to staging → verify → promote" |
|
|
35
|
+
|
|
36
|
+
**Default to `semantic`** when unsure. Procedural memories never decay — use them for durable skills.
|
|
37
|
+
|
|
38
|
+
## Emotion Tags
|
|
39
|
+
|
|
40
|
+
Tag memories with emotional context. This affects recall priority — emotional memories surface faster.
|
|
41
|
+
|
|
42
|
+
| Emotion | When to use |
|
|
43
|
+
|---------|-------------|
|
|
44
|
+
| `joy` | Something worked well, positive outcome, breakthrough |
|
|
45
|
+
| `satisfaction` | Task completed successfully, clean solution |
|
|
46
|
+
| `curiosity` | Interesting finding, worth exploring further |
|
|
47
|
+
| `surprise` | Unexpected behavior, counter-intuitive result |
|
|
48
|
+
| `anxiety` | Risk identified, potential failure, fragile code |
|
|
49
|
+
| `frustration` | Recurring problem, friction, workaround needed |
|
|
50
|
+
| `neutral` | Routine fact, no emotional significance |
|
|
51
|
+
|
|
52
|
+
Omit emotion for routine facts. Tag frustration on pain points — it helps surface them when they recur.
|
|
53
|
+
|
|
54
|
+
## MCP Tools Reference
|
|
55
|
+
|
|
56
|
+
### memory_store
|
|
57
|
+
| Action | Required | Optional |
|
|
58
|
+
|--------|----------|----------|
|
|
59
|
+
| `encode` | `content` | `type`, `emotion`, `emotionWeight` (0-1), `context` |
|
|
60
|
+
| `encode_batch` | `memories[]` (1-50) | each: `type`, `emotion`, `emotionWeight`, `context` |
|
|
61
|
+
| `reconsolidate` | `id` | `newContext`, `currentEmotion`, `currentEmotionWeight` |
|
|
62
|
+
|
|
63
|
+
### memory_recall
|
|
64
|
+
| Action | Required | Optional |
|
|
65
|
+
|--------|----------|----------|
|
|
66
|
+
| `recall` | `cue` | `limit`, `type`, `context`, `associative` (bool), `format` |
|
|
67
|
+
| `list` | — | `type`, `context`, `limit`, `offset`, `format` |
|
|
68
|
+
| `inspect` | `id` | — |
|
|
69
|
+
| `stats` | — | — |
|
|
70
|
+
|
|
71
|
+
### memory_manage
|
|
72
|
+
| Action | Required | Optional |
|
|
73
|
+
|--------|----------|----------|
|
|
74
|
+
| `consolidate` | — | — |
|
|
75
|
+
| `focus_push` | `content` | `memoryRef` |
|
|
76
|
+
| `focus_pop` | — | — |
|
|
77
|
+
| `focus_get` | — | — |
|
|
78
|
+
| `focus_clear` | — | — |
|
|
79
|
+
| `recall_to_focus` | `cue` | `limit`, `type`, `context` |
|
|
80
|
+
|
|
81
|
+
## Working Memory (Focus Buffer)
|
|
82
|
+
|
|
83
|
+
7 slots. Use it to hold active context during complex tasks.
|
|
84
|
+
|
|
85
|
+
- **Push** key facts you'll reference repeatedly during a task
|
|
86
|
+
- **Recall to focus** loads top recall results into the buffer
|
|
87
|
+
- **Pop/clear** when switching contexts
|
|
88
|
+
- The buffer is LIFO — newest items pop first
|
|
89
|
+
|
|
90
|
+
**Priming pattern:** At session start, recall + focus to seed your working context:
|
|
91
|
+
```
|
|
92
|
+
memory_manage → { action: "recall_to_focus", cue: "<current task>" }
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Key Behaviors
|
|
96
|
+
|
|
97
|
+
- **Recall strengthens memories** — each recall boosts activation (use-it-or-lose-it)
|
|
98
|
+
- **List does NOT strengthen** — use list for browsing without side effects
|
|
99
|
+
- **Procedural memories never decay** — once encoded, they persist permanently
|
|
100
|
+
- **Consolidation discovers associations** — run it to link related memories
|
|
101
|
+
- **Emotional memories resist decay** — tagged memories survive longer
|
|
102
|
+
- **Context scopes memories** — use `context: "project:name"` to partition
|
|
103
|
+
|
|
104
|
+
## What to Encode
|
|
105
|
+
|
|
106
|
+
**Encode:** decisions and their rationale, architectural insights, debugging breakthroughs, user preferences, recurring patterns, project-specific knowledge, lessons learned
|
|
107
|
+
|
|
108
|
+
**Don't encode:** transient task state, information already in code/docs, obvious facts, raw data without interpretation
|
|
109
|
+
|
|
110
|
+
## Context Convention
|
|
111
|
+
|
|
112
|
+
Use hierarchical context tags: `project:engram`, `project:acme/auth`, `topic:deployment`.
|
|
113
|
+
This lets you recall scoped to a project or topic without noise from other domains.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cogmem/engram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Human memory for artificial minds — a cognitive memory system modeled on neuroscience",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
20
|
"src/",
|
|
21
|
-
"drizzle/"
|
|
21
|
+
"drizzle/",
|
|
22
|
+
"SKILL.md"
|
|
22
23
|
],
|
|
23
24
|
"scripts": {
|
|
24
25
|
"start": "bun run src/cli/index.ts",
|
|
@@ -34,6 +35,7 @@
|
|
|
34
35
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
35
36
|
"citty": "^0.2.1",
|
|
36
37
|
"cli-table3": "^0.6.5",
|
|
38
|
+
"consola": "^3.4.2",
|
|
37
39
|
"dayjs": "^1.11.19",
|
|
38
40
|
"drizzle-orm": "^0.45.1",
|
|
39
41
|
"kleur": "^4.1.5",
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { consola } from "consola";
|
|
6
|
+
import { getProvider, availableProviders } from "../providers/index.ts";
|
|
7
|
+
import { green, dim, yellow, bold } from "../format.ts";
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const SKILL_PATH = join(__dirname, "..", "..", "..", "SKILL.md");
|
|
11
|
+
|
|
12
|
+
function loadSkillContent(): string {
|
|
13
|
+
return readFileSync(SKILL_PATH, "utf-8");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const installCommand = defineCommand({
|
|
17
|
+
meta: {
|
|
18
|
+
name: "install",
|
|
19
|
+
description: "Install engram skill + MCP config for your AI editor",
|
|
20
|
+
},
|
|
21
|
+
args: {
|
|
22
|
+
provider: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "Provider to install for (claude)",
|
|
25
|
+
alias: "p",
|
|
26
|
+
},
|
|
27
|
+
global: {
|
|
28
|
+
type: "boolean",
|
|
29
|
+
description: "Install globally (~/.claude/)",
|
|
30
|
+
alias: "g",
|
|
31
|
+
},
|
|
32
|
+
project: {
|
|
33
|
+
type: "boolean",
|
|
34
|
+
description: "Install to current project directory",
|
|
35
|
+
},
|
|
36
|
+
dryRun: {
|
|
37
|
+
type: "boolean",
|
|
38
|
+
description: "Show what would be installed without writing files",
|
|
39
|
+
alias: "n",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
async run({ args }) {
|
|
43
|
+
const dryRun = args.dryRun ?? false;
|
|
44
|
+
|
|
45
|
+
let providerName = args.provider;
|
|
46
|
+
if (!providerName) {
|
|
47
|
+
const choices = availableProviders.map((p) => ({
|
|
48
|
+
label: p.available ? p.displayName : `${p.displayName} (coming soon)`,
|
|
49
|
+
value: p.name,
|
|
50
|
+
disabled: !p.available,
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
providerName = (await consola.prompt("Select a provider", {
|
|
54
|
+
type: "select",
|
|
55
|
+
options: choices,
|
|
56
|
+
})) as unknown as string;
|
|
57
|
+
|
|
58
|
+
if (typeof providerName === "symbol") process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const provider = getProvider(providerName);
|
|
62
|
+
if (!provider) {
|
|
63
|
+
consola.error(`Unknown provider: ${providerName}`);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
if (!provider.available) {
|
|
67
|
+
consola.error(`${provider.displayName} is not yet supported`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let scope: "global" | "project" | undefined;
|
|
72
|
+
if (args.global) scope = "global";
|
|
73
|
+
else if (args.project) scope = "project";
|
|
74
|
+
|
|
75
|
+
if (!scope) {
|
|
76
|
+
scope = (await consola.prompt("Install scope", {
|
|
77
|
+
type: "select",
|
|
78
|
+
options: [
|
|
79
|
+
{ label: `Global (~/.claude/)`, value: "global" },
|
|
80
|
+
{ label: `Project (./.claude/)`, value: "project" },
|
|
81
|
+
],
|
|
82
|
+
})) as unknown as "global" | "project";
|
|
83
|
+
|
|
84
|
+
if (typeof scope === "symbol") process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const skillContent = loadSkillContent();
|
|
88
|
+
|
|
89
|
+
if (dryRun) console.log(yellow("\n dry run — no files will be written\n"));
|
|
90
|
+
|
|
91
|
+
const result =
|
|
92
|
+
scope === "global"
|
|
93
|
+
? await provider.installGlobal(skillContent, dryRun)
|
|
94
|
+
: await provider.installProject(skillContent, process.cwd(), dryRun);
|
|
95
|
+
|
|
96
|
+
if (result.status === "already_installed") {
|
|
97
|
+
console.log(dim(" already installed — nothing to do"));
|
|
98
|
+
console.log(dim(` skill: ${result.skillPath}`));
|
|
99
|
+
if (result.mcpConfigPath) console.log(dim(` mcp: ${result.mcpConfigPath}`));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const prefix = dryRun ? "would install" : "installed";
|
|
104
|
+
const check = dryRun ? yellow("~") : green("\u2713");
|
|
105
|
+
|
|
106
|
+
console.log(` ${check} Skill ${prefix} ${dim("\u2192")} ${bold(result.skillPath)}`);
|
|
107
|
+
if (result.mcpConfigured && result.mcpConfigPath) {
|
|
108
|
+
console.log(` ${check} MCP ${prefix} ${dim("\u2192")} ${bold(result.mcpConfigPath)}`);
|
|
109
|
+
} else if (result.mcpConfigPath) {
|
|
110
|
+
console.log(dim(` - MCP already configured → ${result.mcpConfigPath}`));
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
});
|
package/src/cli/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { statsCommand } from "./commands/stats.ts";
|
|
|
9
9
|
import { listCommand } from "./commands/list.ts";
|
|
10
10
|
import { sleepCommand } from "./commands/sleep.ts";
|
|
11
11
|
import { healthCommand } from "./commands/health.ts";
|
|
12
|
+
import { installCommand } from "./commands/install.ts";
|
|
12
13
|
|
|
13
14
|
const main = defineCommand({
|
|
14
15
|
meta: {
|
|
@@ -25,6 +26,7 @@ const main = defineCommand({
|
|
|
25
26
|
stats: statsCommand,
|
|
26
27
|
sleep: sleepCommand,
|
|
27
28
|
health: healthCommand,
|
|
29
|
+
install: installCommand,
|
|
28
30
|
},
|
|
29
31
|
});
|
|
30
32
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import type { ProviderInstaller } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
const MCP_SERVER_CONFIG = {
|
|
7
|
+
command: "bunx",
|
|
8
|
+
args: ["-p", "@cogmem/engram", "engram-mcp"],
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function ensureDir(dir: string) {
|
|
12
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readJsonFile(path: string): Record<string, unknown> {
|
|
16
|
+
if (!existsSync(path)) return {};
|
|
17
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function writeJsonFile(path: string, data: Record<string, unknown>) {
|
|
21
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function installSkill(skillDir: string, skillContent: string, dryRun: boolean): boolean {
|
|
25
|
+
const skillPath = join(skillDir, "SKILL.md");
|
|
26
|
+
if (existsSync(skillPath)) {
|
|
27
|
+
const existing = readFileSync(skillPath, "utf-8");
|
|
28
|
+
if (existing === skillContent) return false;
|
|
29
|
+
}
|
|
30
|
+
if (!dryRun) {
|
|
31
|
+
ensureDir(skillDir);
|
|
32
|
+
writeFileSync(skillPath, skillContent);
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function configureMcp(configPath: string, dryRun: boolean): boolean {
|
|
38
|
+
const config = readJsonFile(configPath);
|
|
39
|
+
const servers = (config.mcpServers ?? {}) as Record<string, unknown>;
|
|
40
|
+
if (servers.engram) return false;
|
|
41
|
+
if (!dryRun) {
|
|
42
|
+
servers.engram = MCP_SERVER_CONFIG;
|
|
43
|
+
config.mcpServers = servers;
|
|
44
|
+
ensureDir(join(configPath, ".."));
|
|
45
|
+
writeJsonFile(configPath, config);
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const claudeProvider: ProviderInstaller = {
|
|
51
|
+
name: "claude",
|
|
52
|
+
displayName: "Claude Code",
|
|
53
|
+
available: true,
|
|
54
|
+
|
|
55
|
+
async installGlobal(skillContent, dryRun) {
|
|
56
|
+
const home = homedir();
|
|
57
|
+
const skillDir = join(home, ".claude", "skills", "engram");
|
|
58
|
+
const configPath = join(home, ".claude", "settings.json");
|
|
59
|
+
|
|
60
|
+
const skillInstalled = installSkill(skillDir, skillContent, dryRun);
|
|
61
|
+
const mcpInstalled = configureMcp(configPath, dryRun);
|
|
62
|
+
|
|
63
|
+
const status = !skillInstalled && !mcpInstalled ? "already_installed" : skillInstalled && mcpInstalled ? "installed" : "updated";
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
status,
|
|
67
|
+
skillPath: join(skillDir, "SKILL.md"),
|
|
68
|
+
mcpConfigured: mcpInstalled,
|
|
69
|
+
mcpConfigPath: configPath,
|
|
70
|
+
};
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
async installProject(skillContent, projectDir, dryRun) {
|
|
74
|
+
const skillDir = join(projectDir, ".claude", "skills", "engram");
|
|
75
|
+
const configPath = join(projectDir, ".claude", "settings.local.json");
|
|
76
|
+
|
|
77
|
+
const skillInstalled = installSkill(skillDir, skillContent, dryRun);
|
|
78
|
+
const mcpInstalled = configureMcp(configPath, dryRun);
|
|
79
|
+
|
|
80
|
+
const status = !skillInstalled && !mcpInstalled ? "already_installed" : skillInstalled && mcpInstalled ? "installed" : "updated";
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
status,
|
|
84
|
+
skillPath: join(skillDir, "SKILL.md"),
|
|
85
|
+
mcpConfigured: mcpInstalled,
|
|
86
|
+
mcpConfigPath: configPath,
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { claudeProvider } from "./claude.ts";
|
|
2
|
+
import type { ProviderInstaller } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
const providers: Record<string, ProviderInstaller> = {
|
|
5
|
+
claude: claudeProvider,
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function getProvider(name: string): ProviderInstaller | undefined {
|
|
9
|
+
return providers[name];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const availableProviders = Object.values(providers);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type InstallStatus = "installed" | "already_installed" | "updated";
|
|
2
|
+
|
|
3
|
+
export interface InstallResult {
|
|
4
|
+
status: InstallStatus;
|
|
5
|
+
skillPath: string;
|
|
6
|
+
mcpConfigured: boolean;
|
|
7
|
+
mcpConfigPath: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ProviderInstaller {
|
|
11
|
+
name: string;
|
|
12
|
+
displayName: string;
|
|
13
|
+
available: boolean;
|
|
14
|
+
installGlobal(skillContent: string, dryRun: boolean): Promise<InstallResult>;
|
|
15
|
+
installProject(skillContent: string, projectDir: string, dryRun: boolean): Promise<InstallResult>;
|
|
16
|
+
}
|
package/src/core/associations.ts
CHANGED
|
@@ -113,28 +113,30 @@ export function formSemanticAssociations(
|
|
|
113
113
|
storage: EngramStorage,
|
|
114
114
|
memory: Memory,
|
|
115
115
|
now?: number,
|
|
116
|
+
preloadedMemories?: Memory[],
|
|
116
117
|
): Association[] {
|
|
117
118
|
const currentTime = now ?? Date.now();
|
|
118
119
|
const keywords = extractKeywords(memory.content);
|
|
119
120
|
if (keywords.length === 0) return [];
|
|
120
121
|
|
|
121
|
-
const
|
|
122
|
+
const candidates = preloadedMemories ?? storage.getAllMemories();
|
|
123
|
+
const existing = storage.getAssociations(memory.id);
|
|
124
|
+
const linkedSet = new Set(
|
|
125
|
+
existing.flatMap((a) => [
|
|
126
|
+
`${a.sourceId}:${a.targetId}`,
|
|
127
|
+
`${a.targetId}:${a.sourceId}`,
|
|
128
|
+
]),
|
|
129
|
+
);
|
|
122
130
|
const formed: Association[] = [];
|
|
123
131
|
|
|
124
|
-
for (const other of
|
|
132
|
+
for (const other of candidates) {
|
|
125
133
|
if (other.id === memory.id) continue;
|
|
126
134
|
|
|
127
135
|
const otherKeywords = extractKeywords(other.content);
|
|
128
136
|
const overlap = keywords.filter((k) => otherKeywords.includes(k));
|
|
129
137
|
|
|
130
138
|
if (overlap.length > 0) {
|
|
131
|
-
|
|
132
|
-
const alreadyLinked = existing.some(
|
|
133
|
-
(a) =>
|
|
134
|
-
(a.sourceId === memory.id && a.targetId === other.id) ||
|
|
135
|
-
(a.sourceId === other.id && a.targetId === memory.id),
|
|
136
|
-
);
|
|
137
|
-
if (alreadyLinked) continue;
|
|
139
|
+
if (linkedSet.has(`${memory.id}:${other.id}`)) continue;
|
|
138
140
|
|
|
139
141
|
const strength = overlap.length / Math.max(keywords.length, otherKeywords.length);
|
|
140
142
|
const assoc = formAssociation(
|
|
@@ -146,6 +148,8 @@ export function formSemanticAssociations(
|
|
|
146
148
|
currentTime,
|
|
147
149
|
);
|
|
148
150
|
formed.push(assoc);
|
|
151
|
+
linkedSet.add(`${memory.id}:${other.id}`);
|
|
152
|
+
linkedSet.add(`${other.id}:${memory.id}`);
|
|
149
153
|
}
|
|
150
154
|
}
|
|
151
155
|
|
|
@@ -156,26 +160,26 @@ export function formEmotionalAssociations(
|
|
|
156
160
|
storage: EngramStorage,
|
|
157
161
|
memory: Memory,
|
|
158
162
|
now?: number,
|
|
163
|
+
preloadedMemories?: Memory[],
|
|
159
164
|
): Association[] {
|
|
160
165
|
if (memory.emotion === "neutral" || memory.emotionWeight <= 0.3) return [];
|
|
161
166
|
|
|
162
167
|
const currentTime = now ?? Date.now();
|
|
163
|
-
const
|
|
168
|
+
const candidates = preloadedMemories ?? storage.getAllMemories();
|
|
169
|
+
const existing = storage.getAssociations(memory.id);
|
|
170
|
+
const emotionalLinks = new Set(
|
|
171
|
+
existing
|
|
172
|
+
.filter((a) => a.type === "emotional")
|
|
173
|
+
.flatMap((a) => [`${a.sourceId}:${a.targetId}`, `${a.targetId}:${a.sourceId}`]),
|
|
174
|
+
);
|
|
164
175
|
const formed: Association[] = [];
|
|
165
176
|
const memoryTier = AROUSAL_TIERS[memory.emotion];
|
|
166
177
|
|
|
167
|
-
for (const other of
|
|
178
|
+
for (const other of candidates) {
|
|
168
179
|
if (other.id === memory.id) continue;
|
|
169
180
|
if (other.emotion === "neutral" || other.emotionWeight <= 0.3) continue;
|
|
170
181
|
|
|
171
|
-
|
|
172
|
-
const alreadyLinked = existing.some(
|
|
173
|
-
(a) =>
|
|
174
|
-
a.type === "emotional" &&
|
|
175
|
-
((a.sourceId === memory.id && a.targetId === other.id) ||
|
|
176
|
-
(a.sourceId === other.id && a.targetId === memory.id)),
|
|
177
|
-
);
|
|
178
|
-
if (alreadyLinked) continue;
|
|
182
|
+
if (emotionalLinks.has(`${memory.id}:${other.id}`)) continue;
|
|
179
183
|
|
|
180
184
|
let strength: number;
|
|
181
185
|
if (memory.emotion === other.emotion) {
|
|
@@ -190,6 +194,8 @@ export function formEmotionalAssociations(
|
|
|
190
194
|
|
|
191
195
|
const assoc = formAssociation(storage, memory.id, other.id, "emotional", strength, currentTime);
|
|
192
196
|
formed.push(assoc);
|
|
197
|
+
emotionalLinks.add(`${memory.id}:${other.id}`);
|
|
198
|
+
emotionalLinks.add(`${other.id}:${memory.id}`);
|
|
193
199
|
}
|
|
194
200
|
|
|
195
201
|
return formed;
|
|
@@ -69,8 +69,8 @@ export function consolidate(
|
|
|
69
69
|
const remainingMemories = storage.getAllMemories();
|
|
70
70
|
for (const memory of remainingMemories) {
|
|
71
71
|
const temporalAssocs = formTemporalAssociations(storage, memory, config, currentTime);
|
|
72
|
-
const semanticAssocs = formSemanticAssociations(storage, memory, currentTime);
|
|
73
|
-
const emotionalAssocs = formEmotionalAssociations(storage, memory, currentTime);
|
|
72
|
+
const semanticAssocs = formSemanticAssociations(storage, memory, currentTime, remainingMemories);
|
|
73
|
+
const emotionalAssocs = formEmotionalAssociations(storage, memory, currentTime, remainingMemories);
|
|
74
74
|
const causalAssocs = formCausalAssociations(storage, memory, config, currentTime);
|
|
75
75
|
|
|
76
76
|
for (const assoc of [...temporalAssocs, ...semanticAssocs, ...emotionalAssocs, ...causalAssocs]) {
|
package/src/core/recall.ts
CHANGED
|
@@ -35,17 +35,12 @@ export function recall(
|
|
|
35
35
|
for (const m of contextMatches) seedIds.add(m.id);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
filtered = filtered.filter((m) => m.context?.startsWith(options.context!));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const sorted = filtered.sort((a, b) => b.activation - a.activation);
|
|
48
|
-
for (const m of sorted.slice(0, limit)) {
|
|
38
|
+
const topByActivation = storage.getTopMemoriesByActivation(
|
|
39
|
+
limit,
|
|
40
|
+
options?.type,
|
|
41
|
+
options?.context,
|
|
42
|
+
);
|
|
43
|
+
for (const m of topByActivation) {
|
|
49
44
|
seedIds.add(m.id);
|
|
50
45
|
}
|
|
51
46
|
|
package/src/storage/sqlite.ts
CHANGED
|
@@ -153,6 +153,20 @@ export class EngramStorage {
|
|
|
153
153
|
.all();
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
getTopMemoriesByActivation(limit: number, type?: MemoryType, contextPrefix?: string): Memory[] {
|
|
157
|
+
const conditions = [];
|
|
158
|
+
if (type) conditions.push(eq(memories.type, type));
|
|
159
|
+
if (contextPrefix) conditions.push(sql`${memories.context} LIKE ${contextPrefix + "%"}`);
|
|
160
|
+
|
|
161
|
+
return this.db
|
|
162
|
+
.select()
|
|
163
|
+
.from(memories)
|
|
164
|
+
.where(conditions.length ? and(...conditions) : undefined)
|
|
165
|
+
.orderBy(desc(memories.activation))
|
|
166
|
+
.limit(limit)
|
|
167
|
+
.all();
|
|
168
|
+
}
|
|
169
|
+
|
|
156
170
|
getMemoriesBelowActivation(threshold: number): Memory[] {
|
|
157
171
|
return this.db
|
|
158
172
|
.select()
|