@unson/brainbase-mcp 0.35.0 → 0.37.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.
|
@@ -24,6 +24,7 @@ const MAX_TITLE_LENGTH = 200;
|
|
|
24
24
|
const MAX_SEARCH_LENGTH = 200;
|
|
25
25
|
const DEFAULT_LIMIT = 20;
|
|
26
26
|
const MAX_LIMIT = 100;
|
|
27
|
+
const MAX_SOURCE_REFS_APPEND_JSON_DEPTH = 128;
|
|
27
28
|
const RESERVED_IDEMPOTENCY_PREFIXES = ['api:', 'workflow:'];
|
|
28
29
|
const CREATE_FIELDS = new Set([
|
|
29
30
|
'completion_contract',
|
|
@@ -49,6 +50,7 @@ const MUTABLE_FIELDS = new Set([
|
|
|
49
50
|
'waiting_on',
|
|
50
51
|
'review_at',
|
|
51
52
|
'source_refs',
|
|
53
|
+
'source_refs_append',
|
|
52
54
|
'project_codes',
|
|
53
55
|
'purpose_label',
|
|
54
56
|
]);
|
|
@@ -164,6 +166,90 @@ function normalizeSourceRefs(value) {
|
|
|
164
166
|
fail('validation_error', 'source_refs must be an array', 400, { field: 'source_refs' });
|
|
165
167
|
return value;
|
|
166
168
|
}
|
|
169
|
+
function isPlainJsonObject(value) {
|
|
170
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
171
|
+
return false;
|
|
172
|
+
const prototype = Object.getPrototypeOf(value);
|
|
173
|
+
return prototype === Object.prototype || prototype === null;
|
|
174
|
+
}
|
|
175
|
+
function assertJsonValue(value, field, ancestors = new Set(), depth = 0) {
|
|
176
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
177
|
+
return;
|
|
178
|
+
if (typeof value === 'number') {
|
|
179
|
+
if (Number.isFinite(value))
|
|
180
|
+
return;
|
|
181
|
+
fail('validation_error', `${field} must contain JSON-serializable values`, 400, { field });
|
|
182
|
+
}
|
|
183
|
+
if (typeof value !== 'object') {
|
|
184
|
+
fail('validation_error', `${field} must contain JSON-serializable values`, 400, { field });
|
|
185
|
+
}
|
|
186
|
+
if (depth >= MAX_SOURCE_REFS_APPEND_JSON_DEPTH) {
|
|
187
|
+
fail('validation_error', `${field} exceeds the maximum JSON nesting depth`, 400, { field });
|
|
188
|
+
}
|
|
189
|
+
if (ancestors.has(value)) {
|
|
190
|
+
fail('validation_error', `${field} must contain JSON-serializable values`, 400, { field });
|
|
191
|
+
}
|
|
192
|
+
ancestors.add(value);
|
|
193
|
+
if (Array.isArray(value)) {
|
|
194
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
195
|
+
if (key === 'length')
|
|
196
|
+
continue;
|
|
197
|
+
if (typeof key !== 'string' || !/^(?:0|[1-9]\d*)$/u.test(key)
|
|
198
|
+
|| Number(key) >= value.length
|
|
199
|
+
|| !Object.prototype.propertyIsEnumerable.call(value, key)) {
|
|
200
|
+
fail('validation_error', `${field} must contain JSON-serializable values`, 400, { field });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
204
|
+
if (!Object.prototype.hasOwnProperty.call(value, index)) {
|
|
205
|
+
fail('validation_error', `${field} must contain JSON-serializable values`, 400, { field });
|
|
206
|
+
}
|
|
207
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
208
|
+
if (!descriptor || !('value' in descriptor)) {
|
|
209
|
+
fail('validation_error', `${field} must contain JSON-serializable values`, 400, { field });
|
|
210
|
+
}
|
|
211
|
+
assertJsonValue(descriptor.value, `${field}[${index}]`, ancestors, depth + 1);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
else if (isPlainJsonObject(value)) {
|
|
215
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
216
|
+
if (typeof key !== 'string' || !Object.prototype.propertyIsEnumerable.call(value, key)) {
|
|
217
|
+
fail('validation_error', `${field} must contain JSON-serializable values`, 400, { field });
|
|
218
|
+
}
|
|
219
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
220
|
+
if (!descriptor || !('value' in descriptor)) {
|
|
221
|
+
fail('validation_error', `${field} must contain JSON-serializable values`, 400, { field });
|
|
222
|
+
}
|
|
223
|
+
assertJsonValue(descriptor.value, `${field}.${key}`, ancestors, depth + 1);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
fail('validation_error', `${field} must contain plain JSON objects`, 400, { field });
|
|
228
|
+
}
|
|
229
|
+
ancestors.delete(value);
|
|
230
|
+
}
|
|
231
|
+
function normalizeSourceRefsAppend(value) {
|
|
232
|
+
if (value === undefined)
|
|
233
|
+
return undefined;
|
|
234
|
+
if (!Array.isArray(value)) {
|
|
235
|
+
fail('validation_error', 'source_refs_append must be an array of plain JSON objects', 400, { field: 'source_refs_append' });
|
|
236
|
+
}
|
|
237
|
+
// Validate the array container itself so sparse, symbol-keyed, accessor, or
|
|
238
|
+
// otherwise non-JSON properties cannot bypass the per-reference checks.
|
|
239
|
+
assertJsonValue(value, 'source_refs_append');
|
|
240
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
241
|
+
if (!Object.prototype.hasOwnProperty.call(value, index)) {
|
|
242
|
+
fail('validation_error', 'source_refs_append must be an array of plain JSON objects', 400, { field: 'source_refs_append' });
|
|
243
|
+
}
|
|
244
|
+
const ref = value[index];
|
|
245
|
+
const field = `source_refs_append[${index}]`;
|
|
246
|
+
if (!isPlainJsonObject(ref)) {
|
|
247
|
+
fail('validation_error', 'source_refs_append must be an array of plain JSON objects', 400, { field: 'source_refs_append' });
|
|
248
|
+
}
|
|
249
|
+
assertJsonValue(ref, field);
|
|
250
|
+
}
|
|
251
|
+
return value;
|
|
252
|
+
}
|
|
167
253
|
function canonicalize(value) {
|
|
168
254
|
if (Array.isArray(value))
|
|
169
255
|
return value.map(canonicalize);
|
|
@@ -178,6 +264,50 @@ function canonicalize(value) {
|
|
|
178
264
|
function fingerprint(value) {
|
|
179
265
|
return createHash('sha256').update(JSON.stringify(canonicalize(value))).digest('hex');
|
|
180
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Appended source references use code-unit key ordering so the same JSON
|
|
269
|
+
* object has one fingerprint across runtimes and locale settings. Keep the
|
|
270
|
+
* existing canonicalizer above unchanged because its fingerprints are already
|
|
271
|
+
* persisted for older create/update/transition operations.
|
|
272
|
+
*/
|
|
273
|
+
function canonicalizeSourceRefsAppend(value) {
|
|
274
|
+
if (Array.isArray(value))
|
|
275
|
+
return value.map(canonicalizeSourceRefsAppend);
|
|
276
|
+
if (value && typeof value === 'object') {
|
|
277
|
+
return Object.fromEntries(Object.entries(value)
|
|
278
|
+
.filter(([, entry]) => entry !== undefined)
|
|
279
|
+
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
|
280
|
+
.map(([key, entry]) => [key, canonicalizeSourceRefsAppend(entry)]));
|
|
281
|
+
}
|
|
282
|
+
return value;
|
|
283
|
+
}
|
|
284
|
+
function sourceRefsAppendFingerprint(value) {
|
|
285
|
+
return createHash('sha256').update(JSON.stringify(canonicalizeSourceRefsAppend(value))).digest('hex');
|
|
286
|
+
}
|
|
287
|
+
function mergeSourceRefs(existing, additions) {
|
|
288
|
+
if (existing != null && !Array.isArray(existing)) {
|
|
289
|
+
fail('validation_error', 'source_refs must be an array', 400, { field: 'source_refs' });
|
|
290
|
+
}
|
|
291
|
+
const existingRefs = Array.isArray(existing) ? existing : [];
|
|
292
|
+
const merged = [...existingRefs];
|
|
293
|
+
const known = new Set();
|
|
294
|
+
for (const ref of existingRefs) {
|
|
295
|
+
try {
|
|
296
|
+
known.add(sourceRefsAppendFingerprint(ref));
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
// Preserve legacy references exactly, but do not block valid additions.
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
for (const addition of additions) {
|
|
303
|
+
const key = sourceRefsAppendFingerprint(addition);
|
|
304
|
+
if (known.has(key))
|
|
305
|
+
continue;
|
|
306
|
+
known.add(key);
|
|
307
|
+
merged.push(addition);
|
|
308
|
+
}
|
|
309
|
+
return merged;
|
|
310
|
+
}
|
|
181
311
|
function validHttpUrl(value) {
|
|
182
312
|
if (typeof value !== 'string' || !value.trim())
|
|
183
313
|
return null;
|
|
@@ -570,7 +700,11 @@ export class CanonicalTaskService {
|
|
|
570
700
|
// Keep this fingerprint independent of the wall clock. A restart may
|
|
571
701
|
// reconstruct the same command at a different instant, while the marker
|
|
572
702
|
// must still identify the already-applied mutation.
|
|
573
|
-
const
|
|
703
|
+
const fingerprintOperation = input.action === 'update'
|
|
704
|
+
&& Object.prototype.hasOwnProperty.call(input.patch, 'source_refs_append')
|
|
705
|
+
? sourceRefsAppendFingerprint
|
|
706
|
+
: fingerprint;
|
|
707
|
+
const operationFingerprint = fingerprintOperation({
|
|
574
708
|
action: input.action,
|
|
575
709
|
taskId,
|
|
576
710
|
expectedVersion,
|
|
@@ -620,8 +754,17 @@ export class CanonicalTaskService {
|
|
|
620
754
|
to: input.transitionStatus,
|
|
621
755
|
}, { currentTask: this.normalizeResponse(latest) });
|
|
622
756
|
}
|
|
757
|
+
const persistedPatch = { ...patch };
|
|
758
|
+
if (input.action === 'update' && 'source_refs_append' in input.patch) {
|
|
759
|
+
const additions = input.patch.source_refs_append;
|
|
760
|
+
if (!Array.isArray(additions)) {
|
|
761
|
+
fail('validation_error', 'source_refs_append must be an array of plain JSON objects', 400, { field: 'source_refs_append' });
|
|
762
|
+
}
|
|
763
|
+
delete persistedPatch.source_refs_append;
|
|
764
|
+
persistedPatch.source_refs = mergeSourceRefs(latest.source_refs, additions);
|
|
765
|
+
}
|
|
623
766
|
const updated = await this.read(() => this.repository.update(taskId, {
|
|
624
|
-
...
|
|
767
|
+
...persistedPatch,
|
|
625
768
|
_last_operation_key: operation,
|
|
626
769
|
_last_operation_fingerprint: operationFingerprint,
|
|
627
770
|
}, ...this.scopeArgs(input.context)));
|
|
@@ -689,6 +832,13 @@ export class CanonicalTaskService {
|
|
|
689
832
|
normalizeUpdateInput(input) {
|
|
690
833
|
if (!input || typeof input !== 'object')
|
|
691
834
|
fail('validation_error', 'Task update is required');
|
|
835
|
+
const hasSourceRefsAppend = Object.prototype.hasOwnProperty.call(input, 'source_refs_append');
|
|
836
|
+
if ('source_refs_append' in input) {
|
|
837
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, 'source_refs_append');
|
|
838
|
+
if (!hasSourceRefsAppend || !descriptor || !('value' in descriptor)) {
|
|
839
|
+
fail('validation_error', 'source_refs_append must be an own data property', 400, { field: 'source_refs_append' });
|
|
840
|
+
}
|
|
841
|
+
}
|
|
692
842
|
const unknownFields = Object.keys(input).filter((field) => !MUTABLE_FIELDS.has(field));
|
|
693
843
|
if (unknownFields.length > 0)
|
|
694
844
|
fail('validation_error', 'Task update contains unsupported fields', 400, { fields: unknownFields });
|
|
@@ -713,8 +863,19 @@ export class CanonicalTaskService {
|
|
|
713
863
|
patch.waiting_on = normalizeString(input.waiting_on, 'waiting_on', { max: 500 });
|
|
714
864
|
if ('review_at' in input)
|
|
715
865
|
patch.review_at = normalizeIsoDate(input.review_at, 'review_at');
|
|
866
|
+
if ('source_refs' in input && hasSourceRefsAppend) {
|
|
867
|
+
fail('validation_error', 'source_refs and source_refs_append cannot be combined', 400, {
|
|
868
|
+
fields: ['source_refs', 'source_refs_append'],
|
|
869
|
+
});
|
|
870
|
+
}
|
|
716
871
|
if ('source_refs' in input)
|
|
717
872
|
patch.source_refs = normalizeSourceRefs(input.source_refs) ?? [];
|
|
873
|
+
if (hasSourceRefsAppend) {
|
|
874
|
+
const descriptor = Object.getOwnPropertyDescriptor(input, 'source_refs_append');
|
|
875
|
+
// The descriptor was checked above before any value access. Reuse its
|
|
876
|
+
// data value so a getter cannot run between validation and normalization.
|
|
877
|
+
patch.source_refs_append = normalizeSourceRefsAppend(descriptor?.value) ?? [];
|
|
878
|
+
}
|
|
718
879
|
if ('project_codes' in input)
|
|
719
880
|
patch.project_codes = normalizeProjectCodes(input.project_codes) ?? [];
|
|
720
881
|
if ('purpose_label' in input)
|
|
@@ -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
|
package/dist/meeting-minutes.js
CHANGED
|
@@ -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',
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.37.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": "
|
|
482
|
+
"gitHead": "55c2d213cafd20800d51c9669f4d7c8d727f7968"
|
|
483
483
|
}
|