wicked-brain 0.14.2 → 0.14.3
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
|
@@ -13,6 +13,12 @@ import { join } from "node:path";
|
|
|
13
13
|
import { getBusDb, isBusAvailable, emitEvent } from "./bus.mjs";
|
|
14
14
|
import { promoteFact } from "./memory-promoter.mjs";
|
|
15
15
|
|
|
16
|
+
// Subscriber identity on the bus. Used both to register the subscription and to
|
|
17
|
+
// locate its cursor for the TTL self-heal — keep them in one place so the two
|
|
18
|
+
// can't drift.
|
|
19
|
+
const PLUGIN = "wicked-brain";
|
|
20
|
+
const FACT_FILTER = "wicked.fact.extracted";
|
|
21
|
+
|
|
16
22
|
/**
|
|
17
23
|
* Start the auto-memorize subscriber.
|
|
18
24
|
* Returns the subscription handle (with .stop()) or null if the bus is unavailable.
|
|
@@ -38,10 +44,22 @@ export async function startMemorySubscriber({ brainPath, brainId, db }) {
|
|
|
38
44
|
|
|
39
45
|
const memoryDir = join(brainPath, "memory");
|
|
40
46
|
|
|
47
|
+
// Self-heal a cursor stranded behind the bus TTL window (e.g. after a long
|
|
48
|
+
// server outage). The subscriber RESUMES its existing cursor, so cursor_init
|
|
49
|
+
// "latest" does not recover a stale one — poll() would throw WB-003 every
|
|
50
|
+
// cycle and auto-memorize would stall until manually reset. Advance it to the
|
|
51
|
+
// latest event before subscribing.
|
|
52
|
+
const healed = fastForwardStaleCursor(busDb, PLUGIN, FACT_FILTER);
|
|
53
|
+
if (healed) {
|
|
54
|
+
console.error(
|
|
55
|
+
`[memory-subscriber] cursor was behind the TTL window; repositioned ${healed.from} -> ${healed.to} to replay survivors`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
41
59
|
const sub = subscribe({
|
|
42
60
|
db: busDb,
|
|
43
|
-
plugin:
|
|
44
|
-
filter:
|
|
61
|
+
plugin: PLUGIN,
|
|
62
|
+
filter: FACT_FILTER,
|
|
45
63
|
cursor_init: "latest",
|
|
46
64
|
pollIntervalMs: 5000,
|
|
47
65
|
maxRetries: 3,
|
|
@@ -84,6 +102,63 @@ export async function startMemorySubscriber({ brainPath, brainId, db }) {
|
|
|
84
102
|
return sub;
|
|
85
103
|
}
|
|
86
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Fast-forward a subscriber cursor that has fallen behind the bus TTL window.
|
|
107
|
+
*
|
|
108
|
+
* After a long server outage the durable cursor can sit below the oldest
|
|
109
|
+
* surviving event; wicked-bus poll() then throws WB-003 ("cursor behind the TTL
|
|
110
|
+
* window") every cycle. The subscriber resumes its existing cursor (cursor_init
|
|
111
|
+
* only applies on first registration), so it never recovers on its own. This
|
|
112
|
+
* mirrors poll()'s WB-003 check and, when behind, repositions the cursor to just
|
|
113
|
+
* before the oldest surviving event so the subscriber still replays everything
|
|
114
|
+
* left in the bus (at-least-once) instead of discarding the survivors.
|
|
115
|
+
*
|
|
116
|
+
* No-op when the cursor is current, when there are no events, or when no cursor
|
|
117
|
+
* exists yet (a fresh subscriber initializes at "latest" anyway). Never throws —
|
|
118
|
+
* a self-heal failure must not block server startup.
|
|
119
|
+
*
|
|
120
|
+
* @param {import('better-sqlite3').Database} busDb
|
|
121
|
+
* @param {string} plugin
|
|
122
|
+
* @param {string} filter event_type_filter the subscriber registered with
|
|
123
|
+
* @returns {{from:number,to:number}|null} the adjustment made, or null for no-op
|
|
124
|
+
*/
|
|
125
|
+
export function fastForwardStaleCursor(busDb, plugin, filter) {
|
|
126
|
+
try {
|
|
127
|
+
const bounds = busDb
|
|
128
|
+
.prepare("SELECT MIN(event_id) AS min_id FROM events")
|
|
129
|
+
.get();
|
|
130
|
+
if (!bounds || bounds.min_id == null) return null; // no events to be behind of
|
|
131
|
+
|
|
132
|
+
const row = busDb
|
|
133
|
+
.prepare(
|
|
134
|
+
`SELECT c.cursor_id AS cursor_id, c.last_event_id AS last_event_id
|
|
135
|
+
FROM subscriptions s
|
|
136
|
+
INNER JOIN cursors c ON c.subscription_id = s.subscription_id
|
|
137
|
+
WHERE s.plugin = ? AND s.role = 'subscriber'
|
|
138
|
+
AND s.event_type_filter = ?
|
|
139
|
+
AND s.deregistered_at IS NULL AND c.deregistered_at IS NULL
|
|
140
|
+
ORDER BY s.registered_at DESC
|
|
141
|
+
LIMIT 1`,
|
|
142
|
+
)
|
|
143
|
+
.get(plugin, filter);
|
|
144
|
+
if (!row) return null; // no existing cursor — fresh subscribe inits at "latest"
|
|
145
|
+
|
|
146
|
+
// Mirror wicked-bus poll(): WB-003 fires when last_event_id < oldest - 1.
|
|
147
|
+
// Reposition to oldest-1 (not latest) so the subscriber replays every event
|
|
148
|
+
// that survived the sweep instead of discarding the backlog.
|
|
149
|
+
const target = bounds.min_id - 1;
|
|
150
|
+
if (row.last_event_id < target) {
|
|
151
|
+
busDb
|
|
152
|
+
.prepare("UPDATE cursors SET last_event_id = ? WHERE cursor_id = ?")
|
|
153
|
+
.run(target, row.cursor_id);
|
|
154
|
+
return { from: row.last_event_id, to: target };
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
} catch {
|
|
158
|
+
return null; // never block startup on the self-heal
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
87
162
|
/**
|
|
88
163
|
* Render a memory descriptor as a markdown file with YAML-ish frontmatter.
|
|
89
164
|
* Minimal serializer — no YAML lib. Matches the format used by wicked-brain:memory.
|