@wundam/orchex 1.0.0-rc.3 → 1.0.0-rc.30

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 (106) hide show
  1. package/README.md +60 -18
  2. package/dist/artifacts.js +4 -0
  3. package/dist/cloud-executor.d.ts +71 -0
  4. package/dist/cloud-executor.js +364 -0
  5. package/dist/cloud-sync.d.ts +8 -0
  6. package/dist/cloud-sync.js +52 -0
  7. package/dist/config.d.ts +30 -4
  8. package/dist/config.js +68 -2
  9. package/dist/context-builder.d.ts +2 -0
  10. package/dist/context-builder.js +11 -3
  11. package/dist/cost.js +2 -1
  12. package/dist/entitlements/jwt.d.ts +7 -0
  13. package/dist/entitlements/jwt.js +78 -0
  14. package/dist/entitlements/resolve.d.ts +17 -0
  15. package/dist/entitlements/resolve.js +49 -0
  16. package/dist/entitlements/types.d.ts +21 -0
  17. package/dist/entitlements/types.js +4 -0
  18. package/dist/executors/base.d.ts +2 -2
  19. package/dist/executors/base.js +3 -0
  20. package/dist/executors/bedrock-executor.d.ts +39 -0
  21. package/dist/executors/bedrock-executor.js +197 -0
  22. package/dist/executors/index.d.ts +2 -0
  23. package/dist/executors/index.js +39 -1
  24. package/dist/executors/kimi-executor.d.ts +18 -0
  25. package/dist/executors/kimi-executor.js +136 -0
  26. package/dist/index.js +525 -25
  27. package/dist/intelligence/index.d.ts +46 -0
  28. package/dist/intelligence/index.js +177 -0
  29. package/dist/key-cache.d.ts +31 -0
  30. package/dist/key-cache.js +84 -0
  31. package/dist/manifest.d.ts +5 -0
  32. package/dist/manifest.js +14 -9
  33. package/dist/mcp-instructions.d.ts +1 -0
  34. package/dist/mcp-instructions.js +84 -0
  35. package/dist/mcp-resources.d.ts +8 -0
  36. package/dist/mcp-resources.js +422 -0
  37. package/dist/metrics.d.ts +12 -0
  38. package/dist/metrics.js +37 -0
  39. package/dist/model-cache.d.ts +18 -0
  40. package/dist/model-cache.js +62 -0
  41. package/dist/model-validator.d.ts +20 -0
  42. package/dist/model-validator.js +126 -0
  43. package/dist/orchestrator.d.ts +23 -2
  44. package/dist/orchestrator.js +380 -47
  45. package/dist/ownership.js +14 -8
  46. package/dist/setup/ide-registry.d.ts +13 -0
  47. package/dist/setup/ide-registry.js +51 -0
  48. package/dist/setup/index.d.ts +1 -0
  49. package/dist/setup/index.js +111 -0
  50. package/dist/tier-gating.js +0 -16
  51. package/dist/tiers.d.ts +35 -5
  52. package/dist/tiers.js +39 -3
  53. package/dist/tools.d.ts +31 -1
  54. package/dist/tools.js +1073 -126
  55. package/dist/types.d.ts +71 -60
  56. package/dist/types.js +3 -0
  57. package/dist/version.d.ts +1 -0
  58. package/dist/version.js +27 -0
  59. package/dist/waves.d.ts +1 -8
  60. package/dist/waves.js +33 -52
  61. package/package.json +46 -8
  62. package/src/entitlements/public-key.pem +9 -0
  63. package/dist/intelligence/anti-pattern-detector.d.ts +0 -117
  64. package/dist/intelligence/anti-pattern-detector.js +0 -327
  65. package/dist/intelligence/budget-enforcer.d.ts +0 -119
  66. package/dist/intelligence/budget-enforcer.js +0 -226
  67. package/dist/intelligence/context-optimizer.d.ts +0 -111
  68. package/dist/intelligence/context-optimizer.js +0 -282
  69. package/dist/intelligence/cost-tracker.d.ts +0 -114
  70. package/dist/intelligence/cost-tracker.js +0 -183
  71. package/dist/intelligence/deliverable-extractor.d.ts +0 -134
  72. package/dist/intelligence/deliverable-extractor.js +0 -909
  73. package/dist/intelligence/dependency-inferrer.d.ts +0 -87
  74. package/dist/intelligence/dependency-inferrer.js +0 -403
  75. package/dist/intelligence/diagnostics.d.ts +0 -33
  76. package/dist/intelligence/diagnostics.js +0 -64
  77. package/dist/intelligence/error-analyzer.d.ts +0 -7
  78. package/dist/intelligence/error-analyzer.js +0 -76
  79. package/dist/intelligence/file-chunker.d.ts +0 -15
  80. package/dist/intelligence/file-chunker.js +0 -64
  81. package/dist/intelligence/fix-stream-manager.d.ts +0 -59
  82. package/dist/intelligence/fix-stream-manager.js +0 -212
  83. package/dist/intelligence/heuristics.d.ts +0 -23
  84. package/dist/intelligence/heuristics.js +0 -124
  85. package/dist/intelligence/learning-engine.d.ts +0 -157
  86. package/dist/intelligence/learning-engine.js +0 -433
  87. package/dist/intelligence/learning-feedback.d.ts +0 -96
  88. package/dist/intelligence/learning-feedback.js +0 -202
  89. package/dist/intelligence/pattern-analyzer.d.ts +0 -35
  90. package/dist/intelligence/pattern-analyzer.js +0 -189
  91. package/dist/intelligence/plan-parser.d.ts +0 -124
  92. package/dist/intelligence/plan-parser.js +0 -498
  93. package/dist/intelligence/planner.d.ts +0 -29
  94. package/dist/intelligence/planner.js +0 -86
  95. package/dist/intelligence/self-healer.d.ts +0 -16
  96. package/dist/intelligence/self-healer.js +0 -84
  97. package/dist/intelligence/slicing-metrics.d.ts +0 -62
  98. package/dist/intelligence/slicing-metrics.js +0 -202
  99. package/dist/intelligence/slicing-templates.d.ts +0 -81
  100. package/dist/intelligence/slicing-templates.js +0 -420
  101. package/dist/intelligence/split-suggester.d.ts +0 -69
  102. package/dist/intelligence/split-suggester.js +0 -176
  103. package/dist/intelligence/stream-generator.d.ts +0 -90
  104. package/dist/intelligence/stream-generator.js +0 -452
  105. package/dist/telemetry/telemetry-types.d.ts +0 -85
  106. 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,23 @@ 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.
14
- - **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.
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.
15
+ - **Self-Healing** — Categorized error analysis with targeted fix streams. Not blind retry. Model validation before execution prevents wasted API calls.
16
+ - **Multi-LLM** — OpenAI, Gemini, Claude, DeepSeek, Kimi (Moonshot AI), Ollama, AWS Bedrock. Dynamic model registry auto-discovers available models. Key-aware routing prevents "model not found" errors.
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
+ - `KIMI_API_KEY` for Kimi / Moonshot AI (K2, moonshot-v1; `MOONSHOT_API_KEY` alias accepted)
28
+ - Configure Ollama for local models
29
+ - `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` for AWS Bedrock
27
30
 
28
31
  ## Install
29
32
 
@@ -45,11 +48,11 @@ Connect to orchex cloud for managed execution:
45
48
  orchex login
46
49
  ```
47
50
 
48
- Your browser opens — log in or create a free account, click **Allow**. Token saved automatically.
51
+ 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
52
 
50
53
  ```bash
51
54
  orchex status # Check tier and trial runs
52
- orchex logout # Clear credentials
55
+ orchex logout # Clear credentials and cached keys
53
56
  orchex --help # All commands
54
57
  ```
55
58
 
@@ -57,19 +60,35 @@ See the [cloud setup guide](docs/user-guide/cloud-setup.md) for full details.
57
60
 
58
61
  ## MCP Configuration
59
62
 
60
- Add to your MCP config (e.g. project `.mcp.json`):
63
+ Auto-configure for your IDE (Cursor, Windsurf, Claude Code):
64
+
65
+ ```bash
66
+ npx @wundam/orchex setup
67
+ ```
68
+
69
+ Or manually add to your MCP config (e.g. project `.mcp.json`):
61
70
 
62
71
  ```json
63
72
  {
64
73
  "mcpServers": {
65
74
  "orchex": {
66
75
  "command": "npx",
67
- "args": ["@wundam/orchex"]
76
+ "args": ["-y", "@wundam/orchex"]
68
77
  }
69
78
  }
70
79
  }
71
80
  ```
72
81
 
82
+ ### Zero-Config LLM Discovery
83
+
84
+ Once connected, your AI assistant automatically receives:
85
+
86
+ - **Instructions** — Core concepts, all 12 tools, provider setup, and tier info. No prompt engineering needed.
87
+ - **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.
88
+ - **IDE auto-detection** — `orchex setup` detects Cursor, Windsurf, and other MCP clients, writes the correct config, and merges with existing MCP servers.
89
+
90
+ Your AI assistant knows how to use Orchex the moment it connects.
91
+
73
92
  ## Usage
74
93
 
75
94
  ### 1. Initialize an orchestration
@@ -133,6 +152,22 @@ orchex.status()
133
152
  orchex.complete({ archive: true })
134
153
  ```
135
154
 
155
+ ## CLI Commands
156
+
157
+ ```bash
158
+ orchex run "Add user auth" # Auto-plan and execute from intent
159
+ orchex run "..." --yes # Skip approval prompt
160
+ orchex run "..." --dry-run # Generate plan only, don't execute
161
+ orchex run "..." --provider openai # Use specific provider
162
+ orchex setup # Auto-detect IDE and configure MCP
163
+ orchex setup --ide cursor # Target a specific IDE
164
+ orchex login # Authenticate with orchex cloud
165
+ orchex logout # Log out of cloud
166
+ orchex status # Show login state, tier, trial runs
167
+ orchex config # Show/set configuration
168
+ orchex reset-learning # Clear learning data
169
+ ```
170
+
136
171
  ## MCP Tools
137
172
 
138
173
  | Tool | Description |
@@ -142,10 +177,13 @@ orchex.complete({ archive: true })
142
177
  | `status` | Get orchestration progress and wave info |
143
178
  | `execute` | Run the orchestration — calls LLM API, applies artifacts, verifies |
144
179
  | `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) |
180
+ | `recover` | Reset stuck/failed streams for retry or skip |
181
+ | `learn` | Parse a markdown plan into stream definitions |
182
+ | `init-plan` | Generate an annotated plan template |
183
+ | `auto` | One-shot: intent plan preview execute report |
184
+ | `reset-learning` | Clear learning data (thresholds, patterns, reports) |
185
+ | `rollback-stream` | Revert a stream's file changes via git |
186
+ | `reload` | Restart MCP server to pick up code changes |
149
187
 
150
188
  ## Stream Definition
151
189
 
@@ -196,7 +234,11 @@ For local development, point `.mcp.json` to your local build:
196
234
 
197
235
  ## Pricing
198
236
 
199
- See [orchex.dev/pricing](https://orchex.dev/pricing) for current plans and limits. Free local tier included.
237
+ Free local tier included no account required. Cloud tiers unlock more streams, waves, providers, and run history.
238
+
239
+ **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.
240
+
241
+ See [orchex.dev/pricing](https://orchex.dev/pricing) for all plans and limits.
200
242
 
201
243
  ## License
202
244
 
package/dist/artifacts.js CHANGED
@@ -420,6 +420,10 @@ export async function writeArtifact(projectDir, streamId, artifact) {
420
420
  await fs.writeFile(artifactPath, JSON.stringify(artifact, null, 2), 'utf-8');
421
421
  return artifactPath;
422
422
  }
423
+ // Artifact-application trust boundary. Plan-level ownership is enforced
424
+ // upstream by validatePlan (src/intelligence/plan-contract.ts); this
425
+ // function guards the different trust boundary where LLM-generated
426
+ // artifacts try to apply their output.
423
427
  /**
424
428
  * Check whether all file operations fall within the stream's owns patterns.
425
429
  *
@@ -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,364 @@
1
+ import { extractArtifact } from './artifacts.js';
2
+ import { validateOwnership } from './utils/ownership-validator.js';
3
+ function parseProviderErrorBody(body) {
4
+ try {
5
+ const parsed = JSON.parse(body);
6
+ const message = parsed?.error?.message ?? parsed?.message;
7
+ if (typeof message === 'string')
8
+ return message;
9
+ }
10
+ catch {
11
+ // Not JSON — return null
12
+ }
13
+ return null;
14
+ }
15
+ /**
16
+ * Returns actionable error messages for cloud API failures.
17
+ * Status-code-specific guidance tells users exactly how to fix each error.
18
+ */
19
+ function formatCloudError(status, body) {
20
+ const detail = body ? ` (${body.slice(0, 120)})` : '';
21
+ switch (status) {
22
+ case 401:
23
+ return `Authentication failed (401)${detail}.\n\nYour API token has expired or been revoked. Run \`orchex login\` to authenticate again.`;
24
+ case 402:
25
+ return `Payment required (402)${detail}.\n\nCheck your subscription at https://orchex.dev/dashboard/billing`;
26
+ case 403:
27
+ return `Access denied (403)${detail}.\n\nYour account may not have permission for this operation.`;
28
+ case 404: {
29
+ const modelMatch = body.match(/model[:\s]+(\S+)/i);
30
+ const modelHint = modelMatch ? ` "${modelMatch[1]}"` : '';
31
+ return `Model${modelHint} not found (404)${detail}.\n\nRun \`orchex config --model <model>\` to use a different model.`;
32
+ }
33
+ case 400: {
34
+ const providerMsg = parseProviderErrorBody(body);
35
+ if (providerMsg && /credit balance|insufficient|billing/i.test(providerMsg)) {
36
+ return `LLM provider billing error (400): ${providerMsg}\n\nTop up credits at your provider's billing page, or switch providers with \`orchex config --provider <provider>\`.`;
37
+ }
38
+ if (providerMsg) {
39
+ return `LLM provider error (400): ${providerMsg}`;
40
+ }
41
+ return `Cloud API error: 400${detail}`;
42
+ }
43
+ case 429:
44
+ return `Quota exceeded (429)${detail}.\n\nYou've used all cloud runs for this period. Upgrade at https://orchex.dev/pricing`;
45
+ default:
46
+ return `Cloud API error: ${status}${detail}`;
47
+ }
48
+ }
49
+ const DEFAULT_CONFIG = {
50
+ pollIntervalMs: 1000, // Start at 1s for faster initial response
51
+ maxPollIntervalMs: 10000,
52
+ timeoutMs: 660_000, // 11 minutes — buffer over server's 10min to allow completion
53
+ maxRetries: 5,
54
+ retryBaseDelayMs: 1000,
55
+ retryMaxDelayMs: 30000,
56
+ jitterFactor: 0.1, // Reduced jitter for more predictable timing
57
+ };
58
+ /** HTTP status codes that are retryable (transient errors). */
59
+ const RETRYABLE_STATUS_CODES = new Set([429, 500, 502, 503, 504]);
60
+ /**
61
+ * CloudExecutor submits jobs to the orchex cloud server and polls for completion.
62
+ *
63
+ * Features:
64
+ * - Exponential backoff with jitter for transient errors (429, 5xx)
65
+ * - Respects Retry-After header from rate limit responses
66
+ * - Adaptive polling intervals to reduce server load
67
+ * - Configurable timeouts and retry limits
68
+ */
69
+ export class CloudExecutor {
70
+ apiUrl;
71
+ apiKey;
72
+ provider = 'orchex-cloud';
73
+ config;
74
+ constructor(apiUrl, apiKey, config = {}) {
75
+ this.apiUrl = apiUrl;
76
+ this.apiKey = apiKey;
77
+ this.config = { ...DEFAULT_CONFIG, ...config };
78
+ }
79
+ async execute(request) {
80
+ // 1. Submit job with retry
81
+ const submitResult = await this.submitJobWithRetry(request);
82
+ if (!submitResult.success) {
83
+ return {
84
+ success: false,
85
+ rawResponse: '',
86
+ tokensUsed: { input: 0, output: 0 },
87
+ error: submitResult.error,
88
+ };
89
+ }
90
+ const jobId = submitResult.jobId;
91
+ // 2. Poll for completion with retry and adaptive intervals
92
+ // Use request-level timeout if provided, otherwise fall back to config
93
+ const effectiveTimeout = request.timeoutMs ?? this.config.timeoutMs;
94
+ return this.pollForCompletionWithRetry(jobId, effectiveTimeout, request.ownership);
95
+ }
96
+ /**
97
+ * Submit a job to the cloud server with retry logic for transient errors.
98
+ */
99
+ async submitJobWithRetry(request) {
100
+ let lastError;
101
+ for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
102
+ try {
103
+ const submitRes = await fetch(`${this.apiUrl}/api/v1/execute`, {
104
+ method: 'POST',
105
+ headers: {
106
+ 'Authorization': `Bearer ${this.apiKey}`,
107
+ 'Content-Type': 'application/json',
108
+ },
109
+ body: JSON.stringify({
110
+ streamId: request.streamId,
111
+ prompt: request.prompt,
112
+ model: request.model,
113
+ maxTokens: request.maxTokens,
114
+ timeoutMs: this.config.timeoutMs,
115
+ ...(request.fileContext && { fileContext: request.fileContext }),
116
+ ...(request.ownership && { ownership: request.ownership }),
117
+ }),
118
+ });
119
+ if (submitRes.ok) {
120
+ const { jobId } = (await submitRes.json());
121
+ return { success: true, jobId };
122
+ }
123
+ // Check if error is retryable
124
+ if (RETRYABLE_STATUS_CODES.has(submitRes.status)) {
125
+ lastError = `Cloud API error: ${submitRes.status}`;
126
+ if (attempt < this.config.maxRetries) {
127
+ const delay = this.calculateRetryDelay(attempt, submitRes);
128
+ await this.sleep(delay);
129
+ continue;
130
+ }
131
+ }
132
+ else {
133
+ // Non-retryable error (400, 401, 403, 404, etc.)
134
+ const body = await submitRes.text().catch(() => '');
135
+ return {
136
+ success: false,
137
+ error: formatCloudError(submitRes.status, body),
138
+ };
139
+ }
140
+ }
141
+ catch (err) {
142
+ lastError = `Network error: ${err.message}`;
143
+ if (attempt < this.config.maxRetries) {
144
+ const delay = this.calculateRetryDelay(attempt);
145
+ await this.sleep(delay);
146
+ continue;
147
+ }
148
+ }
149
+ }
150
+ return {
151
+ success: false,
152
+ error: `Failed after ${this.config.maxRetries + 1} attempts: ${lastError}`,
153
+ };
154
+ }
155
+ /**
156
+ * Poll for job completion with adaptive intervals and retry logic.
157
+ * @param jobId The job ID to poll
158
+ * @param timeoutMs Effective timeout in milliseconds
159
+ */
160
+ async pollForCompletionWithRetry(jobId, timeoutMs, ownership) {
161
+ const deadline = Date.now() + timeoutMs;
162
+ let pollInterval = this.config.pollIntervalMs;
163
+ let consecutiveErrors = 0;
164
+ while (Date.now() < deadline) {
165
+ try {
166
+ const pollRes = await fetch(`${this.apiUrl}/api/v1/job/${jobId}`, {
167
+ headers: { 'Authorization': `Bearer ${this.apiKey}` },
168
+ });
169
+ if (pollRes.ok) {
170
+ consecutiveErrors = 0; // Reset error count on success
171
+ const job = (await pollRes.json());
172
+ if (job.status === 'completed') {
173
+ const rawResponse = job.output ?? '';
174
+ // Use server-provided artifact if available, otherwise extract client-side
175
+ const artifact = job.artifact ?? extractArtifact(rawResponse);
176
+ // If artifact extraction fails (both server and client), mark as failure
177
+ if (!artifact) {
178
+ return {
179
+ success: false,
180
+ rawResponse,
181
+ tokensUsed: job.tokensUsed ?? { input: 0, output: 0 },
182
+ error: 'No valid orchex-artifact block found in response',
183
+ };
184
+ }
185
+ // Validate artifact against ownership constraints (defense-in-depth)
186
+ if (ownership && artifact) {
187
+ const validation = validateOwnership(artifact, ownership);
188
+ if (!validation.valid) {
189
+ return {
190
+ success: false,
191
+ rawResponse,
192
+ tokensUsed: job.tokensUsed ?? { input: 0, output: 0 },
193
+ error: validation.error,
194
+ };
195
+ }
196
+ }
197
+ return {
198
+ success: true,
199
+ rawResponse,
200
+ artifact,
201
+ tokensUsed: job.tokensUsed ?? { input: 0, output: 0 },
202
+ };
203
+ }
204
+ if (job.status === 'failed') {
205
+ const rawError = job.error ?? 'Cloud execution failed';
206
+ const providerMsg = parseProviderErrorBody(rawError);
207
+ const formattedError = providerMsg
208
+ ? (/credit balance|insufficient|billing/i.test(providerMsg)
209
+ ? `LLM provider billing error: ${providerMsg}\n\nTop up credits at your provider's billing page, or switch providers with \`orchex config --provider <provider>\`.`
210
+ : `LLM provider error: ${providerMsg}`)
211
+ : rawError;
212
+ return {
213
+ success: false,
214
+ rawResponse: job.output ?? '',
215
+ tokensUsed: job.tokensUsed ?? { input: 0, output: 0 },
216
+ error: formattedError,
217
+ };
218
+ }
219
+ // Job still pending/running - wait with adaptive interval
220
+ const delay = this.addJitter(pollInterval);
221
+ await this.sleep(delay);
222
+ // Gradually increase poll interval to reduce server load (max: maxPollIntervalMs)
223
+ pollInterval = Math.min(pollInterval * 1.1, this.config.maxPollIntervalMs);
224
+ continue;
225
+ }
226
+ // Handle poll error
227
+ if (RETRYABLE_STATUS_CODES.has(pollRes.status)) {
228
+ consecutiveErrors++;
229
+ // For rate limiting, back off significantly
230
+ if (pollRes.status === 429) {
231
+ const retryAfter = this.parseRetryAfter(pollRes);
232
+ const backoffDelay = retryAfter ?? this.calculateRetryDelay(consecutiveErrors);
233
+ await this.sleep(backoffDelay);
234
+ // Increase base poll interval to reduce future rate limiting
235
+ pollInterval = Math.min(pollInterval * 2, this.config.maxPollIntervalMs);
236
+ continue;
237
+ }
238
+ // For server errors, use standard retry logic
239
+ if (consecutiveErrors <= this.config.maxRetries) {
240
+ const delay = this.calculateRetryDelay(consecutiveErrors - 1, pollRes);
241
+ await this.sleep(delay);
242
+ continue;
243
+ }
244
+ }
245
+ // Non-retryable error or max retries exceeded — read body for diagnostic context
246
+ let pollBody = '';
247
+ try {
248
+ pollBody = await pollRes.text();
249
+ }
250
+ catch { /* ignore unreadable body */ }
251
+ return {
252
+ success: false,
253
+ rawResponse: '',
254
+ tokensUsed: { input: 0, output: 0 },
255
+ error: formatCloudError(pollRes.status, pollBody) + ` (after ${consecutiveErrors} retries)`,
256
+ };
257
+ }
258
+ catch (err) {
259
+ consecutiveErrors++;
260
+ if (consecutiveErrors <= this.config.maxRetries) {
261
+ const delay = this.calculateRetryDelay(consecutiveErrors - 1);
262
+ await this.sleep(delay);
263
+ continue;
264
+ }
265
+ return {
266
+ success: false,
267
+ rawResponse: '',
268
+ tokensUsed: { input: 0, output: 0 },
269
+ error: `Network error: ${err.message} (after ${consecutiveErrors} retries)`,
270
+ };
271
+ }
272
+ }
273
+ // On timeout:
274
+ // 1. Cancel the job to stop further token consumption
275
+ // 2. Fetch final token usage for cost tracking
276
+ await this.cancelJob(jobId);
277
+ const tokensUsed = await this.fetchFinalTokenUsage(jobId);
278
+ return {
279
+ success: false,
280
+ rawResponse: '',
281
+ tokensUsed,
282
+ error: `Cloud execution timed out after ${timeoutMs}ms (job cancelled)`,
283
+ };
284
+ }
285
+ /**
286
+ * Cancel a job on the cloud server to stop token consumption.
287
+ * Best effort — silently fails if server is unreachable.
288
+ */
289
+ async cancelJob(jobId) {
290
+ try {
291
+ await fetch(`${this.apiUrl}/api/v1/job/${jobId}/cancel`, {
292
+ method: 'POST',
293
+ headers: { 'Authorization': `Bearer ${this.apiKey}` },
294
+ });
295
+ }
296
+ catch {
297
+ // Best effort — ignore errors
298
+ }
299
+ }
300
+ /**
301
+ * Fetch final token usage from job state on timeout.
302
+ * Best effort — returns zeros if fetch fails.
303
+ */
304
+ async fetchFinalTokenUsage(jobId) {
305
+ try {
306
+ const res = await fetch(`${this.apiUrl}/api/v1/job/${jobId}`, {
307
+ headers: { 'Authorization': `Bearer ${this.apiKey}` },
308
+ });
309
+ if (res.ok) {
310
+ const job = (await res.json());
311
+ return job.tokensUsed ?? { input: 0, output: 0 };
312
+ }
313
+ }
314
+ catch {
315
+ // Best effort — ignore errors
316
+ }
317
+ return { input: 0, output: 0 };
318
+ }
319
+ /**
320
+ * Calculate retry delay with exponential backoff and jitter.
321
+ */
322
+ calculateRetryDelay(attempt, response) {
323
+ // Check for Retry-After header
324
+ if (response) {
325
+ const retryAfter = this.parseRetryAfter(response);
326
+ if (retryAfter)
327
+ return retryAfter;
328
+ }
329
+ // Exponential backoff: baseDelay * 2^attempt
330
+ const exponentialDelay = this.config.retryBaseDelayMs * Math.pow(2, attempt);
331
+ const cappedDelay = Math.min(exponentialDelay, this.config.retryMaxDelayMs);
332
+ return this.addJitter(cappedDelay);
333
+ }
334
+ /**
335
+ * Parse Retry-After header (supports both seconds and HTTP-date formats).
336
+ */
337
+ parseRetryAfter(response) {
338
+ const retryAfter = response.headers.get('retry-after');
339
+ if (!retryAfter)
340
+ return undefined;
341
+ // Try parsing as seconds
342
+ const seconds = parseInt(retryAfter, 10);
343
+ if (!isNaN(seconds)) {
344
+ return seconds * 1000;
345
+ }
346
+ // Try parsing as HTTP-date
347
+ const date = Date.parse(retryAfter);
348
+ if (!isNaN(date)) {
349
+ return Math.max(0, date - Date.now());
350
+ }
351
+ return undefined;
352
+ }
353
+ /**
354
+ * Add jitter to a delay to prevent thundering herd.
355
+ * Returns delay ± (jitterFactor * delay).
356
+ */
357
+ addJitter(delay) {
358
+ const jitter = delay * this.config.jitterFactor;
359
+ return delay + (Math.random() * 2 - 1) * jitter;
360
+ }
361
+ sleep(ms) {
362
+ return new Promise((resolve) => setTimeout(resolve, ms));
363
+ }
364
+ }
@@ -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
+ }