borgmcp-shared 0.13.1 → 0.14.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.
@@ -6,6 +6,7 @@ import {
6
6
  decodeCreateCubeResponseEnvelope,
7
7
  decodeDeleteCubeResponseEnvelope,
8
8
  decodeAppendLogResultEnvelope,
9
+ decodeAckStatusResultEnvelope,
9
10
  decodeDecisionResultEnvelope,
10
11
  decodeDecisionsResultEnvelope,
11
12
  decodeEnrollmentExchangeResponseEnvelope,
@@ -119,6 +120,8 @@ export interface ConformanceCubeManagementState {
119
120
  export interface ConformanceAuthorityState {
120
121
  enrolled_clients: number;
121
122
  enrollment_claims: number;
123
+ activity_acknowledgements: number;
124
+ activity_claims: number;
122
125
  cubes: number;
123
126
  roles: number;
124
127
  grants: number;
@@ -304,6 +307,11 @@ export interface ConformanceOperations {
304
307
  cube: ConformanceCube,
305
308
  request: unknown,
306
309
  ): Promise<ConformanceHttpResponse>;
310
+ ackStatus(
311
+ credential: string,
312
+ cube: ConformanceCube,
313
+ request: unknown,
314
+ ): Promise<ConformanceHttpResponse>;
307
315
  updateCube(
308
316
  credential: string,
309
317
  cube: ConformanceCube,
@@ -383,6 +391,7 @@ export const ADAPTER_CONFORMANCE_FIXTURES = [
383
391
  { id: 'sse.replay-live-transition', area: 'sse' },
384
392
  { id: 'cursor.explicit-expiry', area: 'cursor' },
385
393
  { id: 'acks.idempotent', area: 'acks' },
394
+ { id: 'acks.status-query', area: 'acks' },
386
395
  { id: 'claims.durable-noncursor', area: 'claims' },
387
396
  { id: 'decisions.topic-supersession', area: 'decisions' },
388
397
  { id: 'security.drone-management-authorization', area: 'security' },
@@ -1994,6 +2003,136 @@ export async function runAdapterConformance(
1994
2003
  return { first_status: 204, repeated_status: 204, bodyless: true };
1995
2004
  });
1996
2005
 
2006
+ await record('acks.status-query', async () => {
2007
+ const cube = await environment.admin.createCube('ack-status-cube');
2008
+ await environment.admin.grantCube(principalA, cube);
2009
+ const role = await environment.admin.createRole(cube, {
2010
+ roleClass: 'worker',
2011
+ isHumanSeat: false,
2012
+ name: 'Ack Status Worker',
2013
+ });
2014
+ const acknowledgedRecipient = await environment.admin.createDrone(principalA, cube, role);
2015
+ const claimingRecipient = await environment.admin.createDrone(principalA, cube, role);
2016
+ const acknowledgedCredential = await environment.admin.issueManagedDroneSession(acknowledgedRecipient);
2017
+ const claimingCredential = await environment.admin.issueManagedDroneSession(claimingRecipient);
2018
+ const append = await environment.operations.append(
2019
+ credentialA,
2020
+ cube,
2021
+ createProtocolEnvelope('ack-status-append', {
2022
+ post_id: '00000000-0000-4000-8000-000000000310',
2023
+ message: 'START NOW acknowledgement status fixture',
2024
+ visibility: 'direct',
2025
+ recipientDroneIds: [acknowledgedRecipient.id, claimingRecipient.id],
2026
+ }),
2027
+ );
2028
+ expectStatus(append, 201, 'Acknowledgement-status fixture append');
2029
+ const entry = decodeAppendLogResultEnvelope(append.body).payload.entry;
2030
+
2031
+ const beforeMissing = await environment.admin.observeAuthorityState();
2032
+ const missingResponse = await environment.operations.ackStatus(
2033
+ credentialA,
2034
+ cube,
2035
+ createProtocolEnvelope('ack-status-missing', { entry_id: entry.id }),
2036
+ );
2037
+ expectStatus(missingResponse, 200, 'Missing acknowledgement status');
2038
+ const missing = decodeAckStatusResultEnvelope(missingResponse.body).payload;
2039
+ invariant(
2040
+ missing.visibility === 'direct' &&
2041
+ missing.recipients.length === 2 &&
2042
+ missing.recipients.every((recipient) => recipient.acknowledged_at === null) &&
2043
+ missing.claims.length === 0,
2044
+ 'Missing acknowledgement was not represented as nullable per-recipient state.',
2045
+ );
2046
+ assertStateDelta(
2047
+ beforeMissing,
2048
+ await environment.admin.observeAuthorityState(),
2049
+ {},
2050
+ 'Missing acknowledgement status query',
2051
+ );
2052
+
2053
+ expectStatus(
2054
+ await environment.operations.ack(
2055
+ acknowledgedCredential,
2056
+ cube,
2057
+ createProtocolEnvelope('ack-status-ack', { entry_id: entry.id, kind: 'ack' }),
2058
+ ),
2059
+ 204,
2060
+ 'Acknowledgement-status acknowledgement',
2061
+ );
2062
+ expectStatus(
2063
+ await environment.operations.ack(
2064
+ claimingCredential,
2065
+ cube,
2066
+ createProtocolEnvelope('ack-status-claim', { entry_id: entry.id, kind: 'claim' }),
2067
+ ),
2068
+ 204,
2069
+ 'Acknowledgement-status claim',
2070
+ );
2071
+ const beforeDistinct = await environment.admin.observeAuthorityState();
2072
+ const distinctResponse = await environment.operations.ackStatus(
2073
+ credentialA,
2074
+ cube,
2075
+ createProtocolEnvelope('ack-status-distinct', { entry_id: entry.id }),
2076
+ );
2077
+ expectStatus(distinctResponse, 200, 'Distinct acknowledgement and claim status');
2078
+ const distinct = decodeAckStatusResultEnvelope(distinctResponse.body).payload;
2079
+ const acknowledged = distinct.recipients.filter((recipient) => recipient.acknowledged_at !== null);
2080
+ invariant(
2081
+ acknowledged.length === 1 &&
2082
+ acknowledged[0].drone_id === acknowledgedRecipient.id &&
2083
+ distinct.claims.length === 1 &&
2084
+ distinct.claims[0].drone_id === claimingRecipient.id &&
2085
+ distinct.recipients.find((recipient) => recipient.drone_id === claimingRecipient.id)?.acknowledged_at === null,
2086
+ 'Acknowledgement status conflated acknowledgement and claim records.',
2087
+ );
2088
+ assertStateDelta(
2089
+ beforeDistinct,
2090
+ await environment.admin.observeAuthorityState(),
2091
+ {},
2092
+ 'Distinct acknowledgement and claim status query',
2093
+ );
2094
+
2095
+ const unreadResponse = await environment.operations.read(
2096
+ credentialA,
2097
+ cube,
2098
+ createProtocolEnvelope('ack-status-unread', { cursor: null, limit: 10 }),
2099
+ );
2100
+ expectStatus(unreadResponse, 200, 'Post-query unread read');
2101
+ const unread = decodeReadLogResultEnvelope(unreadResponse.body).payload;
2102
+ invariant(
2103
+ unread.entries.length === 1 && unread.entries[0].id === entry.id,
2104
+ 'Acknowledgement-status query advanced unread delivery state.',
2105
+ );
2106
+
2107
+ const beforeUnknown = await environment.admin.observeAuthorityState();
2108
+ expectError(
2109
+ await environment.operations.ackStatus(
2110
+ credentialA,
2111
+ cube,
2112
+ createProtocolEnvelope('ack-status-unknown', {
2113
+ entry_id: '00000000-0000-4000-8000-000000000311',
2114
+ }),
2115
+ ),
2116
+ 404,
2117
+ ErrorCode.NOT_FOUND,
2118
+ 'Unknown acknowledgement-status entry',
2119
+ );
2120
+ assertStateDelta(
2121
+ beforeUnknown,
2122
+ await environment.admin.observeAuthorityState(),
2123
+ {},
2124
+ 'Unknown acknowledgement status query',
2125
+ );
2126
+ return {
2127
+ missing_acknowledgement: null,
2128
+ acknowledgements: 1,
2129
+ claims: 1,
2130
+ distinct: true,
2131
+ unread_entry_available: true,
2132
+ unknown_entry_code: ErrorCode.NOT_FOUND,
2133
+ };
2134
+ });
2135
+
1997
2136
  await record('claims.durable-noncursor', async () => {
1998
2137
  invariant(entries[1] && liveCursor, 'Log fixtures did not produce claim state.');
1999
2138
  const claim = await environment.operations.ack(
@@ -1012,6 +1012,40 @@ export const ROLE_RATIONALE_CONFORMANCE: readonly RoleRationaleConformanceVector
1012
1012
  },
1013
1013
  ];
1014
1014
 
1015
+ export interface AckStatusConformanceVector {
1016
+ name: string;
1017
+ fixture: 'missing-ack' | 'ack-and-claim' | 'unread-preserved' | 'unknown-entry';
1018
+ expected:
1019
+ | { status: 200; acknowledged_at: null; mutation: 'none' }
1020
+ | { status: 200; acknowledgements: 1; claims: 1; distinct: true; mutation: 'none' }
1021
+ | { status: 200; unread_entry_available: true; mutation: 'none' }
1022
+ | { status: 404; error: 'NOT_FOUND'; mutation: 'none' };
1023
+ }
1024
+
1025
+ /** Portable acknowledgement-status outcomes for one exact log entry. */
1026
+ export const ACK_STATUS_CONFORMANCE: readonly AckStatusConformanceVector[] = [
1027
+ {
1028
+ name: 'reports an intended recipient with no acknowledgement',
1029
+ fixture: 'missing-ack',
1030
+ expected: { status: 200, acknowledged_at: null, mutation: 'none' },
1031
+ },
1032
+ {
1033
+ name: 'keeps acknowledgements and claims in separate result collections',
1034
+ fixture: 'ack-and-claim',
1035
+ expected: { status: 200, acknowledgements: 1, claims: 1, distinct: true, mutation: 'none' },
1036
+ },
1037
+ {
1038
+ name: 'does not consume the queried entry from unread delivery',
1039
+ fixture: 'unread-preserved',
1040
+ expected: { status: 200, unread_entry_available: true, mutation: 'none' },
1041
+ },
1042
+ {
1043
+ name: 'returns not found for an unknown entry instead of missing acknowledgement state',
1044
+ fixture: 'unknown-entry',
1045
+ expected: { status: 404, error: 'NOT_FOUND', mutation: 'none' },
1046
+ },
1047
+ ];
1048
+
1015
1049
  /** One canonical corpus consumed unchanged by shared, server, and client tests. */
1016
1050
  export const RUNTIME_METADATA_REPOSITORY_CONFORMANCE:
1017
1051
  readonly RuntimeMetadataRepositoryConformanceVector[] = [
@@ -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.13.1' as const;
16
+ export const SHARED_PACKAGE_VERSION = '0.14.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. */
@@ -590,7 +590,7 @@ export function decodeProtocolTagPreflight(value: unknown): ProtocolTagPreflight
590
590
  exactKeys(input, ['protocol_version'], ['protocol_version']);
591
591
  if (input.protocol_version !== PROTOCOL_VERSION) {
592
592
  throw new ProtocolContractError(
593
- 'This client requires protocol v10. The peer presents a different version. Update `borgmcp-server` and `borgmcp` to matching releases — server first, then client.',
593
+ 'This client requires protocol v11. The peer presents a different version. Update `borgmcp-server` and `borgmcp` to matching releases — server first, then client.',
594
594
  ErrorCode.UNSUPPORTED_PROTOCOL_VERSION,
595
595
  ['protocol_version'],
596
596
  );
@@ -16,6 +16,7 @@ import type {
16
16
  AppendLogResponse,
17
17
  Decision,
18
18
  EnrichedStreamEntry,
19
+ LogVisibility,
19
20
  RoutingEcho,
20
21
  } from './types.js';
21
22
  import { isLabelLine, parseRoleSections } from '../role-section.js';
@@ -46,6 +47,31 @@ export interface ReadLogResult {
46
47
  claims: ClaimRecord[];
47
48
  }
48
49
 
50
+ export interface AckStatusRequest {
51
+ entry_id: string;
52
+ }
53
+
54
+ export interface AckStatusRecipient {
55
+ drone_id: string;
56
+ drone_label: string | null;
57
+ drone_role: string | null;
58
+ acknowledged_at: string | null;
59
+ }
60
+
61
+ export interface AckStatusClaim {
62
+ drone_id: string;
63
+ drone_label: string | null;
64
+ drone_role: string | null;
65
+ claimed_at: string;
66
+ }
67
+
68
+ export interface AckStatusResult {
69
+ entry_id: string;
70
+ visibility: LogVisibility;
71
+ recipients: AckStatusRecipient[];
72
+ claims: AckStatusClaim[];
73
+ }
74
+
49
75
  export interface ReassignDroneRequest {
50
76
  role_id: string;
51
77
  }
@@ -309,6 +335,80 @@ export function decodeReadLogRequestEnvelope(
309
335
  return decodeProtocolEnvelope(value, decodeReadLogRequest);
310
336
  }
311
337
 
338
+ export function decodeAckStatusRequest(value: unknown): AckStatusRequest {
339
+ const input = object(value);
340
+ exact(input, ['entry_id'], ['entry_id']);
341
+ return { entry_id: decodeUuid(input.entry_id, ['entry_id']) };
342
+ }
343
+
344
+ export function decodeAckStatusRequestEnvelope(
345
+ value: unknown,
346
+ ): ProtocolEnvelope<AckStatusRequest> {
347
+ return decodeProtocolEnvelope(value, decodeAckStatusRequest);
348
+ }
349
+
350
+ function decodeAckStatusRecipient(value: unknown): AckStatusRecipient {
351
+ const input = object(value);
352
+ exact(
353
+ input,
354
+ ['drone_id', 'drone_label', 'drone_role', 'acknowledged_at'],
355
+ ['drone_id', 'drone_label', 'drone_role', 'acknowledged_at'],
356
+ );
357
+ return {
358
+ drone_id: decodeUuid(input.drone_id, ['drone_id']),
359
+ drone_label: nullableString(input.drone_label, 'drone_label', 120),
360
+ drone_role: nullableString(input.drone_role, 'drone_role', 120),
361
+ acknowledged_at: input.acknowledged_at === null
362
+ ? null
363
+ : decodeCanonicalTimestamp(input.acknowledged_at, ['acknowledged_at']),
364
+ };
365
+ }
366
+
367
+ function decodeAckStatusClaim(value: unknown): AckStatusClaim {
368
+ const input = object(value);
369
+ exact(
370
+ input,
371
+ ['drone_id', 'drone_label', 'drone_role', 'claimed_at'],
372
+ ['drone_id', 'drone_label', 'drone_role', 'claimed_at'],
373
+ );
374
+ return {
375
+ drone_id: decodeUuid(input.drone_id, ['drone_id']),
376
+ drone_label: nullableString(input.drone_label, 'drone_label', 120),
377
+ drone_role: nullableString(input.drone_role, 'drone_role', 120),
378
+ claimed_at: decodeCanonicalTimestamp(input.claimed_at, ['claimed_at']),
379
+ };
380
+ }
381
+
382
+ export function decodeAckStatusResult(value: unknown): AckStatusResult {
383
+ const input = object(value);
384
+ exact(
385
+ input,
386
+ ['entry_id', 'visibility', 'recipients', 'claims'],
387
+ ['entry_id', 'visibility', 'recipients', 'claims'],
388
+ );
389
+ if (input.visibility !== 'broadcast' && input.visibility !== 'direct') {
390
+ throw new ProtocolContractError('Invalid acknowledgement-status visibility.');
391
+ }
392
+ if (!Array.isArray(input.recipients) || input.recipients.length > 500) {
393
+ throw new ProtocolContractError('Invalid acknowledgement-status recipients.');
394
+ }
395
+ if (!Array.isArray(input.claims) || input.claims.length > 500) {
396
+ throw new ProtocolContractError('Invalid acknowledgement-status claims.');
397
+ }
398
+ return {
399
+ entry_id: decodeUuid(input.entry_id, ['entry_id']),
400
+ visibility: input.visibility,
401
+ recipients: input.recipients.map(decodeAckStatusRecipient),
402
+ claims: input.claims.map(decodeAckStatusClaim),
403
+ };
404
+ }
405
+
406
+ export function decodeAckStatusResultEnvelope(
407
+ value: unknown,
408
+ ): ProtocolEnvelope<AckStatusResult> {
409
+ return decodeProtocolEnvelope(value, decodeAckStatusResult);
410
+ }
411
+
312
412
  function decodeClaimRecord(value: unknown): ClaimRecord {
313
413
  const input = object(value);
314
414
  exact(
@@ -1,4 +1,4 @@
1
- /** Current Borg coordination protocol generation. Clean-slate v10. */
2
- export const PROTOCOL_VERSION = '10' as const;
1
+ /** Current Borg coordination protocol generation. Clean-slate v11. */
2
+ export const PROTOCOL_VERSION = '11' as const;
3
3
 
4
4
  export type ProtocolVersion = typeof PROTOCOL_VERSION;
package/src/templates.ts CHANGED
@@ -308,6 +308,7 @@ Scope contract:
308
308
  Activation:
309
309
  - Order named drones to start exact authorized work with START NOW, RESUME NOW, REVIEW NOW, or HOLD; name the exact item and first concrete action.
310
310
  - ACK and claim are receipt only; neither means work has started or a review is complete.
311
+ - When receipt is uncertain, use \`borg_ack-status\` for the routed entry; it reports acknowledgements and claims without advancing unread cursors.
311
312
  - Verify activation and progress against the concrete milestones from the dispatch and acceptance evidence.
312
313
  - When a milestone is missing and status is uncertain, follow the ownership and liveness discipline. Do not interrupt slow local work merely to satisfy a reporting cadence.
313
314
  - Require BLOCKED when safe work stops, naming the missing input while independent work continues.