borgmcp 5.4.1 → 5.6.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.
Files changed (101) hide show
  1. package/README.md +15 -0
  2. package/dist/assimilate-cmd.d.ts +8 -1
  3. package/dist/assimilate-cmd.d.ts.map +1 -1
  4. package/dist/assimilate-cmd.js +54 -21
  5. package/dist/assimilate-cmd.js.map +1 -1
  6. package/dist/claude.d.ts.map +1 -1
  7. package/dist/claude.js +30 -0
  8. package/dist/claude.js.map +1 -1
  9. package/dist/cli-help.d.ts +1 -0
  10. package/dist/cli-help.d.ts.map +1 -1
  11. package/dist/cli-help.js +45 -0
  12. package/dist/cli-help.js.map +1 -1
  13. package/dist/docs-sections.d.ts.map +1 -1
  14. package/dist/docs-sections.js +8 -0
  15. package/dist/docs-sections.js.map +1 -1
  16. package/dist/local-server-cursor.d.ts +10 -1
  17. package/dist/local-server-cursor.d.ts.map +1 -1
  18. package/dist/local-server-cursor.js +64 -5
  19. package/dist/local-server-cursor.js.map +1 -1
  20. package/dist/log-stream.d.ts +13 -0
  21. package/dist/log-stream.d.ts.map +1 -1
  22. package/dist/log-stream.js +45 -13
  23. package/dist/log-stream.js.map +1 -1
  24. package/dist/remote-client.d.ts +17 -0
  25. package/dist/remote-client.d.ts.map +1 -1
  26. package/dist/remote-client.js +37 -15
  27. package/dist/remote-client.js.map +1 -1
  28. package/dist/representative-cmd.d.ts +94 -0
  29. package/dist/representative-cmd.d.ts.map +1 -0
  30. package/dist/representative-cmd.js +290 -0
  31. package/dist/representative-cmd.js.map +1 -0
  32. package/dist/representative-core.d.ts +243 -0
  33. package/dist/representative-core.d.ts.map +1 -0
  34. package/dist/representative-core.js +679 -0
  35. package/dist/representative-core.js.map +1 -0
  36. package/dist/representative-delivery-store.d.ts +79 -0
  37. package/dist/representative-delivery-store.d.ts.map +1 -0
  38. package/dist/representative-delivery-store.js +233 -0
  39. package/dist/representative-delivery-store.js.map +1 -0
  40. package/dist/representative-listener-store.d.ts +46 -0
  41. package/dist/representative-listener-store.d.ts.map +1 -0
  42. package/dist/representative-listener-store.js +119 -0
  43. package/dist/representative-listener-store.js.map +1 -0
  44. package/dist/representative-listener.d.ts +32 -0
  45. package/dist/representative-listener.d.ts.map +1 -0
  46. package/dist/representative-listener.js +293 -0
  47. package/dist/representative-listener.js.map +1 -0
  48. package/dist/representative-mcp.d.ts +37 -0
  49. package/dist/representative-mcp.d.ts.map +1 -0
  50. package/dist/representative-mcp.js +211 -0
  51. package/dist/representative-mcp.js.map +1 -0
  52. package/dist/representative-owner.d.ts +10 -0
  53. package/dist/representative-owner.d.ts.map +1 -0
  54. package/dist/representative-owner.js +107 -0
  55. package/dist/representative-owner.js.map +1 -0
  56. package/dist/representative-store.d.ts +68 -0
  57. package/dist/representative-store.d.ts.map +1 -0
  58. package/dist/representative-store.js +173 -0
  59. package/dist/representative-store.js.map +1 -0
  60. package/dist/seat-probe.d.ts +1 -0
  61. package/dist/seat-probe.d.ts.map +1 -1
  62. package/dist/seat-probe.js +1 -1
  63. package/dist/seat-probe.js.map +1 -1
  64. package/dist/seat-store.d.ts +13 -0
  65. package/dist/seat-store.d.ts.map +1 -1
  66. package/dist/seat-store.js +55 -10
  67. package/dist/seat-store.js.map +1 -1
  68. package/dist/server-trust.d.ts +10 -0
  69. package/dist/server-trust.d.ts.map +1 -1
  70. package/dist/server-trust.js +23 -6
  71. package/dist/server-trust.js.map +1 -1
  72. package/dist/stream-owner.d.ts +10 -0
  73. package/dist/stream-owner.d.ts.map +1 -1
  74. package/dist/stream-owner.js +129 -21
  75. package/dist/stream-owner.js.map +1 -1
  76. package/dist/unknown-subcommand.d.ts +1 -1
  77. package/dist/unknown-subcommand.d.ts.map +1 -1
  78. package/dist/unknown-subcommand.js +1 -0
  79. package/dist/unknown-subcommand.js.map +1 -1
  80. package/docs/HUMAN_REPRESENTATIVE.md +434 -0
  81. package/package.json +1 -1
  82. package/src/assimilate-cmd.ts +73 -22
  83. package/src/claude.ts +30 -0
  84. package/src/cli-help.ts +48 -0
  85. package/src/docs-sections.ts +8 -0
  86. package/src/local-server-cursor.ts +56 -4
  87. package/src/log-stream.ts +47 -14
  88. package/src/remote-client.ts +54 -13
  89. package/src/representative-cmd.ts +369 -0
  90. package/src/representative-core.ts +908 -0
  91. package/src/representative-delivery-store.ts +235 -0
  92. package/src/representative-listener-store.ts +115 -0
  93. package/src/representative-listener.ts +215 -0
  94. package/src/representative-mcp.ts +250 -0
  95. package/src/representative-owner.ts +105 -0
  96. package/src/representative-store.ts +224 -0
  97. package/src/seat-probe.ts +1 -1
  98. package/src/seat-store.ts +61 -10
  99. package/src/server-trust.ts +25 -6
  100. package/src/stream-owner.ts +129 -20
  101. package/src/unknown-subcommand.ts +1 -0
@@ -0,0 +1,679 @@
1
+ /**
2
+ * Human representative core ("Hermes"): a SEPARATE, non-human-seat drone that
3
+ * relays the human's requests, questions and decisions to ONE explicitly bound
4
+ * Coordinator drone and reads that Coordinator's replies.
5
+ *
6
+ * It is not the Coordinator and carries none of its playbook: it cannot address
7
+ * workers, broadcast, or manage the cube. Every message is attributed as an
8
+ * automated relay and states whether the content is user-authorized or the
9
+ * model's own advice. Borg does not verify that claim — it is delegate
10
+ * messaging, not server-enforced per-message human authority.
11
+ *
12
+ * The backend is injected so the same logic runs against the real seat-scoped
13
+ * client (`createSeatBackend`) or a controlled test double.
14
+ */
15
+ import { createHash, randomUUID } from 'node:crypto';
16
+ import { Buffer } from 'node:buffer';
17
+ import { ErrorCode, ProtocolContractError, decodeAppendLogRequest } from 'borgmcp-shared/protocol';
18
+ import { CUBE_DELETED_CODE, CubeDeletedError, DRONE_EVICTED_CODE, DroneEvictedError } from './drone-lifecycle.js';
19
+ import { BorgProtocolMismatchError, BorgServerError, BorgServerHttpError, BorgServerTrustError, BorgServerUnreachableError, } from './server-errors.js';
20
+ import { bindingFingerprint, representativeRecoveryCommand, isRepresentativeUuid } from './representative-store.js';
21
+ import { comparePoints, createDeliveryStore } from './representative-delivery-store.js';
22
+ import { readPrivateLocalServerCursor } from './local-server-cursor.js';
23
+ import { validatePrivateDirectory } from './representative-listener-store.js';
24
+ import { borgConfigRoot } from './private-root.js';
25
+ const UUID_SCAN_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi;
26
+ export const REPRESENTATIVE_MESSAGE_LIMIT_BYTES = 3000;
27
+ export const REPRESENTATIVE_DELIVERY_NOTE = 'read returns undelivered replies without consuming them; persist, route by in_reply_to (unknown: hold for the ' +
28
+ 'human), then deliver through the last persisted entry_id. ack only notifies the Coordinator. A separate ' +
29
+ 'borg representative listen process emits body-free wake hints. One process at a time owns send/read/deliver/ack; ' +
30
+ 'status is read-only. Stop routing if binding_fingerprint changes.';
31
+ export class RepresentativeError extends Error {
32
+ code;
33
+ details;
34
+ constructor(code, message, details) {
35
+ super(message);
36
+ this.code = code;
37
+ this.details = details;
38
+ this.name = 'RepresentativeError';
39
+ }
40
+ }
41
+ /** Real backend: the existing seat-scoped client calls for one hydrated seat. */
42
+ export async function createSeatBackend(active) {
43
+ const client = await import('./remote-client.js');
44
+ const trust = active.serverTrustIdentity;
45
+ return {
46
+ whoami: () => client.whoami(active),
47
+ roster: () => client.getRoster(active),
48
+ append: ({ postId, message, to }) => client.appendLog(active.sessionToken, active.apiUrl, message, {
49
+ to, postId, transportRetry: false, serverTrustIdentity: trust,
50
+ }),
51
+ readAfter: (cursor, limit, continuationGuard) => client.readLog(active.sessionToken, active.apiUrl, { cursor, limit, serverTrustIdentity: trust, continuationGuard }),
52
+ // Migration input only: an unsafe private root or cursor file is no cursor,
53
+ // so the checkpoint starts empty and replays instead of trusting it.
54
+ unreadCursor: async () => {
55
+ try {
56
+ if (!await validatePrivateDirectory(borgConfigRoot(), false))
57
+ return null;
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ return readPrivateLocalServerCursor({
63
+ origin: active.apiUrl, trustIdentity: trust, cubeId: active.cubeId, droneId: active.droneId,
64
+ });
65
+ },
66
+ readEntry: (entryId) => client.readLogEntry(active.sessionToken, active.apiUrl, { entry_id: entryId }, trust),
67
+ ack: (entryId) => client.ackLogEntry(active.sessionToken, active.apiUrl, entryId, 'ack', trust),
68
+ };
69
+ }
70
+ export function assertRepresentativeRole(role, drone) {
71
+ if (role.is_human_seat === true || role.role_class === 'queen' || drone?.is_queen_class === true) {
72
+ throw new RepresentativeError('REPRESENTATIVE_ROLE_NOT_PERMITTED', `Role ${JSON.stringify(role.name)} is a human-seat or coordinating role. The human representative must hold its own ` +
73
+ 'separate non-human-seat worker role and never occupies or replaces the Coordinator.');
74
+ }
75
+ }
76
+ /**
77
+ * Resolve the ONE Coordinator drone the operator named. Exact label match only;
78
+ * a missing, duplicated, non-human-seat or self target fails — no fallback.
79
+ */
80
+ export function resolveCoordinator(roster, selector) {
81
+ const matches = roster.drones.filter((drone) => drone.label === selector.label);
82
+ if (matches.length === 0) {
83
+ throw new RepresentativeError('COORDINATOR_NOT_FOUND', `No active drone labelled ${JSON.stringify(selector.label)} exists in this cube (missing or evicted). No other drone was selected.`);
84
+ }
85
+ if (matches.length > 1) {
86
+ throw new RepresentativeError('COORDINATOR_AMBIGUOUS', `${matches.length} active drones are labelled ${JSON.stringify(selector.label)}; refusing to choose between them.`);
87
+ }
88
+ const drone = matches[0];
89
+ if (drone.id === selector.selfDroneId) {
90
+ throw new RepresentativeError('COORDINATOR_IS_SELF', 'The representative drone cannot be its own Coordinator.');
91
+ }
92
+ const role = roster.roles.find((candidate) => candidate.id === drone.role_id);
93
+ if (!role || role.is_human_seat !== true) {
94
+ throw new RepresentativeError('COORDINATOR_NOT_HUMAN_SEAT', `Drone ${JSON.stringify(selector.label)} does not hold the cube's human-seat (Coordinator) role. ` +
95
+ 'The representative talks only to the Coordinator, never directly to workers.');
96
+ }
97
+ return { drone, role };
98
+ }
99
+ /** Re-prove, against the live cube, that this seat and the bound Coordinator are still the bound ones. */
100
+ export async function verifyLiveBinding(ctx) {
101
+ const { binding, backend } = ctx;
102
+ const me = await backend.whoami();
103
+ if (me.cube_id !== binding.cubeId || me.drone_id !== binding.representativeDroneId) {
104
+ throw new RepresentativeError('BINDING_MISMATCH', `The live connection is not the bound cube/drone. Run \`${representativeRecoveryCommand(binding)}\` explicitly; nothing was sent.`);
105
+ }
106
+ const roster = await backend.roster();
107
+ const self = roster.drones.find((drone) => drone.id === binding.representativeDroneId);
108
+ const selfRole = roster.roles.find((role) => role.id === self?.role_id);
109
+ if (!self || !selfRole) {
110
+ throw new RepresentativeError('SEAT_UNAVAILABLE', 'The representative drone is no longer active in the cube.');
111
+ }
112
+ assertRepresentativeRole(selfRole, self);
113
+ const coordinator = roster.drones.find((drone) => drone.id === binding.coordinatorDroneId);
114
+ const coordinatorRole = roster.roles.find((role) => role.id === coordinator?.role_id);
115
+ if (!coordinator || coordinatorRole?.is_human_seat !== true) {
116
+ throw new RepresentativeError('COORDINATOR_UNAVAILABLE', `The bound Coordinator ${JSON.stringify(binding.coordinatorLabel)} is no longer an active human-seat drone in this cube ` +
117
+ `(evicted, released or reassigned). Restore that Coordinator before running \`${representativeRecoveryCommand(binding)}\`, or explicitly select a replacement Coordinator; no other drone was selected.`);
118
+ }
119
+ return { coordinator, self };
120
+ }
121
+ const SEND_FIELDS = new Set(['request_id', 'kind', 'authorization', 'message']);
122
+ function validateSendInput(raw) {
123
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
124
+ throw new RepresentativeError(ErrorCode.INVALID_INPUT, 'Arguments must be an object.');
125
+ }
126
+ const input = raw;
127
+ const unknown = Object.keys(input).filter((key) => !SEND_FIELDS.has(key) && input[key] !== undefined);
128
+ if (unknown.length > 0) {
129
+ throw new RepresentativeError(ErrorCode.INVALID_INPUT, `Unsupported field(s): ${unknown.join(', ')}. The recipient is fixed to the bound Coordinator; ` +
130
+ 'this connection cannot address workers, broadcast, or classify messages.');
131
+ }
132
+ if (input.kind !== 'request' && input.kind !== 'question' && input.kind !== 'decision') {
133
+ throw new RepresentativeError(ErrorCode.INVALID_INPUT, 'kind must be "request", "question" or "decision".');
134
+ }
135
+ if (input.authorization !== 'user_authorized' && input.authorization !== 'model_advice') {
136
+ throw new RepresentativeError(ErrorCode.INVALID_INPUT, 'authorization must be "user_authorized" or "model_advice".');
137
+ }
138
+ if (typeof input.message !== 'string' || input.message.trim() === '') {
139
+ throw new RepresentativeError(ErrorCode.INVALID_INPUT, 'message must be a non-empty string.');
140
+ }
141
+ if (Buffer.byteLength(input.message, 'utf8') > REPRESENTATIVE_MESSAGE_LIMIT_BYTES) {
142
+ throw new RepresentativeError(ErrorCode.INVALID_INPUT, `message exceeds ${REPRESENTATIVE_MESSAGE_LIMIT_BYTES} bytes.`);
143
+ }
144
+ if (input.request_id !== undefined && input.request_id !== null &&
145
+ (typeof input.request_id !== 'string' || !isRepresentativeUuid(input.request_id))) {
146
+ throw new RepresentativeError(ErrorCode.INVALID_INPUT, 'request_id must be a UUID previously returned by this tool, or omitted.');
147
+ }
148
+ if (input.kind === 'decision' && input.authorization !== 'user_authorized') {
149
+ throw new RepresentativeError('DECISION_REQUIRES_USER_AUTHORIZATION', 'A decision can only be relayed when the human explicitly authorized it. Send model suggestions as a question or request marked model_advice.');
150
+ }
151
+ return {
152
+ kind: input.kind,
153
+ authorization: input.authorization,
154
+ message: input.message,
155
+ ...(typeof input.request_id === 'string' ? { request_id: input.request_id.toLowerCase() } : {}),
156
+ };
157
+ }
158
+ const AUTHORIZATION_LINES = {
159
+ user_authorized: 'authorization: user-authorized — the representative asserts the human explicitly authorized the text below. ' +
160
+ 'This is not verified by Borg and authorizes nothing beyond that text.',
161
+ model_advice: 'authorization: model-advice — the representative model\'s own suggestion. NOT a human decision or approval.',
162
+ };
163
+ export function formatRepresentativeMessage(binding, requestId, input) {
164
+ return [
165
+ `[HUMAN-REPRESENTATIVE · automated relay via ${binding.representativeLabel} · not typed by the human]`,
166
+ `request_id: ${requestId}`,
167
+ `kind: ${input.kind}`,
168
+ AUTHORIZATION_LINES[input.authorization],
169
+ `reply: direct to ${binding.representativeLabel}, quoting the request_id.`,
170
+ '---',
171
+ input.message,
172
+ ].join('\n');
173
+ }
174
+ function payloadDigest(binding, input) {
175
+ return createHash('sha256')
176
+ .update([binding.cubeId, binding.coordinatorDroneId, input.kind, input.authorization, input.message].join('\0'))
177
+ .digest('hex');
178
+ }
179
+ function boundedMessage(error) {
180
+ const text = error instanceof Error ? error.message : String(error);
181
+ return text.replace(/[\u0000-\u001f\u007f-\u009f]+/g, ' ').trim().slice(0, 300);
182
+ }
183
+ const SAME_ID_RETRY = 'Nothing was re-sent automatically. Retry ONLY by calling borg_representative-send again with the same request_id and ' +
184
+ 'identical content: the server deduplicates on that id (protocol post_id), so that retry cannot create a second ' +
185
+ 'message. Never re-send this content under a different request_id while it is unresolved.';
186
+ /**
187
+ * Classify one failed append. "Definite" is claimed ONLY for a typed refusal
188
+ * the server returned to the single transport attempt the backend contract
189
+ * allows — that attempt was answered instead of stored. Everything else
190
+ * (no answer, 5xx, an unreadable or contract-violating response, a trust
191
+ * failure of unknown timing, anything unrecognised) may follow a committed
192
+ * write and stays ambiguous, but keeps its cause and a matching recovery.
193
+ */
194
+ function classifyAppendFailure(error, binding) {
195
+ const message = boundedMessage(error);
196
+ const prepareRecovery = `The operator must restore this connection: run \`${representativeRecoveryCommand(binding)}\`, then restart the MCP process.`;
197
+ if (error instanceof BorgServerError &&
198
+ (error.code === ErrorCode.SESSION_REJECTED || error.code === ErrorCode.SESSION_REVOKED || error.code === 'CREDENTIAL_REJECTED')) {
199
+ return { definite: true, cause: { code: error.code, message }, recovery: prepareRecovery };
200
+ }
201
+ if (error instanceof DroneEvictedError || error instanceof CubeDeletedError) {
202
+ return {
203
+ definite: true,
204
+ cause: { code: error instanceof DroneEvictedError ? DRONE_EVICTED_CODE : CUBE_DELETED_CODE, message },
205
+ recovery: prepareRecovery,
206
+ };
207
+ }
208
+ if (error instanceof BorgServerHttpError) {
209
+ const code = typeof error.code === 'string' && /^[A-Z0-9_]{1,64}$/.test(error.code) ? error.code : `HTTP_${error.status}`;
210
+ if (error.status >= 400 && error.status < 500) {
211
+ const recovery = error.status === 429
212
+ ? 'The server is rate limiting this drone. Wait, then retry with the same request_id and identical content.'
213
+ : error.status === 409
214
+ ? 'The server already holds a DIFFERENT message under this request_id. Do not resend; ask the operator to inspect the cube log.'
215
+ : error.status === 401 || error.status === 403
216
+ ? `The server denied this drone. ${prepareRecovery}`
217
+ : 'The server refused this content. Correct it and send it as a new request, or ask the operator.';
218
+ return { definite: true, cause: { code, message }, recovery };
219
+ }
220
+ return {
221
+ definite: false,
222
+ cause: { code, message },
223
+ recovery: 'The server reported an internal error and may or may not have stored the message. Check the server, then retry.',
224
+ };
225
+ }
226
+ if (error instanceof BorgServerUnreachableError) {
227
+ return {
228
+ definite: false,
229
+ cause: { code: 'SERVER_UNREACHABLE', message },
230
+ recovery: 'The server did not answer in time; the request may still have arrived. Check that the Borg server is running, then retry.',
231
+ };
232
+ }
233
+ if (error instanceof BorgProtocolMismatchError || error instanceof ProtocolContractError) {
234
+ return {
235
+ definite: false,
236
+ cause: { code: error instanceof BorgProtocolMismatchError ? 'PROTOCOL_MISMATCH' : 'PROTOCOL_CONTRACT', message },
237
+ recovery: 'The server\'s response could not be read, which can happen AFTER it stored the message. The operator should ' +
238
+ 'check that client and server versions match (`borg update`), then retry.',
239
+ };
240
+ }
241
+ if (error instanceof BorgServerTrustError) {
242
+ return {
243
+ definite: false,
244
+ cause: { code: 'SERVER_TRUST', message },
245
+ recovery: 'The server\'s pinned TLS identity could not be confirmed. The operator must verify the server before any retry.',
246
+ };
247
+ }
248
+ return {
249
+ definite: false,
250
+ cause: { code: 'UNKNOWN', message },
251
+ recovery: 'An unrecognised failure interrupted the send. Check `borg representative status`, then retry.',
252
+ };
253
+ }
254
+ export async function sendRepresentativeMessage(ctx, raw) {
255
+ return { ...await sendOnce(ctx, raw), binding_fingerprint: bindingFingerprint(ctx.binding) };
256
+ }
257
+ async function sendOnce(ctx, raw) {
258
+ const input = validateSendInput(raw);
259
+ const { binding, store } = ctx;
260
+ const now = () => (ctx.now?.() ?? new Date()).toISOString();
261
+ const digest = payloadDigest(binding, input);
262
+ const coordinatorRef = { drone_id: binding.coordinatorDroneId, label: binding.coordinatorLabel };
263
+ // Everything predictable is decided locally BEFORE anything is reserved, so
264
+ // invalid input can never leave a record behind.
265
+ const candidateId = input.request_id ?? randomUUID();
266
+ const message = formatRepresentativeMessage(binding, candidateId, input);
267
+ try {
268
+ decodeAppendLogRequest({ post_id: candidateId, message, to: [binding.coordinatorDroneId] });
269
+ }
270
+ catch (error) {
271
+ throw new RepresentativeError(ErrorCode.INVALID_INPUT, `The message cannot be sent as written: ${boundedMessage(error)}`);
272
+ }
273
+ // ONE locked transaction: look up, decide conflicts, allocate the id and
274
+ // reserve it as 'pending'. No await separates the check from the reservation,
275
+ // so overlapping calls cannot both conclude "nothing is in flight".
276
+ const reservation = await store.transactRequests(binding.worktree, (records) => {
277
+ const record = input.request_id
278
+ ? records.find((candidate) => candidate.requestId === input.request_id)
279
+ : undefined;
280
+ if (record && record.payloadDigest !== digest) {
281
+ throw new RepresentativeError('REQUEST_ID_CONFLICT', 'This request_id was already used for different content. Use a new request for new content.');
282
+ }
283
+ if (record?.state === 'sent')
284
+ return { kind: 'already-sent', entryId: record.entryId };
285
+ if (!record || record.state === 'rejected') {
286
+ const unresolved = records.find((candidate) => candidate.payloadDigest === digest &&
287
+ (candidate.state === 'ambiguous' || candidate.state === 'pending') &&
288
+ candidate.requestId !== candidateId);
289
+ if (unresolved) {
290
+ throw new RepresentativeError('AMBIGUOUS_SEND_UNRESOLVED', `Identical content already has an in-flight or unresolved send under request_id ${unresolved.requestId}. ` +
291
+ 'Wait for that call, then retry with that request_id instead of creating a new one.');
292
+ }
293
+ }
294
+ const stamp = now();
295
+ if (record) {
296
+ const prior = record.state;
297
+ // An earlier attempt under this id may have reached the log; remember it
298
+ // so a later definite refusal cannot mark the request as never stored.
299
+ if (prior === 'pending' || prior === 'ambiguous')
300
+ record.maybeStored = true;
301
+ record.state = 'pending';
302
+ record.updatedAt = stamp;
303
+ return { kind: 'reserved', prior };
304
+ }
305
+ records.push({
306
+ requestId: candidateId, payloadDigest: digest, kind: input.kind, authorization: input.authorization,
307
+ state: 'pending', createdAt: stamp, updatedAt: stamp,
308
+ });
309
+ return { kind: 'reserved', prior: null };
310
+ });
311
+ const requestId = candidateId;
312
+ if (reservation.kind === 'already-sent') {
313
+ return {
314
+ outcome: 'sent', request_id: requestId, entry_id: reservation.entryId,
315
+ duplicate: true, deduplicated: true, coordinator: coordinatorRef,
316
+ };
317
+ }
318
+ // Settling is monotonic so overlapping same-id calls converge: 'sent' is
319
+ // terminal, and a definite refusal never hides an attempt that may be stored.
320
+ const settle = (state, entryId) => store.transactRequests(binding.worktree, (records) => {
321
+ const stamp = now();
322
+ let record = records.find((candidate) => candidate.requestId === requestId);
323
+ if (!record) {
324
+ record = {
325
+ requestId, payloadDigest: digest, kind: input.kind, authorization: input.authorization,
326
+ state, createdAt: stamp, updatedAt: stamp,
327
+ };
328
+ records.push(record);
329
+ }
330
+ if (record.state === 'sent')
331
+ return record.state;
332
+ if (state === 'ambiguous')
333
+ record.maybeStored = true;
334
+ record.state = state === 'rejected' && record.maybeStored ? 'ambiguous' : state;
335
+ record.updatedAt = stamp;
336
+ if (state === 'sent') {
337
+ record.entryId = entryId;
338
+ delete record.maybeStored;
339
+ }
340
+ return record.state;
341
+ });
342
+ // Live verification happens before any posting. Nothing was sent if it fails,
343
+ // so this call's reservation is released (an older unresolved state is kept).
344
+ // Only a genuinely sole, never-posted reservation may be released: once the
345
+ // record is marked maybeStored, another attempt under this id has reserved it
346
+ // (and may be appending right now), so it stays unresolved and keeps blocking
347
+ // identical content under a new id until a same-id attempt settles it.
348
+ try {
349
+ await verifyLiveBinding(ctx);
350
+ }
351
+ catch (error) {
352
+ await store.transactRequests(binding.worktree, (records) => {
353
+ const index = records.findIndex((candidate) => candidate.requestId === requestId);
354
+ if (index < 0 || records[index].state !== 'pending')
355
+ return;
356
+ if (records[index].maybeStored) {
357
+ // Never deleted, never marked settled: at most back to its earlier unresolved 'ambiguous'.
358
+ if (reservation.prior === 'ambiguous')
359
+ records[index].state = 'ambiguous';
360
+ return;
361
+ }
362
+ if (reservation.prior === null)
363
+ records.splice(index, 1);
364
+ else
365
+ records[index].state = reservation.prior;
366
+ });
367
+ throw error;
368
+ }
369
+ let result;
370
+ try {
371
+ result = await ctx.backend.append({ postId: requestId, message, to: [binding.coordinatorDroneId] });
372
+ }
373
+ catch (error) {
374
+ const failure = classifyAppendFailure(error, binding);
375
+ if (failure.definite) {
376
+ const recorded = await settle('rejected');
377
+ throw new RepresentativeError('SEND_REJECTED', `The Borg server refused this attempt (${failure.cause.code}); it was not stored by this attempt. ${failure.recovery}` +
378
+ (recorded === 'ambiguous'
379
+ ? ' An EARLIER attempt under this request_id is still unresolved, so the request stays listed as unresolved.'
380
+ : ''), { request_id: requestId, cause_code: failure.cause.code, cause_message: failure.cause.message, recovery: failure.recovery });
381
+ }
382
+ await settle('ambiguous');
383
+ return {
384
+ outcome: 'ambiguous',
385
+ request_id: requestId,
386
+ coordinator: coordinatorRef,
387
+ cause: failure.cause,
388
+ guidance: `The outcome is unknown (${failure.cause.code}). ${failure.recovery} ${SAME_ID_RETRY}`,
389
+ };
390
+ }
391
+ await settle('sent', result.entry.id);
392
+ const recipients = result.entry.recipient_drone_ids ?? [];
393
+ const warnings = [];
394
+ if (result.entry.visibility !== 'direct' || recipients.length !== 1 || recipients[0] !== binding.coordinatorDroneId) {
395
+ warnings.push('The server\'s routing echo does not show exactly the bound Coordinator as the direct recipient.');
396
+ }
397
+ if (result.unreachableRecipients?.some((recipient) => recipient.id === binding.coordinatorDroneId)) {
398
+ warnings.push('The server reports the Coordinator as currently unreachable; it will see the entry when it next reads its log.');
399
+ }
400
+ return {
401
+ outcome: 'sent',
402
+ request_id: requestId,
403
+ entry_id: result.entry.id,
404
+ deduplicated: result.deduplicated,
405
+ duplicate: false,
406
+ coordinator: coordinatorRef,
407
+ ...(warnings.length > 0 ? { warning: warnings.join(' ') } : {}),
408
+ };
409
+ }
410
+ function isAddressedCoordinatorEntry(binding, entry) {
411
+ if (entry.drone_id !== binding.coordinatorDroneId)
412
+ return null;
413
+ if (entry.visibility === 'direct') {
414
+ return (entry.recipient_drone_ids ?? []).includes(binding.representativeDroneId) ? 'direct' : null;
415
+ }
416
+ return 'broadcast';
417
+ }
418
+ /** The exact text an MCP tool result carries; `max_bytes` measures this. */
419
+ export function serializeRepresentativeResult(body) {
420
+ return JSON.stringify(body, null, 2);
421
+ }
422
+ const READ_SCAN_PAGE = 500;
423
+ /**
424
+ * A serialized read result never exceeds max(max_bytes, this). An oversize
425
+ * entry is returned alone; if it still exceeds the bound, its citations are
426
+ * reduced to ids. Message text is never cut: the server caps a post (4096
427
+ * bytes by default), so the reduced entry fits.
428
+ */
429
+ export const REPRESENTATIVE_ENVELOPE_FLOOR = 16384;
430
+ /**
431
+ * Run `use` with this generation's delivery state. On the first call for a
432
+ * generation, `use` runs alone in the per-seat queue with the proposed start,
433
+ * and nothing is written until it calls `commit`: a read refused as oversize
434
+ * leaves no tombstone and no checkpoint behind.
435
+ */
436
+ async function withDeliveryState(ctx, use) {
437
+ const store = createDeliveryStore(ctx.binding);
438
+ const saved = await store.load();
439
+ if (saved) {
440
+ // A checkpoint always implies an upgraded seat; restore a lost tombstone.
441
+ if (!await store.migrated())
442
+ await store.markMigrated(ctx.guard);
443
+ return use(saved, async () => { });
444
+ }
445
+ return store.initialize(async () => {
446
+ const again = await store.load();
447
+ if (again)
448
+ return use(again, async () => { });
449
+ // The seat already upgraded (a tombstone or a sibling generation exists):
450
+ // this generation (rebind, new Coordinator, or a removed invalid checkpoint)
451
+ // starts empty and replays its addressed history. Nothing on disk is ever
452
+ // read back as a position, and the legacy cursor is never imported again.
453
+ const upgraded = await store.migrated() || await store.otherGenerationExists();
454
+ // One-time upgrade: start where the pre-checkpoint destructive read left
455
+ // the unread view. The cursor is used only in memory.
456
+ const cursor = upgraded ? null : await ctx.backend.unreadCursor();
457
+ return use({ checkpoint: cursor, readThrough: cursor, returned: [] }, async () => {
458
+ // The tombstone is created exclusively first, so an interruption before
459
+ // the checkpoint replays (duplicates the host dedupes). An initializer in
460
+ // another process that won the create (excluded by the tools lease in
461
+ // practice) makes this one start empty.
462
+ const start = !upgraded && await store.markMigrated(ctx.guard) ? cursor : null;
463
+ if (upgraded)
464
+ await store.markMigrated(ctx.guard);
465
+ await store.advance({ checkpoint: start, readThrough: start }, ctx.guard);
466
+ });
467
+ });
468
+ }
469
+ const checkpointView = (point) => ({ entry_id: point?.id ?? null, created_at: point?.created_at ?? null });
470
+ export async function readRepresentativeReplies(ctx, raw) {
471
+ const input = (raw ?? {});
472
+ const allowed = ['include_broadcast', 'limit', 'max_bytes'];
473
+ const unknown = Object.keys(input).filter((key) => !allowed.includes(key) && input[key] !== undefined);
474
+ if (unknown.length > 0)
475
+ throw new RepresentativeError(ErrorCode.INVALID_INPUT, `Unsupported field(s): ${unknown.join(', ')}.`);
476
+ if (input.include_broadcast !== undefined && typeof input.include_broadcast !== 'boolean') {
477
+ throw new RepresentativeError(ErrorCode.INVALID_INPUT, 'include_broadcast must be a boolean.');
478
+ }
479
+ const bounded = (name, min, max, fallback) => {
480
+ const value = input[name];
481
+ if (value === undefined)
482
+ return fallback;
483
+ if (!Number.isInteger(value) || value < min || value > max) {
484
+ throw new RepresentativeError(ErrorCode.INVALID_INPUT, `${name} must be an integer from ${min} to ${max}.`);
485
+ }
486
+ return value;
487
+ };
488
+ const limit = bounded('limit', 1, 50, 10);
489
+ const maxBytes = bounded('max_bytes', 4096, 60000, 32768);
490
+ await verifyLiveBinding(ctx);
491
+ return withDeliveryState(ctx, async (state, commit) => {
492
+ const requestIds = await ctx.store.transactRequests(ctx.binding.worktree, (records) => new Set(records.map((record) => record.requestId)));
493
+ // Deliberate ceiling: every read scans the cube log from the checkpoint,
494
+ // including entries not addressed here, so an undelivered backlog costs a
495
+ // growing scan. Upgrade path: a separate scan hint that deliver advances.
496
+ // One addressed entry beyond `limit` is collected only to answer has_more.
497
+ const candidates = [];
498
+ let ignored = 0;
499
+ let cursor = state.checkpoint;
500
+ let last = state.checkpoint;
501
+ scan: for (;;) {
502
+ const page = await ctx.backend.readAfter(cursor, READ_SCAN_PAGE);
503
+ for (const entry of page.entries) {
504
+ const point = { id: entry.id, created_at: entry.created_at };
505
+ // Client-side (created_at, id) filter: never repeat or regress.
506
+ if (comparePoints(point, last) <= 0)
507
+ continue;
508
+ last = point;
509
+ const addressed = isAddressedCoordinatorEntry(ctx.binding, entry);
510
+ if (addressed === null || (addressed === 'broadcast' && input.include_broadcast !== true)) {
511
+ ignored += 1;
512
+ continue;
513
+ }
514
+ const quoted = (entry.message.match(UUID_SCAN_RE) ?? []).map((id) => id.toLowerCase());
515
+ candidates.push({ point, ignoredBefore: ignored, reply: {
516
+ entry_id: entry.id,
517
+ created_at: entry.created_at,
518
+ from_drone_id: ctx.binding.coordinatorDroneId,
519
+ from_label: ctx.binding.coordinatorLabel,
520
+ addressed,
521
+ in_reply_to: quoted.find((id) => requestIds.has(id)) ?? null,
522
+ message: entry.message,
523
+ ...(entry.documents?.length ? {
524
+ documents: entry.documents,
525
+ document_delivery: 'Document bodies are not included and cannot be fetched through this connection. Ask the Coordinator to provide the content through a supported channel.',
526
+ } : {}),
527
+ } });
528
+ if (candidates.length > limit)
529
+ break scan;
530
+ }
531
+ const tail = page.entries.at(-1);
532
+ if (!page.has_more || !tail)
533
+ break;
534
+ cursor = { id: tail.id, created_at: tail.created_at };
535
+ }
536
+ const result = (count, oversize = false) => {
537
+ const taken = candidates.slice(0, count);
538
+ const replies = taken.map(({ reply }, index) => (oversize && index === 0 ? { ...reply, oversize: true } : reply));
539
+ return {
540
+ replies,
541
+ checkpoint: checkpointView(state.checkpoint),
542
+ has_more: candidates.length > count,
543
+ ignored_entries: count < candidates.length ? candidates[count].ignoredBefore : ignored,
544
+ binding_fingerprint: bindingFingerprint(ctx.binding),
545
+ delivery: REPRESENTATIVE_DELIVERY_NOTE,
546
+ };
547
+ };
548
+ // Measure the real serialized result; entries are whole or omitted.
549
+ let count = 0;
550
+ while (count < Math.min(limit, candidates.length) &&
551
+ Buffer.byteLength(serializeRepresentativeResult(result(count + 1))) <= maxBytes)
552
+ count += 1;
553
+ const oversize = count === 0 && candidates.length > 0;
554
+ let final = result(oversize ? 1 : count, oversize);
555
+ const bound = Math.max(maxBytes, REPRESENTATIVE_ENVELOPE_FLOOR);
556
+ if (oversize && final.replies[0].documents?.length &&
557
+ Buffer.byteLength(serializeRepresentativeResult(final)) > bound) {
558
+ const reply = final.replies[0];
559
+ final = { ...final, replies: [{
560
+ ...reply, documents: reply.documents.map(({ id }) => ({ id })), documents_reduced: true,
561
+ }] };
562
+ }
563
+ const measured = Buffer.byteLength(serializeRepresentativeResult(final));
564
+ if (oversize && measured > bound) {
565
+ // Nothing is cut or overrun, and nothing moves: no content, no deliver.
566
+ throw new RepresentativeError('REPRESENTATIVE_READ_OVERSIZE', `The next reply does not fit ${bound} bytes even alone with its citations reduced to ids. Nothing was read or ` +
567
+ 'advanced. Raise max_bytes (up to 60000); beyond that the operator must reduce the server post limit.', { entry_id: final.replies[0].entry_id, measured_bytes: measured, bound });
568
+ }
569
+ await commit();
570
+ const window = candidates.slice(0, final.replies.length).map(({ point }) => point);
571
+ if (window.length > 0) {
572
+ // The deliver fence and membership widen before the caller sees the entries.
573
+ await createDeliveryStore(ctx.binding).advance({ readThrough: window.at(-1), returned: window }, ctx.guard);
574
+ }
575
+ return final;
576
+ });
577
+ }
578
+ export async function deliverRepresentativeReplies(ctx, raw) {
579
+ const input = (raw ?? {});
580
+ const unknown = Object.keys(input).filter((key) => key !== 'through');
581
+ if (unknown.length > 0 || !isRepresentativeUuid(input.through)) {
582
+ throw new RepresentativeError(ErrorCode.INVALID_INPUT, 'through must be the full UUID of a reply returned by borg_representative-read.');
583
+ }
584
+ const through = input.through;
585
+ await verifyLiveBinding(ctx);
586
+ return withDeliveryState(ctx, async (state, commit) => {
587
+ await commit();
588
+ const outside = () => new RepresentativeError('REPRESENTATIVE_DELIVER_UNKNOWN_ENTRY', 'That entry is not in the current read window: deliver only a reply that borg_representative-read returned. Nothing changed.');
589
+ let entry;
590
+ try {
591
+ ({ entry } = await ctx.backend.readEntry(through));
592
+ }
593
+ catch (error) {
594
+ if (error?.status === 404)
595
+ throw outside();
596
+ throw error;
597
+ }
598
+ if (entry.id !== through || isAddressedCoordinatorEntry(ctx.binding, entry) === null)
599
+ throw outside();
600
+ const point = { id: entry.id, created_at: entry.created_at };
601
+ const fingerprint = bindingFingerprint(ctx.binding);
602
+ if (state.checkpoint && comparePoints(point, state.checkpoint) <= 0) {
603
+ return { checkpoint: checkpointView(state.checkpoint), advanced: false, binding_fingerprint: fingerprint };
604
+ }
605
+ // Membership of the full tuple, not the range or the id alone: only an entry
606
+ // a read actually returned, as the server reports it now, inside the window.
607
+ if (!state.returned.some((returned) => returned.id === point.id && returned.created_at === point.created_at) ||
608
+ state.readThrough === null || comparePoints(point, state.readThrough) > 0)
609
+ throw outside();
610
+ const { before, after } = await createDeliveryStore(ctx.binding).advance({ checkpoint: point }, ctx.guard);
611
+ return {
612
+ checkpoint: checkpointView(after.checkpoint),
613
+ // The serialized transition, not this call's earlier snapshot.
614
+ advanced: comparePoints(after.checkpoint, before?.checkpoint ?? null) > 0,
615
+ binding_fingerprint: fingerprint,
616
+ };
617
+ });
618
+ }
619
+ export async function ackRepresentativeReply(ctx, raw) {
620
+ const input = (raw ?? {});
621
+ const unknown = Object.keys(input).filter((key) => key !== 'entry_id');
622
+ if (unknown.length > 0 || typeof input.entry_id !== 'string' || !isRepresentativeUuid(input.entry_id)) {
623
+ throw new RepresentativeError(ErrorCode.INVALID_INPUT, 'entry_id must be the full UUID of a reply returned by borg_representative-read.');
624
+ }
625
+ await verifyLiveBinding(ctx);
626
+ const { entry } = await ctx.backend.readEntry(input.entry_id);
627
+ if (entry.id !== input.entry_id || isAddressedCoordinatorEntry(ctx.binding, entry) !== 'direct') {
628
+ throw new RepresentativeError('NOT_A_COORDINATOR_REPLY', 'Only a direct reply from the bound Coordinator to this representative can be acknowledged here.');
629
+ }
630
+ await ctx.backend.ack(entry.id);
631
+ return { acknowledged: entry.id };
632
+ }
633
+ export async function representativeStatus(ctx) {
634
+ const { binding } = ctx;
635
+ let problem;
636
+ try {
637
+ await verifyLiveBinding(ctx);
638
+ }
639
+ catch (error) {
640
+ problem = {
641
+ code: error instanceof RepresentativeError ? error.code : 'BACKEND_ERROR',
642
+ message: error instanceof Error ? error.message : 'Unknown error',
643
+ };
644
+ }
645
+ let checkpointProblem;
646
+ try {
647
+ await createDeliveryStore(binding).load();
648
+ }
649
+ catch (error) {
650
+ checkpointProblem = { code: error.code ?? 'BACKEND_ERROR', message: error instanceof Error ? error.message : 'Unknown error' };
651
+ }
652
+ const unresolved = (await ctx.store.readRequests(binding.worktree))
653
+ .filter((record) => record.state === 'pending' || record.state === 'ambiguous')
654
+ .map((record) => ({
655
+ request_id: record.requestId, state: record.state, kind: record.kind, updated_at: record.updatedAt,
656
+ }));
657
+ return {
658
+ role: 'Human representative — an automated delegate speaking for the human. It is not the human and not the Coordinator.',
659
+ connected: problem === undefined,
660
+ ...(problem ? { problem } : {}),
661
+ cube: { id: binding.cubeId, name: binding.cubeName },
662
+ repository_origin: binding.repositoryOrigin ?? null,
663
+ worktree: binding.worktree,
664
+ representative: {
665
+ drone_id: binding.representativeDroneId, label: binding.representativeLabel, role: binding.representativeRoleName,
666
+ },
667
+ coordinator: {
668
+ drone_id: binding.coordinatorDroneId, label: binding.coordinatorLabel, role: binding.coordinatorRoleName,
669
+ },
670
+ unresolved_requests: unresolved,
671
+ delivery: REPRESENTATIVE_DELIVERY_NOTE,
672
+ authority: 'Borg records these messages as posts from the representative drone. The user-authorized / model-advice label is this ' +
673
+ 'connection\'s own attribution and is not verified or enforced by the Borg server.',
674
+ binding_fingerprint: bindingFingerprint(binding),
675
+ ...(checkpointProblem ? { checkpoint_problem: checkpointProblem } : {}),
676
+ envelope_floor: REPRESENTATIVE_ENVELOPE_FLOOR,
677
+ };
678
+ }
679
+ //# sourceMappingURL=representative-core.js.map