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.
- package/README.md +7 -4
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +12 -0
- package/dist/claude.js.map +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +16 -6
- package/dist/cli-help.js.map +1 -1
- package/dist/docs-sections.js +1 -1
- package/dist/docs-sections.js.map +1 -1
- package/dist/hermes-plugin-install.d.ts +19 -0
- package/dist/hermes-plugin-install.d.ts.map +1 -0
- package/dist/hermes-plugin-install.js +131 -0
- package/dist/hermes-plugin-install.js.map +1 -0
- package/dist/local-server-cursor.d.ts +9 -0
- package/dist/local-server-cursor.d.ts.map +1 -1
- package/dist/local-server-cursor.js +50 -1
- package/dist/local-server-cursor.js.map +1 -1
- package/dist/log-stream.d.ts +13 -0
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +45 -13
- package/dist/log-stream.js.map +1 -1
- package/dist/remote-client.d.ts +3 -0
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +7 -1
- package/dist/remote-client.js.map +1 -1
- package/dist/representative-cmd.d.ts +11 -0
- package/dist/representative-cmd.d.ts.map +1 -1
- package/dist/representative-cmd.js +48 -15
- package/dist/representative-cmd.js.map +1 -1
- package/dist/representative-core.d.ts +53 -7
- package/dist/representative-core.d.ts.map +1 -1
- package/dist/representative-core.js +229 -43
- package/dist/representative-core.js.map +1 -1
- package/dist/representative-delivery-store.d.ts +79 -0
- package/dist/representative-delivery-store.d.ts.map +1 -0
- package/dist/representative-delivery-store.js +233 -0
- package/dist/representative-delivery-store.js.map +1 -0
- package/dist/representative-listener-store.d.ts +46 -0
- package/dist/representative-listener-store.d.ts.map +1 -0
- package/dist/representative-listener-store.js +119 -0
- package/dist/representative-listener-store.js.map +1 -0
- package/dist/representative-listener.d.ts +32 -0
- package/dist/representative-listener.d.ts.map +1 -0
- package/dist/representative-listener.js +293 -0
- package/dist/representative-listener.js.map +1 -0
- package/dist/representative-mcp.d.ts +10 -3
- package/dist/representative-mcp.d.ts.map +1 -1
- package/dist/representative-mcp.js +48 -19
- package/dist/representative-mcp.js.map +1 -1
- package/dist/representative-store.d.ts +7 -0
- package/dist/representative-store.d.ts.map +1 -1
- package/dist/representative-store.js +17 -2
- package/dist/representative-store.js.map +1 -1
- package/dist/seat-probe.d.ts +1 -0
- package/dist/seat-probe.d.ts.map +1 -1
- package/dist/seat-probe.js +1 -1
- package/dist/seat-probe.js.map +1 -1
- package/dist/server-trust.d.ts +10 -0
- package/dist/server-trust.d.ts.map +1 -1
- package/dist/server-trust.js +23 -6
- package/dist/server-trust.js.map +1 -1
- package/dist/stream-owner.d.ts.map +1 -1
- package/dist/stream-owner.js +32 -3
- package/dist/stream-owner.js.map +1 -1
- package/docs/HUMAN_REPRESENTATIVE.md +310 -36
- package/hermes-plugin/borg-representative-push/__init__.py +743 -0
- package/hermes-plugin/borg-representative-push/plugin.yaml +39 -0
- package/package.json +3 -1
- package/src/claude.ts +12 -0
- package/src/cli-help.ts +16 -6
- package/src/docs-sections.ts +1 -1
- package/src/hermes-plugin-install.ts +147 -0
- package/src/local-server-cursor.ts +45 -1
- package/src/log-stream.ts +47 -14
- package/src/remote-client.ts +9 -1
- package/src/representative-cmd.ts +52 -20
- package/src/representative-core.ts +255 -46
- package/src/representative-delivery-store.ts +235 -0
- package/src/representative-listener-store.ts +115 -0
- package/src/representative-listener.ts +215 -0
- package/src/representative-mcp.ts +58 -17
- package/src/representative-store.ts +18 -2
- package/src/seat-probe.ts +1 -1
- package/src/server-trust.ts +25 -6
- 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
|
|
74
|
-
|
|
75
|
-
|
|
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` |
|
|
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,
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
228
|
-
receive `REPRESENTATIVE_OWNERSHIP_REQUIRED` before any ledger
|
|
229
|
-
write, cursor access
|
|
230
|
-
start time. Use that host, or wait for it to exit before
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
252
|
-
replies with an unknown or missing request ID for the human instead of
|
|
253
|
-
them. Borg cannot enforce these duties inside the host; it provides
|
|
254
|
-
|
|
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`. |
|