@geravant/sinain 1.10.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/package.json +1 -1
- package/sinain-agent/CLAUDE.md +50 -0
- package/sinain-agent/run.sh +7 -3
- package/sinain-core/src/index.ts +297 -26
- package/sinain-core/src/learning/local-curation.ts +373 -0
- package/sinain-core/src/server.ts +197 -0
- package/sinain-mcp-server/index.ts +34 -4
- 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/package.json
CHANGED
package/sinain-agent/CLAUDE.md
CHANGED
|
@@ -55,6 +55,56 @@ When responding to escalations:
|
|
|
55
55
|
4. Optionally call `sinain_get_knowledge` to review the portable knowledge document
|
|
56
56
|
5. Optionally call `sinain_get_feedback` to review recent escalation scores
|
|
57
57
|
|
|
58
|
+
## Knowledge System
|
|
59
|
+
|
|
60
|
+
Knowledge is stored in a **dual-database** architecture with two SQLite triplestore databases:
|
|
61
|
+
|
|
62
|
+
| Database | Path | Written by |
|
|
63
|
+
|----------|------|------------|
|
|
64
|
+
| **Local** | `~/.sinain/memory/knowledge-graph.db` | `LocalCurationService` (session distillation on shutdown, periodic curation every 30 min) |
|
|
65
|
+
| **Workspace** | `~/.openclaw/workspace/memory/knowledge-graph.db` | Heartbeat curation scripts (`sinain_heartbeat_tick`) |
|
|
66
|
+
|
|
67
|
+
### Knowledge Tools
|
|
68
|
+
|
|
69
|
+
| Tool | What it does |
|
|
70
|
+
|------|-------------|
|
|
71
|
+
| `sinain_get_knowledge` | Read the portable knowledge document (playbook + top facts) from workspace |
|
|
72
|
+
| `sinain_knowledge_query` | Query facts by entity/keyword — queries **both** DBs via sinain-core API |
|
|
73
|
+
| `sinain_distill_session` | Explicitly distill current session into knowledge updates |
|
|
74
|
+
|
|
75
|
+
### HTTP Knowledge API (sinain-core, port 9500)
|
|
76
|
+
|
|
77
|
+
These endpoints query **both** databases and merge results:
|
|
78
|
+
|
|
79
|
+
| Endpoint | Purpose |
|
|
80
|
+
|----------|---------|
|
|
81
|
+
| `GET /knowledge` | Portable knowledge document |
|
|
82
|
+
| `GET /knowledge/facts?entities=X&max=N` | Query facts by keyword tags |
|
|
83
|
+
| `GET /knowledge/entities?max=N` | List all entities with attributes |
|
|
84
|
+
| `GET /knowledge/export?domain=X&max=N` | Export facts as portable JSON |
|
|
85
|
+
| `POST /knowledge/import` | Import facts (deduplicates automatically) |
|
|
86
|
+
| `GET /knowledge/ui` | Web UI for browsing/managing knowledge |
|
|
87
|
+
|
|
88
|
+
### How Knowledge Flows
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
Session (screen + audio) → LocalCurationService → Local DB
|
|
92
|
+
↓ (queried together)
|
|
93
|
+
Heartbeat tick → curation scripts ──────────→ Workspace DB
|
|
94
|
+
↓
|
|
95
|
+
Knowledge API (localhost:9500) ← merges both DBs ← queries
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- **Local DB** gets real-time session knowledge (audio transcripts, screen patterns, German lessons, etc.)
|
|
99
|
+
- **Workspace DB** gets heartbeat-curated knowledge (playbook patterns, feedback analysis)
|
|
100
|
+
- The Knowledge API merges both — use `sinain_knowledge_query` for combined results
|
|
101
|
+
- Facts have confidence decay (60-day half-life) — reinforcement resets the clock
|
|
102
|
+
- Export/import via `/knowledge/export` → `/knowledge/import` enables cross-instance transfer
|
|
103
|
+
|
|
104
|
+
### Using Knowledge in Escalation Responses
|
|
105
|
+
|
|
106
|
+
When responding to escalations, call `sinain_knowledge_query` with relevant entities to enrich your response with long-term knowledge. Example: if the user is working on German grammar, query `sinain_knowledge_query({ entities: ["german", "grammar"] })` to retrieve previously learned patterns.
|
|
107
|
+
|
|
58
108
|
## Spawning Background Tasks
|
|
59
109
|
|
|
60
110
|
When an escalation suggests deeper research would help:
|
package/sinain-agent/run.sh
CHANGED
|
@@ -221,9 +221,13 @@ Call sinain_get_escalation to see the full context, then call sinain_respond wit
|
|
|
221
221
|
Response guidelines: 5-10 sentences, address errors first, reference specific screen/audio context, never NO_REPLY. Max 4000 chars for coding context, 3000 otherwise.'
|
|
222
222
|
|
|
223
223
|
HEARTBEAT_PROMPT='You are the sinain HUD agent. Run the heartbeat cycle:
|
|
224
|
-
1. Call sinain_heartbeat_tick with a brief session summary
|
|
225
|
-
2. If the result contains a suggestion, post it to HUD via sinain_post_feed
|
|
226
|
-
3. Call
|
|
224
|
+
1. Call sinain_heartbeat_tick with a brief session summary (runs signal analysis, session distillation, knowledge integration, insight synthesis)
|
|
225
|
+
2. If the result contains a suggestion or insight, post it to HUD via sinain_post_feed
|
|
226
|
+
3. Call sinain_get_knowledge to review the merged knowledge document (draws from both local and workspace databases)
|
|
227
|
+
4. Optionally call sinain_knowledge_query with relevant entities to check long-term knowledge state
|
|
228
|
+
5. Call sinain_get_feedback to review recent escalation scores
|
|
229
|
+
|
|
230
|
+
Knowledge context: sinain-core maintains two knowledge databases — local (session distillation) and workspace (heartbeat curation). The knowledge tools query both via the sinain-core API. Facts have confidence decay (60-day half-life).'
|
|
227
231
|
|
|
228
232
|
# --- Main loop ---
|
|
229
233
|
|
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);
|