@jungjaehoon/mama-core 1.9.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/db/migrations/043-add-tool-trace-failure-code.sql +11 -0
  2. package/dist/agent-graph/alias-write.js +3 -0
  3. package/dist/agent-graph/entity-resolve.js +3 -7
  4. package/dist/agent-graph/graph-query.js +5 -4
  5. package/dist/agent-graph/types.d.ts +30 -0
  6. package/dist/cases/store.d.ts +0 -4
  7. package/dist/cases/store.js +0 -66
  8. package/dist/cases/wiki-page-index.d.ts +0 -13
  9. package/dist/cases/wiki-page-index.js +0 -153
  10. package/dist/connectors/event-index.d.ts +1 -22
  11. package/dist/connectors/event-index.js +0 -157
  12. package/dist/context-compile/channel-grant.d.ts +46 -0
  13. package/dist/context-compile/channel-grant.js +66 -0
  14. package/dist/context-compile/compiler.js +5 -2
  15. package/dist/context-compile/index.d.ts +1 -0
  16. package/dist/context-compile/index.js +1 -0
  17. package/dist/context-compile/packet-store.d.ts +0 -1
  18. package/dist/context-compile/packet-store.js +0 -16
  19. package/dist/context-compile/source-readers.d.ts +1 -0
  20. package/dist/context-compile/source-readers.js +156 -23
  21. package/dist/context-compile/types.d.ts +17 -0
  22. package/dist/context-compile/visibility.js +28 -4
  23. package/dist/db-adapter/node-sqlite-adapter.js +4 -0
  24. package/dist/db-manager.d.ts +0 -13
  25. package/dist/db-manager.js +0 -66
  26. package/dist/decision-formatter.d.ts +0 -11
  27. package/dist/decision-formatter.js +0 -122
  28. package/dist/decision-tracker.d.ts +0 -12
  29. package/dist/decision-tracker.js +0 -50
  30. package/dist/edges/ref-validation.d.ts +1 -2
  31. package/dist/edges/ref-validation.js +33 -13
  32. package/dist/edges/store.d.ts +1 -3
  33. package/dist/edges/store.js +0 -214
  34. package/dist/edges/types.d.ts +10 -0
  35. package/dist/embedding-client.d.ts +0 -17
  36. package/dist/embedding-client.js +0 -80
  37. package/dist/embedding-server/mobile/auth.d.ts +1 -10
  38. package/dist/embedding-server/mobile/auth.js +0 -15
  39. package/dist/embeddings.d.ts +0 -12
  40. package/dist/embeddings.js +0 -68
  41. package/dist/entities/audit-metrics.d.ts +0 -5
  42. package/dist/entities/audit-metrics.js +0 -4
  43. package/dist/entities/candidate-generator.d.ts +1 -2
  44. package/dist/entities/candidate-generator.js +0 -412
  45. package/dist/entities/entity-list.d.ts +0 -1
  46. package/dist/entities/entity-list.js +0 -96
  47. package/dist/entities/entity-orphan-list.d.ts +0 -9
  48. package/dist/entities/entity-orphan-list.js +0 -88
  49. package/dist/entities/entity-search.d.ts +0 -1
  50. package/dist/entities/entity-search.js +0 -147
  51. package/dist/entities/errors.d.ts +0 -10
  52. package/dist/entities/errors.js +1 -25
  53. package/dist/entities/lineage-store.d.ts +0 -23
  54. package/dist/entities/lineage-store.js +0 -104
  55. package/dist/entities/policy-store.d.ts +0 -9
  56. package/dist/entities/policy-store.js +0 -188
  57. package/dist/entities/provenance-query.d.ts +0 -1
  58. package/dist/entities/provenance-query.js +0 -164
  59. package/dist/entities/resolution-engine.d.ts +1 -8
  60. package/dist/entities/resolution-engine.js +0 -66
  61. package/dist/entities/rollback-preview.d.ts +0 -4
  62. package/dist/entities/rollback-preview.js +0 -232
  63. package/dist/entities/store.d.ts +1 -10
  64. package/dist/entities/store.js +0 -49
  65. package/dist/index.d.ts +15 -31
  66. package/dist/index.js +4 -46
  67. package/dist/mama-api.d.ts +19 -1
  68. package/dist/mama-api.js +97 -2
  69. package/dist/memory/api.d.ts +8 -0
  70. package/dist/memory/api.js +55 -2
  71. package/dist/memory/event-store.d.ts +0 -1
  72. package/dist/memory/event-store.js +0 -15
  73. package/dist/memory/finding-store.d.ts +0 -1
  74. package/dist/memory/finding-store.js +0 -12
  75. package/dist/memory/scope-backfill.d.ts +0 -3
  76. package/dist/memory/scope-backfill.js +0 -74
  77. package/dist/memory/scope-store.d.ts +0 -1
  78. package/dist/memory/scope-store.js +0 -4
  79. package/dist/memory/truth-store.d.ts +0 -3
  80. package/dist/memory/truth-store.js +0 -22
  81. package/dist/memory/types.d.ts +0 -1
  82. package/dist/memory/types.js +0 -13
  83. package/dist/memory-store.d.ts +0 -16
  84. package/dist/memory-store.js +0 -51
  85. package/dist/model-runs/tool-trace-store.js +9 -5
  86. package/dist/model-runs/types.d.ts +3 -0
  87. package/dist/ollama-client.d.ts +0 -9
  88. package/dist/ollama-client.js +0 -125
  89. package/dist/outcome-tracker.d.ts +0 -14
  90. package/dist/outcome-tracker.js +0 -44
  91. package/dist/progress-indicator.d.ts +0 -4
  92. package/dist/progress-indicator.js +0 -7
  93. package/dist/provenance/source-ref.d.ts +0 -9
  94. package/dist/provenance/source-ref.js +0 -145
  95. package/dist/query-intent.d.ts +0 -4
  96. package/dist/query-intent.js +0 -30
  97. package/dist/relevance-scorer.d.ts +0 -9
  98. package/dist/relevance-scorer.js +0 -81
  99. package/dist/search/feedback-store.d.ts +0 -1
  100. package/dist/search/feedback-store.js +0 -4
  101. package/dist/search/ranker-rescore.d.ts +0 -3
  102. package/dist/search/ranker-rescore.js +0 -27
  103. package/dist/search/ranker-trainer.d.ts +0 -8
  104. package/dist/search/ranker-trainer.js +0 -376
  105. package/dist/tier-validator.d.ts +0 -12
  106. package/dist/tier-validator.js +0 -56
  107. package/package.json +1 -10
  108. package/dist/cases/case-links.d.ts +0 -116
  109. package/dist/cases/case-links.js +0 -794
  110. package/dist/cases/composition-overrides.d.ts +0 -90
  111. package/dist/cases/composition-overrides.js +0 -724
  112. package/dist/cases/corrections.d.ts +0 -156
  113. package/dist/cases/corrections.js +0 -1045
  114. package/dist/cases/freshness.d.ts +0 -57
  115. package/dist/cases/freshness.js +0 -283
  116. package/dist/cases/live-state.d.ts +0 -48
  117. package/dist/cases/live-state.js +0 -256
  118. package/dist/cases/membership-explain.d.ts +0 -56
  119. package/dist/cases/membership-explain.js +0 -140
  120. package/dist/cases/membership-matcher.d.ts +0 -23
  121. package/dist/cases/membership-matcher.js +0 -193
  122. package/dist/cases/merge-split.d.ts +0 -61
  123. package/dist/cases/merge-split.js +0 -368
  124. package/dist/cases/role-inference.d.ts +0 -10
  125. package/dist/cases/role-inference.js +0 -85
  126. package/dist/cases/sqlite-transaction.d.ts +0 -10
  127. package/dist/cases/sqlite-transaction.js +0 -62
  128. package/dist/cases/target-ref.d.ts +0 -25
  129. package/dist/cases/target-ref.js +0 -22
  130. package/dist/cases/tombstone-sweeper.d.ts +0 -13
  131. package/dist/cases/tombstone-sweeper.js +0 -128
  132. package/dist/entities/entity-impact.d.ts +0 -41
  133. package/dist/entities/entity-impact.js +0 -140
  134. package/dist/entities/exact-merge-backfill.d.ts +0 -11
  135. package/dist/entities/exact-merge-backfill.js +0 -161
  136. package/dist/entities/lineage-backfill.d.ts +0 -12
  137. package/dist/entities/lineage-backfill.js +0 -101
  138. package/dist/memory-inject.d.ts +0 -24
  139. package/dist/memory-inject.js +0 -116
@@ -5,32 +5,10 @@ exports.connectorEventIndexId = connectorEventIndexId;
5
5
  exports.connectorEventContentHash = connectorEventContentHash;
6
6
  exports.upsertConnectorEventIndex = upsertConnectorEventIndex;
7
7
  exports.getConnectorEventIndexRecord = getConnectorEventIndexRecord;
8
- exports.listConnectorEventsByDatetimeRange = listConnectorEventsByDatetimeRange;
9
- exports.searchConnectorEventsByFTS = searchConnectorEventsByFTS;
10
8
  exports.readConnectorCursor = readConnectorCursor;
11
- exports.upsertConnectorCursor = upsertConnectorCursor;
12
9
  exports.deleteExpiredConnectorEvents = deleteExpiredConnectorEvents;
13
- exports.computeStalenessStatus = computeStalenessStatus;
14
10
  const node_crypto_1 = require("node:crypto");
15
11
  const canonicalize_js_1 = require("../canonicalize.js");
16
- const DEFAULT_RANGE_LIMIT = 100;
17
- const DEFAULT_SEARCH_LIMIT = 25;
18
- const MAX_RANGE_LIMIT = 500;
19
- const FIVE_MINUTES_MS = 5 * 60 * 1000;
20
- const FIFTEEN_MINUTES_MS = 15 * 60 * 1000;
21
- const SIXTY_MINUTES_MS = 60 * 60 * 1000;
22
- function placeholders(values) {
23
- if (values.length === 0) {
24
- throw new Error('Cannot build SQL placeholders for an empty list.');
25
- }
26
- return values.map(() => '?').join(', ');
27
- }
28
- function positiveLimit(value, fallback, max = MAX_RANGE_LIMIT) {
29
- if (value === undefined) {
30
- return fallback;
31
- }
32
- return Math.min(max, Math.max(0, Math.floor(value)));
33
- }
34
12
  function nowIso() {
35
13
  return new Date().toISOString();
36
14
  }
@@ -119,16 +97,6 @@ function mapConnectorCursorRow(row) {
119
97
  indexed_count: Number(row.indexed_count),
120
98
  };
121
99
  }
122
- function connectorFilterSql(connectors, alias = '') {
123
- if (!connectors || connectors.length === 0) {
124
- return '';
125
- }
126
- const prefix = alias.length > 0 ? `${alias}.` : '';
127
- return ` AND ${prefix}source_connector IN (${placeholders(connectors)})`;
128
- }
129
- function hasOwn(input, key) {
130
- return Object.prototype.hasOwnProperty.call(input, key);
131
- }
132
100
  function connectorEventIndexId(sourceConnector, sourceId) {
133
101
  const digest = (0, node_crypto_1.createHash)('sha1').update(`${sourceConnector}\0${sourceId}`, 'utf8').digest('hex');
134
102
  return `evt_${digest.slice(0, 16)}`;
@@ -214,59 +182,6 @@ function getConnectorEventIndexRecord(adapter, sourceConnector, sourceId) {
214
182
  .get(sourceConnector, sourceId);
215
183
  return row ? mapConnectorEventIndexRow(row) : null;
216
184
  }
217
- function listConnectorEventsByDatetimeRange(adapter, input) {
218
- const limit = positiveLimit(input.limit, DEFAULT_RANGE_LIMIT);
219
- if (limit === 0) {
220
- return [];
221
- }
222
- const order = input.order === 'desc' ? 'DESC' : 'ASC';
223
- const connectors = input.connectors?.filter(Boolean) ?? [];
224
- const params = [input.fromMs, input.toMs, ...connectors, limit];
225
- const rows = adapter
226
- .prepare(`
227
- SELECT *
228
- FROM connector_event_index
229
- WHERE event_datetime >= ?
230
- AND event_datetime <= ?
231
- ${connectorFilterSql(connectors)}
232
- ORDER BY event_datetime ${order}, event_index_id ASC
233
- LIMIT ?
234
- `)
235
- .all(...params);
236
- return rows.map(mapConnectorEventIndexRow);
237
- }
238
- function searchConnectorEventsByFTS(adapter, query, options = {}) {
239
- const normalizedQuery = query.trim();
240
- if (normalizedQuery.length === 0) {
241
- return [];
242
- }
243
- const limit = positiveLimit(options.limit, DEFAULT_SEARCH_LIMIT, 100);
244
- if (limit === 0) {
245
- return [];
246
- }
247
- const connectors = options.connectors?.filter(Boolean) ?? [];
248
- const params = [normalizedQuery, ...connectors, limit];
249
- const rows = adapter
250
- .prepare(`
251
- SELECT e.*, bm25(connector_event_index_fts) AS rank
252
- FROM connector_event_index_fts
253
- JOIN connector_event_index e
254
- ON e.event_index_id = connector_event_index_fts.event_index_id
255
- WHERE connector_event_index_fts MATCH ?
256
- ${connectorFilterSql(connectors, 'e')}
257
- ORDER BY rank ASC, e.event_datetime DESC, e.event_index_id ASC
258
- LIMIT ?
259
- `)
260
- .all(...params);
261
- return rows.map((row) => {
262
- const rank = Number(row.rank);
263
- return {
264
- ...mapConnectorEventIndexRow(row),
265
- rank,
266
- score: Number.isFinite(rank) ? 1 / (1 + Math.exp(rank)) : 0,
267
- };
268
- });
269
- }
270
185
  function readConnectorCursor(adapter, connectorName) {
271
186
  const row = adapter
272
187
  .prepare(`
@@ -278,49 +193,6 @@ function readConnectorCursor(adapter, connectorName) {
278
193
  .get(connectorName);
279
194
  return row ? mapConnectorCursorRow(row) : null;
280
195
  }
281
- function upsertConnectorCursor(adapter, input) {
282
- const current = readConnectorCursor(adapter, input.connector_name);
283
- const merged = {
284
- connector_name: input.connector_name,
285
- last_seen_timestamp_ms: input.last_seen_timestamp_ms ?? current?.last_seen_timestamp_ms ?? 0,
286
- last_seen_source_id: input.last_seen_source_id ?? current?.last_seen_source_id ?? '',
287
- last_sweep_at: hasOwn(input, 'last_sweep_at')
288
- ? (input.last_sweep_at ?? null)
289
- : (current?.last_sweep_at ?? null),
290
- last_success_at: hasOwn(input, 'last_success_at')
291
- ? (input.last_success_at ?? null)
292
- : (current?.last_success_at ?? null),
293
- last_error: hasOwn(input, 'last_error')
294
- ? (input.last_error ?? null)
295
- : (current?.last_error ?? null),
296
- last_error_at: hasOwn(input, 'last_error_at')
297
- ? (input.last_error_at ?? null)
298
- : (current?.last_error_at ?? null),
299
- indexed_count: input.indexed_count ?? current?.indexed_count ?? 0,
300
- };
301
- adapter
302
- .prepare(`
303
- INSERT INTO connector_event_index_cursors (
304
- connector_name, last_seen_timestamp_ms, last_seen_source_id, last_sweep_at,
305
- last_success_at, last_error, last_error_at, indexed_count
306
- )
307
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
308
- ON CONFLICT(connector_name) DO UPDATE SET
309
- last_seen_timestamp_ms = excluded.last_seen_timestamp_ms,
310
- last_seen_source_id = excluded.last_seen_source_id,
311
- last_sweep_at = excluded.last_sweep_at,
312
- last_success_at = excluded.last_success_at,
313
- last_error = excluded.last_error,
314
- last_error_at = excluded.last_error_at,
315
- indexed_count = excluded.indexed_count
316
- `)
317
- .run(merged.connector_name, merged.last_seen_timestamp_ms, merged.last_seen_source_id, merged.last_sweep_at, merged.last_success_at, merged.last_error, merged.last_error_at, merged.indexed_count);
318
- const saved = readConnectorCursor(adapter, input.connector_name);
319
- if (!saved) {
320
- throw new Error(`Failed to read connector cursor after upsert: ${input.connector_name}`);
321
- }
322
- return saved;
323
- }
324
196
  function deleteExpiredConnectorEvents(adapter, input) {
325
197
  if (!Number.isFinite(input.nowMs)) {
326
198
  throw new Error('deleteExpiredConnectorEvents.nowMs must be a finite number.');
@@ -349,33 +221,4 @@ function deleteExpiredConnectorEvents(adapter, input) {
349
221
  .run(cutoffMs);
350
222
  return { rows_deleted: result.changes };
351
223
  }
352
- function computeStalenessStatus(cursor, options) {
353
- if (!cursor) {
354
- return 'never_swept';
355
- }
356
- if (cursor.last_error !== null) {
357
- return 'unhealthy';
358
- }
359
- if (cursor.last_success_at === null) {
360
- return 'never_swept';
361
- }
362
- const lastSuccessMs = Date.parse(cursor.last_success_at);
363
- if (!Number.isFinite(lastSuccessMs)) {
364
- return 'unhealthy';
365
- }
366
- const elapsedMs = options.nowMs - lastSuccessMs;
367
- const staleAfterMs = options.staleAfterMs ?? FIVE_MINUTES_MS;
368
- const warnAfterMs = options.stalenessWarnAfterMs ?? FIFTEEN_MINUTES_MS;
369
- const unhealthyAfterMs = options.unhealthyAfterMs ?? SIXTY_MINUTES_MS;
370
- if (elapsedMs > unhealthyAfterMs) {
371
- return 'unhealthy';
372
- }
373
- if (elapsedMs > warnAfterMs) {
374
- return 'warn';
375
- }
376
- if (elapsedMs > staleAfterMs) {
377
- return 'stale-but-warming';
378
- }
379
- return 'healthy';
380
- }
381
224
  //# sourceMappingURL=event-index.js.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The raw visibility rule, in one place.
3
+ *
4
+ * There were three copies of this decision. The reader had one, twin-ref validation had a
5
+ * second, and the standalone citation path had a third that carried a comment promising it
6
+ * reproduced the reader "clause for clause" - which it did, until the reader changed. The
7
+ * differential test that was supposed to catch that kept passing, because its fixtures
8
+ * never exercised the branch production takes. Copies pinned by a test are still copies;
9
+ * the test just tells you later.
10
+ *
11
+ * So the rule lives here, and everything that needs it takes it from here. It has to exist
12
+ * in two compiled forms - a boolean for checking one row you already hold, and SQL for
13
+ * selecting rows you do not - and those two forms are adjacent in this file with a
14
+ * differential over a matrix that includes what production sends. Two forms of one rule is
15
+ * the irreducible minimum; three copies of a decision is not.
16
+ */
17
+ /** Connector -> the channels of it that may be read. */
18
+ export type ChannelGrant = Record<string, readonly string[]>;
19
+ /**
20
+ * The rule: an event is visible when its connector is granted and its channel is one of
21
+ * the channels granted for that connector.
22
+ *
23
+ * A connector absent from the grant is denied. A connector present with an empty list is
24
+ * also denied - "granted the connector but no channel of it" is not a useful state, and
25
+ * reading it as "all channels" is how a grant becomes a wildcard.
26
+ */
27
+ export declare function isChannelGranted(connector: string, channel: string | null | undefined, grant: ChannelGrant): boolean;
28
+ export interface ChannelGrantClause {
29
+ sql: string;
30
+ params: unknown[];
31
+ }
32
+ /**
33
+ * The same rule as a SQL predicate.
34
+ *
35
+ * Returns null when nothing is readable, which callers must treat as "select nothing"
36
+ * rather than "no filter" - an empty WHERE is how a visibility rule turns into a full scan
37
+ * at the exact moment it should have refused.
38
+ *
39
+ * `requestedConnectors` narrows further: a connector the caller did not ask for is not read
40
+ * even though it is granted. The grant is a ceiling, never an instruction.
41
+ */
42
+ export declare function channelGrantClause(grant: ChannelGrant, requestedConnectors: readonly string[] | undefined, columns: {
43
+ connector: string;
44
+ channel: string;
45
+ }): ChannelGrantClause | null;
46
+ //# sourceMappingURL=channel-grant.d.ts.map
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ /**
3
+ * The raw visibility rule, in one place.
4
+ *
5
+ * There were three copies of this decision. The reader had one, twin-ref validation had a
6
+ * second, and the standalone citation path had a third that carried a comment promising it
7
+ * reproduced the reader "clause for clause" - which it did, until the reader changed. The
8
+ * differential test that was supposed to catch that kept passing, because its fixtures
9
+ * never exercised the branch production takes. Copies pinned by a test are still copies;
10
+ * the test just tells you later.
11
+ *
12
+ * So the rule lives here, and everything that needs it takes it from here. It has to exist
13
+ * in two compiled forms - a boolean for checking one row you already hold, and SQL for
14
+ * selecting rows you do not - and those two forms are adjacent in this file with a
15
+ * differential over a matrix that includes what production sends. Two forms of one rule is
16
+ * the irreducible minimum; three copies of a decision is not.
17
+ */
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.isChannelGranted = isChannelGranted;
20
+ exports.channelGrantClause = channelGrantClause;
21
+ /**
22
+ * The rule: an event is visible when its connector is granted and its channel is one of
23
+ * the channels granted for that connector.
24
+ *
25
+ * A connector absent from the grant is denied. A connector present with an empty list is
26
+ * also denied - "granted the connector but no channel of it" is not a useful state, and
27
+ * reading it as "all channels" is how a grant becomes a wildcard.
28
+ */
29
+ function isChannelGranted(connector, channel, grant) {
30
+ if (typeof channel !== 'string' || channel.length === 0)
31
+ return false;
32
+ const channels = grant[connector];
33
+ return Array.isArray(channels) && channels.includes(channel);
34
+ }
35
+ /**
36
+ * The same rule as a SQL predicate.
37
+ *
38
+ * Returns null when nothing is readable, which callers must treat as "select nothing"
39
+ * rather than "no filter" - an empty WHERE is how a visibility rule turns into a full scan
40
+ * at the exact moment it should have refused.
41
+ *
42
+ * `requestedConnectors` narrows further: a connector the caller did not ask for is not read
43
+ * even though it is granted. The grant is a ceiling, never an instruction.
44
+ */
45
+ function channelGrantClause(grant, requestedConnectors, columns) {
46
+ const pairs = [];
47
+ const params = [];
48
+ for (const [connector, channels] of Object.entries(grant)) {
49
+ if (Array.isArray(requestedConnectors) && !requestedConnectors.includes(connector))
50
+ continue;
51
+ // Array.isArray, not `?? []`: the boolean form denies a malformed grant quietly, so
52
+ // this one must not throw on it. Two forms of a rule that disagree on bad input have
53
+ // already started to be two rules.
54
+ if (!Array.isArray(channels))
55
+ continue;
56
+ const unique = [
57
+ ...new Set(channels.filter((channel) => typeof channel === 'string' && channel.length > 0)),
58
+ ];
59
+ if (unique.length === 0)
60
+ continue;
61
+ pairs.push(`(${columns.connector} = ? AND ${columns.channel} IN (${unique.map(() => '?').join(', ')}))`);
62
+ params.push(connector, ...unique);
63
+ }
64
+ return pairs.length === 0 ? null : { sql: `(${pairs.join(' OR ')})`, params };
65
+ }
66
+ //# sourceMappingURL=channel-grant.js.map
@@ -152,7 +152,7 @@ function hasSeedRefVisibilityConstraints(input, boundary) {
152
152
  input.range !== undefined ||
153
153
  (input.as_of !== undefined && input.as_of !== null));
154
154
  }
155
- function assertSeedRefsVisibleToInput(adapter, input, seedRefs) {
155
+ function assertSeedRefsVisibleToInput(adapter, input, seedRefs, boundary) {
156
156
  if (seedRefs.length === 0) {
157
157
  return [];
158
158
  }
@@ -179,6 +179,9 @@ function assertSeedRefsVisibleToInput(adapter, input, seedRefs) {
179
179
  tenantId: input.tenant_id,
180
180
  startMs: minVisibleMs(input),
181
181
  asOfMs: maxVisibleMs(input),
182
+ // Without this a caller could seed a raw id from a channel the grant refuses and have
183
+ // its identifiers surface in the packet - a citation path reaching past the read path.
184
+ ...(boundary?.channels ? { channels: boundary.channels } : {}),
182
185
  });
183
186
  for (const ref of seedRefs) {
184
187
  if (ref.kind !== 'raw') {
@@ -278,7 +281,7 @@ async function compileContext(input, deps = {}) {
278
281
  });
279
282
  }
280
283
  if (deps.adapter || hasSeedRefVisibilityConstraints(effectiveInput, boundary)) {
281
- canonicalSeedRefs = assertSeedRefsVisibleToInput(deps.adapter, effectiveInput, seedRefs);
284
+ canonicalSeedRefs = assertSeedRefsVisibleToInput(deps.adapter, effectiveInput, seedRefs, boundary);
282
285
  }
283
286
  const canonicalScopes = (0, visibility_js_2.canonicalizeContextScopes)(effectiveInput.scopes);
284
287
  const readInput = sourceInput(effectiveInput, boundary);
@@ -1,5 +1,6 @@
1
1
  export * from './types.js';
2
2
  export * from './boundary-defaults.js';
3
+ export * from './channel-grant.js';
3
4
  export * from './ref.js';
4
5
  export * from './visibility.js';
5
6
  export * from './packet-store.js';
@@ -16,6 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./types.js"), exports);
18
18
  __exportStar(require("./boundary-defaults.js"), exports);
19
+ __exportStar(require("./channel-grant.js"), exports);
19
20
  __exportStar(require("./ref.js"), exports);
20
21
  __exportStar(require("./visibility.js"), exports);
21
22
  __exportStar(require("./packet-store.js"), exports);
@@ -3,7 +3,6 @@ import type { ContextPacketRecord, TrustedContextPacketLookupInput } from './typ
3
3
  type ContextPacketAdapter = Pick<DatabaseAdapter, 'prepare'>;
4
4
  export declare function insertContextPacket(adapter: ContextPacketAdapter, packet: ContextPacketRecord): ContextPacketRecord;
5
5
  export declare function getContextPacket(adapter: ContextPacketAdapter, packetId: string): ContextPacketRecord | null;
6
- export declare function listContextPacketsForModelRun(adapter: ContextPacketAdapter, modelRunId: string, limit?: number): ContextPacketRecord[];
7
6
  export declare function getContextPacketForTrustedUse(adapter: ContextPacketAdapter, input: TrustedContextPacketLookupInput): ContextPacketRecord | null;
8
7
  export {};
9
8
  //# sourceMappingURL=packet-store.d.ts.map
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.insertContextPacket = insertContextPacket;
4
4
  exports.getContextPacket = getContextPacket;
5
- exports.listContextPacketsForModelRun = listContextPacketsForModelRun;
6
5
  exports.getContextPacketForTrustedUse = getContextPacketForTrustedUse;
7
6
  function parseJson(row, field) {
8
7
  const value = row[field];
@@ -222,21 +221,6 @@ function getContextPacket(adapter, packetId) {
222
221
  const row = adapter.prepare('SELECT * FROM context_packets WHERE packet_id = ?').get(packetId);
223
222
  return row ? mapPacketRow(row) : null;
224
223
  }
225
- function listContextPacketsForModelRun(adapter, modelRunId, limit = 50) {
226
- if (!Number.isFinite(limit) || limit <= 0) {
227
- return [];
228
- }
229
- const rows = adapter
230
- .prepare(`
231
- SELECT *
232
- FROM context_packets
233
- WHERE model_run_id = ?
234
- ORDER BY created_at DESC, packet_id DESC
235
- LIMIT ?
236
- `)
237
- .all(modelRunId, Math.floor(limit));
238
- return rows.map((row) => mapPacketRow(row));
239
- }
240
224
  function getContextPacketForTrustedUse(adapter, input) {
241
225
  const packet = getContextPacket(adapter, input.packetId);
242
226
  if (!packet) {
@@ -64,6 +64,7 @@ export interface ContextSourceReaderDeps {
64
64
  export declare function sourceRefsFromCandidates(candidates: readonly ContextCandidate[]): ContextRef[];
65
65
  export declare function readMemoryCandidates(input: ContextSourceReadInput, deps?: ContextSourceReaderDeps): Promise<ContextSourceReadResult>;
66
66
  export declare function readRawCandidates(adapter: ContextSourceAdapter, input: ContextSourceReadInput): ContextSourceReadResult;
67
+ export declare function setContextSourceClockForTests(clock: () => number): void;
67
68
  export declare function readGraphCandidates(adapter: ContextSourceAdapter, input: ContextSourceReadInput, visibleRefs: readonly ContextRef[], deps?: ContextSourceReaderDeps): ContextSourceReadResult;
68
69
  export {};
69
70
  //# sourceMappingURL=source-readers.d.ts.map
@@ -3,12 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sourceRefsFromCandidates = sourceRefsFromCandidates;
4
4
  exports.readMemoryCandidates = readMemoryCandidates;
5
5
  exports.readRawCandidates = readRawCandidates;
6
+ exports.setContextSourceClockForTests = setContextSourceClockForTests;
6
7
  exports.readGraphCandidates = readGraphCandidates;
7
8
  const ref_validation_js_1 = require("../edges/ref-validation.js");
8
9
  const api_js_1 = require("../memory/api.js");
9
10
  const ref_js_1 = require("./ref.js");
10
11
  const visibility_js_1 = require("./visibility.js");
11
12
  const boundary_defaults_js_1 = require("./boundary-defaults.js");
13
+ const channel_grant_js_1 = require("./channel-grant.js");
12
14
  function hiddenAggregate() {
13
15
  return { total: 0, by_kind: {}, by_reason: {} };
14
16
  }
@@ -504,6 +506,18 @@ function readRawCandidates(adapter, input) {
504
506
  if (!tableExists(adapter, 'connector_event_index')) {
505
507
  return resultFromCandidates([], hiddenAggregate());
506
508
  }
509
+ // These guards hold on BOTH paths. An explicitly emptied project window and an empty
510
+ // scope list are deliberate "read nothing" requests, and the grant branch skipping them
511
+ // would have turned two fail-closed states into reads.
512
+ if ((effectiveInput.connectors ?? []).length === 0 ||
513
+ (effectiveInput.scopes ?? []).length === 0 ||
514
+ hasExplicitEmptyProjectWindow(effectiveInput)) {
515
+ return resultFromCandidates([], hiddenAggregate());
516
+ }
517
+ const channelGrant = effectiveInput.boundary?.channels;
518
+ if (channelGrant) {
519
+ return readRawCandidatesWithinGrant(adapter, effectiveInput, channelGrant);
520
+ }
507
521
  const connectors = effectiveInput.connectors ?? [];
508
522
  const scopes = effectiveInput.scopes ?? [];
509
523
  const projectIds = (effectiveInput.project_refs ?? []).map((project) => project.id);
@@ -512,11 +526,6 @@ function readRawCandidates(adapter, input) {
512
526
  projectIds.length > 0 ||
513
527
  Boolean(effectiveInput.tenant_id);
514
528
  const includesLegacyGlobalSystem = hasGlobalSystemScope && !hasScopedVisibility;
515
- if (connectors.length === 0 ||
516
- scopes.length === 0 ||
517
- hasExplicitEmptyProjectWindow(effectiveInput)) {
518
- return resultFromCandidates([], hiddenAggregate());
519
- }
520
529
  const clauses = [`source_connector IN (${placeholders(connectors)})`];
521
530
  const params = [...connectors];
522
531
  if (projectIds.length > 0) {
@@ -571,29 +580,150 @@ function readRawCandidates(adapter, input) {
571
580
  LIMIT ?
572
581
  `)
573
582
  .all(...params);
574
- const candidates = rows.map((row) => {
575
- const sourceId = typeof row.source_id === 'string' && row.source_id.trim().length > 0
576
- ? row.source_id
577
- : undefined;
578
- const ref = {
583
+ return resultFromCandidates(rows.map(rawRowToCandidate), hiddenAggregate());
584
+ }
585
+ /**
586
+ * Raw visibility decided by the caller's (connector, channel) grant.
587
+ *
588
+ * Two conditions, both necessary: the connector is granted, and the channel is one of the
589
+ * channels granted for it. Nothing else gates a row - not project, not tenant, not the
590
+ * derived scope columns. See ContextBoundary.channels for why those were removed rather
591
+ * than kept alongside: measured on the live index, they hide two thirds of the corpus from
592
+ * any caller precise enough to name a scope, and a reader that answers narrow questions
593
+ * from nothing is worse than one that answers them from the events.
594
+ *
595
+ * The grant is enforced in SQL rather than filtered afterwards, so `limit` counts rows the
596
+ * caller may actually see. Filtering after the limit silently turns "the 200 most recent
597
+ * events" into "however many of the 200 most recent events happened to be readable".
598
+ */
599
+ function readRawCandidatesWithinGrant(adapter, effectiveInput, grant) {
600
+ const requested = effectiveInput.connectors;
601
+ if (Array.isArray(requested) && requested.length === 0) {
602
+ return resultFromCandidates([], hiddenAggregate());
603
+ }
604
+ // The rule is compiled to SQL in exactly one place (channel-grant.ts), which is also
605
+ // where its boolean form lives. Building the clause here a second time is how the three
606
+ // copies this replaces came about.
607
+ const granted = (0, channel_grant_js_1.channelGrantClause)(grant, requested, {
608
+ connector: 'source_connector',
609
+ channel: 'channel',
610
+ });
611
+ // No overlap between what was asked for and what is granted reads nothing. It must not
612
+ // fall through to an unfiltered scan.
613
+ if (granted === null) {
614
+ return resultFromCandidates([], refusedAggregate(countRefusedByGrant(adapter, effectiveInput)));
615
+ }
616
+ const clauses = [granted.sql];
617
+ const params = [...granted.params];
618
+ const min = minVisibleTimeMs(effectiveInput);
619
+ if (min !== null) {
620
+ clauses.push('COALESCE(event_datetime, source_timestamp_ms) >= ?');
621
+ params.push(min);
622
+ }
623
+ // EVIDENCE IS WHAT HAS ALREADY HAPPENED, so the window ends now unless the caller named
624
+ // a later end explicitly.
625
+ //
626
+ // Found by running this reader against a snapshot of the live index rather than against
627
+ // fixtures. `event_datetime` is when the event OCCURS, and a calendar entry occurs in
628
+ // the future: 2,762 live rows are future-dated, one of them in 2056. Ordering by that
629
+ // column descending gave them the entire top-100 - a report asking what happened in the
630
+ // last seven days received one hundred calendar entries and zero messages. Excluding
631
+ // them, the same query returns 99 messages and one calendar entry.
632
+ //
633
+ // Nothing caught this before because the predicate this replaces returned no rows at
634
+ // all, so its ordering never mattered. A caller that genuinely wants the future still
635
+ // gets it by setting range.end_ms or as_of past now.
636
+ const max = maxVisibleTimeMs(effectiveInput) ?? nowMs();
637
+ clauses.push('COALESCE(event_datetime, source_timestamp_ms) <= ?');
638
+ params.push(max);
639
+ params.push(normalizeLimit(effectiveInput.limit));
640
+ const rows = adapter
641
+ .prepare(`
642
+ SELECT event_index_id, source_connector, source_id, channel, title, content,
643
+ event_datetime, source_timestamp_ms
644
+ FROM connector_event_index
645
+ WHERE ${clauses.join('\n AND ')}
646
+ ORDER BY COALESCE(event_datetime, source_timestamp_ms) DESC, event_index_id ASC
647
+ LIMIT ?
648
+ `)
649
+ .all(...params);
650
+ return resultFromCandidates(rows.map(rawRowToCandidate), refusedAggregate(countRefusedByGrant(adapter, effectiveInput, granted)));
651
+ }
652
+ /**
653
+ * How many events the grant kept out.
654
+ *
655
+ * Without this the caller cannot tell "this channel was quiet" from "you are not allowed to
656
+ * see this channel", because the filtering happens in SQL and leaves no trace. That
657
+ * distinction is the entire point: an owner asking about a board whose events are all
658
+ * refused would otherwise get a confident empty answer with a clean diagnostics block.
659
+ *
660
+ * Counted in the requested connectors only - rows of connectors the run never asked about
661
+ * are not "refused", they are simply not in question.
662
+ */
663
+ function countRefusedByGrant(adapter, effectiveInput, granted = null) {
664
+ const requested = effectiveInput.connectors ?? [];
665
+ if (requested.length === 0)
666
+ return 0;
667
+ const clauses = [`source_connector IN (${placeholders(requested)})`];
668
+ const params = [...requested];
669
+ if (granted !== null) {
670
+ // `NOT (connector = ? AND channel IN (...))` is NULL when channel is NULL, so a
671
+ // NULL-channel row is neither selected nor counted - it vanishes from both sides of
672
+ // the answer. A connector that does not populate channel would have every event
673
+ // refused and the count would report zero, which is precisely the confident empty
674
+ // answer this count exists to prevent.
675
+ clauses.push(`(channel IS NULL OR NOT ${granted.sql})`);
676
+ params.push(...granted.params);
677
+ }
678
+ const min = minVisibleTimeMs(effectiveInput);
679
+ const max = maxVisibleTimeMs(effectiveInput);
680
+ if (min !== null) {
681
+ clauses.push('COALESCE(event_datetime, source_timestamp_ms) >= ?');
682
+ params.push(min);
683
+ }
684
+ if (max !== null) {
685
+ clauses.push('COALESCE(event_datetime, source_timestamp_ms) <= ?');
686
+ params.push(max);
687
+ }
688
+ const row = adapter
689
+ .prepare(`SELECT COUNT(*) AS n FROM connector_event_index WHERE ${clauses.join(' AND ')}`)
690
+ .get(...params);
691
+ return Number(row?.n ?? 0);
692
+ }
693
+ /** Injectable only so a test can pin the boundary; production reads the clock. */
694
+ let nowMs = () => Date.now();
695
+ function setContextSourceClockForTests(clock) {
696
+ nowMs = clock;
697
+ }
698
+ function refusedAggregate(refused) {
699
+ const aggregate = hiddenAggregate();
700
+ if (refused > 0) {
701
+ aggregate.total = refused;
702
+ aggregate.by_kind.raw = refused;
703
+ aggregate.by_reason.channel_not_granted = refused;
704
+ }
705
+ return aggregate;
706
+ }
707
+ function rawRowToCandidate(row) {
708
+ const sourceId = typeof row.source_id === 'string' && row.source_id.trim().length > 0
709
+ ? row.source_id
710
+ : undefined;
711
+ return {
712
+ ref: {
579
713
  kind: 'raw',
580
714
  connector: String(row.source_connector),
581
715
  raw_id: String(row.event_index_id),
582
716
  ...(sourceId ? { source_id: sourceId } : {}),
583
717
  channel_id: typeof row.channel === 'string' ? row.channel : null,
584
- };
585
- return {
586
- ref,
587
- title: String(row.title ?? 'Raw event'),
588
- excerpt: String(row.content ?? '').slice(0, 500),
589
- score: 0.7,
590
- timestamp_ms: parseTimestampMs(row.event_datetime ?? row.source_timestamp_ms),
591
- source: 'raw',
592
- visible: true,
593
- support: emptySupport('connector_event_index'),
594
- };
595
- });
596
- return resultFromCandidates(candidates, hiddenAggregate());
718
+ },
719
+ title: String(row.title ?? 'Raw event'),
720
+ excerpt: String(row.content ?? '').slice(0, 500),
721
+ score: 0.7,
722
+ timestamp_ms: parseTimestampMs(row.event_datetime ?? row.source_timestamp_ms),
723
+ source: 'raw',
724
+ visible: true,
725
+ support: emptySupport('connector_event_index'),
726
+ };
597
727
  }
598
728
  function readGraphCandidates(adapter, input, visibleRefs, deps = {}) {
599
729
  const effectiveInput = normalizeTimeFilters(applyBoundaryDefaults(input));
@@ -633,6 +763,9 @@ function readGraphCandidates(adapter, input, visibleRefs, deps = {}) {
633
763
  startMs: min,
634
764
  asOfMs: max,
635
765
  limit: normalizeLimit(effectiveInput.limit),
766
+ // Graph neighbours carry an excerpt, so an ungranted raw row reached through an edge
767
+ // would disclose content the reader itself refuses.
768
+ ...(effectiveInput.boundary?.channels ? { channels: effectiveInput.boundary.channels } : {}),
636
769
  }).filter((edge) => (min === null || edge.created_at >= min) && (max === null || edge.created_at <= max));
637
770
  const candidates = [];
638
771
  const emitted = new Set();
@@ -49,6 +49,23 @@ export interface ContextBoundary {
49
49
  tenant_id?: string | null;
50
50
  as_of?: string | number | null;
51
51
  range?: ContextRange;
52
+ /**
53
+ * Which channels of each connector may be read, keyed by connector.
54
+ *
55
+ * When present this DECIDES raw visibility, and the scope/project/tenant clauses are not
56
+ * applied to raw rows. That is a deliberate replacement, not a relaxation, and the reason
57
+ * is measurable: on a live index of 30,671 events, `memory_scope_id` is populated on 37%
58
+ * of rows in three mutually incompatible namespaces, and 63% carry no scope at all - so a
59
+ * caller that names any specific scope loses exactly the two thirds of the corpus that
60
+ * were never labelled. A filter that hides most of the data from anyone precise enough to
61
+ * ask a narrow question is not a privacy boundary; it is a leak of a different kind, the
62
+ * kind where the system quietly answers from nothing.
63
+ *
64
+ * The grant is (connector, channel) because that is what the owner actually configures
65
+ * and what the connector actually emits. The caller computes it - this module enforces
66
+ * what it is given and never widens it.
67
+ */
68
+ channels?: Record<string, readonly string[]>;
52
69
  }
53
70
  export interface CanonicalContextScopes {
54
71
  scopes: MemoryScopeRef[];