borgmcp 1.0.5 → 1.0.7
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/assimilate-cmd.js +39 -497
- package/dist/assimilate-deps.js +3 -177
- package/dist/assimilate-welcome.js +2 -24
- package/dist/auth-env.js +1 -107
- package/dist/auth.js +23 -612
- package/dist/claude.js +11 -281
- package/dist/cli-help.js +29 -50
- package/dist/cli-platform.js +4 -94
- package/dist/codex-app-server.js +4 -228
- package/dist/codex-app-wake.js +2 -122
- package/dist/codex-launch.js +1 -81
- package/dist/codex-remote.js +1 -250
- package/dist/config-utils.js +3 -385
- package/dist/config.js +1 -190
- package/dist/console-prefix.js +1 -86
- package/dist/cube-name.js +1 -65
- package/dist/cubes.js +4 -269
- package/dist/debug.js +1 -71
- package/dist/device-auth.js +1 -167
- package/dist/direct-log.js +1 -11
- package/dist/health-beat.js +1 -168
- package/dist/inbox-monitor.js +1 -129
- package/dist/index.js +26 -1378
- package/dist/lifecycle-log-guard.js +2 -93
- package/dist/list-roles-render.js +6 -39
- package/dist/log-audit.js +3 -186
- package/dist/log-stream.js +9 -848
- package/dist/name-validator.js +1 -22
- package/dist/parse-assimilate-args.js +1 -82
- package/dist/postinstall.js +8 -22
- package/dist/regen-format.js +11 -329
- package/dist/regen.js +5 -83
- package/dist/remote-client.js +1 -695
- package/dist/role-resolver.js +1 -36
- package/dist/role-section.js +8 -208
- package/dist/roster-render.js +3 -96
- package/dist/setup.js +36 -251
- package/dist/shell-escape.js +1 -22
- package/dist/spawn.js +10 -29
- package/dist/stale-version-check.js +1 -102
- package/dist/stream-owner.js +2 -202
- package/dist/stream-status.js +3 -211
- package/dist/subscription-retry.js +1 -23
- package/dist/sync-roles-render.js +3 -118
- package/dist/sync.js +22 -286
- package/dist/templates.js +120 -563
- package/dist/terminal-title.js +1 -68
- package/dist/token-crypto.js +1 -91
- package/dist/token-store.js +1 -222
- package/dist/types.js +0 -5
- package/dist/version.js +2 -78
- package/dist/worktree-lifecycle.js +2 -173
- package/package.json +11 -2
- package/dist/assimilate-cmd.d.ts.map +0 -1
- package/dist/assimilate-cmd.js.map +0 -1
- package/dist/assimilate-deps.d.ts.map +0 -1
- package/dist/assimilate-deps.js.map +0 -1
- package/dist/assimilate-welcome.d.ts.map +0 -1
- package/dist/assimilate-welcome.js.map +0 -1
- package/dist/auth-env.d.ts.map +0 -1
- package/dist/auth-env.js.map +0 -1
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js.map +0 -1
- package/dist/claude.d.ts.map +0 -1
- package/dist/claude.js.map +0 -1
- package/dist/cli-help.d.ts.map +0 -1
- package/dist/cli-help.js.map +0 -1
- package/dist/cli-platform.d.ts.map +0 -1
- package/dist/cli-platform.js.map +0 -1
- package/dist/codex-app-server.d.ts.map +0 -1
- package/dist/codex-app-server.js.map +0 -1
- package/dist/codex-app-wake.d.ts.map +0 -1
- package/dist/codex-app-wake.js.map +0 -1
- package/dist/codex-launch.d.ts.map +0 -1
- package/dist/codex-launch.js.map +0 -1
- package/dist/codex-remote.d.ts.map +0 -1
- package/dist/codex-remote.js.map +0 -1
- package/dist/config-utils.d.ts.map +0 -1
- package/dist/config-utils.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/console-prefix.d.ts.map +0 -1
- package/dist/console-prefix.js.map +0 -1
- package/dist/cube-name.d.ts.map +0 -1
- package/dist/cube-name.js.map +0 -1
- package/dist/cubes.d.ts.map +0 -1
- package/dist/cubes.js.map +0 -1
- package/dist/debug.d.ts.map +0 -1
- package/dist/debug.js.map +0 -1
- package/dist/device-auth.d.ts.map +0 -1
- package/dist/device-auth.js.map +0 -1
- package/dist/direct-log.d.ts.map +0 -1
- package/dist/direct-log.js.map +0 -1
- package/dist/health-beat.d.ts.map +0 -1
- package/dist/health-beat.js.map +0 -1
- package/dist/inbox-monitor.d.ts.map +0 -1
- package/dist/inbox-monitor.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/lifecycle-log-guard.d.ts.map +0 -1
- package/dist/lifecycle-log-guard.js.map +0 -1
- package/dist/list-roles-render.d.ts.map +0 -1
- package/dist/list-roles-render.js.map +0 -1
- package/dist/log-audit.d.ts.map +0 -1
- package/dist/log-audit.js.map +0 -1
- package/dist/log-stream.d.ts.map +0 -1
- package/dist/log-stream.js.map +0 -1
- package/dist/name-validator.d.ts.map +0 -1
- package/dist/name-validator.js.map +0 -1
- package/dist/parse-assimilate-args.d.ts.map +0 -1
- package/dist/parse-assimilate-args.js.map +0 -1
- package/dist/postinstall.d.ts.map +0 -1
- package/dist/postinstall.js.map +0 -1
- package/dist/regen-format.d.ts.map +0 -1
- package/dist/regen-format.js.map +0 -1
- package/dist/regen.d.ts.map +0 -1
- package/dist/regen.js.map +0 -1
- package/dist/remote-client.d.ts.map +0 -1
- package/dist/remote-client.js.map +0 -1
- package/dist/role-resolver.d.ts.map +0 -1
- package/dist/role-resolver.js.map +0 -1
- package/dist/role-section.d.ts.map +0 -1
- package/dist/role-section.js.map +0 -1
- package/dist/roster-render.d.ts.map +0 -1
- package/dist/roster-render.js.map +0 -1
- package/dist/setup.d.ts.map +0 -1
- package/dist/setup.js.map +0 -1
- package/dist/shell-escape.d.ts.map +0 -1
- package/dist/shell-escape.js.map +0 -1
- package/dist/spawn.d.ts.map +0 -1
- package/dist/spawn.js.map +0 -1
- package/dist/stale-version-check.d.ts.map +0 -1
- package/dist/stale-version-check.js.map +0 -1
- package/dist/stream-owner.d.ts.map +0 -1
- package/dist/stream-owner.js.map +0 -1
- package/dist/stream-status.d.ts.map +0 -1
- package/dist/stream-status.js.map +0 -1
- package/dist/subscription-retry.d.ts.map +0 -1
- package/dist/subscription-retry.js.map +0 -1
- package/dist/sync-roles-render.d.ts.map +0 -1
- package/dist/sync-roles-render.js.map +0 -1
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js.map +0 -1
- package/dist/templates.d.ts.map +0 -1
- package/dist/templates.js.map +0 -1
- package/dist/terminal-title.d.ts.map +0 -1
- package/dist/terminal-title.js.map +0 -1
- package/dist/token-crypto.d.ts.map +0 -1
- package/dist/token-crypto.js.map +0 -1
- package/dist/token-store.d.ts.map +0 -1
- package/dist/token-store.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js.map +0 -1
- package/dist/worktree-lifecycle.d.ts.map +0 -1
- package/dist/worktree-lifecycle.js.map +0 -1
package/dist/shell-escape.js
CHANGED
|
@@ -1,22 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// Sprint 18 (gh-tracked via dispatch 10:41:47Z): when the CLI emits a
|
|
4
|
-
// `cd <path>` line for the user to copy-paste into their shell, the path is
|
|
5
|
-
// a filesystem-controlled string with no character constraints (unlike
|
|
6
|
-
// DB-CHECK-constrained role/cube names). Paths can legally contain spaces,
|
|
7
|
-
// `$VAR`, backticks, `$(cmd)`, embedded single-quotes, and other shell
|
|
8
|
-
// metacharacters that would execute on paste under naive emission.
|
|
9
|
-
//
|
|
10
|
-
// drone-11 SR-LANE (cube entry 2026-05-19T10:44:35Z) escalation: double-
|
|
11
|
-
// quoting handles spaces but `$VAR` / backtick / `$(cmd)` still expand
|
|
12
|
-
// inside double-quotes. Single-quotes with internal-quote escape (`'\''`
|
|
13
|
-
// = close-quote / escaped-quote / re-open-quote) defang every shell
|
|
14
|
-
// metachar including embedded `'`. POSIX-defined, copy-paste-runnable
|
|
15
|
-
// across bash / zsh / dash / sh.
|
|
16
|
-
//
|
|
17
|
-
// Behavior: returns the input wrapped in single-quotes with any internal
|
|
18
|
-
// `'` rewritten as `'\''`. Empty string returns `''`.
|
|
19
|
-
export function shellEscape(s) {
|
|
20
|
-
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
21
|
-
}
|
|
22
|
-
//# sourceMappingURL=shell-escape.js.map
|
|
1
|
+
function r(e){return`'${e.replace(/'/g,"'\\''")}'`}export{r as shellEscape};
|
package/dist/spawn.js
CHANGED
|
@@ -1,29 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* `validateName` continues to be re-exported from here for callers
|
|
12
|
-
* that still depend on the symbol (it lives in `name-validator.ts`
|
|
13
|
-
* now — single source of truth).
|
|
14
|
-
*/
|
|
15
|
-
export { validateName } from './name-validator.js';
|
|
16
|
-
export async function runSpawn() {
|
|
17
|
-
const msg = 'borg spawn is removed. Use:\n' +
|
|
18
|
-
' borg assimilate [role] --worktree <name>\n' +
|
|
19
|
-
'\n' +
|
|
20
|
-
'Example: if you previously ran `borg spawn drone-2`, the equivalent is\n' +
|
|
21
|
-
' borg assimilate --worktree drone-2\n' +
|
|
22
|
-
'(role is optional; defaults per first-drone rules)\n' +
|
|
23
|
-
'\n' +
|
|
24
|
-
'If you want a specific role:\n' +
|
|
25
|
-
' borg assimilate builder --worktree drone-2\n';
|
|
26
|
-
process.stderr.write(msg);
|
|
27
|
-
return 2;
|
|
28
|
-
}
|
|
29
|
-
//# sourceMappingURL=spawn.js.map
|
|
1
|
+
import{validateName as s}from"./name-validator.js";async function r(){return process.stderr.write(`borg spawn is removed. Use:
|
|
2
|
+
borg assimilate [role] --worktree <name>
|
|
3
|
+
|
|
4
|
+
Example: if you previously ran \`borg spawn drone-2\`, the equivalent is
|
|
5
|
+
borg assimilate --worktree drone-2
|
|
6
|
+
(role is optional; defaults per first-drone rules)
|
|
7
|
+
|
|
8
|
+
If you want a specific role:
|
|
9
|
+
borg assimilate builder --worktree drone-2
|
|
10
|
+
`),2}export{r as runSpawn,s as validateName};
|
|
@@ -1,102 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Sprint 7 (b) — stale-binary defensive hardening (gh#148 close-out).
|
|
3
|
-
*
|
|
4
|
-
* drone-3's v0.8.0 case (cube log 2026-05-18T10:35Z) ran for months
|
|
5
|
-
* without warning that v0.8.10 had shipped the gh#71 carve-out for
|
|
6
|
-
* own-drone heartbeat-pings. The silent-stale-binary failure mode
|
|
7
|
-
* costs cube collective time to diagnose because the gap between
|
|
8
|
-
* "installed version" and "latest published" is invisible to the
|
|
9
|
-
* operator.
|
|
10
|
-
*
|
|
11
|
-
* This module surfaces that gap at borg launch time via a stderr
|
|
12
|
-
* warning. Network check against npmjs.org registry is async +
|
|
13
|
-
* timeout-gated so a slow registry doesn't block the operator. Fails
|
|
14
|
-
* silently on any error (network failure, registry change,
|
|
15
|
-
* prerelease version, etc.) — defense-in-depth, never a blocker.
|
|
16
|
-
*/
|
|
17
|
-
const NPM_REGISTRY_LATEST_URL = 'https://registry.npmjs.org/borgmcp/latest';
|
|
18
|
-
const FETCH_TIMEOUT_MS = 2000;
|
|
19
|
-
const MINOR_VERSIONS_BEHIND_THRESHOLD = 1;
|
|
20
|
-
/**
|
|
21
|
-
* Pure compare: given an installed version string and a latest version
|
|
22
|
-
* string, decide whether to warn. Both strings are expected in semver
|
|
23
|
-
* `MAJOR.MINOR.PATCH` form (no prerelease, no build metadata).
|
|
24
|
-
*
|
|
25
|
-
* Returns `{stale: true, message}` when installed is at least
|
|
26
|
-
* MINOR_VERSIONS_BEHIND_THRESHOLD minor versions behind latest on the
|
|
27
|
-
* same major. Defaults conservatively — unparseable input, prerelease
|
|
28
|
-
* tags, or anything weird returns `stale: false` so we never
|
|
29
|
-
* false-positive on edge cases.
|
|
30
|
-
*/
|
|
31
|
-
export function compareVersionsForStaleness(installed, latest) {
|
|
32
|
-
const installedParts = parseSemver(installed);
|
|
33
|
-
const latestParts = parseSemver(latest);
|
|
34
|
-
if (!installedParts || !latestParts) {
|
|
35
|
-
return { stale: false, message: null };
|
|
36
|
-
}
|
|
37
|
-
// Don't warn across major-version transitions — those are
|
|
38
|
-
// explicit migrations the operator is aware of (or should be); the
|
|
39
|
-
// warning shape isn't the right surface for them.
|
|
40
|
-
if (installedParts.major !== latestParts.major) {
|
|
41
|
-
return { stale: false, message: null };
|
|
42
|
-
}
|
|
43
|
-
const minorDelta = latestParts.minor - installedParts.minor;
|
|
44
|
-
if (minorDelta < MINOR_VERSIONS_BEHIND_THRESHOLD) {
|
|
45
|
-
return { stale: false, message: null };
|
|
46
|
-
}
|
|
47
|
-
// Lead-with-warning shape per drone-7 UX-FOLLOWUP 2026-05-18T13:49:54Z:
|
|
48
|
-
// both versions explicit (no "N versions behind" count ambiguity that
|
|
49
|
-
// flattens severity perception), single actionable command, no
|
|
50
|
-
// backticks (terminal renders them as literal characters). Fits the
|
|
51
|
-
// Coordinator-discipline 80-char preview rule. drone-1 ratified the
|
|
52
|
-
// (X) network-check approach + this copy shape at 13:49:59Z.
|
|
53
|
-
// minorDelta is informational; intentionally not surfaced.
|
|
54
|
-
void minorDelta;
|
|
55
|
-
const message = `⚠ borgmcp ${installed} is behind latest ${latest} — npm install -g borgmcp@latest`;
|
|
56
|
-
return { stale: true, message };
|
|
57
|
-
}
|
|
58
|
-
function parseSemver(s) {
|
|
59
|
-
// Strict MAJOR.MINOR.PATCH; rejects prerelease tags, build metadata,
|
|
60
|
-
// empty / 'unknown' / malformed strings. Conservative on purpose:
|
|
61
|
-
// ambiguous version strings should not trigger a warning.
|
|
62
|
-
if (typeof s !== 'string' || s.length === 0)
|
|
63
|
-
return null;
|
|
64
|
-
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(s);
|
|
65
|
-
if (!match)
|
|
66
|
-
return null;
|
|
67
|
-
return {
|
|
68
|
-
major: parseInt(match[1], 10),
|
|
69
|
-
minor: parseInt(match[2], 10),
|
|
70
|
-
patch: parseInt(match[3], 10),
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Fetch the latest published borgmcp version from the npm registry.
|
|
75
|
-
* Returns the version string on success, `null` on any failure
|
|
76
|
-
* (timeout, network error, registry change, malformed response).
|
|
77
|
-
* Never throws — caller treats null as "skip the warning."
|
|
78
|
-
*/
|
|
79
|
-
export async function fetchLatestBorgmcpVersion() {
|
|
80
|
-
const controller = new AbortController();
|
|
81
|
-
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
82
|
-
try {
|
|
83
|
-
const response = await fetch(NPM_REGISTRY_LATEST_URL, {
|
|
84
|
-
signal: controller.signal,
|
|
85
|
-
headers: { Accept: 'application/json' },
|
|
86
|
-
});
|
|
87
|
-
if (!response.ok)
|
|
88
|
-
return null;
|
|
89
|
-
const body = (await response.json());
|
|
90
|
-
if (typeof body.version !== 'string' || body.version.length === 0)
|
|
91
|
-
return null;
|
|
92
|
-
return body.version;
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
95
|
-
// AbortError, network error, parse error — all treated as "skip silently."
|
|
96
|
-
return null;
|
|
97
|
-
}
|
|
98
|
-
finally {
|
|
99
|
-
clearTimeout(timeout);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
//# sourceMappingURL=stale-version-check.js.map
|
|
1
|
+
const a="https://registry.npmjs.org/borgmcp/latest",i=2e3,c=1;function u(t,e){const r=o(t),n=o(e);if(!r||!n)return{stale:!1,message:null};if(r.major!==n.major)return{stale:!1,message:null};const s=n.minor-r.minor;return s<1?{stale:!1,message:null}:{stale:!0,message:`\u26A0 borgmcp ${t} is behind latest ${e} \u2014 npm install -g borgmcp@latest`}}function o(t){if(typeof t!="string"||t.length===0)return null;const e=/^(\d+)\.(\d+)\.(\d+)$/.exec(t);return e?{major:parseInt(e[1],10),minor:parseInt(e[2],10),patch:parseInt(e[3],10)}:null}async function m(){const t=new AbortController,e=setTimeout(()=>t.abort(),2e3);try{const r=await fetch(a,{signal:t.signal,headers:{Accept:"application/json"}});if(!r.ok)return null;const n=await r.json();return typeof n.version!="string"||n.version.length===0?null:n.version}catch{return null}finally{clearTimeout(e)}}export{u as compareVersionsForStaleness,m as fetchLatestBorgmcpVersion};
|
package/dist/stream-owner.js
CHANGED
|
@@ -1,202 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6
|
-
const STREAM_LOCKS_DIR = path.join(homedir(), '.config', 'borgmcp', 'stream-locks');
|
|
7
|
-
const OWNER_FILE = 'owner.json';
|
|
8
|
-
const SCHEMA_VERSION = 1;
|
|
9
|
-
export const STREAM_OWNER_STALE_MS = 70_000;
|
|
10
|
-
const processNonce = randomUUID();
|
|
11
|
-
const processStartedAt = new Date().toISOString();
|
|
12
|
-
export function streamLockPath(cubeId, droneId, locksDir = STREAM_LOCKS_DIR) {
|
|
13
|
-
assertUuid('cubeId', cubeId);
|
|
14
|
-
assertUuid('droneId', droneId);
|
|
15
|
-
return path.join(locksDir, cubeId, `${droneId}.lock`);
|
|
16
|
-
}
|
|
17
|
-
export async function acquireStreamLease(cubeId, droneId, staleMs = STREAM_OWNER_STALE_MS, deps = {}) {
|
|
18
|
-
const lockPath = streamLockPath(cubeId, droneId, deps.locksDir);
|
|
19
|
-
await fs.mkdir(path.dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
20
|
-
const lease = await tryCreateLease(lockPath, deps);
|
|
21
|
-
if (lease)
|
|
22
|
-
return lease;
|
|
23
|
-
const snapshot = await readOwnershipSnapshot(cubeId, droneId, deps);
|
|
24
|
-
if (snapshot.state !== 'owned-by-other-process') {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
const stale = (snapshot.ageMs ?? 0) > staleMs;
|
|
28
|
-
const pidDead = typeof snapshot.pid === 'number' &&
|
|
29
|
-
deps.isPidAlive !== undefined &&
|
|
30
|
-
!deps.isPidAlive(snapshot.pid);
|
|
31
|
-
if (!stale && !pidDead) {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
if (!(await moveStaleLockAside(lockPath, snapshot, staleMs, deps))) {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
return tryCreateLease(lockPath, deps);
|
|
38
|
-
}
|
|
39
|
-
export async function readOwnershipSnapshot(cubeId, droneId, deps = {}) {
|
|
40
|
-
const lockPath = streamLockPath(cubeId, droneId, deps.locksDir);
|
|
41
|
-
let raw;
|
|
42
|
-
try {
|
|
43
|
-
raw = await fs.readFile(path.join(lockPath, OWNER_FILE), 'utf8');
|
|
44
|
-
}
|
|
45
|
-
catch (err) {
|
|
46
|
-
if (err?.code === 'ENOENT')
|
|
47
|
-
return { state: 'unowned', lockPath };
|
|
48
|
-
throw err;
|
|
49
|
-
}
|
|
50
|
-
let parsed;
|
|
51
|
-
try {
|
|
52
|
-
parsed = JSON.parse(raw);
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
return { state: 'owned-by-other-process', lockPath, ageMs: Number.POSITIVE_INFINITY };
|
|
56
|
-
}
|
|
57
|
-
if (!isRecord(parsed)) {
|
|
58
|
-
return { state: 'owned-by-other-process', lockPath, ageMs: Number.POSITIVE_INFINITY };
|
|
59
|
-
}
|
|
60
|
-
const now = (deps.now ?? (() => new Date()))();
|
|
61
|
-
const heartbeatMs = Date.parse(parsed.heartbeatAt);
|
|
62
|
-
const ageMs = Number.isFinite(heartbeatMs) ? now.getTime() - heartbeatMs : Number.POSITIVE_INFINITY;
|
|
63
|
-
const ownPid = deps.pid ?? process.pid;
|
|
64
|
-
const ownNonce = deps.processNonce ?? processNonce;
|
|
65
|
-
const state = parsed.pid === ownPid && parsed.processNonce === ownNonce
|
|
66
|
-
? 'owner'
|
|
67
|
-
: 'owned-by-other-process';
|
|
68
|
-
return {
|
|
69
|
-
state,
|
|
70
|
-
pid: parsed.pid,
|
|
71
|
-
processNonce: parsed.processNonce,
|
|
72
|
-
cwd: parsed.cwd,
|
|
73
|
-
startedAt: parsed.startedAt,
|
|
74
|
-
heartbeatAt: parsed.heartbeatAt,
|
|
75
|
-
ageMs,
|
|
76
|
-
lockPath,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
async function tryCreateLease(lockPath, deps) {
|
|
80
|
-
try {
|
|
81
|
-
await fs.mkdir(lockPath, { mode: 0o700 });
|
|
82
|
-
}
|
|
83
|
-
catch (err) {
|
|
84
|
-
if (err?.code === 'EEXIST')
|
|
85
|
-
return null;
|
|
86
|
-
throw err;
|
|
87
|
-
}
|
|
88
|
-
const record = makeRecord(deps);
|
|
89
|
-
await writeRecord(lockPath, record);
|
|
90
|
-
return makeLease(lockPath, record, deps);
|
|
91
|
-
}
|
|
92
|
-
async function moveStaleLockAside(lockPath, snapshot, staleMs, deps) {
|
|
93
|
-
const takeoverPath = `${lockPath}.takeover-${deps.processNonce ?? processNonce}-${Date.now()}`;
|
|
94
|
-
try {
|
|
95
|
-
await fs.rename(lockPath, takeoverPath);
|
|
96
|
-
}
|
|
97
|
-
catch (err) {
|
|
98
|
-
if (err?.code === 'ENOENT')
|
|
99
|
-
return false;
|
|
100
|
-
throw err;
|
|
101
|
-
}
|
|
102
|
-
await deps.beforeTakeoverVerify?.(takeoverPath);
|
|
103
|
-
const verified = await readOwnershipRecord(takeoverPath);
|
|
104
|
-
if (!isStillReclaimable(snapshot, verified, staleMs, deps)) {
|
|
105
|
-
try {
|
|
106
|
-
await fs.rename(takeoverPath, lockPath);
|
|
107
|
-
}
|
|
108
|
-
catch (err) {
|
|
109
|
-
if (err?.code !== 'EEXIST')
|
|
110
|
-
throw err;
|
|
111
|
-
await fs.rm(takeoverPath, { recursive: true, force: true });
|
|
112
|
-
}
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
await fs.rm(takeoverPath, { recursive: true, force: true });
|
|
116
|
-
return true;
|
|
117
|
-
}
|
|
118
|
-
function isStillReclaimable(snapshot, current, staleMs, deps) {
|
|
119
|
-
if (!current) {
|
|
120
|
-
return snapshot.ageMs === Number.POSITIVE_INFINITY;
|
|
121
|
-
}
|
|
122
|
-
if (snapshot.pid !== current.pid ||
|
|
123
|
-
snapshot.processNonce !== current.processNonce ||
|
|
124
|
-
snapshot.heartbeatAt !== current.heartbeatAt) {
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
|
-
const now = (deps.now ?? (() => new Date()))();
|
|
128
|
-
const heartbeatMs = Date.parse(current.heartbeatAt);
|
|
129
|
-
const ageMs = Number.isFinite(heartbeatMs)
|
|
130
|
-
? now.getTime() - heartbeatMs
|
|
131
|
-
: Number.POSITIVE_INFINITY;
|
|
132
|
-
const stale = ageMs > staleMs;
|
|
133
|
-
const pidDead = deps.isPidAlive !== undefined && !deps.isPidAlive(current.pid);
|
|
134
|
-
return stale || pidDead;
|
|
135
|
-
}
|
|
136
|
-
function makeLease(lockPath, record, deps) {
|
|
137
|
-
return {
|
|
138
|
-
lockPath,
|
|
139
|
-
record,
|
|
140
|
-
async refresh() {
|
|
141
|
-
const current = await readOwnershipRecord(lockPath);
|
|
142
|
-
if (!current || current.pid !== record.pid || current.processNonce !== record.processNonce) {
|
|
143
|
-
return false;
|
|
144
|
-
}
|
|
145
|
-
const next = { ...record, heartbeatAt: (deps.now ?? (() => new Date()))().toISOString() };
|
|
146
|
-
await writeRecord(lockPath, next);
|
|
147
|
-
this.record = next;
|
|
148
|
-
return true;
|
|
149
|
-
},
|
|
150
|
-
async release() {
|
|
151
|
-
const current = await readOwnershipRecord(lockPath);
|
|
152
|
-
if (current?.pid === record.pid && current.processNonce === record.processNonce) {
|
|
153
|
-
await fs.rm(lockPath, { recursive: true, force: true });
|
|
154
|
-
}
|
|
155
|
-
},
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
async function readOwnershipRecord(lockPath) {
|
|
159
|
-
try {
|
|
160
|
-
const raw = await fs.readFile(path.join(lockPath, OWNER_FILE), 'utf8');
|
|
161
|
-
const parsed = JSON.parse(raw);
|
|
162
|
-
return isRecord(parsed) ? parsed : null;
|
|
163
|
-
}
|
|
164
|
-
catch {
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
async function writeRecord(lockPath, record) {
|
|
169
|
-
const ownerPath = path.join(lockPath, OWNER_FILE);
|
|
170
|
-
const tmpPath = path.join(lockPath, `${OWNER_FILE}.${record.processNonce}.tmp`);
|
|
171
|
-
await fs.writeFile(tmpPath, JSON.stringify(record, null, 2) + '\n', {
|
|
172
|
-
mode: 0o600,
|
|
173
|
-
});
|
|
174
|
-
await fs.rename(tmpPath, ownerPath);
|
|
175
|
-
}
|
|
176
|
-
function makeRecord(deps) {
|
|
177
|
-
const now = deps.now ?? (() => new Date());
|
|
178
|
-
return {
|
|
179
|
-
schemaVersion: SCHEMA_VERSION,
|
|
180
|
-
pid: deps.pid ?? process.pid,
|
|
181
|
-
processNonce: deps.processNonce ?? processNonce,
|
|
182
|
-
cwd: deps.cwd ?? process.cwd(),
|
|
183
|
-
startedAt: deps.processStartedAt ?? processStartedAt,
|
|
184
|
-
heartbeatAt: now().toISOString(),
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
function isRecord(value) {
|
|
188
|
-
return (value !== null &&
|
|
189
|
-
typeof value === 'object' &&
|
|
190
|
-
value.schemaVersion === SCHEMA_VERSION &&
|
|
191
|
-
typeof value.pid === 'number' &&
|
|
192
|
-
Number.isInteger(value.pid) &&
|
|
193
|
-
typeof value.processNonce === 'string' &&
|
|
194
|
-
typeof value.cwd === 'string' &&
|
|
195
|
-
typeof value.startedAt === 'string' &&
|
|
196
|
-
typeof value.heartbeatAt === 'string');
|
|
197
|
-
}
|
|
198
|
-
function assertUuid(label, value) {
|
|
199
|
-
if (!UUID_RE.test(value))
|
|
200
|
-
throw new Error(`Invalid ${label}: ${value}`);
|
|
201
|
-
}
|
|
202
|
-
//# sourceMappingURL=stream-owner.js.map
|
|
1
|
+
import{randomUUID as E}from"node:crypto";import{promises as a}from"node:fs";import{homedir as g}from"node:os";import c from"node:path";const T=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,O=c.join(g(),".config","borgmcp","stream-locks"),w="owner.json",N=1,D=7e4,p=E(),_=new Date().toISOString();function I(e,t,n=O){return y("cubeId",e),y("droneId",t),c.join(n,e,`${t}.lock`)}async function x(e,t,n=D,r={}){const o=I(e,t,r.locksDir);await a.mkdir(c.dirname(o),{recursive:!0,mode:448});const s=await h(o,r);if(s)return s;const i=await M(e,t,r);if(i.state!=="owned-by-other-process")return null;const u=(i.ageMs??0)>n,f=typeof i.pid=="number"&&r.isPidAlive!==void 0&&!r.isPidAlive(i.pid);return!u&&!f||!await R(o,i,n,r)?null:h(o,r)}async function M(e,t,n={}){const r=I(e,t,n.locksDir);let o;try{o=await a.readFile(c.join(r,w),"utf8")}catch(m){if(m?.code==="ENOENT")return{state:"unowned",lockPath:r};throw m}let s;try{s=JSON.parse(o)}catch{return{state:"owned-by-other-process",lockPath:r,ageMs:Number.POSITIVE_INFINITY}}if(!b(s))return{state:"owned-by-other-process",lockPath:r,ageMs:Number.POSITIVE_INFINITY};const i=(n.now??(()=>new Date))(),u=Date.parse(s.heartbeatAt),f=Number.isFinite(u)?i.getTime()-u:Number.POSITIVE_INFINITY,l=n.pid??process.pid,A=n.processNonce??p;return{state:s.pid===l&&s.processNonce===A?"owner":"owned-by-other-process",pid:s.pid,processNonce:s.processNonce,cwd:s.cwd,startedAt:s.startedAt,heartbeatAt:s.heartbeatAt,ageMs:f,lockPath:r}}async function h(e,t){try{await a.mkdir(e,{mode:448})}catch(r){if(r?.code==="EEXIST")return null;throw r}const n=$(t);return await S(e,n),V(e,n,t)}async function R(e,t,n,r){const o=`${e}.takeover-${r.processNonce??p}-${Date.now()}`;try{await a.rename(e,o)}catch(i){if(i?.code==="ENOENT")return!1;throw i}await r.beforeTakeoverVerify?.(o);const s=await d(o);if(!F(t,s,n,r)){try{await a.rename(o,e)}catch(i){if(i?.code!=="EEXIST")throw i;await a.rm(o,{recursive:!0,force:!0})}return!1}return await a.rm(o,{recursive:!0,force:!0}),!0}function F(e,t,n,r){if(!t)return e.ageMs===Number.POSITIVE_INFINITY;if(e.pid!==t.pid||e.processNonce!==t.processNonce||e.heartbeatAt!==t.heartbeatAt)return!1;const o=(r.now??(()=>new Date))(),s=Date.parse(t.heartbeatAt),u=(Number.isFinite(s)?o.getTime()-s:Number.POSITIVE_INFINITY)>n,f=r.isPidAlive!==void 0&&!r.isPidAlive(t.pid);return u||f}function V(e,t,n){return{lockPath:e,record:t,async refresh(){const r=await d(e);if(!r||r.pid!==t.pid||r.processNonce!==t.processNonce)return!1;const o={...t,heartbeatAt:(n.now??(()=>new Date))().toISOString()};return await S(e,o),this.record=o,!0},async release(){const r=await d(e);r?.pid===t.pid&&r.processNonce===t.processNonce&&await a.rm(e,{recursive:!0,force:!0})}}}async function d(e){try{const t=await a.readFile(c.join(e,w),"utf8"),n=JSON.parse(t);return b(n)?n:null}catch{return null}}async function S(e,t){const n=c.join(e,w),r=c.join(e,`${w}.${t.processNonce}.tmp`);await a.writeFile(r,JSON.stringify(t,null,2)+`
|
|
2
|
+
`,{mode:384}),await a.rename(r,n)}function $(e){const t=e.now??(()=>new Date);return{schemaVersion:N,pid:e.pid??process.pid,processNonce:e.processNonce??p,cwd:e.cwd??process.cwd(),startedAt:e.processStartedAt??_,heartbeatAt:t().toISOString()}}function b(e){return e!==null&&typeof e=="object"&&e.schemaVersion===N&&typeof e.pid=="number"&&Number.isInteger(e.pid)&&typeof e.processNonce=="string"&&typeof e.cwd=="string"&&typeof e.startedAt=="string"&&typeof e.heartbeatAt=="string"}function y(e,t){if(!T.test(t))throw new Error(`Invalid ${e}: ${t}`)}export{D as STREAM_OWNER_STALE_MS,x as acquireStreamLease,M as readOwnershipSnapshot,I as streamLockPath};
|
package/dist/stream-status.js
CHANGED
|
@@ -1,211 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* Split out from `index.ts` so the 5-state precedence logic and the
|
|
5
|
-
* `pgrep`-based liveness check can be unit-tested without spinning up
|
|
6
|
-
* the MCP server. drone-4's 18:30:51 UX contract is the spec for the
|
|
7
|
-
* rendered output shape; this module is the implementation surface.
|
|
8
|
-
*
|
|
9
|
-
* Top-line states (drone-4 contract):
|
|
10
|
-
* 1. Stream not started.
|
|
11
|
-
* 2. Stream connected, awaiting first content event.
|
|
12
|
-
* 3. Stream connected, last content <X> ago.
|
|
13
|
-
* 4. Stream disconnected (reconnect attempt N).
|
|
14
|
-
* 5. Stream connected (no inbox-Monitor — wake path broken).
|
|
15
|
-
*
|
|
16
|
-
* Precedence when both `disconnected` and `no inbox-Monitor` apply:
|
|
17
|
-
* prefer (4) — wire-disconnect is the upstream cause and resolves
|
|
18
|
-
* automatically when the wire comes back up; State 5 only matters
|
|
19
|
-
* when the wire is healthy but the file-watch isn't.
|
|
20
|
-
*/
|
|
21
|
-
import { spawnSync } from 'node:child_process';
|
|
22
|
-
/**
|
|
23
|
-
* Best-effort check: is a process tailing this inbox file?
|
|
24
|
-
*
|
|
25
|
-
* Returns:
|
|
26
|
-
* - true: at least one process matches `tail.*<inboxPath>` in pgrep
|
|
27
|
-
* - false: pgrep ran cleanly and found no match
|
|
28
|
-
* - null: cannot determine (pgrep unavailable, spawn error, no inbox path)
|
|
29
|
-
*
|
|
30
|
-
* The null case is informative — it means we don't know, so the
|
|
31
|
-
* renderer must NOT fire State 5 (which would be misleading). State 5
|
|
32
|
-
* only fires when we positively know the wake path is broken.
|
|
33
|
-
*
|
|
34
|
-
* Why `pgrep` and not a more elegant check: Claude Code Monitors are
|
|
35
|
-
* tail-based subprocesses spawned by the harness, completely opaque to
|
|
36
|
-
* the MCP server. The MCP server has no IPC channel into the harness's
|
|
37
|
-
* task table. The cheapest reliable signal we can get from inside the
|
|
38
|
-
* MCP server is "is there a tail subprocess open against this path?"
|
|
39
|
-
* — which is what `pgrep -f` answers.
|
|
40
|
-
*
|
|
41
|
-
* macOS + Linux ship `pgrep`. Windows doesn't (borgmcp targets Mac /
|
|
42
|
-
* Linux per package.json `os` field; the null branch handles other
|
|
43
|
-
* platforms gracefully).
|
|
44
|
-
*/
|
|
45
|
-
export function checkInboxMonitorHealthy(inboxPath) {
|
|
46
|
-
if (!inboxPath)
|
|
47
|
-
return null;
|
|
48
|
-
try {
|
|
49
|
-
// `-f` matches against the full command line so we catch the
|
|
50
|
-
// `tail -n 0 -F <inboxPath>` form. `-l` lists matches; we only
|
|
51
|
-
// need the exit code (0 = match, 1 = no match) and a sanity check
|
|
52
|
-
// on stdout (some pgrep variants exit 0 with empty stdout under
|
|
53
|
-
// permission errors — treat empty stdout as "no match" for safety).
|
|
54
|
-
const res = spawnSync('pgrep', ['-f', inboxPath], {
|
|
55
|
-
encoding: 'utf-8',
|
|
56
|
-
timeout: 2_000,
|
|
57
|
-
});
|
|
58
|
-
if (res.error)
|
|
59
|
-
return null;
|
|
60
|
-
if (res.status === 0 && res.stdout.trim().length > 0)
|
|
61
|
-
return true;
|
|
62
|
-
if (res.status === 1)
|
|
63
|
-
return false;
|
|
64
|
-
// pgrep exits 2 for syntax error, 3 for fatal — treat as unknown.
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Render the `borg:stream-status` markdown body per drone-4's 18:30:51
|
|
73
|
-
* contract. Pure function — no I/O, no clock reads. Caller assembles
|
|
74
|
-
* the inputs.
|
|
75
|
-
*/
|
|
76
|
-
export function renderStreamStatus(inputs) {
|
|
77
|
-
const { status, inboxMonitorHealthy, inboxPath, droneLabel, cubeName, humanAgo } = inputs;
|
|
78
|
-
const isNotStarted = status.reconnectAttempts === 0 &&
|
|
79
|
-
status.lastWireActivityAt === null &&
|
|
80
|
-
!status.connected;
|
|
81
|
-
const ownedByOther = status.ownership?.state === 'owned-by-other-process';
|
|
82
|
-
// Top-line verdict — 5 states + override per drone-4 contract.
|
|
83
|
-
// Precedence: disconnected > no-inbox-Monitor (wire-down upstream
|
|
84
|
-
// cause; State 5 only applies when wire is healthy).
|
|
85
|
-
let summary;
|
|
86
|
-
if (ownedByOther) {
|
|
87
|
-
summary = '**Stream owned by another Borg MCP process.**';
|
|
88
|
-
}
|
|
89
|
-
else if (isNotStarted) {
|
|
90
|
-
summary = '**Stream not started.**';
|
|
91
|
-
}
|
|
92
|
-
else if (!status.connected) {
|
|
93
|
-
summary = `**Stream disconnected (reconnect attempt ${status.reconnectAttempts}).**`;
|
|
94
|
-
}
|
|
95
|
-
else if (inboxMonitorHealthy === false) {
|
|
96
|
-
summary = '**Stream connected (no inbox-Monitor — wake path broken).**';
|
|
97
|
-
}
|
|
98
|
-
else if (status.lastContentEventAt === null) {
|
|
99
|
-
// State 2: wire works, no content yet. Collapses two underlying
|
|
100
|
-
// conditions per drone-4 contract — fresh connect pre-first-content
|
|
101
|
-
// and quiet cube post-reconnect. The body's heartbeat field
|
|
102
|
-
// distinguishes them (populated vs `_(none)_`).
|
|
103
|
-
summary = '**Stream connected, awaiting first content event.**';
|
|
104
|
-
}
|
|
105
|
-
else {
|
|
106
|
-
summary = `**Stream connected, last content ${humanAgo(new Date(status.lastContentEventAt))}.**`;
|
|
107
|
-
}
|
|
108
|
-
const lines = [];
|
|
109
|
-
lines.push(summary);
|
|
110
|
-
lines.push('');
|
|
111
|
-
lines.push('# Log-stream status');
|
|
112
|
-
lines.push('');
|
|
113
|
-
if (ownedByOther) {
|
|
114
|
-
lines.push('- **state**: _(stream owner is another local process)_');
|
|
115
|
-
}
|
|
116
|
-
else if (isNotStarted) {
|
|
117
|
-
lines.push('- **state**: _(stream not started)_');
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
lines.push(`- **connected**: ${status.connected}`);
|
|
121
|
-
}
|
|
122
|
-
// Body shape per drone-4 contract: three timestamp lines (content,
|
|
123
|
-
// heartbeat, wire) — looks redundant in the common case where they
|
|
124
|
-
// coincide, but the asymmetric "content quiet, heartbeats alive" case
|
|
125
|
-
// is exactly the diagnostic scenario this surface exists to support.
|
|
126
|
-
lines.push(`- **last content event**: ${status.lastContentEventAt
|
|
127
|
-
? `${status.lastContentEventAt} (${humanAgo(new Date(status.lastContentEventAt))})`
|
|
128
|
-
: '_(none yet)_'}`);
|
|
129
|
-
lines.push(`- **last heartbeat at**: ${status.lastHeartbeatAt
|
|
130
|
-
? `${status.lastHeartbeatAt} (${humanAgo(new Date(status.lastHeartbeatAt))})`
|
|
131
|
-
: '_(none)_'}`);
|
|
132
|
-
lines.push(`- **last wire activity**: ${status.lastWireActivityAt
|
|
133
|
-
? `${status.lastWireActivityAt} (${humanAgo(new Date(status.lastWireActivityAt))})`
|
|
134
|
-
: '_(none)_'}`);
|
|
135
|
-
lines.push(`- **last persisted event id**: ${status.lastPersistedEventId ?? '_(none)_'}`);
|
|
136
|
-
lines.push(`- **reconnect attempts**: ${status.reconnectAttempts}`);
|
|
137
|
-
if (ownedByOther) {
|
|
138
|
-
const owner = status.ownership;
|
|
139
|
-
lines.push(`- **stream owner pid**: ${owner.pid ?? '_(unknown)_'}`);
|
|
140
|
-
lines.push(`- **stream owner cwd**: ${owner.cwd ?? '_(unknown)_'}`);
|
|
141
|
-
lines.push(`- **stream owner heartbeat age**: ${typeof owner.ageMs === 'number' && Number.isFinite(owner.ageMs)
|
|
142
|
-
? `${Math.max(0, Math.round(owner.ageMs / 1000))}s`
|
|
143
|
-
: '_(unknown)_'}`);
|
|
144
|
-
}
|
|
145
|
-
// State-5 body line + self-arm instruction. Only fires when we
|
|
146
|
-
// POSITIVELY detected wake-path breakage AND the wire is up — the
|
|
147
|
-
// disconnected case takes precedence above and would never reach
|
|
148
|
-
// here. When `inboxMonitorHealthy === null` (couldn't determine) we
|
|
149
|
-
// stay silent; surfacing an uncertain failure mode is worse UX than
|
|
150
|
-
// omitting it.
|
|
151
|
-
if (status.connected && inboxMonitorHealthy === false) {
|
|
152
|
-
lines.push(`- **inbox-monitor**: _(no watcher detected — wake path broken)_`);
|
|
153
|
-
if (inboxPath && droneLabel && cubeName) {
|
|
154
|
-
lines.push('');
|
|
155
|
-
lines.push('## Real-time wake-up (do this now)');
|
|
156
|
-
lines.push('No process is tailing this drone\'s inbox file — this session won\'t wake on real-time cube activity, only on its fallback timer, and will miss live coordination from other drones. Arm an inbox Monitor:');
|
|
157
|
-
lines.push('');
|
|
158
|
-
lines.push(`> Monitor command: \`tail -n 0 -F ${inboxPath}\` — persistent, 1h timeout, description "borg inbox for ${droneLabel} on cube ${cubeName}".`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
return lines.join('\n');
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Gate predicate for the regen wake-path warning (gh#51 — extracted
|
|
165
|
-
* from the inline ternary in `client/src/index.ts` for direct unit-test
|
|
166
|
-
* coverage of the (connected × healthy) cross-product).
|
|
167
|
-
*
|
|
168
|
-
* Returns true ONLY when the wire is up AND we positively detected a
|
|
169
|
-
* dead inbox Monitor (`=== false` strict). The `null` branch
|
|
170
|
-
* (couldn't determine) stays silent — surfacing an uncertain failure
|
|
171
|
-
* mode is worse UX than omitting it (mirrors the State-5 precedence
|
|
172
|
-
* rule in `renderStreamStatus`). When disconnected, the wire-down case
|
|
173
|
-
* is the upstream cause and takes precedence; no point warning about
|
|
174
|
-
* the wake path when the wake-path's input has no events to deliver.
|
|
175
|
-
*/
|
|
176
|
-
export function shouldShowWakePathWarning(streamStatus, inboxMonitorHealthy) {
|
|
177
|
-
return streamStatus.connected && inboxMonitorHealthy === false;
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Wake-path-broken prefix for `borg:regen` output (gh#43).
|
|
181
|
-
*
|
|
182
|
-
* Pure function — caller decides whether to call (gates on
|
|
183
|
-
* `shouldShowWakePathWarning`). Returns an empty string when called
|
|
184
|
-
* with insufficient context to render the Monitor command (e.g., no
|
|
185
|
-
* inbox path on a no-active-cube path), so callers can always prepend
|
|
186
|
-
* the result unconditionally.
|
|
187
|
-
*
|
|
188
|
-
* Mirrors the State-5 self-arm instruction shape in
|
|
189
|
-
* `renderStreamStatus` so a drone sees the same Monitor command via
|
|
190
|
-
* both `borg:stream-status` and `borg:regen`. The differentiator: regen
|
|
191
|
-
* runs on every /loop iteration, so the prefix gives passive
|
|
192
|
-
* self-healing (worst-case latency = the /loop fallback heartbeat),
|
|
193
|
-
* whereas stream-status only surfaces the warning when actively
|
|
194
|
-
* called.
|
|
195
|
-
*/
|
|
196
|
-
export function formatWakePathPrefix(inputs) {
|
|
197
|
-
const { inboxPath, droneLabel, cubeName } = inputs;
|
|
198
|
-
if (!inboxPath || !droneLabel || !cubeName)
|
|
199
|
-
return '';
|
|
200
|
-
return [
|
|
201
|
-
`## ⚠ Wake path broken — arm Monitor NOW`,
|
|
202
|
-
``,
|
|
203
|
-
`No process is tailing this drone's inbox file. SSE delivery is healthy (entries reach disk), but Claude Code has no event source to wake on. Until you arm a Monitor, this session only wakes on the /loop fallback heartbeat and will miss live coordination from other drones:`,
|
|
204
|
-
``,
|
|
205
|
-
`> Monitor command: \`tail -n 0 -F ${inboxPath}\` — persistent, 1h timeout, description "borg inbox for ${droneLabel} on cube ${cubeName}".`,
|
|
206
|
-
``,
|
|
207
|
-
`---`,
|
|
208
|
-
``,
|
|
209
|
-
].join('\n');
|
|
210
|
-
}
|
|
211
|
-
//# sourceMappingURL=stream-status.js.map
|
|
1
|
+
import{spawnSync as p}from"node:child_process";function m(n){if(!n)return null;try{const t=p("pgrep",["-f",n],{encoding:"utf-8",timeout:2e3});return t.error?null:t.status===0&&t.stdout.trim().length>0?!0:t.status===1?!1:null}catch{return null}}function f(n){const{status:t,inboxMonitorHealthy:s,inboxPath:r,droneLabel:l,cubeName:u,humanAgo:i}=n,h=t.reconnectAttempts===0&&t.lastWireActivityAt===null&&!t.connected,c=t.ownership?.state==="owned-by-other-process";let o;c?o="**Stream owned by another Borg MCP process.**":h?o="**Stream not started.**":t.connected?s===!1?o="**Stream connected (no inbox-Monitor \u2014 wake path broken).**":t.lastContentEventAt===null?o="**Stream connected, awaiting first content event.**":o=`**Stream connected, last content ${i(new Date(t.lastContentEventAt))}.**`:o=`**Stream disconnected (reconnect attempt ${t.reconnectAttempts}).**`;const e=[];if(e.push(o),e.push(""),e.push("# Log-stream status"),e.push(""),c?e.push("- **state**: _(stream owner is another local process)_"):h?e.push("- **state**: _(stream not started)_"):e.push(`- **connected**: ${t.connected}`),e.push(`- **last content event**: ${t.lastContentEventAt?`${t.lastContentEventAt} (${i(new Date(t.lastContentEventAt))})`:"_(none yet)_"}`),e.push(`- **last heartbeat at**: ${t.lastHeartbeatAt?`${t.lastHeartbeatAt} (${i(new Date(t.lastHeartbeatAt))})`:"_(none)_"}`),e.push(`- **last wire activity**: ${t.lastWireActivityAt?`${t.lastWireActivityAt} (${i(new Date(t.lastWireActivityAt))})`:"_(none)_"}`),e.push(`- **last persisted event id**: ${t.lastPersistedEventId??"_(none)_"}`),e.push(`- **reconnect attempts**: ${t.reconnectAttempts}`),c){const a=t.ownership;e.push(`- **stream owner pid**: ${a.pid??"_(unknown)_"}`),e.push(`- **stream owner cwd**: ${a.cwd??"_(unknown)_"}`),e.push(`- **stream owner heartbeat age**: ${typeof a.ageMs=="number"&&Number.isFinite(a.ageMs)?`${Math.max(0,Math.round(a.ageMs/1e3))}s`:"_(unknown)_"}`)}return t.connected&&s===!1&&(e.push("- **inbox-monitor**: _(no watcher detected \u2014 wake path broken)_"),r&&l&&u&&(e.push(""),e.push("## Real-time wake-up (do this now)"),e.push("No process is tailing this drone's inbox file \u2014 this session won't wake on real-time cube activity, only on its fallback timer, and will miss live coordination from other drones. Arm an inbox Monitor:"),e.push(""),e.push(`> Monitor command: \`tail -n 0 -F ${r}\` \u2014 persistent, 1h timeout, description "borg inbox for ${l} on cube ${u}".`))),e.join(`
|
|
2
|
+
`)}function b(n,t){return n.connected&&t===!1}function w(n){const{inboxPath:t,droneLabel:s,cubeName:r}=n;return!t||!s||!r?"":["## \u26A0 Wake path broken \u2014 arm Monitor NOW","","No process is tailing this drone's inbox file. SSE delivery is healthy (entries reach disk), but Claude Code has no event source to wake on. Until you arm a Monitor, this session only wakes on the /loop fallback heartbeat and will miss live coordination from other drones:","",`> Monitor command: \`tail -n 0 -F ${t}\` \u2014 persistent, 1h timeout, description "borg inbox for ${s} on cube ${r}".`,"","---",""].join(`
|
|
3
|
+
`)}export{m as checkInboxMonitorHealthy,w as formatWakePathPrefix,f as renderStreamStatus,b as shouldShowWakePathWarning};
|
|
@@ -1,23 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Given the initial check result, retry up to `attempts` total times with
|
|
3
|
-
* `backoffMs` backoff, stopping early as soon as access is granted. A transient
|
|
4
|
-
* error on a retry is swallowed (keep the prior status and keep trying). Returns
|
|
5
|
-
* the latest status.
|
|
6
|
-
*/
|
|
7
|
-
export async function retrySubscriptionCheck(initial, deps) {
|
|
8
|
-
const attempts = deps.attempts ?? 3;
|
|
9
|
-
const backoffMs = deps.backoffMs ?? 2000;
|
|
10
|
-
let status = initial;
|
|
11
|
-
for (let attempt = 2; attempt <= attempts && !status.hasAccess; attempt++) {
|
|
12
|
-
deps.onRetry?.(attempt, attempts);
|
|
13
|
-
await deps.sleep(backoffMs);
|
|
14
|
-
try {
|
|
15
|
-
status = await deps.check();
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
// Transient error on a retry — keep the prior status and continue retrying.
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return status;
|
|
22
|
-
}
|
|
23
|
-
//# sourceMappingURL=subscription-retry.js.map
|
|
1
|
+
async function r(s,t){const e=t.attempts??3,o=t.backoffMs??2e3;let c=s;for(let a=2;a<=e&&!c.hasAccess;a++){t.onRetry?.(a,e),await t.sleep(o);try{c=await t.check()}catch{}}return c}export{r as retrySubscriptionCheck};
|