@unblocklabs/unblock-memory 0.3.14 → 0.3.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -0
- package/dist/src/abortable.d.ts +2 -0
- package/dist/src/abortable.js +21 -0
- package/dist/src/cluster-review.d.ts +47 -0
- package/dist/src/cluster-review.js +64 -0
- package/dist/src/config.d.ts +5 -0
- package/dist/src/config.js +22 -3
- package/dist/src/curation.js +4 -1
- package/dist/src/diagnostics.d.ts +39 -0
- package/dist/src/diagnostics.js +18 -0
- package/dist/src/evidence-review.d.ts +41 -0
- package/dist/src/evidence-review.js +50 -0
- package/dist/src/manager.d.ts +84 -4
- package/dist/src/manager.js +72 -3
- package/dist/src/memory-whisperer.d.ts +2 -1
- package/dist/src/memory-whisperer.js +45 -9
- package/dist/src/plugin.js +8 -20
- package/dist/src/quality-audit.d.ts +3 -0
- package/dist/src/quality-audit.js +6 -3
- package/dist/src/quality-triage.d.ts +9 -0
- package/dist/src/quality-triage.js +38 -0
- package/dist/src/review-tools.d.ts +5 -0
- package/dist/src/review-tools.js +116 -0
- package/dist/src/session-noise.d.ts +20 -0
- package/dist/src/session-noise.js +142 -0
- package/dist/src/session-projector.d.ts +6 -0
- package/dist/src/session-projector.js +16 -0
- package/dist/src/session-sync.d.ts +3 -1
- package/dist/src/session-sync.js +4 -1
- package/dist/src/skill-whisperer.d.ts +2 -1
- package/dist/src/skill-whisperer.js +24 -8
- package/dist/src/tool-context.d.ts +7 -0
- package/dist/src/tool-context.js +17 -0
- package/dist/src/typesafe-review.d.ts +40 -0
- package/dist/src/typesafe-review.js +133 -0
- package/openclaw.plugin.json +17 -1
- package/package.json +2 -2
- package/skills/memory-curator/SKILL.md +11 -0
- package/skills/people-whisperer/SKILL.md +7 -0
package/README.md
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
# Unblock Memory
|
|
2
2
|
|
|
3
|
+
## Review and diagnostics
|
|
4
|
+
|
|
5
|
+
- `memory_diagnostics` reports credential **availability only**, per-agent process-local
|
|
6
|
+
whisperer counters, projection version, old indexed-session projection count, and
|
|
7
|
+
embedding readiness. Counters are bounded to 100 agents and reset on restart.
|
|
8
|
+
No prompts, excerpts, paths, keys, or provider error bodies enter these counters.
|
|
9
|
+
Parser cleanup/budget-skip counts are persisted with the latest completed
|
|
10
|
+
`memory_sync_status`; unchanged sessions are not counted again. QMD structural
|
|
11
|
+
omission counts cover this manager's embedding passes, not the whole corpus.
|
|
12
|
+
- Quality-audit groups distinguish `preserve_evidence_repair`, `inspect_scaffolding`,
|
|
13
|
+
and `context_review`, reusing cached noise/evidence judgments without another call.
|
|
14
|
+
Evidence-preserving repair tasks sort first. Maintenance tasks expose indexed
|
|
15
|
+
fingerprint presence; `not_present_in_index` is **not** a verified repair and
|
|
16
|
+
never resolves or deletes the task. Chunk boundaries may simply have changed.
|
|
17
|
+
- `memory_review_cluster` uses the existing `qualityAudit` opt-in/corpus allowlist.
|
|
18
|
+
It judges up to three representative and three low-membership members, deduplicates
|
|
19
|
+
the sample, and skips unapproved or >2,000-character chunks whole. Repeated defect
|
|
20
|
+
labels are investigation leads only. Stale/changed samples are rejected; useful
|
|
21
|
+
or uncertain members are retained. No tasks or sources are modified.
|
|
22
|
+
- `memory_review_claim` accepts one atomic claim (up to 2,000 characters) and 1–3
|
|
23
|
+
citations `{path, from, lines}`. It reads approved indexed evidence itself (at
|
|
24
|
+
most 6,000 characters), returns supports/contradicts/insufficient_evidence with
|
|
25
|
+
confidence and source hashes, and never writes or authorizes a write. Support
|
|
26
|
+
below 0.9 confidence is marked for review. This threshold is provisional, not a
|
|
27
|
+
guarantee of truth; read original evidence and verify current-state claims.
|
|
28
|
+
|
|
29
|
+
New optional configuration (corpora must already be configured):
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"evidenceReview": { "enabled": true, "corpora": ["memory", "knowledge", "sessions"] },
|
|
34
|
+
"memoryWhisperer": {
|
|
35
|
+
"enabled": true, "corpora": ["memory", "knowledge"], "complementaryHints": true
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Both additions default off. Claim review sends the proposed claim and approved
|
|
41
|
+
source excerpts to TypeSafe; cluster review sends approved sampled excerpts.
|
|
42
|
+
Complementary hints use one extra bounded call over at most four already-useful
|
|
43
|
+
candidates (six directional comparisons). Only redundancy probability >=0.9
|
|
44
|
+
removes a hint; distinct evidence and contradictions should remain. Provider errors
|
|
45
|
+
retain baseline hints, while the existing total turn deadline/cancellation still
|
|
46
|
+
suppresses late results. Missing keys or disabled TypeSafe never enable these calls.
|
|
47
|
+
The retrieval corpus/session boundaries are unchanged.
|
|
48
|
+
|
|
3
49
|
Workspace-native memory for OpenClaw, powered internally by `@unblocklabs/qmd`.
|
|
4
50
|
It keeps one warm QMD store per agent and exposes the standard `memory_search`
|
|
5
51
|
and `memory_get` tools. Search uses semantic chunking and direct QMD vector
|
|
@@ -34,6 +80,27 @@ remain opt-in and should stay outside file-corpus globs (new default:
|
|
|
34
80
|
messages. Truncated sessions stay explicitly incomplete; enabling archive
|
|
35
81
|
enrichment is not part of this version.
|
|
36
82
|
|
|
83
|
+
### Conservative ingestion cleanup
|
|
84
|
+
|
|
85
|
+
Session projections unwrap complete, recognized task/attachment envelopes while
|
|
86
|
+
keeping the actual result, task/status, filename, MIME type, and untrusted-content
|
|
87
|
+
label. Internal task cleanup requires structured inter-session provenance, not
|
|
88
|
+
just matching text. Unknown formats, malformed envelopes, and code examples stay
|
|
89
|
+
intact. Assistant messages and Loggie's separate projection path are unaffected.
|
|
90
|
+
Raw session events and workspace memory files are never rewritten.
|
|
91
|
+
Attachment matching has a fixed work budget; oversized or repeatedly nested/
|
|
92
|
+
incomplete envelopes leave the entire message unchanged rather than blocking sync.
|
|
93
|
+
|
|
94
|
+
The companion QMD semantic-chunking update skips only source-confirmed standalone
|
|
95
|
+
REM heading/marker spans and orphan closing fences. Reflections and useful text
|
|
96
|
+
remain searchable, with original source offsets. These are deterministic rules,
|
|
97
|
+
not TypeSafe judgments; audit flags never authorize automatic memory deletion.
|
|
98
|
+
|
|
99
|
+
This release pins QMD 2.9.6. Projector/chunker version changes refresh derived
|
|
100
|
+
projections and embeddings on their next normal sync; the first sync may take
|
|
101
|
+
longer while re-embedding. No manual deletion of source memories or review tasks
|
|
102
|
+
is needed.
|
|
103
|
+
|
|
37
104
|
## Installation
|
|
38
105
|
|
|
39
106
|
From npm:
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Stop waiting without cancelling shared work that other callers still need. */
|
|
2
|
+
export async function abortable(pending, signal) {
|
|
3
|
+
if (!signal)
|
|
4
|
+
return pending;
|
|
5
|
+
let onAbort = () => { };
|
|
6
|
+
const cancelled = new Promise((_resolve, reject) => {
|
|
7
|
+
onAbort = () => reject(signal.reason);
|
|
8
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
9
|
+
if (signal.aborted)
|
|
10
|
+
onAbort();
|
|
11
|
+
});
|
|
12
|
+
try {
|
|
13
|
+
// Observe late failures even when cancellation wins the race.
|
|
14
|
+
const result = await Promise.race([pending, cancelled]);
|
|
15
|
+
signal.throwIfAborted();
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
signal.removeEventListener("abort", onAbort);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { QMDStore } from "@unblocklabs/qmd";
|
|
2
|
+
import { type ResolvedSource } from "./sources.js";
|
|
3
|
+
/** Inspect center and edge samples; never extrapolate their labels to the rest of a cluster. */
|
|
4
|
+
export declare function reviewClusterIngestion(params: {
|
|
5
|
+
db: QMDStore["internal"]["db"];
|
|
6
|
+
sources: readonly ResolvedSource[];
|
|
7
|
+
clusterId: string;
|
|
8
|
+
apiKey: string;
|
|
9
|
+
timeoutMs: number;
|
|
10
|
+
signal: AbortSignal;
|
|
11
|
+
read?: <T>(run: () => T) => Promise<T>;
|
|
12
|
+
}): Promise<{
|
|
13
|
+
status: "unavailable";
|
|
14
|
+
reason: string;
|
|
15
|
+
sample?: undefined;
|
|
16
|
+
considered?: undefined;
|
|
17
|
+
runId?: undefined;
|
|
18
|
+
clusterSize?: undefined;
|
|
19
|
+
} | {
|
|
20
|
+
status: "ok";
|
|
21
|
+
runId: string;
|
|
22
|
+
clusterId: string;
|
|
23
|
+
members: {
|
|
24
|
+
flagged: boolean;
|
|
25
|
+
defect: "encoding" | "wrapper" | "boilerplate" | "none_or_uncertain";
|
|
26
|
+
confidence: number;
|
|
27
|
+
path: string;
|
|
28
|
+
hash: string;
|
|
29
|
+
seq: number;
|
|
30
|
+
from: number;
|
|
31
|
+
fingerprint: string;
|
|
32
|
+
}[];
|
|
33
|
+
recurring: {
|
|
34
|
+
defect: string;
|
|
35
|
+
examples: {
|
|
36
|
+
path: string;
|
|
37
|
+
from: number;
|
|
38
|
+
fingerprint: string;
|
|
39
|
+
}[];
|
|
40
|
+
}[];
|
|
41
|
+
sampled: number;
|
|
42
|
+
considered: number;
|
|
43
|
+
clusterSize: number | undefined;
|
|
44
|
+
policy: string;
|
|
45
|
+
scope: string;
|
|
46
|
+
reason?: undefined;
|
|
47
|
+
}>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readAnalysisSummary, readCluster } from "./analysis.js";
|
|
2
|
+
import { parseSafeVirtualPath } from "./sources.js";
|
|
3
|
+
import { reviewClusterDefects } from "./typesafe-review.js";
|
|
4
|
+
import { chunkFingerprint } from "./curation.js";
|
|
5
|
+
/** Inspect center and edge samples; never extrapolate their labels to the rest of a cluster. */
|
|
6
|
+
export async function reviewClusterIngestion(params) {
|
|
7
|
+
params.signal.throwIfAborted();
|
|
8
|
+
const sources = new Map(params.sources.filter(source => source.kind !== "skills").map(source => [source.collection, source]));
|
|
9
|
+
const read = params.read ?? (async (run) => run());
|
|
10
|
+
const snapshot = await read(() => {
|
|
11
|
+
const center = readCluster(params.db, params.clusterId, 3, 0, "representative");
|
|
12
|
+
if (center.status !== "ok" || center.stale)
|
|
13
|
+
return { status: "unavailable", reason: "Cluster missing or stale; refresh analysis first" };
|
|
14
|
+
const edge = readCluster(params.db, params.clusterId, 3, 0, "score_asc");
|
|
15
|
+
const candidates = [...new Map([...(center.members ?? []), ...(edge.members ?? [])].map(member => [`${member.hash}:${member.seq}`, member])).values()];
|
|
16
|
+
const sample = candidates.flatMap(member => {
|
|
17
|
+
const path = member.sourcePaths.find(path => parseSafeVirtualPath(path, sources));
|
|
18
|
+
if (!path)
|
|
19
|
+
return [];
|
|
20
|
+
const safe = parseSafeVirtualPath(path, sources);
|
|
21
|
+
// Reload the full chunk; analysis previews may be truncated. Never judge a silently cut prefix.
|
|
22
|
+
const row = params.db.prepare(`SELECT c.doc, v.pos, v.chunk_len FROM documents d JOIN content c ON c.hash = d.hash
|
|
23
|
+
JOIN content_vectors v ON v.hash = d.hash WHERE d.active = 1 AND d.collection = ? AND d.path = ? AND d.hash = ? AND v.seq = ?`)
|
|
24
|
+
.get(safe.source.collection, safe.relativePath, member.hash, member.seq);
|
|
25
|
+
if (!row || row.pos < 0 || row.chunk_len < 1 || row.pos + row.chunk_len > row.doc.length || row.chunk_len > 2000)
|
|
26
|
+
return [];
|
|
27
|
+
const text = row.doc.slice(row.pos, row.pos + row.chunk_len);
|
|
28
|
+
return [{ path, hash: member.hash, seq: member.seq, from: row.doc.slice(0, row.pos).split("\n").length,
|
|
29
|
+
fingerprint: chunkFingerprint(text), text }];
|
|
30
|
+
});
|
|
31
|
+
return { status: "ready", sample, considered: candidates.length, runId: center.runId, clusterSize: center.cluster?.availableSize };
|
|
32
|
+
});
|
|
33
|
+
if (snapshot.status !== "ready")
|
|
34
|
+
return snapshot;
|
|
35
|
+
const { sample } = snapshot;
|
|
36
|
+
if (!sample.length)
|
|
37
|
+
return { status: "unavailable", reason: "No complete bounded members in approved corpora" };
|
|
38
|
+
const judgments = await reviewClusterDefects({ ...params, excerpts: sample.map(member => member.text) });
|
|
39
|
+
params.signal.throwIfAborted();
|
|
40
|
+
return read(() => {
|
|
41
|
+
const current = readAnalysisSummary(params.db);
|
|
42
|
+
if (!current || current.stale || current.runId !== snapshot.runId)
|
|
43
|
+
return { status: "unavailable", reason: "Analysis changed during review; retry" };
|
|
44
|
+
for (const member of sample) {
|
|
45
|
+
const safe = parseSafeVirtualPath(member.path, sources);
|
|
46
|
+
if (!safe)
|
|
47
|
+
return { status: "unavailable", reason: "Source scope changed; retry" };
|
|
48
|
+
const row = params.db.prepare(`SELECT c.doc, v.pos, v.chunk_len FROM documents d JOIN content c ON c.hash = d.hash
|
|
49
|
+
JOIN content_vectors v ON v.hash = d.hash WHERE d.active = 1 AND d.collection = ? AND d.path = ? AND d.hash = ? AND v.seq = ?`)
|
|
50
|
+
.get(safe.source.collection, safe.relativePath, member.hash, member.seq);
|
|
51
|
+
if (!row || chunkFingerprint(row.doc.slice(row.pos, row.pos + row.chunk_len)) !== member.fingerprint)
|
|
52
|
+
return { status: "unavailable", reason: "Sample changed during review; retry" };
|
|
53
|
+
}
|
|
54
|
+
const members = sample.map(({ text: _text, ...member }, index) => ({ ...member, ...judgments[index],
|
|
55
|
+
flagged: judgments[index].defect !== "none_or_uncertain" && judgments[index].confidence >= 0.9 }));
|
|
56
|
+
const recurring = ["wrapper", "encoding", "boilerplate"].flatMap(defect => {
|
|
57
|
+
const examples = members.filter(member => member.flagged && member.defect === defect);
|
|
58
|
+
return examples.length >= 2 ? [{ defect, examples: examples.map(member => ({ path: member.path, from: member.from, fingerprint: member.fingerprint })) }] : [];
|
|
59
|
+
});
|
|
60
|
+
return { status: "ok", runId: snapshot.runId, clusterId: params.clusterId, members, recurring,
|
|
61
|
+
sampled: sample.length, considered: snapshot.considered, clusterSize: snapshot.clusterSize,
|
|
62
|
+
policy: "jev-1.13.0:cluster-defects-v1", scope: "Center/edge sample of approved complete chunks only. Recurring labels are hypotheses, not proof of a shared cause or permission to change any member. Unreviewed members remain unknown." };
|
|
63
|
+
});
|
|
64
|
+
}
|
package/dist/src/config.d.ts
CHANGED
|
@@ -36,6 +36,10 @@ export type UnblockMemoryConfig = {
|
|
|
36
36
|
corpora: readonly string[];
|
|
37
37
|
minNoise: number;
|
|
38
38
|
};
|
|
39
|
+
evidenceReview: {
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
corpora: readonly string[];
|
|
42
|
+
};
|
|
39
43
|
people: {
|
|
40
44
|
enabled: boolean;
|
|
41
45
|
whisperer: {
|
|
@@ -54,6 +58,7 @@ export type UnblockMemoryConfig = {
|
|
|
54
58
|
};
|
|
55
59
|
memoryWhisperer: {
|
|
56
60
|
enabled: boolean;
|
|
61
|
+
complementaryHints: boolean;
|
|
57
62
|
corpora: readonly string[];
|
|
58
63
|
historyMessages: number;
|
|
59
64
|
minUsefulness: number;
|
package/dist/src/config.js
CHANGED
|
@@ -79,7 +79,7 @@ const DEFAULT_SKILL_WHISPERER = {
|
|
|
79
79
|
cooldownTurns: 10,
|
|
80
80
|
};
|
|
81
81
|
const DEFAULT_MEMORY_WHISPERER = {
|
|
82
|
-
enabled: false, corpora: [], historyMessages: 5, minUsefulness: 0.9,
|
|
82
|
+
enabled: false, complementaryHints: false, corpora: [], historyMessages: 5, minUsefulness: 0.9,
|
|
83
83
|
maxHints: 2, cooldownTurns: 10, timeoutMs: 3000,
|
|
84
84
|
};
|
|
85
85
|
function resolveMemoryWhisperer(value, corpora) {
|
|
@@ -93,6 +93,9 @@ function resolveMemoryWhisperer(value, corpora) {
|
|
|
93
93
|
const enabled = config.enabled ?? false;
|
|
94
94
|
if (typeof enabled !== "boolean")
|
|
95
95
|
throw new Error("unblock-memory memoryWhisperer.enabled must be a boolean");
|
|
96
|
+
const complementaryHints = config.complementaryHints ?? false;
|
|
97
|
+
if (typeof complementaryHints !== "boolean")
|
|
98
|
+
throw new Error("memoryWhisperer.complementaryHints must be a boolean");
|
|
96
99
|
const selected = config.corpora ?? [];
|
|
97
100
|
if (!Array.isArray(selected) || !selected.every((name) => typeof name === "string" && corpora.some(corpus => corpus.name === name && corpus.kind !== "skills"))) {
|
|
98
101
|
throw new Error("unblock-memory memoryWhisperer.corpora must list configured non-skill corpora");
|
|
@@ -112,7 +115,7 @@ function resolveMemoryWhisperer(value, corpora) {
|
|
|
112
115
|
throw new Error("unblock-memory memoryWhisperer.minUsefulness must be between 0 and 1");
|
|
113
116
|
}
|
|
114
117
|
return {
|
|
115
|
-
enabled, corpora: [...new Set(selected)], historyMessages, cooldownTurns, minUsefulness,
|
|
118
|
+
enabled, complementaryHints, corpora: [...new Set(selected)], historyMessages, cooldownTurns, minUsefulness,
|
|
116
119
|
maxHints: positiveInteger(config.maxHints, 2, "memoryWhisperer.maxHints", 2),
|
|
117
120
|
timeoutMs: positiveInteger(config.timeoutMs, 3000, "memoryWhisperer.timeoutMs", 10_000),
|
|
118
121
|
};
|
|
@@ -258,6 +261,7 @@ export function resolveConfig(value) {
|
|
|
258
261
|
analysis: {},
|
|
259
262
|
typesafe: { ...DEFAULT_TYPESAFE_CONFIG },
|
|
260
263
|
qualityAudit: { ...DEFAULT_QUALITY_AUDIT },
|
|
264
|
+
evidenceReview: { enabled: false, corpora: [] },
|
|
261
265
|
people: DEFAULT_PEOPLE_CONFIG,
|
|
262
266
|
skillWhisperer: DEFAULT_SKILL_WHISPERER,
|
|
263
267
|
memoryWhisperer: { ...DEFAULT_MEMORY_WHISPERER },
|
|
@@ -267,9 +271,23 @@ export function resolveConfig(value) {
|
|
|
267
271
|
throw new Error("unblock-memory config must be an object");
|
|
268
272
|
}
|
|
269
273
|
const config = value;
|
|
270
|
-
assertOnlyKeys(config, ["corpora", "keepEmbeddingModelWarm", "analysis", "people", "skillWhisperer", "memoryWhisperer", "typesafe", "qualityAudit"], "config");
|
|
274
|
+
assertOnlyKeys(config, ["corpora", "keepEmbeddingModelWarm", "analysis", "people", "skillWhisperer", "memoryWhisperer", "typesafe", "qualityAudit", "evidenceReview"], "config");
|
|
271
275
|
const corpora = resolveCorpora(config.corpora);
|
|
272
276
|
const people = resolvePeople(config.people);
|
|
277
|
+
let evidenceReview = { enabled: false, corpora: [] };
|
|
278
|
+
if (config.evidenceReview !== undefined) {
|
|
279
|
+
const value = config.evidenceReview;
|
|
280
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
281
|
+
throw new Error("evidenceReview must be an object");
|
|
282
|
+
assertOnlyKeys(value, ["enabled", "corpora"], "evidenceReview");
|
|
283
|
+
try {
|
|
284
|
+
const approved = resolveQualityAudit(value, corpora);
|
|
285
|
+
evidenceReview = { enabled: approved.enabled, corpora: approved.corpora };
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
throw new Error("evidenceReview requires a boolean enabled and explicit configured non-skill corpora when enabled");
|
|
289
|
+
}
|
|
290
|
+
}
|
|
273
291
|
if (config.keepEmbeddingModelWarm !== undefined &&
|
|
274
292
|
typeof config.keepEmbeddingModelWarm !== "boolean") {
|
|
275
293
|
throw new Error("unblock-memory keepEmbeddingModelWarm must be a boolean");
|
|
@@ -328,5 +346,6 @@ export function resolveConfig(value) {
|
|
|
328
346
|
}
|
|
329
347
|
return { corpora, keepEmbeddingModelWarm, analysis: analysisConfig, people, skillWhisperer,
|
|
330
348
|
qualityAudit: resolveQualityAudit(config.qualityAudit, corpora),
|
|
349
|
+
evidenceReview,
|
|
331
350
|
memoryWhisperer: resolveMemoryWhisperer(config.memoryWhisperer, corpora), typesafe: resolveTypeSafe(config.typesafe) };
|
|
332
351
|
}
|
package/dist/src/curation.js
CHANGED
|
@@ -155,7 +155,10 @@ export class CurationStore {
|
|
|
155
155
|
return this.#db.prepare(`
|
|
156
156
|
SELECT * FROM maintenance_tasks
|
|
157
157
|
WHERE status = ?
|
|
158
|
-
ORDER BY
|
|
158
|
+
ORDER BY CASE WHEN type = 'quality_review' AND json_valid(detail) THEN
|
|
159
|
+
CASE WHEN json_extract(detail, '$.evidence') >= 0.8 AND
|
|
160
|
+
(json_extract(detail, '$.noise') >= 0.8 OR reason = 'possible_double_encoded_message') THEN 0 ELSE 1 END
|
|
161
|
+
ELSE 1 END, created_at, id
|
|
159
162
|
LIMIT ?
|
|
160
163
|
`).all(status, limit).map((row) => task(row));
|
|
161
164
|
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
type Whisperer = "skill" | "memory";
|
|
2
|
+
type Outcome = "missing_key" | "typesafe_disabled" | "no_candidates" | "rejected" | "cooldown" | "emitted" | "failed" | "timed_out" | "cancelled" | "unavailable" | "payload_limit" | "redundancy_unavailable";
|
|
3
|
+
/** Process-local, content-free and bounded. Agent IDs are keys, never included in snapshots. */
|
|
4
|
+
export declare class WhispererDiagnostics {
|
|
5
|
+
#private;
|
|
6
|
+
record(agentId: string, whisperer: Whisperer, outcome: Outcome): void;
|
|
7
|
+
snapshot(agentId: string): {
|
|
8
|
+
skill: {
|
|
9
|
+
unavailable?: number | undefined;
|
|
10
|
+
rejected?: number | undefined;
|
|
11
|
+
failed?: number | undefined;
|
|
12
|
+
missing_key?: number | undefined;
|
|
13
|
+
typesafe_disabled?: number | undefined;
|
|
14
|
+
no_candidates?: number | undefined;
|
|
15
|
+
cooldown?: number | undefined;
|
|
16
|
+
emitted?: number | undefined;
|
|
17
|
+
timed_out?: number | undefined;
|
|
18
|
+
cancelled?: number | undefined;
|
|
19
|
+
payload_limit?: number | undefined;
|
|
20
|
+
redundancy_unavailable?: number | undefined;
|
|
21
|
+
};
|
|
22
|
+
memory: {
|
|
23
|
+
unavailable?: number | undefined;
|
|
24
|
+
rejected?: number | undefined;
|
|
25
|
+
failed?: number | undefined;
|
|
26
|
+
missing_key?: number | undefined;
|
|
27
|
+
typesafe_disabled?: number | undefined;
|
|
28
|
+
no_candidates?: number | undefined;
|
|
29
|
+
cooldown?: number | undefined;
|
|
30
|
+
emitted?: number | undefined;
|
|
31
|
+
timed_out?: number | undefined;
|
|
32
|
+
cancelled?: number | undefined;
|
|
33
|
+
payload_limit?: number | undefined;
|
|
34
|
+
redundancy_unavailable?: number | undefined;
|
|
35
|
+
};
|
|
36
|
+
scope: string;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Process-local, content-free and bounded. Agent IDs are keys, never included in snapshots. */
|
|
2
|
+
export class WhispererDiagnostics {
|
|
3
|
+
#agents = new Map();
|
|
4
|
+
record(agentId, whisperer, outcome) {
|
|
5
|
+
let entry = this.#agents.get(agentId);
|
|
6
|
+
if (!entry) {
|
|
7
|
+
if (this.#agents.size >= 100)
|
|
8
|
+
this.#agents.delete(this.#agents.keys().next().value);
|
|
9
|
+
entry = { skill: {}, memory: {} };
|
|
10
|
+
this.#agents.set(agentId, entry);
|
|
11
|
+
}
|
|
12
|
+
entry[whisperer][outcome] = Math.min(Number.MAX_SAFE_INTEGER, (entry[whisperer][outcome] ?? 0) + 1);
|
|
13
|
+
}
|
|
14
|
+
snapshot(agentId) {
|
|
15
|
+
const entry = this.#agents.get(agentId);
|
|
16
|
+
return { skill: { ...entry?.skill }, memory: { ...entry?.memory }, scope: "process lifetime; up to 100 agents" };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { QMDStore } from "@unblocklabs/qmd";
|
|
2
|
+
import { type ResolvedSource } from "./sources.js";
|
|
3
|
+
export type EvidenceCitation = {
|
|
4
|
+
path: string;
|
|
5
|
+
from: number;
|
|
6
|
+
lines: number;
|
|
7
|
+
};
|
|
8
|
+
export declare function reviewIndexedClaim(params: {
|
|
9
|
+
db: QMDStore["internal"]["db"];
|
|
10
|
+
sources: readonly ResolvedSource[];
|
|
11
|
+
claim: string;
|
|
12
|
+
citations: readonly EvidenceCitation[];
|
|
13
|
+
apiKey: string;
|
|
14
|
+
timeoutMs: number;
|
|
15
|
+
signal: AbortSignal;
|
|
16
|
+
read?: <T>(run: () => T) => Promise<T>;
|
|
17
|
+
}): Promise<{
|
|
18
|
+
status: "unavailable";
|
|
19
|
+
verdict: "insufficient_evidence";
|
|
20
|
+
needsReview: boolean;
|
|
21
|
+
reason: string;
|
|
22
|
+
} | {
|
|
23
|
+
evidence: {
|
|
24
|
+
path: string;
|
|
25
|
+
from: number;
|
|
26
|
+
lines: number;
|
|
27
|
+
documentHash: string;
|
|
28
|
+
excerptHash: string;
|
|
29
|
+
}[];
|
|
30
|
+
policy: string;
|
|
31
|
+
scope: string;
|
|
32
|
+
verdict: "supports" | "contradicts" | "insufficient_evidence";
|
|
33
|
+
confidence: number;
|
|
34
|
+
probabilities: {
|
|
35
|
+
supports: number;
|
|
36
|
+
contradicts: number;
|
|
37
|
+
insufficient_evidence: number;
|
|
38
|
+
};
|
|
39
|
+
needsReview: boolean;
|
|
40
|
+
status: "ok";
|
|
41
|
+
}>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { parseSafeVirtualPath } from "./sources.js";
|
|
3
|
+
import { reviewTypeSafeClaim } from "./typesafe-review.js";
|
|
4
|
+
export async function reviewIndexedClaim(params) {
|
|
5
|
+
const unavailable = (reason) => ({ status: "unavailable", verdict: "insufficient_evidence", needsReview: true, reason });
|
|
6
|
+
if (!params.claim.trim() || params.claim.length > 2000 || !params.citations.length || params.citations.length > 3)
|
|
7
|
+
return unavailable("Invalid review bounds");
|
|
8
|
+
params.signal.throwIfAborted();
|
|
9
|
+
const sources = new Map(params.sources.filter(source => source.kind !== "skills").map(source => [source.collection, source]));
|
|
10
|
+
const read = params.read ?? (async (run) => run());
|
|
11
|
+
const snapshot = await read(() => {
|
|
12
|
+
const evidence = [];
|
|
13
|
+
for (const citation of params.citations) {
|
|
14
|
+
const safe = parseSafeVirtualPath(citation.path, sources);
|
|
15
|
+
if (!safe || !Number.isInteger(citation.from) || citation.from < 1 || !Number.isInteger(citation.lines) || citation.lines < 1 || citation.lines > 120)
|
|
16
|
+
return unavailable("Evidence is unavailable or outside approved corpora");
|
|
17
|
+
const row = params.db.prepare(`SELECT d.hash, c.doc FROM documents d JOIN content c ON c.hash = d.hash
|
|
18
|
+
WHERE d.active = 1 AND d.collection = ? AND d.path = ?`).get(safe.source.collection, safe.relativePath);
|
|
19
|
+
if (!row)
|
|
20
|
+
return unavailable("Evidence is not indexed");
|
|
21
|
+
const lines = row.doc.split("\n");
|
|
22
|
+
if (citation.from > lines.length)
|
|
23
|
+
return unavailable("Evidence range is outside the indexed source");
|
|
24
|
+
const text = lines.slice(citation.from - 1, citation.from - 1 + citation.lines).join("\n");
|
|
25
|
+
if (!text.trim())
|
|
26
|
+
return unavailable("Evidence range is empty");
|
|
27
|
+
evidence.push({ path: safe.normalized, from: citation.from, lines: Math.min(citation.lines, lines.length - citation.from + 1),
|
|
28
|
+
text, documentHash: row.hash, excerptHash: createHash("sha256").update(text).digest("hex") });
|
|
29
|
+
}
|
|
30
|
+
return { status: "ready", evidence };
|
|
31
|
+
});
|
|
32
|
+
if (snapshot.status !== "ready")
|
|
33
|
+
return snapshot;
|
|
34
|
+
const { evidence } = snapshot;
|
|
35
|
+
if (evidence.reduce((sum, item) => sum + item.text.length, 0) > 6000)
|
|
36
|
+
return unavailable("Evidence exceeds 6000 characters; choose a narrower complete passage");
|
|
37
|
+
const judgment = await reviewTypeSafeClaim({ ...params, evidence: evidence.map(item => item.text) });
|
|
38
|
+
params.signal.throwIfAborted();
|
|
39
|
+
return read(() => {
|
|
40
|
+
for (const item of evidence) {
|
|
41
|
+
const safe = parseSafeVirtualPath(item.path, sources);
|
|
42
|
+
if (!safe || !params.db.prepare("SELECT 1 FROM documents WHERE active = 1 AND collection = ? AND path = ? AND hash = ?")
|
|
43
|
+
.get(safe.source.collection, safe.relativePath, item.documentHash))
|
|
44
|
+
return unavailable("Indexed evidence changed during review; retry");
|
|
45
|
+
}
|
|
46
|
+
return { status: "ok", ...judgment,
|
|
47
|
+
evidence: evidence.map(({ text: _text, ...citation }) => citation),
|
|
48
|
+
policy: "jev-1.13.0:claim-v1", scope: "Advisory support check against cited indexed excerpts only, not current truth or authorization to write. Verify original sources and identity before promotion." };
|
|
49
|
+
});
|
|
50
|
+
}
|
package/dist/src/manager.d.ts
CHANGED
|
@@ -2,10 +2,13 @@ import type { QMDStore, VectorSearchResult } from "@unblocklabs/qmd";
|
|
|
2
2
|
import { type AnalysisRunner, type MemoryAnalysisSummary, type MemoryClusterDetail, type MemoryClusterList, type MemoryClusterSort, type MemoryReclusterOptions } from "./analysis.js";
|
|
3
3
|
import type { CorpusMemorySearchResult, CorpusSearchOptions, MemoryEmbeddingProbeResult, MemoryProviderStatus, MemoryReadResult, MemoryRequestContext, MemorySearchManagerContract, MemorySyncParams } from "./contracts.js";
|
|
4
4
|
import type { ChatType } from "./config.js";
|
|
5
|
-
import { type MaintenanceStatus, type TemporalBasis } from "./curation.js";
|
|
5
|
+
import { type MaintenanceStatus, type MaintenanceTask, type TemporalBasis } from "./curation.js";
|
|
6
6
|
import { type SessionSyncResult } from "./session-sync.js";
|
|
7
7
|
import { type ResolvedSource } from "./sources.js";
|
|
8
8
|
import { type QualityCursor } from "./quality-audit.js";
|
|
9
|
+
import { qualityTaskPresence } from "./quality-triage.js";
|
|
10
|
+
import { reviewIndexedClaim } from "./evidence-review.js";
|
|
11
|
+
import { reviewClusterIngestion } from "./cluster-review.js";
|
|
9
12
|
export type ManagerStore = Pick<QMDStore, "update" | "embed" | "getStatus" | "listCollections" | "searchLex" | "vsearch" | "get" | "getDocumentBody" | "close">;
|
|
10
13
|
export type ManagerSessionConfig = {
|
|
11
14
|
agentId: string;
|
|
@@ -40,6 +43,15 @@ export declare function expandSessionSearchHit(result: Pick<VectorSearchResult,
|
|
|
40
43
|
}>;
|
|
41
44
|
export declare class QmdMemoryManager implements MemorySearchManagerContract {
|
|
42
45
|
#private;
|
|
46
|
+
diagnostics(): Promise<{
|
|
47
|
+
projectorVersion: number;
|
|
48
|
+
semanticChunkingVersion: number | null | undefined;
|
|
49
|
+
sessionsNeedingProjection: number;
|
|
50
|
+
needsEmbedding: number;
|
|
51
|
+
embeddingReady: boolean;
|
|
52
|
+
structuralChunksOmitted: number | null;
|
|
53
|
+
scope: string;
|
|
54
|
+
}>;
|
|
43
55
|
constructor(params: {
|
|
44
56
|
dbPath: string;
|
|
45
57
|
curationPath?: string;
|
|
@@ -56,6 +68,71 @@ export declare class QmdMemoryManager implements MemorySearchManagerContract {
|
|
|
56
68
|
syncSessions(force?: boolean, onPhase?: (phase: "projecting" | "indexing") => void): Promise<SessionSyncResult>;
|
|
57
69
|
recluster(options?: MemoryReclusterOptions, signal?: AbortSignal): Promise<MemoryAnalysisSummary>;
|
|
58
70
|
listClusters(limit?: number): Promise<MemoryClusterList>;
|
|
71
|
+
reviewClaim(params: Omit<Parameters<typeof reviewIndexedClaim>[0], "db" | "sources" | "read"> & {
|
|
72
|
+
corpora: readonly string[];
|
|
73
|
+
}): Promise<{
|
|
74
|
+
status: "unavailable";
|
|
75
|
+
verdict: "insufficient_evidence";
|
|
76
|
+
needsReview: boolean;
|
|
77
|
+
reason: string;
|
|
78
|
+
} | {
|
|
79
|
+
evidence: {
|
|
80
|
+
path: string;
|
|
81
|
+
from: number;
|
|
82
|
+
lines: number;
|
|
83
|
+
documentHash: string;
|
|
84
|
+
excerptHash: string;
|
|
85
|
+
}[];
|
|
86
|
+
policy: string;
|
|
87
|
+
scope: string;
|
|
88
|
+
verdict: "supports" | "contradicts" | "insufficient_evidence";
|
|
89
|
+
confidence: number;
|
|
90
|
+
probabilities: {
|
|
91
|
+
supports: number;
|
|
92
|
+
contradicts: number;
|
|
93
|
+
insufficient_evidence: number;
|
|
94
|
+
};
|
|
95
|
+
needsReview: boolean;
|
|
96
|
+
status: "ok";
|
|
97
|
+
}>;
|
|
98
|
+
reviewCluster(params: Omit<Parameters<typeof reviewClusterIngestion>[0], "db" | "sources" | "read"> & {
|
|
99
|
+
corpora: readonly string[];
|
|
100
|
+
}): Promise<{
|
|
101
|
+
status: "unavailable";
|
|
102
|
+
reason: string;
|
|
103
|
+
sample?: undefined;
|
|
104
|
+
considered?: undefined;
|
|
105
|
+
runId?: undefined;
|
|
106
|
+
clusterSize?: undefined;
|
|
107
|
+
} | {
|
|
108
|
+
status: "ok";
|
|
109
|
+
runId: string;
|
|
110
|
+
clusterId: string;
|
|
111
|
+
members: {
|
|
112
|
+
flagged: boolean;
|
|
113
|
+
defect: "encoding" | "wrapper" | "boilerplate" | "none_or_uncertain";
|
|
114
|
+
confidence: number;
|
|
115
|
+
path: string;
|
|
116
|
+
hash: string;
|
|
117
|
+
seq: number;
|
|
118
|
+
from: number;
|
|
119
|
+
fingerprint: string;
|
|
120
|
+
}[];
|
|
121
|
+
recurring: {
|
|
122
|
+
defect: string;
|
|
123
|
+
examples: {
|
|
124
|
+
path: string;
|
|
125
|
+
from: number;
|
|
126
|
+
fingerprint: string;
|
|
127
|
+
}[];
|
|
128
|
+
}[];
|
|
129
|
+
sampled: number;
|
|
130
|
+
considered: number;
|
|
131
|
+
clusterSize: number | undefined;
|
|
132
|
+
policy: string;
|
|
133
|
+
scope: string;
|
|
134
|
+
reason?: undefined;
|
|
135
|
+
}>;
|
|
59
136
|
fetchCluster(params: {
|
|
60
137
|
clusterId: string;
|
|
61
138
|
topK?: number;
|
|
@@ -65,7 +142,9 @@ export declare class QmdMemoryManager implements MemorySearchManagerContract {
|
|
|
65
142
|
listMaintenanceTasks(params?: {
|
|
66
143
|
status?: MaintenanceStatus;
|
|
67
144
|
limit?: number;
|
|
68
|
-
}):
|
|
145
|
+
}): Promise<(MaintenanceTask & {
|
|
146
|
+
indexPresence?: ReturnType<typeof qualityTaskPresence>;
|
|
147
|
+
})[]>;
|
|
69
148
|
auditQuality(params: {
|
|
70
149
|
corpora: readonly string[];
|
|
71
150
|
apiKey: string;
|
|
@@ -89,7 +168,8 @@ export declare class QmdMemoryManager implements MemorySearchManagerContract {
|
|
|
89
168
|
source: string;
|
|
90
169
|
reason: string;
|
|
91
170
|
pending: number;
|
|
92
|
-
examples:
|
|
171
|
+
examples: MaintenanceTask[];
|
|
172
|
+
triage: ReturnType<typeof import("./quality-triage.js").qualityTriage>;
|
|
93
173
|
}[];
|
|
94
174
|
policy: string;
|
|
95
175
|
scope: string;
|
|
@@ -108,7 +188,7 @@ export declare class QmdMemoryManager implements MemorySearchManagerContract {
|
|
|
108
188
|
basis: TemporalBasis;
|
|
109
189
|
evidence: string;
|
|
110
190
|
};
|
|
111
|
-
}):
|
|
191
|
+
}): MaintenanceTask | undefined;
|
|
112
192
|
search(query: string, opts?: CorpusSearchOptions): Promise<CorpusMemorySearchResult[]>;
|
|
113
193
|
searchSkills(query: string, minScore: number, limit: number): Promise<SkillSearchCandidate[]>;
|
|
114
194
|
readFile(params: {
|