@unblocklabs/unblock-memory 0.3.29 → 0.4.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.
Files changed (50) hide show
  1. package/README.md +2 -2
  2. package/dist/src/config.d.ts +0 -1
  3. package/dist/src/config.js +2 -6
  4. package/dist/src/diagnostics.d.ts +1 -3
  5. package/dist/src/manager.d.ts +4 -8
  6. package/dist/src/manager.js +17 -45
  7. package/dist/src/memory-passage.d.ts +26 -0
  8. package/dist/src/memory-passage.js +48 -0
  9. package/dist/src/memory-whisperer.d.ts +2 -1
  10. package/dist/src/memory-whisperer.js +21 -17
  11. package/dist/src/mlx-query.d.ts +3 -10
  12. package/dist/src/mlx-query.js +49 -25
  13. package/dist/src/query-contract.d.ts +31 -0
  14. package/dist/src/query-contract.js +58 -0
  15. package/dist/src/query-tokenizer/contract.json +10 -0
  16. package/dist/src/response-stages.d.ts +3 -3
  17. package/dist/src/training-candidates.d.ts +2 -1
  18. package/dist/src/training-candidates.js +12 -9
  19. package/dist/src/training-input.d.ts +13 -10
  20. package/dist/src/training-input.js +22 -18
  21. package/dist/src/training-judge.d.ts +16 -46
  22. package/dist/src/training-judge.js +6 -53
  23. package/dist/src/training-models.d.ts +15 -7
  24. package/dist/src/training-models.js +36 -38
  25. package/dist/src/training-queries.d.ts +23 -7
  26. package/dist/src/training-queries.js +247 -102
  27. package/dist/src/training-retrieval.d.ts +9 -2
  28. package/dist/src/training-retrieval.js +36 -12
  29. package/dist/src/training-runtime.js +8 -7
  30. package/dist/src/training-store.d.ts +46 -16
  31. package/dist/src/training-store.js +83 -29
  32. package/dist/src/training.d.ts +10 -0
  33. package/dist/src/training.js +7 -5
  34. package/dist/src/typesafe.d.ts +58 -8
  35. package/dist/src/typesafe.js +47 -29
  36. package/dist/src/whisperer-context.d.ts +0 -8
  37. package/dist/src/whisperer-context.js +0 -23
  38. package/docs/configuration.md +3 -3
  39. package/docs/memory-training.md +114 -58
  40. package/docs/retrieval.md +34 -24
  41. package/openclaw.plugin.json +2 -3
  42. package/package.json +5 -2
  43. package/src/mlx-query-worker.py +5 -8
  44. package/src/query-tokenizer/LICENSE +71 -0
  45. package/src/query-tokenizer/README.md +21 -0
  46. package/src/query-tokenizer/chat_template.jinja +115 -0
  47. package/src/query-tokenizer/contract.json +10 -0
  48. package/src/query-tokenizer/tokenizer.json +323830 -0
  49. package/src/query-tokenizer/tokenizer_config.json +19 -0
  50. package/src/query_contract.py +60 -0
@@ -1,5 +1,6 @@
1
1
  import type { UnblockMemoryConfig } from "./config.js";
2
- import type { TrainingInput } from "./training-input.js";
2
+ import type { QueryPair } from "./query-contract.js";
3
+ import { type TrainingInput } from "./training-input.js";
3
4
  import { historicalTrainingSearch } from "./training-retrieval.js";
4
5
  import type { QueryEvaluation, TrainingStore } from "./training-store.js";
5
6
  type Source = {
@@ -26,6 +27,7 @@ export declare function generateTrainingQueries(source: Source, store: TrainingS
26
27
  failed: number;
27
28
  ambiguous: number;
28
29
  blocked: number;
30
+ flagged: number;
29
31
  inputBytes: number;
30
32
  budgetLimited: boolean;
31
33
  refreshed: {
@@ -41,13 +43,20 @@ export declare function generateTrainingQueries(source: Source, store: TrainingS
41
43
  changed: number;
42
44
  unchanged: number;
43
45
  retired: number;
46
+ review: {
47
+ sessionId: string;
48
+ seq: number;
49
+ reason: string;
50
+ }[];
44
51
  };
45
52
  }>;
46
- export declare function selectTrainingQueries(queries: QueryEvaluation[]): string[];
47
- export declare function evaluateTrainingQueries(source: Source, store: TrainingStore, config: UnblockMemoryConfig, options: Options & {
53
+ /** Raw top-three average, including values below runtime's injection threshold. */
54
+ export declare function trainingQueryScore(probabilities: readonly number[]): number;
55
+ export declare function selectTrainingQueries(queries: readonly QueryEvaluation[]): QueryPair;
56
+ export declare function evaluateTrainingQueries(source: Source, store: TrainingStore, config: UnblockMemoryConfig, runtime: unknown, options: Options & {
48
57
  maxCalls?: number;
58
+ maxInputBytes?: number;
49
59
  concurrency?: number;
50
- excludeJudgments?: string[];
51
60
  }, createSearch?: typeof historicalTrainingSearch): Promise<{
52
61
  concurrency: number;
53
62
  threshold: number;
@@ -63,6 +72,7 @@ export declare function evaluateTrainingQueries(source: Source, store: TrainingS
63
72
  failed: number;
64
73
  ambiguous: number;
65
74
  blocked: number;
75
+ flagged: number;
66
76
  inputBytes: number;
67
77
  budgetLimited: boolean;
68
78
  refreshed: {
@@ -78,6 +88,11 @@ export declare function evaluateTrainingQueries(source: Source, store: TrainingS
78
88
  changed: number;
79
89
  unchanged: number;
80
90
  retired: number;
91
+ review: {
92
+ sessionId: string;
93
+ seq: number;
94
+ reason: string;
95
+ }[];
81
96
  };
82
97
  }>;
83
98
  export declare function exportQueryTraining(store: TrainingStore, threshold?: number): Generator<{
@@ -86,7 +101,7 @@ export declare function exportQueryTraining(store: TrainingStore, threshold?: nu
86
101
  inputHash: string;
87
102
  recallProbability: number;
88
103
  threshold: number;
89
- target: string[];
104
+ target: QueryPair;
90
105
  source: {
91
106
  nodeId: string;
92
107
  agentId: string;
@@ -97,7 +112,7 @@ export declare function exportQueryTraining(store: TrainingStore, threshold?: nu
97
112
  inputHash: string;
98
113
  timestamp: number;
99
114
  corpusHash: string;
100
- teacherId: string;
115
+ teacherIds: string[];
101
116
  corpusReport: {
102
117
  sessions: number;
103
118
  chunks: number;
@@ -106,7 +121,8 @@ export declare function exportQueryTraining(store: TrainingStore, threshold?: nu
106
121
  excludedChunks: number;
107
122
  };
108
123
  queries: QueryEvaluation[];
109
- selected: string[];
124
+ selected: QueryPair | null;
125
+ review: string[];
110
126
  };
111
127
  splitGroup: string;
112
128
  provenance: {
@@ -2,10 +2,11 @@ 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, TRAINING_TEACHER_MODEL, TRAINING_TEACHER_PROMPT, TRAINING_TEACHER_VERSION } from "./training-models.js";
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 SELECTION_VERSION = "conversation-utility-top5-sum-v2";
8
+ const LANES = ["lex", "vec"];
9
+ const SELECTION_VERSION = "independent-lanes-top3-mean-v2";
9
10
  export const TRAINING_EVALUATION_CONCURRENCY = 4;
10
11
  function bounds(options) {
11
12
  for (const value of [options.maxExamples, options.maxCalls, options.maxInputBytes, options.concurrency]) {
@@ -13,13 +14,14 @@ function bounds(options) {
13
14
  throw new Error("Training bounds must be positive integers");
14
15
  }
15
16
  }
16
- function teacherRequest(example) {
17
+ function teacherRequest(example, lane, feedback) {
17
18
  return { version: TRAINING_TEACHER_VERSION, model: TRAINING_TEACHER_MODEL, inputHash: example.inputHash,
18
- input: JSON.parse(example.inputJson) };
19
+ input: JSON.parse(example.inputJson), lane, round: feedback ? 2 : 1,
20
+ ...(feedback ? { feedback: feedback.map(({ query, score }) => ({ query, score })) } : {}) };
19
21
  }
20
22
  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 paid operation, with no hidden transport retries. */
23
+ blocked: 0, flagged: 0, inputBytes: 0, budgetLimited: false });
24
+ /** One fenced checkpoint per operation. Storage failures are fatal; provider failures are local. */
23
25
  async function checkpoint(store, step, result, operation) {
24
26
  if (step.result !== undefined) {
25
27
  result.cached++;
@@ -37,14 +39,19 @@ async function checkpoint(store, step, result, operation) {
37
39
  value = await operation();
38
40
  }
39
41
  catch (error) {
42
+ // Historical retrieval is local code. Only an explicitly unavailable corpus
43
+ // is a reviewable data case; do not swallow SQLite, I/O or snapshot-integrity errors.
44
+ if (step.stage === "retrieve" && !(error instanceof HistoricalCorpusUnavailableError))
45
+ throw error;
40
46
  const hostCode = error && typeof error === "object" && "code" in error && typeof error.code === "string" &&
41
47
  /^LLM_[A-Z_]+$/u.test(error.code) ? error.code : undefined;
42
- const status = hostCode === "LLM_COMPLETION_NOT_AUTHORIZED" ||
48
+ const status = error instanceof HistoricalCorpusUnavailableError || hostCode === "LLM_COMPLETION_NOT_AUTHORIZED" ||
43
49
  (error instanceof TypeSafeRequestError && error.code === "http_error" &&
44
50
  error.status !== undefined && error.status >= 400 && error.status < 500) ? "failed" : "ambiguous";
45
51
  store.finishStep(step.stage, step.id, attempt, { status,
46
52
  error: error instanceof TypeSafeRequestError && error.code === "http_error" ?
47
- `http_${error.status}` : hostCode ?? "request_or_response_uncertain" });
53
+ `http_${error.status}` : error instanceof HistoricalCorpusUnavailableError ? "historical_corpus_unavailable" :
54
+ hostCode ?? "request_or_response_uncertain" });
48
55
  result[status]++;
49
56
  return;
50
57
  }
@@ -52,6 +59,18 @@ async function checkpoint(store, step, result, operation) {
52
59
  result.completed++;
53
60
  return value;
54
61
  }
62
+ function save(store, step, value) {
63
+ const attempt = store.startStep(step.stage, step.id, step.request);
64
+ store.finishStep(step.stage, step.id, attempt, { result: value });
65
+ return value;
66
+ }
67
+ async function drain(tasks) {
68
+ const settled = await Promise.allSettled(tasks);
69
+ const failure = settled.find(item => item.status === "rejected");
70
+ if (failure)
71
+ throw failure.reason;
72
+ return settled.flatMap(item => item.status === "fulfilled" ? [item.value] : []);
73
+ }
55
74
  export async function generateTrainingQueries(source, store, runtime, options) {
56
75
  bounds(options);
57
76
  const refreshed = collectTraining(source, store, { existingOnly: true }), result = summary();
@@ -59,8 +78,8 @@ export async function generateTrainingQueries(source, store, runtime, options) {
59
78
  let teacher;
60
79
  const examples = store.queryExamples(options.threshold);
61
80
  let next = 0, stopped = false;
62
- const workers = await Promise.allSettled(Array.from({ length: Math.min(options.concurrency ?? 8, examples.length) }, async () => {
63
- while (!stopped && !result.budgetLimited && !result.failed && !result.ambiguous) {
81
+ await drain(Array.from({ length: Math.min(options.concurrency ?? 8, examples.length) }, async () => {
82
+ while (!stopped && !result.budgetLimited) {
64
83
  const example = examples[next++];
65
84
  if (!example)
66
85
  return;
@@ -68,29 +87,49 @@ export async function generateTrainingQueries(source, store, runtime, options) {
68
87
  if (seen.has(example.inputHash))
69
88
  continue;
70
89
  seen.add(example.inputHash);
71
- const request = teacherRequest(example), step = store.step("generate", request);
72
- if (step.result) {
73
- result.cached++;
90
+ const steps = LANES.map(lane => ({ lane, step: store.step("generate", teacherRequest(example, lane)) }));
91
+ if (steps.every(({ step }) => step.result)) {
92
+ result.cached += steps.length;
74
93
  continue;
75
94
  }
76
- if (step.status !== "pending") {
77
- result.blocked++;
95
+ if (steps.every(({ step }) => step.result || step.status !== "pending")) {
96
+ const problems = steps.filter(({ step }) => !step.result).map(({ step }) => step.id);
97
+ result.blocked += problems.length;
98
+ result.flagged++;
99
+ store.flagReview(example, "generation-unresolved", { steps: problems });
78
100
  continue;
79
101
  }
80
102
  if (options.maxExamples !== undefined && result.examples >= options.maxExamples)
81
103
  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
104
  result.examples++;
88
- result.inputBytes += bytes;
89
- if (options.dryRun)
90
- continue;
91
- teacher ??= trainingTeacher(runtime, source.agentId); // Capability check BEFORE committing a paid attempt.
92
- if (!await checkpoint(store, step, result, () => teacher(request.input)))
93
- return;
105
+ const problems = [];
106
+ for (const { lane, step } of steps) {
107
+ if (step.result) {
108
+ result.cached++;
109
+ continue;
110
+ }
111
+ if (step.status !== "pending") {
112
+ result.blocked++;
113
+ problems.push(step.id);
114
+ continue;
115
+ }
116
+ const input = JSON.parse(example.inputJson);
117
+ const bytes = Buffer.byteLength(trainingTeacherMessage(input, lane)) + Buffer.byteLength(trainingTeacherPrompt(lane));
118
+ if (result.inputBytes + bytes > options.maxInputBytes) {
119
+ result.budgetLimited = true;
120
+ break;
121
+ }
122
+ result.inputBytes += bytes;
123
+ if (options.dryRun)
124
+ continue;
125
+ teacher ??= trainingTeacher(runtime, source.agentId);
126
+ if (!await checkpoint(store, step, result, () => teacher(input, lane)))
127
+ problems.push(step.id);
128
+ }
129
+ if (problems.length) {
130
+ store.flagReview(example, "generation-unresolved", { steps: problems });
131
+ result.flagged++;
132
+ }
94
133
  }
95
134
  catch (error) {
96
135
  stopped = true;
@@ -98,142 +137,250 @@ export async function generateTrainingQueries(source, store, runtime, options) {
98
137
  }
99
138
  }
100
139
  }));
101
- const failure = workers.find(item => item.status === "rejected");
102
- if (failure)
103
- throw failure.reason;
104
140
  return { refreshed, ...result, threshold: options.threshold ?? TRAINING_GATE_THRESHOLD, model: TRAINING_TEACHER_MODEL };
105
141
  }
142
+ /** Raw top-three average, including values below runtime's injection threshold. */
143
+ export function trainingQueryScore(probabilities) {
144
+ const top = probabilities.toSorted((a, b) => b - a).slice(0, 3);
145
+ return top.length ? top.reduce((sum, value) => sum + value, 0) / top.length : 0;
146
+ }
106
147
  export function selectTrainingQueries(queries) {
107
- // Stable ties preserve teacher order; neither low scores nor overlapping hits reject a query.
108
- return queries.toSorted((a, b) => b.score - a.score).slice(0, 3).map(q => q.query);
148
+ const winner = (lane) => {
149
+ const ranked = queries.filter(query => query.lane === lane).toSorted((a, b) => b.score - a.score);
150
+ if (!ranked[0])
151
+ throw new Error(`No resolved ${lane} candidates`);
152
+ return ranked[0].query; // Stable ties preserve round and teacher order.
153
+ };
154
+ return { lex: winner("lex"), vec: winner("vec") };
109
155
  }
110
- export async function evaluateTrainingQueries(source, store, config, options, createSearch = historicalTrainingSearch) {
156
+ export async function evaluateTrainingQueries(source, store, config, runtime, options, createSearch = historicalTrainingSearch) {
111
157
  bounds(options);
112
- if (options.excludeJudgments?.some(id => !/^[a-f0-9]{64}$/u.test(id)))
113
- throw new Error("Invalid excluded judgment hash");
114
158
  const corpus = config.corpora.find(c => c.kind === "sessions");
115
159
  if (!corpus)
116
160
  throw new Error("Training retrieval requires a configured sessions corpus");
117
161
  const refreshed = collectTraining(source, store, { existingOnly: true });
118
162
  const result = { ...summary(), retrievals: 0, evaluated: 0, awaitingTeacher: 0 };
119
163
  const concurrency = options.concurrency ?? TRAINING_EVALUATION_CONCURRENCY;
120
- let key;
164
+ let key, teacher;
121
165
  const apiKey = () => key ??= resolveTypeSafeApiKey(config.typesafe).then(value => {
122
166
  if (!value)
123
167
  throw new Error("TypeSafe is disabled or its credential is unavailable");
124
168
  return value;
125
169
  });
126
170
  let stopped = false;
127
- const shouldStop = () => stopped || result.budgetLimited || result.failed > 0 || result.ambiguous > 0 || result.blocked > 0;
171
+ const shouldStop = () => stopped || result.budgetLimited;
172
+ const room = () => {
173
+ if (shouldStop())
174
+ return false;
175
+ if (options.maxCalls !== undefined && result.calls >= options.maxCalls) {
176
+ result.budgetLimited = true;
177
+ return false;
178
+ }
179
+ return true;
180
+ };
128
181
  const judgments = new Map();
182
+ const revisions = new Map();
183
+ const reviews = new Map(store.reviews().map(review => [review.sourceId, review]));
129
184
  const evaluate = async (example) => {
130
- const teacher = store.step("generate", teacherRequest(example));
131
- if (!teacher.result) {
132
- result.awaitingTeacher++;
185
+ const previousReview = reviews.get(example.id);
186
+ if (previousReview && "steps" in previousReview.details) {
187
+ const blocked = previousReview.details.steps.filter(id => {
188
+ const status = store.stepRecordStatus(id);
189
+ return status !== undefined && status !== "pending" && status !== "complete";
190
+ });
191
+ // A known failed example cannot resolve before manual retry. Do not scan its
192
+ // historical corpus or consume the example budget on every invocation.
193
+ if (blocked.length) {
194
+ result.blocked += blocked.length;
195
+ result.flagged++;
196
+ return;
197
+ }
198
+ }
199
+ const initial = LANES.map(lane => ({ lane, step: store.step("generate", teacherRequest(example, lane)) }));
200
+ if (initial.some(({ step }) => !step.result)) {
201
+ const blocked = initial.filter(({ step }) => !step.result && step.status !== "pending");
202
+ if (blocked.length) {
203
+ result.blocked += blocked.length;
204
+ result.flagged++;
205
+ store.flagReview(example, "generation-unresolved", { steps: blocked.map(({ step }) => step.id) });
206
+ }
207
+ else
208
+ result.awaitingTeacher++;
133
209
  return;
134
210
  }
135
211
  if (options.maxExamples !== undefined && result.examples >= options.maxExamples)
136
212
  return;
137
213
  store.renew();
138
- const snapshot = await createSearch(source.stateDir, corpus.chatTypes, example.timestamp);
214
+ let snapshot;
215
+ try {
216
+ snapshot = await createSearch(source.stateDir, corpus.chatTypes, example.timestamp);
217
+ }
218
+ catch (error) {
219
+ if (!(error instanceof HistoricalCorpusUnavailableError))
220
+ throw error;
221
+ store.flagReview(example, "historical-snapshot-unavailable", { timestamp: example.timestamp });
222
+ result.flagged++;
223
+ return;
224
+ }
139
225
  try {
140
226
  const evaluation = store.step("evaluate", { version: SELECTION_VERSION, sourceId: example.id, inputHash: example.inputHash,
141
- timestamp: example.timestamp, teacherId: teacher.id, corpusHash: snapshot.corpusHash,
227
+ timestamp: example.timestamp, teacherIds: initial.map(({ step }) => step.id), corpusHash: snapshot.corpusHash,
142
228
  retrievalVersion: TRAINING_RETRIEVAL_VERSION, judgeVersion: CONTEXT_JUDGE_VERSION,
143
- exclusions: options.excludeJudgments?.toSorted() ?? [], options: TRAINING_SEARCH_OPTIONS });
229
+ minUsefulness: config.memoryWhisperer.minUsefulness, options: TRAINING_SEARCH_OPTIONS });
144
230
  if (evaluation.result) {
145
231
  result.cached++;
232
+ if (evaluation.result.review.length) {
233
+ result.flagged++;
234
+ store.flagReview(example, "no-useful-evidence", { lanes: evaluation.result.review, evaluationId: evaluation.id });
235
+ }
236
+ else
237
+ store.clearReview(example.id);
146
238
  return;
147
239
  }
148
240
  if (evaluation.status !== "pending") {
149
241
  result.blocked++;
242
+ result.flagged++;
243
+ store.flagReview(example, "evaluation-unresolved", { steps: [evaluation.id] });
150
244
  return;
151
245
  }
152
- // Snapshot creation is async: reserve the shared example budget only after it settles.
153
246
  if (shouldStop() || (options.maxExamples !== undefined && result.examples >= options.maxExamples))
154
247
  return;
155
248
  result.examples++;
156
249
  if (options.dryRun)
157
250
  return;
158
251
  const input = JSON.parse(example.inputJson);
252
+ const problems = new Set();
159
253
  const judge = (hit) => {
160
254
  const request = contextJudgeRequest(input, snapshot.maxDate, hit);
161
- // Same identity as the experiment: duplicate query memberships share one judgment.
162
- const identity = trainingHash([CONTEXT_JUDGE_VERSION, example.inputHash, hit.position, request]);
255
+ const identity = trainingHash([CONTEXT_JUDGE_VERSION, request]);
163
256
  let pending = judgments.get(identity);
164
257
  if (!pending) {
165
258
  pending = (async () => {
166
- const excluded = options.excludeJudgments?.includes(identity) || store.judgmentExcluded(identity);
167
- const step = store.step("judge", { identity, request, excluded });
259
+ const step = store.step("judge", { identity, request });
168
260
  if (step.result) {
169
261
  result.cached++;
170
262
  return { id: step.id, result: step.result };
171
263
  }
172
- if (excluded) {
173
- const attempt = store.startStep("judge", step.id, step.request);
174
- const value = { excluded: true, reason: "operator-exclusion" };
175
- store.finishStep("judge", step.id, attempt, { result: value });
176
- return { id: step.id, result: value };
264
+ if (step.status !== "pending") {
265
+ result.blocked++;
266
+ return { id: step.id, result: undefined };
177
267
  }
178
268
  const credential = await apiKey();
179
- if (shouldStop())
269
+ if (!room())
180
270
  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
271
  return { id: step.id, result: await checkpoint(store, step, result, () => judgeTrainingPassage(request, credential)) };
186
272
  })();
187
273
  judgments.set(identity, pending);
188
274
  }
189
275
  return pending;
190
276
  };
191
- // Keep teacher order, and drain every in-flight operation before closing its snapshot.
192
- const settled = await Promise.allSettled(teacher.result.queries.map(async (query) => {
277
+ const scoreQueries = async (lane, round, generation, generationId) => await drain(generation.queries.map(async (query) => {
193
278
  try {
194
- if (shouldStop())
279
+ const score = store.step("score", { version: SELECTION_VERSION, lane, round, generationId, query,
280
+ sourceId: example.id, corpusHash: snapshot.corpusHash, judgeVersion: CONTEXT_JUDGE_VERSION,
281
+ retrievalVersion: TRAINING_RETRIEVAL_VERSION });
282
+ if (score.result) {
283
+ result.cached++;
284
+ return score.result;
285
+ }
286
+ if (score.status !== "pending") {
287
+ problems.add(score.id);
288
+ result.blocked++;
195
289
  return;
196
- const retrieval = store.step("retrieve", { version: TRAINING_RETRIEVAL_VERSION, query,
290
+ }
291
+ const retrieval = store.step("retrieve", { version: TRAINING_RETRIEVAL_VERSION, query, lane,
197
292
  sourceId: example.id, maxDate: snapshot.maxDate, corpusHash: snapshot.corpusHash, options: TRAINING_SEARCH_OPTIONS });
198
- if (shouldStop())
293
+ if (!retrieval.result && retrieval.status === "pending" && !room())
199
294
  return;
200
- // No await between this check and checkpoint's synchronous reservation of result.calls.
201
- if (!retrieval.result && options.maxCalls !== undefined && result.calls >= options.maxCalls) {
202
- result.budgetLimited = true;
295
+ const retrieved = await checkpoint(store, retrieval, result, async () => ({ query, lane, maxDate: snapshot.maxDate,
296
+ corpusHash: snapshot.corpusHash, hits: await snapshot.search(query, lane) }));
297
+ if (!retrieved) {
298
+ problems.add(retrieval.id);
203
299
  return;
204
300
  }
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
301
  if (!retrieval.result)
210
302
  result.retrievals++;
211
- const settledHits = await Promise.allSettled(retrieved.hits.map(judge));
212
- const failure = settledHits.find(item => item.status === "rejected");
213
- if (failure)
214
- throw failure.reason;
215
- const scored = settledHits.flatMap(item => item.status === "fulfilled" ? [item.value] : []);
216
- if (scored.some(item => !item.result))
303
+ const scored = await drain(retrieved.hits.map(judge));
304
+ const missing = scored.filter(item => !item.result);
305
+ if (missing.length) {
306
+ for (const item of missing) {
307
+ const record = store.stepRecordStatus(item.id);
308
+ if (record && record !== "pending")
309
+ problems.add(item.id);
310
+ }
217
311
  return;
218
- const scores = scored.flatMap(item => item.result && "score" in item.result ? [item.result.score] : []);
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 })) };
312
+ }
313
+ const probabilities = scored.map(item => item.result.probability);
314
+ return save(store, score, { query, lane, round, retrievalId: retrieval.id,
315
+ score: trainingQueryScore(probabilities), maxProbability: Math.max(0, ...probabilities), judgments: scored.map(item => item.id) });
221
316
  }
222
317
  catch (error) {
223
318
  stopped = true;
224
319
  throw error;
225
320
  }
226
321
  }));
227
- const failure = settled.find(item => item.status === "rejected");
228
- if (failure)
229
- throw failure.reason;
230
- const queries = settled.flatMap(item => item.status === "fulfilled" && item.value ? [item.value] : []);
231
- if (queries.length !== 10)
232
- return; // Resume only unfinished queries; never repeat completed calls.
233
- const selected = selectTrainingQueries(queries);
234
- const attempt = store.startStep("evaluate", evaluation.id, evaluation.request);
235
- store.finishStep("evaluate", evaluation.id, attempt, { result: { sourceId: example.id, inputHash: example.inputHash,
236
- timestamp: example.timestamp, corpusHash: snapshot.corpusHash, corpusReport: snapshot.report, teacherId: teacher.id, queries, selected } });
322
+ const lanes = await drain(initial.map(async ({ lane, step }) => {
323
+ try {
324
+ const first = await scoreQueries(lane, 1, step.result, step.id);
325
+ if (first.some(item => !item) || shouldStop())
326
+ return;
327
+ const feedback = first.map(item => ({ query: item.query, score: item.score }));
328
+ // The only feedback sent to Luna is this lane's exact earlier queries and scores.
329
+ const revision = store.step("generate", teacherRequest(example, lane, feedback));
330
+ let pending = revisions.get(revision.id);
331
+ if (!pending) {
332
+ pending = (async () => {
333
+ if (!revision.result && revision.status === "pending") {
334
+ const bytes = Buffer.byteLength(trainingTeacherMessage(input, lane, feedback)) + Buffer.byteLength(trainingTeacherPrompt(lane));
335
+ if (!room())
336
+ return;
337
+ if (result.inputBytes + bytes > (options.maxInputBytes ?? 3_000_000)) {
338
+ result.budgetLimited = true;
339
+ return;
340
+ }
341
+ result.inputBytes += bytes;
342
+ teacher ??= trainingTeacher(runtime, source.agentId);
343
+ }
344
+ return checkpoint(store, revision, result, () => teacher(input, lane, feedback));
345
+ })();
346
+ revisions.set(revision.id, pending);
347
+ }
348
+ const revised = await pending;
349
+ if (!revised) {
350
+ if (store.stepRecordStatus(revision.id))
351
+ problems.add(revision.id);
352
+ return;
353
+ }
354
+ const second = await scoreQueries(lane, 2, revised, revision.id);
355
+ if (second.some(item => !item))
356
+ return;
357
+ return { teacherIds: [step.id, revision.id], queries: [...first, ...second].filter((item) => !!item) };
358
+ }
359
+ catch (error) {
360
+ stopped = true;
361
+ throw error;
362
+ }
363
+ }));
364
+ if (problems.size) {
365
+ result.flagged++;
366
+ store.flagReview(example, "evaluation-unresolved", { steps: [...problems] });
367
+ return;
368
+ }
369
+ if (lanes.some(lane => !lane))
370
+ return; // Budget pause: completed operations remain reusable.
371
+ const queries = lanes.flatMap(lane => lane.queries);
372
+ const review = LANES.filter(lane => !queries.some(query => query.lane === lane && query.judgments.length > 0 &&
373
+ query.maxProbability >= config.memoryWhisperer.minUsefulness))
374
+ .map(lane => `${lane}-no-useful-evidence`);
375
+ const selected = review.length ? null : selectTrainingQueries(queries);
376
+ save(store, evaluation, { sourceId: example.id, inputHash: example.inputHash, timestamp: example.timestamp,
377
+ corpusHash: snapshot.corpusHash, corpusReport: snapshot.report, teacherIds: lanes.flatMap(lane => lane.teacherIds), queries, selected, review });
378
+ if (review.length) {
379
+ store.flagReview(example, "no-useful-evidence", { lanes: review, evaluationId: evaluation.id });
380
+ result.flagged++;
381
+ }
382
+ else
383
+ store.clearReview(example.id);
237
384
  result.evaluated++;
238
385
  }
239
386
  finally {
@@ -242,7 +389,7 @@ export async function evaluateTrainingQueries(source, store, config, options, cr
242
389
  };
243
390
  const examples = store.queryExamples(options.threshold);
244
391
  let next = 0;
245
- const workers = await Promise.allSettled(Array.from({ length: Math.min(concurrency, examples.length) }, async () => {
392
+ await drain(Array.from({ length: Math.min(concurrency, examples.length) }, async () => {
246
393
  while (!shouldStop() && (options.maxExamples === undefined || result.examples < options.maxExamples)) {
247
394
  const example = examples[next++];
248
395
  if (!example)
@@ -256,27 +403,25 @@ export async function evaluateTrainingQueries(source, store, config, options, cr
256
403
  }
257
404
  }
258
405
  }));
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
406
  return { refreshed, ...result, concurrency, threshold: options.threshold ?? TRAINING_GATE_THRESHOLD,
263
- retrievalMethod: "vector10-bm25-10", callUnit: "retrieval operation or uncached passage judgment" };
407
+ retrievalMethod: "independent-lex10-vec10", callUnit: "teacher completion, retrieval operation or uncached passage judgment" };
264
408
  }
265
409
  export function* exportQueryTraining(store, threshold = TRAINING_GATE_THRESHOLD) {
266
410
  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 })) {
411
+ const unresolved = new Set(store.reviews().map(review => review.sourceId)), exported = new Set();
412
+ for (const evaluation of store.completedEvaluations({ selection: SELECTION_VERSION, retrieval: TRAINING_RETRIEVAL_VERSION, judge: CONTEXT_JUDGE_VERSION })) {
269
413
  const source = active.get(evaluation.sourceId);
270
414
  if (!source || source.inputHash !== evaluation.inputHash || source.timestamp !== evaluation.timestamp || exported.has(source.id))
271
415
  continue;
272
- if (store.step("generate", teacherRequest(source)).id !== evaluation.teacherId)
416
+ exported.add(source.id); // Never fall back to an older accepted corpus after a newer flagged result.
417
+ if (!evaluation.selected || unresolved.has(source.id))
418
+ continue;
419
+ if (!LANES.every(lane => evaluation.teacherIds.includes(store.step("generate", teacherRequest(source, lane)).id)))
273
420
  continue;
274
- exported.add(source.id); // Only the newest corpus evaluation per source.
275
- const provenance = [evaluation.teacherId, ...evaluation.queries.flatMap(q => [q.retrievalId, ...q.judgments?.map(j => j.id) ?? []])];
421
+ const provenance = [...evaluation.teacherIds, ...evaluation.queries.flatMap(q => [q.retrievalId, ...q.judgments])];
276
422
  yield { stage: "query-training", input: JSON.parse(source.inputJson), inputHash: source.inputHash,
277
- recallProbability: source.recallProbability, threshold,
278
- target: evaluation.selected, source: store.sourceDetails(source.id), evaluation,
279
- splitGroup: trainingHash(source.sessionId), // Also keep identical inputHash groups together across sessions/nodes.
423
+ recallProbability: source.recallProbability, threshold, target: evaluation.selected,
424
+ source: store.sourceDetails(source.id), evaluation, splitGroup: trainingHash(source.sessionId),
280
425
  provenance: [...new Set(provenance)].map(id => store.stepRecord(id)) };
281
426
  }
282
427
  }
@@ -1,6 +1,7 @@
1
1
  import type { ChatType } from "./config.js";
2
2
  import type { SessionMessageSpan } from "./session-projector.js";
3
- export declare const TRAINING_RETRIEVAL_VERSION = "qmd-2.10.1-historical-prefix-depth10-v2";
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
  }>;