@sentry/junior 0.57.0 → 0.58.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.
Files changed (54) hide show
  1. package/dist/app.js +1301 -1278
  2. package/dist/chat/agent-dispatch/types.d.ts +0 -1
  3. package/dist/chat/conversation-privacy.d.ts +23 -0
  4. package/dist/chat/logging.d.ts +2 -0
  5. package/dist/chat/mcp/tool-manager.d.ts +18 -5
  6. package/dist/chat/mcp/tool-name.d.ts +2 -0
  7. package/dist/chat/pi/client.d.ts +2 -0
  8. package/dist/chat/pi/derived-state.d.ts +5 -0
  9. package/dist/chat/pi/traced-stream.d.ts +5 -1
  10. package/dist/chat/prompt.d.ts +3 -9
  11. package/dist/chat/respond-helpers.d.ts +5 -3
  12. package/dist/chat/respond.d.ts +1 -0
  13. package/dist/chat/runtime/conversation-message.d.ts +10 -0
  14. package/dist/chat/runtime/processing-reaction.d.ts +2 -4
  15. package/dist/chat/runtime/reply-executor.d.ts +13 -16
  16. package/dist/chat/runtime/slack-runtime.d.ts +19 -32
  17. package/dist/chat/runtime/thread-state.d.ts +1 -1
  18. package/dist/chat/runtime/turn-input.d.ts +29 -0
  19. package/dist/chat/runtime/turn-preparation.d.ts +4 -24
  20. package/dist/chat/runtime/turn.d.ts +2 -3
  21. package/dist/chat/sentry-links.d.ts +4 -0
  22. package/dist/chat/services/context-compaction.d.ts +3 -4
  23. package/dist/chat/services/pending-auth.d.ts +1 -1
  24. package/dist/chat/services/subscribed-reply-policy.d.ts +2 -13
  25. package/dist/chat/services/timeout-resume.d.ts +1 -2
  26. package/dist/chat/services/turn-session-record.d.ts +82 -0
  27. package/dist/chat/slack/assistant-thread/title.d.ts +4 -1
  28. package/dist/chat/state/artifacts.d.ts +1 -0
  29. package/dist/chat/state/conversation.d.ts +0 -1
  30. package/dist/chat/state/session-log.d.ts +117 -0
  31. package/dist/chat/state/ttl.d.ts +2 -0
  32. package/dist/chat/state/turn-session.d.ts +89 -0
  33. package/dist/chat/tools/advisor/tool.d.ts +2 -0
  34. package/dist/chat/tools/agent-tools.d.ts +2 -1
  35. package/dist/chat/tools/skill/call-mcp-tool.d.ts +7 -3
  36. package/dist/chat/tools/skill/search-mcp-tools.d.ts +15 -3
  37. package/dist/chat/tools/types.d.ts +0 -1
  38. package/dist/{chunk-AA5TIFN5.js → chunk-FKEKRBUB.js} +267 -735
  39. package/dist/{chunk-TTUY467K.js → chunk-H652GMDH.js} +30 -14
  40. package/dist/chunk-I4FDGMFI.js +950 -0
  41. package/dist/{chunk-D3G3YOU4.js → chunk-ITOW4DED.js} +1 -1
  42. package/dist/chunk-QDGD5WVN.js +708 -0
  43. package/dist/cli/check.js +2 -2
  44. package/dist/cli/init.js +0 -1
  45. package/dist/cli/snapshot-warmup.js +5 -3
  46. package/dist/instrumentation.js +3 -0
  47. package/dist/reporting.d.ts +113 -0
  48. package/dist/reporting.js +390 -0
  49. package/package.json +25 -11
  50. package/dist/chat/services/turn-checkpoint.d.ts +0 -74
  51. package/dist/chat/state/pi-session-message-store.d.ts +0 -15
  52. package/dist/chat/state/turn-session-store.d.ts +0 -49
  53. package/dist/handlers/diagnostics-dashboard.d.ts +0 -2
  54. package/dist/handlers/diagnostics.d.ts +0 -2
@@ -2,7 +2,7 @@ import {
2
2
  getPluginForSkillPath,
3
3
  getPluginSkillRoots,
4
4
  logWarn
5
- } from "./chunk-TTUY467K.js";
5
+ } from "./chunk-H652GMDH.js";
6
6
  import {
7
7
  skillRoots
8
8
  } from "./chunk-5LUISFEY.js";
@@ -0,0 +1,708 @@
1
+ import {
2
+ getStateAdapter,
3
+ toOptionalTrimmed
4
+ } from "./chunk-FKEKRBUB.js";
5
+ import {
6
+ getPluginRuntimeDependencies,
7
+ getPluginRuntimePostinstall,
8
+ withSpan
9
+ } from "./chunk-H652GMDH.js";
10
+
11
+ // src/chat/sandbox/runtime-dependency-snapshots.ts
12
+ import { createHash } from "crypto";
13
+ import { Sandbox } from "@vercel/sandbox";
14
+
15
+ // src/chat/sandbox/noninteractive-command.ts
16
+ var NON_INTERACTIVE_ENV = {
17
+ CI: "1",
18
+ TERM: "dumb",
19
+ NO_COLOR: "1",
20
+ PAGER: "cat",
21
+ GIT_PAGER: "cat",
22
+ GH_PROMPT_DISABLED: "1",
23
+ GH_NO_UPDATE_NOTIFIER: "1",
24
+ GH_NO_EXTENSION_UPDATE_NOTIFIER: "1",
25
+ GH_SPINNER_DISABLED: "1",
26
+ GIT_TERMINAL_PROMPT: "0",
27
+ GCM_INTERACTIVE: "never",
28
+ DEBIAN_FRONTEND: "noninteractive",
29
+ // Git credential isolation: prevent git from sending its own auth so the
30
+ // sandbox egress proxy's header transforms are the sole credential source.
31
+ GIT_ASKPASS: "/bin/true",
32
+ GIT_CONFIG_NOSYSTEM: "1",
33
+ GIT_CONFIG_COUNT: "2",
34
+ GIT_CONFIG_KEY_0: "credential.helper",
35
+ GIT_CONFIG_VALUE_0: "",
36
+ GIT_CONFIG_KEY_1: "http.emptyAuth",
37
+ GIT_CONFIG_VALUE_1: "true"
38
+ };
39
+ function shellQuote(value) {
40
+ return `'${value.replace(/'/g, "'\\''")}'`;
41
+ }
42
+ function buildEnvExports(options) {
43
+ const lines = [];
44
+ if (options.pathPrefix) {
45
+ lines.push(`export PATH="${options.pathPrefix}"`);
46
+ }
47
+ for (const [key, value] of Object.entries(NON_INTERACTIVE_ENV)) {
48
+ lines.push(`export ${key}=${shellQuote(value)}`);
49
+ }
50
+ for (const [key, value] of Object.entries(options.env ?? {})) {
51
+ lines.push(`export ${key}=${shellQuote(value)}`);
52
+ }
53
+ return lines;
54
+ }
55
+ function toCommandScript(input) {
56
+ return [shellQuote(input.cmd), ...(input.args ?? []).map(shellQuote)].join(
57
+ " "
58
+ );
59
+ }
60
+ function buildNonInteractiveShellScript(script, options = {}) {
61
+ return [...buildEnvExports(options), "exec </dev/null", script].join(" && ");
62
+ }
63
+ function buildNonInteractiveCommand(input) {
64
+ return {
65
+ cmd: "bash",
66
+ args: [
67
+ input.login ? "-lc" : "-c",
68
+ buildNonInteractiveShellScript(toCommandScript(input), {
69
+ env: input.env,
70
+ pathPrefix: input.pathPrefix
71
+ })
72
+ ]
73
+ };
74
+ }
75
+ async function runNonInteractiveCommand(sandbox, input) {
76
+ const command = {
77
+ ...buildNonInteractiveCommand(input),
78
+ ...input.cwd ? { cwd: input.cwd } : {},
79
+ ...input.sudo !== void 0 ? { sudo: input.sudo } : {}
80
+ };
81
+ return await sandbox.runCommand(command);
82
+ }
83
+
84
+ // src/chat/sandbox/credentials.ts
85
+ function getVercelSandboxCredentials() {
86
+ const token = toOptionalTrimmed(process.env.VERCEL_TOKEN);
87
+ const teamId = toOptionalTrimmed(process.env.VERCEL_TEAM_ID);
88
+ const projectId = toOptionalTrimmed(process.env.VERCEL_PROJECT_ID);
89
+ if (token && teamId && projectId) {
90
+ return { token, teamId, projectId };
91
+ }
92
+ return void 0;
93
+ }
94
+
95
+ // src/chat/sandbox/workspace.ts
96
+ function createSandboxInstance(sandbox) {
97
+ return {
98
+ sandboxId: sandbox.name,
99
+ get sandboxEgressId() {
100
+ return sandbox.currentSession().sessionId;
101
+ },
102
+ fs: sandbox.fs,
103
+ extendTimeout(duration) {
104
+ return sandbox.extendTimeout(duration);
105
+ },
106
+ mkDir(path) {
107
+ return sandbox.mkDir(path);
108
+ },
109
+ readFileToBuffer(input) {
110
+ return sandbox.readFileToBuffer(input);
111
+ },
112
+ runCommand(input) {
113
+ return sandbox.runCommand(input);
114
+ },
115
+ async snapshot() {
116
+ const snapshot = await sandbox.snapshot();
117
+ return { snapshotId: snapshot.snapshotId };
118
+ },
119
+ stop() {
120
+ return sandbox.stop();
121
+ },
122
+ update(params) {
123
+ return sandbox.update(params);
124
+ },
125
+ writeFiles(files) {
126
+ return sandbox.writeFiles(files);
127
+ }
128
+ };
129
+ }
130
+
131
+ // src/chat/sandbox/paths.ts
132
+ function normalizeWorkspaceRoot(input) {
133
+ const candidate = (input ?? "").trim();
134
+ if (!candidate) {
135
+ return "/vercel/sandbox";
136
+ }
137
+ const normalized = candidate.replace(/\/+$/, "");
138
+ return normalized.startsWith("/") ? normalized : `/${normalized}`;
139
+ }
140
+ var SANDBOX_WORKSPACE_ROOT = normalizeWorkspaceRoot(
141
+ process.env.VERCEL_SANDBOX_WORKSPACE_DIR
142
+ );
143
+ var SANDBOX_SKILLS_ROOT = `${SANDBOX_WORKSPACE_ROOT}/skills`;
144
+ var SANDBOX_DATA_ROOT = `${SANDBOX_WORKSPACE_ROOT}/data`;
145
+ function sandboxSkillDir(skillName) {
146
+ return `${SANDBOX_SKILLS_ROOT}/${skillName}`;
147
+ }
148
+ function sandboxSkillFile(skillName) {
149
+ return `${sandboxSkillDir(skillName)}/SKILL.md`;
150
+ }
151
+
152
+ // src/chat/sandbox/runtime-dependency-snapshots.ts
153
+ var SNAPSHOT_CACHE_PREFIX = "junior:sandbox_snapshot_profile";
154
+ var SNAPSHOT_LOCK_PREFIX = "junior:sandbox_snapshot_lock";
155
+ var SNAPSHOT_PROFILE_VERSION = 1;
156
+ var SNAPSHOT_CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
157
+ var SNAPSHOT_BUILD_LOCK_TTL_MS = 10 * 60 * 1e3;
158
+ var SNAPSHOT_WAIT_FOR_LOCK_MS = SNAPSHOT_BUILD_LOCK_TTL_MS + 30 * 1e3;
159
+ var DEFAULT_FLOATING_DEP_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
160
+ function sleep(ms) {
161
+ return new Promise((resolve) => {
162
+ setTimeout(resolve, ms);
163
+ });
164
+ }
165
+ function profileCacheKey(profileHash) {
166
+ return `${SNAPSHOT_CACHE_PREFIX}:${profileHash}`;
167
+ }
168
+ function profileLockKey(profileHash) {
169
+ return `${SNAPSHOT_LOCK_PREFIX}:${profileHash}`;
170
+ }
171
+ function isExactNpmVersion(version) {
172
+ return /^\d+\.\d+\.\d+(?:[-+][a-z0-9.]+)?$/i.test(version.trim());
173
+ }
174
+ function hasFloatingSelector(dep) {
175
+ return dep.type === "npm" && !isExactNpmVersion(dep.version);
176
+ }
177
+ function parseFloatingDepMaxAgeMs() {
178
+ const raw = process.env.SANDBOX_SNAPSHOT_FLOATING_MAX_AGE_MS;
179
+ if (!raw?.trim()) {
180
+ return DEFAULT_FLOATING_DEP_MAX_AGE_MS;
181
+ }
182
+ const parsed = Number.parseInt(raw, 10);
183
+ if (!Number.isFinite(parsed) || parsed < 0) {
184
+ return DEFAULT_FLOATING_DEP_MAX_AGE_MS;
185
+ }
186
+ return parsed;
187
+ }
188
+ function buildDependencyProfile(runtime) {
189
+ const dependencies = getPluginRuntimeDependencies();
190
+ const postinstall = getPluginRuntimePostinstall();
191
+ if (dependencies.length === 0 && postinstall.length === 0) {
192
+ return null;
193
+ }
194
+ const rebuildEpoch = process.env.SANDBOX_SNAPSHOT_REBUILD_EPOCH?.trim() ?? "";
195
+ const hasFloatingVersions = dependencies.some((dep) => hasFloatingSelector(dep)) || postinstall.length > 0;
196
+ const hashInput = JSON.stringify({
197
+ version: SNAPSHOT_PROFILE_VERSION,
198
+ runtime,
199
+ rebuildEpoch,
200
+ dependencies,
201
+ postinstall
202
+ });
203
+ const profileHash = createHash("sha256").update(hashInput).digest("hex");
204
+ return {
205
+ profileHash,
206
+ dependencyCount: dependencies.length,
207
+ hasFloatingVersions,
208
+ dependencies,
209
+ postinstall
210
+ };
211
+ }
212
+ function getRuntimeDependencyProfileHash(runtime) {
213
+ return buildDependencyProfile(runtime)?.profileHash;
214
+ }
215
+ function shouldRebuildCachedSnapshot(profile, cached) {
216
+ if (!profile.hasFloatingVersions) {
217
+ return false;
218
+ }
219
+ const maxAgeMs = parseFloatingDepMaxAgeMs();
220
+ if (maxAgeMs === 0) {
221
+ return true;
222
+ }
223
+ return Date.now() - cached.createdAtMs > maxAgeMs;
224
+ }
225
+ async function getCachedSnapshot(profileHash) {
226
+ try {
227
+ const state = getStateAdapter();
228
+ await state.connect();
229
+ const raw = await state.get(profileCacheKey(profileHash));
230
+ if (typeof raw !== "string") {
231
+ return null;
232
+ }
233
+ const parsed = JSON.parse(raw);
234
+ if (!parsed || typeof parsed !== "object" || typeof parsed.profileHash !== "string" || typeof parsed.snapshotId !== "string" || typeof parsed.runtime !== "string" || typeof parsed.createdAtMs !== "number" || typeof parsed.dependencyCount !== "number") {
235
+ return null;
236
+ }
237
+ return parsed;
238
+ } catch {
239
+ return null;
240
+ }
241
+ }
242
+ async function setCachedSnapshot(entry) {
243
+ const state = getStateAdapter();
244
+ await state.connect();
245
+ await state.set(
246
+ profileCacheKey(entry.profileHash),
247
+ JSON.stringify(entry),
248
+ SNAPSHOT_CACHE_TTL_MS
249
+ );
250
+ }
251
+ async function withSnapshotSpan(name, op, attributes, callback) {
252
+ return await withSpan(name, op, {}, callback, attributes);
253
+ }
254
+ async function runOrThrow(sandbox, params, label) {
255
+ const result = await runNonInteractiveCommand(sandbox, params);
256
+ if (result.exitCode === 0) {
257
+ return;
258
+ }
259
+ const stderr = (await result.stderr()).trim();
260
+ const stdout = (await result.stdout()).trim();
261
+ const detail = stderr || stdout || "command failed";
262
+ throw new Error(`${label} failed: ${detail}`);
263
+ }
264
+ async function tryRun(sandbox, params) {
265
+ const result = await runNonInteractiveCommand(sandbox, params);
266
+ if (result.exitCode === 0) {
267
+ return { ok: true };
268
+ }
269
+ const stderr = (await result.stderr()).trim();
270
+ const stdout = (await result.stdout()).trim();
271
+ return { ok: false, detail: stderr || stdout || "command failed" };
272
+ }
273
+ async function installGhCliViaDnf(sandbox) {
274
+ const direct = await tryRun(sandbox, {
275
+ cmd: "dnf",
276
+ args: ["install", "-y", "gh"],
277
+ sudo: true
278
+ });
279
+ if (direct.ok) {
280
+ return;
281
+ }
282
+ const dnf5Repo = await tryRun(sandbox, {
283
+ cmd: "dnf",
284
+ args: [
285
+ "config-manager",
286
+ "addrepo",
287
+ "--from-repofile=https://cli.github.com/packages/rpm/gh-cli.repo"
288
+ ],
289
+ sudo: true
290
+ });
291
+ if (!dnf5Repo.ok) {
292
+ await runOrThrow(
293
+ sandbox,
294
+ {
295
+ cmd: "dnf",
296
+ args: ["install", "-y", "dnf-command(config-manager)"],
297
+ sudo: true
298
+ },
299
+ "dnf install dnf-command(config-manager)"
300
+ );
301
+ await runOrThrow(
302
+ sandbox,
303
+ {
304
+ cmd: "dnf",
305
+ args: [
306
+ "config-manager",
307
+ "--add-repo",
308
+ "https://cli.github.com/packages/rpm/gh-cli.repo"
309
+ ],
310
+ sudo: true
311
+ },
312
+ "dnf config-manager --add-repo gh-cli.repo"
313
+ );
314
+ }
315
+ await runOrThrow(
316
+ sandbox,
317
+ {
318
+ cmd: "dnf",
319
+ args: ["install", "-y", "gh", "--repo", "gh-cli"],
320
+ sudo: true
321
+ },
322
+ "dnf install gh --repo gh-cli"
323
+ );
324
+ }
325
+ function runtimeDependencyFilePath(url, sha256) {
326
+ let urlBasename = "package.rpm";
327
+ try {
328
+ const pathname = new URL(url).pathname;
329
+ const segments = pathname.split("/").filter(Boolean);
330
+ const candidate = segments[segments.length - 1];
331
+ if (candidate) {
332
+ urlBasename = candidate;
333
+ }
334
+ } catch {
335
+ }
336
+ const sanitizedBasename = urlBasename.replace(/[^a-zA-Z0-9._-]/g, "_");
337
+ return `/tmp/junior-runtime-${sha256.slice(0, 12)}-${sanitizedBasename}`;
338
+ }
339
+ async function installRuntimeDependencies(sandbox, deps) {
340
+ const systemDeps = deps.filter(
341
+ (dep) => dep.type === "system"
342
+ );
343
+ const npmPackages = deps.filter(
344
+ (dep) => dep.type === "npm"
345
+ ).map((dep) => `${dep.package}@${dep.version}`);
346
+ if (systemDeps.length > 0) {
347
+ await withSnapshotSpan(
348
+ "sandbox.snapshot.install_system",
349
+ "sandbox.snapshot.install.system",
350
+ {
351
+ "app.sandbox.snapshot.install.system_count": systemDeps.length
352
+ },
353
+ async () => {
354
+ for (const dep of systemDeps) {
355
+ if ("url" in dep) {
356
+ const rpmPath = runtimeDependencyFilePath(dep.url, dep.sha256);
357
+ await runOrThrow(
358
+ sandbox,
359
+ {
360
+ cmd: "curl",
361
+ args: ["-fsSL", dep.url, "-o", rpmPath]
362
+ },
363
+ `curl download ${dep.url}`
364
+ );
365
+ const checksumResult = await runNonInteractiveCommand(sandbox, {
366
+ cmd: "sha256sum",
367
+ args: [rpmPath]
368
+ });
369
+ const checksumStdout = (await checksumResult.stdout()).trim();
370
+ const checksumStderr = (await checksumResult.stderr()).trim();
371
+ if (checksumResult.exitCode !== 0) {
372
+ throw new Error(
373
+ `sha256sum failed: ${checksumStderr || checksumStdout || "command failed"}`
374
+ );
375
+ }
376
+ const actualChecksum = checksumStdout.split(/\s+/)[0]?.toLowerCase();
377
+ if (!actualChecksum) {
378
+ throw new Error("sha256sum produced empty output");
379
+ }
380
+ if (actualChecksum !== dep.sha256) {
381
+ throw new Error(
382
+ `checksum mismatch for ${dep.url}: expected ${dep.sha256}, got ${actualChecksum}`
383
+ );
384
+ }
385
+ await runOrThrow(
386
+ sandbox,
387
+ {
388
+ cmd: "dnf",
389
+ args: ["install", "-y", rpmPath],
390
+ sudo: true
391
+ },
392
+ `dnf install ${dep.url}`
393
+ );
394
+ continue;
395
+ }
396
+ if (dep.package === "gh") {
397
+ await installGhCliViaDnf(sandbox);
398
+ continue;
399
+ }
400
+ await runOrThrow(
401
+ sandbox,
402
+ {
403
+ cmd: "dnf",
404
+ args: ["install", "-y", dep.package],
405
+ sudo: true
406
+ },
407
+ `dnf install ${dep.package}`
408
+ );
409
+ }
410
+ }
411
+ );
412
+ }
413
+ if (npmPackages.length > 0) {
414
+ await withSnapshotSpan(
415
+ "sandbox.snapshot.install_npm",
416
+ "sandbox.snapshot.install.npm",
417
+ {
418
+ "app.sandbox.snapshot.install.npm_count": npmPackages.length
419
+ },
420
+ async () => {
421
+ await runOrThrow(
422
+ sandbox,
423
+ {
424
+ cmd: "npm",
425
+ args: [
426
+ "install",
427
+ "--global",
428
+ "--prefix",
429
+ `${SANDBOX_WORKSPACE_ROOT}/.junior`,
430
+ ...npmPackages
431
+ ]
432
+ },
433
+ "npm install"
434
+ );
435
+ }
436
+ );
437
+ }
438
+ }
439
+ async function runRuntimePostinstall(sandbox, commands) {
440
+ if (commands.length === 0) {
441
+ return;
442
+ }
443
+ await withSnapshotSpan(
444
+ "sandbox.snapshot.runtime_postinstall",
445
+ "sandbox.snapshot.runtime_postinstall",
446
+ {
447
+ "app.sandbox.snapshot.runtime_postinstall.count": commands.length
448
+ },
449
+ async () => {
450
+ for (const command of commands) {
451
+ const result = await runNonInteractiveCommand(sandbox, {
452
+ cmd: command.cmd,
453
+ args: command.args,
454
+ login: true,
455
+ pathPrefix: `${SANDBOX_WORKSPACE_ROOT}/.junior/bin:$PATH`,
456
+ ...command.sudo !== void 0 ? { sudo: command.sudo } : {}
457
+ });
458
+ if (result.exitCode === 0) {
459
+ continue;
460
+ }
461
+ const stderr = (await result.stderr()).trim();
462
+ const stdout = (await result.stdout()).trim();
463
+ const detail = stderr || stdout || "command failed";
464
+ throw new Error(`runtime-postinstall ${command.cmd} failed: ${detail}`);
465
+ }
466
+ }
467
+ );
468
+ }
469
+ async function createDependencySnapshot(profile, runtime, timeoutMs) {
470
+ return await withSnapshotSpan(
471
+ "sandbox.snapshot.build",
472
+ "sandbox.snapshot.build",
473
+ {
474
+ "app.sandbox.runtime": runtime,
475
+ "app.sandbox.snapshot.dependency_count": profile.dependencyCount
476
+ },
477
+ async () => {
478
+ const sandboxCredentials = getVercelSandboxCredentials();
479
+ const sandbox = createSandboxInstance(
480
+ await Sandbox.create({
481
+ timeout: timeoutMs,
482
+ runtime,
483
+ ...sandboxCredentials ?? {}
484
+ })
485
+ );
486
+ try {
487
+ await installRuntimeDependencies(sandbox, profile.dependencies);
488
+ await runRuntimePostinstall(sandbox, profile.postinstall);
489
+ return await withSnapshotSpan(
490
+ "sandbox.snapshot.capture",
491
+ "sandbox.snapshot.capture",
492
+ {
493
+ "app.sandbox.snapshot.dependency_count": profile.dependencyCount
494
+ },
495
+ async () => {
496
+ const snapshot = await sandbox.snapshot();
497
+ return snapshot.snapshotId;
498
+ }
499
+ );
500
+ } finally {
501
+ try {
502
+ await sandbox.stop();
503
+ } catch {
504
+ }
505
+ }
506
+ }
507
+ );
508
+ }
509
+ async function withBuildLock(profileHash, callback, canUseCachedSnapshot, hooks) {
510
+ const state = getStateAdapter();
511
+ await state.connect();
512
+ const lockKey = profileLockKey(profileHash);
513
+ const tryAcquireLock = async () => await state.acquireLock(lockKey, SNAPSHOT_BUILD_LOCK_TTL_MS);
514
+ let lock = await tryAcquireLock();
515
+ if (lock) {
516
+ try {
517
+ const result = await callback();
518
+ return {
519
+ snapshotId: result.snapshotId,
520
+ source: result.source,
521
+ waitedForLock: false
522
+ };
523
+ } finally {
524
+ await state.releaseLock(lock);
525
+ }
526
+ }
527
+ return await withSnapshotSpan(
528
+ "sandbox.snapshot.lock_wait",
529
+ "sandbox.snapshot.lock_wait",
530
+ {
531
+ "app.sandbox.snapshot.profile_hash": profileHash
532
+ },
533
+ async () => {
534
+ await hooks?.onWaitingForLock?.();
535
+ const waitUntil = Date.now() + SNAPSHOT_WAIT_FOR_LOCK_MS;
536
+ while (Date.now() < waitUntil) {
537
+ const cached2 = await getCachedSnapshot(profileHash);
538
+ if (cached2?.snapshotId && canUseCachedSnapshot(cached2)) {
539
+ return {
540
+ snapshotId: cached2.snapshotId,
541
+ source: "wait_cache",
542
+ waitedForLock: true
543
+ };
544
+ }
545
+ lock = await tryAcquireLock();
546
+ if (lock) {
547
+ try {
548
+ const result = await callback();
549
+ return {
550
+ snapshotId: result.snapshotId,
551
+ source: result.source,
552
+ waitedForLock: true
553
+ };
554
+ } finally {
555
+ await state.releaseLock(lock);
556
+ }
557
+ }
558
+ await sleep(500);
559
+ }
560
+ const cached = await getCachedSnapshot(profileHash);
561
+ if (cached?.snapshotId && canUseCachedSnapshot(cached)) {
562
+ return {
563
+ snapshotId: cached.snapshotId,
564
+ source: "wait_cache",
565
+ waitedForLock: true
566
+ };
567
+ }
568
+ throw new Error("Timed out waiting for snapshot build lock");
569
+ }
570
+ );
571
+ }
572
+ function toResolveOutcome(forceRebuild, source, waitedForLock) {
573
+ if (source === "built") {
574
+ return forceRebuild ? "forced_rebuild" : "rebuilt";
575
+ }
576
+ if (waitedForLock || source === "wait_cache") {
577
+ return "cache_hit_after_lock_wait";
578
+ }
579
+ return "cache_hit";
580
+ }
581
+ function getRebuildReason(params) {
582
+ if (params.forceRebuild) {
583
+ return params.staleSnapshotId ? "snapshot_missing" : "force_rebuild";
584
+ }
585
+ if (params.cached?.snapshotId && params.shouldRebuildCached) {
586
+ return "floating_stale";
587
+ }
588
+ if (!params.cached?.snapshotId) {
589
+ return "cache_miss";
590
+ }
591
+ return void 0;
592
+ }
593
+ async function resolveRuntimeDependencySnapshot(params) {
594
+ return await withSnapshotSpan(
595
+ "sandbox.snapshot.resolve",
596
+ "sandbox.snapshot.resolve",
597
+ {
598
+ "app.sandbox.runtime": params.runtime,
599
+ "app.sandbox.snapshot.force_rebuild": Boolean(params.forceRebuild)
600
+ },
601
+ async () => {
602
+ await params.onProgress?.("resolve_start");
603
+ const resolveStartedAtMs = Date.now();
604
+ const profile = buildDependencyProfile(params.runtime);
605
+ if (!profile) {
606
+ return {
607
+ dependencyCount: 0,
608
+ cacheHit: false,
609
+ resolveOutcome: "no_profile"
610
+ };
611
+ }
612
+ const cached = await getCachedSnapshot(profile.profileHash);
613
+ const cachedNeedsRebuild = Boolean(
614
+ cached?.snapshotId && shouldRebuildCachedSnapshot(profile, cached)
615
+ );
616
+ if (!params.forceRebuild && cached?.snapshotId && !cachedNeedsRebuild) {
617
+ await params.onProgress?.("cache_hit");
618
+ return {
619
+ snapshotId: cached.snapshotId,
620
+ profileHash: profile.profileHash,
621
+ dependencyCount: profile.dependencyCount,
622
+ cacheHit: true,
623
+ resolveOutcome: "cache_hit"
624
+ };
625
+ }
626
+ const rebuildReason = getRebuildReason({
627
+ forceRebuild: params.forceRebuild,
628
+ staleSnapshotId: params.staleSnapshotId,
629
+ cached,
630
+ shouldRebuildCached: cachedNeedsRebuild
631
+ });
632
+ const canUseCachedSnapshot = (candidate) => {
633
+ if (params.forceRebuild) {
634
+ if (params.staleSnapshotId) {
635
+ return candidate.snapshotId !== params.staleSnapshotId;
636
+ }
637
+ return candidate.createdAtMs > resolveStartedAtMs;
638
+ }
639
+ return !shouldRebuildCachedSnapshot(profile, candidate);
640
+ };
641
+ const lockResult = await withBuildLock(
642
+ profile.profileHash,
643
+ async () => {
644
+ const latest = await getCachedSnapshot(profile.profileHash);
645
+ if (latest?.snapshotId && canUseCachedSnapshot(latest)) {
646
+ await params.onProgress?.("cache_hit");
647
+ return {
648
+ snapshotId: latest.snapshotId,
649
+ source: "callback_cache"
650
+ };
651
+ }
652
+ await params.onProgress?.("building_snapshot");
653
+ const nextSnapshotId = await createDependencySnapshot(
654
+ profile,
655
+ params.runtime,
656
+ params.timeoutMs
657
+ );
658
+ await setCachedSnapshot({
659
+ profileHash: profile.profileHash,
660
+ snapshotId: nextSnapshotId,
661
+ runtime: params.runtime,
662
+ createdAtMs: Date.now(),
663
+ dependencyCount: profile.dependencyCount
664
+ });
665
+ await params.onProgress?.("build_complete");
666
+ return { snapshotId: nextSnapshotId, source: "built" };
667
+ },
668
+ canUseCachedSnapshot,
669
+ {
670
+ onWaitingForLock: async () => {
671
+ await params.onProgress?.("waiting_for_lock");
672
+ }
673
+ }
674
+ );
675
+ return {
676
+ snapshotId: lockResult.snapshotId,
677
+ profileHash: profile.profileHash,
678
+ dependencyCount: profile.dependencyCount,
679
+ cacheHit: lockResult.source !== "built",
680
+ resolveOutcome: toResolveOutcome(
681
+ Boolean(params.forceRebuild),
682
+ lockResult.source,
683
+ lockResult.waitedForLock
684
+ ),
685
+ ...rebuildReason ? { rebuildReason } : {}
686
+ };
687
+ }
688
+ );
689
+ }
690
+ function isSnapshotMissingError(error) {
691
+ const searchable = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
692
+ return searchable.includes("snapshot") && (searchable.includes("not found") || searchable.includes("unknown") || searchable.includes("404"));
693
+ }
694
+
695
+ export {
696
+ SANDBOX_WORKSPACE_ROOT,
697
+ SANDBOX_SKILLS_ROOT,
698
+ SANDBOX_DATA_ROOT,
699
+ sandboxSkillDir,
700
+ sandboxSkillFile,
701
+ buildNonInteractiveShellScript,
702
+ runNonInteractiveCommand,
703
+ getVercelSandboxCredentials,
704
+ createSandboxInstance,
705
+ getRuntimeDependencyProfileHash,
706
+ resolveRuntimeDependencySnapshot,
707
+ isSnapshotMissingError
708
+ };