@vellumai/assistant 0.8.9-staging.1 → 0.8.9-staging.2

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.
Files changed (62) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/openapi.yaml +6 -7
  3. package/package.json +1 -1
  4. package/src/__tests__/agent-loop-exit-reason.test.ts +33 -18
  5. package/src/__tests__/app-builder-tool-scripts.test.ts +21 -0
  6. package/src/__tests__/app-executors.test.ts +132 -0
  7. package/src/__tests__/compactor-web-search-strip.test.ts +213 -0
  8. package/src/__tests__/context-overflow-reducer.test.ts +1 -1
  9. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +19 -16
  10. package/src/__tests__/conversation-agent-loop-overflow.test.ts +48 -29
  11. package/src/__tests__/conversation-agent-loop.test.ts +48 -96
  12. package/src/__tests__/conversation-history-web-search.test.ts +1 -1
  13. package/src/__tests__/conversation-provider-retry-repair.test.ts +52 -49
  14. package/src/__tests__/memory-retrieval-hook.test.ts +68 -30
  15. package/src/__tests__/pre-model-call-sanitize.test.ts +109 -0
  16. package/src/acp/__tests__/helpers/exec-file-stub.ts +12 -7
  17. package/src/acp/__tests__/prepare-agent-env.test.ts +0 -40
  18. package/src/acp/__tests__/session-manager-resume.test.ts +179 -27
  19. package/src/acp/auto-install.test.ts +130 -46
  20. package/src/acp/auto-install.ts +86 -31
  21. package/src/acp/prepare-agent-env.ts +8 -9
  22. package/src/acp/resolve-agent.test.ts +26 -123
  23. package/src/acp/resolve-agent.ts +26 -85
  24. package/src/acp/resume-hint.ts +1 -3
  25. package/src/acp/session-manager.ts +32 -20
  26. package/src/acp/types.ts +0 -8
  27. package/src/agent/loop.ts +42 -26
  28. package/src/config/bundled-skills/acp/SKILL.md +14 -14
  29. package/src/config/bundled-skills/acp/TOOLS.json +2 -2
  30. package/src/config/bundled-skills/app-builder/SKILL.md +3 -3
  31. package/src/config/bundled-skills/app-builder/TOOLS.json +43 -2
  32. package/src/config/bundled-skills/app-builder/tools/app-update.ts +18 -0
  33. package/src/config/bundled-tool-registry.ts +2 -0
  34. package/src/config/call-site-defaults.ts +0 -2
  35. package/src/config/feature-flag-registry.json +1 -1
  36. package/src/config/schemas/call-site-catalog.ts +0 -14
  37. package/src/config/schemas/llm.ts +0 -2
  38. package/src/context/compactor.ts +22 -4
  39. package/src/daemon/conversation-agent-loop.ts +15 -57
  40. package/src/daemon/conversation.ts +23 -3
  41. package/src/daemon/disk-pressure-policy.ts +0 -1
  42. package/src/daemon/lifecycle.ts +0 -7
  43. package/src/{daemon → plugins/defaults/compaction}/context-overflow-reducer.ts +7 -7
  44. package/src/plugins/defaults/compaction/manager-store.ts +13 -0
  45. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +6 -23
  46. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +24 -25
  47. package/src/runtime/routes/__tests__/acp-routes.test.ts +71 -39
  48. package/src/runtime/routes/__tests__/plugins-routes.test.ts +29 -4
  49. package/src/runtime/routes/plugins-routes.ts +10 -9
  50. package/src/tools/acp/list-agents.test.ts +2 -2
  51. package/src/tools/acp/spawn.test.ts +103 -225
  52. package/src/tools/acp/spawn.ts +10 -120
  53. package/src/tools/apps/executors.ts +153 -42
  54. package/src/proactive-artifact/aux-message-injector.ts +0 -97
  55. package/src/proactive-artifact/decision.test.ts +0 -226
  56. package/src/proactive-artifact/decision.ts +0 -165
  57. package/src/proactive-artifact/index.ts +0 -7
  58. package/src/proactive-artifact/job.test.ts +0 -962
  59. package/src/proactive-artifact/job.ts +0 -372
  60. package/src/proactive-artifact/message-copy.ts +0 -58
  61. package/src/proactive-artifact/trigger-state.test.ts +0 -286
  62. package/src/proactive-artifact/trigger-state.ts +0 -123
@@ -39,6 +39,20 @@ const conversationRuntimeAssemblyRealSnapshot = {
39
39
 
40
40
  // ── Module mocks (must precede imports of the module under test) ─────
41
41
 
42
+ // The real AgentLoop resolves the per-conversation ContextWindowManager from
43
+ // the compaction store keyed by conversationId. These overflow tests build
44
+ // fake conversations whose manager is a canned stub, so register each stub in a
45
+ // map the mocked store reads from.
46
+ const fakeContextWindowManagers = new Map<string, unknown>();
47
+ mock.module("../plugins/defaults/compaction/manager-store.js", () => ({
48
+ createContextWindowManager: () => undefined,
49
+ getContextWindowManager: (conversationId: string) =>
50
+ fakeContextWindowManagers.get(conversationId),
51
+ disposeContextWindowManager: (conversationId: string) => {
52
+ fakeContextWindowManagers.delete(conversationId);
53
+ },
54
+ }));
55
+
42
56
  mock.module("../util/logger.js", () => ({
43
57
  getLogger: () =>
44
58
  new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
@@ -137,35 +151,38 @@ mock.module("../context/token-estimator.js", () => ({
137
151
  let mockReducerStepFn:
138
152
  | ((msgs: Message[], cfg: unknown, state: unknown) => unknown)
139
153
  | null = null;
140
- mock.module("../daemon/context-overflow-reducer.js", () => ({
141
- createInitialReducerState: () => ({
142
- appliedTiers: [],
143
- injectionMode: "full" as const,
144
- exhausted: false,
154
+ mock.module(
155
+ "../plugins/defaults/compaction/context-overflow-reducer.js",
156
+ () => ({
157
+ createInitialReducerState: () => ({
158
+ appliedTiers: [],
159
+ injectionMode: "full" as const,
160
+ exhausted: false,
161
+ }),
162
+ reduceContextOverflow: async (
163
+ msgs: Message[],
164
+ cfg: unknown,
165
+ state: unknown,
166
+ ) => {
167
+ if (mockReducerStepFn) return mockReducerStepFn(msgs, cfg, state);
168
+ return {
169
+ messages: msgs,
170
+ tier: "forced_compaction",
171
+ state: {
172
+ appliedTiers: [
173
+ "forced_compaction",
174
+ "tool_result_truncation",
175
+ "media_stubbing",
176
+ "injection_downgrade",
177
+ ],
178
+ injectionMode: "full",
179
+ exhausted: true,
180
+ },
181
+ estimatedTokens: 1000,
182
+ };
183
+ },
145
184
  }),
146
- reduceContextOverflow: async (
147
- msgs: Message[],
148
- cfg: unknown,
149
- state: unknown,
150
- ) => {
151
- if (mockReducerStepFn) return mockReducerStepFn(msgs, cfg, state);
152
- return {
153
- messages: msgs,
154
- tier: "forced_compaction",
155
- state: {
156
- appliedTiers: [
157
- "forced_compaction",
158
- "tool_result_truncation",
159
- "media_stubbing",
160
- "injection_downgrade",
161
- ],
162
- injectionMode: "full",
163
- exhausted: true,
164
- },
165
- estimatedTokens: 1000,
166
- };
167
- },
168
- }));
185
+ );
169
186
 
170
187
  // Policy: default to fail_gracefully
171
188
  let mockOverflowAction: string = "fail_gracefully";
@@ -471,7 +488,7 @@ function makeCtx(
471
488
  toolExecutor,
472
489
  });
473
490
 
474
- return {
491
+ const ctx = {
475
492
  conversationId: "test-conv",
476
493
  messages: [
477
494
  { role: "user", content: [{ type: "text", text: "Hello" }] },
@@ -582,6 +599,8 @@ function makeCtx(
582
599
 
583
600
  ...ctxOverrides,
584
601
  } as unknown as Conversation;
602
+ fakeContextWindowManagers.set(conversationId, ctx.contextWindowManager);
603
+ return ctx;
585
604
  }
586
605
 
587
606
  /**
@@ -28,6 +28,20 @@ let mockUiConfig: { userTimezone?: string; detectedTimezone?: string } = {};
28
28
 
29
29
  // ── Module mocks (must precede imports of the module under test) ─────
30
30
 
31
+ // The real AgentLoop resolves the per-conversation ContextWindowManager from
32
+ // the compaction store keyed by conversationId. These orchestrator tests build
33
+ // fake conversations whose manager is a canned stub, so register each stub in a
34
+ // map the mocked store reads from.
35
+ const fakeContextWindowManagers = new Map<string, unknown>();
36
+ mock.module("../plugins/defaults/compaction/manager-store.js", () => ({
37
+ createContextWindowManager: () => undefined,
38
+ getContextWindowManager: (conversationId: string) =>
39
+ fakeContextWindowManagers.get(conversationId),
40
+ disposeContextWindowManager: (conversationId: string) => {
41
+ fakeContextWindowManagers.delete(conversationId);
42
+ },
43
+ }));
44
+
31
45
  mock.module("../util/logger.js", () => ({
32
46
  getLogger: () =>
33
47
  new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
@@ -100,35 +114,38 @@ mock.module("../context/token-estimator.js", () => ({
100
114
  let mockReducerStepFn:
101
115
  | ((msgs: Message[], cfg: unknown, state: unknown) => unknown)
102
116
  | null = null;
103
- mock.module("../daemon/context-overflow-reducer.js", () => ({
104
- createInitialReducerState: () => ({
105
- appliedTiers: [],
106
- injectionMode: "full" as const,
107
- exhausted: false,
117
+ mock.module(
118
+ "../plugins/defaults/compaction/context-overflow-reducer.js",
119
+ () => ({
120
+ createInitialReducerState: () => ({
121
+ appliedTiers: [],
122
+ injectionMode: "full" as const,
123
+ exhausted: false,
124
+ }),
125
+ reduceContextOverflow: async (
126
+ msgs: Message[],
127
+ cfg: unknown,
128
+ state: unknown,
129
+ ) => {
130
+ if (mockReducerStepFn) return mockReducerStepFn(msgs, cfg, state);
131
+ return {
132
+ messages: msgs,
133
+ tier: "forced_compaction",
134
+ state: {
135
+ appliedTiers: [
136
+ "forced_compaction",
137
+ "tool_result_truncation",
138
+ "media_stubbing",
139
+ "injection_downgrade",
140
+ ],
141
+ injectionMode: "full",
142
+ exhausted: true,
143
+ },
144
+ estimatedTokens: 1000,
145
+ };
146
+ },
108
147
  }),
109
- reduceContextOverflow: async (
110
- msgs: Message[],
111
- cfg: unknown,
112
- state: unknown,
113
- ) => {
114
- if (mockReducerStepFn) return mockReducerStepFn(msgs, cfg, state);
115
- return {
116
- messages: msgs,
117
- tier: "forced_compaction",
118
- state: {
119
- appliedTiers: [
120
- "forced_compaction",
121
- "tool_result_truncation",
122
- "media_stubbing",
123
- "injection_downgrade",
124
- ],
125
- injectionMode: "full",
126
- exhausted: true,
127
- },
128
- estimatedTokens: 1000,
129
- };
130
- },
131
- }));
148
+ );
132
149
 
133
150
  // Policy: default to fail_gracefully
134
151
  let mockOverflowAction: string = "fail_gracefully";
@@ -520,17 +537,6 @@ mock.module("../memory/llm-request-log-store.js", () => ({
520
537
  setAgentLoopExitReasonOnLatestLog: setAgentLoopExitReasonOnLatestLogMock,
521
538
  }));
522
539
 
523
- let mockHasProactiveArtifactCompleted = true;
524
- let mockTryClaimProactiveArtifactTrigger = false;
525
- const runProactiveArtifactJobMock = mock(
526
- async (_params: Record<string, unknown>) => {},
527
- );
528
- mock.module("../proactive-artifact/index.js", () => ({
529
- hasProactiveArtifactCompleted: () => mockHasProactiveArtifactCompleted,
530
- tryClaimProactiveArtifactTrigger: () => mockTryClaimProactiveArtifactTrigger,
531
- runProactiveArtifactJob: runProactiveArtifactJobMock,
532
- }));
533
-
534
540
  // ── Imports (after mocks) ────────────────────────────────────────────
535
541
 
536
542
  import { AgentLoop } from "../agent/loop.js";
@@ -594,7 +600,7 @@ function makeCtx(
594
600
  toolExecutor,
595
601
  });
596
602
 
597
- return {
603
+ const ctx = {
598
604
  conversationId: "test-conv",
599
605
  messages: [
600
606
  { role: "user", content: [{ type: "text", text: "Hello" }] },
@@ -710,6 +716,8 @@ function makeCtx(
710
716
 
711
717
  ...ctxOverrides,
712
718
  } as unknown as Conversation;
719
+ fakeContextWindowManagers.set(conversationId, ctx.contextWindowManager);
720
+ return ctx;
713
721
  }
714
722
 
715
723
  type CompactionResult = Parameters<typeof applyCompactionResult>[1];
@@ -765,9 +773,6 @@ beforeEach(() => {
765
773
  title: null,
766
774
  };
767
775
  mockMessageById = null;
768
- mockHasProactiveArtifactCompleted = true;
769
- mockTryClaimProactiveArtifactTrigger = false;
770
- runProactiveArtifactJobMock.mockClear();
771
776
  setConversationHistoryStrippedAtMock.mockClear();
772
777
  setConversationHistoryStrippedAtMock.mockImplementation(() => {});
773
778
  applyRuntimeInjectionsMock.mockClear();
@@ -862,59 +867,6 @@ describe("session-agent-loop", () => {
862
867
  });
863
868
  });
864
869
 
865
- describe("proactive artifact trigger", () => {
866
- test("does not start proactive artifact jobs after foreground user turns", async () => {
867
- mockConversationRow = {
868
- ...mockConversationRow,
869
- id: "test-conv",
870
- conversationType: "standard",
871
- };
872
- mockMessageById = {
873
- id: "user-msg-1",
874
- conversationId: "test-conv",
875
- createdAt: 1000,
876
- };
877
- mockHasProactiveArtifactCompleted = false;
878
- mockTryClaimProactiveArtifactTrigger = true;
879
-
880
- // A two-call agent turn: the model invokes `app_create`, then wraps up
881
- // with a final text reply.
882
- const ctx = makeCtx({
883
- conversationId: "test-conv",
884
- providerResponses: [
885
- {
886
- content: [
887
- { type: "text", text: "I'll build that app." },
888
- {
889
- type: "tool_use",
890
- id: "tool-1",
891
- name: "app_create",
892
- input: { name: "Flow" },
893
- },
894
- ],
895
- model: "mock-model",
896
- usage: { inputTokens: 10, outputTokens: 5 },
897
- stopReason: "tool_use",
898
- },
899
- textResponse("Done."),
900
- ],
901
- toolExecutor: async () => ({ content: "{}", isError: false }),
902
- });
903
- await runAgentLoopImpl(
904
- ctx,
905
- "build a kanban app",
906
- "user-msg-1",
907
- () => {},
908
- {
909
- isUserMessage: true,
910
- },
911
- );
912
- await new Promise((resolve) => setTimeout(resolve, 0));
913
-
914
- expect(runProactiveArtifactJobMock).toHaveBeenCalledTimes(0);
915
- });
916
- });
917
-
918
870
  describe("disk pressure injection context", () => {
919
871
  // The loop sets `ctx.diskPressureCleanupModeActive` for the duration of the
920
872
  // turn (the disk-pressure-warning injector reads it via the per-conversation
@@ -807,7 +807,7 @@ describe("web_search_tool_result structural guard", () => {
807
807
  // Media token counting iterates tool_result.contentBlocks for nested
808
808
  // image/file blocks. web_search_tool_result has opaque content with no
809
809
  // contentBlocks property, so it cannot contain nested media.
810
- "daemon/context-overflow-reducer.ts",
810
+ "plugins/defaults/compaction/context-overflow-reducer.ts",
811
811
 
812
812
  // Final orphan-pair safety pass in the Slack transcript renderer.
813
813
  // Server-side block types (`server_tool_use`, `web_search_tool_result`)
@@ -100,56 +100,59 @@ mock.module("../context/token-estimator.js", () => ({
100
100
  // but these tests exercise the Conversation-level flow, not the reducer internals.
101
101
  // The reducer mock delegates to the compactFn to simulate a forced compaction
102
102
  // tier, matching the real reducer's behavior for Tier 1.
103
- mock.module("../daemon/context-overflow-reducer.js", () => ({
104
- createInitialReducerState: () => ({
105
- appliedTiers: [],
106
- injectionMode: "full" as const,
107
- exhausted: false,
108
- }),
109
- reduceContextOverflow: async (
110
- msgs: Message[],
111
- _cfg: unknown,
112
- _state: unknown,
113
- compactFn?: (
114
- m: Message[],
115
- s: AbortSignal | undefined,
116
- o: Record<string, unknown>,
117
- ) => Promise<{
118
- compacted: boolean;
119
- messages: Message[];
120
- compactedPersistedMessages?: number;
121
- summaryText?: string;
122
- [k: string]: unknown;
123
- }>,
124
- signal?: AbortSignal,
125
- ) => {
126
- let resultMessages = msgs;
127
- let compactionResult;
128
- if (compactFn) {
129
- const cr = await compactFn(msgs, signal, { force: true });
130
- if (cr.compacted) {
131
- resultMessages = cr.messages;
132
- compactionResult = cr;
103
+ mock.module(
104
+ "../plugins/defaults/compaction/context-overflow-reducer.js",
105
+ () => ({
106
+ createInitialReducerState: () => ({
107
+ appliedTiers: [],
108
+ injectionMode: "full" as const,
109
+ exhausted: false,
110
+ }),
111
+ reduceContextOverflow: async (
112
+ msgs: Message[],
113
+ _cfg: unknown,
114
+ _state: unknown,
115
+ compactFn?: (
116
+ m: Message[],
117
+ s: AbortSignal | undefined,
118
+ o: Record<string, unknown>,
119
+ ) => Promise<{
120
+ compacted: boolean;
121
+ messages: Message[];
122
+ compactedPersistedMessages?: number;
123
+ summaryText?: string;
124
+ [k: string]: unknown;
125
+ }>,
126
+ signal?: AbortSignal,
127
+ ) => {
128
+ let resultMessages = msgs;
129
+ let compactionResult;
130
+ if (compactFn) {
131
+ const cr = await compactFn(msgs, signal, { force: true });
132
+ if (cr.compacted) {
133
+ resultMessages = cr.messages;
134
+ compactionResult = cr;
135
+ }
133
136
  }
134
- }
135
- return {
136
- messages: resultMessages,
137
- tier: "forced_compaction",
138
- state: {
139
- appliedTiers: [
140
- "forced_compaction",
141
- "tool_result_truncation",
142
- "media_stubbing",
143
- "injection_downgrade",
144
- ],
145
- injectionMode: "full",
146
- exhausted: true,
147
- },
148
- estimatedTokens: 1000,
149
- compactionResult,
150
- };
151
- },
152
- }));
137
+ return {
138
+ messages: resultMessages,
139
+ tier: "forced_compaction",
140
+ state: {
141
+ appliedTiers: [
142
+ "forced_compaction",
143
+ "tool_result_truncation",
144
+ "media_stubbing",
145
+ "injection_downgrade",
146
+ ],
147
+ injectionMode: "full",
148
+ exhausted: true,
149
+ },
150
+ estimatedTokens: 1000,
151
+ compactionResult,
152
+ };
153
+ },
154
+ }),
155
+ );
153
156
 
154
157
  mock.module("../daemon/context-overflow-policy.js", () => ({
155
158
  resolveOverflowAction: () => "fail_gracefully",
@@ -6,10 +6,11 @@
6
6
  * metadata, recall log, `memory_recalled` event), trust gating, the runtime
7
7
  * injection the hook applies for every actor (and the assembled-block persist),
8
8
  * error propagation, and abort-signal forwarding. Uses `mock.module` to stub
9
- * the persistence helpers and `applyRuntimeInjections` so the test doesn't touch
10
- * the developer's real `~/.vellum`, database, or live conversation state. The
11
- * memory graph handle is a hand-rolled fake passed on the hook context — the
12
- * hook only needs `prepareMemory`.
9
+ * the persistence helpers, `applyRuntimeInjections`, and the conversation
10
+ * registry / trust resolver so the test doesn't touch the developer's real
11
+ * `~/.vellum`, database, or live conversation state. The memory graph handle,
12
+ * abort signal, and trust class are self-resolved by the hook from a fake
13
+ * conversation installed in the registry mock.
13
14
  */
14
15
 
15
16
  import { beforeEach, describe, expect, mock, test } from "bun:test";
@@ -37,7 +38,25 @@ mock.module("../daemon/conversation-runtime-assembly.js", () => ({
37
38
  applyRuntimeInjections: applyRuntimeInjectionsMock,
38
39
  }));
39
40
 
41
+ // The hook self-resolves the live conversation and its trust class; both are
42
+ // driven from these refs so each test controls what the hook sees.
43
+ let currentConversation: Conversation | undefined;
44
+ let currentTrustClass: "guardian" | "unknown" = "guardian";
45
+ const findConversationOrSubagentMock = mock(
46
+ (_conversationId?: string) => currentConversation,
47
+ );
48
+ mock.module("../daemon/conversation-registry.js", () => ({
49
+ findConversationOrSubagent: findConversationOrSubagentMock,
50
+ }));
51
+ mock.module("../daemon/trust-context.js", () => ({
52
+ resolveTrustClass: () => currentTrustClass,
53
+ }));
54
+ mock.module("../config/loader.js", () => ({
55
+ getConfig: () => ({}) as AssistantConfig,
56
+ }));
57
+
40
58
  import type { AssistantConfig } from "../config/schema.js";
59
+ import type { Conversation } from "../daemon/conversation.js";
41
60
  import type { ServerMessage } from "../daemon/message-protocol.js";
42
61
  import type { ConversationGraphMemory } from "../memory/graph/conversation-graph-memory.js";
43
62
  import type { QdrantSparseVector } from "../memory/qdrant-client.js";
@@ -118,18 +137,13 @@ function makeFakeGraphMemory(overrides?: {
118
137
  function makeHookCtx(
119
138
  overrides: Partial<MemoryRetrievalHookContext> = {},
120
139
  ): MemoryRetrievalHookContext {
121
- const { memory } = makeFakeGraphMemory();
122
140
  return {
123
- graphMemory: memory,
124
- config: {} as AssistantConfig,
125
141
  onEvent: () => {},
126
- isTrustedActor: true,
127
142
  conversationId: "conv-test",
128
143
  userMessageId: "msg-test",
129
144
  logger: {
130
145
  warn: () => {},
131
146
  } as unknown as MemoryRetrievalHookContext["logger"],
132
- signal: new AbortController().signal,
133
147
  latestMessages: [],
134
148
  requestId: "req-test",
135
149
  mode: "full",
@@ -140,10 +154,30 @@ function makeHookCtx(
140
154
  };
141
155
  }
142
156
 
157
+ /**
158
+ * Install a fake live conversation for the hook to self-resolve by id: the
159
+ * graph handle, abort signal, and trust class all come from here rather than
160
+ * the hook context.
161
+ */
162
+ function installConversation(
163
+ graphMemory: ConversationGraphMemory,
164
+ opts?: { trusted?: boolean; signal?: AbortSignal },
165
+ ): void {
166
+ currentTrustClass = opts?.trusted === false ? "unknown" : "guardian";
167
+ currentConversation = {
168
+ graphMemory,
169
+ trustContext: undefined,
170
+ abortController: { signal: opts?.signal ?? new AbortController().signal },
171
+ } as unknown as Conversation;
172
+ }
173
+
143
174
  beforeEach(() => {
144
175
  updateMessageMetadataMock.mockReset();
145
176
  recordMemoryRecallLogMock.mockReset();
146
177
  applyRuntimeInjectionsMock.mockClear();
178
+ findConversationOrSubagentMock.mockClear();
179
+ currentConversation = undefined;
180
+ currentTrustClass = "guardian";
147
181
  });
148
182
 
149
183
  describe("user-prompt-submit-temp hook (memory retrieval)", () => {
@@ -154,7 +188,8 @@ describe("user-prompt-submit-temp hook (memory retrieval)", () => {
154
188
  const { memory, prepareMemoryMock } = makeFakeGraphMemory({
155
189
  messages: injected,
156
190
  });
157
- const ctx = makeHookCtx({ graphMemory: memory, isTrustedActor: true });
191
+ installConversation(memory, { trusted: true });
192
+ const ctx = makeHookCtx();
158
193
 
159
194
  await userPromptSubmitMemoryRetrieval(ctx);
160
195
 
@@ -176,7 +211,8 @@ describe("user-prompt-submit-temp hook (memory retrieval)", () => {
176
211
  userQueryVector: userDense,
177
212
  userQuerySparseVector: userSparse,
178
213
  });
179
- const userCtx = makeHookCtx({ graphMemory: withUserQuery.memory });
214
+ installConversation(withUserQuery.memory, { trusted: true });
215
+ const userCtx = makeHookCtx();
180
216
  await userPromptSubmitMemoryRetrieval(userCtx);
181
217
  // User-query pair wins — never crossed with the summary signal — and is
182
218
  // recorded back onto the graph handle for the PKB-reminder injector.
@@ -189,7 +225,8 @@ describe("user-prompt-submit-temp hook (memory retrieval)", () => {
189
225
  queryVector: summaryDense,
190
226
  sparseVector: summarySparse,
191
227
  });
192
- const summaryCtx = makeHookCtx({ graphMemory: summaryOnly.memory });
228
+ installConversation(summaryOnly.memory, { trusted: true });
229
+ const summaryCtx = makeHookCtx();
193
230
  await userPromptSubmitMemoryRetrieval(summaryCtx);
194
231
  expect(summaryOnly.recordPkbQueryVectorsMock).toHaveBeenCalledWith(
195
232
  summaryDense,
@@ -209,7 +246,8 @@ describe("user-prompt-submit-temp hook (memory retrieval)", () => {
209
246
  messages: injected,
210
247
  blocks: { unifiedTurnContext: "tc-block" },
211
248
  }));
212
- const ctx = makeHookCtx({ graphMemory: memory, userMessageId: "msg-77" });
249
+ installConversation(memory, { trusted: true });
250
+ const ctx = makeHookCtx({ userMessageId: "msg-77" });
213
251
 
214
252
  await userPromptSubmitMemoryRetrieval(ctx);
215
253
 
@@ -229,11 +267,8 @@ describe("user-prompt-submit-temp hook (memory retrieval)", () => {
229
267
  const seeded: Message[] = [
230
268
  { role: "user", content: [{ type: "text", text: "seeded" }] },
231
269
  ];
232
- const ctx = makeHookCtx({
233
- graphMemory: memory,
234
- isTrustedActor: false,
235
- latestMessages: seeded,
236
- });
270
+ installConversation(memory, { trusted: false });
271
+ const ctx = makeHookCtx({ latestMessages: seeded });
237
272
 
238
273
  await userPromptSubmitMemoryRetrieval(ctx);
239
274
 
@@ -250,11 +285,8 @@ describe("user-prompt-submit-temp hook (memory retrieval)", () => {
250
285
  const seeded: Message[] = [
251
286
  { role: "user", content: [{ type: "text", text: "seeded" }] },
252
287
  ];
253
- const ctx = makeHookCtx({
254
- graphMemory: memory,
255
- isTrustedActor: false,
256
- latestMessages: seeded,
257
- });
288
+ installConversation(memory, { trusted: false });
289
+ const ctx = makeHookCtx({ latestMessages: seeded });
258
290
 
259
291
  await userPromptSubmitMemoryRetrieval(ctx);
260
292
 
@@ -273,8 +305,8 @@ describe("user-prompt-submit-temp hook (memory retrieval)", () => {
273
305
  injectedBlockText: "injected-block",
274
306
  metrics: makeMetrics(),
275
307
  });
308
+ installConversation(memory, { trusted: true });
276
309
  const ctx = makeHookCtx({
277
- graphMemory: memory,
278
310
  onEvent: (msg) => received.push(msg),
279
311
  userMessageId: "msg-42",
280
312
  conversationId: "conv-42",
@@ -298,7 +330,8 @@ describe("user-prompt-submit-temp hook (memory retrieval)", () => {
298
330
 
299
331
  test("skips metadata persist when no block text is injected", async () => {
300
332
  const { memory } = makeFakeGraphMemory({ injectedBlockText: null });
301
- const ctx = makeHookCtx({ graphMemory: memory });
333
+ installConversation(memory, { trusted: true });
334
+ const ctx = makeHookCtx();
302
335
 
303
336
  await userPromptSubmitMemoryRetrieval(ctx);
304
337
 
@@ -321,16 +354,17 @@ describe("user-prompt-submit-temp hook (memory retrieval)", () => {
321
354
  const graphMemory = {
322
355
  prepareMemory: failingPrepare,
323
356
  } as unknown as ConversationGraphMemory;
324
- const ctx = makeHookCtx({ graphMemory, isTrustedActor: true });
357
+ installConversation(graphMemory, { trusted: true });
358
+ const ctx = makeHookCtx();
325
359
 
326
360
  await expect(userPromptSubmitMemoryRetrieval(ctx)).rejects.toThrow(
327
361
  "retrieval failed",
328
362
  );
329
363
  });
330
364
 
331
- test("forwards the context abort signal into prepareMemory", async () => {
332
- // The hook hands its `ctx.signal` straight to `prepareMemory` so an
333
- // external cancel aborts the underlying retrieval.
365
+ test("forwards the conversation abort signal into prepareMemory", async () => {
366
+ // The hook hands the live conversation's abort signal straight to
367
+ // `prepareMemory` so an external cancel aborts the underlying retrieval.
334
368
  let capturedSignal: AbortSignal | undefined;
335
369
  const prepareMemoryMock = mock(
336
370
  async (
@@ -355,7 +389,11 @@ describe("user-prompt-submit-temp hook (memory retrieval)", () => {
355
389
  recordPkbQueryVectors: mock(() => {}),
356
390
  } as unknown as ConversationGraphMemory;
357
391
  const controller = new AbortController();
358
- const ctx = makeHookCtx({ graphMemory, signal: controller.signal });
392
+ installConversation(graphMemory, {
393
+ trusted: true,
394
+ signal: controller.signal,
395
+ });
396
+ const ctx = makeHookCtx();
359
397
 
360
398
  await userPromptSubmitMemoryRetrieval(ctx);
361
399