@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.
- package/README.md +2 -2
- package/dist/src/config.d.ts +0 -1
- package/dist/src/config.js +2 -6
- 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 +21 -17
- package/dist/src/mlx-query.d.ts +3 -10
- package/dist/src/mlx-query.js +49 -25
- package/dist/src/query-contract.d.ts +31 -0
- package/dist/src/query-contract.js +58 -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 +23 -7
- package/dist/src/training-queries.js +247 -102
- 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 +83 -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 +3 -3
- package/docs/memory-training.md +114 -58
- package/docs/retrieval.md +34 -24
- package/openclaw.plugin.json +2 -3
- package/package.json +5 -2
- 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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { UnblockMemoryConfig } from "./config.js";
|
|
2
|
-
import type {
|
|
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
|
-
|
|
47
|
-
export declare function
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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,
|
|
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";
|
|
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
|
|
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}` :
|
|
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
|
-
|
|
63
|
-
while (!stopped && !result.budgetLimited
|
|
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
|
|
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.
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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
|
|
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
|
|
131
|
-
if (
|
|
132
|
-
|
|
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
|
-
|
|
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,
|
|
227
|
+
timestamp: example.timestamp, teacherIds: initial.map(({ step }) => step.id), corpusHash: snapshot.corpusHash,
|
|
142
228
|
retrievalVersion: TRAINING_RETRIEVAL_VERSION, judgeVersion: CONTEXT_JUDGE_VERSION,
|
|
143
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
173
|
-
|
|
174
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
293
|
+
if (!retrieval.result && retrieval.status === "pending" && !room())
|
|
199
294
|
return;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
|
212
|
-
const
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}>;
|