@bli-cockpit/memory-mcp 0.1.5 → 0.1.6
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/README.md +22 -8
- package/dist/hooks/contract.d.ts +8 -0
- package/dist/hooks/contract.js +19 -4
- package/dist/hooks/log-line.d.ts +7 -5
- package/dist/hooks/log-line.js +20 -5
- package/dist/hooks/run.d.ts +6 -4
- package/dist/hooks/run.js +7 -5
- package/dist/hooks/stop.d.ts +7 -0
- package/dist/hooks/stop.js +26 -0
- package/dist/server.js +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -81,15 +81,18 @@ Each reads Claude Code's JSON payload on stdin and:
|
|
|
81
81
|
|---|---|---|---|
|
|
82
82
|
| `session-start` | `cwd` | `POST /api/memory/v4/profile` | `<bli-memory-context>` with User Profile + Recent Context, `◪` per line |
|
|
83
83
|
| `prompt` | `prompt` | `POST /api/memory/search` (top 5, ≥ 0.55 similarity) | `<bli-memory-recall>`, `◪` per line |
|
|
84
|
-
| `stop` | `transcript_path` | `POST /api/memory/save` in `extract` mode | nothing, ever |
|
|
84
|
+
| `stop` | `transcript_path` | `POST /api/memory/save` in `extract` mode — the door answers **202 queued** and distils after the response (BLI-3730) | nothing, ever |
|
|
85
85
|
|
|
86
86
|
Four rules they all keep, because a hook runs on every turn of every session on
|
|
87
87
|
every machine:
|
|
88
88
|
|
|
89
89
|
- **Never block.** Each has a deadline below the timeout the installer
|
|
90
|
-
registers — 8 s /
|
|
91
|
-
on the stdin read. On any deadline it prints nothing and gives the
|
|
92
|
-
process back.
|
|
90
|
+
registers — 8 s / **2 s** / 15 s against 30 / 5 / 30 — including a hard
|
|
91
|
+
deadline on the stdin read. On any deadline it prints nothing and gives the
|
|
92
|
+
host its process back. The prompt hook's budget is sized to the PERSON
|
|
93
|
+
waiting with a prompt already typed, not to the host's 5 s ceiling
|
|
94
|
+
(BLI-3730): nobody notices a recall that gives up under its timeout,
|
|
95
|
+
everybody notices a turn that stalls before the model starts.
|
|
93
96
|
- **Always exit 0.** Claude Code reads exit 2 as "block this prompt" and any
|
|
94
97
|
other non-zero as an error it shows the person. An unpaired machine, an
|
|
95
98
|
outage, a malformed payload: all quiet in the transcript.
|
|
@@ -98,9 +101,9 @@ every machine:
|
|
|
98
101
|
the same to it. Zero hits is data and prints nothing.
|
|
99
102
|
- **One stderr receipt per run**, on every branch, one of three shapes
|
|
100
103
|
(BLI-3664, `hooks/log-line.ts`):
|
|
101
|
-
`[bli-memory] prompt ok {"hits":0,"elapsed_ms":140,"deadline_ms":
|
|
104
|
+
`[bli-memory] prompt ok {"hits":0,"elapsed_ms":140,"deadline_ms":2000}`,
|
|
102
105
|
`[bli-memory] prompt skipped {"reason":"no_prompt"}`, or
|
|
103
|
-
`[bli-memory] prompt failed {"reason":"dashboard_unreachable","elapsed_ms":
|
|
106
|
+
`[bli-memory] prompt failed {"reason":"dashboard_unreachable","elapsed_ms":1502,"deadline_ms":2000}`.
|
|
104
107
|
`reason` on `skipped`/`failed` is always one of a closed set — the four
|
|
105
108
|
network reasons `timeout`, `http_<status>`, `dashboard_unreachable`,
|
|
106
109
|
`bad_response` (Tower answered with a body that was not readable JSON);
|
|
@@ -112,10 +115,12 @@ every machine:
|
|
|
112
115
|
`redaction_failed`) — never a stack trace, never Tower's own error text,
|
|
113
116
|
never a prompt, a memory, a path or a token. A reason `toLogReason` has not
|
|
114
117
|
been taught becomes `unknown_internal:<name>`, never a silent
|
|
115
|
-
`bad_response`. A `failed`
|
|
118
|
+
`bad_response`. A `failed` **stop** also gets a one-line `systemMessage` (the
|
|
116
119
|
same JSON-on-stdout shape the Supermemory plugin's own banner uses),
|
|
117
120
|
because Claude Code does not surface a hook's stderr anywhere a person
|
|
118
|
-
reads it.
|
|
121
|
+
reads it and a failed save means the turn was not kept. A failed RECALL is
|
|
122
|
+
silent (BLI-3730): it costs the person nothing and a banner on every prompt
|
|
123
|
+
during an outage is noise they cannot act on.
|
|
119
124
|
|
|
120
125
|
`hook stop` sends the last user/assistant exchange (at most 12 KB, tail-read so
|
|
121
126
|
a 40 MB transcript costs nothing) through the collector's own redactor —
|
|
@@ -124,6 +129,15 @@ that masks every uploaded transcript — and lets the librarian's
|
|
|
124
129
|
extract/reconcile decide what is worth keeping. A re-entrant Stop
|
|
125
130
|
(`stop_hook_active: true`) saves nothing.
|
|
126
131
|
|
|
132
|
+
Since BLI-3730 the door **queues** that work: it writes one durable
|
|
133
|
+
`memory_extract_jobs` row, answers 202, and distils after the response, so the
|
|
134
|
+
hook reports `ok {"chars":…,"masked":…,"queued":true}` and never claims a save
|
|
135
|
+
it cannot see. `saved` is absent on that branch rather than 0 — "accepted,
|
|
136
|
+
pending" and "nothing was worth keeping" must never read as the same line. The
|
|
137
|
+
outcome lands on the job row and in Tower's own
|
|
138
|
+
`[bli-memory] extract job done {added, updated, unchanged, conflicted,
|
|
139
|
+
extract_ms, reconcile_ms}`.
|
|
140
|
+
|
|
127
141
|
## Auth
|
|
128
142
|
|
|
129
143
|
None to set up. The server reads the collector's existing paired session at
|
package/dist/hooks/contract.d.ts
CHANGED
|
@@ -101,6 +101,14 @@ export interface HookOutcome {
|
|
|
101
101
|
* HTTP 200.
|
|
102
102
|
*/
|
|
103
103
|
conflicts?: number;
|
|
104
|
+
/**
|
|
105
|
+
* BLI-3730: the door accepted the turn and will distil it after the
|
|
106
|
+
* response (HTTP 202). The hook cannot say what was saved — nothing has been
|
|
107
|
+
* yet — so it says the true thing instead of guessing, and `saved` stays
|
|
108
|
+
* absent rather than being reported as 0, which would read as "nothing was
|
|
109
|
+
* worth keeping".
|
|
110
|
+
*/
|
|
111
|
+
queued?: boolean;
|
|
104
112
|
}
|
|
105
113
|
/** The container this hook's memories live in, and how it was derived. */
|
|
106
114
|
export interface HookContainer {
|
package/dist/hooks/contract.js
CHANGED
|
@@ -40,10 +40,25 @@ export const HOOK_EVENTS = ["session-start", "prompt", "stop"];
|
|
|
40
40
|
*/
|
|
41
41
|
export const HOOK_BUDGETS = {
|
|
42
42
|
"session-start": { totalMs: 8_000, stdinMs: 2_000, requestMs: 6_000, gitMs: 2_500 },
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
/**
|
|
44
|
+
* A person is waiting on this one with their prompt already typed, so the
|
|
45
|
+
* whole run is capped at TWO SECONDS and the door call at 1,500 ms
|
|
46
|
+
* (BLI-3730). The old 3,000/2,200 pair was chosen against the installer's
|
|
47
|
+
* 5 s host timeout, which is the wrong number to size against: nobody
|
|
48
|
+
* notices a hook that gives up under its host's ceiling, everybody notices
|
|
49
|
+
* a turn that stalls for three seconds before the model starts. A recall
|
|
50
|
+
* that misses this window prints NOTHING and logs `timeout` — the shelf is
|
|
51
|
+
* not worth the wait, and the person never finds out, by design.
|
|
52
|
+
*/
|
|
53
|
+
prompt: { totalMs: 2_000, stdinMs: 600, requestMs: 1_500, gitMs: 500 },
|
|
54
|
+
/**
|
|
55
|
+
* `extract` mode is queued server-side now (BLI-3730): the door writes a
|
|
56
|
+
* durable job, answers 202 and does the model work after the response, so
|
|
57
|
+
* this hook is waiting on one INSERT rather than on two model calls and
|
|
58
|
+
* twelve neighbour searches. The generous budget stays anyway — it prints
|
|
59
|
+
* nothing either way, and the one thing worse than a slow Stop hook is a
|
|
60
|
+
* Stop hook that abandons a save the door was about to accept.
|
|
61
|
+
*/
|
|
47
62
|
stop: { totalMs: 15_000, stdinMs: 3_000, requestMs: 13_000, gitMs: 2_500 },
|
|
48
63
|
};
|
|
49
64
|
/** The wrappers Claude Code injects verbatim. Kept in one place, tested. */
|
package/dist/hooks/log-line.d.ts
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* 1. **stderr, on every branch, one line, one shape.** `buildLogLine` below.
|
|
16
16
|
* This is what an operator tailing a launchd-style log — or a future
|
|
17
17
|
* collector upload of hook logs — can grep.
|
|
18
|
-
* 2. **`systemMessage`, on
|
|
18
|
+
* 2. **`systemMessage`, on a failed STOP only** (narrowed in BLI-3730 — see
|
|
19
|
+
* `buildSystemMessage`; a failed recall is silent). Claude Code does not surface a
|
|
19
20
|
* hook's stderr as a visible line; the Supermemory plugin's own banner
|
|
20
21
|
* (`~/.claude/plugins/.../recall-directive.js`) is a JSON object on
|
|
21
22
|
* STDOUT carrying a `systemMessage` field (`hooks/stdin.js:writeOutput`,
|
|
@@ -81,13 +82,14 @@ export declare function toLogReason(reason: string): HookLogReason;
|
|
|
81
82
|
*/
|
|
82
83
|
export declare function buildLogLine(event: HookEvent, outcome: HookOutcome, elapsedMs: number, deadlineMs: number): string;
|
|
83
84
|
/**
|
|
84
|
-
* One short sentence for a `failed` outcome only, in the JSON envelope
|
|
85
|
-
* host actually renders (`{"systemMessage": "…"}` on stdout — see the
|
|
85
|
+
* One short sentence for a `failed` STOP outcome only, in the JSON envelope
|
|
86
|
+
* the host actually renders (`{"systemMessage": "…"}` on stdout — see the
|
|
86
87
|
* Supermemory mechanism cited in this file's header). `null` on every other
|
|
87
88
|
* branch: `ok`/`empty` already say their piece through the injected block,
|
|
88
|
-
*
|
|
89
|
+
* `skipped` is a deliberate no-op nobody needs interrupted for, and a failed
|
|
90
|
+
* RECALL is not a person's problem (BLI-3730).
|
|
89
91
|
*/
|
|
90
|
-
export declare function buildSystemMessage(outcome: HookOutcome): string | null;
|
|
92
|
+
export declare function buildSystemMessage(event: HookEvent, outcome: HookOutcome): string | null;
|
|
91
93
|
/** Renders one JSON block to stdout — the only shape a hook may emit there
|
|
92
94
|
* besides plain injected context. Used by `run.ts` on `failed` only, so the
|
|
93
95
|
* "one block or nothing" stdout contract for every other branch is untouched. */
|
package/dist/hooks/log-line.js
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
* 1. **stderr, on every branch, one line, one shape.** `buildLogLine` below.
|
|
16
16
|
* This is what an operator tailing a launchd-style log — or a future
|
|
17
17
|
* collector upload of hook logs — can grep.
|
|
18
|
-
* 2. **`systemMessage`, on
|
|
18
|
+
* 2. **`systemMessage`, on a failed STOP only** (narrowed in BLI-3730 — see
|
|
19
|
+
* `buildSystemMessage`; a failed recall is silent). Claude Code does not surface a
|
|
19
20
|
* hook's stderr as a visible line; the Supermemory plugin's own banner
|
|
20
21
|
* (`~/.claude/plugins/.../recall-directive.js`) is a JSON object on
|
|
21
22
|
* STDOUT carrying a `systemMessage` field (`hooks/stdin.js:writeOutput`,
|
|
@@ -138,21 +139,35 @@ export function buildLogLine(event, outcome, elapsedMs, deadlineMs) {
|
|
|
138
139
|
// nothing AND conflicted with nothing is a materially different
|
|
139
140
|
// fact from one that conflicted on every candidate.
|
|
140
141
|
...(outcome.conflicts === undefined ? {} : { conflicts: outcome.conflicts }),
|
|
142
|
+
// BLI-3730: the door took the turn and will distil it after the
|
|
143
|
+
// response. Printed so `ok {queued:true}` and `ok {saved:0}` —
|
|
144
|
+
// "accepted, pending" and "nothing worth keeping" — can never be
|
|
145
|
+
// read as the same line.
|
|
146
|
+
...(outcome.queued === undefined ? {} : { queued: outcome.queued }),
|
|
141
147
|
elapsed_ms: elapsedMs,
|
|
142
148
|
deadline_ms: deadlineMs,
|
|
143
149
|
};
|
|
144
150
|
return `${HOOK_LOG_TAG} ${event} ${word} ${JSON.stringify(fields)}\n`;
|
|
145
151
|
}
|
|
146
152
|
/**
|
|
147
|
-
* One short sentence for a `failed` outcome only, in the JSON envelope
|
|
148
|
-
* host actually renders (`{"systemMessage": "…"}` on stdout — see the
|
|
153
|
+
* One short sentence for a `failed` STOP outcome only, in the JSON envelope
|
|
154
|
+
* the host actually renders (`{"systemMessage": "…"}` on stdout — see the
|
|
149
155
|
* Supermemory mechanism cited in this file's header). `null` on every other
|
|
150
156
|
* branch: `ok`/`empty` already say their piece through the injected block,
|
|
151
|
-
*
|
|
157
|
+
* `skipped` is a deliberate no-op nobody needs interrupted for, and a failed
|
|
158
|
+
* RECALL is not a person's problem (BLI-3730).
|
|
152
159
|
*/
|
|
153
|
-
export function buildSystemMessage(outcome) {
|
|
160
|
+
export function buildSystemMessage(event, outcome) {
|
|
154
161
|
if (outcome.status !== "failed")
|
|
155
162
|
return null;
|
|
163
|
+
// BLI-3730: RECALL is silent, always. A `UserPromptSubmit` or `SessionStart`
|
|
164
|
+
// hook that could not reach Tower has cost the person nothing — the turn
|
|
165
|
+
// runs, the model simply has one fewer source — and a banner on every
|
|
166
|
+
// prompt during an outage is noise a person cannot act on and cannot turn
|
|
167
|
+
// off. The Stop hook is different in kind: it is the only hook that WRITES,
|
|
168
|
+
// and a failure there means the turn was not kept. That is worth one line.
|
|
169
|
+
if (event !== "stop")
|
|
170
|
+
return null;
|
|
156
171
|
return `BLI Memory: ${failureSentence(toLogReason(outcome.reason))}`;
|
|
157
172
|
}
|
|
158
173
|
function failureSentence(reason) {
|
package/dist/hooks/run.d.ts
CHANGED
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
* memory recall may never do either.
|
|
16
16
|
* 3. **stdout discipline.** Nothing is written until the hook has finished and
|
|
17
17
|
* produced a whole block. A partial injection is worse than none — with
|
|
18
|
-
* ONE exception, added in BLI-3664: a `failed`
|
|
19
|
-
* `{"systemMessage": "…"}` JSON block,
|
|
20
|
-
* Claude Code actually renders to a person
|
|
21
|
-
*
|
|
18
|
+
* ONE exception, added in BLI-3664 and narrowed in BLI-3730: a `failed`
|
|
19
|
+
* STOP outcome also gets a single `{"systemMessage": "…"}` JSON block,
|
|
20
|
+
* because that is the one shape Claude Code actually renders to a person
|
|
21
|
+
* (see `log-line.ts`'s header) and a lost SAVE is worth a line. A failed
|
|
22
|
+
* recall is silent — it costs the person nothing and they cannot act on
|
|
23
|
+
* it. Every other status's stdout is exactly as before.
|
|
22
24
|
* 4. **One stderr receipt per run, on every branch.** `[bli-memory] <event>
|
|
23
25
|
* ok|skipped|failed {…}` (`log-line.ts:buildLogLine`) — metadata only,
|
|
24
26
|
* never a prompt, a memory, a path or a token, and a `reason` drawn from a
|
package/dist/hooks/run.js
CHANGED
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
* memory recall may never do either.
|
|
16
16
|
* 3. **stdout discipline.** Nothing is written until the hook has finished and
|
|
17
17
|
* produced a whole block. A partial injection is worse than none — with
|
|
18
|
-
* ONE exception, added in BLI-3664: a `failed`
|
|
19
|
-
* `{"systemMessage": "…"}` JSON block,
|
|
20
|
-
* Claude Code actually renders to a person
|
|
21
|
-
*
|
|
18
|
+
* ONE exception, added in BLI-3664 and narrowed in BLI-3730: a `failed`
|
|
19
|
+
* STOP outcome also gets a single `{"systemMessage": "…"}` JSON block,
|
|
20
|
+
* because that is the one shape Claude Code actually renders to a person
|
|
21
|
+
* (see `log-line.ts`'s header) and a lost SAVE is worth a line. A failed
|
|
22
|
+
* recall is silent — it costs the person nothing and they cannot act on
|
|
23
|
+
* it. Every other status's stdout is exactly as before.
|
|
22
24
|
* 4. **One stderr receipt per run, on every branch.** `[bli-memory] <event>
|
|
23
25
|
* ok|skipped|failed {…}` (`log-line.ts:buildLogLine`) — metadata only,
|
|
24
26
|
* never a prompt, a memory, a path or a token, and a `reason` drawn from a
|
|
@@ -63,7 +65,7 @@ export async function runHook(event, deps = {}) {
|
|
|
63
65
|
// prints nothing to the model (no additionalContext), but DOES get a
|
|
64
66
|
// `systemMessage` block, which is a status banner, not a memory. Every
|
|
65
67
|
// other status's stdout is unchanged from before BLI-3664.
|
|
66
|
-
const systemMessage = buildSystemMessage(outcome);
|
|
68
|
+
const systemMessage = buildSystemMessage(event, outcome);
|
|
67
69
|
if (systemMessage) {
|
|
68
70
|
stdout.write(`${renderSystemMessageBlock(systemMessage)}\n`);
|
|
69
71
|
}
|
package/dist/hooks/stop.d.ts
CHANGED
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
* nothing saved, `reentrant`.
|
|
17
17
|
* - Nothing is printed on stdout, ever. Stop's stdout is shown to the person
|
|
18
18
|
* in transcript mode, and "I saved a memory" on every turn is noise.
|
|
19
|
+
*
|
|
20
|
+
* BLI-3730: the door answers **202** for `mode: "extract"` — it writes a
|
|
21
|
+
* durable job and distils it after the response — so the normal successful
|
|
22
|
+
* outcome here is `queued`, with no decisions to count. The synchronous
|
|
23
|
+
* branch below is kept for a `wait: true` caller and for a Tower old enough
|
|
24
|
+
* to still answer 200 with decisions; it is not dead code, it is the older
|
|
25
|
+
* half of a wire contract that has to keep working during a rollout.
|
|
19
26
|
*/
|
|
20
27
|
import { type HookOutcome } from "./contract.js";
|
|
21
28
|
import type { HookRunContext } from "./run-context.js";
|
package/dist/hooks/stop.js
CHANGED
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
* nothing saved, `reentrant`.
|
|
17
17
|
* - Nothing is printed on stdout, ever. Stop's stdout is shown to the person
|
|
18
18
|
* in transcript mode, and "I saved a memory" on every turn is noise.
|
|
19
|
+
*
|
|
20
|
+
* BLI-3730: the door answers **202** for `mode: "extract"` — it writes a
|
|
21
|
+
* durable job and distils it after the response — so the normal successful
|
|
22
|
+
* outcome here is `queued`, with no decisions to count. The synchronous
|
|
23
|
+
* branch below is kept for a `wait: true` caller and for a Tower old enough
|
|
24
|
+
* to still answer 200 with decisions; it is not dead code, it is the older
|
|
25
|
+
* half of a wire contract that has to keep working during a rollout.
|
|
19
26
|
*/
|
|
20
27
|
import { createHash } from "node:crypto";
|
|
21
28
|
import { doorReason, postMemoryDoor } from "../door.js";
|
|
@@ -75,6 +82,25 @@ export async function runStopHook(context, options = {}) {
|
|
|
75
82
|
if (!response.ok) {
|
|
76
83
|
return { status: "failed", reason: doorReason(response), stdout: "", saved: 0 };
|
|
77
84
|
}
|
|
85
|
+
// BLI-3730: the door queued it. Extraction is one model call (two on the
|
|
86
|
+
// BLI-3713 retry) plus one embed and one neighbour search per fact, and on
|
|
87
|
+
// 2026-09-05 twelve facts outran this hook's whole 15 s deadline — every
|
|
88
|
+
// turn ended with "BLI Memory: Tower did not answer in time" while the work
|
|
89
|
+
// was still in flight. The door writes a durable job row, answers 202 and
|
|
90
|
+
// does the work after the response, so this hook is now waiting on one
|
|
91
|
+
// INSERT. Nothing is claimed saved, because nothing is yet: `queued` is the
|
|
92
|
+
// true thing to report, and `saved` stays absent rather than 0, which would
|
|
93
|
+
// read as "nothing was worth keeping".
|
|
94
|
+
if (response.status === 202 || response.body["queued"] === true) {
|
|
95
|
+
return {
|
|
96
|
+
status: "ok",
|
|
97
|
+
reason: "queued",
|
|
98
|
+
stdout: "",
|
|
99
|
+
queued: true,
|
|
100
|
+
chars: masked.text.length,
|
|
101
|
+
masked: masked.masked,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
78
104
|
const decisionsRaw = response.body["decisions"];
|
|
79
105
|
const decisions = Array.isArray(decisionsRaw)
|
|
80
106
|
? decisionsRaw.filter((decision) => Boolean(decision) && typeof decision === "object")
|
package/dist/server.js
CHANGED
|
@@ -229,6 +229,12 @@ export function createServer(deps) {
|
|
|
229
229
|
containerTag,
|
|
230
230
|
customId: args.customId,
|
|
231
231
|
mode: args.extract ? "extract" : "verbatim",
|
|
232
|
+
// BLI-3730: `extract` is queued by default (the Stop hook cannot hold
|
|
233
|
+
// a connection through two model calls). An agent that ASKED for
|
|
234
|
+
// distillation is a different caller: it is waiting for the answer and
|
|
235
|
+
// its host allows far longer than a Stop hook does, so this tool takes
|
|
236
|
+
// the synchronous branch and can still report what was written.
|
|
237
|
+
...(args.extract ? { wait: true } : {}),
|
|
232
238
|
sourceRef: { kind: "mcp", device_id: deps.session.deviceId },
|
|
233
239
|
});
|
|
234
240
|
if (!response.ok)
|