@unblocklabs/unblock-memory 0.3.15 → 0.3.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +198 -0
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.js +4 -1
- package/dist/src/plugin.js +2 -0
- package/dist/src/response-audit.d.ts +87 -0
- package/dist/src/response-audit.js +193 -0
- package/dist/src/response-config.d.ts +13 -0
- package/dist/src/response-config.js +43 -0
- package/dist/src/response-episodes.d.ts +68 -0
- package/dist/src/response-episodes.js +242 -0
- package/dist/src/response-identity.d.ts +15 -0
- package/dist/src/response-identity.js +34 -0
- package/dist/src/response-judge.d.ts +224 -0
- package/dist/src/response-judge.js +248 -0
- package/dist/src/response-memory.d.ts +8 -0
- package/dist/src/response-memory.js +25 -0
- package/dist/src/response-outcome.d.ts +30 -0
- package/dist/src/response-outcome.js +51 -0
- package/dist/src/response-reviews.d.ts +27 -0
- package/dist/src/response-reviews.js +116 -0
- package/dist/src/response-runtime.d.ts +3 -0
- package/dist/src/response-runtime.js +150 -0
- package/dist/src/response-stages.d.ts +184 -0
- package/dist/src/response-stages.js +38 -0
- package/dist/src/response-store.d.ts +180 -0
- package/dist/src/response-store.js +411 -0
- package/dist/src/response-text.d.ts +6 -0
- package/dist/src/response-text.js +37 -0
- package/dist/src/typesafe-review.d.ts +5 -0
- package/dist/src/typesafe-review.js +6 -5
- package/openclaw.plugin.json +20 -1
- package/package.json +1 -1
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmodSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
import { responseOutcome, RESPONSE_REPORT_VERSION } from "./response-outcome.js";
|
|
6
|
+
import { ResponsePeople } from "./response-identity.js";
|
|
7
|
+
import { ResponseReviews, RESPONSE_REVIEW_POLICY } from "./response-reviews.js";
|
|
8
|
+
/** Separate operator-only database: not a memory corpus and never injected into agent prompts. */
|
|
9
|
+
export class ResponseAuditStore {
|
|
10
|
+
#db;
|
|
11
|
+
reviews;
|
|
12
|
+
constructor(path) {
|
|
13
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
14
|
+
this.#db = new DatabaseSync(path);
|
|
15
|
+
chmodSync(path, 0o600);
|
|
16
|
+
this.#db.exec(`PRAGMA busy_timeout=1000;
|
|
17
|
+
CREATE TABLE IF NOT EXISTS response_lease (id INTEGER PRIMARY KEY CHECK(id=1), token TEXT, expires INTEGER);
|
|
18
|
+
CREATE TABLE IF NOT EXISTS response_results (
|
|
19
|
+
cohort TEXT, id TEXT, session_id TEXT NOT NULL, input_hash TEXT NOT NULL,
|
|
20
|
+
episode_at INTEGER NOT NULL, active INTEGER NOT NULL, status TEXT NOT NULL,
|
|
21
|
+
attempts INTEGER NOT NULL DEFAULT 0, attempted_at INTEGER, assessed_at INTEGER, result TEXT,
|
|
22
|
+
PRIMARY KEY(cohort,id));
|
|
23
|
+
CREATE TABLE IF NOT EXISTS response_scans (cohort TEXT PRIMARY KEY, observed_at INTEGER, coverage TEXT);
|
|
24
|
+
CREATE INDEX IF NOT EXISTS response_results_time ON response_results(cohort,episode_at);`);
|
|
25
|
+
this.#db.exec(`CREATE TABLE IF NOT EXISTS response_checkpoints (
|
|
26
|
+
cohort TEXT NOT NULL, session_id TEXT NOT NULL, revision TEXT NOT NULL, coverage TEXT NOT NULL,
|
|
27
|
+
PRIMARY KEY(cohort,session_id));
|
|
28
|
+
CREATE TABLE IF NOT EXISTS response_cursors (cohort TEXT PRIMARY KEY,cursor TEXT NOT NULL);
|
|
29
|
+
CREATE TABLE IF NOT EXISTS response_schedule (
|
|
30
|
+
id INTEGER PRIMARY KEY CHECK(id=1), interval_ms INTEGER NOT NULL, next_due INTEGER NOT NULL);
|
|
31
|
+
CREATE TABLE IF NOT EXISTS response_stages (
|
|
32
|
+
key TEXT PRIMARY KEY, stage TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending',
|
|
33
|
+
attempts INTEGER NOT NULL DEFAULT 0, attempted_at INTEGER, assessed_at INTEGER, result TEXT);
|
|
34
|
+
CREATE TABLE IF NOT EXISTS response_stage_links (
|
|
35
|
+
cohort TEXT NOT NULL, episode_id TEXT NOT NULL, input_hash TEXT NOT NULL, stage TEXT NOT NULL, key TEXT NOT NULL,
|
|
36
|
+
PRIMARY KEY(cohort,episode_id,stage));`);
|
|
37
|
+
this.reviews = new ResponseReviews(this.#db);
|
|
38
|
+
}
|
|
39
|
+
/** Claim one bounded scheduled attempt, never replay every missed interval. */
|
|
40
|
+
claimScheduled(now, intervalMs) {
|
|
41
|
+
this.#db.exec("BEGIN IMMEDIATE");
|
|
42
|
+
try {
|
|
43
|
+
this.#db.prepare(`INSERT INTO response_schedule VALUES(1,?,?) ON CONFLICT(id) DO UPDATE SET
|
|
44
|
+
next_due=next_due + excluded.interval_ms - interval_ms, interval_ms=excluded.interval_ms`)
|
|
45
|
+
.run(intervalMs, now + intervalMs);
|
|
46
|
+
const claimed = this.#db.prepare("UPDATE response_schedule SET next_due=? WHERE id=1 AND next_due<=?")
|
|
47
|
+
.run(now + intervalMs, now).changes > 0;
|
|
48
|
+
this.#db.exec("COMMIT");
|
|
49
|
+
return claimed;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
this.#db.exec("ROLLBACK");
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
acquire(now) {
|
|
57
|
+
const token = randomUUID();
|
|
58
|
+
const result = this.#db.prepare(`INSERT INTO response_lease VALUES(1,?,?)
|
|
59
|
+
ON CONFLICT(id) DO UPDATE SET token=excluded.token,expires=excluded.expires WHERE response_lease.expires < ?`)
|
|
60
|
+
.run(token, now + 180_000, now);
|
|
61
|
+
return result.changes ? token : undefined;
|
|
62
|
+
}
|
|
63
|
+
release(token) { this.#db.prepare("DELETE FROM response_lease WHERE token=?").run(token); }
|
|
64
|
+
activeSessions(cohort, since) {
|
|
65
|
+
return this.#db.prepare(`SELECT DISTINCT session_id FROM response_results
|
|
66
|
+
WHERE cohort=? AND active=1 AND episode_at>=? ORDER BY session_id`).all(cohort, since).map(row => String(row.session_id));
|
|
67
|
+
}
|
|
68
|
+
observe(cohort, sessionId, episodes) {
|
|
69
|
+
this.#db.exec("BEGIN IMMEDIATE");
|
|
70
|
+
try {
|
|
71
|
+
this.#db.prepare("UPDATE response_results SET active=0 WHERE cohort=? AND session_id=?").run(cohort, sessionId);
|
|
72
|
+
const insert = this.#db.prepare(`INSERT INTO response_results(cohort,id,session_id,input_hash,episode_at,active,status)
|
|
73
|
+
VALUES(?,?,?,?,?,1,'pending') ON CONFLICT(cohort,id) DO UPDATE SET active=1,
|
|
74
|
+
input_hash=excluded.input_hash,episode_at=excluded.episode_at,
|
|
75
|
+
status=CASE WHEN input_hash=excluded.input_hash THEN status ELSE 'pending' END,
|
|
76
|
+
attempts=CASE WHEN input_hash=excluded.input_hash THEN attempts ELSE 0 END,
|
|
77
|
+
attempted_at=CASE WHEN input_hash=excluded.input_hash THEN attempted_at ELSE NULL END,
|
|
78
|
+
assessed_at=CASE WHEN input_hash=excluded.input_hash THEN assessed_at ELSE NULL END,
|
|
79
|
+
result=CASE WHEN input_hash=excluded.input_hash THEN result ELSE NULL END`);
|
|
80
|
+
for (const e of episodes)
|
|
81
|
+
insert.run(cohort, e.id, sessionId, e.inputHash, e.timestamp);
|
|
82
|
+
this.reviews.reconcile(cohort);
|
|
83
|
+
this.#db.exec("COMMIT");
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
this.#db.exec("ROLLBACK");
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
needsJudgment(cohort, e, now) {
|
|
91
|
+
const row = this.#db.prepare("SELECT status,attempts,attempted_at FROM response_results WHERE cohort=? AND id=? AND input_hash=? AND active=1")
|
|
92
|
+
.get(cohort, e.id, e.inputHash);
|
|
93
|
+
return !!row && row.status !== "ok" && Number(row.attempts) < 3 && (!row.attempted_at || now - Number(row.attempted_at) >= 600_000);
|
|
94
|
+
}
|
|
95
|
+
attempted(cohort, e, now) {
|
|
96
|
+
this.#db.prepare("UPDATE response_results SET attempts=attempts+1,attempted_at=?,status='failed' WHERE cohort=? AND id=? AND input_hash=?")
|
|
97
|
+
.run(now, cohort, e.id, e.inputHash);
|
|
98
|
+
}
|
|
99
|
+
stale(cohort, e) {
|
|
100
|
+
// Snapshot races are not provider failures and must not exhaust their retry budget.
|
|
101
|
+
this.#db.prepare(`UPDATE response_results SET attempts=MAX(0,attempts-1),attempted_at=NULL,status='pending'
|
|
102
|
+
WHERE cohort=? AND id=? AND input_hash=? AND active=1 AND status='failed'`).run(cohort, e.id, e.inputHash);
|
|
103
|
+
}
|
|
104
|
+
save(cohort, e, result, now) {
|
|
105
|
+
result = { ...result, human: result.human ?? new ResponsePeople().resolve(e) };
|
|
106
|
+
this.#db.exec("BEGIN IMMEDIATE");
|
|
107
|
+
try {
|
|
108
|
+
const changed = this.#db.prepare("UPDATE response_results SET status='ok',assessed_at=?,result=? WHERE cohort=? AND id=? AND input_hash=? AND active=1")
|
|
109
|
+
.run(now, JSON.stringify(result), cohort, e.id, e.inputHash).changes;
|
|
110
|
+
if (changed)
|
|
111
|
+
this.reviews.sync(cohort, e.id, e.inputHash, result, now);
|
|
112
|
+
this.#db.exec("COMMIT");
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
this.#db.exec("ROLLBACK");
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
cursor(cohort) { return String(this.#db.prepare("SELECT cursor FROM response_cursors WHERE cohort=?").get(cohort)?.cursor ?? ""); }
|
|
120
|
+
advance(cohort, cursor) {
|
|
121
|
+
this.#db.prepare("INSERT INTO response_cursors VALUES(?,?) ON CONFLICT(cohort) DO UPDATE SET cursor=excluded.cursor").run(cohort, cursor);
|
|
122
|
+
}
|
|
123
|
+
checkpoint(cohort, session) {
|
|
124
|
+
const row = this.#db.prepare("SELECT revision,coverage FROM response_checkpoints WHERE cohort=? AND session_id=?").get(cohort, session);
|
|
125
|
+
return row ? { revision: String(row.revision), coverage: JSON.parse(String(row.coverage)) } : undefined;
|
|
126
|
+
}
|
|
127
|
+
checkpointSave(cohort, session, revision, coverage) {
|
|
128
|
+
this.#db.prepare("INSERT INTO response_checkpoints VALUES(?,?,?,?) ON CONFLICT(cohort,session_id) DO UPDATE SET revision=excluded.revision,coverage=excluded.coverage")
|
|
129
|
+
.run(cohort, session, revision, JSON.stringify(coverage));
|
|
130
|
+
}
|
|
131
|
+
checkpointForget(cohort, session) {
|
|
132
|
+
this.#db.prepare("DELETE FROM response_checkpoints WHERE cohort=? AND session_id=?").run(cohort, session);
|
|
133
|
+
}
|
|
134
|
+
sessionWork(cohort, session, since, now) {
|
|
135
|
+
const row = this.#db.prepare(`SELECT COUNT(*) total,SUM(CASE WHEN status!='ok' AND attempts<3 AND
|
|
136
|
+
(attempted_at IS NULL OR attempted_at<=?) THEN 1 ELSE 0 END) due FROM response_results
|
|
137
|
+
WHERE cohort=? AND session_id=? AND active=1 AND episode_at>=?`).get(now - 600_000, cohort, session, since);
|
|
138
|
+
return { total: Number(row.total), due: Number(row.due ?? 0) };
|
|
139
|
+
}
|
|
140
|
+
pendingWork(cohort, since, now) {
|
|
141
|
+
return Number(this.#db.prepare(`SELECT COUNT(*) n FROM response_results WHERE cohort=? AND active=1 AND episode_at>=?
|
|
142
|
+
AND status!='ok' AND attempts<3 AND (attempted_at IS NULL OR attempted_at<=?)`).get(cohort, since, now - 600_000).n);
|
|
143
|
+
}
|
|
144
|
+
stage(cohort, e, stage, key) {
|
|
145
|
+
this.#db.prepare("INSERT OR IGNORE INTO response_stages(key,stage) VALUES(?,?)").run(key, stage);
|
|
146
|
+
this.#db.prepare(`INSERT INTO response_stage_links VALUES(?,?,?,?,?) ON CONFLICT(cohort,episode_id,stage)
|
|
147
|
+
DO UPDATE SET input_hash=excluded.input_hash,key=excluded.key`).run(cohort, e.id, e.inputHash, stage, key);
|
|
148
|
+
const row = this.#db.prepare("SELECT result FROM response_stages WHERE key=? AND status='ok'").get(key);
|
|
149
|
+
return row ? JSON.parse(String(row.result)) : undefined;
|
|
150
|
+
}
|
|
151
|
+
stageBegin(keys, now) {
|
|
152
|
+
this.#db.exec("BEGIN IMMEDIATE");
|
|
153
|
+
try {
|
|
154
|
+
for (const key of keys) {
|
|
155
|
+
const changed = this.#db.prepare(`UPDATE response_stages SET attempts=attempts+1,attempted_at=?,status='failed'
|
|
156
|
+
WHERE key=? AND status!='ok' AND attempts<3 AND (attempted_at IS NULL OR attempted_at<=?)`).run(now, key, now - 600_000).changes;
|
|
157
|
+
if (!changed)
|
|
158
|
+
throw new Error("Response stage in backoff or retry exhausted");
|
|
159
|
+
}
|
|
160
|
+
this.#db.exec("COMMIT");
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
this.#db.exec("ROLLBACK");
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
stageSave(key, result, now) {
|
|
168
|
+
this.#db.prepare("UPDATE response_stages SET status='ok',result=?,assessed_at=? WHERE key=?").run(JSON.stringify(result), now, key);
|
|
169
|
+
}
|
|
170
|
+
retryFailed(cohort) {
|
|
171
|
+
this.#db.exec("BEGIN IMMEDIATE");
|
|
172
|
+
try {
|
|
173
|
+
const episodes = this.#db.prepare(`UPDATE response_results AS r SET attempts=0,attempted_at=NULL,status='pending'
|
|
174
|
+
WHERE cohort=? AND active=1 AND (status!='ok' OR json_extract(result,'$.memory.status')='unavailable' OR EXISTS (
|
|
175
|
+
SELECT 1 FROM response_stage_links l JOIN response_stages s ON s.key=l.key
|
|
176
|
+
WHERE l.cohort=r.cohort AND l.episode_id=r.id AND l.input_hash=r.input_hash AND s.status!='ok'))`).run(cohort).changes;
|
|
177
|
+
const stages = this.#db.prepare(`UPDATE response_stages SET attempts=0,attempted_at=NULL,status='pending'
|
|
178
|
+
WHERE status!='ok' AND key IN (SELECT l.key FROM response_stage_links l JOIN response_results r
|
|
179
|
+
ON r.cohort=l.cohort AND r.id=l.episode_id AND r.input_hash=l.input_hash WHERE r.cohort=? AND r.active=1)`)
|
|
180
|
+
.run(cohort).changes;
|
|
181
|
+
this.#db.exec("COMMIT");
|
|
182
|
+
return { stages, episodes };
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
this.#db.exec("ROLLBACK");
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
scan(cohort, coverage, now) {
|
|
190
|
+
this.#db.prepare("INSERT INTO response_scans VALUES(?,?,?) ON CONFLICT(cohort) DO UPDATE SET observed_at=excluded.observed_at,coverage=excluded.coverage")
|
|
191
|
+
.run(cohort, now, JSON.stringify(coverage));
|
|
192
|
+
}
|
|
193
|
+
report(cohort, since, id, options = {}) {
|
|
194
|
+
const until = options.until ?? Number.MAX_SAFE_INTEGER, bucket = options.bucket ?? "week";
|
|
195
|
+
if (!Number.isFinite(since) || !Number.isFinite(until) || until <= since || !["day", "week"].includes(bucket))
|
|
196
|
+
throw new Error("Invalid response report range");
|
|
197
|
+
if (!!options.senderId !== !!options.accountScope)
|
|
198
|
+
throw new Error("Sender reports require both sender ID and account scope");
|
|
199
|
+
const filters = [
|
|
200
|
+
["$.human.senderId", options.senderId], ["$.human.accountScope", options.accountScope], ["$.human.personId", options.personId],
|
|
201
|
+
["$.quality.taskType.choice", options.taskType], ["$.agentModel", options.agentModel],
|
|
202
|
+
].filter((pair) => pair[1] !== undefined);
|
|
203
|
+
const rows = this.#db.prepare(`SELECT id,episode_at,assessed_at,status,result FROM response_results
|
|
204
|
+
WHERE cohort=? AND active=1 AND episode_at>=? AND episode_at<? ${id ? "AND id=?" : ""}
|
|
205
|
+
${filters.map(() => "AND json_extract(result,?)=?").join(" ")}
|
|
206
|
+
ORDER BY episode_at DESC,id LIMIT 10001`).all(cohort, since, until, ...(id ? [id] : []), ...filters.flat());
|
|
207
|
+
const scan = this.#db.prepare("SELECT observed_at,coverage FROM response_scans WHERE cohort=?").get(cohort);
|
|
208
|
+
const groups = new Map();
|
|
209
|
+
const examples = [];
|
|
210
|
+
for (const row of rows.slice(0, 10000)) {
|
|
211
|
+
if (row.status !== "ok" || typeof row.result !== "string")
|
|
212
|
+
continue;
|
|
213
|
+
const r = JSON.parse(row.result);
|
|
214
|
+
const date = new Date(Number(row.episode_at));
|
|
215
|
+
if (bucket === "week")
|
|
216
|
+
date.setUTCDate(date.getUTCDate() - (date.getUTCDay() + 6) % 7);
|
|
217
|
+
const week = date.toISOString().slice(0, 10);
|
|
218
|
+
const taskType = r.quality.taskType.confidence >= 0.8 ? r.quality.taskType.choice : "uncertain";
|
|
219
|
+
const human = r.human ?? null;
|
|
220
|
+
const key = JSON.stringify([week, taskType, r.agentModel, human?.key ?? null]);
|
|
221
|
+
const g = groups.get(key) ?? { week, human, taskType, agentModel: r.agentModel, evaluated: 0, assessable: 0, fitAssessable: 0,
|
|
222
|
+
fulfillmentScored: 0, deliverableFitScored: 0,
|
|
223
|
+
fulfillmentSum: 0, deliverableSum: 0, feedbackCertain: 0, accepted: 0, reworkCertain: 0, rework: 0, memoryGap: 0, dissatisfied: 0,
|
|
224
|
+
sentimentAssessed: 0, sentimentCertain: 0, emotionAssessed: 0, annoyed: 0, frustrated: 0,
|
|
225
|
+
annoyanceUncertain: 0, frustrationUncertain: 0, dissatisfactionIntensityScored: 0, intensitySum: 0,
|
|
226
|
+
underdeliveryCertain: 0, underdelivery: 0, failureReasons: {},
|
|
227
|
+
feedbackTargets: {}, retrospectiveAssessed: 0, laterCorrections: 0, deliveryAdmissions: 0,
|
|
228
|
+
outcomeKnown: 0, acknowledgedSuccess: 0, reportedShortfall: 0, outcomeReasons: {} };
|
|
229
|
+
g.evaluated++;
|
|
230
|
+
const underdelivery = r.quality.underdelivery.noul >= 0.8;
|
|
231
|
+
if (r.quality.underdelivery.noul <= 0.2 || underdelivery)
|
|
232
|
+
g.underdeliveryCertain++;
|
|
233
|
+
if (underdelivery) {
|
|
234
|
+
g.underdelivery++;
|
|
235
|
+
const reason = r.quality.failureReason.confidence >= 0.8 ? r.quality.failureReason.choice : "uncertain";
|
|
236
|
+
g.failureReasons[reason] = (g.failureReasons[reason] ?? 0) + 1;
|
|
237
|
+
}
|
|
238
|
+
if (r.quality.assessability.choice === "assessable" && r.quality.assessability.confidence >= 0.8) {
|
|
239
|
+
g.assessable++;
|
|
240
|
+
if (r.quality.fulfillment.confidence >= 0.8) {
|
|
241
|
+
g.fulfillmentScored++;
|
|
242
|
+
g.fulfillmentSum += r.quality.fulfillment.score;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (r.quality.fitAssessability.choice === "assessable" && r.quality.fitAssessability.confidence >= 0.8) {
|
|
246
|
+
g.fitAssessable++;
|
|
247
|
+
if (r.quality.deliverableFit.confidence >= 0.8) {
|
|
248
|
+
g.deliverableFitScored++;
|
|
249
|
+
g.deliverableSum += r.quality.deliverableFit.score;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const target = r.feedback.target.confidence >= 0.8 ? r.feedback.target.choice : "uncertain";
|
|
253
|
+
g.feedbackTargets[target] = (g.feedbackTargets[target] ?? 0) + 1;
|
|
254
|
+
const laterCorrection = (r.retrospective.judgment?.correction.noul ?? 0) >= 0.8;
|
|
255
|
+
const deliveryAdmission = (r.retrospective.judgment?.deliveryAdmission.noul ?? 0) >= 0.8;
|
|
256
|
+
const outcome = responseOutcome(r);
|
|
257
|
+
const reportedShortfall = outcome.status === "reported_shortfall";
|
|
258
|
+
if (reportedShortfall || outcome.status === "acknowledged_success") {
|
|
259
|
+
g.outcomeKnown++;
|
|
260
|
+
if (reportedShortfall) {
|
|
261
|
+
g.reportedShortfall++;
|
|
262
|
+
for (const label of outcome.reasons.length ? outcome.reasons : ["uncertain"]) {
|
|
263
|
+
g.outcomeReasons[label] = (g.outcomeReasons[label] ?? 0) + 1;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
else
|
|
267
|
+
g.acknowledgedSuccess++;
|
|
268
|
+
}
|
|
269
|
+
if (r.retrospective.judgment)
|
|
270
|
+
g.retrospectiveAssessed++;
|
|
271
|
+
if (laterCorrection)
|
|
272
|
+
g.laterCorrections++;
|
|
273
|
+
if (deliveryAdmission)
|
|
274
|
+
g.deliveryAdmissions++;
|
|
275
|
+
if (r.feedback.feedbackType.confidence >= 0.8) {
|
|
276
|
+
g.feedbackCertain++;
|
|
277
|
+
if (r.feedback.feedbackType.choice === "acceptance")
|
|
278
|
+
g.accepted++;
|
|
279
|
+
}
|
|
280
|
+
const burden = r.feedback.avoidableRework.noul;
|
|
281
|
+
if (burden <= 0.2 || burden >= 0.8) {
|
|
282
|
+
g.reworkCertain++;
|
|
283
|
+
if (burden >= 0.8)
|
|
284
|
+
g.rework++;
|
|
285
|
+
}
|
|
286
|
+
const memoryGap = r.feedback.memoryGap.noul >= 0.8;
|
|
287
|
+
const sentiment = r.feedback.sentiment;
|
|
288
|
+
const sentimentCertain = !!sentiment && sentiment.confidence >= 0.8 && sentiment.choice !== "unclear";
|
|
289
|
+
const dissatisfied = sentimentCertain && ["dissatisfied", "mixed"].includes(sentiment.choice);
|
|
290
|
+
if (sentiment)
|
|
291
|
+
g.sentimentAssessed++;
|
|
292
|
+
if (sentimentCertain)
|
|
293
|
+
g.sentimentCertain++;
|
|
294
|
+
const { annoyance, frustration, dissatisfactionIntensity: intensity } = r.feedback;
|
|
295
|
+
const annoyed = !!annoyance && annoyance.noul >= 0.8;
|
|
296
|
+
const frustrated = !!frustration && frustration.noul >= 0.8;
|
|
297
|
+
if (annoyance && frustration) {
|
|
298
|
+
g.emotionAssessed++;
|
|
299
|
+
if (annoyed)
|
|
300
|
+
g.annoyed++;
|
|
301
|
+
if (frustrated)
|
|
302
|
+
g.frustrated++;
|
|
303
|
+
if (annoyance.noul > 0.2 && annoyance.noul < 0.8)
|
|
304
|
+
g.annoyanceUncertain++;
|
|
305
|
+
if (frustration.noul > 0.2 && frustration.noul < 0.8)
|
|
306
|
+
g.frustrationUncertain++;
|
|
307
|
+
}
|
|
308
|
+
if (intensity && intensity.confidence >= 0.8) {
|
|
309
|
+
g.dissatisfactionIntensityScored++;
|
|
310
|
+
g.intensitySum += intensity.score;
|
|
311
|
+
}
|
|
312
|
+
if (memoryGap)
|
|
313
|
+
g.memoryGap++;
|
|
314
|
+
if (dissatisfied)
|
|
315
|
+
g.dissatisfied++;
|
|
316
|
+
groups.set(key, g);
|
|
317
|
+
const signals = [underdelivery ? "clear_underdelivery" : "", burden >= 0.8 ? "avoidable_rework" : "", memoryGap ? "reported_memory_gap" : "", dissatisfied ? `dissatisfied:${target}` : "",
|
|
318
|
+
annoyed ? `annoyed:${target}` : "", frustrated ? `frustrated:${target}` : "",
|
|
319
|
+
laterCorrection ? "later_correction" : "", deliveryAdmission ? "delivery_admission" : "", reportedShortfall ? "reported_shortfall" : ""].filter(Boolean);
|
|
320
|
+
if (signals.length && examples.length < 20)
|
|
321
|
+
examples.push({ id: String(row.id), episodeAt: Number(row.episode_at), assessedAt: Number(row.assessed_at), signals, outcome });
|
|
322
|
+
}
|
|
323
|
+
const summaries = [...groups.values()].map(({ fulfillmentSum, deliverableSum, intensitySum, ...g }) => ({ ...g,
|
|
324
|
+
periodStart: g.week,
|
|
325
|
+
fulfillmentMean: g.fulfillmentScored ? fulfillmentSum / g.fulfillmentScored : null,
|
|
326
|
+
deliverableFitMean: g.deliverableFitScored ? deliverableSum / g.deliverableFitScored : null,
|
|
327
|
+
sentimentNotAssessed: g.evaluated - g.sentimentAssessed,
|
|
328
|
+
sentimentUnknown: g.sentimentAssessed - g.sentimentCertain,
|
|
329
|
+
sentimentCoverage: g.sentimentAssessed / g.evaluated,
|
|
330
|
+
// Observed rates include uncertainty in their denominator, never silently label it neutral.
|
|
331
|
+
dissatisfactionRate: g.sentimentAssessed ? g.dissatisfied / g.sentimentAssessed : null,
|
|
332
|
+
sentimentUnknownRate: g.sentimentAssessed ? (g.sentimentAssessed - g.sentimentCertain) / g.sentimentAssessed : null,
|
|
333
|
+
annoyanceRate: g.emotionAssessed ? g.annoyed / g.emotionAssessed : null,
|
|
334
|
+
frustrationRate: g.emotionAssessed ? g.frustrated / g.emotionAssessed : null,
|
|
335
|
+
dissatisfactionIntensityMean: g.dissatisfactionIntensityScored ? intensitySum / g.dissatisfactionIntensityScored : null,
|
|
336
|
+
observedSuccessRate: g.outcomeKnown ? g.acknowledgedSuccess / g.outcomeKnown : null,
|
|
337
|
+
observedSuccessRate95Interval: wilson(g.acknowledgedSuccess, g.outcomeKnown),
|
|
338
|
+
outcomeUnknown: g.evaluated - g.outcomeKnown,
|
|
339
|
+
outcomeCoverage: g.outcomeKnown / g.evaluated,
|
|
340
|
+
acknowledgedRate: g.acknowledgedSuccess / g.evaluated,
|
|
341
|
+
reportedShortfallRate: g.reportedShortfall / g.evaluated,
|
|
342
|
+
unknownRate: (g.evaluated - g.outcomeKnown) / g.evaluated,
|
|
343
|
+
underdeliveryRate: g.underdeliveryCertain ? g.underdelivery / g.underdeliveryCertain : null,
|
|
344
|
+
underdeliveryRate95Interval: wilson(g.underdelivery, g.underdeliveryCertain),
|
|
345
|
+
reworkRate: g.reworkCertain ? g.rework / g.reworkCertain : null,
|
|
346
|
+
reworkRate95Interval: wilson(g.rework, g.reworkCertain), smallSample: g.evaluated < 20,
|
|
347
|
+
}));
|
|
348
|
+
const trends = summaries.flatMap(current => {
|
|
349
|
+
const previous = summaries.filter(g => g.taskType === current.taskType && g.agentModel === current.agentModel &&
|
|
350
|
+
g.human?.key === current.human?.key && g.week < current.week)
|
|
351
|
+
.sort((a, b) => b.week.localeCompare(a.week))[0];
|
|
352
|
+
if (!previous)
|
|
353
|
+
return [];
|
|
354
|
+
return ["fulfillment", "deliverableFit", "acknowledgedRate", "reportedShortfallRate", "unknownRate",
|
|
355
|
+
"dissatisfactionRate", "sentimentUnknownRate", "annoyanceRate", "frustrationRate", "dissatisfactionIntensity"].map(dimension => {
|
|
356
|
+
const scoreDimension = dimension === "fulfillment" || dimension === "deliverableFit" || dimension === "dissatisfactionIntensity" ? dimension : null;
|
|
357
|
+
const sentimentDimension = dimension === "dissatisfactionRate" || dimension === "sentimentUnknownRate";
|
|
358
|
+
const emotionDimension = dimension === "annoyanceRate" || dimension === "frustrationRate";
|
|
359
|
+
const beforeN = scoreDimension ? previous[`${scoreDimension}Scored`] : sentimentDimension ? previous.sentimentAssessed : emotionDimension ? previous.emotionAssessed : previous.evaluated;
|
|
360
|
+
const afterN = scoreDimension ? current[`${scoreDimension}Scored`] : sentimentDimension ? current.sentimentAssessed : emotionDimension ? current.emotionAssessed : current.evaluated;
|
|
361
|
+
const before = dimension === "fulfillment" || dimension === "deliverableFit" || dimension === "dissatisfactionIntensity" ? previous[`${dimension}Mean`] : previous[dimension];
|
|
362
|
+
const after = dimension === "fulfillment" || dimension === "deliverableFit" || dimension === "dissatisfactionIntensity" ? current[`${dimension}Mean`] : current[dimension];
|
|
363
|
+
const beforeCoverage = scoreDimension || sentimentDimension || emotionDimension ? beforeN / previous.evaluated : previous.outcomeCoverage;
|
|
364
|
+
const afterCoverage = scoreDimension || sentimentDimension || emotionDimension ? afterN / current.evaluated : current.outcomeCoverage;
|
|
365
|
+
const coverageChanged = beforeCoverage !== afterCoverage;
|
|
366
|
+
const enough = beforeN >= 20 && afterN >= 20 && current.taskType !== "uncertain" && current.agentModel !== "unknown";
|
|
367
|
+
const status = !current.human || !current.human.accountScope || current.taskType === "uncertain" || current.agentModel === "unknown" ? "unknown_stratum" :
|
|
368
|
+
!enough ? "insufficient_samples" : (scoreDimension || sentimentDimension || emotionDimension) && coverageChanged ? "coverage_changed" : "descriptive_comparison";
|
|
369
|
+
return { fromWeek: previous.week, toWeek: current.week, taskType: current.taskType, agentModel: current.agentModel,
|
|
370
|
+
fromPeriod: previous.week, toPeriod: current.week, human: current.human,
|
|
371
|
+
dimension, beforeN, afterN, status,
|
|
372
|
+
before, after, beforeEvaluated: previous.evaluated, afterEvaluated: current.evaluated,
|
|
373
|
+
beforeCoverage, afterCoverage, coverageChanged,
|
|
374
|
+
denominator: dimension === "dissatisfactionIntensity" ? "confident_intensity_exchanges" : scoreDimension ? "confident_assessable_exchanges" : sentimentDimension ? "sentiment_assessed_exchanges" : emotionDimension ? "emotion_assessed_exchanges" : "all_evaluated_exchanges",
|
|
375
|
+
scale: dimension === "dissatisfactionIntensity" ? "0_to_3_expressed_dissatisfaction" : scoreDimension ? "0_to_3_visible_quality" : "0_to_1_observed_rate",
|
|
376
|
+
delta: status === "descriptive_comparison" ? after - before : null };
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
const stages = this.#db.prepare(`SELECT s.stage,s.status,COUNT(*) count,
|
|
380
|
+
SUM(CASE WHEN s.status!='ok' AND s.attempts>=3 THEN 1 ELSE 0 END) exhausted
|
|
381
|
+
FROM response_stage_links l JOIN response_stages s ON s.key=l.key JOIN response_results r
|
|
382
|
+
ON r.cohort=l.cohort AND r.id=l.episode_id AND r.input_hash=l.input_hash
|
|
383
|
+
WHERE r.cohort=? AND r.active=1 AND r.episode_at>=? AND r.episode_at<? GROUP BY s.stage,s.status`).all(cohort, since, until);
|
|
384
|
+
return { cohort, reportVersion: RESPONSE_REPORT_VERSION, reviewPolicy: RESPONSE_REVIEW_POLICY, bucket, since, until,
|
|
385
|
+
annotations: this.reviews.annotations(since, until), stages, stageCountsScope: "cohort_and_date_range_before_person_filters",
|
|
386
|
+
rubricScope: "Responded-to exchanges only; observational, not factual verification or causal performance attribution.",
|
|
387
|
+
coverage: scan ? { observedAt: scan.observed_at, ...JSON.parse(String(scan.coverage)) } : null,
|
|
388
|
+
stored: rows.length, reportCapped: rows.length > 10000,
|
|
389
|
+
failedOrPending: rows.filter(r => r.status !== "ok").length,
|
|
390
|
+
groups: summaries, trends, examples,
|
|
391
|
+
...(id ? { episode: rows[0] ? { ...rows[0], result: rows[0].result ? JSON.parse(String(rows[0].result)) : null } : null } : {}),
|
|
392
|
+
limitations: ["Confidence cutoffs are provisional and need human calibration.", "Unseen artifacts and external correctness are not scored as verified.",
|
|
393
|
+
"Sentiment is expressed reaction, not verified agent fault. Annoyance and frustration may overlap. Intensity is not confidence or failure severity.",
|
|
394
|
+
"Sentiment rates are confident positive signals among sentiment-assessed exchanges (mixed includes dissatisfaction). Read unknown counts/rates alongside them; disabled or legacy-missing fields are not neutral. Emotion rates have their own assessed and uncertain counts.",
|
|
395
|
+
"Observed success is acknowledgment among known outcomes, not all responses or verified task success. Unknowns are excluded; changes in coverage can change this rate. Reasons can overlap.",
|
|
396
|
+
"Week buckets can be partial and repeated exchanges in a session are not independent. Intervals are descriptive, not calibrated uncertainty about the agent's overall performance.",
|
|
397
|
+
"Score deltas require 20 confident samples per dimension in both periods and unchanged scored coverage. Rate deltas require 20 evaluated exchanges per period; acknowledgment, shortfall and unknown rates share that denominator and must be read together.",
|
|
398
|
+
"Coverage flags and fixed rubric/report versions do not eliminate judge threshold variability or selection bias. Deltas are descriptive, not statistical change-point or causal evidence.",
|
|
399
|
+
"No feedback is not success. Compare like task types and judge versions; score movement alone is not proof of improvement.",
|
|
400
|
+
"Evidence references describe the assessed snapshot; transcript rewrites are reconciled on the next in-scope scan."] };
|
|
401
|
+
}
|
|
402
|
+
close() { this.#db.close(); }
|
|
403
|
+
}
|
|
404
|
+
function wilson(successes, total) {
|
|
405
|
+
if (!total)
|
|
406
|
+
return null;
|
|
407
|
+
const z = 1.96, p = successes / total, denominator = 1 + z * z / total;
|
|
408
|
+
const center = (p + z * z / (2 * total)) / denominator;
|
|
409
|
+
const half = z * Math.sqrt((p * (1 - p) + z * z / (4 * total)) / total) / denominator;
|
|
410
|
+
return [Math.max(0, center - half), Math.min(1, center + half)];
|
|
411
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Parse only recognized transport envelopes. Ambiguous wrappers are excluded,
|
|
2
|
+
* never flattened into a human's request or used to approve embedded speakers. */
|
|
3
|
+
export declare function responseUserText(input: string, senderId: string): {
|
|
4
|
+
text: string;
|
|
5
|
+
contextLimited: boolean;
|
|
6
|
+
} | undefined;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Parse only recognized transport envelopes. Ambiguous wrappers are excluded,
|
|
2
|
+
* never flattened into a human's request or used to approve embedded speakers. */
|
|
3
|
+
export function responseUserText(input, senderId) {
|
|
4
|
+
let text = input.trim();
|
|
5
|
+
let contextLimited = false;
|
|
6
|
+
const header = /^Conversation info: ⟦openclaw:ctx⟧\r?\n```json\r?\n([^]*?)\r?\n```\r?\n/.exec(text);
|
|
7
|
+
if (header) {
|
|
8
|
+
let meta;
|
|
9
|
+
try {
|
|
10
|
+
meta = JSON.parse(header[1]);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
if (!meta || meta.sender?.id !== senderId || typeof meta.sender.name !== "string")
|
|
16
|
+
return undefined;
|
|
17
|
+
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)];
|
|
19
|
+
if (markers.length !== 1 || markers[0][1] !== meta.sender.name)
|
|
20
|
+
return undefined;
|
|
21
|
+
const marker = markers[0];
|
|
22
|
+
contextLimited = meta.history_truncated === true || rest.slice(0, marker.index).includes("Chat history since last reply:");
|
|
23
|
+
text = rest.slice(marker.index + marker[0].length).trim();
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
const from = /^From: [^\r\n]+ \(([^()\r\n]+)\)\r?\n/.exec(text);
|
|
27
|
+
if (from) {
|
|
28
|
+
if (from[1] !== senderId)
|
|
29
|
+
return undefined;
|
|
30
|
+
text = text.slice(from[0].length).trim();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Unrecognized/nested envelopes cannot safely supply the current human text.
|
|
34
|
+
if (!text || text.includes("⟦openclaw:ctx⟧") || /^Chat history since last reply:/m.test(text))
|
|
35
|
+
return undefined;
|
|
36
|
+
return { text, contextLimited };
|
|
37
|
+
}
|
|
@@ -3,6 +3,11 @@ type RequestOptions = {
|
|
|
3
3
|
timeoutMs: number;
|
|
4
4
|
signal: AbortSignal;
|
|
5
5
|
};
|
|
6
|
+
type Json = string | number | boolean | null | Json[] | {
|
|
7
|
+
[key: string]: Json;
|
|
8
|
+
};
|
|
9
|
+
export declare const TYPESAFE_REVIEW_MODEL = "jev-1.13.0";
|
|
10
|
+
export declare function askTypeSafeReview(params: RequestOptions, state: Json, questions: Json): Promise<unknown>;
|
|
6
11
|
/** The source is an indexed snapshot, not proof of current truth or permission to write. */
|
|
7
12
|
export declare function reviewTypeSafeClaim(params: RequestOptions & {
|
|
8
13
|
claim: string;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { Value } from "typebox/value";
|
|
3
|
-
|
|
3
|
+
export const TYPESAFE_REVIEW_MODEL = "jev-1.13.0";
|
|
4
|
+
export async function askTypeSafeReview(params, state, questions) {
|
|
4
5
|
const signal = AbortSignal.any([params.signal, AbortSignal.timeout(params.timeoutMs)]);
|
|
5
6
|
try {
|
|
6
7
|
signal.throwIfAborted();
|
|
7
8
|
const response = await fetch("https://api.typesafe.ai/v1/systemone", {
|
|
8
9
|
method: "POST", redirect: "error", signal,
|
|
9
10
|
headers: { Authorization: `Bearer ${params.apiKey}`, "Content-Type": "application/json" },
|
|
10
|
-
body: JSON.stringify({ model:
|
|
11
|
+
body: JSON.stringify({ model: TYPESAFE_REVIEW_MODEL, state, questions }),
|
|
11
12
|
});
|
|
12
13
|
if (!response.ok) {
|
|
13
14
|
await response.body?.cancel();
|
|
@@ -31,7 +32,7 @@ const relationSchema = Type.Object({ answers: Type.Object({ relation: Type.Objec
|
|
|
31
32
|
}) }) });
|
|
32
33
|
/** The source is an indexed snapshot, not proof of current truth or permission to write. */
|
|
33
34
|
export async function reviewTypeSafeClaim(params) {
|
|
34
|
-
const payload = await
|
|
35
|
+
const payload = await askTypeSafeReview(params, { claim: params.claim, evidence: [...params.evidence] }, { relation: {
|
|
35
36
|
type: "choice",
|
|
36
37
|
instructions: {
|
|
37
38
|
question: "Does `evidence` support the exact atomic claim in `claim`?",
|
|
@@ -81,7 +82,7 @@ export async function reviewMemoryRedundancy(params) {
|
|
|
81
82
|
},
|
|
82
83
|
},
|
|
83
84
|
}]));
|
|
84
|
-
const payload = await
|
|
85
|
+
const payload = await askTypeSafeReview(params, { excerpts: [...params.excerpts] }, questions);
|
|
85
86
|
if (!Value.Check(nouls, payload) || Object.keys(payload.answers).length !== pairs.length ||
|
|
86
87
|
pairs.some((_pair, i) => !Object.hasOwn(payload.answers, `pair_${i}`)))
|
|
87
88
|
throw new Error("TypeSafe returned invalid redundancy judgments");
|
|
@@ -125,7 +126,7 @@ export async function reviewClusterDefects(params) {
|
|
|
125
126
|
none_or_uncertain: { definition: "Meaningful source content or insufficient evidence of the specific ingestion defects above.", examples: ["A useful JSON configuration", "A concrete deployment decision", "A quoted notification discussed as the subject of a technical explanation"] },
|
|
126
127
|
},
|
|
127
128
|
}]));
|
|
128
|
-
const payload = await
|
|
129
|
+
const payload = await askTypeSafeReview(params, { excerpts: [...params.excerpts] }, questions);
|
|
129
130
|
if (!Value.Check(schema, payload) || Object.keys(payload.answers).length !== params.excerpts.length ||
|
|
130
131
|
params.excerpts.some((_text, i) => !Object.hasOwn(payload.answers, `member_${i}`)))
|
|
131
132
|
throw new Error("TypeSafe returned invalid cluster judgments");
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "unblock-memory",
|
|
3
3
|
"name": "Unblock Memory",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.16",
|
|
5
5
|
"description": "Indexes, retrieves, and analyzes configured workspace memory with existing QMD vectors.",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"activation": { "onStartup": true },
|
|
@@ -43,6 +43,11 @@
|
|
|
43
43
|
"memory_people_sync": { "sideEffecting": true, "optional": true }
|
|
44
44
|
},
|
|
45
45
|
"uiHints": {
|
|
46
|
+
"responseAudit.enabled": { "label": "Response Quality Audit", "help": "Opt in to background TypeSafe evaluation of approved Slack humans. Operator-only reports; no prompt or memory writes." },
|
|
47
|
+
"responseAudit.sentimentEnabled": { "label": "Human Sentiment Analysis", "help": "Default on within an enabled, approved response audit. Includes annoyance, frustration and expressed intensity; does not imply agent fault." },
|
|
48
|
+
"responseAudit.intervalMinutes": { "label": "Response Audit Interval (minutes)", "help": "Shared cadence for quality and enabled sentiment analysis. Unchanged successful exchanges are cached. Zero means manual-only." },
|
|
49
|
+
"responseAudit.senderIds": { "label": "Approved Human Sender IDs", "help": "Explicit human Slack user IDs whose exchanges may be sent to TypeSafe. Also requires trusted human identity or owner metadata; explicit bots are always excluded." },
|
|
50
|
+
"responseAudit.memoryCorpora": { "label": "Response Audit Memory Evidence", "help": "Optional configured file corpora approved for current-index memory-gap investigation. Does not prove historical availability." },
|
|
46
51
|
"qualityAudit.enabled": {
|
|
47
52
|
"label": "Memory Quality Audit",
|
|
48
53
|
"help": "Enable an on-demand TypeSafe audit. Records review indicators only; never edits or suppresses data."
|
|
@@ -99,6 +104,20 @@
|
|
|
99
104
|
"type": "object",
|
|
100
105
|
"additionalProperties": false,
|
|
101
106
|
"properties": {
|
|
107
|
+
"responseAudit": {
|
|
108
|
+
"type": "object", "additionalProperties": false,
|
|
109
|
+
"properties": {
|
|
110
|
+
"enabled": { "type": "boolean", "default": false },
|
|
111
|
+
"sentimentEnabled": { "type": "boolean", "default": true },
|
|
112
|
+
"senderIds": { "type": "array", "maxItems": 50, "items": { "type": "string", "pattern": "\\S" }, "default": [] },
|
|
113
|
+
"chatTypes": { "type": "array", "minItems": 1, "maxItems": 50, "items": { "type": "string", "enum": ["direct", "group", "channel"] }, "default": ["direct"] },
|
|
114
|
+
"historyMessages": { "type": "integer", "minimum": 0, "maximum": 20, "default": 6 },
|
|
115
|
+
"lookbackDays": { "type": "integer", "minimum": 1, "maximum": 90, "default": 30 },
|
|
116
|
+
"maxEpisodes": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 },
|
|
117
|
+
"intervalMinutes": { "type": "integer", "minimum": 0, "maximum": 1440, "default": 60 },
|
|
118
|
+
"memoryCorpora": { "type": "array", "maxItems": 50, "items": { "type": "string", "pattern": "\\S" }, "default": [] }
|
|
119
|
+
}
|
|
120
|
+
},
|
|
102
121
|
"qualityAudit": {
|
|
103
122
|
"type": "object",
|
|
104
123
|
"additionalProperties": false,
|