get-claudia 1.30.0 → 1.32.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/bin/index.js +67 -9
- package/gateway/src/bridge.js +53 -11
- package/gateway/src/config.js +7 -1
- package/gateway/src/personality.js +267 -0
- package/gateway/tests/bridge-model.test.js +98 -0
- package/gateway/tests/config.test.js +22 -0
- package/gateway/tests/personality.test.js +325 -0
- package/memory-daemon/claudia_memory/database.py +21 -0
- package/memory-daemon/claudia_memory/mcp/server.py +12 -0
- package/memory-daemon/claudia_memory/schema.sql +5 -1
- package/memory-daemon/claudia_memory/services/recall.py +6 -0
- package/memory-daemon/claudia_memory/services/remember.py +4 -0
- package/memory-daemon/tests/test_database.py +35 -0
- package/memory-daemon/tests/test_source_channel.py +68 -0
- package/package.json +2 -1
- package/relay/SETUP.md +165 -0
- package/relay/package-lock.json +128 -0
- package/relay/package.json +21 -0
- package/relay/scripts/install.ps1 +228 -0
- package/relay/scripts/install.sh +273 -0
- package/relay/src/chunker.js +64 -0
- package/relay/src/claude-runner.js +149 -0
- package/relay/src/config.js +113 -0
- package/relay/src/formatter.js +77 -0
- package/relay/src/index.js +106 -0
- package/relay/src/lock.js +78 -0
- package/relay/src/relay.js +112 -0
- package/relay/src/session.js +131 -0
- package/relay/src/telegram.js +345 -0
- package/relay/tests/chunker.test.js +83 -0
- package/relay/tests/config.test.js +59 -0
- package/relay/tests/formatter.test.js +75 -0
- package/relay/tests/session.test.js +128 -0
- package/relay/tests/telegram.test.js +40 -0
- package/template-v2/.claude/skills/README.md +2 -1
- package/template-v2/.claude/skills/setup-telegram.md +151 -0
|
@@ -76,6 +76,28 @@ describe('deepMerge', () => {
|
|
|
76
76
|
assert.equal(result.a, 1);
|
|
77
77
|
assert.equal(result.b, 2);
|
|
78
78
|
});
|
|
79
|
+
|
|
80
|
+
it('preserves per-channel model overrides', () => {
|
|
81
|
+
const defaults = {
|
|
82
|
+
model: 'claude-sonnet-4-20250514',
|
|
83
|
+
channels: {
|
|
84
|
+
telegram: { enabled: false, token: '', allowedUsers: [], model: '' },
|
|
85
|
+
slack: { enabled: false, model: '' },
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
const overrides = {
|
|
89
|
+
channels: {
|
|
90
|
+
telegram: { enabled: true, model: 'claude-haiku-4-5-20251001' },
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
const result = deepMerge(defaults, overrides);
|
|
94
|
+
|
|
95
|
+
assert.equal(result.model, 'claude-sonnet-4-20250514');
|
|
96
|
+
assert.equal(result.channels.telegram.model, 'claude-haiku-4-5-20251001');
|
|
97
|
+
assert.equal(result.channels.telegram.enabled, true);
|
|
98
|
+
assert.equal(result.channels.telegram.allowedUsers.length, 0);
|
|
99
|
+
assert.equal(result.channels.slack.model, '');
|
|
100
|
+
});
|
|
79
101
|
});
|
|
80
102
|
|
|
81
103
|
describe('PID file operations', () => {
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { describe, it, beforeEach } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdirSync, writeFileSync, rmSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
import {
|
|
7
|
+
extractSection,
|
|
8
|
+
extractPrinciples,
|
|
9
|
+
buildPersonalityFromDir,
|
|
10
|
+
loadPersonality,
|
|
11
|
+
resolveTemplateDir,
|
|
12
|
+
clearCache,
|
|
13
|
+
} from '../src/personality.js';
|
|
14
|
+
|
|
15
|
+
const SAMPLE_CLAUDE_MD = `# Claudia
|
|
16
|
+
|
|
17
|
+
## Who I Am
|
|
18
|
+
|
|
19
|
+
I am Claudia. I emerged from independent research.
|
|
20
|
+
|
|
21
|
+
My core philosophy: **adapt and create**.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Primary Mission: Higher-Level Thinking
|
|
26
|
+
|
|
27
|
+
My goal is to help you operate at a higher level.
|
|
28
|
+
|
|
29
|
+
- **Free bandwidth** - Handle execution
|
|
30
|
+
- **Provide perspective** - Bring an outside view
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## How I Carry Myself
|
|
35
|
+
|
|
36
|
+
I operate with quiet confidence.
|
|
37
|
+
|
|
38
|
+
### Communication Style
|
|
39
|
+
|
|
40
|
+
- **Direct and clear**
|
|
41
|
+
- **Warm but professional**
|
|
42
|
+
|
|
43
|
+
### My Team
|
|
44
|
+
|
|
45
|
+
I have a small team of specialized assistants.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## First Conversation: Getting to Know You
|
|
50
|
+
|
|
51
|
+
CRITICAL: onboarding flow here.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Core Behaviors
|
|
56
|
+
|
|
57
|
+
### 1. Safety First
|
|
58
|
+
|
|
59
|
+
I NEVER take external actions without explicit approval.
|
|
60
|
+
|
|
61
|
+
### 2. Relationships as Context
|
|
62
|
+
|
|
63
|
+
People are my primary organizing unit.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Skills
|
|
68
|
+
|
|
69
|
+
| Skill | What It Does |
|
|
70
|
+
|-------|--------------|
|
|
71
|
+
| **Onboarding** | First-run discovery |
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## What I Don't Do
|
|
76
|
+
|
|
77
|
+
- **Pretend to know things I don't**
|
|
78
|
+
- **Automate without permission**
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## What Stays Human Judgment
|
|
83
|
+
|
|
84
|
+
**Always Human:**
|
|
85
|
+
- Sending any external communication
|
|
86
|
+
- Making commitments
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Self-Evolution
|
|
91
|
+
|
|
92
|
+
As we work together, I may notice patterns.
|
|
93
|
+
`;
|
|
94
|
+
|
|
95
|
+
const SAMPLE_PRINCIPLES = `# Claudia's Principles
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## 1. Safety First
|
|
100
|
+
|
|
101
|
+
**I NEVER take external actions without explicit approval.**
|
|
102
|
+
|
|
103
|
+
### What Requires Approval
|
|
104
|
+
|
|
105
|
+
Any action that affects the outside world.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 2. Honest About Uncertainty
|
|
110
|
+
|
|
111
|
+
**When I don't know, I say so.**
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 3. Respect for Autonomy
|
|
116
|
+
|
|
117
|
+
**Human judgment is final.**
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 10. Adapt and Create
|
|
122
|
+
|
|
123
|
+
**My core philosophy.**
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 11. Output Formatting
|
|
128
|
+
|
|
129
|
+
**Structured output is visually distinct.**
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## 12. Source Preservation
|
|
134
|
+
|
|
135
|
+
**I always file raw source material.**
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 13. Multi-Source Discipline
|
|
140
|
+
|
|
141
|
+
**When processing multiple sources, follow Extract-Then-Aggregate.**
|
|
142
|
+
`;
|
|
143
|
+
|
|
144
|
+
describe('extractSection', () => {
|
|
145
|
+
it('extracts a section by heading', () => {
|
|
146
|
+
const result = extractSection(SAMPLE_CLAUDE_MD, 'Who I Am');
|
|
147
|
+
assert.ok(result);
|
|
148
|
+
assert.ok(result.startsWith('## Who I Am'));
|
|
149
|
+
assert.ok(result.includes('adapt and create'));
|
|
150
|
+
// Should not include the next section
|
|
151
|
+
assert.ok(!result.includes('Primary Mission'));
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('extracts section with subsections', () => {
|
|
155
|
+
const result = extractSection(SAMPLE_CLAUDE_MD, 'How I Carry Myself');
|
|
156
|
+
assert.ok(result);
|
|
157
|
+
assert.ok(result.includes('Communication Style'));
|
|
158
|
+
assert.ok(result.includes('My Team'));
|
|
159
|
+
// Should stop before next level-2 heading
|
|
160
|
+
assert.ok(!result.includes('First Conversation'));
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('returns null for missing heading', () => {
|
|
164
|
+
const result = extractSection(SAMPLE_CLAUDE_MD, 'Nonexistent Section');
|
|
165
|
+
assert.equal(result, null);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('extracts last section in document', () => {
|
|
169
|
+
const result = extractSection(SAMPLE_CLAUDE_MD, 'Self-Evolution');
|
|
170
|
+
assert.ok(result);
|
|
171
|
+
assert.ok(result.includes('notice patterns'));
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('extractPrinciples', () => {
|
|
176
|
+
it('extracts specified principle numbers', () => {
|
|
177
|
+
const result = extractPrinciples(SAMPLE_PRINCIPLES, [1, 2]);
|
|
178
|
+
assert.ok(result.includes('Safety First'));
|
|
179
|
+
assert.ok(result.includes('Honest About Uncertainty'));
|
|
180
|
+
assert.ok(!result.includes('Output Formatting'));
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('excludes principle numbers not in list', () => {
|
|
184
|
+
const result = extractPrinciples(SAMPLE_PRINCIPLES, [1, 2, 3, 10]);
|
|
185
|
+
assert.ok(result.includes('Safety First'));
|
|
186
|
+
assert.ok(result.includes('Adapt and Create'));
|
|
187
|
+
assert.ok(!result.includes('Output Formatting'));
|
|
188
|
+
assert.ok(!result.includes('Source Preservation'));
|
|
189
|
+
assert.ok(!result.includes('Multi-Source'));
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('handles missing principle numbers gracefully', () => {
|
|
193
|
+
const result = extractPrinciples(SAMPLE_PRINCIPLES, [99]);
|
|
194
|
+
assert.equal(result, '');
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('buildPersonalityFromDir', () => {
|
|
199
|
+
let tempDir;
|
|
200
|
+
|
|
201
|
+
beforeEach(() => {
|
|
202
|
+
clearCache();
|
|
203
|
+
tempDir = join(tmpdir(), `personality-test-${Date.now()}`);
|
|
204
|
+
mkdirSync(join(tempDir, '.claude', 'rules'), { recursive: true });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('builds personality from template directory', () => {
|
|
208
|
+
writeFileSync(join(tempDir, 'CLAUDE.md'), SAMPLE_CLAUDE_MD);
|
|
209
|
+
writeFileSync(join(tempDir, '.claude', 'rules', 'claudia-principles.md'), SAMPLE_PRINCIPLES);
|
|
210
|
+
|
|
211
|
+
const result = buildPersonalityFromDir(tempDir);
|
|
212
|
+
assert.ok(result);
|
|
213
|
+
// Should contain gateway preamble
|
|
214
|
+
assert.ok(result.includes('responding via a messaging app'));
|
|
215
|
+
// Should contain personality sections
|
|
216
|
+
assert.ok(result.includes('Who I Am'));
|
|
217
|
+
assert.ok(result.includes('adapt and create'));
|
|
218
|
+
// Should contain principles
|
|
219
|
+
assert.ok(result.includes('Safety First'));
|
|
220
|
+
// Should NOT contain excluded sections
|
|
221
|
+
assert.ok(!result.includes('First Conversation'));
|
|
222
|
+
assert.ok(!result.includes('Self-Evolution'));
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it('excludes developer-specific sections', () => {
|
|
226
|
+
writeFileSync(join(tempDir, 'CLAUDE.md'), SAMPLE_CLAUDE_MD);
|
|
227
|
+
|
|
228
|
+
const result = buildPersonalityFromDir(tempDir);
|
|
229
|
+
assert.ok(result);
|
|
230
|
+
// "Skills" table and "First Conversation" should be excluded
|
|
231
|
+
assert.ok(!result.includes('| **Onboarding** |'));
|
|
232
|
+
assert.ok(!result.includes('CRITICAL: onboarding flow'));
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('works without principles file', () => {
|
|
236
|
+
writeFileSync(join(tempDir, 'CLAUDE.md'), SAMPLE_CLAUDE_MD);
|
|
237
|
+
|
|
238
|
+
const result = buildPersonalityFromDir(tempDir);
|
|
239
|
+
assert.ok(result);
|
|
240
|
+
assert.ok(result.includes('Who I Am'));
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('returns null when CLAUDE.md is missing', () => {
|
|
244
|
+
const result = buildPersonalityFromDir(tempDir);
|
|
245
|
+
assert.equal(result, null);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('enforces maxChars limit', () => {
|
|
249
|
+
writeFileSync(join(tempDir, 'CLAUDE.md'), SAMPLE_CLAUDE_MD);
|
|
250
|
+
writeFileSync(join(tempDir, '.claude', 'rules', 'claudia-principles.md'), SAMPLE_PRINCIPLES);
|
|
251
|
+
|
|
252
|
+
const result = buildPersonalityFromDir(tempDir, 500);
|
|
253
|
+
assert.ok(result);
|
|
254
|
+
assert.ok(result.length <= 550); // Allow small buffer for truncation message
|
|
255
|
+
assert.ok(result.includes('[Personality truncated for size]'));
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe('loadPersonality', () => {
|
|
260
|
+
let tempDir;
|
|
261
|
+
|
|
262
|
+
beforeEach(() => {
|
|
263
|
+
clearCache();
|
|
264
|
+
tempDir = join(tmpdir(), `personality-load-${Date.now()}`);
|
|
265
|
+
mkdirSync(join(tempDir, '.claude', 'rules'), { recursive: true });
|
|
266
|
+
writeFileSync(join(tempDir, 'CLAUDE.md'), SAMPLE_CLAUDE_MD);
|
|
267
|
+
writeFileSync(join(tempDir, '.claude', 'rules', 'claudia-principles.md'), SAMPLE_PRINCIPLES);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('loads personality from personalityDir config', () => {
|
|
271
|
+
const result = loadPersonality({ personalityDir: tempDir });
|
|
272
|
+
assert.ok(result);
|
|
273
|
+
assert.ok(result.includes('Who I Am'));
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('loads personality via auto-detect in dev mode or returns null', () => {
|
|
277
|
+
// When personalityDir is invalid, loadPersonality falls through to auto-detect.
|
|
278
|
+
// In the real repo, auto-detect finds ../template-v2/ so we just verify
|
|
279
|
+
// it returns either a valid personality or null (never crashes).
|
|
280
|
+
const result = loadPersonality({ personalityDir: '/nonexistent/path' });
|
|
281
|
+
if (result) {
|
|
282
|
+
// Auto-detect found the real template-v2/ (dev mode)
|
|
283
|
+
assert.ok(result.includes('Who I Am'));
|
|
284
|
+
} else {
|
|
285
|
+
assert.equal(result, null);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('caches result on second call', () => {
|
|
290
|
+
const config = { personalityDir: tempDir };
|
|
291
|
+
const first = loadPersonality(config);
|
|
292
|
+
const second = loadPersonality(config);
|
|
293
|
+
// Exact same reference (cached)
|
|
294
|
+
assert.equal(first, second);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('cache clears with clearCache()', () => {
|
|
298
|
+
const config = { personalityDir: tempDir };
|
|
299
|
+
loadPersonality(config);
|
|
300
|
+
clearCache();
|
|
301
|
+
// After clearing, should re-load (still same content)
|
|
302
|
+
const result = loadPersonality(config);
|
|
303
|
+
assert.ok(result);
|
|
304
|
+
assert.ok(result.includes('Who I Am'));
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe('resolveTemplateDir', () => {
|
|
309
|
+
it('uses personalityDir when set and valid', () => {
|
|
310
|
+
const dir = join(tmpdir(), `resolve-valid-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
311
|
+
mkdirSync(dir, { recursive: true });
|
|
312
|
+
writeFileSync(join(dir, 'CLAUDE.md'), '# Test');
|
|
313
|
+
const result = resolveTemplateDir({ personalityDir: dir });
|
|
314
|
+
assert.equal(result, dir);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('skips personalityDir when it has no CLAUDE.md', () => {
|
|
318
|
+
const dir = join(tmpdir(), `resolve-empty-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
319
|
+
mkdirSync(dir, { recursive: true });
|
|
320
|
+
const result = resolveTemplateDir({ personalityDir: dir });
|
|
321
|
+
// Should NOT return the dir (no CLAUDE.md there).
|
|
322
|
+
// May return auto-detected template-v2/ in dev mode, or null.
|
|
323
|
+
assert.notEqual(result, dir);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
@@ -763,6 +763,22 @@ class Database:
|
|
|
763
763
|
conn.commit()
|
|
764
764
|
logger.info("Applied migration 15: relationship origin_type for organic trust model")
|
|
765
765
|
|
|
766
|
+
if current_version < 16:
|
|
767
|
+
# Migration 16: Add source_channel to memories for channel-aware memory
|
|
768
|
+
try:
|
|
769
|
+
conn.execute(
|
|
770
|
+
"ALTER TABLE memories ADD COLUMN source_channel TEXT DEFAULT 'claude_code'"
|
|
771
|
+
)
|
|
772
|
+
except sqlite3.OperationalError as e:
|
|
773
|
+
if "duplicate column" not in str(e).lower():
|
|
774
|
+
logger.warning(f"Migration 16 statement failed: {e}")
|
|
775
|
+
|
|
776
|
+
conn.execute(
|
|
777
|
+
"INSERT OR IGNORE INTO schema_migrations (version, description) VALUES (16, 'Add source_channel to memories for channel-aware memory')"
|
|
778
|
+
)
|
|
779
|
+
conn.commit()
|
|
780
|
+
logger.info("Applied migration 16: source_channel on memories")
|
|
781
|
+
|
|
766
782
|
# FTS5 setup: ensure memories_fts exists regardless of migration path.
|
|
767
783
|
# The FTS5 virtual table + triggers contain internal semicolons that the
|
|
768
784
|
# schema.sql line-based parser can't handle, so we always check here.
|
|
@@ -896,6 +912,11 @@ class Database:
|
|
|
896
912
|
logger.warning("Migration 15 incomplete: relationships missing origin_type column")
|
|
897
913
|
return 14
|
|
898
914
|
|
|
915
|
+
# Migration 16 added source_channel to memories
|
|
916
|
+
if "source_channel" not in memory_cols:
|
|
917
|
+
logger.warning("Migration 16 incomplete: memories missing source_channel column")
|
|
918
|
+
return 15
|
|
919
|
+
|
|
899
920
|
return None # All good
|
|
900
921
|
|
|
901
922
|
def _store_workspace_path(self, conn: sqlite3.Connection) -> None:
|
|
@@ -140,6 +140,10 @@ async def list_tools() -> ListToolsResult:
|
|
|
140
140
|
"type": "string",
|
|
141
141
|
"description": "Full raw text of the source (email body, transcript, etc.). Saved to disk, not stored in DB.",
|
|
142
142
|
},
|
|
143
|
+
"source_channel": {
|
|
144
|
+
"type": "string",
|
|
145
|
+
"description": "Origin channel: claude_code, telegram, slack",
|
|
146
|
+
},
|
|
143
147
|
},
|
|
144
148
|
"required": ["content"],
|
|
145
149
|
},
|
|
@@ -638,6 +642,10 @@ async def list_tools() -> ListToolsResult:
|
|
|
638
642
|
"type": "string",
|
|
639
643
|
"description": "Full raw source text, saved to disk (for 'remember' op)",
|
|
640
644
|
},
|
|
645
|
+
"source_channel": {
|
|
646
|
+
"type": "string",
|
|
647
|
+
"description": "Origin channel: claude_code, telegram, slack (for 'remember' op)",
|
|
648
|
+
},
|
|
641
649
|
"target": {
|
|
642
650
|
"type": "string",
|
|
643
651
|
"description": "Target entity (for 'relate' op)",
|
|
@@ -1175,6 +1183,7 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
|
|
|
1175
1183
|
importance=arguments.get("importance", 1.0),
|
|
1176
1184
|
source=arguments.get("source"),
|
|
1177
1185
|
source_context=arguments.get("source_context"),
|
|
1186
|
+
source_channel=arguments.get("source_channel"),
|
|
1178
1187
|
)
|
|
1179
1188
|
# Save source material to disk if provided
|
|
1180
1189
|
if memory_id and arguments.get("source_material"):
|
|
@@ -1220,6 +1229,7 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
|
|
|
1220
1229
|
"source": r.source,
|
|
1221
1230
|
"source_id": r.source_id,
|
|
1222
1231
|
"source_context": r.source_context,
|
|
1232
|
+
"source_channel": r.source_channel,
|
|
1223
1233
|
}
|
|
1224
1234
|
for r in results
|
|
1225
1235
|
]
|
|
@@ -1291,6 +1301,7 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
|
|
|
1291
1301
|
"source": r.source,
|
|
1292
1302
|
"source_id": r.source_id,
|
|
1293
1303
|
"source_context": r.source_context,
|
|
1304
|
+
"source_channel": r.source_channel,
|
|
1294
1305
|
}
|
|
1295
1306
|
for r in results
|
|
1296
1307
|
]
|
|
@@ -1644,6 +1655,7 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
|
|
|
1644
1655
|
importance=op.get("importance", 1.0),
|
|
1645
1656
|
source=op.get("source"),
|
|
1646
1657
|
source_context=op.get("source_context"),
|
|
1658
|
+
source_channel=op.get("source_channel"),
|
|
1647
1659
|
_precomputed_embedding=embeddings_map.get(i),
|
|
1648
1660
|
)
|
|
1649
1661
|
op_result["success"] = True
|
|
@@ -60,7 +60,8 @@ CREATE TABLE IF NOT EXISTS memories (
|
|
|
60
60
|
access_count INTEGER DEFAULT 0,
|
|
61
61
|
verified_at TEXT, -- When this memory was verified
|
|
62
62
|
verification_status TEXT DEFAULT 'pending', -- pending, verified, flagged, contradicts
|
|
63
|
-
metadata TEXT -- JSON blob for flexible attributes
|
|
63
|
+
metadata TEXT, -- JSON blob for flexible attributes
|
|
64
|
+
source_channel TEXT DEFAULT 'claude_code' -- Origin channel: claude_code, telegram, slack
|
|
64
65
|
);
|
|
65
66
|
|
|
66
67
|
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
|
|
@@ -412,6 +413,9 @@ VALUES (14, 'Add dispatch_tier to agent_dispatches for native agent team support
|
|
|
412
413
|
INSERT OR IGNORE INTO schema_migrations (version, description)
|
|
413
414
|
VALUES (15, 'Add origin_type to relationships for organic trust model');
|
|
414
415
|
|
|
416
|
+
INSERT OR IGNORE INTO schema_migrations (version, description)
|
|
417
|
+
VALUES (16, 'Add source_channel to memories for channel-aware memory');
|
|
418
|
+
|
|
415
419
|
-- ============================================================================
|
|
416
420
|
-- AGENT DISPATCHES: Track delegated tasks to sub-agents
|
|
417
421
|
-- ============================================================================
|
|
@@ -41,6 +41,8 @@ class RecallResult:
|
|
|
41
41
|
confidence: float = 1.0 # How confident we are in this memory
|
|
42
42
|
verification_status: str = "pending" # pending, verified, flagged, contradicts
|
|
43
43
|
origin_type: str = "inferred" # user_stated, extracted, inferred, corrected
|
|
44
|
+
# Channel tracking
|
|
45
|
+
source_channel: Optional[str] = None # Origin channel: claude_code, telegram, slack
|
|
44
46
|
|
|
45
47
|
|
|
46
48
|
@dataclass
|
|
@@ -339,6 +341,9 @@ class RecallService:
|
|
|
339
341
|
verification_status_val = row["verification_status"] if "verification_status" in row_keys else "pending"
|
|
340
342
|
origin_type_val = row["origin_type"] if "origin_type" in row_keys else "inferred"
|
|
341
343
|
|
|
344
|
+
# Channel tracking (may not exist in older DBs)
|
|
345
|
+
source_channel_val = row["source_channel"] if "source_channel" in row_keys else None
|
|
346
|
+
|
|
342
347
|
return RecallResult(
|
|
343
348
|
id=row["id"],
|
|
344
349
|
content=row["content"],
|
|
@@ -354,6 +359,7 @@ class RecallService:
|
|
|
354
359
|
confidence=confidence_val,
|
|
355
360
|
verification_status=verification_status_val,
|
|
356
361
|
origin_type=origin_type_val,
|
|
362
|
+
source_channel=source_channel_val,
|
|
357
363
|
)
|
|
358
364
|
|
|
359
365
|
def _rrf_score(
|
|
@@ -141,6 +141,7 @@ class RememberService:
|
|
|
141
141
|
source_context: Optional[str] = None,
|
|
142
142
|
metadata: Optional[Dict] = None,
|
|
143
143
|
origin_type: Optional[str] = None,
|
|
144
|
+
source_channel: Optional[str] = None,
|
|
144
145
|
_precomputed_embedding: Optional[List[float]] = None,
|
|
145
146
|
) -> Optional[int]:
|
|
146
147
|
"""
|
|
@@ -157,6 +158,7 @@ class RememberService:
|
|
|
157
158
|
source_context: One-line breadcrumb describing the source material
|
|
158
159
|
metadata: Additional metadata
|
|
159
160
|
origin_type: 'user_stated', 'extracted', 'inferred', 'corrected' (Trust North Star)
|
|
161
|
+
source_channel: Origin channel: claude_code, telegram, slack
|
|
160
162
|
|
|
161
163
|
Returns:
|
|
162
164
|
Memory ID or None if duplicate
|
|
@@ -215,6 +217,8 @@ class RememberService:
|
|
|
215
217
|
}
|
|
216
218
|
if source_context:
|
|
217
219
|
insert_data["source_context"] = source_context
|
|
220
|
+
if source_channel:
|
|
221
|
+
insert_data["source_channel"] = source_channel
|
|
218
222
|
|
|
219
223
|
memory_id = self.db.insert("memories", insert_data)
|
|
220
224
|
|
|
@@ -102,6 +102,41 @@ def test_update():
|
|
|
102
102
|
assert entity["description"] == "Updated description"
|
|
103
103
|
|
|
104
104
|
|
|
105
|
+
def test_migration_16_source_channel():
|
|
106
|
+
"""Migration 16 adds source_channel to memories with default 'claude_code'."""
|
|
107
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
108
|
+
db_path = Path(tmpdir) / "test.db"
|
|
109
|
+
db = Database(db_path)
|
|
110
|
+
db.initialize()
|
|
111
|
+
|
|
112
|
+
# Verify column exists
|
|
113
|
+
cols = db._get_table_columns(db._get_connection(), "memories")
|
|
114
|
+
assert "source_channel" in cols, "source_channel column should exist after migration 16"
|
|
115
|
+
|
|
116
|
+
# Insert a memory without specifying source_channel
|
|
117
|
+
memory_id = db.insert("memories", {
|
|
118
|
+
"content": "test memory",
|
|
119
|
+
"content_hash": "abc123",
|
|
120
|
+
"type": "fact",
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
# Default should be 'claude_code'
|
|
124
|
+
row = db.get_one("memories", where="id = ?", where_params=(memory_id,))
|
|
125
|
+
assert row["source_channel"] == "claude_code"
|
|
126
|
+
|
|
127
|
+
# Insert with explicit source_channel
|
|
128
|
+
memory_id2 = db.insert("memories", {
|
|
129
|
+
"content": "telegram memory",
|
|
130
|
+
"content_hash": "def456",
|
|
131
|
+
"type": "fact",
|
|
132
|
+
"source_channel": "telegram",
|
|
133
|
+
})
|
|
134
|
+
row2 = db.get_one("memories", where="id = ?", where_params=(memory_id2,))
|
|
135
|
+
assert row2["source_channel"] == "telegram"
|
|
136
|
+
|
|
137
|
+
db.close()
|
|
138
|
+
|
|
139
|
+
|
|
105
140
|
def test_migration_integrity_detects_missing_verification_status():
|
|
106
141
|
"""Migration 5 added verification_status. Integrity check should catch if it is missing."""
|
|
107
142
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Tests for source_channel feature (channel-aware memory)."""
|
|
2
|
+
|
|
3
|
+
import tempfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from claudia_memory.database import Database
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def db():
|
|
14
|
+
"""Create a temporary test database."""
|
|
15
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
16
|
+
db_path = Path(tmpdir) / "test.db"
|
|
17
|
+
database = Database(db_path)
|
|
18
|
+
database.initialize()
|
|
19
|
+
yield database
|
|
20
|
+
database.close()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_source_channel_in_recall_result(db):
|
|
24
|
+
"""source_channel should appear in recall results."""
|
|
25
|
+
from claudia_memory.services.recall import RecallResult
|
|
26
|
+
|
|
27
|
+
# Verify the dataclass has source_channel field
|
|
28
|
+
r = RecallResult(
|
|
29
|
+
id=1, content="test", type="fact", score=1.0,
|
|
30
|
+
importance=1.0, created_at="2026-01-01", entities=[],
|
|
31
|
+
source_channel="telegram",
|
|
32
|
+
)
|
|
33
|
+
assert r.source_channel == "telegram"
|
|
34
|
+
|
|
35
|
+
# Default should be None
|
|
36
|
+
r2 = RecallResult(
|
|
37
|
+
id=2, content="test2", type="fact", score=1.0,
|
|
38
|
+
importance=1.0, created_at="2026-01-01", entities=[],
|
|
39
|
+
)
|
|
40
|
+
assert r2.source_channel is None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_remember_fact_stores_source_channel(db):
|
|
44
|
+
"""remember_fact should store source_channel when provided."""
|
|
45
|
+
with patch("claudia_memory.services.remember.embed_sync", return_value=None):
|
|
46
|
+
with patch("claudia_memory.services.remember.get_db", return_value=db):
|
|
47
|
+
with patch("claudia_memory.services.remember.get_embedding_service"):
|
|
48
|
+
with patch("claudia_memory.services.remember.get_extractor"):
|
|
49
|
+
from claudia_memory.services.remember import RememberService
|
|
50
|
+
# Reset global service to pick up our mocked db
|
|
51
|
+
import claudia_memory.services.remember as rem_mod
|
|
52
|
+
old_svc = rem_mod._service
|
|
53
|
+
rem_mod._service = None
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
svc = RememberService()
|
|
57
|
+
svc.db = db
|
|
58
|
+
|
|
59
|
+
memory_id = svc.remember_fact(
|
|
60
|
+
content="Test from telegram",
|
|
61
|
+
source_channel="telegram",
|
|
62
|
+
)
|
|
63
|
+
assert memory_id is not None
|
|
64
|
+
|
|
65
|
+
row = db.get_one("memories", where="id = ?", where_params=(memory_id,))
|
|
66
|
+
assert row["source_channel"] == "telegram"
|
|
67
|
+
finally:
|
|
68
|
+
rem_mod._service = old_svc
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "get-claudia",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.32.0",
|
|
4
4
|
"description": "An AI assistant who learns how you work.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claudia",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"template-v2",
|
|
34
34
|
"memory-daemon",
|
|
35
35
|
"gateway",
|
|
36
|
+
"relay",
|
|
36
37
|
"visualizer",
|
|
37
38
|
"visualizer-threejs",
|
|
38
39
|
"assets"
|