@vellumai/assistant 0.10.3-dev.202606282033.2a8a8de → 0.10.3-dev.202606282129.2e2ccad

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.10.3-dev.202606282033.2a8a8de",
3
+ "version": "0.10.3-dev.202606282129.2e2ccad",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -0,0 +1,141 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { elevatePointerConversationToGuardian } from "../daemon/pointer-conversation-trust.js";
4
+ import {
5
+ INTERNAL_GUARDIAN_TRUST_CONTEXT,
6
+ type TrustContext,
7
+ } from "../daemon/trust-context.js";
8
+ import { resolveCapabilities } from "../runtime/capabilities.js";
9
+
10
+ /**
11
+ * Fake conversation that mirrors `Conversation.loadFromDb`'s trust gate: a
12
+ * memory-capable (guardian) trust class sees the full guardian history; any
13
+ * other class sees an empty (filtered) history. `ensureActorScopedHistory`
14
+ * re-applies that gate against the current trust context, exactly like the real
15
+ * reload path.
16
+ */
17
+ class FakeConversation {
18
+ trustContext: TrustContext | undefined;
19
+ visibleHistory: string[] = [];
20
+ setTrustContextCalls: Array<TrustContext | null> = [];
21
+ ensureCalls = 0;
22
+ private readonly guardianHistory: string[];
23
+ private readonly processing: boolean;
24
+
25
+ constructor(opts: {
26
+ trustContext?: TrustContext;
27
+ guardianHistory?: string[];
28
+ processing?: boolean;
29
+ }) {
30
+ this.trustContext = opts.trustContext;
31
+ this.guardianHistory = opts.guardianHistory ?? ["m1", "m2", "m3"];
32
+ this.processing = opts.processing ?? false;
33
+ this.applyHistoryForTrust();
34
+ }
35
+
36
+ private applyHistoryForTrust(): void {
37
+ this.visibleHistory = resolveCapabilities(this.trustContext?.trustClass)
38
+ .canAccessMemory
39
+ ? [...this.guardianHistory]
40
+ : [];
41
+ }
42
+
43
+ isProcessing(): boolean {
44
+ return this.processing;
45
+ }
46
+
47
+ setTrustContext(ctx: TrustContext | null): void {
48
+ this.setTrustContextCalls.push(ctx);
49
+ this.trustContext = ctx ?? undefined;
50
+ }
51
+
52
+ async ensureActorScopedHistory(): Promise<void> {
53
+ this.ensureCalls++;
54
+ this.applyHistoryForTrust();
55
+ }
56
+ }
57
+
58
+ const CONTACT_CONTEXT: TrustContext = {
59
+ sourceChannel: "vellum",
60
+ trustClass: "unverified_contact",
61
+ };
62
+
63
+ const GUARDIAN_CONTEXT: TrustContext = {
64
+ sourceChannel: "vellum",
65
+ trustClass: "guardian",
66
+ };
67
+
68
+ describe("elevatePointerConversationToGuardian", () => {
69
+ test("elevates a cold trust-less conversation and rehydrates guardian history", async () => {
70
+ const conv = new FakeConversation({ trustContext: undefined });
71
+ // A cold (trust-less) load filters guardian history to empty.
72
+ expect(conv.visibleHistory).toEqual([]);
73
+
74
+ const restore = await elevatePointerConversationToGuardian(conv);
75
+
76
+ expect(conv.trustContext).toBe(INTERNAL_GUARDIAN_TRUST_CONTEXT);
77
+ expect(conv.ensureCalls).toBe(1);
78
+ // History is rehydrated rather than shipped empty.
79
+ expect(conv.visibleHistory).toEqual(["m1", "m2", "m3"]);
80
+
81
+ restore();
82
+ expect(conv.trustContext).toBeUndefined();
83
+ });
84
+
85
+ test("restores the prior non-memory trust context after the turn", async () => {
86
+ const conv = new FakeConversation({ trustContext: CONTACT_CONTEXT });
87
+ expect(conv.visibleHistory).toEqual([]);
88
+
89
+ const restore = await elevatePointerConversationToGuardian(conv);
90
+ expect(conv.trustContext).toBe(INTERNAL_GUARDIAN_TRUST_CONTEXT);
91
+ expect(conv.visibleHistory).toEqual(["m1", "m2", "m3"]);
92
+
93
+ restore();
94
+ expect(conv.trustContext).toBe(CONTACT_CONTEXT);
95
+ });
96
+
97
+ test("no-ops for a conversation that already has memory access", async () => {
98
+ const conv = new FakeConversation({ trustContext: GUARDIAN_CONTEXT });
99
+ expect(conv.visibleHistory).toEqual(["m1", "m2", "m3"]);
100
+
101
+ const restore = await elevatePointerConversationToGuardian(conv);
102
+
103
+ // No elevation, no redundant reload.
104
+ expect(conv.setTrustContextCalls).toEqual([]);
105
+ expect(conv.ensureCalls).toBe(0);
106
+ expect(conv.trustContext).toBe(GUARDIAN_CONTEXT);
107
+
108
+ restore();
109
+ expect(conv.setTrustContextCalls).toEqual([]);
110
+ expect(conv.trustContext).toBe(GUARDIAN_CONTEXT);
111
+ });
112
+
113
+ test("does not mutate trust while the conversation is processing", async () => {
114
+ const conv = new FakeConversation({
115
+ trustContext: undefined,
116
+ processing: true,
117
+ });
118
+
119
+ const restore = await elevatePointerConversationToGuardian(conv);
120
+
121
+ expect(conv.setTrustContextCalls).toEqual([]);
122
+ expect(conv.ensureCalls).toBe(0);
123
+ expect(conv.trustContext).toBeUndefined();
124
+
125
+ restore();
126
+ expect(conv.setTrustContextCalls).toEqual([]);
127
+ });
128
+
129
+ test("restore leaves trust alone when a later turn replaced it", async () => {
130
+ const conv = new FakeConversation({ trustContext: undefined });
131
+ const restore = await elevatePointerConversationToGuardian(conv);
132
+ expect(conv.trustContext).toBe(INTERNAL_GUARDIAN_TRUST_CONTEXT);
133
+
134
+ // Simulate a new turn legitimately replacing the trust context mid-flight.
135
+ conv.setTrustContext(GUARDIAN_CONTEXT);
136
+ restore();
137
+
138
+ // The restorer must not clobber the new turn's context.
139
+ expect(conv.trustContext).toBe(GUARDIAN_CONTEXT);
140
+ });
141
+ });
@@ -122,6 +122,7 @@ import {
122
122
  rebuildBm25CorpusStatsAndReseedSkills,
123
123
  } from "./memory-v2-startup.js";
124
124
  import { startOrphanReaper } from "./orphan-reaper.js";
125
+ import { elevatePointerConversationToGuardian } from "./pointer-conversation-trust.js";
125
126
  import { processMessage } from "./process-message.js";
126
127
  import { runProfilerSweep } from "./profiler-run-store.js";
127
128
  import {
@@ -1117,6 +1118,14 @@ export async function runDaemon(): Promise<void> {
1117
1118
  const conversation =
1118
1119
  await server.getConversationForMessages(conversationId);
1119
1120
 
1121
+ // Pointer turns are guardian-gated owner self-maintenance: elevate to
1122
+ // the internal guardian context and rehydrate history so a cold
1123
+ // (evicted) load doesn't filter guardian history to empty and ship a
1124
+ // cache-missing turn. `restoreTrustContext` undoes the elevation after
1125
+ // the turn. See pointer-conversation-trust.ts for the full rationale.
1126
+ const restoreTrustContext =
1127
+ await elevatePointerConversationToGuardian(conversation);
1128
+
1120
1129
  // Constrain pointer generation to a tool-disabled path so call-
1121
1130
  // status events cannot trigger unintended side-effect tools.
1122
1131
  // Incrementing toolsDisabledDepth causes the resolveTools callback
@@ -1242,6 +1251,8 @@ export async function runDaemon(): Promise<void> {
1242
1251
  } finally {
1243
1252
  // Restore tool availability so subsequent turns aren't affected.
1244
1253
  conversation.toolsDisabledDepth--;
1254
+ // Undo the temporary guardian elevation installed above.
1255
+ restoreTrustContext();
1245
1256
  }
1246
1257
  },
1247
1258
  );
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Guardian-trust elevation for call-status pointer turns.
3
+ *
4
+ * Call-status pointer messages are generated as owner self-maintenance turns:
5
+ * only guardian-gated audiences route through the daemon processor (see
6
+ * `resolvePointerAudienceTrust` in `calls/call-pointer-messages.ts`), so the
7
+ * generation should run under the internal guardian context.
8
+ *
9
+ * The subtlety is history: `Conversation.loadFromDb` filters the persisted
10
+ * history to non-guardian provenance whenever the active trust context cannot
11
+ * access memory. On a cold (evicted) load — common on a memory-pressured daemon
12
+ * that evicts idle conversations — a freshly rebuilt conversation has no trust
13
+ * context, so a guardian-authored conversation's entire history is filtered to
14
+ * empty. The pointer turn would then ship with only its freshly persisted
15
+ * instruction, and because dropping the prior history (and tools) shifts the
16
+ * cacheable prefix, the request misses the prompt cache completely.
17
+ *
18
+ * Elevating to the guardian context and rehydrating before the turn restores
19
+ * the full history and makes the turn's shape deterministic regardless of
20
+ * eviction state.
21
+ */
22
+ import { resolveCapabilities } from "../runtime/capabilities.js";
23
+ import {
24
+ INTERNAL_GUARDIAN_TRUST_CONTEXT,
25
+ type TrustContext,
26
+ } from "./trust-context.js";
27
+
28
+ /** Minimal `Conversation` surface needed to elevate pointer trust. */
29
+ export interface GuardianElevatableConversation {
30
+ readonly trustContext?: TrustContext | undefined;
31
+ isProcessing(): boolean;
32
+ setTrustContext(ctx: TrustContext | null): void;
33
+ ensureActorScopedHistory(): Promise<void>;
34
+ }
35
+
36
+ /**
37
+ * Elevate a pointer conversation to the internal guardian context and rehydrate
38
+ * its history so guardian-authored history is not filtered to empty on a cold
39
+ * load. Returns a function that restores the prior trust context.
40
+ *
41
+ * No-ops (returning a no-op restorer) when the conversation is already
42
+ * memory-capable, or when it is mid-turn — mutating `trustContext` during an
43
+ * active loop would elevate that turn's actor trust (mirrors the warm-path guard
44
+ * in `conversation-store.ts`).
45
+ */
46
+ export async function elevatePointerConversationToGuardian(
47
+ conversation: GuardianElevatableConversation,
48
+ ): Promise<() => void> {
49
+ const priorTrustContext = conversation.trustContext;
50
+ const shouldElevate =
51
+ !conversation.isProcessing() &&
52
+ !resolveCapabilities(priorTrustContext?.trustClass).canAccessMemory;
53
+ if (!shouldElevate) return () => {};
54
+
55
+ conversation.setTrustContext(INTERNAL_GUARDIAN_TRUST_CONTEXT);
56
+ await conversation.ensureActorScopedHistory();
57
+
58
+ return () => {
59
+ // Undo only the elevation this call installed. If a new turn started at an
60
+ // await boundary and legitimately updated trustContext, the reference will
61
+ // differ and we leave it alone.
62
+ if (conversation.trustContext === INTERNAL_GUARDIAN_TRUST_CONTEXT) {
63
+ conversation.setTrustContext(priorTrustContext ?? null);
64
+ }
65
+ };
66
+ }