@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.
Files changed (39) hide show
  1. package/README.md +67 -0
  2. package/dist/src/abortable.d.ts +2 -0
  3. package/dist/src/abortable.js +21 -0
  4. package/dist/src/cluster-review.d.ts +47 -0
  5. package/dist/src/cluster-review.js +64 -0
  6. package/dist/src/config.d.ts +5 -0
  7. package/dist/src/config.js +22 -3
  8. package/dist/src/curation.js +4 -1
  9. package/dist/src/diagnostics.d.ts +39 -0
  10. package/dist/src/diagnostics.js +18 -0
  11. package/dist/src/evidence-review.d.ts +41 -0
  12. package/dist/src/evidence-review.js +50 -0
  13. package/dist/src/manager.d.ts +84 -4
  14. package/dist/src/manager.js +72 -3
  15. package/dist/src/memory-whisperer.d.ts +2 -1
  16. package/dist/src/memory-whisperer.js +45 -9
  17. package/dist/src/plugin.js +8 -20
  18. package/dist/src/quality-audit.d.ts +3 -0
  19. package/dist/src/quality-audit.js +6 -3
  20. package/dist/src/quality-triage.d.ts +9 -0
  21. package/dist/src/quality-triage.js +38 -0
  22. package/dist/src/review-tools.d.ts +5 -0
  23. package/dist/src/review-tools.js +116 -0
  24. package/dist/src/session-noise.d.ts +20 -0
  25. package/dist/src/session-noise.js +142 -0
  26. package/dist/src/session-projector.d.ts +6 -0
  27. package/dist/src/session-projector.js +16 -0
  28. package/dist/src/session-sync.d.ts +3 -1
  29. package/dist/src/session-sync.js +4 -1
  30. package/dist/src/skill-whisperer.d.ts +2 -1
  31. package/dist/src/skill-whisperer.js +24 -8
  32. package/dist/src/tool-context.d.ts +7 -0
  33. package/dist/src/tool-context.js +17 -0
  34. package/dist/src/typesafe-review.d.ts +40 -0
  35. package/dist/src/typesafe-review.js +133 -0
  36. package/openclaw.plugin.json +17 -1
  37. package/package.json +2 -2
  38. package/skills/memory-curator/SKILL.md +11 -0
  39. 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,2 @@
1
+ /** Stop waiting without cancelling shared work that other callers still need. */
2
+ export declare function abortable<T>(pending: Promise<T>, signal?: AbortSignal): Promise<T>;
@@ -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
+ }
@@ -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;
@@ -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
  }
@@ -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 created_at, id
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
+ }
@@ -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
- }): import("./curation.js").MaintenanceTask[];
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: import("./curation.js").MaintenanceTask[];
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
- }): import("./curation.js").MaintenanceTask | undefined;
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: {