@unblocklabs/unblock-memory 0.7.1 → 0.7.3
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/dist/src/analysis.js +36 -25
- package/dist/src/config.js +2 -17
- package/dist/src/contracts.d.ts +1 -1
- package/dist/src/extraction-chunk.d.ts +1 -0
- package/dist/src/extraction-chunk.js +1 -0
- package/dist/src/extraction-model.js +5 -4
- package/dist/src/extraction-runtime.js +2 -7
- package/dist/src/extraction-source.js +6 -2
- package/dist/src/extraction-store.d.ts +3 -0
- package/dist/src/extraction-store.js +14 -10
- package/dist/src/extraction-worker.js +31 -15
- package/dist/src/manager.d.ts +1 -1
- package/dist/src/manager.js +66 -85
- package/dist/src/memory-whisperer.js +6 -10
- package/dist/src/people-primer-config.js +1 -4
- package/dist/src/people-primer.js +6 -8
- package/dist/src/people-store.js +9 -1
- package/dist/src/people-tools.js +2 -6
- package/dist/src/plugin.js +6 -14
- package/dist/src/quality-audit.js +11 -2
- package/dist/src/query-contract.js +18 -10
- package/dist/src/response-audit.js +3 -3
- package/dist/src/response-judge.js +1 -2
- package/dist/src/response-reviews.d.ts +1 -1
- package/dist/src/response-reviews.js +4 -2
- package/dist/src/response-store.js +3 -2
- package/dist/src/retrieval-telemetry.d.ts +1 -1
- package/dist/src/review-tools.d.ts +24 -0
- package/dist/src/review-tools.js +22 -26
- package/dist/src/runtime.js +2 -1
- package/dist/src/session-projector.js +8 -4
- package/dist/src/session-sync.js +2 -1
- package/dist/src/skill-whisperer.js +51 -24
- package/dist/src/training-candidates.d.ts +12 -13
- package/dist/src/training-candidates.js +11 -81
- package/dist/src/training-gate.js +2 -2
- package/dist/src/training-queries.js +5 -2
- package/dist/src/training-retrieval.d.ts +15 -2
- package/dist/src/training-retrieval.js +63 -39
- package/dist/src/training-store.d.ts +2 -8
- package/dist/src/training-store.js +15 -20
- package/dist/src/typesafe-client.d.ts +5 -0
- package/dist/src/typesafe-client.js +4 -0
- package/dist/src/typesafe-review.js +4 -6
- package/dist/src/typesafe.d.ts +1 -9
- package/dist/src/typesafe.js +4 -10
- package/dist/src/whisperer-context.d.ts +5 -1
- package/dist/src/whisperer-context.js +12 -7
- package/dist/src/workspace-path-classifier.js +39 -32
- package/docs/configuration.md +3 -2
- package/docs/memory-training.md +8 -7
- package/docs/retrieval.md +15 -1
- package/openclaw.plugin.json +2 -2
- package/package.json +6 -4
package/dist/src/analysis.js
CHANGED
|
@@ -200,22 +200,21 @@ export function latestAnalysisCollections(db) {
|
|
|
200
200
|
return undefined;
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
|
-
function count(db, sql, runId) {
|
|
204
|
-
return db.prepare(sql).get(runId)?.count ?? 0;
|
|
205
|
-
}
|
|
206
203
|
function analysisSummary(db, run) {
|
|
207
|
-
const clusters
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
204
|
+
const { clusters, expectedNonNoise } = db.prepare(`
|
|
205
|
+
SELECT COUNT(*) AS clusters, COALESCE(SUM(size), 0) AS expectedNonNoise
|
|
206
|
+
FROM memory_analysis_clusters WHERE run_id = ?
|
|
207
|
+
`).get(run.id);
|
|
208
|
+
const { members, nonNoise, noise, unassigned } = db.prepare(`
|
|
209
|
+
SELECT COUNT(*) AS members,
|
|
210
|
+
COALESCE(SUM(m.cluster_id <> -1), 0) AS nonNoise,
|
|
211
|
+
COALESCE(SUM(m.cluster_id = -1), 0) AS noise,
|
|
212
|
+
COALESCE(SUM(m.cluster_id <> -1 AND c.cluster_id IS NULL), 0) AS unassigned
|
|
214
213
|
FROM memory_analysis_memberships m
|
|
215
214
|
LEFT JOIN memory_analysis_clusters c
|
|
216
215
|
ON c.run_id = m.run_id AND c.cluster_id = m.cluster_id
|
|
217
|
-
WHERE m.run_id = ?
|
|
218
|
-
|
|
216
|
+
WHERE m.run_id = ?
|
|
217
|
+
`).get(run.id);
|
|
219
218
|
if (nonNoise !== expectedNonNoise || members !== nonNoise + noise || unassigned > 0)
|
|
220
219
|
return undefined;
|
|
221
220
|
return {
|
|
@@ -261,8 +260,8 @@ function byteSlice(text, maxBytes) {
|
|
|
261
260
|
return "";
|
|
262
261
|
return bytes.subarray(0, maxBytes - 3).toString("utf8").replace(/\uFFFD$/u, "") + "…";
|
|
263
262
|
}
|
|
264
|
-
function
|
|
265
|
-
|
|
263
|
+
function representativeOrder(clusterId) {
|
|
264
|
+
return clusterId === -1
|
|
266
265
|
? "m.outlier_score DESC, m.hash, m.seq"
|
|
267
266
|
: `CASE WHEN m.representative_rank IS NULL THEN 1 ELSE 0 END,
|
|
268
267
|
m.representative_rank,
|
|
@@ -270,9 +269,11 @@ function members(db, runId, clusterId, limit, offset = 0, sort = "representative
|
|
|
270
269
|
m.outlier_score,
|
|
271
270
|
m.hash,
|
|
272
271
|
m.seq`;
|
|
272
|
+
}
|
|
273
|
+
function members(db, runId, clusterId, limit, offset = 0, sort = "representative", maxExcerptBytes = MAX_EXCERPT_BYTES, maxTotalBytes = MAX_TOTAL_EXCERPT_BYTES, maxTotalAliases = MAX_TOTAL_ALIASES, temporal = {}) {
|
|
273
274
|
const score = clusterId === -1 ? "m.outlier_score" : "m.probability";
|
|
274
275
|
const order = {
|
|
275
|
-
representative: representativeOrder,
|
|
276
|
+
representative: representativeOrder(clusterId),
|
|
276
277
|
score_desc: `${score} DESC, m.hash, m.seq`,
|
|
277
278
|
score_asc: `${score} ASC, m.hash, m.seq`,
|
|
278
279
|
date_desc: "julianday(COALESCE(m.event_time, m.source_modified_at)) DESC, m.hash, m.seq",
|
|
@@ -396,24 +397,34 @@ function readMetadata(run) {
|
|
|
396
397
|
}
|
|
397
398
|
return { stale: false, staleSince: null, analyzedAt: run.completed_at };
|
|
398
399
|
}
|
|
400
|
+
function clusterPreview(db, runId, clusterId, maxBytes, aliasLimit) {
|
|
401
|
+
const row = db.prepare(`
|
|
402
|
+
SELECT m.hash, m.seq, m.probability, m.pos, m.chunk_len, m.doc
|
|
403
|
+
FROM memory_analysis_available_memberships m
|
|
404
|
+
WHERE m.run_id = ? AND m.cluster_id = ?
|
|
405
|
+
ORDER BY ${representativeOrder(clusterId)}
|
|
406
|
+
LIMIT 1
|
|
407
|
+
`).get(runId, clusterId);
|
|
408
|
+
if (!row)
|
|
409
|
+
return undefined;
|
|
410
|
+
return {
|
|
411
|
+
hash: row.hash,
|
|
412
|
+
seq: row.seq,
|
|
413
|
+
probability: row.probability,
|
|
414
|
+
text: byteSlice(row.doc.slice(row.pos, row.pos + row.chunk_len), maxBytes),
|
|
415
|
+
sourcePaths: sourcePaths(db, row.hash, Math.min(MAX_ALIASES_PER_MEMBER, aliasLimit)),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
399
418
|
function toSummary(db, run, row, includePreview, previewBytes = 600, aliasLimit = MAX_TOTAL_ALIASES) {
|
|
400
419
|
const preview = includePreview
|
|
401
|
-
?
|
|
420
|
+
? clusterPreview(db, run.id, row.cluster_id, previewBytes, aliasLimit)
|
|
402
421
|
: undefined;
|
|
403
422
|
return {
|
|
404
423
|
clusterId: clusterReference(run.id, row.cluster_id),
|
|
405
424
|
size: row.size,
|
|
406
425
|
availableSize: row.available_size,
|
|
407
426
|
meanProbability: row.mean_probability,
|
|
408
|
-
...(preview ? {
|
|
409
|
-
preview: {
|
|
410
|
-
hash: preview.hash,
|
|
411
|
-
seq: preview.seq,
|
|
412
|
-
probability: preview.probability,
|
|
413
|
-
text: preview.text,
|
|
414
|
-
sourcePaths: preview.sourcePaths,
|
|
415
|
-
},
|
|
416
|
-
} : {}),
|
|
427
|
+
...(preview ? { preview } : {}),
|
|
417
428
|
};
|
|
418
429
|
}
|
|
419
430
|
function noiseRow(db, runId) {
|
package/dist/src/config.js
CHANGED
|
@@ -291,23 +291,8 @@ function resolvePeople(value) {
|
|
|
291
291
|
};
|
|
292
292
|
}
|
|
293
293
|
export function resolveConfig(value) {
|
|
294
|
-
if (value === undefined || value === null)
|
|
295
|
-
|
|
296
|
-
corpora: DEFAULT_CORPORA,
|
|
297
|
-
extraction: resolveExtraction(undefined, DEFAULT_CORPORA),
|
|
298
|
-
keepEmbeddingModelWarm: true,
|
|
299
|
-
analysis: {},
|
|
300
|
-
typesafe: { ...DEFAULT_TYPESAFE_CONFIG },
|
|
301
|
-
qualityAudit: { ...DEFAULT_QUALITY_AUDIT },
|
|
302
|
-
evidenceReview: { enabled: false, corpora: [] },
|
|
303
|
-
responseAudit: resolveResponseAudit(undefined, DEFAULT_CORPORA),
|
|
304
|
-
insideOut: resolveInsideOut(undefined),
|
|
305
|
-
peoplePrimer: resolvePeoplePrimer(undefined, DEFAULT_CORPORA, false),
|
|
306
|
-
people: DEFAULT_PEOPLE_CONFIG,
|
|
307
|
-
skillWhisperer: DEFAULT_SKILL_WHISPERER,
|
|
308
|
-
memoryWhisperer: { ...DEFAULT_MEMORY_WHISPERER, api: resolveQueryApi(undefined) },
|
|
309
|
-
};
|
|
310
|
-
}
|
|
294
|
+
if (value === undefined || value === null)
|
|
295
|
+
value = {};
|
|
311
296
|
if (typeof value !== "object" || Array.isArray(value)) {
|
|
312
297
|
throw new Error("unblock-memory config must be an object");
|
|
313
298
|
}
|
package/dist/src/contracts.d.ts
CHANGED
|
@@ -29,7 +29,7 @@ export type SessionSearchFilter = {
|
|
|
29
29
|
conversationId?: string;
|
|
30
30
|
};
|
|
31
31
|
export type MemoryRequestContext = Pick<OpenClawPluginToolContext, "sessionKey" | "sessionId" | "messageChannel" | "agentAccountId" | "nativeChannelId" | "deliveryContext">;
|
|
32
|
-
export type CorpusSearchOptions = NonNullable<Parameters<MemorySearchManagerContract["search"]>[1]> & {
|
|
32
|
+
export type CorpusSearchOptions = Omit<NonNullable<Parameters<MemorySearchManagerContract["search"]>[1]>, "lexicalOnly"> & {
|
|
33
33
|
corpora?: readonly string[];
|
|
34
34
|
/** Internal vector-hint budget; oversized matched chunks are omitted, never sliced. */
|
|
35
35
|
maxSnippetChars?: number;
|
|
@@ -98,6 +98,7 @@ export async function readExtractionChunk(params) {
|
|
|
98
98
|
const finish = (index, offset = 0) => {
|
|
99
99
|
const next = page.messages[index];
|
|
100
100
|
return { kind: "page", messages, newIds, entryCount, fence: startingFence ?? page.cursor,
|
|
101
|
+
exhausted: !next && !page.hasMore,
|
|
101
102
|
cursor: next ? PREFIX + JSON.stringify({ before, fence: page.cursor, index, offset, id: next.id, hash: digest(next) }) : page.cursor };
|
|
102
103
|
};
|
|
103
104
|
for (let i = resume?.index ?? 0; i < page.messages.length; i++) {
|
|
@@ -2,7 +2,7 @@ import { Type } from "typebox";
|
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
3
|
import { Tiktoken } from "js-tiktoken/lite";
|
|
4
4
|
import o200kBase from "js-tiktoken/ranks/o200k_base";
|
|
5
|
-
import { requestTypeSafe } from "./typesafe-client.js";
|
|
5
|
+
import { requestTypeSafe, TYPESAFE_NOUL_SCHEMA as noul } from "./typesafe-client.js";
|
|
6
6
|
export const EXTRACTION_VERSION = "lasting-facts-v11";
|
|
7
7
|
// Jev allows 32k for state plus its longest question. Leave room for validation
|
|
8
8
|
// instructions, proposal evidence and tokenizer differences, not just Luna input.
|
|
@@ -34,6 +34,7 @@ Only extract facts supported by at least one NEW message. Context-only messages
|
|
|
34
34
|
If a fact is already in existing memories, omit it. Source dates control chronology, not processing order: during backfill, never replace a newer observed fact with an older one, or emit the older value as an undated duplicate. If new evidence clearly corrects/changes an existing memory, set replaces to its exact id and emit the concise replacement. Otherwise replaces is null. Never replace unrelated facts. If a previous claim is explicitly withdrawn without a replacement, retain a concise qualified correction rather than inventing a value.
|
|
35
35
|
Cite exact contiguous substrings copied from the supplied messages using their ids. Preserve Markdown markers, mentions, whitespace and punctuation inside each quote. Never join separated spans into one quote or clean up its formatting. Prefer short exact spans; use separate evidence entries when needed. Include enough evidence to resolve corrections and speaker attribution. Output an empty memories array when nothing qualifies. Do not fill a quota.`;
|
|
36
36
|
let tokenizer;
|
|
37
|
+
let promptTokens;
|
|
37
38
|
// o200k_base is an explicit budgeting proxy, not a claim about the host's private tokenizer.
|
|
38
39
|
function extractionTokens(text) {
|
|
39
40
|
return (tokenizer ??= new Tiktoken(o200kBase)).encode(text, [], []).length;
|
|
@@ -45,7 +46,7 @@ function extractionInput(messages, newIds, existing) {
|
|
|
45
46
|
return JSON.stringify({ messages: messages.map(datedMessage), newMessageIds: newIds, existing, schema: proposalSchema });
|
|
46
47
|
}
|
|
47
48
|
export function extractionOverhead(existing) {
|
|
48
|
-
return extractionTokens(EXTRACTION_PROMPT) + extractionTokens(extractionInput([], [], existing)) + 512;
|
|
49
|
+
return (promptTokens ??= extractionTokens(EXTRACTION_PROMPT)) + extractionTokens(extractionInput([], [], existing)) + 512;
|
|
49
50
|
}
|
|
50
51
|
export function extractionMessageTokens(message) {
|
|
51
52
|
return extractionTokens(JSON.stringify(datedMessage(message))) + extractionTokens(JSON.stringify(message.id)) + 8;
|
|
@@ -84,8 +85,8 @@ export async function extractWithLuna(runtime, agentId, messages, newIds, existi
|
|
|
84
85
|
const judgments = Type.Object({ answers: Type.Object({
|
|
85
86
|
supported: Type.Object({ type: Type.Literal("choice"), choice: Type.Union([Type.Literal("supported"), Type.Literal("contradicted"), Type.Literal("unsupported")]),
|
|
86
87
|
probabilities: Type.Object({ supported: Type.Number({ minimum: 0, maximum: 1 }), contradicted: Type.Number({ minimum: 0, maximum: 1 }), unsupported: Type.Number({ minimum: 0, maximum: 1 }) }) }),
|
|
87
|
-
useful:
|
|
88
|
-
replacement: Type.Optional(
|
|
88
|
+
useful: noul,
|
|
89
|
+
replacement: Type.Optional(noul),
|
|
89
90
|
}) });
|
|
90
91
|
export async function validateExtractedMemory(params) {
|
|
91
92
|
const { proposal, messages, existing } = params;
|
|
@@ -7,6 +7,7 @@ import { resolveAgentDir, resolveStateDir } from "openclaw/plugin-sdk/memory-cor
|
|
|
7
7
|
import { MEMORY_DATABASE, hasMemoryTable } from "./memory-database.js";
|
|
8
8
|
import { ExtractionStore } from "./extraction-store.js";
|
|
9
9
|
import { extractionSessions } from "./extraction-source.js";
|
|
10
|
+
import { dateOption } from "./date-option.js";
|
|
10
11
|
export function registerExtraction(api, config, runtime) {
|
|
11
12
|
let lifetime = new AbortController();
|
|
12
13
|
let timer, running;
|
|
@@ -36,13 +37,7 @@ export function registerExtraction(api, config, runtime) {
|
|
|
36
37
|
.option("--since <date>", "Explicit bounded backfill from YYYY-MM-DD UTC; repeat to resume")
|
|
37
38
|
.option("--session <id>", "Restrict this run to one session")
|
|
38
39
|
.action(async (opts) => {
|
|
39
|
-
|
|
40
|
-
if (opts.since !== undefined) {
|
|
41
|
-
since = Date.parse(opts.since);
|
|
42
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(opts.since) || !Number.isFinite(since) || new Date(since).toISOString().slice(0, 10) !== opts.since) {
|
|
43
|
-
throw new Error("since must be a valid YYYY-MM-DD UTC date");
|
|
44
|
-
}
|
|
45
|
-
}
|
|
40
|
+
const since = opts.since === undefined ? undefined : dateOption(opts.since, 0);
|
|
46
41
|
try {
|
|
47
42
|
// Native harnesses live in the Gateway, not the cold CLI metadata loader.
|
|
48
43
|
const result = await callGatewayFromCli("unblock-memory.extract", { timeout: "600000" }, { agentId: opts.agent, ...(since === undefined ? {} : { since }), ...(opts.session ? { sessionId: opts.session } : {}) });
|
|
@@ -11,13 +11,17 @@ export function extractionSessions(databasePath, agentId, chatTypes) {
|
|
|
11
11
|
}
|
|
12
12
|
return db.prepare(`SELECT w.session_id,w.session_key,w.chat_type,COALESCE(w.channel,c.channel) AS provider,
|
|
13
13
|
COALESCE(w.account_id,c.account_id) AS account_id,COALESCE(c.native_channel_id,c.native_direct_user_id,c.peer_id,w.primary_conversation_id) AS conversation_id,
|
|
14
|
-
COALESCE(w.started_at,w.created_at) AS started_at, COALESCE(w.transcript_updated_at,w.updated_at,w.created_at) AS changed_at
|
|
14
|
+
COALESCE(w.started_at,w.created_at) AS started_at, COALESCE(w.transcript_updated_at,w.updated_at,w.created_at) AS changed_at,
|
|
15
|
+
r.generation,(SELECT MAX(seq) FROM transcript_events WHERE session_id=w.session_id) AS tail FROM session_windows w
|
|
15
16
|
LEFT JOIN conversations c ON c.conversation_id=w.primary_conversation_id
|
|
17
|
+
LEFT JOIN transcript_rewrite_watermarks r ON r.session_id=w.session_id
|
|
16
18
|
WHERE w.chat_type IN (${chatTypes.map(() => "?").join(",")}) ORDER BY w.created_at,w.session_id`)
|
|
17
19
|
.all(...chatTypes).map(r => ({ sessionId: String(r.session_id), sessionKey: String(r.session_key),
|
|
18
20
|
chatType: r.chat_type, provider: r.provider === null ? undefined : String(r.provider),
|
|
19
21
|
accountId: r.account_id === null ? undefined : String(r.account_id),
|
|
20
|
-
conversationId: r.conversation_id === null ? undefined : String(r.conversation_id), startedAt: Number(r.started_at), changedAt: Number(r.changed_at)
|
|
22
|
+
conversationId: r.conversation_id === null ? undefined : String(r.conversation_id), startedAt: Number(r.started_at), changedAt: Number(r.changed_at),
|
|
23
|
+
// The host uses this append-stable watermark for transcript cache validation.
|
|
24
|
+
sourceRevision: r.generation === null ? undefined : JSON.stringify([r.generation, r.tail]) }));
|
|
21
25
|
}
|
|
22
26
|
finally {
|
|
23
27
|
db.close();
|
|
@@ -3,11 +3,13 @@ import type { SessionMetadata } from "./session-projector.js";
|
|
|
3
3
|
export type ExtractionSession = SessionMetadata & {
|
|
4
4
|
sessionKey: string;
|
|
5
5
|
changedAt?: number;
|
|
6
|
+
sourceRevision?: string;
|
|
6
7
|
};
|
|
7
8
|
type Checkpoint = {
|
|
8
9
|
cursor: string | null;
|
|
9
10
|
context: ExtractionMessage[];
|
|
10
11
|
since: number;
|
|
12
|
+
completeRevision: string | null;
|
|
11
13
|
};
|
|
12
14
|
export type ExtractedRecord = {
|
|
13
15
|
id: string;
|
|
@@ -42,6 +44,7 @@ export declare class ExtractionStore {
|
|
|
42
44
|
}[];
|
|
43
45
|
owner: string;
|
|
44
46
|
version: string;
|
|
47
|
+
completeRevision?: string;
|
|
45
48
|
}): number;
|
|
46
49
|
error(sessionId: string, code: string): void;
|
|
47
50
|
report(): {
|
|
@@ -20,7 +20,7 @@ export class ExtractionStore {
|
|
|
20
20
|
session_id TEXT PRIMARY KEY, metadata TEXT NOT NULL CHECK(json_valid(metadata)), cursor TEXT,
|
|
21
21
|
extract_since INTEGER NOT NULL,
|
|
22
22
|
context TEXT NOT NULL DEFAULT '[]' CHECK(json_valid(context)), updated_at INTEGER NOT NULL,
|
|
23
|
-
error TEXT
|
|
23
|
+
error TEXT, complete_revision TEXT
|
|
24
24
|
) STRICT;
|
|
25
25
|
CREATE TABLE IF NOT EXISTS extracted_memories (
|
|
26
26
|
id TEXT NOT NULL, revision INTEGER NOT NULL, session_id TEXT NOT NULL REFERENCES extraction_sessions(session_id),
|
|
@@ -33,6 +33,9 @@ export class ExtractionStore {
|
|
|
33
33
|
CREATE UNIQUE INDEX IF NOT EXISTS extracted_duplicate ON extracted_memories(session_id,digest) WHERE status='active';
|
|
34
34
|
INSERT OR IGNORE INTO memory_schema VALUES('extraction',1);
|
|
35
35
|
`);
|
|
36
|
+
if (!this.db.prepare("SELECT 1 FROM pragma_table_info('extraction_sessions') WHERE name='complete_revision'").get()) {
|
|
37
|
+
this.db.exec("ALTER TABLE extraction_sessions ADD COLUMN complete_revision TEXT");
|
|
38
|
+
}
|
|
36
39
|
this.db.prepare("INSERT OR IGNORE INTO extraction_worker(id,live_since) VALUES(1,?)").run(Date.now());
|
|
37
40
|
}
|
|
38
41
|
catch (error) {
|
|
@@ -58,23 +61,24 @@ export class ExtractionStore {
|
|
|
58
61
|
checkpoint(session, since) {
|
|
59
62
|
this.db.prepare(`INSERT INTO extraction_sessions(session_id,metadata,updated_at,extract_since) VALUES(?,?,?,?)
|
|
60
63
|
ON CONFLICT(session_id) DO UPDATE SET metadata=excluded.metadata`).run(session.sessionId, JSON.stringify(session), Date.now(), since ?? this.liveSince());
|
|
61
|
-
const row = this.db.prepare("SELECT cursor,context,extract_since FROM extraction_sessions WHERE session_id=?").get(session.sessionId);
|
|
64
|
+
const row = this.db.prepare("SELECT cursor,context,extract_since,complete_revision FROM extraction_sessions WHERE session_id=?").get(session.sessionId);
|
|
62
65
|
if (since !== undefined && since < Number(row.extract_since)) {
|
|
63
66
|
// A broader explicitly approved backfill replays this session. Existing facts are
|
|
64
67
|
// supplied to Luna and exact duplicates are ignored; history is never deleted.
|
|
65
|
-
this.db.prepare("UPDATE extraction_sessions SET cursor=NULL,context='[]',extract_since=? WHERE session_id=?").run(since, session.sessionId);
|
|
66
|
-
return { cursor: null, context: [], since };
|
|
68
|
+
this.db.prepare("UPDATE extraction_sessions SET cursor=NULL,context='[]',complete_revision=NULL,extract_since=? WHERE session_id=?").run(since, session.sessionId);
|
|
69
|
+
return { cursor: null, context: [], since, completeRevision: null };
|
|
67
70
|
}
|
|
68
|
-
return { cursor: row.cursor, context: JSON.parse(String(row.context)), since: Number(row.extract_since)
|
|
71
|
+
return { cursor: row.cursor, context: JSON.parse(String(row.context)), since: Number(row.extract_since),
|
|
72
|
+
completeRevision: row.complete_revision };
|
|
69
73
|
}
|
|
70
74
|
records(sessionId, paths) {
|
|
71
75
|
const keys = paths?.filter(path => /^[a-f0-9-]+\/[1-9]\d*$/.test(path)).map(path => path.split("/"));
|
|
72
76
|
if (keys && !keys.length)
|
|
73
77
|
return [];
|
|
74
78
|
return this.db.prepare(`SELECT m.*,s.metadata FROM extracted_memories m JOIN extraction_sessions s USING(session_id)
|
|
75
|
-
WHERE status='active'
|
|
79
|
+
WHERE status='active' ${sessionId === undefined ? "" : "AND session_id=?"}
|
|
76
80
|
${keys ? `AND (m.id,m.revision) IN (VALUES ${keys.map(() => "(?,?)").join(",")})` : ""}
|
|
77
|
-
ORDER BY observed_at,id`).all(sessionId
|
|
81
|
+
ORDER BY observed_at,id`).all(...(sessionId === undefined ? [] : [sessionId]), ...(keys?.flat() ?? []))
|
|
78
82
|
.map(r => ({ id: String(r.id), revision: Number(r.revision), text: String(r.text), sessionId: String(r.session_id),
|
|
79
83
|
observedAt: Number(r.observed_at), evidence: JSON.parse(String(r.evidence)), metadata: JSON.parse(String(r.metadata)), judgment: JSON.parse(String(r.judgment)) }));
|
|
80
84
|
}
|
|
@@ -83,7 +87,7 @@ export class ExtractionStore {
|
|
|
83
87
|
try {
|
|
84
88
|
this.renew(owner);
|
|
85
89
|
this.db.prepare("UPDATE extracted_memories SET status='withdrawn' WHERE session_id=? AND status='active'").run(sessionId);
|
|
86
|
-
this.db.prepare("UPDATE extraction_sessions SET cursor=?,context='[]',error=NULL WHERE session_id=?").run(cursor, sessionId);
|
|
90
|
+
this.db.prepare("UPDATE extraction_sessions SET cursor=?,context='[]',complete_revision=NULL,error=NULL WHERE session_id=?").run(cursor, sessionId);
|
|
87
91
|
this.db.exec("COMMIT");
|
|
88
92
|
}
|
|
89
93
|
catch (error) {
|
|
@@ -119,8 +123,8 @@ export class ExtractionStore {
|
|
|
119
123
|
.run(id, revision, params.session.sessionId, proposal.text.trim(), digest, JSON.stringify(proposal.evidence), observedAt, JSON.stringify(judgment), params.version, Date.now());
|
|
120
124
|
written++;
|
|
121
125
|
}
|
|
122
|
-
this.db.prepare("UPDATE extraction_sessions SET cursor=?,context=?,updated_at=?,error=NULL WHERE session_id=?")
|
|
123
|
-
.run(params.cursor, JSON.stringify(params.context), Date.now(), params.session.sessionId);
|
|
126
|
+
this.db.prepare("UPDATE extraction_sessions SET cursor=?,context=?,updated_at=?,complete_revision=?,error=NULL WHERE session_id=?")
|
|
127
|
+
.run(params.cursor, JSON.stringify(params.context), Date.now(), params.completeRevision ?? null, params.session.sessionId);
|
|
124
128
|
this.db.exec("COMMIT");
|
|
125
129
|
return written;
|
|
126
130
|
}
|
|
@@ -32,9 +32,10 @@ export async function runExtraction(params) {
|
|
|
32
32
|
const visible = new Set(sessions.map(s => s.sessionId));
|
|
33
33
|
if (params.sessionId && !visible.has(params.sessionId))
|
|
34
34
|
throw new Error("Unknown or unapproved extraction session");
|
|
35
|
-
for (const
|
|
36
|
-
if (!visible.has(
|
|
37
|
-
store.reset(
|
|
35
|
+
for (const { session_id } of store.db.prepare("SELECT DISTINCT session_id FROM extracted_memories WHERE status='active'").all()) {
|
|
36
|
+
if (!visible.has(String(session_id)))
|
|
37
|
+
store.reset(String(session_id), null, owner);
|
|
38
|
+
}
|
|
38
39
|
// Oldest last-checked first: bounded runs must not starve later sessions.
|
|
39
40
|
const checked = new Map(store.db.prepare("SELECT session_id,updated_at FROM extraction_sessions").all()
|
|
40
41
|
.map(r => [String(r.session_id), Number(r.updated_at)]));
|
|
@@ -48,6 +49,10 @@ export async function runExtraction(params) {
|
|
|
48
49
|
if (processed >= config.maxBatches)
|
|
49
50
|
break;
|
|
50
51
|
const checkpoint = store.checkpoint(session, params.since);
|
|
52
|
+
if (session.sourceRevision !== undefined && checkpoint.completeRevision === session.sourceRevision) {
|
|
53
|
+
unchanged++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
51
56
|
let stage = "read";
|
|
52
57
|
try {
|
|
53
58
|
const existing = store.records(session.sessionId);
|
|
@@ -69,7 +74,8 @@ export async function runExtraction(params) {
|
|
|
69
74
|
continue;
|
|
70
75
|
}
|
|
71
76
|
if (!page.entryCount) {
|
|
72
|
-
store.db.prepare("UPDATE extraction_sessions SET updated_at=? WHERE session_id=?")
|
|
77
|
+
store.db.prepare("UPDATE extraction_sessions SET updated_at=?,complete_revision=? WHERE session_id=?")
|
|
78
|
+
.run(Date.now(), page.exhausted ? session.sourceRevision ?? null : null, session.sessionId);
|
|
73
79
|
unchanged++;
|
|
74
80
|
continue;
|
|
75
81
|
}
|
|
@@ -82,17 +88,26 @@ export async function runExtraction(params) {
|
|
|
82
88
|
const proposals = newIds.length ? await (params.extract ?? extractWithLuna)(params.runtime, params.agentId, messages, newIds, existing.map(m => ({ id: m.id, text: m.text, observedAt: m.observedAt })), signal) : [];
|
|
83
89
|
const accepted = [];
|
|
84
90
|
stage = "validate";
|
|
85
|
-
for (
|
|
91
|
+
for (let start = 0; start < proposals.length; start += 4) {
|
|
86
92
|
signal.throwIfAborted();
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
if (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
// Isolated judgments overlap, but failed chunks drain before their lease is released.
|
|
94
|
+
const judgments = await Promise.allSettled(proposals.slice(start, start + 4).map(async (proposal) => ({ proposal,
|
|
95
|
+
judgment: await (params.validate ?? validateExtractedMemory)({ proposal, messages, newIds, existing, apiKey, signal, thresholds: config }) })));
|
|
96
|
+
const failure = judgments.find(result => result.status === "rejected");
|
|
97
|
+
if (failure)
|
|
98
|
+
throw failure.reason;
|
|
99
|
+
for (const result of judgments) {
|
|
100
|
+
if (result.status !== "fulfilled")
|
|
101
|
+
continue;
|
|
102
|
+
const { proposal, judgment } = result.value;
|
|
103
|
+
const observedAt = judgment.accepted ? Math.max(...proposal.evidence.map(e => messages.find(m => m.id === e.messageId).timestamp)) : 0;
|
|
104
|
+
const prior = existing.find(m => m.id === proposal.replaces);
|
|
105
|
+
if (judgment.accepted && (!prior || prior.observedAt <= observedAt))
|
|
106
|
+
accepted.push({ proposal: { ...proposal,
|
|
107
|
+
evidence: proposal.evidence.map(e => ({ ...e, messageId: messages.find(m => m.id === e.messageId).sourceMessageId ?? e.messageId })) }, judgment, observedAt });
|
|
108
|
+
else
|
|
109
|
+
rejectedCount++;
|
|
110
|
+
}
|
|
96
111
|
}
|
|
97
112
|
// A branch rewrite during inference invalidates its proposals. Appends are safe and processed next time.
|
|
98
113
|
const check = await readPage(params.agentId, params.agentName, session, page.fence);
|
|
@@ -107,7 +122,8 @@ export async function runExtraction(params) {
|
|
|
107
122
|
const context = extractionHistory(messages, config.historyMessages);
|
|
108
123
|
stage = "commit";
|
|
109
124
|
const written = store.commit({ session, expected: checkpoint.cursor, cursor: page.cursor,
|
|
110
|
-
context, accepted, owner, version: EXTRACTION_VERSION
|
|
125
|
+
context, accepted, owner, version: EXTRACTION_VERSION,
|
|
126
|
+
completeRevision: page.exhausted ? session.sourceRevision : undefined });
|
|
111
127
|
processed++;
|
|
112
128
|
acceptedCount += written;
|
|
113
129
|
}
|
package/dist/src/manager.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { reviewIndexedClaim } from "./evidence-review.js";
|
|
|
12
12
|
import { reviewClusterIngestion } from "./cluster-review.js";
|
|
13
13
|
import type { QueryPair } from "./query-contract.js";
|
|
14
14
|
export { expandSessionSearchHit } from "./memory-passage.js";
|
|
15
|
-
export type ManagerStore = Pick<QMDStore, "update" | "embed" | "getStatus" | "listCollections" | "
|
|
15
|
+
export type ManagerStore = Pick<QMDStore, "update" | "embed" | "getStatus" | "listCollections" | "vsearch" | "get" | "close">;
|
|
16
16
|
export type ManagerSessionConfig = {
|
|
17
17
|
agentId: string;
|
|
18
18
|
agentName: string;
|