@unblocklabs/unblock-memory 0.3.23 → 0.3.25

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 (46) hide show
  1. package/README.md +3 -0
  2. package/dist/src/config.js +2 -2
  3. package/dist/src/contracts.d.ts +5 -3
  4. package/dist/src/diagnostics.d.ts +31 -4
  5. package/dist/src/diagnostics.js +13 -3
  6. package/dist/src/manager.d.ts +27 -1
  7. package/dist/src/manager.js +42 -7
  8. package/dist/src/memory-whisperer.js +24 -10
  9. package/dist/src/plugin.js +21 -27
  10. package/dist/src/retrieval-telemetry.d.ts +39 -0
  11. package/dist/src/retrieval-telemetry.js +40 -0
  12. package/dist/src/session-projector.d.ts +32 -1
  13. package/dist/src/session-projector.js +84 -12
  14. package/dist/src/session-sync.d.ts +3 -2
  15. package/dist/src/session-sync.js +7 -5
  16. package/dist/src/training-candidates.d.ts +13 -0
  17. package/dist/src/training-candidates.js +75 -0
  18. package/dist/src/training-gate.d.ts +27 -0
  19. package/dist/src/training-gate.js +33 -0
  20. package/dist/src/training-input.d.ts +51 -0
  21. package/dist/src/training-input.js +199 -0
  22. package/dist/src/training-judge.d.ts +74 -0
  23. package/dist/src/training-judge.js +57 -0
  24. package/dist/src/training-models.d.ts +20 -0
  25. package/dist/src/training-models.js +72 -0
  26. package/dist/src/training-queries.d.ts +120 -0
  27. package/dist/src/training-queries.js +281 -0
  28. package/dist/src/training-retrieval.d.ts +37 -0
  29. package/dist/src/training-retrieval.js +176 -0
  30. package/dist/src/training-runtime.d.ts +4 -0
  31. package/dist/src/training-runtime.js +140 -0
  32. package/dist/src/training-store.d.ts +160 -0
  33. package/dist/src/training-store.js +300 -0
  34. package/dist/src/training.d.ts +57 -0
  35. package/dist/src/training.js +104 -0
  36. package/dist/src/typesafe-review.d.ts +1 -2
  37. package/dist/src/typesafe-review.js +3 -11
  38. package/dist/src/typesafe-transport.d.ts +10 -0
  39. package/dist/src/typesafe-transport.js +26 -0
  40. package/dist/src/typesafe.d.ts +1 -1
  41. package/dist/src/typesafe.js +27 -62
  42. package/docs/configuration.md +8 -7
  43. package/docs/memory-training.md +147 -0
  44. package/docs/retrieval.md +48 -17
  45. package/openclaw.plugin.json +4 -4
  46. package/package.json +3 -1
@@ -0,0 +1,300 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { openMemoryDatabase } from "./memory-database.js";
3
+ import { trainingHash, TRAINING_PREPARATION } from "./training-input.js";
4
+ import { TRAINING_GATE_MODEL, TRAINING_GATE_THRESHOLD, TRAINING_GATE_VERSION } from "./training-gate.js";
5
+ const LEASE_MS = 120_000;
6
+ export class TrainingStore {
7
+ #db;
8
+ #owner = randomUUID();
9
+ #nodeId;
10
+ #agentId;
11
+ constructor(path, agentId) {
12
+ this.#db = openMemoryDatabase(path);
13
+ this.#agentId = agentId;
14
+ try {
15
+ const version = this.#db.prepare("SELECT version FROM memory_schema WHERE component='training'").get()?.version;
16
+ if (version !== undefined && version !== 1)
17
+ throw new Error("Unsupported training database version");
18
+ this.#db.exec(`
19
+ CREATE TABLE IF NOT EXISTS training_identity (singleton INTEGER PRIMARY KEY CHECK(singleton=1), node_id TEXT NOT NULL, agent_id TEXT NOT NULL) STRICT;
20
+ CREATE TABLE IF NOT EXISTS training_lock (singleton INTEGER PRIMARY KEY CHECK(singleton=1), owner TEXT NOT NULL, expires INTEGER NOT NULL) STRICT;
21
+ CREATE TABLE IF NOT EXISTS training_inputs (hash TEXT PRIMARY KEY, preparation TEXT NOT NULL, input_json TEXT NOT NULL) STRICT;
22
+ CREATE TABLE IF NOT EXISTS training_examples (
23
+ id TEXT PRIMARY KEY, session_id TEXT NOT NULL, event_seq INTEGER NOT NULL, timestamp INTEGER NOT NULL,
24
+ input_hash TEXT NOT NULL REFERENCES training_inputs(hash), context_limited INTEGER NOT NULL,
25
+ active INTEGER NOT NULL, UNIQUE(session_id,event_seq)
26
+ ) STRICT;
27
+ CREATE TABLE IF NOT EXISTS training_gates (
28
+ id TEXT PRIMARY KEY, input_hash TEXT NOT NULL REFERENCES training_inputs(hash),
29
+ prompt_version TEXT NOT NULL, requested_model TEXT NOT NULL,
30
+ status TEXT NOT NULL CHECK(status IN ('pending','attempted','complete','failed','ambiguous')),
31
+ probability REAL, actual_model TEXT, input_tokens INTEGER, output_tokens INTEGER,
32
+ error TEXT, completed_at INTEGER,
33
+ UNIQUE(input_hash,prompt_version,requested_model)
34
+ ) STRICT;
35
+ CREATE TABLE IF NOT EXISTS training_attempts (
36
+ id INTEGER PRIMARY KEY, gate_id TEXT NOT NULL REFERENCES training_gates(id),
37
+ started_at INTEGER NOT NULL, finished_at INTEGER, status TEXT NOT NULL,
38
+ error TEXT, result_json TEXT
39
+ ) STRICT;
40
+ CREATE INDEX IF NOT EXISTS training_examples_input ON training_examples(input_hash,active);
41
+ CREATE TABLE IF NOT EXISTS training_steps (
42
+ id TEXT PRIMARY KEY, stage TEXT NOT NULL, request_json TEXT NOT NULL,
43
+ status TEXT NOT NULL CHECK(status IN ('pending','attempted','complete','failed','ambiguous')),
44
+ result_json TEXT, error TEXT, completed_at INTEGER
45
+ ) STRICT;
46
+ CREATE TABLE IF NOT EXISTS training_step_attempts (
47
+ id INTEGER PRIMARY KEY, step_id TEXT NOT NULL REFERENCES training_steps(id), started_at INTEGER NOT NULL,
48
+ finished_at INTEGER, status TEXT NOT NULL, error TEXT, result_json TEXT
49
+ ) STRICT;
50
+ INSERT OR IGNORE INTO memory_schema VALUES ('training',1);
51
+ `);
52
+ this.#db.prepare("INSERT OR IGNORE INTO training_identity VALUES (1,?,?)").run(randomUUID(), agentId);
53
+ const identity = this.#db.prepare("SELECT node_id,agent_id FROM training_identity WHERE singleton=1").get();
54
+ if (identity.agent_id !== agentId)
55
+ throw new Error("Training database belongs to a different agent");
56
+ this.#nodeId = String(identity.node_id);
57
+ }
58
+ catch (error) {
59
+ this.#db.close();
60
+ throw error;
61
+ }
62
+ }
63
+ #transaction(fn) {
64
+ this.#db.exec("BEGIN IMMEDIATE");
65
+ try {
66
+ const result = fn();
67
+ this.#db.exec("COMMIT");
68
+ return result;
69
+ }
70
+ catch (error) {
71
+ this.#db.exec("ROLLBACK");
72
+ throw error;
73
+ }
74
+ }
75
+ #checkLease() {
76
+ const lease = this.#db.prepare("SELECT owner,expires FROM training_lock WHERE singleton=1").get();
77
+ if (lease?.owner !== this.#owner || Number(lease.expires) <= Date.now())
78
+ throw new Error("Training lease lost; no further API requests permitted");
79
+ }
80
+ async locked(fn) {
81
+ this.#transaction(() => {
82
+ const lock = this.#db.prepare(`INSERT INTO training_lock VALUES (1,?,?) ON CONFLICT(singleton)
83
+ DO UPDATE SET owner=excluded.owner,expires=excluded.expires WHERE training_lock.expires<=?`)
84
+ .run(this.#owner, Date.now() + LEASE_MS, Date.now());
85
+ if (!lock.changes)
86
+ throw new Error("Another memory-training command is running");
87
+ // A request may have been billed before a crashed process saved the response.
88
+ this.#db.exec(`UPDATE training_gates SET status='ambiguous',error='interrupted' WHERE status='attempted';
89
+ UPDATE training_attempts SET status='ambiguous',error='interrupted' WHERE status='attempted';
90
+ UPDATE training_steps SET status='ambiguous',error='interrupted' WHERE status='attempted';
91
+ UPDATE training_step_attempts SET status='ambiguous',error='interrupted' WHERE status='attempted';`);
92
+ });
93
+ let leaseError;
94
+ const heartbeat = setInterval(() => { try {
95
+ this.renew();
96
+ }
97
+ catch (error) {
98
+ leaseError = error;
99
+ } }, 10_000);
100
+ heartbeat.unref();
101
+ try {
102
+ const value = await fn();
103
+ if (leaseError)
104
+ throw leaseError;
105
+ return value;
106
+ }
107
+ finally {
108
+ clearInterval(heartbeat);
109
+ this.#db.prepare("DELETE FROM training_lock WHERE owner=?").run(this.#owner);
110
+ }
111
+ }
112
+ renew() {
113
+ this.#transaction(() => {
114
+ this.#checkLease();
115
+ this.#db.prepare("UPDATE training_lock SET expires=? WHERE owner=?").run(Date.now() + LEASE_MS, this.#owner);
116
+ });
117
+ }
118
+ sessions() { return this.#db.prepare("SELECT DISTINCT session_id FROM training_examples").all().map(r => String(r.session_id)); }
119
+ syncSession(sessionId, examples, since, until, existingOnly) {
120
+ return this.#transaction(() => {
121
+ this.#checkLease();
122
+ const previous = new Map(this.#db.prepare("SELECT event_seq,input_hash,active FROM training_examples WHERE session_id=?").all(sessionId)
123
+ .map(row => [Number(row.event_seq), row]));
124
+ this.#db.prepare("UPDATE training_examples SET active=0 WHERE session_id=?").run(sessionId);
125
+ const counts = { added: 0, changed: 0, unchanged: 0, retired: 0 };
126
+ const retained = new Set();
127
+ for (const example of examples) {
128
+ const old = previous.get(example.seq);
129
+ if (!old && (existingOnly || example.timestamp < since || example.timestamp >= until))
130
+ continue;
131
+ retained.add(example.seq);
132
+ this.#db.prepare("INSERT OR IGNORE INTO training_inputs VALUES (?,?,?)")
133
+ .run(example.inputHash, TRAINING_PREPARATION, JSON.stringify(example.input));
134
+ this.#db.prepare(`INSERT INTO training_examples VALUES (?,?,?,?,?,?,1)
135
+ ON CONFLICT(session_id,event_seq) DO UPDATE SET timestamp=excluded.timestamp,input_hash=excluded.input_hash,
136
+ context_limited=excluded.context_limited,active=1`)
137
+ .run(trainingHash([this.#nodeId, this.#agentId, sessionId, example.seq]), sessionId, example.seq, example.timestamp, example.inputHash, Number(example.contextLimited));
138
+ const id = trainingHash([example.inputHash, TRAINING_GATE_VERSION, TRAINING_GATE_MODEL]);
139
+ this.#db.prepare(`INSERT OR IGNORE INTO training_gates (id,input_hash,prompt_version,requested_model,status) VALUES (?,?,?,?,'pending')`)
140
+ .run(id, example.inputHash, TRAINING_GATE_VERSION, TRAINING_GATE_MODEL);
141
+ if (!old)
142
+ counts.added++;
143
+ else if (old.input_hash !== example.inputHash || old.active !== 1)
144
+ counts.changed++;
145
+ else
146
+ counts.unchanged++;
147
+ }
148
+ counts.retired = [...previous].filter(([seq, row]) => row.active === 1 && !retained.has(seq)).length;
149
+ return counts;
150
+ });
151
+ }
152
+ unavailable(sessionId) {
153
+ this.#checkLease();
154
+ this.#db.prepare("UPDATE training_examples SET active=-1 WHERE session_id=? AND active=1").run(sessionId);
155
+ }
156
+ #scope = `g.prompt_version=? AND g.requested_model=? AND EXISTS
157
+ (SELECT 1 FROM training_examples e WHERE e.input_hash=g.input_hash AND e.active=1)`;
158
+ pending(limit) {
159
+ return this.#db.prepare(`SELECT g.id,g.input_hash inputHash,i.input_json inputJson
160
+ FROM training_gates g JOIN training_inputs i ON i.hash=g.input_hash
161
+ WHERE ${this.#scope} AND g.status='pending' ORDER BY
162
+ (SELECT MAX(timestamp) FROM training_examples WHERE input_hash=g.input_hash AND active=1) DESC,g.id LIMIT ?`)
163
+ .all(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, limit ?? -1);
164
+ }
165
+ start(id) {
166
+ return this.#transaction(() => {
167
+ this.#checkLease();
168
+ if (!this.#db.prepare("UPDATE training_gates SET status='attempted',error=NULL WHERE id=? AND status='pending'").run(id).changes) {
169
+ throw new Error("Training gate is not pending");
170
+ }
171
+ return Number(this.#db.prepare("INSERT INTO training_attempts (gate_id,started_at,status) VALUES (?,?,'attempted')")
172
+ .run(id, Date.now()).lastInsertRowid);
173
+ });
174
+ }
175
+ finish(id, attempt, result) {
176
+ this.#transaction(() => {
177
+ this.#checkLease();
178
+ const ok = "probability" in result;
179
+ const status = ok ? "complete" : result.status, error = ok ? null : result.error;
180
+ this.#db.prepare(`UPDATE training_gates SET status=?,probability=?,actual_model=?,input_tokens=?,output_tokens=?,error=?,completed_at=? WHERE id=?`)
181
+ .run(status, ok ? result.probability : null, ok ? result.model : null, ok ? result.usage.input_tokens : null, ok ? result.usage.output_tokens : null, error, Date.now(), id);
182
+ this.#db.prepare("UPDATE training_attempts SET status=?,finished_at=?,error=?,result_json=? WHERE id=? AND gate_id=?")
183
+ .run(status, Date.now(), error, ok ? JSON.stringify(result) : null, attempt, id);
184
+ });
185
+ }
186
+ retry(includeAmbiguous) {
187
+ this.#checkLease();
188
+ 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));
189
+ }
190
+ activeExamples() {
191
+ return this.#db.prepare(`SELECT e.id,e.input_hash inputHash,i.input_json inputJson,e.session_id sessionId,e.timestamp
192
+ FROM training_examples e JOIN training_inputs i ON i.hash=e.input_hash
193
+ WHERE e.active=1 ORDER BY e.timestamp DESC,e.id`).all();
194
+ }
195
+ queryExamples(threshold = TRAINING_GATE_THRESHOLD) {
196
+ if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1)
197
+ throw new Error("Threshold must be between 0 and 1");
198
+ const probabilities = new Map(this.#db.prepare(`SELECT g.input_hash,g.probability FROM training_gates g
199
+ WHERE ${this.#scope} AND g.status='complete' AND g.probability>=?`)
200
+ .all(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, threshold).map(row => [String(row.input_hash), Number(row.probability)]));
201
+ return this.activeExamples().flatMap(example => {
202
+ const recallProbability = probabilities.get(example.inputHash);
203
+ return recallProbability === undefined ? [] : [{ ...example, recallProbability }];
204
+ });
205
+ }
206
+ step(stage, request) {
207
+ const id = trainingHash([stage, request]);
208
+ const row = this.#db.prepare("SELECT status,result_json FROM training_steps WHERE id=? AND stage=?").get(id, stage);
209
+ return { id, stage, request, status: (row?.status ?? "pending"),
210
+ result: row?.status === "complete" ? JSON.parse(String(row.result_json)) : undefined };
211
+ }
212
+ judgmentExcluded(identity) {
213
+ return !!this.#db.prepare(`SELECT 1 FROM training_steps WHERE stage='judge' AND status='complete'
214
+ AND json_extract(request_json,'$.identity')=? AND json_extract(result_json,'$.excluded')=1 LIMIT 1`).get(identity);
215
+ }
216
+ startStep(stage, id, request) {
217
+ return this.#transaction(() => {
218
+ this.#checkLease();
219
+ this.#db.prepare("INSERT OR IGNORE INTO training_steps (id,stage,request_json,status) VALUES (?,?,?,'pending')")
220
+ .run(id, stage, JSON.stringify(request));
221
+ if (!this.#db.prepare("UPDATE training_steps SET status='attempted',error=NULL WHERE id=? AND status='pending'").run(id).changes) {
222
+ throw new Error("Training step is not pending");
223
+ }
224
+ return Number(this.#db.prepare("INSERT INTO training_step_attempts (step_id,started_at,status) VALUES (?,?,'attempted')")
225
+ .run(id, Date.now()).lastInsertRowid);
226
+ });
227
+ }
228
+ finishStep(stage, id, attempt, outcome) {
229
+ this.#transaction(() => {
230
+ this.#checkLease();
231
+ const ok = "result" in outcome, status = ok ? "complete" : outcome.status;
232
+ const json = ok ? JSON.stringify(outcome.result) : null, error = ok ? null : outcome.error;
233
+ if (!this.#db.prepare(`UPDATE training_steps SET status=?,result_json=?,error=?,completed_at=? WHERE id=? AND stage=? AND status='attempted'`)
234
+ .run(status, json, error, Date.now(), id, stage).changes)
235
+ throw new Error("Training step attempt lost");
236
+ if (!this.#db.prepare(`UPDATE training_step_attempts SET status=?,finished_at=?,error=?,result_json=? WHERE id=? AND step_id=? AND status='attempted'`)
237
+ .run(status, Date.now(), error, json, attempt, id).changes)
238
+ throw new Error("Training attempt lost");
239
+ });
240
+ }
241
+ completedEvaluations(versions) {
242
+ return this.#db.prepare(`SELECT result_json FROM training_steps WHERE stage='evaluate' AND status='complete'
243
+ AND (? IS NULL OR (json_extract(request_json,'$.version')=? AND json_extract(request_json,'$.retrievalVersion')=?)) ORDER BY completed_at DESC,id`)
244
+ .all(versions?.selection ?? null, versions?.selection ?? null, versions?.retrieval ?? null)
245
+ .map(row => JSON.parse(String(row.result_json)));
246
+ }
247
+ sourceDetails(id) {
248
+ const row = this.#db.prepare(`SELECT session_id sessionId,event_seq userEventId,timestamp,context_limited contextLimited
249
+ FROM training_examples WHERE id=? AND active=1`).get(id);
250
+ if (!row)
251
+ throw new Error("Inactive training source");
252
+ return { nodeId: this.#nodeId, agentId: this.#agentId, sourceId: id, ...row };
253
+ }
254
+ stepRecord(id) {
255
+ const row = this.#db.prepare("SELECT id,stage,request_json,result_json,completed_at FROM training_steps WHERE id=? AND status='complete'").get(id);
256
+ if (!row)
257
+ throw new Error("Missing training provenance checkpoint");
258
+ return { id, stage: row.stage, request: JSON.parse(String(row.request_json)),
259
+ result: JSON.parse(String(row.result_json)), completedAt: row.completed_at };
260
+ }
261
+ status(threshold) {
262
+ const stages = this.#db.prepare(`SELECT g.status,COUNT(*) count FROM training_gates g WHERE ${this.#scope} GROUP BY g.status`)
263
+ .all(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL);
264
+ const labels = this.#db.prepare(`SELECT COUNT(*) complete,COALESCE(SUM(g.probability>=?),0) positive,
265
+ COALESCE(SUM(g.input_tokens),0) inputTokens,COALESCE(SUM(g.output_tokens),0) outputTokens
266
+ FROM training_gates g WHERE ${this.#scope} AND g.status='complete'`).get(threshold, TRAINING_GATE_VERSION, TRAINING_GATE_MODEL);
267
+ return { nodeId: this.#nodeId, agentId: this.#agentId, preparation: TRAINING_PREPARATION,
268
+ promptVersion: TRAINING_GATE_VERSION, requestedModel: TRAINING_GATE_MODEL, threshold,
269
+ examples: this.#db.prepare("SELECT active,COUNT(*) count FROM training_examples GROUP BY active").all(),
270
+ collectedInputs: Number(this.#db.prepare("SELECT COUNT(DISTINCT input_hash) count FROM training_examples WHERE active=1").get().count),
271
+ queryInputs: Number(labels.positive),
272
+ stages, complete: Number(labels.complete), positive: Number(labels.positive),
273
+ inputTokens: Number(labels.inputTokens), outputTokens: Number(labels.outputTokens),
274
+ negative: Number(labels.complete) - Number(labels.positive),
275
+ queryStages: this.#db.prepare("SELECT stage,status,COUNT(*) count FROM training_steps GROUP BY stage,status ORDER BY stage,status").all(),
276
+ queryAttempts: this.#db.prepare(`SELECT s.stage,a.status,COUNT(*) count,
277
+ COUNT(json_extract(a.result_json,'$.usage.input_tokens')) usageReported,
278
+ SUM(json_extract(a.result_json,'$.usage.input_tokens')) inputTokens,
279
+ SUM(json_extract(a.result_json,'$.usage.output_tokens')) outputTokens
280
+ FROM training_step_attempts a JOIN training_steps s ON s.id=a.step_id GROUP BY s.stage,a.status`).all(),
281
+ attempts: this.#db.prepare(`SELECT status,COUNT(*) count,
282
+ COALESCE(SUM(json_extract(result_json,'$.usage.input_tokens')),0) inputTokens,
283
+ COALESCE(SUM(json_extract(result_json,'$.usage.output_tokens')),0) outputTokens
284
+ FROM training_attempts GROUP BY status`).all() };
285
+ }
286
+ *exportRows(threshold) {
287
+ const rows = this.#db.prepare(`SELECT g.*,i.input_json FROM training_gates g JOIN training_inputs i ON i.hash=g.input_hash
288
+ WHERE ${this.#scope} AND g.status='complete' ORDER BY g.id`).iterate(TRAINING_GATE_VERSION, TRAINING_GATE_MODEL);
289
+ for (const row of rows) {
290
+ yield { stage: "recall-gate", inputHash: row.input_hash, preparation: TRAINING_PREPARATION,
291
+ input: JSON.parse(String(row.input_json)), recallProbability: row.probability,
292
+ recallNeeded: Number(row.probability) >= threshold, threshold, model: row.actual_model, promptVersion: row.prompt_version,
293
+ usage: { input_tokens: row.input_tokens, output_tokens: row.output_tokens },
294
+ sources: this.#db.prepare(`SELECT id,session_id sessionId,event_seq userEventId,timestamp,context_limited contextLimited
295
+ FROM training_examples WHERE input_hash=? AND active=1 ORDER BY session_id,event_seq`).all(String(row.input_hash))
296
+ .map(source => ({ nodeId: this.#nodeId, agentId: this.#agentId, ...source })) };
297
+ }
298
+ }
299
+ close() { this.#db.close(); }
300
+ }
@@ -0,0 +1,57 @@
1
+ import type { UnblockMemoryConfig } from "./config.js";
2
+ import type { TrainingStore } from "./training-store.js";
3
+ type Source = {
4
+ databasePath: string;
5
+ agentId: string;
6
+ };
7
+ /** A full active-branch rescan is cheap/local; only changed exact inputs need inference. */
8
+ export declare function collectTraining(source: Source, store?: TrainingStore, options?: {
9
+ since?: number;
10
+ until?: number;
11
+ existingOnly?: boolean;
12
+ }): {
13
+ sessions: number;
14
+ excludedSessions: number;
15
+ oversizedSessions: number;
16
+ eligible: number;
17
+ users: number;
18
+ filtered: number;
19
+ oversized: number;
20
+ unanswered: number;
21
+ added: number;
22
+ changed: number;
23
+ unchanged: number;
24
+ retired: number;
25
+ };
26
+ /** Parallel, bounded paid work. Negative results are just as cacheable as positives. */
27
+ export declare function runTraining(source: Source, store: TrainingStore, config: UnblockMemoryConfig, options: {
28
+ maxExamples?: number;
29
+ maxInputBytes: number;
30
+ concurrency?: number;
31
+ dryRun?: boolean;
32
+ }): Promise<{
33
+ refreshed: {
34
+ sessions: number;
35
+ excludedSessions: number;
36
+ oversizedSessions: number;
37
+ eligible: number;
38
+ users: number;
39
+ filtered: number;
40
+ oversized: number;
41
+ unanswered: number;
42
+ added: number;
43
+ changed: number;
44
+ unchanged: number;
45
+ retired: number;
46
+ };
47
+ pendingSelected: number;
48
+ calls: number;
49
+ completed: number;
50
+ failed: number;
51
+ ambiguous: number;
52
+ inputBytes: number;
53
+ inputTokens: number;
54
+ outputTokens: number;
55
+ budgetLimited: boolean;
56
+ }>;
57
+ export {};
@@ -0,0 +1,104 @@
1
+ import { TypeSafeHttpError } from "./typesafe-transport.js";
2
+ import { resolveTypeSafeApiKey } from "./typesafe.js";
3
+ import { TrainingTranscriptReader } from "./training-input.js";
4
+ import { judgeTrainingInput, TRAINING_GATE_QUESTIONS } from "./training-gate.js";
5
+ /** A full active-branch rescan is cheap/local; only changed exact inputs need inference. */
6
+ export function collectTraining(source, store, options = {}) {
7
+ const since = options.since ?? 0, until = options.until ?? Number.MAX_SAFE_INTEGER;
8
+ const reader = new TrainingTranscriptReader(source.databasePath, source.agentId);
9
+ const result = { sessions: 0, excludedSessions: 0, oversizedSessions: 0, eligible: 0,
10
+ users: 0, filtered: 0, oversized: 0, unanswered: 0, added: 0, changed: 0, unchanged: 0, retired: 0 };
11
+ try {
12
+ const sessions = new Set(options.existingOnly ? store?.sessions() : [...reader.sessions(), ...store?.sessions() ?? []]);
13
+ for (const id of sessions) {
14
+ store?.renew();
15
+ const extracted = reader.read(id);
16
+ result.sessions++;
17
+ if (extracted && "oversized" in extracted) {
18
+ result.oversizedSessions++;
19
+ store?.unavailable(id);
20
+ continue;
21
+ }
22
+ if (!extracted)
23
+ result.excludedSessions++;
24
+ else {
25
+ for (const key of ["users", "filtered", "oversized", "unanswered"])
26
+ result[key] += extracted.coverage[key];
27
+ result.eligible += extracted.examples.filter(e => e.timestamp >= since && e.timestamp < until).length;
28
+ }
29
+ const counts = store?.syncSession(id, extracted?.examples ?? [], since, until, options.existingOnly ?? false);
30
+ if (counts)
31
+ for (const key of ["added", "changed", "unchanged", "retired"])
32
+ result[key] += counts[key];
33
+ }
34
+ return result;
35
+ }
36
+ finally {
37
+ reader.close();
38
+ }
39
+ }
40
+ /** Parallel, bounded paid work. Negative results are just as cacheable as positives. */
41
+ export async function runTraining(source, store, config, options) {
42
+ if ((options.maxExamples !== undefined && (!Number.isSafeInteger(options.maxExamples) || options.maxExamples < 1)) ||
43
+ !Number.isSafeInteger(options.maxInputBytes) || options.maxInputBytes < 1)
44
+ throw new Error("Invalid training run bounds");
45
+ const refreshed = collectTraining(source, store, { existingOnly: true });
46
+ const concurrency = options.concurrency ?? 256;
47
+ if (!Number.isSafeInteger(concurrency) || concurrency < 1)
48
+ throw new Error("Invalid training concurrency");
49
+ const jobs = store.pending(options.maxExamples);
50
+ const result = { refreshed, pendingSelected: jobs.length, calls: 0, completed: 0, failed: 0, ambiguous: 0,
51
+ inputBytes: 0, inputTokens: 0, outputTokens: 0, budgetLimited: false };
52
+ // Resolve only when there is work. Reruns with no pending inputs need no credentials.
53
+ const key = jobs.length && !options.dryRun ? await resolveTypeSafeApiKey(config.typesafe) : undefined;
54
+ if (jobs.length && !options.dryRun && !key)
55
+ throw new Error("TypeSafe is disabled or its credential is unavailable");
56
+ let next = 0, stopped = false;
57
+ const workers = await Promise.allSettled(Array.from({ length: Math.min(concurrency, jobs.length) }, async () => {
58
+ while (!stopped && !result.budgetLimited && !result.failed && !result.ambiguous) {
59
+ const job = jobs[next++];
60
+ if (!job)
61
+ return;
62
+ try {
63
+ const input = JSON.parse(job.inputJson);
64
+ const bytes = Buffer.byteLength(JSON.stringify({ state: input, questions: TRAINING_GATE_QUESTIONS }));
65
+ if (result.inputBytes + bytes > options.maxInputBytes) {
66
+ result.budgetLimited = true;
67
+ return;
68
+ }
69
+ result.inputBytes += bytes;
70
+ if (options.dryRun)
71
+ continue;
72
+ store.renew();
73
+ const attempt = store.start(job.id); // Commit BEFORE the request can leave this process.
74
+ result.calls++;
75
+ let judgment;
76
+ try {
77
+ // Offline labeling has its own deadline, not Whisperer's latency-sensitive timeout.
78
+ judgment = await judgeTrainingInput(input, key, AbortSignal.timeout(30_000));
79
+ }
80
+ catch (error) {
81
+ const definite = error instanceof TypeSafeHttpError && error.status >= 400 && error.status < 500;
82
+ const status = definite ? "failed" : "ambiguous";
83
+ store.finish(job.id, attempt, { status, error: error instanceof TypeSafeHttpError ? `http_${error.status}` : "request_or_response_uncertain" });
84
+ result[status]++;
85
+ // Stop on the first failure rather than spending the rest of the budget during an outage.
86
+ return;
87
+ }
88
+ // Storage failures must not be misclassified as provider failures/retried.
89
+ store.finish(job.id, attempt, judgment);
90
+ result.completed++;
91
+ result.inputTokens += judgment.usage.input_tokens;
92
+ result.outputTokens += judgment.usage.output_tokens;
93
+ }
94
+ catch (error) {
95
+ stopped = true;
96
+ throw error;
97
+ }
98
+ }
99
+ }));
100
+ const failure = workers.find(item => item.status === "rejected");
101
+ if (failure)
102
+ throw failure.reason;
103
+ return result;
104
+ }
@@ -6,7 +6,7 @@ type RequestOptions = {
6
6
  type Json = string | number | boolean | null | Json[] | {
7
7
  [key: string]: Json;
8
8
  };
9
- export declare const TYPESAFE_REVIEW_MODEL = "jev-1.13.0";
9
+ export { TYPESAFE_MODEL as TYPESAFE_REVIEW_MODEL } from "./typesafe-transport.js";
10
10
  export declare function askTypeSafeReview(params: RequestOptions, state: Json, questions: Json): Promise<unknown>;
11
11
  /** The source is an indexed snapshot, not proof of current truth or permission to write. */
12
12
  export declare function reviewTypeSafeClaim(params: RequestOptions & {
@@ -50,4 +50,3 @@ export declare function reviewClusterDefects(params: RequestOptions & {
50
50
  defect: "encoding" | "wrapper" | "boilerplate" | "none_or_uncertain";
51
51
  confidence: number;
52
52
  }[]>;
53
- export {};
@@ -1,21 +1,13 @@
1
1
  import { Type } from "typebox";
2
2
  import { Value } from "typebox/value";
3
3
  import { backgroundWordCount, PEOPLE_BACKGROUND_MAX_WORDS } from "./people-background.js";
4
- export const TYPESAFE_REVIEW_MODEL = "jev-1.13.0";
4
+ import { postTypeSafe } from "./typesafe-transport.js";
5
+ export { TYPESAFE_MODEL as TYPESAFE_REVIEW_MODEL } from "./typesafe-transport.js";
5
6
  export async function askTypeSafeReview(params, state, questions) {
6
7
  const signal = AbortSignal.any([params.signal, AbortSignal.timeout(params.timeoutMs)]);
7
8
  try {
8
9
  signal.throwIfAborted();
9
- const response = await fetch("https://api.typesafe.ai/v1/systemone", {
10
- method: "POST", redirect: "error", signal,
11
- headers: { Authorization: `Bearer ${params.apiKey}`, "Content-Type": "application/json" },
12
- body: JSON.stringify({ model: TYPESAFE_REVIEW_MODEL, state, questions }),
13
- });
14
- if (!response.ok) {
15
- await response.body?.cancel();
16
- throw new Error("HTTP failure");
17
- }
18
- return await response.json();
10
+ return await postTypeSafe({ apiKey: params.apiKey, signal }, state, questions);
19
11
  }
20
12
  catch {
21
13
  throw new Error(signal.aborted ? "TypeSafe review aborted" : "TypeSafe review unavailable");
@@ -0,0 +1,10 @@
1
+ export declare const TYPESAFE_MODEL = "jev-1.13.0";
2
+ export declare class TypeSafeHttpError extends Error {
3
+ readonly status: number;
4
+ constructor(status: number);
5
+ }
6
+ /** Shared wire protocol; callers own deadlines, judgments and public errors. */
7
+ export declare function postTypeSafe(params: {
8
+ apiKey: string;
9
+ signal: AbortSignal;
10
+ }, state: unknown, questions: unknown): Promise<unknown>;
@@ -0,0 +1,26 @@
1
+ export const TYPESAFE_MODEL = "jev-1.13.0";
2
+ export class TypeSafeHttpError extends Error {
3
+ status;
4
+ constructor(status) {
5
+ super(`TypeSafe HTTP ${status}`);
6
+ this.status = status;
7
+ }
8
+ }
9
+ /** Shared wire protocol; callers own deadlines, judgments and public errors. */
10
+ export async function postTypeSafe(params, state, questions) {
11
+ const response = await fetch("https://api.typesafe.ai/v1/systemone", {
12
+ method: "POST", redirect: "error", signal: params.signal,
13
+ headers: { Authorization: `Bearer ${params.apiKey}`, "Content-Type": "application/json" },
14
+ body: JSON.stringify({ model: TYPESAFE_MODEL, state, questions }),
15
+ });
16
+ if (!response.ok) {
17
+ try {
18
+ await response.body?.cancel();
19
+ }
20
+ finally {
21
+ // Preserve the status even if cancellation fails; never include provider content.
22
+ throw new TypeSafeHttpError(response.status);
23
+ }
24
+ }
25
+ return response.json();
26
+ }
@@ -41,7 +41,7 @@ export declare function judgeTypeSafeMemories(params: {
41
41
  candidates: readonly {
42
42
  excerpt: string;
43
43
  corpus: string;
44
- startedAt?: number;
44
+ messageTimestamp?: string;
45
45
  }[];
46
46
  }): Promise<number[]>;
47
47
  export {};