@unson/brainbase-mcp 0.34.0 → 0.36.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.
@@ -66,6 +66,12 @@ export interface MinutesDocumentRecord {
66
66
  readonly created_actor_id: string;
67
67
  readonly updated_at: string;
68
68
  readonly revision: number;
69
+ /** Set when the document was taken back from its readers. Versions and bodies are kept. */
70
+ readonly withdrawal?: MinutesWithdrawalReceipt;
71
+ }
72
+ export interface MinutesWithdrawalReceipt {
73
+ readonly actor_id: string;
74
+ readonly withdrawn_at: string;
69
75
  }
70
76
  export interface MeetingMinutesSnapshot {
71
77
  readonly schema_version: typeof MEETING_MINUTES_SNAPSHOT_VERSION;
@@ -76,7 +82,7 @@ export interface MeetingMinutesSnapshot {
76
82
  readonly versions: readonly MinutesVersionRecord[];
77
83
  readonly idempotency: readonly MeetingMinutesIdempotencyRecord[];
78
84
  }
79
- export type MeetingMinutesIdempotencyOperation = 'create_meeting' | 'create_minutes' | 'save_version' | 'confirm_version';
85
+ export type MeetingMinutesIdempotencyOperation = 'create_meeting' | 'create_minutes' | 'save_version' | 'confirm_version' | 'withdraw_minutes';
80
86
  /** Durable write receipt. It lets a retried mutation find its exact target after restart. */
81
87
  export interface MeetingMinutesIdempotencyRecord {
82
88
  readonly key: string;
@@ -144,6 +150,12 @@ export interface ConfirmMinutesVersionInput {
144
150
  readonly idempotency_key?: string;
145
151
  readonly idempotency_fingerprint?: string;
146
152
  }
153
+ export interface WithdrawMinutesInput {
154
+ readonly minutes_id: MinutesId;
155
+ readonly expected_revision: number;
156
+ readonly idempotency_key?: string;
157
+ readonly idempotency_fingerprint?: string;
158
+ }
147
159
  export interface MeetingMinutesSummary {
148
160
  readonly meeting: MeetingRecord;
149
161
  readonly minutes: readonly MinutesDocumentRecord[];
@@ -186,6 +198,8 @@ export interface MeetingMinutesStore {
186
198
  get_version(meeting_id: MeetingId, minutes_id: MinutesId, version_id: MinutesVersionId, context: MeetingMinutesRequestContext): Promise<MinutesVersionRecord>;
187
199
  save_version(input: SaveMinutesVersionInput, context: MeetingMinutesRequestContext): Promise<MeetingMinutesDetail>;
188
200
  confirm_version(input: ConfirmMinutesVersionInput, context: MeetingMinutesRequestContext): Promise<MeetingMinutesDetail>;
201
+ /** Takes a document back from its readers (story-meeting-minutes-withdraw-v1). Nothing is deleted. */
202
+ withdraw_minutes(input: WithdrawMinutesInput, context: MeetingMinutesRequestContext): Promise<MeetingMinutesDetail>;
189
203
  }
190
204
  /**
191
205
  * Provider-neutral exact-version seam used by lineage and other read-only
@@ -289,6 +289,12 @@ function assertSnapshotShape(value) {
289
289
  checkDate(raw.updated_at, `Minutes document ${minutesId}.updated_at`);
290
290
  if (typeof raw.created_actor_id !== 'string' || !raw.created_actor_id.trim())
291
291
  throw new MeetingMinutesError('corrupt_record', `Minutes document ${minutesId}.created_actor_id is invalid`);
292
+ if (raw.withdrawal !== undefined) {
293
+ if (!isRecord(raw.withdrawal) || typeof raw.withdrawal.actor_id !== 'string' || !raw.withdrawal.actor_id.trim()) {
294
+ throw new MeetingMinutesError('corrupt_record', `Minutes document ${minutesId}.withdrawal is invalid`);
295
+ }
296
+ checkDate(raw.withdrawal.withdrawn_at, `Minutes document ${minutesId}.withdrawal.withdrawn_at`);
297
+ }
292
298
  }
293
299
  const versionIds = new Set();
294
300
  const versionById = new Map();
@@ -401,7 +407,8 @@ function assertSnapshotShape(value) {
401
407
  if (typeof key !== 'string' || key.trim() === '' || key.length > 200
402
408
  || typeof principalId !== 'string' || principalId.trim() === ''
403
409
  || typeof fingerprint !== 'string' || fingerprint.trim() === '' || fingerprint.length > 512
404
- || (operation !== 'create_meeting' && operation !== 'create_minutes' && operation !== 'save_version' && operation !== 'confirm_version')
410
+ || (operation !== 'create_meeting' && operation !== 'create_minutes' && operation !== 'save_version' && operation !== 'confirm_version'
411
+ && operation !== 'withdraw_minutes')
405
412
  || typeof meetingId !== 'string' || !meetingIds.has(meetingId)) {
406
413
  throw new MeetingMinutesError('corrupt_record', 'Idempotency record is invalid');
407
414
  }
@@ -421,9 +428,9 @@ function assertSnapshotShape(value) {
421
428
  || documentById.get(minutesId)?.meeting_id !== meetingId) {
422
429
  throw new MeetingMinutesError('corrupt_record', 'Idempotency record minutes_id is invalid');
423
430
  }
424
- if (operation === 'create_minutes') {
431
+ if (operation === 'create_minutes' || operation === 'withdraw_minutes') {
425
432
  if (versionId !== undefined)
426
- throw new MeetingMinutesError('corrupt_record', 'create_minutes idempotency record has an unexpected version_id');
433
+ throw new MeetingMinutesError('corrupt_record', `${operation} idempotency record has an unexpected version_id`);
427
434
  continue;
428
435
  }
429
436
  if (typeof versionId !== 'string' || !versionIds.has(versionId)
@@ -601,7 +608,15 @@ function detail(snapshot, meeting) {
601
608
  return freezeClone({ meeting, minutes, versions, snapshot_revision: snapshot.revision });
602
609
  }
603
610
  function summary(snapshot, meeting) {
604
- return freezeClone({ meeting, minutes: snapshot.documents.filter((candidate) => candidate.meeting_id === meeting.meeting_id) });
611
+ // A withdrawn document is taken back from listings; its detail stays readable for history.
612
+ return freezeClone({
613
+ meeting,
614
+ minutes: snapshot.documents.filter((candidate) => candidate.meeting_id === meeting.meeting_id && !candidate.withdrawal)
615
+ });
616
+ }
617
+ function assertNotWithdrawn(document) {
618
+ if (document.withdrawal)
619
+ throw invalid(`Minutes document ${document.minutes_id} was withdrawn`);
605
620
  }
606
621
  function ensureRevision(actual, expected) {
607
622
  if (!Number.isInteger(expected) || expected < 1)
@@ -974,6 +989,7 @@ export function createMeetingMinutesStore(options) {
974
989
  const meeting = findMeeting(snapshot, document.meeting_id);
975
990
  await authorized(authorizer, 'write', meeting, context);
976
991
  await authorized(authorizer, 'write', document, context);
992
+ assertNotWithdrawn(document);
977
993
  ensureRevision(document.revision, input.expected_revision);
978
994
  const predecessor = input.predecessor_version_id === undefined ? document.current_version_id : nonEmptyString(input.predecessor_version_id, 'predecessor_version_id');
979
995
  if (predecessor !== document.current_version_id)
@@ -1046,6 +1062,7 @@ export function createMeetingMinutesStore(options) {
1046
1062
  const version = snapshot.versions.find((candidate) => candidate.version_id === versionId && candidate.minutes_id === minutesId);
1047
1063
  if (!version)
1048
1064
  throw new MeetingMinutesError('not_found', `Minutes version ${versionId} was not found`);
1065
+ assertNotWithdrawn(document);
1049
1066
  ensureRevision(document.revision, input.expected_revision);
1050
1067
  if (version.confirmation) {
1051
1068
  if (!idempotency.token)
@@ -1102,7 +1119,52 @@ export function createMeetingMinutesStore(options) {
1102
1119
  throw new MeetingMinutesError('readback_mismatch', 'Confirmation did not read back');
1103
1120
  return detailForCaller(readback, savedMeeting, context);
1104
1121
  }
1105
- return { list_meetings, create_meeting, create_minutes, get_meeting, get_version, save_version, confirm_version };
1122
+ async function withdraw_minutes(input, context) {
1123
+ const principal = assertPrincipal(context);
1124
+ if (!isRecord(input))
1125
+ throw invalid('withdrawal input must be an object');
1126
+ const minutesId = nonEmptyString(input.minutes_id, 'minutes_id');
1127
+ const snapshot = await readSnapshot();
1128
+ const idempotency = await resolveIdempotency(snapshot, principal, input, 'withdraw_minutes', context, { minutes_id: minutesId });
1129
+ if (idempotency.replay)
1130
+ return idempotency.replay;
1131
+ const document = findDocument(snapshot, minutesId);
1132
+ const meeting = findMeeting(snapshot, document.meeting_id);
1133
+ await authorized(authorizer, 'write', meeting, context);
1134
+ await authorized(authorizer, 'write', document, context);
1135
+ // Withdrawing twice keeps the first record; a retry is not a second withdrawal.
1136
+ if (document.withdrawal && !idempotency.token)
1137
+ return detailForCaller(snapshot, meeting, context);
1138
+ if (!document.withdrawal)
1139
+ ensureRevision(document.revision, input.expected_revision);
1140
+ const timestamp = isoNow(now);
1141
+ const next = snapshotMutable(snapshot);
1142
+ next.revision += 1;
1143
+ if (!document.withdrawal) {
1144
+ next.documents = next.documents.map((candidate) => candidate.minutes_id === minutesId
1145
+ ? { ...candidate, withdrawal: { actor_id: principal, withdrawn_at: timestamp }, updated_at: timestamp, revision: candidate.revision + 1 }
1146
+ : candidate);
1147
+ }
1148
+ addIdempotencyRecord(next, idempotency.token, 'withdraw_minutes', {
1149
+ meeting_id: meeting.meeting_id,
1150
+ minutes_id: minutesId
1151
+ }, principal);
1152
+ let readback;
1153
+ try {
1154
+ readback = await writeSnapshot(next, snapshot.revision);
1155
+ }
1156
+ catch (error) {
1157
+ const replay = await replayAfterConflict(error, idempotency.token, principal, 'withdraw_minutes', context, { minutes_id: minutesId });
1158
+ if (replay)
1159
+ return replay;
1160
+ throw error;
1161
+ }
1162
+ const saved = findDocument(readback, minutesId);
1163
+ if (!saved.withdrawal)
1164
+ throw new MeetingMinutesError('readback_mismatch', 'Withdrawal did not read back');
1165
+ return detailForCaller(readback, findMeeting(readback, meeting.meeting_id), context);
1166
+ }
1167
+ return { list_meetings, create_meeting, create_minutes, get_meeting, get_version, save_version, confirm_version, withdraw_minutes };
1106
1168
  }
1107
1169
  function participantIdsInput(value) {
1108
1170
  return participantIds(value);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unson/brainbase-mcp",
3
- "version": "0.34.0",
3
+ "version": "0.36.0",
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",
@@ -479,5 +479,5 @@
479
479
  "engines": {
480
480
  "node": ">=20"
481
481
  },
482
- "gitHead": "9275d1ad54f053c85229a44b1c4cba811969cd29"
482
+ "gitHead": "f7389b6d15e12a3fb7312d305be6f6f9660e8c79"
483
483
  }
@@ -1514,6 +1514,47 @@ function memoryLines(answer) {
1514
1514
  return lines;
1515
1515
  }
1516
1516
 
1517
+ /**
1518
+ * @param {() => (WaitingQueuesAnswer | Promise<WaitingQueuesAnswer>)} [options.waitingQueues] Other queues of things
1519
+ * the owner decides, listed next to the waiting judgments (story-world-waiting-queues-v1, ledger P17 / D9). The
1520
+ * world knows nothing about them beyond `{ status: 'available', queues: [{ id, label, count, href, link_label? }] }`.
1521
+ * A queue with zero is not shown; an unreadable one is never shown as zero. `href` must be `https:` or a link
1522
+ * inside the host (`/…`, `?…`); others get no link. Without it the world is drawn exactly as before.
1523
+ * @typedef {{ status: string, reason?: string, queues?: object[] }} WaitingQueuesAnswer
1524
+ */
1525
+ async function readWaitingQueues(waitingQueues) {
1526
+ if (typeof waitingQueues !== 'function') return null;
1527
+ try {
1528
+ const answer = await waitingQueues();
1529
+ return answer && typeof answer === 'object' ? answer : { status: 'failed', reason: 'invalid_answer' };
1530
+ } catch (error) {
1531
+ return { status: 'failed', reason: error instanceof Error ? error.message : 'request_failed' };
1532
+ }
1533
+ }
1534
+
1535
+ /** The waiting queues as world notes. Zero is quiet; unknown is said out loud and never counted as zero. */
1536
+ function waitingQueueLines(answer) {
1537
+ if (!answer) return [];
1538
+ if (answer.status !== 'available' || !Array.isArray(answer.queues)) {
1539
+ return [{ text: `ほかの待っているもの:読めない(${answer.reason ?? answer.status ?? '理由不明'})。件数は0件とは確認できません。`, tone: 'warning', summary: 'ほかの待っているもの:読めない', link: null }];
1540
+ }
1541
+ const lines = [];
1542
+ for (const queue of answer.queues) {
1543
+ const label = typeof queue?.label === 'string' && queue.label.trim() ? queue.label.trim() : null;
1544
+ if (!label) continue;
1545
+ const count = queue.count;
1546
+ if (!Number.isInteger(count) || count < 0) {
1547
+ lines.push({ text: `${label}:読めない(${queue.reason ?? '理由不明'})。件数は0件とは確認できません。`, tone: 'warning', summary: `${label}:読めない`, link: null });
1548
+ continue;
1549
+ }
1550
+ if (count === 0) continue;
1551
+ const href = exitHrefKind(queue.href) ? queue.href : null;
1552
+ const linkLabel = typeof queue.link_label === 'string' && queue.link_label.trim() ? queue.link_label.trim() : `${label}を開く`;
1553
+ lines.push({ text: `${label} ${count}件。`, tone: 'attention', summary: `${label} ${count}件`, link: href ? { href, label: linkLabel, className: 'bb-world-waiting-queue-link' } : null });
1554
+ }
1555
+ return lines;
1556
+ }
1557
+
1517
1558
  /** At most this many waiting judgments are listed in the world; the rest are counted, never hidden. */
1518
1559
  const WAITING_LIST_LIMIT = 3;
1519
1560
 
@@ -1540,7 +1581,7 @@ function defaultJudgmentHref(decisionAttemptId) {
1540
1581
  return decisionAttemptId ? `#value-proofs?decision=${encodeURIComponent(decisionAttemptId)}` : '#value-proofs';
1541
1582
  }
1542
1583
 
1543
- export function createWorldView({ root, rail, page, presentation = 'standard', document: explicitDocument, fetcher, projectHref = (project) => `#projects?project=${encodeURIComponent(project.id)}`, taskHref = null, judgmentHref = defaultJudgmentHref, selection = addressSelection(), businessExits = null, memoryStatus = null }) {
1584
+ export function createWorldView({ root, rail, page, presentation = 'standard', document: explicitDocument, fetcher, projectHref = (project) => `#projects?project=${encodeURIComponent(project.id)}`, taskHref = null, judgmentHref = defaultJudgmentHref, selection = addressSelection(), businessExits = null, memoryStatus = null, waitingQueues = null }) {
1544
1585
  const exitsEnabled = typeof businessExits === 'function';
1545
1586
  const doc = explicitDocument ?? globalThis.document;
1546
1587
  const reducedMotion = Boolean(globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches);
@@ -2384,6 +2425,7 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
2384
2425
 
2385
2426
  async function load() {
2386
2427
  const memoryRead = readMemoryStatus(memoryStatus);
2428
+ const queuesRead = readWaitingQueues(waitingQueues);
2387
2429
  const businessesResult = await readJson(fetcher, '/api/extensions/world/businesses');
2388
2430
  if (disposed) return;
2389
2431
  // A world drawn from an organization's Graph shows only that organization's judgments: the host keeps
@@ -2463,6 +2505,9 @@ export function createWorldView({ root, rail, page, presentation = 'standard', d
2463
2505
  if (judgmentsConnected) {
2464
2506
  for (const line of waitingLines(proofs, judgmentHref)) note('待っている判断', line.text, 'attention', line.summary, line.link);
2465
2507
  }
2508
+ const queues = await queuesRead;
2509
+ if (disposed) return;
2510
+ for (const line of waitingQueueLines(queues)) note('待っている', line.text, line.tone, line.summary, line.link);
2466
2511
  const rowItems = new Map(rows.map((row) => [row.key, Array.isArray(row.items) ? row.items : []]));
2467
2512
  const memory = await memoryRead;
2468
2513
  if (disposed) return;