@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
@@ -17,7 +17,7 @@ import {
17
17
  } from "node:fs";
18
18
  import { basename, dirname, extname, join } from "node:path";
19
19
 
20
- import { eq } from "drizzle-orm";
20
+ import { and, eq, inArray } from "drizzle-orm";
21
21
  import { v4 as uuid } from "uuid";
22
22
 
23
23
  import {
@@ -31,7 +31,7 @@ import { getWorkspaceDir } from "../util/platform.js";
31
31
  import { getConversationAttachmentsDirPath } from "./conversation-directories.js";
32
32
  import { getDb } from "./db-connection.js";
33
33
  import { rawAll, rawGet, rawRun } from "./raw-query.js";
34
- import { attachments, messageAttachments } from "./schema.js";
34
+ import { attachments, messageAttachments, messages } from "./schema.js";
35
35
 
36
36
  export interface StoredAttachment {
37
37
  id: string;
@@ -206,16 +206,23 @@ function insertMessageAttachmentLink(
206
206
  attachmentId: string,
207
207
  position: number,
208
208
  ): void {
209
- const db = getDb();
210
- db.insert(messageAttachments)
211
- .values({
212
- id: uuid(),
213
- messageId,
214
- attachmentId,
215
- position,
216
- createdAt: Date.now(),
217
- })
218
- .run();
209
+ rawRun(
210
+ "attachments:insertMessageAttachmentLink",
211
+ `INSERT INTO message_attachments
212
+ (id, message_id, attachment_id, position, created_at)
213
+ SELECT ?, ?, ?, ?, ?
214
+ WHERE NOT EXISTS (
215
+ SELECT 1 FROM message_attachments
216
+ WHERE message_id = ? AND attachment_id = ?
217
+ )`,
218
+ uuid(),
219
+ messageId,
220
+ attachmentId,
221
+ position,
222
+ Date.now(),
223
+ messageId,
224
+ attachmentId,
225
+ );
219
226
  }
220
227
 
221
228
  function persistAttachmentFilePath(
@@ -1256,6 +1263,20 @@ export function linkAttachmentToMessage(
1256
1263
  throw new Error(`Message not found: ${messageId}`);
1257
1264
  }
1258
1265
 
1266
+ const existing = getDb()
1267
+ .select({ attachmentId: messageAttachments.attachmentId })
1268
+ .from(messageAttachments)
1269
+ .where(
1270
+ and(
1271
+ eq(messageAttachments.messageId, messageId),
1272
+ eq(messageAttachments.attachmentId, attachmentId),
1273
+ ),
1274
+ )
1275
+ .get();
1276
+ if (existing) {
1277
+ return existing.attachmentId;
1278
+ }
1279
+
1259
1280
  const scopedAttachmentId = scopeAttachmentToConversation(
1260
1281
  attachmentId,
1261
1282
  ctx.conversationId,
@@ -1301,41 +1322,60 @@ export function getAttachmentsForMessage(
1301
1322
  export function getAttachmentMetadataForMessage(
1302
1323
  messageId: string,
1303
1324
  ): StoredAttachment[] {
1304
- const db = getDb();
1305
- const links = db
1306
- .select({ attachmentId: messageAttachments.attachmentId })
1307
- .from(messageAttachments)
1308
- .where(eq(messageAttachments.messageId, messageId))
1309
- .orderBy(messageAttachments.position)
1310
- .all();
1325
+ return getAttachmentMetadataForMessages([messageId]).map(
1326
+ (row) => row.attachment,
1327
+ );
1328
+ }
1311
1329
 
1312
- if (links.length === 0) {
1313
- return [];
1314
- }
1330
+ export interface MessageAttachmentMetadata {
1331
+ messageId: string;
1332
+ messageMetadata: string | null;
1333
+ attachment: StoredAttachment;
1334
+ }
1315
1335
 
1316
- const results: StoredAttachment[] = [];
1317
- for (const link of links) {
1318
- if (!link.attachmentId) {
1319
- continue;
1320
- }
1321
- const row = db
1322
- .select({
1323
- id: attachments.id,
1324
- originalFilename: attachments.originalFilename,
1325
- mimeType: attachments.mimeType,
1326
- sizeBytes: attachments.sizeBytes,
1327
- kind: attachments.kind,
1328
- thumbnailBase64: attachments.thumbnailBase64,
1329
- createdAt: attachments.createdAt,
1330
- })
1331
- .from(attachments)
1332
- .where(eq(attachments.id, link.attachmentId))
1333
- .get();
1334
- if (row) {
1335
- results.push(row);
1336
- }
1336
+ /**
1337
+ * Return linked attachment metadata together with the metadata of each source
1338
+ * message. Rows follow the requested message order, then link position.
1339
+ */
1340
+ export function getAttachmentMetadataForMessages(
1341
+ messageIds: string[],
1342
+ ): MessageAttachmentMetadata[] {
1343
+ if (messageIds.length === 0) {
1344
+ return [];
1337
1345
  }
1338
- return results;
1346
+ const db = getDb();
1347
+ const rows = db
1348
+ .select({
1349
+ messageId: messageAttachments.messageId,
1350
+ messageMetadata: messages.metadata,
1351
+ position: messageAttachments.position,
1352
+ id: attachments.id,
1353
+ originalFilename: attachments.originalFilename,
1354
+ mimeType: attachments.mimeType,
1355
+ sizeBytes: attachments.sizeBytes,
1356
+ kind: attachments.kind,
1357
+ thumbnailBase64: attachments.thumbnailBase64,
1358
+ createdAt: attachments.createdAt,
1359
+ })
1360
+ .from(messageAttachments)
1361
+ .innerJoin(messages, eq(messages.id, messageAttachments.messageId))
1362
+ .innerJoin(attachments, eq(attachments.id, messageAttachments.attachmentId))
1363
+ .where(inArray(messageAttachments.messageId, messageIds))
1364
+ .all();
1365
+ const messageOrder = new Map(messageIds.map((id, index) => [id, index]));
1366
+ rows.sort((a, b) => {
1367
+ const messageDelta =
1368
+ (messageOrder.get(a.messageId) ?? 0) -
1369
+ (messageOrder.get(b.messageId) ?? 0);
1370
+ return messageDelta !== 0 ? messageDelta : a.position - b.position;
1371
+ });
1372
+ return rows.map(
1373
+ ({ messageId, messageMetadata, position: _position, ...a }) => ({
1374
+ messageId,
1375
+ messageMetadata,
1376
+ attachment: a,
1377
+ }),
1378
+ );
1339
1379
  }
1340
1380
 
1341
1381
  /**
@@ -94,6 +94,8 @@ import {
94
94
  import { deleteConversationRowsInBatches } from "./conversation-row-batch-delete.js";
95
95
  import {
96
96
  BACKGROUND_CONVERSATION_TYPES,
97
+ COMPUTER_USE_SCREENSHOT_ATTACHMENT_IDS_KEY,
98
+ computerUseScreenshotAttachmentIdsFromMetadata,
97
99
  type ConversationCreateType,
98
100
  type ConversationOrigin,
99
101
  isHiddenMessageMetadata,
@@ -413,6 +415,9 @@ export const messageMetadataSchema = z
413
415
  * channel/interface fields cannot stand in for it.
414
416
  */
415
417
  voiceSessionTurn: z.boolean().optional(),
418
+ [COMPUTER_USE_SCREENSHOT_ATTACHMENT_IDS_KEY]: z
419
+ .array(z.string())
420
+ .optional(),
416
421
  /**
417
422
  * Discriminates daemon-authored rows from ordinary turns.
418
423
  * `"system_card"` marks pre-composed status cards (the /compact, /clean,
@@ -1849,7 +1854,25 @@ function populateForkContentsInProcess(args: PopulateForkContentsArgs): void {
1849
1854
  });
1850
1855
  }
1851
1856
 
1852
- widenForkSightFrameTags(messagesToCopy, forkedMessageIds, attachmentIdMap);
1857
+ remapForkWorkspaceAttachmentRefs(
1858
+ messagesToCopy,
1859
+ forkedMessageIds,
1860
+ attachmentIdMap,
1861
+ );
1862
+ widenForkAttachmentIdTags(
1863
+ messagesToCopy,
1864
+ forkedMessageIds,
1865
+ attachmentIdMap,
1866
+ SIGHT_FRAME_ATTACHMENT_IDS_KEY,
1867
+ sightFrameAttachmentIdsFromMetadata,
1868
+ );
1869
+ widenForkAttachmentIdTags(
1870
+ messagesToCopy,
1871
+ forkedMessageIds,
1872
+ attachmentIdMap,
1873
+ COMPUTER_USE_SCREENSHOT_ATTACHMENT_IDS_KEY,
1874
+ computerUseScreenshotAttachmentIdsFromMetadata,
1875
+ );
1853
1876
 
1854
1877
  // Set lastMessageAt to the max createdAt of copied messages so the
1855
1878
  // forked conversation sorts correctly by message recency.
@@ -1895,31 +1918,99 @@ function populateForkContentsInProcess(args: PopulateForkContentsArgs): void {
1895
1918
  }
1896
1919
 
1897
1920
  /**
1898
- * Extend the copied rows' camera-frame tags to name the fork's cloned
1899
- * attachment ids alongside the source ids they were written with.
1921
+ * Remap copied workspace references to the fork-scoped attachment ids created
1922
+ * by the relink loop.
1900
1923
  *
1901
- * A fork leaves its rows describing their attachments two different ways:
1902
- * `messages.content` is copied byte for byte and still names the SOURCE
1903
- * attachment ids, while `message_attachments` is re-linked to freshly CLONED
1904
- * rows under new ids. Readers split along that seam. Camera-frame retention
1905
- * matches the tag against the ids in the content blocks (source ids), and the
1906
- * compactor builds its image manifest from the links (cloned ids) and stamps
1907
- * those onto the frames it rebuilds. A tag naming only one vocabulary goes
1908
- * blind on the other, so it names both.
1924
+ * The same source attachment can be linked to several copied messages. The
1925
+ * shared map keeps every copied reference and message link on one cloned row,
1926
+ * while references to attachments outside the copied window stay unchanged.
1927
+ */
1928
+ function remapForkWorkspaceAttachmentRefs(
1929
+ messagesToCopy: MessageRow[],
1930
+ forkedMessageIds: Map<string, string>,
1931
+ attachmentIdMap: Map<string, string>,
1932
+ ): void {
1933
+ if (attachmentIdMap.size === 0) {
1934
+ return;
1935
+ }
1936
+
1937
+ const db = getDb();
1938
+ for (const message of messagesToCopy) {
1939
+ const forkedMessageId = forkedMessageIds.get(message.id);
1940
+ if (!forkedMessageId) {
1941
+ continue;
1942
+ }
1943
+
1944
+ const remappedContent = remapWorkspaceAttachmentRefs(
1945
+ message.content,
1946
+ attachmentIdMap,
1947
+ );
1948
+ if (remappedContent === message.content) {
1949
+ continue;
1950
+ }
1951
+
1952
+ db.update(messages)
1953
+ .set({ content: JSON.stringify(remappedContent) })
1954
+ .where(eq(messages.id, forkedMessageId))
1955
+ .run();
1956
+ }
1957
+ }
1958
+
1959
+ function remapWorkspaceAttachmentRefs(
1960
+ value: unknown,
1961
+ attachmentIdMap: ReadonlyMap<string, string>,
1962
+ ): unknown {
1963
+ if (Array.isArray(value)) {
1964
+ let changed = false;
1965
+ const remapped = value.map((entry) => {
1966
+ const next = remapWorkspaceAttachmentRefs(entry, attachmentIdMap);
1967
+ changed ||= next !== entry;
1968
+ return next;
1969
+ });
1970
+ return changed ? remapped : value;
1971
+ }
1972
+
1973
+ if (value == null || typeof value !== "object") {
1974
+ return value;
1975
+ }
1976
+
1977
+ const record = value as Record<string, unknown>;
1978
+ let changed = false;
1979
+ const remapped: Record<string, unknown> = {};
1980
+ for (const [key, entry] of Object.entries(record)) {
1981
+ let next = remapWorkspaceAttachmentRefs(entry, attachmentIdMap);
1982
+ if (
1983
+ key === "attachmentId" &&
1984
+ record.type === "workspace_ref" &&
1985
+ typeof entry === "string"
1986
+ ) {
1987
+ next = attachmentIdMap.get(entry) ?? entry;
1988
+ }
1989
+ changed ||= next !== entry;
1990
+ remapped[key] = next;
1991
+ }
1992
+ return changed ? remapped : value;
1993
+ }
1994
+
1995
+ /**
1996
+ * Extend a copied row's attachment-id metadata to name cloned ids alongside
1997
+ * the source ids it was written with.
1909
1998
  *
1910
- * Widening rather than remapping is deliberate: replacing the source ids would
1911
- * fix the compactor's frames by breaking every frame the fork holds directly,
1912
- * which is the common case. Extra ids are inert, since an id no block carries
1913
- * simply never matches.
1999
+ * Copied message content uses the fork-scoped ids, while metadata begins as a
2000
+ * copy of the source row. Naming both ids preserves compatibility with readers
2001
+ * of either vocabulary. Extra ids are inert because an id no block or linked
2002
+ * attachment carries simply never matches.
1914
2003
  *
1915
2004
  * Runs after the attachment loop because that loop is what produces the id map.
1916
2005
  * Only rows that actually carry a tag are rewritten, so an ordinary fork does
1917
2006
  * no extra writes.
1918
2007
  */
1919
- function widenForkSightFrameTags(
2008
+ function widenForkAttachmentIdTags(
1920
2009
  messagesToCopy: MessageRow[],
1921
2010
  forkedMessageIds: Map<string, string>,
1922
2011
  attachmentIdMap: Map<string, string>,
2012
+ metadataKey: string,
2013
+ readIds: (metadata: Record<string, unknown> | null | undefined) => string[],
1923
2014
  ): void {
1924
2015
  if (attachmentIdMap.size === 0) {
1925
2016
  return;
@@ -1931,7 +2022,7 @@ function widenForkSightFrameTags(
1931
2022
  continue;
1932
2023
  }
1933
2024
  const sourceMetadata = parseMessageMetadata(message.metadata);
1934
- const sourceIds = sightFrameAttachmentIdsFromMetadata(sourceMetadata);
2025
+ const sourceIds = readIds(sourceMetadata);
1935
2026
  if (sourceIds.length === 0) {
1936
2027
  continue;
1937
2028
  }
@@ -1955,7 +2046,7 @@ function widenForkSightFrameTags(
1955
2046
  .set({
1956
2047
  metadata: JSON.stringify({
1957
2048
  ...(forkedMetadata ?? {}),
1958
- [SIGHT_FRAME_ATTACHMENT_IDS_KEY]: [...widened],
2049
+ [metadataKey]: [...widened],
1959
2050
  }),
1960
2051
  })
1961
2052
  .where(eq(messages.id, forkedMessageId))
@@ -2806,8 +2897,8 @@ export interface ConversationAttachmentListing {
2806
2897
  * Driven from `messages` so the lineage predicate rides
2807
2898
  * `idx_messages_conversation_created_at`. An attachment linked to more than
2808
2899
  * one row is listed once, on the newest row that carries it. Tool-result rows
2809
- * are left out: the transcript never shows them, and the assistant row carries
2810
- * the promoted copy of every image a tool produced.
2900
+ * are left out: reply-linked output belongs in Files, while tool-result-only
2901
+ * media stays available through tool history.
2811
2902
  *
2812
2903
  * The lineage-wide select still reads every linked row: an exact `total` and
2813
2904
  * the metadata-derived flags are only known after the role and visibility
@@ -1,3 +1,4 @@
1
+ import type { ToolDefinition } from "../providers/types.js";
1
2
  import type {
2
3
  AddMessageOptions,
3
4
  ConversationRow,
@@ -5,6 +6,7 @@ import type {
5
6
  MessageRow,
6
7
  } from "./conversation-crud.js";
7
8
  import type { ArchiveStatusFilter } from "./conversation-queries.js";
9
+ import type { ConversationToolSurface } from "./conversation-tool-surface.js";
8
10
  import type { ConversationType } from "./conversation-types.js";
9
11
 
10
12
  /**
@@ -52,6 +54,34 @@ export async function getMessages(
52
54
  return fn(conversationId);
53
55
  }
54
56
 
57
+ /**
58
+ * What the conversation's most recent live turn sent to the provider that a
59
+ * replaying fork reproduces verbatim (its tool definitions and the
60
+ * delegation-section state of its system prompt), or `null` when none has
61
+ * been recorded.
62
+ */
63
+ export async function getRecordedConversationToolSurface(
64
+ conversationId: string,
65
+ ): Promise<ConversationToolSurface | null> {
66
+ const { getConversationToolSurface: fn } =
67
+ await import("./conversation-tool-surface.js");
68
+ return fn(conversationId);
69
+ }
70
+
71
+ /**
72
+ * The tool definitions the conversation's most recent live turn sent to the
73
+ * provider, or `null` when none has been recorded. A projection of
74
+ * {@link getRecordedConversationToolSurface}: a fork replaying the source's
75
+ * request needs the whole record, whose delegation-section state the system
76
+ * prompt tier of the provider cache prefix depends on.
77
+ */
78
+ export async function getConversationToolSurface(
79
+ conversationId: string,
80
+ ): Promise<ToolDefinition[] | null> {
81
+ const surface = await getRecordedConversationToolSurface(conversationId);
82
+ return surface?.tools ?? null;
83
+ }
84
+
55
85
  /** Whether the conversation currently has a turn in flight. */
56
86
  export async function isConversationProcessing(id: string): Promise<boolean> {
57
87
  const { isConversationProcessing: fn } =
@@ -0,0 +1,135 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import { eq, sql } from "drizzle-orm";
4
+
5
+ import type { ToolDefinition } from "../providers/types.js";
6
+ import { getDb } from "./db-connection.js";
7
+ import { conversationToolSurfaces } from "./schema/index.js";
8
+
9
+ /**
10
+ * Per-conversation record of what the most recent live turn sent to the
11
+ * provider that a fork wake cannot re-derive (`conversation_tool_surfaces`).
12
+ *
13
+ * The provider prompt cache is a byte-exact prefix match over
14
+ * `tools -> system -> messages`, so a background wake that forks a
15
+ * conversation can only reuse the source's cached prefix by sending the SAME
16
+ * tools array under the SAME system prompt. Re-deriving either on the fork
17
+ * cannot guarantee that: the fork may run in another process (a different
18
+ * tool registry), with no connected clients (different host-tool gates),
19
+ * under a presence the persisted message stamps do not encode, or under a
20
+ * wake scope that cannot spawn subagents where the source could (the system
21
+ * prompt's delegation section). Recording the resolved values and replaying
22
+ * them verbatim can.
23
+ */
24
+
25
+ /** What a live turn sent that a replaying fork reproduces verbatim. */
26
+ export interface ConversationToolSurface {
27
+ /** The tool definitions the turn sent, exactly as resolved. */
28
+ tools: ToolDefinition[];
29
+ /**
30
+ * Whether the turn's system prompt rendered the parallel-delegation
31
+ * section (`01-parallel-tasks`), which the prompt derives from the same
32
+ * resolved tool surface plus the turn's channel. `null` when unknown: the
33
+ * turn ran on a system-prompt override, or the row predates the column.
34
+ */
35
+ delegateIndependentTasks: boolean | null;
36
+ }
37
+
38
+ /** Content hash of the serialized surface. */
39
+ export function hashConversationToolSurface(
40
+ surface: ConversationToolSurface,
41
+ ): string {
42
+ return hashSurface(
43
+ JSON.stringify(surface.tools),
44
+ surface.delegateIndependentTasks,
45
+ );
46
+ }
47
+
48
+ function hashSurface(
49
+ toolsJson: string,
50
+ delegateIndependentTasks: boolean | null,
51
+ ): string {
52
+ const marker =
53
+ delegateIndependentTasks === null
54
+ ? ""
55
+ : delegateIndependentTasks
56
+ ? "1"
57
+ : "0";
58
+ return createHash("sha256")
59
+ .update(toolsJson)
60
+ .update("\n")
61
+ .update(marker)
62
+ .digest("hex")
63
+ .slice(0, 32);
64
+ }
65
+
66
+ /**
67
+ * Persist `surface` as the conversation's current wire surface and return
68
+ * its hash. `knownHash` is the hash the caller last recorded for this
69
+ * conversation (undefined when it has recorded nothing this process
70
+ * lifetime): a matching hash skips the statement outright. Otherwise the
71
+ * upsert only rewrites a row whose stored hash differs, so a conversation
72
+ * reloaded from disk never rewrites an unchanged surface either.
73
+ */
74
+ export function recordConversationToolSurface(
75
+ conversationId: string,
76
+ surface: ConversationToolSurface,
77
+ knownHash?: string,
78
+ ): string {
79
+ const toolsJson = JSON.stringify(surface.tools);
80
+ const { delegateIndependentTasks } = surface;
81
+ const toolsHash = hashSurface(toolsJson, delegateIndependentTasks);
82
+ if (knownHash === toolsHash) {
83
+ return toolsHash;
84
+ }
85
+ const updatedAt = Date.now();
86
+ getDb()
87
+ .insert(conversationToolSurfaces)
88
+ .values({
89
+ conversationId,
90
+ toolsJson,
91
+ toolsHash,
92
+ delegateIndependentTasks,
93
+ updatedAt,
94
+ })
95
+ .onConflictDoUpdate({
96
+ target: conversationToolSurfaces.conversationId,
97
+ set: { toolsJson, toolsHash, delegateIndependentTasks, updatedAt },
98
+ setWhere: sql`${conversationToolSurfaces.toolsHash} <> excluded.tools_hash`,
99
+ })
100
+ .run();
101
+ return toolsHash;
102
+ }
103
+
104
+ /**
105
+ * The surface the conversation's most recent live turn sent, or `null` when
106
+ * no turn has recorded one (or the stored JSON is unreadable).
107
+ */
108
+ export function getConversationToolSurface(
109
+ conversationId: string,
110
+ ): ConversationToolSurface | null {
111
+ const row = getDb()
112
+ .select({
113
+ toolsJson: conversationToolSurfaces.toolsJson,
114
+ delegateIndependentTasks:
115
+ conversationToolSurfaces.delegateIndependentTasks,
116
+ })
117
+ .from(conversationToolSurfaces)
118
+ .where(eq(conversationToolSurfaces.conversationId, conversationId))
119
+ .get();
120
+ if (!row) {
121
+ return null;
122
+ }
123
+ try {
124
+ const parsed: unknown = JSON.parse(row.toolsJson);
125
+ if (!Array.isArray(parsed)) {
126
+ return null;
127
+ }
128
+ return {
129
+ tools: parsed as ToolDefinition[],
130
+ delegateIndependentTasks: row.delegateIndependentTasks ?? null,
131
+ };
132
+ } catch {
133
+ return null;
134
+ }
135
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import {
4
+ COMPUTER_USE_SCREENSHOT_ATTACHMENT_IDS_KEY,
5
+ computerUseScreenshotAttachmentIdsFromMetadata,
6
+ } from "./conversation-types.js";
7
+
8
+ describe("computerUseScreenshotAttachmentIdsFromMetadata", () => {
9
+ test("returns valid nonempty attachment ids", () => {
10
+ expect(
11
+ computerUseScreenshotAttachmentIdsFromMetadata({
12
+ [COMPUTER_USE_SCREENSHOT_ATTACHMENT_IDS_KEY]: [
13
+ "attachment-1",
14
+ "",
15
+ 42,
16
+ "attachment-2",
17
+ ],
18
+ }),
19
+ ).toEqual(["attachment-1", "attachment-2"]);
20
+ });
21
+
22
+ test("treats absent and malformed metadata as legacy", () => {
23
+ expect(computerUseScreenshotAttachmentIdsFromMetadata(undefined)).toEqual(
24
+ [],
25
+ );
26
+ expect(
27
+ computerUseScreenshotAttachmentIdsFromMetadata({
28
+ [COMPUTER_USE_SCREENSHOT_ATTACHMENT_IDS_KEY]: "attachment-1",
29
+ }),
30
+ ).toEqual([]);
31
+ });
32
+ });
@@ -82,7 +82,10 @@ export function isBackgroundConversationType(
82
82
  * columns into one label.
83
83
  */
84
84
  export type ConversationKind =
85
- "user" | "background" | "background_memory_consolidation" | "scheduled";
85
+ | "user"
86
+ | "background"
87
+ | "background_memory_consolidation"
88
+ | "scheduled";
86
89
 
87
90
  /**
88
91
  * Single classifier shared by the LLM-context routes and the notification
@@ -270,6 +273,10 @@ export function isVoiceSessionUserMessage(
270
273
  */
271
274
  export const SIGHT_FRAME_ATTACHMENT_IDS_KEY = "sightFrameAttachmentIds";
272
275
 
276
+ /** Metadata key naming computer-use screenshots automatically linked to a reply. */
277
+ export const COMPUTER_USE_SCREENSHOT_ATTACHMENT_IDS_KEY =
278
+ "computerUseScreenshotAttachmentIds";
279
+
273
280
  /**
274
281
  * The row's attachments that arrived as ambient camera frames, by attachment
275
282
  * id. A user attachment carries no metadata of its own, so the marking lives on
@@ -291,6 +298,23 @@ export function sightFrameAttachmentIdsFromMetadata(
291
298
  );
292
299
  }
293
300
 
301
+ /**
302
+ * Attachment ids whose placement on this row was automatic computer-use
303
+ * screenshot promotion. Malformed and legacy metadata conservatively yields
304
+ * no marked attachments.
305
+ */
306
+ export function computerUseScreenshotAttachmentIdsFromMetadata(
307
+ metadata: Record<string, unknown> | null | undefined,
308
+ ): string[] {
309
+ const ids = metadata?.[COMPUTER_USE_SCREENSHOT_ATTACHMENT_IDS_KEY];
310
+ if (!Array.isArray(ids)) {
311
+ return [];
312
+ }
313
+ return ids.filter(
314
+ (id): id is string => typeof id === "string" && id.length > 0,
315
+ );
316
+ }
317
+
294
318
  /**
295
319
  * True when a stored `messages.metadata` column belongs to a standalone
296
320
  * ambient camera keep: a row the call's own gate sampled, which no one spoke.
@@ -360,9 +384,7 @@ export function isDesktopOriginatedUserMessage(
360
384
  return false;
361
385
  }
362
386
  const clientOs = parseClientOs((client as Record<string, unknown>).os);
363
- return (
364
- clientOs === "macos" || clientOs === "windows" || clientOs === "linux"
365
- );
387
+ return clientOs === "macos" || clientOs === "windows" || clientOs === "linux";
366
388
  }
367
389
 
368
390
  /**