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
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
workflowDocumentInputs,
|
|
11
11
|
} from "@/lib/db/schema";
|
|
12
12
|
import { eq, and, desc, inArray, like } from "drizzle-orm";
|
|
13
|
-
import { ok, err, type ToolContext } from "./helpers";
|
|
13
|
+
import { ok, err, resolveEntityId, type ToolContext } from "./helpers";
|
|
14
|
+
import { extractKeywords, jaccard } from "@/lib/util/similarity";
|
|
14
15
|
|
|
15
16
|
const VALID_WORKFLOW_STATUSES = [
|
|
16
17
|
"draft",
|
|
@@ -20,6 +21,148 @@ const VALID_WORKFLOW_STATUSES = [
|
|
|
20
21
|
"failed",
|
|
21
22
|
] as const;
|
|
22
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Minimum weighted-Jaccard score for two workflows to count as "near
|
|
26
|
+
* duplicates". Combined score = NAME_WEIGHT * nameJaccard +
|
|
27
|
+
* STEPS_WEIGHT * stepsJaccard.
|
|
28
|
+
*
|
|
29
|
+
* Why weighted-and-split rather than a single pooled Jaccard? A pooled
|
|
30
|
+
* Jaccard over name+step text at threshold 0.7 was flagging legitimate
|
|
31
|
+
* target-entity variants (e.g. "Enrich contacts" vs "Enrich accounts",
|
|
32
|
+
* "Daily standup digest" vs "Weekly standup digest") as duplicates,
|
|
33
|
+
* forcing users to pass `force: true` for every such pair and eroding
|
|
34
|
+
* trust in the guardrail. Splitting the signal lets the one-token
|
|
35
|
+
* difference in names AND prompts contribute to two independent
|
|
36
|
+
* Jaccards, which together pull combined similarity below 0.7 while
|
|
37
|
+
* structural duplicates (identical steps + near-identical name) still
|
|
38
|
+
* exceed the threshold.
|
|
39
|
+
*
|
|
40
|
+
* Tuning rationale:
|
|
41
|
+
* - 0.7 threshold preserved from the original implementation.
|
|
42
|
+
* - 0.5/0.5 weights (no tags). The feature spec sketched a 0.3/0.5/0.2
|
|
43
|
+
* split over name/steps/tags, but workflows do not persist tags in
|
|
44
|
+
* their definition JSON today. Without a tags signal, 0.5/0.5
|
|
45
|
+
* empirically separates legitimate variants (Enrich contacts vs
|
|
46
|
+
* accounts: 0.60; Daily vs Weekly standup: 0.68) from structural
|
|
47
|
+
* duplicates (identical steps with renamed workflow: 0.75+) with
|
|
48
|
+
* headroom on both sides. Revisit weights if tag data lands.
|
|
49
|
+
*
|
|
50
|
+
* If a future false-positive case surfaces, add a regression test in
|
|
51
|
+
* `workflow-tools-dedup.test.ts` → "legitimate variant tolerance" and
|
|
52
|
+
* re-tune rather than bumping `force: true` everywhere.
|
|
53
|
+
*
|
|
54
|
+
* See `features/chat-dedup-variant-tolerance.md`.
|
|
55
|
+
*/
|
|
56
|
+
const WORKFLOW_DEDUP_THRESHOLD = 0.7;
|
|
57
|
+
const WORKFLOW_NAME_WEIGHT = 0.5;
|
|
58
|
+
const WORKFLOW_STEPS_WEIGHT = 0.5;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Split a workflow into its two comparable text signals: the name alone,
|
|
62
|
+
* and a concatenation of every step's name + prompt. Callers pass each
|
|
63
|
+
* signal through `extractKeywords` separately so name-level tokens don't
|
|
64
|
+
* get drowned out by the much larger step-text bag, and vice versa.
|
|
65
|
+
*
|
|
66
|
+
* Malformed definition JSON falls back to `stepsText = ""`.
|
|
67
|
+
*/
|
|
68
|
+
function workflowSignals(
|
|
69
|
+
name: string,
|
|
70
|
+
definitionJson: string | null
|
|
71
|
+
): { nameText: string; stepsText: string } {
|
|
72
|
+
if (!definitionJson) return { nameText: name, stepsText: "" };
|
|
73
|
+
try {
|
|
74
|
+
const def = JSON.parse(definitionJson);
|
|
75
|
+
const stepParts: string[] = [];
|
|
76
|
+
if (Array.isArray(def?.steps)) {
|
|
77
|
+
for (const step of def.steps) {
|
|
78
|
+
if (typeof step?.name === "string") stepParts.push(step.name);
|
|
79
|
+
if (typeof step?.prompt === "string") stepParts.push(step.prompt);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { nameText: name, stepsText: stepParts.join(" ") };
|
|
83
|
+
} catch {
|
|
84
|
+
return { nameText: name, stepsText: "" };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface SimilarWorkflowMatch {
|
|
89
|
+
id: string;
|
|
90
|
+
name: string;
|
|
91
|
+
similarity: number;
|
|
92
|
+
reason: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Find workflows in the same project that look similar to a candidate.
|
|
97
|
+
*
|
|
98
|
+
* Two-tier check:
|
|
99
|
+
* 1. Exact name match (case-insensitive) → similarity 1.0
|
|
100
|
+
* 2. Jaccard similarity over extracted keywords from name+step titles+prompts
|
|
101
|
+
*
|
|
102
|
+
* Returns up to 3 matches with similarity >= WORKFLOW_DEDUP_THRESHOLD,
|
|
103
|
+
* sorted by similarity descending. Used by `create_workflow` to warn the
|
|
104
|
+
* LLM before blindly inserting another row in long conversations where
|
|
105
|
+
* the sliding-window context builder evicts earlier creations.
|
|
106
|
+
*
|
|
107
|
+
* When projectId is null (no active project), returns [] — cross-project
|
|
108
|
+
* dedup would be misleading, and the handful of null-project rows that
|
|
109
|
+
* exist aren't worth de-duplicating against each other.
|
|
110
|
+
*/
|
|
111
|
+
export async function findSimilarWorkflows(
|
|
112
|
+
projectId: string | null,
|
|
113
|
+
candidateName: string,
|
|
114
|
+
candidateDefinitionJson: string
|
|
115
|
+
): Promise<SimilarWorkflowMatch[]> {
|
|
116
|
+
if (!projectId) return [];
|
|
117
|
+
|
|
118
|
+
const existing = await db
|
|
119
|
+
.select({
|
|
120
|
+
id: workflows.id,
|
|
121
|
+
name: workflows.name,
|
|
122
|
+
definition: workflows.definition,
|
|
123
|
+
})
|
|
124
|
+
.from(workflows)
|
|
125
|
+
.where(eq(workflows.projectId, projectId));
|
|
126
|
+
|
|
127
|
+
const matches: SimilarWorkflowMatch[] = [];
|
|
128
|
+
const candidateSignals = workflowSignals(candidateName, candidateDefinitionJson);
|
|
129
|
+
const candidateNameKeywords = extractKeywords(candidateSignals.nameText);
|
|
130
|
+
const candidateStepKeywords = extractKeywords(candidateSignals.stepsText);
|
|
131
|
+
const candidateNameLower = candidateName.trim().toLowerCase();
|
|
132
|
+
|
|
133
|
+
for (const row of existing) {
|
|
134
|
+
// Tier 1: exact name match (case-insensitive)
|
|
135
|
+
if (row.name.trim().toLowerCase() === candidateNameLower) {
|
|
136
|
+
matches.push({
|
|
137
|
+
id: row.id,
|
|
138
|
+
name: row.name,
|
|
139
|
+
similarity: 1,
|
|
140
|
+
reason: `Same name: "${row.name}"`,
|
|
141
|
+
});
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Tier 2: weighted Jaccard — name and step signals scored separately,
|
|
146
|
+
// then combined with WORKFLOW_NAME_WEIGHT / WORKFLOW_STEPS_WEIGHT so
|
|
147
|
+
// target-entity variants (same verb, different noun) are not flagged.
|
|
148
|
+
const existingSignals = workflowSignals(row.name, row.definition);
|
|
149
|
+
const nameJ = jaccard(candidateNameKeywords, extractKeywords(existingSignals.nameText));
|
|
150
|
+
const stepsJ = jaccard(candidateStepKeywords, extractKeywords(existingSignals.stepsText));
|
|
151
|
+
const similarity =
|
|
152
|
+
WORKFLOW_NAME_WEIGHT * nameJ + WORKFLOW_STEPS_WEIGHT * stepsJ;
|
|
153
|
+
if (similarity >= WORKFLOW_DEDUP_THRESHOLD) {
|
|
154
|
+
matches.push({
|
|
155
|
+
id: row.id,
|
|
156
|
+
name: row.name,
|
|
157
|
+
similarity,
|
|
158
|
+
reason: `Similar content to "${row.name}" (${Math.round(similarity * 100)}%)`,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return matches.sort((a, b) => b.similarity - a.similarity).slice(0, 3);
|
|
164
|
+
}
|
|
165
|
+
|
|
23
166
|
export function workflowTools(ctx: ToolContext) {
|
|
24
167
|
return [
|
|
25
168
|
defineTool(
|
|
@@ -69,7 +212,7 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
69
212
|
|
|
70
213
|
defineTool(
|
|
71
214
|
"create_workflow",
|
|
72
|
-
"Create a new workflow with a definition. The definition must include a pattern (sequence, parallel, checkpoint, planner-executor, swarm, loop) and steps array.",
|
|
215
|
+
"Create a new workflow with a definition. The definition must include a pattern (sequence, parallel, checkpoint, planner-executor, swarm, loop) and steps array. Sequence-pattern steps can be either task steps (with prompt + assignedAgent/agentProfile) or delay steps (with delayDuration like '3d', '2h', '30m', '1w') that pause the workflow between tasks — use delay steps for time-distributed sequences (outreach cadences, drip campaigns, cooling periods) rather than creating separate workflows or schedules. IMPORTANT: for the 'run agent on every row of a table' pattern, prefer enrich_table over create_workflow — enrich_table generates the optimal loop configuration, binds each row as {{row.field}} context, wires up the postAction row writeback, and handles idempotent skip of already-populated rows. Hand-rolled equivalents miss these safeguards.",
|
|
73
216
|
{
|
|
74
217
|
name: z.string().min(1).max(200).describe("Workflow name"),
|
|
75
218
|
projectId: z
|
|
@@ -79,7 +222,11 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
79
222
|
definition: z
|
|
80
223
|
.string()
|
|
81
224
|
.describe(
|
|
82
|
-
'Workflow definition as JSON string. Must include "pattern" and "steps" array.
|
|
225
|
+
'Workflow definition as JSON string. Must include "pattern" and "steps" array. ' +
|
|
226
|
+
'Task step example: {"id":"s1","name":"Research","prompt":"Do X","assignedAgent":"claude"}. ' +
|
|
227
|
+
'Delay step example (sequence pattern only): {"id":"s2","name":"Wait 3 days","delayDuration":"3d"}. ' +
|
|
228
|
+
'A complete drip sequence: {"pattern":"sequence","steps":[{"id":"s1","name":"Initial","prompt":"Send first email","assignedAgent":"claude"},{"id":"s2","name":"Wait","delayDuration":"3d"},{"id":"s3","name":"Follow-up","prompt":"Send follow-up","assignedAgent":"claude"}]}. ' +
|
|
229
|
+
'Delay bounds: 1m to 30d. Delay steps must NOT have prompt/profile fields.'
|
|
83
230
|
),
|
|
84
231
|
documentIds: z
|
|
85
232
|
.array(z.string())
|
|
@@ -93,6 +240,12 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
93
240
|
.describe(
|
|
94
241
|
"Runtime to use for workflow execution (e.g., 'openai-direct', 'anthropic-direct'). Use list_runtimes to see available options. Omit to use the system default."
|
|
95
242
|
),
|
|
243
|
+
force: z
|
|
244
|
+
.boolean()
|
|
245
|
+
.optional()
|
|
246
|
+
.describe(
|
|
247
|
+
"Set to true to bypass the near-duplicate check and always create a new workflow. Only use this when the user has explicitly confirmed they want a second workflow alongside a similar existing one (e.g., 'v2', 'alternate approach'). The dedup check already tolerates target-entity variants (e.g., 'Enrich contacts' vs 'Enrich accounts', 'Daily' vs 'Weekly' standup digest) — so you should NOT pass force=true for those. Default false."
|
|
248
|
+
),
|
|
96
249
|
},
|
|
97
250
|
async (args) => {
|
|
98
251
|
try {
|
|
@@ -127,6 +280,28 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
127
280
|
}
|
|
128
281
|
|
|
129
282
|
const effectiveProjectId = args.projectId ?? ctx.projectId ?? null;
|
|
283
|
+
|
|
284
|
+
// Dedup guard: long chat conversations can truncate the earlier
|
|
285
|
+
// create_workflow tool call out of the sliding-window context, so
|
|
286
|
+
// the LLM loses its own history and re-creates on "redesign"
|
|
287
|
+
// requests. Check for near-duplicates in the same project before
|
|
288
|
+
// inserting. Pass force=true to bypass.
|
|
289
|
+
if (!args.force) {
|
|
290
|
+
const similar = await findSimilarWorkflows(
|
|
291
|
+
effectiveProjectId,
|
|
292
|
+
args.name,
|
|
293
|
+
args.definition
|
|
294
|
+
);
|
|
295
|
+
if (similar.length > 0) {
|
|
296
|
+
return ok({
|
|
297
|
+
status: "similar-found",
|
|
298
|
+
message:
|
|
299
|
+
"Found similar workflow(s) in this project. Use update_workflow to modify an existing one, or pass force=true to create a new workflow alongside them.",
|
|
300
|
+
matches: similar,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
130
305
|
const now = new Date();
|
|
131
306
|
const id = crypto.randomUUID();
|
|
132
307
|
|
|
@@ -211,13 +386,17 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
211
386
|
},
|
|
212
387
|
async (args) => {
|
|
213
388
|
try {
|
|
214
|
-
const
|
|
389
|
+
const resolved = await resolveEntityId(workflows, workflows.id, args.workflowId);
|
|
390
|
+
if ("error" in resolved) return err(resolved.error);
|
|
391
|
+
const workflowId = resolved.id;
|
|
392
|
+
|
|
393
|
+
const workflow = db
|
|
215
394
|
.select()
|
|
216
395
|
.from(workflows)
|
|
217
|
-
.where(eq(workflows.id,
|
|
396
|
+
.where(eq(workflows.id, workflowId))
|
|
218
397
|
.get();
|
|
219
398
|
|
|
220
|
-
if (!workflow) return err(`Workflow not found: ${
|
|
399
|
+
if (!workflow) return err(`Workflow not found: ${workflowId}`);
|
|
221
400
|
|
|
222
401
|
const { parseWorkflowState } = await import("@/lib/workflows/engine");
|
|
223
402
|
const { definition, state } = parseWorkflowState(workflow.definition);
|
|
@@ -266,13 +445,17 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
266
445
|
},
|
|
267
446
|
async (args) => {
|
|
268
447
|
try {
|
|
269
|
-
const
|
|
448
|
+
const resolved = await resolveEntityId(workflows, workflows.id, args.workflowId);
|
|
449
|
+
if ("error" in resolved) return err(resolved.error);
|
|
450
|
+
const workflowId = resolved.id;
|
|
451
|
+
|
|
452
|
+
const existing = db
|
|
270
453
|
.select()
|
|
271
454
|
.from(workflows)
|
|
272
|
-
.where(eq(workflows.id,
|
|
455
|
+
.where(eq(workflows.id, workflowId))
|
|
273
456
|
.get();
|
|
274
457
|
|
|
275
|
-
if (!existing) return err(`Workflow not found: ${
|
|
458
|
+
if (!existing) return err(`Workflow not found: ${workflowId}`);
|
|
276
459
|
if (existing.status !== "draft")
|
|
277
460
|
return err(`Cannot edit a workflow in '${existing.status}' status. Only draft workflows can be edited.`);
|
|
278
461
|
|
|
@@ -293,12 +476,12 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
293
476
|
await db
|
|
294
477
|
.update(workflows)
|
|
295
478
|
.set(updates)
|
|
296
|
-
.where(eq(workflows.id,
|
|
479
|
+
.where(eq(workflows.id, workflowId));
|
|
297
480
|
|
|
298
481
|
const [workflow] = await db
|
|
299
482
|
.select()
|
|
300
483
|
.from(workflows)
|
|
301
|
-
.where(eq(workflows.id,
|
|
484
|
+
.where(eq(workflows.id, workflowId));
|
|
302
485
|
|
|
303
486
|
ctx.onToolResult?.("update_workflow", workflow);
|
|
304
487
|
return ok({
|
|
@@ -321,13 +504,17 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
321
504
|
},
|
|
322
505
|
async (args) => {
|
|
323
506
|
try {
|
|
324
|
-
const
|
|
507
|
+
const resolved = await resolveEntityId(workflows, workflows.id, args.workflowId);
|
|
508
|
+
if ("error" in resolved) return err(resolved.error);
|
|
509
|
+
const workflowId = resolved.id;
|
|
510
|
+
|
|
511
|
+
const existing = db
|
|
325
512
|
.select()
|
|
326
513
|
.from(workflows)
|
|
327
|
-
.where(eq(workflows.id,
|
|
514
|
+
.where(eq(workflows.id, workflowId))
|
|
328
515
|
.get();
|
|
329
516
|
|
|
330
|
-
if (!existing) return err(`Workflow not found: ${
|
|
517
|
+
if (!existing) return err(`Workflow not found: ${workflowId}`);
|
|
331
518
|
if (existing.status === "active")
|
|
332
519
|
return err("Cannot delete an active workflow. Pause or stop it first.");
|
|
333
520
|
|
|
@@ -335,7 +522,7 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
335
522
|
const childTasks = await db
|
|
336
523
|
.select({ id: tasks.id })
|
|
337
524
|
.from(tasks)
|
|
338
|
-
.where(eq(tasks.workflowId,
|
|
525
|
+
.where(eq(tasks.workflowId, workflowId));
|
|
339
526
|
|
|
340
527
|
const taskIds = childTasks.map((t) => t.id);
|
|
341
528
|
for (const taskId of taskIds) {
|
|
@@ -343,10 +530,10 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
343
530
|
await db.delete(agentLogs).where(eq(agentLogs.taskId, taskId));
|
|
344
531
|
await db.delete(documents).where(eq(documents.taskId, taskId));
|
|
345
532
|
}
|
|
346
|
-
await db.delete(tasks).where(eq(tasks.workflowId,
|
|
347
|
-
await db.delete(workflows).where(eq(workflows.id,
|
|
533
|
+
await db.delete(tasks).where(eq(tasks.workflowId, workflowId));
|
|
534
|
+
await db.delete(workflows).where(eq(workflows.id, workflowId));
|
|
348
535
|
|
|
349
|
-
return ok({ message: "Workflow deleted", workflowId
|
|
536
|
+
return ok({ message: "Workflow deleted", workflowId, name: existing.name });
|
|
350
537
|
} catch (e) {
|
|
351
538
|
return err(e instanceof Error ? e.message : "Failed to delete workflow");
|
|
352
539
|
}
|
|
@@ -361,13 +548,17 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
361
548
|
},
|
|
362
549
|
async (args) => {
|
|
363
550
|
try {
|
|
364
|
-
const
|
|
551
|
+
const resolved = await resolveEntityId(workflows, workflows.id, args.workflowId);
|
|
552
|
+
if ("error" in resolved) return err(resolved.error);
|
|
553
|
+
const workflowId = resolved.id;
|
|
554
|
+
|
|
555
|
+
const workflow = db
|
|
365
556
|
.select()
|
|
366
557
|
.from(workflows)
|
|
367
|
-
.where(eq(workflows.id,
|
|
558
|
+
.where(eq(workflows.id, workflowId))
|
|
368
559
|
.get();
|
|
369
560
|
|
|
370
|
-
if (!workflow) return err(`Workflow not found: ${
|
|
561
|
+
if (!workflow) return err(`Workflow not found: ${workflowId}`);
|
|
371
562
|
|
|
372
563
|
// Allow re-execution from crashed "active" if no live tasks
|
|
373
564
|
if (workflow.status === "active") {
|
|
@@ -376,7 +567,7 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
376
567
|
.from(tasks)
|
|
377
568
|
.where(
|
|
378
569
|
and(
|
|
379
|
-
eq(tasks.workflowId,
|
|
570
|
+
eq(tasks.workflowId, workflowId),
|
|
380
571
|
inArray(tasks.status, ["running", "queued"])
|
|
381
572
|
)
|
|
382
573
|
);
|
|
@@ -404,7 +595,7 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
404
595
|
.set({ status: "cancelled", updatedAt: new Date() })
|
|
405
596
|
.where(
|
|
406
597
|
and(
|
|
407
|
-
eq(tasks.workflowId,
|
|
598
|
+
eq(tasks.workflowId, workflowId),
|
|
408
599
|
inArray(tasks.status, ["running", "queued"])
|
|
409
600
|
)
|
|
410
601
|
);
|
|
@@ -419,27 +610,71 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
419
610
|
status: "draft",
|
|
420
611
|
updatedAt: new Date(),
|
|
421
612
|
})
|
|
422
|
-
.where(eq(workflows.id,
|
|
613
|
+
.where(eq(workflows.id, workflowId));
|
|
423
614
|
}
|
|
424
615
|
|
|
425
616
|
// Atomic claim: set to active
|
|
426
617
|
await db
|
|
427
618
|
.update(workflows)
|
|
428
619
|
.set({ status: "active", updatedAt: new Date() })
|
|
429
|
-
.where(eq(workflows.id,
|
|
620
|
+
.where(eq(workflows.id, workflowId));
|
|
430
621
|
|
|
431
622
|
// Fire-and-forget
|
|
432
623
|
const { executeWorkflow } = await import("@/lib/workflows/engine");
|
|
433
|
-
executeWorkflow(
|
|
624
|
+
executeWorkflow(workflowId).catch(() => {});
|
|
434
625
|
|
|
435
|
-
ctx.onToolResult?.("execute_workflow", { id:
|
|
436
|
-
return ok({ message: "Workflow execution started", workflowId
|
|
626
|
+
ctx.onToolResult?.("execute_workflow", { id: workflowId, name: workflow.name });
|
|
627
|
+
return ok({ message: "Workflow execution started", workflowId, name: workflow.name });
|
|
437
628
|
} catch (e) {
|
|
438
629
|
return err(e instanceof Error ? e.message : "Failed to execute workflow");
|
|
439
630
|
}
|
|
440
631
|
}
|
|
441
632
|
),
|
|
442
633
|
|
|
634
|
+
defineTool(
|
|
635
|
+
"resume_workflow",
|
|
636
|
+
"Resume a workflow that is paused at a delay step, immediately skipping the remaining delay. Use when the user says 'resume now' or 'skip the wait' for a paused workflow. Only works if the workflow status is 'paused' — a 409 response means the scheduler already resumed it. Requires approval.",
|
|
637
|
+
{
|
|
638
|
+
workflowId: z.string().describe("The workflow ID to resume"),
|
|
639
|
+
},
|
|
640
|
+
async (args) => {
|
|
641
|
+
try {
|
|
642
|
+
const resolved = await resolveEntityId(workflows, workflows.id, args.workflowId);
|
|
643
|
+
if ("error" in resolved) return err(resolved.error);
|
|
644
|
+
const workflowId = resolved.id;
|
|
645
|
+
|
|
646
|
+
const workflow = db
|
|
647
|
+
.select()
|
|
648
|
+
.from(workflows)
|
|
649
|
+
.where(eq(workflows.id, workflowId))
|
|
650
|
+
.get();
|
|
651
|
+
|
|
652
|
+
if (!workflow) return err(`Workflow not found: ${workflowId}`);
|
|
653
|
+
|
|
654
|
+
if (workflow.status !== "paused") {
|
|
655
|
+
return err(
|
|
656
|
+
`Workflow is not paused (current status: ${workflow.status}). Only paused workflows can be resumed.`,
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const { resumeWorkflow } = await import("@/lib/workflows/engine");
|
|
661
|
+
// Fire-and-forget: resumeWorkflow performs atomic status transition internally.
|
|
662
|
+
resumeWorkflow(workflowId).catch((error) => {
|
|
663
|
+
console.error(`Workflow ${workflowId} resume failed:`, error);
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
ctx.onToolResult?.("resume_workflow", { id: workflowId, name: workflow.name });
|
|
667
|
+
return ok({
|
|
668
|
+
message: "Workflow resume dispatched",
|
|
669
|
+
workflowId,
|
|
670
|
+
name: workflow.name,
|
|
671
|
+
});
|
|
672
|
+
} catch (e) {
|
|
673
|
+
return err(e instanceof Error ? e.message : "Failed to resume workflow");
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
),
|
|
677
|
+
|
|
443
678
|
defineTool(
|
|
444
679
|
"get_workflow_status",
|
|
445
680
|
"Get the current execution status of a workflow, including step-by-step progress.",
|
|
@@ -448,13 +683,17 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
448
683
|
},
|
|
449
684
|
async (args) => {
|
|
450
685
|
try {
|
|
451
|
-
const
|
|
686
|
+
const resolved = await resolveEntityId(workflows, workflows.id, args.workflowId);
|
|
687
|
+
if ("error" in resolved) return err(resolved.error);
|
|
688
|
+
const workflowId = resolved.id;
|
|
689
|
+
|
|
690
|
+
const workflow = db
|
|
452
691
|
.select()
|
|
453
692
|
.from(workflows)
|
|
454
|
-
.where(eq(workflows.id,
|
|
693
|
+
.where(eq(workflows.id, workflowId))
|
|
455
694
|
.get();
|
|
456
695
|
|
|
457
|
-
if (!workflow) return err(`Workflow not found: ${
|
|
696
|
+
if (!workflow) return err(`Workflow not found: ${workflowId}`);
|
|
458
697
|
|
|
459
698
|
const { parseWorkflowState } = await import("@/lib/workflows/engine");
|
|
460
699
|
const { definition, state } = parseWorkflowState(workflow.definition);
|
|
@@ -531,11 +770,15 @@ export function workflowTools(ctx: ToolContext) {
|
|
|
531
770
|
}
|
|
532
771
|
|
|
533
772
|
if (args.sourceWorkflowId) {
|
|
773
|
+
const resolvedSrc = await resolveEntityId(workflows, workflows.id, args.sourceWorkflowId);
|
|
774
|
+
if ("error" in resolvedSrc) return err(resolvedSrc.error);
|
|
775
|
+
const srcWorkflowId = resolvedSrc.id;
|
|
776
|
+
|
|
534
777
|
// Find task IDs belonging to the source workflow
|
|
535
778
|
const workflowTasks = await db
|
|
536
779
|
.select({ id: tasks.id })
|
|
537
780
|
.from(tasks)
|
|
538
|
-
.where(eq(tasks.workflowId,
|
|
781
|
+
.where(eq(tasks.workflowId, srcWorkflowId));
|
|
539
782
|
|
|
540
783
|
const taskIds = workflowTasks.map((t) => t.id);
|
|
541
784
|
if (taskIds.length > 0) {
|
package/src/lib/chat/types.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getRuntimeFeatures,
|
|
3
|
+
resolveAgentRuntime,
|
|
4
|
+
type RuntimeFeatures,
|
|
5
|
+
} from "@/lib/agents/runtime/catalog";
|
|
6
|
+
|
|
1
7
|
/** Screenshot attachment metadata stored in message metadata.attachments */
|
|
2
8
|
export interface ScreenshotAttachment {
|
|
3
9
|
documentId: string;
|
|
@@ -107,6 +113,15 @@ export function getRuntimeForModel(modelId: string): string {
|
|
|
107
113
|
return /^(gpt|o\d)/.test(modelId) ? "openai-codex-app-server" : "claude-code";
|
|
108
114
|
}
|
|
109
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Model → LLM-surface features. Thin wrapper around getRuntimeForModel +
|
|
118
|
+
* getRuntimeFeatures so chat callers don't need to know runtime IDs.
|
|
119
|
+
*/
|
|
120
|
+
export function getFeaturesForModel(modelId: string): RuntimeFeatures {
|
|
121
|
+
const runtimeId = resolveAgentRuntime(getRuntimeForModel(modelId));
|
|
122
|
+
return getRuntimeFeatures(runtimeId);
|
|
123
|
+
}
|
|
124
|
+
|
|
110
125
|
/** Suggested prompt category with expandable sub-prompts */
|
|
111
126
|
export interface PromptCategory {
|
|
112
127
|
id: string;
|
|
@@ -2,8 +2,12 @@ export const SETTINGS_KEYS = {
|
|
|
2
2
|
AUTH_METHOD: "auth.method",
|
|
3
3
|
AUTH_API_KEY: "auth.apiKey",
|
|
4
4
|
AUTH_API_KEY_SOURCE: "auth.apiKeySource",
|
|
5
|
+
OPENAI_AUTH_METHOD: "openai.authMethod",
|
|
5
6
|
OPENAI_AUTH_API_KEY: "openai.authApiKey",
|
|
6
7
|
OPENAI_AUTH_API_KEY_SOURCE: "openai.authApiKeySource",
|
|
8
|
+
OPENAI_AUTH_OAUTH_CONNECTED: "openai.oauthConnected",
|
|
9
|
+
OPENAI_AUTH_ACCOUNT: "openai.account",
|
|
10
|
+
OPENAI_AUTH_RATE_LIMITS: "openai.rateLimits",
|
|
7
11
|
PERMISSIONS_ALLOW: "permissions.allow",
|
|
8
12
|
BUDGET_POLICY: "usage.budgetPolicy",
|
|
9
13
|
BUDGET_WARNING_STATE: "usage.budgetWarningState",
|
|
@@ -19,24 +23,12 @@ export const SETTINGS_KEYS = {
|
|
|
19
23
|
ROUTING_PREFERENCE: "routing.preference",
|
|
20
24
|
OLLAMA_BASE_URL: "ollama.baseUrl",
|
|
21
25
|
OLLAMA_DEFAULT_MODEL: "ollama.defaultModel",
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// Supabase cloud
|
|
29
|
-
SUPABASE_URL: "cloud.supabaseUrl",
|
|
30
|
-
SUPABASE_ANON_KEY: "cloud.supabaseAnonKey",
|
|
31
|
-
// Telemetry (opt-in)
|
|
32
|
-
TELEMETRY_ENABLED: "telemetry.enabled",
|
|
33
|
-
TELEMETRY_RUNTIME_ID: "telemetry.runtimeId",
|
|
34
|
-
TELEMETRY_BATCH: "telemetry.batch",
|
|
35
|
-
// Cloud sync
|
|
36
|
-
DEVICE_ID: "sync.deviceId",
|
|
37
|
-
LAST_SYNC_AT: "sync.lastSyncAt",
|
|
38
|
-
// Stripe
|
|
39
|
-
STRIPE_CUSTOMER_ID: "billing.stripeCustomerId",
|
|
26
|
+
// Schedule orchestration
|
|
27
|
+
SCHEDULE_MAX_CONCURRENT: "schedule.maxConcurrent",
|
|
28
|
+
SCHEDULE_MAX_RUN_DURATION_SEC: "schedule.maxRunDurationSec",
|
|
29
|
+
SCHEDULE_CHAT_PRESSURE_DELAY_SEC: "schedule.chatPressureDelaySec",
|
|
30
|
+
// Environment / profile sync
|
|
31
|
+
AUTO_PROMOTE_SKILLS: "environment.autoPromoteSkills",
|
|
40
32
|
} as const;
|
|
41
33
|
|
|
42
34
|
export type RoutingPreference = "cost" | "latency" | "quality" | "manual";
|
|
@@ -2,10 +2,16 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
import { readFileSync } from "fs";
|
|
3
3
|
import { join } from "path";
|
|
4
4
|
import * as schema from "@/lib/db/schema";
|
|
5
|
+
import { db } from "@/lib/db";
|
|
6
|
+
import { conversations, documents } from "@/lib/db/schema";
|
|
7
|
+
import { clearAllData } from "../clear";
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* Safety-net test: every table exported from schema.ts must appear in clear.ts
|
|
8
|
-
* (except
|
|
11
|
+
* (except tables in INTENTIONALLY_PRESERVED, which are kept across clears:
|
|
12
|
+
* - settings: auth config
|
|
13
|
+
* - snapshots: backups, not working data
|
|
14
|
+
* - license: paid tier activation — clearing data must not silently downgrade)
|
|
9
15
|
*
|
|
10
16
|
* When you add a new table to schema.ts, this test will fail until you add a
|
|
11
17
|
* corresponding db.delete() call to clear.ts in the correct FK-safe order.
|
|
@@ -13,7 +19,7 @@ import * as schema from "@/lib/db/schema";
|
|
|
13
19
|
describe("clearAllData coverage", () => {
|
|
14
20
|
const INTENTIONALLY_PRESERVED = ["settings", "snapshots"];
|
|
15
21
|
|
|
16
|
-
it("deletes every schema table (except
|
|
22
|
+
it("deletes every schema table (except preserved ones)", () => {
|
|
17
23
|
const clearSource = readFileSync(
|
|
18
24
|
join(__dirname, "..", "clear.ts"),
|
|
19
25
|
"utf-8"
|
|
@@ -40,3 +46,51 @@ describe("clearAllData coverage", () => {
|
|
|
40
46
|
expect(missing, `Tables missing from clear.ts: ${missing.join(", ")}`).toEqual([]);
|
|
41
47
|
});
|
|
42
48
|
});
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* FK ordering regression: `documents.conversation_id` references `conversations.id`.
|
|
52
|
+
* If clearAllData deletes `conversations` before `documents`, SQLite raises
|
|
53
|
+
* FOREIGN KEY constraint failed. This test seeds a document attached to a
|
|
54
|
+
* conversation and then calls clearAllData to ensure the ordering holds.
|
|
55
|
+
*
|
|
56
|
+
* Incident: the stagent-growth domain clone (2026-04-07) hit this because its
|
|
57
|
+
* seeded data included chat-attached documents.
|
|
58
|
+
*/
|
|
59
|
+
describe("clearAllData FK ordering", () => {
|
|
60
|
+
it("clears a conversation that has an attached document without FK violation", () => {
|
|
61
|
+
const now = new Date();
|
|
62
|
+
const conversationId = "test-conv-fk-ordering";
|
|
63
|
+
const documentId = "test-doc-fk-ordering";
|
|
64
|
+
|
|
65
|
+
db.insert(conversations)
|
|
66
|
+
.values({
|
|
67
|
+
id: conversationId,
|
|
68
|
+
runtimeId: "test-runtime",
|
|
69
|
+
status: "active",
|
|
70
|
+
createdAt: now,
|
|
71
|
+
updatedAt: now,
|
|
72
|
+
})
|
|
73
|
+
.run();
|
|
74
|
+
|
|
75
|
+
db.insert(documents)
|
|
76
|
+
.values({
|
|
77
|
+
id: documentId,
|
|
78
|
+
filename: "fk-ordering-test.txt",
|
|
79
|
+
originalName: "fk-ordering-test.txt",
|
|
80
|
+
mimeType: "text/plain",
|
|
81
|
+
size: 10,
|
|
82
|
+
storagePath: "/tmp/fk-ordering-test.txt",
|
|
83
|
+
conversationId,
|
|
84
|
+
createdAt: now,
|
|
85
|
+
updatedAt: now,
|
|
86
|
+
})
|
|
87
|
+
.run();
|
|
88
|
+
|
|
89
|
+
expect(() => clearAllData()).not.toThrow();
|
|
90
|
+
|
|
91
|
+
const remainingConvs = db.select().from(conversations).all();
|
|
92
|
+
const remainingDocs = db.select().from(documents).all();
|
|
93
|
+
expect(remainingConvs).toHaveLength(0);
|
|
94
|
+
expect(remainingDocs).toHaveLength(0);
|
|
95
|
+
});
|
|
96
|
+
});
|