gencow 0.1.245 → 0.1.246

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.
@@ -0,0 +1,52 @@
1
+ const DEFAULT_ADMISSION_TIMEOUT_MS = 5 * 60 * 1000;
2
+
3
+ function pendingResponse({ requestId, appName, resumeCommand = null }) {
4
+ const response = new Response(
5
+ JSON.stringify({
6
+ error: "Deployment admission is still running. The server request was not cancelled.",
7
+ code: "PLATFORM_DEPLOYMENT_ADMISSION_PENDING",
8
+ stage: "admission",
9
+ retryClass: "retryable-infra",
10
+ userAction: "VIEW_STATUS",
11
+ requestId,
12
+ appName,
13
+ statusCommand: "gencow deploy status",
14
+ resumeCommand,
15
+ serverOperationContinues: "UNKNOWN",
16
+ databaseState: "UNKNOWN",
17
+ incumbentServing: "UNKNOWN",
18
+ operationState: "UNKNOWN",
19
+ }),
20
+ { status: 503, headers: { "Content-Type": "application/json", "Retry-After": "5" } },
21
+ );
22
+ // The caller must surface this state without replaying the same admission
23
+ // request as a new deployment. Keep this marker private to the in-process
24
+ // response path; the public body carries the stable diagnostic contract.
25
+ response.deploymentAdmissionPending = true;
26
+ return response;
27
+ }
28
+
29
+ export async function postDeploymentAdmissionWithTimeout({
30
+ send,
31
+ requestId = null,
32
+ appName,
33
+ resumeCommand = requestId ? `gencow deploy --resume ${requestId}` : null,
34
+ timeoutMs = DEFAULT_ADMISSION_TIMEOUT_MS,
35
+ setTimeoutImpl = setTimeout,
36
+ clearTimeoutImpl = clearTimeout,
37
+ }) {
38
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) throw new Error("DEPLOYMENT_ADMISSION_TIMEOUT_INVALID");
39
+ const controller = new AbortController();
40
+ let timer;
41
+ const deadline = new Promise((resolve) => {
42
+ timer = setTimeoutImpl(() => {
43
+ resolve(pendingResponse({ requestId, appName, resumeCommand }));
44
+ }, timeoutMs);
45
+ timer?.unref?.();
46
+ });
47
+ try {
48
+ return await Promise.race([send({ signal: controller.signal }), deadline]);
49
+ } finally {
50
+ if (timer !== undefined) clearTimeoutImpl(timer);
51
+ }
52
+ }
@@ -310,6 +310,32 @@ export function renderDeployFailureDiagnosticLines(responseBody, statusText, opt
310
310
  Number.isSafeInteger(Number(contract.operationId))
311
311
  ? `gencow deploy status --deployment ${contract.operationId}`
312
312
  : "gencow deploy status";
313
+ if (contract.code === "PLATFORM_DEPLOYMENT_ADMISSION_PENDING") {
314
+ lines.push(
315
+ `Database state: ${safeText(responseBody.databaseState, 32) ?? "UNKNOWN"}`,
316
+ `Incumbent serving: ${safeText(responseBody.incumbentServing, 32) ?? "UNKNOWN"}`,
317
+ `Operation state: ${safeText(responseBody.operationState, 32) ?? "UNKNOWN"}`,
318
+ );
319
+ const requestId = safeText(responseBody.requestId, 128);
320
+ if (requestId && /^[A-Za-z0-9_-]{8,128}$/u.test(requestId)) lines.push(`Request ID: ${requestId}`);
321
+ lines.push("Server outcome: unknown; inspect the existing request before submitting another deployment.");
322
+ const resumeCommand = safeText(responseBody.resumeCommand, 240);
323
+ const resumeMatch = resumeCommand?.match(
324
+ /^gencow deploy (?:--resume [A-Za-z0-9_-]{8,128}|--static(?: --prod)? --app ([A-Za-z0-9_-]{1,128})(?: --client [A-Za-z0-9_-]{1,128})? --resume [A-Za-z0-9_-]{8,128})$/u,
325
+ );
326
+ const resumeToken = resumeCommand?.match(/--resume ([A-Za-z0-9_-]{8,128})$/u)?.[1];
327
+ const resumeAppName = resumeMatch?.[1];
328
+ const expectedResumeApp = options.expectedAppName ?? responseBody.appName;
329
+ if (
330
+ resumeMatch &&
331
+ requestId &&
332
+ resumeToken === requestId &&
333
+ (!resumeAppName || !expectedResumeApp || resumeAppName === expectedResumeApp)
334
+ ) {
335
+ lines.push(`Next command: ${resumeCommand}`);
336
+ return lines;
337
+ }
338
+ }
313
339
  lines.push(`Next command: ${selfService?.action.command ?? exactStatusCommand}`);
314
340
  if (contract.blockingOperationId && responseBody.retryReadinessReason === "ACTIVE_OWNER" && selfService) {
315
341
  lines.push(`Then check retry gate: ${exactStatusCommand}`);
@@ -20,6 +20,7 @@ import { buildDeployClaimFromArchive } from "./deploy-package-claim.mjs";
20
20
  import { renderDeployFailureDiagnosticLines } from "./deploy-failure-diagnostic.mjs";
21
21
  import { readJsonObjectResponse } from "./http-response-json.mjs";
22
22
  import { pollAcceptedDeploymentOperation } from "./deployment-operation-poll.mjs";
23
+ import { postDeploymentAdmissionWithTimeout } from "./deploy-admission-boundary.mjs";
23
24
  import { submitDeploymentWithIdempotentRetry } from "./deployment-admission-retry.mjs";
24
25
  import { acquireDeployReceipt, releaseDeployReceipt } from "./deploy-receipt-store.mjs";
25
26
  import { CYAN, DIM, RED, RESET, YELLOW, error, info, log, success, warn } from "./output.mjs";
@@ -154,6 +155,7 @@ export function createDeployPackageRuntime({
154
155
  } = {}) {
155
156
  let migrationProjectConfig = null;
156
157
  let preparedDeployCodegenArtifacts = null;
158
+ let lastDeployOutcome = "idle";
157
159
  function cleanupPreparedDeploySource() {
158
160
  preparedDeployCodegenArtifacts?.cleanup?.();
159
161
  preparedDeployCodegenArtifacts = null;
@@ -596,6 +598,7 @@ export function createDeployPackageRuntime({
596
598
  releaseAttemptId = null,
597
599
  clientName = null,
598
600
  }) {
601
+ lastDeployOutcome = "pending";
599
602
  const cwd = cwdImpl();
600
603
  if (deployClaim?.claim?.migrationBundle?.manifestSha256) {
601
604
  emitMigrationResponsibilityNotice(warnImpl);
@@ -612,7 +615,7 @@ export function createDeployPackageRuntime({
612
615
  : null;
613
616
  let activeBundleBuffer = receipt?.bundleBuffer ?? bundleBuffer;
614
617
  let activeDeployClaim = receipt?.deployClaim ?? deployClaim;
615
- const deployOnce = async (targetAppId) =>
618
+ const deployOnce = async (targetAppId, signal) =>
616
619
  platformFetchImpl(creds, `/platform/apps/${targetAppId}/deploy?env=${envTarget}`, {
617
620
  method: "POST",
618
621
  headers: {
@@ -633,6 +636,7 @@ export function createDeployPackageRuntime({
633
636
  : {}),
634
637
  },
635
638
  body: activeBundleBuffer,
639
+ signal,
636
640
  });
637
641
  const pollAcceptedDeployment = (initialResponse) =>
638
642
  pollAcceptedDeploymentOperation({
@@ -643,11 +647,15 @@ export function createDeployPackageRuntime({
643
647
  setTimeoutImpl,
644
648
  });
645
649
  const submitDeployAdmission = () => {
646
- // A legacy request without a deploy claim has no stable pre-receipt
647
- // identity. Never retry that request automatically.
648
- if (!activeDeployClaim) return deployOnce(targetAppId);
649
650
  return submitDeploymentWithIdempotentRetry({
650
- submit: () => deployOnce(targetAppId),
651
+ submit: () =>
652
+ postDeploymentAdmissionWithTimeout({
653
+ requestId: activeDeployClaim?.claim?.requestId ?? explicitRequestId,
654
+ appName: targetAppId,
655
+ send: ({ signal }) => deployOnce(targetAppId, signal),
656
+ }),
657
+ retryOnResponse: Boolean(activeDeployClaim),
658
+ retryOnTransport: Boolean(activeDeployClaim),
651
659
  sleepImpl: (ms) => new Promise((resolveDelay) => setTimeoutImpl(resolveDelay, ms)),
652
660
  });
653
661
  };
@@ -679,6 +687,9 @@ export function createDeployPackageRuntime({
679
687
  if (deployRes.status === 202) {
680
688
  deployRes = await pollAcceptedDeployment(deployRes);
681
689
  }
690
+ } catch (error) {
691
+ lastDeployOutcome = "pending";
692
+ throw error;
682
693
  } finally {
683
694
  stopSpinner(spinner);
684
695
  }
@@ -727,6 +738,7 @@ export function createDeployPackageRuntime({
727
738
  errData?.code === "DEPLOY_REQUEST_IN_PROGRESS" ||
728
739
  errData?.code === "CONTROL_PLANE_OBSERVATION_INCOMPLETE" ||
729
740
  errData?.retryClass === "retryable-infra";
741
+ lastDeployOutcome = retryableResponse ? "pending" : "failed";
730
742
  if (receipt && !retryableResponse) releaseDeployReceipt(receipt);
731
743
  const migrationDiagnostic = isActionableMigrationDiagnostic(errData?.diagnostic)
732
744
  ? normalizeRetiredMigrationGateDiagnostic(extractActionableMigrationDiagnostic(errData.diagnostic))
@@ -751,6 +763,7 @@ export function createDeployPackageRuntime({
751
763
 
752
764
  const deployData = await readJsonObjectResponse(deployRes);
753
765
  if (!deployData) {
766
+ lastDeployOutcome = "unknown";
754
767
  errorImpl(
755
768
  `Deploy failed: no terminal deployment receipt was returned after packaging.\nCode: CLI_DEPLOY_TERMINAL_RECEIPT_MISSING\nStage: controller\nHTTP status: ${deployRes?.status ?? "unknown"}\nNext action: RETRY`,
756
769
  );
@@ -758,6 +771,7 @@ export function createDeployPackageRuntime({
758
771
  return null;
759
772
  }
760
773
  emitMigrationAdvisories(deployData?.warnings, warnImpl);
774
+ lastDeployOutcome = "completed";
761
775
  if (receipt) releaseDeployReceipt(receipt);
762
776
  const deployElapsed = ((Date.now() - startTime) / 1000).toFixed(1);
763
777
  logImpl("");
@@ -816,5 +830,6 @@ export function createDeployPackageRuntime({
816
830
  packageDeployProject,
817
831
  createCloudAppIfNeeded,
818
832
  deployBundleWithRetry,
833
+ getLastDeployOutcome: () => lastDeployOutcome,
819
834
  };
820
835
  }
@@ -28,10 +28,12 @@ export function parseDeployArgs(deployArgs) {
28
28
  isRollback: false,
29
29
  region: null,
30
30
  requestId: null,
31
+ resumeRequested: false,
31
32
  staticDeploy: false,
32
33
  staticDirArg: null,
33
34
  unknownArg: null,
34
35
  };
36
+ Object.defineProperty(parsed, "resumeRequested", { value: false, writable: true });
35
37
 
36
38
  for (let index = 0; index < deployArgs.length; index += 1) {
37
39
  const arg = deployArgs[index];
@@ -47,6 +49,7 @@ export function parseDeployArgs(deployArgs) {
47
49
  break;
48
50
  }
49
51
  parsed.requestId = value;
52
+ if (arg === "--resume") parsed.resumeRequested = true;
50
53
  index += 1;
51
54
  } else if (arg.startsWith("--request-id=") || arg.startsWith("--resume=")) {
52
55
  const value = arg.slice(arg.indexOf("=") + 1);
@@ -55,6 +58,7 @@ export function parseDeployArgs(deployArgs) {
55
58
  break;
56
59
  }
57
60
  parsed.requestId = value;
61
+ if (arg.startsWith("--resume=")) parsed.resumeRequested = true;
58
62
  } else if (arg === "--region") {
59
63
  const value = deployArgs[index + 1];
60
64
  if (!value || value.startsWith("-")) {
@@ -518,7 +522,7 @@ export function createDeployCommand({
518
522
  }
519
523
 
520
524
  let staticArtifactPreflight = null;
521
- if (parsed.staticDeploy) {
525
+ if (parsed.staticDeploy && !parsed.resumeRequested) {
522
526
  try {
523
527
  staticArtifactPreflight = preflightStaticDeployArtifactImpl({
524
528
  staticDirArg: resolvedStaticDirArg,
@@ -608,7 +612,7 @@ export function createDeployCommand({
608
612
  return;
609
613
  }
610
614
 
611
- if (parsed.envTarget === "prod" && appId && !parsed.existingBundleMode) {
615
+ if (parsed.envTarget === "prod" && appId && !parsed.existingBundleMode && !parsed.resumeRequested) {
612
616
  const prodTarget = await ensureProductionDeployTarget({
613
617
  appId: rootAppId,
614
618
  displayName,
@@ -649,6 +653,7 @@ export function createDeployCommand({
649
653
  envTarget: parsed.envTarget,
650
654
  forceDeploy: parsed.forceDeploy,
651
655
  requestId: parsed.requestId,
656
+ resumeStaticOnly: parsed.resumeRequested,
652
657
  gencowJsonPath,
653
658
  hasDetectedBackend,
654
659
  selectedClient,
@@ -153,12 +153,16 @@ export async function deployBackendPackage({
153
153
  releaseAttemptId: releaseAttempt?.attemptId ?? null,
154
154
  clientName,
155
155
  });
156
- if (!deployed) await markAttemptFailed("deploy", "DEPLOY_FAILED");
156
+ if (!deployed && !["pending", "unknown"].includes(activePackageRuntime.getLastDeployOutcome?.())) {
157
+ await markAttemptFailed("deploy", "DEPLOY_FAILED");
158
+ }
157
159
  } catch (caught) {
158
- await markAttemptFailed(
159
- packageResult ? "deploy" : "package",
160
- packageResult ? "DEPLOY_FAILED" : "PACKAGE_FAILED",
161
- );
160
+ if (activePackageRuntime.getLastDeployOutcome?.() !== "pending") {
161
+ await markAttemptFailed(
162
+ packageResult ? "deploy" : "package",
163
+ packageResult ? "DEPLOY_FAILED" : "PACKAGE_FAILED",
164
+ );
165
+ }
162
166
  throw caught;
163
167
  } finally {
164
168
  if (packageResult) {
@@ -220,6 +224,7 @@ export async function runStaticDeployFlow({
220
224
  envTarget,
221
225
  forceDeploy,
222
226
  requestId = null,
227
+ resumeStaticOnly = false,
223
228
  gencowJsonPath,
224
229
  hasDetectedBackend,
225
230
  staticDirArg,
@@ -251,7 +256,7 @@ export async function runStaticDeployFlow({
251
256
  const frontendCwd = cwdImpl();
252
257
  let activeAppId = appId;
253
258
  let artifactPreflight = staticArtifactPreflight;
254
- if (!artifactPreflight) {
259
+ if (!artifactPreflight && !resumeStaticOnly) {
255
260
  try {
256
261
  artifactPreflight = preflightStaticDeployArtifactImpl({
257
262
  staticDirArg,
@@ -265,8 +270,8 @@ export async function runStaticDeployFlow({
265
270
  return;
266
271
  }
267
272
  }
268
- const resolvedStaticDir = artifactPreflight?.targetDir;
269
- if (!resolvedStaticDir) {
273
+ const resolvedStaticDir = artifactPreflight?.targetDir ?? staticDirArg;
274
+ if (!resolvedStaticDir && !resumeStaticOnly) {
270
275
  errorImpl("Static files directory not found.");
271
276
  infoImpl("Auto-detect order: dist, out, build, .next/out");
272
277
  infoImpl("Specify manually: gencow deploy --static <dir>");
@@ -274,7 +279,7 @@ export async function runStaticDeployFlow({
274
279
  return;
275
280
  }
276
281
 
277
- const shouldDeployBackend = hasDetectedBackend;
282
+ const shouldDeployBackend = hasDetectedBackend && !resumeStaticOnly;
278
283
  let releaseAttemptId = null;
279
284
  if (shouldDeployBackend) {
280
285
  logImpl(`\n${BOLD}${CYAN}Gencow Deploy (Fullstack)${RESET}\n`);
@@ -321,8 +326,10 @@ export async function runStaticDeployFlow({
321
326
  envTarget,
322
327
  forceDeploy,
323
328
  hasBackend: shouldDeployBackend,
329
+ resumeStaticOnly,
324
330
  releaseAttemptId,
325
331
  releaseSourceRoot: backendRoot,
332
+ requestId,
326
333
  },
327
334
  });
328
335
  }
@@ -1,7 +1,10 @@
1
1
  const INITIAL_DEPLOYMENT_ADMISSION_ATTEMPTS = 3;
2
2
 
3
3
  function isRetryableInitialDeploymentAdmission(response) {
4
- return Number.isInteger(response?.status) && response.status >= 500 && response.status <= 599;
4
+ return response?.deploymentAdmissionPending !== true
5
+ && Number.isInteger(response?.status)
6
+ && response.status >= 500
7
+ && response.status <= 599;
5
8
  }
6
9
 
7
10
  /**
@@ -12,17 +15,19 @@ function isRetryableInitialDeploymentAdmission(response) {
12
15
  export async function submitDeploymentWithIdempotentRetry({
13
16
  submit,
14
17
  sleepImpl = (ms) => new Promise((resolveDelay) => setTimeout(resolveDelay, ms)),
18
+ retryOnResponse = true,
19
+ retryOnTransport = true,
15
20
  }) {
16
21
  let response;
17
22
  for (let attempt = 1; attempt <= INITIAL_DEPLOYMENT_ADMISSION_ATTEMPTS; attempt += 1) {
18
23
  try {
19
24
  response = await submit();
20
25
  } catch (error) {
21
- if (attempt === INITIAL_DEPLOYMENT_ADMISSION_ATTEMPTS) throw error;
26
+ if (!retryOnTransport || attempt === INITIAL_DEPLOYMENT_ADMISSION_ATTEMPTS) throw error;
22
27
  await sleepImpl(250 * attempt);
23
28
  continue;
24
29
  }
25
- if (!isRetryableInitialDeploymentAdmission(response) || attempt === INITIAL_DEPLOYMENT_ADMISSION_ATTEMPTS) {
30
+ if (!retryOnResponse || !isRetryableInitialDeploymentAdmission(response) || attempt === INITIAL_DEPLOYMENT_ADMISSION_ATTEMPTS) {
26
31
  return response;
27
32
  }
28
33
  await sleepImpl(250 * attempt);
@@ -20,6 +20,12 @@ import { getCliInvocationSelection } from "./project-context.mjs";
20
20
  import { pollAcceptedDeploymentOperation } from "./deployment-operation-poll.mjs";
21
21
  import { renderDeployFailureDiagnosticLines } from "./deploy-failure-diagnostic.mjs";
22
22
  import { writeProjectMetadata } from "./deploy-project-metadata.mjs";
23
+ import { postDeploymentAdmissionWithTimeout } from "./deploy-admission-boundary.mjs";
24
+ import {
25
+ loadStaticDeployReceipt,
26
+ releaseStaticDeployReceipt,
27
+ saveStaticDeployReceipt,
28
+ } from "./static-deploy-receipt-store.mjs";
23
29
 
24
30
  function shouldPreserveFinalizeAttempt(error) {
25
31
  return error?.retryable === true || error?.message === "APP_SERVING_ROUTE_COMMIT_PENDING";
@@ -242,23 +248,55 @@ export function createStaticDeployRuntime({
242
248
  opts = {},
243
249
  }) {
244
250
  const cwd = cwdImpl();
251
+ const receiptContext = {
252
+ clientName: opts.clientName ?? getCliInvocationSelection()?.client ?? null,
253
+ sourceRoot: resolvePathImpl(opts.releaseSourceRoot || cwd),
254
+ metadataPath: resolvePathImpl(gencowJsonPath || resolvePathImpl(cwd, "gencow.json")),
255
+ };
245
256
  const hasBackend = opts.hasBackend === true;
257
+ let resumedStaticReceipt = null;
258
+ if (opts.resumeStaticOnly && opts.requestId) {
259
+ if (!appId) {
260
+ errorImpl("Static deployment resume requires the original app target.");
261
+ infoImpl("Code: STATIC_DEPLOY_RECEIPT_SCOPE_REQUIRED");
262
+ exitImpl(1);
263
+ return;
264
+ }
265
+ try {
266
+ resumedStaticReceipt = loadStaticDeployReceipt({
267
+ cwd,
268
+ requestId: opts.requestId,
269
+ appId,
270
+ envTarget: opts.envTarget || "dev",
271
+ context: receiptContext,
272
+ });
273
+ } catch (caught) {
274
+ const contextMismatch = caught.message === "STATIC_DEPLOY_RECEIPT_CONTEXT_MISMATCH";
275
+ errorImpl("Static deployment resume could not validate the saved receipt.");
276
+ infoImpl(`Code: ${contextMismatch ? caught.message : "STATIC_DEPLOY_RECEIPT_UNAVAILABLE"}`);
277
+ infoImpl("No deployment request sent; saved receipt retained. Retry from the original workspace with the original client selection.");
278
+ exitImpl(1);
279
+ return;
280
+ }
281
+ }
246
282
  let artifactPreflight;
247
- try {
248
- artifactPreflight = preflightStaticDeployArtifact({
249
- staticDirArg,
250
- cwd,
251
- existsSyncImpl,
252
- lstatSyncImpl,
253
- readdirSyncImpl,
254
- resolvePathImpl,
255
- });
256
- } catch (manifestError) {
257
- errorImpl(manifestError instanceof Error ? manifestError.message : String(manifestError));
258
- exitImpl(1);
259
- return;
283
+ if (!resumedStaticReceipt) {
284
+ try {
285
+ artifactPreflight = preflightStaticDeployArtifact({
286
+ staticDirArg,
287
+ cwd,
288
+ existsSyncImpl,
289
+ lstatSyncImpl,
290
+ readdirSyncImpl,
291
+ resolvePathImpl,
292
+ });
293
+ } catch (manifestError) {
294
+ errorImpl(manifestError instanceof Error ? manifestError.message : String(manifestError));
295
+ exitImpl(1);
296
+ return;
297
+ }
260
298
  }
261
- const targetDir = artifactPreflight?.targetDir;
299
+ const targetDir = artifactPreflight?.targetDir ?? resumedStaticReceipt?.sourceDir;
262
300
 
263
301
  if (!targetDir) {
264
302
  errorImpl("Static files directory not found.");
@@ -273,10 +311,10 @@ export function createStaticDeployRuntime({
273
311
  infoImpl(appId ? `App ID: ${appId}` : `App: new (ID auto-generated)`);
274
312
  logImpl("");
275
313
 
276
- const { artifactManifest } = artifactPreflight;
277
- infoImpl(`Artifact: ${artifactManifest.mode} (${artifactManifest.fileCount} files)`);
314
+ const artifactManifest = artifactPreflight?.artifactManifest;
315
+ if (artifactManifest) infoImpl(`Artifact: ${artifactManifest.mode} (${artifactManifest.fileCount} files)`);
278
316
 
279
- try {
317
+ if (!resumedStaticReceipt) try {
280
318
  const apiRefFiles = findBuiltApiReferenceFiles({
281
319
  targetDir: resolvePathImpl(cwd, targetDir),
282
320
  readFileSyncImpl,
@@ -341,8 +379,8 @@ export function createStaticDeployRuntime({
341
379
  await new Promise((resolveDelay) => setTimeoutImpl(resolveDelay, 3000));
342
380
  }
343
381
 
344
- let releaseAttemptId = opts.releaseAttemptId || null;
345
- if (!releaseAttemptId && prepareReleaseAttemptImpl) {
382
+ let releaseAttemptId = opts.releaseAttemptId || resumedStaticReceipt?.releaseAttemptId || null;
383
+ if (!releaseAttemptId && !resumedStaticReceipt && prepareReleaseAttemptImpl) {
346
384
  const releaseAttempt = await prepareReleaseAttemptImpl({
347
385
  root: opts.releaseSourceRoot || cwd,
348
386
  sourceRootKind: "static",
@@ -376,6 +414,15 @@ export function createStaticDeployRuntime({
376
414
  const tmpBundle = resolvePathImpl(cwd, ".gencow", "static-bundle.tar.gz");
377
415
  mkdirSyncImpl(resolvePathImpl(tmpBundle, ".."), { recursive: true });
378
416
 
417
+ let bundleSize;
418
+ let bundleBuffer;
419
+ let deployClaim;
420
+ if (resumedStaticReceipt) {
421
+ bundleBuffer = resumedStaticReceipt.bundleBuffer;
422
+ bundleSize = bundleBuffer.length;
423
+ deployClaim = resumedStaticReceipt.deployClaim;
424
+ successImpl(`Resuming exact static bundle: ${(bundleSize / 1024).toFixed(1)} KB`);
425
+ } else {
379
426
  try {
380
427
  await createTarGzipArchive({
381
428
  cwd: resolvePathImpl(cwd, targetDir),
@@ -390,7 +437,7 @@ export function createStaticDeployRuntime({
390
437
  return;
391
438
  }
392
439
 
393
- const bundleSize = statSyncImpl(tmpBundle).size;
440
+ bundleSize = statSyncImpl(tmpBundle).size;
394
441
  if (bundleSize > 100 * 1024 * 1024) {
395
442
  await markAttemptFailed("package", "PACKAGE_TOO_LARGE");
396
443
  errorImpl(`Bundle too large: ${(bundleSize / (1024 * 1024)).toFixed(1)} MB (max 100 MB)`);
@@ -404,13 +451,13 @@ export function createStaticDeployRuntime({
404
451
  }
405
452
  successImpl(`Bundle created: ${(bundleSize / 1024).toFixed(1)} KB`);
406
453
 
407
- const bundleBuffer = readFileSyncImpl(tmpBundle);
408
- let deployClaim;
454
+ bundleBuffer = readFileSyncImpl(tmpBundle);
409
455
  try {
410
456
  deployClaim = await buildDeployClaimFromArchiveImpl({
411
457
  bundleBuffer,
412
458
  declaredRuntime: "none",
413
459
  cliVersion,
460
+ ...(opts.requestId ? { requestId: opts.requestId } : {}),
414
461
  });
415
462
  } catch (caught) {
416
463
  await markAttemptFailed("package", "DEPLOY_CONTRACT_FAILED");
@@ -423,6 +470,32 @@ export function createStaticDeployRuntime({
423
470
  exitImpl(1);
424
471
  return;
425
472
  }
473
+ }
474
+ let staticReceipt = resumedStaticReceipt;
475
+ if (!staticReceipt && deployClaim?.claim?.requestId) {
476
+ try {
477
+ staticReceipt = saveStaticDeployReceipt({
478
+ cwd,
479
+ requestId: deployClaim.claim.requestId,
480
+ appId,
481
+ envTarget: opts.envTarget || "dev",
482
+ sourceDir: targetDir,
483
+ context: receiptContext,
484
+ releaseAttemptId,
485
+ bundleBuffer,
486
+ deployClaim,
487
+ });
488
+ } catch (caught) {
489
+ if (caught.message === "STATIC_DEPLOY_RECEIPT_IDENTITY_CONFLICT") {
490
+ errorImpl("Static deploy request ID is already bound to a different bundle or release.");
491
+ infoImpl("Code: STATIC_DEPLOY_RECEIPT_IDENTITY_CONFLICT");
492
+ infoImpl("No deployment request sent; use the saved receipt or omit --request-id for a new deployment.");
493
+ exitImpl(1);
494
+ return;
495
+ }
496
+ throw caught;
497
+ }
498
+ }
426
499
 
427
500
  infoImpl("Deploying static files...");
428
501
  let deployRes;
@@ -430,22 +503,31 @@ export function createStaticDeployRuntime({
430
503
  try {
431
504
  deployRes = await postStaticDeployWithTransientProxyRetry({
432
505
  send: () =>
433
- platformFetchImpl(creds, `/platform/apps/${appId}/deploy-static`, {
434
- method: "POST",
435
- headers: {
436
- "Content-Type": "application/octet-stream",
437
- "X-Deploy-Local-Dir": cwd,
438
- "X-Gencow-Deploy-Claim": deployClaim.header,
439
- "X-Gencow-Deploy-Protocol": "1",
440
- "X-Gencow-CLI-Version": deployClaim.claim.generatedBy.version,
441
- ...(releaseAttemptId
442
- ? {
443
- "X-Gencow-Release-Attempt-Id": releaseAttemptId,
444
- "X-Gencow-Release-Environment": opts.envTarget || "dev",
445
- }
446
- : {}),
447
- },
448
- body: bundleBuffer,
506
+ postDeploymentAdmissionWithTimeout({
507
+ requestId: deployClaim.claim.requestId,
508
+ appName: appId,
509
+ // Resume static admission directly; fullstack flow skips the
510
+ // already-completed backend component for this scoped receipt.
511
+ resumeCommand: `gencow deploy --static${opts.envTarget === "prod" ? " --prod" : ""} --app ${appId}${opts.clientName ? ` --client ${opts.clientName}` : ""} --resume ${deployClaim.claim.requestId}`,
512
+ send: ({ signal }) =>
513
+ platformFetchImpl(creds, `/platform/apps/${appId}/deploy-static`, {
514
+ method: "POST",
515
+ headers: {
516
+ "Content-Type": "application/octet-stream",
517
+ "X-Deploy-Local-Dir": cwd,
518
+ "X-Gencow-Deploy-Claim": deployClaim.header,
519
+ "X-Gencow-Deploy-Protocol": "1",
520
+ "X-Gencow-CLI-Version": deployClaim.claim.generatedBy.version,
521
+ ...(releaseAttemptId
522
+ ? {
523
+ "X-Gencow-Release-Attempt-Id": releaseAttemptId,
524
+ "X-Gencow-Release-Environment": opts.envTarget || "dev",
525
+ }
526
+ : {}),
527
+ },
528
+ body: bundleBuffer,
529
+ signal,
530
+ }),
449
531
  }),
450
532
  retryDelaysMs: staticDeployRetryDelaysMs,
451
533
  delayImpl: staticDeployRetryDelayImpl,
@@ -510,8 +592,15 @@ export function createStaticDeployRuntime({
510
592
  errData.operationId === undefined &&
511
593
  errData.deployId === undefined &&
512
594
  errData.state === undefined;
513
- if (verifiedTerminalFailure || rejectedBeforeOperation)
595
+ const retryableInfrastructureFailure = errData?.retryClass === "retryable-infra";
596
+ if ((verifiedTerminalFailure || rejectedBeforeOperation) && !retryableInfrastructureFailure)
514
597
  await markAttemptFailed("deploy", "DEPLOY_FAILED");
598
+ if (
599
+ staticReceipt &&
600
+ (verifiedTerminalFailure || rejectedBeforeOperation) &&
601
+ !retryableInfrastructureFailure
602
+ )
603
+ releaseStaticDeployReceipt(staticReceipt);
515
604
  const [headline, ...details] = renderDeployFailureDiagnosticLines(errData, deployRes.statusText, {
516
605
  expectedAppName: appId,
517
606
  });
@@ -540,10 +629,12 @@ export function createStaticDeployRuntime({
540
629
  } catch (caught) {
541
630
  if (!shouldPreserveFinalizeAttempt(caught)) {
542
631
  await markAttemptFailed("finalize", "APP_RELEASE_FINALIZE_FAILED");
632
+ if (staticReceipt) releaseStaticDeployReceipt(staticReceipt);
543
633
  }
544
634
  throw caught;
545
635
  }
546
636
  }
637
+ if (staticReceipt) releaseStaticDeployReceipt(staticReceipt);
547
638
  logImpl("");
548
639
  successImpl("Static deploy complete!");
549
640
  infoImpl(`App ID: ${appId}`);
@@ -572,15 +663,17 @@ export function createStaticDeployRuntime({
572
663
  }
573
664
 
574
665
  const envSyncRoot = opts.releaseSourceRoot || cwd;
575
- const envSyncConfig = loadConfigImpl
666
+ const envSyncConfig = !opts.resumeStaticOnly && loadConfigImpl
576
667
  ? await loadConfigImpl({ cwd: envSyncRoot, processEnv: process.env, throwOnInvalid: false })
577
668
  : null;
578
- updateEnvLocalUrlImpl(data.url, {
579
- config: envSyncConfig,
580
- cwd: envSyncRoot,
581
- clientName: opts.clientName ?? getCliInvocationSelection()?.client ?? null,
582
- projectDir: envSyncRoot,
583
- });
669
+ if (!opts.resumeStaticOnly) {
670
+ updateEnvLocalUrlImpl(data.url, {
671
+ config: envSyncConfig,
672
+ cwd: envSyncRoot,
673
+ clientName: opts.clientName ?? getCliInvocationSelection()?.client ?? null,
674
+ projectDir: envSyncRoot,
675
+ });
676
+ }
584
677
  logImpl("");
585
678
  if (!existsSyncImpl(gencowJsonPath)) {
586
679
  writeProjectMetadata({
@@ -0,0 +1,100 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+
5
+ const REQUEST_ID_PATTERN = /^[A-Za-z0-9_-]{8,128}$/u;
6
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/u;
7
+
8
+ function sha256(value) {
9
+ return createHash("sha256").update(value).digest("hex");
10
+ }
11
+
12
+ function directoryFor(cwd, requestId) {
13
+ if (typeof cwd !== "string" || cwd.includes("\0") || !REQUEST_ID_PATTERN.test(requestId)) {
14
+ throw new Error("STATIC_DEPLOY_RECEIPT_INPUT_INVALID");
15
+ }
16
+ return resolve(cwd, ".gencow", "static-deploy-receipts", requestId);
17
+ }
18
+
19
+ export function saveStaticDeployReceipt({
20
+ cwd,
21
+ requestId,
22
+ appId,
23
+ envTarget,
24
+ sourceDir,
25
+ releaseAttemptId = null,
26
+ bundleBuffer,
27
+ deployClaim,
28
+ context = null,
29
+ }) {
30
+ const directory = directoryFor(cwd, requestId);
31
+ const archiveSha256 = Buffer.isBuffer(bundleBuffer) ? sha256(bundleBuffer) : null;
32
+ if (
33
+ !Buffer.isBuffer(bundleBuffer) ||
34
+ deployClaim?.claim?.requestId !== requestId ||
35
+ deployClaim.claim.sourceBundleSha256 !== archiveSha256
36
+ ) {
37
+ throw new Error("STATIC_DEPLOY_RECEIPT_INPUT_INVALID");
38
+ }
39
+ mkdirSync(resolve(directory, ".."), { recursive: true, mode: 0o700 });
40
+ const receipt = {
41
+ version: 1,
42
+ component: "static",
43
+ requestId,
44
+ appId,
45
+ envTarget,
46
+ sourceDir,
47
+ context,
48
+ releaseAttemptId,
49
+ archiveSha256,
50
+ claim: deployClaim.claim,
51
+ claimHeader: deployClaim.header,
52
+ };
53
+ const temporary = `${directory}.tmp-${process.pid}`;
54
+ mkdirSync(temporary, { recursive: true, mode: 0o700 });
55
+ writeFileSync(resolve(temporary, "archive.tar.gz"), bundleBuffer, { mode: 0o600 });
56
+ writeFileSync(resolve(temporary, "receipt.json"), `${JSON.stringify(receipt, null, 2)}\n`, { mode: 0o600 });
57
+ try {
58
+ renameSync(temporary, directory);
59
+ } catch (error) {
60
+ rmSync(temporary, { recursive: true, force: true });
61
+ if (!existsSync(directory)) throw error;
62
+ const existing = loadStaticDeployReceipt({ cwd, requestId, appId, envTarget, context });
63
+ if (!existing.bundleBuffer.equals(bundleBuffer) || existing.deployClaim.header !== deployClaim.header ||
64
+ existing.releaseAttemptId !== releaseAttemptId) throw new Error("STATIC_DEPLOY_RECEIPT_IDENTITY_CONFLICT");
65
+ return existing;
66
+ }
67
+ return { directory, bundleBuffer, deployClaim, releaseAttemptId };
68
+ }
69
+
70
+ export function loadStaticDeployReceipt({ cwd, requestId, appId, envTarget, context = null }) {
71
+ const directory = directoryFor(cwd, requestId);
72
+ const receipt = JSON.parse(readFileSync(resolve(directory, "receipt.json"), "utf8"));
73
+ const bundleBuffer = readFileSync(resolve(directory, "archive.tar.gz"));
74
+ if (
75
+ receipt.version !== 1 || receipt.component !== "static" || receipt.requestId !== requestId || receipt.appId !== appId ||
76
+ receipt.envTarget !== envTarget || typeof receipt.sourceDir !== "string" ||
77
+ !SHA256_PATTERN.test(receipt.archiveSha256) ||
78
+ sha256(bundleBuffer) !== receipt.archiveSha256 ||
79
+ receipt.claim?.sourceBundleSha256 !== receipt.archiveSha256 ||
80
+ receipt.claim?.requestId !== requestId ||
81
+ typeof receipt.claimHeader !== "string"
82
+ ) throw new Error("STATIC_DEPLOY_RECEIPT_CORRUPT");
83
+ if (context && receipt.context && (
84
+ receipt.context.clientName !== context.clientName || receipt.context.sourceRoot !== context.sourceRoot ||
85
+ receipt.context.metadataPath !== context.metadataPath)) {
86
+ throw new Error("STATIC_DEPLOY_RECEIPT_CONTEXT_MISMATCH");
87
+ }
88
+ return {
89
+ directory,
90
+ sourceDir: receipt.sourceDir,
91
+ bundleBuffer,
92
+ releaseAttemptId: receipt.releaseAttemptId ?? null,
93
+ deployClaim: { claim: receipt.claim, header: receipt.claimHeader },
94
+ };
95
+ }
96
+
97
+ export function releaseStaticDeployReceipt(receipt) {
98
+ if (!receipt?.directory) throw new Error("STATIC_DEPLOY_RECEIPT_RELEASE_INVALID");
99
+ rmSync(receipt.directory, { recursive: true, force: true });
100
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gencow",
3
- "version": "0.1.245",
3
+ "version": "0.1.246",
4
4
  "description": "Gencow — AI Backend Engine",
5
5
  "type": "module",
6
6
  "bin": {
@@ -38,9 +38,9 @@
38
38
  "@types/node": "^25.9.5",
39
39
  "better-auth": "^1.6.23",
40
40
  "@typescript/native": "npm:typescript@7.0.2",
41
- "@gencow/client": "0.2.7",
42
41
  "@gencow/react": "0.2.7",
43
- "@gencow/core": "0.1.45"
42
+ "@gencow/core": "0.1.45",
43
+ "@gencow/client": "0.2.7"
44
44
  },
45
45
  "scripts": {
46
46
  "prebuild": "pnpm --filter @gencow/migration-contract run build && pnpm --filter @gencow/server run build",
@@ -160748,6 +160748,10 @@ var init_server_platform_media_worker_storage_runtime = __esm({
160748
160748
  });
160749
160749
 
160750
160750
  // ../internal/dist/src/platform-scheduled-job-secret.js
160751
+ function globalSigningSecretStore() {
160752
+ const globalObject2 = globalThis;
160753
+ return globalObject2[PLATFORM_SIGNING_SECRET_GLOBAL_KEY] ??= {};
160754
+ }
160751
160755
  function usableSecret(value) {
160752
160756
  const secret = value?.trim();
160753
160757
  return secret && secret !== DEFAULT_PLATFORM_SECRET ? secret : null;
@@ -160759,17 +160763,38 @@ function capturePlatformScheduledJobSigningSecret(env2 = process.env) {
160759
160763
  if (env2.IS_PLATFORM !== "true")
160760
160764
  return void 0;
160761
160765
  scheduledJobSigningSecretSnapshot = resolveSecret(env2);
160766
+ globalSigningSecretStore().value = scheduledJobSigningSecretSnapshot ?? void 0;
160762
160767
  return scheduledJobSigningSecretSnapshot ?? void 0;
160763
160768
  }
160764
160769
  function getPlatformScheduledJobSigningSecret() {
160765
- return scheduledJobSigningSecretSnapshot ?? resolveSecret(process.env) ?? void 0;
160770
+ return scheduledJobSigningSecretSnapshot ?? globalSigningSecretStore().value;
160771
+ }
160772
+ function initializePlatformScheduledJobSigningSecret(secret) {
160773
+ const usable = usableSecret(secret);
160774
+ if (!usable)
160775
+ throw new Error("SCHEDULED_JOB_DELIVERY_SIGNING_SECRET_UNAVAILABLE");
160776
+ const existing = scheduledJobSigningSecretSnapshot ?? globalSigningSecretStore().value;
160777
+ if (existing && existing !== usable) {
160778
+ throw new Error("SCHEDULED_JOB_DELIVERY_SIGNING_SECRET_ALREADY_INITIALIZED");
160779
+ }
160780
+ scheduledJobSigningSecretSnapshot = usable;
160781
+ globalSigningSecretStore().value = usable;
160766
160782
  }
160767
- var DEFAULT_PLATFORM_SECRET, scheduledJobSigningSecretSnapshot;
160783
+ async function loadPlatformProvisionerWithScheduledJobSigningSecret(load) {
160784
+ const secret = getPlatformScheduledJobSigningSecret();
160785
+ if (!secret)
160786
+ throw new Error("SCHEDULED_JOB_DELIVERY_SIGNING_SECRET_UNAVAILABLE");
160787
+ const provisioner = await load();
160788
+ provisioner.initializePlatformScheduledJobSigningSecret(secret);
160789
+ return provisioner;
160790
+ }
160791
+ var DEFAULT_PLATFORM_SECRET, scheduledJobSigningSecretSnapshot, PLATFORM_SIGNING_SECRET_GLOBAL_KEY;
160768
160792
  var init_platform_scheduled_job_secret = __esm({
160769
160793
  "../internal/dist/src/platform-scheduled-job-secret.js"() {
160770
160794
  "use strict";
160771
160795
  DEFAULT_PLATFORM_SECRET = "gencow-default-secret";
160772
160796
  scheduledJobSigningSecretSnapshot = null;
160797
+ PLATFORM_SIGNING_SECRET_GLOBAL_KEY = /* @__PURE__ */ Symbol.for("gencow.platformScheduledJobSigningSecret");
160773
160798
  }
160774
160799
  });
160775
160800
 
@@ -186891,7 +186916,18 @@ async function beginAppNameD1Delete(input) {
186891
186916
  (public.app_name_incarnation_handoffs.state = 'd1_blocked'
186892
186917
  AND public.app_name_incarnation_handoffs.d1_app_id = ${input.appId}
186893
186918
  AND public.app_name_incarnation_handoffs.d1_app_instance_id = ${input.appInstanceId}
186894
- AND public.app_name_incarnation_handoffs.d1_delete_operation_id = ${input.deleteOperationId})
186919
+ AND (
186920
+ public.app_name_incarnation_handoffs.d1_delete_operation_id = ${input.deleteOperationId}
186921
+ OR EXISTS (
186922
+ SELECT 1
186923
+ FROM public.app_lifecycle_operations prior_delete
186924
+ WHERE prior_delete.id = public.app_name_incarnation_handoffs.d1_delete_operation_id
186925
+ AND prior_delete.app_id = ${input.appId}
186926
+ AND prior_delete.app_instance_id = ${input.appInstanceId}
186927
+ AND prior_delete.app_name = ${input.publicName}
186928
+ AND prior_delete.state = 'failed_delete_recovery_required'
186929
+ )
186930
+ ))
186895
186931
  )
186896
186932
  RETURNING id
186897
186933
  `);
@@ -191890,6 +191926,7 @@ var init_provisioner_cron_sync = __esm({
191890
191926
  "use strict";
191891
191927
  init_postgres_temporal2();
191892
191928
  init_platform_scheduled_job_secret();
191929
+ init_platform_scheduled_job_secret();
191893
191930
  PLATFORM_SECRET = process.env.PLATFORM_INTERNAL_SECRET || process.env.BETTER_AUTH_SECRET || "gencow-default-secret";
191894
191931
  MAX_CRON_JOBS_FREE = 5;
191895
191932
  CRON_MANIFEST_RELATIVE_PATH = ".gencow/cron-jobs.json";
@@ -430467,6 +430504,7 @@ __export(provisioner_exports, {
430467
430504
  hasLiveRuntimeProcess: () => hasLiveRuntimeProcess2,
430468
430505
  hasWarmPoolBytecodeBundle: () => hasWarmPoolBytecodeBundle3,
430469
430506
  initRunner: () => initRunner,
430507
+ initializePlatformScheduledJobSigningSecret: () => initializePlatformScheduledJobSigningSecret,
430470
430508
  isManagedAppProcessCommand: () => isManagedAppProcessCommand2,
430471
430509
  isRunning: () => isRunning,
430472
430510
  listWarmPoolAssignedApps: () => listWarmPoolAssignedApps,
@@ -431158,6 +431196,9 @@ async function prepareControlPlaneCandidateWorkspace(params) {
431158
431196
  }
431159
431197
  async function buildControlPlaneCandidateEnv(params) {
431160
431198
  try {
431199
+ if (!getPlatformScheduledJobSigningSecret()) {
431200
+ capturePlatformScheduledJobSigningSecret(process.env);
431201
+ }
431161
431202
  return await buildProvisionerRuntimeEnv({
431162
431203
  appName: params.appName,
431163
431204
  port: params.port,
@@ -431643,6 +431684,8 @@ var init_provisioner = __esm({
431643
431684
  init_provisioner_shutdown();
431644
431685
  init_warm_pool_backend();
431645
431686
  init_provisioner_cron_sync();
431687
+ init_platform_scheduled_job_secret();
431688
+ init_provisioner_cron_sync();
431646
431689
  init_provisioner_schema();
431647
431690
  init_migration_push_preflight_error();
431648
431691
  init_provisioner_wake_timing();
@@ -442214,11 +442257,8 @@ function createRuntimeRouteProjectionConverger(input) {
442214
442257
  return input.db.transaction(async (tx) => {
442215
442258
  await acquireLock(tx, `app-activation:${target.appId}`);
442216
442259
  const routePort = input.getAppPort(target.appName);
442217
- if (routePort !== void 0 && target.expectedPort !== void 0 && routePort !== target.expectedPort) {
442218
- throw new Error("RUNTIME_ROUTE_PROJECTION_BINDING_CHANGED");
442219
- }
442220
442260
  const [appRows, desiredRows, activationRows, instanceRows] = await Promise.all([
442221
- tx.select({ name: apps.name }).from(apps).where(eq68(apps.id, target.appId)),
442261
+ tx.select({ name: apps.name, catalogPort: apps.port }).from(apps).where(eq68(apps.id, target.appId)),
442222
442262
  tx.select({
442223
442263
  desiredActivationId: appRuntimeDesiredState.desiredActivationId,
442224
442264
  desiredRuntimeState: appRuntimeDesiredState.desiredRuntimeState,
@@ -442261,6 +442301,10 @@ function createRuntimeRouteProjectionConverger(input) {
442261
442301
  const servingWithoutInstance = desired?.desiredRuntimeState === "serving" && desired.nodeId === input.currentNodeId && desired.desiredActivationId !== null && activationRows.some((activation) => activation.id === desired.desiredActivationId) && !instanceRows.some(
442262
442302
  (instance) => instance.activationEpoch === desired.activationEpoch && instance.state === "serving"
442263
442303
  );
442304
+ const staleCatalogRoutePort = desiredAbsentOnNode && routePort !== void 0 && target.expectedPort !== void 0 && routePort !== target.expectedPort && appRows[0]?.catalogPort === routePort;
442305
+ if (routePort !== void 0 && target.expectedPort !== void 0 && routePort !== target.expectedPort && !staleCatalogRoutePort) {
442306
+ throw new Error("RUNTIME_ROUTE_PROJECTION_BINDING_CHANGED");
442307
+ }
442264
442308
  if (desired?.authorityMode !== "enforce" || !desiredAbsentOnNode && !desiredMovedFromNode && !stoppedDeletionProjection && !servingWithoutInstance) {
442265
442309
  throw new Error("RUNTIME_ROUTE_PROJECTION_STATE_AMBIGUOUS");
442266
442310
  }
@@ -446822,11 +446866,11 @@ async function probeSlotCapability(input) {
446822
446866
  method: "GET",
446823
446867
  path: path4
446824
446868
  }),
446825
- signal: AbortSignal.timeout(750)
446869
+ signal: AbortSignal.timeout(input.timeoutMs ?? DEFAULT_SLOT_CAPABILITY_PROBE_TIMEOUT_MS)
446826
446870
  });
446827
446871
  if (response.status === 404 && input.port !== input.currentPort) {
446828
446872
  const readiness = await input.fetchImpl(`${origin}/health/ready`, {
446829
- signal: AbortSignal.timeout(750)
446873
+ signal: AbortSignal.timeout(input.timeoutMs ?? DEFAULT_SLOT_CAPABILITY_PROBE_TIMEOUT_MS)
446830
446874
  });
446831
446875
  return readiness.ok ? "legacy" : "unavailable";
446832
446876
  }
@@ -446880,7 +446924,10 @@ async function convergeDeploymentRouteAcrossEligibleSlots(input) {
446880
446924
  env: env2,
446881
446925
  boundNodeOrigin: boundNodeOrigin?.origin
446882
446926
  };
446883
- const capability2 = port === currentPort ? await probeCurrentSlotCapabilityWithRetry(probeInput) : await probeSlotCapability(probeInput);
446927
+ const capability2 = port === currentPort && input.currentSlotKnownCapable ? "capable" : port === currentPort ? await probeCurrentSlotCapabilityWithRetry(probeInput) : await probeSlotCapability({
446928
+ ...probeInput,
446929
+ timeoutMs: input.currentSlotKnownCapable ? KNOWN_CURRENT_SLOT_SIBLING_PROBE_TIMEOUT_MS : void 0
446930
+ });
446884
446931
  return { port, capability: capability2 };
446885
446932
  })
446886
446933
  );
@@ -446934,12 +446981,14 @@ async function convergeDeploymentRouteAcrossEligibleSlots(input) {
446934
446981
  }
446935
446982
  return { routeConvergence: "converged", acknowledgedSlots };
446936
446983
  }
446937
- var DEFAULT_PLATFORM_GATEWAY_ENV_FILE;
446984
+ var DEFAULT_PLATFORM_GATEWAY_ENV_FILE, DEFAULT_SLOT_CAPABILITY_PROBE_TIMEOUT_MS, KNOWN_CURRENT_SLOT_SIBLING_PROBE_TIMEOUT_MS;
446938
446985
  var init_runtime_route_fleet_client = __esm({
446939
446986
  "../platform/src/runtime-route-fleet-client.ts"() {
446940
446987
  "use strict";
446941
446988
  init_node_proxy_signature();
446942
446989
  DEFAULT_PLATFORM_GATEWAY_ENV_FILE = "/etc/gencow/platform-internal-gateway.env";
446990
+ DEFAULT_SLOT_CAPABILITY_PROBE_TIMEOUT_MS = 750;
446991
+ KNOWN_CURRENT_SLOT_SIBLING_PROBE_TIMEOUT_MS = 250;
446943
446992
  }
446944
446993
  });
446945
446994
 
@@ -447364,6 +447413,7 @@ async function restartRetainedRuntimeActivation(input) {
447364
447413
  })
447365
447414
  );
447366
447415
  await convergeDeploymentRouteAcrossEligibleSlots({
447416
+ currentSlotKnownCapable: true,
447367
447417
  binding: buildCanonicalTenantRouteBinding({
447368
447418
  appId: current.appId,
447369
447419
  appName: current.appName,
@@ -447444,21 +447494,27 @@ var init_runtime_retained_activation_recovery = __esm({
447444
447494
  });
447445
447495
 
447446
447496
  // ../platform/src/runtime-retained-activation-wake.ts
447497
+ function retainedActivationRestartOwnsServingRoute(restoreOperationId) {
447498
+ return !restoreOperationId;
447499
+ }
447447
447500
  function createRetainedActivationWakeAction(input) {
447448
447501
  return (snapshot) => {
447449
447502
  const restoreOperationId = deleteRestoreOperationIdForApp(input.recoveryCommand, snapshot.appId);
447450
447503
  return restartRetainedActivationWithFallback({
447451
447504
  restoreOperationId,
447452
- restart: () => observeRuntimeWakePhase(
447453
- currentRuntimeWakePhaseTrace(),
447454
- { phase: "retained_activation_restart", failureReason: "phase_failed" },
447455
- () => restartRetainedRuntimeActivation({
447456
- db: input.db,
447457
- snapshot,
447458
- currentNodeId: input.currentNodeId,
447459
- restoreOperationId
447460
- })
447461
- ),
447505
+ restart: async () => {
447506
+ await observeRuntimeWakePhase(
447507
+ currentRuntimeWakePhaseTrace(),
447508
+ { phase: "retained_activation_restart", failureReason: "phase_failed" },
447509
+ () => restartRetainedRuntimeActivation({
447510
+ db: input.db,
447511
+ snapshot,
447512
+ currentNodeId: input.currentNodeId,
447513
+ restoreOperationId
447514
+ })
447515
+ );
447516
+ return retainedActivationRestartOwnsServingRoute(restoreOperationId) ? { retainedActivationRestartCompleted: true } : void 0;
447517
+ },
447462
447518
  restore: (operationId2) => input.replaceCurrentDeployment(snapshot, "restore_exact_generation", void 0, operationId2)
447463
447519
  });
447464
447520
  };
@@ -450353,6 +450409,35 @@ async function planExplicitRuntimeStop(input) {
450353
450409
  if (!reset[0]) throw new Error("RUNTIME_EXPLICIT_STOP_RETRY_CAS_FAILED");
450354
450410
  }
450355
450411
  }
450412
+ const matchingActiveDrain = (await tx.execute(sql140`
450413
+ SELECT d.lifecycle_request_id, r.correlation_id, d.id AS drain_id, d.activation_id, d.instance_id FROM runtime_drain_requests d
450414
+ JOIN runtime_lifecycle_requests r ON r.id = d.lifecycle_request_id
450415
+ WHERE d.app_id = ${input.appId}
450416
+ AND d.activation_id = ${activation.id}
450417
+ AND d.instance_id = ${instance.instanceId}
450418
+ AND d.reason = 'app_delete'
450419
+ AND d.state IN ('planned', 'armed', 'dispatched')
450420
+ AND r.action = ${input.reason}
450421
+ AND (r.actor_kind = ${input.actorKind} OR (${input.reason === "app_delete" && input.actorKind === "owner" && input.correlationId.startsWith("app_delete_")} AND r.actor_kind = 'platform'))
450422
+ AND r.actor_id_hash = ${actorIdHash}
450423
+ AND (r.correlation_id = ${input.correlationId} OR (${input.reason === "app_delete" && input.correlationId.startsWith("app_delete_")} AND r.correlation_id LIKE ${`lifecycle_app_delete_${input.appId}_%`}))
450424
+ LIMIT 1
450425
+ `)).rows?.[0];
450426
+ if (matchingActiveDrain) {
450427
+ return {
450428
+ appId: input.appId,
450429
+ appName: input.appName,
450430
+ activationId: matchingActiveDrain.activation_id,
450431
+ instanceId: matchingActiveDrain.instance_id,
450432
+ lifecycleRequestId: matchingActiveDrain.lifecycle_request_id,
450433
+ drainId: matchingActiveDrain.drain_id,
450434
+ reason: input.reason,
450435
+ actorKind: input.actorKind,
450436
+ actorIdHash,
450437
+ correlationId: matchingActiveDrain.correlation_id,
450438
+ replayed: true
450439
+ };
450440
+ }
450356
450441
  const activeDrains = await tx.select().from(runtimeDrainRequests).where(
450357
450442
  and90(
450358
450443
  eq97(runtimeDrainRequests.appId, input.appId),
@@ -453676,6 +453761,9 @@ function isDeferredActionDecision(decision) {
453676
453761
  function isRuntimeArtifactTerminalization(value) {
453677
453762
  return typeof value === "object" && value !== null && value.terminalized === true && value.terminalization === "runtime_artifact_unavailable";
453678
453763
  }
453764
+ function isRetainedActivationRestartCompleted(value) {
453765
+ return typeof value === "object" && value !== null && value.retainedActivationRestartCompleted === true;
453766
+ }
453679
453767
  async function reconcileRuntimeControlPlaneApp(input) {
453680
453768
  const snapshot = await input.loadSnapshot(input.appId);
453681
453769
  if (snapshot.appId !== input.appId) throw new Error("CONTROL_PLANE_SNAPSHOT_SCOPE_MISMATCH");
@@ -453710,6 +453798,9 @@ async function reconcileRuntimeControlPlaneApp(input) {
453710
453798
  await input.acquireLock(input.appId, async (lockedDb) => {
453711
453799
  await input.recordDecision?.({ appId: input.appId, ...settled }, lockedDb);
453712
453800
  });
453801
+ if (deferredAction.result.decision === "restart_retained_activation" && isRetainedActivationRestartCompleted(actionResult)) {
453802
+ return { ...settled, retainedActivationRestartCompleted: true };
453803
+ }
453713
453804
  return settled;
453714
453805
  }
453715
453806
  var init_runtime_reconciliation_controller = __esm({
@@ -455779,7 +455870,7 @@ function publicAppDeleteErrorCode(error51) {
455779
455870
  const code = stableAppDeleteErrorCode(error51);
455780
455871
  return PUBLICLY_NORMALIZED_APP_DELETE_CODES.has(code) ? "APP_DELETE_OPERATION_PENDING" : code;
455781
455872
  }
455782
- function appDeleteFailureWithReceipt(error51, receipt) {
455873
+ function appDeleteFailureWithReceipt(error51, receipt, statusQueryName = "apps.deleteStatus") {
455783
455874
  const code = publicAppDeleteErrorCode(error51);
455784
455875
  const restorationDiagnostic = code === "APP_DELETE_RESTORE_IN_PROGRESS" ? {
455785
455876
  stateImpact: "The same restore receipt remains active; no replacement delete or restore operation was created.",
@@ -455795,6 +455886,13 @@ function appDeleteFailureWithReceipt(error51, receipt) {
455795
455886
  retryClass: "owner-action",
455796
455887
  userAction: "RETRY_DELETE_WITH_NEW_REQUEST",
455797
455888
  nextAction: "RETRY_DELETE_WITH_NEW_REQUEST"
455889
+ } : statusQueryName === "admin.apps.deleteStatus" ? {
455890
+ stateImpact: "The same delete operation remains authoritative; no replacement operation was created.",
455891
+ retryable: false,
455892
+ retryPrecondition: `The administrator must POST /api/query with {"name":"${statusQueryName}","args":{"name":"<app>","operationId":"<operationId>"}} and wait for deletion to reach a terminal state.`,
455893
+ retryClass: "platform-action",
455894
+ userAction: "VIEW_DELETE_STATUS",
455895
+ nextAction: "VIEW_DELETE_STATUS"
455798
455896
  } : {};
455799
455897
  return Object.assign(new Error(code, { cause: error51 }), {
455800
455898
  code,
@@ -458907,7 +459005,7 @@ async function bootstrapPlatformRuntime(params) {
458907
459005
  const { buildAppRuntimeUnavailableResult, ensureAppRuntimeReady } = await import(resolve124(params.functionsPath, "../src/app-runtime-recovery.ts"));
458908
459006
  const { createPublicRuntimeWakeCorrelation, executeLegacyRuntimeWakeByName } = await import(resolve124(params.functionsPath, "../src/simple-lifecycle/legacy-runtime-effect-adapter.ts"));
458909
459007
  await initializePlatformRuntimeGenerationForServer(params.functionsPath);
458910
- const { getAppPort: getLivePort, setAppPort: setAppPort2, removeAppPort: removeAppPort2, removeAppPortIfEquals: removeAppPortIfEquals2, setAppMeta: setAppMeta2, setLastAccess: markLastAccess, hasLiveRuntimeProcess: hasLiveRuntimeProcess3, resolveLiveRuntimePort: resolveLiveRuntimePort3, reapWarmPoolStale: reapWarmPoolStale2, getWakeCapacitySnapshot: getWakeCapacitySnapshot2, setWakeCapacityMaxConcurrentRunning: setWakeCapacityMaxConcurrentRunning2, setWakeCapacityUpdateInProgress: setWakeCapacityUpdateInProgress2, generateInternalToken: generateInternalToken3, setDatabasePreflightRawSql: setDatabasePreflightRawSql2, setPlatformRuntimeDb: setPlatformRuntimeDb2, buildAppDatabaseUrl: buildAppDatabaseUrl2 } = await import(resolve124(params.functionsPath, "../src/provisioner.ts"));
459008
+ const { getAppPort: getLivePort, setAppPort: setAppPort2, removeAppPort: removeAppPort2, removeAppPortIfEquals: removeAppPortIfEquals2, setAppMeta: setAppMeta2, setLastAccess: markLastAccess, hasLiveRuntimeProcess: hasLiveRuntimeProcess3, resolveLiveRuntimePort: resolveLiveRuntimePort3, reapWarmPoolStale: reapWarmPoolStale2, getWakeCapacitySnapshot: getWakeCapacitySnapshot2, setWakeCapacityMaxConcurrentRunning: setWakeCapacityMaxConcurrentRunning2, setWakeCapacityUpdateInProgress: setWakeCapacityUpdateInProgress2, generateInternalToken: generateInternalToken3, setDatabasePreflightRawSql: setDatabasePreflightRawSql2, setPlatformRuntimeDb: setPlatformRuntimeDb2, buildAppDatabaseUrl: buildAppDatabaseUrl2 } = await loadPlatformProvisionerWithScheduledJobSigningSecret(() => import(resolve124(params.functionsPath, "../src/provisioner.ts")));
458911
459009
  setDatabasePreflightRawSql2(params.rawSql);
458912
459010
  wirePlatformDatabaseUrls(setPlatformRuntimeDb2, params.db, params.databaseUrlResolvers);
458913
459011
  const storageDirectRuntime = await createPlatformStorageDirectRuntime(params.functionsPath, buildAppDatabaseUrl2);
@@ -459336,6 +459434,7 @@ var init_server_platform_runtime_bootstrap = __esm({
459336
459434
  init_server_platform_control_plane_observer_runtime();
459337
459435
  init_server_platform_cron_scheduler();
459338
459436
  init_server_scheduled_jobs_runtime();
459437
+ init_platform_scheduled_job_secret();
459339
459438
  init_server_execution();
459340
459439
  init_server_wake_signal_diagnostics_route();
459341
459440
  init_server_platform_worker_diagnostics();
@@ -465643,6 +465742,7 @@ var SAFE_DIAGNOSTIC_STATE_IMPACTS = /* @__PURE__ */ new Set([
465643
465742
  "Runtime reconciliation made no state change; the existing delete operation remains authoritative.",
465644
465743
  "The failed delete receipt remains unchanged; the app runtime was not started.",
465645
465744
  "The same restore receipt remains active; no replacement delete or restore operation was created.",
465745
+ "The same delete operation remains authoritative; no replacement operation was created.",
465646
465746
  "The previous restore is complete; this request did not create a delete operation.",
465647
465747
  "No matching upload grant was found; no storage catalog object was finalized.",
465648
465748
  "The upload grant is expired or aborted; no storage catalog object was finalized.",
@@ -465666,6 +465766,7 @@ var SAFE_DIAGNOSTIC_RETRY_PRECONDITIONS = /* @__PURE__ */ new Set([
465666
465766
  "Refresh the app list before retrying with the same request ID.",
465667
465767
  "Refresh the app list to confirm whether the app is already removed.",
465668
465768
  'The owner must POST /api/query with {"name":"apps.deleteStatus","args":{"name":"<app>","operationId":"<operationId>"}} and wait for deletion to reach a terminal state.',
465769
+ 'The administrator must POST /api/query with {"name":"admin.apps.deleteStatus","args":{"name":"<app>","operationId":"<operationId>"}} and wait for deletion to reach a terminal state.',
465669
465770
  "Wait until the stated runtime or serving-route condition is clear, then retry the same owner wake/resume operation.",
465670
465771
  "Wait until the active app deployment attempt finishes and its lease clears; then retry the same owner wake.",
465671
465772
  "Wait until this restore receipt leaves restoring; then check the app status.",