@jungjaehoon/mama-core 1.4.2 → 1.6.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 (181) hide show
  1. package/README.md +48 -10
  2. package/db/migrations/026-create-canonical-entity-tables.sql +99 -0
  3. package/db/migrations/027-create-entity-review-support.sql +44 -0
  4. package/db/migrations/028-create-entity-audit-tables.sql +45 -0
  5. package/db/migrations/029-extend-entity-observations-observation-type.sql +60 -0
  6. package/db/migrations/030-case-first-memory-substrate.sql +901 -0
  7. package/db/migrations/031-wiki-page-index-provenance-columns.sql +2 -0
  8. package/db/migrations/032-add-memory-provenance-columns.sql +21 -0
  9. package/db/migrations/033-create-model-runs-and-tool-traces.sql +53 -0
  10. package/db/migrations/034-add-connector-event-scope-columns.sql +14 -0
  11. package/db/migrations/035-create-twin-edges.sql +63 -0
  12. package/db/migrations/036-create-agent-situation-packets.sql +61 -0
  13. package/dist/agent-graph/alias-write.d.ts +3 -0
  14. package/dist/agent-graph/alias-write.js +231 -0
  15. package/dist/agent-graph/entity-resolve.d.ts +3 -0
  16. package/dist/agent-graph/entity-resolve.js +275 -0
  17. package/dist/agent-graph/errors.d.ts +4 -0
  18. package/dist/agent-graph/errors.js +11 -0
  19. package/dist/agent-graph/graph-query.d.ts +5 -0
  20. package/dist/agent-graph/graph-query.js +359 -0
  21. package/dist/agent-graph/index.d.ts +6 -0
  22. package/dist/agent-graph/index.js +22 -0
  23. package/dist/agent-graph/types.d.ts +162 -0
  24. package/dist/agent-graph/types.js +3 -0
  25. package/dist/agent-situation/builder.d.ts +8 -0
  26. package/dist/agent-situation/builder.js +239 -0
  27. package/dist/agent-situation/cache-key.d.ts +4 -0
  28. package/dist/agent-situation/cache-key.js +64 -0
  29. package/dist/agent-situation/index.d.ts +7 -0
  30. package/dist/agent-situation/index.js +28 -0
  31. package/dist/agent-situation/packet-store.d.ts +27 -0
  32. package/dist/agent-situation/packet-store.js +228 -0
  33. package/dist/agent-situation/ranking-policy.d.ts +18 -0
  34. package/dist/agent-situation/ranking-policy.js +75 -0
  35. package/dist/agent-situation/source-readers.d.ts +52 -0
  36. package/dist/agent-situation/source-readers.js +300 -0
  37. package/dist/agent-situation/types.d.ts +195 -0
  38. package/dist/agent-situation/types.js +3 -0
  39. package/dist/canonicalize.d.ts +38 -0
  40. package/dist/canonicalize.js +106 -0
  41. package/dist/cases/case-links.d.ts +116 -0
  42. package/dist/cases/case-links.js +794 -0
  43. package/dist/cases/composition-overrides.d.ts +90 -0
  44. package/dist/cases/composition-overrides.js +724 -0
  45. package/dist/cases/corrections.d.ts +156 -0
  46. package/dist/cases/corrections.js +1045 -0
  47. package/dist/cases/freshness.d.ts +57 -0
  48. package/dist/cases/freshness.js +283 -0
  49. package/dist/cases/live-state.d.ts +48 -0
  50. package/dist/cases/live-state.js +256 -0
  51. package/dist/cases/membership-explain.d.ts +56 -0
  52. package/dist/cases/membership-explain.js +140 -0
  53. package/dist/cases/membership-matcher.d.ts +23 -0
  54. package/dist/cases/membership-matcher.js +193 -0
  55. package/dist/cases/merge-split.d.ts +61 -0
  56. package/dist/cases/merge-split.js +368 -0
  57. package/dist/cases/role-inference.d.ts +10 -0
  58. package/dist/cases/role-inference.js +85 -0
  59. package/dist/cases/search-rollup.d.ts +27 -0
  60. package/dist/cases/search-rollup.js +309 -0
  61. package/dist/cases/sqlite-transaction.d.ts +10 -0
  62. package/dist/cases/sqlite-transaction.js +62 -0
  63. package/dist/cases/store.d.ts +74 -0
  64. package/dist/cases/store.js +816 -0
  65. package/dist/cases/target-ref.d.ts +25 -0
  66. package/dist/cases/target-ref.js +22 -0
  67. package/dist/cases/timeline-range.d.ts +6 -0
  68. package/dist/cases/timeline-range.js +322 -0
  69. package/dist/cases/tombstone-sweeper.d.ts +13 -0
  70. package/dist/cases/tombstone-sweeper.js +128 -0
  71. package/dist/cases/types.d.ts +244 -0
  72. package/dist/cases/types.js +47 -0
  73. package/dist/cases/wiki-page-index.d.ts +50 -0
  74. package/dist/cases/wiki-page-index.js +350 -0
  75. package/dist/connectors/event-index.d.ts +45 -0
  76. package/dist/connectors/event-index.js +381 -0
  77. package/dist/connectors/raw-query.d.ts +18 -0
  78. package/dist/connectors/raw-query.js +305 -0
  79. package/dist/connectors/types.d.ts +109 -0
  80. package/dist/connectors/types.js +3 -0
  81. package/dist/db-adapter/node-sqlite-adapter.d.ts +6 -0
  82. package/dist/db-adapter/node-sqlite-adapter.js +131 -0
  83. package/dist/db-manager.d.ts +12 -1
  84. package/dist/db-manager.js +60 -5
  85. package/dist/decision-formatter.d.ts +2 -0
  86. package/dist/decision-formatter.js +11 -8
  87. package/dist/edges/ref-validation.d.ts +8 -0
  88. package/dist/edges/ref-validation.js +262 -0
  89. package/dist/edges/store.d.ts +10 -0
  90. package/dist/edges/store.js +343 -0
  91. package/dist/edges/types.d.ts +86 -0
  92. package/dist/edges/types.js +18 -0
  93. package/dist/embedding-server/mobile/daemon.d.ts +1 -0
  94. package/dist/embedding-server/mobile/daemon.js +11 -1
  95. package/dist/embeddings.js +13 -0
  96. package/dist/entities/audit-metrics.d.ts +73 -0
  97. package/dist/entities/audit-metrics.js +128 -0
  98. package/dist/entities/candidate-generator.d.ts +9 -0
  99. package/dist/entities/candidate-generator.js +436 -0
  100. package/dist/entities/entity-impact.d.ts +41 -0
  101. package/dist/entities/entity-impact.js +140 -0
  102. package/dist/entities/entity-linked-decision-counts.d.ts +2 -0
  103. package/dist/entities/entity-linked-decision-counts.js +29 -0
  104. package/dist/entities/entity-list.d.ts +23 -0
  105. package/dist/entities/entity-list.js +99 -0
  106. package/dist/entities/entity-orphan-list.d.ts +26 -0
  107. package/dist/entities/entity-orphan-list.js +91 -0
  108. package/dist/entities/entity-search.d.ts +21 -0
  109. package/dist/entities/entity-search.js +150 -0
  110. package/dist/entities/errors.d.ts +65 -0
  111. package/dist/entities/errors.js +124 -0
  112. package/dist/entities/exact-merge-backfill.d.ts +11 -0
  113. package/dist/entities/exact-merge-backfill.js +161 -0
  114. package/dist/entities/lineage-backfill.d.ts +12 -0
  115. package/dist/entities/lineage-backfill.js +101 -0
  116. package/dist/entities/lineage-store.d.ts +51 -0
  117. package/dist/entities/lineage-store.js +308 -0
  118. package/dist/entities/normalization.d.ts +14 -0
  119. package/dist/entities/normalization.js +63 -0
  120. package/dist/entities/policy-store.d.ts +17 -0
  121. package/dist/entities/policy-store.js +263 -0
  122. package/dist/entities/policy-types.d.ts +72 -0
  123. package/dist/entities/policy-types.js +59 -0
  124. package/dist/entities/projection.d.ts +8 -0
  125. package/dist/entities/projection.js +82 -0
  126. package/dist/entities/provenance-query.d.ts +33 -0
  127. package/dist/entities/provenance-query.js +167 -0
  128. package/dist/entities/read-identity.d.ts +31 -0
  129. package/dist/entities/read-identity.js +264 -0
  130. package/dist/entities/recall-bridge.d.ts +5 -0
  131. package/dist/entities/recall-bridge.js +171 -0
  132. package/dist/entities/resolution-engine.d.ts +15 -0
  133. package/dist/entities/resolution-engine.js +69 -0
  134. package/dist/entities/rollback-preview.d.ts +39 -0
  135. package/dist/entities/rollback-preview.js +235 -0
  136. package/dist/entities/source-locator.d.ts +3 -0
  137. package/dist/entities/source-locator.js +32 -0
  138. package/dist/entities/store.d.ts +75 -0
  139. package/dist/entities/store.js +465 -0
  140. package/dist/entities/types.d.ts +162 -0
  141. package/dist/entities/types.js +30 -0
  142. package/dist/index.d.ts +62 -3
  143. package/dist/index.js +116 -26
  144. package/dist/mama-api.d.ts +53 -35
  145. package/dist/mama-api.js +437 -78
  146. package/dist/memory/api.d.ts +26 -38
  147. package/dist/memory/api.js +772 -68
  148. package/dist/memory/event-store.d.ts +3 -0
  149. package/dist/memory/event-store.js +21 -2
  150. package/dist/memory/provenance-audit.d.ts +22 -0
  151. package/dist/memory/provenance-audit.js +112 -0
  152. package/dist/memory/provenance-query.d.ts +11 -0
  153. package/dist/memory/provenance-query.js +193 -0
  154. package/dist/memory/provenance.d.ts +29 -0
  155. package/dist/memory/provenance.js +136 -0
  156. package/dist/memory/scope-backfill.d.ts +17 -0
  157. package/dist/memory/scope-backfill.js +77 -0
  158. package/dist/memory/types.d.ts +97 -16
  159. package/dist/model-runs/store.d.ts +13 -0
  160. package/dist/model-runs/store.js +457 -0
  161. package/dist/model-runs/tool-trace-store.d.ts +4 -0
  162. package/dist/model-runs/tool-trace-store.js +114 -0
  163. package/dist/model-runs/types.d.ts +69 -0
  164. package/dist/model-runs/types.js +5 -0
  165. package/dist/runtime/trusted-provenance.d.ts +3 -0
  166. package/dist/runtime/trusted-provenance.js +6 -0
  167. package/dist/search/feedback-store.d.ts +59 -0
  168. package/dist/search/feedback-store.js +211 -0
  169. package/dist/search/question-type.d.ts +5 -0
  170. package/dist/search/question-type.js +41 -0
  171. package/dist/search/ranker-features.d.ts +23 -0
  172. package/dist/search/ranker-features.js +104 -0
  173. package/dist/search/ranker-rescore.d.ts +31 -0
  174. package/dist/search/ranker-rescore.js +226 -0
  175. package/dist/search/ranker-trainer.d.ts +94 -0
  176. package/dist/search/ranker-trainer.js +860 -0
  177. package/dist/search/search-quality.d.ts +36 -0
  178. package/dist/search/search-quality.js +29 -0
  179. package/dist/test-utils.d.ts +14 -0
  180. package/dist/test-utils.js +52 -0
  181. package/package.json +28 -3
@@ -23,6 +23,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
23
23
  };
24
24
  Object.defineProperty(exports, "__esModule", { value: true });
25
25
  exports.initDB = initDB;
26
+ exports.assertTestProcessIsNotUsingRealDb = assertTestProcessIsNotUsingRealDb;
26
27
  exports.getDB = getDB;
27
28
  exports.getAdapter = getAdapter;
28
29
  exports.closeDB = closeDB;
@@ -43,6 +44,7 @@ exports.isTestMode = isTestMode;
43
44
  exports.fts5Search = fts5Search;
44
45
  exports.reindexEmbeddings = reindexEmbeddings;
45
46
  const path_1 = __importDefault(require("path"));
47
+ const os_1 = __importDefault(require("os"));
46
48
  const debug_logger_js_1 = require("./debug-logger.js");
47
49
  const progress_indicator_js_1 = require("./progress-indicator.js");
48
50
  const index_js_1 = require("./db-adapter/index.js");
@@ -53,6 +55,7 @@ let isInitialized = false;
53
55
  let initializingPromise = null; // Single-flight guard for concurrent callers
54
56
  // Migration directory (moved to src/db/migrations for M1.2)
55
57
  const MIGRATIONS_DIR = path_1.default.join(__dirname, '..', 'db', 'migrations');
58
+ const REAL_USER_DB_PATH = path_1.default.join(os_1.default.homedir(), '.claude', 'mama-memory.db');
56
59
  /**
57
60
  * Initialize SQLite database adapter and connect
58
61
  *
@@ -65,6 +68,7 @@ const MIGRATIONS_DIR = path_1.default.join(__dirname, '..', 'db', 'migrations');
65
68
  * @returns SQLite database connection
66
69
  */
67
70
  async function initDB() {
71
+ assertTestProcessIsNotUsingRealDb();
68
72
  // Already initialized - return immediately
69
73
  if (isInitialized) {
70
74
  return dbConnection;
@@ -79,6 +83,7 @@ async function initDB() {
79
83
  (0, progress_indicator_js_1.logSearching)('Initializing database...');
80
84
  // Create SQLite adapter
81
85
  dbAdapter = (0, index_js_1.createAdapter)();
86
+ assertTestProcessIsNotUsingRealDb(resolveAdapterDbPath(dbAdapter), 'adapter.getDbPath()');
82
87
  // Connect to database
83
88
  dbConnection = await dbAdapter.connect();
84
89
  // Run migrations (includes 012-create-checkpoints-table.sql)
@@ -104,6 +109,48 @@ async function initDB() {
104
109
  })();
105
110
  return initializingPromise;
106
111
  }
112
+ function assertTestProcessIsNotUsingRealDb(effectivePath, effectivePathSource = 'adapter') {
113
+ if (!isDatabaseBoundaryTestMode()) {
114
+ return;
115
+ }
116
+ const configuredPaths = [
117
+ { name: 'MAMA_DB_PATH', value: process.env.MAMA_DB_PATH },
118
+ { name: 'MAMA_DATABASE_PATH', value: process.env.MAMA_DATABASE_PATH },
119
+ ];
120
+ if (effectivePath) {
121
+ configuredPaths.push({ name: effectivePathSource, value: effectivePath });
122
+ }
123
+ for (const configuredPath of configuredPaths) {
124
+ if (!configuredPath.value) {
125
+ continue;
126
+ }
127
+ const resolvedPath = path_1.default.resolve(expandHomePath(configuredPath.value));
128
+ if (resolvedPath === path_1.default.resolve(REAL_USER_DB_PATH)) {
129
+ throw new Error(`[db-boundary] Refusing to initDB against real DB ${REAL_USER_DB_PATH} ` +
130
+ `from a test process (${configuredPath.name}=${configuredPath.value}). ` +
131
+ 'Set MAMA_DB_PATH to a temporary path before initDB.');
132
+ }
133
+ }
134
+ }
135
+ function resolveAdapterDbPath(adapter) {
136
+ if (typeof adapter.getDbPath === 'function') {
137
+ return adapter.getDbPath();
138
+ }
139
+ return adapter.dbPath;
140
+ }
141
+ function isDatabaseBoundaryTestMode() {
142
+ return isTestMode();
143
+ }
144
+ function expandHomePath(value) {
145
+ const home = os_1.default.homedir();
146
+ if (value === '~') {
147
+ return home;
148
+ }
149
+ if (value.startsWith('~/')) {
150
+ return path_1.default.join(home, value.slice(2));
151
+ }
152
+ return value.replaceAll('${HOME}', home);
153
+ }
107
154
  /**
108
155
  * Get database connection (singleton pattern)
109
156
  *
@@ -267,6 +314,13 @@ async function insertDecisionWithEmbedding(decision) {
267
314
  throw new Error(`Invalid event_date: must be ISO 8601 YYYY-MM-DD (got: ${d})`);
268
315
  }
269
316
  }
317
+ if (decision.event_datetime !== null && decision.event_datetime !== undefined) {
318
+ if (typeof decision.event_datetime !== 'number' ||
319
+ !Number.isFinite(decision.event_datetime) ||
320
+ decision.event_datetime <= 0) {
321
+ throw new Error(`Invalid event_datetime: must be a positive millisecond timestamp (got: ${decision.event_datetime})`);
322
+ }
323
+ }
270
324
  try {
271
325
  // Generate embedding BEFORE transaction (required for SQLite's sync transaction)
272
326
  // Note: Redact topic for privacy - only log length
@@ -299,14 +353,15 @@ async function insertDecisionWithEmbedding(decision) {
299
353
  confidence, created_at, updated_at,
300
354
  needs_validation, validation_attempts, last_validated_at, usage_count,
301
355
  trust_context, usage_success, usage_failure, time_saved,
302
- evidence, alternatives, risks, event_date
303
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
356
+ evidence, alternatives, risks, event_date, event_datetime,
357
+ agent_id, model_run_id, envelope_hash, gateway_call_id, source_refs_json, provenance_json
358
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
304
359
  `);
305
360
  const insertResult = stmt.run(decision.id, decision.topic, decision.decision, decision.reasoning || null, decision.outcome || null, decision.failure_reason || null, decision.limitation || null, decision.user_involvement || null, decision.session_id || null, decision.supersedes || null, decision.superseded_by || null, decision.refined_from ? JSON.stringify(decision.refined_from) : null, decision.confidence !== undefined ? decision.confidence : 0.5,
306
361
  // IMPORTANT: All timestamps are stored in milliseconds (Date.now()).
307
362
  // The schema DEFAULT uses unixepoch() (seconds) but is never used
308
363
  // since all inserts go through this function which always provides ms.
309
- decision.created_at || Date.now(), decision.updated_at || Date.now(), decision.needs_validation !== undefined ? decision.needs_validation : 0, decision.validation_attempts || 0, decision.last_validated_at || null, decision.usage_count || 0, decision.trust_context || null, decision.usage_success || 0, decision.usage_failure || 0, decision.time_saved || 0, decision.evidence || null, decision.alternatives || null, decision.risks || null, decision.event_date || null);
364
+ decision.created_at || Date.now(), decision.updated_at || Date.now(), decision.needs_validation !== undefined ? decision.needs_validation : 0, decision.validation_attempts || 0, decision.last_validated_at || null, decision.usage_count || 0, decision.trust_context || null, decision.usage_success || 0, decision.usage_failure || 0, decision.time_saved || 0, decision.evidence || null, decision.alternatives || null, decision.risks || null, decision.event_date || null, decision.event_datetime ?? null, decision.agent_id ?? null, decision.model_run_id ?? null, decision.envelope_hash ?? null, decision.gateway_call_id ?? null, decision.source_refs_json ?? null, decision.provenance_json ?? null);
310
365
  const rowid = Number(insertResult.lastInsertRowid);
311
366
  // Insert embedding in same transaction to ensure rowid matching
312
367
  (0, debug_logger_js_1.info)(`[db-manager] Vector search enabled: ${adapter.vectorSearchEnabled}`);
@@ -668,10 +723,10 @@ function resetDBState(options = {}) {
668
723
  /**
669
724
  * Check if running in test mode
670
725
  *
671
- * Returns true if MAMA_TEST_MODE or VITEST env vars are set
726
+ * Returns true if MAMA_TEST_MODE, VITEST, or NODE_ENV=test env vars are set
672
727
  */
673
728
  function isTestMode() {
674
- return !!(process.env.MAMA_TEST_MODE || process.env.VITEST);
729
+ return Boolean(process.env.MAMA_TEST_MODE || process.env.VITEST || process.env.NODE_ENV === 'test');
675
730
  }
676
731
  /**
677
732
  * FTS5 keyword search on decisions table.
@@ -38,6 +38,8 @@ export interface DecisionForFormat {
38
38
  risks?: string;
39
39
  /** ISO 8601 date when the event actually occurred. Null if not set. */
40
40
  event_date?: string | null;
41
+ /** Source event timestamp in milliseconds when known. Null if not set. */
42
+ event_datetime?: number | null;
41
43
  }
42
44
  /**
43
45
  * Trust context object
@@ -140,7 +140,7 @@ Top ${full.length} Most Relevant Decisions:
140
140
  `.trim();
141
141
  for (let i = 0; i < full.length; i++) {
142
142
  const d = full[i];
143
- const duration = calculateDuration(d.created_at);
143
+ const duration = calculateDuration(getDecisionTimestamp(d));
144
144
  const outcomeEmoji = getOutcomeEmoji(d.outcome ?? null);
145
145
  const relevancePercent = Math.round((d.relevanceScore || 0) * 100);
146
146
  context += `\n\n${i + 1}. ${d.decision} (${duration}, relevance: ${relevancePercent}%) ${outcomeEmoji}`;
@@ -166,7 +166,7 @@ Top ${full.length} Most Relevant Decisions:
166
166
  * Format small decision history (3 or fewer)
167
167
  */
168
168
  function formatSmallHistory(current, history) {
169
- const duration = calculateDuration(current.created_at);
169
+ const duration = calculateDuration(getDecisionTimestamp(current));
170
170
  let context = `
171
171
  🧠 DECISION HISTORY: ${current.topic}
172
172
 
@@ -284,6 +284,9 @@ function calculateDuration(timestamp) {
284
284
  }
285
285
  return `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`;
286
286
  }
287
+ function getDecisionTimestamp(decision) {
288
+ return decision.event_datetime ?? decision.created_at;
289
+ }
287
290
  /**
288
291
  * Calculate duration between two timestamps in days
289
292
  */
@@ -560,7 +563,7 @@ function formatTeaserList(decisions, topN = 3) {
560
563
  output += `\n "${preview}"`;
561
564
  // Recency metadata (NEW - Gaussian Decay)
562
565
  if (d.recency_age_days !== undefined && d.created_at) {
563
- const timeAgo = calculateDuration(d.created_at);
566
+ const timeAgo = calculateDuration(getDecisionTimestamp(d));
564
567
  const recencyScore = d.recency_score ? Math.round(d.recency_score * 100) : null;
565
568
  const finalScore = d.final_score ? Math.round(d.final_score * 100) : null;
566
569
  output += `\n ⏰ ${timeAgo}`;
@@ -582,7 +585,7 @@ function formatTeaser(decision) {
582
585
  if (!decision) {
583
586
  return null;
584
587
  }
585
- const timeAgo = calculateDuration(decision.created_at);
588
+ const timeAgo = calculateDuration(getDecisionTimestamp(decision));
586
589
  // Extract preview (first 60 chars)
587
590
  const preview = decision.decision.length > 60 ? decision.decision.substring(0, 60) + '...' : decision.decision;
588
591
  // Extract files from trust_context or show generic
@@ -633,7 +636,7 @@ function formatRecall(decisions, semanticEdges = null) {
633
636
  * Format single decision with full detail
634
637
  */
635
638
  function formatSingleDecision(decision) {
636
- const timeAgo = calculateDuration(decision.created_at);
639
+ const timeAgo = calculateDuration(getDecisionTimestamp(decision));
637
640
  const confidencePercent = Math.round((decision.confidence || 0) * 100);
638
641
  const outcomeEmoji = getOutcomeEmoji(decision.outcome ?? null);
639
642
  const outcomeText = decision.outcome || 'Not yet tracked';
@@ -725,7 +728,7 @@ function formatDecisionHistory(decisions, semanticEdges = null) {
725
728
  📋 Decision History: ${topic}
726
729
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
727
730
 
728
- Latest Decision (${calculateDuration(latest.created_at)}):
731
+ Latest Decision (${calculateDuration(getDecisionTimestamp(latest))}):
729
732
  ${latest.decision}
730
733
  `.trim();
731
734
  // Show brief reasoning if available
@@ -777,7 +780,7 @@ ${latest.decision}
777
780
  output += `\nPrevious Decisions (${older.length}):\n`;
778
781
  for (let i = 0; i < Math.min(older.length, 5); i++) {
779
782
  const d = older[i];
780
- const timeAgo = calculateDuration(d.created_at);
783
+ const timeAgo = calculateDuration(getDecisionTimestamp(d));
781
784
  const emoji = getOutcomeEmoji(d.outcome ?? null);
782
785
  output += `\n${i + 2}. ${d.decision} (${timeAgo}) ${emoji}`;
783
786
  if (d.outcome === 'FAILED' && d.failure_reason) {
@@ -876,7 +879,7 @@ function formatList(decisions, options = {}) {
876
879
  output += '━'.repeat(60) + '\n';
877
880
  for (let i = 0; i < items.length; i++) {
878
881
  const d = items[i];
879
- const timeAgo = calculateDuration(d.created_at);
882
+ const timeAgo = calculateDuration(getDecisionTimestamp(d));
880
883
  const type = d.user_involvement === 'approved' ? '👤 User' : '🤖 Assistant';
881
884
  const status = d.outcome ? getOutcomeEmoji(d.outcome) + ' ' + d.outcome : '⏳ Pending';
882
885
  const confidence = Math.round((d.confidence || 0) * 100);
@@ -0,0 +1,8 @@
1
+ import type { DatabaseAdapter } from '../db-manager.js';
2
+ import type { ListVisibleTwinEdgesOptions, TwinEdgeRecord, TwinRef, TwinScopeRef, TwinVisibility } from './types.js';
3
+ type TwinRefVisibilityAdapter = Pick<DatabaseAdapter, 'prepare'>;
4
+ export declare function assertTwinRefsVisible(adapter: TwinRefVisibilityAdapter, refs: readonly TwinRef[], visibility?: TwinVisibility): void;
5
+ export declare function assertTwinRefsVisibleToScopes(adapter: TwinRefVisibilityAdapter, refs: readonly TwinRef[], scopes?: TwinScopeRef[]): void;
6
+ export declare function listVisibleTwinEdgesForRefs(adapter: TwinRefVisibilityAdapter, refs: readonly TwinRef[], options?: ListVisibleTwinEdgesOptions): TwinEdgeRecord[];
7
+ export {};
8
+ //# sourceMappingURL=ref-validation.d.ts.map
@@ -0,0 +1,262 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.assertTwinRefsVisible = assertTwinRefsVisible;
4
+ exports.assertTwinRefsVisibleToScopes = assertTwinRefsVisibleToScopes;
5
+ exports.listVisibleTwinEdgesForRefs = listVisibleTwinEdgesForRefs;
6
+ const store_js_1 = require("./store.js");
7
+ const TABLE_COLUMN_PRAGMAS = {
8
+ decisions: 'PRAGMA table_info(decisions)',
9
+ case_truth: 'PRAGMA table_info(case_truth)',
10
+ };
11
+ const tableColumnCache = new WeakMap();
12
+ function scopeKey(scope) {
13
+ return `${scope.kind}\0${scope.id}`;
14
+ }
15
+ function hasScopes(scopes) {
16
+ return Array.isArray(scopes) && scopes.length > 0;
17
+ }
18
+ function hasConnectors(connectors) {
19
+ return Array.isArray(connectors) && connectors.length > 0;
20
+ }
21
+ function hasProjectRefs(projectRefs) {
22
+ return Array.isArray(projectRefs) && projectRefs.length > 0;
23
+ }
24
+ function visibilityFromScopes(scopes) {
25
+ return { scopes: scopes ? [...scopes] : undefined };
26
+ }
27
+ function asOfMs(visibility) {
28
+ return typeof visibility.asOfMs === 'number' ? visibility.asOfMs : null;
29
+ }
30
+ function isAtOrBeforeAsOf(value, visibility) {
31
+ const asOf = asOfMs(visibility);
32
+ return asOf === null || (typeof value === 'number' && value <= asOf);
33
+ }
34
+ function parseTimestamp(value) {
35
+ if (typeof value === 'number' && Number.isFinite(value)) {
36
+ return Math.floor(value);
37
+ }
38
+ if (typeof value === 'string') {
39
+ const parsed = Date.parse(value);
40
+ return Number.isFinite(parsed) ? parsed : null;
41
+ }
42
+ return null;
43
+ }
44
+ function tableColumns(adapter, table) {
45
+ const pragma = TABLE_COLUMN_PRAGMAS[table];
46
+ if (!pragma) {
47
+ throw new Error(`Unsupported table for column introspection: ${table}`);
48
+ }
49
+ let adapterCache = tableColumnCache.get(adapter);
50
+ if (!adapterCache) {
51
+ adapterCache = new Map();
52
+ tableColumnCache.set(adapter, adapterCache);
53
+ }
54
+ const cached = adapterCache.get(table);
55
+ if (cached) {
56
+ return cached;
57
+ }
58
+ const rows = adapter.prepare(pragma).all();
59
+ const columns = new Set(rows.map((row) => row.name));
60
+ adapterCache.set(table, columns);
61
+ return columns;
62
+ }
63
+ function isMemoryVisible(adapter, id, visibility) {
64
+ const row = adapter
65
+ .prepare(`
66
+ SELECT created_at, event_datetime
67
+ FROM decisions
68
+ WHERE id = ?
69
+ LIMIT 1
70
+ `)
71
+ .get(id);
72
+ if (!row) {
73
+ return false;
74
+ }
75
+ if (!isAtOrBeforeAsOf(row.event_datetime ?? row.created_at, visibility)) {
76
+ return false;
77
+ }
78
+ const scopes = visibility.scopes;
79
+ if (!hasScopes(scopes)) {
80
+ return true;
81
+ }
82
+ const scopeClauses = scopes.map(() => '(ms.kind = ? AND ms.external_id = ?)').join(' OR ');
83
+ const params = scopes.flatMap((scope) => [scope.kind, scope.id]);
84
+ const bindingRow = adapter
85
+ .prepare(`
86
+ SELECT 1 AS ok
87
+ FROM memory_scope_bindings msb
88
+ JOIN memory_scopes ms ON ms.id = msb.scope_id
89
+ WHERE msb.memory_id = ?
90
+ AND (${scopeClauses})
91
+ LIMIT 1
92
+ `)
93
+ .get(id, ...params);
94
+ if (bindingRow?.ok) {
95
+ return true;
96
+ }
97
+ const columns = tableColumns(adapter, 'decisions');
98
+ if (columns.has('memory_scope_kind') && columns.has('memory_scope_id')) {
99
+ const row = adapter
100
+ .prepare(`
101
+ SELECT 1 AS ok
102
+ FROM decisions
103
+ WHERE id = ?
104
+ AND (${scopes.map(() => '(memory_scope_kind = ? AND memory_scope_id = ?)').join(' OR ')})
105
+ LIMIT 1
106
+ `)
107
+ .get(id, ...params);
108
+ return Boolean(row?.ok);
109
+ }
110
+ return false;
111
+ }
112
+ function parseCaseScopeRefs(scopeRefs) {
113
+ if (typeof scopeRefs !== 'string' || scopeRefs.length === 0) {
114
+ return [];
115
+ }
116
+ try {
117
+ const parsed = JSON.parse(scopeRefs);
118
+ if (!Array.isArray(parsed)) {
119
+ return [];
120
+ }
121
+ return parsed.filter((item) => item !== null &&
122
+ typeof item === 'object' &&
123
+ typeof item.kind === 'string' &&
124
+ typeof item.id === 'string');
125
+ }
126
+ catch {
127
+ return [];
128
+ }
129
+ }
130
+ function isCaseVisible(adapter, id, visibility) {
131
+ const row = adapter.prepare('SELECT * FROM case_truth WHERE case_id = ? LIMIT 1').get(id);
132
+ if (!row) {
133
+ return false;
134
+ }
135
+ const timestamp = parseTimestamp(row.last_activity_at) ?? parseTimestamp(row.updated_at);
136
+ if (!isAtOrBeforeAsOf(timestamp, visibility)) {
137
+ return false;
138
+ }
139
+ const scopes = visibility.scopes;
140
+ if (!hasScopes(scopes)) {
141
+ return true;
142
+ }
143
+ const requested = new Set(scopes.map(scopeKey));
144
+ const scopeRefs = parseCaseScopeRefs(row.scope_refs);
145
+ if (scopeRefs.some((scope) => requested.has(scopeKey(scope)))) {
146
+ return true;
147
+ }
148
+ const columns = tableColumns(adapter, 'case_truth');
149
+ if (columns.has('memory_scope_kind') && columns.has('memory_scope_id')) {
150
+ return scopes.some((scope) => row.memory_scope_kind === scope.kind && row.memory_scope_id === scope.id);
151
+ }
152
+ return false;
153
+ }
154
+ function isRawVisible(adapter, id, visibility) {
155
+ const row = adapter
156
+ .prepare('SELECT * FROM connector_event_index WHERE event_index_id = ? LIMIT 1')
157
+ .get(id);
158
+ if (!row) {
159
+ return false;
160
+ }
161
+ if (hasConnectors(visibility.connectors) &&
162
+ !visibility.connectors.includes(String(row.source_connector))) {
163
+ return false;
164
+ }
165
+ if (hasProjectRefs(visibility.projectRefs) &&
166
+ !visibility.projectRefs.some((project) => project.id === row.project_id)) {
167
+ return false;
168
+ }
169
+ if (typeof visibility.tenantId === 'string' &&
170
+ visibility.tenantId.length > 0 &&
171
+ row.tenant_id !== visibility.tenantId) {
172
+ return false;
173
+ }
174
+ if (!isAtOrBeforeAsOf(Number(row.event_datetime ?? row.source_timestamp_ms), visibility)) {
175
+ return false;
176
+ }
177
+ if (!hasScopes(visibility.scopes)) {
178
+ return true;
179
+ }
180
+ return visibility.scopes.some((scope) => row.memory_scope_kind === scope.kind && row.memory_scope_id === scope.id);
181
+ }
182
+ function isEntityVisible(adapter, id, visibility) {
183
+ const row = adapter
184
+ .prepare('SELECT scope_kind, scope_id, created_at FROM entity_nodes WHERE id = ? AND status = ? LIMIT 1')
185
+ .get(id, 'active');
186
+ if (!row) {
187
+ return false;
188
+ }
189
+ if (!isAtOrBeforeAsOf(row.created_at, visibility)) {
190
+ return false;
191
+ }
192
+ const scopes = visibility.scopes;
193
+ if (!hasScopes(scopes)) {
194
+ return true;
195
+ }
196
+ return scopes.some((scope) => row.scope_kind === scope.kind && row.scope_id === scope.id);
197
+ }
198
+ function isTwinRefVisible(adapter, ref, visibility, visitedEdges, edgeCache) {
199
+ if (ref.kind === 'entity') {
200
+ return isEntityVisible(adapter, ref.id, visibility);
201
+ }
202
+ if (ref.kind === 'report') {
203
+ return !hasScopes(visibility.scopes);
204
+ }
205
+ if (ref.kind === 'memory') {
206
+ return isMemoryVisible(adapter, ref.id, visibility);
207
+ }
208
+ if (ref.kind === 'case') {
209
+ return isCaseVisible(adapter, ref.id, visibility);
210
+ }
211
+ if (ref.kind === 'raw') {
212
+ return isRawVisible(adapter, ref.id, visibility);
213
+ }
214
+ if (visitedEdges.has(ref.id)) {
215
+ return false;
216
+ }
217
+ let edge = edgeCache.get(ref.id);
218
+ if (edge === undefined) {
219
+ edge = (0, store_js_1.getTwinEdge)(adapter, ref.id);
220
+ edgeCache.set(ref.id, edge);
221
+ }
222
+ if (!edge) {
223
+ return false;
224
+ }
225
+ if (!isAtOrBeforeAsOf(edge.created_at, visibility)) {
226
+ return false;
227
+ }
228
+ const pathWithCurrent = new Set(visitedEdges);
229
+ pathWithCurrent.add(ref.id);
230
+ return (isTwinRefVisible(adapter, edge.subject_ref, visibility, new Set(pathWithCurrent), edgeCache) &&
231
+ isTwinRefVisible(adapter, edge.object_ref, visibility, new Set(pathWithCurrent), edgeCache));
232
+ }
233
+ function assertTwinRefsVisible(adapter, refs, visibility = {}) {
234
+ const edgeCache = new Map();
235
+ for (const ref of refs) {
236
+ if (!isTwinRefVisible(adapter, ref, visibility, new Set(), edgeCache)) {
237
+ throw new Error(`Twin ref is not visible to requested visibility: ${ref.kind}:${ref.id}`);
238
+ }
239
+ }
240
+ }
241
+ function assertTwinRefsVisibleToScopes(adapter, refs, scopes) {
242
+ assertTwinRefsVisible(adapter, refs, visibilityFromScopes(scopes));
243
+ }
244
+ function listVisibleTwinEdgesForRefs(adapter, refs, options = {}) {
245
+ const edgeCache = new Map();
246
+ const edgeTypes = normalizeEdgeTypes(options.edgeTypes);
247
+ const edges = (0, store_js_1.listTwinEdgesForRefs)(adapter, refs).filter((edge) => (edgeTypes.size === 0 || edgeTypes.has(edge.edge_type)) &&
248
+ (typeof options.asOfMs !== 'number' || edge.created_at <= options.asOfMs) &&
249
+ isTwinRefVisible(adapter, edge.subject_ref, options, new Set(), edgeCache) &&
250
+ isTwinRefVisible(adapter, edge.object_ref, options, new Set(), edgeCache));
251
+ const limit = typeof options.limit === 'number' && Number.isFinite(options.limit)
252
+ ? Math.max(0, Math.floor(options.limit))
253
+ : edges.length;
254
+ return edges.slice(0, limit);
255
+ }
256
+ function normalizeEdgeTypes(edgeTypes) {
257
+ if (!Array.isArray(edgeTypes) || edgeTypes.length === 0) {
258
+ return new Set();
259
+ }
260
+ return new Set(edgeTypes);
261
+ }
262
+ //# sourceMappingURL=ref-validation.js.map
@@ -0,0 +1,10 @@
1
+ import type { DatabaseAdapter } from '../db-manager.js';
2
+ import { type InsertTwinEdgeInput, type TwinEdgeRecord, type TwinRef } from './types.js';
3
+ type TwinEdgeReadAdapter = Pick<DatabaseAdapter, 'prepare'>;
4
+ type TwinEdgeAdapter = Pick<DatabaseAdapter, 'prepare' | 'transaction'>;
5
+ export declare function mapTwinEdgeRow(row: Record<string, unknown>): TwinEdgeRecord;
6
+ export declare function getTwinEdge(adapter: TwinEdgeReadAdapter, edgeId: string): TwinEdgeRecord | null;
7
+ export declare function listTwinEdgesForRefs(adapter: TwinEdgeReadAdapter, refs: readonly TwinRef[]): TwinEdgeRecord[];
8
+ export declare function insertTwinEdge(adapter: TwinEdgeAdapter, input: InsertTwinEdgeInput): TwinEdgeRecord;
9
+ export {};
10
+ //# sourceMappingURL=store.d.ts.map