@jungjaehoon/mama-core 2.2.3 → 2.4.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,32 @@
1
+ -- 066: awareness_operational_issues (One MAMA Phase 3 Task 2).
2
+ --
3
+ -- Failures become first-class evidence: gateway tool failures, envelope scope
4
+ -- mismatches, dead owner-event batches and ledger stagnation are recorded here,
5
+ -- aggregated by (surface, signature), and read into every packet so the agent
6
+ -- sees its own failures. The shape below is the exact shape the owner's
7
+ -- installed database already carries from the retired 043-060 chain, plus
8
+ -- `occurrences`. A fresh install gets the whole table here; an installed table
9
+ -- WITHOUT `occurrences` gets the column added at runtime (pragma-guarded ALTER
10
+ -- in packages/standalone/src/observability/operational-issues.ts), because
11
+ -- SQLite has no ADD COLUMN IF NOT EXISTS and this chain is plain SQL.
12
+ --
13
+ -- `status` carries no CHECK on purpose: the installed column has none, and a
14
+ -- CHECK would force a table rebuild for no gain. Values used: open,
15
+ -- repair_requested, closed - validated in TypeScript.
16
+ CREATE TABLE IF NOT EXISTS awareness_operational_issues (
17
+ issue_id TEXT PRIMARY KEY,
18
+ dedupe_key TEXT UNIQUE NOT NULL,
19
+ surface TEXT NOT NULL,
20
+ severity TEXT NOT NULL,
21
+ status TEXT NOT NULL,
22
+ source_delta_id TEXT,
23
+ first_seen_at INTEGER NOT NULL CHECK (first_seen_at >= 0),
24
+ last_seen_at INTEGER NOT NULL CHECK (last_seen_at >= first_seen_at),
25
+ next_retry_at INTEGER,
26
+ last_error TEXT,
27
+ owner_agent TEXT,
28
+ occurrences INTEGER NOT NULL DEFAULT 1
29
+ );
30
+
31
+ CREATE INDEX IF NOT EXISTS idx_awareness_operational_issues_status_seen
32
+ ON awareness_operational_issues(status, last_seen_at DESC);
@@ -0,0 +1,14 @@
1
+ -- Migration 067: give connector_event_index a first-class source_entity_id so an agent can read an
2
+ -- entity's change history (the revisions save() preserves) through the same grant-bounded core index
3
+ -- it already searches. Previously sourceEntityId lived only inside metadata_json, with no by-entity
4
+ -- query. Additive and backward-compatible: existing rows are backfilled from metadata_json.
5
+ ALTER TABLE connector_event_index ADD COLUMN source_entity_id TEXT;
6
+
7
+ UPDATE connector_event_index
8
+ SET source_entity_id = json_extract(metadata_json, '$.sourceEntityId')
9
+ WHERE source_entity_id IS NULL
10
+ AND metadata_json IS NOT NULL
11
+ AND json_extract(metadata_json, '$.sourceEntityId') IS NOT NULL;
12
+
13
+ CREATE INDEX IF NOT EXISTS idx_connector_event_source_entity
14
+ ON connector_event_index(source_connector, source_entity_id);
@@ -135,15 +135,16 @@ function upsertConnectorEventIndex(adapter, input) {
135
135
  adapter
136
136
  .prepare(`
137
137
  INSERT INTO connector_event_index (
138
- event_index_id, source_connector, source_type, source_id, source_locator,
139
- channel, author, title, content, event_datetime, event_date, source_timestamp_ms,
140
- source_cursor, tenant_id, project_id, memory_scope_kind, memory_scope_id,
141
- metadata_json, artifact_locator, artifact_title, content_hash, indexed_at, updated_at,
142
- expires_at
138
+ event_index_id, source_connector, source_type, source_id, source_entity_id,
139
+ source_locator, channel, author, title, content, event_datetime, event_date,
140
+ source_timestamp_ms, source_cursor, tenant_id, project_id, memory_scope_kind,
141
+ memory_scope_id, metadata_json, artifact_locator, artifact_title, content_hash,
142
+ indexed_at, updated_at, expires_at
143
143
  )
144
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
144
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
145
145
  ON CONFLICT(source_connector, source_id) DO UPDATE SET
146
146
  source_type = excluded.source_type,
147
+ source_entity_id = excluded.source_entity_id,
147
148
  source_locator = excluded.source_locator,
148
149
  channel = excluded.channel,
149
150
  author = excluded.author,
@@ -182,7 +183,7 @@ function upsertConnectorEventIndex(adapter, input) {
182
183
  ELSE connector_event_index.operator_observation_seq
183
184
  END
184
185
  `)
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);
186
+ .run(eventIndexId, input.source_connector, input.source_type, input.source_id, input.source_entity_id ?? 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);
186
187
  const saved = getConnectorEventIndexRecord(adapter, input.source_connector, input.source_id);
187
188
  if (!saved) {
188
189
  throw new Error(`Failed to read connector_event_index row after upsert: ${input.source_connector}/${input.source_id}`);
@@ -1,5 +1,5 @@
1
1
  import type { DatabaseAdapter } from '../db-manager.js';
2
- import type { RawSearchHit, RawSearchInput, RawSearchResult, RawSearchScopeFilter } from './types.js';
2
+ import type { RawSearchHit, RawDocument, RawSearchInput, RawSearchResult, RawSearchScopeFilter } from './types.js';
3
3
  type RawQueryAdapter = Pick<DatabaseAdapter, 'prepare'>;
4
4
  interface RawWindowInput {
5
5
  connectors?: string[];
@@ -9,10 +9,32 @@ interface RawWindowInput {
9
9
  }
10
10
  export declare function searchRaw(adapter: RawQueryAdapter, input: RawSearchInput): RawSearchResult;
11
11
  export declare function searchAllRaw(adapter: RawQueryAdapter, input: RawSearchInput): RawSearchResult;
12
- export declare function getRawById(adapter: RawQueryAdapter, rawId: string, visibility: Pick<RawSearchInput, 'connectors' | 'scopes'>): RawSearchHit | null;
12
+ export declare function getRawById(adapter: RawQueryAdapter, rawId: string, visibility: Pick<RawSearchInput, 'connectors' | 'scopes'>): RawDocument | null;
13
13
  export declare function getRawWindow(adapter: RawQueryAdapter, rawId: string, input: RawWindowInput): {
14
14
  target: RawSearchHit;
15
15
  items: RawSearchHit[];
16
16
  } | null;
17
+ export interface RawHistoryInput {
18
+ /** The entity whose revisions to read. Provide this OR `rawId`. */
19
+ entityId?: string;
20
+ /**
21
+ * A raw event id (event_index_id) to anchor on: its entity is resolved under the SAME
22
+ * visibility, then that entity's revisions are returned. An anchor the caller may not see
23
+ * resolves to nothing, so a rawId cannot widen what the caller can read.
24
+ */
25
+ rawId?: string;
26
+ connectors?: string[];
27
+ scopes?: RawSearchScopeFilter[];
28
+ fromMs?: number;
29
+ toMs?: number;
30
+ limit?: number;
31
+ cursor?: string;
32
+ }
33
+ /**
34
+ * Chronological change history of one upstream entity (rows sharing source_entity_id), bounded by
35
+ * the SAME connector/scope visibility as search - a citation must not out-read reading. Oldest first,
36
+ * cursor-paged. Rows with a NULL source_entity_id (no revision grouping) never match an entity query.
37
+ */
38
+ export declare function getRawHistory(adapter: RawQueryAdapter, input: RawHistoryInput): RawSearchResult;
17
39
  export type { RawSearchHit, RawSearchInput, RawSearchResult, RawSearchScopeFilter, } from './types.js';
18
40
  //# sourceMappingURL=raw-query.d.ts.map
@@ -4,6 +4,7 @@ exports.searchRaw = searchRaw;
4
4
  exports.searchAllRaw = searchAllRaw;
5
5
  exports.getRawById = getRawById;
6
6
  exports.getRawWindow = getRawWindow;
7
+ exports.getRawHistory = getRawHistory;
7
8
  const types_js_1 = require("../memory/types.js");
8
9
  const event_index_js_1 = require("./event-index.js");
9
10
  const DEFAULT_RAW_LIMIT = 25;
@@ -220,7 +221,7 @@ function getRawById(adapter, rawId, visibility) {
220
221
  LIMIT 1
221
222
  `)
222
223
  .get(...params);
223
- return row ? toRawHit(row) : null;
224
+ return row ? { ...toRawHit(row), content: String(row.content) } : null;
224
225
  }
225
226
  function getRawWindow(adapter, rawId, input) {
226
227
  const targetParams = [rawId];
@@ -302,4 +303,110 @@ function afterWindowRows(adapter, target, input, limit) {
302
303
  `)
303
304
  .all(...params, limit);
304
305
  }
306
+ // The entity a raw event belongs to, plus its connector. source_entity_id is unique only WITHIN a
307
+ // connector, so history must carry the connector too or two connectors sharing an id would merge.
308
+ function resolveEntityAnchor(adapter, rawId, visibility) {
309
+ const clauses = ['e.event_index_id = ?'];
310
+ const params = [rawId];
311
+ appendFilters(clauses, params, { query: '*', ...visibility });
312
+ const row = adapter
313
+ .prepare(`SELECT source_entity_id, source_connector FROM connector_event_index e WHERE ${clauses.join(' AND ')} LIMIT 1`)
314
+ .get(...params);
315
+ return row &&
316
+ typeof row.source_entity_id === 'string' &&
317
+ row.source_entity_id.length > 0 &&
318
+ typeof row.source_connector === 'string' &&
319
+ row.source_connector.length > 0
320
+ ? { entityId: row.source_entity_id, connector: row.source_connector }
321
+ : null;
322
+ }
323
+ function singleConnector(connectors) {
324
+ const normalized = normalizeConnectors(connectors);
325
+ return normalized.length === 1 ? normalized[0] : null;
326
+ }
327
+ function encodeHistoryCursor(row) {
328
+ return Buffer.from(JSON.stringify({ timestampMs: row.source_timestamp_ms, rawId: row.event_index_id }), 'utf8').toString('base64url');
329
+ }
330
+ function decodeHistoryCursor(cursor) {
331
+ if (!cursor) {
332
+ return null;
333
+ }
334
+ try {
335
+ const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
336
+ if (typeof parsed.timestampMs !== 'number' ||
337
+ !Number.isFinite(parsed.timestampMs) ||
338
+ typeof parsed.rawId !== 'string' ||
339
+ parsed.rawId.length === 0) {
340
+ throw new Error('invalid shape');
341
+ }
342
+ return { timestampMs: parsed.timestampMs, rawId: parsed.rawId };
343
+ }
344
+ catch (err) {
345
+ const message = err instanceof Error ? err.message : String(err);
346
+ throw new Error(`Invalid raw history cursor: ${message}`);
347
+ }
348
+ }
349
+ /**
350
+ * Chronological change history of one upstream entity (rows sharing source_entity_id), bounded by
351
+ * the SAME connector/scope visibility as search - a citation must not out-read reading. Oldest first,
352
+ * cursor-paged. Rows with a NULL source_entity_id (no revision grouping) never match an entity query.
353
+ */
354
+ function getRawHistory(adapter, input) {
355
+ // Revision identity is connector-scoped. The rawId path takes the anchor's own connector; the
356
+ // entityId path requires the caller to name exactly one connector, since an entity id alone is
357
+ // ambiguous across connectors.
358
+ const trimmedEntityId = input.entityId?.trim() ?? '';
359
+ const anchor = trimmedEntityId.length > 0
360
+ ? (() => {
361
+ const connector = singleConnector(input.connectors);
362
+ return connector ? { entityId: trimmedEntityId, connector } : null;
363
+ })()
364
+ : input.rawId && input.rawId.trim().length > 0
365
+ ? resolveEntityAnchor(adapter, input.rawId.trim(), {
366
+ connectors: input.connectors,
367
+ scopes: input.scopes,
368
+ })
369
+ : null;
370
+ if (!anchor) {
371
+ return { hits: [], next_cursor: null };
372
+ }
373
+ const limit = normalizeLimit(input.limit);
374
+ if (limit === 0) {
375
+ return { hits: [], next_cursor: null };
376
+ }
377
+ const clauses = ['e.source_entity_id = ?'];
378
+ const params = [anchor.entityId];
379
+ appendFilters(clauses, params, {
380
+ query: '*',
381
+ connectors: [anchor.connector],
382
+ scopes: input.scopes,
383
+ fromMs: input.fromMs,
384
+ toMs: input.toMs,
385
+ });
386
+ const cursor = decodeHistoryCursor(input.cursor);
387
+ if (cursor) {
388
+ clauses.push('(e.source_timestamp_ms > ? OR (e.source_timestamp_ms = ? AND e.event_index_id > ?))');
389
+ params.push(cursor.timestampMs, cursor.timestampMs, cursor.rawId);
390
+ }
391
+ const rows = adapter
392
+ .prepare(`
393
+ SELECT e.*, 0 AS rank
394
+ FROM connector_event_index e
395
+ WHERE ${clauses.join(' AND ')}
396
+ ORDER BY e.source_timestamp_ms ASC, e.event_index_id ASC
397
+ LIMIT ?
398
+ `)
399
+ .all(...params, limit + 1);
400
+ const pageRows = rows.slice(0, limit);
401
+ const nextRow = rows.length > limit ? pageRows[pageRows.length - 1] : undefined;
402
+ return {
403
+ hits: pageRows.map(toRawHit),
404
+ next_cursor: nextRow
405
+ ? encodeHistoryCursor({
406
+ source_timestamp_ms: Number(nextRow.source_timestamp_ms),
407
+ event_index_id: String(nextRow.event_index_id),
408
+ })
409
+ : null,
410
+ };
411
+ }
305
412
  //# sourceMappingURL=raw-query.js.map
@@ -32,6 +32,8 @@ export interface UpsertConnectorEventIndexInput {
32
32
  source_connector: string;
33
33
  source_type: string;
34
34
  source_id: string;
35
+ /** Stable upstream entity across revisions; enables grant-bounded change-history reads. */
36
+ source_entity_id?: string | null;
35
37
  source_locator?: string | null;
36
38
  channel?: string | null;
37
39
  author?: string | null;
@@ -103,6 +105,10 @@ export interface RawSearchHit {
103
105
  source_ref: string | null;
104
106
  metadata: Record<string, unknown>;
105
107
  }
108
+ /** Explicit detail access includes the complete source; search results remain bounded previews. */
109
+ export interface RawDocument extends RawSearchHit {
110
+ content: string;
111
+ }
106
112
  export interface RawSearchResult {
107
113
  hits: RawSearchHit[];
108
114
  next_cursor: string | null;
@@ -70,6 +70,8 @@ export declare class NodeSQLiteAdapter extends DatabaseAdapter {
70
70
  private assertMigration032Complete;
71
71
  private recoverConnectorEventScopeMigration034;
72
72
  private assertMigration034Complete;
73
+ private recoverConnectorEventSourceEntityIdMigration067;
74
+ private assertMigration067Complete;
73
75
  private recoverConnectorEventOperatorSeqMigration039;
74
76
  private assertMigration039Complete;
75
77
  private recoverConnectorEventSequencesMigration062;
@@ -409,6 +409,11 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
409
409
  (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} recovered successfully`);
410
410
  continue;
411
411
  }
412
+ if (message.includes('duplicate column') && version === 67) {
413
+ this.recoverConnectorEventSourceEntityIdMigration067();
414
+ (0, debug_logger_js_1.info)(`[node-sqlite-adapter] Migration ${file} recovered successfully`);
415
+ continue;
416
+ }
412
417
  if (message.includes('no such column') && version === 62) {
413
418
  (0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} deferred until connector structure recovery (${message})`);
414
419
  continue;
@@ -418,6 +423,11 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
418
423
  (0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} deferred until connector structure recovery (${message})`);
419
424
  continue;
420
425
  }
426
+ if (version === 67 &&
427
+ (message.includes('no such column') || message.includes('no such table'))) {
428
+ (0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} deferred until connector structure recovery (${message})`);
429
+ continue;
430
+ }
421
431
  if (message.includes('duplicate column')) {
422
432
  (0, debug_logger_js_1.warn)(`[node-sqlite-adapter] Migration ${file} skipped (duplicate column - already applied)`);
423
433
  this.prepare('INSERT OR IGNORE INTO schema_version (version) VALUES (?)').run(version);
@@ -515,6 +525,13 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
515
525
  (0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped legacy connector event refresh migration');
516
526
  }
517
527
  }
528
+ const hasMissingSourceEntityIdFeature = !this.tableColumns('connector_event_index').has('source_entity_id') ||
529
+ !this.indexExists('idx_connector_event_source_entity') ||
530
+ !this.schemaVersionExists(67);
531
+ if (hasMissingSourceEntityIdFeature) {
532
+ this.recoverConnectorEventSourceEntityIdMigration067();
533
+ (0, debug_logger_js_1.info)('[node-sqlite-adapter] Repaired skipped connector event source_entity_id migration');
534
+ }
518
535
  }
519
536
  if (!this.tableExists('twin_edges')) {
520
537
  this.applyRepairMigration(migrationsDir, '035-create-twin-edges.sql', 'twin edge ledger');
@@ -743,6 +760,40 @@ class NodeSQLiteAdapter extends base_adapter_js_1.DatabaseAdapter {
743
760
  }
744
761
  }
745
762
  }
763
+ recoverConnectorEventSourceEntityIdMigration067() {
764
+ this.transaction(() => {
765
+ const columns = this.tableColumns('connector_event_index');
766
+ if (!columns.has('source_entity_id')) {
767
+ this.exec('ALTER TABLE connector_event_index ADD COLUMN source_entity_id TEXT');
768
+ }
769
+ // Backfill only when metadata_json exists; a legacy/minimal table rebuilt by the repair path
770
+ // may not have it yet. New rows carry source_entity_id at write time regardless.
771
+ if (columns.has('metadata_json')) {
772
+ this.exec(`
773
+ UPDATE connector_event_index
774
+ SET source_entity_id = json_extract(metadata_json, '$.sourceEntityId')
775
+ WHERE source_entity_id IS NULL
776
+ AND metadata_json IS NOT NULL
777
+ AND json_extract(metadata_json, '$.sourceEntityId') IS NOT NULL
778
+ `);
779
+ }
780
+ this.exec(`
781
+ CREATE INDEX IF NOT EXISTS idx_connector_event_source_entity
782
+ ON connector_event_index(source_connector, source_entity_id)
783
+ `);
784
+ this.assertMigration067Complete();
785
+ this.prepare('INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)').run(67, 'Add connector_event_index source_entity_id');
786
+ });
787
+ }
788
+ assertMigration067Complete() {
789
+ const columns = this.tableColumns('connector_event_index');
790
+ if (!columns.has('source_entity_id')) {
791
+ throw new Error('Migration 067 recovery failed: missing connector_event_index.source_entity_id');
792
+ }
793
+ if (!this.indexExists('idx_connector_event_source_entity')) {
794
+ throw new Error('Migration 067 recovery failed: missing index idx_connector_event_source_entity');
795
+ }
796
+ }
746
797
  recoverConnectorEventOperatorSeqMigration039() {
747
798
  this.transaction(() => {
748
799
  const columns = this.tableColumns('connector_event_index');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jungjaehoon/mama-core",
3
- "version": "2.2.3",
3
+ "version": "2.4.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",