@vellumai/assistant 0.12.6 → 0.12.7-staging.1

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 (97) hide show
  1. package/.env.example +2 -0
  2. package/ARCHITECTURE.md +14 -14
  3. package/docs/architecture/integrations.md +5 -5
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +2 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/channels.ts +8 -4
  6. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +2 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/channels.ts +8 -4
  8. package/node_modules/@vellumai/gateway-client/src/__tests__/route-trust-class.test.ts +111 -0
  9. package/node_modules/@vellumai/gateway-client/src/admission-policy-contract.ts +5 -2
  10. package/node_modules/@vellumai/gateway-client/src/index.ts +9 -0
  11. package/node_modules/@vellumai/gateway-client/src/route-trust-class.ts +94 -0
  12. package/node_modules/@vellumai/service-contracts/src/__tests__/channels.test.ts +2 -0
  13. package/node_modules/@vellumai/service-contracts/src/channels.ts +8 -4
  14. package/openapi.yaml +153 -5
  15. package/package.json +1 -1
  16. package/src/__tests__/channel-policy.test.ts +7 -0
  17. package/src/__tests__/conversation-routes-disk-view.test.ts +141 -2
  18. package/src/__tests__/identity-routes.test.ts +1 -0
  19. package/src/__tests__/list-messages-client-message-id.test.ts +43 -0
  20. package/src/__tests__/openai-provider.test.ts +27 -0
  21. package/src/__tests__/skill-execute-input.test.ts +11 -0
  22. package/src/__tests__/tool-approval-handler.test.ts +4 -0
  23. package/src/__tests__/tool-preview-lifecycle.test.ts +26 -0
  24. package/src/agent/loop-unparseable-tool-retry.test.ts +333 -0
  25. package/src/agent/loop.ts +133 -12
  26. package/src/calls/__tests__/call-controller.test.ts +273 -1
  27. package/src/calls/__tests__/media-stream-server-integration.test.ts +40 -0
  28. package/src/calls/__tests__/media-stream-stt-session.test.ts +33 -0
  29. package/src/calls/call-constants.ts +10 -0
  30. package/src/calls/call-controller.ts +359 -25
  31. package/src/calls/media-stream-server.ts +18 -0
  32. package/src/calls/media-stream-stt-session.ts +30 -9
  33. package/src/channels/config.ts +8 -0
  34. package/src/cli/lib/bundled-marketplace.json +39 -0
  35. package/src/config/composer-preferences.test.ts +97 -0
  36. package/src/config/composer-preferences.ts +95 -0
  37. package/src/config/feature-flag-registry.json +16 -8
  38. package/src/config/schemas/llm.ts +1 -0
  39. package/src/contacts/types.ts +1 -0
  40. package/src/daemon/conversation-agent-loop-handlers.ts +18 -2
  41. package/src/daemon/conversation-agent-loop.ts +3 -0
  42. package/src/daemon/conversation-messaging.ts +4 -0
  43. package/src/daemon/conversation-process.ts +15 -0
  44. package/src/daemon/conversation-queue-manager.ts +2 -0
  45. package/src/daemon/conversation.ts +2 -0
  46. package/src/daemon/handlers/shared.ts +13 -2
  47. package/src/daemon/message-types/sync.ts +1 -0
  48. package/src/daemon/process-message.ts +5 -0
  49. package/src/i18n/__tests__/i18n.test.ts +25 -0
  50. package/src/i18n/assistant-text.ts +18 -0
  51. package/src/i18n/index.ts +1 -0
  52. package/src/i18n/messages.ts +11 -0
  53. package/src/ipc/routes/__tests__/route-adapter.test.ts +40 -0
  54. package/src/ipc/routes/route-adapter.ts +9 -0
  55. package/src/notifications/README.md +1 -1
  56. package/src/notifications/signal.ts +4 -0
  57. package/src/persistence/conversation-key-store.ts +12 -11
  58. package/src/providers/__tests__/unparseable-tool-args.test.ts +25 -0
  59. package/src/providers/inference/adapter-factory.ts +6 -0
  60. package/src/providers/ionet/client.test.ts +143 -0
  61. package/src/providers/ionet/client.ts +37 -0
  62. package/src/providers/model-catalog.ts +40 -0
  63. package/src/providers/openai/chat-completions-provider.ts +33 -2
  64. package/src/providers/retry.ts +10 -4
  65. package/src/providers/unparseable-tool-args.ts +29 -5
  66. package/src/runtime/__tests__/http-router-trust.test.ts +360 -0
  67. package/src/runtime/__tests__/shared-principal-lookup.test.ts +346 -0
  68. package/src/runtime/auth/__tests__/guard-tests.test.ts +51 -1
  69. package/src/runtime/auth/__tests__/route-policy.test.ts +85 -1
  70. package/src/runtime/auth/__tests__/scopes.test.ts +36 -0
  71. package/src/runtime/auth/route-policy.ts +44 -0
  72. package/src/runtime/auth/scopes.ts +15 -0
  73. package/src/runtime/auth/types.ts +1 -0
  74. package/src/runtime/channel-reply-delivery.ts +2 -0
  75. package/src/runtime/http-router.ts +61 -1
  76. package/src/runtime/http-types.ts +3 -0
  77. package/src/runtime/migrations/__tests__/vbundle-streaming-validator.test.ts +35 -10
  78. package/src/runtime/migrations/__tests__/vbundle-validator-v1-schema.test.ts +48 -0
  79. package/src/runtime/migrations/vbundle-builder.ts +19 -3
  80. package/src/runtime/migrations/vbundle-streaming-importer.ts +14 -19
  81. package/src/runtime/migrations/vbundle-streaming-validator.ts +14 -7
  82. package/src/runtime/migrations/vbundle-validator.ts +54 -1
  83. package/src/runtime/routes/composer-settings-routes.test.ts +71 -0
  84. package/src/runtime/routes/composer-settings-routes.ts +76 -0
  85. package/src/runtime/routes/conversation-routes.ts +54 -14
  86. package/src/runtime/routes/identity-routes.ts +2 -0
  87. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -0
  88. package/src/runtime/routes/index.ts +2 -0
  89. package/src/runtime/routes/migration-routes.ts +3 -1
  90. package/src/runtime/routes/types.ts +4 -1
  91. package/src/runtime/shared-principal-lookup.ts +235 -0
  92. package/src/tools/host-terminal/host-shell.ts +4 -2
  93. package/src/tools/shared/shell-output.test.ts +42 -0
  94. package/src/tools/shared/shell-output.ts +27 -4
  95. package/src/tools/skills/execute.ts +10 -7
  96. package/src/tools/terminal/shell.ts +2 -0
  97. package/src/tools/tool-approval-handler.ts +9 -1
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.12.6
6
+ version: 0.12.7
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
@@ -4849,6 +4849,7 @@ paths:
4849
4849
  - a2a
4850
4850
  - discord
4851
4851
  - plugin
4852
+ - vellum-shared
4852
4853
  assistantId:
4853
4854
  type: string
4854
4855
  hasPendingChallenge:
@@ -5279,6 +5280,7 @@ paths:
5279
5280
  - a2a
5280
5281
  - discord
5281
5282
  - plugin
5283
+ - vellum-shared
5282
5284
  ready:
5283
5285
  type: boolean
5284
5286
  setupStatus:
@@ -5403,6 +5405,7 @@ paths:
5403
5405
  - a2a
5404
5406
  - discord
5405
5407
  - plugin
5408
+ - vellum-shared
5406
5409
  includeRemote:
5407
5410
  description: Include remote checks (default true)
5408
5411
  type: boolean
@@ -5434,6 +5437,7 @@ paths:
5434
5437
  - a2a
5435
5438
  - discord
5436
5439
  - plugin
5440
+ - vellum-shared
5437
5441
  ready:
5438
5442
  type: boolean
5439
5443
  setupStatus:
@@ -5567,6 +5571,7 @@ paths:
5567
5571
  - a2a
5568
5572
  - discord
5569
5573
  - plugin
5574
+ - vellum-shared
5570
5575
  description: Channel to send on
5571
5576
  target:
5572
5577
  type: object
@@ -5617,6 +5622,7 @@ paths:
5617
5622
  - a2a
5618
5623
  - discord
5619
5624
  - plugin
5625
+ - vellum-shared
5620
5626
  chatId:
5621
5627
  type: string
5622
5628
  threadId:
@@ -5835,6 +5841,135 @@ paths:
5835
5841
  required:
5836
5842
  - recorded
5837
5843
  additionalProperties: false
5844
+ /v1/composer/settings:
5845
+ get:
5846
+ operationId: composer_settings_get
5847
+ summary: Get the current user's composer preferences
5848
+ tags:
5849
+ - settings
5850
+ responses:
5851
+ "200":
5852
+ description: Successful response
5853
+ content:
5854
+ application/json:
5855
+ schema:
5856
+ type: object
5857
+ properties:
5858
+ preferences:
5859
+ type: object
5860
+ properties:
5861
+ favoriteModeIds:
5862
+ default: []
5863
+ maxItems: 5
5864
+ type: array
5865
+ items:
5866
+ type: string
5867
+ minLength: 1
5868
+ maxLength: 200
5869
+ lastModeId:
5870
+ default: null
5871
+ anyOf:
5872
+ - type: string
5873
+ minLength: 1
5874
+ maxLength: 200
5875
+ - type: "null"
5876
+ lastAutonomy:
5877
+ default: null
5878
+ anyOf:
5879
+ - type: string
5880
+ enum:
5881
+ - none
5882
+ - low
5883
+ - medium
5884
+ - high
5885
+ - type: "null"
5886
+ required:
5887
+ - favoriteModeIds
5888
+ - lastModeId
5889
+ - lastAutonomy
5890
+ additionalProperties: false
5891
+ required:
5892
+ - preferences
5893
+ additionalProperties: false
5894
+ patch:
5895
+ operationId: composer_settings_patch
5896
+ summary: Update the current user's composer preferences
5897
+ tags:
5898
+ - settings
5899
+ requestBody:
5900
+ required: true
5901
+ content:
5902
+ application/json:
5903
+ schema:
5904
+ type: object
5905
+ properties:
5906
+ favoriteModeIds:
5907
+ maxItems: 5
5908
+ type: array
5909
+ items:
5910
+ type: string
5911
+ minLength: 1
5912
+ maxLength: 200
5913
+ lastModeId:
5914
+ anyOf:
5915
+ - type: string
5916
+ minLength: 1
5917
+ maxLength: 200
5918
+ - type: "null"
5919
+ lastAutonomy:
5920
+ anyOf:
5921
+ - type: string
5922
+ enum:
5923
+ - none
5924
+ - low
5925
+ - medium
5926
+ - high
5927
+ - type: "null"
5928
+ additionalProperties: false
5929
+ responses:
5930
+ "200":
5931
+ description: Successful response
5932
+ content:
5933
+ application/json:
5934
+ schema:
5935
+ type: object
5936
+ properties:
5937
+ preferences:
5938
+ type: object
5939
+ properties:
5940
+ favoriteModeIds:
5941
+ default: []
5942
+ maxItems: 5
5943
+ type: array
5944
+ items:
5945
+ type: string
5946
+ minLength: 1
5947
+ maxLength: 200
5948
+ lastModeId:
5949
+ default: null
5950
+ anyOf:
5951
+ - type: string
5952
+ minLength: 1
5953
+ maxLength: 200
5954
+ - type: "null"
5955
+ lastAutonomy:
5956
+ default: null
5957
+ anyOf:
5958
+ - type: string
5959
+ enum:
5960
+ - none
5961
+ - low
5962
+ - medium
5963
+ - high
5964
+ - type: "null"
5965
+ required:
5966
+ - favoriteModeIds
5967
+ - lastModeId
5968
+ - lastAutonomy
5969
+ additionalProperties: false
5970
+ required:
5971
+ - preferences
5972
+ additionalProperties: false
5838
5973
  /v1/config:
5839
5974
  get:
5840
5975
  operationId: config_get
@@ -6025,6 +6160,7 @@ paths:
6025
6160
  - minimax
6026
6161
  - atlascloud
6027
6162
  - baseten
6163
+ - ionet
6028
6164
  - poolside
6029
6165
  connectionName:
6030
6166
  type: string
@@ -8073,6 +8209,7 @@ paths:
8073
8209
  - a2a
8074
8210
  - discord
8075
8211
  - plugin
8212
+ - vellum-shared
8076
8213
  description:
8077
8214
  Filter by origin channel. When provided, only conversations with this exact origin_channel value are
8078
8215
  returned. Omit to include all channels.
@@ -8237,6 +8374,7 @@ paths:
8237
8374
  - a2a
8238
8375
  - discord
8239
8376
  - plugin
8377
+ - vellum-shared
8240
8378
  - type: "null"
8241
8379
  assistantAttention:
8242
8380
  type: object
@@ -8607,6 +8745,7 @@ paths:
8607
8745
  - a2a
8608
8746
  - discord
8609
8747
  - plugin
8748
+ - vellum-shared
8610
8749
  - type: "null"
8611
8750
  assistantAttention:
8612
8751
  type: object
@@ -9821,6 +9960,7 @@ paths:
9821
9960
  - a2a
9822
9961
  - discord
9823
9962
  - plugin
9963
+ - vellum-shared
9824
9964
  - type: "null"
9825
9965
  assistantAttention:
9826
9966
  type: object
@@ -14470,6 +14610,8 @@ paths:
14470
14610
  type: boolean
14471
14611
  chatsSettings:
14472
14612
  type: boolean
14613
+ composerSettings:
14614
+ type: boolean
14473
14615
  required:
14474
14616
  - memoryOptOut
14475
14617
  - retryLastTurn
@@ -14706,6 +14848,8 @@ paths:
14706
14848
  type: boolean
14707
14849
  chatsSettings:
14708
14850
  type: boolean
14851
+ composerSettings:
14852
+ type: boolean
14709
14853
  required:
14710
14854
  - memoryOptOut
14711
14855
  - retryLastTurn
@@ -16973,8 +17117,8 @@ paths:
16973
17117
  type: string
16974
17118
  description:
16975
17119
  "Filter by provider id. One of: anthropic, openai, gemini, ollama, fireworks, together, openrouter,
16976
- vercel-ai-gateway, litellm, opencode, openai-compatible, minimax, atlascloud, baseten, poolside, typesafe,
16977
- vellum"
17120
+ vercel-ai-gateway, litellm, opencode, openai-compatible, minimax, atlascloud, baseten, ionet, poolside,
17121
+ typesafe, vellum"
16978
17122
  responses:
16979
17123
  "200":
16980
17124
  description: Successful response
@@ -17246,8 +17390,8 @@ paths:
17246
17390
  type: string
17247
17391
  description:
17248
17392
  "Filter by provider. One of: anthropic, openai, gemini, ollama, fireworks, together, openrouter,
17249
- vercel-ai-gateway, litellm, opencode, openai-compatible, minimax, atlascloud, baseten, poolside, typesafe,
17250
- vellum, chatgpt"
17393
+ vercel-ai-gateway, litellm, opencode, openai-compatible, minimax, atlascloud, baseten, ionet, poolside,
17394
+ typesafe, vellum, chatgpt"
17251
17395
  responses:
17252
17396
  "200":
17253
17397
  description: Successful response
@@ -25225,6 +25369,7 @@ paths:
25225
25369
  - a2a
25226
25370
  - discord
25227
25371
  - plugin
25372
+ - vellum-shared
25228
25373
  - scheduler
25229
25374
  - watcher
25230
25375
  sourceContextId:
@@ -39425,6 +39570,7 @@ components:
39425
39570
  - minimax
39426
39571
  - atlascloud
39427
39572
  - baseten
39573
+ - ionet
39428
39574
  - poolside
39429
39575
  - typesafe
39430
39576
  - vellum
@@ -39689,6 +39835,7 @@ components:
39689
39835
  - minimax
39690
39836
  - atlascloud
39691
39837
  - baseten
39838
+ - ionet
39692
39839
  - poolside
39693
39840
  connectionName:
39694
39841
  type: string
@@ -40077,6 +40224,7 @@ components:
40077
40224
  - minimax
40078
40225
  - atlascloud
40079
40226
  - baseten
40227
+ - ionet
40080
40228
  - poolside
40081
40229
  - type: "null"
40082
40230
  connectionName:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.12.6",
3
+ "version": "0.12.7-staging.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -16,6 +16,7 @@ import {
16
16
  isNotificationDeliverable,
17
17
  } from "../channels/config.js";
18
18
  import { CHANNEL_IDS, type ChannelId } from "../channels/types.js";
19
+ import { getTransportForChannel } from "../messaging/providers/index.js";
19
20
 
20
21
  describe("channel policy registry", () => {
21
22
  // ── Exhaustiveness ────────────────────────────────────────────────────
@@ -133,6 +134,12 @@ describe("channel policy registry", () => {
133
134
  expect(getConversationStrategy("phone")).toBe("not_deliverable");
134
135
  });
135
136
 
137
+ test("vellum-shared has no transport and receives no notifications", () => {
138
+ expect(isNotificationDeliverable("vellum-shared")).toBe(false);
139
+ expect(getConversationStrategy("vellum-shared")).toBe("not_deliverable");
140
+ expect(getTransportForChannel("vellum-shared")).toBeUndefined();
141
+ });
142
+
136
143
  test("deliverable channels include vellum and telegram", () => {
137
144
  const deliverable = getDeliverableChannels();
138
145
  expect(deliverable).toContain("vellum");
@@ -1,11 +1,18 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
2
8
  import { join } from "node:path";
3
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
9
+ import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
4
10
 
5
11
  import { createAssistantMessage } from "../agent/message-types.js";
6
12
  import type { Conversation } from "../daemon/conversation.js";
7
13
  import type { EnqueueMessageOptions } from "../daemon/conversation-messaging.js";
8
14
  import { persistUserMessage } from "../daemon/conversation-messaging.js";
15
+ import * as gatewayClient from "../ipc/gateway-client.js";
9
16
  import {
10
17
  addMessage,
11
18
  findMessageIdByClientMessageId,
@@ -18,11 +25,16 @@ import {
18
25
  syncMessageToDisk,
19
26
  } from "../persistence/conversation-disk-view.js";
20
27
  import {
28
+ _resetFirstConversationSeenForTesting,
21
29
  getConversationByKey,
22
30
  getOrCreateConversation as getOrCreateConversationMapping,
23
31
  } from "../persistence/conversation-key-store.js";
24
32
  import { getDb } from "../persistence/db-connection.js";
25
33
  import { initializeDb } from "../persistence/db-init.js";
34
+ import {
35
+ conversationKeys,
36
+ conversations,
37
+ } from "../persistence/schema/index.js";
26
38
  import {
27
39
  AssistantEventHub,
28
40
  assistantEventHub,
@@ -30,6 +42,7 @@ import {
30
42
  import type { AuthContext } from "../runtime/auth/types.js";
31
43
  import * as pendingInteractions from "../runtime/pending-interactions.js";
32
44
  import { handleSendMessage } from "../runtime/routes/conversation-routes.js";
45
+ import { assertNotLiveDb } from "./assert-not-live-db.js";
33
46
  import { setOverridesForTesting } from "./feature-flag-test-helpers.js";
34
47
  import { callHandler } from "./helpers/call-route-handler.js";
35
48
  import { mockUnownedModeSessions } from "./helpers/mock-conversation.js";
@@ -541,6 +554,132 @@ describe("POST /v1/messages — body.conversationId direct id lookup", () => {
541
554
  );
542
555
  }
543
556
 
557
+ test.each(["unavailable", "rejected"])(
558
+ "an autonomy write that is %s rolls back a minted chat before retry",
559
+ async (failure) => {
560
+ _resetFirstConversationSeenForTesting();
561
+ const bootstrapFiles = ["BOOTSTRAP.md", "BOOTSTRAP-REFERENCE.md"].map(
562
+ (name) => join(testDir, name),
563
+ );
564
+ for (const path of bootstrapFiles) {
565
+ writeFileSync(path, "Onboarding instructions");
566
+ }
567
+ const ipc = spyOn(gatewayClient, "ipcCall");
568
+ if (failure === "unavailable") {
569
+ ipc.mockResolvedValueOnce(undefined);
570
+ } else {
571
+ ipc.mockRejectedValueOnce(new Error("Gateway unavailable"));
572
+ }
573
+ const body = {
574
+ content: "Start a new chat",
575
+ sourceChannel: "vellum",
576
+ interface: "macos",
577
+ riskThreshold: "none",
578
+ };
579
+ try {
580
+ if (failure === "rejected") {
581
+ await expect(sendMessage(body)).rejects.toThrow(
582
+ "Gateway unavailable",
583
+ );
584
+ } else {
585
+ const failed = await sendMessage(body);
586
+ expect(failed.status).toBe(500);
587
+ }
588
+ expect(getDb().select().from(conversations).all()).toHaveLength(0);
589
+ expect(getDb().select().from(conversationKeys).all()).toHaveLength(0);
590
+ expect(conversationInstances.size).toBe(0);
591
+ expect(bootstrapFiles.every((path) => existsSync(path))).toBe(true);
592
+
593
+ ipc.mockResolvedValueOnce({ ok: true });
594
+ const retried = await sendMessage(body);
595
+ expect(retried.status).toBe(202);
596
+ const rows = getDb().select().from(conversations).all();
597
+ expect(rows).toHaveLength(1);
598
+ expect(getDb().select().from(conversationKeys).all()).toHaveLength(1);
599
+ await waitFor(() => {
600
+ const messages = readPersistedMessages(rows[0]!.id);
601
+ return messages.length === 2 ? messages : undefined;
602
+ });
603
+ expect(bootstrapFiles.every((path) => existsSync(path))).toBe(true);
604
+ getOrCreateConversationMapping("second-successful-chat");
605
+ expect(bootstrapFiles.some((path) => existsSync(path))).toBe(false);
606
+ } finally {
607
+ ipc.mockRestore();
608
+ for (const path of bootstrapFiles) {
609
+ assertNotLiveDb(path);
610
+ rmSync(path, { force: true });
611
+ }
612
+ }
613
+ },
614
+ );
615
+
616
+ test("an autonomy write failure preserves an existing chat and its messages", async () => {
617
+ const seeded = getOrCreateConversationMapping("existing-chat");
618
+ await addMessage(seeded.conversationId, "user", "Existing message");
619
+ const ipc = spyOn(gatewayClient, "ipcCall").mockResolvedValueOnce(
620
+ undefined,
621
+ );
622
+ try {
623
+ const response = await sendMessage({
624
+ conversationId: seeded.conversationId,
625
+ content: "Continue this chat",
626
+ sourceChannel: "vellum",
627
+ interface: "macos",
628
+ riskThreshold: "none",
629
+ });
630
+ expect(response.status).toBe(500);
631
+ expect(getConversation(seeded.conversationId)).not.toBeNull();
632
+ expect(readPersistedMessages(seeded.conversationId)).toHaveLength(1);
633
+ } finally {
634
+ ipc.mockRestore();
635
+ }
636
+ });
637
+
638
+ test("a minted chat with a concurrent message survives an autonomy write failure", async () => {
639
+ const ipc = spyOn(gatewayClient, "ipcCall").mockImplementationOnce(
640
+ async () => {
641
+ const row = getDb().select().from(conversations).all()[0]!;
642
+ await addMessage(row.id, "user", "Concurrent message");
643
+ return undefined;
644
+ },
645
+ );
646
+ try {
647
+ const response = await sendMessage({
648
+ content: "Start a new chat",
649
+ sourceChannel: "vellum",
650
+ interface: "macos",
651
+ riskThreshold: "none",
652
+ });
653
+ expect(response.status).toBe(500);
654
+ const rows = getDb().select().from(conversations).all();
655
+ expect(rows).toHaveLength(1);
656
+ expect(readPersistedMessages(rows[0]!.id)).toHaveLength(1);
657
+ } finally {
658
+ ipc.mockRestore();
659
+ }
660
+ });
661
+
662
+ test("an autonomy write failure preserves a keyed chat for retry or concurrent senders", async () => {
663
+ const ipc = spyOn(gatewayClient, "ipcCall").mockResolvedValueOnce(
664
+ undefined,
665
+ );
666
+ try {
667
+ const response = await sendMessage({
668
+ conversationKey: "shared-chat-key",
669
+ content: "Send to a keyed chat",
670
+ sourceChannel: "vellum",
671
+ interface: "macos",
672
+ riskThreshold: "none",
673
+ });
674
+ expect(response.status).toBe(500);
675
+ const mapping = getConversationByKey("shared-chat-key");
676
+ expect(mapping).not.toBeNull();
677
+ expect(getConversation(mapping!.conversationId)).not.toBeNull();
678
+ } finally {
679
+ ipc.mockRestore();
680
+ }
681
+ });
682
+
544
683
  test("body.conversationId=<existing-id> scopes the send to that conversation", async () => {
545
684
  // Pre-materialise a conversation via the key path, then send a message
546
685
  // by its assistant-minted internal id.
@@ -167,6 +167,7 @@ describe("identity routes — health endpoint", () => {
167
167
  retryLastTurn: true,
168
168
  appPins: true,
169
169
  chatsSettings: true,
170
+ composerSettings: true,
170
171
  });
171
172
 
172
173
  // Profiler should either be absent or show enabled: false
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { beforeEach, describe, expect, test } from "bun:test";
10
10
 
11
+ import { MESSAGE_KEYS } from "../i18n/index.js";
11
12
  import { setConfig } from "./helpers/set-config.js";
12
13
 
13
14
  // Keep the memory system off so addMessage skips indexing side effects.
@@ -31,6 +32,7 @@ function resetTables() {
31
32
 
32
33
  interface MessagePayload {
33
34
  role: string;
35
+ textSegments?: string[];
34
36
  clientMessageId?: string;
35
37
  }
36
38
 
@@ -59,6 +61,47 @@ describe("handleListMessages clientMessageId", () => {
59
61
  expect(body.messages[0].clientMessageId).toBe("nonce-abc");
60
62
  });
61
63
 
64
+ test("localizes persisted assistant keys without rewriting legacy text", async () => {
65
+ const localizedConv = createConversation();
66
+ await addMessage(
67
+ localizedConv.id,
68
+ "assistant",
69
+ JSON.stringify([
70
+ {
71
+ type: "text",
72
+ text: MESSAGE_KEYS.AGENT_LOOP_UNPARSEABLE_TOOL_RETRY_STOP,
73
+ },
74
+ ]),
75
+ { skipIndexing: true },
76
+ );
77
+
78
+ const localizedResponse = await handleListMessages({
79
+ queryParams: { conversationId: localizedConv.id },
80
+ headers: { "accept-language": "es" },
81
+ });
82
+ const localizedBody = localizedResponse as { messages: MessagePayload[] };
83
+ expect(localizedBody.messages[0]?.textSegments).toEqual([
84
+ "Me detuve después de varias llamadas de herramienta con formato incorrecto para evitar que esta conversación creciera indefinidamente. Inténtalo de nuevo o cambia de modelo si el problema continúa.",
85
+ ]);
86
+
87
+ const legacyConv = createConversation();
88
+ const legacy =
89
+ "I stopped after repeated malformed tool calls to keep this conversation from growing indefinitely. Please try again, or switch models if the problem continues.";
90
+ await addMessage(
91
+ legacyConv.id,
92
+ "assistant",
93
+ JSON.stringify([{ type: "text", text: legacy }]),
94
+ { skipIndexing: true },
95
+ );
96
+
97
+ const legacyResponse = await handleListMessages({
98
+ queryParams: { conversationId: legacyConv.id },
99
+ headers: { "accept-language": "es" },
100
+ });
101
+ const legacyBody = legacyResponse as { messages: MessagePayload[] };
102
+ expect(legacyBody.messages[0]?.textSegments).toEqual([legacy]);
103
+ });
104
+
62
105
  test("omits clientMessageId when the row was persisted without one", async () => {
63
106
  // GIVEN a user message persisted without a client nonce (e.g. a channel send)
64
107
  const conv = createConversation();
@@ -1178,6 +1178,33 @@ describe("OpenAIProvider", () => {
1178
1178
  });
1179
1179
  });
1180
1180
 
1181
+ test("bounds malformed arguments in the diagnostic raw response only", async () => {
1182
+ const raw = '{"content":"' + "x".repeat(1_000);
1183
+ fakeChunks = [
1184
+ ...toolCallChunks([{ id: "call_large_bad", name: "test", args: raw }]),
1185
+ usageChunk(10, 5),
1186
+ ];
1187
+
1188
+ const result = await provider.sendMessage([userMsg("test")]);
1189
+
1190
+ expect(result.content[0]).toEqual({
1191
+ type: "tool_use",
1192
+ id: "call_large_bad",
1193
+ name: "test",
1194
+ input: { _raw: raw },
1195
+ });
1196
+ const rawResponse = result.rawResponse as {
1197
+ choices: Array<{
1198
+ message: {
1199
+ tool_calls: Array<{ function: { arguments: string } }>;
1200
+ };
1201
+ }>;
1202
+ };
1203
+ expect(
1204
+ rawResponse.choices[0]!.message.tool_calls[0]!.function.arguments,
1205
+ ).toBe(raw.slice(0, 200) + "…");
1206
+ });
1207
+
1181
1208
  // -----------------------------------------------------------------------
1182
1209
  // stream_options and model
1183
1210
  // -----------------------------------------------------------------------
@@ -188,6 +188,17 @@ describe("recoverSkillExecuteEnvelope", () => {
188
188
  expect(recoverSkillExecuteEnvelope(wrapped)).toBe(wrapped);
189
189
  });
190
190
 
191
+ test("keeps recovered JSON wrapped when it lacks a string tool name", () => {
192
+ for (const raw of [
193
+ JSON.stringify({ input: { content: "hi" }, activity: "x" }),
194
+ JSON.stringify({ tool: 42, input: {}, activity: "x" }),
195
+ JSON.stringify(["document_update"]),
196
+ ]) {
197
+ const wrapped = { _raw: raw };
198
+ expect(recoverSkillExecuteEnvelope(wrapped)).toBe(wrapped);
199
+ }
200
+ });
201
+
191
202
  test("passes a normal envelope through untouched", () => {
192
203
  const envelope = {
193
204
  tool: "document_update",
@@ -1357,6 +1357,10 @@ describe("ToolApprovalHandler / unparseable tool args gate", () => {
1357
1357
  }
1358
1358
  expect(result.result.content).not.toContain(raw);
1359
1359
  expect(result.result.content).toContain("…");
1360
+ expect(auditCalls.error).toHaveLength(1);
1361
+ expect(auditCalls.error[0].input).toEqual({
1362
+ _raw: raw.slice(0, 200) + "…",
1363
+ });
1360
1364
  });
1361
1365
 
1362
1366
  test("legitimate input containing a _raw field among others is not rejected", async () => {
@@ -341,6 +341,32 @@ describe("tool preview lifecycle", () => {
341
341
  expect((emitted as any).conversationId).toBe("test-session-id");
342
342
  });
343
343
 
344
+ test("caps cumulative app preview payloads and stops forwarding growth", () => {
345
+ const collector = createEventCollector();
346
+ const deps = createMockDeps({ onEvent: collector.onEvent });
347
+ const accumulatedJson = '{"code":"' + "x".repeat(300_000);
348
+
349
+ handleInputJsonDelta(state, deps, {
350
+ type: "input_json_delta",
351
+ toolName: "app_create",
352
+ toolUseId: "toolu_large_app",
353
+ accumulatedJson,
354
+ });
355
+ handleInputJsonDelta(state, deps, {
356
+ type: "input_json_delta",
357
+ toolName: "app_create",
358
+ toolUseId: "toolu_large_app",
359
+ accumulatedJson: accumulatedJson + "more",
360
+ });
361
+
362
+ expect(collector.events).toHaveLength(1);
363
+ const emitted = collector.events[0] as Extract<
364
+ AssistantEvent,
365
+ { type: "tool_input_delta" }
366
+ >;
367
+ expect(emitted.content).toBe(accumulatedJson.slice(0, 256_000));
368
+ });
369
+
344
370
  test("a streaming ui_show visual announces its surface exactly once", () => {
345
371
  const collector = createEventCollector();
346
372
  const deps = createMockDeps({ onEvent: collector.onEvent });