@vellumai/assistant 0.10.3-staging.2 → 0.10.4-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 (239) hide show
  1. package/openapi.yaml +73 -56
  2. package/package.json +1 -1
  3. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +83 -31
  4. package/src/__tests__/assistant-stream-state.test.ts +3 -76
  5. package/src/__tests__/background-workers-disk-pressure.test.ts +4 -2
  6. package/src/__tests__/channel-approval-routes.test.ts +21 -26
  7. package/src/__tests__/channel-delivery-store.test.ts +28 -0
  8. package/src/__tests__/channel-guardian.test.ts +82 -32
  9. package/src/__tests__/channel-inbound-disk-pressure.test.ts +11 -19
  10. package/src/__tests__/channel-reply-delivery.test.ts +6 -2
  11. package/src/__tests__/compaction-ledger-store.test.ts +128 -0
  12. package/src/__tests__/config-loader-backfill.test.ts +148 -0
  13. package/src/__tests__/consult-deadline.test.ts +60 -0
  14. package/src/__tests__/contact-store-interaction-info.test.ts +156 -0
  15. package/src/__tests__/contact-store-user-file.test.ts +7 -10
  16. package/src/__tests__/contacts-relay-reads.test.ts +6 -9
  17. package/src/__tests__/contacts-write.test.ts +0 -2
  18. package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -2
  19. package/src/__tests__/conversation-agent-loop.test.ts +98 -7
  20. package/src/__tests__/conversation-attention-telegram.test.ts +9 -11
  21. package/src/__tests__/conversation-error.test.ts +18 -0
  22. package/src/__tests__/conversation-fork-crud.test.ts +354 -24
  23. package/src/__tests__/conversation-title-service.test.ts +222 -201
  24. package/src/__tests__/db-compaction-events-migration.test.ts +129 -0
  25. package/src/__tests__/delete-propagation.test.ts +5 -3
  26. package/src/__tests__/dm-backfill.test.ts +6 -4
  27. package/src/__tests__/emit-signal-routing-intent.test.ts +2 -6
  28. package/src/__tests__/guardian-binding-drift-heal.test.ts +43 -23
  29. package/src/__tests__/guardian-dispatch.test.ts +50 -5
  30. package/src/__tests__/guardian-routing-state.test.ts +6 -10
  31. package/src/__tests__/helpers/channel-test-adapter.ts +45 -12
  32. package/src/__tests__/helpers/create-guardian-binding.ts +15 -23
  33. package/src/__tests__/helpers/mock-logger.ts +1 -0
  34. package/src/__tests__/helpers/seed-contact-channel.ts +96 -0
  35. package/src/__tests__/inbound-invite-redemption.test.ts +87 -10
  36. package/src/__tests__/invite-redemption-service.test.ts +273 -53
  37. package/src/__tests__/invite-routes-http.test.ts +34 -0
  38. package/src/__tests__/invite-service-ipc.test.ts +65 -2
  39. package/src/__tests__/list-messages-page-latest.test.ts +173 -4
  40. package/src/__tests__/mcp-config-secret-boundary.test.ts +3 -0
  41. package/src/__tests__/non-member-access-request.test.ts +15 -13
  42. package/src/__tests__/onboarding-persona-write.test.ts +52 -22
  43. package/src/__tests__/persist-onboarding-artifacts.test.ts +1 -0
  44. package/src/__tests__/persona-resolver.test.ts +75 -45
  45. package/src/__tests__/plugin-bootstrap.test.ts +13 -5
  46. package/src/__tests__/plugin-disabled-state.test.ts +190 -0
  47. package/src/__tests__/provider-usage-tracking.test.ts +1 -1
  48. package/src/__tests__/reaction-intercept-cold-cache-warm.test.ts +135 -0
  49. package/src/__tests__/reaction-intercept-member-verdict-warm.test.ts +158 -0
  50. package/src/__tests__/reaction-persistence.test.ts +51 -4
  51. package/src/__tests__/relay-server.test.ts +88 -31
  52. package/src/__tests__/runtime-attachment-metadata.test.ts +9 -11
  53. package/src/__tests__/settings-routes.test.ts +32 -0
  54. package/src/__tests__/slack-block-formatting.test.ts +1 -38
  55. package/src/__tests__/sse-actor-principal-guardian-source.test.ts +13 -36
  56. package/src/__tests__/stt-hints.test.ts +6 -3
  57. package/src/__tests__/subagent-fork-prompt-role.test.ts +195 -0
  58. package/src/__tests__/subagent-fork-spawn.test.ts +6 -7
  59. package/src/__tests__/subagent-role-registry.test.ts +17 -4
  60. package/src/__tests__/subagent-spawn-and-await.test.ts +546 -0
  61. package/src/__tests__/subagent-tools.test.ts +398 -3
  62. package/src/__tests__/thread-backfill.test.ts +3 -3
  63. package/src/__tests__/tool-preview-lifecycle.test.ts +26 -10
  64. package/src/__tests__/tool-start-timestamp.test.ts +4 -3
  65. package/src/__tests__/trusted-contact-approval-notifier.test.ts +37 -51
  66. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -2
  67. package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +9 -7
  68. package/src/__tests__/trusted-contact-multichannel.test.ts +16 -7
  69. package/src/__tests__/trusted-contact-verification.test.ts +79 -54
  70. package/src/__tests__/voice-guardian-cold-cache-warm.test.ts +137 -0
  71. package/src/__tests__/voice-invite-redemption.test.ts +183 -20
  72. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +3 -3
  73. package/src/__tests__/workspace-migration-111-prune-seeded-callsite-defaults.test.ts +2 -2
  74. package/src/__tests__/workspace-migration-112-remove-advisor-callsite-override.test.ts +170 -0
  75. package/src/__tests__/workspace-migration-drop-user-md.test.ts +196 -238
  76. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +35 -47
  77. package/src/agent/loop-exclusive-tool.test.ts +19 -15
  78. package/src/agent/loop-native-web-search.test.ts +200 -0
  79. package/src/agent/loop.ts +108 -1
  80. package/src/api/responses/conversation-message.ts +9 -0
  81. package/src/approvals/guardian-request-resolvers.ts +16 -4
  82. package/src/calls/__tests__/relay-setup-router.test.ts +10 -18
  83. package/src/calls/guardian-dispatch.ts +14 -11
  84. package/src/calls/inbound-trust-reader.ts +7 -1
  85. package/src/calls/relay-access-wait.ts +6 -6
  86. package/src/calls/relay-server.ts +22 -2
  87. package/src/calls/relay-setup-router.ts +10 -10
  88. package/src/cli/commands/__tests__/conversations-slack.test.ts +1 -0
  89. package/src/cli/commands/contacts.ts +10 -7
  90. package/src/cli/commands/memory/__tests__/worker.test.ts +147 -17
  91. package/src/cli/commands/memory/worker.ts +97 -30
  92. package/src/cli/commands/plugins.ts +3 -146
  93. package/src/cli/lib/__tests__/list-installed-plugins.test.ts +17 -17
  94. package/src/cli/lib/__tests__/publish-plugin.test.ts +98 -0
  95. package/src/cli/lib/publish-plugin.ts +231 -1
  96. package/src/config/__tests__/sync-gated-profiles.test.ts +5 -7
  97. package/src/config/bundled-skills/subagent/SKILL.md +16 -1
  98. package/src/config/bundled-skills/subagent/TOOLS.json +5 -4
  99. package/src/config/call-site-defaults.ts +0 -6
  100. package/src/config/llm-resolver.ts +0 -3
  101. package/src/config/schemas/call-site-catalog.ts +0 -7
  102. package/src/config/schemas/heartbeat.ts +2 -5
  103. package/src/config/schemas/llm.ts +3 -12
  104. package/src/config/schemas/memory-lifecycle.ts +1 -1
  105. package/src/config/seed-inference-profiles.ts +76 -35
  106. package/src/config/sync-gated-profiles.ts +0 -3
  107. package/src/contacts/__tests__/contacts-write-revoke-relay.test.ts +7 -8
  108. package/src/contacts/__tests__/member-write-relay.test.ts +35 -11
  109. package/src/contacts/contact-store.ts +27 -237
  110. package/src/contacts/contacts-write.ts +18 -58
  111. package/src/contacts/gateway-channel-read.ts +51 -0
  112. package/src/contacts/member-write-relay.ts +25 -31
  113. package/src/contacts/types.ts +3 -15
  114. package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -44
  115. package/src/daemon/conversation-agent-loop-handlers.ts +29 -10
  116. package/src/daemon/conversation-agent-loop.ts +68 -61
  117. package/src/daemon/conversation-error.ts +7 -10
  118. package/src/daemon/conversation-tool-setup.ts +0 -10
  119. package/src/daemon/conversation.ts +10 -0
  120. package/src/daemon/external-plugins-bootstrap.ts +8 -2
  121. package/src/daemon/handlers/__tests__/config-a2a-accept.test.ts +0 -1
  122. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +0 -2
  123. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +0 -2
  124. package/src/daemon/handlers/__tests__/config-channels.test.ts +9 -14
  125. package/src/daemon/handlers/config-channels.ts +14 -29
  126. package/src/daemon/lifecycle.ts +16 -4
  127. package/src/daemon/message-types/surfaces.ts +2 -0
  128. package/src/heartbeat/heartbeat-service.ts +5 -0
  129. package/src/home/relationship-state-writer.ts +5 -0
  130. package/src/memory/__tests__/embedding-cache.test.ts +136 -0
  131. package/src/memory/compaction-ledger-store.ts +107 -0
  132. package/src/memory/conversation-crud.ts +136 -61
  133. package/src/memory/conversation-title-service.ts +173 -24
  134. package/src/memory/embedding-backend.ts +8 -1
  135. package/src/memory/embedding-cache.ts +139 -0
  136. package/src/memory/jobs-worker.ts +75 -29
  137. package/src/memory/memory-retrospective-job.ts +5 -0
  138. package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +27 -5
  139. package/src/memory/migrations/302-create-compaction-events.ts +107 -0
  140. package/src/memory/migrations/303-add-conversation-creation-seq.ts +33 -0
  141. package/src/memory/migrations/__tests__/209-strip-thinking-from-consolidated.test.ts +79 -6
  142. package/src/memory/schema/contacts.ts +6 -2
  143. package/src/memory/schema/conversations.ts +39 -0
  144. package/src/memory/steps.ts +1090 -367
  145. package/src/memory/worker-control.ts +104 -18
  146. package/src/memory/worker-process.ts +17 -0
  147. package/src/messaging/channel-binding-metadata.ts +31 -0
  148. package/src/messaging/channel-binding-schema.ts +51 -0
  149. package/src/messaging/providers/__tests__/callback-routing.test.ts +45 -0
  150. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +195 -0
  151. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +11 -0
  152. package/src/messaging/providers/a2a/deliver.ts +5 -1
  153. package/src/messaging/providers/a2a/transport.ts +10 -0
  154. package/src/messaging/providers/callback-routing.ts +48 -0
  155. package/src/messaging/providers/channel-transport.ts +55 -0
  156. package/src/messaging/providers/index.ts +65 -241
  157. package/src/messaging/providers/slack/binding-metadata.ts +62 -0
  158. package/src/messaging/providers/slack/transport.ts +92 -0
  159. package/src/messaging/providers/telegram-bot/transport.ts +51 -0
  160. package/src/messaging/providers/whatsapp/transport.ts +38 -0
  161. package/src/notifications/__tests__/broadcaster.test.ts +0 -8
  162. package/src/notifications/__tests__/connected-channels.test.ts +8 -36
  163. package/src/notifications/__tests__/destination-resolver.test.ts +12 -117
  164. package/src/notifications/destination-resolver.ts +7 -23
  165. package/src/notifications/emit-signal.ts +5 -11
  166. package/src/plugins/defaults/index.ts +0 -35
  167. package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +11 -0
  168. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +243 -2
  169. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +167 -14
  170. package/src/plugins/disabled-state.ts +31 -0
  171. package/src/plugins/registry.ts +55 -12
  172. package/src/prompts/persona-resolver.ts +43 -11
  173. package/src/providers/call-site-routing.ts +41 -0
  174. package/src/providers/provider-send-message.ts +6 -0
  175. package/src/providers/ratelimit.ts +6 -0
  176. package/src/providers/registry.ts +1 -1
  177. package/src/providers/retry.ts +6 -0
  178. package/src/providers/types.ts +13 -0
  179. package/src/providers/usage-tracking.ts +6 -0
  180. package/src/runtime/__tests__/guardian-vellum-migration.test.ts +30 -27
  181. package/src/runtime/__tests__/local-principal-trust.test.ts +16 -18
  182. package/src/runtime/__tests__/member-verdict-cache.test.ts +119 -0
  183. package/src/runtime/__tests__/trust-verdict-consumer.test.ts +115 -168
  184. package/src/runtime/access-request-helper.ts +1 -2
  185. package/src/runtime/actor-trust-resolver.ts +44 -17
  186. package/src/runtime/anchored-guardian.test.ts +7 -54
  187. package/src/runtime/anchored-guardian.ts +4 -53
  188. package/src/runtime/assistant-stream-state.ts +12 -74
  189. package/src/runtime/channel-reply-delivery.ts +3 -8
  190. package/src/runtime/guardian-vellum-migration.ts +18 -16
  191. package/src/runtime/invite-redemption-service.ts +25 -10
  192. package/src/runtime/local-actor-identity.test.ts +108 -0
  193. package/src/runtime/local-actor-identity.ts +27 -20
  194. package/src/runtime/member-verdict-cache.ts +0 -0
  195. package/src/runtime/routes/__tests__/contact-routes.test.ts +100 -7
  196. package/src/runtime/routes/__tests__/global-search-routes.test.ts +1 -2
  197. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +2 -1
  198. package/src/runtime/routes/contact-routes.ts +40 -25
  199. package/src/runtime/routes/conversation-list-routes.ts +1 -29
  200. package/src/runtime/routes/conversation-routes.ts +27 -7
  201. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +0 -10
  202. package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -8
  203. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +19 -0
  204. package/src/runtime/routes/settings-routes.ts +8 -3
  205. package/src/runtime/services/conversation-serializer.ts +6 -49
  206. package/src/runtime/slack-block-formatting.ts +0 -15
  207. package/src/runtime/trust-verdict-consumer.ts +36 -41
  208. package/src/subagent/__tests__/consult-prompt.test.ts +35 -0
  209. package/src/{plugins/defaults/advisor/__tests__/transcript.test.ts → subagent/__tests__/consult-transcript.test.ts} +47 -10
  210. package/src/{plugins/defaults/advisor/steering.ts → subagent/consult-prompt.ts} +17 -39
  211. package/src/{plugins/defaults/advisor/transcript.ts → subagent/consult-transcript.ts} +18 -8
  212. package/src/subagent/index.ts +1 -1
  213. package/src/subagent/manager.ts +245 -33
  214. package/src/subagent/types.ts +8 -1
  215. package/src/tools/registry.ts +10 -3
  216. package/src/tools/subagent/consult-deadline.ts +49 -0
  217. package/src/tools/subagent/spawn.ts +234 -5
  218. package/src/util/logger.ts +9 -0
  219. package/src/util/platform.ts +14 -0
  220. package/src/workspace/migrations/031-drop-user-md.ts +232 -148
  221. package/src/workspace/migrations/112-remove-advisor-callsite-override.ts +64 -0
  222. package/src/workspace/migrations/registry.ts +2 -0
  223. package/src/plugins/defaults/advisor/__tests__/advisor-gate.test.ts +0 -56
  224. package/src/plugins/defaults/advisor/__tests__/advisor-state-store.test.ts +0 -43
  225. package/src/plugins/defaults/advisor/__tests__/agent-loop-integration.test.ts +0 -137
  226. package/src/plugins/defaults/advisor/__tests__/consult.test.ts +0 -314
  227. package/src/plugins/defaults/advisor/__tests__/context-pack-gating.test.ts +0 -106
  228. package/src/plugins/defaults/advisor/__tests__/context-pack.test.ts +0 -60
  229. package/src/plugins/defaults/advisor/__tests__/hooks.test.ts +0 -138
  230. package/src/plugins/defaults/advisor/advisor-gate.ts +0 -29
  231. package/src/plugins/defaults/advisor/advisor-state-store.ts +0 -94
  232. package/src/plugins/defaults/advisor/config.ts +0 -21
  233. package/src/plugins/defaults/advisor/consult.ts +0 -197
  234. package/src/plugins/defaults/advisor/context-pack.ts +0 -288
  235. package/src/plugins/defaults/advisor/hooks/post-model-call.ts +0 -34
  236. package/src/plugins/defaults/advisor/hooks/pre-model-call.ts +0 -30
  237. package/src/plugins/defaults/advisor/hooks/user-prompt-submit.ts +0 -19
  238. package/src/plugins/defaults/advisor/package.json +0 -14
  239. package/src/plugins/defaults/advisor/tools/advisor.ts +0 -92
@@ -2,11 +2,45 @@ import { validateInferenceProfileKey } from "../../config/inference-profile-vali
2
2
  import { resolveDefaultProfileKey } from "../../config/llm-resolver.js";
3
3
  import { getConfig } from "../../config/loader.js";
4
4
  import { findConversation } from "../../daemon/conversation-registry.js";
5
- import { getConversationOverrideProfile } from "../../memory/conversation-crud.js";
6
- import type { Message } from "../../providers/types.js";
7
- import { getSubagentManager } from "../../subagent/index.js";
5
+ import type { ServerMessage } from "../../daemon/message-protocol.js";
6
+ import {
7
+ getConversationOverrideProfile,
8
+ getMessages,
9
+ } from "../../memory/conversation-crud.js";
10
+ import type { ContentBlock, Message } from "../../providers/types.js";
11
+ import {
12
+ advisorRequestText,
13
+ buildAdvisorSystem,
14
+ } from "../../subagent/consult-prompt.js";
15
+ import { sanitizeConsultTranscript } from "../../subagent/consult-transcript.js";
16
+ import {
17
+ getSubagentManager,
18
+ SubagentAbortedError,
19
+ } from "../../subagent/index.js";
8
20
  import type { SubagentRole } from "../../subagent/types.js";
21
+ import { getLogger } from "../../util/logger.js";
9
22
  import type { ToolContext, ToolExecutionResult } from "../types.js";
23
+ import { createConsultDeadline } from "./consult-deadline.js";
24
+
25
+ const log = getLogger("subagent-spawn");
26
+
27
+ /**
28
+ * Idle ceiling on a single advisor consult: abort only after this much time
29
+ * passes with NO streamed token (thinking or text). A reasoning advisor profile
30
+ * streams its reasoning while it works, so a fixed wall-clock ceiling would kill
31
+ * it mid-thought; an idle window instead fires only when the consult is
32
+ * genuinely stalled (or never starts). Generous enough to also span
33
+ * time-to-first-token over a large inherited transcript.
34
+ */
35
+ const ADVISOR_IDLE_TIMEOUT_MS = 60_000;
36
+
37
+ /**
38
+ * Absolute backstop on a single advisor consult regardless of streaming
39
+ * progress, so a runaway or looping stream can't block the parent forever.
40
+ * Either ceiling still yields the partial guidance (recovered in the
41
+ * `SubagentAbortedError` branch below), not a discard.
42
+ */
43
+ const ADVISOR_MAX_TIMEOUT_MS = 300_000;
10
44
 
11
45
  export async function executeSubagentSpawn(
12
46
  input: Record<string, unknown>,
@@ -63,6 +97,19 @@ export async function executeSubagentSpawn(
63
97
  };
64
98
  }
65
99
 
100
+ // ── Advisor role: synchronous, tool-less, stronger-model consult ──
101
+ // Branch before the fire-and-forget path: the advisor blocks on the run and
102
+ // returns its guidance as the tool result in the same turn.
103
+ if (role === "advisor") {
104
+ return runAdvisorConsult({
105
+ context,
106
+ label,
107
+ objective,
108
+ sendToClient: sendToClient as (msg: ServerMessage) => void,
109
+ requestedOverrideProfile,
110
+ });
111
+ }
112
+
66
113
  // ── Fork mode: resolve parent context ────────────────────────────
67
114
  let forkFields:
68
115
  | {
@@ -138,8 +185,8 @@ export async function executeSubagentSpawn(
138
185
  objective,
139
186
  context: extraContext,
140
187
  sendResultToUser,
141
- // For fork mode, role is ignored by the manager (forced to general),
142
- // but we still omit it from the config to signal intent.
188
+ // Regular forks omit the role so they default to general; the advisor
189
+ // role is special-cased earlier via runAdvisorConsult, not here.
143
190
  ...(!fork && role ? { role: role as SubagentRole } : {}),
144
191
  ...(inheritedOverrideProfile
145
192
  ? { overrideProfile: inheritedOverrideProfile }
@@ -168,3 +215,185 @@ export async function executeSubagentSpawn(
168
215
  return { content: `Failed to spawn subagent: ${msg}`, isError: true };
169
216
  }
170
217
  }
218
+
219
+ // ── Advisor consult ──────────────────────────────────────────────────
220
+
221
+ /**
222
+ * Run the `advisor` role as a synchronous, context-inheriting, stronger-model
223
+ * consult and return its guidance as the tool result.
224
+ *
225
+ * Inherits the parent transcript (sanitized), frames it as advice via
226
+ * `buildAdvisorSystem`, runs tool-less on `llm.advisorProfile` (unless the
227
+ * caller passed an explicit `inference_profile`), and is bounded by a
228
+ * progress-aware deadline: an idle window (`ADVISOR_IDLE_TIMEOUT_MS`) reset on
229
+ * every streamed token so a reasoning model isn't killed mid-thought, plus an
230
+ * absolute `ADVISOR_MAX_TIMEOUT_MS` backstop. If either ceiling is hit, the
231
+ * partial guidance produced so far is recovered and returned with a "may be cut
232
+ * off" note rather than discarded. Degrades to a benign non-error notice on any
233
+ * other failure (including the depth-limit rejection when a subagent itself
234
+ * calls the advisor).
235
+ */
236
+ async function runAdvisorConsult(args: {
237
+ context: ToolContext;
238
+ label: string;
239
+ /** The agent's own `objective` — its framing of what it wants advised on. */
240
+ objective: string;
241
+ sendToClient: (msg: ServerMessage) => void;
242
+ requestedOverrideProfile: string | undefined;
243
+ }): Promise<ToolExecutionResult> {
244
+ const { context, label, objective, sendToClient, requestedOverrideProfile } =
245
+ args;
246
+
247
+ try {
248
+ const parentConversation = findConversation(context.conversationId);
249
+ if (!parentConversation) {
250
+ return {
251
+ content:
252
+ "(advisor unavailable: parent conversation could not be resolved)",
253
+ isError: false,
254
+ };
255
+ }
256
+
257
+ // Snapshot the parent's in-memory transcript and system prompt, then append
258
+ // the in-flight assistant turn (the plan/text the model wrote THIS turn,
259
+ // before calling the advisor). The in-memory array does not yet hold that
260
+ // turn — the agent loop only writes it back to `conversation.messages` after
261
+ // the turn settles — but it is already persisted to the DB (the assistant
262
+ // row is finalized at `message_complete`, which fires before tool execution).
263
+ // `sanitizeConsultTranscript` then strips the dangling advisor `tool_use`
264
+ // off that final assistant turn so the inherited transcript is provider-safe.
265
+ const parentSystemPrompt = parentConversation.getCurrentSystemPrompt();
266
+ const withInFlight = appendInFlightAssistantTurn(
267
+ [...parentConversation.messages],
268
+ context.conversationId,
269
+ );
270
+ const sanitizedMessages = sanitizeConsultTranscript(withInFlight);
271
+
272
+ // Default to the stronger advisor profile when the caller did not pin one;
273
+ // an explicit `inference_profile` wins (already forced upstream).
274
+ const advisorProfile = getConfig().llm.advisorProfile;
275
+ const overrideProfile = requestedOverrideProfile ?? advisorProfile;
276
+ const forceOverrideProfile = overrideProfile !== undefined;
277
+
278
+ // Progress-aware deadline: reset on every streamed token so the consult
279
+ // isn't killed mid-thought, with an absolute backstop. Combine it with the
280
+ // caller's own signal.
281
+ const deadline = createConsultDeadline({
282
+ idleMs: ADVISOR_IDLE_TIMEOUT_MS,
283
+ maxMs: ADVISOR_MAX_TIMEOUT_MS,
284
+ });
285
+ const signal = context.signal
286
+ ? AbortSignal.any([context.signal, deadline.signal])
287
+ : deadline.signal;
288
+ // Every streamed chunk (thinking or text) counts as progress and resets the
289
+ // idle window, then forwards to the caller's stream sink if one is present.
290
+ const onText = (chunk: string): void => {
291
+ deadline.recordProgress();
292
+ context.onOutput?.(chunk);
293
+ };
294
+
295
+ try {
296
+ const advice = await getSubagentManager().spawnAndAwait(
297
+ {
298
+ parentConversationId: context.conversationId,
299
+ label,
300
+ // Carry the agent's own objective into the consult request — the
301
+ // agent states the task here, and the inherited transcript can be thin.
302
+ objective: advisorRequestText(objective),
303
+ sendResultToUser: false,
304
+ role: "advisor",
305
+ fork: true,
306
+ parentMessages: sanitizedMessages,
307
+ systemPromptOverride: buildAdvisorSystem(parentSystemPrompt),
308
+ ...(overrideProfile ? { overrideProfile } : {}),
309
+ ...(forceOverrideProfile ? { forceOverrideProfile: true } : {}),
310
+ ...(context.toolUseId ? { parentToolUseId: context.toolUseId } : {}),
311
+ },
312
+ sendToClient,
313
+ { signal, onText },
314
+ );
315
+
316
+ const trimmed = advice.trim();
317
+ return {
318
+ content:
319
+ trimmed.length > 0 ? trimmed : "(advisor returned no guidance)",
320
+ isError: false,
321
+ };
322
+ } finally {
323
+ deadline.dispose();
324
+ }
325
+ } catch (err) {
326
+ // Timed out mid-generation: salvage whatever guidance the advisor had
327
+ // written rather than throwing it away. Partial strategic advice is far
328
+ // more useful to the agent than an "unavailable" notice — especially on a
329
+ // slow reasoning profile that needs most of the window to think.
330
+ if (err instanceof SubagentAbortedError) {
331
+ const partial = err.partialText.trim();
332
+ if (partial.length > 0) {
333
+ log.warn(
334
+ { conversationId: context.conversationId },
335
+ "Advisor consult timed out; returning partial guidance",
336
+ );
337
+ return {
338
+ content: `${partial}\n\n_(The advisor reached its time limit while still writing — the guidance above may be cut off.)_`,
339
+ isError: false,
340
+ };
341
+ }
342
+ }
343
+ const reason = err instanceof Error ? err.message : String(err);
344
+ log.warn(
345
+ { err, conversationId: context.conversationId },
346
+ "Advisor consult failed",
347
+ );
348
+ // Never fail the turn — the advisor is advice, not a blocker.
349
+ return { content: `(advisor unavailable: ${reason})`, isError: false };
350
+ }
351
+ }
352
+
353
+ /**
354
+ * Append the in-flight assistant turn (persisted this turn before the advisor
355
+ * tool ran) to an in-memory message snapshot, unless the snapshot already ends
356
+ * with it. The latest persisted assistant row carries the plan/text the model
357
+ * wrote immediately before calling the advisor plus the dangling advisor
358
+ * `tool_use`; `sanitizeConsultTranscript` strips the dangling call.
359
+ *
360
+ * Best-effort: a malformed or missing row leaves the snapshot unchanged so the
361
+ * consult still runs over the in-memory history.
362
+ */
363
+ function appendInFlightAssistantTurn(
364
+ messages: Message[],
365
+ conversationId: string,
366
+ ): Message[] {
367
+ // When the snapshot already ends on an assistant turn, the in-flight turn is
368
+ // present (or there is none to add) — appending the latest row would duplicate it.
369
+ if (messages[messages.length - 1]?.role === "assistant") return messages;
370
+
371
+ let rows;
372
+ try {
373
+ rows = getMessages(conversationId);
374
+ } catch {
375
+ return messages;
376
+ }
377
+ if (!rows || rows.length === 0) return messages;
378
+
379
+ const lastRow = rows[rows.length - 1];
380
+ if (lastRow.role !== "assistant") return messages;
381
+
382
+ let blocks: ContentBlock[];
383
+ try {
384
+ const parsed = JSON.parse(lastRow.content);
385
+ if (Array.isArray(parsed)) {
386
+ blocks = parsed as ContentBlock[];
387
+ } else if (typeof parsed === "string") {
388
+ blocks = [{ type: "text", text: parsed }];
389
+ } else {
390
+ return messages;
391
+ }
392
+ } catch {
393
+ // Plain-text content (no JSON envelope).
394
+ blocks = [{ type: "text", text: lastRow.content }];
395
+ }
396
+
397
+ if (blocks.length === 0) return messages;
398
+ return [...messages, { role: "assistant", content: blocks }];
399
+ }
@@ -49,6 +49,15 @@ function logFilePathForDate(dir: string, date: Date): string {
49
49
  return join(dir, `${LOG_FILE_PREFIX}${formatDate(date)}${LOG_FILE_SUFFIX}`);
50
50
  }
51
51
 
52
+ /**
53
+ * Returns the path to today's log file (`<logsDir>/assistant-YYYY-MM-DD.log`).
54
+ * Used by callers that need to open the same file the logger writes to, e.g.
55
+ * the memory worker spawner piping the child's stderr into the log file.
56
+ */
57
+ export function getCurrentLogFilePath(): string {
58
+ return logFilePathForDate(getLogsDir(), new Date());
59
+ }
60
+
52
61
  export function pruneOldLogFiles(dir: string, retentionDays: number): number {
53
62
  if (!existsSync(dir)) return 0;
54
63
 
@@ -247,6 +247,20 @@ export function getMemoryWorkerPidPath(): string {
247
247
  return join(getWorkspaceDir(), "memory-worker.pid");
248
248
  }
249
249
 
250
+ /**
251
+ * Returns the path to the memory sync-runner marker file
252
+ * ($VELLUM_WORKSPACE_DIR/memory-sync-runner.pid).
253
+ *
254
+ * The daemon's in-process memory-jobs supervisor writes this marker (with its
255
+ * own PID) while the synchronous in-process runner is actively draining the
256
+ * queue, and removes it when it stands down for an out-of-process worker. It
257
+ * lets `assistant memory worker status` report whether the synchronous runner
258
+ * is still going without an IPC round-trip to the daemon.
259
+ */
260
+ export function getMemorySyncRunnerMarkerPath(): string {
261
+ return join(getWorkspaceDir(), "memory-sync-runner.pid");
262
+ }
263
+
250
264
  /**
251
265
  * Returns the workspace root for user-facing state.
252
266
  *