neoagent 3.2.1-beta.1 → 3.2.1-beta.11

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 (198) hide show
  1. package/extensions/chrome-browser/background.mjs +318 -88
  2. package/extensions/chrome-browser/http.mjs +136 -0
  3. package/extensions/chrome-browser/protocol.mjs +654 -90
  4. package/flutter_app/lib/main_chat.dart +118 -739
  5. package/flutter_app/lib/main_controller.dart +157 -24
  6. package/flutter_app/lib/main_integrations.dart +607 -8
  7. package/flutter_app/lib/main_models.dart +7 -2
  8. package/flutter_app/lib/main_operations.dart +334 -370
  9. package/flutter_app/lib/main_security.dart +266 -112
  10. package/flutter_app/lib/main_settings.dart +342 -3
  11. package/flutter_app/lib/main_shared.dart +14 -11
  12. package/flutter_app/lib/src/backend_client.dart +97 -0
  13. package/flutter_app/lib/src/desktop_companion_actions.dart +185 -31
  14. package/flutter_app/lib/src/desktop_companion_io.dart +319 -86
  15. package/flutter_app/windows/runner/flutter_window.cpp +143 -32
  16. package/landing/index.html +3 -1
  17. package/lib/manager.js +106 -89
  18. package/lib/schema_migrations.js +115 -13
  19. package/package.json +30 -15
  20. package/runtime/paths.js +49 -5
  21. package/server/db/database.js +2 -2
  22. package/server/guest-agent.cli.package.json +13 -0
  23. package/server/guest_agent.js +85 -40
  24. package/server/http/middleware.js +24 -0
  25. package/server/http/routes.js +12 -6
  26. package/server/public/.last_build_id +1 -1
  27. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  28. package/server/public/flutter_bootstrap.js +2 -2
  29. package/server/public/main.dart.js +81930 -80509
  30. package/server/routes/admin.js +1 -1
  31. package/server/routes/android.js +30 -34
  32. package/server/routes/behavior.js +80 -0
  33. package/server/routes/browser.js +23 -15
  34. package/server/routes/desktop.js +18 -1
  35. package/server/routes/integrations.js +107 -1
  36. package/server/routes/memory.js +1 -0
  37. package/server/routes/settings.js +20 -5
  38. package/server/routes/social_reach.js +12 -3
  39. package/server/routes/social_video.js +4 -0
  40. package/server/services/agents/manager.js +1 -1
  41. package/server/services/ai/capabilityHealth.js +62 -96
  42. package/server/services/ai/compaction.js +7 -2
  43. package/server/services/ai/history.js +45 -6
  44. package/server/services/ai/integrated_tools/http_request.js +8 -0
  45. package/server/services/ai/loop/agent_engine_core.js +452 -176
  46. package/server/services/ai/loop/blank_recovery.js +5 -4
  47. package/server/services/ai/loop/callbacks.js +1 -0
  48. package/server/services/ai/loop/completion_judge.js +291 -8
  49. package/server/services/ai/loop/conversation_loop.js +515 -342
  50. package/server/services/ai/loop/messaging_delivery.js +176 -57
  51. package/server/services/ai/loop/model_call_guard.js +91 -0
  52. package/server/services/ai/loop/model_io.js +20 -45
  53. package/server/services/ai/loop/progress_classification.js +2 -0
  54. package/server/services/ai/loop/tool_dispatch.js +19 -8
  55. package/server/services/ai/loopPolicy.js +48 -21
  56. package/server/services/ai/messagingFallback.js +17 -17
  57. package/server/services/ai/model_discovery.js +227 -0
  58. package/server/services/ai/model_failure_cache.js +108 -0
  59. package/server/services/ai/model_identity.js +71 -0
  60. package/server/services/ai/models.js +68 -163
  61. package/server/services/ai/providerRetry.js +17 -59
  62. package/server/services/ai/provider_selector.js +166 -0
  63. package/server/services/ai/providers/anthropic.js +2 -2
  64. package/server/services/ai/providers/claudeCode.js +21 -33
  65. package/server/services/ai/providers/githubCopilot.js +41 -20
  66. package/server/services/ai/providers/google.js +135 -97
  67. package/server/services/ai/providers/grok.js +4 -3
  68. package/server/services/ai/providers/grokOauth.js +19 -27
  69. package/server/services/ai/providers/nvidia.js +10 -5
  70. package/server/services/ai/providers/ollama.js +111 -84
  71. package/server/services/ai/providers/ollama_stream.js +142 -0
  72. package/server/services/ai/providers/openai.js +39 -5
  73. package/server/services/ai/providers/openaiCodex.js +11 -4
  74. package/server/services/ai/providers/openrouter.js +29 -7
  75. package/server/services/ai/providers/provider_error.js +36 -0
  76. package/server/services/ai/settings.js +26 -2
  77. package/server/services/ai/systemPrompt.js +32 -123
  78. package/server/services/ai/taskAnalysis.js +104 -11
  79. package/server/services/ai/toolEvidence.js +256 -29
  80. package/server/services/ai/tools.js +248 -117
  81. package/server/services/android/controller.js +770 -237
  82. package/server/services/android/process.js +140 -0
  83. package/server/services/android/sdk_download.js +143 -0
  84. package/server/services/android/uia.js +6 -5
  85. package/server/services/artifacts/store.js +24 -0
  86. package/server/services/behavior/config.js +251 -0
  87. package/server/services/behavior/defaults.js +68 -0
  88. package/server/services/behavior/delivery.js +176 -0
  89. package/server/services/behavior/index.js +43 -0
  90. package/server/services/behavior/model_client.js +35 -0
  91. package/server/services/behavior/modules/agent_identity.js +37 -0
  92. package/server/services/behavior/modules/channel_style.js +28 -0
  93. package/server/services/behavior/modules/index.js +29 -0
  94. package/server/services/behavior/modules/norms.js +101 -0
  95. package/server/services/behavior/modules/persona.js +48 -0
  96. package/server/services/behavior/modules/persona_prompt.js +33 -0
  97. package/server/services/behavior/modules/social_memory.js +86 -0
  98. package/server/services/behavior/modules/social_observability.js +94 -0
  99. package/server/services/behavior/modules/social_signals.js +41 -0
  100. package/server/services/behavior/modules/theory_of_mind.js +110 -0
  101. package/server/services/behavior/modules/turn_taking.js +237 -0
  102. package/server/services/behavior/pipeline.js +285 -0
  103. package/server/services/behavior/registry.js +78 -0
  104. package/server/services/behavior/signals.js +107 -0
  105. package/server/services/behavior/state.js +99 -0
  106. package/server/services/behavior/system_prompt.js +75 -0
  107. package/server/services/browser/controller.js +843 -385
  108. package/server/services/browser/extension/gateway.js +40 -16
  109. package/server/services/browser/extension/protocol.js +15 -1
  110. package/server/services/browser/extension/provider.js +71 -47
  111. package/server/services/browser/extension/registry.js +155 -34
  112. package/server/services/cli/executor.js +62 -9
  113. package/server/services/credentials/bitwarden_cli.js +322 -0
  114. package/server/services/credentials/broker.js +594 -0
  115. package/server/services/desktop/gateway.js +41 -4
  116. package/server/services/desktop/protocol.js +3 -0
  117. package/server/services/desktop/provider.js +39 -42
  118. package/server/services/desktop/registry.js +137 -52
  119. package/server/services/integrations/bitwarden/constants.js +14 -0
  120. package/server/services/integrations/bitwarden/provider.js +197 -0
  121. package/server/services/integrations/bitwarden/snapshot.js +65 -0
  122. package/server/services/integrations/figma/provider.js +78 -12
  123. package/server/services/integrations/github/common.js +11 -6
  124. package/server/services/integrations/github/provider.js +52 -53
  125. package/server/services/integrations/google/provider.js +55 -19
  126. package/server/services/integrations/home_assistant/network.js +17 -20
  127. package/server/services/integrations/home_assistant/provider.js +7 -5
  128. package/server/services/integrations/home_assistant/tools.js +17 -5
  129. package/server/services/integrations/http.js +51 -0
  130. package/server/services/integrations/manager.js +159 -53
  131. package/server/services/integrations/microsoft/provider.js +80 -13
  132. package/server/services/integrations/neoarchive/provider.js +55 -29
  133. package/server/services/integrations/neorecall/client.js +17 -10
  134. package/server/services/integrations/neorecall/provider.js +20 -11
  135. package/server/services/integrations/notion/provider.js +16 -13
  136. package/server/services/integrations/oauth_provider.js +115 -51
  137. package/server/services/integrations/registry.js +2 -0
  138. package/server/services/integrations/slack/provider.js +98 -9
  139. package/server/services/integrations/spotify/provider.js +67 -71
  140. package/server/services/integrations/trello/provider.js +21 -7
  141. package/server/services/integrations/weather/provider.js +18 -12
  142. package/server/services/integrations/whatsapp/provider.js +76 -16
  143. package/server/services/manager.js +110 -1
  144. package/server/services/memory/embedding_index.js +20 -8
  145. package/server/services/memory/embeddings.js +151 -90
  146. package/server/services/memory/ingestion.js +50 -9
  147. package/server/services/memory/ingestion_documents.js +13 -3
  148. package/server/services/memory/manager.js +66 -52
  149. package/server/services/messaging/access_policy.js +10 -6
  150. package/server/services/messaging/automation.js +240 -34
  151. package/server/services/messaging/discord.js +11 -1
  152. package/server/services/messaging/formatting_guides.js +5 -4
  153. package/server/services/messaging/http_platforms.js +37 -16
  154. package/server/services/messaging/inbound_queue.js +143 -28
  155. package/server/services/messaging/inbound_store.js +257 -0
  156. package/server/services/messaging/manager.js +344 -51
  157. package/server/services/messaging/telegram.js +10 -1
  158. package/server/services/messaging/typing_keepalive.js +5 -2
  159. package/server/services/messaging/whatsapp.js +33 -14
  160. package/server/services/network/http.js +210 -0
  161. package/server/services/network/safe_request.js +307 -0
  162. package/server/services/runtime/backends/local-vm.js +227 -67
  163. package/server/services/runtime/docker-vm-manager.js +9 -0
  164. package/server/services/runtime/guest_bootstrap.js +30 -4
  165. package/server/services/runtime/guest_image.js +43 -12
  166. package/server/services/runtime/manager.js +77 -23
  167. package/server/services/runtime/validation.js +7 -6
  168. package/server/services/security/tool_categories.js +6 -0
  169. package/server/services/social_reach/channels/github.js +10 -4
  170. package/server/services/social_reach/channels/reddit.js +4 -4
  171. package/server/services/social_reach/channels/rss.js +2 -2
  172. package/server/services/social_reach/channels/social_video.js +13 -8
  173. package/server/services/social_reach/channels/v2ex.js +21 -8
  174. package/server/services/social_reach/channels/x.js +2 -2
  175. package/server/services/social_reach/channels/xueqiu.js +5 -5
  176. package/server/services/social_reach/service.js +9 -6
  177. package/server/services/social_reach/utils.js +65 -14
  178. package/server/services/social_video/captions.js +2 -2
  179. package/server/services/social_video/service.js +343 -68
  180. package/server/services/tasks/integration_runtime.js +18 -8
  181. package/server/services/tasks/runtime.js +39 -4
  182. package/server/services/voice/agentBridge.js +17 -4
  183. package/server/services/voice/bufferedLiveRelayAdapter.js +5 -0
  184. package/server/services/voice/liveSession.js +31 -0
  185. package/server/services/voice/message.js +1 -1
  186. package/server/services/voice/openaiSpeech.js +33 -8
  187. package/server/services/voice/providers.js +233 -151
  188. package/server/services/voice/runtime.js +2 -2
  189. package/server/services/voice/runtimeManager.js +118 -20
  190. package/server/services/voice/turnRunner.js +6 -0
  191. package/server/services/wearable/firmware_manifest.js +51 -13
  192. package/server/services/wearable/service.js +1 -0
  193. package/server/utils/abort.js +96 -0
  194. package/server/utils/cloud-security.js +110 -3
  195. package/server/utils/files.js +31 -0
  196. package/server/utils/image_payload.js +95 -0
  197. package/server/utils/logger.js +19 -0
  198. package/server/utils/retry.js +107 -0
@@ -19,8 +19,10 @@ const {
19
19
  const { summarizeCapabilityHealth } = require('../capabilityHealth');
20
20
  const { shouldAcceptTaskComplete } = require('../completion');
21
21
  const { shortenRunId, summarizeForLog } = require('../logFormat');
22
+ const { getProviderForUser } = require('../provider_selector');
22
23
  const { runConversation } = require('./conversation_loop');
23
24
  const {
25
+ TERMINAL_STATUSES,
24
26
  checkpointRun,
25
27
  closeRun,
26
28
  getRunControl,
@@ -30,6 +32,8 @@ const {
30
32
  const {
31
33
  buildChurnAssessmentPrompt,
32
34
  buildCompletionDecisionPrompt,
35
+ enforceTerminalReplyDecision,
36
+ enforceChurnAssessment,
33
37
  normalizeChurnAssessment,
34
38
  normalizeCompletionDecision,
35
39
  resolveRunGoalContext,
@@ -67,7 +71,7 @@ const {
67
71
  const {
68
72
  requestModelResponse: requestModelResponseImpl,
69
73
  requestStructuredJson: requestStructuredJsonImpl,
70
- withModelCallTimeout,
74
+ runAbortableModelCall,
71
75
  } = require('./model_io');
72
76
  const {
73
77
  publishInterimUpdate: publishInterimUpdateImpl,
@@ -83,6 +87,7 @@ const {
83
87
  clampRunContext,
84
88
  } = require('../messagingFallback');
85
89
  const {
90
+ assessResearchAdequacy,
86
91
  summarizeToolExecutions,
87
92
  } = require('../toolEvidence');
88
93
  const {
@@ -97,6 +102,13 @@ const {
97
102
  normalizeRetrievalPlan,
98
103
  shouldEnhanceRetrieval,
99
104
  } = require('../../memory/retrieval_reasoning');
105
+ const {
106
+ createAbortError,
107
+ createLinkedAbortController,
108
+ isAbortError,
109
+ runWithAbortTimeout,
110
+ throwIfAborted,
111
+ } = require('../../../utils/abort');
100
112
 
101
113
  function buildInitialRunMetadata(options = {}) {
102
114
  const metadata = {};
@@ -106,6 +118,9 @@ function buildInitialRunMetadata(options = {}) {
106
118
  if (options.widgetId != null && String(options.widgetId).trim()) {
107
119
  metadata.widgetId = options.widgetId;
108
120
  }
121
+ if (options.messagingInboundJobId != null && String(options.messagingInboundJobId).trim()) {
122
+ metadata.messagingInboundJobId = String(options.messagingInboundJobId).trim();
123
+ }
109
124
  return metadata;
110
125
  }
111
126
 
@@ -131,99 +146,6 @@ function buildAnalyzeTaskFallback(forceMode, userMessage = '') {
131
146
  };
132
147
  }
133
148
 
134
- async function getProviderForUser(userId, task = '', isSubagent = false, modelOverride = null, providerConfig = {}) {
135
- const { getSupportedModels, createProviderInstance } = require('../models');
136
- const agentId = providerConfig.agentId || null;
137
- const aiSettings = getAiSettings(userId, agentId);
138
- const models = await getSupportedModels(userId, agentId);
139
-
140
- let enabledIds = Array.isArray(aiSettings.enabled_models) ? aiSettings.enabled_models : [];
141
- const defaultChatModel = aiSettings.default_chat_model || 'auto';
142
- const defaultSubagentModel = aiSettings.default_subagent_model || 'auto';
143
- const smarterSelection = aiSettings.smarter_model_selector !== false && aiSettings.smarter_model_selector !== 'false';
144
-
145
- const knownModelIds = new Set(models.map((m) => m.id));
146
- const selectableModels = models.filter((m) => m.available !== false);
147
-
148
- enabledIds = Array.isArray(enabledIds)
149
- ? enabledIds
150
- .map((id) => String(id))
151
- .filter((id) => knownModelIds.has(id))
152
- : [];
153
-
154
- let availableModels = selectableModels.filter((m) => enabledIds.includes(m.id));
155
- if (availableModels.length === 0) {
156
- enabledIds = selectableModels.map((m) => m.id);
157
- availableModels = [...selectableModels];
158
- }
159
-
160
- const fallbackModel = availableModels.length > 0 ? availableModels[0] : selectableModels[0];
161
-
162
- if (!fallbackModel) {
163
- throw new Error('No AI providers are currently available. Open Settings and configure at least one provider.');
164
- }
165
-
166
- let selectedModelDef = fallbackModel;
167
- const userSelectedDefault = isSubagent ? defaultSubagentModel : defaultChatModel;
168
-
169
- if (modelOverride && typeof modelOverride === 'string') {
170
- const requested = models.find((m) => m.id === modelOverride.trim());
171
- if (requested && requested.available !== false && enabledIds.includes(requested.id)) {
172
- selectedModelDef = requested;
173
- return {
174
- provider: createProviderInstance(selectedModelDef.provider, userId, providerConfig),
175
- model: selectedModelDef.id,
176
- providerName: selectedModelDef.provider
177
- };
178
- }
179
- }
180
-
181
- if (userSelectedDefault && userSelectedDefault !== 'auto') {
182
- selectedModelDef = models.find((m) => m.id === userSelectedDefault) || fallbackModel;
183
- } else {
184
- const selectionHint = providerConfig.selectionHint && typeof providerConfig.selectionHint === 'object'
185
- ? providerConfig.selectionHint
186
- : {};
187
- const preferredPurpose = String(selectionHint.purpose || '').trim().toLowerCase();
188
- const highAutonomy = selectionHint.autonomyLevel === 'high' || selectionHint.complexity === 'complex';
189
- const requiredConfidence = String(selectionHint.requiredConfidence || '').trim().toLowerCase();
190
- const costMode = String(selectionHint.costMode || aiSettings.cost_mode || 'balanced_auto').trim().toLowerCase();
191
- const requestedPurpose = ['planning', 'coding', 'general', 'fast'].includes(preferredPurpose)
192
- ? preferredPurpose
193
- : '';
194
- const priceRank = { free: 0, cheap: 1, medium: 2, expensive: 3 };
195
- const chooseForPurpose = (purpose) => {
196
- const candidates = availableModels.filter((model) => model.purpose === purpose);
197
- if (candidates.length === 0) return null;
198
- if (['economy', 'cost_saver', 'lowest_cost'].includes(costMode)) {
199
- return [...candidates].sort((left, right) => (
200
- (priceRank[left.priceTier] ?? 99) - (priceRank[right.priceTier] ?? 99)
201
- ))[0];
202
- }
203
- if (['quality', 'highest_quality'].includes(costMode) || requiredConfidence === 'high') {
204
- return candidates.find((model) => model.priceTier !== 'free' && model.priceTier !== 'cheap') || candidates[0];
205
- }
206
- return candidates[0];
207
- };
208
-
209
- if (smarterSelection && requestedPurpose) {
210
- selectedModelDef = chooseForPurpose(requestedPurpose) || fallbackModel;
211
- } else if (smarterSelection && highAutonomy) {
212
- selectedModelDef = chooseForPurpose('planning') || chooseForPurpose('general') || fallbackModel;
213
- } else if (isSubagent) {
214
- selectedModelDef = chooseForPurpose('fast') || fallbackModel;
215
- } else {
216
- selectedModelDef = chooseForPurpose('general') || fallbackModel;
217
- }
218
- }
219
-
220
- return {
221
- provider: createProviderInstance(selectedModelDef.provider, userId, providerConfig),
222
- model: selectedModelDef.id,
223
- providerName: selectedModelDef.provider
224
- };
225
- }
226
-
227
149
  function estimateTokenValue(value) {
228
150
  if (!value) return 0;
229
151
  if (typeof value === 'string') return Math.ceil(value.length / 4);
@@ -234,6 +156,13 @@ class AgentEngine {
234
156
  constructor(io, services = {}) {
235
157
  this.io = io;
236
158
  this.activeRuns = new Map();
159
+ this.activeRunPromises = new Set();
160
+ this.backgroundTasks = new Set();
161
+ this.backgroundTaskQueues = new Map();
162
+ this.subagentStartupTasks = new Set();
163
+ this.lifecycleAbortController = new AbortController();
164
+ this.shuttingDown = false;
165
+ this.shutdownPromise = null;
237
166
  this.subagents = new Map();
238
167
  this.app = services.app || null;
239
168
  this.browserController = services.browserController || null;
@@ -249,6 +178,107 @@ class AgentEngine {
249
178
  this.messagingDeliveryRetry = services.messagingDeliveryRetry || {};
250
179
  }
251
180
 
181
+ trackBackgroundTask(task, options = {}) {
182
+ if (typeof task !== 'function') {
183
+ throw new TypeError('Background task must be a function.');
184
+ }
185
+ if (this.shuttingDown || this.lifecycleAbortController.signal.aborted) {
186
+ return Promise.reject(createAbortError(
187
+ this.lifecycleAbortController.signal,
188
+ 'Agent engine is shutting down.',
189
+ ));
190
+ }
191
+
192
+ const key = String(options.key || '').trim();
193
+ const previous = key ? this.backgroundTaskQueues.get(key) : null;
194
+ if (previous && options.coalesce === true) return previous;
195
+
196
+ const linked = createLinkedAbortController([
197
+ this.lifecycleAbortController.signal,
198
+ options.signal,
199
+ ]);
200
+ const tracked = Promise.resolve().then(async () => {
201
+ if (previous) {
202
+ try {
203
+ await previous;
204
+ } catch {
205
+ // A failed predecessor must not permanently poison this queue.
206
+ }
207
+ }
208
+ throwIfAborted(linked.signal, 'Background task aborted.');
209
+ return task(linked.signal);
210
+ }).finally(() => {
211
+ linked.cleanup();
212
+ });
213
+
214
+ this.backgroundTasks.add(tracked);
215
+ if (key) this.backgroundTaskQueues.set(key, tracked);
216
+ const cleanup = () => {
217
+ this.backgroundTasks.delete(tracked);
218
+ if (key && this.backgroundTaskQueues.get(key) === tracked) {
219
+ this.backgroundTaskQueues.delete(key);
220
+ }
221
+ };
222
+ tracked.then(cleanup, cleanup);
223
+ return tracked;
224
+ }
225
+
226
+ shutdown(options = {}) {
227
+ if (this.shutdownPromise) return this.shutdownPromise;
228
+
229
+ const reason = String(
230
+ options.reason || 'Server shutting down while agent work was in progress.',
231
+ );
232
+ const timeoutMs = Math.max(100, Number(options.timeoutMs) || 10000);
233
+ this.shuttingDown = true;
234
+ this.lifecycleAbortController.abort(reason);
235
+ this.interruptAllActiveRuns(reason);
236
+
237
+ const childShutdowns = [];
238
+ const childPromises = [];
239
+ for (const record of this.subagents.values()) {
240
+ if (typeof record.engine?.shutdown === 'function') {
241
+ childShutdowns.push(record.engine.shutdown({ reason, timeoutMs }));
242
+ }
243
+ if (record.promise) childPromises.push(record.promise);
244
+ }
245
+ const pending = [
246
+ ...this.activeRunPromises,
247
+ ...this.backgroundTasks,
248
+ ...this.subagentStartupTasks,
249
+ ...childShutdowns,
250
+ ...childPromises,
251
+ ];
252
+
253
+ this.shutdownPromise = (async () => {
254
+ if (pending.length === 0) {
255
+ return { state: 'stopped', timedOut: false, pendingCount: 0 };
256
+ }
257
+
258
+ let timeout = null;
259
+ const timeoutResult = Symbol('agent-engine-shutdown-timeout');
260
+ const result = await Promise.race([
261
+ Promise.allSettled(pending),
262
+ new Promise((resolve) => {
263
+ timeout = setTimeout(() => resolve(timeoutResult), timeoutMs);
264
+ }),
265
+ ]);
266
+ if (timeout) clearTimeout(timeout);
267
+ const timedOut = result === timeoutResult;
268
+ return {
269
+ state: timedOut ? 'timeout' : 'stopped',
270
+ timedOut,
271
+ pendingCount: timedOut
272
+ ? this.activeRunPromises.size
273
+ + this.backgroundTasks.size
274
+ + this.subagentStartupTasks.size
275
+ + Array.from(this.subagents.values()).filter((record) => !record.settled).length
276
+ : 0,
277
+ };
278
+ })();
279
+ return this.shutdownPromise;
280
+ }
281
+
252
282
  async buildSystemPrompt(userId, context = {}) {
253
283
  const { buildSystemPromptSections } = require('../systemPrompt');
254
284
  const { MemoryManager } = require('../../memory/manager');
@@ -287,18 +317,34 @@ class AgentEngine {
287
317
  options = {},
288
318
  returnDetails = false,
289
319
  }) {
290
- const initial = await memoryManager.recallMemory(userId, query, 12, { agentId });
320
+ const signal = options.signal || this.getRunMeta(runId)?.abortController?.signal || null;
321
+ const initial = await memoryManager.recallMemory(userId, query, 12, {
322
+ agentId,
323
+ signal,
324
+ });
291
325
 
292
326
  const pendingChunks = memoryManager.getPendingExtractionChunks?.(userId, agentId, 5) || [];
293
327
  if (pendingChunks.length) {
294
- this.extractPendingChunks(pendingChunks, {
295
- userId,
296
- agentId,
297
- provider,
298
- providerName,
299
- model,
300
- memoryManager,
301
- }).catch((err) => console.warn('[Memory] Background chunk extraction failed:', err.message));
328
+ this.trackBackgroundTask((backgroundSignal) => this.extractPendingChunks(
329
+ pendingChunks,
330
+ {
331
+ userId,
332
+ agentId,
333
+ provider,
334
+ providerName,
335
+ model,
336
+ memoryManager,
337
+ signal: backgroundSignal,
338
+ },
339
+ ), {
340
+ key: `memory-extraction:${userId}:${agentId || 'main'}`,
341
+ coalesce: true,
342
+ signal,
343
+ }).catch((err) => {
344
+ if (!isAbortError(err, signal) && !this.shuttingDown) {
345
+ console.warn('[Memory] Background chunk extraction failed:', err.message);
346
+ }
347
+ });
302
348
  }
303
349
 
304
350
  const decision = shouldEnhanceRetrieval(initial);
@@ -335,7 +381,7 @@ class AgentEngine {
335
381
  normalize: (raw) => normalizeRetrievalPlan(raw, query),
336
382
  fallback: normalizeRetrievalPlan({}, query),
337
383
  reasoningEffort: this.getReasoningEffort(providerName, options),
338
- telemetry: { runId, stepId, userId, agentId },
384
+ telemetry: { runId, stepId, userId, agentId, signal },
339
385
  phase: 'memory_retrieval_plan',
340
386
  });
341
387
  plan = planned.value;
@@ -346,6 +392,7 @@ class AgentEngine {
346
392
  agentId,
347
393
  validAt: plan.validAt,
348
394
  includeHistory: plan.temporalMode === 'historical',
395
+ signal,
349
396
  }));
350
397
  }
351
398
  merged = mergeRetrievalResults(resultSets, 30);
@@ -360,7 +407,7 @@ class AgentEngine {
360
407
  normalize: (raw) => normalizeRerankResult(raw, merged),
361
408
  fallback: merged,
362
409
  reasoningEffort: this.getReasoningEffort(providerName, options),
363
- telemetry: { runId, stepId, userId, agentId },
410
+ telemetry: { runId, stepId, userId, agentId, signal },
364
411
  phase: 'memory_retrieval_rerank',
365
412
  });
366
413
  reranked = rerankResponse.value;
@@ -368,6 +415,7 @@ class AgentEngine {
368
415
  reranked = merged;
369
416
  }
370
417
  } catch (error) {
418
+ if (isAbortError(error, signal)) throw error;
371
419
  console.warn('[Memory] Retrieval enhancement failed:', error.message);
372
420
  plan = null;
373
421
  merged = initial;
@@ -406,10 +454,8 @@ class AgentEngine {
406
454
  providerName,
407
455
  model,
408
456
  memoryManager,
457
+ signal = null,
409
458
  }) {
410
- const ids = chunks.map((c) => c.id);
411
- memoryManager.markChunksExtracted?.(ids, { success: true });
412
-
413
459
  const consolidationSchema = JSON.stringify({
414
460
  memory_candidates: [{
415
461
  memory: 'Concise standalone fact.',
@@ -430,6 +476,7 @@ class AgentEngine {
430
476
 
431
477
  for (const chunk of chunks) {
432
478
  try {
479
+ throwIfAborted(signal, 'Memory extraction aborted.');
433
480
  const result = await this.requestStructuredJson({
434
481
  provider,
435
482
  providerName,
@@ -446,6 +493,7 @@ class AgentEngine {
446
493
  maxTokens: 800,
447
494
  normalize: (raw) => normalizeMemoryCandidates(raw?.memory_candidates || []),
448
495
  fallback: [],
496
+ telemetry: { userId, agentId, signal },
449
497
  phase: 'document_extraction',
450
498
  });
451
499
 
@@ -457,10 +505,13 @@ class AgentEngine {
457
505
  trustLevel: 'external_source',
458
506
  sourceChunkMemoryId: chunk.id,
459
507
  },
508
+ signal,
460
509
  });
461
510
  }
511
+ memoryManager.markChunksExtracted?.([chunk.id], { success: true });
462
512
  } catch (err) {
463
513
  memoryManager.markChunksExtracted?.([chunk.id], { success: false });
514
+ if (isAbortError(err, signal)) throw err;
464
515
  console.warn('[Memory] Document chunk extraction failed:', err.message);
465
516
  }
466
517
  }
@@ -523,6 +574,7 @@ class AgentEngine {
523
574
  ? deliverableResult.artifacts.slice(0, 6)
524
575
  : [],
525
576
  },
577
+ signal: this.getRunMeta(runId)?.abortController?.signal || null,
526
578
  },
527
579
  );
528
580
  } catch (error) {
@@ -586,6 +638,53 @@ class AgentEngine {
586
638
  });
587
639
  }
588
640
 
641
+ async inferStructured({
642
+ userId,
643
+ agentId = null,
644
+ modelId = null,
645
+ purpose = 'fast',
646
+ system,
647
+ prompt,
648
+ maxTokens = 400,
649
+ fallback = {},
650
+ signal = null,
651
+ }) {
652
+ const selected = await getProviderForUser(
653
+ userId,
654
+ prompt,
655
+ false,
656
+ modelId,
657
+ {
658
+ agentId,
659
+ signal,
660
+ selectionHint: {
661
+ purpose: purpose === 'general' ? 'general' : 'fast',
662
+ costMode: purpose === 'fast' ? 'economy' : 'balanced_auto',
663
+ },
664
+ },
665
+ );
666
+ const result = await this.requestStructuredJson({
667
+ provider: selected.provider,
668
+ providerName: selected.providerName,
669
+ model: selected.model,
670
+ messages: system ? [{ role: 'system', content: String(system) }] : [],
671
+ prompt: String(prompt || ''),
672
+ maxTokens,
673
+ normalize: (value) => value,
674
+ fallback,
675
+ telemetry: { userId, agentId, signal },
676
+ phase: 'behavior_inference',
677
+ });
678
+ return {
679
+ parsed: result.value,
680
+ raw: result.raw,
681
+ usage: result.usage,
682
+ model: selected.model,
683
+ modelSelectionId: selected.modelSelectionId,
684
+ providerName: selected.providerName,
685
+ };
686
+ }
687
+
589
688
  async requestModelResponse({
590
689
  provider,
591
690
  providerName,
@@ -742,6 +841,11 @@ class AgentEngine {
742
841
  }) {
743
842
  const runMeta = options?.runId ? this.getRunMeta(options.runId) : null;
744
843
  const goalContext = resolveRunGoalContext(runMeta, analysis, plan);
844
+ const researchAdequacy = assessResearchAdequacy({
845
+ analysis,
846
+ goalContext,
847
+ toolExecutions,
848
+ });
745
849
  const response = await this.requestStructuredJson({
746
850
  provider,
747
851
  providerName,
@@ -757,6 +861,8 @@ class AgentEngine {
757
861
  lastReply,
758
862
  iteration,
759
863
  maxIterations,
864
+ analysis,
865
+ researchAdequacy,
760
866
  }),
761
867
  maxTokens: 500,
762
868
  normalize: (raw) => normalizeCompletionDecision(raw, 'continue'),
@@ -766,9 +872,15 @@ class AgentEngine {
766
872
  phase: 'completion_decision',
767
873
  });
768
874
  return {
769
- decision: response.value,
875
+ decision: enforceTerminalReplyDecision(response.value, lastReply, {
876
+ analysis,
877
+ goalContext,
878
+ toolExecutions,
879
+ researchAdequacy,
880
+ }),
770
881
  usage: response.usage,
771
882
  raw: response.raw,
883
+ researchAdequacy,
772
884
  };
773
885
  }
774
886
 
@@ -854,6 +966,12 @@ class AgentEngine {
854
966
  goalContext,
855
967
  toolExecutions,
856
968
  iteration,
969
+ analysis,
970
+ researchAdequacy: assessResearchAdequacy({
971
+ analysis,
972
+ goalContext,
973
+ toolExecutions,
974
+ }),
857
975
  }),
858
976
  maxTokens: 200,
859
977
  normalize: normalizeChurnAssessment,
@@ -862,9 +980,20 @@ class AgentEngine {
862
980
  telemetry: options,
863
981
  phase: 'churn_assessment',
864
982
  });
983
+ const researchAdequacy = assessResearchAdequacy({
984
+ analysis,
985
+ goalContext,
986
+ toolExecutions,
987
+ });
865
988
  return {
866
- assessment: response.value,
989
+ assessment: enforceChurnAssessment(response.value, {
990
+ analysis,
991
+ goalContext,
992
+ toolExecutions,
993
+ researchAdequacy,
994
+ }),
867
995
  usage: response.usage,
996
+ researchAdequacy,
868
997
  };
869
998
  }
870
999
 
@@ -891,6 +1020,15 @@ class AgentEngine {
891
1020
  content: [
892
1021
  'Return JSON only. Distill the current thread working state. Keep it concise and concrete.',
893
1022
  'Track summary, open_commitments, unresolved_questions, referenced_entities, and last_verified_facts. Do not invent facts.',
1023
+ options.memoryScope?.scopeType === 'channel'
1024
+ ? [
1025
+ 'This is a shared room. Extract only durable room or participant facts that are safe and useful in this room.',
1026
+ 'Do not infer or store facts about the authenticated owner from another participant’s message.',
1027
+ options.context?.socialIntelligence?.message?.sender
1028
+ ? `For facts about the current speaker, use the canonical subject "${options.source}:${options.context.socialIntelligence.message.sender}".`
1029
+ : '',
1030
+ ].filter(Boolean).join(' ')
1031
+ : '',
894
1032
  buildMemoryConsolidationInstructions(new Date().toISOString()),
895
1033
  'Schema:',
896
1034
  JSON.stringify({
@@ -931,11 +1069,12 @@ class AgentEngine {
931
1069
  }
932
1070
  ];
933
1071
 
934
- const response = await withModelCallTimeout(
935
- provider.chat(promptMessages, [], {
1072
+ const response = await runAbortableModelCall(
1073
+ (signal) => provider.chat(promptMessages, [], {
936
1074
  model,
937
1075
  maxTokens: 800,
938
1076
  reasoningEffort: this.getReasoningEffort(providerName, options),
1077
+ signal,
939
1078
  }),
940
1079
  options,
941
1080
  'Conversation state refresh',
@@ -966,6 +1105,15 @@ class AgentEngine {
966
1105
  agentId: options.agentId || null,
967
1106
  conversationId,
968
1107
  runId,
1108
+ scope: options.memoryScope || undefined,
1109
+ metadata: options.memoryScope
1110
+ ? {
1111
+ trustLevel: 'shared_room_conversation',
1112
+ platform: options.source || null,
1113
+ chatId: options.chatId || null,
1114
+ }
1115
+ : undefined,
1116
+ signal: options.signal,
969
1117
  },
970
1118
  );
971
1119
  const { invalidateSystemPromptCache } = require('../systemPrompt');
@@ -982,8 +1130,8 @@ class AgentEngine {
982
1130
  return executeToolImpl(this, toolName, args, context);
983
1131
  }
984
1132
 
985
- isReadOnlyToolCall(toolCall) {
986
- return isReadOnlyToolCallImpl(this, toolCall);
1133
+ isReadOnlyToolCall(toolCall, toolDefinition = null) {
1134
+ return isReadOnlyToolCallImpl(toolCall, toolDefinition);
987
1135
  }
988
1136
 
989
1137
  async executeReadOnlyBatch(toolCalls, context = {}) {
@@ -1119,6 +1267,15 @@ class AgentEngine {
1119
1267
 
1120
1268
  await new Promise((resolve) => { runMeta.resumeRun = resolve; });
1121
1269
  runMeta.resumeRun = null;
1270
+ if (runMeta.status === 'stopped' || runMeta.status === 'interrupted') {
1271
+ return { action: runMeta.status === 'interrupted' ? 'interrupt' : 'stop' };
1272
+ }
1273
+ const persistedStatus = db.prepare('SELECT status FROM agent_runs WHERE id = ?').get(runId)?.status;
1274
+ if (persistedStatus !== 'running') {
1275
+ const action = persistedStatus === 'interrupted' ? 'interrupt' : 'stop';
1276
+ runMeta.status = persistedStatus || 'stopped';
1277
+ return { action };
1278
+ }
1122
1279
  runMeta.abortController = new AbortController();
1123
1280
  runMeta.status = 'running';
1124
1281
  this.startMessagingProgressSupervisor(runId);
@@ -1152,20 +1309,6 @@ class AgentEngine {
1152
1309
  return options.reasoningEffort || process.env.REASONING_EFFORT || 'low';
1153
1310
  }
1154
1311
 
1155
- shouldFastCompleteVoiceReply({
1156
- options = {},
1157
- toolExecutions = [],
1158
- failedStepCount = 0,
1159
- messagingSent = false,
1160
- lastReply = '',
1161
- }) {
1162
- return options.latencyProfile === 'voice'
1163
- && toolExecutions.length === 0
1164
- && failedStepCount === 0
1165
- && !messagingSent
1166
- && Boolean(String(lastReply || '').trim());
1167
- }
1168
-
1169
1312
  getMessagingRetryLimit(maxIterations) {
1170
1313
  // Cap at 3: more than 3 autonomous messaging retries indicates a structural
1171
1314
  // problem (model unavailable, bad config) that more retries won't solve.
@@ -1282,15 +1425,37 @@ class AgentEngine {
1282
1425
  }
1283
1426
 
1284
1427
  async runWithModel(userId, userMessage, options = {}, _modelOverride = null) {
1285
- return runConversation(this, userId, userMessage, options, _modelOverride);
1286
- }
1287
-
1288
- async _runWithModelInternal(userId, userMessage, options = {}, _modelOverride = null) {
1289
- return runConversation(this, userId, userMessage, options, _modelOverride);
1428
+ if (this.shuttingDown || this.lifecycleAbortController.signal.aborted) {
1429
+ const error = createAbortError(
1430
+ this.lifecycleAbortController.signal,
1431
+ 'Agent engine is shutting down.',
1432
+ );
1433
+ error.code = error.code || 'AGENT_ENGINE_SHUTTING_DOWN';
1434
+ throw error;
1435
+ }
1436
+ const runPromise = runConversation(this, userId, userMessage, options, _modelOverride);
1437
+ this.activeRunPromises.add(runPromise);
1438
+ try {
1439
+ return await runPromise;
1440
+ } finally {
1441
+ this.activeRunPromises.delete(runPromise);
1442
+ }
1290
1443
  }
1291
1444
 
1292
1445
  async spawnSubagent(userId, parentRunId, task, options = {}) {
1293
- const parentRunMeta = this.getRunMeta(parentRunId);
1446
+ if (this.shuttingDown || this.lifecycleAbortController.signal.aborted) {
1447
+ throw createAbortError(this.lifecycleAbortController.signal, 'Agent engine is shutting down.');
1448
+ }
1449
+
1450
+ let parentRunMeta = this.getRunMeta(parentRunId);
1451
+ if (
1452
+ !parentRunMeta
1453
+ || parentRunMeta.userId !== userId
1454
+ || parentRunMeta.status !== 'running'
1455
+ || parentRunMeta.aborted === true
1456
+ ) {
1457
+ return { error: 'The parent run is no longer active, so a sub-agent cannot be started.' };
1458
+ }
1294
1459
  const parentDepth = Math.max(0, Number(parentRunMeta?.subagentDepth) || 0);
1295
1460
  if (parentDepth >= 1) {
1296
1461
  return {
@@ -1300,24 +1465,77 @@ class AgentEngine {
1300
1465
 
1301
1466
  const aiSettings = getAiSettings(userId, options.agentId || null);
1302
1467
  const maxSubagentsPerRun = Math.max(1, Number(aiSettings.subagent_max_children_per_run) || 10);
1303
- const existingSubagents = Array.from(this.subagents.values())
1304
- .filter((record) => record.parentRunId === parentRunId);
1305
- if (existingSubagents.length >= maxSubagentsPerRun) {
1468
+ const countExistingSubagents = () => Array.from(this.subagents.values())
1469
+ .filter((record) => record.parentRunId === parentRunId).length;
1470
+ if (countExistingSubagents() >= maxSubagentsPerRun) {
1306
1471
  return {
1307
- error: `This run has already spawned ${existingSubagents.length} sub-agents. The limit for one run is ${maxSubagentsPerRun}.`,
1472
+ error: `This run has already spawned ${countExistingSubagents()} sub-agents. The limit for one run is ${maxSubagentsPerRun}.`,
1308
1473
  };
1309
1474
  }
1310
1475
 
1311
1476
  const handle = uuidv4();
1312
1477
  const childRunId = uuidv4();
1313
1478
  let relevantMemories = [];
1479
+ const startupLink = createLinkedAbortController([
1480
+ this.lifecycleAbortController.signal,
1481
+ parentRunMeta.abortController?.signal,
1482
+ options.signal,
1483
+ ]);
1484
+ let memoryRecall = null;
1314
1485
  try {
1315
- relevantMemories = this.memoryManager
1316
- ? await this.memoryManager.recallMemory(userId, task, 4, {
1317
- agentId: options.agentId || null,
1318
- })
1319
- : [];
1320
- } catch {}
1486
+ throwIfAborted(startupLink.signal, 'Sub-agent startup was aborted.');
1487
+ memoryRecall = this.memoryManager
1488
+ ? runWithAbortTimeout(
1489
+ (signal) => this.memoryManager.recallMemory(userId, task, 4, {
1490
+ agentId: options.agentId || null,
1491
+ signal,
1492
+ }),
1493
+ {
1494
+ label: 'Sub-agent memory recall',
1495
+ signal: startupLink.signal,
1496
+ },
1497
+ )
1498
+ : Promise.resolve([]);
1499
+ this.subagentStartupTasks.add(memoryRecall);
1500
+ relevantMemories = await memoryRecall;
1501
+ throwIfAborted(startupLink.signal, 'Sub-agent startup was aborted.');
1502
+ } catch (error) {
1503
+ if (isAbortError(error, startupLink.signal)) throw error;
1504
+ console.warn('[AgentEngine] Sub-agent memory recall failed:', error?.message || error);
1505
+ } finally {
1506
+ if (memoryRecall) this.subagentStartupTasks.delete(memoryRecall);
1507
+ startupLink.cleanup();
1508
+ }
1509
+
1510
+ parentRunMeta = this.getRunMeta(parentRunId);
1511
+ if (
1512
+ this.shuttingDown
1513
+ || this.lifecycleAbortController.signal.aborted
1514
+ || !parentRunMeta
1515
+ || parentRunMeta.userId !== userId
1516
+ || parentRunMeta.status !== 'running'
1517
+ || parentRunMeta.aborted === true
1518
+ ) {
1519
+ throw createAbortError(
1520
+ this.lifecycleAbortController.signal.aborted
1521
+ ? this.lifecycleAbortController.signal
1522
+ : parentRunMeta?.abortController?.signal,
1523
+ 'The parent run stopped before the sub-agent could start.',
1524
+ );
1525
+ }
1526
+ const currentSubagentCount = countExistingSubagents();
1527
+ if (currentSubagentCount >= maxSubagentsPerRun) {
1528
+ return {
1529
+ error: `This run has already spawned ${currentSubagentCount} sub-agents. The limit for one run is ${maxSubagentsPerRun}.`,
1530
+ };
1531
+ }
1532
+
1533
+ const childLink = createLinkedAbortController([
1534
+ this.lifecycleAbortController.signal,
1535
+ parentRunMeta.abortController?.signal,
1536
+ options.signal,
1537
+ ]);
1538
+ throwIfAborted(childLink.signal, 'Sub-agent startup was aborted.');
1321
1539
  const subEngine = new AgentEngine(this.io, {
1322
1540
  app: options.app || this.app,
1323
1541
  browserController: this.browserController,
@@ -1363,6 +1581,8 @@ class AgentEngine {
1363
1581
  error: null,
1364
1582
  engine: subEngine,
1365
1583
  promise: null,
1584
+ settled: false,
1585
+ deleteWhenSettled: false,
1366
1586
  };
1367
1587
  this.subagents.set(handle, record);
1368
1588
  this.emit(userId, 'run:subagent', {
@@ -1386,9 +1606,11 @@ class AgentEngine {
1386
1606
  agentId: options.agentId || null,
1387
1607
  subagentDepth: parentDepth + 1,
1388
1608
  disallowedToolNames: ['spawn_subagent'],
1609
+ signal: childLink.signal,
1389
1610
  },
1390
1611
  options.model || null
1391
1612
  );
1613
+ if (record.status === 'cancelled' || childLink.signal.aborted) return record;
1392
1614
  record.status = result.status || 'completed';
1393
1615
  let structured = null;
1394
1616
  try {
@@ -1416,16 +1638,23 @@ class AgentEngine {
1416
1638
  });
1417
1639
  return record;
1418
1640
  } catch (err) {
1419
- record.status = 'failed';
1420
- record.error = err.message;
1641
+ const cancelled = record.status === 'cancelled' || isAbortError(err, childLink.signal);
1642
+ record.status = cancelled ? 'cancelled' : 'failed';
1643
+ record.error = cancelled ? null : (err?.message || String(err));
1421
1644
  this.emit(userId, 'run:subagent', {
1422
1645
  runId: parentRunId,
1423
1646
  handle,
1424
1647
  childRunId,
1425
- status: 'failed',
1426
- error: err.message,
1648
+ status: record.status,
1649
+ error: record.error,
1427
1650
  });
1428
- throw err;
1651
+ return record;
1652
+ } finally {
1653
+ record.settled = true;
1654
+ childLink.cleanup();
1655
+ if (record.deleteWhenSettled && this.subagents.get(handle) === record) {
1656
+ this.subagents.delete(handle);
1657
+ }
1429
1658
  }
1430
1659
  })();
1431
1660
 
@@ -1562,21 +1791,43 @@ class AgentEngine {
1562
1791
  }));
1563
1792
  }
1564
1793
 
1565
- cleanupSubagentsForRun(parentRunId, options = {}) {
1566
- if (!parentRunId) return;
1794
+ async cleanupSubagentsForRun(parentRunId, options = {}) {
1795
+ if (!parentRunId) return { cancelled: 0, timedOut: 0 };
1567
1796
  const cancelRunning = options.cancelRunning !== false;
1568
- for (const [handle, record] of this.subagents.entries()) {
1797
+ const shutdowns = [];
1798
+ const records = [];
1799
+ for (const [handle, record] of Array.from(this.subagents.entries())) {
1569
1800
  if (record.parentRunId !== parentRunId) continue;
1801
+ records.push([handle, record]);
1802
+ record.deleteWhenSettled = true;
1570
1803
  if (cancelRunning && record.status === 'running') {
1571
- try {
1572
- record.engine?.abort(record.childRunId);
1573
- record.status = 'cancelled';
1574
- } catch (err) {
1575
- console.warn(`[AgentEngine] Failed to abort subagent ${handle}:`, err?.message);
1576
- }
1804
+ record.status = 'cancelled';
1805
+ this.emit(record.userId, 'run:subagent', {
1806
+ runId: record.parentRunId,
1807
+ handle,
1808
+ childRunId: record.childRunId,
1809
+ status: 'cancelled',
1810
+ });
1811
+ shutdowns.push(Promise.resolve(record.engine?.shutdown?.({
1812
+ reason: 'Parent run finished before the sub-agent completed.',
1813
+ timeoutMs: Math.max(100, Number(options.timeoutMs) || 10000),
1814
+ })));
1815
+ } else if (record.status === 'running') {
1816
+ record.deleteWhenSettled = false;
1817
+ }
1818
+ }
1819
+ const results = await Promise.allSettled(shutdowns);
1820
+ for (const [handle, record] of records) {
1821
+ if ((record.settled || !record.promise) && this.subagents.get(handle) === record) {
1822
+ this.subagents.delete(handle);
1577
1823
  }
1578
- this.subagents.delete(handle);
1579
1824
  }
1825
+ return {
1826
+ cancelled: shutdowns.length,
1827
+ timedOut: results.filter((result) => (
1828
+ result.status === 'fulfilled' && result.value?.timedOut === true
1829
+ )).length,
1830
+ };
1580
1831
  }
1581
1832
 
1582
1833
  async waitForSubagent(handle, options = {}) {
@@ -1639,7 +1890,6 @@ class AgentEngine {
1639
1890
  };
1640
1891
  }
1641
1892
 
1642
- record.engine?.abort(record.childRunId);
1643
1893
  record.status = 'cancelled';
1644
1894
  this.emit(record.userId, 'run:subagent', {
1645
1895
  runId: record.parentRunId,
@@ -1648,7 +1898,15 @@ class AgentEngine {
1648
1898
  status: 'cancelled',
1649
1899
  });
1650
1900
 
1651
- return { handle, status: 'cancelled' };
1901
+ const shutdown = await record.engine?.shutdown?.({
1902
+ reason: 'Sub-agent cancelled by its parent run.',
1903
+ timeoutMs: Math.max(100, Number(options.timeoutMs) || 10000),
1904
+ });
1905
+ return {
1906
+ handle,
1907
+ status: 'cancelled',
1908
+ timedOut: shutdown?.timedOut === true,
1909
+ };
1652
1910
  }
1653
1911
 
1654
1912
  interruptRun(runId, reason = 'Server shutting down while run was in progress.') {
@@ -1665,6 +1923,7 @@ class AgentEngine {
1665
1923
  runMeta.stopReason = reason;
1666
1924
  runMeta.aborted = true;
1667
1925
  runMeta.abortController?.abort(reason);
1926
+ runMeta.resumeRun?.();
1668
1927
  this.emit(runMeta.userId, 'run:stopping', { runId });
1669
1928
  for (const pid of runMeta.toolPids) {
1670
1929
  if (this.runtimeManager && typeof this.runtimeManager.killCommand === 'function') {
@@ -1695,20 +1954,26 @@ class AgentEngine {
1695
1954
  }
1696
1955
  }
1697
1956
 
1698
- stopRun(runId) {
1957
+ stopRun(runId, reason = 'Stopped by request.') {
1958
+ const stopReason = String(reason || 'Stopped by request.').slice(0, 1000);
1699
1959
  const runMeta = this.activeRuns.get(runId);
1700
- const persistedRun = runMeta || db.prepare('SELECT user_id AS userId FROM agent_runs WHERE id = ?').get(runId);
1960
+ const persistedRun = runMeta || db.prepare(
1961
+ 'SELECT user_id AS userId, status FROM agent_runs WHERE id = ?',
1962
+ ).get(runId);
1963
+ if (!persistedRun || TERMINAL_STATUSES.has(persistedRun.status)) return false;
1701
1964
  if (persistedRun?.userId != null) {
1702
- requestRunControl(runId, persistedRun.userId, 'stop', 'Stopped by request.');
1965
+ requestRunControl(runId, persistedRun.userId, 'stop', stopReason);
1703
1966
  }
1704
1967
  const delegatedChildren = db.prepare(
1705
1968
  "SELECT child_run_id FROM agent_delegations WHERE parent_run_id = ? AND status = 'running'"
1706
1969
  ).all(runId);
1707
1970
  if (runMeta) {
1708
1971
  runMeta.status = 'stopped';
1972
+ runMeta.stopReason = stopReason;
1709
1973
  runMeta.aborted = true;
1710
- runMeta.abortController?.abort('Run stopped.');
1711
- this.emit(runMeta.userId, 'run:stopping', { runId });
1974
+ runMeta.abortController?.abort(stopReason);
1975
+ runMeta.resumeRun?.();
1976
+ this.emit(runMeta.userId, 'run:stopping', { runId, reason: stopReason });
1712
1977
  for (const pid of runMeta.toolPids) {
1713
1978
  if (this.runtimeManager && typeof this.runtimeManager.killCommand === 'function') {
1714
1979
  void this.runtimeManager.killCommand(runMeta.userId, pid, 'aborted');
@@ -1718,13 +1983,22 @@ class AgentEngine {
1718
1983
  }
1719
1984
  for (const child of delegatedChildren) {
1720
1985
  if (child.child_run_id && child.child_run_id !== runId) {
1721
- this.stopRun(child.child_run_id);
1986
+ this.stopRun(child.child_run_id, stopReason);
1722
1987
  }
1723
1988
  }
1724
1989
  db.prepare(
1725
- "UPDATE agent_delegations SET status = 'stopped', updated_at = datetime('now'), completed_at = datetime('now') WHERE parent_run_id = ? AND status = 'running'"
1726
- ).run(runId);
1727
- closeRun(runId, 'stopped', {}, ['running', 'pausing', 'paused', 'resuming']);
1990
+ `UPDATE agent_delegations
1991
+ SET status = 'stopped', error = COALESCE(NULLIF(error, ''), ?),
1992
+ updated_at = datetime('now'), completed_at = datetime('now')
1993
+ WHERE parent_run_id = ? AND status = 'running'`
1994
+ ).run(stopReason, runId);
1995
+ closeRun(
1996
+ runId,
1997
+ 'stopped',
1998
+ { error: stopReason },
1999
+ ['running', 'pausing', 'paused', 'resuming'],
2000
+ );
2001
+ return true;
1728
2002
  }
1729
2003
 
1730
2004
  pauseRun(runId, { userId, reason = '' } = {}) {
@@ -1766,15 +2040,17 @@ class AgentEngine {
1766
2040
  return true;
1767
2041
  }
1768
2042
 
1769
- abort(runId, { userId } = {}) {
2043
+ abort(runId, { userId, reason = 'Stopped by request.' } = {}) {
1770
2044
  if (!runId) return false;
2045
+ const runMeta = this.activeRuns.get(runId);
2046
+ const persistedRun = runMeta
2047
+ || db.prepare('SELECT user_id AS userId, status FROM agent_runs WHERE id = ?').get(runId);
2048
+ if (!persistedRun || TERMINAL_STATUSES.has(persistedRun.status)) return false;
1771
2049
  if (userId != null) {
1772
- // Ownership gate: never let one user abort another user's active run.
1773
- const runMeta = this.activeRuns.get(runId);
1774
- if (runMeta && Number(runMeta.userId) !== Number(userId)) return false;
2050
+ // Ownership gate applies to both active and persisted runs.
2051
+ if (Number(persistedRun.userId) !== Number(userId)) return false;
1775
2052
  }
1776
- this.stopRun(runId);
1777
- return true;
2053
+ return this.stopRun(runId, reason);
1778
2054
  }
1779
2055
 
1780
2056
  abortAll(userId) {