borgmcp 5.4.0 → 5.5.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 (72) hide show
  1. package/README.md +12 -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 +22 -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 +42 -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 +1 -1
  17. package/dist/local-server-cursor.d.ts.map +1 -1
  18. package/dist/local-server-cursor.js +14 -4
  19. package/dist/local-server-cursor.js.map +1 -1
  20. package/dist/remote-client.d.ts +14 -0
  21. package/dist/remote-client.d.ts.map +1 -1
  22. package/dist/remote-client.js +30 -14
  23. package/dist/remote-client.js.map +1 -1
  24. package/dist/representative-cmd.d.ts +87 -0
  25. package/dist/representative-cmd.d.ts.map +1 -0
  26. package/dist/representative-cmd.js +286 -0
  27. package/dist/representative-cmd.js.map +1 -0
  28. package/dist/representative-core.d.ts +197 -0
  29. package/dist/representative-core.d.ts.map +1 -0
  30. package/dist/representative-core.js +493 -0
  31. package/dist/representative-core.js.map +1 -0
  32. package/dist/representative-mcp.d.ts +30 -0
  33. package/dist/representative-mcp.d.ts.map +1 -0
  34. package/dist/representative-mcp.js +182 -0
  35. package/dist/representative-mcp.js.map +1 -0
  36. package/dist/representative-owner.d.ts +10 -0
  37. package/dist/representative-owner.d.ts.map +1 -0
  38. package/dist/representative-owner.js +107 -0
  39. package/dist/representative-owner.js.map +1 -0
  40. package/dist/representative-store.d.ts +61 -0
  41. package/dist/representative-store.d.ts.map +1 -0
  42. package/dist/representative-store.js +158 -0
  43. package/dist/representative-store.js.map +1 -0
  44. package/dist/seat-store.d.ts +13 -0
  45. package/dist/seat-store.d.ts.map +1 -1
  46. package/dist/seat-store.js +55 -10
  47. package/dist/seat-store.js.map +1 -1
  48. package/dist/stream-owner.d.ts +10 -0
  49. package/dist/stream-owner.d.ts.map +1 -1
  50. package/dist/stream-owner.js +98 -19
  51. package/dist/stream-owner.js.map +1 -1
  52. package/dist/unknown-subcommand.d.ts +1 -1
  53. package/dist/unknown-subcommand.d.ts.map +1 -1
  54. package/dist/unknown-subcommand.js +1 -0
  55. package/dist/unknown-subcommand.js.map +1 -1
  56. package/docs/HUMAN_REPRESENTATIVE.md +269 -0
  57. package/docs/RELEASING.md +2 -2
  58. package/package.json +2 -2
  59. package/src/assimilate-cmd.ts +73 -22
  60. package/src/claude.ts +22 -0
  61. package/src/cli-help.ts +45 -0
  62. package/src/docs-sections.ts +8 -0
  63. package/src/local-server-cursor.ts +11 -3
  64. package/src/remote-client.ts +45 -12
  65. package/src/representative-cmd.ts +363 -0
  66. package/src/representative-core.ts +699 -0
  67. package/src/representative-mcp.ts +209 -0
  68. package/src/representative-owner.ts +105 -0
  69. package/src/representative-store.ts +208 -0
  70. package/src/seat-store.ts +61 -10
  71. package/src/stream-owner.ts +96 -19
  72. package/src/unknown-subcommand.ts +1 -0
@@ -0,0 +1,699 @@
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
+
16
+ import { createHash, randomUUID } from 'node:crypto';
17
+ import { Buffer } from 'node:buffer';
18
+ import { ErrorCode, ProtocolContractError, decodeAppendLogRequest, type Role, type RosterDrone as ProtocolDrone, type EnrichedStreamEntry, type DocumentCitation } from 'borgmcp-shared/protocol';
19
+ import type { ActiveCube } from './cubes.js';
20
+ import { CUBE_DELETED_CODE, CubeDeletedError, DRONE_EVICTED_CODE, DroneEvictedError } from './drone-lifecycle.js';
21
+ import {
22
+ BorgProtocolMismatchError,
23
+ BorgServerError,
24
+ BorgServerHttpError,
25
+ BorgServerTrustError,
26
+ BorgServerUnreachableError,
27
+ } from './server-errors.js';
28
+ import { representativeRecoveryCommand, isRepresentativeUuid, type RepresentativeBinding, type RepresentativeStore } from './representative-store.js';
29
+
30
+ 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;
31
+ export const REPRESENTATIVE_MESSAGE_LIMIT_BYTES = 3000;
32
+
33
+ export const REPRESENTATIVE_DELIVERY_NOTE =
34
+ 'Explicit send/read round trips only: this connection has no background wake or push delivery. ' +
35
+ 'Coordinator replies are seen only when borg_representative-read is called. Each read consumes the unread view of ' +
36
+ 'everything it fetched. The host must persist each read result before relaying it, map request_id to its conversation, ' +
37
+ 'route replies by in_reply_to, and hold replies with an unknown or missing request_id for the human. An already-read ' +
38
+ 'reply cannot be retrieved through this connection. The first send/read/ack takes an exclusive process lease for this ' +
39
+ 'representative drone; other processes refuse those calls without ledger, cursor or network activity. Status stays ' +
40
+ 'read-only and reports ownership. After owner exit, death or lease expiry another process can take over. ' +
41
+ 'A process that loses its lease refuses further calls until restarted. Ownership does not route conversations inside the host. ' +
42
+ 'A local lease cannot cancel an in-flight request; retry an ambiguous send with its original request_id.';
43
+
44
+ export type RepresentativeErrorCode =
45
+ | typeof ErrorCode.INVALID_INPUT
46
+ | 'DECISION_REQUIRES_USER_AUTHORIZATION'
47
+ | 'REQUEST_ID_CONFLICT'
48
+ | 'AMBIGUOUS_SEND_UNRESOLVED'
49
+ | 'SEND_REJECTED'
50
+ | 'REPRESENTATIVE_OWNERSHIP_REQUIRED'
51
+ | 'NOT_PREPARED'
52
+ | 'SEAT_UNAVAILABLE'
53
+ | 'BINDING_MISMATCH'
54
+ | 'BINDING_CONFLICT'
55
+ | 'COORDINATOR_NOT_FOUND'
56
+ | 'COORDINATOR_AMBIGUOUS'
57
+ | 'COORDINATOR_NOT_HUMAN_SEAT'
58
+ | 'COORDINATOR_IS_SELF'
59
+ | 'COORDINATOR_UNAVAILABLE'
60
+ | 'REPRESENTATIVE_ROLE_NOT_PERMITTED'
61
+ | 'REPRESENTATIVE_ROLE_MISMATCH'
62
+ | 'NOT_A_COORDINATOR_REPLY';
63
+
64
+ export interface RepresentativeErrorDetails {
65
+ owner?: import('./stream-owner.js').StreamOwnershipSnapshot;
66
+ request_id?: string;
67
+ cause_code?: string;
68
+ cause_message?: string;
69
+ recovery?: string;
70
+ }
71
+
72
+ export class RepresentativeError extends Error {
73
+ constructor(
74
+ readonly code: RepresentativeErrorCode,
75
+ message: string,
76
+ readonly details?: RepresentativeErrorDetails,
77
+ ) {
78
+ super(message);
79
+ this.name = 'RepresentativeError';
80
+ }
81
+ }
82
+
83
+ type RosterRole = Pick<Role, 'id' | 'name' | 'is_human_seat' | 'role_class'>;
84
+ type RosterDrone = Pick<ProtocolDrone, 'id' | 'label' | 'role_id' | 'is_queen_class'>;
85
+ type LogEntry = Pick<EnrichedStreamEntry, 'id' | 'drone_id' | 'message' | 'visibility' | 'created_at' | 'recipient_drone_ids' | 'documents'>;
86
+
87
+ /** The only Borg operations the representative may perform, all seat-scoped. */
88
+ export interface RepresentativeBackend {
89
+ whoami(): Promise<{ cube_id: string; cube_name: string; drone_id: string; drone_label: string; role_id: string; role_name: string }>;
90
+ roster(): Promise<{ drones: RosterDrone[]; roles: RosterRole[] }>;
91
+ /**
92
+ * MUST make a single transport attempt and surface the client's typed errors
93
+ * unchanged: a typed 4xx/401/410 refusal is reported as "not stored" only
94
+ * because no hidden retry can sit between a committed write and that answer.
95
+ */
96
+ append(input: { postId: string; message: string; to: string[] }): Promise<{
97
+ entry: LogEntry;
98
+ deduplicated: boolean;
99
+ unreachableRecipients?: Array<{ id: string; label: string }>;
100
+ }>;
101
+ /**
102
+ * Drains THIS seat's own unread cursor only (no other drone's cursor exists
103
+ * here) — the WHOLE returned page, including entries the caller then filters
104
+ * out. `limit` is a page-size hint: the client's digest mode may return more.
105
+ */
106
+ readUnread(limit?: number, continuationGuard?: () => Promise<void>): Promise<{ entries: LogEntry[]; has_more?: boolean }>;
107
+ readEntry(entryId: string): Promise<{ entry: LogEntry }>;
108
+ ack(entryId: string): Promise<void>;
109
+ }
110
+
111
+ export interface RepresentativeContext {
112
+ binding: RepresentativeBinding;
113
+ backend: RepresentativeBackend;
114
+ store: RepresentativeStore;
115
+ now?: () => Date;
116
+ }
117
+
118
+ /** Real backend: the existing seat-scoped client calls for one hydrated seat. */
119
+ export async function createSeatBackend(active: ActiveCube): Promise<RepresentativeBackend> {
120
+ const client = await import('./remote-client.js');
121
+ const trust = active.serverTrustIdentity;
122
+ return {
123
+ whoami: () => client.whoami(active),
124
+ roster: () => client.getRoster(active),
125
+ append: ({ postId, message, to }) =>
126
+ client.appendLog(active.sessionToken, active.apiUrl, message, {
127
+ to, postId, transportRetry: false, serverTrustIdentity: trust,
128
+ }),
129
+ readUnread: (limit, continuationGuard) =>
130
+ client.readLog(active.sessionToken, active.apiUrl, { unreadOnly: true, limit, serverTrustIdentity: trust, continuationGuard }),
131
+ readEntry: (entryId) =>
132
+ client.readLogEntry(active.sessionToken, active.apiUrl, { entry_id: entryId }, trust),
133
+ ack: (entryId) => client.ackLogEntry(active.sessionToken, active.apiUrl, entryId, 'ack', trust),
134
+ };
135
+ }
136
+
137
+ export function assertRepresentativeRole(
138
+ role: { name: string; is_human_seat?: boolean; role_class?: string },
139
+ drone?: { is_queen_class?: boolean },
140
+ ): void {
141
+ if (role.is_human_seat === true || role.role_class === 'queen' || drone?.is_queen_class === true) {
142
+ throw new RepresentativeError(
143
+ 'REPRESENTATIVE_ROLE_NOT_PERMITTED',
144
+ `Role ${JSON.stringify(role.name)} is a human-seat or coordinating role. The human representative must hold its own ` +
145
+ 'separate non-human-seat worker role and never occupies or replaces the Coordinator.',
146
+ );
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Resolve the ONE Coordinator drone the operator named. Exact label match only;
152
+ * a missing, duplicated, non-human-seat or self target fails — no fallback.
153
+ */
154
+ export function resolveCoordinator(
155
+ roster: { drones: RosterDrone[]; roles: RosterRole[] },
156
+ selector: { label: string; selfDroneId: string },
157
+ ): { drone: RosterDrone; role: RosterRole } {
158
+ const matches = roster.drones.filter((drone) => drone.label === selector.label);
159
+ if (matches.length === 0) {
160
+ throw new RepresentativeError(
161
+ 'COORDINATOR_NOT_FOUND',
162
+ `No active drone labelled ${JSON.stringify(selector.label)} exists in this cube (missing or evicted). No other drone was selected.`,
163
+ );
164
+ }
165
+ if (matches.length > 1) {
166
+ throw new RepresentativeError(
167
+ 'COORDINATOR_AMBIGUOUS',
168
+ `${matches.length} active drones are labelled ${JSON.stringify(selector.label)}; refusing to choose between them.`,
169
+ );
170
+ }
171
+ const drone = matches[0];
172
+ if (drone.id === selector.selfDroneId) {
173
+ throw new RepresentativeError('COORDINATOR_IS_SELF', 'The representative drone cannot be its own Coordinator.');
174
+ }
175
+ const role = roster.roles.find((candidate) => candidate.id === drone.role_id);
176
+ if (!role || role.is_human_seat !== true) {
177
+ throw new RepresentativeError(
178
+ 'COORDINATOR_NOT_HUMAN_SEAT',
179
+ `Drone ${JSON.stringify(selector.label)} does not hold the cube's human-seat (Coordinator) role. ` +
180
+ 'The representative talks only to the Coordinator, never directly to workers.',
181
+ );
182
+ }
183
+ return { drone, role };
184
+ }
185
+
186
+ /** Re-prove, against the live cube, that this seat and the bound Coordinator are still the bound ones. */
187
+ async function verifyLiveBinding(ctx: RepresentativeContext): Promise<{ coordinator: RosterDrone; self: RosterDrone }> {
188
+ const { binding, backend } = ctx;
189
+ const me = await backend.whoami();
190
+ if (me.cube_id !== binding.cubeId || me.drone_id !== binding.representativeDroneId) {
191
+ throw new RepresentativeError(
192
+ 'BINDING_MISMATCH',
193
+ `The live connection is not the bound cube/drone. Run \`${representativeRecoveryCommand(binding)}\` explicitly; nothing was sent.`,
194
+ );
195
+ }
196
+ const roster = await backend.roster();
197
+ const self = roster.drones.find((drone) => drone.id === binding.representativeDroneId);
198
+ const selfRole = roster.roles.find((role) => role.id === self?.role_id);
199
+ if (!self || !selfRole) {
200
+ throw new RepresentativeError('SEAT_UNAVAILABLE', 'The representative drone is no longer active in the cube.');
201
+ }
202
+ assertRepresentativeRole(selfRole, self);
203
+ const coordinator = roster.drones.find((drone) => drone.id === binding.coordinatorDroneId);
204
+ const coordinatorRole = roster.roles.find((role) => role.id === coordinator?.role_id);
205
+ if (!coordinator || coordinatorRole?.is_human_seat !== true) {
206
+ throw new RepresentativeError(
207
+ 'COORDINATOR_UNAVAILABLE',
208
+ `The bound Coordinator ${JSON.stringify(binding.coordinatorLabel)} is no longer an active human-seat drone in this cube ` +
209
+ `(evicted, released or reassigned). Restore that Coordinator before running \`${representativeRecoveryCommand(binding)}\`, or explicitly select a replacement Coordinator; no other drone was selected.`,
210
+ );
211
+ }
212
+ return { coordinator, self };
213
+ }
214
+
215
+ export type RepresentativeKind = 'request' | 'question' | 'decision';
216
+ export type RepresentativeAuthorization = 'user_authorized' | 'model_advice';
217
+
218
+ export interface RepresentativeSendInput {
219
+ request_id?: string;
220
+ kind: RepresentativeKind;
221
+ authorization: RepresentativeAuthorization;
222
+ message: string;
223
+ }
224
+
225
+ export interface RepresentativeSendResult {
226
+ outcome: 'sent' | 'ambiguous';
227
+ request_id: string;
228
+ entry_id?: string;
229
+ /** True when the server recognised the post id and stored nothing new. */
230
+ deduplicated?: boolean;
231
+ /** True when the local ledger already held this exact sent request; no network call was made. */
232
+ duplicate?: boolean;
233
+ coordinator: { drone_id: string; label: string };
234
+ warning?: string;
235
+ /** Ambiguous only: the sanitized underlying failure. */
236
+ cause?: SendFailureCause;
237
+ guidance?: string;
238
+ }
239
+
240
+ const SEND_FIELDS = new Set(['request_id', 'kind', 'authorization', 'message']);
241
+
242
+ function validateSendInput(raw: unknown): RepresentativeSendInput {
243
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
244
+ throw new RepresentativeError(ErrorCode.INVALID_INPUT, 'Arguments must be an object.');
245
+ }
246
+ const input = raw as Record<string, unknown>;
247
+ const unknown = Object.keys(input).filter((key) => !SEND_FIELDS.has(key) && input[key] !== undefined);
248
+ if (unknown.length > 0) {
249
+ throw new RepresentativeError(
250
+ ErrorCode.INVALID_INPUT,
251
+ `Unsupported field(s): ${unknown.join(', ')}. The recipient is fixed to the bound Coordinator; ` +
252
+ 'this connection cannot address workers, broadcast, or classify messages.',
253
+ );
254
+ }
255
+ if (input.kind !== 'request' && input.kind !== 'question' && input.kind !== 'decision') {
256
+ throw new RepresentativeError(ErrorCode.INVALID_INPUT, 'kind must be "request", "question" or "decision".');
257
+ }
258
+ if (input.authorization !== 'user_authorized' && input.authorization !== 'model_advice') {
259
+ throw new RepresentativeError(ErrorCode.INVALID_INPUT, 'authorization must be "user_authorized" or "model_advice".');
260
+ }
261
+ if (typeof input.message !== 'string' || input.message.trim() === '') {
262
+ throw new RepresentativeError(ErrorCode.INVALID_INPUT, 'message must be a non-empty string.');
263
+ }
264
+ if (Buffer.byteLength(input.message, 'utf8') > REPRESENTATIVE_MESSAGE_LIMIT_BYTES) {
265
+ throw new RepresentativeError(ErrorCode.INVALID_INPUT, `message exceeds ${REPRESENTATIVE_MESSAGE_LIMIT_BYTES} bytes.`);
266
+ }
267
+ if (input.request_id !== undefined && input.request_id !== null &&
268
+ (typeof input.request_id !== 'string' || !isRepresentativeUuid(input.request_id))) {
269
+ throw new RepresentativeError(ErrorCode.INVALID_INPUT, 'request_id must be a UUID previously returned by this tool, or omitted.');
270
+ }
271
+ if (input.kind === 'decision' && input.authorization !== 'user_authorized') {
272
+ throw new RepresentativeError(
273
+ 'DECISION_REQUIRES_USER_AUTHORIZATION',
274
+ 'A decision can only be relayed when the human explicitly authorized it. Send model suggestions as a question or request marked model_advice.',
275
+ );
276
+ }
277
+ return {
278
+ kind: input.kind,
279
+ authorization: input.authorization,
280
+ message: input.message,
281
+ ...(typeof input.request_id === 'string' ? { request_id: input.request_id.toLowerCase() } : {}),
282
+ };
283
+ }
284
+
285
+ const AUTHORIZATION_LINES: Record<RepresentativeAuthorization, string> = {
286
+ user_authorized:
287
+ 'authorization: user-authorized — the representative asserts the human explicitly authorized the text below. ' +
288
+ 'This is not verified by Borg and authorizes nothing beyond that text.',
289
+ model_advice:
290
+ 'authorization: model-advice — the representative model\'s own suggestion. NOT a human decision or approval.',
291
+ };
292
+
293
+ export function formatRepresentativeMessage(
294
+ binding: RepresentativeBinding,
295
+ requestId: string,
296
+ input: RepresentativeSendInput,
297
+ ): string {
298
+ return [
299
+ `[HUMAN-REPRESENTATIVE · automated relay via ${binding.representativeLabel} · not typed by the human]`,
300
+ `request_id: ${requestId}`,
301
+ `kind: ${input.kind}`,
302
+ AUTHORIZATION_LINES[input.authorization],
303
+ `reply: direct to ${binding.representativeLabel}, quoting the request_id.`,
304
+ '---',
305
+ input.message,
306
+ ].join('\n');
307
+ }
308
+
309
+ function payloadDigest(binding: RepresentativeBinding, input: RepresentativeSendInput): string {
310
+ return createHash('sha256')
311
+ .update([binding.cubeId, binding.coordinatorDroneId, input.kind, input.authorization, input.message].join('\0'))
312
+ .digest('hex');
313
+ }
314
+
315
+ export interface SendFailureCause {
316
+ /** Stable, bounded cause code — a typed server/protocol code where one exists. */
317
+ code: string;
318
+ /** Sanitized, bounded underlying message. Never a credential: client errors carry none. */
319
+ message: string;
320
+ }
321
+
322
+ function boundedMessage(error: unknown): string {
323
+ const text = error instanceof Error ? error.message : String(error);
324
+ return text.replace(/[\u0000-\u001f\u007f-\u009f]+/g, ' ').trim().slice(0, 300);
325
+ }
326
+
327
+ const SAME_ID_RETRY =
328
+ 'Nothing was re-sent automatically. Retry ONLY by calling borg_representative-send again with the same request_id and ' +
329
+ 'identical content: the server deduplicates on that id (protocol post_id), so that retry cannot create a second ' +
330
+ 'message. Never re-send this content under a different request_id while it is unresolved.';
331
+
332
+ /**
333
+ * Classify one failed append. "Definite" is claimed ONLY for a typed refusal
334
+ * the server returned to the single transport attempt the backend contract
335
+ * allows — that attempt was answered instead of stored. Everything else
336
+ * (no answer, 5xx, an unreadable or contract-violating response, a trust
337
+ * failure of unknown timing, anything unrecognised) may follow a committed
338
+ * write and stays ambiguous, but keeps its cause and a matching recovery.
339
+ */
340
+ function classifyAppendFailure(error: unknown, binding: RepresentativeBinding): { definite: boolean; cause: SendFailureCause; recovery: string } {
341
+ const message = boundedMessage(error);
342
+ const prepareRecovery = `The operator must restore this connection: run \`${representativeRecoveryCommand(binding)}\`, then restart the MCP process.`;
343
+ if (error instanceof BorgServerError &&
344
+ (error.code === ErrorCode.SESSION_REJECTED || error.code === ErrorCode.SESSION_REVOKED || error.code === 'CREDENTIAL_REJECTED')) {
345
+ return { definite: true, cause: { code: error.code, message }, recovery: prepareRecovery };
346
+ }
347
+ if (error instanceof DroneEvictedError || error instanceof CubeDeletedError) {
348
+ return {
349
+ definite: true,
350
+ cause: { code: error instanceof DroneEvictedError ? DRONE_EVICTED_CODE : CUBE_DELETED_CODE, message },
351
+ recovery: prepareRecovery,
352
+ };
353
+ }
354
+ if (error instanceof BorgServerHttpError) {
355
+ const code = typeof error.code === 'string' && /^[A-Z0-9_]{1,64}$/.test(error.code) ? error.code : `HTTP_${error.status}`;
356
+ if (error.status >= 400 && error.status < 500) {
357
+ const recovery = error.status === 429
358
+ ? 'The server is rate limiting this drone. Wait, then retry with the same request_id and identical content.'
359
+ : error.status === 409
360
+ ? 'The server already holds a DIFFERENT message under this request_id. Do not resend; ask the operator to inspect the cube log.'
361
+ : error.status === 401 || error.status === 403
362
+ ? `The server denied this drone. ${prepareRecovery}`
363
+ : 'The server refused this content. Correct it and send it as a new request, or ask the operator.';
364
+ return { definite: true, cause: { code, message }, recovery };
365
+ }
366
+ return {
367
+ definite: false,
368
+ cause: { code, message },
369
+ recovery: 'The server reported an internal error and may or may not have stored the message. Check the server, then retry.',
370
+ };
371
+ }
372
+ if (error instanceof BorgServerUnreachableError) {
373
+ return {
374
+ definite: false,
375
+ cause: { code: 'SERVER_UNREACHABLE', message },
376
+ recovery: 'The server did not answer in time; the request may still have arrived. Check that the Borg server is running, then retry.',
377
+ };
378
+ }
379
+ if (error instanceof BorgProtocolMismatchError || error instanceof ProtocolContractError) {
380
+ return {
381
+ definite: false,
382
+ cause: { code: error instanceof BorgProtocolMismatchError ? 'PROTOCOL_MISMATCH' : 'PROTOCOL_CONTRACT', message },
383
+ recovery:
384
+ 'The server\'s response could not be read, which can happen AFTER it stored the message. The operator should ' +
385
+ 'check that client and server versions match (`borg update`), then retry.',
386
+ };
387
+ }
388
+ if (error instanceof BorgServerTrustError) {
389
+ return {
390
+ definite: false,
391
+ cause: { code: 'SERVER_TRUST', message },
392
+ recovery: 'The server\'s pinned TLS identity could not be confirmed. The operator must verify the server before any retry.',
393
+ };
394
+ }
395
+ return {
396
+ definite: false,
397
+ cause: { code: 'UNKNOWN', message },
398
+ recovery: 'An unrecognised failure interrupted the send. Check `borg representative status`, then retry.',
399
+ };
400
+ }
401
+
402
+ export async function sendRepresentativeMessage(
403
+ ctx: RepresentativeContext,
404
+ raw: unknown,
405
+ ): Promise<RepresentativeSendResult> {
406
+ const input = validateSendInput(raw);
407
+ const { binding, store } = ctx;
408
+ const now = () => (ctx.now?.() ?? new Date()).toISOString();
409
+ const digest = payloadDigest(binding, input);
410
+ const coordinatorRef = { drone_id: binding.coordinatorDroneId, label: binding.coordinatorLabel };
411
+
412
+ // Everything predictable is decided locally BEFORE anything is reserved, so
413
+ // invalid input can never leave a record behind.
414
+ const candidateId = input.request_id ?? randomUUID();
415
+ const message = formatRepresentativeMessage(binding, candidateId, input);
416
+ try {
417
+ decodeAppendLogRequest({ post_id: candidateId, message, to: [binding.coordinatorDroneId] });
418
+ } catch (error) {
419
+ throw new RepresentativeError(ErrorCode.INVALID_INPUT, `The message cannot be sent as written: ${boundedMessage(error)}`);
420
+ }
421
+
422
+ // ONE locked transaction: look up, decide conflicts, allocate the id and
423
+ // reserve it as 'pending'. No await separates the check from the reservation,
424
+ // so overlapping calls cannot both conclude "nothing is in flight".
425
+ const reservation = await store.transactRequests(binding.worktree, (records) => {
426
+ const record = input.request_id
427
+ ? records.find((candidate) => candidate.requestId === input.request_id)
428
+ : undefined;
429
+ if (record && record.payloadDigest !== digest) {
430
+ throw new RepresentativeError(
431
+ 'REQUEST_ID_CONFLICT',
432
+ 'This request_id was already used for different content. Use a new request for new content.',
433
+ );
434
+ }
435
+ if (record?.state === 'sent') return { kind: 'already-sent' as const, entryId: record.entryId };
436
+ if (!record || record.state === 'rejected') {
437
+ const unresolved = records.find((candidate) =>
438
+ candidate.payloadDigest === digest &&
439
+ (candidate.state === 'ambiguous' || candidate.state === 'pending') &&
440
+ candidate.requestId !== candidateId);
441
+ if (unresolved) {
442
+ throw new RepresentativeError(
443
+ 'AMBIGUOUS_SEND_UNRESOLVED',
444
+ `Identical content already has an in-flight or unresolved send under request_id ${unresolved.requestId}. ` +
445
+ 'Wait for that call, then retry with that request_id instead of creating a new one.',
446
+ );
447
+ }
448
+ }
449
+ const stamp = now();
450
+ if (record) {
451
+ const prior = record.state;
452
+ // An earlier attempt under this id may have reached the log; remember it
453
+ // so a later definite refusal cannot mark the request as never stored.
454
+ if (prior === 'pending' || prior === 'ambiguous') record.maybeStored = true;
455
+ record.state = 'pending';
456
+ record.updatedAt = stamp;
457
+ return { kind: 'reserved' as const, prior };
458
+ }
459
+ records.push({
460
+ requestId: candidateId, payloadDigest: digest, kind: input.kind, authorization: input.authorization,
461
+ state: 'pending', createdAt: stamp, updatedAt: stamp,
462
+ });
463
+ return { kind: 'reserved' as const, prior: null };
464
+ });
465
+ const requestId = candidateId;
466
+ if (reservation.kind === 'already-sent') {
467
+ return {
468
+ outcome: 'sent', request_id: requestId, entry_id: reservation.entryId,
469
+ duplicate: true, deduplicated: true, coordinator: coordinatorRef,
470
+ };
471
+ }
472
+
473
+ // Settling is monotonic so overlapping same-id calls converge: 'sent' is
474
+ // terminal, and a definite refusal never hides an attempt that may be stored.
475
+ const settle = (state: 'sent' | 'ambiguous' | 'rejected', entryId?: string) =>
476
+ store.transactRequests(binding.worktree, (records) => {
477
+ const stamp = now();
478
+ let record = records.find((candidate) => candidate.requestId === requestId);
479
+ if (!record) {
480
+ record = {
481
+ requestId, payloadDigest: digest, kind: input.kind, authorization: input.authorization,
482
+ state, createdAt: stamp, updatedAt: stamp,
483
+ };
484
+ records.push(record);
485
+ }
486
+ if (record.state === 'sent') return record.state;
487
+ if (state === 'ambiguous') record.maybeStored = true;
488
+ record.state = state === 'rejected' && record.maybeStored ? 'ambiguous' : state;
489
+ record.updatedAt = stamp;
490
+ if (state === 'sent') {
491
+ record.entryId = entryId;
492
+ delete record.maybeStored;
493
+ }
494
+ return record.state;
495
+ });
496
+
497
+ // Live verification happens before any posting. Nothing was sent if it fails,
498
+ // so this call's reservation is released (an older unresolved state is kept).
499
+ // Only a genuinely sole, never-posted reservation may be released: once the
500
+ // record is marked maybeStored, another attempt under this id has reserved it
501
+ // (and may be appending right now), so it stays unresolved and keeps blocking
502
+ // identical content under a new id until a same-id attempt settles it.
503
+ try {
504
+ await verifyLiveBinding(ctx);
505
+ } catch (error) {
506
+ await store.transactRequests(binding.worktree, (records) => {
507
+ const index = records.findIndex((candidate) => candidate.requestId === requestId);
508
+ if (index < 0 || records[index].state !== 'pending') return;
509
+ if (records[index].maybeStored) {
510
+ // Never deleted, never marked settled: at most back to its earlier unresolved 'ambiguous'.
511
+ if (reservation.prior === 'ambiguous') records[index].state = 'ambiguous';
512
+ return;
513
+ }
514
+ if (reservation.prior === null) records.splice(index, 1);
515
+ else records[index].state = reservation.prior;
516
+ });
517
+ throw error;
518
+ }
519
+
520
+ let result: Awaited<ReturnType<RepresentativeBackend['append']>>;
521
+ try {
522
+ result = await ctx.backend.append({ postId: requestId, message, to: [binding.coordinatorDroneId] });
523
+ } catch (error) {
524
+ const failure = classifyAppendFailure(error, binding);
525
+ if (failure.definite) {
526
+ const recorded = await settle('rejected');
527
+ throw new RepresentativeError(
528
+ 'SEND_REJECTED',
529
+ `The Borg server refused this attempt (${failure.cause.code}); it was not stored by this attempt. ${failure.recovery}` +
530
+ (recorded === 'ambiguous'
531
+ ? ' An EARLIER attempt under this request_id is still unresolved, so the request stays listed as unresolved.'
532
+ : ''),
533
+ { request_id: requestId, cause_code: failure.cause.code, cause_message: failure.cause.message, recovery: failure.recovery },
534
+ );
535
+ }
536
+ await settle('ambiguous');
537
+ return {
538
+ outcome: 'ambiguous',
539
+ request_id: requestId,
540
+ coordinator: coordinatorRef,
541
+ cause: failure.cause,
542
+ guidance: `The outcome is unknown (${failure.cause.code}). ${failure.recovery} ${SAME_ID_RETRY}`,
543
+ };
544
+ }
545
+
546
+ await settle('sent', result.entry.id);
547
+ const recipients = result.entry.recipient_drone_ids ?? [];
548
+ const warnings: string[] = [];
549
+ if (result.entry.visibility !== 'direct' || recipients.length !== 1 || recipients[0] !== binding.coordinatorDroneId) {
550
+ warnings.push('The server\'s routing echo does not show exactly the bound Coordinator as the direct recipient.');
551
+ }
552
+ if (result.unreachableRecipients?.some((recipient) => recipient.id === binding.coordinatorDroneId)) {
553
+ warnings.push('The server reports the Coordinator as currently unreachable; it will see the entry when it next reads its log.');
554
+ }
555
+ return {
556
+ outcome: 'sent',
557
+ request_id: requestId,
558
+ entry_id: result.entry.id,
559
+ deduplicated: result.deduplicated,
560
+ duplicate: false,
561
+ coordinator: coordinatorRef,
562
+ ...(warnings.length > 0 ? { warning: warnings.join(' ') } : {}),
563
+ };
564
+ }
565
+
566
+ export interface RepresentativeReply {
567
+ documents?: DocumentCitation[];
568
+ document_delivery?: string;
569
+ entry_id: string;
570
+ created_at: string;
571
+ from_drone_id: string;
572
+ from_label: string;
573
+ addressed: 'direct' | 'broadcast';
574
+ /** A ledger request id quoted in the reply text. Textual correlation only. */
575
+ in_reply_to: string | null;
576
+ message: string;
577
+ }
578
+
579
+ function isAddressedCoordinatorEntry(binding: RepresentativeBinding, entry: LogEntry): 'direct' | 'broadcast' | null {
580
+ if (entry.drone_id !== binding.coordinatorDroneId) return null;
581
+ if (entry.visibility === 'direct') {
582
+ return (entry.recipient_drone_ids ?? []).includes(binding.representativeDroneId) ? 'direct' : null;
583
+ }
584
+ return 'broadcast';
585
+ }
586
+
587
+ export async function readRepresentativeReplies(
588
+ ctx: RepresentativeContext,
589
+ raw: unknown,
590
+ ): Promise<{ replies: RepresentativeReply[]; ignored_entries: number; has_more: boolean; delivery: string }> {
591
+ const input = (raw ?? {}) as Record<string, unknown>;
592
+ const unknown = Object.keys(input).filter((key) => key !== 'include_broadcast' && key !== 'limit' && input[key] !== undefined);
593
+ if (unknown.length > 0) throw new RepresentativeError(ErrorCode.INVALID_INPUT, `Unsupported field(s): ${unknown.join(', ')}.`);
594
+ if (input.include_broadcast !== undefined && typeof input.include_broadcast !== 'boolean') {
595
+ throw new RepresentativeError(ErrorCode.INVALID_INPUT, 'include_broadcast must be a boolean.');
596
+ }
597
+ if (input.limit !== undefined && (!Number.isInteger(input.limit) || (input.limit as number) < 1 || (input.limit as number) > 200)) {
598
+ throw new RepresentativeError(ErrorCode.INVALID_INPUT, 'limit must be an integer from 1 to 200.');
599
+ }
600
+ await verifyLiveBinding(ctx);
601
+ const page = await ctx.backend.readUnread(input.limit as number | undefined);
602
+ const requestIds = await ctx.store.transactRequests(ctx.binding.worktree, (records) =>
603
+ new Set(records.map((record) => record.requestId)));
604
+ const replies: RepresentativeReply[] = [];
605
+ for (const entry of page.entries) {
606
+ const addressed = isAddressedCoordinatorEntry(ctx.binding, entry);
607
+ if (addressed === null || (addressed === 'broadcast' && input.include_broadcast !== true)) continue;
608
+ const quoted = (entry.message.match(UUID_SCAN_RE) ?? []).map((id) => id.toLowerCase());
609
+ replies.push({
610
+ entry_id: entry.id,
611
+ created_at: entry.created_at,
612
+ from_drone_id: ctx.binding.coordinatorDroneId,
613
+ from_label: ctx.binding.coordinatorLabel,
614
+ addressed,
615
+ in_reply_to: quoted.find((id) => requestIds.has(id)) ?? null,
616
+ message: entry.message,
617
+ ...(entry.documents?.length ? {
618
+ documents: entry.documents,
619
+ 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.',
620
+ } : {}),
621
+ });
622
+ }
623
+ return {
624
+ replies,
625
+ ignored_entries: page.entries.length - replies.length,
626
+ has_more: page.has_more === true,
627
+ delivery: REPRESENTATIVE_DELIVERY_NOTE,
628
+ };
629
+ }
630
+
631
+ export async function ackRepresentativeReply(
632
+ ctx: RepresentativeContext,
633
+ raw: unknown,
634
+ ): Promise<{ acknowledged: string }> {
635
+ const input = (raw ?? {}) as Record<string, unknown>;
636
+ const unknown = Object.keys(input).filter((key) => key !== 'entry_id');
637
+ if (unknown.length > 0 || typeof input.entry_id !== 'string' || !isRepresentativeUuid(input.entry_id)) {
638
+ throw new RepresentativeError(ErrorCode.INVALID_INPUT, 'entry_id must be the full UUID of a reply returned by borg_representative-read.');
639
+ }
640
+ await verifyLiveBinding(ctx);
641
+ const { entry } = await ctx.backend.readEntry(input.entry_id);
642
+ if (entry.id !== input.entry_id || isAddressedCoordinatorEntry(ctx.binding, entry) !== 'direct') {
643
+ throw new RepresentativeError(
644
+ 'NOT_A_COORDINATOR_REPLY',
645
+ 'Only a direct reply from the bound Coordinator to this representative can be acknowledged here.',
646
+ );
647
+ }
648
+ await ctx.backend.ack(entry.id);
649
+ return { acknowledged: entry.id };
650
+ }
651
+
652
+ export async function representativeStatus(ctx: RepresentativeContext): Promise<{
653
+ role: string;
654
+ connected: boolean;
655
+ problem?: { code: string; message: string };
656
+ cube: { id: string; name: string };
657
+ repository_origin: string | null;
658
+ worktree: string;
659
+ representative: { drone_id: string; label: string; role: string };
660
+ coordinator: { drone_id: string; label: string; role: string };
661
+ unresolved_requests: Array<{ request_id: string; state: string; kind: string; updated_at: string }>;
662
+ delivery: string;
663
+ authority: string;
664
+ }> {
665
+ const { binding } = ctx;
666
+ let problem: { code: string; message: string } | undefined;
667
+ try {
668
+ await verifyLiveBinding(ctx);
669
+ } catch (error) {
670
+ problem = {
671
+ code: error instanceof RepresentativeError ? error.code : 'BACKEND_ERROR',
672
+ message: error instanceof Error ? error.message : 'Unknown error',
673
+ };
674
+ }
675
+ const unresolved = (await ctx.store.readRequests(binding.worktree))
676
+ .filter((record) => record.state === 'pending' || record.state === 'ambiguous')
677
+ .map((record) => ({
678
+ request_id: record.requestId, state: record.state, kind: record.kind, updated_at: record.updatedAt,
679
+ }));
680
+ return {
681
+ role: 'Human representative — an automated delegate speaking for the human. It is not the human and not the Coordinator.',
682
+ connected: problem === undefined,
683
+ ...(problem ? { problem } : {}),
684
+ cube: { id: binding.cubeId, name: binding.cubeName },
685
+ repository_origin: binding.repositoryOrigin ?? null,
686
+ worktree: binding.worktree,
687
+ representative: {
688
+ drone_id: binding.representativeDroneId, label: binding.representativeLabel, role: binding.representativeRoleName,
689
+ },
690
+ coordinator: {
691
+ drone_id: binding.coordinatorDroneId, label: binding.coordinatorLabel, role: binding.coordinatorRoleName,
692
+ },
693
+ unresolved_requests: unresolved,
694
+ delivery: REPRESENTATIVE_DELIVERY_NOTE,
695
+ authority:
696
+ 'Borg records these messages as posts from the representative drone. The user-authorized / model-advice label is this ' +
697
+ 'connection\'s own attribution and is not verified or enforced by the Borg server.',
698
+ };
699
+ }