@vellumai/assistant 0.12.2 → 0.12.3-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 (298) hide show
  1. package/.env.example +2 -0
  2. package/AGENTS.md +1 -1
  3. package/docs/architecture/integrations.md +7 -0
  4. package/docs/architecture/memory.md +17 -2
  5. package/docs/credential-execution-service.md +1 -1
  6. package/docs/desktop-browser-cli.md +7 -3
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  9. package/node_modules/@vellumai/environments/src/shell.test.ts +21 -0
  10. package/node_modules/@vellumai/environments/src/shell.ts +24 -0
  11. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  12. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  13. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +8 -2
  14. package/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
  15. package/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
  16. package/openapi.yaml +54 -5
  17. package/package.json +1 -1
  18. package/scripts/smoke-desktop-browser-cli.ts +1 -0
  19. package/scripts/sync-llm-catalog.ts +3 -0
  20. package/scripts/voice-ttft-spike.ts +2 -2
  21. package/src/__tests__/agent-loop.test.ts +259 -0
  22. package/src/__tests__/agent-wake-delegation-prompt.test.ts +64 -2
  23. package/src/__tests__/anthropic-provider.test.ts +55 -0
  24. package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
  25. package/src/__tests__/attachments-store.test.ts +22 -3
  26. package/src/__tests__/channel-approval.test.ts +9 -14
  27. package/src/__tests__/channel-reply-delivery.test.ts +56 -0
  28. package/src/__tests__/chat-credential-redaction.test.ts +23 -0
  29. package/src/__tests__/computer-use-screenshot-attachments.test.ts +411 -0
  30. package/src/__tests__/computer-use-screenshot-selection.test.ts +50 -0
  31. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
  32. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
  33. package/src/__tests__/conversation-agent-loop.test.ts +257 -6
  34. package/src/__tests__/conversation-attachments.test.ts +106 -0
  35. package/src/__tests__/conversation-fork-crud.test.ts +191 -0
  36. package/src/__tests__/conversation-rendered-delegation-state.test.ts +276 -0
  37. package/src/__tests__/credential-execution-client.test.ts +70 -53
  38. package/src/__tests__/db-conversation-tool-surface.test.ts +221 -0
  39. package/src/__tests__/events-tail-route.test.ts +33 -0
  40. package/src/__tests__/history-repair.test.ts +53 -0
  41. package/src/__tests__/inference-profile-session-handler.test.ts +26 -0
  42. package/src/__tests__/list-messages-tool-merge.test.ts +85 -1
  43. package/src/__tests__/llm-catalog-parity.test.ts +27 -4
  44. package/src/__tests__/oauth-apps-routes.test.ts +1 -0
  45. package/src/__tests__/oauth-commands-routes.test.ts +114 -101
  46. package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
  47. package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
  48. package/src/__tests__/oauth-providers-routes.test.ts +2 -0
  49. package/src/__tests__/persist-media-references.test.ts +50 -0
  50. package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
  51. package/src/__tests__/run-conversation-turn-persistence.test.ts +138 -1
  52. package/src/__tests__/schedule-routes.test.ts +20 -0
  53. package/src/__tests__/scheduler-result-notification.test.ts +23 -4
  54. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  55. package/src/__tests__/secret-routes-platform-proxy.test.ts +86 -31
  56. package/src/__tests__/secret-routes-scrub.test.ts +22 -27
  57. package/src/__tests__/secret-scanner.test.ts +20 -0
  58. package/src/__tests__/secure-keys-managed-failover.test.ts +8 -0
  59. package/src/__tests__/secure-keys.test.ts +7 -3
  60. package/src/__tests__/server-tool-pairing.test.ts +107 -0
  61. package/src/__tests__/skills.test.ts +5 -4
  62. package/src/__tests__/subagent-tool-gate-mode.test.ts +226 -0
  63. package/src/__tests__/terminal-tools.test.ts +8 -0
  64. package/src/__tests__/tool-result-follow-up.test.ts +99 -0
  65. package/src/__tests__/tool-result-metadata-plumbing.test.ts +63 -0
  66. package/src/__tests__/unicode.test.ts +36 -0
  67. package/src/agent/loop.ts +37 -12
  68. package/src/agent/tool-result-follow-up.ts +70 -0
  69. package/src/api/attachment-provenance.test.ts +68 -0
  70. package/src/api/computer-use-tool.test.ts +55 -0
  71. package/src/api/computer-use-tool.ts +27 -0
  72. package/src/api/events/assistant-outbound-attachment.ts +3 -0
  73. package/src/api/events/desktop-activity-changed.ts +10 -0
  74. package/src/api/events/question-request.ts +1 -0
  75. package/src/api/index.ts +11 -0
  76. package/src/api/responses/conversation-message.ts +3 -0
  77. package/src/approvals/approval-primitive.ts +5 -2
  78. package/src/approvals/scoped-approval-grants.ts +6 -2
  79. package/src/browser/virtual-desktop-target.ts +1 -2
  80. package/src/cli/commands/__tests__/cli-test-harness.ts +21 -3
  81. package/src/cli/commands/__tests__/plugins.test.ts +20 -1
  82. package/src/cli/commands/__tests__/schedules.test.ts +14 -0
  83. package/src/cli/commands/bash.help.ts +4 -3
  84. package/src/cli/commands/browser.help.ts +5 -1
  85. package/src/cli/commands/credentials.help.ts +3 -3
  86. package/src/cli/commands/oauth/index.help.ts +10 -0
  87. package/src/cli/commands/oauth/providers.ts +12 -0
  88. package/src/cli/commands/oauth/request.test.ts +179 -2
  89. package/src/cli/commands/oauth/request.ts +41 -10
  90. package/src/cli/commands/plugins.ts +12 -5
  91. package/src/cli/commands/schedules.ts +2 -0
  92. package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
  93. package/src/cli/lib/__tests__/install-from-github.test.ts +41 -0
  94. package/src/cli/lib/__tests__/local-plugin-upgrade.test.ts +133 -51
  95. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +8 -2
  96. package/src/cli/lib/bundled-marketplace.json +855 -0
  97. package/src/cli/lib/bundled-plugin-packages.json +783 -1
  98. package/src/cli/lib/inspect-plugin.ts +11 -4
  99. package/src/cli/lib/upgrade-plugin.ts +19 -11
  100. package/src/cli/output.ts +18 -4
  101. package/src/config/__tests__/default-provider.test.ts +4 -0
  102. package/src/config/bundled-skills/schedule/SKILL.md +6 -6
  103. package/src/config/loader.ts +9 -17
  104. package/src/config/platform-identity.ts +8 -8
  105. package/src/config/profile-text-generation.test.ts +51 -0
  106. package/src/config/profile-text-generation.ts +51 -0
  107. package/src/config/schemas/__tests__/memory-v3.test.ts +12 -0
  108. package/src/config/schemas/llm.ts +7 -2
  109. package/src/config/schemas/mcp.ts +5 -1
  110. package/src/config/schemas/memory-v3.ts +12 -0
  111. package/src/credential-execution/ces-runtime.ts +2 -2
  112. package/src/credential-execution/executable-discovery.ts +36 -134
  113. package/src/credential-execution/process-manager.test.ts +20 -24
  114. package/src/credential-execution/process-manager.ts +24 -19
  115. package/src/daemon/__tests__/conversation-tool-setup.test.ts +43 -0
  116. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +36 -0
  117. package/src/daemon/assistant-attachments.ts +20 -12
  118. package/src/daemon/chat-credential-redaction.ts +16 -1
  119. package/src/daemon/conversation-agent-loop-handlers.ts +74 -3
  120. package/src/daemon/conversation-agent-loop.ts +12 -0
  121. package/src/daemon/conversation-attachments.ts +93 -12
  122. package/src/daemon/conversation-tool-setup.ts +71 -3
  123. package/src/daemon/conversation-turn-finalize.ts +26 -13
  124. package/src/daemon/conversation.ts +72 -17
  125. package/src/daemon/daemon-control.ts +3 -7
  126. package/src/daemon/lifecycle.ts +4 -4
  127. package/src/daemon/mcp-reload-service.ts +1 -1
  128. package/src/daemon/message-types/shared.ts +1 -0
  129. package/src/daemon/orphan-reaper.ts +4 -3
  130. package/src/daemon/persist-media-references.ts +20 -3
  131. package/src/daemon/tool-setup-types.ts +6 -0
  132. package/src/daemon/wake-conversation-ops.ts +50 -15
  133. package/src/desktop/desktop-automation-lease.test.ts +230 -0
  134. package/src/desktop/desktop-automation-lease.ts +90 -6
  135. package/src/desktop/desktop-help.ts +15 -0
  136. package/src/desktop/desktop-stream-bridge.test.ts +65 -10
  137. package/src/desktop/desktop-stream-bridge.ts +3 -3
  138. package/src/desktop/desktop-wallpaper-renderer.ts +132 -0
  139. package/src/desktop/desktop-wallpaper-worker.ts +17 -0
  140. package/src/desktop/desktop-wallpaper.test.ts +48 -7
  141. package/src/desktop/desktop-wallpaper.ts +34 -102
  142. package/src/desktop/virtual-desktop-feature.ts +1 -1
  143. package/src/mcp/__tests__/manager-tool-caps.test.ts +111 -0
  144. package/src/mcp/__tests__/startup.test.ts +30 -7
  145. package/src/mcp/__tests__/tool-caps.test.ts +107 -0
  146. package/src/mcp/manager.ts +168 -101
  147. package/src/mcp/startup.ts +33 -13
  148. package/src/mcp/tool-caps.ts +176 -0
  149. package/src/messaging/provider-message-metadata.ts +3 -3
  150. package/src/notifications/__tests__/copy-composer.test.ts +70 -0
  151. package/src/notifications/copy-composer.ts +11 -3
  152. package/src/notifications/schedule-result-producer.ts +4 -6
  153. package/src/oauth/AGENTS.md +2 -0
  154. package/src/oauth/__tests__/identity-verifier.test.ts +36 -1
  155. package/src/oauth/identity-verifier.ts +24 -0
  156. package/src/oauth/oauth-store.ts +11 -1
  157. package/src/oauth/provider-serializer.ts +1 -0
  158. package/src/oauth/seed-providers.ts +12 -0
  159. package/src/permissions/confirmation-guardian-request.test.ts +16 -0
  160. package/src/permissions/confirmation-guardian-request.ts +2 -3
  161. package/src/permissions/question-prompter.test.ts +31 -0
  162. package/src/permissions/question-prompter.ts +2 -0
  163. package/src/persistence/attachments-store.ts +84 -44
  164. package/src/persistence/conversation-crud.ts +111 -20
  165. package/src/persistence/conversation-plugin-facade.ts +30 -0
  166. package/src/persistence/conversation-tool-surface.ts +135 -0
  167. package/src/persistence/conversation-types.test.ts +32 -0
  168. package/src/persistence/conversation-types.ts +26 -4
  169. package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +78 -0
  170. package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +29 -0
  171. package/src/persistence/migrations/379-oauth-providers-response-ok-field.test.ts +92 -0
  172. package/src/persistence/migrations/379-oauth-providers-response-ok-field.ts +32 -0
  173. package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.test.ts +90 -0
  174. package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.ts +34 -0
  175. package/src/persistence/schema/conversation-tool-surfaces.ts +32 -0
  176. package/src/persistence/schema/index.ts +1 -0
  177. package/src/persistence/schema/oauth.ts +1 -0
  178. package/src/persistence/steps.ts +24 -3
  179. package/src/plugin-api/conversation-turn.ts +31 -7
  180. package/src/plugin-api/index.ts +11 -1
  181. package/src/plugin-api/plugin-channel-turn-trust.test.ts +133 -0
  182. package/src/plugin-api/plugin-channel-turn-trust.ts +71 -0
  183. package/src/plugins/defaults/memory/AGENTS.md +39 -5
  184. package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +365 -0
  185. package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +43 -0
  186. package/src/plugins/defaults/memory/__tests__/conversation-memory-purge.test.ts +1 -0
  187. package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +2 -0
  188. package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +17 -0
  189. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +74 -0
  190. package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +161 -0
  191. package/src/plugins/defaults/memory/__tests__/relocated-memory-test-rows.ts +10 -0
  192. package/src/plugins/defaults/memory/buffer-file.ts +354 -0
  193. package/src/plugins/defaults/memory/buffer-format.ts +40 -0
  194. package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -2
  195. package/src/plugins/defaults/memory/context-search/format.ts +2 -1
  196. package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -1
  197. package/src/plugins/defaults/memory/context-search/sources/workspace.ts +2 -1
  198. package/src/plugins/defaults/memory/conversation-memory-purge.ts +4 -0
  199. package/src/plugins/defaults/memory/graph/capability-seed.ts +1 -2
  200. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -42
  201. package/src/plugins/defaults/memory/host-utils.ts +0 -10
  202. package/src/plugins/defaults/memory/injectors.ts +4 -3
  203. package/src/plugins/defaults/memory/memory-retrospective-job.ts +72 -183
  204. package/src/plugins/defaults/memory/memory-run-evidence.ts +213 -0
  205. package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +1 -1
  206. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +407 -99
  207. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +10 -0
  208. package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +107 -7
  209. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +307 -86
  210. package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +34 -0
  211. package/src/plugins/defaults/memory/substrate/page-index.ts +2 -1
  212. package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +89 -49
  213. package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
  214. package/src/plugins/defaults/memory/tools.ts +1 -1
  215. package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
  216. package/src/plugins/defaults/memory/v1/graph/extraction.ts +2 -1
  217. package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
  218. package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +5 -0
  219. package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +5 -2
  220. package/src/plugins/defaults/memory/v2/reranker.ts +2 -1
  221. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +81 -1
  222. package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +87 -0
  223. package/src/plugins/defaults/memory/v3/__tests__/plugin-schema.test.ts +12 -0
  224. package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +183 -4
  225. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +103 -2
  226. package/src/plugins/defaults/memory/v3/card.ts +2 -1
  227. package/src/plugins/defaults/memory/v3/injector.ts +212 -178
  228. package/src/plugins/defaults/memory/v3/orchestrate.ts +112 -25
  229. package/src/plugins/defaults/memory/v3/plugin-schema.ts +54 -4
  230. package/src/plugins/defaults/memory/v3/pool-log-store.ts +253 -1
  231. package/src/plugins/defaults/memory/v3/pool-select.test.ts +35 -2
  232. package/src/plugins/defaults/memory/v3/pool-select.ts +31 -19
  233. package/src/plugins/defaults/memory/v3/sections.ts +2 -1
  234. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +41 -14
  235. package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +4 -1
  236. package/src/plugins/defaults/tool-result-truncate/terminal.ts +1 -46
  237. package/src/prompts/__tests__/parallel-tasks-section.test.ts +25 -0
  238. package/src/prompts/delegation-gate.ts +57 -0
  239. package/src/prompts/system-prompt.ts +14 -32
  240. package/src/providers/inference/adapter-factory.ts +6 -0
  241. package/src/providers/jev/client.test.ts +260 -0
  242. package/src/providers/jev/client.ts +518 -0
  243. package/src/providers/model-catalog.ts +58 -3
  244. package/src/providers/server-tool-pairing.ts +16 -7
  245. package/src/runtime/AGENTS.md +2 -2
  246. package/src/runtime/__tests__/agent-wake.test.ts +94 -1
  247. package/src/runtime/agent-wake.ts +31 -4
  248. package/src/runtime/guardian-action-service.ts +2 -17
  249. package/src/runtime/guardian-reply-router.ts +1 -8
  250. package/src/runtime/http-server.ts +2 -2
  251. package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +89 -0
  252. package/src/runtime/migrations/vbundle-import-policy.ts +33 -11
  253. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +24 -2
  254. package/src/runtime/routes/channel-route-shared.ts +1 -9
  255. package/src/runtime/routes/conversation-routes.ts +29 -4
  256. package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
  257. package/src/runtime/routes/desktop-setup-routes.ts +8 -5
  258. package/src/runtime/routes/guardian-approval-interception.ts +24 -0
  259. package/src/runtime/routes/inbound-message-handler.ts +2 -3
  260. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
  261. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -4
  262. package/src/runtime/routes/inference-profile-session-handler.ts +11 -1
  263. package/src/runtime/routes/inference-profiles-routes.ts +9 -0
  264. package/src/runtime/routes/oauth-commands-routes.ts +57 -54
  265. package/src/runtime/routes/oauth-providers.ts +4 -0
  266. package/src/runtime/routes/oauth-request-hints.test.ts +246 -0
  267. package/src/runtime/routes/oauth-request-hints.ts +149 -0
  268. package/src/runtime/routes/schedule-routes.ts +2 -0
  269. package/src/runtime/routes/secret-routes.ts +99 -119
  270. package/src/schedule/__tests__/worker-mcp-bootstrap.test.ts +107 -0
  271. package/src/schedule/__tests__/worker-mcp-readiness.test.ts +173 -0
  272. package/src/schedule/__tests__/worker-mcp-tools.test.ts +13 -47
  273. package/src/schedule/run-script.ts +2 -2
  274. package/src/schedule/scheduler.ts +47 -11
  275. package/src/schedule/tool-surface-readiness.ts +65 -0
  276. package/src/schedule/worker-mcp.ts +85 -0
  277. package/src/schedule/worker.ts +9 -31
  278. package/src/security/secure-keys.ts +60 -48
  279. package/src/tools/ask-question/ask-question-tool.test.ts +167 -5
  280. package/src/tools/ask-question/ask-question-tool.ts +119 -23
  281. package/src/tools/browser/browser-execution.ts +7 -1
  282. package/src/tools/host-terminal/host-shell.ts +12 -6
  283. package/src/tools/shared/filesystem/file-ops-service.ts +1 -31
  284. package/src/tools/shared/shell-output.test.ts +10 -0
  285. package/src/tools/shared/shell-output.ts +14 -2
  286. package/src/tools/skills/sandbox-runner.ts +13 -2
  287. package/src/tools/skills/scaffold-managed.ts +2 -1
  288. package/src/tools/terminal/__tests__/safe-env.test.ts +33 -4
  289. package/src/tools/terminal/__tests__/sanitized-bash.test.ts +0 -14
  290. package/src/tools/terminal/safe-env.ts +35 -24
  291. package/src/tools/terminal/sanitized-bash.ts +15 -2
  292. package/src/tools/terminal/shell-launch.test.ts +162 -0
  293. package/src/tools/terminal/shell.test.ts +29 -0
  294. package/src/tools/terminal/shell.ts +13 -7
  295. package/src/util/browser-human-verification.ts +14 -0
  296. package/src/util/host-process.test.ts +17 -1
  297. package/src/util/host-process.ts +24 -0
  298. package/src/util/unicode.ts +29 -0
@@ -5,6 +5,7 @@ import { beforeEach, describe, expect, test } from "bun:test";
5
5
  import { eq, like } from "drizzle-orm";
6
6
 
7
7
  import {
8
+ getAttachmentContent,
8
9
  getAttachmentsForMessage,
9
10
  linkAttachmentToMessage,
10
11
  uploadAttachment,
@@ -17,10 +18,17 @@ import {
17
18
  import {
18
19
  addMessage,
19
20
  createConversation,
21
+ deleteConversation,
20
22
  forkConversation,
21
23
  getMessages,
24
+ listConversationAttachments,
25
+ updateMessageMetadata,
22
26
  } from "../persistence/conversation-crud.js";
23
27
  import { getConversationDirPath } from "../persistence/conversation-disk-view.js";
28
+ import {
29
+ COMPUTER_USE_SCREENSHOT_ATTACHMENT_IDS_KEY,
30
+ computerUseScreenshotAttachmentIdsFromMetadata,
31
+ } from "../persistence/conversation-types.js";
24
32
  import {
25
33
  getDb,
26
34
  getLogsDb,
@@ -63,6 +71,7 @@ import {
63
71
  recordInjected as recordV3Injected,
64
72
  } from "../plugins/defaults/memory/v3/ever-injected-store.js";
65
73
  import { ensureMemoryV3InjectedSectionsSchema } from "../plugins/defaults/memory/v3/plugin-schema.js";
74
+ import { handleListMessages } from "../runtime/routes/conversation-routes.js";
66
75
 
67
76
  await initializeDb();
68
77
 
@@ -748,6 +757,188 @@ describe("forkConversation", () => {
748
757
  );
749
758
  });
750
759
 
760
+ test("remaps automatic screenshot refs and widens fork provenance", async () => {
761
+ const screenshotBase64 =
762
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==";
763
+ const source = createConversation("Computer use thread");
764
+ await addMessage(source.id, "user", "Open the example", {
765
+ skipIndexing: true,
766
+ });
767
+ await addMessage(
768
+ source.id,
769
+ "assistant",
770
+ JSON.stringify([
771
+ {
772
+ type: "tool_use",
773
+ id: "computer-use-call",
774
+ name: "computer_use_click",
775
+ input: { x: 10, y: 20 },
776
+ },
777
+ ]),
778
+ { skipIndexing: true },
779
+ );
780
+ const screenshot = await uploadAttachment(
781
+ "computer-use-click.png",
782
+ "image/png",
783
+ screenshotBase64,
784
+ );
785
+ const toolResult = await addMessage(
786
+ source.id,
787
+ "user",
788
+ JSON.stringify([
789
+ {
790
+ type: "tool_result",
791
+ tool_use_id: "computer-use-call",
792
+ content: "Clicked",
793
+ contentBlocks: [
794
+ {
795
+ type: "image",
796
+ source: {
797
+ type: "workspace_ref",
798
+ media_type: "image/png",
799
+ attachmentId: screenshot.id,
800
+ sizeBytes: Buffer.from(screenshotBase64, "base64").byteLength,
801
+ },
802
+ },
803
+ ],
804
+ },
805
+ ]),
806
+ { skipIndexing: true },
807
+ );
808
+ linkAttachmentToMessage(toolResult.id, screenshot.id, 0);
809
+
810
+ const reply = await addMessage(
811
+ source.id,
812
+ "assistant",
813
+ JSON.stringify([{ type: "text", text: "Done." }]),
814
+ { skipIndexing: true },
815
+ );
816
+ linkAttachmentToMessage(reply.id, screenshot.id, 0);
817
+ const explicit = await uploadAttachment(
818
+ "report.pdf",
819
+ "application/pdf",
820
+ "JVBERg==",
821
+ );
822
+ linkAttachmentToMessage(reply.id, explicit.id, 1);
823
+ updateMessageMetadata(reply.id, {
824
+ [COMPUTER_USE_SCREENSHOT_ATTACHMENT_IDS_KEY]: [screenshot.id],
825
+ });
826
+
827
+ const fork = forkConversation({ conversationId: source.id });
828
+ const forkRows = getMessages(fork.id);
829
+ const forkToolResult = forkRows.find(
830
+ (row) =>
831
+ row.role === "user" &&
832
+ JSON.stringify(row.content).includes("tool_result"),
833
+ );
834
+ const forkReply = forkRows.find(
835
+ (row) =>
836
+ row.role === "assistant" &&
837
+ JSON.stringify(row.content).includes("Done."),
838
+ );
839
+ expect(forkToolResult).toBeDefined();
840
+ expect(forkReply).toBeDefined();
841
+
842
+ const forkReplyAttachments = getAttachmentsForMessage(forkReply!.id);
843
+ const clonedScreenshot = forkReplyAttachments.find(
844
+ (attachment) => attachment.originalFilename === "computer-use-click.png",
845
+ );
846
+ const clonedExplicit = forkReplyAttachments.find(
847
+ (attachment) => attachment.originalFilename === "report.pdf",
848
+ );
849
+ expect(clonedScreenshot?.id).toBeDefined();
850
+ expect(clonedScreenshot?.id).not.toBe(screenshot.id);
851
+ expect(clonedExplicit?.id).toBeDefined();
852
+ expect(JSON.stringify(forkToolResult!.content)).toContain(
853
+ `"attachmentId":"${clonedScreenshot!.id}"`,
854
+ );
855
+ expect(JSON.stringify(forkToolResult!.content)).not.toContain(
856
+ `"attachmentId":"${screenshot.id}"`,
857
+ );
858
+ const persistedSourceToolResult = getMessages(source.id).find(
859
+ (row) => row.id === toolResult.id,
860
+ );
861
+ expect(JSON.stringify(persistedSourceToolResult?.content)).toContain(
862
+ `"attachmentId":"${screenshot.id}"`,
863
+ );
864
+ expect(JSON.stringify(persistedSourceToolResult?.content)).not.toContain(
865
+ `"attachmentId":"${clonedScreenshot!.id}"`,
866
+ );
867
+ expect(
868
+ getAttachmentsForMessage(forkToolResult!.id).map(
869
+ (attachment) => attachment.id,
870
+ ),
871
+ ).toEqual([clonedScreenshot!.id]);
872
+
873
+ deleteConversation(source.id);
874
+
875
+ const forkJsonl = readFileSync(
876
+ join(getConversationDirPath(fork.id, fork.createdAt), "messages.jsonl"),
877
+ "utf-8",
878
+ );
879
+ expect(forkJsonl).toContain(
880
+ '"toolResults":[{"content":"Clicked"}],"attachments":["computer-use-click.png"]',
881
+ );
882
+
883
+ const forkMarkerIds = computerUseScreenshotAttachmentIdsFromMetadata(
884
+ parseMetadata(forkReply!.metadata) as Record<string, unknown>,
885
+ );
886
+ expect(forkMarkerIds).toEqual([screenshot.id, clonedScreenshot!.id]);
887
+ expect(forkMarkerIds).not.toContain(clonedExplicit!.id);
888
+
889
+ const history = (await handleListMessages({
890
+ queryParams: { conversationId: fork.id },
891
+ })) as {
892
+ messages: Array<{
893
+ toolCalls?: Array<{ imageAttachmentIds?: string[] }>;
894
+ attachments: Array<{
895
+ id: string;
896
+ computerUseScreenshot?: boolean;
897
+ }>;
898
+ }>;
899
+ };
900
+ expect(
901
+ history.messages.flatMap((message) =>
902
+ (message.toolCalls ?? []).flatMap(
903
+ (toolCall) => toolCall.imageAttachmentIds ?? [],
904
+ ),
905
+ ),
906
+ ).toEqual([clonedScreenshot!.id]);
907
+ const hydratedScreenshot = history.messages
908
+ .flatMap((message) => message.attachments)
909
+ .find((attachment) => attachment.id === clonedScreenshot!.id);
910
+ const hydratedExplicit = history.messages
911
+ .flatMap((message) => message.attachments)
912
+ .find((attachment) => attachment.id === clonedExplicit!.id);
913
+ expect(hydratedScreenshot?.computerUseScreenshot).toBe(true);
914
+ expect(hydratedExplicit?.computerUseScreenshot).toBeUndefined();
915
+ expect(getAttachmentContent(screenshot.id)).toBeNull();
916
+ expect(getAttachmentContent(clonedScreenshot!.id)?.toString("base64")).toBe(
917
+ screenshotBase64,
918
+ );
919
+
920
+ const firstFilesPage = listConversationAttachments(fork.id, {
921
+ limit: 1,
922
+ offset: 0,
923
+ });
924
+ const secondFilesPage = listConversationAttachments(fork.id, {
925
+ limit: 1,
926
+ offset: 1,
927
+ });
928
+ expect(firstFilesPage.total).toBe(2);
929
+ expect(secondFilesPage.total).toBe(2);
930
+ expect(
931
+ [...firstFilesPage.attachments, ...secondFilesPage.attachments].map(
932
+ (attachment) => attachment.id,
933
+ ),
934
+ ).toEqual([clonedScreenshot!.id, clonedExplicit!.id]);
935
+ expect(
936
+ [...firstFilesPage.attachments, ...secondFilesPage.attachments].every(
937
+ (attachment) => attachment.messageId === forkReply!.id,
938
+ ),
939
+ ).toBe(true);
940
+ });
941
+
751
942
  test("inherits the source conversation's inference profile", async () => {
752
943
  const source = createConversation("Pinned profile thread");
753
944
  await addMessage(source.id, "user", "Use the balanced profile", {
@@ -0,0 +1,276 @@
1
+ /**
2
+ * `Conversation.buildCurrentSystemPrompt` captures the delegation-section
3
+ * state the prompt it builds renders (`renderedDelegateIndependentTasks`),
4
+ * which the wire-surface recorder persists for a retrospective fork to
5
+ * replay. The loop sends the prompt built before the run and never rebuilds
6
+ * it mid-run, so the captured state has to be the one that build used: not a
7
+ * re-derivation at send time, and not the state of a build that failed while
8
+ * the loop kept its previous prompt.
9
+ */
10
+ import { beforeEach, describe, expect, mock, test } from "bun:test";
11
+
12
+ import { CompactionCircuit } from "../agent/compaction-circuit.js";
13
+ import type { AgentEvent } from "../agent/loop.js";
14
+ import type { AssistantEvent } from "../api/index.js";
15
+ import type { Message, ProviderResponse } from "../providers/types.js";
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Mocks — must precede Conversation import
19
+ // ---------------------------------------------------------------------------
20
+
21
+ mock.module("../providers/registry.js", () => ({
22
+ getProvider: () => ({ name: "mock-provider" }),
23
+ initializeProviders: async () => {},
24
+ }));
25
+
26
+ mock.module("../config/assistant-feature-flags.js", () => ({
27
+ isAssistantFeatureFlagEnabled: () => true,
28
+ }));
29
+
30
+ // The builder under the conversation's control: records the options each
31
+ // build receives and can be made to throw, which is what a wake's prompt
32
+ // sync swallows before running on the previous prompt.
33
+ let builtWith: Array<Record<string, unknown> | undefined> = [];
34
+ let throwOnBuild = false;
35
+ mock.module("../prompts/system-prompt.js", () => ({
36
+ buildSystemPrompt: (options?: Record<string, unknown>) => {
37
+ if (throwOnBuild) {
38
+ throw new Error("prompt build failed");
39
+ }
40
+ builtWith.push(options);
41
+ return "system prompt";
42
+ },
43
+ }));
44
+
45
+ mock.module("../config/skills.js", () => ({
46
+ loadSkillCatalog: () => [],
47
+ loadSkillBySelector: () => ({ skill: null }),
48
+ ensureSkillIcon: async () => null,
49
+ }));
50
+
51
+ mock.module("../config/skill-state.js", () => ({
52
+ resolveSkillStates: () => [],
53
+ }));
54
+
55
+ mock.module("../permissions/trust-store.js", () => ({
56
+ addRule: () => {},
57
+ findHighestPriorityRule: () => null,
58
+ clearCache: () => {},
59
+ }));
60
+
61
+ mock.module("../security/secret-allowlist.js", () => ({
62
+ resetAllowlist: () => {},
63
+ }));
64
+
65
+ mock.module("../persistence/conversation-crud.js", () => ({
66
+ setConversationProcessingStartedAt: () => {},
67
+ isConversationProcessing: () => false,
68
+ setConversationOriginChannelIfUnset: () => {},
69
+ updateConversationContextWindow: () => {},
70
+ deleteMessageById: () => {},
71
+ provenanceFromTrustContext: () => ({
72
+ source: "user",
73
+ trustContext: undefined,
74
+ }),
75
+ getConversationOriginInterface: () => null,
76
+ getConversationOriginChannel: () => null,
77
+ getMessages: () => [],
78
+ getConversation: () => ({
79
+ id: "conv-1",
80
+ contextSummary: null,
81
+ contextCompactedMessageCount: 0,
82
+ totalInputTokens: 0,
83
+ totalOutputTokens: 0,
84
+ totalEstimatedCost: 0,
85
+ }),
86
+ createConversation: () => ({ id: "conv-1" }),
87
+ addMessage: () => ({ id: `msg-${Date.now()}` }),
88
+ updateConversationUsage: () => {},
89
+ updateConversationTitle: () => {},
90
+ reserveMessage: mock(async () => ({ id: "msg-reserve" })),
91
+ }));
92
+
93
+ mock.module("../persistence/conversation-queries.js", () => ({
94
+ listConversations: () => [],
95
+ }));
96
+
97
+ mock.module("../persistence/attachments-store.js", () => ({
98
+ uploadAttachment: () => ({ id: `att-${Date.now()}` }),
99
+ linkAttachmentToMessage: () => {},
100
+ }));
101
+
102
+ mock.module("../plugins/defaults/compaction/window-manager.js", () => ({
103
+ ContextWindowManager: class {
104
+ estimateInputTokens() {
105
+ return 0;
106
+ }
107
+ get tokenCountInputs() {
108
+ return { systemPrompt: "", tools: undefined };
109
+ }
110
+ constructor() {}
111
+ updateConfig() {}
112
+ shouldCompact() {
113
+ return { needed: false, estimatedTokens: 0 };
114
+ }
115
+ async maybeCompact() {
116
+ return { compacted: false };
117
+ }
118
+ resetOverflowRecovery() {}
119
+ },
120
+ createContextSummaryMessage: () => ({
121
+ role: "user",
122
+ content: [{ type: "text", text: "summary" }],
123
+ }),
124
+ getSummaryFromContextMessage: () => null,
125
+ }));
126
+
127
+ mock.module("../persistence/llm-usage-store.js", () => ({
128
+ recordUsageEvent: () => ({ id: "mock-id", createdAt: Date.now() }),
129
+ listUsageEvents: () => [],
130
+ }));
131
+
132
+ mock.module("../agent/loop.js", () => ({
133
+ AgentLoop: class {
134
+ compactionCircuit = new CompactionCircuit("test-conv");
135
+ constructor() {}
136
+ getToolTokenBudget() {
137
+ return 0;
138
+ }
139
+ getResolvedTools() {
140
+ return [];
141
+ }
142
+ getActiveModel() {
143
+ return undefined;
144
+ }
145
+ setSystemPrompt() {}
146
+ async run(_options: {
147
+ messages: Message[];
148
+ onEvent: (event: AgentEvent) => void;
149
+ }): Promise<Message[]> {
150
+ return [];
151
+ }
152
+ },
153
+ }));
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // Import Conversation AFTER mocks
157
+ // ---------------------------------------------------------------------------
158
+
159
+ import { Conversation } from "../daemon/conversation.js";
160
+
161
+ // ---------------------------------------------------------------------------
162
+ // Helpers
163
+ // ---------------------------------------------------------------------------
164
+
165
+ function makeProvider() {
166
+ return {
167
+ name: "mock",
168
+ async sendMessage(): Promise<ProviderResponse> {
169
+ return {
170
+ content: [],
171
+ model: "mock",
172
+ usage: { inputTokens: 0, outputTokens: 0 },
173
+ stopReason: "end_turn",
174
+ };
175
+ },
176
+ };
177
+ }
178
+
179
+ let conversationSeq = 0;
180
+
181
+ /**
182
+ * A conversation whose prompt is built, or, given a prompt other than the
183
+ * builder's, one running on a verbatim system-prompt override.
184
+ */
185
+ function makeConversation(systemPrompt = "system prompt"): Conversation {
186
+ conversationSeq += 1;
187
+ return new Conversation(
188
+ `conv-rendered-delegation-${conversationSeq}`,
189
+ makeProvider(),
190
+ systemPrompt,
191
+ (_msg: AssistantEvent) => {},
192
+ "/tmp",
193
+ { maxTokens: 4096 },
194
+ );
195
+ }
196
+
197
+ function lastBuildDelegation(): unknown {
198
+ return builtWith.at(-1)?.delegateIndependentTasks;
199
+ }
200
+
201
+ beforeEach(() => {
202
+ builtWith = [];
203
+ throwOnBuild = false;
204
+ });
205
+
206
+ // ---------------------------------------------------------------------------
207
+ // Tests
208
+ // ---------------------------------------------------------------------------
209
+
210
+ describe("Conversation.buildCurrentSystemPrompt delegation-state capture", () => {
211
+ test("captures the state the build rendered and hands the builder the same value", () => {
212
+ const conversation = makeConversation();
213
+ expect(conversation.renderedDelegateIndependentTasks).toBeUndefined();
214
+
215
+ conversation.buildCurrentSystemPrompt();
216
+
217
+ // An unrestricted turn can spawn, so the section renders on.
218
+ expect(conversation.renderedDelegateIndependentTasks).toBe(true);
219
+ expect(lastBuildDelegation()).toBe(true);
220
+ });
221
+
222
+ test("a change to the gate's inputs after the build does not touch the captured state until the next build", () => {
223
+ const conversation = makeConversation();
224
+ conversation.buildCurrentSystemPrompt();
225
+ expect(conversation.renderedDelegateIndependentTasks).toBe(true);
226
+
227
+ // The loop is still sending the prompt built above, so the recorder must
228
+ // see that prompt's state, whatever the gate would derive now.
229
+ conversation.setSubagentAllowedTools(new Set(["remember"]));
230
+ expect(conversation.renderedDelegateIndependentTasks).toBe(true);
231
+
232
+ conversation.buildCurrentSystemPrompt();
233
+ expect(conversation.renderedDelegateIndependentTasks).toBe(false);
234
+ expect(lastBuildDelegation()).toBe(false);
235
+ });
236
+
237
+ test("a failed build leaves the previously captured state in place", () => {
238
+ const conversation = makeConversation();
239
+ conversation.buildCurrentSystemPrompt();
240
+ expect(conversation.renderedDelegateIndependentTasks).toBe(true);
241
+
242
+ // A wake's prompt sync swallows this and runs on the previous prompt,
243
+ // whose section is still on, even though a rebuild would now render it
244
+ // off.
245
+ conversation.setSubagentAllowedTools(new Set(["remember"]));
246
+ throwOnBuild = true;
247
+ expect(() => conversation.buildCurrentSystemPrompt()).toThrow(
248
+ "prompt build failed",
249
+ );
250
+
251
+ expect(conversation.renderedDelegateIndependentTasks).toBe(true);
252
+ });
253
+
254
+ test("a verbatim system-prompt override captures unknown", () => {
255
+ const conversation = makeConversation("override prompt");
256
+ builtWith = [];
257
+
258
+ expect(conversation.buildCurrentSystemPrompt()).toBe("override prompt");
259
+
260
+ expect(conversation.renderedDelegateIndependentTasks).toBeNull();
261
+ // The override is returned as is; nothing is built.
262
+ expect(builtWith).toEqual([]);
263
+ });
264
+
265
+ test("a replaying wake's recorded state wins over the derivation", () => {
266
+ const conversation = makeConversation();
267
+ // Replaying a channel-sourced conversation that recorded the section off,
268
+ // on a wake whose own scope could spawn.
269
+ conversation.delegateIndependentTasksReplay = false;
270
+
271
+ conversation.buildCurrentSystemPrompt();
272
+
273
+ expect(conversation.renderedDelegateIndependentTasks).toBe(false);
274
+ expect(lastBuildDelegation()).toBe(false);
275
+ });
276
+ });
@@ -20,6 +20,7 @@ import { tmpdir } from "node:os";
20
20
  import { join, resolve } from "node:path";
21
21
  import { describe, expect, test } from "bun:test";
22
22
 
23
+ import { resolveIpcEndpoint } from "@vellumai/ipc-server-utils";
23
24
  import { CES_PROTOCOL_VERSION } from "@vellumai/service-contracts/credential-rpc";
24
25
 
25
26
  import {
@@ -79,44 +80,83 @@ function createMockTransport(): CesTransport & {
79
80
  }
80
81
 
81
82
  // ---------------------------------------------------------------------------
82
- // Managed discovery — fail closed when socket is missing or handshake fails
83
+ // CES discovery — CES_BOOTSTRAP_SOCKET_DIR, not workspace
83
84
  // ---------------------------------------------------------------------------
84
85
 
86
+ function withBootstrapDir(dir: string): () => void {
87
+ const savedDir = process.env["CES_BOOTSTRAP_SOCKET_DIR"];
88
+ process.env["CES_BOOTSTRAP_SOCKET_DIR"] = dir;
89
+ return () => {
90
+ if (savedDir !== undefined) {
91
+ process.env["CES_BOOTSTRAP_SOCKET_DIR"] = savedDir;
92
+ } else {
93
+ delete process.env["CES_BOOTSTRAP_SOCKET_DIR"];
94
+ }
95
+ };
96
+ }
97
+
85
98
  describe("managed CES discovery", () => {
86
- test("returns unavailable when bootstrap socket does not exist", async () => {
87
- // In a non-containerized test environment, the managed socket path
88
- // does not exist. Verify fail-closed behavior.
89
- const saved = process.env["CES_BOOTSTRAP_SOCKET"];
99
+ test("returns unavailable when bootstrap socket does not exist", () => {
100
+ const bootstrapDir = mkdtempSync(join(tmpdir(), "ces-missing-"));
101
+ const restore = withBootstrapDir(bootstrapDir);
90
102
  try {
91
- // Point at a non-existent path to ensure predictable behavior
92
- process.env["CES_BOOTSTRAP_SOCKET"] = "/tmp/ces-test-nonexistent.sock";
93
- const result = await discoverManagedCes();
103
+ const result = discoverManagedCes();
94
104
  expect(result.mode).toBe("unavailable");
95
105
  expect((result as { reason: string }).reason).toContain(
96
106
  "CES bootstrap socket not found",
97
107
  );
98
108
  } finally {
99
- if (saved !== undefined) {
100
- process.env["CES_BOOTSTRAP_SOCKET"] = saved;
101
- } else {
102
- delete process.env["CES_BOOTSTRAP_SOCKET"];
103
- }
109
+ restore();
110
+ rmSync(bootstrapDir, { recursive: true, force: true });
104
111
  }
105
112
  });
106
113
 
107
- test("never returns a fallback or in-process mode", async () => {
108
- const saved = process.env["CES_BOOTSTRAP_SOCKET"];
114
+ test("never returns a fallback or in-process mode", () => {
115
+ const bootstrapDir = mkdtempSync(join(tmpdir(), "ces-missing-"));
116
+ const restore = withBootstrapDir(bootstrapDir);
109
117
  try {
110
- process.env["CES_BOOTSTRAP_SOCKET"] = "/tmp/ces-test-nonexistent.sock";
111
- const result = await discoverManagedCes();
112
- // Must be "managed" or "unavailable" — no fallback modes.
118
+ const result = discoverManagedCes();
113
119
  expect(["managed", "unavailable"]).toContain(result.mode);
114
120
  } finally {
115
- if (saved !== undefined) {
116
- process.env["CES_BOOTSTRAP_SOCKET"] = saved;
117
- } else {
118
- delete process.env["CES_BOOTSTRAP_SOCKET"];
119
- }
121
+ restore();
122
+ rmSync(bootstrapDir, { recursive: true, force: true });
123
+ }
124
+ });
125
+ });
126
+
127
+ describe("CES bootstrap socket discovery", () => {
128
+ test("looks for the socket under CES_BOOTSTRAP_SOCKET_DIR", () => {
129
+ const bootstrapDir = mkdtempSync(join(tmpdir(), "ces-bootstrap-"));
130
+ const restore = withBootstrapDir(bootstrapDir);
131
+ try {
132
+ const socketPath = resolveIpcEndpoint("ces", {
133
+ workspaceDir: bootstrapDir,
134
+ }).path;
135
+ const result = discoverManagedCes();
136
+ expect(result.mode).toBe("unavailable");
137
+ expect((result as { reason: string }).reason).toContain(socketPath);
138
+ } finally {
139
+ restore();
140
+ rmSync(bootstrapDir, { recursive: true, force: true });
141
+ }
142
+ });
143
+
144
+ test("does not discover CES from VELLUM_WORKSPACE_DIR", () => {
145
+ const bootstrapDir = mkdtempSync(join(tmpdir(), "ces-bootstrap-"));
146
+ const restore = withBootstrapDir(bootstrapDir);
147
+ try {
148
+ const result = discoverManagedCes();
149
+ const workspaceDir = process.env.VELLUM_WORKSPACE_DIR;
150
+ expect(workspaceDir).toBeDefined();
151
+ const workspaceSocket = resolveIpcEndpoint("ces", {
152
+ workspaceDir: workspaceDir!,
153
+ }).path;
154
+ expect((result as { reason: string }).reason).not.toContain(
155
+ workspaceSocket,
156
+ );
157
+ } finally {
158
+ restore();
159
+ rmSync(bootstrapDir, { recursive: true, force: true });
120
160
  }
121
161
  });
122
162
  });
@@ -126,30 +166,9 @@ describe("managed CES discovery", () => {
126
166
  // ---------------------------------------------------------------------------
127
167
 
128
168
  describe("discoverCesWithRetry", () => {
129
- function withManagedEnv(socketPath: string): () => void {
130
- const savedSocket = process.env["CES_BOOTSTRAP_SOCKET"];
131
- const savedSocketDir = process.env["CES_BOOTSTRAP_SOCKET_DIR"];
132
- const savedContainerized = process.env["IS_CONTAINERIZED"];
133
- delete process.env["CES_BOOTSTRAP_SOCKET_DIR"];
134
- process.env["CES_BOOTSTRAP_SOCKET"] = socketPath;
135
- process.env["IS_CONTAINERIZED"] = "true";
136
- return () => {
137
- const restore = (key: string, value: string | undefined) => {
138
- if (value !== undefined) {
139
- process.env[key] = value;
140
- } else {
141
- delete process.env[key];
142
- }
143
- };
144
- restore("CES_BOOTSTRAP_SOCKET", savedSocket);
145
- restore("CES_BOOTSTRAP_SOCKET_DIR", savedSocketDir);
146
- restore("IS_CONTAINERIZED", savedContainerized);
147
- };
148
- }
149
-
150
169
  test("returns unavailable after polling when the socket never appears", async () => {
151
- const socketPath = join(tmpdir(), `ces-retry-missing-${Date.now()}.sock`);
152
- const restore = withManagedEnv(socketPath);
170
+ const bootstrapDir = mkdtempSync(join(tmpdir(), "ces-retry-missing-"));
171
+ const restore = withBootstrapDir(bootstrapDir);
153
172
  try {
154
173
  const start = Date.now();
155
174
  const result = await discoverCesWithRetry({
@@ -157,22 +176,19 @@ describe("discoverCesWithRetry", () => {
157
176
  intervalMs: 20,
158
177
  });
159
178
  expect(result.mode).toBe("unavailable");
160
- // It must actually have waited rather than failing on the first probe.
161
179
  expect(Date.now() - start).toBeGreaterThanOrEqual(150);
162
180
  } finally {
163
181
  restore();
182
+ rmSync(bootstrapDir, { recursive: true, force: true });
164
183
  }
165
184
  });
166
185
 
167
186
  test("resolves to managed once the socket is re-bound mid-poll", async () => {
168
187
  const dir = mkdtempSync(join(tmpdir(), "ces-retry-"));
169
- const socketPath = join(dir, "ces.sock");
170
- const restore = withManagedEnv(socketPath);
188
+ const socketPath = resolveIpcEndpoint("ces", { workspaceDir: dir }).path;
189
+ const restore = withBootstrapDir(dir);
190
+ const timer = setTimeout(() => writeFileSync(socketPath, ""), 80);
171
191
  try {
172
- // Simulate the sidecar re-binding its bootstrap socket shortly after
173
- // the assistant begins probing.
174
- setTimeout(() => writeFileSync(socketPath, ""), 80);
175
-
176
192
  const result = await discoverCesWithRetry({
177
193
  timeoutMs: 2_000,
178
194
  intervalMs: 20,
@@ -180,6 +196,7 @@ describe("discoverCesWithRetry", () => {
180
196
  expect(result.mode).toBe("managed");
181
197
  expect((result as { socketPath: string }).socketPath).toBe(socketPath);
182
198
  } finally {
199
+ clearTimeout(timer);
183
200
  restore();
184
201
  rmSync(dir, { recursive: true, force: true });
185
202
  }