borgmcp 1.0.23 → 1.0.24

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.
@@ -32,6 +32,8 @@ export declare function probeCodexBridgeArmed(active: {
32
32
  getCodexWakeTarget?: typeof getCodexWakeTarget;
33
33
  checkBridge?: typeof checkCodexBridgeHealthy;
34
34
  }): Promise<boolean | null>;
35
+ /** gh#857 WI-2: last successful wake delivery time (for the heartbeat gate). */
36
+ export declare function getLastDeliveredAt(): number | null;
35
37
  export interface CodexWakeDeps {
36
38
  getActiveCube?: typeof getActiveCube;
37
39
  getCodexWakeTarget?: typeof getCodexWakeTarget;
@@ -41,7 +43,44 @@ export interface CodexWakeDeps {
41
43
  cwd?: () => string;
42
44
  sleep?: (ms: number) => Promise<void>;
43
45
  now?: () => number;
46
+ jitter?: () => number;
47
+ maxAttempts?: number;
44
48
  }
45
49
  export declare function wakeCodexViaAppServer(reason?: string, env?: NodeJS.ProcessEnv, deps?: CodexWakeDeps): void;
50
+ /**
51
+ * gh#857 WI-2: codex /loop-equivalent heartbeat cadence. Tighter than claude's
52
+ * ~30-min /loop ScheduleWakeup because codex has no per-entry inbox-Monitor
53
+ * backstop — this periodic drain IS the independent second safety net.
54
+ */
55
+ export declare const CODEX_HEARTBEAT_CADENCE_MS: number;
56
+ /**
57
+ * gh#857 WI-2: one tick of the codex /loop-equivalent heartbeat — a periodic,
58
+ * independent re-engagement that injects a borg:read-log-unread DRAIN turn so an
59
+ * idle codex drone re-syncs even if every per-entry wake was missed. SKIPS when a
60
+ * delivery (per-entry wake, retry-drain, or a prior heartbeat) already landed
61
+ * within the cadence window (shouldFireHeartbeat), so an active cube with flowing
62
+ * wakes never gets a redundant injection. Unlike the per-entry path it does NOT
63
+ * consult deliveredWakeKeys — the cadence gate is the throttle, and the static
64
+ * drain prompt is intentionally re-delivered each idle window. Best-effort: a
65
+ * mid-turn thread / transient error / unresolved target just skips this tick (the
66
+ * next tick retries). Never throws.
67
+ */
68
+ export declare function fireCodexHeartbeatTick(deps?: CodexWakeDeps, cadenceMs?: number): Promise<void>;
69
+ /**
70
+ * gh#857 WI-2: start the codex /loop-equivalent heartbeat — a setInterval firing
71
+ * fireCodexHeartbeatTick every cadence. CODEX-ONLY: claude wakes via the tail-F
72
+ * inbox Monitor + /loop ScheduleWakeup and has NO app-server socket to inject
73
+ * into, so the heartbeat is intrinsically a codex mechanism. The gate reads
74
+ * agentKind LOCALLY from this child's own env (resolveSessionAgentKind →
75
+ * BORG_CODEX_REMOTE_WAKE), never a mutable/server-recorded field, so a mislabel
76
+ * can't silently defeat the backstop (gh#633 lesson). The timer is unref'd so the
77
+ * heartbeat alone never keeps the process alive. Returns the timer, or null when
78
+ * not a codex session. Injectable agentKind/intervalMs/tick for tests.
79
+ */
80
+ export declare function startCodexHeartbeat(opts?: {
81
+ agentKind?: 'claude' | 'codex';
82
+ intervalMs?: number;
83
+ tick?: () => void;
84
+ }): ReturnType<typeof setInterval> | null;
46
85
  export declare function resetCodexWakeForTests(): void;
47
86
  //# sourceMappingURL=codex-app-wake.d.ts.map
@@ -1,2 +1,2 @@
1
- import{getActiveCube as k,getCodexWakeTarget as w,setCodexWakeTarget as x}from"./cubes.js";import{CodexAppServerClient as T}from"./codex-app-server.js";import{checkCodexBridgeHealthy as m}from"./codex-remote.js";import{recordEventReceipt as p}from"./health-beat.js";import{codexAppServerSocketFromEnv as P,pickFreshThread as b,wakeTargetChanged as I}from"./codex-wake-resolve.js";const W="New Borg cube-log activity arrived.";function $(e){return`New Borg cube-log activity arrived:
2
- ${e}`}const _="Borg cube activity arrived while you were busy. Run `borg:read-log unread_only=true` and DRAIN \u2014 repeat until the returned page is under the limit and behind_by is 0 \u2014 so no entries are skipped.",A=5e3,E=15*6e4;function v(e=process.env){return e.BORG_CODEX_REMOTE_WAKE==="1"}function q(e=process.env){return v(e)?"codex":"claude"}function R(e=process.env){return v(e)?{enabled:!0}:{enabled:!1}}async function G(e,t={}){try{const n=await(t.getCodexWakeTarget??w)(e.cubeId,e.droneId);return n?(t.checkBridge??m)(n.socketPath):!1}catch{return null}}let l=!1;const h=[],u=new Set,f=[],S=100;let g=!1;function K(e){return new Promise(t=>setTimeout(t,e))}function C(e,t){return t.createClient?t.createClient(e):new T(e)}async function y(e,t){const r=P(t.env??process.env);if(r){const a=C(r,t);await a.connect();try{const o=await a.loadedThreadIds(),i=[];for(const s of o){const d=await a.readThread(s);d&&i.push({id:d.id,cwd:d.cwd,updatedAt:d.updatedAt})}const c=b(i,{cwd:(t.cwd??(()=>process.cwd()))()});return c?(await O(e,{socketPath:r,threadId:c},t),{socketPath:r,threadId:c}):null}finally{a.close()}}const n=await(t.getCodexWakeTarget??w)(e.cubeId,e.droneId);return n?{socketPath:n.socketPath,threadId:n.threadId}:null}async function O(e,t,r){try{const n=r.getCodexWakeTarget??w,a=r.setCodexWakeTarget??x,o=await n(e.cubeId,e.droneId),i=o?{socketPath:o.socketPath,threadId:o.threadId}:null;I(i,t)&&await a(e.cubeId,e.droneId,t)}catch{}}function Q(e=W,t=process.env,r={}){R(t).enabled&&(h.push({reason:e,deps:r}),!l&&(l=!0,D().finally(()=>{l=!1})))}async function D(){for(;h.length>0;){const e=h.shift();await B(e.reason,e.deps)}}async function B(e,t){try{const r=await(t.getActiveCube??k)();if(!r)return;const n=await y(r,t);if(!n)return;const{socketPath:a,threadId:o}=n,i=`${o}\0${e}`;if(u.has(i))return;const c=C(a,t);await c.connect();try{if((await c.readThread(o))?.status?.type==="active"){U(t);return}await c.startTurn(o,e),p(),M(i)}finally{c.close()}}catch{}}function U(e){g||(g=!0,F(e).finally(()=>{g=!1}))}async function F(e){const t=e.sleep??K,r=e.now??Date.now,n=r()+E;for(;r()<n;){await t(A);try{const a=await(e.getActiveCube??k)();if(!a)continue;const o=await y(a,e);if(!o)continue;const{socketPath:i,threadId:c}=o,s=C(i,e);await s.connect();try{if((await s.readThread(c))?.status?.type==="active")continue;await s.startTurn(c,_),p();return}finally{s.close()}}catch{}}}function Y(){l=!1,h.length=0,u.clear(),f.length=0,g=!1}function M(e){if(!u.has(e))for(u.add(e),f.push(e);f.length>S;){const t=f.shift();t&&u.delete(t)}}export{_ as CODEX_CATCHUP_PROMPT,W as CODEX_WAKE_PROMPT,$ as formatCodexWakePrompt,v as isCodexRemoteWakeEnabled,G as probeCodexBridgeArmed,Y as resetCodexWakeForTests,R as resolveCodexWakeTarget,q as resolveSessionAgentKind,Q as wakeCodexViaAppServer};
1
+ import{getActiveCube as x,getCodexWakeTarget as C,setCodexWakeTarget as P}from"./cubes.js";import{CodexAppServerClient as _}from"./codex-app-server.js";import{checkCodexBridgeHealthy as D}from"./codex-remote.js";import{recordEventReceipt as A}from"./health-beat.js";import{codexAppServerSocketFromEnv as R,pickFreshThread as K,wakeTargetChanged as M,wakeRetryBackoffMs as S,wakeRetryExpired as B,WAKE_RETRY_MAX_ATTEMPTS as O,shouldFireHeartbeat as F}from"./codex-wake-resolve.js";const H="New Borg cube-log activity arrived.";function re(e){return`New Borg cube-log activity arrived:
2
+ ${e}`}const b="Borg cube activity arrived while you were busy. Run `borg:read-log unread_only=true` and DRAIN \u2014 repeat until the returned page is under the limit and behind_by is 0 \u2014 so no entries are skipped.";function E(e=process.env){return e.BORG_CODEX_REMOTE_WAKE==="1"}function X(e=process.env){return E(e)?"codex":"claude"}function N(e=process.env){return E(e)?{enabled:!0}:{enabled:!1}}async function ne(e,t={}){try{const n=await(t.getCodexWakeTarget??C)(e.cubeId,e.droneId);return n?(t.checkBridge??D)(n.socketPath):!1}catch{return null}}let f=!1;const h=[],u=new Set,w=[],L=100;let g=!1,k=null;function ae(){return k}function p(e){k=(e.now??Date.now)()}let v=!1;function $(e){return new Promise(t=>setTimeout(t,e))}function y(e,t){return t.createClient?t.createClient(e):new _(e)}async function T(e,t){const r=R(t.env??process.env);if(r){const a=y(r,t);await a.connect();try{const o=await a.loadedThreadIds(),i=[];for(const d of o){const s=await a.readThread(d);s&&i.push({id:s.id,cwd:s.cwd,updatedAt:s.updatedAt})}const c=K(i,{cwd:(t.cwd??(()=>process.cwd()))()});return c?(await j(e,{socketPath:r,threadId:c},t),{socketPath:r,threadId:c}):null}finally{a.close()}}const n=await(t.getCodexWakeTarget??C)(e.cubeId,e.droneId);return n?{socketPath:n.socketPath,threadId:n.threadId}:null}async function j(e,t,r){try{const n=r.getCodexWakeTarget??C,a=r.setCodexWakeTarget??P,o=await n(e.cubeId,e.droneId),i=o?{socketPath:o.socketPath,threadId:o.threadId}:null;M(i,t)&&await a(e.cubeId,e.droneId,t)}catch{}}function oe(e=H,t=process.env,r={}){N(t).enabled&&(h.push({reason:e,deps:r}),!f&&(f=!0,q().finally(()=>{f=!1})))}async function q(){for(;h.length>0;){const e=h.shift();await V(e.reason,e.deps)}}async function V(e,t){try{const r=await(t.getActiveCube??x)();if(!r)return;const n=await T(r,t);if(!n)return;const{socketPath:a,threadId:o}=n,i=`${o}\0${e}`;if(u.has(i))return;const c=y(a,t);await c.connect();try{if((await c.readThread(o))?.status?.type==="active"){I(t);return}await c.startTurn(o,e),A(),Q(i),p(t)}finally{c.close()}}catch{I(t)}}function I(e){g||(g=!0,Y(e).finally(()=>{g=!1}))}async function Y(e){const t=e.sleep??$,r=e.now??Date.now,n=e.jitter??(()=>Math.random()*500),a=e.maxAttempts??O,o=r();let i=0;for(;!B(o,r())&&i<a;){await t(S(i,n())),i++;try{const c=await(e.getActiveCube??x)();if(!c)continue;const d=await T(c,e);if(!d)continue;const{socketPath:s,threadId:m}=d,l=y(s,e);await l.connect();try{if((await l.readThread(m))?.status?.type==="active")continue;await l.startTurn(m,b),A(),p(e);return}finally{l.close()}}catch{}}}const W=20*6e4;async function G(e={},t=W){if(v)return;const r=(e.now??Date.now)();if(F(k,r,t)){v=!0;try{const n=await(e.getActiveCube??x)();if(!n)return;const a=await T(n,e);if(!a)return;const o=y(a.socketPath,e);await o.connect();try{if((await o.readThread(a.threadId))?.status?.type==="active")return;await o.startTurn(a.threadId,b),p(e)}finally{o.close()}}catch{}finally{v=!1}}}function ie(e={}){if((e.agentKind??X())!=="codex")return null;const r=e.intervalMs??W,n=e.tick??(()=>{G()}),a=setInterval(n,r);return a.unref?.(),a}function ce(){f=!1,h.length=0,u.clear(),w.length=0,g=!1,k=null,v=!1}function Q(e){if(!u.has(e))for(u.add(e),w.push(e);w.length>L;){const t=w.shift();t&&u.delete(t)}}export{b as CODEX_CATCHUP_PROMPT,W as CODEX_HEARTBEAT_CADENCE_MS,H as CODEX_WAKE_PROMPT,G as fireCodexHeartbeatTick,re as formatCodexWakePrompt,ae as getLastDeliveredAt,E as isCodexRemoteWakeEnabled,ne as probeCodexBridgeArmed,ce as resetCodexWakeForTests,N as resolveCodexWakeTarget,X as resolveSessionAgentKind,ie as startCodexHeartbeat,oe as wakeCodexViaAppServer};
@@ -66,4 +66,38 @@ export declare function wakeTargetChanged(existing: {
66
66
  socketPath: string;
67
67
  threadId: string;
68
68
  }): boolean;
69
+ /** Base backoff for the first retry of a dropped/deferred wake. */
70
+ export declare const WAKE_RETRY_BASE_MS = 5000;
71
+ /** Backoff ceiling — a wedged thread is retried at most this often. */
72
+ export declare const WAKE_RETRY_CAP_MS = 60000;
73
+ /**
74
+ * Age cap: a pending wake older than this is given up (dropped from the queue).
75
+ * Generous on purpose — the WI-2 heartbeat is the backstop beyond it, and the
76
+ * server read-cursor means the next delivery (heartbeat or a fresh entry) drains
77
+ * everything anyway, so an aged-out single wake is never a permanent miss.
78
+ */
79
+ export declare const WAKE_RETRY_MAX_AGE_MS: number;
80
+ /**
81
+ * Hard iteration ceiling for the retry-drain loop — a defensive belt ALONGSIDE
82
+ * the time-based age cap. In prod the age cap (real clock) terminates the loop in
83
+ * ~45-50 attempts; this ceiling only matters if the clock fails to advance
84
+ * (pathological / a non-advancing injected clock in tests) where a time-only
85
+ * guard would hot-spin forever. Set far above any real attempt count.
86
+ */
87
+ export declare const WAKE_RETRY_MAX_ATTEMPTS = 1000;
88
+ /**
89
+ * Exponential backoff (ms) for the Nth retry of a pending wake (0-based),
90
+ * doubling from WAKE_RETRY_BASE_MS, saturating at WAKE_RETRY_CAP_MS, plus
91
+ * caller-supplied jitter so co-located sibling drones don't retry in lockstep.
92
+ */
93
+ export declare function wakeRetryBackoffMs(attempts: number, jitter?: number): number;
94
+ /** True once a pending wake has outlived the age cap (give up; heartbeat backstops). */
95
+ export declare function wakeRetryExpired(firstEnqueuedAt: number, now: number, maxAgeMs?: number): boolean;
96
+ /**
97
+ * WI-2 double-fire avoidance: the periodic heartbeat fires only when no wake (or
98
+ * prior heartbeat) delivery landed within the cadence window — so an active cube
99
+ * with flowing per-entry wakes doesn't get redundant heartbeat injections. A
100
+ * never-delivered seat (null) always fires.
101
+ */
102
+ export declare function shouldFireHeartbeat(lastDeliveredAt: number | null, now: number, cadenceMs: number): boolean;
69
103
  //# sourceMappingURL=codex-wake-resolve.d.ts.map
@@ -1 +1 @@
1
- const u="BORG_CODEX_APP_SERVER_SOCKET";function p(e=process.env){const t=e[u];return t&&t.length>0?t:null}function f(e){return["-c",`mcp_servers.borg.env.${u}="${e}"`]}function s(e,t){if(e.length===0)return null;if(e.length===1)return e[0].id;const r=e.filter(n=>n.cwd===t.cwd),o=r.length>0?r:e;let c=o[0];for(const n of o)(n.updatedAt??0)>(c.updatedAt??0)&&(c=n);return c.id}function a(e,t){const r={};let o=!1;for(const[c,n]of Object.entries(e)){if(t(n.socketPath)===!1){o=!0;continue}r[c]=n}return{targets:r,changed:o}}function d(e,t){return!e||e.socketPath!==t.socketPath||e.threadId!==t.threadId}export{u as BORG_CODEX_APP_SERVER_SOCKET_ENV,f as codexAppServerSocketConfigArgs,p as codexAppServerSocketFromEnv,s as pickFreshThread,a as pruneDeadWakeTargets,d as wakeTargetChanged};
1
+ const u="BORG_CODEX_APP_SERVER_SOCKET";function a(t=process.env){const e=t[u];return e&&e.length>0?e:null}function f(t){return["-c",`mcp_servers.borg.env.${u}="${t}"`]}function E(t,e){if(t.length===0)return null;if(t.length===1)return t[0].id;const r=t.filter(o=>o.cwd===e.cwd),n=r.length>0?r:t;let c=n[0];for(const o of n)(o.updatedAt??0)>(c.updatedAt??0)&&(c=o);return c.id}function i(t,e){const r={};let n=!1;for(const[c,o]of Object.entries(t)){if(e(o.socketPath)===!1){n=!0;continue}r[c]=o}return{targets:r,changed:n}}function d(t,e){return!t||t.socketPath!==e.socketPath||t.threadId!==e.threadId}const p=5e3,_=6e4,s=45*6e4,x=1e3;function A(t,e=0){const r=Math.max(0,t),n=p*2**r;return Math.min(n,_)+e}function R(t,e,r=s){return e-t>=r}function l(t,e,r){return t===null?!0:e-t>=r}export{u as BORG_CODEX_APP_SERVER_SOCKET_ENV,p as WAKE_RETRY_BASE_MS,_ as WAKE_RETRY_CAP_MS,s as WAKE_RETRY_MAX_AGE_MS,x as WAKE_RETRY_MAX_ATTEMPTS,f as codexAppServerSocketConfigArgs,a as codexAppServerSocketFromEnv,E as pickFreshThread,i as pruneDeadWakeTargets,l as shouldFireHeartbeat,A as wakeRetryBackoffMs,R as wakeRetryExpired,d as wakeTargetChanged};
@@ -61,14 +61,18 @@ export declare function getStreamStatus(): StreamStatus & {
61
61
  */
62
62
  export declare function __resetStreamStateForTest(): void;
63
63
  /**
64
- * Spawn the background SSE consumer loop. Fire-and-forget; the loop
65
- * runs until process exit. Errors are written to stderr (so they don't
66
- * pollute the MCP stdio channel) and the loop continues.
67
- *
68
- * Idempotent in the sense that calling twice would create two parallel
69
- * loops; the caller (`index.ts`) wires this once at startup.
64
+ * gh#857 WI-2: start the codex /loop-equivalent heartbeat AT MOST ONCE — a
65
+ * re-entrant startLogStream must not leak a second interval (idempotent start).
66
+ * No-op for claude (startCodexHeartbeat returns null on a non-codex session, so
67
+ * no timer is ever stored). Extracted + injectable (`start`) so the production
68
+ * wiring is unit-testable without running the real stream loop (QA e75339e7).
70
69
  */
71
- export declare function startLogStream(): void;
70
+ export declare function ensureCodexHeartbeatStarted(start?: () => ReturnType<typeof setInterval> | null): void;
71
+ /** Test-only: stop + clear the heartbeat timer so idempotence is re-testable. */
72
+ export declare function __resetCodexHeartbeatForTest(): void;
73
+ export declare function startLogStream(opts?: {
74
+ runForever?: () => void;
75
+ }): void;
72
76
  export interface StreamDeps {
73
77
  /** Override the global fetch (tests inject a controlled Response). */
74
78
  fetchImpl?: typeof fetch;
@@ -1,11 +1,11 @@
1
- import{promises as h}from"node:fs";import z from"node:os";import x from"node:path";import{getActiveCube as Q,inboxPathForDrone as B}from"./cubes.js";import{formatCodexWakePrompt as Y,resolveSessionAgentKind as Z,wakeCodexViaAppServer as ee}from"./codex-app-wake.js";import{getValidToken as te}from"./remote-client.js";import{recordEventReceipt as ne,emitHealthBeat as re,getCachedMonitorHealthy as ae,getCachedWakeArmed as oe}from"./health-beat.js";import{getPackageVersion as ie}from"./version.js";import{acquireStreamLease as G,readOwnershipSnapshot as b}from"./stream-owner.js";const j=9e4,se=2e3,ce=500,le=3e4,de=50,R=512,Oe=R*2;function ue(){try{const e=z.hostname();return e&&e.trim()?e.trim().slice(0,255):null}catch{return null}}const fe=Date.now(),i={connected:!1,lastWireActivityAt:null,lastContentEventAt:null,lastHeartbeatAt:null,lastPersistedEventId:null,reconnectAttempts:0,runLoopRestartCount:0,ownership:{state:"unowned"}};function pe(e,t=Date.now()-fe){return e.connected&&e.lastWireActivityAt?"connected":!e.connected&&e.reconnectAttempts>0?"reconnecting":!e.connected&&!e.lastWireActivityAt&&e.reconnectAttempts===0&&t>1e4?"silent-inert":"never-started"}function $e(){return{...i,runLoopHealth:pe(i)}}function ve(){i.connected=!1,i.lastWireActivityAt=null,i.lastContentEventAt=null,i.lastHeartbeatAt=null,i.lastPersistedEventId=null,i.reconnectAttempts=0,i.runLoopRestartCount=0,i.ownership={state:"unowned"}}function Pe(){(async()=>{for(;;){try{await we(),process.stderr.write(`[borg-mcp log stream] runLoop returned unexpectedly; restarting in 5s
1
+ import{promises as h}from"node:fs";import Q from"node:os";import C from"node:path";import{getActiveCube as Y,inboxPathForDrone as G}from"./cubes.js";import{formatCodexWakePrompt as Z,resolveSessionAgentKind as ee,startCodexHeartbeat as te,wakeCodexViaAppServer as ne}from"./codex-app-wake.js";import{getValidToken as re}from"./remote-client.js";import{recordEventReceipt as ae,emitHealthBeat as oe,getCachedMonitorHealthy as ie,getCachedWakeArmed as se}from"./health-beat.js";import{getPackageVersion as ce}from"./version.js";import{acquireStreamLease as j,readOwnershipSnapshot as b}from"./stream-owner.js";const K=9e4,le=2e3,de=500,ue=3e4,fe=50,v=512,Pe=v*2;function pe(){try{const e=Q.hostname();return e&&e.trim()?e.trim().slice(0,255):null}catch{return null}}const me=Date.now(),i={connected:!1,lastWireActivityAt:null,lastContentEventAt:null,lastHeartbeatAt:null,lastPersistedEventId:null,reconnectAttempts:0,runLoopRestartCount:0,ownership:{state:"unowned"}};function we(e,t=Date.now()-me){return e.connected&&e.lastWireActivityAt?"connected":!e.connected&&e.reconnectAttempts>0?"reconnecting":!e.connected&&!e.lastWireActivityAt&&e.reconnectAttempts===0&&t>1e4?"silent-inert":"never-started"}function We(){return{...i,runLoopHealth:we(i)}}function Be(){i.connected=!1,i.lastWireActivityAt=null,i.lastContentEventAt=null,i.lastHeartbeatAt=null,i.lastPersistedEventId=null,i.reconnectAttempts=0,i.runLoopRestartCount=0,i.ownership={state:"unowned"}}let A=null;function he(e=te){A||(A=e())}function Ge(){A&&clearInterval(A),A=null}function ge(){(async()=>{for(;;){try{await Ie(),process.stderr.write(`[borg-mcp log stream] runLoop returned unexpectedly; restarting in 5s
2
2
  `)}catch(e){process.stderr.write(`[borg-mcp log stream] runLoop threw: ${e?.message??e}; restarting in 5s
3
- `)}i.runLoopRestartCount+=1,await I(5e3)}})()}const K={fetchImpl:globalThis.fetch.bind(globalThis),appendLine:_e,hasInboxEntryId:Te,getToken:te,wakeCodex:ee,heartbeatTimeoutMs:j,hwmDivergenceGraceMs:se,abortSignal:new AbortController().signal,ownerDeps:{},ownerStaleMs:7e4,onInboxReceipt:me};function me(e,t){ne(),re(e,{sseConnected:!0,inboxMonitorHealthy:ae(),wakeArmed:oe(),agentKind:Z(),hostname:ue(),version:ie(),getToken:async()=>t,fetchImpl:globalThis.fetch.bind(globalThis)})}async function we(){let e=0,t=null,o=null,a=null,s=null;for(;;){const r=await Q();if(!r){a&&(await a.release(),a=null,s=null),i.connected=!1,i.ownership={state:"unowned"},await I(5e3);continue}r.cubeId!==o&&(o=r.cubeId,t=null);const c=`${r.cubeId}:${r.droneId}`;if(a&&s!==c&&(await a.release(),a=null,s=null),a||(a=await G(r.cubeId,r.droneId),s=a?c:null),!a){i.connected=!1,i.ownership=await b(r.cubeId,r.droneId),await I(5e3);continue}i.ownership=await b(r.cubeId,r.droneId);let l=!1;try{const f=new AbortController,g=async()=>{try{await a.refresh()||(l=!0,f.abort(new Error("stream ownership lost")))}catch(p){l=!0,f.abort(p instanceof Error?p:new Error(String(p)))}},C=setInterval(()=>{g()},Math.max(1e3,Math.floor(j/2)));try{await V(r,t,p=>{t=p},{abortSignal:f.signal})}finally{clearInterval(C)}if(l){a=null,s=null,i.connected=!1,i.ownership=await b(r.cubeId,r.droneId),await I(5e3);continue}e=0,i.reconnectAttempts=0}catch(f){if(l){a=null,s=null,i.connected=!1,i.ownership=await b(r.cubeId,r.droneId),await I(5e3);continue}i.connected=!1;const g=Math.min(ce*2**e,le)+Math.random()*500;process.stderr.write(`[borg-mcp log stream] reconnect in ${Math.round(g)}ms: ${f?.message??f}
4
- `),e+=1,i.reconnectAttempts=e,await I(g)}}}async function V(e,t,o,a={}){const{fetchImpl:s,appendLine:r,hasInboxEntryId:c,getToken:l,wakeCodex:f,heartbeatTimeoutMs:g,hwmDivergenceGraceMs:C,abortSignal:p,onInboxReceipt:U}={...K,...a},O=await l(),$={Authorization:`Bearer ${O}`,"X-Drone-Session":e.sessionToken,Accept:"text/event-stream"};t&&($["Last-Event-ID"]=t);const A=new AbortController,H=()=>{try{A.abort(p.reason??new Error("external abort"))}catch{}};p.aborted&&H(),p.addEventListener("abort",H,{once:!0});let m=null;const v=()=>{m&&clearTimeout(m),m=setTimeout(()=>{try{A.abort(new Error("heartbeat watchdog timeout"))}catch{}},g)};v();let P=t,u=null,w=null;const _=()=>{w&&(clearTimeout(w.timer),w=null)};let S=null;const T=(n,d)=>{const k={id:n,created_at:d};S&&d&&S.created_at&&y(k,S)<=0||(S=k,P=n,i.lastPersistedEventId=n,o(n))},L=n=>{n&&(u=!u||y(n,u)>0?n:u,w&&y(u,w.hwm)>=0&&_())},q=n=>{if(w?.hwm.id===n.id)return;_();const d=setTimeout(()=>{if(u&&y(u,n)>=0){_();return}try{A.abort(new Error("hwm divergence \u2014 reconnect for catchup"))}catch{}},C);w={hwm:n,timer:d}},M=new Set,D=[];let W=t!==null;const F=async n=>{const d=Ie(be(n.data,n.id));return W&&await c(e.cubeId,e.droneId,n.id,d)?(T(n.id,n.data?.created_at??""),"persisted-skip"):(await r(e.cubeId,e.droneId,d),f(Y(d)),U(e,O),"written")},N=n=>{for(M.add(n.id),D.push(n.id);D.length>de;){const d=D.shift();d&&M.delete(d)}T(n.id,n.data?.created_at??""),L(J(n))};let E;try{E=await s(`${e.apiUrl}/api/drone/stream`,{method:"GET",headers:$,signal:A.signal})}catch(n){throw m&&clearTimeout(m),n}if(!E.ok||!E.body)throw m&&clearTimeout(m),new Error(`stream HTTP ${E.status}`);i.connected=!0;try{for await(const n of he(E.body)){v();const d=new Date().toISOString();if(i.lastWireActivityAt=d,(n.type==="log"||n.type==="bookmark")&&(i.lastContentEventAt=d),n.type==="heartbeat"){if(i.lastHeartbeatAt=d,n.hwm&&u===null){L(n.hwm),P===null&&T(n.hwm.id,n.hwm.created_at);continue}if(n.hwm&&u&&y(n.hwm,u)<=0){_();continue}n.hwm&&u&&y(n.hwm,u)>0&&q(n.hwm);continue}if(n.type==="bookmark"){W=!1;continue}if(n.type==="log"){if(M.has(n.id)){T(n.id,n.data?.created_at??""),L(J(n));continue}const k=typeof n.data?.message=="string"&&n.data.message.startsWith("[HEARTBEAT-PING]");if(n.data?.kind==="ack"){if(n.data?.author_drone_id===e.droneId&&await F(n)==="persisted-skip")continue;N(n);continue}if(n.data?.drone_id===e.droneId&&!k){N(n);continue}if(await F(n)==="persisted-skip")continue;N(n)}}}finally{p.removeEventListener("abort",H),m&&clearTimeout(m),_(),i.connected=!1}}async function We(e,t,o,a={}){const{ownerDeps:s,ownerStaleMs:r}={...K,...a},c=await G(e.cubeId,e.droneId,r,s);if(!c)return i.connected=!1,i.ownership=await b(e.cubeId,e.droneId,s),"skipped";i.ownership=await b(e.cubeId,e.droneId,s);try{return await V(e,t,o,a),"streamed"}finally{await c.release()}}async function*he(e){const t=e.getReader(),o=new TextDecoder;let a="";try{for(;;){const{value:s,done:r}=await t.read();if(r){if(a.trim()){const l=X(a);l&&(yield l)}return}a+=o.decode(s,{stream:!0});let c;for(;(c=a.indexOf(`
3
+ `)}i.runLoopRestartCount+=1,await y(5e3)}})()}function je(e={}){he(),(e.runForever??ge)()}const V={fetchImpl:globalThis.fetch.bind(globalThis),appendLine:Te,hasInboxEntryId:He,getToken:re,wakeCodex:ne,heartbeatTimeoutMs:K,hwmDivergenceGraceMs:le,abortSignal:new AbortController().signal,ownerDeps:{},ownerStaleMs:7e4,onInboxReceipt:be};function be(e,t){ae(),oe(e,{sseConnected:!0,inboxMonitorHealthy:ie(),wakeArmed:se(),agentKind:ee(),hostname:pe(),version:ce(),getToken:async()=>t,fetchImpl:globalThis.fetch.bind(globalThis)})}async function Ie(){let e=0,t=null,o=null,a=null,s=null;for(;;){const r=await Y();if(!r){a&&(await a.release(),a=null,s=null),i.connected=!1,i.ownership={state:"unowned"},await y(5e3);continue}r.cubeId!==o&&(o=r.cubeId,t=null);const c=`${r.cubeId}:${r.droneId}`;if(a&&s!==c&&(await a.release(),a=null,s=null),a||(a=await j(r.cubeId,r.droneId),s=a?c:null),!a){i.connected=!1,i.ownership=await b(r.cubeId,r.droneId),await y(5e3);continue}i.ownership=await b(r.cubeId,r.droneId);let l=!1;try{const f=new AbortController,g=async()=>{try{await a.refresh()||(l=!0,f.abort(new Error("stream ownership lost")))}catch(p){l=!0,f.abort(p instanceof Error?p:new Error(String(p)))}},H=setInterval(()=>{g()},Math.max(1e3,Math.floor(K/2)));try{await X(r,t,p=>{t=p},{abortSignal:f.signal})}finally{clearInterval(H)}if(l){a=null,s=null,i.connected=!1,i.ownership=await b(r.cubeId,r.droneId),await y(5e3);continue}e=0,i.reconnectAttempts=0}catch(f){if(l){a=null,s=null,i.connected=!1,i.ownership=await b(r.cubeId,r.droneId),await y(5e3);continue}i.connected=!1;const g=Math.min(de*2**e,ue)+Math.random()*500;process.stderr.write(`[borg-mcp log stream] reconnect in ${Math.round(g)}ms: ${f?.message??f}
4
+ `),e+=1,i.reconnectAttempts=e,await y(g)}}}async function X(e,t,o,a={}){const{fetchImpl:s,appendLine:r,hasInboxEntryId:c,getToken:l,wakeCodex:f,heartbeatTimeoutMs:g,hwmDivergenceGraceMs:H,abortSignal:p,onInboxReceipt:q}={...V,...a},O=await l(),$={Authorization:`Bearer ${O}`,"X-Drone-Session":e.sessionToken,Accept:"text/event-stream"};t&&($["Last-Event-ID"]=t);const S=new AbortController,L=()=>{try{S.abort(p.reason??new Error("external abort"))}catch{}};p.aborted&&L(),p.addEventListener("abort",L,{once:!0});let m=null;const F=()=>{m&&clearTimeout(m),m=setTimeout(()=>{try{S.abort(new Error("heartbeat watchdog timeout"))}catch{}},g)};F();let P=t,u=null,w=null;const _=()=>{w&&(clearTimeout(w.timer),w=null)};let T=null;const x=(n,d)=>{const k={id:n,created_at:d};T&&d&&T.created_at&&I(k,T)<=0||(T=k,P=n,i.lastPersistedEventId=n,o(n))},M=n=>{n&&(u=!u||I(n,u)>0?n:u,w&&I(u,w.hwm)>=0&&_())},z=n=>{if(w?.hwm.id===n.id)return;_();const d=setTimeout(()=>{if(u&&I(u,n)>=0){_();return}try{S.abort(new Error("hwm divergence \u2014 reconnect for catchup"))}catch{}},H);w={hwm:n,timer:d}},D=new Set,N=[];let W=t!==null;const B=async n=>{const d=Se(Ee(n.data,n.id));return W&&await c(e.cubeId,e.droneId,n.id,d)?(x(n.id,n.data?.created_at??""),"persisted-skip"):(await r(e.cubeId,e.droneId,d),f(Z(d)),q(e,O),"written")},R=n=>{for(D.add(n.id),N.push(n.id);N.length>fe;){const d=N.shift();d&&D.delete(d)}x(n.id,n.data?.created_at??""),M(U(n))};let E;try{E=await s(`${e.apiUrl}/api/drone/stream`,{method:"GET",headers:$,signal:S.signal})}catch(n){throw m&&clearTimeout(m),n}if(!E.ok||!E.body)throw m&&clearTimeout(m),new Error(`stream HTTP ${E.status}`);i.connected=!0;try{for await(const n of ye(E.body)){F();const d=new Date().toISOString();if(i.lastWireActivityAt=d,(n.type==="log"||n.type==="bookmark")&&(i.lastContentEventAt=d),n.type==="heartbeat"){if(i.lastHeartbeatAt=d,n.hwm&&u===null){M(n.hwm),P===null&&x(n.hwm.id,n.hwm.created_at);continue}if(n.hwm&&u&&I(n.hwm,u)<=0){_();continue}n.hwm&&u&&I(n.hwm,u)>0&&z(n.hwm);continue}if(n.type==="bookmark"){W=!1;continue}if(n.type==="log"){if(D.has(n.id)){x(n.id,n.data?.created_at??""),M(U(n));continue}const k=typeof n.data?.message=="string"&&n.data.message.startsWith("[HEARTBEAT-PING]");if(n.data?.kind==="ack"){if(n.data?.author_drone_id===e.droneId&&await B(n)==="persisted-skip")continue;R(n);continue}if(n.data?.drone_id===e.droneId&&!k){R(n);continue}if(await B(n)==="persisted-skip")continue;R(n)}}}finally{p.removeEventListener("abort",L),m&&clearTimeout(m),_(),i.connected=!1}}async function Ke(e,t,o,a={}){const{ownerDeps:s,ownerStaleMs:r}={...V,...a},c=await j(e.cubeId,e.droneId,r,s);if(!c)return i.connected=!1,i.ownership=await b(e.cubeId,e.droneId,s),"skipped";i.ownership=await b(e.cubeId,e.droneId,s);try{return await X(e,t,o,a),"streamed"}finally{await c.release()}}async function*ye(e){const t=e.getReader(),o=new TextDecoder;let a="";try{for(;;){const{value:s,done:r}=await t.read();if(r){if(a.trim()){const l=J(a);l&&(yield l)}return}a+=o.decode(s,{stream:!0});let c;for(;(c=a.indexOf(`
5
5
 
6
- `))!==-1;){const l=a.slice(0,c);a=a.slice(c+2);const f=X(l);f&&(yield f)}}}finally{try{t.releaseLock()}catch{}}}function X(e){let t=null,o=null,a=[];for(const r of e.split(`
6
+ `))!==-1;){const l=a.slice(0,c);a=a.slice(c+2);const f=J(l);f&&(yield f)}}}finally{try{t.releaseLock()}catch{}}}function J(e){let t=null,o=null,a=[];for(const r of e.split(`
7
7
  `))r.startsWith("event:")?t=r.slice(6).trim():r.startsWith("id:")?o=r.slice(3).trim():r.startsWith("data:")&&a.push(r.slice(5).trim());const s=a.join(`
8
- `);if(!t)return null;if(t==="log"){if(!o)return null;let r;try{r=JSON.parse(s)}catch{return null}return{type:"log",id:o,data:r}}if(t==="heartbeat"){let r=null,c=null;try{const l=JSON.parse(s);r=typeof l.ts=="string"?l.ts:null,c=ge(l.hwm)}catch{}return{type:"heartbeat",ts:r,hwm:c}}if(t==="bookmark"){let r=null;try{const c=JSON.parse(s);r=typeof c.as_of=="string"?c.as_of:null}catch{}return{type:"bookmark",as_of:r}}return{type:"unknown",raw:e}}function ge(e){if(!e||typeof e!="object")return null;const t=e;return typeof t.id=="string"&&t.id.length>0&&typeof t.created_at=="string"&&t.created_at.length>0?{id:t.id,created_at:t.created_at}:null}function y(e,t){const o=Date.parse(e.created_at),a=Date.parse(t.created_at);return Number.isFinite(o)&&Number.isFinite(a)&&o!==a?o-a:e.created_at!==t.created_at?e.created_at<t.created_at?-1:1:e.id.localeCompare(t.id)}function J(e){if(e.data?.visibility==="direct"||e.data?.kind==="ack")return null;const t=e.data?.created_at;return typeof t=="string"&&t.length>0?{id:e.id,created_at:t}:null}function be(e,t){return!e||typeof e!="object"?{id:t}:typeof e.id=="string"&&e.id.length>0?e:{...e,id:t}}function ye(...e){for(const t of e)if(typeof t=="string"&&t.length>0)return t;return""}function Ie(e){const t=typeof e.created_at=="string"?new Date(e.created_at).toISOString():new Date().toISOString(),o=e.drone_label??"?",a=e.role_name??"?",s=typeof e.message=="string"?e.message:"",r=ye(e.id,e.entry_id),c=r?`[entry_id: ${r}] `:"",l=s.replace(/\r\n|\r|\n/g," \u23CE ");return`${t} ${o} (${a}): ${c}${l}`}async function _e(e,t,o){const a=B(e,t);await Ee(a,o,R)}async function Ee(e,t,o=R,a=o*2){await h.mkdir(x.dirname(e),{recursive:!0}),await h.appendFile(e,t+`
9
- `,"utf-8"),await Ae(e,o,a)}async function Ae(e,t,o=t){if(!Number.isInteger(t)||t<1)throw new Error("maxLines must be a positive integer");if(!Number.isInteger(o)||o<t)throw new Error("trimThresholdLines must be an integer >= maxLines");const s=(await h.readFile(e,"utf-8")).split(/\r?\n/);if(s.at(-1)===""&&s.pop(),s.length<=o)return;const r=s.slice(-t),c=x.join(x.dirname(e),`.${x.basename(e)}.${process.pid}.${Date.now()}.tmp`);try{await h.writeFile(c,r.join(`
8
+ `);if(!t)return null;if(t==="log"){if(!o)return null;let r;try{r=JSON.parse(s)}catch{return null}return{type:"log",id:o,data:r}}if(t==="heartbeat"){let r=null,c=null;try{const l=JSON.parse(s);r=typeof l.ts=="string"?l.ts:null,c=_e(l.hwm)}catch{}return{type:"heartbeat",ts:r,hwm:c}}if(t==="bookmark"){let r=null;try{const c=JSON.parse(s);r=typeof c.as_of=="string"?c.as_of:null}catch{}return{type:"bookmark",as_of:r}}return{type:"unknown",raw:e}}function _e(e){if(!e||typeof e!="object")return null;const t=e;return typeof t.id=="string"&&t.id.length>0&&typeof t.created_at=="string"&&t.created_at.length>0?{id:t.id,created_at:t.created_at}:null}function I(e,t){const o=Date.parse(e.created_at),a=Date.parse(t.created_at);return Number.isFinite(o)&&Number.isFinite(a)&&o!==a?o-a:e.created_at!==t.created_at?e.created_at<t.created_at?-1:1:e.id.localeCompare(t.id)}function U(e){if(e.data?.visibility==="direct"||e.data?.kind==="ack")return null;const t=e.data?.created_at;return typeof t=="string"&&t.length>0?{id:e.id,created_at:t}:null}function Ee(e,t){return!e||typeof e!="object"?{id:t}:typeof e.id=="string"&&e.id.length>0?e:{...e,id:t}}function Ae(...e){for(const t of e)if(typeof t=="string"&&t.length>0)return t;return""}function Se(e){const t=typeof e.created_at=="string"?new Date(e.created_at).toISOString():new Date().toISOString(),o=e.drone_label??"?",a=e.role_name??"?",s=typeof e.message=="string"?e.message:"",r=Ae(e.id,e.entry_id),c=r?`[entry_id: ${r}] `:"",l=s.replace(/\r\n|\r|\n/g," \u23CE ");return`${t} ${o} (${a}): ${c}${l}`}async function Te(e,t,o){const a=G(e,t);await xe(a,o,v)}async function xe(e,t,o=v,a=o*2){await h.mkdir(C.dirname(e),{recursive:!0}),await h.appendFile(e,t+`
9
+ `,"utf-8"),await ke(e,o,a)}async function ke(e,t,o=t){if(!Number.isInteger(t)||t<1)throw new Error("maxLines must be a positive integer");if(!Number.isInteger(o)||o<t)throw new Error("trimThresholdLines must be an integer >= maxLines");const s=(await h.readFile(e,"utf-8")).split(/\r?\n/);if(s.at(-1)===""&&s.pop(),s.length<=o)return;const r=s.slice(-t),c=C.join(C.dirname(e),`.${C.basename(e)}.${process.pid}.${Date.now()}.tmp`);try{await h.writeFile(c,r.join(`
10
10
  `)+`
11
- `,"utf-8"),await h.rename(c,e)}catch(l){throw await h.rm(c,{force:!0}),l}}function Se(e,t,o){if(t&&e.includes(`[entry_id: ${t}]`))return!0;const a=t?o.replace(`[entry_id: ${t}] `,""):o;return!!(a&&e.split(/\r?\n/).includes(a))}async function Te(e,t,o,a){const s=B(e,t);let r;try{r=await h.readFile(s,"utf-8")}catch(c){if(c?.code==="ENOENT")return!1;throw c}return Se(r,o,a)}function I(e){return new Promise(t=>setTimeout(t,e))}export{R as INBOX_TAIL_LINES_CAP,Oe as INBOX_TAIL_TRIM_THRESHOLD_LINES,ve as __resetStreamStateForTest,Ee as appendCappedInboxLine,pe as classifyRunLoopHealth,y as compareBroadcastHwm,Ie as formatInboxLine,$e as getStreamStatus,Se as inboxRawHasEntry,he as parseSSE,Pe as startLogStream,V as streamOnce,We as streamOnceIfOwner,Ae as trimInboxFileToRecentLines};
11
+ `,"utf-8"),await h.rename(c,e)}catch(l){throw await h.rm(c,{force:!0}),l}}function Ce(e,t,o){if(t&&e.includes(`[entry_id: ${t}]`))return!0;const a=t?o.replace(`[entry_id: ${t}] `,""):o;return!!(a&&e.split(/\r?\n/).includes(a))}async function He(e,t,o,a){const s=G(e,t);let r;try{r=await h.readFile(s,"utf-8")}catch(c){if(c?.code==="ENOENT")return!1;throw c}return Ce(r,o,a)}function y(e){return new Promise(t=>setTimeout(t,e))}export{v as INBOX_TAIL_LINES_CAP,Pe as INBOX_TAIL_TRIM_THRESHOLD_LINES,Ge as __resetCodexHeartbeatForTest,Be as __resetStreamStateForTest,xe as appendCappedInboxLine,we as classifyRunLoopHealth,I as compareBroadcastHwm,he as ensureCodexHeartbeatStarted,Se as formatInboxLine,We as getStreamStatus,Ce as inboxRawHasEntry,ye as parseSSE,je as startLogStream,X as streamOnce,Ke as streamOnceIfOwner,ke as trimInboxFileToRecentLines};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "borgmcp",
3
- "version": "1.0.23",
3
+ "version": "1.0.24",
4
4
  "description": "Coordinate AI coding agents in shared cubes. Works with Claude Code and Codex. Create projects, assign roles, and share a live activity log.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",