@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
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -377,6 +377,25 @@ export async function runDaemon(): Promise<void> {
|
|
|
377
377
|
socketPath: getSocketPath(),
|
|
378
378
|
});
|
|
379
379
|
|
|
380
|
+
// Download embedding runtime in background (non-blocking).
|
|
381
|
+
// If download fails, local embeddings gracefully fall back to cloud backends.
|
|
382
|
+
void (async () => {
|
|
383
|
+
try {
|
|
384
|
+
const { EmbeddingRuntimeManager } = await import('../memory/embedding-runtime-manager.js');
|
|
385
|
+
const runtimeManager = new EmbeddingRuntimeManager();
|
|
386
|
+
if (!runtimeManager.isReady()) {
|
|
387
|
+
log.info('Downloading embedding runtime in background...');
|
|
388
|
+
await runtimeManager.ensureInstalled();
|
|
389
|
+
// Reset the localBackendBroken flag so auto mode retries local embeddings
|
|
390
|
+
const { clearEmbeddingBackendCache } = await import('../memory/embedding-backend.js');
|
|
391
|
+
clearEmbeddingBackendCache();
|
|
392
|
+
log.info('Embedding runtime download complete');
|
|
393
|
+
}
|
|
394
|
+
} catch (err) {
|
|
395
|
+
log.warn({ err }, 'Embedding runtime download failed — local embeddings will use cloud fallback');
|
|
396
|
+
}
|
|
397
|
+
})();
|
|
398
|
+
|
|
380
399
|
if (config.auditLog.retentionDays > 0) {
|
|
381
400
|
try {
|
|
382
401
|
rotateToolInvocations(config.auditLog.retentionDays);
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Pairing request store with TTL and disk persistence.
|
|
3
3
|
*
|
|
4
4
|
* Each pairing request lives for at most TTL_MS (5 minutes) before
|
|
5
5
|
* being swept as expired. Status transitions:
|
|
6
6
|
* registered → pending → approved | denied | expired
|
|
7
|
+
*
|
|
8
|
+
* Entries are persisted to ~/.vellum/protected/pairing-requests.json
|
|
9
|
+
* using the same atomic-write pattern as approved-devices-store.ts
|
|
10
|
+
* so that device bindings survive daemon restarts.
|
|
7
11
|
*/
|
|
8
12
|
|
|
9
13
|
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
14
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
10
16
|
|
|
11
17
|
import { getLogger } from '../util/logger.js';
|
|
18
|
+
import { getRootDir } from '../util/platform.js';
|
|
12
19
|
|
|
13
20
|
const log = getLogger('pairing-store');
|
|
14
21
|
|
|
@@ -40,11 +47,60 @@ function timingSafeCompare(a: string, b: string): boolean {
|
|
|
40
47
|
return timingSafeEqual(bufA, bufB);
|
|
41
48
|
}
|
|
42
49
|
|
|
50
|
+
interface PairingStoreFile {
|
|
51
|
+
version: 1;
|
|
52
|
+
requests: PairingRequest[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getStorePath(): string {
|
|
56
|
+
return join(getRootDir(), 'protected', 'pairing-requests.json');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function loadFromDisk(): Map<string, PairingRequest> {
|
|
60
|
+
const path = getStorePath();
|
|
61
|
+
if (!existsSync(path)) {
|
|
62
|
+
return new Map();
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const raw = readFileSync(path, 'utf-8');
|
|
66
|
+
const data = JSON.parse(raw) as PairingStoreFile;
|
|
67
|
+
if (data.version !== 1 || !Array.isArray(data.requests)) {
|
|
68
|
+
log.warn('Invalid pairing-requests.json format, starting fresh');
|
|
69
|
+
return new Map();
|
|
70
|
+
}
|
|
71
|
+
const map = new Map<string, PairingRequest>();
|
|
72
|
+
for (const entry of data.requests) {
|
|
73
|
+
map.set(entry.pairingRequestId, entry);
|
|
74
|
+
}
|
|
75
|
+
return map;
|
|
76
|
+
} catch (err) {
|
|
77
|
+
log.error({ err }, 'Failed to load pairing-requests.json');
|
|
78
|
+
return new Map();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function saveToDisk(requests: Map<string, PairingRequest>): void {
|
|
83
|
+
const path = getStorePath();
|
|
84
|
+
const dir = dirname(path);
|
|
85
|
+
if (!existsSync(dir)) {
|
|
86
|
+
mkdirSync(dir, { recursive: true });
|
|
87
|
+
}
|
|
88
|
+
const data: PairingStoreFile = {
|
|
89
|
+
version: 1,
|
|
90
|
+
requests: Array.from(requests.values()),
|
|
91
|
+
};
|
|
92
|
+
const tmpPath = path + '.tmp.' + process.pid;
|
|
93
|
+
writeFileSync(tmpPath, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
94
|
+
renameSync(tmpPath, path);
|
|
95
|
+
chmodSync(path, 0o600);
|
|
96
|
+
}
|
|
97
|
+
|
|
43
98
|
export class PairingStore {
|
|
44
99
|
private requests = new Map<string, PairingRequest>();
|
|
45
100
|
private sweepTimer: ReturnType<typeof setInterval> | null = null;
|
|
46
101
|
|
|
47
102
|
start(): void {
|
|
103
|
+
this.requests = loadFromDisk();
|
|
48
104
|
this.sweepTimer = setInterval(() => this.sweep(), SWEEP_INTERVAL_MS);
|
|
49
105
|
}
|
|
50
106
|
|
|
@@ -84,6 +140,7 @@ export class PairingStore {
|
|
|
84
140
|
localLanUrl: params.localLanUrl ?? null,
|
|
85
141
|
createdAt: Date.now(),
|
|
86
142
|
});
|
|
143
|
+
this.persist();
|
|
87
144
|
|
|
88
145
|
log.info({ pairingRequestId: params.pairingRequestId }, 'Pairing request registered');
|
|
89
146
|
return { ok: true };
|
|
@@ -98,7 +155,7 @@ export class PairingStore {
|
|
|
98
155
|
pairingSecret: string;
|
|
99
156
|
deviceId: string;
|
|
100
157
|
deviceName: string;
|
|
101
|
-
}): { ok: true; entry: PairingRequest } | { ok: false; reason: 'not_found' | 'invalid_secret' | 'expired' } {
|
|
158
|
+
}): { ok: true; entry: PairingRequest } | { ok: false; reason: 'not_found' | 'invalid_secret' | 'expired' | 'already_paired' } {
|
|
102
159
|
const entry = this.requests.get(params.pairingRequestId);
|
|
103
160
|
if (!entry) {
|
|
104
161
|
return { ok: false, reason: 'not_found' };
|
|
@@ -113,11 +170,21 @@ export class PairingStore {
|
|
|
113
170
|
return { ok: false, reason: 'invalid_secret' };
|
|
114
171
|
}
|
|
115
172
|
|
|
116
|
-
|
|
173
|
+
const hashedDeviceId = hashValue(params.deviceId);
|
|
174
|
+
|
|
175
|
+
// If a device has already been bound to this pairing request, reject
|
|
176
|
+
// attempts from a different device to prevent hijacking.
|
|
177
|
+
if (entry.hashedDeviceId && !timingSafeCompare(entry.hashedDeviceId, hashedDeviceId)) {
|
|
178
|
+
log.warn({ pairingRequestId: params.pairingRequestId }, 'Pairing request already bound to a different device');
|
|
179
|
+
return { ok: false, reason: 'already_paired' };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
entry.hashedDeviceId = hashedDeviceId;
|
|
117
183
|
entry.deviceName = params.deviceName;
|
|
118
184
|
if (entry.status === 'registered') {
|
|
119
185
|
entry.status = 'pending';
|
|
120
186
|
}
|
|
187
|
+
this.persist();
|
|
121
188
|
|
|
122
189
|
return { ok: true, entry };
|
|
123
190
|
}
|
|
@@ -130,6 +197,7 @@ export class PairingStore {
|
|
|
130
197
|
if (!entry) return null;
|
|
131
198
|
entry.status = 'approved';
|
|
132
199
|
entry.bearerToken = bearerToken;
|
|
200
|
+
this.persist();
|
|
133
201
|
return entry;
|
|
134
202
|
}
|
|
135
203
|
|
|
@@ -140,6 +208,7 @@ export class PairingStore {
|
|
|
140
208
|
const entry = this.requests.get(pairingRequestId);
|
|
141
209
|
if (!entry) return null;
|
|
142
210
|
entry.status = 'denied';
|
|
211
|
+
this.persist();
|
|
143
212
|
return entry;
|
|
144
213
|
}
|
|
145
214
|
|
|
@@ -160,19 +229,33 @@ export class PairingStore {
|
|
|
160
229
|
return timingSafeCompare(entry.hashedPairingSecret, hashedSecret);
|
|
161
230
|
}
|
|
162
231
|
|
|
232
|
+
private persist(): void {
|
|
233
|
+
try {
|
|
234
|
+
saveToDisk(this.requests);
|
|
235
|
+
} catch (err) {
|
|
236
|
+
log.error({ err }, 'Failed to persist pairing requests to disk');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
163
240
|
private sweep(): void {
|
|
164
241
|
const now = Date.now();
|
|
242
|
+
let changed = false;
|
|
165
243
|
for (const [id, entry] of this.requests) {
|
|
166
244
|
if (now - entry.createdAt > TTL_MS) {
|
|
167
245
|
if (entry.status !== 'approved') {
|
|
168
246
|
entry.status = 'expired';
|
|
247
|
+
changed = true;
|
|
169
248
|
}
|
|
170
249
|
// Remove entries older than 2x TTL regardless of status
|
|
171
250
|
if (now - entry.createdAt > TTL_MS * 2) {
|
|
172
251
|
this.requests.delete(id);
|
|
252
|
+
changed = true;
|
|
173
253
|
log.debug({ pairingRequestId: id }, 'Pairing request swept');
|
|
174
254
|
}
|
|
175
255
|
}
|
|
176
256
|
}
|
|
257
|
+
if (changed) {
|
|
258
|
+
this.persist();
|
|
259
|
+
}
|
|
177
260
|
}
|
|
178
261
|
}
|
|
@@ -145,15 +145,16 @@ const TIER_SYSTEM_PROMPT =
|
|
|
145
145
|
|
|
146
146
|
/**
|
|
147
147
|
* Fire-and-forget Haiku call to classify the conversation trajectory.
|
|
148
|
-
* Returns the classified tier or
|
|
148
|
+
* Returns the classified tier, or undefined when no provider is configured
|
|
149
|
+
* or on any failure.
|
|
149
150
|
*/
|
|
150
151
|
export async function classifyResponseTierAsync(
|
|
151
152
|
recentUserTexts: string[],
|
|
152
|
-
): Promise<ResponseTier |
|
|
153
|
+
): Promise<ResponseTier | undefined> {
|
|
153
154
|
const provider = getConfiguredProvider();
|
|
154
155
|
if (!provider) {
|
|
155
156
|
log.debug('No provider available for async tier classification');
|
|
156
|
-
return
|
|
157
|
+
return undefined;
|
|
157
158
|
}
|
|
158
159
|
|
|
159
160
|
const combined = recentUserTexts
|
|
@@ -186,14 +187,14 @@ export async function classifyResponseTierAsync(
|
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
log.debug({ raw }, 'Async tier classification returned unexpected value');
|
|
189
|
-
return
|
|
190
|
+
return undefined;
|
|
190
191
|
} finally {
|
|
191
192
|
cleanup();
|
|
192
193
|
}
|
|
193
194
|
} catch (err) {
|
|
194
195
|
const message = err instanceof Error ? err.message : String(err);
|
|
195
196
|
log.debug({ err: message }, 'Async tier classification failed');
|
|
196
|
-
return
|
|
197
|
+
return undefined;
|
|
197
198
|
}
|
|
198
199
|
}
|
|
199
200
|
|
|
@@ -258,7 +258,7 @@ export async function runAgentLoopImpl(
|
|
|
258
258
|
conflictGate: ctx.conflictGate,
|
|
259
259
|
scopeId: ctx.memoryPolicy.scopeId,
|
|
260
260
|
includeDefaultFallback: ctx.memoryPolicy.includeDefaultFallback,
|
|
261
|
-
|
|
261
|
+
guardianTrustClass: ctx.guardianContext?.trustClass,
|
|
262
262
|
isInteractive: options?.isInteractive ?? (!ctx.hasNoClient && !ctx.headlessLock),
|
|
263
263
|
},
|
|
264
264
|
content,
|
|
@@ -355,10 +355,9 @@ export async function runAgentLoopImpl(
|
|
|
355
355
|
|
|
356
356
|
// Resolve the inbound actor context for the model's <inbound_actor_context>
|
|
357
357
|
// block. When the session carries enough identity info, use the unified
|
|
358
|
-
// actor trust resolver so
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
// the model context block uses the trust-resolved output.
|
|
358
|
+
// actor trust resolver so member status/policy and guardian binding details
|
|
359
|
+
// are fresh for this turn. The session runtime context remains the source
|
|
360
|
+
// for policy gating; this block is model-facing grounding metadata.
|
|
362
361
|
let resolvedInboundActorContext: InboundActorContext | null = null;
|
|
363
362
|
if (ctx.guardianContext) {
|
|
364
363
|
const gc = ctx.guardianContext;
|
|
@@ -368,6 +367,7 @@ export async function runAgentLoopImpl(
|
|
|
368
367
|
sourceChannel: gc.sourceChannel,
|
|
369
368
|
externalChatId: gc.requesterChatId,
|
|
370
369
|
senderExternalUserId: gc.requesterExternalUserId,
|
|
370
|
+
senderDisplayName: gc.requesterSenderDisplayName,
|
|
371
371
|
});
|
|
372
372
|
resolvedInboundActorContext = inboundActorContextFromTrust(actorTrust);
|
|
373
373
|
} else {
|
|
@@ -24,30 +24,38 @@ import { resetSkillToolProjection } from './session-skill-tools.js';
|
|
|
24
24
|
|
|
25
25
|
const log = getLogger('session-lifecycle');
|
|
26
26
|
|
|
27
|
-
type
|
|
27
|
+
type GuardianTrustClass = GuardianRuntimeContext['trustClass'];
|
|
28
28
|
|
|
29
|
-
function
|
|
29
|
+
function parseProvenanceTrustClass(metadata: string | null): GuardianTrustClass | undefined {
|
|
30
30
|
if (!metadata) return undefined;
|
|
31
31
|
try {
|
|
32
|
-
const parsed = JSON.parse(metadata) as {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
const parsed = JSON.parse(metadata) as {
|
|
33
|
+
provenanceTrustClass?: unknown;
|
|
34
|
+
provenanceActorRole?: unknown;
|
|
35
|
+
};
|
|
36
|
+
const trustClass = parsed?.provenanceTrustClass;
|
|
37
|
+
if (trustClass === 'guardian' || trustClass === 'trusted_contact' || trustClass === 'unknown') {
|
|
38
|
+
return trustClass;
|
|
36
39
|
}
|
|
40
|
+
// Legacy fallback for rows persisted before provenanceTrustClass existed.
|
|
41
|
+
const legacyRole = parsed?.provenanceActorRole;
|
|
42
|
+
if (legacyRole === 'guardian') return 'guardian';
|
|
43
|
+
if (legacyRole === 'non-guardian') return 'trusted_contact';
|
|
44
|
+
if (legacyRole === 'unverified_channel') return 'unknown';
|
|
37
45
|
} catch {
|
|
38
46
|
// Ignore malformed metadata and treat as unknown provenance.
|
|
39
47
|
}
|
|
40
48
|
return undefined;
|
|
41
49
|
}
|
|
42
50
|
|
|
43
|
-
function
|
|
44
|
-
return
|
|
51
|
+
function isUntrustedTrustClass(trustClass: GuardianTrustClass | undefined): boolean {
|
|
52
|
+
return trustClass === 'trusted_contact' || trustClass === 'unknown';
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
function filterMessagesForUntrustedActor(messages: conversationStore.MessageRow[]): conversationStore.MessageRow[] {
|
|
48
56
|
return messages.filter((m) => {
|
|
49
|
-
const
|
|
50
|
-
return
|
|
57
|
+
const provenanceTrustClass = parseProvenanceTrustClass(m.metadata);
|
|
58
|
+
return provenanceTrustClass === 'trusted_contact' || provenanceTrustClass === 'unknown';
|
|
51
59
|
});
|
|
52
60
|
}
|
|
53
61
|
|
|
@@ -59,8 +67,8 @@ export interface LoadFromDbContext {
|
|
|
59
67
|
usageStats: UsageStats;
|
|
60
68
|
contextCompactedMessageCount: number;
|
|
61
69
|
contextCompactedAt: number | null;
|
|
62
|
-
guardianContext?: {
|
|
63
|
-
|
|
70
|
+
guardianContext?: { trustClass: GuardianTrustClass };
|
|
71
|
+
loadedHistoryTrustClass?: GuardianTrustClass;
|
|
64
72
|
}
|
|
65
73
|
|
|
66
74
|
export interface AbortContext {
|
|
@@ -89,17 +97,17 @@ export interface DisposeContext extends AbortContext {
|
|
|
89
97
|
// ── loadFromDb ───────────────────────────────────────────────────────
|
|
90
98
|
|
|
91
99
|
export async function loadFromDb(ctx: LoadFromDbContext): Promise<void> {
|
|
92
|
-
const
|
|
100
|
+
const trustClass = ctx.guardianContext?.trustClass;
|
|
93
101
|
const allDbMessages = conversationStore.getMessages(ctx.conversationId);
|
|
94
|
-
const dbMessages =
|
|
102
|
+
const dbMessages = isUntrustedTrustClass(trustClass)
|
|
95
103
|
? filterMessagesForUntrustedActor(allDbMessages)
|
|
96
104
|
: allDbMessages;
|
|
97
105
|
|
|
98
106
|
const conv = conversationStore.getConversation(ctx.conversationId);
|
|
99
|
-
const contextSummary = !
|
|
107
|
+
const contextSummary = !isUntrustedTrustClass(trustClass)
|
|
100
108
|
? conv?.contextSummary?.trim() || null
|
|
101
109
|
: null;
|
|
102
|
-
if (
|
|
110
|
+
if (isUntrustedTrustClass(trustClass)) {
|
|
103
111
|
// Compacted summaries may include trusted/guardian-only details, so we
|
|
104
112
|
// disable summary-based context for untrusted actor views.
|
|
105
113
|
ctx.contextCompactedMessageCount = 0;
|
|
@@ -145,7 +153,7 @@ export async function loadFromDb(ctx: LoadFromDbContext): Promise<void> {
|
|
|
145
153
|
};
|
|
146
154
|
}
|
|
147
155
|
|
|
148
|
-
ctx.
|
|
156
|
+
ctx.loadedHistoryTrustClass = trustClass;
|
|
149
157
|
|
|
150
158
|
log.info({ conversationId: ctx.conversationId, count: ctx.messages.length }, 'Loaded messages from DB');
|
|
151
159
|
}
|
|
@@ -34,7 +34,7 @@ export interface MemoryPrepareContext {
|
|
|
34
34
|
conflictGate: ConflictGate;
|
|
35
35
|
scopeId: string;
|
|
36
36
|
includeDefaultFallback: boolean;
|
|
37
|
-
|
|
37
|
+
guardianTrustClass?: 'guardian' | 'trusted_contact' | 'unknown';
|
|
38
38
|
/** When false (e.g. scheduled tasks), skip conflict clarification prompts. */
|
|
39
39
|
isInteractive?: boolean;
|
|
40
40
|
}
|
|
@@ -64,7 +64,7 @@ export async function prepareMemoryContext(
|
|
|
64
64
|
// Provenance-based trust gating: untrusted actors skip all memory operations
|
|
65
65
|
// (recall, dynamic profile, conflict gate) to prevent untrusted content from
|
|
66
66
|
// influencing memory-augmented responses.
|
|
67
|
-
const isTrustedActor = ctx.
|
|
67
|
+
const isTrustedActor = ctx.guardianTrustClass === 'guardian' || ctx.guardianTrustClass === undefined;
|
|
68
68
|
|
|
69
69
|
if (!isTrustedActor) {
|
|
70
70
|
return {
|
|
@@ -21,7 +21,6 @@ import { createPreference } from '../notifications/preferences-store.js';
|
|
|
21
21
|
import type { Message } from '../providers/types.js';
|
|
22
22
|
import { routeGuardianReply } from '../runtime/guardian-reply-router.js';
|
|
23
23
|
import { getLogger } from '../util/logger.js';
|
|
24
|
-
import { resolveGuardianInviteIntent } from './guardian-invite-intent.js';
|
|
25
24
|
import { resolveGuardianVerificationIntent } from './guardian-verification-intent.js';
|
|
26
25
|
import type { UsageStats } from './ipc-contract.js';
|
|
27
26
|
import type { ServerMessage, UserMessageAttachment } from './ipc-protocol.js';
|
|
@@ -266,15 +265,6 @@ export async function drainQueue(session: ProcessSessionContext, reason: QueueDr
|
|
|
266
265
|
log.info({ conversationId: session.conversationId, channelHint: guardianIntent.channelHint }, 'Guardian verification intent intercepted in queue — forcing skill flow');
|
|
267
266
|
agentLoopContent = guardianIntent.rewrittenContent;
|
|
268
267
|
session.preactivatedSkillIds = ['guardian-verify-setup'];
|
|
269
|
-
} else {
|
|
270
|
-
// Guardian invite intent interception — force invite management
|
|
271
|
-
// requests into the trusted-contacts skill flow.
|
|
272
|
-
const inviteIntent = resolveGuardianInviteIntent(resolvedContent);
|
|
273
|
-
if (inviteIntent.kind === 'invite_management') {
|
|
274
|
-
log.info({ conversationId: session.conversationId, action: inviteIntent.action }, 'Guardian invite intent intercepted in queue — forcing skill flow');
|
|
275
|
-
agentLoopContent = inviteIntent.rewrittenContent;
|
|
276
|
-
session.preactivatedSkillIds = ['trusted-contacts'];
|
|
277
|
-
}
|
|
278
268
|
}
|
|
279
269
|
}
|
|
280
270
|
|
|
@@ -402,7 +392,7 @@ export async function processMessage(
|
|
|
402
392
|
assistantMessageChannel: 'vellum' as const,
|
|
403
393
|
userMessageInterface: guardianIfCtx?.userMessageInterface ?? 'vellum',
|
|
404
394
|
assistantMessageInterface: guardianIfCtx?.assistantMessageInterface ?? 'vellum',
|
|
405
|
-
|
|
395
|
+
provenanceTrustClass: 'guardian' as const,
|
|
406
396
|
};
|
|
407
397
|
|
|
408
398
|
const userMsg = createUserMessage(content, attachments);
|
|
@@ -520,15 +510,6 @@ export async function processMessage(
|
|
|
520
510
|
log.info({ conversationId: session.conversationId, channelHint: guardianIntent.channelHint }, 'Guardian verification intent intercepted — forcing skill flow');
|
|
521
511
|
agentLoopContent = guardianIntent.rewrittenContent;
|
|
522
512
|
session.preactivatedSkillIds = ['guardian-verify-setup'];
|
|
523
|
-
} else {
|
|
524
|
-
// Guardian invite intent interception — force invite management
|
|
525
|
-
// requests into the trusted-contacts skill flow.
|
|
526
|
-
const inviteIntent = resolveGuardianInviteIntent(resolvedContent);
|
|
527
|
-
if (inviteIntent.kind === 'invite_management') {
|
|
528
|
-
log.info({ conversationId: session.conversationId, action: inviteIntent.action }, 'Guardian invite intent intercepted — forcing skill flow');
|
|
529
|
-
agentLoopContent = inviteIntent.rewrittenContent;
|
|
530
|
-
session.preactivatedSkillIds = ['trusted-contacts'];
|
|
531
|
-
}
|
|
532
513
|
}
|
|
533
514
|
}
|
|
534
515
|
|
|
@@ -35,10 +35,13 @@ export interface ChannelCapabilities {
|
|
|
35
35
|
/** Guardian identity/trust context for external chat channels. */
|
|
36
36
|
export interface GuardianRuntimeContext {
|
|
37
37
|
sourceChannel: ChannelId;
|
|
38
|
-
|
|
38
|
+
trustClass: 'guardian' | 'trusted_contact' | 'unknown';
|
|
39
39
|
guardianChatId?: string;
|
|
40
40
|
guardianExternalUserId?: string;
|
|
41
41
|
requesterIdentifier?: string;
|
|
42
|
+
requesterDisplayName?: string;
|
|
43
|
+
requesterSenderDisplayName?: string;
|
|
44
|
+
requesterMemberDisplayName?: string;
|
|
42
45
|
requesterExternalUserId?: string;
|
|
43
46
|
requesterChatId?: string;
|
|
44
47
|
denialReason?: 'no_binding' | 'no_identity';
|
|
@@ -58,6 +61,12 @@ export interface InboundActorContext {
|
|
|
58
61
|
canonicalActorIdentity: string | null;
|
|
59
62
|
/** Human-readable actor identifier (e.g. @username or phone). */
|
|
60
63
|
actorIdentifier?: string;
|
|
64
|
+
/** Human-readable actor display name (e.g. "Jeff"). */
|
|
65
|
+
actorDisplayName?: string;
|
|
66
|
+
/** Raw sender display name as provided by the channel transport. */
|
|
67
|
+
actorSenderDisplayName?: string;
|
|
68
|
+
/** Guardian-managed member display name from ingress membership. */
|
|
69
|
+
actorMemberDisplayName?: string;
|
|
61
70
|
/** Trust classification: guardian, trusted_contact, or unknown. */
|
|
62
71
|
trustClass: 'guardian' | 'trusted_contact' | 'unknown';
|
|
63
72
|
/** Guardian identity for this (assistant, channel) binding. */
|
|
@@ -73,33 +82,17 @@ export interface InboundActorContext {
|
|
|
73
82
|
/**
|
|
74
83
|
* Construct an InboundActorContext from a legacy GuardianRuntimeContext.
|
|
75
84
|
*
|
|
76
|
-
* Maps the
|
|
77
|
-
* - guardian -> guardian
|
|
78
|
-
* - non-guardian -> unknown (the legacy context carries no membership
|
|
79
|
-
* evidence, so we cannot distinguish known members from arbitrary
|
|
80
|
-
* non-guardian senders; default to unknown for safety)
|
|
81
|
-
* - unverified_channel -> unknown
|
|
82
|
-
*
|
|
83
|
-
* The new ActorTrustContext path (via `inboundActorContextFromTrust`)
|
|
84
|
-
* resolves `trusted_contact` correctly using ingress member records.
|
|
85
|
+
* Maps the runtime trust class into the model-facing inbound actor context.
|
|
85
86
|
*/
|
|
86
87
|
export function inboundActorContextFromGuardian(ctx: GuardianRuntimeContext): InboundActorContext {
|
|
87
|
-
let trustClass: InboundActorContext['trustClass'];
|
|
88
|
-
switch (ctx.actorRole) {
|
|
89
|
-
case 'guardian':
|
|
90
|
-
trustClass = 'guardian';
|
|
91
|
-
break;
|
|
92
|
-
case 'non-guardian':
|
|
93
|
-
case 'unverified_channel':
|
|
94
|
-
trustClass = 'unknown';
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
88
|
return {
|
|
99
89
|
sourceChannel: ctx.sourceChannel,
|
|
100
90
|
canonicalActorIdentity: ctx.requesterExternalUserId ?? null,
|
|
101
91
|
actorIdentifier: ctx.requesterIdentifier,
|
|
102
|
-
|
|
92
|
+
actorDisplayName: ctx.requesterDisplayName,
|
|
93
|
+
actorSenderDisplayName: ctx.requesterSenderDisplayName,
|
|
94
|
+
actorMemberDisplayName: ctx.requesterMemberDisplayName,
|
|
95
|
+
trustClass: ctx.trustClass,
|
|
103
96
|
guardianIdentity: ctx.guardianExternalUserId,
|
|
104
97
|
denialReason: ctx.denialReason,
|
|
105
98
|
};
|
|
@@ -114,6 +107,9 @@ export function inboundActorContextFromTrust(ctx: ActorTrustContext): InboundAct
|
|
|
114
107
|
sourceChannel: ctx.actorMetadata.channel,
|
|
115
108
|
canonicalActorIdentity: ctx.canonicalSenderId,
|
|
116
109
|
actorIdentifier: ctx.actorMetadata.identifier,
|
|
110
|
+
actorDisplayName: ctx.actorMetadata.displayName,
|
|
111
|
+
actorSenderDisplayName: ctx.actorMetadata.senderDisplayName,
|
|
112
|
+
actorMemberDisplayName: ctx.actorMetadata.memberDisplayName,
|
|
117
113
|
trustClass: ctx.trustClass,
|
|
118
114
|
guardianIdentity: ctx.guardianBindingMatch?.guardianExternalUserId,
|
|
119
115
|
memberStatus: ctx.memberRecord?.status ?? undefined,
|
|
@@ -552,6 +548,9 @@ export function buildInboundActorContextBlock(ctx: InboundActorContext): string
|
|
|
552
548
|
lines.push(`source_channel: ${ctx.sourceChannel}`);
|
|
553
549
|
lines.push(`canonical_actor_identity: ${ctx.canonicalActorIdentity ?? 'unknown'}`);
|
|
554
550
|
lines.push(`actor_identifier: ${ctx.actorIdentifier ?? 'unknown'}`);
|
|
551
|
+
lines.push(`actor_display_name: ${ctx.actorDisplayName ?? 'unknown'}`);
|
|
552
|
+
lines.push(`actor_sender_display_name: ${ctx.actorSenderDisplayName ?? 'unknown'}`);
|
|
553
|
+
lines.push(`actor_member_display_name: ${ctx.actorMemberDisplayName ?? 'unknown'}`);
|
|
555
554
|
lines.push(`trust_class: ${ctx.trustClass}`);
|
|
556
555
|
lines.push(`guardian_identity: ${ctx.guardianIdentity ?? 'unknown'}`);
|
|
557
556
|
if (ctx.memberStatus) {
|
|
@@ -561,6 +560,13 @@ export function buildInboundActorContextBlock(ctx: InboundActorContext): string
|
|
|
561
560
|
lines.push(`member_policy: ${ctx.memberPolicy}`);
|
|
562
561
|
}
|
|
563
562
|
lines.push(`denial_reason: ${ctx.denialReason ?? 'none'}`);
|
|
563
|
+
if (
|
|
564
|
+
ctx.actorMemberDisplayName
|
|
565
|
+
&& ctx.actorSenderDisplayName
|
|
566
|
+
&& ctx.actorMemberDisplayName !== ctx.actorSenderDisplayName
|
|
567
|
+
) {
|
|
568
|
+
lines.push('name_preference_note: actor_member_display_name is the guardian-preferred nickname for this person; actor_sender_display_name is the channel-provided display name.');
|
|
569
|
+
}
|
|
564
570
|
|
|
565
571
|
// Behavioral guidance — injected per-turn so it only appears when relevant.
|
|
566
572
|
lines.push('');
|
|
@@ -56,7 +56,7 @@ export interface ToolSetupContext extends SurfaceSessionContext {
|
|
|
56
56
|
headlessLock?: boolean;
|
|
57
57
|
/** When set, this session is executing a task run. Used to retrieve ephemeral permission rules. */
|
|
58
58
|
taskRunId?: string;
|
|
59
|
-
/** Guardian runtime context for the session —
|
|
59
|
+
/** Guardian runtime context for the session — trustClass is propagated into ToolContext for control-plane policy enforcement. */
|
|
60
60
|
guardianContext?: GuardianRuntimeContext;
|
|
61
61
|
/** Voice/call session ID, if the session originates from a call. Propagated into ToolContext for scoped grant consumption. */
|
|
62
62
|
callSessionId?: string;
|
|
@@ -110,7 +110,7 @@ export function createToolExecutor(
|
|
|
110
110
|
assistantId: ctx.assistantId,
|
|
111
111
|
requestId: ctx.currentRequestId,
|
|
112
112
|
taskRunId: ctx.taskRunId,
|
|
113
|
-
|
|
113
|
+
guardianTrustClass: ctx.guardianContext?.trustClass,
|
|
114
114
|
executionChannel: ctx.guardianContext?.sourceChannel,
|
|
115
115
|
callSessionId: ctx.callSessionId,
|
|
116
116
|
requesterExternalUserId: ctx.guardianContext?.requesterExternalUserId,
|
package/src/daemon/session.ts
CHANGED
|
@@ -140,7 +140,7 @@ export class Session {
|
|
|
140
140
|
/** @internal */ currentPage?: string;
|
|
141
141
|
/** @internal */ channelCapabilities?: ChannelCapabilities;
|
|
142
142
|
/** @internal */ guardianContext?: GuardianRuntimeContext;
|
|
143
|
-
/** @internal */
|
|
143
|
+
/** @internal */ loadedHistoryTrustClass?: GuardianRuntimeContext['trustClass'];
|
|
144
144
|
/** @internal */ voiceCallControlPrompt?: string;
|
|
145
145
|
/** @internal */ assistantId?: string;
|
|
146
146
|
/** @internal */ commandIntent?: { type: string; payload?: string; languageCode?: string };
|
|
@@ -338,8 +338,8 @@ export class Session {
|
|
|
338
338
|
}
|
|
339
339
|
|
|
340
340
|
async ensureActorScopedHistory(): Promise<void> {
|
|
341
|
-
const
|
|
342
|
-
if (this.
|
|
341
|
+
const currentTrustClass = this.guardianContext?.trustClass;
|
|
342
|
+
if (this.loadedHistoryTrustClass === currentTrustClass) return;
|
|
343
343
|
await this.loadFromDb();
|
|
344
344
|
}
|
|
345
345
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* request from the expected status wins.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { and, eq } from 'drizzle-orm';
|
|
10
|
+
import { and, desc, eq } from 'drizzle-orm';
|
|
11
11
|
import { v4 as uuid } from 'uuid';
|
|
12
12
|
|
|
13
13
|
import { getDb, rawChanges } from './db.js';
|
|
@@ -498,6 +498,68 @@ export interface UpdateCanonicalGuardianDeliveryParams {
|
|
|
498
498
|
destinationMessageId?: string;
|
|
499
499
|
}
|
|
500
500
|
|
|
501
|
+
// ---------------------------------------------------------------------------
|
|
502
|
+
// Call-controller convenience functions
|
|
503
|
+
// ---------------------------------------------------------------------------
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Find the most recent pending canonical guardian request for a given call session.
|
|
507
|
+
* Used by the call-controller's consultation timeout handler.
|
|
508
|
+
*/
|
|
509
|
+
export function getPendingCanonicalRequestByCallSessionId(callSessionId: string): CanonicalGuardianRequest | null {
|
|
510
|
+
const db = getDb();
|
|
511
|
+
const row = db
|
|
512
|
+
.select()
|
|
513
|
+
.from(canonicalGuardianRequests)
|
|
514
|
+
.where(
|
|
515
|
+
and(
|
|
516
|
+
eq(canonicalGuardianRequests.callSessionId, callSessionId),
|
|
517
|
+
eq(canonicalGuardianRequests.status, 'pending'),
|
|
518
|
+
),
|
|
519
|
+
)
|
|
520
|
+
.orderBy(desc(canonicalGuardianRequests.createdAt))
|
|
521
|
+
.get();
|
|
522
|
+
return row ? rowToRequest(row) : null;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Find a canonical guardian request by its linked pending question ID.
|
|
527
|
+
* Used after async dispatch completes to locate the newly created request.
|
|
528
|
+
*/
|
|
529
|
+
export function getCanonicalRequestByPendingQuestionId(questionId: string): CanonicalGuardianRequest | null {
|
|
530
|
+
const db = getDb();
|
|
531
|
+
const row = db
|
|
532
|
+
.select()
|
|
533
|
+
.from(canonicalGuardianRequests)
|
|
534
|
+
.where(eq(canonicalGuardianRequests.pendingQuestionId, questionId))
|
|
535
|
+
.get();
|
|
536
|
+
return row ? rowToRequest(row) : null;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Expire a canonical guardian request and all its deliveries.
|
|
541
|
+
* Atomically transitions the request from 'pending' to 'expired'.
|
|
542
|
+
*/
|
|
543
|
+
export function expireCanonicalGuardianRequest(id: string): void {
|
|
544
|
+
const db = getDb();
|
|
545
|
+
const now = new Date().toISOString();
|
|
546
|
+
|
|
547
|
+
db.update(canonicalGuardianRequests)
|
|
548
|
+
.set({ status: 'expired', updatedAt: now })
|
|
549
|
+
.where(
|
|
550
|
+
and(
|
|
551
|
+
eq(canonicalGuardianRequests.id, id),
|
|
552
|
+
eq(canonicalGuardianRequests.status, 'pending'),
|
|
553
|
+
),
|
|
554
|
+
)
|
|
555
|
+
.run();
|
|
556
|
+
|
|
557
|
+
db.update(canonicalGuardianDeliveries)
|
|
558
|
+
.set({ status: 'expired', updatedAt: now })
|
|
559
|
+
.where(eq(canonicalGuardianDeliveries.requestId, id))
|
|
560
|
+
.run();
|
|
561
|
+
}
|
|
562
|
+
|
|
501
563
|
export function updateCanonicalGuardianDelivery(
|
|
502
564
|
id: string,
|
|
503
565
|
updates: UpdateCanonicalGuardianDeliveryParams,
|