@vellumai/assistant 0.11.4-staging.2 → 0.11.4-staging.3

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 (189) hide show
  1. package/AGENTS.md +8 -2
  2. package/ARCHITECTURE.md +2 -0
  3. package/docs/architecture/memory.md +15 -0
  4. package/docs/browser-use-architecture-phase2.md +128 -56
  5. package/docs/flux-turn-detection-spike.md +11 -6
  6. package/knip.json +3 -0
  7. package/openapi.yaml +136 -76
  8. package/package.json +1 -1
  9. package/scripts/write-plugin-api-shim.ts +10 -0
  10. package/src/__tests__/app-control-flow.test.ts +1 -0
  11. package/src/__tests__/approval-routes-http.test.ts +2 -2
  12. package/src/__tests__/assistant-feature-flag-guard.test.ts +25 -3
  13. package/src/__tests__/channel-setup-panel-ack.test.ts +1 -1
  14. package/src/__tests__/compaction-events.test.ts +8 -10
  15. package/src/__tests__/conversation-agent-loop.test.ts +4 -1
  16. package/src/__tests__/conversation-confirmation-signals.test.ts +112 -0
  17. package/src/__tests__/conversation-notifiers-provenance.test.ts +1 -1
  18. package/src/__tests__/conversation-queue.test.ts +39 -62
  19. package/src/__tests__/conversation-routes-disk-view.test.ts +1 -1
  20. package/src/__tests__/conversation-routes-enabled-plugins.test.ts +1 -1
  21. package/src/__tests__/conversation-routes-guardian-reply.test.ts +9 -9
  22. package/src/__tests__/conversation-routes-hidden-queue.test.ts +1 -1
  23. package/src/__tests__/conversation-routes-slash-commands.test.ts +1 -1
  24. package/src/__tests__/conversation-slash-queue.test.ts +3 -0
  25. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +1 -0
  26. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +1 -0
  27. package/src/__tests__/conversation-surfaces-app-control.test.ts +1 -0
  28. package/src/__tests__/conversation-surfaces-app-open.test.ts +1 -1
  29. package/src/__tests__/conversation-surfaces-data-persist.test.ts +1 -1
  30. package/src/__tests__/conversation-surfaces-history-restored-completion.test.ts +21 -14
  31. package/src/__tests__/conversation-surfaces-queued-emit.test.ts +1 -0
  32. package/src/__tests__/conversation-surfaces-standalone-payloads.test.ts +1 -0
  33. package/src/__tests__/conversation-surfaces-standalone.test.ts +1 -0
  34. package/src/__tests__/conversation-surfaces-state-update.test.ts +1 -1
  35. package/src/__tests__/conversation-surfaces-table-action.test.ts +1 -1
  36. package/src/__tests__/conversation-surfaces-task-progress.test.ts +1 -1
  37. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +1 -1
  38. package/src/__tests__/conversation-tool-setup-attribution.test.ts +1 -1
  39. package/src/__tests__/cu-unified-flow.test.ts +1 -0
  40. package/src/__tests__/document-sync-tags.test.ts +0 -75
  41. package/src/__tests__/gateway-only-guard.test.ts +2 -5
  42. package/src/__tests__/http-user-message-parity.test.ts +1 -1
  43. package/src/__tests__/init-feature-flag-overrides.test.ts +49 -0
  44. package/src/__tests__/managed-skill-lifecycle.test.ts +7 -0
  45. package/src/__tests__/media-generate-image.test.ts +131 -21
  46. package/src/__tests__/memory-retrieval-hook.test.ts +94 -2
  47. package/src/__tests__/plugin-api-webhook-url.test.ts +10 -7
  48. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +7 -3
  49. package/src/__tests__/proxy-approval-callback.test.ts +1 -0
  50. package/src/__tests__/qdrant-manager.test.ts +14 -1
  51. package/src/__tests__/run-due-schedules.test.ts +21 -0
  52. package/src/__tests__/scaffold-managed-skill-tool.test.ts +187 -18
  53. package/src/__tests__/schedule-routes.test.ts +23 -0
  54. package/src/__tests__/schedule-store.test.ts +17 -0
  55. package/src/__tests__/secret-ingress-http.test.ts +1 -1
  56. package/src/__tests__/send-endpoint-busy.test.ts +3 -3
  57. package/src/__tests__/starter-task-flow.test.ts +5 -4
  58. package/src/__tests__/subagent-fork-prompt-role.test.ts +1 -1
  59. package/src/__tests__/subagent-spawn-and-await.test.ts +4 -7
  60. package/src/__tests__/subagent-tool-gate-mode.test.ts +1 -1
  61. package/src/__tests__/subagent-tools.test.ts +81 -101
  62. package/src/__tests__/surface-completion-in-flight-snapshot.test.ts +1 -0
  63. package/src/__tests__/ui-choice-copy-surfaces.test.ts +1 -1
  64. package/src/__tests__/ui-visual-surface.test.ts +1 -1
  65. package/src/__tests__/ui-voice-picker-surface.test.ts +1 -1
  66. package/src/__tests__/ui-work-result-surface.test.ts +1 -1
  67. package/src/__tests__/voice-scoped-grant-consumer.test.ts +5 -3
  68. package/src/__tests__/voice-session-bridge.test.ts +85 -29
  69. package/src/acp/session-manager.ts +8 -1
  70. package/src/api/surfaces.ts +5 -0
  71. package/src/calls/__tests__/voice-session-bridge.test.ts +21 -10
  72. package/src/calls/__tests__/voice-triage-escalate.test.ts +8 -0
  73. package/src/calls/voice-session-bridge.ts +44 -25
  74. package/src/calls/voice-triage-escalate.ts +1 -0
  75. package/src/cli/bundled-modules.ts +29 -0
  76. package/src/cli/commands/db/repair.ts +4 -8
  77. package/src/cli/commands/domain.ts +6 -3
  78. package/src/cli/commands/email.ts +6 -3
  79. package/src/cli/commands/keys.ts +8 -3
  80. package/src/cli/commands/plugins.ts +85 -36
  81. package/src/cli/commands/schedules.ts +35 -1
  82. package/src/cli/lib/bundled-marketplace.json +1 -1
  83. package/src/config/__tests__/balanced-model-experiment.test.ts +278 -0
  84. package/src/config/assistant-feature-flags.ts +36 -15
  85. package/src/config/balanced-model-experiment.ts +35 -0
  86. package/src/config/bundled-skills/image-studio/SKILL.md +5 -4
  87. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -1
  88. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +101 -0
  89. package/src/config/bundled-skills/skill-management/TOOLS.json +9 -3
  90. package/src/config/bundled-skills/subagent/SKILL.md +17 -12
  91. package/src/config/bundled-skills/subagent/TOOLS.json +4 -4
  92. package/src/config/call-site-defaults.ts +7 -0
  93. package/src/config/default-profile-catalog.ts +96 -4
  94. package/src/config/feature-flag-registry.json +11 -11
  95. package/src/config/skills.ts +9 -2
  96. package/src/daemon/__tests__/conversation-surfaces-launch.test.ts +1 -1
  97. package/src/daemon/conversation-agent-loop.ts +14 -13
  98. package/src/daemon/conversation-notifiers.ts +11 -9
  99. package/src/daemon/conversation-process.ts +0 -27
  100. package/src/daemon/conversation-store.ts +4 -4
  101. package/src/daemon/conversation-surfaces.ts +27 -13
  102. package/src/daemon/conversation-tool-setup.ts +2 -4
  103. package/src/daemon/conversation.ts +81 -44
  104. package/src/daemon/doordash-steps.ts +2 -2
  105. package/src/daemon/lifecycle.ts +14 -1
  106. package/src/daemon/process-message.ts +0 -13
  107. package/src/daemon/windows-compiled-entry.ts +4 -0
  108. package/src/documents/document-store.ts +5 -235
  109. package/src/hooks/types.ts +5 -0
  110. package/src/ipc/gateway-flag-listener.ts +17 -3
  111. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +118 -0
  112. package/src/live-voice/live-voice-manager.ts +16 -3
  113. package/src/live-voice/live-voice-session.ts +63 -9
  114. package/src/live-voice/windows-compiled-live-voice.ts +4 -0
  115. package/src/monitoring/control.ts +1 -0
  116. package/src/monitoring/db-integrity-sample.ts +4 -5
  117. package/src/permissions/prompter.ts +1 -5
  118. package/src/persistence/conversation-queries.ts +66 -16
  119. package/src/persistence/embeddings/qdrant-manager.ts +84 -49
  120. package/src/persistence/migrations/360-add-document-workspace-path.ts +5 -14
  121. package/src/persistence/schema/documents.ts +4 -4
  122. package/src/plugin-api/constants.ts +8 -0
  123. package/src/plugin-api/index.ts +5 -1
  124. package/src/plugin-api/webhook-url.ts +13 -11
  125. package/src/plugins/defaults/main.ts +6 -7
  126. package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +77 -0
  127. package/src/plugins/defaults/memory/graph/conversation-graph-memory.ts +24 -6
  128. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +53 -5
  129. package/src/plugins/defaults/memory/memory-retrospective-job.ts +4 -4
  130. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +18 -0
  131. package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +66 -1
  132. package/src/plugins/defaults/memory/v3/injector.ts +8 -0
  133. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +23 -9
  134. package/src/plugins/defaults/memory/worker-control.ts +1 -0
  135. package/src/plugins/defaults/worker-entrypoints.ts +3 -0
  136. package/src/plugins/mtime-cache.ts +17 -0
  137. package/src/prompts/templates/system-sections.ts +0 -7
  138. package/src/providers/__tests__/context-overflow-error.test.ts +24 -0
  139. package/src/providers/__tests__/retry-callsite.test.ts +20 -0
  140. package/src/providers/openai/chat-completions-provider.ts +11 -1
  141. package/src/providers/speech-to-text/__tests__/deepgram-flux-realtime.test.ts +11 -6
  142. package/src/providers/speech-to-text/deepgram-flux-realtime.ts +9 -49
  143. package/src/routes/control.ts +1 -0
  144. package/src/routes/route-host-client.ts +1 -0
  145. package/src/runtime/AGENTS.md +16 -17
  146. package/src/runtime/agent-wake.ts +15 -12
  147. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +170 -1
  148. package/src/runtime/routes/__tests__/schedule-routes-disarm-reason.test.ts +215 -0
  149. package/src/runtime/routes/conversation-list-routes.ts +54 -22
  150. package/src/runtime/routes/conversation-management-routes.ts +2 -3
  151. package/src/runtime/routes/conversation-routes.ts +11 -13
  152. package/src/runtime/routes/documents-routes.ts +3 -222
  153. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +2 -0
  154. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +3 -0
  155. package/src/runtime/routes/playground/inject-failures.ts +2 -2
  156. package/src/runtime/routes/playground/reset-circuit.ts +1 -1
  157. package/src/runtime/routes/schedule-routes.ts +94 -7
  158. package/src/runtime/routes/workspace-routes.ts +0 -9
  159. package/src/runtime/routes/workspace-utils.ts +3 -13
  160. package/src/runtime/services/conversation-serializer.ts +7 -2
  161. package/src/schedule/__tests__/plugin-schedule-declarations.test.ts +68 -5
  162. package/src/schedule/__tests__/plugin-schedule-reconciler.test.ts +81 -0
  163. package/src/schedule/plugin-schedule-availability.ts +58 -0
  164. package/src/schedule/plugin-schedule-declarations.ts +23 -27
  165. package/src/schedule/plugin-schedule-reconciler.ts +12 -3
  166. package/src/schedule/schedule-store.ts +5 -1
  167. package/src/schedule/scheduler.ts +9 -4
  168. package/src/schedule/worker-control.ts +1 -0
  169. package/src/subagent/__tests__/consult-prompt.test.ts +26 -15
  170. package/src/subagent/consult-context.ts +11 -11
  171. package/src/subagent/consult-prompt.ts +26 -35
  172. package/src/subagent/manager.ts +20 -37
  173. package/src/subagent/notify.ts +7 -1
  174. package/src/subagent/types.ts +8 -6
  175. package/src/tools/acp/spawn.ts +6 -4
  176. package/src/tools/skills/scaffold-managed.ts +25 -7
  177. package/src/tools/subagent/spawn.ts +28 -88
  178. package/src/tools/ui-surface/surface-shape-docs.ts +1 -1
  179. package/src/util/__tests__/worker-process-command.test.ts +37 -0
  180. package/src/util/logger.ts +16 -0
  181. package/src/util/worker-process.ts +37 -4
  182. package/src/windows-compiled-cli.ts +32 -0
  183. package/src/windows-compiled-entry.ts +4 -0
  184. package/src/windows-compiled-logger.ts +6 -0
  185. package/src/windows-compiled-worker-entry.ts +29 -0
  186. package/src/__tests__/document-workspace-file.test.ts +0 -467
  187. package/src/daemon/interactive-turn-sender.ts +0 -59
  188. package/src/subagent/__tests__/consult-transcript.test.ts +0 -184
  189. package/src/subagent/consult-transcript.ts +0 -90
@@ -5,20 +5,18 @@ import { validateInferenceProfileKey } from "../../config/inference-profile-vali
5
5
  import { getConfig } from "../../config/loader.js";
6
6
  import { profileSupportsTools } from "../../config/profile-tool-support.js";
7
7
  import { findConversation } from "../../daemon/conversation-registry.js";
8
- import { getMessages } from "../../persistence/conversation-crud.js";
9
8
  import {
10
9
  countRecentSimilarSpawns,
11
10
  normalizeSpawnObjective,
12
11
  type RecentSimilarSpawns,
13
12
  type SimilarSpawnTally,
14
13
  } from "../../persistence/subagent-store.js";
15
- import type { ContentBlock, Message } from "../../providers/types.js";
14
+ import type { Message } from "../../providers/types.js";
16
15
  import { buildAdvisorContext } from "../../subagent/consult-context.js";
17
16
  import {
18
17
  advisorRequestText,
19
18
  buildAdvisorSystem,
20
19
  } from "../../subagent/consult-prompt.js";
21
- import { sanitizeConsultTranscript } from "../../subagent/consult-transcript.js";
22
20
  import {
23
21
  getSubagentManager,
24
22
  SubagentAbortedError,
@@ -49,7 +47,7 @@ const log = getLogger("subagent-spawn");
49
47
  * reasoning while it works, so a fixed wall-clock ceiling would kill it
50
48
  * mid-thought; an idle window instead fires only when the consult is genuinely
51
49
  * stalled (or never starts). Generous enough to also span time-to-first-token
52
- * over a large inherited transcript.
50
+ * on a slow reasoning profile.
53
51
  */
54
52
  const ADVISOR_IDLE_TIMEOUT_MS = 60_000;
55
53
 
@@ -540,13 +538,19 @@ function inFlightGuardResult(
540
538
  // ── Advisor consult ──────────────────────────────────────────────────
541
539
 
542
540
  /**
543
- * Run the `advisor` role as a synchronous, context-inheriting, stronger-model
544
- * consult and return its guidance as the tool result.
541
+ * Run the `advisor` role as a synchronous, stronger-model consult and return
542
+ * its guidance as the tool result.
545
543
  *
546
- * Inherits the parent transcript (sanitized), frames it as advice via
547
- * `buildAdvisorSystem`, and runs on `llm.advisorProfile` (unless the caller
548
- * passed an explicit `inference_profile`) under both the advisor role allowlist
549
- * and `denySideEffectTools`, so the only tools it can reach are the first-party
544
+ * The consult sees only what it is handed: the spawning agent's own `objective`
545
+ * as a written brief, plus the situational context pack from
546
+ * `buildAdvisorContext`. Nothing of the parent conversation's transcript or
547
+ * system prompt travels with it, so a consult costs the brief rather than a
548
+ * re-prefill of the whole chat at premium rates.
549
+ *
550
+ * It is framed as advice via `buildAdvisorSystem` and runs on
551
+ * `llm.advisorProfile` (unless the caller passed an explicit
552
+ * `inference_profile`) under both the advisor role allowlist and
553
+ * `denySideEffectTools`, so the only tools it can reach are the first-party
550
554
  * built-in readers. It is bounded on two axes, because the consult holds up the
551
555
  * user-facing turn while it runs: a progress-aware deadline (an idle window,
552
556
  * `ADVISOR_IDLE_TIMEOUT_MS`, reset on every streamed token and every tool event
@@ -562,7 +566,7 @@ function inFlightGuardResult(
562
566
  async function runAdvisorConsult(args: {
563
567
  context: ToolContext;
564
568
  label: string;
565
- /** The agent's own `objective` — its framing of what it wants advised on. */
569
+ /** The agent's own `objective`: the brief the advisor advises off. */
566
570
  objective: string;
567
571
  sendToClient: (msg: AssistantEvent) => void;
568
572
  requestedOverrideProfile: string | undefined;
@@ -576,35 +580,16 @@ async function runAdvisorConsult(args: {
576
580
  let profileNote: string | undefined;
577
581
 
578
582
  try {
583
+ // The parent conversation is looked up only for its warm skill catalog, so
584
+ // an unresolvable one (e.g. evicted) costs the skills section of the pack
585
+ // and nothing else: the consult itself runs off the brief.
579
586
  const parentConversation = findConversation(context.conversationId);
580
- if (!parentConversation) {
581
- return {
582
- content:
583
- "(advisor unavailable: parent conversation could not be resolved)",
584
- isError: false,
585
- };
586
- }
587
-
588
- // Snapshot the parent's in-memory transcript and system prompt, then append
589
- // the in-flight assistant turn (the plan/text the model wrote THIS turn,
590
- // before calling the advisor). The in-memory array does not yet hold that
591
- // turn — the agent loop only writes it back to `conversation.messages` after
592
- // the turn settles — but it is already persisted to the DB (the assistant
593
- // row is finalized at `message_complete`, which fires before tool execution).
594
- // `sanitizeConsultTranscript` then strips the dangling advisor `tool_use`
595
- // off that final assistant turn so the inherited transcript is provider-safe.
596
- const parentSystemPrompt = parentConversation.getCurrentSystemPrompt();
597
- const withInFlight = appendInFlightAssistantTurn(
598
- [...parentConversation.messages],
599
- context.conversationId,
600
- );
601
- const sanitizedMessages = sanitizeConsultTranscript(withInFlight);
602
587
 
603
588
  // Situational awareness for the advisor: the parent's live tool set, the
604
589
  // full skill catalog, and its workspace. Assembled off the per-turn
605
590
  // ToolContext snapshot (trust, channel) so the personal-memory sections
606
591
  // are gated exactly like the runtime injectors. Best-effort: a null pack
607
- // just means the consult runs on transcript + system prompt alone.
592
+ // just means the consult runs on the brief alone.
608
593
  const situationalContext = await buildAdvisorContext({
609
594
  conversationId: context.conversationId,
610
595
  workingDir: context.workingDir,
@@ -614,7 +599,7 @@ async function runAdvisorConsult(args: {
614
599
  enabledPluginSet: context.enabledPluginSet,
615
600
  // The parent's warm per-turn catalog keeps the synchronous on-disk
616
601
  // catalog scan out of the consult path.
617
- skillCatalog: parentConversation.skillProjectionCache?.catalog,
602
+ skillCatalog: parentConversation?.skillProjectionCache?.catalog,
618
603
  });
619
604
 
620
605
  // Default to the stronger advisor profile when the caller did not pin one;
@@ -623,7 +608,7 @@ async function runAdvisorConsult(args: {
623
608
  let overrideProfile = requestedOverrideProfile ?? config.llm.advisorProfile;
624
609
  // The advisor carries read tools, so a profile the catalog states cannot
625
610
  // call them is handed a surface it can never use and answers from the
626
- // transcript alone. Fall back to the call site's own default and say so
611
+ // brief alone. Fall back to the call site's own default and say so
627
612
  // alongside the guidance, the way a regular spawn reports it. The check is
628
613
  // unconditional, matching the tools it protects, and only a catalog `false`
629
614
  // redirects, so a model the catalog has never heard of is left alone.
@@ -697,16 +682,15 @@ async function runAdvisorConsult(args: {
697
682
  {
698
683
  parentConversationId: context.conversationId,
699
684
  label,
700
- // Carry the agent's own objective into the consult request — the
701
- // agent states the task here, and the inherited transcript can be
702
- // thin. The situational pack rides in the model request only
703
- // (`requestText`), keeping the system prompt minimal and the
704
- // display-facing `objective` free of bulky internal context.
685
+ // The agent's own objective IS the brief the consult runs on, so it
686
+ // carries into the request verbatim. The situational pack rides in
687
+ // the model request only (`requestText`), keeping the system prompt
688
+ // minimal and the display-facing `objective` free of bulky internal
689
+ // context.
705
690
  objective: advisorRequestText(objective),
706
691
  requestText: advisorRequestText(objective, situationalContext),
707
692
  sendResultToUser: false,
708
693
  role: "advisor",
709
- fork: true,
710
694
  // The advisor's read-only guarantee cannot rest on tool NAMES. A
711
695
  // workspace tool may register under `file_read` (registerWorkspaceTools
712
696
  // stashes the built-in and installs its own implementation), and a
@@ -718,10 +702,9 @@ async function runAdvisorConsult(args: {
718
702
  //
719
703
  // The advisor is a ROLE, not an `LLMCallSiteEnum` value, so its usage
720
704
  // lands under `subagentSpawn` like any other subagent. This is what
721
- // makes advisor consults separable from regular forks in telemetry.
705
+ // makes advisor consults separable from regular spawns in telemetry.
722
706
  spawnMode: "advisor_consult",
723
- parentMessages: sanitizedMessages,
724
- systemPromptOverride: buildAdvisorSystem(parentSystemPrompt),
707
+ systemPromptOverride: buildAdvisorSystem(),
725
708
  ...(overrideProfile ? { overrideProfile } : {}),
726
709
  ...(forceOverrideProfile ? { forceOverrideProfile: true } : {}),
727
710
  // A consult is delegated work of the invoking turn, so its spend
@@ -817,46 +800,3 @@ function withAdvisorNote(
817
800
  }
818
801
  return `${guidance}\n\n${present.map((note) => `_(${note})_`).join("\n")}`;
819
802
  }
820
-
821
- /**
822
- * Append the in-flight assistant turn (persisted this turn before the advisor
823
- * tool ran) to an in-memory message snapshot, unless the snapshot already ends
824
- * with it. The latest persisted assistant row carries the plan/text the model
825
- * wrote immediately before calling the advisor plus the dangling advisor
826
- * `tool_use`; `sanitizeConsultTranscript` strips the dangling call.
827
- *
828
- * Best-effort: a malformed or missing row leaves the snapshot unchanged so the
829
- * consult still runs over the in-memory history.
830
- */
831
- function appendInFlightAssistantTurn(
832
- messages: Message[],
833
- conversationId: string,
834
- ): Message[] {
835
- // When the snapshot already ends on an assistant turn, the in-flight turn is
836
- // present (or there is none to add) — appending the latest row would duplicate it.
837
- if (messages[messages.length - 1]?.role === "assistant") {
838
- return messages;
839
- }
840
-
841
- let rows;
842
- try {
843
- rows = getMessages(conversationId);
844
- } catch {
845
- return messages;
846
- }
847
- if (!rows || rows.length === 0) {
848
- return messages;
849
- }
850
-
851
- const lastRow = rows[rows.length - 1];
852
- if (lastRow.role !== "assistant") {
853
- return messages;
854
- }
855
-
856
- const blocks: ContentBlock[] = lastRow.content;
857
-
858
- if (blocks.length === 0) {
859
- return messages;
860
- }
861
- return [...messages, { role: "assistant", content: blocks }];
862
- }
@@ -123,7 +123,7 @@ export const SURFACE_SHAPE_DOCS: Record<string, SurfaceShapeDoc> = {
123
123
  work_result: {
124
124
  purpose: "structured receipt after completed work",
125
125
  shape:
126
- '{ eyebrow?, status?: "completed"|"partial"|"failed"|"in_progress", summary?, metrics?: [{ label, value, detail?, tone?: "neutral"|"positive"|"warning"|"negative" }], sections?: [{ id?, title, description?, type?: "items"|"timeline"|"diff"|"artifacts"|"warnings", items?: [{ id?, title, description?, status?, tone?, metadata?: [{ label, value }], href? }], diffs?: [{ label?, before?, after? }] }] } — structured receipt after real work; keep display-only unless follow-up buttons are needed',
126
+ '{ eyebrow?, status?: "completed"|"partial"|"failed"|"in_progress", summary?, metrics?: [{ label, value, detail?, tone?: "neutral"|"positive"|"warning"|"negative" }], sections?: [{ id?, title, description?, type?: "items"|"timeline"|"diff"|"artifacts"|"warnings", items?: [{ id?, title, description?, status?, tone?, metadata?: [{ label, value }], href? }], diffs?: [{ label?, before?, after? }] }] }: structured receipt after real work; keep display-only unless follow-up buttons are needed. An item `href` makes the row a link: an in-app path (e.g. "/assistant/skills/<skillId>?tab=history") opens in place, an https URL opens externally; other schemes are ignored',
127
127
  missingContent: (data) =>
128
128
  hasContent(data)
129
129
  ? null
@@ -0,0 +1,37 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import { resolveWorkerCommand } from "../worker-process.js";
4
+
5
+ const entry = new URL("file:///source/monitoring/worker.ts");
6
+
7
+ describe("resolveWorkerCommand", () => {
8
+ test("uses the packaged Windows worker executable when present", () => {
9
+ expect(
10
+ resolveWorkerCommand(entry, "monitoring", {
11
+ platform: "win32",
12
+ execPath: "/runtime/vellum-daemon.exe",
13
+ executableExists: () => true,
14
+ }),
15
+ ).toEqual(["/runtime/vellum-worker.exe", "monitoring"]);
16
+ });
17
+
18
+ test("routes integrity checks through the packaged worker", () => {
19
+ expect(
20
+ resolveWorkerCommand(entry, "db-integrity", {
21
+ platform: "win32",
22
+ execPath: "/runtime/vellum-worker.exe",
23
+ executableExists: () => true,
24
+ }),
25
+ ).toEqual(["/runtime/vellum-worker.exe", "db-integrity"]);
26
+ });
27
+
28
+ test("falls back to the source entry outside a packaged runtime", () => {
29
+ expect(
30
+ resolveWorkerCommand(entry, "monitoring", {
31
+ platform: "win32",
32
+ execPath: "/runtime/vellum-daemon.exe",
33
+ executableExists: () => false,
34
+ }),
35
+ ).toEqual(["bun", "--smol", "run", "/source/monitoring/worker.ts"]);
36
+ });
37
+ });
@@ -19,6 +19,16 @@ import { logSerializers } from "./log-redact.js";
19
19
  import { getLogsDir } from "./platform.js";
20
20
 
21
21
  const loadModule = createRequire(import.meta.url);
22
+ let bundledPino: typeof import("pino") | undefined;
23
+ let bundledPinoPretty: typeof import("pino-pretty") | undefined;
24
+
25
+ export function setBundledLoggerModules(
26
+ pino: typeof import("pino"),
27
+ pinoPretty: typeof import("pino-pretty"),
28
+ ): void {
29
+ bundledPino = pino;
30
+ bundledPinoPretty = pinoPretty;
31
+ }
22
32
 
23
33
  /**
24
34
  * pino loads on first logger construction, not import, so CLI processes that
@@ -26,11 +36,17 @@ const loadModule = createRequire(import.meta.url);
26
36
  * the real dual-export and ESM-shaped test mocks.
27
37
  */
28
38
  function loadPino(): typeof import("pino") {
39
+ if (bundledPino) {
40
+ return bundledPino;
41
+ }
29
42
  const mod = loadModule("pino") as { default?: unknown };
30
43
  return (mod.default ?? mod) as typeof import("pino");
31
44
  }
32
45
 
33
46
  function loadPinoPretty(): typeof import("pino-pretty") {
47
+ if (bundledPinoPretty) {
48
+ return bundledPinoPretty;
49
+ }
34
50
  const mod = loadModule("pino-pretty") as { default?: unknown };
35
51
  return (mod.default ?? mod) as typeof import("pino-pretty");
36
52
  }
@@ -16,7 +16,7 @@ import {
16
16
  readFileSync,
17
17
  unlinkSync,
18
18
  } from "node:fs";
19
- import { dirname } from "node:path";
19
+ import { dirname, join } from "node:path";
20
20
  import { fileURLToPath } from "node:url";
21
21
 
22
22
  import { getCurrentLogFilePath } from "./logger.js";
@@ -122,6 +122,37 @@ export interface SpawnWorkerProcessOptions {
122
122
  detached?: boolean;
123
123
  }
124
124
 
125
+ export type PackagedWorkerEntry =
126
+ | "monitoring"
127
+ | "schedule"
128
+ | "memory"
129
+ | "routes"
130
+ | "db-integrity";
131
+
132
+ interface WorkerCommandRuntime {
133
+ platform: NodeJS.Platform;
134
+ execPath: string;
135
+ executableExists: (path: string) => boolean;
136
+ }
137
+
138
+ export function resolveWorkerCommand(
139
+ entry: URL,
140
+ packagedEntry: PackagedWorkerEntry | undefined,
141
+ runtime: WorkerCommandRuntime = {
142
+ platform: process.platform,
143
+ execPath: process.execPath,
144
+ executableExists: existsSync,
145
+ },
146
+ ): string[] {
147
+ if (runtime.platform === "win32" && packagedEntry) {
148
+ const executable = join(dirname(runtime.execPath), "vellum-worker.exe");
149
+ if (runtime.executableExists(executable)) {
150
+ return [executable, packagedEntry];
151
+ }
152
+ }
153
+ return ["bun", "--smol", "run", fileURLToPath(entry)];
154
+ }
155
+
125
156
  type WorkerReadyOutcome = "ready" | "exited" | "timeout";
126
157
 
127
158
  /**
@@ -178,6 +209,8 @@ export async function spawnWorkerProcess(args: {
178
209
  pidPath: string;
179
210
  /** Worker entry script, e.g. `new URL("./worker.ts", import.meta.url)`. */
180
211
  entry: URL;
212
+ /** Entry bundled into vellum-worker.exe for packaged Windows runtimes. */
213
+ packagedEntry?: PackagedWorkerEntry;
181
214
  /** Human-readable name used in spawn-failure messages, e.g. "Memory worker". */
182
215
  workerLabel: string;
183
216
  options?: SpawnWorkerProcessOptions;
@@ -208,15 +241,15 @@ export async function spawnWorkerProcess(args: {
208
241
  // output is at least visible to the spawning process.
209
242
  }
210
243
 
211
- // Workers are latency-insensitive, so run them in bun's small-heap mode
212
- // with a RAM-size hint sized to the container — see worker-memory.ts.
244
+ // Source workers use bun's small-heap mode. Packaged Windows workers use the
245
+ // compiled worker executable. Both receive the RAM hint from worker-memory.
213
246
  //
214
247
  // `fileURLToPath`, not `.pathname`: a URL's pathname is percent-encoded, so
215
248
  // an install path containing a space (every macOS desktop install lives
216
249
  // under "Application Support") would reach bun as "Application%20Support"
217
250
  // and the entry would not be found.
218
251
  const child = Bun.spawn({
219
- cmd: ["bun", "--smol", "run", fileURLToPath(args.entry)],
252
+ cmd: resolveWorkerCommand(args.entry, args.packagedEntry),
220
253
  env: workerMemoryEnv(),
221
254
  stdio: ["ignore", "ignore", stderrFd],
222
255
  detached,
@@ -0,0 +1,32 @@
1
+ import { setBundledCliModules } from "./cli/bundled-modules.js";
2
+ import * as pluginDiff from "./cli/lib/diff-plugin.js";
3
+ import * as pluginInspect from "./cli/lib/inspect-plugin.js";
4
+ import * as pluginInstallGitHub from "./cli/lib/install-from-github.js";
5
+ import * as pluginInstallPlatform from "./cli/lib/install-from-platform.js";
6
+ import * as pluginInstalled from "./cli/lib/list-installed-plugins.js";
7
+ import * as pluginCatalogCache from "./cli/lib/plugin-catalog-cache.js";
8
+ import * as pluginCatalogLocal from "./cli/lib/plugin-catalog-local.js";
9
+ import * as pluginPinHistory from "./cli/lib/plugin-pin-history.js";
10
+ import * as pluginSurfaces from "./cli/lib/plugin-surfaces.js";
11
+ import * as pluginSearch from "./cli/lib/search-plugins.js";
12
+ import * as pluginUninstall from "./cli/lib/uninstall-plugin.js";
13
+ import * as pluginUpgrade from "./cli/lib/upgrade-plugin.js";
14
+ import * as configEnv from "./config/env.js";
15
+ import * as providerSecretCatalog from "./providers/provider-secret-catalog.js";
16
+
17
+ setBundledCliModules({
18
+ configEnv,
19
+ providerSecretCatalog,
20
+ pluginCatalogCache,
21
+ pluginCatalogLocal,
22
+ pluginDiff,
23
+ pluginInspect,
24
+ pluginInstallGitHub,
25
+ pluginInstallPlatform,
26
+ pluginInstalled,
27
+ pluginPinHistory,
28
+ pluginSearch,
29
+ pluginSurfaces,
30
+ pluginUninstall,
31
+ pluginUpgrade,
32
+ });
@@ -0,0 +1,4 @@
1
+ import "./windows-compiled-logger.js";
2
+ import "./windows-compiled-cli.js";
3
+
4
+ await import("./index.js");
@@ -0,0 +1,6 @@
1
+ import pino from "pino";
2
+ import pinoPretty from "pino-pretty";
3
+
4
+ import { setBundledLoggerModules } from "./util/logger.js";
5
+
6
+ setBundledLoggerModules(pino, pinoPretty);
@@ -0,0 +1,29 @@
1
+ import "./windows-compiled-logger.js";
2
+
3
+ const worker = process.argv[2];
4
+
5
+ switch (worker) {
6
+ case "monitoring":
7
+ await import("./monitoring/worker.js");
8
+ break;
9
+ case "schedule":
10
+ await import("./schedule/worker.js");
11
+ break;
12
+ case "memory":
13
+ await (
14
+ await import("./plugins/defaults/worker-entrypoints.js")
15
+ ).loadDefaultMemoryWorker();
16
+ break;
17
+ case "routes":
18
+ await import("./embedded/plugin-api.js");
19
+ await import("./routes/worker.js");
20
+ break;
21
+ case "db-integrity": {
22
+ const { runIntegrityCheck } =
23
+ await import("./monitoring/db-integrity-check.js");
24
+ console.log(JSON.stringify(runIntegrityCheck(process.argv[3] ?? "")));
25
+ break;
26
+ }
27
+ default:
28
+ throw new Error(`Unknown Windows worker entry: ${worker ?? "missing"}`);
29
+ }