@unblocklabs/unblock-memory 0.3.18 → 0.3.20
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 +33 -4
- package/dist/src/curation.js +40 -44
- package/dist/src/memory-database.d.ts +6 -0
- package/dist/src/memory-database.js +157 -0
- package/dist/src/people-store.js +19 -21
- package/dist/src/response-audit.js +4 -2
- package/dist/src/response-identity.js +5 -0
- package/dist/src/response-runtime.js +5 -5
- package/dist/src/response-store.d.ts +1 -1
- package/dist/src/response-store.js +36 -29
- package/dist/src/runtime.js +2 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -154,8 +154,9 @@ Choice confidence from Noul yes-probability. Multiple supported reasons can coex
|
|
|
154
154
|
`reportVersion` identifies composition/reporting semantics independently of the
|
|
155
155
|
judge rubric, allowing cached judgments to be re-reported without re-inference.
|
|
156
156
|
|
|
157
|
-
Results live in the agent's private
|
|
158
|
-
the memory index.
|
|
157
|
+
Results live in operator-only tables in the agent's private
|
|
158
|
+
`unblock-memory/unblock-memory.sqlite`, outside the memory index. These tables
|
|
159
|
+
are not searched or injected into agent prompts. They store judgments and source event references/hashes, not copies
|
|
159
160
|
of conversations. Identical successful inputs are cached; source rewrites invalidate
|
|
160
161
|
in-scope results on the next scan. Reports partition by fixed judge/rubric/context
|
|
161
162
|
configuration, UTC week, task type and agent model. They expose eligible/assessed
|
|
@@ -599,7 +600,7 @@ For optional read-only diagnostics, `memory_people_prime({ personId, agentName?,
|
|
|
599
600
|
draft: { blurb, citations: [{ path, from, lines }] } })` still reviews a snippet
|
|
600
601
|
without writing. Agents do not need this extra call in the normal update workflow.
|
|
601
602
|
|
|
602
|
-
Judgments are cached privately in `
|
|
603
|
+
Judgments are cached privately in `unblock-memory.sqlite` (maximum 2,000 entries), keyed
|
|
603
604
|
by person, agent, exact evidence/context, questions,
|
|
604
605
|
and judge version. No source text or credentials are stored in the cache.
|
|
605
606
|
Retrieval reruns against the current index; unchanged judgments are reused.
|
|
@@ -758,10 +759,38 @@ explicit intervals remain unchanged on upgrade; set them to `60` for hourly chec
|
|
|
758
759
|
|
|
759
760
|
Indexes live at `~/.openclaw/agents/<agentId>/unblock-memory/index.sqlite` (or the
|
|
760
761
|
equivalent configured OpenClaw state directory). Durable agent-supplied event
|
|
761
|
-
dates
|
|
762
|
+
dates, maintenance proposals, people/dossiers and response audits live separately
|
|
763
|
+
in `unblock-memory.sqlite`, so a QMD
|
|
762
764
|
index rebuild does not discard them. The first lookup builds the index;
|
|
763
765
|
Markdown filesystem changes queue a debounced, serialized background refresh.
|
|
764
766
|
|
|
767
|
+
### Durable database migration (v0.3.20)
|
|
768
|
+
|
|
769
|
+
Each agent has two active plugin databases: rebuildable `index.sqlite` and durable
|
|
770
|
+
`unblock-memory.sqlite`. The latter uses WAL, private permissions and component
|
|
771
|
+
schema versions. Store modules and tool access remain separate: consolidating files
|
|
772
|
+
does not expose operator response audits to memory searches or whisperers.
|
|
773
|
+
|
|
774
|
+
**Stop the Gateway and any plugin CLI writers before upgrading.** On first
|
|
775
|
+
durable-store access, the plugin imports existing `curation.sqlite`, `people.sqlite`
|
|
776
|
+
and `response-audit.sqlite` files, including disabled features, in one transaction.
|
|
777
|
+
It includes committed WAL data, verifies row counts/values, integrity and foreign
|
|
778
|
+
keys, and records completion. Missing stores are normal. Unsupported or invalid
|
|
779
|
+
data aborts the import without a partial cutover; the next access retries.
|
|
780
|
+
QMD and transcript databases are not migrated.
|
|
781
|
+
|
|
782
|
+
Old files remain untouched as **inert recovery copies**, not active stores.
|
|
783
|
+
Completed migration never reimports or writes to them. Do not run old and new
|
|
784
|
+
plugin versions together: old writers can keep changing their separate files.
|
|
785
|
+
Back up the new database with SQLite's online backup API, or stop all writers
|
|
786
|
+
and safely checkpoint WAL first. Copying only a live `.sqlite` file is unsafe.
|
|
787
|
+
|
|
788
|
+
To roll back before any new writes, stop all writers, preserve the new database
|
|
789
|
+
and its WAL/SHM sidecars, and restore the old plugin against the retained files.
|
|
790
|
+
**After new writes, the legacy files are stale:** rollback requires an explicit
|
|
791
|
+
reverse data migration or accepting the loss of post-upgrade changes.
|
|
792
|
+
Retain recovery files until the upgrade is verified; cleanup is a separate step.
|
|
793
|
+
|
|
765
794
|
## Memory quality audit
|
|
766
795
|
|
|
767
796
|
`memory_audit_quality` is an on-demand, source-read-only audit. TypeSafe flags likely
|
package/dist/src/curation.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import { dirname } from "node:path";
|
|
4
|
-
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { openMemoryDatabase } from "./memory-database.js";
|
|
5
3
|
const TEMPORAL_BASES = ["path", "frontmatter", "session", "agent_verified"];
|
|
6
4
|
const MAINTENANCE_TASK_TYPES = ["ambiguous_event_time", "exact_duplicate", "quality_review"];
|
|
7
5
|
const MAINTENANCE_STATUSES = ["pending", "resolved", "deferred", "irrelevant"];
|
|
@@ -42,35 +40,41 @@ export function chunkFingerprint(text) {
|
|
|
42
40
|
export class CurationStore {
|
|
43
41
|
#db;
|
|
44
42
|
constructor(path) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
43
|
+
this.#db = openMemoryDatabase(path);
|
|
44
|
+
try {
|
|
45
|
+
this.#db.exec("BEGIN IMMEDIATE");
|
|
46
|
+
const version = this.#db.prepare("SELECT version FROM memory_schema WHERE component='curation'").get()?.version;
|
|
47
|
+
if (version !== undefined && version !== 1)
|
|
48
|
+
throw new Error("Unsupported curation schema version");
|
|
49
|
+
this.#db.exec(`
|
|
50
|
+
CREATE TABLE IF NOT EXISTS temporal_annotations (
|
|
51
|
+
corpus TEXT NOT NULL,
|
|
52
|
+
collection TEXT NOT NULL,
|
|
53
|
+
path TEXT NOT NULL,
|
|
54
|
+
content_fingerprint TEXT NOT NULL DEFAULT '',
|
|
55
|
+
event_time TEXT NOT NULL,
|
|
56
|
+
basis TEXT NOT NULL CHECK (basis IN ('path', 'frontmatter', 'session', 'agent_verified')),
|
|
57
|
+
evidence TEXT NOT NULL,
|
|
58
|
+
qmd_hash TEXT,
|
|
59
|
+
qmd_seq INTEGER,
|
|
60
|
+
created_at TEXT NOT NULL,
|
|
61
|
+
updated_at TEXT NOT NULL,
|
|
62
|
+
PRIMARY KEY (corpus, collection, path, content_fingerprint)
|
|
63
|
+
);
|
|
66
64
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
65
|
+
`);
|
|
66
|
+
this.#ensureMaintenanceSchema();
|
|
67
|
+
this.#db.exec(`CREATE TABLE IF NOT EXISTS quality_judgments (
|
|
68
|
+
cache_key TEXT PRIMARY KEY,
|
|
69
|
+
noise REAL NOT NULL CHECK (noise BETWEEN 0 AND 1),
|
|
70
|
+
evidence REAL NOT NULL CHECK (evidence BETWEEN 0 AND 1)
|
|
71
|
+
)`);
|
|
72
|
+
this.#db.exec("INSERT OR IGNORE INTO memory_schema VALUES ('curation',1); COMMIT");
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
this.#db.close();
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
74
78
|
}
|
|
75
79
|
#ensureMaintenanceSchema() {
|
|
76
80
|
this.#db.exec(`
|
|
@@ -93,19 +97,11 @@ export class CurationStore {
|
|
|
93
97
|
const schema = this.#db.prepare("SELECT sql FROM sqlite_master WHERE name = 'maintenance_tasks'")
|
|
94
98
|
.get();
|
|
95
99
|
if (!schema.sql.includes("'quality_review'")) {
|
|
96
|
-
this.#db.exec("
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
DROP TABLE maintenance_tasks;
|
|
102
|
-
ALTER TABLE maintenance_tasks_quality RENAME TO maintenance_tasks;`);
|
|
103
|
-
this.#db.exec("COMMIT");
|
|
104
|
-
}
|
|
105
|
-
catch (error) {
|
|
106
|
-
this.#db.exec("ROLLBACK");
|
|
107
|
-
throw error;
|
|
108
|
-
}
|
|
100
|
+
this.#db.exec(schema.sql.replace("maintenance_tasks", "maintenance_tasks_quality")
|
|
101
|
+
.replace("'exact_duplicate'", "'exact_duplicate', 'quality_review'"));
|
|
102
|
+
this.#db.exec(`INSERT INTO maintenance_tasks_quality SELECT * FROM maintenance_tasks;
|
|
103
|
+
DROP TABLE maintenance_tasks;
|
|
104
|
+
ALTER TABLE maintenance_tasks_quality RENAME TO maintenance_tasks;`);
|
|
109
105
|
}
|
|
110
106
|
this.#db.exec(`
|
|
111
107
|
CREATE INDEX IF NOT EXISTS maintenance_tasks_status_created
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
export declare const MEMORY_DATABASE = "unblock-memory.sqlite";
|
|
3
|
+
/** Separate domain stores share settings, not a monolithic data-access API. */
|
|
4
|
+
export declare function openMemoryDatabase(path: string): DatabaseSync;
|
|
5
|
+
/** File existence no longer tells us which feature has initialized its tables. */
|
|
6
|
+
export declare function hasMemoryTable(path: string, table: string): boolean;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { closeSync, constants, existsSync, fchmodSync, lstatSync, mkdirSync, openSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
export const MEMORY_DATABASE = "unblock-memory.sqlite";
|
|
5
|
+
const legacyStores = [
|
|
6
|
+
{ file: "curation.sqlite", component: "curation", tables: [
|
|
7
|
+
"temporal_annotations", "maintenance_tasks", "quality_judgments",
|
|
8
|
+
] },
|
|
9
|
+
{ file: "people.sqlite", component: "people", tables: [
|
|
10
|
+
"companies", "people", "person_identities", "person_dossiers", "people_todos",
|
|
11
|
+
"person_whisper_receipts", "person_evidence_receipts", "person_dossier_changes", "person_primer_judgments",
|
|
12
|
+
] },
|
|
13
|
+
{ file: "response-audit.sqlite", component: "responses", tables: [
|
|
14
|
+
"response_lease", "response_results", "response_scans", "response_checkpoints", "response_cursors",
|
|
15
|
+
"response_schedule", "response_stages", "response_stage_links", "response_review_tasks",
|
|
16
|
+
"response_review_evidence", "response_review_decisions", "response_review_versions", "response_annotations",
|
|
17
|
+
] },
|
|
18
|
+
];
|
|
19
|
+
function identifier(name) { return `"${name.replaceAll('"', '""')}"`; }
|
|
20
|
+
function requireRegularFile(path) {
|
|
21
|
+
const file = lstatSync(path, { throwIfNoEntry: false });
|
|
22
|
+
if (file && !file.isFile())
|
|
23
|
+
throw new Error("Memory database must be a regular file, not a symlink");
|
|
24
|
+
}
|
|
25
|
+
/** Separate domain stores share settings, not a monolithic data-access API. */
|
|
26
|
+
export function openMemoryDatabase(path) {
|
|
27
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
28
|
+
requireRegularFile(path);
|
|
29
|
+
const descriptor = openSync(path, constants.O_CREAT | constants.O_APPEND | constants.O_WRONLY | constants.O_NOFOLLOW, 0o600);
|
|
30
|
+
try {
|
|
31
|
+
fchmodSync(descriptor, 0o600);
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
closeSync(descriptor);
|
|
35
|
+
}
|
|
36
|
+
const db = new DatabaseSync(path);
|
|
37
|
+
try {
|
|
38
|
+
db.exec(`PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;
|
|
39
|
+
PRAGMA trusted_schema=OFF;
|
|
40
|
+
CREATE TABLE IF NOT EXISTS memory_schema (component TEXT PRIMARY KEY, version INTEGER NOT NULL) STRICT;`);
|
|
41
|
+
// Explicit standalone store paths remain useful to tests and offline tools.
|
|
42
|
+
// Only the production filename opts into sibling-file consolidation.
|
|
43
|
+
if (basename(path) === MEMORY_DATABASE)
|
|
44
|
+
consolidate(db, dirname(path));
|
|
45
|
+
return db;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
db.close();
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function consolidate(db, directory) {
|
|
53
|
+
const completed = () => {
|
|
54
|
+
const version = db.prepare("SELECT version FROM memory_schema WHERE component='storage'").get()?.version;
|
|
55
|
+
if (version !== undefined && version !== 1)
|
|
56
|
+
throw new Error("Unsupported durable storage version");
|
|
57
|
+
return version === 1;
|
|
58
|
+
};
|
|
59
|
+
if (completed())
|
|
60
|
+
return;
|
|
61
|
+
const sources = legacyStores.filter(source => lstatSync(join(directory, source.file), { throwIfNoEntry: false }));
|
|
62
|
+
const attached = [];
|
|
63
|
+
try {
|
|
64
|
+
for (const source of sources) {
|
|
65
|
+
const path = join(directory, source.file);
|
|
66
|
+
requireRegularFile(path);
|
|
67
|
+
const alias = `legacy_${source.component}`;
|
|
68
|
+
db.prepare(`ATTACH DATABASE ? AS ${identifier(alias)}`).run(path);
|
|
69
|
+
attached.push(alias);
|
|
70
|
+
}
|
|
71
|
+
// Attached legacy writer locks cover the entire snapshot/copy/verification.
|
|
72
|
+
// They cannot stop old binaries writing again later: upgrade with writers stopped.
|
|
73
|
+
db.exec("PRAGMA foreign_keys=OFF; BEGIN IMMEDIATE");
|
|
74
|
+
try {
|
|
75
|
+
if (!completed()) {
|
|
76
|
+
const existing = db.prepare("SELECT name FROM main.sqlite_schema WHERE type='table' AND name!='memory_schema'").get();
|
|
77
|
+
if (existing)
|
|
78
|
+
throw new Error("Unmarked durable database is not empty; refusing to overwrite it");
|
|
79
|
+
for (const source of sources) {
|
|
80
|
+
const schema = identifier(`legacy_${source.component}`);
|
|
81
|
+
const version = Number(db.prepare(`PRAGMA ${schema}.user_version`).get()?.user_version);
|
|
82
|
+
if (source.component === "people" ? version < 0 || version > 4 : version !== 0) {
|
|
83
|
+
throw new Error(`Unsupported legacy ${source.component} schema version`);
|
|
84
|
+
}
|
|
85
|
+
const integrity = db.prepare(`PRAGMA ${schema}.integrity_check`).all();
|
|
86
|
+
if (integrity.length !== 1 || integrity[0]?.integrity_check !== "ok")
|
|
87
|
+
throw new Error(`Invalid legacy ${source.component} database`);
|
|
88
|
+
const objects = db.prepare(`SELECT type,name,tbl_name,sql FROM ${schema}.sqlite_schema WHERE name NOT LIKE 'sqlite_%'`).all();
|
|
89
|
+
if (source.component === "people" && version === 0 && objects.length)
|
|
90
|
+
throw new Error("Unversioned legacy people schema");
|
|
91
|
+
for (const object of objects) {
|
|
92
|
+
if ((object.type !== "table" && object.type !== "index") || typeof object.sql !== "string" ||
|
|
93
|
+
!source.tables.some(name => name === object.tbl_name) || /CREATE\s+VIRTUAL\s+TABLE/i.test(object.sql)) {
|
|
94
|
+
throw new Error(`Unsupported legacy ${source.component} schema object`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const tables = objects.filter(object => object.type === "table");
|
|
98
|
+
if (source.component === "people" && version > 0) {
|
|
99
|
+
const required = ["companies", "people", "person_identities", "person_dossiers", "people_todos",
|
|
100
|
+
...(version >= 2 ? ["person_whisper_receipts"] : []),
|
|
101
|
+
...(version === 2 ? ["person_evidence_receipts"] : []),
|
|
102
|
+
...(version === 4 ? ["person_dossier_changes"] : [])];
|
|
103
|
+
if (required.some(name => !tables.some(table => table.name === name)))
|
|
104
|
+
throw new Error("Incomplete legacy people schema");
|
|
105
|
+
}
|
|
106
|
+
for (const table of tables)
|
|
107
|
+
db.exec(String(table.sql));
|
|
108
|
+
for (const table of tables) {
|
|
109
|
+
const name = identifier(String(table.name));
|
|
110
|
+
db.exec(`INSERT INTO main.${name} SELECT * FROM ${schema}.${name}`);
|
|
111
|
+
const counts = db.prepare(`SELECT (SELECT count(*) FROM main.${name}) AS actual,
|
|
112
|
+
(SELECT count(*) FROM ${schema}.${name}) AS expected`).get();
|
|
113
|
+
const different = db.prepare(`SELECT * FROM ${schema}.${name} EXCEPT SELECT * FROM main.${name}`).get();
|
|
114
|
+
const extra = db.prepare(`SELECT * FROM main.${name} EXCEPT SELECT * FROM ${schema}.${name}`).get();
|
|
115
|
+
if (counts?.actual !== counts?.expected || different || extra)
|
|
116
|
+
throw new Error(`Legacy ${source.component} copy verification failed`);
|
|
117
|
+
}
|
|
118
|
+
for (const index of objects.filter(object => object.type === "index"))
|
|
119
|
+
db.exec(String(index.sql));
|
|
120
|
+
if (source.component === "people")
|
|
121
|
+
db.prepare("INSERT INTO memory_schema VALUES ('people',?)").run(version);
|
|
122
|
+
}
|
|
123
|
+
if (db.prepare("PRAGMA main.foreign_key_check").get())
|
|
124
|
+
throw new Error("Migrated memory has broken foreign keys");
|
|
125
|
+
const integrity = db.prepare("PRAGMA main.integrity_check").all();
|
|
126
|
+
if (integrity.length !== 1 || integrity[0]?.integrity_check !== "ok")
|
|
127
|
+
throw new Error("Migrated memory integrity check failed");
|
|
128
|
+
db.prepare("INSERT INTO memory_schema VALUES ('storage',1)").run();
|
|
129
|
+
}
|
|
130
|
+
db.exec("COMMIT");
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
db.exec("ROLLBACK");
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
finally {
|
|
137
|
+
db.exec("PRAGMA foreign_keys=ON");
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
for (const alias of attached.reverse())
|
|
142
|
+
db.exec(`DETACH DATABASE ${identifier(alias)}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** File existence no longer tells us which feature has initialized its tables. */
|
|
146
|
+
export function hasMemoryTable(path, table) {
|
|
147
|
+
if (!existsSync(path) && !(basename(path) === MEMORY_DATABASE &&
|
|
148
|
+
legacyStores.some(source => existsSync(join(dirname(path), source.file)))))
|
|
149
|
+
return false;
|
|
150
|
+
const db = openMemoryDatabase(path);
|
|
151
|
+
try {
|
|
152
|
+
return Boolean(db.prepare("SELECT 1 FROM sqlite_schema WHERE type='table' AND name=?").get(table));
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
db.close();
|
|
156
|
+
}
|
|
157
|
+
}
|
package/dist/src/people-store.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { MEMORY_DATABASE, openMemoryDatabase } from "./memory-database.js";
|
|
6
5
|
import { resolveStateDir } from "openclaw/plugin-sdk/memory-core-host-engine-foundation";
|
|
7
6
|
import { normalizeAgentIdStrict } from "openclaw/plugin-sdk/routing";
|
|
8
7
|
import { Type } from "typebox";
|
|
@@ -138,13 +137,10 @@ export class PeopleStore {
|
|
|
138
137
|
#maxOpenTodos;
|
|
139
138
|
#maxBlurbChars;
|
|
140
139
|
constructor(path, options) {
|
|
141
|
-
|
|
142
|
-
this.#db = new DatabaseSync(path);
|
|
140
|
+
this.#db = openMemoryDatabase(path);
|
|
143
141
|
this.#maxOpenTodos = options.maxOpenTodos;
|
|
144
142
|
this.#maxBlurbChars = options.maxBlurbChars;
|
|
145
143
|
try {
|
|
146
|
-
chmodSync(path, 0o600);
|
|
147
|
-
this.#db.exec("PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000; PRAGMA foreign_keys = ON");
|
|
148
144
|
this.#migrate();
|
|
149
145
|
}
|
|
150
146
|
catch (error) {
|
|
@@ -740,23 +736,24 @@ export class PeopleStore {
|
|
|
740
736
|
}
|
|
741
737
|
}
|
|
742
738
|
#migrate() {
|
|
743
|
-
const current = this.#db.prepare("PRAGMA user_version").get();
|
|
744
|
-
if (current.user_version === 4)
|
|
745
|
-
return;
|
|
746
|
-
if (current.user_version !== 0 &&
|
|
747
|
-
current.user_version !== 1 &&
|
|
748
|
-
current.user_version !== 2 &&
|
|
749
|
-
current.user_version !== 3) {
|
|
750
|
-
throw new Error(`unsupported PeopleSQL schema version: ${current.user_version}`);
|
|
751
|
-
}
|
|
752
739
|
this.#db.exec("BEGIN IMMEDIATE");
|
|
753
740
|
try {
|
|
754
|
-
|
|
741
|
+
// Re-read under the write lock so concurrent first opens cannot both migrate.
|
|
742
|
+
const version = this.#db.prepare("SELECT version FROM memory_schema WHERE component='people'").get()?.version
|
|
743
|
+
?? this.#db.prepare("PRAGMA user_version").get()?.user_version ?? 0;
|
|
744
|
+
if (![0, 1, 2, 3, 4].includes(Number(version)))
|
|
745
|
+
throw new Error(`unsupported PeopleSQL schema version: ${version}`);
|
|
746
|
+
if (version === 4) {
|
|
747
|
+
this.#db.prepare("INSERT OR IGNORE INTO memory_schema VALUES ('people',4)").run();
|
|
748
|
+
this.#db.exec("COMMIT");
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
if (version === 2) {
|
|
755
752
|
this.#db.exec(`
|
|
756
753
|
DROP TABLE person_evidence_receipts;
|
|
757
754
|
`);
|
|
758
755
|
}
|
|
759
|
-
else if (
|
|
756
|
+
else if (version === 1) {
|
|
760
757
|
this.#db.exec(`
|
|
761
758
|
DROP INDEX people_policy_seen;
|
|
762
759
|
ALTER TABLE people DROP COLUMN refinement_enabled;
|
|
@@ -772,7 +769,7 @@ export class PeopleStore {
|
|
|
772
769
|
) STRICT;
|
|
773
770
|
`);
|
|
774
771
|
}
|
|
775
|
-
else if (
|
|
772
|
+
else if (version === 0) {
|
|
776
773
|
this.#db.exec(`
|
|
777
774
|
CREATE TABLE companies (
|
|
778
775
|
id TEXT PRIMARY KEY,
|
|
@@ -859,7 +856,8 @@ export class PeopleStore {
|
|
|
859
856
|
|
|
860
857
|
CREATE INDEX person_dossier_changes_person_changed
|
|
861
858
|
ON person_dossier_changes(person_id, changed_at DESC);
|
|
862
|
-
|
|
859
|
+
INSERT INTO memory_schema VALUES ('people',4)
|
|
860
|
+
ON CONFLICT(component) DO UPDATE SET version=excluded.version;
|
|
863
861
|
`);
|
|
864
862
|
this.#db.exec("COMMIT");
|
|
865
863
|
}
|
|
@@ -884,7 +882,7 @@ export class PeopleStores {
|
|
|
884
882
|
const canonicalAgentId = normalized.value;
|
|
885
883
|
let store = this.#stores.get(canonicalAgentId);
|
|
886
884
|
if (!store) {
|
|
887
|
-
store = new PeopleStore(join(this.#stateRoot, "agents", canonicalAgentId, "unblock-memory",
|
|
885
|
+
store = new PeopleStore(join(this.#stateRoot, "agents", canonicalAgentId, "unblock-memory", MEMORY_DATABASE), this.#options);
|
|
888
886
|
this.#stores.set(canonicalAgentId, store);
|
|
889
887
|
}
|
|
890
888
|
return store;
|
|
@@ -35,7 +35,7 @@ export async function auditResponses(options) {
|
|
|
35
35
|
return { status: "unavailable", reason: "TypeSafe API key not configured" };
|
|
36
36
|
}
|
|
37
37
|
let reader, store, lease;
|
|
38
|
-
|
|
38
|
+
let people;
|
|
39
39
|
const now = Date.now();
|
|
40
40
|
const coverage = { sessions: 0, sessionLimitReached: false, sessionsOverBudget: 0, completedResponses: 0,
|
|
41
41
|
reconciledSessions: 0, reconciliationDeferred: 0,
|
|
@@ -51,6 +51,8 @@ export async function auditResponses(options) {
|
|
|
51
51
|
if (!lease)
|
|
52
52
|
return { status: "already_running" };
|
|
53
53
|
}
|
|
54
|
+
// The first store open may have imported people into the shared database.
|
|
55
|
+
people = new ResponsePeople(options.peoplePath);
|
|
54
56
|
reader = new ResponseTranscriptReader(options.databasePath, agentId);
|
|
55
57
|
const since = now - config.responseAudit.lookbackDays * 86400_000;
|
|
56
58
|
store?.reviews.refresh(cohort, since);
|
|
@@ -184,7 +186,7 @@ export async function auditResponses(options) {
|
|
|
184
186
|
if (lease)
|
|
185
187
|
store?.release(lease);
|
|
186
188
|
store?.close();
|
|
187
|
-
people
|
|
189
|
+
people?.close();
|
|
188
190
|
}
|
|
189
191
|
}
|
|
190
192
|
function references(e) {
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
2
3
|
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
import { MEMORY_DATABASE } from "./memory-database.js";
|
|
3
5
|
/** Identity is trusted metadata, never inferred from names or transcript text. */
|
|
4
6
|
export class ResponsePeople {
|
|
5
7
|
#db;
|
|
6
8
|
constructor(path) {
|
|
9
|
+
// Dry-run audits must not migrate or create stores just to resolve identities.
|
|
10
|
+
if (path && basename(path) === MEMORY_DATABASE && !existsSync(path))
|
|
11
|
+
path = join(dirname(path), "people.sqlite");
|
|
7
12
|
if (!path || !existsSync(path))
|
|
8
13
|
return;
|
|
9
14
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { hasMemoryTable, MEMORY_DATABASE } from "./memory-database.js";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { resolveAgentDir, resolveAgentWorkspaceDir, resolveStateDir } from "openclaw/plugin-sdk/memory-core-host-engine-foundation";
|
|
4
4
|
import { listAgentIds } from "openclaw/plugin-sdk/agent-runtime";
|
|
@@ -22,8 +22,8 @@ export function registerResponseAudit(api, config) {
|
|
|
22
22
|
const agentId = normalized.value;
|
|
23
23
|
const state = join(resolveStateDir(), "agents", agentId, "unblock-memory");
|
|
24
24
|
return { agentId, config, databasePath: join(resolveAgentDir(cfg, agentId), "openclaw-agent.sqlite"),
|
|
25
|
-
storePath: join(state,
|
|
26
|
-
peoplePath: join(state,
|
|
25
|
+
storePath: join(state, MEMORY_DATABASE), indexPath: join(state, "index.sqlite"),
|
|
26
|
+
peoplePath: join(state, MEMORY_DATABASE),
|
|
27
27
|
sources: resolveSources(resolveAgentWorkspaceDir(cfg, agentId), config.corpora.filter(c => c.kind === "files")
|
|
28
28
|
.filter(c => config.responseAudit.memoryCorpora.includes(c.name))) };
|
|
29
29
|
};
|
|
@@ -47,7 +47,7 @@ export function registerResponseAudit(api, config) {
|
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
const { storePath } = options(cfg, opts.agent);
|
|
50
|
-
if (!
|
|
50
|
+
if (!hasMemoryTable(storePath, "response_results")) {
|
|
51
51
|
console.log(JSON.stringify({ status: "not_run" }));
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
@@ -68,7 +68,7 @@ export function registerResponseAudit(api, config) {
|
|
|
68
68
|
if (!config.responseAudit.enabled)
|
|
69
69
|
throw new Error("Response audit is disabled");
|
|
70
70
|
const { storePath } = options(cfg, agent);
|
|
71
|
-
if (!
|
|
71
|
+
if (!hasMemoryTable(storePath, "response_results"))
|
|
72
72
|
throw new Error("Response audit has not run");
|
|
73
73
|
const store = new ResponseAuditStore(storePath);
|
|
74
74
|
try {
|
|
@@ -31,7 +31,7 @@ export type ResponseReportOptions = {
|
|
|
31
31
|
taskType?: string;
|
|
32
32
|
agentModel?: string;
|
|
33
33
|
};
|
|
34
|
-
/**
|
|
34
|
+
/** Operator-only tables: not a memory corpus and never injected into agent prompts. */
|
|
35
35
|
export declare class ResponseAuditStore {
|
|
36
36
|
#private;
|
|
37
37
|
readonly reviews: ResponseReviews;
|
|
@@ -1,40 +1,47 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import { dirname } from "node:path";
|
|
4
|
-
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { openMemoryDatabase } from "./memory-database.js";
|
|
5
3
|
import { responseOutcome, RESPONSE_REPORT_VERSION } from "./response-outcome.js";
|
|
6
4
|
import { ResponsePeople } from "./response-identity.js";
|
|
7
5
|
import { ResponseReviews, RESPONSE_REVIEW_POLICY } from "./response-reviews.js";
|
|
8
|
-
/**
|
|
6
|
+
/** Operator-only tables: not a memory corpus and never injected into agent prompts. */
|
|
9
7
|
export class ResponseAuditStore {
|
|
10
8
|
#db;
|
|
11
9
|
reviews;
|
|
12
10
|
constructor(path) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
11
|
+
this.#db = openMemoryDatabase(path);
|
|
12
|
+
try {
|
|
13
|
+
this.#db.exec("BEGIN IMMEDIATE");
|
|
14
|
+
const version = this.#db.prepare("SELECT version FROM memory_schema WHERE component='responses'").get()?.version;
|
|
15
|
+
if (version !== undefined && version !== 1)
|
|
16
|
+
throw new Error("Unsupported response audit schema version");
|
|
17
|
+
this.#db.exec(`
|
|
18
|
+
CREATE TABLE IF NOT EXISTS response_lease (id INTEGER PRIMARY KEY CHECK(id=1), token TEXT, expires INTEGER);
|
|
19
|
+
CREATE TABLE IF NOT EXISTS response_results (
|
|
20
|
+
cohort TEXT, id TEXT, session_id TEXT NOT NULL, input_hash TEXT NOT NULL,
|
|
21
|
+
episode_at INTEGER NOT NULL, active INTEGER NOT NULL, status TEXT NOT NULL,
|
|
22
|
+
attempts INTEGER NOT NULL DEFAULT 0, attempted_at INTEGER, assessed_at INTEGER, result TEXT,
|
|
23
|
+
PRIMARY KEY(cohort,id));
|
|
24
|
+
CREATE TABLE IF NOT EXISTS response_scans (cohort TEXT PRIMARY KEY, observed_at INTEGER, coverage TEXT);
|
|
25
|
+
CREATE INDEX IF NOT EXISTS response_results_time ON response_results(cohort,episode_at);`);
|
|
26
|
+
this.#db.exec(`CREATE TABLE IF NOT EXISTS response_checkpoints (
|
|
27
|
+
cohort TEXT NOT NULL, session_id TEXT NOT NULL, revision TEXT NOT NULL, coverage TEXT NOT NULL,
|
|
28
|
+
PRIMARY KEY(cohort,session_id));
|
|
29
|
+
CREATE TABLE IF NOT EXISTS response_cursors (cohort TEXT PRIMARY KEY,cursor TEXT NOT NULL);
|
|
30
|
+
CREATE TABLE IF NOT EXISTS response_schedule (
|
|
31
|
+
id INTEGER PRIMARY KEY CHECK(id=1), interval_ms INTEGER NOT NULL, next_due INTEGER NOT NULL);
|
|
32
|
+
CREATE TABLE IF NOT EXISTS response_stages (
|
|
33
|
+
key TEXT PRIMARY KEY, stage TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending',
|
|
34
|
+
attempts INTEGER NOT NULL DEFAULT 0, attempted_at INTEGER, assessed_at INTEGER, result TEXT);
|
|
35
|
+
CREATE TABLE IF NOT EXISTS response_stage_links (
|
|
36
|
+
cohort TEXT NOT NULL, episode_id TEXT NOT NULL, input_hash TEXT NOT NULL, stage TEXT NOT NULL, key TEXT NOT NULL,
|
|
37
|
+
PRIMARY KEY(cohort,episode_id,stage));`);
|
|
38
|
+
this.reviews = new ResponseReviews(this.#db);
|
|
39
|
+
this.#db.exec("INSERT OR IGNORE INTO memory_schema VALUES ('responses',1); COMMIT");
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
this.#db.close();
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
38
45
|
}
|
|
39
46
|
/** Claim one bounded scheduled attempt, never replay every missed interval. */
|
|
40
47
|
claimScheduled(now, intervalMs) {
|
package/dist/src/runtime.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { MEMORY_DATABASE } from "./memory-database.js";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { resolveAgentDir, resolveAgentWorkspaceDir, resolveStateDir, } from "openclaw/plugin-sdk/memory-core-host-engine-foundation";
|
|
5
6
|
import { listAgentIds, resolveAgentIdentity } from "openclaw/plugin-sdk/agent-runtime";
|
|
@@ -240,7 +241,7 @@ export class QmdMemoryRuntime {
|
|
|
240
241
|
const manager = new QmdMemoryManager({
|
|
241
242
|
workspaceDir,
|
|
242
243
|
dbPath: join(stateDir, "index.sqlite"),
|
|
243
|
-
curationPath: join(stateDir,
|
|
244
|
+
curationPath: join(stateDir, MEMORY_DATABASE),
|
|
244
245
|
sources,
|
|
245
246
|
keepModelsWarm: this.#keepEmbeddingModelWarm,
|
|
246
247
|
analysisExecutable: this.#analysisExecutable,
|
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.20",
|
|
5
5
|
"description": "Indexes, retrieves, and analyzes configured workspace memory with existing QMD vectors.",
|
|
6
6
|
"kind": "memory",
|
|
7
7
|
"activation": { "onStartup": true },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unblocklabs/unblock-memory",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.20",
|
|
4
4
|
"description": "Workspace-native memory for OpenClaw, powered by QMD",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"preflight": "npm run knip && npm run build && npm run typecheck && npm test && npm run plugin:inspect && npm run plugin:inspect:runtime && npm pack --dry-run"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@unblocklabs/qmd": "https://github.com/unblocklabs-ai/qmd/releases/download/v2.
|
|
38
|
+
"@unblocklabs/qmd": "https://github.com/unblocklabs-ai/qmd/releases/download/v2.10.0/unblocklabs-qmd-2.10.0.tgz",
|
|
39
39
|
"chokidar": "5.0.0",
|
|
40
40
|
"picomatch": "^4.0.5",
|
|
41
41
|
"typebox": "1.3.6"
|