@geravant/sinain 1.0.18 → 1.1.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 +163 -1257
- package/install.js +12 -2
- package/launcher.js +622 -0
- package/openclaw.plugin.json +4 -0
- package/pack-prepare.js +48 -0
- package/package.json +26 -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 +80 -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 +812 -0
- package/sinain-core/src/escalation/message-builder.ts +323 -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 +507 -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 +417 -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/adapters/generic/adapter.ts +103 -0
- package/sinain-knowledge/adapters/interface.ts +72 -0
- package/sinain-knowledge/adapters/openclaw/adapter.ts +223 -0
- package/sinain-knowledge/curation/engine.ts +493 -0
- package/sinain-knowledge/curation/resilience.ts +336 -0
- package/sinain-knowledge/data/git-store.ts +312 -0
- package/sinain-knowledge/data/schema.ts +89 -0
- package/sinain-knowledge/data/snapshot.ts +226 -0
- package/sinain-knowledge/data/store.ts +488 -0
- package/sinain-knowledge/deploy/cli.ts +214 -0
- package/sinain-knowledge/deploy/manifest.ts +80 -0
- package/sinain-knowledge/protocol/bindings/generic.md +5 -0
- package/sinain-knowledge/protocol/bindings/openclaw.md +5 -0
- package/sinain-knowledge/protocol/heartbeat.md +62 -0
- package/sinain-knowledge/protocol/renderer.ts +56 -0
- package/sinain-knowledge/protocol/skill.md +335 -0
- package/sinain-mcp-server/index.ts +337 -0
- package/sinain-mcp-server/package.json +19 -0
- package/sinain-mcp-server/tsconfig.json +15 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { log, error } from "../log.js";
|
|
5
|
+
|
|
6
|
+
const TAG = "task-store";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Persistent task entry for spawn tasks.
|
|
10
|
+
*/
|
|
11
|
+
export interface PendingTaskEntry {
|
|
12
|
+
runId: string;
|
|
13
|
+
childSessionKey: string;
|
|
14
|
+
label?: string;
|
|
15
|
+
startedAt: number;
|
|
16
|
+
pollingEmitted: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const STORE_DIR = path.join(os.homedir(), ".sinain-core");
|
|
20
|
+
const STORE_PATH = path.join(STORE_DIR, "pending-tasks.json");
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Load pending tasks from disk.
|
|
24
|
+
* Returns empty map if file doesn't exist or is corrupted.
|
|
25
|
+
*/
|
|
26
|
+
export function loadPendingTasks(): Map<string, PendingTaskEntry> {
|
|
27
|
+
try {
|
|
28
|
+
if (!fs.existsSync(STORE_PATH)) {
|
|
29
|
+
return new Map();
|
|
30
|
+
}
|
|
31
|
+
const data = fs.readFileSync(STORE_PATH, "utf-8");
|
|
32
|
+
const parsed = JSON.parse(data);
|
|
33
|
+
if (!Array.isArray(parsed)) {
|
|
34
|
+
return new Map();
|
|
35
|
+
}
|
|
36
|
+
const map = new Map<string, PendingTaskEntry>();
|
|
37
|
+
for (const [key, value] of parsed) {
|
|
38
|
+
if (typeof key === "string" && value && typeof value.runId === "string") {
|
|
39
|
+
map.set(key, value as PendingTaskEntry);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
log(TAG, `loaded ${map.size} pending task(s) from disk`);
|
|
43
|
+
return map;
|
|
44
|
+
} catch (err: any) {
|
|
45
|
+
error(TAG, `failed to load pending tasks: ${err.message}`);
|
|
46
|
+
return new Map();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Save pending tasks to disk atomically.
|
|
52
|
+
* Uses write-then-rename for crash safety.
|
|
53
|
+
*/
|
|
54
|
+
export function savePendingTasks(tasks: Map<string, PendingTaskEntry>): void {
|
|
55
|
+
try {
|
|
56
|
+
// Ensure directory exists
|
|
57
|
+
if (!fs.existsSync(STORE_DIR)) {
|
|
58
|
+
fs.mkdirSync(STORE_DIR, { recursive: true });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Convert map to array of entries for JSON serialization
|
|
62
|
+
const entries = Array.from(tasks.entries());
|
|
63
|
+
const tmpPath = STORE_PATH + ".tmp";
|
|
64
|
+
|
|
65
|
+
fs.writeFileSync(tmpPath, JSON.stringify(entries, null, 2), "utf-8");
|
|
66
|
+
fs.renameSync(tmpPath, STORE_PATH);
|
|
67
|
+
} catch (err: any) {
|
|
68
|
+
error(TAG, `failed to save pending tasks: ${err.message}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Delete the task store file (cleanup).
|
|
74
|
+
*/
|
|
75
|
+
export function clearPendingTasks(): void {
|
|
76
|
+
try {
|
|
77
|
+
if (fs.existsSync(STORE_PATH)) {
|
|
78
|
+
fs.unlinkSync(STORE_PATH);
|
|
79
|
+
log(TAG, "cleared pending tasks file");
|
|
80
|
+
}
|
|
81
|
+
} catch (err: any) {
|
|
82
|
+
error(TAG, `failed to clear pending tasks: ${err.message}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"sourceMap": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist", "eval"]
|
|
18
|
+
}
|