@vellumai/assistant 0.3.28 → 0.4.1
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/ARCHITECTURE.md +33 -3
- package/bun.lock +4 -1
- package/docs/trusted-contact-access.md +9 -2
- package/package.json +6 -3
- package/scripts/ipc/generate-swift.ts +3 -3
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +80 -0
- package/src/__tests__/agent-loop-thinking.test.ts +1 -1
- package/src/__tests__/approval-routes-http.test.ts +13 -5
- package/src/__tests__/asset-materialize-tool.test.ts +2 -0
- package/src/__tests__/asset-search-tool.test.ts +2 -0
- package/src/__tests__/assistant-events-sse-hardening.test.ts +4 -2
- package/src/__tests__/attachments-store.test.ts +2 -0
- package/src/__tests__/browser-skill-endstate.test.ts +3 -3
- package/src/__tests__/call-controller.test.ts +30 -29
- package/src/__tests__/call-routes-http.test.ts +34 -32
- package/src/__tests__/call-start-guardian-guard.test.ts +2 -0
- package/src/__tests__/channel-invite-transport.test.ts +6 -6
- package/src/__tests__/channel-reply-delivery.test.ts +19 -0
- package/src/__tests__/channel-retry-sweep.test.ts +130 -0
- package/src/__tests__/clarification-resolver.test.ts +2 -0
- package/src/__tests__/claude-code-skill-regression.test.ts +2 -0
- package/src/__tests__/claude-code-tool-profiles.test.ts +2 -0
- package/src/__tests__/commit-message-enrichment-service.test.ts +9 -1
- package/src/__tests__/computer-use-session-lifecycle.test.ts +2 -0
- package/src/__tests__/computer-use-session-working-dir.test.ts +1 -0
- package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +2 -0
- package/src/__tests__/config-schema.test.ts +5 -5
- package/src/__tests__/config-watcher.test.ts +3 -1
- package/src/__tests__/connection-policy.test.ts +14 -5
- package/src/__tests__/contacts-tools.test.ts +3 -1
- package/src/__tests__/contradiction-checker.test.ts +2 -0
- package/src/__tests__/conversation-pairing.test.ts +10 -0
- package/src/__tests__/conversation-routes.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +16 -6
- package/src/__tests__/credential-vault-unit.test.ts +2 -2
- package/src/__tests__/credential-vault.test.ts +5 -4
- package/src/__tests__/daemon-lifecycle.test.ts +9 -0
- package/src/__tests__/daemon-server-session-init.test.ts +27 -0
- package/src/__tests__/elevenlabs-config.test.ts +2 -0
- package/src/__tests__/encrypted-store.test.ts +10 -5
- package/src/__tests__/followup-tools.test.ts +3 -1
- package/src/__tests__/gateway-only-enforcement.test.ts +21 -21
- package/src/__tests__/gmail-integration.test.ts +0 -1
- package/src/__tests__/guardian-control-plane-policy.test.ts +25 -21
- package/src/__tests__/guardian-dispatch.test.ts +2 -0
- package/src/__tests__/guardian-grant-minting.test.ts +68 -1
- package/src/__tests__/guardian-outbound-http.test.ts +12 -9
- package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
- package/src/__tests__/handle-user-message-secret-resume.test.ts +1 -0
- package/src/__tests__/handlers-slack-config.test.ts +3 -1
- package/src/__tests__/handlers-telegram-config.test.ts +3 -1
- package/src/__tests__/handlers-twilio-config.test.ts +3 -1
- package/src/__tests__/handlers-twitter-config.test.ts +3 -1
- package/src/__tests__/handlers-user-message-approval-consumption.test.ts +318 -0
- package/src/__tests__/heartbeat-service.test.ts +20 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +33 -0
- package/src/__tests__/ingress-reconcile.test.ts +3 -1
- package/src/__tests__/ingress-routes-http.test.ts +231 -4
- package/src/__tests__/intent-routing.test.ts +2 -0
- package/src/__tests__/ipc-snapshot.test.ts +13 -0
- package/src/__tests__/media-generate-image.test.ts +21 -0
- package/src/__tests__/media-reuse-story.e2e.test.ts +2 -0
- package/src/__tests__/memory-regressions.test.ts +20 -20
- package/src/__tests__/non-member-access-request.test.ts +183 -9
- package/src/__tests__/notification-decision-fallback.test.ts +2 -0
- package/src/__tests__/notification-decision-strategy.test.ts +61 -0
- package/src/__tests__/notification-guardian-path.test.ts +2 -0
- package/src/__tests__/oauth-connect-handler.test.ts +3 -1
- package/src/__tests__/oauth2-gateway-transport.test.ts +2 -0
- package/src/__tests__/onboarding-starter-tasks.test.ts +4 -4
- package/src/__tests__/pairing-routes.test.ts +171 -0
- package/src/__tests__/playbook-execution.test.ts +3 -1
- package/src/__tests__/playbook-tools.test.ts +3 -1
- package/src/__tests__/provider-error-scenarios.test.ts +59 -8
- package/src/__tests__/proxy-approval-callback.test.ts +2 -0
- package/src/__tests__/recording-handler.test.ts +11 -0
- package/src/__tests__/recording-intent-handler.test.ts +15 -0
- package/src/__tests__/recording-state-machine.test.ts +13 -2
- package/src/__tests__/registry.test.ts +7 -3
- package/src/__tests__/relay-server.test.ts +148 -28
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
- package/src/__tests__/runtime-events-sse-parity.test.ts +21 -0
- package/src/__tests__/runtime-events-sse.test.ts +4 -2
- package/src/__tests__/sandbox-diagnostics.test.ts +2 -0
- package/src/__tests__/schedule-tools.test.ts +3 -1
- package/src/__tests__/send-endpoint-busy.test.ts +288 -0
- package/src/__tests__/session-abort-tool-results.test.ts +23 -0
- package/src/__tests__/session-agent-loop.test.ts +16 -0
- package/src/__tests__/session-conflict-gate.test.ts +21 -0
- package/src/__tests__/session-load-history-repair.test.ts +27 -17
- package/src/__tests__/session-pre-run-repair.test.ts +23 -0
- package/src/__tests__/session-profile-injection.test.ts +21 -0
- package/src/__tests__/session-provider-retry-repair.test.ts +20 -0
- package/src/__tests__/session-queue.test.ts +23 -0
- package/src/__tests__/session-runtime-assembly.test.ts +50 -12
- package/src/__tests__/session-skill-tools.test.ts +27 -5
- package/src/__tests__/session-slash-known.test.ts +23 -0
- package/src/__tests__/session-slash-queue.test.ts +23 -0
- package/src/__tests__/session-slash-unknown.test.ts +23 -0
- package/src/__tests__/session-workspace-cache-state.test.ts +7 -0
- package/src/__tests__/session-workspace-injection.test.ts +21 -0
- package/src/__tests__/session-workspace-tool-tracking.test.ts +21 -0
- package/src/__tests__/shell-credential-ref.test.ts +2 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +6 -6
- package/src/__tests__/skill-load-feature-flag.test.ts +5 -4
- package/src/__tests__/skill-projection-feature-flag.test.ts +22 -0
- package/src/__tests__/skills.test.ts +8 -4
- package/src/__tests__/slack-channel-config.test.ts +3 -1
- package/src/__tests__/subagent-tools.test.ts +19 -0
- package/src/__tests__/swarm-recursion.test.ts +2 -0
- package/src/__tests__/swarm-session-integration.test.ts +2 -0
- package/src/__tests__/swarm-tool.test.ts +2 -0
- package/src/__tests__/system-prompt.test.ts +3 -1
- package/src/__tests__/task-compiler.test.ts +3 -1
- package/src/__tests__/task-management-tools.test.ts +3 -1
- package/src/__tests__/task-tools.test.ts +3 -1
- package/src/__tests__/terminal-sandbox.test.ts +13 -12
- package/src/__tests__/terminal-tools.test.ts +2 -0
- package/src/__tests__/tool-approval-handler.test.ts +15 -15
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -0
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +2 -0
- package/src/__tests__/tool-grant-request-escalation.test.ts +7 -7
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +48 -0
- package/src/__tests__/trusted-contact-multichannel.test.ts +22 -19
- package/src/__tests__/trusted-contact-verification.test.ts +91 -0
- package/src/__tests__/twilio-routes-elevenlabs.test.ts +2 -0
- package/src/__tests__/twitter-auth-handler.test.ts +3 -1
- package/src/__tests__/twitter-cli-routing.test.ts +3 -1
- package/src/__tests__/view-image-tool.test.ts +3 -1
- package/src/__tests__/voice-invite-redemption.test.ts +329 -0
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +7 -5
- package/src/__tests__/voice-session-bridge.test.ts +10 -10
- package/src/__tests__/work-item-output.test.ts +3 -1
- package/src/__tests__/workspace-lifecycle.test.ts +13 -2
- package/src/calls/call-controller.ts +26 -23
- package/src/calls/guardian-action-sweep.ts +10 -2
- package/src/calls/relay-server.ts +216 -27
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +3 -3
- package/src/cli.ts +12 -0
- package/src/config/agent-schema.ts +14 -3
- package/src/config/calls-schema.ts +6 -6
- package/src/config/core-schema.ts +3 -3
- package/src/config/feature-flag-registry.json +8 -0
- package/src/config/mcp-schema.ts +1 -1
- package/src/config/memory-schema.ts +27 -19
- package/src/config/schema.ts +21 -21
- package/src/config/skills-schema.ts +7 -7
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +139 -16
- package/src/daemon/handlers/config-inbox.ts +4 -4
- package/src/daemon/handlers/sessions.ts +148 -4
- package/src/daemon/ipc-contract/messages.ts +16 -0
- package/src/daemon/ipc-contract-inventory.json +1 -0
- package/src/daemon/lifecycle.ts +19 -0
- package/src/daemon/pairing-store.ts +86 -3
- package/src/daemon/response-tier.ts +6 -5
- package/src/daemon/session-agent-loop.ts +5 -5
- package/src/daemon/session-lifecycle.ts +25 -17
- package/src/daemon/session-memory.ts +2 -2
- package/src/daemon/session-process.ts +1 -20
- package/src/daemon/session-runtime-assembly.ts +28 -22
- package/src/daemon/session-tool-setup.ts +2 -2
- package/src/daemon/session.ts +3 -3
- package/src/memory/canonical-guardian-store.ts +63 -1
- package/src/memory/channel-guardian-store.ts +1 -0
- package/src/memory/conversation-crud.ts +7 -7
- package/src/memory/db-init.ts +4 -0
- package/src/memory/embedding-local.ts +257 -39
- package/src/memory/embedding-runtime-manager.ts +471 -0
- package/src/memory/guardian-bindings.ts +25 -1
- package/src/memory/indexer.ts +3 -3
- package/src/memory/ingress-invite-store.ts +45 -0
- package/src/memory/job-handlers/backfill.ts +16 -9
- package/src/memory/migrations/037-voice-invite-columns.ts +16 -0
- package/src/memory/migrations/index.ts +1 -0
- package/src/memory/qdrant-client.ts +31 -22
- package/src/memory/schema.ts +4 -0
- package/src/notifications/copy-composer.ts +15 -0
- package/src/runtime/access-request-helper.ts +43 -7
- package/src/runtime/actor-trust-resolver.ts +46 -50
- package/src/runtime/channel-invite-transports/voice.ts +58 -0
- package/src/runtime/channel-retry-sweep.ts +18 -6
- package/src/runtime/guardian-context-resolver.ts +38 -96
- package/src/runtime/guardian-reply-router.ts +31 -1
- package/src/runtime/ingress-service.ts +80 -3
- package/src/runtime/invite-redemption-service.ts +141 -2
- package/src/runtime/routes/channel-route-shared.ts +1 -1
- package/src/runtime/routes/channel-routes.ts +1 -1
- package/src/runtime/routes/conversation-routes.ts +166 -2
- package/src/runtime/routes/guardian-approval-interception.ts +17 -6
- package/src/runtime/routes/inbound-message-handler.ts +41 -10
- package/src/runtime/routes/ingress-routes.ts +52 -4
- package/src/runtime/routes/pairing-routes.ts +3 -0
- package/src/tools/guardian-control-plane-policy.ts +2 -2
- package/src/tools/reminder/reminder-store.ts +10 -14
- package/src/tools/tool-approval-handler.ts +11 -11
- package/src/tools/types.ts +2 -2
- package/src/util/logger.ts +20 -8
- package/src/util/platform.ts +10 -0
- package/src/util/voice-code.ts +29 -0
- package/src/daemon/guardian-invite-intent.ts +0 -124
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Downloads and manages the local embedding runtime post-hatch.
|
|
3
|
+
*
|
|
4
|
+
* Instead of shipping heavy native + JS dependencies inside the .app bundle,
|
|
5
|
+
* we download them from npm and run embeddings in a **separate bun process**.
|
|
6
|
+
* The compiled daemon binary cannot resolve bare specifiers in dynamically
|
|
7
|
+
* imported files, so we spawn a standalone bun process that runs an embed
|
|
8
|
+
* worker script communicating via JSON-lines over stdin/stdout.
|
|
9
|
+
*
|
|
10
|
+
* The runtime is stored in ~/.vellum/workspace/embedding-models/ and used
|
|
11
|
+
* by embedding-local.ts on demand.
|
|
12
|
+
*
|
|
13
|
+
* Follows the same download/install pattern as qdrant-manager.ts.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { arch, platform } from 'node:os';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
|
|
20
|
+
import { getLogger } from '../util/logger.js';
|
|
21
|
+
import { getEmbeddingModelsDir } from '../util/platform.js';
|
|
22
|
+
import { PromiseGuard } from '../util/promise-guard.js';
|
|
23
|
+
|
|
24
|
+
const log = getLogger('embedding-runtime-manager');
|
|
25
|
+
|
|
26
|
+
// Pinned versions matching assistant/bun.lock
|
|
27
|
+
const ONNXRUNTIME_NODE_VERSION = '1.21.0';
|
|
28
|
+
const ONNXRUNTIME_COMMON_VERSION = '1.21.0';
|
|
29
|
+
const TRANSFORMERS_VERSION = '3.8.1';
|
|
30
|
+
|
|
31
|
+
/** Bun version to download when system bun is not available. */
|
|
32
|
+
const BUN_VERSION = '1.2.0';
|
|
33
|
+
|
|
34
|
+
/** Composite version string for cache invalidation. */
|
|
35
|
+
const RUNTIME_VERSION = `ort-${ONNXRUNTIME_NODE_VERSION}_hf-${TRANSFORMERS_VERSION}`;
|
|
36
|
+
|
|
37
|
+
const WORKER_FILENAME = 'embed-worker.mjs';
|
|
38
|
+
|
|
39
|
+
/** Module-level guard so concurrent in-process calls share one download. */
|
|
40
|
+
const installGuard = new PromiseGuard<void>();
|
|
41
|
+
|
|
42
|
+
interface VersionManifest {
|
|
43
|
+
runtimeVersion: string;
|
|
44
|
+
onnxruntimeNodeVersion: string;
|
|
45
|
+
onnxruntimeCommonVersion: string;
|
|
46
|
+
transformersVersion: string;
|
|
47
|
+
platform: string;
|
|
48
|
+
arch: string;
|
|
49
|
+
installedAt: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── npm tarball helpers ─────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
function npmTarballUrl(pkg: string, version: string): string {
|
|
55
|
+
// Scoped packages encode the scope in the URL
|
|
56
|
+
const encoded = pkg.replace('/', '%2f');
|
|
57
|
+
const basename = pkg.startsWith('@') ? pkg.split('/')[1] : pkg;
|
|
58
|
+
return `https://registry.npmjs.org/${encoded}/-/${basename}-${version}.tgz`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function downloadAndExtract(
|
|
62
|
+
url: string,
|
|
63
|
+
targetDir: string,
|
|
64
|
+
signal?: AbortSignal,
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
log.info({ url, targetDir }, 'Downloading npm package');
|
|
67
|
+
|
|
68
|
+
const response = await fetch(url, { signal });
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
throw new Error(`Failed to download ${url}: ${response.status} ${response.statusText}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const tarball = await response.arrayBuffer();
|
|
74
|
+
|
|
75
|
+
// npm tarballs extract to package/, we need to redirect to targetDir
|
|
76
|
+
mkdirSync(targetDir, { recursive: true });
|
|
77
|
+
|
|
78
|
+
const tmpTar = join(targetDir, `download-${Date.now()}.tgz`);
|
|
79
|
+
writeFileSync(tmpTar, Buffer.from(tarball));
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
// Extract tarball, stripping the leading "package/" directory
|
|
83
|
+
const proc = Bun.spawn({
|
|
84
|
+
cmd: ['tar', 'xzf', tmpTar, '-C', targetDir, '--strip-components=1'],
|
|
85
|
+
stdout: 'ignore',
|
|
86
|
+
stderr: 'pipe',
|
|
87
|
+
});
|
|
88
|
+
await proc.exited;
|
|
89
|
+
if (proc.exitCode !== 0) {
|
|
90
|
+
const stderr = await new Response(proc.stderr).text();
|
|
91
|
+
throw new Error(`Failed to extract ${url}: ${stderr}`);
|
|
92
|
+
}
|
|
93
|
+
} finally {
|
|
94
|
+
try { rmSync(tmpTar); } catch { /* ignore */ }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Worker script content ───────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
function generateWorkerScript(): string {
|
|
101
|
+
// This script is run by a standalone bun process (not the compiled daemon).
|
|
102
|
+
// Because it runs in a real bun runtime, bare specifier resolution works
|
|
103
|
+
// normally — node_modules/ in the same directory is found automatically.
|
|
104
|
+
return `\
|
|
105
|
+
// embed-worker.mjs — Auto-generated by EmbeddingRuntimeManager
|
|
106
|
+
// Runs in a separate bun process, communicates via JSON-lines over stdin/stdout.
|
|
107
|
+
import { pipeline, env } from '@huggingface/transformers';
|
|
108
|
+
|
|
109
|
+
const model = process.argv[2];
|
|
110
|
+
const cacheDir = process.argv[3];
|
|
111
|
+
if (cacheDir && env) env.cacheDir = cacheDir;
|
|
112
|
+
|
|
113
|
+
let extractor;
|
|
114
|
+
try {
|
|
115
|
+
extractor = await pipeline('feature-extraction', model, { dtype: 'fp32' });
|
|
116
|
+
process.stdout.write(JSON.stringify({ type: 'ready' }) + '\\n');
|
|
117
|
+
} catch (err) {
|
|
118
|
+
process.stdout.write(JSON.stringify({ type: 'error', error: err.message || String(err) }) + '\\n');
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Sequential request queue to avoid concurrent ONNX inference
|
|
123
|
+
const decoder = new TextDecoder();
|
|
124
|
+
let buffer = '';
|
|
125
|
+
let processing = false;
|
|
126
|
+
const queue = [];
|
|
127
|
+
|
|
128
|
+
process.stdin.on('data', (chunk) => {
|
|
129
|
+
buffer += typeof chunk === 'string' ? chunk : decoder.decode(chunk, { stream: true });
|
|
130
|
+
let idx;
|
|
131
|
+
while ((idx = buffer.indexOf('\\n')) !== -1) {
|
|
132
|
+
const line = buffer.slice(0, idx);
|
|
133
|
+
buffer = buffer.slice(idx + 1);
|
|
134
|
+
if (line.trim()) queue.push(line);
|
|
135
|
+
}
|
|
136
|
+
if (!processing) processQueue();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
async function processQueue() {
|
|
140
|
+
processing = true;
|
|
141
|
+
while (queue.length > 0) {
|
|
142
|
+
const line = queue.shift();
|
|
143
|
+
let req;
|
|
144
|
+
try { req = JSON.parse(line); } catch { continue; }
|
|
145
|
+
try {
|
|
146
|
+
const output = await extractor(req.texts, { pooling: 'cls', normalize: true });
|
|
147
|
+
const vectors = output.tolist();
|
|
148
|
+
process.stdout.write(JSON.stringify({ id: req.id, vectors }) + '\\n');
|
|
149
|
+
} catch (err) {
|
|
150
|
+
process.stdout.write(JSON.stringify({ id: req.id, error: err.message || String(err) }) + '\\n');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
processing = false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
process.stdin.on('end', () => process.exit(0));
|
|
157
|
+
`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── Main manager ────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
export class EmbeddingRuntimeManager {
|
|
163
|
+
private readonly baseDir: string;
|
|
164
|
+
|
|
165
|
+
constructor(baseDir?: string) {
|
|
166
|
+
this.baseDir = baseDir ?? getEmbeddingModelsDir();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Check if the embedding runtime is installed and up-to-date. */
|
|
170
|
+
isReady(): boolean {
|
|
171
|
+
const manifest = this.readManifest();
|
|
172
|
+
if (!manifest) return false;
|
|
173
|
+
if (manifest.runtimeVersion !== RUNTIME_VERSION) return false;
|
|
174
|
+
|
|
175
|
+
// Verify the worker script exists and a bun binary is available
|
|
176
|
+
return existsSync(this.getWorkerPath()) && this.getBunPath() !== undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Path to the embed worker script. */
|
|
180
|
+
getWorkerPath(): string {
|
|
181
|
+
return join(this.baseDir, WORKER_FILENAME);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Find a usable bun binary.
|
|
186
|
+
* Checks: downloaded copy → common install locations.
|
|
187
|
+
*/
|
|
188
|
+
getBunPath(): string | undefined {
|
|
189
|
+
// 1. Downloaded bun
|
|
190
|
+
const downloadedBun = join(this.baseDir, 'bin', 'bun');
|
|
191
|
+
if (existsSync(downloadedBun)) return downloadedBun;
|
|
192
|
+
|
|
193
|
+
// 2. Common installation paths — the compiled daemon inherits a
|
|
194
|
+
// restricted PATH, so we check well-known prefixes directly.
|
|
195
|
+
const home = process.env.HOME ?? '';
|
|
196
|
+
for (const p of [
|
|
197
|
+
join(home, '.bun', 'bin', 'bun'),
|
|
198
|
+
'/opt/homebrew/bin/bun',
|
|
199
|
+
'/usr/local/bin/bun',
|
|
200
|
+
]) {
|
|
201
|
+
if (existsSync(p)) return p;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Download and install the embedding runtime if not already present.
|
|
209
|
+
* Safe to call concurrently — in-process calls share one promise via
|
|
210
|
+
* PromiseGuard, and cross-process calls are serialized via a lock file.
|
|
211
|
+
*/
|
|
212
|
+
async ensureInstalled(signal?: AbortSignal): Promise<void> {
|
|
213
|
+
if (this.isReady()) return;
|
|
214
|
+
|
|
215
|
+
// Deduplicate concurrent in-process calls
|
|
216
|
+
await installGuard.run(() => this.acquireLockAndInstall(signal));
|
|
217
|
+
|
|
218
|
+
// If another process was downloading and we skipped, or if the download
|
|
219
|
+
// somehow failed silently, reset the guard so we can retry next time.
|
|
220
|
+
if (!this.isReady()) {
|
|
221
|
+
installGuard.reset();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private async acquireLockAndInstall(signal?: AbortSignal): Promise<void> {
|
|
226
|
+
// Re-check after acquiring the in-process guard
|
|
227
|
+
if (this.isReady()) return;
|
|
228
|
+
|
|
229
|
+
// Cross-process lock to prevent duplicate downloads
|
|
230
|
+
const lockPath = join(this.baseDir, '.downloading');
|
|
231
|
+
if (existsSync(lockPath)) {
|
|
232
|
+
try {
|
|
233
|
+
const lockContent = readFileSync(lockPath, 'utf-8').trim();
|
|
234
|
+
const lockPid = parseInt(lockContent, 10);
|
|
235
|
+
if (!isNaN(lockPid) && lockPid !== process.pid) {
|
|
236
|
+
try {
|
|
237
|
+
process.kill(lockPid, 0);
|
|
238
|
+
log.info({ lockPid }, 'Another process is downloading the embedding runtime, skipping');
|
|
239
|
+
return;
|
|
240
|
+
} catch {
|
|
241
|
+
log.info({ lockPid }, 'Cleaning up stale download lock');
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
// Can't read lock file, proceed
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
mkdirSync(this.baseDir, { recursive: true });
|
|
250
|
+
|
|
251
|
+
// Write a .gitignore so the workspace git repo ignores this directory
|
|
252
|
+
const gitignorePath = join(this.baseDir, '.gitignore');
|
|
253
|
+
if (!existsSync(gitignorePath)) {
|
|
254
|
+
writeFileSync(gitignorePath, '*\n!.gitignore\n');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
writeFileSync(lockPath, String(process.pid));
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
await this.install(signal);
|
|
261
|
+
} finally {
|
|
262
|
+
try { rmSync(lockPath); } catch { /* ignore */ }
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private async install(signal?: AbortSignal): Promise<void> {
|
|
267
|
+
const os = platform();
|
|
268
|
+
const cpu = arch();
|
|
269
|
+
log.info({ os, cpu, runtimeVersion: RUNTIME_VERSION }, 'Installing embedding runtime');
|
|
270
|
+
|
|
271
|
+
// Work in a temp directory for atomic install
|
|
272
|
+
const tmpDir = join(this.baseDir, `.installing-${Date.now()}`);
|
|
273
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
// Step 1: Download npm packages (and bun if needed) in parallel
|
|
277
|
+
const nodeModules = join(tmpDir, 'node_modules');
|
|
278
|
+
const downloads: Promise<void>[] = [
|
|
279
|
+
downloadAndExtract(
|
|
280
|
+
npmTarballUrl('onnxruntime-node', ONNXRUNTIME_NODE_VERSION),
|
|
281
|
+
join(nodeModules, 'onnxruntime-node'),
|
|
282
|
+
signal,
|
|
283
|
+
),
|
|
284
|
+
downloadAndExtract(
|
|
285
|
+
npmTarballUrl('onnxruntime-common', ONNXRUNTIME_COMMON_VERSION),
|
|
286
|
+
join(nodeModules, 'onnxruntime-common'),
|
|
287
|
+
signal,
|
|
288
|
+
),
|
|
289
|
+
downloadAndExtract(
|
|
290
|
+
npmTarballUrl('@huggingface/transformers', TRANSFORMERS_VERSION),
|
|
291
|
+
join(nodeModules, '@huggingface', 'transformers'),
|
|
292
|
+
signal,
|
|
293
|
+
),
|
|
294
|
+
];
|
|
295
|
+
|
|
296
|
+
// Download bun binary if not already available on the system
|
|
297
|
+
if (!this.getBunPath()) {
|
|
298
|
+
downloads.push(this.downloadBunBinary(tmpDir, signal));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
await Promise.all(downloads);
|
|
302
|
+
|
|
303
|
+
if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');
|
|
304
|
+
|
|
305
|
+
log.info('npm packages downloaded, stripping non-platform binaries');
|
|
306
|
+
|
|
307
|
+
// Step 2: Strip non-platform native binaries
|
|
308
|
+
const onnxBinDir = join(nodeModules, 'onnxruntime-node', 'bin', 'napi-v3');
|
|
309
|
+
if (existsSync(onnxBinDir)) {
|
|
310
|
+
const entries = readdirSync(onnxBinDir);
|
|
311
|
+
for (const entry of entries) {
|
|
312
|
+
// Keep all darwin architectures (arm64 and x86_64) since uname -m
|
|
313
|
+
// is unreliable under Rosetta (returns x86_64 on Apple Silicon)
|
|
314
|
+
if (entry !== os) {
|
|
315
|
+
rmSync(join(onnxBinDir, entry), { recursive: true, force: true });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Strip non-runtime files to reduce disk usage.
|
|
321
|
+
// Keep lib/ directories — they contain JS entry points needed for bare
|
|
322
|
+
// specifier imports in the worker subprocess.
|
|
323
|
+
const onnxNodeDir = join(nodeModules, 'onnxruntime-node');
|
|
324
|
+
rmSync(join(onnxNodeDir, 'script'), { recursive: true, force: true });
|
|
325
|
+
rmSync(join(onnxNodeDir, 'README.md'), { force: true });
|
|
326
|
+
rmSync(join(nodeModules, 'onnxruntime-common', 'README.md'), { force: true });
|
|
327
|
+
|
|
328
|
+
// Step 3: Create a stub "sharp" package so that the pre-built
|
|
329
|
+
// transformers.node.mjs can import it without error. The bundle checks
|
|
330
|
+
// `if (sharp)` at module initialization time — the stub must be truthy.
|
|
331
|
+
// We only use text embeddings, never image processing.
|
|
332
|
+
const sharpDir = join(nodeModules, 'sharp');
|
|
333
|
+
mkdirSync(sharpDir, { recursive: true });
|
|
334
|
+
writeFileSync(join(sharpDir, 'package.json'), '{"name":"sharp","version":"0.0.0","main":"index.js"}\n');
|
|
335
|
+
writeFileSync(join(sharpDir, 'index.js'), [
|
|
336
|
+
'// Stub: only text embeddings are used, no image processing.',
|
|
337
|
+
'// Must be a truthy function so transformers.node.mjs initialization passes.',
|
|
338
|
+
'function sharp() { throw new Error("sharp stub: image processing not available"); }',
|
|
339
|
+
'sharp.format = {};',
|
|
340
|
+
'module.exports = sharp;',
|
|
341
|
+
'',
|
|
342
|
+
].join('\n'));
|
|
343
|
+
|
|
344
|
+
// Step 4: Write embed worker script
|
|
345
|
+
writeFileSync(join(tmpDir, WORKER_FILENAME), generateWorkerScript());
|
|
346
|
+
|
|
347
|
+
// Step 5: Write version manifest
|
|
348
|
+
const manifest: VersionManifest = {
|
|
349
|
+
runtimeVersion: RUNTIME_VERSION,
|
|
350
|
+
onnxruntimeNodeVersion: ONNXRUNTIME_NODE_VERSION,
|
|
351
|
+
onnxruntimeCommonVersion: ONNXRUNTIME_COMMON_VERSION,
|
|
352
|
+
transformersVersion: TRANSFORMERS_VERSION,
|
|
353
|
+
platform: os,
|
|
354
|
+
arch: cpu,
|
|
355
|
+
installedAt: new Date().toISOString(),
|
|
356
|
+
};
|
|
357
|
+
writeFileSync(join(tmpDir, 'version.json'), JSON.stringify(manifest, null, 2) + '\n');
|
|
358
|
+
|
|
359
|
+
// Step 6: Atomic swap — remove old install and rename temp to final
|
|
360
|
+
// Preserve model-cache/, bin/ (downloaded bun), and .gitignore
|
|
361
|
+
const modelCacheDir = join(this.baseDir, 'model-cache');
|
|
362
|
+
const hadModelCache = existsSync(modelCacheDir);
|
|
363
|
+
let tmpModelCache: string | null = null;
|
|
364
|
+
if (hadModelCache) {
|
|
365
|
+
tmpModelCache = join(this.baseDir, `.model-cache-preserve-${Date.now()}`);
|
|
366
|
+
renameSync(modelCacheDir, tmpModelCache);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Preserve downloaded bun binary if it exists and we didn't just download a new one
|
|
370
|
+
const existingBinDir = join(this.baseDir, 'bin');
|
|
371
|
+
const newBinDir = join(tmpDir, 'bin');
|
|
372
|
+
const hadBinDir = existsSync(existingBinDir) && !existsSync(newBinDir);
|
|
373
|
+
let tmpBinDir: string | null = null;
|
|
374
|
+
if (hadBinDir) {
|
|
375
|
+
tmpBinDir = join(this.baseDir, `.bin-preserve-${Date.now()}`);
|
|
376
|
+
renameSync(existingBinDir, tmpBinDir);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Remove old install (preserving dotfiles like .gitignore, .downloading, temp dirs)
|
|
380
|
+
for (const entry of readdirSync(this.baseDir)) {
|
|
381
|
+
if (entry.startsWith('.') || entry === tmpDir.split('/').pop()) continue;
|
|
382
|
+
rmSync(join(this.baseDir, entry), { recursive: true, force: true });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Move new files into place
|
|
386
|
+
for (const entry of readdirSync(tmpDir)) {
|
|
387
|
+
renameSync(join(tmpDir, entry), join(this.baseDir, entry));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Restore model cache
|
|
391
|
+
if (tmpModelCache && existsSync(tmpModelCache)) {
|
|
392
|
+
renameSync(tmpModelCache, modelCacheDir);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Restore bun binary
|
|
396
|
+
if (tmpBinDir && existsSync(tmpBinDir)) {
|
|
397
|
+
renameSync(tmpBinDir, existingBinDir);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
log.info({ runtimeVersion: RUNTIME_VERSION }, 'Embedding runtime installed successfully');
|
|
401
|
+
} catch (err) {
|
|
402
|
+
log.error({ err }, 'Failed to install embedding runtime');
|
|
403
|
+
throw err;
|
|
404
|
+
} finally {
|
|
405
|
+
// Clean up temp directory
|
|
406
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private async downloadBunBinary(installDir: string, signal?: AbortSignal): Promise<void> {
|
|
411
|
+
const os = platform();
|
|
412
|
+
const cpu = arch() === 'arm64' ? 'aarch64' : arch();
|
|
413
|
+
const target = `${os}-${cpu}`;
|
|
414
|
+
const url = `https://github.com/oven-sh/bun/releases/download/bun-v${BUN_VERSION}/bun-${target}.zip`;
|
|
415
|
+
|
|
416
|
+
log.info({ url, target, bunVersion: BUN_VERSION }, 'Downloading bun binary');
|
|
417
|
+
|
|
418
|
+
const response = await fetch(url, {
|
|
419
|
+
signal,
|
|
420
|
+
redirect: 'follow',
|
|
421
|
+
});
|
|
422
|
+
if (!response.ok) {
|
|
423
|
+
throw new Error(`Failed to download bun: ${response.status} ${response.statusText}`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const zipData = await response.arrayBuffer();
|
|
427
|
+
const binDir = join(installDir, 'bin');
|
|
428
|
+
mkdirSync(binDir, { recursive: true });
|
|
429
|
+
|
|
430
|
+
const tmpZip = join(binDir, `bun-download-${Date.now()}.zip`);
|
|
431
|
+
writeFileSync(tmpZip, Buffer.from(zipData));
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
// Extract zip
|
|
435
|
+
const proc = Bun.spawn({
|
|
436
|
+
cmd: ['unzip', '-o', tmpZip, '-d', binDir],
|
|
437
|
+
stdout: 'ignore',
|
|
438
|
+
stderr: 'pipe',
|
|
439
|
+
});
|
|
440
|
+
await proc.exited;
|
|
441
|
+
if (proc.exitCode !== 0) {
|
|
442
|
+
const stderr = await new Response(proc.stderr).text();
|
|
443
|
+
throw new Error(`Failed to extract bun zip: ${stderr}`);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Move binary from bun-{target}/bun to bin/bun
|
|
447
|
+
const extractedBun = join(binDir, `bun-${target}`, 'bun');
|
|
448
|
+
if (existsSync(extractedBun)) {
|
|
449
|
+
renameSync(extractedBun, join(binDir, 'bun'));
|
|
450
|
+
rmSync(join(binDir, `bun-${target}`), { recursive: true, force: true });
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Make executable
|
|
454
|
+
chmodSync(join(binDir, 'bun'), 0o755);
|
|
455
|
+
} finally {
|
|
456
|
+
try { rmSync(tmpZip); } catch { /* ignore */ }
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
log.info('Bun binary downloaded successfully');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private readManifest(): VersionManifest | null {
|
|
463
|
+
const manifestPath = join(this.baseDir, 'version.json');
|
|
464
|
+
if (!existsSync(manifestPath)) return null;
|
|
465
|
+
try {
|
|
466
|
+
return JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
467
|
+
} catch {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* for a given (assistantId, channel) pair.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { and, eq } from 'drizzle-orm';
|
|
8
|
+
import { and, asc, desc, eq } from 'drizzle-orm';
|
|
9
9
|
import { v4 as uuid } from 'uuid';
|
|
10
10
|
|
|
11
11
|
import { getDb } from './db.js';
|
|
@@ -103,6 +103,30 @@ export function getActiveBinding(assistantId: string, channel: string): Guardian
|
|
|
103
103
|
return row ? rowToBinding(row) : null;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* List all active guardian bindings for an assistant across all channels.
|
|
108
|
+
* Deterministic ordering: verifiedAt DESC (most recently verified first),
|
|
109
|
+
* then channel ASC (alphabetical tiebreaker).
|
|
110
|
+
*/
|
|
111
|
+
export function listActiveBindingsByAssistant(assistantId: string): GuardianBinding[] {
|
|
112
|
+
const db = getDb();
|
|
113
|
+
return db
|
|
114
|
+
.select()
|
|
115
|
+
.from(channelGuardianBindings)
|
|
116
|
+
.where(
|
|
117
|
+
and(
|
|
118
|
+
eq(channelGuardianBindings.assistantId, assistantId),
|
|
119
|
+
eq(channelGuardianBindings.status, 'active'),
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
.orderBy(
|
|
123
|
+
desc(channelGuardianBindings.verifiedAt),
|
|
124
|
+
asc(channelGuardianBindings.channel),
|
|
125
|
+
)
|
|
126
|
+
.all()
|
|
127
|
+
.map(rowToBinding);
|
|
128
|
+
}
|
|
129
|
+
|
|
106
130
|
export function revokeBinding(assistantId: string, channel: string): boolean {
|
|
107
131
|
const db = getDb();
|
|
108
132
|
const now = Date.now();
|
package/src/memory/indexer.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface IndexMessageInput {
|
|
|
23
23
|
createdAt: number;
|
|
24
24
|
scopeId?: string;
|
|
25
25
|
// Provenance for trust-aware extraction gating (M3)
|
|
26
|
-
|
|
26
|
+
provenanceTrustClass?: 'guardian' | 'trusted_contact' | 'unknown';
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export interface IndexMessageResult {
|
|
@@ -39,7 +39,7 @@ export function indexMessageNow(
|
|
|
39
39
|
|
|
40
40
|
// Provenance-based trust gating: only guardian and legacy (undefined) actors
|
|
41
41
|
// are trusted for extraction and conflict resolution.
|
|
42
|
-
const isTrustedActor = input.
|
|
42
|
+
const isTrustedActor = input.provenanceTrustClass === 'guardian' || input.provenanceTrustClass === undefined;
|
|
43
43
|
|
|
44
44
|
const text = extractTextFromStoredMessageContent(input.content);
|
|
45
45
|
if (text.length === 0) {
|
|
@@ -123,7 +123,7 @@ export function indexMessageNow(
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
if (!isTrustedActor && (shouldExtract || shouldResolveConflicts)) {
|
|
126
|
-
log.info(`Skipping extraction/conflict jobs for untrusted actor (
|
|
126
|
+
log.info(`Skipping extraction/conflict jobs for untrusted actor (trustClass=${input.provenanceTrustClass})`);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
enqueueSummaryRollupJobsIfDue();
|
|
@@ -33,6 +33,10 @@ export interface IngressInvite {
|
|
|
33
33
|
redeemedByExternalUserId: string | null;
|
|
34
34
|
redeemedByExternalChatId: string | null;
|
|
35
35
|
redeemedAt: number | null;
|
|
36
|
+
// Voice invite fields (null for non-voice invites)
|
|
37
|
+
expectedExternalUserId: string | null;
|
|
38
|
+
voiceCodeHash: string | null;
|
|
39
|
+
voiceCodeDigits: number | null;
|
|
36
40
|
createdAt: number;
|
|
37
41
|
updatedAt: number;
|
|
38
42
|
}
|
|
@@ -90,6 +94,9 @@ function rowToInvite(row: typeof assistantIngressInvites.$inferSelect): IngressI
|
|
|
90
94
|
redeemedByExternalUserId: row.redeemedByExternalUserId,
|
|
91
95
|
redeemedByExternalChatId: row.redeemedByExternalChatId,
|
|
92
96
|
redeemedAt: row.redeemedAt,
|
|
97
|
+
expectedExternalUserId: row.expectedExternalUserId,
|
|
98
|
+
voiceCodeHash: row.voiceCodeHash,
|
|
99
|
+
voiceCodeDigits: row.voiceCodeDigits,
|
|
93
100
|
createdAt: row.createdAt,
|
|
94
101
|
updatedAt: row.updatedAt,
|
|
95
102
|
};
|
|
@@ -127,6 +134,10 @@ export function createInvite(params: {
|
|
|
127
134
|
note?: string;
|
|
128
135
|
maxUses?: number;
|
|
129
136
|
expiresInMs?: number;
|
|
137
|
+
// Voice invite metadata (all optional — omitted for non-voice invites)
|
|
138
|
+
expectedExternalUserId?: string;
|
|
139
|
+
voiceCodeHash?: string;
|
|
140
|
+
voiceCodeDigits?: number;
|
|
130
141
|
}): { invite: IngressInvite; rawToken: string } {
|
|
131
142
|
const db = getDb();
|
|
132
143
|
const now = Date.now();
|
|
@@ -148,6 +159,9 @@ export function createInvite(params: {
|
|
|
148
159
|
redeemedByExternalUserId: null,
|
|
149
160
|
redeemedByExternalChatId: null,
|
|
150
161
|
redeemedAt: null,
|
|
162
|
+
expectedExternalUserId: params.expectedExternalUserId ?? null,
|
|
163
|
+
voiceCodeHash: params.voiceCodeHash ?? null,
|
|
164
|
+
voiceCodeDigits: params.voiceCodeDigits ?? null,
|
|
151
165
|
createdAt: now,
|
|
152
166
|
updatedAt: now,
|
|
153
167
|
};
|
|
@@ -432,3 +446,34 @@ export function findByTokenHash(tokenHash: string): IngressInvite | null {
|
|
|
432
446
|
|
|
433
447
|
return row ? rowToInvite(row) : null;
|
|
434
448
|
}
|
|
449
|
+
|
|
450
|
+
// ---------------------------------------------------------------------------
|
|
451
|
+
// findActiveVoiceInvites
|
|
452
|
+
// ---------------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Find all active voice invites bound to a specific caller identity.
|
|
456
|
+
* Used by the voice invite redemption flow to locate candidate invites
|
|
457
|
+
* before code hash matching.
|
|
458
|
+
*/
|
|
459
|
+
export function findActiveVoiceInvites(params: {
|
|
460
|
+
assistantId: string;
|
|
461
|
+
expectedExternalUserId: string;
|
|
462
|
+
}): IngressInvite[] {
|
|
463
|
+
const db = getDb();
|
|
464
|
+
|
|
465
|
+
const rows = db
|
|
466
|
+
.select()
|
|
467
|
+
.from(assistantIngressInvites)
|
|
468
|
+
.where(
|
|
469
|
+
and(
|
|
470
|
+
eq(assistantIngressInvites.assistantId, params.assistantId),
|
|
471
|
+
eq(assistantIngressInvites.sourceChannel, 'voice'),
|
|
472
|
+
eq(assistantIngressInvites.status, 'active'),
|
|
473
|
+
eq(assistantIngressInvites.expectedExternalUserId, params.expectedExternalUserId),
|
|
474
|
+
),
|
|
475
|
+
)
|
|
476
|
+
.all();
|
|
477
|
+
|
|
478
|
+
return rows.map(rowToInvite);
|
|
479
|
+
}
|
|
@@ -24,21 +24,28 @@ const BACKFILL_CHECKPOINT_ID_KEY = 'memory:backfill:last_message_id';
|
|
|
24
24
|
const RELATION_BACKFILL_CHECKPOINT_KEY = 'memory:relation_backfill:last_created_at';
|
|
25
25
|
const RELATION_BACKFILL_CHECKPOINT_ID_KEY = 'memory:relation_backfill:last_message_id';
|
|
26
26
|
|
|
27
|
-
type
|
|
27
|
+
type ProvenanceTrustClass = 'guardian' | 'trusted_contact' | 'unknown';
|
|
28
28
|
|
|
29
|
-
function
|
|
29
|
+
function parseProvenanceTrustClass(rawMetadata: string | null): ProvenanceTrustClass | undefined {
|
|
30
30
|
if (!rawMetadata) return undefined;
|
|
31
31
|
try {
|
|
32
32
|
const parsedJson: unknown = JSON.parse(rawMetadata);
|
|
33
33
|
const parsed = messageMetadataSchema.safeParse(parsedJson);
|
|
34
|
-
|
|
34
|
+
if (!parsed.success) return undefined;
|
|
35
|
+
if (parsed.data.provenanceTrustClass) return parsed.data.provenanceTrustClass;
|
|
36
|
+
// Legacy fallback for rows written before provenanceTrustClass existed.
|
|
37
|
+
const legacyRole = (parsedJson as { provenanceActorRole?: unknown }).provenanceActorRole;
|
|
38
|
+
if (legacyRole === 'guardian') return 'guardian';
|
|
39
|
+
if (legacyRole === 'non-guardian') return 'trusted_contact';
|
|
40
|
+
if (legacyRole === 'unverified_channel') return 'unknown';
|
|
41
|
+
return undefined;
|
|
35
42
|
} catch {
|
|
36
43
|
return undefined;
|
|
37
44
|
}
|
|
38
45
|
}
|
|
39
46
|
|
|
40
|
-
function
|
|
41
|
-
return
|
|
47
|
+
function isTrustedTrustClass(trustClass: ProvenanceTrustClass | undefined): boolean {
|
|
48
|
+
return trustClass === 'guardian' || trustClass === undefined;
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
export function backfillJob(job: MemoryJob, config: AssistantConfig): void {
|
|
@@ -68,7 +75,7 @@ export function backfillJob(job: MemoryJob, config: AssistantConfig): void {
|
|
|
68
75
|
scopeId = getConversationMemoryScopeId(message.conversationId);
|
|
69
76
|
scopeCache.set(message.conversationId, scopeId);
|
|
70
77
|
}
|
|
71
|
-
const
|
|
78
|
+
const provenanceTrustClass = parseProvenanceTrustClass(message.metadata ?? null);
|
|
72
79
|
indexMessageNow({
|
|
73
80
|
messageId: message.id,
|
|
74
81
|
conversationId: message.conversationId,
|
|
@@ -76,7 +83,7 @@ export function backfillJob(job: MemoryJob, config: AssistantConfig): void {
|
|
|
76
83
|
content: message.content,
|
|
77
84
|
createdAt: message.createdAt,
|
|
78
85
|
scopeId,
|
|
79
|
-
|
|
86
|
+
provenanceTrustClass,
|
|
80
87
|
}, config.memory);
|
|
81
88
|
}
|
|
82
89
|
const lastMessage = batch[batch.length - 1];
|
|
@@ -139,8 +146,8 @@ export function backfillEntityRelationsJob(job: MemoryJob, config: AssistantConf
|
|
|
139
146
|
let queuedExtractEntityJobs = 0;
|
|
140
147
|
let skippedUntrusted = 0;
|
|
141
148
|
for (const message of batch) {
|
|
142
|
-
const
|
|
143
|
-
if (!
|
|
149
|
+
const provenanceTrustClass = parseProvenanceTrustClass(message.metadata ?? null);
|
|
150
|
+
if (!isTrustedTrustClass(provenanceTrustClass)) {
|
|
144
151
|
skippedUntrusted += 1;
|
|
145
152
|
continue;
|
|
146
153
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DrizzleDb } from '../db-connection.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Add voice invite columns to assistant_ingress_invites for guardian-initiated
|
|
5
|
+
* voice invite codes. All columns are nullable to keep existing invite rows
|
|
6
|
+
* compatible.
|
|
7
|
+
*
|
|
8
|
+
* - expected_external_user_id: E.164 phone number for identity binding
|
|
9
|
+
* - voice_code_hash: SHA-256 hash of the short numeric code
|
|
10
|
+
* - voice_code_digits: configurable digit count (nullable — NULL for non-voice invites)
|
|
11
|
+
*/
|
|
12
|
+
export function migrateVoiceInviteColumns(database: DrizzleDb): void {
|
|
13
|
+
try { database.run(/*sql*/ `ALTER TABLE assistant_ingress_invites ADD COLUMN expected_external_user_id TEXT`); } catch { /* already exists */ }
|
|
14
|
+
try { database.run(/*sql*/ `ALTER TABLE assistant_ingress_invites ADD COLUMN voice_code_hash TEXT`); } catch { /* already exists */ }
|
|
15
|
+
try { database.run(/*sql*/ `ALTER TABLE assistant_ingress_invites ADD COLUMN voice_code_digits INTEGER`); } catch { /* already exists */ }
|
|
16
|
+
}
|