@geravant/sinain 1.9.0 → 1.10.1
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/.env.example +17 -14
- package/HEARTBEAT.md +1 -1
- package/README.md +4 -7
- package/index.ts +1 -3
- package/package.json +1 -1
- package/sense_client/ocr.py +6 -3
- package/sinain-agent/CLAUDE.md +50 -1
- package/sinain-agent/run.sh +18 -8
- package/sinain-core/src/agent/analyzer.ts +31 -56
- package/sinain-core/src/agent/loop.ts +11 -10
- package/sinain-core/src/config.ts +17 -14
- package/sinain-core/src/index.ts +297 -26
- package/sinain-core/src/learning/local-curation.ts +373 -0
- package/sinain-core/src/overlay/commands.ts +9 -0
- package/sinain-core/src/overlay/ws-handler.ts +3 -0
- package/sinain-core/src/server.ts +197 -0
- package/sinain-core/src/types.ts +13 -10
- package/sinain-knowledge/curation/engine.ts +0 -17
- package/sinain-knowledge/protocol/heartbeat.md +1 -1
- package/sinain-mcp-server/index.ts +38 -24
- package/sinain-memory/__pycache__/common.cpython-312.pyc +0 -0
- package/sinain-memory/__pycache__/knowledge_integrator.cpython-312.pyc +0 -0
- package/sinain-memory/__pycache__/session_distiller.cpython-312.pyc +0 -0
- package/sinain-memory/__pycache__/triplestore.cpython-312.pyc +0 -0
- package/sinain-memory/eval/retrieval_benchmark.jsonl +12 -0
- package/sinain-memory/eval/retrieval_evaluator.py +186 -0
- package/sinain-memory/graph_query.py +34 -1
- package/sinain-memory/knowledge_integrator.py +54 -0
- package/sinain-memory/triplestore.py +76 -5
- package/sinain-agent/.env.example +0 -23
- package/sinain-memory/git_backup.sh +0 -19
package/sinain-core/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { Tracer } from "./trace/tracer.js";
|
|
|
16
16
|
import { TraceStore } from "./trace/trace-store.js";
|
|
17
17
|
import { FeedbackStore } from "./learning/feedback-store.js";
|
|
18
18
|
import { SignalCollector } from "./learning/signal-collector.js";
|
|
19
|
+
import { LocalCurationService } from "./learning/local-curation.js";
|
|
19
20
|
import { createAppServer } from "./server.js";
|
|
20
21
|
import { Profiler } from "./profiler.js";
|
|
21
22
|
import { CostTracker } from "./cost/tracker.js";
|
|
@@ -32,6 +33,266 @@ function resolveWorkspace(): string {
|
|
|
32
33
|
return raw.startsWith("~") ? raw.replace("~", process.env.HOME || "") : raw;
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
/** Resolve the local memory directory (independent of OpenClaw workspace). */
|
|
37
|
+
function resolveLocalMemoryDir(): string {
|
|
38
|
+
const raw = process.env.SINAIN_MEMORY_DIR || `${process.env.HOME}/.sinain/memory`;
|
|
39
|
+
return raw.startsWith("~") ? raw.replace("~", process.env.HOME || "") : raw;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Query knowledge facts from both local and workspace databases.
|
|
44
|
+
* Checks local (~/.sinain/memory) first, then workspace (~/.openclaw/workspace/memory).
|
|
45
|
+
* Merges results, deduplicates, returns up to maxFacts.
|
|
46
|
+
*/
|
|
47
|
+
async function queryKnowledgeFactsMulti(entities: string[], maxFacts: number): Promise<string> {
|
|
48
|
+
const { execFileSync } = await import("node:child_process");
|
|
49
|
+
const { resolve } = await import("node:path");
|
|
50
|
+
const { dirname } = await import("node:path");
|
|
51
|
+
const { fileURLToPath } = await import("node:url");
|
|
52
|
+
|
|
53
|
+
// Candidate database paths (local first, then workspace)
|
|
54
|
+
const localDir = resolveLocalMemoryDir();
|
|
55
|
+
const workspaceDir = `${resolveWorkspace()}/memory`;
|
|
56
|
+
const dbPaths = [
|
|
57
|
+
`${localDir}/knowledge-graph.db`,
|
|
58
|
+
`${workspaceDir}/knowledge-graph.db`,
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
// Candidate script paths
|
|
62
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
63
|
+
const scriptCandidates = [
|
|
64
|
+
resolve(__dir, "..", "..", "sinain-hud-plugin", "sinain-memory", "graph_query.py"),
|
|
65
|
+
resolve(__dir, "..", "sinain-memory", "graph_query.py"),
|
|
66
|
+
`${resolveWorkspace()}/sinain-memory/graph_query.py`,
|
|
67
|
+
];
|
|
68
|
+
const scriptPath = scriptCandidates.find(p => existsSync(p)) || scriptCandidates[0];
|
|
69
|
+
|
|
70
|
+
const results: string[] = [];
|
|
71
|
+
for (const dbPath of dbPaths) {
|
|
72
|
+
if (!existsSync(dbPath)) continue;
|
|
73
|
+
try {
|
|
74
|
+
const args = [scriptPath, "--db", dbPath, "--max-facts", String(maxFacts), "--format", "text"];
|
|
75
|
+
if (entities.length > 0) args.push("--entities", JSON.stringify(entities));
|
|
76
|
+
const out = execFileSync("python3", args, { timeout: 5000, encoding: "utf-8" }).trim();
|
|
77
|
+
if (out) results.push(out);
|
|
78
|
+
} catch { /* skip failed db */ }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (results.length === 0) return "";
|
|
82
|
+
if (results.length === 1) return results[0];
|
|
83
|
+
|
|
84
|
+
// Merge and deduplicate lines from both sources
|
|
85
|
+
const seen = new Set<string>();
|
|
86
|
+
const merged: string[] = [];
|
|
87
|
+
for (const block of results) {
|
|
88
|
+
for (const line of block.split("\n")) {
|
|
89
|
+
const key = line.replace(/\(confidence:.*$/, "").trim();
|
|
90
|
+
if (key && !seen.has(key)) {
|
|
91
|
+
seen.add(key);
|
|
92
|
+
merged.push(line);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return merged.slice(0, maxFacts).join("\n");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** List all entities from both local and workspace knowledge graphs. */
|
|
100
|
+
async function listKnowledgeEntitiesMulti(max: number): Promise<string> {
|
|
101
|
+
const { execFileSync } = await import("node:child_process");
|
|
102
|
+
const { resolve, dirname } = await import("node:path");
|
|
103
|
+
const { fileURLToPath } = await import("node:url");
|
|
104
|
+
|
|
105
|
+
const localDir = resolveLocalMemoryDir();
|
|
106
|
+
const workspaceDir = `${resolveWorkspace()}/memory`;
|
|
107
|
+
const dbPaths = [
|
|
108
|
+
`${localDir}/knowledge-graph.db`,
|
|
109
|
+
`${workspaceDir}/knowledge-graph.db`,
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
113
|
+
const scriptCandidates = [
|
|
114
|
+
resolve(__dir, "..", "..", "sinain-hud-plugin", "sinain-memory", "graph_query.py"),
|
|
115
|
+
resolve(__dir, "..", "sinain-memory", "graph_query.py"),
|
|
116
|
+
`${resolveWorkspace()}/sinain-memory/graph_query.py`,
|
|
117
|
+
];
|
|
118
|
+
const scriptPath = scriptCandidates.find(p => existsSync(p)) || scriptCandidates[0];
|
|
119
|
+
|
|
120
|
+
const allFacts: any[] = [];
|
|
121
|
+
for (const dbPath of dbPaths) {
|
|
122
|
+
if (!existsSync(dbPath)) continue;
|
|
123
|
+
try {
|
|
124
|
+
const out = execFileSync("python3", [
|
|
125
|
+
scriptPath, "--db", dbPath, "--top", String(max), "--format", "json",
|
|
126
|
+
], { timeout: 5000, encoding: "utf-8" });
|
|
127
|
+
const parsed = JSON.parse(out);
|
|
128
|
+
if (parsed.facts) allFacts.push(...parsed.facts);
|
|
129
|
+
} catch { /* skip */ }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Deduplicate by entityId, merge
|
|
133
|
+
const seen = new Set<string>();
|
|
134
|
+
const unique = allFacts.filter(f => {
|
|
135
|
+
const id = f.entityId || "";
|
|
136
|
+
if (seen.has(id)) return false;
|
|
137
|
+
seen.add(id);
|
|
138
|
+
return true;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return JSON.stringify(unique.slice(0, max));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Export knowledge facts as a portable JSON module. */
|
|
145
|
+
async function exportKnowledgeMulti(domain: string | null, max: number): Promise<string> {
|
|
146
|
+
const { execFileSync } = await import("node:child_process");
|
|
147
|
+
const { resolve, dirname } = await import("node:path");
|
|
148
|
+
const { fileURLToPath } = await import("node:url");
|
|
149
|
+
|
|
150
|
+
const localDir = resolveLocalMemoryDir();
|
|
151
|
+
const workspaceDir = `${resolveWorkspace()}/memory`;
|
|
152
|
+
const dbPaths = [
|
|
153
|
+
`${localDir}/knowledge-graph.db`,
|
|
154
|
+
`${workspaceDir}/knowledge-graph.db`,
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
158
|
+
const scriptCandidates = [
|
|
159
|
+
resolve(__dir, "..", "..", "sinain-hud-plugin", "sinain-memory", "graph_query.py"),
|
|
160
|
+
`${resolveWorkspace()}/sinain-memory/graph_query.py`,
|
|
161
|
+
];
|
|
162
|
+
const scriptPath = scriptCandidates.find(p => existsSync(p)) || scriptCandidates[0];
|
|
163
|
+
|
|
164
|
+
const allFacts: any[] = [];
|
|
165
|
+
for (const dbPath of dbPaths) {
|
|
166
|
+
if (!existsSync(dbPath)) continue;
|
|
167
|
+
try {
|
|
168
|
+
const out = execFileSync("python3", [
|
|
169
|
+
scriptPath, "--db", dbPath, "--top", String(max), "--format", "json",
|
|
170
|
+
], { timeout: 5000, encoding: "utf-8" });
|
|
171
|
+
const parsed = JSON.parse(out);
|
|
172
|
+
if (parsed.facts) allFacts.push(...parsed.facts);
|
|
173
|
+
} catch { /* skip */ }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Deduplicate
|
|
177
|
+
const seen = new Set<string>();
|
|
178
|
+
let facts = allFacts.filter(f => {
|
|
179
|
+
const id = f.entityId || "";
|
|
180
|
+
if (seen.has(id)) return false;
|
|
181
|
+
seen.add(id);
|
|
182
|
+
return true;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// Filter by domain if specified
|
|
186
|
+
if (domain) {
|
|
187
|
+
facts = facts.filter(f => f.domain === domain);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return JSON.stringify({
|
|
191
|
+
format: "sinain-knowledge-export",
|
|
192
|
+
version: 1,
|
|
193
|
+
exportedAt: new Date().toISOString(),
|
|
194
|
+
domain: domain || "all",
|
|
195
|
+
count: facts.length,
|
|
196
|
+
facts: facts.slice(0, max),
|
|
197
|
+
}, null, 2);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Import knowledge facts from a portable JSON module into the local graph. */
|
|
201
|
+
async function importKnowledgeToLocal(data: string): Promise<string> {
|
|
202
|
+
const { execFileSync } = await import("node:child_process");
|
|
203
|
+
const { resolve, dirname } = await import("node:path");
|
|
204
|
+
const { fileURLToPath } = await import("node:url");
|
|
205
|
+
const { mkdirSync } = await import("node:fs");
|
|
206
|
+
|
|
207
|
+
let parsed: any;
|
|
208
|
+
try {
|
|
209
|
+
parsed = JSON.parse(data);
|
|
210
|
+
} catch {
|
|
211
|
+
return JSON.stringify({ ok: false, error: "Invalid JSON" });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const facts = parsed.facts || (Array.isArray(parsed) ? parsed : null);
|
|
215
|
+
if (!facts || !Array.isArray(facts) || facts.length === 0) {
|
|
216
|
+
const keys = Object.keys(parsed).join(", ");
|
|
217
|
+
return JSON.stringify({ ok: false, error: `No 'facts' array found. Expected sinain knowledge export format: {"facts":[...]}. Got keys: ${keys}` });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const localDir = resolveLocalMemoryDir();
|
|
221
|
+
mkdirSync(localDir, { recursive: true });
|
|
222
|
+
const dbPath = `${localDir}/knowledge-graph.db`;
|
|
223
|
+
|
|
224
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
225
|
+
const scriptsDir = resolve(__dir, "..", "..", "sinain-hud-plugin", "sinain-memory");
|
|
226
|
+
|
|
227
|
+
// Convert facts to graph ops for knowledge_integrator
|
|
228
|
+
const graphOps = facts.map((f: any) => ({
|
|
229
|
+
op: "assert",
|
|
230
|
+
entity: f.entity || f.entityId?.replace(/^fact:/, "").replace(/-[a-f0-9]{12}$/, "") || "unknown",
|
|
231
|
+
attribute: f.attribute || "value",
|
|
232
|
+
value: f.value || "",
|
|
233
|
+
confidence: parseFloat(f.confidence || "0.7"),
|
|
234
|
+
domain: f.domain || "",
|
|
235
|
+
}));
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
// Use triplestore directly via Python
|
|
239
|
+
const script = `
|
|
240
|
+
import json, sys
|
|
241
|
+
sys.path.insert(0, "${scriptsDir}")
|
|
242
|
+
from triplestore import TripleStore
|
|
243
|
+
import hashlib
|
|
244
|
+
|
|
245
|
+
db_path = "${dbPath}"
|
|
246
|
+
store = TripleStore(db_path)
|
|
247
|
+
ops = json.loads(sys.stdin.read())
|
|
248
|
+
stats = {"asserted": 0, "skipped": 0}
|
|
249
|
+
|
|
250
|
+
for op in ops:
|
|
251
|
+
entity = op.get("entity", "")
|
|
252
|
+
value = op.get("value", "")
|
|
253
|
+
if not entity or not value:
|
|
254
|
+
stats["skipped"] += 1
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
h = hashlib.sha256(f"{entity}:{op.get('attribute','')}:{value}".encode()).hexdigest()[:12]
|
|
258
|
+
slug = entity.replace(" ", "-").lower()[:30]
|
|
259
|
+
entity_id = f"fact:{slug}-{h}"
|
|
260
|
+
|
|
261
|
+
# Check if already exists
|
|
262
|
+
existing = store.entity(entity_id)
|
|
263
|
+
if existing:
|
|
264
|
+
stats["skipped"] += 1
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
tx = store.begin_tx("import", metadata=json.dumps({"source": "web-import"}))
|
|
268
|
+
store.assert_triple(tx, entity_id, "entity", entity)
|
|
269
|
+
store.assert_triple(tx, entity_id, "attribute", op.get("attribute", "value"))
|
|
270
|
+
store.assert_triple(tx, entity_id, "value", value)
|
|
271
|
+
store.assert_triple(tx, entity_id, "confidence", str(op.get("confidence", 0.7)))
|
|
272
|
+
store.assert_triple(tx, entity_id, "first_seen", "${new Date().toISOString()}")
|
|
273
|
+
store.assert_triple(tx, entity_id, "last_reinforced", "${new Date().toISOString()}")
|
|
274
|
+
store.assert_triple(tx, entity_id, "reinforce_count", "1")
|
|
275
|
+
if op.get("domain"):
|
|
276
|
+
store.assert_triple(tx, entity_id, "domain", op["domain"])
|
|
277
|
+
stats["asserted"] += 1
|
|
278
|
+
|
|
279
|
+
store.close()
|
|
280
|
+
print(json.dumps(stats))
|
|
281
|
+
`;
|
|
282
|
+
|
|
283
|
+
const result = execFileSync("python3", ["-c", script], {
|
|
284
|
+
input: JSON.stringify(graphOps),
|
|
285
|
+
timeout: 10_000,
|
|
286
|
+
encoding: "utf-8",
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
const stats = JSON.parse(result.trim());
|
|
290
|
+
return JSON.stringify({ ok: true, stats, imported: stats.asserted, skipped: stats.skipped });
|
|
291
|
+
} catch (err: any) {
|
|
292
|
+
return JSON.stringify({ ok: false, error: err.message?.slice(0, 200) });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
35
296
|
async function main() {
|
|
36
297
|
log(TAG, "sinain-core starting...");
|
|
37
298
|
|
|
@@ -78,6 +339,11 @@ async function main() {
|
|
|
78
339
|
? new FeedbackStore(config.learningConfig.feedbackDir, config.learningConfig.retentionDays)
|
|
79
340
|
: null;
|
|
80
341
|
|
|
342
|
+
// ── Initialize local knowledge pipeline ──
|
|
343
|
+
const localCuration = new LocalCurationService();
|
|
344
|
+
localCuration.distillPendingSession(); // Recover any session saved before a force-kill
|
|
345
|
+
localCuration.startPeriodicCuration();
|
|
346
|
+
|
|
81
347
|
// ── Initialize trait engine ──
|
|
82
348
|
const traitRoster = loadTraitRoster(config.traitConfig.configPath);
|
|
83
349
|
const traitEngine = new TraitEngine(traitRoster, config.traitConfig);
|
|
@@ -90,17 +356,7 @@ async function main() {
|
|
|
90
356
|
openclawConfig: config.openclawConfig,
|
|
91
357
|
profiler,
|
|
92
358
|
feedbackStore: feedbackStore ?? undefined,
|
|
93
|
-
queryKnowledgeFacts:
|
|
94
|
-
const workspace = resolveWorkspace();
|
|
95
|
-
const dbPath = `${workspace}/memory/knowledge-graph.db`;
|
|
96
|
-
const scriptPath = `${workspace}/sinain-memory/graph_query.py`;
|
|
97
|
-
try {
|
|
98
|
-
const { execFileSync } = await import("node:child_process");
|
|
99
|
-
const args = [scriptPath, "--db", dbPath, "--max-facts", String(maxFacts), "--format", "text"];
|
|
100
|
-
if (entities.length > 0) args.push("--entities", JSON.stringify(entities));
|
|
101
|
-
return execFileSync("python3", args, { timeout: 5000, encoding: "utf-8" });
|
|
102
|
-
} catch { return ""; }
|
|
103
|
-
},
|
|
359
|
+
queryKnowledgeFacts: queryKnowledgeFactsMulti,
|
|
104
360
|
});
|
|
105
361
|
|
|
106
362
|
// ── Initialize agent loop (event-driven) ──
|
|
@@ -420,24 +676,19 @@ async function main() {
|
|
|
420
676
|
getEscalationPending: () => escalator.getPendingHttp(),
|
|
421
677
|
respondEscalation: (id: string, response: string) => escalator.respondHttp(id, response),
|
|
422
678
|
|
|
423
|
-
// Knowledge graph integration
|
|
679
|
+
// Knowledge graph integration (checks both local and workspace DBs)
|
|
424
680
|
getKnowledgeDocPath: () => {
|
|
425
|
-
|
|
426
|
-
const
|
|
427
|
-
|
|
681
|
+
// Check local first, then workspace
|
|
682
|
+
const localPath = `${resolveLocalMemoryDir()}/sinain-knowledge.md`;
|
|
683
|
+
const workspacePath = `${resolveWorkspace()}/memory/sinain-knowledge.md`;
|
|
684
|
+
try { if (existsSync(localPath)) return localPath; } catch {}
|
|
685
|
+
try { if (existsSync(workspacePath)) return workspacePath; } catch {}
|
|
428
686
|
return null;
|
|
429
687
|
},
|
|
430
|
-
queryKnowledgeFacts:
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
try {
|
|
435
|
-
const { execFileSync } = await import("node:child_process");
|
|
436
|
-
const args = [scriptPath, "--db", dbPath, "--max-facts", String(maxFacts), "--format", "text"];
|
|
437
|
-
if (entities.length > 0) args.push("--entities", JSON.stringify(entities));
|
|
438
|
-
return execFileSync("python3", args, { timeout: 5000, encoding: "utf-8" });
|
|
439
|
-
} catch { return ""; }
|
|
440
|
-
},
|
|
688
|
+
queryKnowledgeFacts: queryKnowledgeFactsMulti,
|
|
689
|
+
listKnowledgeEntities: listKnowledgeEntitiesMulti,
|
|
690
|
+
exportKnowledge: exportKnowledgeMulti,
|
|
691
|
+
importKnowledge: importKnowledgeToLocal,
|
|
441
692
|
|
|
442
693
|
// Spawn background agent task (from HUD Shift+Enter or bare agent POST /spawn)
|
|
443
694
|
onSpawnCommand: (text: string) => {
|
|
@@ -563,6 +814,26 @@ async function main() {
|
|
|
563
814
|
signalCollector?.destroy();
|
|
564
815
|
feedbackStore?.destroy();
|
|
565
816
|
traceStore?.destroy();
|
|
817
|
+
|
|
818
|
+
// Save session knowledge — write feed items to disk FIRST (instant),
|
|
819
|
+
// then attempt LLM distillation. If tsx force-kills before distillation
|
|
820
|
+
// finishes, the saved file is recovered on next startup.
|
|
821
|
+
localCuration.stop();
|
|
822
|
+
const feedItems = feedBuffer.query(0);
|
|
823
|
+
try {
|
|
824
|
+
localCuration.savePendingSession(feedItems);
|
|
825
|
+
} catch (err: any) {
|
|
826
|
+
warn(TAG, `failed to save pending session: ${err.message?.slice(0, 100)}`);
|
|
827
|
+
}
|
|
828
|
+
try {
|
|
829
|
+
if (feedItems.length >= 3) {
|
|
830
|
+
log(TAG, `distilling session (${feedItems.length} feed items)...`);
|
|
831
|
+
await localCuration.distillSession(feedItems);
|
|
832
|
+
}
|
|
833
|
+
} catch (err: any) {
|
|
834
|
+
warn(TAG, `session distillation failed (will retry on next startup): ${err.message?.slice(0, 100)}`);
|
|
835
|
+
}
|
|
836
|
+
|
|
566
837
|
await server.destroy();
|
|
567
838
|
log(TAG, "goodbye");
|
|
568
839
|
process.exit(0);
|