borgmcp 3.11.1 → 3.12.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 (38) hide show
  1. package/THIRD_PARTY_NOTICES.md +5 -0
  2. package/dist/assimilate-deps.d.ts.map +1 -1
  3. package/dist/assimilate-deps.js +11 -0
  4. package/dist/assimilate-deps.js.map +1 -1
  5. package/dist/codex-app-wake.d.ts +3 -2
  6. package/dist/codex-app-wake.d.ts.map +1 -1
  7. package/dist/codex-app-wake.js +42 -9
  8. package/dist/codex-app-wake.js.map +1 -1
  9. package/dist/cube-activity-wake-copy.d.ts +1 -1
  10. package/dist/cube-activity-wake-copy.d.ts.map +1 -1
  11. package/dist/cube-activity-wake-copy.js +1 -1
  12. package/dist/cube-activity-wake-copy.js.map +1 -1
  13. package/dist/log-stream.d.ts +3 -3
  14. package/dist/log-stream.d.ts.map +1 -1
  15. package/dist/log-stream.js +8 -13
  16. package/dist/log-stream.js.map +1 -1
  17. package/dist/opencode-drone.d.ts +1 -1
  18. package/dist/opencode-drone.d.ts.map +1 -1
  19. package/dist/opencode-drone.js +16 -2
  20. package/dist/opencode-drone.js.map +1 -1
  21. package/dist/remote-client.d.ts +2 -21
  22. package/dist/remote-client.d.ts.map +1 -1
  23. package/dist/remote-client.js +8 -6
  24. package/dist/remote-client.js.map +1 -1
  25. package/dist/server-handshake.d.ts +6 -1
  26. package/dist/server-handshake.d.ts.map +1 -1
  27. package/dist/server-handshake.js +1 -0
  28. package/dist/server-handshake.js.map +1 -1
  29. package/docs/LOCAL_SERVER.md +4 -8
  30. package/docs/RELEASING.md +32 -308
  31. package/package.json +3 -2
  32. package/src/assimilate-deps.ts +11 -0
  33. package/src/codex-app-wake.ts +42 -8
  34. package/src/cube-activity-wake-copy.ts +1 -1
  35. package/src/log-stream.ts +24 -15
  36. package/src/opencode-drone.ts +14 -1
  37. package/src/remote-client.ts +11 -25
  38. package/src/server-handshake.ts +4 -1
package/docs/RELEASING.md CHANGED
@@ -8,8 +8,9 @@ npm Trusted Publishing; no long-lived npm token is stored or exposed.
8
8
  ## Release Integrity
9
9
 
10
10
  Release provenance is established by protected annotated tags, GitHub build
11
- provenance, and npm Trusted Publishing. Tags and publication workflows are
12
- immutable: never move, replace, reuse, or rerun them.
11
+ provenance, and npm Trusted Publishing. Never move, replace, or reuse a tag.
12
+ A failed workflow may be rerun until npm accepts a stage; stage acceptance
13
+ consumes the version.
13
14
 
14
15
  ## Release Prerequisites
15
16
 
@@ -20,19 +21,15 @@ Before creating the release tag, independently verify all of these conditions:
20
21
  - the extraction review confirms no private backend secrets, deployment
21
22
  configuration, customer data, local state, or duplicated shared contracts
22
23
  entered the public package;
23
- - the exact audited registry dependency `borgmcp-shared@0.11.0` remains locked to
24
+ - the exact audited registry dependency `borgmcp-shared@0.12.2` remains locked to
24
25
  its canonical tarball and integrity
25
- `sha512-I8mixCbSrLKyOAAyqEI/HZJ8cML2rz3r812Up8pr547OdAk9LxZevdCo7ojG42ZwrUmS5u7iKQPg7Vk1XvtX1g==`;
26
- - the coupled shared/server/client candidates have all been built against the
27
- same exact `borgmcp-shared` version and passed the complete local dogfood gate
28
- before any stage is approved;
26
+ `sha512-l459XEeqk0cSz1+Z8yk8cCVWik4/CX4OBTRZqj6n1SZYvDpzWJksUz82FA9k4taf//rs43Tfl1tpWXnRHAqxOQ==`;
27
+ - the coupled shared/server/client candidates use the same exact
28
+ `borgmcp-shared` version;
29
29
  - the selected stable client version is unused and the exact release commit is
30
30
  on protected `main`;
31
31
  - the repository and protected npm environment settings pass an operator audit;
32
- - the exact release source passes Code Review, Security Review, Release Quality,
33
- extraction, and package gates; and
34
- - the immutable annotated tag and publication receive explicit release
35
- authorization.
32
+ - the exact release source passes exact-SHA CI and one Code Review.
36
33
 
37
34
  `scripts/verify-release-readiness.mjs` makes the source-side blockers
38
35
  machine-checkable. A release tag created before they are resolved fails before
@@ -41,18 +38,19 @@ dependency installation or publication.
41
38
  ### Release branches
42
39
 
43
40
  Release branches use the `release/` prefix and enter protected `main` through a
44
- pull request. Direct pushes only update staging branches; publication still
45
- requires the complete protected release gate.
41
+ pull request. After exact-SHA CI and one Code Review, merge the preparation and
42
+ create its annotated version tag. The tag automatically verifies and stages the
43
+ package.
46
44
 
47
45
  ## Repository Controls
48
46
 
49
47
  Repository settings are operator-owned and are not changed by this workflow.
50
48
  Before preparing a candidate, independently verify:
51
49
 
52
- 1. The `npm-publish` environment disables administrator bypass, requires the
53
- reviewed human approver, and allows only the protected release refs. Its
54
- `NPM_EXPECTED_OWNER` variable must match the sole reviewed maintainer of the
55
- existing `borgmcp` package. It must contain no npm token.
50
+ 1. The `npm-publish` environment disables administrator bypass, has no required
51
+ reviewer, and allows only `v*.*.*` tags. Its `NPM_EXPECTED_OWNER` variable
52
+ must match the sole reviewed maintainer of the existing `borgmcp` package.
53
+ It must contain no npm token.
56
54
  2. npm Trusted Publishing is configured for organization `Byte-Ventures`,
57
55
  repository `borg-mcp-client`, workflow `publish.yml`, and environment
58
56
  `npm-publish`. Its allowed actions enable `npm stage publish` and disable
@@ -69,212 +67,8 @@ Before preparing a candidate, independently verify:
69
67
 
70
68
  ## Release Workflow
71
69
 
72
- ### Clean-environment rig lifecycle
73
-
74
- Every clean-environment verification rig has an explicit identity and an
75
- explicit end-of-life. Run it from a Borg-launched session and keep filesystem
76
- workspaces under the exact disposable scratch root Borg exports. Give each
77
- workspace a name beginning with `borg-rig-` followed by its owner, purpose, and
78
- a unique suffix. Before the first command that can create files, anchor the rig
79
- as its own npm project:
80
-
81
- ```sh
82
- BORG_SCRATCH_ROOT="${BORG_LAUNCH_SCRATCH:?run from a Borg-launched session}"
83
- RIG_OWNER="$(basename "$BORG_SCRATCH_ROOT")"
84
- RIG_NONCE="${RIG_NONCE:-$(date +%Y%m%d%H%M%S)-$$}"
85
- RIG_ID="borg-rig-${RIG_OWNER}-release-${RIG_NONCE}"
86
- RIG_ROOT="$BORG_SCRATCH_ROOT/$RIG_ID"
87
- mkdir -p "$RIG_ROOT"
88
- printf '%s\n' '{"private":true}' > "$RIG_ROOT/package.json"
89
- ```
90
-
91
- The manifest anchor is required before any `npm install`, `npm update`, or
92
- other npm command. Without it, npm can walk up from an empty scratch directory
93
- and write the operator's `package.json` instead of creating project-local
94
- state. In a source checkout, the committed `release:exercise` follows this rule
95
- for its temporary consumer; other QA scripts and manual rigs must do the same.
96
-
97
- The system temporary root is shared with the whole machine, so an unbounded
98
- listing there is not an inspectable cleanup check. Bound that leg by the
99
- invoking user and the run-window markers, which select directories whose
100
- timestamps fall within this run window without requiring a prefix inventory.
101
- New `mkdtemp` prefixes therefore require no documentation change. Keep both
102
- time predicates and the ownership predicate: removing the run-window bound
103
- turns a populated shared temp root back into an uninspectable listing.
104
-
105
- On a machine where several runs share one user account, this listing can
106
- include another run's live workspace. Removal is scoped to paths created by
107
- this run; anything else in the output is reported, not deleted. Unrelated
108
- same-user temporary directories created during the window can therefore
109
- appear in the result, so never remove a path merely because this listing found
110
- it.
111
-
112
- ```sh
113
- TEMP_ROOT="${TMPDIR:-/tmp}"
114
- TEMP_OWNER="$(id -un)"
115
- TEMP_SCAN_START="$TEMP_ROOT/.${RIG_ID}-temp-scan-start"
116
- TEMP_SCAN_END="$TEMP_ROOT/.${RIG_ID}-temp-scan-end"
117
-
118
- list_recent_owned_temp_rigs() {
119
- find "$TEMP_ROOT" -mindepth 1 -prune \
120
- -type d \
121
- -user "$TEMP_OWNER" \
122
- -newer "$TEMP_SCAN_START" \
123
- ! -newer "$TEMP_SCAN_END" \
124
- -print
125
- }
126
-
127
- touch "$TEMP_SCAN_START"
128
- ```
129
-
130
- Container-backed rigs use the same `RIG_ID` as the container name and carry
131
- both labels below. Register exact-target cleanup before launching the rig:
132
-
133
- ```sh
134
- list_owned_rig_containers() {
135
- docker container ls --all \
136
- --filter label=borg-rig=1 \
137
- --filter "label=borg-rig-owner=${RIG_OWNER:?}" \
138
- --format '{{.ID}}\t{{.Names}}\t{{.Status}}'
139
- }
140
-
141
- cleanup_done=0
142
- cleanup() {
143
- [ "$cleanup_done" -eq 0 ] || return
144
- cleanup_done=1
145
- trap - EXIT HUP INT TERM
146
- docker container rm --force "$RIG_ID" >/dev/null 2>&1 || true
147
- rm -rf -- "$RIG_ROOT"
148
- list_owned_rig_containers
149
- find "$BORG_SCRATCH_ROOT" -mindepth 1 -print
150
- find "$BORG_SCRATCH_ROOT" -name 'borg-rig-*' -print
151
- if [ -e "$TEMP_SCAN_START" ]; then
152
- touch "$TEMP_SCAN_END"
153
- list_recent_owned_temp_rigs
154
- fi
155
- rm -f -- "$TEMP_SCAN_START" "$TEMP_SCAN_END"
156
- }
157
- trap cleanup EXIT
158
- trap 'exit 129' HUP
159
- trap 'exit 130' INT
160
- trap 'exit 143' TERM
161
- ```
162
-
163
- List containers and scratch paths before starting. The filters distinguish this
164
- session's rigs from unrelated containers on a shared host:
165
-
166
- ```sh
167
- list_owned_rig_containers
168
- find "$BORG_SCRATCH_ROOT" -mindepth 1 -print
169
- find "$BORG_SCRATCH_ROOT" -name 'borg-rig-*' -print
170
- ```
171
-
172
- Only after the cleanup traps and pre-launch listings are in place, launch a
173
- container-backed rig. `--rm` is preferred; a runtime without automatic removal
174
- must remove the exact `RIG_ID` in its cleanup path and retain the labels for a
175
- bounded sweep:
176
-
177
- ```sh
178
- RIG_IMAGE="${RIG_IMAGE:?set the rig image}"
179
- docker run --rm \
180
- --name "$RIG_ID" \
181
- --label borg-rig=1 \
182
- --label "borg-rig-owner=$RIG_OWNER" \
183
- "$RIG_IMAGE"
184
- ```
185
-
186
- The unfiltered filesystem listings expose legacy names as well as conforming
187
- rigs. The bounded temporary-directory function reports same-user directories
188
- created during the run window, including unrelated work; it does not establish
189
- which process owns them. The cleanup path runs the container and scratch
190
- listings after removing this rig, then runs the bounded temporary-directory
191
- listing between the start and end markers. Inspect and report its results. If a
192
- rig creates an exact system-temporary path, record that path when it is created
193
- and remove that exact path during cleanup; never remove a path solely because
194
- the bounded listing found it.
195
-
196
- Register cleanup before launching any process, run it on success and failure,
197
- and do not deliver a verdict until the container listing and both scratch
198
- listings show no rig owned by this run, every exact system-temporary path
199
- recorded by the rig has been removed, and the bounded temporary-directory
200
- listing has been inspected and reported. For a non-`--rm` container runtime,
201
- remove only the named rig or the same owner label; never prune unrelated
202
- containers. A completed verification
203
- therefore implies zero running or stopped rig containers and no rig workspace
204
- created by this run left in either the session scratch root or any exact
205
- system-temporary path the rig recorded.
206
-
207
- ### Pre-tag composed exercise
208
-
209
- Before creating a release tag, exercise the packed client against the selected
210
- server artifact:
211
-
212
- ```sh
213
- npm run release:exercise -- \
214
- --server /absolute/path/to/borgmcp-server.tgz \
215
- --server-integrity 'sha512-...'
216
- ```
217
-
218
- The server integrity must come from the reviewed producer of that artifact. Run
219
- the command once with the published counterpart. When the client and server are
220
- being released together, run it again with the co-releasing server candidate.
221
- The default path builds and packs the current client; `--client-tarball` exists
222
- only for reproducing a reviewed packed client or running a negative control.
223
-
224
- This harness exercises reviewed release candidates and their published
225
- counterparts. It is not a containment sandbox for hostile or otherwise untrusted
226
- packages and must not be used as one. The temporary install and data directories
227
- isolate the reviewed exercise from normal product state; they are not an
228
- operating-system security boundary.
229
-
230
- The harness requires Node.js 22, npm 11.18.0, Python 3, and macOS or Linux. It
231
- installs both tarballs into a private temporary project, bootstraps isolated
232
- server data, and starts the installed server directly. It never uses
233
- `borg server setup`, because registry-backed activation would select the
234
- previously published server instead of a candidate.
235
-
236
- Both composed terminal journeys run under a real PTY:
237
-
238
- - `borg server dashboard` must render the attached-viewer footer, exit cleanly
239
- on Ctrl-C, restore the cursor and alternate screen, and leave the server's
240
- pinned-TLS process healthy at `GET /healthz` (204, empty body).
241
- - `borg server start` must render the foreground-server footer, exit cleanly on
242
- Ctrl-C, restore the terminal, stop the server, and release its health endpoint.
243
-
244
- The client facade resolves `borg-mcp-server` by bare name. The harness therefore
245
- uses a controlled shim only to arrange resolution, then verifies the outcome:
246
- before trusting a frame, it reads the live process command and requires the
247
- absolute installed server entry. Every client or server executable must resolve
248
- inside the installed package root covered by the SRI cited for that role. Its
249
- JSON report records the absolute path, version, and independently supplied
250
- integrity for the client, dashboard listener, dashboard viewer, and foreground
251
- listener/viewer roles. PATH ordering alone is never accepted as identity evidence.
252
-
253
- The harness is a fail-closed release gate. A missing frame, non-PTY execution,
254
- wrong journey footer, substituted artifact, nonzero exit, absent terminal
255
- restore sequence, failed post-exit health assertion, timeout, or oversized
256
- transcript fails the command. Its controls must remain demonstrably bidirectional:
257
- the current client/server pairing passes; and a deliberately wrong server
258
- integrity fails before either journey starts.
259
-
260
- The terminal-restore negative control is specifically the packed pre-#146 client
261
- from commit `81da7b970ffb4e76a35c7bc551c419fec702a3b6` composed with published
262
- `borgmcp-server@0.2.0`, registry integrity
263
- `sha512-squb0+vdy0q7l/4FeV7OTvSm7OiFWGsAjGhcVEXYrQc9K/8jJYduqPS90VBwaJRT6z221Gxf3xQ5SeZe/Qoncw==`.
264
- That composition must fail on the missing cursor-restore sequence. A newer server
265
- candidate can mask the old client's defect and is not a valid counterpart for
266
- this negative control.
267
-
268
- ```sh
269
- npm run release:exercise -- \
270
- --server borgmcp-server@0.2.0 \
271
- --server-integrity 'sha512-squb0+vdy0q7l/4FeV7OTvSm7OiFWGsAjGhcVEXYrQc9K/8jJYduqPS90VBwaJRT6z221Gxf3xQ5SeZe/Qoncw==' \
272
- --client-tarball /absolute/path/to/pre-146-borgmcp-2.1.1.tgz
273
- ```
274
-
275
70
  The only trigger is a protected annotated `v<package version>` tag. Manual
276
- dispatch is intentionally absent so a second run cannot rebuild or publish an
277
- existing tag. The workflow rejects reruns, root `.npmrc`
71
+ dispatch is intentionally absent. The workflow rejects root `.npmrc`
278
72
  configuration, non-tag events, lightweight or malformed tags, version mismatch,
279
73
  source/tag mismatch, and tags whose commits are not on protected `main`.
280
74
 
@@ -283,7 +77,8 @@ The unprivileged `verify` job performs one sequence:
283
77
  1. Verify the public-source boundary, extraction readiness, exact shared-package
284
78
  pin, and canonical registry lock metadata.
285
79
  2. Install the lockfile once with lifecycle scripts disabled and audit it.
286
- 3. Run type checks, tests, and one readable build; reject generated `dist` drift.
80
+ 3. Run one readable build and reject generated `dist` drift. Tests already ran
81
+ in CI on the exact source.
287
82
  4. Produce one npm tarball.
288
83
  5. Verify that tarball once for package identity, license/notice, source and map
289
84
  completeness, executable bins, archive safety, dependency integrity, and
@@ -294,14 +89,6 @@ The unprivileged `verify` job performs one sequence:
294
89
  7. Upload only the tarball and its verifier-generated report as the same-run
295
90
  release artifact.
296
91
 
297
- After `verify` succeeds, the designated Queen operator alone approves the
298
- `npm-publish` environment. There is no separate pre-stage exact-artifact
299
- Security gate: the verify job is the mechanical authority for the exact bytes
300
- that the stage job consumes. Environment approval authorizes submitting those
301
- bytes to npm's private staged-publishing service; it does not make the version
302
- public, permit a rerun or rebuilt artifact, or authorize approval by another
303
- actor.
304
-
305
92
  The protected `publish` job alone receives `id-token: write`. It downloads the
306
93
  same-run artifact and rejects a report whose package name or version differs
307
94
  from the release, a version that already exists, an unclaimed package, or an
@@ -313,48 +100,15 @@ package.
313
100
 
314
101
  Successful completion of the workflow means npm accepted the immutable staged
315
102
  tarball. It does not mean the version is public. Stage acceptance consumes the
316
- tagged attempt and version under the burned-version rule, but must not trigger
103
+ version, but must not trigger
317
104
  release announcements, issue closure, consumer pins, site synchronization, or
318
105
  claims that the version was published. There is no workflow registry readback:
319
106
  stage inspection and approval require an interactive npm identity and cannot use
320
107
  the workflow's OIDC credential.
321
108
 
322
- ### Coupled stage approval
323
-
324
- Before approving anything, the operator requires successful stage workflows for
325
- `borgmcp-shared`, `borgmcp-server`, and `borgmcp`. Use authenticated
326
- `npm stage list` and `npm stage view <UUID>` to record and inspect all three stage
327
- UUIDs and verify each package, version, and eventual `latest` tag. For every
328
- package, `npm stage download <UUID>` is mandatory; compute its SHA-512 SRI and
329
- require an exact match with that workflow run's same-run artifact report. Bind
330
- the source annotated tag object, commit, and workflow run separately from GitHub
331
- evidence, because npm stage inspection does not expose them. Exercise the exact
332
- downloaded coupled set before approval. Confirm that public `latest`, public
333
- `versions`, and client update resolution still expose the prior coherent release
334
- set. If a pending stage appears on any public surface, approve nothing and halt
335
- this mechanism before consumers can observe it.
336
-
337
- Approve the verified UUIDs in one operator session, using interactive 2FA, in
338
- this exact order:
339
-
340
- 1. `npm stage approve <shared-stage-uuid>`
341
- 2. `npm stage approve <server-stage-uuid>`
342
- 3. `npm stage approve <client-stage-uuid>`
343
-
344
- The approvals are not atomic. Shared approval leaves existing client and server
345
- pins coherent. Server approval opens a bounded mismatch window between public
346
- server `latest` and client `latest`; client approval closes it. After an approval
347
- returns successfully, continue only while the next stage remains valid. For an
348
- ambiguous result, inspect authenticated stage state and canonical public package
349
- version/integrity before acting; never repeat an approval blindly.
350
-
351
- The terminal release boundary is successful interactive stage approval followed
352
- by canonical registry visibility, integrity verification, and registry
353
- provenance-attestation inspection. Stage inspection cannot establish provenance
354
- before approval; Trusted Publishing carries it to the live package, where the
355
- registry attestation is authoritative. Only then may the release be announced,
356
- issues closed, consumer pins or site data synchronized, or the version described
357
- as published.
109
+ Inspect the npm stage, then approve it interactively. This stage approval is the
110
+ sole human publication boundary. Confirm canonical live package visibility and
111
+ integrity before announcing the release or updating dependent packages.
358
112
 
359
113
  After all three packages cross that boundary, create their GitHub Releases in
360
114
  the same shared → server → client operator session. In each package repository,
@@ -364,46 +118,16 @@ run:
364
118
  GITHUB_TOKEN="$(gh auth token)" node scripts/create-github-release.mjs <version>
365
119
  ```
366
120
 
367
- The script binds the annotated tag, merged release pull request, successful tag
368
- workflow artifact, and live npm integrity before creating the Release. It copies
369
- the merged release pull request body verbatim and refuses an existing Release.
370
-
371
- Separately, once the release is installable from the canonical registry, install
372
- it into an isolated prefix and exercise the real user update path end to end.
373
- This is product verification, not candidate validation: failure routes a new
374
- reviewed fix and never invalidates, rebuilds, retags, or reruns the immutable
375
- release. Do not repeat byte comparisons, packed-version checks, source-tree
376
- verification, dist-tag readback, or provenance reconstruction that the
377
- exact-artifact `verify` and stage jobs already completed. The canonical
378
- live-version integrity check is required because it establishes the approval
379
- boundary; it is not a duplicate candidate verification.
380
-
381
- No separate checksum file is needed: the tarball verifier records canonical
382
- SHA-512 SRI in the artifact report. GitHub's same-run artifact transport and the
383
- report bind the reviewed candidate without repeated SHA512 choreography.
384
-
385
- Rely on npm Trusted Publishing. Perform the one authoritative registry
386
- provenance-attestation inspection at the approved-live boundary; do not
387
- reconstruct DSSE, in-toto, SLSA, workflow-ref, or builder statements locally or
388
- add redundant provenance readback.
389
- Do not add cross-run tuple variables, cross-run artifact selection, duplicate
390
- builds, duplicate package verification, checksum bundles, or SBOM ceremony.
121
+ The script binds the annotated tag, live npm package and integrity, and Release
122
+ absence before creating the Release. It reads
123
+ the curated `docs/releases/<version>.md` notes from the exact tagged commit,
124
+ renders them under `News and fixes`, and refuses missing or blank notes and an
125
+ existing Release.
391
126
 
392
127
  ## Stop And Recovery Conditions
393
128
 
394
- Stop when source, settings, ownership, tag, artifact, test, audit, review, or
395
- authorization evidence is missing or inconsistent. Never move or reuse a failed
396
- tag, rerun a failed release workflow, overwrite an npm version, unpublish to hide
397
- a failure, or substitute a local rebuild. Recovery starts from a fresh reviewed
398
- source change and, after any registry mutation, a separately authorized version.
399
-
400
- Before any approval, missing or inconsistent stage evidence means approve none.
401
- Reject all three stages only when abandoning the coupled candidate; Borg treats
402
- every rejected or unusable tagged stage as burned, even though npm permits some
403
- re-staging. Replacement versions require fresh review, tags, and stage runs.
404
- After shared approval but before server approval, stopping is compatibility-safe
405
- for the existing client/server pair, but shared is immutable and any repair uses
406
- fresh versions. After server approval, prioritize the already-verified client
407
- approval because the public pair is mismatched. If that client stage cannot be
408
- approved, do not reject it or rerun the tag; prepare newly reviewed matching
409
- server/client recovery versions and state the live mismatch window explicitly.
129
+ Stop when source, settings, ownership, tag, artifact, test, audit, or review
130
+ evidence is missing or inconsistent. Never move or reuse a tag,
131
+ overwrite an npm version, unpublish to hide a failure, or substitute a local
132
+ rebuild. A pre-stage workflow failure may be rerun against the same immutable
133
+ tag. After npm accepts a stage, recovery uses a fresh reviewed version.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borgmcp",
3
- "version": "3.11.1",
3
+ "version": "3.12.0",
4
4
  "description": "Coordinate AI coding agents in shared cubes. Works with Claude Code, Codex, and OpenCode.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -48,6 +48,7 @@
48
48
  "README.md",
49
49
  "LICENSE",
50
50
  "NOTICE",
51
+ "THIRD_PARTY_NOTICES.md",
51
52
  "SECURITY.md",
52
53
  "CONTRIBUTING.md"
53
54
  ],
@@ -71,7 +72,7 @@
71
72
  },
72
73
  "dependencies": {
73
74
  "@modelcontextprotocol/sdk": "^1.0.4",
74
- "borgmcp-shared": "0.11.0",
75
+ "borgmcp-shared": "0.12.2",
75
76
  "chalk": "^5.3.0",
76
77
  "prompts": "^2.4.2",
77
78
  "which": "^4.0.0"
@@ -40,6 +40,7 @@ import {
40
40
  resumeLocalBorgServerEnrollment,
41
41
  sendBorgServerAttach,
42
42
  } from './server-handshake.js';
43
+ import { advanceLocalServerCursor } from './local-server-cursor.js';
43
44
  import {
44
45
  findIncompleteSiblingAttempt,
45
46
  observeSeat,
@@ -483,6 +484,16 @@ export function buildDefaultAssimilateDeps(
483
484
  );
484
485
  }
485
486
  }
487
+ if (prepared.initialLogCursor) {
488
+ const binding = {
489
+ origin: apiUrl,
490
+ trustIdentity: serverTrustIdentity,
491
+ cubeId: prepared.cube.id,
492
+ droneId: prepared.drone.id,
493
+ };
494
+ await advanceLocalServerCursor(binding, prepared.initialLogCursor);
495
+ await advanceLocalServerCursor({ ...binding, purpose: 'stream' }, prepared.initialLogCursor);
496
+ }
486
497
  return {
487
498
  cube_id: prepared.cube.id,
488
499
  drone_id: prepared.drone.id,
@@ -6,7 +6,7 @@ import {
6
6
  } from './cubes.js';
7
7
  import { CodexAppServerClient } from './codex-app-server.js';
8
8
  import { checkCodexBridgeHealthy } from './codex-remote.js';
9
- import { hasPendingWakeActivity } from './remote-client.js';
9
+ import { hasPendingWakeActivity, hasPendingWakeEntry } from './remote-client.js';
10
10
  import {
11
11
  BORG_CODEX_REMOTE_WAKE_ENV,
12
12
  resolveSessionAgentKind,
@@ -98,6 +98,7 @@ let wakeInFlight = false;
98
98
  const pendingWakeRequests: Array<{
99
99
  reason: string;
100
100
  deliveryIdentity?: string;
101
+ sourceEntryId?: string;
101
102
  deps: CodexWakeDeps;
102
103
  }> = [];
103
104
  const deliveredWakeKeys = new Set<string>();
@@ -108,6 +109,8 @@ const DELIVERED_WAKE_KEY_CAP = 100;
108
109
  // (mid-turn thread) or missed (transient error) into ONE retried-until-delivered
109
110
  // drain. The coalesce gate means a burst collapses to one poller, not N.
110
111
  let retryDrainInFlight = false;
112
+ const retryDrainSourceEntryIds = new Set<string>();
113
+ let retryDrainHasUnscopedWork = false;
111
114
 
112
115
  // gh#857 WI-2: timestamp of the last SUCCESSFUL wake delivery (per-entry OR
113
116
  // retry-drain OR heartbeat). The heartbeat reads this (shouldFireHeartbeat) to
@@ -192,6 +195,7 @@ export interface CodexWakeDeps {
192
195
  // Per-entry and retry-drain paths do not use this: their pending obligation is
193
196
  // already established by a concrete delivered/deferred event.
194
197
  hasPendingWork?: (active: ActiveCube) => Promise<boolean>;
198
+ hasPendingEntry?: (active: ActiveCube, entryId: string) => Promise<boolean>;
195
199
  // gh#861 finding 2: lease-ownership gate for the heartbeat tick — a lease-LOSING
196
200
  // duplicate child must NOT tick/inject (symmetry with the per-entry path, which
197
201
  // only fires inside an SSE session that holds the stream lease). Heartbeat-only;
@@ -280,10 +284,11 @@ export function wakeCodexViaAppServer(
280
284
  env: NodeJS.ProcessEnv = process.env,
281
285
  deps: CodexWakeDeps = {},
282
286
  deliveryIdentity?: string,
287
+ sourceEntryId?: string,
283
288
  ): void {
284
289
  const target = resolveCodexWakeTarget(env);
285
290
  if (!target.enabled) return;
286
- pendingWakeRequests.push({ reason, deliveryIdentity, deps });
291
+ pendingWakeRequests.push({ reason, deliveryIdentity, sourceEntryId, deps });
287
292
  if (wakeInFlight) return;
288
293
 
289
294
  wakeInFlight = true;
@@ -295,21 +300,30 @@ export function wakeCodexViaAppServer(
295
300
  async function drainCodexWakeQueue(): Promise<void> {
296
301
  while (pendingWakeRequests.length > 0) {
297
302
  const request = pendingWakeRequests.shift()!;
298
- await wakeCodexTargeted(request.reason, request.deliveryIdentity, request.deps);
303
+ await wakeCodexTargeted(
304
+ request.reason, request.deliveryIdentity, request.sourceEntryId, request.deps,
305
+ );
299
306
  }
300
307
  }
301
308
 
302
- async function wakeCodexTargeted(reason: string, deliveryIdentity: string | undefined, deps: CodexWakeDeps): Promise<void> {
309
+ async function wakeCodexTargeted(
310
+ reason: string,
311
+ deliveryIdentity: string | undefined,
312
+ sourceEntryId: string | undefined,
313
+ deps: CodexWakeDeps,
314
+ ): Promise<void> {
303
315
  // gh#861 finding 1: another path (heartbeat/retry-drain) is mid-inject into the
304
316
  // same thread — defer to the retry-drain so this entry isn't double-injected nor
305
317
  // lost (the drain re-syncs the whole burst via the server read-cursor).
306
318
  if (!tryAcquireInjectLock()) {
307
- scheduleRetryDrain(deps);
319
+ scheduleRetryDrain(deps, sourceEntryId);
308
320
  return;
309
321
  }
310
322
  try {
311
323
  const active = await (deps.getActiveCube ?? getActiveCube)();
312
324
  if (!active) return;
325
+ const pendingEntry = deps.hasPendingEntry ?? hasPendingWakeEntry;
326
+ if (sourceEntryId && !(await pendingEntry(active, sourceEntryId))) return;
313
327
  // gh#855: resolve FRESH (live env socket + re-resolved thread), falling back
314
328
  // to the launch-recorded file only when the env socket is absent.
315
329
  const resolved = await resolveFreshCodexWakeTarget(active, deps);
@@ -326,9 +340,10 @@ async function wakeCodexTargeted(reason: string, deliveryIdentity: string | unde
326
340
  // now. Schedule the retry-drain (coalesced, retried-until-delivered) so
327
341
  // the burst's entries are drained once the thread goes idle; codex has no
328
342
  // on-disk tail fallback like Claude's borg-inbox-monitor.
329
- scheduleRetryDrain(deps);
343
+ scheduleRetryDrain(deps, sourceEntryId);
330
344
  return;
331
345
  }
346
+ if (sourceEntryId && !(await pendingEntry(active, sourceEntryId))) return;
332
347
  await client.startTurn(threadId, reason);
333
348
  rememberDeliveredWake(wakeKey);
334
349
  markDelivered(deps);
@@ -340,7 +355,7 @@ async function wakeCodexTargeted(reason: string, deliveryIdentity: string | unde
340
355
  // swallowed (the old best-effort drop let a single blip lose an entry).
341
356
  // Schedule the retry-drain so the wake is retried-until-delivered; the SSE
342
357
  // stream is never broken (this is fire-and-forget).
343
- scheduleRetryDrain(deps);
358
+ scheduleRetryDrain(deps, sourceEntryId);
344
359
  } finally {
345
360
  releaseInjectLock();
346
361
  }
@@ -357,7 +372,9 @@ async function wakeCodexTargeted(reason: string, deliveryIdentity: string | unde
357
372
  * (wakeRetryExpired); the gh#857 WI-2 heartbeat is the backstop beyond that.
358
373
  * Never throws into the SSE path (fire-and-forget).
359
374
  */
360
- function scheduleRetryDrain(deps: CodexWakeDeps): void {
375
+ function scheduleRetryDrain(deps: CodexWakeDeps, sourceEntryId?: string): void {
376
+ if (sourceEntryId) retryDrainSourceEntryIds.add(sourceEntryId);
377
+ else retryDrainHasUnscopedWork = true;
361
378
  if (retryDrainInFlight) return; // coalesce: one loop covers all deferred/missed wakes
362
379
  retryDrainInFlight = true;
363
380
  void runRetryDrainLoop(deps).finally(() => {
@@ -383,6 +400,15 @@ async function runRetryDrainLoop(deps: CodexWakeDeps): Promise<void> {
383
400
  try {
384
401
  const active = await (deps.getActiveCube ?? getActiveCube)();
385
402
  if (!active) continue; // no active cube yet → keep retrying (until age cap)
403
+ const pendingEntry = deps.hasPendingEntry ?? hasPendingWakeEntry;
404
+ for (const entryId of retryDrainSourceEntryIds) {
405
+ try {
406
+ if (!(await pendingEntry(active, entryId))) retryDrainSourceEntryIds.delete(entryId);
407
+ } catch {
408
+ // Retain the obligation until unread state can be checked.
409
+ }
410
+ }
411
+ if (!retryDrainHasUnscopedWork && retryDrainSourceEntryIds.size === 0) return;
386
412
  // gh#855: same FRESH resolution as the per-entry wake, so a stale launch
387
413
  // probe can't defeat the retry-drain either.
388
414
  const resolved = await resolveFreshCodexWakeTarget(active, deps);
@@ -395,7 +421,13 @@ async function runRetryDrainLoop(deps: CodexWakeDeps): Promise<void> {
395
421
  if (thread?.status?.type === 'active') {
396
422
  continue; // re-defer: still mid-turn (backoff before next poll)
397
423
  }
424
+ for (const entryId of retryDrainSourceEntryIds) {
425
+ if (!(await pendingEntry(active, entryId))) retryDrainSourceEntryIds.delete(entryId);
426
+ }
427
+ if (!retryDrainHasUnscopedWork && retryDrainSourceEntryIds.size === 0) return;
398
428
  await client.startTurn(threadId, CODEX_CATCHUP_PROMPT);
429
+ retryDrainSourceEntryIds.clear();
430
+ retryDrainHasUnscopedWork = false;
399
431
  markDelivered(deps);
400
432
  return; // drain delivered → server read-cursor drains all unread → done
401
433
  } finally {
@@ -515,6 +547,8 @@ export function resetCodexWakeForTests(): void {
515
547
  deliveredWakeKeys.clear();
516
548
  deliveredWakeKeyOrder.length = 0;
517
549
  retryDrainInFlight = false;
550
+ retryDrainSourceEntryIds.clear();
551
+ retryDrainHasUnscopedWork = false;
518
552
  lastDeliveredAt = null;
519
553
  heartbeatInFlight = false;
520
554
  injectInFlight = false;
@@ -1,5 +1,5 @@
1
1
  export const CUBE_ACTIVITY_RESUME_WAKE_MESSAGE =
2
- 'Borg cube activity arrived while you were busy. Reading cube messages does not end your current task. Drain `borg_read-log unread_only=true` until caught up, handle actionable entries, then RESUME the interrupted work.';
2
+ 'Borg cube activity arrived while you were busy. Reading cube messages does not end your current task. Drain `borg_read-log unread_only=true` until caught up, handle actionable entries, then RESUME the interrupted work. If the unread drain is empty, resume silently without a liveness post or full regen.';
3
3
 
4
4
  export function formatCubeActivityWakeMessage(detail: string): string {
5
5
  return `${CUBE_ACTIVITY_RESUME_WAKE_MESSAGE}\n${detail}`;