agent-working-memory 0.7.2 → 0.7.4
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 +20 -2
- package/dist/api/routes.js +1 -1
- package/dist/cli.js +1 -1
- package/dist/coordination/index.d.ts.map +1 -1
- package/dist/coordination/index.js +18 -1
- package/dist/coordination/index.js.map +1 -1
- package/dist/coordination/routes.d.ts.map +1 -1
- package/dist/coordination/routes.js +152 -14
- package/dist/coordination/routes.js.map +1 -1
- package/dist/coordination/schemas.d.ts +18 -1
- package/dist/coordination/schemas.d.ts.map +1 -1
- package/dist/coordination/schemas.js +14 -2
- package/dist/coordination/schemas.js.map +1 -1
- package/dist/core/salience.d.ts +3 -1
- package/dist/core/salience.d.ts.map +1 -1
- package/dist/core/salience.js +90 -15
- package/dist/core/salience.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/mcp.js +2 -2
- package/package.json +1 -1
- package/src/api/routes.ts +1 -1
- package/src/cli.ts +1 -1
- package/src/coordination/index.ts +20 -1
- package/src/coordination/routes.ts +193 -15
- package/src/coordination/schemas.ts +17 -2
- package/src/core/salience.ts +88 -17
- package/src/index.ts +1 -1
- package/src/mcp.ts +2 -2
package/src/core/salience.ts
CHANGED
|
@@ -13,7 +13,33 @@
|
|
|
13
13
|
import type { SalienceFeatures, MemoryClass } from '../types/index.js';
|
|
14
14
|
import type { EngramStore } from '../storage/sqlite.js';
|
|
15
15
|
|
|
16
|
-
export type SalienceEventType = 'decision' | 'friction' | 'surprise' | 'causal' | 'observation';
|
|
16
|
+
export type SalienceEventType = 'decision' | 'friction' | 'surprise' | 'causal' | 'observation' | 'user_feedback';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Auto-detect user-feedback memories: content that begins with a known user's
|
|
20
|
+
* name + a feedback verb. These memories represent direct human decisions and
|
|
21
|
+
* must never be discarded. Examples:
|
|
22
|
+
* "Robert verbatim: 'LMS programs-first like CRM'"
|
|
23
|
+
* "Katherine said the CEC cycle resets on promotion"
|
|
24
|
+
* "Nancy directed Tier 1 has no grace period"
|
|
25
|
+
*
|
|
26
|
+
* Why this exists: the BM25 novelty check collapses near-duplicates regardless
|
|
27
|
+
* of whether the content is a NEW decision or a repeat observation. User
|
|
28
|
+
* feedback often shares terminology with prior memories ("LMS", "ECP",
|
|
29
|
+
* "officials") and gets discarded at salience 0.14 (verified in activity log
|
|
30
|
+
* 2026-05-06T19:08:47). Detecting "Robert said X" → canonical class bypasses
|
|
31
|
+
* the salience filter entirely.
|
|
32
|
+
*
|
|
33
|
+
* Tune the name list as new staff join. Pattern requires word boundary at
|
|
34
|
+
* start so "Roberta" or "Hannahs" don't match.
|
|
35
|
+
*/
|
|
36
|
+
const USER_FEEDBACK_PATTERN = /^(Robert|Katherine|Catherine|Nancy|Brandy|Brandi|Hannah|Marilyn|Kaylee|Pete|Abby|Tom|Wendy|Sita|Nick|Rob|Joan|Jennifer|Cindy|Jason|Alex|Molly)\s+(said|verbatim|feedback|asked|wants|prefers|requested|requested|directed|decided|confirmed|clarified|chose|specified|explained)\b/i;
|
|
37
|
+
|
|
38
|
+
/** Returns true if the content looks like direct user feedback that should auto-promote to canonical. */
|
|
39
|
+
export function detectUserFeedback(content: string): boolean {
|
|
40
|
+
if (typeof content !== 'string' || content.length === 0) return false;
|
|
41
|
+
return USER_FEEDBACK_PATTERN.test(content.trim());
|
|
42
|
+
}
|
|
17
43
|
|
|
18
44
|
export interface SalienceInput {
|
|
19
45
|
content: string;
|
|
@@ -56,15 +82,28 @@ export function evaluateSalience(
|
|
|
56
82
|
activeThreshold: number = 0.4,
|
|
57
83
|
stagingThreshold: number = 0.2
|
|
58
84
|
): SalienceResult {
|
|
85
|
+
// Auto-detect user feedback before scoring. If content matches the pattern,
|
|
86
|
+
// force eventType='user_feedback' and memoryClass='canonical'. This bypasses
|
|
87
|
+
// the BM25 novelty floor that was discarding pivotal user decisions at 0.14.
|
|
88
|
+
let resolvedEventType: SalienceEventType = input.eventType ?? 'observation';
|
|
89
|
+
let resolvedMemoryClass: MemoryClass = input.memoryClass ?? 'working';
|
|
90
|
+
let autoPromoted = false;
|
|
91
|
+
if (detectUserFeedback(input.content)) {
|
|
92
|
+
resolvedEventType = 'user_feedback';
|
|
93
|
+
resolvedMemoryClass = 'canonical';
|
|
94
|
+
autoPromoted = true;
|
|
95
|
+
}
|
|
96
|
+
|
|
59
97
|
const features: SalienceFeatures = {
|
|
60
98
|
surprise: input.surprise ?? 0,
|
|
61
99
|
decisionMade: input.decisionMade ?? false,
|
|
62
100
|
causalDepth: input.causalDepth ?? 0,
|
|
63
101
|
resolutionEffort: input.resolutionEffort ?? 0,
|
|
64
|
-
eventType:
|
|
102
|
+
eventType: resolvedEventType,
|
|
65
103
|
};
|
|
66
104
|
|
|
67
105
|
const reasonCodes: string[] = [];
|
|
106
|
+
if (autoPromoted) reasonCodes.push('auto:user_feedback');
|
|
68
107
|
|
|
69
108
|
// Novelty: 1.0 = completely new info, 0 = exact duplicate exists
|
|
70
109
|
// Default to 0.8 (assume mostly novel) when caller doesn't check
|
|
@@ -91,13 +130,14 @@ export function evaluateSalience(
|
|
|
91
130
|
case 'friction': typeBonus = 0.2; reasonCodes.push('event:friction'); break;
|
|
92
131
|
case 'surprise': typeBonus = 0.25; reasonCodes.push('event:surprise'); break;
|
|
93
132
|
case 'causal': typeBonus = 0.2; reasonCodes.push('event:causal'); break;
|
|
133
|
+
case 'user_feedback': typeBonus = 0.3; reasonCodes.push('event:user_feedback'); break;
|
|
94
134
|
case 'observation': break;
|
|
95
135
|
}
|
|
96
136
|
|
|
97
137
|
let score = Math.min(surpriseScore + decisionScore + causalScore + effortScore + noveltyScore + typeBonus, 1.0);
|
|
98
138
|
|
|
99
139
|
// Memory class overrides
|
|
100
|
-
const memoryClass =
|
|
140
|
+
const memoryClass = resolvedMemoryClass;
|
|
101
141
|
|
|
102
142
|
if (memoryClass === 'canonical') {
|
|
103
143
|
// Canonical memories: salience floor of 0.7, never go to staging
|
|
@@ -150,17 +190,35 @@ export function computeNovelty(store: EngramStore, agentId: string, concept: str
|
|
|
150
190
|
// Higher score = stronger match = less novel.
|
|
151
191
|
const topScore = results[0].bm25Score;
|
|
152
192
|
|
|
153
|
-
//
|
|
154
|
-
//
|
|
193
|
+
// Quadratic dampening (1 - topScore²) so mid-range matches don't kill novelty.
|
|
194
|
+
// Old curve was linear (1 - topScore) which floored at 0.1 for almost any match
|
|
195
|
+
// in a populated DB, killing the salience signal.
|
|
196
|
+
// Curve comparison (topScore → novelty):
|
|
197
|
+
// 0.30 → 0.91 (different topic — strong novelty)
|
|
198
|
+
// 0.60 → 0.64 (loosely related — partial credit)
|
|
199
|
+
// 0.80 → 0.36 (related but distinct — meaningful signal)
|
|
200
|
+
// 0.95 → 0.10 (near-dupe — still suppress)
|
|
201
|
+
const baseNovelty = 1.0 - topScore * topScore;
|
|
202
|
+
|
|
203
|
+
// Concept penalty scoped to recent matches only — re-using the same concept
|
|
204
|
+
// string for a NEW topic months later shouldn't be punished. Penalty was 0.4
|
|
205
|
+
// (too harsh); now 0.3 and only applies if any matched result is < 30 days old.
|
|
155
206
|
const conceptLower = conceptStr.toLowerCase().trim();
|
|
156
|
-
const
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
207
|
+
const cutoffMs = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
208
|
+
const exactConceptRecent = results.some(r => {
|
|
209
|
+
if (r.engram?.concept?.toLowerCase().trim() !== conceptLower) return false;
|
|
210
|
+
const created = r.engram?.createdAt as Date | string | number | undefined;
|
|
211
|
+
if (!created) return true; // No timestamp — treat as recent (conservative)
|
|
212
|
+
const createdMs = created instanceof Date
|
|
213
|
+
? created.getTime()
|
|
214
|
+
: typeof created === 'number' ? created : Date.parse(created);
|
|
215
|
+
return Number.isFinite(createdMs) && createdMs >= cutoffMs;
|
|
216
|
+
});
|
|
217
|
+
const conceptPenalty = exactConceptRecent ? 0.3 : 0;
|
|
218
|
+
|
|
219
|
+
// Floor lowered to 0.05 (was 0.10) so true duplicates can score near-zero
|
|
220
|
+
// and clearly stay below stagingThreshold (0.2). Ceiling unchanged.
|
|
221
|
+
return Math.max(0.05, Math.min(0.95, baseNovelty - conceptPenalty));
|
|
164
222
|
} catch {
|
|
165
223
|
// If BM25 search fails (e.g., FTS not ready), assume novel
|
|
166
224
|
return 0.8;
|
|
@@ -206,12 +264,25 @@ export function computeNoveltyWithMatch(
|
|
|
206
264
|
const top = allResults[0];
|
|
207
265
|
const topScore = top.bm25Score;
|
|
208
266
|
|
|
209
|
-
//
|
|
267
|
+
// Quadratic dampening — see computeNovelty for curve rationale
|
|
268
|
+
const baseNovelty = 1.0 - topScore * topScore;
|
|
269
|
+
|
|
270
|
+
// Recent-only concept penalty (30d window)
|
|
210
271
|
const conceptLower = conceptStr.toLowerCase().trim();
|
|
211
|
-
const
|
|
212
|
-
const
|
|
272
|
+
const cutoffMs = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
|
273
|
+
const exactConceptRecent = allResults.some(r => {
|
|
274
|
+
const eng = r.engram as { concept?: string; createdAt?: Date | string | number };
|
|
275
|
+
if (eng?.concept?.toLowerCase().trim() !== conceptLower) return false;
|
|
276
|
+
const created = eng?.createdAt;
|
|
277
|
+
if (!created) return true;
|
|
278
|
+
const createdMs = created instanceof Date
|
|
279
|
+
? created.getTime()
|
|
280
|
+
: typeof created === 'number' ? created : Date.parse(created);
|
|
281
|
+
return Number.isFinite(createdMs) && createdMs >= cutoffMs;
|
|
282
|
+
});
|
|
283
|
+
const conceptPenalty = exactConceptRecent ? 0.3 : 0;
|
|
213
284
|
|
|
214
|
-
const novelty = Math.max(0.
|
|
285
|
+
const novelty = Math.max(0.05, Math.min(0.95, baseNovelty - conceptPenalty));
|
|
215
286
|
return { novelty, matchedEngramId: top.engram.id, matchScore: topScore };
|
|
216
287
|
} catch {
|
|
217
288
|
return { novelty: 0.8, matchedEngramId: null, matchScore: 0 };
|
package/src/index.ts
CHANGED
|
@@ -177,7 +177,7 @@ async function main() {
|
|
|
177
177
|
|
|
178
178
|
// Start server
|
|
179
179
|
await app.listen({ port: PORT, host: '0.0.0.0' });
|
|
180
|
-
console.log(`AgentWorkingMemory v0.7.
|
|
180
|
+
console.log(`AgentWorkingMemory v0.7.4 listening on port ${PORT}`);
|
|
181
181
|
|
|
182
182
|
// Graceful shutdown
|
|
183
183
|
const shutdown = async () => {
|
package/src/mcp.ts
CHANGED
|
@@ -78,7 +78,7 @@ const INCOGNITO = process.env.AWM_INCOGNITO === '1' || process.env.AWM_INCOGNITO
|
|
|
78
78
|
|
|
79
79
|
if (INCOGNITO) {
|
|
80
80
|
console.error('AWM: incognito mode — all memory tools disabled, nothing will be recorded');
|
|
81
|
-
const server = new McpServer({ name: 'agent-working-memory', version: '0.7.
|
|
81
|
+
const server = new McpServer({ name: 'agent-working-memory', version: '0.7.4' });
|
|
82
82
|
const transport = new StdioServerTransport();
|
|
83
83
|
server.connect(transport).catch(err => {
|
|
84
84
|
console.error('MCP server failed:', err);
|
|
@@ -115,7 +115,7 @@ let coordDb: import('better-sqlite3').Database | null = null;
|
|
|
115
115
|
|
|
116
116
|
const server = new McpServer({
|
|
117
117
|
name: 'agent-working-memory',
|
|
118
|
-
version: '0.7.
|
|
118
|
+
version: '0.7.4',
|
|
119
119
|
});
|
|
120
120
|
|
|
121
121
|
server.registerResource(
|