agent-working-memory 0.11.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.
Files changed (115) hide show
  1. package/README.md +71 -297
  2. package/dist/adapters/claude-code.d.ts.map +1 -1
  3. package/dist/adapters/claude-code.js +63 -3
  4. package/dist/adapters/claude-code.js.map +1 -1
  5. package/dist/adapters/common.d.ts.map +1 -1
  6. package/dist/adapters/common.js +358 -306
  7. package/dist/adapters/common.js.map +1 -1
  8. package/dist/api/routes.d.ts.map +1 -1
  9. package/dist/api/routes.js +29 -7
  10. package/dist/api/routes.js.map +1 -1
  11. package/dist/coordination/routes.d.ts.map +1 -1
  12. package/dist/coordination/routes.js +174 -170
  13. package/dist/coordination/routes.js.map +1 -1
  14. package/dist/core/embeddings.d.ts.map +1 -1
  15. package/dist/core/embeddings.js +4 -1
  16. package/dist/core/embeddings.js.map +1 -1
  17. package/dist/core/entity-extract.d.ts +3 -0
  18. package/dist/core/entity-extract.d.ts.map +1 -0
  19. package/dist/core/entity-extract.js +47 -0
  20. package/dist/core/entity-extract.js.map +1 -0
  21. package/dist/core/format-recall.d.ts +16 -0
  22. package/dist/core/format-recall.d.ts.map +1 -0
  23. package/dist/core/format-recall.js +24 -0
  24. package/dist/core/format-recall.js.map +1 -0
  25. package/dist/core/query-expander.js +1 -1
  26. package/dist/core/query-expander.js.map +1 -1
  27. package/dist/core/reranker.js +1 -1
  28. package/dist/core/reranker.js.map +1 -1
  29. package/dist/core/salience.d.ts.map +1 -1
  30. package/dist/core/salience.js +14 -2
  31. package/dist/core/salience.js.map +1 -1
  32. package/dist/core/whoami.d.ts +24 -0
  33. package/dist/core/whoami.d.ts.map +1 -0
  34. package/dist/core/whoami.js +66 -0
  35. package/dist/core/whoami.js.map +1 -0
  36. package/dist/core/write-pipeline.d.ts +9 -0
  37. package/dist/core/write-pipeline.d.ts.map +1 -1
  38. package/dist/core/write-pipeline.js +109 -68
  39. package/dist/core/write-pipeline.js.map +1 -1
  40. package/dist/core/write-telemetry.d.ts +33 -0
  41. package/dist/core/write-telemetry.d.ts.map +1 -0
  42. package/dist/core/write-telemetry.js +110 -0
  43. package/dist/core/write-telemetry.js.map +1 -0
  44. package/dist/engine/activation.d.ts +22 -12
  45. package/dist/engine/activation.d.ts.map +1 -1
  46. package/dist/engine/activation.js +133 -17
  47. package/dist/engine/activation.js.map +1 -1
  48. package/dist/engine/consolidation-scheduler.d.ts +1 -1
  49. package/dist/engine/consolidation-scheduler.js +1 -1
  50. package/dist/engine/consolidation.d.ts +1 -0
  51. package/dist/engine/consolidation.d.ts.map +1 -1
  52. package/dist/engine/consolidation.js +18 -0
  53. package/dist/engine/consolidation.js.map +1 -1
  54. package/dist/engine/eval.d.ts.map +1 -1
  55. package/dist/engine/eval.js +5 -1
  56. package/dist/engine/eval.js.map +1 -1
  57. package/dist/hooks/sidecar.d.ts +26 -0
  58. package/dist/hooks/sidecar.d.ts.map +1 -1
  59. package/dist/hooks/sidecar.js +30 -0
  60. package/dist/hooks/sidecar.js.map +1 -1
  61. package/dist/index.js +20 -2
  62. package/dist/index.js.map +1 -1
  63. package/dist/mcp.d.ts +2 -1
  64. package/dist/mcp.d.ts.map +1 -1
  65. package/dist/mcp.js +222 -108
  66. package/dist/mcp.js.map +1 -1
  67. package/dist/recipes/index.d.ts +57 -0
  68. package/dist/recipes/index.d.ts.map +1 -0
  69. package/dist/recipes/index.js +81 -0
  70. package/dist/recipes/index.js.map +1 -0
  71. package/dist/storage/pglite-schema.d.ts.map +1 -1
  72. package/dist/storage/pglite-schema.js +27 -0
  73. package/dist/storage/pglite-schema.js.map +1 -1
  74. package/dist/storage/pglite.d.ts +5 -0
  75. package/dist/storage/pglite.d.ts.map +1 -1
  76. package/dist/storage/pglite.js +180 -138
  77. package/dist/storage/pglite.js.map +1 -1
  78. package/dist/storage/postgres.d.ts +5 -0
  79. package/dist/storage/postgres.d.ts.map +1 -1
  80. package/dist/storage/postgres.js +180 -138
  81. package/dist/storage/postgres.js.map +1 -1
  82. package/dist/storage/sqlite.d.ts +9 -0
  83. package/dist/storage/sqlite.d.ts.map +1 -1
  84. package/dist/storage/sqlite.js +394 -326
  85. package/dist/storage/sqlite.js.map +1 -1
  86. package/dist/types/engram.d.ts +14 -0
  87. package/dist/types/engram.d.ts.map +1 -1
  88. package/dist/types/engram.js.map +1 -1
  89. package/package.json +1 -1
  90. package/src/adapters/claude-code.ts +66 -3
  91. package/src/adapters/common.ts +567 -515
  92. package/src/api/routes.ts +999 -971
  93. package/src/coordination/routes.ts +2155 -2150
  94. package/src/core/embeddings.ts +4 -1
  95. package/src/core/entity-extract.ts +47 -0
  96. package/src/core/format-recall.ts +25 -0
  97. package/src/core/query-expander.ts +1 -1
  98. package/src/core/reranker.ts +1 -1
  99. package/src/core/salience.ts +529 -514
  100. package/src/core/whoami.ts +92 -0
  101. package/src/core/write-pipeline.ts +60 -8
  102. package/src/core/write-telemetry.ts +131 -0
  103. package/src/engine/activation.ts +1468 -1369
  104. package/src/engine/consolidation-scheduler.ts +1 -1
  105. package/src/engine/consolidation.ts +887 -869
  106. package/src/engine/eval.ts +6 -1
  107. package/src/hooks/sidecar.ts +55 -0
  108. package/src/index.ts +248 -227
  109. package/src/mcp.ts +1387 -1270
  110. package/src/recipes/index.ts +125 -0
  111. package/src/storage/pglite-schema.ts +27 -0
  112. package/src/storage/pglite.ts +1420 -1372
  113. package/src/storage/postgres.ts +1523 -1475
  114. package/src/storage/sqlite.ts +1936 -1861
  115. package/src/types/engram.ts +22 -0
@@ -0,0 +1,92 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Instance identity (D3, 2026-07-30).
6
+ *
7
+ * Answers "which AWM am I talking to?" — the recurring confusion class where
8
+ * sessions mix up the hosted multi-agent connector with the local instance,
9
+ * or run blind to sibling agent spaces in the same store (an evaluation once
10
+ * analyzed this project without seeing the `personal` space that held its
11
+ * design decisions).
12
+ *
13
+ * Exposed as the `memory_whoami` MCP tool and the `GET /whoami` HTTP route.
14
+ * Read-only; additive module.
15
+ */
16
+
17
+ import { dirname, join, resolve } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { VERSION } from '../version.js';
20
+ import { getConfiguredBackend, getConfiguredPath } from '../storage/factory.js';
21
+
22
+ export interface WhoamiInfo {
23
+ agentId: string;
24
+ workspace: string | null;
25
+ surface: 'mcp' | 'http';
26
+ mode: 'standalone' | 'hive';
27
+ backend: string;
28
+ storePath: string;
29
+ version: string;
30
+ codePath: string;
31
+ pid: number;
32
+ ports: { http: number | null; hookSidecar: number | null };
33
+ siblingAgents: string[];
34
+ }
35
+
36
+ /** Store subset whoami needs — every backend implements listAgentIds. */
37
+ export interface WhoamiStore {
38
+ listAgentIds(): string[] | Promise<string[]>;
39
+ }
40
+
41
+ function packageRoot(): string {
42
+ // src/core/whoami.ts (dev) and dist/core/whoami.js (build) both sit two
43
+ // levels below the package root.
44
+ const here = dirname(fileURLToPath(import.meta.url));
45
+ return resolve(join(here, '..', '..'));
46
+ }
47
+
48
+ export async function buildWhoami(
49
+ store: WhoamiStore,
50
+ agentId: string,
51
+ surface: 'mcp' | 'http',
52
+ ): Promise<WhoamiInfo> {
53
+ const coordination = process.env.AWM_COORDINATION === 'true' || process.env.AWM_COORDINATION === '1';
54
+ let siblings: string[] = [];
55
+ try {
56
+ const all = await store.listAgentIds();
57
+ siblings = all.filter(a => a !== agentId).sort();
58
+ } catch { /* identity must never fail on a listing error */ }
59
+ const httpPort = Number(process.env.AWM_PORT ?? '');
60
+ const hookPort = Number(process.env.AWM_HOOK_PORT ?? '');
61
+ return {
62
+ agentId,
63
+ workspace: process.env.AWM_WORKSPACE ?? null,
64
+ surface,
65
+ mode: coordination ? 'hive' : 'standalone',
66
+ backend: getConfiguredBackend(),
67
+ storePath: getConfiguredPath(),
68
+ version: VERSION,
69
+ codePath: packageRoot(),
70
+ pid: process.pid,
71
+ ports: {
72
+ http: Number.isFinite(httpPort) && httpPort > 0 ? httpPort : null,
73
+ hookSidecar: Number.isFinite(hookPort) && hookPort > 0 ? hookPort : null,
74
+ },
75
+ siblingAgents: siblings,
76
+ };
77
+ }
78
+
79
+ /** Human-readable rendering for the MCP tool response. */
80
+ export function formatWhoami(w: WhoamiInfo): string {
81
+ return [
82
+ `Agent: ${w.agentId}${w.workspace ? ` (workspace: ${w.workspace})` : ''}`,
83
+ `Mode: ${w.mode} · Surface: ${w.surface} · Version: ${w.version}`,
84
+ `Backend: ${w.backend}`,
85
+ `Store: ${w.storePath}`,
86
+ `Code: ${w.codePath} (pid ${w.pid})`,
87
+ `Ports: http=${w.ports.http ?? 'off'} hookSidecar=${w.ports.hookSidecar ?? 'off'}`,
88
+ w.siblingAgents.length
89
+ ? `Sibling agent spaces in this store: ${w.siblingAgents.join(', ')} — recall is scoped to '${w.agentId}'; other spaces need workspace recall, \`awm export --agent <id>\`, or a session configured for that agent.`
90
+ : 'Sibling agent spaces in this store: none',
91
+ ].join('\n');
92
+ }
@@ -48,6 +48,8 @@ import {
48
48
  } from './salience.js';
49
49
  import { embed } from './embeddings.js';
50
50
  import { extractMetaTags } from './auto-tagger.js';
51
+ import { extractEntitiesFromTags } from './entity-extract.js';
52
+ import { reportWrite } from './write-telemetry.js';
51
53
  import { DEFAULT_AGENT_CONFIG } from '../types/agent.js';
52
54
 
53
55
  /** Confidence floor below which a matched engram is treated as "decaying out". */
@@ -98,6 +100,15 @@ export interface WriteInput {
98
100
  * because it's deterministically retrieved; opt in if a caller wants
99
101
  * cognitive recall over a structural engram. (0.8 Cluster A) */
100
102
  embed?: boolean;
103
+ /** Provenance (D5, log-only): 'user-stated' | 'tool-output' | 'inference' | 'recipe'. */
104
+ originClass?: string;
105
+ /** Provenance (D5): session/conversation id performing the write. */
106
+ writerSession?: string;
107
+ /** Provenance (D5): cognition-recipe id+version when originClass is 'recipe'. */
108
+ recipeId?: string;
109
+ /** Temporal validity (D8): ISO bounds on when the FACT holds. */
110
+ validFrom?: string;
111
+ validTo?: string;
101
112
  }
102
113
 
103
114
  export interface WriteResult {
@@ -163,7 +174,10 @@ export async function performWrite(
163
174
  // Profiling: AWM_PROFILE_WRITE=1 logs ms-per-phase to stderr (v0.8.2).
164
175
  // Off by default; zero cost when unset.
165
176
  const profile = process.env.AWM_PROFILE_WRITE === '1';
166
- const startTotal = profile ? performance.now() : 0;
177
+ // Timings are collected unconditionally since D1 (2026-07-30): a handful of
178
+ // performance.now() calls are free, and the always-on slow-write telemetry
179
+ // (write-telemetry.ts) needs them to explain stalls in production.
180
+ const startTotal = performance.now();
167
181
  let tNovelty = 0, tCreate = 0, tEmbed = 0;
168
182
 
169
183
  // Pre-embed once (v0.8.5): the embedding is needed for cosine-based novelty
@@ -177,22 +191,22 @@ export async function performWrite(
177
191
  // hook below will retry).
178
192
  // Disable via AWM_NOVELTY_EMBED=0 to keep writes ultra-fast at the cost
179
193
  // of cross-backend novelty consistency.
180
- const tStartEmbed = profile ? performance.now() : 0;
194
+ const tStartEmbed = performance.now();
181
195
  let prewriteEmbedding: number[] | null = null;
182
196
  if (process.env.AWM_NOVELTY_EMBED !== '0') {
183
197
  try {
184
198
  prewriteEmbedding = await embed(`${input.concept} ${input.content}`);
185
199
  } catch { /* fall through — async embed will retry later */ }
186
200
  }
187
- if (profile) tEmbed = performance.now() - tStartEmbed;
201
+ tEmbed = performance.now() - tStartEmbed;
188
202
 
189
- const tStartNovelty = profile ? performance.now() : 0;
203
+ const tStartNovelty = performance.now();
190
204
  const noveltyResult = await computeNoveltyWithMatch(
191
205
  store, input.agentId, input.concept, input.content,
192
206
  input.workspace ?? null,
193
207
  prewriteEmbedding,
194
208
  );
195
- if (profile) tNovelty = performance.now() - tStartNovelty;
209
+ tNovelty = performance.now() - tStartNovelty;
196
210
 
197
211
  // Effective event type — auto-promote user-feedback content.
198
212
  const effectiveEventType: SalienceEventType =
@@ -216,8 +230,10 @@ export async function performWrite(
216
230
  });
217
231
 
218
232
  // -- Reinforce / Supersede branching --
219
- const tStartCreate = profile ? performance.now() : 0;
233
+ const tStartCreate = performance.now();
234
+ let busyHit = false;
220
235
  let result: WriteResult | null = null;
236
+ try {
221
237
  if (enableReinforcement && noveltyResult.matchedEngramId) {
222
238
  const matched = await store.getEngram(noveltyResult.matchedEngramId);
223
239
  // Agent-scope guard: NEVER reinforce/supersede an engram that belongs to a DIFFERENT agent. A
@@ -285,10 +301,34 @@ export async function performWrite(
285
301
  supersedesId: input.supersedes,
286
302
  }, prewriteEmbedding);
287
303
  }
288
- if (profile) tCreate = performance.now() - tStartCreate;
304
+ } catch (e) {
305
+ const es = `${(e as { code?: unknown })?.code ?? ''} ${(e as Error)?.message ?? ''}`;
306
+ busyHit = /busy/i.test(es);
307
+ reportWrite({
308
+ totalMs: performance.now() - startTotal,
309
+ embedMs: tEmbed,
310
+ noveltyMs: tNovelty,
311
+ persistMs: performance.now() - tStartCreate,
312
+ action: 'error',
313
+ agentId: input.agentId,
314
+ busyHit,
315
+ });
316
+ throw e;
317
+ }
318
+ tCreate = performance.now() - tStartCreate;
319
+
320
+ const total = performance.now() - startTotal;
321
+ reportWrite({
322
+ totalMs: total,
323
+ embedMs: tEmbed,
324
+ noveltyMs: tNovelty,
325
+ persistMs: tCreate,
326
+ action: result.action,
327
+ agentId: input.agentId,
328
+ busyHit,
329
+ });
289
330
 
290
331
  if (profile) {
291
- const total = performance.now() - startTotal;
292
332
  // Single-line stderr log; cheap to grep, easy to disable.
293
333
  // Format: [write] action=create novelty=42.1ms create=18.3ms total=60.4ms agent=x id=y
294
334
  // eslint-disable-next-line no-console
@@ -478,6 +518,11 @@ async function createNewEngram(
478
518
  supersedes: meta.supersedesId,
479
519
  sequence: input.sequence,
480
520
  references: input.references,
521
+ originClass: input.originClass,
522
+ writerSession: input.writerSession,
523
+ recipeId: input.recipeId,
524
+ validFrom: input.validFrom,
525
+ validTo: input.validTo,
481
526
  embedding: prewriteEmbedding && prewriteEmbedding.length > 0
482
527
  ? prewriteEmbedding
483
528
  : undefined,
@@ -487,6 +532,13 @@ async function createNewEngram(
487
532
  await store.updateStage(engram.id, 'staging');
488
533
  }
489
534
 
535
+ // Entity inverted index (D9): index structured entities from tags at write
536
+ // time. Best-effort — retrieval does not depend on it yet (D11).
537
+ try {
538
+ const entities = extractEntitiesFromTags(tags);
539
+ if (entities.length > 0) await store.recordEntityMentions(engram.id, input.agentId, entities);
540
+ } catch { /* non-fatal */ }
541
+
490
542
  // Supersession side-effects: mark the old engram, add causal edge.
491
543
  if (meta.supersedesId) {
492
544
  try {
@@ -0,0 +1,131 @@
1
+ // Copyright 2026 Robert Winter / Complete Ideas
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Write-path telemetry (D1, 2026-07-30).
6
+ *
7
+ * Purpose: prove the mechanism behind slow writes before building the
8
+ * serialized write service. Captures, per write:
9
+ * - phase wall times (embed / novelty / persist)
10
+ * - event-loop lag at write completion (was this process's loop blocked?)
11
+ * - whether a consolidation cycle was running IN THIS PROCESS
12
+ * - whether SQLITE_BUSY was hit
13
+ * - embed-model cold-load duration (first inference)
14
+ *
15
+ * Always-on slow-write warning: any write slower than AWM_SLOW_WRITE_MS
16
+ * (default 250ms; set 0 to disable) emits ONE structured stderr line.
17
+ * AWM_PROFILE_WRITE=1 continues to log every write as before.
18
+ *
19
+ * Design notes: additive module — no existing export changes (MWA vendors
20
+ * dist internals; nothing here alters existing paths). All timers are
21
+ * unref'd so they never hold the process open.
22
+ */
23
+
24
+ const SLOW_WRITE_MS = (() => {
25
+ const v = Number(process.env.AWM_SLOW_WRITE_MS ?? '250');
26
+ return Number.isFinite(v) ? v : 250;
27
+ })();
28
+
29
+ // ---------- Event-loop lag monitor ----------
30
+
31
+ const TICK_MS = 500;
32
+ let lastTick = 0;
33
+ let maxLagSinceRead = 0;
34
+ let monitorStarted = false;
35
+
36
+ /** Start the loop-lag heartbeat. Idempotent; call once at process boot. */
37
+ export function startLoopLagMonitor(): void {
38
+ if (monitorStarted) return;
39
+ monitorStarted = true;
40
+ lastTick = performance.now();
41
+ const t = setInterval(() => {
42
+ const now = performance.now();
43
+ const lag = now - lastTick - TICK_MS;
44
+ if (lag > maxLagSinceRead) maxLagSinceRead = lag;
45
+ lastTick = now;
46
+ }, TICK_MS);
47
+ if (typeof t.unref === 'function') t.unref();
48
+ }
49
+
50
+ /**
51
+ * Max observed loop lag (ms beyond the expected tick) since the last read.
52
+ * Reading resets the max. A large value means this process's event loop was
53
+ * blocked (in-process ML, consolidation, sync SQL) during the window.
54
+ */
55
+ export function readMaxLoopLag(): number {
56
+ const v = maxLagSinceRead;
57
+ maxLagSinceRead = 0;
58
+ return Math.max(0, Math.round(v));
59
+ }
60
+
61
+ // ---------- In-process consolidation state ----------
62
+
63
+ let consolidationActive = false;
64
+ let consolidationAgent: string | null = null;
65
+ let consolidationStartedAt = 0;
66
+
67
+ export function setConsolidationActive(active: boolean, agentId?: string): void {
68
+ consolidationActive = active;
69
+ consolidationAgent = active ? (agentId ?? null) : null;
70
+ consolidationStartedAt = active ? performance.now() : 0;
71
+ }
72
+
73
+ export function getConsolidationState(): { active: boolean; agentId: string | null; runningMs: number } {
74
+ return {
75
+ active: consolidationActive,
76
+ agentId: consolidationAgent,
77
+ runningMs: consolidationActive ? Math.round(performance.now() - consolidationStartedAt) : 0,
78
+ };
79
+ }
80
+
81
+ // ---------- Model cold-load record ----------
82
+
83
+ let modelColdLoadMs: number | null = null;
84
+
85
+ export function noteModelLoad(ms: number): void {
86
+ if (modelColdLoadMs === null) {
87
+ modelColdLoadMs = Math.round(ms);
88
+ process.stderr.write(`[awm] embed model cold load: ${modelColdLoadMs}ms\n`);
89
+ }
90
+ }
91
+
92
+ export function getModelColdLoadMs(): number | null {
93
+ return modelColdLoadMs;
94
+ }
95
+
96
+ // ---------- Slow-write reporter ----------
97
+
98
+ export interface WriteTimings {
99
+ totalMs: number;
100
+ embedMs: number;
101
+ noveltyMs: number;
102
+ persistMs: number;
103
+ action: string; // created | reinforced | superseded | discarded | ...
104
+ agentId: string;
105
+ busyHit: boolean; // a SQLITE_BUSY was caught (and rethrown) during persist
106
+ }
107
+
108
+ /**
109
+ * Report a completed write. Emits one structured stderr line when the write
110
+ * exceeded the slow threshold (always-on) — including loop lag and in-process
111
+ * consolidation state, the two signals that distinguish "DB lock wait" from
112
+ * "my own event loop was blocked".
113
+ */
114
+ export function reportWrite(t: WriteTimings): void {
115
+ if (SLOW_WRITE_MS <= 0) return;
116
+ if (t.totalMs < SLOW_WRITE_MS) return;
117
+ const consol = getConsolidationState();
118
+ const parts = [
119
+ `[awm] SLOW WRITE ${Math.round(t.totalMs)}ms`,
120
+ `agent=${t.agentId}`,
121
+ `action=${t.action}`,
122
+ `embed=${Math.round(t.embedMs)}ms`,
123
+ `novelty=${Math.round(t.noveltyMs)}ms`,
124
+ `persist=${Math.round(t.persistMs)}ms`,
125
+ `loopLagMax=${readMaxLoopLag()}ms`,
126
+ `consolidating=${consol.active ? `yes(${consol.agentId ?? '?'},${consol.runningMs}ms)` : 'no'}`,
127
+ `busy=${t.busyHit ? 'yes' : 'no'}`,
128
+ modelColdLoadMs !== null ? `modelColdLoad=${modelColdLoadMs}ms` : 'modelColdLoad=none',
129
+ ];
130
+ process.stderr.write(parts.join(' ') + '\n');
131
+ }