agent-working-memory 0.16.1 → 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 -318
- package/dist/adapters/common.d.ts +34 -0
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +528 -444
- 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/salience.d.ts +2 -0
- package/dist/core/salience.d.ts.map +1 -1
- package/dist/core/salience.js +45 -18
- package/dist/core/salience.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 +104 -4
- 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 +67 -5
- 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 +106 -106
- 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 +16 -5
- package/src/adapters/common.ts +881 -800
- 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/salience.ts +45 -18
- 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 +143 -4
- package/src/engine/activation.ts +27 -4
- package/src/engine/consolidation.ts +950 -887
- 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 {
|
|
@@ -169,8 +219,19 @@ export async function performWrite(
|
|
|
169
219
|
input: WriteInput,
|
|
170
220
|
): Promise<WriteResult> {
|
|
171
221
|
const { store, connectionEngine } = engines;
|
|
222
|
+
// An explicit `supersedes` argument outranks every novelty signal: the caller has
|
|
223
|
+
// NAMED the engram this write replaces. Gating reinforcement off it routes the write
|
|
224
|
+
// to createNewEngram, which is the only path that honors meta.supersedesId (see the
|
|
225
|
+
// supersession side-effects below). Without this gate the argument was silently
|
|
226
|
+
// discarded by every branch in the block below — R1 (healthy match reinforced),
|
|
227
|
+
// R2 (the superseder reinforced instead), R1b (an archived match revived), and R3,
|
|
228
|
+
// which supersedes `matched.id` rather than the id the caller actually asked for.
|
|
172
229
|
const enableReinforcement = input.enableReinforcement !== false
|
|
173
|
-
&& process.env.AWM_WRITE_PIPELINE !== 'off'
|
|
230
|
+
&& process.env.AWM_WRITE_PIPELINE !== 'off'
|
|
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;
|
|
174
235
|
|
|
175
236
|
// Profiling: AWM_PROFILE_WRITE=1 logs ms-per-phase to stderr (v0.8.2).
|
|
176
237
|
// Off by default; zero cost when unset.
|
|
@@ -223,7 +284,7 @@ export async function performWrite(
|
|
|
223
284
|
content: input.content,
|
|
224
285
|
eventType: effectiveEventType,
|
|
225
286
|
surprise: input.surprise,
|
|
226
|
-
decisionMade: input.decisionMade,
|
|
287
|
+
decisionMade: input.decisionMade || input.intent === 'decision',
|
|
227
288
|
causalDepth: input.causalDepth,
|
|
228
289
|
resolutionEffort: input.resolutionEffort,
|
|
229
290
|
novelty: noveltyResult.novelty,
|
|
@@ -234,6 +295,7 @@ export async function performWrite(
|
|
|
234
295
|
const tStartCreate = performance.now();
|
|
235
296
|
let busyHit = false;
|
|
236
297
|
let result: WriteResult | null = null;
|
|
298
|
+
let trustBlocked: WriteResult['trustBlocked'];
|
|
237
299
|
try {
|
|
238
300
|
if (enableReinforcement && noveltyResult.matchedEngramId) {
|
|
239
301
|
const matched = await store.getEngram(noveltyResult.matchedEngramId);
|
|
@@ -241,7 +303,12 @@ export async function performWrite(
|
|
|
241
303
|
// workspace/hive recall can surface another agent's same-concept engram as the match; mutating it
|
|
242
304
|
// (merge content, re-embed, bump confidence, or supersede) would corrupt that agent's memory. A
|
|
243
305
|
// cross-agent match falls through to createNewEngram so this agent gets its own copy.
|
|
244
|
-
|
|
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) {
|
|
245
312
|
const newConcept = (input.concept ?? '').toLowerCase().trim();
|
|
246
313
|
const matchedConcept = (matched.concept ?? '').toLowerCase().trim();
|
|
247
314
|
const sameConcept = newConcept === matchedConcept && newConcept.length > 0;
|
|
@@ -312,14 +379,86 @@ export async function performWrite(
|
|
|
312
379
|
}
|
|
313
380
|
}
|
|
314
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
|
+
|
|
315
424
|
// -- Default: create new engram (if no reinforce/supersede branch fired) --
|
|
316
425
|
if (!result) {
|
|
317
|
-
|
|
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
|
+
}
|
|
435
|
+
// A correction of a near-duplicate has low novelty BY CONSTRUCTION: the replacement
|
|
436
|
+
// says almost what the engram it replaces said. Left on its own salience it lands at
|
|
437
|
+
// `discard` (confidence 0.25, tagged `low-salience`, below HEALTHY_CONFIDENCE_FLOOR)
|
|
438
|
+
// or at `staging` — while the supersession side-effect fires regardless. The old
|
|
439
|
+
// engram would be retired and its replacement too weak to stand in for it, so the
|
|
440
|
+
// fact leaves recall altogether: strictly worse than dropping the argument. R3 above
|
|
441
|
+
// already forces `active` for precisely this reason; an explicit `supersedes` is the
|
|
442
|
+
// same correction intent, stated by the caller instead of inferred from eventType.
|
|
443
|
+
const supersedeSalience: SalienceResult = input.supersedes
|
|
444
|
+
? {
|
|
445
|
+
...salience,
|
|
446
|
+
disposition: 'active' as const,
|
|
447
|
+
reasonCodes: [...salience.reasonCodes, 'supersede:override-active'],
|
|
448
|
+
}
|
|
449
|
+
: salience;
|
|
450
|
+
result = await createNewEngram(engines, input, supersedeSalience, noveltyResult, {
|
|
318
451
|
effectiveEventType,
|
|
319
452
|
effectiveMemoryClass,
|
|
320
453
|
supersedesId: input.supersedes,
|
|
321
454
|
}, prewriteEmbedding);
|
|
322
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
|
+
}
|
|
323
462
|
} catch (e) {
|
|
324
463
|
const es = `${(e as { code?: unknown })?.code ?? ''} ${(e as Error)?.message ?? ''}`;
|
|
325
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)
|