@geravant/sinain 1.10.1 → 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 +1 -1
- package/sinain-agent/run.sh +66 -7
- 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 +20 -36
- package/sinain-core/src/learning/local-curation.ts +4 -4
- 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 +121 -0
- package/sinain-core/src/types.ts +22 -28
- package/sinain-mcp-server/index.ts +28 -0
- package/sinain-memory/eval/assertions.py +0 -21
- package/sinain-core/src/agent/traits.ts +0 -520
- package/sinain-memory/__pycache__/common.cpython-312.pyc +0 -0
- package/sinain-memory/__pycache__/knowledge_integrator.cpython-312.pyc +0 -0
- package/sinain-memory/__pycache__/session_distiller.cpython-312.pyc +0 -0
- package/sinain-memory/__pycache__/triplestore.cpython-312.pyc +0 -0
package/package.json
CHANGED
package/sinain-agent/CLAUDE.md
CHANGED
|
@@ -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(
|
|
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
|
|
package/sinain-agent/run.sh
CHANGED
|
@@ -69,11 +69,22 @@ invoke_agent() {
|
|
|
69
69
|
case "$AGENT" in
|
|
70
70
|
claude)
|
|
71
71
|
local turns="${2:-$AGENT_MAX_TURNS}"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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":"..."}
|
|
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"
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
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
|
|
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,
|
|
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
|
}
|
|
@@ -520,11 +520,11 @@ ${recentLines.join("\n")}`;
|
|
|
520
520
|
message: task,
|
|
521
521
|
sessionKey: childSessionKey,
|
|
522
522
|
lane: "subagent",
|
|
523
|
-
extraSystemPrompt: this.buildChildSystemPrompt(task, label),
|
|
523
|
+
extraSystemPrompt: await this.buildChildSystemPrompt(task, label),
|
|
524
524
|
deliver: false,
|
|
525
525
|
idempotencyKey: idemKey,
|
|
526
526
|
label: label || undefined,
|
|
527
|
-
},
|
|
527
|
+
}, 10 * 60_000, { expectFinal: true });
|
|
528
528
|
|
|
529
529
|
log(TAG, `spawn-task RPC response: ${JSON.stringify(result).slice(0, 500)}`);
|
|
530
530
|
this.stats.totalSpawnResponses++;
|
|
@@ -581,8 +581,44 @@ ${recentLines.join("\n")}`;
|
|
|
581
581
|
}
|
|
582
582
|
}
|
|
583
583
|
|
|
584
|
-
/** Build a focused system prompt for the child subagent. */
|
|
585
|
-
private buildChildSystemPrompt(task: string, label?: string): string {
|
|
584
|
+
/** Build a focused system prompt for the child subagent, enriched with knowledge. */
|
|
585
|
+
private async buildChildSystemPrompt(task: string, label?: string): Promise<string> {
|
|
586
|
+
// Fetch relevant knowledge facts (same enrichment as escalations)
|
|
587
|
+
let knowledgeSection = "";
|
|
588
|
+
if (this.deps.queryKnowledgeFacts) {
|
|
589
|
+
try {
|
|
590
|
+
const entities: string[] = [];
|
|
591
|
+
// Extract keywords from task text for entity matching
|
|
592
|
+
const techKeywords = [
|
|
593
|
+
"react-native", "react", "flutter", "swift", "kotlin", "python",
|
|
594
|
+
"typescript", "node", "docker", "sinain", "openclaw", "overlay",
|
|
595
|
+
"sense", "audio", "transcription", "escalation", "knowledge",
|
|
596
|
+
];
|
|
597
|
+
const lower = task.toLowerCase();
|
|
598
|
+
for (const kw of techKeywords) {
|
|
599
|
+
if (lower.includes(kw)) entities.push(kw);
|
|
600
|
+
}
|
|
601
|
+
// Also extract capitalized proper nouns (file names, project names)
|
|
602
|
+
const nouns = task.match(/\b[A-Z][a-z]{2,}\b/g);
|
|
603
|
+
if (nouns) entities.push(...nouns.map(n => n.toLowerCase()).slice(0, 3));
|
|
604
|
+
|
|
605
|
+
if (entities.length > 0) {
|
|
606
|
+
const facts = await this.deps.queryKnowledgeFacts(entities.slice(0, 5), 5);
|
|
607
|
+
if (facts && facts.trim().length > 20) {
|
|
608
|
+
knowledgeSection = `\n## Relevant Knowledge\n${facts.trim()}`;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
} catch (err) {
|
|
612
|
+
log(TAG, `spawn knowledge enrichment failed: ${String(err)}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Include latest digest for screen/audio context
|
|
617
|
+
const latestDigest = this.deps.feedBuffer.latest()?.text;
|
|
618
|
+
const contextSection = latestDigest
|
|
619
|
+
? `\n## Current User Context\n${latestDigest.slice(0, 500)}`
|
|
620
|
+
: "";
|
|
621
|
+
|
|
586
622
|
return [
|
|
587
623
|
"# Subagent Context",
|
|
588
624
|
"",
|
|
@@ -596,8 +632,9 @@ ${recentLines.join("\n")}`;
|
|
|
596
632
|
"1. Stay focused — do your assigned task, nothing else",
|
|
597
633
|
"2. Your final message will be reported to the requester",
|
|
598
634
|
"3. Be concise but informative",
|
|
599
|
-
"",
|
|
600
|
-
|
|
635
|
+
label ? `\nLabel: ${label}` : "",
|
|
636
|
+
knowledgeSection,
|
|
637
|
+
contextSection,
|
|
601
638
|
].filter(Boolean).join("\n");
|
|
602
639
|
}
|
|
603
640
|
|
|
@@ -633,19 +670,9 @@ ${recentLines.join("\n")}`;
|
|
|
633
670
|
return;
|
|
634
671
|
}
|
|
635
672
|
|
|
636
|
-
const maxWaitMs = 5 * 60 * 1000; // 5 minutes
|
|
637
673
|
const pollIntervalMs = 5000; // 5 seconds
|
|
638
674
|
|
|
639
675
|
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
676
|
|
|
650
677
|
if (!this.wsClient.isConnected) {
|
|
651
678
|
// Retry later
|
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";
|
|
@@ -344,10 +343,6 @@ async function main() {
|
|
|
344
343
|
localCuration.distillPendingSession(); // Recover any session saved before a force-kill
|
|
345
344
|
localCuration.startPeriodicCuration();
|
|
346
345
|
|
|
347
|
-
// ── Initialize trait engine ──
|
|
348
|
-
const traitRoster = loadTraitRoster(config.traitConfig.configPath);
|
|
349
|
-
const traitEngine = new TraitEngine(traitRoster, config.traitConfig);
|
|
350
|
-
|
|
351
346
|
// ── Initialize escalation ──
|
|
352
347
|
const escalator = new Escalator({
|
|
353
348
|
feedBuffer,
|
|
@@ -372,33 +367,6 @@ async function main() {
|
|
|
372
367
|
// Handle recorder commands
|
|
373
368
|
const stopResult = recorder.handleCommand(entry.record);
|
|
374
369
|
|
|
375
|
-
// Dispatch task via subagent spawn
|
|
376
|
-
if (entry.task || stopResult) {
|
|
377
|
-
let task: string;
|
|
378
|
-
let label: string | undefined;
|
|
379
|
-
|
|
380
|
-
if (stopResult && stopResult.segments > 0 && entry.task) {
|
|
381
|
-
// Recording stopped with explicit task instruction
|
|
382
|
-
task = `${entry.task}\n\n[Recording: "${stopResult.title}", ${stopResult.durationS}s]\n${stopResult.transcript}`;
|
|
383
|
-
label = stopResult.title;
|
|
384
|
-
} else if (stopResult && stopResult.segments > 0) {
|
|
385
|
-
// Recording stopped without explicit task — default to cleanup/summarize
|
|
386
|
-
task = `Clean up and summarize this recording transcript:\n\n[Recording: "${stopResult.title}", ${stopResult.durationS}s]\n${stopResult.transcript}`;
|
|
387
|
-
label = stopResult.title;
|
|
388
|
-
} else if (entry.task) {
|
|
389
|
-
// Standalone task without recording
|
|
390
|
-
task = entry.task;
|
|
391
|
-
} else {
|
|
392
|
-
task = "";
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
if (task) {
|
|
396
|
-
escalator.dispatchSpawnTask(task, label).catch(err => {
|
|
397
|
-
error(TAG, "spawn task dispatch error:", err);
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
370
|
// Escalation continues as normal
|
|
403
371
|
escalator.onAgentAnalysis(entry, contextWindow);
|
|
404
372
|
},
|
|
@@ -422,8 +390,6 @@ async function main() {
|
|
|
422
390
|
};
|
|
423
391
|
return ctx;
|
|
424
392
|
} : undefined,
|
|
425
|
-
traitEngine,
|
|
426
|
-
traitLogDir: config.traitConfig.logDir,
|
|
427
393
|
getKnowledgeDocPath: () => {
|
|
428
394
|
const workspace = resolveWorkspace();
|
|
429
395
|
const p = `${workspace}/memory/sinain-knowledge.md`;
|
|
@@ -564,6 +530,9 @@ async function main() {
|
|
|
564
530
|
// ── Screen capture active flag ──
|
|
565
531
|
let screenActive = true;
|
|
566
532
|
|
|
533
|
+
// ── Escalation pause/resume state ──
|
|
534
|
+
let savedEscalationMode: typeof config.escalationConfig.mode | null = null;
|
|
535
|
+
|
|
567
536
|
// ── Create HTTP + WS server ──
|
|
568
537
|
const server = createAppServer({
|
|
569
538
|
config,
|
|
@@ -674,6 +643,7 @@ async function main() {
|
|
|
674
643
|
|
|
675
644
|
// Bare agent HTTP escalation bridge
|
|
676
645
|
getEscalationPending: () => escalator.getPendingHttp(),
|
|
646
|
+
isEscalationPaused: () => savedEscalationMode !== null,
|
|
677
647
|
respondEscalation: (id: string, response: string) => escalator.respondHttp(id, response),
|
|
678
648
|
|
|
679
649
|
// Knowledge graph integration (checks both local and workspace DBs)
|
|
@@ -733,7 +703,22 @@ async function main() {
|
|
|
733
703
|
wsHandler.updateState({ screen: screenActive ? "active" : "off" });
|
|
734
704
|
return screenActive;
|
|
735
705
|
},
|
|
736
|
-
|
|
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
|
+
},
|
|
737
722
|
});
|
|
738
723
|
|
|
739
724
|
// Broadcast initial screen state so overlay gets correct status on connect
|
|
@@ -795,7 +780,6 @@ async function main() {
|
|
|
795
780
|
log(TAG, ` mic: ${config.micEnabled ? (config.micConfig.autoStart ? "active" : "standby") : "disabled"}`);
|
|
796
781
|
log(TAG, ` agent: ${config.agentConfig.enabled ? "enabled" : "disabled"}`);
|
|
797
782
|
log(TAG, ` escal: ${config.escalationConfig.mode}`);
|
|
798
|
-
log(TAG, ` traits: ${config.traitConfig.enabled ? "enabled" : "disabled"} (${traitRoster.length} traits)`);
|
|
799
783
|
log(TAG, ` cost: display=${config.costDisplayEnabled ? "on" : "off"} (always logged)`);
|
|
800
784
|
|
|
801
785
|
// ── Graceful shutdown ──
|
|
@@ -95,7 +95,7 @@ export class LocalCurationService {
|
|
|
95
95
|
* Called during shutdown — instant (no LLM), survives tsx force-kill.
|
|
96
96
|
*/
|
|
97
97
|
savePendingSession(feedItems: FeedItem[]): void {
|
|
98
|
-
if (feedItems.length <
|
|
98
|
+
if (feedItems.length < 1) {
|
|
99
99
|
log(TAG, `skipping save — only ${feedItems.length} feed items`);
|
|
100
100
|
return;
|
|
101
101
|
}
|
|
@@ -135,7 +135,7 @@ export class LocalCurationService {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
const items: FeedItem[] = data.items || [];
|
|
138
|
-
if (items.length <
|
|
138
|
+
if (items.length < 1) {
|
|
139
139
|
log(TAG, `pending session too small (${items.length} items) — removing`);
|
|
140
140
|
unlinkSync(pendingPath);
|
|
141
141
|
return;
|
|
@@ -160,7 +160,7 @@ export class LocalCurationService {
|
|
|
160
160
|
* picked up on next startup via distillPendingSession().
|
|
161
161
|
*/
|
|
162
162
|
async distillSession(feedItems: FeedItem[]): Promise<void> {
|
|
163
|
-
if (feedItems.length <
|
|
163
|
+
if (feedItems.length < 1) {
|
|
164
164
|
log(TAG, `skipping distillation — only ${feedItems.length} feed items`);
|
|
165
165
|
return;
|
|
166
166
|
}
|
|
@@ -332,7 +332,7 @@ export class LocalCurationService {
|
|
|
332
332
|
|
|
333
333
|
/** Fallback: write raw feed summary when distillation fails. */
|
|
334
334
|
private writeDailyNotesFallback(feedItems: FeedItem[]): void {
|
|
335
|
-
if (feedItems.length <
|
|
335
|
+
if (feedItems.length < 1) return;
|
|
336
336
|
|
|
337
337
|
const date = new Date().toISOString().slice(0, 10);
|
|
338
338
|
const notesPath = resolve(this.memoryDir, `${date}.md`);
|