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.
- package/README.md +5 -42
- package/dist/cli.js +42 -18
- package/docs/.coverage-gaps.json +13 -55
- package/docs/.last-generated +1 -1
- package/docs/features/provider-runtimes.md +4 -0
- package/docs/features/schedules.md +32 -4
- package/docs/features/settings.md +28 -5
- package/docs/features/tables.md +9 -2
- package/docs/features/workflows.md +10 -4
- package/docs/journeys/developer.md +15 -1
- package/docs/journeys/personal-use.md +21 -4
- 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/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/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/diagnostics/chat-streams/route.ts +65 -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/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 +0 -21
- 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/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 +6 -9
- package/src/components/chat/__tests__/chat-session-provider.test.tsx +408 -0
- package/src/components/chat/chat-command-popover.tsx +2 -2
- package/src/components/chat/chat-input.tsx +2 -3
- package/src/components/chat/chat-session-provider.tsx +720 -0
- package/src/components/chat/chat-shell.tsx +92 -401
- 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/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/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/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/app-sidebar.tsx +4 -3
- package/src/components/shared/command-palette.tsx +4 -5
- 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 +19 -0
- package/src/components/tasks/task-card.tsx +26 -3
- package/src/components/tasks/task-chip-bar.tsx +24 -0
- 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/instrumentation-node.ts +94 -0
- package/src/instrumentation.ts +4 -48
- package/src/lib/agents/__tests__/claude-agent.test.ts +199 -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/claude-agent.ts +155 -18
- package/src/lib/agents/execution-manager.ts +0 -35
- package/src/lib/agents/learned-context.ts +0 -12
- package/src/lib/agents/learning-session.ts +18 -5
- package/src/lib/agents/profiles/__tests__/registry.test.ts +6 -4
- package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +70 -0
- package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +32 -0
- package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
- package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
- package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
- package/src/lib/agents/runtime/openai-codex.ts +29 -60
- package/src/lib/agents/runtime/types.ts +8 -0
- package/src/lib/book/chapter-mapping.ts +11 -0
- package/src/lib/book/content.ts +10 -0
- package/src/lib/chat/__tests__/active-streams.test.ts +49 -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__/stream-telemetry.test.ts +151 -0
- package/src/lib/chat/active-streams.ts +27 -0
- package/src/lib/chat/codex-engine.ts +16 -17
- package/src/lib/chat/context-builder.ts +5 -3
- package/src/lib/chat/engine.ts +50 -3
- package/src/lib/chat/reconcile.ts +117 -0
- package/src/lib/chat/stagent-tools.ts +1 -0
- package/src/lib/chat/stream-telemetry.ts +132 -0
- package/src/lib/chat/suggested-prompts.ts +28 -1
- package/src/lib/chat/system-prompt.ts +26 -1
- package/src/lib/chat/tool-catalog.ts +2 -1
- package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
- package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
- package/src/lib/chat/tools/__tests__/task-tools.test.ts +352 -0
- package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +217 -0
- package/src/lib/chat/tools/document-tools.ts +29 -13
- package/src/lib/chat/tools/helpers.ts +39 -0
- package/src/lib/chat/tools/notification-tools.ts +9 -5
- 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/table-tools.ts +71 -0
- package/src/lib/chat/tools/task-tools.ts +84 -20
- package/src/lib/chat/tools/workflow-tools.ts +234 -32
- package/src/lib/constants/settings.ts +8 -18
- package/src/lib/data/__tests__/clear.test.ts +56 -2
- package/src/lib/data/clear.ts +20 -15
- 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 +45 -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 +68 -23
- package/src/lib/environment/workspace-context.ts +13 -1
- 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 +131 -0
- package/src/lib/instance/bootstrap.ts +270 -0
- package/src/lib/instance/detect.ts +49 -0
- package/src/lib/instance/fingerprint.ts +78 -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 +153 -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 +232 -13
- 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/instantiator.ts +22 -1
- package/src/lib/workflows/blueprints/types.ts +10 -2
- package/src/lib/workflows/delay.ts +106 -0
- package/src/lib/workflows/engine.ts +207 -4
- 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/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
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
123
|
-
|
|
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
|
|
10
|
-
* properly handles all FK relationships
|
|
11
|
-
* This prevents
|
|
12
|
-
*
|
|
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, "..", "
|
|
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
|
|
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("
|
|
132
|
+
it("checks project existence before deleting", () => {
|
|
133
133
|
const deleteSection = deleteRouteSource.slice(
|
|
134
|
-
deleteRouteSource.indexOf("export
|
|
134
|
+
deleteRouteSource.indexOf("export function deleteProjectCascade")
|
|
135
135
|
);
|
|
136
|
-
|
|
137
|
-
expect(deleteSection).toContain("
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
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) {
|