borgmcp 2.13.0 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +1 -1
- package/dist/inbox-monitor.d.ts.map +1 -1
- package/dist/inbox-monitor.js +6 -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 +44 -12
- 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 -7
- package/docs/LOCAL_SERVER.md +3 -7
- package/docs/RELEASING.md +10 -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 +7 -3
- package/src/log-stream.ts +49 -16
- package/src/opencode-drone.ts +16 -5
- package/src/opencode-wake-copy.ts +4 -1
- package/src/roster-render.ts +6 -4
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,6 +36,7 @@ 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*(.*)$/;
|
|
@@ -87,11 +88,14 @@ export function formatEventLine(inboxLine: string): string | null {
|
|
|
87
88
|
|
|
88
89
|
export function formatFreshEventLine(
|
|
89
90
|
inboxLine: string,
|
|
90
|
-
deduper: RecentLineDeduper
|
|
91
|
+
deduper: RecentLineDeduper,
|
|
92
|
+
includeWakeMessage = false,
|
|
91
93
|
): string | null {
|
|
92
94
|
const pretty = formatEventLine(inboxLine);
|
|
93
95
|
if (pretty === null) return null;
|
|
94
|
-
return deduper.remember(inboxLine)
|
|
96
|
+
return deduper.remember(inboxLine)
|
|
97
|
+
? includeWakeMessage ? formatCubeActivityWakeMessage(pretty) : pretty
|
|
98
|
+
: null;
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
export function seedDeduperFromInboxTail(
|
|
@@ -914,7 +918,7 @@ function main(): void {
|
|
|
914
918
|
|
|
915
919
|
const rl = createInterface({ input: tail.stdout, crlfDelay: Infinity });
|
|
916
920
|
rl.on('line', (line) => {
|
|
917
|
-
const pretty = formatFreshEventLine(line, deduper);
|
|
921
|
+
const pretty = formatFreshEventLine(line, deduper, true);
|
|
918
922
|
if (pretty !== null) {
|
|
919
923
|
console.log(pretty);
|
|
920
924
|
// 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,
|
|
@@ -830,23 +840,27 @@ export async function streamOnce(
|
|
|
830
840
|
ev: Extract<ParsedEvent, { type: 'log' }>
|
|
831
841
|
): Promise<'persisted-skip' | 'written'> => {
|
|
832
842
|
const line = formatInboxLine(withSseEventId(ev.data, ev.id));
|
|
833
|
-
const
|
|
843
|
+
const deliveryId = ev.wake_nonce ?? ev.id;
|
|
844
|
+
const isReping = ev.wake_nonce !== undefined;
|
|
845
|
+
const alreadyPersisted = (ev.wake_nonce !== undefined || isCatchingUp) && (
|
|
834
846
|
// gh#441: pass the rendered line so the dedup can also recognize LEGACY
|
|
835
847
|
// (no-entry_id-prefix) on-disk lines, not just the [entry_id:] marker.
|
|
836
848
|
await hasInboxEntryId(active.cubeId, active.droneId, ev.id, line)
|
|
837
849
|
);
|
|
838
850
|
if (alreadyPersisted) {
|
|
839
|
-
// Replay still re-enters the OpenCode delivery queue
|
|
840
|
-
//
|
|
841
|
-
|
|
851
|
+
// Replay still re-enters the OpenCode delivery queue. Ordinary entries
|
|
852
|
+
// correlate by canonical inbox text; re-pings add a stable nonce marker
|
|
853
|
+
// to the injected text so each distinct nonce submits exactly once.
|
|
854
|
+
await injectOpenCode(formatOpenCodeWakePrompt(line, ev.wake_nonce), deliveryId, isReping);
|
|
842
855
|
markEventPersisted(ev.id, ev.data?.created_at ?? '');
|
|
843
856
|
return 'persisted-skip';
|
|
844
857
|
}
|
|
845
858
|
// The inbox append is the durable record. OpenCode injection is only the
|
|
846
859
|
// wake attempt and may return before the agent finishes processing.
|
|
847
860
|
await appendLine(active.cubeId, active.droneId, line);
|
|
848
|
-
if (!(await injectOpenCode(line, ev.
|
|
849
|
-
wakeCodex(formatCodexWakePrompt(line));
|
|
861
|
+
if (!(await injectOpenCode(formatOpenCodeWakePrompt(line, ev.wake_nonce), deliveryId, true))) {
|
|
862
|
+
if (ev.wake_nonce === undefined) wakeCodex(formatCodexWakePrompt(line));
|
|
863
|
+
else wakeCodex(formatCodexWakePrompt(line), ev.wake_nonce);
|
|
850
864
|
}
|
|
851
865
|
return 'written';
|
|
852
866
|
};
|
|
@@ -1007,7 +1021,7 @@ export async function streamOnce(
|
|
|
1007
1021
|
const entryId =
|
|
1008
1022
|
event.id ??
|
|
1009
1023
|
`control:eviction:${active.cubeId}:${active.droneId}:${event.reason ?? ''}`;
|
|
1010
|
-
await injectOpenCode(line, entryId, true);
|
|
1024
|
+
await injectOpenCode(formatCubeActivityWakeMessage(line), entryId, true);
|
|
1011
1025
|
} catch {
|
|
1012
1026
|
// Inbox write failed — the Path-B 410 backstop still tears the drone
|
|
1013
1027
|
// down on its next authed call. Best-effort wake only.
|
|
@@ -1064,6 +1078,10 @@ export async function streamOnce(
|
|
|
1064
1078
|
continue;
|
|
1065
1079
|
}
|
|
1066
1080
|
if (event.type === 'log') {
|
|
1081
|
+
const isHeartbeatPing =
|
|
1082
|
+
typeof event.data?.message === 'string' &&
|
|
1083
|
+
event.data.message.startsWith('[HEARTBEAT-PING]');
|
|
1084
|
+
|
|
1067
1085
|
// DEDUP per §(3) recent-ids contract: an out-of-order DO
|
|
1068
1086
|
// broadcast followed by reconnect+catchup can replay an entry
|
|
1069
1087
|
// we already persisted. The entry IS on disk from an earlier
|
|
@@ -1073,10 +1091,14 @@ export async function streamOnce(
|
|
|
1073
1091
|
// Last-Event-ID on the next reconnect reflects the highest
|
|
1074
1092
|
// id we've actually got persisted). UUIDs are not ordinally
|
|
1075
1093
|
// comparable with created_at, so set membership is the check.
|
|
1094
|
+
const isRecentWakeReplay = recentIds.has(event.id) && event.wake_nonce !== undefined;
|
|
1076
1095
|
if (recentIds.has(event.id)) {
|
|
1096
|
+
// Nonce replays continue through every ordinary routing filter below;
|
|
1097
|
+
// only their durable append is skipped. Other duplicates retain the
|
|
1098
|
+
// historical fast path.
|
|
1077
1099
|
markEventPersisted(event.id, event.data?.created_at ?? '');
|
|
1078
1100
|
markBroadcastPersisted(broadcastHwmFromLogEvent(event));
|
|
1079
|
-
continue;
|
|
1101
|
+
if (!isRecentWakeReplay) continue;
|
|
1080
1102
|
}
|
|
1081
1103
|
|
|
1082
1104
|
// OWN-DRONE FILTER: restore the silent-self property — parity
|
|
@@ -1103,10 +1125,6 @@ export async function streamOnce(
|
|
|
1103
1125
|
// drone through the disk-write path; the existing rate-limit
|
|
1104
1126
|
// in the server heartbeat contract (max 1 ping per drone per ~1h)
|
|
1105
1127
|
// 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
1128
|
// Sprint 26 ack-fan-out: ack events have `kind: 'ack'` plus an
|
|
1111
1129
|
// `author_drone_id` field naming the recipient (the author of
|
|
1112
1130
|
// the entry that got acked). Only the author writes the ack
|
|
@@ -1117,6 +1135,10 @@ export async function streamOnce(
|
|
|
1117
1135
|
// here BEFORE the legacy own-drone filter so the ack-specific
|
|
1118
1136
|
// semantic takes precedence.
|
|
1119
1137
|
if (event.data?.kind === 'ack') {
|
|
1138
|
+
if (isRecentWakeReplay) {
|
|
1139
|
+
await recordSeen(event);
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1120
1142
|
if (event.data?.author_drone_id === active.droneId) {
|
|
1121
1143
|
if ((await writeInboxLine(event)) === 'persisted-skip') continue;
|
|
1122
1144
|
}
|
|
@@ -1151,6 +1173,15 @@ export async function streamOnce(
|
|
|
1151
1173
|
continue;
|
|
1152
1174
|
}
|
|
1153
1175
|
|
|
1176
|
+
if (isRecentWakeReplay) {
|
|
1177
|
+
const line = formatInboxLine(withSseEventId(event.data, event.id));
|
|
1178
|
+
const wakeNonce = event.wake_nonce;
|
|
1179
|
+
if (wakeNonce !== undefined && !(await injectOpenCode(formatOpenCodeWakePrompt(line, wakeNonce), wakeNonce, true))) {
|
|
1180
|
+
wakeCodex(formatCodexWakePrompt(line), wakeNonce);
|
|
1181
|
+
}
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1154
1185
|
// Regular inbound entry: write the inbox line (catchup-dedup aware),
|
|
1155
1186
|
// then advance both cursors.
|
|
1156
1187
|
if ((await writeInboxLine(event)) === 'persisted-skip') continue;
|
|
@@ -1206,7 +1237,7 @@ export async function streamOnceIfOwner(
|
|
|
1206
1237
|
// ------------------------------------------------------------------
|
|
1207
1238
|
|
|
1208
1239
|
export type ParsedEvent =
|
|
1209
|
-
| { type: 'log'; id: string; data: any; cursor?: LocalServerCursor }
|
|
1240
|
+
| { type: 'log'; id: string; data: any; wake_nonce?: string; cursor?: LocalServerCursor }
|
|
1210
1241
|
| { type: 'heartbeat'; ts: string | null; hwm: BroadcastHwm | null }
|
|
1211
1242
|
| { type: 'bookmark'; as_of: string | null }
|
|
1212
1243
|
// gh#877 Path-A: terminal eviction control frame (wake hint, zero authority).
|
|
@@ -1327,10 +1358,12 @@ function parseEventBlock(block: string): ParsedEvent | null {
|
|
|
1327
1358
|
const validCursor = cursor &&
|
|
1328
1359
|
typeof cursor.id === 'string' &&
|
|
1329
1360
|
typeof cursor.created_at === 'string';
|
|
1361
|
+
const entry = parsed?.entry ?? parsed;
|
|
1330
1362
|
return {
|
|
1331
1363
|
type: 'log',
|
|
1332
1364
|
id,
|
|
1333
|
-
data:
|
|
1365
|
+
data: entry,
|
|
1366
|
+
...(typeof entry?.wake_nonce === 'string' ? { wake_nonce: entry.wake_nonce } : {}),
|
|
1334
1367
|
...(validCursor ? { cursor } : {}),
|
|
1335
1368
|
};
|
|
1336
1369
|
}
|
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(
|