Skip to content

Commit db3ce55

Browse files
author
Shaw
committed
chore: preserve additional local work before PR merges
2 parents 7d35f6c + 148d2ac commit db3ce55

15 files changed

Lines changed: 841 additions & 25 deletions

File tree

packages/agent/src/runtime/eliza.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1717,6 +1717,7 @@ function resolveDefaultPgliteDataDir(config: ElizaConfig): string {
17171717
export function applyDatabaseConfigToEnv(config: ElizaConfig): void {
17181718
const db = config.database;
17191719
const provider = db?.provider ?? "pglite";
1720+
const databaseUrl = process.env.DATABASE_URL?.trim();
17201721

17211722
if (provider === "postgres" && db?.postgres) {
17221723
const pg = db.postgres;
@@ -1734,6 +1735,10 @@ export function applyDatabaseConfigToEnv(config: ElizaConfig): void {
17341735
process.env.POSTGRES_URL = url;
17351736
// Clear PGLite dir so plugin-sql does not fall back to PGLite
17361737
delete process.env.PGLITE_DATA_DIR;
1738+
} else if (!db?.provider && databaseUrl && !process.env.POSTGRES_URL) {
1739+
process.env.POSTGRES_URL = databaseUrl;
1740+
delete process.env.PGLITE_DATA_DIR;
1741+
logger.info("[eliza] DATABASE_URL detected: using Postgres database");
17371742
} else {
17381743
// PGLite mode (default): ensure no leftover POSTGRES_URL and pin
17391744
// PGLite to the workspace path unless overridden by config/env.

packages/app-core/src/entry.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,20 @@ if (
3535
);
3636
}
3737

38+
// Bridge DATABASE_URL → POSTGRES_URL. Cloud provisioners (docker-sandbox-provider,
39+
// k8s manifests, Railway env) inject DATABASE_URL, but plugin-sql reads
40+
// POSTGRES_URL via runtime.getSetting("POSTGRES_URL"). Without this bridge,
41+
// sandboxes silently fall back to PGLite at /root/.eliza/.elizadb instead of
42+
// connecting to the injected Neon database — losing all memories on container
43+
// restart and breaking memory transfer / centralized observability.
44+
if (process.env.DATABASE_URL && !process.env.POSTGRES_URL) {
45+
process.env.POSTGRES_URL = process.env.DATABASE_URL;
46+
// Stderr only — logger isn't initialized yet at this point in boot.
47+
process.stderr.write(
48+
"[entry] DATABASE_URL detected: bridged to POSTGRES_URL for plugin-sql\n",
49+
);
50+
}
51+
3852
// Keep `npx elizaai` startup readable by default.
3953
// This runs before CLI/runtime imports so @elizaos/core logger picks it up.
4054
if (!process.env.LOG_LEVEL) {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
export type {
2+
DiarizationSegment,
3+
OwnerChallenge,
4+
OwnerConfidence,
5+
VoiceEmbeddingSummary,
6+
VoiceProfile,
7+
VoiceProfileQuality,
8+
} from "./types.ts";
9+
export {
10+
InMemoryVoiceProfileStore,
11+
} from "./store.ts";
12+
export type { VoiceProfileSearchHit, VoiceProfileStore } from "./store.ts";
13+
export {
14+
MOCK_DIARIZATION_PIPELINE,
15+
} from "./diarization-pipeline.ts";
16+
export type { DiarizationPipeline } from "./diarization-pipeline.ts";
17+
export { scoreOwnerConfidence } from "./owner-confidence.ts";
18+
export type { OwnerConfidenceInput } from "./owner-confidence.ts";
19+
export {
20+
InMemoryChallengeService,
21+
} from "./private-challenge.ts";
22+
export type {
23+
ChallengeService,
24+
InMemoryChallengeServiceOptions,
25+
} from "./private-challenge.ts";
26+
export {
27+
NAIVE_NICKNAME_EVALUATOR,
28+
} from "./nickname-evaluator.ts";
29+
export type {
30+
NicknameEvaluator,
31+
NicknameProposal,
32+
} from "./nickname-evaluator.ts";
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
export interface NicknameProposal {
2+
nickname: string;
3+
subject: "owner" | "household";
4+
confidence: number;
5+
supportingTranscriptId: string;
6+
}
7+
8+
export interface NicknameEvaluator {
9+
evaluate(
10+
transcript: { id: string; text: string }[],
11+
): Promise<NicknameProposal[]>;
12+
}
13+
14+
interface NicknamePattern {
15+
regex: RegExp;
16+
subject: "owner" | "household";
17+
confidence: number;
18+
}
19+
20+
const PATTERNS: NicknamePattern[] = [
21+
{
22+
regex: /\bcall me ([A-Z][a-zA-Z'\- ]{0,30})\b/,
23+
subject: "owner",
24+
confidence: 0.85,
25+
},
26+
{
27+
regex: /\bmy name is ([A-Z][a-zA-Z'\- ]{0,30})\b/,
28+
subject: "owner",
29+
confidence: 0.95,
30+
},
31+
{
32+
regex: /\bI go by ([A-Z][a-zA-Z'\- ]{0,30})\b/,
33+
subject: "owner",
34+
confidence: 0.8,
35+
},
36+
];
37+
38+
function trimNickname(raw: string): string {
39+
return raw.replace(/[\s.,!?;:]+$/, "").trim();
40+
}
41+
42+
export const NAIVE_NICKNAME_EVALUATOR: NicknameEvaluator = {
43+
async evaluate(
44+
transcript: { id: string; text: string }[],
45+
): Promise<NicknameProposal[]> {
46+
const out: NicknameProposal[] = [];
47+
for (const entry of transcript) {
48+
for (const pattern of PATTERNS) {
49+
const m = pattern.regex.exec(entry.text);
50+
if (m === null) continue;
51+
const captured = m[1];
52+
if (captured === undefined) continue;
53+
const nickname = trimNickname(captured);
54+
if (nickname.length === 0) continue;
55+
out.push({
56+
nickname,
57+
subject: pattern.subject,
58+
confidence: pattern.confidence,
59+
supportingTranscriptId: entry.id,
60+
});
61+
}
62+
}
63+
return out;
64+
},
65+
};
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { OwnerConfidence } from "./types.ts";
2+
3+
export interface OwnerConfidenceInput {
4+
voiceSimilarityToOwnerProfile: number;
5+
deviceTrustLevel: "low" | "medium" | "high";
6+
recentlyAuthenticated: boolean;
7+
contextExpectsOwner: boolean;
8+
challengeRecentlyPassed: boolean;
9+
}
10+
11+
const DEVICE_TRUST_WEIGHT: Record<"low" | "medium" | "high", number> = {
12+
low: 0.0,
13+
medium: 0.1,
14+
high: 0.2,
15+
};
16+
17+
const CHALLENGE_WEIGHT = 0.45;
18+
const RECENT_AUTH_WEIGHT = 0.35;
19+
const VOICE_WEIGHT_CAP = 0.25;
20+
const CONTEXT_WEIGHT = 0.1;
21+
22+
export function scoreOwnerConfidence(
23+
input: OwnerConfidenceInput,
24+
): OwnerConfidence {
25+
const reasons: string[] = [];
26+
let score = 0;
27+
28+
if (input.challengeRecentlyPassed) {
29+
score += CHALLENGE_WEIGHT;
30+
reasons.push("challenge-recently-passed");
31+
}
32+
if (input.recentlyAuthenticated) {
33+
score += RECENT_AUTH_WEIGHT;
34+
reasons.push("recently-authenticated");
35+
}
36+
37+
const clampedVoice = Math.max(
38+
0,
39+
Math.min(1, input.voiceSimilarityToOwnerProfile),
40+
);
41+
if (clampedVoice > 0) {
42+
const voiceContribution = clampedVoice * VOICE_WEIGHT_CAP;
43+
score += voiceContribution;
44+
reasons.push(`voice-similarity:${clampedVoice.toFixed(2)}`);
45+
}
46+
47+
const trust = DEVICE_TRUST_WEIGHT[input.deviceTrustLevel];
48+
if (trust > 0) {
49+
score += trust;
50+
reasons.push(`device-trust:${input.deviceTrustLevel}`);
51+
}
52+
53+
if (input.contextExpectsOwner) {
54+
score += CONTEXT_WEIGHT;
55+
reasons.push("context-expects-owner");
56+
}
57+
58+
const clamped = Math.max(0, Math.min(1, score));
59+
return { score: clamped, reasons };
60+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { createHash, randomUUID } from "node:crypto";
2+
import type { OwnerChallenge } from "./types.ts";
3+
4+
export interface ChallengeService {
5+
issue(seed?: string): Promise<OwnerChallenge>;
6+
verify(id: string, answer: string): Promise<boolean>;
7+
}
8+
9+
export interface InMemoryChallengeServiceOptions {
10+
now?: () => number;
11+
ttlMs?: number;
12+
expectedAnswer?: string;
13+
}
14+
15+
const DEFAULT_TTL_MS = 5 * 60_000;
16+
17+
function sha256Hex(value: string): string {
18+
return createHash("sha256").update(value).digest("hex");
19+
}
20+
21+
export class InMemoryChallengeService implements ChallengeService {
22+
private readonly now: () => number;
23+
private readonly ttlMs: number;
24+
private readonly expectedAnswer: string | null;
25+
private readonly active = new Map<string, OwnerChallenge>();
26+
27+
constructor(options: InMemoryChallengeServiceOptions = {}) {
28+
this.now = options.now ?? Date.now;
29+
this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
30+
this.expectedAnswer = options.expectedAnswer ?? null;
31+
}
32+
33+
async issue(seed?: string): Promise<OwnerChallenge> {
34+
const createdAt = this.now();
35+
const id = randomUUID();
36+
const prompt = seed ?? "Confirm your private phrase";
37+
const answerSource =
38+
this.expectedAnswer ?? `${id}:${seed ?? "default"}`;
39+
const challenge: OwnerChallenge = {
40+
id,
41+
prompt,
42+
expectedAnswerHash: sha256Hex(answerSource),
43+
createdAt,
44+
expiresAt: createdAt + this.ttlMs,
45+
};
46+
this.active.set(id, challenge);
47+
return challenge;
48+
}
49+
50+
async verify(id: string, answer: string): Promise<boolean> {
51+
const challenge = this.active.get(id);
52+
if (challenge === undefined) return false;
53+
if (this.now() > challenge.expiresAt) {
54+
this.active.delete(id);
55+
return false;
56+
}
57+
const ok = sha256Hex(answer) === challenge.expectedAnswerHash;
58+
if (ok) this.active.delete(id);
59+
return ok;
60+
}
61+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
import type { AudioState, BatteryState, SystemTime, WifiState } from "../../types";
3+
import { LINUX_BRIDGE_CHANNELS } from "../bridge-contract";
4+
import { createLinuxBridgeClient } from "../client";
5+
import type { BridgeTransport } from "../transport";
6+
7+
interface Recorder {
8+
onCalls: Array<{ channel: string }>;
9+
sendCalls: Array<{ channel: string; payload: unknown }>;
10+
emit(channel: string, payload: unknown): void;
11+
}
12+
13+
function makeTransport(): { transport: BridgeTransport; rec: Recorder } {
14+
const handlers = new Map<string, Set<(payload: unknown) => void>>();
15+
const rec: Recorder = {
16+
onCalls: [],
17+
sendCalls: [],
18+
emit(channel, payload) {
19+
const set = handlers.get(channel);
20+
if (!set) return;
21+
for (const h of set) h(payload);
22+
},
23+
};
24+
const transport: BridgeTransport = {
25+
on<T>(channel: string, handler: (payload: T) => void) {
26+
rec.onCalls.push({ channel });
27+
const cast = handler as (payload: unknown) => void;
28+
let set = handlers.get(channel);
29+
if (!set) {
30+
set = new Set();
31+
handlers.set(channel, set);
32+
}
33+
set.add(cast);
34+
return () => {
35+
set?.delete(cast);
36+
};
37+
},
38+
send: vi.fn(async (channel: string, payload: unknown) => {
39+
rec.sendCalls.push({ channel, payload });
40+
return { ok: true } as never;
41+
}),
42+
};
43+
return { transport, rec };
44+
}
45+
46+
describe("createLinuxBridgeClient", () => {
47+
it("subscribes to wifi/audio/battery/time state channels", () => {
48+
const { transport, rec } = makeTransport();
49+
const client = createLinuxBridgeClient(transport);
50+
51+
const wifi: WifiState[] = [];
52+
const audio: AudioState[] = [];
53+
const battery: BatteryState[] = [];
54+
const time: SystemTime[] = [];
55+
56+
const offW = client.subscribeWifi((s) => wifi.push(s));
57+
const offA = client.subscribeAudio((s) => audio.push(s));
58+
const offB = client.subscribeBattery((s) => battery.push(s));
59+
const offT = client.subscribeTime((s) => time.push(s));
60+
61+
expect(rec.onCalls.map((c) => c.channel)).toEqual([
62+
LINUX_BRIDGE_CHANNELS.wifi.state,
63+
LINUX_BRIDGE_CHANNELS.audio.state,
64+
LINUX_BRIDGE_CHANNELS.battery.state,
65+
LINUX_BRIDGE_CHANNELS.time.state,
66+
]);
67+
68+
rec.emit(LINUX_BRIDGE_CHANNELS.wifi.state, { connected: true, ssid: "x" });
69+
rec.emit(LINUX_BRIDGE_CHANNELS.audio.state, { level: 0.3, muted: false });
70+
rec.emit(LINUX_BRIDGE_CHANNELS.battery.state, { percent: 80, charging: true });
71+
rec.emit(LINUX_BRIDGE_CHANNELS.time.state, {
72+
now: 123,
73+
locale: "en-US",
74+
timeZone: "UTC",
75+
});
76+
77+
expect(wifi).toHaveLength(1);
78+
expect(audio).toHaveLength(1);
79+
expect(battery).toHaveLength(1);
80+
expect(time).toHaveLength(1);
81+
82+
offW();
83+
offA();
84+
offB();
85+
offT();
86+
87+
rec.emit(LINUX_BRIDGE_CHANNELS.wifi.state, { connected: false });
88+
expect(wifi).toHaveLength(1);
89+
});
90+
91+
it("sends typed audio + power + settings commands", async () => {
92+
const { transport, rec } = makeTransport();
93+
const client = createLinuxBridgeClient(transport);
94+
95+
await client.setAudioLevel(0.42);
96+
await client.setAudioMuted(true);
97+
await client.shutdown();
98+
await client.restart();
99+
await client.suspend();
100+
await client.openSettings();
101+
102+
expect(rec.sendCalls).toEqual([
103+
{ channel: LINUX_BRIDGE_CHANNELS.audio.setLevel, payload: { level: 0.42 } },
104+
{ channel: LINUX_BRIDGE_CHANNELS.audio.setMuted, payload: { muted: true } },
105+
{ channel: LINUX_BRIDGE_CHANNELS.power.shutdown, payload: {} },
106+
{ channel: LINUX_BRIDGE_CHANNELS.power.restart, payload: {} },
107+
{ channel: LINUX_BRIDGE_CHANNELS.power.suspend, payload: {} },
108+
{ channel: LINUX_BRIDGE_CHANNELS.settings.open, payload: {} },
109+
]);
110+
});
111+
});

0 commit comments

Comments
 (0)