alma-memory 0.5.1__py3-none-any.whl → 0.7.0__py3-none-any.whl

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 (111) hide show
  1. alma/__init__.py +296 -226
  2. alma/compression/__init__.py +33 -0
  3. alma/compression/pipeline.py +980 -0
  4. alma/confidence/__init__.py +47 -47
  5. alma/confidence/engine.py +540 -540
  6. alma/confidence/types.py +351 -351
  7. alma/config/loader.py +157 -157
  8. alma/consolidation/__init__.py +23 -23
  9. alma/consolidation/engine.py +678 -678
  10. alma/consolidation/prompts.py +84 -84
  11. alma/core.py +1189 -430
  12. alma/domains/__init__.py +30 -30
  13. alma/domains/factory.py +359 -359
  14. alma/domains/schemas.py +448 -448
  15. alma/domains/types.py +272 -272
  16. alma/events/__init__.py +75 -75
  17. alma/events/emitter.py +285 -284
  18. alma/events/storage_mixin.py +246 -246
  19. alma/events/types.py +126 -126
  20. alma/events/webhook.py +425 -425
  21. alma/exceptions.py +49 -49
  22. alma/extraction/__init__.py +31 -31
  23. alma/extraction/auto_learner.py +265 -265
  24. alma/extraction/extractor.py +420 -420
  25. alma/graph/__init__.py +106 -106
  26. alma/graph/backends/__init__.py +32 -32
  27. alma/graph/backends/kuzu.py +624 -624
  28. alma/graph/backends/memgraph.py +432 -432
  29. alma/graph/backends/memory.py +236 -236
  30. alma/graph/backends/neo4j.py +417 -417
  31. alma/graph/base.py +159 -159
  32. alma/graph/extraction.py +198 -198
  33. alma/graph/store.py +860 -860
  34. alma/harness/__init__.py +35 -35
  35. alma/harness/base.py +386 -386
  36. alma/harness/domains.py +705 -705
  37. alma/initializer/__init__.py +37 -37
  38. alma/initializer/initializer.py +418 -418
  39. alma/initializer/types.py +250 -250
  40. alma/integration/__init__.py +62 -62
  41. alma/integration/claude_agents.py +444 -444
  42. alma/integration/helena.py +423 -423
  43. alma/integration/victor.py +471 -471
  44. alma/learning/__init__.py +101 -86
  45. alma/learning/decay.py +878 -0
  46. alma/learning/forgetting.py +1446 -1446
  47. alma/learning/heuristic_extractor.py +390 -390
  48. alma/learning/protocols.py +374 -374
  49. alma/learning/validation.py +346 -346
  50. alma/mcp/__init__.py +123 -45
  51. alma/mcp/__main__.py +156 -156
  52. alma/mcp/resources.py +122 -122
  53. alma/mcp/server.py +955 -591
  54. alma/mcp/tools.py +3254 -509
  55. alma/observability/__init__.py +91 -84
  56. alma/observability/config.py +302 -302
  57. alma/observability/guidelines.py +170 -0
  58. alma/observability/logging.py +424 -424
  59. alma/observability/metrics.py +583 -583
  60. alma/observability/tracing.py +440 -440
  61. alma/progress/__init__.py +21 -21
  62. alma/progress/tracker.py +607 -607
  63. alma/progress/types.py +250 -250
  64. alma/retrieval/__init__.py +134 -53
  65. alma/retrieval/budget.py +525 -0
  66. alma/retrieval/cache.py +1304 -1061
  67. alma/retrieval/embeddings.py +202 -202
  68. alma/retrieval/engine.py +850 -427
  69. alma/retrieval/modes.py +365 -0
  70. alma/retrieval/progressive.py +560 -0
  71. alma/retrieval/scoring.py +344 -344
  72. alma/retrieval/trust_scoring.py +637 -0
  73. alma/retrieval/verification.py +797 -0
  74. alma/session/__init__.py +19 -19
  75. alma/session/manager.py +442 -399
  76. alma/session/types.py +288 -288
  77. alma/storage/__init__.py +101 -90
  78. alma/storage/archive.py +233 -0
  79. alma/storage/azure_cosmos.py +1259 -1259
  80. alma/storage/base.py +1083 -583
  81. alma/storage/chroma.py +1443 -1443
  82. alma/storage/constants.py +103 -103
  83. alma/storage/file_based.py +614 -614
  84. alma/storage/migrations/__init__.py +21 -21
  85. alma/storage/migrations/base.py +321 -321
  86. alma/storage/migrations/runner.py +323 -323
  87. alma/storage/migrations/version_stores.py +337 -337
  88. alma/storage/migrations/versions/__init__.py +11 -11
  89. alma/storage/migrations/versions/v1_0_0.py +373 -373
  90. alma/storage/migrations/versions/v1_1_0_workflow_context.py +551 -0
  91. alma/storage/pinecone.py +1080 -1080
  92. alma/storage/postgresql.py +1948 -1559
  93. alma/storage/qdrant.py +1306 -1306
  94. alma/storage/sqlite_local.py +3041 -1457
  95. alma/testing/__init__.py +46 -46
  96. alma/testing/factories.py +301 -301
  97. alma/testing/mocks.py +389 -389
  98. alma/types.py +292 -264
  99. alma/utils/__init__.py +19 -0
  100. alma/utils/tokenizer.py +521 -0
  101. alma/workflow/__init__.py +83 -0
  102. alma/workflow/artifacts.py +170 -0
  103. alma/workflow/checkpoint.py +311 -0
  104. alma/workflow/context.py +228 -0
  105. alma/workflow/outcomes.py +189 -0
  106. alma/workflow/reducers.py +393 -0
  107. {alma_memory-0.5.1.dist-info → alma_memory-0.7.0.dist-info}/METADATA +210 -72
  108. alma_memory-0.7.0.dist-info/RECORD +112 -0
  109. alma_memory-0.5.1.dist-info/RECORD +0 -93
  110. {alma_memory-0.5.1.dist-info → alma_memory-0.7.0.dist-info}/WHEEL +0 -0
  111. {alma_memory-0.5.1.dist-info → alma_memory-0.7.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,551 @@
1
+ """
2
+ ALMA Schema Migration v1.1.0 - Workflow Context Layer.
3
+
4
+ This migration adds support for AGtestari Workflow Studio integration:
5
+ - Checkpoints table: Crash recovery and state persistence
6
+ - Workflow Outcomes table: Learning from completed workflows
7
+ - Artifact Links table: Connecting artifacts to memories
8
+ - Workflow scope columns on existing tables
9
+
10
+ Sprint 0 Task 0.2, 0.3, 0.4, 0.7
11
+ Designed by: @data-analyst (Dara)
12
+ Reviewed by: @architect (Aria)
13
+ """
14
+
15
+ from typing import Any
16
+
17
+ from alma.storage.migrations.base import Migration, register_migration
18
+
19
+ # =============================================================================
20
+ # POSTGRESQL MIGRATIONS
21
+ # =============================================================================
22
+
23
+
24
+ @register_migration(backend="postgresql")
25
+ class PostgreSQLWorkflowContextMigration(Migration):
26
+ """
27
+ PostgreSQL migration for workflow context layer.
28
+
29
+ Includes pgvector support for semantic search on workflow outcomes.
30
+ """
31
+
32
+ version = "1.1.0"
33
+ description = (
34
+ "Add workflow context layer (checkpoints, workflow_outcomes, artifact_links)"
35
+ )
36
+ depends_on = "1.0.0"
37
+
38
+ def upgrade(self, connection: Any) -> None:
39
+ """Apply workflow context schema changes."""
40
+ cursor = connection.cursor()
41
+
42
+ # Get schema from connection or default to public
43
+ schema = getattr(connection, "_schema", "public")
44
+
45
+ # Check if pgvector is available
46
+ cursor.execute("""
47
+ SELECT EXISTS (
48
+ SELECT 1 FROM pg_extension WHERE extname = 'vector'
49
+ )
50
+ """)
51
+ has_pgvector = cursor.fetchone()[0]
52
+
53
+ # Determine embedding type
54
+ # Default embedding dim for all-MiniLM-L6-v2
55
+ embedding_dim = 384
56
+ embedding_type = f"VECTOR({embedding_dim})" if has_pgvector else "BYTEA"
57
+
58
+ # =====================================================================
59
+ # TABLE 1: Checkpoints - Crash recovery and state persistence
60
+ # =====================================================================
61
+ cursor.execute(f"""
62
+ CREATE TABLE IF NOT EXISTS {schema}.alma_checkpoints (
63
+ -- Primary key
64
+ id TEXT PRIMARY KEY,
65
+
66
+ -- Workflow context (required)
67
+ run_id TEXT NOT NULL,
68
+ node_id TEXT NOT NULL,
69
+
70
+ -- State data
71
+ state_json JSONB NOT NULL,
72
+ state_hash TEXT NOT NULL, -- SHA256 hash for change detection
73
+
74
+ -- Sequencing
75
+ sequence_number INTEGER NOT NULL,
76
+
77
+ -- Parallel execution support
78
+ branch_id TEXT, -- NULL for main branch
79
+ parent_checkpoint_id TEXT REFERENCES {schema}.alma_checkpoints(id),
80
+
81
+ -- Timestamps
82
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
83
+
84
+ -- Extensibility
85
+ metadata JSONB,
86
+
87
+ -- Constraints
88
+ CONSTRAINT uk_checkpoint_run_seq UNIQUE (run_id, sequence_number)
89
+ )
90
+ """)
91
+
92
+ # Indexes for checkpoint queries
93
+ cursor.execute(f"""
94
+ CREATE INDEX IF NOT EXISTS idx_checkpoints_run_seq
95
+ ON {schema}.alma_checkpoints(run_id, sequence_number DESC)
96
+ """)
97
+ cursor.execute(f"""
98
+ CREATE INDEX IF NOT EXISTS idx_checkpoints_run_branch
99
+ ON {schema}.alma_checkpoints(run_id, branch_id)
100
+ WHERE branch_id IS NOT NULL
101
+ """)
102
+ cursor.execute(f"""
103
+ CREATE INDEX IF NOT EXISTS idx_checkpoints_created
104
+ ON {schema}.alma_checkpoints(created_at)
105
+ """)
106
+
107
+ # Comment on table
108
+ cursor.execute(f"""
109
+ COMMENT ON TABLE {schema}.alma_checkpoints IS
110
+ 'Workflow state checkpoints for crash recovery. Each checkpoint captures state after a node completes.'
111
+ """)
112
+
113
+ # =====================================================================
114
+ # TABLE 2: Workflow Outcomes - Learning from completed workflows
115
+ # =====================================================================
116
+ cursor.execute(f"""
117
+ CREATE TABLE IF NOT EXISTS {schema}.alma_workflow_outcomes (
118
+ -- Primary key
119
+ id TEXT PRIMARY KEY,
120
+
121
+ -- Multi-tenant hierarchy
122
+ tenant_id TEXT NOT NULL DEFAULT 'default',
123
+ workflow_id TEXT NOT NULL,
124
+ workflow_version TEXT DEFAULT '1.0',
125
+ run_id TEXT NOT NULL UNIQUE, -- One outcome per run
126
+
127
+ -- Outcome data
128
+ success BOOLEAN NOT NULL,
129
+ duration_ms INTEGER NOT NULL,
130
+
131
+ -- Node statistics
132
+ node_count INTEGER NOT NULL,
133
+ nodes_succeeded INTEGER NOT NULL DEFAULT 0,
134
+ nodes_failed INTEGER NOT NULL DEFAULT 0,
135
+
136
+ -- Error tracking
137
+ error_message TEXT,
138
+
139
+ -- Artifacts (stored as JSON array of ArtifactRef)
140
+ artifacts_json JSONB,
141
+
142
+ -- Learning metrics
143
+ learnings_extracted INTEGER DEFAULT 0,
144
+
145
+ -- Timestamps
146
+ timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
147
+
148
+ -- Semantic search (pgvector or fallback)
149
+ embedding {embedding_type},
150
+
151
+ -- Extensibility
152
+ metadata JSONB,
153
+
154
+ -- Constraints
155
+ CONSTRAINT chk_nodes_count CHECK (
156
+ nodes_succeeded + nodes_failed <= node_count
157
+ )
158
+ )
159
+ """)
160
+
161
+ # Indexes for workflow outcome queries
162
+ cursor.execute(f"""
163
+ CREATE INDEX IF NOT EXISTS idx_wo_tenant
164
+ ON {schema}.alma_workflow_outcomes(tenant_id)
165
+ """)
166
+ cursor.execute(f"""
167
+ CREATE INDEX IF NOT EXISTS idx_wo_workflow
168
+ ON {schema}.alma_workflow_outcomes(tenant_id, workflow_id)
169
+ """)
170
+ cursor.execute(f"""
171
+ CREATE INDEX IF NOT EXISTS idx_wo_success
172
+ ON {schema}.alma_workflow_outcomes(tenant_id, success)
173
+ """)
174
+ cursor.execute(f"""
175
+ CREATE INDEX IF NOT EXISTS idx_wo_timestamp
176
+ ON {schema}.alma_workflow_outcomes(timestamp DESC)
177
+ """)
178
+
179
+ # pgvector index for semantic search (if available)
180
+ if has_pgvector:
181
+ try:
182
+ cursor.execute(f"""
183
+ CREATE INDEX IF NOT EXISTS idx_wo_embedding
184
+ ON {schema}.alma_workflow_outcomes
185
+ USING ivfflat (embedding vector_cosine_ops)
186
+ WITH (lists = 100)
187
+ """)
188
+ except Exception:
189
+ # IVFFlat requires data to build, skip if table is empty
190
+ pass
191
+
192
+ # Comment on table
193
+ cursor.execute(f"""
194
+ COMMENT ON TABLE {schema}.alma_workflow_outcomes IS
195
+ 'Aggregated outcomes from completed workflow runs. Used for learning patterns across workflows.'
196
+ """)
197
+
198
+ # =====================================================================
199
+ # TABLE 3: Artifact Links - Connecting artifacts to memories
200
+ # =====================================================================
201
+ cursor.execute(f"""
202
+ CREATE TABLE IF NOT EXISTS {schema}.alma_artifact_links (
203
+ -- Primary key
204
+ id TEXT PRIMARY KEY,
205
+
206
+ -- Link to memory item
207
+ memory_id TEXT NOT NULL,
208
+ memory_type TEXT NOT NULL, -- 'heuristic', 'outcome', 'domain_knowledge', etc.
209
+
210
+ -- Artifact reference
211
+ artifact_id TEXT NOT NULL,
212
+ artifact_type TEXT NOT NULL, -- 'screenshot', 'report', 'log', etc.
213
+
214
+ -- Storage location (Cloudflare R2)
215
+ storage_path TEXT NOT NULL, -- e.g., 'r2://alma-artifacts/tenant/workflow/artifact.png'
216
+
217
+ -- Integrity
218
+ content_hash TEXT NOT NULL, -- SHA256 for verification
219
+ size_bytes INTEGER NOT NULL,
220
+
221
+ -- Timestamps
222
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
223
+
224
+ -- Extensibility
225
+ metadata JSONB,
226
+
227
+ -- Constraints
228
+ CONSTRAINT chk_size_positive CHECK (size_bytes > 0)
229
+ )
230
+ """)
231
+
232
+ # Indexes for artifact queries
233
+ cursor.execute(f"""
234
+ CREATE INDEX IF NOT EXISTS idx_artifact_memory
235
+ ON {schema}.alma_artifact_links(memory_id, memory_type)
236
+ """)
237
+ cursor.execute(f"""
238
+ CREATE INDEX IF NOT EXISTS idx_artifact_type
239
+ ON {schema}.alma_artifact_links(artifact_type)
240
+ """)
241
+ cursor.execute(f"""
242
+ CREATE INDEX IF NOT EXISTS idx_artifact_created
243
+ ON {schema}.alma_artifact_links(created_at)
244
+ """)
245
+
246
+ # Comment on table
247
+ cursor.execute(f"""
248
+ COMMENT ON TABLE {schema}.alma_artifact_links IS
249
+ 'Links between memory items and external artifacts stored in Cloudflare R2.'
250
+ """)
251
+
252
+ # =====================================================================
253
+ # ALTER EXISTING TABLES: Add workflow scope columns
254
+ # =====================================================================
255
+
256
+ # Add workflow columns to heuristics (if they don't exist)
257
+ self._add_column_if_not_exists(
258
+ cursor, schema, "alma_heuristics", "tenant_id", "TEXT DEFAULT 'default'"
259
+ )
260
+ self._add_column_if_not_exists(
261
+ cursor, schema, "alma_heuristics", "workflow_id", "TEXT"
262
+ )
263
+ self._add_column_if_not_exists(
264
+ cursor, schema, "alma_heuristics", "run_id", "TEXT"
265
+ )
266
+ self._add_column_if_not_exists(
267
+ cursor, schema, "alma_heuristics", "node_id", "TEXT"
268
+ )
269
+
270
+ # Add workflow columns to outcomes
271
+ self._add_column_if_not_exists(
272
+ cursor, schema, "alma_outcomes", "tenant_id", "TEXT DEFAULT 'default'"
273
+ )
274
+ self._add_column_if_not_exists(
275
+ cursor, schema, "alma_outcomes", "workflow_id", "TEXT"
276
+ )
277
+ self._add_column_if_not_exists(
278
+ cursor, schema, "alma_outcomes", "run_id", "TEXT"
279
+ )
280
+ self._add_column_if_not_exists(
281
+ cursor, schema, "alma_outcomes", "node_id", "TEXT"
282
+ )
283
+
284
+ # Add workflow columns to domain_knowledge
285
+ self._add_column_if_not_exists(
286
+ cursor,
287
+ schema,
288
+ "alma_domain_knowledge",
289
+ "tenant_id",
290
+ "TEXT DEFAULT 'default'",
291
+ )
292
+ self._add_column_if_not_exists(
293
+ cursor, schema, "alma_domain_knowledge", "workflow_id", "TEXT"
294
+ )
295
+
296
+ # Add workflow columns to anti_patterns
297
+ self._add_column_if_not_exists(
298
+ cursor, schema, "alma_anti_patterns", "tenant_id", "TEXT DEFAULT 'default'"
299
+ )
300
+ self._add_column_if_not_exists(
301
+ cursor, schema, "alma_anti_patterns", "workflow_id", "TEXT"
302
+ )
303
+
304
+ # Add indexes for scope filtering
305
+ cursor.execute(f"""
306
+ CREATE INDEX IF NOT EXISTS idx_heuristics_tenant
307
+ ON {schema}.alma_heuristics(tenant_id)
308
+ WHERE tenant_id IS NOT NULL
309
+ """)
310
+ cursor.execute(f"""
311
+ CREATE INDEX IF NOT EXISTS idx_heuristics_workflow
312
+ ON {schema}.alma_heuristics(workflow_id)
313
+ WHERE workflow_id IS NOT NULL
314
+ """)
315
+ cursor.execute(f"""
316
+ CREATE INDEX IF NOT EXISTS idx_outcomes_tenant
317
+ ON {schema}.alma_outcomes(tenant_id)
318
+ WHERE tenant_id IS NOT NULL
319
+ """)
320
+ cursor.execute(f"""
321
+ CREATE INDEX IF NOT EXISTS idx_outcomes_workflow
322
+ ON {schema}.alma_outcomes(workflow_id)
323
+ WHERE workflow_id IS NOT NULL
324
+ """)
325
+
326
+ connection.commit()
327
+
328
+ def _add_column_if_not_exists(
329
+ self, cursor: Any, schema: str, table: str, column: str, definition: str
330
+ ) -> None:
331
+ """Safely add a column if it doesn't exist."""
332
+ cursor.execute(f"""
333
+ DO $$
334
+ BEGIN
335
+ IF NOT EXISTS (
336
+ SELECT 1 FROM information_schema.columns
337
+ WHERE table_schema = '{schema}'
338
+ AND table_name = '{table}'
339
+ AND column_name = '{column}'
340
+ ) THEN
341
+ ALTER TABLE {schema}.{table} ADD COLUMN {column} {definition};
342
+ END IF;
343
+ END
344
+ $$;
345
+ """)
346
+
347
+ def downgrade(self, connection: Any) -> None:
348
+ """Revert workflow context schema changes."""
349
+ cursor = connection.cursor()
350
+ schema = getattr(connection, "_schema", "public")
351
+
352
+ # Drop new tables (in reverse dependency order)
353
+ cursor.execute(f"DROP TABLE IF EXISTS {schema}.alma_artifact_links CASCADE")
354
+ cursor.execute(f"DROP TABLE IF EXISTS {schema}.alma_workflow_outcomes CASCADE")
355
+ cursor.execute(f"DROP TABLE IF EXISTS {schema}.alma_checkpoints CASCADE")
356
+
357
+ # Note: We don't remove columns from existing tables in downgrade
358
+ # as it could cause data loss. They are nullable and won't affect existing code.
359
+
360
+ connection.commit()
361
+
362
+
363
+ # =============================================================================
364
+ # SQLITE MIGRATIONS
365
+ # =============================================================================
366
+
367
+
368
+ @register_migration(backend="sqlite")
369
+ class SQLiteWorkflowContextMigration(Migration):
370
+ """
371
+ SQLite migration for workflow context layer.
372
+
373
+ Uses BLOB for embeddings (no pgvector equivalent in SQLite).
374
+ """
375
+
376
+ version = "1.1.0"
377
+ description = (
378
+ "Add workflow context layer (checkpoints, workflow_outcomes, artifact_links)"
379
+ )
380
+ depends_on = "1.0.0"
381
+
382
+ def upgrade(self, connection: Any) -> None:
383
+ """Apply workflow context schema changes."""
384
+ cursor = connection.cursor()
385
+
386
+ # =====================================================================
387
+ # TABLE 1: Checkpoints
388
+ # Matches alma.workflow.checkpoint.Checkpoint dataclass
389
+ # =====================================================================
390
+ cursor.execute("""
391
+ CREATE TABLE IF NOT EXISTS checkpoints (
392
+ id TEXT PRIMARY KEY,
393
+ run_id TEXT NOT NULL,
394
+ node_id TEXT NOT NULL,
395
+ state TEXT NOT NULL,
396
+ sequence_number INTEGER DEFAULT 0,
397
+ branch_id TEXT,
398
+ parent_checkpoint_id TEXT,
399
+ state_hash TEXT,
400
+ metadata TEXT,
401
+ created_at TEXT NOT NULL
402
+ )
403
+ """)
404
+
405
+ cursor.execute("""
406
+ CREATE INDEX IF NOT EXISTS idx_checkpoints_run
407
+ ON checkpoints(run_id)
408
+ """)
409
+ cursor.execute("""
410
+ CREATE INDEX IF NOT EXISTS idx_checkpoints_run_branch
411
+ ON checkpoints(run_id, branch_id)
412
+ """)
413
+ cursor.execute("""
414
+ CREATE INDEX IF NOT EXISTS idx_checkpoints_run_seq
415
+ ON checkpoints(run_id, sequence_number DESC)
416
+ """)
417
+
418
+ # =====================================================================
419
+ # TABLE 2: Workflow Outcomes
420
+ # Matches alma.workflow.outcomes.WorkflowOutcome dataclass
421
+ # =====================================================================
422
+ cursor.execute("""
423
+ CREATE TABLE IF NOT EXISTS workflow_outcomes (
424
+ id TEXT PRIMARY KEY,
425
+ tenant_id TEXT,
426
+ workflow_id TEXT NOT NULL,
427
+ run_id TEXT NOT NULL,
428
+ agent TEXT NOT NULL,
429
+ project_id TEXT NOT NULL,
430
+ result TEXT NOT NULL,
431
+ summary TEXT,
432
+ strategies_used TEXT,
433
+ successful_patterns TEXT,
434
+ failed_patterns TEXT,
435
+ extracted_heuristics TEXT,
436
+ extracted_anti_patterns TEXT,
437
+ duration_seconds REAL,
438
+ node_count INTEGER,
439
+ error_message TEXT,
440
+ metadata TEXT,
441
+ created_at TEXT NOT NULL
442
+ )
443
+ """)
444
+
445
+ cursor.execute("""
446
+ CREATE INDEX IF NOT EXISTS idx_wo_tenant
447
+ ON workflow_outcomes(tenant_id)
448
+ """)
449
+ cursor.execute("""
450
+ CREATE INDEX IF NOT EXISTS idx_wo_workflow
451
+ ON workflow_outcomes(workflow_id)
452
+ """)
453
+ cursor.execute("""
454
+ CREATE INDEX IF NOT EXISTS idx_wo_project_agent
455
+ ON workflow_outcomes(project_id, agent)
456
+ """)
457
+
458
+ # =====================================================================
459
+ # TABLE 3: Artifact Links
460
+ # Matches alma.workflow.artifacts.ArtifactRef dataclass
461
+ # =====================================================================
462
+ cursor.execute("""
463
+ CREATE TABLE IF NOT EXISTS artifact_links (
464
+ id TEXT PRIMARY KEY,
465
+ memory_id TEXT NOT NULL,
466
+ artifact_type TEXT NOT NULL,
467
+ storage_url TEXT NOT NULL,
468
+ filename TEXT,
469
+ mime_type TEXT,
470
+ size_bytes INTEGER,
471
+ checksum TEXT,
472
+ metadata TEXT,
473
+ created_at TEXT NOT NULL
474
+ )
475
+ """)
476
+
477
+ cursor.execute("""
478
+ CREATE INDEX IF NOT EXISTS idx_artifact_memory
479
+ ON artifact_links(memory_id)
480
+ """)
481
+
482
+ # =====================================================================
483
+ # ALTER EXISTING TABLES: Add workflow scope columns
484
+ # =====================================================================
485
+ # SQLite doesn't support IF NOT EXISTS for ALTER TABLE, so we need to check
486
+
487
+ existing_tables = [
488
+ "heuristics",
489
+ "outcomes",
490
+ "domain_knowledge",
491
+ "anti_patterns",
492
+ ]
493
+ new_columns = [
494
+ ("tenant_id", "TEXT DEFAULT 'default'"),
495
+ ("workflow_id", "TEXT"),
496
+ ("run_id", "TEXT"),
497
+ ("node_id", "TEXT"),
498
+ ]
499
+
500
+ for table in existing_tables:
501
+ # Check which columns already exist
502
+ cursor.execute(f"PRAGMA table_info({table})")
503
+ existing_cols = {row[1] for row in cursor.fetchall()}
504
+
505
+ # Only add run_id and node_id to heuristics and outcomes
506
+ cols_to_add = (
507
+ new_columns if table in ["heuristics", "outcomes"] else new_columns[:2]
508
+ )
509
+
510
+ for col_name, col_def in cols_to_add:
511
+ if col_name not in existing_cols:
512
+ try:
513
+ cursor.execute(
514
+ f"ALTER TABLE {table} ADD COLUMN {col_name} {col_def}"
515
+ )
516
+ except Exception:
517
+ pass # Column might already exist
518
+
519
+ # Add scope indexes
520
+ cursor.execute("""
521
+ CREATE INDEX IF NOT EXISTS idx_heuristics_tenant
522
+ ON heuristics(tenant_id)
523
+ """)
524
+ cursor.execute("""
525
+ CREATE INDEX IF NOT EXISTS idx_heuristics_workflow
526
+ ON heuristics(workflow_id)
527
+ """)
528
+ cursor.execute("""
529
+ CREATE INDEX IF NOT EXISTS idx_outcomes_tenant
530
+ ON outcomes(tenant_id)
531
+ """)
532
+ cursor.execute("""
533
+ CREATE INDEX IF NOT EXISTS idx_outcomes_workflow
534
+ ON outcomes(workflow_id)
535
+ """)
536
+
537
+ connection.commit()
538
+
539
+ def downgrade(self, connection: Any) -> None:
540
+ """Revert workflow context schema changes."""
541
+ cursor = connection.cursor()
542
+
543
+ # Drop new tables
544
+ cursor.execute("DROP TABLE IF EXISTS artifact_links")
545
+ cursor.execute("DROP TABLE IF EXISTS workflow_outcomes")
546
+ cursor.execute("DROP TABLE IF EXISTS checkpoints")
547
+
548
+ # Note: SQLite doesn't support DROP COLUMN easily
549
+ # The added columns will remain but won't affect existing code
550
+
551
+ connection.commit()