borgmcp-shared 0.11.0 → 0.12.2

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 (38) hide show
  1. package/README.md +7 -1
  2. package/RELEASES.md +14 -0
  3. package/dist/conformance/adapter.d.ts +4 -0
  4. package/dist/conformance/adapter.d.ts.map +1 -1
  5. package/dist/conformance/adapter.js +109 -10
  6. package/dist/conformance/adapter.js.map +1 -1
  7. package/dist/conformance/index.d.ts +13 -0
  8. package/dist/conformance/index.d.ts.map +1 -1
  9. package/dist/conformance/index.js +84 -14
  10. package/dist/conformance/index.js.map +1 -1
  11. package/dist/protocol/contract.d.ts +2 -1
  12. package/dist/protocol/contract.d.ts.map +1 -1
  13. package/dist/protocol/contract.js +9 -4
  14. package/dist/protocol/contract.js.map +1 -1
  15. package/dist/protocol/coordination.d.ts.map +1 -1
  16. package/dist/protocol/coordination.js +8 -2
  17. package/dist/protocol/coordination.js.map +1 -1
  18. package/dist/protocol/errors.d.ts +1 -0
  19. package/dist/protocol/errors.d.ts.map +1 -1
  20. package/dist/protocol/errors.js +1 -0
  21. package/dist/protocol/errors.js.map +1 -1
  22. package/dist/protocol/types.d.ts +2 -0
  23. package/dist/protocol/types.d.ts.map +1 -1
  24. package/dist/protocol/version.d.ts +1 -1
  25. package/dist/protocol/version.js +1 -1
  26. package/docs/release-records.json +117 -0
  27. package/docs/releases/0.12.0.md +7 -0
  28. package/docs/releases/0.12.1.md +7 -0
  29. package/docs/releases/0.12.2.md +7 -0
  30. package/docs/releasing.md +62 -75
  31. package/package.json +2 -1
  32. package/src/conformance/adapter.ts +137 -9
  33. package/src/conformance/index.ts +102 -15
  34. package/src/protocol/contract.ts +11 -5
  35. package/src/protocol/coordination.ts +8 -2
  36. package/src/protocol/errors.ts +1 -0
  37. package/src/protocol/types.ts +2 -0
  38. package/src/protocol/version.ts +2 -2
@@ -37,6 +37,7 @@ import {
37
37
  type DroneRuntimeMetadata,
38
38
  } from '../protocol/index.js';
39
39
  import {
40
+ APPEND_LOG_IDEMPOTENCY_CONFORMANCE,
40
41
  CREATE_CUBE_ASSOCIATION_CONFORMANCE,
41
42
  CREATE_CUBE_RETRY_CONFORMANCE,
42
43
  INVITATION_ARTIFACT_CONFORMANCE,
@@ -208,6 +209,11 @@ export interface ConformanceAdmin {
208
209
  readonly isMandatory?: boolean;
209
210
  }): Promise<ConformanceRole>;
210
211
  referenceRoleFromTaxonomy(cube: ConformanceCube, role: ConformanceRole): Promise<void>;
212
+ configureMessageClassRouting(
213
+ cube: ConformanceCube,
214
+ className: string,
215
+ recipientDroneIds: readonly string[],
216
+ ): Promise<void>;
211
217
  createDrone(
212
218
  principal: ConformancePrincipal,
213
219
  cube: ConformanceCube,
@@ -361,6 +367,7 @@ export const ADAPTER_CONFORMANCE_FIXTURES = [
361
367
  { id: 'security.adapter-boundary-injection', area: 'security' },
362
368
  { id: 'security.oversize-request', area: 'security' },
363
369
  { id: 'security.cross-cube-isolation', area: 'security' },
370
+ { id: 'log.append-idempotency', area: 'log' },
364
371
  { id: 'log.read-cursor-tuple', area: 'cursor' },
365
372
  { id: 'sse.replay-live-transition', area: 'sse' },
366
373
  { id: 'cursor.explicit-expiry', area: 'cursor' },
@@ -1323,7 +1330,10 @@ export async function runAdapterConformance(
1323
1330
  await record('security.adapter-boundary-injection', async () => {
1324
1331
  const injectedMessage = "'); DROP TABLE log_entries; --\r\ndata: forged-sse-frame";
1325
1332
  const injectedBody = JSON.stringify(
1326
- createProtocolEnvelope('inject-b1', { message: injectedMessage }),
1333
+ createProtocolEnvelope('inject-b1', {
1334
+ post_id: '00000000-0000-4000-8000-000000000301',
1335
+ message: injectedMessage,
1336
+ }),
1327
1337
  );
1328
1338
  invariant(
1329
1339
  utf8ByteLength(injectedBody) <= PROTOCOL_LIMIT_CEILINGS.max_request_bytes &&
@@ -1342,7 +1352,10 @@ export async function runAdapterConformance(
1342
1352
  const sentinel = await environment.operations.append(
1343
1353
  credentialB,
1344
1354
  cubeB,
1345
- createProtocolEnvelope('inject-b2', { message: 'post-injection-sentinel' }),
1355
+ createProtocolEnvelope('inject-b2', {
1356
+ post_id: '00000000-0000-4000-8000-000000000302',
1357
+ message: 'post-injection-sentinel',
1358
+ }),
1346
1359
  );
1347
1360
  expectStatus(sentinel, 201, 'Post-injection sentinel append');
1348
1361
  const read = await environment.operations.read(
@@ -1389,7 +1402,10 @@ export async function runAdapterConformance(
1389
1402
 
1390
1403
  await record('security.oversize-request', async () => {
1391
1404
  const baseBody = JSON.stringify(
1392
- createProtocolEnvelope('oversize-a1', { message: 'must-not-persist' }),
1405
+ createProtocolEnvelope('oversize-a1', {
1406
+ post_id: '00000000-0000-4000-8000-000000000303',
1407
+ message: 'must-not-persist',
1408
+ }),
1393
1409
  );
1394
1410
  const oversizedBody = baseBody + ' '.repeat(
1395
1411
  Math.max(0, PROTOCOL_LIMIT_CEILINGS.max_request_bytes - utf8ByteLength(baseBody) + 1),
@@ -1417,7 +1433,10 @@ export async function runAdapterConformance(
1417
1433
  const secretAppend = await environment.operations.append(
1418
1434
  credentialB,
1419
1435
  cubeB,
1420
- createProtocolEnvelope('append-b1', { message: 'principal-b-secret' }),
1436
+ createProtocolEnvelope('append-b1', {
1437
+ post_id: '00000000-0000-4000-8000-000000000304',
1438
+ message: 'principal-b-secret',
1439
+ }),
1421
1440
  );
1422
1441
  expectStatus(secretAppend, 201, 'Principal B append');
1423
1442
  const denied = await environment.operations.read(
@@ -1429,6 +1448,99 @@ export async function runAdapterConformance(
1429
1448
  return { status: 404, code: ErrorCode.NOT_FOUND };
1430
1449
  });
1431
1450
 
1451
+ await record('log.append-idempotency', async () => {
1452
+ const principal = await environment.admin.createPrincipal('append-idempotency');
1453
+ const otherPrincipal = await environment.admin.createPrincipal('append-idempotency-other');
1454
+ const cube = await environment.admin.createCube('append-idempotency');
1455
+ await environment.admin.grantCube(principal, cube);
1456
+ await environment.admin.grantCube(otherPrincipal, cube);
1457
+ const credential = await environment.admin.issueDroneSession(principal);
1458
+ const otherCredential = await environment.admin.issueDroneSession(otherPrincipal);
1459
+ const role = await environment.admin.createRole(cube, {
1460
+ roleClass: 'worker',
1461
+ isHumanSeat: false,
1462
+ });
1463
+ const firstRecipient = await environment.admin.createDrone(principal, cube, role);
1464
+ const secondRecipient = await environment.admin.createDrone(principal, cube, role);
1465
+ await environment.admin.configureMessageClassRouting(cube, 'conformance-review', [firstRecipient.id]);
1466
+ await environment.admin.configureMessageClassRouting(cube, 'conformance-security', [firstRecipient.id]);
1467
+ const postId = '00000000-0000-4000-8000-000000000313';
1468
+ const initialPayload = {
1469
+ post_id: postId,
1470
+ message: 'once',
1471
+ visibility: 'direct' as const,
1472
+ recipientDroneIds: [firstRecipient.id],
1473
+ class: 'review',
1474
+ to: ['Coordinator'],
1475
+ };
1476
+ const first = await environment.operations.append(
1477
+ credential,
1478
+ cube,
1479
+ createProtocolEnvelope('append-idempotency-first', initialPayload),
1480
+ );
1481
+ expectStatus(first, 201, 'Initial idempotent append');
1482
+ const firstResult = decodeAppendLogResultEnvelope(first.body).payload;
1483
+ invariant(!firstResult.deduplicated, 'Initial append was reported as deduplicated.');
1484
+ for (const [index, vector] of APPEND_LOG_IDEMPOTENCY_CONFORMANCE.entries()) {
1485
+ const classRouting = vector.mutation === 'resolved_class_routing';
1486
+ const vectorPostId = classRouting
1487
+ ? '00000000-0000-4000-8000-000000000314'
1488
+ : postId;
1489
+ if (classRouting) {
1490
+ const baseline = await environment.operations.append(
1491
+ credential,
1492
+ cube,
1493
+ createProtocolEnvelope('append-idempotency-class-baseline', {
1494
+ post_id: vectorPostId,
1495
+ message: 'once',
1496
+ class: 'conformance-review',
1497
+ }),
1498
+ );
1499
+ expectStatus(baseline, 201, 'Resolved class-routing baseline');
1500
+ }
1501
+ const payload = classRouting ? {
1502
+ post_id: vectorPostId,
1503
+ message: 'once',
1504
+ class: 'conformance-security',
1505
+ } : {
1506
+ ...initialPayload,
1507
+ ...(vector.mutation === 'message' ? { message: 'changed' } : {}),
1508
+ ...(vector.mutation === 'visibility' ? { visibility: 'broadcast' as const } : {}),
1509
+ ...(vector.mutation === 'recipient_set' ? { recipientDroneIds: [secondRecipient.id] } : {}),
1510
+ ...(vector.mutation === 'ignored_request_shape' ? { class: 'security', to: ['Security Auditor'] } : {}),
1511
+ };
1512
+ const response = await environment.operations.append(
1513
+ vector.actor === 'same' ? credential : otherCredential,
1514
+ cube,
1515
+ createProtocolEnvelope(`append-idempotency-${index}`, payload),
1516
+ );
1517
+ if (vector.expected === 'POST_ID_CONFLICT') {
1518
+ expectError(response, 409, ErrorCode.POST_ID_CONFLICT, vector.name);
1519
+ continue;
1520
+ }
1521
+ expectStatus(response, 201, vector.name);
1522
+ const result = decodeAppendLogResultEnvelope(response.body).payload;
1523
+ invariant(result.deduplicated === (vector.expected === 'deduplicated'), `${vector.name} returned the wrong deduplicated flag.`);
1524
+ invariant(
1525
+ vector.expected === 'deduplicated'
1526
+ ? result.entry.id === firstResult.entry.id
1527
+ : result.entry.id !== firstResult.entry.id,
1528
+ `${vector.name} returned the wrong entry identity.`,
1529
+ );
1530
+ }
1531
+ const read = await environment.operations.read(
1532
+ credential,
1533
+ cube,
1534
+ createProtocolEnvelope('append-idempotency-read', { cursor: null, limit: 10 }),
1535
+ );
1536
+ expectStatus(read, 200, 'Idempotent append read');
1537
+ invariant(
1538
+ decodeReadLogResultEnvelope(read.body).payload.entries.length === 3,
1539
+ 'Append collision controls persisted the wrong number of entries.',
1540
+ );
1541
+ return { persisted_entries: 3, stable_entry: true, deduplicated: true, conflicts: 4, cross_author_created: true };
1542
+ });
1543
+
1432
1544
  const entries: Array<{ id: string; created_at: string; message: string }> = [];
1433
1545
  let readCursor: LogCursor | null = null;
1434
1546
  await record('log.read-cursor-tuple', async () => {
@@ -1436,7 +1548,10 @@ export async function runAdapterConformance(
1436
1548
  const response = await environment.operations.append(
1437
1549
  credentialA,
1438
1550
  cubeA,
1439
- createProtocolEnvelope(`append-a${index + 1}`, { message }),
1551
+ createProtocolEnvelope(`append-a${index + 1}`, {
1552
+ post_id: `00000000-0000-4000-8000-${String(305 + index).padStart(12, '0')}`,
1553
+ message,
1554
+ }),
1440
1555
  );
1441
1556
  expectStatus(response, 201, `Append ${message}`);
1442
1557
  const entry = decodeAppendLogResultEnvelope(response.body).payload.entry;
@@ -1470,7 +1585,10 @@ export async function runAdapterConformance(
1470
1585
  const appendDelta = environment.operations.append(
1471
1586
  credentialA,
1472
1587
  cubeA,
1473
- createProtocolEnvelope('append-a4', { message: 'delta' }),
1588
+ createProtocolEnvelope('append-a4', {
1589
+ post_id: '00000000-0000-4000-8000-000000000308',
1590
+ message: 'delta',
1591
+ }),
1474
1592
  );
1475
1593
  expectStatus(await appendDelta, 201, 'Transition append');
1476
1594
  } finally {
@@ -1494,7 +1612,10 @@ export async function runAdapterConformance(
1494
1612
  const epsilonResponse = await environment.operations.append(
1495
1613
  credentialA,
1496
1614
  cubeA,
1497
- createProtocolEnvelope('append-a5', { message: 'epsilon' }),
1615
+ createProtocolEnvelope('append-a5', {
1616
+ post_id: '00000000-0000-4000-8000-000000000309',
1617
+ message: 'epsilon',
1618
+ }),
1498
1619
  );
1499
1620
  expectStatus(epsilonResponse, 201, 'Live append');
1500
1621
  const epsilon = logEvent(await within(noDuplicate, 'Live epsilon event', streamDeadlineMs), 'Live append');
@@ -2014,6 +2135,7 @@ export async function runAdapterConformance(
2014
2135
  cubeA,
2015
2136
  createProtocolEnvelope('evict-direct-target', {
2016
2137
  message: 'must-not-fan-out',
2138
+ post_id: '00000000-0000-4000-8000-000000000310',
2017
2139
  visibility: 'direct',
2018
2140
  recipientDroneIds: [evictedDrone.id],
2019
2141
  }),
@@ -2203,7 +2325,10 @@ export async function runAdapterConformance(
2203
2325
  const attributed = await environment.operations.append(
2204
2326
  evictedSession,
2205
2327
  roleContractCube,
2206
- createProtocolEnvelope('role-delete-attribution', { message: 'attribution survives' }),
2328
+ createProtocolEnvelope('role-delete-attribution', {
2329
+ post_id: '00000000-0000-4000-8000-000000000311',
2330
+ message: 'attribution survives',
2331
+ }),
2207
2332
  );
2208
2333
  expectStatus(attributed, 201, 'Pre-delete attributed log');
2209
2334
  const attributedEntry = decodeAppendLogResultEnvelope(attributed.body).payload.entry;
@@ -2867,7 +2992,10 @@ export async function runAdapterConformance(
2867
2992
  const append = await environment.operations.append(
2868
2993
  creator.credential,
2869
2994
  cube,
2870
- createProtocolEnvelope('delete-log', { message: 'deleted with cube' }),
2995
+ createProtocolEnvelope('delete-log', {
2996
+ post_id: '00000000-0000-4000-8000-000000000312',
2997
+ message: 'deleted with cube',
2998
+ }),
2871
2999
  );
2872
3000
  expectStatus(append, 201, 'Deletion fixture log');
2873
3001
  const entry = decodeAppendLogResultEnvelope(append.body).payload.entry;
@@ -11,7 +11,7 @@ import {
11
11
  encodeInvitationArtifact,
12
12
  type InvitationArtifact,
13
13
  } from '../protocol/contract.js';
14
- import type { EnrichedStreamEntry } from '../protocol/types.js';
14
+ import type { AppendLogRequest, EnrichedStreamEntry } from '../protocol/types.js';
15
15
  import { ROLE_IN_USE_DELETE_MESSAGE } from '../protocol/coordination.js';
16
16
 
17
17
  export * from './adapter.js';
@@ -99,17 +99,54 @@ export interface AppendLogResultConformanceVector {
99
99
  accepts: boolean;
100
100
  }
101
101
 
102
+ export interface AppendLogRequestConformanceVector {
103
+ name: string;
104
+ request: unknown;
105
+ accepts: boolean;
106
+ }
107
+
108
+ export interface AppendLogIdempotencyConformanceVector {
109
+ name: string;
110
+ actor: 'same' | 'different';
111
+ mutation: 'none' | 'message' | 'visibility' | 'recipient_set' | 'ignored_request_shape' | 'resolved_class_routing';
112
+ expected: 'deduplicated' | 'POST_ID_CONFLICT' | 'created';
113
+ }
114
+
115
+ /** Author-scoped post identity and exact resolved-routing retry outcomes. */
116
+ export const APPEND_LOG_IDEMPOTENCY_CONFORMANCE: readonly AppendLogIdempotencyConformanceVector[] = [
117
+ { name: 'deduplicates an exact same-author retry', actor: 'same', mutation: 'none', expected: 'deduplicated' },
118
+ { name: 'rejects a same-author message collision', actor: 'same', mutation: 'message', expected: 'POST_ID_CONFLICT' },
119
+ { name: 'rejects a same-author visibility collision', actor: 'same', mutation: 'visibility', expected: 'POST_ID_CONFLICT' },
120
+ { name: 'rejects a same-author recipient-set collision', actor: 'same', mutation: 'recipient_set', expected: 'POST_ID_CONFLICT' },
121
+ { name: 'deduplicates ignored class selectors with identical resolved delivery', actor: 'same', mutation: 'ignored_request_shape', expected: 'deduplicated' },
122
+ { name: 'rejects a same-author resolved class-routing collision', actor: 'same', mutation: 'resolved_class_routing', expected: 'POST_ID_CONFLICT' },
123
+ { name: 'creates an independent cross-author post', actor: 'different', mutation: 'none', expected: 'created' },
124
+ ];
125
+
126
+ const APPEND_LOG_REQUEST = {
127
+ post_id: '00000000-0000-4000-8000-000000000205',
128
+ message: 'REVIEW-READY: example',
129
+ } satisfies AppendLogRequest;
130
+
131
+ export const APPEND_LOG_REQUEST_CONFORMANCE: readonly AppendLogRequestConformanceVector[] = [
132
+ { name: 'accepts a canonical post UUID', request: APPEND_LOG_REQUEST, accepts: true },
133
+ { name: 'rejects an omitted post UUID', request: { message: APPEND_LOG_REQUEST.message }, accepts: false },
134
+ { name: 'rejects an invalid post UUID', request: { ...APPEND_LOG_REQUEST, post_id: 'not-a-uuid' }, accepts: false },
135
+ { name: 'rejects unknown request fields', request: { ...APPEND_LOG_REQUEST, extra: true }, accepts: false },
136
+ ];
137
+
102
138
  /** Runtime response vectors keep the append-log decoder aligned with its public type. */
103
139
  export const APPEND_LOG_RESULT_CONFORMANCE: readonly AppendLogResultConformanceVector[] = [
104
140
  {
105
141
  name: 'accepts a response without optional routing metadata',
106
- response: { entry: APPEND_LOG_ENTRY },
142
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false },
107
143
  accepts: true,
108
144
  },
109
145
  {
110
146
  name: 'accepts exact routing metadata and unreachable recipients',
111
147
  response: {
112
148
  entry: APPEND_LOG_ENTRY,
149
+ deduplicated: false,
113
150
  routing: APPEND_LOG_ROUTING,
114
151
  unreachableRecipients: [{ id: 'missing-reviewer', label: 'Missing Reviewer' }],
115
152
  },
@@ -117,73 +154,84 @@ export const APPEND_LOG_RESULT_CONFORMANCE: readonly AppendLogResultConformanceV
117
154
  },
118
155
  {
119
156
  name: 'accepts a null routing echo and an empty unreachable-recipient list',
120
- response: { entry: APPEND_LOG_ENTRY, routing: null, unreachableRecipients: [] },
157
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: true, routing: null, unreachableRecipients: [] },
121
158
  accepts: true,
122
159
  },
160
+ {
161
+ name: 'rejects an omitted deduplication result',
162
+ response: { entry: APPEND_LOG_ENTRY },
163
+ accepts: false,
164
+ },
165
+ {
166
+ name: 'rejects a non-boolean deduplication result',
167
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: 'false' },
168
+ accepts: false,
169
+ },
123
170
  {
124
171
  name: 'rejects unknown top-level response fields',
125
- response: { entry: APPEND_LOG_ENTRY, routing: APPEND_LOG_ROUTING, extra: true },
172
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: APPEND_LOG_ROUTING, extra: true },
126
173
  accepts: false,
127
174
  },
128
175
  {
129
176
  name: 'rejects a non-object routing echo',
130
- response: { entry: APPEND_LOG_ENTRY, routing: 'review' },
177
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: 'review' },
131
178
  accepts: false,
132
179
  },
133
180
  {
134
181
  name: 'rejects missing routing fields',
135
- response: { entry: APPEND_LOG_ENTRY, routing: { class: 'review' } },
182
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { class: 'review' } },
136
183
  accepts: false,
137
184
  },
138
185
  {
139
186
  name: 'rejects unknown routing fields',
140
- response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, extra: true } },
187
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, extra: true } },
141
188
  accepts: false,
142
189
  },
143
190
  {
144
191
  name: 'rejects an invalid routing class',
145
- response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, class: 7 } },
192
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, class: 7 } },
146
193
  accepts: false,
147
194
  },
148
195
  {
149
196
  name: 'rejects an invalid routing recipient collection',
150
- response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, recipients: 'reviewer' } },
197
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, recipients: 'reviewer' } },
151
198
  accepts: false,
152
199
  },
153
200
  {
154
201
  name: 'rejects invalid routing recipient members',
155
- response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, recipients: [7] } },
202
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, recipients: [7] } },
156
203
  accepts: false,
157
204
  },
158
205
  {
159
206
  name: 'rejects an invalid routing fell-open flag',
160
- response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, fellOpen: 'false' } },
207
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, fellOpen: 'false' } },
161
208
  accepts: false,
162
209
  },
163
210
  {
164
211
  name: 'rejects an invalid routing message',
165
- response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, message: 7 } },
212
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, message: 7 } },
166
213
  accepts: false,
167
214
  },
168
215
  {
169
216
  name: 'rejects a non-array unreachable-recipient list',
170
- response: { entry: APPEND_LOG_ENTRY, unreachableRecipients: {} },
217
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, unreachableRecipients: {} },
171
218
  accepts: false,
172
219
  },
173
220
  {
174
221
  name: 'rejects a non-object unreachable recipient',
175
- response: { entry: APPEND_LOG_ENTRY, unreachableRecipients: ['missing-reviewer'] },
222
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, unreachableRecipients: ['missing-reviewer'] },
176
223
  accepts: false,
177
224
  },
178
225
  {
179
226
  name: 'rejects missing unreachable-recipient fields',
180
- response: { entry: APPEND_LOG_ENTRY, unreachableRecipients: [{ id: 'missing-reviewer' }] },
227
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, unreachableRecipients: [{ id: 'missing-reviewer' }] },
181
228
  accepts: false,
182
229
  },
183
230
  {
184
231
  name: 'rejects unknown unreachable-recipient fields',
185
232
  response: {
186
233
  entry: APPEND_LOG_ENTRY,
234
+ deduplicated: false,
187
235
  unreachableRecipients: [{ id: 'missing-reviewer', label: 'Missing Reviewer', extra: true }],
188
236
  },
189
237
  accepts: false,
@@ -192,6 +240,7 @@ export const APPEND_LOG_RESULT_CONFORMANCE: readonly AppendLogResultConformanceV
192
240
  name: 'rejects an invalid unreachable-recipient id',
193
241
  response: {
194
242
  entry: APPEND_LOG_ENTRY,
243
+ deduplicated: false,
195
244
  unreachableRecipients: [{ id: 7, label: 'Missing Reviewer' }],
196
245
  },
197
246
  accepts: false,
@@ -200,6 +249,7 @@ export const APPEND_LOG_RESULT_CONFORMANCE: readonly AppendLogResultConformanceV
200
249
  name: 'rejects an invalid unreachable-recipient label',
201
250
  response: {
202
251
  entry: APPEND_LOG_ENTRY,
252
+ deduplicated: false,
203
253
  unreachableRecipients: [{ id: 'missing-reviewer', label: null }],
204
254
  },
205
255
  accepts: false,
@@ -750,11 +800,48 @@ const ATTACH_RESPONSE = {
750
800
  runtime_metadata_reported: false,
751
801
  },
752
802
  session: { id: '40000000-0000-4000-8000-000000000001' },
803
+ initial_log_cursor: null,
753
804
  } satisfies AttachResponse;
754
805
 
755
806
  /** Wire vectors for the v3 non-expiring attach-session response. */
756
807
  export const ATTACH_SESSION_CONFORMANCE: readonly AttachSessionConformanceVector[] = [
757
808
  { name: 'accepts exact non-expiring session id', response: ATTACH_RESPONSE, accepts: true },
809
+ {
810
+ name: 'accepts a canonical initial log cursor',
811
+ response: {
812
+ ...ATTACH_RESPONSE,
813
+ initial_log_cursor: {
814
+ id: '50000000-0000-4000-8000-000000000001',
815
+ created_at: '2026-07-14T10:00:00.000Z',
816
+ },
817
+ },
818
+ accepts: true,
819
+ },
820
+ {
821
+ name: 'rejects an omitted initial log cursor',
822
+ response: (({ initial_log_cursor: _, ...response }) => response)(ATTACH_RESPONSE),
823
+ accepts: false,
824
+ },
825
+ {
826
+ name: 'rejects an invalid initial log cursor UUID',
827
+ response: {
828
+ ...ATTACH_RESPONSE,
829
+ initial_log_cursor: { id: 'not-a-uuid', created_at: '2026-07-14T10:00:00.000Z' },
830
+ },
831
+ accepts: false,
832
+ },
833
+ {
834
+ name: 'rejects unknown initial log cursor fields',
835
+ response: {
836
+ ...ATTACH_RESPONSE,
837
+ initial_log_cursor: {
838
+ id: '50000000-0000-4000-8000-000000000001',
839
+ created_at: '2026-07-14T10:00:00.000Z',
840
+ extra: true,
841
+ },
842
+ },
843
+ accepts: false,
844
+ },
758
845
  {
759
846
  name: 'rejects retired expires_at field',
760
847
  response: {
@@ -13,7 +13,7 @@ import type {
13
13
  } from './types.js';
14
14
 
15
15
  export const SHARED_PACKAGE_NAME = 'borgmcp-shared' as const;
16
- export const SHARED_PACKAGE_VERSION = '0.11.0' as const;
16
+ export const SHARED_PACKAGE_VERSION = '0.12.2' as const;
17
17
  /** Maximum UTF-8 payload for each newly recorded decision text field. */
18
18
  export const DECISION_TEXT_MAX_BYTES = 512 as const;
19
19
  /** Maximum UTF-8 size of role detailed-description text and any returned section slice. */
@@ -580,7 +580,7 @@ export function decodeProtocolTagPreflight(value: unknown): ProtocolTagPreflight
580
580
  exactKeys(input, ['protocol_version'], ['protocol_version']);
581
581
  if (input.protocol_version !== PROTOCOL_VERSION) {
582
582
  throw new ProtocolContractError(
583
- 'This client requires protocol v8. The peer presents a different version. Update `borgmcp-server` and `borgmcp` to matching releases — server first, then client.',
583
+ 'This client requires protocol v9. The peer presents a different version. Update `borgmcp-server` and `borgmcp` to matching releases — server first, then client.',
584
584
  ErrorCode.UNSUPPORTED_PROTOCOL_VERSION,
585
585
  ['protocol_version'],
586
586
  );
@@ -997,10 +997,11 @@ export function decodeAppendLogRequest(value: unknown): import('./types.js').App
997
997
  const input = record(value);
998
998
  exactKeys(
999
999
  input,
1000
- ['message', 'visibility', 'recipientDroneIds', 'class', 'to'],
1001
- ['message'],
1000
+ ['post_id', 'message', 'visibility', 'recipientDroneIds', 'class', 'to'],
1001
+ ['post_id', 'message'],
1002
1002
  );
1003
1003
  const output: import('./types.js').AppendLogRequest = {
1004
+ post_id: decodeUuid(input.post_id, ['post_id']),
1004
1005
  message: boundedString(input.message, 1, 10_240, ['message']),
1005
1006
  };
1006
1007
  if (input.visibility !== undefined) {
@@ -1259,6 +1260,7 @@ export interface AttachResponse {
1259
1260
  role: AttachRole;
1260
1261
  drone: AttachDrone;
1261
1262
  session: AttachSession;
1263
+ initial_log_cursor: LogCursor | null;
1262
1264
  }
1263
1265
 
1264
1266
  function decodeAttachCube(value: unknown, path: readonly (string | number)[]): AttachCube {
@@ -1393,12 +1395,13 @@ export function decodeAttachRequestEnvelope(
1393
1395
  */
1394
1396
  export function decodeAttachResponse(value: unknown): AttachResponse {
1395
1397
  const input = record(value);
1396
- exactKeys(input, ['result', 'cube', 'role', 'drone', 'session'], [
1398
+ exactKeys(input, ['result', 'cube', 'role', 'drone', 'session', 'initial_log_cursor'], [
1397
1399
  'result',
1398
1400
  'cube',
1399
1401
  'role',
1400
1402
  'drone',
1401
1403
  'session',
1404
+ 'initial_log_cursor',
1402
1405
  ]);
1403
1406
  if (input.result !== 'created' && input.result !== 'reused') {
1404
1407
  fail('Expected result "created" or "reused".', ['result']);
@@ -1409,6 +1412,9 @@ export function decodeAttachResponse(value: unknown): AttachResponse {
1409
1412
  role: decodeAttachRole(input.role, ['role']),
1410
1413
  drone: decodeAttachDrone(input.drone, ['drone']),
1411
1414
  session: decodeAttachSession(input.session, ['session']),
1415
+ initial_log_cursor: input.initial_log_cursor === null
1416
+ ? null
1417
+ : decodeLogCursor(input.initial_log_cursor, ['initial_log_cursor']),
1412
1418
  };
1413
1419
  }
1414
1420
 
@@ -376,8 +376,14 @@ function decodeUnreachableRecipient(
376
376
 
377
377
  export function decodeAppendLogResult(value: unknown): AppendLogResult {
378
378
  const input = object(value);
379
- exact(input, ['entry', 'routing', 'unreachableRecipients'], ['entry']);
380
- const output: AppendLogResult = { entry: decodeEnrichedStreamEntry(input.entry) };
379
+ exact(input, ['entry', 'deduplicated', 'routing', 'unreachableRecipients'], ['entry', 'deduplicated']);
380
+ if (typeof input.deduplicated !== 'boolean') {
381
+ throw new ProtocolContractError('Invalid append-log deduplicated flag.');
382
+ }
383
+ const output: AppendLogResult = {
384
+ entry: decodeEnrichedStreamEntry(input.entry),
385
+ deduplicated: input.deduplicated,
386
+ };
381
387
  if (input.routing !== undefined) {
382
388
  output.routing = input.routing === null ? null : decodeRoutingEcho(input.routing);
383
389
  }
@@ -14,6 +14,7 @@ export enum ErrorCode {
14
14
  NOT_FOUND = 'NOT_FOUND',
15
15
  REPOSITORY_ALREADY_ASSOCIATED = 'REPOSITORY_ALREADY_ASSOCIATED',
16
16
  CUBE_ALREADY_ASSOCIATED = 'CUBE_ALREADY_ASSOCIATED',
17
+ POST_ID_CONFLICT = 'POST_ID_CONFLICT',
17
18
  ROLE_IN_USE = 'ROLE_IN_USE',
18
19
  DEFAULT_ROLE_REQUIRED = 'DEFAULT_ROLE_REQUIRED',
19
20
  ROLE_REQUIRED = 'ROLE_REQUIRED',
@@ -171,6 +171,7 @@ export interface RoutingEcho {
171
171
  }
172
172
 
173
173
  export interface AppendLogRequest {
174
+ post_id: string;
174
175
  message: string;
175
176
  visibility?: LogVisibility;
176
177
  recipientDroneIds?: string[];
@@ -180,6 +181,7 @@ export interface AppendLogRequest {
180
181
 
181
182
  export interface AppendLogResponse {
182
183
  entry: ActivityLogEntry;
184
+ deduplicated: boolean;
183
185
  routing?: RoutingEcho | null;
184
186
  unreachableRecipients?: Array<{ id: string; label: string }>;
185
187
  }
@@ -1,4 +1,4 @@
1
- /** Current Borg coordination protocol generation. Clean-slate v8. */
2
- export const PROTOCOL_VERSION = '8' as const;
1
+ /** Current Borg coordination protocol generation. Clean-slate v9. */
2
+ export const PROTOCOL_VERSION = '9' as const;
3
3
 
4
4
  export type ProtocolVersion = typeof PROTOCOL_VERSION;