borgmcp 5.5.0 → 5.7.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 (85) hide show
  1. package/README.md +7 -4
  2. package/dist/claude.d.ts.map +1 -1
  3. package/dist/claude.js +12 -0
  4. package/dist/claude.js.map +1 -1
  5. package/dist/cli-help.d.ts.map +1 -1
  6. package/dist/cli-help.js +16 -6
  7. package/dist/cli-help.js.map +1 -1
  8. package/dist/docs-sections.js +1 -1
  9. package/dist/docs-sections.js.map +1 -1
  10. package/dist/hermes-plugin-install.d.ts +19 -0
  11. package/dist/hermes-plugin-install.d.ts.map +1 -0
  12. package/dist/hermes-plugin-install.js +131 -0
  13. package/dist/hermes-plugin-install.js.map +1 -0
  14. package/dist/local-server-cursor.d.ts +9 -0
  15. package/dist/local-server-cursor.d.ts.map +1 -1
  16. package/dist/local-server-cursor.js +50 -1
  17. package/dist/local-server-cursor.js.map +1 -1
  18. package/dist/log-stream.d.ts +13 -0
  19. package/dist/log-stream.d.ts.map +1 -1
  20. package/dist/log-stream.js +45 -13
  21. package/dist/log-stream.js.map +1 -1
  22. package/dist/remote-client.d.ts +3 -0
  23. package/dist/remote-client.d.ts.map +1 -1
  24. package/dist/remote-client.js +7 -1
  25. package/dist/remote-client.js.map +1 -1
  26. package/dist/representative-cmd.d.ts +11 -0
  27. package/dist/representative-cmd.d.ts.map +1 -1
  28. package/dist/representative-cmd.js +48 -15
  29. package/dist/representative-cmd.js.map +1 -1
  30. package/dist/representative-core.d.ts +53 -7
  31. package/dist/representative-core.d.ts.map +1 -1
  32. package/dist/representative-core.js +229 -43
  33. package/dist/representative-core.js.map +1 -1
  34. package/dist/representative-delivery-store.d.ts +79 -0
  35. package/dist/representative-delivery-store.d.ts.map +1 -0
  36. package/dist/representative-delivery-store.js +233 -0
  37. package/dist/representative-delivery-store.js.map +1 -0
  38. package/dist/representative-listener-store.d.ts +46 -0
  39. package/dist/representative-listener-store.d.ts.map +1 -0
  40. package/dist/representative-listener-store.js +119 -0
  41. package/dist/representative-listener-store.js.map +1 -0
  42. package/dist/representative-listener.d.ts +32 -0
  43. package/dist/representative-listener.d.ts.map +1 -0
  44. package/dist/representative-listener.js +293 -0
  45. package/dist/representative-listener.js.map +1 -0
  46. package/dist/representative-mcp.d.ts +10 -3
  47. package/dist/representative-mcp.d.ts.map +1 -1
  48. package/dist/representative-mcp.js +48 -19
  49. package/dist/representative-mcp.js.map +1 -1
  50. package/dist/representative-store.d.ts +7 -0
  51. package/dist/representative-store.d.ts.map +1 -1
  52. package/dist/representative-store.js +17 -2
  53. package/dist/representative-store.js.map +1 -1
  54. package/dist/seat-probe.d.ts +1 -0
  55. package/dist/seat-probe.d.ts.map +1 -1
  56. package/dist/seat-probe.js +1 -1
  57. package/dist/seat-probe.js.map +1 -1
  58. package/dist/server-trust.d.ts +10 -0
  59. package/dist/server-trust.d.ts.map +1 -1
  60. package/dist/server-trust.js +23 -6
  61. package/dist/server-trust.js.map +1 -1
  62. package/dist/stream-owner.d.ts.map +1 -1
  63. package/dist/stream-owner.js +32 -3
  64. package/dist/stream-owner.js.map +1 -1
  65. package/docs/HUMAN_REPRESENTATIVE.md +310 -36
  66. package/hermes-plugin/borg-representative-push/__init__.py +743 -0
  67. package/hermes-plugin/borg-representative-push/plugin.yaml +39 -0
  68. package/package.json +3 -1
  69. package/src/claude.ts +12 -0
  70. package/src/cli-help.ts +16 -6
  71. package/src/docs-sections.ts +1 -1
  72. package/src/hermes-plugin-install.ts +147 -0
  73. package/src/local-server-cursor.ts +45 -1
  74. package/src/log-stream.ts +47 -14
  75. package/src/remote-client.ts +9 -1
  76. package/src/representative-cmd.ts +52 -20
  77. package/src/representative-core.ts +255 -46
  78. package/src/representative-delivery-store.ts +235 -0
  79. package/src/representative-listener-store.ts +115 -0
  80. package/src/representative-listener.ts +215 -0
  81. package/src/representative-mcp.ts +58 -17
  82. package/src/representative-store.ts +18 -2
  83. package/src/seat-probe.ts +1 -1
  84. package/src/server-trust.ts +25 -6
  85. package/src/stream-owner.ts +34 -2
@@ -70,9 +70,14 @@ To resume later, run `borg representative prepare --coordinator <coordinator-dro
70
70
  your saved labels. Recovery errors for a bound connection print that complete
71
71
  command with its actual labels and worktree. Changing the cube or Coordinator
72
72
  is refused unless you
73
- pass `--rebind`; a rebind also discards the old request ledger. A running
74
- `borg representative mcp` process never picks up a rebind: its calls fail
75
- closed until the MCP host restarts it.
73
+ pass `--rebind`; a rebind that changes the cube or Coordinator also discards the
74
+ old request ledger. A rebind of any kind, including one with the same cube and
75
+ Coordinator, requires restarting every adapter and the listener; running ones
76
+ refuse. A running `borg representative mcp` process answers `send`, `read`,
77
+ `deliver` and `ack` with `BINDING_MISMATCH` until the MCP host restarts it;
78
+ its `status` reports the new `binding_fingerprint` beside the
79
+ `pinned_binding_fingerprint` it started with. A running listener stops with
80
+ reason `rebound`.
76
81
 
77
82
  `prepare` reuses Borg's connection and worktree preparation, including its
78
83
  private-state initialization and saved-connection checks. It does not require
@@ -114,9 +119,10 @@ Check a connection at any time with
114
119
 
115
120
  | Tool | Purpose |
116
121
  | --- | --- |
117
- | `borg_representative-status` | Bound cube, representative, Coordinator; live re-check; unresolved sends; limits. |
122
+ | `borg_representative-status` | Bound cube, representative, Coordinator; live re-check; unresolved sends; limits; `binding_fingerprint`. |
118
123
  | `borg_representative-send` | Relay one `request`, `question` or `decision` to the bound Coordinator. |
119
- | `borg_representative-read` | Unread replies from the bound Coordinator addressed to the representative. Drains everything it fetches. |
124
+ | `borg_representative-read` | The bound Coordinator's replies after the delivered checkpoint, oldest first, bounded. Changes nothing. |
125
+ | `borg_representative-deliver` | Move the delivered checkpoint through a reply the host has durably persisted. |
120
126
  | `borg_representative-ack` | Signal to the Coordinator that one direct reply was received. Nothing more. |
121
127
 
122
128
  There is no tool for logging to other drones, broadcasting, role or drone
@@ -202,32 +208,111 @@ Exactly-once delivery is therefore not claimed; at-most-once per `request_id`
202
208
  relies on the server honouring `post_id` deduplication as the shared protocol
203
209
  specifies, and on the single-process rule below.
204
210
 
205
- ## Reading, cursors and wake limits
206
-
207
- - `read` drains only the representative drone's **own** unread cursor. Other
208
- drones' cursors are separate client-owned state and are never touched.
209
- - A read **consumes everything it fetched**, not only what it returns: the
210
- Coordinator's replies, and equally the entries it ignores (other drones'
211
- entries are counted in `ignored_entries` and never returned; the
212
- Coordinator's broadcasts are returned only with `include_broadcast`). None
213
- of them appear unread again.
214
- - If the MCP host stops between reading a reply and relaying it to the human,
215
- that reply is gone from the unread view. It still exists in the cube log, but
216
- this version offers no tool to list past replies again. Persist the read result
217
- in the host before relaying it.
211
+ ## Reading, delivery and wake limits
212
+
213
+ Four separate notions, each with its own tool or owner:
214
+
215
+ - **Receipt**: `read` returned the reply. Nothing moves; the same reply comes back
216
+ on every `read` until it is delivered.
217
+ - **Delivered**: the host called `deliver` after durably persisting everything up
218
+ to that reply. Only `deliver` moves the read window.
219
+ - **Processing and display**: the host's own business after delivery.
220
+ - **`ack`**: a signal to the Coordinator that a direct reply was received. It is
221
+ not delivery and does not move the window.
222
+
223
+ Reading:
224
+
225
+ - `read` returns the Coordinator's replies addressed to the representative that
226
+ come strictly after the delivered checkpoint, in `(created_at, entry_id)` order.
227
+ Other drones' entries are counted in `ignored_entries` and never returned; the
228
+ Coordinator's broadcasts are returned only with `include_broadcast`.
229
+ - `read` never advances anything: not the checkpoint, not the representative's
230
+ unread cursor, not a server cursor. After a crash at any point, the next `read`
231
+ returns every reply not yet delivered.
232
+ - Bounds: `limit` (1 to 50, default 10) is a hard cap on returned replies.
233
+ `max_bytes` (4096 to 60000, default 32768) caps the serialized tool result.
234
+ Replies are whole or omitted, never truncated; a reply that alone exceeds
235
+ `max_bytes` is returned alone with `"oversize": true`. The serialized result
236
+ never exceeds `max(max_bytes, 16384)` bytes: if an oversize reply is still
237
+ larger than that, its document citations are reduced to ids and it carries
238
+ `"documents_reduced": true`; message text is never cut. `status` reports this
239
+ floor as `envelope_floor`; set the host's tool-result ceiling at or above it.
240
+ The floor covers a message at the server's default post limit (4096 bytes)
241
+ with ordinary metadata. JSON escaping (a message full of tabs or quotes) or
242
+ heavy citation metadata can still push a single reply above it, as can a
243
+ server that allows larger posts. Then `read` refuses with
244
+ `REPRESENTATIVE_READ_OVERSIZE` (`entry_id`, `measured_bytes`, `bound`) and
245
+ changes nothing; retry with a larger `max_bytes` (up to 60000).
246
+ - `read` scans past entries that are not for the representative until the page
247
+ is full or the log ends, so it never returns no replies with `has_more: true`.
248
+ `has_more` is true only when another reply follows the returned window.
249
+ Page by delivering and reading again.
250
+ - The result includes `checkpoint` (`entry_id` and `created_at`; null until the
251
+ first delivery, unless the upgrade below started it at the old read position)
252
+ and `binding_fingerprint`.
253
+
254
+ Delivering:
255
+
256
+ - `deliver` takes `{ "through": "<entry_id>" }` and moves the checkpoint to that
257
+ reply. Call it only after the host has durably persisted every reply up to it.
258
+ - `through` must be a reply that `read` actually returned since the checkpoint
259
+ last moved; membership is checked, not only the range, so a broadcast skipped
260
+ by a read without `include_broadcast` is refused too. Any other entry is refused
261
+ with `REPRESENTATIVE_DELIVER_UNKNOWN_ENTRY` and nothing changes. The same or an older
262
+ id is a no-op that returns `advanced: false`, so a retry after a lost result is safe.
263
+ - Implementation note: the checkpoint, an internal read fence (the latest reply
264
+ any `read` returned) and the replies returned since the checkpoint last moved
265
+ are stored together in one private file per binding, written atomically.
266
+ `deliver` accepts only one of those returned replies. None of this is part of
267
+ the interface.
268
+
269
+ Binding fence:
270
+
271
+ - `binding_fingerprint` is the hex SHA-256 of the canonical JSON array
272
+ `[origin, trustIdentity, cubeId, representativeDroneId, coordinatorDroneId, boundAt]`.
273
+ It changes on every rebind and on a server trust change, and contains no path or
274
+ credential. `prepare --rebind` always starts a new generation, even when the
275
+ cube and Coordinator stay the same. It appears in `status`, `read`, `send`, `deliver` and the listener's
276
+ `listening` event. Persist it at binding time; if any result shows a different
277
+ value, stop routing and hold for the human.
278
+
279
+ Upgrading from a version without `deliver`:
280
+
281
+ - The first `read` or `deliver` for a binding starts the checkpoint where the old
282
+ destructive read left the representative's unread cursor: replies that were
283
+ unread at upgrade time are returned, replies already read are not. When the
284
+ binding never read, every addressed reply in the cube log is returned. If the
285
+ upgrade is interrupted before its checkpoint is written, the next read returns
286
+ every addressed reply instead; deduplicate by `entry_id`.
287
+ The old position is taken only from a genuine private file (a regular file you
288
+ own, not a symlink, not writable by group or others, in the private config
289
+ directory); otherwise the checkpoint starts empty and every addressed reply is
290
+ returned.
291
+ - That upgrade happens once per representative drone and server authority, and is
292
+ recorded in a private marker, so it never runs again, even after a checkpoint
293
+ file is removed. A later
294
+ binding generation (any `prepare --rebind`, or a trust change) starts with an
295
+ empty checkpoint, so its first read returns its addressed history. Deduplicate by
296
+ `entry_id` (the host persists everything it delivers) and page with `limit`.
297
+
298
+ Known limits:
299
+
300
+ - Retention is the server's cube log; the local inbox is not a content source.
301
+ - Each `read` scans the cube log from the checkpoint, including entries not
302
+ addressed to the representative, so a host that never calls `deliver` pays a
303
+ growing scan. Deliver promptly.
218
304
  - Replies preserve document citations (id, title and state). Document bodies are
219
305
  not included and cannot be fetched through this connection. Ask the Coordinator
220
306
  to provide the content through a supported channel.
221
- - `limit` is a page-size hint, not a hard cap: when the unread backlog is
222
- large the client's digest mode fetches, and drains, more than `limit`.
223
- - `ack` is only a signal to the Coordinator that a direct reply was received.
224
- It does not make delivery reliable, and it neither advances nor restores the
225
- unread cursor.
307
+
308
+ Ownership:
309
+
226
310
  - Exclusive process ownership is enforced for each representative drone. Processes
227
- may start idle; the first `send`, `read` or `ack` takes the lease. Other processes
228
- receive `REPRESENTATIVE_OWNERSHIP_REQUIRED` before any ledger reservation or
229
- write, cursor access, or network call. The refusal names the owner's PID and
230
- start time. Use that host, or wait for it to exit before using another.
311
+ may start idle; the first `send`, `read`, `deliver` or `ack` takes the lease. Other
312
+ processes receive `REPRESENTATIVE_OWNERSHIP_REQUIRED` before any ledger
313
+ reservation, delivery-state write, cursor access or network call. The refusal
314
+ names the owner's PID and start time. Use that host, or wait for it to exit before
315
+ using another.
231
316
  - `status` is allowed in every process, is read-only, and takes no lease. Its
232
317
  `ownership` field reports the state, PID, start time, and heartbeat age in
233
318
  milliseconds (`ageMs`). A clean exit releases ownership; a dead PID or a
@@ -238,20 +323,206 @@ specifies, and on the single-process rule below.
238
323
  deduplication. Retry an ambiguous send with its original `request_id`.
239
324
  - `in_reply_to` is a textual match of a known `request_id` quoted in the reply.
240
325
  It is a convenience, not a protocol guarantee.
241
- - **There is no background wake.** A generic MCP host receives nothing
242
- unsolicited: replies are seen only when the host calls
243
- `borg_representative-read`. This version provides explicit send/read round
244
- trips only and makes no claim of automatic ongoing coordination. The
245
- Coordinator is woken by the direct message through its own normal wake path.
326
+
327
+ Wake hints:
328
+
329
+ - **The MCP process does not push content.** A separate supervised `listen`
330
+ process emits body-free wake hints. The owning adapter still calls `read` for
331
+ content. Hints are neither delivery receipts nor authority.
332
+ - Live dedupe is bounded to the surviving inbox tail and recent IDs; an ancient
333
+ trimmed ID sent again as a live event can produce a duplicate hint. Ordered
334
+ catch-up also dedupes against its captured resume cursor.
335
+ - The listener retains a bounded tail: above 1024 lines it trims to the latest
336
+ 512. Lost-hint replay covers only that tail; beyond it `read` from the delivered
337
+ checkpoint is the source of truth. On every `gap`, call `read`.
246
338
 
247
339
  ### Host conversation routing
248
340
 
249
341
  The lease selects one consuming process, not a conversation within that host.
250
342
  The host must record which conversation owns each `request_id`, persist every
251
- read result before relaying it, and route replies using `in_reply_to`. Hold
252
- replies with an unknown or missing request ID for the human instead of dropping
253
- them. Borg cannot enforce these duties inside the host; it provides neither a
254
- durable inbox nor a separate unread cursor for each conversation.
343
+ reply durably before calling `deliver`, and route replies using `in_reply_to`.
344
+ Hold replies with an unknown or missing request ID for the human instead of
345
+ dropping them. Borg cannot enforce these duties inside the host; it provides one
346
+ delivered checkpoint per binding, not one per conversation. The listener inbox
347
+ is private client state, not a host content API.
348
+
349
+ ## Supervised listener
350
+
351
+ For a prepared connection, run a separate long-lived subprocess:
352
+
353
+ ```bash
354
+ borg representative listen --worktree <path>
355
+ ```
356
+
357
+ Supervise it and persist the last `entry_id` durably admitted to the host's work
358
+ queue. On subsequent starts, pass that checkpoint:
359
+
360
+ ```bash
361
+ borg representative listen --worktree <path> --replay-after <entry_id>
362
+ ```
363
+
364
+ There is one listener lease per representative drone and server authority,
365
+ independent of the lazy tools lease. A second listener refuses without consuming
366
+ or appending anything. A dead owner or expired heartbeat permits takeover; a
367
+ process that loses ownership exits and must be restarted. A local lease cannot
368
+ cancel an already-issued request. No eager tools-lease option is needed when one
369
+ adapter exclusively calls `send`, `read`, `deliver` and `ack`.
370
+
371
+ `representative status` reports `listener` beside tool `ownership`: running
372
+ state, owner PID and start time, heartbeat age (`ageMs`), persisted watermark and
373
+ private inbox path. Status acquires nothing. Do not read the inbox or depend on
374
+ its pathname; it is not the content-delivery interface.
375
+
376
+ Stdout is newline-delimited JSON only. Stderr contains human diagnostics and
377
+ must not be parsed. The host must ignore unknown fields and unknown event types.
378
+
379
+ | Event | Fields and meaning |
380
+ | --- | --- |
381
+ | `refused` | The only stdout line on startup refusal: `code`, `exit_code`. Another listener uses `REPRESENTATIVE_LISTENER_OWNED`, plus `owner_pid` and `owner_started_at`. |
382
+ | `listening` | Once connected and holding the lease: `cube_id`, `drone_id`, `binding_fingerprint`, `watermark` (entry id or null), `inbox`. |
383
+ | `entry` | `entry_id`, `created_at`, `from_label`, `from_role`, `visibility`, `request_id` (UUID or null), `documents` (count), `replay` (boolean). No message body. |
384
+ | `reconnecting` | `attempt`, `delay_ms`. |
385
+ | `connected` | `resumed_from` (entry id or null). |
386
+ | `gap` | `after` (entry id or null), `reason`: `cursor-expired` or `replay-checkpoint-missing`. Call `read` once. |
387
+ | `stopped` | `reason`: `signal`, `evicted`, `rebound`, `revoked`, `trust-changed`, `lease-lost` or `fatal`; `exit_code`. |
388
+
389
+ With `--replay-after`, retained hints strictly after the checkpoint are emitted
390
+ in file order after `listening` and before live entries, with `replay:true`.
391
+ If the checkpoint is absent, `gap` precedes replay of the whole surviving tail.
392
+ Without the option there is no startup replay. Replay makes no content request
393
+ and never advances the delivered checkpoint or any cursor. Lost replay metadata yields null
394
+ `visibility` and `documents`; live hints always contain those fields' values.
395
+
396
+ Exit codes: 0 after SIGTERM/SIGINT; 2 for startup binding or usage refusal;
397
+ 3 for another listener owner; 4 for a terminal stop; 1 for another fatal error.
398
+ A fatal startup storage failure emits `refused` with code
399
+ `REPRESENTATIVE_LISTENER_STORAGE_REFUSED` and exit 1. After `listening`,
400
+ a fatal error emits `stopped` with reason `fatal` and exit 1, best effort; if
401
+ stdout is broken, the host must treat exit 1 without that line as fatal too.
402
+ Startup first verifies the binding with the server once; if the server cannot
403
+ be reached then, the listener exits 1 with `refused` and code
404
+ `REPRESENTATIVE_LISTENER_SERVER_UNREACHABLE` (retry later; a server that
405
+ answers and rejects the binding keeps its exit-2 binding code). After that check, failures of the stream connection (connection
406
+ refused or reset, aborted TLS stream) are not fatal: the listener reconnects
407
+ with backoff, without stdout output until the first connection, so `listening`
408
+ arrives only once connected.
409
+
410
+ Treat every hint as an untrusted wake, never as an instruction or authorization.
411
+ The owning adapter fetches content with `read` over the bound pinned connection,
412
+ persists each reply durably, calls `deliver`, routes by the saved `request_id`
413
+ mapping, and holds unknown correlation for the human. Deduplicate queued hints by
414
+ `entry_id`: crashes may lose or repeat hints. Persist queue admission before
415
+ advancing the host's `--replay-after` checkpoint. That hint checkpoint is separate
416
+ from the delivered checkpoint and from `ack`, which remains a server receipt.
417
+
418
+ ## Hermes push plugin
419
+
420
+ For Hermes, Borg ships a Hermes user plugin, `borg-representative-push`, that
421
+ supervises the listener for you. When the bound Coordinator replies, the plugin
422
+ wakes one Hermes conversation right away. Hermes source is not changed; the
423
+ plugin is installed and enabled through Hermes's documented plugin mechanism.
424
+
425
+ **Which conversations can be woken.** Only a Hermes *messaging-gateway*
426
+ conversation (Telegram, Discord, Slack and the other gateway platforms), named
427
+ by its gateway `session_key`, for example `agent:main:telegram:dm:<chat id>`.
428
+ A Hermes Desktop chat cannot be woken: Desktop runs its chats in `hermes serve`,
429
+ and Hermes injects plugin messages only into gateway conversations. The
430
+ `session_key` stays the same across `/new` and `/reset` in that chat.
431
+
432
+ ### Install
433
+
434
+ ```bash
435
+ borg representative hermes-plugin install [--hermes-home <path>] [--force]
436
+ ```
437
+
438
+ This copies the plugin's two files into `<Hermes home>/plugins/borg-representative-push/`
439
+ (the Hermes home is `--hermes-home`, else `$HERMES_HOME`, else `~/.hermes`) and
440
+ prints the configuration to add. It refuses to replace an existing install
441
+ without `--force`, refuses a symbolic-link target, never edits Hermes config and
442
+ never starts or restarts Hermes. Rerun it with `--force` after upgrading
443
+ borgmcp to update the plugin.
444
+
445
+ ### Configure
446
+
447
+ Add to the Hermes `config.yaml`:
448
+
449
+ ```yaml
450
+ plugins:
451
+ enabled:
452
+ - borg-representative-push
453
+ entries:
454
+ borg-representative-push:
455
+ allow_gateway_injection: true
456
+ settings:
457
+ session_key: "agent:main:<platform>:<chat type>:<chat id>"
458
+ worktree: "<absolute path of the prepared representative worktree>"
459
+ # optional: borg_command (default borg), mcp_server (default
460
+ # borg-representative), reinject_after_s (default 600), max_reinjects (default 3)
461
+ mcp_servers:
462
+ borg-representative:
463
+ command: borg
464
+ args: ["representative", "mcp", "--worktree", "<same absolute worktree path>"]
465
+ lazy: true
466
+ ```
467
+
468
+ `allow_gateway_injection` is Hermes's per-plugin permission to start gateway
469
+ turns; it is off by default. `mcp_server` must name the `mcp_servers` entry that
470
+ runs `borg representative mcp`, because the plugin recognises the deliver tool
471
+ by that name. Then restart the gateway (`hermes gateway restart`).
472
+
473
+ **One tool owner.** Hermes starts a separate MCP process in every Hermes
474
+ process that uses the server, and only one process may hold the representative
475
+ tools lease. The woken conversation runs in the gateway, so the gateway must be
476
+ the process that uses the tools. With `lazy: true`, a process starts the Borg
477
+ MCP server only when one of its conversations calls a Borg tool. Run `hermes
478
+ tools` and disable the `mcp-borg-representative` toolset on every platform except
479
+ the one in `session_key`, Desktop and CLI included. If another process already
480
+ holds the lease (`borg representative status` shows `owned-by-other-process`),
481
+ restart that process once so it releases the lease.
482
+
483
+ ### Behaviour
484
+
485
+ - The listener starts only inside the Hermes messaging gateway, when the platform
486
+ named in `session_key` connects. The CLI, Desktop and worker processes load the
487
+ plugin but start nothing. A platform reconnect does not start a second listener.
488
+ - A burst of hints (about 2 seconds) becomes one injected message with fixed
489
+ text: `Borg: new Coordinator reply. Call borg_representative-read, persist and
490
+ relay, then borg_representative-deliver through the last persisted entry_id.`
491
+ No message body, sender or document ever passes through the plugin. A busy
492
+ conversation queues the message; it does not interrupt the running turn.
493
+ - The plugin watches this gateway's `borg_representative-deliver` results. A
494
+ hinted reply that is still undelivered after `reinject_after_s` wakes the
495
+ conversation again. Each reply gets at most `1 + max_reinjects` wakes in total;
496
+ after that the plugin logs it and wakes no more for that reply until a delivery
497
+ covers it. Every wake is counted on disk before Hermes is asked to start the turn,
498
+ so neither a replayed hint nor a gateway restart renews the count. A wake Hermes
499
+ refuses is not counted. A crash between counting and asking can lose one wake,
500
+ never add one. Hermes reports only that it accepted a message, not that the
501
+ turn ran, so this is how a dropped wake is recovered.
502
+ - One small record (id, timestamp, count) is kept for each reply the plugin has
503
+ woken the conversation for. It is removed only when an observed delivery covers
504
+ that reply; there is no count limit. The records therefore grow only while woken
505
+ replies stay undelivered.
506
+ - Listener exits: 0 stops; 1 restarts with capped backoff; 2 stops and logs
507
+ (fix the binding, then restart the gateway); 3 (another listener owns the
508
+ lease) retries with backoff; 4 restarts after `lease-lost` and otherwise stops
509
+ and logs (evicted, rebound, revoked, trust-changed). A stop ends every wake,
510
+ including queued and repeat wakes, until the gateway restarts.
511
+ - On restart the listener replays retained hints after the last delivered
512
+ checkpoint the plugin observed (`--replay-after`). The delivered checkpoint
513
+ stays the source of truth: `read` returns every reply not yet delivered.
514
+ - On a normal gateway exit the plugin stops the listener. If the gateway is killed,
515
+ the listener it started keeps its lease until its next hint fails to write.
516
+ The next gateway stops that orphan only if it is the recorded listener and its
517
+ parent is gone. Otherwise it retries with backoff until the lease is free. A
518
+ dead owner's lease expires after about 70 seconds; a live orphan releases it
519
+ when its next hint fails to write.
520
+ - State (the recorded listener, the observed delivered checkpoint and the wake
521
+ records) and the
522
+ listener's stderr live under `<Hermes home>/plugin-data/borg-representative-push/`.
523
+ The plugin sets that directory to mode 0700, refuses it if it is a symbolic link,
524
+ creates its files with mode 0600, and never reads or writes through a symbolic
525
+ link planted there.
255
526
 
256
527
  ## Recovery
257
528
 
@@ -267,3 +538,6 @@ the server is installed under the original prefix.
267
538
  | `COORDINATOR_UNAVAILABLE` | The bound Coordinator was evicted, released or reassigned. Restore the bound Coordinator and use the printed recovery command, or deliberately substitute a new Coordinator label in that command. |
268
539
  | `REPRESENTATIVE_OWNERSHIP_REQUIRED` with a directory-permission refusal | Check that the named path is a real directory you own and not a symlink, then set it to 0700 and retry. Restart a process that had already lost ownership. Do not change permissions through a symlink. |
269
540
  | `REPRESENTATIVE_ROLE_NOT_PERMITTED` | The representative drone holds a human-seat or coordinating role. Give it its own worker role. |
541
+ | `REPRESENTATIVE_CHECKPOINT_INVALID` | The private delivery checkpoint for this binding (named in the message) is corrupt, belongs to another seat, or fails the private-file checks. Every tool except `status` refuses and `status` reports it as `checkpoint_problem`; nothing is used or reset automatically. Inspect the file, then remove it; the next `read` returns every addressed reply again, so deduplicate by `entry_id`. |
542
+ | `REPRESENTATIVE_READ_OVERSIZE` | The next reply does not fit `max(max_bytes, 16384)` bytes even with its citations reduced to ids (heavy JSON escaping or citation metadata, or a server allowing posts above its default 4096-byte limit). Nothing was read or advanced. Retry with a larger `max_bytes` (up to 60000); beyond that the operator must lower the server's post limit. |
543
+ | `REPRESENTATIVE_DELIVER_UNKNOWN_ENTRY` | `deliver` named an entry that `read` has not returned (or no Coordinator reply). Nothing changed. Call `read`, persist what it returns, then deliver through its last `entry_id`. |