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,399 @@
1
+ # Chat Session Persistence Provider — Closeout Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Close out the `chat-session-persistence-provider` feature by filling the remaining AC gaps (telemetry code + doc comment + smoke test) and flipping the spec status from `planned` → `completed`.
6
+
7
+ **Architecture:** The provider, layout wiring, `ChatShell` refactor, and unit tests are already shipped. What remains is the `client.stream.view-remount` telemetry code described in spec §5 (a documented reason code plus a useEffect cleanup emitter), followed by a real browser smoke test per the spec's manual repro steps, and finally status + changelog updates.
8
+
9
+ **Tech Stack:** Next.js 16 App Router, React 19 client context, Vitest + @testing-library/react, SSE readers via `fetch().body.getReader()`.
10
+
11
+ ---
12
+
13
+ ## NOT in scope
14
+
15
+ - **SSE resume protocol (`lastEventId` replay).** Spec "Scope Boundaries" explicitly defers this; the provider preserves state across view switches but not across full page reloads. Unchanged.
16
+ - **Web Worker isolation for the SSE reader.** Still deferred per spec.
17
+ - **Multi-tab BroadcastChannel sync.** Out of scope per spec.
18
+ - **Server-side engine / reconcile / route-handler changes.** The provider fix is purely client-architecture; server code stays untouched.
19
+ - **Provider or ChatShell rewrite.** Both are already correct. This plan only *augments* them with the telemetry hook.
20
+ - **New TDR.** Spec notes a TDR is only warranted if the layout-provider pattern gets reused (e.g., workflow execution state). It hasn't been, so no TDR.
21
+
22
+ ## What already exists
23
+
24
+ | Artifact | Location | State |
25
+ |---|---|---|
26
+ | `ChatSessionProvider` with full action surface | `src/components/chat/chat-session-provider.tsx` (720 LOC) | Shipped. Holds `conversations`, `activeId`, `messagesByConversation`, `streamingState` (with `AbortController`), `modelId`, `availableModels`, `hydrated`. |
27
+ | Provider mounted in root layout | `src/app/layout.tsx:101,114` wraps `<main>` | Shipped. |
28
+ | `ChatShell` refactored to thin consumer | `src/components/chat/chat-shell.tsx` | Shipped. Zero chat-domain `useState`; only `mobileListOpen` + `hoverPreview` remain (both view-local). |
29
+ | `setMessages([])` catch-all removed | `chat-session-provider.tsx:198` | Shipped. Only appears in comments documenting the old bug. |
30
+ | Provider unit tests (4/4 green) | `src/components/chat/__tests__/chat-session-provider.test.tsx` (408 LOC) | Shipped. Covers unmount/remount preservation, fetch-failure tolerance, SSE delta accumulation, abort. |
31
+ | Dev diagnostics endpoint | `src/app/api/diagnostics/chat-streams/route.ts` | Shipped. Reads the ring buffer from `stream-telemetry.ts`. |
32
+ | 3 client reason codes documented | `src/lib/chat/stream-telemetry.ts:28-30` | Shipped. `client.stream.done`, `client.stream.user-abort`, `client.stream.reader-error`. |
33
+
34
+ The **only** missing code artifact is the 4th client reason code `client.stream.view-remount` described in spec §5, plus its emission site.
35
+
36
+ ## Error & Rescue Registry
37
+
38
+ | Failure mode | Detection | Recovery |
39
+ |---|---|---|
40
+ | `ChatShell` unmounts mid-stream but provider does not persist state (regression of the provider hoisting). | Browser smoke test shows the assistant message clears on nav-away. | Check that `<ChatSessionProvider>` is in `layout.tsx`, not inside `/chat` route. |
41
+ | Telemetry log prefix drifts from `[chat-stream]`. | Unit test assertion on `console.info` prefix fails. | Keep the literal `[chat-stream]` prefix — it's the grep contract used by the diagnostics endpoint / log scrapers. |
42
+ | View-remount code fires on **initial** mount (false positive). | Unit test fails: cleanup should only fire if `isStreaming` was true at cleanup time. | Read `isStreaming` via ref (not closure) inside cleanup so we capture the value at unmount, not at effect setup. |
43
+
44
+ ---
45
+
46
+ ## Task 1: Document the 4th client reason code
47
+
48
+ **Files:**
49
+ - Modify: `src/lib/chat/stream-telemetry.ts:26-31`
50
+
51
+ - [ ] **Step 1: Extend the docblock**
52
+
53
+ Edit the top docblock so the "Three client-side reason codes" section becomes "Four client-side reason codes" and adds the new bullet. The list today (lines 26-30):
54
+
55
+ ```typescript
56
+ * Three client-side reason codes (logged via console.info with a stable
57
+ * prefix so tests and grep can find them):
58
+ * - client.stream.done — reader.read() returned done: true
59
+ * - client.stream.user-abort — user clicked Stop / AbortController fired
60
+ * - client.stream.reader-error — reader.read() or decode threw
61
+ ```
62
+
63
+ Replace with:
64
+
65
+ ```typescript
66
+ * Four client-side reason codes (logged via console.info with a stable
67
+ * prefix so tests and grep can find them):
68
+ * - client.stream.done — reader.read() returned done: true
69
+ * - client.stream.user-abort — user clicked Stop / AbortController fired
70
+ * - client.stream.reader-error — reader.read() or decode threw
71
+ * - client.stream.view-remount — a chat-consuming component unmounted
72
+ * while a stream was in flight. The stream
73
+ * itself continues in the provider; this
74
+ * code exists so diagnostics can confirm
75
+ * the provider-hoisting fix is holding.
76
+ ```
77
+
78
+ - [ ] **Step 2: Verify no other grep hits need updating**
79
+
80
+ Run: `rg "Three client-side reason codes" src`
81
+ Expected: no matches (the string was only in this file).
82
+
83
+ - [ ] **Step 3: Commit**
84
+
85
+ ```bash
86
+ git add src/lib/chat/stream-telemetry.ts
87
+ git commit -m "docs(chat): document client.stream.view-remount reason code"
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Task 2: Write the failing test for the cleanup emitter
93
+
94
+ **Files:**
95
+ - Modify: `src/components/chat/__tests__/chat-session-provider.test.tsx` (add one new test block)
96
+
97
+ - [ ] **Step 1: Add the test**
98
+
99
+ Append to the existing test file, inside the `describe("ChatSessionProvider", ...)` block:
100
+
101
+ ```typescript
102
+ it("emits client.stream.view-remount when a consumer unmounts while streaming", async () => {
103
+ // Arrange: a consumer component that reads isStreaming from the provider
104
+ // and, on unmount, logs the view-remount telemetry if a stream was active.
105
+ const consoleInfoSpy = vi.spyOn(console, "info").mockImplementation(() => {});
106
+
107
+ function StreamingConsumer() {
108
+ const { isStreaming, sendMessage } = useChatSession();
109
+ const isStreamingRef = useRef(isStreaming);
110
+ useEffect(() => {
111
+ isStreamingRef.current = isStreaming;
112
+ }, [isStreaming]);
113
+ useEffect(() => {
114
+ return () => {
115
+ if (isStreamingRef.current) {
116
+ // eslint-disable-next-line no-console
117
+ console.info("[chat-stream] client.stream.view-remount", {
118
+ conversationId: null,
119
+ });
120
+ }
121
+ };
122
+ }, []);
123
+ return (
124
+ <button onClick={() => void sendMessage("hi")}>send</button>
125
+ );
126
+ }
127
+
128
+ // Use a never-resolving SSE body so isStreaming stays true until unmount.
129
+ const neverResolve = new Promise<Response>(() => {});
130
+ global.fetch = vi.fn((url: string) => {
131
+ if (url.startsWith("/api/chat/conversations") && !url.includes("messages")) {
132
+ return Promise.resolve(new Response(JSON.stringify({ id: "conv-vm" }), { status: 200 }));
133
+ }
134
+ if (url.includes("/stream")) return neverResolve;
135
+ return Promise.resolve(new Response("[]", { status: 200 }));
136
+ }) as typeof fetch;
137
+
138
+ const { unmount, getByText } = render(
139
+ <ChatSessionProvider>
140
+ <StreamingConsumer />
141
+ </ChatSessionProvider>
142
+ );
143
+
144
+ fireEvent.click(getByText("send"));
145
+ // Let sendMessage start the stream (isStreaming flips true)
146
+ await waitFor(() => {
147
+ // Consumer cleanup hasn't fired yet; we just need the streaming flag set.
148
+ });
149
+
150
+ unmount();
151
+
152
+ expect(consoleInfoSpy).toHaveBeenCalledWith(
153
+ "[chat-stream] client.stream.view-remount",
154
+ expect.objectContaining({ conversationId: expect.anything() })
155
+ );
156
+
157
+ consoleInfoSpy.mockRestore();
158
+ });
159
+ ```
160
+
161
+ Import additions at the top of the test file (only if missing):
162
+
163
+ ```typescript
164
+ import { useEffect, useRef } from "react";
165
+ import { fireEvent, render, waitFor } from "@testing-library/react";
166
+ ```
167
+
168
+ - [ ] **Step 2: Run the test to verify it passes (self-contained)**
169
+
170
+ Run: `npx vitest run src/components/chat/__tests__/chat-session-provider.test.tsx -t view-remount`
171
+
172
+ Expected: the test passes, because the `StreamingConsumer` component defined inside the test itself emits the log. This test is the **contract template** — Task 3 moves the emitter into `ChatShell` so real consumers honor the contract.
173
+
174
+ - [ ] **Step 3: Commit**
175
+
176
+ ```bash
177
+ git add src/components/chat/__tests__/chat-session-provider.test.tsx
178
+ git commit -m "test(chat): add view-remount telemetry contract test"
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Task 3: Emit `client.stream.view-remount` from `ChatShell`
184
+
185
+ **Files:**
186
+ - Modify: `src/components/chat/chat-shell.tsx` (add one useEffect + ref at the top of the component)
187
+
188
+ - [ ] **Step 1: Add the ref + cleanup effect**
189
+
190
+ Open `src/components/chat/chat-shell.tsx`. Directly after the `const session = useChatSession(); const { ... } = session;` destructure (around line 54), insert:
191
+
192
+ ```typescript
193
+ // Track streaming state in a ref so the unmount cleanup sees the latest
194
+ // value, not the value at effect-setup time. If ChatShell unmounts while
195
+ // a stream is in flight (user navigated away), log a telemetry breadcrumb.
196
+ // The stream itself continues inside ChatSessionProvider — this log only
197
+ // exists to confirm the provider-hoisting fix is holding. See
198
+ // `src/lib/chat/stream-telemetry.ts` for the full reason code list.
199
+ const isStreamingRef = useRef(isStreaming);
200
+ useEffect(() => {
201
+ isStreamingRef.current = isStreaming;
202
+ }, [isStreaming]);
203
+ useEffect(() => {
204
+ return () => {
205
+ if (isStreamingRef.current) {
206
+ console.info("[chat-stream] client.stream.view-remount", {
207
+ conversationId: activeId,
208
+ });
209
+ }
210
+ };
211
+ // Intentionally empty deps: we want this exactly-once cleanup on unmount.
212
+ // eslint-disable-next-line react-hooks/exhaustive-deps
213
+ }, []);
214
+ ```
215
+
216
+ Add `useRef` to the React import at the top of the file (line 3). Change:
217
+
218
+ ```typescript
219
+ import { useState, useCallback, useEffect, useMemo } from "react";
220
+ ```
221
+
222
+ to:
223
+
224
+ ```typescript
225
+ import { useState, useCallback, useEffect, useMemo, useRef } from "react";
226
+ ```
227
+
228
+ - [ ] **Step 2: Confirm TypeScript is clean**
229
+
230
+ Run: `npx tsc --noEmit`
231
+
232
+ Expected: no errors.
233
+
234
+ - [ ] **Step 3: Run the full provider test file to confirm no regressions**
235
+
236
+ Run: `npx vitest run src/components/chat/__tests__/chat-session-provider.test.tsx`
237
+
238
+ Expected: 5 tests pass (the original 4 plus the new view-remount contract test from Task 2).
239
+
240
+ - [ ] **Step 4: Commit**
241
+
242
+ ```bash
243
+ git add src/components/chat/chat-shell.tsx
244
+ git commit -m "feat(chat): emit client.stream.view-remount on ChatShell unmount"
245
+ ```
246
+
247
+ ---
248
+
249
+ ## Task 4: Manual browser smoke test
250
+
251
+ This verifies the fix against the original bug report, not just the logic. Spec AC requires it explicitly: *"Manual repro: start a 5-10s streaming response, click Dashboard, wait 10s, return to /chat. Assistant message is complete or still streaming live. Prior user turn and assistant content intact."*
252
+
253
+ **No code changes in this task — pure verification.**
254
+
255
+ - [ ] **Step 1: Start a clean dev server**
256
+
257
+ Per `MEMORY.md` "Clean Next.js restart procedure":
258
+
259
+ ```bash
260
+ pkill -f "next dev --turbopack$"; pkill -f "next-server"; sleep 2
261
+ npm run dev
262
+ ```
263
+
264
+ Wait until the console shows `Ready in …`.
265
+
266
+ - [ ] **Step 2: Open `http://localhost:3000/chat` in the browser**
267
+
268
+ Use Claude in Chrome (first choice, per MEMORY.md) or Chrome DevTools MCP. Retry once on failure before falling back to Playwright.
269
+
270
+ - [ ] **Step 3: Trigger a 5–10s streaming response on Claude runtime**
271
+
272
+ Select Claude model. Send a prompt that reliably takes 5–10s, e.g.:
273
+
274
+ ```
275
+ Explain in 3 short paragraphs how SSE backpressure works.
276
+ ```
277
+
278
+ - [ ] **Step 4: Mid-stream, navigate away and back**
279
+
280
+ While the assistant message is still streaming:
281
+ 1. Click "Dashboard" in the sidebar.
282
+ 2. Wait 10 seconds.
283
+ 3. Click "Chat" to return.
284
+
285
+ Expected:
286
+ - Assistant message either completed or still streaming live
287
+ - Prior user turn intact
288
+ - Prior assistant content intact (no blank)
289
+
290
+ - [ ] **Step 5: Repeat 5× rapidly**
291
+
292
+ Click sidebar items in quick succession (Dashboard → Projects → Workflows → Chat) while a stream is in flight. Do this five times.
293
+
294
+ Expected: zero turn loss, zero blank conversations.
295
+
296
+ - [ ] **Step 6: Repeat steps 3–5 on the GPT (Codex) runtime**
297
+
298
+ Switch model to a GPT option. Repeat the test sequence. Expected: same zero-loss behavior.
299
+
300
+ - [ ] **Step 7: Verify the diagnostics endpoint**
301
+
302
+ Open `http://localhost:3000/api/diagnostics/chat-streams` in a new tab.
303
+
304
+ Expected:
305
+ - `stream.abandoned` count is zero for the test window.
306
+ - `client.stream.view-remount` log lines appear in the dev-server console for each nav-away that happened during streaming.
307
+
308
+ - [ ] **Step 8: Record results in the feature spec**
309
+
310
+ Append a "Verification run — 2026-04-14" section to `features/chat-session-persistence-provider.md` with:
311
+ - Runtimes tested (Claude + GPT)
312
+ - Number of nav-away cycles
313
+ - Observed `stream.abandoned` count (expected 0)
314
+ - Observed `client.stream.view-remount` occurrences (expected >0 — proves the telemetry hook works)
315
+ - Any anomaly
316
+
317
+ Commit it:
318
+
319
+ ```bash
320
+ git add features/chat-session-persistence-provider.md
321
+ git commit -m "docs(features): record chat-session-persistence-provider smoke run"
322
+ ```
323
+
324
+ ---
325
+
326
+ ## Task 5: Close out spec status and changelog
327
+
328
+ **Files:**
329
+ - Modify: `features/chat-session-persistence-provider.md` (frontmatter `status:`)
330
+ - Modify: `features/changelog.md` (add entry)
331
+
332
+ - [ ] **Step 1: Flip spec status**
333
+
334
+ Change the frontmatter in `features/chat-session-persistence-provider.md`:
335
+
336
+ ```yaml
337
+ status: planned
338
+ ```
339
+
340
+ to:
341
+
342
+ ```yaml
343
+ status: completed
344
+ ```
345
+
346
+ - [ ] **Step 2: Add a changelog entry**
347
+
348
+ Append to `features/changelog.md` under the latest date section (create a new `## 2026-04-14` heading if needed):
349
+
350
+ ```markdown
351
+ - **chat-session-persistence-provider** — Closed out. Provider + layout + ChatShell refactor already shipped earlier; this pass adds the `client.stream.view-remount` telemetry reason code and emitter to satisfy AC §5, plus a browser smoke-test verification run. No server-side changes. Spec flipped to `completed`.
352
+ ```
353
+
354
+ - [ ] **Step 3: Final verification**
355
+
356
+ Run:
357
+
358
+ ```bash
359
+ npm test -- src/components/chat
360
+ npx tsc --noEmit
361
+ ```
362
+
363
+ Expected: all tests pass, zero TS errors.
364
+
365
+ - [ ] **Step 4: Commit**
366
+
367
+ ```bash
368
+ git add features/chat-session-persistence-provider.md features/changelog.md
369
+ git commit -m "docs(features): mark chat-session-persistence-provider complete"
370
+ ```
371
+
372
+ ---
373
+
374
+ ## Verification summary
375
+
376
+ After all 5 tasks:
377
+
378
+ | Acceptance criterion from spec | Verified by |
379
+ |---|---|
380
+ | `chat-session-provider.tsx` exists with action surface | Pre-existing; confirmed in Task 1 scope check |
381
+ | `layout.tsx` wraps `<main>` with `<ChatSessionProvider>` | Pre-existing; lines 101/114 |
382
+ | `ChatShell` holds zero chat-domain `useState` | Pre-existing; only view-local state remains |
383
+ | No `setMessages([])` catch-all | Pre-existing; only in comments |
384
+ | **Manual repro (view-switch, 5× rapid, both runtimes)** | **Task 4** |
385
+ | **`/api/diagnostics/chat-streams` shows zero `stream.abandoned`** | **Task 4 step 7** |
386
+ | Stop button aborts via AbortController | Pre-existing provider test |
387
+ | Unit tests in provider test file | Pre-existing 4 + new 1 = 5 |
388
+ | **`client.stream.view-remount` reason code added** | **Task 1 + Task 3** |
389
+ | `npm test` passes, `npx tsc --noEmit` clean | **Task 5 step 3** |
390
+
391
+ ## Self-review
392
+
393
+ **Spec coverage:** every AC bullet maps to a pre-existing artifact or a task above.
394
+
395
+ **Placeholder scan:** no TBDs, TODOs, "add appropriate error handling" phrases, or "similar to Task N" shortcuts. Each task contains complete code.
396
+
397
+ **Type consistency:** `isStreamingRef`, `useChatSession()`, and telemetry log prefix `[chat-stream]` are used identically across Tasks 2 and 3.
398
+
399
+ **Smoke-test budget:** this plan does **not** touch any module under `src/lib/agents/runtime/`, `src/lib/workflows/engine.ts`, or anything that statically imports `@/lib/chat/stagent-tools`. The project override's mandatory smoke task is not triggered. Task 4's smoke step is driven by the spec's own AC, not the runtime-registry gate.
@@ -0,0 +1,201 @@
1
+ # Spec B — Chat SSE Resilience Hotfix
2
+
3
+ **Status:** Approved
4
+ **Created:** 2026-04-08
5
+ **Scope mode:** REDUCE
6
+ **Related:** [Schedule Orchestration (Spec A)](./2026-04-08-schedule-orchestration-design.md), [Swarm Visibility (Spec C)](./2026-04-08-swarm-visibility-design.md)
7
+
8
+ ## Context
9
+
10
+ On 2026-04-08 at 12:20:49 UTC, five scheduled agents fired simultaneously and consumed ~12,600 combined turns on Claude Opus 4.6. A user sent a chat message ~66 seconds later; the SSE stream dropped mid-stream and the assistant message persisted with `content: ""` and `status: "streaming"`. The user saw the conversation "jank and reset."
11
+
12
+ This hotfix addresses the symptom — placeholder chat messages left in an empty/streaming state — independent of the underlying schedule-orchestration work (Spec A). It is a ~40 LOC defensive change that can ship in hours, in parallel with Spec A implementation.
13
+
14
+ ## Goal
15
+
16
+ Uphold the invariant:
17
+
18
+ > After `sendMessage()` returns or throws, no `chat_messages` row for that conversation remains with `status='streaming'` and `content=''`.
19
+
20
+ ## Root cause analysis
21
+
22
+ Code inspection of `src/app/api/chat/conversations/[id]/messages/route.ts` and `src/lib/chat/engine.ts` reveals three paths by which the invariant can be broken:
23
+
24
+ 1. **Finally-block bypass via iterator abandonment.** When the route handler consumer `break`s out of the `for await` loop (route.ts:83), the async iterator's `return()` method is invoked. In an async generator, `return()` jumps to the `finally` block, **skipping the `catch` block entirely**. Engine.ts's catch at line 644 never runs, so `updateMessageContent()` is never called. The placeholder row from engine.ts:246 stays at `content=''`.
25
+
26
+ 2. **Defensive fallback gap in error path.** engine.ts:680 writes `fullText || errorMessage`. If both are empty strings (e.g., `diagnoseProcessError()` returns empty from a blank stderr), the DB gets `content=''`.
27
+
28
+ 3. **DB write hang under contention.** Under WAL contention from concurrent schedulers, `await updateMessageContent()` in the catch path can block past the HTTP request lifetime. Next.js tears down the request before it resolves; the update never commits.
29
+
30
+ 4. **No orphan reconciliation.** Historical `streaming` rows from crashed processes or prior bugs remain visible in the UI forever.
31
+
32
+ ## Fix design
33
+
34
+ ### Change 1 — Finally-block safety net
35
+
36
+ In `src/lib/chat/engine.ts`, modify the top-level `finally` block (currently line 700, containing only `cleanupConversation(conversationId)`):
37
+
38
+ ```typescript
39
+ } finally {
40
+ try {
41
+ const current = await getMessage(assistantMsg.id);
42
+ if (current && current.status === "streaming") {
43
+ const salvage =
44
+ fullText && fullText.trim().length > 0
45
+ ? fullText
46
+ : "(Response interrupted. Please try again.)";
47
+ await updateMessageContent(assistantMsg.id, salvage);
48
+ await updateMessageStatus(
49
+ assistantMsg.id,
50
+ fullText && fullText.length > 50 ? "complete" : "error",
51
+ );
52
+ }
53
+ } catch (finalizeErr) {
54
+ console.error("[chat] finalize safety net failed:", finalizeErr);
55
+ }
56
+ cleanupConversation(conversationId);
57
+ }
58
+ ```
59
+
60
+ **Why at the finally level:** catches every code path — happy path (already `complete`, safety net is no-op), engine catch path (already wrote content, safety net is no-op), abandoned iterator path (NEW — catches the bug), generator throw path (NEW — catches the bug).
61
+
62
+ ### Change 2 — Defensive fallback in error path
63
+
64
+ At `src/lib/chat/engine.ts:680`, replace `fullText || errorMessage` with:
65
+
66
+ ```typescript
67
+ fullText || errorMessage || "(Response failed — no error detail available.)"
68
+ ```
69
+
70
+ Eliminates the empty-string write even if both sources are blank.
71
+
72
+ ### Change 3 — Truncate oversized errorMessage
73
+
74
+ Before writing `errorMessage` to the DB, truncate at 4KB:
75
+
76
+ ```typescript
77
+ const safeErrorMessage = errorMessage.length > 4096
78
+ ? errorMessage.slice(0, 4096) + "... (truncated)"
79
+ : errorMessage;
80
+ ```
81
+
82
+ Prevents bloat from multi-MB stderr dumps.
83
+
84
+ ### Change 4 — Orphan reconciliation sweep
85
+
86
+ Add a helper in `src/lib/chat/reconcile.ts` (new file):
87
+
88
+ ```typescript
89
+ export async function reconcileStreamingMessages(): Promise<number> {
90
+ const cutoff = new Date(Date.now() - 10 * 60 * 1000); // 10 minutes ago
91
+ const orphans = await db
92
+ .select()
93
+ .from(chatMessages)
94
+ .where(
95
+ and(
96
+ eq(chatMessages.status, "streaming"),
97
+ lt(chatMessages.createdAt, cutoff),
98
+ ),
99
+ );
100
+
101
+ for (const row of orphans) {
102
+ await db
103
+ .update(chatMessages)
104
+ .set({
105
+ status: "error",
106
+ content:
107
+ row.content && row.content.length > 0
108
+ ? row.content
109
+ : "(Interrupted — this response was not completed. Please retry.)",
110
+ })
111
+ .where(eq(chatMessages.id, row.id));
112
+ }
113
+
114
+ return orphans.length;
115
+ }
116
+ ```
117
+
118
+ Call from the chat conversations page loader (fire-and-forget). 10-min cutoff is far longer than any legitimate streaming duration — no risk of clobbering in-flight responses.
119
+
120
+ ### Change 5 — Route handler cleanup
121
+
122
+ In `src/app/api/chat/conversations/[id]/messages/route.ts:95-98`, wrap `controller.close()` in a try/catch so a throw during close doesn't mask earlier errors:
123
+
124
+ ```typescript
125
+ } finally {
126
+ clearInterval(keepalive);
127
+ try {
128
+ controller.close();
129
+ } catch {
130
+ // Already closed; nothing to do
131
+ }
132
+ }
133
+ ```
134
+
135
+ ## Data model changes
136
+
137
+ **None.** Uses existing schema.
138
+
139
+ ## Tests
140
+
141
+ ### Unit tests (new)
142
+
143
+ **`src/lib/chat/__tests__/engine.finalize-safety-net.test.ts`:**
144
+
145
+ 1. **Mid-stream SDK throw with partial content**: mock SDK to yield 3 chunks then throw; assert placeholder ends up with salvaged `fullText` as content and `status='complete'` (because fullText > 50 chars).
146
+ 2. **Mid-stream SDK throw with no content**: mock SDK to throw before any text; assert placeholder ends up with fallback string and `status='error'`.
147
+ 3. **Empty errorMessage AND empty fullText**: mock `diagnoseProcessError` to return empty and SDK to throw immediately; assert the line-680 fallback string is written, never `''`.
148
+ 4. **Iterator abandonment (consumer break)**: mock consumer that breaks on first yield; assert finally-block safety net salvages the row even though catch didn't run.
149
+ 5. **Happy path no-op**: mock SDK to complete normally; assert finally-block safety net sees `status='complete'` and does nothing.
150
+
151
+ **`src/lib/chat/__tests__/reconcile.test.ts`:**
152
+
153
+ 6. **20-min-old streaming row**: seed a row with `status='streaming'`, `createdAt = now - 20min`; assert reconcile marks it `error` with fallback content.
154
+ 7. **30-sec-old streaming row**: seed a row with `status='streaming'`, `createdAt = now - 30s`; assert reconcile leaves it untouched.
155
+ 8. **Partial content preservation**: seed a row with `status='streaming'`, `content='Hello wor'`, old timestamp; assert reconcile preserves the partial content, marks `error`.
156
+
157
+ ### Integration
158
+
159
+ 9. **Manual repro**: open chat, start a long prompt, send `SIGSTOP` to Next.js mid-stream for 15s, resume → assert assistant message ends finalized (never `streaming`/`content=''`).
160
+ 10. **Spec A interaction**: after Spec A lands, fire 5 schedules via `POST /api/schedules/:id/execute?force=true`, send a chat message, force the SSE to drop → assert no `chat_messages` row with `content=''` remains.
161
+
162
+ ## Error & Rescue Registry
163
+
164
+ | Error | Trigger | Impact | Rescue |
165
+ |---|---|---|---|
166
+ | Finalize safety-net DB write itself fails | Disk full, WAL locked | Placeholder stays empty (regression) | `try/catch` around the finalize block; log to console; `cleanupConversation` still runs |
167
+ | `getMessage()` returns undefined in finally | Race with delete | TypeError | Null-check (`if (current && ...)`) |
168
+ | Orphan sweep deletes legitimate in-flight row | 10-min window too tight | User sees interrupted message falsely | Use 10 min (far longer than any real SDK turn); monitor sweep hits post-ship |
169
+ | `errorMessage` is a multi-MB stderr dump | `diagnoseProcessError` returns huge string | Bloated chat_messages row | Truncate at 4KB (Change 3) |
170
+ | Reconcile runs concurrently with a new message | Race between page load and new send | Double-write | Reconcile's UPDATE is idempotent; only touches rows matching `status='streaming' AND createdAt < cutoff` |
171
+ | `controller.close()` throws in finally | Stream already closed by peer | Unhandled rejection | try/catch (Change 5) |
172
+
173
+ ## NOT in scope (deferred)
174
+
175
+ - **SSE client-side reconnect / replay from last event ID** — future spec "Chat Streaming v2"
176
+ - **Heartbeat-based client timeout detection** — future spec "Chat Streaming v2"
177
+ - **Moving chat off the shared Node event loop** (worker isolation) — addressed by Spec A's concurrency cap instead
178
+ - **Refactor of `diagnoseProcessError()`** — use fallback string at call site instead
179
+ - **Adding `lastHeartbeatAt` column for more precise orphan detection** — defer until 10-min cutoff proves insufficient
180
+
181
+ ## Files touched
182
+
183
+ - `src/lib/chat/engine.ts` — finally block (Change 1), error-path fallback (Change 2), truncation (Change 3)
184
+ - `src/app/api/chat/conversations/[id]/messages/route.ts` — controller.close try/catch (Change 5)
185
+ - `src/lib/chat/reconcile.ts` — NEW file with `reconcileStreamingMessages()` (Change 4)
186
+ - `src/app/chat/page.tsx` — call `reconcileStreamingMessages()` in loader (fire-and-forget)
187
+ - `src/lib/chat/__tests__/engine.finalize-safety-net.test.ts` — NEW
188
+ - `src/lib/chat/__tests__/reconcile.test.ts` — NEW
189
+
190
+ ## Verification
191
+
192
+ 1. All new unit tests pass.
193
+ 2. Full chat test suite regression green.
194
+ 3. Manual SIGSTOP repro (step 9 above) shows no orphaned `streaming` rows.
195
+ 4. Post-ship query: `SELECT COUNT(*) FROM chat_messages WHERE content='' AND status IN ('streaming','pending')` stays at 0 after first full chat page reload.
196
+
197
+ ## Ship plan
198
+
199
+ - No feature flag — hotfix is unconditional safety.
200
+ - Ships independently of Spec A (zero shared code).
201
+ - Ship as a standalone PR; commit separately from orchestration work for clean bisect-ability.