stagent 0.9.5 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (277) hide show
  1. package/README.md +5 -42
  2. package/dist/cli.js +42 -18
  3. package/docs/.coverage-gaps.json +13 -55
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/provider-runtimes.md +4 -0
  6. package/docs/features/schedules.md +32 -4
  7. package/docs/features/settings.md +28 -5
  8. package/docs/features/tables.md +9 -2
  9. package/docs/features/workflows.md +10 -4
  10. package/docs/journeys/developer.md +15 -1
  11. package/docs/journeys/personal-use.md +21 -4
  12. package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
  13. package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
  14. package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
  15. package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
  16. package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
  17. package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
  18. package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
  19. package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
  20. package/package.json +3 -2
  21. package/src/__tests__/instrumentation-smoke.test.ts +15 -0
  22. package/src/app/analytics/page.tsx +1 -21
  23. package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
  24. package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
  25. package/src/app/api/instance/config/route.ts +41 -0
  26. package/src/app/api/instance/init/route.ts +34 -0
  27. package/src/app/api/instance/upgrade/check/route.ts +26 -0
  28. package/src/app/api/instance/upgrade/route.ts +96 -0
  29. package/src/app/api/instance/upgrade/status/route.ts +35 -0
  30. package/src/app/api/memory/route.ts +0 -11
  31. package/src/app/api/notifications/route.ts +4 -2
  32. package/src/app/api/projects/[id]/route.ts +5 -155
  33. package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
  34. package/src/app/api/schedules/[id]/execute/route.ts +111 -0
  35. package/src/app/api/schedules/[id]/route.ts +9 -1
  36. package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
  37. package/src/app/api/schedules/route.ts +3 -12
  38. package/src/app/api/settings/openai/login/route.ts +22 -0
  39. package/src/app/api/settings/openai/logout/route.ts +7 -0
  40. package/src/app/api/settings/openai/route.ts +21 -1
  41. package/src/app/api/settings/providers/route.ts +35 -8
  42. package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
  43. package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
  44. package/src/app/api/tables/[id]/enrich/route.ts +147 -0
  45. package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
  46. package/src/app/api/tasks/[id]/execute/route.ts +0 -21
  47. package/src/app/api/workflows/[id]/resume/route.ts +59 -0
  48. package/src/app/api/workflows/[id]/status/route.ts +22 -8
  49. package/src/app/api/workspace/context/route.ts +2 -0
  50. package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
  51. package/src/app/chat/page.tsx +11 -0
  52. package/src/app/inbox/page.tsx +12 -5
  53. package/src/app/layout.tsx +42 -21
  54. package/src/app/page.tsx +0 -2
  55. package/src/app/settings/page.tsx +6 -9
  56. package/src/components/chat/__tests__/chat-session-provider.test.tsx +408 -0
  57. package/src/components/chat/chat-command-popover.tsx +2 -2
  58. package/src/components/chat/chat-input.tsx +2 -3
  59. package/src/components/chat/chat-session-provider.tsx +720 -0
  60. package/src/components/chat/chat-shell.tsx +92 -401
  61. package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
  62. package/src/components/instance/instance-section.tsx +382 -0
  63. package/src/components/instance/upgrade-badge.tsx +219 -0
  64. package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
  65. package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
  66. package/src/components/notifications/batch-proposal-review.tsx +20 -5
  67. package/src/components/notifications/inbox-list.tsx +11 -2
  68. package/src/components/notifications/notification-item.tsx +56 -2
  69. package/src/components/notifications/pending-approval-host.tsx +56 -37
  70. package/src/components/schedules/schedule-create-sheet.tsx +19 -1
  71. package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
  72. package/src/components/schedules/schedule-form.tsx +31 -0
  73. package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
  74. package/src/components/settings/auth-method-selector.tsx +19 -4
  75. package/src/components/settings/auth-status-badge.tsx +28 -3
  76. package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
  77. package/src/components/settings/openai-runtime-section.tsx +7 -1
  78. package/src/components/settings/providers-runtimes-section.tsx +138 -19
  79. package/src/components/shared/app-sidebar.tsx +4 -3
  80. package/src/components/shared/command-palette.tsx +4 -5
  81. package/src/components/shared/theme-toggle.tsx +5 -24
  82. package/src/components/shared/workspace-indicator.tsx +61 -2
  83. package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
  84. package/src/components/tables/table-create-sheet.tsx +4 -0
  85. package/src/components/tables/table-enrichment-runs.tsx +103 -0
  86. package/src/components/tables/table-enrichment-sheet.tsx +538 -0
  87. package/src/components/tables/table-spreadsheet.tsx +29 -5
  88. package/src/components/tables/table-toolbar.tsx +10 -1
  89. package/src/components/tasks/kanban-board.tsx +1 -0
  90. package/src/components/tasks/kanban-column.tsx +53 -14
  91. package/src/components/tasks/task-bento-grid.tsx +19 -0
  92. package/src/components/tasks/task-card.tsx +26 -3
  93. package/src/components/tasks/task-chip-bar.tsx +24 -0
  94. package/src/components/tasks/task-result-renderer.tsx +1 -1
  95. package/src/components/workflows/delay-step-body.tsx +109 -0
  96. package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
  97. package/src/components/workflows/loop-status-view.tsx +1 -1
  98. package/src/components/workflows/shared/step-result.tsx +78 -0
  99. package/src/components/workflows/shared/workflow-header.tsx +141 -0
  100. package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
  101. package/src/components/workflows/swarm-dashboard.tsx +2 -15
  102. package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
  103. package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
  104. package/src/components/workflows/workflow-form-view.tsx +133 -16
  105. package/src/components/workflows/workflow-status-view.tsx +30 -740
  106. package/src/instrumentation-node.ts +94 -0
  107. package/src/instrumentation.ts +4 -48
  108. package/src/lib/agents/__tests__/claude-agent.test.ts +199 -0
  109. package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
  110. package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
  111. package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
  112. package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
  113. package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
  114. package/src/lib/agents/claude-agent.ts +155 -18
  115. package/src/lib/agents/execution-manager.ts +0 -35
  116. package/src/lib/agents/learned-context.ts +0 -12
  117. package/src/lib/agents/learning-session.ts +18 -5
  118. package/src/lib/agents/profiles/__tests__/registry.test.ts +6 -4
  119. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +70 -0
  120. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +32 -0
  121. package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
  122. package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
  123. package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
  124. package/src/lib/agents/runtime/openai-codex.ts +29 -60
  125. package/src/lib/agents/runtime/types.ts +8 -0
  126. package/src/lib/book/chapter-mapping.ts +11 -0
  127. package/src/lib/book/content.ts +10 -0
  128. package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
  129. package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
  130. package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
  131. package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
  132. package/src/lib/chat/active-streams.ts +27 -0
  133. package/src/lib/chat/codex-engine.ts +16 -17
  134. package/src/lib/chat/context-builder.ts +5 -3
  135. package/src/lib/chat/engine.ts +50 -3
  136. package/src/lib/chat/reconcile.ts +117 -0
  137. package/src/lib/chat/stagent-tools.ts +1 -0
  138. package/src/lib/chat/stream-telemetry.ts +132 -0
  139. package/src/lib/chat/suggested-prompts.ts +28 -1
  140. package/src/lib/chat/system-prompt.ts +26 -1
  141. package/src/lib/chat/tool-catalog.ts +2 -1
  142. package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
  143. package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
  144. package/src/lib/chat/tools/__tests__/task-tools.test.ts +352 -0
  145. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +217 -0
  146. package/src/lib/chat/tools/document-tools.ts +29 -13
  147. package/src/lib/chat/tools/helpers.ts +39 -0
  148. package/src/lib/chat/tools/notification-tools.ts +9 -5
  149. package/src/lib/chat/tools/project-tools.ts +33 -0
  150. package/src/lib/chat/tools/schedule-tools.ts +44 -11
  151. package/src/lib/chat/tools/table-tools.ts +71 -0
  152. package/src/lib/chat/tools/task-tools.ts +84 -20
  153. package/src/lib/chat/tools/workflow-tools.ts +234 -32
  154. package/src/lib/constants/settings.ts +8 -18
  155. package/src/lib/data/__tests__/clear.test.ts +56 -2
  156. package/src/lib/data/clear.ts +20 -15
  157. package/src/lib/data/delete-project.ts +171 -0
  158. package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
  159. package/src/lib/db/bootstrap.ts +45 -16
  160. package/src/lib/db/index.ts +5 -0
  161. package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
  162. package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
  163. package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
  164. package/src/lib/db/migrations/0026_drop_license.sql +3 -0
  165. package/src/lib/db/migrations/meta/_journal.json +21 -0
  166. package/src/lib/db/schema.ts +68 -23
  167. package/src/lib/environment/workspace-context.ts +13 -1
  168. package/src/lib/import/dedup.ts +4 -54
  169. package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
  170. package/src/lib/instance/__tests__/detect.test.ts +115 -0
  171. package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
  172. package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
  173. package/src/lib/instance/__tests__/settings.test.ts +83 -0
  174. package/src/lib/instance/__tests__/upgrade-poller.test.ts +131 -0
  175. package/src/lib/instance/bootstrap.ts +270 -0
  176. package/src/lib/instance/detect.ts +49 -0
  177. package/src/lib/instance/fingerprint.ts +78 -0
  178. package/src/lib/instance/git-ops.ts +95 -0
  179. package/src/lib/instance/settings.ts +61 -0
  180. package/src/lib/instance/types.ts +77 -0
  181. package/src/lib/instance/upgrade-poller.ts +153 -0
  182. package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
  183. package/src/lib/notifications/visibility.ts +33 -0
  184. package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
  185. package/src/lib/schedules/__tests__/config.test.ts +62 -0
  186. package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
  187. package/src/lib/schedules/__tests__/integration.test.ts +82 -0
  188. package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
  189. package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
  190. package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
  191. package/src/lib/schedules/collision-check.ts +105 -0
  192. package/src/lib/schedules/config.ts +53 -0
  193. package/src/lib/schedules/scheduler.ts +232 -13
  194. package/src/lib/schedules/slot-claim.ts +105 -0
  195. package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
  196. package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
  197. package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
  198. package/src/lib/settings/openai-auth.ts +105 -10
  199. package/src/lib/settings/openai-login-manager.ts +260 -0
  200. package/src/lib/settings/runtime-setup.ts +14 -4
  201. package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
  202. package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
  203. package/src/lib/tables/enrichment-planner.ts +454 -0
  204. package/src/lib/tables/enrichment.ts +328 -0
  205. package/src/lib/tables/query-builder.ts +5 -2
  206. package/src/lib/tables/trigger-evaluator.ts +3 -2
  207. package/src/lib/theme.ts +71 -0
  208. package/src/lib/usage/ledger.ts +2 -18
  209. package/src/lib/util/__tests__/similarity.test.ts +106 -0
  210. package/src/lib/util/similarity.ts +77 -0
  211. package/src/lib/utils/format-timestamp.ts +24 -0
  212. package/src/lib/utils/stagent-paths.ts +12 -0
  213. package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
  214. package/src/lib/validators/__tests__/settings.test.ts +10 -0
  215. package/src/lib/validators/blueprint.ts +70 -9
  216. package/src/lib/validators/profile.ts +2 -2
  217. package/src/lib/validators/settings.ts +3 -1
  218. package/src/lib/workflows/__tests__/delay.test.ts +196 -0
  219. package/src/lib/workflows/__tests__/engine.test.ts +8 -0
  220. package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
  221. package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
  222. package/src/lib/workflows/blueprints/instantiator.ts +22 -1
  223. package/src/lib/workflows/blueprints/types.ts +10 -2
  224. package/src/lib/workflows/delay.ts +106 -0
  225. package/src/lib/workflows/engine.ts +207 -4
  226. package/src/lib/workflows/loop-executor.ts +349 -24
  227. package/src/lib/workflows/post-action.ts +91 -0
  228. package/src/lib/workflows/types.ts +166 -1
  229. package/src/app/api/license/checkout/route.ts +0 -28
  230. package/src/app/api/license/portal/route.ts +0 -26
  231. package/src/app/api/license/route.ts +0 -89
  232. package/src/app/api/license/usage/route.ts +0 -63
  233. package/src/app/api/marketplace/browse/route.ts +0 -15
  234. package/src/app/api/marketplace/import/route.ts +0 -28
  235. package/src/app/api/marketplace/publish/route.ts +0 -40
  236. package/src/app/api/onboarding/email/route.ts +0 -53
  237. package/src/app/api/settings/telemetry/route.ts +0 -14
  238. package/src/app/api/sync/export/route.ts +0 -54
  239. package/src/app/api/sync/restore/route.ts +0 -37
  240. package/src/app/api/sync/sessions/route.ts +0 -24
  241. package/src/app/auth/callback/route.ts +0 -73
  242. package/src/app/marketplace/page.tsx +0 -19
  243. package/src/components/analytics/analytics-gate-card.tsx +0 -101
  244. package/src/components/marketplace/blueprint-card.tsx +0 -61
  245. package/src/components/marketplace/marketplace-browser.tsx +0 -131
  246. package/src/components/onboarding/email-capture-card.tsx +0 -104
  247. package/src/components/settings/activation-form.tsx +0 -95
  248. package/src/components/settings/cloud-account-section.tsx +0 -147
  249. package/src/components/settings/cloud-sync-section.tsx +0 -155
  250. package/src/components/settings/subscription-section.tsx +0 -410
  251. package/src/components/settings/telemetry-section.tsx +0 -80
  252. package/src/components/shared/premium-gate-overlay.tsx +0 -50
  253. package/src/components/shared/schedule-gate-dialog.tsx +0 -64
  254. package/src/components/shared/upgrade-banner.tsx +0 -112
  255. package/src/hooks/use-supabase-auth.ts +0 -79
  256. package/src/lib/billing/email.ts +0 -54
  257. package/src/lib/billing/products.ts +0 -80
  258. package/src/lib/billing/stripe.ts +0 -101
  259. package/src/lib/cloud/supabase-browser.ts +0 -32
  260. package/src/lib/cloud/supabase-client.ts +0 -56
  261. package/src/lib/license/__tests__/features.test.ts +0 -56
  262. package/src/lib/license/__tests__/key-format.test.ts +0 -88
  263. package/src/lib/license/__tests__/manager.test.ts +0 -64
  264. package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
  265. package/src/lib/license/cloud-validation.ts +0 -60
  266. package/src/lib/license/features.ts +0 -44
  267. package/src/lib/license/key-format.ts +0 -101
  268. package/src/lib/license/limit-check.ts +0 -111
  269. package/src/lib/license/limit-queries.ts +0 -51
  270. package/src/lib/license/manager.ts +0 -345
  271. package/src/lib/license/notifications.ts +0 -59
  272. package/src/lib/license/tier-limits.ts +0 -71
  273. package/src/lib/marketplace/marketplace-client.ts +0 -107
  274. package/src/lib/sync/cloud-sync.ts +0 -235
  275. package/src/lib/telemetry/conversion-events.ts +0 -71
  276. package/src/lib/telemetry/queue.ts +0 -122
  277. package/src/lib/validators/license.ts +0 -33
@@ -3,9 +3,6 @@ import { db } from "@/lib/db";
3
3
  import { agentMemory } from "@/lib/db/schema";
4
4
  import { and, eq, desc } from "drizzle-orm";
5
5
  import { randomUUID } from "crypto";
6
- import { checkLimit, buildLimitErrorBody } from "@/lib/license/limit-check";
7
- import { getMemoryCount } from "@/lib/license/limit-queries";
8
- import { createTierLimitNotification } from "@/lib/license/notifications";
9
6
 
10
7
  /**
11
8
  * GET /api/memory?profileId=xxx&category=fact&status=active
@@ -78,14 +75,6 @@ export async function POST(req: NextRequest) {
78
75
  );
79
76
  }
80
77
 
81
- // Tier limit check — memory cap per profile
82
- const currentCount = getMemoryCount(profileId);
83
- const limitResult = checkLimit("agentMemories", currentCount);
84
- if (!limitResult.allowed) {
85
- createTierLimitNotification("agentMemories", currentCount, limitResult.limit).catch(() => {});
86
- return NextResponse.json(buildLimitErrorBody("agentMemories", limitResult), { status: 402 });
87
- }
88
-
89
78
  const now = new Date();
90
79
  const id = randomUUID();
91
80
  // Convert 0-1 confidence to 0-1000, default 700
@@ -1,7 +1,9 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
2
  import { db } from "@/lib/db";
3
3
  import { notifications } from "@/lib/db/schema";
4
- import { eq, and, desc, sql, count } from "drizzle-orm";
4
+ import { eq, and, desc, count } from "drizzle-orm";
5
+
6
+ import { buildDefaultNotificationVisibilityCondition } from "@/lib/notifications/visibility";
5
7
 
6
8
  export async function GET(req: NextRequest) {
7
9
  const url = new URL(req.url);
@@ -9,7 +11,7 @@ export async function GET(req: NextRequest) {
9
11
  const type = url.searchParams.get("type");
10
12
  const countOnly = url.searchParams.get("countOnly");
11
13
 
12
- const conditions = [];
14
+ const conditions = [buildDefaultNotificationVisibilityCondition()];
13
15
  if (unread === "true") conditions.push(eq(notifications.read, false));
14
16
  if (type) conditions.push(eq(notifications.type, type as typeof notifications.type.enumValues[number]));
15
17
 
@@ -2,36 +2,11 @@ import { NextRequest, NextResponse } from "next/server";
2
2
  import { db } from "@/lib/db";
3
3
  import {
4
4
  projects,
5
- tasks,
6
- workflows,
7
- documents,
8
- schedules,
9
- agentLogs,
10
- notifications,
11
- learnedContext,
12
- usageLedger,
13
- environmentSyncOps,
14
- environmentCheckpoints,
15
- environmentArtifacts,
16
- environmentScans,
17
- chatMessages,
18
- conversations,
19
5
  projectDocumentDefaults,
20
- userTables,
21
- userTableColumns,
22
- userTableRows,
23
- userTableViews,
24
- userTableImports,
25
- userTableRelationships,
26
- tableDocumentInputs,
27
- taskTableInputs,
28
- workflowTableInputs,
29
- scheduleTableInputs,
30
- userTableTriggers,
31
- userTableRowHistory,
32
6
  } from "@/lib/db/schema";
33
- import { eq, inArray } from "drizzle-orm";
7
+ import { eq } from "drizzle-orm";
34
8
  import { updateProjectSchema } from "@/lib/validators/project";
9
+ import { deleteProjectCascade } from "@/lib/data/delete-project";
35
10
 
36
11
  export async function GET(
37
12
  _req: NextRequest,
@@ -109,137 +84,12 @@ export async function DELETE(
109
84
  { params }: { params: Promise<{ id: string }> }
110
85
  ) {
111
86
  const { id } = await params;
112
- const [existing] = await db
113
- .select()
114
- .from(projects)
115
- .where(eq(projects.id, id));
116
-
117
- if (!existing) {
118
- return NextResponse.json({ error: "Not found" }, { status: 404 });
119
- }
120
87
 
121
88
  try {
122
- // Cascade-delete in FK-safe order (children before parents)
123
- // Follows the same pattern as clear.ts and workflow DELETE
124
-
125
- // 1. Collect child IDs for nested FK chains
126
- const taskIds = db
127
- .select({ id: tasks.id })
128
- .from(tasks)
129
- .where(eq(tasks.projectId, id))
130
- .all()
131
- .map((r) => r.id);
132
-
133
- const workflowIds = db
134
- .select({ id: workflows.id })
135
- .from(workflows)
136
- .where(eq(workflows.projectId, id))
137
- .all()
138
- .map((r) => r.id);
139
-
140
- const conversationIds = db
141
- .select({ id: conversations.id })
142
- .from(conversations)
143
- .where(eq(conversations.projectId, id))
144
- .all()
145
- .map((r) => r.id);
146
-
147
- const scanIds = db
148
- .select({ id: environmentScans.id })
149
- .from(environmentScans)
150
- .where(eq(environmentScans.projectId, id))
151
- .all()
152
- .map((r) => r.id);
153
-
154
- const checkpointIds = db
155
- .select({ id: environmentCheckpoints.id })
156
- .from(environmentCheckpoints)
157
- .where(eq(environmentCheckpoints.projectId, id))
158
- .all()
159
- .map((r) => r.id);
160
-
161
- // 2. Environment tables (deepest children first)
162
- if (checkpointIds.length > 0) {
163
- db.delete(environmentSyncOps)
164
- .where(inArray(environmentSyncOps.checkpointId, checkpointIds))
165
- .run();
166
- db.delete(environmentCheckpoints)
167
- .where(inArray(environmentCheckpoints.id, checkpointIds))
168
- .run();
169
- }
170
- if (scanIds.length > 0) {
171
- db.delete(environmentArtifacts)
172
- .where(inArray(environmentArtifacts.scanId, scanIds))
173
- .run();
174
- db.delete(environmentScans)
175
- .where(inArray(environmentScans.id, scanIds))
176
- .run();
89
+ const deleted = deleteProjectCascade(id);
90
+ if (!deleted) {
91
+ return NextResponse.json({ error: "Not found" }, { status: 404 });
177
92
  }
178
-
179
- // 3. Chat tables (messages before conversations)
180
- if (conversationIds.length > 0) {
181
- db.delete(chatMessages)
182
- .where(inArray(chatMessages.conversationId, conversationIds))
183
- .run();
184
- db.delete(conversations)
185
- .where(inArray(conversations.id, conversationIds))
186
- .run();
187
- }
188
-
189
- // 4. Usage ledger (references projectId, workflowId, taskId)
190
- db.delete(usageLedger).where(eq(usageLedger.projectId, id)).run();
191
-
192
- // 5. Task children (logs, notifications, documents, learned context)
193
- if (taskIds.length > 0) {
194
- db.delete(agentLogs).where(inArray(agentLogs.taskId, taskIds)).run();
195
- db.delete(notifications)
196
- .where(inArray(notifications.taskId, taskIds))
197
- .run();
198
- db.delete(documents).where(inArray(documents.taskId, taskIds)).run();
199
- db.delete(learnedContext)
200
- .where(inArray(learnedContext.sourceTaskId, taskIds))
201
- .run();
202
- }
203
-
204
- // 6. Project document defaults (junction table)
205
- db.delete(projectDocumentDefaults).where(eq(projectDocumentDefaults.projectId, id)).run();
206
-
207
- // 6b. User-defined tables — cascade-delete children before parent
208
- const tableIds = db
209
- .select({ id: userTables.id })
210
- .from(userTables)
211
- .where(eq(userTables.projectId, id))
212
- .all()
213
- .map((r) => r.id);
214
-
215
- if (tableIds.length > 0) {
216
- // Junction tables first
217
- db.delete(tableDocumentInputs).where(inArray(tableDocumentInputs.tableId, tableIds)).run();
218
- db.delete(taskTableInputs).where(inArray(taskTableInputs.tableId, tableIds)).run();
219
- db.delete(workflowTableInputs).where(inArray(workflowTableInputs.tableId, tableIds)).run();
220
- db.delete(scheduleTableInputs).where(inArray(scheduleTableInputs.tableId, tableIds)).run();
221
- // Children
222
- db.delete(userTableRowHistory).where(inArray(userTableRowHistory.tableId, tableIds)).run();
223
- db.delete(userTableTriggers).where(inArray(userTableTriggers.tableId, tableIds)).run();
224
- db.delete(userTableImports).where(inArray(userTableImports.tableId, tableIds)).run();
225
- db.delete(userTableViews).where(inArray(userTableViews.tableId, tableIds)).run();
226
- db.delete(userTableRelationships).where(inArray(userTableRelationships.fromTableId, tableIds)).run();
227
- db.delete(userTableRows).where(inArray(userTableRows.tableId, tableIds)).run();
228
- db.delete(userTableColumns).where(inArray(userTableColumns.tableId, tableIds)).run();
229
- db.delete(userTables).where(inArray(userTables.id, tableIds)).run();
230
- }
231
-
232
- // 7. Direct project children
233
- db.delete(documents).where(eq(documents.projectId, id)).run();
234
- db.delete(tasks).where(eq(tasks.projectId, id)).run();
235
- if (workflowIds.length > 0) {
236
- db.delete(workflows).where(inArray(workflows.id, workflowIds)).run();
237
- }
238
- db.delete(schedules).where(eq(schedules.projectId, id)).run();
239
-
240
- // 7. Finally delete the project
241
- db.delete(projects).where(eq(projects.id, id)).run();
242
-
243
93
  return NextResponse.json({ success: true });
244
94
  } catch (err) {
245
95
  console.error("Project delete failed:", err);
@@ -6,14 +6,14 @@ import * as schema from "@/lib/db/schema";
6
6
  /**
7
7
  * Safety-net regression tests for project cascade deletion.
8
8
  *
9
- * These verify that the DELETE handler in projects/[id]/route.ts
10
- * properly handles all FK relationships before deleting a project.
11
- * This prevents the "Failed to delete project" FK constraint error
12
- * that occurs when related records exist.
9
+ * These verify that the shared deleteProjectCascade function in
10
+ * src/lib/data/delete-project.ts properly handles all FK relationships
11
+ * before deleting a project. This prevents "Failed to delete project"
12
+ * FK constraint errors when related records exist.
13
13
  */
14
14
  describe("project DELETE cascade coverage", () => {
15
15
  const deleteRouteSource = readFileSync(
16
- join(__dirname, "..", "[id]", "route.ts"),
16
+ join(__dirname, "..", "..", "..", "..", "lib", "data", "delete-project.ts"),
17
17
  "utf-8"
18
18
  );
19
19
 
@@ -115,7 +115,7 @@ describe("project DELETE cascade coverage", () => {
115
115
  // Find the LAST occurrence of db.delete(child) and FIRST occurrence of db.delete(parent)
116
116
  // within the DELETE function (not the import section)
117
117
  const deleteSection = deleteRouteSource.slice(
118
- deleteRouteSource.indexOf("export async function DELETE")
118
+ deleteRouteSource.indexOf("export function deleteProjectCascade")
119
119
  );
120
120
  const childPos = deleteSection.lastIndexOf(`db.delete(${child})`);
121
121
  const parentPos = deleteSection.indexOf(`db.delete(${parent})`);
@@ -129,21 +129,12 @@ describe("project DELETE cascade coverage", () => {
129
129
  ).toEqual([]);
130
130
  });
131
131
 
132
- it("wraps deletion in try/catch for error handling", () => {
132
+ it("checks project existence before deleting", () => {
133
133
  const deleteSection = deleteRouteSource.slice(
134
- deleteRouteSource.indexOf("export async function DELETE")
134
+ deleteRouteSource.indexOf("export function deleteProjectCascade")
135
135
  );
136
- expect(deleteSection).toContain("try {");
137
- expect(deleteSection).toContain("catch");
138
- expect(deleteSection).toContain("status: 500");
139
- });
140
-
141
- it("verifies project exists before attempting delete", () => {
142
- const deleteSection = deleteRouteSource.slice(
143
- deleteRouteSource.indexOf("export async function DELETE")
144
- );
145
- expect(deleteSection).toContain("Not found");
146
- expect(deleteSection).toContain("status: 404");
136
+ // The shared function checks if the project exists and returns false if not
137
+ expect(deleteSection).toContain("if (!existing) return false");
147
138
  });
148
139
 
149
140
  /**
@@ -0,0 +1,111 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import { schedules, tasks, usageLedger } from "@/lib/db/schema";
4
+ import { eq } from "drizzle-orm";
5
+ import { executeTaskWithRuntime } from "@/lib/agents/runtime";
6
+ import { claimSlot, countRunningScheduledSlots } from "@/lib/schedules/slot-claim";
7
+ import {
8
+ getScheduleMaxConcurrent,
9
+ getScheduleMaxRunDurationSec,
10
+ } from "@/lib/schedules/config";
11
+ import { randomUUID } from "crypto";
12
+
13
+ /**
14
+ * Manually fire a schedule. Honors the global concurrency cap by default.
15
+ * Use `?force=true` to bypass the cap (logged to usage_ledger as
16
+ * "manual_force_bypass" for audit).
17
+ *
18
+ * Security note: force bypass is audit-logged synchronously before task
19
+ * execution begins, so every bypass leaves a permanent record regardless of
20
+ * task outcome.
21
+ */
22
+ export async function POST(
23
+ req: NextRequest,
24
+ { params }: { params: Promise<{ id: string }> },
25
+ ) {
26
+ const { id: scheduleId } = await params;
27
+ const force = req.nextUrl.searchParams.get("force") === "true";
28
+
29
+ const [schedule] = db
30
+ .select()
31
+ .from(schedules)
32
+ .where(eq(schedules.id, scheduleId))
33
+ .all();
34
+ if (!schedule) {
35
+ return NextResponse.json({ error: "schedule_not_found" }, { status: 404 });
36
+ }
37
+
38
+ const taskId = randomUUID();
39
+ const firingNumber = schedule.firingCount + 1;
40
+ const now = new Date();
41
+
42
+ db.insert(tasks)
43
+ .values({
44
+ id: taskId,
45
+ projectId: schedule.projectId,
46
+ workflowId: null,
47
+ scheduleId: schedule.id,
48
+ title: `${schedule.name} — manual firing #${firingNumber}`,
49
+ description: schedule.prompt,
50
+ status: "queued",
51
+ assignedAgent: schedule.assignedAgent,
52
+ agentProfile: schedule.agentProfile,
53
+ priority: 2,
54
+ sourceType: "scheduled",
55
+ maxTurns: schedule.maxTurns,
56
+ createdAt: now,
57
+ updatedAt: now,
58
+ })
59
+ .run();
60
+
61
+ const cap = getScheduleMaxConcurrent();
62
+ const leaseSec = schedule.maxRunDurationSec ?? getScheduleMaxRunDurationSec();
63
+
64
+ // When force=true, pass an effectively infinite cap so the subquery COUNT
65
+ // can never exceed it. This lets `claimSlot` atomically transition the task
66
+ // to "running" even when the real cap is full.
67
+ const effectiveCap = force ? Number.MAX_SAFE_INTEGER : cap;
68
+ const { claimed } = claimSlot(taskId, effectiveCap, leaseSec);
69
+
70
+ if (!claimed) {
71
+ db.delete(tasks).where(eq(tasks.id, taskId)).run();
72
+ const slotEtaSec = 60;
73
+ return NextResponse.json(
74
+ {
75
+ error: "capacity_full",
76
+ message: `Swarm at capacity (${countRunningScheduledSlots()}/${cap}). Retry in ~${slotEtaSec}s or add ?force=true to bypass.`,
77
+ slotEtaSec,
78
+ },
79
+ { status: 429 },
80
+ );
81
+ }
82
+
83
+ // Audit log written synchronously before task execution so that a force
84
+ // bypass is always recorded even if the task itself fails immediately.
85
+ if (force) {
86
+ const nowTs = new Date();
87
+ db.insert(usageLedger)
88
+ .values({
89
+ id: randomUUID(),
90
+ taskId,
91
+ scheduleId: schedule.id,
92
+ projectId: schedule.projectId,
93
+ activityType: "manual_force_bypass",
94
+ runtimeId: "manual",
95
+ providerId: "manual",
96
+ status: "completed",
97
+ costMicros: 0,
98
+ startedAt: nowTs,
99
+ finishedAt: nowTs,
100
+ })
101
+ .run();
102
+ }
103
+
104
+ // Fire-and-forget: the route returns immediately with taskId; execution runs
105
+ // in the background. Errors are logged but do not affect the 200 response.
106
+ executeTaskWithRuntime(taskId).catch((err) => {
107
+ console.error(`[api/schedules/execute] task ${taskId} failed:`, err);
108
+ });
109
+
110
+ return NextResponse.json({ taskId, forced: force });
111
+ }
@@ -4,6 +4,7 @@ import { schedules, tasks } from "@/lib/db/schema";
4
4
  import { eq, like } from "drizzle-orm";
5
5
  import { parseInterval, computeNextFireTime } from "@/lib/schedules/interval-parser";
6
6
  import { parseNaturalLanguage } from "@/lib/schedules/nlp-parser";
7
+ import { checkCollision } from "@/lib/schedules/collision-check";
7
8
  import { resolveAgentRuntime } from "@/lib/agents/runtime/catalog";
8
9
  import { validateRuntimeProfileAssignment } from "@/lib/agents/profiles/assignment-validation";
9
10
 
@@ -199,7 +200,14 @@ export async function PATCH(
199
200
  .from(schedules)
200
201
  .where(eq(schedules.id, id));
201
202
 
202
- return NextResponse.json(updated);
203
+ const effectiveCron = (updates.cronExpression as string | undefined) ?? schedule.cronExpression;
204
+ const warnings = checkCollision(
205
+ effectiveCron,
206
+ schedule.avgTurnsPerFiring ?? 0,
207
+ schedule.projectId ?? null,
208
+ schedule.id,
209
+ );
210
+ return NextResponse.json({ schedule: updated, warnings });
203
211
  }
204
212
 
205
213
  export async function DELETE(
@@ -0,0 +1,118 @@
1
+ import { describe, it, expect, beforeEach, vi } from "vitest";
2
+ import { db } from "@/lib/db";
3
+ import { tasks, schedules, projects, settings, usageLedger } from "@/lib/db/schema";
4
+ import { eq } from "drizzle-orm";
5
+ import { randomUUID } from "crypto";
6
+ import { NextRequest } from "next/server";
7
+ import { POST } from "../[id]/execute/route";
8
+
9
+ vi.mock("@/lib/agents/runtime", () => ({
10
+ executeTaskWithRuntime: vi.fn().mockResolvedValue(undefined),
11
+ }));
12
+
13
+ function req(url: string): NextRequest {
14
+ return new NextRequest(new URL(url, "http://localhost"));
15
+ }
16
+
17
+ function seedSchedule(): string {
18
+ const pid = randomUUID();
19
+ const sid = randomUUID();
20
+ const now = new Date();
21
+ db.insert(projects)
22
+ .values({ id: pid, name: "p", status: "active", createdAt: now, updatedAt: now })
23
+ .run();
24
+ db.insert(schedules)
25
+ .values({
26
+ id: sid,
27
+ projectId: pid,
28
+ name: "manual",
29
+ prompt: "test",
30
+ cronExpression: "0 0 * * *",
31
+ status: "active",
32
+ type: "scheduled",
33
+ firingCount: 0,
34
+ suppressionCount: 0,
35
+ heartbeatSpentToday: 0,
36
+ failureStreak: 0,
37
+ turnBudgetBreachStreak: 0,
38
+ createdAt: now,
39
+ updatedAt: now,
40
+ })
41
+ .run();
42
+ return sid;
43
+ }
44
+
45
+ describe("POST /api/schedules/:id/execute", () => {
46
+ beforeEach(() => {
47
+ db.delete(usageLedger).run();
48
+ db.delete(tasks).run();
49
+ db.delete(schedules).run();
50
+ db.delete(projects).run();
51
+ db.delete(settings).where(eq(settings.key, "schedule.maxConcurrent")).run();
52
+ db.insert(settings)
53
+ .values({ key: "schedule.maxConcurrent", value: "1", updatedAt: new Date() })
54
+ .run();
55
+ });
56
+
57
+ it("fires when capacity available, returns 200 with taskId", async () => {
58
+ const sid = seedSchedule();
59
+ const res = await POST(req(`/api/schedules/${sid}/execute`), {
60
+ params: Promise.resolve({ id: sid }),
61
+ });
62
+ expect(res.status).toBe(200);
63
+ const body = await res.json();
64
+ expect(body.taskId).toBeDefined();
65
+ });
66
+
67
+ it("returns 429 when cap is full", async () => {
68
+ const sid1 = seedSchedule();
69
+ const sid2 = seedSchedule();
70
+
71
+ const res1 = await POST(req(`/api/schedules/${sid1}/execute`), {
72
+ params: Promise.resolve({ id: sid1 }),
73
+ });
74
+ expect(res1.status).toBe(200);
75
+
76
+ const res2 = await POST(req(`/api/schedules/${sid2}/execute`), {
77
+ params: Promise.resolve({ id: sid2 }),
78
+ });
79
+ expect(res2.status).toBe(429);
80
+ const body = await res2.json();
81
+ expect(body.error).toBe("capacity_full");
82
+ expect(body.slotEtaSec).toBeGreaterThanOrEqual(0);
83
+
84
+ const remaining = db.select().from(tasks).all();
85
+ expect(remaining.length).toBe(1); // only sid1's task remains; sid2's was cleaned up on refusal
86
+ });
87
+
88
+ it("bypasses the cap when ?force=true and writes audit-log entry", async () => {
89
+ const sid1 = seedSchedule();
90
+ const sid2 = seedSchedule();
91
+
92
+ await POST(req(`/api/schedules/${sid1}/execute`), {
93
+ params: Promise.resolve({ id: sid1 }),
94
+ });
95
+
96
+ const res2 = await POST(
97
+ req(`/api/schedules/${sid2}/execute?force=true`),
98
+ { params: Promise.resolve({ id: sid2 }) },
99
+ );
100
+ expect(res2.status).toBe(200);
101
+ const body2 = await res2.json();
102
+
103
+ const ledger = db
104
+ .select()
105
+ .from(usageLedger)
106
+ .where(eq(usageLedger.activityType, "manual_force_bypass"))
107
+ .all();
108
+ expect(ledger.length).toBe(1);
109
+ expect(ledger[0].taskId).toBe(body2.taskId);
110
+ });
111
+
112
+ it("returns 404 when the schedule does not exist", async () => {
113
+ const res = await POST(req("/api/schedules/nonexistent/execute"), {
114
+ params: Promise.resolve({ id: "nonexistent" }),
115
+ });
116
+ expect(res.status).toBe(404);
117
+ });
118
+ });
@@ -6,9 +6,7 @@ import { parseInterval, computeNextFireTime } from "@/lib/schedules/interval-par
6
6
  import { parseNaturalLanguage } from "@/lib/schedules/nlp-parser";
7
7
  import { resolveAgentRuntime } from "@/lib/agents/runtime/catalog";
8
8
  import { validateRuntimeProfileAssignment } from "@/lib/agents/profiles/assignment-validation";
9
- import { checkLimit, buildLimitErrorBody } from "@/lib/license/limit-check";
10
- import { getActiveScheduleCount } from "@/lib/license/limit-queries";
11
- import { createTierLimitNotification } from "@/lib/license/notifications";
9
+ import { checkCollision } from "@/lib/schedules/collision-check";
12
10
 
13
11
  export async function GET() {
14
12
  const result = await db
@@ -130,14 +128,6 @@ export async function POST(req: NextRequest) {
130
128
  return NextResponse.json({ error: compatibilityError }, { status: 400 });
131
129
  }
132
130
 
133
- // Tier limit check — active schedule cap
134
- const activeCount = getActiveScheduleCount();
135
- const limitResult = checkLimit("activeSchedules", activeCount);
136
- if (!limitResult.allowed) {
137
- createTierLimitNotification("activeSchedules", activeCount, limitResult.limit).catch(() => {});
138
- return NextResponse.json(buildLimitErrorBody("activeSchedules", limitResult), { status: 402 });
139
- }
140
-
141
131
  const id = crypto.randomUUID();
142
132
  const now = new Date();
143
133
  const nextFireAt = computeNextFireTime(cronExpression, now);
@@ -201,5 +191,6 @@ export async function POST(req: NextRequest) {
201
191
  .from(schedules)
202
192
  .where(eq(schedules.id, id));
203
193
 
204
- return NextResponse.json(created, { status: 201 });
194
+ const warnings = checkCollision(cronExpression, 0, projectId ?? null, null);
195
+ return NextResponse.json({ schedule: created, warnings }, { status: 201 });
205
196
  }
@@ -0,0 +1,22 @@
1
+ import { NextResponse } from "next/server";
2
+ import {
3
+ cancelOpenAIChatGPTLogin,
4
+ getOpenAILoginState,
5
+ startOpenAIChatGPTLogin,
6
+ } from "@/lib/settings/openai-login-manager";
7
+ import { setOpenAIAuthSettings } from "@/lib/settings/openai-auth";
8
+
9
+ export async function GET() {
10
+ return NextResponse.json(getOpenAILoginState());
11
+ }
12
+
13
+ export async function POST() {
14
+ await setOpenAIAuthSettings({ method: "oauth" });
15
+ const state = await startOpenAIChatGPTLogin();
16
+ return NextResponse.json(state);
17
+ }
18
+
19
+ export async function DELETE() {
20
+ const state = await cancelOpenAIChatGPTLogin();
21
+ return NextResponse.json(state);
22
+ }
@@ -0,0 +1,7 @@
1
+ import { NextResponse } from "next/server";
2
+ import { logoutStagentCodexAuth } from "@/lib/agents/runtime/openai-codex-auth";
3
+
4
+ export async function POST() {
5
+ await logoutStagentCodexAuth();
6
+ return NextResponse.json({ success: true });
7
+ }
@@ -3,11 +3,31 @@ import {
3
3
  getOpenAIAuthSettings,
4
4
  setOpenAIAuthSettings,
5
5
  } from "@/lib/settings/openai-auth";
6
+ import { readStagentCodexAuthState } from "@/lib/agents/runtime/openai-codex-auth";
6
7
  import { updateOpenAISettingsSchema } from "@/lib/validators/settings";
7
8
 
8
9
  export async function GET() {
9
10
  const settings = await getOpenAIAuthSettings();
10
- return NextResponse.json(settings);
11
+ if (settings.method !== "oauth") {
12
+ return NextResponse.json(settings);
13
+ }
14
+
15
+ try {
16
+ const current = await readStagentCodexAuthState({ refreshToken: true });
17
+ return NextResponse.json({
18
+ ...settings,
19
+ oauthConnected: current.connected,
20
+ account: current.account,
21
+ rateLimits: current.rateLimits,
22
+ });
23
+ } catch {
24
+ return NextResponse.json({
25
+ ...settings,
26
+ oauthConnected: false,
27
+ account: null,
28
+ rateLimits: null,
29
+ });
30
+ }
11
31
  }
12
32
 
13
33
  export async function POST(req: NextRequest) {