@vellumai/assistant 0.10.0-dev.202606192330.7061672 → 0.10.0-dev.202606200318.c052d10

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.0-dev.202606192330.7061672",
3
+ "version": "0.10.0-dev.202606200318.c052d10",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -860,6 +860,123 @@ describe("loadFromDb metadata injection rehydration", () => {
860
860
  ]);
861
861
  });
862
862
 
863
+ test("rehydration order matches live assembly for background-turn / channel-capabilities / non-interactive-context", async () => {
864
+ // A background/scheduled source's live turns inject three extra blocks that
865
+ // the metadata persist layer now captures so a reloaded or forked
866
+ // conversation (memory retrospective) reproduces them byte-for-byte.
867
+ // Live assembly lands them at:
868
+ // - `<background_turn>` — prepend-user-tail injector order 15, between
869
+ // `<workspace>` (10) and `<turn_context>` (20).
870
+ // - `<channel_capabilities>` — Step-3 prepend, just below `<turn_context>`
871
+ // and above the after-memory region.
872
+ // - `<non_interactive_context>` — Step-3 APPEND, the very last block.
873
+ // Expected layout (cf. live `applyRuntimeInjections`):
874
+ // [<workspace>, <background_turn>, <turn_context>, <channel_capabilities>,
875
+ // <memory>dynamic</memory>, <info>v2static</info>, <memory>v3</memory>,
876
+ // <NOW.md>, <system_reminder>, <knowledge_base>, ...original,
877
+ // <non_interactive_context>]
878
+ // Rehydration must reproduce this or a background-source fork busts the
879
+ // message-tier prefix cache at the first divergent block.
880
+ mockConversation = defaultConv();
881
+ mockDbMessages = [
882
+ {
883
+ id: "m1",
884
+ role: "user",
885
+ content: JSON.stringify([{ type: "text", text: "First turn" }]),
886
+ metadata: JSON.stringify({
887
+ workspaceBlock: "<workspace>\nworkspace body\n</workspace>",
888
+ backgroundTurnBlock: "<background_turn>\nbg body\n</background_turn>",
889
+ turnContextBlock: "<turn_context>\nctx payload\n</turn_context>",
890
+ channelCapabilitiesBlock:
891
+ "<channel_capabilities>\nchannel: vellum\n</channel_capabilities>",
892
+ memoryInjectedBlock: "mem payload",
893
+ memoryV2StaticBlock:
894
+ "<info>\n## Essentials\n\nAlice prefers VS Code.\n</info>",
895
+ memoryV3InjectedBlock:
896
+ "header line\n\n# memory/concepts/page-a.md\nhead a",
897
+ nowScratchpadBlock: "<NOW.md>\nnow body\n</NOW.md>",
898
+ pkbSystemReminderBlock:
899
+ "<system_reminder>\npkb reminder body\n</system_reminder>",
900
+ pkbContextBlock: "<knowledge_base>\nkb body\n</knowledge_base>",
901
+ nonInteractiveContextBlock:
902
+ "<non_interactive_context>\nno human present\n</non_interactive_context>",
903
+ }),
904
+ },
905
+ {
906
+ id: "m2",
907
+ role: "assistant",
908
+ content: JSON.stringify([{ type: "text", text: "Reply" }]),
909
+ },
910
+ {
911
+ id: "m3",
912
+ role: "user",
913
+ content: JSON.stringify([{ type: "text", text: "Tail" }]),
914
+ },
915
+ ];
916
+
917
+ const conversation = makeConversation();
918
+ await conversation.loadFromDb();
919
+ const messages = conversation.getMessages();
920
+
921
+ expect(messages).toHaveLength(3);
922
+ expect(messages[0].content).toEqual([
923
+ { type: "text", text: "<workspace>\nworkspace body\n</workspace>" },
924
+ { type: "text", text: "<background_turn>\nbg body\n</background_turn>" },
925
+ { type: "text", text: "<turn_context>\nctx payload\n</turn_context>" },
926
+ {
927
+ type: "text",
928
+ text: "<channel_capabilities>\nchannel: vellum\n</channel_capabilities>",
929
+ },
930
+ { type: "text", text: "<memory>\nmem payload\n</memory>" },
931
+ {
932
+ type: "text",
933
+ text: "<info>\n## Essentials\n\nAlice prefers VS Code.\n</info>",
934
+ },
935
+ {
936
+ type: "text",
937
+ text: "<memory>\nheader line\n\n# memory/concepts/page-a.md\nhead a\n</memory>",
938
+ },
939
+ { type: "text", text: "<NOW.md>\nnow body\n</NOW.md>" },
940
+ {
941
+ type: "text",
942
+ text: "<system_reminder>\npkb reminder body\n</system_reminder>",
943
+ },
944
+ { type: "text", text: "<knowledge_base>\nkb body\n</knowledge_base>" },
945
+ { type: "text", text: "First turn" },
946
+ {
947
+ type: "text",
948
+ text: "<non_interactive_context>\nno human present\n</non_interactive_context>",
949
+ },
950
+ ]);
951
+ });
952
+
953
+ test("tail user row skips background-turn / channel-capabilities / non-interactive-context", async () => {
954
+ // The tail row re-injects these fresh next turn, so rehydration must skip
955
+ // them on the tail — mirroring `<turn_context>` / `<workspace>`.
956
+ mockConversation = defaultConv();
957
+ mockDbMessages = [
958
+ {
959
+ id: "m1",
960
+ role: "user",
961
+ content: JSON.stringify([{ type: "text", text: "Tail" }]),
962
+ metadata: JSON.stringify({
963
+ backgroundTurnBlock: "<background_turn>\nbg body\n</background_turn>",
964
+ channelCapabilitiesBlock:
965
+ "<channel_capabilities>\nchannel: vellum\n</channel_capabilities>",
966
+ nonInteractiveContextBlock:
967
+ "<non_interactive_context>\nno human present\n</non_interactive_context>",
968
+ }),
969
+ },
970
+ ];
971
+
972
+ const conversation = makeConversation();
973
+ await conversation.loadFromDb();
974
+ const messages = conversation.getMessages();
975
+
976
+ expect(messages).toHaveLength(1);
977
+ expect(messages[0].content).toEqual([{ type: "text", text: "Tail" }]);
978
+ });
979
+
863
980
  test("untrusted-actor view does not rehydrate memoryV2StaticBlock", async () => {
864
981
  mockConversation = defaultConv();
865
982
  // Rows with `trusted_contact` / `unknown` provenance survive the
@@ -88,6 +88,7 @@ import {
88
88
  loadSlackActiveThreadFocusBlock,
89
89
  loadSlackChronologicalContext,
90
90
  loadSlackChronologicalMessages,
91
+ NON_INTERACTIVE_CONTEXT_BLOCK,
91
92
  resolveChannelCapabilities,
92
93
  resolveTurnInboundActorContext,
93
94
  resolveTurnModelProfileLabel,
@@ -941,6 +942,32 @@ describe("applyRuntimeInjections — injection mode", () => {
941
942
  expect(allText).toContain("<knowledge_base>");
942
943
  });
943
944
 
945
+ test("captures background-turn / channel-capabilities / non-interactive-context for metadata persistence", async () => {
946
+ // These three blocks must be captured into `blocks` so persistInjectionBlocks
947
+ // writes them to message metadata and loadFromDb rehydrates them on a
948
+ // reload/fork (background-source memory retrospective cache parity — see the
949
+ // rehydration-order tests in conversation-lifecycle.test.ts). background-turn
950
+ // fires only for a background/scheduled live conversation, so register one
951
+ // under the turn's conversation id.
952
+ setConversation("injection-mode-conv", {
953
+ conversationId: "injection-mode-conv",
954
+ workingDir: "/sandbox",
955
+ workspaceTopLevelContext: "",
956
+ workspaceTopLevelDirty: false,
957
+ surfaceState: new Map(),
958
+ channelCapabilities,
959
+ conversationType: "background",
960
+ } as never);
961
+
962
+ const { blocks } = await applyRuntimeInjections(baseMessages, fullOptions);
963
+
964
+ expect(blocks.backgroundTurnBlock).toContain("<background_turn>");
965
+ expect(blocks.channelCapabilitiesBlock).toContain("<channel_capabilities>");
966
+ expect(blocks.nonInteractiveContextBlock).toBe(
967
+ NON_INTERACTIVE_CONTEXT_BLOCK,
968
+ );
969
+ });
970
+
944
971
  test("explicit mode: 'full' behaves the same as default", async () => {
945
972
  const { messages: result } = await applyRuntimeInjections(baseMessages, {
946
973
  ...fullOptions,
@@ -681,13 +681,14 @@ export function stripNowScratchpad(messages: Message[]): Message[] {
681
681
  }
682
682
 
683
683
  /**
684
- * Prepend channel capability context to the last user message so the
685
- * model knows what the current channel can and cannot do.
684
+ * Build the `<channel_capabilities>` block text for the given capabilities, or
685
+ * `null` on the happy path (desktop, full capabilities, no special context)
686
+ * where no block is injected. Split from {@link injectChannelCapabilityContext}
687
+ * so callers can capture the exact injected text for metadata persistence.
686
688
  */
687
- export function injectChannelCapabilityContext(
688
- message: Message,
689
+ export function buildChannelCapabilityBlock(
689
690
  caps: ChannelCapabilities,
690
- ): Message {
691
+ ): string | null {
691
692
  // Happy path: desktop with full capabilities and no special context — skip injection.
692
693
  if (
693
694
  caps.dashboardCapable &&
@@ -696,7 +697,7 @@ export function injectChannelCapabilityContext(
696
697
  !isGroupChatType(caps.chatType) &&
697
698
  caps.clientOS !== "macos"
698
699
  ) {
699
- return message;
700
+ return null;
700
701
  }
701
702
 
702
703
  const lines: string[] = ["<channel_capabilities>"];
@@ -769,7 +770,16 @@ export function injectChannelCapabilityContext(
769
770
 
770
771
  lines.push("</channel_capabilities>");
771
772
 
772
- const block = lines.join("\n");
773
+ return lines.join("\n");
774
+ }
775
+
776
+ /** Prepend the `<channel_capabilities>` block (if any) to a user message. */
777
+ export function injectChannelCapabilityContext(
778
+ message: Message,
779
+ caps: ChannelCapabilities,
780
+ ): Message {
781
+ const block = buildChannelCapabilityBlock(caps);
782
+ if (block === null) return message;
773
783
  return {
774
784
  ...message,
775
785
  content: [{ type: "text", text: block }, ...message.content],
@@ -1512,6 +1522,14 @@ export function loadSlackActiveThreadFocusBlock(
1512
1522
  */
1513
1523
  export type InjectionMode = "full" | "minimal";
1514
1524
 
1525
+ /**
1526
+ * The `<non_interactive_context>` block appended on non-interactive turns.
1527
+ * Module-scoped so `applyRuntimeInjections` both injects it and captures the
1528
+ * exact text for metadata persistence from a single source of truth.
1529
+ */
1530
+ export const NON_INTERACTIVE_CONTEXT_BLOCK =
1531
+ "<non_interactive_context>\nNon-interactive scheduled task — do not ask for clarification or confirmation. Follow the instructions exactly using your best judgment. If recalled memory contains conflicting notes, prefer the explicit instruction in this message.\n</non_interactive_context>";
1532
+
1515
1533
  /**
1516
1534
  * Per-turn injection bytes captured so `loadFromDb` can rehydrate historical
1517
1535
  * user messages byte-for-byte after a daemon restart or conversation
@@ -1527,6 +1545,27 @@ export interface RuntimeInjectionBlocks {
1527
1545
  nowScratchpadBlock?: string;
1528
1546
  pkbContextBlock?: string;
1529
1547
  memoryV2StaticBlock?: string;
1548
+ /**
1549
+ * The `<background_turn>` block the `background-turn` default injector
1550
+ * attached this turn (background/scheduled non-interactive turns only).
1551
+ * Persisted under `metadata.backgroundTurnBlock` so `loadFromDb` rehydrates
1552
+ * it byte-for-byte on reload/fork — without it, a fork of a background
1553
+ * source (memory retrospective) drops the block and breaks message-tier
1554
+ * prefix-cache parity with the source's live turns.
1555
+ */
1556
+ backgroundTurnBlock?: string;
1557
+ /**
1558
+ * The `<channel_capabilities>` block injected this turn (non-default channel
1559
+ * capabilities). Persisted under `metadata.channelCapabilitiesBlock` for the
1560
+ * same reload/fork cache-parity reason as {@link backgroundTurnBlock}.
1561
+ */
1562
+ channelCapabilitiesBlock?: string;
1563
+ /**
1564
+ * The `<non_interactive_context>` block injected this turn (non-interactive
1565
+ * turns only). Persisted under `metadata.nonInteractiveContextBlock` for the
1566
+ * same reload/fork cache-parity reason as {@link backgroundTurnBlock}.
1567
+ */
1568
+ nonInteractiveContextBlock?: string;
1530
1569
  /**
1531
1570
  * UNWRAPPED inner text of the memory-v3 frozen net-new card block the v3
1532
1571
  * injector attached this turn, mirroring v2's unwrapped `memoryInjectedBlock`
@@ -2108,6 +2147,9 @@ export async function applyRuntimeInjections(
2108
2147
  let pkbSystemReminderCaptured: string | undefined;
2109
2148
  let memoryV2StaticCaptured: string | undefined;
2110
2149
  let memoryV3Captured: string | undefined;
2150
+ let backgroundTurnCaptured: string | undefined;
2151
+ let channelCapabilitiesCaptured: string | undefined;
2152
+ let nonInteractiveContextCaptured: string | undefined;
2111
2153
  const initialTail = runMessages[runMessages.length - 1];
2112
2154
  const initialTailIsUser = !!initialTail && initialTail.role === "user";
2113
2155
  if (initialTailIsUser) {
@@ -2131,6 +2173,9 @@ export async function applyRuntimeInjections(
2131
2173
  case "memory-v2-static":
2132
2174
  memoryV2StaticCaptured = block.text;
2133
2175
  break;
2176
+ case "background-turn":
2177
+ backgroundTurnCaptured = block.text;
2178
+ break;
2134
2179
  case MEMORY_V3_BLOCK_ID: {
2135
2180
  // The v3 frozen card block is persisted UNWRAPPED (the v2
2136
2181
  // `memoryInjectedBlock` contract — rehydration re-wraps on use).
@@ -2260,16 +2305,14 @@ export async function applyRuntimeInjections(
2260
2305
  if (isNonInteractive) {
2261
2306
  const userTail = result[result.length - 1];
2262
2307
  if (userTail && userTail.role === "user") {
2308
+ nonInteractiveContextCaptured = NON_INTERACTIVE_CONTEXT_BLOCK;
2263
2309
  result = [
2264
2310
  ...result.slice(0, -1),
2265
2311
  {
2266
2312
  ...userTail,
2267
2313
  content: [
2268
2314
  ...userTail.content,
2269
- {
2270
- type: "text" as const,
2271
- text: "<non_interactive_context>\nNon-interactive scheduled task — do not ask for clarification or confirmation. Follow the instructions exactly using your best judgment. If recalled memory contains conflicting notes, prefer the explicit instruction in this message.\n</non_interactive_context>",
2272
- },
2315
+ { type: "text" as const, text: NON_INTERACTIVE_CONTEXT_BLOCK },
2273
2316
  ],
2274
2317
  },
2275
2318
  ];
@@ -2310,10 +2353,21 @@ export async function applyRuntimeInjections(
2310
2353
  if (channelCapabilities) {
2311
2354
  const userTail = result[result.length - 1];
2312
2355
  if (userTail && userTail.role === "user") {
2313
- result = [
2314
- ...result.slice(0, -1),
2315
- injectChannelCapabilityContext(userTail, channelCapabilities),
2316
- ];
2356
+ const channelCapabilityBlock =
2357
+ buildChannelCapabilityBlock(channelCapabilities);
2358
+ if (channelCapabilityBlock !== null) {
2359
+ channelCapabilitiesCaptured = channelCapabilityBlock;
2360
+ result = [
2361
+ ...result.slice(0, -1),
2362
+ {
2363
+ ...userTail,
2364
+ content: [
2365
+ { type: "text" as const, text: channelCapabilityBlock },
2366
+ ...userTail.content,
2367
+ ],
2368
+ },
2369
+ ];
2370
+ }
2317
2371
  }
2318
2372
  }
2319
2373
 
@@ -2372,6 +2426,9 @@ export async function applyRuntimeInjections(
2372
2426
  pkbContextBlock: pkbContextCaptured,
2373
2427
  memoryV2StaticBlock: memoryV2StaticCaptured,
2374
2428
  memoryV3InjectedBlock: memoryV3Captured,
2429
+ backgroundTurnBlock: backgroundTurnCaptured,
2430
+ channelCapabilitiesBlock: channelCapabilitiesCaptured,
2431
+ nonInteractiveContextBlock: nonInteractiveContextCaptured,
2375
2432
  memoryV3Active,
2376
2433
  injectorChainBlock,
2377
2434
  },
@@ -976,6 +976,18 @@ export class Conversation {
976
976
  const meta = JSON.parse(m.metadata);
977
977
  const isTail = index === arr.length - 1;
978
978
 
979
+ // `<non_interactive_context>` is the only rehydrated block that
980
+ // APPENDS to the tail (live injection appends it in Step 3), so it
981
+ // must land after the original content. Apply it first — before the
982
+ // prepends below — so the prepends stack in front of it and it stays
983
+ // last, matching the live layout.
984
+ if (!isTail && typeof meta.nonInteractiveContextBlock === "string") {
985
+ content = [
986
+ ...content,
987
+ { type: "text" as const, text: meta.nonInteractiveContextBlock },
988
+ ];
989
+ }
990
+
979
991
  // Rehydrate in reverse injection order (innermost block first)
980
992
  // so the resulting layout matches `applyRuntimeInjections`'s
981
993
  // after-memory-prefix splices in ascending injector order
@@ -1086,6 +1098,17 @@ export class Conversation {
1086
1098
  ];
1087
1099
  }
1088
1100
 
1101
+ // `<channel_capabilities>` lands just below `<turn_context>`: live
1102
+ // injection prepends it (Step 3) before the prepend-user-tail chain
1103
+ // blocks (Step 4), so it must be prepended BEFORE turnContextBlock
1104
+ // here to land one slot deeper than `<turn_context>`.
1105
+ if (!isTail && typeof meta.channelCapabilitiesBlock === "string") {
1106
+ content = [
1107
+ { type: "text" as const, text: meta.channelCapabilitiesBlock },
1108
+ ...content,
1109
+ ];
1110
+ }
1111
+
1089
1112
  if (!isTail && typeof meta.turnContextBlock === "string") {
1090
1113
  content = [
1091
1114
  { type: "text" as const, text: meta.turnContextBlock },
@@ -1093,6 +1116,16 @@ export class Conversation {
1093
1116
  ];
1094
1117
  }
1095
1118
 
1119
+ // `<background_turn>` lands between `<workspace>` and `<turn_context>`
1120
+ // (injector order 15, between workspace 10 and unified-turn-context
1121
+ // 20), so prepend it AFTER turnContextBlock and BEFORE workspaceBlock.
1122
+ if (!isTail && typeof meta.backgroundTurnBlock === "string") {
1123
+ content = [
1124
+ { type: "text" as const, text: meta.backgroundTurnBlock },
1125
+ ...content,
1126
+ ];
1127
+ }
1128
+
1096
1129
  if (!isTail && typeof meta.workspaceBlock === "string") {
1097
1130
  content = [
1098
1131
  { type: "text" as const, text: meta.workspaceBlock },
@@ -685,39 +685,12 @@ describe("memoryRetrospectiveJob", () => {
685
685
  expect(wakeCalls[0]!.opts.toolGateMode).toBe("execution");
686
686
  });
687
687
 
688
- // -------------------------------------------------------------------------
689
- // Source background-turn parity (isNonInteractive)
690
- // -------------------------------------------------------------------------
691
-
692
- test("background source wake runs non-interactive (reproduces <background_turn>)", async () => {
693
- conversationOverrides["src-conv-1"] = {
694
- source: "heartbeat",
695
- forkParentMessageId: null,
696
- title: "Heartbeat",
697
- conversationType: "background",
698
- };
699
-
700
- const outcome = await memoryRetrospectiveJob(makeJob(), stubConfig);
701
-
702
- expect(outcome.kind).toBe("invoked");
703
- expect(wakeCalls).toHaveLength(1);
704
- expect(wakeCalls[0]!.opts.isNonInteractive).toBe(true);
705
- });
706
-
707
- test("standard source → wake stays interactive (no spurious <background_turn>)", async () => {
708
- conversationOverrides["src-conv-1"] = {
709
- source: "user",
710
- forkParentMessageId: null,
711
- title: "Source conversation",
712
- conversationType: "standard",
713
- };
714
-
715
- const outcome = await memoryRetrospectiveJob(makeJob(), stubConfig);
716
-
717
- expect(outcome.kind).toBe("invoked");
718
- expect(wakeCalls).toHaveLength(1);
719
- expect(wakeCalls[0]!.opts.isNonInteractive).toBe(false);
720
- });
688
+ // Source background-turn cache parity is no longer a wake-time concern: the
689
+ // fork reproduces the source's `<background_turn>` / `<channel_capabilities>`
690
+ // / `<non_interactive_context>` blocks via metadata rehydration in
691
+ // `Conversation.loadFromDb` (the wake never re-runs runtime injection). That
692
+ // round-trip is covered by the byte-parity test in
693
+ // `conversation-runtime-assembly.test.ts`.
721
694
 
722
695
  // -------------------------------------------------------------------------
723
696
  // Source-derived persona override
@@ -148,6 +148,13 @@ export const messageMetadataSchema = z
148
148
  nowScratchpadBlock: z.string().optional(),
149
149
  pkbContextBlock: z.string().optional(),
150
150
  memoryV2StaticBlock: z.string().optional(),
151
+ /** `<background_turn>` block (background/scheduled non-interactive turns),
152
+ * rehydrated by `loadFromDb` for reload/fork prefix-cache parity. */
153
+ backgroundTurnBlock: z.string().optional(),
154
+ /** `<channel_capabilities>` block, rehydrated for the same reason. */
155
+ channelCapabilitiesBlock: z.string().optional(),
156
+ /** `<non_interactive_context>` block, rehydrated for the same reason. */
157
+ nonInteractiveContextBlock: z.string().optional(),
151
158
  })
152
159
  .passthrough();
153
160
 
@@ -65,7 +65,6 @@ import {
65
65
  getMessagesAfter,
66
66
  resolveOverrideProfile,
67
67
  } from "./conversation-crud.js";
68
- import { isBackgroundConversationType } from "./conversation-types.js";
69
68
  import {
70
69
  enqueueMemoryJob,
71
70
  type MemoryJob,
@@ -314,14 +313,13 @@ async function runForkBasedRetrospective(
314
313
  // {@link WakeToolContextPin}.
315
314
  toolGateMode: "execution" as const,
316
315
  toolContextPin,
317
- // Reproduce the source's turn block for message-tier cache-prefix
318
- // parity: background/scheduled sources ran non-interactive live turns
319
- // (which carry `<background_turn>`), so the fork must run non-interactive
320
- // too. Standard/user sources ran interactive (no `<background_turn>`), so
321
- // the fork stays interactive preserving their existing byte parity.
322
- isNonInteractive: isBackgroundConversationType(
323
- sourceConversation.conversationType,
324
- ),
316
+ // Message-tier cache-prefix parity — reproducing the source's
317
+ // `<background_turn>` / `<channel_capabilities>` / `<non_interactive_context>`
318
+ // blocks is handled by metadata rehydration, not by re-running runtime
319
+ // injection on the fork: the source's live turns persist those blocks onto
320
+ // message metadata, the fork copies that metadata, and
321
+ // `Conversation.loadFromDb` rehydrates them byte-for-byte. The wake never
322
+ // re-runs the injection pipeline, so it needs no interactivity hint here.
325
323
  // Profile forcing (model/thinking/effort parity) is a separate concern
326
324
  // and stays keyed on `matchConversationProfile` via `matchedProfile`.
327
325
  ...(matchedProfile !== undefined
@@ -203,9 +203,8 @@ afterAll(() => {
203
203
  });
204
204
 
205
205
  const { getDb } = await import("../../db-connection.js");
206
- const { resetDbForTesting } = await import(
207
- "../../../__tests__/db-test-helpers.js"
208
- );
206
+ const { resetDbForTesting } =
207
+ await import("../../../__tests__/db-test-helpers.js");
209
208
  const { initializeDb } = await import("../../db-init.js");
210
209
  const { rawExec } = await import("../../raw-query.js");
211
210
  const { conversations, memoryJobs, messages } = await import("../../schema.js");
@@ -249,6 +248,10 @@ function makeJob(
249
248
  };
250
249
  }
251
250
 
251
+ // The first `initializeDb()` in a fresh test process builds the migration
252
+ // template by running every step; later calls restore it by file copy. That
253
+ // cold build can exceed bun's default 5s hook timeout under CI load, so give
254
+ // the hook generous headroom.
252
255
  beforeEach(() => {
253
256
  resetDbForTesting();
254
257
  initializeDb();
@@ -282,7 +285,7 @@ beforeEach(() => {
282
285
 
283
286
  migrationCalls.length = 0;
284
287
  migrationOutcome = { type: "ok" };
285
- });
288
+ }, 30_000);
286
289
 
287
290
  // ---------------------------------------------------------------------------
288
291
  // memoryV2MigrateJob
@@ -194,6 +194,9 @@ function persistInjectionBlocks(
194
194
  !blocks.pkbContextBlock &&
195
195
  !blocks.memoryV2StaticBlock &&
196
196
  !blocks.memoryV3InjectedBlock &&
197
+ !blocks.backgroundTurnBlock &&
198
+ !blocks.channelCapabilitiesBlock &&
199
+ !blocks.nonInteractiveContextBlock &&
197
200
  !removeV2Block
198
201
  ) {
199
202
  return;
@@ -229,6 +232,17 @@ function persistInjectionBlocks(
229
232
  if (blocks.memoryV2StaticBlock) {
230
233
  metadataUpdates.memoryV2StaticBlock = blocks.memoryV2StaticBlock;
231
234
  }
235
+ if (blocks.backgroundTurnBlock) {
236
+ metadataUpdates.backgroundTurnBlock = blocks.backgroundTurnBlock;
237
+ }
238
+ if (blocks.channelCapabilitiesBlock) {
239
+ metadataUpdates.channelCapabilitiesBlock =
240
+ blocks.channelCapabilitiesBlock;
241
+ }
242
+ if (blocks.nonInteractiveContextBlock) {
243
+ metadataUpdates.nonInteractiveContextBlock =
244
+ blocks.nonInteractiveContextBlock;
245
+ }
232
246
  updateMessageMetadata(ctx.userMessageId, metadataUpdates);
233
247
  } catch (err) {
234
248
  ctx.logger.warn(
@@ -196,14 +196,6 @@ export interface WakeOptions {
196
196
  * retrospectives whose conversation title already says "(Retrospective)").
197
197
  */
198
198
  suppressWakeSurface?: boolean;
199
- /**
200
- * Run the wake's turn as non-interactive (no client present). Threads to the
201
- * agent loop's `isNonInteractive`, which gates the `<non_interactive_context>`
202
- * and `<background_turn>` injectors. Fork-based memory retrospectives set this
203
- * to the source conversation's background-turn state so the fork reproduces
204
- * the source's injected turn block (prompt-cache prefix parity). Default false.
205
- */
206
- isNonInteractive?: boolean;
207
199
  /**
208
200
  * Optional exact tool allowlist for this wake. Used by internal maintenance
209
201
  * jobs that need the assistant's judgment but must not execute arbitrary
@@ -596,7 +588,6 @@ export async function wakeAgentForOpportunity(
596
588
  opts.forceOverrideProfile ??
597
589
  getConversationOverrideProfile(conversationId);
598
590
  const callSite = opts.callSite ?? "mainAgent";
599
- const isNonInteractive = opts.isNonInteractive ?? false;
600
591
  const config = getConfig();
601
592
  const effectiveContextWindow = resolveEffectiveContextWindow({
602
593
  llm: config.llm,
@@ -1144,7 +1135,6 @@ export async function wakeAgentForOpportunity(
1144
1135
  trust: wakeTrust,
1145
1136
  overrideProfile,
1146
1137
  forceOverrideProfile,
1147
- isNonInteractive,
1148
1138
  // The wake's compaction lives in the pre-run gate above
1149
1139
  // (`conversation.maybeCompact()`), never in the loop: the in-loop
1150
1140
  // budget gate and overflow-recovery ladder stay disabled because