@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,13 +1,17 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync } from "node:fs";
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
- export const TRAINING_RETRIEVAL_VERSION = "qmd-2.10.1-historical-prefix-depth10-v2";
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
- if (!existsSync(indexPath))
51
- throw new Error("No QMD index; sync sessions before evaluating training queries");
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 Error("Historical snapshot has no vectors; refusing a BM25-only evaluation");
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, `Historical request made at ${maxDate}. Current, now and latest refer to that timestamp.`);
201
- return hits.map(hit => trainingHit(hit, metadata, cutoff));
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 spans = metadata.get(hit.file)?.filter(s => s.start < hit.bestChunkPos + hit.bestChunk.length && s.end > hit.bestChunkPos);
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
- return { path: hit.file, position: hit.bestChunkPos, text: hit.bestChunk, dates: [...new Set(spans.map(s => s.timestamp))],
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
  }
@@ -66,7 +66,7 @@ export function registerMemoryTraining(api, config) {
66
66
  process.exitCode = 1;
67
67
  });
68
68
  for (const command of ["generate", "evaluate"]) {
69
- const stage = root.command(command).description(command === "generate" ? "Generate ten exact queries with isolated gpt-6-luna" : "Rank queries using existing historical QMD results")
69
+ const stage = root.command(command).description(command === "generate" ? "Generate five candidates per lex/vec lane with isolated gpt-6-luna" : "Grade, revise once per lane and select independent lex/vec winners")
70
70
  .option("--agent <id>", "Agent id", "main")
71
71
  .option("--threshold <p>", "Minimum completed recall probability", String(TRAINING_GATE_THRESHOLD))
72
72
  .option("--max-examples <n>", "Optional maximum uncached examples")
@@ -75,18 +75,18 @@ export function registerMemoryTraining(api, config) {
75
75
  stage.option("--max-input-bytes <n>", "Teacher input byte budget", "3000000")
76
76
  .option("--concurrency <n>", "Concurrent isolated teacher completions", "8");
77
77
  else
78
- stage.option("--max-calls <n>", "Maximum new retrieval operations plus uncached passage judgments")
79
- .option("--exclude-judgment <hash...>", "Explicitly exclude exact judgment hashes; persist exclusions, never score as zero")
78
+ stage.option("--max-calls <n>", "Maximum new teacher completions, retrieval operations and passage judgments")
79
+ .option("--max-input-bytes <n>", "Revision teacher input byte budget", "3000000")
80
80
  .option("--concurrency <n>", "Concurrent historical inputs; remote passage judgments run concurrently", String(TRAINING_EVALUATION_CONCURRENCY));
81
81
  stage.action(async (opts) => {
82
82
  const options = { maxExamples: opts.maxExamples === undefined ? undefined : Number(opts.maxExamples),
83
83
  dryRun: opts.dryRun, threshold: thresholdOption(opts.threshold) };
84
84
  const result = await withStore(opts.agent, {}, async (store, source) => command === "generate"
85
85
  ? await generateTrainingQueries(source, store, api.runtime, { ...options, maxInputBytes: Number(opts.maxInputBytes), concurrency: Number(opts.concurrency) })
86
- : await evaluateTrainingQueries(source, store, config, { ...options, maxCalls: opts.maxCalls === undefined ? undefined : Number(opts.maxCalls),
87
- concurrency: Number(opts.concurrency), excludeJudgments: opts.excludeJudgment }));
86
+ : await evaluateTrainingQueries(source, store, config, api.runtime, { ...options, maxCalls: opts.maxCalls === undefined ? undefined : Number(opts.maxCalls),
87
+ concurrency: Number(opts.concurrency), maxInputBytes: Number(opts.maxInputBytes) }));
88
88
  console.log(JSON.stringify(result, null, 2));
89
- if (result.failed || result.ambiguous || result.blocked)
89
+ if (result.failed || result.ambiguous || result.blocked || result.flagged)
90
90
  process.exitCode = 1;
91
91
  });
92
92
  }
@@ -97,9 +97,10 @@ export function registerMemoryTraining(api, config) {
97
97
  console.log(JSON.stringify(await withStore(opts.agent, { readOnly: true }, store => store.status(threshold)), null, 2));
98
98
  });
99
99
  root.command("retry-failed").option("--agent <id>", "Agent id", "main")
100
+ .requiredOption("--id <hash...>", "Exact reviewed recall-gate or query-step hashes from status")
100
101
  .option("--include-ambiguous", "Explicitly permit retrying requests that may already have been billed")
101
102
  .action(async (opts) => {
102
- const reset = await withStore(opts.agent, {}, store => store.retry(opts.includeAmbiguous === true));
103
+ const reset = await withStore(opts.agent, {}, store => store.retry(opts.includeAmbiguous === true, opts.id));
103
104
  console.log(JSON.stringify({ reset, calls: 0 }));
104
105
  });
105
106
  root.command("export").option("--agent <id>", "Agent id", "main")
@@ -1,8 +1,9 @@
1
1
  import { type TrainingExample, type TrainingInput } from "./training-input.js";
2
2
  import { type judgeTrainingInput } from "./training-gate.js";
3
- import type { TeacherResult } from "./training-models.js";
3
+ import { type TeacherResult, type TrainingRound } from "./training-models.js";
4
4
  import type { TrainingHit } from "./training-retrieval.js";
5
- import type { parseContextJudgment } from "./training-judge.js";
5
+ import type { judgeTrainingPassage } from "./training-judge.js";
6
+ import type { QueryLane, QueryPair } from "./query-contract.js";
6
7
  type GateResult = Awaited<ReturnType<typeof judgeTrainingInput>>;
7
8
  type Job = {
8
9
  id: string;
@@ -18,19 +19,19 @@ export type TrainingSourceExample = {
18
19
  };
19
20
  export type QueryEvaluation = {
20
21
  query: string;
22
+ lane: QueryLane;
23
+ round: TrainingRound;
21
24
  retrievalId: string;
22
25
  score: number;
23
- judgments?: {
24
- id: string;
25
- excluded: boolean;
26
- }[];
26
+ maxProbability: number;
27
+ judgments: string[];
27
28
  };
28
29
  type TrainingEvaluation = {
29
30
  sourceId: string;
30
31
  inputHash: string;
31
32
  timestamp: number;
32
33
  corpusHash: string;
33
- teacherId: string;
34
+ teacherIds: string[];
34
35
  corpusReport: {
35
36
  sessions: number;
36
37
  chunks: number;
@@ -39,23 +40,31 @@ type TrainingEvaluation = {
39
40
  excludedChunks: number;
40
41
  };
41
42
  queries: QueryEvaluation[];
42
- selected: string[];
43
+ selected: QueryPair | null;
44
+ review: string[];
43
45
  };
44
46
  export type TrainingStepResults = {
45
47
  generate: TeacherResult;
46
48
  retrieve: {
47
49
  query: string;
50
+ lane: QueryLane;
48
51
  maxDate: string;
49
52
  corpusHash: string;
50
53
  hits: TrainingHit[];
51
54
  };
52
- judge: ReturnType<typeof parseContextJudgment> | {
53
- excluded: true;
54
- reason: "operator-exclusion";
55
- };
55
+ judge: Awaited<ReturnType<typeof judgeTrainingPassage>>;
56
+ score: QueryEvaluation;
56
57
  evaluate: TrainingEvaluation;
57
58
  };
58
59
  type StepStatus = "pending" | "attempted" | "complete" | "failed" | "ambiguous";
60
+ type ReviewDetails = {
61
+ steps: string[];
62
+ } | {
63
+ lanes: string[];
64
+ evaluationId: string;
65
+ } | {
66
+ timestamp: number;
67
+ };
59
68
  export declare class TrainingStore {
60
69
  #private;
61
70
  constructor(path: string, agentId: string);
@@ -75,7 +84,7 @@ export declare class TrainingStore {
75
84
  status: "failed" | "ambiguous";
76
85
  error: string;
77
86
  }): void;
78
- retry(includeAmbiguous: boolean): number;
87
+ retry(includeAmbiguous: boolean, ids: readonly string[]): number;
79
88
  activeExamples(): TrainingSourceExample[];
80
89
  queryExamples(threshold?: number): {
81
90
  recallProbability: number;
@@ -85,14 +94,24 @@ export declare class TrainingStore {
85
94
  sessionId: string;
86
95
  timestamp: number;
87
96
  }[];
88
- step<S extends keyof TrainingStepResults>(stage: S, request: unknown): {
97
+ step<S extends keyof TrainingStepResults>(stage: S, parameters: Record<string, unknown>): {
89
98
  id: string;
90
99
  stage: S;
91
- request: unknown;
100
+ request: {
101
+ recipe: string;
102
+ };
92
103
  status: StepStatus;
93
104
  result: TrainingStepResults[S] | undefined;
94
105
  };
95
- judgmentExcluded(identity: string): boolean;
106
+ flagReview(example: TrainingSourceExample, reason: string, details: ReviewDetails): void;
107
+ clearReview(sourceId: string): void;
108
+ reviews(): {
109
+ sourceId: string;
110
+ inputHash: string;
111
+ reason: string;
112
+ details: ReviewDetails;
113
+ updatedAt: number;
114
+ }[];
96
115
  startStep(stage: keyof TrainingStepResults, id: string, request: unknown): number;
97
116
  finishStep<S extends keyof TrainingStepResults>(stage: S, id: string, attempt: number, outcome: {
98
117
  result: TrainingStepResults[S];
@@ -103,6 +122,7 @@ export declare class TrainingStore {
103
122
  completedEvaluations(versions?: {
104
123
  selection: string;
105
124
  retrieval: string;
125
+ judge: string;
106
126
  }): TrainingEvaluation[];
107
127
  sourceDetails(id: string): {
108
128
  nodeId: string;
@@ -116,6 +136,7 @@ export declare class TrainingStore {
116
136
  result: unknown;
117
137
  completedAt: import("node:sqlite").SQLOutputValue;
118
138
  };
139
+ stepRecordStatus(id: string): StepStatus | undefined;
119
140
  status(threshold: number): {
120
141
  nodeId: string;
121
142
  agentId: string;
@@ -132,6 +153,15 @@ export declare class TrainingStore {
132
153
  inputTokens: number;
133
154
  outputTokens: number;
134
155
  negative: number;
156
+ recipe: string;
157
+ reviews: {
158
+ sourceId: string;
159
+ inputHash: string;
160
+ reason: string;
161
+ details: ReviewDetails;
162
+ updatedAt: number;
163
+ }[];
164
+ retryable: Record<string, import("node:sqlite").SQLOutputValue>[];
135
165
  queryStages: Record<string, import("node:sqlite").SQLOutputValue>[];
136
166
  queryAttempts: Record<string, import("node:sqlite").SQLOutputValue>[];
137
167
  attempts: Record<string, import("node:sqlite").SQLOutputValue>[];
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { openMemoryDatabase } from "./memory-database.js";
3
3
  import { trainingHash, TRAINING_PREPARATION } from "./training-input.js";
4
4
  import { TRAINING_GATE_MODEL, TRAINING_GATE_THRESHOLD, TRAINING_GATE_VERSION } from "./training-gate.js";
5
+ import { TRAINING_RECIPE_VERSION } from "./training-models.js";
5
6
  const LEASE_MS = 120_000;
6
7
  export class TrainingStore {
7
8
  #db;
@@ -43,8 +44,10 @@ export class TrainingStore {
43
44
  status TEXT NOT NULL CHECK(status IN ('pending','attempted','complete','failed','ambiguous')),
44
45
  result_json TEXT, error TEXT, completed_at INTEGER
45
46
  ) STRICT;
46
- CREATE INDEX IF NOT EXISTS training_judgment_exclusions ON training_steps(json_extract(request_json,'$.identity'))
47
- WHERE stage='judge' AND status='complete' AND json_extract(result_json,'$.excluded')=1;
47
+ CREATE TABLE IF NOT EXISTS training_reviews (
48
+ source_id TEXT PRIMARY KEY REFERENCES training_examples(id), input_hash TEXT NOT NULL,
49
+ recipe TEXT NOT NULL, reason TEXT NOT NULL, details_json TEXT NOT NULL, updated_at INTEGER NOT NULL
50
+ ) STRICT;
48
51
  CREATE TABLE IF NOT EXISTS training_step_attempts (
49
52
  id INTEGER PRIMARY KEY, step_id TEXT NOT NULL REFERENCES training_steps(id), started_at INTEGER NOT NULL,
50
53
  finished_at INTEGER, status TEXT NOT NULL, error TEXT, result_json TEXT
@@ -87,10 +90,17 @@ export class TrainingStore {
87
90
  if (!lock.changes)
88
91
  throw new Error("Another memory-training command is running");
89
92
  // A request may have been billed before a crashed process saved the response.
90
- this.#db.exec(`UPDATE training_gates SET status='ambiguous',error='interrupted' WHERE status='attempted';
91
- UPDATE training_attempts SET status='ambiguous',error='interrupted' WHERE status='attempted';
92
- UPDATE training_steps SET status='ambiguous',error='interrupted' WHERE status='attempted';
93
- UPDATE training_step_attempts SET status='ambiguous',error='interrupted' WHERE status='attempted';`);
93
+ this.#db.prepare(`UPDATE training_gates SET status='ambiguous',error='interrupted' WHERE status='attempted'
94
+ AND prompt_version=? AND requested_model=? AND input_hash IN (SELECT hash FROM training_inputs WHERE preparation=?)`)
95
+ .run(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, TRAINING_PREPARATION);
96
+ this.#db.prepare(`UPDATE training_attempts SET status='ambiguous',error='interrupted' WHERE status='attempted'
97
+ AND gate_id IN (SELECT g.id FROM training_gates g JOIN training_inputs i ON i.hash=g.input_hash
98
+ WHERE g.prompt_version=? AND g.requested_model=? AND i.preparation=?)`)
99
+ .run(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, TRAINING_PREPARATION);
100
+ this.#db.prepare(`UPDATE training_steps SET status='ambiguous',error='interrupted' WHERE status='attempted'
101
+ AND json_extract(request_json,'$.recipe')=?`).run(TRAINING_RECIPE_VERSION);
102
+ this.#db.prepare(`UPDATE training_step_attempts SET status='ambiguous',error='interrupted' WHERE status='attempted'
103
+ AND step_id IN (SELECT id FROM training_steps WHERE json_extract(request_json,'$.recipe')=?)`).run(TRAINING_RECIPE_VERSION);
94
104
  });
95
105
  let leaseError;
96
106
  const heartbeat = setInterval(() => { try {
@@ -156,13 +166,14 @@ export class TrainingStore {
156
166
  this.#db.prepare("UPDATE training_examples SET active=-1 WHERE session_id=? AND active=1").run(sessionId);
157
167
  }
158
168
  #scope = `g.prompt_version=? AND g.requested_model=? AND EXISTS
159
- (SELECT 1 FROM training_examples e WHERE e.input_hash=g.input_hash AND e.active=1)`;
169
+ (SELECT 1 FROM training_examples e JOIN training_inputs i ON i.hash=e.input_hash
170
+ WHERE e.input_hash=g.input_hash AND e.active=1 AND i.preparation=?)`;
160
171
  pending(limit) {
161
172
  return this.#db.prepare(`SELECT g.id,g.input_hash inputHash,i.input_json inputJson
162
173
  FROM training_gates g JOIN training_inputs i ON i.hash=g.input_hash
163
174
  WHERE ${this.#scope} AND g.status='pending' ORDER BY
164
175
  (SELECT MAX(timestamp) FROM training_examples WHERE input_hash=g.input_hash AND active=1) DESC,g.id LIMIT ?`)
165
- .all(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, limit ?? -1);
176
+ .all(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, TRAINING_PREPARATION, limit ?? -1);
166
177
  }
167
178
  start(id) {
168
179
  return this.#transaction(() => {
@@ -185,35 +196,61 @@ export class TrainingStore {
185
196
  .run(status, Date.now(), error, ok ? JSON.stringify(result) : null, attempt, id);
186
197
  });
187
198
  }
188
- retry(includeAmbiguous) {
199
+ retry(includeAmbiguous, ids) {
200
+ if (!ids.length || ids.some(id => !/^[a-f0-9]{64}$/u.test(id)))
201
+ throw new Error("Specify exact training gate or step hashes to retry");
189
202
  this.#checkLease();
190
- return this.#transaction(() => ["training_gates", "training_steps"].reduce((n, table) => n + Number(this.#db.prepare(`UPDATE ${table} SET status='pending',error=NULL WHERE status='failed' ${includeAmbiguous ? "OR status='ambiguous'" : ""}`).run().changes), 0));
203
+ return this.#transaction(() => ids.reduce((count, id) => {
204
+ const statuses = includeAmbiguous ? "('failed','ambiguous')" : "('failed')";
205
+ const gates = this.#db.prepare(`UPDATE training_gates SET status='pending',error=NULL WHERE id=?
206
+ AND prompt_version=? AND requested_model=? AND status IN ${statuses}
207
+ AND input_hash IN (SELECT hash FROM training_inputs WHERE preparation=?)`)
208
+ .run(id, TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, TRAINING_PREPARATION);
209
+ const steps = this.#db.prepare(`UPDATE training_steps SET status='pending',error=NULL WHERE id=?
210
+ AND json_extract(request_json,'$.recipe')=? AND status IN ${statuses}`).run(id, TRAINING_RECIPE_VERSION);
211
+ return count + Number(gates.changes) + Number(steps.changes);
212
+ }, 0));
191
213
  }
192
214
  activeExamples() {
193
215
  return this.#db.prepare(`SELECT e.id,e.input_hash inputHash,i.input_json inputJson,e.session_id sessionId,e.timestamp
194
216
  FROM training_examples e JOIN training_inputs i ON i.hash=e.input_hash
195
- WHERE e.active=1 ORDER BY e.timestamp DESC,e.id`).all();
217
+ WHERE e.active=1 AND i.preparation=? ORDER BY e.timestamp DESC,e.id`).all(TRAINING_PREPARATION);
196
218
  }
197
219
  queryExamples(threshold = TRAINING_GATE_THRESHOLD) {
198
220
  if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1)
199
221
  throw new Error("Threshold must be between 0 and 1");
200
222
  const probabilities = new Map(this.#db.prepare(`SELECT g.input_hash,g.probability FROM training_gates g
201
223
  WHERE ${this.#scope} AND g.status='complete' AND g.probability>=?`)
202
- .all(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, threshold).map(row => [String(row.input_hash), Number(row.probability)]));
224
+ .all(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, TRAINING_PREPARATION, threshold).map(row => [String(row.input_hash), Number(row.probability)]));
203
225
  return this.activeExamples().flatMap(example => {
204
226
  const recallProbability = probabilities.get(example.inputHash);
205
227
  return recallProbability === undefined ? [] : [{ ...example, recallProbability }];
206
228
  });
207
229
  }
208
- step(stage, request) {
230
+ step(stage, parameters) {
231
+ const request = { ...parameters, recipe: TRAINING_RECIPE_VERSION };
209
232
  const id = trainingHash([stage, request]);
210
233
  const row = this.#db.prepare("SELECT status,result_json FROM training_steps WHERE id=? AND stage=?").get(id, stage);
211
234
  return { id, stage, request, status: (row?.status ?? "pending"),
212
235
  result: row?.status === "complete" ? JSON.parse(String(row.result_json)) : undefined };
213
236
  }
214
- judgmentExcluded(identity) {
215
- return !!this.#db.prepare(`SELECT 1 FROM training_steps WHERE stage='judge' AND status='complete'
216
- AND json_extract(request_json,'$.identity')=? AND json_extract(result_json,'$.excluded')=1 LIMIT 1`).get(identity);
237
+ flagReview(example, reason, details) {
238
+ this.#checkLease();
239
+ this.#db.prepare(`INSERT INTO training_reviews VALUES (?,?,?,?,?,?) ON CONFLICT(source_id) DO UPDATE SET
240
+ input_hash=excluded.input_hash,recipe=excluded.recipe,reason=excluded.reason,
241
+ details_json=excluded.details_json,updated_at=excluded.updated_at`)
242
+ .run(example.id, example.inputHash, TRAINING_RECIPE_VERSION, reason, JSON.stringify(details), Date.now());
243
+ }
244
+ clearReview(sourceId) {
245
+ this.#checkLease();
246
+ this.#db.prepare("DELETE FROM training_reviews WHERE source_id=? AND recipe=?").run(sourceId, TRAINING_RECIPE_VERSION);
247
+ }
248
+ reviews() {
249
+ return this.#db.prepare(`SELECT r.source_id sourceId,r.input_hash inputHash,r.reason,r.details_json details,r.updated_at updatedAt
250
+ FROM training_reviews r JOIN training_examples e ON e.id=r.source_id
251
+ WHERE r.recipe=? AND e.active=1 AND e.input_hash=r.input_hash ORDER BY r.updated_at DESC,r.source_id`)
252
+ .all(TRAINING_RECIPE_VERSION).map(row => ({ sourceId: String(row.sourceId), inputHash: String(row.inputHash),
253
+ reason: String(row.reason), details: JSON.parse(String(row.details)), updatedAt: Number(row.updatedAt) }));
217
254
  }
218
255
  startStep(stage, id, request) {
219
256
  return this.#transaction(() => {
@@ -242,8 +279,10 @@ export class TrainingStore {
242
279
  }
243
280
  completedEvaluations(versions) {
244
281
  return this.#db.prepare(`SELECT result_json FROM training_steps WHERE stage='evaluate' AND status='complete'
245
- AND (? IS NULL OR (json_extract(request_json,'$.version')=? AND json_extract(request_json,'$.retrievalVersion')=?)) ORDER BY completed_at DESC,id`)
246
- .all(versions?.selection ?? null, versions?.selection ?? null, versions?.retrieval ?? null)
282
+ AND json_extract(request_json,'$.recipe')=?
283
+ AND (? IS NULL OR (json_extract(request_json,'$.version')=? AND json_extract(request_json,'$.retrievalVersion')=?
284
+ AND json_extract(request_json,'$.judgeVersion')=?)) ORDER BY completed_at DESC,id`)
285
+ .all(TRAINING_RECIPE_VERSION, versions?.selection ?? null, versions?.selection ?? null, versions?.retrieval ?? null, versions?.judge ?? null)
247
286
  .map(row => JSON.parse(String(row.result_json)));
248
287
  }
249
288
  sourceDetails(id) {
@@ -260,34 +299,49 @@ export class TrainingStore {
260
299
  return { id, stage: row.stage, request: JSON.parse(String(row.request_json)),
261
300
  result: JSON.parse(String(row.result_json)), completedAt: row.completed_at };
262
301
  }
302
+ stepRecordStatus(id) {
303
+ return this.#db.prepare("SELECT status FROM training_steps WHERE id=? AND json_extract(request_json,'$.recipe')=?")
304
+ .get(id, TRAINING_RECIPE_VERSION)?.status;
305
+ }
263
306
  status(threshold) {
264
307
  const stages = this.#db.prepare(`SELECT g.status,COUNT(*) count FROM training_gates g WHERE ${this.#scope} GROUP BY g.status`)
265
- .all(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL);
308
+ .all(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, TRAINING_PREPARATION);
266
309
  const labels = this.#db.prepare(`SELECT COUNT(*) complete,COALESCE(SUM(g.probability>=?),0) positive,
267
310
  COALESCE(SUM(g.input_tokens),0) inputTokens,COALESCE(SUM(g.output_tokens),0) outputTokens
268
- FROM training_gates g WHERE ${this.#scope} AND g.status='complete'`).get(threshold, TRAINING_GATE_VERSION, TRAINING_GATE_MODEL);
311
+ FROM training_gates g WHERE ${this.#scope} AND g.status='complete'`).get(threshold, TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, TRAINING_PREPARATION);
269
312
  return { nodeId: this.#nodeId, agentId: this.#agentId, preparation: TRAINING_PREPARATION,
270
313
  promptVersion: TRAINING_GATE_VERSION, requestedModel: TRAINING_GATE_MODEL, threshold,
271
- examples: this.#db.prepare("SELECT active,COUNT(*) count FROM training_examples GROUP BY active").all(),
272
- collectedInputs: Number(this.#db.prepare("SELECT COUNT(DISTINCT input_hash) count FROM training_examples WHERE active=1").get().count),
314
+ examples: this.#db.prepare(`SELECT e.active,COUNT(*) count FROM training_examples e
315
+ JOIN training_inputs i ON i.hash=e.input_hash WHERE i.preparation=? GROUP BY e.active`).all(TRAINING_PREPARATION),
316
+ collectedInputs: Number(this.#db.prepare(`SELECT COUNT(DISTINCT e.input_hash) count FROM training_examples e
317
+ JOIN training_inputs i ON i.hash=e.input_hash WHERE e.active=1 AND i.preparation=?`).get(TRAINING_PREPARATION).count),
273
318
  queryInputs: Number(labels.positive),
274
319
  stages, complete: Number(labels.complete), positive: Number(labels.positive),
275
320
  inputTokens: Number(labels.inputTokens), outputTokens: Number(labels.outputTokens),
276
321
  negative: Number(labels.complete) - Number(labels.positive),
277
- queryStages: this.#db.prepare("SELECT stage,status,COUNT(*) count FROM training_steps GROUP BY stage,status ORDER BY stage,status").all(),
322
+ recipe: TRAINING_RECIPE_VERSION, reviews: this.reviews(),
323
+ retryable: this.#db.prepare(`SELECT id,stage,status,error FROM training_steps
324
+ WHERE json_extract(request_json,'$.recipe')=? AND status IN ('failed','ambiguous')
325
+ UNION ALL SELECT g.id,'recall',g.status,g.error FROM training_gates g WHERE ${this.#scope}
326
+ AND g.status IN ('failed','ambiguous') ORDER BY stage,id`)
327
+ .all(TRAINING_RECIPE_VERSION, TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, TRAINING_PREPARATION),
328
+ queryStages: this.#db.prepare(`SELECT stage,status,COUNT(*) count FROM training_steps
329
+ WHERE json_extract(request_json,'$.recipe')=? GROUP BY stage,status ORDER BY stage,status`).all(TRAINING_RECIPE_VERSION),
278
330
  queryAttempts: this.#db.prepare(`SELECT s.stage,a.status,COUNT(*) count,
279
331
  COUNT(json_extract(a.result_json,'$.usage.input_tokens')) usageReported,
280
332
  SUM(json_extract(a.result_json,'$.usage.input_tokens')) inputTokens,
281
333
  SUM(json_extract(a.result_json,'$.usage.output_tokens')) outputTokens
282
- FROM training_step_attempts a JOIN training_steps s ON s.id=a.step_id GROUP BY s.stage,a.status`).all(),
283
- attempts: this.#db.prepare(`SELECT status,COUNT(*) count,
284
- COALESCE(SUM(json_extract(result_json,'$.usage.input_tokens')),0) inputTokens,
285
- COALESCE(SUM(json_extract(result_json,'$.usage.output_tokens')),0) outputTokens
286
- FROM training_attempts GROUP BY status`).all() };
334
+ FROM training_step_attempts a JOIN training_steps s ON s.id=a.step_id
335
+ WHERE json_extract(s.request_json,'$.recipe')=? GROUP BY s.stage,a.status`).all(TRAINING_RECIPE_VERSION),
336
+ attempts: this.#db.prepare(`SELECT a.status,COUNT(*) count,
337
+ COALESCE(SUM(json_extract(a.result_json,'$.usage.input_tokens')),0) inputTokens,
338
+ COALESCE(SUM(json_extract(a.result_json,'$.usage.output_tokens')),0) outputTokens
339
+ FROM training_attempts a JOIN training_gates g ON g.id=a.gate_id WHERE ${this.#scope} GROUP BY a.status`)
340
+ .all(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, TRAINING_PREPARATION) };
287
341
  }
288
342
  *exportRows(threshold) {
289
343
  const rows = this.#db.prepare(`SELECT g.*,i.input_json FROM training_gates g JOIN training_inputs i ON i.hash=g.input_hash
290
- WHERE ${this.#scope} AND g.status='complete' ORDER BY g.id`).iterate(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL);
344
+ WHERE ${this.#scope} AND g.status='complete' ORDER BY g.id`).iterate(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, TRAINING_PREPARATION);
291
345
  for (const row of rows) {
292
346
  yield { stage: "recall-gate", inputHash: row.input_hash, preparation: TRAINING_PREPARATION,
293
347
  input: JSON.parse(String(row.input_json)), recallProbability: row.probability,
@@ -22,6 +22,11 @@ export declare function collectTraining(source: Source, store?: TrainingStore, o
22
22
  changed: number;
23
23
  unchanged: number;
24
24
  retired: number;
25
+ review: {
26
+ sessionId: string;
27
+ seq: number;
28
+ reason: string;
29
+ }[];
25
30
  };
26
31
  /** Parallel, bounded paid work. Negative results are just as cacheable as positives. */
27
32
  export declare function runTraining(source: Source, store: TrainingStore, config: UnblockMemoryConfig, options: {
@@ -43,6 +48,11 @@ export declare function runTraining(source: Source, store: TrainingStore, config
43
48
  changed: number;
44
49
  unchanged: number;
45
50
  retired: number;
51
+ review: {
52
+ sessionId: string;
53
+ seq: number;
54
+ reason: string;
55
+ }[];
46
56
  };
47
57
  pendingSelected: number;
48
58
  calls: number;
@@ -6,12 +6,13 @@ export function collectTraining(source, store, options = {}) {
6
6
  const since = options.since ?? 0, until = options.until ?? Number.MAX_SAFE_INTEGER;
7
7
  const reader = new TrainingTranscriptReader(source.databasePath, source.agentId);
8
8
  const result = { sessions: 0, excludedSessions: 0, oversizedSessions: 0, eligible: 0,
9
- users: 0, filtered: 0, oversized: 0, unanswered: 0, added: 0, changed: 0, unchanged: 0, retired: 0 };
9
+ users: 0, filtered: 0, oversized: 0, unanswered: 0, added: 0, changed: 0, unchanged: 0, retired: 0,
10
+ review: [] };
10
11
  try {
11
12
  const sessions = new Set(options.existingOnly ? store?.sessions() : [...reader.sessions(), ...store?.sessions() ?? []]);
12
13
  for (const id of sessions) {
13
14
  store?.renew();
14
- const extracted = reader.read(id);
15
+ const extracted = reader.read(id, () => store?.renew());
15
16
  result.sessions++;
16
17
  if (extracted && "oversized" in extracted) {
17
18
  result.oversizedSessions++;
@@ -21,6 +22,7 @@ export function collectTraining(source, store, options = {}) {
21
22
  if (!extracted)
22
23
  result.excludedSessions++;
23
24
  else {
25
+ result.review.push(...extracted.review.map(item => ({ sessionId: id, ...item })));
24
26
  for (const key of ["users", "filtered", "oversized", "unanswered"])
25
27
  result[key] += extracted.coverage[key];
26
28
  result.eligible += extracted.examples.filter(e => e.timestamp >= since && e.timestamp < until).length;
@@ -54,7 +56,7 @@ export async function runTraining(source, store, config, options) {
54
56
  throw new Error("TypeSafe is disabled or its credential is unavailable");
55
57
  let next = 0, stopped = false;
56
58
  const workers = await Promise.allSettled(Array.from({ length: Math.min(concurrency, jobs.length) }, async () => {
57
- while (!stopped && !result.budgetLimited && !result.failed && !result.ambiguous) {
59
+ while (!stopped && !result.budgetLimited) {
58
60
  const job = jobs[next++];
59
61
  if (!job)
60
62
  return;
@@ -83,8 +85,8 @@ export async function runTraining(source, store, config, options) {
83
85
  store.finish(job.id, attempt, { status, error: error instanceof TypeSafeRequestError && error.code === "http_error" ?
84
86
  `http_${error.status}` : "request_or_response_uncertain" });
85
87
  result[status]++;
86
- // Stop on the first failure rather than spending the rest of the budget during an outage.
87
- return;
88
+ // Flag this input for operator review; do not stall untouched inputs or retry it.
89
+ continue;
88
90
  }
89
91
  // Storage failures must not be misclassified as provider failures/retried.
90
92
  store.finish(job.id, attempt, judgment);
@@ -1,4 +1,4 @@
1
- import type { memoryConversation } from "./whisperer-context.js";
1
+ import type { QueryConversation } from "./query-contract.js";
2
2
  /** Select from trusted candidates; never accept a provider-generated path or skill name. */
3
3
  export declare function selectTypeSafeSkill(params: {
4
4
  apiKey: string;
@@ -29,15 +29,65 @@ export declare function judgeTypeSafeQuality(params: {
29
29
  sourceKind: "files" | "sessions";
30
30
  }[];
31
31
  }): Promise<QualityJudgment[]>;
32
- /** One HTTP request per candidate, all launched together; result order matches input order. */
32
+ export declare const MEMORY_JUDGE_VERSION = "jev-1.13.0:memory-usefulness-v2";
33
+ export type MemoryPassage = {
34
+ excerpt: string;
35
+ corpus: string;
36
+ sourcePath: string;
37
+ dates: readonly string[];
38
+ };
39
+ /** The sole versioned passage prompt/state builder for both training and runtime. */
40
+ export declare function memoryUsefulnessRequest(conversation: QueryConversation, candidate: MemoryPassage, asOf: string): {
41
+ model: string;
42
+ state: {
43
+ conversation: {
44
+ history: {
45
+ role: "user" | "assistant";
46
+ content: string;
47
+ }[];
48
+ currentRequest: string;
49
+ };
50
+ asOf: string;
51
+ candidates: {
52
+ excerpt: string;
53
+ corpus: string;
54
+ sourcePath: string;
55
+ dates: readonly string[];
56
+ }[];
57
+ };
58
+ questions: {
59
+ memory_0: {
60
+ type: string;
61
+ instructions: string;
62
+ criteria: {
63
+ true: string;
64
+ false: string;
65
+ };
66
+ };
67
+ };
68
+ };
69
+ export declare function judgeMemoryPassage(request: ReturnType<typeof memoryUsefulnessRequest>, params: {
70
+ apiKey: string;
71
+ timeoutMs: number;
72
+ signal?: AbortSignal;
73
+ }): Promise<{
74
+ probability: number;
75
+ answer: {
76
+ type: "noul";
77
+ noul: number;
78
+ };
79
+ model: "jev-1.13.0";
80
+ usage: {
81
+ input_tokens: number;
82
+ output_tokens: number;
83
+ } | null;
84
+ }>;
85
+ /** One HTTP request per candidate; result order matches input order. */
33
86
  export declare function judgeTypeSafeMemories(params: {
34
87
  apiKey: string;
35
88
  timeoutMs: number;
36
89
  signal: AbortSignal;
37
- conversation: ReturnType<typeof memoryConversation>;
38
- candidates: readonly {
39
- excerpt: string;
40
- corpus: string;
41
- messageTimestamp?: string;
42
- }[];
90
+ conversation: QueryConversation;
91
+ asOf: string;
92
+ candidates: readonly MemoryPassage[];
43
93
  }): Promise<number[]>;