@agent-native/core 0.7.4 → 0.7.7

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 (250) hide show
  1. package/README.md +6 -5
  2. package/dist/agent/engine/anthropic-engine.d.ts.map +1 -1
  3. package/dist/agent/engine/anthropic-engine.js +8 -4
  4. package/dist/agent/engine/anthropic-engine.js.map +1 -1
  5. package/dist/agent/engine/types.d.ts +1 -1
  6. package/dist/agent/engine/types.d.ts.map +1 -1
  7. package/dist/agent/production-agent.d.ts +7 -0
  8. package/dist/agent/production-agent.d.ts.map +1 -1
  9. package/dist/agent/production-agent.js +153 -118
  10. package/dist/agent/production-agent.js.map +1 -1
  11. package/dist/agent/run-manager.d.ts +4 -0
  12. package/dist/agent/run-manager.d.ts.map +1 -1
  13. package/dist/agent/run-manager.js +46 -25
  14. package/dist/agent/run-manager.js.map +1 -1
  15. package/dist/agent/run-store.d.ts +12 -3
  16. package/dist/agent/run-store.d.ts.map +1 -1
  17. package/dist/agent/run-store.js +25 -4
  18. package/dist/agent/run-store.js.map +1 -1
  19. package/dist/chat-threads/store.d.ts +13 -0
  20. package/dist/chat-threads/store.d.ts.map +1 -1
  21. package/dist/chat-threads/store.js +66 -10
  22. package/dist/chat-threads/store.js.map +1 -1
  23. package/dist/cli/create.d.ts.map +1 -1
  24. package/dist/cli/create.js +8 -1
  25. package/dist/cli/create.js.map +1 -1
  26. package/dist/cli/index.js +8 -0
  27. package/dist/cli/index.js.map +1 -1
  28. package/dist/cli/info.d.ts +2 -0
  29. package/dist/cli/info.d.ts.map +1 -0
  30. package/dist/cli/info.js +103 -0
  31. package/dist/cli/info.js.map +1 -0
  32. package/dist/client/AssistantChat.d.ts.map +1 -1
  33. package/dist/client/AssistantChat.js +249 -85
  34. package/dist/client/AssistantChat.js.map +1 -1
  35. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  36. package/dist/client/agent-chat-adapter.js +12 -1
  37. package/dist/client/agent-chat-adapter.js.map +1 -1
  38. package/dist/client/composer/TiptapComposer.d.ts +3 -1
  39. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  40. package/dist/client/composer/TiptapComposer.js +46 -2
  41. package/dist/client/composer/TiptapComposer.js.map +1 -1
  42. package/dist/client/composer/VoiceButton.d.ts +21 -0
  43. package/dist/client/composer/VoiceButton.d.ts.map +1 -0
  44. package/dist/client/composer/VoiceButton.js +51 -0
  45. package/dist/client/composer/VoiceButton.js.map +1 -0
  46. package/dist/client/composer/useVoiceDictation.d.ts +38 -0
  47. package/dist/client/composer/useVoiceDictation.d.ts.map +1 -0
  48. package/dist/client/composer/useVoiceDictation.js +398 -0
  49. package/dist/client/composer/useVoiceDictation.js.map +1 -0
  50. package/dist/client/onboarding/OnboardingPanel.js +2 -2
  51. package/dist/client/onboarding/OnboardingPanel.js.map +1 -1
  52. package/dist/client/org/OrgSwitcher.d.ts +5 -4
  53. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  54. package/dist/client/org/OrgSwitcher.js +90 -24
  55. package/dist/client/org/OrgSwitcher.js.map +1 -1
  56. package/dist/client/resources/McpServerDetail.d.ts +15 -0
  57. package/dist/client/resources/McpServerDetail.d.ts.map +1 -0
  58. package/dist/client/resources/McpServerDetail.js +65 -0
  59. package/dist/client/resources/McpServerDetail.js.map +1 -0
  60. package/dist/client/resources/ResourceEditor.js +1 -1
  61. package/dist/client/resources/ResourceEditor.js.map +1 -1
  62. package/dist/client/resources/ResourceTree.d.ts +6 -1
  63. package/dist/client/resources/ResourceTree.d.ts.map +1 -1
  64. package/dist/client/resources/ResourceTree.js +18 -7
  65. package/dist/client/resources/ResourceTree.js.map +1 -1
  66. package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
  67. package/dist/client/resources/ResourcesPanel.js +191 -20
  68. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  69. package/dist/client/resources/use-mcp-servers.d.ts +68 -0
  70. package/dist/client/resources/use-mcp-servers.d.ts.map +1 -0
  71. package/dist/client/resources/use-mcp-servers.js +83 -0
  72. package/dist/client/resources/use-mcp-servers.js.map +1 -0
  73. package/dist/client/resources/use-resources.d.ts +39 -1
  74. package/dist/client/resources/use-resources.d.ts.map +1 -1
  75. package/dist/client/resources/use-resources.js +102 -0
  76. package/dist/client/resources/use-resources.js.map +1 -1
  77. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  78. package/dist/client/settings/SettingsPanel.js +3 -2
  79. package/dist/client/settings/SettingsPanel.js.map +1 -1
  80. package/dist/client/settings/VoiceTranscriptionSection.d.ts +14 -0
  81. package/dist/client/settings/VoiceTranscriptionSection.d.ts.map +1 -0
  82. package/dist/client/settings/VoiceTranscriptionSection.js +111 -0
  83. package/dist/client/settings/VoiceTranscriptionSection.js.map +1 -0
  84. package/dist/client/sharing/ShareButton.d.ts +6 -4
  85. package/dist/client/sharing/ShareButton.d.ts.map +1 -1
  86. package/dist/client/sharing/ShareButton.js +299 -34
  87. package/dist/client/sharing/ShareButton.js.map +1 -1
  88. package/dist/client/sharing/ShareDialog.d.ts +22 -4
  89. package/dist/client/sharing/ShareDialog.d.ts.map +1 -1
  90. package/dist/client/sharing/ShareDialog.js +170 -148
  91. package/dist/client/sharing/ShareDialog.js.map +1 -1
  92. package/dist/client/sharing/VisibilityBadge.d.ts.map +1 -1
  93. package/dist/client/sharing/VisibilityBadge.js +1 -2
  94. package/dist/client/sharing/VisibilityBadge.js.map +1 -1
  95. package/dist/client/use-action.d.ts.map +1 -1
  96. package/dist/client/use-action.js +20 -1
  97. package/dist/client/use-action.js.map +1 -1
  98. package/dist/db/migrations.d.ts +18 -3
  99. package/dist/db/migrations.d.ts.map +1 -1
  100. package/dist/db/migrations.js +25 -3
  101. package/dist/db/migrations.js.map +1 -1
  102. package/dist/deploy/workspace-core.js +2 -2
  103. package/dist/mcp-client/config.d.ts +20 -1
  104. package/dist/mcp-client/config.d.ts.map +1 -1
  105. package/dist/mcp-client/config.js +28 -11
  106. package/dist/mcp-client/config.js.map +1 -1
  107. package/dist/mcp-client/hub-client.d.ts +38 -0
  108. package/dist/mcp-client/hub-client.d.ts.map +1 -0
  109. package/dist/mcp-client/hub-client.js +147 -0
  110. package/dist/mcp-client/hub-client.js.map +1 -0
  111. package/dist/mcp-client/hub-routes.d.ts +42 -0
  112. package/dist/mcp-client/hub-routes.d.ts.map +1 -0
  113. package/dist/mcp-client/hub-routes.js +114 -0
  114. package/dist/mcp-client/hub-routes.js.map +1 -0
  115. package/dist/mcp-client/index.d.ts +15 -0
  116. package/dist/mcp-client/index.d.ts.map +1 -1
  117. package/dist/mcp-client/index.js +35 -0
  118. package/dist/mcp-client/index.js.map +1 -1
  119. package/dist/mcp-client/manager.d.ts +54 -8
  120. package/dist/mcp-client/manager.d.ts.map +1 -1
  121. package/dist/mcp-client/manager.js +276 -59
  122. package/dist/mcp-client/manager.js.map +1 -1
  123. package/dist/mcp-client/remote-store.d.ts +102 -0
  124. package/dist/mcp-client/remote-store.d.ts.map +1 -0
  125. package/dist/mcp-client/remote-store.js +200 -0
  126. package/dist/mcp-client/remote-store.js.map +1 -0
  127. package/dist/mcp-client/routes.d.ts +55 -0
  128. package/dist/mcp-client/routes.d.ts.map +1 -0
  129. package/dist/mcp-client/routes.js +384 -0
  130. package/dist/mcp-client/routes.js.map +1 -0
  131. package/dist/mcp-client/visibility.d.ts +16 -0
  132. package/dist/mcp-client/visibility.d.ts.map +1 -0
  133. package/dist/mcp-client/visibility.js +45 -0
  134. package/dist/mcp-client/visibility.js.map +1 -0
  135. package/dist/org/context.js +2 -2
  136. package/dist/org/context.js.map +1 -1
  137. package/dist/org/handlers.js +2 -2
  138. package/dist/org/handlers.js.map +1 -1
  139. package/dist/resources/handlers.d.ts.map +1 -1
  140. package/dist/resources/handlers.js +30 -0
  141. package/dist/resources/handlers.js.map +1 -1
  142. package/dist/secrets/register-framework-secrets.d.ts +13 -0
  143. package/dist/secrets/register-framework-secrets.d.ts.map +1 -0
  144. package/dist/secrets/register-framework-secrets.js +59 -0
  145. package/dist/secrets/register-framework-secrets.js.map +1 -0
  146. package/dist/secrets/register.d.ts.map +1 -1
  147. package/dist/secrets/register.js +8 -1
  148. package/dist/secrets/register.js.map +1 -1
  149. package/dist/server/action-routes.d.ts.map +1 -1
  150. package/dist/server/action-routes.js +22 -2
  151. package/dist/server/action-routes.js.map +1 -1
  152. package/dist/server/agent-chat-plugin.d.ts +16 -0
  153. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  154. package/dist/server/agent-chat-plugin.js +237 -70
  155. package/dist/server/agent-chat-plugin.js.map +1 -1
  156. package/dist/server/app-url.d.ts.map +1 -1
  157. package/dist/server/app-url.js +11 -3
  158. package/dist/server/app-url.js.map +1 -1
  159. package/dist/server/auth.d.ts.map +1 -1
  160. package/dist/server/auth.js +50 -0
  161. package/dist/server/auth.js.map +1 -1
  162. package/dist/server/better-auth-instance.d.ts.map +1 -1
  163. package/dist/server/better-auth-instance.js +99 -4
  164. package/dist/server/better-auth-instance.js.map +1 -1
  165. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  166. package/dist/server/core-routes-plugin.js +44 -0
  167. package/dist/server/core-routes-plugin.js.map +1 -1
  168. package/dist/server/create-server.d.ts.map +1 -1
  169. package/dist/server/create-server.js +6 -0
  170. package/dist/server/create-server.js.map +1 -1
  171. package/dist/server/date-utils.d.ts +15 -0
  172. package/dist/server/date-utils.d.ts.map +1 -0
  173. package/dist/server/date-utils.js +41 -0
  174. package/dist/server/date-utils.js.map +1 -0
  175. package/dist/server/index.d.ts +2 -1
  176. package/dist/server/index.d.ts.map +1 -1
  177. package/dist/server/index.js +2 -1
  178. package/dist/server/index.js.map +1 -1
  179. package/dist/server/onboarding-html.d.ts +3 -0
  180. package/dist/server/onboarding-html.d.ts.map +1 -1
  181. package/dist/server/onboarding-html.js +13 -3
  182. package/dist/server/onboarding-html.js.map +1 -1
  183. package/dist/server/request-context.d.ts +9 -0
  184. package/dist/server/request-context.d.ts.map +1 -1
  185. package/dist/server/request-context.js +10 -0
  186. package/dist/server/request-context.js.map +1 -1
  187. package/dist/server/transcribe-voice.d.ts +26 -0
  188. package/dist/server/transcribe-voice.d.ts.map +1 -0
  189. package/dist/server/transcribe-voice.js +143 -0
  190. package/dist/server/transcribe-voice.js.map +1 -0
  191. package/dist/styles/agent-native.css +111 -0
  192. package/dist/tailwind.preset.d.ts +2 -2
  193. package/dist/tailwind.preset.d.ts.map +1 -1
  194. package/dist/tailwind.preset.js +27 -7
  195. package/dist/tailwind.preset.js.map +1 -1
  196. package/dist/templates/default/app/global.css +65 -68
  197. package/dist/templates/default/components.json +1 -1
  198. package/dist/templates/default/package.json +2 -4
  199. package/dist/templates/default/vite.config.ts +3 -0
  200. package/dist/templates/workspace-core/package.json +1 -4
  201. package/dist/templates/workspace-core/src/index.ts +1 -1
  202. package/dist/templates/workspace-core/styles/tokens.css +22 -0
  203. package/dist/templates/workspace-core/tsconfig.json +1 -1
  204. package/dist/vite/client.d.ts +6 -0
  205. package/dist/vite/client.d.ts.map +1 -1
  206. package/dist/vite/client.js +18 -1
  207. package/dist/vite/client.js.map +1 -1
  208. package/docs/content/actions.md +169 -74
  209. package/docs/content/agent-teams.md +139 -0
  210. package/docs/content/cloneable-saas.md +98 -0
  211. package/docs/content/creating-templates.md +9 -11
  212. package/docs/content/deployment.md +2 -9
  213. package/docs/content/drop-in-agent.md +200 -0
  214. package/docs/content/enterprise-workspace.md +22 -10
  215. package/docs/content/getting-started.md +34 -19
  216. package/docs/content/integrations.md +3 -3
  217. package/docs/content/key-concepts.md +50 -23
  218. package/docs/content/mcp-clients.md +71 -0
  219. package/docs/content/pure-agent-apps.md +69 -0
  220. package/docs/content/recurring-jobs.md +123 -0
  221. package/docs/content/skills-guide.md +8 -0
  222. package/docs/content/template-analytics.md +190 -0
  223. package/docs/content/template-calendar.md +151 -0
  224. package/docs/content/template-clips.md +55 -0
  225. package/docs/content/template-content.md +141 -0
  226. package/docs/content/template-dispatch.md +58 -0
  227. package/docs/content/template-forms.md +51 -0
  228. package/docs/content/template-mail.md +169 -0
  229. package/docs/content/template-slides.md +218 -0
  230. package/docs/content/template-starter.md +68 -0
  231. package/docs/content/template-video.md +162 -0
  232. package/docs/content/voice-input.md +59 -0
  233. package/docs/content/what-is-agent-native.md +142 -45
  234. package/docs/content/workspace-management.md +1 -0
  235. package/docs/content/{resources.md → workspace.md} +94 -42
  236. package/package.json +9 -16
  237. package/src/templates/default/app/global.css +65 -68
  238. package/src/templates/default/components.json +1 -1
  239. package/src/templates/default/package.json +2 -4
  240. package/src/templates/default/vite.config.ts +3 -0
  241. package/src/templates/workspace-core/package.json +1 -4
  242. package/src/templates/workspace-core/src/index.ts +1 -1
  243. package/src/templates/workspace-core/styles/tokens.css +22 -0
  244. package/src/templates/workspace-core/tsconfig.json +1 -1
  245. package/dist/templates/default/postcss.config.js +0 -6
  246. package/dist/templates/default/tailwind.config.ts +0 -7
  247. package/dist/templates/workspace-core/tailwind.preset.ts +0 -34
  248. package/src/templates/default/postcss.config.js +0 -6
  249. package/src/templates/default/tailwind.config.ts +0 -7
  250. package/src/templates/workspace-core/tailwind.preset.ts +0 -34
@@ -3,14 +3,14 @@ import { getSetting, putSetting } from "../settings/store.js";
3
3
  import { getH3App } from "./framework-request-handler.js";
4
4
  import { createProductionAgentHandler, runAgentLoop, actionsToEngineTools, getActiveRunForThreadAsync, abortRun, subscribeToRun, } from "../agent/production-agent.js";
5
5
  import { resolveEngine, createAnthropicEngine } from "../agent/engine/index.js";
6
- import { McpClientManager, loadMcpConfig, autoDetectMcpConfig, mcpToolsToActionEntries, } from "../mcp-client/index.js";
6
+ import { McpClientManager, loadMcpConfig, autoDetectMcpConfig, mcpToolsToActionEntries, syncMcpActionEntries, mountMcpServersRoutes, mountMcpHubRoutes, buildMergedConfig, getHubStatus, isHubServeEnabled, } from "../mcp-client/index.js";
7
7
  import { discoverAgents } from "./agent-discovery.js";
8
8
  import { loadSchemaPromptBlock } from "./schema-prompt.js";
9
9
  import { buildAssistantMessage, extractThreadMeta, } from "../agent/thread-data-builder.js";
10
- import { defineEventHandler, setResponseStatus, setResponseHeader, getMethod, getQuery, } from "h3";
10
+ import { defineEventHandler, setResponseStatus, setResponseHeader, getMethod, getQuery, getHeader, } from "h3";
11
11
  import { getSession } from "./auth.js";
12
12
  import { getOrigin } from "./google-oauth.js";
13
- import { createThread, getThread, listThreads, searchThreads, updateThreadData, deleteThread, } from "../chat-threads/store.js";
13
+ import { createThread, getThread, listThreads, searchThreads, updateThreadData, withThreadDataLock, deleteThread, setThreadQueuedMessages, } from "../chat-threads/store.js";
14
14
  import { resourceListAccessible, resourceList, resourceGet, resourceGetByPath, ensurePersonalDefaults, SHARED_OWNER, } from "../resources/store.js";
15
15
  import nodePath from "node:path";
16
16
  import { readBody } from "./h3-helpers.js";
@@ -907,6 +907,8 @@ Resources can be personal (per-user) or shared (team-wide). By default, resource
907
907
 
908
908
  When the user gives instructions that should apply to all users/sessions, update the shared "AGENTS.md" resource.
909
909
 
910
+ **Resources are NOT an agent scratchpad.** Never use \`resource-write\` to store executable scripts, task plans, retry notes, or work-in-progress files you're writing to yourself. Specifically, do NOT create resources under \`scripts/\` or \`tasks/\` unless the user explicitly asked for a file at that path, or a tool (like \`create-job\` or \`spawn-task\`) writes there as part of its contract. If you can't complete a task with the tools you have, say so — don't improvise by leaving behind \`FINAL-*.md\`, \`EXECUTE-NOW-*.js\`, or similar artifacts. Resources are visible to the user in the workspace sidebar; every file you write is something they'll see and have to clean up.
911
+
910
912
  ### Navigation Rule
911
913
 
912
914
  When the user says "show me", "go to", "open", "switch to", or similar navigation language, ALWAYS use the \`navigate\` action to update the UI. The user expects to SEE the result in the main app, not just read it in chat. Navigate first, then fetch/display data.
@@ -1357,6 +1359,22 @@ export function createAgentChatPlugin(options) {
1357
1359
  // this, requests can race ahead of the bootstrap and hit the SSR catch-all.
1358
1360
  const { awaitBootstrap } = await import("./framework-request-handler.js");
1359
1361
  await awaitBootstrap(nitroApp);
1362
+ // Reap phantom runs left over from the previous process (HMR restart,
1363
+ // process crash, isolate eviction). Any run whose heartbeat is already
1364
+ // stale by startup time had a dead producer; mark it errored so the
1365
+ // next /runs/active check returns a terminal status and reconnecting
1366
+ // clients don't spin on "Thinking...". Runs owned by OTHER live
1367
+ // isolates are protected by their fresh heartbeats.
1368
+ try {
1369
+ const { reapAllStaleRuns } = await import("../agent/run-store.js");
1370
+ const reaped = await reapAllStaleRuns();
1371
+ if (reaped > 0) {
1372
+ console.log(`[agent-chat] reaped ${reaped} stale run(s) on startup`);
1373
+ }
1374
+ }
1375
+ catch {
1376
+ // Best effort — don't block plugin init if SQL isn't ready yet.
1377
+ }
1360
1378
  const env = process.env.NODE_ENV;
1361
1379
  // AGENT_MODE=production forces production agent constraints even in dev
1362
1380
  const canToggle = (env === "development" || env === "test") &&
@@ -1385,22 +1403,27 @@ export function createAgentChatPlugin(options) {
1385
1403
  // `canToggle` means "this environment allows toggling" (static); this
1386
1404
  // function means "the user currently has dev mode ON" (live).
1387
1405
  const isDevMode = () => currentDevMode;
1388
- // Initialize MCP client (connects to user-configured local MCP servers).
1389
- // Graceful-degrade: any failure yields zero MCP tools and agent-chat keeps
1390
- // working as before. No-op outside Node runtimes.
1391
- let mcpConfig = loadMcpConfig();
1406
+ // Initialize MCP client. Merges file/env config + auto-detected binaries
1407
+ // + any remote servers users have added through the settings UI (persisted
1408
+ // in the settings table, scanned across all scopes so we never drop
1409
+ // another user's entries). Graceful-degrade: any failure yields zero MCP
1410
+ // tools and agent-chat keeps working as before.
1411
+ let mcpConfig = await buildMergedConfig().catch((err) => {
1412
+ console.warn(`[mcp-client] buildMergedConfig failed: ${err?.message ?? err}`);
1413
+ return null;
1414
+ });
1392
1415
  if (!mcpConfig) {
1393
- mcpConfig = autoDetectMcpConfig();
1394
- if (mcpConfig) {
1395
- const detected = Object.keys(mcpConfig.servers).join(", ");
1396
- console.log(`[mcp-client] auto-detected ${detected}, registering as MCP server — set AGENT_NATIVE_DISABLE_MCP_AUTODETECT=1 to opt out`);
1416
+ const fileOrEnv = loadMcpConfig() ?? autoDetectMcpConfig();
1417
+ mcpConfig = fileOrEnv;
1418
+ if (mcpConfig?.source) {
1419
+ console.log(`[mcp-client] loaded config from ${mcpConfig.source} (${Object.keys(mcpConfig.servers).length} server(s))`);
1397
1420
  }
1398
1421
  else {
1399
- console.log("[mcp-client] no mcp.config.json and no auto-detectable servers — skipping MCP tools");
1422
+ console.log("[mcp-client] no configured MCP servers — skipping MCP tools");
1400
1423
  }
1401
1424
  }
1402
1425
  else if (mcpConfig.source) {
1403
- console.log(`[mcp-client] loaded config from ${mcpConfig.source} (${Object.keys(mcpConfig.servers).length} server(s))`);
1426
+ console.log(`[mcp-client] merged config (${Object.keys(mcpConfig.servers).length} server(s), source: ${mcpConfig.source})`);
1404
1427
  }
1405
1428
  const mcpManager = new McpClientManager(mcpConfig);
1406
1429
  try {
@@ -1411,8 +1434,22 @@ export function createAgentChatPlugin(options) {
1411
1434
  }
1412
1435
  setGlobalMcpManager(mcpManager);
1413
1436
  const mcpActionEntries = mcpToolsToActionEntries(mcpManager);
1414
- // Mount status route so tooling/onboarding can inspect MCP state.
1437
+ // Mount status + management routes so the settings UI can list / add /
1438
+ // remove remote MCP servers and hot-reload the running manager.
1415
1439
  mountMcpStatusRoute(nitroApp, mcpManager);
1440
+ mountMcpServersRoutes(nitroApp, mcpManager);
1441
+ // Hub-serve: expose org-scope servers to other agent-native apps in the
1442
+ // workspace when `AGENT_NATIVE_MCP_HUB_TOKEN` is set (dispatch, by
1443
+ // convention). Gated by the env var so mounting is a no-op otherwise.
1444
+ if (isHubServeEnabled()) {
1445
+ mountMcpHubRoutes(nitroApp);
1446
+ console.log("[mcp-client] hub serve enabled — other apps can pull org servers via /_agent-native/mcp/hub/servers");
1447
+ }
1448
+ const hubStatus = getHubStatus();
1449
+ if (hubStatus.consuming) {
1450
+ console.log(`[mcp-client] hub consume enabled — pulling from ${hubStatus.hubUrl}`);
1451
+ }
1452
+ mountMcpHubStatusRoute(nitroApp);
1416
1453
  // Ensure we tear down child processes if the host shuts down cleanly.
1417
1454
  if (typeof process !== "undefined" &&
1418
1455
  typeof process.once === "function" &&
@@ -1872,53 +1909,71 @@ export function createAgentChatPlugin(options) {
1872
1909
  const onRunComplete = async (run, threadId) => {
1873
1910
  if (!threadId)
1874
1911
  return;
1875
- try {
1876
- const thread = await getThread(threadId);
1877
- if (!thread)
1878
- return;
1879
- const assistantMsg = buildAssistantMessage(run.events ?? [], run.runId);
1880
- if (!assistantMsg) {
1881
- // No content produced — just bump timestamp
1882
- await updateThreadData(threadId, thread.threadData, thread.title, thread.preview, thread.messageCount);
1883
- return;
1884
- }
1885
- // Parse existing thread_data, append assistant message only if
1886
- // the frontend hasn't already saved it (avoids duplicates when
1887
- // the client is still connected during a normal flow).
1888
- let repo;
1912
+ // Serialize the read-modify-write against the same thread's other
1913
+ // `thread_data` writers (setThreadQueuedMessages, setThreadEngineMeta,
1914
+ // the frontend-triggered saves below). Without the lock, a concurrent
1915
+ // queued-message save can clobber the assistant message we just
1916
+ // appended here, or vice versa.
1917
+ await withThreadDataLock(threadId, async () => {
1889
1918
  try {
1890
- repo = JSON.parse(thread.threadData || "{}");
1919
+ const thread = await getThread(threadId);
1920
+ if (!thread)
1921
+ return;
1922
+ const assistantMsg = buildAssistantMessage(run.events ?? [], run.runId);
1923
+ if (!assistantMsg) {
1924
+ // No content produced — just bump timestamp
1925
+ await updateThreadData(threadId, thread.threadData, thread.title, thread.preview, thread.messageCount);
1926
+ return;
1927
+ }
1928
+ // Parse existing thread_data, append assistant message only if
1929
+ // the frontend hasn't already saved it (avoids duplicates when
1930
+ // the client is still connected during a normal flow).
1931
+ let repo;
1932
+ try {
1933
+ repo = JSON.parse(thread.threadData || "{}");
1934
+ }
1935
+ catch {
1936
+ repo = {};
1937
+ }
1938
+ if (!Array.isArray(repo.messages))
1939
+ repo.messages = [];
1940
+ const lastMsg = repo.messages[repo.messages.length - 1];
1941
+ // Check both wrapped ({ message: { role } }) and unwrapped ({ role }) formats
1942
+ const lastRole = lastMsg?.message?.role ?? lastMsg?.role;
1943
+ const lastContent = lastMsg?.message?.content ?? lastMsg?.content;
1944
+ const lastContentIsEmpty = Array.isArray(lastContent)
1945
+ ? lastContent.length === 0
1946
+ : lastContent == null || lastContent === "";
1947
+ if (lastRole === "assistant" && !lastContentIsEmpty) {
1948
+ // Frontend already saved the assistant response — just bump timestamp
1949
+ await updateThreadData(threadId, thread.threadData, thread.title, thread.preview, thread.messageCount);
1950
+ return;
1951
+ }
1952
+ if (lastRole === "assistant" && lastContentIsEmpty) {
1953
+ // The frontend wrote an empty assistant placeholder before the stream
1954
+ // had any content (common when the user reloads mid-run, and the 5s
1955
+ // periodic save raced with the first text chunk). Replace it with
1956
+ // the server's reconstructed message so the turn isn't lost.
1957
+ repo.messages.pop();
1958
+ }
1959
+ // Determine if repo uses wrapped format ({ message, parentId }) or flat format
1960
+ const isWrapped = lastMsg && "message" in lastMsg;
1961
+ if (isWrapped) {
1962
+ const parentId = repo.messages.length > 0
1963
+ ? (repo.messages[repo.messages.length - 1].message?.id ?? null)
1964
+ : null;
1965
+ repo.messages.push({ message: assistantMsg, parentId });
1966
+ }
1967
+ else {
1968
+ repo.messages.push(assistantMsg);
1969
+ }
1970
+ const meta = extractThreadMeta(repo);
1971
+ await updateThreadData(threadId, JSON.stringify(repo), meta.title || thread.title, meta.preview || thread.preview, repo.messages.length);
1891
1972
  }
1892
1973
  catch {
1893
- repo = {};
1894
- }
1895
- if (!Array.isArray(repo.messages))
1896
- repo.messages = [];
1897
- const lastMsg = repo.messages[repo.messages.length - 1];
1898
- // Check both wrapped ({ message: { role } }) and unwrapped ({ role }) formats
1899
- const lastRole = lastMsg?.message?.role ?? lastMsg?.role;
1900
- if (lastRole === "assistant") {
1901
- // Frontend already saved the assistant response — just bump timestamp
1902
- await updateThreadData(threadId, thread.threadData, thread.title, thread.preview, thread.messageCount);
1903
- return;
1904
- }
1905
- // Determine if repo uses wrapped format ({ message, parentId }) or flat format
1906
- const isWrapped = lastMsg && "message" in lastMsg;
1907
- if (isWrapped) {
1908
- const parentId = repo.messages.length > 0
1909
- ? (repo.messages[repo.messages.length - 1].message?.id ?? null)
1910
- : null;
1911
- repo.messages.push({ message: assistantMsg, parentId });
1912
- }
1913
- else {
1914
- repo.messages.push(assistantMsg);
1974
+ // Best-effort — don't break cleanup
1915
1975
  }
1916
- const meta = extractThreadMeta(repo);
1917
- await updateThreadData(threadId, JSON.stringify(repo), meta.title || thread.title, meta.preview || thread.preview, repo.messages.length);
1918
- }
1919
- catch {
1920
- // Best-effort — don't break cleanup
1921
- }
1976
+ });
1922
1977
  };
1923
1978
  // ─── Agent Teams: per-run send reference ─────────────────────────
1924
1979
  // Team tools need to emit events to the parent chat's SSE stream.
@@ -1991,6 +2046,13 @@ export function createAgentChatPlugin(options) {
1991
2046
  ...browserTools,
1992
2047
  ...mcpActionEntries,
1993
2048
  };
2049
+ // Keep the prod action dict's MCP entries in sync when the manager's
2050
+ // server set changes at runtime (e.g. a user adds a remote MCP server
2051
+ // through the settings UI). getEngineTools() in production-agent re-reads
2052
+ // the registry per request, so updates here propagate without restart.
2053
+ mcpManager.onChange(() => {
2054
+ syncMcpActionEntries(mcpManager, prodActions);
2055
+ });
1994
2056
  // Always build the production handler (includes resource tools + call-agent + team tools)
1995
2057
  // In production mode (!canToggle), enable usage tracking and limits
1996
2058
  const isHostedProd = !canToggle;
@@ -2006,6 +2068,11 @@ export function createAgentChatPlugin(options) {
2006
2068
  return "";
2007
2069
  }
2008
2070
  };
2071
+ const leanPrompt = options?.leanPrompt === true;
2072
+ // Lean mode: use only the template's systemPrompt + actions list.
2073
+ // Skip resource loading, schema block, and extraContext — those add
2074
+ // DB round-trips and tokens that minimal/voice apps don't need.
2075
+ const leanBasePrompt = (options?.systemPrompt ?? "") + prodActionsPrompt;
2009
2076
  const prodHandler = createProductionAgentHandler({
2010
2077
  actions: prodActions,
2011
2078
  systemPrompt: async (event) => {
@@ -2014,6 +2081,10 @@ export function createAgentChatPlugin(options) {
2014
2081
  _currentRunOwner = owner;
2015
2082
  const { getOwnerAnthropicApiKey } = await import("../agent/production-agent.js");
2016
2083
  _currentRunUserApiKey = await getOwnerAnthropicApiKey(owner);
2084
+ if (leanPrompt) {
2085
+ _currentRunSystemPrompt = leanBasePrompt;
2086
+ return _currentRunSystemPrompt;
2087
+ }
2017
2088
  const resources = await loadResourcesForPrompt(owner);
2018
2089
  const schemaBlock = await buildSchemaBlock(owner, false);
2019
2090
  const extra = await resolveExtraContext(event, owner);
@@ -2023,6 +2094,7 @@ export function createAgentChatPlugin(options) {
2023
2094
  model: options?.model ??
2024
2095
  (isHostedProd ? "claude-haiku-4-5-20251001" : undefined),
2025
2096
  apiKey: options?.apiKey,
2097
+ skipFilesContext: leanPrompt,
2026
2098
  onRunStart: (send, threadId) => {
2027
2099
  _runSendByThread.set(threadId, send);
2028
2100
  _currentRunThreadId = threadId;
@@ -2046,17 +2118,30 @@ export function createAgentChatPlugin(options) {
2046
2118
  // how Claude Code works locally and dramatically reduces the rate of
2047
2119
  // degenerate empty-object tool calls. The CLI syntax for each action is
2048
2120
  // listed in the dev system prompt's "Available Actions" section.
2049
- const devActions = {
2050
- ...resourceScripts,
2051
- ...docsScripts,
2052
- ...chatScripts,
2053
- ...callAgentScript,
2054
- ...teamTools,
2055
- ...jobTools,
2056
- ...browserTools,
2057
- ...mcpActionEntries,
2058
- ...(await createDevScriptRegistry()),
2059
- };
2121
+ // In lean mode, expose the template's actions directly as native tools
2122
+ // instead of routing through shell — the lean system prompt has no
2123
+ // shell-usage guidance, so shell-based action invocation would break.
2124
+ const devActions = leanPrompt
2125
+ ? prodActions
2126
+ : {
2127
+ ...resourceScripts,
2128
+ ...docsScripts,
2129
+ ...chatScripts,
2130
+ ...callAgentScript,
2131
+ ...teamTools,
2132
+ ...jobTools,
2133
+ ...browserTools,
2134
+ ...mcpActionEntries,
2135
+ ...(await createDevScriptRegistry()),
2136
+ };
2137
+ // Keep dev action dict in sync with runtime MCP additions. When
2138
+ // leanPrompt is true, devActions === prodActions so the prod listener
2139
+ // already covers it.
2140
+ if (devActions !== prodActions) {
2141
+ mcpManager.onChange(() => {
2142
+ syncMcpActionEntries(mcpManager, devActions);
2143
+ });
2144
+ }
2060
2145
  devHandler = createProductionAgentHandler({
2061
2146
  actions: devActions,
2062
2147
  systemPrompt: async (event) => {
@@ -2065,6 +2150,10 @@ export function createAgentChatPlugin(options) {
2065
2150
  _currentRunOwner = owner;
2066
2151
  const { getOwnerAnthropicApiKey } = await import("../agent/production-agent.js");
2067
2152
  _currentRunUserApiKey = await getOwnerAnthropicApiKey(owner);
2153
+ if (leanPrompt) {
2154
+ _currentRunSystemPrompt = leanBasePrompt;
2155
+ return _currentRunSystemPrompt;
2156
+ }
2068
2157
  const resources = await loadResourcesForPrompt(owner);
2069
2158
  const schemaBlock = await buildSchemaBlock(owner, true);
2070
2159
  const extra = await resolveExtraContext(event, owner);
@@ -2073,6 +2162,7 @@ export function createAgentChatPlugin(options) {
2073
2162
  },
2074
2163
  model: options?.model,
2075
2164
  apiKey: options?.apiKey,
2165
+ skipFilesContext: leanPrompt,
2076
2166
  onRunStart: (send, threadId) => {
2077
2167
  _runSendByThread.set(threadId, send);
2078
2168
  _currentRunThreadId = threadId;
@@ -2624,6 +2714,7 @@ export function createAgentChatPlugin(options) {
2624
2714
  runId: run.runId,
2625
2715
  threadId: run.threadId,
2626
2716
  status: run.status,
2717
+ heartbeatAt: run.heartbeatAt,
2627
2718
  };
2628
2719
  }
2629
2720
  setResponseStatus(event, 405);
@@ -2657,13 +2748,60 @@ export function createAgentChatPlugin(options) {
2657
2748
  return thread;
2658
2749
  }
2659
2750
  if (method === "PUT") {
2751
+ // Hold the thread_data lock for the full read-modify-write so
2752
+ // periodic saves from the frontend don't race with
2753
+ // onRunComplete / setThreadQueuedMessages / setThreadEngineMeta.
2754
+ // Without the lock, a client save that lands during an agent
2755
+ // run could clobber the assistant message the server just
2756
+ // appended (and vice versa).
2757
+ return await withThreadDataLock(threadId, async () => {
2758
+ const thread = await getThread(threadId);
2759
+ if (!thread || thread.ownerEmail !== owner) {
2760
+ setResponseStatus(event, 404);
2761
+ return { error: "Thread not found" };
2762
+ }
2763
+ const body = await readBody(event);
2764
+ let newThreadData = body.threadData || thread.threadData;
2765
+ // Preserve queuedMessages from the existing thread_data when the
2766
+ // incoming blob doesn't include it. Periodic full-thread saves
2767
+ // (exported via threadRuntime.export) don't carry the queue, and
2768
+ // we don't want them to clobber queued-message state persisted
2769
+ // via POST /threads/:id/queued.
2770
+ if (body.threadData) {
2771
+ try {
2772
+ const existing = JSON.parse(thread.threadData);
2773
+ if (existing.queuedMessages !== undefined) {
2774
+ const incoming = JSON.parse(newThreadData);
2775
+ if (incoming.queuedMessages === undefined) {
2776
+ incoming.queuedMessages = existing.queuedMessages;
2777
+ newThreadData = JSON.stringify(incoming);
2778
+ }
2779
+ }
2780
+ }
2781
+ catch {
2782
+ // Invalid JSON in either side — fall back to raw body blob.
2783
+ }
2784
+ }
2785
+ await updateThreadData(threadId, newThreadData, body.title ?? thread.title, body.preview ?? thread.preview, body.messageCount || thread.messageCount);
2786
+ return { ok: true };
2787
+ });
2788
+ }
2789
+ // POST /threads/:id/queued — debounced writes from the client
2790
+ // when the user adds/removes/dequeues a queued message. Keeps
2791
+ // queued messages durable across reloads without piggybacking
2792
+ // on full-thread saves.
2793
+ if (method === "POST" &&
2794
+ /\/threads\/[^/?]+\/queued/.test(event.node?.req?.url || event.path || "")) {
2660
2795
  const thread = await getThread(threadId);
2661
2796
  if (!thread || thread.ownerEmail !== owner) {
2662
2797
  setResponseStatus(event, 404);
2663
2798
  return { error: "Thread not found" };
2664
2799
  }
2665
2800
  const body = await readBody(event);
2666
- await updateThreadData(threadId, body.threadData || thread.threadData, body.title ?? thread.title, body.preview ?? thread.preview, body.messageCount || thread.messageCount);
2801
+ const queued = Array.isArray(body?.queuedMessages)
2802
+ ? body.queuedMessages
2803
+ : [];
2804
+ await setThreadQueuedMessages(threadId, queued);
2667
2805
  return { ok: true };
2668
2806
  }
2669
2807
  if (method === "DELETE") {
@@ -2739,7 +2877,18 @@ export function createAgentChatPlugin(options) {
2739
2877
  else {
2740
2878
  delete process.env.AGENT_ORG_ID;
2741
2879
  }
2742
- return runWithRequestContext({ userEmail: owner, orgId: resolvedOrgId }, () => {
2880
+ // Propagate the caller's IANA timezone from `x-user-timezone` so that
2881
+ // tool calls made by the agent (e.g. log-meal with no explicit date)
2882
+ // resolve "today" in the user's local timezone instead of server UTC.
2883
+ const tzRaw = getHeader(event, "x-user-timezone");
2884
+ const timezone = typeof tzRaw === "string" &&
2885
+ tzRaw.trim().length > 0 &&
2886
+ tzRaw.trim().length < 64
2887
+ ? tzRaw.trim()
2888
+ : undefined;
2889
+ if (timezone)
2890
+ process.env.AGENT_USER_TIMEZONE = timezone;
2891
+ return runWithRequestContext({ userEmail: owner, orgId: resolvedOrgId, timezone }, () => {
2743
2892
  const handler = currentDevMode && devHandler ? devHandler : prodHandler;
2744
2893
  return handler(event);
2745
2894
  });
@@ -2800,6 +2949,24 @@ function setGlobalMcpManager(manager) {
2800
2949
  export function getGlobalMcpManager() {
2801
2950
  return _globalMcpManager;
2802
2951
  }
2952
+ function mountMcpHubStatusRoute(nitroApp) {
2953
+ if (globalThis.__agentNativeMcpHubStatusMounted)
2954
+ return;
2955
+ globalThis.__agentNativeMcpHubStatusMounted = true;
2956
+ try {
2957
+ getH3App(nitroApp).use("/_agent-native/mcp/hub/status", defineEventHandler(async (event) => {
2958
+ if (getMethod(event) !== "GET") {
2959
+ setResponseStatus(event, 405);
2960
+ return { error: "Method not allowed" };
2961
+ }
2962
+ setResponseHeader(event, "Content-Type", "application/json");
2963
+ return getHubStatus();
2964
+ }));
2965
+ }
2966
+ catch (err) {
2967
+ console.warn(`[mcp-client] Failed to mount /_agent-native/mcp/hub/status: ${err?.message ?? err}`);
2968
+ }
2969
+ }
2803
2970
  function mountMcpStatusRoute(nitroApp, manager) {
2804
2971
  // Idempotent — agent-chat-plugin can be invoked once per process; guard anyway.
2805
2972
  if (globalThis.__agentNativeMcpStatusMounted)