run402-mcp 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/tools/assets-put.d.ts.map +1 -1
- package/dist/tools/assets-put.js +27 -0
- package/dist/tools/assets-put.js.map +1 -1
- package/dist/tools/jobs.d.ts +90 -0
- package/dist/tools/jobs.d.ts.map +1 -0
- package/dist/tools/jobs.js +102 -0
- package/dist/tools/jobs.js.map +1 -0
- package/package.json +4 -5
- package/sdk/README.md +72 -1
- package/sdk/dist/index.d.ts +3 -0
- package/sdk/dist/index.d.ts.map +1 -1
- package/sdk/dist/index.js +3 -0
- package/sdk/dist/index.js.map +1 -1
- package/sdk/dist/namespaces/assets.d.ts.map +1 -1
- package/sdk/dist/namespaces/assets.js +129 -2
- package/sdk/dist/namespaces/assets.js.map +1 -1
- package/sdk/dist/namespaces/assets.types.d.ts +116 -0
- package/sdk/dist/namespaces/assets.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.d.ts +5 -0
- package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.js +127 -32
- package/sdk/dist/namespaces/deploy.js.map +1 -1
- package/sdk/dist/namespaces/deploy.types.d.ts +44 -0
- package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/jobs.d.ts +74 -0
- package/sdk/dist/namespaces/jobs.d.ts.map +1 -0
- package/sdk/dist/namespaces/jobs.js +82 -0
- package/sdk/dist/namespaces/jobs.js.map +1 -0
- package/sdk/dist/scoped.d.ts +11 -0
- package/sdk/dist/scoped.d.ts.map +1 -1
- package/sdk/dist/scoped.js +22 -0
- package/sdk/dist/scoped.js.map +1 -1
|
@@ -362,8 +362,29 @@ function requireNonEmptyStringQueryOption(value, label, context) {
|
|
|
362
362
|
return value;
|
|
363
363
|
}
|
|
364
364
|
// ─── Internal pipeline ───────────────────────────────────────────────────────
|
|
365
|
+
/**
|
|
366
|
+
* Compute the sorted set of slice kinds the spec carried. Surfaces on
|
|
367
|
+
* `commit.phase` and `ready` events so agents can group per-phase
|
|
368
|
+
* telemetry by slice category. `assets` slice → `"asset"`; any of
|
|
369
|
+
* `database` / `functions` / `site` → `"release"`. Order is stable
|
|
370
|
+
* (release before asset).
|
|
371
|
+
*/
|
|
372
|
+
function deriveSliceKinds(spec) {
|
|
373
|
+
// Guard against non-object spec — the validate phase throws below
|
|
374
|
+
// (INVALID_SPEC), but this is called before validation in applyOnce so
|
|
375
|
+
// we must not blow up first.
|
|
376
|
+
if (!spec || typeof spec !== "object")
|
|
377
|
+
return [];
|
|
378
|
+
const set = new Set();
|
|
379
|
+
if (spec.database || spec.functions || spec.site)
|
|
380
|
+
set.add("release");
|
|
381
|
+
if (spec.assets)
|
|
382
|
+
set.add("asset");
|
|
383
|
+
return [...set].sort((a, b) => (a === "release" ? -1 : 1));
|
|
384
|
+
}
|
|
365
385
|
async function applyOnce(client, spec, opts, emit) {
|
|
366
386
|
const allowWarningCodes = normalizeAllowWarningCodes(opts.allowWarningCodes);
|
|
387
|
+
const sliceKinds = deriveSliceKinds(spec);
|
|
367
388
|
emit({ type: "plan.started" });
|
|
368
389
|
const { plan, byteReaders } = await planInternal(client, spec, opts.idempotencyKey);
|
|
369
390
|
emit({ type: "plan.diff", diff: plan.diff });
|
|
@@ -383,10 +404,15 @@ async function applyOnce(client, spec, opts, emit) {
|
|
|
383
404
|
// event and resolve before we hit upload.
|
|
384
405
|
}
|
|
385
406
|
await uploadMissing(client, spec.project, plan.missing_content, byteReaders, emit);
|
|
386
|
-
emit({
|
|
407
|
+
emit({
|
|
408
|
+
type: "commit.phase",
|
|
409
|
+
phase: "validate",
|
|
410
|
+
status: "started",
|
|
411
|
+
...(sliceKinds.length > 0 ? { slice_kinds: sliceKinds } : {}),
|
|
412
|
+
});
|
|
387
413
|
const { planId } = requirePersistedPlan(plan, "applying deploy");
|
|
388
414
|
const commit = await commitInternal(client, planId, opts.idempotencyKey);
|
|
389
|
-
const result = await pollUntilReady(client, commit, plan.diff, plan.warnings, emit, spec.project);
|
|
415
|
+
const result = await pollUntilReady(client, commit, plan.diff, plan.warnings, emit, spec.project, sliceKinds);
|
|
390
416
|
// v1.48 unified-apply: thread the plan response's `asset_entries[]` back
|
|
391
417
|
// into DeployResult.assets so callers reading `result.assets.byKey[key]`
|
|
392
418
|
// get the gateway-authoritative `AssetRef` envelope (URLs, SRI, etag).
|
|
@@ -873,6 +899,7 @@ async function uploadMissing(client, projectId, presence, byteReaders, emit) {
|
|
|
873
899
|
label: reader?.label ?? p.sha256,
|
|
874
900
|
sha256: p.sha256,
|
|
875
901
|
reason: "present",
|
|
902
|
+
...(reader?.slice ? { slice_kind: reader.slice } : {}),
|
|
876
903
|
});
|
|
877
904
|
}
|
|
878
905
|
// Filter to refs the gateway reported as missing for this project.
|
|
@@ -925,6 +952,7 @@ async function uploadMissing(client, projectId, presence, byteReaders, emit) {
|
|
|
925
952
|
sha256: session.sha256,
|
|
926
953
|
done,
|
|
927
954
|
total,
|
|
955
|
+
...(reader.slice ? { slice_kind: reader.slice } : {}),
|
|
928
956
|
});
|
|
929
957
|
}
|
|
930
958
|
// Plan-level finalize — marks the plan committed in the deploy_plans
|
|
@@ -1040,7 +1068,7 @@ async function putToS3(fetchFn, url, body, checksumBase64, partNumber) {
|
|
|
1040
1068
|
}
|
|
1041
1069
|
return res.headers.get("etag");
|
|
1042
1070
|
}
|
|
1043
|
-
async function pollUntilReady(client, commit, diff, warnings, emit, projectId) {
|
|
1071
|
+
async function pollUntilReady(client, commit, diff, warnings, emit, projectId, sliceKinds = []) {
|
|
1044
1072
|
if (commit.status === "failed") {
|
|
1045
1073
|
throw translateGatewayError(commit.error, "commit", null, commit.operation_id);
|
|
1046
1074
|
}
|
|
@@ -1054,7 +1082,12 @@ async function pollUntilReady(client, commit, diff, warnings, emit, projectId) {
|
|
|
1054
1082
|
context: "committing deploy",
|
|
1055
1083
|
});
|
|
1056
1084
|
}
|
|
1057
|
-
emit({
|
|
1085
|
+
emit({
|
|
1086
|
+
type: "ready",
|
|
1087
|
+
releaseId: commit.release_id,
|
|
1088
|
+
urls: commit.urls,
|
|
1089
|
+
...(sliceKinds.length > 0 ? { slice_kinds: sliceKinds } : {}),
|
|
1090
|
+
});
|
|
1058
1091
|
return {
|
|
1059
1092
|
release_id: commit.release_id,
|
|
1060
1093
|
operation_id: commit.operation_id,
|
|
@@ -1065,9 +1098,16 @@ async function pollUntilReady(client, commit, diff, warnings, emit, projectId) {
|
|
|
1065
1098
|
}
|
|
1066
1099
|
const opHeaders = projectId ? await apikeyHeaders(client, projectId) : {};
|
|
1067
1100
|
const initialSnapshot = await client.request(`/apply/v1/operations/${encodeURIComponent(commit.operation_id)}`, { headers: opHeaders, context: "fetching deploy operation" });
|
|
1068
|
-
return await pollSnapshotUntilReady(client, initialSnapshot, diff, warnings, emit, projectId);
|
|
1069
|
-
}
|
|
1070
|
-
async function pollSnapshotUntilReady(client, initial, diff, warnings, emit, projectId) {
|
|
1101
|
+
return await pollSnapshotUntilReady(client, initialSnapshot, diff, warnings, emit, projectId, sliceKinds);
|
|
1102
|
+
}
|
|
1103
|
+
async function pollSnapshotUntilReady(client, initial, diff, warnings, emit, projectId, sliceKinds = []) {
|
|
1104
|
+
// Helper to spread slice_kinds onto every commit.phase / ready emit so
|
|
1105
|
+
// agents grouping per-slice telemetry don't need to track the apply's
|
|
1106
|
+
// spec separately. The low-level commit/upload helpers that pass no
|
|
1107
|
+
// sliceKinds get an empty array → field is omitted from events.
|
|
1108
|
+
const withSliceKinds = (ev) => sliceKinds.length > 0
|
|
1109
|
+
? { ...ev, slice_kinds: sliceKinds }
|
|
1110
|
+
: ev;
|
|
1071
1111
|
let snapshot = initial;
|
|
1072
1112
|
const opHeaders = projectId ? await apikeyHeaders(client, projectId) : {};
|
|
1073
1113
|
let lastPhaseEmitted = null;
|
|
@@ -1107,7 +1147,7 @@ async function pollSnapshotUntilReady(client, initial, diff, warnings, emit, pro
|
|
|
1107
1147
|
return;
|
|
1108
1148
|
if (nextPhase !== undefined && prev.phase === nextPhase)
|
|
1109
1149
|
return;
|
|
1110
|
-
emit({ type: "commit.phase", phase: prev.phase, status: closeStatus });
|
|
1150
|
+
emit(withSliceKinds({ type: "commit.phase", phase: prev.phase, status: closeStatus }));
|
|
1111
1151
|
};
|
|
1112
1152
|
while (true) {
|
|
1113
1153
|
if (lastPhaseEmitted !== snapshot.status) {
|
|
@@ -1115,7 +1155,7 @@ async function pollSnapshotUntilReady(client, initial, diff, warnings, emit, pro
|
|
|
1115
1155
|
if (ev) {
|
|
1116
1156
|
if (ev.type === "commit.phase")
|
|
1117
1157
|
closePreviousPhase(ev.phase);
|
|
1118
|
-
emit(ev);
|
|
1158
|
+
emit(withSliceKinds(ev));
|
|
1119
1159
|
lastPhaseEmitted = snapshot.status;
|
|
1120
1160
|
}
|
|
1121
1161
|
// If `ev` is null (status not in the phase map, e.g. "ready"), leave
|
|
@@ -1133,7 +1173,7 @@ async function pollSnapshotUntilReady(client, initial, diff, warnings, emit, pro
|
|
|
1133
1173
|
});
|
|
1134
1174
|
}
|
|
1135
1175
|
closePreviousPhase();
|
|
1136
|
-
emit({ type: "ready", releaseId: snapshot.release_id, urls: snapshot.urls });
|
|
1176
|
+
emit(withSliceKinds({ type: "ready", releaseId: snapshot.release_id, urls: snapshot.urls }));
|
|
1137
1177
|
return {
|
|
1138
1178
|
release_id: snapshot.release_id,
|
|
1139
1179
|
operation_id: snapshot.operation_id,
|
|
@@ -1215,12 +1255,18 @@ async function startInternal(client, spec, opts) {
|
|
|
1215
1255
|
reason: plan.payment_required.reason,
|
|
1216
1256
|
});
|
|
1217
1257
|
}
|
|
1258
|
+
const sliceKinds = deriveSliceKinds(spec);
|
|
1218
1259
|
const resultPromise = (async () => {
|
|
1219
1260
|
await uploadMissing(client, spec.project, plan.missing_content, byteReaders, emit);
|
|
1220
|
-
emit({
|
|
1261
|
+
emit({
|
|
1262
|
+
type: "commit.phase",
|
|
1263
|
+
phase: "validate",
|
|
1264
|
+
status: "started",
|
|
1265
|
+
...(sliceKinds.length > 0 ? { slice_kinds: sliceKinds } : {}),
|
|
1266
|
+
});
|
|
1221
1267
|
const { planId } = requirePersistedPlan(plan, "starting deploy");
|
|
1222
1268
|
const commit = await commitInternal(client, planId, opts.idempotencyKey);
|
|
1223
|
-
return await pollUntilReady(client, commit, plan.diff, plan.warnings, emit, spec.project);
|
|
1269
|
+
return await pollUntilReady(client, commit, plan.diff, plan.warnings, emit, spec.project, sliceKinds);
|
|
1224
1270
|
})();
|
|
1225
1271
|
// Avoid an unhandled-rejection at construction time. Consumers must call
|
|
1226
1272
|
// .result() to actually observe the error.
|
|
@@ -2033,7 +2079,13 @@ function invalidSecretSpec(message, resource) {
|
|
|
2033
2079
|
}
|
|
2034
2080
|
async function normalizeReleaseSpec(client, spec) {
|
|
2035
2081
|
const byteReaders = new Map();
|
|
2036
|
-
|
|
2082
|
+
// Slice-tagged `remember`. Each slice category creates its own remember
|
|
2083
|
+
// closure so the registered reader carries `reader.slice = "release" |
|
|
2084
|
+
// "asset"`. On cross-kind dedup (same SHA from both a release-bound
|
|
2085
|
+
// slice and the asset slice) the value escalates to `"mixed"`. This
|
|
2086
|
+
// value surfaces on `content.upload.*` events so agents can group
|
|
2087
|
+
// upload telemetry by slice kind.
|
|
2088
|
+
const makeRemember = (slice) => (resolved) => {
|
|
2037
2089
|
// Propagate the final content-type onto the deferred reader so the CAS
|
|
2038
2090
|
// upload session can declare it correctly. Callers may set
|
|
2039
2091
|
// ref.contentType *after* resolveContent returns (e.g. normalizeFileSet
|
|
@@ -2042,18 +2094,25 @@ async function normalizeReleaseSpec(client, spec) {
|
|
|
2042
2094
|
resolved.reader.contentType = resolved.ref.contentType;
|
|
2043
2095
|
}
|
|
2044
2096
|
if (!byteReaders.has(resolved.ref.sha256)) {
|
|
2097
|
+
resolved.reader.slice = slice;
|
|
2045
2098
|
byteReaders.set(resolved.ref.sha256, resolved.reader);
|
|
2046
2099
|
}
|
|
2047
2100
|
else {
|
|
2048
2101
|
// Already remembered — but if the existing reader has no contentType
|
|
2049
|
-
// and we just learned it, fill it in.
|
|
2102
|
+
// and we just learned it, fill it in. Also escalate slice tag when
|
|
2103
|
+
// the second registration comes from a different kind.
|
|
2050
2104
|
const existing = byteReaders.get(resolved.ref.sha256);
|
|
2051
2105
|
if (resolved.ref.contentType && !existing.contentType) {
|
|
2052
2106
|
existing.contentType = resolved.ref.contentType;
|
|
2053
2107
|
}
|
|
2108
|
+
if (existing.slice && existing.slice !== slice && existing.slice !== "mixed") {
|
|
2109
|
+
existing.slice = "mixed";
|
|
2110
|
+
}
|
|
2054
2111
|
}
|
|
2055
2112
|
return resolved.ref;
|
|
2056
2113
|
};
|
|
2114
|
+
const rememberRelease = makeRemember("release");
|
|
2115
|
+
const rememberAsset = makeRemember("asset");
|
|
2057
2116
|
const normalized = { project: spec.project };
|
|
2058
2117
|
if (spec.base)
|
|
2059
2118
|
normalized.base = spec.base;
|
|
@@ -2074,19 +2133,19 @@ async function normalizeReleaseSpec(client, spec) {
|
|
|
2074
2133
|
db.zero_downtime = spec.database.zero_downtime;
|
|
2075
2134
|
}
|
|
2076
2135
|
if (spec.database.migrations && spec.database.migrations.length > 0) {
|
|
2077
|
-
db.migrations = await Promise.all(spec.database.migrations.map(async (m) => normalizeMigration(client, spec.project, m,
|
|
2136
|
+
db.migrations = await Promise.all(spec.database.migrations.map(async (m) => normalizeMigration(client, spec.project, m, rememberRelease)));
|
|
2078
2137
|
}
|
|
2079
2138
|
normalized.database = db;
|
|
2080
2139
|
}
|
|
2081
2140
|
if (spec.functions) {
|
|
2082
2141
|
const fns = {};
|
|
2083
2142
|
if (spec.functions.replace) {
|
|
2084
|
-
fns.replace = await normalizeFunctionMap(spec.functions.replace,
|
|
2143
|
+
fns.replace = await normalizeFunctionMap(spec.functions.replace, rememberRelease);
|
|
2085
2144
|
}
|
|
2086
2145
|
if (spec.functions.patch) {
|
|
2087
2146
|
fns.patch = {};
|
|
2088
2147
|
if (spec.functions.patch.set) {
|
|
2089
|
-
fns.patch.set = await normalizeFunctionMap(spec.functions.patch.set,
|
|
2148
|
+
fns.patch.set = await normalizeFunctionMap(spec.functions.patch.set, rememberRelease);
|
|
2090
2149
|
}
|
|
2091
2150
|
if (spec.functions.patch.delete)
|
|
2092
2151
|
fns.patch.delete = spec.functions.patch.delete;
|
|
@@ -2096,7 +2155,7 @@ async function normalizeReleaseSpec(client, spec) {
|
|
|
2096
2155
|
if (spec.site) {
|
|
2097
2156
|
const publicPaths = "public_paths" in spec.site ? spec.site.public_paths : undefined;
|
|
2098
2157
|
if ("replace" in spec.site && spec.site.replace) {
|
|
2099
|
-
const map = await normalizeFileSet(spec.site.replace,
|
|
2158
|
+
const map = await normalizeFileSet(spec.site.replace, rememberRelease);
|
|
2100
2159
|
normalized.site = {
|
|
2101
2160
|
replace: map,
|
|
2102
2161
|
...(publicPaths ? { public_paths: publicPaths } : {}),
|
|
@@ -2105,7 +2164,7 @@ async function normalizeReleaseSpec(client, spec) {
|
|
|
2105
2164
|
else if ("patch" in spec.site && spec.site.patch) {
|
|
2106
2165
|
const patch = {};
|
|
2107
2166
|
if (spec.site.patch.put) {
|
|
2108
|
-
patch.put = await normalizeFileSet(spec.site.patch.put,
|
|
2167
|
+
patch.put = await normalizeFileSet(spec.site.patch.put, rememberRelease);
|
|
2109
2168
|
}
|
|
2110
2169
|
if (spec.site.patch.delete)
|
|
2111
2170
|
patch.delete = spec.site.patch.delete;
|
|
@@ -2123,7 +2182,7 @@ async function normalizeReleaseSpec(client, spec) {
|
|
|
2123
2182
|
// register a byte-reader; emit the wire-shaped `AssetPutEntry[]`.
|
|
2124
2183
|
// Cross-kind SHA dedup is automatic via the shared `byteReaders` map.
|
|
2125
2184
|
if (spec.assets) {
|
|
2126
|
-
normalized.assets = await normalizeAssetSlice(spec.assets,
|
|
2185
|
+
normalized.assets = await normalizeAssetSlice(spec.assets, rememberAsset);
|
|
2127
2186
|
}
|
|
2128
2187
|
return { normalized, byteReaders };
|
|
2129
2188
|
}
|
|
@@ -2173,39 +2232,75 @@ async function normalizeFileSet(set, remember) {
|
|
|
2173
2232
|
* attacker-controlled or filesystem-derived keys (`__proto__`,
|
|
2174
2233
|
* `constructor`, `toString`) don't collide with prototype properties.
|
|
2175
2234
|
*
|
|
2176
|
-
* Totals are
|
|
2177
|
-
* `
|
|
2178
|
-
*
|
|
2179
|
-
*
|
|
2235
|
+
* Totals are derived from the plan response's per-entry `status`:
|
|
2236
|
+
* - `"upload_pending"` → bytes_uploaded (the SDK is about to PUT these to S3)
|
|
2237
|
+
* - `"present"` or `"satisfied_by_plan"` → bytes_reused (already in CAS,
|
|
2238
|
+
* dedup hit either project-locally or via a same-spec sibling)
|
|
2239
|
+
* `duration_ms` is filled in by `manifestFromResult` at the NodeAssets layer.
|
|
2180
2240
|
*/
|
|
2181
2241
|
function buildAssetManifestFromPlanEntries(entries) {
|
|
2182
2242
|
const list = [];
|
|
2183
2243
|
const byKey = Object.create(null);
|
|
2184
2244
|
const manifest = Object.create(null);
|
|
2245
|
+
let bytesUploaded = 0;
|
|
2246
|
+
let bytesReused = 0;
|
|
2185
2247
|
for (const entry of entries) {
|
|
2248
|
+
const ref = entry.asset_ref;
|
|
2186
2249
|
const e = {
|
|
2187
2250
|
key: entry.key,
|
|
2188
2251
|
sha256: entry.sha256,
|
|
2189
2252
|
size_bytes: entry.size_bytes,
|
|
2190
2253
|
content_type: entry.content_type,
|
|
2191
2254
|
visibility: entry.visibility,
|
|
2192
|
-
url:
|
|
2193
|
-
immutable_url:
|
|
2194
|
-
cdn_url:
|
|
2195
|
-
cdn_immutable_url:
|
|
2196
|
-
sri:
|
|
2197
|
-
etag:
|
|
2198
|
-
content_digest:
|
|
2255
|
+
url: ref.url,
|
|
2256
|
+
immutable_url: ref.immutable_url,
|
|
2257
|
+
cdn_url: ref.cdn_url,
|
|
2258
|
+
cdn_immutable_url: ref.cdn_immutable_url,
|
|
2259
|
+
sri: ref.sri,
|
|
2260
|
+
etag: ref.etag,
|
|
2261
|
+
content_digest: ref.content_digest,
|
|
2199
2262
|
};
|
|
2263
|
+
// v1.49+ image-variant pass-through. Only emitted when the gateway
|
|
2264
|
+
// returned them (image MIMEs ≥320×320; HEIC/HEIF sources also include
|
|
2265
|
+
// `display_jpeg`). Pre-v1.49 plan responses omit these fields entirely
|
|
2266
|
+
// and the manifest entry stays bytewise-identical to before.
|
|
2267
|
+
if (ref.width_px !== undefined)
|
|
2268
|
+
e.width_px = ref.width_px;
|
|
2269
|
+
if (ref.height_px !== undefined)
|
|
2270
|
+
e.height_px = ref.height_px;
|
|
2271
|
+
if (ref.blurhash !== undefined)
|
|
2272
|
+
e.blurhash = ref.blurhash;
|
|
2273
|
+
if (ref.variant_spec_version !== undefined) {
|
|
2274
|
+
e.variant_spec_version = ref.variant_spec_version;
|
|
2275
|
+
}
|
|
2276
|
+
if (ref.display_url !== undefined)
|
|
2277
|
+
e.display_url = ref.display_url;
|
|
2278
|
+
if (ref.display_immutable_url !== undefined) {
|
|
2279
|
+
e.display_immutable_url = ref.display_immutable_url;
|
|
2280
|
+
}
|
|
2281
|
+
if (ref.variants !== undefined)
|
|
2282
|
+
e.variants = ref.variants;
|
|
2200
2283
|
list.push(e);
|
|
2201
2284
|
byKey[entry.key] = e;
|
|
2202
2285
|
manifest[entry.key] = e;
|
|
2286
|
+
if (entry.status === "upload_pending") {
|
|
2287
|
+
bytesUploaded += entry.size_bytes;
|
|
2288
|
+
}
|
|
2289
|
+
else {
|
|
2290
|
+
// "present" or "satisfied_by_plan" — already in CAS or covered by a sibling.
|
|
2291
|
+
bytesReused += entry.size_bytes;
|
|
2292
|
+
}
|
|
2203
2293
|
}
|
|
2204
2294
|
return {
|
|
2205
2295
|
list,
|
|
2206
2296
|
byKey,
|
|
2207
2297
|
manifest,
|
|
2208
|
-
totals: {
|
|
2298
|
+
totals: {
|
|
2299
|
+
files: entries.length,
|
|
2300
|
+
bytes_uploaded: bytesUploaded,
|
|
2301
|
+
bytes_reused: bytesReused,
|
|
2302
|
+
duration_ms: 0,
|
|
2303
|
+
},
|
|
2209
2304
|
};
|
|
2210
2305
|
}
|
|
2211
2306
|
// ─── Asset slice normalization (v1.48 unified-apply) ─────────────────────────
|