stagent 0.9.6 → 0.11.0

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 (396) hide show
  1. package/README.md +20 -44
  2. package/dist/cli.js +66 -18
  3. package/docs/.coverage-gaps.json +144 -56
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/agent-intelligence.md +12 -2
  6. package/docs/features/chat.md +40 -5
  7. package/docs/features/cost-usage.md +1 -1
  8. package/docs/features/documents.md +5 -2
  9. package/docs/features/inbox-notifications.md +10 -2
  10. package/docs/features/keyboard-navigation.md +12 -3
  11. package/docs/features/provider-runtimes.md +20 -2
  12. package/docs/features/schedules.md +32 -4
  13. package/docs/features/settings.md +28 -5
  14. package/docs/features/shared-components.md +7 -3
  15. package/docs/features/tables.md +11 -2
  16. package/docs/features/tool-permissions.md +6 -2
  17. package/docs/features/workflows.md +14 -4
  18. package/docs/index.md +1 -1
  19. package/docs/journeys/developer.md +39 -2
  20. package/docs/journeys/personal-use.md +32 -8
  21. package/docs/journeys/power-user.md +45 -14
  22. package/docs/journeys/work-use.md +17 -8
  23. package/docs/manifest.json +15 -15
  24. package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
  25. package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
  26. package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
  27. package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
  28. package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
  29. package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
  30. package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
  31. package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
  32. package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
  33. package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
  34. package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
  35. package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
  36. package/next.config.mjs +1 -0
  37. package/package.json +3 -2
  38. package/src/__tests__/instrumentation-smoke.test.ts +15 -0
  39. package/src/app/analytics/page.tsx +1 -21
  40. package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
  41. package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
  42. package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
  43. package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
  44. package/src/app/api/chat/export/route.ts +52 -0
  45. package/src/app/api/chat/files/search/route.ts +50 -0
  46. package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
  47. package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
  48. package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
  49. package/src/app/api/environment/skills/route.ts +13 -0
  50. package/src/app/api/instance/config/route.ts +41 -0
  51. package/src/app/api/instance/init/route.ts +34 -0
  52. package/src/app/api/instance/upgrade/check/route.ts +26 -0
  53. package/src/app/api/instance/upgrade/route.ts +96 -0
  54. package/src/app/api/instance/upgrade/status/route.ts +35 -0
  55. package/src/app/api/memory/route.ts +0 -11
  56. package/src/app/api/notifications/route.ts +4 -2
  57. package/src/app/api/projects/[id]/route.ts +5 -155
  58. package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
  59. package/src/app/api/schedules/[id]/execute/route.ts +111 -0
  60. package/src/app/api/schedules/[id]/route.ts +9 -1
  61. package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
  62. package/src/app/api/schedules/route.ts +3 -12
  63. package/src/app/api/settings/chat/pins/route.ts +94 -0
  64. package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
  65. package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
  66. package/src/app/api/settings/environment/route.ts +26 -0
  67. package/src/app/api/settings/openai/login/route.ts +22 -0
  68. package/src/app/api/settings/openai/logout/route.ts +7 -0
  69. package/src/app/api/settings/openai/route.ts +21 -1
  70. package/src/app/api/settings/providers/route.ts +35 -8
  71. package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
  72. package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
  73. package/src/app/api/tables/[id]/enrich/route.ts +147 -0
  74. package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
  75. package/src/app/api/tasks/[id]/execute/route.ts +52 -33
  76. package/src/app/api/tasks/[id]/respond/route.ts +31 -15
  77. package/src/app/api/tasks/[id]/resume/route.ts +24 -3
  78. package/src/app/api/workflows/[id]/resume/route.ts +59 -0
  79. package/src/app/api/workflows/[id]/status/route.ts +22 -8
  80. package/src/app/api/workspace/context/route.ts +2 -0
  81. package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
  82. package/src/app/chat/page.tsx +11 -0
  83. package/src/app/documents/page.tsx +4 -1
  84. package/src/app/inbox/page.tsx +12 -5
  85. package/src/app/layout.tsx +42 -21
  86. package/src/app/page.tsx +0 -2
  87. package/src/app/settings/page.tsx +8 -9
  88. package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
  89. package/src/components/chat/__tests__/chat-session-provider.test.tsx +573 -0
  90. package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
  91. package/src/components/chat/capability-banner.tsx +68 -0
  92. package/src/components/chat/chat-command-popover.tsx +670 -49
  93. package/src/components/chat/chat-input.tsx +104 -10
  94. package/src/components/chat/chat-message.tsx +12 -3
  95. package/src/components/chat/chat-session-provider.tsx +790 -0
  96. package/src/components/chat/chat-shell.tsx +151 -401
  97. package/src/components/chat/command-tab-bar.tsx +68 -0
  98. package/src/components/chat/conversation-template-picker.tsx +421 -0
  99. package/src/components/chat/help-dialog.tsx +39 -0
  100. package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
  101. package/src/components/chat/skill-row.tsx +147 -0
  102. package/src/components/documents/document-browser.tsx +37 -19
  103. package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
  104. package/src/components/instance/instance-section.tsx +382 -0
  105. package/src/components/instance/upgrade-badge.tsx +219 -0
  106. package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
  107. package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
  108. package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
  109. package/src/components/notifications/batch-proposal-review.tsx +20 -5
  110. package/src/components/notifications/inbox-list.tsx +11 -2
  111. package/src/components/notifications/notification-item.tsx +56 -2
  112. package/src/components/notifications/pending-approval-host.tsx +56 -37
  113. package/src/components/notifications/permission-response-actions.tsx +155 -1
  114. package/src/components/schedules/schedule-create-sheet.tsx +19 -1
  115. package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
  116. package/src/components/schedules/schedule-form.tsx +31 -0
  117. package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
  118. package/src/components/settings/auth-method-selector.tsx +19 -4
  119. package/src/components/settings/auth-status-badge.tsx +28 -3
  120. package/src/components/settings/environment-section.tsx +102 -0
  121. package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
  122. package/src/components/settings/openai-runtime-section.tsx +7 -1
  123. package/src/components/settings/providers-runtimes-section.tsx +138 -19
  124. package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
  125. package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
  126. package/src/components/shared/app-sidebar.tsx +4 -3
  127. package/src/components/shared/command-palette.tsx +266 -7
  128. package/src/components/shared/filter-hint.tsx +70 -0
  129. package/src/components/shared/filter-input.tsx +59 -0
  130. package/src/components/shared/saved-searches-manager.tsx +199 -0
  131. package/src/components/shared/theme-toggle.tsx +5 -24
  132. package/src/components/shared/workspace-indicator.tsx +61 -2
  133. package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
  134. package/src/components/tables/table-create-sheet.tsx +4 -0
  135. package/src/components/tables/table-enrichment-runs.tsx +103 -0
  136. package/src/components/tables/table-enrichment-sheet.tsx +538 -0
  137. package/src/components/tables/table-spreadsheet.tsx +29 -5
  138. package/src/components/tables/table-toolbar.tsx +10 -1
  139. package/src/components/tasks/kanban-board.tsx +1 -0
  140. package/src/components/tasks/kanban-column.tsx +53 -14
  141. package/src/components/tasks/task-bento-grid.tsx +31 -2
  142. package/src/components/tasks/task-card.tsx +29 -3
  143. package/src/components/tasks/task-chip-bar.tsx +54 -1
  144. package/src/components/tasks/task-result-renderer.tsx +1 -1
  145. package/src/components/workflows/delay-step-body.tsx +109 -0
  146. package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
  147. package/src/components/workflows/loop-status-view.tsx +1 -1
  148. package/src/components/workflows/shared/step-result.tsx +78 -0
  149. package/src/components/workflows/shared/workflow-header.tsx +141 -0
  150. package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
  151. package/src/components/workflows/swarm-dashboard.tsx +2 -15
  152. package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
  153. package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
  154. package/src/components/workflows/workflow-form-view.tsx +133 -16
  155. package/src/components/workflows/workflow-status-view.tsx +30 -740
  156. package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
  157. package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
  158. package/src/hooks/use-active-skills.ts +110 -0
  159. package/src/hooks/use-chat-autocomplete.ts +120 -7
  160. package/src/hooks/use-enriched-skills.ts +19 -0
  161. package/src/hooks/use-pinned-entries.ts +104 -0
  162. package/src/hooks/use-recent-user-messages.ts +19 -0
  163. package/src/hooks/use-saved-searches.ts +142 -0
  164. package/src/instrumentation-node.ts +94 -0
  165. package/src/instrumentation.ts +4 -48
  166. package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
  167. package/src/lib/agents/__tests__/claude-agent.test.ts +212 -0
  168. package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
  169. package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
  170. package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
  171. package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
  172. package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
  173. package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
  174. package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
  175. package/src/lib/agents/claude-agent.ts +217 -21
  176. package/src/lib/agents/execution-manager.ts +0 -35
  177. package/src/lib/agents/handoff/bus.ts +2 -2
  178. package/src/lib/agents/learned-context.ts +0 -12
  179. package/src/lib/agents/learning-session.ts +18 -5
  180. package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
  181. package/src/lib/agents/profiles/__tests__/registry.test.ts +53 -4
  182. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +97 -0
  183. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +36 -0
  184. package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
  185. package/src/lib/agents/profiles/registry.ts +18 -0
  186. package/src/lib/agents/profiles/types.ts +7 -1
  187. package/src/lib/agents/router.ts +3 -6
  188. package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
  189. package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
  190. package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
  191. package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
  192. package/src/lib/agents/runtime/catalog.ts +121 -0
  193. package/src/lib/agents/runtime/claude-sdk.ts +32 -0
  194. package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
  195. package/src/lib/agents/runtime/execution-target.ts +456 -0
  196. package/src/lib/agents/runtime/index.ts +4 -0
  197. package/src/lib/agents/runtime/launch-failure.ts +101 -0
  198. package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
  199. package/src/lib/agents/runtime/openai-codex.ts +64 -60
  200. package/src/lib/agents/runtime/openai-direct.ts +8 -0
  201. package/src/lib/agents/runtime/types.ts +8 -0
  202. package/src/lib/agents/task-dispatch.ts +220 -0
  203. package/src/lib/agents/tool-permissions.ts +16 -1
  204. package/src/lib/book/chapter-mapping.ts +11 -0
  205. package/src/lib/book/content.ts +10 -0
  206. package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
  207. package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
  208. package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
  209. package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
  210. package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
  211. package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
  212. package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
  213. package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
  214. package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
  215. package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
  216. package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
  217. package/src/lib/chat/__tests__/types.test.ts +28 -0
  218. package/src/lib/chat/active-skills.ts +31 -0
  219. package/src/lib/chat/active-streams.ts +27 -0
  220. package/src/lib/chat/clean-filter-input.ts +30 -0
  221. package/src/lib/chat/codex-engine.ts +46 -24
  222. package/src/lib/chat/command-tabs.ts +61 -0
  223. package/src/lib/chat/context-builder.ts +146 -4
  224. package/src/lib/chat/dismissals.ts +73 -0
  225. package/src/lib/chat/engine.ts +159 -18
  226. package/src/lib/chat/files/__tests__/search.test.ts +135 -0
  227. package/src/lib/chat/files/expand-mention.ts +76 -0
  228. package/src/lib/chat/files/search.ts +99 -0
  229. package/src/lib/chat/reconcile.ts +117 -0
  230. package/src/lib/chat/skill-composition.ts +210 -0
  231. package/src/lib/chat/skill-conflict.ts +105 -0
  232. package/src/lib/chat/stagent-tools.ts +7 -19
  233. package/src/lib/chat/stream-telemetry.ts +137 -0
  234. package/src/lib/chat/suggested-prompts.ts +28 -1
  235. package/src/lib/chat/system-prompt.ts +48 -1
  236. package/src/lib/chat/tool-catalog.ts +35 -4
  237. package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
  238. package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
  239. package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
  240. package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
  241. package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
  242. package/src/lib/chat/tools/__tests__/task-tools.test.ts +399 -0
  243. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +351 -0
  244. package/src/lib/chat/tools/blueprint-tools.ts +190 -0
  245. package/src/lib/chat/tools/document-tools.ts +29 -13
  246. package/src/lib/chat/tools/helpers.ts +41 -0
  247. package/src/lib/chat/tools/notification-tools.ts +9 -5
  248. package/src/lib/chat/tools/profile-tools.ts +120 -23
  249. package/src/lib/chat/tools/project-tools.ts +33 -0
  250. package/src/lib/chat/tools/schedule-tools.ts +44 -11
  251. package/src/lib/chat/tools/skill-tools.ts +183 -0
  252. package/src/lib/chat/tools/table-tools.ts +71 -0
  253. package/src/lib/chat/tools/task-tools.ts +89 -21
  254. package/src/lib/chat/tools/workflow-tools.ts +275 -32
  255. package/src/lib/chat/types.ts +15 -0
  256. package/src/lib/constants/settings.ts +10 -18
  257. package/src/lib/data/__tests__/clear.test.ts +56 -2
  258. package/src/lib/data/clear.ts +17 -16
  259. package/src/lib/data/delete-project.ts +171 -0
  260. package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
  261. package/src/lib/db/bootstrap.ts +62 -16
  262. package/src/lib/db/index.ts +5 -0
  263. package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
  264. package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
  265. package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
  266. package/src/lib/db/migrations/0026_drop_license.sql +3 -0
  267. package/src/lib/db/migrations/meta/_journal.json +21 -0
  268. package/src/lib/db/schema.ts +94 -23
  269. package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
  270. package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
  271. package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
  272. package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
  273. package/src/lib/environment/data.ts +9 -0
  274. package/src/lib/environment/list-skills.ts +176 -0
  275. package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
  276. package/src/lib/environment/parsers/skill.ts +26 -5
  277. package/src/lib/environment/profile-generator.ts +54 -0
  278. package/src/lib/environment/skill-enrichment.ts +106 -0
  279. package/src/lib/environment/skill-recommendations.ts +66 -0
  280. package/src/lib/environment/workspace-context.ts +13 -1
  281. package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
  282. package/src/lib/filters/__tests__/parse.test.ts +135 -0
  283. package/src/lib/filters/parse.ts +86 -0
  284. package/src/lib/import/dedup.ts +4 -54
  285. package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
  286. package/src/lib/instance/__tests__/detect.test.ts +115 -0
  287. package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
  288. package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
  289. package/src/lib/instance/__tests__/settings.test.ts +83 -0
  290. package/src/lib/instance/__tests__/upgrade-poller.test.ts +181 -0
  291. package/src/lib/instance/bootstrap.ts +270 -0
  292. package/src/lib/instance/detect.ts +49 -0
  293. package/src/lib/instance/fingerprint.ts +76 -0
  294. package/src/lib/instance/git-ops.ts +95 -0
  295. package/src/lib/instance/settings.ts +61 -0
  296. package/src/lib/instance/types.ts +77 -0
  297. package/src/lib/instance/upgrade-poller.ts +205 -0
  298. package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
  299. package/src/lib/notifications/visibility.ts +33 -0
  300. package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
  301. package/src/lib/schedules/__tests__/config.test.ts +62 -0
  302. package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
  303. package/src/lib/schedules/__tests__/integration.test.ts +82 -0
  304. package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
  305. package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
  306. package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
  307. package/src/lib/schedules/collision-check.ts +105 -0
  308. package/src/lib/schedules/config.ts +53 -0
  309. package/src/lib/schedules/scheduler.ts +236 -17
  310. package/src/lib/schedules/slot-claim.ts +105 -0
  311. package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
  312. package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
  313. package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
  314. package/src/lib/settings/openai-auth.ts +105 -10
  315. package/src/lib/settings/openai-login-manager.ts +260 -0
  316. package/src/lib/settings/runtime-setup.ts +14 -4
  317. package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
  318. package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
  319. package/src/lib/tables/enrichment-planner.ts +454 -0
  320. package/src/lib/tables/enrichment.ts +328 -0
  321. package/src/lib/tables/query-builder.ts +5 -2
  322. package/src/lib/tables/trigger-evaluator.ts +3 -2
  323. package/src/lib/theme.ts +71 -0
  324. package/src/lib/usage/ledger.ts +2 -18
  325. package/src/lib/util/__tests__/similarity.test.ts +106 -0
  326. package/src/lib/util/similarity.ts +77 -0
  327. package/src/lib/utils/format-timestamp.ts +24 -0
  328. package/src/lib/utils/stagent-paths.ts +12 -0
  329. package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
  330. package/src/lib/validators/__tests__/settings.test.ts +10 -0
  331. package/src/lib/validators/blueprint.ts +70 -9
  332. package/src/lib/validators/profile.ts +2 -2
  333. package/src/lib/validators/settings.ts +3 -1
  334. package/src/lib/workflows/__tests__/delay.test.ts +196 -0
  335. package/src/lib/workflows/__tests__/engine.test.ts +8 -0
  336. package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
  337. package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
  338. package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
  339. package/src/lib/workflows/blueprints/instantiator.ts +22 -1
  340. package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
  341. package/src/lib/workflows/blueprints/types.ts +16 -2
  342. package/src/lib/workflows/delay.ts +106 -0
  343. package/src/lib/workflows/engine.ts +212 -7
  344. package/src/lib/workflows/loop-executor.ts +349 -24
  345. package/src/lib/workflows/post-action.ts +91 -0
  346. package/src/lib/workflows/types.ts +166 -1
  347. package/src/test/setup.ts +10 -0
  348. package/src/app/api/license/checkout/route.ts +0 -28
  349. package/src/app/api/license/portal/route.ts +0 -26
  350. package/src/app/api/license/route.ts +0 -89
  351. package/src/app/api/license/usage/route.ts +0 -63
  352. package/src/app/api/marketplace/browse/route.ts +0 -15
  353. package/src/app/api/marketplace/import/route.ts +0 -28
  354. package/src/app/api/marketplace/publish/route.ts +0 -40
  355. package/src/app/api/onboarding/email/route.ts +0 -53
  356. package/src/app/api/settings/telemetry/route.ts +0 -14
  357. package/src/app/api/sync/export/route.ts +0 -54
  358. package/src/app/api/sync/restore/route.ts +0 -37
  359. package/src/app/api/sync/sessions/route.ts +0 -24
  360. package/src/app/auth/callback/route.ts +0 -73
  361. package/src/app/marketplace/page.tsx +0 -19
  362. package/src/components/analytics/analytics-gate-card.tsx +0 -101
  363. package/src/components/marketplace/blueprint-card.tsx +0 -61
  364. package/src/components/marketplace/marketplace-browser.tsx +0 -131
  365. package/src/components/onboarding/email-capture-card.tsx +0 -104
  366. package/src/components/settings/activation-form.tsx +0 -95
  367. package/src/components/settings/cloud-account-section.tsx +0 -147
  368. package/src/components/settings/cloud-sync-section.tsx +0 -155
  369. package/src/components/settings/subscription-section.tsx +0 -410
  370. package/src/components/settings/telemetry-section.tsx +0 -80
  371. package/src/components/shared/premium-gate-overlay.tsx +0 -50
  372. package/src/components/shared/schedule-gate-dialog.tsx +0 -64
  373. package/src/components/shared/upgrade-banner.tsx +0 -112
  374. package/src/hooks/use-supabase-auth.ts +0 -79
  375. package/src/lib/billing/email.ts +0 -54
  376. package/src/lib/billing/products.ts +0 -80
  377. package/src/lib/billing/stripe.ts +0 -101
  378. package/src/lib/cloud/supabase-browser.ts +0 -32
  379. package/src/lib/cloud/supabase-client.ts +0 -56
  380. package/src/lib/license/__tests__/features.test.ts +0 -56
  381. package/src/lib/license/__tests__/key-format.test.ts +0 -88
  382. package/src/lib/license/__tests__/manager.test.ts +0 -64
  383. package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
  384. package/src/lib/license/cloud-validation.ts +0 -60
  385. package/src/lib/license/features.ts +0 -44
  386. package/src/lib/license/key-format.ts +0 -101
  387. package/src/lib/license/limit-check.ts +0 -111
  388. package/src/lib/license/limit-queries.ts +0 -51
  389. package/src/lib/license/manager.ts +0 -345
  390. package/src/lib/license/notifications.ts +0 -59
  391. package/src/lib/license/tier-limits.ts +0 -71
  392. package/src/lib/marketplace/marketplace-client.ts +0 -107
  393. package/src/lib/sync/cloud-sync.ts +0 -235
  394. package/src/lib/telemetry/conversion-events.ts +0 -71
  395. package/src/lib/telemetry/queue.ts +0 -122
  396. package/src/lib/validators/license.ts +0 -33
@@ -0,0 +1,117 @@
1
+ import { db } from "@/lib/db";
2
+ import { chatMessages } from "@/lib/db/schema";
3
+ import { and, eq, lt } from "drizzle-orm";
4
+ import { recordTermination } from "./stream-telemetry";
5
+
6
+ const INTERRUPTED_FALLBACK =
7
+ "(Response interrupted. Please try again.)";
8
+
9
+ const ORPHAN_FALLBACK =
10
+ "(Interrupted — this response was not completed. Please retry.)";
11
+
12
+ /**
13
+ * Safety-net finalizer called from the chat engine's top-level `finally` block.
14
+ *
15
+ * Guarantees the invariant: no `chat_messages` row remains in
16
+ * `status='streaming'` after `sendMessage()` returns or throws. Catches every
17
+ * code path the engine's own `catch` block misses — most notably async
18
+ * iterator abandonment, where a consumer `break`ing out of a `for await` loop
19
+ * triggers the generator's `return()` method and jumps straight to `finally`,
20
+ * skipping `catch` entirely.
21
+ *
22
+ * No-op if the message is already in a terminal state. Idempotent.
23
+ *
24
+ * When the salvage path actually fires (row was still in streaming → now
25
+ * updated to complete/error), records a `stream.abandoned` telemetry event
26
+ * so maintainers can see that the engine's own happy/catch paths both
27
+ * missed the termination. A non-zero count here signals a real gap that
28
+ * may warrant investigation — e.g., the dev HMR interrupting a stream,
29
+ * or a consumer break pattern that bypasses the telemetry in both primary
30
+ * code paths.
31
+ */
32
+ export async function finalizeStreamingMessage(
33
+ messageId: string,
34
+ fullText: string,
35
+ ): Promise<void> {
36
+ const current = db
37
+ .select()
38
+ .from(chatMessages)
39
+ .where(eq(chatMessages.id, messageId))
40
+ .get();
41
+
42
+ if (!current || current.status !== "streaming") {
43
+ return;
44
+ }
45
+
46
+ const hasContent = fullText && fullText.trim().length > 0;
47
+ const salvage = hasContent ? fullText : INTERRUPTED_FALLBACK;
48
+ const nextStatus = hasContent && fullText.length > 50 ? "complete" : "error";
49
+
50
+ db.update(chatMessages)
51
+ .set({ status: nextStatus, content: salvage })
52
+ .where(eq(chatMessages.id, messageId))
53
+ .run();
54
+
55
+ // Telemetry: this code path means neither stream.completed nor the
56
+ // engine's catch-block recordTermination fired. Capture the gap so
57
+ // the diagnostics endpoint can surface it.
58
+ recordTermination({
59
+ reason: "stream.abandoned",
60
+ conversationId: current.conversationId ?? null,
61
+ messageId,
62
+ durationMs: current.createdAt
63
+ ? Date.now() - new Date(current.createdAt).getTime()
64
+ : null,
65
+ error: hasContent ? undefined : "no content streamed before abandonment",
66
+ });
67
+ }
68
+
69
+ /**
70
+ * Sweep orphaned chat assistant messages left in `status='streaming'` past a
71
+ * reasonable cutoff. These rows are produced when the chat engine's finally
72
+ * block is bypassed (process crash, iterator abandonment under heavy load,
73
+ * HTTP disconnect before update commits, etc.).
74
+ *
75
+ * Safe to call idempotently at chat page load. Uses a 10-minute cutoff — far
76
+ * longer than any legitimate in-flight streaming response — so in-flight rows
77
+ * are never clobbered.
78
+ *
79
+ * Returns the number of rows swept. Never throws.
80
+ */
81
+ export async function reconcileStreamingMessages(): Promise<number> {
82
+ const cutoff = new Date(Date.now() - 10 * 60 * 1000);
83
+ const orphans = db
84
+ .select()
85
+ .from(chatMessages)
86
+ .where(
87
+ and(
88
+ eq(chatMessages.status, "streaming"),
89
+ lt(chatMessages.createdAt, cutoff),
90
+ ),
91
+ )
92
+ .all();
93
+
94
+ for (const row of orphans) {
95
+ const salvage =
96
+ row.content && row.content.length > 0 ? row.content : ORPHAN_FALLBACK;
97
+ db.update(chatMessages)
98
+ .set({ status: "error", content: salvage })
99
+ .where(eq(chatMessages.id, row.id))
100
+ .run();
101
+
102
+ // Telemetry: record the orphan sweep so diagnostics can tell how often
103
+ // the safety net actually fires vs. how often the normal finalize path
104
+ // catches everything first. If this code ever logs a row, the engine's
105
+ // `finally` block missed it.
106
+ recordTermination({
107
+ reason: "stream.reconciled.stale",
108
+ conversationId: row.conversationId ?? null,
109
+ messageId: row.id,
110
+ durationMs: row.createdAt
111
+ ? Date.now() - new Date(row.createdAt).getTime()
112
+ : null,
113
+ });
114
+ }
115
+
116
+ return orphans.length;
117
+ }
@@ -0,0 +1,210 @@
1
+ /**
2
+ * Shared composition service — single source of truth for activate_skill /
3
+ * deactivate_skill logic, used by both the chat tool handler and the thin
4
+ * HTTP routes (POST /api/chat/conversations/[id]/skills/activate|deactivate).
5
+ *
6
+ * The chat tool delegates here so the UI can reach the same behaviour over
7
+ * HTTP without going through MCP.
8
+ *
9
+ * See `features/chat-composition-ui-v1.md`.
10
+ */
11
+
12
+ import { eq } from "drizzle-orm";
13
+ import { db } from "@/lib/db";
14
+ import { conversations } from "@/lib/db/schema";
15
+ import { mergeActiveSkillIds } from "@/lib/chat/active-skills";
16
+ import type { SkillConflict } from "@/lib/chat/skill-conflict";
17
+
18
+ export type ActivateSkillResult =
19
+ | {
20
+ kind: "ok";
21
+ activatedSkillId: string;
22
+ activeSkillIds: string[];
23
+ skillName: string;
24
+ note?: string;
25
+ }
26
+ | {
27
+ kind: "conflicts";
28
+ activeSkillIds: string[];
29
+ conflicts: SkillConflict[];
30
+ hint: string;
31
+ }
32
+ | { kind: "error"; message: string };
33
+
34
+ export type DeactivateSkillResult =
35
+ | { kind: "ok"; previousSkillId: string | null }
36
+ | { kind: "error"; message: string };
37
+
38
+ /**
39
+ * Activate a skill on a conversation, respecting runtime composition limits.
40
+ *
41
+ * @param conversationId Target conversation.
42
+ * @param skillId Opaque skill id from list_skills.
43
+ * @param mode "replace" (default) clears prior active skills; "add"
44
+ * appends — runtime must support composition.
45
+ * @param force When mode="add", skip conflict heuristic warnings.
46
+ */
47
+ export async function activateSkill(args: {
48
+ conversationId: string;
49
+ skillId: string;
50
+ mode?: "replace" | "add";
51
+ force?: boolean;
52
+ }): Promise<ActivateSkillResult> {
53
+ const { conversationId, skillId, mode = "replace", force = false } = args;
54
+
55
+ try {
56
+ const { getSkill } = await import("@/lib/environment/list-skills");
57
+ const skill = getSkill(skillId);
58
+ if (!skill) return { kind: "error", message: `Skill not found: ${skillId}` };
59
+
60
+ const existing = await db
61
+ .select({
62
+ id: conversations.id,
63
+ activeSkillId: conversations.activeSkillId,
64
+ activeSkillIds: conversations.activeSkillIds,
65
+ runtimeId: conversations.runtimeId,
66
+ })
67
+ .from(conversations)
68
+ .where(eq(conversations.id, conversationId))
69
+ .get();
70
+
71
+ if (!existing) {
72
+ return { kind: "error", message: `Conversation not found: ${conversationId}` };
73
+ }
74
+
75
+ if (mode === "add") {
76
+ const { getRuntimeFeatures } = await import("@/lib/agents/runtime/catalog");
77
+ let features;
78
+ try {
79
+ features = getRuntimeFeatures(
80
+ existing.runtimeId as Parameters<typeof getRuntimeFeatures>[0]
81
+ );
82
+ } catch {
83
+ return {
84
+ kind: "error",
85
+ message: `Unknown runtime '${existing.runtimeId ?? "(none)"}' — cannot determine composition support`,
86
+ };
87
+ }
88
+
89
+ if (!features.supportsSkillComposition) {
90
+ return {
91
+ kind: "error",
92
+ message: `Runtime '${existing.runtimeId}' does not support skill composition — switch to a Claude/Codex/direct runtime to compose skills`,
93
+ };
94
+ }
95
+
96
+ const currentIds = mergeActiveSkillIds(existing.activeSkillId, existing.activeSkillIds);
97
+
98
+ if (currentIds.includes(skillId)) {
99
+ return {
100
+ kind: "ok",
101
+ activatedSkillId: skillId,
102
+ activeSkillIds: currentIds,
103
+ skillName: skill.name,
104
+ note: "skill already active",
105
+ };
106
+ }
107
+
108
+ if (currentIds.length >= features.maxActiveSkills) {
109
+ return {
110
+ kind: "error",
111
+ message: `Max active skills (${features.maxActiveSkills}) reached on '${existing.runtimeId}' — deactivate one first`,
112
+ };
113
+ }
114
+
115
+ if (!force && currentIds.length > 0) {
116
+ const { detectSkillConflicts } = await import("@/lib/chat/skill-conflict");
117
+ const allConflicts: SkillConflict[] = [];
118
+ for (const otherId of currentIds) {
119
+ const other = getSkill(otherId);
120
+ if (!other) continue;
121
+ const conflicts = detectSkillConflicts(
122
+ { id: skill.id, name: skill.name, content: skill.content },
123
+ { id: other.id, name: other.name, content: other.content }
124
+ );
125
+ allConflicts.push(...conflicts);
126
+ }
127
+ if (allConflicts.length > 0) {
128
+ return {
129
+ kind: "conflicts",
130
+ activeSkillIds: currentIds,
131
+ conflicts: allConflicts,
132
+ hint: "Re-call with force=true to add anyway",
133
+ };
134
+ }
135
+ }
136
+
137
+ // Append: store ALL composed IDs in the new column. Keep legacy
138
+ // activeSkillId as-is so single-skill read paths still work.
139
+ const newComposed = [...(existing.activeSkillIds ?? []), skillId];
140
+ await db
141
+ .update(conversations)
142
+ .set({ activeSkillIds: newComposed, updatedAt: new Date() })
143
+ .where(eq(conversations.id, conversationId));
144
+
145
+ return {
146
+ kind: "ok",
147
+ activatedSkillId: skillId,
148
+ activeSkillIds: mergeActiveSkillIds(existing.activeSkillId, newComposed),
149
+ skillName: skill.name,
150
+ };
151
+ }
152
+
153
+ // mode === "replace" (legacy / default)
154
+ await db
155
+ .update(conversations)
156
+ .set({
157
+ activeSkillId: skillId,
158
+ activeSkillIds: [],
159
+ updatedAt: new Date(),
160
+ })
161
+ .where(eq(conversations.id, conversationId));
162
+
163
+ return {
164
+ kind: "ok",
165
+ activatedSkillId: skillId,
166
+ activeSkillIds: [skillId],
167
+ skillName: skill.name,
168
+ };
169
+ } catch (e) {
170
+ return {
171
+ kind: "error",
172
+ message: e instanceof Error ? e.message : "activate_skill failed",
173
+ };
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Clear the active skill (and composed skills) on a conversation.
179
+ */
180
+ export async function deactivateSkill(args: {
181
+ conversationId: string;
182
+ }): Promise<DeactivateSkillResult> {
183
+ const { conversationId } = args;
184
+ try {
185
+ const existing = await db
186
+ .select({
187
+ id: conversations.id,
188
+ activeSkillId: conversations.activeSkillId,
189
+ })
190
+ .from(conversations)
191
+ .where(eq(conversations.id, conversationId))
192
+ .get();
193
+
194
+ if (!existing) {
195
+ return { kind: "error", message: `Conversation not found: ${conversationId}` };
196
+ }
197
+
198
+ await db
199
+ .update(conversations)
200
+ .set({ activeSkillId: null, activeSkillIds: [], updatedAt: new Date() })
201
+ .where(eq(conversations.id, conversationId));
202
+
203
+ return { kind: "ok", previousSkillId: existing.activeSkillId ?? null };
204
+ } catch (e) {
205
+ return {
206
+ kind: "error",
207
+ message: e instanceof Error ? e.message : "deactivate_skill failed",
208
+ };
209
+ }
210
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Lightweight heuristic that flags when two SKILL.md bodies issue
3
+ * divergent directives on the same topic. Pure function — no I/O.
4
+ *
5
+ * Approach (v1):
6
+ * 1. For each skill, extract "directive lines" containing one of:
7
+ * always | never | must | prefer | use | avoid | don't | do not
8
+ * 2. Tokenize each directive into content words (lowercase, drop
9
+ * stopwords + the directive verb itself).
10
+ * 3. For each pair of directives across the two skills with significant
11
+ * keyword overlap (≥2 shared content words ≥4 chars), check if their
12
+ * directive verbs disagree (always vs never, prefer vs avoid, etc.).
13
+ * 4. Surface the disagreeing pair as a SkillConflict.
14
+ *
15
+ * False positives are acceptable — the consumer presents excerpts to the
16
+ * user, not a binary block. False negatives (semantic conflict without
17
+ * keyword overlap) await the embedding-based v2.
18
+ */
19
+
20
+ export interface SkillMarkdown {
21
+ id: string;
22
+ name: string;
23
+ content: string;
24
+ }
25
+
26
+ export interface SkillConflict {
27
+ skillA: string; // skill name
28
+ skillB: string; // skill name
29
+ sharedTopic: string; // joined keywords that overlapped
30
+ excerptA: string; // the directive line from A
31
+ excerptB: string; // the directive line from B
32
+ }
33
+
34
+ const POSITIVE_DIRECTIVES = new Set(["always", "must", "prefer", "use", "do"]);
35
+ const NEGATIVE_DIRECTIVES = new Set(["never", "avoid", "don't", "dont", "skip"]);
36
+ const ALL_DIRECTIVES = new Set([...POSITIVE_DIRECTIVES, ...NEGATIVE_DIRECTIVES]);
37
+
38
+ const STOPWORDS = new Set([
39
+ "the", "a", "an", "and", "or", "but", "for", "with", "without",
40
+ "to", "of", "in", "on", "at", "by", "from", "as", "is", "are",
41
+ "be", "this", "that", "these", "those", "it", "its", "before",
42
+ "after", "during", "into", "out",
43
+ ]);
44
+
45
+ interface DirectiveLine {
46
+ raw: string;
47
+ polarity: "positive" | "negative";
48
+ keywords: Set<string>;
49
+ }
50
+
51
+ function extractDirectives(content: string): DirectiveLine[] {
52
+ const lines = content.split(/\r?\n/);
53
+ const out: DirectiveLine[] = [];
54
+ for (const rawLine of lines) {
55
+ const line = rawLine.trim();
56
+ if (!line || line.startsWith("#") || line.startsWith("```")) continue;
57
+ const lower = line.toLowerCase();
58
+ let polarity: "positive" | "negative" | null = null;
59
+ for (const tok of lower.split(/\s+/)) {
60
+ const cleaned = tok.replace(/[^a-z']/g, "");
61
+ if (POSITIVE_DIRECTIVES.has(cleaned)) { polarity = "positive"; break; }
62
+ if (NEGATIVE_DIRECTIVES.has(cleaned)) { polarity = "negative"; break; }
63
+ }
64
+ if (!polarity) continue;
65
+ const keywords = new Set<string>();
66
+ for (const tok of lower.split(/[^a-z0-9]+/)) {
67
+ if (tok.length < 4) continue;
68
+ if (STOPWORDS.has(tok) || ALL_DIRECTIVES.has(tok)) continue;
69
+ keywords.add(tok);
70
+ }
71
+ if (keywords.size === 0) continue;
72
+ out.push({ raw: line, polarity, keywords });
73
+ }
74
+ return out;
75
+ }
76
+
77
+ function intersect(a: Set<string>, b: Set<string>): string[] {
78
+ const out: string[] = [];
79
+ for (const tok of a) if (b.has(tok)) out.push(tok);
80
+ return out;
81
+ }
82
+
83
+ export function detectSkillConflicts(
84
+ a: SkillMarkdown,
85
+ b: SkillMarkdown
86
+ ): SkillConflict[] {
87
+ const directivesA = extractDirectives(a.content);
88
+ const directivesB = extractDirectives(b.content);
89
+ const conflicts: SkillConflict[] = [];
90
+ for (const da of directivesA) {
91
+ for (const db of directivesB) {
92
+ if (da.polarity === db.polarity) continue;
93
+ const shared = intersect(da.keywords, db.keywords);
94
+ if (shared.length < 2) continue;
95
+ conflicts.push({
96
+ skillA: a.name,
97
+ skillB: b.name,
98
+ sharedTopic: shared.slice(0, 3).join(", "),
99
+ excerptA: da.raw,
100
+ excerptB: db.raw,
101
+ });
102
+ }
103
+ }
104
+ return conflicts;
105
+ }
@@ -21,6 +21,9 @@ import { chatHistoryTools } from "./tools/chat-history-tools";
21
21
  import { handoffTools } from "./tools/handoff-tools";
22
22
  import { tableTools } from "./tools/table-tools";
23
23
  import { runtimeTools } from "./tools/runtime-tools";
24
+ import { blueprintTools } from "./tools/blueprint-tools";
25
+ import { skillTools } from "./tools/skill-tools";
26
+
24
27
 
25
28
  // ── Tool server types ────────────────────────────────────────────────
26
29
 
@@ -56,6 +59,8 @@ function collectAllTools(ctx: ToolContext): ToolDefinition[] {
56
59
  ...handoffTools(ctx),
57
60
  ...tableTools(ctx),
58
61
  ...runtimeTools(ctx),
62
+ ...blueprintTools(ctx),
63
+ ...skillTools(ctx),
59
64
  ];
60
65
  }
61
66
 
@@ -70,8 +75,9 @@ function collectAllTools(ctx: ToolContext): ToolDefinition[] {
70
75
  export function createToolServer(
71
76
  projectId?: string | null,
72
77
  onToolResult?: (toolName: string, result: unknown) => void,
78
+ projectDir?: string | null,
73
79
  ): ToolServer {
74
- const ctx: ToolContext = { projectId, onToolResult };
80
+ const ctx: ToolContext = { projectId, projectDir, onToolResult };
75
81
  const allTools = collectAllTools(ctx);
76
82
 
77
83
  // Handler lookup map (built once, shared across modes)
@@ -113,21 +119,3 @@ export function createToolServer(
113
119
  };
114
120
  }
115
121
 
116
- // ── Backward-compatible export ───────────────────────────────────────
117
-
118
- /**
119
- * Create an in-process MCP server exposing all Stagent tools.
120
- * The `projectId` closure auto-scopes operations to the active project.
121
- * `onToolResult` is called after each successful CRUD operation with the
122
- * tool name and returned entity data — used by the entity detector to
123
- * generate deterministic Quick Access navigation links.
124
- *
125
- * @deprecated Use `createToolServer()` for new code. This wrapper exists
126
- * for backward compatibility with the chat engine.
127
- */
128
- export function createStagentMcpServer(
129
- projectId?: string | null,
130
- onToolResult?: (toolName: string, result: unknown) => void,
131
- ) {
132
- return createToolServer(projectId, onToolResult).asMcpServer();
133
- }
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Chat stream termination telemetry.
3
+ *
4
+ * Lightweight, in-memory ring buffer that records how SSE chat streams
5
+ * terminate. Added in response to a sibling-repo bug report claiming
6
+ * conversations refresh mid-stream — the proposed root cause (Next.js dev
7
+ * HMR remounting ChatShell) is already mitigated in this repo, so rather
8
+ * than port a speculative fix, we instrument the termination boundaries
9
+ * and let real data decide whether a resume protocol is worth building.
10
+ *
11
+ * Six server-side reason codes:
12
+ * - stream.completed — normal end-of-generator (success path)
13
+ * - stream.aborted.signal — req.signal fired, engine catch block entered
14
+ * - stream.aborted.client — ReadableStream cancel callback fired
15
+ * - stream.finalized.error — non-abort exception in engine catch block
16
+ * - stream.abandoned — generator return() called by consumer
17
+ * (finally ran but catch was skipped). Covers
18
+ * iterator abandonment — the case where the
19
+ * route's for-await breaks out gracefully and
20
+ * the engine's own happy/catch paths are both
21
+ * bypassed. Recorded from finalizeStreamingMessage
22
+ * when it actually performs a salvage update.
23
+ * - stream.reconciled.stale — reconcileStreamingMessages swept an orphan
24
+ * at chat page load (10-min cutoff)
25
+ *
26
+ * Four client-side reason codes (logged via console.info with a stable
27
+ * prefix so tests and grep can find them):
28
+ * - client.stream.done — reader.read() returned done: true
29
+ * - client.stream.user-abort — user clicked Stop / AbortController fired
30
+ * - client.stream.reader-error — reader.read() or decode threw
31
+ * - client.stream.view-remount — a chat-consuming component unmounted
32
+ * while a stream was in flight. The stream
33
+ * itself continues in the provider; this
34
+ * code exists so diagnostics can confirm
35
+ * the provider-hoisting fix is holding.
36
+ *
37
+ * As of the `chat-session-persistence-provider` feature, the SSE reader
38
+ * loop runs inside `ChatSessionProvider` (rendered from the root layout),
39
+ * not inside the route-scoped `ChatShell`. Sidebar navigation no longer
40
+ * tears down the reader loop, so "client.stream.user-abort" should only
41
+ * fire when the user explicitly clicks Stop. If it starts firing on plain
42
+ * view switches again, something has regressed the provider hoisting.
43
+ * HMR in dev can still reset the provider module — that is expected.
44
+ *
45
+ * Read via the dev-only `GET /api/diagnostics/chat-streams` endpoint.
46
+ * The buffer is process-local — a server restart clears it, which is fine
47
+ * for dev diagnostics and avoids adding a persistence layer that would
48
+ * itself need testing.
49
+ */
50
+
51
+ export type TerminationReason =
52
+ | "stream.completed"
53
+ | "stream.aborted.signal"
54
+ | "stream.aborted.client"
55
+ | "stream.finalized.error"
56
+ | "stream.abandoned"
57
+ | "stream.reconciled.stale";
58
+
59
+ export interface TerminationEvent {
60
+ reason: TerminationReason;
61
+ conversationId: string | null;
62
+ messageId: string | null;
63
+ durationMs: number | null;
64
+ error?: string;
65
+ timestamp: number;
66
+ }
67
+
68
+ /** Ring buffer capacity — ~500 events is ~50KB, negligible for a dev tool. */
69
+ const CAPACITY = 500;
70
+
71
+ /**
72
+ * Module-level circular buffer. Newer events overwrite older ones once
73
+ * capacity is reached. Writes are O(1), reads copy-out in order.
74
+ *
75
+ * Next.js dev HMR may re-import this module and reset the buffer — that
76
+ * is expected behavior and not a bug. The buffer is intentionally not
77
+ * persisted; its purpose is "what happened in the last N minutes of this
78
+ * process", not forensic logging.
79
+ */
80
+ const buffer: TerminationEvent[] = new Array(CAPACITY);
81
+ let writeIndex = 0;
82
+ let writeCount = 0;
83
+
84
+ export function recordTermination(event: Omit<TerminationEvent, "timestamp">): void {
85
+ const full: TerminationEvent = { ...event, timestamp: Date.now() };
86
+ buffer[writeIndex] = full;
87
+ writeIndex = (writeIndex + 1) % CAPACITY;
88
+ writeCount++;
89
+ }
90
+
91
+ /**
92
+ * Return all recorded events in chronological order (oldest → newest).
93
+ * Copies out of the ring buffer so callers can't mutate internal state.
94
+ */
95
+ export function readTerminations(): TerminationEvent[] {
96
+ const count = Math.min(writeCount, CAPACITY);
97
+ if (count === 0) return [];
98
+ const result: TerminationEvent[] = new Array(count);
99
+ // Start at the oldest slot. When the buffer is full, the oldest is at
100
+ // writeIndex (the next slot to be overwritten). When not full, it's at 0.
101
+ const start = writeCount > CAPACITY ? writeIndex : 0;
102
+ for (let i = 0; i < count; i++) {
103
+ result[i] = buffer[(start + i) % CAPACITY]!;
104
+ }
105
+ return result;
106
+ }
107
+
108
+ /**
109
+ * Aggregate event counts by reason code for the last `windowMs` milliseconds.
110
+ * Pass 0 or omit to get counts across the entire buffer.
111
+ */
112
+ export function countTerminations(windowMs = 0): Record<TerminationReason, number> {
113
+ const counts: Record<TerminationReason, number> = {
114
+ "stream.completed": 0,
115
+ "stream.aborted.signal": 0,
116
+ "stream.aborted.client": 0,
117
+ "stream.finalized.error": 0,
118
+ "stream.abandoned": 0,
119
+ "stream.reconciled.stale": 0,
120
+ };
121
+ const cutoff = windowMs > 0 ? Date.now() - windowMs : 0;
122
+ for (const event of readTerminations()) {
123
+ if (event.timestamp >= cutoff) {
124
+ counts[event.reason]++;
125
+ }
126
+ }
127
+ return counts;
128
+ }
129
+
130
+ /**
131
+ * Reset the buffer. Intended for tests — do not call in production code.
132
+ */
133
+ export function __resetForTesting(): void {
134
+ for (let i = 0; i < CAPACITY; i++) buffer[i] = undefined as never;
135
+ writeIndex = 0;
136
+ writeCount = 0;
137
+ }
@@ -1,5 +1,5 @@
1
1
  import { db } from "@/lib/db";
2
- import { projects, tasks, workflows, schedules } from "@/lib/db/schema";
2
+ import { projects, tasks, schedules, userTables } from "@/lib/db/schema";
3
3
  import { eq, desc } from "drizzle-orm";
4
4
  import type { PromptCategory, SuggestedPrompt } from "./types";
5
5
 
@@ -40,6 +40,25 @@ async function buildExplorePrompts(): Promise<SuggestedPrompt[]> {
40
40
  });
41
41
  }
42
42
 
43
+ // Context-sensitive suggestion: if any user tables exist, surface an
44
+ // enrichment prompt for the most recently updated one. Users commonly have
45
+ // tables with empty cells waiting to be enriched; even without a column-level
46
+ // scan, pointing the chat LLM at enrich_table makes the bulk-row fan-out
47
+ // capability discoverable via suggested prompts rather than only via direct
48
+ // intent.
49
+ const recentTable = await db
50
+ .select({ id: userTables.id, name: userTables.name })
51
+ .from(userTables)
52
+ .orderBy(desc(userTables.updatedAt))
53
+ .limit(1);
54
+
55
+ if (recentTable.length > 0) {
56
+ prompts.push({
57
+ label: `Enrich "${truncate(recentTable[0].name, 28)}" rows`,
58
+ prompt: `I'd like to enrich rows in the "${recentTable[0].name}" table (id: ${recentTable[0].id}) using an agent. Ask me which column is missing data, what prompt template to use (reference row fields naturally), and which agent profile is best. Then use enrich_table to kick off the loop workflow.`,
59
+ });
60
+ }
61
+
43
62
  // Fill with static fallbacks
44
63
  const fallbacks: SuggestedPrompt[] = [
45
64
  {
@@ -76,6 +95,14 @@ function buildCreatePrompts(): SuggestedPrompt[] {
76
95
  label: "Set up a multi-step workflow",
77
96
  prompt: "Help me design a multi-step workflow. I want to define a sequence of tasks with dependencies. Ask me what the workflow should accomplish and suggest a structure.",
78
97
  },
98
+ {
99
+ label: "Design a drip sequence",
100
+ prompt: "Help me build a drip workflow with delay steps between sends. Ask me about the cadence (e.g. 3 days between touches), the number of touches, and the content goal for each step. Then use create_workflow with a sequence pattern, interleaving task steps and delay steps (delayDuration format: Nm|Nh|Nd|Nw, bounds 1m..30d). Do not create separate workflows or schedules — a single workflow with inline delay steps is the idiomatic pattern.",
101
+ },
102
+ {
103
+ label: "Enrich a table with an agent",
104
+ prompt: "I have a table with missing data that I want an agent to fill in. Help me use enrich_table to fan out rows to the agent. Ask me which table, which column is missing, what prompt template to use (the row is available as JSON context — tell the agent to read the relevant fields and return just the value, or NOT_FOUND if none can be determined), and which agent profile is best (sales-researcher, content-creator, data-analyst, etc.). Do not hand-roll a loop workflow — enrich_table already handles the loop, row binding, postAction writeback, and idempotent skip.",
105
+ },
79
106
  {
80
107
  label: "Draft a document outline",
81
108
  prompt: "Help me create a structured document outline. Ask me about the topic, audience, and purpose, then suggest sections and key points to cover.",