@geravant/sinain 1.0.19 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -1
- package/cli.js +176 -0
- package/index.ts +4 -2
- package/install.js +89 -14
- package/launcher.js +622 -0
- package/openclaw.plugin.json +4 -0
- package/pack-prepare.js +48 -0
- package/package.json +24 -5
- package/sense_client/README.md +82 -0
- package/sense_client/__init__.py +1 -0
- package/sense_client/__main__.py +462 -0
- package/sense_client/app_detector.py +54 -0
- package/sense_client/app_detector_win.py +83 -0
- package/sense_client/capture.py +215 -0
- package/sense_client/capture_win.py +88 -0
- package/sense_client/change_detector.py +86 -0
- package/sense_client/config.py +64 -0
- package/sense_client/gate.py +145 -0
- package/sense_client/ocr.py +347 -0
- package/sense_client/privacy.py +65 -0
- package/sense_client/requirements.txt +13 -0
- package/sense_client/roi_extractor.py +84 -0
- package/sense_client/sender.py +173 -0
- package/sense_client/tests/__init__.py +0 -0
- package/sense_client/tests/test_stream1_optimizations.py +234 -0
- package/setup-overlay.js +82 -0
- package/sinain-agent/.env.example +17 -0
- package/sinain-agent/CLAUDE.md +87 -0
- package/sinain-agent/mcp-config.json +12 -0
- package/sinain-agent/run.sh +248 -0
- package/sinain-core/.env.example +93 -0
- package/sinain-core/package-lock.json +552 -0
- package/sinain-core/package.json +21 -0
- package/sinain-core/src/agent/analyzer.ts +366 -0
- package/sinain-core/src/agent/context-window.ts +172 -0
- package/sinain-core/src/agent/loop.ts +404 -0
- package/sinain-core/src/agent/situation-writer.ts +187 -0
- package/sinain-core/src/agent/traits.ts +520 -0
- package/sinain-core/src/audio/capture-spawner-macos.ts +44 -0
- package/sinain-core/src/audio/capture-spawner-win.ts +37 -0
- package/sinain-core/src/audio/capture-spawner.ts +14 -0
- package/sinain-core/src/audio/pipeline.ts +335 -0
- package/sinain-core/src/audio/transcription-local.ts +141 -0
- package/sinain-core/src/audio/transcription.ts +278 -0
- package/sinain-core/src/buffers/feed-buffer.ts +71 -0
- package/sinain-core/src/buffers/sense-buffer.ts +425 -0
- package/sinain-core/src/config.ts +245 -0
- package/sinain-core/src/escalation/escalation-slot.ts +136 -0
- package/sinain-core/src/escalation/escalator.ts +828 -0
- package/sinain-core/src/escalation/message-builder.ts +370 -0
- package/sinain-core/src/escalation/openclaw-ws.ts +726 -0
- package/sinain-core/src/escalation/scorer.ts +166 -0
- package/sinain-core/src/index.ts +537 -0
- package/sinain-core/src/learning/feedback-store.ts +253 -0
- package/sinain-core/src/learning/signal-collector.ts +218 -0
- package/sinain-core/src/log.ts +24 -0
- package/sinain-core/src/overlay/commands.ts +126 -0
- package/sinain-core/src/overlay/ws-handler.ts +267 -0
- package/sinain-core/src/privacy/index.ts +18 -0
- package/sinain-core/src/privacy/presets.ts +40 -0
- package/sinain-core/src/privacy/redact.ts +92 -0
- package/sinain-core/src/profiler.ts +181 -0
- package/sinain-core/src/recorder.ts +186 -0
- package/sinain-core/src/server.ts +456 -0
- package/sinain-core/src/trace/trace-store.ts +73 -0
- package/sinain-core/src/trace/tracer.ts +94 -0
- package/sinain-core/src/types.ts +427 -0
- package/sinain-core/src/util/dedup.ts +48 -0
- package/sinain-core/src/util/task-store.ts +84 -0
- package/sinain-core/tsconfig.json +18 -0
- package/sinain-knowledge/curation/engine.ts +137 -24
- package/sinain-knowledge/data/git-store.ts +26 -0
- package/sinain-knowledge/data/store.ts +117 -0
- package/sinain-mcp-server/index.ts +417 -0
- package/sinain-mcp-server/package.json +19 -0
- package/sinain-mcp-server/tsconfig.json +15 -0
- package/sinain-memory/graph_query.py +185 -0
- package/sinain-memory/knowledge_integrator.py +450 -0
- package/sinain-memory/memory-config.json +3 -1
- package/sinain-memory/session_distiller.py +162 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { Trace } from "../types.js";
|
|
4
|
+
import { log, error } from "../log.js";
|
|
5
|
+
|
|
6
|
+
const TAG = "trace-store";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Persistent JSONL trace log.
|
|
10
|
+
* Each day gets its own file: ~/.sinain-core/traces/2025-02-03.jsonl
|
|
11
|
+
*
|
|
12
|
+
* Format enables:
|
|
13
|
+
* cat traces/2025-02-03.jsonl | jq '.metrics.totalLatencyMs'
|
|
14
|
+
* Replay in eval harness
|
|
15
|
+
* Import into dashboards
|
|
16
|
+
*/
|
|
17
|
+
export class TraceStore {
|
|
18
|
+
private dir: string;
|
|
19
|
+
private currentDate = "";
|
|
20
|
+
private currentStream: fs.WriteStream | null = null;
|
|
21
|
+
|
|
22
|
+
constructor(dir: string) {
|
|
23
|
+
this.dir = dir;
|
|
24
|
+
try {
|
|
25
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
26
|
+
} catch (err: any) {
|
|
27
|
+
if (err.code !== "EEXIST") {
|
|
28
|
+
error(TAG, "failed to create trace dir:", err.message);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Append a trace to today's JSONL file. */
|
|
34
|
+
append(trace: Trace): void {
|
|
35
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
36
|
+
|
|
37
|
+
// Rotate file on date change
|
|
38
|
+
if (date !== this.currentDate) {
|
|
39
|
+
if (this.currentStream) {
|
|
40
|
+
this.currentStream.end();
|
|
41
|
+
}
|
|
42
|
+
const filePath = path.join(this.dir, `${date}.jsonl`);
|
|
43
|
+
this.currentStream = fs.createWriteStream(filePath, { flags: "a" });
|
|
44
|
+
this.currentDate = date;
|
|
45
|
+
log(TAG, `writing to ${filePath}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (this.currentStream) {
|
|
49
|
+
this.currentStream.write(JSON.stringify(trace) + "\n");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Read all traces for a given date. */
|
|
54
|
+
queryDay(date: string): Trace[] {
|
|
55
|
+
const filePath = path.join(this.dir, `${date}.jsonl`);
|
|
56
|
+
try {
|
|
57
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
58
|
+
return content.split("\n")
|
|
59
|
+
.filter(line => line.trim())
|
|
60
|
+
.map(line => JSON.parse(line) as Trace);
|
|
61
|
+
} catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Close the write stream. */
|
|
67
|
+
destroy(): void {
|
|
68
|
+
if (this.currentStream) {
|
|
69
|
+
this.currentStream.end();
|
|
70
|
+
this.currentStream = null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import type { Trace, Span, TraceMetrics, MetricsSummary } from "../types.js";
|
|
3
|
+
import type { TraceContext } from "../agent/loop.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Structured trace recording for every agent tick.
|
|
7
|
+
* Each tick produces a Trace with nested Spans and aggregated TraceMetrics.
|
|
8
|
+
*
|
|
9
|
+
* Traces are kept in a rolling buffer (max 500) and exposed via GET /traces.
|
|
10
|
+
* Optionally persisted to JSONL via TraceStore.
|
|
11
|
+
*/
|
|
12
|
+
export class Tracer {
|
|
13
|
+
private traces: Trace[] = [];
|
|
14
|
+
private maxTraces = 500;
|
|
15
|
+
|
|
16
|
+
/** Start a new trace for a tick. Returns a TraceContext for recording spans. */
|
|
17
|
+
startTrace(tickId: number): TraceContext {
|
|
18
|
+
const trace: Trace = {
|
|
19
|
+
traceId: crypto.randomUUID(),
|
|
20
|
+
tickId,
|
|
21
|
+
ts: Date.now(),
|
|
22
|
+
spans: [],
|
|
23
|
+
metrics: {} as TraceMetrics,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let currentSpanStart = 0;
|
|
27
|
+
let currentSpanName = "";
|
|
28
|
+
|
|
29
|
+
const ctx: TraceContext = {
|
|
30
|
+
startSpan(name: string): void {
|
|
31
|
+
currentSpanName = name;
|
|
32
|
+
currentSpanStart = Date.now();
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
endSpan(attrs?: Record<string, unknown>): void {
|
|
36
|
+
if (!currentSpanName) return;
|
|
37
|
+
const span: Span = {
|
|
38
|
+
name: currentSpanName,
|
|
39
|
+
startTs: currentSpanStart,
|
|
40
|
+
endTs: Date.now(),
|
|
41
|
+
attributes: attrs || {},
|
|
42
|
+
status: attrs?.status === "error" ? "error" : "ok",
|
|
43
|
+
error: attrs?.error as string | undefined,
|
|
44
|
+
};
|
|
45
|
+
trace.spans.push(span);
|
|
46
|
+
currentSpanName = "";
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
finish(metrics: Record<string, unknown>): void {
|
|
50
|
+
trace.metrics = metrics as unknown as TraceMetrics;
|
|
51
|
+
this._commit();
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
_commit: () => {
|
|
55
|
+
this.traces.push(trace);
|
|
56
|
+
if (this.traces.length > this.maxTraces) {
|
|
57
|
+
this.traces.shift();
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
} as TraceContext & { _commit: () => void };
|
|
61
|
+
|
|
62
|
+
return ctx;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Get traces after a given tickId, with limit. */
|
|
66
|
+
getTraces(afterTickId = 0, limit = 50): Trace[] {
|
|
67
|
+
return this.traces
|
|
68
|
+
.filter(t => t.tickId > afterTickId)
|
|
69
|
+
.slice(-limit);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Compute summary metrics over all stored traces. */
|
|
73
|
+
getMetricsSummary(): MetricsSummary {
|
|
74
|
+
if (this.traces.length === 0) {
|
|
75
|
+
return { count: 0, latencyP50: 0, latencyP95: 0, avgCostPerTick: 0, totalCost: 0 };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const latencies = this.traces
|
|
79
|
+
.map(t => t.metrics.totalLatencyMs)
|
|
80
|
+
.filter(l => l > 0)
|
|
81
|
+
.sort((a, b) => a - b);
|
|
82
|
+
|
|
83
|
+
const costs = this.traces.map(t => t.metrics.llmCost);
|
|
84
|
+
const totalCost = costs.reduce((a, b) => a + b, 0);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
count: this.traces.length,
|
|
88
|
+
latencyP50: latencies[Math.floor(latencies.length / 2)] || 0,
|
|
89
|
+
latencyP95: latencies[Math.floor(latencies.length * 0.95)] || 0,
|
|
90
|
+
avgCostPerTick: totalCost / this.traces.length,
|
|
91
|
+
totalCost,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
// ── Wire protocol types (overlay ↔ sinain-core) ──
|
|
2
|
+
|
|
3
|
+
export type Priority = "normal" | "high" | "urgent";
|
|
4
|
+
export type FeedChannel = "stream" | "agent";
|
|
5
|
+
|
|
6
|
+
/** sinain-core → Overlay: feed item */
|
|
7
|
+
export interface FeedMessage {
|
|
8
|
+
type: "feed";
|
|
9
|
+
text: string;
|
|
10
|
+
priority: Priority;
|
|
11
|
+
ts: number;
|
|
12
|
+
channel: FeedChannel;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** sinain-core → Overlay: status update */
|
|
16
|
+
export interface StatusMessage {
|
|
17
|
+
type: "status";
|
|
18
|
+
audio: string;
|
|
19
|
+
mic: string;
|
|
20
|
+
screen: string;
|
|
21
|
+
connection: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** sinain-core → Overlay: heartbeat ping */
|
|
25
|
+
export interface PingMessage {
|
|
26
|
+
type: "ping";
|
|
27
|
+
ts: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** sinain-core → Overlay: spawn task lifecycle update */
|
|
31
|
+
export type SpawnTaskStatus = "spawned" | "polling" | "completed" | "failed" | "timeout";
|
|
32
|
+
|
|
33
|
+
export interface SpawnTaskMessage {
|
|
34
|
+
type: "spawn_task";
|
|
35
|
+
taskId: string;
|
|
36
|
+
label: string;
|
|
37
|
+
status: SpawnTaskStatus;
|
|
38
|
+
startedAt: number;
|
|
39
|
+
completedAt?: number;
|
|
40
|
+
resultPreview?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Overlay → sinain-core: user typed a message */
|
|
44
|
+
export interface UserMessage {
|
|
45
|
+
type: "message";
|
|
46
|
+
text: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Overlay → sinain-core: command (toggle_audio, toggle_screen, etc.) */
|
|
50
|
+
export interface CommandMessage {
|
|
51
|
+
type: "command";
|
|
52
|
+
action: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Overlay → sinain-core: heartbeat pong */
|
|
56
|
+
export interface PongMessage {
|
|
57
|
+
type: "pong";
|
|
58
|
+
ts: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Overlay → sinain-core: process profiling metrics */
|
|
62
|
+
export interface ProfilingMessage {
|
|
63
|
+
type: "profiling";
|
|
64
|
+
rssMb: number;
|
|
65
|
+
uptimeS: number;
|
|
66
|
+
ts: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type OutboundMessage = FeedMessage | StatusMessage | PingMessage | SpawnTaskMessage;
|
|
70
|
+
export type InboundMessage = UserMessage | CommandMessage | PongMessage | ProfilingMessage;
|
|
71
|
+
|
|
72
|
+
// ── Feed buffer types ──
|
|
73
|
+
|
|
74
|
+
export interface FeedItem {
|
|
75
|
+
id: number;
|
|
76
|
+
text: string;
|
|
77
|
+
priority: Priority;
|
|
78
|
+
ts: number;
|
|
79
|
+
source: "audio" | "sense" | "agent" | "openclaw" | "system";
|
|
80
|
+
channel: FeedChannel;
|
|
81
|
+
audioSource?: AudioSourceTag;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Sense buffer types ──
|
|
85
|
+
|
|
86
|
+
export interface SenseEvent {
|
|
87
|
+
id: number;
|
|
88
|
+
type: "text" | "visual" | "context";
|
|
89
|
+
ts: number;
|
|
90
|
+
ocr: string;
|
|
91
|
+
imageData?: string; // base64 JPEG thumbnail (stripped from older events)
|
|
92
|
+
imageBbox?: number[]; // [x, y, w, h] of the captured region
|
|
93
|
+
meta: {
|
|
94
|
+
ssim: number;
|
|
95
|
+
app: string;
|
|
96
|
+
windowTitle?: string;
|
|
97
|
+
screen: number;
|
|
98
|
+
};
|
|
99
|
+
receivedAt: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Audio pipeline types ──
|
|
103
|
+
|
|
104
|
+
export type AudioSourceTag = "system" | "mic";
|
|
105
|
+
|
|
106
|
+
export interface AudioPipelineConfig {
|
|
107
|
+
device: string;
|
|
108
|
+
sampleRate: number;
|
|
109
|
+
channels: number;
|
|
110
|
+
chunkDurationMs: number;
|
|
111
|
+
vadEnabled: boolean;
|
|
112
|
+
vadThreshold: number;
|
|
113
|
+
captureCommand: "sox" | "ffmpeg" | "screencapturekit";
|
|
114
|
+
autoStart: boolean;
|
|
115
|
+
gainDb: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface TraitConfig {
|
|
119
|
+
enabled: boolean;
|
|
120
|
+
configPath: string; // path to ~/.sinain/traits.json
|
|
121
|
+
entropyHigh: boolean; // Phase 2: boosts entropy roll to 15%
|
|
122
|
+
logDir: string; // path to ~/.sinain-core/traits/
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface TraitLogEntry {
|
|
126
|
+
ts: string;
|
|
127
|
+
tickId: number;
|
|
128
|
+
enabled: boolean;
|
|
129
|
+
voice: string;
|
|
130
|
+
voice_stat: number;
|
|
131
|
+
voice_confidence: number;
|
|
132
|
+
activation_scores: Record<string, number>;
|
|
133
|
+
context_app: string;
|
|
134
|
+
hud_length: number;
|
|
135
|
+
synthesis: boolean;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
export interface AudioChunk {
|
|
140
|
+
buffer: Buffer;
|
|
141
|
+
source: string;
|
|
142
|
+
ts: number;
|
|
143
|
+
durationMs: number;
|
|
144
|
+
energy: number;
|
|
145
|
+
audioSource: AudioSourceTag;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Transcription types ──
|
|
149
|
+
|
|
150
|
+
export type TranscriptionBackend = "openrouter" | "local";
|
|
151
|
+
|
|
152
|
+
export interface TranscriptionConfig {
|
|
153
|
+
backend: TranscriptionBackend;
|
|
154
|
+
openrouterApiKey: string;
|
|
155
|
+
geminiModel: string;
|
|
156
|
+
language: string;
|
|
157
|
+
/** Local whisper-cpp settings (only used when backend=local) */
|
|
158
|
+
local: {
|
|
159
|
+
bin: string;
|
|
160
|
+
modelPath: string;
|
|
161
|
+
language: string;
|
|
162
|
+
timeoutMs: number;
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface TranscriptResult {
|
|
167
|
+
text: string;
|
|
168
|
+
source: "openrouter" | "whisper";
|
|
169
|
+
refined: boolean;
|
|
170
|
+
confidence: number;
|
|
171
|
+
ts: number;
|
|
172
|
+
audioSource: AudioSourceTag;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Recorder types ──
|
|
176
|
+
|
|
177
|
+
export interface RecordCommand {
|
|
178
|
+
command: "start" | "stop";
|
|
179
|
+
label?: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface RecorderStatus {
|
|
183
|
+
recording: boolean;
|
|
184
|
+
label: string | null;
|
|
185
|
+
startedAt: number | null;
|
|
186
|
+
segments: number;
|
|
187
|
+
durationMs: number;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface StopResult {
|
|
191
|
+
title: string;
|
|
192
|
+
transcript: string;
|
|
193
|
+
segments: number;
|
|
194
|
+
durationS: number;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── Agent types ──
|
|
198
|
+
|
|
199
|
+
export type EscalationMode = "off" | "selective" | "focus" | "rich";
|
|
200
|
+
export type ContextRichness = "lean" | "standard" | "rich";
|
|
201
|
+
|
|
202
|
+
export interface AgentConfig {
|
|
203
|
+
enabled: boolean;
|
|
204
|
+
model: string;
|
|
205
|
+
visionModel: string;
|
|
206
|
+
visionEnabled: boolean;
|
|
207
|
+
openrouterApiKey: string;
|
|
208
|
+
maxTokens: number;
|
|
209
|
+
temperature: number;
|
|
210
|
+
pushToFeed: boolean;
|
|
211
|
+
debounceMs: number;
|
|
212
|
+
maxIntervalMs: number;
|
|
213
|
+
cooldownMs: number;
|
|
214
|
+
maxAgeMs: number;
|
|
215
|
+
fallbackModels: string[];
|
|
216
|
+
/** Maximum entries to keep in agent history buffer (default: 50) */
|
|
217
|
+
historyLimit: number;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface AgentResult {
|
|
221
|
+
hud: string;
|
|
222
|
+
digest: string;
|
|
223
|
+
record?: RecordCommand;
|
|
224
|
+
task?: string;
|
|
225
|
+
latencyMs: number;
|
|
226
|
+
tokensIn: number;
|
|
227
|
+
tokensOut: number;
|
|
228
|
+
model: string;
|
|
229
|
+
parsedOk: boolean;
|
|
230
|
+
voice?: string;
|
|
231
|
+
voice_stat?: number;
|
|
232
|
+
voice_confidence?: number;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export interface AgentEntry extends AgentResult {
|
|
236
|
+
id: number;
|
|
237
|
+
ts: number;
|
|
238
|
+
pushed: boolean;
|
|
239
|
+
contextFreshnessMs: number | null;
|
|
240
|
+
context: {
|
|
241
|
+
currentApp: string;
|
|
242
|
+
appHistory: string[];
|
|
243
|
+
audioCount: number;
|
|
244
|
+
screenCount: number;
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ── Context window ──
|
|
249
|
+
|
|
250
|
+
export interface RichnessPreset {
|
|
251
|
+
maxScreenEvents: number;
|
|
252
|
+
maxAudioEntries: number;
|
|
253
|
+
maxOcrChars: number;
|
|
254
|
+
maxTranscriptChars: number;
|
|
255
|
+
maxImages: number;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export interface ContextWindow {
|
|
259
|
+
audio: FeedItem[];
|
|
260
|
+
screen: SenseEvent[];
|
|
261
|
+
images?: { data: string; app: string; ts: number }[];
|
|
262
|
+
currentApp: string;
|
|
263
|
+
appHistory: { app: string; ts: number }[];
|
|
264
|
+
audioCount: number;
|
|
265
|
+
screenCount: number;
|
|
266
|
+
windowMs: number;
|
|
267
|
+
newestEventTs: number;
|
|
268
|
+
preset: RichnessPreset;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ── Escalation types ──
|
|
272
|
+
|
|
273
|
+
export type EscalationTransport = "ws" | "http" | "auto";
|
|
274
|
+
|
|
275
|
+
export interface EscalationConfig {
|
|
276
|
+
mode: EscalationMode;
|
|
277
|
+
cooldownMs: number;
|
|
278
|
+
staleMs: number; // force escalation after this many ms of silence (0 = disabled)
|
|
279
|
+
transport: EscalationTransport;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export interface OpenClawConfig {
|
|
283
|
+
gatewayWsUrl: string;
|
|
284
|
+
gatewayToken: string;
|
|
285
|
+
hookUrl: string;
|
|
286
|
+
hookToken: string;
|
|
287
|
+
sessionKey: string;
|
|
288
|
+
phase1TimeoutMs: number; // default: 30_000
|
|
289
|
+
phase2TimeoutMs: number; // default: 120_000
|
|
290
|
+
pingIntervalMs: number; // default: 30_000
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ── Trace types ──
|
|
294
|
+
|
|
295
|
+
export interface Trace {
|
|
296
|
+
traceId: string;
|
|
297
|
+
tickId: number;
|
|
298
|
+
ts: number;
|
|
299
|
+
spans: Span[];
|
|
300
|
+
metrics: TraceMetrics;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export interface Span {
|
|
304
|
+
name: string;
|
|
305
|
+
startTs: number;
|
|
306
|
+
endTs: number;
|
|
307
|
+
attributes: Record<string, unknown>;
|
|
308
|
+
status: "ok" | "error";
|
|
309
|
+
error?: string;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export interface TraceMetrics {
|
|
313
|
+
totalLatencyMs: number;
|
|
314
|
+
llmLatencyMs: number;
|
|
315
|
+
llmInputTokens: number;
|
|
316
|
+
llmOutputTokens: number;
|
|
317
|
+
llmCost: number;
|
|
318
|
+
escalated: boolean;
|
|
319
|
+
escalationScore: number;
|
|
320
|
+
escalationLatencyMs?: number;
|
|
321
|
+
contextScreenEvents: number;
|
|
322
|
+
contextAudioEntries: number;
|
|
323
|
+
contextRichness: ContextRichness;
|
|
324
|
+
digestLength: number;
|
|
325
|
+
hudChanged: boolean;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export interface MetricsSummary {
|
|
329
|
+
count: number;
|
|
330
|
+
latencyP50: number;
|
|
331
|
+
latencyP95: number;
|
|
332
|
+
avgCostPerTick: number;
|
|
333
|
+
totalCost: number;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ── Bridge state (overlay connection) ──
|
|
337
|
+
|
|
338
|
+
export interface BridgeState {
|
|
339
|
+
audio: "active" | "muted";
|
|
340
|
+
mic: "active" | "muted";
|
|
341
|
+
screen: "active" | "off";
|
|
342
|
+
traits?: "active" | "off";
|
|
343
|
+
connection: "connected" | "disconnected" | "connecting";
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ── Learning / feedback types ──
|
|
347
|
+
|
|
348
|
+
export interface FeedbackSignals {
|
|
349
|
+
errorCleared: boolean | null;
|
|
350
|
+
noReEscalation: boolean | null;
|
|
351
|
+
dwellTimeMs: number | null;
|
|
352
|
+
quickAppSwitch: boolean | null;
|
|
353
|
+
compositeScore: number; // -1.0 to 1.0
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export interface FeedbackRecord {
|
|
357
|
+
id: string; // UUID
|
|
358
|
+
ts: number;
|
|
359
|
+
tickId: number;
|
|
360
|
+
// Input
|
|
361
|
+
digest: string;
|
|
362
|
+
hud: string;
|
|
363
|
+
currentApp: string;
|
|
364
|
+
escalationScore: number;
|
|
365
|
+
escalationReasons: string[];
|
|
366
|
+
codingContext: boolean;
|
|
367
|
+
// Output
|
|
368
|
+
escalationMessage: string; // trimmed to 2KB
|
|
369
|
+
openclawResponse: string; // trimmed to 2KB
|
|
370
|
+
responseLatencyMs: number;
|
|
371
|
+
// Feedback signals (filled async)
|
|
372
|
+
signals: FeedbackSignals;
|
|
373
|
+
tags: string[];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export interface LearningConfig {
|
|
377
|
+
enabled: boolean;
|
|
378
|
+
feedbackDir: string;
|
|
379
|
+
retentionDays: number;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ── Privacy matrix types ──
|
|
383
|
+
|
|
384
|
+
export type PrivacyLevel = "full" | "redacted" | "summary" | "none";
|
|
385
|
+
export type PrivacyDest = "local_buffer" | "local_llm" | "triple_store" | "openrouter" | "agent_gateway";
|
|
386
|
+
|
|
387
|
+
export interface PrivacyRow {
|
|
388
|
+
local_buffer: PrivacyLevel;
|
|
389
|
+
local_llm: PrivacyLevel;
|
|
390
|
+
triple_store: PrivacyLevel;
|
|
391
|
+
openrouter: PrivacyLevel;
|
|
392
|
+
agent_gateway: PrivacyLevel;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export interface PrivacyMatrix {
|
|
396
|
+
audio_transcript: PrivacyRow;
|
|
397
|
+
screen_ocr: PrivacyRow;
|
|
398
|
+
screen_images: PrivacyRow;
|
|
399
|
+
window_titles: PrivacyRow;
|
|
400
|
+
credentials: PrivacyRow;
|
|
401
|
+
metadata: PrivacyRow;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export interface PrivacyConfig {
|
|
405
|
+
mode: string; // "off" | "standard" | "strict" | "paranoid" | "custom"
|
|
406
|
+
matrix: PrivacyMatrix;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ── Full core config ──
|
|
410
|
+
|
|
411
|
+
export interface CoreConfig {
|
|
412
|
+
port: number;
|
|
413
|
+
audioConfig: AudioPipelineConfig;
|
|
414
|
+
audioAltDevice: string;
|
|
415
|
+
micConfig: AudioPipelineConfig;
|
|
416
|
+
micEnabled: boolean;
|
|
417
|
+
transcriptionConfig: TranscriptionConfig;
|
|
418
|
+
agentConfig: AgentConfig;
|
|
419
|
+
escalationConfig: EscalationConfig;
|
|
420
|
+
openclawConfig: OpenClawConfig;
|
|
421
|
+
situationMdPath: string;
|
|
422
|
+
traceEnabled: boolean;
|
|
423
|
+
traceDir: string;
|
|
424
|
+
learningConfig: LearningConfig;
|
|
425
|
+
traitConfig: TraitConfig;
|
|
426
|
+
privacyConfig: PrivacyConfig;
|
|
427
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bigram-based text deduplication for audio transcripts.
|
|
3
|
+
* Prevents repetitive audio (music, TV, looping sounds) from
|
|
4
|
+
* filling the context buffer with near-identical entries.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Extract character bigrams from text, lowercased. */
|
|
8
|
+
function bigrams(text: string): Set<string> {
|
|
9
|
+
const s = text.toLowerCase().trim();
|
|
10
|
+
const set = new Set<string>();
|
|
11
|
+
for (let i = 0; i < s.length - 1; i++) {
|
|
12
|
+
set.add(s.slice(i, i + 2));
|
|
13
|
+
}
|
|
14
|
+
return set;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Dice coefficient similarity between two strings (0.0–1.0). */
|
|
18
|
+
export function bigramSimilarity(a: string, b: string): number {
|
|
19
|
+
if (a === b) return 1.0;
|
|
20
|
+
const ba = bigrams(a);
|
|
21
|
+
const bb = bigrams(b);
|
|
22
|
+
if (ba.size === 0 && bb.size === 0) return 1.0;
|
|
23
|
+
if (ba.size === 0 || bb.size === 0) return 0.0;
|
|
24
|
+
let intersection = 0;
|
|
25
|
+
for (const bg of ba) {
|
|
26
|
+
if (bb.has(bg)) intersection++;
|
|
27
|
+
}
|
|
28
|
+
return (2 * intersection) / (ba.size + bb.size);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if a transcript is a near-duplicate of recent transcripts.
|
|
33
|
+
* Returns true if similarity > threshold against any of the recent entries.
|
|
34
|
+
*/
|
|
35
|
+
export function isDuplicateTranscript(
|
|
36
|
+
text: string,
|
|
37
|
+
recentTexts: string[],
|
|
38
|
+
threshold = 0.80,
|
|
39
|
+
): boolean {
|
|
40
|
+
const trimmed = text.trim();
|
|
41
|
+
if (trimmed.length < 5) return false; // Don't dedup very short text
|
|
42
|
+
for (const recent of recentTexts) {
|
|
43
|
+
if (bigramSimilarity(trimmed, recent) > threshold) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|