agent-working-memory 0.7.16 → 0.8.5
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 +156 -15
- package/dist/adapters/claude-code.d.ts.map +1 -1
- package/dist/adapters/claude-code.js +2 -16
- package/dist/adapters/claude-code.js.map +1 -1
- package/dist/adapters/codex.d.ts.map +1 -1
- package/dist/adapters/codex.js +2 -11
- package/dist/adapters/codex.js.map +1 -1
- package/dist/adapters/common.d.ts +18 -0
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +254 -141
- package/dist/adapters/common.js.map +1 -1
- package/dist/adapters/cursor.d.ts.map +1 -1
- package/dist/adapters/cursor.js +2 -15
- package/dist/adapters/cursor.js.map +1 -1
- package/dist/adapters/http.d.ts.map +1 -1
- package/dist/adapters/http.js +28 -34
- package/dist/adapters/http.js.map +1 -1
- package/dist/api/routes.d.ts +1 -1
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +280 -113
- package/dist/api/routes.js.map +1 -1
- package/dist/cli/migrate.d.ts +42 -0
- package/dist/cli/migrate.d.ts.map +1 -0
- package/dist/cli/migrate.js +268 -0
- package/dist/cli/migrate.js.map +1 -0
- package/dist/cli.js +45 -1
- package/dist/cli.js.map +1 -1
- package/dist/coordination/circuit-breaker.d.ts +24 -0
- package/dist/coordination/circuit-breaker.d.ts.map +1 -0
- package/dist/coordination/circuit-breaker.js +70 -0
- package/dist/coordination/circuit-breaker.js.map +1 -0
- package/dist/coordination/failure-modes.d.ts +21 -0
- package/dist/coordination/failure-modes.d.ts.map +1 -0
- package/dist/coordination/failure-modes.js +49 -0
- package/dist/coordination/failure-modes.js.map +1 -0
- package/dist/coordination/peer-decisions.js +7 -7
- package/dist/coordination/routes.d.ts.map +1 -1
- package/dist/coordination/routes.js +44 -1
- package/dist/coordination/routes.js.map +1 -1
- package/dist/coordination/schema.d.ts.map +1 -1
- package/dist/coordination/schema.js +18 -0
- package/dist/coordination/schema.js.map +1 -1
- package/dist/coordination/schemas.d.ts +9 -9
- package/dist/coordination/stale.d.ts +9 -1
- package/dist/coordination/stale.d.ts.map +1 -1
- package/dist/coordination/stale.js +53 -9
- package/dist/coordination/stale.js.map +1 -1
- package/dist/core/embeddings.d.ts +16 -10
- package/dist/core/embeddings.d.ts.map +1 -1
- package/dist/core/embeddings.js +54 -38
- package/dist/core/embeddings.js.map +1 -1
- package/dist/core/ml-worker-entry.d.ts +17 -0
- package/dist/core/ml-worker-entry.d.ts.map +1 -0
- package/dist/core/ml-worker-entry.js +193 -0
- package/dist/core/ml-worker-entry.js.map +1 -0
- package/dist/core/ml-worker.d.ts +59 -0
- package/dist/core/ml-worker.d.ts.map +1 -0
- package/dist/core/ml-worker.js +253 -0
- package/dist/core/ml-worker.js.map +1 -0
- package/dist/core/query-expander.d.ts +13 -15
- package/dist/core/query-expander.d.ts.map +1 -1
- package/dist/core/query-expander.js +48 -54
- package/dist/core/query-expander.js.map +1 -1
- package/dist/core/reranker.d.ts +9 -19
- package/dist/core/reranker.d.ts.map +1 -1
- package/dist/core/reranker.js +33 -35
- package/dist/core/reranker.js.map +1 -1
- package/dist/core/salience.d.ts +32 -5
- package/dist/core/salience.d.ts.map +1 -1
- package/dist/core/salience.js +201 -28
- package/dist/core/salience.js.map +1 -1
- package/dist/core/write-pipeline.d.ts +128 -0
- package/dist/core/write-pipeline.d.ts.map +1 -0
- package/dist/core/write-pipeline.js +405 -0
- package/dist/core/write-pipeline.js.map +1 -0
- package/dist/engine/activation.d.ts +2 -2
- package/dist/engine/activation.d.ts.map +1 -1
- package/dist/engine/activation.js +238 -115
- package/dist/engine/activation.js.map +1 -1
- package/dist/engine/confidence.d.ts +62 -0
- package/dist/engine/confidence.d.ts.map +1 -0
- package/dist/engine/confidence.js +100 -0
- package/dist/engine/confidence.js.map +1 -0
- package/dist/engine/connections.d.ts +47 -8
- package/dist/engine/connections.d.ts.map +1 -1
- package/dist/engine/connections.js +83 -22
- package/dist/engine/connections.js.map +1 -1
- package/dist/engine/consolidation-scheduler.d.ts +46 -10
- package/dist/engine/consolidation-scheduler.d.ts.map +1 -1
- package/dist/engine/consolidation-scheduler.js +170 -50
- package/dist/engine/consolidation-scheduler.js.map +1 -1
- package/dist/engine/consolidation.d.ts +16 -2
- package/dist/engine/consolidation.d.ts.map +1 -1
- package/dist/engine/consolidation.js +143 -44
- package/dist/engine/consolidation.js.map +1 -1
- package/dist/engine/eval.d.ts +2 -2
- package/dist/engine/eval.d.ts.map +1 -1
- package/dist/engine/eval.js +9 -9
- package/dist/engine/eval.js.map +1 -1
- package/dist/engine/eviction.d.ts +4 -4
- package/dist/engine/eviction.d.ts.map +1 -1
- package/dist/engine/eviction.js +15 -15
- package/dist/engine/eviction.js.map +1 -1
- package/dist/engine/retraction.d.ts +93 -5
- package/dist/engine/retraction.d.ts.map +1 -1
- package/dist/engine/retraction.js +238 -19
- package/dist/engine/retraction.js.map +1 -1
- package/dist/engine/staging.d.ts +1 -1
- package/dist/engine/staging.d.ts.map +1 -1
- package/dist/engine/staging.js +3 -3
- package/dist/engine/staging.js.map +1 -1
- package/dist/hooks/sidecar.d.ts +1 -1
- package/dist/hooks/sidecar.d.ts.map +1 -1
- package/dist/hooks/sidecar.js +4 -4
- package/dist/hooks/sidecar.js.map +1 -1
- package/dist/index.js +48 -31
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +110 -155
- package/dist/mcp.js.map +1 -1
- package/dist/storage/factory.d.ts +55 -0
- package/dist/storage/factory.d.ts.map +1 -0
- package/dist/storage/factory.js +135 -0
- package/dist/storage/factory.js.map +1 -0
- package/dist/storage/pglite-schema.d.ts +20 -0
- package/dist/storage/pglite-schema.d.ts.map +1 -0
- package/dist/storage/pglite-schema.js +165 -0
- package/dist/storage/pglite-schema.js.map +1 -0
- package/dist/storage/pglite.d.ts +187 -0
- package/dist/storage/pglite.d.ts.map +1 -0
- package/dist/storage/pglite.js +1114 -0
- package/dist/storage/pglite.js.map +1 -0
- package/dist/storage/sqlite.d.ts +117 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +357 -8
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/storage/store.d.ts +63 -0
- package/dist/storage/store.d.ts.map +1 -0
- package/dist/storage/store.js +27 -0
- package/dist/storage/store.js.map +1 -0
- package/dist/types/engram.d.ts +92 -2
- package/dist/types/engram.d.ts.map +1 -1
- package/dist/types/engram.js.map +1 -1
- package/package.json +3 -1
- package/src/adapters/claude-code.ts +218 -234
- package/src/adapters/codex.ts +252 -262
- package/src/adapters/common.ts +411 -284
- package/src/adapters/cursor.ts +135 -150
- package/src/adapters/http.ts +93 -100
- package/src/adapters/index.ts +31 -31
- package/src/adapters/types.ts +75 -75
- package/src/api/index.ts +3 -3
- package/src/api/routes.ts +367 -120
- package/src/cli/migrate.ts +307 -0
- package/src/cli.ts +37 -1
- package/src/coordination/circuit-breaker.ts +83 -0
- package/src/coordination/events.ts +90 -90
- package/src/coordination/failure-modes.ts +50 -0
- package/src/coordination/peer-decisions.ts +105 -105
- package/src/coordination/plugin-loader.ts +60 -60
- package/src/coordination/plugin.ts +44 -44
- package/src/coordination/routes.ts +56 -1
- package/src/coordination/schema.ts +13 -0
- package/src/coordination/stale.ts +80 -11
- package/src/coordination/types.ts +311 -311
- package/src/coordination/write-mutex.ts +69 -69
- package/src/core/auto-tagger.ts +168 -168
- package/src/core/decay.ts +63 -63
- package/src/core/embeddings.ts +110 -93
- package/src/core/index.ts +5 -5
- package/src/core/logger.ts +36 -36
- package/src/core/ml-worker-entry.ts +194 -0
- package/src/core/ml-worker.ts +281 -0
- package/src/core/query-expander.ts +122 -128
- package/src/core/reranker.ts +119 -125
- package/src/core/salience.ts +200 -33
- package/src/core/write-pipeline.ts +521 -0
- package/src/engine/activation.ts +235 -109
- package/src/engine/confidence.ts +120 -0
- package/src/engine/connections.ts +162 -103
- package/src/engine/consolidation-scheduler.ts +242 -125
- package/src/engine/consolidation.ts +138 -45
- package/src/engine/eval.ts +102 -102
- package/src/engine/eviction.ts +101 -101
- package/src/engine/index.ts +8 -8
- package/src/engine/retraction.ts +366 -116
- package/src/engine/staging.ts +74 -74
- package/src/hooks/sidecar.ts +5 -5
- package/src/index.ts +42 -28
- package/src/mcp.ts +105 -152
- package/src/storage/factory.ts +147 -0
- package/src/storage/index.ts +3 -3
- package/src/storage/pglite-schema.ts +166 -0
- package/src/storage/pglite.ts +1363 -0
- package/src/storage/sqlite.ts +402 -7
- package/src/storage/store.ts +80 -0
- package/src/types/agent.ts +67 -67
- package/src/types/checkpoint.ts +46 -46
- package/src/types/engram.ts +101 -3
- package/src/types/eval.ts +100 -100
- package/src/types/index.ts +6 -6
package/src/core/reranker.ts
CHANGED
|
@@ -1,125 +1,119 @@
|
|
|
1
|
-
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
/**
|
|
4
|
-
* Cross-Encoder Re-Ranker
|
|
5
|
-
*
|
|
6
|
-
* Uses Xenova/ms-marco-MiniLM-L-6-v2 (~22MB ONNX)
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
let
|
|
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
|
-
}
|
|
121
|
-
}
|
|
122
|
-
results.sort((a, b) => b.score - a.score);
|
|
123
|
-
return results;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Cross-Encoder Re-Ranker - scores (query, passage) pairs for relevance.
|
|
5
|
+
*
|
|
6
|
+
* Uses Xenova/ms-marco-MiniLM-L-6-v2 (~22MB ONNX) trained on MS-MARCO
|
|
7
|
+
* passage ranking. Unlike bi-encoders, cross-encoders see both query and
|
|
8
|
+
* passage together via full attention - much better at judging if a
|
|
9
|
+
* passage actually answers a question.
|
|
10
|
+
*
|
|
11
|
+
* AWM 0.8.x: inference dispatches through ml-worker.ts (currently in-process
|
|
12
|
+
* — see ml-worker.ts for the worker_threads → in-process revert rationale).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
AutoTokenizer,
|
|
17
|
+
AutoModelForSequenceClassification,
|
|
18
|
+
type PreTrainedTokenizer,
|
|
19
|
+
type PreTrainedModel,
|
|
20
|
+
} from '@huggingface/transformers';
|
|
21
|
+
import { dispatchRerank, registerInProcessHandlers } from './ml-worker.js';
|
|
22
|
+
|
|
23
|
+
const DEFAULT_MODEL = 'Xenova/ms-marco-MiniLM-L-6-v2';
|
|
24
|
+
const MODEL_ID = process.env.AWM_RERANKER_MODEL || DEFAULT_MODEL;
|
|
25
|
+
|
|
26
|
+
// --- In-process fallback ---
|
|
27
|
+
|
|
28
|
+
let tokenizer: PreTrainedTokenizer | null = null;
|
|
29
|
+
let model: PreTrainedModel | null = null;
|
|
30
|
+
let initPromise: Promise<void> | null = null;
|
|
31
|
+
|
|
32
|
+
async function ensureLoaded(): Promise<void> {
|
|
33
|
+
if (tokenizer && model) return;
|
|
34
|
+
if (initPromise) return initPromise;
|
|
35
|
+
initPromise = (async () => {
|
|
36
|
+
tokenizer = await AutoTokenizer.from_pretrained(MODEL_ID);
|
|
37
|
+
model = await AutoModelForSequenceClassification.from_pretrained(MODEL_ID, { dtype: 'fp32' });
|
|
38
|
+
console.log(`Re-ranker model loaded in-process: ${MODEL_ID}`);
|
|
39
|
+
})();
|
|
40
|
+
return initPromise;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sigmoid(x: number): number {
|
|
44
|
+
return 1 / (1 + Math.exp(-x));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function inProcessRerank(args: { query: string; passages: string[] }): Promise<Array<{ index: number; score: number }>> {
|
|
48
|
+
const { query, passages } = args;
|
|
49
|
+
if (passages.length === 0) return [];
|
|
50
|
+
await ensureLoaded();
|
|
51
|
+
|
|
52
|
+
// Batch path
|
|
53
|
+
try {
|
|
54
|
+
const queries = passages.map(() => query);
|
|
55
|
+
const inputs = tokenizer!(queries, {
|
|
56
|
+
text_pair: passages,
|
|
57
|
+
padding: true,
|
|
58
|
+
truncation: true,
|
|
59
|
+
return_tensors: 'pt',
|
|
60
|
+
});
|
|
61
|
+
const output = await model!(inputs);
|
|
62
|
+
const logits = output.logits ?? output.last_hidden_state;
|
|
63
|
+
const data = logits.data as Float32Array | number[];
|
|
64
|
+
const results: Array<{ index: number; score: number }> = [];
|
|
65
|
+
for (let i = 0; i < passages.length; i++) {
|
|
66
|
+
const rawLogit = Number(data[i] ?? 0);
|
|
67
|
+
results.push({ index: i, score: sigmoid(rawLogit) });
|
|
68
|
+
}
|
|
69
|
+
results.sort((a, b) => b.score - a.score);
|
|
70
|
+
return results;
|
|
71
|
+
} catch {
|
|
72
|
+
// Per-passage fallback (the original 0.7.13 path)
|
|
73
|
+
const results: Array<{ index: number; score: number }> = [];
|
|
74
|
+
for (let i = 0; i < passages.length; i++) {
|
|
75
|
+
try {
|
|
76
|
+
const inputs = tokenizer!(query, {
|
|
77
|
+
text_pair: passages[i],
|
|
78
|
+
padding: true,
|
|
79
|
+
truncation: true,
|
|
80
|
+
return_tensors: 'pt',
|
|
81
|
+
});
|
|
82
|
+
const output = await model!(inputs);
|
|
83
|
+
const logits = output.logits ?? output.last_hidden_state;
|
|
84
|
+
const rawLogit = logits.data[0] as number;
|
|
85
|
+
results.push({ index: i, score: sigmoid(rawLogit) });
|
|
86
|
+
} catch {
|
|
87
|
+
results.push({ index: i, score: 0 });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
results.sort((a, b) => b.score - a.score);
|
|
91
|
+
return results;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Register the in-process handler with the pool
|
|
96
|
+
registerInProcessHandlers({ rerank: inProcessRerank });
|
|
97
|
+
|
|
98
|
+
// --- Public API ---
|
|
99
|
+
|
|
100
|
+
/** Kept for backwards compat. */
|
|
101
|
+
export async function getReranker(): Promise<any> {
|
|
102
|
+
await ensureLoaded();
|
|
103
|
+
return model;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface RerankResult {
|
|
107
|
+
index: number;
|
|
108
|
+
score: number; // sigmoid-normalized relevance (0-1)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Re-rank candidate passages against a query using the cross-encoder.
|
|
113
|
+
* Returns results sorted by relevance score (descending).
|
|
114
|
+
* Dispatches to the worker pool (or in-process fallback).
|
|
115
|
+
*/
|
|
116
|
+
export async function rerank(query: string, passages: string[]): Promise<RerankResult[]> {
|
|
117
|
+
if (passages.length === 0) return [];
|
|
118
|
+
return dispatchRerank({ query, passages });
|
|
119
|
+
}
|
package/src/core/salience.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { SalienceFeatures, MemoryClass } from '../types/index.js';
|
|
14
|
-
import type { EngramStore } from '../storage/
|
|
14
|
+
import type { IEngramStore as EngramStore } from '../storage/store.js';
|
|
15
15
|
|
|
16
16
|
export type SalienceEventType = 'decision' | 'friction' | 'surprise' | 'causal' | 'observation' | 'user_feedback';
|
|
17
17
|
|
|
@@ -76,6 +76,46 @@ export function detectVerifiedFinding(content: string): boolean {
|
|
|
76
76
|
return dateCount + idCount >= 2;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Auto-detect trivial routine operations: file reads, status pings, log-line
|
|
81
|
+
* completions. These have high BM25 novelty (each one has different filenames,
|
|
82
|
+
* timestamps, attempt counts) but represent NO learning value — they're the
|
|
83
|
+
* sort of background chatter a working agent generates by the thousand.
|
|
84
|
+
*
|
|
85
|
+
* Why this exists: the novelty weight (0.45) puts a floor at ~0.45 for every
|
|
86
|
+
* write on a fresh agent, which prevents trivial observations from ever
|
|
87
|
+
* routing to 'discard'. self-test 1.2 ("File read completed successfully for
|
|
88
|
+
* file 0") explicitly asks for trivial → discard. We can't detect triviality
|
|
89
|
+
* from features alone — the caller passes surprise=0, effort=0 but novelty
|
|
90
|
+
* computes to 1.0 — so we need a content shape check.
|
|
91
|
+
*
|
|
92
|
+
* Pattern requires:
|
|
93
|
+
* - A routine verb phrase: "completed", "succeeded", "finished", "returned",
|
|
94
|
+
* "loaded", "saved", "read", "wrote", "synced", "pinged", "checked",
|
|
95
|
+
* "started", "stopped", "rotated", "flushed"
|
|
96
|
+
* - Generic operational noun: file/log/request/response/status/job/connection
|
|
97
|
+
* - Total length under ~150 chars (trivial events are short)
|
|
98
|
+
*
|
|
99
|
+
* Matched memories get a salience CAP at 0.10 (below the 0.2 stagingThreshold,
|
|
100
|
+
* so they route to 'discard'). Caller can still force-store via
|
|
101
|
+
* memory_class=canonical or memory_class=structural.
|
|
102
|
+
*/
|
|
103
|
+
const TRIVIAL_VERB_PATTERN = /\b(completed|succeeded|finished|returned|loaded|saved|read|wrote|synced|pinged|checked|started|stopped|rotated|flushed)\b/i;
|
|
104
|
+
const TRIVIAL_NOUN_PATTERN = /\b(file|log|request|response|status|job|connection|task|cron|sync|tick|batch)\b/i;
|
|
105
|
+
|
|
106
|
+
/** Returns true if the content looks like a routine operational ping that adds no learning value. */
|
|
107
|
+
export function detectTrivialOperation(content: string): boolean {
|
|
108
|
+
if (typeof content !== 'string' || content.length === 0) return false;
|
|
109
|
+
const text = content.trim();
|
|
110
|
+
if (text.length > 150) return false;
|
|
111
|
+
if (!TRIVIAL_VERB_PATTERN.test(text)) return false;
|
|
112
|
+
if (!TRIVIAL_NOUN_PATTERN.test(text)) return false;
|
|
113
|
+
// Don't trip on verified findings — they share some verbs (Completed) but
|
|
114
|
+
// have concrete identifiers. detectVerifiedFinding has priority.
|
|
115
|
+
if (detectVerifiedFinding(text)) return false;
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
79
119
|
export interface SalienceInput {
|
|
80
120
|
content: string;
|
|
81
121
|
eventType?: SalienceEventType;
|
|
@@ -124,6 +164,7 @@ export function evaluateSalience(
|
|
|
124
164
|
let resolvedMemoryClass: MemoryClass = input.memoryClass ?? 'working';
|
|
125
165
|
let autoPromoted = false;
|
|
126
166
|
let verifiedFindingFloor = false;
|
|
167
|
+
let trivialOperationCap = false;
|
|
127
168
|
if (detectUserFeedback(input.content)) {
|
|
128
169
|
resolvedEventType = 'user_feedback';
|
|
129
170
|
resolvedMemoryClass = 'canonical';
|
|
@@ -136,6 +177,10 @@ export function evaluateSalience(
|
|
|
136
177
|
resolvedEventType = 'decision';
|
|
137
178
|
}
|
|
138
179
|
verifiedFindingFloor = true;
|
|
180
|
+
} else if (detectTrivialOperation(input.content)) {
|
|
181
|
+
// Trivial routine operation — cap salience below stagingThreshold so it
|
|
182
|
+
// routes to 'discard'. Caller can still force-keep via canonical/structural.
|
|
183
|
+
trivialOperationCap = true;
|
|
139
184
|
}
|
|
140
185
|
|
|
141
186
|
const features: SalienceFeatures = {
|
|
@@ -149,6 +194,7 @@ export function evaluateSalience(
|
|
|
149
194
|
const reasonCodes: string[] = [];
|
|
150
195
|
if (autoPromoted) reasonCodes.push('auto:user_feedback');
|
|
151
196
|
if (verifiedFindingFloor) reasonCodes.push('auto:verified_finding');
|
|
197
|
+
if (trivialOperationCap) reasonCodes.push('auto:trivial_operation');
|
|
152
198
|
|
|
153
199
|
// Novelty: 1.0 = completely new info, 0 = exact duplicate exists
|
|
154
200
|
// Default to 0.8 (assume mostly novel) when caller doesn't check
|
|
@@ -168,18 +214,62 @@ export function evaluateSalience(
|
|
|
168
214
|
if (novelty > 0.7) reasonCodes.push('novel_information');
|
|
169
215
|
if (novelty < 0.3) reasonCodes.push('redundant_information');
|
|
170
216
|
|
|
171
|
-
// Event type bonus
|
|
217
|
+
// Event type bonus — gated by signal strength. The bonus represents the
|
|
218
|
+
// confidence that an event of this type warrants the type-specific boost.
|
|
219
|
+
// If the caller labels something `friction` but every signal is near zero,
|
|
220
|
+
// they're telling the system the friction was minor — the typeBonus is
|
|
221
|
+
// attenuated to reflect that. Without this gate, any labeled friction
|
|
222
|
+
// event clears the active threshold on novelty alone (self-test 1.4).
|
|
172
223
|
let typeBonus = 0;
|
|
224
|
+
let typeReason = '';
|
|
173
225
|
switch (features.eventType) {
|
|
174
|
-
case 'decision': typeBonus = 0.15;
|
|
175
|
-
case 'friction': typeBonus = 0.2;
|
|
176
|
-
case 'surprise': typeBonus = 0.25;
|
|
177
|
-
case 'causal': typeBonus = 0.2;
|
|
178
|
-
case 'user_feedback': typeBonus = 0.3;
|
|
226
|
+
case 'decision': typeBonus = 0.15; typeReason = 'event:decision'; break;
|
|
227
|
+
case 'friction': typeBonus = 0.2; typeReason = 'event:friction'; break;
|
|
228
|
+
case 'surprise': typeBonus = 0.25; typeReason = 'event:surprise'; break;
|
|
229
|
+
case 'causal': typeBonus = 0.2; typeReason = 'event:causal'; break;
|
|
230
|
+
case 'user_feedback': typeBonus = 0.3; typeReason = 'event:user_feedback'; break;
|
|
179
231
|
case 'observation': break;
|
|
180
232
|
}
|
|
233
|
+
// Signal-weakness gate. The novelty score alone (~0.45 for fresh content)
|
|
234
|
+
// would clear the active threshold (0.4), so any labelled event with no
|
|
235
|
+
// backing numerical signals lands as 'active' regardless of the label's
|
|
236
|
+
// semantics. That's wrong for friction/causal: those types describe events
|
|
237
|
+
// that *happened to the agent* and benefit from explicit intensity signals.
|
|
238
|
+
// surprise / user_feedback / decision-with-decisionMade are exempt: their
|
|
239
|
+
// label alone is the signal.
|
|
240
|
+
const exemptFromAttenuation =
|
|
241
|
+
features.eventType === 'user_feedback'
|
|
242
|
+
|| features.eventType === 'surprise'
|
|
243
|
+
|| (features.eventType === 'decision' && features.decisionMade);
|
|
244
|
+
const signalStrength = features.surprise + features.causalDepth + features.resolutionEffort + (features.decisionMade ? 0.5 : 0);
|
|
245
|
+
const signalsAreWeak = !exemptFromAttenuation && signalStrength < 0.5;
|
|
246
|
+
|
|
247
|
+
if (typeBonus > 0 && signalsAreWeak) {
|
|
248
|
+
typeBonus *= 0.25; // weak-signal event: keep a hint, not the full bonus
|
|
249
|
+
typeReason += ':attenuated';
|
|
250
|
+
}
|
|
251
|
+
if (typeReason) reasonCodes.push(typeReason);
|
|
252
|
+
|
|
253
|
+
// Cap the novelty contribution when signals are weak AND the eventType
|
|
254
|
+
// claims a typeBonus (friction/causal). Without this, novelty=1.0 alone
|
|
255
|
+
// (0.45 noveltyScore) clears the active threshold (0.4), making any
|
|
256
|
+
// weakly-signalled non-exempt write 'active' regardless of intent.
|
|
257
|
+
// Plain observations (typeBonus=0) are NOT capped — a novel observation
|
|
258
|
+
// is still default-active even without explicit signals.
|
|
259
|
+
let cappedNoveltyScore = noveltyScore;
|
|
260
|
+
if (signalsAreWeak && typeBonus > 0) {
|
|
261
|
+
cappedNoveltyScore = Math.min(noveltyScore, 0.30);
|
|
262
|
+
if (cappedNoveltyScore < noveltyScore) reasonCodes.push('novelty:capped');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let score = Math.min(surpriseScore + decisionScore + causalScore + effortScore + cappedNoveltyScore + typeBonus, 1.0);
|
|
181
266
|
|
|
182
|
-
|
|
267
|
+
// Apply triviality cap BEFORE memoryClass floor — the floor still wins for
|
|
268
|
+
// canonical/structural writes (covered below). Trivial cap forces routine
|
|
269
|
+
// operational chatter below stagingThreshold.
|
|
270
|
+
if (trivialOperationCap) {
|
|
271
|
+
score = Math.min(score, 0.1);
|
|
272
|
+
}
|
|
183
273
|
|
|
184
274
|
// Memory class overrides
|
|
185
275
|
const memoryClass = resolvedMemoryClass;
|
|
@@ -188,6 +278,15 @@ export function evaluateSalience(
|
|
|
188
278
|
// Canonical memories: salience floor of 0.7, never go to staging
|
|
189
279
|
score = Math.max(score, 0.7);
|
|
190
280
|
reasonCodes.push('class:canonical');
|
|
281
|
+
} else if (memoryClass === 'structural') {
|
|
282
|
+
// Structural memories (0.8): system-written event-log records — chapter
|
|
283
|
+
// analyses, promise advancements, materialized-view feeds. Floor 0.7 like
|
|
284
|
+
// canonical (always preserved by construction) but distinct reasonCode
|
|
285
|
+
// so retrieval paths can filter them out of cognitive `/activate` by
|
|
286
|
+
// default. Caller controls embedding + temporal-edge skipping in the
|
|
287
|
+
// write pipeline.
|
|
288
|
+
score = Math.max(score, 0.7);
|
|
289
|
+
reasonCodes.push('class:structural');
|
|
191
290
|
} else if (memoryClass === 'ephemeral') {
|
|
192
291
|
reasonCodes.push('class:ephemeral');
|
|
193
292
|
} else if (verifiedFindingFloor) {
|
|
@@ -196,8 +295,9 @@ export function evaluateSalience(
|
|
|
196
295
|
}
|
|
197
296
|
|
|
198
297
|
let disposition: 'active' | 'staging' | 'discard';
|
|
199
|
-
if (memoryClass === 'canonical') {
|
|
200
|
-
// Canonical
|
|
298
|
+
if (memoryClass === 'canonical' || memoryClass === 'structural') {
|
|
299
|
+
// Canonical = source-of-truth; structural = system-written record.
|
|
300
|
+
// Both always go active — they represent intentional permanent state.
|
|
201
301
|
disposition = 'active';
|
|
202
302
|
reasonCodes.push('disposition:active');
|
|
203
303
|
} else if (score >= activeThreshold) {
|
|
@@ -224,14 +324,14 @@ export function evaluateSalience(
|
|
|
224
324
|
*
|
|
225
325
|
* The check is cheap (~1ms) because BM25 is synchronous SQLite FTS5.
|
|
226
326
|
*/
|
|
227
|
-
export function computeNovelty(store: EngramStore, agentId: string, concept: string, content: string): number {
|
|
327
|
+
export async function computeNovelty(store: EngramStore, agentId: string, concept: string, content: string): Promise<number> {
|
|
228
328
|
try {
|
|
229
329
|
// Search using concept + first 100 chars of content (enough to detect duplicates, fast)
|
|
230
330
|
const contentStr = typeof content === 'string' ? content : '';
|
|
231
331
|
const conceptStr = typeof concept === 'string' ? concept : '';
|
|
232
332
|
const searchText = `${conceptStr} ${contentStr.slice(0, 100)}`;
|
|
233
333
|
|
|
234
|
-
const results = store.searchBM25WithRank(agentId, searchText, 5);
|
|
334
|
+
const results = await store.searchBM25WithRank(agentId, searchText, 5);
|
|
235
335
|
if (results.length === 0) return 1.0; // Nothing similar — fully novel
|
|
236
336
|
|
|
237
337
|
// searchBM25WithRank normalizes scores to 0..1 via |rank|/(1+|rank|).
|
|
@@ -283,43 +383,108 @@ export interface NoveltyResult {
|
|
|
283
383
|
}
|
|
284
384
|
|
|
285
385
|
/**
|
|
286
|
-
* Compute novelty score AND return the best matching engram (for
|
|
287
|
-
*
|
|
386
|
+
* Compute novelty score AND return the best matching engram (for
|
|
387
|
+
* reinforcement-on-duplicate).
|
|
388
|
+
*
|
|
389
|
+
* **v0.8.5+: dual-signal novelty (BM25 ∨ cosine, max).**
|
|
390
|
+
*
|
|
391
|
+
* When `embedding` is provided, computes both:
|
|
392
|
+
* - BM25 lexical match (existing path) — catches verbatim duplicates,
|
|
393
|
+
* identifier-driven matches, recall-output reingestion attempts.
|
|
394
|
+
* - Cosine semantic match (new) — catches paraphrased duplicates,
|
|
395
|
+
* vocabulary-drifted restatements of the same fact, cross-role
|
|
396
|
+
* rephrasings (user question → assistant answer about same fact).
|
|
397
|
+
*
|
|
398
|
+
* Takes `max(bm25Score, cosineSimilarity)` and returns the engram from
|
|
399
|
+
* whichever signal won. Why both?
|
|
400
|
+
* - BM25 is *backend-dependent* — Postgres ts_rank_cd and SQLite FTS5
|
|
401
|
+
* BM25 are different algorithms producing different rankings for
|
|
402
|
+
* short-text matches (verified empirically 2026-05-26). Cosine is
|
|
403
|
+
* *backend-agnostic* — same embedding model produces identical
|
|
404
|
+
* similarity scores on either backend.
|
|
405
|
+
* - Cosine alone misses the exact-text cases BM25 catches (recall
|
|
406
|
+
* output leakage, identifier matching). BM25 alone misses the
|
|
407
|
+
* semantic cases cosine catches (paraphrase, vocabulary drift —
|
|
408
|
+
* the LoCoMo pattern of "user said X" across conversations).
|
|
409
|
+
*
|
|
410
|
+
* When `embedding` is null/omitted, falls back to BM25-only (preserves
|
|
411
|
+
* backward compat with v0.8.4 and earlier callers).
|
|
412
|
+
*
|
|
288
413
|
* Optionally checks workspace-scoped memories too (cross-agent dedup).
|
|
289
414
|
*/
|
|
290
|
-
export function computeNoveltyWithMatch(
|
|
415
|
+
export async function computeNoveltyWithMatch(
|
|
291
416
|
store: EngramStore, agentId: string, concept: string, content: string,
|
|
292
|
-
workspace?: string | null
|
|
293
|
-
|
|
417
|
+
workspace?: string | null,
|
|
418
|
+
embedding?: number[] | null,
|
|
419
|
+
): Promise<NoveltyResult> {
|
|
294
420
|
try {
|
|
295
421
|
const contentStr = typeof content === 'string' ? content : '';
|
|
296
422
|
const conceptStr = typeof concept === 'string' ? concept : '';
|
|
297
423
|
const searchText = `${conceptStr} ${contentStr.slice(0, 100)}`;
|
|
298
424
|
|
|
299
|
-
//
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
// Workspace search — only if the store supports it (v0.5.4+)
|
|
303
|
-
let wsResults: { engram: { id: string }; bm25Score: number }[] = [];
|
|
425
|
+
// BM25 channel (existing) — agent-scoped + optional workspace.
|
|
426
|
+
const bm25Results = await store.searchBM25WithRank(agentId, searchText, 3);
|
|
427
|
+
let wsResults: { engram: { id: string; concept?: string; createdAt?: Date | string | number }; bm25Score: number }[] = [];
|
|
304
428
|
if (workspace && typeof (store as any).searchBM25WithRankWorkspace === 'function') {
|
|
305
|
-
wsResults = (store as any).searchBM25WithRankWorkspace(agentId, searchText, 3, workspace);
|
|
429
|
+
wsResults = await (store as any).searchBM25WithRankWorkspace(agentId, searchText, 3, workspace);
|
|
430
|
+
}
|
|
431
|
+
const allBm25 = [...bm25Results, ...wsResults];
|
|
432
|
+
allBm25.sort((a, b) => b.bm25Score - a.bm25Score);
|
|
433
|
+
const topBm25 = allBm25[0]
|
|
434
|
+
? { engramId: allBm25[0].engram.id, score: allBm25[0].bm25Score, engram: allBm25[0].engram }
|
|
435
|
+
: null;
|
|
436
|
+
|
|
437
|
+
// Cosine channel (v0.8.5) — only when caller supplies an embedding.
|
|
438
|
+
// The embed cost is paid once in the write-pipeline pre-novelty and
|
|
439
|
+
// re-used for the engram's stored vector, so we don't double-embed.
|
|
440
|
+
let topCosine: { engramId: string; score: number; engram: any } | null = null;
|
|
441
|
+
if (embedding && embedding.length > 0) {
|
|
442
|
+
try {
|
|
443
|
+
const hits = await store.searchByVector(agentId, embedding, 3);
|
|
444
|
+
if (hits.length > 0) {
|
|
445
|
+
const h = hits[0];
|
|
446
|
+
// pgvector distance ≈ 1 - cosineSimilarity for unit-norm BGE vectors.
|
|
447
|
+
// SQLite searchByVector returns distance = 1 - sim in the same form.
|
|
448
|
+
// Clamp into [0, 1] to be safe with floating-point drift.
|
|
449
|
+
const sim = Math.max(0, Math.min(1, 1 - h.distance));
|
|
450
|
+
topCosine = { engramId: h.engram.id, score: sim, engram: h.engram };
|
|
451
|
+
}
|
|
452
|
+
} catch { /* cosine channel optional — fall back to BM25 alone */ }
|
|
306
453
|
}
|
|
307
454
|
|
|
308
|
-
|
|
309
|
-
|
|
455
|
+
// Combine: take the higher-confidence signal. If both fired and they
|
|
456
|
+
// identify the same engram, scores reinforce each other (we still take
|
|
457
|
+
// max, but the matched engram is the same). If they identify *different*
|
|
458
|
+
// engrams (one semantic match, one lexical), the higher score wins —
|
|
459
|
+
// typically the more discriminating signal for that particular content.
|
|
460
|
+
//
|
|
461
|
+
// Tested MIN and cosine-primary on 2026-05-26 to address PGlite token
|
|
462
|
+
// bloat; both dropped accuracy 7–20pp across backends. The bloat is a
|
|
463
|
+
// recall-output problem (returning full merged engram content when only
|
|
464
|
+
// a slice matches the query), not a novelty problem. Keeping MAX
|
|
465
|
+
// preserves the 100% / 97.5% accuracy we had on PGlite / SQLite.
|
|
466
|
+
let combinedTop: { engramId: string; score: number; engram: any } | null;
|
|
467
|
+
if (topCosine && topBm25) {
|
|
468
|
+
combinedTop = topCosine.score >= topBm25.score ? topCosine : topBm25;
|
|
469
|
+
} else if (topCosine) {
|
|
470
|
+
combinedTop = topCosine;
|
|
471
|
+
} else if (topBm25) {
|
|
472
|
+
combinedTop = topBm25;
|
|
473
|
+
} else {
|
|
474
|
+
return { novelty: 1.0, matchedEngramId: null, matchScore: 0 };
|
|
475
|
+
}
|
|
310
476
|
|
|
311
|
-
|
|
312
|
-
const top = allResults[0];
|
|
313
|
-
const topScore = top.bm25Score;
|
|
477
|
+
const topScore = combinedTop.score;
|
|
314
478
|
|
|
315
479
|
// Quadratic dampening — see computeNovelty for curve rationale
|
|
316
480
|
const baseNovelty = 1.0 - topScore * topScore;
|
|
317
481
|
|
|
318
|
-
// Recent-only concept penalty (30d window)
|
|
482
|
+
// Recent-only concept penalty (30d window). Check across all matches we
|
|
483
|
+
// saw on EITHER channel — exact-concept repeat counts as a near-duplicate
|
|
484
|
+
// regardless of which signal noticed it.
|
|
319
485
|
const conceptLower = conceptStr.toLowerCase().trim();
|
|
320
486
|
const cutoffMs = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
321
|
-
const
|
|
322
|
-
const eng = r.engram as { concept?: string; createdAt?: Date | string | number };
|
|
487
|
+
const checkExactConcept = (eng: { concept?: string; createdAt?: Date | string | number }): boolean => {
|
|
323
488
|
if (eng?.concept?.toLowerCase().trim() !== conceptLower) return false;
|
|
324
489
|
const created = eng?.createdAt;
|
|
325
490
|
if (!created) return true;
|
|
@@ -327,11 +492,13 @@ export function computeNoveltyWithMatch(
|
|
|
327
492
|
? created.getTime()
|
|
328
493
|
: typeof created === 'number' ? created : Date.parse(created);
|
|
329
494
|
return Number.isFinite(createdMs) && createdMs >= cutoffMs;
|
|
330
|
-
}
|
|
495
|
+
};
|
|
496
|
+
const exactConceptRecent = allBm25.some(r => checkExactConcept(r.engram))
|
|
497
|
+
|| (topCosine ? checkExactConcept(topCosine.engram) : false);
|
|
331
498
|
const conceptPenalty = exactConceptRecent ? 0.3 : 0;
|
|
332
499
|
|
|
333
500
|
const novelty = Math.max(0.05, Math.min(0.95, baseNovelty - conceptPenalty));
|
|
334
|
-
return { novelty, matchedEngramId:
|
|
501
|
+
return { novelty, matchedEngramId: combinedTop.engramId, matchScore: topScore };
|
|
335
502
|
} catch {
|
|
336
503
|
return { novelty: 0.8, matchedEngramId: null, matchScore: 0 };
|
|
337
504
|
}
|