@jungjaehoon/mama-core 2.1.1 → 2.2.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.
@@ -0,0 +1,114 @@
1
+ ALTER TABLE connector_event_index
2
+ ADD COLUMN operator_observation_seq INTEGER CHECK (
3
+ operator_observation_seq IS NULL OR operator_observation_seq >= 1
4
+ );
5
+
6
+ CREATE TABLE IF NOT EXISTS connector_event_index_observation_cursors (
7
+ source_connector TEXT PRIMARY KEY,
8
+ next_seq INTEGER NOT NULL CHECK (next_seq >= 1)
9
+ );
10
+
11
+ WITH ranked AS (
12
+ SELECT event_index_id,
13
+ ROW_NUMBER() OVER (
14
+ PARTITION BY source_connector
15
+ ORDER BY source_timestamp_ms, rowid
16
+ ) AS seq
17
+ FROM connector_event_index
18
+ )
19
+ UPDATE connector_event_index
20
+ SET operator_observation_seq = (
21
+ SELECT seq FROM ranked WHERE ranked.event_index_id = connector_event_index.event_index_id
22
+ );
23
+
24
+ INSERT INTO connector_event_index_observation_cursors (source_connector, next_seq)
25
+ SELECT source_connector, MAX(operator_observation_seq) + 1
26
+ FROM connector_event_index
27
+ GROUP BY source_connector
28
+ ON CONFLICT(source_connector) DO UPDATE SET next_seq = CASE
29
+ WHEN connector_event_index_observation_cursors.next_seq < excluded.next_seq
30
+ THEN excluded.next_seq
31
+ ELSE connector_event_index_observation_cursors.next_seq
32
+ END;
33
+
34
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_connector_event_index_observation_seq
35
+ ON connector_event_index(source_connector, operator_observation_seq)
36
+ WHERE operator_observation_seq IS NOT NULL;
37
+
38
+ CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_operator_ingest_seq_au
39
+ AFTER UPDATE OF operator_ingest_seq ON connector_event_index
40
+ WHEN NEW.operator_ingest_seq IS NULL AND OLD.operator_ingest_seq IS NOT NULL
41
+ BEGIN
42
+ INSERT INTO connector_event_index_operator_seq_cursors
43
+ (source_connector, channel, next_seq)
44
+ VALUES (NEW.source_connector, COALESCE(NEW.channel, ''), 1)
45
+ ON CONFLICT(source_connector, channel) DO NOTHING;
46
+ UPDATE connector_event_index
47
+ SET operator_ingest_seq = (
48
+ SELECT next_seq FROM connector_event_index_operator_seq_cursors
49
+ WHERE source_connector = NEW.source_connector
50
+ AND channel = COALESCE(NEW.channel, '')
51
+ )
52
+ WHERE event_index_id = NEW.event_index_id;
53
+ UPDATE connector_event_index_operator_seq_cursors
54
+ SET next_seq = next_seq + 1
55
+ WHERE source_connector = NEW.source_connector
56
+ AND channel = COALESCE(NEW.channel, '');
57
+ END;
58
+
59
+ CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_observation_seq_ai
60
+ AFTER INSERT ON connector_event_index
61
+ WHEN NEW.operator_observation_seq IS NULL
62
+ BEGIN
63
+ INSERT INTO connector_event_index_observation_cursors
64
+ (source_connector, next_seq)
65
+ VALUES (NEW.source_connector, 1)
66
+ ON CONFLICT(source_connector) DO NOTHING;
67
+ UPDATE connector_event_index
68
+ SET operator_observation_seq = (
69
+ SELECT next_seq FROM connector_event_index_observation_cursors
70
+ WHERE source_connector = NEW.source_connector
71
+ )
72
+ WHERE event_index_id = NEW.event_index_id;
73
+ UPDATE connector_event_index_observation_cursors
74
+ SET next_seq = next_seq + 1
75
+ WHERE source_connector = NEW.source_connector;
76
+ END;
77
+
78
+ CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_observation_seq_au
79
+ AFTER UPDATE OF operator_observation_seq ON connector_event_index
80
+ WHEN NEW.operator_observation_seq IS NULL AND OLD.operator_observation_seq IS NOT NULL
81
+ BEGIN
82
+ INSERT INTO connector_event_index_observation_cursors
83
+ (source_connector, next_seq)
84
+ VALUES (NEW.source_connector, 1)
85
+ ON CONFLICT(source_connector) DO NOTHING;
86
+ UPDATE connector_event_index
87
+ SET operator_observation_seq = (
88
+ SELECT next_seq FROM connector_event_index_observation_cursors
89
+ WHERE source_connector = NEW.source_connector
90
+ )
91
+ WHERE event_index_id = NEW.event_index_id;
92
+ UPDATE connector_event_index_observation_cursors
93
+ SET next_seq = next_seq + 1
94
+ WHERE source_connector = NEW.source_connector;
95
+ END;
96
+
97
+ CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_observation_seq_explicit_ai
98
+ AFTER INSERT ON connector_event_index
99
+ WHEN NEW.operator_observation_seq IS NOT NULL
100
+ BEGIN
101
+ INSERT INTO connector_event_index_observation_cursors
102
+ (source_connector, next_seq)
103
+ VALUES (NEW.source_connector, 1)
104
+ ON CONFLICT(source_connector) DO NOTHING;
105
+ UPDATE connector_event_index_observation_cursors
106
+ SET next_seq = CASE
107
+ WHEN next_seq <= NEW.operator_observation_seq THEN NEW.operator_observation_seq + 1
108
+ ELSE next_seq
109
+ END
110
+ WHERE source_connector = NEW.source_connector;
111
+ END;
112
+
113
+ INSERT INTO schema_version (version, description)
114
+ VALUES (62, 'Refresh connector event delivery and observation sequences');
@@ -0,0 +1,33 @@
1
+ SELECT content_hash, metadata_json, source_timestamp_ms, source_type, channel,
2
+ operator_ingest_seq, operator_observation_seq
3
+ FROM connector_event_index
4
+ LIMIT 0;
5
+
6
+ CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_legacy_content_refresh_au
7
+ AFTER UPDATE OF content_hash, metadata_json, source_timestamp_ms, source_type, channel
8
+ ON connector_event_index
9
+ WHEN (
10
+ OLD.content_hash IS NOT NEW.content_hash
11
+ OR OLD.metadata_json IS NOT NEW.metadata_json
12
+ OR OLD.source_timestamp_ms IS NOT NEW.source_timestamp_ms
13
+ OR OLD.source_type IS NOT NEW.source_type
14
+ OR OLD.channel IS NOT NEW.channel
15
+ ) AND (
16
+ NEW.operator_ingest_seq IS OLD.operator_ingest_seq
17
+ OR NEW.operator_observation_seq IS OLD.operator_observation_seq
18
+ )
19
+ BEGIN
20
+ UPDATE connector_event_index
21
+ SET operator_ingest_seq = CASE
22
+ WHEN NEW.operator_ingest_seq IS OLD.operator_ingest_seq THEN NULL
23
+ ELSE NEW.operator_ingest_seq
24
+ END,
25
+ operator_observation_seq = CASE
26
+ WHEN NEW.operator_observation_seq IS OLD.operator_observation_seq THEN NULL
27
+ ELSE NEW.operator_observation_seq
28
+ END
29
+ WHERE event_index_id = NEW.event_index_id;
30
+ END;
31
+
32
+ INSERT OR IGNORE INTO schema_version (version, description)
33
+ VALUES (63, 'Refresh sequences for legacy connector event updates');
@@ -0,0 +1,27 @@
1
+ CREATE TABLE IF NOT EXISTS principals (
2
+ principal_id TEXT PRIMARY KEY,
3
+ kind TEXT NOT NULL CHECK (kind IN ('owner', 'member')),
4
+ display_name TEXT,
5
+ status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'suspended', 'offboarded')),
6
+ created_at INTEGER NOT NULL,
7
+ updated_at INTEGER NOT NULL
8
+ );
9
+
10
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_principals_at_most_one_owner
11
+ ON principals(kind)
12
+ WHERE kind = 'owner' AND status = 'active';
13
+
14
+ CREATE TABLE IF NOT EXISTS external_identities (
15
+ connector TEXT NOT NULL,
16
+ namespace TEXT NOT NULL,
17
+ external_id TEXT NOT NULL,
18
+ principal_id TEXT NOT NULL REFERENCES principals(principal_id),
19
+ created_at INTEGER NOT NULL,
20
+ PRIMARY KEY (connector, namespace, external_id)
21
+ );
22
+
23
+ CREATE INDEX IF NOT EXISTS idx_external_identities_principal
24
+ ON external_identities(principal_id);
25
+
26
+ INSERT OR IGNORE INTO schema_version (version, description)
27
+ VALUES (64, 'Create principals and external_identities for member registry');
@@ -77,6 +77,8 @@ function mapConnectorEventIndexRow(row) {
77
77
  artifact_locator: row.artifact_locator === null ? null : String(row.artifact_locator),
78
78
  artifact_title: row.artifact_title === null ? null : String(row.artifact_title),
79
79
  content_hash: toBuffer(row.content_hash),
80
+ operator_ingest_seq: Number(row.operator_ingest_seq),
81
+ operator_observation_seq: Number(row.operator_observation_seq),
80
82
  indexed_at: String(row.indexed_at),
81
83
  updated_at: String(row.updated_at),
82
84
  expires_at: row.expires_at === null ? null : String(row.expires_at),
@@ -160,7 +162,25 @@ function upsertConnectorEventIndex(adapter, input) {
160
162
  artifact_title = excluded.artifact_title,
161
163
  content_hash = excluded.content_hash,
162
164
  updated_at = excluded.updated_at,
163
- expires_at = excluded.expires_at
165
+ expires_at = excluded.expires_at,
166
+ operator_ingest_seq = CASE
167
+ WHEN connector_event_index.content_hash IS NOT excluded.content_hash
168
+ OR connector_event_index.metadata_json IS NOT excluded.metadata_json
169
+ OR connector_event_index.source_timestamp_ms IS NOT excluded.source_timestamp_ms
170
+ OR connector_event_index.source_type IS NOT excluded.source_type
171
+ OR connector_event_index.channel IS NOT excluded.channel
172
+ THEN NULL
173
+ ELSE connector_event_index.operator_ingest_seq
174
+ END,
175
+ operator_observation_seq = CASE
176
+ WHEN connector_event_index.content_hash IS NOT excluded.content_hash
177
+ OR connector_event_index.metadata_json IS NOT excluded.metadata_json
178
+ OR connector_event_index.source_timestamp_ms IS NOT excluded.source_timestamp_ms
179
+ OR connector_event_index.source_type IS NOT excluded.source_type
180
+ OR connector_event_index.channel IS NOT excluded.channel
181
+ THEN NULL
182
+ ELSE connector_event_index.operator_observation_seq
183
+ END
164
184
  `)
165
185
  .run(eventIndexId, input.source_connector, input.source_type, input.source_id, input.source_locator ?? null, input.channel ?? null, input.author ?? null, input.title ?? null, input.content, eventDatetime, eventDate, sourceTimestampMs, input.source_cursor ?? null, input.tenant_id ?? null, input.project_id ?? null, input.memory_scope_kind ?? null, input.memory_scope_id ?? null, metadataJson, input.artifact_locator ?? null, input.artifact_title ?? null, contentHash, input.indexed_at ?? timestamp, timestamp, input.expires_at ?? null);
166
186
  const saved = getConnectorEventIndexRecord(adapter, input.source_connector, input.source_id);
@@ -22,6 +22,8 @@ export interface ConnectorEventIndexRecord {
22
22
  artifact_locator: string | null;
23
23
  artifact_title: string | null;
24
24
  content_hash: Buffer;
25
+ operator_ingest_seq: number;
26
+ operator_observation_seq: number;
25
27
  indexed_at: string;
26
28
  updated_at: string;
27
29
  expires_at: string | null;
@@ -72,6 +72,9 @@ export declare class NodeSQLiteAdapter extends DatabaseAdapter {
72
72
  private assertMigration034Complete;
73
73
  private recoverConnectorEventOperatorSeqMigration039;
74
74
  private assertMigration039Complete;
75
+ private recoverConnectorEventSequencesMigration062;
76
+ private assertMigration062Complete;
77
+ private recoverConnectorEventLegacyRefreshMigration063;
75
78
  private tableColumns;
76
79
  private tableExists;
77
80
  private indexExists;
@@ -404,6 +404,20 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
404
404
  (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} recovered successfully`);
405
405
  continue;
406
406
  }
407
+ if (message.includes('duplicate column') && version === 62) {
408
+ this.recoverConnectorEventSequencesMigration062();
409
+ (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} recovered successfully`);
410
+ continue;
411
+ }
412
+ if (message.includes('no such column') && version === 62) {
413
+ (0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} deferred until connector structure recovery (${message})`);
414
+ continue;
415
+ }
416
+ if (version === 63 &&
417
+ (message.includes('no such column') || message.includes('no such table'))) {
418
+ (0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} deferred until connector structure recovery (${message})`);
419
+ continue;
420
+ }
407
421
  if (message.includes('duplicate column')) {
408
422
  (0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} skipped (duplicate column - already applied)`);
409
423
  this.prepare('INSERT OR IGNORE INTO schema_version (version) VALUES (?)').run(version);
@@ -480,6 +494,27 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
480
494
  this.recoverConnectorEventOperatorSeqMigration039();
481
495
  (0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped connector event operator sequence migration');
482
496
  }
497
+ const connectorColumnsAfterOperatorSeqRepair = this.tableColumns('connector_event_index');
498
+ if (connectorColumnsAfterOperatorSeqRepair.has('source_timestamp_ms')) {
499
+ const hasMissingObservationSeqFeature = !connectorColumnsAfterOperatorSeqRepair.has('operator_observation_seq') ||
500
+ !this.tableExists('connector_event_index_observation_cursors') ||
501
+ !this.indexExists('idx_connector_event_index_observation_seq') ||
502
+ !this.triggerExists('trg_connector_event_index_operator_ingest_seq_au') ||
503
+ !this.triggerExists('trg_connector_event_index_observation_seq_ai') ||
504
+ !this.triggerExists('trg_connector_event_index_observation_seq_au') ||
505
+ !this.triggerExists('trg_connector_event_index_observation_seq_explicit_ai') ||
506
+ !this.schemaVersionExists(62);
507
+ if (hasMissingObservationSeqFeature) {
508
+ this.recoverConnectorEventSequencesMigration062();
509
+ (0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped connector event observation sequence migration');
510
+ }
511
+ const hasMissingLegacyRefreshFeature = !this.triggerExists('trg_connector_event_index_legacy_content_refresh_au') ||
512
+ !this.schemaVersionExists(63);
513
+ if (hasMissingLegacyRefreshFeature) {
514
+ this.recoverConnectorEventLegacyRefreshMigration063();
515
+ (0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped legacy connector event refresh migration');
516
+ }
517
+ }
483
518
  }
484
519
  if (!this.tableExists('twin_edges')) {
485
520
  this.applyRepairMigration(migrationsDir, '035-create-twin-edges.sql', 'twin edge ledger');
@@ -844,6 +879,226 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
844
879
  }
845
880
  }
846
881
  }
882
+ recoverConnectorEventSequencesMigration062() {
883
+ this.transaction(() => {
884
+ const columns = this.tableColumns('connector_event_index');
885
+ if (!columns.has('operator_observation_seq')) {
886
+ this.exec(`
887
+ ALTER TABLE connector_event_index
888
+ ADD COLUMN operator_observation_seq INTEGER CHECK (
889
+ operator_observation_seq IS NULL OR operator_observation_seq >= 1
890
+ )
891
+ `);
892
+ }
893
+ this.exec(`
894
+ CREATE TABLE IF NOT EXISTS connector_event_index_observation_cursors (
895
+ source_connector TEXT PRIMARY KEY,
896
+ next_seq INTEGER NOT NULL CHECK (next_seq >= 1)
897
+ )
898
+ `);
899
+ this.exec(`
900
+ WITH existing_max AS (
901
+ SELECT source_connector, MAX(operator_observation_seq) AS max_seq
902
+ FROM connector_event_index
903
+ WHERE operator_observation_seq IS NOT NULL
904
+ GROUP BY source_connector
905
+ ), ranked_nulls AS (
906
+ SELECT event_index_id,
907
+ source_connector,
908
+ ROW_NUMBER() OVER (
909
+ PARTITION BY source_connector
910
+ ORDER BY source_timestamp_ms, rowid
911
+ ) AS seq
912
+ FROM connector_event_index
913
+ WHERE operator_observation_seq IS NULL
914
+ )
915
+ UPDATE connector_event_index
916
+ SET operator_observation_seq = (
917
+ SELECT COALESCE(existing_max.max_seq, 0) + ranked_nulls.seq
918
+ FROM ranked_nulls
919
+ LEFT JOIN existing_max
920
+ ON existing_max.source_connector = ranked_nulls.source_connector
921
+ WHERE ranked_nulls.event_index_id = connector_event_index.event_index_id
922
+ )
923
+ WHERE operator_observation_seq IS NULL
924
+ `);
925
+ this.exec(`
926
+ INSERT INTO connector_event_index_observation_cursors (source_connector, next_seq)
927
+ SELECT source_connector, MAX(operator_observation_seq) + 1
928
+ FROM connector_event_index
929
+ GROUP BY source_connector
930
+ ON CONFLICT(source_connector) DO UPDATE SET next_seq = CASE
931
+ WHEN connector_event_index_observation_cursors.next_seq < excluded.next_seq
932
+ THEN excluded.next_seq
933
+ ELSE connector_event_index_observation_cursors.next_seq
934
+ END
935
+ `);
936
+ this.exec(`
937
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_connector_event_index_observation_seq
938
+ ON connector_event_index(source_connector, operator_observation_seq)
939
+ WHERE operator_observation_seq IS NOT NULL
940
+ `);
941
+ this.exec(`
942
+ CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_operator_ingest_seq_au
943
+ AFTER UPDATE OF operator_ingest_seq ON connector_event_index
944
+ WHEN NEW.operator_ingest_seq IS NULL AND OLD.operator_ingest_seq IS NOT NULL
945
+ BEGIN
946
+ INSERT INTO connector_event_index_operator_seq_cursors
947
+ (source_connector, channel, next_seq)
948
+ VALUES (NEW.source_connector, COALESCE(NEW.channel, ''), 1)
949
+ ON CONFLICT(source_connector, channel) DO NOTHING;
950
+ UPDATE connector_event_index
951
+ SET operator_ingest_seq = (
952
+ SELECT next_seq FROM connector_event_index_operator_seq_cursors
953
+ WHERE source_connector = NEW.source_connector
954
+ AND channel = COALESCE(NEW.channel, '')
955
+ )
956
+ WHERE event_index_id = NEW.event_index_id;
957
+ UPDATE connector_event_index_operator_seq_cursors
958
+ SET next_seq = next_seq + 1
959
+ WHERE source_connector = NEW.source_connector
960
+ AND channel = COALESCE(NEW.channel, '');
961
+ END
962
+ `);
963
+ this.exec(`
964
+ CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_observation_seq_ai
965
+ AFTER INSERT ON connector_event_index
966
+ WHEN NEW.operator_observation_seq IS NULL
967
+ BEGIN
968
+ INSERT INTO connector_event_index_observation_cursors
969
+ (source_connector, next_seq)
970
+ VALUES (NEW.source_connector, 1)
971
+ ON CONFLICT(source_connector) DO NOTHING;
972
+ UPDATE connector_event_index
973
+ SET operator_observation_seq = (
974
+ SELECT next_seq FROM connector_event_index_observation_cursors
975
+ WHERE source_connector = NEW.source_connector
976
+ )
977
+ WHERE event_index_id = NEW.event_index_id;
978
+ UPDATE connector_event_index_observation_cursors
979
+ SET next_seq = next_seq + 1
980
+ WHERE source_connector = NEW.source_connector;
981
+ END
982
+ `);
983
+ this.exec(`
984
+ CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_observation_seq_au
985
+ AFTER UPDATE OF operator_observation_seq ON connector_event_index
986
+ WHEN NEW.operator_observation_seq IS NULL AND OLD.operator_observation_seq IS NOT NULL
987
+ BEGIN
988
+ INSERT INTO connector_event_index_observation_cursors
989
+ (source_connector, next_seq)
990
+ VALUES (NEW.source_connector, 1)
991
+ ON CONFLICT(source_connector) DO NOTHING;
992
+ UPDATE connector_event_index
993
+ SET operator_observation_seq = (
994
+ SELECT next_seq FROM connector_event_index_observation_cursors
995
+ WHERE source_connector = NEW.source_connector
996
+ )
997
+ WHERE event_index_id = NEW.event_index_id;
998
+ UPDATE connector_event_index_observation_cursors
999
+ SET next_seq = next_seq + 1
1000
+ WHERE source_connector = NEW.source_connector;
1001
+ END
1002
+ `);
1003
+ this.exec(`
1004
+ CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_observation_seq_explicit_ai
1005
+ AFTER INSERT ON connector_event_index
1006
+ WHEN NEW.operator_observation_seq IS NOT NULL
1007
+ BEGIN
1008
+ INSERT INTO connector_event_index_observation_cursors
1009
+ (source_connector, next_seq)
1010
+ VALUES (NEW.source_connector, 1)
1011
+ ON CONFLICT(source_connector) DO NOTHING;
1012
+ UPDATE connector_event_index_observation_cursors
1013
+ SET next_seq = CASE
1014
+ WHEN next_seq <= NEW.operator_observation_seq THEN NEW.operator_observation_seq + 1
1015
+ ELSE next_seq
1016
+ END
1017
+ WHERE source_connector = NEW.source_connector;
1018
+ END
1019
+ `);
1020
+ this.assertMigration062Complete();
1021
+ this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(62, 'Refresh connector event delivery and observation sequences');
1022
+ });
1023
+ }
1024
+ assertMigration062Complete() {
1025
+ const columns = this.tableColumns('connector_event_index');
1026
+ if (!columns.has('operator_observation_seq')) {
1027
+ throw new Error('Migration 062 recovery failed: missing connector_event_index.operator_observation_seq');
1028
+ }
1029
+ if (!this.tableExists('connector_event_index_observation_cursors')) {
1030
+ throw new Error('Migration 062 recovery failed: missing connector_event_index_observation_cursors');
1031
+ }
1032
+ const cursorColumns = this.tableColumns('connector_event_index_observation_cursors');
1033
+ for (const columnName of ['source_connector', 'next_seq']) {
1034
+ if (!cursorColumns.has(columnName)) {
1035
+ throw new Error(`Migration 062 recovery failed: missing connector_event_index_observation_cursors.${columnName}`);
1036
+ }
1037
+ }
1038
+ if (!this.indexExists('idx_connector_event_index_observation_seq')) {
1039
+ throw new Error('Migration 062 recovery failed: missing index idx_connector_event_index_observation_seq');
1040
+ }
1041
+ for (const triggerName of [
1042
+ 'trg_connector_event_index_operator_ingest_seq_au',
1043
+ 'trg_connector_event_index_observation_seq_ai',
1044
+ 'trg_connector_event_index_observation_seq_au',
1045
+ 'trg_connector_event_index_observation_seq_explicit_ai',
1046
+ ]) {
1047
+ if (!this.triggerExists(triggerName)) {
1048
+ throw new Error(`Migration 062 recovery failed: missing trigger ${triggerName}`);
1049
+ }
1050
+ }
1051
+ }
1052
+ recoverConnectorEventLegacyRefreshMigration063() {
1053
+ this.transaction(() => {
1054
+ this.assertMigration062Complete();
1055
+ const connectorColumns = this.tableColumns('connector_event_index');
1056
+ for (const columnName of [
1057
+ 'content_hash',
1058
+ 'metadata_json',
1059
+ 'source_timestamp_ms',
1060
+ 'source_type',
1061
+ 'channel',
1062
+ 'operator_ingest_seq',
1063
+ 'operator_observation_seq',
1064
+ ]) {
1065
+ if (!connectorColumns.has(columnName)) {
1066
+ throw new Error(`Migration 063 recovery failed: missing connector_event_index.${columnName}`);
1067
+ }
1068
+ }
1069
+ this.exec(`
1070
+ CREATE TRIGGER IF NOT EXISTS trg_connector_event_index_legacy_content_refresh_au
1071
+ AFTER UPDATE OF content_hash, metadata_json, source_timestamp_ms, source_type, channel
1072
+ ON connector_event_index
1073
+ WHEN (
1074
+ OLD.content_hash IS NOT NEW.content_hash
1075
+ OR OLD.metadata_json IS NOT NEW.metadata_json
1076
+ OR OLD.source_timestamp_ms IS NOT NEW.source_timestamp_ms
1077
+ OR OLD.source_type IS NOT NEW.source_type
1078
+ OR OLD.channel IS NOT NEW.channel
1079
+ ) AND (
1080
+ NEW.operator_ingest_seq IS OLD.operator_ingest_seq
1081
+ OR NEW.operator_observation_seq IS OLD.operator_observation_seq
1082
+ )
1083
+ BEGIN
1084
+ UPDATE connector_event_index
1085
+ SET operator_ingest_seq = CASE
1086
+ WHEN NEW.operator_ingest_seq IS OLD.operator_ingest_seq THEN NULL
1087
+ ELSE NEW.operator_ingest_seq
1088
+ END,
1089
+ operator_observation_seq = CASE
1090
+ WHEN NEW.operator_observation_seq IS OLD.operator_observation_seq THEN NULL
1091
+ ELSE NEW.operator_observation_seq
1092
+ END
1093
+ WHERE event_index_id = NEW.event_index_id;
1094
+ END
1095
+ `);
1096
+ if (!this.triggerExists('trg_connector_event_index_legacy_content_refresh_au')) {
1097
+ throw new Error('Migration 063 recovery failed: missing trigger trg_connector_event_index_legacy_content_refresh_au');
1098
+ }
1099
+ this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(63, 'Refresh sequences for legacy connector event updates');
1100
+ });
1101
+ }
847
1102
  tableColumns(tableName) {
848
1103
  if (!SQLITE_IDENTIFIER_PATTERN.test(tableName)) {
849
1104
  throw new Error('Invalid SQLite table identifier');
@@ -32,9 +32,9 @@ exports.SHUTDOWN_TOKEN = process.env.MAMA_SHUTDOWN_TOKEN || crypto_1.default.ran
32
32
  const embeddings_js_1 = require("../embeddings.js");
33
33
  const config_loader_js_1 = require("../config-loader.js");
34
34
  const db_manager_js_1 = require("../db-manager.js");
35
- // Import Session API handler (Mobile Chat)
35
+ // Import Session API handler (chat session API)
36
36
  const session_api_js_1 = require("./mobile/session-api.js");
37
- // Import WebSocket handler (Mobile Chat - V2 with MessageRouter)
37
+ // Import WebSocket handler (chat WebSocket API - V2 with MessageRouter)
38
38
  const websocket_handler_js_1 = require("./mobile/websocket-handler.js");
39
39
  // Import Session Manager
40
40
  const session_manager_js_1 = require("./mobile/session-manager.js");
@@ -95,7 +95,7 @@ async function handleRequest(req, res) {
95
95
  // Security Note: CORS '*' is safe here because:
96
96
  // 1. Server binds to localhost only (127.0.0.1)
97
97
  // 2. No sensitive data exposed (user's own decisions)
98
- // 3. Required for browser-based Graph Viewer
98
+ // 3. Required for the browser-based Viewer
99
99
  res.setHeader('Content-Type', 'application/json');
100
100
  res.setHeader('Access-Control-Allow-Origin', '*');
101
101
  // Health check
@@ -212,7 +212,7 @@ async function handleRequest(req, res) {
212
212
  }
213
213
  return;
214
214
  }
215
- // Session API routes (Mobile Chat)
215
+ // Session API routes (chat session API)
216
216
  try {
217
217
  const sessionHandled = await sessionHandler(req, res);
218
218
  if (sessionHandled) {
@@ -257,7 +257,7 @@ async function handleRequest(req, res) {
257
257
  <body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #1a1a2e; color: #eee;">
258
258
  <div style="text-align: center; padding: 2rem;">
259
259
  <h1>🧠 MAMA Viewer</h1>
260
- <p>Viewer and Chat features are only available in <strong>Standalone</strong> mode.</p>
260
+ <p>The Viewer is only available in <strong>Standalone</strong> mode.</p>
261
261
  <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>
262
262
  <p style="color: #666; font-size: 0.9rem; margin-top: 2rem;">Current: MCP Server (embedding only)</p>
263
263
  </div>
@@ -313,7 +313,7 @@ async function startEmbeddingServer(port = exports.DEFAULT_PORT, options = {}) {
313
313
  // Check if HTTP server is disabled
314
314
  if (process.env.MAMA_DISABLE_HTTP_SERVER === 'true') {
315
315
  console.error('[EmbeddingHTTP] HTTP server disabled via MAMA_DISABLE_HTTP_SERVER');
316
- console.error('[EmbeddingHTTP] Graph Viewer and Mobile Chat will not be available');
316
+ console.error('[EmbeddingHTTP] The Viewer and the chat WebSocket API will not be available');
317
317
  return null;
318
318
  }
319
319
  // Initialize database for graph API
@@ -0,0 +1,37 @@
1
+ import type { DatabaseAdapter as DBManagerAdapter } from '../db-manager.js';
2
+ export interface PrincipalRow {
3
+ principalId: string;
4
+ kind: 'owner' | 'member';
5
+ status: 'active' | 'suspended' | 'offboarded';
6
+ }
7
+ export interface PrincipalRepository {
8
+ resolveByExternal(connector: string, namespace: string, externalId: string): PrincipalRow | null;
9
+ registerMember(input: {
10
+ displayName?: string;
11
+ connector: string;
12
+ namespace: string;
13
+ externalId: string;
14
+ now: number;
15
+ }): string;
16
+ bindIdentity(principalId: string, connector: string, namespace: string, externalId: string, now: number): void;
17
+ suspend(principalId: string, now: number): void;
18
+ offboard(principalId: string, now: number): void;
19
+ ensureOwner(input: {
20
+ connector: string;
21
+ namespace: string;
22
+ externalId: string;
23
+ now: number;
24
+ }): 'created' | 'exists' | 'conflict';
25
+ listMembers(): Array<{
26
+ principalId: string;
27
+ displayName?: string;
28
+ status: string;
29
+ }>;
30
+ }
31
+ export type PrincipalRegistrationErrorCode = 'identity_bound_to_owner' | 'member_not_active';
32
+ export declare class PrincipalRegistrationError extends Error {
33
+ readonly code: PrincipalRegistrationErrorCode;
34
+ constructor(code: PrincipalRegistrationErrorCode, message: string);
35
+ }
36
+ export declare function createPrincipalRepository(adapter: Pick<DBManagerAdapter, 'prepare' | 'transaction'>): PrincipalRepository;
37
+ //# sourceMappingURL=principal-repository.d.ts.map
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PrincipalRegistrationError = void 0;
4
+ exports.createPrincipalRepository = createPrincipalRepository;
5
+ const node_crypto_1 = require("node:crypto");
6
+ class PrincipalRegistrationError extends Error {
7
+ code;
8
+ constructor(code, message) {
9
+ super(message);
10
+ this.name = 'PrincipalRegistrationError';
11
+ this.code = code;
12
+ }
13
+ }
14
+ exports.PrincipalRegistrationError = PrincipalRegistrationError;
15
+ function mintPrincipalId(kind, connector, namespace, externalId) {
16
+ const identityKey = JSON.stringify([kind, connector, namespace, externalId]);
17
+ const digest = (0, node_crypto_1.createHash)('sha256').update(identityKey, 'utf8').digest('hex');
18
+ return `principal_${digest}`;
19
+ }
20
+ function mapPrincipal(row) {
21
+ return {
22
+ principalId: row.principal_id,
23
+ kind: row.kind,
24
+ status: row.status,
25
+ };
26
+ }
27
+ function createPrincipalRepository(adapter) {
28
+ const resolveByExternalStatement = adapter.prepare(`SELECT p.principal_id, p.kind, p.status
29
+ FROM external_identities AS e
30
+ JOIN principals AS p ON p.principal_id = e.principal_id
31
+ WHERE e.connector = ? AND e.namespace = ? AND e.external_id = ?`);
32
+ const insertPrincipalStatement = adapter.prepare(`INSERT INTO principals (
33
+ principal_id, kind, display_name, status, created_at, updated_at
34
+ ) VALUES (?, ?, ?, 'active', ?, ?)`);
35
+ const insertIdentityStatement = adapter.prepare(`INSERT INTO external_identities (
36
+ connector, namespace, external_id, principal_id, created_at
37
+ ) VALUES (?, ?, ?, ?, ?)`);
38
+ const selectPrincipalKindStatement = adapter.prepare('SELECT kind FROM principals WHERE principal_id = ?');
39
+ const updatePrincipalStatusStatement = adapter.prepare('UPDATE principals SET status = ?, updated_at = ? WHERE principal_id = ?');
40
+ const selectActiveOwnerStatement = adapter.prepare("SELECT principal_id FROM principals WHERE kind = 'owner' AND status = 'active' LIMIT 1");
41
+ const listMembersStatement = adapter.prepare(`SELECT principal_id, display_name, status
42
+ FROM principals
43
+ WHERE kind = 'member'
44
+ ORDER BY created_at ASC, principal_id ASC`);
45
+ function resolveByExternal(connector, namespace, externalId) {
46
+ const row = resolveByExternalStatement.get(connector, namespace, externalId);
47
+ return row ? mapPrincipal(row) : null;
48
+ }
49
+ function bindIdentity(principalId, connector, namespace, externalId, now) {
50
+ insertIdentityStatement.run(connector, namespace, externalId, principalId, now);
51
+ }
52
+ function registerMember(input) {
53
+ const existing = resolveByExternal(input.connector, input.namespace, input.externalId);
54
+ if (existing) {
55
+ if (existing.kind === 'member' && existing.status === 'active') {
56
+ return existing.principalId;
57
+ }
58
+ if (existing.kind === 'owner') {
59
+ throw new PrincipalRegistrationError('identity_bound_to_owner', 'External identity is already bound to an owner principal');
60
+ }
61
+ throw new PrincipalRegistrationError('member_not_active', 'External identity is already bound to a non-active member principal');
62
+ }
63
+ const principalId = mintPrincipalId('member', input.connector, input.namespace, input.externalId);
64
+ adapter.transaction(() => {
65
+ insertPrincipalStatement.run(principalId, 'member', input.displayName ?? null, input.now, input.now);
66
+ bindIdentity(principalId, input.connector, input.namespace, input.externalId, input.now);
67
+ });
68
+ return principalId;
69
+ }
70
+ function transitionMember(principalId, status, now) {
71
+ const principal = selectPrincipalKindStatement.get(principalId);
72
+ if (!principal) {
73
+ throw new Error(`Principal not found: ${principalId}`);
74
+ }
75
+ if (principal.kind === 'owner') {
76
+ throw new Error(`Owner principals cannot be ${status}`);
77
+ }
78
+ updatePrincipalStatusStatement.run(status, now, principalId);
79
+ }
80
+ function ensureOwner(input) {
81
+ return adapter.transaction(() => {
82
+ const existing = resolveByExternal(input.connector, input.namespace, input.externalId);
83
+ if (existing) {
84
+ return existing.kind === 'owner' ? 'exists' : 'conflict';
85
+ }
86
+ if (selectActiveOwnerStatement.get()) {
87
+ return 'conflict';
88
+ }
89
+ const principalId = mintPrincipalId('owner', input.connector, input.namespace, input.externalId);
90
+ insertPrincipalStatement.run(principalId, 'owner', null, input.now, input.now);
91
+ bindIdentity(principalId, input.connector, input.namespace, input.externalId, input.now);
92
+ return 'created';
93
+ });
94
+ }
95
+ function listMembers() {
96
+ return listMembersStatement.all().map((row) => {
97
+ const member = {
98
+ principalId: row.principal_id,
99
+ status: row.status,
100
+ };
101
+ return row.display_name === null ? member : { ...member, displayName: row.display_name };
102
+ });
103
+ }
104
+ return {
105
+ resolveByExternal,
106
+ registerMember,
107
+ bindIdentity,
108
+ suspend(principalId, now) {
109
+ transitionMember(principalId, 'suspended', now);
110
+ },
111
+ offboard(principalId, now) {
112
+ transitionMember(principalId, 'offboarded', now);
113
+ },
114
+ ensureOwner,
115
+ listMembers,
116
+ };
117
+ }
118
+ //# sourceMappingURL=principal-repository.js.map
package/dist/index.d.ts CHANGED
@@ -72,6 +72,7 @@ export * from './cases/wiki-page-index.js';
72
72
  export * from './connectors/event-index.js';
73
73
  export * from './connectors/raw-query.js';
74
74
  export * from './connectors/types.js';
75
+ export * from './identity/principal-repository.js';
75
76
  export * from './search/question-type.js';
76
77
  export * from './search/feedback-store.js';
77
78
  export * from './search/ranker-features.js';
package/dist/index.js CHANGED
@@ -264,6 +264,7 @@ __exportStar(require("./cases/wiki-page-index.js"), exports);
264
264
  __exportStar(require("./connectors/event-index.js"), exports);
265
265
  __exportStar(require("./connectors/raw-query.js"), exports);
266
266
  __exportStar(require("./connectors/types.js"), exports);
267
+ __exportStar(require("./identity/principal-repository.js"), exports);
267
268
  __exportStar(require("./search/question-type.js"), exports);
268
269
  __exportStar(require("./search/feedback-store.js"), exports);
269
270
  __exportStar(require("./search/ranker-features.js"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jungjaehoon/mama-core",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "MAMA Core - Shared modules for Memory-Augmented MCP Assistant",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",