agent-working-memory 0.12.0 → 0.12.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/README.md +69 -324
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +36 -7
- package/dist/adapters/common.js.map +1 -1
- package/dist/core/embeddings.js +1 -1
- package/dist/core/embeddings.js.map +1 -1
- package/dist/core/format-recall.d.ts +16 -0
- package/dist/core/format-recall.d.ts.map +1 -0
- package/dist/core/format-recall.js +24 -0
- package/dist/core/format-recall.js.map +1 -0
- package/dist/core/query-expander.js +1 -1
- package/dist/core/query-expander.js.map +1 -1
- package/dist/core/reranker.js +1 -1
- package/dist/core/reranker.js.map +1 -1
- package/dist/hooks/sidecar.d.ts +26 -0
- package/dist/hooks/sidecar.d.ts.map +1 -1
- package/dist/hooks/sidecar.js +30 -0
- package/dist/hooks/sidecar.js.map +1 -1
- package/dist/mcp.js +64 -18
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/common.ts +36 -7
- package/src/core/embeddings.ts +1 -1
- package/src/core/format-recall.ts +25 -0
- package/src/core/query-expander.ts +1 -1
- package/src/core/reranker.ts +1 -1
- package/src/hooks/sidecar.ts +55 -0
- package/src/mcp.ts +64 -18
package/src/adapters/common.ts
CHANGED
|
@@ -300,6 +300,29 @@ and entity-bridge boosts at recall time.
|
|
|
300
300
|
- \`person=<Name>\` for stakeholder quotes / decisions
|
|
301
301
|
- \`version=<X.Y.Z>\` for release-specific findings
|
|
302
302
|
|
|
303
|
+
### Entity index — exact-match recall for named things (default off)
|
|
304
|
+
Structured identifier tags (\`ticket=\`, \`person=\`, \`horse=\`, \`member=\`, bare 4+ digit
|
|
305
|
+
ids, etc.) feed a dedicated entity inverted index, separate from BM25/embedding scoring.
|
|
306
|
+
A query naming an entity ("ticket 19252", "Kaleigh Collett") can reach the memory through
|
|
307
|
+
this index even when the wording doesn't lexically match — it's a deterministic exact
|
|
308
|
+
lookup, immune to vocabulary mismatch. Keep identifier tags exact and consistent for
|
|
309
|
+
this reason, not just for the BM25 boost described above.
|
|
310
|
+
|
|
311
|
+
Off by default; opt in with \`AWM_ENTITY_INDEX_FETCH=1\` (bounded by
|
|
312
|
+
\`AWM_ENTITY_INDEX_CAP\`, default 12). Matched entities get no score boost — they're
|
|
313
|
+
guaranteed a reranker audition instead, so the cross-encoder alone decides whether they
|
|
314
|
+
surface. Worth trialing on identifier-heavy workloads (ticket/event numbers, named
|
|
315
|
+
people/things you refer to by name often); not yet the default pending evaluation.
|
|
316
|
+
|
|
317
|
+
### Temporal validity — memories that expire or start in the future
|
|
318
|
+
\`memory_write\` accepts \`valid_from\` / \`valid_to\` (ISO dates). Use \`valid_to\` on
|
|
319
|
+
**operational** facts with a real shelf life — a deploy state, "waiting on X's reply",
|
|
320
|
+
a ticket status — so the memory expires instead of relying on you to remember it's
|
|
321
|
+
stale. Recall renders \`[valid until …]\` on results carrying this field. Use
|
|
322
|
+
\`valid_from\` for a fact that becomes true on a known future date (a policy change, a
|
|
323
|
+
season that hasn't started yet). Don't set either for durable facts — most memories
|
|
324
|
+
don't need them.
|
|
325
|
+
|
|
303
326
|
### Memory classes (controls how strictly the salience filter gates the write)
|
|
304
327
|
- \`memory_class: canonical\` — source-of-truth memories. Floor 0.7 salience, never staged.
|
|
305
328
|
Use for: user-stated decisions, project requirements, verified architectural facts,
|
|
@@ -456,8 +479,14 @@ memory_write(
|
|
|
456
479
|
|
|
457
480
|
### Also:
|
|
458
481
|
- To track work items: memory_task_add, memory_task_update, memory_task_list, memory_task_next
|
|
482
|
+
- \`memory_whoami\` (MCP tool) / \`GET /whoami\` — identify the instance you're actually
|
|
483
|
+
talking to: agent id, workspace, mode, backend, store path, code provenance, sibling
|
|
484
|
+
agent spaces sharing the store. Call this FIRST whenever you're unsure which store,
|
|
485
|
+
which agent identity, or which running code you're dealing with — before reasoning
|
|
486
|
+
about AWM's own state from a stale memory or an assumed port number.
|
|
459
487
|
- AWM is shared across all agents in real time. When any agent writes or supersedes a
|
|
460
|
-
memory, every other agent can recall it immediately
|
|
488
|
+
memory, every other agent can recall it immediately — but only within the same
|
|
489
|
+
workspace and agent scope.
|
|
461
490
|
|
|
462
491
|
### Output compression (token efficiency, output-only)
|
|
463
492
|
When a tool returns a LARGE STRUCTURED result you need to keep in context — a JSON
|
|
@@ -500,11 +529,7 @@ for A/B testing if a regression appears in your workload:
|
|
|
500
529
|
Recall pipeline (0.7.x):
|
|
501
530
|
- \`AWM_DISABLE_POOL_FILTER=1\` — disables the candidate pool reduction
|
|
502
531
|
pre-filter in recall. Reverts to scoring all active candidates.
|
|
503
|
-
- \`
|
|
504
|
-
0 disables the always-on slow-write stderr line).
|
|
505
|
-
- \`memory_whoami\` (MCP) / \`GET /whoami\` — identify the instance (agent, mode,
|
|
506
|
-
backend, store path, code provenance, sibling agent spaces) when unsure
|
|
507
|
-
which AWM you are talking to.
|
|
532
|
+
- \`AWM_ENTITY_INDEX_FETCH=1\` — see "Entity index" above (0.12.x, default off).
|
|
508
533
|
- \`AWM_DISABLE_SLIM_CACHE=1\` — disables the in-memory slim cache.
|
|
509
534
|
Reverts to per-recall SQL fetch + Buffer→Float32Array conversion.
|
|
510
535
|
- \`AWM_DISABLE_RERANK_SKIP=1\` — disables the cross-encoder skip on
|
|
@@ -512,7 +537,11 @@ Recall pipeline (0.7.x):
|
|
|
512
537
|
- \`AWM_DISABLE_EXPANSION_CACHE=1\` — disables the query expansion skip
|
|
513
538
|
heuristic + LRU cache. Forces every recall through flan-t5-small.
|
|
514
539
|
|
|
515
|
-
Write pipeline + lifecycle (0.8.x):
|
|
540
|
+
Write pipeline + lifecycle (0.8.x, plus 0.12.x telemetry):
|
|
541
|
+
- \`AWM_SLOW_WRITE_MS=250\` (0.12.x) — any write slower than this logs one stderr
|
|
542
|
+
line with a phase-time breakdown (embed/novelty/persist, event-loop lag,
|
|
543
|
+
embed-model cold-load ms). \`0\` disables. Useful for diagnosing why a session's
|
|
544
|
+
first write/recall feels slow.
|
|
516
545
|
- \`AWM_REINFORCE_MAX_CONTENT_LEN=1500\` — max chars an engram's content
|
|
517
546
|
can grow to via merge-on-reinforce (drop-oldest on overflow). Higher =
|
|
518
547
|
preserves more reinforced detail; lower = leaner recall output.
|
package/src/core/embeddings.ts
CHANGED
|
@@ -37,7 +37,7 @@ async function loadInProcess(): Promise<FeatureExtractionPipeline> {
|
|
|
37
37
|
inProcessInitPromise = pipeline('feature-extraction', MODEL_ID, { dtype: 'fp32' }).then(pipe => {
|
|
38
38
|
inProcessInstance = pipe;
|
|
39
39
|
noteModelLoad(performance.now() - tLoadStart);
|
|
40
|
-
console.
|
|
40
|
+
console.error(`Embedding model loaded in-process: ${MODEL_ID} (${DIMENSIONS}d)`);
|
|
41
41
|
return pipe;
|
|
42
42
|
});
|
|
43
43
|
return inProcessInitPromise;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ActivationResult } from '../types/engram.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Formats one memory_recall result line for the MCP text response.
|
|
5
|
+
*
|
|
6
|
+
* Extracted out of the inline closure in mcp.ts (0.12.1) so the format itself
|
|
7
|
+
* — specifically, that every result carries its engram id — can be unit
|
|
8
|
+
* tested without booting the MCP server (mcp.ts has a top-level `await` and
|
|
9
|
+
* opens the store as a side effect of import, so it cannot be imported by a
|
|
10
|
+
* test directly).
|
|
11
|
+
*
|
|
12
|
+
* The id is placed right after the score, not at the end of the line: result
|
|
13
|
+
* bodies can be long, and a consumer scanning for `[id: ...]` shouldn't have
|
|
14
|
+
* to read past a paragraph of content to find it.
|
|
15
|
+
*/
|
|
16
|
+
export function formatRecallResultLine(r: ActivationResult, index: number): string {
|
|
17
|
+
const body = r.summary ?? r.engram.content;
|
|
18
|
+
const chain = r.engram.supersededBy
|
|
19
|
+
? ` ⚠ SUPERSEDED by ${r.engram.supersededBy} — treat as historical; recall/fetch the successor before relying on this.`
|
|
20
|
+
: '';
|
|
21
|
+
const validity = r.engram.validTo
|
|
22
|
+
? ` [valid until ${r.engram.validTo}]`
|
|
23
|
+
: '';
|
|
24
|
+
return `${index + 1}. **${r.engram.concept}** (${r.score.toFixed(3)}) [id: ${r.engram.id}]${validity}: ${body}${chain}`;
|
|
25
|
+
}
|
|
@@ -30,7 +30,7 @@ async function loadInProcess(): Promise<Text2TextGenerationPipeline> {
|
|
|
30
30
|
if (inProcessInitPromise) return inProcessInitPromise;
|
|
31
31
|
inProcessInitPromise = pipeline('text2text-generation', MODEL_ID, { dtype: 'fp32' }).then(pipe => {
|
|
32
32
|
inProcessInstance = pipe as Text2TextGenerationPipeline;
|
|
33
|
-
console.
|
|
33
|
+
console.error(`Query expander loaded in-process: ${MODEL_ID}`);
|
|
34
34
|
return inProcessInstance;
|
|
35
35
|
});
|
|
36
36
|
return inProcessInitPromise;
|
package/src/core/reranker.ts
CHANGED
|
@@ -35,7 +35,7 @@ async function ensureLoaded(): Promise<void> {
|
|
|
35
35
|
initPromise = (async () => {
|
|
36
36
|
tokenizer = await AutoTokenizer.from_pretrained(MODEL_ID);
|
|
37
37
|
model = await AutoModelForSequenceClassification.from_pretrained(MODEL_ID, { dtype: 'fp32' });
|
|
38
|
-
console.
|
|
38
|
+
console.error(`Re-ranker model loaded in-process: ${MODEL_ID}`);
|
|
39
39
|
})();
|
|
40
40
|
return initPromise;
|
|
41
41
|
}
|
package/src/hooks/sidecar.ts
CHANGED
|
@@ -28,6 +28,28 @@ export interface SidecarDeps {
|
|
|
28
28
|
secret: string | null;
|
|
29
29
|
port: number;
|
|
30
30
|
onConsolidate?: (agentId: string, reason: string) => void;
|
|
31
|
+
/**
|
|
32
|
+
* 0.12.2: warm recall for hooks. The sidecar runs in the same process as
|
|
33
|
+
* the MCP server's activation engine and its loaded ML models, so exposing
|
|
34
|
+
* recall here gives Claude Code hooks (e.g. a UserPromptSubmit PRIME hook)
|
|
35
|
+
* warm-latency recall with no standing server — the process lifecycle is
|
|
36
|
+
* owned by the session that spawned it. Optional so older callers and
|
|
37
|
+
* tests keep working unchanged.
|
|
38
|
+
*/
|
|
39
|
+
activate?: (q: {
|
|
40
|
+
context: string;
|
|
41
|
+
limit?: number;
|
|
42
|
+
requireConfidence?: number;
|
|
43
|
+
granularity?: 'full' | 'compact' | 'auto';
|
|
44
|
+
}) => Promise<Array<{
|
|
45
|
+
engram: {
|
|
46
|
+
id: string; concept: string; content: string;
|
|
47
|
+
createdAt?: string; memoryClass?: string; validTo?: string | null;
|
|
48
|
+
};
|
|
49
|
+
score: number;
|
|
50
|
+
summary?: string;
|
|
51
|
+
confidence?: number;
|
|
52
|
+
}>>;
|
|
31
53
|
}
|
|
32
54
|
|
|
33
55
|
interface HookInput {
|
|
@@ -187,6 +209,39 @@ export function startSidecar(deps: SidecarDeps): { close: () => void } {
|
|
|
187
209
|
return;
|
|
188
210
|
}
|
|
189
211
|
|
|
212
|
+
// POST /memory/activate — warm recall for hooks (0.12.2). Behind the auth
|
|
213
|
+
// gate above. Returns a trimmed subset of the HTTP API's response shape
|
|
214
|
+
// ({results: [{engram, score, summary, confidence}]}) so a hook written
|
|
215
|
+
// against either endpoint needs no branching.
|
|
216
|
+
if (req.url === '/memory/activate' && req.method === 'POST') {
|
|
217
|
+
if (!deps.activate) {
|
|
218
|
+
json(res, 501, { error: 'activate not wired on this sidecar' });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const body = JSON.parse((await readBody(req)) || '{}') as {
|
|
223
|
+
context?: string; query?: string; limit?: number;
|
|
224
|
+
requireConfidence?: number; granularity?: 'full' | 'compact' | 'auto';
|
|
225
|
+
};
|
|
226
|
+
const context = body.context ?? body.query;
|
|
227
|
+
if (!context) {
|
|
228
|
+
json(res, 400, { error: 'context (or query) is required' });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const results = await deps.activate({
|
|
232
|
+
context,
|
|
233
|
+
limit: body.limit,
|
|
234
|
+
requireConfidence: body.requireConfidence,
|
|
235
|
+
granularity: body.granularity,
|
|
236
|
+
});
|
|
237
|
+
log(agentId, 'hook:recall', `"${context.slice(0, 80)}" → ${results.length} results (sidecar)`);
|
|
238
|
+
json(res, 200, { results });
|
|
239
|
+
} catch (err) {
|
|
240
|
+
json(res, 500, { error: (err as Error).message });
|
|
241
|
+
}
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
|
|
190
245
|
// POST /hooks/checkpoint — auto-checkpoint from hook events
|
|
191
246
|
if (req.url === '/hooks/checkpoint' && req.method === 'POST') {
|
|
192
247
|
try {
|
package/src/mcp.ts
CHANGED
|
@@ -73,7 +73,9 @@ import type { ConsciousState } from './types/checkpoint.js';
|
|
|
73
73
|
import type { SalienceEventType } from './core/salience.js';
|
|
74
74
|
import type { TaskStatus, TaskPriority } from './types/engram.js';
|
|
75
75
|
import { DEFAULT_AGENT_CONFIG } from './types/agent.js';
|
|
76
|
-
import { embed } from './core/embeddings.js';
|
|
76
|
+
import { embed, getEmbedder } from './core/embeddings.js';
|
|
77
|
+
import { getReranker } from './core/reranker.js';
|
|
78
|
+
import { getExpander } from './core/query-expander.js';
|
|
77
79
|
import { startSidecar } from './hooks/sidecar.js';
|
|
78
80
|
import { initLogger, log, getLogPath } from './core/logger.js';
|
|
79
81
|
import { VERSION } from './version.js';
|
|
@@ -83,6 +85,7 @@ import { queryPeerDecisions, formatPeerDecisions } from './coordination/peer-dec
|
|
|
83
85
|
import { startLoopLagMonitor } from './core/write-telemetry.js';
|
|
84
86
|
import { buildWhoami, formatWhoami } from './core/whoami.js';
|
|
85
87
|
import { renderTaskEndInvitation, validateRecipeWrite, recipeSlug, getRecipe } from './recipes/index.js';
|
|
88
|
+
import { formatRecallResultLine } from './core/format-recall.js';
|
|
86
89
|
|
|
87
90
|
// --- Incognito Mode ---
|
|
88
91
|
// When AWM_INCOGNITO=1, register zero tools. Claude won't see memory tools at all.
|
|
@@ -461,22 +464,11 @@ Returns the most relevant memories ranked by text relevance, temporal recency, a
|
|
|
461
464
|
};
|
|
462
465
|
}
|
|
463
466
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
// D8 (2026-07-30): conflict surfacing — a superseded memory that still
|
|
470
|
-
// ranks is shown WITH its replacement pointer instead of silently
|
|
471
|
-
// down-ranked. The model should trust the successor.
|
|
472
|
-
const chain = r.engram.supersededBy
|
|
473
|
-
? ` ⚠ SUPERSEDED by ${r.engram.supersededBy} — treat as historical; recall/fetch the successor before relying on this.`
|
|
474
|
-
: '';
|
|
475
|
-
const validity = r.engram.validTo
|
|
476
|
-
? ` [valid until ${r.engram.validTo}]`
|
|
477
|
-
: '';
|
|
478
|
-
return `${i + 1}. **${r.engram.concept}** (${r.score.toFixed(3)})${validity}: ${body}${chain}`;
|
|
479
|
-
});
|
|
467
|
+
// Confidence-adaptive output (Paper 3: cognitive teaming) and D8
|
|
468
|
+
// (2026-07-30) conflict surfacing both live in the shared formatter now —
|
|
469
|
+
// see core/format-recall.ts for why it's extracted (0.12.1: unit-testable
|
|
470
|
+
// without booting the server) and why the id sits after the score.
|
|
471
|
+
const lines = results.map(formatRecallResultLine);
|
|
480
472
|
|
|
481
473
|
return {
|
|
482
474
|
content: [{
|
|
@@ -564,7 +556,7 @@ Use this when:
|
|
|
564
556
|
|
|
565
557
|
The old memory stays in the database (searchable for history) but is heavily down-ranked in recall so the current version dominates.`,
|
|
566
558
|
{
|
|
567
|
-
old_engram_id: z.string().describe('ID of the outdated memory'),
|
|
559
|
+
old_engram_id: z.string().describe('ID of the outdated memory (from memory_recall results, or memory_write\'s own response if you just wrote it)'),
|
|
568
560
|
new_engram_id: z.string().describe('ID of the replacement memory'),
|
|
569
561
|
reason: z.string().optional().describe('Why the old memory is outdated'),
|
|
570
562
|
},
|
|
@@ -1288,6 +1280,35 @@ async function main() {
|
|
|
1288
1280
|
agentId: AGENT_ID,
|
|
1289
1281
|
secret: HOOK_SECRET,
|
|
1290
1282
|
port: HOOK_PORT,
|
|
1283
|
+
// 0.12.2: warm recall for hooks — the sidecar shares this process's
|
|
1284
|
+
// activation engine and loaded models, so a UserPromptSubmit hook can get
|
|
1285
|
+
// warm-latency recall without any standing server. Trimmed result shape
|
|
1286
|
+
// (no embeddings/phase scores — hooks don't need them and the vectors
|
|
1287
|
+
// alone would 10x the payload).
|
|
1288
|
+
activate: async (q) => {
|
|
1289
|
+
const results = await activationEngine.activate({
|
|
1290
|
+
agentId: AGENT_ID,
|
|
1291
|
+
context: q.context,
|
|
1292
|
+
limit: q.limit,
|
|
1293
|
+
requireConfidence: q.requireConfidence,
|
|
1294
|
+
granularity: q.granularity,
|
|
1295
|
+
});
|
|
1296
|
+
return results.map(r => ({
|
|
1297
|
+
engram: {
|
|
1298
|
+
id: r.engram.id,
|
|
1299
|
+
concept: r.engram.concept,
|
|
1300
|
+
content: r.engram.content,
|
|
1301
|
+
createdAt: r.engram.createdAt instanceof Date
|
|
1302
|
+
? r.engram.createdAt.toISOString()
|
|
1303
|
+
: (r.engram.createdAt as unknown as string | undefined),
|
|
1304
|
+
memoryClass: r.engram.memoryClass,
|
|
1305
|
+
validTo: r.engram.validTo,
|
|
1306
|
+
},
|
|
1307
|
+
score: r.score,
|
|
1308
|
+
summary: r.summary,
|
|
1309
|
+
confidence: r.confidence,
|
|
1310
|
+
}));
|
|
1311
|
+
},
|
|
1291
1312
|
onConsolidate: async (agentId, reason) => {
|
|
1292
1313
|
console.error(`[mcp] consolidation triggered: ${reason}`);
|
|
1293
1314
|
const result = await consolidationEngine.consolidate(agentId);
|
|
@@ -1296,6 +1317,31 @@ async function main() {
|
|
|
1296
1317
|
},
|
|
1297
1318
|
});
|
|
1298
1319
|
|
|
1320
|
+
// 0.12.2: eager warm — fire-and-forget, mirrors index.ts:208-218. Without
|
|
1321
|
+
// this, every Claude Code session paid the full cold cost (~3s measured on
|
|
1322
|
+
// a 29.7k-engram store: slim cache ~0.9s + three model loads ~1.8s) on its
|
|
1323
|
+
// FIRST recall, which is exactly the "first recall is slow → recall gets
|
|
1324
|
+
// avoided" failure mode. Warming here overlaps session startup instead.
|
|
1325
|
+
// All output is stderr-safe (stdout carries JSON-RPC frames).
|
|
1326
|
+
// Escape hatch: AWM_NO_EAGER_WARM=1 restores lazy loading.
|
|
1327
|
+
if (process.env.AWM_NO_EAGER_WARM !== '1') {
|
|
1328
|
+
getEmbedder().catch(err => console.error('Embedding model unavailable:', err.message));
|
|
1329
|
+
getReranker().catch(err => console.error('Reranker model unavailable:', err.message));
|
|
1330
|
+
getExpander().catch(err => console.error('Query expander model unavailable:', err.message));
|
|
1331
|
+
if (BACKEND === 'sqlite') {
|
|
1332
|
+
setImmediate(() => {
|
|
1333
|
+
try {
|
|
1334
|
+
const t0 = Date.now();
|
|
1335
|
+
(store as unknown as EngramStore).warmSlimCache();
|
|
1336
|
+
const stats = (store as unknown as EngramStore).getSlimCacheStats();
|
|
1337
|
+
console.error(`Slim cache warmed: ${stats.size} entries in ${Date.now() - t0}ms`);
|
|
1338
|
+
} catch (err) {
|
|
1339
|
+
console.error(`Slim cache warm failed: ${(err as Error).message}`);
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1299
1345
|
// Coordination MCP tools (opt-in via AWM_COORDINATION=true)
|
|
1300
1346
|
// AWM 0.8.x: coordination requires SQLite (uses store.getDb()). On PGlite,
|
|
1301
1347
|
// coordination is auto-disabled with a warning; re-enable when coordination
|