@unblocklabs/unblock-memory 0.7.0 → 0.7.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/dist/src/analysis.js +11 -12
- package/dist/src/contracts.d.ts +1 -1
- package/dist/src/extraction-chunk.d.ts +1 -0
- package/dist/src/extraction-chunk.js +1 -0
- package/dist/src/extraction-model.js +5 -4
- package/dist/src/extraction-runtime.js +2 -7
- package/dist/src/extraction-source.js +6 -2
- package/dist/src/extraction-store.d.ts +3 -0
- package/dist/src/extraction-store.js +14 -10
- package/dist/src/extraction-worker.js +31 -15
- package/dist/src/manager.d.ts +1 -1
- package/dist/src/manager.js +63 -79
- package/dist/src/memory-whisperer.js +6 -10
- package/dist/src/people-primer.js +6 -8
- package/dist/src/people-store.js +2 -0
- package/dist/src/people-tools.js +2 -6
- package/dist/src/plugin.js +6 -14
- package/dist/src/query-contract.js +18 -10
- package/dist/src/response-audit.js +3 -3
- package/dist/src/response-judge.js +1 -2
- package/dist/src/response-store.js +2 -1
- package/dist/src/retrieval-telemetry.d.ts +1 -1
- package/dist/src/review-tools.d.ts +24 -0
- package/dist/src/review-tools.js +22 -26
- package/dist/src/session-projector.js +8 -4
- package/dist/src/skill-whisperer.js +44 -15
- package/dist/src/training-candidates.d.ts +12 -13
- package/dist/src/training-candidates.js +11 -81
- package/dist/src/training-gate.js +2 -2
- package/dist/src/training-queries.js +5 -2
- package/dist/src/training-retrieval.d.ts +15 -2
- package/dist/src/training-retrieval.js +63 -39
- package/dist/src/typesafe-client.d.ts +5 -0
- package/dist/src/typesafe-client.js +4 -0
- package/dist/src/typesafe-review.js +4 -6
- package/dist/src/typesafe.d.ts +1 -9
- package/dist/src/typesafe.js +4 -10
- package/docs/configuration.md +3 -2
- package/docs/retrieval.md +15 -1
- package/openclaw.plugin.json +2 -2
- package/package.json +2 -2
|
@@ -3,7 +3,7 @@ import { trainingHash } from "./training-input.js";
|
|
|
3
3
|
import { TRAINING_GATE_THRESHOLD } from "./training-gate.js";
|
|
4
4
|
import { resolveTypeSafeApiKey, TypeSafeRequestError } from "./typesafe-client.js";
|
|
5
5
|
import { trainingTeacher, trainingTeacherMessage, trainingTeacherPrompt, TRAINING_TEACHER_MODEL, TRAINING_TEACHER_VERSION } from "./training-models.js";
|
|
6
|
-
import { historicalTrainingSearch, HistoricalCorpusUnavailableError, TRAINING_RETRIEVAL_VERSION, TRAINING_SEARCH_OPTIONS } from "./training-retrieval.js";
|
|
6
|
+
import { historicalTrainingSearch, historicalTrainingSource, HistoricalCorpusUnavailableError, TRAINING_RETRIEVAL_VERSION, TRAINING_SEARCH_OPTIONS } from "./training-retrieval.js";
|
|
7
7
|
import { contextJudgeRequest, judgeTrainingPassage, CONTEXT_JUDGE_VERSION } from "./training-judge.js";
|
|
8
8
|
const LANES = ["lex", "vec"];
|
|
9
9
|
const SELECTION_VERSION = "independent-lanes-top3-mean-v2";
|
|
@@ -162,6 +162,7 @@ export async function evaluateTrainingQueries(source, store, config, runtime, op
|
|
|
162
162
|
const refreshed = collectTraining(source, store, { existingOnly: true });
|
|
163
163
|
const result = { ...summary(), retrievals: 0, evaluated: 0, awaitingTeacher: 0 };
|
|
164
164
|
const concurrency = options.concurrency ?? TRAINING_EVALUATION_CONCURRENCY;
|
|
165
|
+
let historicalSource;
|
|
165
166
|
let key, teacher;
|
|
166
167
|
const apiKey = () => key ??= resolveTypeSafeApiKey(config.typesafe).then(value => {
|
|
167
168
|
if (!value)
|
|
@@ -214,7 +215,9 @@ export async function evaluateTrainingQueries(source, store, config, runtime, op
|
|
|
214
215
|
store.renew();
|
|
215
216
|
let snapshot;
|
|
216
217
|
try {
|
|
217
|
-
snapshot =
|
|
218
|
+
snapshot = createSearch === historicalTrainingSearch
|
|
219
|
+
? await (await (historicalSource ??= historicalTrainingSource(source.stateDir, corpus.chatTypes)))(example.timestamp)
|
|
220
|
+
: await createSearch(source.stateDir, corpus.chatTypes, example.timestamp);
|
|
218
221
|
}
|
|
219
222
|
catch (error) {
|
|
220
223
|
if (!(error instanceof HistoricalCorpusUnavailableError))
|
|
@@ -27,8 +27,21 @@ export declare function historicalPrefix(body: string, spans: readonly SessionMe
|
|
|
27
27
|
body: string;
|
|
28
28
|
spans: SessionMessageSpan[];
|
|
29
29
|
} | undefined;
|
|
30
|
-
/**
|
|
31
|
-
|
|
30
|
+
/** Capture source bytes once per run; each cutoff still owns its index and native model context. */
|
|
31
|
+
export declare function historicalTrainingSource(stateDir: string, chatTypes: readonly ChatType[]): Promise<(cutoff: number, openStore?: typeof import("@unblocklabs/qmd")["createStore"]) => Promise<{
|
|
32
|
+
corpusHash: string;
|
|
33
|
+
report: {
|
|
34
|
+
sessions: number;
|
|
35
|
+
chunks: number;
|
|
36
|
+
excluded: number;
|
|
37
|
+
truncated: number;
|
|
38
|
+
excludedChunks: number;
|
|
39
|
+
};
|
|
40
|
+
maxDate: string;
|
|
41
|
+
search: (query: string, lane: QueryLane) => Promise<TrainingHit[]>;
|
|
42
|
+
close: () => Promise<void>;
|
|
43
|
+
}>>;
|
|
44
|
+
/** Standalone calls get a fresh source capture; evaluation runs reuse their own capture. */
|
|
32
45
|
export declare function historicalTrainingSearch(stateDir: string, chatTypes: readonly ChatType[], cutoff: number, openStore?: typeof import("@unblocklabs/qmd")["createStore"]): Promise<{
|
|
33
46
|
corpusHash: string;
|
|
34
47
|
report: {
|
|
@@ -47,9 +47,8 @@ export function historicalPrefix(body, spans, cutoff) {
|
|
|
47
47
|
return;
|
|
48
48
|
return { body: body.slice(0, parsed[safe.length]?.span.start ?? body.length), spans: safe.map(s => s.span) };
|
|
49
49
|
}
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
export async function historicalTrainingSearch(stateDir, chatTypes, cutoff, openStore) {
|
|
50
|
+
/** Capture source bytes once per run; each cutoff still owns its index and native model context. */
|
|
51
|
+
export async function historicalTrainingSource(stateDir, chatTypes) {
|
|
53
52
|
const indexPath = join(stateDir, "index.sqlite");
|
|
54
53
|
try {
|
|
55
54
|
statSync(indexPath);
|
|
@@ -62,22 +61,13 @@ export async function historicalTrainingSearch(stateDir, chatTypes, cutoff, open
|
|
|
62
61
|
}
|
|
63
62
|
const manifest = await readSessionManifest(join(stateDir, "sessions-manifest.json"));
|
|
64
63
|
const source = resolveSessionSource(join(stateDir, "sessions"), chatTypes);
|
|
65
|
-
const createStore = openStore ?? (await import("@unblocklabs/qmd")).createStore;
|
|
66
|
-
const maxDate = new Date(cutoff).toISOString();
|
|
67
|
-
const report = { sessions: 0, chunks: 0, excluded: 0, truncated: 0, excludedChunks: 0 };
|
|
68
|
-
const metadata = new Map();
|
|
69
|
-
const fingerprint = createHash("sha256").update(JSON.stringify([TRAINING_RETRIEVAL_VERSION, chatTypes.toSorted(), maxDate]));
|
|
70
64
|
// Use QMD's exact binding, never mix node:sqlite with sqlite-vec.
|
|
71
65
|
const requireQmd = createRequire(import.meta.resolve("@unblocklabs/qmd"));
|
|
72
66
|
const Database = requireQmd("better-sqlite3");
|
|
73
67
|
const sourceDb = new Database(indexPath, { readonly: true, fileMustExist: true });
|
|
74
|
-
const
|
|
75
|
-
let
|
|
76
|
-
|
|
77
|
-
sourceDb.close();
|
|
78
|
-
sourceClosed = true;
|
|
79
|
-
} documents.length = 0; };
|
|
80
|
-
let indexed;
|
|
68
|
+
const sessions = [];
|
|
69
|
+
let excluded = 0;
|
|
70
|
+
let settings;
|
|
81
71
|
try {
|
|
82
72
|
const extension = requireQmd("sqlite-vec");
|
|
83
73
|
if (!extension || typeof extension !== "object" || !("getLoadablePath" in extension) || typeof extension.getLoadablePath !== "function") {
|
|
@@ -88,16 +78,13 @@ export async function historicalTrainingSearch(stateDir, chatTypes, cutoff, open
|
|
|
88
78
|
throw new Error("QMD sqlite-vec path unavailable");
|
|
89
79
|
sourceDb.loadExtension(extensionPath);
|
|
90
80
|
sourceDb.exec("PRAGMA query_only=ON; PRAGMA busy_timeout=5000; BEGIN");
|
|
91
|
-
|
|
92
|
-
fingerprint.update(JSON.stringify(settings));
|
|
81
|
+
settings = sourceDb.prepare("SELECT key,value FROM store_config WHERE key='embedding_chunk_strategy'").all();
|
|
93
82
|
const document = sourceDb.prepare(`SELECT d.hash,c.doc FROM documents d JOIN content c ON c.hash=d.hash
|
|
94
83
|
WHERE d.active=1 AND d.collection=? AND d.path=?`);
|
|
95
84
|
const chunks = sourceDb.prepare(`SELECT cv.seq,cv.pos,cv.chunk_len,cv.model,cv.embed_fingerprint,v.embedding
|
|
96
85
|
FROM content_vectors cv JOIN vectors_vec v ON v.hash_seq=cv.hash||'_'||cv.seq
|
|
97
86
|
WHERE cv.hash=? ORDER BY cv.seq`);
|
|
98
|
-
|
|
99
|
-
// Both passes use this same read transaction. Cached evaluations still
|
|
100
|
-
// verify every prefix/vector byte, but never build an index or load a model.
|
|
87
|
+
const vectors = new Map();
|
|
101
88
|
let yieldAt = performance.now() + 25;
|
|
102
89
|
for (const session of Object.values(manifest.sessions).sort((a, b) => a.documentPath.localeCompare(b.documentPath))) {
|
|
103
90
|
if (performance.now() >= yieldAt) {
|
|
@@ -106,28 +93,67 @@ export async function historicalTrainingSearch(stateDir, chatTypes, cutoff, open
|
|
|
106
93
|
}
|
|
107
94
|
// Loggie projections can retroactively annotate old text using later revisions.
|
|
108
95
|
// Workspace files and meetings lack immutable historical content proof here.
|
|
109
|
-
if (!chatTypes.includes(session.chatType) || session.provider === "loggie"
|
|
110
|
-
|
|
96
|
+
if (!chatTypes.includes(session.chatType) || session.provider === "loggie") {
|
|
97
|
+
excluded++;
|
|
111
98
|
continue;
|
|
112
99
|
}
|
|
113
100
|
const row = document.get(source.collection, session.documentPath);
|
|
114
101
|
if (!row || createHash("sha256").update(row.doc).digest("hex") !== session.projectionHash) {
|
|
102
|
+
excluded++;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
let captured = vectors.get(row.hash);
|
|
106
|
+
if (!captured) {
|
|
107
|
+
captured = [];
|
|
108
|
+
for (const chunk of chunks.iterate(row.hash)) {
|
|
109
|
+
if (performance.now() >= yieldAt) {
|
|
110
|
+
await yieldToEventLoop();
|
|
111
|
+
yieldAt = performance.now() + 25;
|
|
112
|
+
}
|
|
113
|
+
captured.push({ ...chunk, embedding: Buffer.from(chunk.embedding) });
|
|
114
|
+
}
|
|
115
|
+
vectors.set(row.hash, captured);
|
|
116
|
+
}
|
|
117
|
+
sessions.push({ session, body: row.doc, chunks: captured });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
sourceDb.close();
|
|
122
|
+
}
|
|
123
|
+
return async (cutoff, openStore) => {
|
|
124
|
+
const createStore = openStore ?? (await import("@unblocklabs/qmd")).createStore;
|
|
125
|
+
const maxDate = new Date(cutoff).toISOString();
|
|
126
|
+
const report = { sessions: 0, chunks: 0, excluded, truncated: 0, excludedChunks: 0 };
|
|
127
|
+
const metadata = new Map();
|
|
128
|
+
const fingerprint = createHash("sha256").update(JSON.stringify([TRAINING_RETRIEVAL_VERSION, chatTypes.toSorted(), maxDate]));
|
|
129
|
+
fingerprint.update(JSON.stringify(settings));
|
|
130
|
+
const documents = [];
|
|
131
|
+
let dimensions, closed = false;
|
|
132
|
+
let indexed;
|
|
133
|
+
let yieldAt = performance.now() + 25;
|
|
134
|
+
for (const { session, body, chunks } of sessions) {
|
|
135
|
+
if (performance.now() >= yieldAt) {
|
|
136
|
+
await yieldToEventLoop();
|
|
137
|
+
yieldAt = performance.now() + 25;
|
|
138
|
+
}
|
|
139
|
+
if (session.startedAt >= cutoff) {
|
|
115
140
|
report.excluded++;
|
|
116
141
|
continue;
|
|
117
142
|
}
|
|
118
|
-
const prefix = historicalPrefix(
|
|
143
|
+
const prefix = historicalPrefix(body, session.messages, cutoff);
|
|
119
144
|
if (!prefix) {
|
|
120
145
|
report.excluded++;
|
|
121
146
|
continue;
|
|
122
147
|
}
|
|
123
148
|
const hash = createHash("sha256").update(prefix.body).digest("hex");
|
|
124
|
-
|
|
149
|
+
const safeChunks = [];
|
|
150
|
+
documents.push({ path: session.documentPath, hash, body: prefix.body, chunks: safeChunks });
|
|
125
151
|
metadata.set(`qmd://${source.collection}/${session.documentPath}`, prefix.spans);
|
|
126
152
|
fingerprint.update(JSON.stringify([session.documentPath, hash]));
|
|
127
153
|
report.sessions++;
|
|
128
154
|
if (prefix.spans.length < (session.messages?.length ?? 0))
|
|
129
155
|
report.truncated++;
|
|
130
|
-
for (const chunk of chunks
|
|
156
|
+
for (const chunk of chunks) {
|
|
131
157
|
if (performance.now() >= yieldAt) {
|
|
132
158
|
await yieldToEventLoop();
|
|
133
159
|
yieldAt = performance.now() + 25;
|
|
@@ -136,13 +162,14 @@ export async function historicalTrainingSearch(stateDir, chatTypes, cutoff, open
|
|
|
136
162
|
report.excludedChunks++;
|
|
137
163
|
continue;
|
|
138
164
|
}
|
|
139
|
-
const bytes =
|
|
165
|
+
const bytes = chunk.embedding;
|
|
140
166
|
if (bytes.length % 4 || !bytes.length)
|
|
141
167
|
throw new Error("Invalid historical embedding");
|
|
142
168
|
const size = bytes.length / 4;
|
|
143
169
|
dimensions ??= size;
|
|
144
170
|
if (dimensions !== size)
|
|
145
171
|
throw new Error("Mixed historical embedding dimensions");
|
|
172
|
+
safeChunks.push(chunk);
|
|
146
173
|
fingerprint.update(JSON.stringify([chunk.seq, chunk.pos, chunk.chunk_len, chunk.model, chunk.embed_fingerprint])).update(bytes);
|
|
147
174
|
report.chunks++;
|
|
148
175
|
}
|
|
@@ -175,14 +202,13 @@ export async function historicalTrainingSearch(stateDir, chatTypes, cutoff, open
|
|
|
175
202
|
}
|
|
176
203
|
qmd.internal.insertContent(document.hash, document.body, maxDate);
|
|
177
204
|
qmd.internal.insertDocument(source.collection, document.path, "Transcript", document.hash, maxDate, maxDate);
|
|
178
|
-
for (const chunk of
|
|
205
|
+
for (const chunk of document.chunks) {
|
|
179
206
|
if (performance.now() >= yieldAt) {
|
|
180
207
|
await yieldToEventLoop();
|
|
181
208
|
yieldAt = performance.now() + 25;
|
|
182
209
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const bytes = Buffer.from(chunk.embedding), vector = new Float32Array(bytes.length / 4);
|
|
210
|
+
const bytes = Buffer.from(chunk.embedding.buffer, chunk.embedding.byteOffset, chunk.embedding.byteLength);
|
|
211
|
+
const vector = new Float32Array(bytes.length / 4);
|
|
186
212
|
for (let i = 0; i < vector.length; i++)
|
|
187
213
|
vector[i] = bytes.readFloatLE(i * 4);
|
|
188
214
|
qmd.internal.insertEmbedding(document.hash, chunk.seq, chunk.pos, vector, chunk.model, maxDate, 1, chunk.embed_fingerprint, chunk.chunk_len);
|
|
@@ -195,9 +221,6 @@ export async function historicalTrainingSearch(stateDir, chatTypes, cutoff, open
|
|
|
195
221
|
await qmd?.close();
|
|
196
222
|
throw error;
|
|
197
223
|
}
|
|
198
|
-
finally {
|
|
199
|
-
closeSource();
|
|
200
|
-
}
|
|
201
224
|
};
|
|
202
225
|
return { corpusHash, report, maxDate,
|
|
203
226
|
search: async (query, lane) => {
|
|
@@ -224,14 +247,15 @@ export async function historicalTrainingSearch(stateDir, chatTypes, cutoff, open
|
|
|
224
247
|
await indexed.then(qmd => qmd.close(), () => { });
|
|
225
248
|
}
|
|
226
249
|
finally {
|
|
227
|
-
|
|
250
|
+
documents.length = 0;
|
|
228
251
|
}
|
|
229
252
|
} };
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
/** Standalone calls get a fresh source capture; evaluation runs reuse their own capture. */
|
|
256
|
+
export async function historicalTrainingSearch(stateDir, chatTypes, cutoff, openStore) {
|
|
257
|
+
const source = await historicalTrainingSource(stateDir, chatTypes);
|
|
258
|
+
return source(cutoff, openStore);
|
|
235
259
|
}
|
|
236
260
|
function historicalChunk(chunk, length) {
|
|
237
261
|
return Number.isSafeInteger(chunk.pos) && Number.isSafeInteger(chunk.chunk_len) && chunk.pos >= 0 && chunk.chunk_len > 0 &&
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
1
2
|
import type { UnblockMemoryConfig } from "./config.js";
|
|
2
3
|
export declare const TYPESAFE_MODEL = "jev-1.13.0";
|
|
4
|
+
export declare const TYPESAFE_NOUL_SCHEMA: Type.TObject<{
|
|
5
|
+
type: Type.TLiteral<"noul">;
|
|
6
|
+
noul: Type.TNumber;
|
|
7
|
+
}>;
|
|
3
8
|
export declare class TypeSafeRequestError extends Error {
|
|
4
9
|
readonly code: "timeout" | "cancelled" | "http_error" | "invalid_response" | "network_error";
|
|
5
10
|
readonly status?: number | undefined;
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { parseEnv } from "node:util";
|
|
3
|
+
import { Type } from "typebox";
|
|
3
4
|
export const TYPESAFE_MODEL = "jev-1.13.0";
|
|
5
|
+
export const TYPESAFE_NOUL_SCHEMA = Type.Object({
|
|
6
|
+
type: Type.Literal("noul"), noul: Type.Number({ minimum: 0, maximum: 1 }),
|
|
7
|
+
});
|
|
4
8
|
export class TypeSafeRequestError extends Error {
|
|
5
9
|
code;
|
|
6
10
|
status;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
3
|
import { backgroundWordCount, PEOPLE_BACKGROUND_MAX_WORDS } from "./people-background.js";
|
|
4
|
-
import { requestTypeSafe } from "./typesafe-client.js";
|
|
4
|
+
import { requestTypeSafe, TYPESAFE_NOUL_SCHEMA } from "./typesafe-client.js";
|
|
5
5
|
export { TYPESAFE_MODEL as TYPESAFE_REVIEW_MODEL } from "./typesafe-client.js";
|
|
6
6
|
const relationSchema = Type.Object({ answers: Type.Object({ relation: Type.Object({
|
|
7
7
|
type: Type.Literal("choice"),
|
|
@@ -61,8 +61,8 @@ export async function reviewTypeSafeClaim(params) {
|
|
|
61
61
|
let background;
|
|
62
62
|
if (params.personBackground) {
|
|
63
63
|
const schema = Type.Object({ answers: Type.Object({
|
|
64
|
-
backgroundOnly:
|
|
65
|
-
explicitSupport:
|
|
64
|
+
backgroundOnly: TYPESAFE_NOUL_SCHEMA,
|
|
65
|
+
explicitSupport: TYPESAFE_NOUL_SCHEMA,
|
|
66
66
|
}) });
|
|
67
67
|
if (!Value.Check(schema, payload))
|
|
68
68
|
throw new Error("TypeSafe returned an invalid background review");
|
|
@@ -73,9 +73,7 @@ export async function reviewTypeSafeClaim(params) {
|
|
|
73
73
|
needsReview: answer.choice !== "supports" || answer.confidence < 0.9 ||
|
|
74
74
|
(background !== undefined && (background.backgroundOnly < 0.9 || background.explicitSupport < 0.9)) };
|
|
75
75
|
}
|
|
76
|
-
const nouls = Type.Object({ answers: Type.Record(Type.String(),
|
|
77
|
-
type: Type.Literal("noul"), noul: Type.Number({ minimum: 0, maximum: 1 }),
|
|
78
|
-
})) });
|
|
76
|
+
const nouls = Type.Object({ answers: Type.Record(Type.String(), TYPESAFE_NOUL_SCHEMA) });
|
|
79
77
|
/** Directional coverage, not topic similarity. Bounded at six comparisons of four ranked candidates. */
|
|
80
78
|
export async function reviewMemoryRedundancy(params) {
|
|
81
79
|
if (params.excerpts.length > 4)
|
package/dist/src/typesafe.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { QueryConversation } from "./query-contract.js";
|
|
|
3
3
|
export declare function selectTypeSafeSkill(params: {
|
|
4
4
|
apiKey: string;
|
|
5
5
|
timeoutMs: number;
|
|
6
|
+
signal?: AbortSignal;
|
|
6
7
|
currentRequest: string;
|
|
7
8
|
history: readonly {
|
|
8
9
|
role: "user" | "assistant";
|
|
@@ -82,12 +83,3 @@ export declare function judgeMemoryPassage(request: ReturnType<typeof memoryUsef
|
|
|
82
83
|
output_tokens: number;
|
|
83
84
|
} | null;
|
|
84
85
|
}>;
|
|
85
|
-
/** One HTTP request per candidate; result order matches input order. */
|
|
86
|
-
export declare function judgeTypeSafeMemories(params: {
|
|
87
|
-
apiKey: string;
|
|
88
|
-
timeoutMs: number;
|
|
89
|
-
signal: AbortSignal;
|
|
90
|
-
conversation: QueryConversation;
|
|
91
|
-
asOf: string;
|
|
92
|
-
candidates: readonly MemoryPassage[];
|
|
93
|
-
}): Promise<number[]>;
|
package/dist/src/typesafe.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
|
-
import { requestTypeSafe, TypeSafeRequestError, TYPESAFE_MODEL } from "./typesafe-client.js";
|
|
3
|
+
import { requestTypeSafe, TypeSafeRequestError, TYPESAFE_MODEL, TYPESAFE_NOUL_SCHEMA } from "./typesafe-client.js";
|
|
4
4
|
import { MEMORY_PASSAGE_CHARS } from "./memory-passage.js";
|
|
5
5
|
const SKILL_MIN_USEFULNESS = 0.7;
|
|
6
6
|
/** Select from trusted candidates; never accept a provider-generated path or skill name. */
|
|
@@ -26,7 +26,7 @@ export async function selectTypeSafeSkill(params) {
|
|
|
26
26
|
/** One skill per request; rank the comparable usefulness probabilities in code. */
|
|
27
27
|
async function judgeTypeSafeSkill(params) {
|
|
28
28
|
let payload;
|
|
29
|
-
payload = await requestTypeSafe(
|
|
29
|
+
payload = await requestTypeSafe(params, { currentRequest: params.currentRequest, history: params.history,
|
|
30
30
|
candidate: { name: params.candidate.name, description: params.candidate.description } }, { useful: {
|
|
31
31
|
type: "noul",
|
|
32
32
|
instructions: {
|
|
@@ -51,9 +51,7 @@ async function judgeTypeSafeSkill(params) {
|
|
|
51
51
|
return payload.answers.useful.noul;
|
|
52
52
|
}
|
|
53
53
|
const memoryAnswersSchema = Type.Object({
|
|
54
|
-
answers: Type.Record(Type.String(),
|
|
55
|
-
type: Type.Literal("noul"), noul: Type.Number({ minimum: 0, maximum: 1 }),
|
|
56
|
-
})),
|
|
54
|
+
answers: Type.Record(Type.String(), TYPESAFE_NOUL_SCHEMA),
|
|
57
55
|
});
|
|
58
56
|
export const QUALITY_JUDGE_VERSION = "jev-1.13.0:quality-v3-isolated";
|
|
59
57
|
/** These are indicators for review, never authorization to delete or rewrite. */
|
|
@@ -137,7 +135,7 @@ export function memoryUsefulnessRequest(conversation, candidate, asOf) {
|
|
|
137
135
|
}
|
|
138
136
|
const memoryJudgmentSchema = Type.Object({
|
|
139
137
|
model: Type.Literal(TYPESAFE_MODEL),
|
|
140
|
-
answers: Type.Object({ memory_0:
|
|
138
|
+
answers: Type.Object({ memory_0: TYPESAFE_NOUL_SCHEMA }, { additionalProperties: false }),
|
|
141
139
|
usage: Type.Optional(Type.Object({ input_tokens: Type.Integer({ minimum: 0 }), output_tokens: Type.Integer({ minimum: 0 }) })),
|
|
142
140
|
});
|
|
143
141
|
function parseMemoryJudgment(payload) {
|
|
@@ -150,7 +148,3 @@ function parseMemoryJudgment(payload) {
|
|
|
150
148
|
export async function judgeMemoryPassage(request, params) {
|
|
151
149
|
return parseMemoryJudgment(await requestTypeSafe(params, request.state, request.questions));
|
|
152
150
|
}
|
|
153
|
-
/** One HTTP request per candidate; result order matches input order. */
|
|
154
|
-
export async function judgeTypeSafeMemories(params) {
|
|
155
|
-
return Promise.all(params.candidates.map(async (candidate) => (await judgeMemoryPassage(memoryUsefulnessRequest(params.conversation, candidate, params.asOf), params)).probability));
|
|
156
|
-
}
|
package/docs/configuration.md
CHANGED
|
@@ -86,7 +86,8 @@ The plugin reads only `TYPESAFE_API_KEY` from the environment. Standalone QMD al
|
|
|
86
86
|
supports `TYPESAFE_API_KEY_FILE`; these are separate credential resolvers.
|
|
87
87
|
`typesafe.timeoutMs` is not a universal total deadline: People Primer and dossier
|
|
88
88
|
save review use `peoplePrimer.timeoutMs` per request; Memory Whisperer has its
|
|
89
|
-
own overall budget.
|
|
89
|
+
own overall budget. Skill Whisperer's fixed three-second total budget includes
|
|
90
|
+
credential resolution, retrieval and judging, even when its provider timeout is longer.
|
|
90
91
|
|
|
91
92
|
## Example profiles
|
|
92
93
|
|
|
@@ -214,7 +215,7 @@ contract; these tables explain their effects.
|
|
|
214
215
|
| `typesafe.apiKey` | Unset | Explicit inline key; mutually exclusive with key file; prefer file |
|
|
215
216
|
| `typesafe.apiKeyFile` | Unset | Absolute raw-key or dotenv file, reread at credential resolution; explicit missing file never falls back to a different key |
|
|
216
217
|
| `typesafe.timeoutMs` | `1500` | 1–10,000 per request for Skill/Memory Whisperer, quality/claim/cluster review and response audit; **primer and dossier save use `peoplePrimer.timeoutMs` instead** |
|
|
217
|
-
| `skillWhisperer.enabled` | `false` | Requires explicit skills corpus and appropriate host hook access |
|
|
218
|
+
| `skillWhisperer.enabled` | `false` | Requires explicit skills corpus and appropriate host hook access; fixed 3-second total deadline |
|
|
218
219
|
| `skillWhisperer.historyMessages` | `5` | Nonnegative integer; prior visible messages used for routing |
|
|
219
220
|
| `skillWhisperer.minScore` | `0.5` | 0–1, **local vector fallback only**, ignored for TypeSafe shortlist admission |
|
|
220
221
|
| `skillWhisperer.cooldownTurns` | `10` | Nonnegative user-turn count; no fallback to weaker cooling-down alternatives |
|
package/docs/retrieval.md
CHANGED
|
@@ -380,6 +380,12 @@ for the trusted LAN/loopback only; use TLS or a private tunnel across untrusted
|
|
|
380
380
|
Visible conversation text is sent to this endpoint, never tool outputs/thinking.
|
|
381
381
|
Approve only corpora suitable for every audience of the agent.
|
|
382
382
|
|
|
383
|
+
The packaged `src/mlx-query-worker.py` is a standalone JSON-lines worker for
|
|
384
|
+
separately managed model-serving integrations, not a Gateway subprocess or HTTP
|
|
385
|
+
server. It is retained with `query_contract.py` and the pinned tokenizer assets;
|
|
386
|
+
the latter also define the training-data contract. A host service owns any adapter
|
|
387
|
+
between that private worker protocol and the HTTP API below.
|
|
388
|
+
|
|
383
389
|
`POST /generate` accepts `{id, conversation: {history, currentRequest}}` and returns
|
|
384
390
|
`{response: {id, text}}`, where `text` is JSON with exactly two nonempty strings,
|
|
385
391
|
`lex` and `vec`. The request ID must match. Redirects, malformed/oversized responses,
|
|
@@ -464,6 +470,13 @@ successful siblings can still qualify. If all fail, no hint is emitted. It does
|
|
|
464
470
|
not switch to vector-only selection. There are no automatic HTTP retries. Other
|
|
465
471
|
credential-file read errors likewise produce a warning and no hint.
|
|
466
472
|
|
|
473
|
+
The complete skill contribution has a fixed three-second deadline, including
|
|
474
|
+
credential resolution, retrieval and provider calls. A slow skill lookup cannot
|
|
475
|
+
hold up ready memory or people hints beyond that budget. Session teardown,
|
|
476
|
+
superseding turns and Gateway shutdown cancel its wait and provider requests;
|
|
477
|
+
shared indexing/embedding work can finish for other callers, but cannot trigger
|
|
478
|
+
late Jev calls or hints for the cancelled run.
|
|
479
|
+
|
|
467
480
|
**Privacy:** enabled TypeSafe selection sends up to 12,000 characters of current
|
|
468
481
|
prompt/recent user-assistant text, plus the shortlisted names/descriptions, to
|
|
469
482
|
`api.typesafe.ai`. Source-path fields, full skill procedures, tool-result messages,
|
|
@@ -476,7 +489,8 @@ The defaults use five prior messages, a vector-only score threshold of `0.5`,
|
|
|
476
489
|
and a ten-turn cooldown. A skill is cooling down after either a suggestion or a
|
|
477
490
|
successful direct `read` of its indexed `SKILL.md`. When the selected
|
|
478
491
|
skill is cooling down, no hint is emitted; Skill Whisperer does not fall through
|
|
479
|
-
to a weaker match.
|
|
492
|
+
to a weaker match. When every shortlisted skill is cooling down, Jev is skipped.
|
|
493
|
+
Cooldown state is per session and intentionally resets with
|
|
480
494
|
the Gateway. Shell-command reads are not tracked.
|
|
481
495
|
|
|
482
496
|
The `skills` corpus shares the existing QMD store and warm embedding model but
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "unblock-memory",
|
|
3
3
|
"name": "Unblock Memory",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.2",
|
|
5
5
|
"description": "Indexes, retrieves, and analyzes configured workspace memory with existing QMD vectors.",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"activation": { "onStartup": true },
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"help": "Shared provider gate for opted-in whisperers, people primer/save review, evidence/quality reviews and response audits. Does not enable features or approve corpora by itself. Ordinary search and People Whisperer injection stay local."
|
|
68
68
|
},
|
|
69
69
|
"typesafe.apiKey": { "label": "TypeSafe API Key", "sensitive": true },
|
|
70
|
-
"typesafe.timeoutMs": { "label": "TypeSafe Request Timeout", "help": "Per-request timeout, not the whole operation. People primer/dossier review use peoplePrimer.timeoutMs instead; Memory Whisperer
|
|
70
|
+
"typesafe.timeoutMs": { "label": "TypeSafe Request Timeout", "help": "Per-request timeout, not the whole operation. People primer/dossier review use peoplePrimer.timeoutMs instead; Memory Whisperer has its own total deadline, and Skill Whisperer has a fixed 3-second total budget." },
|
|
71
71
|
"typesafe.apiKeyFile": {
|
|
72
72
|
"label": "TypeSafe Key File",
|
|
73
73
|
"help": "Absolute path to a plaintext API key or a dotenv file containing TYPESAFE_API_KEY. Prefer this over storing a key in config."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unblocklabs/unblock-memory",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Workspace-native memory for OpenClaw, powered by QMD",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@huggingface/tokenizers": "0.2.0",
|
|
49
|
-
"@unblocklabs/qmd": "https://github.com/unblocklabs-ai/qmd/releases/download/v2.
|
|
49
|
+
"@unblocklabs/qmd": "https://github.com/unblocklabs-ai/qmd/releases/download/v2.11.0/unblocklabs-qmd-2.11.0.tgz",
|
|
50
50
|
"chokidar": "5.0.0",
|
|
51
51
|
"js-tiktoken": "1.0.21",
|
|
52
52
|
"picomatch": "^4.0.5",
|