@geravant/sinain 1.10.0 → 1.11.0
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 +73 -10
- package/sinain-core/src/agent/analyzer.ts +4 -27
- package/sinain-core/src/agent/loop.ts +10 -40
- package/sinain-core/src/agent/situation-writer.ts +0 -16
- package/sinain-core/src/config.ts +1 -9
- package/sinain-core/src/escalation/escalator.ts +43 -16
- package/sinain-core/src/index.ts +316 -61
- package/sinain-core/src/learning/local-curation.ts +373 -0
- package/sinain-core/src/overlay/commands.ts +31 -11
- package/sinain-core/src/overlay/ws-handler.ts +10 -1
- package/sinain-core/src/server.ts +318 -0
- package/sinain-core/src/types.ts +22 -28
- package/sinain-mcp-server/index.ts +62 -4
- package/sinain-memory/eval/assertions.py +0 -21
- 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-core/src/agent/traits.ts +0 -520
package/sinain-core/src/index.ts
CHANGED
|
@@ -8,7 +8,6 @@ import { AudioPipeline } from "./audio/pipeline.js";
|
|
|
8
8
|
import type { CaptureSpawner } from "./audio/capture-spawner.js";
|
|
9
9
|
import { TranscriptionService } from "./audio/transcription.js";
|
|
10
10
|
import { AgentLoop } from "./agent/loop.js";
|
|
11
|
-
import { TraitEngine, loadTraitRoster } from "./agent/traits.js";
|
|
12
11
|
import { shortAppName } from "./agent/context-window.js";
|
|
13
12
|
import { Escalator } from "./escalation/escalator.js";
|
|
14
13
|
import { Recorder } from "./recorder.js";
|
|
@@ -16,6 +15,7 @@ import { Tracer } from "./trace/tracer.js";
|
|
|
16
15
|
import { TraceStore } from "./trace/trace-store.js";
|
|
17
16
|
import { FeedbackStore } from "./learning/feedback-store.js";
|
|
18
17
|
import { SignalCollector } from "./learning/signal-collector.js";
|
|
18
|
+
import { LocalCurationService } from "./learning/local-curation.js";
|
|
19
19
|
import { createAppServer } from "./server.js";
|
|
20
20
|
import { Profiler } from "./profiler.js";
|
|
21
21
|
import { CostTracker } from "./cost/tracker.js";
|
|
@@ -32,6 +32,266 @@ function resolveWorkspace(): string {
|
|
|
32
32
|
return raw.startsWith("~") ? raw.replace("~", process.env.HOME || "") : raw;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/** Resolve the local memory directory (independent of OpenClaw workspace). */
|
|
36
|
+
function resolveLocalMemoryDir(): string {
|
|
37
|
+
const raw = process.env.SINAIN_MEMORY_DIR || `${process.env.HOME}/.sinain/memory`;
|
|
38
|
+
return raw.startsWith("~") ? raw.replace("~", process.env.HOME || "") : raw;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Query knowledge facts from both local and workspace databases.
|
|
43
|
+
* Checks local (~/.sinain/memory) first, then workspace (~/.openclaw/workspace/memory).
|
|
44
|
+
* Merges results, deduplicates, returns up to maxFacts.
|
|
45
|
+
*/
|
|
46
|
+
async function queryKnowledgeFactsMulti(entities: string[], maxFacts: number): Promise<string> {
|
|
47
|
+
const { execFileSync } = await import("node:child_process");
|
|
48
|
+
const { resolve } = await import("node:path");
|
|
49
|
+
const { dirname } = await import("node:path");
|
|
50
|
+
const { fileURLToPath } = await import("node:url");
|
|
51
|
+
|
|
52
|
+
// Candidate database paths (local first, then workspace)
|
|
53
|
+
const localDir = resolveLocalMemoryDir();
|
|
54
|
+
const workspaceDir = `${resolveWorkspace()}/memory`;
|
|
55
|
+
const dbPaths = [
|
|
56
|
+
`${localDir}/knowledge-graph.db`,
|
|
57
|
+
`${workspaceDir}/knowledge-graph.db`,
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
// Candidate script paths
|
|
61
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
62
|
+
const scriptCandidates = [
|
|
63
|
+
resolve(__dir, "..", "..", "sinain-hud-plugin", "sinain-memory", "graph_query.py"),
|
|
64
|
+
resolve(__dir, "..", "sinain-memory", "graph_query.py"),
|
|
65
|
+
`${resolveWorkspace()}/sinain-memory/graph_query.py`,
|
|
66
|
+
];
|
|
67
|
+
const scriptPath = scriptCandidates.find(p => existsSync(p)) || scriptCandidates[0];
|
|
68
|
+
|
|
69
|
+
const results: string[] = [];
|
|
70
|
+
for (const dbPath of dbPaths) {
|
|
71
|
+
if (!existsSync(dbPath)) continue;
|
|
72
|
+
try {
|
|
73
|
+
const args = [scriptPath, "--db", dbPath, "--max-facts", String(maxFacts), "--format", "text"];
|
|
74
|
+
if (entities.length > 0) args.push("--entities", JSON.stringify(entities));
|
|
75
|
+
const out = execFileSync("python3", args, { timeout: 5000, encoding: "utf-8" }).trim();
|
|
76
|
+
if (out) results.push(out);
|
|
77
|
+
} catch { /* skip failed db */ }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (results.length === 0) return "";
|
|
81
|
+
if (results.length === 1) return results[0];
|
|
82
|
+
|
|
83
|
+
// Merge and deduplicate lines from both sources
|
|
84
|
+
const seen = new Set<string>();
|
|
85
|
+
const merged: string[] = [];
|
|
86
|
+
for (const block of results) {
|
|
87
|
+
for (const line of block.split("\n")) {
|
|
88
|
+
const key = line.replace(/\(confidence:.*$/, "").trim();
|
|
89
|
+
if (key && !seen.has(key)) {
|
|
90
|
+
seen.add(key);
|
|
91
|
+
merged.push(line);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return merged.slice(0, maxFacts).join("\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** List all entities from both local and workspace knowledge graphs. */
|
|
99
|
+
async function listKnowledgeEntitiesMulti(max: number): Promise<string> {
|
|
100
|
+
const { execFileSync } = await import("node:child_process");
|
|
101
|
+
const { resolve, dirname } = await import("node:path");
|
|
102
|
+
const { fileURLToPath } = await import("node:url");
|
|
103
|
+
|
|
104
|
+
const localDir = resolveLocalMemoryDir();
|
|
105
|
+
const workspaceDir = `${resolveWorkspace()}/memory`;
|
|
106
|
+
const dbPaths = [
|
|
107
|
+
`${localDir}/knowledge-graph.db`,
|
|
108
|
+
`${workspaceDir}/knowledge-graph.db`,
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
112
|
+
const scriptCandidates = [
|
|
113
|
+
resolve(__dir, "..", "..", "sinain-hud-plugin", "sinain-memory", "graph_query.py"),
|
|
114
|
+
resolve(__dir, "..", "sinain-memory", "graph_query.py"),
|
|
115
|
+
`${resolveWorkspace()}/sinain-memory/graph_query.py`,
|
|
116
|
+
];
|
|
117
|
+
const scriptPath = scriptCandidates.find(p => existsSync(p)) || scriptCandidates[0];
|
|
118
|
+
|
|
119
|
+
const allFacts: any[] = [];
|
|
120
|
+
for (const dbPath of dbPaths) {
|
|
121
|
+
if (!existsSync(dbPath)) continue;
|
|
122
|
+
try {
|
|
123
|
+
const out = execFileSync("python3", [
|
|
124
|
+
scriptPath, "--db", dbPath, "--top", String(max), "--format", "json",
|
|
125
|
+
], { timeout: 5000, encoding: "utf-8" });
|
|
126
|
+
const parsed = JSON.parse(out);
|
|
127
|
+
if (parsed.facts) allFacts.push(...parsed.facts);
|
|
128
|
+
} catch { /* skip */ }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Deduplicate by entityId, merge
|
|
132
|
+
const seen = new Set<string>();
|
|
133
|
+
const unique = allFacts.filter(f => {
|
|
134
|
+
const id = f.entityId || "";
|
|
135
|
+
if (seen.has(id)) return false;
|
|
136
|
+
seen.add(id);
|
|
137
|
+
return true;
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
return JSON.stringify(unique.slice(0, max));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Export knowledge facts as a portable JSON module. */
|
|
144
|
+
async function exportKnowledgeMulti(domain: string | null, max: number): Promise<string> {
|
|
145
|
+
const { execFileSync } = await import("node:child_process");
|
|
146
|
+
const { resolve, dirname } = await import("node:path");
|
|
147
|
+
const { fileURLToPath } = await import("node:url");
|
|
148
|
+
|
|
149
|
+
const localDir = resolveLocalMemoryDir();
|
|
150
|
+
const workspaceDir = `${resolveWorkspace()}/memory`;
|
|
151
|
+
const dbPaths = [
|
|
152
|
+
`${localDir}/knowledge-graph.db`,
|
|
153
|
+
`${workspaceDir}/knowledge-graph.db`,
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
157
|
+
const scriptCandidates = [
|
|
158
|
+
resolve(__dir, "..", "..", "sinain-hud-plugin", "sinain-memory", "graph_query.py"),
|
|
159
|
+
`${resolveWorkspace()}/sinain-memory/graph_query.py`,
|
|
160
|
+
];
|
|
161
|
+
const scriptPath = scriptCandidates.find(p => existsSync(p)) || scriptCandidates[0];
|
|
162
|
+
|
|
163
|
+
const allFacts: any[] = [];
|
|
164
|
+
for (const dbPath of dbPaths) {
|
|
165
|
+
if (!existsSync(dbPath)) continue;
|
|
166
|
+
try {
|
|
167
|
+
const out = execFileSync("python3", [
|
|
168
|
+
scriptPath, "--db", dbPath, "--top", String(max), "--format", "json",
|
|
169
|
+
], { timeout: 5000, encoding: "utf-8" });
|
|
170
|
+
const parsed = JSON.parse(out);
|
|
171
|
+
if (parsed.facts) allFacts.push(...parsed.facts);
|
|
172
|
+
} catch { /* skip */ }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Deduplicate
|
|
176
|
+
const seen = new Set<string>();
|
|
177
|
+
let facts = allFacts.filter(f => {
|
|
178
|
+
const id = f.entityId || "";
|
|
179
|
+
if (seen.has(id)) return false;
|
|
180
|
+
seen.add(id);
|
|
181
|
+
return true;
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Filter by domain if specified
|
|
185
|
+
if (domain) {
|
|
186
|
+
facts = facts.filter(f => f.domain === domain);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return JSON.stringify({
|
|
190
|
+
format: "sinain-knowledge-export",
|
|
191
|
+
version: 1,
|
|
192
|
+
exportedAt: new Date().toISOString(),
|
|
193
|
+
domain: domain || "all",
|
|
194
|
+
count: facts.length,
|
|
195
|
+
facts: facts.slice(0, max),
|
|
196
|
+
}, null, 2);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Import knowledge facts from a portable JSON module into the local graph. */
|
|
200
|
+
async function importKnowledgeToLocal(data: string): Promise<string> {
|
|
201
|
+
const { execFileSync } = await import("node:child_process");
|
|
202
|
+
const { resolve, dirname } = await import("node:path");
|
|
203
|
+
const { fileURLToPath } = await import("node:url");
|
|
204
|
+
const { mkdirSync } = await import("node:fs");
|
|
205
|
+
|
|
206
|
+
let parsed: any;
|
|
207
|
+
try {
|
|
208
|
+
parsed = JSON.parse(data);
|
|
209
|
+
} catch {
|
|
210
|
+
return JSON.stringify({ ok: false, error: "Invalid JSON" });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const facts = parsed.facts || (Array.isArray(parsed) ? parsed : null);
|
|
214
|
+
if (!facts || !Array.isArray(facts) || facts.length === 0) {
|
|
215
|
+
const keys = Object.keys(parsed).join(", ");
|
|
216
|
+
return JSON.stringify({ ok: false, error: `No 'facts' array found. Expected sinain knowledge export format: {"facts":[...]}. Got keys: ${keys}` });
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const localDir = resolveLocalMemoryDir();
|
|
220
|
+
mkdirSync(localDir, { recursive: true });
|
|
221
|
+
const dbPath = `${localDir}/knowledge-graph.db`;
|
|
222
|
+
|
|
223
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
224
|
+
const scriptsDir = resolve(__dir, "..", "..", "sinain-hud-plugin", "sinain-memory");
|
|
225
|
+
|
|
226
|
+
// Convert facts to graph ops for knowledge_integrator
|
|
227
|
+
const graphOps = facts.map((f: any) => ({
|
|
228
|
+
op: "assert",
|
|
229
|
+
entity: f.entity || f.entityId?.replace(/^fact:/, "").replace(/-[a-f0-9]{12}$/, "") || "unknown",
|
|
230
|
+
attribute: f.attribute || "value",
|
|
231
|
+
value: f.value || "",
|
|
232
|
+
confidence: parseFloat(f.confidence || "0.7"),
|
|
233
|
+
domain: f.domain || "",
|
|
234
|
+
}));
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
// Use triplestore directly via Python
|
|
238
|
+
const script = `
|
|
239
|
+
import json, sys
|
|
240
|
+
sys.path.insert(0, "${scriptsDir}")
|
|
241
|
+
from triplestore import TripleStore
|
|
242
|
+
import hashlib
|
|
243
|
+
|
|
244
|
+
db_path = "${dbPath}"
|
|
245
|
+
store = TripleStore(db_path)
|
|
246
|
+
ops = json.loads(sys.stdin.read())
|
|
247
|
+
stats = {"asserted": 0, "skipped": 0}
|
|
248
|
+
|
|
249
|
+
for op in ops:
|
|
250
|
+
entity = op.get("entity", "")
|
|
251
|
+
value = op.get("value", "")
|
|
252
|
+
if not entity or not value:
|
|
253
|
+
stats["skipped"] += 1
|
|
254
|
+
continue
|
|
255
|
+
|
|
256
|
+
h = hashlib.sha256(f"{entity}:{op.get('attribute','')}:{value}".encode()).hexdigest()[:12]
|
|
257
|
+
slug = entity.replace(" ", "-").lower()[:30]
|
|
258
|
+
entity_id = f"fact:{slug}-{h}"
|
|
259
|
+
|
|
260
|
+
# Check if already exists
|
|
261
|
+
existing = store.entity(entity_id)
|
|
262
|
+
if existing:
|
|
263
|
+
stats["skipped"] += 1
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
tx = store.begin_tx("import", metadata=json.dumps({"source": "web-import"}))
|
|
267
|
+
store.assert_triple(tx, entity_id, "entity", entity)
|
|
268
|
+
store.assert_triple(tx, entity_id, "attribute", op.get("attribute", "value"))
|
|
269
|
+
store.assert_triple(tx, entity_id, "value", value)
|
|
270
|
+
store.assert_triple(tx, entity_id, "confidence", str(op.get("confidence", 0.7)))
|
|
271
|
+
store.assert_triple(tx, entity_id, "first_seen", "${new Date().toISOString()}")
|
|
272
|
+
store.assert_triple(tx, entity_id, "last_reinforced", "${new Date().toISOString()}")
|
|
273
|
+
store.assert_triple(tx, entity_id, "reinforce_count", "1")
|
|
274
|
+
if op.get("domain"):
|
|
275
|
+
store.assert_triple(tx, entity_id, "domain", op["domain"])
|
|
276
|
+
stats["asserted"] += 1
|
|
277
|
+
|
|
278
|
+
store.close()
|
|
279
|
+
print(json.dumps(stats))
|
|
280
|
+
`;
|
|
281
|
+
|
|
282
|
+
const result = execFileSync("python3", ["-c", script], {
|
|
283
|
+
input: JSON.stringify(graphOps),
|
|
284
|
+
timeout: 10_000,
|
|
285
|
+
encoding: "utf-8",
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const stats = JSON.parse(result.trim());
|
|
289
|
+
return JSON.stringify({ ok: true, stats, imported: stats.asserted, skipped: stats.skipped });
|
|
290
|
+
} catch (err: any) {
|
|
291
|
+
return JSON.stringify({ ok: false, error: err.message?.slice(0, 200) });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
35
295
|
async function main() {
|
|
36
296
|
log(TAG, "sinain-core starting...");
|
|
37
297
|
|
|
@@ -78,9 +338,10 @@ async function main() {
|
|
|
78
338
|
? new FeedbackStore(config.learningConfig.feedbackDir, config.learningConfig.retentionDays)
|
|
79
339
|
: null;
|
|
80
340
|
|
|
81
|
-
// ── Initialize
|
|
82
|
-
const
|
|
83
|
-
|
|
341
|
+
// ── Initialize local knowledge pipeline ──
|
|
342
|
+
const localCuration = new LocalCurationService();
|
|
343
|
+
localCuration.distillPendingSession(); // Recover any session saved before a force-kill
|
|
344
|
+
localCuration.startPeriodicCuration();
|
|
84
345
|
|
|
85
346
|
// ── Initialize escalation ──
|
|
86
347
|
const escalator = new Escalator({
|
|
@@ -90,17 +351,7 @@ async function main() {
|
|
|
90
351
|
openclawConfig: config.openclawConfig,
|
|
91
352
|
profiler,
|
|
92
353
|
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
|
-
},
|
|
354
|
+
queryKnowledgeFacts: queryKnowledgeFactsMulti,
|
|
104
355
|
});
|
|
105
356
|
|
|
106
357
|
// ── Initialize agent loop (event-driven) ──
|
|
@@ -116,33 +367,6 @@ async function main() {
|
|
|
116
367
|
// Handle recorder commands
|
|
117
368
|
const stopResult = recorder.handleCommand(entry.record);
|
|
118
369
|
|
|
119
|
-
// Dispatch task via subagent spawn
|
|
120
|
-
if (entry.task || stopResult) {
|
|
121
|
-
let task: string;
|
|
122
|
-
let label: string | undefined;
|
|
123
|
-
|
|
124
|
-
if (stopResult && stopResult.segments > 0 && entry.task) {
|
|
125
|
-
// Recording stopped with explicit task instruction
|
|
126
|
-
task = `${entry.task}\n\n[Recording: "${stopResult.title}", ${stopResult.durationS}s]\n${stopResult.transcript}`;
|
|
127
|
-
label = stopResult.title;
|
|
128
|
-
} else if (stopResult && stopResult.segments > 0) {
|
|
129
|
-
// Recording stopped without explicit task — default to cleanup/summarize
|
|
130
|
-
task = `Clean up and summarize this recording transcript:\n\n[Recording: "${stopResult.title}", ${stopResult.durationS}s]\n${stopResult.transcript}`;
|
|
131
|
-
label = stopResult.title;
|
|
132
|
-
} else if (entry.task) {
|
|
133
|
-
// Standalone task without recording
|
|
134
|
-
task = entry.task;
|
|
135
|
-
} else {
|
|
136
|
-
task = "";
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (task) {
|
|
140
|
-
escalator.dispatchSpawnTask(task, label).catch(err => {
|
|
141
|
-
error(TAG, "spawn task dispatch error:", err);
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
370
|
// Escalation continues as normal
|
|
147
371
|
escalator.onAgentAnalysis(entry, contextWindow);
|
|
148
372
|
},
|
|
@@ -166,8 +390,6 @@ async function main() {
|
|
|
166
390
|
};
|
|
167
391
|
return ctx;
|
|
168
392
|
} : undefined,
|
|
169
|
-
traitEngine,
|
|
170
|
-
traitLogDir: config.traitConfig.logDir,
|
|
171
393
|
getKnowledgeDocPath: () => {
|
|
172
394
|
const workspace = resolveWorkspace();
|
|
173
395
|
const p = `${workspace}/memory/sinain-knowledge.md`;
|
|
@@ -308,6 +530,9 @@ async function main() {
|
|
|
308
530
|
// ── Screen capture active flag ──
|
|
309
531
|
let screenActive = true;
|
|
310
532
|
|
|
533
|
+
// ── Escalation pause/resume state ──
|
|
534
|
+
let savedEscalationMode: typeof config.escalationConfig.mode | null = null;
|
|
535
|
+
|
|
311
536
|
// ── Create HTTP + WS server ──
|
|
312
537
|
const server = createAppServer({
|
|
313
538
|
config,
|
|
@@ -418,26 +643,22 @@ async function main() {
|
|
|
418
643
|
|
|
419
644
|
// Bare agent HTTP escalation bridge
|
|
420
645
|
getEscalationPending: () => escalator.getPendingHttp(),
|
|
646
|
+
isEscalationPaused: () => savedEscalationMode !== null,
|
|
421
647
|
respondEscalation: (id: string, response: string) => escalator.respondHttp(id, response),
|
|
422
648
|
|
|
423
|
-
// Knowledge graph integration
|
|
649
|
+
// Knowledge graph integration (checks both local and workspace DBs)
|
|
424
650
|
getKnowledgeDocPath: () => {
|
|
425
|
-
|
|
426
|
-
const
|
|
427
|
-
|
|
651
|
+
// Check local first, then workspace
|
|
652
|
+
const localPath = `${resolveLocalMemoryDir()}/sinain-knowledge.md`;
|
|
653
|
+
const workspacePath = `${resolveWorkspace()}/memory/sinain-knowledge.md`;
|
|
654
|
+
try { if (existsSync(localPath)) return localPath; } catch {}
|
|
655
|
+
try { if (existsSync(workspacePath)) return workspacePath; } catch {}
|
|
428
656
|
return null;
|
|
429
657
|
},
|
|
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
|
-
},
|
|
658
|
+
queryKnowledgeFacts: queryKnowledgeFactsMulti,
|
|
659
|
+
listKnowledgeEntities: listKnowledgeEntitiesMulti,
|
|
660
|
+
exportKnowledge: exportKnowledgeMulti,
|
|
661
|
+
importKnowledge: importKnowledgeToLocal,
|
|
441
662
|
|
|
442
663
|
// Spawn background agent task (from HUD Shift+Enter or bare agent POST /spawn)
|
|
443
664
|
onSpawnCommand: (text: string) => {
|
|
@@ -482,7 +703,22 @@ async function main() {
|
|
|
482
703
|
wsHandler.updateState({ screen: screenActive ? "active" : "off" });
|
|
483
704
|
return screenActive;
|
|
484
705
|
},
|
|
485
|
-
|
|
706
|
+
onToggleEscalation: () => {
|
|
707
|
+
if (savedEscalationMode === null) {
|
|
708
|
+
// Pause: save current mode, switch to off
|
|
709
|
+
savedEscalationMode = config.escalationConfig.mode;
|
|
710
|
+
escalator.setMode("off");
|
|
711
|
+
log(TAG, `escalation paused (was: ${savedEscalationMode})`);
|
|
712
|
+
return false;
|
|
713
|
+
} else {
|
|
714
|
+
// Resume: restore saved mode
|
|
715
|
+
const mode = savedEscalationMode;
|
|
716
|
+
savedEscalationMode = null;
|
|
717
|
+
escalator.setMode(mode);
|
|
718
|
+
log(TAG, `escalation resumed (mode: ${mode})`);
|
|
719
|
+
return true;
|
|
720
|
+
}
|
|
721
|
+
},
|
|
486
722
|
});
|
|
487
723
|
|
|
488
724
|
// Broadcast initial screen state so overlay gets correct status on connect
|
|
@@ -544,7 +780,6 @@ async function main() {
|
|
|
544
780
|
log(TAG, ` mic: ${config.micEnabled ? (config.micConfig.autoStart ? "active" : "standby") : "disabled"}`);
|
|
545
781
|
log(TAG, ` agent: ${config.agentConfig.enabled ? "enabled" : "disabled"}`);
|
|
546
782
|
log(TAG, ` escal: ${config.escalationConfig.mode}`);
|
|
547
|
-
log(TAG, ` traits: ${config.traitConfig.enabled ? "enabled" : "disabled"} (${traitRoster.length} traits)`);
|
|
548
783
|
log(TAG, ` cost: display=${config.costDisplayEnabled ? "on" : "off"} (always logged)`);
|
|
549
784
|
|
|
550
785
|
// ── Graceful shutdown ──
|
|
@@ -563,6 +798,26 @@ async function main() {
|
|
|
563
798
|
signalCollector?.destroy();
|
|
564
799
|
feedbackStore?.destroy();
|
|
565
800
|
traceStore?.destroy();
|
|
801
|
+
|
|
802
|
+
// Save session knowledge — write feed items to disk FIRST (instant),
|
|
803
|
+
// then attempt LLM distillation. If tsx force-kills before distillation
|
|
804
|
+
// finishes, the saved file is recovered on next startup.
|
|
805
|
+
localCuration.stop();
|
|
806
|
+
const feedItems = feedBuffer.query(0);
|
|
807
|
+
try {
|
|
808
|
+
localCuration.savePendingSession(feedItems);
|
|
809
|
+
} catch (err: any) {
|
|
810
|
+
warn(TAG, `failed to save pending session: ${err.message?.slice(0, 100)}`);
|
|
811
|
+
}
|
|
812
|
+
try {
|
|
813
|
+
if (feedItems.length >= 3) {
|
|
814
|
+
log(TAG, `distilling session (${feedItems.length} feed items)...`);
|
|
815
|
+
await localCuration.distillSession(feedItems);
|
|
816
|
+
}
|
|
817
|
+
} catch (err: any) {
|
|
818
|
+
warn(TAG, `session distillation failed (will retry on next startup): ${err.message?.slice(0, 100)}`);
|
|
819
|
+
}
|
|
820
|
+
|
|
566
821
|
await server.destroy();
|
|
567
822
|
log(TAG, "goodbye");
|
|
568
823
|
process.exit(0);
|