@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
|
@@ -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
|
+
});
|
package/src/daemon/lifecycle.ts
CHANGED
|
@@ -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
|
+
}
|