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.
- package/README.md +321 -319
- package/dist/adapters/common.d.ts +34 -0
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +528 -446
- package/dist/adapters/common.js.map +1 -1
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +19 -0
- package/dist/api/routes.js.map +1 -1
- package/dist/cli/migrate.d.ts.map +1 -1
- package/dist/cli/migrate.js +31 -29
- package/dist/cli/migrate.js.map +1 -1
- package/dist/core/asked-value.d.ts +37 -0
- package/dist/core/asked-value.d.ts.map +1 -0
- package/dist/core/asked-value.js +77 -0
- package/dist/core/asked-value.js.map +1 -0
- package/dist/core/conflicts.d.ts +89 -0
- package/dist/core/conflicts.d.ts.map +1 -0
- package/dist/core/conflicts.js +267 -0
- package/dist/core/conflicts.js.map +1 -0
- package/dist/core/embeddings.d.ts +14 -2
- package/dist/core/embeddings.d.ts.map +1 -1
- package/dist/core/embeddings.js +32 -10
- package/dist/core/embeddings.js.map +1 -1
- package/dist/core/format-recall.d.ts +21 -1
- package/dist/core/format-recall.d.ts.map +1 -1
- package/dist/core/format-recall.js +60 -5
- package/dist/core/format-recall.js.map +1 -1
- package/dist/core/ml-worker.d.ts +24 -1
- package/dist/core/ml-worker.d.ts.map +1 -1
- package/dist/core/ml-worker.js +165 -4
- package/dist/core/ml-worker.js.map +1 -1
- package/dist/core/model-daemon-client.d.ts +77 -0
- package/dist/core/model-daemon-client.d.ts.map +1 -0
- package/dist/core/model-daemon-client.js +383 -0
- package/dist/core/model-daemon-client.js.map +1 -0
- package/dist/core/model-daemon-server.d.ts +71 -0
- package/dist/core/model-daemon-server.d.ts.map +1 -0
- package/dist/core/model-daemon-server.js +363 -0
- package/dist/core/model-daemon-server.js.map +1 -0
- package/dist/core/model-daemon.d.ts +137 -0
- package/dist/core/model-daemon.d.ts.map +1 -0
- package/dist/core/model-daemon.js +231 -0
- package/dist/core/model-daemon.js.map +1 -0
- package/dist/core/query-expander.d.ts +5 -0
- package/dist/core/query-expander.d.ts.map +1 -1
- package/dist/core/query-expander.js +3 -2
- package/dist/core/query-expander.js.map +1 -1
- package/dist/core/recall-config.d.ts +1 -1
- package/dist/core/recall-config.d.ts.map +1 -1
- package/dist/core/recall-config.js +5 -0
- package/dist/core/recall-config.js.map +1 -1
- package/dist/core/rerank-window.d.ts +7 -2
- package/dist/core/rerank-window.d.ts.map +1 -1
- package/dist/core/rerank-window.js +9 -4
- package/dist/core/rerank-window.js.map +1 -1
- package/dist/core/rerank2.d.ts +52 -2
- package/dist/core/rerank2.d.ts.map +1 -1
- package/dist/core/rerank2.js +60 -3
- package/dist/core/rerank2.js.map +1 -1
- package/dist/core/reranker.d.ts +19 -0
- package/dist/core/reranker.d.ts.map +1 -1
- package/dist/core/reranker.js +76 -12
- package/dist/core/reranker.js.map +1 -1
- package/dist/core/trust.d.ts +32 -0
- package/dist/core/trust.d.ts.map +1 -0
- package/dist/core/trust.js +66 -0
- package/dist/core/trust.js.map +1 -0
- package/dist/core/validity.d.ts +21 -0
- package/dist/core/validity.d.ts.map +1 -0
- package/dist/core/validity.js +41 -0
- package/dist/core/validity.js.map +1 -0
- package/dist/core/whoami.d.ts +3 -0
- package/dist/core/whoami.d.ts.map +1 -1
- package/dist/core/whoami.js +14 -0
- package/dist/core/whoami.js.map +1 -1
- package/dist/core/write-pipeline.d.ts +48 -0
- package/dist/core/write-pipeline.d.ts.map +1 -1
- package/dist/core/write-pipeline.js +80 -3
- package/dist/core/write-pipeline.js.map +1 -1
- package/dist/engine/activation.d.ts.map +1 -1
- package/dist/engine/activation.js +27 -4
- package/dist/engine/activation.js.map +1 -1
- package/dist/engine/consolidation.d.ts.map +1 -1
- package/dist/engine/consolidation.js +53 -4
- package/dist/engine/consolidation.js.map +1 -1
- package/dist/hooks/prime-query.d.ts +57 -0
- package/dist/hooks/prime-query.d.ts.map +1 -0
- package/dist/hooks/prime-query.js +388 -0
- package/dist/hooks/prime-query.js.map +1 -0
- package/dist/hooks/prime.d.ts +8 -0
- package/dist/hooks/prime.d.ts.map +1 -1
- package/dist/hooks/prime.js +21 -2
- package/dist/hooks/prime.js.map +1 -1
- package/dist/hooks/sidecar.d.ts +7 -0
- package/dist/hooks/sidecar.d.ts.map +1 -1
- package/dist/hooks/sidecar.js +10 -2
- package/dist/hooks/sidecar.js.map +1 -1
- package/dist/index.js +11 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +148 -103
- package/dist/mcp.js.map +1 -1
- package/dist/model-daemon.d.ts +8 -0
- package/dist/model-daemon.d.ts.map +1 -0
- package/dist/model-daemon.js +45 -0
- package/dist/model-daemon.js.map +1 -0
- package/dist/storage/factory.d.ts.map +1 -1
- package/dist/storage/factory.js +5 -0
- package/dist/storage/factory.js.map +1 -1
- package/dist/storage/pglite-schema.d.ts.map +1 -1
- package/dist/storage/pglite-schema.js +176 -170
- package/dist/storage/pglite-schema.js.map +1 -1
- package/dist/storage/pglite.d.ts +5 -0
- package/dist/storage/pglite.d.ts.map +1 -1
- package/dist/storage/pglite.js +165 -153
- package/dist/storage/pglite.js.map +1 -1
- package/dist/storage/postgres.d.ts +5 -0
- package/dist/storage/postgres.d.ts.map +1 -1
- package/dist/storage/postgres.js +165 -153
- package/dist/storage/postgres.js.map +1 -1
- package/dist/storage/sqlite.d.ts +22 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +400 -363
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/types/engram.d.ts +33 -0
- package/dist/types/engram.d.ts.map +1 -1
- package/dist/types/engram.js.map +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/README.md +1 -1
- package/plugin/bin/awm-mcp-launcher.cjs +2 -2
- package/plugin/skills/awm-memory/SKILL.md +11 -2
- package/src/adapters/common.ts +881 -802
- package/src/api/routes.ts +20 -0
- package/src/cli/migrate.ts +309 -307
- package/src/core/asked-value.ts +74 -0
- package/src/core/conflicts.ts +280 -0
- package/src/core/embeddings.ts +35 -10
- package/src/core/format-recall.ts +83 -25
- package/src/core/ml-worker.ts +458 -281
- package/src/core/model-daemon-client.ts +374 -0
- package/src/core/model-daemon-server.ts +344 -0
- package/src/core/model-daemon.ts +243 -0
- package/src/core/query-expander.ts +127 -126
- package/src/core/recall-config.ts +5 -0
- package/src/core/rerank-window.ts +164 -159
- package/src/core/rerank2.ts +143 -82
- package/src/core/reranker.ts +78 -11
- package/src/core/trust.ts +65 -0
- package/src/core/validity.ts +47 -0
- package/src/core/whoami.ts +136 -120
- package/src/core/write-pipeline.ts +119 -3
- package/src/engine/activation.ts +27 -4
- package/src/engine/consolidation.ts +950 -899
- package/src/hooks/prime-query.ts +375 -0
- package/src/hooks/prime.ts +25 -2
- package/src/hooks/sidecar.ts +13 -3
- package/src/index.ts +255 -248
- package/src/mcp.ts +1521 -1475
- package/src/model-daemon.ts +48 -0
- package/src/storage/factory.ts +25 -19
- package/src/storage/pglite-schema.ts +199 -193
- package/src/storage/pglite.ts +1513 -1498
- package/src/storage/postgres.ts +1613 -1598
- package/src/storage/sqlite.ts +2056 -2019
- package/src/types/engram.ts +30 -0
package/src/core/whoami.ts
CHANGED
|
@@ -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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
`
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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);
|
package/src/engine/activation.ts
CHANGED
|
@@ -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:
|
|
1141
|
-
//
|
|
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 —
|
|
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)
|