@xdarkicex/openclaw-memory-libravdb 1.4.0 → 1.4.2
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/docs/contributing.md +11 -0
- package/docs/gating.md +9 -2
- package/docs/mathematics-v2.md +9 -3
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/cli.ts +21 -8
- package/src/context-engine.ts +287 -93
- package/src/durable-namespace.ts +34 -0
- package/src/memory-runtime.ts +138 -28
- package/src/types.ts +7 -8
package/docs/contributing.md
CHANGED
|
@@ -75,3 +75,14 @@ Before opening a PR:
|
|
|
75
75
|
- any new gating signal must come with calibration or invariant coverage
|
|
76
76
|
- any retrieval math change must be reflected in [mathematics-v2.md](./mathematics-v2.md)
|
|
77
77
|
- any gating change must be reflected in [gating.md](./gating.md)
|
|
78
|
+
|
|
79
|
+
## Release Versioning
|
|
80
|
+
|
|
81
|
+
`package.json` is the source of truth for the release version.
|
|
82
|
+
|
|
83
|
+
The release automation syncs `openclaw.plugin.json` from `package.json` during the
|
|
84
|
+
auto-bump/tag flow, and the publish workflow refuses to publish if the Git tag,
|
|
85
|
+
`package.json`, and `openclaw.plugin.json` versions do not all match.
|
|
86
|
+
|
|
87
|
+
The daemon release workflow enforces the same alignment before generating the
|
|
88
|
+
Homebrew formula, so package, manifest, tag, and formula versioning stay in lockstep.
|
package/docs/gating.md
CHANGED
|
@@ -76,8 +76,15 @@ The repetition term is a product, not a sum:
|
|
|
76
76
|
\[ R(t) = F(t) \cdot (1 - S(t)) \]
|
|
77
77
|
|
|
78
78
|
with:
|
|
79
|
-
\[ F(t) = \min\left(\frac{\mathrm{hitsAbove}(\mathrm{turns:
|
|
80
|
-
\[ S(t) = \min\left(\frac{\mathrm{hitsAbove}(\mathrm{user:
|
|
79
|
+
\[ F(t) = \min\left(\frac{\mathrm{hitsAbove}(\mathrm{turns:u}, 0.80, k=10)}{5}, 1\right) \]
|
|
80
|
+
\[ S(t) = \min\left(\frac{\mathrm{hitsAbove}(\mathrm{user:u}, 0.85, k=5)}{3}, 1\right) \]
|
|
81
|
+
|
|
82
|
+
where $u$ is the resolved durable namespace used by the host boundary. The
|
|
83
|
+
resolver chooses $u$ in this order: explicit `userId`, then the
|
|
84
|
+
session-key-derived namespace, then `agentId`, and finally the resolver
|
|
85
|
+
fallback/default when no host identity is available. When the host does not
|
|
86
|
+
provide a `userId`, the gate still measures repetition and saturation against a
|
|
87
|
+
stable durable scope.
|
|
81
88
|
|
|
82
89
|
Why a product? High input frequency should help only if durable memory is not already saturated. High saturation must veto the repetition term regardless of frequency. The veto property is structural: $S(t) = 1 \Rightarrow R(t) = 0$.
|
|
83
90
|
|
package/docs/mathematics-v2.md
CHANGED
|
@@ -61,7 +61,7 @@ $$
|
|
|
61
61
|
S(d)=
|
|
62
62
|
\begin{cases}
|
|
63
63
|
1.0 & \text{if } d \text{ is from the active session} \\
|
|
64
|
-
0.6 & \text{if } d \text{ is from durable
|
|
64
|
+
0.6 & \text{if } d \text{ is from durable namespace memory} \\
|
|
65
65
|
0.3 & \text{if } d \text{ is from global memory}
|
|
66
66
|
\end{cases}
|
|
67
67
|
$$
|
|
@@ -182,7 +182,7 @@ product $\lambda_s \Delta t_d$ is dimensionless, as required by the exponential.
|
|
|
182
182
|
The current implementation uses different constants by scope:
|
|
183
183
|
|
|
184
184
|
- active session: $\lambda_s = 0.0001$
|
|
185
|
-
- durable
|
|
185
|
+
- durable namespace memory: $\lambda_s = 0.00001$
|
|
186
186
|
- global memory: $\lambda_s = 0.000002$
|
|
187
187
|
|
|
188
188
|
The implied half-lives make the decay constants auditable at a glance:
|
|
@@ -200,9 +200,15 @@ $$
|
|
|
200
200
|
If those half-lives feel wrong for a given deployment, adjust $\lambda_s$ via
|
|
201
201
|
config — do not change the decay formula itself.
|
|
202
202
|
|
|
203
|
-
This makes session context fade fastest,
|
|
203
|
+
This makes session context fade fastest, durable namespace memory fade more slowly, and
|
|
204
204
|
global memory remain the most stable.
|
|
205
205
|
|
|
206
|
+
When the host supplies an explicit `userId`, the durable namespace matches that
|
|
207
|
+
`userId`. When the host does not provide a `userId`, the plugin derives a stable
|
|
208
|
+
durable namespace from the session key, or falls back to `session:${sessionId}`
|
|
209
|
+
when both `userId` and `sessionKey` are absent, so the retrieval math and scope
|
|
210
|
+
weighting stay unchanged even when the host does not expose a separate user principal.
|
|
211
|
+
|
|
206
212
|
**Note on symbol disambiguation.** The symbol $\lambda_s$ here denotes the
|
|
207
213
|
scope-specific recency decay constant with units $\mathrm{s}^{-1}$. Section 7.3
|
|
208
214
|
uses $\lambda_r$ for a separate recency constant in the planned authority weight.
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import { stdin, stdout } from "node:process";
|
|
3
3
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
|
4
|
+
import { resolveDurableNamespace } from "./durable-namespace.js";
|
|
4
5
|
import type { PluginRuntime } from "./plugin-runtime.js";
|
|
5
6
|
import type { LoggerLike, PluginConfig } from "./types.js";
|
|
6
7
|
|
|
@@ -26,6 +27,7 @@ type ExportResult = {
|
|
|
26
27
|
|
|
27
28
|
type CliOptionBag = {
|
|
28
29
|
userId?: string;
|
|
30
|
+
sessionKey?: string;
|
|
29
31
|
sessionId?: string;
|
|
30
32
|
limit?: string | number;
|
|
31
33
|
yes?: boolean;
|
|
@@ -70,12 +72,13 @@ export function registerMemoryCli(
|
|
|
70
72
|
.action(() => void runStatus(runtime, cfg, logger));
|
|
71
73
|
|
|
72
74
|
const flush = ensureCommand(root, "flush")
|
|
73
|
-
.description("Wipe a
|
|
75
|
+
.description("Wipe a durable memory namespace after confirmation");
|
|
74
76
|
if (flush.requiredOption) {
|
|
75
77
|
flush.requiredOption("--user-id <userId>", "User id whose durable memory should be deleted");
|
|
76
78
|
} else {
|
|
77
79
|
flush.option("--user-id <userId>", "User id whose durable memory should be deleted");
|
|
78
80
|
}
|
|
81
|
+
flush.option("--session-key <sessionKey>", "Session key whose derived durable namespace should be deleted");
|
|
79
82
|
flush
|
|
80
83
|
.option("--yes", "Skip the confirmation prompt")
|
|
81
84
|
.action((opts) => void runFlush(runtime, opts, logger));
|
|
@@ -83,6 +86,7 @@ export function registerMemoryCli(
|
|
|
83
86
|
const exportCmd = ensureCommand(root, "export")
|
|
84
87
|
.description("Stream stored memories as newline-delimited JSON");
|
|
85
88
|
exportCmd.option("--user-id <userId>", "Restrict export to a single user namespace");
|
|
89
|
+
exportCmd.option("--session-key <sessionKey>", "Restrict export to a derived session-key namespace");
|
|
86
90
|
exportCmd.action((opts) => void runExport(runtime, opts, logger));
|
|
87
91
|
|
|
88
92
|
const journal = ensureCommand(root, "journal")
|
|
@@ -147,15 +151,15 @@ async function runStatus(runtime: PluginRuntime, cfg: PluginConfig, logger: Logg
|
|
|
147
151
|
}
|
|
148
152
|
|
|
149
153
|
async function runFlush(runtime: PluginRuntime, opts: CliOptionBag | undefined, logger: LoggerLike): Promise<void> {
|
|
150
|
-
const
|
|
151
|
-
if (!
|
|
152
|
-
logger.error("LibraVDB flush requires --user-id <userId>.");
|
|
154
|
+
const namespace = resolveCliNamespace(opts);
|
|
155
|
+
if (!namespace) {
|
|
156
|
+
logger.error("LibraVDB flush requires --user-id <userId> or --session-key <sessionKey>.");
|
|
153
157
|
process.exitCode = 1;
|
|
154
158
|
return;
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
if (!opts?.yes) {
|
|
158
|
-
const confirmed = await confirm(`Delete durable memory
|
|
162
|
+
const confirmed = await confirm(`Delete durable memory namespace ${namespace}? [y/N] `);
|
|
159
163
|
if (!confirmed) {
|
|
160
164
|
console.log("Aborted.");
|
|
161
165
|
return;
|
|
@@ -164,8 +168,8 @@ async function runFlush(runtime: PluginRuntime, opts: CliOptionBag | undefined,
|
|
|
164
168
|
|
|
165
169
|
try {
|
|
166
170
|
const rpc = await runtime.getRpc();
|
|
167
|
-
await rpc.call("flush_namespace", {
|
|
168
|
-
console.log(`Deleted durable memory namespace
|
|
171
|
+
await rpc.call("flush_namespace", { namespace });
|
|
172
|
+
console.log(`Deleted durable memory namespace ${namespace}.`);
|
|
169
173
|
} catch (error) {
|
|
170
174
|
logger.error(`LibraVDB flush failed: ${formatError(error)}`);
|
|
171
175
|
process.exitCode = 1;
|
|
@@ -176,7 +180,7 @@ async function runExport(runtime: PluginRuntime, opts: CliOptionBag | undefined,
|
|
|
176
180
|
try {
|
|
177
181
|
const rpc = await runtime.getRpc();
|
|
178
182
|
const result = await rpc.call<ExportResult>("export_memory", {
|
|
179
|
-
|
|
183
|
+
namespace: resolveCliNamespace(opts),
|
|
180
184
|
});
|
|
181
185
|
for (const record of result.records ?? []) {
|
|
182
186
|
stdout.write(`${JSON.stringify(record)}\n`);
|
|
@@ -233,6 +237,15 @@ function normalizeLimit(limit: string | number | undefined): number | undefined
|
|
|
233
237
|
return undefined;
|
|
234
238
|
}
|
|
235
239
|
|
|
240
|
+
function resolveCliNamespace(opts: CliOptionBag | undefined): string | undefined {
|
|
241
|
+
const userId = opts?.userId?.trim();
|
|
242
|
+
const sessionKey = opts?.sessionKey?.trim();
|
|
243
|
+
if (!userId && !sessionKey) {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
return resolveDurableNamespace({ userId, sessionKey });
|
|
247
|
+
}
|
|
248
|
+
|
|
236
249
|
type CliRegistrar = {
|
|
237
250
|
registerCli?(
|
|
238
251
|
builder: (ctx: { program: CliProgram }) => void,
|
package/src/context-engine.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import {
|
|
2
3
|
DEFAULT_CONTINUITY_MIN_TURNS,
|
|
3
4
|
DEFAULT_CONTINUITY_PRIOR_CONTEXT_TOKENS,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
} from "./temporal.js";
|
|
20
21
|
import type { TemporalRecoveryRankingResult } from "./temporal.js";
|
|
21
22
|
import { countTokens, estimateTokens, fitPromptBudget, fitPromptBudgetFirstFit } from "./tokens.js";
|
|
23
|
+
import { resolveDurableNamespace } from "./durable-namespace.js";
|
|
22
24
|
import type { RpcGetter } from "./plugin-runtime.js";
|
|
23
25
|
import type {
|
|
24
26
|
ContextAssembleArgs,
|
|
@@ -27,6 +29,7 @@ import type {
|
|
|
27
29
|
ContextCompactArgs,
|
|
28
30
|
ContextIngestArgs,
|
|
29
31
|
GatingResult,
|
|
32
|
+
MemoryMessage,
|
|
30
33
|
PluginConfig,
|
|
31
34
|
RecallCache,
|
|
32
35
|
SearchResult,
|
|
@@ -42,6 +45,8 @@ const SESSION_RAW_COLLECTION_PREFIX = "session_raw:";
|
|
|
42
45
|
const SESSION_SUMMARY_COLLECTION_PREFIX = "session_summary:";
|
|
43
46
|
const SESSION_EDGE_COLLECTION_PREFIX = "session_edge:";
|
|
44
47
|
const SESSION_STATE_COLLECTION_PREFIX = "session_state:";
|
|
48
|
+
const AFTER_TURN_DEDUPE_TTL_MS = 60 * 60 * 1000;
|
|
49
|
+
const AFTER_TURN_DEDUPE_MAX_ENTRIES = 1024;
|
|
45
50
|
|
|
46
51
|
export function buildContextEngineFactory(
|
|
47
52
|
getRpc: RpcGetter,
|
|
@@ -52,8 +57,9 @@ export function buildContextEngineFactory(
|
|
|
52
57
|
let authoredSoftCache: SearchResult[] | null = null;
|
|
53
58
|
let authoredVariantCache: SearchResult[] | null = null;
|
|
54
59
|
const authoredVariantRecallCache = new Map<string, SearchResult[]>();
|
|
60
|
+
const afterTurnIngestedKeys = new Map<string, number>();
|
|
55
61
|
|
|
56
|
-
// Session-scoped elevated-guidance cache keyed by sessionId + generation +
|
|
62
|
+
// Session-scoped elevated-guidance cache keyed by sessionId + generation + durable namespace + queryText
|
|
57
63
|
const elevatedRecallCache = new Map<string, SearchResult[]>();
|
|
58
64
|
const elevatedRecallGeneration = new Map<string, number>();
|
|
59
65
|
|
|
@@ -63,9 +69,10 @@ export function buildContextEngineFactory(
|
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
return {
|
|
66
|
-
info: { id: "libravdb-memory" },
|
|
72
|
+
info: { id: "libravdb-memory", name: "LibraVDB Memory", ownsCompaction: true },
|
|
67
73
|
ownsCompaction: true,
|
|
68
|
-
async bootstrap({ sessionId, userId }: ContextBootstrapArgs) {
|
|
74
|
+
async bootstrap({ sessionId, sessionKey, userId }: ContextBootstrapArgs) {
|
|
75
|
+
const durableNamespace = resolveDurableNamespace({ userId, sessionKey, fallback: `session:${sessionId}` });
|
|
69
76
|
const rpc = await getRpc();
|
|
70
77
|
await rpc.call("ensure_collections", {
|
|
71
78
|
collections: [
|
|
@@ -75,8 +82,8 @@ export function buildContextEngineFactory(
|
|
|
75
82
|
sessionEdgeCollection(sessionId),
|
|
76
83
|
sessionStateCollection(sessionId),
|
|
77
84
|
...(useSessionRecallProjection(cfg) ? [sessionRecallCollection(sessionId)] : []),
|
|
78
|
-
`turns:${
|
|
79
|
-
`user:${
|
|
85
|
+
`turns:${durableNamespace}`,
|
|
86
|
+
`user:${durableNamespace}`,
|
|
80
87
|
"global",
|
|
81
88
|
AUTHORED_HARD_COLLECTION,
|
|
82
89
|
AUTHORED_SOFT_COLLECTION,
|
|
@@ -98,110 +105,93 @@ export function buildContextEngineFactory(
|
|
|
98
105
|
validateSection7StartupHardReserve(cfg, authoredHard);
|
|
99
106
|
return { ok: true };
|
|
100
107
|
},
|
|
101
|
-
async ingest({ sessionId, userId, message, isHeartbeat }: ContextIngestArgs) {
|
|
108
|
+
async ingest({ sessionId, sessionKey, userId, message, isHeartbeat }: ContextIngestArgs) {
|
|
102
109
|
if (isHeartbeat) {
|
|
103
110
|
return { ingested: false };
|
|
104
111
|
}
|
|
105
112
|
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
userId,
|
|
112
|
-
sessionId,
|
|
113
|
-
type: "turn",
|
|
114
|
-
provenance_class: "session_turn",
|
|
115
|
-
stability_weight: stabilityWeightForMessage(message.role),
|
|
116
|
-
};
|
|
117
|
-
// Elevated cache is session-scoped, so invalidate immediately on every ingest
|
|
118
|
-
clearElevatedCacheForSession(sessionId);
|
|
119
|
-
const rawSessionId = `${sessionId}:${ts}`;
|
|
120
|
-
const rawSessionInsert = rpc.call("insert_session_turn", {
|
|
113
|
+
const result = await ingestCanonicalMessage({
|
|
114
|
+
getRpc,
|
|
115
|
+
cfg,
|
|
116
|
+
recallCache,
|
|
117
|
+
clearElevatedCacheForSession,
|
|
121
118
|
sessionId,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
119
|
+
sessionKey,
|
|
120
|
+
userId,
|
|
121
|
+
message,
|
|
125
122
|
});
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
123
|
+
return { ingested: result.ingested };
|
|
124
|
+
},
|
|
125
|
+
async afterTurn({ sessionId, sessionKey, userId, messages, prePromptMessageCount, isHeartbeat }: {
|
|
126
|
+
sessionId: string;
|
|
127
|
+
sessionKey?: string;
|
|
128
|
+
userId?: string;
|
|
129
|
+
messages: Array<{ role: string; content: unknown }>;
|
|
130
|
+
prePromptMessageCount: number;
|
|
131
|
+
isHeartbeat?: boolean;
|
|
132
|
+
}) {
|
|
133
|
+
if (isHeartbeat) {
|
|
134
|
+
return;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
});
|
|
137
|
+
const startIndex = Math.max(0, prePromptMessageCount - 1);
|
|
138
|
+
const turnMessages = messages.slice(startIndex);
|
|
139
|
+
const normalizedTurnMessages = turnMessages.flatMap((turnMessage, offset) => {
|
|
140
|
+
const normalized = normalizeHostMessage(turnMessage);
|
|
141
|
+
if (!normalized) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
return [{ index: startIndex + offset, normalized }] as const;
|
|
145
|
+
});
|
|
146
|
+
for (let offset = 0; offset < normalizedTurnMessages.length; offset++) {
|
|
147
|
+
const { index, normalized } = normalizedTurnMessages[offset];
|
|
149
148
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
149
|
+
const dedupeKey = `${sessionId}\n${index}\n${normalized.role}\n${hashMessageContent(normalized.content)}`;
|
|
150
|
+
if (hasRecentAfterTurnIngest(afterTurnIngestedKeys, dedupeKey)) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
154
153
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
gating_r: gating.r,
|
|
172
|
-
gating_d: gating.d,
|
|
173
|
-
gating_p: gating.p,
|
|
174
|
-
gating_a: gating.a,
|
|
175
|
-
gating_dtech: gating.dtech,
|
|
176
|
-
gating_gconv: gating.gconv,
|
|
177
|
-
gating_gtech: gating.gtech,
|
|
178
|
-
},
|
|
179
|
-
}).catch(console.error);
|
|
180
|
-
}
|
|
181
|
-
} catch {
|
|
182
|
-
// Session storage already happened; skip durable promotion on gating failure.
|
|
154
|
+
const result = await ingestCanonicalMessage({
|
|
155
|
+
getRpc,
|
|
156
|
+
cfg,
|
|
157
|
+
recallCache,
|
|
158
|
+
clearElevatedCacheForSession,
|
|
159
|
+
sessionId,
|
|
160
|
+
sessionKey,
|
|
161
|
+
userId,
|
|
162
|
+
message: {
|
|
163
|
+
...normalized,
|
|
164
|
+
id: `after-turn:${index}`,
|
|
165
|
+
},
|
|
166
|
+
skipProjectionRebuild: offset !== normalizedTurnMessages.length - 1,
|
|
167
|
+
});
|
|
168
|
+
if (result.ingested) {
|
|
169
|
+
rememberAfterTurnIngest(afterTurnIngestedKeys, dedupeKey);
|
|
183
170
|
}
|
|
184
171
|
}
|
|
185
|
-
|
|
186
|
-
return { ingested: true };
|
|
187
172
|
},
|
|
188
|
-
async assemble({ sessionId, userId, messages, tokenBudget }: ContextAssembleArgs) {
|
|
173
|
+
async assemble({ sessionId, sessionKey, userId, messages, tokenBudget, ...rest }: ContextAssembleArgs & Record<string, unknown>) {
|
|
189
174
|
const PROFILE = process.env.OPENCLAW_PROFILE_ASSEMBLE === "1";
|
|
190
175
|
const DEBUG_RECOVERY = process.env.LONGMEMEVAL_DEBUG_RANKING === "1";
|
|
176
|
+
const durableNamespace = resolveDurableNamespace({ userId, sessionKey, fallback: `session:${sessionId}` });
|
|
177
|
+
const originalMessages = messages;
|
|
178
|
+
const normalizedMessages = normalizeConversationMessages(messages as Array<{ role: string; content: unknown }>);
|
|
191
179
|
|
|
192
|
-
const queryText =
|
|
180
|
+
const queryText =
|
|
181
|
+
(typeof rest.prompt === "string" && rest.prompt.trim() ? rest.prompt : undefined) ??
|
|
182
|
+
normalizedMessages.at(-1)?.content ?? "";
|
|
193
183
|
if (!queryText) {
|
|
194
184
|
return {
|
|
195
|
-
messages,
|
|
196
|
-
estimatedTokens: countTokens(
|
|
185
|
+
messages: originalMessages,
|
|
186
|
+
estimatedTokens: countTokens(originalMessages),
|
|
197
187
|
systemPromptAddition: "",
|
|
198
188
|
} satisfies ContextAssembleResult;
|
|
199
189
|
}
|
|
200
190
|
const temporalQuery = detectTemporalQuerySignal(queryText);
|
|
201
191
|
const temporalSelectorGuard = decideTemporalSelectorGuard(queryText, temporalQuery);
|
|
202
192
|
|
|
203
|
-
const excluded = recentIds(
|
|
204
|
-
const cached = recallCache.take({ userId, queryText });
|
|
193
|
+
const excluded = recentIds(normalizedMessages, 4);
|
|
194
|
+
const cached = recallCache.take({ userId: durableNamespace, queryText });
|
|
205
195
|
|
|
206
196
|
const rpc = await getRpc();
|
|
207
197
|
|
|
@@ -265,8 +255,9 @@ export function buildContextEngineFactory(
|
|
|
265
255
|
temporalQuery,
|
|
266
256
|
temporalSelectorGuard,
|
|
267
257
|
sessionId,
|
|
268
|
-
userId,
|
|
269
|
-
|
|
258
|
+
userId: durableNamespace,
|
|
259
|
+
visibleMessages: originalMessages,
|
|
260
|
+
messages: normalizedMessages,
|
|
270
261
|
tokenBudget,
|
|
271
262
|
profiler,
|
|
272
263
|
debugRecovery: DEBUG_RECOVERY,
|
|
@@ -282,8 +273,8 @@ export function buildContextEngineFactory(
|
|
|
282
273
|
: result;
|
|
283
274
|
} catch {
|
|
284
275
|
return {
|
|
285
|
-
messages,
|
|
286
|
-
estimatedTokens: countTokens(
|
|
276
|
+
messages: originalMessages,
|
|
277
|
+
estimatedTokens: countTokens(originalMessages),
|
|
287
278
|
systemPromptAddition: "",
|
|
288
279
|
} satisfies ContextAssembleResult;
|
|
289
280
|
}
|
|
@@ -302,6 +293,7 @@ export function buildContextEngineFactory(
|
|
|
302
293
|
temporalSelectorGuard,
|
|
303
294
|
sessionId,
|
|
304
295
|
userId,
|
|
296
|
+
visibleMessages,
|
|
305
297
|
messages,
|
|
306
298
|
tokenBudget,
|
|
307
299
|
profiler,
|
|
@@ -320,6 +312,7 @@ export function buildContextEngineFactory(
|
|
|
320
312
|
temporalSelectorGuard: ReturnType<typeof decideTemporalSelectorGuard>;
|
|
321
313
|
sessionId: string;
|
|
322
314
|
userId: string;
|
|
315
|
+
visibleMessages: MemoryMessage[];
|
|
323
316
|
messages: Array<{ role: string; content: string }>;
|
|
324
317
|
tokenBudget: number;
|
|
325
318
|
profiler: { mark(label: string): void; emit(): void } | null;
|
|
@@ -369,8 +362,8 @@ export function buildContextEngineFactory(
|
|
|
369
362
|
content: buildInjectedMemoryMessageContent(item),
|
|
370
363
|
}));
|
|
371
364
|
return {
|
|
372
|
-
messages: [...selectedMessages, ...
|
|
373
|
-
estimatedTokens: countTokens(selectedMessages) + countTokens(
|
|
365
|
+
messages: [...selectedMessages, ...visibleMessages],
|
|
366
|
+
estimatedTokens: countTokens(selectedMessages) + countTokens(visibleMessages),
|
|
374
367
|
systemPromptAddition: buildDegradedMemoryHeader(degradedReasons, selected),
|
|
375
368
|
};
|
|
376
369
|
}
|
|
@@ -706,8 +699,8 @@ export function buildContextEngineFactory(
|
|
|
706
699
|
}));
|
|
707
700
|
|
|
708
701
|
return {
|
|
709
|
-
messages: [...selectedMessages, ...
|
|
710
|
-
estimatedTokens: countTokens(selectedMessages) + countTokens(
|
|
702
|
+
messages: [...selectedMessages, ...visibleMessages],
|
|
703
|
+
estimatedTokens: countTokens(selectedMessages) + countTokens(visibleMessages),
|
|
711
704
|
systemPromptAddition: buildMemoryHeader(selected),
|
|
712
705
|
_debug: debugRecovery
|
|
713
706
|
? {
|
|
@@ -981,6 +974,207 @@ function clampFraction(value: number | undefined): number {
|
|
|
981
974
|
return Math.min(1, Math.max(0, value));
|
|
982
975
|
}
|
|
983
976
|
|
|
977
|
+
async function ingestCanonicalMessage(params: {
|
|
978
|
+
getRpc: RpcGetter;
|
|
979
|
+
cfg: PluginConfig;
|
|
980
|
+
recallCache: RecallCache<SearchResult>;
|
|
981
|
+
clearElevatedCacheForSession: (sessionId: string) => void;
|
|
982
|
+
sessionId: string;
|
|
983
|
+
sessionKey?: string;
|
|
984
|
+
userId?: string;
|
|
985
|
+
message: MemoryMessage;
|
|
986
|
+
skipProjectionRebuild?: boolean;
|
|
987
|
+
}): Promise<{ ingested: boolean }> {
|
|
988
|
+
const normalized = normalizeMemoryMessage(params.message);
|
|
989
|
+
if (!normalized) {
|
|
990
|
+
return { ingested: false };
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const rpc = await params.getRpc();
|
|
994
|
+
const ts = Date.now();
|
|
995
|
+
const durableNamespace = resolveDurableNamespace({
|
|
996
|
+
userId: params.userId,
|
|
997
|
+
sessionKey: params.sessionKey,
|
|
998
|
+
fallback: `session:${params.sessionId}`,
|
|
999
|
+
});
|
|
1000
|
+
const turnId = normalized.id ?? `${ts}`;
|
|
1001
|
+
const sessionMeta = {
|
|
1002
|
+
role: normalized.role,
|
|
1003
|
+
ts,
|
|
1004
|
+
userId: durableNamespace,
|
|
1005
|
+
sessionId: params.sessionId,
|
|
1006
|
+
type: "turn",
|
|
1007
|
+
provenance_class: "session_turn",
|
|
1008
|
+
stability_weight: stabilityWeightForMessage(normalized.role),
|
|
1009
|
+
source_turn_id: turnId,
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
params.clearElevatedCacheForSession(params.sessionId);
|
|
1013
|
+
const rawSessionInsert = rpc.call("insert_session_turn", {
|
|
1014
|
+
sessionId: params.sessionId,
|
|
1015
|
+
id: `${params.sessionId}:${turnId}`,
|
|
1016
|
+
text: normalized.content,
|
|
1017
|
+
metadata: sessionMeta,
|
|
1018
|
+
});
|
|
1019
|
+
try {
|
|
1020
|
+
await rawSessionInsert;
|
|
1021
|
+
if (useSessionRecallProjection(params.cfg) && !params.skipProjectionRebuild) {
|
|
1022
|
+
await rebuildSessionRecallProjection(rpc, params.cfg, params.sessionId);
|
|
1023
|
+
}
|
|
1024
|
+
} catch {
|
|
1025
|
+
return { ingested: false };
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (normalized.role !== "user") {
|
|
1029
|
+
return { ingested: true };
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
try {
|
|
1033
|
+
params.recallCache.clearUser(durableNamespace);
|
|
1034
|
+
await rpc.call("insert_text", {
|
|
1035
|
+
collection: `turns:${durableNamespace}`,
|
|
1036
|
+
id: `${durableNamespace}:${turnId}`,
|
|
1037
|
+
text: normalized.content,
|
|
1038
|
+
metadata: {
|
|
1039
|
+
...sessionMeta,
|
|
1040
|
+
provenance_class: "turn_index",
|
|
1041
|
+
},
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
const gating = await rpc.call<GatingResult>("gating_scalar", {
|
|
1045
|
+
userId: durableNamespace,
|
|
1046
|
+
text: normalized.content,
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
if (gating.g >= (params.cfg.ingestionGateThreshold ?? 0.35)) {
|
|
1050
|
+
void rpc.call("insert_text", {
|
|
1051
|
+
collection: `user:${durableNamespace}`,
|
|
1052
|
+
id: `${durableNamespace}:${turnId}`,
|
|
1053
|
+
text: normalized.content,
|
|
1054
|
+
metadata: {
|
|
1055
|
+
role: normalized.role,
|
|
1056
|
+
ts,
|
|
1057
|
+
sessionId: params.sessionId,
|
|
1058
|
+
type: "turn",
|
|
1059
|
+
userId: durableNamespace,
|
|
1060
|
+
source_turn_id: turnId,
|
|
1061
|
+
provenance_class: "durable_user_memory",
|
|
1062
|
+
stability_weight: Math.max(stabilityWeightForMessage(normalized.role), gating.g),
|
|
1063
|
+
gating_score: gating.g,
|
|
1064
|
+
gating_t: gating.t,
|
|
1065
|
+
gating_h: gating.h,
|
|
1066
|
+
gating_r: gating.r,
|
|
1067
|
+
gating_d: gating.d,
|
|
1068
|
+
gating_p: gating.p,
|
|
1069
|
+
gating_a: gating.a,
|
|
1070
|
+
gating_dtech: gating.dtech,
|
|
1071
|
+
gating_gconv: gating.gconv,
|
|
1072
|
+
gating_gtech: gating.gtech,
|
|
1073
|
+
},
|
|
1074
|
+
}).catch(console.error);
|
|
1075
|
+
}
|
|
1076
|
+
} catch {
|
|
1077
|
+
// Session storage already happened; skip durable promotion on gating failure.
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
return { ingested: true };
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function hashMessageContent(content: string): string {
|
|
1084
|
+
return createHash("sha256").update(content).digest("hex");
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function pruneAfterTurnIngestKeys(cache: Map<string, number>, now: number): void {
|
|
1088
|
+
for (const [key, seenAt] of cache) {
|
|
1089
|
+
if (now - seenAt > AFTER_TURN_DEDUPE_TTL_MS) {
|
|
1090
|
+
cache.delete(key);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
while (cache.size > AFTER_TURN_DEDUPE_MAX_ENTRIES) {
|
|
1094
|
+
const oldestKey = cache.keys().next().value;
|
|
1095
|
+
if (!oldestKey) {
|
|
1096
|
+
break;
|
|
1097
|
+
}
|
|
1098
|
+
cache.delete(oldestKey);
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function hasRecentAfterTurnIngest(cache: Map<string, number>, key: string): boolean {
|
|
1103
|
+
const now = Date.now();
|
|
1104
|
+
pruneAfterTurnIngestKeys(cache, now);
|
|
1105
|
+
return cache.has(key);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function rememberAfterTurnIngest(cache: Map<string, number>, key: string): void {
|
|
1109
|
+
const now = Date.now();
|
|
1110
|
+
cache.delete(key);
|
|
1111
|
+
cache.set(key, now);
|
|
1112
|
+
pruneAfterTurnIngestKeys(cache, now);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function normalizeHostMessage(message: { role: string; content: unknown } | undefined): MemoryMessage | null {
|
|
1116
|
+
if (!message || !shouldIngestRole(message.role)) {
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
const content = extractMessageText(message.content);
|
|
1120
|
+
if (!content) {
|
|
1121
|
+
return null;
|
|
1122
|
+
}
|
|
1123
|
+
return {
|
|
1124
|
+
role: message.role,
|
|
1125
|
+
content,
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function normalizeConversationMessages(messages: Array<{ role: string; content: unknown }>): MemoryMessage[] {
|
|
1130
|
+
return messages
|
|
1131
|
+
.map((message) => normalizeHostMessage(message))
|
|
1132
|
+
.filter((message): message is MemoryMessage => message !== null);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function normalizeMemoryMessage(message: MemoryMessage): MemoryMessage | null {
|
|
1136
|
+
if (!shouldIngestRole(message.role)) {
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
const content = extractMessageText(message.content);
|
|
1140
|
+
if (!content) {
|
|
1141
|
+
return null;
|
|
1142
|
+
}
|
|
1143
|
+
return {
|
|
1144
|
+
...message,
|
|
1145
|
+
content,
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function shouldIngestRole(role: string): boolean {
|
|
1150
|
+
return role === "user" || role === "assistant" || role === "system";
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function extractMessageText(content: unknown): string {
|
|
1154
|
+
if (typeof content === "string") {
|
|
1155
|
+
return content;
|
|
1156
|
+
}
|
|
1157
|
+
if (!Array.isArray(content)) {
|
|
1158
|
+
return "";
|
|
1159
|
+
}
|
|
1160
|
+
return content
|
|
1161
|
+
.flatMap((part) => {
|
|
1162
|
+
if (!part || typeof part !== "object") {
|
|
1163
|
+
return [];
|
|
1164
|
+
}
|
|
1165
|
+
const type = (part as { type?: unknown }).type;
|
|
1166
|
+
if (
|
|
1167
|
+
(type === "text" || type === "input_text" || type === "output_text") &&
|
|
1168
|
+
typeof (part as { text?: unknown }).text === "string"
|
|
1169
|
+
) {
|
|
1170
|
+
return [(part as { text: string }).text];
|
|
1171
|
+
}
|
|
1172
|
+
return [];
|
|
1173
|
+
})
|
|
1174
|
+
.join("\n")
|
|
1175
|
+
.trim();
|
|
1176
|
+
}
|
|
1177
|
+
|
|
984
1178
|
function validateSection7StartupHardReserve(cfg: PluginConfig, authoredHard: SearchResult[]): void {
|
|
985
1179
|
if (authoredHard.length === 0) {
|
|
986
1180
|
return;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const SESSION_KEY_NAMESPACE_PREFIX = "session-key:";
|
|
2
|
+
const AGENT_ID_NAMESPACE_PREFIX = "agent-id:";
|
|
3
|
+
|
|
4
|
+
export function resolveDurableNamespace(params: {
|
|
5
|
+
userId?: string;
|
|
6
|
+
sessionKey?: string;
|
|
7
|
+
agentId?: string;
|
|
8
|
+
fallback?: string;
|
|
9
|
+
}): string {
|
|
10
|
+
const explicitUserId = firstNonEmpty(params.userId);
|
|
11
|
+
if (explicitUserId) {
|
|
12
|
+
return explicitUserId;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const sessionKey = firstNonEmpty(params.sessionKey);
|
|
16
|
+
if (sessionKey) {
|
|
17
|
+
return `${SESSION_KEY_NAMESPACE_PREFIX}${sessionKey}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const agentId = firstNonEmpty(params.agentId);
|
|
21
|
+
if (agentId) {
|
|
22
|
+
return `${AGENT_ID_NAMESPACE_PREFIX}${agentId}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return firstNonEmpty(params.fallback) ?? "default";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function firstNonEmpty(value: string | undefined): string | undefined {
|
|
29
|
+
if (typeof value !== "string") {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const trimmed = value.trim();
|
|
33
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
34
|
+
}
|
package/src/memory-runtime.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RpcGetter } from "./plugin-runtime.js";
|
|
2
|
+
import { resolveDurableNamespace } from "./durable-namespace.js";
|
|
2
3
|
import type { PluginConfig, SearchResult } from "./types.js";
|
|
3
4
|
|
|
4
5
|
type MemorySearchParams = {
|
|
@@ -12,10 +13,12 @@ type MemorySearchParams = {
|
|
|
12
13
|
userId?: string;
|
|
13
14
|
agentId?: string;
|
|
14
15
|
sessionId?: string;
|
|
16
|
+
sessionKey?: string;
|
|
15
17
|
context?: {
|
|
16
18
|
userId?: string;
|
|
17
19
|
agentId?: string;
|
|
18
20
|
sessionId?: string;
|
|
21
|
+
sessionKey?: string;
|
|
19
22
|
};
|
|
20
23
|
};
|
|
21
24
|
|
|
@@ -32,8 +35,9 @@ type MemoryRuntimeStatus = {
|
|
|
32
35
|
export function buildMemoryRuntimeBridge(getRpc: RpcGetter, cfg: PluginConfig) {
|
|
33
36
|
return {
|
|
34
37
|
async getMemorySearchManager(params: { agentId?: string; purpose?: string } = {}) {
|
|
38
|
+
const status = await readStatus(getRpc, params.purpose);
|
|
35
39
|
return {
|
|
36
|
-
manager: createMemorySearchManager(getRpc, cfg, params),
|
|
40
|
+
manager: createMemorySearchManager(getRpc, cfg, params, status),
|
|
37
41
|
};
|
|
38
42
|
},
|
|
39
43
|
resolveMemoryBackendConfig() {
|
|
@@ -51,23 +55,36 @@ function createMemorySearchManager(
|
|
|
51
55
|
getRpc: RpcGetter,
|
|
52
56
|
cfg: PluginConfig,
|
|
53
57
|
defaults: { agentId?: string; purpose?: string },
|
|
58
|
+
initialStatus: MemoryRuntimeStatus & Record<string, unknown>,
|
|
54
59
|
) {
|
|
60
|
+
let cachedStatus = initialStatus;
|
|
61
|
+
|
|
55
62
|
return {
|
|
56
|
-
async search(
|
|
63
|
+
async search(queryOrParams: string | MemorySearchParams = {}, opts: MemorySearchParams = {}) {
|
|
64
|
+
const legacyCall = typeof queryOrParams === "string";
|
|
65
|
+
const params = legacyCall
|
|
66
|
+
? {
|
|
67
|
+
query: queryOrParams,
|
|
68
|
+
limit: opts.limit ?? opts.k ?? opts.topK,
|
|
69
|
+
sessionId: opts.sessionId,
|
|
70
|
+
sessionKey: opts.sessionKey,
|
|
71
|
+
userId: opts.userId,
|
|
72
|
+
agentId: opts.agentId,
|
|
73
|
+
context: opts.context,
|
|
74
|
+
}
|
|
75
|
+
: queryOrParams;
|
|
57
76
|
const queryText = firstString(params.query, params.text, params.input, params.q);
|
|
58
77
|
if (!queryText) {
|
|
59
|
-
return { results: [], error: "Missing query text for LibraVDB memory search" };
|
|
78
|
+
return legacyCall ? { results: [], error: "Missing query text for LibraVDB memory search" } : [];
|
|
60
79
|
}
|
|
61
80
|
|
|
62
|
-
const userId = firstString(
|
|
63
|
-
params.userId,
|
|
64
|
-
params.context?.userId,
|
|
65
|
-
params.agentId,
|
|
66
|
-
params.context?.agentId,
|
|
67
|
-
defaults.agentId,
|
|
68
|
-
"default",
|
|
69
|
-
)!;
|
|
70
81
|
const sessionId = firstString(params.sessionId, params.context?.sessionId);
|
|
82
|
+
const userId = resolveDurableNamespace({
|
|
83
|
+
userId: firstString(params.userId, params.context?.userId),
|
|
84
|
+
sessionKey: firstString(params.sessionKey, params.context?.sessionKey),
|
|
85
|
+
agentId: firstString(params.agentId, params.context?.agentId, defaults.agentId),
|
|
86
|
+
fallback: sessionId ? `session:${sessionId}` : undefined,
|
|
87
|
+
});
|
|
71
88
|
const k = normalizePositiveInteger(params.k, params.limit, params.topK, cfg.topK, 8);
|
|
72
89
|
const collections = resolveSearchCollections(cfg, userId, sessionId);
|
|
73
90
|
const rpc = await getRpc();
|
|
@@ -85,11 +102,24 @@ function createMemorySearchManager(
|
|
|
85
102
|
excludeByCollection: {},
|
|
86
103
|
});
|
|
87
104
|
|
|
105
|
+
const legacyResults = result.results.map((item) => ({
|
|
106
|
+
...item,
|
|
107
|
+
content: item.text,
|
|
108
|
+
}));
|
|
109
|
+
if (legacyCall) {
|
|
110
|
+
return { results: legacyResults };
|
|
111
|
+
}
|
|
112
|
+
return result.results.map(toMemorySearchResult);
|
|
113
|
+
},
|
|
114
|
+
async readFile(params: { relPath: string; from?: number; lines?: number }) {
|
|
115
|
+
const located = await loadSearchResultText(getRpc, params.relPath);
|
|
116
|
+
const fromLine = Math.max(1, params.from ?? 1);
|
|
117
|
+
const lineCount = Math.max(1, params.lines ?? 200);
|
|
118
|
+
const lines = located.text.split("\n");
|
|
119
|
+
const text = lines.slice(fromLine - 1, fromLine - 1 + lineCount).join("\n");
|
|
88
120
|
return {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
content: item.text,
|
|
92
|
-
})),
|
|
121
|
+
text,
|
|
122
|
+
path: located.path,
|
|
93
123
|
};
|
|
94
124
|
},
|
|
95
125
|
async ingest() {
|
|
@@ -97,24 +127,26 @@ function createMemorySearchManager(
|
|
|
97
127
|
return { ingested: false, delegatedToContextEngine: true };
|
|
98
128
|
},
|
|
99
129
|
async sync() {
|
|
100
|
-
|
|
101
|
-
// context-engine lifecycle.
|
|
130
|
+
cachedStatus = await readStatus(getRpc, defaults.purpose);
|
|
102
131
|
return { synced: true, delegatedToContextEngine: true };
|
|
103
132
|
},
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
133
|
+
status() {
|
|
134
|
+
return cachedStatus;
|
|
135
|
+
},
|
|
136
|
+
async probeEmbeddingAvailability() {
|
|
107
137
|
return {
|
|
108
|
-
ok:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
gatingThreshold: status.gatingThreshold,
|
|
113
|
-
abstractiveReady: status.abstractiveReady ?? false,
|
|
114
|
-
embeddingProfile: status.embeddingProfile ?? "unknown",
|
|
115
|
-
purpose: defaults.purpose,
|
|
138
|
+
ok: cachedStatus.ok ?? false,
|
|
139
|
+
...(cachedStatus.ok === false && typeof cachedStatus.message === "string"
|
|
140
|
+
? { error: cachedStatus.message }
|
|
141
|
+
: {}),
|
|
116
142
|
};
|
|
117
143
|
},
|
|
144
|
+
async probeVectorAvailability() {
|
|
145
|
+
return cachedStatus.ok ?? false;
|
|
146
|
+
},
|
|
147
|
+
async close() {
|
|
148
|
+
// The sidecar connection is shared by the plugin runtime.
|
|
149
|
+
},
|
|
118
150
|
};
|
|
119
151
|
}
|
|
120
152
|
|
|
@@ -140,6 +172,84 @@ function firstString(...values: Array<string | undefined>): string | undefined {
|
|
|
140
172
|
return values.find((value) => typeof value === "string" && value.length > 0);
|
|
141
173
|
}
|
|
142
174
|
|
|
175
|
+
function toMemorySearchResult(item: SearchResult) {
|
|
176
|
+
const collection = typeof item.metadata.collection === "string" ? item.metadata.collection : "memory";
|
|
177
|
+
return {
|
|
178
|
+
path: encodeSearchResultPath(collection, item.id),
|
|
179
|
+
startLine: 1,
|
|
180
|
+
endLine: Math.max(1, item.text.split("\n").length),
|
|
181
|
+
score: item.score,
|
|
182
|
+
snippet: item.text,
|
|
183
|
+
source: collection.startsWith("session:") || collection.startsWith("session_") ? "sessions" : "memory",
|
|
184
|
+
citation: `${collection}:${item.id}`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function loadSearchResultText(getRpc: RpcGetter, relPath: string): Promise<{ path: string; text: string }> {
|
|
189
|
+
const { collection, id } = decodeSearchResultPath(relPath);
|
|
190
|
+
const rpc = await getRpc();
|
|
191
|
+
const result = await rpc.call<{ results: SearchResult[] }>("list_collection", { collection });
|
|
192
|
+
const item = result.results.find((entry) => entry.id === id);
|
|
193
|
+
if (!item) {
|
|
194
|
+
throw new Error(`LibraVDB memory path not found: ${relPath}`);
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
path: relPath,
|
|
198
|
+
text: item.text,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function encodeSearchResultPath(collection: string, id: string): string {
|
|
203
|
+
return `${encodeURIComponent(collection)}::${encodeURIComponent(id)}`;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function decodeSearchResultPath(relPath: string): { collection: string; id: string } {
|
|
207
|
+
const separator = relPath.indexOf("::");
|
|
208
|
+
if (separator <= 0) {
|
|
209
|
+
throw new Error(`Unsupported LibraVDB memory path: ${relPath}`);
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
collection: decodeURIComponent(relPath.slice(0, separator)),
|
|
213
|
+
id: decodeURIComponent(relPath.slice(separator + 2)),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function readStatus(
|
|
218
|
+
getRpc: RpcGetter,
|
|
219
|
+
purpose: string | undefined,
|
|
220
|
+
): Promise<MemoryRuntimeStatus & Record<string, unknown>> {
|
|
221
|
+
try {
|
|
222
|
+
const rpc = await getRpc();
|
|
223
|
+
const status = await rpc.call<MemoryRuntimeStatus & Record<string, unknown>>("status", {});
|
|
224
|
+
return {
|
|
225
|
+
...status,
|
|
226
|
+
backend: "builtin",
|
|
227
|
+
provider: "libravdb",
|
|
228
|
+
model: status.embeddingProfile ?? "unknown",
|
|
229
|
+
ok: status.ok ?? false,
|
|
230
|
+
message: status.message ?? "ok",
|
|
231
|
+
turnCount: status.turnCount ?? 0,
|
|
232
|
+
memoryCount: status.memoryCount ?? 0,
|
|
233
|
+
gatingThreshold: status.gatingThreshold,
|
|
234
|
+
abstractiveReady: status.abstractiveReady ?? false,
|
|
235
|
+
embeddingProfile: status.embeddingProfile ?? "unknown",
|
|
236
|
+
purpose,
|
|
237
|
+
};
|
|
238
|
+
} catch (error) {
|
|
239
|
+
return {
|
|
240
|
+
backend: "builtin",
|
|
241
|
+
provider: "libravdb",
|
|
242
|
+
model: "unknown",
|
|
243
|
+
ok: false,
|
|
244
|
+
message: error instanceof Error && error.message ? error.message : String(error),
|
|
245
|
+
turnCount: 0,
|
|
246
|
+
memoryCount: 0,
|
|
247
|
+
embeddingProfile: "unknown",
|
|
248
|
+
purpose,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
143
253
|
function normalizePositiveInteger(...values: Array<number | undefined>): number {
|
|
144
254
|
for (const value of values) {
|
|
145
255
|
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
package/src/types.ts
CHANGED
|
@@ -172,21 +172,20 @@ export interface RecallCache<T = unknown> {
|
|
|
172
172
|
clearUser(userId: string): void;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
export interface
|
|
175
|
+
export interface ContextNamespaceArgs {
|
|
176
176
|
sessionId: string;
|
|
177
|
-
|
|
177
|
+
sessionKey?: string;
|
|
178
|
+
userId?: string;
|
|
178
179
|
}
|
|
179
180
|
|
|
180
|
-
export interface
|
|
181
|
-
|
|
182
|
-
|
|
181
|
+
export interface ContextBootstrapArgs extends ContextNamespaceArgs {}
|
|
182
|
+
|
|
183
|
+
export interface ContextIngestArgs extends ContextNamespaceArgs {
|
|
183
184
|
message: MemoryMessage;
|
|
184
185
|
isHeartbeat?: boolean;
|
|
185
186
|
}
|
|
186
187
|
|
|
187
|
-
export interface ContextAssembleArgs {
|
|
188
|
-
sessionId: string;
|
|
189
|
-
userId: string;
|
|
188
|
+
export interface ContextAssembleArgs extends ContextNamespaceArgs {
|
|
190
189
|
messages: MemoryMessage[];
|
|
191
190
|
tokenBudget: number;
|
|
192
191
|
}
|