agent-working-memory 0.14.1 → 0.14.6
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 +240 -653
- package/dist/adapters/claude-code.d.ts.map +1 -1
- package/dist/adapters/claude-code.js +234 -131
- package/dist/adapters/claude-code.js.map +1 -1
- package/dist/adapters/common.d.ts +44 -5
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +111 -41
- package/dist/adapters/common.js.map +1 -1
- package/dist/adapters/hook-scripts.d.ts +54 -0
- package/dist/adapters/hook-scripts.d.ts.map +1 -0
- package/dist/adapters/hook-scripts.js +340 -0
- package/dist/adapters/hook-scripts.js.map +1 -0
- package/dist/adapters/types.d.ts +13 -3
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/cli.js +21 -5
- package/dist/cli.js.map +1 -1
- package/dist/core/agent-id.d.ts +19 -0
- package/dist/core/agent-id.d.ts.map +1 -0
- package/dist/core/agent-id.js +28 -0
- package/dist/core/agent-id.js.map +1 -0
- package/dist/core/whoami.d.ts +9 -1
- package/dist/core/whoami.d.ts.map +1 -1
- package/dist/core/whoami.js +12 -2
- package/dist/core/whoami.js.map +1 -1
- package/dist/engine/activation.d.ts +2 -0
- package/dist/engine/activation.d.ts.map +1 -1
- package/dist/engine/activation.js +18 -3
- package/dist/engine/activation.js.map +1 -1
- package/dist/engine/eval.d.ts +15 -0
- package/dist/engine/eval.d.ts.map +1 -1
- package/dist/engine/eval.js +23 -0
- package/dist/engine/eval.js.map +1 -1
- package/dist/hooks/prime.d.ts +10 -4
- package/dist/hooks/prime.d.ts.map +1 -1
- package/dist/hooks/prime.js +16 -6
- package/dist/hooks/prime.js.map +1 -1
- package/dist/hooks/sidecar.d.ts +18 -2
- package/dist/hooks/sidecar.d.ts.map +1 -1
- package/dist/hooks/sidecar.js +30 -4
- package/dist/hooks/sidecar.js.map +1 -1
- package/dist/mcp.js +48 -19
- package/dist/mcp.js.map +1 -1
- package/dist/storage/pglite.d.ts +7 -0
- package/dist/storage/pglite.d.ts.map +1 -1
- package/dist/storage/pglite.js +17 -3
- package/dist/storage/pglite.js.map +1 -1
- package/dist/storage/postgres.d.ts +7 -0
- package/dist/storage/postgres.d.ts.map +1 -1
- package/dist/storage/postgres.js +17 -3
- package/dist/storage/postgres.js.map +1 -1
- package/dist/storage/sqlite.d.ts +12 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +24 -3
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/types/engram.d.ts +16 -0
- package/dist/types/engram.d.ts.map +1 -1
- package/dist/types/eval.d.ts +2 -0
- package/dist/types/eval.d.ts.map +1 -1
- package/package.json +94 -72
- package/src/adapters/claude-code.ts +223 -125
- package/src/adapters/common.ts +117 -42
- package/src/adapters/hook-scripts.ts +370 -0
- package/src/adapters/types.ts +14 -3
- package/src/cli.ts +19 -5
- package/src/core/agent-id.ts +28 -0
- package/src/core/whoami.ts +11 -1
- package/src/engine/activation.ts +18 -3
- package/src/engine/eval.ts +34 -0
- package/src/hooks/prime.ts +22 -7
- package/src/hooks/sidecar.ts +50 -6
- package/src/mcp.ts +50 -18
- package/src/storage/pglite.ts +22 -4
- package/src/storage/postgres.ts +22 -4
- package/src/storage/sqlite.ts +26 -4
- package/src/types/engram.ts +16 -0
- package/src/types/eval.ts +3 -1
package/src/adapters/types.ts
CHANGED
|
@@ -28,8 +28,12 @@ export interface SetupContext {
|
|
|
28
28
|
hasDist: boolean;
|
|
29
29
|
/** Hook sidecar secret token */
|
|
30
30
|
hookSecret: string;
|
|
31
|
-
/** Hook sidecar port */
|
|
31
|
+
/** Hook sidecar port (preferred; the sidecar walks upward from here when busy) */
|
|
32
32
|
hookPort: string;
|
|
33
|
+
/** How many ports upward from hookPort the sidecar may try (0.14.2) */
|
|
34
|
+
hookPortRange: string;
|
|
35
|
+
/** Install the UserPromptSubmit prime hook (0.14.6; `--no-prime` turns it off) */
|
|
36
|
+
installPrime: boolean;
|
|
33
37
|
/** Whether to use global scope */
|
|
34
38
|
isGlobal: boolean;
|
|
35
39
|
/** Windows platform */
|
|
@@ -70,6 +74,13 @@ export interface CLIAdapter {
|
|
|
70
74
|
/** Configure hooks (if supported). Returns action summary. */
|
|
71
75
|
writeHooks(ctx: SetupContext, skip: boolean): string;
|
|
72
76
|
|
|
73
|
-
/** Validate that the setup is healthy. */
|
|
74
|
-
diagnose(ctx: SetupContext): DiagnosticResult[]
|
|
77
|
+
/** Validate that the setup is healthy. May probe the network, hence possibly async. */
|
|
78
|
+
diagnose(ctx: SetupContext): DiagnosticResult[] | Promise<DiagnosticResult[]>;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The env block of an AWM server entry this adapter previously wrote, if any.
|
|
82
|
+
* `awm setup` re-runs preserve these values unless a flag overrides them, so an
|
|
83
|
+
* upgrade never repoints the database or renames the agent (0.14.6).
|
|
84
|
+
*/
|
|
85
|
+
readExistingEnv?(isGlobal: boolean, cwd: string): Record<string, string> | null;
|
|
75
86
|
}
|
package/src/cli.ts
CHANGED
|
@@ -74,7 +74,10 @@ Setup options:
|
|
|
74
74
|
--no-instructions Skip instruction file (CLAUDE.md, AGENTS.md, etc.)
|
|
75
75
|
--no-claude-md Alias for --no-instructions
|
|
76
76
|
--no-hooks Skip hook installation
|
|
77
|
-
--
|
|
77
|
+
--no-prime Skip the UserPromptSubmit prime hook (auto-inject memories per prompt)
|
|
78
|
+
--hook-port PORT Preferred sidecar port (default: 8401; each session walks upward when busy)
|
|
79
|
+
--hook-port-range N Ports to try upward from --hook-port (default: 10)
|
|
80
|
+
Re-running setup keeps your existing agent id, db path, port and any env you added.
|
|
78
81
|
|
|
79
82
|
Examples:
|
|
80
83
|
awm setup --global Claude Code, global (recommended)
|
|
@@ -95,7 +98,9 @@ async function setup() {
|
|
|
95
98
|
let skipInstructions = false;
|
|
96
99
|
let isGlobal = false;
|
|
97
100
|
let skipHooks = false;
|
|
98
|
-
let
|
|
101
|
+
let installPrime = true;
|
|
102
|
+
let hookPort: string | undefined;
|
|
103
|
+
let hookPortRange: string | undefined;
|
|
99
104
|
|
|
100
105
|
for (let i = 1; i < args.length; i++) {
|
|
101
106
|
if (args[i] === '--agent-id' && args[i + 1]) {
|
|
@@ -106,8 +111,12 @@ async function setup() {
|
|
|
106
111
|
skipInstructions = true;
|
|
107
112
|
} else if (args[i] === '--no-hooks') {
|
|
108
113
|
skipHooks = true;
|
|
114
|
+
} else if (args[i] === '--no-prime') {
|
|
115
|
+
installPrime = false;
|
|
109
116
|
} else if (args[i] === '--hook-port' && args[i + 1]) {
|
|
110
117
|
hookPort = args[++i];
|
|
118
|
+
} else if (args[i] === '--hook-port-range' && args[i + 1]) {
|
|
119
|
+
hookPortRange = args[++i];
|
|
111
120
|
} else if (args[i] === '--global') {
|
|
112
121
|
isGlobal = true;
|
|
113
122
|
} else if (!args[i].startsWith('--')) {
|
|
@@ -133,8 +142,10 @@ async function setup() {
|
|
|
133
142
|
isGlobal = true;
|
|
134
143
|
}
|
|
135
144
|
|
|
136
|
-
// Build context
|
|
137
|
-
|
|
145
|
+
// Build context. A previous install's values survive unless a flag overrides them
|
|
146
|
+
// (0.14.6) — an upgrade must never repoint the database or rename the agent.
|
|
147
|
+
const existingEnv = adapter.readExistingEnv?.(isGlobal, process.cwd()) ?? null;
|
|
148
|
+
const ctx = buildSetupContext({ agentId, dbPath, isGlobal, hookPort, hookPortRange, installPrime, existingEnv });
|
|
138
149
|
|
|
139
150
|
// Run adapter
|
|
140
151
|
const configAction = adapter.writeMcpConfig(ctx);
|
|
@@ -157,6 +168,7 @@ AWM configured for ${adapter.name}${isGlobal ? ' (global)' : ''}
|
|
|
157
168
|
Next steps:
|
|
158
169
|
1. Restart ${adapter.name} to pick up the MCP server
|
|
159
170
|
2. Memory tools will appear automatically${adapter.id === 'codex' ? ' (verify with /mcp)' : ''}
|
|
171
|
+
3. \`awm doctor\` checks the install and lists the live sidecars
|
|
160
172
|
`.trim());
|
|
161
173
|
}
|
|
162
174
|
|
|
@@ -197,7 +209,9 @@ async function doctor() {
|
|
|
197
209
|
}
|
|
198
210
|
|
|
199
211
|
console.log(` ${adapter.name}:`);
|
|
200
|
-
const
|
|
212
|
+
const existingEnv = adapter.readExistingEnv?.(true, process.cwd()) ?? null;
|
|
213
|
+
const ctx = buildSetupContext({ isGlobal: true, existingEnv });
|
|
214
|
+
const results = await adapter.diagnose(ctx);
|
|
201
215
|
for (const r of results) {
|
|
202
216
|
const icon = r.status === 'ok' ? '+' : r.status === 'warn' ? '~' : 'x';
|
|
203
217
|
console.log(` [${icon}] ${r.check}: ${r.message}`);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Copyright 2026 Robert Winter / Complete Ideas
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fallback agent identity when AWM_AGENT_ID / WORKER_NAME are unset.
|
|
6
|
+
*
|
|
7
|
+
* Derived from the project directory so a plain `claude` launch still binds to
|
|
8
|
+
* the right store: `Personal-Projects` → 'personal'; everything else → 'work'.
|
|
9
|
+
* The AWM package itself lives under Personal-Projects, so an AgentSynapse
|
|
10
|
+
* checkout is guarded first — a stray server cwd must not mis-bind.
|
|
11
|
+
*
|
|
12
|
+
* ONE definition, three consumers that must agree:
|
|
13
|
+
* - src/mcp.ts — what the server actually binds to
|
|
14
|
+
* - src/adapters/common.ts — what `awm setup` reports and seeds
|
|
15
|
+
* - the shipped hook scripts (src/adapters/hook-scripts.ts) carry a
|
|
16
|
+
* line-for-line JavaScript mirror, because a hook cannot import from dist.
|
|
17
|
+
* tests/adapters/hook-scripts.test.ts asserts the mirror agrees with this.
|
|
18
|
+
*/
|
|
19
|
+
export function deriveAgentFromDir(dir: string): string {
|
|
20
|
+
const d = String(dir ?? '').replace(/\\/g, '/');
|
|
21
|
+
if (/\/AgentSynapse\//i.test(d)) return 'work';
|
|
22
|
+
return /\/Personal-Projects(\/|$)/i.test(d) ? 'personal' : 'work';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The agent id a server process would resolve to for `dir`, honouring the env first. */
|
|
26
|
+
export function resolveAgentId(dir: string, env: NodeJS.ProcessEnv = process.env): string {
|
|
27
|
+
return env.AWM_AGENT_ID ?? env.WORKER_NAME ?? deriveAgentFromDir(dir);
|
|
28
|
+
}
|
package/src/core/whoami.ts
CHANGED
|
@@ -59,6 +59,14 @@ export async function buildWhoami(
|
|
|
59
59
|
store: WhoamiStore,
|
|
60
60
|
agentId: string,
|
|
61
61
|
surface: 'mcp' | 'http',
|
|
62
|
+
/**
|
|
63
|
+
* 0.14.2: the port the hook sidecar actually BOUND, from the running handle.
|
|
64
|
+
* Before this, whoami echoed AWM_HOOK_PORT from the environment — so a session
|
|
65
|
+
* whose sidecar had lost the port fight and disabled hooks still reported
|
|
66
|
+
* "hookSidecar=8401". Pass `null` for "tried and failed"; omit (undefined)
|
|
67
|
+
* for surfaces that have no sidecar, which falls back to the env value.
|
|
68
|
+
*/
|
|
69
|
+
boundHookPort?: number | null,
|
|
62
70
|
): Promise<WhoamiInfo> {
|
|
63
71
|
const coordination = process.env.AWM_COORDINATION === 'true' || process.env.AWM_COORDINATION === '1';
|
|
64
72
|
let siblings: string[] = [];
|
|
@@ -80,7 +88,9 @@ export async function buildWhoami(
|
|
|
80
88
|
pid: process.pid,
|
|
81
89
|
ports: {
|
|
82
90
|
http: Number.isFinite(httpPort) && httpPort > 0 ? httpPort : null,
|
|
83
|
-
hookSidecar:
|
|
91
|
+
hookSidecar: boundHookPort !== undefined
|
|
92
|
+
? boundHookPort
|
|
93
|
+
: (Number.isFinite(hookPort) && hookPort > 0 ? hookPort : null),
|
|
84
94
|
},
|
|
85
95
|
recall: { fingerprint: recallConfigFingerprint(), flags: activeRecallConfig() },
|
|
86
96
|
siblingAgents: siblings,
|
package/src/engine/activation.ts
CHANGED
|
@@ -502,8 +502,11 @@ export class ActivationEngine {
|
|
|
502
502
|
// top-N (~30 candidates) — its on-demand `getAssociationsFor` lookups are
|
|
503
503
|
// cheap (<5ms total).
|
|
504
504
|
const assocStats = await this.store.getAssociationStatsForBatch(candidates.map(e => e.id));
|
|
505
|
+
// 0.14.4: decay clock is overridable (query.now) so evals against a frozen
|
|
506
|
+
// snapshot are reproducible across days. See ActivationQuery.now.
|
|
507
|
+
const nowMs = query.now ?? Date.now();
|
|
505
508
|
const scored = candidates.map(engram => {
|
|
506
|
-
const ageDays = (
|
|
509
|
+
const ageDays = (nowMs - engram.createdAt.getTime()) / (1000 * 60 * 60 * 24);
|
|
507
510
|
const stats = assocStats.get(engram.id) ?? { count: 0, sumWeight: 0 };
|
|
508
511
|
|
|
509
512
|
// --- Text relevance (keyword signals) ---
|
|
@@ -638,7 +641,7 @@ export class ActivationEngine {
|
|
|
638
641
|
candidateMap.set(r.engram.id, r.engram);
|
|
639
642
|
// Score the new candidate
|
|
640
643
|
const engram = r.engram;
|
|
641
|
-
const ageDays = (
|
|
644
|
+
const ageDays = (nowMs - engram.createdAt.getTime()) / (1000 * 60 * 60 * 24);
|
|
642
645
|
const associations = await this.store.getAssociationsFor(engram.id);
|
|
643
646
|
const cTokens = tokenize(engram.concept);
|
|
644
647
|
const ctTokens = tokenize(engram.content);
|
|
@@ -1222,8 +1225,9 @@ export class ActivationEngine {
|
|
|
1222
1225
|
|
|
1223
1226
|
// Log activation event for eval
|
|
1224
1227
|
const latencyMs = performance.now() - startTime;
|
|
1228
|
+
const eventId = randomUUID();
|
|
1225
1229
|
await this.store.logActivationEvent({
|
|
1226
|
-
id:
|
|
1230
|
+
id: eventId,
|
|
1227
1231
|
agentId: query.agentId,
|
|
1228
1232
|
timestamp: new Date(),
|
|
1229
1233
|
context: query.context,
|
|
@@ -1232,11 +1236,22 @@ export class ActivationEngine {
|
|
|
1232
1236
|
latencyMs,
|
|
1233
1237
|
engramIds: activatedIds,
|
|
1234
1238
|
});
|
|
1239
|
+
// 0.14.3: expose the event id so callers can hand it back on
|
|
1240
|
+
// memory_feedback. Before this, the id was generated here and dropped —
|
|
1241
|
+
// every one of the 872 retrieval_feedback rows in the live store has
|
|
1242
|
+
// activation_event_id = NULL, so feedback could never be joined to the
|
|
1243
|
+
// recall it judged. Stamped on each result (same value across the set,
|
|
1244
|
+
// like `confidence`) so the MCP layer can render it once in the footer.
|
|
1245
|
+
for (const r of results) r.activationEventId = eventId;
|
|
1246
|
+
this.lastActivationEventId = eventId;
|
|
1235
1247
|
}
|
|
1236
1248
|
|
|
1237
1249
|
return results;
|
|
1238
1250
|
}
|
|
1239
1251
|
|
|
1252
|
+
/** Id of the most recent non-internal activation this engine logged (0.14.3). */
|
|
1253
|
+
lastActivationEventId: string | null = null;
|
|
1254
|
+
|
|
1240
1255
|
/**
|
|
1241
1256
|
* Multi-graph traversal (MAGMA-inspired).
|
|
1242
1257
|
*
|
package/src/engine/eval.ts
CHANGED
|
@@ -75,6 +75,8 @@ export class EvalEngine {
|
|
|
75
75
|
activationCount: activationStats.count,
|
|
76
76
|
avgPrecisionAtK: precision,
|
|
77
77
|
avgLatencyMs: activationStats.avgLatencyMs,
|
|
78
|
+
p50LatencyMs: activationStats.p50LatencyMs,
|
|
79
|
+
p90LatencyMs: activationStats.p90LatencyMs,
|
|
78
80
|
p95LatencyMs: activationStats.p95LatencyMs,
|
|
79
81
|
|
|
80
82
|
totalEdges: allAssociations.length,
|
|
@@ -104,4 +106,36 @@ export class EvalEngine {
|
|
|
104
106
|
Math.max(activeEngrams.length + retractedEngrams.length, 1),
|
|
105
107
|
};
|
|
106
108
|
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* 0.14.3: outcome-shaped usage numbers for memory_stats. Each of these can
|
|
112
|
+
* move in BOTH directions, unlike edge utility. Cheap: one pass over the
|
|
113
|
+
* agent's engrams plus two count queries.
|
|
114
|
+
*/
|
|
115
|
+
async computeUsage(agentId: string): Promise<UsageMetrics> {
|
|
116
|
+
const active = await this.store.getEngramsByAgent(agentId, 'active');
|
|
117
|
+
const since30 = new Date(Date.now() - 30 * 24 * 3600_000);
|
|
118
|
+
const writes30d = active.filter(e => e.createdAt >= since30).length;
|
|
119
|
+
const neverRecalled = active.filter(e => e.accessCount === 0).length;
|
|
120
|
+
const act30 = await this.store.getActivationStats(agentId, 30 * 24);
|
|
121
|
+
const fb = await this.store.getLinkedFeedbackStats(agentId, 7 * 24);
|
|
122
|
+
return {
|
|
123
|
+
writes30d,
|
|
124
|
+
recalls30d: act30.count,
|
|
125
|
+
recallsPerWrite30d: writes30d > 0 ? act30.count / writes30d : 0,
|
|
126
|
+
neverRecalledShare: active.length > 0 ? neverRecalled / active.length : 0,
|
|
127
|
+
feedbackLinked7d: fb.total,
|
|
128
|
+
usefulShare7d: fb.total > 0 ? fb.useful / fb.total : 0,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface UsageMetrics {
|
|
134
|
+
writes30d: number;
|
|
135
|
+
recalls30d: number;
|
|
136
|
+
recallsPerWrite30d: number;
|
|
137
|
+
neverRecalledShare: number;
|
|
138
|
+
/** feedback rows in the window that carry an activation_event_id (0.14.3+) */
|
|
139
|
+
feedbackLinked7d: number;
|
|
140
|
+
usefulShare7d: number;
|
|
107
141
|
}
|
package/src/hooks/prime.ts
CHANGED
|
@@ -36,7 +36,12 @@ import { packRecallByBudget, estimateTokens, type PackedRecall } from '../core/t
|
|
|
36
36
|
|
|
37
37
|
/** Shape returned by the sidecar's `activate` dependency. */
|
|
38
38
|
export interface PrimeCandidate {
|
|
39
|
-
engram: {
|
|
39
|
+
engram: {
|
|
40
|
+
id: string; concept: string; content: string; memoryClass?: string;
|
|
41
|
+
validTo?: string | null;
|
|
42
|
+
/** Used for the age label; Date from the store, string over HTTP. */
|
|
43
|
+
createdAt?: Date | string | null;
|
|
44
|
+
};
|
|
40
45
|
score: number;
|
|
41
46
|
summary?: string;
|
|
42
47
|
confidence?: number;
|
|
@@ -64,21 +69,31 @@ export interface PrimeResult {
|
|
|
64
69
|
export const PRIME_DEFAULTS = {
|
|
65
70
|
maxTokens: 600,
|
|
66
71
|
/**
|
|
67
|
-
* 0.25
|
|
68
|
-
*
|
|
69
|
-
*
|
|
72
|
+
* 0.10 (was 0.25 until 0.14.6). The confidence this gates on describes the
|
|
73
|
+
* SHAPE of the score distribution — how far the top results stand out from
|
|
74
|
+
* the rest — not relevance. Measured 2026-09-11 on a 30k-memory store: at
|
|
75
|
+
* 0.25 two specific prompts with one clear winner (top score 0.26 and 0.38)
|
|
76
|
+
* were silenced while a vague prompt passed at 0.92. Those specific prompts
|
|
77
|
+
* are exactly what priming exists for. 0.10 passed all three and still
|
|
78
|
+
* abstains on the garbage case (confidence ≈ 0.05).
|
|
70
79
|
*/
|
|
71
|
-
minConfidence: 0.
|
|
80
|
+
minConfidence: 0.10,
|
|
72
81
|
minScore: 0.10,
|
|
73
82
|
} as const;
|
|
74
83
|
|
|
75
84
|
/** One injected line. Deliberately terser than the MCP recall format — this is
|
|
76
85
|
* context the agent didn't ask for, so it should read as a brief note, not a
|
|
77
86
|
* report. The id is retained so the agent can act on it (feedback, supersede). */
|
|
78
|
-
function formatPrimeLine(c: PrimeCandidate): string {
|
|
87
|
+
function formatPrimeLine(c: PrimeCandidate, now: number = Date.now()): string {
|
|
79
88
|
const body = c.summary ?? c.engram.content;
|
|
80
89
|
const validity = c.engram.validTo ? ` [valid until ${c.engram.validTo}]` : '';
|
|
81
|
-
|
|
90
|
+
// Class + age make the volatility rubric mechanical: a 90-day-old working
|
|
91
|
+
// memory reads differently from a 2-day-old canonical one, and the agent
|
|
92
|
+
// should not have to look either up before deciding how much to trust it.
|
|
93
|
+
const cls = c.engram.memoryClass ?? 'working';
|
|
94
|
+
const created = c.engram.createdAt ? new Date(c.engram.createdAt as any).getTime() : NaN;
|
|
95
|
+
const age = Number.isFinite(created) ? ` · ${Math.max(0, Math.round((now - created) / 86_400_000))}d` : '';
|
|
96
|
+
return `- [${cls}${age}] ${c.engram.concept}${validity} [${c.engram.id}]: ${body}`;
|
|
82
97
|
}
|
|
83
98
|
|
|
84
99
|
/**
|
package/src/hooks/sidecar.ts
CHANGED
|
@@ -27,7 +27,20 @@ export interface SidecarDeps {
|
|
|
27
27
|
store: EngramStore;
|
|
28
28
|
agentId: string;
|
|
29
29
|
secret: string | null;
|
|
30
|
+
/** Preferred port. If busy, the sidecar walks upward (see `portRange`). */
|
|
30
31
|
port: number;
|
|
32
|
+
/**
|
|
33
|
+
* 0.14.2: how many consecutive ports to try starting at `port` (default 10).
|
|
34
|
+
* Every Claude Code session spawns its own MCP process, and each one used to
|
|
35
|
+
* ask for exactly 8401; on EADDRINUSE the sidecar logged "hooks disabled" and
|
|
36
|
+
* gave up, so with N concurrent sessions only the first had working hooks —
|
|
37
|
+
* and `memory_whoami` still reported the configured port as if it were bound.
|
|
38
|
+
* Observed 2026-09-11: 4 of 5 live sessions had no sidecar at all.
|
|
39
|
+
* Hook clients probe the same range and match on `/health.agentId`.
|
|
40
|
+
*/
|
|
41
|
+
portRange?: number;
|
|
42
|
+
/** Reported by /health so hook clients can prefer the newest build. */
|
|
43
|
+
version?: string;
|
|
31
44
|
onConsolidate?: (agentId: string, reason: string) => void;
|
|
32
45
|
/**
|
|
33
46
|
* 0.12.2: warm recall for hooks. The sidecar runs in the same process as
|
|
@@ -157,8 +170,16 @@ function json(res: ServerResponse, status: number, body: Record<string, unknown>
|
|
|
157
170
|
|
|
158
171
|
const AUTO_CHECKPOINT_INTERVAL_MS = 15 * 60_000; // 15 minutes
|
|
159
172
|
|
|
160
|
-
export
|
|
173
|
+
export interface SidecarHandle {
|
|
174
|
+
close: () => void;
|
|
175
|
+
/** The port actually bound, or null while binding / if every port in range was busy. */
|
|
176
|
+
boundPort: () => number | null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function startSidecar(deps: SidecarDeps): SidecarHandle {
|
|
161
180
|
const { store, agentId, secret, port, onConsolidate } = deps;
|
|
181
|
+
const portRange = Math.max(1, deps.portRange ?? 10);
|
|
182
|
+
let bound: number | null = null;
|
|
162
183
|
|
|
163
184
|
const server = createServer(async (req, res) => {
|
|
164
185
|
// CORS preflight
|
|
@@ -170,7 +191,12 @@ export function startSidecar(deps: SidecarDeps): { close: () => void } {
|
|
|
170
191
|
|
|
171
192
|
// Health check — no auth required
|
|
172
193
|
if (req.url === '/health' && req.method === 'GET') {
|
|
173
|
-
|
|
194
|
+
// port/pid/version let a hook that probes 8401..841x pick the right
|
|
195
|
+
// process: same agentId as its session, newest version on a tie.
|
|
196
|
+
json(res, 200, {
|
|
197
|
+
status: 'ok', sidecar: true, agentId,
|
|
198
|
+
port: bound, pid: process.pid, version: deps.version ?? null,
|
|
199
|
+
});
|
|
174
200
|
return;
|
|
175
201
|
}
|
|
176
202
|
|
|
@@ -340,17 +366,34 @@ export function startSidecar(deps: SidecarDeps): { close: () => void } {
|
|
|
340
366
|
json(res, 404, { error: 'Not found' });
|
|
341
367
|
});
|
|
342
368
|
|
|
343
|
-
|
|
344
|
-
|
|
369
|
+
// Bind to the first free port in [port, port + portRange). A listen error
|
|
370
|
+
// fires 'error' before 'listening', so we re-listen on the next candidate
|
|
371
|
+
// from the error handler; any other error, or exhausting the range, leaves
|
|
372
|
+
// the sidecar unbound (hooks disabled) but never takes the MCP server down.
|
|
373
|
+
let attempt = 0;
|
|
374
|
+
const tryListen = () => {
|
|
375
|
+
const candidate = port + attempt;
|
|
376
|
+
server.listen(candidate, '127.0.0.1');
|
|
377
|
+
};
|
|
378
|
+
server.on('listening', () => {
|
|
379
|
+
const addr = server.address();
|
|
380
|
+
bound = addr && typeof addr === 'object' ? addr.port : port + attempt;
|
|
381
|
+
const note = bound === port ? '' : ` (preferred ${port} was busy)`;
|
|
382
|
+
console.error(`AWM hook sidecar listening on 127.0.0.1:${bound}${note}`);
|
|
345
383
|
});
|
|
346
|
-
|
|
347
384
|
server.on('error', (err: NodeJS.ErrnoException) => {
|
|
385
|
+
if (err.code === 'EADDRINUSE' && attempt + 1 < portRange) {
|
|
386
|
+
attempt++;
|
|
387
|
+
tryListen();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
348
390
|
if (err.code === 'EADDRINUSE') {
|
|
349
|
-
console.error(`AWM hook sidecar:
|
|
391
|
+
console.error(`AWM hook sidecar: ports ${port}-${port + portRange - 1} all in use, hooks disabled`);
|
|
350
392
|
} else {
|
|
351
393
|
console.error('AWM hook sidecar error:', err.message);
|
|
352
394
|
}
|
|
353
395
|
});
|
|
396
|
+
tryListen();
|
|
354
397
|
|
|
355
398
|
// --- Silent auto-checkpoint every 15 minutes ---
|
|
356
399
|
const autoCheckpointTimer = setInterval(async () => {
|
|
@@ -383,5 +426,6 @@ export function startSidecar(deps: SidecarDeps): { close: () => void } {
|
|
|
383
426
|
clearInterval(autoCheckpointTimer);
|
|
384
427
|
server.close();
|
|
385
428
|
},
|
|
429
|
+
boundPort: () => bound,
|
|
386
430
|
};
|
|
387
431
|
}
|
package/src/mcp.ts
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
import { readFileSync } from 'node:fs';
|
|
33
|
+
import { deriveAgentFromDir as deriveAgentFromDirShared } from './core/agent-id.js';
|
|
33
34
|
import { resolve, basename } from 'node:path';
|
|
34
35
|
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
35
36
|
|
|
@@ -111,19 +112,21 @@ const BACKEND: StoreBackend = getConfiguredBackend();
|
|
|
111
112
|
const DB_PATH = process.env.AWM_DB_PATH ?? (BACKEND === 'pglite' ? 'memory-pglite' : 'memory.db');
|
|
112
113
|
|
|
113
114
|
// Fallback agent selection when AWM_AGENT_ID/WORKER_NAME are unset: derive from
|
|
114
|
-
// the project directory
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
// server's reads/writes never diverge. Guard the AWM package's own path
|
|
119
|
-
// (it lives under Personal-Projects) so a stray server cwd can't mis-bind.
|
|
120
|
-
function deriveAgentFromDir(): string {
|
|
121
|
-
const dir = (process.env.CLAUDE_PROJECT_DIR ?? process.cwd()).replace(/\\/g, '/');
|
|
122
|
-
if (/\/AgentSynapse\//i.test(dir)) return 'work';
|
|
123
|
-
return /\/Personal-Projects(\/|$)/i.test(dir) ? 'personal' : 'work';
|
|
124
|
-
}
|
|
115
|
+
// the project directory (core/agent-id.ts — one definition shared with `awm setup`
|
|
116
|
+
// and mirrored in the shipped hook scripts, so the server, the installer and the
|
|
117
|
+
// hooks always name the same pool for a given directory).
|
|
118
|
+
const deriveAgentFromDir = () => deriveAgentFromDirShared(process.env.CLAUDE_PROJECT_DIR ?? process.cwd());
|
|
125
119
|
const AGENT_ID = process.env.AWM_AGENT_ID ?? process.env.WORKER_NAME ?? deriveAgentFromDir();
|
|
126
120
|
const HOOK_PORT = parseInt(process.env.AWM_HOOK_PORT ?? '8401', 10);
|
|
121
|
+
// 0.14.2: ports to try upward from HOOK_PORT when it is busy (see sidecar.ts).
|
|
122
|
+
const HOOK_PORT_RANGE = Math.max(1, parseInt(process.env.AWM_HOOK_PORT_RANGE ?? '10', 10) || 10);
|
|
123
|
+
// Set once the sidecar starts; whoami reads the bound port from it.
|
|
124
|
+
let sidecarHandle: { boundPort: () => number | null } | null = null;
|
|
125
|
+
function sidecarPortLabel(): string {
|
|
126
|
+
const p = sidecarHandle?.boundPort() ?? null;
|
|
127
|
+
if (p === null) return `not bound (preferred ${HOOK_PORT}; hooks disabled)`;
|
|
128
|
+
return p === HOOK_PORT ? `127.0.0.1:${p}` : `127.0.0.1:${p} (preferred ${HOOK_PORT} was busy)`;
|
|
129
|
+
}
|
|
127
130
|
const HOOK_SECRET = process.env.AWM_HOOK_SECRET ?? null;
|
|
128
131
|
|
|
129
132
|
initLogger(DB_PATH);
|
|
@@ -521,10 +524,15 @@ Returns the most relevant memories ranked by text relevance, temporal recency, a
|
|
|
521
524
|
};
|
|
522
525
|
}
|
|
523
526
|
|
|
527
|
+
// 0.14.3: surface the activation event id once, so memory_feedback can join
|
|
528
|
+
// to this recall. Rendered after the token footer; cheap (one short line).
|
|
529
|
+
const evId = results[0]?.activationEventId;
|
|
530
|
+
const evFooter = evId ? `\n[recall_id: ${evId}]` : '';
|
|
531
|
+
|
|
524
532
|
return {
|
|
525
533
|
content: [{
|
|
526
534
|
type: 'text' as const,
|
|
527
|
-
text: packed.lines.join('\n') + peerSuffix + formatTokenFooter(packed, params.max_tokens),
|
|
535
|
+
text: packed.lines.join('\n') + peerSuffix + formatTokenFooter(packed, params.max_tokens) + evFooter,
|
|
528
536
|
}],
|
|
529
537
|
};
|
|
530
538
|
}
|
|
@@ -539,9 +547,19 @@ Always call this after using a recalled memory so the system learns what's valua
|
|
|
539
547
|
engram_id: z.string().describe('ID of the memory (from memory_recall results)'),
|
|
540
548
|
useful: z.boolean().describe('Was this memory actually helpful?'),
|
|
541
549
|
context: z.string().optional().describe('Brief note on why it was/wasn\'t useful'),
|
|
550
|
+
recall_id: z.string().optional().describe(
|
|
551
|
+
'The [recall_id: …] printed at the end of the memory_recall output that returned this memory. '
|
|
552
|
+
+ 'Pass it so the feedback is joined to that recall; omitted, it defaults to the most recent recall in this session.',
|
|
553
|
+
),
|
|
542
554
|
},
|
|
543
555
|
async (params) => {
|
|
544
|
-
|
|
556
|
+
// 0.14.3: join feedback to the recall that produced it. Explicit recall_id
|
|
557
|
+
// wins; otherwise fall back to the engine's most recent logged activation —
|
|
558
|
+
// the common case is "recall, use, feedback" in one turn. Before this the
|
|
559
|
+
// MCP path hardcoded null (the HTTP route already accepted the id), which
|
|
560
|
+
// is why every retrieval_feedback row in the live store was orphaned.
|
|
561
|
+
const eventId = params.recall_id ?? activationEngine.lastActivationEventId ?? null;
|
|
562
|
+
await store.logRetrievalFeedback(eventId, params.engram_id, params.useful, params.context ?? '');
|
|
545
563
|
|
|
546
564
|
const engram = await store.getEngram(params.engram_id);
|
|
547
565
|
if (engram) {
|
|
@@ -645,7 +663,7 @@ server.tool(
|
|
|
645
663
|
`Identify THIS AWM instance — agent id, mode (standalone/hive), backend, store path, code provenance, ports, and the sibling agent spaces present in the same store. Call when unsure which AWM instance or memory space you are talking to.`,
|
|
646
664
|
{},
|
|
647
665
|
async () => {
|
|
648
|
-
const info = await buildWhoami(store, AGENT_ID, 'mcp');
|
|
666
|
+
const info = await buildWhoami(store, AGENT_ID, 'mcp', sidecarHandle ? sidecarHandle.boundPort() : undefined);
|
|
649
667
|
return { content: [{ type: 'text', text: formatWhoami(info) }] };
|
|
650
668
|
},
|
|
651
669
|
);
|
|
@@ -658,6 +676,11 @@ Also shows the activity log path so the user can tail it to see what's happening
|
|
|
658
676
|
async () => {
|
|
659
677
|
const metrics = await evalEngine.computeMetrics(AGENT_ID);
|
|
660
678
|
const checkpoint = await store.getCheckpoint(AGENT_ID);
|
|
679
|
+
// 0.14.3: outcome numbers, not activity counters. "Edge utility" (share of
|
|
680
|
+
// edges ever activated) was monotone — it could only rise — and read as
|
|
681
|
+
// health when it was not. Latency was a mean over a column that mixes cold
|
|
682
|
+
// loads and stalls (live store: mean 20 s, median 1.6 s); medians only now.
|
|
683
|
+
const usage = await evalEngine.computeUsage(AGENT_ID);
|
|
661
684
|
const lines = [
|
|
662
685
|
`Agent: ${AGENT_ID}`,
|
|
663
686
|
`Active memories: ${metrics.activeEngramCount}`,
|
|
@@ -665,9 +688,12 @@ Also shows the activity log path so the user can tail it to see what's happening
|
|
|
665
688
|
`Retracted: ${metrics.retractedCount}`,
|
|
666
689
|
`Avg confidence: ${metrics.avgConfidence.toFixed(3)}`,
|
|
667
690
|
`Total edges: ${metrics.totalEdges}`,
|
|
668
|
-
|
|
691
|
+
``,
|
|
692
|
+
`Write:recall (30d): 1 : ${usage.recallsPerWrite30d.toFixed(2)} (${usage.writes30d} writes, ${usage.recalls30d} recalls)`,
|
|
693
|
+
`Never recalled: ${(usage.neverRecalledShare * 100).toFixed(0)}% of active memories`,
|
|
694
|
+
`Recall→use (7d): ${usage.feedbackLinked7d > 0 ? (usage.usefulShare7d * 100).toFixed(0) + '% useful of ' + usage.feedbackLinked7d + ' judged' : 'no linked feedback yet'}`,
|
|
669
695
|
`Activations (24h): ${metrics.activationCount}`,
|
|
670
|
-
`
|
|
696
|
+
`Recall latency (24h): p50 ${metrics.p50LatencyMs.toFixed(0)}ms p90 ${metrics.p90LatencyMs.toFixed(0)}ms`,
|
|
671
697
|
``,
|
|
672
698
|
`Session writes: ${checkpoint?.auto.writeCountSinceConsolidation ?? 0}`,
|
|
673
699
|
`Session recalls: ${checkpoint?.auto.recallCountSinceConsolidation ?? 0}`,
|
|
@@ -675,7 +701,10 @@ Also shows the activity log path so the user can tail it to see what's happening
|
|
|
675
701
|
`Checkpoint: ${checkpoint?.executionState ? checkpoint.executionState.currentTask : 'none'}`,
|
|
676
702
|
``,
|
|
677
703
|
`Activity log: ${getLogPath() ?? 'not configured'}`,
|
|
678
|
-
|
|
704
|
+
// Report the port actually BOUND, not the one configured. Before 0.14.2
|
|
705
|
+
// this line said 8401 in every session, including the ones whose sidecar
|
|
706
|
+
// had lost the port and silently disabled hooks.
|
|
707
|
+
`Hook sidecar: ${sidecarPortLabel()}`,
|
|
679
708
|
];
|
|
680
709
|
|
|
681
710
|
return {
|
|
@@ -1336,6 +1365,8 @@ async function main() {
|
|
|
1336
1365
|
agentId: AGENT_ID,
|
|
1337
1366
|
secret: HOOK_SECRET,
|
|
1338
1367
|
port: HOOK_PORT,
|
|
1368
|
+
portRange: HOOK_PORT_RANGE,
|
|
1369
|
+
version: VERSION,
|
|
1339
1370
|
// 0.12.2: warm recall for hooks — the sidecar shares this process's
|
|
1340
1371
|
// activation engine and loaded models, so a UserPromptSubmit hook can get
|
|
1341
1372
|
// warm-latency recall without any standing server. Trimmed result shape
|
|
@@ -1419,7 +1450,8 @@ async function main() {
|
|
|
1419
1450
|
|
|
1420
1451
|
// Log to stderr (stdout is reserved for MCP protocol)
|
|
1421
1452
|
console.error(`AgentWorkingMemory MCP server started (agent: ${AGENT_ID}, db: ${DB_PATH})`);
|
|
1422
|
-
|
|
1453
|
+
sidecarHandle = sidecar;
|
|
1454
|
+
console.error(`Hook sidecar preferred 127.0.0.1:${HOOK_PORT} (range ${HOOK_PORT_RANGE})${HOOK_SECRET ? ' (auth enabled)' : ' (no auth — set AWM_HOOK_SECRET)'}`);
|
|
1423
1455
|
|
|
1424
1456
|
// Clean shutdown
|
|
1425
1457
|
const cleanup = async () => {
|
package/src/storage/pglite.ts
CHANGED
|
@@ -1020,7 +1020,7 @@ export class PGliteEngramStore {
|
|
|
1020
1020
|
};
|
|
1021
1021
|
}
|
|
1022
1022
|
|
|
1023
|
-
async getActivationStats(agentId: string, windowHours: number = 24): Promise<{ count: number; avgLatencyMs: number; p95LatencyMs: number }> {
|
|
1023
|
+
async getActivationStats(agentId: string, windowHours: number = 24): Promise<{ count: number; avgLatencyMs: number; p50LatencyMs: number; p90LatencyMs: number; p95LatencyMs: number }> {
|
|
1024
1024
|
await this.readyPromise;
|
|
1025
1025
|
// Flush any buffered activation events so stats reflect the latest writes.
|
|
1026
1026
|
await this.flushActivationEvents();
|
|
@@ -1031,17 +1031,35 @@ export class PGliteEngramStore {
|
|
|
1031
1031
|
ORDER BY latency_ms ASC`,
|
|
1032
1032
|
[agentId, since],
|
|
1033
1033
|
);
|
|
1034
|
-
if (result.rows.length === 0) return { count: 0, avgLatencyMs: 0, p95LatencyMs: 0 };
|
|
1034
|
+
if (result.rows.length === 0) return { count: 0, avgLatencyMs: 0, p50LatencyMs: 0, p90LatencyMs: 0, p95LatencyMs: 0 };
|
|
1035
1035
|
const latencies = result.rows.map((r) => Number(r.latency_ms));
|
|
1036
1036
|
const total = latencies.reduce((s, l) => s + l, 0);
|
|
1037
|
-
|
|
1037
|
+
// 0.14.3: p50/p90 — see sqlite.ts for why the mean is not usable here.
|
|
1038
|
+
const pct = (q: number) => latencies[Math.min(Math.floor(latencies.length * q), latencies.length - 1)];
|
|
1038
1039
|
return {
|
|
1039
1040
|
count: latencies.length,
|
|
1040
1041
|
avgLatencyMs: total / latencies.length,
|
|
1041
|
-
|
|
1042
|
+
p50LatencyMs: pct(0.5),
|
|
1043
|
+
p90LatencyMs: pct(0.9),
|
|
1044
|
+
p95LatencyMs: pct(0.95),
|
|
1042
1045
|
};
|
|
1043
1046
|
}
|
|
1044
1047
|
|
|
1048
|
+
/** 0.14.3: feedback rows joined to an activation event — see sqlite.ts. */
|
|
1049
|
+
async getLinkedFeedbackStats(agentId: string, windowHours: number = 24 * 7): Promise<{ total: number; useful: number }> {
|
|
1050
|
+
await this.readyPromise;
|
|
1051
|
+
const since = new Date(Date.now() - windowHours * 3600_000).toISOString();
|
|
1052
|
+
const result = await this.db.query<any>(
|
|
1053
|
+
`SELECT COUNT(*) AS total, COUNT(CASE WHEN rf.useful = TRUE THEN 1 END) AS useful
|
|
1054
|
+
FROM retrieval_feedback rf
|
|
1055
|
+
JOIN activation_events ae ON ae.id = rf.activation_event_id
|
|
1056
|
+
WHERE ae.agent_id = $1 AND rf.timestamp > $2`,
|
|
1057
|
+
[agentId, since],
|
|
1058
|
+
);
|
|
1059
|
+
const row = result.rows[0] ?? { total: 0, useful: 0 };
|
|
1060
|
+
return { total: Number(row.total), useful: Number(row.useful) };
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1045
1063
|
async getConsolidatedCount(agentId: string): Promise<number> {
|
|
1046
1064
|
await this.readyPromise;
|
|
1047
1065
|
const result = await this.db.query<any>(
|
package/src/storage/postgres.ts
CHANGED
|
@@ -1119,7 +1119,7 @@ export class PostgresEngramStore {
|
|
|
1119
1119
|
};
|
|
1120
1120
|
}
|
|
1121
1121
|
|
|
1122
|
-
async getActivationStats(agentId: string, windowHours: number = 24): Promise<{ count: number; avgLatencyMs: number; p95LatencyMs: number }> {
|
|
1122
|
+
async getActivationStats(agentId: string, windowHours: number = 24): Promise<{ count: number; avgLatencyMs: number; p50LatencyMs: number; p90LatencyMs: number; p95LatencyMs: number }> {
|
|
1123
1123
|
await this.readyPromise;
|
|
1124
1124
|
// Flush any buffered activation events so stats reflect the latest writes.
|
|
1125
1125
|
await this.flushActivationEvents();
|
|
@@ -1130,17 +1130,35 @@ export class PostgresEngramStore {
|
|
|
1130
1130
|
ORDER BY latency_ms ASC`,
|
|
1131
1131
|
[agentId, since],
|
|
1132
1132
|
);
|
|
1133
|
-
if (result.rows.length === 0) return { count: 0, avgLatencyMs: 0, p95LatencyMs: 0 };
|
|
1133
|
+
if (result.rows.length === 0) return { count: 0, avgLatencyMs: 0, p50LatencyMs: 0, p90LatencyMs: 0, p95LatencyMs: 0 };
|
|
1134
1134
|
const latencies = result.rows.map((r) => Number(r.latency_ms));
|
|
1135
1135
|
const total = latencies.reduce((s, l) => s + l, 0);
|
|
1136
|
-
|
|
1136
|
+
// 0.14.3: p50/p90 — see sqlite.ts for why the mean is not usable here.
|
|
1137
|
+
const pct = (q: number) => latencies[Math.min(Math.floor(latencies.length * q), latencies.length - 1)];
|
|
1137
1138
|
return {
|
|
1138
1139
|
count: latencies.length,
|
|
1139
1140
|
avgLatencyMs: total / latencies.length,
|
|
1140
|
-
|
|
1141
|
+
p50LatencyMs: pct(0.5),
|
|
1142
|
+
p90LatencyMs: pct(0.9),
|
|
1143
|
+
p95LatencyMs: pct(0.95),
|
|
1141
1144
|
};
|
|
1142
1145
|
}
|
|
1143
1146
|
|
|
1147
|
+
/** 0.14.3: feedback rows joined to an activation event — see sqlite.ts. */
|
|
1148
|
+
async getLinkedFeedbackStats(agentId: string, windowHours: number = 24 * 7): Promise<{ total: number; useful: number }> {
|
|
1149
|
+
await this.readyPromise;
|
|
1150
|
+
const since = new Date(Date.now() - windowHours * 3600_000).toISOString();
|
|
1151
|
+
const result = await this.q<any>(
|
|
1152
|
+
`SELECT COUNT(*) AS total, COUNT(CASE WHEN rf.useful = TRUE THEN 1 END) AS useful
|
|
1153
|
+
FROM retrieval_feedback rf
|
|
1154
|
+
JOIN activation_events ae ON ae.id = rf.activation_event_id
|
|
1155
|
+
WHERE ae.agent_id = $1 AND rf.timestamp > $2`,
|
|
1156
|
+
[agentId, since],
|
|
1157
|
+
);
|
|
1158
|
+
const row = result.rows[0] ?? { total: 0, useful: 0 };
|
|
1159
|
+
return { total: Number(row.total), useful: Number(row.useful) };
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1144
1162
|
async getConsolidatedCount(agentId: string): Promise<number> {
|
|
1145
1163
|
await this.readyPromise;
|
|
1146
1164
|
const result = await this.q<any>(
|