@vellumai/assistant 0.8.9-staging.3 → 0.8.9-staging.5

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 (91) hide show
  1. package/openapi.yaml +16 -115
  2. package/package.json +1 -1
  3. package/src/__tests__/anthropic-provider.test.ts +23 -8
  4. package/src/__tests__/context-window-manager-compact-retry.test.ts +120 -14
  5. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
  6. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  7. package/src/__tests__/gateway-only-guard.test.ts +5 -0
  8. package/src/__tests__/llm-resolver.test.ts +73 -0
  9. package/src/__tests__/plugin-external-api.test.ts +68 -0
  10. package/src/__tests__/published-app-updater.test.ts +138 -0
  11. package/src/api/responses/memory-v3-selection-log.ts +19 -10
  12. package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
  13. package/src/cli/commands/memory-v3.ts +57 -199
  14. package/src/config/call-site-defaults.ts +0 -1
  15. package/src/config/llm-resolver.ts +39 -7
  16. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
  17. package/src/config/schemas/call-site-catalog.ts +0 -7
  18. package/src/config/schemas/llm.ts +17 -1
  19. package/src/config/schemas/memory-v3.ts +58 -8
  20. package/src/config/seed-inference-profiles.ts +18 -0
  21. package/src/daemon/conversation-agent-loop.ts +9 -17
  22. package/src/daemon/external-plugins-bootstrap.ts +8 -3
  23. package/src/daemon/tool-side-effects.ts +15 -0
  24. package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
  25. package/src/memory/v2/__tests__/page-store.test.ts +22 -0
  26. package/src/memory/v2/consolidation-job.ts +2 -72
  27. package/src/memory/v2/types.ts +5 -0
  28. package/src/plugins/defaults/compaction/window-manager.ts +56 -0
  29. package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +19 -66
  30. package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
  31. package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
  32. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +139 -129
  33. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +332 -164
  34. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +516 -293
  35. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
  36. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +51 -21
  37. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
  38. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
  39. package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
  40. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +66 -11
  41. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
  42. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +271 -110
  43. package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +0 -22
  44. package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +26 -62
  45. package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
  46. package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
  47. package/src/plugins/defaults/memory-v3-shadow/injector.ts +3 -2
  48. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +415 -181
  49. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +196 -56
  50. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +39 -2
  51. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
  52. package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +15 -7
  53. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
  54. package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
  55. package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
  56. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +75 -3
  57. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +111 -78
  58. package/src/plugins/defaults/memory-v3-shadow/types.ts +52 -19
  59. package/src/plugins/defaults/memory-v3-shadow/working-set.ts +4 -1
  60. package/src/plugins/external-api.ts +114 -0
  61. package/src/providers/anthropic/client.ts +9 -10
  62. package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
  63. package/src/providers/inference/logit-bias.ts +55 -0
  64. package/src/providers/openai/chat-completions-provider.ts +19 -1
  65. package/src/providers/retry.ts +22 -0
  66. package/src/providers/types.ts +6 -0
  67. package/src/runtime/routes/app-management-routes.ts +3 -0
  68. package/src/runtime/routes/memory-v3-routes.ts +64 -314
  69. package/src/services/published-app-updater.ts +30 -8
  70. package/src/tools/skills/load.ts +1 -1
  71. package/src/plugins/defaults/memory-v3-shadow/__tests__/assign.test.ts +0 -242
  72. package/src/plugins/defaults/memory-v3-shadow/__tests__/core.test.ts +0 -39
  73. package/src/plugins/defaults/memory-v3-shadow/__tests__/health.test.ts +0 -219
  74. package/src/plugins/defaults/memory-v3-shadow/__tests__/needle.test.ts +0 -107
  75. package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +0 -13
  76. package/src/plugins/defaults/memory-v3-shadow/__tests__/reconcile.test.ts +0 -274
  77. package/src/plugins/defaults/memory-v3-shadow/__tests__/router.test.ts +0 -337
  78. package/src/plugins/defaults/memory-v3-shadow/__tests__/selector.test.ts +0 -470
  79. package/src/plugins/defaults/memory-v3-shadow/__tests__/snapshot.test.ts +0 -168
  80. package/src/plugins/defaults/memory-v3-shadow/__tests__/tree.test.ts +0 -192
  81. package/src/plugins/defaults/memory-v3-shadow/assign.ts +0 -272
  82. package/src/plugins/defaults/memory-v3-shadow/core.ts +0 -26
  83. package/src/plugins/defaults/memory-v3-shadow/health.ts +0 -0
  84. package/src/plugins/defaults/memory-v3-shadow/needle.ts +0 -115
  85. package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +0 -26
  86. package/src/plugins/defaults/memory-v3-shadow/reconcile.ts +0 -527
  87. package/src/plugins/defaults/memory-v3-shadow/router.ts +0 -190
  88. package/src/plugins/defaults/memory-v3-shadow/selector.ts +0 -226
  89. package/src/plugins/defaults/memory-v3-shadow/snapshot.ts +0 -209
  90. package/src/plugins/defaults/memory-v3-shadow/tree.ts +0 -174
  91. package/src/util/map-limit.ts +0 -27
package/openapi.yaml CHANGED
@@ -3,7 +3,7 @@
3
3
  openapi: 3.1.0
4
4
  info:
5
5
  title: Vellum Assistant API
6
- version: 0.8.8
6
+ version: 0.8.9
7
7
  description: Auto-generated OpenAPI specification for the Vellum Assistant runtime HTTP server.
8
8
  servers:
9
9
  - url: http://127.0.0.1:7821
@@ -480,6 +480,8 @@ paths:
480
480
  type: string
481
481
  createdAt:
482
482
  type: number
483
+ updatedAt:
484
+ type: number
483
485
  version:
484
486
  type: string
485
487
  contentId:
@@ -488,6 +490,7 @@ paths:
488
490
  - id
489
491
  - name
490
492
  - createdAt
493
+ - updatedAt
491
494
  - version
492
495
  - contentId
493
496
  additionalProperties: false
@@ -15854,10 +15857,10 @@ paths:
15854
15857
  type: object
15855
15858
  properties: {}
15856
15859
  additionalProperties: false
15857
- /v1/memory/v3/health:
15860
+ /v1/memory/v3/backfill-sections:
15858
15861
  post:
15859
- operationId: memory_v3_health_post
15860
- summary: Print the v3 structural health report (read-only)
15862
+ operationId: memory_v3_backfillsections_post
15863
+ summary: "One-time: embed every page's sections (incl synthetic skill/CLI rows) into the dense store"
15861
15864
  tags:
15862
15865
  - memory
15863
15866
  responses:
@@ -15868,31 +15871,16 @@ paths:
15868
15871
  schema:
15869
15872
  type: object
15870
15873
  properties:
15871
- rendered:
15872
- type: string
15873
- counts:
15874
- type: object
15875
- properties:
15876
- unassigned:
15877
- type: number
15878
- danglingRefs:
15879
- type: number
15880
- novelClusters:
15881
- type: number
15882
- oversizedLeaves:
15883
- type: number
15884
- tinyLeaves:
15885
- type: number
15886
- required:
15887
- - unassigned
15888
- - danglingRefs
15889
- - novelClusters
15890
- - oversizedLeaves
15891
- - tinyLeaves
15892
- additionalProperties: false
15874
+ articles:
15875
+ type: number
15876
+ sections:
15877
+ type: number
15878
+ failures:
15879
+ type: number
15893
15880
  required:
15894
- - rendered
15895
- - counts
15881
+ - articles
15882
+ - sections
15883
+ - failures
15896
15884
  additionalProperties: false
15897
15885
  /v1/memory/v3/rebuild-index:
15898
15886
  post:
@@ -15914,93 +15902,6 @@ paths:
15914
15902
  required:
15915
15903
  - ok
15916
15904
  additionalProperties: false
15917
- /v1/memory/v3/reconcile:
15918
- post:
15919
- operationId: memory_v3_reconcile_post
15920
- summary: v1 convergence/prune pass over page+core refs (no rename detection without a prior snapshot)
15921
- tags:
15922
- - memory
15923
- responses:
15924
- "200":
15925
- description: Successful response
15926
- content:
15927
- application/json:
15928
- schema:
15929
- type: object
15930
- properties:
15931
- renames:
15932
- type: array
15933
- items:
15934
- type: object
15935
- properties:
15936
- id:
15937
- type: string
15938
- oldPath:
15939
- type: string
15940
- newPath:
15941
- type: string
15942
- required:
15943
- - oldPath
15944
- - newPath
15945
- additionalProperties: false
15946
- deleted:
15947
- type: array
15948
- items:
15949
- type: string
15950
- prunedCore:
15951
- type: array
15952
- items:
15953
- type: string
15954
- required:
15955
- - renames
15956
- - deleted
15957
- - prunedCore
15958
- additionalProperties: false
15959
- /v1/memory/v3/set-core:
15960
- post:
15961
- operationId: memory_v3_setcore_post
15962
- summary: Add/remove always-on core leaves (validates + previews cost)
15963
- tags:
15964
- - memory
15965
- responses:
15966
- "200":
15967
- description: Successful response
15968
- content:
15969
- application/json:
15970
- schema:
15971
- type: object
15972
- properties:
15973
- nextCore:
15974
- type: array
15975
- items:
15976
- type: string
15977
- alwaysOnPageCount:
15978
- type: number
15979
- written:
15980
- type: boolean
15981
- required:
15982
- - nextCore
15983
- - alwaysOnPageCount
15984
- - written
15985
- additionalProperties: false
15986
- requestBody:
15987
- required: true
15988
- content:
15989
- application/json:
15990
- schema:
15991
- type: object
15992
- properties:
15993
- add:
15994
- type: array
15995
- items:
15996
- type: string
15997
- remove:
15998
- type: array
15999
- items:
16000
- type: string
16001
- write:
16002
- type: boolean
16003
- additionalProperties: false
16004
15905
  /v1/messages:
16005
15906
  get:
16006
15907
  operationId: messages_get
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.8.9-staging.3",
3
+ "version": "0.8.9-staging.5",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -2,7 +2,11 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  import type { Anthropic } from "@anthropic-ai/sdk";
4
4
 
5
- import type { Message, ToolDefinition } from "../providers/types.js";
5
+ import type {
6
+ ContentBlock,
7
+ Message,
8
+ ToolDefinition,
9
+ } from "../providers/types.js";
6
10
 
7
11
  // ---------------------------------------------------------------------------
8
12
  // Mock Anthropic SDK — must be before importing the provider
@@ -102,7 +106,6 @@ mock.module("@anthropic-ai/sdk", () => ({
102
106
  }));
103
107
 
104
108
  // Import after mocking
105
- import { cachedTextBlock } from "../plugins/defaults/memory-v3-shadow/provider-blocks.js";
106
109
  import { AnthropicProvider } from "../providers/anthropic/client.js";
107
110
  import {
108
111
  isPlaceholderSentinelText,
@@ -138,6 +141,18 @@ function toolResultMsg(toolUseId: string, content: string): Message {
138
141
  };
139
142
  }
140
143
 
144
+ // A stable prefix text block carrying a 1h-TTL cache_control breakpoint, as a
145
+ // caller may stamp before the provider sees it. The internal ContentBlock type
146
+ // omits cache_control (only the provider transforms it onto the wire), so reach
147
+ // it via a cast.
148
+ function cachedPrefixBlock(text: string): ContentBlock {
149
+ return {
150
+ type: "text",
151
+ text,
152
+ cache_control: { type: "ephemeral", ttl: "1h" },
153
+ } as unknown as ContentBlock;
154
+ }
155
+
141
156
  const sampleTools: ToolDefinition[] = [
142
157
  {
143
158
  name: "file_read",
@@ -325,13 +340,13 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
325
340
  });
326
341
 
327
342
  test("preserves a caller's 1h block cache_control and sends the extended-cache beta (non-Haiku)", async () => {
328
- // v3's `cachedTextBlock` stamps a stable prefix block with a 1h TTL; the
329
- // non-Haiku path must forward it unchanged and send the beta header.
343
+ // A caller that stamps a stable prefix block with a 1h TTL: the non-Haiku
344
+ // path must forward it unchanged and send the beta header.
330
345
  await provider.sendMessage([
331
346
  {
332
347
  role: "user",
333
348
  content: [
334
- cachedTextBlock("stable pages block"),
349
+ cachedPrefixBlock("stable pages block"),
335
350
  { type: "text", text: "volatile current message" },
336
351
  ],
337
352
  },
@@ -349,7 +364,7 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
349
364
 
350
365
  test("strips ttl from a caller's block cache_control and omits the beta for Haiku", async () => {
351
366
  // Haiku doesn't support the extended-cache-ttl beta, so a caller-stamped
352
- // 1h ttl (e.g. from `cachedTextBlock`) must be stripped before sending.
367
+ // 1h ttl on a message block must be stripped before sending.
353
368
  const haiku = new AnthropicProvider(
354
369
  "sk-ant-test",
355
370
  "claude-haiku-4-5-20251001",
@@ -358,7 +373,7 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
358
373
  {
359
374
  role: "user",
360
375
  content: [
361
- cachedTextBlock("stable pages block"),
376
+ cachedPrefixBlock("stable pages block"),
362
377
  { type: "text", text: "volatile current message" },
363
378
  ],
364
379
  },
@@ -385,7 +400,7 @@ describe("AnthropicProvider — Cache-Control Characterization", () => {
385
400
  {
386
401
  role: "user",
387
402
  content: [
388
- cachedTextBlock("stable pages block"),
403
+ cachedPrefixBlock("stable pages block"),
389
404
  { type: "text", text: "volatile current message" },
390
405
  ],
391
406
  },
@@ -56,6 +56,11 @@ let runCalls: Array<{
56
56
  let runResults: CompactionRunResult[] = [];
57
57
  const estimateReturns: number[] = [];
58
58
 
59
+ // Captured arguments from each runEmergencyCompaction invocation, so the
60
+ // emergencyCompact tests can assert the manager fills in the fields it owns.
61
+ let emergencyCalls: Record<string, unknown>[] = [];
62
+ let emergencyResult: CompactionRunResult | undefined;
63
+
59
64
  mock.module("../context/token-estimator.js", () => ({
60
65
  estimatePromptTokens: (): number => {
61
66
  const next = estimateReturns.shift();
@@ -92,6 +97,15 @@ mock.module("../context/compactor.js", () => ({
92
97
  }
93
98
  return result;
94
99
  },
100
+ runEmergencyCompaction: async (
101
+ args: Record<string, unknown>,
102
+ ): Promise<CompactionRunResult> => {
103
+ emergencyCalls.push(args);
104
+ if (!emergencyResult) {
105
+ throw new Error("emergencyResult not seeded");
106
+ }
107
+ return emergencyResult;
108
+ },
95
109
  }));
96
110
 
97
111
  import { ContextWindowManager } from "../plugins/defaults/compaction/window-manager.js";
@@ -142,24 +156,28 @@ function compactResult(
142
156
  };
143
157
  }
144
158
 
159
+ function makeConfig(maxAttempts: number = 3) {
160
+ return {
161
+ enabled: true,
162
+ maxInputTokens: 200_000,
163
+ targetBudgetRatio: 0.3,
164
+ compactThreshold: 0.8,
165
+ summaryBudgetRatio: 0.05,
166
+ overflowRecovery: {
167
+ enabled: true,
168
+ safetyMarginRatio: 0.05,
169
+ maxAttempts,
170
+ interactiveLatestTurnCompression: "summarize" as const,
171
+ nonInteractiveLatestTurnCompression: "summarize" as const,
172
+ },
173
+ };
174
+ }
175
+
145
176
  function buildManager(maxAttempts: number = 3): ContextWindowManager {
146
177
  return new ContextWindowManager({
147
178
  provider: makeProvider(),
148
179
  systemPrompt: "you are a test assistant",
149
- config: {
150
- enabled: true,
151
- maxInputTokens: 200_000,
152
- targetBudgetRatio: 0.3,
153
- compactThreshold: 0.8,
154
- summaryBudgetRatio: 0.05,
155
- overflowRecovery: {
156
- enabled: true,
157
- safetyMarginRatio: 0.05,
158
- maxAttempts,
159
- interactiveLatestTurnCompression: "summarize",
160
- nonInteractiveLatestTurnCompression: "summarize",
161
- },
162
- },
180
+ config: makeConfig(maxAttempts),
163
181
  conversationId: "conv-test",
164
182
  });
165
183
  }
@@ -289,3 +307,91 @@ describe("ContextWindowManager.maybeCompact retry budget", () => {
289
307
  expect(result.estimatedInputTokens).toBe(165_000);
290
308
  });
291
309
  });
310
+
311
+ describe("ContextWindowManager.emergencyCompact", () => {
312
+ beforeEach(() => {
313
+ emergencyCalls = [];
314
+ emergencyResult = undefined;
315
+ estimateReturns.length = 0;
316
+ });
317
+
318
+ test("supplies manager-owned fields and forwards only overflow inputs", async () => {
319
+ /**
320
+ * The manager owns provider, system prompt, token budget, conversation
321
+ * id, compaction config, and non-persisted prefix count; the caller
322
+ * provides only the overflow-specific inputs.
323
+ */
324
+ // GIVEN a manager seeded with a non-persisted prefix and a canned result
325
+ emergencyResult = compactResult({
326
+ compacted: true,
327
+ compactedMessages: 40,
328
+ summaryText: "emergency summary",
329
+ });
330
+ const manager = buildManager();
331
+ manager.seedNonPersistedPrefix(3);
332
+ const messages = makeMessages(8);
333
+ const signal = new AbortController().signal;
334
+
335
+ // WHEN emergency compaction runs
336
+ const result = await manager.emergencyCompact(
337
+ messages,
338
+ { previousEstimatedInputTokens: 242_201, overrideProfile: "fast" },
339
+ signal,
340
+ );
341
+
342
+ // THEN the manager fills its owned fields and forwards the caller inputs
343
+ expect(emergencyCalls.length).toBe(1);
344
+ const args = emergencyCalls[0]!;
345
+ expect(args.conversationId).toBe("conv-test");
346
+ expect(args.systemPrompt).toBe("you are a test assistant");
347
+ expect(args.maxInputTokens).toBe(200_000);
348
+ expect(args.nonPersistedPrefixCount).toBe(3);
349
+ expect(args.tools).toBeUndefined();
350
+ expect(args.force).toBe(true);
351
+ expect(args.signal).toBe(signal);
352
+ expect(args.messages).toBe(messages);
353
+ expect(args.previousEstimatedInputTokens).toBe(242_201);
354
+ expect(args.overrideProfile).toBe("fast");
355
+ expect((args.provider as { name: string }).name).toBe("mock-provider");
356
+ expect(result.summaryText).toBe("emergency summary");
357
+ });
358
+
359
+ test("defaults overrideProfile to null when omitted", async () => {
360
+ /**
361
+ * Callers that don't resolve a per-conversation profile get an explicit
362
+ * null forwarded to the summary call.
363
+ */
364
+ // GIVEN a manager and a canned result
365
+ emergencyResult = compactResult({ compacted: true });
366
+ const manager = buildManager();
367
+
368
+ // WHEN emergency compaction runs without an override profile
369
+ await manager.emergencyCompact(makeMessages(6), {
370
+ previousEstimatedInputTokens: 210_000,
371
+ });
372
+
373
+ // THEN overrideProfile is forwarded as null
374
+ expect(emergencyCalls[0]!.overrideProfile).toBeNull();
375
+ });
376
+
377
+ test("throws when the manager has no conversation id", async () => {
378
+ /**
379
+ * Emergency compaction needs a conversation id for attachment and
380
+ * timestamp lookups; a manager without one cannot run it.
381
+ */
382
+ // GIVEN a manager constructed without a conversation id
383
+ const manager = new ContextWindowManager({
384
+ provider: makeProvider(),
385
+ systemPrompt: "you are a test assistant",
386
+ config: makeConfig(),
387
+ });
388
+
389
+ // WHEN/THEN emergency compaction rejects without invoking the compactor
390
+ await expect(
391
+ manager.emergencyCompact(makeMessages(4), {
392
+ previousEstimatedInputTokens: 210_000,
393
+ }),
394
+ ).rejects.toThrow(/conversationId/);
395
+ expect(emergencyCalls.length).toBe(0);
396
+ });
397
+ });
@@ -2,9 +2,9 @@
2
2
  * Regression tests for app surface refresh and eventing side effects in
3
3
  * createToolExecutor (conversation-tool-setup.ts).
4
4
  *
5
- * Tests verify that app_refresh, app_create, and app_delete hooks fire
6
- * correctly, and that removed hooks (app_update, app_file_edit,
7
- * app_file_write) no longer trigger side effects.
5
+ * Tests verify that app_refresh, app_update, app_create, and app_delete hooks
6
+ * fire correctly, and that non-hooked tools (app_file_edit, app_file_write) do
7
+ * not trigger side effects.
8
8
  *
9
9
  * File-change detection for file_write/file_edit is handled by
10
10
  * AppSourceWatcher (see app-source-watcher.test.ts).
@@ -248,6 +248,76 @@ describe("session-tool-setup app refresh side effects", () => {
248
248
  });
249
249
  });
250
250
 
251
+ // ── app_update ──────────────────────────────────────────────────────
252
+
253
+ describe("app_update", () => {
254
+ test("triggers refreshSurfacesForApp and broadcast on success", async () => {
255
+ const ctx = makeCtx();
256
+ const executor = makeFakeExecutor({
257
+ content: '{"updated":true,"appId":"app-7"}',
258
+ isError: false,
259
+ });
260
+
261
+ const toolFn = createToolExecutor(
262
+ executor as unknown as ToolExecutor,
263
+ noopPrompter,
264
+ noopSecretPrompter,
265
+ ctx,
266
+ noopLifecycleHandler,
267
+ );
268
+
269
+ await toolFn("app_update", { app_id: "app-7" });
270
+
271
+ expect(refreshSpy).toHaveBeenCalledTimes(1);
272
+ expect((refreshSpy.mock.calls as unknown[][])[0][1]).toBe("app-7");
273
+ expectAppChangeBroadcast("app-7");
274
+ expect(updatePublishedSpy).toHaveBeenCalledTimes(1);
275
+ expect((updatePublishedSpy.mock.calls as unknown[][])[0][0]).toBe(
276
+ "app-7",
277
+ );
278
+ });
279
+
280
+ test("skips side effects when result is an error", async () => {
281
+ const ctx = makeCtx();
282
+ const executor = makeFakeExecutor({
283
+ content: "Error: not found",
284
+ isError: true,
285
+ });
286
+
287
+ const toolFn = createToolExecutor(
288
+ executor as unknown as ToolExecutor,
289
+ noopPrompter,
290
+ noopSecretPrompter,
291
+ ctx,
292
+ noopLifecycleHandler,
293
+ );
294
+
295
+ await toolFn("app_update", { app_id: "app-err" });
296
+
297
+ expect(refreshSpy).not.toHaveBeenCalled();
298
+ expect(broadcastSpy).not.toHaveBeenCalled();
299
+ expect(updatePublishedSpy).not.toHaveBeenCalled();
300
+ });
301
+
302
+ test("skips side effects when app_id is missing", async () => {
303
+ const ctx = makeCtx();
304
+ const executor = makeFakeExecutor({ content: "{}", isError: false });
305
+
306
+ const toolFn = createToolExecutor(
307
+ executor as unknown as ToolExecutor,
308
+ noopPrompter,
309
+ noopSecretPrompter,
310
+ ctx,
311
+ noopLifecycleHandler,
312
+ );
313
+
314
+ await toolFn("app_update", {});
315
+
316
+ expect(refreshSpy).not.toHaveBeenCalled();
317
+ expect(broadcastSpy).not.toHaveBeenCalled();
318
+ });
319
+ });
320
+
251
321
  // ── app_create side effects ─────────────────────────────────────────
252
322
 
253
323
  describe("app_create side effects", () => {
@@ -518,7 +588,6 @@ describe("session-tool-setup app refresh side effects", () => {
518
588
  "write_file",
519
589
  "shell",
520
590
  "app_list",
521
- "app_update",
522
591
  "app_file_edit",
523
592
  "app_file_write",
524
593
  ]) {
@@ -230,6 +230,7 @@ describe("Invariant 2: no generic plaintext secret read API", () => {
230
230
  "runtime/routes/platform-routes.ts", // CLI platform connect/disconnect/status routes (CLI-migrated to IPC)
231
231
  "ipc/skill-routes/providers.ts", // host.providers.secureKeys.getProviderKey IPC route (out-of-process SkillHost companion)
232
232
  "daemon/external-plugins-bootstrap.ts", // reads credentials at plugin init (manifest.requiresCredential) via the CES-mediated getSecureKeyAsync path
233
+ "plugins/external-api.ts", // globalThis runtime bridge that exposes getSecureKeyAsync to dynamically-imported workspace plugins (compiled-binary plugin loading)
233
234
  "inbound/platform-callback-registration.ts", // managed credential lookup for platform base URL, assistant ID, and API key
234
235
  "tts/providers/elevenlabs-provider.ts", // ElevenLabs TTS API key lookup
235
236
  "tts/providers/deepgram-provider.ts", // Deepgram TTS API key lookup
@@ -88,6 +88,11 @@ function isGatewayInternal(filePath: string): boolean {
88
88
  /** Additional files allowed for the interpolated-port check only (use gateway port, not runtime). */
89
89
  const INTERPOLATED_PORT_ALLOWLIST = new Set([
90
90
  "apps/macos/src/main/bundle-flow.ts",
91
+ // Electron main bridges the host proxy to a local assistant's gateway
92
+ // (gatewayPort) over loopback with a Guardian-minted gateway token — same
93
+ // class as bundle-flow.ts. Added with #34049 (host proxy for cloud/managed
94
+ // assistants); the allowlist entry was missed there.
95
+ "apps/macos/src/main/host-proxy-router.ts",
91
96
  ]);
92
97
 
93
98
  /** Shared violation filter: exempt test files, gateway internals, and allowlisted paths. */
@@ -1249,3 +1249,76 @@ describe("resolveDefaultProfileKey", () => {
1249
1249
  expect(resolveDefaultProfileKey("mainAgent", llm)).toBe("balanced");
1250
1250
  });
1251
1251
  });
1252
+
1253
+ describe("resolveCallSiteConfig logitBias provenance", () => {
1254
+ const kimi = "accounts/fireworks/models/kimi-k2p6";
1255
+
1256
+ test("forwards logitBias from the active profile that opted in", () => {
1257
+ const llm = LLMSchema.parse({
1258
+ default: fullDefault,
1259
+ profiles: {
1260
+ "balanced-economy": {
1261
+ provider: "fireworks",
1262
+ model: kimi,
1263
+ logitBias: "suppress-cjk",
1264
+ },
1265
+ },
1266
+ activeProfile: "balanced-economy",
1267
+ });
1268
+ expect(resolveCallSiteConfig("mainAgent", llm).logitBias).toBe(
1269
+ "suppress-cjk",
1270
+ );
1271
+ });
1272
+
1273
+ test("a higher-precedence override profile that omits logitBias clears it", () => {
1274
+ // Active profile opts in, but a pinned (override) Kimi profile did not —
1275
+ // the override must not inherit suppress-cjk just because it resolves to
1276
+ // Fireworks.
1277
+ const llm = LLMSchema.parse({
1278
+ default: fullDefault,
1279
+ profiles: {
1280
+ "balanced-economy": {
1281
+ provider: "fireworks",
1282
+ model: kimi,
1283
+ logitBias: "suppress-cjk",
1284
+ },
1285
+ "my-kimi": { provider: "fireworks", model: kimi },
1286
+ },
1287
+ activeProfile: "balanced-economy",
1288
+ });
1289
+ const resolved = resolveCallSiteConfig("mainAgent", llm, {
1290
+ overrideProfile: "my-kimi",
1291
+ });
1292
+ expect(resolved.logitBias).toBeUndefined();
1293
+ });
1294
+
1295
+ test("does not leak the active profile's logitBias into a call site's own profile", () => {
1296
+ // For non-main call sites the call-site profile wins; since it didn't opt
1297
+ // in, the active profile's preset must not bleed through.
1298
+ const llm = LLMSchema.parse({
1299
+ default: fullDefault,
1300
+ profiles: {
1301
+ "balanced-economy": {
1302
+ provider: "fireworks",
1303
+ model: kimi,
1304
+ logitBias: "suppress-cjk",
1305
+ },
1306
+ plain: { provider: "anthropic", model: "claude-opus-4-7" },
1307
+ },
1308
+ activeProfile: "balanced-economy",
1309
+ callSites: { memoryExtraction: { profile: "plain" } },
1310
+ });
1311
+ expect(
1312
+ resolveCallSiteConfig("memoryExtraction", llm).logitBias,
1313
+ ).toBeUndefined();
1314
+ });
1315
+
1316
+ test("a logitBias on a non-profile layer (llm.default) does not apply when the winning profile omits it", () => {
1317
+ const llm = LLMSchema.parse({
1318
+ default: { ...fullDefault, logitBias: "suppress-cjk" },
1319
+ profiles: { plain: { provider: "anthropic", model: "claude-opus-4-7" } },
1320
+ activeProfile: "plain",
1321
+ });
1322
+ expect(resolveCallSiteConfig("mainAgent", llm).logitBias).toBeUndefined();
1323
+ });
1324
+ });