@unson/brainbase-mcp 0.41.3 → 0.41.6

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.
@@ -1,3 +1,4 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
1
2
  import { createHash } from 'node:crypto';
2
3
  import { lstat, mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
3
4
  import { join, resolve } from 'node:path';
@@ -12,6 +13,23 @@ export const JUDGMENT_HISTORY_ENTRYPOINTS = Object.freeze([
12
13
  'company_os',
13
14
  'unknown'
14
15
  ]);
16
+ const judgmentHistoryTimingSlots = new AsyncLocalStorage();
17
+ /** Isolates Server-Timing for one HTTP request from other in-flight reads. */
18
+ export function withJudgmentHistoryRequestTimings(fn) {
19
+ return judgmentHistoryTimingSlots.run({ current: null }, fn);
20
+ }
21
+ /** Timings published by the in-flight request, when one is bound. */
22
+ export function judgmentHistoryRequestTimings() {
23
+ const slot = judgmentHistoryTimingSlots.getStore();
24
+ if (!slot)
25
+ return { bound: false, timings: null };
26
+ return { bound: true, timings: slot.current };
27
+ }
28
+ function publishJudgmentHistoryReadTimings(timings) {
29
+ const slot = judgmentHistoryTimingSlots.getStore();
30
+ if (slot)
31
+ slot.current = timings;
32
+ }
15
33
  export class JudgmentHistoryReaderError extends Error {
16
34
  statusCode;
17
35
  code;
@@ -22,6 +40,82 @@ export class JudgmentHistoryReaderError extends Error {
22
40
  this.name = 'JudgmentHistoryReaderError';
23
41
  }
24
42
  }
43
+ export const DEFAULT_LOCAL_JUDGMENT_HISTORY_READ_PARALLEL = 8;
44
+ export const MAX_JUDGMENT_HISTORY_RESOLUTION_BATCH = 100;
45
+ /** Parsed artifacts kept for one local source. The oldest entry is dropped past this. */
46
+ export const DEFAULT_JUDGMENT_HISTORY_JOURNAL_CACHE_LIMIT = 4_096;
47
+ /**
48
+ * Remembers parsed journal artifacts for one local source.
49
+ * The key is (session, turn, file name, mtime). A later read of the same turn
50
+ * misses when that file's mtime changes, and the previous bytes are not reused.
51
+ * A lookup refreshes recency. Past `limit`, the least recently used entry is dropped.
52
+ */
53
+ export class JudgmentHistoryJournalScanCache {
54
+ #files = new Map();
55
+ #limit;
56
+ #hits = 0;
57
+ #misses = 0;
58
+ constructor(limit = DEFAULT_JUDGMENT_HISTORY_JOURNAL_CACHE_LIMIT) {
59
+ if (!Number.isInteger(limit) || limit < 1)
60
+ throw new TypeError('cache limit must be a positive integer');
61
+ this.#limit = limit;
62
+ }
63
+ get limit() {
64
+ return this.#limit;
65
+ }
66
+ get size() {
67
+ return this.#files.size;
68
+ }
69
+ lookup(session, turn, fileName, mtimeNs) {
70
+ const key = `${session}\0${turn}\0${fileName}`;
71
+ const entry = this.#files.get(key);
72
+ if (!entry || entry.mtimeNs !== mtimeNs) {
73
+ if (entry)
74
+ this.#files.delete(key);
75
+ return null;
76
+ }
77
+ this.#files.delete(key);
78
+ this.#files.set(key, entry);
79
+ this.#hits += 1;
80
+ return entry.result;
81
+ }
82
+ store(session, turn, fileName, mtimeNs, result) {
83
+ const key = `${session}\0${turn}\0${fileName}`;
84
+ this.#files.delete(key);
85
+ this.#files.set(key, { mtimeNs, result });
86
+ while (this.#files.size > this.#limit) {
87
+ const oldest = this.#files.keys().next().value;
88
+ if (oldest === undefined)
89
+ break;
90
+ this.#files.delete(oldest);
91
+ }
92
+ }
93
+ noteMiss() {
94
+ this.#misses += 1;
95
+ }
96
+ get hits() {
97
+ return this.#hits;
98
+ }
99
+ get misses() {
100
+ return this.#misses;
101
+ }
102
+ }
103
+ /** Runs `map` with at most `limit` calls in flight. Results keep input order. */
104
+ export async function mapWithConcurrencyLimit(items, limit, map) {
105
+ if (!Number.isInteger(limit) || limit < 1)
106
+ throw new TypeError('parallel limit must be a positive integer');
107
+ const results = new Array(items.length);
108
+ let next = 0;
109
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
110
+ while (next < items.length) {
111
+ const index = next;
112
+ next += 1;
113
+ results[index] = await map(items[index], index);
114
+ }
115
+ });
116
+ await Promise.all(workers);
117
+ return results;
118
+ }
25
119
  export class JudgmentHistoryFeedbackError extends Error {
26
120
  statusCode;
27
121
  code;
@@ -945,21 +1039,12 @@ function mergeRecordWithFinal(adoptionRecord, final, sessionRef, key, turnInput,
945
1039
  };
946
1040
  return { ...baseRecord, missing_fields: missingFields(baseRecord, firstPresent(blocks, 'missing_fields')) };
947
1041
  }
948
- async function readLocalRecords(root, configuredEntrypoint, maxFiles, maxRecordBytes) {
1042
+ async function loadNewestJournalGroups(root, maxFiles, maxRecordBytes, cache, readParallel) {
949
1043
  const sessions = await readdir(root, { withFileTypes: true });
950
1044
  const groups = new Map();
1045
+ const discovered = [];
951
1046
  let invalidCount = 0;
952
- let candidateCount = 0;
953
- let capped = false;
954
- const stop = () => {
955
- if (candidateCount >= maxFiles) {
956
- capped = true;
957
- return true;
958
- }
959
- candidateCount += 1;
960
- return false;
961
- };
962
- outer: for (const session of sessions.sort((left, right) => left.name.localeCompare(right.name))) {
1047
+ for (const session of sessions.sort((left, right) => left.name.localeCompare(right.name))) {
963
1048
  if (session.isSymbolicLink()) {
964
1049
  invalidCount += 1;
965
1050
  continue;
@@ -975,7 +1060,6 @@ async function readLocalRecords(root, configuredEntrypoint, maxFiles, maxRecordB
975
1060
  invalidCount += 1;
976
1061
  continue;
977
1062
  }
978
- const groupMap = new Map();
979
1063
  const recognized = names.map((entry) => {
980
1064
  const name = entry.name;
981
1065
  if (ADOPTION_FILE.test(name))
@@ -1002,12 +1086,9 @@ async function readLocalRecords(root, configuredEntrypoint, maxFiles, maxRecordB
1002
1086
  invalidCount += 1;
1003
1087
  continue;
1004
1088
  }
1005
- if (stop())
1006
- break outer;
1007
- const group = groupMap.get(key) ?? groupFor(groups, session.name, key);
1008
- groupMap.set(key, group);
1009
1089
  const path = join(sessionPath, candidate.entry.name);
1010
1090
  if (candidate.kind === 'events') {
1091
+ const group = groupFor(groups, session.name, key);
1011
1092
  group.eventDirectorySeen = true;
1012
1093
  if (candidate.entry.isSymbolicLink() || !candidate.entry.isDirectory()) {
1013
1094
  group.invalid += 1;
@@ -1024,33 +1105,106 @@ async function readLocalRecords(root, configuredEntrypoint, maxFiles, maxRecordB
1024
1105
  for (const eventEntry of eventEntries.sort((left, right) => left.name.localeCompare(right.name))) {
1025
1106
  if (!eventEntry.name.endsWith('.json'))
1026
1107
  continue;
1027
- if (stop())
1028
- break outer;
1029
- const eventResult = await readBoundedJson(join(path, eventEntry.name), maxRecordBytes);
1030
- const eventKey = eventEntry.name.endsWith('.json') ? eventEntry.name.slice(0, -5) : undefined;
1031
- if (eventResult.invalid || !eventResult.value || !validJournalEvent(eventResult.value, session.name, candidate.key, eventKey)) {
1032
- group.invalid += 1;
1033
- continue;
1034
- }
1035
- group.events.push(eventResult.value);
1108
+ discovered.push({
1109
+ session: session.name,
1110
+ key,
1111
+ kind: 'event',
1112
+ path: join(path, eventEntry.name),
1113
+ name: eventEntry.name,
1114
+ eventKey: eventEntry.name.slice(0, -5),
1115
+ mtimeNs: 0n
1116
+ });
1036
1117
  }
1037
1118
  continue;
1038
1119
  }
1039
- const result = await readBoundedJson(path, maxRecordBytes);
1040
- if (result.invalid || !result.value) {
1120
+ discovered.push({
1121
+ session: session.name,
1122
+ key,
1123
+ kind: candidate.kind,
1124
+ path,
1125
+ name: candidate.entry.name,
1126
+ mtimeNs: 0n
1127
+ });
1128
+ }
1129
+ }
1130
+ const stated = await mapWithConcurrencyLimit(discovered, readParallel, async (candidate) => {
1131
+ let mtimeNs = 0n;
1132
+ try {
1133
+ mtimeNs = (await lstat(candidate.path, { bigint: true })).mtimeNs;
1134
+ }
1135
+ catch {
1136
+ mtimeNs = 0n;
1137
+ }
1138
+ return { ...candidate, mtimeNs };
1139
+ });
1140
+ const newestFirst = [...stated].sort((left, right) => {
1141
+ if (left.mtimeNs === right.mtimeNs)
1142
+ return left.path.localeCompare(right.path);
1143
+ return left.mtimeNs > right.mtimeNs ? -1 : 1;
1144
+ });
1145
+ const capped = newestFirst.length > maxFiles;
1146
+ const selectedPaths = new Set(newestFirst.slice(0, maxFiles).map((candidate) => candidate.path));
1147
+ const selected = stated.filter((candidate) => selectedPaths.has(candidate.path));
1148
+ let cacheHits = 0;
1149
+ let cacheMisses = 0;
1150
+ const loaded = await mapWithConcurrencyLimit(selected, readParallel, async (candidate) => {
1151
+ const hit = cache.lookup(candidate.session, candidate.key, candidate.name, candidate.mtimeNs);
1152
+ if (hit) {
1153
+ cacheHits += 1;
1154
+ return { path: candidate.path, result: hit };
1155
+ }
1156
+ cacheMisses += 1;
1157
+ cache.noteMiss();
1158
+ const result = await readBoundedJson(candidate.path, maxRecordBytes);
1159
+ cache.store(candidate.session, candidate.key, candidate.name, candidate.mtimeNs, result);
1160
+ return { path: candidate.path, result };
1161
+ });
1162
+ const byPath = new Map(loaded.map((item) => [item.path, item.result]));
1163
+ for (const candidate of selected) {
1164
+ const group = groupFor(groups, candidate.session, candidate.key);
1165
+ const result = byPath.get(candidate.path);
1166
+ if (!result) {
1167
+ group.invalid += 1;
1168
+ continue;
1169
+ }
1170
+ if (candidate.kind === 'event') {
1171
+ group.eventDirectorySeen = true;
1172
+ if (result.invalid || !result.value || !validJournalEvent(result.value, candidate.session, candidate.key, candidate.eventKey)) {
1041
1173
  group.invalid += 1;
1042
1174
  continue;
1043
1175
  }
1044
- if (candidate.kind === 'adoption')
1045
- group.adoption = result.value;
1046
- else if (candidate.kind === 'final')
1047
- group.final = result.value;
1048
- else if (candidate.kind === 'episode')
1049
- group.episode = result.value;
1050
- else
1051
- group.turnInput = result.value;
1176
+ group.events.push(result.value);
1177
+ continue;
1178
+ }
1179
+ if (result.invalid || !result.value) {
1180
+ group.invalid += 1;
1181
+ continue;
1052
1182
  }
1183
+ if (candidate.kind === 'adoption')
1184
+ group.adoption = result.value;
1185
+ else if (candidate.kind === 'final')
1186
+ group.final = result.value;
1187
+ else if (candidate.kind === 'episode')
1188
+ group.episode = result.value;
1189
+ else
1190
+ group.turnInput = result.value;
1053
1191
  }
1192
+ return { groups, invalidCount, candidateCount: selected.length, capped, cacheHits, cacheMisses };
1193
+ }
1194
+ async function readLocalRecords(root, configuredEntrypoint, maxFiles, maxRecordBytes, cache, readParallel) {
1195
+ const loaded = await loadNewestJournalGroups(root, maxFiles, maxRecordBytes, cache, readParallel);
1196
+ const groups = loaded.groups;
1197
+ let invalidCount = loaded.invalidCount;
1198
+ let candidateCount = loaded.candidateCount;
1199
+ let capped = loaded.capped;
1200
+ const stop = () => {
1201
+ if (candidateCount >= maxFiles) {
1202
+ capped = true;
1203
+ return true;
1204
+ }
1205
+ candidateCount += 1;
1206
+ return false;
1207
+ };
1054
1208
  const records = [];
1055
1209
  const seen = new Set();
1056
1210
  for (const group of groups.values()) {
@@ -1139,7 +1293,7 @@ async function readLocalRecords(root, configuredEntrypoint, maxFiles, maxRecordB
1139
1293
  }
1140
1294
  records.sort(compareRecords);
1141
1295
  const contentDigest = sha256(canonicalJson({ records, candidateCount, invalidCount, capped }));
1142
- return { records, invalidCount, candidateCount, capped, contentDigest };
1296
+ return { records, invalidCount, candidateCount, capped, contentDigest, cacheHits: loaded.cacheHits, cacheMisses: loaded.cacheMisses };
1143
1297
  }
1144
1298
  function compareRecords(left, right) {
1145
1299
  const dateOrder = right.recorded_at.localeCompare(left.recorded_at);
@@ -1171,30 +1325,50 @@ export function createLocalJudgmentHistorySource(options) {
1171
1325
  throw new TypeError('max_files must be a positive integer');
1172
1326
  if (!Number.isInteger(maxRecordBytes) || maxRecordBytes < 1)
1173
1327
  throw new TypeError('max_record_bytes must be a positive integer');
1328
+ const readParallel = options.read_parallel ?? DEFAULT_LOCAL_JUDGMENT_HISTORY_READ_PARALLEL;
1329
+ if (!Number.isInteger(readParallel) || readParallel < 1)
1330
+ throw new TypeError('read_parallel must be a positive integer');
1331
+ const cache = options.cache ?? new JudgmentHistoryJournalScanCache();
1174
1332
  const root = resolve(options.root);
1333
+ let lastTimings = null;
1175
1334
  return {
1176
1335
  storage: 'local',
1336
+ timings: () => lastTimings,
1177
1337
  async read() {
1338
+ const started = performance.now();
1339
+ const finish = (scanned, cacheHits = 0, cacheMisses = 0, snapshot) => {
1340
+ const journalMs = performance.now() - started;
1341
+ const timings = {
1342
+ total_ms: journalMs,
1343
+ journal_ms: journalMs,
1344
+ page_ms: 0,
1345
+ cache_hits: cacheHits,
1346
+ cache_misses: cacheMisses,
1347
+ parallel_limit: readParallel,
1348
+ scanned_files: scanned
1349
+ };
1350
+ lastTimings = timings;
1351
+ return { ...snapshot, timings };
1352
+ };
1178
1353
  try {
1179
1354
  const rootStats = await lstat(root);
1180
1355
  if (rootStats.isSymbolicLink()) {
1181
- return { status: 'unavailable', records: null, coverage: unavailableCoverage('local', 'journal_symlink_rejected') };
1356
+ return finish(0, 0, 0, { status: 'unavailable', records: null, coverage: unavailableCoverage('local', 'journal_symlink_rejected') });
1357
+ }
1358
+ if (!rootStats.isDirectory()) {
1359
+ return finish(0, 0, 0, { status: 'unavailable', records: null, coverage: unavailableCoverage('local', 'journal_not_directory') });
1182
1360
  }
1183
- if (!rootStats.isDirectory())
1184
- return { status: 'unavailable', records: null, coverage: unavailableCoverage('local', 'journal_not_directory') };
1185
1361
  }
1186
1362
  catch (error) {
1187
- if (error.code === 'ENOENT') {
1188
- return { status: 'unavailable', records: null, coverage: unavailableCoverage('local', 'journal_unavailable') };
1189
- }
1190
- return { status: 'unavailable', records: null, coverage: unavailableCoverage('local', 'journal_unreadable') };
1363
+ const reason = error.code === 'ENOENT' ? 'journal_unavailable' : 'journal_unreadable';
1364
+ return finish(0, 0, 0, { status: 'unavailable', records: null, coverage: unavailableCoverage('local', reason) });
1191
1365
  }
1192
1366
  let result;
1193
1367
  try {
1194
- result = await readLocalRecords(root, configuredEntrypoint, maxFiles, maxRecordBytes);
1368
+ result = await readLocalRecords(root, configuredEntrypoint, maxFiles, maxRecordBytes, cache, readParallel);
1195
1369
  }
1196
1370
  catch {
1197
- return { status: 'unavailable', records: null, coverage: unavailableCoverage('local', 'journal_unreadable') };
1371
+ return finish(0, 0, 0, { status: 'unavailable', records: null, coverage: unavailableCoverage('local', 'journal_unreadable') });
1198
1372
  }
1199
1373
  const status = result.invalidCount > 0 || result.capped ? 'partial' : 'available';
1200
1374
  const reason = result.capped ? 'journal_scan_capped' : result.invalidCount > 0 ? 'some_journal_records_unreadable' : null;
@@ -1211,7 +1385,7 @@ export function createLocalJudgmentHistorySource(options) {
1211
1385
  sources,
1212
1386
  total: status === 'available' ? result.records.length : null
1213
1387
  };
1214
- return {
1388
+ return finish(result.candidateCount, result.cacheHits, result.cacheMisses, {
1215
1389
  status,
1216
1390
  records: result.records,
1217
1391
  coverage,
@@ -1224,7 +1398,7 @@ export function createLocalJudgmentHistorySource(options) {
1224
1398
  capped: result.capped,
1225
1399
  content_digest: result.contentDigest
1226
1400
  })
1227
- };
1401
+ });
1228
1402
  }
1229
1403
  };
1230
1404
  }
@@ -1417,105 +1591,190 @@ export function createJudgmentHistoryReader(options) {
1417
1591
  if (!options?.source || typeof options.source.read !== 'function')
1418
1592
  throw new TypeError('source is required');
1419
1593
  const now = options.now ?? (() => new Date());
1594
+ let lastTimings = null;
1595
+ const takeSource = async () => {
1596
+ const snapshot = await options.source.read();
1597
+ return {
1598
+ source: buildSourceSnapshot(snapshot),
1599
+ sourceTimings: snapshot.timings ?? options.source.timings?.() ?? null
1600
+ };
1601
+ };
1602
+ const remember = (readStarted, pageStarted, sourceTimings) => {
1603
+ const finished = performance.now();
1604
+ const timings = {
1605
+ total_ms: finished - readStarted,
1606
+ journal_ms: sourceTimings?.journal_ms ?? Math.max(0, pageStarted - readStarted),
1607
+ page_ms: Math.max(0, finished - pageStarted),
1608
+ cache_hits: sourceTimings?.cache_hits ?? 0,
1609
+ cache_misses: sourceTimings?.cache_misses ?? 0,
1610
+ parallel_limit: sourceTimings?.parallel_limit ?? 0,
1611
+ scanned_files: sourceTimings?.scanned_files ?? 0
1612
+ };
1613
+ lastTimings = timings;
1614
+ publishJudgmentHistoryReadTimings(timings);
1615
+ };
1420
1616
  return {
1617
+ timings: () => lastTimings,
1421
1618
  async home(inputQuery = {}) {
1422
1619
  const query = normalizeQuery(inputQuery);
1423
1620
  if (query.project !== null && options.authorizeProject && !(await options.authorizeProject(query.project))) {
1424
1621
  throw new JudgmentHistoryReaderError(403, 'project_scope_forbidden', 'project is outside the trusted scope');
1425
1622
  }
1426
- const source = buildSourceSnapshot(await options.source.read());
1427
- let records = source.records;
1428
- const filters = { period: query.period, project: query.project, entrypoint: query.entrypoint };
1429
- if (source.status === 'unavailable' || records === null) {
1430
- return {
1623
+ const readStarted = performance.now();
1624
+ const loaded = await takeSource();
1625
+ const source = loaded.source;
1626
+ const pageStarted = performance.now();
1627
+ try {
1628
+ let records = source.records;
1629
+ const filters = { period: query.period, project: query.project, entrypoint: query.entrypoint };
1630
+ if (source.status === 'unavailable' || records === null) {
1631
+ return {
1632
+ contract_version: JUDGMENT_HISTORY_CONTRACT_VERSION,
1633
+ status: source.status,
1634
+ records: null,
1635
+ coverage: { ...source.coverage, total: null },
1636
+ pagination: { cursor: query.cursor, next_cursor: null, limit: query.limit, has_more: false },
1637
+ filters
1638
+ };
1639
+ }
1640
+ let scopeUnverified = false;
1641
+ if (query.project === null && options.authorizeProject) {
1642
+ const visible = [];
1643
+ for (const record of records) {
1644
+ // A record without a trusted project binding must never enter a
1645
+ // scoped aggregate, because its owner cannot be authorized.
1646
+ if (record.project_code === null) {
1647
+ scopeUnverified = true;
1648
+ continue;
1649
+ }
1650
+ if (await options.authorizeProject(record.project_code))
1651
+ visible.push(record);
1652
+ }
1653
+ records = visible;
1654
+ }
1655
+ const effectiveNow = now();
1656
+ const start = periodStart(query.period, effectiveNow);
1657
+ const anchor = query.period === 'all' ? null : startOfJstDay(effectiveNow);
1658
+ const status = scopeUnverified && source.status === 'available' ? 'partial' : source.status;
1659
+ const baseCoverage = scopeUnverified
1660
+ ? { ...source.coverage, complete: false, reason: 'project_scope_unverified', total: null }
1661
+ : { ...source.coverage, total: null };
1662
+ const base = {
1431
1663
  contract_version: JUDGMENT_HISTORY_CONTRACT_VERSION,
1432
- status: source.status,
1664
+ status,
1433
1665
  records: null,
1434
- coverage: { ...source.coverage, total: null },
1666
+ coverage: baseCoverage,
1435
1667
  pagination: { cursor: query.cursor, next_cursor: null, limit: query.limit, has_more: false },
1436
1668
  filters
1437
1669
  };
1670
+ const filtered = records.filter((record) => {
1671
+ if (query.project !== null && record.project_code !== query.project)
1672
+ return false;
1673
+ if (query.entrypoint !== null && record.entrypoint !== query.entrypoint)
1674
+ return false;
1675
+ if (start && Date.parse(record.recorded_at) < start.getTime())
1676
+ return false;
1677
+ return true;
1678
+ });
1679
+ const filter = filterKey(query, start, anchor);
1680
+ const offset = query.cursor ? decodeCursor(query.cursor, filter, source.snapshot_id) : 0;
1681
+ const page = filtered.slice(offset, offset + query.limit);
1682
+ const hasMore = offset + page.length < filtered.length;
1683
+ const nextCursor = hasMore ? encodeCursor({ filter, snapshot: source.snapshot_id, offset: offset + page.length }) : null;
1684
+ const coverage = {
1685
+ ...baseCoverage,
1686
+ total: baseCoverage.complete ? filtered.length : null
1687
+ };
1688
+ return {
1689
+ ...base,
1690
+ records: page,
1691
+ coverage,
1692
+ pagination: { cursor: query.cursor, next_cursor: nextCursor, limit: query.limit, has_more: hasMore }
1693
+ };
1438
1694
  }
1439
- let scopeUnverified = false;
1440
- if (query.project === null && options.authorizeProject) {
1441
- const visible = [];
1442
- for (const record of records) {
1443
- // A record without a trusted project binding must never enter a
1444
- // scoped aggregate, because its owner cannot be authorized.
1445
- if (record.project_code === null) {
1446
- scopeUnverified = true;
1447
- continue;
1448
- }
1449
- if (await options.authorizeProject(record.project_code))
1450
- visible.push(record);
1451
- }
1452
- records = visible;
1695
+ finally {
1696
+ remember(readStarted, pageStarted, loaded.sourceTimings);
1453
1697
  }
1454
- const effectiveNow = now();
1455
- const start = periodStart(query.period, effectiveNow);
1456
- const anchor = query.period === 'all' ? null : startOfJstDay(effectiveNow);
1457
- const status = scopeUnverified && source.status === 'available' ? 'partial' : source.status;
1458
- const baseCoverage = scopeUnverified
1459
- ? { ...source.coverage, complete: false, reason: 'project_scope_unverified', total: null }
1460
- : { ...source.coverage, total: null };
1461
- const base = {
1462
- contract_version: JUDGMENT_HISTORY_CONTRACT_VERSION,
1463
- status,
1464
- records: null,
1465
- coverage: baseCoverage,
1466
- pagination: { cursor: query.cursor, next_cursor: null, limit: query.limit, has_more: false },
1467
- filters
1468
- };
1469
- const filtered = records.filter((record) => {
1470
- if (query.project !== null && record.project_code !== query.project)
1471
- return false;
1472
- if (query.entrypoint !== null && record.entrypoint !== query.entrypoint)
1473
- return false;
1474
- if (start && Date.parse(record.recorded_at) < start.getTime())
1475
- return false;
1476
- return true;
1477
- });
1478
- const filter = filterKey(query, start, anchor);
1479
- const offset = query.cursor ? decodeCursor(query.cursor, filter, source.snapshot_id) : 0;
1480
- const page = filtered.slice(offset, offset + query.limit);
1481
- const hasMore = offset + page.length < filtered.length;
1482
- const nextCursor = hasMore ? encodeCursor({ filter, snapshot: source.snapshot_id, offset: offset + page.length }) : null;
1483
- const coverage = {
1484
- ...baseCoverage,
1485
- total: baseCoverage.complete ? filtered.length : null
1486
- };
1487
- return {
1488
- ...base,
1489
- records: page,
1490
- coverage,
1491
- pagination: { cursor: query.cursor, next_cursor: nextCursor, limit: query.limit, has_more: hasMore }
1492
- };
1493
1698
  },
1494
1699
  async detail(recordId) {
1495
1700
  const safeRecordId = safeId(recordId);
1496
1701
  if (!safeRecordId)
1497
1702
  throw new JudgmentHistoryReaderError(400, 'invalid_record_id', 'record_id is invalid');
1498
- const source = buildSourceSnapshot(await options.source.read());
1499
- if (source.status === 'unavailable' || source.records === null) {
1703
+ const readStarted = performance.now();
1704
+ const loaded = await takeSource();
1705
+ const source = loaded.source;
1706
+ const pageStarted = performance.now();
1707
+ try {
1708
+ if (source.status === 'unavailable' || source.records === null) {
1709
+ return {
1710
+ contract_version: JUDGMENT_HISTORY_CONTRACT_VERSION,
1711
+ status: source.status,
1712
+ record: null,
1713
+ coverage: source.coverage
1714
+ };
1715
+ }
1716
+ const record = source.records.find((candidate) => candidate.record_id === safeRecordId) ?? null;
1717
+ if (record && options.authorizeProject) {
1718
+ if (record.project_code === null || !(await options.authorizeProject(record.project_code))) {
1719
+ throw new JudgmentHistoryReaderError(403, 'project_scope_forbidden', 'record is outside the trusted scope');
1720
+ }
1721
+ }
1500
1722
  return {
1501
1723
  contract_version: JUDGMENT_HISTORY_CONTRACT_VERSION,
1502
1724
  status: source.status,
1503
- record: null,
1504
- coverage: source.coverage
1725
+ record,
1726
+ coverage: { ...source.coverage, total: source.coverage.complete ? source.records.length : null }
1505
1727
  };
1506
1728
  }
1507
- const record = source.records.find((candidate) => candidate.record_id === safeRecordId) ?? null;
1508
- if (record && options.authorizeProject) {
1509
- if (record.project_code === null || !(await options.authorizeProject(record.project_code))) {
1510
- throw new JudgmentHistoryReaderError(403, 'project_scope_forbidden', 'record is outside the trusted scope');
1729
+ finally {
1730
+ remember(readStarted, pageStarted, loaded.sourceTimings);
1731
+ }
1732
+ },
1733
+ async findByResolutionIds(resolutionIds) {
1734
+ if (!Array.isArray(resolutionIds) || resolutionIds.length < 1 || resolutionIds.length > MAX_JUDGMENT_HISTORY_RESOLUTION_BATCH) {
1735
+ throw new JudgmentHistoryReaderError(400, 'invalid_resolution_ids', `resolution ids must contain between 1 and ${MAX_JUDGMENT_HISTORY_RESOLUTION_BATCH} ids`);
1736
+ }
1737
+ const ids = resolutionIds.map((id) => {
1738
+ const safe = safeId(id);
1739
+ if (!safe)
1740
+ throw new JudgmentHistoryReaderError(400, 'invalid_resolution_id', 'resolution id is invalid');
1741
+ return safe;
1742
+ });
1743
+ const readStarted = performance.now();
1744
+ const loaded = await takeSource();
1745
+ const source = loaded.source;
1746
+ const pageStarted = performance.now();
1747
+ try {
1748
+ if (source.status === 'unavailable' || source.records === null) {
1749
+ return {
1750
+ contract_version: JUDGMENT_HISTORY_CONTRACT_VERSION,
1751
+ status: source.status,
1752
+ records: ids.map(() => null),
1753
+ coverage: source.coverage
1754
+ };
1755
+ }
1756
+ const byId = new Map(source.records.map((record) => [record.record_id, record]));
1757
+ const records = [];
1758
+ for (const id of ids) {
1759
+ const record = byId.get(id) ?? null;
1760
+ if (!record || !options.authorizeProject) {
1761
+ records.push(record);
1762
+ continue;
1763
+ }
1764
+ // A forbidden or unbound project is indistinguishable from a missing id.
1765
+ const allowed = record.project_code !== null && await options.authorizeProject(record.project_code);
1766
+ records.push(allowed ? record : null);
1511
1767
  }
1768
+ return {
1769
+ contract_version: JUDGMENT_HISTORY_CONTRACT_VERSION,
1770
+ status: source.status,
1771
+ records,
1772
+ coverage: { ...source.coverage, total: source.coverage.complete ? source.records.length : null }
1773
+ };
1774
+ }
1775
+ finally {
1776
+ remember(readStarted, pageStarted, loaded.sourceTimings);
1512
1777
  }
1513
- return {
1514
- contract_version: JUDGMENT_HISTORY_CONTRACT_VERSION,
1515
- status: source.status,
1516
- record,
1517
- coverage: { ...source.coverage, total: source.coverage.complete ? source.records.length : null }
1518
- };
1519
1778
  }
1520
1779
  };
1521
1780
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.41.3",
3
+ "version": "0.41.6",
4
4
  "description": "Local-first Brainbase judgment OS that lets supported AI agents reuse a user's approved goals, priorities, decisions, and judgment criteria.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -483,5 +483,5 @@
483
483
  "engines": {
484
484
  "node": ">=20"
485
485
  },
486
- "gitHead": "19cc7821541c7e563e4d08fd4b2b09936e80a701"
486
+ "gitHead": "0c67baf7d7c57f4f671c966c8053705af65ba4d0"
487
487
  }
@@ -62,6 +62,10 @@
62
62
  --bb-color-notice-control-border: #ffffff45;
63
63
  --bb-color-inline-error: #9b3d3d;
64
64
  --bb-color-record-missing: #80601c;
65
+ --bb-color-drawer-overlay: rgb(13 25 46 / 6%);
66
+ --bb-color-drawer-overlay-clear: rgb(13 25 46 / 0%);
67
+ --bb-shadow-drawer: -18px 0 48px rgb(13 25 46 / 10%);
68
+ --bb-shadow-popover: 0 8px 28px rgb(13 25 46 / 14%);
65
69
  --bb-color-dialog-overlay: #0d192e66;
66
70
  --bb-color-scrollbar-rgb: 36,50,75;
67
71
  --bb-color-scrollbar-dark-rgb: 203,212,225;