@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.
Files changed (63) hide show
  1. package/dist/src/agent-transcript.d.ts +8 -0
  2. package/dist/src/agent-transcript.js +29 -0
  3. package/dist/src/cluster-review.js +1 -1
  4. package/dist/src/config.d.ts +4 -0
  5. package/dist/src/config.js +14 -1
  6. package/dist/src/date-option.d.ts +1 -0
  7. package/dist/src/date-option.js +8 -0
  8. package/dist/src/diagnostics.d.ts +13 -4
  9. package/dist/src/diagnostics.js +5 -0
  10. package/dist/src/manager.d.ts +3 -1
  11. package/dist/src/manager.js +61 -57
  12. package/dist/src/memory-whisperer.d.ts +2 -1
  13. package/dist/src/memory-whisperer.js +241 -114
  14. package/dist/src/mlx-query.d.ts +22 -0
  15. package/dist/src/mlx-query.js +155 -0
  16. package/dist/src/people-dossier-review.js +1 -1
  17. package/dist/src/people-hooks.d.ts +1 -1
  18. package/dist/src/people-hooks.js +13 -7
  19. package/dist/src/people-primer-tool.js +1 -1
  20. package/dist/src/people-primer.js +3 -2
  21. package/dist/src/plugin.js +10 -5
  22. package/dist/src/quality-audit.js +86 -81
  23. package/dist/src/response-audit.js +1 -1
  24. package/dist/src/response-episodes.js +4 -11
  25. package/dist/src/response-judge.d.ts +1 -1
  26. package/dist/src/response-judge.js +18 -15
  27. package/dist/src/response-runtime.js +1 -8
  28. package/dist/src/response-stages.d.ts +8 -8
  29. package/dist/src/response-text.d.ts +5 -0
  30. package/dist/src/response-text.js +23 -1
  31. package/dist/src/retrieval-telemetry.d.ts +2 -2
  32. package/dist/src/retrieval-telemetry.js +1 -1
  33. package/dist/src/review-tools.js +1 -1
  34. package/dist/src/session-sync.js +8 -22
  35. package/dist/src/skill-whisperer.d.ts +1 -2
  36. package/dist/src/skill-whisperer.js +77 -72
  37. package/dist/src/training-candidates.d.ts +9 -1
  38. package/dist/src/training-candidates.js +12 -7
  39. package/dist/src/training-gate.js +3 -3
  40. package/dist/src/training-input.js +6 -34
  41. package/dist/src/training-judge.js +3 -3
  42. package/dist/src/training-queries.js +5 -4
  43. package/dist/src/training-runtime.js +1 -8
  44. package/dist/src/training.js +5 -4
  45. package/dist/src/typesafe-client.d.ts +15 -0
  46. package/dist/src/typesafe-client.js +75 -0
  47. package/dist/src/typesafe-review.d.ts +2 -6
  48. package/dist/src/typesafe-review.js +49 -55
  49. package/dist/src/typesafe.d.ts +3 -7
  50. package/dist/src/typesafe.js +108 -163
  51. package/dist/src/whisperer-context.d.ts +1 -0
  52. package/dist/src/whisperer-context.js +9 -0
  53. package/dist/src/whisperer-prompt.d.ts +3 -0
  54. package/dist/src/whisperer-prompt.js +21 -0
  55. package/docs/configuration.md +12 -2
  56. package/docs/peoplesql.md +3 -1
  57. package/docs/response-audit.md +4 -1
  58. package/docs/retrieval.md +128 -20
  59. package/openclaw.plugin.json +13 -4
  60. package/package.json +2 -1
  61. package/src/mlx-query-worker.py +104 -0
  62. package/dist/src/typesafe-transport.d.ts +0 -10
  63. package/dist/src/typesafe-transport.js +0 -26
@@ -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, "&amp;").replace(/</gu, "&lt;").replace(/>/gu, "&gt;");
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
- api.on("before_prompt_build", (_event, context) => {
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 ? { prependContext: previous.contribution } : undefined;
120
+ return previous.runId === runId ? peopleContext(previous.contribution) : undefined;
116
121
  }
117
122
  const blurb = store.getDossierBlurb(person.id);
118
- const prependContext = blurb
123
+ const contribution = blurb
119
124
  ? renderPeopleWhisper(blurb, config.whisperer.maxChars)
120
125
  : undefined;
121
- if (!prependContext)
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: prependContext,
132
+ contribution,
128
133
  });
129
- return receipt.runId === runId ? { prependContext: receipt.contribution } : undefined;
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 { askTypeSafeReview, TYPESAFE_REVIEW_MODEL } from "./typesafe-review.js";
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 askTypeSafeReview({ apiKey: params.apiKey, timeoutMs: config.timeoutMs, signal }, state, questions);
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");
@@ -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 = new WhispererDiagnostics();
452
- registerSkillWhisperer(api, runtime, config.skillWhisperer, config.typesafe, diagnostics);
453
- registerMemoryWhisperer(api, runtime, config.memoryWhisperer, config.typesafe, diagnostics);
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
- for (let offset = 0; offset < page.length; offset += BATCH_SIZE) {
65
- check();
66
- const batch = page.slice(offset, offset + BATCH_SIZE).map(row => {
67
- const source = sources.get(row.collection);
68
- const text = row.doc.slice(row.pos, row.pos + row.chunk_len);
69
- const fingerprint = chunkFingerprint(text);
70
- const cacheKey = chunkFingerprint(JSON.stringify([QUALITY_JUDGE_VERSION, source.kind, fingerprint]));
71
- const eligible = Boolean(parseSafeVirtualPath(`qmd://${source.collection}/${row.path}`, sources)) && row.pos >= 0 && row.chunk_len > 0 &&
72
- row.pos + row.chunk_len <= row.doc.length;
73
- const structure = qualityStructure(text);
74
- const judgment = eligible && text.length <= MAX_CHUNK_CHARS
75
- ? curation.qualityJudgment(cacheKey) : undefined;
76
- return { row, source, text, fingerprint, cacheKey, structure, judgment, eligible };
77
- });
78
- const missing = [...new Map(batch.filter(item => item.eligible && item.text.length <= MAX_CHUNK_CHARS &&
79
- item.structure !== "empty" && !item.judgment).map(item => [item.cacheKey, item])).values()];
80
- const answers = await judgeTypeSafeQuality({
81
- apiKey: params.apiKey, timeoutMs: params.timeoutMs, signal,
82
- chunks: missing.map(item => ({ text: item.text, sourceKind: item.source.kind === "sessions" ? "sessions" : "files" })),
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 fresh = new Map(missing.map((item, index) => [item.cacheKey, answers[index]]));
86
- judged += answers.length;
87
- for (const item of batch) {
88
- check();
89
- const { row, source, text, fingerprint, cacheKey, structure } = item;
90
- const advance = () => { next = { documentId: row.document_id, seq: row.seq }; };
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
- if (task.status !== "pending")
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
- const version = this.#db.prepare("PRAGMA user_version").get()?.user_version;
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(`SELECT COUNT(*) n,COALESCE(SUM(length(e.event_json)),0) bytes
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(`SELECT e.seq,e.event_json eventJson,e.created_at createdAt
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-v1";
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 { askTypeSafeReview, TYPESAFE_REVIEW_MODEL } from "./typesafe-review.js";
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-v1" };
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 askTypeSafeReview(params, state, {
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 askTypeSafeReview(params, { ...state, feedback: episode.feedback }, questions);
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 askTypeSafeReview(params, { before: episode.before, originalRequest: episode.request, originalAnswer: episode.answer,
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 = Object.fromEntries(candidates.map((_c, i) => [`candidate_${i}`, { type: "noul",
237
- instructions: { question: `Would the information in \`candidates[${i}]\` materially address the specific context gap expressed in \`feedback\` about \`answer\`?`,
238
- limits: "Judge substantive relevance, not shared vocabulary. These are CURRENT indexed excerpts; their presence does not prove historical availability, truth, or agent fault.", trust } }]));
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 payload = await askTypeSafeReview(params, { request: episode.request, answer: episode.answer,
242
- feedback: episode.feedback, candidates: candidates.map(c => ({ text: c.text })) }, questions);
243
- const schema = Type.Object({ answers: Type.Object(Object.fromEntries(candidates.map((_c, i) => [`candidate_${i}`, noul]))) });
244
- if (!Value.Check(schema, payload))
245
- throw new Error("Invalid memory opportunity judgment");
246
- return candidates.map((c, i) => ({ path: c.path, hash: c.hash, usefulness: payload.answers[`candidate_${i}`].noul,
247
- basis: "current_index_only; historical availability and retrieval exposure unknown" }));
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
- function dateOption(value, fallback) {
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]+ from ([^\r\n]+)\r?\n\r?\n/gm)];
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. */
@@ -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 }),