wicked-brain 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wicked-brain",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Digital brain as skills for AI coding CLIs — no vector DB, no embeddings, no infrastructure",
|
|
6
6
|
"keywords": [
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"better-sqlite3": "^12.0.0",
|
|
36
|
-
"wicked-bus": "^
|
|
36
|
+
"wicked-bus": "^2.0.0"
|
|
37
37
|
},
|
|
38
38
|
"files": [
|
|
39
39
|
"install.mjs",
|
|
@@ -6,7 +6,13 @@ import { argv, pid, exit } from "node:process";
|
|
|
6
6
|
import { FileWatcher } from "../lib/file-watcher.mjs";
|
|
7
7
|
import { SqliteSearch } from "../lib/sqlite-search.mjs";
|
|
8
8
|
import { LspClient } from "../lib/lsp-client.mjs";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
emitEvent,
|
|
11
|
+
waitForBus,
|
|
12
|
+
listBusDeadLetters,
|
|
13
|
+
replayBusDeadLetter,
|
|
14
|
+
dropBusDeadLetter,
|
|
15
|
+
} from "../lib/bus.mjs";
|
|
10
16
|
import { startMemorySubscriber } from "../lib/memory-subscriber.mjs";
|
|
11
17
|
import { renderViewerHtml } from "../lib/viewer-page.mjs";
|
|
12
18
|
import { walkBrainContent, purgeBrainContent } from "../lib/brain-walker.mjs";
|
|
@@ -201,6 +207,17 @@ const actions = {
|
|
|
201
207
|
emitEvent("wicked.link.confirmed", "brain.link", {
|
|
202
208
|
source_id: p.source_id, target_path: p.target_path, verdict: p.verdict, brain_id: brainId,
|
|
203
209
|
});
|
|
210
|
+
// Surface contradictions on a dedicated event stream so downstream
|
|
211
|
+
// consumers don't have to filter by verdict on the generic confirmed event.
|
|
212
|
+
if (p.verdict === "contradict") {
|
|
213
|
+
emitEvent("wicked.link.contradicted", "brain.link", {
|
|
214
|
+
source_id: p.source_id,
|
|
215
|
+
target_path: p.target_path,
|
|
216
|
+
confidence: result?.confidence ?? null,
|
|
217
|
+
evidence_count: result?.evidence_count ?? null,
|
|
218
|
+
brain_id: brainId,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
204
221
|
return result;
|
|
205
222
|
},
|
|
206
223
|
link_health: () => db.linkHealth(),
|
|
@@ -259,6 +276,21 @@ const actions = {
|
|
|
259
276
|
: { skipped: true },
|
|
260
277
|
};
|
|
261
278
|
},
|
|
279
|
+
// DLQ inspection — read-only listing, scoped to wicked-brain's plugin.
|
|
280
|
+
// Surfaces dead-lettered fact events from the auto-memorize subscriber
|
|
281
|
+
// so operators can decide whether to replay or drop them.
|
|
282
|
+
dlq_list: (p = {}) => ({
|
|
283
|
+
dead_letters: listBusDeadLetters({
|
|
284
|
+
cursor_id: p.cursor_id,
|
|
285
|
+
limit: p.limit,
|
|
286
|
+
}),
|
|
287
|
+
}),
|
|
288
|
+
// Mark one DLQ entry for replay. The managed subscriber drains pending
|
|
289
|
+
// replays before each poll cycle — success here means queued, not delivered.
|
|
290
|
+
// dl_id alone identifies the row (it's the bus's primary key).
|
|
291
|
+
dlq_replay: (p = {}) => replayBusDeadLetter({ dl_id: p.dl_id }),
|
|
292
|
+
// Permanently delete a DLQ row. Use when replay would just dead-letter again.
|
|
293
|
+
dlq_drop: (p = {}) => dropBusDeadLetter({ dl_id: p.dl_id }),
|
|
262
294
|
purge_brain: async (p = {}) => {
|
|
263
295
|
// Destructive. Wipes chunks/, wiki/, and memory/ content and clears the
|
|
264
296
|
// SQLite index. Requires p.confirm === "DELETE" to execute — typed
|
|
@@ -283,6 +315,9 @@ const WRITE_ACTIONS = new Set([
|
|
|
283
315
|
"confirm_link",
|
|
284
316
|
"reonboard",
|
|
285
317
|
"purge_brain",
|
|
318
|
+
// DLQ replay/drop mutate the bus DB; list is read-only.
|
|
319
|
+
"dlq_replay",
|
|
320
|
+
"dlq_drop",
|
|
286
321
|
]);
|
|
287
322
|
|
|
288
323
|
// HTTP server
|
package/server/lib/bus.mjs
CHANGED
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const DOMAIN = "wicked-brain";
|
|
12
|
+
const PLUGIN = "wicked-brain";
|
|
12
13
|
|
|
13
14
|
let busEmit = null;
|
|
15
|
+
let busListDeadLetters = null;
|
|
16
|
+
let busReplayDeadLetter = null;
|
|
17
|
+
let busDropDeadLetter = null;
|
|
14
18
|
let busDb = null;
|
|
15
19
|
let busConfig = null;
|
|
16
20
|
let available = false;
|
|
@@ -25,6 +29,9 @@ async function init() {
|
|
|
25
29
|
const dbPath = bus.resolveDbPath();
|
|
26
30
|
busDb = bus.openDb(dbPath);
|
|
27
31
|
busEmit = bus.emit;
|
|
32
|
+
busListDeadLetters = bus.listDeadLetters;
|
|
33
|
+
busReplayDeadLetter = bus.replayDeadLetter;
|
|
34
|
+
busDropDeadLetter = bus.dropDeadLetter;
|
|
28
35
|
available = true;
|
|
29
36
|
} catch {
|
|
30
37
|
// wicked-bus not installed or not initialized — degrade silently
|
|
@@ -83,3 +90,82 @@ export async function waitForBus() {
|
|
|
83
90
|
await ready;
|
|
84
91
|
return available;
|
|
85
92
|
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* List dead-lettered events scoped to wicked-brain's plugin.
|
|
96
|
+
* Returns [] when the bus is unavailable so callers don't have to branch.
|
|
97
|
+
*
|
|
98
|
+
* Upstream takes camelCase `cursorId`; we keep snake_case at this layer
|
|
99
|
+
* for consistency with the rest of wicked-brain's API and translate here.
|
|
100
|
+
* `limit` is parsed to an integer because params arriving from the HTTP
|
|
101
|
+
* dispatch layer can be strings (and upstream rejects non-integers by
|
|
102
|
+
* silently falling back to its default).
|
|
103
|
+
*
|
|
104
|
+
* @param {object} [opts]
|
|
105
|
+
* @param {string} [opts.cursor_id] filter to one cursor
|
|
106
|
+
* @param {number|string} [opts.limit=100]
|
|
107
|
+
* @returns {Array}
|
|
108
|
+
*/
|
|
109
|
+
export function listBusDeadLetters(opts = {}) {
|
|
110
|
+
if (!available || !busListDeadLetters) return [];
|
|
111
|
+
const limit = parseInt(opts.limit ?? 100, 10) || 100;
|
|
112
|
+
const upstreamOpts = { plugin: PLUGIN, limit };
|
|
113
|
+
if (opts.cursor_id) upstreamOpts.cursorId = opts.cursor_id;
|
|
114
|
+
try {
|
|
115
|
+
return busListDeadLetters(busDb, upstreamOpts);
|
|
116
|
+
} catch {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Mark a dead letter for replay. The managed subscriber drains pending
|
|
123
|
+
* replays before each poll cycle, so a successful return means the request
|
|
124
|
+
* is queued, not that the event has re-delivered yet.
|
|
125
|
+
*
|
|
126
|
+
* Upstream signature is positional `(db, dlId)`. dl_id is globally unique
|
|
127
|
+
* (the bus's own primary key) so plugin/cursor scoping is implicit.
|
|
128
|
+
*
|
|
129
|
+
* @param {object} args
|
|
130
|
+
* @param {string} args.dl_id
|
|
131
|
+
* @returns {{ ok: boolean, error?: string }}
|
|
132
|
+
*/
|
|
133
|
+
export function replayBusDeadLetter({ dl_id } = {}) {
|
|
134
|
+
if (!available || !busReplayDeadLetter) {
|
|
135
|
+
return { ok: false, error: "bus unavailable" };
|
|
136
|
+
}
|
|
137
|
+
if (!dl_id) {
|
|
138
|
+
return { ok: false, error: "dl_id required" };
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
busReplayDeadLetter(busDb, dl_id);
|
|
142
|
+
return { ok: true };
|
|
143
|
+
} catch (err) {
|
|
144
|
+
return { ok: false, error: err.message };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Drop (delete) a dead letter row. Use when an event is no longer relevant
|
|
150
|
+
* — replay would just dead-letter again.
|
|
151
|
+
*
|
|
152
|
+
* Upstream signature is positional `(db, dlId)`. dl_id is globally unique.
|
|
153
|
+
*
|
|
154
|
+
* @param {object} args
|
|
155
|
+
* @param {string} args.dl_id
|
|
156
|
+
* @returns {{ ok: boolean, error?: string }}
|
|
157
|
+
*/
|
|
158
|
+
export function dropBusDeadLetter({ dl_id } = {}) {
|
|
159
|
+
if (!available || !busDropDeadLetter) {
|
|
160
|
+
return { ok: false, error: "bus unavailable" };
|
|
161
|
+
}
|
|
162
|
+
if (!dl_id) {
|
|
163
|
+
return { ok: false, error: "dl_id required" };
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
busDropDeadLetter(busDb, dl_id);
|
|
167
|
+
return { ok: true };
|
|
168
|
+
} catch (err) {
|
|
169
|
+
return { ok: false, error: err.message };
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -43,7 +43,7 @@ export async function startMemorySubscriber({ brainPath, brainId, db }) {
|
|
|
43
43
|
plugin: "wicked-brain",
|
|
44
44
|
filter: "wicked.fact.extracted",
|
|
45
45
|
cursor_init: "latest",
|
|
46
|
-
pollIntervalMs:
|
|
46
|
+
pollIntervalMs: 5000,
|
|
47
47
|
maxRetries: 3,
|
|
48
48
|
backoffMs: [1000, 5000, 30000],
|
|
49
49
|
handler: async (event) => {
|
package/server/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wicked-brain-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SQLite FTS5 search server for wicked-brain digital knowledge bases",
|
|
6
6
|
"keywords": [
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"better-sqlite3": "^12.0.0",
|
|
40
|
-
"wicked-bus": "^
|
|
40
|
+
"wicked-bus": "^2.0.0"
|
|
41
41
|
},
|
|
42
42
|
"engines": {
|
|
43
43
|
"node": ">=18.0.0"
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wicked-brain:dlq
|
|
3
|
+
description: |
|
|
4
|
+
Inspect, replay, or drop dead-lettered events from wicked-brain's bus
|
|
5
|
+
subscriber. The auto-memorize subscriber consumes `wicked.fact.extracted`
|
|
6
|
+
and dead-letters events that exhaust their retry budget. Without this
|
|
7
|
+
skill, those events sit untouched forever — a fixable handler bug
|
|
8
|
+
silently loses every fact.
|
|
9
|
+
|
|
10
|
+
Use when: "show DLQ", "replay dead letters", "what events failed",
|
|
11
|
+
"drop dead letter", "memory subscriber DLQ", "stuck fact events".
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# wicked-brain:dlq
|
|
15
|
+
|
|
16
|
+
List, replay, and drop dead-lettered events held by wicked-bus on behalf of
|
|
17
|
+
the wicked-brain auto-memorize subscriber.
|
|
18
|
+
|
|
19
|
+
## Cross-Platform Notes
|
|
20
|
+
|
|
21
|
+
Uses `npx wicked-brain-call` for all server interaction. Cross-platform on
|
|
22
|
+
macOS, Linux, and Windows.
|
|
23
|
+
|
|
24
|
+
- Paths use forward slashes
|
|
25
|
+
- No Unix-only shell features
|
|
26
|
+
|
|
27
|
+
## Config
|
|
28
|
+
|
|
29
|
+
Brain discovery + server lifecycle are handled by `wicked-brain-call`. The
|
|
30
|
+
server bridges to the wicked-bus DB it opened at startup, so DLQ rows are
|
|
31
|
+
always scoped to `plugin: "wicked-brain"`.
|
|
32
|
+
|
|
33
|
+
If the bus is unavailable (not installed, or the DB is unreachable), `list`
|
|
34
|
+
returns an empty array and `replay` / `drop` return `{ ok: false, error: "bus unavailable" }`.
|
|
35
|
+
|
|
36
|
+
## Parameters
|
|
37
|
+
|
|
38
|
+
- **mode** (required): `list`, `replay`, or `drop`
|
|
39
|
+
- **cursor_id** (list, optional): filter to one subscriber cursor
|
|
40
|
+
- **dl_id** (replay/drop, required): dead-letter row id, returned by `list`. Globally unique — alone identifies the row.
|
|
41
|
+
- **limit** (list, optional, default 100): max rows to return
|
|
42
|
+
|
|
43
|
+
## List mode
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx wicked-brain-call dlq_list --param limit=50
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Optional cursor scoping:
|
|
50
|
+
```bash
|
|
51
|
+
npx wicked-brain-call dlq_list --param cursor_id={cursor_id}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Returns `{ dead_letters: [...] }`. Each row has `dl_id`, `cursor_id`,
|
|
55
|
+
`event_type`, `domain`, `subdomain`, `payload`, `dead_lettered_at`,
|
|
56
|
+
`attempts`, `last_error`.
|
|
57
|
+
|
|
58
|
+
The `payload` field is denormalized at DLQ time — the originating row in
|
|
59
|
+
`events` may have been swept by the 24h `dedup_expires_at`, so it reflects
|
|
60
|
+
the event as it failed, not current state.
|
|
61
|
+
|
|
62
|
+
## Replay mode
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx wicked-brain-call dlq_replay --param dl_id={dl_id}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Marks the row for replay. The managed subscriber drains pending replays
|
|
69
|
+
before each poll cycle (5s by default), so a successful return means
|
|
70
|
+
*queued*, not *delivered*. If the handler still rejects the event, it
|
|
71
|
+
will dead-letter again with an incremented attempt count.
|
|
72
|
+
|
|
73
|
+
Replay is for recovery — not transparent retry. The original
|
|
74
|
+
`idempotency_key` may already have been swept (24h TTL), so a re-emission
|
|
75
|
+
inside the handler will not be deduped against the original event.
|
|
76
|
+
|
|
77
|
+
## Drop mode
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx wicked-brain-call dlq_drop --param dl_id={dl_id}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Permanently deletes the DLQ row. Use when replay would just dead-letter
|
|
84
|
+
again — for example, when the source event was malformed and there's no
|
|
85
|
+
fix path.
|
|
86
|
+
|
|
87
|
+
## Workflow
|
|
88
|
+
|
|
89
|
+
A typical recovery loop:
|
|
90
|
+
|
|
91
|
+
1. `list` to see what's pending and inspect `last_error`.
|
|
92
|
+
2. Fix the handler (`server/lib/memory-promoter.mjs` or `memory-subscriber.mjs`).
|
|
93
|
+
3. Restart the server so the fix takes effect.
|
|
94
|
+
4. `replay` each row.
|
|
95
|
+
5. `list` again to confirm the queue is empty.
|
|
96
|
+
6. `drop` any rows that can't be fixed.
|
|
97
|
+
|
|
98
|
+
## Reporting
|
|
99
|
+
|
|
100
|
+
Always surface `dl_id`, `cursor_id`, `event_type`, `attempts`, and
|
|
101
|
+
`last_error` for each row so the operator has enough context to choose
|
|
102
|
+
between replay and drop.
|