agent-working-memory 0.17.0 → 0.18.1

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 (165) hide show
  1. package/README.md +321 -319
  2. package/dist/adapters/common.d.ts +34 -0
  3. package/dist/adapters/common.d.ts.map +1 -1
  4. package/dist/adapters/common.js +528 -446
  5. package/dist/adapters/common.js.map +1 -1
  6. package/dist/api/routes.d.ts.map +1 -1
  7. package/dist/api/routes.js +19 -0
  8. package/dist/api/routes.js.map +1 -1
  9. package/dist/cli/migrate.d.ts.map +1 -1
  10. package/dist/cli/migrate.js +31 -29
  11. package/dist/cli/migrate.js.map +1 -1
  12. package/dist/core/asked-value.d.ts +37 -0
  13. package/dist/core/asked-value.d.ts.map +1 -0
  14. package/dist/core/asked-value.js +77 -0
  15. package/dist/core/asked-value.js.map +1 -0
  16. package/dist/core/conflicts.d.ts +89 -0
  17. package/dist/core/conflicts.d.ts.map +1 -0
  18. package/dist/core/conflicts.js +267 -0
  19. package/dist/core/conflicts.js.map +1 -0
  20. package/dist/core/embeddings.d.ts +14 -2
  21. package/dist/core/embeddings.d.ts.map +1 -1
  22. package/dist/core/embeddings.js +32 -10
  23. package/dist/core/embeddings.js.map +1 -1
  24. package/dist/core/format-recall.d.ts +21 -1
  25. package/dist/core/format-recall.d.ts.map +1 -1
  26. package/dist/core/format-recall.js +60 -5
  27. package/dist/core/format-recall.js.map +1 -1
  28. package/dist/core/ml-worker.d.ts +24 -1
  29. package/dist/core/ml-worker.d.ts.map +1 -1
  30. package/dist/core/ml-worker.js +165 -4
  31. package/dist/core/ml-worker.js.map +1 -1
  32. package/dist/core/model-daemon-client.d.ts +77 -0
  33. package/dist/core/model-daemon-client.d.ts.map +1 -0
  34. package/dist/core/model-daemon-client.js +383 -0
  35. package/dist/core/model-daemon-client.js.map +1 -0
  36. package/dist/core/model-daemon-server.d.ts +71 -0
  37. package/dist/core/model-daemon-server.d.ts.map +1 -0
  38. package/dist/core/model-daemon-server.js +363 -0
  39. package/dist/core/model-daemon-server.js.map +1 -0
  40. package/dist/core/model-daemon.d.ts +137 -0
  41. package/dist/core/model-daemon.d.ts.map +1 -0
  42. package/dist/core/model-daemon.js +231 -0
  43. package/dist/core/model-daemon.js.map +1 -0
  44. package/dist/core/query-expander.d.ts +5 -0
  45. package/dist/core/query-expander.d.ts.map +1 -1
  46. package/dist/core/query-expander.js +3 -2
  47. package/dist/core/query-expander.js.map +1 -1
  48. package/dist/core/recall-config.d.ts +1 -1
  49. package/dist/core/recall-config.d.ts.map +1 -1
  50. package/dist/core/recall-config.js +5 -0
  51. package/dist/core/recall-config.js.map +1 -1
  52. package/dist/core/rerank-window.d.ts +7 -2
  53. package/dist/core/rerank-window.d.ts.map +1 -1
  54. package/dist/core/rerank-window.js +9 -4
  55. package/dist/core/rerank-window.js.map +1 -1
  56. package/dist/core/rerank2.d.ts +52 -2
  57. package/dist/core/rerank2.d.ts.map +1 -1
  58. package/dist/core/rerank2.js +60 -3
  59. package/dist/core/rerank2.js.map +1 -1
  60. package/dist/core/reranker.d.ts +19 -0
  61. package/dist/core/reranker.d.ts.map +1 -1
  62. package/dist/core/reranker.js +76 -12
  63. package/dist/core/reranker.js.map +1 -1
  64. package/dist/core/trust.d.ts +32 -0
  65. package/dist/core/trust.d.ts.map +1 -0
  66. package/dist/core/trust.js +66 -0
  67. package/dist/core/trust.js.map +1 -0
  68. package/dist/core/validity.d.ts +21 -0
  69. package/dist/core/validity.d.ts.map +1 -0
  70. package/dist/core/validity.js +41 -0
  71. package/dist/core/validity.js.map +1 -0
  72. package/dist/core/whoami.d.ts +3 -0
  73. package/dist/core/whoami.d.ts.map +1 -1
  74. package/dist/core/whoami.js +14 -0
  75. package/dist/core/whoami.js.map +1 -1
  76. package/dist/core/write-pipeline.d.ts +48 -0
  77. package/dist/core/write-pipeline.d.ts.map +1 -1
  78. package/dist/core/write-pipeline.js +80 -3
  79. package/dist/core/write-pipeline.js.map +1 -1
  80. package/dist/engine/activation.d.ts.map +1 -1
  81. package/dist/engine/activation.js +27 -4
  82. package/dist/engine/activation.js.map +1 -1
  83. package/dist/engine/consolidation.d.ts.map +1 -1
  84. package/dist/engine/consolidation.js +53 -4
  85. package/dist/engine/consolidation.js.map +1 -1
  86. package/dist/hooks/prime-query.d.ts +57 -0
  87. package/dist/hooks/prime-query.d.ts.map +1 -0
  88. package/dist/hooks/prime-query.js +388 -0
  89. package/dist/hooks/prime-query.js.map +1 -0
  90. package/dist/hooks/prime.d.ts +8 -0
  91. package/dist/hooks/prime.d.ts.map +1 -1
  92. package/dist/hooks/prime.js +21 -2
  93. package/dist/hooks/prime.js.map +1 -1
  94. package/dist/hooks/sidecar.d.ts +7 -0
  95. package/dist/hooks/sidecar.d.ts.map +1 -1
  96. package/dist/hooks/sidecar.js +10 -2
  97. package/dist/hooks/sidecar.js.map +1 -1
  98. package/dist/index.js +11 -3
  99. package/dist/index.js.map +1 -1
  100. package/dist/mcp.js +148 -103
  101. package/dist/mcp.js.map +1 -1
  102. package/dist/model-daemon.d.ts +8 -0
  103. package/dist/model-daemon.d.ts.map +1 -0
  104. package/dist/model-daemon.js +45 -0
  105. package/dist/model-daemon.js.map +1 -0
  106. package/dist/storage/factory.d.ts.map +1 -1
  107. package/dist/storage/factory.js +5 -0
  108. package/dist/storage/factory.js.map +1 -1
  109. package/dist/storage/pglite-schema.d.ts.map +1 -1
  110. package/dist/storage/pglite-schema.js +176 -170
  111. package/dist/storage/pglite-schema.js.map +1 -1
  112. package/dist/storage/pglite.d.ts +5 -0
  113. package/dist/storage/pglite.d.ts.map +1 -1
  114. package/dist/storage/pglite.js +165 -153
  115. package/dist/storage/pglite.js.map +1 -1
  116. package/dist/storage/postgres.d.ts +5 -0
  117. package/dist/storage/postgres.d.ts.map +1 -1
  118. package/dist/storage/postgres.js +165 -153
  119. package/dist/storage/postgres.js.map +1 -1
  120. package/dist/storage/sqlite.d.ts +22 -0
  121. package/dist/storage/sqlite.d.ts.map +1 -1
  122. package/dist/storage/sqlite.js +400 -363
  123. package/dist/storage/sqlite.js.map +1 -1
  124. package/dist/types/engram.d.ts +33 -0
  125. package/dist/types/engram.d.ts.map +1 -1
  126. package/dist/types/engram.js.map +1 -1
  127. package/package.json +1 -1
  128. package/plugin/.claude-plugin/plugin.json +1 -1
  129. package/plugin/README.md +1 -1
  130. package/plugin/bin/awm-mcp-launcher.cjs +2 -2
  131. package/plugin/skills/awm-memory/SKILL.md +11 -2
  132. package/src/adapters/common.ts +881 -802
  133. package/src/api/routes.ts +20 -0
  134. package/src/cli/migrate.ts +309 -307
  135. package/src/core/asked-value.ts +74 -0
  136. package/src/core/conflicts.ts +280 -0
  137. package/src/core/embeddings.ts +35 -10
  138. package/src/core/format-recall.ts +83 -25
  139. package/src/core/ml-worker.ts +458 -281
  140. package/src/core/model-daemon-client.ts +374 -0
  141. package/src/core/model-daemon-server.ts +344 -0
  142. package/src/core/model-daemon.ts +243 -0
  143. package/src/core/query-expander.ts +127 -126
  144. package/src/core/recall-config.ts +5 -0
  145. package/src/core/rerank-window.ts +164 -159
  146. package/src/core/rerank2.ts +143 -82
  147. package/src/core/reranker.ts +78 -11
  148. package/src/core/trust.ts +65 -0
  149. package/src/core/validity.ts +47 -0
  150. package/src/core/whoami.ts +136 -120
  151. package/src/core/write-pipeline.ts +119 -3
  152. package/src/engine/activation.ts +27 -4
  153. package/src/engine/consolidation.ts +950 -899
  154. package/src/hooks/prime-query.ts +375 -0
  155. package/src/hooks/prime.ts +25 -2
  156. package/src/hooks/sidecar.ts +13 -3
  157. package/src/index.ts +255 -248
  158. package/src/mcp.ts +1521 -1475
  159. package/src/model-daemon.ts +48 -0
  160. package/src/storage/factory.ts +25 -19
  161. package/src/storage/pglite-schema.ts +199 -193
  162. package/src/storage/pglite.ts +1513 -1498
  163. package/src/storage/postgres.ts +1613 -1598
  164. package/src/storage/sqlite.ts +2056 -2019
  165. package/src/types/engram.ts +30 -0
@@ -1,120 +1,136 @@
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
- import { activeRecallConfig, recallConfigFingerprint } from './recall-config.js';
22
- import { embeddingHealth } from './embeddings.js';
23
-
24
- export interface WhoamiInfo {
25
- agentId: string;
26
- workspace: string | null;
27
- surface: 'mcp' | 'http';
28
- mode: 'standalone' | 'hive';
29
- backend: string;
30
- storePath: string;
31
- version: string;
32
- codePath: string;
33
- pid: number;
34
- ports: { http: number | null; hookSidecar: number | null };
35
- /**
36
- * Effective recall configuration. `whoami` is the "what am I actually
37
- * running" tool, and the version alone does not answer that: a build can be
38
- * current while the recall flags that change its behavior are unset, or set
39
- * to something unintended. GET /health already reports this, but HTTP is off
40
- * by default, so an MCP-only session had no way to see it.
41
- */
42
- recall: { fingerprint: string; flags: Record<string, string> };
43
- siblingAgents: string[];
44
- }
45
-
46
- /** Store subset whoami needs — every backend implements listAgentIds. */
47
- export interface WhoamiStore {
48
- listAgentIds(): string[] | Promise<string[]>;
49
- }
50
-
51
- function packageRoot(): string {
52
- // src/core/whoami.ts (dev) and dist/core/whoami.js (build) both sit two
53
- // levels below the package root.
54
- const here = dirname(fileURLToPath(import.meta.url));
55
- return resolve(join(here, '..', '..'));
56
- }
57
-
58
- export async function buildWhoami(
59
- store: WhoamiStore,
60
- agentId: string,
61
- surface: 'mcp' | 'http',
62
- /**
63
- * 0.14.2: the port the hook sidecar actually BOUND, from the running handle.
64
- * Before this, whoami echoed AWM_HOOK_PORT from the environment — so a session
65
- * whose sidecar had lost the port fight and disabled hooks still reported
66
- * "hookSidecar=8401". Pass `null` for "tried and failed"; omit (undefined)
67
- * for surfaces that have no sidecar, which falls back to the env value.
68
- */
69
- boundHookPort?: number | null,
70
- ): Promise<WhoamiInfo> {
71
- const coordination = process.env.AWM_COORDINATION === 'true' || process.env.AWM_COORDINATION === '1';
72
- let siblings: string[] = [];
73
- try {
74
- const all = await store.listAgentIds();
75
- siblings = all.filter(a => a !== agentId).sort();
76
- } catch { /* identity must never fail on a listing error */ }
77
- const httpPort = Number(process.env.AWM_PORT ?? '');
78
- const hookPort = Number(process.env.AWM_HOOK_PORT ?? '');
79
- return {
80
- agentId,
81
- workspace: process.env.AWM_WORKSPACE ?? null,
82
- surface,
83
- mode: coordination ? 'hive' : 'standalone',
84
- backend: getConfiguredBackend(),
85
- storePath: getConfiguredPath(),
86
- version: VERSION,
87
- codePath: packageRoot(),
88
- pid: process.pid,
89
- ports: {
90
- http: Number.isFinite(httpPort) && httpPort > 0 ? httpPort : null,
91
- hookSidecar: boundHookPort !== undefined
92
- ? boundHookPort
93
- : (Number.isFinite(hookPort) && hookPort > 0 ? hookPort : null),
94
- },
95
- recall: { fingerprint: recallConfigFingerprint(), flags: activeRecallConfig() },
96
- siblingAgents: siblings,
97
- };
98
- }
99
-
100
- /** Human-readable rendering for the MCP tool response. */
101
- export function formatWhoami(w: WhoamiInfo): string {
102
- return [
103
- `Agent: ${w.agentId}${w.workspace ? ` (workspace: ${w.workspace})` : ''}`,
104
- `Mode: ${w.mode} · Surface: ${w.surface} · Version: ${w.version}`,
105
- `Backend: ${w.backend}`,
106
- `Store: ${w.storePath}`,
107
- `Code: ${w.codePath} (pid ${w.pid})`,
108
- `Ports: http=${w.ports.http ?? 'off'} hookSidecar=${w.ports.hookSidecar ?? 'off'}`,
109
- `Recall config: ${w.recall.fingerprint}`,
110
- ...(embeddingHealth().dimensionMismatches > 0
111
- ? [`\u26a0 EMBEDDING INTEGRITY: ${embeddingHealth().dimensionMismatches} dimension mismatches ` +
112
- `(expected ${embeddingHealth().expectedDimensions}d). Affected memories score 0 on the ` +
113
- `vector channel — the corpus is probably half-migrated. Re-embed the whole store or ` +
114
- `revert AWM_EMBED_MODEL/AWM_EMBED_DIMS.`]
115
- : []),
116
- w.siblingAgents.length
117
- ? `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.`
118
- : 'Sibling agent spaces in this store: none',
119
- ].join('\n');
120
- }
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
+ import { activeRecallConfig, recallConfigFingerprint } from './recall-config.js';
22
+ import { embeddingHealth } from './embeddings.js';
23
+ import { modelDaemonStatus } from './ml-worker.js';
24
+
25
+ export interface WhoamiInfo {
26
+ agentId: string;
27
+ workspace: string | null;
28
+ surface: 'mcp' | 'http';
29
+ mode: 'standalone' | 'hive';
30
+ backend: string;
31
+ storePath: string;
32
+ version: string;
33
+ codePath: string;
34
+ pid: number;
35
+ ports: { http: number | null; hookSidecar: number | null };
36
+ /**
37
+ * Effective recall configuration. `whoami` is the "what am I actually
38
+ * running" tool, and the version alone does not answer that: a build can be
39
+ * current while the recall flags that change its behavior are unset, or set
40
+ * to something unintended. GET /health already reports this, but HTTP is off
41
+ * by default, so an MCP-only session had no way to see it.
42
+ */
43
+ recall: { fingerprint: string; flags: Record<string, string> };
44
+ /** Where inference runs: this process, or the shared model daemon (AWM_MODEL_DAEMON=1). */
45
+ models: ReturnType<typeof modelDaemonStatus>;
46
+ siblingAgents: string[];
47
+ }
48
+
49
+ /** Store subset whoami needs — every backend implements listAgentIds. */
50
+ export interface WhoamiStore {
51
+ listAgentIds(): string[] | Promise<string[]>;
52
+ }
53
+
54
+ function packageRoot(): string {
55
+ // src/core/whoami.ts (dev) and dist/core/whoami.js (build) both sit two
56
+ // levels below the package root.
57
+ const here = dirname(fileURLToPath(import.meta.url));
58
+ return resolve(join(here, '..', '..'));
59
+ }
60
+
61
+ export async function buildWhoami(
62
+ store: WhoamiStore,
63
+ agentId: string,
64
+ surface: 'mcp' | 'http',
65
+ /**
66
+ * 0.14.2: the port the hook sidecar actually BOUND, from the running handle.
67
+ * Before this, whoami echoed AWM_HOOK_PORT from the environment — so a session
68
+ * whose sidecar had lost the port fight and disabled hooks still reported
69
+ * "hookSidecar=8401". Pass `null` for "tried and failed"; omit (undefined)
70
+ * for surfaces that have no sidecar, which falls back to the env value.
71
+ */
72
+ boundHookPort?: number | null,
73
+ ): Promise<WhoamiInfo> {
74
+ const coordination = process.env.AWM_COORDINATION === 'true' || process.env.AWM_COORDINATION === '1';
75
+ let siblings: string[] = [];
76
+ try {
77
+ const all = await store.listAgentIds();
78
+ siblings = all.filter(a => a !== agentId).sort();
79
+ } catch { /* identity must never fail on a listing error */ }
80
+ const httpPort = Number(process.env.AWM_PORT ?? '');
81
+ const hookPort = Number(process.env.AWM_HOOK_PORT ?? '');
82
+ return {
83
+ agentId,
84
+ workspace: process.env.AWM_WORKSPACE ?? null,
85
+ surface,
86
+ mode: coordination ? 'hive' : 'standalone',
87
+ backend: getConfiguredBackend(),
88
+ storePath: getConfiguredPath(),
89
+ version: VERSION,
90
+ codePath: packageRoot(),
91
+ pid: process.pid,
92
+ ports: {
93
+ http: Number.isFinite(httpPort) && httpPort > 0 ? httpPort : null,
94
+ hookSidecar: boundHookPort !== undefined
95
+ ? boundHookPort
96
+ : (Number.isFinite(hookPort) && hookPort > 0 ? hookPort : null),
97
+ },
98
+ recall: { fingerprint: recallConfigFingerprint(), flags: activeRecallConfig() },
99
+ models: modelDaemonStatus(),
100
+ siblingAgents: siblings,
101
+ };
102
+ }
103
+
104
+ function formatModels(m: WhoamiInfo['models']): string {
105
+ if (!m.enabled) return 'in this process';
106
+ const fallback = m.fallbacks
107
+ ? ` · ${m.fallbacks} fallback(s) to in-process, last: ${m.lastFallbackReason} at ${m.lastFallbackAt}`
108
+ : '';
109
+ const where = m.mode === 'daemon' ? `shared daemon pid ${m.daemonPid}`
110
+ : m.mode === 'in-process-fallback' ? 'in this process (daemon unavailable)'
111
+ : 'shared daemon (not connected yet)';
112
+ return `${where} · fingerprint ${m.fingerprint} · ${m.requests} request(s)${fallback}`;
113
+ }
114
+
115
+ /** Human-readable rendering for the MCP tool response. */
116
+ export function formatWhoami(w: WhoamiInfo): string {
117
+ return [
118
+ `Agent: ${w.agentId}${w.workspace ? ` (workspace: ${w.workspace})` : ''}`,
119
+ `Mode: ${w.mode} · Surface: ${w.surface} · Version: ${w.version}`,
120
+ `Backend: ${w.backend}`,
121
+ `Store: ${w.storePath}`,
122
+ `Code: ${w.codePath} (pid ${w.pid})`,
123
+ `Ports: http=${w.ports.http ?? 'off'} hookSidecar=${w.ports.hookSidecar ?? 'off'}`,
124
+ `Recall config: ${w.recall.fingerprint}`,
125
+ `Models: ${formatModels(w.models)}`,
126
+ ...(embeddingHealth().dimensionMismatches > 0
127
+ ? [`\u26a0 EMBEDDING INTEGRITY: ${embeddingHealth().dimensionMismatches} dimension mismatches ` +
128
+ `(expected ${embeddingHealth().expectedDimensions}d). Affected memories score 0 on the ` +
129
+ `vector channel — the corpus is probably half-migrated. Re-embed the whole store or ` +
130
+ `revert AWM_EMBED_MODEL/AWM_EMBED_DIMS.`]
131
+ : []),
132
+ w.siblingAgents.length
133
+ ? `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.`
134
+ : 'Sibling agent spaces in this store: none',
135
+ ].join('\n');
136
+ }
@@ -38,6 +38,7 @@
38
38
  import type { IEngramStore as EngramStore } from '../storage/store.js';
39
39
  import type { ConnectionEngine } from '../engine/connections.js';
40
40
  import type { Engram, MemoryClass, MemoryType } from '../types/engram.js';
41
+ import { mergeBlockedByTrust, trustTier } from './trust.js';
41
42
  import {
42
43
  evaluateSalience,
43
44
  computeNoveltyWithMatch,
@@ -56,6 +57,15 @@ import { buildRetrievalText } from './retrieval-text.js';
56
57
  /** Confidence floor below which a matched engram is treated as "decaying out". */
57
58
  export const HEALTHY_CONFIDENCE_FLOOR = 0.3;
58
59
 
60
+ /**
61
+ * Lowest novelty match score at which `correction: true` may supersede the
62
+ * match without an id. matchScore is the stronger of BM25 and cosine
63
+ * similarity (0–1). A paraphrased correction of one fact scores well above
64
+ * this (the probe's flight correction: 0.95); two unrelated memories about
65
+ * the same subject do not.
66
+ */
67
+ const CORRECTION_MIN_MATCH = 0.6;
68
+
59
69
  /** Confidence delta applied on reinforcement. Bounded by REINFORCE_CONFIDENCE_CEIL. */
60
70
  export const REINFORCE_CONFIDENCE_DELTA = 0.05;
61
71
  export const REINFORCE_CONFIDENCE_CEIL = 0.95;
@@ -83,12 +93,41 @@ export interface WriteInput {
83
93
  eventType?: SalienceEventType;
84
94
  surprise?: number;
85
95
  decisionMade?: boolean;
96
+ /**
97
+ * Caller-declared purpose of the write: 'decision' | 'question' | 'todo' |
98
+ * 'finding' | 'context'. Stored as an `intent=` tag by the callers, and read HERE
99
+ * so a declared decision reaches the salience filter.
100
+ *
101
+ * It did not, before 2026-10-12: `intent` became a tag and nothing else, while the
102
+ * redundant `decisionMade` boolean was the only wired path, so a caller following
103
+ * the documented tagging discipline got no salience credit for saying this was a
104
+ * decision. Measured over all 496 discarded writes in the live store
105
+ * (`low-salience`, `work`+`personal`): those tagged `intent=decision` were later
106
+ * recalled at **77.3% against a 38.1% base rate**, +41pp — the strongest
107
+ * discriminator of any feature tested, discarded anyway.
108
+ *
109
+ * Mapped in the pipeline rather than at each call site so both write surfaces, and
110
+ * any future one, inherit it; widening the content-based operational-verb list was
111
+ * tested on the same population and rejected, because every candidate token scored
112
+ * at the base rate.
113
+ */
114
+ intent?: string;
86
115
  causalDepth?: number;
87
116
  resolutionEffort?: number;
88
117
  /** Confidence override. When unset, defaults from disposition + eventType priors. */
89
118
  confidence?: number;
90
119
  /** Explicit supersession requested by caller (independent of correction-on-match). */
91
120
  supersedes?: string;
121
+ /**
122
+ * This write corrects an existing memory whose id the caller does not have
123
+ * (2026-10-19). The closest active memory about the same thing — the same
124
+ * concept, else the novelty match when it is close enough — is superseded,
125
+ * the correction goes active at no less than that memory's confidence, and
126
+ * the result says what was replaced (`correctionOf`). Never reinforce-merges.
127
+ * With nothing close enough, the write is stored as new and `correctionOf`
128
+ * is null. An explicit `supersedes` outranks this.
129
+ */
130
+ correction?: boolean;
92
131
  /** Workspace scope for cross-agent novelty (v0.5.4+ stores only). */
93
132
  workspace?: string | null;
94
133
  /** Set to false to skip the reinforce/supersede branching and always create. */
@@ -132,6 +171,17 @@ export interface WriteResult {
132
171
  };
133
172
  /** Supersession detail — present only for action='supersede'. */
134
173
  supersedeOf?: { id: string };
174
+ /**
175
+ * Set when the write asked `correction: true`: the memory it replaced, or
176
+ * null when nothing close enough existed and the write was stored as new.
177
+ */
178
+ correctionOf?: { id: string; concept: string; matchScore: number } | null;
179
+ /**
180
+ * Set when a merge, supersede, revive or correction was refused because the
181
+ * write is less trusted than the memory it matched (src/core/trust.ts). The
182
+ * write was stored as its own engram instead.
183
+ */
184
+ trustBlocked?: { matchedId: string; writeTrust: number | null; matchedTrust: number | null };
135
185
  }
136
186
 
137
187
  export interface WritePipelineEngines {
@@ -178,7 +228,10 @@ export async function performWrite(
178
228
  // which supersedes `matched.id` rather than the id the caller actually asked for.
179
229
  const enableReinforcement = input.enableReinforcement !== false
180
230
  && process.env.AWM_WRITE_PIPELINE !== 'off'
181
- && !input.supersedes;
231
+ && !input.supersedes
232
+ // A correction must never reinforce-merge: merging is exactly how a changed
233
+ // value ended up inside the stale memory with a "--- reinforced ---" marker.
234
+ && !input.correction;
182
235
 
183
236
  // Profiling: AWM_PROFILE_WRITE=1 logs ms-per-phase to stderr (v0.8.2).
184
237
  // Off by default; zero cost when unset.
@@ -231,7 +284,7 @@ export async function performWrite(
231
284
  content: input.content,
232
285
  eventType: effectiveEventType,
233
286
  surprise: input.surprise,
234
- decisionMade: input.decisionMade,
287
+ decisionMade: input.decisionMade || input.intent === 'decision',
235
288
  causalDepth: input.causalDepth,
236
289
  resolutionEffort: input.resolutionEffort,
237
290
  novelty: noveltyResult.novelty,
@@ -242,6 +295,7 @@ export async function performWrite(
242
295
  const tStartCreate = performance.now();
243
296
  let busyHit = false;
244
297
  let result: WriteResult | null = null;
298
+ let trustBlocked: WriteResult['trustBlocked'];
245
299
  try {
246
300
  if (enableReinforcement && noveltyResult.matchedEngramId) {
247
301
  const matched = await store.getEngram(noveltyResult.matchedEngramId);
@@ -249,7 +303,12 @@ export async function performWrite(
249
303
  // workspace/hive recall can surface another agent's same-concept engram as the match; mutating it
250
304
  // (merge content, re-embed, bump confidence, or supersede) would corrupt that agent's memory. A
251
305
  // cross-agent match falls through to createNewEngram so this agent gets its own copy.
252
- if (matched && matched.agentId === input.agentId) {
306
+ // Trust guard (2026-10-19, src/core/trust.ts): a less-trusted write never merges into,
307
+ // supersedes or revives a more-trusted memory. It falls through to its own engram.
308
+ if (matched && matched.agentId === input.agentId && mergeBlockedByTrust(input.tags, matched.tags)) {
309
+ trustBlocked = { matchedId: matched.id, writeTrust: trustTier(input.tags), matchedTrust: trustTier(matched.tags) };
310
+ }
311
+ if (matched && matched.agentId === input.agentId && !trustBlocked) {
253
312
  const newConcept = (input.concept ?? '').toLowerCase().trim();
254
313
  const matchedConcept = (matched.concept ?? '').toLowerCase().trim();
255
314
  const sameConcept = newConcept === matchedConcept && newConcept.length > 0;
@@ -320,8 +379,59 @@ export async function performWrite(
320
379
  }
321
380
  }
322
381
 
382
+ // -- Correction by intent, not by id (2026-10-19) --
383
+ // The MWA evaluation probe wrote "now departs 8:40" as an ordinary observation
384
+ // under a new concept: novelty 0.10, so it was stored at confidence 0.25 and
385
+ // ranked BELOW the stale "10:15". The writer knew it was a correction; it did
386
+ // not know the old id, and the only in-band way to say so (eventType surprise
387
+ // on the SAME concept, R3 above) is undocumented and needs the exact concept.
388
+ // `correction: true` names the intent. The target is the novelty match — the
389
+ // closest memory in content — when it is close enough to be the same fact;
390
+ // a superseded match is followed to its superseder. Below the floor nothing
391
+ // is replaced and the caller is told, because superseding a stranger would
392
+ // retire a true fact.
393
+ let correctionOf: WriteResult['correctionOf'];
394
+ if (!result && input.correction && !input.supersedes) {
395
+ correctionOf = null;
396
+ let target: Engram | null = noveltyResult.matchedEngramId
397
+ ? await store.getEngram(noveltyResult.matchedEngramId)
398
+ : null;
399
+ if (target?.supersededBy) target = await store.getEngram(target.supersededBy);
400
+ const closeEnough = noveltyResult.matchScore >= CORRECTION_MIN_MATCH;
401
+ if (target && closeEnough && target.agentId === input.agentId && mergeBlockedByTrust(input.tags, target.tags)) {
402
+ trustBlocked = { matchedId: target.id, writeTrust: trustTier(input.tags), matchedTrust: trustTier(target.tags) };
403
+ }
404
+ if (target && closeEnough && target.agentId === input.agentId && !trustBlocked
405
+ && !target.retracted && target.stage !== 'archived' && !target.supersededBy) {
406
+ const correctionSalience: SalienceResult = {
407
+ ...salience,
408
+ disposition: 'active' as const,
409
+ reasonCodes: [...salience.reasonCodes, 'correction:override-active'],
410
+ };
411
+ result = await createNewEngram(engines, {
412
+ ...input,
413
+ // The replacement must not stand weaker than what it replaces.
414
+ confidence: Math.max(input.confidence ?? 0, target.confidence),
415
+ }, correctionSalience, noveltyResult, {
416
+ effectiveEventType,
417
+ effectiveMemoryClass,
418
+ supersedesId: target.id,
419
+ }, prewriteEmbedding);
420
+ correctionOf = { id: target.id, concept: target.concept, matchScore: noveltyResult.matchScore };
421
+ }
422
+ }
423
+
323
424
  // -- Default: create new engram (if no reinforce/supersede branch fired) --
324
425
  if (!result) {
426
+ // An explicit `supersedes` names the target, but naming it does not make the
427
+ // write trustworthy: an untrusted write must not retire a trusted memory by id.
428
+ if (input.supersedes) {
429
+ const named = await store.getEngram(input.supersedes);
430
+ if (named && mergeBlockedByTrust(input.tags, named.tags)) {
431
+ trustBlocked = { matchedId: named.id, writeTrust: trustTier(input.tags), matchedTrust: trustTier(named.tags) };
432
+ input = { ...input, supersedes: undefined };
433
+ }
434
+ }
325
435
  // A correction of a near-duplicate has low novelty BY CONSTRUCTION: the replacement
326
436
  // says almost what the engram it replaces said. Left on its own salience it lands at
327
437
  // `discard` (confidence 0.25, tagged `low-salience`, below HEALTHY_CONFIDENCE_FLOOR)
@@ -343,6 +453,12 @@ export async function performWrite(
343
453
  supersedesId: input.supersedes,
344
454
  }, prewriteEmbedding);
345
455
  }
456
+ // A correction always reports what it did, including "nothing" (null).
457
+ if (input.correction && !input.supersedes && result) result.correctionOf = correctionOf ?? null;
458
+ if (trustBlocked && result) {
459
+ result.trustBlocked = trustBlocked;
460
+ if (result.salience) result.salience = { ...result.salience, reasonCodes: [...result.salience.reasonCodes, 'trust:no-merge'] };
461
+ }
346
462
  } catch (e) {
347
463
  const es = `${(e as { code?: unknown })?.code ?? ''} ${(e as Error)?.message ?? ''}`;
348
464
  busyHit = /busy/i.test(es);
@@ -59,6 +59,8 @@ import type {
59
59
  } from '../types/index.js';
60
60
  import type { IEngramStore as EngramStore } from '../storage/store.js';
61
61
  import { reorderByReranker, rerank2Enabled, rerank2WindowSize } from '../core/rerank2.js';
62
+ import { validityState } from '../core/validity.js';
63
+ import { detectConflicts } from '../core/conflicts.js';
62
64
  import { buildRerankPassage, rerankTruncation, rerankWindowMode } from '../core/rerank-window.js';
63
65
  import { parseTemporal, temporalEnabled, temporalBoost, type TemporalMatch } from '../core/temporal-query.js';
64
66
  import { aliasTermsFor } from '../core/alias-map.js';
@@ -1137,11 +1139,17 @@ export class ActivationEngine {
1137
1139
  }
1138
1140
  }
1139
1141
 
1140
- // Phase 8c: Supersession penalty — superseded memories are deprioritized.
1141
- // They aren't wrong (that's retraction), just outdated.
1142
+ // Phase 8c: Stale penalty — superseded memories, and (since 2026-10-19)
1143
+ // memories outside their valid_from/valid_to window, are deprioritized.
1144
+ // They aren't wrong (that's retraction), just outdated or not yet true.
1145
+ // The factor is also recorded on phaseScores so Phase 9b's re-sort by
1146
+ // reranker score honours it — without that, rerank2 put a superseded
1147
+ // memory above its successor (see src/core/rerank2.ts).
1148
+ const validityNow = query.asOf ?? Date.now();
1142
1149
  for (const item of rerankPool) {
1143
- if (item.engram.supersededBy) {
1144
- item.score *= 0.15; // Severe down-rank — successor should dominate
1150
+ if (item.engram.supersededBy || validityState(item.engram, validityNow) !== 'current') {
1151
+ item.score *= 0.15; // Severe down-rank — the current version should dominate
1152
+ item.phaseScores.stalePenalty = 0.15;
1145
1153
  }
1146
1154
  }
1147
1155
 
@@ -1306,6 +1314,21 @@ export class ActivationEngine {
1306
1314
  };
1307
1315
  });
1308
1316
 
1317
+ // Conflict surfacing over the returned set (2026-10-19): two versions of
1318
+ // one fact used to come back as two undated lines with nothing to say they
1319
+ // disagreed. Pairwise over `limit` results, so it is a handful of string
1320
+ // comparisons; see src/core/conflicts.ts for the rule and its known limit.
1321
+ if (results.length > 1) {
1322
+ const marks = detectConflicts(results, validityNow);
1323
+ results.forEach((r, i) => { if (marks[i].length > 0) r.conflictsWith = marks[i]; });
1324
+ // The marks do NOT reorder results. Measured 2026-10-19: moving the
1325
+ // newest of a marked pair ahead cost identifier s@1 93.0% -> 74.7% with
1326
+ // s@5 unchanged, because on a real store the detector also marks related
1327
+ // memories that differ in a ticket id or a date — a cue to check the
1328
+ // source, not a verdict on rank. A harness that wants newest-first among
1329
+ // marked results can apply conflicts.ts `newestFirstAmongConflicts` itself.
1330
+ }
1331
+
1309
1332
  const activatedIds = results.map(r => r.engram.id);
1310
1333
 
1311
1334
  // Side effects: touch, co-activate, defer Hebbian to validation gate (skip for internal/system calls)