agent-working-memory 0.7.15 → 0.7.17

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.
Files changed (43) hide show
  1. package/README.md +16 -0
  2. package/dist/adapters/claude-code.d.ts.map +1 -1
  3. package/dist/adapters/claude-code.js +2 -16
  4. package/dist/adapters/claude-code.js.map +1 -1
  5. package/dist/adapters/codex.d.ts.map +1 -1
  6. package/dist/adapters/codex.js +2 -11
  7. package/dist/adapters/codex.js.map +1 -1
  8. package/dist/adapters/common.d.ts +18 -0
  9. package/dist/adapters/common.d.ts.map +1 -1
  10. package/dist/adapters/common.js +177 -15
  11. package/dist/adapters/common.js.map +1 -1
  12. package/dist/adapters/cursor.d.ts.map +1 -1
  13. package/dist/adapters/cursor.js +2 -15
  14. package/dist/adapters/cursor.js.map +1 -1
  15. package/dist/adapters/http.d.ts.map +1 -1
  16. package/dist/adapters/http.js +6 -12
  17. package/dist/adapters/http.js.map +1 -1
  18. package/dist/api/routes.d.ts.map +1 -1
  19. package/dist/api/routes.js +45 -57
  20. package/dist/api/routes.js.map +1 -1
  21. package/dist/cli.js +103 -103
  22. package/dist/core/write-pipeline.d.ts +120 -0
  23. package/dist/core/write-pipeline.d.ts.map +1 -0
  24. package/dist/core/write-pipeline.js +236 -0
  25. package/dist/core/write-pipeline.js.map +1 -0
  26. package/dist/engine/activation.d.ts.map +1 -1
  27. package/dist/engine/activation.js +5 -11
  28. package/dist/engine/activation.js.map +1 -1
  29. package/dist/index.js +1 -1
  30. package/dist/mcp.js +107 -178
  31. package/dist/mcp.js.map +1 -1
  32. package/package.json +1 -1
  33. package/src/adapters/claude-code.ts +2 -18
  34. package/src/adapters/codex.ts +2 -12
  35. package/src/adapters/common.ts +191 -15
  36. package/src/adapters/cursor.ts +2 -17
  37. package/src/adapters/http.ts +5 -12
  38. package/src/api/routes.ts +714 -723
  39. package/src/cli.ts +719 -719
  40. package/src/core/write-pipeline.ts +343 -0
  41. package/src/engine/activation.ts +5 -11
  42. package/src/index.ts +212 -212
  43. package/src/mcp.ts +1121 -1192
@@ -0,0 +1,343 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * Unified write pipeline — shared implementation of the three write-time
5
+ * memory rules (R1/R2/R3) that distinguish AWM as a "memory" system
6
+ * (selective retention) rather than "storage" (retrieve-all-then-dump):
7
+ *
8
+ * R1 — Reinforce on duplicate. Repeat = stronger memory. When a new
9
+ * write shares the EXACT same concept as an existing engram,
10
+ * boost that engram's confidence + access count instead of
11
+ * creating a new near-duplicate. One strong engram beats N weak
12
+ * ones.
13
+ *
14
+ * R2 — Pick the RIGHT match. Skip the match if it's already
15
+ * superseded, unhealthy (confidence < 0.3), or not in active
16
+ * stage. If the match is superseded, reinforce the SUPERSEDER
17
+ * instead (the "we fixed it, now we know better" chain).
18
+ *
19
+ * R3 — Corrections override. When the write's eventType is `surprise`
20
+ * or `friction` AND the matched engram is the same concept, the
21
+ * new write SUPERSEDES the matched engram instead of reinforcing
22
+ * it. Fresh truth beats old habit.
23
+ *
24
+ * Critical implementation detail (lesson from LoCoMo 2026-05-12): the
25
+ * match-vs-create pivot is **concept equality**, not raw novelty.
26
+ * Thresholding on novelty alone collapses distinct facts that happen to
27
+ * share template language (e.g. 419 conversation turns all prefixed
28
+ * "[session_3] Caroline: ..." merged into 7 engrams, recall coverage
29
+ * halved). Concept equality is the sharp signal: same concept means
30
+ * the writer is restating the same topic.
31
+ *
32
+ * Disposition (active/staging/discard) is still set from evaluateSalience
33
+ * for the CREATE path. REINFORCE writes never reach staging — they just
34
+ * touch an existing engram. SUPERSEDE writes follow the disposition of
35
+ * the new engram (typically active because corrections are high-salience).
36
+ */
37
+
38
+ import type { EngramStore } from '../storage/sqlite.js';
39
+ import type { ConnectionEngine } from '../engine/connections.js';
40
+ import type { Engram, MemoryClass, MemoryType } from '../types/engram.js';
41
+ import {
42
+ evaluateSalience,
43
+ computeNoveltyWithMatch,
44
+ detectUserFeedback,
45
+ type SalienceEventType,
46
+ type SalienceResult,
47
+ type NoveltyResult,
48
+ } from './salience.js';
49
+ import { embed } from './embeddings.js';
50
+ import { DEFAULT_AGENT_CONFIG } from '../types/agent.js';
51
+
52
+ /** Confidence floor below which a matched engram is treated as "decaying out". */
53
+ export const HEALTHY_CONFIDENCE_FLOOR = 0.3;
54
+
55
+ /** Confidence delta applied on reinforcement. Bounded by REINFORCE_CONFIDENCE_CEIL. */
56
+ export const REINFORCE_CONFIDENCE_DELTA = 0.05;
57
+ export const REINFORCE_CONFIDENCE_CEIL = 0.95;
58
+
59
+ /** Default disposition-confidence priors when the caller doesn't supply one. */
60
+ const CONFIDENCE_PRIORS: Record<string, number> = {
61
+ decision: 0.65,
62
+ friction: 0.60,
63
+ causal: 0.60,
64
+ surprise: 0.55,
65
+ user_feedback: 0.70,
66
+ observation: 0.45,
67
+ };
68
+
69
+ export type WriteAction = 'create' | 'reinforce' | 'supersede';
70
+
71
+ export interface WriteInput {
72
+ agentId: string;
73
+ concept: string;
74
+ content: string;
75
+ /** Tags as the caller wants them stored (already assembled). */
76
+ tags?: string[];
77
+ memoryClass?: MemoryClass;
78
+ memoryType?: MemoryType;
79
+ eventType?: SalienceEventType;
80
+ surprise?: number;
81
+ decisionMade?: boolean;
82
+ causalDepth?: number;
83
+ resolutionEffort?: number;
84
+ /** Confidence override. When unset, defaults from disposition + eventType priors. */
85
+ confidence?: number;
86
+ /** Explicit supersession requested by caller (independent of correction-on-match). */
87
+ supersedes?: string;
88
+ /** Workspace scope for cross-agent novelty (v0.5.4+ stores only). */
89
+ workspace?: string | null;
90
+ /** Set to false to skip the reinforce/supersede branching and always create. */
91
+ enableReinforcement?: boolean;
92
+ }
93
+
94
+ export interface WriteResult {
95
+ action: WriteAction;
96
+ /**
97
+ * For action='create' or 'supersede': the newly created engram.
98
+ * For action='reinforce': the EXISTING engram that was reinforced
99
+ * (its confidence and access_count have been bumped in place).
100
+ */
101
+ engram: Engram;
102
+ /** Salience result — present for create/supersede; null for reinforce (no new salience evaluation). */
103
+ salience: SalienceResult | null;
104
+ /** Novelty + match info, useful for caller logging. */
105
+ noveltyResult: NoveltyResult;
106
+ /** Reinforcement detail — present only for action='reinforce'. */
107
+ reinforce?: {
108
+ previousConfidence: number;
109
+ newConfidence: number;
110
+ previousAccessCount: number;
111
+ };
112
+ /** Supersession detail — present only for action='supersede'. */
113
+ supersedeOf?: { id: string };
114
+ }
115
+
116
+ export interface WritePipelineEngines {
117
+ store: EngramStore;
118
+ connectionEngine: ConnectionEngine;
119
+ }
120
+
121
+ /**
122
+ * Run a write through the unified pipeline.
123
+ *
124
+ * Side effects (always):
125
+ * - Compute novelty + best match
126
+ * - Evaluate salience for audit
127
+ *
128
+ * Side effects (action-dependent):
129
+ * - REINFORCE: touchEngram + updateConfidence on the matched engram;
130
+ * no new engram is created.
131
+ * - SUPERSEDE: createEngram with supersedes=matched.id, then call
132
+ * supersedeEngram. Async embed + enqueue follow.
133
+ * - CREATE: createEngram, async embed + enqueue. If salience says
134
+ * staging, updateStage to 'staging'.
135
+ *
136
+ * The caller is responsible for:
137
+ * - Tag assembly (callers know their own metadata format)
138
+ * - Temporal adjacency edges
139
+ * - Episode assignment
140
+ * - Auto-checkpoint tracking
141
+ * - Decision propagation
142
+ *
143
+ * Set process.env.AWM_WRITE_PIPELINE=off to revert to legacy create-only
144
+ * behavior (the same write inputs but every write creates a new engram).
145
+ */
146
+ export function performWrite(
147
+ engines: WritePipelineEngines,
148
+ input: WriteInput,
149
+ ): WriteResult {
150
+ const { store, connectionEngine } = engines;
151
+ const enableReinforcement = input.enableReinforcement !== false
152
+ && process.env.AWM_WRITE_PIPELINE !== 'off';
153
+
154
+ const noveltyResult = computeNoveltyWithMatch(
155
+ store, input.agentId, input.concept, input.content, input.workspace ?? null,
156
+ );
157
+
158
+ // Effective event type — auto-promote user-feedback content.
159
+ const effectiveEventType: SalienceEventType =
160
+ input.eventType ?? (detectUserFeedback(input.content) ? 'user_feedback' : 'observation');
161
+
162
+ // Effective memory class — auto-canonical for user-feedback and verified findings.
163
+ let effectiveMemoryClass: MemoryClass | undefined = input.memoryClass;
164
+ if (!effectiveMemoryClass && effectiveEventType === 'user_feedback') {
165
+ effectiveMemoryClass = 'canonical';
166
+ }
167
+
168
+ const salience = evaluateSalience({
169
+ content: input.content,
170
+ eventType: effectiveEventType,
171
+ surprise: input.surprise,
172
+ decisionMade: input.decisionMade,
173
+ causalDepth: input.causalDepth,
174
+ resolutionEffort: input.resolutionEffort,
175
+ novelty: noveltyResult.novelty,
176
+ memoryClass: effectiveMemoryClass,
177
+ });
178
+
179
+ // -- Reinforce / Supersede branching --
180
+ if (enableReinforcement && noveltyResult.matchedEngramId) {
181
+ const matched = store.getEngram(noveltyResult.matchedEngramId);
182
+ if (matched) {
183
+ const newConcept = (input.concept ?? '').toLowerCase().trim();
184
+ const matchedConcept = (matched.concept ?? '').toLowerCase().trim();
185
+ const sameConcept = newConcept === matchedConcept && newConcept.length > 0;
186
+
187
+ if (sameConcept) {
188
+ const isCorrectionSignal = effectiveEventType === 'surprise'
189
+ || effectiveEventType === 'friction';
190
+
191
+ if (isCorrectionSignal) {
192
+ // R3 — supersede the matched engram with the new write
193
+ return createNewEngram(engines, input, salience, noveltyResult, {
194
+ effectiveEventType,
195
+ effectiveMemoryClass,
196
+ supersedesId: matched.id,
197
+ });
198
+ }
199
+
200
+ // R2 — health check on the matched engram
201
+ const isHealthy = matched.stage === 'active'
202
+ && matched.confidence >= HEALTHY_CONFIDENCE_FLOOR
203
+ && matched.supersededBy == null;
204
+
205
+ if (isHealthy) {
206
+ // R1 — reinforce
207
+ return reinforceMatched(store, matched, noveltyResult, salience);
208
+ }
209
+
210
+ // Unhealthy match but it was superseded — try to reinforce the superseder
211
+ if (matched.supersededBy) {
212
+ const superseder = store.getEngram(matched.supersededBy);
213
+ if (superseder && superseder.stage === 'active'
214
+ && superseder.confidence >= HEALTHY_CONFIDENCE_FLOOR
215
+ && superseder.supersededBy == null) {
216
+ return reinforceMatched(store, superseder, noveltyResult, salience);
217
+ }
218
+ }
219
+
220
+ // Otherwise fall through to create new
221
+ }
222
+ }
223
+ }
224
+
225
+ // -- Default: create new engram --
226
+ return createNewEngram(engines, input, salience, noveltyResult, {
227
+ effectiveEventType,
228
+ effectiveMemoryClass,
229
+ supersedesId: input.supersedes,
230
+ });
231
+ }
232
+
233
+ function reinforceMatched(
234
+ store: EngramStore,
235
+ matched: Engram,
236
+ noveltyResult: NoveltyResult,
237
+ salience: SalienceResult,
238
+ ): WriteResult {
239
+ const previousConfidence = matched.confidence;
240
+ const previousAccessCount = matched.accessCount;
241
+ const newConfidence = Math.min(
242
+ REINFORCE_CONFIDENCE_CEIL,
243
+ previousConfidence + REINFORCE_CONFIDENCE_DELTA,
244
+ );
245
+ store.updateConfidence(matched.id, newConfidence);
246
+ store.touchEngram(matched.id);
247
+
248
+ // Return the engram with the updated values reflected (the DB write
249
+ // happened above; the in-memory object is one snapshot behind).
250
+ const refreshed: Engram = {
251
+ ...matched,
252
+ confidence: newConfidence,
253
+ accessCount: previousAccessCount + 1,
254
+ lastAccessed: new Date(),
255
+ };
256
+
257
+ return {
258
+ action: 'reinforce',
259
+ engram: refreshed,
260
+ salience: null,
261
+ noveltyResult,
262
+ reinforce: { previousConfidence, newConfidence, previousAccessCount },
263
+ };
264
+ }
265
+
266
+ function createNewEngram(
267
+ engines: WritePipelineEngines,
268
+ input: WriteInput,
269
+ salience: SalienceResult,
270
+ noveltyResult: NoveltyResult,
271
+ meta: {
272
+ effectiveEventType: SalienceEventType;
273
+ effectiveMemoryClass: MemoryClass | undefined;
274
+ supersedesId: string | undefined;
275
+ },
276
+ ): WriteResult {
277
+ const { store, connectionEngine } = engines;
278
+
279
+ const isLowSalience = salience.disposition === 'discard';
280
+
281
+ // Confidence: caller wins, then disposition-aware prior, then fall back
282
+ // to eventType prior.
283
+ const confidence = input.confidence
284
+ ?? (isLowSalience
285
+ ? 0.25
286
+ : salience.disposition === 'staging'
287
+ ? 0.40
288
+ : CONFIDENCE_PRIORS[meta.effectiveEventType] ?? 0.45);
289
+
290
+ const tags = [...(input.tags ?? [])];
291
+ if (isLowSalience && !tags.includes('low-salience')) tags.push('low-salience');
292
+
293
+ const engram = store.createEngram({
294
+ agentId: input.agentId,
295
+ concept: input.concept,
296
+ content: input.content,
297
+ tags,
298
+ salience: salience.score,
299
+ confidence,
300
+ salienceFeatures: salience.features,
301
+ reasonCodes: salience.reasonCodes,
302
+ memoryClass: meta.effectiveMemoryClass,
303
+ memoryType: input.memoryType,
304
+ ttl: salience.disposition === 'staging' ? DEFAULT_AGENT_CONFIG.stagingTtlMs : undefined,
305
+ supersedes: meta.supersedesId,
306
+ });
307
+
308
+ if (salience.disposition === 'staging') {
309
+ store.updateStage(engram.id, 'staging');
310
+ }
311
+
312
+ // Supersession side-effects: mark the old engram, add causal edge.
313
+ if (meta.supersedesId) {
314
+ try {
315
+ const oldEngram = store.getEngram(meta.supersedesId);
316
+ if (oldEngram) {
317
+ store.supersedeEngram(meta.supersedesId, engram.id);
318
+ store.upsertAssociation(engram.id, oldEngram.id, 0.8, 'causal', 0.9);
319
+ }
320
+ } catch { /* supersession is best-effort */ }
321
+ }
322
+
323
+ // Connection discovery — only for non-staged writes (active or low-salience)
324
+ if (salience.disposition === 'active' || isLowSalience) {
325
+ try { connectionEngine.enqueue(engram.id); } catch { /* non-fatal */ }
326
+ }
327
+
328
+ // Async embed — never blocks the response, failure non-fatal
329
+ embed(`${input.concept} ${input.content}`)
330
+ .then(vec => {
331
+ try { store.updateEmbedding(engram.id, vec); } catch { /* engram may be evicted */ }
332
+ })
333
+ .catch(() => { /* embed failure tolerated */ });
334
+
335
+ const action: WriteAction = meta.supersedesId ? 'supersede' : 'create';
336
+ return {
337
+ action,
338
+ engram,
339
+ salience,
340
+ noveltyResult,
341
+ supersedeOf: meta.supersedesId ? { id: meta.supersedesId } : undefined,
342
+ };
343
+ }
@@ -580,15 +580,8 @@ export class ActivationEngine {
580
580
  .sort((a, b) => b.score - a.score);
581
581
 
582
582
  // Phase 7: Cross-encoder re-ranking — scores (query, passage) pairs directly
583
- // Widens the pool to find relevant results that keyword matching missed
584
- //
585
- // Pool size (0.7.13+): max(limit*2, 15). Was max(limit*3, 30); reduced because
586
- // the cross-encoder cost scales linearly with passage count and 30 was overkill
587
- // when limit is typically 5-10. Phase-breakdown showed reranker was 65% of the
588
- // post-0.7.12 recall floor — halving the pool is a direct ~50% reranker savings
589
- // (~100ms recovered on most queries) with negligible top-K quality impact at
590
- // limit=5/10 (the user wants top-5 or top-10; reranking 30 to find top-5 reranks
591
- // many candidates that won't be returned).
583
+ // Widens the pool to find relevant results that keyword matching missed.
584
+ // 0.7.13: max(limit*2, 15) — halved the cross-encoder cost (was max(limit*3, 30))
592
585
  const rerankPool = pool.slice(0, Math.max(limit * 2, 15));
593
586
 
594
587
  // Reranker skip heuristic (0.7.10+): if BM25 already has a clear winner with
@@ -679,7 +672,9 @@ export class ActivationEngine {
679
672
  ? Math.max(...simValues)
680
673
  : 1.0;
681
674
 
682
- // Stricter gate when caller explicitly requests abstention (e.g., noise filter queries)
675
+ // Required-channels for hard abstention:
676
+ // abstention-explicit (caller passed abstentionThreshold > 0): 3 of 3
677
+ // default: 2 of 3 — precision-first
683
678
  const requiredChannels = abstentionThreshold > 0 ? 3 : 2;
684
679
 
685
680
  // Hard abstention: fewer than required channels agree AND semantic drift is high
@@ -689,7 +684,6 @@ export class ActivationEngine {
689
684
 
690
685
  // Soft penalty: only 1 channel agrees or margin is thin
691
686
  if (channelsAgreeing < 2 || margin < 0.05) {
692
- // If caller explicitly requested abstention, honor it when agreement is weak
693
687
  if (abstentionThreshold > 0) {
694
688
  return [];
695
689
  }