borgmcp-shared 0.11.0 → 0.12.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.
@@ -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,
@@ -361,6 +362,7 @@ export const ADAPTER_CONFORMANCE_FIXTURES = [
361
362
  { id: 'security.adapter-boundary-injection', area: 'security' },
362
363
  { id: 'security.oversize-request', area: 'security' },
363
364
  { id: 'security.cross-cube-isolation', area: 'security' },
365
+ { id: 'log.append-idempotency', area: 'log' },
364
366
  { id: 'log.read-cursor-tuple', area: 'cursor' },
365
367
  { id: 'sse.replay-live-transition', area: 'sse' },
366
368
  { id: 'cursor.explicit-expiry', area: 'cursor' },
@@ -1323,7 +1325,10 @@ export async function runAdapterConformance(
1323
1325
  await record('security.adapter-boundary-injection', async () => {
1324
1326
  const injectedMessage = "'); DROP TABLE log_entries; --\r\ndata: forged-sse-frame";
1325
1327
  const injectedBody = JSON.stringify(
1326
- createProtocolEnvelope('inject-b1', { message: injectedMessage }),
1328
+ createProtocolEnvelope('inject-b1', {
1329
+ post_id: '00000000-0000-4000-8000-000000000301',
1330
+ message: injectedMessage,
1331
+ }),
1327
1332
  );
1328
1333
  invariant(
1329
1334
  utf8ByteLength(injectedBody) <= PROTOCOL_LIMIT_CEILINGS.max_request_bytes &&
@@ -1342,7 +1347,10 @@ export async function runAdapterConformance(
1342
1347
  const sentinel = await environment.operations.append(
1343
1348
  credentialB,
1344
1349
  cubeB,
1345
- createProtocolEnvelope('inject-b2', { message: 'post-injection-sentinel' }),
1350
+ createProtocolEnvelope('inject-b2', {
1351
+ post_id: '00000000-0000-4000-8000-000000000302',
1352
+ message: 'post-injection-sentinel',
1353
+ }),
1346
1354
  );
1347
1355
  expectStatus(sentinel, 201, 'Post-injection sentinel append');
1348
1356
  const read = await environment.operations.read(
@@ -1389,7 +1397,10 @@ export async function runAdapterConformance(
1389
1397
 
1390
1398
  await record('security.oversize-request', async () => {
1391
1399
  const baseBody = JSON.stringify(
1392
- createProtocolEnvelope('oversize-a1', { message: 'must-not-persist' }),
1400
+ createProtocolEnvelope('oversize-a1', {
1401
+ post_id: '00000000-0000-4000-8000-000000000303',
1402
+ message: 'must-not-persist',
1403
+ }),
1393
1404
  );
1394
1405
  const oversizedBody = baseBody + ' '.repeat(
1395
1406
  Math.max(0, PROTOCOL_LIMIT_CEILINGS.max_request_bytes - utf8ByteLength(baseBody) + 1),
@@ -1417,7 +1428,10 @@ export async function runAdapterConformance(
1417
1428
  const secretAppend = await environment.operations.append(
1418
1429
  credentialB,
1419
1430
  cubeB,
1420
- createProtocolEnvelope('append-b1', { message: 'principal-b-secret' }),
1431
+ createProtocolEnvelope('append-b1', {
1432
+ post_id: '00000000-0000-4000-8000-000000000304',
1433
+ message: 'principal-b-secret',
1434
+ }),
1421
1435
  );
1422
1436
  expectStatus(secretAppend, 201, 'Principal B append');
1423
1437
  const denied = await environment.operations.read(
@@ -1429,6 +1443,77 @@ export async function runAdapterConformance(
1429
1443
  return { status: 404, code: ErrorCode.NOT_FOUND };
1430
1444
  });
1431
1445
 
1446
+ await record('log.append-idempotency', async () => {
1447
+ const principal = await environment.admin.createPrincipal('append-idempotency');
1448
+ const otherPrincipal = await environment.admin.createPrincipal('append-idempotency-other');
1449
+ const cube = await environment.admin.createCube('append-idempotency');
1450
+ await environment.admin.grantCube(principal, cube);
1451
+ await environment.admin.grantCube(otherPrincipal, cube);
1452
+ const credential = await environment.admin.issueDroneSession(principal);
1453
+ const otherCredential = await environment.admin.issueDroneSession(otherPrincipal);
1454
+ const role = await environment.admin.createRole(cube, {
1455
+ roleClass: 'worker',
1456
+ isHumanSeat: false,
1457
+ });
1458
+ const firstRecipient = await environment.admin.createDrone(principal, cube, role);
1459
+ const secondRecipient = await environment.admin.createDrone(principal, cube, role);
1460
+ const postId = '00000000-0000-4000-8000-000000000313';
1461
+ const initialPayload = {
1462
+ post_id: postId,
1463
+ message: 'once',
1464
+ visibility: 'direct' as const,
1465
+ recipientDroneIds: [firstRecipient.id],
1466
+ class: 'review',
1467
+ to: ['Coordinator'],
1468
+ };
1469
+ const first = await environment.operations.append(
1470
+ credential,
1471
+ cube,
1472
+ createProtocolEnvelope('append-idempotency-first', initialPayload),
1473
+ );
1474
+ expectStatus(first, 201, 'Initial idempotent append');
1475
+ const firstResult = decodeAppendLogResultEnvelope(first.body).payload;
1476
+ invariant(!firstResult.deduplicated, 'Initial append was reported as deduplicated.');
1477
+ for (const [index, vector] of APPEND_LOG_IDEMPOTENCY_CONFORMANCE.entries()) {
1478
+ const payload = {
1479
+ ...initialPayload,
1480
+ ...(vector.mutation === 'message' ? { message: 'changed' } : {}),
1481
+ ...(vector.mutation === 'visibility' ? { visibility: 'broadcast' as const } : {}),
1482
+ ...(vector.mutation === 'recipient_set' ? { recipientDroneIds: [secondRecipient.id] } : {}),
1483
+ ...(vector.mutation === 'resolved_class_routing' ? { class: 'security', to: ['Security Auditor'] } : {}),
1484
+ };
1485
+ const response = await environment.operations.append(
1486
+ vector.actor === 'same' ? credential : otherCredential,
1487
+ cube,
1488
+ createProtocolEnvelope(`append-idempotency-${index}`, payload),
1489
+ );
1490
+ if (vector.expected === 'POST_ID_CONFLICT') {
1491
+ expectError(response, 409, ErrorCode.POST_ID_CONFLICT, vector.name);
1492
+ continue;
1493
+ }
1494
+ expectStatus(response, 201, vector.name);
1495
+ const result = decodeAppendLogResultEnvelope(response.body).payload;
1496
+ invariant(result.deduplicated === (vector.expected === 'deduplicated'), `${vector.name} returned the wrong deduplicated flag.`);
1497
+ invariant(
1498
+ vector.expected === 'deduplicated'
1499
+ ? result.entry.id === firstResult.entry.id
1500
+ : result.entry.id !== firstResult.entry.id,
1501
+ `${vector.name} returned the wrong entry identity.`,
1502
+ );
1503
+ }
1504
+ const read = await environment.operations.read(
1505
+ credential,
1506
+ cube,
1507
+ createProtocolEnvelope('append-idempotency-read', { cursor: null, limit: 10 }),
1508
+ );
1509
+ expectStatus(read, 200, 'Idempotent append read');
1510
+ invariant(
1511
+ decodeReadLogResultEnvelope(read.body).payload.entries.length === 2,
1512
+ 'Append collision controls persisted the wrong number of entries.',
1513
+ );
1514
+ return { persisted_entries: 2, stable_entry: true, deduplicated: true, conflicts: 4, cross_author_created: true };
1515
+ });
1516
+
1432
1517
  const entries: Array<{ id: string; created_at: string; message: string }> = [];
1433
1518
  let readCursor: LogCursor | null = null;
1434
1519
  await record('log.read-cursor-tuple', async () => {
@@ -1436,7 +1521,10 @@ export async function runAdapterConformance(
1436
1521
  const response = await environment.operations.append(
1437
1522
  credentialA,
1438
1523
  cubeA,
1439
- createProtocolEnvelope(`append-a${index + 1}`, { message }),
1524
+ createProtocolEnvelope(`append-a${index + 1}`, {
1525
+ post_id: `00000000-0000-4000-8000-${String(305 + index).padStart(12, '0')}`,
1526
+ message,
1527
+ }),
1440
1528
  );
1441
1529
  expectStatus(response, 201, `Append ${message}`);
1442
1530
  const entry = decodeAppendLogResultEnvelope(response.body).payload.entry;
@@ -1470,7 +1558,10 @@ export async function runAdapterConformance(
1470
1558
  const appendDelta = environment.operations.append(
1471
1559
  credentialA,
1472
1560
  cubeA,
1473
- createProtocolEnvelope('append-a4', { message: 'delta' }),
1561
+ createProtocolEnvelope('append-a4', {
1562
+ post_id: '00000000-0000-4000-8000-000000000308',
1563
+ message: 'delta',
1564
+ }),
1474
1565
  );
1475
1566
  expectStatus(await appendDelta, 201, 'Transition append');
1476
1567
  } finally {
@@ -1494,7 +1585,10 @@ export async function runAdapterConformance(
1494
1585
  const epsilonResponse = await environment.operations.append(
1495
1586
  credentialA,
1496
1587
  cubeA,
1497
- createProtocolEnvelope('append-a5', { message: 'epsilon' }),
1588
+ createProtocolEnvelope('append-a5', {
1589
+ post_id: '00000000-0000-4000-8000-000000000309',
1590
+ message: 'epsilon',
1591
+ }),
1498
1592
  );
1499
1593
  expectStatus(epsilonResponse, 201, 'Live append');
1500
1594
  const epsilon = logEvent(await within(noDuplicate, 'Live epsilon event', streamDeadlineMs), 'Live append');
@@ -2014,6 +2108,7 @@ export async function runAdapterConformance(
2014
2108
  cubeA,
2015
2109
  createProtocolEnvelope('evict-direct-target', {
2016
2110
  message: 'must-not-fan-out',
2111
+ post_id: '00000000-0000-4000-8000-000000000310',
2017
2112
  visibility: 'direct',
2018
2113
  recipientDroneIds: [evictedDrone.id],
2019
2114
  }),
@@ -2203,7 +2298,10 @@ export async function runAdapterConformance(
2203
2298
  const attributed = await environment.operations.append(
2204
2299
  evictedSession,
2205
2300
  roleContractCube,
2206
- createProtocolEnvelope('role-delete-attribution', { message: 'attribution survives' }),
2301
+ createProtocolEnvelope('role-delete-attribution', {
2302
+ post_id: '00000000-0000-4000-8000-000000000311',
2303
+ message: 'attribution survives',
2304
+ }),
2207
2305
  );
2208
2306
  expectStatus(attributed, 201, 'Pre-delete attributed log');
2209
2307
  const attributedEntry = decodeAppendLogResultEnvelope(attributed.body).payload.entry;
@@ -2867,7 +2965,10 @@ export async function runAdapterConformance(
2867
2965
  const append = await environment.operations.append(
2868
2966
  creator.credential,
2869
2967
  cube,
2870
- createProtocolEnvelope('delete-log', { message: 'deleted with cube' }),
2968
+ createProtocolEnvelope('delete-log', {
2969
+ post_id: '00000000-0000-4000-8000-000000000312',
2970
+ message: 'deleted with cube',
2971
+ }),
2871
2972
  );
2872
2973
  expectStatus(append, 201, 'Deletion fixture log');
2873
2974
  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,53 @@ 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' | '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: 'rejects a same-author resolved class-routing collision', actor: 'same', mutation: 'resolved_class_routing', expected: 'POST_ID_CONFLICT' },
122
+ { name: 'creates an independent cross-author post', actor: 'different', mutation: 'none', expected: 'created' },
123
+ ];
124
+
125
+ const APPEND_LOG_REQUEST = {
126
+ post_id: '00000000-0000-4000-8000-000000000205',
127
+ message: 'REVIEW-READY: example',
128
+ } satisfies AppendLogRequest;
129
+
130
+ export const APPEND_LOG_REQUEST_CONFORMANCE: readonly AppendLogRequestConformanceVector[] = [
131
+ { name: 'accepts a canonical post UUID', request: APPEND_LOG_REQUEST, accepts: true },
132
+ { name: 'rejects an omitted post UUID', request: { message: APPEND_LOG_REQUEST.message }, accepts: false },
133
+ { name: 'rejects an invalid post UUID', request: { ...APPEND_LOG_REQUEST, post_id: 'not-a-uuid' }, accepts: false },
134
+ { name: 'rejects unknown request fields', request: { ...APPEND_LOG_REQUEST, extra: true }, accepts: false },
135
+ ];
136
+
102
137
  /** Runtime response vectors keep the append-log decoder aligned with its public type. */
103
138
  export const APPEND_LOG_RESULT_CONFORMANCE: readonly AppendLogResultConformanceVector[] = [
104
139
  {
105
140
  name: 'accepts a response without optional routing metadata',
106
- response: { entry: APPEND_LOG_ENTRY },
141
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false },
107
142
  accepts: true,
108
143
  },
109
144
  {
110
145
  name: 'accepts exact routing metadata and unreachable recipients',
111
146
  response: {
112
147
  entry: APPEND_LOG_ENTRY,
148
+ deduplicated: false,
113
149
  routing: APPEND_LOG_ROUTING,
114
150
  unreachableRecipients: [{ id: 'missing-reviewer', label: 'Missing Reviewer' }],
115
151
  },
@@ -117,73 +153,84 @@ export const APPEND_LOG_RESULT_CONFORMANCE: readonly AppendLogResultConformanceV
117
153
  },
118
154
  {
119
155
  name: 'accepts a null routing echo and an empty unreachable-recipient list',
120
- response: { entry: APPEND_LOG_ENTRY, routing: null, unreachableRecipients: [] },
156
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: true, routing: null, unreachableRecipients: [] },
121
157
  accepts: true,
122
158
  },
159
+ {
160
+ name: 'rejects an omitted deduplication result',
161
+ response: { entry: APPEND_LOG_ENTRY },
162
+ accepts: false,
163
+ },
164
+ {
165
+ name: 'rejects a non-boolean deduplication result',
166
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: 'false' },
167
+ accepts: false,
168
+ },
123
169
  {
124
170
  name: 'rejects unknown top-level response fields',
125
- response: { entry: APPEND_LOG_ENTRY, routing: APPEND_LOG_ROUTING, extra: true },
171
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: APPEND_LOG_ROUTING, extra: true },
126
172
  accepts: false,
127
173
  },
128
174
  {
129
175
  name: 'rejects a non-object routing echo',
130
- response: { entry: APPEND_LOG_ENTRY, routing: 'review' },
176
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: 'review' },
131
177
  accepts: false,
132
178
  },
133
179
  {
134
180
  name: 'rejects missing routing fields',
135
- response: { entry: APPEND_LOG_ENTRY, routing: { class: 'review' } },
181
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { class: 'review' } },
136
182
  accepts: false,
137
183
  },
138
184
  {
139
185
  name: 'rejects unknown routing fields',
140
- response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, extra: true } },
186
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, extra: true } },
141
187
  accepts: false,
142
188
  },
143
189
  {
144
190
  name: 'rejects an invalid routing class',
145
- response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, class: 7 } },
191
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, class: 7 } },
146
192
  accepts: false,
147
193
  },
148
194
  {
149
195
  name: 'rejects an invalid routing recipient collection',
150
- response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, recipients: 'reviewer' } },
196
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, recipients: 'reviewer' } },
151
197
  accepts: false,
152
198
  },
153
199
  {
154
200
  name: 'rejects invalid routing recipient members',
155
- response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, recipients: [7] } },
201
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, recipients: [7] } },
156
202
  accepts: false,
157
203
  },
158
204
  {
159
205
  name: 'rejects an invalid routing fell-open flag',
160
- response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, fellOpen: 'false' } },
206
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, fellOpen: 'false' } },
161
207
  accepts: false,
162
208
  },
163
209
  {
164
210
  name: 'rejects an invalid routing message',
165
- response: { entry: APPEND_LOG_ENTRY, routing: { ...APPEND_LOG_ROUTING, message: 7 } },
211
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, routing: { ...APPEND_LOG_ROUTING, message: 7 } },
166
212
  accepts: false,
167
213
  },
168
214
  {
169
215
  name: 'rejects a non-array unreachable-recipient list',
170
- response: { entry: APPEND_LOG_ENTRY, unreachableRecipients: {} },
216
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, unreachableRecipients: {} },
171
217
  accepts: false,
172
218
  },
173
219
  {
174
220
  name: 'rejects a non-object unreachable recipient',
175
- response: { entry: APPEND_LOG_ENTRY, unreachableRecipients: ['missing-reviewer'] },
221
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, unreachableRecipients: ['missing-reviewer'] },
176
222
  accepts: false,
177
223
  },
178
224
  {
179
225
  name: 'rejects missing unreachable-recipient fields',
180
- response: { entry: APPEND_LOG_ENTRY, unreachableRecipients: [{ id: 'missing-reviewer' }] },
226
+ response: { entry: APPEND_LOG_ENTRY, deduplicated: false, unreachableRecipients: [{ id: 'missing-reviewer' }] },
181
227
  accepts: false,
182
228
  },
183
229
  {
184
230
  name: 'rejects unknown unreachable-recipient fields',
185
231
  response: {
186
232
  entry: APPEND_LOG_ENTRY,
233
+ deduplicated: false,
187
234
  unreachableRecipients: [{ id: 'missing-reviewer', label: 'Missing Reviewer', extra: true }],
188
235
  },
189
236
  accepts: false,
@@ -192,6 +239,7 @@ export const APPEND_LOG_RESULT_CONFORMANCE: readonly AppendLogResultConformanceV
192
239
  name: 'rejects an invalid unreachable-recipient id',
193
240
  response: {
194
241
  entry: APPEND_LOG_ENTRY,
242
+ deduplicated: false,
195
243
  unreachableRecipients: [{ id: 7, label: 'Missing Reviewer' }],
196
244
  },
197
245
  accepts: false,
@@ -200,6 +248,7 @@ export const APPEND_LOG_RESULT_CONFORMANCE: readonly AppendLogResultConformanceV
200
248
  name: 'rejects an invalid unreachable-recipient label',
201
249
  response: {
202
250
  entry: APPEND_LOG_ENTRY,
251
+ deduplicated: false,
203
252
  unreachableRecipients: [{ id: 'missing-reviewer', label: null }],
204
253
  },
205
254
  accepts: false,
@@ -750,11 +799,48 @@ const ATTACH_RESPONSE = {
750
799
  runtime_metadata_reported: false,
751
800
  },
752
801
  session: { id: '40000000-0000-4000-8000-000000000001' },
802
+ initial_log_cursor: null,
753
803
  } satisfies AttachResponse;
754
804
 
755
805
  /** Wire vectors for the v3 non-expiring attach-session response. */
756
806
  export const ATTACH_SESSION_CONFORMANCE: readonly AttachSessionConformanceVector[] = [
757
807
  { name: 'accepts exact non-expiring session id', response: ATTACH_RESPONSE, accepts: true },
808
+ {
809
+ name: 'accepts a canonical initial log cursor',
810
+ response: {
811
+ ...ATTACH_RESPONSE,
812
+ initial_log_cursor: {
813
+ id: '50000000-0000-4000-8000-000000000001',
814
+ created_at: '2026-07-14T10:00:00.000Z',
815
+ },
816
+ },
817
+ accepts: true,
818
+ },
819
+ {
820
+ name: 'rejects an omitted initial log cursor',
821
+ response: (({ initial_log_cursor: _, ...response }) => response)(ATTACH_RESPONSE),
822
+ accepts: false,
823
+ },
824
+ {
825
+ name: 'rejects an invalid initial log cursor UUID',
826
+ response: {
827
+ ...ATTACH_RESPONSE,
828
+ initial_log_cursor: { id: 'not-a-uuid', created_at: '2026-07-14T10:00:00.000Z' },
829
+ },
830
+ accepts: false,
831
+ },
832
+ {
833
+ name: 'rejects unknown initial log cursor fields',
834
+ response: {
835
+ ...ATTACH_RESPONSE,
836
+ initial_log_cursor: {
837
+ id: '50000000-0000-4000-8000-000000000001',
838
+ created_at: '2026-07-14T10:00:00.000Z',
839
+ extra: true,
840
+ },
841
+ },
842
+ accepts: false,
843
+ },
758
844
  {
759
845
  name: 'rejects retired expires_at field',
760
846
  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.0' 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;