@vellumai/assistant 0.10.5-staging.1 → 0.10.5-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.
@@ -0,0 +1,407 @@
1
+ /**
2
+ * Queue-branch contract for hidden sends in POST /v1/messages.
3
+ *
4
+ * A hidden send is a machine signal (e.g. the channel-setup wizard-close
5
+ * marker), not a user decision — when it queues behind an in-flight turn it
6
+ * must NOT supersede pending interactions: no auto-denied confirmations, no
7
+ * steer of a parked ask_question. A visible send in the same state keeps the
8
+ * existing supersede behavior ("the user chose to move on").
9
+ */
10
+ import { afterEach, describe, expect, mock, test } from "bun:test";
11
+
12
+ mock.module("../config/env.js", () => ({ isHttpAuthDisabled: () => true }));
13
+
14
+ mock.module("../config/loader.js", () => ({
15
+ getConfig: () => ({
16
+ ui: {},
17
+ model: "claude-opus-4-7",
18
+ provider: "anthropic",
19
+ memory: { enabled: false },
20
+ rateLimit: { maxRequestsPerMinute: 0 },
21
+ secretDetection: { enabled: false },
22
+ contextWindow: { maxInputTokens: 200000 },
23
+ llm: {
24
+ default: {
25
+ provider: "anthropic",
26
+ model: "claude-opus-4-7",
27
+ maxTokens: 64000,
28
+ effort: "max" as const,
29
+ speed: "standard" as const,
30
+ temperature: null,
31
+ thinking: { enabled: true, streamThinking: true },
32
+ contextWindow: {
33
+ enabled: true,
34
+ maxInputTokens: 200000,
35
+ targetBudgetRatio: 0.3,
36
+ compactThreshold: 0.8,
37
+ summaryBudgetRatio: 0.05,
38
+ },
39
+ },
40
+ profiles: {},
41
+ callSites: {},
42
+ pricingOverrides: [],
43
+ },
44
+ services: {
45
+ inference: {
46
+ mode: "your-own",
47
+ provider: "anthropic",
48
+ model: "claude-opus-4-7",
49
+ },
50
+ "image-generation": {
51
+ mode: "your-own",
52
+ provider: "gemini",
53
+ model: "gemini-3.1-flash-image-preview",
54
+ },
55
+ "web-search": { mode: "your-own", provider: "inference-provider-native" },
56
+ },
57
+ }),
58
+ }));
59
+
60
+ mock.module("../util/logger.js", () => ({
61
+ getLogger: () =>
62
+ new Proxy({} as Record<string, unknown>, {
63
+ get: () => () => {},
64
+ }),
65
+ }));
66
+
67
+ mock.module("../persistence/conversation-key-store.js", () => ({
68
+ getOrCreateConversation: () => ({ conversationId: "conv-hidden-queue" }),
69
+ getConversationByKey: () => null,
70
+ }));
71
+
72
+ mock.module("../runtime/guardian-reply-router.js", () => ({
73
+ routeGuardianReply: async () => ({
74
+ consumed: false,
75
+ decisionApplied: false,
76
+ type: "not_consumed",
77
+ }),
78
+ }));
79
+
80
+ mock.module("../contacts/canonical-guardian-store.js", () => ({
81
+ createCanonicalGuardianRequest: () => ({
82
+ id: "canonical-id",
83
+ requestCode: "ABC123",
84
+ }),
85
+ generateCanonicalRequestCode: () => "ABC123",
86
+ listPendingCanonicalGuardianRequestsByDestinationConversation: () => [],
87
+ listCanonicalGuardianRequests: () => [],
88
+ listPendingRequestsByConversationScope: () => [],
89
+ resolveCanonicalGuardianRequest: () => undefined,
90
+ }));
91
+
92
+ mock.module("../runtime/confirmation-request-guardian-bridge.js", () => ({
93
+ bridgeConfirmationRequestToGuardian: async () => undefined,
94
+ }));
95
+
96
+ mock.module("../persistence/conversation-crud.js", () => ({
97
+ setConversationProcessingStartedAt: () => {},
98
+ isConversationProcessing: () => false,
99
+ addMessage: async (_conversationId: string, role: string) => ({
100
+ id: role === "user" ? "persisted-user-id" : "persisted-assistant-id",
101
+ deduplicated: false,
102
+ }),
103
+ extractImageSourcePaths: () => undefined,
104
+ getConversation: () => null,
105
+ getConversationOverrideProfile: () => undefined,
106
+ getMessages: () => [],
107
+ isHiddenMessageMetadata: (meta: Record<string, unknown> | undefined) =>
108
+ meta?.hidden === true,
109
+ provenanceFromTrustContext: (ctx: unknown) =>
110
+ ctx
111
+ ? { provenanceTrustClass: (ctx as Record<string, unknown>).trustClass }
112
+ : { provenanceTrustClass: "unknown" },
113
+ setConversationOriginChannelIfUnset: () => {},
114
+ setConversationOriginInterfaceIfUnset: () => {},
115
+ setConversationInferenceProfile: () => {},
116
+ setConversationEnabledPlugins: () => {},
117
+ reserveMessage: mock(async () => ({ id: "msg-reserve" })),
118
+ }));
119
+
120
+ mock.module("../persistence/conversation-disk-view.js", () => ({
121
+ syncMessageToDisk: () => {},
122
+ updateMetaFile: () => {},
123
+ }));
124
+
125
+ mock.module("../persistence/attachments-store.js", () => ({
126
+ getAttachmentsByIds: () => [],
127
+ getSourcePathsForAttachments: () => new Map(),
128
+ attachmentExists: () => false,
129
+ linkAttachmentToMessage: () => {},
130
+ attachInlineAttachmentToMessage: () => {},
131
+ validateAttachmentUpload: () => ({ ok: true }),
132
+ }));
133
+
134
+ mock.module("../daemon/conversation-process.js", () => ({
135
+ buildModelInfoEvent: () => ({
136
+ type: "model_info",
137
+ model: "claude-opus-4-7",
138
+ provider: "anthropic",
139
+ configuredProviders: ["anthropic"],
140
+ }),
141
+ isModelSlashCommand: () => false,
142
+ formatCompactResult: () => "",
143
+ }));
144
+
145
+ mock.module("../runtime/local-actor-identity.js", () => ({
146
+ resolveLocalTrustContext: () => ({
147
+ trustClass: "guardian",
148
+ sourceChannel: "vellum",
149
+ }),
150
+ }));
151
+
152
+ mock.module("../runtime/trust-context-resolver.js", () => ({
153
+ resolveTrustContext: () => ({
154
+ trustClass: "guardian",
155
+ sourceChannel: "vellum",
156
+ }),
157
+ withSourceChannel: (sourceChannel: unknown, ctx: unknown) => ({
158
+ ...(ctx as Record<string, unknown>),
159
+ sourceChannel,
160
+ }),
161
+ }));
162
+
163
+ mock.module("../contacts/guardian-delivery-reader.js", () => ({
164
+ getGuardianDelivery: async () => [
165
+ {
166
+ channelType: "vellum",
167
+ contactId: "guardian-contact",
168
+ principalId: "test-user",
169
+ address: "test-user",
170
+ status: "active",
171
+ },
172
+ ],
173
+ }));
174
+
175
+ mock.module("../ipc/gateway-client.js", () => ({
176
+ ipcCall: async () => ({ ok: true }),
177
+ }));
178
+
179
+ import type { Conversation } from "../daemon/conversation.js";
180
+ import {
181
+ deleteConversation,
182
+ setConversation,
183
+ } from "../daemon/conversation-registry.js";
184
+ import * as pendingInteractions from "../runtime/pending-interactions.js";
185
+ import { handleSendMessage } from "../runtime/routes/conversation-routes.js";
186
+ import { callHandler } from "./helpers/call-route-handler.js";
187
+
188
+ const CONV_ID = "conv-hidden-queue";
189
+
190
+ interface BusyConversationSpies {
191
+ conversation: Conversation;
192
+ enqueuedMetadata: () => Record<string, unknown> | undefined;
193
+ denyAllCount: () => number;
194
+ abortCount: () => number;
195
+ agentLoopOptions: () => Record<string, unknown> | undefined;
196
+ }
197
+
198
+ /**
199
+ * A live conversation with a pending tool confirmation. `processing: true`
200
+ * models a mid-turn conversation (queue branch); `false` an idle one whose
201
+ * confirmation outlived its turn (e.g. a guardian approval awaiting a
202
+ * channel reply).
203
+ */
204
+ function makeConversationWithPendingConfirmation(
205
+ processing: boolean,
206
+ ): BusyConversationSpies {
207
+ let enqueuedMetadata: Record<string, unknown> | undefined;
208
+ let denyAllCount = 0;
209
+ let abortCount = 0;
210
+ let agentLoopOptions: Record<string, unknown> | undefined;
211
+ const conversation = {
212
+ conversationId: CONV_ID,
213
+ messages: [],
214
+ abortController: {
215
+ abort: () => {
216
+ abortCount += 1;
217
+ },
218
+ },
219
+ currentRequestId: undefined,
220
+ queue: {
221
+ length: 0,
222
+ promoteToHead: (requestId: string) => ({ requestId }),
223
+ },
224
+ pendingSteerRepair: false,
225
+ setTrustContext: () => {},
226
+ updateClient: () => {},
227
+ emitConfirmationStateChanged: () => {},
228
+ emitActivityState: () => {},
229
+ setTurnChannelContext: () => {},
230
+ setTurnInterfaceContext: () => {},
231
+ getTurnChannelContext: () => null,
232
+ getTurnInterfaceContext: () => null,
233
+ ensureActorScopedHistory: async () => {},
234
+ isProcessing: () => processing,
235
+ setProcessing: () => {},
236
+ hasAnyPendingConfirmation: () => true,
237
+ denyAllPendingConfirmations: () => {
238
+ denyAllCount += 1;
239
+ },
240
+ enqueueMessage: (options: { metadata?: Record<string, unknown> }) => {
241
+ enqueuedMetadata = options.metadata;
242
+ return { queued: true, requestId: "queued-id" };
243
+ },
244
+ persistUserMessage: async () => ({
245
+ id: "persisted-user-id",
246
+ deduplicated: false,
247
+ }),
248
+ runAgentLoop: async (
249
+ _content: string,
250
+ _messageId: string,
251
+ options?: Record<string, unknown>,
252
+ ) => {
253
+ agentLoopOptions = options;
254
+ },
255
+ setPreactivatedSkillIds: () => {},
256
+ drainQueue: async () => {},
257
+ warmPromptCache: () => {},
258
+ getMessages: () => [],
259
+ assistantId: "self",
260
+ trustContext: undefined,
261
+ hasPendingConfirmation: () => false,
262
+ setHostBrowserProxy: () => {},
263
+ setHostCuProxy: () => {},
264
+ setHostAppControlProxy: () => {},
265
+ addPreactivatedSkillId: () => {},
266
+ usageStats: { inputTokens: 1000, outputTokens: 500, estimatedCost: 0.05 },
267
+ } as unknown as Conversation;
268
+ return {
269
+ conversation,
270
+ enqueuedMetadata: () => enqueuedMetadata,
271
+ denyAllCount: () => denyAllCount,
272
+ abortCount: () => abortCount,
273
+ agentLoopOptions: () => agentLoopOptions,
274
+ };
275
+ }
276
+
277
+ function makeBusyConversation(): BusyConversationSpies {
278
+ return makeConversationWithPendingConfirmation(true);
279
+ }
280
+
281
+ function makeRequest(extras: Record<string, unknown> = {}) {
282
+ return new Request("http://localhost/v1/messages", {
283
+ method: "POST",
284
+ headers: {
285
+ "Content-Type": "application/json",
286
+ "x-vellum-actor-principal-id": "test-user",
287
+ "x-vellum-principal-type": "actor",
288
+ },
289
+ body: JSON.stringify({
290
+ conversationKey: "hidden-queue-key",
291
+ content:
292
+ "[User action on channel_setup surface: closed the slack setup wizard]",
293
+ sourceChannel: "vellum",
294
+ interface: "macos",
295
+ ...extras,
296
+ }),
297
+ });
298
+ }
299
+
300
+ function makeDeps(conversation: Conversation) {
301
+ return {
302
+ sendMessageDeps: {
303
+ getOrCreateConversation: async () => conversation,
304
+ assistantEventHub: { publish: async () => {} } as never,
305
+ resolveAttachments: () => [],
306
+ },
307
+ };
308
+ }
309
+
310
+ const registeredRequestIds: string[] = [];
311
+ function registerConfirmation(): void {
312
+ const requestId = `pending-confirmation-${registeredRequestIds.length}`;
313
+ pendingInteractions.register(requestId, {
314
+ conversationId: CONV_ID,
315
+ kind: "confirmation",
316
+ });
317
+ registeredRequestIds.push(requestId);
318
+ }
319
+
320
+ afterEach(() => {
321
+ for (const id of registeredRequestIds) {
322
+ pendingInteractions.resolve(id, "cancelled");
323
+ }
324
+ registeredRequestIds.length = 0;
325
+ deleteConversation(CONV_ID);
326
+ });
327
+
328
+ describe("hidden sends queued behind an in-flight turn", () => {
329
+ test("carry hidden metadata and do NOT supersede pending interactions", async () => {
330
+ const spies = makeBusyConversation();
331
+ setConversation(CONV_ID, spies.conversation);
332
+ registerConfirmation();
333
+
334
+ const res = await callHandler(
335
+ (args) => handleSendMessage(args, makeDeps(spies.conversation)),
336
+ makeRequest({ hidden: true }),
337
+ undefined,
338
+ 202,
339
+ );
340
+
341
+ expect(res.status).toBe(202);
342
+ // Queued with the transcript-suppression flag intact...
343
+ expect(spies.enqueuedMetadata()?.hidden).toBe(true);
344
+ // ...without auto-denying the live approval prompt or aborting the turn:
345
+ // a passive UI event is not the user choosing to move on.
346
+ expect(spies.denyAllCount()).toBe(0);
347
+ expect(spies.abortCount()).toBe(0);
348
+ });
349
+
350
+ test("visible sends in the same state keep the supersede behavior", async () => {
351
+ const spies = makeBusyConversation();
352
+ setConversation(CONV_ID, spies.conversation);
353
+ registerConfirmation();
354
+
355
+ const res = await callHandler(
356
+ (args) => handleSendMessage(args, makeDeps(spies.conversation)),
357
+ makeRequest({ content: "actually, do this instead" }),
358
+ undefined,
359
+ 202,
360
+ );
361
+
362
+ expect(res.status).toBe(202);
363
+ expect(spies.enqueuedMetadata()?.hidden).toBeUndefined();
364
+ // The typed message supersedes: pending confirmations are auto-denied.
365
+ expect(spies.denyAllCount()).toBe(1);
366
+ });
367
+ });
368
+
369
+ describe("hidden sends to an idle conversation with a pending confirmation", () => {
370
+ test("do NOT auto-deny the confirmation and mark the turn as a hidden prompt", async () => {
371
+ const spies = makeConversationWithPendingConfirmation(false);
372
+ setConversation(CONV_ID, spies.conversation);
373
+ registerConfirmation();
374
+
375
+ const res = await callHandler(
376
+ (args) => handleSendMessage(args, makeDeps(spies.conversation)),
377
+ makeRequest({ hidden: true }),
378
+ undefined,
379
+ 202,
380
+ );
381
+
382
+ expect(res.status).toBe(202);
383
+ // The confirmation that outlived its turn (e.g. a guardian approval
384
+ // awaiting a channel reply) survives the machine signal...
385
+ expect(spies.denyAllCount()).toBe(0);
386
+ // ...and the turn is flagged so prompt-as-user-speech consumers (title
387
+ // generation) skip it.
388
+ expect(spies.agentLoopOptions()?.isHiddenPrompt).toBe(true);
389
+ });
390
+
391
+ test("visible sends to the same state keep the idle auto-deny cleanup", async () => {
392
+ const spies = makeConversationWithPendingConfirmation(false);
393
+ setConversation(CONV_ID, spies.conversation);
394
+ registerConfirmation();
395
+
396
+ const res = await callHandler(
397
+ (args) => handleSendMessage(args, makeDeps(spies.conversation)),
398
+ makeRequest({ content: "hello again" }),
399
+ undefined,
400
+ 202,
401
+ );
402
+
403
+ expect(res.status).toBe(202);
404
+ expect(spies.denyAllCount()).toBe(1);
405
+ expect(spies.agentLoopOptions()?.isHiddenPrompt).toBeUndefined();
406
+ });
407
+ });
@@ -4,7 +4,7 @@
4
4
  * A backgrounded bash/host_bash run posts a `<background_event
5
5
  * source="background-tool">` wake on completion, stamped with
6
6
  * `metadata.backgroundToolCompletion`. The history projection must surface
7
- * that structured record (and the `backgroundToolNotification` flag) so the
7
+ * that structured record (and the `backgroundEventNotification` flag) so the
8
8
  * web can rebuild a terminal inline card after a daemon restart.
9
9
  */
10
10
 
@@ -48,7 +48,7 @@ function resetTables() {
48
48
 
49
49
  interface ProjectedMessage {
50
50
  role: string;
51
- backgroundToolNotification?: boolean;
51
+ backgroundEventNotification?: boolean;
52
52
  backgroundToolCompletion?: Record<string, unknown>;
53
53
  }
54
54
 
@@ -76,8 +76,8 @@ describe("handleListMessages background-tool completion projection", () => {
76
76
  };
77
77
  // Mirror persistWakeTriggerMessage: a user-role
78
78
  // `<background_event source="background-tool">` row carrying the structured
79
- // completion. Not flagged `hidden`; the client suppresses it from the
80
- // transcript via `backgroundToolNotification`.
79
+ // completion. The client suppresses it from the transcript via
80
+ // `backgroundEventNotification`.
81
81
  await addMessage(
82
82
  conv.id,
83
83
  "user",
@@ -110,7 +110,7 @@ describe("handleListMessages background-tool completion projection", () => {
110
110
  (m) => m.backgroundToolCompletion !== undefined,
111
111
  );
112
112
  expect(wakeRow).toBeDefined();
113
- expect(wakeRow?.backgroundToolNotification).toBe(true);
113
+ expect(wakeRow?.backgroundEventNotification).toBe(true);
114
114
  expect(wakeRow?.backgroundToolCompletion).toEqual(completion);
115
115
  });
116
116
 
@@ -134,7 +134,7 @@ describe("handleListMessages background-tool completion projection", () => {
134
134
  expect(response.messages).toHaveLength(2);
135
135
  for (const message of response.messages) {
136
136
  expect(message.backgroundToolCompletion).toBeUndefined();
137
- expect(message.backgroundToolNotification).toBeUndefined();
137
+ expect(message.backgroundEventNotification).toBeUndefined();
138
138
  expect(() => ConversationMessageSchema.parse(message)).not.toThrow();
139
139
  }
140
140
  });
@@ -178,6 +178,31 @@ describe("handleListMessages in-memory queue", () => {
178
178
  expect(att?.id).toBe("req-att:attachment:0");
179
179
  });
180
180
 
181
+ test("filters hidden queued messages from the snapshot, keeping positions contiguous", async () => {
182
+ // A hidden send (e.g. the channel-setup wizard-close marker) that queued
183
+ // behind an in-flight turn must not surface as a queued bubble on a
184
+ // reload/reconnect fetch; visible siblings keep 1-based positions.
185
+ const conv = createConversation();
186
+ registerLiveConversation(conv.id, [
187
+ makeQueued({
188
+ requestId: "req-hidden",
189
+ content:
190
+ "[User action on channel_setup surface: closed the slack setup wizard]",
191
+ metadata: { hidden: true },
192
+ }),
193
+ makeQueued({ requestId: "req-visible", content: "a real message" }),
194
+ ]);
195
+
196
+ const response = handleListMessages({
197
+ queryParams: { conversationId: conv.id },
198
+ });
199
+ const body = response as { messages: MessagePayload[] };
200
+
201
+ expect(body.messages).toHaveLength(1);
202
+ expect(body.messages[0].id).toBe("req-visible");
203
+ expect(body.messages[0].queuePosition).toBe(1);
204
+ });
205
+
181
206
  test("does not append queued messages to an older-history page", async () => {
182
207
  // GIVEN history requested with beforeTimestamp (older page)
183
208
  const conv = createConversation();
@@ -32,6 +32,30 @@ describe("slack-app-setup skill regression", () => {
32
32
  );
33
33
  });
34
34
 
35
+ test("verifies on the wizard-closed auto-notify instead of manual confirmation", () => {
36
+ // Step 2 must promise the auto-notify rather than asking the user to
37
+ // report back...
38
+ expect(skillContent).toContain(
39
+ "The wizard will auto-notify me when you close it",
40
+ );
41
+ expect(skillContent).not.toContain("let me know when you're done");
42
+ // ...and Step 3 must trigger on the notification, keyed to the exact
43
+ // marker the web client sends on drawer close (see
44
+ // `clients/web/src/domains/chat/channel-setup-close-notify.ts`).
45
+ expect(skillContent).toContain("wizard-closed notification");
46
+ expect(skillContent).toContain(
47
+ "[User action on channel_setup surface: closed the slack setup wizard]",
48
+ );
49
+ // Phone-sized clients open the setup on the Contacts page, which cannot
50
+ // auto-notify on completion — the client signals the relocation with a
51
+ // hand-off marker and the skill must react by asking the user to report
52
+ // back instead of waiting for a wizard-closed notification.
53
+ expect(skillContent).toContain(
54
+ "[User action on channel_setup surface: moved the slack setup to the Contacts page]",
55
+ );
56
+ expect(skillContent).toContain("Hand-off notification");
57
+ });
58
+
35
59
  test("retains the Settings clearing path", () => {
36
60
  expect(skillContent).toContain(
37
61
  "same Slack settings handler used by Settings",
@@ -185,6 +185,22 @@ describe("title-generate user-prompt-submit hook", () => {
185
185
  expect(call).not.toHaveProperty("onTitleUpdated");
186
186
  });
187
187
 
188
+ test("skips title generation for hidden machine-signal prompts", async () => {
189
+ // A hidden send (e.g. the channel-setup wizard-close marker) is not user
190
+ // speech — minting a title from it would surface invisible scaffolding
191
+ // text in the sidebar.
192
+ const ctx = makeCtx({
193
+ prompt:
194
+ "[User action on channel_setup surface: closed the slack setup wizard]",
195
+ isHiddenPrompt: true,
196
+ });
197
+
198
+ await userPromptSubmit(ctx);
199
+ await flushMacrotasks();
200
+
201
+ expect(queueGenerateConversationTitleMock).toHaveBeenCalledTimes(0);
202
+ });
203
+
188
204
  test("does not block: returns before the title job is scheduled", async () => {
189
205
  // GIVEN a fresh user prompt submission
190
206
  const ctx = makeCtx();
@@ -567,13 +567,13 @@ export const ConversationMessageSchema = z.object({
567
567
  contentBlocks: z.array(ConversationContentBlockSchema).optional(),
568
568
  subagentNotification: ConversationSubagentNotificationSchema.optional(),
569
569
  acpNotification: ConversationAcpNotificationSchema.optional(),
570
- /** Set on the persisted `<background_event source="background-tool">` wake a
571
- * backgrounded bash/host_bash run posts on completion. Like the subagent/ACP
572
- * notifications, the row stays in state (the LLM reads it) but is filtered
573
- * from the rendered transcript — the inline card carries the status. */
574
- backgroundToolNotification: z.boolean().optional(),
570
+ /** Set on any persisted `<background_event source="...">` wake trigger row.
571
+ * Like the subagent/ACP notifications, the row stays in state (the LLM reads
572
+ * it) but is filtered from the rendered transcript the user-facing wake
573
+ * card carries the status. */
574
+ backgroundEventNotification: z.boolean().optional(),
575
575
  /** Structured completion of a backgrounded bash/host_bash run, stamped on the
576
- * same persisted background-event wake row as `backgroundToolNotification`.
576
+ * same persisted background-event wake row as `backgroundEventNotification`.
577
577
  * Lets the web reconstruct a terminal inline card after a daemon restart
578
578
  * (the in-memory completed ring does not survive restarts). `id` equals the
579
579
  * spawning tool call's `{backgrounded,id}` id. */
@@ -261,6 +261,13 @@ export async function runAgentLoopImpl(
261
261
  isInteractive?: boolean;
262
262
  isUserMessage?: boolean;
263
263
  titleText?: string;
264
+ /**
265
+ * True when the triggering message is a transcript-suppressed machine
266
+ * signal (`metadata.hidden`). Forwarded to the user-prompt-submit hook
267
+ * context so prompt-as-user-speech consumers (title generation) skip
268
+ * the turn.
269
+ */
270
+ isHiddenPrompt?: boolean;
264
271
  /**
265
272
  * LLM call-site identifier threaded into the per-call provider config.
266
273
  * Adapter callers (heartbeat, filing, scheduler, etc.) pass their own
@@ -903,6 +910,7 @@ export async function runAgentLoopImpl(
903
910
  userMessageId,
904
911
  requestId: reqId,
905
912
  prompt: options?.titleText ?? content,
913
+ isHiddenPrompt: options?.isHiddenPrompt === true,
906
914
  originalMessages: ctx.messages,
907
915
  latestMessages: ctx.messages,
908
916
  logger: rlog,