@unblocklabs/unblock-memory 0.3.27 → 0.3.28

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 (53) hide show
  1. package/dist/src/cluster-review.js +1 -1
  2. package/dist/src/config.d.ts +4 -0
  3. package/dist/src/config.js +14 -1
  4. package/dist/src/diagnostics.d.ts +13 -4
  5. package/dist/src/diagnostics.js +5 -0
  6. package/dist/src/manager.d.ts +3 -1
  7. package/dist/src/manager.js +31 -0
  8. package/dist/src/memory-whisperer.d.ts +2 -1
  9. package/dist/src/memory-whisperer.js +240 -112
  10. package/dist/src/mlx-query.d.ts +22 -0
  11. package/dist/src/mlx-query.js +155 -0
  12. package/dist/src/people-dossier-review.js +1 -1
  13. package/dist/src/people-hooks.d.ts +1 -1
  14. package/dist/src/people-hooks.js +13 -7
  15. package/dist/src/people-primer-tool.js +1 -1
  16. package/dist/src/people-primer.js +3 -2
  17. package/dist/src/plugin.js +10 -5
  18. package/dist/src/quality-audit.js +86 -81
  19. package/dist/src/response-audit.js +1 -1
  20. package/dist/src/response-judge.d.ts +1 -1
  21. package/dist/src/response-judge.js +18 -15
  22. package/dist/src/response-stages.d.ts +8 -8
  23. package/dist/src/response-text.d.ts +5 -0
  24. package/dist/src/response-text.js +23 -1
  25. package/dist/src/retrieval-telemetry.d.ts +2 -2
  26. package/dist/src/retrieval-telemetry.js +1 -1
  27. package/dist/src/review-tools.js +1 -1
  28. package/dist/src/skill-whisperer.d.ts +1 -1
  29. package/dist/src/skill-whisperer.js +76 -63
  30. package/dist/src/training-candidates.d.ts +9 -1
  31. package/dist/src/training-candidates.js +12 -7
  32. package/dist/src/training-gate.js +3 -3
  33. package/dist/src/training-input.js +2 -23
  34. package/dist/src/training-judge.js +3 -3
  35. package/dist/src/training-queries.js +5 -4
  36. package/dist/src/training.js +5 -4
  37. package/dist/src/typesafe-client.d.ts +15 -0
  38. package/dist/src/typesafe-client.js +75 -0
  39. package/dist/src/typesafe-review.d.ts +2 -6
  40. package/dist/src/typesafe-review.js +49 -55
  41. package/dist/src/typesafe.d.ts +3 -7
  42. package/dist/src/typesafe.js +108 -163
  43. package/dist/src/whisperer-prompt.d.ts +3 -0
  44. package/dist/src/whisperer-prompt.js +21 -0
  45. package/docs/configuration.md +12 -2
  46. package/docs/peoplesql.md +3 -1
  47. package/docs/response-audit.md +4 -1
  48. package/docs/retrieval.md +128 -20
  49. package/openclaw.plugin.json +13 -4
  50. package/package.json +2 -1
  51. package/src/mlx-query-worker.py +104 -0
  52. package/dist/src/typesafe-transport.d.ts +0 -10
  53. package/dist/src/typesafe-transport.js +0 -26
@@ -50,8 +50,8 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
50
50
  quality: {
51
51
  taskType: {
52
52
  type: "choice";
53
- choice: "action" | "question" | "artifact" | "discussion" | "other";
54
53
  confidence: number;
54
+ choice: "action" | "question" | "artifact" | "discussion" | "other";
55
55
  probabilities: {
56
56
  [x: string]: number;
57
57
  [x: number]: number;
@@ -59,8 +59,8 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
59
59
  };
60
60
  assessability: {
61
61
  type: "choice";
62
- choice: "assessable" | "not_assessable";
63
62
  confidence: number;
63
+ choice: "assessable" | "not_assessable";
64
64
  probabilities: {
65
65
  [x: string]: number;
66
66
  [x: number]: number;
@@ -68,8 +68,8 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
68
68
  };
69
69
  fitAssessability: {
70
70
  type: "choice";
71
- choice: "assessable" | "not_assessable";
72
71
  confidence: number;
72
+ choice: "assessable" | "not_assessable";
73
73
  probabilities: {
74
74
  [x: string]: number;
75
75
  [x: number]: number;
@@ -81,8 +81,8 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
81
81
  };
82
82
  failureReason: {
83
83
  type: "choice";
84
- choice: "none_or_unclear" | "missing_requested_work" | "wrong_deliverable" | "missed_constraint" | "insufficient_answer_depth" | "unnecessary_deferral";
85
84
  confidence: number;
85
+ choice: "none_or_unclear" | "missing_requested_work" | "wrong_deliverable" | "missed_constraint" | "insufficient_answer_depth" | "unnecessary_deferral";
86
86
  probabilities: {
87
87
  [x: string]: number;
88
88
  [x: number]: number;
@@ -112,8 +112,8 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
112
112
  };
113
113
  consistency: {
114
114
  type: "choice";
115
- choice: "not_assessable" | "consistent" | "contradicted";
116
115
  confidence: number;
116
+ choice: "not_assessable" | "consistent" | "contradicted";
117
117
  probabilities: {
118
118
  [x: string]: number;
119
119
  [x: number]: number;
@@ -123,8 +123,8 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
123
123
  feedback: {
124
124
  feedbackType: {
125
125
  type: "choice";
126
- choice: "mixed" | "unrelated" | "unclear" | "acceptance" | "correction" | "continuation";
127
126
  confidence: number;
127
+ choice: "mixed" | "unrelated" | "unclear" | "acceptance" | "correction" | "continuation";
128
128
  probabilities: {
129
129
  [x: string]: number;
130
130
  [x: number]: number;
@@ -132,8 +132,8 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
132
132
  };
133
133
  target: {
134
134
  type: "choice";
135
- choice: "delivery" | "mixed" | "unclear" | "current_answer" | "earlier_behavior" | "proactive_action" | "external" | "new_work";
136
135
  confidence: number;
136
+ choice: "delivery" | "mixed" | "unclear" | "current_answer" | "earlier_behavior" | "proactive_action" | "external" | "new_work";
137
137
  probabilities: {
138
138
  [x: string]: number;
139
139
  [x: number]: number;
@@ -154,8 +154,8 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
154
154
  } & Partial<{
155
155
  sentiment: {
156
156
  type: "choice";
157
- choice: "satisfied" | "dissatisfied" | "mixed" | "neutral" | "unrelated" | "unclear";
158
157
  confidence: number;
158
+ choice: "satisfied" | "dissatisfied" | "mixed" | "neutral" | "unrelated" | "unclear";
159
159
  probabilities: {
160
160
  [x: string]: number;
161
161
  [x: number]: number;
@@ -1,3 +1,8 @@
1
+ /** Infer legacy transport sender metadata only to unwrap visible conversation text. */
2
+ export declare function conversationUserText(raw: string, sender?: unknown): {
3
+ text: string;
4
+ contextLimited: boolean;
5
+ } | undefined;
1
6
  /** Parse only recognized transport envelopes. Ambiguous wrappers are excluded,
2
7
  * never flattened into a human's request or used to approve embedded speakers. */
3
8
  export declare function responseUserText(input: string, senderId: string): {
@@ -1,3 +1,25 @@
1
+ /** Infer legacy transport sender metadata only to unwrap visible conversation text. */
2
+ export function conversationUserText(raw, sender) {
3
+ let senderId = typeof sender === "string" ? sender : undefined;
4
+ if (senderId === undefined) {
5
+ const header = /^Conversation info: ⟦openclaw:ctx⟧\r?\n```json\r?\n([^]*?)\r?\n```\r?\n/.exec(raw.trim());
6
+ if (header) {
7
+ let metadata;
8
+ try {
9
+ metadata = JSON.parse(header[1]);
10
+ }
11
+ catch {
12
+ return;
13
+ }
14
+ if (metadata && typeof metadata === "object" && "sender" in metadata && metadata.sender &&
15
+ typeof metadata.sender === "object" && "id" in metadata.sender && typeof metadata.sender.id === "string") {
16
+ senderId = metadata.sender.id;
17
+ }
18
+ }
19
+ senderId ??= /^From: [^\r\n]+ \(([^()\r\n]+)\)\r?\n/.exec(raw.trim())?.[1];
20
+ }
21
+ return responseUserText(raw, senderId ?? "");
22
+ }
1
23
  /** Parse only recognized transport envelopes. Ambiguous wrappers are excluded,
2
24
  * never flattened into a human's request or used to approve embedded speakers. */
3
25
  export function responseUserText(input, senderId) {
@@ -15,7 +37,7 @@ export function responseUserText(input, senderId) {
15
37
  if (!meta || meta.sender?.id !== senderId || typeof meta.sender.name !== "string")
16
38
  return undefined;
17
39
  const rest = text.slice(header[0].length);
18
- const markers = [...rest.matchAll(/^System: \[[^\]\r\n]+\] Slack message in [^\r\n]+ from ([^\r\n]+)\r?\n\r?\n/gm)];
40
+ const markers = [...rest.matchAll(/^System: \[[^\]\r\n]+\] Slack (?:message in [^\r\n]+|DM) from ([^\r\n]+)\r?\n\r?\n/gm)];
19
41
  if (markers.length !== 1 || markers[0][1] !== meta.sender.name)
20
42
  return undefined;
21
43
  const marker = markers[0];
@@ -1,6 +1,6 @@
1
1
  type RetrievalOperation = "vector" | "lexical" | "memoryWhisperer";
2
2
  type RetrievalOutcome = "ok" | "empty" | "failed" | "cancelled" | "timed_out" | "skipped";
3
- declare const fields: readonly ["elapsedMs", "retrievalMs", "judgeMs", "candidates", "eligible", "results", "contextChars"];
3
+ declare const fields: readonly ["elapsedMs", "retrievalMs", "judgeMs", "gateMs", "generationMs", "candidates", "eligible", "results", "contextChars"];
4
4
  type Field = typeof fields[number];
5
5
  export type RetrievalObservation = {
6
6
  outcome: RetrievalOutcome;
@@ -16,11 +16,11 @@ export declare class RetrievalTelemetry {
16
16
  [k: string]: {
17
17
  calls: number;
18
18
  outcomes: {
19
+ cancelled?: number | undefined;
19
20
  ok?: number | undefined;
20
21
  skipped?: number | undefined;
21
22
  failed?: number | undefined;
22
23
  empty?: number | undefined;
23
- cancelled?: number | undefined;
24
24
  timed_out?: number | undefined;
25
25
  };
26
26
  measurements: {
@@ -1,4 +1,4 @@
1
- const fields = ["elapsedMs", "retrievalMs", "judgeMs", "candidates", "eligible", "results", "contextChars"];
1
+ const fields = ["elapsedMs", "retrievalMs", "judgeMs", "gateMs", "generationMs", "candidates", "eligible", "results", "contextChars"];
2
2
  const recentLimit = 256;
3
3
  const boundedAdd = (left, right) => Math.min(Number.MAX_SAFE_INTEGER, left + right);
4
4
  /** Only fixed operation/outcome names and nonnegative numbers enter this store. */
@@ -2,7 +2,7 @@ import { Type } from "typebox";
2
2
  import { Value } from "typebox/value";
3
3
  import { jsonResult } from "openclaw/plugin-sdk/agent-runtime";
4
4
  import { getContext } from "./tool-context.js";
5
- import { resolveTypeSafeApiKey } from "./typesafe.js";
5
+ import { resolveTypeSafeApiKey } from "./typesafe-client.js";
6
6
  import { abortable } from "./abortable.js";
7
7
  const claimParameters = Type.Object({
8
8
  claim: Type.String({ pattern: "\\S", maxLength: 2000 }),
@@ -13,5 +13,5 @@ type SkillWhispererRuntime = {
13
13
  }, path: string): string | undefined;
14
14
  };
15
15
  export declare function buildSkillWhispererQuery(prompt: string, messages: readonly unknown[], historyMessages: number): string;
16
- export declare function registerSkillWhisperer(api: OpenClawPluginApi, runtime: SkillWhispererRuntime, config: UnblockMemoryConfig["skillWhisperer"], typesafe: UnblockMemoryConfig["typesafe"], diagnostics?: WhispererDiagnostics): void;
16
+ export declare function registerSkillWhisperer(api: OpenClawPluginApi, runtime: SkillWhispererRuntime, config: UnblockMemoryConfig["skillWhisperer"], typesafe: UnblockMemoryConfig["typesafe"], diagnostics?: WhispererDiagnostics): Parameters<typeof api.on<"before_prompt_build">>[1] | undefined;
17
17
  export {};
@@ -1,5 +1,6 @@
1
1
  import { basename } from "node:path";
2
- import { resolveTypeSafeApiKey, selectTypeSafeSkill } from "./typesafe.js";
2
+ import { selectTypeSafeSkill } from "./typesafe.js";
3
+ import { resolveTypeSafeApiKey, TypeSafeRequestError } from "./typesafe-client.js";
3
4
  import { messageText } from "./whisperer-context.js";
4
5
  const CANDIDATE_LIMIT = 10;
5
6
  const MAX_QUERY_CHARS = 12_000;
@@ -55,78 +56,89 @@ export function registerSkillWhisperer(api, runtime, config, typesafe, diagnosti
55
56
  return state;
56
57
  };
57
58
  const active = (agentId) => ({ cfg: api.config, agentId });
58
- api.on("before_prompt_build", async (event, context) => {
59
+ const beforePrompt = async (event, context) => {
60
+ const { agentId, runId } = context;
59
61
  const scope = sessionScope(context);
60
- if (context.trigger !== "user" || !scope || !context.runId || !context.agentId)
62
+ if (context.trigger !== "user" || !scope || !runId || !agentId)
61
63
  return;
62
64
  const state = stateFor(scope);
63
- if (state.lastRunId === context.runId)
64
- return;
65
- state.lastRunId = context.runId;
65
+ if (state.lastRunId === runId)
66
+ return state.result;
67
+ state.lastRunId = runId;
66
68
  state.turn += 1;
67
- try {
68
- const runtimeParams = active(context.agentId);
69
- const apiKey = await resolveTypeSafeApiKey(typesafe);
70
- if (!apiKey)
71
- diagnostics?.record(context.agentId, "skill", typesafe.enabled ? "missing_key" : "typesafe_disabled");
72
- const candidates = await runtime.searchSkills(runtimeParams, buildSkillWhispererQuery(event.prompt, event.messages, config.historyMessages), apiKey ? -1 : config.minScore, CANDIDATE_LIMIT);
73
- const resolvedCandidates = candidates.flatMap((candidate) => {
74
- const canonicalPath = runtime.resolveSkillPath(runtimeParams, candidate.path);
75
- return canonicalPath ? [{ candidate, canonicalPath }] : [];
76
- });
77
- let resolved = resolvedCandidates[0];
78
- if (!resolved) {
79
- diagnostics?.record(context.agentId, "skill", "no_candidates");
80
- return;
81
- }
82
- if (apiKey) {
83
- const shortlist = resolvedCandidates.slice(0, TYPESAFE_CANDIDATE_LIMIT);
84
- const selectedIndex = await selectTypeSafeSkill({
85
- apiKey, timeoutMs: typesafe.timeoutMs,
86
- ...typeSafeConversation(event.prompt, event.messages, config.historyMessages),
87
- candidates: shortlist.map(({ candidate }) => candidate),
69
+ // Reuse even an in-flight result when the host rebuilds this run's prompt.
70
+ const run = async () => {
71
+ try {
72
+ const runtimeParams = active(agentId);
73
+ const apiKey = await resolveTypeSafeApiKey(typesafe);
74
+ if (!apiKey)
75
+ diagnostics?.record(agentId, "skill", typesafe.enabled ? "missing_key" : "typesafe_disabled");
76
+ const candidates = await runtime.searchSkills(runtimeParams, buildSkillWhispererQuery(event.prompt, event.messages, config.historyMessages), apiKey ? -1 : config.minScore, CANDIDATE_LIMIT);
77
+ const resolvedCandidates = candidates.flatMap((candidate) => {
78
+ const canonicalPath = runtime.resolveSkillPath(runtimeParams, candidate.path);
79
+ return canonicalPath ? [{ candidate, canonicalPath }] : [];
88
80
  });
89
- if (selectedIndex === undefined) {
90
- diagnostics?.record(context.agentId, "skill", "rejected");
81
+ let resolved = resolvedCandidates[0];
82
+ if (!resolved) {
83
+ diagnostics?.record(agentId, "skill", "no_candidates");
91
84
  return;
92
85
  }
93
- resolved = shortlist[selectedIndex];
94
- }
95
- else if (resolved && resolved.candidate.score < config.minScore) {
96
- diagnostics?.record(context.agentId, "skill", "rejected");
97
- return;
98
- }
99
- if (!resolved)
100
- return;
101
- // A selection completing after session teardown must not resurrect its hint.
102
- if (sessions.get(scope) !== state || state.lastRunId !== context.runId) {
103
- diagnostics?.record(context.agentId, "skill", "cancelled");
104
- return;
86
+ if (apiKey) {
87
+ const shortlist = resolvedCandidates.slice(0, TYPESAFE_CANDIDATE_LIMIT);
88
+ const selectedIndex = await selectTypeSafeSkill({
89
+ apiKey, timeoutMs: typesafe.timeoutMs,
90
+ ...typeSafeConversation(event.prompt, event.messages, config.historyMessages),
91
+ candidates: shortlist.map(({ candidate }) => candidate),
92
+ onCandidateFailure: (candidateIndex, error) => {
93
+ diagnostics?.record(agentId, "skill", "judge_candidate_failed");
94
+ api.logger.warn("unblock-memory skill_whisperer " + JSON.stringify({ event: "candidate_failed",
95
+ agentId, runId, candidateIndex, errorCode: error instanceof TypeSafeRequestError ? error.code : "unexpected",
96
+ httpStatus: error instanceof TypeSafeRequestError ? error.status : undefined }));
97
+ },
98
+ });
99
+ if (selectedIndex === undefined) {
100
+ diagnostics?.record(agentId, "skill", "rejected");
101
+ return;
102
+ }
103
+ resolved = shortlist[selectedIndex];
104
+ }
105
+ else if (resolved && resolved.candidate.score < config.minScore) {
106
+ diagnostics?.record(agentId, "skill", "rejected");
107
+ return;
108
+ }
109
+ if (!resolved)
110
+ return;
111
+ // A selection completing after session teardown must not resurrect its hint.
112
+ if (sessions.get(scope) !== state || state.lastRunId !== runId) {
113
+ diagnostics?.record(agentId, "skill", "cancelled");
114
+ return;
115
+ }
116
+ const { candidate: selected, canonicalPath } = resolved;
117
+ if (apiKey && runtime.resolveSkillPath(runtimeParams, selected.path) !== canonicalPath)
118
+ return;
119
+ const previous = state.skills.get(canonicalPath);
120
+ const lastSeen = Math.max(previous?.suggested ?? -Infinity, previous?.opened ?? -Infinity);
121
+ if (state.turn - lastSeen <= config.cooldownTurns) {
122
+ diagnostics?.record(agentId, "skill", "cooldown");
123
+ return;
124
+ }
125
+ const history = state.skills.get(canonicalPath) ?? {};
126
+ history.suggested = state.turn;
127
+ state.skills.set(canonicalPath, history);
128
+ diagnostics?.record(agentId, "skill", "emitted");
129
+ return {
130
+ appendContext: `<skill>This skill may be relevant: ${JSON.stringify(selected.path).replace(/</gu, "\\u003c").replace(/>/gu, "\\u003e")}</skill>`,
131
+ };
105
132
  }
106
- const { candidate: selected, canonicalPath } = resolved;
107
- if (apiKey && runtime.resolveSkillPath(runtimeParams, selected.path) !== canonicalPath)
108
- return;
109
- const previous = state.skills.get(canonicalPath);
110
- const lastSeen = Math.max(previous?.suggested ?? -Infinity, previous?.opened ?? -Infinity);
111
- if (state.turn - lastSeen <= config.cooldownTurns) {
112
- diagnostics?.record(context.agentId, "skill", "cooldown");
133
+ catch (error) {
134
+ diagnostics?.record(agentId, "skill", error instanceof TypeSafeRequestError && error.code === "timeout" ? "timed_out" : "failed");
135
+ api.logger.warn("unblock-memory skill whisperer failed; no hint emitted");
113
136
  return;
114
137
  }
115
- const history = state.skills.get(canonicalPath) ?? {};
116
- history.suggested = state.turn;
117
- state.skills.set(canonicalPath, history);
118
- diagnostics?.record(context.agentId, "skill", "emitted");
119
- return {
120
- prependContext: `A potentially relevant skill is available: ${JSON.stringify(selected.name)} ` +
121
- `at ${JSON.stringify(selected.path)}. Check it before proceeding if applicable.`,
122
- };
123
- }
124
- catch (error) {
125
- diagnostics?.record(context.agentId, "skill", error instanceof Error && error.message === "TypeSafe selection timed out" ? "timed_out" : "failed");
126
- api.logger.warn("unblock-memory skill whisperer failed; no hint emitted");
127
- return;
128
- }
129
- });
138
+ };
139
+ state.result = run();
140
+ return state.result;
141
+ };
130
142
  api.on("after_tool_call", (event, context) => {
131
143
  if (event.toolName !== "read" || event.error ||
132
144
  (isRecord(event.result) && event.result.isError === true) || !context.agentId)
@@ -155,4 +167,5 @@ export function registerSkillWhisperer(api, runtime, config, typesafe, diagnosti
155
167
  if (context.sessionKey)
156
168
  sessions.delete(context.sessionKey);
157
169
  });
170
+ return beforePrompt;
158
171
  }
@@ -8,6 +8,14 @@ type Candidate = {
8
8
  explain: {
9
9
  methods: string[];
10
10
  };
11
+ vector?: {
12
+ score: number;
13
+ rank: number;
14
+ };
15
+ bm25?: {
16
+ score: number;
17
+ rank: number;
18
+ };
11
19
  };
12
- export declare function trainingCandidates(qmd: QMDStore, query: string, collection: string, intent: string): Promise<Candidate[]>;
20
+ export declare function trainingCandidates(qmd: QMDStore, query: string, collection: string | string[], intent: string, signal?: AbortSignal): Promise<Candidate[]>;
13
21
  export {};
@@ -26,14 +26,15 @@ function lexicalChunk(chunks, body, highlighted, marker, intent) {
26
26
  return { chunk, matches, intentMatches: intentTerms.filter(term => lower.includes(term)).length };
27
27
  }).sort((a, b) => b.matches - a.matches || b.intentMatches - a.intentMatches || a.chunk.pos - b.chunk.pos)[0]?.chunk;
28
28
  }
29
- export async function trainingCandidates(qmd, query, collection, intent) {
29
+ export async function trainingCandidates(qmd, query, collection, intent, signal) {
30
+ signal?.throwIfAborted();
30
31
  if (!query.trim() || query.length > 12_000)
31
32
  throw new Error("Invalid training query");
32
33
  // QMD exposes its store but not these chunk helpers at the package root.
33
34
  // Resolve relative to its installed SDK, never a global QMD or modified copy.
34
35
  const chunksApi = await import(new URL("./store.js", import.meta.resolve("@unblocklabs/qmd")).href);
35
36
  const candidates = new Map();
36
- const add = (hit, method, rank) => {
37
+ const add = (hit, method, rank, rawScore) => {
37
38
  if (!hit.bestChunk.trim() || hit.bestChunk.length > 12_000)
38
39
  return;
39
40
  const key = JSON.stringify([hit.file, hit.bestChunk.trim()]), existing = candidates.get(key);
@@ -41,16 +42,19 @@ export async function trainingCandidates(qmd, query, collection, intent) {
41
42
  if (!existing.explain.methods.includes(method))
42
43
  existing.explain.methods.push(method);
43
44
  existing.score = Math.max(existing.score, 1 / (rank + 1));
45
+ existing[method] ??= { score: rawScore, rank: rank + 1 };
44
46
  }
45
47
  else
46
- candidates.set(key, { ...hit, score: 1 / (rank + 1), explain: { methods: [method] } });
48
+ candidates.set(key, { ...hit, score: 1 / (rank + 1), explain: { methods: [method] },
49
+ [method]: { score: rawScore, rank: rank + 1 } });
47
50
  };
48
51
  const vectors = await qmd.searchVector(query, { limit: 10, collection });
52
+ signal?.throwIfAborted();
49
53
  for (const [rank, hit] of vectors.entries()) {
50
54
  const pos = hit.chunkPos, len = hit.chunkLen, body = hit.body ?? "";
51
55
  if (pos === undefined || len === undefined || pos < 0 || len <= 0 || pos + len > body.length)
52
56
  continue;
53
- add({ file: hit.filepath, body, bestChunk: body.slice(pos, pos + len), bestChunkPos: pos }, "vector", rank);
57
+ add({ file: hit.filepath, body, bestChunk: body.slice(pos, pos + len), bestChunkPos: pos }, "vector", rank, hit.score);
54
58
  }
55
59
  const expression = queryTerms(query).map(term => `"${chunksApi.normalizeCjkForFTS(term).trim()}"`).join(" OR ");
56
60
  if (expression) {
@@ -58,9 +62,10 @@ export async function trainingCandidates(qmd, query, collection, intent) {
58
62
  const rows = qmd.internal.db.prepare(`SELECT d.collection,d.path,d.hash,c.doc,
59
63
  bm25(documents_fts,1.5,4.0,1.0) AS rank, highlight(documents_fts,2,?,?) AS highlighted
60
64
  FROM documents_fts JOIN documents d ON d.id=documents_fts.rowid JOIN content c ON c.hash=d.hash
61
- WHERE documents_fts MATCH ? AND d.active=1 AND d.collection=?
62
- ORDER BY rank,d.collection,d.path LIMIT 10`).all(marker, marker, expression, collection);
65
+ WHERE documents_fts MATCH ? AND d.active=1 AND d.collection IN (SELECT value FROM json_each(?))
66
+ ORDER BY rank,d.collection,d.path LIMIT 10`).all(marker, marker, expression, JSON.stringify(typeof collection === "string" ? [collection] : collection));
63
67
  for (const [rank, row] of rows.entries()) {
68
+ signal?.throwIfAborted();
64
69
  const file = `qmd://${row.collection}/${row.path}`;
65
70
  const stored = chunksApi.getStoredChunkSpans(qmd.internal.db, row.hash)
66
71
  .filter(span => span.pos >= 0 && span.chunk_len > 0 && span.pos + span.chunk_len <= row.doc.length)
@@ -68,7 +73,7 @@ export async function trainingCandidates(qmd, query, collection, intent) {
68
73
  const chunks = stored.length ? stored : await chunksApi.chunkDocumentAsync(row.doc, undefined, undefined, undefined, file);
69
74
  const selected = lexicalChunk(chunks, row.doc, row.highlighted, marker, intent);
70
75
  if (selected)
71
- add({ file, body: row.doc, bestChunk: selected.text, bestChunkPos: selected.pos }, "bm25", rank);
76
+ add({ file, body: row.doc, bestChunk: selected.text, bestChunkPos: selected.pos }, "bm25", rank, row.rank);
72
77
  }
73
78
  }
74
79
  return [...candidates.values()].sort((a, b) => b.score - a.score);
@@ -1,6 +1,6 @@
1
1
  import { Type } from "typebox";
2
2
  import { Value } from "typebox/value";
3
- import { postTypeSafe, TYPESAFE_MODEL } from "./typesafe-transport.js";
3
+ import { requestTypeSafe, TYPESAFE_MODEL, TypeSafeRequestError } from "./typesafe-client.js";
4
4
  export const TRAINING_GATE_VERSION = "historical-recall-v1";
5
5
  export const TRAINING_GATE_MODEL = TYPESAFE_MODEL;
6
6
  export const TRAINING_GATE_THRESHOLD = 0.7;
@@ -25,9 +25,9 @@ const resultSchema = Type.Object({
25
25
  usage: Type.Object({ input_tokens: Type.Integer({ minimum: 0 }), output_tokens: Type.Integer({ minimum: 0 }) }),
26
26
  });
27
27
  export async function judgeTrainingInput(input, apiKey, signal) {
28
- const result = await postTypeSafe({ apiKey, signal }, input, TRAINING_GATE_QUESTIONS);
28
+ const result = await requestTypeSafe({ apiKey, signal }, input, TRAINING_GATE_QUESTIONS);
29
29
  if (!Value.Check(resultSchema, result) || !Number.isFinite(result.answers.recall_needed.noul)) {
30
- throw new Error("Invalid training gate response");
30
+ throw new TypeSafeRequestError("Invalid training gate response", "invalid_response");
31
31
  }
32
32
  return { probability: result.answers.recall_needed.noul, model: result.model, usage: result.usage };
33
33
  }
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { DatabaseSync } from "node:sqlite";
3
3
  import { messageText } from "./whisperer-context.js";
4
- import { responseUserText } from "./response-text.js";
4
+ import { conversationUserText } from "./response-text.js";
5
5
  // Identical serialized inputs keep their checkpoints when eligibility broadens.
6
6
  export const TRAINING_PREPARATION = "visible-history-v1";
7
7
  // A deliberately conservative byte budget, NOT a tokenizer or a 32k-token target.
@@ -10,27 +10,6 @@ export const trainingHash = (value) => createHash("sha256").update(JSON.stringif
10
10
  function record(value) {
11
11
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
12
12
  }
13
- /** Legacy sender IDs are useful for envelope cleanup, not an admission requirement. */
14
- function userText(raw, sender) {
15
- let senderId = typeof sender === "string" ? sender : undefined;
16
- if (senderId === undefined) {
17
- const header = /^Conversation info: ⟦openclaw:ctx⟧\r?\n```json\r?\n([^]*?)\r?\n```\r?\n/.exec(raw.trim());
18
- if (header) {
19
- let metadata;
20
- try {
21
- metadata = JSON.parse(header[1]);
22
- }
23
- catch {
24
- return;
25
- }
26
- const id = record(record(metadata)?.sender)?.id;
27
- if (typeof id === "string")
28
- senderId = id;
29
- }
30
- senderId ??= /^From: [^\r\n]+ \(([^()\r\n]+)\)\r?\n/.exec(raw.trim())?.[1];
31
- }
32
- return responseUserText(raw, senderId ?? "");
33
- }
34
13
  /** The following answer establishes eligibility, but is never part of that example's input. */
35
14
  export function trainingExamples(rows) {
36
15
  const examples = [];
@@ -88,7 +67,7 @@ export function trainingExamples(rows) {
88
67
  continue;
89
68
  }
90
69
  const raw = typeof meta?.upstreamUserText === "string" ? meta.upstreamUserText : messageText(message)?.text;
91
- const visible = raw ? userText(raw, meta?.senderId ?? message.senderId) : undefined;
70
+ const visible = raw ? conversationUserText(raw, meta?.senderId ?? message.senderId) : undefined;
92
71
  if (!visible || /^(?:\[OpenClaw heartbeat poll\]|\[Queued messages while agent was busy\]|\[Subagent Context\]|<relevant-memories>)/.test(visible.text)) {
93
72
  coverage.filtered++;
94
73
  boundary();
@@ -1,8 +1,8 @@
1
1
  import { Type } from 'typebox';
2
2
  import { Value } from 'typebox/value';
3
- import { postTypeSafe } from './typesafe-transport.js';
3
+ import { requestTypeSafe, TYPESAFE_MODEL } from './typesafe-client.js';
4
4
  export const CONTEXT_JUDGE_VERSION = 'conversation-context-usefulness-v1';
5
- const CONTEXT_JUDGE_MODEL = 'jev-1.13.0';
5
+ const CONTEXT_JUDGE_MODEL = TYPESAFE_MODEL;
6
6
  const CONTEXT_QUESTIONS = {
7
7
  usefulness: {
8
8
  type: 'score',
@@ -53,5 +53,5 @@ export function parseContextJudgment(payload) {
53
53
  return { score: answer.score / 3, answer, model: payload.model, usage: payload.usage ?? null };
54
54
  }
55
55
  export async function judgeTrainingPassage(request, apiKey) {
56
- return parseContextJudgment(await postTypeSafe({ apiKey, signal: AbortSignal.timeout(30_000) }, request.state, request.questions));
56
+ return parseContextJudgment(await requestTypeSafe({ apiKey, timeoutMs: 30_000 }, request.state, request.questions));
57
57
  }
@@ -1,11 +1,10 @@
1
1
  import { collectTraining } from "./training.js";
2
2
  import { trainingHash } from "./training-input.js";
3
3
  import { TRAINING_GATE_THRESHOLD } from "./training-gate.js";
4
- import { resolveTypeSafeApiKey } from "./typesafe.js";
4
+ import { resolveTypeSafeApiKey, TypeSafeRequestError } from "./typesafe-client.js";
5
5
  import { trainingTeacher, trainingTeacherMessage, TRAINING_TEACHER_MODEL, TRAINING_TEACHER_PROMPT, TRAINING_TEACHER_VERSION } from "./training-models.js";
6
6
  import { historicalTrainingSearch, TRAINING_RETRIEVAL_VERSION, TRAINING_SEARCH_OPTIONS } from "./training-retrieval.js";
7
7
  import { contextJudgeRequest, judgeTrainingPassage, CONTEXT_JUDGE_VERSION } from "./training-judge.js";
8
- import { TypeSafeHttpError } from "./typesafe-transport.js";
9
8
  const SELECTION_VERSION = "conversation-utility-top5-sum-v2";
10
9
  export const TRAINING_EVALUATION_CONCURRENCY = 4;
11
10
  function bounds(options) {
@@ -41,9 +40,11 @@ async function checkpoint(store, step, result, operation) {
41
40
  const hostCode = error && typeof error === "object" && "code" in error && typeof error.code === "string" &&
42
41
  /^LLM_[A-Z_]+$/u.test(error.code) ? error.code : undefined;
43
42
  const status = hostCode === "LLM_COMPLETION_NOT_AUTHORIZED" ||
44
- (error instanceof TypeSafeHttpError && error.status >= 400 && error.status < 500) ? "failed" : "ambiguous";
43
+ (error instanceof TypeSafeRequestError && error.code === "http_error" &&
44
+ error.status !== undefined && error.status >= 400 && error.status < 500) ? "failed" : "ambiguous";
45
45
  store.finishStep(step.stage, step.id, attempt, { status,
46
- error: error instanceof TypeSafeHttpError ? `http_${error.status}` : hostCode ?? "request_or_response_uncertain" });
46
+ error: error instanceof TypeSafeRequestError && error.code === "http_error" ?
47
+ `http_${error.status}` : hostCode ?? "request_or_response_uncertain" });
47
48
  result[status]++;
48
49
  return;
49
50
  }
@@ -1,5 +1,4 @@
1
- import { TypeSafeHttpError } from "./typesafe-transport.js";
2
- import { resolveTypeSafeApiKey } from "./typesafe.js";
1
+ import { TypeSafeRequestError, resolveTypeSafeApiKey } from "./typesafe-client.js";
3
2
  import { TrainingTranscriptReader } from "./training-input.js";
4
3
  import { judgeTrainingInput, TRAINING_GATE_QUESTIONS } from "./training-gate.js";
5
4
  /** A full active-branch rescan is cheap/local; only changed exact inputs need inference. */
@@ -78,9 +77,11 @@ export async function runTraining(source, store, config, options) {
78
77
  judgment = await judgeTrainingInput(input, key, AbortSignal.timeout(30_000));
79
78
  }
80
79
  catch (error) {
81
- const definite = error instanceof TypeSafeHttpError && error.status >= 400 && error.status < 500;
80
+ const definite = error instanceof TypeSafeRequestError && error.code === "http_error" &&
81
+ error.status !== undefined && error.status >= 400 && error.status < 500;
82
82
  const status = definite ? "failed" : "ambiguous";
83
- store.finish(job.id, attempt, { status, error: error instanceof TypeSafeHttpError ? `http_${error.status}` : "request_or_response_uncertain" });
83
+ store.finish(job.id, attempt, { status, error: error instanceof TypeSafeRequestError && error.code === "http_error" ?
84
+ `http_${error.status}` : "request_or_response_uncertain" });
84
85
  result[status]++;
85
86
  // Stop on the first failure rather than spending the rest of the budget during an outage.
86
87
  return;
@@ -0,0 +1,15 @@
1
+ import type { UnblockMemoryConfig } from "./config.js";
2
+ export declare const TYPESAFE_MODEL = "jev-1.13.0";
3
+ export declare class TypeSafeRequestError extends Error {
4
+ readonly code: "timeout" | "cancelled" | "http_error" | "invalid_response" | "network_error";
5
+ readonly status?: number | undefined;
6
+ constructor(message: string, code: "timeout" | "cancelled" | "http_error" | "invalid_response" | "network_error", status?: number | undefined);
7
+ }
8
+ /** Explicit credentials take precedence; a missing explicit file never selects another key. */
9
+ export declare function resolveTypeSafeApiKey(config: UnblockMemoryConfig["typesafe"]): Promise<string | undefined>;
10
+ /** One TypeSafe request. The caller owns the response schema and failure policy. */
11
+ export declare function requestTypeSafe(params: {
12
+ apiKey: string;
13
+ timeoutMs?: number;
14
+ signal?: AbortSignal;
15
+ }, state: unknown, questions: unknown): Promise<unknown>;