@vellumai/assistant 0.5.7 → 0.5.8

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 (197) hide show
  1. package/Dockerfile +2 -1
  2. package/docker-entrypoint.sh +9 -0
  3. package/docs/architecture/memory.md +13 -11
  4. package/node_modules/@vellumai/ces-contracts/src/error.ts +1 -1
  5. package/node_modules/@vellumai/ces-contracts/src/grants.ts +1 -1
  6. package/node_modules/@vellumai/ces-contracts/src/handles.ts +1 -1
  7. package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -1
  8. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +1 -1
  9. package/package.json +1 -1
  10. package/src/__tests__/approval-cascade.test.ts +0 -1
  11. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  12. package/src/__tests__/call-controller.test.ts +0 -1
  13. package/src/__tests__/ces-rpc-credential-backend.test.ts +3 -3
  14. package/src/__tests__/ces-startup-timeout.test.ts +40 -0
  15. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  16. package/src/__tests__/config-schema.test.ts +2 -0
  17. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -1
  18. package/src/__tests__/conversation-agent-loop-overflow.test.ts +0 -2
  19. package/src/__tests__/conversation-agent-loop.test.ts +2 -4
  20. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -1
  21. package/src/__tests__/conversation-error.test.ts +15 -1
  22. package/src/__tests__/conversation-messaging-secret-redirect.test.ts +1 -1
  23. package/src/__tests__/conversation-pre-run-repair.test.ts +0 -1
  24. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -1
  25. package/src/__tests__/conversation-queue.test.ts +0 -1
  26. package/src/__tests__/conversation-slash-queue.test.ts +0 -1
  27. package/src/__tests__/conversation-slash-unknown.test.ts +0 -1
  28. package/src/__tests__/conversation-workspace-injection.test.ts +0 -1
  29. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -1
  30. package/src/__tests__/credential-execution-client.test.ts +5 -2
  31. package/src/__tests__/credential-execution-feature-gates.test.ts +31 -16
  32. package/src/__tests__/credential-execution-managed-contract.test.ts +2 -2
  33. package/src/__tests__/credential-security-e2e.test.ts +1 -1
  34. package/src/__tests__/credential-security-invariants.test.ts +2 -5
  35. package/src/__tests__/credentials-cli.test.ts +4 -3
  36. package/src/__tests__/daemon-credential-client.test.ts +123 -0
  37. package/src/__tests__/deterministic-verification-control-plane.test.ts +1 -0
  38. package/src/__tests__/gateway-client-managed-outbound.test.ts +79 -1
  39. package/src/__tests__/journal-context.test.ts +335 -0
  40. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +0 -3
  41. package/src/__tests__/memory-lifecycle-e2e.test.ts +70 -25
  42. package/src/__tests__/memory-recall-quality.test.ts +48 -17
  43. package/src/__tests__/memory-regressions.test.ts +408 -363
  44. package/src/__tests__/memory-retrieval.benchmark.test.ts +0 -3
  45. package/src/__tests__/non-member-access-request.test.ts +2 -2
  46. package/src/__tests__/notification-decision-strategy.test.ts +71 -0
  47. package/src/__tests__/oauth-cli.test.ts +5 -1
  48. package/src/__tests__/provider-commit-message-generator.test.ts +0 -37
  49. package/src/__tests__/provider-error-scenarios.test.ts +0 -267
  50. package/src/__tests__/provider-streaming.benchmark.test.ts +2 -81
  51. package/src/__tests__/relay-server.test.ts +1 -2
  52. package/src/__tests__/script-proxy-injection-runtime.test.ts +1 -1
  53. package/src/__tests__/secret-onetime-send.test.ts +1 -1
  54. package/src/__tests__/secure-keys.test.ts +18 -15
  55. package/src/__tests__/skill-memory.test.ts +17 -3
  56. package/src/__tests__/stale-approval-dedup.test.ts +171 -0
  57. package/src/__tests__/stt-hints.test.ts +437 -0
  58. package/src/__tests__/task-memory-cleanup.test.ts +14 -0
  59. package/src/__tests__/twilio-routes-twiml.test.ts +139 -1
  60. package/src/__tests__/voice-quality.test.ts +58 -0
  61. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  62. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -3
  63. package/src/acp/agent-process.ts +9 -1
  64. package/src/agent/loop.ts +1 -1
  65. package/src/approvals/guardian-request-resolvers.ts +164 -38
  66. package/src/calls/__tests__/tts-text-sanitizer.test.ts +254 -0
  67. package/src/calls/call-controller.ts +9 -5
  68. package/src/calls/fish-audio-client.ts +26 -14
  69. package/src/calls/stt-hints.ts +189 -0
  70. package/src/calls/tts-text-sanitizer.ts +61 -0
  71. package/src/calls/twilio-routes.ts +32 -4
  72. package/src/calls/voice-quality.ts +15 -3
  73. package/src/calls/voice-session-bridge.ts +1 -0
  74. package/src/cli/commands/avatar.ts +2 -2
  75. package/src/cli/commands/credentials.ts +110 -94
  76. package/src/cli/commands/doctor.ts +2 -2
  77. package/src/cli/commands/keys.ts +7 -7
  78. package/src/cli/commands/memory.ts +1 -1
  79. package/src/cli/commands/oauth/connections.ts +11 -29
  80. package/src/cli/commands/oauth/platform.ts +389 -43
  81. package/src/cli/lib/daemon-credential-client.ts +284 -0
  82. package/src/cli.ts +1 -1
  83. package/src/config/bundled-skills/AGENTS.md +34 -0
  84. package/src/config/bundled-skills/acp/SKILL.md +10 -0
  85. package/src/config/bundled-skills/app-builder/SKILL.md +0 -4
  86. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -2
  87. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +1 -0
  88. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +1 -0
  89. package/src/config/bundled-skills/settings/SKILL.md +15 -2
  90. package/src/config/bundled-skills/settings/TOOLS.json +46 -1
  91. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +59 -0
  92. package/src/config/bundled-skills/settings/tools/avatar-update.ts +80 -0
  93. package/src/config/bundled-skills/slack/SKILL.md +1 -1
  94. package/src/config/bundled-tool-registry.ts +4 -0
  95. package/src/config/defaults.ts +0 -2
  96. package/src/config/env-registry.ts +4 -4
  97. package/src/config/env.ts +14 -1
  98. package/src/config/feature-flag-registry.json +1 -1
  99. package/src/config/loader.ts +8 -11
  100. package/src/config/schema.ts +5 -16
  101. package/src/config/schemas/calls.ts +17 -0
  102. package/src/config/schemas/inference.ts +2 -2
  103. package/src/config/schemas/journal.ts +16 -0
  104. package/src/config/schemas/memory-processing.ts +2 -2
  105. package/src/config/types.ts +1 -0
  106. package/src/contacts/contact-store.ts +2 -2
  107. package/src/credential-execution/executable-discovery.ts +1 -1
  108. package/src/credential-execution/startup-timeout.ts +36 -0
  109. package/src/daemon/approval-generators.ts +3 -9
  110. package/src/daemon/conversation-error.ts +13 -1
  111. package/src/daemon/conversation-memory.ts +1 -2
  112. package/src/daemon/conversation-process.ts +18 -1
  113. package/src/daemon/conversation-surfaces.ts +30 -1
  114. package/src/daemon/conversation.ts +20 -9
  115. package/src/daemon/guardian-action-generators.ts +3 -9
  116. package/src/daemon/lifecycle.ts +18 -11
  117. package/src/daemon/message-types/conversations.ts +1 -0
  118. package/src/daemon/server.ts +2 -3
  119. package/src/memory/app-store.ts +31 -0
  120. package/src/memory/db-init.ts +4 -0
  121. package/src/memory/indexer.ts +19 -10
  122. package/src/memory/items-extractor.ts +315 -322
  123. package/src/memory/job-handlers/summarization.ts +26 -16
  124. package/src/memory/jobs-store.ts +33 -1
  125. package/src/memory/journal-memory.ts +214 -0
  126. package/src/memory/migrations/193-add-source-type-columns.ts +81 -0
  127. package/src/memory/migrations/index.ts +1 -0
  128. package/src/memory/migrations/registry.ts +8 -0
  129. package/src/memory/retriever.test.ts +37 -25
  130. package/src/memory/retriever.ts +24 -49
  131. package/src/memory/schema/memory-core.ts +2 -0
  132. package/src/memory/search/formatting.ts +7 -44
  133. package/src/memory/search/staleness.ts +4 -0
  134. package/src/memory/search/tier-classifier.ts +10 -2
  135. package/src/memory/search/types.ts +2 -5
  136. package/src/memory/task-memory-cleanup.ts +4 -3
  137. package/src/notifications/adapters/slack.ts +168 -6
  138. package/src/notifications/broadcaster.ts +1 -0
  139. package/src/notifications/copy-composer.ts +59 -2
  140. package/src/notifications/signal.ts +2 -0
  141. package/src/notifications/types.ts +2 -0
  142. package/src/prompts/journal-context.ts +133 -0
  143. package/src/prompts/persona-resolver.ts +80 -24
  144. package/src/prompts/system-prompt.ts +8 -0
  145. package/src/prompts/templates/SOUL.md +10 -0
  146. package/src/providers/provider-send-message.ts +3 -32
  147. package/src/providers/registry.ts +2 -139
  148. package/src/providers/types.ts +1 -1
  149. package/src/runtime/access-request-helper.ts +4 -0
  150. package/src/runtime/auth/__tests__/guard-tests.test.ts +9 -50
  151. package/src/runtime/auth/route-policy.ts +2 -0
  152. package/src/runtime/gateway-client.ts +47 -4
  153. package/src/runtime/guardian-decision-types.ts +45 -4
  154. package/src/runtime/http-server.ts +5 -2
  155. package/src/runtime/routes/access-request-decision.ts +2 -2
  156. package/src/runtime/routes/app-management-routes.ts +2 -1
  157. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +219 -30
  158. package/src/runtime/routes/approval-strategies/guardian-text-engine-strategy.ts +37 -14
  159. package/src/runtime/routes/channel-readiness-routes.ts +9 -4
  160. package/src/runtime/routes/debug-routes.ts +12 -9
  161. package/src/runtime/routes/guardian-approval-interception.ts +168 -11
  162. package/src/runtime/routes/guardian-approval-prompt.ts +6 -1
  163. package/src/runtime/routes/guardian-approval-reply-helpers.ts +103 -21
  164. package/src/runtime/routes/identity-routes.ts +1 -1
  165. package/src/runtime/routes/inbound-message-handler.ts +31 -1
  166. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +64 -5
  167. package/src/runtime/routes/inbound-stages/background-dispatch.ts +52 -40
  168. package/src/runtime/routes/integrations/twilio.ts +52 -10
  169. package/src/runtime/routes/memory-item-routes.test.ts +3 -3
  170. package/src/runtime/routes/memory-item-routes.ts +25 -11
  171. package/src/runtime/routes/secret-routes.ts +141 -10
  172. package/src/runtime/routes/tts-routes.ts +11 -1
  173. package/src/security/ces-credential-client.ts +18 -9
  174. package/src/security/ces-rpc-credential-backend.ts +4 -3
  175. package/src/security/credential-backend.ts +10 -4
  176. package/src/security/secure-keys.ts +21 -4
  177. package/src/skills/catalog-install.ts +4 -36
  178. package/src/skills/skill-memory.ts +1 -0
  179. package/src/subagent/manager.ts +2 -5
  180. package/src/tools/acp/spawn.ts +78 -1
  181. package/src/tools/credentials/vault.ts +5 -3
  182. package/src/tools/memory/definitions.ts +3 -2
  183. package/src/tools/memory/handlers.ts +10 -7
  184. package/src/tools/terminal/safe-env.ts +1 -0
  185. package/src/util/browser.ts +15 -0
  186. package/src/util/platform.ts +1 -1
  187. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +4 -4
  188. package/src/workspace/migrations/017-seed-persona-dirs.ts +2 -1
  189. package/src/workspace/migrations/018-rekey-compound-credential-keys.ts +184 -0
  190. package/src/workspace/migrations/019-scope-journal-to-guardian.ts +103 -0
  191. package/src/workspace/migrations/migrate-to-workspace-volume.ts +4 -4
  192. package/src/workspace/migrations/registry.ts +4 -0
  193. package/src/workspace/provider-commit-message-generator.ts +12 -21
  194. package/src/__tests__/provider-fail-open-selection.test.ts +0 -271
  195. package/src/__tests__/provider-failover-actual-provider.test.ts +0 -66
  196. package/src/memory/search/lexical.ts +0 -48
  197. package/src/providers/failover.ts +0 -186
@@ -1,271 +0,0 @@
1
- import { describe, expect, mock, test } from "bun:test";
2
-
3
- import { credentialKey } from "../security/credential-key.js";
4
-
5
- // ---------------------------------------------------------------------------
6
- // Mock the underlying dependencies of managed-proxy/context.js rather than
7
- // the context module itself. This avoids global mock bleed: other test files
8
- // that import context.js will still get the real implementation with their
9
- // own dependency mocks.
10
- // ---------------------------------------------------------------------------
11
- let mockPlatformBaseUrl = "";
12
- let mockAssistantApiKey = "";
13
- let mockProviderKeys: Record<string, string> = {};
14
-
15
- const actualEnv = await import("../config/env.js");
16
- mock.module("../config/env.js", () => ({
17
- ...actualEnv,
18
- getPlatformBaseUrl: () => mockPlatformBaseUrl,
19
- }));
20
-
21
- const actualSecureKeys = await import("../security/secure-keys.js");
22
- mock.module("../security/secure-keys.js", () => ({
23
- ...actualSecureKeys,
24
- getSecureKeyAsync: async (key: string) => {
25
- if (key === credentialKey("vellum", "assistant_api_key")) {
26
- return mockAssistantApiKey || null;
27
- }
28
- return mockProviderKeys[key] ?? null;
29
- },
30
- }));
31
-
32
- import type { ProvidersConfig } from "../providers/registry.js";
33
- import {
34
- getFailoverProvider,
35
- getProviderRoutingSource,
36
- initializeProviders,
37
- listProviders,
38
- resolveProviderSelection,
39
- } from "../providers/registry.js";
40
- import { ProviderNotConfiguredError } from "../util/errors.js";
41
-
42
- /**
43
- * Tests for fail-open provider selection: when the configured primary provider
44
- * is unavailable, the system should automatically fall back to the first
45
- * available provider in the provider order.
46
- */
47
-
48
- function makeProvidersConfig(provider: string, model: string): ProvidersConfig {
49
- return {
50
- services: {
51
- inference: { mode: "your-own", provider, model },
52
- "image-generation": {
53
- mode: "your-own",
54
- provider: "gemini",
55
- model: "gemini-3.1-flash-image-preview",
56
- },
57
- "web-search": { mode: "your-own", provider: "inference-provider-native" },
58
- },
59
- };
60
- }
61
-
62
- /** Initialize registry with anthropic + openai for most tests. */
63
- async function setupTwoProviders() {
64
- mockProviderKeys = { anthropic: "test-key", openai: "test-key" };
65
- await initializeProviders(makeProvidersConfig("anthropic", "test-model"));
66
- }
67
-
68
- /** Initialize registry with no providers (empty keys, non-registerable primary). */
69
- async function setupNoProviders() {
70
- mockProviderKeys = {};
71
- await initializeProviders(makeProvidersConfig("gemini", "test-model"));
72
- }
73
-
74
- describe("resolveProviderSelection", () => {
75
- test("configured primary available → selected as primary", async () => {
76
- await setupTwoProviders();
77
- const result = resolveProviderSelection("anthropic", ["openai"]);
78
- expect(result.selectedPrimary).toBe("anthropic");
79
- expect(result.usedFallbackPrimary).toBe(false);
80
- expect(result.availableProviders).toEqual(["anthropic", "openai"]);
81
- });
82
-
83
- test("configured primary unavailable + alternate available → alternate selected", async () => {
84
- await setupTwoProviders();
85
- const result = resolveProviderSelection("gemini", ["anthropic", "openai"]);
86
- expect(result.selectedPrimary).toBe("anthropic");
87
- expect(result.usedFallbackPrimary).toBe(true);
88
- expect(result.availableProviders).toEqual(["anthropic", "openai"]);
89
- });
90
-
91
- test("configured primary unavailable + first alternate also unavailable → second alternate selected", async () => {
92
- await setupTwoProviders();
93
- const result = resolveProviderSelection("gemini", ["fireworks", "openai"]);
94
- expect(result.selectedPrimary).toBe("openai");
95
- expect(result.usedFallbackPrimary).toBe(true);
96
- expect(result.availableProviders).toEqual(["openai"]);
97
- });
98
-
99
- test("deduplicates entries in providerOrder", async () => {
100
- await setupTwoProviders();
101
- const result = resolveProviderSelection("anthropic", [
102
- "anthropic",
103
- "openai",
104
- "openai",
105
- ]);
106
- expect(result.availableProviders).toEqual(["anthropic", "openai"]);
107
- });
108
-
109
- test("unknown entries in providerOrder are filtered out", async () => {
110
- await setupTwoProviders();
111
- const result = resolveProviderSelection("anthropic", [
112
- "nonexistent",
113
- "openai",
114
- ]);
115
- expect(result.availableProviders).toEqual(["anthropic", "openai"]);
116
- });
117
-
118
- test("no available providers → null selectedPrimary", async () => {
119
- await setupTwoProviders();
120
- const result = resolveProviderSelection("gemini", ["fireworks", "ollama"]);
121
- expect(result.selectedPrimary).toBeNull();
122
- expect(result.usedFallbackPrimary).toBe(false);
123
- expect(result.availableProviders).toEqual([]);
124
- });
125
-
126
- test("empty providerOrder with available primary → primary only", async () => {
127
- await setupTwoProviders();
128
- const result = resolveProviderSelection("anthropic", []);
129
- expect(result.selectedPrimary).toBe("anthropic");
130
- expect(result.usedFallbackPrimary).toBe(false);
131
- expect(result.availableProviders).toEqual(["anthropic"]);
132
- });
133
-
134
- test("empty providerOrder with unavailable primary → null", async () => {
135
- await setupTwoProviders();
136
- const result = resolveProviderSelection("gemini", []);
137
- expect(result.selectedPrimary).toBeNull();
138
- expect(result.availableProviders).toEqual([]);
139
- });
140
- });
141
-
142
- describe("getFailoverProvider (fail-open)", () => {
143
- test("returns provider when primary is available", async () => {
144
- await setupTwoProviders();
145
- const provider = getFailoverProvider("anthropic", ["openai"]);
146
- expect(provider).toBeDefined();
147
- });
148
-
149
- test("returns provider when primary is unavailable but alternate exists", async () => {
150
- await setupTwoProviders();
151
- const provider = getFailoverProvider("gemini", ["anthropic", "openai"]);
152
- expect(provider).toBeDefined();
153
- });
154
-
155
- test("throws ProviderNotConfiguredError when no providers are available", async () => {
156
- await setupNoProviders();
157
- expect(() => getFailoverProvider("gemini", ["fireworks"])).toThrow(
158
- ProviderNotConfiguredError,
159
- );
160
- try {
161
- getFailoverProvider("gemini", ["fireworks"]);
162
- } catch (err) {
163
- expect(err).toBeInstanceOf(ProviderNotConfiguredError);
164
- const typed = err as ProviderNotConfiguredError;
165
- expect(typed.requestedProvider).toBe("gemini");
166
- expect(typed.registeredProviders).toEqual([]);
167
- expect(typed.message).toMatch(/No providers available/);
168
- }
169
- });
170
-
171
- test("single available provider returns it directly (no failover wrapper)", async () => {
172
- await setupTwoProviders();
173
- const provider = getFailoverProvider("gemini", ["anthropic"]);
174
- // Should be a RetryProvider wrapping AnthropicProvider, not a FailoverProvider
175
- expect(provider.name).not.toBe("failover");
176
- });
177
- });
178
-
179
- // -------------------------------------------------------------------------
180
- // Managed proxy fallback
181
- // -------------------------------------------------------------------------
182
-
183
- describe("managed proxy fallback", () => {
184
- function enableManagedProxy() {
185
- mockPlatformBaseUrl = "https://platform.example.com";
186
- mockAssistantApiKey = "ast-key-123";
187
- }
188
-
189
- function disableManagedProxy() {
190
- mockPlatformBaseUrl = "";
191
- mockAssistantApiKey = "";
192
- }
193
-
194
- test("anthropic and gemini are registered via managed fallback when no user key but proxy context is valid", async () => {
195
- enableManagedProxy();
196
- try {
197
- mockProviderKeys = { anthropic: "test-key" };
198
- await initializeProviders(makeProvidersConfig("anthropic", "test-model"));
199
- const registered = listProviders();
200
- expect(registered).toEqual(["anthropic", "gemini"]);
201
- } finally {
202
- disableManagedProxy();
203
- }
204
- });
205
-
206
- test("user key takes precedence and managed fallback only fills anthropic and gemini", async () => {
207
- enableManagedProxy();
208
- try {
209
- mockProviderKeys = { anthropic: "test-key", openai: "user-openai-key" };
210
- await initializeProviders(makeProvidersConfig("anthropic", "test-model"));
211
- const registered = listProviders();
212
- expect(registered).toContain("openai");
213
- expect(registered).toContain("anthropic");
214
- expect(registered).toContain("gemini");
215
- expect(registered).not.toContain("fireworks");
216
- expect(registered).not.toContain("openrouter");
217
- } finally {
218
- disableManagedProxy();
219
- }
220
- });
221
-
222
- test("managed fallback not activated when proxy context is disabled", async () => {
223
- disableManagedProxy();
224
- mockProviderKeys = { anthropic: "test-key" };
225
- await initializeProviders(makeProvidersConfig("anthropic", "test-model"));
226
- const registered = listProviders();
227
- // Anthropic is registered via user-key, not managed proxy.
228
- expect(registered).toContain("anthropic");
229
- expect(getProviderRoutingSource("anthropic")).toBe("user-key");
230
- // No other providers are registered — managed fallback is off.
231
- expect(registered).not.toContain("gemini");
232
- expect(registered).not.toContain("openai");
233
- expect(registered).not.toContain("fireworks");
234
- expect(registered).not.toContain("openrouter");
235
- });
236
-
237
- test("managed anthropic and gemini participate in failover selection", async () => {
238
- enableManagedProxy();
239
- try {
240
- mockProviderKeys = { anthropic: "test-key" };
241
- await initializeProviders(makeProvidersConfig("anthropic", "test-model"));
242
- const selection = resolveProviderSelection("anthropic", [
243
- "openai",
244
- "gemini",
245
- "fireworks",
246
- ]);
247
- expect(selection.availableProviders).toEqual(["anthropic", "gemini"]);
248
- expect(selection.selectedPrimary).toBe("anthropic");
249
- expect(selection.usedFallbackPrimary).toBe(false);
250
- } finally {
251
- disableManagedProxy();
252
- }
253
- });
254
-
255
- test("managed gemini can be selected as fallback primary when configured primary is unavailable", async () => {
256
- enableManagedProxy();
257
- try {
258
- // No anthropic key, no gemini key — only managed providers available
259
- mockProviderKeys = {};
260
- await initializeProviders(makeProvidersConfig("openai", "test-model"));
261
- const selection = resolveProviderSelection("openai", [
262
- "gemini",
263
- "anthropic",
264
- ]);
265
- expect(selection.selectedPrimary).toBe("gemini");
266
- expect(selection.usedFallbackPrimary).toBe(true);
267
- } finally {
268
- disableManagedProxy();
269
- }
270
- });
271
- });
@@ -1,66 +0,0 @@
1
- import { describe, expect, mock, test } from "bun:test";
2
-
3
- mock.module("../util/logger.js", () => ({
4
- getLogger: () =>
5
- new Proxy({} as Record<string, unknown>, { get: () => () => {} }),
6
- }));
7
-
8
- import { FailoverProvider } from "../providers/failover.js";
9
- import type {
10
- Message,
11
- Provider,
12
- ProviderResponse,
13
- } from "../providers/types.js";
14
- import { ProviderError } from "../util/errors.js";
15
-
16
- const MESSAGES: Message[] = [
17
- { role: "user", content: [{ type: "text", text: "Hello" }] },
18
- ];
19
-
20
- function successResponse(
21
- overrides?: Partial<ProviderResponse>,
22
- ): ProviderResponse {
23
- return {
24
- content: [{ type: "text", text: "ok" }],
25
- model: "test-model",
26
- usage: { inputTokens: 10, outputTokens: 5 },
27
- stopReason: "end_turn",
28
- ...overrides,
29
- };
30
- }
31
-
32
- describe("FailoverProvider actual provider propagation", () => {
33
- test("stamps the winning provider when failover uses a fallback", async () => {
34
- const primary: Provider = {
35
- name: "openrouter",
36
- async sendMessage() {
37
- throw new ProviderError("down", "openrouter", 500);
38
- },
39
- };
40
- const secondary: Provider = {
41
- name: "fireworks",
42
- async sendMessage() {
43
- return successResponse();
44
- },
45
- };
46
-
47
- const provider = new FailoverProvider([primary, secondary]);
48
- const response = await provider.sendMessage(MESSAGES);
49
-
50
- expect(response.actualProvider).toBe("fireworks");
51
- });
52
-
53
- test("preserves an inner provider's actual provider when already set", async () => {
54
- const inner: Provider = {
55
- name: "retry-wrapper",
56
- async sendMessage() {
57
- return successResponse({ actualProvider: "anthropic" });
58
- },
59
- };
60
-
61
- const provider = new FailoverProvider([inner]);
62
- const response = await provider.sendMessage(MESSAGES);
63
-
64
- expect(response.actualProvider).toBe("anthropic");
65
- });
66
- });
@@ -1,48 +0,0 @@
1
- import { and, desc, eq, inArray, notInArray } from "drizzle-orm";
2
-
3
- import { getDb } from "../db.js";
4
- import { memorySegments } from "../schema.js";
5
- import { computeRecencyScore } from "./ranking.js";
6
- import type { Candidate, CandidateType } from "./types.js";
7
-
8
- export function recencySearch(
9
- conversationId: string,
10
- limit: number,
11
- excludedMessageIds: string[] = [],
12
- scopeIds?: string[],
13
- ): Candidate[] {
14
- if (!conversationId || limit <= 0) return [];
15
- const db = getDb();
16
- const conditions = [eq(memorySegments.conversationId, conversationId)];
17
- if (excludedMessageIds.length > 0) {
18
- conditions.push(notInArray(memorySegments.messageId, excludedMessageIds));
19
- }
20
- if (scopeIds && scopeIds.length > 0) {
21
- conditions.push(inArray(memorySegments.scopeId, scopeIds));
22
- }
23
- const whereClause =
24
- conditions.length > 1 ? and(...conditions) : conditions[0];
25
- const rows = db
26
- .select()
27
- .from(memorySegments)
28
- .where(whereClause)
29
- .orderBy(desc(memorySegments.createdAt))
30
- .limit(limit)
31
- .all();
32
- return rows.map((row) => ({
33
- key: `segment:${row.id}`,
34
- type: "segment" as CandidateType,
35
- id: row.id,
36
- source: "recency",
37
- text: row.text,
38
- kind: "segment",
39
- conversationId: row.conversationId,
40
- messageId: row.messageId,
41
- confidence: 0.55,
42
- importance: 0.5,
43
- createdAt: row.createdAt,
44
- semantic: 0,
45
- recency: computeRecencyScore(row.createdAt),
46
- finalScore: 0,
47
- }));
48
- }
@@ -1,186 +0,0 @@
1
- import { ProviderError } from "../util/errors.js";
2
- import { getLogger } from "../util/logger.js";
3
- import type {
4
- Message,
5
- Provider,
6
- ProviderResponse,
7
- SendMessageOptions,
8
- ToolDefinition,
9
- } from "./types.js";
10
-
11
- const log = getLogger("failover");
12
-
13
- const DEFAULT_COOLDOWN_MS = 60_000;
14
-
15
- interface ProviderHealth {
16
- unhealthySince: number | null;
17
- }
18
-
19
- /**
20
- * Determine whether an error should trigger failover to the next provider.
21
- * Connection errors, auth errors, and 5xx server errors trigger failover.
22
- * 4xx client errors do NOT trigger failover (except 429 rate limit).
23
- */
24
- function isFailoverError(error: unknown): boolean {
25
- if (error instanceof ProviderError && error.statusCode !== undefined) {
26
- // 429 rate limit — try next provider
27
- if (error.statusCode === 429) return true;
28
- // 5xx server errors — try next provider
29
- if (error.statusCode >= 500) return true;
30
- // Other 4xx — don't failover (bad request, auth with wrong format, etc.)
31
- return false;
32
- }
33
-
34
- // Network errors — try next provider
35
- if (error instanceof Error) {
36
- const code = (error as NodeJS.ErrnoException).code;
37
- if (
38
- code === "ECONNRESET" ||
39
- code === "ECONNREFUSED" ||
40
- code === "ETIMEDOUT" ||
41
- code === "EPIPE"
42
- ) {
43
- return true;
44
- }
45
- if (error.cause instanceof Error) {
46
- const causeCode = (error.cause as NodeJS.ErrnoException).code;
47
- if (
48
- causeCode === "ECONNRESET" ||
49
- causeCode === "ECONNREFUSED" ||
50
- causeCode === "ETIMEDOUT" ||
51
- causeCode === "EPIPE"
52
- ) {
53
- return true;
54
- }
55
- }
56
- }
57
-
58
- // ProviderError without a status code = connection/unknown failure
59
- if (error instanceof ProviderError && error.statusCode === undefined) {
60
- return true;
61
- }
62
-
63
- return false;
64
- }
65
-
66
- export interface ProviderHealthStatus {
67
- name: string;
68
- healthy: boolean;
69
- unhealthySince: string | null;
70
- }
71
-
72
- export class FailoverProvider implements Provider {
73
- public readonly name: string;
74
- private readonly healthMap = new Map<string, ProviderHealth>();
75
-
76
- constructor(
77
- private readonly providers: Provider[],
78
- private readonly cooldownMs: number = DEFAULT_COOLDOWN_MS,
79
- ) {
80
- if (providers.length === 0) {
81
- throw new Error("FailoverProvider requires at least one provider");
82
- }
83
- this.name = providers[0].name;
84
- for (const p of providers) {
85
- this.healthMap.set(p.name, { unhealthySince: null });
86
- }
87
- }
88
-
89
- async sendMessage(
90
- messages: Message[],
91
- tools?: ToolDefinition[],
92
- systemPrompt?: string,
93
- options?: SendMessageOptions,
94
- ): Promise<ProviderResponse> {
95
- let lastError: unknown;
96
-
97
- for (const provider of this.providers) {
98
- const health = this.healthMap.get(provider.name)!;
99
- const now = Date.now();
100
-
101
- // Skip providers that are still in cooldown
102
- if (health.unhealthySince != null) {
103
- const elapsed = now - health.unhealthySince;
104
- if (elapsed < this.cooldownMs) {
105
- log.debug(
106
- {
107
- provider: provider.name,
108
- cooldownRemainingMs: this.cooldownMs - elapsed,
109
- },
110
- "Skipping unhealthy provider (in cooldown)",
111
- );
112
- continue;
113
- }
114
- // Cooldown expired — give it another chance
115
- log.info(
116
- { provider: provider.name },
117
- "Provider cooldown expired, retrying",
118
- );
119
- }
120
-
121
- try {
122
- const response = await provider.sendMessage(
123
- messages,
124
- tools,
125
- systemPrompt,
126
- options,
127
- );
128
- // Success — mark healthy
129
- if (health.unhealthySince != null) {
130
- log.info(
131
- { provider: provider.name },
132
- "Provider recovered, marking healthy",
133
- );
134
- health.unhealthySince = null;
135
- }
136
- return {
137
- ...response,
138
- actualProvider: response.actualProvider ?? provider.name,
139
- };
140
- } catch (error) {
141
- lastError = error;
142
-
143
- if (isFailoverError(error)) {
144
- health.unhealthySince = Date.now();
145
- log.warn(
146
- {
147
- provider: provider.name,
148
- error: error instanceof Error ? error.message : String(error),
149
- statusCode:
150
- error instanceof ProviderError ? error.statusCode : undefined,
151
- },
152
- "Provider failed, marked unhealthy",
153
- );
154
- continue;
155
- }
156
-
157
- // Non-failover error (e.g. 400 bad request) — don't try other providers
158
- throw error;
159
- }
160
- }
161
-
162
- // All providers exhausted
163
- throw (
164
- lastError ??
165
- new ProviderError(
166
- "All configured providers are unavailable",
167
- this.name,
168
- undefined,
169
- )
170
- );
171
- }
172
-
173
- getHealthStatus(): ProviderHealthStatus[] {
174
- return this.providers.map((p) => {
175
- const health = this.healthMap.get(p.name)!;
176
- return {
177
- name: p.name,
178
- healthy: health.unhealthySince == null,
179
- unhealthySince:
180
- health.unhealthySince != null
181
- ? new Date(health.unhealthySince).toISOString()
182
- : null,
183
- };
184
- });
185
- }
186
- }