@geravant/sinain 1.0.19 → 1.2.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/README.md +10 -1
- package/cli.js +176 -0
- package/index.ts +4 -2
- package/install.js +89 -14
- package/launcher.js +622 -0
- package/openclaw.plugin.json +4 -0
- package/pack-prepare.js +48 -0
- package/package.json +24 -5
- package/sense_client/README.md +82 -0
- package/sense_client/__init__.py +1 -0
- package/sense_client/__main__.py +462 -0
- package/sense_client/app_detector.py +54 -0
- package/sense_client/app_detector_win.py +83 -0
- package/sense_client/capture.py +215 -0
- package/sense_client/capture_win.py +88 -0
- package/sense_client/change_detector.py +86 -0
- package/sense_client/config.py +64 -0
- package/sense_client/gate.py +145 -0
- package/sense_client/ocr.py +347 -0
- package/sense_client/privacy.py +65 -0
- package/sense_client/requirements.txt +13 -0
- package/sense_client/roi_extractor.py +84 -0
- package/sense_client/sender.py +173 -0
- package/sense_client/tests/__init__.py +0 -0
- package/sense_client/tests/test_stream1_optimizations.py +234 -0
- package/setup-overlay.js +82 -0
- package/sinain-agent/.env.example +17 -0
- package/sinain-agent/CLAUDE.md +87 -0
- package/sinain-agent/mcp-config.json +12 -0
- package/sinain-agent/run.sh +248 -0
- package/sinain-core/.env.example +93 -0
- package/sinain-core/package-lock.json +552 -0
- package/sinain-core/package.json +21 -0
- package/sinain-core/src/agent/analyzer.ts +366 -0
- package/sinain-core/src/agent/context-window.ts +172 -0
- package/sinain-core/src/agent/loop.ts +404 -0
- package/sinain-core/src/agent/situation-writer.ts +187 -0
- package/sinain-core/src/agent/traits.ts +520 -0
- package/sinain-core/src/audio/capture-spawner-macos.ts +44 -0
- package/sinain-core/src/audio/capture-spawner-win.ts +37 -0
- package/sinain-core/src/audio/capture-spawner.ts +14 -0
- package/sinain-core/src/audio/pipeline.ts +335 -0
- package/sinain-core/src/audio/transcription-local.ts +141 -0
- package/sinain-core/src/audio/transcription.ts +278 -0
- package/sinain-core/src/buffers/feed-buffer.ts +71 -0
- package/sinain-core/src/buffers/sense-buffer.ts +425 -0
- package/sinain-core/src/config.ts +245 -0
- package/sinain-core/src/escalation/escalation-slot.ts +136 -0
- package/sinain-core/src/escalation/escalator.ts +828 -0
- package/sinain-core/src/escalation/message-builder.ts +370 -0
- package/sinain-core/src/escalation/openclaw-ws.ts +726 -0
- package/sinain-core/src/escalation/scorer.ts +166 -0
- package/sinain-core/src/index.ts +537 -0
- package/sinain-core/src/learning/feedback-store.ts +253 -0
- package/sinain-core/src/learning/signal-collector.ts +218 -0
- package/sinain-core/src/log.ts +24 -0
- package/sinain-core/src/overlay/commands.ts +126 -0
- package/sinain-core/src/overlay/ws-handler.ts +267 -0
- package/sinain-core/src/privacy/index.ts +18 -0
- package/sinain-core/src/privacy/presets.ts +40 -0
- package/sinain-core/src/privacy/redact.ts +92 -0
- package/sinain-core/src/profiler.ts +181 -0
- package/sinain-core/src/recorder.ts +186 -0
- package/sinain-core/src/server.ts +456 -0
- package/sinain-core/src/trace/trace-store.ts +73 -0
- package/sinain-core/src/trace/tracer.ts +94 -0
- package/sinain-core/src/types.ts +427 -0
- package/sinain-core/src/util/dedup.ts +48 -0
- package/sinain-core/src/util/task-store.ts +84 -0
- package/sinain-core/tsconfig.json +18 -0
- package/sinain-knowledge/curation/engine.ts +137 -24
- package/sinain-knowledge/data/git-store.ts +26 -0
- package/sinain-knowledge/data/store.ts +117 -0
- package/sinain-mcp-server/index.ts +417 -0
- package/sinain-mcp-server/package.json +19 -0
- package/sinain-mcp-server/tsconfig.json +15 -0
- package/sinain-memory/graph_query.py +185 -0
- package/sinain-memory/knowledge_integrator.py +450 -0
- package/sinain-memory/memory-config.json +3 -1
- package/sinain-memory/session_distiller.py +162 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { log, error } from "../log.js";
|
|
5
|
+
|
|
6
|
+
const TAG = "task-store";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Persistent task entry for spawn tasks.
|
|
10
|
+
*/
|
|
11
|
+
export interface PendingTaskEntry {
|
|
12
|
+
runId: string;
|
|
13
|
+
childSessionKey: string;
|
|
14
|
+
label?: string;
|
|
15
|
+
startedAt: number;
|
|
16
|
+
pollingEmitted: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const STORE_DIR = path.join(os.homedir(), ".sinain-core");
|
|
20
|
+
const STORE_PATH = path.join(STORE_DIR, "pending-tasks.json");
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load pending tasks from disk.
|
|
24
|
+
* Returns empty map if file doesn't exist or is corrupted.
|
|
25
|
+
*/
|
|
26
|
+
export function loadPendingTasks(): Map<string, PendingTaskEntry> {
|
|
27
|
+
try {
|
|
28
|
+
if (!fs.existsSync(STORE_PATH)) {
|
|
29
|
+
return new Map();
|
|
30
|
+
}
|
|
31
|
+
const data = fs.readFileSync(STORE_PATH, "utf-8");
|
|
32
|
+
const parsed = JSON.parse(data);
|
|
33
|
+
if (!Array.isArray(parsed)) {
|
|
34
|
+
return new Map();
|
|
35
|
+
}
|
|
36
|
+
const map = new Map<string, PendingTaskEntry>();
|
|
37
|
+
for (const [key, value] of parsed) {
|
|
38
|
+
if (typeof key === "string" && value && typeof value.runId === "string") {
|
|
39
|
+
map.set(key, value as PendingTaskEntry);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
log(TAG, `loaded ${map.size} pending task(s) from disk`);
|
|
43
|
+
return map;
|
|
44
|
+
} catch (err: any) {
|
|
45
|
+
error(TAG, `failed to load pending tasks: ${err.message}`);
|
|
46
|
+
return new Map();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Save pending tasks to disk atomically.
|
|
52
|
+
* Uses write-then-rename for crash safety.
|
|
53
|
+
*/
|
|
54
|
+
export function savePendingTasks(tasks: Map<string, PendingTaskEntry>): void {
|
|
55
|
+
try {
|
|
56
|
+
// Ensure directory exists
|
|
57
|
+
if (!fs.existsSync(STORE_DIR)) {
|
|
58
|
+
fs.mkdirSync(STORE_DIR, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Convert map to array of entries for JSON serialization
|
|
62
|
+
const entries = Array.from(tasks.entries());
|
|
63
|
+
const tmpPath = STORE_PATH + ".tmp";
|
|
64
|
+
|
|
65
|
+
fs.writeFileSync(tmpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
66
|
+
fs.renameSync(tmpPath, STORE_PATH);
|
|
67
|
+
} catch (err: any) {
|
|
68
|
+
error(TAG, `failed to save pending tasks: ${err.message}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Delete the task store file (cleanup).
|
|
74
|
+
*/
|
|
75
|
+
export function clearPendingTasks(): void {
|
|
76
|
+
try {
|
|
77
|
+
if (fs.existsSync(STORE_PATH)) {
|
|
78
|
+
fs.unlinkSync(STORE_PATH);
|
|
79
|
+
log(TAG, "cleared pending tasks file");
|
|
80
|
+
}
|
|
81
|
+
} catch (err: any) {
|
|
82
|
+
error(TAG, `failed to clear pending tasks: ${err.message}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"sourceMap": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist", "eval"]
|
|
18
|
+
}
|
|
@@ -6,11 +6,37 @@
|
|
|
6
6
|
* external process execution.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
|
|
9
12
|
import type { Logger, ScriptRunner } from "../data/schema.js";
|
|
10
13
|
import type { KnowledgeStore } from "../data/store.js";
|
|
11
14
|
import type { ResilienceManager } from "./resilience.js";
|
|
12
15
|
import type { GitSnapshotStore } from "../data/git-store.js";
|
|
13
16
|
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Distillation State
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
interface DistillState {
|
|
22
|
+
lastDistilledFeedId: number;
|
|
23
|
+
lastDistilledTs: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readDistillState(workspaceDir: string): DistillState {
|
|
27
|
+
const p = join(workspaceDir, "memory", "distill-state.json");
|
|
28
|
+
try {
|
|
29
|
+
if (existsSync(p)) return JSON.parse(readFileSync(p, "utf-8"));
|
|
30
|
+
} catch {}
|
|
31
|
+
return { lastDistilledFeedId: 0, lastDistilledTs: "" };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function writeDistillState(workspaceDir: string, state: DistillState): void {
|
|
35
|
+
const dir = join(workspaceDir, "memory");
|
|
36
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
37
|
+
writeFileSync(join(dir, "distill-state.json"), JSON.stringify(state, null, 2), "utf-8");
|
|
38
|
+
}
|
|
39
|
+
|
|
14
40
|
// ============================================================================
|
|
15
41
|
// Types
|
|
16
42
|
// ============================================================================
|
|
@@ -43,14 +69,17 @@ export type ContextAssemblyOpts = {
|
|
|
43
69
|
export class CurationEngine {
|
|
44
70
|
private curationInterval: ReturnType<typeof setInterval> | null = null;
|
|
45
71
|
private gitSnapshotStore: GitSnapshotStore | null = null;
|
|
72
|
+
private sinainCoreUrl: string;
|
|
46
73
|
|
|
47
74
|
constructor(
|
|
48
75
|
private store: KnowledgeStore,
|
|
49
76
|
private runScript: ScriptRunner,
|
|
50
77
|
private resilience: ResilienceManager,
|
|
51
|
-
private config: { userTimezone: string },
|
|
78
|
+
private config: { userTimezone: string; sinainCoreUrl?: string },
|
|
52
79
|
private logger: Logger,
|
|
53
|
-
) {
|
|
80
|
+
) {
|
|
81
|
+
this.sinainCoreUrl = config.sinainCoreUrl || process.env.SINAIN_CORE_URL || "http://localhost:9500";
|
|
82
|
+
}
|
|
54
83
|
|
|
55
84
|
/** Attach a git-backed snapshot store for periodic saves. */
|
|
56
85
|
setGitSnapshotStore(gitStore: GitSnapshotStore): void {
|
|
@@ -143,15 +172,87 @@ export class CurationEngine {
|
|
|
143
172
|
confidence: 0,
|
|
144
173
|
};
|
|
145
174
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 2b. Session distillation check — fetch new feed items and distill if needed
|
|
178
|
+
try {
|
|
179
|
+
const distillState = readDistillState(workspaceDir);
|
|
180
|
+
const feedUrl = `${this.sinainCoreUrl}/feed?after=${distillState.lastDistilledFeedId}`;
|
|
181
|
+
const historyUrl = `${this.sinainCoreUrl}/agent/history?limit=10`;
|
|
182
|
+
|
|
183
|
+
const [feedResp, historyResp] = await Promise.all([
|
|
184
|
+
fetch(feedUrl).then(r => r.json()).catch(() => null),
|
|
185
|
+
fetch(historyUrl).then(r => r.json()).catch(() => null),
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
const feedItems = (feedResp as { messages?: unknown[] })?.messages ?? [];
|
|
189
|
+
const agentHistory = (historyResp as { results?: unknown[] })?.results ?? [];
|
|
190
|
+
|
|
191
|
+
// Only distill if there's meaningful new content (>3 non-trivial items)
|
|
192
|
+
const significantItems = (feedItems as Array<{ text?: string }>).filter(
|
|
193
|
+
(item) => item.text && !item.text.startsWith("[PERIODIC]") && item.text.length > 20,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
if (significantItems.length > 3) {
|
|
197
|
+
this.logger.info(
|
|
198
|
+
`sinain-hud: distillation check — ${significantItems.length} new feed items since id=${distillState.lastDistilledFeedId}`,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
// Combine feed + agent history as transcript
|
|
202
|
+
const transcript = JSON.stringify([...significantItems, ...agentHistory].slice(0, 100));
|
|
203
|
+
const sessionMeta = JSON.stringify({
|
|
204
|
+
ts: new Date().toISOString(),
|
|
205
|
+
sessionKey: params.sessionSummary,
|
|
206
|
+
feedItemCount: significantItems.length,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Step 1: Distill
|
|
210
|
+
const distillT0 = Date.now();
|
|
211
|
+
const digest = await runPythonScript([
|
|
212
|
+
"sinain-memory/session_distiller.py",
|
|
213
|
+
"--memory-dir", "memory/",
|
|
214
|
+
"--transcript", transcript,
|
|
215
|
+
"--session-meta", sessionMeta,
|
|
216
|
+
], 30_000);
|
|
217
|
+
latencyMs.distillation = Date.now() - distillT0;
|
|
218
|
+
|
|
219
|
+
if (digest && !digest.isEmpty && !digest.error) {
|
|
220
|
+
// Step 2: Integrate into playbook + knowledge graph
|
|
221
|
+
const integrateT0 = Date.now();
|
|
222
|
+
const integration = await runPythonScript([
|
|
223
|
+
"sinain-memory/knowledge_integrator.py",
|
|
224
|
+
"--memory-dir", "memory/",
|
|
225
|
+
"--digest", JSON.stringify(digest),
|
|
226
|
+
], 60_000);
|
|
227
|
+
latencyMs.integration = Date.now() - integrateT0;
|
|
228
|
+
|
|
229
|
+
if (integration) {
|
|
230
|
+
this.logger.info(
|
|
231
|
+
`sinain-hud: knowledge integrated — changes=${JSON.stringify(integration.changes ?? {})}, graph=${JSON.stringify(integration.graphStats ?? {})}`,
|
|
232
|
+
);
|
|
233
|
+
result.distillation = { digest, integration };
|
|
234
|
+
|
|
235
|
+
// Regenerate effective playbook after integration
|
|
236
|
+
this.store.generateEffectivePlaybook();
|
|
237
|
+
|
|
238
|
+
// Render knowledge doc
|
|
239
|
+
this.store.renderKnowledgeDoc();
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
this.logger.info(`sinain-hud: distillation produced empty/error result, skipping integration`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Update watermark — always, even if distillation was empty, to advance past these items
|
|
246
|
+
const maxFeedId = (feedItems as Array<{ id?: number }>).reduce(
|
|
247
|
+
(max, item) => Math.max(max, item.id ?? 0), distillState.lastDistilledFeedId,
|
|
248
|
+
);
|
|
249
|
+
writeDistillState(workspaceDir, {
|
|
250
|
+
lastDistilledFeedId: maxFeedId,
|
|
251
|
+
lastDistilledTs: new Date().toISOString(),
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
} catch (err) {
|
|
255
|
+
this.logger.warn(`sinain-hud: distillation check error: ${String(err)}`);
|
|
155
256
|
}
|
|
156
257
|
|
|
157
258
|
// 3. Insight synthesis (60s timeout)
|
|
@@ -438,20 +539,32 @@ export class CurationEngine {
|
|
|
438
539
|
const moduleGuidance = this.store.getActiveModuleGuidance();
|
|
439
540
|
if (moduleGuidance) contextParts.push(moduleGuidance);
|
|
440
541
|
|
|
441
|
-
// Knowledge
|
|
542
|
+
// Knowledge document (pre-rendered, synchronous — replaces triple_query.py)
|
|
543
|
+
const knowledgeDocPath = join(workspaceDir, "memory", "sinain-knowledge.md");
|
|
442
544
|
try {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
545
|
+
if (existsSync(knowledgeDocPath)) {
|
|
546
|
+
const knowledgeDoc = readFileSync(knowledgeDocPath, "utf-8");
|
|
547
|
+
if (knowledgeDoc.length > 50) {
|
|
548
|
+
contextParts.push(`[KNOWLEDGE]\n${knowledgeDoc}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
} catch {}
|
|
552
|
+
|
|
553
|
+
// Dynamic graph enrichment for situation-specific facts (10s timeout)
|
|
554
|
+
try {
|
|
555
|
+
const situationEntities = this.store.extractSituationEntities();
|
|
556
|
+
if (situationEntities.length > 0) {
|
|
557
|
+
const graphResult = await this.runScript(
|
|
558
|
+
["uv", "run", "--with", "requests", "python3",
|
|
559
|
+
"sinain-memory/graph_query.py",
|
|
560
|
+
"--db", "memory/knowledge-graph.db",
|
|
561
|
+
"--entities", JSON.stringify(situationEntities),
|
|
562
|
+
"--max-facts", "5",
|
|
563
|
+
"--format", "text"],
|
|
564
|
+
{ timeoutMs: 10_000, cwd: workspaceDir },
|
|
565
|
+
);
|
|
566
|
+
if (graphResult.code === 0 && graphResult.stdout.trim().length > 20) {
|
|
567
|
+
contextParts.push(`[SITUATION-SPECIFIC KNOWLEDGE]\n${graphResult.stdout.trim()}`);
|
|
455
568
|
}
|
|
456
569
|
}
|
|
457
570
|
} catch {}
|
|
@@ -49,6 +49,7 @@ export class GitSnapshotStore {
|
|
|
49
49
|
cwd: this.repoPath,
|
|
50
50
|
encoding: "utf-8",
|
|
51
51
|
timeout: 15_000,
|
|
52
|
+
maxBuffer: 10 * 1024 * 1024, // 10 MB — snapshot.json can exceed default 1 MB
|
|
52
53
|
}).trim();
|
|
53
54
|
}
|
|
54
55
|
|
|
@@ -192,9 +193,33 @@ export class GitSnapshotStore {
|
|
|
192
193
|
this.git("commit", "-m", message);
|
|
193
194
|
const hash = this.git("rev-parse", "--short", "HEAD");
|
|
194
195
|
this.logger.info(`sinain-knowledge: snapshot saved → ${hash}`);
|
|
196
|
+
await this.push();
|
|
195
197
|
return hash;
|
|
196
198
|
}
|
|
197
199
|
|
|
200
|
+
// ── Push ─────────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
private async push(): Promise<void> {
|
|
203
|
+
try {
|
|
204
|
+
const remotes = this.git("remote");
|
|
205
|
+
if (!remotes) return;
|
|
206
|
+
|
|
207
|
+
// Use a longer timeout for network operations
|
|
208
|
+
execFileSync("git", ["push", "origin", "HEAD"], {
|
|
209
|
+
cwd: this.repoPath,
|
|
210
|
+
encoding: "utf-8",
|
|
211
|
+
timeout: 30_000,
|
|
212
|
+
stdio: "pipe",
|
|
213
|
+
});
|
|
214
|
+
this.logger.info("sinain-knowledge: snapshot pushed to remote");
|
|
215
|
+
} catch (err) {
|
|
216
|
+
// Warn only if there IS a remote but push failed
|
|
217
|
+
this.logger.warn(
|
|
218
|
+
`sinain-knowledge: push failed (${err instanceof Error ? err.message.split("\n")[0] : String(err)})`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
198
223
|
// ── List ─────────────────────────────────────────────────────────────────
|
|
199
224
|
|
|
200
225
|
/**
|
|
@@ -238,6 +263,7 @@ export class GitSnapshotStore {
|
|
|
238
263
|
const dbBuf = execFileSync("git", ["show", `${ref}:${TRIPLES_FILE}`], {
|
|
239
264
|
cwd: this.repoPath,
|
|
240
265
|
timeout: 15_000,
|
|
266
|
+
maxBuffer: 50 * 1024 * 1024, // 50 MB — triplestore can be large
|
|
241
267
|
});
|
|
242
268
|
snapshot.triplestore = { dbBase64: dbBuf.toString("base64") };
|
|
243
269
|
} catch {
|
|
@@ -485,4 +485,121 @@ export class KnowledgeStore {
|
|
|
485
485
|
writeFileSync(tmpPath, content, "utf-8");
|
|
486
486
|
renameSync(tmpPath, situationPath);
|
|
487
487
|
}
|
|
488
|
+
|
|
489
|
+
// ── Knowledge Document ─────────────────────────────────────────────────
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Render sinain-knowledge.md — the portable knowledge document (<8KB).
|
|
493
|
+
* Combines: playbook (working memory) + top graph facts (long-term memory)
|
|
494
|
+
* + recent session digests + active module guidance.
|
|
495
|
+
*/
|
|
496
|
+
renderKnowledgeDoc(): boolean {
|
|
497
|
+
try {
|
|
498
|
+
const parts: string[] = [];
|
|
499
|
+
const now = new Date().toISOString();
|
|
500
|
+
|
|
501
|
+
parts.push(`# Sinain Knowledge`);
|
|
502
|
+
parts.push(`<!-- exported: ${now}, version: 3 -->\n`);
|
|
503
|
+
|
|
504
|
+
// Playbook (working memory)
|
|
505
|
+
const playbook = this.readPlaybook();
|
|
506
|
+
if (playbook) {
|
|
507
|
+
// Strip header/footer comments for the doc
|
|
508
|
+
const body = playbook
|
|
509
|
+
.split("\n")
|
|
510
|
+
.filter((l) => !l.trim().startsWith("<!--"))
|
|
511
|
+
.join("\n")
|
|
512
|
+
.trim();
|
|
513
|
+
if (body) {
|
|
514
|
+
parts.push(`## Playbook (Working Memory)\n${body}\n`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Recent session digests
|
|
519
|
+
const digestsPath = join(this.workspaceDir, "memory", "session-digests.jsonl");
|
|
520
|
+
if (existsSync(digestsPath)) {
|
|
521
|
+
try {
|
|
522
|
+
const lines = readFileSync(digestsPath, "utf-8")
|
|
523
|
+
.split("\n")
|
|
524
|
+
.filter((l) => l.trim())
|
|
525
|
+
.slice(-5);
|
|
526
|
+
if (lines.length > 0) {
|
|
527
|
+
parts.push(`## Recent Sessions`);
|
|
528
|
+
for (const line of lines) {
|
|
529
|
+
try {
|
|
530
|
+
const d = JSON.parse(line);
|
|
531
|
+
if (d.whatHappened) {
|
|
532
|
+
parts.push(`- ${d.whatHappened}`);
|
|
533
|
+
}
|
|
534
|
+
} catch {}
|
|
535
|
+
}
|
|
536
|
+
parts.push("");
|
|
537
|
+
}
|
|
538
|
+
} catch {}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// Active module guidance (brief)
|
|
542
|
+
const guidance = this.getActiveModuleGuidance();
|
|
543
|
+
if (guidance && guidance.length > 20) {
|
|
544
|
+
// Truncate to keep doc under 8KB
|
|
545
|
+
const truncated = guidance.slice(0, 2000);
|
|
546
|
+
parts.push(`## Active Modules\n${truncated}\n`);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const doc = parts.join("\n").trim() + "\n";
|
|
550
|
+
const docPath = join(this.workspaceDir, "memory", "sinain-knowledge.md");
|
|
551
|
+
const memDir = join(this.workspaceDir, "memory");
|
|
552
|
+
if (!existsSync(memDir)) mkdirSync(memDir, { recursive: true });
|
|
553
|
+
writeFileSync(docPath, doc, "utf-8");
|
|
554
|
+
this.logger.info(`sinain-hud: rendered sinain-knowledge.md (${doc.length} chars)`);
|
|
555
|
+
return true;
|
|
556
|
+
} catch (err) {
|
|
557
|
+
this.logger.warn(`sinain-hud: failed to render knowledge doc: ${String(err)}`);
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Extract entity keywords from SITUATION.md for dynamic graph enrichment.
|
|
564
|
+
* Returns lowercase entity names found in the situation text.
|
|
565
|
+
*/
|
|
566
|
+
extractSituationEntities(): string[] {
|
|
567
|
+
const situation = this.readSituation();
|
|
568
|
+
if (!situation) return [];
|
|
569
|
+
|
|
570
|
+
const entities: Set<string> = new Set();
|
|
571
|
+
|
|
572
|
+
// Extract from "Active Application:" line
|
|
573
|
+
const appMatch = situation.match(/Active Application:\s*(.+)/i);
|
|
574
|
+
if (appMatch) {
|
|
575
|
+
const app = appMatch[1].trim().toLowerCase().replace(/\s+/g, "-");
|
|
576
|
+
if (app.length > 2) entities.add(app);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Extract from "Detected Errors:" section
|
|
580
|
+
const errorMatch = situation.match(/Detected Errors:[\s\S]*?(?=\n##|\n---|\Z)/i);
|
|
581
|
+
if (errorMatch) {
|
|
582
|
+
// Look for common error type patterns
|
|
583
|
+
const errorPatterns = errorMatch[0].matchAll(
|
|
584
|
+
/(?:Error|Exception|TypeError|SyntaxError|ReferenceError|HTTP\s*\d{3}|ENOENT|EACCES|timeout)/gi,
|
|
585
|
+
);
|
|
586
|
+
for (const m of errorPatterns) {
|
|
587
|
+
entities.add(m[0].toLowerCase());
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Extract technology keywords
|
|
592
|
+
const techKeywords = [
|
|
593
|
+
"react-native", "react", "flutter", "swift", "kotlin", "python",
|
|
594
|
+
"typescript", "javascript", "node", "docker", "kubernetes",
|
|
595
|
+
"intellij", "vscode", "xcode", "metro", "gradle", "cocoapods",
|
|
596
|
+
"openrouter", "anthropic", "gemini", "openclaw", "sinain",
|
|
597
|
+
];
|
|
598
|
+
const lowerSituation = situation.toLowerCase();
|
|
599
|
+
for (const kw of techKeywords) {
|
|
600
|
+
if (lowerSituation.includes(kw)) entities.add(kw);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return [...entities].slice(0, 10);
|
|
604
|
+
}
|
|
488
605
|
}
|