@bli-cockpit/cli 0.2.84 → 0.2.85
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/commands/doctor-memory-daemon.js +36 -0
- package/dist/commands/doctor.js +10 -0
- package/dist/commands/heartbeat.js +5 -0
- package/dist/commands/memory-daemon-probe.js +118 -0
- package/dist/commands/memory-hook-counts.js +6 -0
- package/dist/commands/memory-install-report.js +5 -0
- package/dist/commands/memory-install.js +5 -0
- package/dist/commands/ops-render-memory.js +4 -0
- package/dist/commands/ops.js +4 -0
- package/dist/commands/public-root.js +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `memory-daemon` row (BLI-3884): is the resident local answerer running?
|
|
3
|
+
*
|
|
4
|
+
* **This row is NEVER red, and it has no fix.** A machine with no daemon is a
|
|
5
|
+
* machine whose recalls take the path they took before the daemon existed —
|
|
6
|
+
* slower by a process start, a TLS handshake and a token check, and completely
|
|
7
|
+
* correct. Colouring that red would train people to ignore doctor, the way an
|
|
8
|
+
* amber "you could be faster" line trains them to ignore everything under it.
|
|
9
|
+
*
|
|
10
|
+
* So there are exactly two outcomes:
|
|
11
|
+
*
|
|
12
|
+
* ok something answered a `ping` on this user's endpoint.
|
|
13
|
+
* skipped nothing did, and the reason is named — `daemon_absent` (the
|
|
14
|
+
* ordinary state on a machine that has not prompted in two hours),
|
|
15
|
+
* `socket_path_too_long`, or whatever the socket said.
|
|
16
|
+
*
|
|
17
|
+
* Starting one is not doctor's job either: the PROMPT HOOK is the daemon's
|
|
18
|
+
* supervisor and spawns one at most once an hour (`memory-mcp`'s
|
|
19
|
+
* `daemon/spawn.ts`). A doctor that started a daemon would be a second
|
|
20
|
+
* supervisor with different rules.
|
|
21
|
+
*/
|
|
22
|
+
import { ok, skipped } from "./doctor-report.js";
|
|
23
|
+
import { memoryDaemonLine, probeMemoryDaemon } from "./memory-daemon-probe.js";
|
|
24
|
+
export async function checkMemoryDaemonState(context, deps = {}) {
|
|
25
|
+
const probe = deps.probe ?? probeMemoryDaemon;
|
|
26
|
+
const reading = await probe({
|
|
27
|
+
...(context.command.homeDir ? { homeDir: context.command.homeDir } : {}),
|
|
28
|
+
});
|
|
29
|
+
return memoryDaemonRow(reading);
|
|
30
|
+
}
|
|
31
|
+
export function memoryDaemonRow(reading) {
|
|
32
|
+
const sentence = memoryDaemonLine(reading);
|
|
33
|
+
return reading.answering
|
|
34
|
+
? ok("memory-daemon", "answering", sentence)
|
|
35
|
+
: skipped("memory-daemon", reading.reason, sentence);
|
|
36
|
+
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { checkSingleInstallState, fixAuthState, fixRootState, readAuthState, readRootState, } from "./doctor-access.js";
|
|
2
2
|
import { backfillCompletionStepState, checkBackfillState, checkDiskState, checkGcState, checkSyncState, fixBackfillState, fixDiskState, fixGcState, fixSyncState, syncBacklogDrainingVerdict, } from "./doctor-pipeline.js";
|
|
3
3
|
import { checkMcpAnswersState } from "./doctor-mcp.js";
|
|
4
|
+
import { checkMemoryDaemonState } from "./doctor-memory-daemon.js";
|
|
4
5
|
import { checkAutostartState, checkMemoryState, fixAutostartState, fixMemoryState, } from "./doctor-registration.js";
|
|
5
6
|
import { dryRunPreview, isInteractiveDoctorFix, maybeReportDoctorEvents, writeDoctorOutput, } from "./doctor-report.js";
|
|
6
7
|
import { refreshSetupReceipt } from "./setup-receipt.js";
|
|
@@ -104,6 +105,14 @@ function doctorInvariants() {
|
|
|
104
105
|
id: "mcp-answers",
|
|
105
106
|
check: (context) => context.deps.checkMcpAnswers(context),
|
|
106
107
|
},
|
|
108
|
+
// BLI-3884. Beside the two rows above because it is the same family — what
|
|
109
|
+
// this machine's agents can reach — and, like `mcp-answers`, it has NO fix:
|
|
110
|
+
// the prompt hook is the daemon's supervisor and starts one itself. It is
|
|
111
|
+
// never red; a machine without one recalls exactly as it did before.
|
|
112
|
+
{
|
|
113
|
+
id: "memory-daemon",
|
|
114
|
+
check: (context) => context.deps.checkMemoryDaemon(context),
|
|
115
|
+
},
|
|
107
116
|
{
|
|
108
117
|
id: "backfill-complete",
|
|
109
118
|
check: (context) => context.deps.checkBackfill(context),
|
|
@@ -146,6 +155,7 @@ function defaultDoctorDeps(hooks) {
|
|
|
146
155
|
checkMemory: checkMemoryState,
|
|
147
156
|
fixMemory: fixMemoryState,
|
|
148
157
|
checkMcpAnswers: (context) => checkMcpAnswersState(context),
|
|
158
|
+
checkMemoryDaemon: (context) => checkMemoryDaemonState(context),
|
|
149
159
|
checkBackfill: checkBackfillState,
|
|
150
160
|
fixBackfill: fixBackfillState,
|
|
151
161
|
checkGc: checkGcState,
|
|
@@ -317,6 +317,11 @@ export async function sendCollectorHeartbeatBestEffort(options) {
|
|
|
317
317
|
memory_hook_runs_24h: memory.receipt?.hook_runs_24h ?? null,
|
|
318
318
|
memory_hook_timeouts_24h: memory.receipt?.hook_timeouts_24h ?? null,
|
|
319
319
|
memory_hook_stats: memory.receipt?.hook_stats_reason ?? null,
|
|
320
|
+
// BLI-3884. Which route answered those recalls. `null` on a machine
|
|
321
|
+
// whose memory-mcp does not name one yet — never zero, which would
|
|
322
|
+
// read as "the daemon answered none".
|
|
323
|
+
memory_hook_via_daemon_24h: memory.receipt?.hook_via_daemon_24h ?? null,
|
|
324
|
+
memory_hook_via_direct_24h: memory.receipt?.hook_via_direct_24h ?? null,
|
|
320
325
|
}));
|
|
321
326
|
return true;
|
|
322
327
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IS THE RESIDENT MEMORY DAEMON ANSWERING? (BLI-3884)
|
|
3
|
+
*
|
|
4
|
+
* `bli-memory-mcp daemon` holds a warm connection, credential and scope so a
|
|
5
|
+
* prompt hook stops paying for a process, a TLS handshake and the device-token
|
|
6
|
+
* gate on every turn. Two collector surfaces need to know whether it is there:
|
|
7
|
+
* `cockpit doctor`'s `memory-daemon` row and `cockpit memory status`.
|
|
8
|
+
*
|
|
9
|
+
* The probe is written HERE, in twenty lines of `node:net`, rather than by
|
|
10
|
+
* importing the memory-mcp client: that package is a devDependency of this one
|
|
11
|
+
* and is not copied into the packed CLI, so an import would be green in the
|
|
12
|
+
* repo and missing on every fleet machine (the `runtimeFiles` trap this repo
|
|
13
|
+
* has paid for more than once). What IS shared is the CONTRACT — the address,
|
|
14
|
+
* the protocol version and the line limit come from
|
|
15
|
+
* `@bli-cockpit/telemetry-core`'s `memory-daemon-endpoint.ts`, which both
|
|
16
|
+
* packages depend on.
|
|
17
|
+
*
|
|
18
|
+
* **It never fixes anything and never fails anything.** A machine with no
|
|
19
|
+
* daemon is a machine whose hooks work exactly as they did before the daemon
|
|
20
|
+
* existed; this reports presence, absence and the reason, and that is all.
|
|
21
|
+
*/
|
|
22
|
+
import net from "node:net";
|
|
23
|
+
import os from "node:os";
|
|
24
|
+
import { MEMORY_DAEMON_MAX_LINE_BYTES, MEMORY_DAEMON_PROTOCOL_VERSION, memoryDaemonEndpoint, } from "@bli-cockpit/telemetry-core";
|
|
25
|
+
/** A probe waits this long in total. A live local socket answers in ~1 ms. */
|
|
26
|
+
export const DAEMON_PROBE_TIMEOUT_MS = 400;
|
|
27
|
+
export function probeMemoryDaemon(options = {}) {
|
|
28
|
+
const now = options.now ?? Date.now;
|
|
29
|
+
const started = now();
|
|
30
|
+
const endpoint = memoryDaemonEndpoint({
|
|
31
|
+
homeDir: options.homeDir ?? os.homedir(),
|
|
32
|
+
...(options.platform ? { platform: options.platform } : {}),
|
|
33
|
+
username: options.username ?? safeUsername(),
|
|
34
|
+
});
|
|
35
|
+
const base = { kind: endpoint.kind, pid: null, startedAt: null };
|
|
36
|
+
if (endpoint.kind === "unix" && endpoint.addressTooLong) {
|
|
37
|
+
return Promise.resolve({
|
|
38
|
+
...base,
|
|
39
|
+
answering: false,
|
|
40
|
+
reason: "socket_path_too_long",
|
|
41
|
+
elapsedMs: 0,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
let settled = false;
|
|
46
|
+
let buffered = "";
|
|
47
|
+
const finish = (answering, reason, extra = {}) => {
|
|
48
|
+
if (settled)
|
|
49
|
+
return;
|
|
50
|
+
settled = true;
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
socket.destroy();
|
|
53
|
+
resolve({ ...base, ...extra, answering, reason, elapsedMs: now() - started });
|
|
54
|
+
};
|
|
55
|
+
const timer = setTimeout(() => finish(false, "probe_timeout"), options.timeoutMs ?? DAEMON_PROBE_TIMEOUT_MS);
|
|
56
|
+
timer.unref?.();
|
|
57
|
+
const socket = (options.connectImpl ?? ((address) => net.createConnection({ path: address })))(endpoint.address);
|
|
58
|
+
socket.on("error", (error) => {
|
|
59
|
+
const code = error.code;
|
|
60
|
+
finish(false, code === "ENOENT" || code === "ECONNREFUSED"
|
|
61
|
+
? "daemon_absent"
|
|
62
|
+
: `daemon_socket_${(code ?? "error").toLowerCase()}`);
|
|
63
|
+
});
|
|
64
|
+
socket.on("connect", () => {
|
|
65
|
+
socket.write(`${JSON.stringify({ v: MEMORY_DAEMON_PROTOCOL_VERSION, event: "ping" })}\n`);
|
|
66
|
+
});
|
|
67
|
+
socket.on("data", (chunk) => {
|
|
68
|
+
buffered += chunk.toString("utf8");
|
|
69
|
+
if (buffered.length > MEMORY_DAEMON_MAX_LINE_BYTES) {
|
|
70
|
+
finish(false, "daemon_answer_too_large");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const newline = buffered.indexOf("\n");
|
|
74
|
+
if (newline === -1)
|
|
75
|
+
return;
|
|
76
|
+
let parsed;
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(buffered.slice(0, newline));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
finish(false, "daemon_answer_unparseable");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const record = (parsed ?? {});
|
|
85
|
+
if (record["ok"] !== true) {
|
|
86
|
+
finish(false, typeof record["reason"] === "string" ? record["reason"] : "daemon_refused");
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
finish(true, "answering", {
|
|
90
|
+
pid: typeof record["pid"] === "number" ? record["pid"] : null,
|
|
91
|
+
startedAt: typeof record["startedAt"] === "string" ? record["startedAt"] : null,
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
socket.on("close", () => finish(false, "daemon_closed_without_answer"));
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/** One sentence a person reads, on either branch. Never a path. */
|
|
98
|
+
export function memoryDaemonLine(reading) {
|
|
99
|
+
if (reading.answering) {
|
|
100
|
+
return (`the memory daemon is answering on this machine's ${reading.kind === "pipe" ? "named pipe" : "local socket"}` +
|
|
101
|
+
`${reading.pid === null ? "" : ` (pid ${reading.pid})`}` +
|
|
102
|
+
` — prompt recalls skip a process start, a fresh connection and the token check`);
|
|
103
|
+
}
|
|
104
|
+
if (reading.reason === "socket_path_too_long") {
|
|
105
|
+
return ("the memory daemon cannot run here: this machine's home directory is deep enough that its socket path" +
|
|
106
|
+
" exceeds the operating system's limit. Recall still works — every prompt takes the direct path.");
|
|
107
|
+
}
|
|
108
|
+
return (`no memory daemon is answering (${reading.reason}); every prompt recall takes the direct path,` +
|
|
109
|
+
" which is how it worked before the daemon existed. The next prompt hook starts one.");
|
|
110
|
+
}
|
|
111
|
+
function safeUsername() {
|
|
112
|
+
try {
|
|
113
|
+
return os.userInfo().username;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -47,6 +47,12 @@ export function readMemoryHookCounts(options) {
|
|
|
47
47
|
hook_timeouts_24h: window.timeouts,
|
|
48
48
|
hook_printed_24h: window.printed,
|
|
49
49
|
hook_failed_24h: window.failed,
|
|
50
|
+
...(window.viaMeasured
|
|
51
|
+
? {
|
|
52
|
+
hook_via_daemon_24h: window.viaDaemon,
|
|
53
|
+
hook_via_direct_24h: window.viaDirect,
|
|
54
|
+
}
|
|
55
|
+
: {}),
|
|
50
56
|
hook_skipped_trivial_24h: window.skippedTrivial,
|
|
51
57
|
},
|
|
52
58
|
reason: "ok",
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Sibling of `memory-install.ts`, named in its header.
|
|
5
5
|
*/
|
|
6
6
|
import { memoryInstallGaps } from "@bli-cockpit/telemetry-core";
|
|
7
|
+
import { memoryDaemonLine } from "./memory-daemon-probe.js";
|
|
7
8
|
import { memoryReceiptLine } from "./memory-install-receipt.js";
|
|
8
9
|
import { DEFAULT_DASHBOARD_URL, getCollectorRuntimePaths, readLocalCollectorConfig, } from "../local-state.js";
|
|
9
10
|
export async function resolveDashboardUrl(command, deps) {
|
|
@@ -94,6 +95,10 @@ export function memoryOutcomeLines(outcome) {
|
|
|
94
95
|
? "BLI Memory was not registered and nothing was written: the bli-memory-mcp server is not on this machine yet."
|
|
95
96
|
: `BLI Memory is not fully registered: ${outcome.reason}.`;
|
|
96
97
|
const lines = [headline, ` ${memoryReceiptLine(outcome.receipt)}`];
|
|
98
|
+
// BLI-3884. Only `status` asks; an absent reading is not printed as "no
|
|
99
|
+
// daemon", because nothing looked.
|
|
100
|
+
if (outcome.daemon)
|
|
101
|
+
lines.push(` ${memoryDaemonLine(outcome.daemon)}`);
|
|
97
102
|
// The trust step is the one thing a person has to do by hand, so it is said
|
|
98
103
|
// here in words rather than left as a status word they have to look up.
|
|
99
104
|
if (outcome.receipt.codex.hooks === "needs_trust") {
|
|
@@ -48,6 +48,7 @@ import { defaultMemoryFileIo, } from "./memory-install-files.js";
|
|
|
48
48
|
import { installClaudeMemoryIntegration, inspectClaudeMemoryIntegration, } from "./memory-install-claude.js";
|
|
49
49
|
import { installCodexMemoryIntegration, inspectCodexMemoryIntegration, } from "./memory-install-codex.js";
|
|
50
50
|
import { resolveMemoryConfig } from "./memory-install-config.js";
|
|
51
|
+
import { probeMemoryDaemon } from "./memory-daemon-probe.js";
|
|
51
52
|
import { expectedCodexTrustRows, readCodexHookTrustFromDisk, } from "./memory-install-codex-hooks.js";
|
|
52
53
|
import { buildMemoryInstallReceipt, writeMemoryReceiptFile, } from "./memory-install-receipt.js";
|
|
53
54
|
import { getCollectorRuntimePaths } from "../local-state.js";
|
|
@@ -208,6 +209,10 @@ export async function inspectMemoryIntegration(command, io, deps = {}) {
|
|
|
208
209
|
})));
|
|
209
210
|
const outcome = {
|
|
210
211
|
action: "status",
|
|
212
|
+
// The one live question on this surface: is the daemon answering? It is
|
|
213
|
+
// bounded, it opens nothing but a local socket, and a machine without one
|
|
214
|
+
// is reported as exactly that rather than as a problem.
|
|
215
|
+
daemon: await probeMemoryDaemon({ ...(homeDir ? { homeDir } : {}) }),
|
|
211
216
|
...aggregate(targets),
|
|
212
217
|
config_source: resolved.source,
|
|
213
218
|
bin_found: resolved.config !== null,
|
|
@@ -103,6 +103,10 @@ export function renderMemoryHooks(hooks, dim) {
|
|
|
103
103
|
lines.push((device.timeouts ?? 0) > 0 ? line : dim(line));
|
|
104
104
|
if (device.performanceLine)
|
|
105
105
|
lines.push(` ${device.performanceLine}`);
|
|
106
|
+
// BLI-3884. Written on the server (`lib/ops/memory-hook-misses.ts`) and
|
|
107
|
+
// printed verbatim, like every other sentence in this section.
|
|
108
|
+
if (device.viaLine)
|
|
109
|
+
lines.push(dim(` ${device.viaLine}`));
|
|
106
110
|
}
|
|
107
111
|
return lines;
|
|
108
112
|
}
|
package/dist/commands/ops.js
CHANGED
|
@@ -148,6 +148,10 @@ async function runOpsStatus(command, io, tower) {
|
|
|
148
148
|
// `runs - skipped_trivial` is how many turns actually asked the door.
|
|
149
149
|
memory_hook_skipped_trivial_24h: memory?.hooks?.totals?.skippedTrivial ?? null,
|
|
150
150
|
memory_hook_devices_reporting: memory?.hooks?.reporting ?? null,
|
|
151
|
+
// BLI-3884: which route answered them, fleet-wide. Null while no machine
|
|
152
|
+
// names one — a rollout fact, not a count of zero.
|
|
153
|
+
memory_hook_via_daemon_24h: memory?.hooks?.via?.daemon ?? null,
|
|
154
|
+
memory_hook_via_direct_24h: memory?.hooks?.via?.direct ?? null,
|
|
151
155
|
})}`);
|
|
152
156
|
return unhealthy.length > 0 ? 1 : 0;
|
|
153
157
|
}
|
|
@@ -15,7 +15,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
18
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
18
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.85");
|
|
19
19
|
return 0;
|
|
20
20
|
}
|
|
21
21
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bli-cockpit/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.85",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-verb-help.mjs && node ../../scripts/assert-public-cli-runtime-files.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@bli-cockpit/memory-mcp": "0.1.
|
|
31
|
-
"@bli-cockpit/mcp": "0.1.
|
|
32
|
-
"@bli-cockpit/telemetry-core": "0.1.
|
|
30
|
+
"@bli-cockpit/memory-mcp": "0.1.17",
|
|
31
|
+
"@bli-cockpit/mcp": "0.1.19",
|
|
32
|
+
"@bli-cockpit/telemetry-core": "0.1.35"
|
|
33
33
|
}
|
|
34
34
|
}
|