@unson/brainbase-mcp 0.45.0 → 0.46.1

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.
package/README.md CHANGED
@@ -247,8 +247,8 @@ Publication is serialized by the GitHub workflow. Direct local `release:publish`
247
247
  Dispatch the reviewed ref once when automatic publication needs recovery:
248
248
 
249
249
  ```bash
250
- RELEASE_REF="<reviewed-develop-commit>"
251
- gh workflow run npm-publish.yml --ref develop -f release_ref="$RELEASE_REF"
250
+ RELEASE_REF="<reviewed-main-commit>"
251
+ gh workflow run npm-publish.yml --ref main -f release_ref="$RELEASE_REF"
252
252
  ```
253
253
 
254
254
  After the workflow succeeds, verify npm `gitHead`, `dist.integrity`, and dist-tag, and verify that the GitHub Release targets the reviewed release commit. If npm already contains the version, treat that version as immutable; fix the source, increment the version, and run the reviewed workflow again.
@@ -25,8 +25,8 @@
25
25
  },
26
26
  {
27
27
  "path": "README.md",
28
- "sha256": "26eacd2a057380b4fd386b2176bd6e790a41c0bb8d684857c53bb5cd947e2d28"
28
+ "sha256": "38f2f0f783bba47f83950cdc286dc03a3e0ee6190b0fbb67d72002e0402c5805"
29
29
  }
30
30
  ],
31
- "digest": "38bf5aff53993b1ee737071ca7952d44e36ac2e5ed3a7adcac65fdd86e8954af"
31
+ "digest": "fa45bdca9bc397be78314d8f24c5a383637e6c016c61d7635f85fc50e3c04800"
32
32
  }
@@ -127,7 +127,13 @@ export declare class CanonicalTaskPostgresRepository {
127
127
  limit?: number;
128
128
  }): Promise<(JsonRecord | null)[]>;
129
129
  findByIdempotencyKey(key: string, storageScope?: string): Promise<JsonRecord | null>;
130
+ private buildInsert;
130
131
  create(input: JsonRecord, storageScope?: string): Promise<JsonRecord>;
132
+ /**
133
+ * Create the Task and run its audit writer in one transaction. A throwing
134
+ * writer rolls back the Task insert; the caller maps the audit error.
135
+ */
136
+ createWithAudit(input: JsonRecord, runAudit: (created: JsonRecord, client: CanonicalTaskPgClient) => Promise<void>, storageScope?: string): Promise<JsonRecord>;
131
137
  update(taskId: string, patch: JsonRecord, storageScope?: string): Promise<JsonRecord>;
132
138
  listPurposeLabels({ projectCode, assigneePersonId, limit }: {
133
139
  projectCode: string;
@@ -418,8 +418,7 @@ export class CanonicalTaskPostgresRepository {
418
418
  const result = await this.query('SELECT * FROM canonical_tasks WHERE idempotency_key = $1 LIMIT 1', [key]);
419
419
  return this.normalize(result.rows[0] || null);
420
420
  }
421
- async create(input, storageScope) {
422
- this.rejectStorageScope(storageScope);
421
+ buildInsert(input) {
423
422
  const columns = [
424
423
  ['id', randomUUID()],
425
424
  ['title', input.title],
@@ -447,14 +446,55 @@ export class CanonicalTaskPostgresRepository {
447
446
  ['last_operation_key', input.last_operation_key ?? null],
448
447
  ['last_operation_fingerprint', input.last_operation_fingerprint ?? null],
449
448
  ];
450
- const result = await this.query(`INSERT INTO canonical_tasks (${columns.map(([column]) => column).join(', ')})
449
+ return {
450
+ sql: `INSERT INTO canonical_tasks (${columns.map(([column]) => column).join(', ')})
451
451
  VALUES (${columns.map(([, , cast = ''], index) => `$${index + 1}${cast}`).join(', ')})
452
452
  ON CONFLICT (idempotency_key) DO NOTHING
453
- RETURNING *`, columns.map(([, value]) => value));
453
+ RETURNING *`,
454
+ values: columns.map(([, value]) => value),
455
+ };
456
+ }
457
+ async create(input, storageScope) {
458
+ this.rejectStorageScope(storageScope);
459
+ const { sql, values } = this.buildInsert(input);
460
+ const result = await this.query(sql, values);
454
461
  if (result.rows[0])
455
462
  return this.normalize(result.rows[0]);
456
463
  return (await this.findByIdempotencyKey(String(input.idempotency_key)));
457
464
  }
465
+ /**
466
+ * Create the Task and run its audit writer in one transaction. A throwing
467
+ * writer rolls back the Task insert; the caller maps the audit error.
468
+ */
469
+ async createWithAudit(input, runAudit, storageScope) {
470
+ this.rejectStorageScope(storageScope);
471
+ if (typeof this.pool.connect !== 'function') {
472
+ throw this.unavailable(new Error('PostgreSQL transaction client is unavailable'));
473
+ }
474
+ const { sql, values } = this.buildInsert(input);
475
+ const client = await this.pool.connect();
476
+ try {
477
+ await client.query('BEGIN');
478
+ const result = await client.query(sql, values);
479
+ const row = result.rows[0] ?? (await client.query('SELECT * FROM canonical_tasks WHERE idempotency_key = $1 LIMIT 1', [input.idempotency_key])).rows[0];
480
+ if (!row)
481
+ throw storeError('task_store_unavailable', 'PostgreSQL Task store is unavailable', 503);
482
+ const created = this.normalize(row);
483
+ await runAudit(created, client);
484
+ await client.query('COMMIT');
485
+ return created;
486
+ }
487
+ catch (error) {
488
+ try {
489
+ await client.query('ROLLBACK');
490
+ }
491
+ catch { /* preserve transaction error */ }
492
+ throw error;
493
+ }
494
+ finally {
495
+ client.release?.();
496
+ }
497
+ }
458
498
  async update(taskId, patch, storageScope) {
459
499
  this.rejectStorageScope(storageScope);
460
500
  // The public kernel marks an applied mutation with `_last_operation_key`
@@ -123,6 +123,11 @@ export interface CanonicalTaskAuditEntry extends JsonRecord {
123
123
  changes: JsonRecord;
124
124
  source_refs: unknown[];
125
125
  }
126
+ export interface CanonicalTaskTransactionClient {
127
+ query(text: string, values?: unknown[]): Promise<{
128
+ rows: Array<Record<string, unknown>>;
129
+ }>;
130
+ }
126
131
  export interface CanonicalTaskRepository {
127
132
  /** Durable PostgreSQL repositories require a release readiness binding. */
128
133
  requiresReadiness?: boolean;
@@ -131,11 +136,20 @@ export interface CanonicalTaskRepository {
131
136
  get(taskId: string, storageScope?: string): Promise<CanonicalTaskRecord | null>;
132
137
  findByIdempotencyKey(key: string, storageScope?: string): Promise<CanonicalTaskRecord | null>;
133
138
  create(input: JsonRecord, storageScope?: string): Promise<CanonicalTaskRecord>;
139
+ /**
140
+ * Create a Task and run the audit writer inside one database transaction.
141
+ * A throwing writer must roll back the Task insert.
142
+ */
143
+ createWithAudit?(input: JsonRecord, runAudit: (created: CanonicalTaskRecord, client: CanonicalTaskTransactionClient) => Promise<void>, storageScope?: string): Promise<CanonicalTaskRecord>;
134
144
  update(taskId: string, patch: JsonRecord, storageScope?: string): Promise<CanonicalTaskRecord>;
135
145
  delete(taskId: string, expectedVersion: number, storageScope?: string): Promise<void>;
136
146
  }
137
147
  export interface CanonicalTaskAuditRepository {
138
- upsertAuditLog(entry: CanonicalTaskAuditEntry): Promise<void>;
148
+ /** Declare support before offering the same-transaction audit contract. */
149
+ supportsTransactionClient?: boolean;
150
+ upsertAuditLog(entry: CanonicalTaskAuditEntry, options?: {
151
+ client?: CanonicalTaskTransactionClient;
152
+ }): Promise<void>;
139
153
  }
140
154
  export interface CanonicalTaskAuthorizationRequest {
141
155
  action: CanonicalTaskAction;
@@ -283,6 +297,7 @@ export declare class CanonicalTaskService {
283
297
  private requireExpectedVersion;
284
298
  private assertVersion;
285
299
  private executeOperation;
300
+ private buildAuditEntry;
286
301
  private audit;
287
302
  private read;
288
303
  private normalizeResponse;
@@ -476,18 +476,29 @@ export class CanonicalTaskService {
476
476
  await this.authorize('read', normalizedContext, existing);
477
477
  return { recovered: true, result: this.normalizeResponse(existing) };
478
478
  };
479
+ const transactionalAudit = typeof this.repository.createWithAudit === 'function'
480
+ && this.auditRepository?.supportsTransactionClient === true;
481
+ let auditedInTransaction = false;
479
482
  const run = async () => {
480
483
  const recovered = await recover();
481
484
  if (recovered.recovered && recovered.result)
482
485
  return recovered.result;
483
486
  await this.assertMutationReady();
484
- const created = await this.read(() => this.repository.create({
487
+ const input = {
485
488
  ...payload,
486
489
  status: 'pending',
487
490
  version: 1,
488
491
  idempotency_key: operation,
489
492
  payload_fingerprint: inputFingerprint,
490
- }, ...this.scopeArgs(normalizedContext)));
493
+ };
494
+ if (transactionalAudit) {
495
+ return this.read(() => this.repository.createWithAudit(input, async (created, client) => {
496
+ const normalizedCreated = this.normalizeResponse(created);
497
+ await this.audit(normalizedContext, 'canonical_task.created', normalizedCreated, { before: null, after: normalizedCreated }, operation, inputFingerprint, client);
498
+ auditedInTransaction = true;
499
+ }, ...this.scopeArgs(normalizedContext)));
500
+ }
501
+ const created = await this.read(() => this.repository.create(input, ...this.scopeArgs(normalizedContext)));
491
502
  return this.normalizeResponse(created);
492
503
  };
493
504
  const result = await this.executeOperation({
@@ -501,10 +512,12 @@ export class CanonicalTaskService {
501
512
  if (normalizedContext.storageScope !== undefined)
502
513
  await this.authorize('read', normalizedContext, result);
503
514
  const normalized = this.normalizeResponse(result);
504
- // Audit each invocation, including a replay/recovery. The deterministic
505
- // entry id lets an upsert sink collapse retries without losing evidence
506
- // that the audit write was attempted again.
507
- await this.audit(normalizedContext, 'canonical_task.created', normalized, { before: null, after: normalized }, operation, inputFingerprint);
515
+ if (!auditedInTransaction) {
516
+ // Audit each invocation, including a replay/recovery. The deterministic
517
+ // entry id lets an upsert sink collapse retries without losing evidence
518
+ // that the audit write was attempted again.
519
+ await this.audit(normalizedContext, 'canonical_task.created', normalized, { before: null, after: normalized }, operation, inputFingerprint);
520
+ }
508
521
  return normalized;
509
522
  }
510
523
  async updateTask(taskId, input, expectedVersionOrContext, maybeContext) {
@@ -1054,9 +1067,9 @@ export class CanonicalTaskService {
1054
1067
  ? this.operationRepository.execute(request)
1055
1068
  : request.run();
1056
1069
  }
1057
- async audit(context, action, task, changes, operationKey, operationFingerprint) {
1070
+ buildAuditEntry(context, action, task, changes, operationKey, operationFingerprint) {
1058
1071
  if (!this.auditRepository)
1059
- return;
1072
+ return null;
1060
1073
  const actor = context.auditPrincipal
1061
1074
  ? normalizeCanonicalTaskPrincipal(context.auditPrincipal)
1062
1075
  : context.principal;
@@ -1078,8 +1091,14 @@ export class CanonicalTaskService {
1078
1091
  changes,
1079
1092
  source_refs: Array.isArray(task.source_refs) ? task.source_refs : [],
1080
1093
  };
1094
+ return entry;
1095
+ }
1096
+ async audit(context, action, task, changes, operationKey, operationFingerprint, client) {
1097
+ const entry = this.buildAuditEntry(context, action, task, changes, operationKey, operationFingerprint);
1098
+ if (!entry)
1099
+ return;
1081
1100
  try {
1082
- await this.auditRepository.upsertAuditLog(entry);
1101
+ await this.auditRepository.upsertAuditLog(entry, client ? { client } : undefined);
1083
1102
  }
1084
1103
  catch {
1085
1104
  throw new CanonicalTaskError('task_audit_unavailable', 'Task audit is unavailable', 503);
@@ -3,6 +3,8 @@ import type { AuthorizationServerMetadata, OAuthClientInformationMixed, OAuthTok
3
3
  import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
4
4
  import { type MeetingSourceAccessDetail, type MeetingSourceCallTool, type MeetingSourceFailure, type MeetingSourceFailureReason, type MeetingSourceMeeting, type MeetingSourceProvider, type MeetingSourceSegment, type TactiqTranscriptBudget, type TactiqTranscriptRead } from './meeting-source-reader.js';
5
5
  import type { OrganizationConnectionBinding, OrganizationConnectionProviderAdapter } from './organization-connection.js';
6
+ export declare const MEETING_SOURCE_LIST_REVISION = 2;
7
+ export declare const MEETING_SOURCE_RELIST_LOOKBACK_MS: number;
6
8
  export type MeetingSourceCapability = 'meetings:list' | 'transcript:read';
7
9
  export type MeetingSourceConnectionStatus = 'pending' | 'connected' | 'reauth_required' | 'disabled' | 'revoked';
8
10
  export interface MeetingSourceConnectionRecord {
@@ -156,6 +158,8 @@ export interface MeetingSourceSyncState {
156
158
  provider: MeetingSourceProvider;
157
159
  syncEnabled: boolean;
158
160
  listedThrough: string | null;
161
+ /** List-reader revision that produced listedThrough; older cursors are re-listed once. */
162
+ listRevision?: number;
159
163
  pending: MeetingSourcePendingMeeting[];
160
164
  unobtainable: Array<{
161
165
  meetingId: string;
@@ -12,6 +12,8 @@ import { createTactiqTranscriptBudget, listMeetings, probeTranscript, readTransc
12
12
  */
13
13
  const HOUR_MS = 60 * 60 * 1000;
14
14
  const DAY_MS = 24 * HOUR_MS;
15
+ export const MEETING_SOURCE_LIST_REVISION = 2;
16
+ export const MEETING_SOURCE_RELIST_LOOKBACK_MS = 30 * DAY_MS;
15
17
  const PROVIDERS = ['plaud', 'tactiq'];
16
18
  const CAPABILITIES = ['meetings:list', 'transcript:read'];
17
19
  const STATUSES = ['pending', 'connected', 'reauth_required', 'disabled', 'revoked'];
@@ -405,11 +407,15 @@ export async function syncMeetingSourceConnection(input) {
405
407
  state.lastAttemptAt = started.toISOString();
406
408
  const callTool = (name, args) => input.runtime.callTool(connection, name, args);
407
409
  const until = input.window ? input.window.until : started.toISOString();
408
- const since = input.window
409
- ? input.window.since
410
- : state.listedThrough
411
- ? new Date(Date.parse(state.listedThrough) - (input.overlapMs ?? DAY_MS)).toISOString()
412
- : new Date(started.getTime() - (input.initialLookbackMs ?? 7 * DAY_MS)).toISOString();
410
+ const normalSince = state.listedThrough
411
+ ? Date.parse(state.listedThrough) - (input.overlapMs ?? DAY_MS)
412
+ : started.getTime() - (input.initialLookbackMs ?? 7 * DAY_MS);
413
+ const sinceMs = !input.window
414
+ && state.listedThrough
415
+ && (state.listRevision ?? 1) < MEETING_SOURCE_LIST_REVISION
416
+ ? Math.min(normalSince, started.getTime() - MEETING_SOURCE_RELIST_LOOKBACK_MS)
417
+ : normalSince;
418
+ const since = input.window ? input.window.since : new Date(sinceMs).toISOString();
413
419
  const finish = async (failure, delivered) => {
414
420
  if (failure) {
415
421
  state.lastError = { ...failure, at: now().toISOString() };
@@ -529,8 +535,11 @@ export async function syncMeetingSourceConnection(input) {
529
535
  if (!listed.complete) {
530
536
  failure ??= { reason: 'incomplete', message: 'The meeting list could not be read completely for the period' };
531
537
  }
532
- else if (!input.window && (!state.listedThrough || Date.parse(until) > Date.parse(state.listedThrough))) {
533
- state.listedThrough = until;
538
+ else if (!input.window) {
539
+ if (!state.listedThrough || Date.parse(until) > Date.parse(state.listedThrough)) {
540
+ state.listedThrough = until;
541
+ }
542
+ state.listRevision = MEETING_SOURCE_LIST_REVISION;
534
543
  }
535
544
  return finish(failure, delivered);
536
545
  }
@@ -153,7 +153,7 @@ async function listPlaud(callTool, since, until, utcOffsetMinutes, pageSize) {
153
153
  const meetings = [];
154
154
  let complete = false;
155
155
  for (let page = 1; page <= MAX_PAGES; page += 1) {
156
- const items = extractList(await invoke(callTool, 'list_files', { date_from: dateFrom, date_to: dateTo, page, page_size: pageSize }));
156
+ const items = plaudListItems(await invoke(callTool, 'list_files', { date_from: dateFrom, date_to: dateTo, page, page_size: pageSize }));
157
157
  for (const item of items) {
158
158
  const meeting = plaudMeeting(item, utcOffsetMinutes);
159
159
  if (!meeting)
@@ -401,10 +401,25 @@ function payloadOf(result) {
401
401
  return record.structuredContent;
402
402
  if (Array.isArray(record.content)) {
403
403
  const text = contentText(record);
404
- return parseJson(text) ?? text;
404
+ const parsed = parseJson(text);
405
+ if (parsed !== null)
406
+ return parsed;
407
+ const wrapped = unwrapUntrustedUserData(text);
408
+ return wrapped.found ? wrapped.value : text;
405
409
  }
406
410
  return result;
407
411
  }
412
+ function unwrapUntrustedUserData(text) {
413
+ const opening = /<(untrusted-user-data-[0-9a-f]+)\s+source="[^"]*">/.exec(text);
414
+ if (!opening)
415
+ return { found: false };
416
+ const start = opening.index + opening[0].length;
417
+ const end = text.lastIndexOf(`</${opening[1]}>`);
418
+ if (end < start)
419
+ return { found: false };
420
+ const inner = text.slice(start, end).trim();
421
+ return { found: true, value: parseJson(inner) ?? inner };
422
+ }
408
423
  function contentText(record) {
409
424
  if (!Array.isArray(record.content))
410
425
  return '';
@@ -414,6 +429,18 @@ function contentText(record) {
414
429
  .map((item) => item.text)
415
430
  .join('\n');
416
431
  }
432
+ function plaudListItems(payload) {
433
+ if (Array.isArray(payload))
434
+ return payload;
435
+ const record = asRecord(payload);
436
+ if (record) {
437
+ for (const key of ['data', 'files', 'items', 'results', 'records', 'recordings']) {
438
+ if (Array.isArray(record[key]))
439
+ return record[key];
440
+ }
441
+ }
442
+ throw new UnexpectedShapeError('Plaud list_files returned a response in an unknown shape');
443
+ }
417
444
  function extractList(payload) {
418
445
  if (Array.isArray(payload))
419
446
  return payload;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.45.0",
3
+ "version": "0.46.1",
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",
@@ -586,7 +586,7 @@
586
586
  "build:world": "node scripts/build-world-vendor.mjs"
587
587
  },
588
588
  "dependencies": {
589
- "@modelcontextprotocol/sdk": "^1.31.0",
589
+ "@modelcontextprotocol/sdk": "^1.32.0",
590
590
  "graphology": "0.26.0",
591
591
  "js-yaml": "^4.1.1",
592
592
  "sigma": "3.0.3",
@@ -620,5 +620,5 @@
620
620
  "url": "https://github.com/Unson-LLC/brainbase/issues"
621
621
  },
622
622
  "homepage": "https://github.com/Unson-LLC/brainbase#readme",
623
- "gitHead": "10ff1144069d177b6a1c3857a69182513ff55372"
623
+ "gitHead": "514e31c00aeb6906c01cb5df51cf9bea0f62eee2"
624
624
  }