@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.
Files changed (41) hide show
  1. package/dist/src/analysis.js +11 -12
  2. package/dist/src/contracts.d.ts +1 -1
  3. package/dist/src/extraction-chunk.d.ts +1 -0
  4. package/dist/src/extraction-chunk.js +1 -0
  5. package/dist/src/extraction-model.js +5 -4
  6. package/dist/src/extraction-runtime.js +2 -7
  7. package/dist/src/extraction-source.js +6 -2
  8. package/dist/src/extraction-store.d.ts +3 -0
  9. package/dist/src/extraction-store.js +14 -10
  10. package/dist/src/extraction-worker.js +31 -15
  11. package/dist/src/manager.d.ts +1 -1
  12. package/dist/src/manager.js +63 -79
  13. package/dist/src/memory-whisperer.js +6 -10
  14. package/dist/src/people-primer.js +6 -8
  15. package/dist/src/people-store.js +2 -0
  16. package/dist/src/people-tools.js +2 -6
  17. package/dist/src/plugin.js +6 -14
  18. package/dist/src/query-contract.js +18 -10
  19. package/dist/src/response-audit.js +3 -3
  20. package/dist/src/response-judge.js +1 -2
  21. package/dist/src/response-store.js +2 -1
  22. package/dist/src/retrieval-telemetry.d.ts +1 -1
  23. package/dist/src/review-tools.d.ts +24 -0
  24. package/dist/src/review-tools.js +22 -26
  25. package/dist/src/session-projector.js +8 -4
  26. package/dist/src/skill-whisperer.js +44 -15
  27. package/dist/src/training-candidates.d.ts +12 -13
  28. package/dist/src/training-candidates.js +11 -81
  29. package/dist/src/training-gate.js +2 -2
  30. package/dist/src/training-queries.js +5 -2
  31. package/dist/src/training-retrieval.d.ts +15 -2
  32. package/dist/src/training-retrieval.js +63 -39
  33. package/dist/src/typesafe-client.d.ts +5 -0
  34. package/dist/src/typesafe-client.js +4 -0
  35. package/dist/src/typesafe-review.js +4 -6
  36. package/dist/src/typesafe.d.ts +1 -9
  37. package/dist/src/typesafe.js +4 -10
  38. package/docs/configuration.md +3 -2
  39. package/docs/retrieval.md +15 -1
  40. package/openclaw.plugin.json +2 -2
  41. 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 = await createSearch(source.stateDir, corpus.chatTypes, example.timestamp);
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
- /** Fingerprint a read-only source snapshot; build its search index only on demand.
31
- * No filesystem projection, live-index mutation, model re-embedding or dependency patch. */
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
- /** Fingerprint a read-only source snapshot; build its search index only on demand.
51
- * No filesystem projection, live-index mutation, model re-embedding or dependency patch. */
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 documents = [];
75
- let sourceClosed = false, closed = false;
76
- const closeSource = () => { if (!sourceClosed) {
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
- const settings = sourceDb.prepare("SELECT key,value FROM store_config WHERE key='embedding_chunk_strategy'").all();
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
- let dimensions;
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" || session.startedAt >= cutoff) {
110
- report.excluded++;
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(row.doc, session.messages, cutoff);
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
- documents.push({ path: session.documentPath, sourceHash: row.hash, hash, body: prefix.body });
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.iterate(row.hash)) {
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 = Buffer.from(chunk.embedding);
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 chunks.iterate(document.sourceHash)) {
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
- if (!historicalChunk(chunk, document.body.length))
184
- continue;
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
- closeSource();
250
+ documents.length = 0;
228
251
  }
229
252
  } };
230
- }
231
- catch (error) {
232
- closeSource();
233
- throw error;
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: Type.Object({ type: Type.Literal("noul"), noul: Type.Number({ minimum: 0, maximum: 1 }) }),
65
- explicitSupport: Type.Object({ type: Type.Literal("noul"), noul: Type.Number({ minimum: 0, maximum: 1 }) }),
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(), Type.Object({
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)
@@ -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[]>;
@@ -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({ apiKey: params.apiKey, timeoutMs: params.timeoutMs }, { currentRequest: params.currentRequest, history: params.history,
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(), Type.Object({
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: Type.Object({ type: Type.Literal("noul"), noul: Type.Number({ minimum: 0, maximum: 1 }) }) }, { additionalProperties: false }),
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
- }
@@ -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. Cooldown state is per session and intentionally resets with
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "unblock-memory",
3
3
  "name": "Unblock Memory",
4
- "version": "0.7.0",
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 also has its own total deadline." },
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.0",
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.10.3/unblocklabs-qmd-2.10.3.tgz",
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",