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.
- package/README.md +20 -44
- package/dist/cli.js +66 -18
- package/docs/.coverage-gaps.json +144 -56
- package/docs/.last-generated +1 -1
- package/docs/features/agent-intelligence.md +12 -2
- package/docs/features/chat.md +40 -5
- package/docs/features/cost-usage.md +1 -1
- package/docs/features/documents.md +5 -2
- package/docs/features/inbox-notifications.md +10 -2
- package/docs/features/keyboard-navigation.md +12 -3
- package/docs/features/provider-runtimes.md +20 -2
- package/docs/features/schedules.md +32 -4
- package/docs/features/settings.md +28 -5
- package/docs/features/shared-components.md +7 -3
- package/docs/features/tables.md +11 -2
- package/docs/features/tool-permissions.md +6 -2
- package/docs/features/workflows.md +14 -4
- package/docs/index.md +1 -1
- package/docs/journeys/developer.md +39 -2
- package/docs/journeys/personal-use.md +32 -8
- package/docs/journeys/power-user.md +45 -14
- package/docs/journeys/work-use.md +17 -8
- package/docs/manifest.json +15 -15
- package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
- package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
- package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
- package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
- package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
- package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
- package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
- package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
- package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
- package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
- package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
- package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
- package/next.config.mjs +1 -0
- package/package.json +3 -2
- package/src/__tests__/instrumentation-smoke.test.ts +15 -0
- package/src/app/analytics/page.tsx +1 -21
- package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
- package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
- package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
- package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
- package/src/app/api/chat/export/route.ts +52 -0
- package/src/app/api/chat/files/search/route.ts +50 -0
- package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
- package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
- package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
- package/src/app/api/environment/skills/route.ts +13 -0
- package/src/app/api/instance/config/route.ts +41 -0
- package/src/app/api/instance/init/route.ts +34 -0
- package/src/app/api/instance/upgrade/check/route.ts +26 -0
- package/src/app/api/instance/upgrade/route.ts +96 -0
- package/src/app/api/instance/upgrade/status/route.ts +35 -0
- package/src/app/api/memory/route.ts +0 -11
- package/src/app/api/notifications/route.ts +4 -2
- package/src/app/api/projects/[id]/route.ts +5 -155
- package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
- package/src/app/api/schedules/[id]/execute/route.ts +111 -0
- package/src/app/api/schedules/[id]/route.ts +9 -1
- package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
- package/src/app/api/schedules/route.ts +3 -12
- package/src/app/api/settings/chat/pins/route.ts +94 -0
- package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
- package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
- package/src/app/api/settings/environment/route.ts +26 -0
- package/src/app/api/settings/openai/login/route.ts +22 -0
- package/src/app/api/settings/openai/logout/route.ts +7 -0
- package/src/app/api/settings/openai/route.ts +21 -1
- package/src/app/api/settings/providers/route.ts +35 -8
- package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
- package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
- package/src/app/api/tables/[id]/enrich/route.ts +147 -0
- package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
- package/src/app/api/tasks/[id]/execute/route.ts +52 -33
- package/src/app/api/tasks/[id]/respond/route.ts +31 -15
- package/src/app/api/tasks/[id]/resume/route.ts +24 -3
- package/src/app/api/workflows/[id]/resume/route.ts +59 -0
- package/src/app/api/workflows/[id]/status/route.ts +22 -8
- package/src/app/api/workspace/context/route.ts +2 -0
- package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
- package/src/app/chat/page.tsx +11 -0
- package/src/app/documents/page.tsx +4 -1
- package/src/app/inbox/page.tsx +12 -5
- package/src/app/layout.tsx +42 -21
- package/src/app/page.tsx +0 -2
- package/src/app/settings/page.tsx +8 -9
- package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
- package/src/components/chat/__tests__/chat-session-provider.test.tsx +573 -0
- package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
- package/src/components/chat/capability-banner.tsx +68 -0
- package/src/components/chat/chat-command-popover.tsx +670 -49
- package/src/components/chat/chat-input.tsx +104 -10
- package/src/components/chat/chat-message.tsx +12 -3
- package/src/components/chat/chat-session-provider.tsx +790 -0
- package/src/components/chat/chat-shell.tsx +151 -401
- package/src/components/chat/command-tab-bar.tsx +68 -0
- package/src/components/chat/conversation-template-picker.tsx +421 -0
- package/src/components/chat/help-dialog.tsx +39 -0
- package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
- package/src/components/chat/skill-row.tsx +147 -0
- package/src/components/documents/document-browser.tsx +37 -19
- package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
- package/src/components/instance/instance-section.tsx +382 -0
- package/src/components/instance/upgrade-badge.tsx +219 -0
- package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
- package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
- package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
- package/src/components/notifications/batch-proposal-review.tsx +20 -5
- package/src/components/notifications/inbox-list.tsx +11 -2
- package/src/components/notifications/notification-item.tsx +56 -2
- package/src/components/notifications/pending-approval-host.tsx +56 -37
- package/src/components/notifications/permission-response-actions.tsx +155 -1
- package/src/components/schedules/schedule-create-sheet.tsx +19 -1
- package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
- package/src/components/schedules/schedule-form.tsx +31 -0
- package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
- package/src/components/settings/auth-method-selector.tsx +19 -4
- package/src/components/settings/auth-status-badge.tsx +28 -3
- package/src/components/settings/environment-section.tsx +102 -0
- package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
- package/src/components/settings/openai-runtime-section.tsx +7 -1
- package/src/components/settings/providers-runtimes-section.tsx +138 -19
- package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
- package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
- package/src/components/shared/app-sidebar.tsx +4 -3
- package/src/components/shared/command-palette.tsx +266 -7
- package/src/components/shared/filter-hint.tsx +70 -0
- package/src/components/shared/filter-input.tsx +59 -0
- package/src/components/shared/saved-searches-manager.tsx +199 -0
- package/src/components/shared/theme-toggle.tsx +5 -24
- package/src/components/shared/workspace-indicator.tsx +61 -2
- package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
- package/src/components/tables/table-create-sheet.tsx +4 -0
- package/src/components/tables/table-enrichment-runs.tsx +103 -0
- package/src/components/tables/table-enrichment-sheet.tsx +538 -0
- package/src/components/tables/table-spreadsheet.tsx +29 -5
- package/src/components/tables/table-toolbar.tsx +10 -1
- package/src/components/tasks/kanban-board.tsx +1 -0
- package/src/components/tasks/kanban-column.tsx +53 -14
- package/src/components/tasks/task-bento-grid.tsx +31 -2
- package/src/components/tasks/task-card.tsx +29 -3
- package/src/components/tasks/task-chip-bar.tsx +54 -1
- package/src/components/tasks/task-result-renderer.tsx +1 -1
- package/src/components/workflows/delay-step-body.tsx +109 -0
- package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
- package/src/components/workflows/loop-status-view.tsx +1 -1
- package/src/components/workflows/shared/step-result.tsx +78 -0
- package/src/components/workflows/shared/workflow-header.tsx +141 -0
- package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
- package/src/components/workflows/swarm-dashboard.tsx +2 -15
- package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
- package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
- package/src/components/workflows/workflow-form-view.tsx +133 -16
- package/src/components/workflows/workflow-status-view.tsx +30 -740
- package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
- package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
- package/src/hooks/use-active-skills.ts +110 -0
- package/src/hooks/use-chat-autocomplete.ts +120 -7
- package/src/hooks/use-enriched-skills.ts +19 -0
- package/src/hooks/use-pinned-entries.ts +104 -0
- package/src/hooks/use-recent-user-messages.ts +19 -0
- package/src/hooks/use-saved-searches.ts +142 -0
- package/src/instrumentation-node.ts +94 -0
- package/src/instrumentation.ts +4 -48
- package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
- package/src/lib/agents/__tests__/claude-agent.test.ts +212 -0
- package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
- package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
- package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
- package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
- package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
- package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
- package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
- package/src/lib/agents/claude-agent.ts +217 -21
- package/src/lib/agents/execution-manager.ts +0 -35
- package/src/lib/agents/handoff/bus.ts +2 -2
- package/src/lib/agents/learned-context.ts +0 -12
- package/src/lib/agents/learning-session.ts +18 -5
- package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
- package/src/lib/agents/profiles/__tests__/registry.test.ts +53 -4
- package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +97 -0
- package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +36 -0
- package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
- package/src/lib/agents/profiles/registry.ts +18 -0
- package/src/lib/agents/profiles/types.ts +7 -1
- package/src/lib/agents/router.ts +3 -6
- package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
- package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
- package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
- package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
- package/src/lib/agents/runtime/catalog.ts +121 -0
- package/src/lib/agents/runtime/claude-sdk.ts +32 -0
- package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
- package/src/lib/agents/runtime/execution-target.ts +456 -0
- package/src/lib/agents/runtime/index.ts +4 -0
- package/src/lib/agents/runtime/launch-failure.ts +101 -0
- package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
- package/src/lib/agents/runtime/openai-codex.ts +64 -60
- package/src/lib/agents/runtime/openai-direct.ts +8 -0
- package/src/lib/agents/runtime/types.ts +8 -0
- package/src/lib/agents/task-dispatch.ts +220 -0
- package/src/lib/agents/tool-permissions.ts +16 -1
- package/src/lib/book/chapter-mapping.ts +11 -0
- package/src/lib/book/content.ts +10 -0
- package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
- package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
- package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
- package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
- package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
- package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
- package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
- package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
- package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
- package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
- package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
- package/src/lib/chat/__tests__/types.test.ts +28 -0
- package/src/lib/chat/active-skills.ts +31 -0
- package/src/lib/chat/active-streams.ts +27 -0
- package/src/lib/chat/clean-filter-input.ts +30 -0
- package/src/lib/chat/codex-engine.ts +46 -24
- package/src/lib/chat/command-tabs.ts +61 -0
- package/src/lib/chat/context-builder.ts +146 -4
- package/src/lib/chat/dismissals.ts +73 -0
- package/src/lib/chat/engine.ts +159 -18
- package/src/lib/chat/files/__tests__/search.test.ts +135 -0
- package/src/lib/chat/files/expand-mention.ts +76 -0
- package/src/lib/chat/files/search.ts +99 -0
- package/src/lib/chat/reconcile.ts +117 -0
- package/src/lib/chat/skill-composition.ts +210 -0
- package/src/lib/chat/skill-conflict.ts +105 -0
- package/src/lib/chat/stagent-tools.ts +7 -19
- package/src/lib/chat/stream-telemetry.ts +137 -0
- package/src/lib/chat/suggested-prompts.ts +28 -1
- package/src/lib/chat/system-prompt.ts +48 -1
- package/src/lib/chat/tool-catalog.ts +35 -4
- package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
- package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
- package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
- package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
- package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
- package/src/lib/chat/tools/__tests__/task-tools.test.ts +399 -0
- package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +351 -0
- package/src/lib/chat/tools/blueprint-tools.ts +190 -0
- package/src/lib/chat/tools/document-tools.ts +29 -13
- package/src/lib/chat/tools/helpers.ts +41 -0
- package/src/lib/chat/tools/notification-tools.ts +9 -5
- package/src/lib/chat/tools/profile-tools.ts +120 -23
- package/src/lib/chat/tools/project-tools.ts +33 -0
- package/src/lib/chat/tools/schedule-tools.ts +44 -11
- package/src/lib/chat/tools/skill-tools.ts +183 -0
- package/src/lib/chat/tools/table-tools.ts +71 -0
- package/src/lib/chat/tools/task-tools.ts +89 -21
- package/src/lib/chat/tools/workflow-tools.ts +275 -32
- package/src/lib/chat/types.ts +15 -0
- package/src/lib/constants/settings.ts +10 -18
- package/src/lib/data/__tests__/clear.test.ts +56 -2
- package/src/lib/data/clear.ts +17 -16
- package/src/lib/data/delete-project.ts +171 -0
- package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
- package/src/lib/db/bootstrap.ts +62 -16
- package/src/lib/db/index.ts +5 -0
- package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
- package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
- package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
- package/src/lib/db/migrations/0026_drop_license.sql +3 -0
- package/src/lib/db/migrations/meta/_journal.json +21 -0
- package/src/lib/db/schema.ts +94 -23
- package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
- package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
- package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
- package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
- package/src/lib/environment/data.ts +9 -0
- package/src/lib/environment/list-skills.ts +176 -0
- package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
- package/src/lib/environment/parsers/skill.ts +26 -5
- package/src/lib/environment/profile-generator.ts +54 -0
- package/src/lib/environment/skill-enrichment.ts +106 -0
- package/src/lib/environment/skill-recommendations.ts +66 -0
- package/src/lib/environment/workspace-context.ts +13 -1
- package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
- package/src/lib/filters/__tests__/parse.test.ts +135 -0
- package/src/lib/filters/parse.ts +86 -0
- package/src/lib/import/dedup.ts +4 -54
- package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
- package/src/lib/instance/__tests__/detect.test.ts +115 -0
- package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
- package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
- package/src/lib/instance/__tests__/settings.test.ts +83 -0
- package/src/lib/instance/__tests__/upgrade-poller.test.ts +181 -0
- package/src/lib/instance/bootstrap.ts +270 -0
- package/src/lib/instance/detect.ts +49 -0
- package/src/lib/instance/fingerprint.ts +76 -0
- package/src/lib/instance/git-ops.ts +95 -0
- package/src/lib/instance/settings.ts +61 -0
- package/src/lib/instance/types.ts +77 -0
- package/src/lib/instance/upgrade-poller.ts +205 -0
- package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
- package/src/lib/notifications/visibility.ts +33 -0
- package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
- package/src/lib/schedules/__tests__/config.test.ts +62 -0
- package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
- package/src/lib/schedules/__tests__/integration.test.ts +82 -0
- package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
- package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
- package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
- package/src/lib/schedules/collision-check.ts +105 -0
- package/src/lib/schedules/config.ts +53 -0
- package/src/lib/schedules/scheduler.ts +236 -17
- package/src/lib/schedules/slot-claim.ts +105 -0
- package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
- package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
- package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
- package/src/lib/settings/openai-auth.ts +105 -10
- package/src/lib/settings/openai-login-manager.ts +260 -0
- package/src/lib/settings/runtime-setup.ts +14 -4
- package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
- package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
- package/src/lib/tables/enrichment-planner.ts +454 -0
- package/src/lib/tables/enrichment.ts +328 -0
- package/src/lib/tables/query-builder.ts +5 -2
- package/src/lib/tables/trigger-evaluator.ts +3 -2
- package/src/lib/theme.ts +71 -0
- package/src/lib/usage/ledger.ts +2 -18
- package/src/lib/util/__tests__/similarity.test.ts +106 -0
- package/src/lib/util/similarity.ts +77 -0
- package/src/lib/utils/format-timestamp.ts +24 -0
- package/src/lib/utils/stagent-paths.ts +12 -0
- package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
- package/src/lib/validators/__tests__/settings.test.ts +10 -0
- package/src/lib/validators/blueprint.ts +70 -9
- package/src/lib/validators/profile.ts +2 -2
- package/src/lib/validators/settings.ts +3 -1
- package/src/lib/workflows/__tests__/delay.test.ts +196 -0
- package/src/lib/workflows/__tests__/engine.test.ts +8 -0
- package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
- package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
- package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
- package/src/lib/workflows/blueprints/instantiator.ts +22 -1
- package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
- package/src/lib/workflows/blueprints/types.ts +16 -2
- package/src/lib/workflows/delay.ts +106 -0
- package/src/lib/workflows/engine.ts +212 -7
- package/src/lib/workflows/loop-executor.ts +349 -24
- package/src/lib/workflows/post-action.ts +91 -0
- package/src/lib/workflows/types.ts +166 -1
- package/src/test/setup.ts +10 -0
- package/src/app/api/license/checkout/route.ts +0 -28
- package/src/app/api/license/portal/route.ts +0 -26
- package/src/app/api/license/route.ts +0 -89
- package/src/app/api/license/usage/route.ts +0 -63
- package/src/app/api/marketplace/browse/route.ts +0 -15
- package/src/app/api/marketplace/import/route.ts +0 -28
- package/src/app/api/marketplace/publish/route.ts +0 -40
- package/src/app/api/onboarding/email/route.ts +0 -53
- package/src/app/api/settings/telemetry/route.ts +0 -14
- package/src/app/api/sync/export/route.ts +0 -54
- package/src/app/api/sync/restore/route.ts +0 -37
- package/src/app/api/sync/sessions/route.ts +0 -24
- package/src/app/auth/callback/route.ts +0 -73
- package/src/app/marketplace/page.tsx +0 -19
- package/src/components/analytics/analytics-gate-card.tsx +0 -101
- package/src/components/marketplace/blueprint-card.tsx +0 -61
- package/src/components/marketplace/marketplace-browser.tsx +0 -131
- package/src/components/onboarding/email-capture-card.tsx +0 -104
- package/src/components/settings/activation-form.tsx +0 -95
- package/src/components/settings/cloud-account-section.tsx +0 -147
- package/src/components/settings/cloud-sync-section.tsx +0 -155
- package/src/components/settings/subscription-section.tsx +0 -410
- package/src/components/settings/telemetry-section.tsx +0 -80
- package/src/components/shared/premium-gate-overlay.tsx +0 -50
- package/src/components/shared/schedule-gate-dialog.tsx +0 -64
- package/src/components/shared/upgrade-banner.tsx +0 -112
- package/src/hooks/use-supabase-auth.ts +0 -79
- package/src/lib/billing/email.ts +0 -54
- package/src/lib/billing/products.ts +0 -80
- package/src/lib/billing/stripe.ts +0 -101
- package/src/lib/cloud/supabase-browser.ts +0 -32
- package/src/lib/cloud/supabase-client.ts +0 -56
- package/src/lib/license/__tests__/features.test.ts +0 -56
- package/src/lib/license/__tests__/key-format.test.ts +0 -88
- package/src/lib/license/__tests__/manager.test.ts +0 -64
- package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
- package/src/lib/license/cloud-validation.ts +0 -60
- package/src/lib/license/features.ts +0 -44
- package/src/lib/license/key-format.ts +0 -101
- package/src/lib/license/limit-check.ts +0 -111
- package/src/lib/license/limit-queries.ts +0 -51
- package/src/lib/license/manager.ts +0 -345
- package/src/lib/license/notifications.ts +0 -59
- package/src/lib/license/tier-limits.ts +0 -71
- package/src/lib/marketplace/marketplace-client.ts +0 -107
- package/src/lib/sync/cloud-sync.ts +0 -235
- package/src/lib/telemetry/conversion-events.ts +0 -71
- package/src/lib/telemetry/queue.ts +0 -122
- 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.
|