@geravant/sinain 1.1.0 → 1.2.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.
@@ -66,8 +66,21 @@ export interface ProfilingMessage {
66
66
  ts: number;
67
67
  }
68
68
 
69
+ /** Overlay → sinain-core: user command to augment next escalation */
70
+ export interface UserCommandMessage {
71
+ type: "user_command";
72
+ text: string;
73
+ }
74
+
69
75
  export type OutboundMessage = FeedMessage | StatusMessage | PingMessage | SpawnTaskMessage;
70
- export type InboundMessage = UserMessage | CommandMessage | PongMessage | ProfilingMessage;
76
+ export type InboundMessage = UserMessage | CommandMessage | PongMessage | ProfilingMessage | UserCommandMessage;
77
+
78
+ /** Abstraction for user commands (text now, voice later). */
79
+ export interface UserCommand {
80
+ text: string;
81
+ ts: number;
82
+ source: "text" | "voice";
83
+ }
71
84
 
72
85
  // ── Feed buffer types ──
73
86
 
@@ -411,7 +424,6 @@ export interface PrivacyConfig {
411
424
  export interface CoreConfig {
412
425
  port: number;
413
426
  audioConfig: AudioPipelineConfig;
414
- audioAltDevice: string;
415
427
  micConfig: AudioPipelineConfig;
416
428
  micEnabled: boolean;
417
429
  transcriptionConfig: TranscriptionConfig;
@@ -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
- // Fire-and-forget: ingest signal into triple store
147
- const tickTs = new Date().toISOString();
148
- runPythonScript([
149
- "sinain-memory/triple_ingest.py",
150
- "--memory-dir", "memory/",
151
- "--tick-ts", tickTs,
152
- "--signal-result", JSON.stringify(signalResult),
153
- "--embed",
154
- ], 15_000).catch(() => {});
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 graph context (10s timeout)
542
+ // Knowledge document (pre-rendered, synchronous — replaces triple_query.py)
543
+ const knowledgeDocPath = join(workspaceDir, "memory", "sinain-knowledge.md");
442
544
  try {
443
- const ragResult = await this.runScript(
444
- ["uv", "run", "--with", "requests", "python3",
445
- "sinain-memory/triple_query.py",
446
- "--memory-dir", "memory",
447
- "--context", "current session",
448
- "--max-chars", "1500"],
449
- { timeoutMs: 10_000, cwd: workspaceDir },
450
- );
451
- if (ragResult.code === 0) {
452
- const parsed = JSON.parse(ragResult.stdout.trim());
453
- if (parsed.context && parsed.context.length > 50) {
454
- contextParts.push(`[KNOWLEDGE GRAPH CONTEXT]\n${parsed.context}`);
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 {}
@@ -193,9 +193,33 @@ export class GitSnapshotStore {
193
193
  this.git("commit", "-m", message);
194
194
  const hash = this.git("rev-parse", "--short", "HEAD");
195
195
  this.logger.info(`sinain-knowledge: snapshot saved → ${hash}`);
196
+ await this.push();
196
197
  return hash;
197
198
  }
198
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
+
199
223
  // ── List ─────────────────────────────────────────────────────────────────
200
224
 
201
225
  /**
@@ -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
  }
@@ -184,26 +184,106 @@ server.tool(
184
184
  },
185
185
  );
186
186
 
187
- // 8. sinain_knowledge_query
187
+ // 8. sinain_get_knowledge
188
+ server.tool(
189
+ "sinain_get_knowledge",
190
+ "Get the portable knowledge document (playbook + long-term facts + recent sessions)",
191
+ {},
192
+ async () => {
193
+ try {
194
+ // Read pre-rendered knowledge doc (fast, no subprocess)
195
+ const docPath = resolve(MEMORY_DIR, "sinain-knowledge.md");
196
+ if (existsSync(docPath)) {
197
+ const content = readFileSync(docPath, "utf-8");
198
+ return textResult(stripPrivateTags(content));
199
+ }
200
+ // Fallback: read playbook directly
201
+ const playbookPath = resolve(MEMORY_DIR, "sinain-playbook.md");
202
+ if (existsSync(playbookPath)) {
203
+ return textResult(stripPrivateTags(readFileSync(playbookPath, "utf-8")));
204
+ }
205
+ return textResult("No knowledge document available yet");
206
+ } catch (err: any) {
207
+ return textResult(`Error reading knowledge: ${err.message}`);
208
+ }
209
+ },
210
+ );
211
+
212
+ // 8b. sinain_knowledge_query (graph query — entity-based lookup)
188
213
  server.tool(
189
214
  "sinain_knowledge_query",
190
- "Query the knowledge graph / memory triples for relevant context",
215
+ "Query the knowledge graph for facts about specific entities/domains",
191
216
  {
192
- context: z.string(),
193
- max_chars: z.number().optional().default(1500),
217
+ entities: z.array(z.string()).optional().default([]),
218
+ max_facts: z.number().optional().default(5),
194
219
  },
195
- async ({ context, max_chars }) => {
220
+ async ({ entities, max_facts }) => {
196
221
  try {
197
- const scriptPath = resolve(SCRIPTS_DIR, "triple_query.py");
198
- const output = await runScript([
199
- scriptPath,
200
- "--memory-dir", MEMORY_DIR,
201
- "--context", context,
202
- "--max-chars", String(max_chars),
203
- ]);
222
+ const dbPath = resolve(MEMORY_DIR, "knowledge-graph.db");
223
+ const scriptPath = resolve(SCRIPTS_DIR, "graph_query.py");
224
+ const args = [scriptPath, "--db", dbPath, "--max-facts", String(max_facts)];
225
+ if (entities.length > 0) {
226
+ args.push("--entities", JSON.stringify(entities));
227
+ }
228
+ const output = await runScript(args);
204
229
  return textResult(stripPrivateTags(output));
205
230
  } catch (err: any) {
206
- return textResult(`Error querying knowledge: ${err.message}`);
231
+ return textResult(`Error querying graph: ${err.message}`);
232
+ }
233
+ },
234
+ );
235
+
236
+ // 8c. sinain_distill_session
237
+ server.tool(
238
+ "sinain_distill_session",
239
+ "Distill the current session into knowledge (playbook updates + graph facts)",
240
+ {
241
+ session_summary: z.string().optional().default("Bare agent session distillation"),
242
+ },
243
+ async ({ session_summary }) => {
244
+ const results: string[] = [];
245
+
246
+ try {
247
+ // Fetch feed items from sinain-core
248
+ const coreUrl = process.env.SINAIN_CORE_URL || "http://localhost:9500";
249
+ const feedResp = await fetch(`${coreUrl}/feed?after=0`).then(r => r.json());
250
+ const historyResp = await fetch(`${coreUrl}/agent/history?limit=10`).then(r => r.json());
251
+
252
+ const feedItems = (feedResp as any).messages ?? [];
253
+ const agentHistory = (historyResp as any).results ?? [];
254
+
255
+ if (feedItems.length < 3) {
256
+ return textResult("Not enough feed items to distill (need >3)");
257
+ }
258
+
259
+ // Step 1: Distill
260
+ const transcript = JSON.stringify([...feedItems, ...agentHistory].slice(0, 100));
261
+ const meta = JSON.stringify({ ts: new Date().toISOString(), sessionKey: session_summary });
262
+
263
+ const distillOutput = await runScript([
264
+ resolve(SCRIPTS_DIR, "session_distiller.py"),
265
+ "--memory-dir", MEMORY_DIR,
266
+ "--transcript", transcript,
267
+ "--session-meta", meta,
268
+ ], 30_000);
269
+ results.push(`[session_distiller] ${distillOutput.trim().slice(0, 500)}`);
270
+
271
+ const digest = JSON.parse(distillOutput.trim());
272
+ if (digest.isEmpty || digest.error) {
273
+ return textResult(`Distillation skipped: ${digest.error || "empty session"}`);
274
+ }
275
+
276
+ // Step 2: Integrate
277
+ const integrateOutput = await runScript([
278
+ resolve(SCRIPTS_DIR, "knowledge_integrator.py"),
279
+ "--memory-dir", MEMORY_DIR,
280
+ "--digest", JSON.stringify(digest),
281
+ ], 60_000);
282
+ results.push(`[knowledge_integrator] ${integrateOutput.trim().slice(0, 500)}`);
283
+
284
+ return textResult(stripPrivateTags(results.join("\n\n")));
285
+ } catch (err: any) {
286
+ return textResult(`Distillation error: ${err.message}`);
207
287
  }
208
288
  },
209
289
  );
@@ -287,7 +367,22 @@ server.tool(
287
367
  },
288
368
  );
289
369
 
290
- // 10. sinain_module_guidance
370
+ // 10. sinain_user_command
371
+ server.tool(
372
+ "sinain_user_command",
373
+ "Queue a user command to augment the next escalation context (forces escalation on next agent tick)",
374
+ { text: z.string().describe("The command text to inject into the next escalation") },
375
+ async ({ text }) => {
376
+ try {
377
+ const data = await coreRequest("POST", "/user/command", { text });
378
+ return textResult(JSON.stringify(data, null, 2));
379
+ } catch (err: any) {
380
+ return textResult(`Error queuing user command: ${err.message}`);
381
+ }
382
+ },
383
+ );
384
+
385
+ // 11. sinain_module_guidance
291
386
  server.tool(
292
387
  "sinain_module_guidance",
293
388
  "Read guidance from all active modules in the workspace",