@geravant/sinain 1.8.0 → 1.10.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/.env.example +14 -13
- package/HEARTBEAT.md +1 -1
- package/README.md +4 -7
- package/cli.js +16 -2
- package/config-shared.js +469 -0
- package/config.js +152 -0
- package/index.ts +1 -3
- package/launcher.js +7 -1
- package/onboard.js +345 -0
- package/package.json +8 -2
- package/sense_client/__main__.py +8 -4
- package/sense_client/gate.py +1 -0
- package/sense_client/ocr.py +58 -25
- package/sense_client/sender.py +2 -0
- package/sense_client/vision.py +31 -11
- package/sinain-agent/CLAUDE.md +0 -1
- package/sinain-agent/run.sh +2 -1
- package/sinain-core/src/agent/analyzer.ts +56 -58
- package/sinain-core/src/agent/loop.ts +37 -11
- package/sinain-core/src/audio/transcription.ts +20 -5
- package/sinain-core/src/config.ts +20 -16
- package/sinain-core/src/cost/tracker.ts +64 -0
- package/sinain-core/src/escalation/escalator.ts +31 -59
- package/sinain-core/src/index.ts +41 -45
- package/sinain-core/src/overlay/commands.ts +12 -0
- package/sinain-core/src/overlay/ws-handler.ts +27 -0
- package/sinain-core/src/server.ts +41 -0
- package/sinain-core/src/types.ts +46 -11
- package/sinain-knowledge/curation/engine.ts +0 -17
- package/sinain-knowledge/protocol/heartbeat.md +1 -1
- package/sinain-mcp-server/index.ts +4 -20
- package/sinain-memory/git_backup.sh +0 -19
package/sinain-core/src/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { TranscriptionService } from "./audio/transcription.js";
|
|
|
10
10
|
import { AgentLoop } from "./agent/loop.js";
|
|
11
11
|
import { TraitEngine, loadTraitRoster } from "./agent/traits.js";
|
|
12
12
|
import { shortAppName } from "./agent/context-window.js";
|
|
13
|
-
import { Escalator
|
|
13
|
+
import { Escalator } from "./escalation/escalator.js";
|
|
14
14
|
import { Recorder } from "./recorder.js";
|
|
15
15
|
import { Tracer } from "./trace/tracer.js";
|
|
16
16
|
import { TraceStore } from "./trace/trace-store.js";
|
|
@@ -18,6 +18,7 @@ import { FeedbackStore } from "./learning/feedback-store.js";
|
|
|
18
18
|
import { SignalCollector } from "./learning/signal-collector.js";
|
|
19
19
|
import { createAppServer } from "./server.js";
|
|
20
20
|
import { Profiler } from "./profiler.js";
|
|
21
|
+
import { CostTracker } from "./cost/tracker.js";
|
|
21
22
|
import type { SenseEvent, EscalationMode, FeedItem } from "./types.js";
|
|
22
23
|
import { isDuplicateTranscript, bigramSimilarity } from "./util/dedup.js";
|
|
23
24
|
import { log, warn, error } from "./log.js";
|
|
@@ -25,41 +26,6 @@ import { initPrivacy, levelFor, applyLevel } from "./privacy/index.js";
|
|
|
25
26
|
|
|
26
27
|
const TAG = "core";
|
|
27
28
|
|
|
28
|
-
/** Build context snapshot for user-initiated spawn tasks. */
|
|
29
|
-
function buildSpawnContext(
|
|
30
|
-
entry: { digest: string; context: { currentApp: string } },
|
|
31
|
-
feedBuffer: FeedBuffer,
|
|
32
|
-
senseBuffer: SenseBuffer,
|
|
33
|
-
): SpawnContext {
|
|
34
|
-
const cutoff = Date.now() - 60_000;
|
|
35
|
-
|
|
36
|
-
// Recent audio: last ~60s of transcripts
|
|
37
|
-
const recentAudio = feedBuffer.queryByTime(cutoff)
|
|
38
|
-
.filter(m => m.channel === "stream" && (m.text.startsWith("[🔊]") || m.text.startsWith("[🎙]")))
|
|
39
|
-
.map(m => m.text)
|
|
40
|
-
.join("\n")
|
|
41
|
-
.slice(0, 2000);
|
|
42
|
-
|
|
43
|
-
// Recent screen: last ~60s of deduped OCR text
|
|
44
|
-
const screenEvents = senseBuffer.queryByTime(cutoff);
|
|
45
|
-
const seenOcr = new Set<string>();
|
|
46
|
-
const screenLines: string[] = [];
|
|
47
|
-
for (const e of screenEvents) {
|
|
48
|
-
if (e.ocr && !seenOcr.has(e.ocr)) {
|
|
49
|
-
seenOcr.add(e.ocr);
|
|
50
|
-
screenLines.push(`[${e.meta.app}] ${e.ocr}`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
const recentScreen = screenLines.join("\n").slice(0, 3000);
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
currentApp: entry.context.currentApp,
|
|
57
|
-
digest: entry.digest,
|
|
58
|
-
recentAudio: recentAudio || undefined,
|
|
59
|
-
recentScreen: recentScreen || undefined,
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
29
|
/** Resolve workspace path, expanding leading ~ to HOME. */
|
|
64
30
|
function resolveWorkspace(): string {
|
|
65
31
|
const raw = process.env.SINAIN_WORKSPACE || `${process.env.HOME}/.openclaw/workspace`;
|
|
@@ -93,6 +59,10 @@ async function main() {
|
|
|
93
59
|
// ── Initialize overlay WS handler ──
|
|
94
60
|
const wsHandler = new WsHandler();
|
|
95
61
|
|
|
62
|
+
// ── Initialize cost tracker ──
|
|
63
|
+
const costTracker = new CostTracker((snapshot) => wsHandler.broadcastCost(snapshot, config.costDisplayEnabled));
|
|
64
|
+
costTracker.startPeriodicLog(60_000);
|
|
65
|
+
|
|
96
66
|
// ── Initialize tracing ──
|
|
97
67
|
const tracer = config.traceEnabled ? new Tracer() : null;
|
|
98
68
|
const traceStore = config.traceEnabled ? new TraceStore(config.traceDir) : null;
|
|
@@ -100,9 +70,6 @@ async function main() {
|
|
|
100
70
|
// ── Initialize recorder ──
|
|
101
71
|
const recorder = new Recorder();
|
|
102
72
|
|
|
103
|
-
// ── Spawn context cache — updated every agent tick for user-initiated spawns ──
|
|
104
|
-
let lastSpawnContext: SpawnContext | null = null;
|
|
105
|
-
|
|
106
73
|
// ── Initialize profiler ──
|
|
107
74
|
const profiler = new Profiler();
|
|
108
75
|
|
|
@@ -146,11 +113,35 @@ async function main() {
|
|
|
146
113
|
getRecorderStatus: () => recorder.getStatus(),
|
|
147
114
|
profiler,
|
|
148
115
|
onAnalysis: (entry, contextWindow) => {
|
|
149
|
-
// Handle recorder commands
|
|
150
|
-
recorder.handleCommand(entry.record);
|
|
116
|
+
// Handle recorder commands
|
|
117
|
+
const stopResult = recorder.handleCommand(entry.record);
|
|
118
|
+
|
|
119
|
+
// Dispatch task via subagent spawn
|
|
120
|
+
if (entry.task || stopResult) {
|
|
121
|
+
let task: string;
|
|
122
|
+
let label: string | undefined;
|
|
123
|
+
|
|
124
|
+
if (stopResult && stopResult.segments > 0 && entry.task) {
|
|
125
|
+
// Recording stopped with explicit task instruction
|
|
126
|
+
task = `${entry.task}\n\n[Recording: "${stopResult.title}", ${stopResult.durationS}s]\n${stopResult.transcript}`;
|
|
127
|
+
label = stopResult.title;
|
|
128
|
+
} else if (stopResult && stopResult.segments > 0) {
|
|
129
|
+
// Recording stopped without explicit task — default to cleanup/summarize
|
|
130
|
+
task = `Clean up and summarize this recording transcript:\n\n[Recording: "${stopResult.title}", ${stopResult.durationS}s]\n${stopResult.transcript}`;
|
|
131
|
+
label = stopResult.title;
|
|
132
|
+
} else if (entry.task) {
|
|
133
|
+
// Standalone task without recording
|
|
134
|
+
task = entry.task;
|
|
135
|
+
} else {
|
|
136
|
+
task = "";
|
|
137
|
+
}
|
|
151
138
|
|
|
152
|
-
|
|
153
|
-
|
|
139
|
+
if (task) {
|
|
140
|
+
escalator.dispatchSpawnTask(task, label).catch(err => {
|
|
141
|
+
error(TAG, "spawn task dispatch error:", err);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
154
145
|
|
|
155
146
|
// Escalation continues as normal
|
|
156
147
|
escalator.onAgentAnalysis(entry, contextWindow);
|
|
@@ -184,6 +175,7 @@ async function main() {
|
|
|
184
175
|
return null;
|
|
185
176
|
},
|
|
186
177
|
feedbackStore: feedbackStore ?? undefined,
|
|
178
|
+
costTracker,
|
|
187
179
|
});
|
|
188
180
|
|
|
189
181
|
// ── Wire learning signal collector (needs agentLoop) ──
|
|
@@ -211,6 +203,7 @@ async function main() {
|
|
|
211
203
|
systemAudioPipeline.setProfiler(profiler);
|
|
212
204
|
if (micPipeline) micPipeline.setProfiler(profiler);
|
|
213
205
|
transcription.setProfiler(profiler);
|
|
206
|
+
transcription.setCostTracker(costTracker);
|
|
214
207
|
|
|
215
208
|
// Wire: audio chunks → transcription (both pipelines share the same transcription service)
|
|
216
209
|
systemAudioPipeline.on("chunk", (chunk) => {
|
|
@@ -322,6 +315,7 @@ async function main() {
|
|
|
322
315
|
senseBuffer,
|
|
323
316
|
wsHandler,
|
|
324
317
|
profiler,
|
|
318
|
+
costTracker,
|
|
325
319
|
feedbackStore: feedbackStore ?? undefined,
|
|
326
320
|
isScreenActive: () => screenActive,
|
|
327
321
|
|
|
@@ -447,7 +441,7 @@ async function main() {
|
|
|
447
441
|
|
|
448
442
|
// Spawn background agent task (from HUD Shift+Enter or bare agent POST /spawn)
|
|
449
443
|
onSpawnCommand: (text: string) => {
|
|
450
|
-
escalator.dispatchSpawnTask(text,
|
|
444
|
+
escalator.dispatchSpawnTask(text, "user-command").catch((err) => {
|
|
451
445
|
log("srv", `spawn via HTTP failed: ${err}`);
|
|
452
446
|
});
|
|
453
447
|
},
|
|
@@ -475,7 +469,7 @@ async function main() {
|
|
|
475
469
|
agentLoop.onNewContext(true);
|
|
476
470
|
},
|
|
477
471
|
onSpawnCommand: (text) => {
|
|
478
|
-
escalator.dispatchSpawnTask(text,
|
|
472
|
+
escalator.dispatchSpawnTask(text, "user-command").catch((err) => {
|
|
479
473
|
log("cmd", `spawn command failed: ${err}`);
|
|
480
474
|
wsHandler.broadcast(`\u26a0 Spawn failed: ${String(err).slice(0, 100)}`, "normal");
|
|
481
475
|
});
|
|
@@ -551,12 +545,14 @@ async function main() {
|
|
|
551
545
|
log(TAG, ` agent: ${config.agentConfig.enabled ? "enabled" : "disabled"}`);
|
|
552
546
|
log(TAG, ` escal: ${config.escalationConfig.mode}`);
|
|
553
547
|
log(TAG, ` traits: ${config.traitConfig.enabled ? "enabled" : "disabled"} (${traitRoster.length} traits)`);
|
|
548
|
+
log(TAG, ` cost: display=${config.costDisplayEnabled ? "on" : "off"} (always logged)`);
|
|
554
549
|
|
|
555
550
|
// ── Graceful shutdown ──
|
|
556
551
|
const shutdown = async (signal: string) => {
|
|
557
552
|
log(TAG, `${signal} received, shutting down...`);
|
|
558
553
|
clearInterval(bufferGaugeTimer);
|
|
559
554
|
if (feedbackSummaryTimer) clearInterval(feedbackSummaryTimer);
|
|
555
|
+
costTracker.stop();
|
|
560
556
|
profiler.stop();
|
|
561
557
|
recorder.forceStop(); // Stop any active recording
|
|
562
558
|
agentLoop.stop();
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
1
2
|
import type { InboundMessage } from "../types.js";
|
|
2
3
|
import type { WsHandler } from "./ws-handler.js";
|
|
3
4
|
import type { AudioPipeline } from "../audio/pipeline.js";
|
|
4
5
|
import type { CoreConfig } from "../types.js";
|
|
5
6
|
import { WebSocket } from "ws";
|
|
7
|
+
import { loadedEnvPath } from "../config.js";
|
|
6
8
|
import { log } from "../log.js";
|
|
7
9
|
|
|
8
10
|
const TAG = "cmd";
|
|
@@ -151,6 +153,16 @@ function handleCommand(action: string, deps: CommandDeps): void {
|
|
|
151
153
|
log(TAG, `traits toggled ${nowEnabled ? "ON" : "OFF"}`);
|
|
152
154
|
break;
|
|
153
155
|
}
|
|
156
|
+
case "open_settings": {
|
|
157
|
+
const envPath = loadedEnvPath || `${process.env.HOME || process.env.USERPROFILE}/.sinain/.env`;
|
|
158
|
+
const cmd = process.platform === "win32" ? "notepad" : "open";
|
|
159
|
+
const args = process.platform === "win32" ? [envPath] : ["-t", envPath];
|
|
160
|
+
execFile(cmd, args, (err) => {
|
|
161
|
+
if (err) log(TAG, `open_settings failed: ${err.message}`);
|
|
162
|
+
});
|
|
163
|
+
log(TAG, `open_settings: ${envPath}`);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
154
166
|
default:
|
|
155
167
|
log(TAG, `unhandled command: ${action}`);
|
|
156
168
|
}
|
|
@@ -7,6 +7,8 @@ import type {
|
|
|
7
7
|
FeedMessage,
|
|
8
8
|
StatusMessage,
|
|
9
9
|
SpawnTaskMessage,
|
|
10
|
+
CostMessage,
|
|
11
|
+
CostSnapshot,
|
|
10
12
|
Priority,
|
|
11
13
|
FeedChannel,
|
|
12
14
|
} from "../types.js";
|
|
@@ -39,6 +41,7 @@ export class WsHandler {
|
|
|
39
41
|
};
|
|
40
42
|
private replayBuffer: FeedMessage[] = [];
|
|
41
43
|
private spawnTaskBuffer: Map<string, SpawnTaskMessage> = new Map();
|
|
44
|
+
private latestCostMsg: CostMessage | null = null;
|
|
42
45
|
|
|
43
46
|
constructor() {
|
|
44
47
|
this.startHeartbeat();
|
|
@@ -89,6 +92,16 @@ export class WsHandler {
|
|
|
89
92
|
log(TAG, `replayed ${this.spawnTaskBuffer.size} spawn tasks to new client`);
|
|
90
93
|
}
|
|
91
94
|
|
|
95
|
+
// Replay current cost snapshot (or send zeroed snapshot so client resets)
|
|
96
|
+
this.sendTo(ws, this.latestCostMsg ?? {
|
|
97
|
+
type: "cost" as const,
|
|
98
|
+
totalCost: 0,
|
|
99
|
+
costBySource: {},
|
|
100
|
+
callCount: 0,
|
|
101
|
+
startedAt: Date.now(),
|
|
102
|
+
displayEnabled: false,
|
|
103
|
+
});
|
|
104
|
+
|
|
92
105
|
ws.on("message", (raw) => {
|
|
93
106
|
try {
|
|
94
107
|
const data = JSON.parse(raw.toString()) as InboundMessage;
|
|
@@ -158,6 +171,20 @@ export class WsHandler {
|
|
|
158
171
|
this.broadcastMessage(msg);
|
|
159
172
|
}
|
|
160
173
|
|
|
174
|
+
/** Broadcast cost snapshot to all connected overlays. */
|
|
175
|
+
broadcastCost(snapshot: CostSnapshot, displayEnabled: boolean): void {
|
|
176
|
+
const msg: CostMessage = {
|
|
177
|
+
type: "cost",
|
|
178
|
+
totalCost: snapshot.totalCost,
|
|
179
|
+
costBySource: snapshot.costBySource,
|
|
180
|
+
callCount: snapshot.callCount,
|
|
181
|
+
startedAt: snapshot.startedAt,
|
|
182
|
+
displayEnabled,
|
|
183
|
+
};
|
|
184
|
+
this.latestCostMsg = msg;
|
|
185
|
+
this.broadcastMessage(msg);
|
|
186
|
+
}
|
|
187
|
+
|
|
161
188
|
/** Update internal state and broadcast. */
|
|
162
189
|
updateState(partial: Partial<BridgeState>): void {
|
|
163
190
|
Object.assign(this.state, partial);
|
|
@@ -2,6 +2,7 @@ import { createServer, type IncomingMessage, type ServerResponse } from "node:ht
|
|
|
2
2
|
import { WebSocketServer, WebSocket } from "ws";
|
|
3
3
|
import type { CoreConfig, SenseEvent } from "./types.js";
|
|
4
4
|
import type { Profiler } from "./profiler.js";
|
|
5
|
+
import type { CostTracker } from "./cost/tracker.js";
|
|
5
6
|
import type { FeedbackStore } from "./learning/feedback-store.js";
|
|
6
7
|
import { FeedBuffer } from "./buffers/feed-buffer.js";
|
|
7
8
|
import { SenseBuffer, type SemanticSenseEvent, type TextDelta } from "./buffers/sense-buffer.js";
|
|
@@ -20,6 +21,7 @@ export interface ServerDeps {
|
|
|
20
21
|
senseBuffer: SenseBuffer;
|
|
21
22
|
wsHandler: WsHandler;
|
|
22
23
|
profiler?: Profiler;
|
|
24
|
+
costTracker?: CostTracker;
|
|
23
25
|
onSenseEvent: (event: SenseEvent) => void;
|
|
24
26
|
onSenseDelta?: (data: { app: string; activity: string; changes: TextDelta[]; priority?: string; ts: number }) => void;
|
|
25
27
|
onFeedPost: (text: string, priority: string) => void;
|
|
@@ -65,6 +67,8 @@ function readBody(req: IncomingMessage, maxBytes: number): Promise<string> {
|
|
|
65
67
|
export function createAppServer(deps: ServerDeps) {
|
|
66
68
|
const { config, feedBuffer, senseBuffer, wsHandler } = deps;
|
|
67
69
|
let senseInBytes = 0;
|
|
70
|
+
const seenVisionCostIds = new Set<string>();
|
|
71
|
+
const visionCostCleanup = setInterval(() => seenVisionCostIds.clear(), 60_000);
|
|
68
72
|
|
|
69
73
|
const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
|
|
70
74
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
@@ -91,6 +95,24 @@ export function createAppServer(deps: ServerDeps) {
|
|
|
91
95
|
senseInBytes += Buffer.byteLength(body);
|
|
92
96
|
deps.profiler?.gauge("network.senseInBytes", senseInBytes);
|
|
93
97
|
const data = JSON.parse(body);
|
|
98
|
+
|
|
99
|
+
// Record vision API cost before dedup — the call is already billed.
|
|
100
|
+
// Dedup by cost_id to avoid double-counting on sender retries.
|
|
101
|
+
const vc = data.vision_cost;
|
|
102
|
+
if (vc && typeof vc.cost === "number" && vc.cost > 0) {
|
|
103
|
+
const costId = vc.cost_id as string | undefined;
|
|
104
|
+
if (!costId || !seenVisionCostIds.has(costId)) {
|
|
105
|
+
if (costId) seenVisionCostIds.add(costId);
|
|
106
|
+
deps.costTracker?.record({
|
|
107
|
+
source: "vision",
|
|
108
|
+
model: vc.model || "unknown",
|
|
109
|
+
cost: vc.cost,
|
|
110
|
+
tokensIn: vc.tokens_in || 0,
|
|
111
|
+
tokensOut: vc.tokens_out || 0,
|
|
112
|
+
ts: Date.now(),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
94
116
|
if (!data.type || data.ts === undefined) {
|
|
95
117
|
res.writeHead(400);
|
|
96
118
|
res.end(JSON.stringify({ ok: false, error: "missing type or ts" }));
|
|
@@ -309,6 +331,24 @@ export function createAppServer(deps: ServerDeps) {
|
|
|
309
331
|
return;
|
|
310
332
|
}
|
|
311
333
|
|
|
334
|
+
// ── /setup/status ── (used by overlay onboarding wizard)
|
|
335
|
+
if (req.method === "GET" && url.pathname === "/setup/status") {
|
|
336
|
+
const health = deps.getHealthPayload();
|
|
337
|
+
res.end(JSON.stringify({
|
|
338
|
+
ok: true,
|
|
339
|
+
setup: {
|
|
340
|
+
openrouterKey: !!config.transcriptionConfig.openrouterApiKey,
|
|
341
|
+
gatewayConfigured: !!config.openclawConfig.gatewayToken,
|
|
342
|
+
gatewayConnected: !!(health as Record<string, any>)?.escalation?.gatewayConnected,
|
|
343
|
+
audioActive: (health as Record<string, any>)?.audio?.state === "active" || (health as Record<string, any>)?.audioPipeline?.state === "running",
|
|
344
|
+
screenActive: deps.isScreenActive(),
|
|
345
|
+
transcriptionBackend: config.transcriptionConfig.backend,
|
|
346
|
+
escalationMode: config.escalationConfig.mode,
|
|
347
|
+
},
|
|
348
|
+
}));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
312
352
|
// ── /user/command ──
|
|
313
353
|
if (req.method === "POST" && url.pathname === "/user/command") {
|
|
314
354
|
const body = await readBody(req, 4096);
|
|
@@ -504,6 +544,7 @@ export function createAppServer(deps: ServerDeps) {
|
|
|
504
544
|
});
|
|
505
545
|
},
|
|
506
546
|
async destroy(): Promise<void> {
|
|
547
|
+
clearInterval(visionCostCleanup);
|
|
507
548
|
wsHandler.destroy();
|
|
508
549
|
wss.close();
|
|
509
550
|
await new Promise<void>((resolve) => httpServer.close(() => resolve()));
|
package/sinain-core/src/types.ts
CHANGED
|
@@ -78,7 +78,36 @@ export interface SpawnCommandMessage {
|
|
|
78
78
|
text: string;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
/** Cost update broadcast to overlay. */
|
|
82
|
+
export interface CostMessage {
|
|
83
|
+
type: "cost";
|
|
84
|
+
totalCost: number;
|
|
85
|
+
costBySource: Record<string, number>;
|
|
86
|
+
callCount: number;
|
|
87
|
+
startedAt: number;
|
|
88
|
+
displayEnabled: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Entry recorded by CostTracker for each LLM call. */
|
|
92
|
+
export interface CostEntry {
|
|
93
|
+
source: "analyzer" | "transcription" | "vision";
|
|
94
|
+
model: string;
|
|
95
|
+
cost: number;
|
|
96
|
+
tokensIn: number;
|
|
97
|
+
tokensOut: number;
|
|
98
|
+
ts: number;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Snapshot of accumulated cost state. */
|
|
102
|
+
export interface CostSnapshot {
|
|
103
|
+
totalCost: number;
|
|
104
|
+
costBySource: Record<string, number>;
|
|
105
|
+
costByModel: Record<string, number>;
|
|
106
|
+
callCount: number;
|
|
107
|
+
startedAt: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type OutboundMessage = FeedMessage | StatusMessage | PingMessage | SpawnTaskMessage | CostMessage;
|
|
82
111
|
export type InboundMessage = UserMessage | CommandMessage | PongMessage | ProfilingMessage | UserCommandMessage | SpawnCommandMessage;
|
|
83
112
|
|
|
84
113
|
/** Abstraction for user commands (text now, voice later). */
|
|
@@ -218,28 +247,31 @@ export interface StopResult {
|
|
|
218
247
|
export type EscalationMode = "off" | "selective" | "focus" | "rich";
|
|
219
248
|
export type ContextRichness = "lean" | "standard" | "rich";
|
|
220
249
|
|
|
221
|
-
export
|
|
250
|
+
export type AnalysisProvider = "openrouter" | "ollama";
|
|
251
|
+
|
|
252
|
+
export interface AnalysisConfig {
|
|
222
253
|
enabled: boolean;
|
|
254
|
+
provider: AnalysisProvider;
|
|
223
255
|
model: string;
|
|
224
256
|
visionModel: string;
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
localVisionModel: string;
|
|
228
|
-
localVisionUrl: string;
|
|
229
|
-
localVisionTimeout: number;
|
|
230
|
-
openrouterApiKey: string;
|
|
257
|
+
endpoint: string;
|
|
258
|
+
apiKey: string;
|
|
231
259
|
maxTokens: number;
|
|
232
260
|
temperature: number;
|
|
261
|
+
fallbackModels: string[];
|
|
262
|
+
timeout: number;
|
|
263
|
+
// Loop timing
|
|
233
264
|
pushToFeed: boolean;
|
|
234
265
|
debounceMs: number;
|
|
235
266
|
maxIntervalMs: number;
|
|
236
267
|
cooldownMs: number;
|
|
237
268
|
maxAgeMs: number;
|
|
238
|
-
fallbackModels: string[];
|
|
239
|
-
/** Maximum entries to keep in agent history buffer (default: 50) */
|
|
240
269
|
historyLimit: number;
|
|
241
270
|
}
|
|
242
271
|
|
|
272
|
+
/** @deprecated Use AnalysisConfig */
|
|
273
|
+
export type AgentConfig = AnalysisConfig;
|
|
274
|
+
|
|
243
275
|
export interface AgentResult {
|
|
244
276
|
hud: string;
|
|
245
277
|
digest: string;
|
|
@@ -253,6 +285,8 @@ export interface AgentResult {
|
|
|
253
285
|
voice?: string;
|
|
254
286
|
voice_stat?: number;
|
|
255
287
|
voice_confidence?: number;
|
|
288
|
+
/** Actual USD cost returned by OpenRouter (undefined if not available). */
|
|
289
|
+
cost?: number;
|
|
256
290
|
}
|
|
257
291
|
|
|
258
292
|
export interface AgentEntry extends AgentResult {
|
|
@@ -437,11 +471,12 @@ export interface CoreConfig {
|
|
|
437
471
|
micConfig: AudioPipelineConfig;
|
|
438
472
|
micEnabled: boolean;
|
|
439
473
|
transcriptionConfig: TranscriptionConfig;
|
|
440
|
-
agentConfig:
|
|
474
|
+
agentConfig: AnalysisConfig;
|
|
441
475
|
escalationConfig: EscalationConfig;
|
|
442
476
|
openclawConfig: OpenClawConfig;
|
|
443
477
|
situationMdPath: string;
|
|
444
478
|
traceEnabled: boolean;
|
|
479
|
+
costDisplayEnabled: boolean;
|
|
445
480
|
traceDir: string;
|
|
446
481
|
learningConfig: LearningConfig;
|
|
447
482
|
traitConfig: TraitConfig;
|
|
@@ -43,7 +43,6 @@ function writeDistillState(workspaceDir: string, state: DistillState): void {
|
|
|
43
43
|
|
|
44
44
|
export type HeartbeatResult = {
|
|
45
45
|
status: string;
|
|
46
|
-
gitBackup: string | null;
|
|
47
46
|
signals: unknown[];
|
|
48
47
|
recommendedAction: { action: string; task: string | null; confidence: number };
|
|
49
48
|
output: unknown | null;
|
|
@@ -95,7 +94,6 @@ export class CurationEngine {
|
|
|
95
94
|
const workspaceDir = this.store.getWorkspaceDir();
|
|
96
95
|
const result: HeartbeatResult = {
|
|
97
96
|
status: "ok",
|
|
98
|
-
gitBackup: null,
|
|
99
97
|
signals: [],
|
|
100
98
|
recommendedAction: { action: "skip", task: null, confidence: 0 },
|
|
101
99
|
output: null,
|
|
@@ -131,20 +129,6 @@ export class CurationEngine {
|
|
|
131
129
|
const latencyMs: Record<string, number> = {};
|
|
132
130
|
const heartbeatStart = Date.now();
|
|
133
131
|
|
|
134
|
-
// 1. Git backup (30s timeout)
|
|
135
|
-
try {
|
|
136
|
-
const t0 = Date.now();
|
|
137
|
-
const gitOut = await this.runScript(
|
|
138
|
-
["bash", "sinain-memory/git_backup.sh"],
|
|
139
|
-
{ timeoutMs: 30_000, cwd: workspaceDir },
|
|
140
|
-
);
|
|
141
|
-
latencyMs.gitBackup = Date.now() - t0;
|
|
142
|
-
result.gitBackup = gitOut.stdout.trim() || "nothing to commit";
|
|
143
|
-
} catch (err) {
|
|
144
|
-
this.logger.warn(`sinain-hud: git backup error: ${String(err)}`);
|
|
145
|
-
result.gitBackup = `error: ${String(err)}`;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
132
|
// Current time string for memory scripts
|
|
149
133
|
const hbTz = this.config.userTimezone;
|
|
150
134
|
const currentTimeStr = new Date().toLocaleString("en-GB", {
|
|
@@ -291,7 +275,6 @@ export class CurationEngine {
|
|
|
291
275
|
output: result.output,
|
|
292
276
|
skipped: result.skipped,
|
|
293
277
|
skipReason: result.skipReason,
|
|
294
|
-
gitBackup: result.gitBackup,
|
|
295
278
|
latencyMs,
|
|
296
279
|
totalLatencyMs,
|
|
297
280
|
};
|
|
@@ -59,4 +59,4 @@ SINAIN_BACKUP_REPO=<git-url> npx sinain
|
|
|
59
59
|
- Token printed at end (or visible in Brev dashboard → Gateway Token)
|
|
60
60
|
- Mac side: `./setup-nemoclaw.sh` → 5 prompts → overlay starts
|
|
61
61
|
|
|
62
|
-
Memory is
|
|
62
|
+
Memory is backed up via knowledge snapshots to `~/.sinain/knowledge-snapshots/`. New instances restore instantly via `SINAIN_BACKUP_REPO`.
|
|
@@ -317,23 +317,7 @@ server.tool(
|
|
|
317
317
|
const results: string[] = [];
|
|
318
318
|
const now = new Date().toISOString();
|
|
319
319
|
|
|
320
|
-
// Step 1:
|
|
321
|
-
const gitBackupPath = resolve(SCRIPTS_DIR, "git_backup.sh");
|
|
322
|
-
if (existsSync(gitBackupPath)) {
|
|
323
|
-
try {
|
|
324
|
-
const out = await new Promise<string>((res, rej) => {
|
|
325
|
-
execFile("bash", [gitBackupPath, MEMORY_DIR], { timeout: 30_000 }, (err, stdout, stderr) => {
|
|
326
|
-
if (err) rej(new Error(`git_backup failed: ${err.message}\n${stderr}`));
|
|
327
|
-
else res(stdout);
|
|
328
|
-
});
|
|
329
|
-
});
|
|
330
|
-
results.push(`[git_backup] ${out.trim() || "OK"}`);
|
|
331
|
-
} catch (err: any) {
|
|
332
|
-
results.push(`[git_backup] FAILED: ${err.message}`);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Step 2: signal_analyzer.py
|
|
320
|
+
// Step 1: signal_analyzer.py
|
|
337
321
|
try {
|
|
338
322
|
const out = await runScript([
|
|
339
323
|
resolve(SCRIPTS_DIR, "signal_analyzer.py"),
|
|
@@ -346,7 +330,7 @@ server.tool(
|
|
|
346
330
|
results.push(`[signal_analyzer] FAILED: ${err.message}`);
|
|
347
331
|
}
|
|
348
332
|
|
|
349
|
-
// Step
|
|
333
|
+
// Step 2: insight_synthesizer.py
|
|
350
334
|
try {
|
|
351
335
|
const out = await runScript([
|
|
352
336
|
resolve(SCRIPTS_DIR, "insight_synthesizer.py"),
|
|
@@ -358,7 +342,7 @@ server.tool(
|
|
|
358
342
|
results.push(`[insight_synthesizer] FAILED: ${err.message}`);
|
|
359
343
|
}
|
|
360
344
|
|
|
361
|
-
// Step
|
|
345
|
+
// Step 3: memory_miner.py
|
|
362
346
|
try {
|
|
363
347
|
const out = await runScript([
|
|
364
348
|
resolve(SCRIPTS_DIR, "memory_miner.py"),
|
|
@@ -369,7 +353,7 @@ server.tool(
|
|
|
369
353
|
results.push(`[memory_miner] FAILED: ${err.message}`);
|
|
370
354
|
}
|
|
371
355
|
|
|
372
|
-
// Step
|
|
356
|
+
// Step 4: playbook_curator.py
|
|
373
357
|
try {
|
|
374
358
|
const out = await runScript([
|
|
375
359
|
resolve(SCRIPTS_DIR, "playbook_curator.py"),
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# Phase 1: Git backup — commit and push any uncommitted changes in the workspace.
|
|
3
|
-
# Runs from the workspace root. Exits 0 on success or nothing to commit, 1 on push failure.
|
|
4
|
-
|
|
5
|
-
set -euo pipefail
|
|
6
|
-
|
|
7
|
-
changes=$(git status --porcelain 2>/dev/null || true)
|
|
8
|
-
|
|
9
|
-
if [ -z "$changes" ]; then
|
|
10
|
-
echo "nothing to commit"
|
|
11
|
-
exit 0
|
|
12
|
-
fi
|
|
13
|
-
|
|
14
|
-
git add -A
|
|
15
|
-
git commit -m "auto: heartbeat $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
16
|
-
git push origin main
|
|
17
|
-
|
|
18
|
-
# Output the commit hash
|
|
19
|
-
git rev-parse --short HEAD
|