@vellumai/assistant 0.5.6 → 0.5.7

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 (305) hide show
  1. package/.env.example +16 -2
  2. package/ARCHITECTURE.md +6 -75
  3. package/Dockerfile +1 -1
  4. package/README.md +0 -2
  5. package/bun.lock +0 -414
  6. package/docs/architecture/keychain-broker.md +45 -240
  7. package/docs/architecture/security.md +0 -17
  8. package/docs/credential-execution-service.md +2 -2
  9. package/node_modules/@vellumai/ces-contracts/package.json +1 -0
  10. package/node_modules/@vellumai/ces-contracts/src/rpc.ts +119 -0
  11. package/node_modules/@vellumai/credential-storage/package.json +1 -0
  12. package/node_modules/@vellumai/egress-proxy/package.json +1 -0
  13. package/package.json +2 -3
  14. package/src/__tests__/actor-token-service.test.ts +0 -114
  15. package/src/__tests__/assistant-feature-flags-integration.test.ts +30 -29
  16. package/src/__tests__/browser-skill-endstate.test.ts +6 -5
  17. package/src/__tests__/btw-routes.test.ts +0 -39
  18. package/src/__tests__/call-domain.test.ts +0 -128
  19. package/src/__tests__/ces-rpc-credential-backend.test.ts +199 -0
  20. package/src/__tests__/channel-approval-routes.test.ts +0 -5
  21. package/src/__tests__/channel-readiness-service.test.ts +1 -60
  22. package/src/__tests__/checker.test.ts +4 -2
  23. package/src/__tests__/cli-command-risk-guard.test.ts +112 -0
  24. package/src/__tests__/config-schema-cmd.test.ts +0 -1
  25. package/src/__tests__/config-schema.test.ts +1 -1
  26. package/src/__tests__/conversation-attention-telegram.test.ts +0 -5
  27. package/src/__tests__/conversation-init.benchmark.test.ts +0 -2
  28. package/src/__tests__/conversation-skill-tools.test.ts +0 -54
  29. package/src/__tests__/conversation-title-service.test.ts +87 -0
  30. package/src/__tests__/credential-execution-feature-gates.test.ts +28 -14
  31. package/src/__tests__/credential-execution-managed-contract.test.ts +33 -18
  32. package/src/__tests__/credential-security-e2e.test.ts +0 -66
  33. package/src/__tests__/credential-security-invariants.test.ts +4 -45
  34. package/src/__tests__/credentials-cli.test.ts +78 -0
  35. package/src/__tests__/db-migration-rollback.test.ts +2015 -1
  36. package/src/__tests__/docker-signing-key-bootstrap.test.ts +34 -143
  37. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -4
  38. package/src/__tests__/guardian-routing-state.test.ts +0 -5
  39. package/src/__tests__/host-shell-tool.test.ts +6 -7
  40. package/src/__tests__/http-user-message-parity.test.ts +3 -103
  41. package/src/__tests__/inbound-invite-redemption.test.ts +0 -4
  42. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -8
  43. package/src/__tests__/intent-routing.test.ts +0 -13
  44. package/src/__tests__/jobs-store-qdrant-breaker.test.ts +178 -0
  45. package/src/__tests__/keychain-broker-client.test.ts +161 -22
  46. package/src/__tests__/memory-jobs-worker-backoff.test.ts +150 -0
  47. package/src/__tests__/migration-export-http.test.ts +2 -2
  48. package/src/__tests__/migration-import-commit-http.test.ts +2 -2
  49. package/src/__tests__/migration-import-preflight-http.test.ts +2 -2
  50. package/src/__tests__/migration-validate-http.test.ts +2 -2
  51. package/src/__tests__/non-member-access-request.test.ts +0 -5
  52. package/src/__tests__/notification-decision-fallback.test.ts +4 -0
  53. package/src/__tests__/notification-decision-identity.test.ts +4 -0
  54. package/src/__tests__/permission-types.test.ts +1 -0
  55. package/src/__tests__/provider-managed-proxy-integration.test.ts +5 -6
  56. package/src/__tests__/qdrant-manager.test.ts +28 -2
  57. package/src/__tests__/registry.test.ts +0 -6
  58. package/src/__tests__/runtime-attachment-metadata.test.ts +0 -4
  59. package/src/__tests__/secret-routes-managed-proxy.test.ts +0 -4
  60. package/src/__tests__/secure-keys.test.ts +83 -263
  61. package/src/__tests__/shell-identity.test.ts +96 -6
  62. package/src/__tests__/skill-feature-flags-integration.test.ts +22 -14
  63. package/src/__tests__/skill-feature-flags.test.ts +46 -45
  64. package/src/__tests__/skill-load-feature-flag.test.ts +7 -10
  65. package/src/__tests__/skill-load-inline-command.test.ts +8 -12
  66. package/src/__tests__/skill-load-inline-includes.test.ts +6 -10
  67. package/src/__tests__/skill-load-tool.test.ts +0 -2
  68. package/src/__tests__/skill-projection-feature-flag.test.ts +33 -29
  69. package/src/__tests__/skills.test.ts +0 -2
  70. package/src/__tests__/slack-inbound-verification.test.ts +0 -4
  71. package/src/__tests__/suggestion-routes.test.ts +1 -32
  72. package/src/__tests__/system-prompt.test.ts +0 -1
  73. package/src/__tests__/tool-executor-shell-integration.test.ts +5 -3
  74. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -5
  75. package/src/__tests__/trusted-contact-multichannel.test.ts +0 -4
  76. package/src/__tests__/update-bulletin.test.ts +0 -2
  77. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +6 -9
  78. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -6
  79. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +252 -0
  80. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +218 -0
  81. package/src/__tests__/workspace-migration-down-functions.test.ts +1009 -0
  82. package/src/__tests__/workspace-migrations-runner.test.ts +114 -0
  83. package/src/calls/audio-store.test.ts +97 -0
  84. package/src/calls/audio-store.ts +205 -0
  85. package/src/calls/call-controller.ts +85 -7
  86. package/src/calls/call-domain.ts +3 -0
  87. package/src/calls/call-store.ts +10 -3
  88. package/src/calls/fish-audio-client.ts +117 -0
  89. package/src/calls/relay-server.ts +27 -0
  90. package/src/calls/twilio-routes.ts +2 -1
  91. package/src/calls/types.ts +1 -0
  92. package/src/calls/voice-ingress-preflight.ts +0 -42
  93. package/src/calls/voice-quality.ts +26 -5
  94. package/src/calls/voice-session-bridge.ts +6 -12
  95. package/src/cli/commands/config.ts +1 -4
  96. package/src/cli/commands/credentials.ts +34 -4
  97. package/src/cli/commands/oauth/index.ts +7 -0
  98. package/src/cli/commands/oauth/platform.ts +179 -0
  99. package/src/cli/commands/platform.ts +3 -3
  100. package/src/config/assistant-feature-flags.ts +186 -5
  101. package/src/config/bundled-skills/messaging/SKILL.md +5 -5
  102. package/src/config/bundled-skills/phone-calls/TOOLS.json +4 -0
  103. package/src/config/bundled-skills/settings/TOOLS.json +2 -2
  104. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +42 -0
  105. package/src/config/bundled-tool-registry.ts +1 -11
  106. package/src/config/env-registry.ts +1 -1
  107. package/src/config/env.ts +8 -14
  108. package/src/config/feature-flag-registry.json +48 -8
  109. package/src/config/loader.ts +98 -31
  110. package/src/config/schema.ts +4 -13
  111. package/src/config/schemas/calls.ts +13 -0
  112. package/src/config/schemas/fish-audio.ts +39 -0
  113. package/src/config/schemas/security.ts +0 -4
  114. package/src/config/types.ts +0 -1
  115. package/src/contacts/contact-store.ts +39 -0
  116. package/src/contacts/types.ts +2 -0
  117. package/src/credential-execution/approval-bridge.ts +1 -0
  118. package/src/credential-execution/executable-discovery.ts +28 -4
  119. package/src/credential-execution/feature-gates.ts +16 -0
  120. package/src/credential-execution/process-manager.ts +38 -0
  121. package/src/daemon/assistant-attachments.ts +9 -0
  122. package/src/daemon/config-watcher.ts +5 -0
  123. package/src/daemon/conversation-tool-setup.ts +0 -105
  124. package/src/daemon/conversation.ts +10 -1
  125. package/src/daemon/handlers/config-vercel.ts +92 -0
  126. package/src/daemon/handlers/skills.ts +2 -15
  127. package/src/daemon/install-symlink.ts +195 -0
  128. package/src/daemon/lifecycle.ts +227 -51
  129. package/src/daemon/message-types/conversations.ts +3 -4
  130. package/src/daemon/message-types/diagnostics.ts +3 -22
  131. package/src/daemon/message-types/messages.ts +0 -2
  132. package/src/daemon/message-types/upgrades.ts +8 -0
  133. package/src/daemon/server.ts +30 -92
  134. package/src/events/domain-events.ts +2 -1
  135. package/src/inbound/platform-callback-registration.ts +3 -3
  136. package/src/instrument.ts +8 -5
  137. package/src/memory/conversation-title-service.ts +50 -1
  138. package/src/memory/db-init.ts +12 -0
  139. package/src/memory/items-extractor.ts +15 -1
  140. package/src/memory/job-handlers/conversation-starters.ts +4 -1
  141. package/src/memory/jobs-store.ts +30 -5
  142. package/src/memory/jobs-worker.ts +31 -7
  143. package/src/memory/migrations/001-job-deferrals.ts +19 -0
  144. package/src/memory/migrations/004-entity-relation-dedup.ts +10 -0
  145. package/src/memory/migrations/005-fingerprint-scope-unique.ts +76 -0
  146. package/src/memory/migrations/006-scope-salted-fingerprints.ts +50 -0
  147. package/src/memory/migrations/007-assistant-id-to-self.ts +10 -0
  148. package/src/memory/migrations/008-remove-assistant-id-columns.ts +34 -0
  149. package/src/memory/migrations/009-llm-usage-events-drop-assistant-id.ts +26 -0
  150. package/src/memory/migrations/014-backfill-inbox-thread-state.ts +10 -0
  151. package/src/memory/migrations/015-drop-active-search-index.ts +17 -0
  152. package/src/memory/migrations/019-notification-tables-schema-migration.ts +12 -0
  153. package/src/memory/migrations/020-rename-macos-ios-channel-to-vellum.ts +121 -0
  154. package/src/memory/migrations/024-embedding-vector-blob.ts +74 -0
  155. package/src/memory/migrations/026a-embeddings-nullable-vector-json.ts +82 -0
  156. package/src/memory/migrations/036-normalize-phone-identities.ts +11 -0
  157. package/src/memory/migrations/116-messages-fts.ts +106 -1
  158. package/src/memory/migrations/126-backfill-guardian-principal-id.ts +52 -0
  159. package/src/memory/migrations/127-guardian-principal-id-not-null.ts +77 -0
  160. package/src/memory/migrations/134-contacts-notes-column.ts +13 -0
  161. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +20 -0
  162. package/src/memory/migrations/136-drop-assistant-id-columns.ts +52 -0
  163. package/src/memory/migrations/140-backfill-usage-cache-accounting.ts +13 -0
  164. package/src/memory/migrations/141-rename-verification-table.ts +54 -0
  165. package/src/memory/migrations/142-rename-verification-session-id-column.ts +25 -0
  166. package/src/memory/migrations/143-rename-guardian-verification-values.ts +35 -0
  167. package/src/memory/migrations/144-rename-voice-to-phone.ts +136 -0
  168. package/src/memory/migrations/145-drop-accounts-table.ts +32 -0
  169. package/src/memory/migrations/147-migrate-reminders-to-schedules.ts +14 -1
  170. package/src/memory/migrations/148-drop-reminders-table.ts +35 -1
  171. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +69 -1
  172. package/src/memory/migrations/162-guardian-timestamps-epoch-ms.ts +290 -0
  173. package/src/memory/migrations/169-rename-gmail-provider-key-to-google.ts +51 -1
  174. package/src/memory/migrations/174-rename-thread-starters-table.ts +47 -1
  175. package/src/memory/migrations/176-drop-capability-card-state.ts +13 -0
  176. package/src/memory/migrations/180-backfill-inline-attachments-to-disk.ts +16 -0
  177. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +28 -1
  178. package/src/memory/migrations/190-call-session-skip-disclosure.ts +15 -0
  179. package/src/memory/migrations/191-backfill-audio-attachment-mime-types.ts +64 -0
  180. package/src/memory/migrations/192-contacts-user-file-column.ts +15 -0
  181. package/src/memory/migrations/index.ts +4 -0
  182. package/src/memory/migrations/registry.ts +90 -0
  183. package/src/memory/migrations/validate-migration-state.ts +137 -11
  184. package/src/memory/qdrant-circuit-breaker.ts +9 -0
  185. package/src/memory/qdrant-manager.ts +64 -7
  186. package/src/memory/schema/calls.ts +1 -0
  187. package/src/memory/schema/contacts.ts +1 -0
  188. package/src/notifications/decision-engine.ts +4 -1
  189. package/src/oauth/connection-resolver.ts +6 -4
  190. package/src/permissions/checker.ts +0 -38
  191. package/src/permissions/shell-identity.ts +76 -22
  192. package/src/permissions/types.ts +4 -2
  193. package/src/platform/client.ts +35 -7
  194. package/src/prompts/persona-resolver.ts +138 -0
  195. package/src/prompts/system-prompt.ts +36 -4
  196. package/src/prompts/templates/users/default.md +1 -0
  197. package/src/providers/registry.ts +27 -40
  198. package/src/runtime/auth/__tests__/credential-service.test.ts +0 -1
  199. package/src/runtime/auth/__tests__/external-assistant-id.test.ts +13 -68
  200. package/src/runtime/auth/external-assistant-id.ts +13 -59
  201. package/src/runtime/auth/route-policy.ts +15 -1
  202. package/src/runtime/auth/token-service.ts +43 -138
  203. package/src/runtime/channel-readiness-service.ts +1 -16
  204. package/src/runtime/http-server.ts +27 -2
  205. package/src/runtime/middleware/error-handler.ts +1 -9
  206. package/src/runtime/routes/audio-routes.ts +40 -0
  207. package/src/runtime/routes/btw-routes.ts +0 -17
  208. package/src/runtime/routes/conversation-query-routes.ts +63 -1
  209. package/src/runtime/routes/conversation-routes.ts +4 -44
  210. package/src/runtime/routes/diagnostics-routes.ts +1 -477
  211. package/src/runtime/routes/identity-routes.ts +18 -29
  212. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +4 -33
  213. package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +1 -1
  214. package/src/runtime/routes/integrations/vercel.ts +89 -0
  215. package/src/runtime/routes/log-export-routes.ts +5 -0
  216. package/src/runtime/routes/memory-item-routes.ts +24 -6
  217. package/src/runtime/routes/migration-rollback-routes.ts +209 -0
  218. package/src/runtime/routes/migration-routes.ts +17 -1
  219. package/src/runtime/routes/notification-routes.ts +58 -0
  220. package/src/runtime/routes/schedule-routes.ts +65 -0
  221. package/src/runtime/routes/settings-routes.ts +41 -1
  222. package/src/runtime/routes/tts-routes.ts +86 -0
  223. package/src/runtime/routes/upgrade-broadcast-routes.ts +26 -2
  224. package/src/runtime/routes/workspace-commit-routes.ts +62 -0
  225. package/src/runtime/routes/workspace-routes.test.ts +22 -1
  226. package/src/runtime/routes/workspace-routes.ts +1 -1
  227. package/src/runtime/routes/workspace-utils.ts +86 -2
  228. package/src/security/ces-credential-client.ts +59 -22
  229. package/src/security/ces-rpc-credential-backend.ts +85 -0
  230. package/src/security/credential-backend.ts +12 -88
  231. package/src/security/keychain-broker-client.ts +10 -2
  232. package/src/security/secure-keys.ts +94 -113
  233. package/src/skills/catalog-install.ts +13 -7
  234. package/src/telemetry/usage-telemetry-reporter.ts +4 -2
  235. package/src/tools/calls/call-start.ts +1 -0
  236. package/src/tools/executor.ts +0 -4
  237. package/src/tools/network/script-proxy/session-manager.ts +19 -4
  238. package/src/tools/network/web-fetch.ts +3 -1
  239. package/src/tools/skills/execute.ts +1 -1
  240. package/src/tools/types.ts +0 -8
  241. package/src/util/errors.ts +0 -12
  242. package/src/util/platform.ts +3 -50
  243. package/src/workspace/git-service.ts +5 -2
  244. package/src/workspace/migrations/001-avatar-rename.ts +15 -0
  245. package/src/workspace/migrations/003-seed-device-id.ts +17 -1
  246. package/src/workspace/migrations/004-extract-collect-usage-data.ts +33 -0
  247. package/src/workspace/migrations/005-add-send-diagnostics.ts +3 -0
  248. package/src/workspace/migrations/006-services-config.ts +49 -0
  249. package/src/workspace/migrations/007-web-search-provider-rename.ts +27 -0
  250. package/src/workspace/migrations/008-voice-timeout-and-max-steps.ts +3 -0
  251. package/src/workspace/migrations/009-backfill-conversation-disk-view.ts +4 -0
  252. package/src/workspace/migrations/010-app-dir-rename.ts +78 -0
  253. package/src/workspace/migrations/011-backfill-installation-id.ts +11 -0
  254. package/src/workspace/migrations/012-rename-conversation-disk-view-dirs.ts +44 -0
  255. package/src/workspace/migrations/013-repair-conversation-disk-view.ts +5 -0
  256. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +153 -0
  257. package/src/workspace/migrations/016-extract-feature-flags-to-protected.ts +156 -0
  258. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +150 -0
  259. package/src/workspace/migrations/017-seed-persona-dirs.ts +95 -0
  260. package/src/workspace/migrations/migrate-to-workspace-volume.ts +23 -1
  261. package/src/workspace/migrations/registry.ts +8 -0
  262. package/src/workspace/migrations/runner.ts +106 -2
  263. package/src/workspace/migrations/types.ts +4 -0
  264. package/src/__tests__/claude-code-skill-regression.test.ts +0 -206
  265. package/src/__tests__/claude-code-tool-profiles.test.ts +0 -99
  266. package/src/__tests__/diagnostics-export.test.ts +0 -288
  267. package/src/__tests__/local-gateway-health.test.ts +0 -209
  268. package/src/__tests__/secret-ingress-handler.test.ts +0 -120
  269. package/src/__tests__/swarm-conversation-integration.test.ts +0 -358
  270. package/src/__tests__/swarm-dag-pathological.test.ts +0 -547
  271. package/src/__tests__/swarm-orchestrator.test.ts +0 -463
  272. package/src/__tests__/swarm-plan-validator.test.ts +0 -384
  273. package/src/__tests__/swarm-recursion.test.ts +0 -197
  274. package/src/__tests__/swarm-router-planner.test.ts +0 -234
  275. package/src/__tests__/swarm-tool.test.ts +0 -185
  276. package/src/__tests__/swarm-worker-backend.test.ts +0 -144
  277. package/src/__tests__/swarm-worker-runner.test.ts +0 -288
  278. package/src/commands/__tests__/cc-command-registry.test.ts +0 -396
  279. package/src/commands/cc-command-registry.ts +0 -248
  280. package/src/config/bundled-skills/claude-code/SKILL.md +0 -53
  281. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -47
  282. package/src/config/bundled-skills/claude-code/tools/claude-code.ts +0 -12
  283. package/src/config/bundled-skills/orchestration/SKILL.md +0 -33
  284. package/src/config/bundled-skills/orchestration/TOOLS.json +0 -35
  285. package/src/config/bundled-skills/orchestration/tools/swarm-delegate.ts +0 -12
  286. package/src/config/schemas/swarm.ts +0 -82
  287. package/src/logfire.ts +0 -135
  288. package/src/runtime/local-gateway-health.ts +0 -275
  289. package/src/security/secret-ingress.ts +0 -68
  290. package/src/swarm/backend-claude-code.ts +0 -225
  291. package/src/swarm/checkpoint.ts +0 -137
  292. package/src/swarm/graph-utils.ts +0 -53
  293. package/src/swarm/index.ts +0 -55
  294. package/src/swarm/limits.ts +0 -66
  295. package/src/swarm/orchestrator.ts +0 -424
  296. package/src/swarm/plan-validator.ts +0 -117
  297. package/src/swarm/router-planner.ts +0 -162
  298. package/src/swarm/router-prompts.ts +0 -39
  299. package/src/swarm/synthesizer.ts +0 -81
  300. package/src/swarm/types.ts +0 -72
  301. package/src/swarm/worker-backend.ts +0 -131
  302. package/src/swarm/worker-prompts.ts +0 -80
  303. package/src/swarm/worker-runner.ts +0 -170
  304. package/src/tools/claude-code/claude-code.ts +0 -610
  305. package/src/tools/swarm/delegate.ts +0 -205
@@ -0,0 +1,178 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import {
5
+ afterAll,
6
+ beforeAll,
7
+ beforeEach,
8
+ describe,
9
+ expect,
10
+ mock,
11
+ test,
12
+ } from "bun:test";
13
+
14
+ const testDir = mkdtempSync(join(tmpdir(), "jobs-store-qdrant-breaker-"));
15
+
16
+ mock.module("../util/platform.js", () => ({
17
+ getDataDir: () => testDir,
18
+ isMacOS: () => process.platform === "darwin",
19
+ isLinux: () => process.platform === "linux",
20
+ isWindows: () => process.platform === "win32",
21
+ getPidPath: () => join(testDir, "test.pid"),
22
+ getDbPath: () => join(testDir, "test.db"),
23
+ getLogPath: () => join(testDir, "test.log"),
24
+ ensureDataDir: () => {},
25
+ }));
26
+
27
+ mock.module("../util/logger.js", () => ({
28
+ getLogger: () =>
29
+ new Proxy({} as Record<string, unknown>, {
30
+ get: () => () => {},
31
+ }),
32
+ }));
33
+
34
+ mock.module("../config/loader.js", () => ({
35
+ loadConfig: () => ({}),
36
+ getConfig: () => ({}),
37
+ invalidateConfigCache: () => {},
38
+ }));
39
+
40
+ import { getDb, initializeDb, resetDb } from "../memory/db.js";
41
+ import {
42
+ claimMemoryJobs,
43
+ enqueueMemoryJob,
44
+ type MemoryJobType,
45
+ } from "../memory/jobs-store.js";
46
+ import {
47
+ _resetQdrantBreaker,
48
+ withQdrantBreaker,
49
+ } from "../memory/qdrant-circuit-breaker.js";
50
+
51
+ describe("claimMemoryJobs with Qdrant circuit breaker", () => {
52
+ beforeAll(() => {
53
+ initializeDb();
54
+ });
55
+
56
+ beforeEach(() => {
57
+ const db = getDb();
58
+ db.run("DELETE FROM memory_jobs");
59
+ _resetQdrantBreaker();
60
+ });
61
+
62
+ afterAll(() => {
63
+ resetDb();
64
+ rmSync(testDir, { recursive: true, force: true });
65
+ });
66
+
67
+ test("claims embed jobs when circuit breaker is closed (healthy)", () => {
68
+ enqueueMemoryJob("embed_segment", { segmentId: "seg-1" });
69
+ enqueueMemoryJob("embed_item", { itemId: "item-1" });
70
+ enqueueMemoryJob("extract_items", { conversationId: "conv-1" });
71
+
72
+ const claimed = claimMemoryJobs(10);
73
+ const types = claimed.map((j) => j.type);
74
+
75
+ expect(types).toContain("embed_segment");
76
+ expect(types).toContain("embed_item");
77
+ expect(types).toContain("extract_items");
78
+ expect(claimed).toHaveLength(3);
79
+ });
80
+
81
+ test("skips embed jobs when circuit breaker is open", async () => {
82
+ // Trip the circuit breaker by recording 5 consecutive failures
83
+ for (let i = 0; i < 5; i++) {
84
+ try {
85
+ await withQdrantBreaker(async () => {
86
+ throw new Error("simulated qdrant failure");
87
+ });
88
+ } catch {
89
+ // expected
90
+ }
91
+ }
92
+
93
+ enqueueMemoryJob("embed_segment", { segmentId: "seg-1" });
94
+ enqueueMemoryJob("embed_item", { itemId: "item-1" });
95
+ enqueueMemoryJob("embed_summary", { summaryId: "sum-1" });
96
+ enqueueMemoryJob("extract_items", { conversationId: "conv-1" });
97
+ enqueueMemoryJob("build_conversation_summary", {
98
+ conversationId: "conv-1",
99
+ });
100
+
101
+ const claimed = claimMemoryJobs(10);
102
+ const types = claimed.map((j) => j.type);
103
+
104
+ // Only non-embed jobs should be claimed
105
+ expect(types).toContain("extract_items");
106
+ expect(types).toContain("build_conversation_summary");
107
+ expect(types).not.toContain("embed_segment");
108
+ expect(types).not.toContain("embed_item");
109
+ expect(types).not.toContain("embed_summary");
110
+ expect(claimed).toHaveLength(2);
111
+ });
112
+
113
+ test("resumes claiming embed jobs after circuit breaker closes", async () => {
114
+ // Trip the circuit breaker
115
+ for (let i = 0; i < 5; i++) {
116
+ try {
117
+ await withQdrantBreaker(async () => {
118
+ throw new Error("simulated qdrant failure");
119
+ });
120
+ } catch {
121
+ // expected
122
+ }
123
+ }
124
+
125
+ // Verify embed jobs are skipped while open
126
+ enqueueMemoryJob("embed_segment", { segmentId: "seg-1" });
127
+ enqueueMemoryJob("extract_items", { conversationId: "conv-1" });
128
+
129
+ const claimedWhileOpen = claimMemoryJobs(10);
130
+ expect(claimedWhileOpen.map((j) => j.type)).not.toContain("embed_segment");
131
+
132
+ // Reset breaker (simulates successful probe closing the circuit)
133
+ _resetQdrantBreaker();
134
+
135
+ // Re-enqueue an embed job (the previous one is now "running")
136
+ enqueueMemoryJob("embed_item", { itemId: "item-2" });
137
+
138
+ const claimedAfterClose = claimMemoryJobs(10);
139
+ const types = claimedAfterClose.map((j) => j.type);
140
+
141
+ expect(types).toContain("embed_item");
142
+ });
143
+
144
+ test("all embed job types are skipped when breaker is open", async () => {
145
+ const embedTypes: MemoryJobType[] = [
146
+ "embed_segment",
147
+ "embed_item",
148
+ "embed_summary",
149
+ "embed_media",
150
+ "embed_attachment",
151
+ ];
152
+
153
+ // Trip the circuit breaker
154
+ for (let i = 0; i < 5; i++) {
155
+ try {
156
+ await withQdrantBreaker(async () => {
157
+ throw new Error("simulated qdrant failure");
158
+ });
159
+ } catch {
160
+ // expected
161
+ }
162
+ }
163
+
164
+ // Enqueue one of each embed type
165
+ for (const type of embedTypes) {
166
+ enqueueMemoryJob(type, { id: `test-${type}` });
167
+ }
168
+ // Also enqueue a non-embed job
169
+ enqueueMemoryJob("extract_entities", { conversationId: "conv-1" });
170
+
171
+ const claimed = claimMemoryJobs(20);
172
+ const types = claimed.map((j) => j.type);
173
+
174
+ // Only the non-embed job should be claimed
175
+ expect(claimed).toHaveLength(1);
176
+ expect(types).toEqual(["extract_entities"]);
177
+ });
178
+ });
@@ -537,8 +537,8 @@ describe("keychain-broker-client", () => {
537
537
  writeFileSync(SOCKET_PATH, "");
538
538
  expect(client.isAvailable()).toBe(false);
539
539
 
540
- // Advance time past the first cooldown (30s)
541
- fakeNow += 30_001;
540
+ // Advance time past the first cooldown (5s)
541
+ fakeNow += 5_001;
542
542
  expect(client.isAvailable()).toBe(true);
543
543
 
544
544
  // Now start a real broker and verify the client reconnects
@@ -558,8 +558,8 @@ describe("keychain-broker-client", () => {
558
558
  const client = createBrokerClient();
559
559
  await client.ping();
560
560
 
561
- // Advance past first cooldown (30s)
562
- fakeNow += 30_001;
561
+ // Advance past first cooldown (5s)
562
+ fakeNow += 5_001;
563
563
 
564
564
  // Start broker — reconnection should succeed and reset counters
565
565
  const broker = createMockBroker();
@@ -578,15 +578,15 @@ describe("keychain-broker-client", () => {
578
578
  rmSync(SOCKET_PATH, { force: true });
579
579
 
580
580
  // This new failure should start from the beginning of the cooldown
581
- // schedule (30s), not escalated.
581
+ // schedule (5s), not escalated.
582
582
  await client.ping();
583
583
 
584
- // Verify cooldown is back to 30s (not 60s)
584
+ // Verify cooldown is back to 5s (not 15s)
585
585
  writeFileSync(SOCKET_PATH, "");
586
586
  expect(client.isAvailable()).toBe(false);
587
587
 
588
- // 30s should be enough to clear cooldown
589
- fakeNow += 30_001;
588
+ // 5s should be enough to clear cooldown
589
+ fakeNow += 5_001;
590
590
  expect(client.isAvailable()).toBe(true);
591
591
  }, 15_000);
592
592
 
@@ -594,60 +594,73 @@ describe("keychain-broker-client", () => {
594
594
  const client = createBrokerClient();
595
595
 
596
596
  // First failure round: two attempts (first + immediate retry) ->
597
- // consecutiveFailures=2, cooldown index = max(2-2,0) = 0 -> 30s.
597
+ // consecutiveFailures=2, cooldown index = max(2-2,0) = 0 -> 5s.
598
598
  await client.ping();
599
599
 
600
600
  writeFileSync(SOCKET_PATH, "");
601
601
  expect(client.isAvailable()).toBe(false);
602
602
 
603
- // 30s should clear the first cooldown
604
- fakeNow += 30_001;
603
+ // 5s should clear the first cooldown
604
+ fakeNow += 5_001;
605
605
  expect(client.isAvailable()).toBe(true);
606
606
 
607
607
  // Remove socket to trigger another failure. After cooldown elapses,
608
608
  // ensureConnected clears unavailableSince and tries connect().
609
609
  // This failure increments consecutiveFailures to 3 (no immediate retry
610
610
  // since consecutiveFailures > 1 after increment).
611
- // Cooldown index = max(3-2,0) = 1 -> 60s.
611
+ // Cooldown index = max(3-2,0) = 1 -> 15s.
612
612
  rmSync(SOCKET_PATH, { force: true });
613
613
  await client.ping();
614
614
 
615
615
  writeFileSync(SOCKET_PATH, "");
616
616
  expect(client.isAvailable()).toBe(false);
617
617
 
618
- fakeNow += 30_001;
619
- expect(client.isAvailable()).toBe(false); // 30s not enough
618
+ fakeNow += 5_001;
619
+ expect(client.isAvailable()).toBe(false); // 5s not enough
620
620
 
621
- fakeNow += 30_000; // total 60_001ms since this cooldown started
621
+ fakeNow += 10_000; // total 15_001ms since this cooldown started
622
622
  expect(client.isAvailable()).toBe(true);
623
623
 
624
- // Another failure -> consecutiveFailures=4, index = max(4-2,0) = 2 -> 120s (2min)
624
+ // Another failure -> consecutiveFailures=4, index = max(4-2,0) = 2 -> 30s
625
625
  rmSync(SOCKET_PATH, { force: true });
626
626
  await client.ping();
627
627
 
628
628
  writeFileSync(SOCKET_PATH, "");
629
629
  expect(client.isAvailable()).toBe(false);
630
630
 
631
- fakeNow += 60_001;
631
+ fakeNow += 15_001;
632
+ expect(client.isAvailable()).toBe(false);
633
+
634
+ fakeNow += 15_000; // total 30_001ms
635
+ expect(client.isAvailable()).toBe(true);
636
+
637
+ // Another failure -> consecutiveFailures=5, index = max(5-2,0) = 3 -> 60s
638
+ rmSync(SOCKET_PATH, { force: true });
639
+ await client.ping();
640
+
641
+ writeFileSync(SOCKET_PATH, "");
642
+ expect(client.isAvailable()).toBe(false);
643
+
644
+ fakeNow += 30_001;
632
645
  expect(client.isAvailable()).toBe(false);
633
646
 
634
- fakeNow += 60_000; // total 120_001ms
647
+ fakeNow += 30_000; // total 60_001ms
635
648
  expect(client.isAvailable()).toBe(true);
636
649
 
637
- // Another failure -> consecutiveFailures=5, index = max(5-2,0) = 3 -> 300s (5min)
650
+ // Another failure -> consecutiveFailures=6, index = min(max(6-2,0), 4) = 4 -> 300s (5min)
638
651
  rmSync(SOCKET_PATH, { force: true });
639
652
  await client.ping();
640
653
 
641
654
  writeFileSync(SOCKET_PATH, "");
642
655
  expect(client.isAvailable()).toBe(false);
643
656
 
644
- fakeNow += 120_001;
657
+ fakeNow += 60_001;
645
658
  expect(client.isAvailable()).toBe(false);
646
659
 
647
- fakeNow += 180_000; // total 300_001ms
660
+ fakeNow += 240_000; // total 300_001ms
648
661
  expect(client.isAvailable()).toBe(true);
649
662
 
650
- // Another failure -> consecutiveFailures=6, index = min(max(6-2,0), 3) = 3 -> 300s (capped)
663
+ // Another failure -> consecutiveFailures=7, index = min(max(7-2,0), 4) = 4 -> 300s (capped)
651
664
  rmSync(SOCKET_PATH, { force: true });
652
665
  await client.ping();
653
666
 
@@ -658,4 +671,130 @@ describe("keychain-broker-client", () => {
658
671
  expect(client.isAvailable()).toBe(true);
659
672
  });
660
673
  });
674
+
675
+ // -----------------------------------------------------------------------
676
+ // Connect timeout
677
+ // -----------------------------------------------------------------------
678
+ describe("connect timeout", () => {
679
+ let stopFn: (() => Promise<void>) | null = null;
680
+
681
+ beforeEach(() => {
682
+ writeFileSync(TOKEN_PATH, TEST_TOKEN);
683
+ });
684
+
685
+ afterEach(async () => {
686
+ if (stopFn) {
687
+ await stopFn();
688
+ stopFn = null;
689
+ }
690
+ });
691
+
692
+ test("rejects connect within 3 seconds when broker is unresponsive", async () => {
693
+ // Create a server that accepts connections but never responds
694
+ // (simulates an unresponsive broker process).
695
+ const activeConns = new Set<import("node:net").Socket>();
696
+ const server = createServer((conn) => {
697
+ activeConns.add(conn);
698
+ conn.on("close", () => activeConns.delete(conn));
699
+ // Accept connection but do nothing — no data, no close
700
+ });
701
+ await new Promise<void>((resolve) => {
702
+ server.listen(SOCKET_PATH, () => resolve());
703
+ });
704
+ stopFn = () =>
705
+ new Promise<void>((resolve) => {
706
+ for (const conn of activeConns) conn.destroy();
707
+ activeConns.clear();
708
+ server.close(() => resolve());
709
+ });
710
+
711
+ const client = createBrokerClient();
712
+ const start = Date.now();
713
+ const result = await client.ping();
714
+ const elapsed = Date.now() - start;
715
+
716
+ // Should return null (graceful fallback) and not hang indefinitely.
717
+ // The connect timeout is 3s; allow some slack but it should be well
718
+ // under 10s (the old behavior would hang for REQUEST_TIMEOUT_MS * retries).
719
+ expect(result).toBeNull();
720
+ expect(elapsed).toBeLessThan(10_000);
721
+ }, 15_000);
722
+
723
+ test("successful connect clears the connect timer", async () => {
724
+ // Normal broker that responds to pings — verifies the timer is cleared
725
+ // and doesn't fire after a successful connection.
726
+ const broker = createMockBroker();
727
+ broker.setHandler(() => ({ ok: true, result: { pong: true } }));
728
+ await broker.start();
729
+ stopFn = () => broker.stop();
730
+
731
+ const client = createBrokerClient();
732
+ const result = await client.ping();
733
+ expect(result).toEqual({ pong: true });
734
+
735
+ // Wait a bit past the connect timeout to ensure no stale timer fires
736
+ await new Promise((r) => setTimeout(r, 100));
737
+
738
+ // Client should still work fine
739
+ const result2 = await client.ping();
740
+ expect(result2).toEqual({ pong: true });
741
+ });
742
+ });
743
+
744
+ // -----------------------------------------------------------------------
745
+ // Reduced initial cooldown
746
+ // -----------------------------------------------------------------------
747
+ describe("reduced initial cooldown", () => {
748
+ const originalDateNow = Date.now;
749
+ let fakeNow: number;
750
+
751
+ beforeEach(() => {
752
+ fakeNow = originalDateNow.call(Date);
753
+ Date.now = () => fakeNow;
754
+ writeFileSync(TOKEN_PATH, TEST_TOKEN);
755
+ });
756
+
757
+ afterEach(() => {
758
+ Date.now = originalDateNow;
759
+ });
760
+
761
+ test("first cooldown is 5 seconds, not 30 seconds", async () => {
762
+ const client = createBrokerClient();
763
+
764
+ // Trigger two connection failures (first + immediate retry)
765
+ await client.ping();
766
+
767
+ writeFileSync(SOCKET_PATH, "");
768
+
769
+ // Should still be in cooldown at 4 seconds
770
+ fakeNow += 4_000;
771
+ expect(client.isAvailable()).toBe(false);
772
+
773
+ // Should be available after 5 seconds
774
+ fakeNow += 1_001;
775
+ expect(client.isAvailable()).toBe(true);
776
+ });
777
+
778
+ test("second cooldown is 15 seconds", async () => {
779
+ const client = createBrokerClient();
780
+
781
+ // First failure round -> cooldown 5s
782
+ await client.ping();
783
+
784
+ // Clear first cooldown
785
+ fakeNow += 5_001;
786
+
787
+ // Second failure -> cooldown 15s
788
+ await client.ping();
789
+
790
+ writeFileSync(SOCKET_PATH, "");
791
+ expect(client.isAvailable()).toBe(false);
792
+
793
+ fakeNow += 14_000;
794
+ expect(client.isAvailable()).toBe(false);
795
+
796
+ fakeNow += 1_001;
797
+ expect(client.isAvailable()).toBe(true);
798
+ });
799
+ });
661
800
  });
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Tests for adaptive poll interval backoff in the memory jobs worker.
3
+ *
4
+ * Verifies that when no jobs are claimable, the poll interval doubles each
5
+ * tick (1.5s -> 3s -> 6s -> ... -> 30s cap), and resets to 1.5s when work
6
+ * is found.
7
+ */
8
+ import { describe, expect, mock, test } from "bun:test";
9
+
10
+ // ── Mocks (must precede imports of tested module) ──────────────────
11
+
12
+ mock.module("../util/platform.js", () => ({
13
+ getDataDir: () => "/tmp/test-backoff",
14
+ isMacOS: () => false,
15
+ isLinux: () => true,
16
+ isWindows: () => false,
17
+ getPidPath: () => "/tmp/test-backoff/test.pid",
18
+ getDbPath: () => "/tmp/test-backoff/test.db",
19
+ getLogPath: () => "/tmp/test-backoff/test.log",
20
+ ensureDataDir: () => {},
21
+ }));
22
+
23
+ mock.module("../util/logger.js", () => ({
24
+ getLogger: () =>
25
+ new Proxy({} as Record<string, unknown>, {
26
+ get: () => () => {},
27
+ }),
28
+ }));
29
+
30
+ // Mock config — memory disabled so runMemoryJobsOnce returns 0 immediately
31
+ mock.module("../config/loader.js", () => ({
32
+ getConfig: () => ({
33
+ memory: { enabled: false },
34
+ }),
35
+ loadConfig: () => ({
36
+ memory: { enabled: false },
37
+ }),
38
+ }));
39
+
40
+ // Mock jobs-store (accesses DB)
41
+ mock.module("../memory/jobs-store.js", () => ({
42
+ resetRunningJobsToPending: () => 0,
43
+ claimMemoryJobs: () => [],
44
+ completeMemoryJob: () => {},
45
+ deferMemoryJob: () => "deferred",
46
+ failMemoryJob: () => {},
47
+ failStalledJobs: () => 0,
48
+ enqueueCleanupStaleSupersededItemsJob: () => null,
49
+ enqueuePruneOldConversationsJob: () => null,
50
+ }));
51
+
52
+ // Mock db.js (rawRun used in sweepStaleItems)
53
+ mock.module("../memory/db.js", () => ({
54
+ rawRun: () => 0,
55
+ }));
56
+
57
+ import {
58
+ POLL_INTERVAL_MAX_MS,
59
+ POLL_INTERVAL_MIN_MS,
60
+ startMemoryJobsWorker,
61
+ } from "../memory/jobs-worker.js";
62
+
63
+ describe("memory jobs worker adaptive poll interval", () => {
64
+ test("exports expected poll interval constants", () => {
65
+ expect(POLL_INTERVAL_MIN_MS).toBe(1_500);
66
+ expect(POLL_INTERVAL_MAX_MS).toBe(30_000);
67
+ });
68
+
69
+ test("backoff sequence doubles from min to max then caps", () => {
70
+ // Verify the math: starting at 1500, doubling each step, capped at 30000
71
+ const intervals: number[] = [];
72
+ let current = POLL_INTERVAL_MIN_MS;
73
+ for (let i = 0; i < 10; i++) {
74
+ intervals.push(current);
75
+ current = Math.min(current * 2, POLL_INTERVAL_MAX_MS);
76
+ }
77
+ expect(intervals).toEqual([
78
+ 1_500, // tick 1
79
+ 3_000, // tick 2
80
+ 6_000, // tick 3
81
+ 12_000, // tick 4
82
+ 24_000, // tick 5
83
+ 30_000, // tick 6 (capped)
84
+ 30_000, // stays capped
85
+ 30_000,
86
+ 30_000,
87
+ 30_000,
88
+ ]);
89
+ });
90
+
91
+ test("worker schedules setTimeout with increasing intervals when idle", async () => {
92
+ const timeoutDelays: number[] = [];
93
+ const originalSetTimeout = globalThis.setTimeout;
94
+ const originalClearTimeout = globalThis.clearTimeout;
95
+
96
+ // Collect pending timer callbacks so we can fire them manually
97
+ const pendingCallbacks: Array<() => void> = [];
98
+
99
+ globalThis.setTimeout = ((fn: () => void, delay?: number) => {
100
+ if (delay !== undefined && delay >= POLL_INTERVAL_MIN_MS) {
101
+ timeoutDelays.push(delay);
102
+ pendingCallbacks.push(fn);
103
+ }
104
+ return 999 as unknown as ReturnType<typeof setTimeout>;
105
+ }) as typeof setTimeout;
106
+ globalThis.clearTimeout = (() => {}) as typeof clearTimeout;
107
+
108
+ try {
109
+ const worker = startMemoryJobsWorker();
110
+
111
+ // Wait for the initial tick() promise to settle
112
+ await new Promise((resolve) => originalSetTimeout(resolve, 20));
113
+
114
+ // Fire pending timer callbacks to advance through the backoff sequence.
115
+ // Each callback triggers tick() which is async, so we await a microtask
116
+ // after each to let the promise chain settle and schedule the next timer.
117
+ for (let i = 0; i < 6; i++) {
118
+ const cb = pendingCallbacks.shift();
119
+ if (cb) {
120
+ cb();
121
+ await new Promise((resolve) => originalSetTimeout(resolve, 20));
122
+ }
123
+ }
124
+
125
+ worker.stop();
126
+
127
+ // We should have captured several setTimeout calls with increasing delays
128
+ expect(timeoutDelays.length).toBeGreaterThanOrEqual(4);
129
+
130
+ // Intervals should be non-decreasing (backoff)
131
+ for (let i = 1; i < timeoutDelays.length; i++) {
132
+ expect(timeoutDelays[i]).toBeGreaterThanOrEqual(timeoutDelays[i - 1]!);
133
+ }
134
+
135
+ // All intervals within bounds
136
+ for (const delay of timeoutDelays) {
137
+ expect(delay).toBeGreaterThanOrEqual(POLL_INTERVAL_MIN_MS);
138
+ expect(delay).toBeLessThanOrEqual(POLL_INTERVAL_MAX_MS);
139
+ }
140
+
141
+ // Should eventually reach the cap
142
+ expect(timeoutDelays[timeoutDelays.length - 1]).toBe(
143
+ POLL_INTERVAL_MAX_MS,
144
+ );
145
+ } finally {
146
+ globalThis.setTimeout = originalSetTimeout;
147
+ globalThis.clearTimeout = originalClearTimeout;
148
+ }
149
+ });
150
+ });
@@ -527,9 +527,9 @@ describe("integration: existing routes unaffected", () => {
527
527
  });
528
528
 
529
529
  test("GET /v1/health still works (not intercepted by migration routes)", async () => {
530
- const { handleHealth } =
530
+ const { handleDetailedHealth } =
531
531
  await import("../runtime/routes/identity-routes.js");
532
- const res = handleHealth();
532
+ const res = handleDetailedHealth();
533
533
  const body = (await res.json()) as Record<string, unknown>;
534
534
 
535
535
  expect(res.status).toBe(200);
@@ -939,9 +939,9 @@ describe("route policy registration", () => {
939
939
 
940
940
  describe("integration: existing routes unaffected", () => {
941
941
  test("GET /v1/health still works", async () => {
942
- const { handleHealth } =
942
+ const { handleDetailedHealth } =
943
943
  await import("../runtime/routes/identity-routes.js");
944
- const res = handleHealth();
944
+ const res = handleDetailedHealth();
945
945
  const body = (await res.json()) as Record<string, unknown>;
946
946
 
947
947
  expect(res.status).toBe(200);
@@ -792,9 +792,9 @@ describe("route policy registration", () => {
792
792
 
793
793
  describe("integration: existing routes unaffected", () => {
794
794
  test("GET /v1/health still works", async () => {
795
- const { handleHealth } =
795
+ const { handleDetailedHealth } =
796
796
  await import("../runtime/routes/identity-routes.js");
797
- const res = handleHealth();
797
+ const res = handleDetailedHealth();
798
798
  const body = (await res.json()) as Record<string, unknown>;
799
799
 
800
800
  expect(res.status).toBe(200);
@@ -684,9 +684,9 @@ describe("route policy registration", () => {
684
684
 
685
685
  describe("integration: existing routes unaffected", () => {
686
686
  test("GET /v1/health still works (not intercepted by migration routes)", async () => {
687
- const { handleHealth } =
687
+ const { handleDetailedHealth } =
688
688
  await import("../runtime/routes/identity-routes.js");
689
- const res = handleHealth();
689
+ const res = handleDetailedHealth();
690
690
  const body = (await res.json()) as Record<string, unknown>;
691
691
 
692
692
  expect(res.status).toBe(200);
@@ -37,11 +37,6 @@ mock.module("../util/logger.js", () => ({
37
37
  }),
38
38
  }));
39
39
 
40
- // Mock security check to always pass
41
- mock.module("../security/secret-ingress.js", () => ({
42
- checkIngressForSecrets: () => ({ blocked: false }),
43
- }));
44
-
45
40
  mock.module("../config/env.js", () => ({
46
41
  isHttpAuthDisabled: () => true,
47
42
  getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
@@ -34,6 +34,10 @@ mock.module("../notifications/conversation-candidates.js", () => ({
34
34
  serializeCandidatesForPrompt: () => undefined,
35
35
  }));
36
36
 
37
+ mock.module("../prompts/persona-resolver.js", () => ({
38
+ resolveGuardianPersona: () => null,
39
+ }));
40
+
37
41
  let configuredProvider: { sendMessage: () => Promise<unknown> } | null = null;
38
42
  let extractedToolUse: unknown = null;
39
43
 
@@ -36,6 +36,10 @@ mock.module("../notifications/conversation-candidates.js", () => ({
36
36
  serializeCandidatesForPrompt: () => undefined,
37
37
  }));
38
38
 
39
+ mock.module("../prompts/persona-resolver.js", () => ({
40
+ resolveGuardianPersona: () => null,
41
+ }));
42
+
39
43
  // ── Identity context mock ─────────────────────────────────────────────
40
44
 
41
45
  let mockIdentityContext: string | null = null;
@@ -15,6 +15,7 @@ describe("isAllowDecision", () => {
15
15
  "always_allow",
16
16
  "always_allow_high_risk",
17
17
  "temporary_override",
18
+ "dangerously_skip_permissions",
18
19
  ];
19
20
 
20
21
  for (const decision of allowDecisions) {