discoclaw 0.5.1 → 0.5.2
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/.context/memory.md +7 -4
- package/dist/discord/actions-memory.js +2 -2
- package/dist/discord/durable-memory.js +41 -8
- package/dist/discord/durable-memory.test.js +106 -7
- package/dist/discord/prompt-common.js +15 -1
- package/dist/discord/reaction-handler.test.js +1 -1
- package/dist/discord/user-turn-to-durable.js +2 -2
- package/package.json +1 -1
package/.context/memory.md
CHANGED
|
@@ -33,7 +33,10 @@ Bot: We've been working through your Express → Fastify migration.
|
|
|
33
33
|
|
|
34
34
|
Structured store of user facts. Each item has a kind (fact, preference, project,
|
|
35
35
|
constraint, person, tool, workflow), deduplication by content hash, and a 200-item
|
|
36
|
-
cap per user.
|
|
36
|
+
cap per user. Items track `hitCount` (incremented each time the item is selected
|
|
37
|
+
for prompt injection) and `lastHitAt` (timestamp of most recent selection).
|
|
38
|
+
Injected into every prompt using a blended score of recency and usage frequency
|
|
39
|
+
rather than raw `updatedAt` alone.
|
|
37
40
|
|
|
38
41
|
**What the user sees:**
|
|
39
42
|
- The bot knows your preferences, projects, and key facts across all conversations.
|
|
@@ -53,7 +56,7 @@ Bot: Given your preference for Rust in systems work, I'd lean that way —
|
|
|
53
56
|
|
|
54
57
|
#### Consolidation
|
|
55
58
|
|
|
56
|
-
When the active item count for a user crosses a threshold (`DISCOCLAW_DURABLE_CONSOLIDATION_THRESHOLD`, default `100`), consolidation can be triggered to prune and merge the list. A single `fast`-tier model call receives all active items and is asked to return a revised list — removing exact duplicates, merging near-duplicates, dropping clearly stale items, and preserving everything that is still plausibly useful. The model must not invent new facts or change the meaning of existing ones.
|
|
59
|
+
When the active item count for a user crosses a threshold (`DISCOCLAW_DURABLE_CONSOLIDATION_THRESHOLD`, default `100`), consolidation can be triggered to prune and merge the list. A single `fast`-tier model call receives all active items and is asked to return a revised list — removing exact duplicates, merging near-duplicates, dropping clearly stale items, and preserving everything that is still plausibly useful. The model must not invent new facts or change the meaning of existing ones. Items with low `hitCount` and stale `lastHitAt` are natural eviction candidates — the blended score surfaces which items the AI actually uses versus those that were added once and never referenced again.
|
|
57
60
|
|
|
58
61
|
The revised list is applied atomically: items absent from the model's output are deprecated via `deprecateItems()`; new or rewritten items are written via `addItem()`. Items present verbatim in the output are left untouched (no unnecessary writes).
|
|
59
62
|
|
|
@@ -183,7 +186,7 @@ no separator). The three memory builders run in `Promise.all` so they add no lat
|
|
|
183
186
|
|
|
184
187
|
| Layer | Default budget | Default state | How it stays within budget |
|
|
185
188
|
|-------|---------------|---------------|---------------------------|
|
|
186
|
-
| Durable memory | 2000 chars | on |
|
|
189
|
+
| Durable memory | 2000 chars | on | Ranks active items by blended score (recency + hit frequency), adds one at a time, stops when next line would exceed budget. Low-scoring items silently excluded. |
|
|
187
190
|
| Rolling summary | 2000 chars | on | The `fast`-tier model is prompted with `"Keep the summary under {maxChars} characters"`. Replaces itself each update rather than growing. |
|
|
188
191
|
| Message history | 3000 chars | on | Fetches up to 10 messages, walks backward from newest. Bot messages truncated to fit; user messages that don't fit cause a hard stop. |
|
|
189
192
|
| Short-term memory | 1000 chars | **on** | Filters by max age (default 6h), sorts newest-first, accumulates lines until budget hit. |
|
|
@@ -202,7 +205,7 @@ might add ~500 chars total. Sections with no data produce zero overhead.
|
|
|
202
205
|
|
|
203
206
|
### Where the budgets are enforced
|
|
204
207
|
|
|
205
|
-
- **Durable**: `selectItemsForInjection()` in `durable-memory.ts:152`
|
|
208
|
+
- **Durable**: `selectItemsForInjection()` in `durable-memory.ts:152` — scores items using `hitCount`, `lastHitAt`, and `updatedAt`; increments hit counters on selected items
|
|
206
209
|
- **Short-term**: `selectEntriesForInjection()` in `shortterm-memory.ts:113`
|
|
207
210
|
- **Summary**: `fast`-tier prompt constraint in `summarizer.ts:63`
|
|
208
211
|
- **History**: `fetchMessageHistory()` in `message-history.ts:38`
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadDurableMemory, saveDurableMemory, addItem, deprecateItems, selectItemsForInjection, formatDurableSection, } from './durable-memory.js';
|
|
1
|
+
import { loadDurableMemory, saveDurableMemory, addItem, deprecateItems, selectItemsForInjection, formatDurableSection, CURRENT_VERSION, } from './durable-memory.js';
|
|
2
2
|
import { durableWriteQueue } from './durable-write-queue.js';
|
|
3
3
|
const MEMORY_TYPE_MAP = {
|
|
4
4
|
memoryRemember: true,
|
|
@@ -71,7 +71,7 @@ export async function executeMemoryAction(action, _ctx, memCtx) {
|
|
|
71
71
|
// ---------------------------------------------------------------------------
|
|
72
72
|
async function loadOrCreate(dir, userId) {
|
|
73
73
|
const store = await loadDurableMemory(dir, userId);
|
|
74
|
-
return store ?? { version:
|
|
74
|
+
return store ?? { version: CURRENT_VERSION, updatedAt: 0, items: [] };
|
|
75
75
|
}
|
|
76
76
|
// ---------------------------------------------------------------------------
|
|
77
77
|
// Prompt section
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
|
-
export const CURRENT_VERSION =
|
|
4
|
+
export const CURRENT_VERSION = 2;
|
|
5
5
|
export function emptyStore() {
|
|
6
6
|
return { version: CURRENT_VERSION, updatedAt: Date.now(), items: [] };
|
|
7
7
|
}
|
|
8
8
|
function migrateStore(store) {
|
|
9
9
|
// Migration blocks run first, then the unknown-version guard.
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
if (store.version === 1) {
|
|
11
|
+
for (const item of store.items) {
|
|
12
|
+
item.hitCount = item.hitCount ?? 0;
|
|
13
|
+
item.lastHitAt = item.lastHitAt ?? 0;
|
|
14
|
+
}
|
|
15
|
+
store.version = 2;
|
|
16
|
+
}
|
|
12
17
|
if (store.version !== CURRENT_VERSION) {
|
|
13
18
|
// Unrecognized (future) version — caller will create a fresh store.
|
|
14
19
|
return null;
|
|
@@ -56,6 +61,31 @@ export function deriveItemId(kind, text) {
|
|
|
56
61
|
const normalized = kind + ':' + text.trim().toLowerCase().replace(/\s+/g, ' ');
|
|
57
62
|
return 'durable-' + crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 8);
|
|
58
63
|
}
|
|
64
|
+
function recencyWeight(timestamp, now) {
|
|
65
|
+
const daysSince = Math.max(0, now - timestamp) / (1000 * 60 * 60 * 24);
|
|
66
|
+
return 1 / (1 + daysSince / 30);
|
|
67
|
+
}
|
|
68
|
+
export function scoreItem(item, now) {
|
|
69
|
+
if (item.hitCount === 0)
|
|
70
|
+
return 0;
|
|
71
|
+
return item.hitCount * recencyWeight(item.lastHitAt, now);
|
|
72
|
+
}
|
|
73
|
+
export function blendedInjectionScore(item, now) {
|
|
74
|
+
const hitBoost = 0.5;
|
|
75
|
+
return recencyWeight(item.updatedAt, now) + hitBoost * item.hitCount * recencyWeight(item.lastHitAt, now);
|
|
76
|
+
}
|
|
77
|
+
export function recordHits(store, itemIds) {
|
|
78
|
+
const idSet = new Set(itemIds);
|
|
79
|
+
const now = Date.now();
|
|
80
|
+
for (const item of store.items) {
|
|
81
|
+
if (idSet.has(item.id)) {
|
|
82
|
+
item.hitCount += 1;
|
|
83
|
+
item.lastHitAt = now;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (idSet.size > 0)
|
|
87
|
+
store.updatedAt = now;
|
|
88
|
+
}
|
|
59
89
|
export function addItem(store, text, source, maxItems, kind = 'fact') {
|
|
60
90
|
const now = Date.now();
|
|
61
91
|
const id = deriveItemId(kind, text);
|
|
@@ -77,25 +107,27 @@ export function addItem(store, text, source, maxItems, kind = 'fact') {
|
|
|
77
107
|
source,
|
|
78
108
|
createdAt: now,
|
|
79
109
|
updatedAt: now,
|
|
110
|
+
hitCount: 0,
|
|
111
|
+
lastHitAt: 0,
|
|
80
112
|
};
|
|
81
113
|
store.items.push(item);
|
|
82
114
|
store.updatedAt = now;
|
|
83
115
|
// Enforce maxItems cap.
|
|
84
116
|
while (store.items.length > maxItems) {
|
|
85
|
-
// Drop
|
|
117
|
+
// Drop lowest-scored deprecated first.
|
|
86
118
|
const deprecatedIdx = store.items
|
|
87
119
|
.map((it, i) => ({ it, i }))
|
|
88
120
|
.filter(({ it }) => it.status === 'deprecated')
|
|
89
|
-
.sort((a, b) => a.it
|
|
121
|
+
.sort((a, b) => scoreItem(a.it, now) - scoreItem(b.it, now))[0];
|
|
90
122
|
if (deprecatedIdx) {
|
|
91
123
|
store.items.splice(deprecatedIdx.i, 1);
|
|
92
124
|
}
|
|
93
125
|
else {
|
|
94
|
-
// Drop
|
|
126
|
+
// Drop lowest-scored active.
|
|
95
127
|
const activeIdx = store.items
|
|
96
128
|
.map((it, i) => ({ it, i }))
|
|
97
129
|
.filter(({ it }) => it.status === 'active')
|
|
98
|
-
.sort((a, b) => a.it
|
|
130
|
+
.sort((a, b) => scoreItem(a.it, now) - scoreItem(b.it, now))[0];
|
|
99
131
|
if (activeIdx) {
|
|
100
132
|
store.items.splice(activeIdx.i, 1);
|
|
101
133
|
}
|
|
@@ -126,9 +158,10 @@ export function deprecateItems(store, substring) {
|
|
|
126
158
|
return { store, deprecatedCount };
|
|
127
159
|
}
|
|
128
160
|
export function selectItemsForInjection(store, maxChars) {
|
|
161
|
+
const now = Date.now();
|
|
129
162
|
const active = store.items
|
|
130
163
|
.filter((item) => item.status === 'active')
|
|
131
|
-
.sort((a, b) => b
|
|
164
|
+
.sort((a, b) => blendedInjectionScore(b, now) - blendedInjectionScore(a, now));
|
|
132
165
|
const selected = [];
|
|
133
166
|
let chars = 0;
|
|
134
167
|
for (const item of active) {
|
|
@@ -2,12 +2,12 @@ import { describe, expect, it } from 'vitest';
|
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { loadDurableMemory, saveDurableMemory, deriveItemId, addItem, deprecateItems, selectItemsForInjection, formatDurableSection, CURRENT_VERSION, } from './durable-memory.js';
|
|
5
|
+
import { loadDurableMemory, saveDurableMemory, deriveItemId, addItem, deprecateItems, selectItemsForInjection, formatDurableSection, scoreItem, blendedInjectionScore, recordHits, CURRENT_VERSION, } from './durable-memory.js';
|
|
6
6
|
async function makeTmpDir() {
|
|
7
7
|
return fs.mkdtemp(path.join(os.tmpdir(), 'durable-memory-test-'));
|
|
8
8
|
}
|
|
9
9
|
function emptyStore() {
|
|
10
|
-
return { version:
|
|
10
|
+
return { version: 2, updatedAt: 0, items: [] };
|
|
11
11
|
}
|
|
12
12
|
function makeItem(overrides = {}) {
|
|
13
13
|
return {
|
|
@@ -19,6 +19,8 @@ function makeItem(overrides = {}) {
|
|
|
19
19
|
source: { type: 'manual' },
|
|
20
20
|
createdAt: 1000,
|
|
21
21
|
updatedAt: 1000,
|
|
22
|
+
hitCount: 0,
|
|
23
|
+
lastHitAt: 0,
|
|
22
24
|
...overrides,
|
|
23
25
|
};
|
|
24
26
|
}
|
|
@@ -30,7 +32,7 @@ describe('loadDurableMemory', () => {
|
|
|
30
32
|
});
|
|
31
33
|
it('parses valid store', async () => {
|
|
32
34
|
const dir = await makeTmpDir();
|
|
33
|
-
const store = { version:
|
|
35
|
+
const store = { version: 2, updatedAt: 1000, items: [] };
|
|
34
36
|
await fs.writeFile(path.join(dir, '12345.json'), JSON.stringify(store), 'utf8');
|
|
35
37
|
const result = await loadDurableMemory(dir, '12345');
|
|
36
38
|
expect(result).toEqual(store);
|
|
@@ -45,12 +47,31 @@ describe('loadDurableMemory', () => {
|
|
|
45
47
|
const dir = await makeTmpDir();
|
|
46
48
|
await expect(loadDurableMemory(dir, '../evil')).rejects.toThrow(/Invalid userId/);
|
|
47
49
|
});
|
|
48
|
-
it('
|
|
50
|
+
it('migrates v1 store to v2 with hitCount and lastHitAt backfilled', async () => {
|
|
49
51
|
const dir = await makeTmpDir();
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
+
const v1Store = {
|
|
53
|
+
version: 1,
|
|
54
|
+
updatedAt: 1000,
|
|
55
|
+
items: [
|
|
56
|
+
{
|
|
57
|
+
id: 'durable-abc12345',
|
|
58
|
+
kind: 'fact',
|
|
59
|
+
text: 'test item',
|
|
60
|
+
tags: [],
|
|
61
|
+
status: 'active',
|
|
62
|
+
source: { type: 'manual' },
|
|
63
|
+
createdAt: 500,
|
|
64
|
+
updatedAt: 1000,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
await fs.writeFile(path.join(dir, 'user1.json'), JSON.stringify(v1Store), 'utf8');
|
|
52
69
|
const result = await loadDurableMemory(dir, 'user1');
|
|
53
|
-
expect(result).
|
|
70
|
+
expect(result).not.toBeNull();
|
|
71
|
+
expect(result.version).toBe(2);
|
|
72
|
+
expect(result.items).toHaveLength(1);
|
|
73
|
+
expect(result.items[0].hitCount).toBe(0);
|
|
74
|
+
expect(result.items[0].lastHitAt).toBe(0);
|
|
54
75
|
});
|
|
55
76
|
it('returns empty store for unsupported version', async () => {
|
|
56
77
|
const dir = await makeTmpDir();
|
|
@@ -279,3 +300,81 @@ describe('formatDurableSection', () => {
|
|
|
279
300
|
expect(result).toMatch(/src: manual, updated/);
|
|
280
301
|
});
|
|
281
302
|
});
|
|
303
|
+
describe('scoreItem', () => {
|
|
304
|
+
it('returns 0 when hitCount is 0', () => {
|
|
305
|
+
const item = makeItem({ hitCount: 0, lastHitAt: 1000 });
|
|
306
|
+
expect(scoreItem(item, Date.now())).toBe(0);
|
|
307
|
+
});
|
|
308
|
+
it('scores higher with recent lastHitAt than distant lastHitAt', () => {
|
|
309
|
+
const now = Date.now();
|
|
310
|
+
const recent = makeItem({ hitCount: 5, lastHitAt: now - 1000 });
|
|
311
|
+
const distant = makeItem({ hitCount: 5, lastHitAt: now - 90 * 24 * 60 * 60 * 1000 });
|
|
312
|
+
expect(scoreItem(recent, now)).toBeGreaterThan(scoreItem(distant, now));
|
|
313
|
+
});
|
|
314
|
+
it('scores higher with more hits', () => {
|
|
315
|
+
const now = Date.now();
|
|
316
|
+
const manyHits = makeItem({ hitCount: 10, lastHitAt: now - 1000 });
|
|
317
|
+
const fewHits = makeItem({ hitCount: 2, lastHitAt: now - 1000 });
|
|
318
|
+
expect(scoreItem(manyHits, now)).toBeGreaterThan(scoreItem(fewHits, now));
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
describe('blendedInjectionScore', () => {
|
|
322
|
+
it('recently-updated + frequently-hit item scores above old + never-hit item', () => {
|
|
323
|
+
const now = Date.now();
|
|
324
|
+
const hotItem = makeItem({
|
|
325
|
+
updatedAt: now - 1000,
|
|
326
|
+
hitCount: 10,
|
|
327
|
+
lastHitAt: now - 1000,
|
|
328
|
+
});
|
|
329
|
+
const coldItem = makeItem({
|
|
330
|
+
updatedAt: now - 60 * 24 * 60 * 60 * 1000,
|
|
331
|
+
hitCount: 0,
|
|
332
|
+
lastHitAt: 0,
|
|
333
|
+
});
|
|
334
|
+
expect(blendedInjectionScore(hotItem, now)).toBeGreaterThan(blendedInjectionScore(coldItem, now));
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
describe('addItem — eviction with hit tracking', () => {
|
|
338
|
+
it('evicts never-hit item before frequently-hit item of same age', () => {
|
|
339
|
+
const store = emptyStore();
|
|
340
|
+
const now = Date.now();
|
|
341
|
+
store.items.push(makeItem({ id: 'never-hit', status: 'active', text: 'never hit', updatedAt: now - 1000, hitCount: 0, lastHitAt: 0 }), makeItem({ id: 'freq-hit', status: 'active', text: 'frequently hit', updatedAt: now - 1000, hitCount: 10, lastHitAt: now - 500 }));
|
|
342
|
+
addItem(store, 'new item', { type: 'manual' }, 2);
|
|
343
|
+
expect(store.items).toHaveLength(2);
|
|
344
|
+
expect(store.items.find((it) => it.id === 'never-hit')).toBeUndefined();
|
|
345
|
+
expect(store.items.find((it) => it.id === 'freq-hit')).toBeDefined();
|
|
346
|
+
});
|
|
347
|
+
it('evicts deprecated never-hit item before active never-hit item', () => {
|
|
348
|
+
const store = emptyStore();
|
|
349
|
+
const now = Date.now();
|
|
350
|
+
store.items.push(makeItem({ id: 'dep-no-hit', status: 'deprecated', text: 'deprecated no hit', updatedAt: now - 1000, hitCount: 0, lastHitAt: 0 }), makeItem({ id: 'active-no-hit', status: 'active', text: 'active no hit', updatedAt: now - 1000, hitCount: 0, lastHitAt: 0 }));
|
|
351
|
+
addItem(store, 'new item', { type: 'manual' }, 2);
|
|
352
|
+
expect(store.items).toHaveLength(2);
|
|
353
|
+
expect(store.items.find((it) => it.id === 'dep-no-hit')).toBeUndefined();
|
|
354
|
+
expect(store.items.find((it) => it.id === 'active-no-hit')).toBeDefined();
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
describe('selectItemsForInjection — blended scoring', () => {
|
|
358
|
+
it('orders by blended score, not just updatedAt', () => {
|
|
359
|
+
const now = Date.now();
|
|
360
|
+
const store = emptyStore();
|
|
361
|
+
store.items.push(makeItem({ id: 'old-hot', text: 'old but popular', status: 'active', updatedAt: now - 30 * 24 * 60 * 60 * 1000, hitCount: 20, lastHitAt: now - 1000 }), makeItem({ id: 'new-cold', text: 'new but ignored', status: 'active', updatedAt: now - 1000, hitCount: 0, lastHitAt: 0 }));
|
|
362
|
+
const items = selectItemsForInjection(store, 10000);
|
|
363
|
+
expect(items).toHaveLength(2);
|
|
364
|
+
expect(items[0].id).toBe('old-hot');
|
|
365
|
+
expect(items[1].id).toBe('new-cold');
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
describe('recordHits', () => {
|
|
369
|
+
it('bumps hitCount and lastHitAt on matching IDs, leaves others untouched', () => {
|
|
370
|
+
const store = emptyStore();
|
|
371
|
+
store.items.push(makeItem({ id: 'hit-me', hitCount: 3, lastHitAt: 500 }), makeItem({ id: 'leave-me', hitCount: 1, lastHitAt: 400 }));
|
|
372
|
+
const before = store.items[1].hitCount;
|
|
373
|
+
const beforeLastHit = store.items[1].lastHitAt;
|
|
374
|
+
recordHits(store, ['hit-me']);
|
|
375
|
+
expect(store.items[0].hitCount).toBe(4);
|
|
376
|
+
expect(store.items[0].lastHitAt).toBeGreaterThan(500);
|
|
377
|
+
expect(store.items[1].hitCount).toBe(before);
|
|
378
|
+
expect(store.items[1].lastHitAt).toBe(beforeLastHit);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { buildPromptPreamble as buildRootPolicyPreamble } from '../root-policy.js';
|
|
4
|
-
import { formatDurableSection, loadDurableMemory, selectItemsForInjection } from './durable-memory.js';
|
|
4
|
+
import { formatDurableSection, loadDurableMemory, saveDurableMemory, selectItemsForInjection, recordHits } from './durable-memory.js';
|
|
5
|
+
import { durableWriteQueue } from './durable-write-queue.js';
|
|
5
6
|
import { buildShortTermMemorySection } from './shortterm-memory.js';
|
|
6
7
|
import { loadWorkspacePermissions, resolveTools } from '../workspace-permissions.js';
|
|
7
8
|
import { isOnboardingComplete } from '../workspace-bootstrap.js';
|
|
@@ -121,6 +122,19 @@ export async function buildDurableMemorySection(opts) {
|
|
|
121
122
|
const items = selectItemsForInjection(store, opts.durableInjectMaxChars);
|
|
122
123
|
if (items.length === 0)
|
|
123
124
|
return '';
|
|
125
|
+
// Record hits on injected items so frequently-used items accumulate
|
|
126
|
+
// a Hebbian signal for scoring and eviction. Fire-and-forget — the
|
|
127
|
+
// background write doesn't block prompt assembly.
|
|
128
|
+
const itemIds = items.map((it) => it.id);
|
|
129
|
+
durableWriteQueue.run(opts.userId, async () => {
|
|
130
|
+
const freshStore = await loadDurableMemory(opts.durableDataDir, opts.userId);
|
|
131
|
+
if (!freshStore)
|
|
132
|
+
return;
|
|
133
|
+
recordHits(freshStore, itemIds);
|
|
134
|
+
await saveDurableMemory(opts.durableDataDir, opts.userId, freshStore);
|
|
135
|
+
}).catch((err) => {
|
|
136
|
+
opts.log?.warn({ err, userId: opts.userId }, 'durable memory hit recording failed');
|
|
137
|
+
});
|
|
124
138
|
return formatDurableSection(items);
|
|
125
139
|
}
|
|
126
140
|
catch (err) {
|
|
@@ -321,7 +321,7 @@ describe('createReactionAddHandler', () => {
|
|
|
321
321
|
const prompt = invokeSpy.mock.calls[0][0].prompt;
|
|
322
322
|
expect(prompt).toContain('Durable memory');
|
|
323
323
|
expect(prompt).toContain('User loves TypeScript');
|
|
324
|
-
await fsP.rm(tmpDir, { recursive: true });
|
|
324
|
+
await fsP.rm(tmpDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
325
325
|
});
|
|
326
326
|
it('prompt includes open-tasks section when taskCtx has open tasks', async () => {
|
|
327
327
|
const { TaskStore } = await import('../tasks/store.js');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadDurableMemory, saveDurableMemory, addItem, deprecateItems, selectItemsForInjection } from './durable-memory.js';
|
|
1
|
+
import { loadDurableMemory, saveDurableMemory, addItem, deprecateItems, selectItemsForInjection, CURRENT_VERSION } from './durable-memory.js';
|
|
2
2
|
import { durableWriteQueue } from './durable-write-queue.js';
|
|
3
3
|
import { extractFirstJsonValue } from './json-extract.js';
|
|
4
4
|
const VALID_KINDS = new Set([
|
|
@@ -120,7 +120,7 @@ export async function applyUserTurnToDurable(opts) {
|
|
|
120
120
|
return;
|
|
121
121
|
await durableWriteQueue.run(opts.userId, async () => {
|
|
122
122
|
const store = (await loadDurableMemory(opts.durableDataDir, opts.userId)) ?? {
|
|
123
|
-
version:
|
|
123
|
+
version: CURRENT_VERSION,
|
|
124
124
|
updatedAt: 0,
|
|
125
125
|
items: [],
|
|
126
126
|
};
|