deepline 0.1.138 → 0.1.140
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 +2 -2
- package/dist/bundling-sources/apps/play-runner-workers/src/coordinator-entry.ts +58 -18
- package/dist/bundling-sources/apps/play-runner-workers/src/workflow-retry.ts +15 -1
- package/dist/bundling-sources/sdk/src/config.ts +13 -310
- package/dist/bundling-sources/sdk/src/release.ts +2 -2
- package/dist/bundling-sources/shared_libs/play-runtime/coordinator-headers.ts +17 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-failure.ts +1 -1
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +27 -2
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backend.ts +3 -0
- package/dist/cli/index.js +92 -576
- package/dist/cli/index.mjs +108 -600
- package/dist/index.js +14 -148
- package/dist/index.mjs +15 -157
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,8 +65,8 @@ Do not route the SDK CLI through `.env`, `.env.local`, or `.env.worktree`.
|
|
|
65
65
|
Config resolution order:
|
|
66
66
|
|
|
67
67
|
```text
|
|
68
|
-
host: explicit SDK option -> DEEPLINE_HOST_URL -> nearest .env.deepline ->
|
|
69
|
-
key: explicit SDK option -> DEEPLINE_API_KEY -> matching nearest .env.deepline ->
|
|
68
|
+
host: explicit SDK option -> DEEPLINE_HOST_URL -> nearest .env.deepline -> saved production auth -> https://code.deepline.com
|
|
69
|
+
key: explicit SDK option -> DEEPLINE_API_KEY -> matching nearest .env.deepline -> saved auth for the resolved host
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
## CLI commands
|
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
import {
|
|
54
54
|
decideWorkflowPlatformRetry,
|
|
55
55
|
PLATFORM_DEPLOY_WORKFLOW_RETRY_LIMIT,
|
|
56
|
+
shouldPublishWorkflowRuntimeFailure,
|
|
56
57
|
} from './workflow-retry';
|
|
57
58
|
import {
|
|
58
59
|
WORKFLOW_RETRY_PARAMS_EXTERNALIZE_AFTER_BYTES,
|
|
@@ -274,6 +275,7 @@ interface CoordinatorEnv {
|
|
|
274
275
|
DEEPLINE_INTERNAL_TOKEN?: string;
|
|
275
276
|
DEEPLINE_TAIL_LOG_TOKEN?: string;
|
|
276
277
|
DEEPLINE_COORDINATOR_DEPLOY_MARKER?: string;
|
|
278
|
+
CF_VERSION_METADATA?: WorkerVersionMetadata;
|
|
277
279
|
VERCEL_PROTECTION_BYPASS_TOKEN?: string;
|
|
278
280
|
DEEPLINE_PLAY_PREVIEW_SLUG?: string;
|
|
279
281
|
/**
|
|
@@ -3038,27 +3040,44 @@ export class DynamicWorkflow extends WorkflowEntrypoint<
|
|
|
3038
3040
|
: null,
|
|
3039
3041
|
},
|
|
3040
3042
|
);
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3043
|
+
if (
|
|
3044
|
+
shouldPublishWorkflowRuntimeFailure({
|
|
3045
|
+
error: innerError,
|
|
3046
|
+
retryAttempts: 0,
|
|
3047
|
+
})
|
|
3048
|
+
) {
|
|
3049
|
+
await markWorkflowRuntimeFailure({
|
|
3050
|
+
env,
|
|
3051
|
+
event: innerEvent,
|
|
3052
|
+
error: innerError,
|
|
3053
|
+
}).catch((markError) => {
|
|
3054
|
+
console.error(
|
|
3055
|
+
'[coordinator] failed to forward DynamicWorkflow runner error',
|
|
3056
|
+
{
|
|
3057
|
+
graphHash,
|
|
3058
|
+
message:
|
|
3059
|
+
markError instanceof Error
|
|
3060
|
+
? markError.message
|
|
3061
|
+
: String(markError),
|
|
3062
|
+
},
|
|
3063
|
+
);
|
|
3064
|
+
});
|
|
3065
|
+
await writeCoordinatorTerminalState(env, {
|
|
3066
|
+
runId: runIdForTrace,
|
|
3067
|
+
status: 'failed',
|
|
3068
|
+
error: failure.message,
|
|
3069
|
+
}).catch(() => undefined);
|
|
3070
|
+
} else {
|
|
3071
|
+
console.warn(
|
|
3072
|
+
'[coordinator] DynamicWorkflow platform reset will be retried; not publishing durable run.failed',
|
|
3048
3073
|
{
|
|
3049
3074
|
graphHash,
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
: String(markError),
|
|
3075
|
+
runId: runIdForTrace,
|
|
3076
|
+
errorCode: failure.code,
|
|
3077
|
+
retryLimit: PLATFORM_DEPLOY_WORKFLOW_RETRY_LIMIT,
|
|
3054
3078
|
},
|
|
3055
3079
|
);
|
|
3056
|
-
}
|
|
3057
|
-
await writeCoordinatorTerminalState(env, {
|
|
3058
|
-
runId: runIdForTrace,
|
|
3059
|
-
status: 'failed',
|
|
3060
|
-
error: failure.message,
|
|
3061
|
-
}).catch(() => undefined);
|
|
3080
|
+
}
|
|
3062
3081
|
throw innerError;
|
|
3063
3082
|
}
|
|
3064
3083
|
},
|
|
@@ -3136,7 +3155,20 @@ async function coordinatorRouteFetch(
|
|
|
3136
3155
|
): Promise<Response> {
|
|
3137
3156
|
const url = new URL(request.url);
|
|
3138
3157
|
if (url.pathname === '/health') {
|
|
3139
|
-
|
|
3158
|
+
const headers = new Headers();
|
|
3159
|
+
headers.set(
|
|
3160
|
+
'x-deepline-runtime-deploy-version',
|
|
3161
|
+
env.CF_VERSION_METADATA?.id ??
|
|
3162
|
+
env.DEEPLINE_COORDINATOR_DEPLOY_MARKER ??
|
|
3163
|
+
'',
|
|
3164
|
+
);
|
|
3165
|
+
if (env.DEEPLINE_COORDINATOR_DEPLOY_MARKER) {
|
|
3166
|
+
headers.set(
|
|
3167
|
+
'x-deepline-deploy-marker',
|
|
3168
|
+
env.DEEPLINE_COORDINATOR_DEPLOY_MARKER,
|
|
3169
|
+
);
|
|
3170
|
+
}
|
|
3171
|
+
return new Response('ok', { status: 200, headers });
|
|
3140
3172
|
}
|
|
3141
3173
|
if (url.pathname === '/warmup/submit') {
|
|
3142
3174
|
const authError = authorizeCoordinatorControlRequest({ request, env });
|
|
@@ -3174,6 +3206,10 @@ async function coordinatorRouteFetch(
|
|
|
3174
3206
|
return Response.json({
|
|
3175
3207
|
ok: true,
|
|
3176
3208
|
deployMarker: env.DEEPLINE_COORDINATOR_DEPLOY_MARKER ?? null,
|
|
3209
|
+
runtimeDeployVersion:
|
|
3210
|
+
env.CF_VERSION_METADATA?.id ??
|
|
3211
|
+
env.DEEPLINE_COORDINATOR_DEPLOY_MARKER ??
|
|
3212
|
+
null,
|
|
3177
3213
|
});
|
|
3178
3214
|
}
|
|
3179
3215
|
if (url.pathname === '/staged-files/put') {
|
|
@@ -3683,6 +3719,10 @@ async function handleWorkflowRoute(input: {
|
|
|
3683
3719
|
status: 'submitted',
|
|
3684
3720
|
workflowInstanceId: instance.id,
|
|
3685
3721
|
instanceState,
|
|
3722
|
+
runtimeDeployVersion:
|
|
3723
|
+
env.CF_VERSION_METADATA?.id ??
|
|
3724
|
+
env.DEEPLINE_COORDINATOR_DEPLOY_MARKER ??
|
|
3725
|
+
null,
|
|
3686
3726
|
coordinatorTimings,
|
|
3687
3727
|
});
|
|
3688
3728
|
} finally {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { normalizePlayRunFailure } from '../../../shared_libs/play-runtime/run-failure';
|
|
2
2
|
|
|
3
|
-
export const PLATFORM_DEPLOY_WORKFLOW_RETRY_LIMIT =
|
|
3
|
+
export const PLATFORM_DEPLOY_WORKFLOW_RETRY_LIMIT = 5;
|
|
4
4
|
|
|
5
5
|
export type WorkflowRetryDecision =
|
|
6
6
|
| {
|
|
@@ -44,3 +44,17 @@ export function decideWorkflowPlatformRetry(input: {
|
|
|
44
44
|
message: failure.message,
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
export function shouldPublishWorkflowRuntimeFailure(input: {
|
|
49
|
+
error: unknown;
|
|
50
|
+
retryAttempts: number;
|
|
51
|
+
}): boolean {
|
|
52
|
+
const message =
|
|
53
|
+
input.error instanceof Error ? input.error.message : String(input.error);
|
|
54
|
+
const decision = decideWorkflowPlatformRetry({
|
|
55
|
+
workflowStatus: 'errored',
|
|
56
|
+
error: message,
|
|
57
|
+
retryAttempts: input.retryAttempts,
|
|
58
|
+
});
|
|
59
|
+
return decision.action !== 'retry';
|
|
60
|
+
}
|
|
@@ -15,31 +15,21 @@
|
|
|
15
15
|
* 1. `options.baseUrl`
|
|
16
16
|
* 2. `DEEPLINE_HOST_URL`
|
|
17
17
|
* 3. nearest project `.env.deepline`
|
|
18
|
-
* 4.
|
|
19
|
-
* 5. production
|
|
20
|
-
* 6. production fallback: `https://code.deepline.com`
|
|
18
|
+
* 4. production host auth file
|
|
19
|
+
* 5. production fallback: `https://code.deepline.com`
|
|
21
20
|
*
|
|
22
21
|
* API key:
|
|
23
22
|
* 1. `options.apiKey`
|
|
24
23
|
* 2. `DEEPLINE_API_KEY`
|
|
25
24
|
* 3. nearest project `.env.deepline`
|
|
26
|
-
* 4.
|
|
27
|
-
* 5. host auth file for the resolved base URL
|
|
25
|
+
* 4. host auth file for the resolved base URL
|
|
28
26
|
*
|
|
29
27
|
* App/runtime env files such as `.env`, `.env.local`, and `.env.worktree` do
|
|
30
28
|
* not route the SDK CLI. Put CLI routing in `.env.deepline`.
|
|
31
29
|
*
|
|
32
30
|
* @module
|
|
33
31
|
*/
|
|
34
|
-
import {
|
|
35
|
-
existsSync,
|
|
36
|
-
mkdirSync,
|
|
37
|
-
readdirSync,
|
|
38
|
-
realpathSync,
|
|
39
|
-
readFileSync,
|
|
40
|
-
statSync,
|
|
41
|
-
writeFileSync,
|
|
42
|
-
} from 'node:fs';
|
|
32
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
43
33
|
import { homedir } from 'node:os';
|
|
44
34
|
import { dirname, join, resolve } from 'node:path';
|
|
45
35
|
import type { DeeplineClientOptions, ResolvedConfig } from './types.js';
|
|
@@ -58,41 +48,8 @@ const DEFAULT_TIMEOUT = 60_000;
|
|
|
58
48
|
const DEFAULT_MAX_RETRIES = 3;
|
|
59
49
|
|
|
60
50
|
const PROJECT_DEEPLINE_ENV_FILE = '.env.deepline';
|
|
61
|
-
const COWORK_IGNORED_WORKSPACE_DIRS = new Set([
|
|
62
|
-
'.auto-memory',
|
|
63
|
-
'.claude',
|
|
64
|
-
'.remote-plugins',
|
|
65
|
-
'outputs',
|
|
66
|
-
'plugins',
|
|
67
|
-
'uploads',
|
|
68
|
-
]);
|
|
69
|
-
const COWORK_PROJECT_MARKERS = [
|
|
70
|
-
'.deepline',
|
|
71
|
-
'.env.deepline',
|
|
72
|
-
'.git',
|
|
73
|
-
'AGENTS.md',
|
|
74
|
-
'package.json',
|
|
75
|
-
'pyproject.toml',
|
|
76
|
-
];
|
|
77
51
|
|
|
78
52
|
type EnvValues = Record<string, string>;
|
|
79
|
-
type ProjectEnvCandidate = {
|
|
80
|
-
filePath: string;
|
|
81
|
-
env: EnvValues;
|
|
82
|
-
source: 'nearest' | 'cowork';
|
|
83
|
-
};
|
|
84
|
-
export type ProjectAuthSource = ProjectEnvCandidate;
|
|
85
|
-
export type ProjectPinTarget =
|
|
86
|
-
| {
|
|
87
|
-
ok: true;
|
|
88
|
-
dir: string;
|
|
89
|
-
source: 'nearest' | 'cowork' | 'cwd';
|
|
90
|
-
}
|
|
91
|
-
| {
|
|
92
|
-
ok: false;
|
|
93
|
-
reason: 'ambiguous_cowork_project';
|
|
94
|
-
candidates: string[];
|
|
95
|
-
};
|
|
96
53
|
|
|
97
54
|
/**
|
|
98
55
|
* Convert a base URL to a filesystem-safe slug for per-host config storage.
|
|
@@ -162,161 +119,9 @@ function findNearestEnvFile(
|
|
|
162
119
|
}
|
|
163
120
|
}
|
|
164
121
|
|
|
165
|
-
function isDirectory(path: string): boolean {
|
|
166
|
-
try {
|
|
167
|
-
return statSync(path).isDirectory();
|
|
168
|
-
} catch {
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function canonicalPath(path: string): string {
|
|
174
|
-
try {
|
|
175
|
-
return realpathSync(path);
|
|
176
|
-
} catch {
|
|
177
|
-
return resolve(path);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function isTruthy(value: string | undefined): boolean {
|
|
182
|
-
return /^(1|true|yes|on)$/i.test(value?.trim() ?? '');
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function sessionRootFromPath(path: string | undefined): string | null {
|
|
186
|
-
const trimmed = path?.trim();
|
|
187
|
-
if (!trimmed) return null;
|
|
188
|
-
const match = /^\/sessions\/[^/]+(?=\/|$)/.exec(trimmed);
|
|
189
|
-
return match?.[0] ?? null;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function coworkSessionRoot(): string | null {
|
|
193
|
-
const home = process.env.HOME?.trim();
|
|
194
|
-
const homeSessionRoot = sessionRootFromPath(home);
|
|
195
|
-
if (homeSessionRoot && isDirectory(join(homeSessionRoot, 'mnt'))) {
|
|
196
|
-
return homeSessionRoot;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const cwdSessionRoot = sessionRootFromPath(process.cwd());
|
|
200
|
-
if (cwdSessionRoot && isDirectory(join(cwdSessionRoot, 'mnt'))) {
|
|
201
|
-
return cwdSessionRoot;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
if (isTruthy(process.env.CLAUDE_CODE_REMOTE) && home) {
|
|
205
|
-
const mountedRoot = join(home, 'mnt');
|
|
206
|
-
if (isDirectory(mountedRoot)) return resolve(home);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return null;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function isCoworkLikeSandbox(): boolean {
|
|
213
|
-
const home = process.env.HOME?.trim();
|
|
214
|
-
return (
|
|
215
|
-
isTruthy(process.env.CLAUDE_CODE_REMOTE) ||
|
|
216
|
-
sessionRootFromPath(home) !== null ||
|
|
217
|
-
sessionRootFromPath(process.cwd()) !== null
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function coworkProjectScore(path: string): number {
|
|
222
|
-
let score = 0;
|
|
223
|
-
for (const marker of COWORK_PROJECT_MARKERS) {
|
|
224
|
-
if (existsSync(join(path, marker))) score += 1;
|
|
225
|
-
}
|
|
226
|
-
return score;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function listCoworkWorkspaceDirCandidates(): string[] {
|
|
230
|
-
if (!isCoworkLikeSandbox()) {
|
|
231
|
-
return [];
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const explicitProjectDir = process.env.CLAUDE_PROJECT_DIR?.trim();
|
|
235
|
-
if (explicitProjectDir && isDirectory(explicitProjectDir)) {
|
|
236
|
-
return [resolve(explicitProjectDir)];
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const sessionRoot = coworkSessionRoot();
|
|
240
|
-
if (!sessionRoot) return [];
|
|
241
|
-
|
|
242
|
-
const mountedRoot = join(sessionRoot, 'mnt');
|
|
243
|
-
if (!isDirectory(mountedRoot)) return [];
|
|
244
|
-
|
|
245
|
-
let names: string[];
|
|
246
|
-
try {
|
|
247
|
-
names = readdirSync(mountedRoot).sort();
|
|
248
|
-
} catch {
|
|
249
|
-
return [];
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const candidates: string[] = [];
|
|
253
|
-
for (const name of names) {
|
|
254
|
-
if (name.startsWith('.') || COWORK_IGNORED_WORKSPACE_DIRS.has(name)) {
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
const candidate = join(mountedRoot, name);
|
|
258
|
-
if (isDirectory(candidate)) candidates.push(candidate);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (candidates.length <= 1) return candidates;
|
|
262
|
-
|
|
263
|
-
const projectLike = candidates.filter(
|
|
264
|
-
(candidate) => coworkProjectScore(candidate) > 0,
|
|
265
|
-
);
|
|
266
|
-
return projectLike.length > 0 ? projectLike : candidates;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function isInIgnoredCoworkMount(path: string): boolean {
|
|
270
|
-
const sessionRoot = coworkSessionRoot();
|
|
271
|
-
if (!sessionRoot) return false;
|
|
272
|
-
const mountedRoot = canonicalPath(join(sessionRoot, 'mnt'));
|
|
273
|
-
const resolvedPath = canonicalPath(path);
|
|
274
|
-
const prefix = `${mountedRoot}/`;
|
|
275
|
-
if (!resolvedPath.startsWith(prefix)) return false;
|
|
276
|
-
const relativePath = resolvedPath.slice(prefix.length);
|
|
277
|
-
const mountName = relativePath.split('/')[0];
|
|
278
|
-
return (
|
|
279
|
-
mountName.startsWith('.') || COWORK_IGNORED_WORKSPACE_DIRS.has(mountName)
|
|
280
|
-
);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function detectCoworkWorkspaceDir(): string | null {
|
|
284
|
-
const candidates = listCoworkWorkspaceDirCandidates();
|
|
285
|
-
return candidates.length === 1 ? candidates[0] : null;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function loadProjectEnvCandidates(
|
|
289
|
-
startDir: string = process.cwd(),
|
|
290
|
-
): ProjectEnvCandidate[] {
|
|
291
|
-
const filePaths: string[] = [];
|
|
292
|
-
const sources = new Map<string, ProjectEnvCandidate['source']>();
|
|
293
|
-
const nearestFile = findNearestEnvFile(PROJECT_DEEPLINE_ENV_FILE, startDir);
|
|
294
|
-
if (nearestFile && !isInIgnoredCoworkMount(nearestFile)) {
|
|
295
|
-
filePaths.push(nearestFile);
|
|
296
|
-
sources.set(resolve(nearestFile), 'nearest');
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const coworkWorkspaceDir = detectCoworkWorkspaceDir();
|
|
300
|
-
if (coworkWorkspaceDir) {
|
|
301
|
-
const coworkFile = join(coworkWorkspaceDir, PROJECT_DEEPLINE_ENV_FILE);
|
|
302
|
-
if (
|
|
303
|
-
existsSync(coworkFile) &&
|
|
304
|
-
!filePaths.some((filePath) => resolve(filePath) === resolve(coworkFile))
|
|
305
|
-
) {
|
|
306
|
-
filePaths.push(coworkFile);
|
|
307
|
-
sources.set(resolve(coworkFile), 'cowork');
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
return filePaths.map((filePath) => ({
|
|
312
|
-
filePath,
|
|
313
|
-
env: parseEnvFile(filePath),
|
|
314
|
-
source: sources.get(resolve(filePath)) ?? 'nearest',
|
|
315
|
-
}));
|
|
316
|
-
}
|
|
317
|
-
|
|
318
122
|
function loadProjectDeeplineEnv(startDir = process.cwd()): EnvValues {
|
|
319
|
-
|
|
123
|
+
const filePath = findNearestEnvFile(PROJECT_DEEPLINE_ENV_FILE, startDir);
|
|
124
|
+
return filePath ? parseEnvFile(filePath) : {};
|
|
320
125
|
}
|
|
321
126
|
|
|
322
127
|
function normalizeBaseUrl(baseUrl: string): string {
|
|
@@ -399,13 +204,11 @@ function loadGlobalCliEnv(): EnvValues {
|
|
|
399
204
|
* Auto-detect the best base URL when none is explicitly provided.
|
|
400
205
|
*/
|
|
401
206
|
function autoDetectBaseUrl(): string {
|
|
402
|
-
const
|
|
207
|
+
const projectEnv = loadProjectDeeplineEnv();
|
|
403
208
|
const globalEnv = loadGlobalCliEnv();
|
|
404
209
|
return (
|
|
405
210
|
normalizeBaseUrl(process.env[HOST_URL_ENV] ?? '') ||
|
|
406
|
-
|
|
407
|
-
...projectEnvs.map(({ env }) => normalizeBaseUrl(env[HOST_URL_ENV])),
|
|
408
|
-
) ||
|
|
211
|
+
normalizeBaseUrl(projectEnv[HOST_URL_ENV] ?? '') ||
|
|
409
212
|
normalizeBaseUrl(globalEnv[HOST_URL_ENV] ?? '') ||
|
|
410
213
|
PROD_URL
|
|
411
214
|
);
|
|
@@ -416,38 +219,18 @@ export function resolveApiKeyForBaseUrl(
|
|
|
416
219
|
explicitApiKey?: string | null,
|
|
417
220
|
): string {
|
|
418
221
|
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
|
419
|
-
const
|
|
222
|
+
const projectEnv = loadProjectDeeplineEnv();
|
|
420
223
|
const cliEnv = loadCliEnv(normalizedBaseUrl || baseUrl);
|
|
224
|
+
const projectBaseUrl = normalizeBaseUrl(projectEnv[HOST_URL_ENV] ?? '');
|
|
225
|
+
const projectKeyApplies = projectBaseUrl === normalizedBaseUrl;
|
|
421
226
|
return firstNonEmpty(
|
|
422
227
|
explicitApiKey,
|
|
423
228
|
process.env[API_KEY_ENV],
|
|
424
|
-
|
|
425
|
-
const projectBaseUrl = normalizeBaseUrl(env[HOST_URL_ENV] ?? '');
|
|
426
|
-
return projectBaseUrl === normalizedBaseUrl ? env[API_KEY_ENV] : '';
|
|
427
|
-
}),
|
|
229
|
+
projectKeyApplies ? projectEnv[API_KEY_ENV] : '',
|
|
428
230
|
cliEnv[API_KEY_ENV],
|
|
429
231
|
);
|
|
430
232
|
}
|
|
431
233
|
|
|
432
|
-
function getResolvedProjectAuthSource(
|
|
433
|
-
baseUrl: string,
|
|
434
|
-
apiKey: string,
|
|
435
|
-
startDir: string = process.cwd(),
|
|
436
|
-
): ProjectAuthSource | null {
|
|
437
|
-
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
|
|
438
|
-
const normalizedApiKey = apiKey.trim();
|
|
439
|
-
if (!normalizedBaseUrl || !normalizedApiKey) return null;
|
|
440
|
-
return (
|
|
441
|
-
loadProjectEnvCandidates(startDir).find(({ env }) => {
|
|
442
|
-
const projectBaseUrl = normalizeBaseUrl(env[HOST_URL_ENV] ?? '');
|
|
443
|
-
return (
|
|
444
|
-
projectBaseUrl === normalizedBaseUrl &&
|
|
445
|
-
(env[API_KEY_ENV] ?? '').trim() === normalizedApiKey
|
|
446
|
-
);
|
|
447
|
-
}) ?? null
|
|
448
|
-
);
|
|
449
|
-
}
|
|
450
|
-
|
|
451
234
|
/**
|
|
452
235
|
* Resolve SDK configuration from the public SDK CLI env contract.
|
|
453
236
|
*/
|
|
@@ -482,7 +265,6 @@ function mergeProjectEnvFile(filePath: string, values: EnvValues): void {
|
|
|
482
265
|
const merged = { ...existing, ...values };
|
|
483
266
|
const dir = dirname(filePath);
|
|
484
267
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
485
|
-
ensureProjectEnvIsIgnored(dir);
|
|
486
268
|
const allowedKeys = new Set([HOST_URL_ENV, API_KEY_ENV]);
|
|
487
269
|
const lines = Object.entries(merged)
|
|
488
270
|
.filter(([key, value]) => allowedKeys.has(key) && value !== '')
|
|
@@ -490,99 +272,20 @@ function mergeProjectEnvFile(filePath: string, values: EnvValues): void {
|
|
|
490
272
|
writeFileSync(filePath, `${lines.join('\n')}\n`, 'utf-8');
|
|
491
273
|
}
|
|
492
274
|
|
|
493
|
-
function ensureProjectEnvIsIgnored(dir: string): void {
|
|
494
|
-
const gitignorePath = join(dir, '.gitignore');
|
|
495
|
-
const entry = PROJECT_DEEPLINE_ENV_FILE;
|
|
496
|
-
const existing = existsSync(gitignorePath)
|
|
497
|
-
? readFileSync(gitignorePath, 'utf-8')
|
|
498
|
-
: '';
|
|
499
|
-
const alreadyIgnored = existing
|
|
500
|
-
.split(/\r?\n/)
|
|
501
|
-
.map((line) => line.trim())
|
|
502
|
-
.some((line) => line === entry || line === `/${entry}`);
|
|
503
|
-
if (alreadyIgnored) return;
|
|
504
|
-
const prefix = existing && !existing.endsWith('\n') ? '\n' : '';
|
|
505
|
-
writeFileSync(gitignorePath, `${existing}${prefix}${entry}\n`, 'utf-8');
|
|
506
|
-
}
|
|
507
|
-
|
|
508
275
|
export function saveProjectDeeplineEnvValues(
|
|
509
276
|
values: EnvValues,
|
|
510
277
|
startDir: string = process.cwd(),
|
|
511
278
|
): string[] {
|
|
512
|
-
const
|
|
513
|
-
if (!target.ok) {
|
|
514
|
-
throw new ConfigError(
|
|
515
|
-
`Cowork project folder is ambiguous. Candidate folders: ${target.candidates.join(
|
|
516
|
-
', ',
|
|
517
|
-
)}. Set CLAUDE_PROJECT_DIR or cd into the intended project folder before running this command.`,
|
|
518
|
-
);
|
|
519
|
-
}
|
|
520
|
-
const filePath = join(target.dir, PROJECT_DEEPLINE_ENV_FILE);
|
|
279
|
+
const filePath = join(resolve(startDir), PROJECT_DEEPLINE_ENV_FILE);
|
|
521
280
|
mergeProjectEnvFile(filePath, values);
|
|
522
281
|
return [filePath];
|
|
523
282
|
}
|
|
524
283
|
|
|
525
|
-
export function resolveProjectPinDir(startDir: string = process.cwd()): string {
|
|
526
|
-
const target = resolveProjectPinTarget(startDir);
|
|
527
|
-
if (!target.ok) {
|
|
528
|
-
throw new ConfigError(
|
|
529
|
-
`Cowork project folder is ambiguous. Candidate folders: ${target.candidates.join(
|
|
530
|
-
', ',
|
|
531
|
-
)}. Set CLAUDE_PROJECT_DIR or cd into the intended project folder before running this command.`,
|
|
532
|
-
);
|
|
533
|
-
}
|
|
534
|
-
return target.dir;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
export function resolveProjectPinTarget(
|
|
538
|
-
startDir: string = process.cwd(),
|
|
539
|
-
): ProjectPinTarget {
|
|
540
|
-
const nearestFile = findNearestEnvFile(PROJECT_DEEPLINE_ENV_FILE, startDir);
|
|
541
|
-
if (nearestFile && !isInIgnoredCoworkMount(nearestFile)) {
|
|
542
|
-
return { ok: true, dir: dirname(nearestFile), source: 'nearest' };
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const coworkCandidates = listCoworkWorkspaceDirCandidates();
|
|
546
|
-
if (coworkCandidates.length === 1) {
|
|
547
|
-
return { ok: true, dir: coworkCandidates[0], source: 'cowork' };
|
|
548
|
-
}
|
|
549
|
-
if (coworkCandidates.length > 1) {
|
|
550
|
-
const resolvedStartDir = canonicalPath(startDir);
|
|
551
|
-
const cwdCandidate = coworkCandidates.find((candidate) => {
|
|
552
|
-
const resolvedCandidate = canonicalPath(candidate);
|
|
553
|
-
return (
|
|
554
|
-
resolvedStartDir === resolvedCandidate ||
|
|
555
|
-
resolvedStartDir.startsWith(`${resolvedCandidate}/`)
|
|
556
|
-
);
|
|
557
|
-
});
|
|
558
|
-
if (cwdCandidate) {
|
|
559
|
-
return { ok: true, dir: cwdCandidate, source: 'cowork' };
|
|
560
|
-
}
|
|
561
|
-
return {
|
|
562
|
-
ok: false,
|
|
563
|
-
reason: 'ambiguous_cowork_project',
|
|
564
|
-
candidates: coworkCandidates,
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
return { ok: true, dir: resolve(startDir), source: 'cwd' };
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
export function getActiveProjectAuthSource(
|
|
572
|
-
startDir: string = process.cwd(),
|
|
573
|
-
): ProjectAuthSource | null {
|
|
574
|
-
return loadProjectEnvCandidates(startDir)[0] ?? null;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
284
|
export {
|
|
578
285
|
baseUrlSlug,
|
|
579
286
|
loadCliEnv,
|
|
580
287
|
loadGlobalCliEnv,
|
|
581
|
-
loadProjectDeeplineEnv,
|
|
582
|
-
getResolvedProjectAuthSource,
|
|
583
|
-
listCoworkWorkspaceDirCandidates,
|
|
584
288
|
parseEnvFile,
|
|
585
|
-
detectCoworkWorkspaceDir,
|
|
586
289
|
autoDetectBaseUrl,
|
|
587
290
|
PROD_URL,
|
|
588
291
|
};
|
|
@@ -101,10 +101,10 @@ export const SDK_RELEASE = {
|
|
|
101
101
|
// 0.1.108 ships explicit dataset column/tool recompute policy and removes
|
|
102
102
|
// the SDK enrich generator's one-second stale policy.
|
|
103
103
|
// 0.1.110 ships authored V2 prebuilts and required top-level play descriptions.
|
|
104
|
-
version: '0.1.
|
|
104
|
+
version: '0.1.140',
|
|
105
105
|
apiContract: '2026-06-dataset-column-cell-stale-hard-cutover',
|
|
106
106
|
supportPolicy: {
|
|
107
|
-
latest: '0.1.
|
|
107
|
+
latest: '0.1.140',
|
|
108
108
|
minimumSupported: '0.1.53',
|
|
109
109
|
deprecatedBelow: '0.1.53',
|
|
110
110
|
commandMinimumSupported: [
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* The header is harmless when there is only one active version (the default).
|
|
14
14
|
*/
|
|
15
15
|
export const COORDINATOR_VERSION_KEY_HEADER = 'Cloudflare-Workers-Version-Key';
|
|
16
|
+
export const COORDINATOR_VERSION_OVERRIDES_HEADER =
|
|
17
|
+
'Cloudflare-Workers-Version-Overrides';
|
|
16
18
|
/**
|
|
17
19
|
* Shared secret the coordinator uses to authenticate dispatcher (Vercel app)
|
|
18
20
|
* traffic. The coordinator also sanity-checks `x-deepline-run-scope` matches
|
|
@@ -65,6 +67,8 @@ export function coordinatorRequestHeaders(input: {
|
|
|
65
67
|
runId: string;
|
|
66
68
|
contentType?: string | null;
|
|
67
69
|
internalToken?: string | null;
|
|
70
|
+
runtimeDeployVersion?: string | null;
|
|
71
|
+
coordinatorWorkerName?: string | null;
|
|
68
72
|
/**
|
|
69
73
|
* When set, the coordinator validates this matches the runId in the URL.
|
|
70
74
|
* Pass the runId on `/cancel` / `/signal` calls so a leaked dispatcher
|
|
@@ -80,6 +84,19 @@ export function coordinatorRequestHeaders(input: {
|
|
|
80
84
|
if (trimmed) {
|
|
81
85
|
headers[COORDINATOR_VERSION_KEY_HEADER] = trimmed;
|
|
82
86
|
}
|
|
87
|
+
const runtimeDeployVersion = input.runtimeDeployVersion?.trim();
|
|
88
|
+
const coordinatorWorkerName = input.coordinatorWorkerName?.trim();
|
|
89
|
+
if (
|
|
90
|
+
runtimeDeployVersion &&
|
|
91
|
+
coordinatorWorkerName &&
|
|
92
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
|
93
|
+
runtimeDeployVersion,
|
|
94
|
+
) &&
|
|
95
|
+
/^[A-Za-z0-9_-]+$/.test(coordinatorWorkerName)
|
|
96
|
+
) {
|
|
97
|
+
headers[COORDINATOR_VERSION_OVERRIDES_HEADER] =
|
|
98
|
+
`${coordinatorWorkerName}="${runtimeDeployVersion}"`;
|
|
99
|
+
}
|
|
83
100
|
const internalToken =
|
|
84
101
|
input.internalToken?.trim() || resolveInternalCoordinatorToken();
|
|
85
102
|
if (internalToken) {
|
|
@@ -2,7 +2,7 @@ const CLOUDFLARE_DURABLE_OBJECT_RESET_RE =
|
|
|
2
2
|
/Durable Object.*(?:code was updated|storage caused object)/;
|
|
3
3
|
|
|
4
4
|
export const PLATFORM_DEPLOY_INTERRUPTED_MESSAGE =
|
|
5
|
-
'Run interrupted by a platform deploy
|
|
5
|
+
'Run interrupted by a platform deploy. Deepline retries this automatically when possible; if this error is still visible, re-run the same command.';
|
|
6
6
|
|
|
7
7
|
export const INTERNAL_RUNTIME_STORAGE_ERROR_MESSAGE =
|
|
8
8
|
'Internal play runtime storage failed. Please retry the run; if this keeps happening, contact Deepline support with the run ID.';
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizePlayRunFailure } from './run-failure';
|
|
2
|
+
|
|
1
3
|
export type PlayRunLedgerStatus =
|
|
2
4
|
| 'queued'
|
|
3
5
|
| 'running'
|
|
@@ -560,6 +562,7 @@ function conflictingTerminalSnapshot(
|
|
|
560
562
|
base: PlayRunLedgerSnapshot,
|
|
561
563
|
eventType: keyof typeof TERMINAL_STATUS_BY_EVENT_TYPE,
|
|
562
564
|
occurredAt: number,
|
|
565
|
+
eventError?: string | null,
|
|
563
566
|
): PlayRunLedgerSnapshot | null {
|
|
564
567
|
if (!isTerminalPlayRunLedgerStatus(base.status)) {
|
|
565
568
|
return null;
|
|
@@ -567,6 +570,18 @@ function conflictingTerminalSnapshot(
|
|
|
567
570
|
if (TERMINAL_STATUS_BY_EVENT_TYPE[eventType] === base.status) {
|
|
568
571
|
return null;
|
|
569
572
|
}
|
|
573
|
+
if (
|
|
574
|
+
base.status === 'completed' &&
|
|
575
|
+
eventType === 'run.failed' &&
|
|
576
|
+
eventError &&
|
|
577
|
+
normalizePlayRunFailure(eventError).code === 'PLATFORM_DEPLOY_INTERRUPTED'
|
|
578
|
+
) {
|
|
579
|
+
return withTiming(
|
|
580
|
+
appendLogLines(base, [
|
|
581
|
+
`[ledger] platform deploy terminal event ${eventType} ignored; status already ${base.status}`,
|
|
582
|
+
]),
|
|
583
|
+
);
|
|
584
|
+
}
|
|
570
585
|
const terminalAt = base.finishedAt ?? base.updatedAt ?? 0;
|
|
571
586
|
if (occurredAt > terminalAt) {
|
|
572
587
|
// Newer terminal evidence reconciles the run. This covers replay/receipt
|
|
@@ -683,7 +698,12 @@ export function reducePlayRunLedgerEvent(
|
|
|
683
698
|
);
|
|
684
699
|
case 'run.failed':
|
|
685
700
|
return (
|
|
686
|
-
conflictingTerminalSnapshot(
|
|
701
|
+
conflictingTerminalSnapshot(
|
|
702
|
+
base,
|
|
703
|
+
event.type,
|
|
704
|
+
occurredAt,
|
|
705
|
+
event.error,
|
|
706
|
+
) ??
|
|
687
707
|
withTiming({
|
|
688
708
|
...settleRunningStepsOnTerminal(base, 'failed', occurredAt),
|
|
689
709
|
status: 'failed',
|
|
@@ -697,7 +717,12 @@ export function reducePlayRunLedgerEvent(
|
|
|
697
717
|
);
|
|
698
718
|
case 'run.cancelled':
|
|
699
719
|
return (
|
|
700
|
-
conflictingTerminalSnapshot(
|
|
720
|
+
conflictingTerminalSnapshot(
|
|
721
|
+
base,
|
|
722
|
+
event.type,
|
|
723
|
+
occurredAt,
|
|
724
|
+
event.error,
|
|
725
|
+
) ??
|
|
701
726
|
withTiming({
|
|
702
727
|
...settleRunningStepsOnTerminal(base, 'failed', occurredAt),
|
|
703
728
|
status: 'cancelled',
|