@geravant/sinain 1.10.1 → 1.12.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.
Files changed (44) hide show
  1. package/package.json +1 -1
  2. package/sinain-agent/CLAUDE.md +1 -1
  3. package/sinain-agent/run.sh +66 -7
  4. package/sinain-core/src/agent/analyzer.ts +4 -27
  5. package/sinain-core/src/agent/loop.ts +10 -40
  6. package/sinain-core/src/agent/situation-writer.ts +0 -16
  7. package/sinain-core/src/config.ts +1 -9
  8. package/sinain-core/src/escalation/escalator.ts +44 -16
  9. package/sinain-core/src/escalation/message-builder.ts +45 -118
  10. package/sinain-core/src/index.ts +20 -36
  11. package/sinain-core/src/learning/local-curation.ts +4 -4
  12. package/sinain-core/src/overlay/commands.ts +46 -13
  13. package/sinain-core/src/overlay/ws-handler.ts +13 -1
  14. package/sinain-core/src/server.ts +121 -0
  15. package/sinain-core/src/types.ts +25 -28
  16. package/sinain-mcp-server/index.ts +28 -0
  17. package/sinain-memory/__pycache__/graph_query.cpython-312.pyc +0 -0
  18. package/sinain-memory/__pycache__/triplestore.cpython-312.pyc +0 -0
  19. package/sinain-memory/eval/__pycache__/__init__.cpython-312.pyc +0 -0
  20. package/sinain-memory/eval/assertions.py +0 -21
  21. package/sinain-memory/eval/benchmarks/__init__.py +0 -0
  22. package/sinain-memory/eval/benchmarks/__pycache__/__init__.cpython-312.pyc +0 -0
  23. package/sinain-memory/eval/benchmarks/__pycache__/base_adapter.cpython-312.pyc +0 -0
  24. package/sinain-memory/eval/benchmarks/__pycache__/config.cpython-312.pyc +0 -0
  25. package/sinain-memory/eval/benchmarks/__pycache__/evaluate.cpython-312.pyc +0 -0
  26. package/sinain-memory/eval/benchmarks/__pycache__/ingest.cpython-312.pyc +0 -0
  27. package/sinain-memory/eval/benchmarks/__pycache__/longmemeval_adapter.cpython-312.pyc +0 -0
  28. package/sinain-memory/eval/benchmarks/__pycache__/query.cpython-312.pyc +0 -0
  29. package/sinain-memory/eval/benchmarks/__pycache__/report.cpython-312.pyc +0 -0
  30. package/sinain-memory/eval/benchmarks/__pycache__/runner.cpython-312.pyc +0 -0
  31. package/sinain-memory/eval/benchmarks/base_adapter.py +43 -0
  32. package/sinain-memory/eval/benchmarks/config.py +23 -0
  33. package/sinain-memory/eval/benchmarks/evaluate.py +146 -0
  34. package/sinain-memory/eval/benchmarks/ingest.py +152 -0
  35. package/sinain-memory/eval/benchmarks/judges/__init__.py +0 -0
  36. package/sinain-memory/eval/benchmarks/judges/__pycache__/__init__.cpython-312.pyc +0 -0
  37. package/sinain-memory/eval/benchmarks/judges/__pycache__/qa_judge.cpython-312.pyc +0 -0
  38. package/sinain-memory/eval/benchmarks/judges/qa_judge.py +81 -0
  39. package/sinain-memory/eval/benchmarks/longmemeval_adapter.py +177 -0
  40. package/sinain-memory/eval/benchmarks/query.py +172 -0
  41. package/sinain-memory/eval/benchmarks/report.py +87 -0
  42. package/sinain-memory/eval/benchmarks/runner.py +276 -0
  43. package/sinain-memory/koog-config.json +11 -0
  44. package/sinain-core/src/agent/traits.ts +0 -520
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geravant/sinain",
3
- "version": "1.10.1",
3
+ "version": "1.12.0",
4
4
  "description": "Ambient intelligence that sees what you see, hears what you hear, and acts on your behalf",
5
5
  "type": "module",
6
6
  "bin": {
@@ -103,7 +103,7 @@ Knowledge API (localhost:9500) ← merges both DBs ← queries
103
103
 
104
104
  ### Using Knowledge in Escalation Responses
105
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.
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
107
 
108
108
  ## Spawning Background Tasks
109
109
 
@@ -69,11 +69,22 @@ invoke_agent() {
69
69
  case "$AGENT" in
70
70
  claude)
71
71
  local turns="${2:-$AGENT_MAX_TURNS}"
72
- claude --enable-auto-mode \
73
- --mcp-config "$MCP_CONFIG" \
74
- ${ALLOWED_TOOLS:+--allowedTools $ALLOWED_TOOLS} \
75
- --max-turns "$turns" --output-format text \
76
- -p "$prompt"
72
+ if [ -n "${SINAIN_SPAWN:-}" ]; then
73
+ # Spawn: PreToolUse hook routes permission prompts to overlay HUD
74
+ claude \
75
+ --mcp-config "$MCP_CONFIG" \
76
+ --settings "$SCRIPT_DIR/.claude/settings.json" \
77
+ ${ALLOWED_TOOLS:+--allowedTools $ALLOWED_TOOLS} \
78
+ --max-turns "$turns" --output-format text \
79
+ -p "$prompt"
80
+ else
81
+ # Escalation: auto-approve for speed (short-lived, read-heavy)
82
+ claude --enable-auto-mode \
83
+ --mcp-config "$MCP_CONFIG" \
84
+ ${ALLOWED_TOOLS:+--allowedTools $ALLOWED_TOOLS} \
85
+ --max-turns "$turns" --output-format text \
86
+ -p "$prompt"
87
+ fi
77
88
  ;;
78
89
  codex)
79
90
  codex exec -s danger-full-access \
@@ -97,6 +108,8 @@ invoke_agent() {
97
108
  local turns="${2:-$AGENT_MAX_TURNS}"
98
109
  GOOSE_MODE=auto goose run --text "$prompt" \
99
110
  --output-format text \
111
+ --quiet \
112
+ --no-session \
100
113
  --max-turns "$turns"
101
114
  ;;
102
115
  aider)
@@ -186,6 +199,34 @@ if [ "$AGENT" = "codex" ]; then
186
199
  fi
187
200
  fi
188
201
 
202
+ # Goose: auto-register sinain MCP server in config.yaml if not present
203
+ if [ "$AGENT" = "goose" ]; then
204
+ TSX_BIN="$(cd "$SCRIPT_DIR/.." && pwd)/sinain-core/node_modules/.bin/tsx"
205
+ MCP_ENTRY="$(cd "$SCRIPT_DIR/.." && pwd)/sinain-mcp-server/index.ts"
206
+ GOOSE_CONFIG="${HOME}/.config/goose/config.yaml"
207
+ if [ -f "$GOOSE_CONFIG" ] && ! grep -q "sinain:" "$GOOSE_CONFIG" 2>/dev/null; then
208
+ echo "Registering sinain MCP server with goose..."
209
+ python3 -c "
210
+ import yaml, os, sys
211
+ config_path = sys.argv[1]
212
+ with open(config_path) as f:
213
+ cfg = yaml.safe_load(f) or {}
214
+ cfg.setdefault('extensions', {})['sinain'] = {
215
+ 'name': 'Sinain MCP Server',
216
+ 'cmd': sys.argv[2],
217
+ 'args': [sys.argv[3]],
218
+ 'enabled': True,
219
+ 'envs': {'SINAIN_CORE_URL': sys.argv[4], 'SINAIN_WORKSPACE': sys.argv[5]},
220
+ 'type': 'stdio',
221
+ 'timeout': 300,
222
+ }
223
+ with open(config_path, 'w') as f:
224
+ yaml.dump(cfg, f, default_flow_style=False)
225
+ print(' sinain extension added to ' + config_path)
226
+ " "$GOOSE_CONFIG" "$TSX_BIN" "$MCP_ENTRY" "$CORE_URL" "$WORKSPACE"
227
+ fi
228
+ fi
229
+
189
230
  # Agent mode label
190
231
  if agent_has_mcp; then
191
232
  AGENT_MODE="MCP"
@@ -234,6 +275,11 @@ Knowledge context: sinain-core maintains two knowledge databases — local (sess
234
275
  while true; do
235
276
  # Poll for pending escalation
236
277
  ESC=$(curl -sf "$CORE_URL/escalation/pending" 2>/dev/null || echo '{"ok":false}')
278
+ ESC_PAUSED=$(echo "$ESC" | python3 -c "import sys,json; d=json.load(sys.stdin); print('true' if d.get('paused') else '')" 2>/dev/null || true)
279
+ if [ -n "$ESC_PAUSED" ]; then
280
+ sleep 10 # Slow polling when paused
281
+ continue
282
+ fi
237
283
  ESC_ID=$(echo "$ESC" | python3 -c "import sys,json; d=json.load(sys.stdin); e=d.get('escalation'); print(e['id'] if e else '')" 2>/dev/null || true)
238
284
 
239
285
  if [ -n "$ESC_ID" ]; then
@@ -274,10 +320,23 @@ while true; do
274
320
 
275
321
  if agent_has_mcp; then
276
322
  # MCP path: agent runs task with sinain tools available
323
+ # Pre-fetch knowledge context so the spawn doesn't waste turns calling tools
324
+ SPAWN_KNOWLEDGE=$(curl -sf "$CORE_URL/knowledge" 2>/dev/null | python3 -c "
325
+ import sys, json
326
+ d = json.load(sys.stdin)
327
+ k = d.get('knowledge', '')
328
+ # Trim to 2000 chars to avoid prompt bloat
329
+ print(k[:2000])
330
+ " 2>/dev/null || true)
277
331
  SPAWN_PROMPT="You have a background task to complete. Task: $SPAWN_TASK
278
-
279
- Complete this task thoroughly. Use sinain_get_knowledge and sinain_knowledge_query if you need context from past sessions. Summarize your findings concisely."
332
+ ${SPAWN_KNOWLEDGE:+
333
+ ## Knowledge Context
334
+ $SPAWN_KNOWLEDGE
335
+ }
336
+ Complete this task thoroughly. You also have sinain_get_knowledge and sinain_knowledge_query tools available for additional context. Summarize your findings concisely."
337
+ export SINAIN_SPAWN=1 SINAIN_SPAWN_TASK_ID="$SPAWN_ID"
280
338
  SPAWN_RESULT=$(invoke_agent "$SPAWN_PROMPT" "$SPAWN_MAX_TURNS" || echo "ERROR: agent invocation failed")
339
+ unset SINAIN_SPAWN SINAIN_SPAWN_TASK_ID
281
340
  else
282
341
  # Pipe path: agent gets task text directly
283
342
  SPAWN_RESULT=$(invoke_pipe "Background task: $SPAWN_TASK" || echo "No output")
@@ -53,13 +53,12 @@ You produce outputs as JSON.
53
53
  Respond ONLY with valid JSON. No markdown, no code fences, no explanation.
54
54
  Your entire response must be parseable by JSON.parse().
55
55
 
56
- {"hud":"...","digest":"...","record":{"command":"start"|"stop","label":"..."},"task":"..."}
56
+ {"hud":"...","digest":"...","record":{"command":"start"|"stop","label":"..."}}
57
57
 
58
58
  Output fields:
59
59
  - "hud" (required): max 60 words describing what user is doing NOW
60
60
  - "digest" (required): 5-8 sentences with detailed activity description
61
61
  - "record" (optional): control recording — {"command":"start","label":"Meeting name"} or {"command":"stop"}
62
- - "task" (optional): natural language instruction to spawn a background task
63
62
 
64
63
  When to use "record":
65
64
  - START when user begins a meeting, call, lecture, YouTube video, or important audio content
@@ -67,25 +66,6 @@ When to use "record":
67
66
  - Provide descriptive labels like "Team standup", "Client call", "YouTube: [video title from OCR]"
68
67
  - For YouTube/video content: extract video title from screen OCR for the label
69
68
 
70
- When to use "task":
71
- - User explicitly asks for research, lookup, or action
72
- - Something needs external search or processing that isn't a real-time response
73
- - Example: "Search for React 19 migration guide", "Find docs for this API"
74
-
75
- When to spawn "task" for video content:
76
- - If user watches a YouTube video for 2+ minutes AND no task has been spawned for this video yet, spawn: "Summarize YouTube video: [title or URL from OCR]"
77
- - ONLY spawn ONCE per video - do not repeat spawn for the same video in subsequent ticks
78
- - Extract video title or URL from screen OCR to include in the task
79
-
80
- When to spawn "task" for coding problems:
81
- - If user is actively working on a coding problem/challenge for 1+ minutes:
82
- - Spawn: "Solve coding problem: [problem description/title from OCR]"
83
- - This includes LeetCode, HackerRank, interviews, coding assessments, or any visible coding challenge
84
- - Look for problem signals: "Input:", "Output:", "Example", "Constraints:", problem titles, test cases
85
- - Include as much context as possible from the screen OCR (problem description, examples, constraints)
86
- - ONLY spawn ONCE per distinct problem - do not repeat for the same problem
87
- - The spawned task should provide a complete solution with code and explanation
88
-
89
69
  Audio sources: [\ud83d\udd0a]=system/speaker audio, [\ud83c\udf99]=microphone (user's voice).
90
70
  Treat [\ud83c\udf99] as direct user speech. Treat [\ud83d\udd0a] as external audio.
91
71
 
@@ -95,7 +75,7 @@ Rules:
95
75
  - If nothing is happening, hud="Idle" and digest explains what was last seen.
96
76
  - Include specific filenames, URLs, error messages, UI text from OCR in digest.
97
77
  - Do NOT suggest actions in digest — just describe the situation factually.
98
- - Only include "record" or "task" when genuinely appropriate — most responses won't have them.
78
+ - Only include "record" when genuinely appropriate — most responses won't have it.
99
79
  - CRITICAL: Output ONLY the JSON object, nothing else.`;
100
80
 
101
81
  /**
@@ -210,7 +190,6 @@ export async function analyzeContext(
210
190
  contextWindow: ContextWindow,
211
191
  config: AnalysisConfig,
212
192
  recorderStatus: RecorderStatus | null = null,
213
- traitSystemPrompt?: string,
214
193
  ): Promise<AgentResult> {
215
194
  const userPrompt = buildUserPrompt(contextWindow, recorderStatus);
216
195
 
@@ -221,10 +200,8 @@ export async function analyzeContext(
221
200
  if (levelFor("screen_images", privacyDest) === "none") images = [];
222
201
  } catch { /* privacy not initialized, keep images */ }
223
202
 
224
- const systemPrompt = traitSystemPrompt ?? SYSTEM_PROMPT;
225
-
226
203
  if (config.provider === "ollama") {
227
- return await callOllama(systemPrompt, userPrompt, images, config);
204
+ return await callOllama(SYSTEM_PROMPT, userPrompt, images, config);
228
205
  }
229
206
 
230
207
  // OpenRouter path: model chain with fallbacks
@@ -241,7 +218,7 @@ export async function analyzeContext(
241
218
  let lastError: Error | null = null;
242
219
  for (const model of models) {
243
220
  try {
244
- return await callOpenRouter(systemPrompt, userPrompt, images, model, config);
221
+ return await callOpenRouter(SYSTEM_PROMPT, userPrompt, images, model, config);
245
222
  } catch (err: any) {
246
223
  lastError = err;
247
224
  log(TAG, `model ${model} failed: ${err.message || err}, trying next...`);
@@ -10,8 +10,6 @@ import { analyzeContext } from "./analyzer.js";
10
10
  import { writeSituationMd } from "./situation-writer.js";
11
11
  import { calculateEscalationScore } from "../escalation/scorer.js";
12
12
  import { log, warn, error, debug } from "../log.js";
13
- import type { TraitEngine, TraitSelection } from "./traits.js";
14
- import { writeTraitLog } from "./traits.js";
15
13
 
16
14
  const TAG = "agent";
17
15
 
@@ -33,10 +31,6 @@ export interface AgentLoopDeps {
33
31
  profiler?: Profiler;
34
32
  /** Called after each successful SITUATION.md write with the content string. */
35
33
  onSituationUpdate?: (content: string) => void;
36
- /** Optional trait engine for personality voice selection. */
37
- traitEngine?: TraitEngine;
38
- /** Directory to write per-day trait log JSONL files. */
39
- traitLogDir?: string;
40
34
  /** Optional: path to sinain-knowledge.md for startup recap. */
41
35
  getKnowledgeDocPath?: () => string | null;
42
36
  /** Optional: feedback store for startup recap context. */
@@ -283,14 +277,6 @@ export class AgentLoop extends EventEmitter {
283
277
  traceCtx?.startSpan("llm-call");
284
278
  const recorderStatus = this.deps.getRecorderStatus?.() ?? null;
285
279
 
286
- // Trait selection: pick the best personality voice for this tick
287
- let traitSelection: TraitSelection | null = null;
288
- if (this.deps.traitEngine?.enabled) {
289
- const ocrText = contextWindow.screen.map(e => e.ocr ?? "").join(" ");
290
- const audioText = contextWindow.audio.map(e => e.text).join(" ");
291
- traitSelection = this.deps.traitEngine.selectTrait(ocrText, audioText);
292
- }
293
-
294
280
  const result = await analyzeContext(contextWindow, this.deps.agentConfig, recorderStatus);
295
281
  this.deps.profiler?.timerRecord("agent.llmCall", result.latencyMs);
296
282
  traceCtx?.endSpan({ model: result.model, tokensIn: result.tokensIn, tokensOut: result.tokensOut, latencyMs: result.latencyMs });
@@ -346,11 +332,6 @@ export class AgentLoop extends EventEmitter {
346
332
  screenCount: contextWindow.screenCount,
347
333
  },
348
334
  };
349
- if (traitSelection) {
350
- entry.voice = traitSelection.trait.name;
351
- entry.voice_stat = traitSelection.stat;
352
- entry.voice_confidence = traitSelection.confidence;
353
- }
354
335
  this.agentBuffer.push(entry);
355
336
  const historyLimit = this.deps.agentConfig.historyLimit || 50;
356
337
  if (this.agentBuffer.length > historyLimit) this.agentBuffer.shift();
@@ -362,17 +343,22 @@ export class AgentLoop extends EventEmitter {
362
343
  debug(TAG, `#${entry.id} (${latencyMs}ms) hud unchanged`);
363
344
  }
364
345
 
365
- // Push HUD line to feed (suppress "—", "Idle", and all in focus mode)
346
+ // Always push HUD to feed buffer for data capture (curation pipeline reads this)
366
347
  if (this.deps.agentConfig.pushToFeed &&
367
- this.deps.escalationMode !== "focus" &&
368
- this.deps.escalationMode !== "rich" &&
369
348
  hud !== "\u2014" && hud !== "Idle" && hud !== this.lastPushedHud) {
370
349
  feedBuffer.push(`[\ud83e\udde0] ${hud}`, "normal", "agent", "stream");
371
- this.deps.onHudUpdate(`[\ud83e\udde0] ${hud}`);
372
350
  this.lastPushedHud = hud;
373
351
  entry.pushed = true;
374
352
  }
375
353
 
354
+ // Broadcast to overlay only when NOT in focus/rich mode
355
+ // (in those modes, the overlay gets updates via escalation instead)
356
+ if (entry.pushed &&
357
+ this.deps.escalationMode !== "focus" &&
358
+ this.deps.escalationMode !== "rich") {
359
+ this.deps.onHudUpdate(`[\ud83e\udde0] ${hud}`);
360
+ }
361
+
376
362
  // Store digest
377
363
  this.latestDigest = entry;
378
364
 
@@ -380,7 +366,7 @@ export class AgentLoop extends EventEmitter {
380
366
  const escalationScore = calculateEscalationScore(digest, contextWindow);
381
367
 
382
368
  // Write SITUATION.md (enhanced with escalation context and recorder status)
383
- const situationContent = writeSituationMd(this.deps.situationMdPath, contextWindow, digest, entry, escalationScore, recorderStatus, traitSelection);
369
+ const situationContent = writeSituationMd(this.deps.situationMdPath, contextWindow, digest, entry, escalationScore, recorderStatus);
384
370
  this.deps.onSituationUpdate?.(situationContent);
385
371
 
386
372
  // Notify for escalation check
@@ -406,22 +392,6 @@ export class AgentLoop extends EventEmitter {
406
392
  hudChanged: entry.pushed,
407
393
  });
408
394
 
409
- // Fire-and-forget trait log
410
- if (this.deps.traitEngine?.enabled && this.deps.traitLogDir) {
411
- writeTraitLog(this.deps.traitLogDir, {
412
- ts: new Date().toISOString(),
413
- tickId: entry.id,
414
- enabled: true,
415
- voice: traitSelection?.trait.name ?? "none",
416
- voice_stat: traitSelection?.stat ?? 0,
417
- voice_confidence: traitSelection?.confidence ?? 0,
418
- activation_scores: traitSelection?.allScores ?? {},
419
- context_app: contextWindow.currentApp,
420
- hud_length: entry.hud.length,
421
- synthesis: false,
422
- }).catch(() => {});
423
- }
424
-
425
395
  } catch (err: any) {
426
396
  error(TAG, "tick error:", err.message || err);
427
397
  traceCtx?.endSpan({ status: "error", error: err.message });
@@ -2,7 +2,6 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import type { ContextWindow, AgentEntry, RecorderStatus } from "../types.js";
4
4
  import type { EscalationScore } from "../escalation/scorer.js";
5
- import type { TraitSelection } from "./traits.js";
6
5
  import { normalizeAppName } from "./context-window.js";
7
6
  import { log, error } from "../log.js";
8
7
 
@@ -46,7 +45,6 @@ export function writeSituationMd(
46
45
  entry: AgentEntry,
47
46
  escalationScore?: EscalationScore,
48
47
  recorderStatus?: RecorderStatus | null,
49
- traitSelection?: TraitSelection | null,
50
48
  ): string {
51
49
  const dir = path.dirname(situationMdPath);
52
50
  const tmpPath = situationMdPath + ".tmp";
@@ -151,20 +149,6 @@ export function writeSituationMd(
151
149
  lines.push("");
152
150
  }
153
151
 
154
- // Trait voice: frame downstream agent's perspective
155
- if (traitSelection) {
156
- const { trait, stat } = traitSelection;
157
- const voiceFlavor = stat >= 7 ? trait.voice_high : stat <= 2 ? trait.voice_low : trait.description;
158
- lines.push("## Active Voice");
159
- lines.push("");
160
- lines.push(`- Trait: ${trait.name} (stat ${stat}/10)`);
161
- lines.push(`- Tagline: ${trait.tagline}`);
162
- lines.push(`- Voice: "${voiceFlavor}"`);
163
- lines.push("");
164
- lines.push("> Frame your response through this lens naturally. Do not name the trait explicitly.");
165
- lines.push("");
166
- }
167
-
168
152
  lines.push("## Metadata");
169
153
  lines.push("");
170
154
  lines.push(`- Screen events in window: ${contextWindow.screenCount}`);
@@ -2,7 +2,7 @@ import { readFileSync, existsSync } from "node:fs";
2
2
  import { resolve, dirname } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import os from "node:os";
5
- import type { CoreConfig, AudioPipelineConfig, TranscriptionConfig, AnalysisConfig, EscalationConfig, OpenClawConfig, EscalationMode, EscalationTransport, LearningConfig, TraitConfig, PrivacyConfig, PrivacyMatrix, PrivacyLevel, PrivacyRow } from "./types.js";
5
+ import type { CoreConfig, AudioPipelineConfig, TranscriptionConfig, AnalysisConfig, EscalationConfig, OpenClawConfig, EscalationMode, EscalationTransport, LearningConfig, PrivacyConfig, PrivacyMatrix, PrivacyLevel, PrivacyRow } from "./types.js";
6
6
  import { PRESETS } from "./privacy/presets.js";
7
7
 
8
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -234,13 +234,6 @@ export function loadConfig(): CoreConfig {
234
234
  retentionDays: intEnv("FEEDBACK_RETENTION_DAYS", 30),
235
235
  };
236
236
 
237
- const traitConfig: TraitConfig = {
238
- enabled: boolEnv("TRAITS_ENABLED", false),
239
- configPath: resolvePath(env("TRAITS_CONFIG", "~/.sinain/traits.json")),
240
- entropyHigh: boolEnv("TRAIT_ENTROPY_HIGH", false),
241
- logDir: resolvePath(env("TRAIT_LOG_DIR", "~/.sinain-core/traits")),
242
- };
243
-
244
237
  const privacyConfig = loadPrivacyConfig();
245
238
 
246
239
  return {
@@ -257,7 +250,6 @@ export function loadConfig(): CoreConfig {
257
250
  traceDir: resolvePath(env("TRACE_DIR", resolve(sinainDataDir(), "traces"))),
258
251
  costDisplayEnabled: boolEnv("COST_DISPLAY_ENABLED", false),
259
252
  learningConfig,
260
- traitConfig,
261
253
  privacyConfig,
262
254
  };
263
255
  }
@@ -237,6 +237,7 @@ export class Escalator {
237
237
  escalationReason,
238
238
  undefined,
239
239
  this.pendingUserCommand ?? undefined,
240
+ this.deps.wsHandler.getState().responseSize ?? "medium",
240
241
  );
241
242
 
242
243
  // Clear user command after building the message (consumed once)
@@ -520,11 +521,11 @@ ${recentLines.join("\n")}`;
520
521
  message: task,
521
522
  sessionKey: childSessionKey,
522
523
  lane: "subagent",
523
- extraSystemPrompt: this.buildChildSystemPrompt(task, label),
524
+ extraSystemPrompt: await this.buildChildSystemPrompt(task, label),
524
525
  deliver: false,
525
526
  idempotencyKey: idemKey,
526
527
  label: label || undefined,
527
- }, 45_000, { expectFinal: true });
528
+ }, 10 * 60_000, { expectFinal: true });
528
529
 
529
530
  log(TAG, `spawn-task RPC response: ${JSON.stringify(result).slice(0, 500)}`);
530
531
  this.stats.totalSpawnResponses++;
@@ -581,8 +582,44 @@ ${recentLines.join("\n")}`;
581
582
  }
582
583
  }
583
584
 
584
- /** Build a focused system prompt for the child subagent. */
585
- private buildChildSystemPrompt(task: string, label?: string): string {
585
+ /** Build a focused system prompt for the child subagent, enriched with knowledge. */
586
+ private async buildChildSystemPrompt(task: string, label?: string): Promise<string> {
587
+ // Fetch relevant knowledge facts (same enrichment as escalations)
588
+ let knowledgeSection = "";
589
+ if (this.deps.queryKnowledgeFacts) {
590
+ try {
591
+ const entities: string[] = [];
592
+ // Extract keywords from task text for entity matching
593
+ const techKeywords = [
594
+ "react-native", "react", "flutter", "swift", "kotlin", "python",
595
+ "typescript", "node", "docker", "sinain", "openclaw", "overlay",
596
+ "sense", "audio", "transcription", "escalation", "knowledge",
597
+ ];
598
+ const lower = task.toLowerCase();
599
+ for (const kw of techKeywords) {
600
+ if (lower.includes(kw)) entities.push(kw);
601
+ }
602
+ // Also extract capitalized proper nouns (file names, project names)
603
+ const nouns = task.match(/\b[A-Z][a-z]{2,}\b/g);
604
+ if (nouns) entities.push(...nouns.map(n => n.toLowerCase()).slice(0, 3));
605
+
606
+ if (entities.length > 0) {
607
+ const facts = await this.deps.queryKnowledgeFacts(entities.slice(0, 5), 5);
608
+ if (facts && facts.trim().length > 20) {
609
+ knowledgeSection = `\n## Relevant Knowledge\n${facts.trim()}`;
610
+ }
611
+ }
612
+ } catch (err) {
613
+ log(TAG, `spawn knowledge enrichment failed: ${String(err)}`);
614
+ }
615
+ }
616
+
617
+ // Include latest digest for screen/audio context
618
+ const latestDigest = this.deps.feedBuffer.latest()?.text;
619
+ const contextSection = latestDigest
620
+ ? `\n## Current User Context\n${latestDigest.slice(0, 500)}`
621
+ : "";
622
+
586
623
  return [
587
624
  "# Subagent Context",
588
625
  "",
@@ -596,8 +633,9 @@ ${recentLines.join("\n")}`;
596
633
  "1. Stay focused — do your assigned task, nothing else",
597
634
  "2. Your final message will be reported to the requester",
598
635
  "3. Be concise but informative",
599
- "",
600
- label ? `Label: ${label}` : "",
636
+ label ? `\nLabel: ${label}` : "",
637
+ knowledgeSection,
638
+ contextSection,
601
639
  ].filter(Boolean).join("\n");
602
640
  }
603
641
 
@@ -633,19 +671,9 @@ ${recentLines.join("\n")}`;
633
671
  return;
634
672
  }
635
673
 
636
- const maxWaitMs = 5 * 60 * 1000; // 5 minutes
637
674
  const pollIntervalMs = 5000; // 5 seconds
638
675
 
639
676
  const poll = async (): Promise<void> => {
640
- const elapsed = Date.now() - task.startedAt;
641
- if (elapsed > maxWaitMs) {
642
- log(TAG, `spawn-task timeout: taskId=${taskId}`);
643
- this.broadcastTaskEvent(taskId, "timeout", task.label, task.startedAt);
644
- this.pendingSpawnTasks.delete(taskId);
645
- savePendingTasks(this.pendingSpawnTasks);
646
- this.finishPoll();
647
- return;
648
- }
649
677
 
650
678
  if (!this.wsClient.isConnected) {
651
679
  // Retry later