@unblocklabs/unblock-memory 0.3.27 → 0.3.29
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 +8 -0
- package/dist/src/agent-transcript.js +29 -0
- package/dist/src/cluster-review.js +1 -1
- package/dist/src/config.d.ts +4 -0
- package/dist/src/config.js +14 -1
- package/dist/src/date-option.d.ts +1 -0
- package/dist/src/date-option.js +8 -0
- package/dist/src/diagnostics.d.ts +13 -4
- package/dist/src/diagnostics.js +5 -0
- package/dist/src/manager.d.ts +3 -1
- package/dist/src/manager.js +61 -57
- package/dist/src/memory-whisperer.d.ts +2 -1
- package/dist/src/memory-whisperer.js +241 -114
- package/dist/src/mlx-query.d.ts +22 -0
- package/dist/src/mlx-query.js +155 -0
- package/dist/src/people-dossier-review.js +1 -1
- package/dist/src/people-hooks.d.ts +1 -1
- package/dist/src/people-hooks.js +13 -7
- package/dist/src/people-primer-tool.js +1 -1
- package/dist/src/people-primer.js +3 -2
- package/dist/src/plugin.js +10 -5
- package/dist/src/quality-audit.js +86 -81
- package/dist/src/response-audit.js +1 -1
- package/dist/src/response-episodes.js +4 -11
- package/dist/src/response-judge.d.ts +1 -1
- package/dist/src/response-judge.js +18 -15
- package/dist/src/response-runtime.js +1 -8
- package/dist/src/response-stages.d.ts +8 -8
- package/dist/src/response-text.d.ts +5 -0
- package/dist/src/response-text.js +23 -1
- package/dist/src/retrieval-telemetry.d.ts +2 -2
- package/dist/src/retrieval-telemetry.js +1 -1
- package/dist/src/review-tools.js +1 -1
- package/dist/src/session-sync.js +8 -22
- package/dist/src/skill-whisperer.d.ts +1 -2
- package/dist/src/skill-whisperer.js +77 -72
- package/dist/src/training-candidates.d.ts +9 -1
- package/dist/src/training-candidates.js +12 -7
- package/dist/src/training-gate.js +3 -3
- package/dist/src/training-input.js +6 -34
- package/dist/src/training-judge.js +3 -3
- package/dist/src/training-queries.js +5 -4
- package/dist/src/training-runtime.js +1 -8
- package/dist/src/training.js +5 -4
- package/dist/src/typesafe-client.d.ts +15 -0
- package/dist/src/typesafe-client.js +75 -0
- package/dist/src/typesafe-review.d.ts +2 -6
- package/dist/src/typesafe-review.js +49 -55
- package/dist/src/typesafe.d.ts +3 -7
- package/dist/src/typesafe.js +108 -163
- package/dist/src/whisperer-context.d.ts +1 -0
- package/dist/src/whisperer-context.js +9 -0
- package/dist/src/whisperer-prompt.d.ts +3 -0
- package/dist/src/whisperer-prompt.js +21 -0
- package/docs/configuration.md +12 -2
- package/docs/peoplesql.md +3 -1
- package/docs/response-audit.md +4 -1
- package/docs/retrieval.md +128 -20
- package/openclaw.plugin.json +13 -4
- package/package.json +2 -1
- package/src/mlx-query-worker.py +104 -0
- package/dist/src/typesafe-transport.d.ts +0 -10
- package/dist/src/typesafe-transport.js +0 -26
package/dist/src/people-hooks.js
CHANGED
|
@@ -20,6 +20,11 @@ export function renderPeopleWhisper(blurb, maxChars) {
|
|
|
20
20
|
const normalized = blurb.trim();
|
|
21
21
|
return normalized ? normalized.slice(0, maxChars) : undefined;
|
|
22
22
|
}
|
|
23
|
+
function peopleContext(contribution) {
|
|
24
|
+
// Escape only at output: durable receipts retain the original dossier text.
|
|
25
|
+
const text = contribution.replace(/&/gu, "&").replace(/</gu, "<").replace(/>/gu, ">");
|
|
26
|
+
return { appendContext: `<people>${text}</people>` };
|
|
27
|
+
}
|
|
23
28
|
export function registerPeopleHooks(api, stores, config) {
|
|
24
29
|
const threadByRun = new Map();
|
|
25
30
|
const pendingThreadByIdentity = new Map();
|
|
@@ -83,7 +88,7 @@ export function registerPeopleHooks(api, stores, config) {
|
|
|
83
88
|
});
|
|
84
89
|
if (!config.whisperer.enabled)
|
|
85
90
|
return;
|
|
86
|
-
|
|
91
|
+
const beforePrompt = (_event, context) => {
|
|
87
92
|
if (context.trigger !== "user" || context.messageProvider !== "slack")
|
|
88
93
|
return;
|
|
89
94
|
const sessionKey = nonBlank(context.sessionKey);
|
|
@@ -112,30 +117,31 @@ export function registerPeopleHooks(api, stores, config) {
|
|
|
112
117
|
return;
|
|
113
118
|
const previous = store.getWhisperReceipt(threadKey, person.id);
|
|
114
119
|
if (previous) {
|
|
115
|
-
return previous.runId === runId ?
|
|
120
|
+
return previous.runId === runId ? peopleContext(previous.contribution) : undefined;
|
|
116
121
|
}
|
|
117
122
|
const blurb = store.getDossierBlurb(person.id);
|
|
118
|
-
const
|
|
123
|
+
const contribution = blurb
|
|
119
124
|
? renderPeopleWhisper(blurb, config.whisperer.maxChars)
|
|
120
125
|
: undefined;
|
|
121
|
-
if (!
|
|
126
|
+
if (!contribution)
|
|
122
127
|
return;
|
|
123
128
|
const receipt = store.recordWhisperReceipt({
|
|
124
129
|
threadKey,
|
|
125
130
|
personId: person.id,
|
|
126
131
|
runId,
|
|
127
|
-
contribution
|
|
132
|
+
contribution,
|
|
128
133
|
});
|
|
129
|
-
return receipt.runId === runId ?
|
|
134
|
+
return receipt.runId === runId ? peopleContext(receipt.contribution) : undefined;
|
|
130
135
|
}
|
|
131
136
|
catch (error) {
|
|
132
137
|
api.logger.warn(`unblock-memory people whisperer lookup failed: ${String(error)}`);
|
|
133
138
|
return;
|
|
134
139
|
}
|
|
135
|
-
}
|
|
140
|
+
};
|
|
136
141
|
api.on("agent_end", (event, context) => {
|
|
137
142
|
const runId = nonBlank(event.runId) ?? nonBlank(context.runId);
|
|
138
143
|
if (runId)
|
|
139
144
|
threadByRun.delete(runId);
|
|
140
145
|
});
|
|
146
|
+
return beforePrompt;
|
|
141
147
|
}
|
|
@@ -2,7 +2,7 @@ import { Type } from "typebox";
|
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
3
|
import { jsonResult, resolveAgentIdentity } from "openclaw/plugin-sdk/agent-runtime";
|
|
4
4
|
import { getContext } from "./tool-context.js";
|
|
5
|
-
import { resolveTypeSafeApiKey } from "./typesafe.js";
|
|
5
|
+
import { resolveTypeSafeApiKey } from "./typesafe-client.js";
|
|
6
6
|
import { primePersonDossier } from "./people-primer.js";
|
|
7
7
|
import { abortable } from "./abortable.js";
|
|
8
8
|
import { backgroundWordCount, PEOPLE_BACKGROUND_MAX_WORDS } from "./people-background.js";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
3
|
import { Value } from "typebox/value";
|
|
4
|
-
import {
|
|
4
|
+
import { requestTypeSafe } from "./typesafe-client.js";
|
|
5
|
+
import { TYPESAFE_REVIEW_MODEL } from "./typesafe-review.js";
|
|
5
6
|
import { abortable } from "./abortable.js";
|
|
6
7
|
const VERSION = "people-primer-background-v4";
|
|
7
8
|
const MAX_EXCERPT_CHARS = 6000;
|
|
@@ -112,7 +113,7 @@ export async function primePersonDossier(params) {
|
|
|
112
113
|
cached++;
|
|
113
114
|
else {
|
|
114
115
|
requests++;
|
|
115
|
-
payload = await
|
|
116
|
+
payload = await requestTypeSafe({ apiKey: params.apiKey, timeoutMs: config.timeoutMs, signal }, state, questions);
|
|
116
117
|
signal.throwIfAborted();
|
|
117
118
|
if (!valid(payload) || !Value.Check(answerSchema, payload))
|
|
118
119
|
throw new Error("Invalid primer judgments");
|
package/dist/src/plugin.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Type } from "typebox";
|
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
3
|
import { jsonResult } from "openclaw/plugin-sdk/agent-runtime";
|
|
4
4
|
import { resolveConfig } from "./config.js";
|
|
5
|
-
import { resolveTypeSafeApiKey } from "./typesafe.js";
|
|
5
|
+
import { resolveTypeSafeApiKey } from "./typesafe-client.js";
|
|
6
6
|
import { registerPeopleHooks } from "./people-hooks.js";
|
|
7
7
|
import { PeopleStores } from "./people-store.js";
|
|
8
8
|
import { registerPeopleTools } from "./people-tools.js";
|
|
@@ -10,6 +10,7 @@ import { registerPeoplePrimerTool } from "./people-primer-tool.js";
|
|
|
10
10
|
import { QmdMemoryRuntime } from "./runtime.js";
|
|
11
11
|
import { registerSkillWhisperer } from "./skill-whisperer.js";
|
|
12
12
|
import { registerMemoryWhisperer } from "./memory-whisperer.js";
|
|
13
|
+
import { registerWhispererPrompt } from "./whisperer-prompt.js";
|
|
13
14
|
import { getContext } from "./tool-context.js";
|
|
14
15
|
import { WhispererDiagnostics } from "./diagnostics.js";
|
|
15
16
|
import { registerReviewTools } from "./review-tools.js";
|
|
@@ -438,19 +439,23 @@ export function registerUnblockMemory(api) {
|
|
|
438
439
|
}));
|
|
439
440
|
api.on("gateway_stop", () => runtime.stopSessionSyncSchedule());
|
|
440
441
|
}
|
|
442
|
+
let peopleWhisperer;
|
|
441
443
|
if (config.people.enabled) {
|
|
442
444
|
const peopleStores = new PeopleStores({
|
|
443
445
|
maxOpenTodos: config.people.todos.maxOpen,
|
|
444
446
|
maxBlurbChars: config.people.whisperer.maxChars,
|
|
445
447
|
});
|
|
446
|
-
registerPeopleHooks(api, peopleStores, config.people);
|
|
448
|
+
peopleWhisperer = registerPeopleHooks(api, peopleStores, config.people);
|
|
447
449
|
registerPeopleTools(api, peopleStores, config, runtime);
|
|
448
450
|
registerPeoplePrimerTool(api, runtime, peopleStores, config);
|
|
449
451
|
api.on("gateway_stop", () => peopleStores.closeAll());
|
|
450
452
|
}
|
|
451
|
-
const diagnostics =
|
|
452
|
-
|
|
453
|
-
|
|
453
|
+
const diagnostics = WhispererDiagnostics.shared();
|
|
454
|
+
registerWhispererPrompt(api, {
|
|
455
|
+
memory: registerMemoryWhisperer(api, runtime, config.memoryWhisperer, config.typesafe, diagnostics),
|
|
456
|
+
skill: registerSkillWhisperer(api, runtime, config.skillWhisperer, config.typesafe, diagnostics),
|
|
457
|
+
people: peopleWhisperer,
|
|
458
|
+
});
|
|
454
459
|
api.registerTool((ctx) => createSearchTool(runtime, ctx), { names: ["memory_search"] });
|
|
455
460
|
api.registerTool((ctx) => createGetTool(runtime, ctx), { names: ["memory_get"] });
|
|
456
461
|
api.registerTool((ctx) => createSyncSessionsTool(runtime, ctx), {
|
|
@@ -3,7 +3,6 @@ import { parseSafeVirtualPath } from "./sources.js";
|
|
|
3
3
|
import { judgeTypeSafeQuality, QUALITY_JUDGE_VERSION } from "./typesafe.js";
|
|
4
4
|
import { qualityTriage } from "./quality-triage.js";
|
|
5
5
|
const MAX_CHUNK_CHARS = 6000;
|
|
6
|
-
const BATCH_SIZE = 4;
|
|
7
6
|
/** A formatting clue, never proof that JSON or structured data is worthless. */
|
|
8
7
|
export function qualityStructure(text) {
|
|
9
8
|
if (!text.trim())
|
|
@@ -61,89 +60,95 @@ export async function auditQualityPage(params) {
|
|
|
61
60
|
AND cv.seq = ? AND cv.pos = ? AND cv.chunk_len = ?`);
|
|
62
61
|
const page = rows.slice(0, limit);
|
|
63
62
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
63
|
+
check();
|
|
64
|
+
const batch = page.map(row => {
|
|
65
|
+
const source = sources.get(row.collection);
|
|
66
|
+
const text = row.doc.slice(row.pos, row.pos + row.chunk_len);
|
|
67
|
+
const fingerprint = chunkFingerprint(text);
|
|
68
|
+
const cacheKey = chunkFingerprint(JSON.stringify([QUALITY_JUDGE_VERSION, source.kind, fingerprint]));
|
|
69
|
+
const eligible = Boolean(parseSafeVirtualPath(`qmd://${source.collection}/${row.path}`, sources)) && row.pos >= 0 && row.chunk_len > 0 &&
|
|
70
|
+
row.pos + row.chunk_len <= row.doc.length;
|
|
71
|
+
const structure = qualityStructure(text);
|
|
72
|
+
const judgment = eligible && text.length <= MAX_CHUNK_CHARS
|
|
73
|
+
? curation.qualityJudgment(cacheKey) : undefined;
|
|
74
|
+
return { row, source, text, fingerprint, cacheKey, structure, judgment, eligible };
|
|
75
|
+
});
|
|
76
|
+
const missing = [...new Map(batch.filter(item => item.eligible && item.text.length <= MAX_CHUNK_CHARS &&
|
|
77
|
+
item.structure !== "empty" && !item.judgment).map(item => [item.cacheKey, item])).values()];
|
|
78
|
+
const answers = await Promise.allSettled(missing.map(async (item) => (await judgeTypeSafeQuality({
|
|
79
|
+
apiKey: params.apiKey, timeoutMs: params.timeoutMs, signal,
|
|
80
|
+
chunks: [{ text: item.text, sourceKind: item.source.kind === "sessions" ? "sessions" : "files" }],
|
|
81
|
+
}))[0]));
|
|
82
|
+
check();
|
|
83
|
+
const fresh = new Map();
|
|
84
|
+
for (const [index, answer] of answers.entries()) {
|
|
85
|
+
if (answer.status !== "fulfilled")
|
|
86
|
+
continue;
|
|
87
|
+
const { cacheKey, row } = missing[index];
|
|
88
|
+
if (!current.get(row.document_id, row.collection, row.path, row.hash, row.seq, row.pos, row.chunk_len))
|
|
89
|
+
continue;
|
|
90
|
+
fresh.set(cacheKey, answer.value);
|
|
91
|
+
// Keep successful sibling judgments even if the cursor stops at a failed candidate.
|
|
92
|
+
curation.cacheQualityJudgment(cacheKey, answer.value);
|
|
93
|
+
judged++;
|
|
94
|
+
}
|
|
95
|
+
for (const item of batch) {
|
|
84
96
|
check();
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
scanned++;
|
|
92
|
-
if (!item.eligible || !parseSafeVirtualPath(`qmd://${source.collection}/${row.path}`, sources) ||
|
|
93
|
-
!current.get(row.document_id, row.collection, row.path, row.hash, row.seq, row.pos, row.chunk_len)) {
|
|
94
|
-
skippedStale++;
|
|
95
|
-
advance();
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
if (text.length > MAX_CHUNK_CHARS) {
|
|
99
|
-
skippedOversized++;
|
|
100
|
-
advance();
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
const judgment = structure === "empty"
|
|
104
|
-
? { noise: 1, evidence: 0 } : item.judgment ?? fresh.get(cacheKey);
|
|
105
|
-
if (!judgment)
|
|
106
|
-
throw new Error("missing quality judgment");
|
|
107
|
-
if (item.judgment)
|
|
108
|
-
cached++;
|
|
109
|
-
else if (structure !== "empty")
|
|
110
|
-
curation.cacheQualityJudgment(cacheKey, judgment);
|
|
111
|
-
if (structure !== "empty" && structure !== "encoded_message" && judgment.noise < params.minNoise) {
|
|
112
|
-
advance();
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
const reason = structure === "empty" ? "empty_content" :
|
|
116
|
-
structure === "encoded_message" ? "possible_double_encoded_message" :
|
|
117
|
-
structure === "serialized_message" ? "possible_serialized_message" : "possible_ingestion_noise";
|
|
118
|
-
const startLine = row.doc.slice(0, row.pos).split("\n").length;
|
|
119
|
-
const endLine = startLine + text.split("\n").length - 1;
|
|
120
|
-
const task = curation.addTask({
|
|
121
|
-
type: "quality_review", corpus: source.corpus, collection: source.collection,
|
|
122
|
-
path: row.path, reason, contentFingerprint: fingerprint,
|
|
123
|
-
detail: JSON.stringify({
|
|
124
|
-
path: `qmd://${source.collection}/${row.path}`, from: startLine, to: endLine,
|
|
125
|
-
excerpt: text.slice(0, 400), excerptTruncated: text.length > 400,
|
|
126
|
-
indicator: structure === "empty" ? "deterministic_empty" :
|
|
127
|
-
structure === "encoded_message" ? "deterministic_encoding" : "typesafe",
|
|
128
|
-
...judgment, policy: QUALITY_JUDGE_VERSION,
|
|
129
|
-
triage: qualityTriage(judgment.noise, judgment.evidence, structure === "encoded_message"),
|
|
130
|
-
instruction: "Inspect original source and ingestion before acting. Verify source/index after any authorized repair. Never manually edit generated session projections.",
|
|
131
|
-
}),
|
|
132
|
-
});
|
|
133
|
-
flagged++;
|
|
97
|
+
const { row, source, text, fingerprint, cacheKey, structure } = item;
|
|
98
|
+
const advance = () => { next = { documentId: row.document_id, seq: row.seq }; };
|
|
99
|
+
scanned++;
|
|
100
|
+
if (!item.eligible || !parseSafeVirtualPath(`qmd://${source.collection}/${row.path}`, sources) ||
|
|
101
|
+
!current.get(row.document_id, row.collection, row.path, row.hash, row.seq, row.pos, row.chunk_len)) {
|
|
102
|
+
skippedStale++;
|
|
134
103
|
advance();
|
|
135
|
-
|
|
136
|
-
continue;
|
|
137
|
-
const triage = qualityTriage(judgment.noise, judgment.evidence, structure === "encoded_message");
|
|
138
|
-
const key = JSON.stringify([source.collection, reason, triage]);
|
|
139
|
-
const group = groups.get(key) ?? {
|
|
140
|
-
corpus: source.corpus, source: source.configuredPath, reason, triage, pending: 0, examples: [],
|
|
141
|
-
};
|
|
142
|
-
group.pending++;
|
|
143
|
-
if (group.examples.length < 3)
|
|
144
|
-
group.examples.push(task);
|
|
145
|
-
groups.set(key, group);
|
|
104
|
+
continue;
|
|
146
105
|
}
|
|
106
|
+
if (text.length > MAX_CHUNK_CHARS) {
|
|
107
|
+
skippedOversized++;
|
|
108
|
+
advance();
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const judgment = structure === "empty"
|
|
112
|
+
? { noise: 1, evidence: 0 } : item.judgment ?? fresh.get(cacheKey);
|
|
113
|
+
if (!judgment)
|
|
114
|
+
throw new Error("missing quality judgment");
|
|
115
|
+
if (item.judgment)
|
|
116
|
+
cached++;
|
|
117
|
+
if (structure !== "empty" && structure !== "encoded_message" && judgment.noise < params.minNoise) {
|
|
118
|
+
advance();
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const reason = structure === "empty" ? "empty_content" :
|
|
122
|
+
structure === "encoded_message" ? "possible_double_encoded_message" :
|
|
123
|
+
structure === "serialized_message" ? "possible_serialized_message" : "possible_ingestion_noise";
|
|
124
|
+
const startLine = row.doc.slice(0, row.pos).split("\n").length;
|
|
125
|
+
const endLine = startLine + text.split("\n").length - 1;
|
|
126
|
+
const task = curation.addTask({
|
|
127
|
+
type: "quality_review", corpus: source.corpus, collection: source.collection,
|
|
128
|
+
path: row.path, reason, contentFingerprint: fingerprint,
|
|
129
|
+
detail: JSON.stringify({
|
|
130
|
+
path: `qmd://${source.collection}/${row.path}`, from: startLine, to: endLine,
|
|
131
|
+
excerpt: text.slice(0, 400), excerptTruncated: text.length > 400,
|
|
132
|
+
indicator: structure === "empty" ? "deterministic_empty" :
|
|
133
|
+
structure === "encoded_message" ? "deterministic_encoding" : "typesafe",
|
|
134
|
+
...judgment, policy: QUALITY_JUDGE_VERSION,
|
|
135
|
+
triage: qualityTriage(judgment.noise, judgment.evidence, structure === "encoded_message"),
|
|
136
|
+
instruction: "Inspect original source and ingestion before acting. Verify source/index after any authorized repair. Never manually edit generated session projections.",
|
|
137
|
+
}),
|
|
138
|
+
});
|
|
139
|
+
flagged++;
|
|
140
|
+
advance();
|
|
141
|
+
if (task.status !== "pending")
|
|
142
|
+
continue;
|
|
143
|
+
const triage = qualityTriage(judgment.noise, judgment.evidence, structure === "encoded_message");
|
|
144
|
+
const key = JSON.stringify([source.collection, reason, triage]);
|
|
145
|
+
const group = groups.get(key) ?? {
|
|
146
|
+
corpus: source.corpus, source: source.configuredPath, reason, triage, pending: 0, examples: [],
|
|
147
|
+
};
|
|
148
|
+
group.pending++;
|
|
149
|
+
if (group.examples.length < 3)
|
|
150
|
+
group.examples.push(task);
|
|
151
|
+
groups.set(key, group);
|
|
147
152
|
}
|
|
148
153
|
return result("ok", rows.length <= limit);
|
|
149
154
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { abortable } from "./abortable.js";
|
|
3
|
-
import { resolveTypeSafeApiKey } from "./typesafe.js";
|
|
3
|
+
import { resolveTypeSafeApiKey } from "./typesafe-client.js";
|
|
4
4
|
import { RESPONSE_EXTRACTOR_VERSION, ResponseTranscriptReader } from "./response-episodes.js";
|
|
5
5
|
import { RESPONSE_RUBRIC_VERSION, RESPONSE_STAGE_VERSIONS, judgeMemoryOpportunity } from "./response-judge.js";
|
|
6
6
|
import { ResponseAuditStore } from "./response-store.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
import { ACTIVE_EVENT_COUNT_SQL, ACTIVE_EVENT_ROWS_SQL, assertAgentTranscriptSchema } from "./agent-transcript.js";
|
|
3
4
|
import { messageText } from "./whisperer-context.js";
|
|
4
5
|
import { responseUserText } from "./response-text.js";
|
|
5
6
|
export const RESPONSE_EXTRACTOR_VERSION = 5;
|
|
@@ -182,11 +183,7 @@ export class ResponseTranscriptReader {
|
|
|
182
183
|
this.#db = new DatabaseSync(path, { readOnly: true });
|
|
183
184
|
try {
|
|
184
185
|
this.#db.exec("PRAGMA query_only=ON; PRAGMA busy_timeout=1000");
|
|
185
|
-
|
|
186
|
-
const meta = this.#db.prepare("SELECT role,agent_id,schema_version FROM schema_meta WHERE meta_key='primary'").get();
|
|
187
|
-
if (![17, 18, 19].includes(Number(version)) || meta?.role !== "agent" || meta.agent_id !== agentId || meta.schema_version !== version) {
|
|
188
|
-
throw new Error("Unsupported response-audit transcript schema or agent");
|
|
189
|
-
}
|
|
186
|
+
assertAgentTranscriptSchema(this.#db, agentId, "Unsupported response-audit transcript schema or agent");
|
|
190
187
|
}
|
|
191
188
|
catch (error) {
|
|
192
189
|
this.#db.close();
|
|
@@ -219,14 +216,10 @@ export class ResponseTranscriptReader {
|
|
|
219
216
|
return null;
|
|
220
217
|
const session = { sessionId, accountId: String(window.accountId),
|
|
221
218
|
conversationId: String(window.conversationId), chatType: String(window.chatType) };
|
|
222
|
-
const count = this.#db.prepare(
|
|
223
|
-
FROM session_transcript_active_events a JOIN transcript_events e ON e.session_id=a.session_id AND e.seq=a.event_seq
|
|
224
|
-
WHERE a.session_id=?`).get(session.sessionId);
|
|
219
|
+
const count = this.#db.prepare(ACTIVE_EVENT_COUNT_SQL).get(session.sessionId);
|
|
225
220
|
if (Number(count.n) > MAX_EVENTS || Number(count.bytes) > MAX_SESSION_BYTES)
|
|
226
221
|
return undefined;
|
|
227
|
-
const rows = this.#db.prepare(
|
|
228
|
-
FROM session_transcript_active_events a JOIN transcript_events e ON e.session_id=a.session_id AND e.seq=a.event_seq
|
|
229
|
-
WHERE a.session_id=? ORDER BY a.active_position`).all(session.sessionId);
|
|
222
|
+
const rows = this.#db.prepare(ACTIVE_EVENT_ROWS_SQL).all(session.sessionId);
|
|
230
223
|
// Exact active content catches in-place edits and branch changes even when writer
|
|
231
224
|
// watermarks are absent. Raw text is never retained in the checkpoint database.
|
|
232
225
|
const revision = hash([RESPONSE_EXTRACTOR_VERSION, session, config.historyMessages, [...config.senderIds].sort(), rows]);
|
|
@@ -6,7 +6,7 @@ export declare const RESPONSE_STAGE_VERSIONS: {
|
|
|
6
6
|
readonly feedback: "feedback-v9";
|
|
7
7
|
readonly sentiment: "sentiment-v10";
|
|
8
8
|
readonly retrospective: "retrospective-v9";
|
|
9
|
-
readonly memory: "memory-
|
|
9
|
+
readonly memory: "memory-v2-isolated";
|
|
10
10
|
};
|
|
11
11
|
declare const qualitySchema: Type.TObject<{
|
|
12
12
|
answers: Type.TObject<{
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
|
-
import {
|
|
3
|
+
import { requestTypeSafe } from "./typesafe-client.js";
|
|
4
|
+
import { TYPESAFE_REVIEW_MODEL } from "./typesafe-review.js";
|
|
4
5
|
export const RESPONSE_RUBRIC_VERSION = `${TYPESAFE_REVIEW_MODEL}:response-v10`;
|
|
5
|
-
export const RESPONSE_STAGE_VERSIONS = { quality: "quality-v9", feedback: "feedback-v9", sentiment: "sentiment-v10", retrospective: "retrospective-v9", memory: "memory-
|
|
6
|
+
export const RESPONSE_STAGE_VERSIONS = { quality: "quality-v9", feedback: "feedback-v9", sentiment: "sentiment-v10", retrospective: "retrospective-v9", memory: "memory-v2-isolated" };
|
|
6
7
|
const probability = Type.Number({ minimum: 0, maximum: 1 });
|
|
7
8
|
const noul = Type.Object({ type: Type.Literal("noul"), noul: probability });
|
|
8
9
|
const score = Type.Object({ type: Type.Literal("score"), score: Type.Number({ minimum: 0, maximum: 3 }),
|
|
@@ -73,7 +74,7 @@ export async function judgeResponse(episode, params, sentimentEnabled = true, ca
|
|
|
73
74
|
contextLimited: episode.contextLimited, evidenceLimit: "Visible conversation only. Artifacts, tool results and external facts are not provided." };
|
|
74
75
|
if (!cache?.quality)
|
|
75
76
|
cache?.begin(["quality"]);
|
|
76
|
-
const qualityPayload = cache?.quality ? { answers: cache.quality } : await
|
|
77
|
+
const qualityPayload = cache?.quality ? { answers: cache.quality } : await requestTypeSafe(params, state, {
|
|
77
78
|
underdelivery: { type: "noul", instructions: { question: "Does the visible `answer` CLEARLY underdeliver on what the human explicitly asked for in `request`, given `before`?",
|
|
78
79
|
required: "Identify a concrete unmet requirement, wrong product, explicit constraint violation, materially shallow answer or unjustified deferral. Judge delivery, not politeness or writing style.",
|
|
79
80
|
exclusions: "Missing verification of unseen work is uncertainty, not failure. Necessary clarification, legitimate safety/approval boundaries, honest blockers and newly added requirements are not underdelivery. Do not infer a failure from memory-search counts or unseen tool activity.", trust } },
|
|
@@ -142,7 +143,7 @@ export async function judgeResponse(episode, params, sentimentEnabled = true, ca
|
|
|
142
143
|
};
|
|
143
144
|
const questions = Object.fromEntries(Object.entries(feedbackQuestions).filter(([key]) => Object.hasOwn(sentimentSchema.properties, key) ? needSentiment : needFeedback));
|
|
144
145
|
cache?.begin([...(needFeedback ? ["feedback"] : []), ...(needSentiment ? ["sentiment"] : [])]);
|
|
145
|
-
const feedbackPayload = await
|
|
146
|
+
const feedbackPayload = await requestTypeSafe(params, { ...state, feedback: episode.feedback }, questions);
|
|
146
147
|
const base = needFeedback ? feedbackPayload : { answers: cache.feedback };
|
|
147
148
|
if (!Value.Check(feedbackSchema, base))
|
|
148
149
|
throw new Error("Invalid response feedback judgment");
|
|
@@ -188,7 +189,7 @@ const retrospectiveSchema = Type.Object({ answers: Type.Object({ correction: nou
|
|
|
188
189
|
/** Later evidence is kept in a third request and never changes the original grade. */
|
|
189
190
|
export async function judgeResponseFollowup(episode, params) {
|
|
190
191
|
const later = ["complete", "partial"].includes(episode.followup.status) ? episode.followup.messages : [];
|
|
191
|
-
const payload = await
|
|
192
|
+
const payload = await requestTypeSafe(params, { before: episode.before, originalRequest: episode.request, originalAnswer: episode.answer,
|
|
192
193
|
humanReply: episode.feedback, nextAssistantResponse: later, evidenceStatus: episode.followup.status }, {
|
|
193
194
|
outcome: { type: "choice", instructions: { question: "What does the available conversation establish about delivery of the ORIGINAL request by originalAnswer?",
|
|
194
195
|
method: "Match each complaint or admission to a concrete requirement or claim in originalRequest/originalAnswer, using before only to resolve existing requirements. Grade the original answer, not the later repair. A concrete shortfall takes precedence over praise.",
|
|
@@ -233,16 +234,18 @@ export async function judgeResponseFollowup(episode, params) {
|
|
|
233
234
|
return { status: episode.followup.status, judgment: payload.answers };
|
|
234
235
|
}
|
|
235
236
|
export async function judgeMemoryOpportunity(episode, candidates, params) {
|
|
236
|
-
const questions =
|
|
237
|
-
instructions: { question:
|
|
238
|
-
limits: "Judge substantive relevance, not shared vocabulary.
|
|
237
|
+
const questions = { candidate_0: { type: "noul",
|
|
238
|
+
instructions: { question: "Would the information in `candidates[0]` materially address the specific context gap expressed in `feedback` about `answer`?",
|
|
239
|
+
limits: "Judge substantive relevance, not shared vocabulary. This is a CURRENT indexed excerpt; its presence does not prove historical availability, truth, or agent fault.", trust } } };
|
|
239
240
|
if (!candidates.length)
|
|
240
241
|
return [];
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
242
|
+
const schema = Type.Object({ answers: Type.Object({ candidate_0: noul }, { additionalProperties: false }) });
|
|
243
|
+
return Promise.all(candidates.map(async (candidate) => {
|
|
244
|
+
const payload = await requestTypeSafe(params, { request: episode.request, answer: episode.answer,
|
|
245
|
+
feedback: episode.feedback, candidates: [{ text: candidate.text }] }, questions);
|
|
246
|
+
if (!Value.Check(schema, payload))
|
|
247
|
+
throw new Error("Invalid memory opportunity judgment");
|
|
248
|
+
return { path: candidate.path, hash: candidate.hash, usefulness: payload.answers.candidate_0.noul,
|
|
249
|
+
basis: "current_index_only; historical availability and retrieval exposure unknown" };
|
|
250
|
+
}));
|
|
248
251
|
}
|
|
@@ -6,14 +6,7 @@ import { normalizeAgentIdStrict } from "openclaw/plugin-sdk/routing";
|
|
|
6
6
|
import { resolveSources } from "./sources.js";
|
|
7
7
|
import { auditResponses, responseCohort } from "./response-audit.js";
|
|
8
8
|
import { ResponseAuditStore } from "./response-store.js";
|
|
9
|
-
|
|
10
|
-
if (value === undefined)
|
|
11
|
-
return fallback;
|
|
12
|
-
if (!/^\d{4}-\d{2}-\d{2}$/.test(value) || !Number.isFinite(Date.parse(value)) || new Date(value).toISOString().slice(0, 10) !== value) {
|
|
13
|
-
throw new Error("Dates must be valid YYYY-MM-DD UTC dates");
|
|
14
|
-
}
|
|
15
|
-
return Date.parse(value);
|
|
16
|
-
}
|
|
9
|
+
import { dateOption } from "./date-option.js";
|
|
17
10
|
export function registerResponseAudit(api, config) {
|
|
18
11
|
const options = (cfg, id) => {
|
|
19
12
|
const normalized = normalizeAgentIdStrict(id);
|
|
@@ -50,8 +50,8 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
|
|
|
50
50
|
quality: {
|
|
51
51
|
taskType: {
|
|
52
52
|
type: "choice";
|
|
53
|
-
choice: "action" | "question" | "artifact" | "discussion" | "other";
|
|
54
53
|
confidence: number;
|
|
54
|
+
choice: "action" | "question" | "artifact" | "discussion" | "other";
|
|
55
55
|
probabilities: {
|
|
56
56
|
[x: string]: number;
|
|
57
57
|
[x: number]: number;
|
|
@@ -59,8 +59,8 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
|
|
|
59
59
|
};
|
|
60
60
|
assessability: {
|
|
61
61
|
type: "choice";
|
|
62
|
-
choice: "assessable" | "not_assessable";
|
|
63
62
|
confidence: number;
|
|
63
|
+
choice: "assessable" | "not_assessable";
|
|
64
64
|
probabilities: {
|
|
65
65
|
[x: string]: number;
|
|
66
66
|
[x: number]: number;
|
|
@@ -68,8 +68,8 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
|
|
|
68
68
|
};
|
|
69
69
|
fitAssessability: {
|
|
70
70
|
type: "choice";
|
|
71
|
-
choice: "assessable" | "not_assessable";
|
|
72
71
|
confidence: number;
|
|
72
|
+
choice: "assessable" | "not_assessable";
|
|
73
73
|
probabilities: {
|
|
74
74
|
[x: string]: number;
|
|
75
75
|
[x: number]: number;
|
|
@@ -81,8 +81,8 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
|
|
|
81
81
|
};
|
|
82
82
|
failureReason: {
|
|
83
83
|
type: "choice";
|
|
84
|
-
choice: "none_or_unclear" | "missing_requested_work" | "wrong_deliverable" | "missed_constraint" | "insufficient_answer_depth" | "unnecessary_deferral";
|
|
85
84
|
confidence: number;
|
|
85
|
+
choice: "none_or_unclear" | "missing_requested_work" | "wrong_deliverable" | "missed_constraint" | "insufficient_answer_depth" | "unnecessary_deferral";
|
|
86
86
|
probabilities: {
|
|
87
87
|
[x: string]: number;
|
|
88
88
|
[x: number]: number;
|
|
@@ -112,8 +112,8 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
|
|
|
112
112
|
};
|
|
113
113
|
consistency: {
|
|
114
114
|
type: "choice";
|
|
115
|
-
choice: "not_assessable" | "consistent" | "contradicted";
|
|
116
115
|
confidence: number;
|
|
116
|
+
choice: "not_assessable" | "consistent" | "contradicted";
|
|
117
117
|
probabilities: {
|
|
118
118
|
[x: string]: number;
|
|
119
119
|
[x: number]: number;
|
|
@@ -123,8 +123,8 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
|
|
|
123
123
|
feedback: {
|
|
124
124
|
feedbackType: {
|
|
125
125
|
type: "choice";
|
|
126
|
-
choice: "mixed" | "unrelated" | "unclear" | "acceptance" | "correction" | "continuation";
|
|
127
126
|
confidence: number;
|
|
127
|
+
choice: "mixed" | "unrelated" | "unclear" | "acceptance" | "correction" | "continuation";
|
|
128
128
|
probabilities: {
|
|
129
129
|
[x: string]: number;
|
|
130
130
|
[x: number]: number;
|
|
@@ -132,8 +132,8 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
|
|
|
132
132
|
};
|
|
133
133
|
target: {
|
|
134
134
|
type: "choice";
|
|
135
|
-
choice: "delivery" | "mixed" | "unclear" | "current_answer" | "earlier_behavior" | "proactive_action" | "external" | "new_work";
|
|
136
135
|
confidence: number;
|
|
136
|
+
choice: "delivery" | "mixed" | "unclear" | "current_answer" | "earlier_behavior" | "proactive_action" | "external" | "new_work";
|
|
137
137
|
probabilities: {
|
|
138
138
|
[x: string]: number;
|
|
139
139
|
[x: number]: number;
|
|
@@ -154,8 +154,8 @@ export declare function assessResponseStages(store: ResponseAuditStore, cohort:
|
|
|
154
154
|
} & Partial<{
|
|
155
155
|
sentiment: {
|
|
156
156
|
type: "choice";
|
|
157
|
-
choice: "satisfied" | "dissatisfied" | "mixed" | "neutral" | "unrelated" | "unclear";
|
|
158
157
|
confidence: number;
|
|
158
|
+
choice: "satisfied" | "dissatisfied" | "mixed" | "neutral" | "unrelated" | "unclear";
|
|
159
159
|
probabilities: {
|
|
160
160
|
[x: string]: number;
|
|
161
161
|
[x: number]: number;
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/** Infer legacy transport sender metadata only to unwrap visible conversation text. */
|
|
2
|
+
export declare function conversationUserText(raw: string, sender?: unknown): {
|
|
3
|
+
text: string;
|
|
4
|
+
contextLimited: boolean;
|
|
5
|
+
} | undefined;
|
|
1
6
|
/** Parse only recognized transport envelopes. Ambiguous wrappers are excluded,
|
|
2
7
|
* never flattened into a human's request or used to approve embedded speakers. */
|
|
3
8
|
export declare function responseUserText(input: string, senderId: string): {
|
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
/** Infer legacy transport sender metadata only to unwrap visible conversation text. */
|
|
2
|
+
export function conversationUserText(raw, sender) {
|
|
3
|
+
let senderId = typeof sender === "string" ? sender : undefined;
|
|
4
|
+
if (senderId === undefined) {
|
|
5
|
+
const header = /^Conversation info: ⟦openclaw:ctx⟧\r?\n```json\r?\n([^]*?)\r?\n```\r?\n/.exec(raw.trim());
|
|
6
|
+
if (header) {
|
|
7
|
+
let metadata;
|
|
8
|
+
try {
|
|
9
|
+
metadata = JSON.parse(header[1]);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (metadata && typeof metadata === "object" && "sender" in metadata && metadata.sender &&
|
|
15
|
+
typeof metadata.sender === "object" && "id" in metadata.sender && typeof metadata.sender.id === "string") {
|
|
16
|
+
senderId = metadata.sender.id;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
senderId ??= /^From: [^\r\n]+ \(([^()\r\n]+)\)\r?\n/.exec(raw.trim())?.[1];
|
|
20
|
+
}
|
|
21
|
+
return responseUserText(raw, senderId ?? "");
|
|
22
|
+
}
|
|
1
23
|
/** Parse only recognized transport envelopes. Ambiguous wrappers are excluded,
|
|
2
24
|
* never flattened into a human's request or used to approve embedded speakers. */
|
|
3
25
|
export function responseUserText(input, senderId) {
|
|
@@ -15,7 +37,7 @@ export function responseUserText(input, senderId) {
|
|
|
15
37
|
if (!meta || meta.sender?.id !== senderId || typeof meta.sender.name !== "string")
|
|
16
38
|
return undefined;
|
|
17
39
|
const rest = text.slice(header[0].length);
|
|
18
|
-
const markers = [...rest.matchAll(/^System: \[[^\]\r\n]+\] Slack message in [^\r\n]
|
|
40
|
+
const markers = [...rest.matchAll(/^System: \[[^\]\r\n]+\] Slack (?:message in [^\r\n]+|DM) from ([^\r\n]+)\r?\n\r?\n/gm)];
|
|
19
41
|
if (markers.length !== 1 || markers[0][1] !== meta.sender.name)
|
|
20
42
|
return undefined;
|
|
21
43
|
const marker = markers[0];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
type RetrievalOperation = "vector" | "lexical" | "memoryWhisperer";
|
|
2
2
|
type RetrievalOutcome = "ok" | "empty" | "failed" | "cancelled" | "timed_out" | "skipped";
|
|
3
|
-
declare const fields: readonly ["elapsedMs", "retrievalMs", "judgeMs", "candidates", "eligible", "results", "contextChars"];
|
|
3
|
+
declare const fields: readonly ["elapsedMs", "retrievalMs", "judgeMs", "gateMs", "generationMs", "candidates", "eligible", "results", "contextChars"];
|
|
4
4
|
type Field = typeof fields[number];
|
|
5
5
|
export type RetrievalObservation = {
|
|
6
6
|
outcome: RetrievalOutcome;
|
|
@@ -16,11 +16,11 @@ export declare class RetrievalTelemetry {
|
|
|
16
16
|
[k: string]: {
|
|
17
17
|
calls: number;
|
|
18
18
|
outcomes: {
|
|
19
|
+
cancelled?: number | undefined;
|
|
19
20
|
ok?: number | undefined;
|
|
20
21
|
skipped?: number | undefined;
|
|
21
22
|
failed?: number | undefined;
|
|
22
23
|
empty?: number | undefined;
|
|
23
|
-
cancelled?: number | undefined;
|
|
24
24
|
timed_out?: number | undefined;
|
|
25
25
|
};
|
|
26
26
|
measurements: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const fields = ["elapsedMs", "retrievalMs", "judgeMs", "candidates", "eligible", "results", "contextChars"];
|
|
1
|
+
const fields = ["elapsedMs", "retrievalMs", "judgeMs", "gateMs", "generationMs", "candidates", "eligible", "results", "contextChars"];
|
|
2
2
|
const recentLimit = 256;
|
|
3
3
|
const boundedAdd = (left, right) => Math.min(Number.MAX_SAFE_INTEGER, left + right);
|
|
4
4
|
/** Only fixed operation/outcome names and nonnegative numbers enter this store. */
|
package/dist/src/review-tools.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Type } from "typebox";
|
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
3
|
import { jsonResult } from "openclaw/plugin-sdk/agent-runtime";
|
|
4
4
|
import { getContext } from "./tool-context.js";
|
|
5
|
-
import { resolveTypeSafeApiKey } from "./typesafe.js";
|
|
5
|
+
import { resolveTypeSafeApiKey } from "./typesafe-client.js";
|
|
6
6
|
import { abortable } from "./abortable.js";
|
|
7
7
|
const claimParameters = Type.Object({
|
|
8
8
|
claim: Type.String({ pattern: "\\S", maxLength: 2000 }),
|