@wundam/orchex 1.0.0-rc.2 → 1.0.0-rc.20

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 (92) hide show
  1. package/README.md +58 -17
  2. package/dist/cloud-executor.d.ts +71 -0
  3. package/dist/cloud-executor.js +335 -0
  4. package/dist/cloud-sync.d.ts +8 -0
  5. package/dist/cloud-sync.js +52 -0
  6. package/dist/config.d.ts +30 -4
  7. package/dist/config.js +61 -2
  8. package/dist/context-builder.d.ts +2 -0
  9. package/dist/context-builder.js +11 -3
  10. package/dist/cost.js +1 -1
  11. package/dist/entitlements/jwt.d.ts +7 -0
  12. package/dist/entitlements/jwt.js +78 -0
  13. package/dist/entitlements/resolve.d.ts +17 -0
  14. package/dist/entitlements/resolve.js +49 -0
  15. package/dist/entitlements/types.d.ts +21 -0
  16. package/dist/entitlements/types.js +4 -0
  17. package/dist/executors/base.d.ts +1 -1
  18. package/dist/executors/bedrock-executor.d.ts +39 -0
  19. package/dist/executors/bedrock-executor.js +197 -0
  20. package/dist/executors/index.d.ts +1 -0
  21. package/dist/executors/index.js +24 -1
  22. package/dist/index.js +440 -23
  23. package/dist/intelligence/index.d.ts +44 -0
  24. package/dist/intelligence/index.js +160 -0
  25. package/dist/key-cache.d.ts +31 -0
  26. package/dist/key-cache.js +84 -0
  27. package/dist/login-helpers.d.ts +25 -0
  28. package/dist/login-helpers.js +54 -0
  29. package/dist/manifest.js +18 -1
  30. package/dist/mcp-instructions.d.ts +1 -0
  31. package/dist/mcp-instructions.js +83 -0
  32. package/dist/mcp-resources.d.ts +8 -0
  33. package/dist/mcp-resources.js +414 -0
  34. package/dist/orchestrator.d.ts +14 -0
  35. package/dist/orchestrator.js +147 -29
  36. package/dist/setup/ide-registry.d.ts +13 -0
  37. package/dist/setup/ide-registry.js +51 -0
  38. package/dist/setup/index.d.ts +1 -0
  39. package/dist/setup/index.js +111 -0
  40. package/dist/tier-gating.js +0 -16
  41. package/dist/tiers.d.ts +35 -5
  42. package/dist/tiers.js +39 -3
  43. package/dist/tools.d.ts +6 -1
  44. package/dist/tools.js +832 -95
  45. package/dist/types.d.ts +71 -60
  46. package/dist/types.js +3 -0
  47. package/package.json +37 -5
  48. package/src/entitlements/public-key.pem +9 -0
  49. package/dist/intelligence/anti-pattern-detector.d.ts +0 -117
  50. package/dist/intelligence/anti-pattern-detector.js +0 -327
  51. package/dist/intelligence/budget-enforcer.d.ts +0 -119
  52. package/dist/intelligence/budget-enforcer.js +0 -226
  53. package/dist/intelligence/context-optimizer.d.ts +0 -111
  54. package/dist/intelligence/context-optimizer.js +0 -282
  55. package/dist/intelligence/cost-tracker.d.ts +0 -114
  56. package/dist/intelligence/cost-tracker.js +0 -183
  57. package/dist/intelligence/deliverable-extractor.d.ts +0 -134
  58. package/dist/intelligence/deliverable-extractor.js +0 -909
  59. package/dist/intelligence/dependency-inferrer.d.ts +0 -87
  60. package/dist/intelligence/dependency-inferrer.js +0 -403
  61. package/dist/intelligence/diagnostics.d.ts +0 -33
  62. package/dist/intelligence/diagnostics.js +0 -64
  63. package/dist/intelligence/error-analyzer.d.ts +0 -7
  64. package/dist/intelligence/error-analyzer.js +0 -76
  65. package/dist/intelligence/file-chunker.d.ts +0 -15
  66. package/dist/intelligence/file-chunker.js +0 -64
  67. package/dist/intelligence/fix-stream-manager.d.ts +0 -59
  68. package/dist/intelligence/fix-stream-manager.js +0 -212
  69. package/dist/intelligence/heuristics.d.ts +0 -23
  70. package/dist/intelligence/heuristics.js +0 -124
  71. package/dist/intelligence/learning-engine.d.ts +0 -157
  72. package/dist/intelligence/learning-engine.js +0 -433
  73. package/dist/intelligence/learning-feedback.d.ts +0 -96
  74. package/dist/intelligence/learning-feedback.js +0 -202
  75. package/dist/intelligence/pattern-analyzer.d.ts +0 -35
  76. package/dist/intelligence/pattern-analyzer.js +0 -189
  77. package/dist/intelligence/plan-parser.d.ts +0 -124
  78. package/dist/intelligence/plan-parser.js +0 -498
  79. package/dist/intelligence/planner.d.ts +0 -29
  80. package/dist/intelligence/planner.js +0 -86
  81. package/dist/intelligence/self-healer.d.ts +0 -16
  82. package/dist/intelligence/self-healer.js +0 -84
  83. package/dist/intelligence/slicing-metrics.d.ts +0 -62
  84. package/dist/intelligence/slicing-metrics.js +0 -202
  85. package/dist/intelligence/slicing-templates.d.ts +0 -81
  86. package/dist/intelligence/slicing-templates.js +0 -420
  87. package/dist/intelligence/split-suggester.d.ts +0 -69
  88. package/dist/intelligence/split-suggester.js +0 -176
  89. package/dist/intelligence/stream-generator.d.ts +0 -90
  90. package/dist/intelligence/stream-generator.js +0 -452
  91. package/dist/telemetry/telemetry-types.d.ts +0 -85
  92. package/dist/telemetry/telemetry-types.js +0 -1
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # orchex
2
2
 
3
- **Turn a plan into parallel AI agents that can't break each other's code.**
3
+ **Describe what you want. Orchex plans, parallelizes, and executes safely.**
4
4
 
5
- The orchestration engine inside your AI coding assistant. Paste a plan doc, orchex splits it into parallel streams with file ownership enforcement, self-healing failures, and multi-LLM routing. Your AI assistant is the driver. Orchex is the engine.
5
+ The orchestration engine inside your AI coding assistant. Describe your intent, orchex auto-generates a plan, splits it into parallel streams with file ownership enforcement, self-healing failures, and multi-LLM routing. Your AI assistant is the driver. Orchex is the engine.
6
6
 
7
7
  ## Why Orchex
8
8
 
@@ -10,20 +10,22 @@ Your AI assistant does tasks one at a time. Orchex makes it do 10 at once — sa
10
10
 
11
11
  - **Parallel Execution** — Multiple streams run simultaneously in dependency-aware waves. 5-10x faster than serial prompting.
12
12
  - **Ownership Enforcement** — Each stream can only modify files in its `owns` array. No two agents touch the same file. Zero conflicts.
13
- - **`orchex learn`** — The magic command. Paste a markdown plan, get executable parallel streams with dependency inference and anti-pattern detection. No other tool does this.
13
+ - **`orchex run`** — Describe what you want, get parallel execution. Auto-generates plans, previews waves, executes with ownership enforcement.
14
+ - **`orchex learn`** — The advanced path. Paste a markdown plan, get executable parallel streams with dependency inference and anti-pattern detection.
14
15
  - **Self-Healing** — Categorized error analysis with targeted fix streams. Not blind retry.
15
- - **Multi-LLM** — OpenAI, Gemini, Claude, DeepSeek, Ollama. Route different orchestrations to different providers.
16
+ - **Multi-LLM** — OpenAI, Gemini, Claude, DeepSeek, Ollama, AWS Bedrock. Route different orchestrations to different providers.
16
17
  - **BYOK** — Bring your own API key from any supported provider. You control costs.
17
18
 
18
19
  ## Prerequisites
19
20
 
20
21
  - [Node.js](https://nodejs.org/) >= 18
21
- - LLM API key (one of the following):
22
+ - LLM API key set via environment variable **or** store on the [dashboard](https://orchex.dev/dashboard/keys) and sync with `orchex login`:
22
23
  - `ANTHROPIC_API_KEY` for Anthropic Claude
23
- - `OPENAI_API_KEY` for OpenAI (GPT-4.1, GPT-4.5)
24
+ - `OPENAI_API_KEY` for OpenAI (GPT-4.1, o1, o3)
24
25
  - `GEMINI_API_KEY` for Google Gemini
25
- - `DEEPSEEK_API_KEY` for DeepSeek (V3, Coder, Reasoner)
26
- - Or configure Ollama for local models
26
+ - `DEEPSEEK_API_KEY` for DeepSeek (V3, Coder, R1)
27
+ - Configure Ollama for local models
28
+ - `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` for AWS Bedrock
27
29
 
28
30
  ## Install
29
31
 
@@ -45,11 +47,11 @@ Connect to orchex cloud for managed execution:
45
47
  orchex login
46
48
  ```
47
49
 
48
- Your browser opens — log in or create a free account, click **Allow**. Token saved automatically.
50
+ Your browser opens — log in or create a free account, click **Allow**. Token saved automatically. API keys stored on the dashboard are synced to your local machine so `orchex run` works without environment variables.
49
51
 
50
52
  ```bash
51
53
  orchex status # Check tier and trial runs
52
- orchex logout # Clear credentials
54
+ orchex logout # Clear credentials and cached keys
53
55
  orchex --help # All commands
54
56
  ```
55
57
 
@@ -57,19 +59,35 @@ See the [cloud setup guide](docs/user-guide/cloud-setup.md) for full details.
57
59
 
58
60
  ## MCP Configuration
59
61
 
60
- Add to your MCP config (e.g. project `.mcp.json`):
62
+ Auto-configure for your IDE (Cursor, Windsurf, Claude Code):
63
+
64
+ ```bash
65
+ npx @wundam/orchex setup
66
+ ```
67
+
68
+ Or manually add to your MCP config (e.g. project `.mcp.json`):
61
69
 
62
70
  ```json
63
71
  {
64
72
  "mcpServers": {
65
73
  "orchex": {
66
74
  "command": "npx",
67
- "args": ["@wundam/orchex"]
75
+ "args": ["-y", "@wundam/orchex"]
68
76
  }
69
77
  }
70
78
  }
71
79
  ```
72
80
 
81
+ ### Zero-Config LLM Discovery
82
+
83
+ Once connected, your AI assistant automatically receives:
84
+
85
+ - **Instructions** — Core concepts, all 12 tools, provider setup, and tier info. No prompt engineering needed.
86
+ - **8 on-demand resources** via `orchex://` URIs — deep guides on streams, waves, ownership, self-healing, providers, examples, and API reference. The LLM reads these when it needs them.
87
+ - **IDE auto-detection** — `orchex setup` detects Cursor, Windsurf, and other MCP clients, writes the correct config, and merges with existing MCP servers.
88
+
89
+ Your AI assistant knows how to use Orchex the moment it connects.
90
+
73
91
  ## Usage
74
92
 
75
93
  ### 1. Initialize an orchestration
@@ -133,6 +151,22 @@ orchex.status()
133
151
  orchex.complete({ archive: true })
134
152
  ```
135
153
 
154
+ ## CLI Commands
155
+
156
+ ```bash
157
+ orchex run "Add user auth" # Auto-plan and execute from intent
158
+ orchex run "..." --yes # Skip approval prompt
159
+ orchex run "..." --dry-run # Generate plan only, don't execute
160
+ orchex run "..." --provider openai # Use specific provider
161
+ orchex setup # Auto-detect IDE and configure MCP
162
+ orchex setup --ide cursor # Target a specific IDE
163
+ orchex login # Authenticate with orchex cloud
164
+ orchex logout # Log out of cloud
165
+ orchex status # Show login state, tier, trial runs
166
+ orchex config # Show/set configuration
167
+ orchex reset-learning # Clear learning data
168
+ ```
169
+
136
170
  ## MCP Tools
137
171
 
138
172
  | Tool | Description |
@@ -142,10 +176,13 @@ orchex.complete({ archive: true })
142
176
  | `status` | Get orchestration progress and wave info |
143
177
  | `execute` | Run the orchestration — calls LLM API, applies artifacts, verifies |
144
178
  | `complete` | Mark streams done or archive orchestration |
145
- | `learn` | Parse a planning document, generate atomic stream definitions, and surface prerequisites |
146
- | `init-plan` | Generate an annotated plan template optimized for `orchex learn` |
147
- | `recover` | Detect and recover stuck streams from mid-execution failures |
148
- | `reload` | Restart the MCP server process (picks up config/code changes) |
179
+ | `recover` | Reset stuck/failed streams for retry or skip |
180
+ | `learn` | Parse a markdown plan into stream definitions |
181
+ | `init-plan` | Generate an annotated plan template |
182
+ | `auto` | One-shot: intent plan preview execute report |
183
+ | `reset-learning` | Clear learning data (thresholds, patterns, reports) |
184
+ | `rollback-stream` | Revert a stream's file changes via git |
185
+ | `reload` | Restart MCP server to pick up code changes |
149
186
 
150
187
  ## Stream Definition
151
188
 
@@ -196,7 +233,11 @@ For local development, point `.mcp.json` to your local build:
196
233
 
197
234
  ## Pricing
198
235
 
199
- See [orchex.dev/pricing](https://orchex.dev/pricing) for current plans and limits. Free local tier included.
236
+ Free local tier included no account required. Cloud tiers unlock more streams, waves, providers, and run history.
237
+
238
+ **Founding Members** — 50 slots at $49/year with Pro-level limits (200 runs, 30 agents, 20 waves, unlimited providers). Locked-in pricing for life. [Claim a slot](https://orchex.dev/founders) while they last.
239
+
240
+ See [orchex.dev/pricing](https://orchex.dev/pricing) for all plans and limits.
200
241
 
201
242
  ## License
202
243
 
@@ -0,0 +1,71 @@
1
+ import type { ExecutorStrategy, ExecutionRequest, ExecutionResult } from './types.js';
2
+ /**
3
+ * Configuration for CloudExecutor retry and polling behavior.
4
+ */
5
+ export interface CloudExecutorConfig {
6
+ /** Base polling interval in milliseconds. Default: 1000 (1 second) */
7
+ pollIntervalMs: number;
8
+ /** Maximum polling interval after backoff. Default: 10000 (10 seconds) */
9
+ maxPollIntervalMs: number;
10
+ /** Total timeout for job execution in milliseconds. Default: 660000 (11 minutes) */
11
+ timeoutMs: number;
12
+ /** Maximum retries for transient errors (429, 5xx). Default: 5 */
13
+ maxRetries: number;
14
+ /** Base delay for exponential backoff in milliseconds. Default: 1000 */
15
+ retryBaseDelayMs: number;
16
+ /** Maximum delay for exponential backoff in milliseconds. Default: 30000 */
17
+ retryMaxDelayMs: number;
18
+ /** Jitter factor (0-1) to randomize delays and prevent thundering herd. Default: 0.1 */
19
+ jitterFactor: number;
20
+ }
21
+ /**
22
+ * CloudExecutor submits jobs to the orchex cloud server and polls for completion.
23
+ *
24
+ * Features:
25
+ * - Exponential backoff with jitter for transient errors (429, 5xx)
26
+ * - Respects Retry-After header from rate limit responses
27
+ * - Adaptive polling intervals to reduce server load
28
+ * - Configurable timeouts and retry limits
29
+ */
30
+ export declare class CloudExecutor implements ExecutorStrategy {
31
+ private apiUrl;
32
+ private apiKey;
33
+ readonly provider = "orchex-cloud";
34
+ private config;
35
+ constructor(apiUrl: string, apiKey: string, config?: Partial<CloudExecutorConfig>);
36
+ execute(request: ExecutionRequest): Promise<ExecutionResult>;
37
+ /**
38
+ * Submit a job to the cloud server with retry logic for transient errors.
39
+ */
40
+ private submitJobWithRetry;
41
+ /**
42
+ * Poll for job completion with adaptive intervals and retry logic.
43
+ * @param jobId The job ID to poll
44
+ * @param timeoutMs Effective timeout in milliseconds
45
+ */
46
+ private pollForCompletionWithRetry;
47
+ /**
48
+ * Cancel a job on the cloud server to stop token consumption.
49
+ * Best effort — silently fails if server is unreachable.
50
+ */
51
+ private cancelJob;
52
+ /**
53
+ * Fetch final token usage from job state on timeout.
54
+ * Best effort — returns zeros if fetch fails.
55
+ */
56
+ private fetchFinalTokenUsage;
57
+ /**
58
+ * Calculate retry delay with exponential backoff and jitter.
59
+ */
60
+ private calculateRetryDelay;
61
+ /**
62
+ * Parse Retry-After header (supports both seconds and HTTP-date formats).
63
+ */
64
+ private parseRetryAfter;
65
+ /**
66
+ * Add jitter to a delay to prevent thundering herd.
67
+ * Returns delay ± (jitterFactor * delay).
68
+ */
69
+ private addJitter;
70
+ private sleep;
71
+ }
@@ -0,0 +1,335 @@
1
+ import { extractArtifact } from './artifacts.js';
2
+ import { validateOwnership } from './utils/ownership-validator.js';
3
+ /**
4
+ * Returns actionable error messages for cloud API failures.
5
+ * Status-code-specific guidance tells users exactly how to fix each error.
6
+ */
7
+ function formatCloudError(status, body) {
8
+ const detail = body ? ` (${body.slice(0, 120)})` : '';
9
+ switch (status) {
10
+ case 401:
11
+ return `Authentication failed (401)${detail}.\n\nYour API token has expired or been revoked. Run \`orchex login\` to authenticate again.`;
12
+ case 402:
13
+ return `Payment required (402)${detail}.\n\nCheck your subscription at https://orchex.dev/dashboard/billing`;
14
+ case 403:
15
+ return `Access denied (403)${detail}.\n\nYour account may not have permission for this operation.`;
16
+ case 404: {
17
+ const modelMatch = body.match(/model[:\s]+(\S+)/i);
18
+ const modelHint = modelMatch ? ` "${modelMatch[1]}"` : '';
19
+ return `Model${modelHint} not found (404)${detail}.\n\nRun \`orchex config --model <model>\` to use a different model.`;
20
+ }
21
+ case 429:
22
+ return `Quota exceeded (429)${detail}.\n\nYou've used all cloud runs for this period. Upgrade at https://orchex.dev/pricing`;
23
+ default:
24
+ return `Cloud API error: ${status}${detail}`;
25
+ }
26
+ }
27
+ const DEFAULT_CONFIG = {
28
+ pollIntervalMs: 1000, // Start at 1s for faster initial response
29
+ maxPollIntervalMs: 10000,
30
+ timeoutMs: 660_000, // 11 minutes — buffer over server's 10min to allow completion
31
+ maxRetries: 5,
32
+ retryBaseDelayMs: 1000,
33
+ retryMaxDelayMs: 30000,
34
+ jitterFactor: 0.1, // Reduced jitter for more predictable timing
35
+ };
36
+ /** HTTP status codes that are retryable (transient errors). */
37
+ const RETRYABLE_STATUS_CODES = new Set([429, 500, 502, 503, 504]);
38
+ /**
39
+ * CloudExecutor submits jobs to the orchex cloud server and polls for completion.
40
+ *
41
+ * Features:
42
+ * - Exponential backoff with jitter for transient errors (429, 5xx)
43
+ * - Respects Retry-After header from rate limit responses
44
+ * - Adaptive polling intervals to reduce server load
45
+ * - Configurable timeouts and retry limits
46
+ */
47
+ export class CloudExecutor {
48
+ apiUrl;
49
+ apiKey;
50
+ provider = 'orchex-cloud';
51
+ config;
52
+ constructor(apiUrl, apiKey, config = {}) {
53
+ this.apiUrl = apiUrl;
54
+ this.apiKey = apiKey;
55
+ this.config = { ...DEFAULT_CONFIG, ...config };
56
+ }
57
+ async execute(request) {
58
+ // 1. Submit job with retry
59
+ const submitResult = await this.submitJobWithRetry(request);
60
+ if (!submitResult.success) {
61
+ return {
62
+ success: false,
63
+ rawResponse: '',
64
+ tokensUsed: { input: 0, output: 0 },
65
+ error: submitResult.error,
66
+ };
67
+ }
68
+ const jobId = submitResult.jobId;
69
+ // 2. Poll for completion with retry and adaptive intervals
70
+ // Use request-level timeout if provided, otherwise fall back to config
71
+ const effectiveTimeout = request.timeoutMs ?? this.config.timeoutMs;
72
+ return this.pollForCompletionWithRetry(jobId, effectiveTimeout, request.ownership);
73
+ }
74
+ /**
75
+ * Submit a job to the cloud server with retry logic for transient errors.
76
+ */
77
+ async submitJobWithRetry(request) {
78
+ let lastError;
79
+ for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
80
+ try {
81
+ const submitRes = await fetch(`${this.apiUrl}/api/v1/execute`, {
82
+ method: 'POST',
83
+ headers: {
84
+ 'Authorization': `Bearer ${this.apiKey}`,
85
+ 'Content-Type': 'application/json',
86
+ },
87
+ body: JSON.stringify({
88
+ streamId: request.streamId,
89
+ prompt: request.prompt,
90
+ model: request.model,
91
+ maxTokens: request.maxTokens,
92
+ timeoutMs: this.config.timeoutMs,
93
+ ...(request.fileContext && { fileContext: request.fileContext }),
94
+ ...(request.ownership && { ownership: request.ownership }),
95
+ }),
96
+ });
97
+ if (submitRes.ok) {
98
+ const { jobId } = (await submitRes.json());
99
+ return { success: true, jobId };
100
+ }
101
+ // Check if error is retryable
102
+ if (RETRYABLE_STATUS_CODES.has(submitRes.status)) {
103
+ lastError = `Cloud API error: ${submitRes.status}`;
104
+ if (attempt < this.config.maxRetries) {
105
+ const delay = this.calculateRetryDelay(attempt, submitRes);
106
+ await this.sleep(delay);
107
+ continue;
108
+ }
109
+ }
110
+ else {
111
+ // Non-retryable error (400, 401, 403, 404, etc.)
112
+ const body = await submitRes.text().catch(() => '');
113
+ return {
114
+ success: false,
115
+ error: formatCloudError(submitRes.status, body),
116
+ };
117
+ }
118
+ }
119
+ catch (err) {
120
+ lastError = `Network error: ${err.message}`;
121
+ if (attempt < this.config.maxRetries) {
122
+ const delay = this.calculateRetryDelay(attempt);
123
+ await this.sleep(delay);
124
+ continue;
125
+ }
126
+ }
127
+ }
128
+ return {
129
+ success: false,
130
+ error: `Failed after ${this.config.maxRetries + 1} attempts: ${lastError}`,
131
+ };
132
+ }
133
+ /**
134
+ * Poll for job completion with adaptive intervals and retry logic.
135
+ * @param jobId The job ID to poll
136
+ * @param timeoutMs Effective timeout in milliseconds
137
+ */
138
+ async pollForCompletionWithRetry(jobId, timeoutMs, ownership) {
139
+ const deadline = Date.now() + timeoutMs;
140
+ let pollInterval = this.config.pollIntervalMs;
141
+ let consecutiveErrors = 0;
142
+ while (Date.now() < deadline) {
143
+ try {
144
+ const pollRes = await fetch(`${this.apiUrl}/api/v1/job/${jobId}`, {
145
+ headers: { 'Authorization': `Bearer ${this.apiKey}` },
146
+ });
147
+ if (pollRes.ok) {
148
+ consecutiveErrors = 0; // Reset error count on success
149
+ const job = (await pollRes.json());
150
+ if (job.status === 'completed') {
151
+ const rawResponse = job.output ?? '';
152
+ // Use server-provided artifact if available, otherwise extract client-side
153
+ const artifact = job.artifact ?? extractArtifact(rawResponse);
154
+ // If artifact extraction fails (both server and client), mark as failure
155
+ if (!artifact) {
156
+ return {
157
+ success: false,
158
+ rawResponse,
159
+ tokensUsed: job.tokensUsed ?? { input: 0, output: 0 },
160
+ error: 'No valid orchex-artifact block found in response',
161
+ };
162
+ }
163
+ // Validate artifact against ownership constraints (defense-in-depth)
164
+ if (ownership && artifact) {
165
+ const validation = validateOwnership(artifact, ownership);
166
+ if (!validation.valid) {
167
+ return {
168
+ success: false,
169
+ rawResponse,
170
+ tokensUsed: job.tokensUsed ?? { input: 0, output: 0 },
171
+ error: validation.error,
172
+ };
173
+ }
174
+ }
175
+ return {
176
+ success: true,
177
+ rawResponse,
178
+ artifact,
179
+ tokensUsed: job.tokensUsed ?? { input: 0, output: 0 },
180
+ };
181
+ }
182
+ if (job.status === 'failed') {
183
+ return {
184
+ success: false,
185
+ rawResponse: job.output ?? '',
186
+ tokensUsed: job.tokensUsed ?? { input: 0, output: 0 },
187
+ error: job.error ?? 'Cloud execution failed',
188
+ };
189
+ }
190
+ // Job still pending/running - wait with adaptive interval
191
+ const delay = this.addJitter(pollInterval);
192
+ await this.sleep(delay);
193
+ // Gradually increase poll interval to reduce server load (max: maxPollIntervalMs)
194
+ pollInterval = Math.min(pollInterval * 1.1, this.config.maxPollIntervalMs);
195
+ continue;
196
+ }
197
+ // Handle poll error
198
+ if (RETRYABLE_STATUS_CODES.has(pollRes.status)) {
199
+ consecutiveErrors++;
200
+ // For rate limiting, back off significantly
201
+ if (pollRes.status === 429) {
202
+ const retryAfter = this.parseRetryAfter(pollRes);
203
+ const backoffDelay = retryAfter ?? this.calculateRetryDelay(consecutiveErrors);
204
+ await this.sleep(backoffDelay);
205
+ // Increase base poll interval to reduce future rate limiting
206
+ pollInterval = Math.min(pollInterval * 2, this.config.maxPollIntervalMs);
207
+ continue;
208
+ }
209
+ // For server errors, use standard retry logic
210
+ if (consecutiveErrors <= this.config.maxRetries) {
211
+ const delay = this.calculateRetryDelay(consecutiveErrors - 1, pollRes);
212
+ await this.sleep(delay);
213
+ continue;
214
+ }
215
+ }
216
+ // Non-retryable error or max retries exceeded — read body for diagnostic context
217
+ let pollBody = '';
218
+ try {
219
+ pollBody = await pollRes.text();
220
+ }
221
+ catch { /* ignore unreadable body */ }
222
+ return {
223
+ success: false,
224
+ rawResponse: '',
225
+ tokensUsed: { input: 0, output: 0 },
226
+ error: formatCloudError(pollRes.status, pollBody) + ` (after ${consecutiveErrors} retries)`,
227
+ };
228
+ }
229
+ catch (err) {
230
+ consecutiveErrors++;
231
+ if (consecutiveErrors <= this.config.maxRetries) {
232
+ const delay = this.calculateRetryDelay(consecutiveErrors - 1);
233
+ await this.sleep(delay);
234
+ continue;
235
+ }
236
+ return {
237
+ success: false,
238
+ rawResponse: '',
239
+ tokensUsed: { input: 0, output: 0 },
240
+ error: `Network error: ${err.message} (after ${consecutiveErrors} retries)`,
241
+ };
242
+ }
243
+ }
244
+ // On timeout:
245
+ // 1. Cancel the job to stop further token consumption
246
+ // 2. Fetch final token usage for cost tracking
247
+ await this.cancelJob(jobId);
248
+ const tokensUsed = await this.fetchFinalTokenUsage(jobId);
249
+ return {
250
+ success: false,
251
+ rawResponse: '',
252
+ tokensUsed,
253
+ error: `Cloud execution timed out after ${timeoutMs}ms (job cancelled)`,
254
+ };
255
+ }
256
+ /**
257
+ * Cancel a job on the cloud server to stop token consumption.
258
+ * Best effort — silently fails if server is unreachable.
259
+ */
260
+ async cancelJob(jobId) {
261
+ try {
262
+ await fetch(`${this.apiUrl}/api/v1/job/${jobId}/cancel`, {
263
+ method: 'POST',
264
+ headers: { 'Authorization': `Bearer ${this.apiKey}` },
265
+ });
266
+ }
267
+ catch {
268
+ // Best effort — ignore errors
269
+ }
270
+ }
271
+ /**
272
+ * Fetch final token usage from job state on timeout.
273
+ * Best effort — returns zeros if fetch fails.
274
+ */
275
+ async fetchFinalTokenUsage(jobId) {
276
+ try {
277
+ const res = await fetch(`${this.apiUrl}/api/v1/job/${jobId}`, {
278
+ headers: { 'Authorization': `Bearer ${this.apiKey}` },
279
+ });
280
+ if (res.ok) {
281
+ const job = (await res.json());
282
+ return job.tokensUsed ?? { input: 0, output: 0 };
283
+ }
284
+ }
285
+ catch {
286
+ // Best effort — ignore errors
287
+ }
288
+ return { input: 0, output: 0 };
289
+ }
290
+ /**
291
+ * Calculate retry delay with exponential backoff and jitter.
292
+ */
293
+ calculateRetryDelay(attempt, response) {
294
+ // Check for Retry-After header
295
+ if (response) {
296
+ const retryAfter = this.parseRetryAfter(response);
297
+ if (retryAfter)
298
+ return retryAfter;
299
+ }
300
+ // Exponential backoff: baseDelay * 2^attempt
301
+ const exponentialDelay = this.config.retryBaseDelayMs * Math.pow(2, attempt);
302
+ const cappedDelay = Math.min(exponentialDelay, this.config.retryMaxDelayMs);
303
+ return this.addJitter(cappedDelay);
304
+ }
305
+ /**
306
+ * Parse Retry-After header (supports both seconds and HTTP-date formats).
307
+ */
308
+ parseRetryAfter(response) {
309
+ const retryAfter = response.headers.get('retry-after');
310
+ if (!retryAfter)
311
+ return undefined;
312
+ // Try parsing as seconds
313
+ const seconds = parseInt(retryAfter, 10);
314
+ if (!isNaN(seconds)) {
315
+ return seconds * 1000;
316
+ }
317
+ // Try parsing as HTTP-date
318
+ const date = Date.parse(retryAfter);
319
+ if (!isNaN(date)) {
320
+ return Math.max(0, date - Date.now());
321
+ }
322
+ return undefined;
323
+ }
324
+ /**
325
+ * Add jitter to a delay to prevent thundering herd.
326
+ * Returns delay ± (jitterFactor * delay).
327
+ */
328
+ addJitter(delay) {
329
+ const jitter = delay * this.config.jitterFactor;
330
+ return delay + (Math.random() * 2 - 1) * jitter;
331
+ }
332
+ sleep(ms) {
333
+ return new Promise((resolve) => setTimeout(resolve, ms));
334
+ }
335
+ }
@@ -0,0 +1,8 @@
1
+ import type { Config } from './config.js';
2
+ import type { ExecutionReport } from './intelligence/index.js';
3
+ /**
4
+ * Fire-and-forget sync of an execution report to the cloud dashboard.
5
+ * Only runs when user is logged in (mode=cloud + apiKey present).
6
+ * Retries once on transient server errors. Logs warnings on failure.
7
+ */
8
+ export declare function syncReportToCloud(config: Config, report: ExecutionReport): Promise<void>;
@@ -0,0 +1,52 @@
1
+ /** HTTP status codes that are retryable (transient server errors). */
2
+ const RETRYABLE_STATUS = new Set([500, 502, 503, 504]);
3
+ /**
4
+ * Fire-and-forget sync of an execution report to the cloud dashboard.
5
+ * Only runs when user is logged in (mode=cloud + apiKey present).
6
+ * Retries once on transient server errors. Logs warnings on failure.
7
+ */
8
+ export async function syncReportToCloud(config, report) {
9
+ if (config.mode !== 'cloud' || !config.apiKey)
10
+ return;
11
+ const apiUrl = config.apiUrl;
12
+ const maxAttempts = 2;
13
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
14
+ try {
15
+ const resp = await fetch(`${apiUrl}/api/v1/history/sync`, {
16
+ method: 'POST',
17
+ headers: {
18
+ 'Authorization': `Bearer ${config.apiKey}`,
19
+ 'Content-Type': 'application/json',
20
+ },
21
+ body: JSON.stringify({ report }),
22
+ });
23
+ if (resp.ok)
24
+ return;
25
+ if (resp.status === 401) {
26
+ console.warn('Cloud sync: token expired. Run `orchex login` to re-authenticate.');
27
+ return;
28
+ }
29
+ if (RETRYABLE_STATUS.has(resp.status) && attempt < maxAttempts) {
30
+ await new Promise(r => setTimeout(r, 1000));
31
+ continue;
32
+ }
33
+ // Capture error body for diagnostics
34
+ let detail = '';
35
+ try {
36
+ const body = await resp.json();
37
+ if (body?.error)
38
+ detail = `: ${body.error}`;
39
+ }
40
+ catch { /* response may not be JSON */ }
41
+ console.warn(`Cloud sync failed: HTTP ${resp.status}${detail} (run ${report.runId}). History may be incomplete.`);
42
+ return;
43
+ }
44
+ catch {
45
+ if (attempt < maxAttempts) {
46
+ await new Promise(r => setTimeout(r, 1000));
47
+ continue;
48
+ }
49
+ console.warn(`Cloud sync failed: network error (run ${report.runId}). History may be incomplete.`);
50
+ }
51
+ }
52
+ }