@unblocklabs/unblock-memory 0.7.2 → 0.7.4
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/agent-transcript.d.ts +36 -5
- package/dist/src/agent-transcript.js +101 -11
- package/dist/src/analysis.js +25 -13
- package/dist/src/config.js +2 -17
- package/dist/src/inside-out-sources.js +21 -12
- package/dist/src/manager.d.ts +1 -1
- package/dist/src/manager.js +3 -6
- package/dist/src/people-primer-config.js +1 -4
- package/dist/src/people-store.js +7 -1
- package/dist/src/quality-audit.js +11 -2
- package/dist/src/response-episodes.d.ts +2 -2
- package/dist/src/response-episodes.js +51 -12
- package/dist/src/response-reviews.d.ts +1 -1
- package/dist/src/response-reviews.js +4 -2
- package/dist/src/response-store.js +1 -1
- package/dist/src/runtime.js +2 -1
- package/dist/src/session-sync.d.ts +1 -1
- package/dist/src/session-sync.js +28 -15
- package/dist/src/skill-whisperer.js +8 -10
- package/dist/src/training-input.d.ts +4 -0
- package/dist/src/training-input.js +8 -6
- package/dist/src/training-queries.d.ts +2 -0
- package/dist/src/training-store.d.ts +2 -8
- package/dist/src/training-store.js +15 -20
- package/dist/src/training.d.ts +2 -0
- package/dist/src/training.js +6 -1
- 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/memory-training.md +11 -8
- package/docs/response-audit.md +2 -2
- package/docs/retrieval.md +10 -4
- package/openclaw.plugin.json +1 -1
- package/package.json +6 -4
|
@@ -1,8 +1,39 @@
|
|
|
1
1
|
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
-
/**
|
|
2
|
+
/** Identity fence, not a payload-format allowlist. Required capabilities are checked below. */
|
|
3
3
|
export declare function agentTranscriptSchemaVersion(db: DatabaseSync, errorMessage?: string): number;
|
|
4
4
|
export declare function assertAgentTranscriptIdentity(db: DatabaseSync, agentId: string, version: number, errorMessage?: string): void;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
type AgentTranscriptRow = {
|
|
6
|
+
seq: number;
|
|
7
|
+
eventJson: string;
|
|
8
|
+
createdAt: number;
|
|
9
|
+
};
|
|
10
|
+
type TranscriptSnapshot = {
|
|
11
|
+
kind: "ready";
|
|
12
|
+
rows: AgentTranscriptRow[];
|
|
13
|
+
} | {
|
|
14
|
+
kind: "cold";
|
|
15
|
+
} | {
|
|
16
|
+
kind: "oversized";
|
|
17
|
+
} | {
|
|
18
|
+
kind: "unreadable";
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* One read-only storage boundary for full-fidelity active transcripts. The public
|
|
22
|
+
* SDK's full reader may restore cold archives; its read-only catalog truncates and
|
|
23
|
+
* redacts content. Neither currently provides the contract these consumers need.
|
|
24
|
+
* Keep payload SQL here until the SDK offers a bounded, full-fidelity read-only API.
|
|
25
|
+
*/
|
|
26
|
+
export declare class AgentTranscriptReader {
|
|
27
|
+
#private;
|
|
28
|
+
readonly db: DatabaseSync;
|
|
29
|
+
constructor(db: DatabaseSync, agentId: string, errorMessage?: string);
|
|
30
|
+
/** Include storage availability in incremental fingerprints, not just hot row counts. */
|
|
31
|
+
coldSql(sessionIdExpression: string): string;
|
|
32
|
+
/** Caller owns a transaction so metadata, cold marker, bounds and rows share one snapshot. */
|
|
33
|
+
read(sessionId: string, limits?: {
|
|
34
|
+
maxEvents?: number;
|
|
35
|
+
maxBytes?: number;
|
|
36
|
+
maxSeq?: number;
|
|
37
|
+
}): TranscriptSnapshot;
|
|
38
|
+
}
|
|
39
|
+
export {};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
import { zstdDecompressSync } from "node:zlib";
|
|
2
|
+
/** Identity fence, not a payload-format allowlist. Required capabilities are checked below. */
|
|
2
3
|
export function agentTranscriptSchemaVersion(db, errorMessage) {
|
|
3
4
|
const version = db.prepare("PRAGMA user_version").get()?.user_version;
|
|
4
|
-
if (typeof version !== "number" || !
|
|
5
|
-
throw new Error(errorMessage ?? "unsupported OpenClaw agent database schema: expected
|
|
5
|
+
if (typeof version !== "number" || !Number.isSafeInteger(version) || version < 17) {
|
|
6
|
+
throw new Error(errorMessage ?? "unsupported OpenClaw agent database schema: expected 17 or newer, " +
|
|
6
7
|
`found ${String(version ?? "unknown")}`);
|
|
7
8
|
}
|
|
8
9
|
return version;
|
|
@@ -18,12 +19,101 @@ export function assertAgentTranscriptIdentity(db, agentId, version, errorMessage
|
|
|
18
19
|
fail(`OpenClaw agent database belongs to ${String(meta?.agentId)}, not ${agentId}`);
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
-
assertAgentTranscriptIdentity(db, agentId, agentTranscriptSchemaVersion(db, errorMessage), errorMessage);
|
|
23
|
-
}
|
|
24
|
-
export const ACTIVE_EVENTS_FROM = `FROM session_transcript_active_events a
|
|
22
|
+
const ACTIVE_EVENTS_FROM = `FROM session_transcript_active_events a
|
|
25
23
|
JOIN transcript_events e ON e.session_id=a.session_id AND e.seq=a.event_seq`;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
const MAX_COMPRESSED_BYTES = 4 * 1024 * 1024;
|
|
25
|
+
const utf8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
26
|
+
/**
|
|
27
|
+
* One read-only storage boundary for full-fidelity active transcripts. The public
|
|
28
|
+
* SDK's full reader may restore cold archives; its read-only catalog truncates and
|
|
29
|
+
* redacts content. Neither currently provides the contract these consumers need.
|
|
30
|
+
* Keep payload SQL here until the SDK offers a bounded, full-fidelity read-only API.
|
|
31
|
+
*/
|
|
32
|
+
export class AgentTranscriptReader {
|
|
33
|
+
db;
|
|
34
|
+
#schemaVersion;
|
|
35
|
+
#compressed;
|
|
36
|
+
#cold;
|
|
37
|
+
#utf8Storage;
|
|
38
|
+
constructor(db, agentId, errorMessage) {
|
|
39
|
+
this.db = db;
|
|
40
|
+
const version = agentTranscriptSchemaVersion(db, errorMessage);
|
|
41
|
+
this.#schemaVersion = version;
|
|
42
|
+
this.#utf8Storage = db.prepare("PRAGMA encoding").get()?.encoding === "UTF-8";
|
|
43
|
+
assertAgentTranscriptIdentity(db, agentId, version, errorMessage);
|
|
44
|
+
const required = {
|
|
45
|
+
transcript_events: ["session_id", "seq", "event_json", "created_at"],
|
|
46
|
+
session_transcript_active_events: ["session_id", "active_position", "event_seq"],
|
|
47
|
+
};
|
|
48
|
+
const columns = (table) => new Set(db.prepare(`PRAGMA table_info(${table})`).all().map(row => row.name));
|
|
49
|
+
for (const [table, names] of Object.entries(required)) {
|
|
50
|
+
const found = columns(table);
|
|
51
|
+
if (names.some(name => !found.has(name)))
|
|
52
|
+
throw new Error(`Unsupported transcript capabilities: ${table}`);
|
|
53
|
+
}
|
|
54
|
+
const payload = columns("transcript_events");
|
|
55
|
+
this.#compressed = payload.has("event_zstd") && payload.has("event_utf8_bytes");
|
|
56
|
+
if ((version >= 23 || payload.has("event_zstd") || payload.has("event_utf8_bytes")) && !this.#compressed) {
|
|
57
|
+
throw new Error("Unsupported transcript compression capabilities");
|
|
58
|
+
}
|
|
59
|
+
this.#cold = columns("session_transcript_cold_archives").has("session_id");
|
|
60
|
+
if (version >= 20 && !this.#cold)
|
|
61
|
+
throw new Error("Unsupported transcript cold-storage capabilities");
|
|
62
|
+
}
|
|
63
|
+
/** Include storage availability in incremental fingerprints, not just hot row counts. */
|
|
64
|
+
coldSql(sessionIdExpression) {
|
|
65
|
+
return this.#cold ? `EXISTS (SELECT 1 FROM session_transcript_cold_archives cold WHERE cold.session_id=${sessionIdExpression})` : "0";
|
|
66
|
+
}
|
|
67
|
+
/** Caller owns a transaction so metadata, cold marker, bounds and rows share one snapshot. */
|
|
68
|
+
read(sessionId, limits = {}) {
|
|
69
|
+
if (!this.db.isTransaction)
|
|
70
|
+
throw new Error("Transcript read requires a snapshot transaction");
|
|
71
|
+
// Long-lived audit/source readers may outlive a host migration. Reopen before
|
|
72
|
+
// trusting cached capabilities; absence of old hot rows is not deletion proof.
|
|
73
|
+
if (this.db.prepare("PRAGMA user_version").get()?.user_version !== this.#schemaVersion)
|
|
74
|
+
return { kind: "unreadable" };
|
|
75
|
+
if (this.#cold && this.db.prepare("SELECT 1 FROM session_transcript_cold_archives WHERE session_id=?").get(sessionId)) {
|
|
76
|
+
return { kind: "cold" };
|
|
77
|
+
}
|
|
78
|
+
const maxEvents = limits.maxEvents ?? 50_000, maxBytes = limits.maxBytes ?? 64 * 1024 * 1024;
|
|
79
|
+
const maxSeq = limits.maxSeq ?? Number.MAX_SAFE_INTEGER;
|
|
80
|
+
// SQLite's BLOB length measures the database encoding, not decoded UTF-8.
|
|
81
|
+
// Non-UTF-8 stores still enforce the byte budget below during bounded iteration.
|
|
82
|
+
const bytes = !this.#utf8Storage ? "0" : this.#compressed
|
|
83
|
+
? "COALESCE(length(CAST(e.event_json AS BLOB)),e.event_utf8_bytes,0)" : "length(CAST(e.event_json AS BLOB))";
|
|
84
|
+
const size = this.db.prepare(`SELECT COUNT(*) n,COALESCE(SUM(${bytes}),0) bytes
|
|
85
|
+
${ACTIVE_EVENTS_FROM} WHERE a.session_id=? AND e.seq<=?`).get(sessionId, maxSeq);
|
|
86
|
+
if (Number(size.n) > maxEvents || Number(size.bytes) > maxBytes)
|
|
87
|
+
return { kind: "oversized" };
|
|
88
|
+
const rows = [];
|
|
89
|
+
let totalBytes = 0;
|
|
90
|
+
for (const row of this.db.prepare(`SELECT e.seq,e.event_json eventJson,e.created_at createdAt
|
|
91
|
+
${this.#compressed ? ",e.event_zstd compressed,e.event_utf8_bytes rawBytes" : ""}
|
|
92
|
+
${ACTIVE_EVENTS_FROM} WHERE a.session_id=? AND e.seq<=? ORDER BY a.active_position`).iterate(sessionId, maxSeq)) {
|
|
93
|
+
let eventJson;
|
|
94
|
+
if (typeof row.eventJson === "string")
|
|
95
|
+
eventJson = row.eventJson;
|
|
96
|
+
else {
|
|
97
|
+
const compressed = row.compressed, rawBytes = row.rawBytes;
|
|
98
|
+
if (!(compressed instanceof Uint8Array) || !compressed.byteLength || compressed.byteLength > MAX_COMPRESSED_BYTES ||
|
|
99
|
+
typeof rawBytes !== "number" || !Number.isSafeInteger(rawBytes) || rawBytes <= 0 || rawBytes > MAX_COMPRESSED_BYTES) {
|
|
100
|
+
return { kind: "unreadable" };
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const decoded = zstdDecompressSync(compressed, { maxOutputLength: rawBytes });
|
|
104
|
+
if (decoded.byteLength !== rawBytes)
|
|
105
|
+
return { kind: "unreadable" };
|
|
106
|
+
eventJson = utf8.decode(decoded);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return { kind: "unreadable" };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
totalBytes += Buffer.byteLength(eventJson);
|
|
113
|
+
if (totalBytes > maxBytes)
|
|
114
|
+
return { kind: "oversized" };
|
|
115
|
+
rows.push({ seq: Number(row.seq), eventJson, createdAt: Number(row.createdAt) });
|
|
116
|
+
}
|
|
117
|
+
return { kind: "ready", rows };
|
|
118
|
+
}
|
|
119
|
+
}
|
package/dist/src/analysis.js
CHANGED
|
@@ -260,8 +260,8 @@ function byteSlice(text, maxBytes) {
|
|
|
260
260
|
return "";
|
|
261
261
|
return bytes.subarray(0, maxBytes - 3).toString("utf8").replace(/\uFFFD$/u, "") + "…";
|
|
262
262
|
}
|
|
263
|
-
function
|
|
264
|
-
|
|
263
|
+
function representativeOrder(clusterId) {
|
|
264
|
+
return clusterId === -1
|
|
265
265
|
? "m.outlier_score DESC, m.hash, m.seq"
|
|
266
266
|
: `CASE WHEN m.representative_rank IS NULL THEN 1 ELSE 0 END,
|
|
267
267
|
m.representative_rank,
|
|
@@ -269,9 +269,11 @@ function members(db, runId, clusterId, limit, offset = 0, sort = "representative
|
|
|
269
269
|
m.outlier_score,
|
|
270
270
|
m.hash,
|
|
271
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 = {}) {
|
|
272
274
|
const score = clusterId === -1 ? "m.outlier_score" : "m.probability";
|
|
273
275
|
const order = {
|
|
274
|
-
representative: representativeOrder,
|
|
276
|
+
representative: representativeOrder(clusterId),
|
|
275
277
|
score_desc: `${score} DESC, m.hash, m.seq`,
|
|
276
278
|
score_asc: `${score} ASC, m.hash, m.seq`,
|
|
277
279
|
date_desc: "julianday(COALESCE(m.event_time, m.source_modified_at)) DESC, m.hash, m.seq",
|
|
@@ -395,24 +397,34 @@ function readMetadata(run) {
|
|
|
395
397
|
}
|
|
396
398
|
return { stale: false, staleSince: null, analyzedAt: run.completed_at };
|
|
397
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
|
+
}
|
|
398
418
|
function toSummary(db, run, row, includePreview, previewBytes = 600, aliasLimit = MAX_TOTAL_ALIASES) {
|
|
399
419
|
const preview = includePreview
|
|
400
|
-
?
|
|
420
|
+
? clusterPreview(db, run.id, row.cluster_id, previewBytes, aliasLimit)
|
|
401
421
|
: undefined;
|
|
402
422
|
return {
|
|
403
423
|
clusterId: clusterReference(run.id, row.cluster_id),
|
|
404
424
|
size: row.size,
|
|
405
425
|
availableSize: row.available_size,
|
|
406
426
|
meanProbability: row.mean_probability,
|
|
407
|
-
...(preview ? {
|
|
408
|
-
preview: {
|
|
409
|
-
hash: preview.hash,
|
|
410
|
-
seq: preview.seq,
|
|
411
|
-
probability: preview.probability,
|
|
412
|
-
text: preview.text,
|
|
413
|
-
sourcePaths: preview.sourcePaths,
|
|
414
|
-
},
|
|
415
|
-
} : {}),
|
|
427
|
+
...(preview ? { preview } : {}),
|
|
416
428
|
};
|
|
417
429
|
}
|
|
418
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
|
}
|
|
@@ -5,7 +5,7 @@ import { createZstdDecompress, zstdDecompressSync } from "node:zlib";
|
|
|
5
5
|
import { createInterface } from "node:readline";
|
|
6
6
|
import { PassThrough } from "node:stream";
|
|
7
7
|
import { pipeline } from "node:stream/promises";
|
|
8
|
-
import {
|
|
8
|
+
import { AgentTranscriptReader } from "./agent-transcript.js";
|
|
9
9
|
/** Files contain branch trees; keep the selected leaf, not abandoned sibling conversations. */
|
|
10
10
|
function selectedBranch(events) {
|
|
11
11
|
const nodes = new Map(events.filter(e => e.id).map(e => [e.id, e]));
|
|
@@ -57,10 +57,11 @@ export async function* readInsideOutSources(paths, errors) {
|
|
|
57
57
|
const db = new DatabaseSync(paths.databasePath, { readOnly: true });
|
|
58
58
|
try {
|
|
59
59
|
db.exec("PRAGMA busy_timeout=1000");
|
|
60
|
-
|
|
60
|
+
const transcripts = new AgentTranscriptReader(db, paths.agentId);
|
|
61
61
|
const sessions = db.prepare(`SELECT w.session_id sessionId, COALESCE(w.channel,c.channel,'local') channel,
|
|
62
62
|
COALESCE(w.account_id,c.account_id,'') account, COALESCE(r.generation,'') revision,
|
|
63
|
-
COALESCE((SELECT MAX(seq) FROM transcript_events WHERE session_id=w.session_id),0) tail
|
|
63
|
+
COALESCE((SELECT MAX(seq) FROM transcript_events WHERE session_id=w.session_id),0) tail,
|
|
64
|
+
${transcripts.coldSql("w.session_id")} cold FROM session_windows w
|
|
64
65
|
LEFT JOIN conversations c ON c.conversation_id=w.primary_conversation_id
|
|
65
66
|
LEFT JOIN transcript_rewrite_watermarks r ON r.session_id=w.session_id
|
|
66
67
|
WHERE (? IS NULL OR w.session_id=?) ORDER BY w.started_at,w.session_id`).all(paths.sessionId ?? null, paths.sessionId ?? null);
|
|
@@ -83,15 +84,23 @@ export async function* readInsideOutSources(paths, errors) {
|
|
|
83
84
|
}
|
|
84
85
|
for (const s of sessions) {
|
|
85
86
|
const sessionId = String(s.sessionId), source = `live:${sessionId}`;
|
|
86
|
-
yield { source, sessionId, revision: String(s.revision), signature: JSON.stringify([s.revision, s.tail]), read: () => {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
87
|
+
yield { source, sessionId, revision: String(s.revision), signature: JSON.stringify([s.revision, s.tail, s.cold]), read: () => {
|
|
88
|
+
db.exec("BEGIN");
|
|
89
|
+
try {
|
|
90
|
+
const snapshot = transcripts.read(sessionId, { maxSeq: Number(s.tail) });
|
|
91
|
+
if (snapshot.kind !== "ready")
|
|
92
|
+
throw new Error(`Transcript unavailable: ${snapshot.kind}`);
|
|
93
|
+
const events = snapshot.rows.map(row => {
|
|
94
|
+
const event = JSON.parse(row.eventJson);
|
|
95
|
+
event.seq = row.seq;
|
|
96
|
+
event.timestamp ??= new Date(row.createdAt).toISOString();
|
|
97
|
+
return event;
|
|
98
|
+
});
|
|
99
|
+
return { sessionId, source, events, ...metadata.get(sessionId) };
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
db.exec("COMMIT");
|
|
103
|
+
}
|
|
95
104
|
} };
|
|
96
105
|
}
|
|
97
106
|
}
|
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" | "vsearch" | "get" | "
|
|
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;
|
package/dist/src/manager.js
CHANGED
|
@@ -991,15 +991,12 @@ export class QmdMemoryManager {
|
|
|
991
991
|
}
|
|
992
992
|
await this.#operationChain;
|
|
993
993
|
const store = await this.#getStore();
|
|
994
|
-
const doc = await store.get(safe.normalized);
|
|
995
|
-
if ("error" in doc || doc.filepath !== safe.normalized) {
|
|
994
|
+
const doc = await store.get(safe.normalized, { includeBody: true });
|
|
995
|
+
if ("error" in doc || doc.filepath !== safe.normalized || doc.body === undefined) {
|
|
996
996
|
return { status: "not_found", text: "", path: params.relPath };
|
|
997
997
|
}
|
|
998
|
-
const content = await store.getDocumentBody(safe.normalized);
|
|
999
|
-
if (content === null)
|
|
1000
|
-
return { status: "not_found", text: "", path: params.relPath };
|
|
1001
998
|
return buildReadResult({
|
|
1002
|
-
content,
|
|
999
|
+
content: doc.body,
|
|
1003
1000
|
path: safe.normalized,
|
|
1004
1001
|
from: params.from,
|
|
1005
1002
|
lines: params.lines,
|
|
@@ -10,12 +10,9 @@ export const peoplePrimerSchema = Type.Object({
|
|
|
10
10
|
timeoutMs: Type.Integer({ minimum: 1, maximum: 60000, default: 30000 }),
|
|
11
11
|
}, { additionalProperties: false });
|
|
12
12
|
export function resolvePeoplePrimer(value, corpora, peopleEnabled) {
|
|
13
|
-
if (value === undefined)
|
|
14
|
-
return { enabled: false, corpora: [], hitsPerQuestion: 30,
|
|
15
|
-
minScore: 0.35, minUsefulness: 0.8, maxEvidencePerQuestion: 3, timeoutMs: 30000 };
|
|
16
13
|
let config;
|
|
17
14
|
try {
|
|
18
|
-
const withDefaults = Value.Default(peoplePrimerSchema, value);
|
|
15
|
+
const withDefaults = Value.Default(peoplePrimerSchema, value === undefined ? {} : value);
|
|
19
16
|
if (!Value.Check(peoplePrimerSchema, withDefaults))
|
|
20
17
|
throw new Error("Invalid config");
|
|
21
18
|
config = withDefaults;
|
package/dist/src/people-store.js
CHANGED
|
@@ -147,6 +147,7 @@ export class PeopleStore {
|
|
|
147
147
|
#db;
|
|
148
148
|
#maxOpenTodos;
|
|
149
149
|
#maxBlurbChars;
|
|
150
|
+
#primerCacheReady = false;
|
|
150
151
|
constructor(path, options) {
|
|
151
152
|
this.#db = openMemoryDatabase(path);
|
|
152
153
|
this.#maxOpenTodos = options.maxOpenTodos;
|
|
@@ -167,12 +168,17 @@ export class PeopleStore {
|
|
|
167
168
|
// Derived, bounded cache: no source text or credentials. Kept outside the
|
|
168
169
|
// authoritative dossier schema so older plugin versions can still open it.
|
|
169
170
|
#ensurePrimerCache() {
|
|
171
|
+
if (this.#primerCacheReady)
|
|
172
|
+
return;
|
|
170
173
|
this.#db.exec(`CREATE TABLE IF NOT EXISTS person_primer_judgments (
|
|
171
174
|
cache_key TEXT PRIMARY KEY,
|
|
172
175
|
person_id TEXT NOT NULL REFERENCES people(id) ON DELETE CASCADE,
|
|
173
176
|
judgment_json TEXT NOT NULL,
|
|
174
177
|
created_at TEXT NOT NULL
|
|
175
|
-
) STRICT
|
|
178
|
+
) STRICT;
|
|
179
|
+
CREATE INDEX IF NOT EXISTS person_primer_judgments_created
|
|
180
|
+
ON person_primer_judgments(created_at DESC, cache_key)`);
|
|
181
|
+
this.#primerCacheReady = true;
|
|
176
182
|
}
|
|
177
183
|
getPrimerJudgment(key) {
|
|
178
184
|
this.#ensurePrimerCache();
|
|
@@ -50,7 +50,7 @@ export async function auditQualityPage(params) {
|
|
|
50
50
|
return result("ok", true);
|
|
51
51
|
const limit = Math.max(1, Math.min(20, Math.floor(params.limit ?? 10)));
|
|
52
52
|
const rows = db.prepare(`SELECT d.id AS document_id, cv.seq, d.collection, d.path,
|
|
53
|
-
d.hash, cv.pos, cv.chunk_len
|
|
53
|
+
d.hash, cv.pos, cv.chunk_len
|
|
54
54
|
FROM documents d JOIN content c ON c.hash = d.hash JOIN content_vectors cv ON cv.hash = d.hash
|
|
55
55
|
WHERE d.active = 1 AND d.collection IN (${[...sources].map(() => "?").join(",")})
|
|
56
56
|
AND (d.id > ? OR (d.id = ? AND cv.seq > ?))
|
|
@@ -59,9 +59,18 @@ export async function auditQualityPage(params) {
|
|
|
59
59
|
WHERE d.id = ? AND d.active = 1 AND d.collection = ? AND d.path = ? AND d.hash = ?
|
|
60
60
|
AND cv.seq = ? AND cv.pos = ? AND cv.chunk_len = ?`);
|
|
61
61
|
const page = rows.slice(0, limit);
|
|
62
|
+
const bodies = new Map();
|
|
63
|
+
const readBody = db.prepare("SELECT doc FROM content WHERE hash = ?");
|
|
62
64
|
try {
|
|
63
65
|
check();
|
|
64
|
-
const batch = page.map(
|
|
66
|
+
const batch = page.map(metadata => {
|
|
67
|
+
let doc = bodies.get(metadata.hash);
|
|
68
|
+
if (doc === undefined) {
|
|
69
|
+
// Content is hash-addressed; this capture does not yield before inference.
|
|
70
|
+
doc = readBody.get(metadata.hash).doc;
|
|
71
|
+
bodies.set(metadata.hash, doc);
|
|
72
|
+
}
|
|
73
|
+
const row = { ...metadata, doc };
|
|
65
74
|
const source = sources.get(row.collection);
|
|
66
75
|
const text = row.doc.slice(row.pos, row.pos + row.chunk_len);
|
|
67
76
|
const fingerprint = chunkFingerprint(text);
|
|
@@ -48,12 +48,12 @@ export declare function responseEpisodes(session: ResponseSession, rows: readonl
|
|
|
48
48
|
episodes: ResponseEpisode[];
|
|
49
49
|
coverage: ResponseCoverage;
|
|
50
50
|
};
|
|
51
|
-
/** Bounded, read-only active transcript snapshot;
|
|
51
|
+
/** Bounded, read-only active transcript snapshot; cold history is deferred, inactive branches excluded. */
|
|
52
52
|
export declare class ResponseTranscriptReader {
|
|
53
53
|
#private;
|
|
54
54
|
constructor(path: string, agentId: string);
|
|
55
55
|
sessions(config: ResponseAuditConfig, now: number, after?: string): ResponseSession[];
|
|
56
|
-
/** null = confirmed absent/ineligible; undefined = over budget,
|
|
56
|
+
/** null = confirmed absent/ineligible; undefined = unavailable/over budget, never evidence of deletion. */
|
|
57
57
|
read(input: ResponseSession | string, config: ResponseAuditConfig): (ReturnType<typeof responseEpisodes> & {
|
|
58
58
|
revision: string;
|
|
59
59
|
}) | null | undefined;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { DatabaseSync } from "node:sqlite";
|
|
3
|
-
import {
|
|
3
|
+
import { AgentTranscriptReader } from "./agent-transcript.js";
|
|
4
4
|
import { messageText } from "./whisperer-context.js";
|
|
5
5
|
import { responseUserText } from "./response-text.js";
|
|
6
6
|
export const RESPONSE_EXTRACTOR_VERSION = 5;
|
|
@@ -176,14 +176,15 @@ export function responseEpisodes(session, rows, config) {
|
|
|
176
176
|
}
|
|
177
177
|
return { episodes, coverage };
|
|
178
178
|
}
|
|
179
|
-
/** Bounded, read-only active transcript snapshot;
|
|
179
|
+
/** Bounded, read-only active transcript snapshot; cold history is deferred, inactive branches excluded. */
|
|
180
180
|
export class ResponseTranscriptReader {
|
|
181
181
|
#db;
|
|
182
|
+
#transcripts;
|
|
182
183
|
constructor(path, agentId) {
|
|
183
184
|
this.#db = new DatabaseSync(path, { readOnly: true });
|
|
184
185
|
try {
|
|
185
186
|
this.#db.exec("PRAGMA query_only=ON; PRAGMA busy_timeout=1000");
|
|
186
|
-
|
|
187
|
+
this.#transcripts = new AgentTranscriptReader(this.#db, agentId, "Unsupported response-audit transcript schema or agent");
|
|
187
188
|
}
|
|
188
189
|
catch (error) {
|
|
189
190
|
this.#db.close();
|
|
@@ -191,17 +192,55 @@ export class ResponseTranscriptReader {
|
|
|
191
192
|
}
|
|
192
193
|
}
|
|
193
194
|
sessions(config, now, after = "") {
|
|
194
|
-
|
|
195
|
+
// Page metadata candidates, then apply the approved-sender filter to decoded
|
|
196
|
+
// events before the audit cap. Unrelated Slack traffic must not hide previews.
|
|
197
|
+
const candidates = this.#db.prepare(`SELECT w.session_id sessionId, COALESCE(w.account_id,c.account_id,'') accountId,
|
|
195
198
|
w.chat_type chatType, w.primary_conversation_id conversationId
|
|
196
199
|
FROM session_windows w JOIN conversations c ON c.conversation_id=w.primary_conversation_id
|
|
197
200
|
WHERE COALESCE(w.channel,c.channel)='slack' AND w.chat_type IN (${config.chatTypes.map(() => "?").join(",")})
|
|
198
201
|
AND w.session_id>?
|
|
199
202
|
AND EXISTS (SELECT 1 FROM transcript_events e JOIN session_transcript_active_events a
|
|
200
|
-
ON a.session_id=e.session_id AND a.event_seq=e.seq WHERE e.session_id=w.session_id AND e.created_at >= ?
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
ON a.session_id=e.session_id AND a.event_seq=e.seq WHERE e.session_id=w.session_id AND e.created_at >= ?)
|
|
204
|
+
ORDER BY w.session_id LIMIT 101`);
|
|
205
|
+
const since = now - config.lookbackDays * 86400_000;
|
|
206
|
+
const sessions = [];
|
|
207
|
+
this.#db.exec("BEGIN");
|
|
208
|
+
try {
|
|
209
|
+
while (sessions.length < 101) {
|
|
210
|
+
const page = candidates.all(...config.chatTypes, after, since);
|
|
211
|
+
for (const session of page) {
|
|
212
|
+
const snapshot = this.#transcripts.read(session.sessionId, { maxEvents: MAX_EVENTS, maxBytes: MAX_SESSION_BYTES });
|
|
213
|
+
// Saved unavailable sessions are reconciled separately by the caller.
|
|
214
|
+
if (snapshot.kind !== "ready")
|
|
215
|
+
continue;
|
|
216
|
+
const approved = snapshot.rows.some(row => {
|
|
217
|
+
if (row.createdAt < since)
|
|
218
|
+
return false;
|
|
219
|
+
let event;
|
|
220
|
+
try {
|
|
221
|
+
event = record(JSON.parse(row.eventJson));
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
const message = record(event?.message), sender = record(message?.__openclaw)?.senderId;
|
|
227
|
+
return event?.type === "message" && message?.role === "user" &&
|
|
228
|
+
typeof sender === "string" && config.senderIds.includes(sender);
|
|
229
|
+
});
|
|
230
|
+
if (approved)
|
|
231
|
+
sessions.push(session);
|
|
232
|
+
if (sessions.length === 101)
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
if (page.length < 101)
|
|
236
|
+
break;
|
|
237
|
+
after = page.at(-1).sessionId;
|
|
238
|
+
}
|
|
239
|
+
return sessions;
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
this.#db.exec("COMMIT");
|
|
243
|
+
}
|
|
205
244
|
}
|
|
206
245
|
read(input, config, previousRevision) {
|
|
207
246
|
this.#db.exec("BEGIN");
|
|
@@ -216,10 +255,10 @@ export class ResponseTranscriptReader {
|
|
|
216
255
|
return null;
|
|
217
256
|
const session = { sessionId, accountId: String(window.accountId),
|
|
218
257
|
conversationId: String(window.conversationId), chatType: String(window.chatType) };
|
|
219
|
-
const
|
|
220
|
-
if (
|
|
258
|
+
const snapshot = this.#transcripts.read(session.sessionId, { maxEvents: MAX_EVENTS, maxBytes: MAX_SESSION_BYTES });
|
|
259
|
+
if (snapshot.kind !== "ready")
|
|
221
260
|
return undefined;
|
|
222
|
-
const rows =
|
|
261
|
+
const rows = snapshot.rows;
|
|
223
262
|
// Exact active content catches in-place edits and branch changes even when writer
|
|
224
263
|
// watermarks are absent. Raw text is never retained in the checkpoint database.
|
|
225
264
|
const revision = hash([RESPONSE_EXTRACTOR_VERSION, session, config.historyMessages, [...config.senderIds].sort(), rows]);
|
|
@@ -8,7 +8,7 @@ export declare class ResponseReviews {
|
|
|
8
8
|
constructor(db: DatabaseSync);
|
|
9
9
|
sync(cohort: string, episodeId: string, inputHash: string, result: ResponseResult, now: number): void;
|
|
10
10
|
refresh(cohort: string, since: number): number;
|
|
11
|
-
reconcile(cohort: string): void;
|
|
11
|
+
reconcile(cohort: string, sessionId: string): void;
|
|
12
12
|
list(cohort: string, id?: string): {
|
|
13
13
|
id: string;
|
|
14
14
|
episodeId: string;
|
|
@@ -68,11 +68,13 @@ export class ResponseReviews {
|
|
|
68
68
|
throw error;
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
|
-
reconcile(cohort) {
|
|
71
|
+
reconcile(cohort, sessionId) {
|
|
72
72
|
this.db.prepare(`UPDATE response_review_evidence AS e SET active=CASE WHEN EXISTS (
|
|
73
73
|
SELECT 1 FROM response_review_tasks t JOIN response_results r ON r.id=t.episode_id
|
|
74
74
|
WHERE t.id=e.task_id AND r.cohort=e.cohort AND r.input_hash=e.input_hash AND r.active=1 AND r.status='ok')
|
|
75
|
-
THEN 1 ELSE 0 END WHERE cohort=? AND active>=0
|
|
75
|
+
THEN 1 ELSE 0 END WHERE cohort=? AND active>=0 AND task_id IN (
|
|
76
|
+
SELECT t.id FROM response_results r JOIN response_review_tasks t ON t.episode_id=r.id
|
|
77
|
+
WHERE r.cohort=? AND r.session_id=?)`).run(cohort, cohort, sessionId);
|
|
76
78
|
}
|
|
77
79
|
list(cohort, id) {
|
|
78
80
|
return this.db.prepare(`SELECT t.*,e.input_hash,e.active,e.detail FROM response_review_tasks t
|
|
@@ -87,7 +87,7 @@ export class ResponseAuditStore {
|
|
|
87
87
|
result=CASE WHEN input_hash=excluded.input_hash THEN result ELSE NULL END`);
|
|
88
88
|
for (const e of episodes)
|
|
89
89
|
insert.run(cohort, e.id, sessionId, e.inputHash, e.timestamp);
|
|
90
|
-
this.reviews.reconcile(cohort);
|
|
90
|
+
this.reviews.reconcile(cohort, sessionId);
|
|
91
91
|
this.#db.exec("COMMIT");
|
|
92
92
|
}
|
|
93
93
|
catch (error) {
|
package/dist/src/runtime.js
CHANGED
|
@@ -106,7 +106,8 @@ export class QmdMemoryRuntime {
|
|
|
106
106
|
return { manager: await pending };
|
|
107
107
|
}
|
|
108
108
|
catch (error) {
|
|
109
|
-
this.#managers.
|
|
109
|
+
if (this.#managers.get(params.agentId) === pending)
|
|
110
|
+
this.#managers.delete(params.agentId);
|
|
110
111
|
return { manager: null, error: error instanceof Error ? error.message : String(error) };
|
|
111
112
|
}
|
|
112
113
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ChatType } from "./config.js";
|
|
2
2
|
import { type SessionMetadata, type SessionProjectionInput, type SessionMessageSpan } from "./session-projector.js";
|
|
3
|
-
export declare const PROJECTOR_VERSION =
|
|
3
|
+
export declare const PROJECTOR_VERSION = 8;
|
|
4
4
|
type IndexedSession = SessionMetadata & {
|
|
5
5
|
sourceGeneration: string;
|
|
6
6
|
maxSeq: number;
|
package/dist/src/session-sync.js
CHANGED
|
@@ -3,10 +3,10 @@ import { existsSync, lstatSync, readFileSync, statSync } from "node:fs";
|
|
|
3
3
|
import { chmod, mkdir, readFile, rename, unlink, utimes, writeFile } from "node:fs/promises";
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { DatabaseSync } from "node:sqlite";
|
|
6
|
-
import {
|
|
6
|
+
import { AgentTranscriptReader, agentTranscriptSchemaVersion, assertAgentTranscriptIdentity } from "./agent-transcript.js";
|
|
7
7
|
import { projectSessionDocument, sessionDocumentPath, } from "./session-projector.js";
|
|
8
8
|
const MANIFEST_VERSION = 1;
|
|
9
|
-
export const PROJECTOR_VERSION =
|
|
9
|
+
export const PROJECTOR_VERSION = 8;
|
|
10
10
|
// Source lives in src/, published code in dist/src/. Read our own pinned dependency
|
|
11
11
|
// metadata, not QMD internals (which may also be substituted by runtime inspectors).
|
|
12
12
|
const sourcePackage = new URL("../package.json", import.meta.url);
|
|
@@ -91,6 +91,7 @@ function readSnapshot(params) {
|
|
|
91
91
|
try {
|
|
92
92
|
db.exec("PRAGMA query_only = ON; PRAGMA busy_timeout = 5000; BEGIN");
|
|
93
93
|
assertSchema(db, params.agentId);
|
|
94
|
+
const transcripts = new AgentTranscriptReader(db, params.agentId);
|
|
94
95
|
const placeholders = params.chatTypes.map(() => "?").join(", ");
|
|
95
96
|
const windows = db.prepare(`
|
|
96
97
|
SELECT
|
|
@@ -104,7 +105,8 @@ function readSnapshot(params) {
|
|
|
104
105
|
COALESCE(window.started_at, window.created_at) AS startedAt,
|
|
105
106
|
rewrite.generation AS sourceGeneration,
|
|
106
107
|
MAX(active.event_seq) AS maxSeq,
|
|
107
|
-
COUNT(active.event_seq) AS activeEventCount
|
|
108
|
+
COUNT(active.event_seq) AS activeEventCount,
|
|
109
|
+
${transcripts.coldSql("window.session_id")} AS cold
|
|
108
110
|
FROM session_windows AS window
|
|
109
111
|
LEFT JOIN conversations AS conversation
|
|
110
112
|
ON conversation.conversation_id = window.primary_conversation_id
|
|
@@ -116,15 +118,9 @@ function readSnapshot(params) {
|
|
|
116
118
|
GROUP BY window.session_id
|
|
117
119
|
ORDER BY window.created_at, window.session_id
|
|
118
120
|
`).all(...params.chatTypes);
|
|
119
|
-
const readEvents = db.prepare(`
|
|
120
|
-
SELECT a.session_id AS sessionId, e.event_json AS eventJson,
|
|
121
|
-
e.created_at AS createdAt
|
|
122
|
-
${ACTIVE_EVENTS_FROM}
|
|
123
|
-
WHERE a.session_id = ?
|
|
124
|
-
ORDER BY a.active_position
|
|
125
|
-
`);
|
|
126
121
|
const events = new Map();
|
|
127
122
|
const changed = new Set();
|
|
123
|
+
const unavailable = new Set();
|
|
128
124
|
for (const window of windows) {
|
|
129
125
|
const metadata = {
|
|
130
126
|
sessionId: window.sessionId,
|
|
@@ -137,7 +133,7 @@ function readSnapshot(params) {
|
|
|
137
133
|
const previous = params.previousManifest.sessions[window.sessionId];
|
|
138
134
|
const documentPath = sessionDocumentPath(metadata);
|
|
139
135
|
const sourceFingerprint = JSON.stringify(window);
|
|
140
|
-
const unchanged = !params.force &&
|
|
136
|
+
const unchanged = !window.cold && !params.force &&
|
|
141
137
|
(previous ? previous.sourceFingerprint === sourceFingerprint &&
|
|
142
138
|
previous.projectorVersion === PROJECTOR_VERSION &&
|
|
143
139
|
previous.documentPath === documentPath &&
|
|
@@ -145,12 +141,22 @@ function readSnapshot(params) {
|
|
|
145
141
|
params.previousManifest.ignoredSessions?.[window.sessionId] === sourceFingerprint);
|
|
146
142
|
if (!unchanged) {
|
|
147
143
|
changed.add(window.sessionId);
|
|
148
|
-
if (!params.metadataOnly)
|
|
149
|
-
|
|
144
|
+
if (!params.metadataOnly) {
|
|
145
|
+
try {
|
|
146
|
+
const snapshot = transcripts.read(window.sessionId);
|
|
147
|
+
if (snapshot.kind === "ready")
|
|
148
|
+
events.set(window.sessionId, snapshot.rows.map(row => ({ ...row, sessionId: window.sessionId })));
|
|
149
|
+
else
|
|
150
|
+
unavailable.add(window.sessionId);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
unavailable.add(window.sessionId);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
150
156
|
}
|
|
151
157
|
}
|
|
152
158
|
db.exec("COMMIT");
|
|
153
|
-
return { windows, events, changed };
|
|
159
|
+
return { windows, events, changed, unavailable };
|
|
154
160
|
}
|
|
155
161
|
catch (error) {
|
|
156
162
|
try {
|
|
@@ -262,6 +268,12 @@ export async function syncSessionProjections(params) {
|
|
|
262
268
|
startedAt: window.startedAt,
|
|
263
269
|
};
|
|
264
270
|
const documentPath = sessionDocumentPath(metadata);
|
|
271
|
+
if (snapshot.unavailable.has(window.sessionId)) {
|
|
272
|
+
counts.failed += 1;
|
|
273
|
+
if (previous)
|
|
274
|
+
sessions[window.sessionId] = previous;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
265
277
|
if (events === undefined) {
|
|
266
278
|
if (previous)
|
|
267
279
|
sessions[window.sessionId] = previous;
|
|
@@ -332,8 +344,9 @@ export async function syncSessionProjections(params) {
|
|
|
332
344
|
else
|
|
333
345
|
counts.unchanged += 1;
|
|
334
346
|
}
|
|
347
|
+
const liveIds = new Set(snapshot.windows.map(window => window.sessionId));
|
|
335
348
|
for (const [sessionId, session] of Object.entries(previousManifest.sessions)) {
|
|
336
|
-
if (sessions[sessionId] ||
|
|
349
|
+
if (sessions[sessionId] || liveIds.has(sessionId))
|
|
337
350
|
continue;
|
|
338
351
|
await remove(projectionPath(params.outputDir, session.documentPath));
|
|
339
352
|
counts.removed += 1;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
2
|
import { selectTypeSafeSkill } from "./typesafe.js";
|
|
3
3
|
import { resolveTypeSafeApiKey, TypeSafeRequestError } from "./typesafe-client.js";
|
|
4
|
-
import { buildSkillWhispererQuery,
|
|
4
|
+
import { buildSkillWhispererQuery, recentSkillMessages } from "./whisperer-context.js";
|
|
5
5
|
import { abortable } from "./abortable.js";
|
|
6
6
|
const CANDIDATE_LIMIT = 10;
|
|
7
7
|
const MAX_QUERY_CHARS = 12_000;
|
|
@@ -10,18 +10,15 @@ const TOTAL_TIMEOUT_MS = 3000;
|
|
|
10
10
|
function isRecord(value) {
|
|
11
11
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
12
12
|
}
|
|
13
|
-
function typeSafeConversation(prompt, messages
|
|
13
|
+
function typeSafeConversation(prompt, messages) {
|
|
14
14
|
const currentRequest = prompt.trim().slice(-MAX_QUERY_CHARS);
|
|
15
15
|
let remaining = MAX_QUERY_CHARS - currentRequest.length;
|
|
16
|
-
const available = messages.flatMap(message => {
|
|
17
|
-
const parsed = messageText(message);
|
|
18
|
-
return parsed ? [{ role: parsed.role, content: parsed.text }] : [];
|
|
19
|
-
});
|
|
20
16
|
const history = [];
|
|
21
|
-
for (
|
|
17
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
22
18
|
if (remaining <= 0)
|
|
23
19
|
break;
|
|
24
|
-
const
|
|
20
|
+
const message = messages[i];
|
|
21
|
+
const content = message.text.slice(-remaining);
|
|
25
22
|
history.unshift({ role: message.role, content });
|
|
26
23
|
remaining -= content.length;
|
|
27
24
|
}
|
|
@@ -73,7 +70,8 @@ export function registerSkillWhisperer(api, runtime, config, typesafe, diagnosti
|
|
|
73
70
|
signal.throwIfAborted();
|
|
74
71
|
if (!apiKey)
|
|
75
72
|
diagnostics?.record(agentId, "skill", typesafe.enabled ? "missing_key" : "typesafe_disabled");
|
|
76
|
-
const
|
|
73
|
+
const recentMessages = recentSkillMessages(event.messages, config.historyMessages);
|
|
74
|
+
const candidates = await abortable(runtime.searchSkills(runtimeParams, buildSkillWhispererQuery(event.prompt, recentMessages), apiKey ? -1 : config.minScore, CANDIDATE_LIMIT), signal);
|
|
77
75
|
signal.throwIfAborted();
|
|
78
76
|
const resolvedCandidates = candidates.flatMap((candidate) => {
|
|
79
77
|
const canonicalPath = runtime.resolveSkillPath(runtimeParams, candidate.path);
|
|
@@ -96,7 +94,7 @@ export function registerSkillWhisperer(api, runtime, config, typesafe, diagnosti
|
|
|
96
94
|
}
|
|
97
95
|
const selectedIndex = await abortable(selectTypeSafeSkill({
|
|
98
96
|
apiKey, timeoutMs: typesafe.timeoutMs, signal,
|
|
99
|
-
...typeSafeConversation(event.prompt,
|
|
97
|
+
...typeSafeConversation(event.prompt, recentMessages),
|
|
100
98
|
candidates: shortlist.map(({ candidate }) => candidate),
|
|
101
99
|
onCandidateFailure: (candidateIndex, error) => {
|
|
102
100
|
if (signal.aborted)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { DatabaseSync } from "node:sqlite";
|
|
3
|
-
import {
|
|
3
|
+
import { AgentTranscriptReader } from "./agent-transcript.js";
|
|
4
4
|
import { messageText } from "./whisperer-context.js";
|
|
5
5
|
import { conversationUserText } from "./response-text.js";
|
|
6
6
|
import { prepareQueryConversation, QUERY_CONTRACT, QueryInputBudgetError } from "./query-contract.js";
|
|
@@ -133,12 +133,13 @@ export function trainingExamples(rows, renew) {
|
|
|
133
133
|
/** Only active events; no Markdown projections, archived branches, or tool bodies. */
|
|
134
134
|
export class TrainingTranscriptReader {
|
|
135
135
|
#db;
|
|
136
|
+
#transcripts;
|
|
136
137
|
#lineage;
|
|
137
138
|
constructor(path, agentId) {
|
|
138
139
|
this.#db = new DatabaseSync(path, { readOnly: true });
|
|
139
140
|
try {
|
|
140
141
|
this.#db.exec("PRAGMA query_only=ON; PRAGMA busy_timeout=1000");
|
|
141
|
-
|
|
142
|
+
this.#transcripts = new AgentTranscriptReader(this.#db, agentId, "Unsupported training transcript schema or agent");
|
|
142
143
|
const columns = this.#db.prepare("PRAGMA table_info(session_windows)").all().map(c => c.name);
|
|
143
144
|
this.#lineage = ["parent_session_key", "spawned_by", "plugin_owner_id", "hook_external_content_source"].every(c => columns.includes(c));
|
|
144
145
|
}
|
|
@@ -161,11 +162,12 @@ export class TrainingTranscriptReader {
|
|
|
161
162
|
/:(?:cron|subagent|heartbeat|hook)(?::|$)/i.test(String(session.session_key)) ||
|
|
162
163
|
session.parent_session_key || session.spawned_by || session.plugin_owner_id || session.hook_external_content_source)
|
|
163
164
|
return null;
|
|
164
|
-
const
|
|
165
|
-
if (
|
|
165
|
+
const snapshot = this.#transcripts.read(sessionId, { maxEvents: 50_000, maxBytes: 32_000_000 });
|
|
166
|
+
if (snapshot.kind === "oversized")
|
|
166
167
|
return { oversized: true };
|
|
167
|
-
|
|
168
|
-
|
|
168
|
+
if (snapshot.kind !== "ready")
|
|
169
|
+
return { unavailable: true };
|
|
170
|
+
return trainingExamples(snapshot.rows, renew);
|
|
169
171
|
}
|
|
170
172
|
finally {
|
|
171
173
|
this.#db.exec("COMMIT");
|
|
@@ -34,6 +34,7 @@ export declare function generateTrainingQueries(source: Source, store: TrainingS
|
|
|
34
34
|
sessions: number;
|
|
35
35
|
excludedSessions: number;
|
|
36
36
|
oversizedSessions: number;
|
|
37
|
+
unavailableSessions: number;
|
|
37
38
|
eligible: number;
|
|
38
39
|
users: number;
|
|
39
40
|
filtered: number;
|
|
@@ -79,6 +80,7 @@ export declare function evaluateTrainingQueries(source: Source, store: TrainingS
|
|
|
79
80
|
sessions: number;
|
|
80
81
|
excludedSessions: number;
|
|
81
82
|
oversizedSessions: number;
|
|
83
|
+
unavailableSessions: number;
|
|
82
84
|
eligible: number;
|
|
83
85
|
users: number;
|
|
84
86
|
filtered: number;
|
|
@@ -85,15 +85,9 @@ export declare class TrainingStore {
|
|
|
85
85
|
error: string;
|
|
86
86
|
}): void;
|
|
87
87
|
retry(includeAmbiguous: boolean, ids: readonly string[]): number;
|
|
88
|
-
|
|
89
|
-
queryExamples(threshold?: number): {
|
|
88
|
+
queryExamples(threshold?: number): (TrainingSourceExample & {
|
|
90
89
|
recallProbability: number;
|
|
91
|
-
|
|
92
|
-
inputHash: string;
|
|
93
|
-
inputJson: string;
|
|
94
|
-
sessionId: string;
|
|
95
|
-
timestamp: number;
|
|
96
|
-
}[];
|
|
90
|
+
})[];
|
|
97
91
|
step<S extends keyof TrainingStepResults>(stage: S, parameters: Record<string, unknown>): {
|
|
98
92
|
id: string;
|
|
99
93
|
stage: S;
|
|
@@ -136,20 +136,21 @@ export class TrainingStore {
|
|
|
136
136
|
this.#db.prepare("UPDATE training_examples SET active=0 WHERE session_id=?").run(sessionId);
|
|
137
137
|
const counts = { added: 0, changed: 0, unchanged: 0, retired: 0 };
|
|
138
138
|
const retained = new Set();
|
|
139
|
+
const input = this.#db.prepare("INSERT OR IGNORE INTO training_inputs VALUES (?,?,?)");
|
|
140
|
+
const source = this.#db.prepare(`INSERT INTO training_examples VALUES (?,?,?,?,?,?,1)
|
|
141
|
+
ON CONFLICT(session_id,event_seq) DO UPDATE SET timestamp=excluded.timestamp,input_hash=excluded.input_hash,
|
|
142
|
+
context_limited=excluded.context_limited,active=1`);
|
|
143
|
+
const gate = this.#db.prepare(`INSERT OR IGNORE INTO training_gates
|
|
144
|
+
(id,input_hash,prompt_version,requested_model,status) VALUES (?,?,?,?,'pending')`);
|
|
139
145
|
for (const example of examples) {
|
|
140
146
|
const old = previous.get(example.seq);
|
|
141
147
|
if (!old && (existingOnly || example.timestamp < since || example.timestamp >= until))
|
|
142
148
|
continue;
|
|
143
149
|
retained.add(example.seq);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
this.#db.prepare(`INSERT INTO training_examples VALUES (?,?,?,?,?,?,1)
|
|
147
|
-
ON CONFLICT(session_id,event_seq) DO UPDATE SET timestamp=excluded.timestamp,input_hash=excluded.input_hash,
|
|
148
|
-
context_limited=excluded.context_limited,active=1`)
|
|
149
|
-
.run(trainingHash([this.#nodeId, this.#agentId, sessionId, example.seq]), sessionId, example.seq, example.timestamp, example.inputHash, Number(example.contextLimited));
|
|
150
|
+
input.run(example.inputHash, TRAINING_PREPARATION, JSON.stringify(example.input));
|
|
151
|
+
source.run(trainingHash([this.#nodeId, this.#agentId, sessionId, example.seq]), sessionId, example.seq, example.timestamp, example.inputHash, Number(example.contextLimited));
|
|
150
152
|
const id = trainingHash([example.inputHash, TRAINING_GATE_VERSION, TRAINING_GATE_MODEL]);
|
|
151
|
-
|
|
152
|
-
.run(id, example.inputHash, TRAINING_GATE_VERSION, TRAINING_GATE_MODEL);
|
|
153
|
+
gate.run(id, example.inputHash, TRAINING_GATE_VERSION, TRAINING_GATE_MODEL);
|
|
153
154
|
if (!old)
|
|
154
155
|
counts.added++;
|
|
155
156
|
else if (old.input_hash !== example.inputHash || old.active !== 1)
|
|
@@ -211,21 +212,15 @@ export class TrainingStore {
|
|
|
211
212
|
return count + Number(gates.changes) + Number(steps.changes);
|
|
212
213
|
}, 0));
|
|
213
214
|
}
|
|
214
|
-
activeExamples() {
|
|
215
|
-
return this.#db.prepare(`SELECT e.id,e.input_hash inputHash,i.input_json inputJson,e.session_id sessionId,e.timestamp
|
|
216
|
-
FROM training_examples e JOIN training_inputs i ON i.hash=e.input_hash
|
|
217
|
-
WHERE e.active=1 AND i.preparation=? ORDER BY e.timestamp DESC,e.id`).all(TRAINING_PREPARATION);
|
|
218
|
-
}
|
|
219
215
|
queryExamples(threshold = TRAINING_GATE_THRESHOLD) {
|
|
220
216
|
if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1)
|
|
221
217
|
throw new Error("Threshold must be between 0 and 1");
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
});
|
|
218
|
+
return this.#db.prepare(`SELECT e.id,e.input_hash inputHash,i.input_json inputJson,e.session_id sessionId,e.timestamp,
|
|
219
|
+
g.probability recallProbability FROM training_examples e JOIN training_inputs i ON i.hash=e.input_hash
|
|
220
|
+
JOIN training_gates g ON g.input_hash=e.input_hash
|
|
221
|
+
WHERE e.active=1 AND i.preparation=? AND g.prompt_version=? AND g.requested_model=?
|
|
222
|
+
AND g.status='complete' AND g.probability>=? ORDER BY e.timestamp DESC,e.id`)
|
|
223
|
+
.all(TRAINING_PREPARATION, TRAINING_GATE_VERSION, TRAINING_GATE_MODEL, threshold);
|
|
229
224
|
}
|
|
230
225
|
step(stage, parameters) {
|
|
231
226
|
const request = { ...parameters, recipe: TRAINING_RECIPE_VERSION };
|
package/dist/src/training.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export declare function collectTraining(source: Source, store?: TrainingStore, o
|
|
|
13
13
|
sessions: number;
|
|
14
14
|
excludedSessions: number;
|
|
15
15
|
oversizedSessions: number;
|
|
16
|
+
unavailableSessions: number;
|
|
16
17
|
eligible: number;
|
|
17
18
|
users: number;
|
|
18
19
|
filtered: number;
|
|
@@ -39,6 +40,7 @@ export declare function runTraining(source: Source, store: TrainingStore, config
|
|
|
39
40
|
sessions: number;
|
|
40
41
|
excludedSessions: number;
|
|
41
42
|
oversizedSessions: number;
|
|
43
|
+
unavailableSessions: number;
|
|
42
44
|
eligible: number;
|
|
43
45
|
users: number;
|
|
44
46
|
filtered: number;
|
package/dist/src/training.js
CHANGED
|
@@ -5,7 +5,7 @@ import { judgeTrainingInput, TRAINING_GATE_QUESTIONS } from "./training-gate.js"
|
|
|
5
5
|
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
|
-
const result = { sessions: 0, excludedSessions: 0, oversizedSessions: 0, eligible: 0,
|
|
8
|
+
const result = { sessions: 0, excludedSessions: 0, oversizedSessions: 0, unavailableSessions: 0, eligible: 0,
|
|
9
9
|
users: 0, filtered: 0, oversized: 0, unanswered: 0, added: 0, changed: 0, unchanged: 0, retired: 0,
|
|
10
10
|
review: [] };
|
|
11
11
|
try {
|
|
@@ -14,6 +14,11 @@ export function collectTraining(source, store, options = {}) {
|
|
|
14
14
|
store?.renew();
|
|
15
15
|
const extracted = reader.read(id, () => store?.renew());
|
|
16
16
|
result.sessions++;
|
|
17
|
+
if (extracted && "unavailable" in extracted) {
|
|
18
|
+
result.unavailableSessions++;
|
|
19
|
+
store?.unavailable(id);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
17
22
|
if (extracted && "oversized" in extracted) {
|
|
18
23
|
result.oversizedSessions++;
|
|
19
24
|
store?.unavailable(id);
|
|
@@ -3,4 +3,8 @@ export declare function messageText(message: unknown): {
|
|
|
3
3
|
role: "user" | "assistant";
|
|
4
4
|
text: string;
|
|
5
5
|
} | undefined;
|
|
6
|
-
export declare function
|
|
6
|
+
export declare function recentSkillMessages(messages: readonly unknown[], limit: number): {
|
|
7
|
+
role: "user" | "assistant";
|
|
8
|
+
text: string;
|
|
9
|
+
}[];
|
|
10
|
+
export declare function buildSkillWhispererQuery(prompt: string, history: readonly NonNullable<ReturnType<typeof messageText>>[]): string;
|
|
@@ -11,11 +11,16 @@ export function messageText(message) {
|
|
|
11
11
|
}).join("\n").trim() : "";
|
|
12
12
|
return text ? { role: message.role, text } : undefined;
|
|
13
13
|
}
|
|
14
|
-
export function
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
export function recentSkillMessages(messages, limit) {
|
|
15
|
+
const history = [];
|
|
16
|
+
for (let i = messages.length - 1; i >= 0 && history.length < limit; i--) {
|
|
17
|
+
const parsed = messageText(messages[i]);
|
|
18
|
+
if (parsed)
|
|
19
|
+
history.push(parsed);
|
|
20
|
+
}
|
|
21
|
+
return history.reverse();
|
|
22
|
+
}
|
|
23
|
+
export function buildSkillWhispererQuery(prompt, history) {
|
|
24
|
+
return [...history.map(message => `${message.role}: ${message.text}`), `user: ${prompt.trim()}`]
|
|
25
|
+
.join("\n\n").slice(-MAX_SKILL_QUERY_CHARS);
|
|
21
26
|
}
|
|
@@ -2,40 +2,47 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { isPathStrictlyInside } from "openclaw/plugin-sdk/file-access-runtime";
|
|
4
4
|
import { readMemoryArtifactProvenance } from "openclaw/plugin-sdk/memory-core-host-runtime-core";
|
|
5
|
-
export const classifyWorkspaceMemoryPaths = async (params) =>
|
|
5
|
+
export const classifyWorkspaceMemoryPaths = async (params) => {
|
|
6
|
+
if (!params.relativePaths.length)
|
|
7
|
+
return [];
|
|
6
8
|
let workspacePath;
|
|
7
|
-
let filePath;
|
|
8
9
|
try {
|
|
9
|
-
|
|
10
|
-
fs.realpath(params.workspaceDir),
|
|
11
|
-
fs.realpath(path.resolve(params.workspaceDir, relativePath)),
|
|
12
|
-
]);
|
|
10
|
+
workspacePath = await fs.realpath(params.workspaceDir);
|
|
13
11
|
}
|
|
14
12
|
catch {
|
|
15
|
-
return { relativePath, originClass: "untrusted" };
|
|
13
|
+
return params.relativePaths.map(relativePath => ({ relativePath, originClass: "untrusted" }));
|
|
16
14
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
relativePath:
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
15
|
+
return await Promise.all(params.relativePaths.map(async (relativePath) => {
|
|
16
|
+
let filePath;
|
|
17
|
+
try {
|
|
18
|
+
filePath = await fs.realpath(path.resolve(params.workspaceDir, relativePath));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return { relativePath, originClass: "untrusted" };
|
|
22
|
+
}
|
|
23
|
+
if (!isPathStrictlyInside(workspacePath, filePath)) {
|
|
24
|
+
return { relativePath, originClass: "untrusted" };
|
|
25
|
+
}
|
|
26
|
+
const workspaceRelativePath = path.relative(workspacePath, filePath);
|
|
27
|
+
const segments = workspaceRelativePath.split(path.sep);
|
|
28
|
+
const curatedRoot = segments.length === 1 &&
|
|
29
|
+
["MEMORY.md", "memory.md", "USER.md"].includes(segments[0]);
|
|
30
|
+
if ((segments.length === 1 && ["DREAMS.md", "dreams.md"].includes(segments[0])) ||
|
|
31
|
+
(segments[0] === "memory" && ["dreaming", ".dreams"].includes(segments[1]))) {
|
|
32
|
+
return { relativePath, originClass: "system" };
|
|
33
|
+
}
|
|
34
|
+
const isWorkspaceMemory = curatedRoot ||
|
|
35
|
+
(segments[0] === "memory" && segments.at(-1)?.endsWith(".md") === true);
|
|
36
|
+
const normalizedPath = workspaceRelativePath.replaceAll(path.sep, "/");
|
|
37
|
+
const recorded = isWorkspaceMemory
|
|
38
|
+
? await readMemoryArtifactProvenance({
|
|
39
|
+
workspaceDir: params.workspaceDir,
|
|
40
|
+
relativePath: normalizedPath,
|
|
41
|
+
})
|
|
42
|
+
: undefined;
|
|
43
|
+
return {
|
|
44
|
+
relativePath,
|
|
45
|
+
originClass: recorded?.originClass ?? (isWorkspaceMemory ? "agent" : "untrusted"),
|
|
46
|
+
};
|
|
47
|
+
}));
|
|
48
|
+
};
|
package/docs/memory-training.md
CHANGED
|
@@ -70,13 +70,17 @@ continue without retrying failed requests. Storage/lease failures still stop the
|
|
|
70
70
|
In-flight work drains and persists before snapshots close. Dry runs make no inference calls.
|
|
71
71
|
|
|
72
72
|
Cached evaluations still validate the exact historical text and vector fingerprint,
|
|
73
|
-
but do not rebuild a QMD index.
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
but do not rebuild a QMD index. Each evaluation run captures immutable source text
|
|
74
|
+
and vector bytes once, then closes the source reader. Each exact cutoff derives its
|
|
75
|
+
own fingerprint and builds a private index only on its first uncached search.
|
|
76
|
+
Concurrent queries at that cutoff share the index; later runs capture fresh source
|
|
77
|
+
bytes, so changed corpus content still invalidates caches.
|
|
76
78
|
|
|
77
79
|
## Input and historical boundaries
|
|
78
80
|
|
|
79
|
-
Read original agent SQLite active-branch conversations
|
|
81
|
+
Read original agent SQLite active-branch conversations through the shared read-only
|
|
82
|
+
adapter, including compressed payloads, while deferring cold or oversized sessions
|
|
83
|
+
without retiring their previously collected examples. This includes
|
|
80
84
|
direct/group/channel chats, excluding cron, heartbeat, spawned/subagent, hook/plugin
|
|
81
85
|
and untyped diagnostic sessions. Exclude explicit bots, synthetic messages,
|
|
82
86
|
analysis/thinking, errors and tool payloads. Ordinary older users need not have
|
|
@@ -99,10 +103,9 @@ unknown/future dates. Never infer dates from quoted headings. Copy existing vect
|
|
|
99
103
|
only for complete chunks within the safe prefix; BM25 sees that same prefix.
|
|
100
104
|
Validate returned passages/dates again. No reembedding or temporary transcripts.
|
|
101
105
|
|
|
102
|
-
QMD
|
|
103
|
-
A small
|
|
104
|
-
|
|
105
|
-
backend and omits query-conditioned scoring. It does not rewrite QMD. Training and
|
|
106
|
+
QMD's public `discoverCandidates` API owns tokenization, FTS-highlight chunk
|
|
107
|
+
selection and source-aware deduplication. A small adapter requests ten from the
|
|
108
|
+
selected backend and omits query-conditioned scoring. Training and
|
|
106
109
|
runtime share complete-excerpt rendering, including the 1,200-character cap;
|
|
107
110
|
oversized matches are discarded rather than truncated.
|
|
108
111
|
|
package/docs/response-audit.md
CHANGED
|
@@ -202,8 +202,8 @@ factual verification. Week buckets
|
|
|
202
202
|
may be partial, and several exchanges in one session are not independent. Wilson
|
|
203
203
|
intervals are descriptive, not calibrated confidence about overall agent ability.
|
|
204
204
|
|
|
205
|
-
Each run selects at most 100 recent sessions
|
|
206
|
-
|
|
205
|
+
Each run selects at most 100 recent sessions containing approved-sender messages for inference,
|
|
206
|
+
each at most 2,000 active events/2M decoded UTF-8 bytes; episodes must fit 24K characters and six feedback messages without
|
|
207
207
|
truncating the answer. Coverage counts describe the scanned sessions; only episodes
|
|
208
208
|
within `lookbackDays` are judged. Caps, failures and no-feedback cases remain visible.
|
|
209
209
|
Saved sessions in the report window are also reconciled independently of that
|
package/docs/retrieval.md
CHANGED
|
@@ -155,13 +155,19 @@ fits. The default is `500`; the original semantic chunk is preserved when
|
|
|
155
155
|
neither complete context fits, so expansion never clips the matched evidence.
|
|
156
156
|
Run
|
|
157
157
|
`memory_sync_sessions` to start a refresh, then use `memory_sync_status` to
|
|
158
|
-
check its progress or result. The read-only adapter
|
|
159
|
-
agent
|
|
160
|
-
|
|
158
|
+
check its progress or result. The shared read-only transcript adapter validates
|
|
159
|
+
agent identity and the storage capabilities it needs, rather than rejecting every
|
|
160
|
+
new schema number. It reads both text and bounded Zstd payloads (schema 23+),
|
|
161
|
+
counts decoded UTF-8 bytes for budgets, and preserves previous projections when
|
|
162
|
+
history is cold, oversized, or unreadable. Cold history is deferred until restored
|
|
163
|
+
by OpenClaw; the plugin never restores or migrates the source database. The public
|
|
164
|
+
full-transcript SDK reader currently permits restoration, while its read-only
|
|
165
|
+
catalog redacts and truncates text, so neither supplies this full-fidelity read-only
|
|
166
|
+
contract. Extraction retains its supported host cursor API. Projections are private derived Markdown under the
|
|
161
167
|
agent's `unblock-memory/sessions` state directory and can be rebuilt from
|
|
162
168
|
OpenClaw at any time. Their embedded text contains only `# Transcript` and
|
|
163
169
|
role-labeled, timestamped speaker messages; filtering metadata remains in the
|
|
164
|
-
session manifest. Projection
|
|
170
|
+
session manifest. Projection v8 retains message metadata and exact character
|
|
165
171
|
boundaries there, without duplicating message bodies. Readers use those boundaries
|
|
166
172
|
only when the projection hash matches the indexed document. Older/mismatched snapshots
|
|
167
173
|
use a conservative heading parser that skips code fences and blockquotes; an unfenced
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "unblock-memory",
|
|
3
3
|
"name": "Unblock Memory",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.4",
|
|
5
5
|
"description": "Indexes, retrieves, and analyzes configured workspace memory with existing QMD vectors.",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"activation": { "onStartup": true },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unblocklabs/unblock-memory",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "Workspace-native memory for OpenClaw, powered by QMD",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,16 +37,18 @@
|
|
|
37
37
|
"build": "tsc -p tsconfig.build.json",
|
|
38
38
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
39
39
|
"knip": "knip --reporter compact",
|
|
40
|
-
"test": "
|
|
40
|
+
"test": "npm run test:node && npm run test:python",
|
|
41
|
+
"test:node": "node --import tsx --test --test-concurrency=1 tests/**/*.test.ts tests/**/*.test.mjs",
|
|
42
|
+
"test:python": "python3 -B tests/prepare-query-training.test.py",
|
|
41
43
|
"eval:retrieval": "node --import tsx eval/retrieval/lab.ts",
|
|
42
44
|
"plugin:inspect": "plugin-inspector check --config plugin-inspector.config.json --no-openclaw",
|
|
43
|
-
"plugin:inspect:runtime": "plugin-inspector check --config plugin-inspector.config.json --no-openclaw --runtime --
|
|
45
|
+
"plugin:inspect:runtime": "plugin-inspector check --config plugin-inspector.config.json --no-openclaw --runtime --real-sdk --allow-execute",
|
|
44
46
|
"release:check": "node scripts/check-release-version.mjs",
|
|
45
47
|
"preflight": "npm run knip && npm run build && npm run typecheck && npm test && npm run plugin:inspect && npm run plugin:inspect:runtime && npm pack --dry-run"
|
|
46
48
|
},
|
|
47
49
|
"dependencies": {
|
|
48
50
|
"@huggingface/tokenizers": "0.2.0",
|
|
49
|
-
"@unblocklabs/qmd": "https://github.com/unblocklabs-ai/qmd/releases/download/v2.11.
|
|
51
|
+
"@unblocklabs/qmd": "https://github.com/unblocklabs-ai/qmd/releases/download/v2.11.1/unblocklabs-qmd-2.11.1.tgz",
|
|
50
52
|
"chokidar": "5.0.0",
|
|
51
53
|
"js-tiktoken": "1.0.21",
|
|
52
54
|
"picomatch": "^4.0.5",
|