borgmcp 2.13.0 → 2.14.1

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/dist/codex-app-wake.d.ts +3 -3
  2. package/dist/codex-app-wake.d.ts.map +1 -1
  3. package/dist/codex-app-wake.js +10 -8
  4. package/dist/codex-app-wake.js.map +1 -1
  5. package/dist/cube-activity-wake-copy.d.ts +3 -0
  6. package/dist/cube-activity-wake-copy.d.ts.map +1 -0
  7. package/dist/cube-activity-wake-copy.js +5 -0
  8. package/dist/cube-activity-wake-copy.js.map +1 -0
  9. package/dist/inbox-monitor.d.ts +13 -1
  10. package/dist/inbox-monitor.d.ts.map +1 -1
  11. package/dist/inbox-monitor.js +29 -3
  12. package/dist/inbox-monitor.js.map +1 -1
  13. package/dist/log-stream.d.ts +2 -1
  14. package/dist/log-stream.d.ts.map +1 -1
  15. package/dist/log-stream.js +71 -17
  16. package/dist/log-stream.js.map +1 -1
  17. package/dist/opencode-drone.d.ts +4 -3
  18. package/dist/opencode-drone.d.ts.map +1 -1
  19. package/dist/opencode-drone.js +14 -5
  20. package/dist/opencode-drone.js.map +1 -1
  21. package/dist/opencode-wake-copy.d.ts.map +1 -1
  22. package/dist/opencode-wake-copy.js +3 -1
  23. package/dist/opencode-wake-copy.js.map +1 -1
  24. package/dist/roster-render.d.ts +2 -0
  25. package/dist/roster-render.d.ts.map +1 -1
  26. package/dist/roster-render.js +4 -4
  27. package/dist/roster-render.js.map +1 -1
  28. package/docs/EXTRACTION_PROVENANCE.md +8 -8
  29. package/docs/LOCAL_SERVER.md +3 -7
  30. package/docs/RELEASING.md +15 -6
  31. package/package.json +2 -2
  32. package/src/codex-app-wake.ts +15 -8
  33. package/src/cube-activity-wake-copy.ts +6 -0
  34. package/src/inbox-monitor.ts +36 -3
  35. package/src/log-stream.ts +78 -20
  36. package/src/opencode-drone.ts +16 -5
  37. package/src/opencode-wake-copy.ts +4 -1
  38. package/src/roster-render.ts +6 -4
@@ -126,7 +126,8 @@ export function renderRoster(inputs) {
126
126
  : '';
127
127
  const regenCountMarker = typeof d.regen_count === 'number' ? ` · \`regen-count:${d.regen_count}\`` : '';
128
128
  if (resolvedSince) {
129
- // T2.1 awake/stale column. `seen_since === true` drone called a
129
+ // T2.1 awake/stale column. When available, wake_state is the server's
130
+ // richer state; older servers fall back to seen_since. `seen_since === true` → drone called a
130
131
  // tool after the resolved timestamp; treat as awake. False or
131
132
  // missing → stale. Missing should not happen when the server
132
133
  // echoed a since, but defending against a shape mismatch is
@@ -140,9 +141,8 @@ export function renderRoster(inputs) {
140
141
  // probe call — pure redundancy. The per-row `last seen X ago`
141
142
  // field carries the diagnostic detail for "how stale is this
142
143
  // particular drone."
143
- const isAwake = d.seen_since === true;
144
- const marker = isAwake ? '`awake`' : '`stale`';
145
- lines.push(`- **${d.label}**${addr} (Role: ${roleName}) — last seen ${lastSeen} · ${marker}${regenCountMarker}${wakePathMarker}${wakePathClassMarker}`);
144
+ const marker = d.wake_state ?? (d.seen_since === true ? 'awake' : 'stale');
145
+ lines.push(`- **${d.label}**${addr} (Role: ${roleName}) last seen ${lastSeen} · \`${marker}\`${regenCountMarker}${wakePathMarker}${wakePathClassMarker}`);
146
146
  }
147
147
  else {
148
148
  lines.push(`- **${d.label}**${addr} (Role: ${roleName}) — last seen ${lastSeen}${regenCountMarker}${wakePathMarker}${wakePathClassMarker}`);
@@ -1 +1 @@
1
- {"version":3,"file":"roster-render.js","sourceRoot":"","sources":["../src/roster-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAE3D,MAAM,CAAC,MAAM,yBAAyB,GACpC,qJAAqJ,CAAC;AA6CxJ;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,iBAAiB,GAA2B;IAChD,QAAQ,EAAE,YAAY;IACtB,IAAI,EAAE,cAAc;CACrB,CAAC;AAEF,MAAM,UAAU,oBAAoB,CAClC,QAAgB,EAChB,SAAoC;IAEpC,MAAM,QAAQ,GAAG,SAAS,KAAK,QAAQ;QACrC,CAAC,CAAC,aAAa;QACf,CAAC,CAAC,SAAS,KAAK,OAAO;YACrB,CAAC,CAAC,OAAO;YACT,CAAC,CAAC,SAAS,KAAK,UAAU;gBACxB,CAAC,CAAC,UAAU;gBACZ,CAAC,CAAC,cAAc,CAAC;IACvB,OAAO,SAAS,QAAQ,iBAAiB,QAAQ,EAAE,CAAC;AACtD,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,KAAqE;IAC1G,IAAI,KAAK,CAAC,iBAAiB,IAAI,KAAK,CAAC,mBAAmB,EAAE,CAAC;QACzD,OAAO,iBAAiB,KAAK,CAAC,iBAAiB,cAAc,KAAK,CAAC,mBAAmB,EAAE,CAAC;IAC3F,CAAC;IACD,IAAI,KAAK,CAAC,iBAAiB;QAAE,OAAO,iBAAiB,KAAK,CAAC,iBAAiB,EAAE,CAAC;IAC/E,IAAI,KAAK,CAAC,mBAAmB;QAAE,OAAO,yBAAyB,KAAK,CAAC,mBAAmB,EAAE,CAAC;IAC3F,OAAO,4BAA4B,CAAC;AACtC,CAAC;AAED,SAAS,aAAa,CACpB,KAAkB,EAClB,KAAgC;IAEhC,IAAI,KAAK,CAAC,yBAAyB,KAAK,IAAI;QAAE,OAAO,cAAc,CAAC;IACpE,OAAO,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,4BAA4B,CAAC,KAAK,CAAC,CAAC;AACzE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,4BAA4B,CAAC,KAAa;IACxD,MAAM,OAAO,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;IACzC,MAAM,cAAc,GAAG,OAAO,CAAC,OAAO,CACpC,oCAAoC,EACpC,WAAW,CACZ,CAAC;IACF,OAAO,cAAc,CAAC,OAAO,CAC3B,yCAAyC,EACzC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,SAAS,CAAC,CAC1C,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,0BAA0B,CACxC,KAAkB,EAClB,OAAoC,EAAE;IAEtC,MAAM,KAAK,GAAG,KAAK,CAAC,UAAU,KAAK,QAAQ;QACzC,CAAC,CAAC,aAAa;QACf,CAAC,CAAC,KAAK,CAAC,UAAU,KAAK,OAAO;YAC5B,CAAC,CAAC,OAAO;YACT,CAAC,CAAC,KAAK,CAAC,UAAU,KAAK,UAAU;gBAC/B,CAAC,CAAC,UAAU;gBACZ,CAAC,CAAC,IAAI,CAAC;IACb,MAAM,KAAK,GAAG;QACZ,sBAAsB,aAAa,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE;QACnD,2BAA2B,aAAa,CAAC,KAAK,EAAE,KAAK,CAAC,cAAc,CAAC,EAAE;QACvE,yBAAyB,aAAa,CAAC,KAAK,EAAE,KAAK,CAAC,iBAAiB,CAAC,EAAE;KACzE,CAAC;IACF,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;QACvB,MAAM,MAAM,GAAG,KAAK,CAAC,mBAAmB,EAAE,OAAO,CAAC,cAAc,EAAE,EAAE,CAAC,CAAC;QACtE,KAAK,CAAC,IAAI,CAAC,mBAAmB,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;IAChE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAgBD,MAAM,UAAU,YAAY,CAAC,MAA0B;IACrD,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC;IACpE,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC/C,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IAE7C,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,qBAAqB,QAAQ,EAAE,CAAC,CAAC;IAC5C,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,IAAI,yBAAyB,GAAG,CAAC,CAAC;IAC7C,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,IAAI,aAAa,EAAE,CAAC;QAClB,kEAAkE;QAClE,sDAAsD;QACtD,KAAK,CAAC,IAAI,CACR,yBAAyB,aAAa,KAAK,QAAQ,CACjD,aAAa,CACd,gEAAgE,CAClE,CAAC;QACF,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjB,CAAC;IAED,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;QACnB,KAAK,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC;QACtC,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAED,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QACrC,MAAM,QAAQ,GAAG,IAAI,EAAE,IAAI,IAAI,SAAS,CAAC;QACzC,6EAA6E;QAC7E,MAAM,IAAI,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,uBAAuB,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7D,MAAM,QAAQ,GAAG,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACvC,MAAM,cAAc,GAClB,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,SAAS,KAAK,MAAM;YACnC,CAAC,CAAC,kBAAkB,iBAAiB,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,SAAS,IAAI;YACrE,CAAC,CAAC,EAAE,CAAC;QACT,MAAM,mBAAmB,GACvB,CAAC,CAAC,qBAAqB,IAAI,CAAC,CAAC,qBAAqB,KAAK,aAAa;YAClE,CAAC,CAAC,wBAAwB,CAAC,CAAC,qBAAqB,IAAI;YACrD,CAAC,CAAC,EAAE,CAAC;QACT,MAAM,gBAAgB,GACpB,OAAO,CAAC,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,WAAW,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QACjF,IAAI,aAAa,EAAE,CAAC;YAClB,kEAAkE;YAClE,8DAA8D;YAC9D,6DAA6D;YAC7D,4DAA4D;YAC5D,yDAAyD;YACzD,8CAA8C;YAC9C,EAAE;YACF,iEAAiE;YACjE,6DAA6D;YAC7D,gEAAgE;YAChE,6DAA6D;YAC7D,8DAA8D;YAC9D,6DAA6D;YAC7D,qBAAqB;YACrB,MAAM,OAAO,GAAG,CAAC,CAAC,UAAU,KAAK,IAAI,CAAC;YACtC,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC;YAC/C,KAAK,CAAC,IAAI,CACR,OAAO,CAAC,CAAC,KAAK,KAAK,IAAI,WAAW,QAAQ,iBAAiB,QAAQ,MAAM,MAAM,GAAG,gBAAgB,GAAG,cAAc,GAAG,mBAAmB,EAAE,CAC5I,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,IAAI,CACR,OAAO,CAAC,CAAC,KAAK,KAAK,IAAI,WAAW,QAAQ,iBAAiB,QAAQ,GAAG,gBAAgB,GAAG,cAAc,GAAG,mBAAmB,EAAE,CAChI,CAAC;QACJ,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,GAAG,0BAA0B,CAAC,CAAC,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACxE,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}
1
+ {"version":3,"file":"roster-render.js","sourceRoot":"","sources":["../src/roster-render.ts"],"names":[],"mappings":"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"}
@@ -22,7 +22,7 @@ The extraction copied the monorepo's `client/src/` production boundary and top-l
22
22
  recovery, `2.0.2`, `2.0.3`, `2.0.4`, `2.0.5`, and `2.0.6` successors were
23
23
  published and registry-verified. The immutable `2.0.7` workflow failed before
24
24
  package creation or npm publication. The `2.0.8`, `2.0.9`, `2.0.10`, `2.0.11`,
25
- `2.1.0`, and `2.1.1` successors were published, and `2.2.0` was subsequently published, and `2.3.0` was subsequently published, and `2.4.0` was subsequently published, and `2.4.1` was subsequently published, and `2.5.0` was subsequently published, and `2.6.0` was subsequently published, and `2.6.1` was subsequently published, and `2.7.0` was subsequently published, and `2.7.1` was subsequently published, and `2.7.2` was subsequently published, and `2.7.3` was subsequently published, and `2.8.0` was subsequently published, and `2.9.0` was subsequently published, and `2.10.0` was subsequently published, and the immutable `2.10.1` release attempt failed before artifact creation or publication, and `2.10.2` was subsequently published, so the next candidate identity is `2.11.0`. Extraction and
25
+ `2.1.0`, and `2.1.1` successors were published, and `2.2.0` was subsequently published, and `2.3.0` was subsequently published, and `2.4.0` was subsequently published, and `2.4.1` was subsequently published, and `2.5.0` was subsequently published, and `2.6.0` was subsequently published, and `2.6.1` was subsequently published, and `2.7.0` was subsequently published, and `2.7.1` was subsequently published, and `2.7.2` was subsequently published, and `2.7.3` was subsequently published, and `2.8.0` was subsequently published, and `2.9.0` was subsequently published, and `2.10.0` was subsequently published, and the immutable `2.10.1` release attempt failed before artifact creation or publication. The `2.10.2`, `2.11.0`, `2.12.1`, and `2.13.0` successors were published; the immutable `2.12.0` attempt failed before artifact creation or publication and was superseded by `2.12.1`. The current release identity is `2.14.1`. Extraction and
26
26
  versioning do not authorize publication.
27
27
 
28
28
  ## Review Holds
@@ -37,16 +37,16 @@ artifact reaches no hosted authority.
37
37
 
38
38
  Local enrollment now uses the reviewed client-generated credential/retry
39
39
  contract, with a pre-request `PENDING` record in the local 0600-permission seat
40
- store, exact-tuple ambiguous retry, and verified activation. The contract now
41
- resolves to the audited registry `borgmcp-shared@0.8.1` with integrity
42
- `sha512-0+UfSDnAO0mWYIqxzzppekatya1gCF9ntZmuk7VVr2Hnf9QeM1S7uPLP/TWwDSTLtczB4EUoKA71E1WZJH2mbg==`.
40
+ store, exact-tuple ambiguous retry, and verified activation. The current client
41
+ candidate resolves to the audited registry `borgmcp-shared@0.9.0` with integrity
42
+ `sha512-bfZPP9JGgBQrCFoZetabqKHc8HLaUqHVR3GJLb/1F1oon7z/B4el4aeBHVvXlxN9+2G7kU/ymPZ/K25nVQapmQ==`.
43
43
  The coupled `borgmcp-server@0.9.0` release was published on 2026-08-02 for the
44
44
  client `borgmcp@2.10.0` line. Its annotated tag object
45
45
  `7021a2e1f551aa0c1716c9e3f29f7b8bc4b0ccb0` peels to protected-main commit
46
46
  `734051375d77013f8fd5b396af12feb53d5af96d`; the same-run artifact integrity is
47
47
  `sha512-lOxIPg3WcjSBte46iTM7SAFVN6Y0b2oizOKAJ9Q1EijBdOlmsh/cQlpUIaa8F2UJPaTDFsXy3M64gpGs6XgKzA==`
48
- in the `borgmcp-server-0.9.0-release` registry decision. Both releases pin the
49
- audited `borgmcp-shared@0.8.1` dependency above. Client `borgmcp@2.10.2` is published. The immutable `v2.0.7`
50
- attempt failed before publication and remains preserved. Publication of the next
51
- candidate remains gated by reviewed `v2.13.0` source, a fresh annotated tag, and
48
+ in the `borgmcp-server-0.9.0-release` registry decision. That server and its
49
+ client line pin historical `borgmcp-shared` version `0.8.1` and remain
50
+ immutable. Client `borgmcp@2.14.0` is published. The immutable `v2.0.7`
51
+ attempt failed before publication and remains preserved. The current release identity and publication gate remain governed by the reviewed `v2.14.1` source, a fresh annotated tag, and
52
52
  the exact-artifact and protected-publication gates.
@@ -235,10 +235,6 @@ The default discovery endpoint is `https://127.0.0.1:7091`. Explicit `--host` va
235
235
 
236
236
  ## Release status
237
237
 
238
- This client candidate pins published `borgmcp-shared@0.8.1` and requires the
239
- matching coupled client/server release for protocol v7 repository resolve,
240
- association, and cube deletion. The already-published server 0.8.0 retains its
241
- immutable older shared dependency pin and is not that coupled artifact. The server owner-enrollment, repository resolve/association,
242
- cube-create, attach, restart, log, and SSE implementation must also pass the full
243
- process-level local dogfood gate. Until that gate opens the self-hosted path remains preview-only, and the client
244
- publish is deferred accordingly.
238
+ This client candidate pins published `borgmcp-shared@0.9.0`. Its release remains
239
+ coupled to a server artifact rebuilt against the same shared version, with both
240
+ artifacts required to pass the complete process-level local dogfood gate.
package/docs/RELEASING.md CHANGED
@@ -200,8 +200,18 @@ Never delete, move, replace, reuse, or rerun that tag, version, or workflow. The
200
200
  `bb8ff57162efdc777bb92f5e1f9d31aaf293d3c6`. Workflow run `30768860612`, attempt 1,
201
201
  successfully published that exact source as `borgmcp@2.10.2`; the same-run artifact report records integrity
202
202
  `sha512-uNeDidPLxZDe0E/RnQ35zaAMZwHcBbdpHrk+McQuV4lyBxoMSdf+MlBDHv7usijCtOBAUf+exlR/l4F9wVtvZg==`.
203
+ Never move, replace, reuse, or rerun that tag or workflow. The annotated `v2.13.0` tag object
204
+ `67a2fd7bf76299dccc94a3260ddfd4b688eacaa1` peels to protected-main commit
205
+ `fc5e4d9299210b70f979c7d39130a9c12ea265ea`. Workflow run `30894287863`, attempt 1,
206
+ successfully published that exact source as `borgmcp@2.13.0`; the same-run artifact report records integrity
207
+ `sha512-YuF3NwY5iBRVKhgdRiK+3GX+6O8O7S/V9g+XYN/1A0so9RLIYoQWpS91qJewqGQ59nyIARjRmk2wKhK8ODxU0Q==`.
208
+ Never move, replace, reuse, or rerun that tag or workflow. The annotated `v2.14.0` tag object
209
+ `eb9d5cb14b08b1a2eb6d48b2492acbc6cecb7bef` peels to protected-main commit
210
+ `4dd92bcbac26f460a4f137f766992e8f93843576`. Workflow run `30929322201`, attempt 1,
211
+ successfully published that exact source as `borgmcp@2.14.0`; the same-run artifact report records integrity
212
+ `sha512-haKgpQbcbaRya1XqYQaOy7yZiHMxeP8QO3fr7ahHaYDNYrDvlGmXNlW9VMtqnRJ9mtVDC5UTkeuQmCRJMzF/Tg==`.
203
213
  Never move, replace, reuse, or rerun that tag or workflow. The next candidate
204
- uses the unused `v2.13.0` identity from a fresh reviewed protected-main commit
214
+ uses the unused `v2.14.1` identity from a fresh reviewed protected-main commit
205
215
  and requires the complete release gate again.
206
216
 
207
217
  ## Release Prerequisites
@@ -213,13 +223,12 @@ Before creating the release tag, independently verify all of these conditions:
213
223
  - the extraction review confirms no private backend secrets, deployment
214
224
  configuration, customer data, local state, or duplicated shared contracts
215
225
  entered the public package;
216
- - the exact audited registry dependency `borgmcp-shared@0.8.1` remains locked to
226
+ - the exact audited registry dependency `borgmcp-shared@0.9.0` remains locked to
217
227
  its canonical tarball and integrity
218
- `sha512-0+UfSDnAO0mWYIqxzzppekatya1gCF9ntZmuk7VVr2Hnf9QeM1S7uPLP/TWwDSTLtczB4EUoKA71E1WZJH2mbg==`;
228
+ `sha512-bfZPP9JGgBQrCFoZetabqKHc8HLaUqHVR3GJLb/1F1oon7z/B4el4aeBHVvXlxN9+2G7kU/ymPZ/K25nVQapmQ==`;
219
229
  - the coupled client/server release is published only after the server artifact
220
- is rebuilt against `borgmcp-shared@0.8.1` and both pass the complete local
221
- dogfood gate; the already-published server 0.8.0 remains immutable at its
222
- older immutable shared dependency pin;
230
+ is rebuilt against `borgmcp-shared@0.9.0` and both pass the complete local
231
+ dogfood gate;
223
232
  - the selected stable client version is unused and the exact release commit is
224
233
  on protected `main`;
225
234
  - the repository and protected npm environment settings pass an operator audit;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borgmcp",
3
- "version": "2.13.0",
3
+ "version": "2.14.1",
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",
@@ -71,7 +71,7 @@
71
71
  },
72
72
  "dependencies": {
73
73
  "@modelcontextprotocol/sdk": "^1.0.4",
74
- "borgmcp-shared": "0.8.1",
74
+ "borgmcp-shared": "0.9.0",
75
75
  "chalk": "^5.3.0",
76
76
  "prompts": "^2.4.2",
77
77
  "which": "^4.0.0"
@@ -21,12 +21,16 @@ import {
21
21
  shouldFireHeartbeat,
22
22
  type CodexThreadInfo,
23
23
  } from './codex-wake-resolve.js';
24
+ import {
25
+ CUBE_ACTIVITY_RESUME_WAKE_MESSAGE,
26
+ formatCubeActivityWakeMessage,
27
+ } from './cube-activity-wake-copy.js';
24
28
 
25
29
  export const CODEX_WAKE_PROMPT =
26
- 'New Borg cube-log activity arrived.';
30
+ CUBE_ACTIVITY_RESUME_WAKE_MESSAGE;
27
31
 
28
32
  export function formatCodexWakePrompt(inboxLine: string): string {
29
- return `New Borg cube-log activity arrived:\n${inboxLine}`;
33
+ return formatCubeActivityWakeMessage(inboxLine);
30
34
  }
31
35
 
32
36
  // gh#708: STATIC catch-up/drain prompt (zero interpolation — no token/secret/PII/
@@ -36,7 +40,8 @@ export function formatCodexWakePrompt(inboxLine: string): string {
36
40
  // fire the already-shipped drain so no entry is skipped. gh#857 WI-2 reuses it as
37
41
  // the periodic heartbeat prompt.
38
42
  export const CODEX_CATCHUP_PROMPT =
39
- 'Borg cube activity arrived while you were busy. Wake triage: run `borg_read-log unread_only=true` and DRAIN — repeat until the returned page is under the limit and behind_by is 0 so no entries are skipped. Then handle actionable entries; if none, resume the prior interrupted work.';
43
+ `${formatCubeActivityWakeMessage('Wake triage: repeat the drain until the returned page is under the limit and behind_by is 0 so no entries are skipped.')} ` +
44
+ 'If there are no actionable entries, resume the prior interrupted work.';
40
45
 
41
46
  export function isCodexRemoteWakeEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
42
47
  return env[BORG_CODEX_REMOTE_WAKE_ENV] === '1';
@@ -92,6 +97,7 @@ export async function probeCodexBridgeArmed(
92
97
  let wakeInFlight = false;
93
98
  const pendingWakeRequests: Array<{
94
99
  reason: string;
100
+ deliveryIdentity?: string;
95
101
  deps: CodexWakeDeps;
96
102
  }> = [];
97
103
  const deliveredWakeKeys = new Set<string>();
@@ -272,11 +278,12 @@ async function maybePersistWakeTarget(
272
278
  export function wakeCodexViaAppServer(
273
279
  reason: string = CODEX_WAKE_PROMPT,
274
280
  env: NodeJS.ProcessEnv = process.env,
275
- deps: CodexWakeDeps = {}
281
+ deps: CodexWakeDeps = {},
282
+ deliveryIdentity?: string,
276
283
  ): void {
277
284
  const target = resolveCodexWakeTarget(env);
278
285
  if (!target.enabled) return;
279
- pendingWakeRequests.push({ reason, deps });
286
+ pendingWakeRequests.push({ reason, deliveryIdentity, deps });
280
287
  if (wakeInFlight) return;
281
288
 
282
289
  wakeInFlight = true;
@@ -288,11 +295,11 @@ export function wakeCodexViaAppServer(
288
295
  async function drainCodexWakeQueue(): Promise<void> {
289
296
  while (pendingWakeRequests.length > 0) {
290
297
  const request = pendingWakeRequests.shift()!;
291
- await wakeCodexTargeted(request.reason, request.deps);
298
+ await wakeCodexTargeted(request.reason, request.deliveryIdentity, request.deps);
292
299
  }
293
300
  }
294
301
 
295
- async function wakeCodexTargeted(reason: string, deps: CodexWakeDeps): Promise<void> {
302
+ async function wakeCodexTargeted(reason: string, deliveryIdentity: string | undefined, deps: CodexWakeDeps): Promise<void> {
296
303
  // gh#861 finding 1: another path (heartbeat/retry-drain) is mid-inject into the
297
304
  // same thread — defer to the retry-drain so this entry isn't double-injected nor
298
305
  // lost (the drain re-syncs the whole burst via the server read-cursor).
@@ -308,7 +315,7 @@ async function wakeCodexTargeted(reason: string, deps: CodexWakeDeps): Promise<v
308
315
  const resolved = await resolveFreshCodexWakeTarget(active, deps);
309
316
  if (!resolved) return;
310
317
  const { socketPath, threadId } = resolved;
311
- const wakeKey = `${threadId}\0${reason}`;
318
+ const wakeKey = `${threadId}\0${deliveryIdentity ?? reason}`;
312
319
  if (deliveredWakeKeys.has(wakeKey)) return; // dedup before opening the wake socket
313
320
  const client = makeCodexClient(socketPath, deps);
314
321
  await client.connect();
@@ -0,0 +1,6 @@
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.';
3
+
4
+ export function formatCubeActivityWakeMessage(detail: string): string {
5
+ return `${CUBE_ACTIVITY_RESUME_WAKE_MESSAGE}\n${detail}`;
6
+ }
@@ -36,11 +36,18 @@ import { chmodSync, linkSync, lstatSync, mkdirSync, readFileSync, realpathSync,
36
36
  import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
37
37
  import { createInterface } from 'node:readline';
38
38
  import { fileURLToPath } from 'node:url';
39
+ import { formatCubeActivityWakeMessage } from './cube-activity-wake-copy.js';
39
40
 
40
41
  const ENTRY_LINE_RE =
41
42
  /^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\S*)\s+(\S+)\s+\(([^)]+)\):\s*(.*)$/;
42
43
 
43
44
  export const RECENT_EMITTED_LINE_CAP = 1024;
45
+ /**
46
+ * Allow a small amount of server/host clock skew at monitor arm. Lines older
47
+ * than this boundary are historical materialized inbox content, not live
48
+ * activity for this monitor instance.
49
+ */
50
+ export const INBOX_ARM_TIMESTAMP_SKEW_MS = 5_000;
44
51
 
45
52
  export class RecentLineDeduper {
46
53
  private readonly seen = new Set<string>();
@@ -85,13 +92,38 @@ export function formatEventLine(inboxLine: string): string | null {
85
92
  return `${label} (${role}): ${summary}`;
86
93
  }
87
94
 
95
+ /**
96
+ * Return whether an inbox entry is fresh enough to have been produced after
97
+ * this monitor armed. A malformed timestamp fails closed when this guard is
98
+ * active; callers without an arm boundary retain the legacy parser behavior.
99
+ */
100
+ export function isInboxLineAtOrAfterArm(
101
+ inboxLine: string,
102
+ armTimeMs: number,
103
+ skewMs = INBOX_ARM_TIMESTAMP_SKEW_MS,
104
+ ): boolean {
105
+ const match = ENTRY_LINE_RE.exec(inboxLine);
106
+ if (!match || !Number.isFinite(armTimeMs) || !Number.isFinite(skewMs) || skewMs < 0) {
107
+ return false;
108
+ }
109
+ const timestampMs = Date.parse(match[1]);
110
+ return Number.isFinite(timestampMs) && timestampMs >= armTimeMs - skewMs;
111
+ }
112
+
88
113
  export function formatFreshEventLine(
89
114
  inboxLine: string,
90
- deduper: RecentLineDeduper
115
+ deduper: RecentLineDeduper,
116
+ includeWakeMessage = false,
117
+ armTimeMs?: number,
91
118
  ): string | null {
92
119
  const pretty = formatEventLine(inboxLine);
93
120
  if (pretty === null) return null;
94
- return deduper.remember(inboxLine) ? pretty : null;
121
+ if (armTimeMs !== undefined && !isInboxLineAtOrAfterArm(inboxLine, armTimeMs)) {
122
+ return null;
123
+ }
124
+ return deduper.remember(inboxLine)
125
+ ? includeWakeMessage ? formatCubeActivityWakeMessage(pretty) : pretty
126
+ : null;
95
127
  }
96
128
 
97
129
  export function seedDeduperFromInboxTail(
@@ -883,6 +915,7 @@ function main(): void {
883
915
  // wire behavior is identical — only the per-line projection changes.
884
916
  // `-n 0` skips backfilling history so fresh sessions don't replay
885
917
  // old entries on every restart.
918
+ const monitorArmTimeMs = Date.now();
886
919
  const deduper = new RecentLineDeduper();
887
920
  seedDeduperFromInboxTail(inboxPath, deduper);
888
921
 
@@ -914,7 +947,7 @@ function main(): void {
914
947
 
915
948
  const rl = createInterface({ input: tail.stdout, crlfDelay: Infinity });
916
949
  rl.on('line', (line) => {
917
- const pretty = formatFreshEventLine(line, deduper);
950
+ const pretty = formatFreshEventLine(line, deduper, true, monitorArmTimeMs);
918
951
  if (pretty !== null) {
919
952
  console.log(pretty);
920
953
  // Delivered → the tail is current; re-anchor the offset to the live
package/src/log-stream.ts CHANGED
@@ -55,6 +55,7 @@ import {
55
55
  startCodexHeartbeat,
56
56
  wakeCodexViaAppServer,
57
57
  } from './codex-app-wake.js';
58
+ import { formatCubeActivityWakeMessage } from './cube-activity-wake-copy.js';
58
59
  import { readBoundedResponseBody } from './server-response.js';
59
60
  import { BorgServerError } from './server-errors.js';
60
61
  import { markSeatRejected } from './seats.js';
@@ -140,6 +141,14 @@ export function setModuleInjectOpenCode(
140
141
  * ring buffer.
141
142
  */
142
143
  const RECENT_IDS_CAP = 50;
144
+
145
+ function formatOpenCodeWakePrompt(line: string, wakeNonce?: string): string {
146
+ const prompt = formatCubeActivityWakeMessage(line);
147
+ return wakeNonce === undefined
148
+ ? prompt
149
+ : `${prompt}\n\n<!-- borg-wake-nonce:${wakeNonce} -->`;
150
+ }
151
+
143
152
  export const INBOX_TAIL_LINES_CAP = 512;
144
153
  export const INBOX_TAIL_TRIM_THRESHOLD_LINES = INBOX_TAIL_LINES_CAP * 2;
145
154
 
@@ -382,7 +391,7 @@ export interface StreamDeps {
382
391
  renderedLine: string
383
392
  ) => Promise<boolean>;
384
393
  /** Optional Codex app-server wake sink; tests inject a spy. */
385
- wakeCodex?: (reason: string) => void;
394
+ wakeCodex?: (reason: string, deliveryIdentity?: string) => void;
386
395
  /** Override the heartbeat watchdog timeout. */
387
396
  heartbeatTimeoutMs?: number;
388
397
  /** Override HWM divergence grace for focused tests. */
@@ -407,7 +416,8 @@ const defaultDeps: Required<StreamDeps> = {
407
416
  getCursor: getLocalServerCursor,
408
417
  appendLine: defaultAppendLine,
409
418
  hasInboxEntryId: defaultHasInboxEntryId,
410
- wakeCodex: wakeCodexViaAppServer,
419
+ wakeCodex: (reason, deliveryIdentity) =>
420
+ wakeCodexViaAppServer(reason, process.env, {}, deliveryIdentity),
411
421
  heartbeatTimeoutMs: HEARTBEAT_TIMEOUT_MS,
412
422
  hwmDivergenceGraceMs: HWM_DIVERGENCE_GRACE_MS,
413
423
  abortSignal: new AbortController().signal,
@@ -729,7 +739,10 @@ export async function streamOnce(
729
739
  // Local mirror of the resume cursor, updated AFTER each successful
730
740
  // disk write (or dedup-recognized replay). Heartbeat-hwm comparison
731
741
  // reads this value.
732
- let lastPersistedEventId: string | null = lastEventId;
742
+ // Seed it from the persisted cursor as well as the in-memory Last-Event-ID:
743
+ // a reconnect after process restart must not treat the cursor's durable
744
+ // horizon as unknown while catchup is being replayed.
745
+ let lastPersistedEventId: string | null = cursor?.id ?? lastEventId;
733
746
  let lastBroadcastHwm: BroadcastHwm | null = null;
734
747
  let pendingHwmDivergence:
735
748
  | { hwm: BroadcastHwm; timer: NodeJS.Timeout }
@@ -759,7 +772,9 @@ export async function streamOnce(
759
772
  // a created_at would itself widen the next reconnect's window (the very
760
773
  // storm this fixes). Proven older-or-equal events are still recorded in
761
774
  // recentIds for dedup but do not move the cursor or re-fire onEventId.
762
- let lastPersistedHwm: BroadcastHwm | null = null;
775
+ let lastPersistedHwm: BroadcastHwm | null = cursor
776
+ ? { id: cursor.id, created_at: cursor.created_at }
777
+ : null;
763
778
  const markEventPersisted = (id: string, createdAt: string) => {
764
779
  const next: BroadcastHwm = { id, created_at: createdAt };
765
780
  if (
@@ -830,23 +845,47 @@ export async function streamOnce(
830
845
  ev: Extract<ParsedEvent, { type: 'log' }>
831
846
  ): Promise<'persisted-skip' | 'written'> => {
832
847
  const line = formatInboxLine(withSseEventId(ev.data, ev.id));
833
- const alreadyPersisted = isCatchingUp && (
834
- // gh#441: pass the rendered line so the dedup can also recognize LEGACY
835
- // (no-entry_id-prefix) on-disk lines, not just the [entry_id:] marker.
848
+ const deliveryId = ev.wake_nonce ?? ev.id;
849
+ const isReping = ev.wake_nonce !== undefined;
850
+ const eventHwm = ev.data?.created_at
851
+ ? { id: ev.id, created_at: ev.data.created_at }
852
+ : null;
853
+ const atOrBeforeResumeCursor = Boolean(
854
+ isCatchingUp &&
855
+ lastPersistedHwm &&
856
+ eventHwm &&
857
+ compareBroadcastHwm(eventHwm, lastPersistedHwm) <= 0
858
+ );
859
+ // The persisted stream cursor is the authoritative catchup horizon. Only
860
+ // fall back to file-content inspection when there is no comparable cursor
861
+ // (legacy/no-cursor sessions), or for a nonce reping whose durable line is
862
+ // intentionally reused while its wake identity is delivered again.
863
+ const consultInboxFile = ev.wake_nonce !== undefined ||
864
+ (isCatchingUp && lastPersistedHwm === null);
865
+ const alreadyPersisted = atOrBeforeResumeCursor || (
866
+ consultInboxFile &&
867
+ // gh#441: pass the rendered line so the fallback dedup can also
868
+ // recognize LEGACY (no-entry_id-prefix) on-disk lines, not just the
869
+ // [entry_id:] marker.
836
870
  await hasInboxEntryId(active.cubeId, active.droneId, ev.id, line)
837
871
  );
838
872
  if (alreadyPersisted) {
839
- // Replay still re-enters the OpenCode delivery queue with the same entry
840
- // ID. The queue confirms/deduplicates it by the unique persisted entry text.
841
- await injectOpenCode(line, ev.id, false);
873
+ // An ordinary entry at/before the persisted cursor is already delivered
874
+ // and must not re-enter the OpenCode queue. Re-pings are the explicit
875
+ // exception: each wake nonce is a new delivery identity even when the
876
+ // underlying inbox line is already durable.
877
+ if (isReping) {
878
+ await injectOpenCode(formatOpenCodeWakePrompt(line, ev.wake_nonce), deliveryId, true);
879
+ }
842
880
  markEventPersisted(ev.id, ev.data?.created_at ?? '');
843
881
  return 'persisted-skip';
844
882
  }
845
883
  // The inbox append is the durable record. OpenCode injection is only the
846
884
  // wake attempt and may return before the agent finishes processing.
847
885
  await appendLine(active.cubeId, active.droneId, line);
848
- if (!(await injectOpenCode(line, ev.id, true))) {
849
- wakeCodex(formatCodexWakePrompt(line));
886
+ if (!(await injectOpenCode(formatOpenCodeWakePrompt(line, ev.wake_nonce), deliveryId, true))) {
887
+ if (ev.wake_nonce === undefined) wakeCodex(formatCodexWakePrompt(line));
888
+ else wakeCodex(formatCodexWakePrompt(line), ev.wake_nonce);
850
889
  }
851
890
  return 'written';
852
891
  };
@@ -1007,7 +1046,7 @@ export async function streamOnce(
1007
1046
  const entryId =
1008
1047
  event.id ??
1009
1048
  `control:eviction:${active.cubeId}:${active.droneId}:${event.reason ?? ''}`;
1010
- await injectOpenCode(line, entryId, true);
1049
+ await injectOpenCode(formatCubeActivityWakeMessage(line), entryId, true);
1011
1050
  } catch {
1012
1051
  // Inbox write failed — the Path-B 410 backstop still tears the drone
1013
1052
  // down on its next authed call. Best-effort wake only.
@@ -1064,6 +1103,10 @@ export async function streamOnce(
1064
1103
  continue;
1065
1104
  }
1066
1105
  if (event.type === 'log') {
1106
+ const isHeartbeatPing =
1107
+ typeof event.data?.message === 'string' &&
1108
+ event.data.message.startsWith('[HEARTBEAT-PING]');
1109
+
1067
1110
  // DEDUP per §(3) recent-ids contract: an out-of-order DO
1068
1111
  // broadcast followed by reconnect+catchup can replay an entry
1069
1112
  // we already persisted. The entry IS on disk from an earlier
@@ -1073,10 +1116,14 @@ export async function streamOnce(
1073
1116
  // Last-Event-ID on the next reconnect reflects the highest
1074
1117
  // id we've actually got persisted). UUIDs are not ordinally
1075
1118
  // comparable with created_at, so set membership is the check.
1119
+ const isRecentWakeReplay = recentIds.has(event.id) && event.wake_nonce !== undefined;
1076
1120
  if (recentIds.has(event.id)) {
1121
+ // Nonce replays continue through every ordinary routing filter below;
1122
+ // only their durable append is skipped. Other duplicates retain the
1123
+ // historical fast path.
1077
1124
  markEventPersisted(event.id, event.data?.created_at ?? '');
1078
1125
  markBroadcastPersisted(broadcastHwmFromLogEvent(event));
1079
- continue;
1126
+ if (!isRecentWakeReplay) continue;
1080
1127
  }
1081
1128
 
1082
1129
  // OWN-DRONE FILTER: restore the silent-self property — parity
@@ -1103,10 +1150,6 @@ export async function streamOnce(
1103
1150
  // drone through the disk-write path; the existing rate-limit
1104
1151
  // in the server heartbeat contract (max 1 ping per drone per ~1h)
1105
1152
  // bounds the silent-self property's relaxation.
1106
- const isHeartbeatPing =
1107
- typeof event.data?.message === 'string' &&
1108
- event.data.message.startsWith('[HEARTBEAT-PING]');
1109
-
1110
1153
  // Sprint 26 ack-fan-out: ack events have `kind: 'ack'` plus an
1111
1154
  // `author_drone_id` field naming the recipient (the author of
1112
1155
  // the entry that got acked). Only the author writes the ack
@@ -1117,6 +1160,10 @@ export async function streamOnce(
1117
1160
  // here BEFORE the legacy own-drone filter so the ack-specific
1118
1161
  // semantic takes precedence.
1119
1162
  if (event.data?.kind === 'ack') {
1163
+ if (isRecentWakeReplay) {
1164
+ await recordSeen(event);
1165
+ continue;
1166
+ }
1120
1167
  if (event.data?.author_drone_id === active.droneId) {
1121
1168
  if ((await writeInboxLine(event)) === 'persisted-skip') continue;
1122
1169
  }
@@ -1151,6 +1198,15 @@ export async function streamOnce(
1151
1198
  continue;
1152
1199
  }
1153
1200
 
1201
+ if (isRecentWakeReplay) {
1202
+ const line = formatInboxLine(withSseEventId(event.data, event.id));
1203
+ const wakeNonce = event.wake_nonce;
1204
+ if (wakeNonce !== undefined && !(await injectOpenCode(formatOpenCodeWakePrompt(line, wakeNonce), wakeNonce, true))) {
1205
+ wakeCodex(formatCodexWakePrompt(line), wakeNonce);
1206
+ }
1207
+ continue;
1208
+ }
1209
+
1154
1210
  // Regular inbound entry: write the inbox line (catchup-dedup aware),
1155
1211
  // then advance both cursors.
1156
1212
  if ((await writeInboxLine(event)) === 'persisted-skip') continue;
@@ -1206,7 +1262,7 @@ export async function streamOnceIfOwner(
1206
1262
  // ------------------------------------------------------------------
1207
1263
 
1208
1264
  export type ParsedEvent =
1209
- | { type: 'log'; id: string; data: any; cursor?: LocalServerCursor }
1265
+ | { type: 'log'; id: string; data: any; wake_nonce?: string; cursor?: LocalServerCursor }
1210
1266
  | { type: 'heartbeat'; ts: string | null; hwm: BroadcastHwm | null }
1211
1267
  | { type: 'bookmark'; as_of: string | null }
1212
1268
  // gh#877 Path-A: terminal eviction control frame (wake hint, zero authority).
@@ -1327,10 +1383,12 @@ function parseEventBlock(block: string): ParsedEvent | null {
1327
1383
  const validCursor = cursor &&
1328
1384
  typeof cursor.id === 'string' &&
1329
1385
  typeof cursor.created_at === 'string';
1386
+ const entry = parsed?.entry ?? parsed;
1330
1387
  return {
1331
1388
  type: 'log',
1332
1389
  id,
1333
- data: parsed?.entry ?? parsed,
1390
+ data: entry,
1391
+ ...(typeof entry?.wake_nonce === 'string' ? { wake_nonce: entry.wake_nonce } : {}),
1334
1392
  ...(validCursor ? { cursor } : {}),
1335
1393
  };
1336
1394
  }
@@ -485,11 +485,15 @@ async function deliverOpenCodeEntry(
485
485
  log(`entry ${delivery.entryId} target unavailable: ${err}`);
486
486
  continue;
487
487
  }
488
- if (!target) return 'failed';
488
+ if (!target) {
489
+ log(`entry ${delivery.entryId} target unavailable: no bound session`);
490
+ return 'failed';
491
+ }
489
492
  }
490
493
 
491
494
  try {
492
495
  if (await findInjectedMessage(target.id, delivery.text)) {
496
+ log(`entry ${delivery.entryId} already present in session ${target.id}`);
493
497
  return 'delivered';
494
498
  }
495
499
  } catch (err) {
@@ -631,9 +635,10 @@ export async function injectInitialKickoff(launch: OpenCodeLaunchKickoff): Promi
631
635
 
632
636
  /**
633
637
  * Queue one durable inbox entry for delivery into the bound OpenCode session.
634
- * The durable inbox text identifies the OpenCode-generated user message, so
635
- * retries and replay can confirm an earlier ambiguous submission without
636
- * supplying an ordering-breaking caller message ID or running it twice.
638
+ * The injected text identifies the OpenCode-generated user message, so retries
639
+ * and replay can confirm an earlier ambiguous submission without supplying an
640
+ * ordering-breaking caller message ID or running it twice. Normal delivery uses
641
+ * canonical inbox text; wake re-pings include their stable nonce marker.
637
642
  */
638
643
  export function injectOpenCodeEntry(
639
644
  text: string,
@@ -641,7 +646,10 @@ export function injectOpenCodeEntry(
641
646
  allowSubmit: boolean = true,
642
647
  ): Promise<boolean> {
643
648
  const owner = state;
644
- if (!owner?.connected) return Promise.resolve(false);
649
+ if (!owner?.connected) {
650
+ log(`entry ${entryId} rejected: OpenCode is not connected`);
651
+ return Promise.resolve(false);
652
+ }
645
653
 
646
654
  const deliveredText = owner.deliveredEntries.get(entryId);
647
655
  if (deliveredText !== undefined) {
@@ -650,6 +658,7 @@ export function injectOpenCodeEntry(
650
658
  rememberBounded(owner.failedEntries, entryId, text);
651
659
  return Promise.resolve(false);
652
660
  }
661
+ log(`entry ${entryId} replay already delivered`);
653
662
  return Promise.resolve(true);
654
663
  }
655
664
 
@@ -660,6 +669,7 @@ export function injectOpenCodeEntry(
660
669
  rememberBounded(owner.failedEntries, entryId, text);
661
670
  return Promise.resolve(false);
662
671
  }
672
+ log(`entry ${entryId} replay remains unconfirmed`);
663
673
  return Promise.resolve(false);
664
674
  }
665
675
 
@@ -670,6 +680,7 @@ export function injectOpenCodeEntry(
670
680
  rememberBounded(owner.failedEntries, entryId, text);
671
681
  return Promise.resolve(false);
672
682
  }
683
+ log(`entry ${entryId} replay joined active delivery`);
673
684
  return active.promise;
674
685
  }
675
686
 
@@ -1,3 +1,5 @@
1
+ import { CUBE_ACTIVITY_RESUME_WAKE_MESSAGE } from './cube-activity-wake-copy.js';
2
+
1
3
  export const OPENCODE_WAKE_PATH_GUIDANCE =
2
4
  'OpenCode wakes through HTTP entry injection into its session through the local HTTP API. ' +
3
5
  'Borg writes each entry to the durable inbox before one injection attempt and ' +
@@ -5,4 +7,5 @@ export const OPENCODE_WAKE_PATH_GUIDANCE =
5
7
  'If injection is rejected or cannot be ' +
6
8
  'confirmed, the durable entry remains available and `borg_stream-status` reports ' +
7
9
  'it as failed or delivered-unconfirmed; run `borg_read-log unread_only=true` and ' +
8
- 'drain the unread log to recover.';
10
+ 'drain the unread log to recover. ' +
11
+ CUBE_ACTIVITY_RESUME_WAKE_MESSAGE;
@@ -28,6 +28,8 @@ export interface RosterDrone {
28
28
  last_seen: string | Date;
29
29
  /** Only present when the request carried `since`. */
30
30
  seen_since?: boolean;
31
+ /** Additive server-side wake state; absent on pre-C3 servers. */
32
+ wake_state?: 'idle' | 'pending' | 'awake' | 'stale';
31
33
  /**
32
34
  * gh#370 — which AI agent is running this drone. Null for drones that
33
35
  * joined before the column existed or via a launcher path that didn't
@@ -215,7 +217,8 @@ export function renderRoster(inputs: RenderRosterInputs): string {
215
217
  const regenCountMarker =
216
218
  typeof d.regen_count === 'number' ? ` · \`regen-count:${d.regen_count}\`` : '';
217
219
  if (resolvedSince) {
218
- // T2.1 awake/stale column. `seen_since === true` drone called a
220
+ // T2.1 awake/stale column. When available, wake_state is the server's
221
+ // richer state; older servers fall back to seen_since. `seen_since === true` → drone called a
219
222
  // tool after the resolved timestamp; treat as awake. False or
220
223
  // missing → stale. Missing should not happen when the server
221
224
  // echoed a since, but defending against a shape mismatch is
@@ -229,10 +232,9 @@ export function renderRoster(inputs: RenderRosterInputs): string {
229
232
  // probe call — pure redundancy. The per-row `last seen X ago`
230
233
  // field carries the diagnostic detail for "how stale is this
231
234
  // particular drone."
232
- const isAwake = d.seen_since === true;
233
- const marker = isAwake ? '`awake`' : '`stale`';
235
+ const marker = d.wake_state ?? (d.seen_since === true ? 'awake' : 'stale');
234
236
  lines.push(
235
- `- **${d.label}**${addr} (Role: ${roleName}) — last seen ${lastSeen} · ${marker}${regenCountMarker}${wakePathMarker}${wakePathClassMarker}`
237
+ `- **${d.label}**${addr} (Role: ${roleName}) — last seen ${lastSeen} · \`${marker}\`${regenCountMarker}${wakePathMarker}${wakePathClassMarker}`
236
238
  );
237
239
  } else {
238
240
  lines.push(