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.
- package/dist/codex-app-wake.d.ts +3 -3
- package/dist/codex-app-wake.d.ts.map +1 -1
- package/dist/codex-app-wake.js +10 -8
- package/dist/codex-app-wake.js.map +1 -1
- package/dist/cube-activity-wake-copy.d.ts +3 -0
- package/dist/cube-activity-wake-copy.d.ts.map +1 -0
- package/dist/cube-activity-wake-copy.js +5 -0
- package/dist/cube-activity-wake-copy.js.map +1 -0
- package/dist/inbox-monitor.d.ts +13 -1
- package/dist/inbox-monitor.d.ts.map +1 -1
- package/dist/inbox-monitor.js +29 -3
- package/dist/inbox-monitor.js.map +1 -1
- package/dist/log-stream.d.ts +2 -1
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +71 -17
- package/dist/log-stream.js.map +1 -1
- package/dist/opencode-drone.d.ts +4 -3
- package/dist/opencode-drone.d.ts.map +1 -1
- package/dist/opencode-drone.js +14 -5
- package/dist/opencode-drone.js.map +1 -1
- package/dist/opencode-wake-copy.d.ts.map +1 -1
- package/dist/opencode-wake-copy.js +3 -1
- package/dist/opencode-wake-copy.js.map +1 -1
- package/dist/roster-render.d.ts +2 -0
- package/dist/roster-render.d.ts.map +1 -1
- package/dist/roster-render.js +4 -4
- package/dist/roster-render.js.map +1 -1
- package/docs/EXTRACTION_PROVENANCE.md +8 -8
- package/docs/LOCAL_SERVER.md +3 -7
- package/docs/RELEASING.md +15 -6
- package/package.json +2 -2
- package/src/codex-app-wake.ts +15 -8
- package/src/cube-activity-wake-copy.ts +6 -0
- package/src/inbox-monitor.ts +36 -3
- package/src/log-stream.ts +78 -20
- package/src/opencode-drone.ts +16 -5
- package/src/opencode-wake-copy.ts +4 -1
- package/src/roster-render.ts +6 -4
package/dist/roster-render.js
CHANGED
|
@@ -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.
|
|
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
|
|
144
|
-
|
|
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;
|
|
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
|
|
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
|
|
41
|
-
resolves to the audited registry `borgmcp-shared@0.
|
|
42
|
-
`sha512-
|
|
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.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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.
|
package/docs/LOCAL_SERVER.md
CHANGED
|
@@ -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.
|
|
239
|
-
|
|
240
|
-
|
|
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.
|
|
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.
|
|
226
|
+
- the exact audited registry dependency `borgmcp-shared@0.9.0` remains locked to
|
|
217
227
|
its canonical tarball and integrity
|
|
218
|
-
`sha512-
|
|
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.
|
|
221
|
-
dogfood gate;
|
|
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.
|
|
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.
|
|
74
|
+
"borgmcp-shared": "0.9.0",
|
|
75
75
|
"chalk": "^5.3.0",
|
|
76
76
|
"prompts": "^2.4.2",
|
|
77
77
|
"which": "^4.0.0"
|
package/src/codex-app-wake.ts
CHANGED
|
@@ -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
|
-
|
|
30
|
+
CUBE_ACTIVITY_RESUME_WAKE_MESSAGE;
|
|
27
31
|
|
|
28
32
|
export function formatCodexWakePrompt(inboxLine: string): string {
|
|
29
|
-
return
|
|
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
|
-
'
|
|
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
|
+
}
|
package/src/inbox-monitor.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
|
|
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 =
|
|
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
|
|
834
|
-
|
|
835
|
-
|
|
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
|
-
//
|
|
840
|
-
//
|
|
841
|
-
|
|
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.
|
|
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:
|
|
1390
|
+
data: entry,
|
|
1391
|
+
...(typeof entry?.wake_nonce === 'string' ? { wake_nonce: entry.wake_nonce } : {}),
|
|
1334
1392
|
...(validCursor ? { cursor } : {}),
|
|
1335
1393
|
};
|
|
1336
1394
|
}
|
package/src/opencode-drone.ts
CHANGED
|
@@ -485,11 +485,15 @@ async function deliverOpenCodeEntry(
|
|
|
485
485
|
log(`entry ${delivery.entryId} target unavailable: ${err}`);
|
|
486
486
|
continue;
|
|
487
487
|
}
|
|
488
|
-
if (!target)
|
|
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
|
|
635
|
-
*
|
|
636
|
-
*
|
|
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)
|
|
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;
|
package/src/roster-render.ts
CHANGED
|
@@ -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.
|
|
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
|
|
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} ·
|
|
237
|
+
`- **${d.label}**${addr} (Role: ${roleName}) — last seen ${lastSeen} · \`${marker}\`${regenCountMarker}${wakePathMarker}${wakePathClassMarker}`
|
|
236
238
|
);
|
|
237
239
|
} else {
|
|
238
240
|
lines.push(
|