@vellumai/assistant 0.12.4 → 0.12.5-dev.202609251017.e467a6c

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 (281) hide show
  1. package/docs/architecture/memory.md +9 -1
  2. package/docs/architecture/scheduling.md +13 -5
  3. package/docs/desktop-browser-cli.md +3 -2
  4. package/docs/guardian-request-flow.md +1 -1
  5. package/node_modules/@vellumai/avatar-manifest/src/__tests__/notification-avatar.test.ts +48 -4
  6. package/node_modules/@vellumai/avatar-manifest/src/notification-avatar.ts +12 -8
  7. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
  8. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
  10. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
  11. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +2 -4
  12. package/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
  13. package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
  14. package/openapi.yaml +152 -3
  15. package/package.json +1 -1
  16. package/src/__tests__/agent-loop.test.ts +76 -0
  17. package/src/__tests__/anthropic-error-formatting.test.ts +9 -0
  18. package/src/__tests__/background-shell-bash.test.ts +34 -0
  19. package/src/__tests__/background-shell-host-bash.test.ts +98 -1
  20. package/src/__tests__/background-tool-registry.test.ts +39 -0
  21. package/src/__tests__/channel-readiness-discord.test.ts +8 -0
  22. package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
  23. package/src/__tests__/client-os-metadata-persistence.test.ts +4 -20
  24. package/src/__tests__/conversation-agent-loop.test.ts +529 -1
  25. package/src/__tests__/conversation-error.test.ts +18 -0
  26. package/src/__tests__/conversation-pairing.test.ts +69 -0
  27. package/src/__tests__/conversation-queue.test.ts +257 -4
  28. package/src/__tests__/conversation-runtime-assembly.test.ts +27 -0
  29. package/src/__tests__/emit-signal-routing-intent.test.ts +62 -0
  30. package/src/__tests__/evict-conversations-for-reload.test.ts +21 -0
  31. package/src/__tests__/gemini-provider.test.ts +9 -0
  32. package/src/__tests__/host-cu-proxy.test.ts +79 -0
  33. package/src/__tests__/host-cu-routes-targeted.test.ts +14 -5
  34. package/src/__tests__/introduction-card-resolver.test.ts +53 -8
  35. package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
  36. package/src/__tests__/non-member-access-request.test.ts +48 -0
  37. package/src/__tests__/notification-events-source-context-probe.test.ts +48 -1
  38. package/src/__tests__/notification-platform-adapter.test.ts +97 -0
  39. package/src/__tests__/notification-vellum-adapter.test.ts +103 -3
  40. package/src/__tests__/platform-callback-registration.test.ts +46 -1
  41. package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
  42. package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
  43. package/src/__tests__/scheduler-disk-pressure.test.ts +1 -0
  44. package/src/__tests__/scheduler-result-notification.test.ts +521 -7
  45. package/src/__tests__/scheduler-wake.test.ts +1 -0
  46. package/src/__tests__/skill-load-inline-command.test.ts +35 -0
  47. package/src/__tests__/subagent-cron-run-attribution.test.ts +41 -2
  48. package/src/__tests__/subagent-manager-notify.test.ts +48 -0
  49. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +6 -6
  50. package/src/__tests__/usage-attribution.test.ts +31 -0
  51. package/src/__tests__/wake-schedule-escalation.test.ts +1 -0
  52. package/src/agent/loop.ts +23 -0
  53. package/src/api/ask-question-tool.ts +50 -0
  54. package/src/api/events/generation-handoff.ts +4 -0
  55. package/src/api/events/message-complete.ts +4 -0
  56. package/src/api/events/tool-result.ts +11 -0
  57. package/src/api/index.ts +11 -0
  58. package/src/api/requests/guardian-actions.test.ts +45 -0
  59. package/src/api/requests/guardian-actions.ts +20 -0
  60. package/src/api/responses/conversation-message.ts +4 -0
  61. package/src/api/responses/home.ts +27 -0
  62. package/src/approvals/guardian-decision-primitive.ts +6 -4
  63. package/src/approvals/guardian-request-resolvers.ts +35 -30
  64. package/src/avatar/notification-avatar.test.ts +42 -34
  65. package/src/avatar/notification-avatar.ts +1 -0
  66. package/src/calls/__tests__/call-controller.test.ts +18 -0
  67. package/src/calls/__tests__/voice-control-protocol.test.ts +6 -1
  68. package/src/calls/__tests__/voice-leg-coordinator.test.ts +70 -0
  69. package/src/calls/__tests__/voice-session-bridge.test.ts +116 -4
  70. package/src/calls/__tests__/voice-triage-escalate.test.ts +77 -11
  71. package/src/calls/call-controller.ts +3 -1
  72. package/src/calls/voice-control-protocol.ts +12 -4
  73. package/src/calls/voice-escalation-judge.ts +2 -2
  74. package/src/calls/voice-leg-coordinator.ts +23 -5
  75. package/src/calls/voice-screen-annotation.ts +44 -0
  76. package/src/calls/voice-session-bridge.ts +63 -20
  77. package/src/calls/voice-triage-escalate.ts +82 -27
  78. package/src/cli/commands/__tests__/conversations-clear.test.ts +156 -0
  79. package/src/cli/commands/conversations.help.ts +7 -2
  80. package/src/cli/commands/conversations.ts +36 -30
  81. package/src/cli/commands/oauth/index.help.ts +8 -2
  82. package/src/cli/commands/oauth/request.test.ts +32 -0
  83. package/src/cli/commands/platform/index.help.ts +2 -1
  84. package/src/cli/commands/platform/index.ts +8 -2
  85. package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +60 -0
  86. package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
  87. package/src/cli/lib/bundled-marketplace.json +35 -0
  88. package/src/cli/lib/bundled-plugin-packages.json +21 -0
  89. package/src/cli/lib/plugin-catalog-cache.ts +17 -2
  90. package/src/cli/lib/plugin-catalog-platform.ts +48 -9
  91. package/src/config/__tests__/default-profile-catalog.test.ts +28 -0
  92. package/src/config/__tests__/platform-identity.test.ts +59 -0
  93. package/src/config/__tests__/profile-order.test.ts +19 -0
  94. package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
  95. package/src/config/bundled-skills/computer-use/SKILL.md +20 -0
  96. package/src/config/bundled-skills/computer-use/TOOLS.json +11 -11
  97. package/src/config/bundled-skills/screen-annotation/SKILL.md +41 -29
  98. package/src/config/bundled-skills/screen-annotation/TOOLS.json +5 -5
  99. package/src/config/call-site-defaults.ts +6 -0
  100. package/src/config/default-profile-catalog.ts +30 -1
  101. package/src/config/default-profile-names.ts +36 -2
  102. package/src/config/feature-flag-registry.json +9 -1
  103. package/src/config/platform-identity.ts +22 -5
  104. package/src/config/profile-order.ts +7 -1
  105. package/src/config/schemas/call-site-catalog.ts +7 -0
  106. package/src/config/schemas/llm.ts +21 -5
  107. package/src/config/sync-gated-profiles.ts +96 -44
  108. package/src/daemon/__tests__/abort-null-controller.test.ts +135 -1
  109. package/src/daemon/__tests__/auto-profile-router.test.ts +380 -0
  110. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +16 -0
  111. package/src/daemon/activity-trail.test.ts +206 -0
  112. package/src/daemon/activity-trail.ts +190 -0
  113. package/src/daemon/auto-profile-router.ts +431 -0
  114. package/src/daemon/computer-use-observation.test.ts +81 -0
  115. package/src/daemon/computer-use-observation.ts +23 -0
  116. package/src/daemon/conversation-agent-loop-handlers.ts +11 -0
  117. package/src/daemon/conversation-agent-loop.ts +157 -32
  118. package/src/daemon/conversation-error.ts +19 -0
  119. package/src/daemon/conversation-lifecycle.ts +70 -7
  120. package/src/daemon/conversation-media-retry.ts +1 -2
  121. package/src/daemon/conversation-messaging.ts +31 -2
  122. package/src/daemon/conversation-process.ts +107 -16
  123. package/src/daemon/conversation-queue-manager.ts +7 -0
  124. package/src/daemon/conversation-runtime-assembly.ts +1 -0
  125. package/src/daemon/conversation-turn-finalize.ts +14 -0
  126. package/src/daemon/conversation.ts +25 -6
  127. package/src/daemon/event-loop-watchdog.test.ts +201 -2
  128. package/src/daemon/event-loop-watchdog.ts +182 -6
  129. package/src/daemon/host-cu-proxy.ts +34 -3
  130. package/src/daemon/pkb-reminder-builder.test.ts +10 -3
  131. package/src/daemon/pkb-reminder-builder.ts +1 -1
  132. package/src/daemon/wake-conversation-ops.ts +4 -2
  133. package/src/desktop/desktop-computer-use.test.ts +14 -4
  134. package/src/home/feed-types.ts +2 -0
  135. package/src/inbound/platform-callback-registration.ts +19 -6
  136. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
  137. package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +83 -0
  138. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +48 -1
  139. package/src/live-voice/__tests__/live-voice-vad.test.ts +34 -2
  140. package/src/live-voice/__tests__/protocol.test.ts +22 -0
  141. package/src/live-voice/continuation-judge.ts +1 -1
  142. package/src/live-voice/live-voice-session.ts +25 -19
  143. package/src/live-voice/protocol.ts +16 -5
  144. package/src/messaging/providers/discord/withdraw.ts +4 -5
  145. package/src/messaging/providers/gmail/client.ts +4 -0
  146. package/src/messaging/providers/slack/withdraw.ts +5 -5
  147. package/src/messaging/providers/telegram-bot/withdraw.ts +4 -5
  148. package/src/monitoring/__tests__/proc-fixtures.ts +28 -0
  149. package/src/monitoring/__tests__/proc-wait-state.test.ts +232 -0
  150. package/src/monitoring/__tests__/process-usage.test.ts +243 -0
  151. package/src/monitoring/proc-wait-state.ts +366 -0
  152. package/src/monitoring/process-memory.ts +4 -3
  153. package/src/monitoring/process-usage.ts +215 -0
  154. package/src/monitoring/resource-sample-types.ts +8 -0
  155. package/src/monitoring/resource-sampler.ts +17 -2
  156. package/src/monitoring/stall-capture.ts +12 -0
  157. package/src/notifications/README.md +88 -9
  158. package/src/notifications/__tests__/assistant-reply-producer.test.ts +390 -119
  159. package/src/notifications/__tests__/background-result-producer.test.ts +1272 -0
  160. package/src/notifications/__tests__/broadcaster.test.ts +292 -1
  161. package/src/notifications/__tests__/completion-policy.test.ts +243 -0
  162. package/src/notifications/__tests__/decision-engine.test.ts +51 -4
  163. package/src/notifications/__tests__/has-pending-background-work.test.ts +281 -0
  164. package/src/notifications/__tests__/home-feed-side-effect.test.ts +161 -3
  165. package/src/notifications/__tests__/resolve-visible-in-source.test.ts +110 -1
  166. package/src/notifications/__tests__/schedule-result-producer.test.ts +11 -4
  167. package/src/notifications/adapters/macos.ts +24 -12
  168. package/src/notifications/adapters/platform.ts +16 -5
  169. package/src/notifications/assistant-reply-producer.ts +96 -71
  170. package/src/notifications/background-result-producer.ts +369 -0
  171. package/src/notifications/broadcaster.ts +28 -8
  172. package/src/notifications/completion-policy.ts +115 -0
  173. package/src/notifications/completion-work.ts +46 -0
  174. package/src/notifications/conversation-pairing.ts +32 -13
  175. package/src/notifications/decision-engine.ts +18 -24
  176. package/src/notifications/emit-signal.ts +16 -1
  177. package/src/notifications/events-store.ts +4 -0
  178. package/src/notifications/has-pending-background-work.ts +91 -0
  179. package/src/notifications/home-feed-mirror.ts +44 -0
  180. package/src/notifications/home-feed-side-effect.ts +54 -28
  181. package/src/notifications/resolve-visible-in-source.ts +56 -2
  182. package/src/notifications/result-output.ts +154 -0
  183. package/src/notifications/schedule-result-producer.ts +6 -187
  184. package/src/notifications/types.ts +2 -0
  185. package/src/oauth/AGENTS.md +9 -1
  186. package/src/oauth/byo-connection.test.ts +28 -0
  187. package/src/persistence/__tests__/recent-conversation-messages.test.ts +41 -0
  188. package/src/persistence/conversation-crud.ts +26 -0
  189. package/src/persistence/conversation-types.ts +15 -37
  190. package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +85 -14
  191. package/src/persistence/jobs-store.ts +229 -0
  192. package/src/persistence/migrations/383-normalize-opencode-host-connections.ts +44 -0
  193. package/src/persistence/migrations/__tests__/383-normalize-opencode-host-connections.test.ts +102 -0
  194. package/src/persistence/steps.ts +12 -0
  195. package/src/platform/sync-avatar.test.ts +29 -0
  196. package/src/plugins/defaults/memory/AGENTS.md +5 -3
  197. package/src/plugins/defaults/memory/__tests__/skill-update-receipt-boundary-guard.test.ts +45 -0
  198. package/src/plugins/defaults/memory/__tests__/skill-update-receipt-job.test.ts +580 -0
  199. package/src/plugins/defaults/memory/__tests__/turn-retrieval.test.ts +21 -0
  200. package/src/plugins/defaults/memory/graph/tools.ts +9 -5
  201. package/src/plugins/defaults/memory/hooks/conversation-deleted.ts +4 -0
  202. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +7 -3
  203. package/src/plugins/defaults/memory/job-handlers.ts +8 -2
  204. package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
  205. package/src/plugins/defaults/memory/tools.test.ts +17 -0
  206. package/src/plugins/defaults/memory/turn-retrieval.ts +8 -0
  207. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +24 -0
  208. package/src/plugins/defaults/memory/v3/injector.ts +4 -4
  209. package/src/plugins/types.ts +2 -0
  210. package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
  211. package/src/providers/anthropic/client.ts +4 -0
  212. package/src/providers/gemini/client.ts +7 -1
  213. package/src/providers/inference/auth.ts +25 -0
  214. package/src/providers/inference/profile-probe.ts +1 -0
  215. package/src/{calls/__tests__/typesafe-noul.test.ts → providers/jev/ask.test.ts} +4 -4
  216. package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +67 -34
  217. package/src/providers/openai/__tests__/api-error-normalization.test.ts +12 -0
  218. package/src/providers/openai/api-error-normalization.ts +4 -0
  219. package/src/providers/retry.ts +2 -0
  220. package/src/providers/search-provider-catalog.ts +10 -0
  221. package/src/providers/types.ts +7 -0
  222. package/src/providers/usage-tracking.ts +5 -3
  223. package/src/runtime/__tests__/agent-wake.test.ts +308 -0
  224. package/src/runtime/access-request-helper.ts +16 -9
  225. package/src/runtime/agent-wake-queue.ts +52 -0
  226. package/src/runtime/agent-wake.ts +111 -18
  227. package/src/runtime/channel-approval-types.ts +6 -38
  228. package/src/runtime/channel-approvals.ts +4 -5
  229. package/src/runtime/http-server.ts +4 -1
  230. package/src/runtime/introduction-policy.test.ts +17 -1
  231. package/src/runtime/introduction-policy.ts +25 -7
  232. package/src/runtime/migrations/vbundle-builder.ts +9 -0
  233. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +21 -1
  234. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
  235. package/src/runtime/routes/__tests__/oauth-proxy-routes.test.ts +61 -3
  236. package/src/runtime/routes/auto-profile-routes.ts +83 -0
  237. package/src/runtime/routes/conversation-management-routes.ts +1 -3
  238. package/src/runtime/routes/conversation-routes.ts +9 -0
  239. package/src/runtime/routes/guardian-action-routes.ts +8 -23
  240. package/src/runtime/routes/host-cu-routes.ts +3 -1
  241. package/src/runtime/routes/index.ts +2 -0
  242. package/src/runtime/routes/inference-profiles-routes.ts +5 -2
  243. package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
  244. package/src/runtime/routes/migration-routes.ts +8 -2
  245. package/src/runtime/routes/oauth-proxy-passthrough.test.ts +32 -0
  246. package/src/runtime/routes/platform-routes.ts +6 -1
  247. package/src/runtime/routes/user-route-dispatcher.ts +1 -2
  248. package/src/runtime/web-presence.ts +3 -4
  249. package/src/schedule/schedule-store.ts +10 -0
  250. package/src/schedule/scheduler.ts +92 -24
  251. package/src/subagent/manager.ts +32 -12
  252. package/src/telegram/__tests__/webhook-health.test.ts +8 -0
  253. package/src/telemetry/telemetry-wire-source.json +1 -1
  254. package/src/telemetry/telemetry-wire.generated.ts +1 -0
  255. package/src/tools/background-tool-registry.ts +30 -3
  256. package/src/tools/browser/cdp-client/local-cdp-client.ts +1 -2
  257. package/src/tools/host-terminal/host-shell.ts +7 -0
  258. package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +28 -1
  259. package/src/tools/network/__tests__/web-fetch-metadata.test.ts +34 -0
  260. package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +35 -1
  261. package/src/tools/network/__tests__/web-search.test.ts +96 -0
  262. package/src/tools/network/web-fetch.ts +30 -9
  263. package/src/tools/network/web-search.ts +235 -4
  264. package/src/tools/skills/load.ts +13 -17
  265. package/src/tools/skills/scaffold-managed.ts +86 -71
  266. package/src/tools/terminal/shell-launch.test.ts +27 -1
  267. package/src/tools/terminal/shell.ts +4 -0
  268. package/src/tools/workspace-tools/loader.ts +1 -2
  269. package/src/usage/attribution.ts +18 -1
  270. package/src/util/abort-reasons.ts +2 -0
  271. package/src/util/errors.ts +1 -0
  272. package/src/util/oauth-request-body.test.ts +5 -1
  273. package/src/util/provider-error-patterns.ts +11 -0
  274. package/src/util/single-flight.test.ts +6 -0
  275. package/src/util/single-flight.ts +2 -0
  276. package/src/util/worker-pipe.test.ts +56 -10
  277. package/src/util/worker-pipe.ts +9 -4
  278. package/src/workspace/git-service.ts +6 -1
  279. package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
  280. package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
  281. package/src/workspace/migrations/registry.ts +2 -0
@@ -87,18 +87,36 @@ mock.module("../notifications/emit-signal.js", () => ({
87
87
  }));
88
88
 
89
89
  import type { AssistantEvent } from "../api/index.js";
90
+ import { getConfig } from "../config/loader.js";
90
91
  import type { Conversation } from "../daemon/conversation.js";
92
+ import {
93
+ type AbortContext,
94
+ abortScheduledRun,
95
+ } from "../daemon/conversation-lifecycle.js";
96
+ import {
97
+ MessageQueue,
98
+ type QueuedDispatch,
99
+ } from "../daemon/conversation-queue-manager.js";
91
100
  import {
92
101
  clearConversations,
93
102
  setConversation,
103
+ setSubagentConversation,
94
104
  } from "../daemon/conversation-registry.js";
95
105
  import { addMessage } from "../persistence/conversation-crud.js";
96
106
  import { getDb } from "../persistence/db-connection.js";
97
107
  import { initializeDb } from "../persistence/db-init.js";
108
+ import { wakeAgentForOpportunity } from "../runtime/agent-wake.js";
98
109
  import { createSchedule, getScheduleRuns } from "../schedule/schedule-store.js";
99
110
  import { runDueSchedulesOnce } from "../schedule/scheduler.js";
100
111
  import { getSubagentManager } from "../subagent/index.js";
101
112
  import type { SubagentState } from "../subagent/types.js";
113
+ import {
114
+ _clearRegistryForTesting,
115
+ cancelBackgroundTool,
116
+ registerBackgroundTool,
117
+ removeBackgroundTool,
118
+ } from "../tools/background-tool-registry.js";
119
+ import { setConfig } from "./helpers/set-config.js";
102
120
 
103
121
  await initializeDb();
104
122
 
@@ -123,7 +141,10 @@ function latestAssistantText(conversationId: string): string | undefined {
123
141
  * run delegated. Returns a handle that settles it, mirroring what a real
124
142
  * child's teardown does.
125
143
  */
126
- function attachRunningChild(parentConversationId: string): {
144
+ function attachRunningChild(
145
+ parentConversationId: string,
146
+ cronRunId?: string,
147
+ ): {
127
148
  settle: () => void;
128
149
  } {
129
150
  const manager = getSubagentManager();
@@ -139,13 +160,14 @@ function attachRunningChild(parentConversationId: string): {
139
160
  >;
140
161
  parentToChildren: Map<string, Set<string>>;
141
162
  };
142
- const subagentId = `sub-${parentConversationId}`;
163
+ const subagentId = `sub-${parentConversationId}-${cronRunId ?? "default"}`;
143
164
  const entry = {
144
165
  conversation: null,
145
166
  state: {
146
167
  config: {
147
168
  id: subagentId,
148
169
  parentConversationId,
170
+ cronRunId,
149
171
  label: "advisor",
150
172
  objective: "Advise on the briefing",
151
173
  role: "advisor",
@@ -160,7 +182,10 @@ function attachRunningChild(parentConversationId: string): {
160
182
  runInFlight: true,
161
183
  };
162
184
  internals.subagents.set(subagentId, entry);
163
- internals.parentToChildren.set(parentConversationId, new Set([subagentId]));
185
+ const children =
186
+ internals.parentToChildren.get(parentConversationId) ?? new Set<string>();
187
+ children.add(subagentId);
188
+ internals.parentToChildren.set(parentConversationId, children);
164
189
  return {
165
190
  settle: () => {
166
191
  entry.state.status = "completed";
@@ -187,6 +212,7 @@ describe("schedule result notification wiring", () => {
187
212
  producerSawText.length = 0;
188
213
  runBackgroundJobShouldFail = false;
189
214
  clearConversations();
215
+ _clearRegistryForTesting();
190
216
  delegateOnRun = undefined;
191
217
  });
192
218
 
@@ -252,12 +278,13 @@ describe("schedule result notification wiring", () => {
252
278
  // The turn's own reply, written before the guidance exists.
253
279
  addMessage(conversationId, "assistant", "Here is a first pass.");
254
280
 
255
- const child = attachRunningChild(conversationId);
281
+ const cronRunId = getScheduleRuns(schedule.id)[0].id;
282
+ const child = attachRunningChild(conversationId, cronRunId);
256
283
  let continuationRunning = false;
257
284
  setConversation(conversationId, {
258
- hasInFlightWork: () =>
259
- getSubagentManager().hasActiveChildren(conversationId) ||
260
- continuationRunning,
285
+ isProcessing: () => continuationRunning,
286
+ currentTurnCronRunId: cronRunId,
287
+ snapshotQueuedMessages: () => [],
261
288
  } as unknown as Conversation);
262
289
 
263
290
  // The advisor settles, then its notification starts the continuation
@@ -307,6 +334,493 @@ describe("schedule result notification wiring", () => {
307
334
  expect(Date.now() - before).toBeLessThan(200);
308
335
  });
309
336
 
337
+ for (const quiet of [false, true]) {
338
+ for (const status of ["completed", "failed", "cancelled"] as const) {
339
+ test(`waits for a ${status} background command and its final wake (quiet=${quiet})`, async () => {
340
+ const schedule = await createSchedule({
341
+ name: "Background report",
342
+ message: "Run the report in the background",
343
+ syntax: "cron",
344
+ expression: "0 9 * * *",
345
+ quiet,
346
+ });
347
+ forceScheduleDue(schedule.id);
348
+ const started = Promise.withResolvers<string>();
349
+ const finishWake = Promise.withResolvers<void>();
350
+ delegateOnRun = (conversationId) => {
351
+ addMessage(conversationId, "assistant", "The command is running.");
352
+ setConversation(conversationId, {
353
+ isProcessing: () => false,
354
+ snapshotQueuedMessages: () => [],
355
+ } as unknown as Conversation);
356
+ registerBackgroundTool({
357
+ id: "bg-scheduled",
358
+ cronRunId: getScheduleRuns(schedule.id)[0].id,
359
+ conversationId,
360
+ toolName: "bash",
361
+ command: "generate-report",
362
+ startedAt: Date.now(),
363
+ cancel: () => {},
364
+ });
365
+ if (status === "cancelled") {
366
+ cancelBackgroundTool("bg-scheduled");
367
+ }
368
+ started.resolve(conversationId);
369
+ };
370
+
371
+ let scheduleSettled = false;
372
+ const scheduledRun = runDueSchedulesOnce().then(() => {
373
+ scheduleSettled = true;
374
+ });
375
+ const conversationId = await started.promise;
376
+ await new Promise((resolve) => setTimeout(resolve, 250));
377
+ expect(scheduleSettled).toBe(false);
378
+ expect(producerCalls).toHaveLength(0);
379
+
380
+ clearConversations();
381
+ removeBackgroundTool("bg-scheduled");
382
+ const wake = wakeAgentForOpportunity(
383
+ {
384
+ conversationId,
385
+ cronRunId: getScheduleRuns(schedule.id)[0].id,
386
+ hint: `Background command ${status}`,
387
+ source: "background-tool",
388
+ },
389
+ {
390
+ resolveTarget: async () => {
391
+ await finishWake.promise;
392
+ addMessage(
393
+ conversationId,
394
+ "assistant",
395
+ `Final result: ${status}.`,
396
+ );
397
+ return null;
398
+ },
399
+ },
400
+ );
401
+ await new Promise((resolve) => setTimeout(resolve, 250));
402
+ expect(scheduleSettled).toBe(false);
403
+ expect(producerCalls).toHaveLength(0);
404
+
405
+ finishWake.resolve();
406
+ await wake;
407
+ await scheduledRun;
408
+ expect(getScheduleRuns(schedule.id)[0].status).toBe("ok");
409
+ expect(producerCalls).toHaveLength(quiet ? 0 : 1);
410
+ if (!quiet) {
411
+ expect(producerSawText).toEqual([`Final result: ${status}.`]);
412
+ }
413
+ });
414
+ }
415
+ }
416
+
417
+ for (const quiet of [false, true]) {
418
+ for (const kind of ["bash", "host_bash", "wake", "subagent"] as const) {
419
+ test(`a timed-out ${kind} fails without a partial result (quiet=${quiet})`, async () => {
420
+ const timeouts = getConfig().timeouts;
421
+ setConfig("timeouts", { ...timeouts, scheduleTurnTimeoutSec: 1 });
422
+ const schedule = await createSchedule({
423
+ name: "Background report",
424
+ message: "Prepare the report",
425
+ syntax: "cron",
426
+ expression: "0 9 * * *",
427
+ maxRetries: 1,
428
+ quiet,
429
+ });
430
+ forceScheduleDue(schedule.id);
431
+ let cancelled = false;
432
+ let parentAborted = false;
433
+ let wake: ReturnType<typeof wakeAgentForOpportunity> | undefined;
434
+ const finishLookup = Promise.withResolvers<void>();
435
+ delegateOnRun = (conversationId) => {
436
+ addMessage(
437
+ conversationId,
438
+ "assistant",
439
+ "The report is still running.",
440
+ );
441
+ const cronRunId = getScheduleRuns(schedule.id)[0].id;
442
+ setConversation(conversationId, {
443
+ isProcessing: () => false,
444
+ snapshotQueuedMessages: () => [],
445
+ abortScheduledRun: (runId: string) => {
446
+ expect(runId).toBe(cronRunId);
447
+ parentAborted = true;
448
+ },
449
+ } as unknown as Conversation);
450
+ if (kind === "wake") {
451
+ wake = wakeAgentForOpportunity(
452
+ {
453
+ conversationId,
454
+ cronRunId: getScheduleRuns(schedule.id)[0].id,
455
+ source: "background-tool",
456
+ hint: "The command finished",
457
+ },
458
+ {
459
+ resolveTarget: async () => {
460
+ await finishLookup.promise;
461
+ return {
462
+ waitForIdle: async () => true,
463
+ isProcessing: () => false,
464
+ } as unknown as Conversation;
465
+ },
466
+ },
467
+ );
468
+ } else {
469
+ let toolConversationId = conversationId;
470
+ if (kind === "subagent") {
471
+ attachRunningChild(conversationId, cronRunId);
472
+ toolConversationId =
473
+ getSubagentManager().getChildrenOf(conversationId)[0]
474
+ .conversationId;
475
+ }
476
+ registerBackgroundTool({
477
+ id: "bg-timeout",
478
+ cronRunId,
479
+ conversationId: toolConversationId,
480
+ toolName: kind,
481
+ command: "generate-report",
482
+ startedAt: Date.now(),
483
+ cancel: () => {
484
+ cancelled = true;
485
+ removeBackgroundTool("bg-timeout");
486
+ },
487
+ });
488
+ }
489
+ };
490
+ try {
491
+ const result = await runDueSchedulesOnce();
492
+ expect(getScheduleRuns(schedule.id)[0]).toMatchObject({
493
+ status: "error",
494
+ error: "Scheduled work did not finish before its time limit",
495
+ });
496
+ expect(producerCalls).toHaveLength(0);
497
+ expect(parentAborted).toBe(true);
498
+ if (kind !== "wake") {
499
+ expect(cancelled).toBe(true);
500
+ }
501
+ finishLookup.resolve();
502
+ if (wake) {
503
+ expect(await wake).toMatchObject({
504
+ invoked: false,
505
+ reason: "timeout",
506
+ });
507
+ }
508
+ expect(result.failed).toBe(1);
509
+ } finally {
510
+ finishLookup.resolve();
511
+ await wake;
512
+ removeBackgroundTool("bg-timeout");
513
+ setConfig("timeouts", timeouts);
514
+ }
515
+ });
516
+ }
517
+ }
518
+
519
+ for (const userTurnActive of [true, false]) {
520
+ test(`timeout preserves unrelated work (user turn active=${userTurnActive})`, async () => {
521
+ const timeouts = getConfig().timeouts;
522
+ setConfig("timeouts", { ...timeouts, scheduleTurnTimeoutSec: 1 });
523
+ const schedule = await createSchedule({
524
+ name: "Report",
525
+ message: "Prepare a report",
526
+ syntax: "cron",
527
+ expression: "0 9 * * *",
528
+ maxRetries: 1,
529
+ });
530
+ forceScheduleDue(schedule.id);
531
+ const controller = new AbortController();
532
+ const queue = new MessageQueue();
533
+ const userEvents: string[] = [];
534
+ const cancelledTools: string[] = [];
535
+ let conversationId = "";
536
+ let runId = "";
537
+ let settleUserChild: (() => void) | undefined;
538
+ delegateOnRun = (id) => {
539
+ conversationId = id;
540
+ runId = getScheduleRuns(schedule.id)[0].id;
541
+ const ctx = {
542
+ conversationId: id,
543
+ currentTurnCronRunId: userTurnActive ? undefined : runId,
544
+ isProcessing: () => true,
545
+ setProcessing: () => {},
546
+ abortController: controller,
547
+ queue,
548
+ pendingInterruptRepair: false,
549
+ prompter: { dispose: () => {} },
550
+ secretPrompter: { dispose: () => {} },
551
+ pendingSurfaceActions: new Map(),
552
+ surfaceActionRequestIds: new Set(),
553
+ surfaceState: new Map(),
554
+ accumulatedSurfaceState: new Map(),
555
+ kickDrainQueue: async () => {},
556
+ } as unknown as AbortContext;
557
+ setConversation(id, {
558
+ ...ctx,
559
+ snapshotQueuedMessages: () => queue.snapshot(),
560
+ abortScheduledRun: (run: string) => abortScheduledRun(ctx, run),
561
+ } as unknown as Conversation);
562
+ for (const owner of [undefined, runId, "run-other"]) {
563
+ queue.push({
564
+ content: "Follow-up",
565
+ attachments: [],
566
+ requestId: `queued-${owner ?? "user"}`,
567
+ sentAt: Date.now(),
568
+ cronRunId: owner,
569
+ onEvent: (event) => {
570
+ if (owner !== runId) {
571
+ userEvents.push(event.type);
572
+ }
573
+ },
574
+ });
575
+ const id = `tool-${owner ?? "user"}`;
576
+ registerBackgroundTool({
577
+ id,
578
+ cronRunId: owner,
579
+ conversationId,
580
+ toolName: "bash",
581
+ command: "example-command",
582
+ startedAt: Date.now(),
583
+ cancel: () => {
584
+ cancelledTools.push(id);
585
+ removeBackgroundTool(id);
586
+ },
587
+ });
588
+ }
589
+ attachRunningChild(id, runId);
590
+ settleUserChild = attachRunningChild(id).settle;
591
+ };
592
+ try {
593
+ await runDueSchedulesOnce();
594
+ expect(controller.signal.aborted).toBe(!userTurnActive);
595
+ expect(queue.snapshot().map((message) => message.requestId)).toEqual([
596
+ "queued-user",
597
+ "queued-run-other",
598
+ ]);
599
+ expect(userEvents).toEqual([]);
600
+ expect(cancelledTools).toEqual([`tool-${runId}`]);
601
+ const children = getSubagentManager().getChildrenOf(conversationId);
602
+ expect(
603
+ children.find((child) => child.config.cronRunId === runId)?.status,
604
+ ).toBe("aborted");
605
+ expect(
606
+ children.find((child) => child.config.cronRunId === undefined)
607
+ ?.status,
608
+ ).toBe("running");
609
+ expect(producerCalls).toHaveLength(0);
610
+ } finally {
611
+ settleUserChild?.();
612
+ _clearRegistryForTesting();
613
+ setConfig("timeouts", timeouts);
614
+ }
615
+ });
616
+ }
617
+
618
+ for (const kind of [
619
+ "turn",
620
+ "queue",
621
+ "command",
622
+ "child",
623
+ "wake",
624
+ "dispatch",
625
+ ] as const) {
626
+ test(`unrelated ${kind} does not keep a settled firing open`, async () => {
627
+ const timeouts = getConfig().timeouts;
628
+ setConfig("timeouts", { ...timeouts, scheduleTurnTimeoutSec: 1 });
629
+ const schedule = await createSchedule({
630
+ name: "Report",
631
+ message: "Prepare a report",
632
+ syntax: "cron",
633
+ expression: "0 9 * * *",
634
+ });
635
+ forceScheduleDue(schedule.id);
636
+ const delegated = Promise.withResolvers<void>();
637
+ const releaseWake = Promise.withResolvers<void>();
638
+ let wake: ReturnType<typeof wakeAgentForOpportunity> | undefined;
639
+ let userChild: ReturnType<typeof attachRunningChild> | undefined;
640
+ let conversationId = "";
641
+ delegateOnRun = (id) => {
642
+ conversationId = id;
643
+ const runId = getScheduleRuns(schedule.id)[0].id;
644
+ const queue = new MessageQueue();
645
+ if (kind === "queue") {
646
+ queue.push({
647
+ content: "User follow-up",
648
+ attachments: [],
649
+ requestId: "queued-user",
650
+ sentAt: Date.now(),
651
+ onEvent: () => {},
652
+ });
653
+ }
654
+ setConversation(id, {
655
+ isProcessing: () => kind === "turn",
656
+ currentTurnCronRunId: undefined,
657
+ pendingQueuedDispatches:
658
+ kind === "dispatch"
659
+ ? new Map<string | null, Set<QueuedDispatch>>([
660
+ [
661
+ "run-other",
662
+ new Set([
663
+ { controller: new AbortController(), messages: [] },
664
+ ]),
665
+ ],
666
+ [
667
+ null,
668
+ new Set([
669
+ { controller: new AbortController(), messages: [] },
670
+ ]),
671
+ ],
672
+ ])
673
+ : new Map(),
674
+ snapshotQueuedMessages: () => queue.snapshot(),
675
+ } as unknown as Conversation);
676
+ registerBackgroundTool({
677
+ id: "owned-command",
678
+ conversationId: id,
679
+ cronRunId: runId,
680
+ toolName: "bash",
681
+ command: "report",
682
+ startedAt: Date.now(),
683
+ cancel: () => {},
684
+ });
685
+ if (kind === "command") {
686
+ registerBackgroundTool({
687
+ id: "user-command",
688
+ conversationId: id,
689
+ toolName: "bash",
690
+ command: "user-work",
691
+ startedAt: Date.now(),
692
+ cancel: () => {},
693
+ });
694
+ }
695
+ if (kind === "child") {
696
+ userChild = attachRunningChild(id);
697
+ }
698
+ if (kind === "wake") {
699
+ wake = wakeAgentForOpportunity(
700
+ {
701
+ conversationId: id,
702
+ source: "background-tool",
703
+ hint: "Unrelated result",
704
+ cronRunId: "run-other",
705
+ },
706
+ {
707
+ resolveTarget: async () => {
708
+ await releaseWake.promise;
709
+ return null;
710
+ },
711
+ },
712
+ );
713
+ }
714
+ delegated.resolve();
715
+ };
716
+ try {
717
+ const scheduledRun = runDueSchedulesOnce();
718
+ await delegated.promise;
719
+ await new Promise((resolve) => setTimeout(resolve, 150));
720
+ expect(producerCalls).toHaveLength(0);
721
+ addMessage(conversationId, "assistant", "Report finished.");
722
+ removeBackgroundTool("owned-command");
723
+ await scheduledRun;
724
+ expect(getScheduleRuns(schedule.id)[0].status).toBe("ok");
725
+ expect(producerSawText).toEqual(["Report finished."]);
726
+ } finally {
727
+ releaseWake.resolve();
728
+ await wake;
729
+ userChild?.settle();
730
+ _clearRegistryForTesting();
731
+ setConfig("timeouts", timeouts);
732
+ }
733
+ });
734
+ }
735
+
736
+ for (const location of [
737
+ "parent",
738
+ "completed-child",
739
+ "reused-child",
740
+ ] as const) {
741
+ test(`waits for owned queued work in a ${location}`, async () => {
742
+ const schedule = await createSchedule({
743
+ name: "Report",
744
+ message: "Prepare a report",
745
+ syntax: "cron",
746
+ expression: "0 9 * * *",
747
+ });
748
+ forceScheduleDue(schedule.id);
749
+ const delegated = Promise.withResolvers<string>();
750
+ const queue = new MessageQueue();
751
+ delegateOnRun = (id) => {
752
+ const runId = getScheduleRuns(schedule.id)[0].id;
753
+ queue.push({
754
+ content: "Continue report",
755
+ attachments: [],
756
+ requestId: "queued-schedule",
757
+ sentAt: Date.now(),
758
+ cronRunId: runId,
759
+ onEvent: () => {},
760
+ });
761
+ const live = {
762
+ isProcessing: () => false,
763
+ snapshotQueuedMessages: () => queue.snapshot(),
764
+ } as unknown as Conversation;
765
+ if (location === "parent") {
766
+ setConversation(id, live);
767
+ } else {
768
+ attachRunningChild(
769
+ id,
770
+ location === "reused-child" ? undefined : runId,
771
+ ).settle();
772
+ setSubagentConversation(
773
+ getSubagentManager().getChildrenOf(id)[0].conversationId,
774
+ live,
775
+ );
776
+ }
777
+ delegated.resolve(id);
778
+ };
779
+ const scheduledRun = runDueSchedulesOnce();
780
+ const id = await delegated.promise;
781
+ await new Promise((resolve) => setTimeout(resolve, 150));
782
+ expect(producerCalls).toHaveLength(0);
783
+ addMessage(id, "assistant", "Final report.");
784
+ queue.clear();
785
+ await scheduledRun;
786
+ expect(getScheduleRuns(schedule.id)[0].status).toBe("ok");
787
+ expect(producerSawText).toEqual(["Final report."]);
788
+ });
789
+ }
790
+
791
+ test("waits for a dequeued dispatch before its processing claim", async () => {
792
+ const schedule = await createSchedule({
793
+ name: "Report",
794
+ message: "Prepare report",
795
+ syntax: "cron",
796
+ expression: "0 9 * * *",
797
+ });
798
+ forceScheduleDue(schedule.id);
799
+ const entered = Promise.withResolvers<string>();
800
+ const dispatches = new Map<string, Set<QueuedDispatch>>();
801
+ delegateOnRun = (id) => {
802
+ dispatches.set(
803
+ getScheduleRuns(schedule.id)[0].id,
804
+ new Set([{ controller: new AbortController(), messages: [] }]),
805
+ );
806
+ setConversation(id, {
807
+ isProcessing: () => false,
808
+ snapshotQueuedMessages: () => [],
809
+ pendingQueuedDispatches: dispatches,
810
+ } as unknown as Conversation);
811
+ entered.resolve(id);
812
+ };
813
+ const scheduledRun = runDueSchedulesOnce();
814
+ const id = await entered.promise;
815
+ await new Promise((resolve) => setTimeout(resolve, 250));
816
+ expect(producerCalls).toHaveLength(0);
817
+ addMessage(id, "assistant", "Final report.");
818
+ dispatches.clear();
819
+ await scheduledRun;
820
+ expect(getScheduleRuns(schedule.id)[0].status).toBe("ok");
821
+ expect(producerSawText).toEqual(["Final report."]);
822
+ });
823
+
310
824
  test("captures runStartedAt before the run, not after", async () => {
311
825
  const before = Date.now();
312
826
  const schedule = await createSchedule({
@@ -8,6 +8,7 @@ const mockWakeAgentForOpportunity = mock(
8
8
  }> => Promise.resolve({ invoked: true, producedToolCalls: false }),
9
9
  );
10
10
  mock.module("../runtime/agent-wake.js", () => ({
11
+ hasPendingAgentWake: () => false,
11
12
  wakeAgentForOpportunity: mockWakeAgentForOpportunity,
12
13
  }));
13
14
 
@@ -109,6 +109,41 @@ async function executeSkillLoad(
109
109
  // ── Tests ─────────────────────────────────────────────────────────────────
110
110
 
111
111
  describe("skill_load inline command expansion", () => {
112
+ test("read-only preloading returns instructions without running inline commands", async () => {
113
+ writeSkill(
114
+ "readonly-example",
115
+ "readonly-example",
116
+ "Example skill",
117
+ "Context: !`echo example`",
118
+ );
119
+ const result = await skillLoadTool.execute(
120
+ { skill: "readonly-example" },
121
+ {
122
+ conversationId: "conversation-1",
123
+ workingDir: TEST_DIR,
124
+ trustClass: "guardian",
125
+ },
126
+ { readOnly: true },
127
+ );
128
+ expect(result.isError).toBe(false);
129
+ expect(result.content).toContain("[inline command not executed]");
130
+ expect(runInlineCommandCalls).toHaveLength(0);
131
+ expect(mockAutoInstall).not.toHaveBeenCalled();
132
+ });
133
+
134
+ test("read-only preloading does not install missing skills", async () => {
135
+ const result = await skillLoadTool.execute(
136
+ { skill: "missing-readonly-skill" },
137
+ {
138
+ conversationId: "conversation-1",
139
+ workingDir: TEST_DIR,
140
+ trustClass: "guardian",
141
+ },
142
+ { readOnly: true },
143
+ );
144
+ expect(result.isError).toBe(true);
145
+ expect(mockAutoInstall).not.toHaveBeenCalled();
146
+ });
112
147
  beforeEach(() => {
113
148
  mkdirSync(join(TEST_DIR, "skills"), { recursive: true });
114
149
  runInlineCommandCalls.length = 0;