@jungjaehoon/mama-core 1.0.1 → 1.0.4

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.
@@ -0,0 +1,140 @@
1
+ -- ══════════════════════════════════════════════════════════════
2
+ -- MAMA (Memory-Augmented MCP Architecture) - Initial Schema
3
+ -- ══════════════════════════════════════════════════════════════
4
+ -- Version: 1.0
5
+ -- Date: 2025-11-14
6
+ -- Purpose: Decision Graph schema for Evolutionary Decision Memory
7
+ -- ══════════════════════════════════════════════════════════════
8
+
9
+ -- ══════════════════════════════════════════════════════════════
10
+ -- 1. Decision Nodes (Core)
11
+ -- ══════════════════════════════════════════════════════════════
12
+ -- Task 1.1: Create decisions table with all fields
13
+
14
+ CREATE TABLE IF NOT EXISTS decisions (
15
+ id TEXT PRIMARY KEY, -- "decision_mesh_structure_001"
16
+ topic TEXT NOT NULL, -- "mesh_structure", "auth_strategy"
17
+ decision TEXT NOT NULL, -- "COMPLEX", "JWT", "< 20 lines"
18
+ reasoning TEXT, -- "Flexibility is important initially"
19
+
20
+ -- Outcome Tracking (Learn-Unlearn-Relearn)
21
+ outcome TEXT, -- "SUCCESS", "FAILED", "PARTIAL", NULL
22
+ failure_reason TEXT, -- "Performance bottleneck at 10K+ meshes"
23
+ limitation TEXT, -- "Missing layer information"
24
+ duration_days INTEGER, -- Days until outcome determined
25
+
26
+ -- User Involvement
27
+ user_involvement TEXT, -- "requested", "approved", "rejected"
28
+ session_id TEXT, -- Foreign key to sessions
29
+
30
+ -- Relationships (Explicit)
31
+ supersedes TEXT, -- Previous decision ID
32
+ superseded_by TEXT, -- Next decision ID (NULL if current)
33
+ refined_from TEXT, -- JSON array: ["id1", "id2"]
34
+
35
+ -- Confidence Evolution
36
+ confidence REAL DEFAULT 0.5, -- Bayesian updated (0.0-1.0)
37
+
38
+ -- Timestamps (stored in MILLISECONDS - JavaScript Date.now() convention)
39
+ created_at INTEGER DEFAULT (unixepoch() * 1000),
40
+ updated_at INTEGER DEFAULT (unixepoch() * 1000),
41
+
42
+ FOREIGN KEY (supersedes) REFERENCES decisions(id),
43
+ FOREIGN KEY (superseded_by) REFERENCES decisions(id),
44
+
45
+ CHECK (confidence >= 0.0 AND confidence <= 1.0),
46
+ CHECK (outcome IN ('SUCCESS', 'FAILED', 'PARTIAL') OR outcome IS NULL),
47
+ CHECK (user_involvement IN ('requested', 'approved', 'rejected') OR user_involvement IS NULL)
48
+ );
49
+
50
+ -- Task 1.5: Add indexes for performance
51
+ CREATE INDEX IF NOT EXISTS idx_decisions_topic ON decisions(topic);
52
+ CREATE INDEX IF NOT EXISTS idx_decisions_outcome ON decisions(outcome);
53
+ CREATE INDEX IF NOT EXISTS idx_decisions_session ON decisions(session_id);
54
+ CREATE INDEX IF NOT EXISTS idx_decisions_supersedes ON decisions(supersedes);
55
+ CREATE INDEX IF NOT EXISTS idx_decisions_created_at ON decisions(created_at);
56
+
57
+ -- ══════════════════════════════════════════════════════════════
58
+ -- 2. Decision Edges (Explicit Relationships)
59
+ -- ══════════════════════════════════════════════════════════════
60
+ -- Task 1.2: Create decision_edges table
61
+
62
+ CREATE TABLE IF NOT EXISTS decision_edges (
63
+ from_id TEXT NOT NULL, -- Source decision
64
+ to_id TEXT NOT NULL, -- Target decision
65
+ relationship TEXT NOT NULL, -- "supersedes", "refines", "contradicts"
66
+ reason TEXT, -- "Learned from performance failure"
67
+ weight REAL DEFAULT 1.0, -- Strength of relationship
68
+ created_at INTEGER DEFAULT (unixepoch() * 1000),
69
+
70
+ PRIMARY KEY (from_id, to_id, relationship),
71
+ FOREIGN KEY (from_id) REFERENCES decisions(id),
72
+ FOREIGN KEY (to_id) REFERENCES decisions(id),
73
+
74
+ CHECK (relationship IN ('supersedes', 'refines', 'contradicts')),
75
+ CHECK (weight >= 0.0 AND weight <= 1.0)
76
+ );
77
+
78
+ -- Task 1.5: Add indexes for graph traversal
79
+ CREATE INDEX IF NOT EXISTS idx_edges_from ON decision_edges(from_id);
80
+ CREATE INDEX IF NOT EXISTS idx_edges_to ON decision_edges(to_id);
81
+ CREATE INDEX IF NOT EXISTS idx_edges_relationship ON decision_edges(relationship);
82
+
83
+ -- ══════════════════════════════════════════════════════════════
84
+ -- 3. Sessions (Context Preservation)
85
+ -- ══════════════════════════════════════════════════════════════
86
+ -- Task 1.3: Create sessions table
87
+
88
+ CREATE TABLE IF NOT EXISTS sessions (
89
+ id TEXT PRIMARY KEY, -- "session_2025-11-14-1000"
90
+ project_path TEXT, -- "/path/to/project"
91
+
92
+ -- Rolling Summary (for next session)
93
+ rolling_summary TEXT, -- Condensed session summary
94
+ latest_exchange TEXT, -- Last 5 messages (JSON)
95
+
96
+ -- Metrics
97
+ action_count INTEGER DEFAULT 0,
98
+ decision_count INTEGER DEFAULT 0,
99
+
100
+ -- Timestamps
101
+ started_at INTEGER DEFAULT (unixepoch() * 1000),
102
+ last_active_at INTEGER DEFAULT (unixepoch() * 1000),
103
+ ended_at INTEGER,
104
+
105
+ CHECK (action_count >= 0),
106
+ CHECK (decision_count >= 0)
107
+ );
108
+
109
+ -- Task 1.5: Add indexes for session queries
110
+ CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_path);
111
+ CREATE INDEX IF NOT EXISTS idx_sessions_ended ON sessions(ended_at);
112
+ CREATE INDEX IF NOT EXISTS idx_sessions_last_active ON sessions(last_active_at);
113
+
114
+ -- ══════════════════════════════════════════════════════════════
115
+ -- 4. Vector Search (sqlite-vss)
116
+ -- ══════════════════════════════════════════════════════════════
117
+ -- Task 1.4: Create vss_memories virtual table
118
+ -- Note: This will be initialized programmatically in memory-store.js
119
+ -- because sqlite-vss requires extension loading first
120
+ --
121
+ -- CREATE VIRTUAL TABLE vss_memories USING vss0(
122
+ -- embedding(384) -- multilingual-e5-small embeddings
123
+ -- );
124
+
125
+ -- ══════════════════════════════════════════════════════════════
126
+ -- Migration Metadata
127
+ -- ══════════════════════════════════════════════════════════════
128
+
129
+ CREATE TABLE IF NOT EXISTS schema_version (
130
+ version INTEGER PRIMARY KEY,
131
+ applied_at INTEGER DEFAULT (unixepoch() * 1000),
132
+ description TEXT
133
+ );
134
+
135
+ INSERT OR IGNORE INTO schema_version (version, description)
136
+ VALUES (1, 'Initial Decision Graph schema');
137
+
138
+ -- ══════════════════════════════════════════════════════════════
139
+ -- End of Migration 001
140
+ -- ══════════════════════════════════════════════════════════════
@@ -0,0 +1,114 @@
1
+ -- ═══════════════════════════════════════════════════════════
2
+ -- MAMA (Memory-Augmented MCP Architecture) - Error Patterns Table
3
+ -- Migration 002: Add error_patterns table
4
+ --
5
+ -- Purpose: Store error patterns and their solutions for auto-resolution
6
+ -- Tasks: 3.1-3.4 (Error pattern schema)
7
+ -- AC #2, #3: Error pattern storage and auto-resolution
8
+ --
9
+ -- Version: 1.0
10
+ -- Date: 2025-11-14
11
+ -- ═══════════════════════════════════════════════════════════
12
+
13
+ -- Error Patterns Table
14
+ -- Stores error patterns, solutions, and success metrics
15
+ CREATE TABLE IF NOT EXISTS error_patterns (
16
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
17
+
18
+ -- Pattern Identification
19
+ error_pattern TEXT NOT NULL, -- Regex pattern or error signature
20
+ error_type TEXT, -- Error category (e.g., 'EADDRINUSE', 'ECONNREFUSED')
21
+
22
+ -- Solution
23
+ solution TEXT NOT NULL, -- Command or action to resolve
24
+ solution_type TEXT DEFAULT 'bash', -- 'bash', 'manual', 'code_fix'
25
+
26
+ -- Success Metrics
27
+ success_rate REAL DEFAULT 1.0, -- Success rate (0.0-1.0)
28
+ occurrences INTEGER DEFAULT 1, -- Total number of times encountered
29
+ successes INTEGER DEFAULT 1, -- Number of successful resolutions
30
+
31
+ -- Temporal Data
32
+ last_seen INTEGER NOT NULL, -- Last occurrence timestamp
33
+ first_seen INTEGER NOT NULL, -- First occurrence timestamp
34
+
35
+ -- Vector Embedding (for semantic matching)
36
+ embedding BLOB, -- 384-dim embedding (multilingual-e5-small)
37
+
38
+ -- Metadata
39
+ created_at INTEGER DEFAULT (unixepoch() * 1000),
40
+ updated_at INTEGER DEFAULT (unixepoch() * 1000),
41
+
42
+ -- Constraints
43
+ UNIQUE(error_pattern) -- One pattern per entry
44
+ );
45
+
46
+ -- Indexes for Performance
47
+ CREATE INDEX IF NOT EXISTS idx_error_patterns_last_seen
48
+ ON error_patterns(last_seen DESC);
49
+
50
+ CREATE INDEX IF NOT EXISTS idx_error_patterns_success_rate
51
+ ON error_patterns(success_rate DESC);
52
+
53
+ CREATE INDEX IF NOT EXISTS idx_error_patterns_error_type
54
+ ON error_patterns(error_type);
55
+
56
+ -- ═══════════════════════════════════════════════════════════
57
+ -- VSS Table Extension for Error Patterns
58
+ -- ═══════════════════════════════════════════════════════════
59
+
60
+ -- Note: Error pattern embeddings will be stored in the same vss_memories table
61
+ -- We'll use rowid mapping: error_patterns.id → vss_memories.rowid
62
+ -- This allows unified vector search across decisions AND error patterns
63
+
64
+ -- ═══════════════════════════════════════════════════════════
65
+ -- Sample Data (Optional - for testing)
66
+ -- ═══════════════════════════════════════════════════════════
67
+
68
+ -- Uncomment to insert sample error patterns for testing:
69
+
70
+ /*
71
+ INSERT INTO error_patterns (
72
+ error_pattern,
73
+ error_type,
74
+ solution,
75
+ solution_type,
76
+ success_rate,
77
+ occurrences,
78
+ successes,
79
+ last_seen,
80
+ first_seen
81
+ ) VALUES
82
+ (
83
+ 'EADDRINUSE.*port.*already in use',
84
+ 'EADDRINUSE',
85
+ 'lsof -ti:{{PORT}} | xargs kill',
86
+ 'bash',
87
+ 1.0,
88
+ 5,
89
+ 5,
90
+ unixepoch(),
91
+ unixepoch() - (30 * 24 * 60 * 60) -- 30 days ago
92
+ ),
93
+ (
94
+ 'ECONNREFUSED.*Connection refused',
95
+ 'ECONNREFUSED',
96
+ 'Check if service is running: systemctl status {{SERVICE}}',
97
+ 'manual',
98
+ 0.8,
99
+ 3,
100
+ 2,
101
+ unixepoch(),
102
+ unixepoch() - (7 * 24 * 60 * 60) -- 7 days ago
103
+ );
104
+ */
105
+
106
+ -- ═══════════════════════════════════════════════════════════
107
+ -- Migration Verification
108
+ -- ═══════════════════════════════════════════════════════════
109
+
110
+ -- Check table creation
111
+ SELECT
112
+ 'Migration 002 Complete: error_patterns table created' AS status,
113
+ COUNT(*) AS row_count
114
+ FROM error_patterns;
@@ -0,0 +1,28 @@
1
+ -- Migration 003: Add Validation Tracking Fields
2
+ -- Story: 014.7.6 - Interactive Learning Loop
3
+ -- Task: 1.2 - Add needs_validation field to decisions table
4
+ -- AC #1, #2: Validation tracking and user feedback collection
5
+ -- Date: 2025-11-14
6
+
7
+ -- Note: This migration is idempotent - duplicate column errors are handled gracefully
8
+ -- SQLite doesn't have "IF NOT EXISTS" for ALTER TABLE
9
+ -- The migration system treats "duplicate column" errors as success (idempotent)
10
+
11
+ -- Add validation tracking fields to decisions table (idempotent via migration system)
12
+ ALTER TABLE decisions ADD COLUMN needs_validation INTEGER DEFAULT 0 CHECK (needs_validation IN (0, 1));
13
+ ALTER TABLE decisions ADD COLUMN validation_attempts INTEGER DEFAULT 0;
14
+ ALTER TABLE decisions ADD COLUMN last_validated_at INTEGER;
15
+ ALTER TABLE decisions ADD COLUMN usage_count INTEGER DEFAULT 0;
16
+
17
+ -- Indexes for efficient queries (CREATE INDEX IF NOT EXISTS is supported)
18
+ CREATE INDEX IF NOT EXISTS idx_decisions_needs_validation ON decisions(needs_validation) WHERE needs_validation = 1;
19
+ CREATE INDEX IF NOT EXISTS idx_decisions_last_validated ON decisions(last_validated_at);
20
+ CREATE INDEX IF NOT EXISTS idx_decisions_usage_count ON decisions(usage_count) WHERE usage_count >= 10;
21
+
22
+ -- Update existing decisions: user_involvement='approved' don't need validation
23
+ -- Only update rows that don't have needs_validation set
24
+ UPDATE decisions SET needs_validation = 0 WHERE user_involvement = 'approved' AND needs_validation IS NULL;
25
+ UPDATE decisions SET needs_validation = 1 WHERE user_involvement = 'system_generated' AND needs_validation IS NULL;
26
+
27
+ -- Note: SQLite doesn't support DROP COLUMN, so rollback keeps columns
28
+ -- Rollback would require recreating the table (not recommended for production)
@@ -0,0 +1,17 @@
1
+ -- Migration 004: Add Trust Context Support
2
+ -- Story 014.7.10: Claude-Friendly Context Formatting
3
+ -- AC #2: Trust Context Schema
4
+ -- Date: 2025-11-14
5
+
6
+ -- Add trust_context JSON field for 5 trust components
7
+ ALTER TABLE decisions ADD COLUMN trust_context TEXT;
8
+
9
+ -- Add usage tracking fields for implicit feedback
10
+ ALTER TABLE decisions ADD COLUMN usage_success INTEGER DEFAULT 0;
11
+ ALTER TABLE decisions ADD COLUMN usage_failure INTEGER DEFAULT 0;
12
+ ALTER TABLE decisions ADD COLUMN time_saved INTEGER DEFAULT 0; -- milliseconds saved
13
+
14
+ -- Indexes for performance
15
+ CREATE INDEX IF NOT EXISTS idx_decisions_usage_success ON decisions(usage_success DESC);
16
+ CREATE INDEX IF NOT EXISTS idx_decisions_time_saved ON decisions(time_saved DESC);
17
+ CREATE INDEX IF NOT EXISTS idx_decisions_needs_validation_usage ON decisions(needs_validation, usage_success) WHERE needs_validation = 1;
@@ -0,0 +1,12 @@
1
+ -- Migration: 002_add_narrative_fields.sql
2
+ -- Description: Add fields for 5-layer narrative (Evidence, Tension)
3
+ -- Date: 2025-11-24
4
+
5
+ -- Add 'evidence' column for storing file paths, logs, or metrics (JSON array or string)
6
+ ALTER TABLE decisions ADD COLUMN evidence TEXT;
7
+
8
+ -- Add 'alternatives' column for Tension layer (what else was considered?)
9
+ ALTER TABLE decisions ADD COLUMN alternatives TEXT;
10
+
11
+ -- Add 'risks' column for Tension layer (what could go wrong?)
12
+ ALTER TABLE decisions ADD COLUMN risks TEXT;
@@ -0,0 +1,41 @@
1
+ -- Migration: 006-add-link-governance.sql
2
+ -- Description: Add governance fields to decision_edges for Epic 3
3
+ -- Date: 2025-11-24
4
+
5
+ -- Add governance metadata to decision_edges
6
+ ALTER TABLE decision_edges ADD COLUMN created_by TEXT DEFAULT 'user' CHECK (created_by IN ('llm', 'user'));
7
+ ALTER TABLE decision_edges ADD COLUMN approved_by_user INTEGER DEFAULT 1 CHECK (approved_by_user IN (0, 1));
8
+ ALTER TABLE decision_edges ADD COLUMN decision_id TEXT; -- Context decision where link was proposed
9
+ ALTER TABLE decision_edges ADD COLUMN evidence TEXT; -- Supporting evidence for the link
10
+ ALTER TABLE decision_edges ADD COLUMN approved_at INTEGER; -- Timestamp when link was approved (NULL for pending)
11
+
12
+ -- Create audit log table for link approval/rejection history
13
+ CREATE TABLE IF NOT EXISTS link_audit_log (
14
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
15
+ from_id TEXT NOT NULL,
16
+ to_id TEXT NOT NULL,
17
+ relationship TEXT NOT NULL,
18
+ action TEXT NOT NULL CHECK (action IN ('proposed', 'approved', 'rejected', 'deprecated')),
19
+ actor TEXT NOT NULL, -- 'llm', 'user', 'system'
20
+ reason TEXT,
21
+ created_at INTEGER DEFAULT (unixepoch() * 1000),
22
+
23
+ FOREIGN KEY (from_id) REFERENCES decisions(id),
24
+ FOREIGN KEY (to_id) REFERENCES decisions(id)
25
+ );
26
+
27
+ CREATE INDEX idx_link_audit_from ON link_audit_log(from_id);
28
+ CREATE INDEX idx_link_audit_to ON link_audit_log(to_id);
29
+ CREATE INDEX idx_link_audit_action ON link_audit_log(action);
30
+ CREATE INDEX idx_link_audit_created ON link_audit_log(created_at);
31
+
32
+ -- Note: Existing links (created before this migration) will have:
33
+ -- - created_by = 'user' (default)
34
+ -- - approved_by_user = 1 (default - considered approved)
35
+ -- - approved_at = created_at (for backward compatibility)
36
+ -- This preserves backward compatibility with v0 auto-generated links
37
+
38
+ -- Set approved_at for existing approved links (backward compatibility)
39
+ UPDATE decision_edges
40
+ SET approved_at = created_at
41
+ WHERE approved_by_user = 1 AND approved_at IS NULL;
@@ -0,0 +1,23 @@
1
+ -- Migration 007: Add restart_metrics table for tracking restart success rate and latency
2
+ -- Story 4.2: Restart success rate/latency monitoring
3
+ -- Replaces in-memory restart-metrics with persistent SQLite storage
4
+
5
+ -- Create restart_metrics table
6
+ CREATE TABLE IF NOT EXISTS restart_metrics (
7
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
8
+ timestamp TEXT NOT NULL,
9
+ session_id TEXT,
10
+ status TEXT NOT NULL CHECK (status IN ('success', 'failure')),
11
+ failure_reason TEXT CHECK (failure_reason IN ('NO_CHECKPOINT', 'LOAD_ERROR', 'CONTEXT_INCOMPLETE', NULL)),
12
+ latency_ms INTEGER NOT NULL,
13
+ mode TEXT NOT NULL CHECK (mode IN ('full', 'summary')),
14
+ narrative_count INTEGER DEFAULT 0,
15
+ link_count INTEGER DEFAULT 0,
16
+ created_at TEXT DEFAULT (datetime('now'))
17
+ );
18
+
19
+ -- Indexes for efficient querying
20
+ CREATE INDEX idx_restart_metrics_timestamp ON restart_metrics(timestamp);
21
+ CREATE INDEX idx_restart_metrics_status ON restart_metrics(status);
22
+ CREATE INDEX idx_restart_metrics_session ON restart_metrics(session_id);
23
+ CREATE INDEX idx_restart_metrics_mode ON restart_metrics(mode);
@@ -0,0 +1,43 @@
1
+ -- Migration: 008-fix-auto-link-defaults.sql
2
+ -- Description: Fix auto-generated links and update default values
3
+ -- Date: 2025-11-25
4
+ --
5
+ -- Problem: Migration 006 set incorrect defaults:
6
+ -- - created_by DEFAULT 'user' (should be 'llm' for auto-generated)
7
+ -- - approved_by_user DEFAULT 1 (should be 0 for pending approval)
8
+ --
9
+ -- This caused auto-generated links to be indistinguishable from user-approved links.
10
+ --
11
+ -- Solution:
12
+ -- 1. Identify auto-generated links by pattern matching reason field
13
+ -- 2. Mark them as created_by='llm', approved_by_user=0
14
+ -- 3. Note: Cannot change column defaults in SQLite, so application code must handle this
15
+
16
+ -- Step 1: Mark auto-generated links (identifiable by pattern in reason field)
17
+ -- Pattern: "Refines previous approach (similarity: X.XX)" or similar auto-generated reasons
18
+ UPDATE decision_edges
19
+ SET
20
+ created_by = 'llm',
21
+ approved_by_user = 0
22
+ WHERE reason LIKE '%similarity:%'
23
+ OR reason LIKE '%Refines previous approach%'
24
+ OR reason LIKE '%auto%'
25
+ OR reason LIKE '%Auto%';
26
+
27
+ -- Step 2: Log the migration action
28
+ INSERT INTO link_audit_log (from_id, to_id, relationship, action, actor, reason, created_at)
29
+ SELECT
30
+ from_id,
31
+ to_id,
32
+ relationship,
33
+ 'deprecated',
34
+ 'system',
35
+ 'Migration 008: Marked as auto-generated link requiring approval',
36
+ unixepoch() * 1000
37
+ FROM decision_edges
38
+ WHERE created_by = 'llm' AND approved_by_user = 0;
39
+
40
+ -- Note: After this migration:
41
+ -- - Auto-generated links will require explicit approval via approve_link tool
42
+ -- - Users can review pending links with get_pending_links tool
43
+ -- - New auto-generated links should set created_by='llm', approved_by_user=0
@@ -0,0 +1,40 @@
1
+ -- Migration: 009-remove-auto-links.sql
2
+ -- Description: Remove all auto-generated links (noise cleanup)
3
+ -- Date: 2025-11-25
4
+ --
5
+ -- Context: 2025-11-25 architecture decision
6
+ -- - Analyzed 462 links: 366 refines (100% cross-topic noise), 56 supersedes (valid), 40 contradicts (mostly noise)
7
+ -- - Key insight: LLM can infer decision evolution from time-ordered search results
8
+ -- - Auto-link generation removed from decision-tracker.js
9
+ -- - MCP tools consolidated from 11 to 4 (save, search, update, load_checkpoint)
10
+ --
11
+ -- This migration removes:
12
+ -- 1. All 'refines' links (100% cross-topic noise)
13
+ -- 2. All 'contradicts' links (unreliable without LLM judgment)
14
+ -- 3. Keeps 'supersedes' links (same-topic, reliable)
15
+
16
+ -- Step 1: Backup to audit log before deletion
17
+ INSERT INTO link_audit_log (from_id, to_id, relationship, action, actor, reason, created_at)
18
+ SELECT
19
+ from_id,
20
+ to_id,
21
+ relationship,
22
+ 'deprecated',
23
+ 'system',
24
+ 'Migration 009: Removed auto-generated noise links (refines, contradicts)',
25
+ unixepoch() * 1000
26
+ FROM decision_edges
27
+ WHERE relationship IN ('refines', 'contradicts');
28
+
29
+ -- Step 2: Delete noise links
30
+ DELETE FROM decision_edges
31
+ WHERE relationship IN ('refines', 'contradicts');
32
+
33
+ -- Step 3: Verify remaining links are valid (supersedes only)
34
+ -- This is a sanity check - should only have supersedes links remaining
35
+ -- SELECT relationship, COUNT(*) FROM decision_edges GROUP BY relationship;
36
+
37
+ -- Note: After this migration:
38
+ -- - Only 'supersedes' edges remain (same-topic, created when decision replaces previous)
39
+ -- - LLM will infer refines/contradicts from search results
40
+ -- - No more auto-link generation in decision-tracker.js
@@ -0,0 +1,56 @@
1
+ -- ══════════════════════════════════════════════════════════════
2
+ -- MAMA Migration 010: Extend Edge Types
3
+ -- ══════════════════════════════════════════════════════════════
4
+ -- Version: 1.3
5
+ -- Date: 2025-11-26
6
+ -- Purpose: Add builds_on, debates, synthesizes relationship types
7
+ -- Story: 2.1 - Edge Type Extension
8
+ -- ══════════════════════════════════════════════════════════════
9
+
10
+ -- SQLite doesn't support ALTER TABLE to modify CHECK constraints.
11
+ -- We need to recreate the table with expanded CHECK constraint.
12
+
13
+ -- Step 1: Create new table with extended edge types
14
+ CREATE TABLE IF NOT EXISTS decision_edges_new (
15
+ from_id TEXT NOT NULL,
16
+ to_id TEXT NOT NULL,
17
+ relationship TEXT NOT NULL,
18
+ reason TEXT,
19
+ weight REAL DEFAULT 1.0,
20
+ created_at INTEGER DEFAULT (unixepoch() * 1000),
21
+ created_by TEXT DEFAULT 'user' CHECK (created_by IN ('llm', 'user')),
22
+ approved_by_user INTEGER DEFAULT 1 CHECK (approved_by_user IN (0, 1)),
23
+ decision_id TEXT,
24
+ evidence TEXT,
25
+
26
+ PRIMARY KEY (from_id, to_id, relationship),
27
+ FOREIGN KEY (from_id) REFERENCES decisions(id),
28
+ FOREIGN KEY (to_id) REFERENCES decisions(id),
29
+
30
+ -- Extended CHECK constraint: original + v1.3 types
31
+ CHECK (relationship IN ('supersedes', 'refines', 'contradicts', 'builds_on', 'debates', 'synthesizes')),
32
+ CHECK (weight >= 0.0 AND weight <= 1.0)
33
+ );
34
+
35
+ -- Step 2: Copy existing data (explicit columns to handle schema variations)
36
+ INSERT OR IGNORE INTO decision_edges_new (from_id, to_id, relationship, reason, weight, created_at, created_by, approved_by_user, decision_id, evidence)
37
+ SELECT from_id, to_id, relationship, reason, weight, created_at, created_by, approved_by_user, decision_id, evidence FROM decision_edges;
38
+
39
+ -- Step 3: Drop old table
40
+ DROP TABLE IF EXISTS decision_edges;
41
+
42
+ -- Step 4: Rename new table
43
+ ALTER TABLE decision_edges_new RENAME TO decision_edges;
44
+
45
+ -- Step 5: Recreate indexes
46
+ CREATE INDEX IF NOT EXISTS idx_edges_from ON decision_edges(from_id);
47
+ CREATE INDEX IF NOT EXISTS idx_edges_to ON decision_edges(to_id);
48
+ CREATE INDEX IF NOT EXISTS idx_edges_relationship ON decision_edges(relationship);
49
+
50
+ -- Update schema version
51
+ INSERT OR REPLACE INTO schema_version (version, description, applied_at)
52
+ VALUES (10, 'Extend edge types: builds_on, debates, synthesizes', unixepoch() * 1000);
53
+
54
+ -- ══════════════════════════════════════════════════════════════
55
+ -- End of Migration 010
56
+ -- ══════════════════════════════════════════════════════════════
@@ -0,0 +1,56 @@
1
+ -- ══════════════════════════════════════════════════════════════
2
+ -- Migration 011: Fix Timestamp Convention
3
+ -- ══════════════════════════════════════════════════════════════
4
+ --
5
+ -- Issue: Schema DEFAULT uses unixepoch() (seconds) but app inserts Date.now() (milliseconds)
6
+ -- Fix: Document convention and add validation trigger to prevent accidental second-based inserts
7
+ --
8
+ -- Note: SQLite doesn't support ALTER COLUMN to change DEFAULT values.
9
+ -- Instead, we add a trigger to validate/convert timestamps on insert.
10
+ -- ══════════════════════════════════════════════════════════════
11
+
12
+ -- Timestamp threshold: 2020-01-01 in milliseconds = 1577836800000
13
+ -- Any value below this is likely in seconds and needs conversion
14
+
15
+ -- Create trigger to validate timestamps are in milliseconds
16
+ -- If a timestamp looks like seconds (< year 2000 in ms), convert it
17
+ DROP TRIGGER IF EXISTS validate_decision_timestamp;
18
+ CREATE TRIGGER validate_decision_timestamp
19
+ AFTER INSERT ON decisions
20
+ WHEN NEW.created_at < 1577836800000 -- Before 2020 in milliseconds = probably seconds
21
+ BEGIN
22
+ UPDATE decisions
23
+ SET created_at = NEW.created_at * 1000,
24
+ updated_at = CASE
25
+ WHEN NEW.updated_at < 1577836800000 THEN NEW.updated_at * 1000
26
+ ELSE NEW.updated_at
27
+ END
28
+ WHERE rowid = NEW.rowid;
29
+ END;
30
+
31
+ -- Same for decision_edges table
32
+ DROP TRIGGER IF EXISTS validate_edge_timestamp;
33
+ CREATE TRIGGER validate_edge_timestamp
34
+ AFTER INSERT ON decision_edges
35
+ WHEN NEW.created_at IS NOT NULL AND NEW.created_at < 1577836800000
36
+ BEGIN
37
+ UPDATE decision_edges
38
+ SET created_at = NEW.created_at * 1000
39
+ WHERE from_id = NEW.from_id AND to_id = NEW.to_id AND relationship = NEW.relationship;
40
+ END;
41
+
42
+ -- Update schema version
43
+ INSERT OR REPLACE INTO schema_version (version, description, applied_at)
44
+ VALUES (11, 'Fix timestamp convention: auto-convert seconds to milliseconds', unixepoch() * 1000);
45
+
46
+ -- ══════════════════════════════════════════════════════════════
47
+ -- Documentation: TIMESTAMP CONVENTION
48
+ -- ══════════════════════════════════════════════════════════════
49
+ -- All timestamps in MAMA are stored in MILLISECONDS (JavaScript Date.now()).
50
+ --
51
+ -- The schema DEFAULT (unixepoch()) returns seconds, but:
52
+ -- 1. All app code explicitly provides Date.now() (milliseconds)
53
+ -- 2. This trigger auto-converts any accidental second-based inserts
54
+ --
55
+ -- This ensures timestamp comparisons always work correctly.
56
+ -- ══════════════════════════════════════════════════════════════
@@ -0,0 +1,23 @@
1
+ -- Migration: 012-create-checkpoints-table.sql
2
+ -- Description: Create checkpoints table for session continuity
3
+ -- Date: 2026-02-01
4
+ --
5
+ -- Context: Move DDL from db-manager.js runtime to proper migration
6
+ -- This table stores session checkpoints for resuming conversations
7
+ --
8
+ -- Note: Timestamps use milliseconds (Date.now()) convention per migration 011
9
+
10
+ -- Create checkpoints table if not exists
11
+ CREATE TABLE IF NOT EXISTS checkpoints (
12
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
13
+ timestamp INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
14
+ summary TEXT NOT NULL,
15
+ open_files TEXT, -- JSON array
16
+ next_steps TEXT,
17
+ recent_conversation TEXT DEFAULT '[]',
18
+ status TEXT DEFAULT 'active' -- 'active', 'archived'
19
+ );
20
+
21
+ -- Add recent_conversation column if not exists (backward compatibility)
22
+ -- SQLite doesn't have IF NOT EXISTS for ALTER TABLE, so we use a workaround
23
+ -- This is safe because SQLite will error on duplicate column, caught by migration runner
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jungjaehoon/mama-core",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "description": "MAMA Core - Shared modules for Memory-Augmented MCP Assistant",
5
5
  "main": "src/index.js",
6
6
  "exports": {
@@ -19,7 +19,11 @@
19
19
  "./debug-logger": "./src/debug-logger.js",
20
20
  "./decision-formatter": "./src/decision-formatter.js",
21
21
  "./memory-inject": "./src/memory-inject.js",
22
- "./ollama-client": "./src/ollama-client.js"
22
+ "./ollama-client": "./src/ollama-client.js",
23
+ "./errors": "./src/errors.js",
24
+ "./time-formatter": "./src/time-formatter.js",
25
+ "./outcome-tracker": "./src/outcome-tracker.js",
26
+ "./query-intent": "./src/query-intent.js"
23
27
  },
24
28
  "scripts": {
25
29
  "test": "vitest run",
@@ -46,6 +50,9 @@
46
50
  "url": "https://github.com/jungjaehoon-lifegamez/MAMA/issues"
47
51
  },
48
52
  "homepage": "https://github.com/jungjaehoon-lifegamez/MAMA#readme",
53
+ "publishConfig": {
54
+ "access": "public"
55
+ },
49
56
  "engines": {
50
57
  "node": ">=18.0.0"
51
58
  },
@@ -62,6 +69,7 @@
62
69
  },
63
70
  "files": [
64
71
  "src/**/*.js",
72
+ "db/**/*.sql",
65
73
  "README.md",
66
74
  "LICENSE"
67
75
  ]
package/src/db-manager.js CHANGED
@@ -230,8 +230,16 @@ async function insertDecisionWithEmbedding(decision) {
230
230
  info(
231
231
  `[db-manager] Generating embedding for decision (topic length: ${decision.topic?.length || 0})`
232
232
  );
233
- const embedding = await generateEnhancedEmbedding(decision);
234
- info(`[db-manager] Embedding generated: ${embedding ? embedding.length : 'null'} dimensions`);
233
+ let embedding = null;
234
+ try {
235
+ embedding = await generateEnhancedEmbedding(decision);
236
+ info(`[db-manager] Embedding generated: ${embedding ? embedding.length : 'null'} dimensions`);
237
+ } catch (embGenErr) {
238
+ // Non-fatal: save decision without embedding (e.g. ONNX model unavailable on CI)
239
+ logError(
240
+ `[db-manager] ⚠️ Embedding generation failed, saving without vector: ${embGenErr.message}`
241
+ );
242
+ }
235
243
 
236
244
  // SQLite: Synchronous transaction including embedding
237
245
  // eslint-disable-next-line no-unused-vars
@@ -286,7 +294,7 @@ async function insertDecisionWithEmbedding(decision) {
286
294
 
287
295
  // Insert embedding in same transaction to ensure rowid matching
288
296
  info(`[db-manager] Vector search enabled: ${adapter.vectorSearchEnabled}`);
289
- if (adapter.vectorSearchEnabled) {
297
+ if (adapter.vectorSearchEnabled && embedding) {
290
298
  try {
291
299
  info(`[db-manager] Inserting embedding for rowid: ${rowid}`);
292
300
  adapter.insertEmbedding(rowid, embedding);
@@ -250,9 +250,9 @@ async function handleRequest(req, res) {
250
250
  <body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #1a1a2e; color: #eee;">
251
251
  <div style="text-align: center; padding: 2rem;">
252
252
  <h1>🧠 MAMA Viewer</h1>
253
- <p>Viewer Chat 기능은 <strong>Standalone</strong> 모드에서만 사용 가능합니다.</p>
253
+ <p>Viewer and Chat features are only available in <strong>Standalone</strong> mode.</p>
254
254
  <p style="color: #888; margin-top: 1rem;">Start Standalone: <code style="background: #333; padding: 0.25rem 0.5rem; border-radius: 4px;">mama start</code></p>
255
- <p style="color: #666; font-size: 0.9rem; margin-top: 2rem;">현재: MCP Server (embedding only)</p>
255
+ <p style="color: #666; font-size: 0.9rem; margin-top: 2rem;">Current: MCP Server (embedding only)</p>
256
256
  </div>
257
257
  </body>
258
258
  </html>`);
@@ -163,10 +163,10 @@ class SessionManager {
163
163
  }
164
164
 
165
165
  const stmt = this.db.prepare(`
166
- SELECT id, project_dir, created_at, last_active, status, pid, client_id
166
+ SELECT id, project_dir, started_at, last_active_at, status, pid, client_id
167
167
  FROM sessions
168
168
  WHERE status = 'active'
169
- ORDER BY created_at DESC
169
+ ORDER BY started_at DESC
170
170
  `);
171
171
 
172
172
  const rows = stmt.all();
@@ -177,8 +177,8 @@ class SessionManager {
177
177
  return {
178
178
  id: row.id,
179
179
  projectDir: row.project_dir,
180
- createdAt: row.created_at,
181
- lastActive: row.last_active,
180
+ createdAt: row.started_at,
181
+ lastActive: row.last_active_at,
182
182
  status: row.status,
183
183
  pid: row.pid,
184
184
  clientId: row.client_id,
@@ -215,9 +215,30 @@ async function handleClientMessage(clientId, message, clientInfo, messageRouter,
215
215
  })
216
216
  );
217
217
 
218
+ // Keep-alive: Send periodic status updates to prevent WebSocket timeout
219
+ // Browser may disconnect idle WebSocket after ~30 seconds
220
+ let processingSeconds = 0;
221
+ const keepAliveInterval = setInterval(() => {
222
+ processingSeconds += 10;
223
+ if (clientInfo.ws.readyState === 1) {
224
+ // WebSocket.OPEN
225
+ clientInfo.ws.send(
226
+ JSON.stringify({
227
+ type: 'typing',
228
+ status: 'processing',
229
+ elapsed: processingSeconds,
230
+ })
231
+ );
232
+ console.error(
233
+ `[WebSocket] Keep-alive sent to ${clientId} (${processingSeconds}s elapsed)`
234
+ );
235
+ }
236
+ }, 10000); // Every 10 seconds
237
+
218
238
  const normalizedMessage = {
219
239
  source: clientInfo.osAgentMode ? 'viewer' : 'mobile',
220
- channelId: clientInfo.userId,
240
+ channelId: 'mama_os_main', // Fixed channel for all MAMA OS viewers
241
+ channelName: clientInfo.osAgentMode ? 'MAMA OS' : 'Mobile App', // Human-readable channel name
221
242
  userId: clientInfo.userId,
222
243
  text: content,
223
244
  metadata: {
@@ -228,7 +249,12 @@ async function handleClientMessage(clientId, message, clientInfo, messageRouter,
228
249
  },
229
250
  };
230
251
 
231
- const result = await messageRouter.process(normalizedMessage);
252
+ let result;
253
+ try {
254
+ result = await messageRouter.process(normalizedMessage);
255
+ } finally {
256
+ clearInterval(keepAliveInterval);
257
+ }
232
258
 
233
259
  // Send response as stream (for Chat tab compatibility)
234
260
  clientInfo.ws.send(
@@ -248,7 +274,7 @@ async function handleClientMessage(clientId, message, clientInfo, messageRouter,
248
274
  })
249
275
  );
250
276
 
251
- console.log(`[WebSocket] Message processed for ${clientId} (${result.duration}ms)`);
277
+ console.error(`[WebSocket] Message processed for ${clientId} (${result.duration}ms)`);
252
278
  } catch (error) {
253
279
  console.error(`[WebSocket] Message processing error:`, error);
254
280
  clientInfo.ws.send(
@@ -277,7 +303,7 @@ async function handleClientMessage(clientId, message, clientInfo, messageRouter,
277
303
  clientInfo.osAgentMode = osAgentMode || false;
278
304
  clientInfo.language = language || 'en';
279
305
 
280
- console.log(
306
+ console.error(
281
307
  `[WebSocket] Client ${clientId} attached to session ${sessionId}${osAgentMode ? ' (OS Agent mode)' : ''}`
282
308
  );
283
309
 
@@ -291,21 +317,49 @@ async function handleClientMessage(clientId, message, clientInfo, messageRouter,
291
317
  );
292
318
 
293
319
  // Send session history if available, or send onboarding greeting
320
+ console.error(
321
+ `[WebSocket] Checking sessionStore for history: ${!!sessionStore}, hasMethod: ${!!(sessionStore && sessionStore.getHistoryByChannel)}`
322
+ );
294
323
  if (sessionStore) {
295
324
  try {
296
- const history = sessionStore.getHistory(sessionId);
325
+ // Use channel-based lookup for sessions
326
+ // Use dynamic source based on osAgentMode (viewer vs mobile)
327
+ const channelId = 'mama_os_main';
328
+ const source = clientInfo.osAgentMode ? 'viewer' : 'mobile';
329
+ console.error(`[WebSocket] Loading history for source=${source}, channelId=${channelId}`);
330
+ const history = sessionStore.getHistoryByChannel
331
+ ? sessionStore.getHistoryByChannel(source, channelId)
332
+ : sessionStore.getHistory(sessionId);
333
+ console.error(`[WebSocket] History loaded: ${history ? history.length : 0} turns`);
297
334
  if (history && history.length > 0) {
335
+ // Convert {user, bot, timestamp} format to {role, content, timestamp} for display
336
+ const formattedMessages = history.flatMap((turn) => {
337
+ const messages = [];
338
+ if (turn.user) {
339
+ messages.push({
340
+ role: 'user',
341
+ content: turn.user,
342
+ timestamp: turn.timestamp,
343
+ });
344
+ }
345
+ if (turn.bot) {
346
+ messages.push({
347
+ role: 'assistant',
348
+ content: turn.bot,
349
+ timestamp: turn.timestamp,
350
+ });
351
+ }
352
+ return messages;
353
+ });
298
354
  clientInfo.ws.send(
299
355
  JSON.stringify({
300
356
  type: 'history',
301
- messages: history.map((turn) => ({
302
- role: turn.role,
303
- content: turn.content,
304
- timestamp: turn.timestamp,
305
- })),
357
+ messages: formattedMessages,
306
358
  })
307
359
  );
308
- console.log(`[WebSocket] Sent ${history.length} history messages to ${clientId}`);
360
+ console.error(
361
+ `[WebSocket] Sent ${formattedMessages.length} history messages to ${clientId}`
362
+ );
309
363
  } else {
310
364
  // No history - check if onboarding mode (SOUL.md not found)
311
365
  const soulPath = path.join(os.homedir(), '.mama', 'SOUL.md');
@@ -320,7 +374,7 @@ async function handleClientMessage(clientId, message, clientInfo, messageRouter,
320
374
  // Send onboarding greeting based on browser language
321
375
  const isKorean = clientInfo.language && clientInfo.language.startsWith('ko');
322
376
  const greeting = isKorean
323
- ? '✨ 방금 깨어났어요.\n\n아직 이름도 없고, 성격도 없고, 기억도 없어요. 그냥... 가능성만 있을 뿐이죠. 🌱\n\n당신은 누구세요? 그리고 중요한 건—저를 어떤 존재로 만들고 싶으세요? 💭'
377
+ ? '✨ I just woke up.\n\nNo name yet, no personality, no memories. Just... pure potential. 🌱\n\nWho are you? And more importantly—who do you want me to become? 💭'
324
378
  : '✨ I just woke up.\n\nNo name yet, no personality, no memories. Just... pure potential. 🌱\n\nWho are you? And more importantly—who do you want me to become? 💭';
325
379
 
326
380
  clientInfo.ws.send(
@@ -335,7 +389,7 @@ async function handleClientMessage(clientId, message, clientInfo, messageRouter,
335
389
  ],
336
390
  })
337
391
  );
338
- console.log(`[WebSocket] Sent onboarding greeting to ${clientId}`);
392
+ console.error(`[WebSocket] Sent onboarding greeting to ${clientId}`);
339
393
  }
340
394
  }
341
395
  } catch (error) {
package/src/embeddings.js CHANGED
@@ -26,6 +26,7 @@ const DEFAULT_CACHE_DIR = path.join(os.homedir(), '.cache', 'huggingface', 'tran
26
26
  // Singleton pattern for model loading
27
27
  let embeddingPipeline = null;
28
28
  let currentModelName = null;
29
+ let modelLoadFailed = false; // Cache load failures to avoid repeated slow retries
29
30
 
30
31
  /**
31
32
  * Load embedding model (configurable)
@@ -52,27 +53,37 @@ async function loadModel() {
52
53
  info('[MAMA] ⚡ Model cache cleared');
53
54
  }
54
55
 
56
+ // Fail fast if a previous load attempt already failed (avoid repeated slow retries)
57
+ if (modelLoadFailed) {
58
+ throw new Error('Embedding model previously failed to load — skipping retry');
59
+ }
60
+
55
61
  // Load model if not already loaded
56
62
  if (!embeddingPipeline) {
57
63
  logLoading(`Loading embedding model: ${modelName}...`);
58
64
  const startTime = Date.now();
59
65
 
60
- // Dynamic import for ES Module compatibility (Railway deployment)
61
- const transformers = await import('@huggingface/transformers');
62
- const { pipeline, env } = transformers;
63
-
64
- // Set shared cache directory (not in node_modules)
65
- // This prevents re-downloading models on every npm install
66
- const cacheDir = process.env.HF_HOME || process.env.TRANSFORMERS_CACHE || DEFAULT_CACHE_DIR;
67
- env.cacheDir = cacheDir;
68
- info(`[MAMA] Model cache directory: ${cacheDir}`);
69
-
70
- embeddingPipeline = await pipeline('feature-extraction', modelName);
71
- currentModelName = modelName;
72
-
73
- const loadTime = Date.now() - startTime;
74
- const config = loadConfig();
75
- logComplete(`Embedding model ready (${loadTime}ms, ${config.embeddingDim}-dim)`);
66
+ try {
67
+ // Dynamic import for ES Module compatibility (Railway deployment)
68
+ const transformers = await import('@huggingface/transformers');
69
+ const { pipeline, env } = transformers;
70
+
71
+ // Set shared cache directory (not in node_modules)
72
+ // This prevents re-downloading models on every npm install
73
+ const cacheDir = process.env.HF_HOME || process.env.TRANSFORMERS_CACHE || DEFAULT_CACHE_DIR;
74
+ env.cacheDir = cacheDir;
75
+ info(`[MAMA] Model cache directory: ${cacheDir}`);
76
+
77
+ embeddingPipeline = await pipeline('feature-extraction', modelName);
78
+ currentModelName = modelName;
79
+
80
+ const loadTime = Date.now() - startTime;
81
+ const config = loadConfig();
82
+ logComplete(`Embedding model ready (${loadTime}ms, ${config.embeddingDim}-dim)`);
83
+ } catch (loadErr) {
84
+ modelLoadFailed = true;
85
+ throw loadErr;
86
+ }
76
87
  }
77
88
 
78
89
  return embeddingPipeline;