get-claudia 1.29.2 → 1.31.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 (36) hide show
  1. package/bin/index.js +67 -9
  2. package/memory-daemon/claudia_memory/database.py +65 -0
  3. package/memory-daemon/claudia_memory/mcp/server.py +93 -0
  4. package/memory-daemon/claudia_memory/schema.sql +9 -1
  5. package/memory-daemon/claudia_memory/services/guards.py +32 -1
  6. package/memory-daemon/claudia_memory/services/recall.py +8 -1
  7. package/memory-daemon/claudia_memory/services/remember.py +184 -30
  8. package/memory-daemon/tests/test_batch_parallel.py +119 -0
  9. package/memory-daemon/tests/test_bitemporal.py +202 -16
  10. package/memory-daemon/tests/test_database.py +35 -0
  11. package/memory-daemon/tests/test_guards.py +3 -2
  12. package/memory-daemon/tests/test_relationship_guards.py +79 -0
  13. package/memory-daemon/tests/test_source_channel.py +68 -0
  14. package/package.json +2 -1
  15. package/relay/SETUP.md +165 -0
  16. package/relay/package-lock.json +128 -0
  17. package/relay/package.json +21 -0
  18. package/relay/scripts/install.ps1 +228 -0
  19. package/relay/scripts/install.sh +273 -0
  20. package/relay/src/chunker.js +64 -0
  21. package/relay/src/claude-runner.js +149 -0
  22. package/relay/src/config.js +113 -0
  23. package/relay/src/formatter.js +77 -0
  24. package/relay/src/index.js +106 -0
  25. package/relay/src/lock.js +78 -0
  26. package/relay/src/relay.js +112 -0
  27. package/relay/src/session.js +131 -0
  28. package/relay/src/telegram.js +345 -0
  29. package/relay/tests/chunker.test.js +83 -0
  30. package/relay/tests/config.test.js +59 -0
  31. package/relay/tests/formatter.test.js +75 -0
  32. package/relay/tests/session.test.js +128 -0
  33. package/relay/tests/telegram.test.js +40 -0
  34. package/template-v2/.claude/skills/README.md +2 -1
  35. package/template-v2/.claude/skills/map-connections/SKILL.md +23 -21
  36. package/template-v2/.claude/skills/setup-telegram.md +151 -0
package/bin/index.js CHANGED
@@ -220,7 +220,7 @@ async function main() {
220
220
 
221
221
  // Helper: run visualizer install script and call back when done (auto-install, no prompt)
222
222
  function runVisualizerSetup(callback) {
223
- console.log(`\n${colors.boldYellow}━━━ Phase 2/3: Brain Visualizer ━━━${colors.reset}\n`);
223
+ console.log(`\n${colors.boldYellow}━━━ Phase 2/4: Brain Visualizer ━━━${colors.reset}\n`);
224
224
 
225
225
  const visualizerScriptPath = isWindows
226
226
  ? join(__dirname, '..', 'visualizer', 'scripts', 'install.ps1')
@@ -263,7 +263,7 @@ async function main() {
263
263
 
264
264
  // Helper: run gateway install script and call back when done
265
265
  function runGatewaySetup(callback) {
266
- console.log(`\n${colors.boldYellow}━━━ Phase 3/3: Messaging Gateway ━━━${colors.reset}\n`);
266
+ console.log(`\n${colors.boldYellow}━━━ Phase 3/4: Messaging Gateway ━━━${colors.reset}\n`);
267
267
 
268
268
  const gatewayScriptPath = isWindows
269
269
  ? join(__dirname, '..', 'gateway', 'scripts', 'install.ps1')
@@ -309,6 +309,54 @@ async function main() {
309
309
  }
310
310
  }
311
311
 
312
+ // Helper: run relay install script and call back when done
313
+ function runRelaySetup(callback) {
314
+ console.log(`\n${colors.boldYellow}━━━ Phase 4/4: Telegram Relay ━━━${colors.reset}\n`);
315
+
316
+ const relayScriptPath = isWindows
317
+ ? join(__dirname, '..', 'relay', 'scripts', 'install.ps1')
318
+ : join(__dirname, '..', 'relay', 'scripts', 'install.sh');
319
+
320
+ if (!existsSync(relayScriptPath)) {
321
+ console.log(`${colors.yellow}!${colors.reset} Relay files not found. Skipping.`);
322
+ callback(false);
323
+ return;
324
+ }
325
+
326
+ try {
327
+ const spawnCmd = isWindows ? powershellPath : 'bash';
328
+ const spawnArgs = isWindows
329
+ ? ['-ExecutionPolicy', 'Bypass', '-File', relayScriptPath]
330
+ : [relayScriptPath];
331
+ const relayResult = spawn(spawnCmd, spawnArgs, {
332
+ stdio: 'inherit',
333
+ env: {
334
+ ...process.env,
335
+ CLAUDIA_RELAY_UPGRADE: isUpgrade ? '1' : '0',
336
+ CLAUDIA_RELAY_SKIP_SETUP: '1'
337
+ }
338
+ });
339
+
340
+ relayResult.on('close', (code) => {
341
+ if (code === 0) {
342
+ console.log(`${colors.green}✓${colors.reset} Relay installed`);
343
+ callback(true);
344
+ } else {
345
+ console.log(`${colors.yellow}!${colors.reset} Relay setup had issues. You can run it later with:`);
346
+ if (isWindows) {
347
+ console.log(` ${colors.cyan}powershell.exe -ExecutionPolicy Bypass -File "${relayScriptPath}"${colors.reset}`);
348
+ } else {
349
+ console.log(` ${colors.cyan}bash ${relayScriptPath}${colors.reset}`);
350
+ }
351
+ callback(false);
352
+ }
353
+ });
354
+ } catch (error) {
355
+ console.log(`${colors.yellow}!${colors.reset} Could not set up relay: ${error.message}`);
356
+ callback(false);
357
+ }
358
+ }
359
+
312
360
  // Helper: run system health check after install
313
361
  function runSystemHealthCheck(callback) {
314
362
  const diagnoseScript = isWindows
@@ -343,20 +391,25 @@ async function main() {
343
391
  }
344
392
 
345
393
  // Helper: finish install after optional components
346
- function finishInstall(memoryInstalled, visualizerInstalled, gatewayInstalled) {
394
+ function finishInstall(memoryInstalled, visualizerInstalled, gatewayInstalled, relayInstalled) {
347
395
  if (memoryInstalled) {
348
396
  // Run health check when memory system was installed
349
397
  runSystemHealthCheck((healthy) => {
350
- showNextSteps(memoryInstalled, visualizerInstalled, gatewayInstalled, healthy);
398
+ showNextSteps(memoryInstalled, visualizerInstalled, gatewayInstalled, relayInstalled, healthy);
351
399
  });
352
400
  } else {
353
- showNextSteps(memoryInstalled, visualizerInstalled, gatewayInstalled, true);
401
+ showNextSteps(memoryInstalled, visualizerInstalled, gatewayInstalled, relayInstalled, true);
354
402
  }
355
403
  }
356
404
 
357
- // Helper: run gateway setup (auto-install like visualizer), then finish
405
+ // Helper: run relay setup after gateway, then finish
406
+ function maybeRunRelay(memoryInstalled, visualizerInstalled, gatewayInstalled) {
407
+ runRelaySetup((relayOk) => finishInstall(memoryInstalled, visualizerInstalled, gatewayInstalled, relayOk));
408
+ }
409
+
410
+ // Helper: run gateway setup (auto-install like visualizer), then chain to relay
358
411
  function maybeRunGateway(memoryInstalled, visualizerInstalled) {
359
- runGatewaySetup((gatewayOk) => finishInstall(memoryInstalled, visualizerInstalled, gatewayOk));
412
+ runGatewaySetup((gatewayOk) => maybeRunRelay(memoryInstalled, visualizerInstalled, gatewayOk));
360
413
  }
361
414
 
362
415
  // Helper: auto-install visualizer after memory (if memory was installed), then chain to gateway
@@ -371,7 +424,7 @@ async function main() {
371
424
  }
372
425
 
373
426
  // Memory system always installs (no prompt)
374
- console.log(`\n${colors.boldYellow}━━━ Phase 1/3: Memory System ━━━${colors.reset}\n`);
427
+ console.log(`\n${colors.boldYellow}━━━ Phase 1/4: Memory System ━━━${colors.reset}\n`);
375
428
 
376
429
  const memoryDaemonPath = isWindows
377
430
  ? join(__dirname, '..', 'memory-daemon', 'scripts', 'install.ps1')
@@ -453,7 +506,7 @@ async function main() {
453
506
  // Memory failed to spawn -- continue with visualizer/gateway
454
507
  maybeRunVisualizer(false);
455
508
 
456
- function showNextSteps(memoryInstalled, visualizerInstalled, gatewayInstalled, systemHealthy = true) {
509
+ function showNextSteps(memoryInstalled, visualizerInstalled, gatewayInstalled, relayInstalled, systemHealthy = true) {
457
510
  const cdStep = isCurrentDir ? '' : ` ${colors.cyan}cd ${targetDir}${colors.reset}\n`;
458
511
 
459
512
  // Installation summary
@@ -465,11 +518,16 @@ async function main() {
465
518
  console.log(`${memoryInstalled ? check : warn} Memory system ${memoryInstalled ? 'Active' : 'Skipped'}`);
466
519
  console.log(`${visualizerInstalled ? check : warn} Brain visualizer ${visualizerInstalled ? 'Active' : 'Skipped'}`);
467
520
  console.log(`${gatewayInstalled ? check : warn} Gateway ${gatewayInstalled ? 'Installed' : 'Skipped'}`);
521
+ console.log(`${relayInstalled ? check : warn} Telegram relay ${relayInstalled ? 'Installed' : 'Skipped'}`);
468
522
 
469
523
  if (gatewayInstalled) {
470
524
  console.log(`${colors.yellow}->${colors.reset} Configure tokens: ~/.claudia/gateway.json`);
471
525
  }
472
526
 
527
+ if (relayInstalled) {
528
+ console.log(`${colors.yellow}->${colors.reset} Configure relay: run /setup-telegram inside Claude`);
529
+ }
530
+
473
531
  if (!systemHealthy) {
474
532
  console.log(`\n${colors.yellow}Some issues were detected above.${colors.reset}`);
475
533
  console.log(`${colors.dim}You can fix them now, or Claudia will work in fallback mode until they're resolved.${colors.reset}`);
@@ -138,6 +138,21 @@ class Database:
138
138
  else:
139
139
  conn.commit()
140
140
 
141
+ @contextmanager
142
+ def transaction(self) -> Generator[sqlite3.Connection, None, None]:
143
+ """Explicit multi-step transaction. Commits on success, rolls back on error.
144
+
145
+ Use this when multiple operations must succeed or fail atomically.
146
+ Unlike connection(), the caller uses conn.execute() directly.
147
+ """
148
+ conn = self._get_connection()
149
+ try:
150
+ yield conn
151
+ conn.commit()
152
+ except Exception:
153
+ conn.rollback()
154
+ raise
155
+
141
156
  @contextmanager
142
157
  def cursor(self) -> Generator[sqlite3.Cursor, None, None]:
143
158
  """Context manager for database cursor"""
@@ -724,6 +739,46 @@ class Database:
724
739
  conn.commit()
725
740
  logger.info("Applied migration 14: dispatch_tier for native agent teams")
726
741
 
742
+ if current_version < 15:
743
+ # Migration 15: Add origin_type to relationships for organic trust model
744
+ try:
745
+ conn.execute(
746
+ "ALTER TABLE relationships ADD COLUMN origin_type TEXT DEFAULT 'extracted'"
747
+ )
748
+ except sqlite3.OperationalError as e:
749
+ if "duplicate column" not in str(e).lower():
750
+ logger.warning(f"Migration 15 statement failed: {e}")
751
+
752
+ # Grandfather existing relationships: all get 'extracted' (the safe default)
753
+ try:
754
+ conn.execute(
755
+ "UPDATE relationships SET origin_type = 'extracted' WHERE origin_type IS NULL"
756
+ )
757
+ except sqlite3.OperationalError as e:
758
+ logger.warning(f"Migration 15 grandfather failed: {e}")
759
+
760
+ conn.execute(
761
+ "INSERT OR IGNORE INTO schema_migrations (version, description) VALUES (15, 'Add origin_type to relationships for organic trust model')"
762
+ )
763
+ conn.commit()
764
+ logger.info("Applied migration 15: relationship origin_type for organic trust model")
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
+
727
782
  # FTS5 setup: ensure memories_fts exists regardless of migration path.
728
783
  # The FTS5 virtual table + triggers contain internal semicolons that the
729
784
  # schema.sql line-based parser can't handle, so we always check here.
@@ -852,6 +907,16 @@ class Database:
852
907
  logger.warning("Migration 14 incomplete: agent_dispatches missing dispatch_tier column")
853
908
  return 13
854
909
 
910
+ # Migration 15 added origin_type to relationships
911
+ if "origin_type" not in rel_cols:
912
+ logger.warning("Migration 15 incomplete: relationships missing origin_type column")
913
+ return 14
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
+
855
920
  return None # All good
856
921
 
857
922
  def _store_workspace_path(self, conn: sqlite3.Connection) -> None:
@@ -57,6 +57,7 @@ from ..services.remember import (
57
57
  get_remember_service,
58
58
  get_unsummarized_turns,
59
59
  invalidate_memory,
60
+ invalidate_relationship,
60
61
  merge_entities,
61
62
  relate_entities,
62
63
  store_reflection,
@@ -139,6 +140,10 @@ async def list_tools() -> ListToolsResult:
139
140
  "type": "string",
140
141
  "description": "Full raw text of the source (email body, transcript, etc.). Saved to disk, not stored in DB.",
141
142
  },
143
+ "source_channel": {
144
+ "type": "string",
145
+ "description": "Origin channel: claude_code, telegram, slack",
146
+ },
142
147
  },
143
148
  "required": ["content"],
144
149
  },
@@ -240,6 +245,16 @@ async def list_tools() -> ListToolsResult:
240
245
  "description": "If true, invalidate existing relationship of same type between same entities and create new one",
241
246
  "default": False,
242
247
  },
248
+ "origin_type": {
249
+ "type": "string",
250
+ "description": "How this was learned: user_stated, extracted, inferred, corrected",
251
+ "default": "extracted",
252
+ },
253
+ "direction": {
254
+ "type": "string",
255
+ "description": "Relationship direction: forward, backward, or bidirectional",
256
+ "default": "bidirectional",
257
+ },
243
258
  },
244
259
  "required": ["source", "target", "relationship"],
245
260
  },
@@ -627,6 +642,10 @@ async def list_tools() -> ListToolsResult:
627
642
  "type": "string",
628
643
  "description": "Full raw source text, saved to disk (for 'remember' op)",
629
644
  },
645
+ "source_channel": {
646
+ "type": "string",
647
+ "description": "Origin channel: claude_code, telegram, slack (for 'remember' op)",
648
+ },
630
649
  "target": {
631
650
  "type": "string",
632
651
  "description": "Target entity (for 'relate' op)",
@@ -639,6 +658,23 @@ async def list_tools() -> ListToolsResult:
639
658
  "type": "number",
640
659
  "description": "Relationship strength 0.0-1.0 (for 'relate' op)",
641
660
  },
661
+ "origin_type": {
662
+ "type": "string",
663
+ "description": "How this was learned: user_stated, extracted, inferred (for 'relate' op)",
664
+ },
665
+ "supersedes": {
666
+ "type": "boolean",
667
+ "description": "Invalidate existing relationship of same type (for 'relate' op)",
668
+ "default": False,
669
+ },
670
+ "valid_at": {
671
+ "type": "string",
672
+ "description": "When this relationship became true (for 'relate' op)",
673
+ },
674
+ "direction": {
675
+ "type": "string",
676
+ "description": "Relationship direction (for 'relate' op)",
677
+ },
642
678
  },
643
679
  "required": ["op"],
644
680
  },
@@ -1060,6 +1096,37 @@ async def list_tools() -> ListToolsResult:
1060
1096
  "required": ["memory_id"],
1061
1097
  },
1062
1098
  ),
1099
+ Tool(
1100
+ name="memory.invalidate_relationship",
1101
+ description=(
1102
+ "Mark a relationship as incorrect or ended without creating a replacement. "
1103
+ "Use when the user says a relationship is wrong, or when someone leaves a "
1104
+ "company, ends a partnership, etc. The relationship is preserved for history "
1105
+ "but excluded from active queries."
1106
+ ),
1107
+ inputSchema={
1108
+ "type": "object",
1109
+ "properties": {
1110
+ "source": {
1111
+ "type": "string",
1112
+ "description": "Source entity name",
1113
+ },
1114
+ "target": {
1115
+ "type": "string",
1116
+ "description": "Target entity name",
1117
+ },
1118
+ "relationship": {
1119
+ "type": "string",
1120
+ "description": "Relationship type to invalidate (works_with, manages, etc.)",
1121
+ },
1122
+ "reason": {
1123
+ "type": "string",
1124
+ "description": "Why this relationship is being invalidated",
1125
+ },
1126
+ },
1127
+ "required": ["source", "target", "relationship"],
1128
+ },
1129
+ ),
1063
1130
  Tool(
1064
1131
  name="memory.audit_history",
1065
1132
  description=(
@@ -1116,6 +1183,7 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
1116
1183
  importance=arguments.get("importance", 1.0),
1117
1184
  source=arguments.get("source"),
1118
1185
  source_context=arguments.get("source_context"),
1186
+ source_channel=arguments.get("source_channel"),
1119
1187
  )
1120
1188
  # Save source material to disk if provided
1121
1189
  if memory_id and arguments.get("source_material"):
@@ -1161,6 +1229,7 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
1161
1229
  "source": r.source,
1162
1230
  "source_id": r.source_id,
1163
1231
  "source_context": r.source_context,
1232
+ "source_channel": r.source_channel,
1164
1233
  }
1165
1234
  for r in results
1166
1235
  ]
@@ -1232,6 +1301,7 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
1232
1301
  "source": r.source,
1233
1302
  "source_id": r.source_id,
1234
1303
  "source_context": r.source_context,
1304
+ "source_channel": r.source_channel,
1235
1305
  }
1236
1306
  for r in results
1237
1307
  ]
@@ -1281,6 +1351,8 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
1281
1351
  strength=arguments.get("strength", 1.0),
1282
1352
  valid_at=arguments.get("valid_at"),
1283
1353
  supersedes=arguments.get("supersedes", False),
1354
+ origin_type=arguments.get("origin_type", "extracted"),
1355
+ direction=arguments.get("direction", "bidirectional"),
1284
1356
  )
1285
1357
  return CallToolResult(
1286
1358
  content=[
@@ -1583,6 +1655,7 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
1583
1655
  importance=op.get("importance", 1.0),
1584
1656
  source=op.get("source"),
1585
1657
  source_context=op.get("source_context"),
1658
+ source_channel=op.get("source_channel"),
1586
1659
  _precomputed_embedding=embeddings_map.get(i),
1587
1660
  )
1588
1661
  op_result["success"] = True
@@ -1604,6 +1677,10 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
1604
1677
  target=op["target"],
1605
1678
  relationship=op["relationship"],
1606
1679
  strength=op.get("strength", 1.0),
1680
+ supersedes=op.get("supersedes", False),
1681
+ valid_at=op.get("valid_at"),
1682
+ direction=op.get("direction", "bidirectional"),
1683
+ origin_type=op.get("origin_type", "extracted"),
1607
1684
  )
1608
1685
  op_result["success"] = True
1609
1686
  op_result["relationship_id"] = relationship_id
@@ -1873,6 +1950,22 @@ async def call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
1873
1950
  ]
1874
1951
  )
1875
1952
 
1953
+ elif name == "memory.invalidate_relationship":
1954
+ result = invalidate_relationship(
1955
+ source=arguments["source"],
1956
+ target=arguments["target"],
1957
+ relationship=arguments["relationship"],
1958
+ reason=arguments.get("reason"),
1959
+ )
1960
+ return CallToolResult(
1961
+ content=[
1962
+ TextContent(
1963
+ type="text",
1964
+ text=json.dumps(result),
1965
+ )
1966
+ ]
1967
+ )
1968
+
1876
1969
  elif name == "memory.audit_history":
1877
1970
  # Get audit history for entity or memory
1878
1971
  entity_id = arguments.get("entity_id")
@@ -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);
@@ -89,6 +90,7 @@ CREATE TABLE IF NOT EXISTS relationships (
89
90
  target_entity_id INTEGER NOT NULL REFERENCES entities(id) ON DELETE CASCADE,
90
91
  relationship_type TEXT NOT NULL, -- works_with, manages, client_of, etc.
91
92
  strength REAL DEFAULT 1.0, -- Relationship strength (decays/grows)
93
+ origin_type TEXT DEFAULT 'extracted', -- user_stated, extracted, inferred, corrected
92
94
  direction TEXT DEFAULT 'bidirectional' CHECK (direction IN ('forward', 'backward', 'bidirectional')),
93
95
  valid_at TEXT, -- When this relationship became true in the real world
94
96
  invalid_at TEXT, -- When this relationship was superseded (NULL = current)
@@ -408,6 +410,12 @@ VALUES (13, 'Add origin_type to memories, agent_dispatches table for Trust North
408
410
  INSERT OR IGNORE INTO schema_migrations (version, description)
409
411
  VALUES (14, 'Add dispatch_tier to agent_dispatches for native agent team support');
410
412
 
413
+ INSERT OR IGNORE INTO schema_migrations (version, description)
414
+ VALUES (15, 'Add origin_type to relationships for organic trust model');
415
+
416
+ INSERT OR IGNORE INTO schema_migrations (version, description)
417
+ VALUES (16, 'Add source_channel to memories for channel-aware memory');
418
+
411
419
  -- ============================================================================
412
420
  -- AGENT DISPATCHES: Track delegated tasks to sub-agents
413
421
  -- ============================================================================
@@ -112,15 +112,37 @@ def validate_entity(
112
112
  return result
113
113
 
114
114
 
115
- def validate_relationship(strength: float = 1.0) -> ValidationResult:
115
+ # Origin-aware strength ceilings: relationships can't exceed their evidence level
116
+ ORIGIN_STRENGTH_CEILING = {
117
+ "user_stated": 1.0, # User said it directly -- full trust
118
+ "extracted": 0.8, # Evidence from a document -- high but not absolute
119
+ "inferred": 0.5, # Co-occurrence guess -- must earn trust through repetition
120
+ "corrected": 1.0, # User corrected it -- full trust
121
+ }
122
+
123
+ # Origin-scaled reinforcement increments (Hebbian: stronger signals potentiate more)
124
+ REINFORCEMENT_BY_ORIGIN = {
125
+ "user_stated": 0.2,
126
+ "extracted": 0.1,
127
+ "inferred": 0.05,
128
+ "corrected": 0.2,
129
+ }
130
+
131
+
132
+ def validate_relationship(
133
+ strength: float = 1.0,
134
+ origin_type: str = "extracted",
135
+ ) -> ValidationResult:
116
136
  """
117
137
  Validate a relationship before storage.
118
138
 
119
139
  Checks:
120
140
  - Strength clamped to [0, 1]
141
+ - Strength capped by origin authority ceiling
121
142
  """
122
143
  result = ValidationResult()
123
144
 
145
+ # Clamp to [0, 1]
124
146
  if strength < 0:
125
147
  result.warnings.append(f"Relationship strength {strength} clamped to 0.0")
126
148
  result.adjustments["strength"] = 0.0
@@ -128,4 +150,13 @@ def validate_relationship(strength: float = 1.0) -> ValidationResult:
128
150
  result.warnings.append(f"Relationship strength {strength} clamped to 1.0")
129
151
  result.adjustments["strength"] = 1.0
130
152
 
153
+ # Cap by origin authority
154
+ ceiling = ORIGIN_STRENGTH_CEILING.get(origin_type, 0.5)
155
+ effective = result.adjustments.get("strength", strength)
156
+ if effective > ceiling:
157
+ result.warnings.append(
158
+ f"Strength {effective} exceeds ceiling {ceiling} for origin '{origin_type}', capped"
159
+ )
160
+ result.adjustments["strength"] = ceiling
161
+
131
162
  return result
@@ -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(
@@ -696,10 +702,12 @@ class RecallService:
696
702
 
697
703
  relationships = []
698
704
  for row in rel_rows:
705
+ row_keys = row.keys()
699
706
  rel_dict = {
700
707
  "type": row["relationship_type"],
701
708
  "direction": row["direction"],
702
709
  "strength": row["strength"],
710
+ "origin_type": row["origin_type"] if "origin_type" in row_keys else "extracted",
703
711
  "other_entity": (
704
712
  row["target_name"]
705
713
  if row["source_entity_id"] == entity["id"]
@@ -713,7 +721,6 @@ class RecallService:
713
721
  }
714
722
  # Include temporal fields when showing historical data
715
723
  if include_historical:
716
- row_keys = row.keys()
717
724
  rel_dict["valid_at"] = row["valid_at"] if "valid_at" in row_keys else None
718
725
  rel_dict["invalid_at"] = row["invalid_at"] if "invalid_at" in row_keys else None
719
726
  relationships.append(rel_dict)