@unblocklabs/unblock-memory 0.3.30 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/src/config.d.ts +3 -4
- package/dist/src/config.js +31 -11
- package/dist/src/diagnostics.d.ts +1 -3
- package/dist/src/manager.d.ts +4 -8
- package/dist/src/manager.js +17 -45
- package/dist/src/memory-passage.d.ts +26 -0
- package/dist/src/memory-passage.js +48 -0
- package/dist/src/memory-whisperer.d.ts +2 -1
- package/dist/src/memory-whisperer.js +37 -42
- package/dist/src/query-contract.d.ts +31 -0
- package/dist/src/query-contract.js +58 -0
- package/dist/src/query-generator.d.ts +15 -0
- package/dist/src/query-generator.js +136 -0
- package/dist/src/query-tokenizer/contract.json +10 -0
- package/dist/src/response-stages.d.ts +3 -3
- package/dist/src/training-candidates.d.ts +2 -1
- package/dist/src/training-candidates.js +12 -9
- package/dist/src/training-input.d.ts +13 -10
- package/dist/src/training-input.js +22 -18
- package/dist/src/training-judge.d.ts +16 -46
- package/dist/src/training-judge.js +6 -53
- package/dist/src/training-models.d.ts +15 -7
- package/dist/src/training-models.js +36 -38
- package/dist/src/training-queries.d.ts +24 -7
- package/dist/src/training-queries.js +237 -101
- package/dist/src/training-retrieval.d.ts +9 -2
- package/dist/src/training-retrieval.js +36 -12
- package/dist/src/training-runtime.js +8 -7
- package/dist/src/training-store.d.ts +46 -16
- package/dist/src/training-store.js +85 -29
- package/dist/src/training.d.ts +10 -0
- package/dist/src/training.js +7 -5
- package/dist/src/typesafe.d.ts +58 -8
- package/dist/src/typesafe.js +47 -29
- package/dist/src/whisperer-context.d.ts +0 -8
- package/dist/src/whisperer-context.js +0 -23
- package/docs/configuration.md +5 -3
- package/docs/memory-training.md +120 -58
- package/docs/retrieval.md +59 -43
- package/openclaw.plugin.json +15 -6
- package/package.json +4 -1
- package/src/mlx-query-worker.py +5 -8
- package/src/query-tokenizer/LICENSE +71 -0
- package/src/query-tokenizer/README.md +21 -0
- package/src/query-tokenizer/chat_template.jinja +115 -0
- package/src/query-tokenizer/contract.json +10 -0
- package/src/query-tokenizer/tokenizer.json +323830 -0
- package/src/query-tokenizer/tokenizer_config.json +19 -0
- package/src/query_contract.py +60 -0
- package/dist/src/mlx-query.d.ts +0 -22
- package/dist/src/mlx-query.js +0 -155
|
@@ -2,10 +2,12 @@ import { collectTraining } from "./training.js";
|
|
|
2
2
|
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
|
-
import { trainingTeacher, trainingTeacherMessage,
|
|
6
|
-
import { historicalTrainingSearch, TRAINING_RETRIEVAL_VERSION, TRAINING_SEARCH_OPTIONS } from "./training-retrieval.js";
|
|
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";
|
|
7
7
|
import { contextJudgeRequest, judgeTrainingPassage, CONTEXT_JUDGE_VERSION } from "./training-judge.js";
|
|
8
|
-
const
|
|
8
|
+
const LANES = ["lex", "vec"];
|
|
9
|
+
const SELECTION_VERSION = "independent-lanes-top3-mean-v2";
|
|
10
|
+
const TARGET_POLICY = "best-per-lane-no-minimum-v1";
|
|
9
11
|
export const TRAINING_EVALUATION_CONCURRENCY = 4;
|
|
10
12
|
function bounds(options) {
|
|
11
13
|
for (const value of [options.maxExamples, options.maxCalls, options.maxInputBytes, options.concurrency]) {
|
|
@@ -13,13 +15,14 @@ function bounds(options) {
|
|
|
13
15
|
throw new Error("Training bounds must be positive integers");
|
|
14
16
|
}
|
|
15
17
|
}
|
|
16
|
-
function teacherRequest(example) {
|
|
18
|
+
function teacherRequest(example, lane, feedback) {
|
|
17
19
|
return { version: TRAINING_TEACHER_VERSION, model: TRAINING_TEACHER_MODEL, inputHash: example.inputHash,
|
|
18
|
-
input: JSON.parse(example.inputJson)
|
|
20
|
+
input: JSON.parse(example.inputJson), lane, round: feedback ? 2 : 1,
|
|
21
|
+
...(feedback ? { feedback: feedback.map(({ query, score }) => ({ query, score })) } : {}) };
|
|
19
22
|
}
|
|
20
23
|
const summary = () => ({ examples: 0, calls: 0, completed: 0, cached: 0, failed: 0, ambiguous: 0,
|
|
21
|
-
blocked: 0, inputBytes: 0, budgetLimited: false });
|
|
22
|
-
/** One fenced checkpoint per
|
|
24
|
+
blocked: 0, flagged: 0, inputBytes: 0, budgetLimited: false });
|
|
25
|
+
/** One fenced checkpoint per operation. Storage failures are fatal; provider failures are local. */
|
|
23
26
|
async function checkpoint(store, step, result, operation) {
|
|
24
27
|
if (step.result !== undefined) {
|
|
25
28
|
result.cached++;
|
|
@@ -37,14 +40,19 @@ async function checkpoint(store, step, result, operation) {
|
|
|
37
40
|
value = await operation();
|
|
38
41
|
}
|
|
39
42
|
catch (error) {
|
|
43
|
+
// Historical retrieval is local code. Only an explicitly unavailable corpus
|
|
44
|
+
// is a reviewable data case; do not swallow SQLite, I/O or snapshot-integrity errors.
|
|
45
|
+
if (step.stage === "retrieve" && !(error instanceof HistoricalCorpusUnavailableError))
|
|
46
|
+
throw error;
|
|
40
47
|
const hostCode = error && typeof error === "object" && "code" in error && typeof error.code === "string" &&
|
|
41
48
|
/^LLM_[A-Z_]+$/u.test(error.code) ? error.code : undefined;
|
|
42
|
-
const status = hostCode === "LLM_COMPLETION_NOT_AUTHORIZED" ||
|
|
49
|
+
const status = error instanceof HistoricalCorpusUnavailableError || hostCode === "LLM_COMPLETION_NOT_AUTHORIZED" ||
|
|
43
50
|
(error instanceof TypeSafeRequestError && error.code === "http_error" &&
|
|
44
51
|
error.status !== undefined && error.status >= 400 && error.status < 500) ? "failed" : "ambiguous";
|
|
45
52
|
store.finishStep(step.stage, step.id, attempt, { status,
|
|
46
53
|
error: error instanceof TypeSafeRequestError && error.code === "http_error" ?
|
|
47
|
-
`http_${error.status}` :
|
|
54
|
+
`http_${error.status}` : error instanceof HistoricalCorpusUnavailableError ? "historical_corpus_unavailable" :
|
|
55
|
+
hostCode ?? "request_or_response_uncertain" });
|
|
48
56
|
result[status]++;
|
|
49
57
|
return;
|
|
50
58
|
}
|
|
@@ -52,6 +60,18 @@ async function checkpoint(store, step, result, operation) {
|
|
|
52
60
|
result.completed++;
|
|
53
61
|
return value;
|
|
54
62
|
}
|
|
63
|
+
function save(store, step, value) {
|
|
64
|
+
const attempt = store.startStep(step.stage, step.id, step.request);
|
|
65
|
+
store.finishStep(step.stage, step.id, attempt, { result: value });
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
async function drain(tasks) {
|
|
69
|
+
const settled = await Promise.allSettled(tasks);
|
|
70
|
+
const failure = settled.find(item => item.status === "rejected");
|
|
71
|
+
if (failure)
|
|
72
|
+
throw failure.reason;
|
|
73
|
+
return settled.flatMap(item => item.status === "fulfilled" ? [item.value] : []);
|
|
74
|
+
}
|
|
55
75
|
export async function generateTrainingQueries(source, store, runtime, options) {
|
|
56
76
|
bounds(options);
|
|
57
77
|
const refreshed = collectTraining(source, store, { existingOnly: true }), result = summary();
|
|
@@ -59,8 +79,8 @@ export async function generateTrainingQueries(source, store, runtime, options) {
|
|
|
59
79
|
let teacher;
|
|
60
80
|
const examples = store.queryExamples(options.threshold);
|
|
61
81
|
let next = 0, stopped = false;
|
|
62
|
-
|
|
63
|
-
while (!stopped && !result.budgetLimited
|
|
82
|
+
await drain(Array.from({ length: Math.min(options.concurrency ?? 8, examples.length) }, async () => {
|
|
83
|
+
while (!stopped && !result.budgetLimited) {
|
|
64
84
|
const example = examples[next++];
|
|
65
85
|
if (!example)
|
|
66
86
|
return;
|
|
@@ -68,29 +88,49 @@ export async function generateTrainingQueries(source, store, runtime, options) {
|
|
|
68
88
|
if (seen.has(example.inputHash))
|
|
69
89
|
continue;
|
|
70
90
|
seen.add(example.inputHash);
|
|
71
|
-
const
|
|
72
|
-
if (step.result) {
|
|
73
|
-
result.cached
|
|
91
|
+
const steps = LANES.map(lane => ({ lane, step: store.step("generate", teacherRequest(example, lane)) }));
|
|
92
|
+
if (steps.every(({ step }) => step.result)) {
|
|
93
|
+
result.cached += steps.length;
|
|
74
94
|
continue;
|
|
75
95
|
}
|
|
76
|
-
if (step.status !== "pending") {
|
|
77
|
-
result.
|
|
96
|
+
if (steps.every(({ step }) => step.result || step.status !== "pending")) {
|
|
97
|
+
const problems = steps.filter(({ step }) => !step.result).map(({ step }) => step.id);
|
|
98
|
+
result.blocked += problems.length;
|
|
99
|
+
result.flagged++;
|
|
100
|
+
store.flagReview(example, "generation-unresolved", { steps: problems });
|
|
78
101
|
continue;
|
|
79
102
|
}
|
|
80
103
|
if (options.maxExamples !== undefined && result.examples >= options.maxExamples)
|
|
81
104
|
return;
|
|
82
|
-
const bytes = Buffer.byteLength(trainingTeacherMessage(request.input)) + Buffer.byteLength(TRAINING_TEACHER_PROMPT);
|
|
83
|
-
if (result.inputBytes + bytes > options.maxInputBytes) {
|
|
84
|
-
result.budgetLimited = true;
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
105
|
result.examples++;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
106
|
+
const problems = [];
|
|
107
|
+
for (const { lane, step } of steps) {
|
|
108
|
+
if (step.result) {
|
|
109
|
+
result.cached++;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (step.status !== "pending") {
|
|
113
|
+
result.blocked++;
|
|
114
|
+
problems.push(step.id);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const input = JSON.parse(example.inputJson);
|
|
118
|
+
const bytes = Buffer.byteLength(trainingTeacherMessage(input, lane)) + Buffer.byteLength(trainingTeacherPrompt(lane));
|
|
119
|
+
if (result.inputBytes + bytes > options.maxInputBytes) {
|
|
120
|
+
result.budgetLimited = true;
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
result.inputBytes += bytes;
|
|
124
|
+
if (options.dryRun)
|
|
125
|
+
continue;
|
|
126
|
+
teacher ??= trainingTeacher(runtime, source.agentId);
|
|
127
|
+
if (!await checkpoint(store, step, result, () => teacher(input, lane)))
|
|
128
|
+
problems.push(step.id);
|
|
129
|
+
}
|
|
130
|
+
if (problems.length) {
|
|
131
|
+
store.flagReview(example, "generation-unresolved", { steps: problems });
|
|
132
|
+
result.flagged++;
|
|
133
|
+
}
|
|
94
134
|
}
|
|
95
135
|
catch (error) {
|
|
96
136
|
stopped = true;
|
|
@@ -98,142 +138,237 @@ export async function generateTrainingQueries(source, store, runtime, options) {
|
|
|
98
138
|
}
|
|
99
139
|
}
|
|
100
140
|
}));
|
|
101
|
-
const failure = workers.find(item => item.status === "rejected");
|
|
102
|
-
if (failure)
|
|
103
|
-
throw failure.reason;
|
|
104
141
|
return { refreshed, ...result, threshold: options.threshold ?? TRAINING_GATE_THRESHOLD, model: TRAINING_TEACHER_MODEL };
|
|
105
142
|
}
|
|
143
|
+
/** Raw top-three average, including values below runtime's injection threshold. */
|
|
144
|
+
export function trainingQueryScore(probabilities) {
|
|
145
|
+
const top = probabilities.toSorted((a, b) => b - a).slice(0, 3);
|
|
146
|
+
return top.length ? top.reduce((sum, value) => sum + value, 0) / top.length : 0;
|
|
147
|
+
}
|
|
106
148
|
export function selectTrainingQueries(queries) {
|
|
107
|
-
|
|
108
|
-
|
|
149
|
+
const winner = (lane) => {
|
|
150
|
+
const ranked = queries.filter(query => query.lane === lane).toSorted((a, b) => b.score - a.score);
|
|
151
|
+
if (!ranked[0])
|
|
152
|
+
throw new Error(`No resolved ${lane} candidates`);
|
|
153
|
+
return ranked[0].query; // Stable ties preserve round and teacher order.
|
|
154
|
+
};
|
|
155
|
+
return { lex: winner("lex"), vec: winner("vec") };
|
|
109
156
|
}
|
|
110
|
-
export async function evaluateTrainingQueries(source, store, config, options, createSearch = historicalTrainingSearch) {
|
|
157
|
+
export async function evaluateTrainingQueries(source, store, config, runtime, options, createSearch = historicalTrainingSearch) {
|
|
111
158
|
bounds(options);
|
|
112
|
-
if (options.excludeJudgments?.some(id => !/^[a-f0-9]{64}$/u.test(id)))
|
|
113
|
-
throw new Error("Invalid excluded judgment hash");
|
|
114
159
|
const corpus = config.corpora.find(c => c.kind === "sessions");
|
|
115
160
|
if (!corpus)
|
|
116
161
|
throw new Error("Training retrieval requires a configured sessions corpus");
|
|
117
162
|
const refreshed = collectTraining(source, store, { existingOnly: true });
|
|
118
163
|
const result = { ...summary(), retrievals: 0, evaluated: 0, awaitingTeacher: 0 };
|
|
119
164
|
const concurrency = options.concurrency ?? TRAINING_EVALUATION_CONCURRENCY;
|
|
120
|
-
let key;
|
|
165
|
+
let key, teacher;
|
|
121
166
|
const apiKey = () => key ??= resolveTypeSafeApiKey(config.typesafe).then(value => {
|
|
122
167
|
if (!value)
|
|
123
168
|
throw new Error("TypeSafe is disabled or its credential is unavailable");
|
|
124
169
|
return value;
|
|
125
170
|
});
|
|
126
171
|
let stopped = false;
|
|
127
|
-
const shouldStop = () => stopped || result.budgetLimited
|
|
172
|
+
const shouldStop = () => stopped || result.budgetLimited;
|
|
173
|
+
const room = () => {
|
|
174
|
+
if (shouldStop())
|
|
175
|
+
return false;
|
|
176
|
+
if (options.maxCalls !== undefined && result.calls >= options.maxCalls) {
|
|
177
|
+
result.budgetLimited = true;
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
return true;
|
|
181
|
+
};
|
|
128
182
|
const judgments = new Map();
|
|
183
|
+
const revisions = new Map();
|
|
184
|
+
const reviews = new Map(store.reviews().map(review => [review.sourceId, review]));
|
|
129
185
|
const evaluate = async (example) => {
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
132
|
-
|
|
186
|
+
const previousReview = reviews.get(example.id);
|
|
187
|
+
if (previousReview && "steps" in previousReview.details) {
|
|
188
|
+
const blocked = previousReview.details.steps.filter(id => {
|
|
189
|
+
const status = store.stepRecordStatus(id);
|
|
190
|
+
return status !== undefined && status !== "pending" && status !== "complete";
|
|
191
|
+
});
|
|
192
|
+
// A known failed example cannot resolve before manual retry. Do not scan its
|
|
193
|
+
// historical corpus or consume the example budget on every invocation.
|
|
194
|
+
if (blocked.length) {
|
|
195
|
+
result.blocked += blocked.length;
|
|
196
|
+
result.flagged++;
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const initial = LANES.map(lane => ({ lane, step: store.step("generate", teacherRequest(example, lane)) }));
|
|
201
|
+
if (initial.some(({ step }) => !step.result)) {
|
|
202
|
+
const blocked = initial.filter(({ step }) => !step.result && step.status !== "pending");
|
|
203
|
+
if (blocked.length) {
|
|
204
|
+
result.blocked += blocked.length;
|
|
205
|
+
result.flagged++;
|
|
206
|
+
store.flagReview(example, "generation-unresolved", { steps: blocked.map(({ step }) => step.id) });
|
|
207
|
+
}
|
|
208
|
+
else
|
|
209
|
+
result.awaitingTeacher++;
|
|
133
210
|
return;
|
|
134
211
|
}
|
|
135
212
|
if (options.maxExamples !== undefined && result.examples >= options.maxExamples)
|
|
136
213
|
return;
|
|
137
214
|
store.renew();
|
|
138
|
-
|
|
215
|
+
let snapshot;
|
|
216
|
+
try {
|
|
217
|
+
snapshot = await createSearch(source.stateDir, corpus.chatTypes, example.timestamp);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (!(error instanceof HistoricalCorpusUnavailableError))
|
|
221
|
+
throw error;
|
|
222
|
+
store.flagReview(example, "historical-snapshot-unavailable", { timestamp: example.timestamp });
|
|
223
|
+
result.flagged++;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
139
226
|
try {
|
|
140
227
|
const evaluation = store.step("evaluate", { version: SELECTION_VERSION, sourceId: example.id, inputHash: example.inputHash,
|
|
141
|
-
timestamp: example.timestamp,
|
|
228
|
+
timestamp: example.timestamp, teacherIds: initial.map(({ step }) => step.id), corpusHash: snapshot.corpusHash,
|
|
142
229
|
retrievalVersion: TRAINING_RETRIEVAL_VERSION, judgeVersion: CONTEXT_JUDGE_VERSION,
|
|
143
|
-
|
|
230
|
+
options: TRAINING_SEARCH_OPTIONS });
|
|
144
231
|
if (evaluation.result) {
|
|
145
232
|
result.cached++;
|
|
233
|
+
store.clearReview(example.id);
|
|
146
234
|
return;
|
|
147
235
|
}
|
|
148
236
|
if (evaluation.status !== "pending") {
|
|
149
237
|
result.blocked++;
|
|
238
|
+
result.flagged++;
|
|
239
|
+
store.flagReview(example, "evaluation-unresolved", { steps: [evaluation.id] });
|
|
150
240
|
return;
|
|
151
241
|
}
|
|
152
|
-
// Snapshot creation is async: reserve the shared example budget only after it settles.
|
|
153
242
|
if (shouldStop() || (options.maxExamples !== undefined && result.examples >= options.maxExamples))
|
|
154
243
|
return;
|
|
155
244
|
result.examples++;
|
|
156
245
|
if (options.dryRun)
|
|
157
246
|
return;
|
|
158
247
|
const input = JSON.parse(example.inputJson);
|
|
248
|
+
const problems = new Set();
|
|
159
249
|
const judge = (hit) => {
|
|
160
250
|
const request = contextJudgeRequest(input, snapshot.maxDate, hit);
|
|
161
|
-
|
|
162
|
-
const identity = trainingHash([CONTEXT_JUDGE_VERSION, example.inputHash, hit.position, request]);
|
|
251
|
+
const identity = trainingHash([CONTEXT_JUDGE_VERSION, request]);
|
|
163
252
|
let pending = judgments.get(identity);
|
|
164
253
|
if (!pending) {
|
|
165
254
|
pending = (async () => {
|
|
166
|
-
const
|
|
167
|
-
const step = store.step("judge", { identity, request, excluded });
|
|
255
|
+
const step = store.step("judge", { identity, request });
|
|
168
256
|
if (step.result) {
|
|
169
257
|
result.cached++;
|
|
170
258
|
return { id: step.id, result: step.result };
|
|
171
259
|
}
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
store.finishStep("judge", step.id, attempt, { result: value });
|
|
176
|
-
return { id: step.id, result: value };
|
|
260
|
+
if (step.status !== "pending") {
|
|
261
|
+
result.blocked++;
|
|
262
|
+
return { id: step.id, result: undefined };
|
|
177
263
|
}
|
|
178
264
|
const credential = await apiKey();
|
|
179
|
-
if (
|
|
265
|
+
if (!room())
|
|
180
266
|
return { id: step.id, result: undefined };
|
|
181
|
-
if (options.maxCalls !== undefined && result.calls >= options.maxCalls) {
|
|
182
|
-
result.budgetLimited = true;
|
|
183
|
-
return { id: step.id, result: undefined };
|
|
184
|
-
}
|
|
185
267
|
return { id: step.id, result: await checkpoint(store, step, result, () => judgeTrainingPassage(request, credential)) };
|
|
186
268
|
})();
|
|
187
269
|
judgments.set(identity, pending);
|
|
188
270
|
}
|
|
189
271
|
return pending;
|
|
190
272
|
};
|
|
191
|
-
|
|
192
|
-
const settled = await Promise.allSettled(teacher.result.queries.map(async (query) => {
|
|
273
|
+
const scoreQueries = async (lane, round, generation, generationId) => await drain(generation.queries.map(async (query) => {
|
|
193
274
|
try {
|
|
194
|
-
|
|
275
|
+
const score = store.step("score", { version: SELECTION_VERSION, lane, round, generationId, query,
|
|
276
|
+
sourceId: example.id, corpusHash: snapshot.corpusHash, judgeVersion: CONTEXT_JUDGE_VERSION,
|
|
277
|
+
retrievalVersion: TRAINING_RETRIEVAL_VERSION });
|
|
278
|
+
if (score.result) {
|
|
279
|
+
result.cached++;
|
|
280
|
+
return score.result;
|
|
281
|
+
}
|
|
282
|
+
if (score.status !== "pending") {
|
|
283
|
+
problems.add(score.id);
|
|
284
|
+
result.blocked++;
|
|
195
285
|
return;
|
|
196
|
-
|
|
286
|
+
}
|
|
287
|
+
const retrieval = store.step("retrieve", { version: TRAINING_RETRIEVAL_VERSION, query, lane,
|
|
197
288
|
sourceId: example.id, maxDate: snapshot.maxDate, corpusHash: snapshot.corpusHash, options: TRAINING_SEARCH_OPTIONS });
|
|
198
|
-
if (
|
|
289
|
+
if (!retrieval.result && retrieval.status === "pending" && !room())
|
|
199
290
|
return;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
291
|
+
const retrieved = await checkpoint(store, retrieval, result, async () => ({ query, lane, maxDate: snapshot.maxDate,
|
|
292
|
+
corpusHash: snapshot.corpusHash, hits: await snapshot.search(query, lane) }));
|
|
293
|
+
if (!retrieved) {
|
|
294
|
+
problems.add(retrieval.id);
|
|
203
295
|
return;
|
|
204
296
|
}
|
|
205
|
-
const retrieved = await checkpoint(store, retrieval, result, async () => ({ query, maxDate: snapshot.maxDate,
|
|
206
|
-
corpusHash: snapshot.corpusHash, hits: await snapshot.search(query) }));
|
|
207
|
-
if (!retrieved)
|
|
208
|
-
return;
|
|
209
297
|
if (!retrieval.result)
|
|
210
298
|
result.retrievals++;
|
|
211
|
-
const
|
|
212
|
-
const
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
299
|
+
const scored = await drain(retrieved.hits.map(judge));
|
|
300
|
+
const missing = scored.filter(item => !item.result);
|
|
301
|
+
if (missing.length) {
|
|
302
|
+
for (const item of missing) {
|
|
303
|
+
const record = store.stepRecordStatus(item.id);
|
|
304
|
+
if (record && record !== "pending")
|
|
305
|
+
problems.add(item.id);
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const probabilities = scored.map(item => item.result.probability);
|
|
310
|
+
return save(store, score, { query, lane, round, retrievalId: retrieval.id,
|
|
311
|
+
score: trainingQueryScore(probabilities), maxProbability: Math.max(0, ...probabilities), judgments: scored.map(item => item.id) });
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
stopped = true;
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
}));
|
|
318
|
+
const lanes = await drain(initial.map(async ({ lane, step }) => {
|
|
319
|
+
try {
|
|
320
|
+
const first = await scoreQueries(lane, 1, step.result, step.id);
|
|
321
|
+
if (first.some(item => !item) || shouldStop())
|
|
322
|
+
return;
|
|
323
|
+
const feedback = first.map(item => ({ query: item.query, score: item.score }));
|
|
324
|
+
// The only feedback sent to Luna is this lane's exact earlier queries and scores.
|
|
325
|
+
const revision = store.step("generate", teacherRequest(example, lane, feedback));
|
|
326
|
+
let pending = revisions.get(revision.id);
|
|
327
|
+
if (!pending) {
|
|
328
|
+
pending = (async () => {
|
|
329
|
+
if (!revision.result && revision.status === "pending") {
|
|
330
|
+
const bytes = Buffer.byteLength(trainingTeacherMessage(input, lane, feedback)) + Buffer.byteLength(trainingTeacherPrompt(lane));
|
|
331
|
+
if (!room())
|
|
332
|
+
return;
|
|
333
|
+
if (result.inputBytes + bytes > (options.maxInputBytes ?? 3_000_000)) {
|
|
334
|
+
result.budgetLimited = true;
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
result.inputBytes += bytes;
|
|
338
|
+
teacher ??= trainingTeacher(runtime, source.agentId);
|
|
339
|
+
}
|
|
340
|
+
return checkpoint(store, revision, result, () => teacher(input, lane, feedback));
|
|
341
|
+
})();
|
|
342
|
+
revisions.set(revision.id, pending);
|
|
343
|
+
}
|
|
344
|
+
const revised = await pending;
|
|
345
|
+
if (!revised) {
|
|
346
|
+
if (store.stepRecordStatus(revision.id))
|
|
347
|
+
problems.add(revision.id);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const second = await scoreQueries(lane, 2, revised, revision.id);
|
|
351
|
+
if (second.some(item => !item))
|
|
217
352
|
return;
|
|
218
|
-
|
|
219
|
-
return { query, retrievalId: retrieval.id, score: scores.sort((a, b) => b - a).slice(0, 5).reduce((a, b) => a + b, 0),
|
|
220
|
-
judgments: scored.map(item => ({ id: item.id, excluded: !!item.result && "excluded" in item.result })) };
|
|
353
|
+
return { teacherIds: [step.id, revision.id], queries: [...first, ...second].filter((item) => !!item) };
|
|
221
354
|
}
|
|
222
355
|
catch (error) {
|
|
223
356
|
stopped = true;
|
|
224
357
|
throw error;
|
|
225
358
|
}
|
|
226
359
|
}));
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
360
|
+
if (problems.size) {
|
|
361
|
+
result.flagged++;
|
|
362
|
+
store.flagReview(example, "evaluation-unresolved", { steps: [...problems] });
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (lanes.some(lane => !lane))
|
|
366
|
+
return; // Budget pause: completed operations remain reusable.
|
|
367
|
+
const queries = lanes.flatMap(lane => lane.queries);
|
|
233
368
|
const selected = selectTrainingQueries(queries);
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
369
|
+
save(store, evaluation, { sourceId: example.id, inputHash: example.inputHash, timestamp: example.timestamp,
|
|
370
|
+
corpusHash: snapshot.corpusHash, corpusReport: snapshot.report, teacherIds: lanes.flatMap(lane => lane.teacherIds), queries, selected, review: [] });
|
|
371
|
+
store.clearReview(example.id);
|
|
237
372
|
result.evaluated++;
|
|
238
373
|
}
|
|
239
374
|
finally {
|
|
@@ -242,7 +377,7 @@ export async function evaluateTrainingQueries(source, store, config, options, cr
|
|
|
242
377
|
};
|
|
243
378
|
const examples = store.queryExamples(options.threshold);
|
|
244
379
|
let next = 0;
|
|
245
|
-
|
|
380
|
+
await drain(Array.from({ length: Math.min(concurrency, examples.length) }, async () => {
|
|
246
381
|
while (!shouldStop() && (options.maxExamples === undefined || result.examples < options.maxExamples)) {
|
|
247
382
|
const example = examples[next++];
|
|
248
383
|
if (!example)
|
|
@@ -256,27 +391,28 @@ export async function evaluateTrainingQueries(source, store, config, options, cr
|
|
|
256
391
|
}
|
|
257
392
|
}
|
|
258
393
|
}));
|
|
259
|
-
const failure = workers.find(item => item.status === "rejected");
|
|
260
|
-
if (failure)
|
|
261
|
-
throw failure.reason; // All workers/snapshots have drained, including on storage failures.
|
|
262
394
|
return { refreshed, ...result, concurrency, threshold: options.threshold ?? TRAINING_GATE_THRESHOLD,
|
|
263
|
-
retrievalMethod: "
|
|
395
|
+
retrievalMethod: "independent-lex10-vec10", callUnit: "teacher completion, retrieval operation or uncached passage judgment" };
|
|
264
396
|
}
|
|
265
397
|
export function* exportQueryTraining(store, threshold = TRAINING_GATE_THRESHOLD) {
|
|
266
398
|
const active = new Map(store.queryExamples(threshold).map(e => [e.id, e]));
|
|
267
|
-
const exported = new Set();
|
|
268
|
-
for (const evaluation of store.completedEvaluations({ selection: SELECTION_VERSION, retrieval: TRAINING_RETRIEVAL_VERSION })) {
|
|
399
|
+
const unresolved = new Set(store.reviews().map(review => review.sourceId)), exported = new Set();
|
|
400
|
+
for (const evaluation of store.completedEvaluations({ selection: SELECTION_VERSION, retrieval: TRAINING_RETRIEVAL_VERSION, judge: CONTEXT_JUDGE_VERSION })) {
|
|
269
401
|
const source = active.get(evaluation.sourceId);
|
|
270
402
|
if (!source || source.inputHash !== evaluation.inputHash || source.timestamp !== evaluation.timestamp || exported.has(source.id))
|
|
271
403
|
continue;
|
|
272
|
-
|
|
404
|
+
exported.add(source.id); // Never fall back to an older accepted corpus after a newer flagged result.
|
|
405
|
+
if (unresolved.has(source.id))
|
|
406
|
+
continue;
|
|
407
|
+
if (!LANES.every(lane => evaluation.teacherIds.includes(store.step("generate", teacherRequest(source, lane)).id)))
|
|
273
408
|
continue;
|
|
274
|
-
|
|
275
|
-
|
|
409
|
+
// Selection is a local policy over completed scores, not another paid step.
|
|
410
|
+
// This also recovers v2 checkpoints whose old usefulness policy saved null.
|
|
411
|
+
const selected = selectTrainingQueries(evaluation.queries);
|
|
412
|
+
const provenance = [...evaluation.teacherIds, ...evaluation.queries.flatMap(q => [q.retrievalId, ...q.judgments])];
|
|
276
413
|
yield { stage: "query-training", input: JSON.parse(source.inputJson), inputHash: source.inputHash,
|
|
277
|
-
recallProbability: source.recallProbability, threshold,
|
|
278
|
-
|
|
279
|
-
splitGroup: trainingHash(source.sessionId), // Also keep identical inputHash groups together across sessions/nodes.
|
|
414
|
+
recallProbability: source.recallProbability, threshold, target: selected, targetPolicy: TARGET_POLICY,
|
|
415
|
+
source: store.sourceDetails(source.id), evaluation: { ...evaluation, selected, review: [] }, splitGroup: trainingHash(source.sessionId),
|
|
280
416
|
provenance: [...new Set(provenance)].map(id => store.stepRecord(id)) };
|
|
281
417
|
}
|
|
282
418
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ChatType } from "./config.js";
|
|
2
2
|
import type { SessionMessageSpan } from "./session-projector.js";
|
|
3
|
-
|
|
3
|
+
import type { QueryLane } from "./query-contract.js";
|
|
4
|
+
export declare const TRAINING_RETRIEVAL_VERSION = "qmd-2.10.2-historical-lanes-depth10-v3:complete-excerpt-1200-v1";
|
|
4
5
|
export declare const TRAINING_SEARCH_OPTIONS: {
|
|
5
6
|
readonly vector: 10;
|
|
6
7
|
readonly bm25: 10;
|
|
@@ -9,12 +10,18 @@ export declare const TRAINING_SEARCH_OPTIONS: {
|
|
|
9
10
|
};
|
|
10
11
|
export type TrainingHit = {
|
|
11
12
|
path: string;
|
|
13
|
+
corpus: string;
|
|
12
14
|
text: string;
|
|
13
15
|
dates: string[];
|
|
14
16
|
position: number;
|
|
17
|
+
startLine: number;
|
|
18
|
+
endLine: number;
|
|
15
19
|
score: number;
|
|
16
20
|
methods: string[];
|
|
17
21
|
};
|
|
22
|
+
/** Missing historical evidence is reviewable; unexpected SQLite/I/O failures remain fatal. */
|
|
23
|
+
export declare class HistoricalCorpusUnavailableError extends Error {
|
|
24
|
+
}
|
|
18
25
|
/** Never infer dates by parsing message bodies: headings can be quoted or forged. */
|
|
19
26
|
export declare function historicalPrefix(body: string, spans: readonly SessionMessageSpan[] | undefined, cutoff: number): {
|
|
20
27
|
body: string;
|
|
@@ -32,6 +39,6 @@ export declare function historicalTrainingSearch(stateDir: string, chatTypes: re
|
|
|
32
39
|
excludedChunks: number;
|
|
33
40
|
};
|
|
34
41
|
maxDate: string;
|
|
35
|
-
search: (query: string) => Promise<TrainingHit[]>;
|
|
42
|
+
search: (query: string, lane: QueryLane) => Promise<TrainingHit[]>;
|
|
36
43
|
close: () => Promise<void>;
|
|
37
44
|
}>;
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { statSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { setImmediate as yieldToEventLoop } from "node:timers/promises";
|
|
6
6
|
import { readSessionManifest } from "./session-sync.js";
|
|
7
7
|
import { resolveSessionSource } from "./sources.js";
|
|
8
8
|
import { trainingCandidates } from "./training-candidates.js";
|
|
9
|
-
|
|
9
|
+
import { MEMORY_PASSAGE_VERSION, renderMemoryPassage, duplicateMemoryPassage } from "./memory-passage.js";
|
|
10
|
+
export const TRAINING_RETRIEVAL_VERSION = `qmd-2.10.2-historical-lanes-depth10-v3:${MEMORY_PASSAGE_VERSION}`;
|
|
10
11
|
export const TRAINING_SEARCH_OPTIONS = { vector: 10, bm25: 10, mergedLimit: null, rerank: false };
|
|
12
|
+
/** Missing historical evidence is reviewable; unexpected SQLite/I/O failures remain fatal. */
|
|
13
|
+
export class HistoricalCorpusUnavailableError extends Error {
|
|
14
|
+
}
|
|
11
15
|
/** Never infer dates by parsing message bodies: headings can be quoted or forged. */
|
|
12
16
|
export function historicalPrefix(body, spans, cutoff) {
|
|
13
17
|
if (!spans?.length || !body.startsWith("# Transcript\n\n") || !Number.isFinite(cutoff))
|
|
@@ -47,8 +51,15 @@ export function historicalPrefix(body, spans, cutoff) {
|
|
|
47
51
|
* No filesystem projection, live-index mutation, model re-embedding or dependency patch. */
|
|
48
52
|
export async function historicalTrainingSearch(stateDir, chatTypes, cutoff, openStore) {
|
|
49
53
|
const indexPath = join(stateDir, "index.sqlite");
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
try {
|
|
55
|
+
statSync(indexPath);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
59
|
+
throw new HistoricalCorpusUnavailableError("No QMD index; sync sessions before evaluating training queries");
|
|
60
|
+
}
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
52
63
|
const manifest = await readSessionManifest(join(stateDir, "sessions-manifest.json"));
|
|
53
64
|
const source = resolveSessionSource(join(stateDir, "sessions"), chatTypes);
|
|
54
65
|
const createStore = openStore ?? (await import("@unblocklabs/qmd")).createStore;
|
|
@@ -189,16 +200,21 @@ export async function historicalTrainingSearch(stateDir, chatTypes, cutoff, open
|
|
|
189
200
|
}
|
|
190
201
|
};
|
|
191
202
|
return { corpusHash, report, maxDate,
|
|
192
|
-
search: async (query) => {
|
|
203
|
+
search: async (query, lane) => {
|
|
193
204
|
if (closed)
|
|
194
205
|
throw new Error("Historical snapshot is closed");
|
|
195
206
|
if (!report.sessions)
|
|
196
207
|
return [];
|
|
197
|
-
if (!report.chunks)
|
|
198
|
-
throw new
|
|
208
|
+
if (lane === "vec" && !report.chunks)
|
|
209
|
+
throw new HistoricalCorpusUnavailableError("Historical snapshot has no vectors; refusing an unresolved vector evaluation");
|
|
199
210
|
const qmd = await (indexed ??= materialize());
|
|
200
|
-
const hits = await trainingCandidates(qmd, query, source.collection,
|
|
201
|
-
|
|
211
|
+
const hits = await trainingCandidates(qmd, query, source.collection, lane);
|
|
212
|
+
const rendered = await Promise.all(hits.map(hit => trainingHit(hit, metadata, cutoff)));
|
|
213
|
+
const distinct = [];
|
|
214
|
+
for (const hit of rendered)
|
|
215
|
+
if (hit && !duplicateMemoryPassage(hit, distinct))
|
|
216
|
+
distinct.push(hit);
|
|
217
|
+
return distinct;
|
|
202
218
|
}, close: async () => {
|
|
203
219
|
if (closed)
|
|
204
220
|
return;
|
|
@@ -221,12 +237,20 @@ function historicalChunk(chunk, length) {
|
|
|
221
237
|
return Number.isSafeInteger(chunk.pos) && Number.isSafeInteger(chunk.chunk_len) && chunk.pos >= 0 && chunk.chunk_len > 0 &&
|
|
222
238
|
chunk.pos + chunk.chunk_len <= length;
|
|
223
239
|
}
|
|
224
|
-
function trainingHit(hit, metadata, cutoff) {
|
|
225
|
-
const
|
|
240
|
+
async function trainingHit(hit, metadata, cutoff) {
|
|
241
|
+
const messages = metadata.get(hit.file);
|
|
242
|
+
const selected = await renderMemoryPassage({ body: hit.body, bestChunk: hit.bestChunk,
|
|
243
|
+
chunkPos: hit.bestChunkPos, chunkLen: hit.bestChunk.length }, messages);
|
|
244
|
+
if (!selected)
|
|
245
|
+
return;
|
|
246
|
+
const spans = messages?.filter(s => s.start < selected.position + (selected.sourceText ?? selected.text).length && s.end > selected.position);
|
|
226
247
|
if (!spans?.length || spans.some(s => !(Date.parse(s.timestamp) < Math.floor(cutoff / 1000) * 1000)) ||
|
|
227
248
|
hit.body.slice(hit.bestChunkPos, hit.bestChunkPos + hit.bestChunk.length) !== hit.bestChunk) {
|
|
228
249
|
throw new Error("QMD returned evidence outside the historical snapshot");
|
|
229
250
|
}
|
|
230
|
-
|
|
251
|
+
const startLine = hit.body.slice(0, selected.position).split("\n").length;
|
|
252
|
+
const endLine = startLine + (selected.sourceText ?? selected.text).split("\n").length - 1;
|
|
253
|
+
return { path: hit.file, corpus: "sessions", position: selected.position, text: selected.text, startLine, endLine,
|
|
254
|
+
dates: [...new Set(spans.map(s => s.timestamp))],
|
|
231
255
|
score: hit.score, methods: hit.explain?.methods ?? [] };
|
|
232
256
|
}
|