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,10 +3,18 @@ import type { ProfileTestReport } from "@/lib/agents/profiles/test-types";
|
|
|
3
3
|
import type { RuntimeCapabilities, RuntimeCatalogEntry } from "./catalog";
|
|
4
4
|
import type { TaskAssistResponse } from "./task-assist-types";
|
|
5
5
|
import type { ProfileAssistRequest, ProfileAssistResponse } from "./profile-assist-types";
|
|
6
|
+
import type {
|
|
7
|
+
OpenAIAccountInfo,
|
|
8
|
+
OpenAIAuthMode,
|
|
9
|
+
OpenAIRateLimitInfo,
|
|
10
|
+
} from "@/lib/settings/openai-auth";
|
|
6
11
|
|
|
7
12
|
export interface RuntimeConnectionResult {
|
|
8
13
|
connected: boolean;
|
|
9
14
|
apiKeySource?: ApiKeySource;
|
|
15
|
+
account?: OpenAIAccountInfo | null;
|
|
16
|
+
rateLimits?: OpenAIRateLimitInfo | null;
|
|
17
|
+
authMode?: OpenAIAuthMode;
|
|
10
18
|
error?: string;
|
|
11
19
|
}
|
|
12
20
|
|
|
@@ -19,6 +19,7 @@ export const CHAPTER_SLUGS: Record<string, string> = {
|
|
|
19
19
|
"ch-11": "ch-11-the-machine-that-builds-machines",
|
|
20
20
|
"ch-12": "ch-12-the-road-ahead",
|
|
21
21
|
"ch-13": "ch-13-the-wealth-manager",
|
|
22
|
+
"ch-14": "ch-14-the-meta-program",
|
|
22
23
|
};
|
|
23
24
|
|
|
24
25
|
interface ChapterMapping {
|
|
@@ -105,6 +106,16 @@ export const CHAPTER_MAPPING: Record<string, ChapterMapping> = {
|
|
|
105
106
|
sourceFiles: ["src/lib/workflows/engine.ts", "src/lib/schedules/scheduler.ts", "src/lib/agents/profiles/registry.ts"],
|
|
106
107
|
caseStudies: ["making-machine-that-builds-machines"],
|
|
107
108
|
},
|
|
109
|
+
"ch-14": {
|
|
110
|
+
docs: ["workflows", "profiles", "schedules", "blueprints"],
|
|
111
|
+
sourceFiles: [
|
|
112
|
+
"src/lib/agents/profiles/registry.ts",
|
|
113
|
+
"src/lib/workflows/blueprints/registry.ts",
|
|
114
|
+
"src/lib/workflows/engine.ts",
|
|
115
|
+
"features/instance-bootstrap.md",
|
|
116
|
+
],
|
|
117
|
+
caseStudies: ["making-machine-that-builds-machines"],
|
|
118
|
+
},
|
|
108
119
|
};
|
|
109
120
|
|
|
110
121
|
/** Get related Playbook doc slugs for a chapter */
|
package/src/lib/book/content.ts
CHANGED
|
@@ -166,6 +166,16 @@ export const CHAPTERS: BookChapter[] = [
|
|
|
166
166
|
relatedDocs: ["workflows", "profiles", "schedules"],
|
|
167
167
|
sections: [],
|
|
168
168
|
},
|
|
169
|
+
{
|
|
170
|
+
id: "ch-14",
|
|
171
|
+
number: 14,
|
|
172
|
+
title: "The Meta-Program",
|
|
173
|
+
subtitle: "When the System You Are Using Is Also the System You Are Building",
|
|
174
|
+
part: PARTS[3],
|
|
175
|
+
readingTime: 16,
|
|
176
|
+
relatedDocs: ["workflows", "profiles", "schedules", "blueprints"],
|
|
177
|
+
sections: [],
|
|
178
|
+
},
|
|
169
179
|
];
|
|
170
180
|
|
|
171
181
|
/**
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
registerChatStream,
|
|
4
|
+
unregisterChatStream,
|
|
5
|
+
getActiveChatStreamCount,
|
|
6
|
+
isAnyChatStreaming,
|
|
7
|
+
} from "../active-streams";
|
|
8
|
+
|
|
9
|
+
describe("active chat streams", () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
for (const id of ["a", "b", "c"]) unregisterChatStream(id);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("starts empty", () => {
|
|
15
|
+
expect(getActiveChatStreamCount()).toBe(0);
|
|
16
|
+
expect(isAnyChatStreaming()).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("tracks a single registered stream", () => {
|
|
20
|
+
registerChatStream("a");
|
|
21
|
+
expect(getActiveChatStreamCount()).toBe(1);
|
|
22
|
+
expect(isAnyChatStreaming()).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("tracks multiple streams independently", () => {
|
|
26
|
+
registerChatStream("a");
|
|
27
|
+
registerChatStream("b");
|
|
28
|
+
expect(getActiveChatStreamCount()).toBe(2);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("is idempotent — registering the same id twice still counts as one", () => {
|
|
32
|
+
registerChatStream("a");
|
|
33
|
+
registerChatStream("a");
|
|
34
|
+
expect(getActiveChatStreamCount()).toBe(1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("unregisters by id", () => {
|
|
38
|
+
registerChatStream("a");
|
|
39
|
+
registerChatStream("b");
|
|
40
|
+
unregisterChatStream("a");
|
|
41
|
+
expect(getActiveChatStreamCount()).toBe(1);
|
|
42
|
+
expect(isAnyChatStreaming()).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("unregistering a non-existent id is a no-op", () => {
|
|
46
|
+
expect(() => unregisterChatStream("never-registered")).not.toThrow();
|
|
47
|
+
expect(getActiveChatStreamCount()).toBe(0);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { db } from "@/lib/db";
|
|
3
|
+
import { conversations, chatMessages } from "@/lib/db/schema";
|
|
4
|
+
import { eq } from "drizzle-orm";
|
|
5
|
+
import { randomUUID } from "crypto";
|
|
6
|
+
import { finalizeStreamingMessage } from "../reconcile";
|
|
7
|
+
|
|
8
|
+
function seedConversation(): string {
|
|
9
|
+
const id = randomUUID();
|
|
10
|
+
const now = new Date();
|
|
11
|
+
db.insert(conversations)
|
|
12
|
+
.values({
|
|
13
|
+
id,
|
|
14
|
+
runtimeId: "test-runtime",
|
|
15
|
+
status: "active",
|
|
16
|
+
createdAt: now,
|
|
17
|
+
updatedAt: now,
|
|
18
|
+
})
|
|
19
|
+
.run();
|
|
20
|
+
return id;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function seedStreaming(convId: string, content: string): string {
|
|
24
|
+
const id = randomUUID();
|
|
25
|
+
db.insert(chatMessages)
|
|
26
|
+
.values({
|
|
27
|
+
id,
|
|
28
|
+
conversationId: convId,
|
|
29
|
+
role: "assistant",
|
|
30
|
+
content,
|
|
31
|
+
status: "streaming",
|
|
32
|
+
createdAt: new Date(),
|
|
33
|
+
})
|
|
34
|
+
.run();
|
|
35
|
+
return id;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe("finalizeStreamingMessage", () => {
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
db.delete(chatMessages).run();
|
|
41
|
+
db.delete(conversations).run();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("is a no-op when the message is already complete", async () => {
|
|
45
|
+
const convId = seedConversation();
|
|
46
|
+
const id = randomUUID();
|
|
47
|
+
db.insert(chatMessages)
|
|
48
|
+
.values({
|
|
49
|
+
id,
|
|
50
|
+
conversationId: convId,
|
|
51
|
+
role: "assistant",
|
|
52
|
+
content: "Already finished",
|
|
53
|
+
status: "complete",
|
|
54
|
+
createdAt: new Date(),
|
|
55
|
+
})
|
|
56
|
+
.run();
|
|
57
|
+
|
|
58
|
+
await finalizeStreamingMessage(id, "ignored salvage text");
|
|
59
|
+
|
|
60
|
+
const row = db
|
|
61
|
+
.select()
|
|
62
|
+
.from(chatMessages)
|
|
63
|
+
.where(eq(chatMessages.id, id))
|
|
64
|
+
.get();
|
|
65
|
+
expect(row?.status).toBe("complete");
|
|
66
|
+
expect(row?.content).toBe("Already finished");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("salvages streaming row with substantial content as complete", async () => {
|
|
70
|
+
const convId = seedConversation();
|
|
71
|
+
const id = seedStreaming(convId, "");
|
|
72
|
+
const partialText =
|
|
73
|
+
"I searched the web and found three relevant articles about the topic. Here are the highlights of what I learned...";
|
|
74
|
+
|
|
75
|
+
await finalizeStreamingMessage(id, partialText);
|
|
76
|
+
|
|
77
|
+
const row = db
|
|
78
|
+
.select()
|
|
79
|
+
.from(chatMessages)
|
|
80
|
+
.where(eq(chatMessages.id, id))
|
|
81
|
+
.get();
|
|
82
|
+
expect(row?.status).toBe("complete");
|
|
83
|
+
expect(row?.content).toBe(partialText);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("marks streaming row with no content as error with fallback string", async () => {
|
|
87
|
+
const convId = seedConversation();
|
|
88
|
+
const id = seedStreaming(convId, "");
|
|
89
|
+
|
|
90
|
+
await finalizeStreamingMessage(id, "");
|
|
91
|
+
|
|
92
|
+
const row = db
|
|
93
|
+
.select()
|
|
94
|
+
.from(chatMessages)
|
|
95
|
+
.where(eq(chatMessages.id, id))
|
|
96
|
+
.get();
|
|
97
|
+
expect(row?.status).toBe("error");
|
|
98
|
+
expect(row?.content).toMatch(/interrupted/i);
|
|
99
|
+
expect(row?.content.length).toBeGreaterThan(0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("marks streaming row with very short content as error, not complete", async () => {
|
|
103
|
+
const convId = seedConversation();
|
|
104
|
+
const id = seedStreaming(convId, "");
|
|
105
|
+
|
|
106
|
+
// 20 chars — not substantial enough to call "complete"
|
|
107
|
+
await finalizeStreamingMessage(id, "Just a short reply.");
|
|
108
|
+
|
|
109
|
+
const row = db
|
|
110
|
+
.select()
|
|
111
|
+
.from(chatMessages)
|
|
112
|
+
.where(eq(chatMessages.id, id))
|
|
113
|
+
.get();
|
|
114
|
+
expect(row?.status).toBe("error");
|
|
115
|
+
expect(row?.content).toBe("Just a short reply.");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("marks streaming row with whitespace-only fullText as error with fallback", async () => {
|
|
119
|
+
const convId = seedConversation();
|
|
120
|
+
const id = seedStreaming(convId, "");
|
|
121
|
+
|
|
122
|
+
await finalizeStreamingMessage(id, " \n\n \t ");
|
|
123
|
+
|
|
124
|
+
const row = db
|
|
125
|
+
.select()
|
|
126
|
+
.from(chatMessages)
|
|
127
|
+
.where(eq(chatMessages.id, id))
|
|
128
|
+
.get();
|
|
129
|
+
expect(row?.status).toBe("error");
|
|
130
|
+
expect(row?.content).toMatch(/interrupted/i);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("is a no-op when the message does not exist", async () => {
|
|
134
|
+
// Should not throw — defensive null check
|
|
135
|
+
await expect(
|
|
136
|
+
finalizeStreamingMessage("nonexistent-id", "some text"),
|
|
137
|
+
).resolves.not.toThrow();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { db } from "@/lib/db";
|
|
3
|
+
import { conversations, chatMessages } from "@/lib/db/schema";
|
|
4
|
+
import { eq } from "drizzle-orm";
|
|
5
|
+
import { randomUUID } from "crypto";
|
|
6
|
+
import { reconcileStreamingMessages } from "../reconcile";
|
|
7
|
+
|
|
8
|
+
function seedConversation(): string {
|
|
9
|
+
const id = randomUUID();
|
|
10
|
+
const now = new Date();
|
|
11
|
+
db.insert(conversations)
|
|
12
|
+
.values({
|
|
13
|
+
id,
|
|
14
|
+
runtimeId: "test-runtime",
|
|
15
|
+
status: "active",
|
|
16
|
+
createdAt: now,
|
|
17
|
+
updatedAt: now,
|
|
18
|
+
})
|
|
19
|
+
.run();
|
|
20
|
+
return id;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function seedMessage(opts: {
|
|
24
|
+
conversationId: string;
|
|
25
|
+
status: "streaming" | "complete" | "error";
|
|
26
|
+
content: string;
|
|
27
|
+
createdAt: Date;
|
|
28
|
+
}): string {
|
|
29
|
+
const id = randomUUID();
|
|
30
|
+
db.insert(chatMessages)
|
|
31
|
+
.values({
|
|
32
|
+
id,
|
|
33
|
+
conversationId: opts.conversationId,
|
|
34
|
+
role: "assistant",
|
|
35
|
+
content: opts.content,
|
|
36
|
+
status: opts.status,
|
|
37
|
+
createdAt: opts.createdAt,
|
|
38
|
+
})
|
|
39
|
+
.run();
|
|
40
|
+
return id;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("reconcileStreamingMessages", () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
// Isolate each test
|
|
46
|
+
db.delete(chatMessages).run();
|
|
47
|
+
db.delete(conversations).run();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("sweeps a 20-min-old streaming row with empty content to error state with fallback", async () => {
|
|
51
|
+
const convId = seedConversation();
|
|
52
|
+
const twentyMinAgo = new Date(Date.now() - 20 * 60 * 1000);
|
|
53
|
+
const msgId = seedMessage({
|
|
54
|
+
conversationId: convId,
|
|
55
|
+
status: "streaming",
|
|
56
|
+
content: "",
|
|
57
|
+
createdAt: twentyMinAgo,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const swept = await reconcileStreamingMessages();
|
|
61
|
+
|
|
62
|
+
expect(swept).toBe(1);
|
|
63
|
+
const row = db
|
|
64
|
+
.select()
|
|
65
|
+
.from(chatMessages)
|
|
66
|
+
.where(eq(chatMessages.id, msgId))
|
|
67
|
+
.get();
|
|
68
|
+
expect(row?.status).toBe("error");
|
|
69
|
+
expect(row?.content).toMatch(/Interrupted/i);
|
|
70
|
+
expect(row?.content.length).toBeGreaterThan(0);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("leaves a 30-second-old streaming row untouched", async () => {
|
|
74
|
+
const convId = seedConversation();
|
|
75
|
+
const thirtySecAgo = new Date(Date.now() - 30 * 1000);
|
|
76
|
+
const msgId = seedMessage({
|
|
77
|
+
conversationId: convId,
|
|
78
|
+
status: "streaming",
|
|
79
|
+
content: "",
|
|
80
|
+
createdAt: thirtySecAgo,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const swept = await reconcileStreamingMessages();
|
|
84
|
+
|
|
85
|
+
expect(swept).toBe(0);
|
|
86
|
+
const row = db
|
|
87
|
+
.select()
|
|
88
|
+
.from(chatMessages)
|
|
89
|
+
.where(eq(chatMessages.id, msgId))
|
|
90
|
+
.get();
|
|
91
|
+
expect(row?.status).toBe("streaming");
|
|
92
|
+
expect(row?.content).toBe("");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("preserves partial content when sweeping old streaming row", async () => {
|
|
96
|
+
const convId = seedConversation();
|
|
97
|
+
const twentyMinAgo = new Date(Date.now() - 20 * 60 * 1000);
|
|
98
|
+
const msgId = seedMessage({
|
|
99
|
+
conversationId: convId,
|
|
100
|
+
status: "streaming",
|
|
101
|
+
content: "Here is what I found so",
|
|
102
|
+
createdAt: twentyMinAgo,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
await reconcileStreamingMessages();
|
|
106
|
+
|
|
107
|
+
const row = db
|
|
108
|
+
.select()
|
|
109
|
+
.from(chatMessages)
|
|
110
|
+
.where(eq(chatMessages.id, msgId))
|
|
111
|
+
.get();
|
|
112
|
+
expect(row?.status).toBe("error");
|
|
113
|
+
expect(row?.content).toBe("Here is what I found so");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("leaves complete messages untouched regardless of age", async () => {
|
|
117
|
+
const convId = seedConversation();
|
|
118
|
+
const twentyMinAgo = new Date(Date.now() - 20 * 60 * 1000);
|
|
119
|
+
const msgId = seedMessage({
|
|
120
|
+
conversationId: convId,
|
|
121
|
+
status: "complete",
|
|
122
|
+
content: "Finished response",
|
|
123
|
+
createdAt: twentyMinAgo,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const swept = await reconcileStreamingMessages();
|
|
127
|
+
|
|
128
|
+
expect(swept).toBe(0);
|
|
129
|
+
const row = db
|
|
130
|
+
.select()
|
|
131
|
+
.from(chatMessages)
|
|
132
|
+
.where(eq(chatMessages.id, msgId))
|
|
133
|
+
.get();
|
|
134
|
+
expect(row?.status).toBe("complete");
|
|
135
|
+
expect(row?.content).toBe("Finished response");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
recordTermination,
|
|
4
|
+
readTerminations,
|
|
5
|
+
countTerminations,
|
|
6
|
+
__resetForTesting,
|
|
7
|
+
} from "../stream-telemetry";
|
|
8
|
+
|
|
9
|
+
describe("stream-telemetry ring buffer", () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
__resetForTesting();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("returns [] before any events are recorded", () => {
|
|
15
|
+
expect(readTerminations()).toEqual([]);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("records events in chronological order", () => {
|
|
19
|
+
recordTermination({
|
|
20
|
+
reason: "stream.completed",
|
|
21
|
+
conversationId: "c1",
|
|
22
|
+
messageId: "m1",
|
|
23
|
+
durationMs: 100,
|
|
24
|
+
});
|
|
25
|
+
recordTermination({
|
|
26
|
+
reason: "stream.aborted.client",
|
|
27
|
+
conversationId: "c2",
|
|
28
|
+
messageId: "m2",
|
|
29
|
+
durationMs: 50,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const events = readTerminations();
|
|
33
|
+
expect(events).toHaveLength(2);
|
|
34
|
+
expect(events[0].reason).toBe("stream.completed");
|
|
35
|
+
expect(events[1].reason).toBe("stream.aborted.client");
|
|
36
|
+
expect(events[0].timestamp).toBeLessThanOrEqual(events[1].timestamp);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("stamps each event with a timestamp", () => {
|
|
40
|
+
const before = Date.now();
|
|
41
|
+
recordTermination({
|
|
42
|
+
reason: "stream.completed",
|
|
43
|
+
conversationId: "c1",
|
|
44
|
+
messageId: "m1",
|
|
45
|
+
durationMs: 0,
|
|
46
|
+
});
|
|
47
|
+
const after = Date.now();
|
|
48
|
+
const events = readTerminations();
|
|
49
|
+
expect(events[0].timestamp).toBeGreaterThanOrEqual(before);
|
|
50
|
+
expect(events[0].timestamp).toBeLessThanOrEqual(after);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("wraps around after 500 events, preserving newest-500 in order", () => {
|
|
54
|
+
// Write 520 events — first 20 should be evicted.
|
|
55
|
+
for (let i = 0; i < 520; i++) {
|
|
56
|
+
recordTermination({
|
|
57
|
+
reason: "stream.completed",
|
|
58
|
+
conversationId: `c${i}`,
|
|
59
|
+
messageId: `m${i}`,
|
|
60
|
+
durationMs: i,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const events = readTerminations();
|
|
65
|
+
expect(events).toHaveLength(500);
|
|
66
|
+
// Oldest surviving event should be #20; newest should be #519.
|
|
67
|
+
expect(events[0].conversationId).toBe("c20");
|
|
68
|
+
expect(events[0].durationMs).toBe(20);
|
|
69
|
+
expect(events[499].conversationId).toBe("c519");
|
|
70
|
+
expect(events[499].durationMs).toBe(519);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("countTerminations groups by reason code across the full buffer", () => {
|
|
74
|
+
recordTermination({ reason: "stream.completed", conversationId: "c", messageId: "m", durationMs: 1 });
|
|
75
|
+
recordTermination({ reason: "stream.completed", conversationId: "c", messageId: "m", durationMs: 1 });
|
|
76
|
+
recordTermination({ reason: "stream.aborted.client", conversationId: "c", messageId: "m", durationMs: 1 });
|
|
77
|
+
recordTermination({ reason: "stream.finalized.error", conversationId: "c", messageId: "m", durationMs: 1, error: "boom" });
|
|
78
|
+
recordTermination({ reason: "stream.abandoned", conversationId: "c", messageId: "m", durationMs: 42 });
|
|
79
|
+
|
|
80
|
+
const counts = countTerminations();
|
|
81
|
+
expect(counts["stream.completed"]).toBe(2);
|
|
82
|
+
expect(counts["stream.aborted.client"]).toBe(1);
|
|
83
|
+
expect(counts["stream.finalized.error"]).toBe(1);
|
|
84
|
+
expect(counts["stream.abandoned"]).toBe(1);
|
|
85
|
+
expect(counts["stream.aborted.signal"]).toBe(0);
|
|
86
|
+
expect(counts["stream.reconciled.stale"]).toBe(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("stream.abandoned is a valid reason code for iterator abandonment", () => {
|
|
90
|
+
// finalizeStreamingMessage records this when the engine's happy and
|
|
91
|
+
// catch paths both missed the termination — the canonical "gap"
|
|
92
|
+
// indicator. Make sure it round-trips through the buffer.
|
|
93
|
+
recordTermination({
|
|
94
|
+
reason: "stream.abandoned",
|
|
95
|
+
conversationId: "c1",
|
|
96
|
+
messageId: "m1",
|
|
97
|
+
durationMs: 100,
|
|
98
|
+
error: "no content streamed before abandonment",
|
|
99
|
+
});
|
|
100
|
+
const events = readTerminations();
|
|
101
|
+
expect(events[0].reason).toBe("stream.abandoned");
|
|
102
|
+
expect(events[0].error).toBe("no content streamed before abandonment");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("countTerminations honors the windowMs filter", async () => {
|
|
106
|
+
recordTermination({ reason: "stream.completed", conversationId: "c", messageId: "m", durationMs: 1 });
|
|
107
|
+
// Wait a few ms so the second event has a strictly later timestamp.
|
|
108
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
109
|
+
const midpoint = Date.now();
|
|
110
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
111
|
+
recordTermination({ reason: "stream.completed", conversationId: "c", messageId: "m", durationMs: 1 });
|
|
112
|
+
|
|
113
|
+
// Use a window that only includes the second event.
|
|
114
|
+
const windowMs = Date.now() - midpoint + 5;
|
|
115
|
+
const counts = countTerminations(windowMs);
|
|
116
|
+
expect(counts["stream.completed"]).toBe(1);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("readTerminations returns a copy, not a live reference", () => {
|
|
120
|
+
recordTermination({ reason: "stream.completed", conversationId: "c", messageId: "m", durationMs: 1 });
|
|
121
|
+
const first = readTerminations();
|
|
122
|
+
recordTermination({ reason: "stream.completed", conversationId: "c2", messageId: "m2", durationMs: 1 });
|
|
123
|
+
// first snapshot should still have only the initial event.
|
|
124
|
+
expect(first).toHaveLength(1);
|
|
125
|
+
expect(readTerminations()).toHaveLength(2);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("records optional error strings on error events", () => {
|
|
129
|
+
recordTermination({
|
|
130
|
+
reason: "stream.finalized.error",
|
|
131
|
+
conversationId: "c",
|
|
132
|
+
messageId: "m",
|
|
133
|
+
durationMs: 42,
|
|
134
|
+
error: "boom",
|
|
135
|
+
});
|
|
136
|
+
expect(readTerminations()[0].error).toBe("boom");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("allows null conversationId / messageId / durationMs for edge cases", () => {
|
|
140
|
+
recordTermination({
|
|
141
|
+
reason: "stream.reconciled.stale",
|
|
142
|
+
conversationId: null,
|
|
143
|
+
messageId: null,
|
|
144
|
+
durationMs: null,
|
|
145
|
+
});
|
|
146
|
+
const events = readTerminations();
|
|
147
|
+
expect(events[0].conversationId).toBeNull();
|
|
148
|
+
expect(events[0].messageId).toBeNull();
|
|
149
|
+
expect(events[0].durationMs).toBeNull();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory tracker for chat conversations that currently have an SSE stream
|
|
3
|
+
* in flight. Used by the scheduler tick loop to apply a soft pressure signal
|
|
4
|
+
* — when chat is active, new schedule firings are deferred by N seconds to
|
|
5
|
+
* keep the Node event loop responsive for the user's conversation.
|
|
6
|
+
*
|
|
7
|
+
* Module-level state; single-process (same Node instance as the scheduler).
|
|
8
|
+
* Must NOT be persisted — crash recovery relies on the set starting empty.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const activeStreams = new Set<string>();
|
|
12
|
+
|
|
13
|
+
export function registerChatStream(conversationId: string): void {
|
|
14
|
+
activeStreams.add(conversationId);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function unregisterChatStream(conversationId: string): void {
|
|
18
|
+
activeStreams.delete(conversationId);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getActiveChatStreamCount(): number {
|
|
22
|
+
return activeStreams.size;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isAnyChatStreaming(): boolean {
|
|
26
|
+
return activeStreams.size > 0;
|
|
27
|
+
}
|
|
@@ -2,7 +2,10 @@ import { db } from "@/lib/db";
|
|
|
2
2
|
import { projects } from "@/lib/db/schema";
|
|
3
3
|
import { eq } from "drizzle-orm";
|
|
4
4
|
import { CodexAppServerClient } from "@/lib/agents/runtime/codex-app-server-client";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
ensureOpenAICodexClientAuthenticated,
|
|
7
|
+
resolveOpenAICodexAuthContext,
|
|
8
|
+
} from "@/lib/agents/runtime/openai-codex-auth";
|
|
6
9
|
import {
|
|
7
10
|
extractUsageSnapshot,
|
|
8
11
|
mergeUsageSnapshot,
|
|
@@ -128,11 +131,17 @@ export async function* sendCodexMessage(
|
|
|
128
131
|
});
|
|
129
132
|
|
|
130
133
|
// Get OpenAI API key
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
await
|
|
134
|
+
let auth;
|
|
135
|
+
try {
|
|
136
|
+
auth = await resolveOpenAICodexAuthContext();
|
|
137
|
+
} catch (error) {
|
|
138
|
+
const message =
|
|
139
|
+
error instanceof Error
|
|
140
|
+
? error.message
|
|
141
|
+
: "OpenAI Codex authentication is not configured.";
|
|
142
|
+
await updateMessageContent(assistantMsg.id, message);
|
|
134
143
|
await updateMessageStatus(assistantMsg.id, "error");
|
|
135
|
-
yield { type: "error", message
|
|
144
|
+
yield { type: "error", message };
|
|
136
145
|
return;
|
|
137
146
|
}
|
|
138
147
|
|
|
@@ -164,20 +173,10 @@ export async function* sendCodexMessage(
|
|
|
164
173
|
}
|
|
165
174
|
|
|
166
175
|
try {
|
|
167
|
-
client = await
|
|
168
|
-
cwd: workspace.cwd,
|
|
169
|
-
env: { OPENAI_API_KEY: apiKey },
|
|
170
|
-
});
|
|
176
|
+
client = await auth.connect(workspace.cwd);
|
|
171
177
|
|
|
172
178
|
// Initialize and authenticate
|
|
173
|
-
await client
|
|
174
|
-
clientInfo: { name: "Stagent", version: "0.1.1" },
|
|
175
|
-
capabilities: null,
|
|
176
|
-
});
|
|
177
|
-
await client.request("account/login/start", {
|
|
178
|
-
type: "apiKey",
|
|
179
|
-
apiKey,
|
|
180
|
-
});
|
|
179
|
+
await ensureOpenAICodexClientAuthenticated(client, auth);
|
|
181
180
|
|
|
182
181
|
// Validate model availability against what the user's account supports
|
|
183
182
|
let validatedModel: string | undefined;
|
|
@@ -108,7 +108,7 @@ async function buildTier2(projectId?: string | null): Promise<string> {
|
|
|
108
108
|
if (recentTasks.length > 0) {
|
|
109
109
|
parts.push("\n### Recent Tasks");
|
|
110
110
|
for (const t of recentTasks) {
|
|
111
|
-
parts.push(`- [${t.status}] ${t.title} (id: ${t.id
|
|
111
|
+
parts.push(`- [${t.status}] ${t.title} (id: ${t.id})`);
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
@@ -123,7 +123,7 @@ async function buildTier2(projectId?: string | null): Promise<string> {
|
|
|
123
123
|
if (activeWorkflows.length > 0) {
|
|
124
124
|
parts.push("\n### Workflows");
|
|
125
125
|
for (const w of activeWorkflows) {
|
|
126
|
-
parts.push(`- [${w.status}] ${w.name} (id: ${w.id
|
|
126
|
+
parts.push(`- [${w.status}] ${w.name} (id: ${w.id})`);
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
|
|
@@ -137,7 +137,7 @@ async function buildTier2(projectId?: string | null): Promise<string> {
|
|
|
137
137
|
if (docs.length > 0) {
|
|
138
138
|
parts.push(`\n### Documents (${docs.length})`);
|
|
139
139
|
for (const d of docs) {
|
|
140
|
-
parts.push(`- ${d.filename} (id: ${d.id
|
|
140
|
+
parts.push(`- ${d.filename} (id: ${d.id})`);
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
@@ -285,6 +285,7 @@ async function buildTier3(mentions: MentionReference[]): Promise<string> {
|
|
|
285
285
|
return truncateToTokenBudget(text, TIER_3_BUDGET);
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
|
|
288
289
|
// ── Public API ─────────────────────────────────────────────────────────
|
|
289
290
|
|
|
290
291
|
export interface ChatContext {
|
|
@@ -312,6 +313,7 @@ export async function buildChatContext(opts: {
|
|
|
312
313
|
const tier0 = buildTier0(opts.projectName, opts.workspace);
|
|
313
314
|
|
|
314
315
|
const systemParts = [tier0];
|
|
316
|
+
|
|
315
317
|
if (tier3) systemParts.push(tier3);
|
|
316
318
|
if (tier2) systemParts.push(tier2);
|
|
317
319
|
|